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