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