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.42 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
if resource.replaces:
558
resource.replaces.replaced_by = None
559
if resource.replaced_by:
560
resource.replaced_by.replaces = None
561
resource.copied_from = None
562
for copy in resource.copies:
563
copy.copied_from = None
564
db.session.delete(resource)
565
db.session.commit()
566
567
return flask.redirect("/")
568
569
570
@app.route("/picture/<int:id>/mark-replacement", methods=["POST"])
571
def mark_replacement(id):
572
resource = db.session.get(PictureResource, id)
573
if resource is None:
574
flask.abort(404)
575
576
current_user = db.session.get(User, flask.session.get("username"))
577
if current_user is None:
578
flask.abort(401)
579
580
if resource.copied_from.author != current_user and not current_user.admin:
581
flask.abort(403)
582
583
resource.copied_from.replaced_by = resource
584
resource.replaces = resource.copied_from
585
586
db.session.commit()
587
588
return flask.redirect("/picture/" + str(resource.copied_from.id))
589
590
591
@app.route("/picture/<int:id>/remove-replacement", methods=["POST"])
592
def remove_replacement(id):
593
resource = db.session.get(PictureResource, id)
594
if resource is None:
595
flask.abort(404)
596
597
current_user = db.session.get(User, flask.session.get("username"))
598
if current_user is None:
599
flask.abort(401)
600
601
if resource.author != current_user and not current_user.admin:
602
flask.abort(403)
603
604
resource.replaced_by.replaces = None
605
resource.replaced_by = None
606
607
db.session.commit()
608
609
return flask.redirect("/picture/" + str(resource.id))
610
611
612
@app.route("/picture/<int:id>/edit-metadata")
613
def edit_picture(id):
614
resource = db.session.get(PictureResource, id)
615
if resource is None:
616
flask.abort(404)
617
618
current_user = db.session.get(User, flask.session.get("username"))
619
if current_user is None:
620
flask.abort(401)
621
622
if resource.author != current_user and not current_user.admin:
623
flask.abort(403)
624
625
licences = Licence.query.order_by(Licence.free.desc(), Licence.pinned.desc(), Licence.title).all()
626
627
types = PictureNature.query.all()
628
629
return flask.render_template("edit-picture.html", resource=resource, licences=licences, types=types,
630
PictureLicence=PictureLicence)
631
632
633
@app.route("/picture/<int:id>/edit-metadata", methods=["POST"])
634
def edit_picture_post(id):
635
resource = db.session.get(PictureResource, id)
636
if resource is None:
637
flask.abort(404)
638
639
current_user = db.session.get(User, flask.session.get("username"))
640
if current_user is None:
641
flask.abort(401)
642
643
if resource.author != current_user and not current_user.admin:
644
flask.abort(403)
645
646
title = flask.request.form["title"]
647
description = flask.request.form["description"]
648
origin_url = flask.request.form["origin_url"]
649
licence_ids = flask.request.form.getlist("licence")
650
nature_id = flask.request.form["nature"]
651
652
if not title:
653
flask.flash("Enter a title")
654
return flask.redirect(flask.request.url)
655
656
if not description:
657
description = ""
658
659
if not nature_id:
660
flask.flash("Select a picture type")
661
return flask.redirect(flask.request.url)
662
663
if not licence_ids:
664
flask.flash("Select licences")
665
return flask.redirect(flask.request.url)
666
667
licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]
668
if not any(licence.free for licence in licences):
669
flask.flash("Select at least one free licence")
670
return flask.redirect(flask.request.url)
671
672
resource.title = title
673
resource.description = description
674
resource.origin_url = origin_url
675
for licence_id in licence_ids:
676
joiner = PictureLicence(resource, db.session.get(Licence, licence_id))
677
db.session.add(joiner)
678
resource.nature = db.session.get(PictureNature, nature_id)
679
680
db.session.commit()
681
682
return flask.redirect("/picture/" + str(resource.id))
683
684
685
@app.route("/picture/<int:id>/copy")
686
def copy_picture(id):
687
resource = db.session.get(PictureResource, id)
688
if resource is None:
689
flask.abort(404)
690
691
current_user = db.session.get(User, flask.session.get("username"))
692
if current_user is None:
693
flask.abort(401)
694
695
new_resource = PictureResource(resource.title, current_user, resource.description, resource.origin_url,
696
[licence.licence_id for licence in resource.licences], resource.file_format,
697
resource.nature)
698
699
for region in resource.regions:
700
db.session.add(PictureRegion(region.json, new_resource, region.object))
701
702
db.session.commit()
703
704
# Create a hard link for the new picture
705
old_path = path.join(config.DATA_PATH, "pictures", str(resource.id))
706
new_path = path.join(config.DATA_PATH, "pictures", str(new_resource.id))
707
os.link(old_path, new_path)
708
709
new_resource.width = resource.width
710
new_resource.height = resource.height
711
new_resource.copied_from = resource
712
713
db.session.commit()
714
715
return flask.redirect("/picture/" + str(new_resource.id))
716
717
718
@app.route("/query-pictures", methods=["POST"]) # sadly GET can't have a body
719
def query_pictures():
720
offset = int(flask.request.args.get("offset", 0))
721
limit = int(flask.request.args.get("limit", 16))
722
ordering = flask.request.args.get("ordering", "date-desc")
723
724
yaml_parser = yaml.YAML()
725
query_data = yaml_parser.load(flask.request.data) or {}
726
727
query = db.session.query(PictureResource)
728
729
requirement_conditions = {
730
"has_object": lambda value: PictureResource.regions.any(
731
PictureRegion.object_id.in_(value)),
732
"nature": lambda value: PictureResource.nature_id.in_(value),
733
"licence": lambda value: PictureResource.licences.any(
734
PictureLicence.licence_id.in_(value)),
735
"author": lambda value: PictureResource.author_name.in_(value),
736
"title": lambda value: PictureResource.title.ilike(value),
737
"description": lambda value: PictureResource.description.ilike(value),
738
"origin_url": lambda value: db.func.lower(db.func.substr(
739
PictureResource.origin_url,
740
db.func.length(db.func.split_part(PictureResource.origin_url, "://", 1)) + 4
741
)).in_(value),
742
"above_width": lambda value: PictureResource.width >= value,
743
"below_width": lambda value: PictureResource.width <= value,
744
"above_height": lambda value: PictureResource.height >= value,
745
"below_height": lambda value: PictureResource.height <= value,
746
"before_date": lambda value: PictureResource.timestamp <= datetime.utcfromtimestamp(
747
value),
748
"after_date": lambda value: PictureResource.timestamp >= datetime.utcfromtimestamp(
749
value)
750
}
751
if "want" in query_data:
752
for i in query_data["want"]:
753
requirement, value = list(i.items())[0]
754
condition = requirement_conditions.get(requirement)
755
if condition:
756
query = query.filter(condition(value))
757
if "exclude" in query_data:
758
for i in query_data["exclude"]:
759
requirement, value = list(i.items())[0]
760
condition = requirement_conditions.get(requirement)
761
if condition:
762
query = query.filter(~condition(value))
763
if not query_data.get("include_obsolete", False):
764
query = query.filter(PictureResource.replaced_by_id.is_(None))
765
766
match ordering:
767
case "date-desc":
768
query = query.order_by(PictureResource.timestamp.desc())
769
case "date-asc":
770
query = query.order_by(PictureResource.timestamp.asc())
771
case "title-asc":
772
query = query.order_by(PictureResource.title.asc())
773
case "title-desc":
774
query = query.order_by(PictureResource.title.desc())
775
case "random":
776
query = query.order_by(db.func.random())
777
case "number-regions-desc":
778
query = query.order_by(db.func.count(PictureResource.regions).desc())
779
case "number-regions-asc":
780
query = query.order_by(db.func.count(PictureResource.regions).asc())
781
782
query = query.offset(offset).limit(limit)
783
resources = query.all()
784
785
json_response = {
786
"date_generated": datetime.utcnow().timestamp(),
787
"resources": [],
788
"offset": offset,
789
"limit": limit,
790
}
791
792
json_resources = json_response["resources"]
793
794
for resource in resources:
795
json_resource = {
796
"id": resource.id,
797
"title": resource.title,
798
"description": resource.description,
799
"timestamp": resource.timestamp.timestamp(),
800
"origin_url": resource.origin_url,
801
"author": resource.author_name,
802
"file_format": resource.file_format,
803
"width": resource.width,
804
"height": resource.height,
805
"nature": resource.nature_id,
806
"licences": [licence.licence_id for licence in resource.licences],
807
"replaces": resource.replaces_id,
808
"replaced_by": resource.replaced_by_id,
809
"regions": [],
810
"download": config.ROOT_URL + flask.url_for("raw_picture", id=resource.id),
811
}
812
for region in resource.regions:
813
json_resource["regions"].append({
814
"object": region.object_id,
815
"type": region.json["type"],
816
"shape": region.json["shape"],
817
})
818
819
json_resources.append(json_resource)
820
821
response = flask.jsonify(json_response)
822
response.headers["Content-Type"] = "application/json"
823
return response
824
825
826
@app.route("/raw/picture/<int:id>")
827
def raw_picture(id):
828
resource = db.session.get(PictureResource, id)
829
if resource is None:
830
flask.abort(404)
831
832
response = flask.send_from_directory(path.join(config.DATA_PATH, "pictures"), str(resource.id))
833
response.mimetype = resource.file_format
834
835
return response
836
837
838
@app.route("/api/object-types")
839
def object_types():
840
objects = db.session.query(PictureObject).all()
841
return flask.jsonify({object.id: object.description for object in objects})
842