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