Python script, ASCII text executable
        
            1
            import json 
        
            2
            from datetime import datetime 
        
            3
            from email.policy import default 
        
            4
            from time import perf_counter 
        
            5
            import flask 
        
            7
            from flask_sqlalchemy import SQLAlchemy 
        
            8
            from flask_bcrypt import Bcrypt 
        
            9
            from flask_httpauth import HTTPBasicAuth 
        
            10
            from markupsafe import escape, Markup 
        
            11
            from flask_migrate import Migrate, current 
        
            12
            from jinja2_fragments.flask import render_block 
        
            13
            from sqlalchemy import false 
        
            14
            from sqlalchemy.orm import backref 
        
            15
            import sqlalchemy.dialects.postgresql 
        
            16
            from os import path 
        
            17
            import os 
        
            18
            from urllib.parse import urlencode 
        
            19
            import mimetypes 
        
            20
            import ruamel.yaml as yaml 
        
            21
            from PIL import Image 
        
            23
            from sqlalchemy.orm.persistence import post_update 
        
            24
            from sqlalchemy.sql.functions import current_user 
        
            25
            import config 
        
            27
            import markdown 
        
            28
            app = flask.Flask(__name__) 
        
            30
            bcrypt = Bcrypt(app) 
        
            31
            app.config["SQLALCHEMY_DATABASE_URI"] = config.DB_URI 
        
            33
            app.config["SECRET_KEY"] = config.DB_PASSWORD 
        
            34
            db = SQLAlchemy(app) 
        
            36
            migrate = Migrate(app, db) 
        
            37
            @app.template_filter("split") 
        
            40
            def split(value, separator=None, maxsplit=-1): 
        
            41
                return value.split(separator, maxsplit) 
        
            42
            @app.template_filter("median") 
        
            45
            def median(value): 
        
            46
                value = list(value)  # prevent generators 
        
            47
                return sorted(value)[len(value) // 2] 
        
            48
            @app.template_filter("set") 
        
            51
            def set_filter(value): 
        
            52
                return set(value) 
        
            53
            @app.template_global() 
        
            56
            def modify_query(**new_values): 
        
            57
                args = flask.request.args.copy() 
        
            58
                for key, value in new_values.items(): 
        
            59
                    args[key] = value 
        
            60
                return f"{flask.request.path}?{urlencode(args)}" 
        
            62
            @app.context_processor 
        
            65
            def default_variables(): 
        
            66
                return { 
        
            67
                    "current_user": db.session.get(User, flask.session.get("username")), 
        
            68
                } 
        
            69
            with app.app_context(): 
        
            72
                class User(db.Model): 
        
            73
                    username = db.Column(db.String(32), unique=True, nullable=False, primary_key=True) 
        
            74
                    password_hashed = db.Column(db.String(60), nullable=False) 
        
            75
                    admin = db.Column(db.Boolean, nullable=False, default=False, server_default="false") 
        
            76
                    pictures = db.relationship("PictureResource", back_populates="author") 
        
            77
                    joined_timestamp = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) 
        
            78
                    galleries = db.relationship("Gallery", back_populates="owner") 
        
            79
                    galleries_joined = db.relationship("UserInGallery", back_populates="user") 
        
            80
                    ratings = db.relationship("PictureRating", back_populates="user") 
        
            81
                    def __init__(self, username, password): 
        
            83
                        self.username = username 
        
            84
                        self.password_hashed = bcrypt.generate_password_hash(password).decode("utf-8") 
        
            85
                    @property 
        
            87
                    def formatted_name(self): 
        
            88
                        if self.admin: 
        
            89
                            return self.username + "*" 
        
            90
                        return self.username 
        
            91
                class Licence(db.Model): 
        
            94
                    id = db.Column(db.String(64), primary_key=True)  # SPDX identifier 
        
            95
                    title = db.Column(db.UnicodeText, nullable=False)  # the official name of the licence 
        
            96
                    description = db.Column(db.UnicodeText, 
        
            97
                                            nullable=False)  # brief description of its permissions and restrictions 
        
            98
                    info_url = db.Column(db.String(1024), 
        
            99
                                         nullable=False)  # the URL to a page with general information about the licence 
        
            100
                    url = db.Column(db.String(1024), 
        
            101
                                    nullable=True)  # the URL to a page with the full text of the licence and more information 
        
            102
                    pictures = db.relationship("PictureLicence", back_populates="licence") 
        
            103
                    free = db.Column(db.Boolean, nullable=False, 
        
            104
                                     default=False)  # whether the licence is free or not 
        
            105
                    logo_url = db.Column(db.String(1024), nullable=True)  # URL to the logo of the licence 
        
            106
                    pinned = db.Column(db.Boolean, nullable=False, 
        
            107
                                       default=False)  # whether the licence should be shown at the top of the list 
        
            108
                    def __init__(self, id, title, description, info_url, url, free, logo_url=None, 
        
            110
                                 pinned=False): 
        
            111
                        self.id = id 
        
            112
                        self.title = title 
        
            113
                        self.description = description 
        
            114
                        self.info_url = info_url 
        
            115
                        self.url = url 
        
            116
                        self.free = free 
        
            117
                        self.logo_url = logo_url 
        
            118
                        self.pinned = pinned 
        
            119
                class PictureLicence(db.Model): 
        
            122
                    id = db.Column(db.Integer, primary_key=True, autoincrement=True) 
        
            123
                    resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id")) 
        
            125
                    licence_id = db.Column(db.String(64), db.ForeignKey("licence.id")) 
        
            126
                    resource = db.relationship("PictureResource", back_populates="licences") 
        
            128
                    licence = db.relationship("Licence", back_populates="pictures") 
        
            129
                    def __init__(self, resource, licence): 
        
            131
                        self.resource = resource 
        
            132
                        self.licence = licence 
        
            133
                class Resource(db.Model): 
        
            136
                    __abstract__ = True 
        
            137
                    id = db.Column(db.Integer, primary_key=True, autoincrement=True) 
        
            139
                    title = db.Column(db.UnicodeText, nullable=False) 
        
            140
                    description = db.Column(db.UnicodeText, nullable=False) 
        
            141
                    timestamp = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) 
        
            142
                    origin_url = db.Column(db.String(2048), 
        
            143
                                           nullable=True)  # should be left empty if it's original or the source is unknown but public domain 
        
            144
                class PictureNature(db.Model): 
        
            147
                    # Examples: 
        
            148
                    # "photo", "paper-scan", "2d-art-photo", "sculpture-photo", "computer-3d", "computer-painting", 
        
            149
                    # "computer-line-art", "diagram", "infographic", "text", "map", "chart-graph", "screen-capture", 
        
            150
                    # "screen-photo", "pattern", "collage", "ai", and so on 
        
            151
                    id = db.Column(db.String(64), primary_key=True) 
        
            152
                    description = db.Column(db.UnicodeText, nullable=False) 
        
            153
                    resources = db.relationship("PictureResource", back_populates="nature") 
        
            154
                    def __init__(self, id, description): 
        
            156
                        self.id = id 
        
            157
                        self.description = description 
        
            158
                class PictureObjectInheritance(db.Model): 
        
            161
                    parent_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"), 
        
            162
                                          primary_key=True) 
        
            163
                    child_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"), 
        
            164
                                         primary_key=True) 
        
            165
                    parent = db.relationship("PictureObject", foreign_keys=[parent_id], 
        
            167
                                             back_populates="child_links") 
        
            168
                    child = db.relationship("PictureObject", foreign_keys=[child_id], 
        
            169
                                            back_populates="parent_links") 
        
            170
                    def __init__(self, parent, child): 
        
            172
                        self.parent = parent 
        
            173
                        self.child = child 
        
            174
                class PictureObject(db.Model): 
        
            177
                    id = db.Column(db.String(64), primary_key=True) 
        
            178
                    description = db.Column(db.UnicodeText, nullable=False) 
        
            179
                    child_links = db.relationship("PictureObjectInheritance", 
        
            181
                                                  foreign_keys=[PictureObjectInheritance.parent_id], 
        
            182
                                                  back_populates="parent") 
        
            183
                    parent_links = db.relationship("PictureObjectInheritance", 
        
            184
                                                   foreign_keys=[PictureObjectInheritance.child_id], 
        
            185
                                                   back_populates="child") 
        
            186
                    def __init__(self, id, description): 
        
            188
                        self.id = id 
        
            189
                        self.description = description 
        
            190
                class PictureRegion(db.Model): 
        
            193
                    # This is for picture region annotations 
        
            194
                    id = db.Column(db.Integer, primary_key=True, autoincrement=True) 
        
            195
                    json = db.Column(sqlalchemy.dialects.postgresql.JSONB, nullable=False) 
        
            196
                    resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), 
        
            198
                                            nullable=False) 
        
            199
                    object_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"), nullable=True) 
        
            200
                    resource = db.relationship("PictureResource", backref="regions") 
        
            202
                    object = db.relationship("PictureObject", backref="regions") 
        
            203
                    def __init__(self, json, resource, object): 
        
            205
                        self.json = json 
        
            206
                        self.resource = resource 
        
            207
                        self.object = object 
        
            208
                class PictureResource(Resource): 
        
            211
                    # This is only for bitmap pictures. Vectors will be stored under a different model 
        
            212
                    # File name is the ID in the picture directory under data, without an extension 
        
            213
                    file_format = db.Column(db.String(64), nullable=False)  # MIME type 
        
            214
                    width = db.Column(db.Integer, nullable=False) 
        
            215
                    height = db.Column(db.Integer, nullable=False) 
        
            216
                    nature_id = db.Column(db.String(32), db.ForeignKey("picture_nature.id"), nullable=True) 
        
            217
                    author_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 
        
            218
                    author = db.relationship("User", back_populates="pictures") 
        
            219
                    nature = db.relationship("PictureNature", back_populates="resources") 
        
            221
                    replaces_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), nullable=True) 
        
            223
                    replaced_by_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), 
        
            224
                                               nullable=True) 
        
            225
                    replaces = db.relationship("PictureResource", remote_side="PictureResource.id", 
        
            227
                                               foreign_keys=[replaces_id], back_populates="replaced_by", 
        
            228
                                               post_update=True) 
        
            229
                    replaced_by = db.relationship("PictureResource", remote_side="PictureResource.id", 
        
            230
                                                  foreign_keys=[replaced_by_id], post_update=True) 
        
            231
                    copied_from_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), 
        
            233
                                               nullable=True) 
        
            234
                    copied_from = db.relationship("PictureResource", remote_side="PictureResource.id", 
        
            235
                                                  backref="copies", foreign_keys=[copied_from_id]) 
        
            236
                    licences = db.relationship("PictureLicence", back_populates="resource") 
        
            238
                    galleries = db.relationship("PictureInGallery", back_populates="resource") 
        
            239
                    ratings = db.relationship("PictureRating", back_populates="resource") 
        
            240
                    def __init__(self, title, author, description, origin_url, licence_ids, mime, 
        
            242
                                 nature=None): 
        
            243
                        self.title = title 
        
            244
                        self.author = author 
        
            245
                        self.description = description 
        
            246
                        self.origin_url = origin_url 
        
            247
                        self.file_format = mime 
        
            248
                        self.width = self.height = 0 
        
            249
                        self.nature = nature 
        
            250
                        db.session.add(self) 
        
            251
                        db.session.commit() 
        
            252
                        for licence_id in licence_ids: 
        
            253
                            joiner = PictureLicence(self, db.session.get(Licence, licence_id)) 
        
            254
                            db.session.add(joiner) 
        
            255
                    def put_annotations(self, json): 
        
            257
                        # Delete all previous annotations 
        
            258
                        db.session.query(PictureRegion).filter_by(resource_id=self.id).delete() 
        
            259
                        for region in json: 
        
            261
                            object_id = region["object"] 
        
            262
                            picture_object = db.session.get(PictureObject, object_id) 
        
            263
                            region_data = { 
        
            265
                                "type": region["type"], 
        
            266
                                "shape": region["shape"], 
        
            267
                            } 
        
            268
                            region_row = PictureRegion(region_data, self, picture_object) 
        
            270
                            db.session.add(region_row) 
        
            271
                    @property 
        
            273
                    def average_rating(self): 
        
            274
                        if not self.ratings: 
        
            275
                            return None 
        
            276
                        return db.session.query(db.func.avg(PictureRating.rating)).filter_by(resource=self).scalar() 
        
            277
                    @property 
        
            279
                    def rating_totals(self): 
        
            280
                        all_ratings = db.session.query(PictureRating.rating).filter_by(resource=self) 
        
            281
                        return {rating: all_ratings.filter_by(rating=rating).count() for rating in range(1, 6)} 
        
            282
                    @property 
        
            284
                    def stars(self): 
        
            285
                        if not self.ratings: 
        
            286
                            return 0 
        
            287
                        average = self.average_rating 
        
            288
                        whole_stars = int(average) 
        
            289
                        partial_star = average - whole_stars 
        
            290
                        return [100] * whole_stars + [int(partial_star * 100)] + [0] * (4 - whole_stars) 
        
            292
                class PictureInGallery(db.Model): 
        
            295
                    id = db.Column(db.Integer, primary_key=True, autoincrement=True) 
        
            296
                    resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), 
        
            297
                                            nullable=False) 
        
            298
                    gallery_id = db.Column(db.Integer, db.ForeignKey("gallery.id"), nullable=False) 
        
            299
                    resource = db.relationship("PictureResource") 
        
            301
                    gallery = db.relationship("Gallery") 
        
            302
                    def __init__(self, resource, gallery): 
        
            304
                        self.resource = resource 
        
            305
                        self.gallery = gallery 
        
            306
                class UserInGallery(db.Model): 
        
            309
                    id = db.Column(db.Integer, primary_key=True, autoincrement=True) 
        
            310
                    username = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 
        
            311
                    gallery_id = db.Column(db.Integer, db.ForeignKey("gallery.id"), nullable=False) 
        
            312
                    user = db.relationship("User") 
        
            314
                    gallery = db.relationship("Gallery") 
        
            315
                    def __init__(self, user, gallery): 
        
            317
                        self.user = user 
        
            318
                        self.gallery = gallery 
        
            319
                class Gallery(db.Model): 
        
            322
                    id = db.Column(db.Integer, primary_key=True, autoincrement=True) 
        
            323
                    title = db.Column(db.UnicodeText, nullable=False) 
        
            324
                    description = db.Column(db.UnicodeText, nullable=False) 
        
            325
                    pictures = db.relationship("PictureInGallery", back_populates="gallery") 
        
            326
                    owner_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 
        
            327
                    owner = db.relationship("User", back_populates="galleries") 
        
            328
                    users = db.relationship("UserInGallery", back_populates="gallery") 
        
            329
                    def __init__(self, title, description, owner): 
        
            331
                        self.title = title 
        
            332
                        self.description = description 
        
            333
                        self.owner = owner 
        
            334
                class PictureRating(db.Model): 
        
            337
                    id = db.Column(db.Integer, primary_key=True, autoincrement=True) 
        
            338
                    resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), nullable=False) 
        
            339
                    username = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 
        
            340
                    rating = db.Column(db.Integer, db.CheckConstraint("rating >= 1 AND rating <= 5"), 
        
            341
                                       nullable=False) 
        
            342
                    resource = db.relationship("PictureResource", back_populates="ratings") 
        
            344
                    user = db.relationship("User", back_populates="ratings") 
        
            345
                    def __init__(self, resource, user, rating): 
        
            347
                        self.resource = resource 
        
            348
                        self.user = user 
        
            349
                        self.rating = rating 
        
            350
            @app.route("/") 
        
            353
            def index(): 
        
            354
                return flask.render_template("home.html", resources=PictureResource.query.order_by( 
        
            355
                    db.func.random()).limit(10).all()) 
        
            356
            @app.route("/accounts/") 
        
            359
            def accounts(): 
        
            360
                return flask.render_template("login.html") 
        
            361
            @app.route("/login", methods=["POST"]) 
        
            364
            def login(): 
        
            365
                username = flask.request.form["username"] 
        
            366
                password = flask.request.form["password"] 
        
            367
                user = db.session.get(User, username) 
        
            369
                if user is None: 
        
            371
                    flask.flash("This username is not registered.") 
        
            372
                    return flask.redirect("/accounts") 
        
            373
                if not bcrypt.check_password_hash(user.password_hashed, password): 
        
            375
                    flask.flash("Incorrect password.") 
        
            376
                    return flask.redirect("/accounts") 
        
            377
                flask.flash("You have been logged in.") 
        
            379
                flask.session["username"] = username 
        
            381
                return flask.redirect("/") 
        
            382
            @app.route("/logout") 
        
            385
            def logout(): 
        
            386
                flask.session.pop("username", None) 
        
            387
                flask.flash("You have been logged out.") 
        
            388
                return flask.redirect("/") 
        
            389
            @app.route("/signup", methods=["POST"]) 
        
            392
            def signup(): 
        
            393
                username = flask.request.form["username"] 
        
            394
                password = flask.request.form["password"] 
        
            395
                if db.session.get(User, username) is not None: 
        
            397
                    flask.flash("This username is already taken.") 
        
            398
                    return flask.redirect("/accounts") 
        
            399
                if set(username) > set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"): 
        
            401
                    flask.flash( 
        
            402
                        "Usernames can only contain the Latin alphabet, digits, hyphens, and underscores.") 
        
            403
                    return flask.redirect("/accounts") 
        
            404
                if len(username) < 3 or len(username) > 32: 
        
            406
                    flask.flash("Usernames must be between 3 and 32 characters long.") 
        
            407
                    return flask.redirect("/accounts") 
        
            408
                if len(password) < 6: 
        
            410
                    flask.flash("Passwords must be at least 6 characters long.") 
        
            411
                    return flask.redirect("/accounts") 
        
            412
                user = User(username, password) 
        
            414
                db.session.add(user) 
        
            415
                db.session.commit() 
        
            416
                flask.session["username"] = username 
        
            418
                flask.flash("You have been registered and logged in.") 
        
            420
                return flask.redirect("/") 
        
            422
            @app.route("/profile", defaults={"username": None}) 
        
            425
            @app.route("/profile/<username>") 
        
            426
            def profile(username): 
        
            427
                if username is None: 
        
            428
                    if "username" in flask.session: 
        
            429
                        return flask.redirect("/profile/" + flask.session["username"]) 
        
            430
                    else: 
        
            431
                        flask.flash("Please log in to perform this action.") 
        
            432
                        return flask.redirect("/accounts") 
        
            433
                user = db.session.get(User, username) 
        
            435
                if user is None: 
        
            436
                    flask.abort(404) 
        
            437
                return flask.render_template("profile.html", user=user) 
        
            439
            @app.route("/object/<id>") 
        
            442
            def has_object(id): 
        
            443
                object_ = db.session.get(PictureObject, id) 
        
            444
                if object_ is None: 
        
            445
                    flask.abort(404) 
        
            446
                query = db.session.query(PictureResource).join(PictureRegion).filter( 
        
            448
                    PictureRegion.object_id == id) 
        
            449
                page = int(flask.request.args.get("page", 1)) 
        
            451
                per_page = int(flask.request.args.get("per_page", 16)) 
        
            452
                resources = query.paginate(page=page, per_page=per_page) 
        
            454
                return flask.render_template("object.html", object=object_, resources=resources, 
        
            456
                                             page_number=page, 
        
            457
                                             page_length=per_page, num_pages=resources.pages, 
        
            458
                                             prev_page=resources.prev_num, 
        
            459
                                             next_page=resources.next_num, PictureRegion=PictureRegion) 
        
            460
            @app.route("/upload") 
        
            463
            def upload(): 
        
            464
                if "username" not in flask.session: 
        
            465
                    flask.flash("Log in to upload pictures.") 
        
            466
                    return flask.redirect("/accounts") 
        
            467
                licences = Licence.query.order_by(Licence.free.desc(), Licence.pinned.desc(), 
        
            469
                                                  Licence.title).all() 
        
            470
                types = PictureNature.query.all() 
        
            472
                return flask.render_template("upload.html", licences=licences, types=types) 
        
            474
            @app.route("/upload", methods=["POST"]) 
        
            477
            def upload_post(): 
        
            478
                title = flask.request.form["title"] 
        
            479
                description = flask.request.form["description"] 
        
            480
                origin_url = flask.request.form["origin_url"] 
        
            481
                author = db.session.get(User, flask.session.get("username")) 
        
            482
                licence_ids = flask.request.form.getlist("licence") 
        
            483
                nature_id = flask.request.form["nature"] 
        
            484
                if author is None: 
        
            486
                    flask.abort(401) 
        
            487
                file = flask.request.files["file"] 
        
            489
                if not file or not file.filename: 
        
            491
                    flask.flash("Select a file") 
        
            492
                    return flask.redirect(flask.request.url) 
        
            493
                if not file.mimetype.startswith("image/") or file.mimetype == "image/svg+xml": 
        
            495
                    flask.flash("Only images are supported") 
        
            496
                    return flask.redirect(flask.request.url) 
        
            497
                if not title: 
        
            499
                    flask.flash("Enter a title") 
        
            500
                    return flask.redirect(flask.request.url) 
        
            501
                if not description: 
        
            503
                    description = "" 
        
            504
                if not nature_id: 
        
            506
                    flask.flash("Select a picture type") 
        
            507
                    return flask.redirect(flask.request.url) 
        
            508
                if not licence_ids: 
        
            510
                    flask.flash("Select licences") 
        
            511
                    return flask.redirect(flask.request.url) 
        
            512
                licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids] 
        
            514
                if not any(licence.free for licence in licences): 
        
            515
                    flask.flash("Select at least one free licence") 
        
            516
                    return flask.redirect(flask.request.url) 
        
            517
                resource = PictureResource(title, author, description, origin_url, licence_ids, 
        
            519
                                           file.mimetype, 
        
            520
                                           db.session.get(PictureNature, nature_id)) 
        
            521
                db.session.add(resource) 
        
            522
                db.session.commit() 
        
            523
                file.save(path.join(config.DATA_PATH, "pictures", str(resource.id))) 
        
            524
                pil_image = Image.open(path.join(config.DATA_PATH, "pictures", str(resource.id))) 
        
            525
                resource.width, resource.height = pil_image.size 
        
            526
                db.session.commit() 
        
            527
                if flask.request.form.get("annotations"): 
        
            529
                    try: 
        
            530
                        resource.put_annotations(json.loads(flask.request.form.get("annotations"))) 
        
            531
                        db.session.commit() 
        
            532
                    except json.JSONDecodeError: 
        
            533
                        flask.flash("Invalid annotations") 
        
            534
                flask.flash("Picture uploaded successfully") 
        
            536
                return flask.redirect("/picture/" + str(resource.id)) 
        
            538
            @app.route("/picture/<int:id>/") 
        
            541
            def picture(id): 
        
            542
                resource = db.session.get(PictureResource, id) 
        
            543
                if resource is None: 
        
            544
                    flask.abort(404) 
        
            545
                image = Image.open(path.join(config.DATA_PATH, "pictures", str(resource.id))) 
        
            547
                current_user = db.session.get(User, flask.session.get("username")) 
        
            549
                have_permission = current_user and (current_user == resource.author or current_user.admin) 
        
            550
                own_rating = None 
        
            552
                if current_user: 
        
            553
                    own_rating = PictureRating.query.filter_by(resource=resource, user=current_user).first() 
        
            554
                return flask.render_template("picture.html", resource=resource, 
        
            556
                                             file_extension=mimetypes.guess_extension(resource.file_format), 
        
            557
                                             size=image.size, copies=resource.copies, 
        
            558
                                             have_permission=have_permission, own_rating=own_rating) 
        
            559
            @app.route("/picture/<int:id>/annotate") 
        
            562
            def annotate_picture(id): 
        
            563
                resource = db.session.get(PictureResource, id) 
        
            564
                if resource is None: 
        
            565
                    flask.abort(404) 
        
            566
                current_user = db.session.get(User, flask.session.get("username")) 
        
            568
                if current_user is None: 
        
            569
                    flask.abort(401) 
        
            570
                if resource.author != current_user and not current_user.admin: 
        
            571
                    flask.abort(403) 
        
            572
                return flask.render_template("picture-annotation.html", resource=resource, 
        
            574
                                             file_extension=mimetypes.guess_extension(resource.file_format)) 
        
            575
            @app.route("/picture/<int:id>/put-annotations-form") 
        
            578
            def put_annotations_form(id): 
        
            579
                resource = db.session.get(PictureResource, id) 
        
            580
                if resource is None: 
        
            581
                    flask.abort(404) 
        
            582
                current_user = db.session.get(User, flask.session.get("username")) 
        
            584
                if current_user is None: 
        
            585
                    flask.abort(401) 
        
            586
                if resource.author != current_user and not current_user.admin: 
        
            588
                    flask.abort(403) 
        
            589
                return flask.render_template("put-annotations-form.html", resource=resource) 
        
            591
            @app.route("/picture/<int:id>/put-annotations-form", methods=["POST"]) 
        
            594
            def put_annotations_form_post(id): 
        
            595
                resource = db.session.get(PictureResource, id) 
        
            596
                if resource is None: 
        
            597
                    flask.abort(404) 
        
            598
                current_user = db.session.get(User, flask.session.get("username")) 
        
            600
                if current_user is None: 
        
            601
                    flask.abort(401) 
        
            602
                if resource.author != current_user and not current_user.admin: 
        
            604
                    flask.abort(403) 
        
            605
                resource.put_annotations(json.loads(flask.request.form["annotations"])) 
        
            607
                db.session.commit() 
        
            609
                return flask.redirect("/picture/" + str(resource.id)) 
        
            611
            @app.route("/picture/<int:id>/save-annotations", methods=["POST"]) 
        
            614
            @app.route("/api/picture/<int:id>/put-annotations", methods=["POST"]) 
        
            615
            def save_annotations(id): 
        
            616
                resource = db.session.get(PictureResource, id) 
        
            617
                if resource is None: 
        
            618
                    flask.abort(404) 
        
            619
                current_user = db.session.get(User, flask.session.get("username")) 
        
            621
                if resource.author != current_user and not current_user.admin: 
        
            622
                    flask.abort(403) 
        
            623
                resource.put_annotations(flask.request.json) 
        
            625
                db.session.commit() 
        
            627
                response = flask.make_response() 
        
            629
                response.status_code = 204 
        
            630
                return response 
        
            631
            @app.route("/picture/<int:id>/get-annotations") 
        
            634
            @app.route("/api/picture/<int:id>/api/get-annotations") 
        
            635
            def get_annotations(id): 
        
            636
                resource = db.session.get(PictureResource, id) 
        
            637
                if resource is None: 
        
            638
                    flask.abort(404) 
        
            639
                regions = db.session.query(PictureRegion).filter_by(resource_id=id).all() 
        
            641
                regions_json = [] 
        
            643
                for region in regions: 
        
            645
                    regions_json.append({ 
        
            646
                        "object": region.object_id, 
        
            647
                        "type": region.json["type"], 
        
            648
                        "shape": region.json["shape"], 
        
            649
                    }) 
        
            650
                return flask.jsonify(regions_json) 
        
            652
            @app.route("/picture/<int:id>/delete") 
        
            655
            def delete_picture(id): 
        
            656
                resource = db.session.get(PictureResource, id) 
        
            657
                if resource is None: 
        
            658
                    flask.abort(404) 
        
            659
                current_user = db.session.get(User, flask.session.get("username")) 
        
            661
                if current_user is None: 
        
            662
                    flask.abort(401) 
        
            663
                if resource.author != current_user and not current_user.admin: 
        
            665
                    flask.abort(403) 
        
            666
                PictureLicence.query.filter_by(resource=resource).delete() 
        
            668
                PictureRegion.query.filter_by(resource=resource).delete() 
        
            669
                PictureInGallery.query.filter_by(resource=resource).delete() 
        
            670
                if resource.replaces: 
        
            671
                    resource.replaces.replaced_by = None 
        
            672
                if resource.replaced_by: 
        
            673
                    resource.replaced_by.replaces = None 
        
            674
                resource.copied_from = None 
        
            675
                for copy in resource.copies: 
        
            676
                    copy.copied_from = None 
        
            677
                db.session.delete(resource) 
        
            678
                db.session.commit() 
        
            679
                return flask.redirect("/") 
        
            681
            @app.route("/picture/<int:id>/mark-replacement", methods=["POST"]) 
        
            684
            def mark_picture_replacement(id): 
        
            685
                resource = db.session.get(PictureResource, id) 
        
            686
                if resource is None: 
        
            687
                    flask.abort(404) 
        
            688
                current_user = db.session.get(User, flask.session.get("username")) 
        
            690
                if current_user is None: 
        
            691
                    flask.abort(401) 
        
            692
                if resource.copied_from.author != current_user and not current_user.admin: 
        
            694
                    flask.abort(403) 
        
            695
                resource.copied_from.replaced_by = resource 
        
            697
                resource.replaces = resource.copied_from 
        
            698
                db.session.commit() 
        
            700
                return flask.redirect("/picture/" + str(resource.copied_from.id)) 
        
            702
            @app.route("/picture/<int:id>/remove-replacement", methods=["POST"]) 
        
            705
            def remove_picture_replacement(id): 
        
            706
                resource = db.session.get(PictureResource, id) 
        
            707
                if resource is None: 
        
            708
                    flask.abort(404) 
        
            709
                current_user = db.session.get(User, flask.session.get("username")) 
        
            711
                if current_user is None: 
        
            712
                    flask.abort(401) 
        
            713
                if resource.author != current_user and not current_user.admin: 
        
            715
                    flask.abort(403) 
        
            716
                resource.replaced_by.replaces = None 
        
            718
                resource.replaced_by = None 
        
            719
                db.session.commit() 
        
            721
                return flask.redirect("/picture/" + str(resource.id)) 
        
            723
            @app.route("/picture/<int:id>/edit-metadata") 
        
            726
            def edit_picture(id): 
        
            727
                resource = db.session.get(PictureResource, id) 
        
            728
                if resource is None: 
        
            729
                    flask.abort(404) 
        
            730
                current_user = db.session.get(User, flask.session.get("username")) 
        
            732
                if current_user is None: 
        
            733
                    flask.abort(401) 
        
            734
                if resource.author != current_user and not current_user.admin: 
        
            736
                    flask.abort(403) 
        
            737
                licences = Licence.query.order_by(Licence.free.desc(), Licence.pinned.desc(), 
        
            739
                                                  Licence.title).all() 
        
            740
                types = PictureNature.query.all() 
        
            742
                return flask.render_template("edit-picture.html", resource=resource, licences=licences, 
        
            744
                                             types=types, 
        
            745
                                             PictureLicence=PictureLicence) 
        
            746
            @app.route("/picture/<int:id>/rate", methods=["POST"]) 
        
            749
            def rate_picture(id): 
        
            750
                resource = db.session.get(PictureResource, id) 
        
            751
                if resource is None: 
        
            752
                    flask.abort(404) 
        
            753
                current_user = db.session.get(User, flask.session.get("username")) 
        
            755
                if current_user is None: 
        
            756
                    flask.abort(401) 
        
            757
                rating = int(flask.request.form.get("rating")) 
        
            759
                if not rating: 
        
            761
                    # Delete the existing rating 
        
            762
                    if PictureRating.query.filter_by(resource=resource, user=current_user).first(): 
        
            763
                        db.session.delete(PictureRating.query.filter_by(resource=resource, 
        
            764
                                                                         user=current_user).first()) 
        
            765
                        db.session.commit() 
        
            766
                    return flask.redirect("/picture/" + str(resource.id)) 
        
            768
                if not 1 <= rating <= 5: 
        
            770
                    flask.flash("Invalid rating") 
        
            771
                    return flask.redirect("/picture/" + str(resource.id)) 
        
            772
                if PictureRating.query.filter_by(resource=resource, user=current_user).first(): 
        
            774
                    PictureRating.query.filter_by(resource=resource, user=current_user).first().rating = rating 
        
            775
                else: 
        
            776
                    # Create a new rating 
        
            777
                    db.session.add(PictureRating(resource, current_user, rating)) 
        
            778
                db.session.commit() 
        
            780
                return flask.redirect("/picture/" + str(resource.id)) 
        
            782
            @app.route("/picture/<int:id>/edit-metadata", methods=["POST"]) 
        
            785
            def edit_picture_post(id): 
        
            786
                resource = db.session.get(PictureResource, id) 
        
            787
                if resource is None: 
        
            788
                    flask.abort(404) 
        
            789
                current_user = db.session.get(User, flask.session.get("username")) 
        
            791
                if current_user is None: 
        
            792
                    flask.abort(401) 
        
            793
                if resource.author != current_user and not current_user.admin: 
        
            795
                    flask.abort(403) 
        
            796
                title = flask.request.form["title"] 
        
            798
                description = flask.request.form["description"] 
        
            799
                origin_url = flask.request.form["origin_url"] 
        
            800
                licence_ids = flask.request.form.getlist("licence") 
        
            801
                nature_id = flask.request.form["nature"] 
        
            802
                if not title: 
        
            804
                    flask.flash("Enter a title") 
        
            805
                    return flask.redirect(flask.request.url) 
        
            806
                if not description: 
        
            808
                    description = "" 
        
            809
                if not nature_id: 
        
            811
                    flask.flash("Select a picture type") 
        
            812
                    return flask.redirect(flask.request.url) 
        
            813
                if not licence_ids: 
        
            815
                    flask.flash("Select licences") 
        
            816
                    return flask.redirect(flask.request.url) 
        
            817
                licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids] 
        
            819
                if not any(licence.free for licence in licences): 
        
            820
                    flask.flash("Select at least one free licence") 
        
            821
                    return flask.redirect(flask.request.url) 
        
            822
                resource.title = title 
        
            824
                resource.description = description 
        
            825
                resource.origin_url = origin_url 
        
            826
                for licence_id in licence_ids: 
        
            827
                    joiner = PictureLicence(resource, db.session.get(Licence, licence_id)) 
        
            828
                    db.session.add(joiner) 
        
            829
                resource.nature = db.session.get(PictureNature, nature_id) 
        
            830
                db.session.commit() 
        
            832
                return flask.redirect("/picture/" + str(resource.id)) 
        
            834
            @app.route("/picture/<int:id>/copy") 
        
            837
            def copy_picture(id): 
        
            838
                resource = db.session.get(PictureResource, id) 
        
            839
                if resource is None: 
        
            840
                    flask.abort(404) 
        
            841
                current_user = db.session.get(User, flask.session.get("username")) 
        
            843
                if current_user is None: 
        
            844
                    flask.abort(401) 
        
            845
                new_resource = PictureResource(resource.title, current_user, resource.description, 
        
            847
                                               resource.origin_url, 
        
            848
                                               [licence.licence_id for licence in resource.licences], 
        
            849
                                               resource.file_format, 
        
            850
                                               resource.nature) 
        
            851
                for region in resource.regions: 
        
            853
                    db.session.add(PictureRegion(region.json, new_resource, region.object)) 
        
            854
                db.session.commit() 
        
            856
                # Create a hard link for the new picture 
        
            858
                old_path = path.join(config.DATA_PATH, "pictures", str(resource.id)) 
        
            859
                new_path = path.join(config.DATA_PATH, "pictures", str(new_resource.id)) 
        
            860
                os.link(old_path, new_path) 
        
            861
                new_resource.width = resource.width 
        
            863
                new_resource.height = resource.height 
        
            864
                new_resource.copied_from = resource 
        
            865
                db.session.commit() 
        
            867
                return flask.redirect("/picture/" + str(new_resource.id)) 
        
            869
            @app.route("/gallery/<int:id>/") 
        
            872
            def gallery(id): 
        
            873
                gallery = db.session.get(Gallery, id) 
        
            874
                if gallery is None: 
        
            875
                    flask.abort(404) 
        
            876
                current_user = db.session.get(User, flask.session.get("username")) 
        
            878
                have_permission = current_user and (current_user == gallery.owner or current_user.admin or UserInGallery.query.filter_by(user=current_user, gallery=gallery).first()) 
        
            880
                return flask.render_template("gallery.html", gallery=gallery, 
        
            882
                                             have_permission=have_permission) 
        
            883
            @app.route("/create-gallery") 
        
            886
            def create_gallery(): 
        
            887
                if "username" not in flask.session: 
        
            888
                    flask.flash("Log in to create galleries.") 
        
            889
                    return flask.redirect("/accounts") 
        
            890
                return flask.render_template("create-gallery.html") 
        
            892
            @app.route("/create-gallery", methods=["POST"]) 
        
            895
            def create_gallery_post(): 
        
            896
                if not flask.session.get("username"): 
        
            897
                    flask.abort(401) 
        
            898
                if not flask.request.form.get("title"): 
        
            900
                    flask.flash("Enter a title") 
        
            901
                    return flask.redirect(flask.request.url) 
        
            902
                description = flask.request.form.get("description", "") 
        
            904
                gallery = Gallery(flask.request.form["title"], description, 
        
            906
                                  db.session.get(User, flask.session["username"])) 
        
            907
                db.session.add(gallery) 
        
            908
                db.session.commit() 
        
            909
                return flask.redirect("/gallery/" + str(gallery.id)) 
        
            911
            @app.route("/gallery/<int:id>/add-picture", methods=["POST"]) 
        
            914
            def gallery_add_picture(id): 
        
            915
                gallery = db.session.get(Gallery, id) 
        
            916
                if gallery is None: 
        
            917
                    flask.abort(404) 
        
            918
                if "username" not in flask.session: 
        
            920
                    flask.abort(401) 
        
            921
                if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first(): 
        
            923
                    flask.abort(403) 
        
            924
                picture_id = flask.request.form.get("picture_id") 
        
            926
                if "/" in picture_id:  # also allow full URLs 
        
            927
                    picture_id = picture_id.rstrip("/").rpartition("/")[1] 
        
            928
                if not picture_id: 
        
            929
                    flask.flash("Select a picture") 
        
            930
                    return flask.redirect("/gallery/" + str(gallery.id)) 
        
            931
                picture_id = int(picture_id) 
        
            932
                picture = db.session.get(PictureResource, picture_id) 
        
            934
                if picture is None: 
        
            935
                    flask.flash("Invalid picture") 
        
            936
                    return flask.redirect("/gallery/" + str(gallery.id)) 
        
            937
                if PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first(): 
        
            939
                    flask.flash("This picture is already in the gallery") 
        
            940
                    return flask.redirect("/gallery/" + str(gallery.id)) 
        
            941
                db.session.add(PictureInGallery(picture, gallery)) 
        
            943
                db.session.commit() 
        
            945
                return flask.redirect("/gallery/" + str(gallery.id)) 
        
            947
            @app.route("/gallery/<int:id>/remove-picture", methods=["POST"]) 
        
            950
            def gallery_remove_picture(id): 
        
            951
                gallery = db.session.get(Gallery, id) 
        
            952
                if gallery is None: 
        
            953
                    flask.abort(404) 
        
            954
                if "username" not in flask.session: 
        
            956
                    flask.abort(401) 
        
            957
                current_user = db.session.get(User, flask.session.get("username")) 
        
            959
                if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first(): 
        
            961
                    flask.abort(403) 
        
            962
                picture_id = int(flask.request.form.get("picture_id")) 
        
            964
                picture = db.session.get(PictureResource, picture_id) 
        
            966
                if picture is None: 
        
            967
                    flask.flash("Invalid picture") 
        
            968
                    return flask.redirect("/gallery/" + str(gallery.id)) 
        
            969
                picture_in_gallery = PictureInGallery.query.filter_by(resource=picture, 
        
            971
                                                                      gallery=gallery).first() 
        
            972
                if picture_in_gallery is None: 
        
            973
                    flask.flash("This picture isn't in the gallery") 
        
            974
                    return flask.redirect("/gallery/" + str(gallery.id)) 
        
            975
                db.session.delete(picture_in_gallery) 
        
            977
                db.session.commit() 
        
            979
                return flask.redirect("/gallery/" + str(gallery.id)) 
        
            981
            @app.route("/gallery/<int:id>/add-pictures-from-query", methods=["POST"]) 
        
            984
            def gallery_add_from_query(id): 
        
            985
                gallery = db.session.get(Gallery, id) 
        
            986
                if gallery is None: 
        
            987
                    flask.abort(404) 
        
            988
                if "username" not in flask.session: 
        
            990
                    flask.abort(401) 
        
            991
                if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first(): 
        
            993
                    flask.abort(403) 
        
            994
                query_yaml = flask.request.form.get("query", "") 
        
            996
                yaml_parser = yaml.YAML() 
        
            998
                query_data = yaml_parser.load(query_yaml) or {} 
        
            999
                query = get_picture_query(query_data) 
        
            1000
                pictures = query.all() 
        
            1002
                count = 0 
        
            1004
                for picture in pictures: 
        
            1006
                    if not PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first(): 
        
            1007
                        db.session.add(PictureInGallery(picture, gallery)) 
        
            1008
                        count += 1 
        
            1009
                db.session.commit() 
        
            1011
                flask.flash(f"Added {count} pictures to the gallery") 
        
            1013
                return flask.redirect("/gallery/" + str(gallery.id)) 
        
            1015
            @app.route("/gallery/<int:id>/users") 
        
            1018
            def gallery_users(id): 
        
            1019
                gallery = db.session.get(Gallery, id) 
        
            1020
                if gallery is None: 
        
            1021
                    flask.abort(404) 
        
            1022
                current_user = db.session.get(User, flask.session.get("username")) 
        
            1024
                have_permission = current_user and (current_user == gallery.owner or current_user.admin) 
        
            1025
                return flask.render_template("gallery-users.html", gallery=gallery, 
        
            1027
                                             have_permission=have_permission) 
        
            1028
            @app.route("/gallery/<int:id>/edit") 
        
            1031
            def edit_gallery(id): 
        
            1032
                gallery = db.session.get(Gallery, id) 
        
            1033
                if gallery is None: 
        
            1034
                    flask.abort(404) 
        
            1035
                current_user = db.session.get(User, flask.session.get("username")) 
        
            1037
                if current_user is None: 
        
            1038
                    flask.abort(401) 
        
            1039
                if current_user != gallery.owner and not current_user.admin: 
        
            1041
                    flask.abort(403) 
        
            1042
                return flask.render_template("edit-gallery.html", gallery=gallery) 
        
            1044
            @app.route("/gallery/<int:id>/edit", methods=["POST"]) 
        
            1047
            def edit_gallery_post(id): 
        
            1048
                gallery = db.session.get(Gallery, id) 
        
            1049
                if gallery is None: 
        
            1050
                    flask.abort(404) 
        
            1051
                current_user = db.session.get(User, flask.session.get("username")) 
        
            1053
                if current_user is None: 
        
            1054
                    flask.abort(401) 
        
            1055
                if current_user != gallery.owner and not current_user.admin: 
        
            1057
                    flask.abort(403) 
        
            1058
                title = flask.request.form["title"] 
        
            1060
                description = flask.request.form.get("description") 
        
            1061
                if not title: 
        
            1063
                    flask.flash("Enter a title") 
        
            1064
                    return flask.redirect(flask.request.url) 
        
            1065
                if not description: 
        
            1067
                    description = "" 
        
            1068
                gallery.title = title 
        
            1070
                gallery.description = description 
        
            1071
                db.session.commit() 
        
            1073
                return flask.redirect("/gallery/" + str(gallery.id)) 
        
            1075
            @app.route("/gallery/<int:id>/users/add", methods=["POST"]) 
        
            1078
            def gallery_add_user(id): 
        
            1079
                gallery = db.session.get(Gallery, id) 
        
            1080
                if gallery is None: 
        
            1081
                    flask.abort(404) 
        
            1082
                current_user = db.session.get(User, flask.session.get("username")) 
        
            1084
                if current_user is None: 
        
            1085
                    flask.abort(401) 
        
            1086
                if current_user != gallery.owner and not current_user.admin: 
        
            1088
                    flask.abort(403) 
        
            1089
                username = flask.request.form.get("username") 
        
            1091
                if username == gallery.owner_name: 
        
            1092
                    flask.flash("The owner is already in the gallery") 
        
            1093
                    return flask.redirect("/gallery/" + str(gallery.id) + "/users") 
        
            1094
                user = db.session.get(User, username) 
        
            1096
                if user is None: 
        
            1097
                    flask.flash("User not found") 
        
            1098
                    return flask.redirect("/gallery/" + str(gallery.id) + "/users") 
        
            1099
                if UserInGallery.query.filter_by(user=user, gallery=gallery).first(): 
        
            1101
                    flask.flash("User is already in the gallery") 
        
            1102
                    return flask.redirect("/gallery/" + str(gallery.id) + "/users") 
        
            1103
                db.session.add(UserInGallery(user, gallery)) 
        
            1105
                db.session.commit() 
        
            1107
                return flask.redirect("/gallery/" + str(gallery.id) + "/users") 
        
            1109
            @app.route("/gallery/<int:id>/users/remove", methods=["POST"]) 
        
            1112
            def gallery_remove_user(id): 
        
            1113
                gallery = db.session.get(Gallery, id) 
        
            1114
                if gallery is None: 
        
            1115
                    flask.abort(404) 
        
            1116
                current_user = db.session.get(User, flask.session.get("username")) 
        
            1118
                if current_user is None: 
        
            1119
                    flask.abort(401) 
        
            1120
                if current_user != gallery.owner and not current_user.admin: 
        
            1122
                    flask.abort(403) 
        
            1123
                username = flask.request.form.get("username") 
        
            1125
                user = db.session.get(User, username) 
        
            1126
                if user is None: 
        
            1127
                    flask.flash("User not found") 
        
            1128
                    return flask.redirect("/gallery/" + str(gallery.id) + "/users") 
        
            1129
                user_in_gallery = UserInGallery.query.filter_by(user=user, gallery=gallery).first() 
        
            1131
                if user_in_gallery is None: 
        
            1132
                    flask.flash("User is not in the gallery") 
        
            1133
                    return flask.redirect("/gallery/" + str(gallery.id) + "/users") 
        
            1134
                db.session.delete(user_in_gallery) 
        
            1136
                db.session.commit() 
        
            1138
                return flask.redirect("/gallery/" + str(gallery.id) + "/users") 
        
            1140
            class APIError(Exception): 
        
            1143
                def __init__(self, status_code, message): 
        
            1144
                    self.status_code = status_code 
        
            1145
                    self.message = message 
        
            1146
            def get_picture_query(query_data): 
        
            1149
                query = db.session.query(PictureResource) 
        
            1150
                requirement_conditions = { 
        
            1152
                    "has_object": lambda value: PictureResource.regions.any( 
        
            1153
                            PictureRegion.object_id.in_(value)), 
        
            1154
                    "nature": lambda value: PictureResource.nature_id.in_(value), 
        
            1155
                    "licence": lambda value: PictureResource.licences.any( 
        
            1156
                            PictureLicence.licence_id.in_(value)), 
        
            1157
                    "author": lambda value: PictureResource.author_name.in_(value), 
        
            1158
                    "title": lambda value: PictureResource.title.ilike(value), 
        
            1159
                    "description": lambda value: PictureResource.description.ilike(value), 
        
            1160
                    "origin_url": lambda value: db.func.lower(db.func.substr( 
        
            1161
                            PictureResource.origin_url, 
        
            1162
                            db.func.length(db.func.split_part(PictureResource.origin_url, "://", 1)) + 4 
        
            1163
                    )).in_(value), 
        
            1164
                    "above_width": lambda value: PictureResource.width >= value, 
        
            1165
                    "below_width": lambda value: PictureResource.width <= value, 
        
            1166
                    "above_height": lambda value: PictureResource.height >= value, 
        
            1167
                    "below_height": lambda value: PictureResource.height <= value, 
        
            1168
                    "before_date": lambda value: PictureResource.timestamp <= datetime.utcfromtimestamp( 
        
            1169
                            value), 
        
            1170
                    "after_date": lambda value: PictureResource.timestamp >= datetime.utcfromtimestamp( 
        
            1171
                            value), 
        
            1172
                    "in_gallery": lambda value: PictureResource.galleries.any(PictureInGallery.gallery_id.in_(value)), 
        
            1173
                    "above_rating": lambda value: db.select(db.func.coalesce(db.func.avg(PictureRating.rating), 5)).where(PictureRating.resource_id == PictureResource.id).scalar_subquery() >= value, 
        
            1174
                    "below_rating": lambda value: db.select(db.func.coalesce(db.func.avg(PictureRating.rating), 0)).where(PictureRating.resource_id == PictureResource.id).scalar_subquery() <= value, 
        
            1175
                    "above_rating_count": lambda value: db.select(db.func.count(PictureRating.id)).where(PictureRating.resource_id == PictureResource.id).correlate(PictureResource).scalar_subquery() >= value, 
        
            1176
                    "below_rating_count": lambda value: db.select(db.func.count(PictureRating.id)).where(PictureRating.resource_id == PictureResource.id).correlate(PictureResource).scalar_subquery() <= value, 
        
            1177
                    "above_region_count": lambda value: db.select(db.func.count(PictureRegion.id)).where(PictureRegion.resource_id == PictureResource.id).correlate(PictureResource).scalar_subquery() >= value, 
        
            1178
                    "below_region_count": lambda value: db.select(db.func.count(PictureRegion.id)).where(PictureRegion.resource_id == PictureResource.id).correlate(PictureResource).scalar_subquery() <= value, 
        
            1179
                    "copied_from": lambda value: PictureResource.copied_from_id.in_(value), 
        
            1180
                } 
        
            1181
                if "want" in query_data: 
        
            1183
                    for i in query_data["want"]: 
        
            1184
                        if len(i) != 1: 
        
            1185
                            raise APIError(400, "Each requirement must have exactly one key") 
        
            1186
                        requirement, value = list(i.items())[0] 
        
            1187
                        if requirement not in requirement_conditions: 
        
            1188
                            raise APIError(400, f"Unknown requirement type: {requirement}") 
        
            1189
                        condition = requirement_conditions[requirement] 
        
            1191
                        query = query.filter(condition(value)) 
        
            1192
                if "exclude" in query_data: 
        
            1193
                    for i in query_data["exclude"]: 
        
            1194
                        if len(i) != 1: 
        
            1195
                            raise APIError(400, "Each exclusion must have exactly one key") 
        
            1196
                        requirement, value = list(i.items())[0] 
        
            1197
                        if requirement not in requirement_conditions: 
        
            1198
                            raise APIError(400, f"Unknown requirement type: {requirement}") 
        
            1199
                        condition = requirement_conditions[requirement] 
        
            1201
                        query = query.filter(~condition(value)) 
        
            1202
                if not query_data.get("include_obsolete", False): 
        
            1203
                    query = query.filter(PictureResource.replaced_by_id.is_(None)) 
        
            1204
                return query 
        
            1206
            @app.route("/query-pictures") 
        
            1209
            def graphical_query_pictures(): 
        
            1210
                return flask.render_template("graphical-query-pictures.html") 
        
            1211
            @app.route("/query-pictures-results") 
        
            1214
            def graphical_query_pictures_results(): 
        
            1215
                query_yaml = flask.request.args.get("query", "") 
        
            1216
                yaml_parser = yaml.YAML() 
        
            1217
                query_data = yaml_parser.load(query_yaml) or {} 
        
            1218
                try: 
        
            1219
                    query = get_picture_query(query_data) 
        
            1220
                except APIError as e: 
        
            1221
                    flask.abort(e.status_code) 
        
            1222
                page = int(flask.request.args.get("page", 1)) 
        
            1224
                per_page = int(flask.request.args.get("per_page", 16)) 
        
            1225
                resources = query.paginate(page=page, per_page=per_page) 
        
            1227
                return flask.render_template("graphical-query-pictures-results.html", resources=resources, 
        
            1229
                                             query=query_yaml, 
        
            1230
                                             page_number=page, page_length=per_page, 
        
            1231
                                             num_pages=resources.pages, 
        
            1232
                                             prev_page=resources.prev_num, next_page=resources.next_num) 
        
            1233
            @app.route("/raw/picture/<int:id>") 
        
            1236
            def raw_picture(id): 
        
            1237
                resource = db.session.get(PictureResource, id) 
        
            1238
                if resource is None: 
        
            1239
                    flask.abort(404) 
        
            1240
                response = flask.send_from_directory(path.join(config.DATA_PATH, "pictures"), 
        
            1242
                                                     str(resource.id)) 
        
            1243
                response.mimetype = resource.file_format 
        
            1244
                return response 
        
            1246
            @app.route("/object/") 
        
            1249
            def graphical_object_types(): 
        
            1250
                return flask.render_template("object-types.html", objects=PictureObject.query.all()) 
        
            1251
            @app.route("/api/object-types") 
        
            1254
            def object_types(): 
        
            1255
                objects = db.session.query(PictureObject).all() 
        
            1256
                return flask.jsonify({object.id: object.description for object in objects}) 
        
            1257
            @app.route("/api/query-pictures", methods=["POST"])  # sadly GET can't have a body 
        
            1260
            def query_pictures(): 
        
            1261
                offset = int(flask.request.args.get("offset", 0)) 
        
            1262
                limit = int(flask.request.args.get("limit", 16)) 
        
            1263
                ordering = flask.request.args.get("ordering", "date-desc") 
        
            1264
                yaml_parser = yaml.YAML() 
        
            1266
                query_data = yaml_parser.load(flask.request.data) or {} 
        
            1267
                try: 
        
            1268
                    query = get_picture_query(query_data) 
        
            1269
                except APIError as e: 
        
            1270
                    return flask.jsonify({"error": e.message}), e.status_code 
        
            1271
                rating_count_subquery = db.select(db.func.count(PictureRating.id)).where( 
        
            1273
                    PictureRating.resource_id == PictureResource.id).scalar_subquery() 
        
            1274
                region_count_subquery = db.select(db.func.count(PictureRegion.id)).where( 
        
            1275
                    PictureRegion.resource_id == PictureResource.id).scalar_subquery() 
        
            1276
                rating_subquery = db.select(db.func.coalesce(db.func.avg(PictureRating.rating), 0)).where( 
        
            1277
                    PictureRating.resource_id == PictureResource.id).scalar_subquery() 
        
            1278
                match ordering: 
        
            1280
                    case "date-desc": 
        
            1281
                        query = query.order_by(PictureResource.timestamp.desc()) 
        
            1282
                    case "date-asc": 
        
            1283
                        query = query.order_by(PictureResource.timestamp.asc()) 
        
            1284
                    case "title-asc": 
        
            1285
                        query = query.order_by(PictureResource.title.asc()) 
        
            1286
                    case "title-desc": 
        
            1287
                        query = query.order_by(PictureResource.title.desc()) 
        
            1288
                    case "random": 
        
            1289
                        query = query.order_by(db.func.random()) 
        
            1290
                    case "number-regions-desc": 
        
            1291
                        query = query.order_by(region_count_subquery.desc()) 
        
            1292
                    case "number-regions-asc": 
        
            1293
                        query = query.order_by(region_count_subquery.asc()) 
        
            1294
                    case "rating-desc": 
        
            1295
                        query = query.order_by(rating_subquery.desc()) 
        
            1296
                    case "rating-asc": 
        
            1297
                        query = query.order_by(rating_subquery.asc()) 
        
            1298
                    case "number-ratings-desc": 
        
            1299
                        query = query.order_by(rating_count_subquery.desc()) 
        
            1300
                    case "number-ratings-asc": 
        
            1301
                        query = query.order_by(rating_count_subquery.asc()) 
        
            1302
                query = query.offset(offset).limit(limit) 
        
            1304
                resources = query.all() 
        
            1305
                json_response = { 
        
            1307
                    "date_generated": datetime.utcnow().timestamp(), 
        
            1308
                    "resources": [], 
        
            1309
                    "offset": offset, 
        
            1310
                    "limit": limit, 
        
            1311
                } 
        
            1312
                json_resources = json_response["resources"] 
        
            1314
                for resource in resources: 
        
            1316
                    json_resource = { 
        
            1317
                        "id": resource.id, 
        
            1318
                        "title": resource.title, 
        
            1319
                        "description": resource.description, 
        
            1320
                        "timestamp": resource.timestamp.timestamp(), 
        
            1321
                        "origin_url": resource.origin_url, 
        
            1322
                        "author": resource.author_name, 
        
            1323
                        "file_format": resource.file_format, 
        
            1324
                        "width": resource.width, 
        
            1325
                        "height": resource.height, 
        
            1326
                        "nature": resource.nature_id, 
        
            1327
                        "licences": [licence.licence_id for licence in resource.licences], 
        
            1328
                        "replaces": resource.replaces_id, 
        
            1329
                        "replaced_by": resource.replaced_by_id, 
        
            1330
                        "regions": [], 
        
            1331
                        "download": config.ROOT_URL + flask.url_for("raw_picture", id=resource.id), 
        
            1332
                    } 
        
            1333
                    for region in resource.regions: 
        
            1334
                        json_resource["regions"].append({ 
        
            1335
                            "object": region.object_id, 
        
            1336
                            "type": region.json["type"], 
        
            1337
                            "shape": region.json["shape"], 
        
            1338
                        }) 
        
            1339
                    json_resources.append(json_resource) 
        
            1341
                return flask.jsonify(json_response) 
        
            1343
            @app.route("/api/picture/<int:id>/") 
        
            1346
            def api_picture(id): 
        
            1347
                resource = db.session.get(PictureResource, id) 
        
            1348
                if resource is None: 
        
            1349
                    flask.abort(404) 
        
            1350
                json_resource = { 
        
            1352
                    "id": resource.id, 
        
            1353
                    "title": resource.title, 
        
            1354
                    "description": resource.description, 
        
            1355
                    "timestamp": resource.timestamp.timestamp(), 
        
            1356
                    "origin_url": resource.origin_url, 
        
            1357
                    "author": resource.author_name, 
        
            1358
                    "file_format": resource.file_format, 
        
            1359
                    "width": resource.width, 
        
            1360
                    "height": resource.height, 
        
            1361
                    "nature": resource.nature_id, 
        
            1362
                    "licences": [licence.licence_id for licence in resource.licences], 
        
            1363
                    "replaces": resource.replaces_id, 
        
            1364
                    "replaced_by": resource.replaced_by_id, 
        
            1365
                    "regions": [], 
        
            1366
                    "download": config.ROOT_URL + flask.url_for("raw_picture", id=resource.id), 
        
            1367
                    "rating_average": resource.average_rating, 
        
            1368
                    "rating_count": resource.rating_totals, 
        
            1369
                } 
        
            1370
                for region in resource.regions: 
        
            1371
                    json_resource["regions"].append({ 
        
            1372
                        "object": region.object_id, 
        
            1373
                        "type": region.json["type"], 
        
            1374
                        "shape": region.json["shape"], 
        
            1375
                    }) 
        
            1376
                return flask.jsonify(json_resource) 
        
            1378
            @app.route("/api/licence/") 
        
            1381
            def api_licences(): 
        
            1382
                licences = db.session.query(Licence).all() 
        
            1383
                json_licences = { 
        
            1384
                    licence.id: { 
        
            1385
                        "title": licence.title, 
        
            1386
                        "free": licence.free, 
        
            1387
                        "pinned": licence.pinned, 
        
            1388
                    } for licence in licences 
        
            1389
                } 
        
            1390
                return flask.jsonify(json_licences) 
        
            1392
            @app.route("/api/licence/<id>/") 
        
            1395
            def api_licence(id): 
        
            1396
                licence = db.session.get(Licence, id) 
        
            1397
                if licence is None: 
        
            1398
                    flask.abort(404) 
        
            1399
                json_licence = { 
        
            1401
                    "id": licence.id, 
        
            1402
                    "title": licence.title, 
        
            1403
                    "description": licence.description, 
        
            1404
                    "info_url": licence.info_url, 
        
            1405
                    "legalese_url": licence.url, 
        
            1406
                    "free": licence.free, 
        
            1407
                    "logo_url": licence.logo_url, 
        
            1408
                    "pinned": licence.pinned, 
        
            1409
                } 
        
            1410
                return flask.jsonify(json_licence) 
        
            1412
            @app.route("/api/nature/") 
        
            1415
            def api_natures(): 
        
            1416
                natures = db.session.query(PictureNature).all() 
        
            1417
                json_natures = { 
        
            1418
                    nature.id: nature.description for nature in natures 
        
            1419
                } 
        
            1420
                return flask.jsonify(json_natures) 
        
            1422
            @app.route("/api/user/") 
        
            1425
            def api_users(): 
        
            1426
                offset = int(flask.request.args.get("offset", 0)) 
        
            1427
                limit = int(flask.request.args.get("limit", 16)) 
        
            1428
                users = db.session.query(User).offset(offset).limit(limit).all() 
        
            1430
                json_users = { 
        
            1432
                    user.username: { 
        
            1433
                        "admin": user.admin, 
        
            1434
                    } for user in users 
        
            1435
                } 
        
            1436
                return flask.jsonify(json_users) 
        
            1438
            @app.route("/api/user/<username>/") 
        
            1441
            def api_user(username): 
        
            1442
                user = db.session.get(User, username) 
        
            1443
                if user is None: 
        
            1444
                    flask.abort(404) 
        
            1445
                json_user = { 
        
            1447
                    "username": user.username, 
        
            1448
                    "admin": user.admin, 
        
            1449
                    "joined": user.joined_timestamp.timestamp(), 
        
            1450
                } 
        
            1451
                return flask.jsonify(json_user) 
        
            1453
            @app.route("/api/login", methods=["POST"]) 
        
            1456
            def api_login(): 
        
            1457
                username = flask.request.json["username"] 
        
            1458
                password = flask.request.json["password"] 
        
            1459
                user = db.session.get(User, username) 
        
            1461
                if user is None: 
        
            1463
                    return flask.jsonify({"error": "This username is not registered. To prevent spam, you must use the HTML interface to register."}), 401 
        
            1464
                if not bcrypt.check_password_hash(user.password_hashed, password): 
        
            1466
                    return flask.jsonify({"error": "Incorrect password"}), 401 
        
            1467
                flask.session["username"] = username 
        
            1469
                return flask.jsonify({"message": "You have been logged in. Your HTTP client must support cookies to use features of this API that require authentication."}) 
        
            1471
            @app.route("/api/logout", methods=["POST"]) 
        
            1474
            def api_logout(): 
        
            1475
                flask.session.pop("username", None) 
        
            1476
                return flask.jsonify({"message": "You have been logged out."}) 
        
            1477
            @app.route("/api/upload", methods=["POST"]) 
        
            1480
            def api_upload(): 
        
            1481
                if "username" not in flask.session: 
        
            1482
                    return flask.jsonify({"error": "You must be logged in to upload pictures"}), 401 
        
            1483
                json_ = json.loads(flask.request.form["json"]) 
        
            1485
                title = json_["title"] 
        
            1486
                description = json_.get("description", "") 
        
            1487
                origin_url = json_.get("origin_url", "") 
        
            1488
                author = db.session.get(User, flask.session["username"]) 
        
            1489
                licence_ids = json_["licence"] 
        
            1490
                nature_id = json_["nature"] 
        
            1491
                file = flask.request.files["file"] 
        
            1492
                if not file or not file.filename: 
        
            1494
                    return flask.jsonify({"error": "An image file must be uploaded"}), 400 
        
            1495
                if not file.mimetype.startswith("image/") or file.mimetype == "image/svg+xml": 
        
            1497
                    return flask.jsonify({"error": "Only bitmap images are supported"}), 400 
        
            1498
                if not title: 
        
            1500
                    return flask.jsonify({"error": "Give a title"}), 400 
        
            1501
                if not description: 
        
            1503
                    description = "" 
        
            1504
                if not nature_id: 
        
            1506
                    return flask.jsonify({"error": "Give a picture type"}), 400 
        
            1507
                if not licence_ids: 
        
            1509
                    return flask.jsonify({"error": "Give licences"}), 400 
        
            1510
                licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids] 
        
            1512
                if not any(licence.free for licence in licences): 
        
            1513
                    return flask.jsonify({"error": "Use at least one free licence"}), 400 
        
            1514
                resource = PictureResource(title, author, description, origin_url, licence_ids, 
        
            1516
                                           file.mimetype, 
        
            1517
                                           db.session.get(PictureNature, nature_id)) 
        
            1518
                db.session.add(resource) 
        
            1519
                db.session.commit() 
        
            1520
                file.save(path.join(config.DATA_PATH, "pictures", str(resource.id))) 
        
            1521
                pil_image = Image.open(path.join(config.DATA_PATH, "pictures", str(resource.id))) 
        
            1522
                resource.width, resource.height = pil_image.size 
        
            1523
                db.session.commit() 
        
            1524
                if json_.get("annotations"): 
        
            1526
                    try: 
        
            1527
                        resource.put_annotations(json_["annotations"]) 
        
            1528
                        db.session.commit() 
        
            1529
                    except json.JSONDecodeError: 
        
            1530
                        return flask.jsonify({"error": "Invalid annotations"}), 400 
        
            1531
                return flask.jsonify({"message": "Picture uploaded successfully"}) 
        
            1533
            @app.route("/api/picture/<int:id>/update", methods=["POST"]) 
        
            1536
            def api_update_picture(id): 
        
            1537
                resource = db.session.get(PictureResource, id) 
        
            1538
                if resource is None: 
        
            1539
                    return flask.jsonify({"error": "Picture not found"}), 404 
        
            1540
                current_user = db.session.get(User, flask.session.get("username")) 
        
            1541
                if current_user is None: 
        
            1542
                    return flask.jsonify({"error": "You must be logged in to edit pictures"}), 401 
        
            1543
                if resource.author != current_user and not current_user.admin: 
        
            1544
                    return flask.jsonify({"error": "You are not the author of this picture"}), 403 
        
            1545
                title = flask.request.json.get("title", resource.title) 
        
            1547
                description = flask.request.json.get("description", resource.description) 
        
            1548
                origin_url = flask.request.json.get("origin_url", resource.origin_url) 
        
            1549
                licence_ids = flask.request.json.get("licence", [licence.licence_id for licence in resource.licences]) 
        
            1550
                nature_id = flask.request.json.get("nature", resource.nature_id) 
        
            1551
                if not title: 
        
            1553
                    return flask.jsonify({"error": "Give a title"}), 400 
        
            1554
                if not description: 
        
            1556
                    description = "" 
        
            1557
                if not nature_id: 
        
            1559
                    return flask.jsonify({"error": "Give a picture type"}), 400 
        
            1560
                if not licence_ids: 
        
            1562
                    return flask.jsonify({"error": "Give licences"}), 400 
        
            1563
                licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids] 
        
            1565
                if not any(licence.free for licence in licences): 
        
            1567
                    return flask.jsonify({"error": "Use at least one free licence"}), 400 
        
            1568
                resource.title = title 
        
            1570
                resource.description = description 
        
            1571
                resource.origin_url = origin_url 
        
            1572
                resource.licences = licences 
        
            1573
                resource.nature = db.session.get(PictureNature, nature_id) 
        
            1574
                db.session.commit() 
        
            1576
                return flask.jsonify({"message": "Picture updated successfully"}) 
        
            1578
            @app.route("/api/picture/<int:id>/rate", methods=["POST"]) 
        
            1581
            def api_rate_picture(id): 
        
            1582
                resource = db.session.get(PictureResource, id) 
        
            1583
                if resource is None: 
        
            1584
                    flask.abort(404) 
        
            1585
                current_user = db.session.get(User, flask.session.get("username")) 
        
            1587
                if current_user is None: 
        
            1588
                    flask.abort(401) 
        
            1589
                rating = int(flask.request.json.get("rating")) 
        
            1591
                if not rating: 
        
            1593
                    # Delete the existing rating 
        
            1594
                    if PictureRating.query.filter_by(resource=resource, user=current_user).first(): 
        
            1595
                        db.session.delete(PictureRating.query.filter_by(resource=resource, 
        
            1596
                                                                         user=current_user).first()) 
        
            1597
                        db.session.commit() 
        
            1598
                    return flask.jsonify({"message": "Existing rating removed"}) 
        
            1600
                if not 1 <= rating <= 5: 
        
            1602
                    flask.flash("Invalid rating") 
        
            1603
                    return flask.jsonify({"error": "Invalid rating"}), 400 
        
            1604
                if PictureRating.query.filter_by(resource=resource, user=current_user).first(): 
        
            1606
                    PictureRating.query.filter_by(resource=resource, user=current_user).first().rating = rating 
        
            1607
                else: 
        
            1608
                    # Create a new rating 
        
            1609
                    db.session.add(PictureRating(resource, current_user, rating)) 
        
            1610
                db.session.commit() 
        
            1612
                return flask.jsonify({"message": "Rating saved"}) 
        
            1614
            @app.route("/api/gallery/<int:id>/") 
        
            1617
            def api_gallery(id): 
        
            1618
                gallery = db.session.get(Gallery, id) 
        
            1619
                if gallery is None: 
        
            1620
                    flask.abort(404) 
        
            1621
                json_gallery = { 
        
            1623
                    "id": gallery.id, 
        
            1624
                    "title": gallery.title, 
        
            1625
                    "description": gallery.description, 
        
            1626
                    "owner": gallery.owner_name, 
        
            1627
                    "users": [user.username for user in gallery.users], 
        
            1628
                } 
        
            1629
                return flask.jsonify(json_gallery) 
        
            1631
            @app.route("/api/gallery/<int:id>/edit", methods=["POST"]) 
        
            1634
            def api_edit_gallery(id): 
        
            1635
                gallery = db.session.get(Gallery, id) 
        
            1636
                if gallery is None: 
        
            1637
                    flask.abort(404) 
        
            1638
                current_user = db.session.get(User, flask.session.get("username")) 
        
            1640
                if current_user is None: 
        
            1641
                    flask.abort(401) 
        
            1642
                if current_user != gallery.owner and not current_user.admin: 
        
            1644
                    flask.abort(403) 
        
            1645
                title = flask.request.json.get("title", gallery.title) 
        
            1647
                description = flask.request.json.get("description", gallery.description) 
        
            1648
                if not title: 
        
            1650
                    return flask.jsonify({"error": "Give a title"}), 400 
        
            1651
                if not description: 
        
            1653
                    description = "" 
        
            1654
                gallery.title = title 
        
            1656
                gallery.description = description 
        
            1657
                db.session.commit() 
        
            1659
                return flask.jsonify({"message": "Gallery updated successfully"}) 
        
            1661
            @app.route("/api/new-gallery", methods=["POST"]) 
        
            1664
            def api_new_gallery(): 
        
            1665
                if "username" not in flask.session: 
        
            1666
                    return flask.jsonify({"error": "You must be logged in to create galleries"}), 401 
        
            1667
                title = flask.request.json.get("title") 
        
            1669
                description = flask.request.json.get("description", "") 
        
            1670
                if not title: 
        
            1672
                    return flask.jsonify({"error": "Give a title"}), 400 
        
            1673
                gallery = Gallery(title, description, db.session.get(User, flask.session["username"])) 
        
            1675
                db.session.add(gallery) 
        
            1676
                db.session.commit() 
        
            1677
                return flask.jsonify({"message": "Gallery created successfully"}) 
        
            1679
            @app.route("/api/gallery/<int:id>/add-picture", methods=["POST"]) 
        
            1682
            def api_gallery_add_picture(id): 
        
            1683
                gallery = db.session.get(Gallery, id) 
        
            1684
                if gallery is None: 
        
            1685
                    flask.abort(404) 
        
            1686
                if "username" not in flask.session: 
        
            1688
                    return flask.jsonify({"error": "You must be logged in to add pictures to galleries"}), 401 
        
            1689
                current_user = db.session.get(User, flask.session.get("username")) 
        
            1691
                if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first(): 
        
            1693
                    return flask.jsonify({"error": "You do not have permission to add pictures to this gallery"}), 403 
        
            1694
                picture_id = flask.request.json.get("picture_id") 
        
            1696
                try: 
        
            1698
                    picture_id = int(picture_id) 
        
            1699
                except ValueError: 
        
            1700
                    return flask.jsonify({"error": "Invalid picture ID"}), 400 
        
            1701
                picture = db.session.get(PictureResource, picture_id) 
        
            1703
                if picture is None: 
        
            1704
                    return flask.jsonify({"error": "The picture doesn't exist"}), 404 
        
            1705
                if PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first(): 
        
            1707
                    return flask.jsonify({"error": "This picture is already in the gallery"}), 400 
        
            1708
                db.session.add(PictureInGallery(picture, gallery)) 
        
            1710
                db.session.commit() 
        
            1712
                return flask.jsonify({"message": "Picture added to gallery"}) 
        
            1714
            @app.route("/api/gallery/<int:id>/remove-picture", methods=["POST"]) 
        
            1717
            def api_gallery_remove_picture(id): 
        
            1718
                gallery = db.session.get(Gallery, id) 
        
            1719
                if gallery is None: 
        
            1720
                    flask.abort(404) 
        
            1721
                if "username" not in flask.session: 
        
            1723
                    return flask.jsonify({"error": "You must be logged in to remove pictures from galleries"}), 401 
        
            1724
                current_user = db.session.get(User, flask.session.get("username")) 
        
            1726
                if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first(): 
        
            1728
                    return flask.jsonify({"error": "You do not have permission to remove pictures from this gallery"}), 403 
        
            1729
                picture_id = flask.request.json.get("picture_id") 
        
            1731
                try: 
        
            1733
                    picture_id = int(picture_id) 
        
            1734
                except ValueError: 
        
            1735
                    return flask.jsonify({"error": "Invalid picture ID"}), 400 
        
            1736
                picture = db.session.get(PictureResource, picture_id) 
        
            1738
                if picture is None: 
        
            1739
                    return flask.jsonify({"error": "The picture doesn't exist"}), 404 
        
            1740
                picture_in_gallery = PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first() 
        
            1742
                if picture_in_gallery is None: 
        
            1743
                    return flask.jsonify({"error": "This picture isn't in the gallery"}), 400 
        
            1744
                db.session.delete(picture_in_gallery) 
        
            1746
                db.session.commit() 
        
            1748
                return flask.jsonify({"message": "Picture removed from gallery"}) 
        
            1750
            @app.route("/api/gallery/<int:id>/users/add", methods=["POST"]) 
        
            1753
            def api_gallery_add_user(id): 
        
            1754
                gallery = db.session.get(Gallery, id) 
        
            1755
                if gallery is None: 
        
            1756
                    flask.abort(404) 
        
            1757
                current_user = db.session.get(User, flask.session.get("username")) 
        
            1759
                if current_user is None: 
        
            1760
                    flask.abort(401) 
        
            1761
                if current_user != gallery.owner and not current_user.admin: 
        
            1763
                    flask.abort(403) 
        
            1764
                username = flask.request.json.get("username") 
        
            1766
                if username == gallery.owner_name: 
        
            1767
                    return flask.jsonify({"error": "The owner cannot be added to trusted users"}), 400 
        
            1768
                user = db.session.get(User, username) 
        
            1770
                if user is None: 
        
            1771
                    return flask.jsonify({"error": "User not found"}), 404 
        
            1772
                if UserInGallery.query.filter_by(user=user, gallery=gallery).first(): 
        
            1774
                    return flask.jsonify({"error": "User is already in the gallery"}), 400 
        
            1775
                db.session.add(UserInGallery(user, gallery)) 
        
            1777
                db.session.commit() 
        
            1779
                return flask.jsonify({"message": "User added to gallery"}) 
        
            1781
            @app.route("/api/gallery/<int:id>/users/remove", methods=["POST"]) 
        
            1784
            def api_gallery_remove_user(id): 
        
            1785
                gallery = db.session.get(Gallery, id) 
        
            1786
                if gallery is None: 
        
            1787
                    flask.abort(404) 
        
            1788
                current_user = db.session.get(User, flask.session.get("username")) 
        
            1790
                if current_user is None: 
        
            1791
                    flask.abort(401) 
        
            1792
                if current_user != gallery.owner and not current_user.admin: 
        
            1794
                    flask.abort(403) 
        
            1795
                username = flask.request.json.get("username") 
        
            1797
                user = db.session.get(User, username) 
        
            1798
                if user is None: 
        
            1799
                    return flask.jsonify({"error": "User not found"}), 404 
        
            1800
                user_in_gallery = UserInGallery.query.filter_by(user=user, gallery=gallery).first() 
        
            1802
                if user_in_gallery is None: 
        
            1803
                    return flask.jsonify({"error": "User is not in the gallery"}), 400 
        
            1804
                db.session.delete(user_in_gallery) 
        
            1806
                db.session.commit() 
        
            1808
                return flask.jsonify({"message": "User removed from gallery"}) 
        
            1810