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