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
import mimetypes
15
16
import config
17
import markdown
18
19
20
app = flask.Flask(__name__)
21
bcrypt = Bcrypt(app)
22
23
24
app.config["SQLALCHEMY_DATABASE_URI"] = config.DB_URI
25
app.config["SECRET_KEY"] = config.DB_PASSWORD
26
27
28
db = SQLAlchemy(app)
29
migrate = Migrate(app, db)
30
31
32
@app.template_filter("split")
33
def split(value, separator=None, maxsplit=-1):
34
return value.split(separator, maxsplit)
35
36
37
38
with app.app_context():
39
class User(db.Model):
40
username = db.Column(db.String(32), unique=True, nullable=False, primary_key=True)
41
password_hashed = db.Column(db.String(60), nullable=False)
42
admin = db.Column(db.Boolean, nullable=False, default=False, server_default="false")
43
pictures = db.relationship("PictureResource", back_populates="author")
44
45
def __init__(self, username, password):
46
self.username = username
47
self.password_hashed = bcrypt.generate_password_hash(password).decode("utf-8")
48
49
50
class Licence(db.Model):
51
id = db.Column(db.String(64), primary_key=True) # SPDX identifier
52
title = db.Column(db.UnicodeText, nullable=False) # the official name of the licence
53
description = db.Column(db.UnicodeText, nullable=False) # brief description of its permissions and restrictions
54
legal_text = db.Column(db.UnicodeText, nullable=False) # the full legal text of the licence
55
url = db.Column(db.String(2048), nullable=True) # the URL to a page with the full text of the licence and more information
56
pictures = db.relationship("PictureLicence", back_populates="licence")
57
free = db.Column(db.Boolean, nullable=False, default=False) # whether the licence is free or not
58
59
def __init__(self, id, title, description, legal_text, url, free):
60
self.id = id
61
self.title = title
62
self.description = description
63
self.legal_text = legal_text
64
self.url = url
65
self.free = free
66
67
68
class PictureLicence(db.Model):
69
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
70
71
resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"))
72
licence_id = db.Column(db.String(32), db.ForeignKey("licence.id"))
73
74
resource = db.relationship("PictureResource", back_populates="licences")
75
licence = db.relationship("Licence", back_populates="pictures")
76
77
def __init__(self, resource_id, licence_id):
78
self.resource_id = resource_id
79
self.licence_id = licence_id
80
81
82
class Resource(db.Model):
83
__abstract__ = True
84
85
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
86
title = db.Column(db.UnicodeText, nullable=False)
87
description = db.Column(db.UnicodeText, nullable=False)
88
timestamp = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
89
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
90
91
92
class PictureNature(db.Model):
93
# Examples:
94
# "photo", "paper-scan", "2d-art-photo", "sculpture-photo", "computer-3d", "computer-painting",
95
# "computer-line-art", "diagram", "infographic", "text", "map", "chart-graph", "screen-capture",
96
# "screen-photo", "pattern", "collage", "ai", and so on
97
id = db.Column(db.String(64), primary_key=True)
98
description = db.Column(db.UnicodeText, nullable=False)
99
resources = db.relationship("PictureResource", back_populates="nature")
100
101
def __init__(self, id, description):
102
self.id = id
103
self.description = description
104
105
106
class PictureObjectInheritance(db.Model):
107
parent_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"),
108
primary_key=True)
109
child_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"),
110
primary_key=True)
111
112
parent = db.relationship("PictureObject", foreign_keys=[parent_id],
113
back_populates="child_links")
114
child = db.relationship("PictureObject", foreign_keys=[child_id],
115
back_populates="parent_links")
116
117
def __init__(self, parent, child):
118
self.parent = parent
119
self.child = child
120
121
122
class PictureObject(db.Model):
123
id = db.Column(db.String(64), primary_key=True)
124
description = db.Column(db.UnicodeText, nullable=False)
125
126
child_links = db.relationship("PictureObjectInheritance",
127
foreign_keys=[PictureObjectInheritance.parent_id],
128
back_populates="parent")
129
parent_links = db.relationship("PictureObjectInheritance",
130
foreign_keys=[PictureObjectInheritance.child_id],
131
back_populates="child")
132
133
def __init__(self, id, description):
134
self.id = id
135
self.description = description
136
137
138
class PictureRegion(db.Model):
139
# This is for picture region annotations
140
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
141
json = db.Column(sqlalchemy.dialects.postgresql.JSONB, nullable=False)
142
143
resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), nullable=False)
144
object_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"), nullable=True)
145
146
resource = db.relationship("PictureResource", backref="regions")
147
object = db.relationship("PictureObject", backref="regions")
148
149
def __init__(self, json, resource, object):
150
self.json = json
151
self.resource = resource
152
self.object = object
153
154
155
class PictureResource(Resource):
156
# This is only for bitmap pictures. Vectors will be stored under a different model
157
# File name is the ID in the picture directory under data, without an extension
158
file_format = db.Column(db.String(64), nullable=False) # MIME type
159
width = db.Column(db.Integer, nullable=False)
160
height = db.Column(db.Integer, nullable=False)
161
nature_id = db.Column(db.String(32), db.ForeignKey("picture_nature.id"), nullable=True)
162
author_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
163
author = db.relationship("User", back_populates="pictures")
164
165
nature = db.relationship("PictureNature", back_populates="resources")
166
167
replaces_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), nullable=True)
168
replaced_by_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"),
169
nullable=True)
170
171
replaces = db.relationship("PictureResource", remote_side="PictureResource.id",
172
foreign_keys=[replaces_id], back_populates="replaced_by")
173
replaced_by = db.relationship("PictureResource", remote_side="PictureResource.id",
174
foreign_keys=[replaced_by_id])
175
176
licences = db.relationship("PictureLicence", back_populates="resource")
177
178
def __init__(self, title, author, description, origin_url, licence_ids, mime, nature=None,
179
replaces=None):
180
self.title = title
181
self.author = author
182
self.description = description
183
self.origin_url = origin_url
184
self.file_format = mime
185
self.width = self.height = 0
186
self.nature = nature
187
db.session.add(self)
188
db.session.commit()
189
for licence_id in licence_ids:
190
joiner = PictureLicence(self.id, licence_id)
191
db.session.add(joiner)
192
if replaces is not None:
193
self.replaces = replaces
194
replaces.replaced_by = self
195
196
197
@app.route("/")
198
def index():
199
return flask.render_template("home.html")
200
201
202
@app.route("/accounts/")
203
def accounts():
204
return flask.render_template("login.html")
205
206
207
@app.route("/login", methods=["POST"])
208
def login():
209
username = flask.request.form["username"]
210
password = flask.request.form["password"]
211
212
user = db.session.get(User, username)
213
214
if user is None:
215
flask.flash("This username is not registered.")
216
return flask.redirect("/accounts")
217
218
if not bcrypt.check_password_hash(user.password_hashed, password):
219
flask.flash("Incorrect password.")
220
return flask.redirect("/accounts")
221
222
flask.flash("You have been logged in.")
223
224
flask.session["username"] = username
225
return flask.redirect("/")
226
227
228
@app.route("/logout")
229
def logout():
230
flask.session.pop("username", None)
231
flask.flash("You have been logged out.")
232
return flask.redirect("/")
233
234
235
@app.route("/signup", methods=["POST"])
236
def signup():
237
username = flask.request.form["username"]
238
password = flask.request.form["password"]
239
240
if db.session.get(User, username) is not None:
241
flask.flash("This username is already taken.")
242
return flask.redirect("/accounts")
243
244
if set(username) > set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"):
245
flask.flash("Usernames can only contain the Latin alphabet, digits, hyphens, and underscores.")
246
return flask.redirect("/accounts")
247
248
if len(username) < 3 or len(username) > 32:
249
flask.flash("Usernames must be between 3 and 32 characters long.")
250
return flask.redirect("/accounts")
251
252
if len(password) < 6:
253
flask.flash("Passwords must be at least 6 characters long.")
254
return flask.redirect("/accounts")
255
256
user = User(username, password)
257
db.session.add(user)
258
db.session.commit()
259
260
flask.session["username"] = username
261
262
flask.flash("You have been registered and logged in.")
263
264
return flask.redirect("/")
265
266
267
@app.route("/profile", defaults={"username": None})
268
@app.route("/profile/<username>")
269
def profile(username):
270
if username is None:
271
if "username" in flask.session:
272
return flask.redirect("/profile/" + flask.session["username"])
273
else:
274
flask.flash("Please log in to perform this action.")
275
return flask.redirect("/accounts")
276
277
user = db.session.get(User, username)
278
if user is None:
279
return flask.abort(404)
280
281
return flask.render_template("profile.html", user=user)
282
283
284
@app.route("/upload")
285
def upload():
286
return flask.render_template("upload.html")
287
288
289
@app.route("/upload", methods=["POST"])
290
def upload_post():
291
title = flask.request.form["title"]
292
description = flask.request.form["description"]
293
origin_url = flask.request.form["origin_url"]
294
author = db.session.get(User, flask.session.get("username"))
295
296
file = flask.request.files["file"]
297
298
if not file or not file.filename:
299
flask.flash("No selected file")
300
return flask.redirect(flask.request.url)
301
302
resource = PictureResource(title, author, description, origin_url, ["CC0-1.0"], file.mimetype)
303
db.session.add(resource)
304
db.session.commit()
305
file.save(path.join(config.DATA_PATH, "pictures", str(resource.id)))
306
307
return flask.redirect("/picture/" + str(resource.id))
308
309
310
@app.route("/picture/<int:id>/")
311
def picture(id):
312
resource = db.session.get(PictureResource, id)
313
if resource is None:
314
return flask.abort(404)
315
316
return flask.render_template("picture.html", resource=resource,
317
file_extension=mimetypes.guess_extension(resource.file_format))
318
319
320
321
@app.route("/picture/<int:id>/annotate")
322
def annotate_picture(id):
323
resource = db.session.get(PictureResource, id)
324
current_user = db.session.get(User, flask.session.get("username"))
325
if resource.author != current_user and not current_user.admin:
326
return flask.abort(403)
327
328
if resource is None:
329
return flask.abort(404)
330
331
return flask.render_template("picture-annotation.html", resource=resource,
332
file_extension=mimetypes.guess_extension(resource.file_format))
333
334
335
@app.route("/picture/<int:id>/save-annotations", methods=["POST"])
336
def save_annotations(id):
337
resource = db.session.get(PictureResource, id)
338
if resource is None:
339
return flask.abort(404)
340
341
current_user = db.session.get(User, flask.session.get("username"))
342
if resource.author != current_user and not current_user.admin:
343
return flask.abort(403)
344
345
# Delete all previous annotations
346
db.session.query(PictureRegion).filter_by(resource_id=id).delete()
347
348
json = flask.request.json
349
for region in json:
350
object_id = region["object"]
351
picture_object = db.session.get(PictureObject, object_id)
352
353
region_data = {
354
"type": region["type"],
355
"shape": region["shape"],
356
}
357
358
region_row = PictureRegion(region_data, resource, picture_object)
359
db.session.add(region_row)
360
361
362
db.session.commit()
363
364
response = flask.make_response()
365
response.status_code = 204
366
return response
367
368
369
@app.route("/picture/<int:id>/get-annotations")
370
def get_annotations(id):
371
resource = db.session.get(PictureResource, id)
372
if resource is None:
373
return flask.abort(404)
374
375
regions = db.session.query(PictureRegion).filter_by(resource_id=id).all()
376
377
regions_json = []
378
379
for region in regions:
380
regions_json.append({
381
"object": region.object_id,
382
"type": region.json["type"],
383
"shape": region.json["shape"],
384
})
385
386
return flask.jsonify(regions_json)
387
388
389
@app.route("/raw/picture/<int:id>")
390
def raw_picture(id):
391
resource = db.session.get(PictureResource, id)
392
if resource is None:
393
return flask.abort(404)
394
395
response = flask.send_from_directory(path.join(config.DATA_PATH, "pictures"), str(resource.id))
396
response.mimetype = resource.file_format
397
398
return response
399
400
401
@app.route("/api/object-types")
402
def object_types():
403
objects = db.session.query(PictureObject).all()
404
return flask.jsonify({object.id: object.description for object in objects})
405