app.py
Python script, ASCII text executable
1
import json
2
import os
3
import mimetypes
4
import flask
5
import ruamel.yaml as yaml
6
import sqlalchemy.dialects.postgresql
7
import config
8
import markdown
9
10
from datetime import datetime
11
from os import path
12
from flask_sqlalchemy import SQLAlchemy
13
from flask_bcrypt import Bcrypt
14
from flask_migrate import Migrate, current
15
from urllib.parse import urlencode
16
from PIL import Image
17
from sqlalchemy.sql.functions import current_user
18
19
app = flask.Flask(__name__)
20
bcrypt = Bcrypt(app)
21
22
app.config["SQLALCHEMY_DATABASE_URI"] = config.DB_URI
23
app.config["SECRET_KEY"] = config.DB_PASSWORD
24
25
db = SQLAlchemy(app)
26
migrate = Migrate(app, db)
27
28
29
@app.template_filter("split")
30
def split(value, separator=None, maxsplit=-1):
31
return value.split(separator, maxsplit)
32
33
34
@app.template_filter("median")
35
def median(value):
36
value = list(value) # prevent generators
37
return sorted(value)[len(value) // 2]
38
39
40
@app.template_filter("set")
41
def set_filter(value):
42
return set(value)
43
44
45
@app.template_global()
46
def modify_query(**new_values):
47
args = flask.request.args.copy()
48
for key, value in new_values.items():
49
args[key] = value
50
51
return f"{flask.request.path}?{urlencode(args)}"
52
53
54
@app.context_processor
55
def default_variables():
56
return {
57
"current_user": db.session.get(User, flask.session.get("username")),
58
}
59
60
61
with app.app_context():
62
class User(db.Model):
63
username = db.Column(db.String(32), unique=True, nullable=False, primary_key=True)
64
password_hashed = db.Column(db.String(60), nullable=False)
65
admin = db.Column(db.Boolean, nullable=False, default=False, server_default="false")
66
pictures = db.relationship("PictureResource", back_populates="author")
67
joined_timestamp = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
68
galleries = db.relationship("Gallery", back_populates="owner")
69
galleries_joined = db.relationship("UserInGallery", back_populates="user")
70
ratings = db.relationship("PictureRating", back_populates="user")
71
72
def __init__(self, username, password):
73
self.username = username
74
self.password_hashed = bcrypt.generate_password_hash(password).decode("utf-8")
75
76
@property
77
def formatted_name(self):
78
if self.admin:
79
return self.username + "*"
80
return self.username
81
82
83
class Licence(db.Model):
84
id = db.Column(db.String(64), primary_key=True) # SPDX identifier
85
title = db.Column(db.UnicodeText, nullable=False) # the official name of the licence
86
description = db.Column(db.UnicodeText,
87
nullable=False) # brief description of its permissions and restrictions
88
info_url = db.Column(db.String(1024),
89
nullable=False) # the URL to a page with general information about the licence
90
url = db.Column(db.String(1024),
91
nullable=True) # the URL to a page with the full text of the licence and more information
92
pictures = db.relationship("PictureLicence", back_populates="licence")
93
free = db.Column(db.Boolean, nullable=False,
94
default=False) # whether the licence is free or not
95
logo_url = db.Column(db.String(1024), nullable=True) # URL to the logo of the licence
96
pinned = db.Column(db.Boolean, nullable=False,
97
default=False) # whether the licence should be shown at the top of the list
98
99
def __init__(self, id, title, description, info_url, url, free, logo_url=None,
100
pinned=False):
101
self.id = id
102
self.title = title
103
self.description = description
104
self.info_url = info_url
105
self.url = url
106
self.free = free
107
self.logo_url = logo_url
108
self.pinned = pinned
109
110
111
class PictureLicence(db.Model):
112
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
113
114
resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"))
115
licence_id = db.Column(db.String(64), db.ForeignKey("licence.id"))
116
117
resource = db.relationship("PictureResource", back_populates="licences")
118
licence = db.relationship("Licence", back_populates="pictures")
119
120
def __init__(self, resource, licence):
121
self.resource = resource
122
self.licence = licence
123
124
125
class Resource(db.Model):
126
__abstract__ = True
127
128
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
129
title = db.Column(db.UnicodeText, nullable=False)
130
description = db.Column(db.UnicodeText, nullable=False)
131
timestamp = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
132
origin_url = db.Column(db.String(2048),
133
nullable=True) # should be left empty if it's original or the source is unknown but public domain
134
135
136
class PictureNature(db.Model):
137
# Examples:
138
# "photo", "paper-scan", "2d-art-photo", "sculpture-photo", "computer-3d", "computer-painting",
139
# "computer-line-art", "diagram", "infographic", "text", "map", "chart-graph", "screen-capture",
140
# "screen-photo", "pattern", "collage", "ai", and so on
141
id = db.Column(db.String(64), primary_key=True)
142
description = db.Column(db.UnicodeText, nullable=False)
143
resources = db.relationship("PictureResource", back_populates="nature")
144
145
def __init__(self, id, description):
146
self.id = id
147
self.description = description
148
149
150
class PictureObjectInheritance(db.Model):
151
parent_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"),
152
primary_key=True)
153
child_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"),
154
primary_key=True)
155
156
parent = db.relationship("PictureObject", foreign_keys=[parent_id],
157
back_populates="child_links")
158
child = db.relationship("PictureObject", foreign_keys=[child_id],
159
back_populates="parent_links")
160
161
def __init__(self, parent, child):
162
self.parent = parent
163
self.child = child
164
165
166
class PictureObject(db.Model):
167
id = db.Column(db.String(64), primary_key=True)
168
description = db.Column(db.UnicodeText, nullable=False)
169
170
child_links = db.relationship("PictureObjectInheritance",
171
foreign_keys=[PictureObjectInheritance.parent_id],
172
back_populates="parent")
173
parent_links = db.relationship("PictureObjectInheritance",
174
foreign_keys=[PictureObjectInheritance.child_id],
175
back_populates="child")
176
177
def __init__(self, id, description, parents):
178
self.id = id
179
self.description = description
180
if parents:
181
for parent in parents:
182
db.session.add(PictureObjectInheritance(parent, self))
183
184
185
class PictureRegion(db.Model):
186
# This is for picture region annotations
187
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
188
json = db.Column(sqlalchemy.dialects.postgresql.JSONB, nullable=False)
189
190
resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"),
191
nullable=False)
192
object_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"), nullable=True)
193
194
resource = db.relationship("PictureResource", backref="regions")
195
object = db.relationship("PictureObject", backref="regions")
196
197
def __init__(self, json, resource, object):
198
self.json = json
199
self.resource = resource
200
self.object = object
201
202
203
class PictureResource(Resource):
204
# This is only for bitmap pictures. Vectors will be stored under a different model
205
# File name is the ID in the picture directory under data, without an extension
206
file_format = db.Column(db.String(64), nullable=False) # MIME type
207
width = db.Column(db.Integer, nullable=False)
208
height = db.Column(db.Integer, nullable=False)
209
nature_id = db.Column(db.String(32), db.ForeignKey("picture_nature.id"), nullable=True)
210
author_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
211
author = db.relationship("User", back_populates="pictures")
212
213
nature = db.relationship("PictureNature", back_populates="resources")
214
215
replaces_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), nullable=True)
216
replaced_by_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"),
217
nullable=True)
218
219
replaces = db.relationship("PictureResource", remote_side="PictureResource.id",
220
foreign_keys=[replaces_id], back_populates="replaced_by",
221
post_update=True)
222
replaced_by = db.relationship("PictureResource", remote_side="PictureResource.id",
223
foreign_keys=[replaced_by_id], post_update=True)
224
225
copied_from_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"),
226
nullable=True)
227
copied_from = db.relationship("PictureResource", remote_side="PictureResource.id",
228
backref="copies", foreign_keys=[copied_from_id])
229
230
licences = db.relationship("PictureLicence", back_populates="resource")
231
galleries = db.relationship("PictureInGallery", back_populates="resource")
232
ratings = db.relationship("PictureRating", back_populates="resource")
233
234
def __init__(self, title, author, description, origin_url, licence_ids, mime,
235
nature=None):
236
self.title = title
237
self.author = author
238
self.description = description
239
self.origin_url = origin_url
240
self.file_format = mime
241
self.width = self.height = 0
242
self.nature = nature
243
db.session.add(self)
244
db.session.commit()
245
for licence_id in licence_ids:
246
joiner = PictureLicence(self, db.session.get(Licence, licence_id))
247
db.session.add(joiner)
248
249
def put_annotations(self, json):
250
# Delete all previous annotations
251
db.session.query(PictureRegion).filter_by(resource_id=self.id).delete()
252
253
for region in json:
254
object_id = region["object"]
255
picture_object = db.session.get(PictureObject, object_id)
256
257
region_data = {
258
"type": region["type"],
259
"shape": region["shape"],
260
}
261
262
region_row = PictureRegion(region_data, self, picture_object)
263
db.session.add(region_row)
264
265
@property
266
def average_rating(self):
267
if not self.ratings:
268
return None
269
return db.session.query(db.func.avg(PictureRating.rating)).filter_by(resource=self).scalar()
270
271
@property
272
def rating_totals(self):
273
all_ratings = db.session.query(PictureRating.rating).filter_by(resource=self)
274
return {rating: all_ratings.filter_by(rating=rating).count() for rating in range(1, 6)}
275
276
@property
277
def stars(self):
278
if not self.ratings:
279
return 0
280
average = self.average_rating
281
whole_stars = int(average)
282
partial_star = average - whole_stars
283
284
return [100] * whole_stars + [int(partial_star * 100)] + [0] * (4 - whole_stars)
285
286
287
class PictureInGallery(db.Model):
288
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
289
resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"),
290
nullable=False)
291
gallery_id = db.Column(db.Integer, db.ForeignKey("gallery.id"), nullable=False)
292
293
resource = db.relationship("PictureResource")
294
gallery = db.relationship("Gallery")
295
296
def __init__(self, resource, gallery):
297
self.resource = resource
298
self.gallery = gallery
299
300
301
class UserInGallery(db.Model):
302
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
303
username = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
304
gallery_id = db.Column(db.Integer, db.ForeignKey("gallery.id"), nullable=False)
305
306
user = db.relationship("User")
307
gallery = db.relationship("Gallery")
308
309
def __init__(self, user, gallery):
310
self.user = user
311
self.gallery = gallery
312
313
314
class Gallery(db.Model):
315
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
316
title = db.Column(db.UnicodeText, nullable=False)
317
description = db.Column(db.UnicodeText, nullable=False)
318
pictures = db.relationship("PictureInGallery", back_populates="gallery")
319
owner_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
320
owner = db.relationship("User", back_populates="galleries")
321
users = db.relationship("UserInGallery", back_populates="gallery")
322
323
def __init__(self, title, description, owner):
324
self.title = title
325
self.description = description
326
self.owner = owner
327
328
329
class PictureRating(db.Model):
330
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
331
resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), nullable=False)
332
username = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
333
rating = db.Column(db.Integer, db.CheckConstraint("rating >= 1 AND rating <= 5"),
334
nullable=False)
335
336
resource = db.relationship("PictureResource", back_populates="ratings")
337
user = db.relationship("User", back_populates="ratings")
338
339
def __init__(self, resource, user, rating):
340
self.resource = resource
341
self.user = user
342
self.rating = rating
343
344
345
@app.route("/")
346
def index():
347
return flask.render_template("home.html", resources=PictureResource.query.filter_by(replaced_by=None).order_by(
348
db.func.random()).limit(10).all())
349
350
351
@app.route("/info/")
352
def usage_guide():
353
with open("help/usage.md") as f:
354
return flask.render_template("help.html", content=markdown.markdown2html(f.read()))
355
356
357
@app.route("/accounts/")
358
def accounts():
359
return flask.render_template("login.html")
360
361
362
@app.route("/login", methods=["POST"])
363
def login():
364
username = flask.request.form["username"]
365
password = flask.request.form["password"]
366
367
user = db.session.get(User, username)
368
369
if user is None:
370
flask.flash("This username is not registered.")
371
return flask.redirect("/accounts")
372
373
if not bcrypt.check_password_hash(user.password_hashed, password):
374
flask.flash("Incorrect password.")
375
return flask.redirect("/accounts")
376
377
flask.flash("You have been logged in.")
378
379
flask.session["username"] = username
380
return flask.redirect("/")
381
382
383
@app.route("/logout")
384
def logout():
385
flask.session.pop("username", None)
386
flask.flash("You have been logged out.")
387
return flask.redirect("/")
388
389
390
@app.route("/signup", methods=["POST"])
391
def signup():
392
username = flask.request.form["username"]
393
password = flask.request.form["password"]
394
395
if db.session.get(User, username) is not None:
396
flask.flash("This username is already taken.")
397
return flask.redirect("/accounts")
398
399
if set(username) > set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"):
400
flask.flash(
401
"Usernames can only contain the Latin alphabet, digits, hyphens, and underscores.")
402
return flask.redirect("/accounts")
403
404
if len(username) < 3 or len(username) > 32:
405
flask.flash("Usernames must be between 3 and 32 characters long.")
406
return flask.redirect("/accounts")
407
408
if len(password) < 6:
409
flask.flash("Passwords must be at least 6 characters long.")
410
return flask.redirect("/accounts")
411
412
user = User(username, password)
413
db.session.add(user)
414
db.session.commit()
415
416
flask.session["username"] = username
417
418
flask.flash("You have been registered and logged in.")
419
420
return flask.redirect("/")
421
422
423
@app.route("/profile", defaults={"username": None})
424
@app.route("/profile/<username>")
425
def profile(username):
426
if username is None:
427
if "username" in flask.session:
428
return flask.redirect("/profile/" + flask.session["username"])
429
else:
430
flask.flash("Please log in to perform this action.")
431
return flask.redirect("/accounts")
432
433
user = db.session.get(User, username)
434
if user is None:
435
flask.abort(404)
436
437
return flask.render_template("profile.html", user=user)
438
439
440
@app.route("/object/<id>")
441
def has_object(id):
442
object_ = db.session.get(PictureObject, id)
443
if object_ is None:
444
flask.abort(404)
445
446
query = db.session.query(PictureResource).join(PictureRegion).filter(
447
PictureRegion.object_id == id)
448
449
page = int(flask.request.args.get("page", 1))
450
per_page = int(flask.request.args.get("per_page", 16))
451
452
resources = query.paginate(page=page, per_page=per_page)
453
454
return flask.render_template("object.html", object=object_, resources=resources,
455
page_number=page,
456
page_length=per_page, num_pages=resources.pages,
457
prev_page=resources.prev_num,
458
next_page=resources.next_num, PictureRegion=PictureRegion)
459
460
461
@app.route("/upload")
462
def upload():
463
if "username" not in flask.session:
464
flask.flash("Log in to upload pictures.")
465
return flask.redirect("/accounts")
466
467
licences = Licence.query.order_by(Licence.free.desc(), Licence.pinned.desc(),
468
Licence.title).all()
469
470
types = PictureNature.query.all()
471
472
return flask.render_template("upload.html", licences=licences, types=types)
473
474
475
@app.route("/upload", methods=["POST"])
476
def upload_post():
477
title = flask.request.form["title"]
478
description = flask.request.form["description"]
479
origin_url = flask.request.form["origin_url"]
480
author = db.session.get(User, flask.session.get("username"))
481
licence_ids = flask.request.form.getlist("licence")
482
nature_id = flask.request.form["nature"]
483
484
if author is None:
485
flask.abort(401)
486
487
file = flask.request.files["file"]
488
489
if not file or not file.filename:
490
flask.flash("Select a file")
491
return flask.redirect(flask.request.url)
492
493
if not file.mimetype.startswith("image/") or file.mimetype == "image/svg+xml":
494
flask.flash("Only images are supported")
495
return flask.redirect(flask.request.url)
496
497
if not title:
498
flask.flash("Enter a title")
499
return flask.redirect(flask.request.url)
500
501
if not description:
502
description = ""
503
504
if not nature_id:
505
flask.flash("Select a picture type")
506
return flask.redirect(flask.request.url)
507
508
if not licence_ids:
509
flask.flash("Select licences")
510
return flask.redirect(flask.request.url)
511
512
licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]
513
if not any(licence.free for licence in licences):
514
flask.flash("Select at least one free licence")
515
return flask.redirect(flask.request.url)
516
517
resource = PictureResource(title, author, description, origin_url, licence_ids,
518
file.mimetype,
519
db.session.get(PictureNature, nature_id))
520
db.session.add(resource)
521
db.session.commit()
522
file.save(path.join(config.DATA_PATH, "pictures", str(resource.id)))
523
pil_image = Image.open(path.join(config.DATA_PATH, "pictures", str(resource.id)))
524
resource.width, resource.height = pil_image.size
525
db.session.commit()
526
527
if flask.request.form.get("annotations"):
528
try:
529
resource.put_annotations(json.loads(flask.request.form.get("annotations")))
530
db.session.commit()
531
except json.JSONDecodeError:
532
flask.flash("Invalid annotations")
533
534
flask.flash("Picture uploaded successfully")
535
536
return flask.redirect("/picture/" + str(resource.id))
537
538
539
@app.route("/picture/<int:id>/")
540
def picture(id):
541
resource = db.session.get(PictureResource, id)
542
if resource is None:
543
flask.abort(404)
544
545
image = Image.open(path.join(config.DATA_PATH, "pictures", str(resource.id)))
546
547
current_user = db.session.get(User, flask.session.get("username"))
548
have_permission = current_user and (current_user == resource.author or current_user.admin)
549
550
own_rating = None
551
if current_user:
552
own_rating = PictureRating.query.filter_by(resource=resource, user=current_user).first()
553
554
return flask.render_template("picture.html", resource=resource,
555
file_extension=mimetypes.guess_extension(resource.file_format),
556
size=image.size, copies=resource.copies,
557
have_permission=have_permission, own_rating=own_rating)
558
559
560
@app.route("/picture/<int:id>/annotate")
561
def annotate_picture(id):
562
resource = db.session.get(PictureResource, id)
563
if resource is None:
564
flask.abort(404)
565
566
current_user = db.session.get(User, flask.session.get("username"))
567
if current_user is None:
568
flask.abort(401)
569
570
if resource.author != current_user and not current_user.admin:
571
flask.abort(403)
572
573
return flask.render_template("picture-annotation.html", resource=resource,
574
file_extension=mimetypes.guess_extension(resource.file_format))
575
576
577
@app.route("/picture/<int:id>/put-annotations-form")
578
def put_annotations_form(id):
579
resource = db.session.get(PictureResource, id)
580
if resource is None:
581
flask.abort(404)
582
583
current_user = db.session.get(User, flask.session.get("username"))
584
if current_user is None:
585
flask.abort(401)
586
587
if resource.author != current_user and not current_user.admin:
588
flask.abort(403)
589
590
return flask.render_template("put-annotations-form.html", resource=resource)
591
592
593
@app.route("/picture/<int:id>/put-annotations-form", methods=["POST"])
594
def put_annotations_form_post(id):
595
resource = db.session.get(PictureResource, id)
596
if resource is None:
597
flask.abort(404)
598
599
current_user = db.session.get(User, flask.session.get("username"))
600
if current_user is None:
601
flask.abort(401)
602
603
if resource.author != current_user and not current_user.admin:
604
flask.abort(403)
605
606
resource.put_annotations(json.loads(flask.request.form["annotations"]))
607
608
db.session.commit()
609
610
return flask.redirect("/picture/" + str(resource.id))
611
612
613
@app.route("/picture/<int:id>/save-annotations", methods=["POST"])
614
@app.route("/api/picture/<int:id>/put-annotations", methods=["POST"])
615
def save_annotations(id):
616
resource = db.session.get(PictureResource, id)
617
if resource is None:
618
flask.abort(404)
619
620
current_user = db.session.get(User, flask.session.get("username"))
621
if resource.author != current_user and not current_user.admin:
622
flask.abort(403)
623
624
resource.put_annotations(flask.request.json)
625
626
db.session.commit()
627
628
response = flask.make_response()
629
response.status_code = 204
630
return response
631
632
633
@app.route("/picture/<int:id>/get-annotations")
634
@app.route("/api/picture/<int:id>/api/get-annotations")
635
def get_annotations(id):
636
resource = db.session.get(PictureResource, id)
637
if resource is None:
638
flask.abort(404)
639
640
regions = db.session.query(PictureRegion).filter_by(resource_id=id).all()
641
642
regions_json = []
643
644
for region in regions:
645
regions_json.append({
646
"object": region.object_id,
647
"type": region.json["type"],
648
"shape": region.json["shape"],
649
})
650
651
return flask.jsonify(regions_json)
652
653
654
@app.route("/picture/<int:id>/delete")
655
def delete_picture(id):
656
resource = db.session.get(PictureResource, id)
657
if resource is None:
658
flask.abort(404)
659
660
current_user = db.session.get(User, flask.session.get("username"))
661
if current_user is None:
662
flask.abort(401)
663
664
if resource.author != current_user and not current_user.admin:
665
flask.abort(403)
666
667
PictureLicence.query.filter_by(resource=resource).delete()
668
PictureRegion.query.filter_by(resource=resource).delete()
669
PictureInGallery.query.filter_by(resource=resource).delete()
670
PictureRating.query.filter_by(resource=resource).delete()
671
if resource.replaces:
672
resource.replaces.replaced_by = None
673
if resource.replaced_by:
674
resource.replaced_by.replaces = None
675
resource.copied_from = None
676
for copy in resource.copies:
677
copy.copied_from = None
678
db.session.delete(resource)
679
db.session.commit()
680
681
return flask.redirect("/")
682
683
684
@app.route("/picture/<int:id>/mark-replacement", methods=["POST"])
685
def mark_picture_replacement(id):
686
resource = db.session.get(PictureResource, id)
687
if resource is None:
688
flask.abort(404)
689
690
current_user = db.session.get(User, flask.session.get("username"))
691
if current_user is None:
692
flask.abort(401)
693
694
if resource.copied_from.author != current_user and not current_user.admin:
695
flask.abort(403)
696
697
resource.copied_from.replaced_by = resource
698
resource.replaces = resource.copied_from
699
700
db.session.commit()
701
702
return flask.redirect("/picture/" + str(resource.copied_from.id))
703
704
705
@app.route("/picture/<int:id>/remove-replacement", methods=["POST"])
706
def remove_picture_replacement(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
resource.replaced_by.replaces = None
719
resource.replaced_by = None
720
721
db.session.commit()
722
723
return flask.redirect("/picture/" + str(resource.id))
724
725
726
@app.route("/picture/<int:id>/edit-metadata")
727
def edit_picture(id):
728
resource = db.session.get(PictureResource, id)
729
if resource is None:
730
flask.abort(404)
731
732
current_user = db.session.get(User, flask.session.get("username"))
733
if current_user is None:
734
flask.abort(401)
735
736
if resource.author != current_user and not current_user.admin:
737
flask.abort(403)
738
739
licences = Licence.query.order_by(Licence.free.desc(), Licence.pinned.desc(),
740
Licence.title).all()
741
742
types = PictureNature.query.all()
743
744
return flask.render_template("edit-picture.html", resource=resource, licences=licences,
745
types=types,
746
PictureLicence=PictureLicence)
747
748
749
@app.route("/picture/<int:id>/rate", methods=["POST"])
750
def rate_picture(id):
751
resource = db.session.get(PictureResource, id)
752
if resource is None:
753
flask.abort(404)
754
755
current_user = db.session.get(User, flask.session.get("username"))
756
if current_user is None:
757
flask.abort(401)
758
759
rating = int(flask.request.form.get("rating"))
760
761
if not rating:
762
# Delete the existing rating
763
if PictureRating.query.filter_by(resource=resource, user=current_user).first():
764
db.session.delete(PictureRating.query.filter_by(resource=resource,
765
user=current_user).first())
766
db.session.commit()
767
768
return flask.redirect("/picture/" + str(resource.id))
769
770
if not 1 <= rating <= 5:
771
flask.flash("Invalid rating")
772
return flask.redirect("/picture/" + str(resource.id))
773
774
if PictureRating.query.filter_by(resource=resource, user=current_user).first():
775
PictureRating.query.filter_by(resource=resource, user=current_user).first().rating = rating
776
else:
777
# Create a new rating
778
db.session.add(PictureRating(resource, current_user, rating))
779
780
db.session.commit()
781
782
return flask.redirect("/picture/" + str(resource.id))
783
784
785
@app.route("/picture/<int:id>/edit-metadata", methods=["POST"])
786
def edit_picture_post(id):
787
resource = db.session.get(PictureResource, id)
788
if resource is None:
789
flask.abort(404)
790
791
current_user = db.session.get(User, flask.session.get("username"))
792
if current_user is None:
793
flask.abort(401)
794
795
if resource.author != current_user and not current_user.admin:
796
flask.abort(403)
797
798
title = flask.request.form["title"]
799
description = flask.request.form["description"]
800
origin_url = flask.request.form["origin_url"]
801
licence_ids = flask.request.form.getlist("licence")
802
nature_id = flask.request.form["nature"]
803
804
if not title:
805
flask.flash("Enter a title")
806
return flask.redirect(flask.request.url)
807
808
if not description:
809
description = ""
810
811
if not nature_id:
812
flask.flash("Select a picture type")
813
return flask.redirect(flask.request.url)
814
815
if not licence_ids:
816
flask.flash("Select licences")
817
return flask.redirect(flask.request.url)
818
819
licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]
820
if not any(licence.free for licence in licences):
821
flask.flash("Select at least one free licence")
822
return flask.redirect(flask.request.url)
823
824
resource.title = title
825
resource.description = description
826
resource.origin_url = origin_url
827
for licence_id in licence_ids:
828
joiner = PictureLicence(resource, db.session.get(Licence, licence_id))
829
db.session.add(joiner)
830
resource.nature = db.session.get(PictureNature, nature_id)
831
832
db.session.commit()
833
834
return flask.redirect("/picture/" + str(resource.id))
835
836
837
@app.route("/picture/<int:id>/copy")
838
def copy_picture(id):
839
resource = db.session.get(PictureResource, id)
840
if resource is None:
841
flask.abort(404)
842
843
current_user = db.session.get(User, flask.session.get("username"))
844
if current_user is None:
845
flask.abort(401)
846
847
new_resource = PictureResource(resource.title, current_user, resource.description,
848
resource.origin_url,
849
[licence.licence_id for licence in resource.licences],
850
resource.file_format,
851
resource.nature)
852
853
for region in resource.regions:
854
db.session.add(PictureRegion(region.json, new_resource, region.object))
855
856
db.session.commit()
857
858
# Create a hard link for the new picture
859
old_path = path.join(config.DATA_PATH, "pictures", str(resource.id))
860
new_path = path.join(config.DATA_PATH, "pictures", str(new_resource.id))
861
os.link(old_path, new_path)
862
863
new_resource.width = resource.width
864
new_resource.height = resource.height
865
new_resource.copied_from = resource
866
867
db.session.commit()
868
869
return flask.redirect("/picture/" + str(new_resource.id))
870
871
872
@app.route("/gallery/<int:id>/")
873
def gallery(id):
874
gallery = db.session.get(Gallery, id)
875
if gallery is None:
876
flask.abort(404)
877
878
current_user = db.session.get(User, flask.session.get("username"))
879
880
have_permission = current_user and (current_user == gallery.owner or current_user.admin or UserInGallery.query.filter_by(user=current_user, gallery=gallery).first())
881
882
return flask.render_template("gallery.html", gallery=gallery,
883
have_permission=have_permission)
884
885
886
@app.route("/create-gallery")
887
def create_gallery():
888
if "username" not in flask.session:
889
flask.flash("Log in to create galleries.")
890
return flask.redirect("/accounts")
891
892
return flask.render_template("create-gallery.html")
893
894
895
@app.route("/create-gallery", methods=["POST"])
896
def create_gallery_post():
897
if not flask.session.get("username"):
898
flask.abort(401)
899
900
if not flask.request.form.get("title"):
901
flask.flash("Enter a title")
902
return flask.redirect(flask.request.url)
903
904
description = flask.request.form.get("description", "")
905
906
gallery = Gallery(flask.request.form["title"], description,
907
db.session.get(User, flask.session["username"]))
908
db.session.add(gallery)
909
db.session.commit()
910
911
return flask.redirect("/gallery/" + str(gallery.id))
912
913
914
@app.route("/gallery/<int:id>/add-picture", methods=["POST"])
915
def gallery_add_picture(id):
916
gallery = db.session.get(Gallery, id)
917
if gallery is None:
918
flask.abort(404)
919
920
if "username" not in flask.session:
921
flask.abort(401)
922
923
if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
924
flask.abort(403)
925
926
picture_id = flask.request.form.get("picture_id")
927
if "/" in picture_id: # also allow full URLs
928
picture_id = picture_id.rstrip("/").rpartition("/")[1]
929
if not picture_id:
930
flask.flash("Select a picture")
931
return flask.redirect("/gallery/" + str(gallery.id))
932
picture_id = int(picture_id)
933
934
picture = db.session.get(PictureResource, picture_id)
935
if picture is None:
936
flask.flash("Invalid picture")
937
return flask.redirect("/gallery/" + str(gallery.id))
938
939
if PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first():
940
flask.flash("This picture is already in the gallery")
941
return flask.redirect("/gallery/" + str(gallery.id))
942
943
db.session.add(PictureInGallery(picture, gallery))
944
945
db.session.commit()
946
947
return flask.redirect("/gallery/" + str(gallery.id))
948
949
950
@app.route("/gallery/<int:id>/remove-picture", methods=["POST"])
951
def gallery_remove_picture(id):
952
gallery = db.session.get(Gallery, id)
953
if gallery is None:
954
flask.abort(404)
955
956
if "username" not in flask.session:
957
flask.abort(401)
958
959
current_user = db.session.get(User, flask.session.get("username"))
960
961
if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
962
flask.abort(403)
963
964
picture_id = int(flask.request.form.get("picture_id"))
965
966
picture = db.session.get(PictureResource, picture_id)
967
if picture is None:
968
flask.flash("Invalid picture")
969
return flask.redirect("/gallery/" + str(gallery.id))
970
971
picture_in_gallery = PictureInGallery.query.filter_by(resource=picture,
972
gallery=gallery).first()
973
if picture_in_gallery is None:
974
flask.flash("This picture isn't in the gallery")
975
return flask.redirect("/gallery/" + str(gallery.id))
976
977
db.session.delete(picture_in_gallery)
978
979
db.session.commit()
980
981
return flask.redirect("/gallery/" + str(gallery.id))
982
983
984
@app.route("/gallery/<int:id>/add-pictures-from-query", methods=["POST"])
985
def gallery_add_from_query(id):
986
gallery = db.session.get(Gallery, id)
987
if gallery is None:
988
flask.abort(404)
989
990
if "username" not in flask.session:
991
flask.abort(401)
992
993
if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
994
flask.abort(403)
995
996
query_yaml = flask.request.form.get("query", "")
997
998
yaml_parser = yaml.YAML()
999
query_data = yaml_parser.load(query_yaml) or {}
1000
query = get_picture_query(query_data)
1001
1002
pictures = query.all()
1003
1004
count = 0
1005
1006
for picture in pictures:
1007
if not PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first():
1008
db.session.add(PictureInGallery(picture, gallery))
1009
count += 1
1010
1011
db.session.commit()
1012
1013
flask.flash(f"Added {count} pictures to the gallery")
1014
1015
return flask.redirect("/gallery/" + str(gallery.id))
1016
1017
1018
@app.route("/gallery/<int:id>/users")
1019
def gallery_users(id):
1020
gallery = db.session.get(Gallery, id)
1021
if gallery is None:
1022
flask.abort(404)
1023
1024
current_user = db.session.get(User, flask.session.get("username"))
1025
have_permission = current_user and (current_user == gallery.owner or current_user.admin)
1026
1027
return flask.render_template("gallery-users.html", gallery=gallery,
1028
have_permission=have_permission)
1029
1030
1031
@app.route("/gallery/<int:id>/edit")
1032
def edit_gallery(id):
1033
gallery = db.session.get(Gallery, id)
1034
if gallery is None:
1035
flask.abort(404)
1036
1037
current_user = db.session.get(User, flask.session.get("username"))
1038
if current_user is None:
1039
flask.abort(401)
1040
1041
if current_user != gallery.owner and not current_user.admin:
1042
flask.abort(403)
1043
1044
return flask.render_template("edit-gallery.html", gallery=gallery)
1045
1046
1047
@app.route("/gallery/<int:id>/edit", methods=["POST"])
1048
def edit_gallery_post(id):
1049
gallery = db.session.get(Gallery, id)
1050
if gallery is None:
1051
flask.abort(404)
1052
1053
current_user = db.session.get(User, flask.session.get("username"))
1054
if current_user is None:
1055
flask.abort(401)
1056
1057
if current_user != gallery.owner and not current_user.admin:
1058
flask.abort(403)
1059
1060
title = flask.request.form["title"]
1061
description = flask.request.form.get("description")
1062
1063
if not title:
1064
flask.flash("Enter a title")
1065
return flask.redirect(flask.request.url)
1066
1067
if not description:
1068
description = ""
1069
1070
gallery.title = title
1071
gallery.description = description
1072
1073
db.session.commit()
1074
1075
return flask.redirect("/gallery/" + str(gallery.id))
1076
1077
1078
@app.route("/gallery/<int:id>/users/add", methods=["POST"])
1079
def gallery_add_user(id):
1080
gallery = db.session.get(Gallery, id)
1081
if gallery is None:
1082
flask.abort(404)
1083
1084
current_user = db.session.get(User, flask.session.get("username"))
1085
if current_user is None:
1086
flask.abort(401)
1087
1088
if current_user != gallery.owner and not current_user.admin:
1089
flask.abort(403)
1090
1091
username = flask.request.form.get("username")
1092
if username == gallery.owner_name:
1093
flask.flash("The owner is already in the gallery")
1094
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1095
1096
user = db.session.get(User, username)
1097
if user is None:
1098
flask.flash("User not found")
1099
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1100
1101
if UserInGallery.query.filter_by(user=user, gallery=gallery).first():
1102
flask.flash("User is already in the gallery")
1103
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1104
1105
db.session.add(UserInGallery(user, gallery))
1106
1107
db.session.commit()
1108
1109
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1110
1111
1112
@app.route("/gallery/<int:id>/users/remove", methods=["POST"])
1113
def gallery_remove_user(id):
1114
gallery = db.session.get(Gallery, id)
1115
if gallery is None:
1116
flask.abort(404)
1117
1118
current_user = db.session.get(User, flask.session.get("username"))
1119
if current_user is None:
1120
flask.abort(401)
1121
1122
if current_user != gallery.owner and not current_user.admin:
1123
flask.abort(403)
1124
1125
username = flask.request.form.get("username")
1126
user = db.session.get(User, username)
1127
if user is None:
1128
flask.flash("User not found")
1129
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1130
1131
user_in_gallery = UserInGallery.query.filter_by(user=user, gallery=gallery).first()
1132
if user_in_gallery is None:
1133
flask.flash("User is not in the gallery")
1134
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1135
1136
db.session.delete(user_in_gallery)
1137
1138
db.session.commit()
1139
1140
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1141
1142
1143
class APIError(Exception):
1144
def __init__(self, status_code, message):
1145
self.status_code = status_code
1146
self.message = message
1147
1148
1149
def get_picture_query(query_data):
1150
query = db.session.query(PictureResource)
1151
1152
descendants_cte = (
1153
db.select(PictureObject.id)
1154
.where(PictureObject.id == PictureRegion.object_id)
1155
.cte(name="descendants_cte", recursive=True)
1156
)
1157
1158
descendants_cte = descendants_cte.union_all(
1159
db.select(PictureObjectInheritance.child_id)
1160
.where(PictureObjectInheritance.parent_id == descendants_cte.c.id)
1161
)
1162
1163
requirement_conditions = {
1164
# Has an object with the ID in the given list
1165
"has_object": lambda value: PictureResource.regions.any(
1166
PictureRegion.object_id.in_(value)),
1167
# Has an object with the ID in the given list, or a subtype of it
1168
"has": lambda value: PictureResource.regions.any(
1169
PictureRegion.object_id.in_(
1170
db.select(descendants_cte.c.id)
1171
)
1172
),
1173
1174
"nature": lambda value: PictureResource.nature_id.in_(value),
1175
"licence": lambda value: PictureResource.licences.any(
1176
PictureLicence.licence_id.in_(value)),
1177
"author": lambda value: PictureResource.author_name.in_(value),
1178
"title": lambda value: PictureResource.title.ilike(value),
1179
"description": lambda value: PictureResource.description.ilike(value),
1180
"origin_url": lambda value: db.func.lower(db.func.substr(
1181
PictureResource.origin_url,
1182
db.func.length(db.func.split_part(PictureResource.origin_url, "://", 1)) + 4
1183
)).in_(value),
1184
"above_width": lambda value: PictureResource.width >= value,
1185
"below_width": lambda value: PictureResource.width <= value,
1186
"above_height": lambda value: PictureResource.height >= value,
1187
"below_height": lambda value: PictureResource.height <= value,
1188
"before_date": lambda value: PictureResource.timestamp <= datetime.utcfromtimestamp(
1189
value),
1190
"after_date": lambda value: PictureResource.timestamp >= datetime.utcfromtimestamp(
1191
value),
1192
"in_gallery": lambda value: PictureResource.galleries.any(PictureInGallery.gallery_id.in_(value)),
1193
"above_rating": lambda value: db.select(db.func.coalesce(db.func.avg(PictureRating.rating), 5)).where(PictureRating.resource_id == PictureResource.id).scalar_subquery() >= value,
1194
"below_rating": lambda value: db.select(db.func.coalesce(db.func.avg(PictureRating.rating), 0)).where(PictureRating.resource_id == PictureResource.id).scalar_subquery() <= value,
1195
"above_rating_count": lambda value: db.select(db.func.count(PictureRating.id)).where(PictureRating.resource_id == PictureResource.id).correlate(PictureResource).scalar_subquery() >= value,
1196
"below_rating_count": lambda value: db.select(db.func.count(PictureRating.id)).where(PictureRating.resource_id == PictureResource.id).correlate(PictureResource).scalar_subquery() <= value,
1197
"above_region_count": lambda value: db.select(db.func.count(PictureRegion.id)).where(PictureRegion.resource_id == PictureResource.id).correlate(PictureResource).scalar_subquery() >= value,
1198
"below_region_count": lambda value: db.select(db.func.count(PictureRegion.id)).where(PictureRegion.resource_id == PictureResource.id).correlate(PictureResource).scalar_subquery() <= value,
1199
"copied_from": lambda value: PictureResource.copied_from_id.in_(value),
1200
}
1201
1202
if "want" in query_data:
1203
for i in query_data["want"]:
1204
if len(i) != 1:
1205
raise APIError(400, "Each requirement must have exactly one key")
1206
requirement, value = list(i.items())[0]
1207
if requirement not in requirement_conditions:
1208
raise APIError(400, f"Unknown requirement type: {requirement}")
1209
1210
condition = requirement_conditions[requirement]
1211
query = query.filter(condition(value))
1212
if "exclude" in query_data:
1213
for i in query_data["exclude"]:
1214
if len(i) != 1:
1215
raise APIError(400, "Each exclusion must have exactly one key")
1216
requirement, value = list(i.items())[0]
1217
if requirement not in requirement_conditions:
1218
raise APIError(400, f"Unknown requirement type: {requirement}")
1219
1220
condition = requirement_conditions[requirement]
1221
query = query.filter(~condition(value))
1222
if not query_data.get("include_obsolete", False):
1223
query = query.filter(PictureResource.replaced_by_id.is_(None))
1224
1225
return query
1226
1227
1228
@app.route("/query-pictures")
1229
def graphical_query_pictures():
1230
return flask.render_template("graphical-query-pictures.html")
1231
1232
1233
@app.route("/query-pictures-results")
1234
def graphical_query_pictures_results():
1235
query_yaml = flask.request.args.get("query", "")
1236
yaml_parser = yaml.YAML()
1237
query_data = yaml_parser.load(query_yaml) or {}
1238
try:
1239
query = get_picture_query(query_data)
1240
except APIError as e:
1241
flask.abort(e.status_code)
1242
1243
page = int(flask.request.args.get("page", 1))
1244
per_page = int(flask.request.args.get("per_page", 16))
1245
1246
resources = query.paginate(page=page, per_page=per_page)
1247
1248
return flask.render_template("graphical-query-pictures-results.html", resources=resources,
1249
query=query_yaml,
1250
page_number=page, page_length=per_page,
1251
num_pages=resources.pages,
1252
prev_page=resources.prev_num, next_page=resources.next_num)
1253
1254
1255
@app.route("/raw/picture/<int:id>")
1256
def raw_picture(id):
1257
resource = db.session.get(PictureResource, id)
1258
if resource is None:
1259
flask.abort(404)
1260
1261
response = flask.send_from_directory(path.join(config.DATA_PATH, "pictures"),
1262
str(resource.id))
1263
response.mimetype = resource.file_format
1264
1265
return response
1266
1267
1268
@app.route("/object/")
1269
def graphical_object_types():
1270
return flask.render_template("object-types.html", objects=PictureObject.query.all())
1271
1272
1273
@app.route("/api/object-types")
1274
def object_types():
1275
objects = db.session.query(PictureObject).all()
1276
return flask.jsonify({object.id: object.description for object in objects})
1277
1278
1279
@app.route("/api/query-pictures", methods=["POST"]) # sadly GET can't have a body
1280
def query_pictures():
1281
offset = int(flask.request.args.get("offset", 0))
1282
limit = int(flask.request.args.get("limit", 16))
1283
ordering = flask.request.args.get("ordering", "date-desc")
1284
1285
yaml_parser = yaml.YAML()
1286
query_data = yaml_parser.load(flask.request.data) or {}
1287
try:
1288
query = get_picture_query(query_data)
1289
except APIError as e:
1290
return flask.jsonify({"error": e.message}), e.status_code
1291
1292
rating_count_subquery = db.select(db.func.count(PictureRating.id)).where(
1293
PictureRating.resource_id == PictureResource.id).scalar_subquery()
1294
region_count_subquery = db.select(db.func.count(PictureRegion.id)).where(
1295
PictureRegion.resource_id == PictureResource.id).scalar_subquery()
1296
rating_subquery = db.select(db.func.coalesce(db.func.avg(PictureRating.rating), 0)).where(
1297
PictureRating.resource_id == PictureResource.id).scalar_subquery()
1298
1299
match ordering:
1300
case "date-desc":
1301
query = query.order_by(PictureResource.timestamp.desc())
1302
case "date-asc":
1303
query = query.order_by(PictureResource.timestamp.asc())
1304
case "title-asc":
1305
query = query.order_by(PictureResource.title.asc())
1306
case "title-desc":
1307
query = query.order_by(PictureResource.title.desc())
1308
case "random":
1309
query = query.order_by(db.func.random())
1310
case "number-regions-desc":
1311
query = query.order_by(region_count_subquery.desc())
1312
case "number-regions-asc":
1313
query = query.order_by(region_count_subquery.asc())
1314
case "rating-desc":
1315
query = query.order_by(rating_subquery.desc())
1316
case "rating-asc":
1317
query = query.order_by(rating_subquery.asc())
1318
case "number-ratings-desc":
1319
query = query.order_by(rating_count_subquery.desc())
1320
case "number-ratings-asc":
1321
query = query.order_by(rating_count_subquery.asc())
1322
1323
query = query.offset(offset).limit(limit)
1324
resources = query.all()
1325
1326
json_response = {
1327
"date_generated": datetime.utcnow().timestamp(),
1328
"resources": [],
1329
"offset": offset,
1330
"limit": limit,
1331
}
1332
1333
json_resources = json_response["resources"]
1334
1335
for resource in resources:
1336
json_resource = {
1337
"id": resource.id,
1338
"title": resource.title,
1339
"description": resource.description,
1340
"timestamp": resource.timestamp.timestamp(),
1341
"origin_url": resource.origin_url,
1342
"author": resource.author_name,
1343
"file_format": resource.file_format,
1344
"width": resource.width,
1345
"height": resource.height,
1346
"nature": resource.nature_id,
1347
"licences": [licence.licence_id for licence in resource.licences],
1348
"replaces": resource.replaces_id,
1349
"replaced_by": resource.replaced_by_id,
1350
"regions": [],
1351
"download": config.ROOT_URL + flask.url_for("raw_picture", id=resource.id),
1352
}
1353
for region in resource.regions:
1354
json_resource["regions"].append({
1355
"object": region.object_id,
1356
"type": region.json["type"],
1357
"shape": region.json["shape"],
1358
})
1359
1360
json_resources.append(json_resource)
1361
1362
return flask.jsonify(json_response)
1363
1364
1365
@app.route("/api/picture/<int:id>/")
1366
def api_picture(id):
1367
resource = db.session.get(PictureResource, id)
1368
if resource is None:
1369
flask.abort(404)
1370
1371
json_resource = {
1372
"id": resource.id,
1373
"title": resource.title,
1374
"description": resource.description,
1375
"timestamp": resource.timestamp.timestamp(),
1376
"origin_url": resource.origin_url,
1377
"author": resource.author_name,
1378
"file_format": resource.file_format,
1379
"width": resource.width,
1380
"height": resource.height,
1381
"nature": resource.nature_id,
1382
"licences": [licence.licence_id for licence in resource.licences],
1383
"replaces": resource.replaces_id,
1384
"replaced_by": resource.replaced_by_id,
1385
"regions": [],
1386
"download": config.ROOT_URL + flask.url_for("raw_picture", id=resource.id),
1387
"rating_average": resource.average_rating,
1388
"rating_count": resource.rating_totals,
1389
}
1390
for region in resource.regions:
1391
json_resource["regions"].append({
1392
"object": region.object_id,
1393
"type": region.json["type"],
1394
"shape": region.json["shape"],
1395
})
1396
1397
return flask.jsonify(json_resource)
1398
1399
1400
@app.route("/api/licence/")
1401
def api_licences():
1402
licences = db.session.query(Licence).all()
1403
json_licences = {
1404
licence.id: {
1405
"title": licence.title,
1406
"free": licence.free,
1407
"pinned": licence.pinned,
1408
} for licence in licences
1409
}
1410
1411
return flask.jsonify(json_licences)
1412
1413
1414
@app.route("/api/licence/<id>/")
1415
def api_licence(id):
1416
licence = db.session.get(Licence, id)
1417
if licence is None:
1418
flask.abort(404)
1419
1420
json_licence = {
1421
"id": licence.id,
1422
"title": licence.title,
1423
"description": licence.description,
1424
"info_url": licence.info_url,
1425
"legalese_url": licence.url,
1426
"free": licence.free,
1427
"logo_url": licence.logo_url,
1428
"pinned": licence.pinned,
1429
}
1430
1431
return flask.jsonify(json_licence)
1432
1433
1434
@app.route("/api/nature/")
1435
def api_natures():
1436
natures = db.session.query(PictureNature).all()
1437
json_natures = {
1438
nature.id: nature.description for nature in natures
1439
}
1440
1441
return flask.jsonify(json_natures)
1442
1443
1444
@app.route("/api/user/")
1445
def api_users():
1446
offset = int(flask.request.args.get("offset", 0))
1447
limit = int(flask.request.args.get("limit", 16))
1448
1449
users = db.session.query(User).offset(offset).limit(limit).all()
1450
1451
json_users = {
1452
user.username: {
1453
"admin": user.admin,
1454
} for user in users
1455
}
1456
1457
return flask.jsonify(json_users)
1458
1459
1460
@app.route("/api/user/<username>/")
1461
def api_user(username):
1462
user = db.session.get(User, username)
1463
if user is None:
1464
flask.abort(404)
1465
1466
json_user = {
1467
"username": user.username,
1468
"admin": user.admin,
1469
"joined": user.joined_timestamp.timestamp(),
1470
}
1471
1472
return flask.jsonify(json_user)
1473
1474
1475
@app.route("/api/login", methods=["POST"])
1476
def api_login():
1477
username = flask.request.json["username"]
1478
password = flask.request.json["password"]
1479
1480
user = db.session.get(User, username)
1481
1482
if user is None:
1483
return flask.jsonify({"error": "This username is not registered. To prevent spam, you must use the HTML interface to register."}), 401
1484
1485
if not bcrypt.check_password_hash(user.password_hashed, password):
1486
return flask.jsonify({"error": "Incorrect password"}), 401
1487
1488
flask.session["username"] = username
1489
1490
return flask.jsonify({"message": "You have been logged in. Your HTTP client must support cookies to use features of this API that require authentication."})
1491
1492
1493
@app.route("/api/logout", methods=["POST"])
1494
def api_logout():
1495
flask.session.pop("username", None)
1496
return flask.jsonify({"message": "You have been logged out."})
1497
1498
1499
@app.route("/api/upload", methods=["POST"])
1500
def api_upload():
1501
if "username" not in flask.session:
1502
return flask.jsonify({"error": "You must be logged in to upload pictures"}), 401
1503
1504
json_ = json.loads(flask.request.form["json"])
1505
title = json_["title"]
1506
description = json_.get("description", "")
1507
origin_url = json_.get("origin_url", "")
1508
author = db.session.get(User, flask.session["username"])
1509
licence_ids = json_["licence"]
1510
nature_id = json_["nature"]
1511
file = flask.request.files["file"]
1512
1513
if not file or not file.filename:
1514
return flask.jsonify({"error": "An image file must be uploaded"}), 400
1515
1516
if not file.mimetype.startswith("image/") or file.mimetype == "image/svg+xml":
1517
return flask.jsonify({"error": "Only bitmap images are supported"}), 400
1518
1519
if not title:
1520
return flask.jsonify({"error": "Give a title"}), 400
1521
1522
if not description:
1523
description = ""
1524
1525
if not nature_id:
1526
return flask.jsonify({"error": "Give a picture type"}), 400
1527
1528
if not licence_ids:
1529
return flask.jsonify({"error": "Give licences"}), 400
1530
1531
licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]
1532
if not any(licence.free for licence in licences):
1533
return flask.jsonify({"error": "Use at least one free licence"}), 400
1534
1535
resource = PictureResource(title, author, description, origin_url, licence_ids,
1536
file.mimetype,
1537
db.session.get(PictureNature, nature_id))
1538
db.session.add(resource)
1539
db.session.commit()
1540
file.save(path.join(config.DATA_PATH, "pictures", str(resource.id)))
1541
pil_image = Image.open(path.join(config.DATA_PATH, "pictures", str(resource.id)))
1542
resource.width, resource.height = pil_image.size
1543
db.session.commit()
1544
1545
if json_.get("annotations"):
1546
try:
1547
resource.put_annotations(json_["annotations"])
1548
db.session.commit()
1549
except json.JSONDecodeError:
1550
return flask.jsonify({"error": "Invalid annotations"}), 400
1551
1552
return flask.jsonify({"message": "Picture uploaded successfully", "id": resource.id})
1553
1554
1555
@app.route("/api/picture/<int:id>/update", methods=["POST"])
1556
def api_update_picture(id):
1557
resource = db.session.get(PictureResource, id)
1558
if resource is None:
1559
return flask.jsonify({"error": "Picture not found"}), 404
1560
current_user = db.session.get(User, flask.session.get("username"))
1561
if current_user is None:
1562
return flask.jsonify({"error": "You must be logged in to edit pictures"}), 401
1563
if resource.author != current_user and not current_user.admin:
1564
return flask.jsonify({"error": "You are not the author of this picture"}), 403
1565
1566
title = flask.request.json.get("title", resource.title)
1567
description = flask.request.json.get("description", resource.description)
1568
origin_url = flask.request.json.get("origin_url", resource.origin_url)
1569
licence_ids = flask.request.json.get("licence", [licence.licence_id for licence in resource.licences])
1570
nature_id = flask.request.json.get("nature", resource.nature_id)
1571
1572
if not title:
1573
return flask.jsonify({"error": "Give a title"}), 400
1574
1575
if not description:
1576
description = ""
1577
1578
if not nature_id:
1579
return flask.jsonify({"error": "Give a picture type"}), 400
1580
1581
if not licence_ids:
1582
return flask.jsonify({"error": "Give licences"}), 400
1583
1584
licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]
1585
1586
if not any(licence.free for licence in licences):
1587
return flask.jsonify({"error": "Use at least one free licence"}), 400
1588
1589
resource.title = title
1590
resource.description = description
1591
resource.origin_url = origin_url
1592
resource.licences = licences
1593
resource.nature = db.session.get(PictureNature, nature_id)
1594
1595
db.session.commit()
1596
1597
return flask.jsonify({"message": "Picture updated successfully"})
1598
1599
1600
@app.route("/api/picture/<int:id>/rate", methods=["POST"])
1601
def api_rate_picture(id):
1602
resource = db.session.get(PictureResource, id)
1603
if resource is None:
1604
flask.abort(404)
1605
1606
current_user = db.session.get(User, flask.session.get("username"))
1607
if current_user is None:
1608
flask.abort(401)
1609
1610
rating = int(flask.request.json.get("rating", 0))
1611
1612
if not rating:
1613
# Delete the existing rating
1614
if PictureRating.query.filter_by(resource=resource, user=current_user).first():
1615
db.session.delete(PictureRating.query.filter_by(resource=resource,
1616
user=current_user).first())
1617
db.session.commit()
1618
1619
return flask.jsonify({"message": "Existing rating removed"})
1620
1621
if not 1 <= rating <= 5:
1622
flask.flash("Invalid rating")
1623
return flask.jsonify({"error": "Invalid rating"}), 400
1624
1625
if PictureRating.query.filter_by(resource=resource, user=current_user).first():
1626
PictureRating.query.filter_by(resource=resource, user=current_user).first().rating = rating
1627
else:
1628
# Create a new rating
1629
db.session.add(PictureRating(resource, current_user, rating))
1630
1631
db.session.commit()
1632
1633
return flask.jsonify({"message": "Rating saved"})
1634
1635
1636
@app.route("/api/gallery/<int:id>/")
1637
def api_gallery(id):
1638
gallery = db.session.get(Gallery, id)
1639
if gallery is None:
1640
flask.abort(404)
1641
1642
json_gallery = {
1643
"id": gallery.id,
1644
"title": gallery.title,
1645
"description": gallery.description,
1646
"owner": gallery.owner_name,
1647
"users": [user.username for user in gallery.users],
1648
}
1649
1650
return flask.jsonify(json_gallery)
1651
1652
1653
@app.route("/api/gallery/<int:id>/edit", methods=["POST"])
1654
def api_edit_gallery(id):
1655
gallery = db.session.get(Gallery, id)
1656
if gallery is None:
1657
flask.abort(404)
1658
1659
current_user = db.session.get(User, flask.session.get("username"))
1660
if current_user is None:
1661
flask.abort(401)
1662
1663
if current_user != gallery.owner and not current_user.admin:
1664
flask.abort(403)
1665
1666
title = flask.request.json.get("title", gallery.title)
1667
description = flask.request.json.get("description", gallery.description)
1668
1669
if not title:
1670
return flask.jsonify({"error": "Give a title"}), 400
1671
1672
if not description:
1673
description = ""
1674
1675
gallery.title = title
1676
gallery.description = description
1677
1678
db.session.commit()
1679
1680
return flask.jsonify({"message": "Gallery updated successfully"})
1681
1682
1683
@app.route("/api/new-gallery", methods=["POST"])
1684
def api_new_gallery():
1685
if "username" not in flask.session:
1686
return flask.jsonify({"error": "You must be logged in to create galleries"}), 401
1687
1688
title = flask.request.json.get("title")
1689
description = flask.request.json.get("description", "")
1690
1691
if not title:
1692
return flask.jsonify({"error": "Give a title"}), 400
1693
1694
gallery = Gallery(title, description, db.session.get(User, flask.session["username"]))
1695
db.session.add(gallery)
1696
db.session.commit()
1697
1698
return flask.jsonify({"message": "Gallery created successfully", "id": gallery.id})
1699
1700
1701
@app.route("/api/gallery/<int:id>/add-picture", methods=["POST"])
1702
def api_gallery_add_picture(id):
1703
gallery = db.session.get(Gallery, id)
1704
if gallery is None:
1705
flask.abort(404)
1706
1707
if "username" not in flask.session:
1708
return flask.jsonify({"error": "You must be logged in to add pictures to galleries"}), 401
1709
1710
current_user = db.session.get(User, flask.session.get("username"))
1711
1712
if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
1713
return flask.jsonify({"error": "You do not have permission to add pictures to this gallery"}), 403
1714
1715
picture_id = flask.request.json.get("picture_id")
1716
1717
try:
1718
picture_id = int(picture_id)
1719
except ValueError:
1720
return flask.jsonify({"error": "Invalid picture ID"}), 400
1721
1722
picture = db.session.get(PictureResource, picture_id)
1723
if picture is None:
1724
return flask.jsonify({"error": "The picture doesn't exist"}), 404
1725
1726
if PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first():
1727
return flask.jsonify({"error": "This picture is already in the gallery"}), 400
1728
1729
db.session.add(PictureInGallery(picture, gallery))
1730
1731
db.session.commit()
1732
1733
return flask.jsonify({"message": "Picture added to gallery"})
1734
1735
1736
@app.route("/api/gallery/<int:id>/remove-picture", methods=["POST"])
1737
def api_gallery_remove_picture(id):
1738
gallery = db.session.get(Gallery, id)
1739
if gallery is None:
1740
flask.abort(404)
1741
1742
if "username" not in flask.session:
1743
return flask.jsonify({"error": "You must be logged in to remove pictures from galleries"}), 401
1744
1745
current_user = db.session.get(User, flask.session.get("username"))
1746
1747
if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
1748
return flask.jsonify({"error": "You do not have permission to remove pictures from this gallery"}), 403
1749
1750
picture_id = flask.request.json.get("picture_id")
1751
1752
try:
1753
picture_id = int(picture_id)
1754
except ValueError:
1755
return flask.jsonify({"error": "Invalid picture ID"}), 400
1756
1757
picture = db.session.get(PictureResource, picture_id)
1758
if picture is None:
1759
return flask.jsonify({"error": "The picture doesn't exist"}), 404
1760
1761
picture_in_gallery = PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first()
1762
if picture_in_gallery is None:
1763
return flask.jsonify({"error": "This picture isn't in the gallery"}), 400
1764
1765
db.session.delete(picture_in_gallery)
1766
1767
db.session.commit()
1768
1769
return flask.jsonify({"message": "Picture removed from gallery"})
1770
1771
1772
@app.route("/api/gallery/<int:id>/users/add", methods=["POST"])
1773
def api_gallery_add_user(id):
1774
gallery = db.session.get(Gallery, id)
1775
if gallery is None:
1776
flask.abort(404)
1777
1778
current_user = db.session.get(User, flask.session.get("username"))
1779
if current_user is None:
1780
flask.abort(401)
1781
1782
if current_user != gallery.owner and not current_user.admin:
1783
flask.abort(403)
1784
1785
username = flask.request.json.get("username")
1786
if username == gallery.owner_name:
1787
return flask.jsonify({"error": "The owner cannot be added to trusted users"}), 400
1788
1789
user = db.session.get(User, username)
1790
if user is None:
1791
return flask.jsonify({"error": "User not found"}), 404
1792
1793
if UserInGallery.query.filter_by(user=user, gallery=gallery).first():
1794
return flask.jsonify({"error": "User is already in the gallery"}), 400
1795
1796
db.session.add(UserInGallery(user, gallery))
1797
1798
db.session.commit()
1799
1800
return flask.jsonify({"message": "User added to gallery"})
1801
1802
1803
@app.route("/api/gallery/<int:id>/users/remove", methods=["POST"])
1804
def api_gallery_remove_user(id):
1805
gallery = db.session.get(Gallery, id)
1806
if gallery is None:
1807
flask.abort(404)
1808
1809
current_user = db.session.get(User, flask.session.get("username"))
1810
if current_user is None:
1811
flask.abort(401)
1812
1813
if current_user != gallery.owner and not current_user.admin:
1814
flask.abort(403)
1815
1816
username = flask.request.json.get("username")
1817
user = db.session.get(User, username)
1818
if user is None:
1819
return flask.jsonify({"error": "User not found"}), 404
1820
1821
user_in_gallery = UserInGallery.query.filter_by(user=user, gallery=gallery).first()
1822
if user_in_gallery is None:
1823
return flask.jsonify({"error": "User is not in the gallery"}), 400
1824
1825
db.session.delete(user_in_gallery)
1826
1827
db.session.commit()
1828
1829
return flask.jsonify({"message": "User removed from gallery"})
1830
1831