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