Web platform for sharing free data for ML and research

By using this site, you agree to have cookies stored on your device, strictly for functional purposes, such as storing your session and preferences.

Dismiss

 app.py

View raw Download
text/x-script.python • 49.18 kiB
Python script, ASCII text executable
        
            
1
import json
2
from datetime import datetime
3
from email.policy import default
4
from time import perf_counter
5
6
import flask
7
from flask_sqlalchemy import SQLAlchemy
8
from flask_bcrypt import Bcrypt
9
from flask_httpauth import HTTPBasicAuth
10
from markupsafe import escape, Markup
11
from flask_migrate import Migrate, current
12
from jinja2_fragments.flask import render_block
13
from sqlalchemy import false
14
from sqlalchemy.orm import backref
15
import sqlalchemy.dialects.postgresql
16
from os import path
17
import os
18
from urllib.parse import urlencode
19
import mimetypes
20
import ruamel.yaml as yaml
21
22
from PIL import Image
23
from sqlalchemy.orm.persistence import post_update
24
from sqlalchemy.sql.functions import current_user
25
26
import config
27
import markdown
28
29
app = flask.Flask(__name__)
30
bcrypt = Bcrypt(app)
31
32
app.config["SQLALCHEMY_DATABASE_URI"] = config.DB_URI
33
app.config["SECRET_KEY"] = config.DB_PASSWORD
34
35
db = SQLAlchemy(app)
36
migrate = Migrate(app, db)
37
38
39
@app.template_filter("split")
40
def split(value, separator=None, maxsplit=-1):
41
return value.split(separator, maxsplit)
42
43
44
@app.template_filter("median")
45
def median(value):
46
value = list(value) # prevent generators
47
return sorted(value)[len(value) // 2]
48
49
50
@app.template_filter("set")
51
def set_filter(value):
52
return set(value)
53
54
55
@app.template_global()
56
def modify_query(**new_values):
57
args = flask.request.args.copy()
58
for key, value in new_values.items():
59
args[key] = value
60
61
return f"{flask.request.path}?{urlencode(args)}"
62
63
64
@app.context_processor
65
def default_variables():
66
return {
67
"current_user": db.session.get(User, flask.session.get("username")),
68
}
69
70
71
with app.app_context():
72
class User(db.Model):
73
username = db.Column(db.String(32), unique=True, nullable=False, primary_key=True)
74
password_hashed = db.Column(db.String(60), nullable=False)
75
admin = db.Column(db.Boolean, nullable=False, default=False, server_default="false")
76
pictures = db.relationship("PictureResource", back_populates="author")
77
joined_timestamp = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
78
galleries = db.relationship("Gallery", back_populates="owner")
79
galleries_joined = db.relationship("UserInGallery", back_populates="user")
80
81
def __init__(self, username, password):
82
self.username = username
83
self.password_hashed = bcrypt.generate_password_hash(password).decode("utf-8")
84
85
@property
86
def formatted_name(self):
87
if self.admin:
88
return self.username + "*"
89
return self.username
90
91
92
class Licence(db.Model):
93
id = db.Column(db.String(64), primary_key=True) # SPDX identifier
94
title = db.Column(db.UnicodeText, nullable=False) # the official name of the licence
95
description = db.Column(db.UnicodeText,
96
nullable=False) # brief description of its permissions and restrictions
97
info_url = db.Column(db.String(1024),
98
nullable=False) # the URL to a page with general information about the licence
99
url = db.Column(db.String(1024),
100
nullable=True) # the URL to a page with the full text of the licence and more information
101
pictures = db.relationship("PictureLicence", back_populates="licence")
102
free = db.Column(db.Boolean, nullable=False,
103
default=False) # whether the licence is free or not
104
logo_url = db.Column(db.String(1024), nullable=True) # URL to the logo of the licence
105
pinned = db.Column(db.Boolean, nullable=False,
106
default=False) # whether the licence should be shown at the top of the list
107
108
def __init__(self, id, title, description, info_url, url, free, logo_url=None,
109
pinned=False):
110
self.id = id
111
self.title = title
112
self.description = description
113
self.info_url = info_url
114
self.url = url
115
self.free = free
116
self.logo_url = logo_url
117
self.pinned = pinned
118
119
120
class PictureLicence(db.Model):
121
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
122
123
resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"))
124
licence_id = db.Column(db.String(64), db.ForeignKey("licence.id"))
125
126
resource = db.relationship("PictureResource", back_populates="licences")
127
licence = db.relationship("Licence", back_populates="pictures")
128
129
def __init__(self, resource, licence):
130
self.resource = resource
131
self.licence = licence
132
133
134
class Resource(db.Model):
135
__abstract__ = True
136
137
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
138
title = db.Column(db.UnicodeText, nullable=False)
139
description = db.Column(db.UnicodeText, nullable=False)
140
timestamp = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
141
origin_url = db.Column(db.String(2048),
142
nullable=True) # should be left empty if it's original or the source is unknown but public domain
143
144
145
class PictureNature(db.Model):
146
# Examples:
147
# "photo", "paper-scan", "2d-art-photo", "sculpture-photo", "computer-3d", "computer-painting",
148
# "computer-line-art", "diagram", "infographic", "text", "map", "chart-graph", "screen-capture",
149
# "screen-photo", "pattern", "collage", "ai", and so on
150
id = db.Column(db.String(64), primary_key=True)
151
description = db.Column(db.UnicodeText, nullable=False)
152
resources = db.relationship("PictureResource", back_populates="nature")
153
154
def __init__(self, id, description):
155
self.id = id
156
self.description = description
157
158
159
class PictureObjectInheritance(db.Model):
160
parent_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"),
161
primary_key=True)
162
child_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"),
163
primary_key=True)
164
165
parent = db.relationship("PictureObject", foreign_keys=[parent_id],
166
back_populates="child_links")
167
child = db.relationship("PictureObject", foreign_keys=[child_id],
168
back_populates="parent_links")
169
170
def __init__(self, parent, child):
171
self.parent = parent
172
self.child = child
173
174
175
class PictureObject(db.Model):
176
id = db.Column(db.String(64), primary_key=True)
177
description = db.Column(db.UnicodeText, nullable=False)
178
179
child_links = db.relationship("PictureObjectInheritance",
180
foreign_keys=[PictureObjectInheritance.parent_id],
181
back_populates="parent")
182
parent_links = db.relationship("PictureObjectInheritance",
183
foreign_keys=[PictureObjectInheritance.child_id],
184
back_populates="child")
185
186
def __init__(self, id, description):
187
self.id = id
188
self.description = description
189
190
191
class PictureRegion(db.Model):
192
# This is for picture region annotations
193
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
194
json = db.Column(sqlalchemy.dialects.postgresql.JSONB, nullable=False)
195
196
resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"),
197
nullable=False)
198
object_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"), nullable=True)
199
200
resource = db.relationship("PictureResource", backref="regions")
201
object = db.relationship("PictureObject", backref="regions")
202
203
def __init__(self, json, resource, object):
204
self.json = json
205
self.resource = resource
206
self.object = object
207
208
209
class PictureResource(Resource):
210
# This is only for bitmap pictures. Vectors will be stored under a different model
211
# File name is the ID in the picture directory under data, without an extension
212
file_format = db.Column(db.String(64), nullable=False) # MIME type
213
width = db.Column(db.Integer, nullable=False)
214
height = db.Column(db.Integer, nullable=False)
215
nature_id = db.Column(db.String(32), db.ForeignKey("picture_nature.id"), nullable=True)
216
author_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
217
author = db.relationship("User", back_populates="pictures")
218
219
nature = db.relationship("PictureNature", back_populates="resources")
220
221
replaces_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), nullable=True)
222
replaced_by_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"),
223
nullable=True)
224
225
replaces = db.relationship("PictureResource", remote_side="PictureResource.id",
226
foreign_keys=[replaces_id], back_populates="replaced_by",
227
post_update=True)
228
replaced_by = db.relationship("PictureResource", remote_side="PictureResource.id",
229
foreign_keys=[replaced_by_id], post_update=True)
230
231
copied_from_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"),
232
nullable=True)
233
copied_from = db.relationship("PictureResource", remote_side="PictureResource.id",
234
backref="copies", foreign_keys=[copied_from_id])
235
236
licences = db.relationship("PictureLicence", back_populates="resource")
237
galleries = db.relationship("PictureInGallery", back_populates="resource")
238
239
def __init__(self, title, author, description, origin_url, licence_ids, mime,
240
nature=None):
241
self.title = title
242
self.author = author
243
self.description = description
244
self.origin_url = origin_url
245
self.file_format = mime
246
self.width = self.height = 0
247
self.nature = nature
248
db.session.add(self)
249
db.session.commit()
250
for licence_id in licence_ids:
251
joiner = PictureLicence(self, db.session.get(Licence, licence_id))
252
db.session.add(joiner)
253
254
def put_annotations(self, json):
255
# Delete all previous annotations
256
db.session.query(PictureRegion).filter_by(resource_id=self.id).delete()
257
258
for region in json:
259
object_id = region["object"]
260
picture_object = db.session.get(PictureObject, object_id)
261
262
region_data = {
263
"type": region["type"],
264
"shape": region["shape"],
265
}
266
267
region_row = PictureRegion(region_data, self, picture_object)
268
db.session.add(region_row)
269
270
271
class PictureInGallery(db.Model):
272
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
273
resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"),
274
nullable=False)
275
gallery_id = db.Column(db.Integer, db.ForeignKey("gallery.id"), nullable=False)
276
277
resource = db.relationship("PictureResource")
278
gallery = db.relationship("Gallery")
279
280
def __init__(self, resource, gallery):
281
self.resource = resource
282
self.gallery = gallery
283
284
285
class UserInGallery(db.Model):
286
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
287
username = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
288
gallery_id = db.Column(db.Integer, db.ForeignKey("gallery.id"), nullable=False)
289
290
user = db.relationship("User")
291
gallery = db.relationship("Gallery")
292
293
def __init__(self, user, gallery):
294
self.user = user
295
self.gallery = gallery
296
297
298
class Gallery(db.Model):
299
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
300
title = db.Column(db.UnicodeText, nullable=False)
301
description = db.Column(db.UnicodeText, nullable=False)
302
pictures = db.relationship("PictureInGallery", back_populates="gallery")
303
owner_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
304
owner = db.relationship("User", back_populates="galleries")
305
users = db.relationship("UserInGallery", back_populates="gallery")
306
307
def __init__(self, title, description, owner):
308
self.title = title
309
self.description = description
310
self.owner = owner
311
312
313
@app.route("/")
314
def index():
315
return flask.render_template("home.html", resources=PictureResource.query.order_by(
316
db.func.random()).limit(10).all())
317
318
319
@app.route("/accounts/")
320
def accounts():
321
return flask.render_template("login.html")
322
323
324
@app.route("/login", methods=["POST"])
325
def login():
326
username = flask.request.form["username"]
327
password = flask.request.form["password"]
328
329
user = db.session.get(User, username)
330
331
if user is None:
332
flask.flash("This username is not registered.")
333
return flask.redirect("/accounts")
334
335
if not bcrypt.check_password_hash(user.password_hashed, password):
336
flask.flash("Incorrect password.")
337
return flask.redirect("/accounts")
338
339
flask.flash("You have been logged in.")
340
341
flask.session["username"] = username
342
return flask.redirect("/")
343
344
345
@app.route("/logout")
346
def logout():
347
flask.session.pop("username", None)
348
flask.flash("You have been logged out.")
349
return flask.redirect("/")
350
351
352
@app.route("/signup", methods=["POST"])
353
def signup():
354
username = flask.request.form["username"]
355
password = flask.request.form["password"]
356
357
if db.session.get(User, username) is not None:
358
flask.flash("This username is already taken.")
359
return flask.redirect("/accounts")
360
361
if set(username) > set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"):
362
flask.flash(
363
"Usernames can only contain the Latin alphabet, digits, hyphens, and underscores.")
364
return flask.redirect("/accounts")
365
366
if len(username) < 3 or len(username) > 32:
367
flask.flash("Usernames must be between 3 and 32 characters long.")
368
return flask.redirect("/accounts")
369
370
if len(password) < 6:
371
flask.flash("Passwords must be at least 6 characters long.")
372
return flask.redirect("/accounts")
373
374
user = User(username, password)
375
db.session.add(user)
376
db.session.commit()
377
378
flask.session["username"] = username
379
380
flask.flash("You have been registered and logged in.")
381
382
return flask.redirect("/")
383
384
385
@app.route("/profile", defaults={"username": None})
386
@app.route("/profile/<username>")
387
def profile(username):
388
if username is None:
389
if "username" in flask.session:
390
return flask.redirect("/profile/" + flask.session["username"])
391
else:
392
flask.flash("Please log in to perform this action.")
393
return flask.redirect("/accounts")
394
395
user = db.session.get(User, username)
396
if user is None:
397
flask.abort(404)
398
399
return flask.render_template("profile.html", user=user)
400
401
402
@app.route("/object/<id>")
403
def has_object(id):
404
object_ = db.session.get(PictureObject, id)
405
if object_ is None:
406
flask.abort(404)
407
408
query = db.session.query(PictureResource).join(PictureRegion).filter(
409
PictureRegion.object_id == id)
410
411
page = int(flask.request.args.get("page", 1))
412
per_page = int(flask.request.args.get("per_page", 16))
413
414
resources = query.paginate(page=page, per_page=per_page)
415
416
return flask.render_template("object.html", object=object_, resources=resources,
417
page_number=page,
418
page_length=per_page, num_pages=resources.pages,
419
prev_page=resources.prev_num,
420
next_page=resources.next_num, PictureRegion=PictureRegion)
421
422
423
@app.route("/upload")
424
def upload():
425
if "username" not in flask.session:
426
flask.flash("Log in to upload pictures.")
427
return flask.redirect("/accounts")
428
429
licences = Licence.query.order_by(Licence.free.desc(), Licence.pinned.desc(),
430
Licence.title).all()
431
432
types = PictureNature.query.all()
433
434
return flask.render_template("upload.html", licences=licences, types=types)
435
436
437
@app.route("/upload", methods=["POST"])
438
def upload_post():
439
title = flask.request.form["title"]
440
description = flask.request.form["description"]
441
origin_url = flask.request.form["origin_url"]
442
author = db.session.get(User, flask.session.get("username"))
443
licence_ids = flask.request.form.getlist("licence")
444
nature_id = flask.request.form["nature"]
445
446
if author is None:
447
flask.abort(401)
448
449
file = flask.request.files["file"]
450
451
if not file or not file.filename:
452
flask.flash("Select a file")
453
return flask.redirect(flask.request.url)
454
455
if not file.mimetype.startswith("image/") or file.mimetype == "image/svg+xml":
456
flask.flash("Only images are supported")
457
return flask.redirect(flask.request.url)
458
459
if not title:
460
flask.flash("Enter a title")
461
return flask.redirect(flask.request.url)
462
463
if not description:
464
description = ""
465
466
if not nature_id:
467
flask.flash("Select a picture type")
468
return flask.redirect(flask.request.url)
469
470
if not licence_ids:
471
flask.flash("Select licences")
472
return flask.redirect(flask.request.url)
473
474
licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]
475
if not any(licence.free for licence in licences):
476
flask.flash("Select at least one free licence")
477
return flask.redirect(flask.request.url)
478
479
resource = PictureResource(title, author, description, origin_url, licence_ids,
480
file.mimetype,
481
db.session.get(PictureNature, nature_id))
482
db.session.add(resource)
483
db.session.commit()
484
file.save(path.join(config.DATA_PATH, "pictures", str(resource.id)))
485
pil_image = Image.open(path.join(config.DATA_PATH, "pictures", str(resource.id)))
486
resource.width, resource.height = pil_image.size
487
db.session.commit()
488
489
if flask.request.form.get("annotations"):
490
try:
491
resource.put_annotations(json.loads(flask.request.form.get("annotations")))
492
db.session.commit()
493
except json.JSONDecodeError:
494
flask.flash("Invalid annotations")
495
496
flask.flash("Picture uploaded successfully")
497
498
return flask.redirect("/picture/" + str(resource.id))
499
500
501
@app.route("/picture/<int:id>/")
502
def picture(id):
503
resource = db.session.get(PictureResource, id)
504
if resource is None:
505
flask.abort(404)
506
507
image = Image.open(path.join(config.DATA_PATH, "pictures", str(resource.id)))
508
509
current_user = db.session.get(User, flask.session.get("username"))
510
have_permission = current_user and (current_user == resource.author or current_user.admin)
511
512
return flask.render_template("picture.html", resource=resource,
513
file_extension=mimetypes.guess_extension(resource.file_format),
514
size=image.size, copies=resource.copies,
515
have_permission=have_permission)
516
517
518
@app.route("/picture/<int:id>/annotate")
519
def annotate_picture(id):
520
resource = db.session.get(PictureResource, id)
521
if resource is None:
522
flask.abort(404)
523
524
current_user = db.session.get(User, flask.session.get("username"))
525
if current_user is None:
526
flask.abort(401)
527
if resource.author != current_user and not current_user.admin:
528
flask.abort(403)
529
530
return flask.render_template("picture-annotation.html", resource=resource,
531
file_extension=mimetypes.guess_extension(resource.file_format))
532
533
534
@app.route("/picture/<int:id>/put-annotations-form")
535
def put_annotations_form(id):
536
resource = db.session.get(PictureResource, id)
537
if resource is None:
538
flask.abort(404)
539
540
current_user = db.session.get(User, flask.session.get("username"))
541
if current_user is None:
542
flask.abort(401)
543
544
if resource.author != current_user and not current_user.admin:
545
flask.abort(403)
546
547
return flask.render_template("put-annotations-form.html", resource=resource)
548
549
550
@app.route("/picture/<int:id>/put-annotations-form", methods=["POST"])
551
def put_annotations_form_post(id):
552
resource = db.session.get(PictureResource, id)
553
if resource is None:
554
flask.abort(404)
555
556
current_user = db.session.get(User, flask.session.get("username"))
557
if current_user is None:
558
flask.abort(401)
559
560
if resource.author != current_user and not current_user.admin:
561
flask.abort(403)
562
563
resource.put_annotations(json.loads(flask.request.form["annotations"]))
564
565
db.session.commit()
566
567
return flask.redirect("/picture/" + str(resource.id))
568
569
570
@app.route("/picture/<int:id>/save-annotations", methods=["POST"])
571
@app.route("/api/picture/<int:id>/put-annotations", methods=["POST"])
572
def save_annotations(id):
573
resource = db.session.get(PictureResource, id)
574
if resource is None:
575
flask.abort(404)
576
577
current_user = db.session.get(User, flask.session.get("username"))
578
if resource.author != current_user and not current_user.admin:
579
flask.abort(403)
580
581
resource.put_annotations(flask.request.json)
582
583
db.session.commit()
584
585
response = flask.make_response()
586
response.status_code = 204
587
return response
588
589
590
@app.route("/picture/<int:id>/get-annotations")
591
@app.route("/api/picture/<int:id>/api/get-annotations")
592
def get_annotations(id):
593
resource = db.session.get(PictureResource, id)
594
if resource is None:
595
flask.abort(404)
596
597
regions = db.session.query(PictureRegion).filter_by(resource_id=id).all()
598
599
regions_json = []
600
601
for region in regions:
602
regions_json.append({
603
"object": region.object_id,
604
"type": region.json["type"],
605
"shape": region.json["shape"],
606
})
607
608
return flask.jsonify(regions_json)
609
610
611
@app.route("/picture/<int:id>/delete")
612
def delete_picture(id):
613
resource = db.session.get(PictureResource, id)
614
if resource is None:
615
flask.abort(404)
616
617
current_user = db.session.get(User, flask.session.get("username"))
618
if current_user is None:
619
flask.abort(401)
620
621
if resource.author != current_user and not current_user.admin:
622
flask.abort(403)
623
624
PictureLicence.query.filter_by(resource=resource).delete()
625
PictureRegion.query.filter_by(resource=resource).delete()
626
PictureInGallery.query.filter_by(resource=resource).delete()
627
if resource.replaces:
628
resource.replaces.replaced_by = None
629
if resource.replaced_by:
630
resource.replaced_by.replaces = None
631
resource.copied_from = None
632
for copy in resource.copies:
633
copy.copied_from = None
634
db.session.delete(resource)
635
db.session.commit()
636
637
return flask.redirect("/")
638
639
640
@app.route("/picture/<int:id>/mark-replacement", methods=["POST"])
641
def mark_picture_replacement(id):
642
resource = db.session.get(PictureResource, id)
643
if resource is None:
644
flask.abort(404)
645
646
current_user = db.session.get(User, flask.session.get("username"))
647
if current_user is None:
648
flask.abort(401)
649
650
if resource.copied_from.author != current_user and not current_user.admin:
651
flask.abort(403)
652
653
resource.copied_from.replaced_by = resource
654
resource.replaces = resource.copied_from
655
656
db.session.commit()
657
658
return flask.redirect("/picture/" + str(resource.copied_from.id))
659
660
661
@app.route("/picture/<int:id>/remove-replacement", methods=["POST"])
662
def remove_picture_replacement(id):
663
resource = db.session.get(PictureResource, id)
664
if resource is None:
665
flask.abort(404)
666
667
current_user = db.session.get(User, flask.session.get("username"))
668
if current_user is None:
669
flask.abort(401)
670
671
if resource.author != current_user and not current_user.admin:
672
flask.abort(403)
673
674
resource.replaced_by.replaces = None
675
resource.replaced_by = None
676
677
db.session.commit()
678
679
return flask.redirect("/picture/" + str(resource.id))
680
681
682
@app.route("/picture/<int:id>/edit-metadata")
683
def edit_picture(id):
684
resource = db.session.get(PictureResource, id)
685
if resource is None:
686
flask.abort(404)
687
688
current_user = db.session.get(User, flask.session.get("username"))
689
if current_user is None:
690
flask.abort(401)
691
692
if resource.author != current_user and not current_user.admin:
693
flask.abort(403)
694
695
licences = Licence.query.order_by(Licence.free.desc(), Licence.pinned.desc(),
696
Licence.title).all()
697
698
types = PictureNature.query.all()
699
700
return flask.render_template("edit-picture.html", resource=resource, licences=licences,
701
types=types,
702
PictureLicence=PictureLicence)
703
704
705
@app.route("/picture/<int:id>/edit-metadata", methods=["POST"])
706
def edit_picture_post(id):
707
resource = db.session.get(PictureResource, id)
708
if resource is None:
709
flask.abort(404)
710
711
current_user = db.session.get(User, flask.session.get("username"))
712
if current_user is None:
713
flask.abort(401)
714
715
if resource.author != current_user and not current_user.admin:
716
flask.abort(403)
717
718
title = flask.request.form["title"]
719
description = flask.request.form["description"]
720
origin_url = flask.request.form["origin_url"]
721
licence_ids = flask.request.form.getlist("licence")
722
nature_id = flask.request.form["nature"]
723
724
if not title:
725
flask.flash("Enter a title")
726
return flask.redirect(flask.request.url)
727
728
if not description:
729
description = ""
730
731
if not nature_id:
732
flask.flash("Select a picture type")
733
return flask.redirect(flask.request.url)
734
735
if not licence_ids:
736
flask.flash("Select licences")
737
return flask.redirect(flask.request.url)
738
739
licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]
740
if not any(licence.free for licence in licences):
741
flask.flash("Select at least one free licence")
742
return flask.redirect(flask.request.url)
743
744
resource.title = title
745
resource.description = description
746
resource.origin_url = origin_url
747
for licence_id in licence_ids:
748
joiner = PictureLicence(resource, db.session.get(Licence, licence_id))
749
db.session.add(joiner)
750
resource.nature = db.session.get(PictureNature, nature_id)
751
752
db.session.commit()
753
754
return flask.redirect("/picture/" + str(resource.id))
755
756
757
@app.route("/picture/<int:id>/copy")
758
def copy_picture(id):
759
resource = db.session.get(PictureResource, id)
760
if resource is None:
761
flask.abort(404)
762
763
current_user = db.session.get(User, flask.session.get("username"))
764
if current_user is None:
765
flask.abort(401)
766
767
new_resource = PictureResource(resource.title, current_user, resource.description,
768
resource.origin_url,
769
[licence.licence_id for licence in resource.licences],
770
resource.file_format,
771
resource.nature)
772
773
for region in resource.regions:
774
db.session.add(PictureRegion(region.json, new_resource, region.object))
775
776
db.session.commit()
777
778
# Create a hard link for the new picture
779
old_path = path.join(config.DATA_PATH, "pictures", str(resource.id))
780
new_path = path.join(config.DATA_PATH, "pictures", str(new_resource.id))
781
os.link(old_path, new_path)
782
783
new_resource.width = resource.width
784
new_resource.height = resource.height
785
new_resource.copied_from = resource
786
787
db.session.commit()
788
789
return flask.redirect("/picture/" + str(new_resource.id))
790
791
792
@app.route("/gallery/<int:id>/")
793
def gallery(id):
794
gallery = db.session.get(Gallery, id)
795
if gallery is None:
796
flask.abort(404)
797
798
current_user = db.session.get(User, flask.session.get("username"))
799
800
have_permission = current_user and (current_user == gallery.owner or current_user.admin or UserInGallery.query.filter_by(user=current_user, gallery=gallery).first())
801
802
return flask.render_template("gallery.html", gallery=gallery,
803
have_permission=have_permission)
804
805
806
@app.route("/create-gallery")
807
def create_gallery():
808
if "username" not in flask.session:
809
flask.flash("Log in to create galleries.")
810
return flask.redirect("/accounts")
811
812
return flask.render_template("create-gallery.html")
813
814
815
@app.route("/create-gallery", methods=["POST"])
816
def create_gallery_post():
817
if not flask.session.get("username"):
818
flask.abort(401)
819
820
if not flask.request.form.get("title"):
821
flask.flash("Enter a title")
822
return flask.redirect(flask.request.url)
823
824
description = flask.request.form.get("description", "")
825
826
gallery = Gallery(flask.request.form["title"], description,
827
db.session.get(User, flask.session["username"]))
828
db.session.add(gallery)
829
db.session.commit()
830
831
return flask.redirect("/gallery/" + str(gallery.id))
832
833
834
@app.route("/gallery/<int:id>/add-picture", methods=["POST"])
835
def gallery_add_picture(id):
836
gallery = db.session.get(Gallery, id)
837
if gallery is None:
838
flask.abort(404)
839
840
if "username" not in flask.session:
841
flask.abort(401)
842
843
if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
844
flask.abort(403)
845
846
picture_id = flask.request.form.get("picture_id")
847
if "/" in picture_id: # also allow full URLs
848
picture_id = picture_id.rstrip("/").rpartition("/")[1]
849
if not picture_id:
850
flask.flash("Select a picture")
851
return flask.redirect("/gallery/" + str(gallery.id))
852
picture_id = int(picture_id)
853
854
picture = db.session.get(PictureResource, picture_id)
855
if picture is None:
856
flask.flash("Invalid picture")
857
return flask.redirect("/gallery/" + str(gallery.id))
858
859
if PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first():
860
flask.flash("This picture is already in the gallery")
861
return flask.redirect("/gallery/" + str(gallery.id))
862
863
db.session.add(PictureInGallery(picture, gallery))
864
865
db.session.commit()
866
867
return flask.redirect("/gallery/" + str(gallery.id))
868
869
870
@app.route("/gallery/<int:id>/remove-picture", methods=["POST"])
871
def gallery_remove_picture(id):
872
gallery = db.session.get(Gallery, id)
873
if gallery is None:
874
flask.abort(404)
875
876
if "username" not in flask.session:
877
flask.abort(401)
878
879
current_user = db.session.get(User, flask.session.get("username"))
880
881
if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
882
flask.abort(403)
883
884
picture_id = int(flask.request.form.get("picture_id"))
885
886
picture = db.session.get(PictureResource, picture_id)
887
if picture is None:
888
flask.flash("Invalid picture")
889
return flask.redirect("/gallery/" + str(gallery.id))
890
891
picture_in_gallery = PictureInGallery.query.filter_by(resource=picture,
892
gallery=gallery).first()
893
if picture_in_gallery is None:
894
flask.flash("This picture isn't in the gallery")
895
return flask.redirect("/gallery/" + str(gallery.id))
896
897
db.session.delete(picture_in_gallery)
898
899
db.session.commit()
900
901
return flask.redirect("/gallery/" + str(gallery.id))
902
903
904
@app.route("/gallery/<int:id>/add-pictures-from-query", methods=["POST"])
905
def gallery_add_from_query(id):
906
gallery = db.session.get(Gallery, id)
907
if gallery is None:
908
flask.abort(404)
909
910
if "username" not in flask.session:
911
flask.abort(401)
912
913
if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
914
flask.abort(403)
915
916
query_yaml = flask.request.form.get("query", "")
917
918
yaml_parser = yaml.YAML()
919
query_data = yaml_parser.load(query_yaml) or {}
920
query = get_picture_query(query_data)
921
922
pictures = query.all()
923
924
count = 0
925
926
for picture in pictures:
927
if not PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first():
928
db.session.add(PictureInGallery(picture, gallery))
929
count += 1
930
931
db.session.commit()
932
933
flask.flash(f"Added {count} pictures to the gallery")
934
935
return flask.redirect("/gallery/" + str(gallery.id))
936
937
938
@app.route("/gallery/<int:id>/users")
939
def gallery_users(id):
940
gallery = db.session.get(Gallery, id)
941
if gallery is None:
942
flask.abort(404)
943
944
current_user = db.session.get(User, flask.session.get("username"))
945
have_permission = current_user and (current_user == gallery.owner or current_user.admin)
946
947
return flask.render_template("gallery-users.html", gallery=gallery,
948
have_permission=have_permission)
949
950
951
@app.route("/gallery/<int:id>/users/add", methods=["POST"])
952
def gallery_add_user(id):
953
gallery = db.session.get(Gallery, id)
954
if gallery is None:
955
flask.abort(404)
956
957
current_user = db.session.get(User, flask.session.get("username"))
958
if current_user is None:
959
flask.abort(401)
960
961
if current_user != gallery.owner and not current_user.admin:
962
flask.abort(403)
963
964
username = flask.request.form.get("username")
965
if username == gallery.owner_name:
966
flask.flash("The owner is already in the gallery")
967
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
968
969
user = db.session.get(User, username)
970
if user is None:
971
flask.flash("User not found")
972
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
973
974
if UserInGallery.query.filter_by(user=user, gallery=gallery).first():
975
flask.flash("User is already in the gallery")
976
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
977
978
db.session.add(UserInGallery(user, gallery))
979
980
db.session.commit()
981
982
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
983
984
985
@app.route("/gallery/<int:id>/users/remove", methods=["POST"])
986
def gallery_remove_user(id):
987
gallery = db.session.get(Gallery, id)
988
if gallery is None:
989
flask.abort(404)
990
991
current_user = db.session.get(User, flask.session.get("username"))
992
if current_user is None:
993
flask.abort(401)
994
995
if current_user != gallery.owner and not current_user.admin:
996
flask.abort(403)
997
998
username = flask.request.form.get("username")
999
user = db.session.get(User, username)
1000
if user is None:
1001
flask.flash("User not found")
1002
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1003
1004
user_in_gallery = UserInGallery.query.filter_by(user=user, gallery=gallery).first()
1005
if user_in_gallery is None:
1006
flask.flash("User is not in the gallery")
1007
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1008
1009
db.session.delete(user_in_gallery)
1010
1011
db.session.commit()
1012
1013
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1014
1015
1016
class APIError(Exception):
1017
def __init__(self, status_code, message):
1018
self.status_code = status_code
1019
self.message = message
1020
1021
1022
def get_picture_query(query_data):
1023
query = db.session.query(PictureResource)
1024
1025
requirement_conditions = {
1026
"has_object": lambda value: PictureResource.regions.any(
1027
PictureRegion.object_id.in_(value)),
1028
"nature": lambda value: PictureResource.nature_id.in_(value),
1029
"licence": lambda value: PictureResource.licences.any(
1030
PictureLicence.licence_id.in_(value)),
1031
"author": lambda value: PictureResource.author_name.in_(value),
1032
"title": lambda value: PictureResource.title.ilike(value),
1033
"description": lambda value: PictureResource.description.ilike(value),
1034
"origin_url": lambda value: db.func.lower(db.func.substr(
1035
PictureResource.origin_url,
1036
db.func.length(db.func.split_part(PictureResource.origin_url, "://", 1)) + 4
1037
)).in_(value),
1038
"above_width": lambda value: PictureResource.width >= value,
1039
"below_width": lambda value: PictureResource.width <= value,
1040
"above_height": lambda value: PictureResource.height >= value,
1041
"below_height": lambda value: PictureResource.height <= value,
1042
"before_date": lambda value: PictureResource.timestamp <= datetime.utcfromtimestamp(
1043
value),
1044
"after_date": lambda value: PictureResource.timestamp >= datetime.utcfromtimestamp(
1045
value),
1046
"in_gallery": lambda value: PictureResource.galleries.any(PictureInGallery.gallery_id.in_(value)),
1047
}
1048
if "want" in query_data:
1049
for i in query_data["want"]:
1050
if len(i) != 1:
1051
raise APIError(400, "Each requirement must have exactly one key")
1052
requirement, value = list(i.items())[0]
1053
if requirement not in requirement_conditions:
1054
raise APIError(400, f"Unknown requirement type: {requirement}")
1055
1056
condition = requirement_conditions[requirement]
1057
query = query.filter(condition(value))
1058
if "exclude" in query_data:
1059
for i in query_data["exclude"]:
1060
if len(i) != 1:
1061
raise APIError(400, "Each exclusion must have exactly one key")
1062
requirement, value = list(i.items())[0]
1063
if requirement not in requirement_conditions:
1064
raise APIError(400, f"Unknown requirement type: {requirement}")
1065
1066
condition = requirement_conditions[requirement]
1067
query = query.filter(~condition(value))
1068
if not query_data.get("include_obsolete", False):
1069
query = query.filter(PictureResource.replaced_by_id.is_(None))
1070
1071
return query
1072
1073
1074
@app.route("/query-pictures")
1075
def graphical_query_pictures():
1076
return flask.render_template("graphical-query-pictures.html")
1077
1078
1079
@app.route("/query-pictures-results")
1080
def graphical_query_pictures_results():
1081
query_yaml = flask.request.args.get("query", "")
1082
yaml_parser = yaml.YAML()
1083
query_data = yaml_parser.load(query_yaml) or {}
1084
try:
1085
query = get_picture_query(query_data)
1086
except APIError as e:
1087
flask.abort(e.status_code)
1088
1089
page = int(flask.request.args.get("page", 1))
1090
per_page = int(flask.request.args.get("per_page", 16))
1091
1092
resources = query.paginate(page=page, per_page=per_page)
1093
1094
return flask.render_template("graphical-query-pictures-results.html", resources=resources,
1095
query=query_yaml,
1096
page_number=page, page_length=per_page,
1097
num_pages=resources.pages,
1098
prev_page=resources.prev_num, next_page=resources.next_num)
1099
1100
1101
@app.route("/raw/picture/<int:id>")
1102
def raw_picture(id):
1103
resource = db.session.get(PictureResource, id)
1104
if resource is None:
1105
flask.abort(404)
1106
1107
response = flask.send_from_directory(path.join(config.DATA_PATH, "pictures"),
1108
str(resource.id))
1109
response.mimetype = resource.file_format
1110
1111
return response
1112
1113
1114
@app.route("/object/")
1115
def graphical_object_types():
1116
return flask.render_template("object-types.html", objects=PictureObject.query.all())
1117
1118
1119
@app.route("/api/object-types")
1120
def object_types():
1121
objects = db.session.query(PictureObject).all()
1122
return flask.jsonify({object.id: object.description for object in objects})
1123
1124
1125
@app.route("/api/query-pictures", methods=["POST"]) # sadly GET can't have a body
1126
def query_pictures():
1127
offset = int(flask.request.args.get("offset", 0))
1128
limit = int(flask.request.args.get("limit", 16))
1129
ordering = flask.request.args.get("ordering", "date-desc")
1130
1131
yaml_parser = yaml.YAML()
1132
query_data = yaml_parser.load(flask.request.data) or {}
1133
try:
1134
query = get_picture_query(query_data)
1135
except APIError as e:
1136
return flask.jsonify({"error": e.message}), e.status_code
1137
1138
match ordering:
1139
case "date-desc":
1140
query = query.order_by(PictureResource.timestamp.desc())
1141
case "date-asc":
1142
query = query.order_by(PictureResource.timestamp.asc())
1143
case "title-asc":
1144
query = query.order_by(PictureResource.title.asc())
1145
case "title-desc":
1146
query = query.order_by(PictureResource.title.desc())
1147
case "random":
1148
query = query.order_by(db.func.random())
1149
case "number-regions-desc":
1150
query = query.order_by(db.func.count(PictureResource.regions).desc())
1151
case "number-regions-asc":
1152
query = query.order_by(db.func.count(PictureResource.regions).asc())
1153
1154
query = query.offset(offset).limit(limit)
1155
resources = query.all()
1156
1157
json_response = {
1158
"date_generated": datetime.utcnow().timestamp(),
1159
"resources": [],
1160
"offset": offset,
1161
"limit": limit,
1162
}
1163
1164
json_resources = json_response["resources"]
1165
1166
for resource in resources:
1167
json_resource = {
1168
"id": resource.id,
1169
"title": resource.title,
1170
"description": resource.description,
1171
"timestamp": resource.timestamp.timestamp(),
1172
"origin_url": resource.origin_url,
1173
"author": resource.author_name,
1174
"file_format": resource.file_format,
1175
"width": resource.width,
1176
"height": resource.height,
1177
"nature": resource.nature_id,
1178
"licences": [licence.licence_id for licence in resource.licences],
1179
"replaces": resource.replaces_id,
1180
"replaced_by": resource.replaced_by_id,
1181
"regions": [],
1182
"download": config.ROOT_URL + flask.url_for("raw_picture", id=resource.id),
1183
}
1184
for region in resource.regions:
1185
json_resource["regions"].append({
1186
"object": region.object_id,
1187
"type": region.json["type"],
1188
"shape": region.json["shape"],
1189
})
1190
1191
json_resources.append(json_resource)
1192
1193
return flask.jsonify(json_response)
1194
1195
1196
@app.route("/api/picture/<int:id>/")
1197
def api_picture(id):
1198
resource = db.session.get(PictureResource, id)
1199
if resource is None:
1200
flask.abort(404)
1201
1202
json_resource = {
1203
"id": resource.id,
1204
"title": resource.title,
1205
"description": resource.description,
1206
"timestamp": resource.timestamp.timestamp(),
1207
"origin_url": resource.origin_url,
1208
"author": resource.author_name,
1209
"file_format": resource.file_format,
1210
"width": resource.width,
1211
"height": resource.height,
1212
"nature": resource.nature_id,
1213
"licences": [licence.licence_id for licence in resource.licences],
1214
"replaces": resource.replaces_id,
1215
"replaced_by": resource.replaced_by_id,
1216
"regions": [],
1217
"download": config.ROOT_URL + flask.url_for("raw_picture", id=resource.id),
1218
}
1219
for region in resource.regions:
1220
json_resource["regions"].append({
1221
"object": region.object_id,
1222
"type": region.json["type"],
1223
"shape": region.json["shape"],
1224
})
1225
1226
return flask.jsonify(json_resource)
1227
1228
1229
@app.route("/api/licence/")
1230
def api_licences():
1231
licences = db.session.query(Licence).all()
1232
json_licences = {
1233
licence.id: {
1234
"title": licence.title,
1235
"free": licence.free,
1236
"pinned": licence.pinned,
1237
} for licence in licences
1238
}
1239
1240
return flask.jsonify(json_licences)
1241
1242
1243
@app.route("/api/licence/<id>/")
1244
def api_licence(id):
1245
licence = db.session.get(Licence, id)
1246
if licence is None:
1247
flask.abort(404)
1248
1249
json_licence = {
1250
"id": licence.id,
1251
"title": licence.title,
1252
"description": licence.description,
1253
"info_url": licence.info_url,
1254
"legalese_url": licence.url,
1255
"free": licence.free,
1256
"logo_url": licence.logo_url,
1257
"pinned": licence.pinned,
1258
}
1259
1260
return flask.jsonify(json_licence)
1261
1262
1263
@app.route("/api/nature/")
1264
def api_natures():
1265
natures = db.session.query(PictureNature).all()
1266
json_natures = {
1267
nature.id: nature.description for nature in natures
1268
}
1269
1270
return flask.jsonify(json_natures)
1271
1272
1273
@app.route("/api/user/")
1274
def api_users():
1275
offset = int(flask.request.args.get("offset", 0))
1276
limit = int(flask.request.args.get("limit", 16))
1277
1278
users = db.session.query(User).offset(offset).limit(limit).all()
1279
1280
json_users = {
1281
user.username: {
1282
"admin": user.admin,
1283
} for user in users
1284
}
1285
1286
return flask.jsonify(json_users)
1287
1288
1289
@app.route("/api/user/<username>/")
1290
def api_user(username):
1291
user = db.session.get(User, username)
1292
if user is None:
1293
flask.abort(404)
1294
1295
json_user = {
1296
"username": user.username,
1297
"admin": user.admin,
1298
"joined": user.joined_timestamp.timestamp(),
1299
}
1300
1301
return flask.jsonify(json_user)
1302
1303
1304
@app.route("/api/login", methods=["POST"])
1305
def api_login():
1306
username = flask.request.json["username"]
1307
password = flask.request.json["password"]
1308
1309
user = db.session.get(User, username)
1310
1311
if user is None:
1312
return flask.jsonify({"error": "This username is not registered. To prevent spam, you must use the HTML interface to register."}), 401
1313
1314
if not bcrypt.check_password_hash(user.password_hashed, password):
1315
return flask.jsonify({"error": "Incorrect password"}), 401
1316
1317
flask.session["username"] = username
1318
1319
return flask.jsonify({"message": "You have been logged in. Your HTTP client must support cookies to use features of this API that require authentication."})
1320
1321
1322
@app.route("/api/logout", methods=["POST"])
1323
def api_logout():
1324
flask.session.pop("username", None)
1325
return flask.jsonify({"message": "You have been logged out."})
1326
1327
1328
@app.route("/api/upload", methods=["POST"])
1329
def api_upload():
1330
if "username" not in flask.session:
1331
return flask.jsonify({"error": "You must be logged in to upload pictures"}), 401
1332
1333
json_ = json.loads(flask.request.form["json"])
1334
title = json_["title"]
1335
description = json_.get("description", "")
1336
origin_url = json_.get("origin_url", "")
1337
author = db.session.get(User, flask.session["username"])
1338
licence_ids = json_["licence"]
1339
nature_id = json_["nature"]
1340
file = flask.request.files["file"]
1341
1342
if not file or not file.filename:
1343
return flask.jsonify({"error": "An image file must be uploaded"}), 400
1344
1345
if not file.mimetype.startswith("image/") or file.mimetype == "image/svg+xml":
1346
return flask.jsonify({"error": "Only bitmap images are supported"}), 400
1347
1348
if not title:
1349
return flask.jsonify({"error": "Give a title"}), 400
1350
1351
if not description:
1352
description = ""
1353
1354
if not nature_id:
1355
return flask.jsonify({"error": "Give a picture type"}), 400
1356
1357
if not licence_ids:
1358
return flask.jsonify({"error": "Give licences"}), 400
1359
1360
licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]
1361
if not any(licence.free for licence in licences):
1362
return flask.jsonify({"error": "Use at least one free licence"}), 400
1363
1364
resource = PictureResource(title, author, description, origin_url, licence_ids,
1365
file.mimetype,
1366
db.session.get(PictureNature, nature_id))
1367
db.session.add(resource)
1368
db.session.commit()
1369
file.save(path.join(config.DATA_PATH, "pictures", str(resource.id)))
1370
pil_image = Image.open(path.join(config.DATA_PATH, "pictures", str(resource.id)))
1371
resource.width, resource.height = pil_image.size
1372
db.session.commit()
1373
1374
if json_.get("annotations"):
1375
try:
1376
resource.put_annotations(json_["annotations"])
1377
db.session.commit()
1378
except json.JSONDecodeError:
1379
return flask.jsonify({"error": "Invalid annotations"}), 400
1380
1381
return flask.jsonify({"message": "Picture uploaded successfully"})
1382
1383
1384
@app.route("/api/picture/<int:id>/update", methods=["POST"])
1385
def api_update_picture(id):
1386
resource = db.session.get(PictureResource, id)
1387
if resource is None:
1388
return flask.jsonify({"error": "Picture not found"}), 404
1389
current_user = db.session.get(User, flask.session.get("username"))
1390
if current_user is None:
1391
return flask.jsonify({"error": "You must be logged in to edit pictures"}), 401
1392
if resource.author != current_user and not current_user.admin:
1393
return flask.jsonify({"error": "You are not the author of this picture"}), 403
1394
1395
title = flask.request.json.get("title", resource.title)
1396
description = flask.request.json.get("description", resource.description)
1397
origin_url = flask.request.json.get("origin_url", resource.origin_url)
1398
licence_ids = flask.request.json.get("licence", [licence.licence_id for licence in resource.licences])
1399
nature_id = flask.request.json.get("nature", resource.nature_id)
1400
1401
if not title:
1402
return flask.jsonify({"error": "Give a title"}), 400
1403
1404
if not description:
1405
description = ""
1406
1407
if not nature_id:
1408
return flask.jsonify({"error": "Give a picture type"}), 400
1409
1410
if not licence_ids:
1411
return flask.jsonify({"error": "Give licences"}), 400
1412
1413
licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]
1414
1415
if not any(licence.free for licence in licences):
1416
return flask.jsonify({"error": "Use at least one free licence"}), 400
1417
1418
resource.title = title
1419
resource.description = description
1420
resource.origin_url = origin_url
1421
resource.licences = licences
1422
resource.nature = db.session.get(PictureNature, nature_id)
1423
1424
db.session.commit()
1425
1426
return flask.jsonify({"message": "Picture updated successfully"})
1427
1428