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 • 14.93 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
licences = Licence.query.order_by(Licence.free.desc(), Licence.pinned.desc(), Licence.title).all()
299
return flask.render_template("upload.html", licences=licences)
300
301
302
@app.route("/upload", methods=["POST"])
303
def upload_post():
304
title = flask.request.form["title"]
305
description = flask.request.form["description"]
306
origin_url = flask.request.form["origin_url"]
307
author = db.session.get(User, flask.session.get("username"))
308
309
file = flask.request.files["file"]
310
311
if not file or not file.filename:
312
flask.flash("No selected file")
313
return flask.redirect(flask.request.url)
314
315
resource = PictureResource(title, author, description, origin_url, ["CC0-1.0"], file.mimetype)
316
db.session.add(resource)
317
db.session.commit()
318
file.save(path.join(config.DATA_PATH, "pictures", str(resource.id)))
319
320
return flask.redirect("/picture/" + str(resource.id))
321
322
323
@app.route("/picture/<int:id>/")
324
def picture(id):
325
resource = db.session.get(PictureResource, id)
326
if resource is None:
327
flask.abort(404)
328
329
image = Image.open(path.join(config.DATA_PATH, "pictures", str(resource.id)))
330
331
return flask.render_template("picture.html", resource=resource,
332
file_extension=mimetypes.guess_extension(resource.file_format),
333
size=image.size)
334
335
336
337
@app.route("/picture/<int:id>/annotate")
338
def annotate_picture(id):
339
resource = db.session.get(PictureResource, id)
340
current_user = db.session.get(User, flask.session.get("username"))
341
if current_user is None:
342
flask.abort(401)
343
if resource.author != current_user and not current_user.admin:
344
flask.abort(403)
345
346
if resource is None:
347
flask.abort(404)
348
349
return flask.render_template("picture-annotation.html", resource=resource,
350
file_extension=mimetypes.guess_extension(resource.file_format))
351
352
353
@app.route("/picture/<int:id>/save-annotations", methods=["POST"])
354
def save_annotations(id):
355
resource = db.session.get(PictureResource, id)
356
if resource is None:
357
flask.abort(404)
358
359
current_user = db.session.get(User, flask.session.get("username"))
360
if resource.author != current_user and not current_user.admin:
361
flask.abort(403)
362
363
# Delete all previous annotations
364
db.session.query(PictureRegion).filter_by(resource_id=id).delete()
365
366
json = flask.request.json
367
for region in json:
368
object_id = region["object"]
369
picture_object = db.session.get(PictureObject, object_id)
370
371
region_data = {
372
"type": region["type"],
373
"shape": region["shape"],
374
}
375
376
region_row = PictureRegion(region_data, resource, picture_object)
377
db.session.add(region_row)
378
379
380
db.session.commit()
381
382
response = flask.make_response()
383
response.status_code = 204
384
return response
385
386
387
@app.route("/picture/<int:id>/get-annotations")
388
def get_annotations(id):
389
resource = db.session.get(PictureResource, id)
390
if resource is None:
391
flask.abort(404)
392
393
regions = db.session.query(PictureRegion).filter_by(resource_id=id).all()
394
395
regions_json = []
396
397
for region in regions:
398
regions_json.append({
399
"object": region.object_id,
400
"type": region.json["type"],
401
"shape": region.json["shape"],
402
})
403
404
return flask.jsonify(regions_json)
405
406
407
@app.route("/raw/picture/<int:id>")
408
def raw_picture(id):
409
resource = db.session.get(PictureResource, id)
410
if resource is None:
411
flask.abort(404)
412
413
response = flask.send_from_directory(path.join(config.DATA_PATH, "pictures"), str(resource.id))
414
response.mimetype = resource.file_format
415
416
return response
417
418
419
@app.route("/api/object-types")
420
def object_types():
421
objects = db.session.query(PictureObject).all()
422
return flask.jsonify({object.id: object.description for object in objects})
423