app.py
Python script, ASCII text executable
1
from datetime import datetime
2
from email.policy import default
3
4
import flask
5
from flask_sqlalchemy import SQLAlchemy
6
from flask_bcrypt import Bcrypt
7
from flask_httpauth import HTTPBasicAuth
8
from markupsafe import escape, Markup
9
from flask_migrate import Migrate
10
from jinja2_fragments.flask import render_block
11
from sqlalchemy.orm import backref
12
import sqlalchemy.dialects.postgresql
13
from os import path
14
15
import config
16
import markdown
17
18
19
app = flask.Flask(__name__)
20
bcrypt = Bcrypt(app)
21
22
23
app.config["SQLALCHEMY_DATABASE_URI"] = config.DB_URI
24
app.config["SECRET_KEY"] = config.DB_PASSWORD
25
26
27
db = SQLAlchemy(app)
28
migrate = Migrate(app, db)
29
30
31
@app.template_filter("split")
32
def split(value, separator=None, maxsplit=-1):
33
return value.split(separator, maxsplit)
34
35
36
37
with app.app_context():
38
class User(db.Model):
39
username = db.Column(db.String(32), unique=True, nullable=False, primary_key=True)
40
password_hashed = db.Column(db.String(60), nullable=False)
41
admin = db.Column(db.Boolean, nullable=False, default=False, server_default="false")
42
43
def __init__(self, username, password):
44
self.username = username
45
self.password_hashed = bcrypt.generate_password_hash(password).decode("utf-8")
46
47
48
class Licence(db.Model):
49
id = db.Column(db.String(32), primary_key=True) # SPDX identifier
50
title = db.Column(db.UnicodeText, nullable=False) # the official name of the licence
51
description = db.Column(db.UnicodeText, nullable=False) # brief description of its permissions and restrictions
52
legal_text = db.Column(db.UnicodeText, nullable=False) # the full legal text of the licence
53
url = db.Column(db.String(2048), nullable=True) # the URL to a page with the full text of the licence and more information
54
pictures = db.relationship("PictureLicence", back_populates="licence")
55
56
def __init__(self, id, title, description, legal_text, url):
57
self.id = id
58
self.title = title
59
self.description = description
60
self.legal_text = legal_text
61
self.url = url
62
63
64
class PictureLicence(db.Model):
65
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
66
67
resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"))
68
licence_id = db.Column(db.String(32), db.ForeignKey("licence.id"))
69
70
resource = db.relationship("PictureResource", back_populates="licences")
71
licence = db.relationship("Licence", back_populates="pictures")
72
73
def __init__(self, resource_id, licence_id):
74
self.resource_id = resource_id
75
self.licence_id = licence_id
76
77
78
class Resource(db.Model):
79
__abstract__ = True
80
81
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
82
title = db.Column(db.UnicodeText, nullable=False)
83
description = db.Column(db.UnicodeText, nullable=False)
84
timestamp = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
85
origin_url = db.Column(db.String(2048), nullable=True) # should be left empty if it's original or the source is unknown but public domain
86
87
88
class PictureNature(db.Model):
89
# Examples:
90
# "photo", "paper-scan", "2d-art-photo", "sculpture-photo", "computer-3d", "computer-painting",
91
# "computer-line-art", "diagram", "infographic", "text", "map", "chart-graph", "screen-capture",
92
# "screen-photo", "pattern", "collage", "ai", and so on
93
id = db.Column(db.String(64), primary_key=True)
94
description = db.Column(db.UnicodeText, nullable=False)
95
resources = db.relationship("PictureResource", back_populates="nature")
96
97
def __init__(self, id, description):
98
self.id = id
99
self.description = description
100
101
102
class PictureObjectInheritance(db.Model):
103
parent_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"),
104
primary_key=True)
105
child_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"),
106
primary_key=True)
107
108
parent = db.relationship("PictureObject", foreign_keys=[parent_id],
109
back_populates="child_links")
110
child = db.relationship("PictureObject", foreign_keys=[child_id],
111
back_populates="parent_links")
112
113
def __init__(self, parent, child):
114
self.parent = parent
115
self.child = child
116
117
118
class PictureObject(db.Model):
119
id = db.Column(db.String(64), primary_key=True)
120
description = db.Column(db.UnicodeText, nullable=False)
121
122
child_links = db.relationship("PictureObjectInheritance",
123
foreign_keys=[PictureObjectInheritance.parent_id],
124
back_populates="parent")
125
parent_links = db.relationship("PictureObjectInheritance",
126
foreign_keys=[PictureObjectInheritance.child_id],
127
back_populates="child")
128
129
def __init__(self, id, description):
130
self.id = id
131
self.description = description
132
133
134
class PictureRegion(db.Model):
135
# This is for picture region annotations
136
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
137
json = db.Column(sqlalchemy.dialects.postgresql.JSONB, nullable=False)
138
139
resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), nullable=False)
140
object_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"), nullable=False)
141
142
resource = db.relationship("PictureResource", backref="regions")
143
object = db.relationship("PictureObject", backref="regions")
144
145
def __init__(self, json, resource, object):
146
self.json = json
147
self.resource = resource
148
self.object = object
149
150
151
class PictureResource(Resource):
152
# This is only for bitmap pictures. Vectors will be stored under a different model
153
# File name is the ID in the picture directory under data, without an extension
154
file_format = db.Column(db.String(64), nullable=False) # MIME type
155
width = db.Column(db.Integer, nullable=False)
156
height = db.Column(db.Integer, nullable=False)
157
nature_id = db.Column(db.String(32), db.ForeignKey("picture_nature.id"), nullable=True)
158
159
nature = db.relationship("PictureNature", back_populates="resources")
160
161
replaces_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), nullable=True)
162
replaced_by_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"),
163
nullable=True)
164
165
replaces = db.relationship("PictureResource", remote_side="PictureResource.id",
166
foreign_keys=[replaces_id], back_populates="replaced_by")
167
replaced_by = db.relationship("PictureResource", remote_side="PictureResource.id",
168
foreign_keys=[replaced_by_id])
169
170
licences = db.relationship("PictureLicence", back_populates="resource")
171
172
def __init__(self, title, description, origin_url, licence_ids, mime, nature=None,
173
replaces=None):
174
self.title = title
175
self.description = description
176
self.origin_url = origin_url
177
self.file_format = mime
178
self.width = self.height = 0
179
self.nature = nature
180
db.session.add(self)
181
db.session.commit()
182
for licence_id in licence_ids:
183
joiner = PictureLicence(self.id, licence_id)
184
db.session.add(joiner)
185
if replaces is not None:
186
self.replaces = replaces
187
replaces.replaced_by = self
188
189
190
@app.route("/")
191
def index():
192
return flask.render_template("home.html")
193
194
195
@app.route("/accounts")
196
def accounts():
197
return flask.render_template("login.html")
198
199
200
@app.route("/login", methods=["POST"])
201
def login():
202
username = flask.request.form["username"]
203
password = flask.request.form["password"]
204
205
user = db.session.get(User, username)
206
207
if user is None:
208
flask.flash("This username is not registered.")
209
return flask.redirect("/accounts")
210
211
if not bcrypt.check_password_hash(user.password_hashed, password):
212
flask.flash("Incorrect password.")
213
return flask.redirect("/accounts")
214
215
flask.flash("You have been logged in.")
216
217
flask.session["username"] = username
218
return flask.redirect("/")
219
220
221
@app.route("/logout")
222
def logout():
223
flask.session.pop("username", None)
224
flask.flash("You have been logged out.")
225
return flask.redirect("/")
226
227
228
@app.route("/signup", methods=["POST"])
229
def signup():
230
username = flask.request.form["username"]
231
password = flask.request.form["password"]
232
233
if db.session.get(User, username) is not None:
234
flask.flash("This username is already taken.")
235
return flask.redirect("/accounts")
236
237
if set(username) > set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"):
238
flask.flash("Usernames can only contain the Latin alphabet, digits, hyphens, and underscores.")
239
return flask.redirect("/accounts")
240
241
if len(username) < 3 or len(username) > 32:
242
flask.flash("Usernames must be between 3 and 32 characters long.")
243
return flask.redirect("/accounts")
244
245
if len(password) < 6:
246
flask.flash("Passwords must be at least 6 characters long.")
247
return flask.redirect("/accounts")
248
249
user = User(username, None, password)
250
db.session.add(user)
251
db.session.commit()
252
253
flask.session["username"] = username
254
255
flask.flash("You have been registered and logged in.")
256
257
return flask.redirect("/")
258
259
260
@app.route("/profile", defaults={"username": None})
261
@app.route("/profile/<username>")
262
def profile(username):
263
if username is None:
264
if "username" in flask.session:
265
return flask.redirect("/profile/" + flask.session["username"])
266
else:
267
flask.flash("Please log in to perform this action.")
268
return flask.redirect("/accounts")
269
270
user = db.session.get(User, username)
271
if user is None:
272
return flask.abort(404)
273
274
return flask.render_template("profile.html", user=user)
275
276
277
@app.route("/upload")
278
def upload():
279
return flask.render_template("upload.html")
280
281
282
@app.route("/upload", methods=["POST"])
283
def upload_post():
284
title = flask.request.form["title"]
285
description = flask.request.form["description"]
286
origin_url = flask.request.form["origin_url"]
287
288
file = flask.request.files["file"]
289
290
if not file or not file.filename:
291
flask.flash("No selected file")
292
return flask.redirect(flask.request.url)
293
294
resource = PictureResource(title, description, origin_url, ["CC0-1.0"], file.mimetype)
295
db.session.add(resource)
296
db.session.commit()
297
file.save(path.join(config.DATA_PATH, "pictures", str(resource.id)))
298
299
return flask.redirect("/picture/" + str(resource.id))
300
301
302
@app.route("/picture/<int:id>")
303
def picture(id):
304
resource = db.session.get(PictureResource, id)
305
if resource is None:
306
return flask.abort(404)
307
308
return flask.render_template("picture.html", resource=resource)
309
310
311
@app.route("/raw/picture/<int:id>")
312
def raw_picture(id):
313
resource = db.session.get(PictureResource, id)
314
if resource is None:
315
return flask.abort(404)
316
317
response = flask.send_from_directory(path.join(config.DATA_PATH, "pictures"), str(resource.id))
318
response.mimetype = resource.file_format
319
320
return response
321