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