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