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
            import config 
        
            17
            import markdown 
        
            18
            app = flask.Flask(__name__) 
        
            21
            bcrypt = Bcrypt(app) 
        
            22
            app.config["SQLALCHEMY_DATABASE_URI"] = config.DB_URI 
        
            25
            app.config["SECRET_KEY"] = config.DB_PASSWORD 
        
            26
            db = SQLAlchemy(app) 
        
            29
            migrate = Migrate(app, db) 
        
            30
            @app.template_filter("split") 
        
            33
            def split(value, separator=None, maxsplit=-1): 
        
            34
                return value.split(separator, maxsplit) 
        
            35
            with app.app_context(): 
        
            39
                class User(db.Model): 
        
            40
                    username = db.Column(db.String(32), unique=True, nullable=False, primary_key=True) 
        
            41
                    password_hashed = db.Column(db.String(60), nullable=False) 
        
            42
                    admin = db.Column(db.Boolean, nullable=False, default=False, server_default="false") 
        
            43
                    pictures = db.relationship("PictureResource", back_populates="author") 
        
            44
                    def __init__(self, username, password): 
        
            46
                        self.username = username 
        
            47
                        self.password_hashed = bcrypt.generate_password_hash(password).decode("utf-8") 
        
            48
                class Licence(db.Model): 
        
            51
                    id = db.Column(db.String(64), primary_key=True)               # SPDX identifier 
        
            52
                    title = db.Column(db.UnicodeText, nullable=False)             # the official name of the licence 
        
            53
                    description = db.Column(db.UnicodeText, nullable=False)       # brief description of its permissions and restrictions 
        
            54
                    legal_text = db.Column(db.UnicodeText, nullable=False)        # the full legal text of the licence 
        
            55
                    url = db.Column(db.String(2048), nullable=True)               # the URL to a page with the full text of the licence and more information 
        
            56
                    pictures = db.relationship("PictureLicence", back_populates="licence") 
        
            57
                    free = db.Column(db.Boolean, nullable=False, default=False)   # whether the licence is free or not 
        
            58
                    def __init__(self, id, title, description, legal_text, url, free): 
        
            60
                        self.id = id 
        
            61
                        self.title = title 
        
            62
                        self.description = description 
        
            63
                        self.legal_text = legal_text 
        
            64
                        self.url = url 
        
            65
                        self.free = free 
        
            66
                class PictureLicence(db.Model): 
        
            69
                    id = db.Column(db.Integer, primary_key=True, autoincrement=True) 
        
            70
                    resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id")) 
        
            72
                    licence_id = db.Column(db.String(32), db.ForeignKey("licence.id")) 
        
            73
                    resource = db.relationship("PictureResource", back_populates="licences") 
        
            75
                    licence = db.relationship("Licence", back_populates="pictures") 
        
            76
                    def __init__(self, resource_id, licence_id): 
        
            78
                        self.resource_id = resource_id 
        
            79
                        self.licence_id = licence_id 
        
            80
                class Resource(db.Model): 
        
            83
                    __abstract__ = True 
        
            84
                    id = db.Column(db.Integer, primary_key=True, autoincrement=True) 
        
            86
                    title = db.Column(db.UnicodeText, nullable=False) 
        
            87
                    description = db.Column(db.UnicodeText, nullable=False) 
        
            88
                    timestamp = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) 
        
            89
                    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 
        
            90
                class PictureNature(db.Model): 
        
            93
                    # Examples: 
        
            94
                    # "photo", "paper-scan", "2d-art-photo", "sculpture-photo", "computer-3d", "computer-painting", 
        
            95
                    # "computer-line-art", "diagram", "infographic", "text", "map", "chart-graph", "screen-capture", 
        
            96
                    # "screen-photo", "pattern", "collage", "ai", and so on 
        
            97
                    id = db.Column(db.String(64), primary_key=True) 
        
            98
                    description = db.Column(db.UnicodeText, nullable=False) 
        
            99
                    resources = db.relationship("PictureResource", back_populates="nature") 
        
            100
                    def __init__(self, id, description): 
        
            102
                        self.id = id 
        
            103
                        self.description = description 
        
            104
                class PictureObjectInheritance(db.Model): 
        
            107
                    parent_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"), 
        
            108
                                          primary_key=True) 
        
            109
                    child_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"), 
        
            110
                                         primary_key=True) 
        
            111
                    parent = db.relationship("PictureObject", foreign_keys=[parent_id], 
        
            113
                                             back_populates="child_links") 
        
            114
                    child = db.relationship("PictureObject", foreign_keys=[child_id], 
        
            115
                                            back_populates="parent_links") 
        
            116
                    def __init__(self, parent, child): 
        
            118
                        self.parent = parent 
        
            119
                        self.child = child 
        
            120
                class PictureObject(db.Model): 
        
            123
                    id = db.Column(db.String(64), primary_key=True) 
        
            124
                    description = db.Column(db.UnicodeText, nullable=False) 
        
            125
                    child_links = db.relationship("PictureObjectInheritance", 
        
            127
                                                  foreign_keys=[PictureObjectInheritance.parent_id], 
        
            128
                                                  back_populates="parent") 
        
            129
                    parent_links = db.relationship("PictureObjectInheritance", 
        
            130
                                                   foreign_keys=[PictureObjectInheritance.child_id], 
        
            131
                                                   back_populates="child") 
        
            132
                    def __init__(self, id, description): 
        
            134
                        self.id = id 
        
            135
                        self.description = description 
        
            136
                class PictureRegion(db.Model): 
        
            139
                    # This is for picture region annotations 
        
            140
                    id = db.Column(db.Integer, primary_key=True, autoincrement=True) 
        
            141
                    json = db.Column(sqlalchemy.dialects.postgresql.JSONB, nullable=False) 
        
            142
                    resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), nullable=False) 
        
            144
                    object_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"), nullable=True) 
        
            145
                    resource = db.relationship("PictureResource", backref="regions") 
        
            147
                    object = db.relationship("PictureObject", backref="regions") 
        
            148
                    def __init__(self, json, resource, object): 
        
            150
                        self.json = json 
        
            151
                        self.resource = resource 
        
            152
                        self.object = object 
        
            153
                class PictureResource(Resource): 
        
            156
                    # This is only for bitmap pictures. Vectors will be stored under a different model 
        
            157
                    # File name is the ID in the picture directory under data, without an extension 
        
            158
                    file_format = db.Column(db.String(64), nullable=False)        # MIME type 
        
            159
                    width = db.Column(db.Integer, nullable=False) 
        
            160
                    height = db.Column(db.Integer, nullable=False) 
        
            161
                    nature_id = db.Column(db.String(32), db.ForeignKey("picture_nature.id"), nullable=True) 
        
            162
                    author_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 
        
            163
                    author = db.relationship("User", back_populates="pictures") 
        
            164
                    nature = db.relationship("PictureNature", back_populates="resources") 
        
            166
                    replaces_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), nullable=True) 
        
            168
                    replaced_by_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), 
        
            169
                                               nullable=True) 
        
            170
                    replaces = db.relationship("PictureResource", remote_side="PictureResource.id", 
        
            172
                                               foreign_keys=[replaces_id], back_populates="replaced_by") 
        
            173
                    replaced_by = db.relationship("PictureResource", remote_side="PictureResource.id", 
        
            174
                                                  foreign_keys=[replaced_by_id]) 
        
            175
                    licences = db.relationship("PictureLicence", back_populates="resource") 
        
            177
                    def __init__(self, title, author, description, origin_url, licence_ids, mime, nature=None, 
        
            179
                                 replaces=None): 
        
            180
                        self.title = title 
        
            181
                        self.author = author 
        
            182
                        self.description = description 
        
            183
                        self.origin_url = origin_url 
        
            184
                        self.file_format = mime 
        
            185
                        self.width = self.height = 0 
        
            186
                        self.nature = nature 
        
            187
                        db.session.add(self) 
        
            188
                        db.session.commit() 
        
            189
                        for licence_id in licence_ids: 
        
            190
                            joiner = PictureLicence(self.id, licence_id) 
        
            191
                            db.session.add(joiner) 
        
            192
                        if replaces is not None: 
        
            193
                            self.replaces = replaces 
        
            194
                            replaces.replaced_by = self 
        
            195
            @app.route("/") 
        
            198
            def index(): 
        
            199
                return flask.render_template("home.html") 
        
            200
            @app.route("/accounts/") 
        
            203
            def accounts(): 
        
            204
                return flask.render_template("login.html") 
        
            205
            @app.route("/login", methods=["POST"]) 
        
            208
            def login(): 
        
            209
                username = flask.request.form["username"] 
        
            210
                password = flask.request.form["password"] 
        
            211
                user = db.session.get(User, username) 
        
            213
                if user is None: 
        
            215
                    flask.flash("This username is not registered.") 
        
            216
                    return flask.redirect("/accounts") 
        
            217
                if not bcrypt.check_password_hash(user.password_hashed, password): 
        
            219
                    flask.flash("Incorrect password.") 
        
            220
                    return flask.redirect("/accounts") 
        
            221
                flask.flash("You have been logged in.") 
        
            223
                flask.session["username"] = username 
        
            225
                return flask.redirect("/") 
        
            226
            @app.route("/logout") 
        
            229
            def logout(): 
        
            230
                flask.session.pop("username", None) 
        
            231
                flask.flash("You have been logged out.") 
        
            232
                return flask.redirect("/") 
        
            233
            @app.route("/signup", methods=["POST"]) 
        
            236
            def signup(): 
        
            237
                username = flask.request.form["username"] 
        
            238
                password = flask.request.form["password"] 
        
            239
                if db.session.get(User, username) is not None: 
        
            241
                    flask.flash("This username is already taken.") 
        
            242
                    return flask.redirect("/accounts") 
        
            243
                if set(username) > set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"): 
        
            245
                    flask.flash("Usernames can only contain the Latin alphabet, digits, hyphens, and underscores.") 
        
            246
                    return flask.redirect("/accounts") 
        
            247
                if len(username) < 3 or len(username) > 32: 
        
            249
                    flask.flash("Usernames must be between 3 and 32 characters long.") 
        
            250
                    return flask.redirect("/accounts") 
        
            251
                if len(password) < 6: 
        
            253
                    flask.flash("Passwords must be at least 6 characters long.") 
        
            254
                    return flask.redirect("/accounts") 
        
            255
                user = User(username, password) 
        
            257
                db.session.add(user) 
        
            258
                db.session.commit() 
        
            259
                flask.session["username"] = username 
        
            261
                flask.flash("You have been registered and logged in.") 
        
            263
                return flask.redirect("/") 
        
            265
            @app.route("/profile", defaults={"username": None}) 
        
            268
            @app.route("/profile/<username>") 
        
            269
            def profile(username): 
        
            270
                if username is None: 
        
            271
                    if "username" in flask.session: 
        
            272
                        return flask.redirect("/profile/" + flask.session["username"]) 
        
            273
                    else: 
        
            274
                        flask.flash("Please log in to perform this action.") 
        
            275
                        return flask.redirect("/accounts") 
        
            276
                user = db.session.get(User, username) 
        
            278
                if user is None: 
        
            279
                    return flask.abort(404) 
        
            280
                return flask.render_template("profile.html", user=user) 
        
            282
            @app.route("/upload") 
        
            285
            def upload(): 
        
            286
                return flask.render_template("upload.html") 
        
            287
            @app.route("/upload", methods=["POST"]) 
        
            290
            def upload_post(): 
        
            291
                title = flask.request.form["title"] 
        
            292
                description = flask.request.form["description"] 
        
            293
                origin_url = flask.request.form["origin_url"] 
        
            294
                author = db.session.get(User, flask.session.get("username")) 
        
            295
                file = flask.request.files["file"] 
        
            297
                if not file or not file.filename: 
        
            299
                    flask.flash("No selected file") 
        
            300
                    return flask.redirect(flask.request.url) 
        
            301
                resource = PictureResource(title, author, description, origin_url, ["CC0-1.0"], file.mimetype) 
        
            303
                db.session.add(resource) 
        
            304
                db.session.commit() 
        
            305
                file.save(path.join(config.DATA_PATH, "pictures", str(resource.id))) 
        
            306
                return flask.redirect("/picture/" + str(resource.id)) 
        
            308
            @app.route("/picture/<int:id>/") 
        
            311
            def picture(id): 
        
            312
                resource = db.session.get(PictureResource, id) 
        
            313
                if resource is None: 
        
            314
                    return flask.abort(404) 
        
            315
                return flask.render_template("picture.html", resource=resource, 
        
            317
                                             file_extension=mimetypes.guess_extension(resource.file_format)) 
        
            318
            @app.route("/picture/<int:id>/annotate") 
        
            322
            def annotate_picture(id): 
        
            323
                resource = db.session.get(PictureResource, id) 
        
            324
                current_user = db.session.get(User, flask.session.get("username")) 
        
            325
                if resource.author != current_user and not current_user.admin: 
        
            326
                    return flask.abort(403) 
        
            327
                if resource is None: 
        
            329
                    return flask.abort(404) 
        
            330
                return flask.render_template("picture-annotation.html", resource=resource, 
        
            332
                                             file_extension=mimetypes.guess_extension(resource.file_format)) 
        
            333
            @app.route("/picture/<int:id>/save-annotations", methods=["POST"]) 
        
            336
            def save_annotations(id): 
        
            337
                resource = db.session.get(PictureResource, id) 
        
            338
                if resource is None: 
        
            339
                    return flask.abort(404) 
        
            340
                current_user = db.session.get(User, flask.session.get("username")) 
        
            342
                if resource.author != current_user and not current_user.admin: 
        
            343
                    return flask.abort(403) 
        
            344
                # Delete all previous annotations 
        
            346
                db.session.query(PictureRegion).filter_by(resource_id=id).delete() 
        
            347
                json = flask.request.json 
        
            349
                for region in json: 
        
            350
                    object_id = region["object"] 
        
            351
                    picture_object = db.session.get(PictureObject, object_id) 
        
            352
                    region_data = { 
        
            354
                        "type": region["type"], 
        
            355
                        "shape": region["shape"], 
        
            356
                    } 
        
            357
                    region_row = PictureRegion(region_data, resource, picture_object) 
        
            359
                    db.session.add(region_row) 
        
            360
                db.session.commit() 
        
            363
                response = flask.make_response() 
        
            365
                response.status_code = 204 
        
            366
                return response 
        
            367
            @app.route("/picture/<int:id>/get-annotations") 
        
            370
            def get_annotations(id): 
        
            371
                resource = db.session.get(PictureResource, id) 
        
            372
                if resource is None: 
        
            373
                    return flask.abort(404) 
        
            374
                regions = db.session.query(PictureRegion).filter_by(resource_id=id).all() 
        
            376
                regions_json = [] 
        
            378
                for region in regions: 
        
            380
                    regions_json.append({ 
        
            381
                        "object": region.object_id, 
        
            382
                        "type": region.json["type"], 
        
            383
                        "shape": region.json["shape"], 
        
            384
                    }) 
        
            385
                return flask.jsonify(regions_json) 
        
            387
            @app.route("/raw/picture/<int:id>") 
        
            390
            def raw_picture(id): 
        
            391
                resource = db.session.get(PictureResource, id) 
        
            392
                if resource is None: 
        
            393
                    return flask.abort(404) 
        
            394
                response = flask.send_from_directory(path.join(config.DATA_PATH, "pictures"), str(resource.id)) 
        
            396
                response.mimetype = resource.file_format 
        
            397
                return response 
        
            399
            @app.route("/api/object-types") 
        
            402
            def object_types(): 
        
            403
                objects = db.session.query(PictureObject).all() 
        
            404
                return flask.jsonify({object.id: object.description for object in objects}) 
        
            405