app.py
Python script, ASCII text executable
1from datetime import datetime 2from email.policy import default 3 4import flask 5from flask_sqlalchemy import SQLAlchemy 6from flask_bcrypt import Bcrypt 7from flask_httpauth import HTTPBasicAuth 8from markupsafe import escape, Markup 9from flask_migrate import Migrate 10from jinja2_fragments.flask import render_block 11from sqlalchemy.orm import backref 12import sqlalchemy.dialects.postgresql 13from os import path 14 15import config 16import markdown 17 18 19app = flask.Flask(__name__) 20bcrypt = Bcrypt(app) 21 22 23app.config["SQLALCHEMY_DATABASE_URI"] = config.DB_URI 24app.config["SECRET_KEY"] = config.DB_PASSWORD 25 26 27db = SQLAlchemy(app) 28migrate = Migrate(app, db) 29 30 31@app.template_filter("split") 32def split(value, separator=None, maxsplit=-1): 33return value.split(separator, maxsplit) 34 35 36 37with app.app_context(): 38class User(db.Model): 39username = db.Column(db.String(32), unique=True, nullable=False, primary_key=True) 40password_hashed = db.Column(db.String(60), nullable=False) 41admin = db.Column(db.Boolean, nullable=False, default=False, server_default="false") 42 43def __init__(self, username, password): 44self.username = username 45self.password_hashed = bcrypt.generate_password_hash(password).decode("utf-8") 46 47 48class Licence(db.Model): 49id = db.Column(db.String(32), primary_key=True) # SPDX identifier 50title = db.Column(db.UnicodeText, nullable=False) # the official name of the licence 51description = db.Column(db.UnicodeText, nullable=False) # brief description of its permissions and restrictions 52legal_text = db.Column(db.UnicodeText, nullable=False) # the full legal text of the licence 53url = db.Column(db.String(2048), nullable=True) # the URL to a page with the full text of the licence and more information 54pictures = db.relationship("PictureLicence", back_populates="licence") 55 56def __init__(self, id, title, description, legal_text, url): 57self.id = id 58self.title = title 59self.description = description 60self.legal_text = legal_text 61self.url = url 62 63 64class PictureLicence(db.Model): 65id = db.Column(db.Integer, primary_key=True, autoincrement=True) 66 67resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id")) 68licence_id = db.Column(db.String(32), db.ForeignKey("licence.id")) 69 70resource = db.relationship("PictureResource", back_populates="licences") 71licence = db.relationship("Licence", back_populates="pictures") 72 73def __init__(self, resource_id, licence_id): 74self.resource_id = resource_id 75self.licence_id = licence_id 76 77 78class Resource(db.Model): 79__abstract__ = True 80 81id = db.Column(db.Integer, primary_key=True, autoincrement=True) 82title = db.Column(db.UnicodeText, nullable=False) 83description = db.Column(db.UnicodeText, nullable=False) 84timestamp = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) 85origin_url = db.Column(db.String(2048), nullable=True) # should be left empty if it's original or the source is unknown but public domain 86 87 88class PictureNature(db.Model): 89# Examples: 90# "photo", "paper-scan", "2d-art-photo", "sculpture-photo", "computer-3d", "computer-painting", 91# "computer-line-art", "diagram", "infographic", "text", "map", "chart-graph", "screen-capture", 92# "screen-photo", "pattern", "collage", "ai", and so on 93id = db.Column(db.String(64), primary_key=True) 94description = db.Column(db.UnicodeText, nullable=False) 95resources = db.relationship("PictureResource", back_populates="nature") 96 97def __init__(self, id, description): 98self.id = id 99self.description = description 100 101 102class PictureObjectInheritance(db.Model): 103parent_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"), 104primary_key=True) 105child_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"), 106primary_key=True) 107 108parent = db.relationship("PictureObject", foreign_keys=[parent_id], 109back_populates="child_links") 110child = db.relationship("PictureObject", foreign_keys=[child_id], 111back_populates="parent_links") 112 113def __init__(self, parent, child): 114self.parent = parent 115self.child = child 116 117 118class PictureObject(db.Model): 119id = db.Column(db.String(64), primary_key=True) 120description = db.Column(db.UnicodeText, nullable=False) 121 122child_links = db.relationship("PictureObjectInheritance", 123foreign_keys=[PictureObjectInheritance.parent_id], 124back_populates="parent") 125parent_links = db.relationship("PictureObjectInheritance", 126foreign_keys=[PictureObjectInheritance.child_id], 127back_populates="child") 128 129def __init__(self, id, description): 130self.id = id 131self.description = description 132 133 134class PictureRegion(db.Model): 135# This is for picture region annotations 136id = db.Column(db.Integer, primary_key=True, autoincrement=True) 137json = db.Column(sqlalchemy.dialects.postgresql.JSONB, nullable=False) 138 139resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), nullable=False) 140object_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"), nullable=False) 141 142resource = db.relationship("PictureResource", backref="regions") 143object = db.relationship("PictureObject", backref="regions") 144 145def __init__(self, json, resource, object): 146self.json = json 147self.resource = resource 148self.object = object 149 150 151class PictureResource(Resource): 152# This is only for bitmap pictures. Vectors will be stored under a different model 153# File name is the ID in the picture directory under data, without an extension 154file_format = db.Column(db.String(64), nullable=False) # MIME type 155width = db.Column(db.Integer, nullable=False) 156height = db.Column(db.Integer, nullable=False) 157nature_id = db.Column(db.String(32), db.ForeignKey("picture_nature.id"), nullable=True) 158 159nature = db.relationship("PictureNature", back_populates="resources") 160 161replaces_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), nullable=True) 162replaced_by_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), 163nullable=True) 164 165replaces = db.relationship("PictureResource", remote_side="PictureResource.id", 166foreign_keys=[replaces_id], back_populates="replaced_by") 167replaced_by = db.relationship("PictureResource", remote_side="PictureResource.id", 168foreign_keys=[replaced_by_id]) 169 170licences = db.relationship("PictureLicence", back_populates="resource") 171 172def __init__(self, title, description, origin_url, licence_ids, mime, nature=None, 173replaces=None): 174self.title = title 175self.description = description 176self.origin_url = origin_url 177self.file_format = mime 178self.width = self.height = 0 179self.nature = nature 180db.session.add(self) 181db.session.commit() 182for licence_id in licence_ids: 183joiner = PictureLicence(self.id, licence_id) 184db.session.add(joiner) 185if replaces is not None: 186self.replaces = replaces 187replaces.replaced_by = self 188 189 190@app.route("/") 191def index(): 192return flask.render_template("home.html") 193 194 195@app.route("/accounts") 196def accounts(): 197return flask.render_template("login.html") 198 199 200@app.route("/login", methods=["POST"]) 201def login(): 202username = flask.request.form["username"] 203password = flask.request.form["password"] 204 205user = db.session.get(User, username) 206 207if user is None: 208flask.flash("This username is not registered.") 209return flask.redirect("/accounts") 210 211if not bcrypt.check_password_hash(user.password_hashed, password): 212flask.flash("Incorrect password.") 213return flask.redirect("/accounts") 214 215flask.flash("You have been logged in.") 216 217flask.session["username"] = username 218return flask.redirect("/") 219 220 221@app.route("/logout") 222def logout(): 223flask.session.pop("username", None) 224flask.flash("You have been logged out.") 225return flask.redirect("/") 226 227 228@app.route("/signup", methods=["POST"]) 229def signup(): 230username = flask.request.form["username"] 231password = flask.request.form["password"] 232 233if db.session.get(User, username) is not None: 234flask.flash("This username is already taken.") 235return flask.redirect("/accounts") 236 237if set(username) > set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"): 238flask.flash("Usernames can only contain the Latin alphabet, digits, hyphens, and underscores.") 239return flask.redirect("/accounts") 240 241if len(username) < 3 or len(username) > 32: 242flask.flash("Usernames must be between 3 and 32 characters long.") 243return flask.redirect("/accounts") 244 245if len(password) < 6: 246flask.flash("Passwords must be at least 6 characters long.") 247return flask.redirect("/accounts") 248 249user = User(username, None, password) 250db.session.add(user) 251db.session.commit() 252 253flask.session["username"] = username 254 255flask.flash("You have been registered and logged in.") 256 257return flask.redirect("/") 258 259 260@app.route("/profile", defaults={"username": None}) 261@app.route("/profile/<username>") 262def profile(username): 263if username is None: 264if "username" in flask.session: 265return flask.redirect("/profile/" + flask.session["username"]) 266else: 267flask.flash("Please log in to perform this action.") 268return flask.redirect("/accounts") 269 270user = db.session.get(User, username) 271if user is None: 272return flask.abort(404) 273 274return flask.render_template("profile.html", user=user) 275 276 277@app.route("/upload") 278def upload(): 279return flask.render_template("upload.html") 280 281 282@app.route("/upload", methods=["POST"]) 283def upload_post(): 284title = flask.request.form["title"] 285description = flask.request.form["description"] 286origin_url = flask.request.form["origin_url"] 287 288file = flask.request.files["file"] 289 290if not file or not file.filename: 291flask.flash("No selected file") 292return flask.redirect(flask.request.url) 293 294resource = PictureResource(title, description, origin_url, ["CC0-1.0"], file.mimetype) 295db.session.add(resource) 296db.session.commit() 297file.save(path.join(config.DATA_PATH, "pictures", str(resource.id))) 298 299return flask.redirect("/picture/" + str(resource.id)) 300 301 302@app.route("/picture/<int:id>") 303def picture(id): 304resource = db.session.get(PictureResource, id) 305if resource is None: 306return flask.abort(404) 307 308return flask.render_template("picture.html", resource=resource) 309 310 311@app.route("/raw/picture/<int:id>") 312def raw_picture(id): 313resource = db.session.get(PictureResource, id) 314if resource is None: 315return flask.abort(404) 316 317response = flask.send_from_directory(path.join(config.DATA_PATH, "pictures"), str(resource.id)) 318response.mimetype = resource.file_format 319 320return response 321