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