<html><head><meta name="color-scheme" content="light dark"></head><body><pre style="word-wrap: break-word; white-space: pre-wrap;">import json
import os
import mimetypes
import flask
import ruamel.yaml as yaml
import sqlalchemy.dialects.postgresql
import config
import markdown

from datetime import datetime
from os import path
from flask_sqlalchemy import SQLAlchemy
from flask_bcrypt import Bcrypt
from flask_migrate import Migrate, current
from urllib.parse import urlencode
from PIL import Image

app = flask.Flask(__name__)
bcrypt = Bcrypt(app)

app.config["SQLALCHEMY_DATABASE_URI"] = config.DB_URI
app.config["SECRET_KEY"] = config.DB_PASSWORD

db = SQLAlchemy(app)
migrate = Migrate(app, db)


@app.template_filter("split")
def split(value, separator=None, maxsplit=-1):
    return value.split(separator, maxsplit)


@app.template_filter("median")
def median(value):
    value = list(value)  # prevent generators
    return sorted(value)[len(value) // 2]


@app.template_filter("set")
def set_filter(value):
    return set(value)


@app.template_global()
def modify_query(**new_values):
    args = flask.request.args.copy()
    for key, value in new_values.items():
        args[key] = value

    return f"{flask.request.path}?{urlencode(args)}"


@app.context_processor
def default_variables():
    return {
        "current_user": db.session.get(User, flask.session.get("username")),
        "site_name": config.SITE_NAME,
    }


with app.app_context():
    class User(db.Model):
        username = db.Column(db.String(32), unique=True, nullable=False, primary_key=True)
        password_hashed = db.Column(db.String(60), nullable=False)
        admin = db.Column(db.Boolean, nullable=False, default=False, server_default="false")
        pictures = db.relationship("PictureResource", back_populates="author")
        joined_timestamp = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
        galleries = db.relationship("Gallery", back_populates="owner")
        galleries_joined = db.relationship("UserInGallery", back_populates="user")
        ratings = db.relationship("PictureRating", back_populates="user")

        def __init__(self, username, password):
            self.username = username
            self.password_hashed = bcrypt.generate_password_hash(password).decode("utf-8")

        @property
        def formatted_name(self):
            if self.admin:
                return self.username + "*"
            return self.username


    class Licence(db.Model):
        id = db.Column(db.String(64), primary_key=True)  # SPDX identifier
        title = db.Column(db.UnicodeText, nullable=False)  # the official name of the licence
        description = db.Column(db.UnicodeText,
                                nullable=False)  # brief description of its permissions and restrictions
        info_url = db.Column(db.String(1024),
                             nullable=False)  # the URL to a page with general information about the licence
        url = db.Column(db.String(1024),
                        nullable=True)  # the URL to a page with the full text of the licence and more information
        pictures = db.relationship("PictureLicence", back_populates="licence")
        free = db.Column(db.Boolean, nullable=False,
                         default=False)  # whether the licence is free or not
        logo_url = db.Column(db.String(1024), nullable=True)  # URL to the logo of the licence
        pinned = db.Column(db.Boolean, nullable=False,
                           default=False)  # whether the licence should be shown at the top of the list

        def __init__(self, id, title, description, info_url, url, free, logo_url=None,
                     pinned=False):
            self.id = id
            self.title = title
            self.description = description
            self.info_url = info_url
            self.url = url
            self.free = free
            self.logo_url = logo_url
            self.pinned = pinned


    class PictureLicence(db.Model):
        id = db.Column(db.Integer, primary_key=True, autoincrement=True)

        resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"))
        licence_id = db.Column(db.String(64), db.ForeignKey("licence.id"))

        resource = db.relationship("PictureResource", back_populates="licences")
        licence = db.relationship("Licence", back_populates="pictures")

        def __init__(self, resource, licence):
            self.resource = resource
            self.licence = licence


    class Resource(db.Model):
        __abstract__ = True

        id = db.Column(db.Integer, primary_key=True, autoincrement=True)
        title = db.Column(db.UnicodeText, nullable=False)
        description = db.Column(db.UnicodeText, nullable=False)
        timestamp = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
        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


    class PictureNature(db.Model):
        # Examples:
        # "photo", "paper-scan", "2d-art-photo", "sculpture-photo", "computer-3d", "computer-painting",
        # "computer-line-art", "diagram", "infographic", "text", "map", "chart-graph", "screen-capture",
        # "screen-photo", "pattern", "collage", "ai", and so on
        id = db.Column(db.String(64), primary_key=True)
        description = db.Column(db.UnicodeText, nullable=False)
        resources = db.relationship("PictureResource", back_populates="nature")

        def __init__(self, id, description):
            self.id = id
            self.description = description


    class PictureObjectInheritance(db.Model):
        parent_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"),
                              primary_key=True)
        child_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"),
                             primary_key=True)

        parent = db.relationship("PictureObject", foreign_keys=[parent_id],
                                 back_populates="child_links")
        child = db.relationship("PictureObject", foreign_keys=[child_id],
                                back_populates="parent_links")

        def __init__(self, parent, child):
            self.parent = parent
            self.child = child


    class PictureObject(db.Model):
        id = db.Column(db.String(64), primary_key=True)
        description = db.Column(db.UnicodeText, nullable=False)

        child_links = db.relationship("PictureObjectInheritance",
                                      foreign_keys=[PictureObjectInheritance.parent_id],
                                      back_populates="parent")
        parent_links = db.relationship("PictureObjectInheritance",
                                       foreign_keys=[PictureObjectInheritance.child_id],
                                       back_populates="child")

        def __init__(self, id, description, parents):
            self.id = id
            self.description = description
            if parents:
                for parent in parents:
                    db.session.add(PictureObjectInheritance(parent, self))


    class PictureRegion(db.Model):
        # This is for picture region annotations
        id = db.Column(db.Integer, primary_key=True, autoincrement=True)
        json = db.Column(sqlalchemy.dialects.postgresql.JSONB, nullable=False)

        resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"),
                                nullable=False)
        object_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"), nullable=True)

        resource = db.relationship("PictureResource", backref="regions")
        object = db.relationship("PictureObject", backref="regions")

        def __init__(self, json, resource, object):
            self.json = json
            self.resource = resource
            self.object = object


    class PictureResource(Resource):
        # This is only for bitmap pictures. Vectors will be stored under a different model
        # File name is the ID in the picture directory under data, without an extension
        file_format = db.Column(db.String(64), nullable=False)  # MIME type
        width = db.Column(db.Integer, nullable=False)
        height = db.Column(db.Integer, nullable=False)
        nature_id = db.Column(db.String(32), db.ForeignKey("picture_nature.id"), nullable=True)
        author_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
        author = db.relationship("User", back_populates="pictures")

        nature = db.relationship("PictureNature", back_populates="resources")

        replaces_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), nullable=True)
        replaced_by_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"),
                                   nullable=True)

        replaces = db.relationship("PictureResource", remote_side="PictureResource.id",
                                   foreign_keys=[replaces_id], back_populates="replaced_by",
                                   post_update=True)
        replaced_by = db.relationship("PictureResource", remote_side="PictureResource.id",
                                      foreign_keys=[replaced_by_id], post_update=True)

        copied_from_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"),
                                   nullable=True)
        copied_from = db.relationship("PictureResource", remote_side="PictureResource.id",
                                      backref="copies", foreign_keys=[copied_from_id])

        licences = db.relationship("PictureLicence", back_populates="resource")
        galleries = db.relationship("PictureInGallery", back_populates="resource")
        ratings = db.relationship("PictureRating", back_populates="resource")

        def __init__(self, title, author, description, origin_url, licence_ids, mime,
                     nature=None):
            self.title = title
            self.author = author
            self.description = description
            self.origin_url = origin_url
            self.file_format = mime
            self.width = self.height = 0
            self.nature = nature
            db.session.add(self)
            db.session.commit()
            for licence_id in licence_ids:
                joiner = PictureLicence(self, db.session.get(Licence, licence_id))
                db.session.add(joiner)

        def put_annotations(self, json):
            # Delete all previous annotations
            db.session.query(PictureRegion).filter_by(resource_id=self.id).delete()

            for region in json:
                object_id = region["object"]
                picture_object = db.session.get(PictureObject, object_id)

                region_data = {
                    "type": region["type"],
                    "shape": region["shape"],
                }

                region_row = PictureRegion(region_data, self, picture_object)
                db.session.add(region_row)

        @property
        def average_rating(self):
            if not self.ratings:
                return None
            return db.session.query(db.func.avg(PictureRating.rating)).filter_by(resource=self).scalar()

        @property
        def rating_totals(self):
            all_ratings = db.session.query(PictureRating.rating).filter_by(resource=self)
            return {rating: all_ratings.filter_by(rating=rating).count() for rating in range(1, 6)}

        @property
        def stars(self):
            if not self.ratings:
                return 0
            average = self.average_rating
            whole_stars = int(average)
            partial_star = average - whole_stars

            return [100] * whole_stars + [int(partial_star * 100)] + [0] * (4 - whole_stars)


    class PictureInGallery(db.Model):
        id = db.Column(db.Integer, primary_key=True, autoincrement=True)
        resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"),
                                nullable=False)
        gallery_id = db.Column(db.Integer, db.ForeignKey("gallery.id"), nullable=False)

        resource = db.relationship("PictureResource")
        gallery = db.relationship("Gallery")

        def __init__(self, resource, gallery):
            self.resource = resource
            self.gallery = gallery


    class UserInGallery(db.Model):
        id = db.Column(db.Integer, primary_key=True, autoincrement=True)
        username = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
        gallery_id = db.Column(db.Integer, db.ForeignKey("gallery.id"), nullable=False)

        user = db.relationship("User")
        gallery = db.relationship("Gallery")

        def __init__(self, user, gallery):
            self.user = user
            self.gallery = gallery


    class Gallery(db.Model):
        id = db.Column(db.Integer, primary_key=True, autoincrement=True)
        title = db.Column(db.UnicodeText, nullable=False)
        description = db.Column(db.UnicodeText, nullable=False)
        pictures = db.relationship("PictureInGallery", back_populates="gallery")
        owner_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
        owner = db.relationship("User", back_populates="galleries")
        users = db.relationship("UserInGallery", back_populates="gallery")

        def __init__(self, title, description, owner):
            self.title = title
            self.description = description
            self.owner = owner


    class PictureRating(db.Model):
        id = db.Column(db.Integer, primary_key=True, autoincrement=True)
        resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), nullable=False)
        username = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
        rating = db.Column(db.Integer, db.CheckConstraint("rating &gt;= 1 AND rating &lt;= 5"),
                           nullable=False)

        resource = db.relationship("PictureResource", back_populates="ratings")
        user = db.relationship("User", back_populates="ratings")

        def __init__(self, resource, user, rating):
            self.resource = resource
            self.user = user
            self.rating = rating


@app.route("/")
def index():
    return flask.render_template("home.html", resources=PictureResource.query.filter_by(replaced_by=None).order_by(
        db.func.random()).limit(10).all())


@app.route("/info/")
def usage_guide():
    with open("help/usage.md") as f:
        return flask.render_template("help.html", content=markdown.markdown2html(f.read()))


@app.route("/accounts/")
def accounts():
    return flask.render_template("login.html")


@app.route("/login", methods=["POST"])
def login():
    username = flask.request.form["username"]
    password = flask.request.form["password"]

    user = db.session.get(User, username)

    if user is None:
        flask.flash("This username is not registered.")
        return flask.redirect("/accounts")

    if not bcrypt.check_password_hash(user.password_hashed, password):
        flask.flash("Incorrect password.")
        return flask.redirect("/accounts")

    flask.flash("You have been logged in.")

    flask.session["username"] = username
    return flask.redirect("/")


@app.route("/logout")
def logout():
    flask.session.pop("username", None)
    flask.flash("You have been logged out.")
    return flask.redirect("/")


@app.route("/signup", methods=["POST"])
def signup():
    username = flask.request.form["username"]
    password = flask.request.form["password"]

    if db.session.get(User, username) is not None:
        flask.flash("This username is already taken.")
        return flask.redirect("/accounts")

    if set(username) &gt; set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"):
        flask.flash(
            "Usernames can only contain the Latin alphabet, digits, hyphens, and underscores.")
        return flask.redirect("/accounts")

    if len(username) &lt; 3 or len(username) &gt; 32:
        flask.flash("Usernames must be between 3 and 32 characters long.")
        return flask.redirect("/accounts")

    if len(password) &lt; 6:
        flask.flash("Passwords must be at least 6 characters long.")
        return flask.redirect("/accounts")

    user = User(username, password)
    db.session.add(user)
    db.session.commit()

    flask.session["username"] = username

    flask.flash("You have been registered and logged in.")

    return flask.redirect("/")


@app.route("/profile", defaults={"username": None})
@app.route("/profile/&lt;username&gt;")
def profile(username):
    if username is None:
        if "username" in flask.session:
            return flask.redirect("/profile/" + flask.session["username"])
        else:
            flask.flash("Please log in to perform this action.")
            return flask.redirect("/accounts")

    user = db.session.get(User, username)
    if user is None:
        flask.abort(404)

    return flask.render_template("profile.html", user=user)


@app.route("/object/&lt;id&gt;")
def has_object(id):
    object_ = db.session.get(PictureObject, id)
    if object_ is None:
        flask.abort(404)

    descendants_cte = (
        db.select(PictureObject.id)
        .where(PictureObject.id == id)
        .cte(name="descendants_cte", recursive=True)
    )

    descendants_cte = descendants_cte.union_all(
        db.select(PictureObjectInheritance.child_id)
        .where(PictureObjectInheritance.parent_id == descendants_cte.c.id)
    )

    query = db.session.query(PictureResource).filter(
        PictureResource.regions.any(
            PictureRegion.object_id.in_(
                db.select(descendants_cte.c.id)
            )
        )
    )

    page = int(flask.request.args.get("page", 1))
    per_page = int(flask.request.args.get("per_page", 16))

    resources = query.paginate(page=page, per_page=per_page)

    return flask.render_template("object.html", object=object_, resources=resources,
                                 page_number=page,
                                 page_length=per_page, num_pages=resources.pages,
                                 prev_page=resources.prev_num,
                                 next_page=resources.next_num, PictureRegion=PictureRegion)


@app.route("/upload")
def upload():
    if "username" not in flask.session:
        flask.flash("Log in to upload pictures.")
        return flask.redirect("/accounts")

    licences = Licence.query.order_by(Licence.free.desc(), Licence.pinned.desc(),
                                      Licence.title).all()

    types = PictureNature.query.all()

    return flask.render_template("upload.html", licences=licences, types=types)


@app.route("/upload", methods=["POST"])
def upload_post():
    title = flask.request.form["title"]
    description = flask.request.form["description"]
    origin_url = flask.request.form["origin_url"]
    author = db.session.get(User, flask.session.get("username"))
    licence_ids = flask.request.form.getlist("licence")
    nature_id = flask.request.form["nature"]

    if author is None:
        flask.abort(401)

    file = flask.request.files["file"]

    if not file or not file.filename:
        flask.flash("Select a file")
        return flask.redirect(flask.request.url)

    if not file.mimetype.startswith("image/") or file.mimetype == "image/svg+xml":
        flask.flash("Only images are supported")
        return flask.redirect(flask.request.url)

    if not title:
        flask.flash("Enter a title")
        return flask.redirect(flask.request.url)

    if not description:
        description = ""

    if not nature_id:
        flask.flash("Select a picture type")
        return flask.redirect(flask.request.url)

    if not licence_ids:
        flask.flash("Select licences")
        return flask.redirect(flask.request.url)

    licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]
    if not any(licence.free for licence in licences):
        flask.flash("Select at least one free licence")
        return flask.redirect(flask.request.url)

    resource = PictureResource(title, author, description, origin_url, licence_ids,
                               file.mimetype,
                               db.session.get(PictureNature, nature_id))
    db.session.add(resource)
    db.session.commit()
    file.save(path.join(config.DATA_PATH, "pictures", str(resource.id)))
    pil_image = Image.open(path.join(config.DATA_PATH, "pictures", str(resource.id)))
    resource.width, resource.height = pil_image.size
    db.session.commit()

    if flask.request.form.get("annotations"):
        try:
            resource.put_annotations(json.loads(flask.request.form.get("annotations")))
            db.session.commit()
        except json.JSONDecodeError:
            flask.flash("Invalid annotations")

    flask.flash("Picture uploaded successfully")

    return flask.redirect("/picture/" + str(resource.id))


@app.route("/picture/&lt;int:id&gt;/")
def picture(id):
    resource = db.session.get(PictureResource, id)
    if resource is None:
        flask.abort(404)

    image = Image.open(path.join(config.DATA_PATH, "pictures", str(resource.id)))

    current_user = db.session.get(User, flask.session.get("username"))
    have_permission = current_user and (current_user == resource.author or current_user.admin)

    own_rating = None
    if current_user:
        own_rating = PictureRating.query.filter_by(resource=resource, user=current_user).first()

    return flask.render_template("picture.html", resource=resource,
                                 file_extension=mimetypes.guess_extension(resource.file_format),
                                 size=image.size, copies=resource.copies,
                                 have_permission=have_permission, own_rating=own_rating)


@app.route("/picture/&lt;int:id&gt;/annotate")
def annotate_picture(id):
    resource = db.session.get(PictureResource, id)
    if resource is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    if resource.author != current_user and not current_user.admin:
        flask.abort(403)

    return flask.render_template("picture-annotation.html", resource=resource,
                                 file_extension=mimetypes.guess_extension(resource.file_format))


@app.route("/picture/&lt;int:id&gt;/put-annotations-form")
def put_annotations_form(id):
    resource = db.session.get(PictureResource, id)
    if resource is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    if resource.author != current_user and not current_user.admin:
        flask.abort(403)

    return flask.render_template("put-annotations-form.html", resource=resource)


@app.route("/picture/&lt;int:id&gt;/put-annotations-form", methods=["POST"])
def put_annotations_form_post(id):
    resource = db.session.get(PictureResource, id)
    if resource is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    if resource.author != current_user and not current_user.admin:
        flask.abort(403)

    resource.put_annotations(json.loads(flask.request.form["annotations"]))

    db.session.commit()

    return flask.redirect("/picture/" + str(resource.id))


@app.route("/picture/&lt;int:id&gt;/save-annotations", methods=["POST"])
@app.route("/api/picture/&lt;int:id&gt;/put-annotations", methods=["POST"])
def save_annotations(id):
    resource = db.session.get(PictureResource, id)
    if resource is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if resource.author != current_user and not current_user.admin:
        flask.abort(403)

    resource.put_annotations(flask.request.json)

    db.session.commit()

    response = flask.make_response()
    response.status_code = 204
    return response


@app.route("/picture/&lt;int:id&gt;/get-annotations")
@app.route("/api/picture/&lt;int:id&gt;/api/get-annotations")
def get_annotations(id):
    resource = db.session.get(PictureResource, id)
    if resource is None:
        flask.abort(404)

    regions = db.session.query(PictureRegion).filter_by(resource_id=id).all()

    regions_json = []

    for region in regions:
        regions_json.append({
            "object": region.object_id,
            "type": region.json["type"],
            "shape": region.json["shape"],
        })

    return flask.jsonify(regions_json)


@app.route("/picture/&lt;int:id&gt;/delete")
def delete_picture(id):
    resource = db.session.get(PictureResource, id)
    if resource is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    if resource.author != current_user and not current_user.admin:
        flask.abort(403)

    PictureLicence.query.filter_by(resource=resource).delete()
    PictureRegion.query.filter_by(resource=resource).delete()
    PictureInGallery.query.filter_by(resource=resource).delete()
    PictureRating.query.filter_by(resource=resource).delete()
    if resource.replaces:
        resource.replaces.replaced_by = None
    if resource.replaced_by:
        resource.replaced_by.replaces = None
    resource.copied_from = None
    for copy in resource.copies:
        copy.copied_from = None
    db.session.delete(resource)
    db.session.commit()

    return flask.redirect("/")


@app.route("/picture/&lt;int:id&gt;/mark-replacement", methods=["POST"])
def mark_picture_replacement(id):
    resource = db.session.get(PictureResource, id)
    if resource is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    if resource.copied_from.author != current_user and not current_user.admin:
        flask.abort(403)

    resource.copied_from.replaced_by = resource
    resource.replaces = resource.copied_from

    db.session.commit()

    return flask.redirect("/picture/" + str(resource.copied_from.id))


@app.route("/picture/&lt;int:id&gt;/remove-replacement", methods=["POST"])
def remove_picture_replacement(id):
    resource = db.session.get(PictureResource, id)
    if resource is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    if resource.author != current_user and not current_user.admin:
        flask.abort(403)

    resource.replaced_by.replaces = None
    resource.replaced_by = None

    db.session.commit()

    return flask.redirect("/picture/" + str(resource.id))


@app.route("/picture/&lt;int:id&gt;/edit-metadata")
def edit_picture(id):
    resource = db.session.get(PictureResource, id)
    if resource is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    if resource.author != current_user and not current_user.admin:
        flask.abort(403)

    licences = Licence.query.order_by(Licence.free.desc(), Licence.pinned.desc(),
                                      Licence.title).all()

    types = PictureNature.query.all()

    return flask.render_template("edit-picture.html", resource=resource, licences=licences,
                                 types=types,
                                 PictureLicence=PictureLicence)


@app.route("/picture/&lt;int:id&gt;/rate", methods=["POST"])
def rate_picture(id):
    resource = db.session.get(PictureResource, id)
    if resource is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    rating = int(flask.request.form.get("rating"))

    if not rating:
        # Delete the existing rating
        if PictureRating.query.filter_by(resource=resource, user=current_user).first():
            db.session.delete(PictureRating.query.filter_by(resource=resource,
                                                             user=current_user).first())
            db.session.commit()

        return flask.redirect("/picture/" + str(resource.id))

    if not 1 &lt;= rating &lt;= 5:
        flask.flash("Invalid rating")
        return flask.redirect("/picture/" + str(resource.id))

    if PictureRating.query.filter_by(resource=resource, user=current_user).first():
        PictureRating.query.filter_by(resource=resource, user=current_user).first().rating = rating
    else:
        # Create a new rating
        db.session.add(PictureRating(resource, current_user, rating))

    db.session.commit()

    return flask.redirect("/picture/" + str(resource.id))


@app.route("/picture/&lt;int:id&gt;/edit-metadata", methods=["POST"])
def edit_picture_post(id):
    resource = db.session.get(PictureResource, id)
    if resource is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    if resource.author != current_user and not current_user.admin:
        flask.abort(403)

    title = flask.request.form["title"]
    description = flask.request.form["description"]
    origin_url = flask.request.form["origin_url"]
    licence_ids = flask.request.form.getlist("licence")
    nature_id = flask.request.form["nature"]

    if not title:
        flask.flash("Enter a title")
        return flask.redirect(flask.request.url)

    if not description:
        description = ""

    if not nature_id:
        flask.flash("Select a picture type")
        return flask.redirect(flask.request.url)

    if not licence_ids:
        flask.flash("Select licences")
        return flask.redirect(flask.request.url)

    licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]
    if not any(licence.free for licence in licences):
        flask.flash("Select at least one free licence")
        return flask.redirect(flask.request.url)

    resource.title = title
    resource.description = description
    resource.origin_url = origin_url
    for licence_id in licence_ids:
        joiner = PictureLicence(resource, db.session.get(Licence, licence_id))
        db.session.add(joiner)
    resource.nature = db.session.get(PictureNature, nature_id)

    db.session.commit()

    return flask.redirect("/picture/" + str(resource.id))


@app.route("/picture/&lt;int:id&gt;/copy")
def copy_picture(id):
    resource = db.session.get(PictureResource, id)
    if resource is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    new_resource = PictureResource(resource.title, current_user, resource.description,
                                   resource.origin_url,
                                   [licence.licence_id for licence in resource.licences],
                                   resource.file_format,
                                   resource.nature)

    for region in resource.regions:
        db.session.add(PictureRegion(region.json, new_resource, region.object))

    db.session.commit()

    # Create a hard link for the new picture
    old_path = path.join(config.DATA_PATH, "pictures", str(resource.id))
    new_path = path.join(config.DATA_PATH, "pictures", str(new_resource.id))
    os.link(old_path, new_path)

    new_resource.width = resource.width
    new_resource.height = resource.height
    new_resource.copied_from = resource

    db.session.commit()

    return flask.redirect("/picture/" + str(new_resource.id))


@app.route("/gallery/&lt;int:id&gt;/")
def gallery(id):
    gallery = db.session.get(Gallery, id)
    if gallery is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))

    have_permission = current_user and (current_user == gallery.owner or current_user.admin or UserInGallery.query.filter_by(user=current_user, gallery=gallery).first())

    return flask.render_template("gallery.html", gallery=gallery,
                                 have_permission=have_permission)


@app.route("/create-gallery")
def create_gallery():
    if "username" not in flask.session:
        flask.flash("Log in to create galleries.")
        return flask.redirect("/accounts")

    return flask.render_template("create-gallery.html")


@app.route("/create-gallery", methods=["POST"])
def create_gallery_post():
    if not flask.session.get("username"):
        flask.abort(401)

    if not flask.request.form.get("title"):
        flask.flash("Enter a title")
        return flask.redirect(flask.request.url)

    description = flask.request.form.get("description", "")

    gallery = Gallery(flask.request.form["title"], description,
                      db.session.get(User, flask.session["username"]))
    db.session.add(gallery)
    db.session.commit()

    return flask.redirect("/gallery/" + str(gallery.id))


@app.route("/gallery/&lt;int:id&gt;/add-picture", methods=["POST"])
def gallery_add_picture(id):
    gallery = db.session.get(Gallery, id)
    if gallery is None:
        flask.abort(404)

    if "username" not in flask.session:
        flask.abort(401)

    if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
        flask.abort(403)

    picture_id = flask.request.form.get("picture_id")
    if "/" in picture_id:  # also allow full URLs
        picture_id = picture_id.rstrip("/").rpartition("/")[1]
    if not picture_id:
        flask.flash("Select a picture")
        return flask.redirect("/gallery/" + str(gallery.id))
    picture_id = int(picture_id)

    picture = db.session.get(PictureResource, picture_id)
    if picture is None:
        flask.flash("Invalid picture")
        return flask.redirect("/gallery/" + str(gallery.id))

    if PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first():
        flask.flash("This picture is already in the gallery")
        return flask.redirect("/gallery/" + str(gallery.id))

    db.session.add(PictureInGallery(picture, gallery))

    db.session.commit()

    return flask.redirect("/gallery/" + str(gallery.id))


@app.route("/gallery/&lt;int:id&gt;/remove-picture", methods=["POST"])
def gallery_remove_picture(id):
    gallery = db.session.get(Gallery, id)
    if gallery is None:
        flask.abort(404)

    if "username" not in flask.session:
        flask.abort(401)

    current_user = db.session.get(User, flask.session.get("username"))

    if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
        flask.abort(403)

    picture_id = int(flask.request.form.get("picture_id"))

    picture = db.session.get(PictureResource, picture_id)
    if picture is None:
        flask.flash("Invalid picture")
        return flask.redirect("/gallery/" + str(gallery.id))

    picture_in_gallery = PictureInGallery.query.filter_by(resource=picture,
                                                          gallery=gallery).first()
    if picture_in_gallery is None:
        flask.flash("This picture isn't in the gallery")
        return flask.redirect("/gallery/" + str(gallery.id))

    db.session.delete(picture_in_gallery)

    db.session.commit()

    return flask.redirect("/gallery/" + str(gallery.id))


@app.route("/gallery/&lt;int:id&gt;/add-pictures-from-query", methods=["POST"])
def gallery_add_from_query(id):
    gallery = db.session.get(Gallery, id)
    if gallery is None:
        flask.abort(404)

    if "username" not in flask.session:
        flask.abort(401)

    if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
        flask.abort(403)

    query_yaml = flask.request.form.get("query", "")

    yaml_parser = yaml.YAML()
    query_data = yaml_parser.load(query_yaml) or {}
    query = get_picture_query(query_data)

    pictures = query.all()

    count = 0

    for picture in pictures:
        if not PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first():
            db.session.add(PictureInGallery(picture, gallery))
            count += 1

    db.session.commit()

    flask.flash(f"Added {count} pictures to the gallery")

    return flask.redirect("/gallery/" + str(gallery.id))


@app.route("/gallery/&lt;int:id&gt;/users")
def gallery_users(id):
    gallery = db.session.get(Gallery, id)
    if gallery is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    have_permission = current_user and (current_user == gallery.owner or current_user.admin)

    return flask.render_template("gallery-users.html", gallery=gallery,
                                 have_permission=have_permission)


@app.route("/gallery/&lt;int:id&gt;/edit")
def edit_gallery(id):
    gallery = db.session.get(Gallery, id)
    if gallery is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    if current_user != gallery.owner and not current_user.admin:
        flask.abort(403)

    return flask.render_template("edit-gallery.html", gallery=gallery)


@app.route("/gallery/&lt;int:id&gt;/edit", methods=["POST"])
def edit_gallery_post(id):
    gallery = db.session.get(Gallery, id)
    if gallery is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    if current_user != gallery.owner and not current_user.admin:
        flask.abort(403)

    title = flask.request.form["title"]
    description = flask.request.form.get("description")

    if not title:
        flask.flash("Enter a title")
        return flask.redirect(flask.request.url)

    if not description:
        description = ""

    gallery.title = title
    gallery.description = description

    db.session.commit()

    return flask.redirect("/gallery/" + str(gallery.id))


@app.route("/gallery/&lt;int:id&gt;/users/add", methods=["POST"])
def gallery_add_user(id):
    gallery = db.session.get(Gallery, id)
    if gallery is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    if current_user != gallery.owner and not current_user.admin:
        flask.abort(403)

    username = flask.request.form.get("username")
    if username == gallery.owner_name:
        flask.flash("The owner is already in the gallery")
        return flask.redirect("/gallery/" + str(gallery.id) + "/users")

    user = db.session.get(User, username)
    if user is None:
        flask.flash("User not found")
        return flask.redirect("/gallery/" + str(gallery.id) + "/users")

    if UserInGallery.query.filter_by(user=user, gallery=gallery).first():
        flask.flash("User is already in the gallery")
        return flask.redirect("/gallery/" + str(gallery.id) + "/users")

    db.session.add(UserInGallery(user, gallery))

    db.session.commit()

    return flask.redirect("/gallery/" + str(gallery.id) + "/users")


@app.route("/gallery/&lt;int:id&gt;/users/remove", methods=["POST"])
def gallery_remove_user(id):
    gallery = db.session.get(Gallery, id)
    if gallery is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    if current_user != gallery.owner and not current_user.admin:
        flask.abort(403)

    username = flask.request.form.get("username")
    user = db.session.get(User, username)
    if user is None:
        flask.flash("User not found")
        return flask.redirect("/gallery/" + str(gallery.id) + "/users")

    user_in_gallery = UserInGallery.query.filter_by(user=user, gallery=gallery).first()
    if user_in_gallery is None:
        flask.flash("User is not in the gallery")
        return flask.redirect("/gallery/" + str(gallery.id) + "/users")

    db.session.delete(user_in_gallery)

    db.session.commit()

    return flask.redirect("/gallery/" + str(gallery.id) + "/users")


class APIError(Exception):
    def __init__(self, status_code, message):
        self.status_code = status_code
        self.message = message


def get_picture_query(query_data):
    query = db.session.query(PictureResource)

    def has_condition(id):
        descendants_cte = (
            db.select(PictureObject.id)
            .where(PictureObject.id == id)
            .cte(name=f"descendants_cte_{id}", recursive=True)
        )

        descendants_cte = descendants_cte.union_all(
            db.select(PictureObjectInheritance.child_id)
            .where(PictureObjectInheritance.parent_id == descendants_cte.c.id)
        )

        return PictureResource.regions.any(
            PictureRegion.object_id.in_(
                db.select(descendants_cte.c.id)
            )
        )

    requirement_conditions = {
        # Has an object with the ID in the given list
        "has_object": lambda value: PictureResource.regions.any(
                PictureRegion.object_id.in_(value)),
        # Has an object with the ID in the given list, or a subtype of it
        "has": lambda value: db.or_(*[has_condition(id) for id in value]),
        "nature": lambda value: PictureResource.nature_id.in_(value),
        "licence": lambda value: PictureResource.licences.any(
                PictureLicence.licence_id.in_(value)),
        "author": lambda value: PictureResource.author_name.in_(value),
        "title": lambda value: PictureResource.title.ilike(value),
        "description": lambda value: PictureResource.description.ilike(value),
        "origin_url": lambda value: db.func.lower(db.func.substr(
                PictureResource.origin_url,
                db.func.length(db.func.split_part(PictureResource.origin_url, "://", 1)) + 4
        )).in_(value),
        "above_width": lambda value: PictureResource.width &gt;= value,
        "below_width": lambda value: PictureResource.width &lt;= value,
        "above_height": lambda value: PictureResource.height &gt;= value,
        "below_height": lambda value: PictureResource.height &lt;= value,
        "before_date": lambda value: PictureResource.timestamp &lt;= datetime.utcfromtimestamp(
                value),
        "after_date": lambda value: PictureResource.timestamp &gt;= datetime.utcfromtimestamp(
                value),
        "in_gallery": lambda value: PictureResource.galleries.any(PictureInGallery.gallery_id.in_(value)),
        "above_rating": lambda value: db.select(db.func.coalesce(db.func.avg(PictureRating.rating), 5)).where(PictureRating.resource_id == PictureResource.id).scalar_subquery() &gt;= value,
        "below_rating": lambda value: db.select(db.func.coalesce(db.func.avg(PictureRating.rating), 0)).where(PictureRating.resource_id == PictureResource.id).scalar_subquery() &lt;= value,
        "above_rating_count": lambda value: db.select(db.func.count(PictureRating.id)).where(PictureRating.resource_id == PictureResource.id).correlate(PictureResource).scalar_subquery() &gt;= value,
        "below_rating_count": lambda value: db.select(db.func.count(PictureRating.id)).where(PictureRating.resource_id == PictureResource.id).correlate(PictureResource).scalar_subquery() &lt;= value,
        "above_region_count": lambda value: db.select(db.func.count(PictureRegion.id)).where(PictureRegion.resource_id == PictureResource.id).correlate(PictureResource).scalar_subquery() &gt;= value,
        "below_region_count": lambda value: db.select(db.func.count(PictureRegion.id)).where(PictureRegion.resource_id == PictureResource.id).correlate(PictureResource).scalar_subquery() &lt;= value,
        "copied_from": lambda value: PictureResource.copied_from_id.in_(value),
    }

    if "want" in query_data:
        for i in query_data["want"]:
            if len(i) != 1:
                raise APIError(400, "Each requirement must have exactly one key")
            requirement, value = list(i.items())[0]
            if requirement not in requirement_conditions:
                raise APIError(400, f"Unknown requirement type: {requirement}")

            condition = requirement_conditions[requirement]
            query = query.filter(condition(value))
    if "exclude" in query_data:
        for i in query_data["exclude"]:
            if len(i) != 1:
                raise APIError(400, "Each exclusion must have exactly one key")
            requirement, value = list(i.items())[0]
            if requirement not in requirement_conditions:
                raise APIError(400, f"Unknown requirement type: {requirement}")

            condition = requirement_conditions[requirement]
            query = query.filter(~condition(value))
    if not query_data.get("include_obsolete", False):
        query = query.filter(PictureResource.replaced_by_id.is_(None))

    return query


@app.route("/query-pictures")
def graphical_query_pictures():
    return flask.render_template("graphical-query-pictures.html")


@app.route("/query-pictures-results")
def graphical_query_pictures_results():
    query_yaml = flask.request.args.get("query", "")
    yaml_parser = yaml.YAML()
    query_data = yaml_parser.load(query_yaml) or {}
    try:
        query = get_picture_query(query_data)
    except APIError as e:
        flask.abort(e.status_code)

    page = int(flask.request.args.get("page", 1))
    per_page = int(flask.request.args.get("per_page", 16))

    resources = query.paginate(page=page, per_page=per_page)

    return flask.render_template("graphical-query-pictures-results.html", resources=resources,
                                 query=query_yaml,
                                 page_number=page, page_length=per_page,
                                 num_pages=resources.pages,
                                 prev_page=resources.prev_num, next_page=resources.next_num)


@app.route("/raw/picture/&lt;int:id&gt;")
def raw_picture(id):
    resource = db.session.get(PictureResource, id)
    if resource is None:
        flask.abort(404)

    response = flask.send_from_directory(path.join(config.DATA_PATH, "pictures"),
                                         str(resource.id))
    response.mimetype = resource.file_format

    return response


@app.route("/object/")
def graphical_object_types():
    return flask.render_template("object-types.html", objects=PictureObject.query.all())


@app.route("/api/object-types")
def object_types():
    objects = db.session.query(PictureObject).all()
    return flask.jsonify({object.id: object.description for object in objects})


@app.route("/api/query-pictures", methods=["POST"])  # sadly GET can't have a body
def query_pictures():
    offset = int(flask.request.args.get("offset", 0))
    limit = int(flask.request.args.get("limit", 16))
    ordering = flask.request.args.get("ordering", "date-desc")

    yaml_parser = yaml.YAML()
    query_data = yaml_parser.load(flask.request.data) or {}
    try:
        query = get_picture_query(query_data)
    except APIError as e:
        return flask.jsonify({"error": e.message}), e.status_code

    rating_count_subquery = db.select(db.func.count(PictureRating.id)).where(
        PictureRating.resource_id == PictureResource.id).scalar_subquery()
    region_count_subquery = db.select(db.func.count(PictureRegion.id)).where(
        PictureRegion.resource_id == PictureResource.id).scalar_subquery()
    rating_subquery = db.select(db.func.coalesce(db.func.avg(PictureRating.rating), 0)).where(
        PictureRating.resource_id == PictureResource.id).scalar_subquery()

    match ordering:
        case "date-desc":
            query = query.order_by(PictureResource.timestamp.desc())
        case "date-asc":
            query = query.order_by(PictureResource.timestamp.asc())
        case "title-asc":
            query = query.order_by(PictureResource.title.asc())
        case "title-desc":
            query = query.order_by(PictureResource.title.desc())
        case "random":
            query = query.order_by(db.func.random())
        case "number-regions-desc":
            query = query.order_by(region_count_subquery.desc())
        case "number-regions-asc":
            query = query.order_by(region_count_subquery.asc())
        case "rating-desc":
            query = query.order_by(rating_subquery.desc())
        case "rating-asc":
            query = query.order_by(rating_subquery.asc())
        case "number-ratings-desc":
            query = query.order_by(rating_count_subquery.desc())
        case "number-ratings-asc":
            query = query.order_by(rating_count_subquery.asc())

    query = query.offset(offset).limit(limit)
    resources = query.all()

    json_response = {
        "date_generated": datetime.utcnow().timestamp(),
        "resources": [],
        "offset": offset,
        "limit": limit,
    }

    json_resources = json_response["resources"]

    for resource in resources:
        json_resource = {
            "id": resource.id,
            "title": resource.title,
            "description": resource.description,
            "timestamp": resource.timestamp.timestamp(),
            "origin_url": resource.origin_url,
            "author": resource.author_name,
            "file_format": resource.file_format,
            "width": resource.width,
            "height": resource.height,
            "nature": resource.nature_id,
            "licences": [licence.licence_id for licence in resource.licences],
            "replaces": resource.replaces_id,
            "replaced_by": resource.replaced_by_id,
            "regions": [],
            "download": config.ROOT_URL + flask.url_for("raw_picture", id=resource.id),
        }
        for region in resource.regions:
            json_resource["regions"].append({
                "object": region.object_id,
                "type": region.json["type"],
                "shape": region.json["shape"],
            })

        json_resources.append(json_resource)

    return flask.jsonify(json_response)


@app.route("/api/picture/&lt;int:id&gt;/")
def api_picture(id):
    resource = db.session.get(PictureResource, id)
    if resource is None:
        flask.abort(404)

    json_resource = {
        "id": resource.id,
        "title": resource.title,
        "description": resource.description,
        "timestamp": resource.timestamp.timestamp(),
        "origin_url": resource.origin_url,
        "author": resource.author_name,
        "file_format": resource.file_format,
        "width": resource.width,
        "height": resource.height,
        "nature": resource.nature_id,
        "licences": [licence.licence_id for licence in resource.licences],
        "replaces": resource.replaces_id,
        "replaced_by": resource.replaced_by_id,
        "regions": [],
        "download": config.ROOT_URL + flask.url_for("raw_picture", id=resource.id),
        "rating_average": resource.average_rating,
        "rating_count": resource.rating_totals,
    }
    for region in resource.regions:
        json_resource["regions"].append({
            "object": region.object_id,
            "type": region.json["type"],
            "shape": region.json["shape"],
        })

    return flask.jsonify(json_resource)


@app.route("/api/licence/")
def api_licences():
    licences = db.session.query(Licence).all()
    json_licences = {
        licence.id: {
            "title": licence.title,
            "free": licence.free,
            "pinned": licence.pinned,
        } for licence in licences
    }

    return flask.jsonify(json_licences)


@app.route("/api/licence/&lt;id&gt;/")
def api_licence(id):
    licence = db.session.get(Licence, id)
    if licence is None:
        flask.abort(404)

    json_licence = {
        "id": licence.id,
        "title": licence.title,
        "description": licence.description,
        "info_url": licence.info_url,
        "legalese_url": licence.url,
        "free": licence.free,
        "logo_url": licence.logo_url,
        "pinned": licence.pinned,
    }

    return flask.jsonify(json_licence)


@app.route("/api/nature/")
def api_natures():
    natures = db.session.query(PictureNature).all()
    json_natures = {
        nature.id: nature.description for nature in natures
    }

    return flask.jsonify(json_natures)


@app.route("/api/user/")
def api_users():
    offset = int(flask.request.args.get("offset", 0))
    limit = int(flask.request.args.get("limit", 16))

    users = db.session.query(User).offset(offset).limit(limit).all()

    json_users = {
        user.username: {
            "admin": user.admin,
        } for user in users
    }

    return flask.jsonify(json_users)


@app.route("/api/user/&lt;username&gt;/")
def api_user(username):
    user = db.session.get(User, username)
    if user is None:
        flask.abort(404)

    json_user = {
        "username": user.username,
        "admin": user.admin,
        "joined": user.joined_timestamp.timestamp(),
    }

    return flask.jsonify(json_user)


@app.route("/api/login", methods=["POST"])
def api_login():
    username = flask.request.json["username"]
    password = flask.request.json["password"]

    user = db.session.get(User, username)

    if user is None:
        return flask.jsonify({"error": "This username is not registered. To prevent spam, you must use the HTML interface to register."}), 401

    if not bcrypt.check_password_hash(user.password_hashed, password):
        return flask.jsonify({"error": "Incorrect password"}), 401

    flask.session["username"] = username

    return flask.jsonify({"message": "You have been logged in. Your HTTP client must support cookies to use features of this API that require authentication."})


@app.route("/api/logout", methods=["POST"])
def api_logout():
    flask.session.pop("username", None)
    return flask.jsonify({"message": "You have been logged out."})


@app.route("/api/upload", methods=["POST"])
def api_upload():
    if "username" not in flask.session:
        return flask.jsonify({"error": "You must be logged in to upload pictures"}), 401

    json_ = json.loads(flask.request.form["json"])
    title = json_["title"]
    description = json_.get("description", "")
    origin_url = json_.get("origin_url", "")
    author = db.session.get(User, flask.session["username"])
    licence_ids = json_["licence"]
    nature_id = json_["nature"]
    file = flask.request.files["file"]

    if not file or not file.filename:
        return flask.jsonify({"error": "An image file must be uploaded"}), 400

    if not file.mimetype.startswith("image/") or file.mimetype == "image/svg+xml":
        return flask.jsonify({"error": "Only bitmap images are supported"}), 400

    if not title:
        return flask.jsonify({"error": "Give a title"}), 400

    if not description:
        description = ""

    if not nature_id:
        return flask.jsonify({"error": "Give a picture type"}), 400

    if not licence_ids:
        return flask.jsonify({"error": "Give licences"}), 400

    licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]
    if not any(licence.free for licence in licences):
        return flask.jsonify({"error": "Use at least one free licence"}), 400

    resource = PictureResource(title, author, description, origin_url, licence_ids,
                               file.mimetype,
                               db.session.get(PictureNature, nature_id))
    db.session.add(resource)
    db.session.commit()
    file.save(path.join(config.DATA_PATH, "pictures", str(resource.id)))
    pil_image = Image.open(path.join(config.DATA_PATH, "pictures", str(resource.id)))
    resource.width, resource.height = pil_image.size
    db.session.commit()

    if json_.get("annotations"):
        try:
            resource.put_annotations(json_["annotations"])
            db.session.commit()
        except json.JSONDecodeError:
            return flask.jsonify({"error": "Invalid annotations"}), 400

    return flask.jsonify({"message": "Picture uploaded successfully", "id": resource.id})


@app.route("/api/picture/&lt;int:id&gt;/update", methods=["POST"])
def api_update_picture(id):
    resource = db.session.get(PictureResource, id)
    if resource is None:
        return flask.jsonify({"error": "Picture not found"}), 404
    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        return flask.jsonify({"error": "You must be logged in to edit pictures"}), 401
    if resource.author != current_user and not current_user.admin:
        return flask.jsonify({"error": "You are not the author of this picture"}), 403

    title = flask.request.json.get("title", resource.title)
    description = flask.request.json.get("description", resource.description)
    origin_url = flask.request.json.get("origin_url", resource.origin_url)
    licence_ids = flask.request.json.get("licence", [licence.licence_id for licence in resource.licences])
    nature_id = flask.request.json.get("nature", resource.nature_id)

    if not title:
        return flask.jsonify({"error": "Give a title"}), 400

    if not description:
        description = ""

    if not nature_id:
        return flask.jsonify({"error": "Give a picture type"}), 400

    if not licence_ids:
        return flask.jsonify({"error": "Give licences"}), 400

    licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]

    if not any(licence.free for licence in licences):
        return flask.jsonify({"error": "Use at least one free licence"}), 400

    resource.title = title
    resource.description = description
    resource.origin_url = origin_url
    resource.licences = licences
    resource.nature = db.session.get(PictureNature, nature_id)

    db.session.commit()

    return flask.jsonify({"message": "Picture updated successfully"})


@app.route("/api/picture/&lt;int:id&gt;/rate", methods=["POST"])
def api_rate_picture(id):
    resource = db.session.get(PictureResource, id)
    if resource is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    rating = int(flask.request.json.get("rating", 0))

    if not rating:
        # Delete the existing rating
        if PictureRating.query.filter_by(resource=resource, user=current_user).first():
            db.session.delete(PictureRating.query.filter_by(resource=resource,
                                                             user=current_user).first())
            db.session.commit()

        return flask.jsonify({"message": "Existing rating removed"})

    if not 1 &lt;= rating &lt;= 5:
        flask.flash("Invalid rating")
        return flask.jsonify({"error": "Invalid rating"}), 400

    if PictureRating.query.filter_by(resource=resource, user=current_user).first():
        PictureRating.query.filter_by(resource=resource, user=current_user).first().rating = rating
    else:
        # Create a new rating
        db.session.add(PictureRating(resource, current_user, rating))

    db.session.commit()

    return flask.jsonify({"message": "Rating saved"})


@app.route("/api/gallery/&lt;int:id&gt;/")
def api_gallery(id):
    gallery = db.session.get(Gallery, id)
    if gallery is None:
        flask.abort(404)

    json_gallery = {
        "id": gallery.id,
        "title": gallery.title,
        "description": gallery.description,
        "owner": gallery.owner_name,
        "users": [user.username for user in gallery.users],
    }

    return flask.jsonify(json_gallery)


@app.route("/api/gallery/&lt;int:id&gt;/edit", methods=["POST"])
def api_edit_gallery(id):
    gallery = db.session.get(Gallery, id)
    if gallery is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    if current_user != gallery.owner and not current_user.admin:
        flask.abort(403)

    title = flask.request.json.get("title", gallery.title)
    description = flask.request.json.get("description", gallery.description)

    if not title:
        return flask.jsonify({"error": "Give a title"}), 400

    if not description:
        description = ""

    gallery.title = title
    gallery.description = description

    db.session.commit()

    return flask.jsonify({"message": "Gallery updated successfully"})


@app.route("/api/new-gallery", methods=["POST"])
def api_new_gallery():
    if "username" not in flask.session:
        return flask.jsonify({"error": "You must be logged in to create galleries"}), 401

    title = flask.request.json.get("title")
    description = flask.request.json.get("description", "")

    if not title:
        return flask.jsonify({"error": "Give a title"}), 400

    gallery = Gallery(title, description, db.session.get(User, flask.session["username"]))
    db.session.add(gallery)
    db.session.commit()

    return flask.jsonify({"message": "Gallery created successfully", "id": gallery.id})


@app.route("/api/gallery/&lt;int:id&gt;/add-picture", methods=["POST"])
def api_gallery_add_picture(id):
    gallery = db.session.get(Gallery, id)
    if gallery is None:
        flask.abort(404)

    if "username" not in flask.session:
        return flask.jsonify({"error": "You must be logged in to add pictures to galleries"}), 401

    current_user = db.session.get(User, flask.session.get("username"))

    if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
        return flask.jsonify({"error": "You do not have permission to add pictures to this gallery"}), 403

    picture_id = flask.request.json.get("picture_id")

    try:
        picture_id = int(picture_id)
    except ValueError:
        return flask.jsonify({"error": "Invalid picture ID"}), 400

    picture = db.session.get(PictureResource, picture_id)
    if picture is None:
        return flask.jsonify({"error": "The picture doesn't exist"}), 404

    if PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first():
        return flask.jsonify({"error": "This picture is already in the gallery"}), 400

    db.session.add(PictureInGallery(picture, gallery))

    db.session.commit()

    return flask.jsonify({"message": "Picture added to gallery"})


@app.route("/api/gallery/&lt;int:id&gt;/remove-picture", methods=["POST"])
def api_gallery_remove_picture(id):
    gallery = db.session.get(Gallery, id)
    if gallery is None:
        flask.abort(404)

    if "username" not in flask.session:
        return flask.jsonify({"error": "You must be logged in to remove pictures from galleries"}), 401

    current_user = db.session.get(User, flask.session.get("username"))

    if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
        return flask.jsonify({"error": "You do not have permission to remove pictures from this gallery"}), 403

    picture_id = flask.request.json.get("picture_id")

    try:
        picture_id = int(picture_id)
    except ValueError:
        return flask.jsonify({"error": "Invalid picture ID"}), 400

    picture = db.session.get(PictureResource, picture_id)
    if picture is None:
        return flask.jsonify({"error": "The picture doesn't exist"}), 404

    picture_in_gallery = PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first()
    if picture_in_gallery is None:
        return flask.jsonify({"error": "This picture isn't in the gallery"}), 400

    db.session.delete(picture_in_gallery)

    db.session.commit()

    return flask.jsonify({"message": "Picture removed from gallery"})


@app.route("/api/gallery/&lt;int:id&gt;/users/add", methods=["POST"])
def api_gallery_add_user(id):
    gallery = db.session.get(Gallery, id)
    if gallery is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    if current_user != gallery.owner and not current_user.admin:
        flask.abort(403)

    username = flask.request.json.get("username")
    if username == gallery.owner_name:
        return flask.jsonify({"error": "The owner cannot be added to trusted users"}), 400

    user = db.session.get(User, username)
    if user is None:
        return flask.jsonify({"error": "User not found"}), 404

    if UserInGallery.query.filter_by(user=user, gallery=gallery).first():
        return flask.jsonify({"error": "User is already in the gallery"}), 400

    db.session.add(UserInGallery(user, gallery))

    db.session.commit()

    return flask.jsonify({"message": "User added to gallery"})


@app.route("/api/gallery/&lt;int:id&gt;/users/remove", methods=["POST"])
def api_gallery_remove_user(id):
    gallery = db.session.get(Gallery, id)
    if gallery is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    if current_user != gallery.owner and not current_user.admin:
        flask.abort(403)

    username = flask.request.json.get("username")
    user = db.session.get(User, username)
    if user is None:
        return flask.jsonify({"error": "User not found"}), 404

    user_in_gallery = UserInGallery.query.filter_by(user=user, gallery=gallery).first()
    if user_in_gallery is None:
        return flask.jsonify({"error": "User is not in the gallery"}), 400

    db.session.delete(user_in_gallery)

    db.session.commit()

    return flask.jsonify({"message": "User removed from gallery"})


@app.route("/api/gallery/&lt;int:id&gt;/delete", methods=["POST"])
def api_delete_gallery(id):
    gallery = db.session.get(Gallery, id)
    if gallery is None:
        flask.abort(404)

    current_user = db.session.get(User, flask.session.get("username"))
    if current_user is None:
        flask.abort(401)

    if current_user != gallery.owner and not current_user.admin:
        flask.abort(403)

    for picture_in_gallery in gallery.pictures:
        db.session.delete(picture_in_gallery)

    for user_in_gallery in gallery.users:
        db.session.delete(user_in_gallery)

    db.session.delete(gallery)

    db.session.commit()

    return flask.jsonify({"message": "Gallery deleted"})

</pre></body></html>