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