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