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