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 84class PictureNature(db.Model): 85# Examples: 86# "photo", "paper-scan", "2d-art-photo", "sculpture-photo", "computer-3d", "computer-painting", 87# "computer-line-art", "diagram", "infographic", "text", "map", "chart-graph", "screen-capture", 88# "screen-photo", "pattern", "collage", "ai", and so on 89id = db.Column(db.String(64), primary_key=True) 90description = db.Column(db.UnicodeText, nullable=False) 91resources = db.relationship("PictureResource", backref="nature") 92 93def __init__(self, id, description): 94self.id = id 95self.description = description 96 97 98class PictureObjectInheritance(db.Model): 99parent_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"), 100primary_key=True) 101child_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"), 102primary_key=True) 103 104parent = db.relationship("PictureObject", foreign_keys=[parent_id], 105backref="child_links") 106child = db.relationship("PictureObject", foreign_keys=[child_id], 107backref="parent_links") 108 109def __init__(self, parent, child): 110self.parent = parent 111self.child = child 112 113 114class PictureObject(db.Model): 115id = db.Column(db.String(64), primary_key=True) 116description = db.Column(db.UnicodeText, nullable=False) 117 118child_links = db.relationship("PictureObjectInheritance", 119foreign_keys=[PictureObjectInheritance.parent_id], 120backref="parent") 121parent_links = db.relationship("PictureObjectInheritance", 122foreign_keys=[PictureObjectInheritance.child_id], 123backref="child") 124 125def __init__(self, id, description): 126self.id = id 127self.description = description 128 129class PictureRegion(db.Model): 130# This is for picture region annotations 131id = db.Column(db.Integer, primary_key=True, autoincrement=True) 132json = db.Column(sqlalchemy.dialects.postgresql.JSONB, nullable=False) 133 134resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), nullable=False) 135object_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"), nullable=False) 136 137resource = db.relationship("PictureResource", backref="regions") 138object = db.relationship("PictureObject", backref="regions") 139 140def __init__(self, json, resource, object): 141self.json = json 142self.resource = resource 143self.object = object 144 145 146class PictureResource(Resource): 147# This is only for bitmap pictures. Vectors will be stored under a different model 148# File name is the ID in the picture directory under data, without an extension 149file_format = db.Column(db.String(64), nullable=False) # MIME type 150width = db.Column(db.Integer, nullable=False) 151height = db.Column(db.Integer, nullable=False) 152nature_id = db.Column(db.String(32), db.ForeignKey("picture_nature.id"), nullable=False) 153 154nature = db.relationship("PictureNature", back_populates="resources") 155 156replaces_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), nullable=True) 157replaced_by_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), nullable=True) # if this is set, this resource is obsolete and hidden from search results, only available if asked for 158 159replaces = db.relationship("PictureResource", foreign_keys=[replaces_id], backref="replaced_by") 160replaced_by = db.relationship("PictureResource", foreign_keys=[replaced_by_id], backref="replaces") 161 162 163licences = db.relationship("ResourceLicence", # a work can be under multiple licences; at least one must be free, but others can be proprietary 164back_populates="resource") 165 166 167def __init__(self, title, description, origin_url, licence_ids, mime, size, nature, replaces=None): 168self.title = title 169self.description = description 170self.origin_url = origin_url 171self.file_format = mime 172self.width, self.height = size 173self.nature = nature 174db.session.add(self) 175db.session.commit() 176for licence_id in licence_ids: 177joiner = PictureLicence(self.id, licence_id) 178db.session.add(joiner) 179if replaces is not None: 180self.replaces = replaces 181replaces.replaced_by = self 182 183 184@app.route("/") 185def index(): 186return flask.render_template("home.html") 187 188 189@app.route("/accounts") 190def accounts(): 191return flask.render_template("login.html") 192 193 194@app.route("/login", methods=["POST"]) 195def login(): 196username = flask.request.form["username"] 197password = flask.request.form["password"] 198 199user = db.session.get(User, username) 200 201if user is None: 202flask.flash("This username is not registered.") 203return flask.redirect("/accounts") 204 205if not bcrypt.check_password_hash(user.password_hashed, password): 206flask.flash("Incorrect password.") 207return flask.redirect("/accounts") 208 209flask.flash("You have been logged in.") 210 211flask.session["username"] = username 212return flask.redirect("/") 213 214 215@app.route("/logout") 216def logout(): 217flask.session.pop("username", None) 218flask.flash("You have been logged out.") 219return flask.redirect("/") 220 221 222@app.route("/signup", methods=["POST"]) 223def signup(): 224username = flask.request.form["username"] 225password = flask.request.form["password"] 226 227if db.session.get(User, username) is not None: 228flask.flash("This username is already taken.") 229return flask.redirect("/accounts") 230 231if set(username) > set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"): 232flask.flash("Usernames can only contain the Latin alphabet, digits, hyphens, and underscores.") 233return flask.redirect("/accounts") 234 235if len(username) < 3 or len(username) > 32: 236flask.flash("Usernames must be between 3 and 32 characters long.") 237return flask.redirect("/accounts") 238 239if len(password) < 6: 240flask.flash("Passwords must be at least 6 characters long.") 241return flask.redirect("/accounts") 242 243user = User(username, None, password) 244db.session.add(user) 245db.session.commit() 246 247flask.session["username"] = username 248 249flask.flash("You have been registered and logged in.") 250 251return flask.redirect("/") 252 253 254@app.route("/profile", defaults={"username": None}) 255@app.route("/profile/<username>") 256def profile(username): 257if username is None: 258if "username" in flask.session: 259return flask.redirect("/profile/" + flask.session["username"]) 260else: 261flask.flash("Please log in to perform this action.") 262return flask.redirect("/accounts") 263 264user = db.session.get(User, username) 265if user is None: 266return flask.abort(404) 267 268return flask.render_template("profile.html", user=user) 269