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