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 • 15.94 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(32), 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
return flask.render_template("upload.html", licences=licences)
304
305
306
@app.route("/upload", methods=["POST"])
307
def upload_post():
308
title = flask.request.form["title"]
309
description = flask.request.form["description"]
310
origin_url = flask.request.form["origin_url"]
311
author = db.session.get(User, flask.session.get("username"))
312
licence_ids = flask.request.form.getlist("licence")
313
314
if author is None:
315
flask.abort(401)
316
317
file = flask.request.files["file"]
318
319
if not file or not file.filename:
320
flask.flash("Select a file")
321
return flask.redirect(flask.request.url)
322
323
if not file.mimetype.startswith("image/"):
324
flask.flash("Only images are supported")
325
return flask.redirect(flask.request.url)
326
327
if not title:
328
flask.flash("Enter a title")
329
return flask.redirect(flask.request.url)
330
331
if not description:
332
flask.flash("Enter a description")
333
return flask.redirect(flask.request.url)
334
335
print(licence_ids)
336
337
if not licence_ids:
338
flask.flash("Select licences")
339
return flask.redirect(flask.request.url)
340
341
licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]
342
if not any(licence.free for licence in licences):
343
flask.flash("Select at least one free licence")
344
return flask.redirect(flask.request.url)
345
346
resource = PictureResource(title, author, description, origin_url, ["CC0-1.0"], file.mimetype)
347
db.session.add(resource)
348
db.session.commit()
349
file.save(path.join(config.DATA_PATH, "pictures", str(resource.id)))
350
351
flask.flash("Picture uploaded successfully")
352
353
return flask.redirect("/picture/" + str(resource.id))
354
355
356
@app.route("/picture/<int:id>/")
357
def picture(id):
358
resource = db.session.get(PictureResource, id)
359
if resource is None:
360
flask.abort(404)
361
362
image = Image.open(path.join(config.DATA_PATH, "pictures", str(resource.id)))
363
364
return flask.render_template("picture.html", resource=resource,
365
file_extension=mimetypes.guess_extension(resource.file_format),
366
size=image.size)
367
368
369
370
@app.route("/picture/<int:id>/annotate")
371
def annotate_picture(id):
372
resource = db.session.get(PictureResource, id)
373
current_user = db.session.get(User, flask.session.get("username"))
374
if current_user is None:
375
flask.abort(401)
376
if resource.author != current_user and not current_user.admin:
377
flask.abort(403)
378
379
if resource is None:
380
flask.abort(404)
381
382
return flask.render_template("picture-annotation.html", resource=resource,
383
file_extension=mimetypes.guess_extension(resource.file_format))
384
385
386
@app.route("/picture/<int:id>/save-annotations", methods=["POST"])
387
def save_annotations(id):
388
resource = db.session.get(PictureResource, id)
389
if resource is None:
390
flask.abort(404)
391
392
current_user = db.session.get(User, flask.session.get("username"))
393
if resource.author != current_user and not current_user.admin:
394
flask.abort(403)
395
396
# Delete all previous annotations
397
db.session.query(PictureRegion).filter_by(resource_id=id).delete()
398
399
json = flask.request.json
400
for region in json:
401
object_id = region["object"]
402
picture_object = db.session.get(PictureObject, object_id)
403
404
region_data = {
405
"type": region["type"],
406
"shape": region["shape"],
407
}
408
409
region_row = PictureRegion(region_data, resource, picture_object)
410
db.session.add(region_row)
411
412
413
db.session.commit()
414
415
response = flask.make_response()
416
response.status_code = 204
417
return response
418
419
420
@app.route("/picture/<int:id>/get-annotations")
421
def get_annotations(id):
422
resource = db.session.get(PictureResource, id)
423
if resource is None:
424
flask.abort(404)
425
426
regions = db.session.query(PictureRegion).filter_by(resource_id=id).all()
427
428
regions_json = []
429
430
for region in regions:
431
regions_json.append({
432
"object": region.object_id,
433
"type": region.json["type"],
434
"shape": region.json["shape"],
435
})
436
437
return flask.jsonify(regions_json)
438
439
440
@app.route("/raw/picture/<int:id>")
441
def raw_picture(id):
442
resource = db.session.get(PictureResource, id)
443
if resource is None:
444
flask.abort(404)
445
446
response = flask.send_from_directory(path.join(config.DATA_PATH, "pictures"), str(resource.id))
447
response.mimetype = resource.file_format
448
449
return response
450
451
452
@app.route("/api/object-types")
453
def object_types():
454
objects = db.session.query(PictureObject).all()
455
return flask.jsonify({object.id: object.description for object in objects})
456