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 • 29.18 kiB
Python script, ASCII text executable
        
            
1
import json
2
from datetime import datetime
3
from email.policy import default
4
from time import perf_counter
5
6
import flask
7
from flask_sqlalchemy import SQLAlchemy
8
from flask_bcrypt import Bcrypt
9
from flask_httpauth import HTTPBasicAuth
10
from markupsafe import escape, Markup
11
from flask_migrate import Migrate, current
12
from jinja2_fragments.flask import render_block
13
from sqlalchemy.orm import backref
14
import sqlalchemy.dialects.postgresql
15
from os import path
16
import os
17
from urllib.parse import urlencode
18
import mimetypes
19
import ruamel.yaml as yaml
20
21
from PIL import Image
22
from sqlalchemy.orm.persistence import post_update
23
24
import config
25
import markdown
26
27
28
app = flask.Flask(__name__)
29
bcrypt = Bcrypt(app)
30
31
32
app.config["SQLALCHEMY_DATABASE_URI"] = config.DB_URI
33
app.config["SECRET_KEY"] = config.DB_PASSWORD
34
35
36
db = SQLAlchemy(app)
37
migrate = Migrate(app, db)
38
39
40
@app.template_filter("split")
41
def split(value, separator=None, maxsplit=-1):
42
return value.split(separator, maxsplit)
43
44
45
@app.template_filter("median")
46
def median(value):
47
value = list(value) # prevent generators
48
return sorted(value)[len(value) // 2]
49
50
51
@app.template_filter("set")
52
def set_filter(value):
53
return set(value)
54
55
56
@app.template_global()
57
def modify_query(**new_values):
58
args = flask.request.args.copy()
59
for key, value in new_values.items():
60
args[key] = value
61
62
return f"{flask.request.path}?{urlencode(args)}"
63
64
65
@app.context_processor
66
def default_variables():
67
return {
68
"current_user": db.session.get(User, flask.session.get("username")),
69
}
70
71
72
with app.app_context():
73
class User(db.Model):
74
username = db.Column(db.String(32), unique=True, nullable=False, primary_key=True)
75
password_hashed = db.Column(db.String(60), nullable=False)
76
admin = db.Column(db.Boolean, nullable=False, default=False, server_default="false")
77
pictures = db.relationship("PictureResource", back_populates="author")
78
79
def __init__(self, username, password):
80
self.username = username
81
self.password_hashed = bcrypt.generate_password_hash(password).decode("utf-8")
82
83
84
class Licence(db.Model):
85
id = db.Column(db.String(64), primary_key=True) # SPDX identifier
86
title = db.Column(db.UnicodeText, nullable=False) # the official name of the licence
87
description = db.Column(db.UnicodeText, nullable=False) # brief description of its permissions and restrictions
88
info_url = db.Column(db.String(1024), nullable=False) # the URL to a page with general information about the licence
89
url = db.Column(db.String(1024), nullable=True) # the URL to a page with the full text of the licence and more information
90
pictures = db.relationship("PictureLicence", back_populates="licence")
91
free = db.Column(db.Boolean, nullable=False, default=False) # whether the licence is free or not
92
logo_url = db.Column(db.String(1024), nullable=True) # URL to the logo of the licence
93
pinned = db.Column(db.Boolean, nullable=False, default=False) # whether the licence should be shown at the top of the list
94
95
def __init__(self, id, title, description, info_url, url, free, logo_url=None, pinned=False):
96
self.id = id
97
self.title = title
98
self.description = description
99
self.info_url = info_url
100
self.url = url
101
self.free = free
102
self.logo_url = logo_url
103
self.pinned = pinned
104
105
106
class PictureLicence(db.Model):
107
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
108
109
resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"))
110
licence_id = db.Column(db.String(64), db.ForeignKey("licence.id"))
111
112
resource = db.relationship("PictureResource", back_populates="licences")
113
licence = db.relationship("Licence", back_populates="pictures")
114
115
def __init__(self, resource, licence):
116
self.resource = resource
117
self.licence = licence
118
119
120
class Resource(db.Model):
121
__abstract__ = True
122
123
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
124
title = db.Column(db.UnicodeText, nullable=False)
125
description = db.Column(db.UnicodeText, nullable=False)
126
timestamp = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
127
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
128
129
130
class PictureNature(db.Model):
131
# Examples:
132
# "photo", "paper-scan", "2d-art-photo", "sculpture-photo", "computer-3d", "computer-painting",
133
# "computer-line-art", "diagram", "infographic", "text", "map", "chart-graph", "screen-capture",
134
# "screen-photo", "pattern", "collage", "ai", and so on
135
id = db.Column(db.String(64), primary_key=True)
136
description = db.Column(db.UnicodeText, nullable=False)
137
resources = db.relationship("PictureResource", back_populates="nature")
138
139
def __init__(self, id, description):
140
self.id = id
141
self.description = description
142
143
144
class PictureObjectInheritance(db.Model):
145
parent_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"),
146
primary_key=True)
147
child_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"),
148
primary_key=True)
149
150
parent = db.relationship("PictureObject", foreign_keys=[parent_id],
151
back_populates="child_links")
152
child = db.relationship("PictureObject", foreign_keys=[child_id],
153
back_populates="parent_links")
154
155
def __init__(self, parent, child):
156
self.parent = parent
157
self.child = child
158
159
160
class PictureObject(db.Model):
161
id = db.Column(db.String(64), primary_key=True)
162
description = db.Column(db.UnicodeText, nullable=False)
163
164
child_links = db.relationship("PictureObjectInheritance",
165
foreign_keys=[PictureObjectInheritance.parent_id],
166
back_populates="parent")
167
parent_links = db.relationship("PictureObjectInheritance",
168
foreign_keys=[PictureObjectInheritance.child_id],
169
back_populates="child")
170
171
def __init__(self, id, description):
172
self.id = id
173
self.description = description
174
175
176
class PictureRegion(db.Model):
177
# This is for picture region annotations
178
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
179
json = db.Column(sqlalchemy.dialects.postgresql.JSONB, nullable=False)
180
181
resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), nullable=False)
182
object_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"), nullable=True)
183
184
resource = db.relationship("PictureResource", backref="regions")
185
object = db.relationship("PictureObject", backref="regions")
186
187
def __init__(self, json, resource, object):
188
self.json = json
189
self.resource = resource
190
self.object = object
191
192
193
class PictureResource(Resource):
194
# This is only for bitmap pictures. Vectors will be stored under a different model
195
# File name is the ID in the picture directory under data, without an extension
196
file_format = db.Column(db.String(64), nullable=False) # MIME type
197
width = db.Column(db.Integer, nullable=False)
198
height = db.Column(db.Integer, nullable=False)
199
nature_id = db.Column(db.String(32), db.ForeignKey("picture_nature.id"), nullable=True)
200
author_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
201
author = db.relationship("User", back_populates="pictures")
202
203
nature = db.relationship("PictureNature", back_populates="resources")
204
205
replaces_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), nullable=True)
206
replaced_by_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"),
207
nullable=True)
208
209
replaces = db.relationship("PictureResource", remote_side="PictureResource.id",
210
foreign_keys=[replaces_id], back_populates="replaced_by",
211
post_update=True)
212
replaced_by = db.relationship("PictureResource", remote_side="PictureResource.id",
213
foreign_keys=[replaced_by_id], post_update=True)
214
215
copied_from_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), nullable=True)
216
copied_from = db.relationship("PictureResource", remote_side="PictureResource.id",
217
backref="copies", foreign_keys=[copied_from_id])
218
219
licences = db.relationship("PictureLicence", back_populates="resource")
220
221
def __init__(self, title, author, description, origin_url, licence_ids, mime, nature=None):
222
self.title = title
223
self.author = author
224
self.description = description
225
self.origin_url = origin_url
226
self.file_format = mime
227
self.width = self.height = 0
228
self.nature = nature
229
db.session.add(self)
230
db.session.commit()
231
for licence_id in licence_ids:
232
joiner = PictureLicence(self, db.session.get(Licence, licence_id))
233
db.session.add(joiner)
234
235
def put_annotations(self, json):
236
# Delete all previous annotations
237
db.session.query(PictureRegion).filter_by(resource_id=self.id).delete()
238
239
for region in json:
240
object_id = region["object"]
241
picture_object = db.session.get(PictureObject, object_id)
242
243
region_data = {
244
"type": region["type"],
245
"shape": region["shape"],
246
}
247
248
region_row = PictureRegion(region_data, self, picture_object)
249
db.session.add(region_row)
250
251
252
@app.route("/")
253
def index():
254
return flask.render_template("home.html", resources=PictureResource.query.order_by(db.func.random()).limit(10).all())
255
256
257
@app.route("/accounts/")
258
def accounts():
259
return flask.render_template("login.html")
260
261
262
@app.route("/login", methods=["POST"])
263
def login():
264
username = flask.request.form["username"]
265
password = flask.request.form["password"]
266
267
user = db.session.get(User, username)
268
269
if user is None:
270
flask.flash("This username is not registered.")
271
return flask.redirect("/accounts")
272
273
if not bcrypt.check_password_hash(user.password_hashed, password):
274
flask.flash("Incorrect password.")
275
return flask.redirect("/accounts")
276
277
flask.flash("You have been logged in.")
278
279
flask.session["username"] = username
280
return flask.redirect("/")
281
282
283
@app.route("/logout")
284
def logout():
285
flask.session.pop("username", None)
286
flask.flash("You have been logged out.")
287
return flask.redirect("/")
288
289
290
@app.route("/signup", methods=["POST"])
291
def signup():
292
username = flask.request.form["username"]
293
password = flask.request.form["password"]
294
295
if db.session.get(User, username) is not None:
296
flask.flash("This username is already taken.")
297
return flask.redirect("/accounts")
298
299
if set(username) > set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"):
300
flask.flash("Usernames can only contain the Latin alphabet, digits, hyphens, and underscores.")
301
return flask.redirect("/accounts")
302
303
if len(username) < 3 or len(username) > 32:
304
flask.flash("Usernames must be between 3 and 32 characters long.")
305
return flask.redirect("/accounts")
306
307
if len(password) < 6:
308
flask.flash("Passwords must be at least 6 characters long.")
309
return flask.redirect("/accounts")
310
311
user = User(username, password)
312
db.session.add(user)
313
db.session.commit()
314
315
flask.session["username"] = username
316
317
flask.flash("You have been registered and logged in.")
318
319
return flask.redirect("/")
320
321
322
@app.route("/profile", defaults={"username": None})
323
@app.route("/profile/<username>")
324
def profile(username):
325
if username is None:
326
if "username" in flask.session:
327
return flask.redirect("/profile/" + flask.session["username"])
328
else:
329
flask.flash("Please log in to perform this action.")
330
return flask.redirect("/accounts")
331
332
user = db.session.get(User, username)
333
if user is None:
334
flask.abort(404)
335
336
return flask.render_template("profile.html", user=user)
337
338
339
@app.route("/object/<id>")
340
def has_object(id):
341
object_ = db.session.get(PictureObject, id)
342
if object_ is None:
343
flask.abort(404)
344
345
query = db.session.query(PictureResource).join(PictureRegion).filter(PictureRegion.object_id == id)
346
347
page = int(flask.request.args.get("page", 1))
348
per_page = int(flask.request.args.get("per_page", 16))
349
350
resources = query.paginate(page=page, per_page=per_page)
351
352
return flask.render_template("object.html", object=object_, resources=resources, page_number=page,
353
page_length=per_page, num_pages=resources.pages, prev_page=resources.prev_num,
354
next_page=resources.next_num, PictureRegion=PictureRegion)
355
356
357
@app.route("/upload")
358
def upload():
359
if "username" not in flask.session:
360
flask.flash("Log in to upload pictures.")
361
return flask.redirect("/accounts")
362
363
licences = Licence.query.order_by(Licence.free.desc(), Licence.pinned.desc(), Licence.title).all()
364
365
types = PictureNature.query.all()
366
367
return flask.render_template("upload.html", licences=licences, types=types)
368
369
370
@app.route("/upload", methods=["POST"])
371
def upload_post():
372
title = flask.request.form["title"]
373
description = flask.request.form["description"]
374
origin_url = flask.request.form["origin_url"]
375
author = db.session.get(User, flask.session.get("username"))
376
licence_ids = flask.request.form.getlist("licence")
377
nature_id = flask.request.form["nature"]
378
379
if author is None:
380
flask.abort(401)
381
382
file = flask.request.files["file"]
383
384
if not file or not file.filename:
385
flask.flash("Select a file")
386
return flask.redirect(flask.request.url)
387
388
if not file.mimetype.startswith("image/"):
389
flask.flash("Only images are supported")
390
return flask.redirect(flask.request.url)
391
392
if not title:
393
flask.flash("Enter a title")
394
return flask.redirect(flask.request.url)
395
396
if not description:
397
description = ""
398
399
if not nature_id:
400
flask.flash("Select a picture type")
401
return flask.redirect(flask.request.url)
402
403
if not licence_ids:
404
flask.flash("Select licences")
405
return flask.redirect(flask.request.url)
406
407
licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]
408
if not any(licence.free for licence in licences):
409
flask.flash("Select at least one free licence")
410
return flask.redirect(flask.request.url)
411
412
resource = PictureResource(title, author, description, origin_url, licence_ids, file.mimetype,
413
db.session.get(PictureNature, nature_id))
414
db.session.add(resource)
415
db.session.commit()
416
file.save(path.join(config.DATA_PATH, "pictures", str(resource.id)))
417
pil_image = Image.open(path.join(config.DATA_PATH, "pictures", str(resource.id)))
418
resource.width, resource.height = pil_image.size
419
db.session.commit()
420
421
if flask.request.form.get("annotations"):
422
try:
423
resource.put_annotations(json.loads(flask.request.form.get("annotations")))
424
db.session.commit()
425
except json.JSONDecodeError:
426
flask.flash("Invalid annotations")
427
428
flask.flash("Picture uploaded successfully")
429
430
return flask.redirect("/picture/" + str(resource.id))
431
432
433
@app.route("/picture/<int:id>/")
434
def picture(id):
435
resource = db.session.get(PictureResource, id)
436
if resource is None:
437
flask.abort(404)
438
439
image = Image.open(path.join(config.DATA_PATH, "pictures", str(resource.id)))
440
441
current_user = db.session.get(User, flask.session.get("username"))
442
have_permission = current_user and (current_user == resource.author or current_user.admin)
443
444
return flask.render_template("picture.html", resource=resource,
445
file_extension=mimetypes.guess_extension(resource.file_format),
446
size=image.size, copies=resource.copies, have_permission=have_permission)
447
448
449
450
@app.route("/picture/<int:id>/annotate")
451
def annotate_picture(id):
452
resource = db.session.get(PictureResource, id)
453
if resource is None:
454
flask.abort(404)
455
456
current_user = db.session.get(User, flask.session.get("username"))
457
if current_user is None:
458
flask.abort(401)
459
if resource.author != current_user and not current_user.admin:
460
flask.abort(403)
461
462
return flask.render_template("picture-annotation.html", resource=resource,
463
file_extension=mimetypes.guess_extension(resource.file_format))
464
465
466
@app.route("/picture/<int:id>/put-annotations-form")
467
def put_annotations_form(id):
468
resource = db.session.get(PictureResource, id)
469
if resource is None:
470
flask.abort(404)
471
472
current_user = db.session.get(User, flask.session.get("username"))
473
if current_user is None:
474
flask.abort(401)
475
476
if resource.author != current_user and not current_user.admin:
477
flask.abort(403)
478
479
return flask.render_template("put-annotations-form.html", resource=resource)
480
481
482
@app.route("/picture/<int:id>/put-annotations-form", methods=["POST"])
483
def put_annotations_form_post(id):
484
resource = db.session.get(PictureResource, id)
485
if resource is None:
486
flask.abort(404)
487
488
current_user = db.session.get(User, flask.session.get("username"))
489
if current_user is None:
490
flask.abort(401)
491
492
if resource.author != current_user and not current_user.admin:
493
flask.abort(403)
494
495
resource.put_annotations(json.loads(flask.request.form["annotations"]))
496
497
db.session.commit()
498
499
return flask.redirect("/picture/" + str(resource.id))
500
501
502
503
@app.route("/picture/<int:id>/save-annotations", methods=["POST"])
504
def save_annotations(id):
505
resource = db.session.get(PictureResource, id)
506
if resource is None:
507
flask.abort(404)
508
509
current_user = db.session.get(User, flask.session.get("username"))
510
if resource.author != current_user and not current_user.admin:
511
flask.abort(403)
512
513
resource.put_annotations(flask.request.json)
514
515
db.session.commit()
516
517
response = flask.make_response()
518
response.status_code = 204
519
return response
520
521
522
@app.route("/picture/<int:id>/get-annotations")
523
def get_annotations(id):
524
resource = db.session.get(PictureResource, id)
525
if resource is None:
526
flask.abort(404)
527
528
regions = db.session.query(PictureRegion).filter_by(resource_id=id).all()
529
530
regions_json = []
531
532
for region in regions:
533
regions_json.append({
534
"object": region.object_id,
535
"type": region.json["type"],
536
"shape": region.json["shape"],
537
})
538
539
return flask.jsonify(regions_json)
540
541
542
@app.route("/picture/<int:id>/delete")
543
def delete_picture(id):
544
resource = db.session.get(PictureResource, id)
545
if resource is None:
546
flask.abort(404)
547
548
current_user = db.session.get(User, flask.session.get("username"))
549
if current_user is None:
550
flask.abort(401)
551
552
if resource.author != current_user and not current_user.admin:
553
flask.abort(403)
554
555
PictureLicence.query.filter_by(resource=resource).delete()
556
PictureRegion.query.filter_by(resource=resource).delete()
557
db.session.delete(resource)
558
db.session.commit()
559
560
return flask.redirect("/")
561
562
563
@app.route("/picture/<int:id>/mark-replacement", methods=["POST"])
564
def mark_replacement(id):
565
resource = db.session.get(PictureResource, id)
566
if resource is None:
567
flask.abort(404)
568
569
current_user = db.session.get(User, flask.session.get("username"))
570
if current_user is None:
571
flask.abort(401)
572
573
if resource.copied_from.author != current_user and not current_user.admin:
574
flask.abort(403)
575
576
resource.copied_from.replaced_by = resource
577
resource.replaces = resource.copied_from
578
579
db.session.commit()
580
581
return flask.redirect("/picture/" + str(resource.copied_from.id))
582
583
584
@app.route("/picture/<int:id>/remove-replacement", methods=["POST"])
585
def remove_replacement(id):
586
resource = db.session.get(PictureResource, id)
587
if resource is None:
588
flask.abort(404)
589
590
current_user = db.session.get(User, flask.session.get("username"))
591
if current_user is None:
592
flask.abort(401)
593
594
if resource.author != current_user and not current_user.admin:
595
flask.abort(403)
596
597
resource.replaced_by.replaces = None
598
resource.replaced_by = None
599
600
db.session.commit()
601
602
return flask.redirect("/picture/" + str(resource.id))
603
604
605
@app.route("/picture/<int:id>/edit-metadata")
606
def edit_picture(id):
607
resource = db.session.get(PictureResource, id)
608
if resource is None:
609
flask.abort(404)
610
611
current_user = db.session.get(User, flask.session.get("username"))
612
if current_user is None:
613
flask.abort(401)
614
615
if resource.author != current_user and not current_user.admin:
616
flask.abort(403)
617
618
licences = Licence.query.order_by(Licence.free.desc(), Licence.pinned.desc(), Licence.title).all()
619
620
types = PictureNature.query.all()
621
622
return flask.render_template("edit-picture.html", resource=resource, licences=licences, types=types,
623
PictureLicence=PictureLicence)
624
625
626
@app.route("/picture/<int:id>/edit-metadata", methods=["POST"])
627
def edit_picture_post(id):
628
resource = db.session.get(PictureResource, id)
629
if resource is None:
630
flask.abort(404)
631
632
current_user = db.session.get(User, flask.session.get("username"))
633
if current_user is None:
634
flask.abort(401)
635
636
if resource.author != current_user and not current_user.admin:
637
flask.abort(403)
638
639
title = flask.request.form["title"]
640
description = flask.request.form["description"]
641
origin_url = flask.request.form["origin_url"]
642
licence_ids = flask.request.form.getlist("licence")
643
nature_id = flask.request.form["nature"]
644
645
if not title:
646
flask.flash("Enter a title")
647
return flask.redirect(flask.request.url)
648
649
if not description:
650
description = ""
651
652
if not nature_id:
653
flask.flash("Select a picture type")
654
return flask.redirect(flask.request.url)
655
656
if not licence_ids:
657
flask.flash("Select licences")
658
return flask.redirect(flask.request.url)
659
660
licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]
661
if not any(licence.free for licence in licences):
662
flask.flash("Select at least one free licence")
663
return flask.redirect(flask.request.url)
664
665
resource.title = title
666
resource.description = description
667
resource.origin_url = origin_url
668
for licence_id in licence_ids:
669
joiner = PictureLicence(resource, db.session.get(Licence, licence_id))
670
db.session.add(joiner)
671
resource.nature = db.session.get(PictureNature, nature_id)
672
673
db.session.commit()
674
675
return flask.redirect("/picture/" + str(resource.id))
676
677
678
@app.route("/picture/<int:id>/copy")
679
def copy_picture(id):
680
resource = db.session.get(PictureResource, id)
681
if resource is None:
682
flask.abort(404)
683
684
current_user = db.session.get(User, flask.session.get("username"))
685
if current_user is None:
686
flask.abort(401)
687
688
new_resource = PictureResource(resource.title, current_user, resource.description, resource.origin_url,
689
[licence.licence_id for licence in resource.licences], resource.file_format,
690
resource.nature)
691
692
for region in resource.regions:
693
db.session.add(PictureRegion(region.json, new_resource, region.object))
694
695
db.session.commit()
696
697
# Create a hard link for the new picture
698
old_path = path.join(config.DATA_PATH, "pictures", str(resource.id))
699
new_path = path.join(config.DATA_PATH, "pictures", str(new_resource.id))
700
os.link(old_path, new_path)
701
702
new_resource.width = resource.width
703
new_resource.height = resource.height
704
new_resource.copied_from = resource
705
706
db.session.commit()
707
708
return flask.redirect("/picture/" + str(new_resource.id))
709
710
711
@app.route("/query-pictures", methods=["POST"]) # sadly GET can't have a body
712
def query_pictures():
713
offset = int(flask.request.args.get("offset", 0))
714
limit = int(flask.request.args.get("limit", 16))
715
ordering = flask.request.args.get("ordering", "date-desc")
716
717
yaml_parser = yaml.YAML()
718
query_data = yaml_parser.load(flask.request.data) or {}
719
720
query = db.session.query(PictureResource)
721
722
requirement_conditions = {
723
"has_object": lambda value: PictureResource.regions.any(
724
PictureRegion.object_id.in_(value)),
725
"nature": lambda value: PictureResource.nature_id.in_(value),
726
"licence": lambda value: PictureResource.licences.any(
727
PictureLicence.licence_id.in_(value)),
728
"author": lambda value: PictureResource.author_name.in_(value),
729
"title": lambda value: PictureResource.title.ilike(value),
730
"description": lambda value: PictureResource.description.ilike(value),
731
"origin_url": lambda value: db.func.lower(db.func.substr(
732
PictureResource.origin_url,
733
db.func.length(db.func.split_part(PictureResource.origin_url, "://", 1)) + 4
734
)).in_(value),
735
"above_width": lambda value: PictureResource.width >= value,
736
"below_width": lambda value: PictureResource.width <= value,
737
"above_height": lambda value: PictureResource.height >= value,
738
"below_height": lambda value: PictureResource.height <= value,
739
"before_date": lambda value: PictureResource.timestamp <= datetime.utcfromtimestamp(
740
value),
741
"after_date": lambda value: PictureResource.timestamp >= datetime.utcfromtimestamp(
742
value)
743
}
744
if "want" in query_data:
745
for i in query_data["want"]:
746
requirement, value = list(i.items())[0]
747
condition = requirement_conditions.get(requirement)
748
if condition:
749
query = query.filter(condition(value))
750
if "exclude" in query_data:
751
for i in query_data["exclude"]:
752
requirement, value = list(i.items())[0]
753
condition = requirement_conditions.get(requirement)
754
if condition:
755
query = query.filter(~condition(value))
756
if not query_data.get("include_obsolete", False):
757
query = query.filter(PictureResource.replaced_by_id.is_(None))
758
759
match ordering:
760
case "date-desc":
761
query = query.order_by(PictureResource.timestamp.desc())
762
case "date-asc":
763
query = query.order_by(PictureResource.timestamp.asc())
764
case "title-asc":
765
query = query.order_by(PictureResource.title.asc())
766
case "title-desc":
767
query = query.order_by(PictureResource.title.desc())
768
case "random":
769
query = query.order_by(db.func.random())
770
case "number-regions-desc":
771
query = query.order_by(db.func.count(PictureResource.regions).desc())
772
case "number-regions-asc":
773
query = query.order_by(db.func.count(PictureResource.regions).asc())
774
775
query = query.offset(offset).limit(limit)
776
resources = query.all()
777
778
json_response = {
779
"date_generated": datetime.utcnow().timestamp(),
780
"resources": [],
781
"offset": offset,
782
"limit": limit,
783
}
784
785
json_resources = json_response["resources"]
786
787
for resource in resources:
788
json_resource = {
789
"id": resource.id,
790
"title": resource.title,
791
"description": resource.description,
792
"timestamp": resource.timestamp.timestamp(),
793
"origin_url": resource.origin_url,
794
"author": resource.author_name,
795
"file_format": resource.file_format,
796
"width": resource.width,
797
"height": resource.height,
798
"nature": resource.nature_id,
799
"licences": [licence.licence_id for licence in resource.licences],
800
"replaces": resource.replaces_id,
801
"replaced_by": resource.replaced_by_id,
802
"regions": [],
803
"download": config.ROOT_URL + flask.url_for("raw_picture", id=resource.id),
804
}
805
for region in resource.regions:
806
json_resource["regions"].append({
807
"object": region.object_id,
808
"type": region.json["type"],
809
"shape": region.json["shape"],
810
})
811
812
json_resources.append(json_resource)
813
814
response = flask.jsonify(json_response)
815
response.headers["Content-Type"] = "application/json"
816
return response
817
818
819
@app.route("/raw/picture/<int:id>")
820
def raw_picture(id):
821
resource = db.session.get(PictureResource, id)
822
if resource is None:
823
flask.abort(404)
824
825
response = flask.send_from_directory(path.join(config.DATA_PATH, "pictures"), str(resource.id))
826
response.mimetype = resource.file_format
827
828
return response
829
830
831
@app.route("/api/object-types")
832
def object_types():
833
objects = db.session.query(PictureObject).all()
834
return flask.jsonify({object.id: object.description for object in objects})
835