Web platform for sharing free data for ML and research

By using this site, you agree to have cookies stored on your device, strictly for functional purposes, such as storing your session and preferences.

Dismiss

 app.py

View raw Download
text/x-script.python • 17.69 kiB
Python script, ASCII text executable
        
            
1
import json
2
from datetime import datetime
3
from email.policy import default
4
5
import flask
6
from flask_sqlalchemy import SQLAlchemy
7
from flask_bcrypt import Bcrypt
8
from flask_httpauth import HTTPBasicAuth
9
from markupsafe import escape, Markup
10
from flask_migrate import Migrate, current
11
from jinja2_fragments.flask import render_block
12
from sqlalchemy.orm import backref
13
import sqlalchemy.dialects.postgresql
14
from os import path
15
import mimetypes
16
17
from PIL import Image
18
19
import config
20
import markdown
21
22
23
app = flask.Flask(__name__)
24
bcrypt = Bcrypt(app)
25
26
27
app.config["SQLALCHEMY_DATABASE_URI"] = config.DB_URI
28
app.config["SECRET_KEY"] = config.DB_PASSWORD
29
30
31
db = SQLAlchemy(app)
32
migrate = Migrate(app, db)
33
34
35
@app.template_filter("split")
36
def split(value, separator=None, maxsplit=-1):
37
return value.split(separator, maxsplit)
38
39
40
@app.template_filter("median")
41
def median(value):
42
value = list(value) # prevent generators
43
return sorted(value)[len(value) // 2]
44
45
46
@app.template_filter("set")
47
def set_filter(value):
48
return set(value)
49
50
51
with app.app_context():
52
class User(db.Model):
53
username = db.Column(db.String(32), unique=True, nullable=False, primary_key=True)
54
password_hashed = db.Column(db.String(60), nullable=False)
55
admin = db.Column(db.Boolean, nullable=False, default=False, server_default="false")
56
pictures = db.relationship("PictureResource", back_populates="author")
57
58
def __init__(self, username, password):
59
self.username = username
60
self.password_hashed = bcrypt.generate_password_hash(password).decode("utf-8")
61
62
63
class Licence(db.Model):
64
id = db.Column(db.String(64), primary_key=True) # SPDX identifier
65
title = db.Column(db.UnicodeText, nullable=False) # the official name of the licence
66
description = db.Column(db.UnicodeText, nullable=False) # brief description of its permissions and restrictions
67
info_url = db.Column(db.String(1024), nullable=False) # the URL to a page with general information about the licence
68
url = db.Column(db.String(1024), nullable=True) # the URL to a page with the full text of the licence and more information
69
pictures = db.relationship("PictureLicence", back_populates="licence")
70
free = db.Column(db.Boolean, nullable=False, default=False) # whether the licence is free or not
71
logo_url = db.Column(db.String(1024), nullable=True) # URL to the logo of the licence
72
pinned = db.Column(db.Boolean, nullable=False, default=False) # whether the licence should be shown at the top of the list
73
74
def __init__(self, id, title, description, info_url, url, free, logo_url=None, pinned=False):
75
self.id = id
76
self.title = title
77
self.description = description
78
self.info_url = info_url
79
self.url = url
80
self.free = free
81
self.logo_url = logo_url
82
self.pinned = pinned
83
84
85
class PictureLicence(db.Model):
86
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
87
88
resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"))
89
licence_id = db.Column(db.String(64), db.ForeignKey("licence.id"))
90
91
resource = db.relationship("PictureResource", back_populates="licences")
92
licence = db.relationship("Licence", back_populates="pictures")
93
94
def __init__(self, resource_id, licence_id):
95
self.resource_id = resource_id
96
self.licence_id = licence_id
97
98
99
class Resource(db.Model):
100
__abstract__ = True
101
102
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
103
title = db.Column(db.UnicodeText, nullable=False)
104
description = db.Column(db.UnicodeText, nullable=False)
105
timestamp = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
106
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
107
108
109
class PictureNature(db.Model):
110
# Examples:
111
# "photo", "paper-scan", "2d-art-photo", "sculpture-photo", "computer-3d", "computer-painting",
112
# "computer-line-art", "diagram", "infographic", "text", "map", "chart-graph", "screen-capture",
113
# "screen-photo", "pattern", "collage", "ai", and so on
114
id = db.Column(db.String(64), primary_key=True)
115
description = db.Column(db.UnicodeText, nullable=False)
116
resources = db.relationship("PictureResource", back_populates="nature")
117
118
def __init__(self, id, description):
119
self.id = id
120
self.description = description
121
122
123
class PictureObjectInheritance(db.Model):
124
parent_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"),
125
primary_key=True)
126
child_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"),
127
primary_key=True)
128
129
parent = db.relationship("PictureObject", foreign_keys=[parent_id],
130
back_populates="child_links")
131
child = db.relationship("PictureObject", foreign_keys=[child_id],
132
back_populates="parent_links")
133
134
def __init__(self, parent, child):
135
self.parent = parent
136
self.child = child
137
138
139
class PictureObject(db.Model):
140
id = db.Column(db.String(64), primary_key=True)
141
description = db.Column(db.UnicodeText, nullable=False)
142
143
child_links = db.relationship("PictureObjectInheritance",
144
foreign_keys=[PictureObjectInheritance.parent_id],
145
back_populates="parent")
146
parent_links = db.relationship("PictureObjectInheritance",
147
foreign_keys=[PictureObjectInheritance.child_id],
148
back_populates="child")
149
150
def __init__(self, id, description):
151
self.id = id
152
self.description = description
153
154
155
class PictureRegion(db.Model):
156
# This is for picture region annotations
157
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
158
json = db.Column(sqlalchemy.dialects.postgresql.JSONB, nullable=False)
159
160
resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), nullable=False)
161
object_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"), nullable=True)
162
163
resource = db.relationship("PictureResource", backref="regions")
164
object = db.relationship("PictureObject", backref="regions")
165
166
def __init__(self, json, resource, object):
167
self.json = json
168
self.resource = resource
169
self.object = object
170
171
172
class PictureResource(Resource):
173
# This is only for bitmap pictures. Vectors will be stored under a different model
174
# File name is the ID in the picture directory under data, without an extension
175
file_format = db.Column(db.String(64), nullable=False) # MIME type
176
width = db.Column(db.Integer, nullable=False)
177
height = db.Column(db.Integer, nullable=False)
178
nature_id = db.Column(db.String(32), db.ForeignKey("picture_nature.id"), nullable=True)
179
author_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
180
author = db.relationship("User", back_populates="pictures")
181
182
nature = db.relationship("PictureNature", back_populates="resources")
183
184
replaces_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), nullable=True)
185
replaced_by_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"),
186
nullable=True)
187
188
replaces = db.relationship("PictureResource", remote_side="PictureResource.id",
189
foreign_keys=[replaces_id], back_populates="replaced_by")
190
replaced_by = db.relationship("PictureResource", remote_side="PictureResource.id",
191
foreign_keys=[replaced_by_id])
192
193
licences = db.relationship("PictureLicence", back_populates="resource")
194
195
def __init__(self, title, author, description, origin_url, licence_ids, mime, nature=None,
196
replaces=None):
197
self.title = title
198
self.author = author
199
self.description = description
200
self.origin_url = origin_url
201
self.file_format = mime
202
self.width = self.height = 0
203
self.nature = nature
204
db.session.add(self)
205
db.session.commit()
206
for licence_id in licence_ids:
207
joiner = PictureLicence(self.id, licence_id)
208
db.session.add(joiner)
209
if replaces is not None:
210
self.replaces = replaces
211
replaces.replaced_by = self
212
213
def put_annotations(self, json):
214
# Delete all previous annotations
215
db.session.query(PictureRegion).filter_by(resource_id=self.id).delete()
216
217
for region in json:
218
object_id = region["object"]
219
picture_object = db.session.get(PictureObject, object_id)
220
221
region_data = {
222
"type": region["type"],
223
"shape": region["shape"],
224
}
225
226
region_row = PictureRegion(region_data, self, picture_object)
227
db.session.add(region_row)
228
229
230
@app.route("/")
231
def index():
232
return flask.render_template("home.html")
233
234
235
@app.route("/accounts/")
236
def accounts():
237
return flask.render_template("login.html")
238
239
240
@app.route("/login", methods=["POST"])
241
def login():
242
username = flask.request.form["username"]
243
password = flask.request.form["password"]
244
245
user = db.session.get(User, username)
246
247
if user is None:
248
flask.flash("This username is not registered.")
249
return flask.redirect("/accounts")
250
251
if not bcrypt.check_password_hash(user.password_hashed, password):
252
flask.flash("Incorrect password.")
253
return flask.redirect("/accounts")
254
255
flask.flash("You have been logged in.")
256
257
flask.session["username"] = username
258
return flask.redirect("/")
259
260
261
@app.route("/logout")
262
def logout():
263
flask.session.pop("username", None)
264
flask.flash("You have been logged out.")
265
return flask.redirect("/")
266
267
268
@app.route("/signup", methods=["POST"])
269
def signup():
270
username = flask.request.form["username"]
271
password = flask.request.form["password"]
272
273
if db.session.get(User, username) is not None:
274
flask.flash("This username is already taken.")
275
return flask.redirect("/accounts")
276
277
if set(username) > set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"):
278
flask.flash("Usernames can only contain the Latin alphabet, digits, hyphens, and underscores.")
279
return flask.redirect("/accounts")
280
281
if len(username) < 3 or len(username) > 32:
282
flask.flash("Usernames must be between 3 and 32 characters long.")
283
return flask.redirect("/accounts")
284
285
if len(password) < 6:
286
flask.flash("Passwords must be at least 6 characters long.")
287
return flask.redirect("/accounts")
288
289
user = User(username, password)
290
db.session.add(user)
291
db.session.commit()
292
293
flask.session["username"] = username
294
295
flask.flash("You have been registered and logged in.")
296
297
return flask.redirect("/")
298
299
300
@app.route("/profile", defaults={"username": None})
301
@app.route("/profile/<username>")
302
def profile(username):
303
if username is None:
304
if "username" in flask.session:
305
return flask.redirect("/profile/" + flask.session["username"])
306
else:
307
flask.flash("Please log in to perform this action.")
308
return flask.redirect("/accounts")
309
310
user = db.session.get(User, username)
311
if user is None:
312
flask.abort(404)
313
314
return flask.render_template("profile.html", user=user)
315
316
317
@app.route("/upload")
318
def upload():
319
if "username" not in flask.session:
320
flask.flash("Log in to upload pictures.")
321
return flask.redirect("/accounts")
322
323
licences = Licence.query.order_by(Licence.free.desc(), Licence.pinned.desc(), Licence.title).all()
324
325
types = PictureNature.query.all()
326
327
return flask.render_template("upload.html", licences=licences, types=types)
328
329
330
@app.route("/upload", methods=["POST"])
331
def upload_post():
332
title = flask.request.form["title"]
333
description = flask.request.form["description"]
334
origin_url = flask.request.form["origin_url"]
335
author = db.session.get(User, flask.session.get("username"))
336
licence_ids = flask.request.form.getlist("licence")
337
nature_id = flask.request.form["nature"]
338
339
if author is None:
340
flask.abort(401)
341
342
file = flask.request.files["file"]
343
344
if not file or not file.filename:
345
flask.flash("Select a file")
346
return flask.redirect(flask.request.url)
347
348
if not file.mimetype.startswith("image/"):
349
flask.flash("Only images are supported")
350
return flask.redirect(flask.request.url)
351
352
if not title:
353
flask.flash("Enter a title")
354
return flask.redirect(flask.request.url)
355
356
if not description:
357
description = ""
358
359
if not nature_id:
360
flask.flash("Select a picture type")
361
return flask.redirect(flask.request.url)
362
363
if not licence_ids:
364
flask.flash("Select licences")
365
return flask.redirect(flask.request.url)
366
367
licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]
368
if not any(licence.free for licence in licences):
369
flask.flash("Select at least one free licence")
370
return flask.redirect(flask.request.url)
371
372
resource = PictureResource(title, author, description, origin_url, licence_ids, file.mimetype,
373
db.session.get(PictureNature, nature_id))
374
db.session.add(resource)
375
db.session.commit()
376
file.save(path.join(config.DATA_PATH, "pictures", str(resource.id)))
377
378
if flask.request.form.get("annotations"):
379
try:
380
resource.put_annotations(json.loads(flask.request.form.get("annotations")))
381
db.session.commit()
382
except json.JSONDecodeError:
383
flask.flash("Invalid annotations")
384
385
flask.flash("Picture uploaded successfully")
386
387
return flask.redirect("/picture/" + str(resource.id))
388
389
390
@app.route("/picture/<int:id>/")
391
def picture(id):
392
resource = db.session.get(PictureResource, id)
393
if resource is None:
394
flask.abort(404)
395
396
image = Image.open(path.join(config.DATA_PATH, "pictures", str(resource.id)))
397
398
return flask.render_template("picture.html", resource=resource,
399
file_extension=mimetypes.guess_extension(resource.file_format),
400
size=image.size)
401
402
403
404
@app.route("/picture/<int:id>/annotate")
405
def annotate_picture(id):
406
resource = db.session.get(PictureResource, id)
407
if resource is None:
408
flask.abort(404)
409
410
current_user = db.session.get(User, flask.session.get("username"))
411
if current_user is None:
412
flask.abort(401)
413
if resource.author != current_user and not current_user.admin:
414
flask.abort(403)
415
416
return flask.render_template("picture-annotation.html", resource=resource,
417
file_extension=mimetypes.guess_extension(resource.file_format))
418
419
420
@app.route("/picture/<int:id>/put-annotations-form")
421
def put_annotations_form(id):
422
resource = db.session.get(PictureResource, id)
423
if resource is None:
424
flask.abort(404)
425
426
current_user = db.session.get(User, flask.session.get("username"))
427
if current_user is None:
428
flask.abort(401)
429
430
if resource.author != current_user and not current_user.admin:
431
flask.abort(403)
432
433
return flask.render_template("put-annotations-form.html", resource=resource)
434
435
436
@app.route("/picture/<int:id>/put-annotations-form", methods=["POST"])
437
def put_annotations_form_post(id):
438
resource = db.session.get(PictureResource, id)
439
if resource is None:
440
flask.abort(404)
441
442
current_user = db.session.get(User, flask.session.get("username"))
443
if current_user is None:
444
flask.abort(401)
445
446
if resource.author != current_user and not current_user.admin:
447
flask.abort(403)
448
449
resource.put_annotations(json.loads(flask.request.form["annotations"]))
450
451
db.session.commit()
452
453
return flask.redirect("/picture/" + str(resource.id))
454
455
456
457
@app.route("/picture/<int:id>/save-annotations", methods=["POST"])
458
def save_annotations(id):
459
resource = db.session.get(PictureResource, id)
460
if resource is None:
461
flask.abort(404)
462
463
current_user = db.session.get(User, flask.session.get("username"))
464
if resource.author != current_user and not current_user.admin:
465
flask.abort(403)
466
467
resource.put_annotations(flask.request.json)
468
469
db.session.commit()
470
471
response = flask.make_response()
472
response.status_code = 204
473
return response
474
475
476
@app.route("/picture/<int:id>/get-annotations")
477
def get_annotations(id):
478
resource = db.session.get(PictureResource, id)
479
if resource is None:
480
flask.abort(404)
481
482
regions = db.session.query(PictureRegion).filter_by(resource_id=id).all()
483
484
regions_json = []
485
486
for region in regions:
487
regions_json.append({
488
"object": region.object_id,
489
"type": region.json["type"],
490
"shape": region.json["shape"],
491
})
492
493
return flask.jsonify(regions_json)
494
495
496
@app.route("/raw/picture/<int:id>")
497
def raw_picture(id):
498
resource = db.session.get(PictureResource, id)
499
if resource is None:
500
flask.abort(404)
501
502
response = flask.send_from_directory(path.join(config.DATA_PATH, "pictures"), str(resource.id))
503
response.mimetype = resource.file_format
504
505
return response
506
507
508
@app.route("/api/object-types")
509
def object_types():
510
objects = db.session.query(PictureObject).all()
511
return flask.jsonify({object.id: object.description for object in objects})
512