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