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