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