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