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