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