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