Python script, ASCII text executable
        
            1
            from datetime import datetime 
        
            2
            from email.policy import default 
        
            3
            import flask 
        
            5
            from flask_sqlalchemy import SQLAlchemy 
        
            6
            from flask_bcrypt import Bcrypt 
        
            7
            from flask_httpauth import HTTPBasicAuth 
        
            8
            from markupsafe import escape, Markup 
        
            9
            from flask_migrate import Migrate 
        
            10
            from jinja2_fragments.flask import render_block 
        
            11
            from sqlalchemy.orm import backref 
        
            12
            import sqlalchemy.dialects.postgresql 
        
            13
            from os import path 
        
            14
            import mimetypes 
        
            15
            from PIL import Image 
        
            17
            import config 
        
            19
            import markdown 
        
            20
            app = flask.Flask(__name__) 
        
            23
            bcrypt = Bcrypt(app) 
        
            24
            app.config["SQLALCHEMY_DATABASE_URI"] = config.DB_URI 
        
            27
            app.config["SECRET_KEY"] = config.DB_PASSWORD 
        
            28
            db = SQLAlchemy(app) 
        
            31
            migrate = Migrate(app, db) 
        
            32
            @app.template_filter("split") 
        
            35
            def split(value, separator=None, maxsplit=-1): 
        
            36
                return value.split(separator, maxsplit) 
        
            37
            with app.app_context(): 
        
            41
                class User(db.Model): 
        
            42
                    username = db.Column(db.String(32), unique=True, nullable=False, primary_key=True) 
        
            43
                    password_hashed = db.Column(db.String(60), nullable=False) 
        
            44
                    admin = db.Column(db.Boolean, nullable=False, default=False, server_default="false") 
        
            45
                    pictures = db.relationship("PictureResource", back_populates="author") 
        
            46
                    def __init__(self, username, password): 
        
            48
                        self.username = username 
        
            49
                        self.password_hashed = bcrypt.generate_password_hash(password).decode("utf-8") 
        
            50
                class Licence(db.Model): 
        
            53
                    id = db.Column(db.String(64), primary_key=True)               # SPDX identifier 
        
            54
                    title = db.Column(db.UnicodeText, nullable=False)             # the official name of the licence 
        
            55
                    description = db.Column(db.UnicodeText, nullable=False)       # brief description of its permissions and restrictions 
        
            56
                    legal_text = db.Column(db.UnicodeText, nullable=False)        # the full legal text of the licence 
        
            57
                    url = db.Column(db.String(2048), nullable=True)               # the URL to a page with the full text of the licence and more information 
        
            58
                    pictures = db.relationship("PictureLicence", back_populates="licence") 
        
            59
                    free = db.Column(db.Boolean, nullable=False, default=False)   # whether the licence is free or not 
        
            60
                    def __init__(self, id, title, description, legal_text, url, free): 
        
            62
                        self.id = id 
        
            63
                        self.title = title 
        
            64
                        self.description = description 
        
            65
                        self.legal_text = legal_text 
        
            66
                        self.url = url 
        
            67
                        self.free = free 
        
            68
                class PictureLicence(db.Model): 
        
            71
                    id = db.Column(db.Integer, primary_key=True, autoincrement=True) 
        
            72
                    resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id")) 
        
            74
                    licence_id = db.Column(db.String(32), db.ForeignKey("licence.id")) 
        
            75
                    resource = db.relationship("PictureResource", back_populates="licences") 
        
            77
                    licence = db.relationship("Licence", back_populates="pictures") 
        
            78
                    def __init__(self, resource_id, licence_id): 
        
            80
                        self.resource_id = resource_id 
        
            81
                        self.licence_id = licence_id 
        
            82
                class Resource(db.Model): 
        
            85
                    __abstract__ = True 
        
            86
                    id = db.Column(db.Integer, primary_key=True, autoincrement=True) 
        
            88
                    title = db.Column(db.UnicodeText, nullable=False) 
        
            89
                    description = db.Column(db.UnicodeText, nullable=False) 
        
            90
                    timestamp = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) 
        
            91
                    origin_url = db.Column(db.String(2048), nullable=True)        # should be left empty if it's original or the source is unknown but public domain 
        
            92
                class PictureNature(db.Model): 
        
            95
                    # Examples: 
        
            96
                    # "photo", "paper-scan", "2d-art-photo", "sculpture-photo", "computer-3d", "computer-painting", 
        
            97
                    # "computer-line-art", "diagram", "infographic", "text", "map", "chart-graph", "screen-capture", 
        
            98
                    # "screen-photo", "pattern", "collage", "ai", and so on 
        
            99
                    id = db.Column(db.String(64), primary_key=True) 
        
            100
                    description = db.Column(db.UnicodeText, nullable=False) 
        
            101
                    resources = db.relationship("PictureResource", back_populates="nature") 
        
            102
                    def __init__(self, id, description): 
        
            104
                        self.id = id 
        
            105
                        self.description = description 
        
            106
                class PictureObjectInheritance(db.Model): 
        
            109
                    parent_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"), 
        
            110
                                          primary_key=True) 
        
            111
                    child_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"), 
        
            112
                                         primary_key=True) 
        
            113
                    parent = db.relationship("PictureObject", foreign_keys=[parent_id], 
        
            115
                                             back_populates="child_links") 
        
            116
                    child = db.relationship("PictureObject", foreign_keys=[child_id], 
        
            117
                                            back_populates="parent_links") 
        
            118
                    def __init__(self, parent, child): 
        
            120
                        self.parent = parent 
        
            121
                        self.child = child 
        
            122
                class PictureObject(db.Model): 
        
            125
                    id = db.Column(db.String(64), primary_key=True) 
        
            126
                    description = db.Column(db.UnicodeText, nullable=False) 
        
            127
                    child_links = db.relationship("PictureObjectInheritance", 
        
            129
                                                  foreign_keys=[PictureObjectInheritance.parent_id], 
        
            130
                                                  back_populates="parent") 
        
            131
                    parent_links = db.relationship("PictureObjectInheritance", 
        
            132
                                                   foreign_keys=[PictureObjectInheritance.child_id], 
        
            133
                                                   back_populates="child") 
        
            134
                    def __init__(self, id, description): 
        
            136
                        self.id = id 
        
            137
                        self.description = description 
        
            138
                class PictureRegion(db.Model): 
        
            141
                    # This is for picture region annotations 
        
            142
                    id = db.Column(db.Integer, primary_key=True, autoincrement=True) 
        
            143
                    json = db.Column(sqlalchemy.dialects.postgresql.JSONB, nullable=False) 
        
            144
                    resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), nullable=False) 
        
            146
                    object_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"), nullable=True) 
        
            147
                    resource = db.relationship("PictureResource", backref="regions") 
        
            149
                    object = db.relationship("PictureObject", backref="regions") 
        
            150
                    def __init__(self, json, resource, object): 
        
            152
                        self.json = json 
        
            153
                        self.resource = resource 
        
            154
                        self.object = object 
        
            155
                class PictureResource(Resource): 
        
            158
                    # This is only for bitmap pictures. Vectors will be stored under a different model 
        
            159
                    # File name is the ID in the picture directory under data, without an extension 
        
            160
                    file_format = db.Column(db.String(64), nullable=False)        # MIME type 
        
            161
                    width = db.Column(db.Integer, nullable=False) 
        
            162
                    height = db.Column(db.Integer, nullable=False) 
        
            163
                    nature_id = db.Column(db.String(32), db.ForeignKey("picture_nature.id"), nullable=True) 
        
            164
                    author_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 
        
            165
                    author = db.relationship("User", back_populates="pictures") 
        
            166
                    nature = db.relationship("PictureNature", back_populates="resources") 
        
            168
                    replaces_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), nullable=True) 
        
            170
                    replaced_by_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), 
        
            171
                                               nullable=True) 
        
            172
                    replaces = db.relationship("PictureResource", remote_side="PictureResource.id", 
        
            174
                                               foreign_keys=[replaces_id], back_populates="replaced_by") 
        
            175
                    replaced_by = db.relationship("PictureResource", remote_side="PictureResource.id", 
        
            176
                                                  foreign_keys=[replaced_by_id]) 
        
            177
                    licences = db.relationship("PictureLicence", back_populates="resource") 
        
            179
                    def __init__(self, title, author, description, origin_url, licence_ids, mime, nature=None, 
        
            181
                                 replaces=None): 
        
            182
                        self.title = title 
        
            183
                        self.author = author 
        
            184
                        self.description = description 
        
            185
                        self.origin_url = origin_url 
        
            186
                        self.file_format = mime 
        
            187
                        self.width = self.height = 0 
        
            188
                        self.nature = nature 
        
            189
                        db.session.add(self) 
        
            190
                        db.session.commit() 
        
            191
                        for licence_id in licence_ids: 
        
            192
                            joiner = PictureLicence(self.id, licence_id) 
        
            193
                            db.session.add(joiner) 
        
            194
                        if replaces is not None: 
        
            195
                            self.replaces = replaces 
        
            196
                            replaces.replaced_by = self 
        
            197
            @app.route("/") 
        
            200
            def index(): 
        
            201
                return flask.render_template("home.html") 
        
            202
            @app.route("/accounts/") 
        
            205
            def accounts(): 
        
            206
                return flask.render_template("login.html") 
        
            207
            @app.route("/login", methods=["POST"]) 
        
            210
            def login(): 
        
            211
                username = flask.request.form["username"] 
        
            212
                password = flask.request.form["password"] 
        
            213
                user = db.session.get(User, username) 
        
            215
                if user is None: 
        
            217
                    flask.flash("This username is not registered.") 
        
            218
                    return flask.redirect("/accounts") 
        
            219
                if not bcrypt.check_password_hash(user.password_hashed, password): 
        
            221
                    flask.flash("Incorrect password.") 
        
            222
                    return flask.redirect("/accounts") 
        
            223
                flask.flash("You have been logged in.") 
        
            225
                flask.session["username"] = username 
        
            227
                return flask.redirect("/") 
        
            228
            @app.route("/logout") 
        
            231
            def logout(): 
        
            232
                flask.session.pop("username", None) 
        
            233
                flask.flash("You have been logged out.") 
        
            234
                return flask.redirect("/") 
        
            235
            @app.route("/signup", methods=["POST"]) 
        
            238
            def signup(): 
        
            239
                username = flask.request.form["username"] 
        
            240
                password = flask.request.form["password"] 
        
            241
                if db.session.get(User, username) is not None: 
        
            243
                    flask.flash("This username is already taken.") 
        
            244
                    return flask.redirect("/accounts") 
        
            245
                if set(username) > set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"): 
        
            247
                    flask.flash("Usernames can only contain the Latin alphabet, digits, hyphens, and underscores.") 
        
            248
                    return flask.redirect("/accounts") 
        
            249
                if len(username) < 3 or len(username) > 32: 
        
            251
                    flask.flash("Usernames must be between 3 and 32 characters long.") 
        
            252
                    return flask.redirect("/accounts") 
        
            253
                if len(password) < 6: 
        
            255
                    flask.flash("Passwords must be at least 6 characters long.") 
        
            256
                    return flask.redirect("/accounts") 
        
            257
                user = User(username, password) 
        
            259
                db.session.add(user) 
        
            260
                db.session.commit() 
        
            261
                flask.session["username"] = username 
        
            263
                flask.flash("You have been registered and logged in.") 
        
            265
                return flask.redirect("/") 
        
            267
            @app.route("/profile", defaults={"username": None}) 
        
            270
            @app.route("/profile/<username>") 
        
            271
            def profile(username): 
        
            272
                if username is None: 
        
            273
                    if "username" in flask.session: 
        
            274
                        return flask.redirect("/profile/" + flask.session["username"]) 
        
            275
                    else: 
        
            276
                        flask.flash("Please log in to perform this action.") 
        
            277
                        return flask.redirect("/accounts") 
        
            278
                user = db.session.get(User, username) 
        
            280
                if user is None: 
        
            281
                    return flask.abort(404) 
        
            282
                return flask.render_template("profile.html", user=user) 
        
            284
            @app.route("/upload") 
        
            287
            def upload(): 
        
            288
                return flask.render_template("upload.html") 
        
            289
            @app.route("/upload", methods=["POST"]) 
        
            292
            def upload_post(): 
        
            293
                title = flask.request.form["title"] 
        
            294
                description = flask.request.form["description"] 
        
            295
                origin_url = flask.request.form["origin_url"] 
        
            296
                author = db.session.get(User, flask.session.get("username")) 
        
            297
                file = flask.request.files["file"] 
        
            299
                if not file or not file.filename: 
        
            301
                    flask.flash("No selected file") 
        
            302
                    return flask.redirect(flask.request.url) 
        
            303
                resource = PictureResource(title, author, description, origin_url, ["CC0-1.0"], file.mimetype) 
        
            305
                db.session.add(resource) 
        
            306
                db.session.commit() 
        
            307
                file.save(path.join(config.DATA_PATH, "pictures", str(resource.id))) 
        
            308
                return flask.redirect("/picture/" + str(resource.id)) 
        
            310
            @app.route("/picture/<int:id>/") 
        
            313
            def picture(id): 
        
            314
                resource = db.session.get(PictureResource, id) 
        
            315
                if resource is None: 
        
            316
                    return flask.abort(404) 
        
            317
                image = Image.open(path.join(config.DATA_PATH, "pictures", str(resource.id))) 
        
            319
                return flask.render_template("picture.html", resource=resource, 
        
            321
                                             file_extension=mimetypes.guess_extension(resource.file_format), 
        
            322
                                             size=image.size) 
        
            323
            @app.route("/picture/<int:id>/annotate") 
        
            327
            def annotate_picture(id): 
        
            328
                resource = db.session.get(PictureResource, id) 
        
            329
                current_user = db.session.get(User, flask.session.get("username")) 
        
            330
                if resource.author != current_user and not current_user.admin: 
        
            331
                    return flask.abort(403) 
        
            332
                if resource is None: 
        
            334
                    return flask.abort(404) 
        
            335
                return flask.render_template("picture-annotation.html", resource=resource, 
        
            337
                                             file_extension=mimetypes.guess_extension(resource.file_format)) 
        
            338
            @app.route("/picture/<int:id>/save-annotations", methods=["POST"]) 
        
            341
            def save_annotations(id): 
        
            342
                resource = db.session.get(PictureResource, id) 
        
            343
                if resource is None: 
        
            344
                    return flask.abort(404) 
        
            345
                current_user = db.session.get(User, flask.session.get("username")) 
        
            347
                if resource.author != current_user and not current_user.admin: 
        
            348
                    return flask.abort(403) 
        
            349
                # Delete all previous annotations 
        
            351
                db.session.query(PictureRegion).filter_by(resource_id=id).delete() 
        
            352
                json = flask.request.json 
        
            354
                for region in json: 
        
            355
                    object_id = region["object"] 
        
            356
                    picture_object = db.session.get(PictureObject, object_id) 
        
            357
                    region_data = { 
        
            359
                        "type": region["type"], 
        
            360
                        "shape": region["shape"], 
        
            361
                    } 
        
            362
                    region_row = PictureRegion(region_data, resource, picture_object) 
        
            364
                    db.session.add(region_row) 
        
            365
                db.session.commit() 
        
            368
                response = flask.make_response() 
        
            370
                response.status_code = 204 
        
            371
                return response 
        
            372
            @app.route("/picture/<int:id>/get-annotations") 
        
            375
            def get_annotations(id): 
        
            376
                resource = db.session.get(PictureResource, id) 
        
            377
                if resource is None: 
        
            378
                    return flask.abort(404) 
        
            379
                regions = db.session.query(PictureRegion).filter_by(resource_id=id).all() 
        
            381
                regions_json = [] 
        
            383
                for region in regions: 
        
            385
                    regions_json.append({ 
        
            386
                        "object": region.object_id, 
        
            387
                        "type": region.json["type"], 
        
            388
                        "shape": region.json["shape"], 
        
            389
                    }) 
        
            390
                return flask.jsonify(regions_json) 
        
            392
            @app.route("/raw/picture/<int:id>") 
        
            395
            def raw_picture(id): 
        
            396
                resource = db.session.get(PictureResource, id) 
        
            397
                if resource is None: 
        
            398
                    return flask.abort(404) 
        
            399
                response = flask.send_from_directory(path.join(config.DATA_PATH, "pictures"), str(resource.id)) 
        
            401
                response.mimetype = resource.file_format 
        
            402
                return response 
        
            404
            @app.route("/api/object-types") 
        
            407
            def object_types(): 
        
            408
                objects = db.session.query(PictureObject).all() 
        
            409
                return flask.jsonify({object.id: object.description for object in objects}) 
        
            410