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 • 28.03 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
foreign_keys=[copied_from_id], back_populates="copies",
218
post_update=True)
219
220
copies = db.relationship("PictureResource", remote_side="PictureResource.id",
221
foreign_keys=[copied_from_id], post_update=True)
222
223
licences = db.relationship("PictureLicence", back_populates="resource")
224
225
def __init__(self, title, author, description, origin_url, licence_ids, mime, nature=None):
226
self.title = title
227
self.author = author
228
self.description = description
229
self.origin_url = origin_url
230
self.file_format = mime
231
self.width = self.height = 0
232
self.nature = nature
233
db.session.add(self)
234
db.session.commit()
235
for licence_id in licence_ids:
236
joiner = PictureLicence(self, db.session.get(Licence, licence_id))
237
db.session.add(joiner)
238
239
def put_annotations(self, json):
240
# Delete all previous annotations
241
db.session.query(PictureRegion).filter_by(resource_id=self.id).delete()
242
243
for region in json:
244
object_id = region["object"]
245
picture_object = db.session.get(PictureObject, object_id)
246
247
region_data = {
248
"type": region["type"],
249
"shape": region["shape"],
250
}
251
252
region_row = PictureRegion(region_data, self, picture_object)
253
db.session.add(region_row)
254
255
256
@app.route("/")
257
def index():
258
return flask.render_template("home.html", resources=PictureResource.query.order_by(db.func.random()).limit(10).all())
259
260
261
@app.route("/accounts/")
262
def accounts():
263
return flask.render_template("login.html")
264
265
266
@app.route("/login", methods=["POST"])
267
def login():
268
username = flask.request.form["username"]
269
password = flask.request.form["password"]
270
271
user = db.session.get(User, username)
272
273
if user is None:
274
flask.flash("This username is not registered.")
275
return flask.redirect("/accounts")
276
277
if not bcrypt.check_password_hash(user.password_hashed, password):
278
flask.flash("Incorrect password.")
279
return flask.redirect("/accounts")
280
281
flask.flash("You have been logged in.")
282
283
flask.session["username"] = username
284
return flask.redirect("/")
285
286
287
@app.route("/logout")
288
def logout():
289
flask.session.pop("username", None)
290
flask.flash("You have been logged out.")
291
return flask.redirect("/")
292
293
294
@app.route("/signup", methods=["POST"])
295
def signup():
296
username = flask.request.form["username"]
297
password = flask.request.form["password"]
298
299
if db.session.get(User, username) is not None:
300
flask.flash("This username is already taken.")
301
return flask.redirect("/accounts")
302
303
if set(username) > set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"):
304
flask.flash("Usernames can only contain the Latin alphabet, digits, hyphens, and underscores.")
305
return flask.redirect("/accounts")
306
307
if len(username) < 3 or len(username) > 32:
308
flask.flash("Usernames must be between 3 and 32 characters long.")
309
return flask.redirect("/accounts")
310
311
if len(password) < 6:
312
flask.flash("Passwords must be at least 6 characters long.")
313
return flask.redirect("/accounts")
314
315
user = User(username, password)
316
db.session.add(user)
317
db.session.commit()
318
319
flask.session["username"] = username
320
321
flask.flash("You have been registered and logged in.")
322
323
return flask.redirect("/")
324
325
326
@app.route("/profile", defaults={"username": None})
327
@app.route("/profile/<username>")
328
def profile(username):
329
if username is None:
330
if "username" in flask.session:
331
return flask.redirect("/profile/" + flask.session["username"])
332
else:
333
flask.flash("Please log in to perform this action.")
334
return flask.redirect("/accounts")
335
336
user = db.session.get(User, username)
337
if user is None:
338
flask.abort(404)
339
340
return flask.render_template("profile.html", user=user)
341
342
343
@app.route("/object/<id>")
344
def has_object(id):
345
object_ = db.session.get(PictureObject, id)
346
if object_ is None:
347
flask.abort(404)
348
349
query = db.session.query(PictureResource).join(PictureRegion).filter(PictureRegion.object_id == id)
350
351
page = int(flask.request.args.get("page", 1))
352
per_page = int(flask.request.args.get("per_page", 16))
353
354
resources = query.paginate(page=page, per_page=per_page)
355
356
return flask.render_template("object.html", object=object_, resources=resources, page_number=page,
357
page_length=per_page, num_pages=resources.pages, prev_page=resources.prev_num,
358
next_page=resources.next_num, PictureRegion=PictureRegion)
359
360
361
@app.route("/upload")
362
def upload():
363
if "username" not in flask.session:
364
flask.flash("Log in to upload pictures.")
365
return flask.redirect("/accounts")
366
367
licences = Licence.query.order_by(Licence.free.desc(), Licence.pinned.desc(), Licence.title).all()
368
369
types = PictureNature.query.all()
370
371
return flask.render_template("upload.html", licences=licences, types=types)
372
373
374
@app.route("/upload", methods=["POST"])
375
def upload_post():
376
title = flask.request.form["title"]
377
description = flask.request.form["description"]
378
origin_url = flask.request.form["origin_url"]
379
author = db.session.get(User, flask.session.get("username"))
380
licence_ids = flask.request.form.getlist("licence")
381
nature_id = flask.request.form["nature"]
382
383
if author is None:
384
flask.abort(401)
385
386
file = flask.request.files["file"]
387
388
if not file or not file.filename:
389
flask.flash("Select a file")
390
return flask.redirect(flask.request.url)
391
392
if not file.mimetype.startswith("image/"):
393
flask.flash("Only images are supported")
394
return flask.redirect(flask.request.url)
395
396
if not title:
397
flask.flash("Enter a title")
398
return flask.redirect(flask.request.url)
399
400
if not description:
401
description = ""
402
403
if not nature_id:
404
flask.flash("Select a picture type")
405
return flask.redirect(flask.request.url)
406
407
if not licence_ids:
408
flask.flash("Select licences")
409
return flask.redirect(flask.request.url)
410
411
licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]
412
if not any(licence.free for licence in licences):
413
flask.flash("Select at least one free licence")
414
return flask.redirect(flask.request.url)
415
416
resource = PictureResource(title, author, description, origin_url, licence_ids, file.mimetype,
417
db.session.get(PictureNature, nature_id))
418
db.session.add(resource)
419
db.session.commit()
420
file.save(path.join(config.DATA_PATH, "pictures", str(resource.id)))
421
pil_image = Image.open(path.join(config.DATA_PATH, "pictures", str(resource.id)))
422
resource.width, resource.height = pil_image.size
423
db.session.commit()
424
425
if flask.request.form.get("annotations"):
426
try:
427
resource.put_annotations(json.loads(flask.request.form.get("annotations")))
428
db.session.commit()
429
except json.JSONDecodeError:
430
flask.flash("Invalid annotations")
431
432
flask.flash("Picture uploaded successfully")
433
434
return flask.redirect("/picture/" + str(resource.id))
435
436
437
@app.route("/picture/<int:id>/")
438
def picture(id):
439
resource = db.session.get(PictureResource, id)
440
if resource is None:
441
flask.abort(404)
442
443
image = Image.open(path.join(config.DATA_PATH, "pictures", str(resource.id)))
444
445
return flask.render_template("picture.html", resource=resource,
446
file_extension=mimetypes.guess_extension(resource.file_format),
447
size=image.size)
448
449
450
451
@app.route("/picture/<int:id>/annotate")
452
def annotate_picture(id):
453
resource = db.session.get(PictureResource, id)
454
if resource is None:
455
flask.abort(404)
456
457
current_user = db.session.get(User, flask.session.get("username"))
458
if current_user is None:
459
flask.abort(401)
460
if resource.author != current_user and not current_user.admin:
461
flask.abort(403)
462
463
return flask.render_template("picture-annotation.html", resource=resource,
464
file_extension=mimetypes.guess_extension(resource.file_format))
465
466
467
@app.route("/picture/<int:id>/put-annotations-form")
468
def put_annotations_form(id):
469
resource = db.session.get(PictureResource, id)
470
if resource is None:
471
flask.abort(404)
472
473
current_user = db.session.get(User, flask.session.get("username"))
474
if current_user is None:
475
flask.abort(401)
476
477
if resource.author != current_user and not current_user.admin:
478
flask.abort(403)
479
480
return flask.render_template("put-annotations-form.html", resource=resource)
481
482
483
@app.route("/picture/<int:id>/put-annotations-form", methods=["POST"])
484
def put_annotations_form_post(id):
485
resource = db.session.get(PictureResource, id)
486
if resource is None:
487
flask.abort(404)
488
489
current_user = db.session.get(User, flask.session.get("username"))
490
if current_user is None:
491
flask.abort(401)
492
493
if resource.author != current_user and not current_user.admin:
494
flask.abort(403)
495
496
resource.put_annotations(json.loads(flask.request.form["annotations"]))
497
498
db.session.commit()
499
500
return flask.redirect("/picture/" + str(resource.id))
501
502
503
504
@app.route("/picture/<int:id>/save-annotations", methods=["POST"])
505
def save_annotations(id):
506
resource = db.session.get(PictureResource, id)
507
if resource is None:
508
flask.abort(404)
509
510
current_user = db.session.get(User, flask.session.get("username"))
511
if resource.author != current_user and not current_user.admin:
512
flask.abort(403)
513
514
resource.put_annotations(flask.request.json)
515
516
db.session.commit()
517
518
response = flask.make_response()
519
response.status_code = 204
520
return response
521
522
523
@app.route("/picture/<int:id>/get-annotations")
524
def get_annotations(id):
525
resource = db.session.get(PictureResource, id)
526
if resource is None:
527
flask.abort(404)
528
529
regions = db.session.query(PictureRegion).filter_by(resource_id=id).all()
530
531
regions_json = []
532
533
for region in regions:
534
regions_json.append({
535
"object": region.object_id,
536
"type": region.json["type"],
537
"shape": region.json["shape"],
538
})
539
540
return flask.jsonify(regions_json)
541
542
543
@app.route("/picture/<int:id>/delete")
544
def delete_picture(id):
545
resource = db.session.get(PictureResource, id)
546
if resource is None:
547
flask.abort(404)
548
549
current_user = db.session.get(User, flask.session.get("username"))
550
if current_user is None:
551
flask.abort(401)
552
553
if resource.author != current_user and not current_user.admin:
554
flask.abort(403)
555
556
PictureLicence.query.filter_by(resource=resource).delete()
557
PictureRegion.query.filter_by(resource=resource).delete()
558
db.session.delete(resource)
559
db.session.commit()
560
561
return flask.redirect("/")
562
563
564
@app.route("/picture/<int:id>/edit-metadata")
565
def edit_picture(id):
566
resource = db.session.get(PictureResource, id)
567
if resource is None:
568
flask.abort(404)
569
570
current_user = db.session.get(User, flask.session.get("username"))
571
if current_user is None:
572
flask.abort(401)
573
574
if resource.author != current_user and not current_user.admin:
575
flask.abort(403)
576
577
licences = Licence.query.order_by(Licence.free.desc(), Licence.pinned.desc(), Licence.title).all()
578
579
types = PictureNature.query.all()
580
581
return flask.render_template("edit-picture.html", resource=resource, licences=licences, types=types,
582
PictureLicence=PictureLicence)
583
584
585
@app.route("/picture/<int:id>/edit-metadata", methods=["POST"])
586
def edit_picture_post(id):
587
resource = db.session.get(PictureResource, id)
588
if resource is None:
589
flask.abort(404)
590
591
current_user = db.session.get(User, flask.session.get("username"))
592
if current_user is None:
593
flask.abort(401)
594
595
if resource.author != current_user and not current_user.admin:
596
flask.abort(403)
597
598
title = flask.request.form["title"]
599
description = flask.request.form["description"]
600
origin_url = flask.request.form["origin_url"]
601
licence_ids = flask.request.form.getlist("licence")
602
nature_id = flask.request.form["nature"]
603
604
if not title:
605
flask.flash("Enter a title")
606
return flask.redirect(flask.request.url)
607
608
if not description:
609
description = ""
610
611
if not nature_id:
612
flask.flash("Select a picture type")
613
return flask.redirect(flask.request.url)
614
615
if not licence_ids:
616
flask.flash("Select licences")
617
return flask.redirect(flask.request.url)
618
619
licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]
620
if not any(licence.free for licence in licences):
621
flask.flash("Select at least one free licence")
622
return flask.redirect(flask.request.url)
623
624
resource.title = title
625
resource.description = description
626
resource.origin_url = origin_url
627
for licence_id in licence_ids:
628
joiner = PictureLicence(resource, db.session.get(Licence, licence_id))
629
db.session.add(joiner)
630
resource.nature = db.session.get(PictureNature, nature_id)
631
632
db.session.commit()
633
634
return flask.redirect("/picture/" + str(resource.id))
635
636
637
@app.route("/picture/<int:id>/copy")
638
def copy_picture(id):
639
resource = db.session.get(PictureResource, id)
640
if resource is None:
641
flask.abort(404)
642
643
current_user = db.session.get(User, flask.session.get("username"))
644
if current_user is None:
645
flask.abort(401)
646
647
new_resource = PictureResource(resource.title, current_user, resource.description, resource.origin_url,
648
[licence.licence_id for licence in resource.licences], resource.file_format,
649
resource.nature)
650
651
for region in resource.regions:
652
db.session.add(PictureRegion(region.json, new_resource, region.object))
653
654
db.session.commit()
655
656
# Create a hard link for the new picture
657
old_path = path.join(config.DATA_PATH, "pictures", str(resource.id))
658
new_path = path.join(config.DATA_PATH, "pictures", str(new_resource.id))
659
os.link(old_path, new_path)
660
661
new_resource.width = resource.width
662
new_resource.height = resource.height
663
new_resource.copied_from = resource
664
665
db.session.commit()
666
667
return flask.redirect("/picture/" + str(new_resource.id))
668
669
670
@app.route("/query-pictures", methods=["POST"]) # sadly GET can't have a body
671
def query_pictures():
672
offset = int(flask.request.args.get("offset", 0))
673
limit = int(flask.request.args.get("limit", 16))
674
ordering = flask.request.args.get("ordering", "date-desc")
675
676
yaml_parser = yaml.YAML()
677
query_data = yaml_parser.load(flask.request.data) or {}
678
679
query = db.session.query(PictureResource)
680
681
requirement_conditions = {
682
"has_object": lambda value: PictureResource.regions.any(
683
PictureRegion.object_id.in_(value)),
684
"nature": lambda value: PictureResource.nature_id.in_(value),
685
"licence": lambda value: PictureResource.licences.any(
686
PictureLicence.licence_id.in_(value)),
687
"author": lambda value: PictureResource.author_name.in_(value),
688
"title": lambda value: PictureResource.title.ilike(value),
689
"description": lambda value: PictureResource.description.ilike(value),
690
"origin_url": lambda value: db.func.lower(db.func.substr(
691
PictureResource.origin_url,
692
db.func.length(db.func.split_part(PictureResource.origin_url, "://", 1)) + 4
693
)).in_(value),
694
"above_width": lambda value: PictureResource.width >= value,
695
"below_width": lambda value: PictureResource.width <= value,
696
"above_height": lambda value: PictureResource.height >= value,
697
"below_height": lambda value: PictureResource.height <= value,
698
"before_date": lambda value: PictureResource.timestamp <= datetime.utcfromtimestamp(
699
value),
700
"after_date": lambda value: PictureResource.timestamp >= datetime.utcfromtimestamp(
701
value)
702
}
703
if "want" in query_data:
704
for i in query_data["want"]:
705
requirement, value = list(i.items())[0]
706
condition = requirement_conditions.get(requirement)
707
if condition:
708
query = query.filter(condition(value))
709
if "exclude" in query_data:
710
for i in query_data["exclude"]:
711
requirement, value = list(i.items())[0]
712
condition = requirement_conditions.get(requirement)
713
if condition:
714
query = query.filter(~condition(value))
715
if not query_data.get("include_obsolete", False):
716
query = query.filter(PictureResource.replaced_by_id.is_(None))
717
718
match ordering:
719
case "date-desc":
720
query = query.order_by(PictureResource.timestamp.desc())
721
case "date-asc":
722
query = query.order_by(PictureResource.timestamp.asc())
723
case "title-asc":
724
query = query.order_by(PictureResource.title.asc())
725
case "title-desc":
726
query = query.order_by(PictureResource.title.desc())
727
case "random":
728
query = query.order_by(db.func.random())
729
case "number-regions-desc":
730
query = query.order_by(db.func.count(PictureResource.regions).desc())
731
case "number-regions-asc":
732
query = query.order_by(db.func.count(PictureResource.regions).asc())
733
734
query = query.offset(offset).limit(limit)
735
resources = query.all()
736
737
json_response = {
738
"date_generated": datetime.utcnow().timestamp(),
739
"resources": [],
740
"offset": offset,
741
"limit": limit,
742
}
743
744
json_resources = json_response["resources"]
745
746
for resource in resources:
747
json_resource = {
748
"id": resource.id,
749
"title": resource.title,
750
"description": resource.description,
751
"timestamp": resource.timestamp.timestamp(),
752
"origin_url": resource.origin_url,
753
"author": resource.author_name,
754
"file_format": resource.file_format,
755
"width": resource.width,
756
"height": resource.height,
757
"nature": resource.nature_id,
758
"licences": [licence.licence_id for licence in resource.licences],
759
"replaces": resource.replaces_id,
760
"replaced_by": resource.replaced_by_id,
761
"regions": [],
762
"download": config.ROOT_URL + flask.url_for("raw_picture", id=resource.id),
763
}
764
for region in resource.regions:
765
json_resource["regions"].append({
766
"object": region.object_id,
767
"type": region.json["type"],
768
"shape": region.json["shape"],
769
})
770
771
json_resources.append(json_resource)
772
773
response = flask.jsonify(json_response)
774
response.headers["Content-Type"] = "application/json"
775
return response
776
777
778
@app.route("/raw/picture/<int:id>")
779
def raw_picture(id):
780
resource = db.session.get(PictureResource, id)
781
if resource is None:
782
flask.abort(404)
783
784
response = flask.send_from_directory(path.join(config.DATA_PATH, "pictures"), str(resource.id))
785
response.mimetype = resource.file_format
786
787
return response
788
789
790
@app.route("/api/object-types")
791
def object_types():
792
objects = db.session.query(PictureObject).all()
793
return flask.jsonify({object.id: object.description for object in objects})
794