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 • 44.32 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/"):
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
def save_annotations(id):
572
resource = db.session.get(PictureResource, id)
573
if resource is None:
574
flask.abort(404)
575
576
current_user = db.session.get(User, flask.session.get("username"))
577
if resource.author != current_user and not current_user.admin:
578
flask.abort(403)
579
580
resource.put_annotations(flask.request.json)
581
582
db.session.commit()
583
584
response = flask.make_response()
585
response.status_code = 204
586
return response
587
588
589
@app.route("/picture/<int:id>/get-annotations")
590
def get_annotations(id):
591
resource = db.session.get(PictureResource, id)
592
if resource is None:
593
flask.abort(404)
594
595
regions = db.session.query(PictureRegion).filter_by(resource_id=id).all()
596
597
regions_json = []
598
599
for region in regions:
600
regions_json.append({
601
"object": region.object_id,
602
"type": region.json["type"],
603
"shape": region.json["shape"],
604
})
605
606
return flask.jsonify(regions_json)
607
608
609
@app.route("/picture/<int:id>/delete")
610
def delete_picture(id):
611
resource = db.session.get(PictureResource, id)
612
if resource is None:
613
flask.abort(404)
614
615
current_user = db.session.get(User, flask.session.get("username"))
616
if current_user is None:
617
flask.abort(401)
618
619
if resource.author != current_user and not current_user.admin:
620
flask.abort(403)
621
622
PictureLicence.query.filter_by(resource=resource).delete()
623
PictureRegion.query.filter_by(resource=resource).delete()
624
PictureInGallery.query.filter_by(resource=resource).delete()
625
if resource.replaces:
626
resource.replaces.replaced_by = None
627
if resource.replaced_by:
628
resource.replaced_by.replaces = None
629
resource.copied_from = None
630
for copy in resource.copies:
631
copy.copied_from = None
632
db.session.delete(resource)
633
db.session.commit()
634
635
return flask.redirect("/")
636
637
638
@app.route("/picture/<int:id>/mark-replacement", methods=["POST"])
639
def mark_picture_replacement(id):
640
resource = db.session.get(PictureResource, id)
641
if resource is None:
642
flask.abort(404)
643
644
current_user = db.session.get(User, flask.session.get("username"))
645
if current_user is None:
646
flask.abort(401)
647
648
if resource.copied_from.author != current_user and not current_user.admin:
649
flask.abort(403)
650
651
resource.copied_from.replaced_by = resource
652
resource.replaces = resource.copied_from
653
654
db.session.commit()
655
656
return flask.redirect("/picture/" + str(resource.copied_from.id))
657
658
659
@app.route("/picture/<int:id>/remove-replacement", methods=["POST"])
660
def remove_picture_replacement(id):
661
resource = db.session.get(PictureResource, id)
662
if resource is None:
663
flask.abort(404)
664
665
current_user = db.session.get(User, flask.session.get("username"))
666
if current_user is None:
667
flask.abort(401)
668
669
if resource.author != current_user and not current_user.admin:
670
flask.abort(403)
671
672
resource.replaced_by.replaces = None
673
resource.replaced_by = None
674
675
db.session.commit()
676
677
return flask.redirect("/picture/" + str(resource.id))
678
679
680
@app.route("/picture/<int:id>/edit-metadata")
681
def edit_picture(id):
682
resource = db.session.get(PictureResource, id)
683
if resource is None:
684
flask.abort(404)
685
686
current_user = db.session.get(User, flask.session.get("username"))
687
if current_user is None:
688
flask.abort(401)
689
690
if resource.author != current_user and not current_user.admin:
691
flask.abort(403)
692
693
licences = Licence.query.order_by(Licence.free.desc(), Licence.pinned.desc(),
694
Licence.title).all()
695
696
types = PictureNature.query.all()
697
698
return flask.render_template("edit-picture.html", resource=resource, licences=licences,
699
types=types,
700
PictureLicence=PictureLicence)
701
702
703
@app.route("/picture/<int:id>/edit-metadata", methods=["POST"])
704
def edit_picture_post(id):
705
resource = db.session.get(PictureResource, id)
706
if resource is None:
707
flask.abort(404)
708
709
current_user = db.session.get(User, flask.session.get("username"))
710
if current_user is None:
711
flask.abort(401)
712
713
if resource.author != current_user and not current_user.admin:
714
flask.abort(403)
715
716
title = flask.request.form["title"]
717
description = flask.request.form["description"]
718
origin_url = flask.request.form["origin_url"]
719
licence_ids = flask.request.form.getlist("licence")
720
nature_id = flask.request.form["nature"]
721
722
if not title:
723
flask.flash("Enter a title")
724
return flask.redirect(flask.request.url)
725
726
if not description:
727
description = ""
728
729
if not nature_id:
730
flask.flash("Select a picture type")
731
return flask.redirect(flask.request.url)
732
733
if not licence_ids:
734
flask.flash("Select licences")
735
return flask.redirect(flask.request.url)
736
737
licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]
738
if not any(licence.free for licence in licences):
739
flask.flash("Select at least one free licence")
740
return flask.redirect(flask.request.url)
741
742
resource.title = title
743
resource.description = description
744
resource.origin_url = origin_url
745
for licence_id in licence_ids:
746
joiner = PictureLicence(resource, db.session.get(Licence, licence_id))
747
db.session.add(joiner)
748
resource.nature = db.session.get(PictureNature, nature_id)
749
750
db.session.commit()
751
752
return flask.redirect("/picture/" + str(resource.id))
753
754
755
@app.route("/picture/<int:id>/copy")
756
def copy_picture(id):
757
resource = db.session.get(PictureResource, id)
758
if resource is None:
759
flask.abort(404)
760
761
current_user = db.session.get(User, flask.session.get("username"))
762
if current_user is None:
763
flask.abort(401)
764
765
new_resource = PictureResource(resource.title, current_user, resource.description,
766
resource.origin_url,
767
[licence.licence_id for licence in resource.licences],
768
resource.file_format,
769
resource.nature)
770
771
for region in resource.regions:
772
db.session.add(PictureRegion(region.json, new_resource, region.object))
773
774
db.session.commit()
775
776
# Create a hard link for the new picture
777
old_path = path.join(config.DATA_PATH, "pictures", str(resource.id))
778
new_path = path.join(config.DATA_PATH, "pictures", str(new_resource.id))
779
os.link(old_path, new_path)
780
781
new_resource.width = resource.width
782
new_resource.height = resource.height
783
new_resource.copied_from = resource
784
785
db.session.commit()
786
787
return flask.redirect("/picture/" + str(new_resource.id))
788
789
790
@app.route("/gallery/<int:id>/")
791
def gallery(id):
792
gallery = db.session.get(Gallery, id)
793
if gallery is None:
794
flask.abort(404)
795
796
current_user = db.session.get(User, flask.session.get("username"))
797
798
have_permission = current_user and (current_user == gallery.owner or current_user.admin or UserInGallery.query.filter_by(user=current_user, gallery=gallery).first())
799
800
return flask.render_template("gallery.html", gallery=gallery,
801
have_permission=have_permission)
802
803
804
@app.route("/create-gallery")
805
def create_gallery():
806
if "username" not in flask.session:
807
flask.flash("Log in to create galleries.")
808
return flask.redirect("/accounts")
809
810
return flask.render_template("create-gallery.html")
811
812
813
@app.route("/create-gallery", methods=["POST"])
814
def create_gallery_post():
815
if not flask.session.get("username"):
816
flask.abort(401)
817
818
if not flask.request.form.get("title"):
819
flask.flash("Enter a title")
820
return flask.redirect(flask.request.url)
821
822
description = flask.request.form.get("description", "")
823
824
gallery = Gallery(flask.request.form["title"], description,
825
db.session.get(User, flask.session["username"]))
826
db.session.add(gallery)
827
db.session.commit()
828
829
return flask.redirect("/gallery/" + str(gallery.id))
830
831
832
@app.route("/gallery/<int:id>/add-picture", methods=["POST"])
833
def gallery_add_picture(id):
834
gallery = db.session.get(Gallery, id)
835
if gallery is None:
836
flask.abort(404)
837
838
if "username" not in flask.session:
839
flask.abort(401)
840
841
if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
842
flask.abort(403)
843
844
picture_id = flask.request.form.get("picture_id")
845
if "/" in picture_id: # also allow full URLs
846
picture_id = picture_id.rstrip("/").rpartition("/")[1]
847
if not picture_id:
848
flask.flash("Select a picture")
849
return flask.redirect("/gallery/" + str(gallery.id))
850
picture_id = int(picture_id)
851
852
picture = db.session.get(PictureResource, picture_id)
853
if picture is None:
854
flask.flash("Invalid picture")
855
return flask.redirect("/gallery/" + str(gallery.id))
856
857
if PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first():
858
flask.flash("This picture is already in the gallery")
859
return flask.redirect("/gallery/" + str(gallery.id))
860
861
db.session.add(PictureInGallery(picture, gallery))
862
863
db.session.commit()
864
865
return flask.redirect("/gallery/" + str(gallery.id))
866
867
868
@app.route("/gallery/<int:id>/remove-picture", methods=["POST"])
869
def gallery_remove_picture(id):
870
gallery = db.session.get(Gallery, id)
871
if gallery is None:
872
flask.abort(404)
873
874
if "username" not in flask.session:
875
flask.abort(401)
876
877
current_user = db.session.get(User, flask.session.get("username"))
878
879
if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
880
flask.abort(403)
881
882
picture_id = int(flask.request.form.get("picture_id"))
883
884
picture = db.session.get(PictureResource, picture_id)
885
if picture is None:
886
flask.flash("Invalid picture")
887
return flask.redirect("/gallery/" + str(gallery.id))
888
889
picture_in_gallery = PictureInGallery.query.filter_by(resource=picture,
890
gallery=gallery).first()
891
if picture_in_gallery is None:
892
flask.flash("This picture isn't in the gallery")
893
return flask.redirect("/gallery/" + str(gallery.id))
894
895
db.session.delete(picture_in_gallery)
896
897
db.session.commit()
898
899
return flask.redirect("/gallery/" + str(gallery.id))
900
901
902
@app.route("/gallery/<int:id>/add-pictures-from-query", methods=["POST"])
903
def gallery_add_from_query(id):
904
gallery = db.session.get(Gallery, id)
905
if gallery is None:
906
flask.abort(404)
907
908
if "username" not in flask.session:
909
flask.abort(401)
910
911
if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
912
flask.abort(403)
913
914
query_yaml = flask.request.form.get("query", "")
915
916
yaml_parser = yaml.YAML()
917
query_data = yaml_parser.load(query_yaml) or {}
918
query = get_picture_query(query_data)
919
920
pictures = query.all()
921
922
count = 0
923
924
for picture in pictures:
925
if not PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first():
926
db.session.add(PictureInGallery(picture, gallery))
927
count += 1
928
929
db.session.commit()
930
931
flask.flash(f"Added {count} pictures to the gallery")
932
933
return flask.redirect("/gallery/" + str(gallery.id))
934
935
936
@app.route("/gallery/<int:id>/users")
937
def gallery_users(id):
938
gallery = db.session.get(Gallery, id)
939
if gallery is None:
940
flask.abort(404)
941
942
current_user = db.session.get(User, flask.session.get("username"))
943
have_permission = current_user and (current_user == gallery.owner or current_user.admin)
944
945
return flask.render_template("gallery-users.html", gallery=gallery,
946
have_permission=have_permission)
947
948
949
@app.route("/gallery/<int:id>/users/add", methods=["POST"])
950
def gallery_add_user(id):
951
gallery = db.session.get(Gallery, id)
952
if gallery is None:
953
flask.abort(404)
954
955
current_user = db.session.get(User, flask.session.get("username"))
956
if current_user is None:
957
flask.abort(401)
958
959
if current_user != gallery.owner and not current_user.admin:
960
flask.abort(403)
961
962
username = flask.request.form.get("username")
963
if username == gallery.owner_name:
964
flask.flash("The owner is already in the gallery")
965
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
966
967
user = db.session.get(User, username)
968
if user is None:
969
flask.flash("User not found")
970
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
971
972
if UserInGallery.query.filter_by(user=user, gallery=gallery).first():
973
flask.flash("User is already in the gallery")
974
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
975
976
db.session.add(UserInGallery(user, gallery))
977
978
db.session.commit()
979
980
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
981
982
983
@app.route("/gallery/<int:id>/users/remove", methods=["POST"])
984
def gallery_remove_user(id):
985
gallery = db.session.get(Gallery, id)
986
if gallery is None:
987
flask.abort(404)
988
989
current_user = db.session.get(User, flask.session.get("username"))
990
if current_user is None:
991
flask.abort(401)
992
993
if current_user != gallery.owner and not current_user.admin:
994
flask.abort(403)
995
996
username = flask.request.form.get("username")
997
user = db.session.get(User, username)
998
if user is None:
999
flask.flash("User not found")
1000
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1001
1002
user_in_gallery = UserInGallery.query.filter_by(user=user, gallery=gallery).first()
1003
if user_in_gallery is None:
1004
flask.flash("User is not in the gallery")
1005
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1006
1007
db.session.delete(user_in_gallery)
1008
1009
db.session.commit()
1010
1011
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1012
1013
1014
class APIError(Exception):
1015
def __init__(self, status_code, message):
1016
self.status_code = status_code
1017
self.message = message
1018
1019
1020
def get_picture_query(query_data):
1021
query = db.session.query(PictureResource)
1022
1023
requirement_conditions = {
1024
"has_object": lambda value: PictureResource.regions.any(
1025
PictureRegion.object_id.in_(value)),
1026
"nature": lambda value: PictureResource.nature_id.in_(value),
1027
"licence": lambda value: PictureResource.licences.any(
1028
PictureLicence.licence_id.in_(value)),
1029
"author": lambda value: PictureResource.author_name.in_(value),
1030
"title": lambda value: PictureResource.title.ilike(value),
1031
"description": lambda value: PictureResource.description.ilike(value),
1032
"origin_url": lambda value: db.func.lower(db.func.substr(
1033
PictureResource.origin_url,
1034
db.func.length(db.func.split_part(PictureResource.origin_url, "://", 1)) + 4
1035
)).in_(value),
1036
"above_width": lambda value: PictureResource.width >= value,
1037
"below_width": lambda value: PictureResource.width <= value,
1038
"above_height": lambda value: PictureResource.height >= value,
1039
"below_height": lambda value: PictureResource.height <= value,
1040
"before_date": lambda value: PictureResource.timestamp <= datetime.utcfromtimestamp(
1041
value),
1042
"after_date": lambda value: PictureResource.timestamp >= datetime.utcfromtimestamp(
1043
value),
1044
"in_gallery": lambda value: PictureResource.galleries.any(PictureInGallery.gallery_id.in_(value)),
1045
}
1046
if "want" in query_data:
1047
for i in query_data["want"]:
1048
if len(i) != 1:
1049
raise APIError(400, "Each requirement must have exactly one key")
1050
requirement, value = list(i.items())[0]
1051
if requirement not in requirement_conditions:
1052
raise APIError(400, f"Unknown requirement type: {requirement}")
1053
1054
condition = requirement_conditions[requirement]
1055
query = query.filter(condition(value))
1056
if "exclude" in query_data:
1057
for i in query_data["exclude"]:
1058
if len(i) != 1:
1059
raise APIError(400, "Each exclusion must have exactly one key")
1060
requirement, value = list(i.items())[0]
1061
if requirement not in requirement_conditions:
1062
raise APIError(400, f"Unknown requirement type: {requirement}")
1063
1064
condition = requirement_conditions[requirement]
1065
query = query.filter(~condition(value))
1066
if not query_data.get("include_obsolete", False):
1067
query = query.filter(PictureResource.replaced_by_id.is_(None))
1068
1069
return query
1070
1071
1072
@app.route("/query-pictures")
1073
def graphical_query_pictures():
1074
return flask.render_template("graphical-query-pictures.html")
1075
1076
1077
@app.route("/query-pictures-results")
1078
def graphical_query_pictures_results():
1079
query_yaml = flask.request.args.get("query", "")
1080
yaml_parser = yaml.YAML()
1081
query_data = yaml_parser.load(query_yaml) or {}
1082
try:
1083
query = get_picture_query(query_data)
1084
except APIError as e:
1085
flask.abort(e.status_code)
1086
1087
page = int(flask.request.args.get("page", 1))
1088
per_page = int(flask.request.args.get("per_page", 16))
1089
1090
resources = query.paginate(page=page, per_page=per_page)
1091
1092
return flask.render_template("graphical-query-pictures-results.html", resources=resources,
1093
query=query_yaml,
1094
page_number=page, page_length=per_page,
1095
num_pages=resources.pages,
1096
prev_page=resources.prev_num, next_page=resources.next_num)
1097
1098
1099
@app.route("/raw/picture/<int:id>")
1100
def raw_picture(id):
1101
resource = db.session.get(PictureResource, id)
1102
if resource is None:
1103
flask.abort(404)
1104
1105
response = flask.send_from_directory(path.join(config.DATA_PATH, "pictures"),
1106
str(resource.id))
1107
response.mimetype = resource.file_format
1108
1109
return response
1110
1111
1112
@app.route("/object/")
1113
def graphical_object_types():
1114
return flask.render_template("object-types.html", objects=PictureObject.query.all())
1115
1116
1117
@app.route("/api/object-types")
1118
def object_types():
1119
objects = db.session.query(PictureObject).all()
1120
return flask.jsonify({object.id: object.description for object in objects})
1121
1122
1123
@app.route("/api/query-pictures", methods=["POST"]) # sadly GET can't have a body
1124
def query_pictures():
1125
offset = int(flask.request.args.get("offset", 0))
1126
limit = int(flask.request.args.get("limit", 16))
1127
ordering = flask.request.args.get("ordering", "date-desc")
1128
1129
yaml_parser = yaml.YAML()
1130
query_data = yaml_parser.load(flask.request.data) or {}
1131
try:
1132
query = get_picture_query(query_data)
1133
except APIError as e:
1134
return flask.jsonify({"error": e.message}), e.status_code
1135
1136
match ordering:
1137
case "date-desc":
1138
query = query.order_by(PictureResource.timestamp.desc())
1139
case "date-asc":
1140
query = query.order_by(PictureResource.timestamp.asc())
1141
case "title-asc":
1142
query = query.order_by(PictureResource.title.asc())
1143
case "title-desc":
1144
query = query.order_by(PictureResource.title.desc())
1145
case "random":
1146
query = query.order_by(db.func.random())
1147
case "number-regions-desc":
1148
query = query.order_by(db.func.count(PictureResource.regions).desc())
1149
case "number-regions-asc":
1150
query = query.order_by(db.func.count(PictureResource.regions).asc())
1151
1152
query = query.offset(offset).limit(limit)
1153
resources = query.all()
1154
1155
json_response = {
1156
"date_generated": datetime.utcnow().timestamp(),
1157
"resources": [],
1158
"offset": offset,
1159
"limit": limit,
1160
}
1161
1162
json_resources = json_response["resources"]
1163
1164
for resource in resources:
1165
json_resource = {
1166
"id": resource.id,
1167
"title": resource.title,
1168
"description": resource.description,
1169
"timestamp": resource.timestamp.timestamp(),
1170
"origin_url": resource.origin_url,
1171
"author": resource.author_name,
1172
"file_format": resource.file_format,
1173
"width": resource.width,
1174
"height": resource.height,
1175
"nature": resource.nature_id,
1176
"licences": [licence.licence_id for licence in resource.licences],
1177
"replaces": resource.replaces_id,
1178
"replaced_by": resource.replaced_by_id,
1179
"regions": [],
1180
"download": config.ROOT_URL + flask.url_for("raw_picture", id=resource.id),
1181
}
1182
for region in resource.regions:
1183
json_resource["regions"].append({
1184
"object": region.object_id,
1185
"type": region.json["type"],
1186
"shape": region.json["shape"],
1187
})
1188
1189
json_resources.append(json_resource)
1190
1191
return flask.jsonify(json_response)
1192
1193
1194
@app.route("/api/picture/<int:id>/")
1195
def api_picture(id):
1196
resource = db.session.get(PictureResource, id)
1197
if resource is None:
1198
flask.abort(404)
1199
1200
json_resource = {
1201
"id": resource.id,
1202
"title": resource.title,
1203
"description": resource.description,
1204
"timestamp": resource.timestamp.timestamp(),
1205
"origin_url": resource.origin_url,
1206
"author": resource.author_name,
1207
"file_format": resource.file_format,
1208
"width": resource.width,
1209
"height": resource.height,
1210
"nature": resource.nature_id,
1211
"licences": [licence.licence_id for licence in resource.licences],
1212
"replaces": resource.replaces_id,
1213
"replaced_by": resource.replaced_by_id,
1214
"regions": [],
1215
"download": config.ROOT_URL + flask.url_for("raw_picture", id=resource.id),
1216
}
1217
for region in resource.regions:
1218
json_resource["regions"].append({
1219
"object": region.object_id,
1220
"type": region.json["type"],
1221
"shape": region.json["shape"],
1222
})
1223
1224
return flask.jsonify(json_resource)
1225
1226
1227
@app.route("/api/licence/")
1228
def api_licences():
1229
licences = db.session.query(Licence).all()
1230
json_licences = {
1231
licence.id: {
1232
"title": licence.title,
1233
"free": licence.free,
1234
"pinned": licence.pinned,
1235
} for licence in licences
1236
}
1237
1238
return flask.jsonify(json_licences)
1239
1240
1241
@app.route("/api/licence/<id>/")
1242
def api_licence(id):
1243
licence = db.session.get(Licence, id)
1244
if licence is None:
1245
flask.abort(404)
1246
1247
json_licence = {
1248
"id": licence.id,
1249
"title": licence.title,
1250
"description": licence.description,
1251
"info_url": licence.info_url,
1252
"legalese_url": licence.url,
1253
"free": licence.free,
1254
"logo_url": licence.logo_url,
1255
"pinned": licence.pinned,
1256
}
1257
1258
return flask.jsonify(json_licence)
1259
1260
1261
@app.route("/api/nature/")
1262
def api_natures():
1263
natures = db.session.query(PictureNature).all()
1264
json_natures = {
1265
nature.id: nature.description for nature in natures
1266
}
1267
1268
return flask.jsonify(json_natures)
1269
1270
1271
@app.route("/api/user/")
1272
def api_users():
1273
offset = int(flask.request.args.get("offset", 0))
1274
limit = int(flask.request.args.get("limit", 16))
1275
1276
users = db.session.query(User).offset(offset).limit(limit).all()
1277
1278
json_users = {
1279
user.username: {
1280
"admin": user.admin,
1281
} for user in users
1282
}
1283
1284
return flask.jsonify(json_users)
1285
1286
1287
@app.route("/api/user/<username>/")
1288
def api_user(username):
1289
user = db.session.get(User, username)
1290
if user is None:
1291
flask.abort(404)
1292
1293
json_user = {
1294
"username": user.username,
1295
"admin": user.admin,
1296
"joined": user.joined_timestamp.timestamp(),
1297
}
1298
1299
return flask.jsonify(json_user)
1300