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