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
PictureRating.query.filter_by(resource=resource).delete()
662
if resource.replaces:
663
resource.replaces.replaced_by = None
664
if resource.replaced_by:
665
resource.replaced_by.replaces = None
666
resource.copied_from = None
667
for copy in resource.copies:
668
copy.copied_from = None
669
db.session.delete(resource)
670
db.session.commit()
671
672
return flask.redirect("/")
673
674
675
@app.route("/picture/<int:id>/mark-replacement", methods=["POST"])
676
def mark_picture_replacement(id):
677
resource = db.session.get(PictureResource, id)
678
if resource is None:
679
flask.abort(404)
680
681
current_user = db.session.get(User, flask.session.get("username"))
682
if current_user is None:
683
flask.abort(401)
684
685
if resource.copied_from.author != current_user and not current_user.admin:
686
flask.abort(403)
687
688
resource.copied_from.replaced_by = resource
689
resource.replaces = resource.copied_from
690
691
db.session.commit()
692
693
return flask.redirect("/picture/" + str(resource.copied_from.id))
694
695
696
@app.route("/picture/<int:id>/remove-replacement", methods=["POST"])
697
def remove_picture_replacement(id):
698
resource = db.session.get(PictureResource, id)
699
if resource is None:
700
flask.abort(404)
701
702
current_user = db.session.get(User, flask.session.get("username"))
703
if current_user is None:
704
flask.abort(401)
705
706
if resource.author != current_user and not current_user.admin:
707
flask.abort(403)
708
709
resource.replaced_by.replaces = None
710
resource.replaced_by = None
711
712
db.session.commit()
713
714
return flask.redirect("/picture/" + str(resource.id))
715
716
717
@app.route("/picture/<int:id>/edit-metadata")
718
def edit_picture(id):
719
resource = db.session.get(PictureResource, id)
720
if resource is None:
721
flask.abort(404)
722
723
current_user = db.session.get(User, flask.session.get("username"))
724
if current_user is None:
725
flask.abort(401)
726
727
if resource.author != current_user and not current_user.admin:
728
flask.abort(403)
729
730
licences = Licence.query.order_by(Licence.free.desc(), Licence.pinned.desc(),
731
Licence.title).all()
732
733
types = PictureNature.query.all()
734
735
return flask.render_template("edit-picture.html", resource=resource, licences=licences,
736
types=types,
737
PictureLicence=PictureLicence)
738
739
740
@app.route("/picture/<int:id>/rate", methods=["POST"])
741
def rate_picture(id):
742
resource = db.session.get(PictureResource, id)
743
if resource is None:
744
flask.abort(404)
745
746
current_user = db.session.get(User, flask.session.get("username"))
747
if current_user is None:
748
flask.abort(401)
749
750
rating = int(flask.request.form.get("rating"))
751
752
if not rating:
753
# Delete the existing rating
754
if PictureRating.query.filter_by(resource=resource, user=current_user).first():
755
db.session.delete(PictureRating.query.filter_by(resource=resource,
756
user=current_user).first())
757
db.session.commit()
758
759
return flask.redirect("/picture/" + str(resource.id))
760
761
if not 1 <= rating <= 5:
762
flask.flash("Invalid rating")
763
return flask.redirect("/picture/" + str(resource.id))
764
765
if PictureRating.query.filter_by(resource=resource, user=current_user).first():
766
PictureRating.query.filter_by(resource=resource, user=current_user).first().rating = rating
767
else:
768
# Create a new rating
769
db.session.add(PictureRating(resource, current_user, rating))
770
771
db.session.commit()
772
773
return flask.redirect("/picture/" + str(resource.id))
774
775
776
@app.route("/picture/<int:id>/edit-metadata", methods=["POST"])
777
def edit_picture_post(id):
778
resource = db.session.get(PictureResource, id)
779
if resource is None:
780
flask.abort(404)
781
782
current_user = db.session.get(User, flask.session.get("username"))
783
if current_user is None:
784
flask.abort(401)
785
786
if resource.author != current_user and not current_user.admin:
787
flask.abort(403)
788
789
title = flask.request.form["title"]
790
description = flask.request.form["description"]
791
origin_url = flask.request.form["origin_url"]
792
licence_ids = flask.request.form.getlist("licence")
793
nature_id = flask.request.form["nature"]
794
795
if not title:
796
flask.flash("Enter a title")
797
return flask.redirect(flask.request.url)
798
799
if not description:
800
description = ""
801
802
if not nature_id:
803
flask.flash("Select a picture type")
804
return flask.redirect(flask.request.url)
805
806
if not licence_ids:
807
flask.flash("Select licences")
808
return flask.redirect(flask.request.url)
809
810
licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]
811
if not any(licence.free for licence in licences):
812
flask.flash("Select at least one free licence")
813
return flask.redirect(flask.request.url)
814
815
resource.title = title
816
resource.description = description
817
resource.origin_url = origin_url
818
for licence_id in licence_ids:
819
joiner = PictureLicence(resource, db.session.get(Licence, licence_id))
820
db.session.add(joiner)
821
resource.nature = db.session.get(PictureNature, nature_id)
822
823
db.session.commit()
824
825
return flask.redirect("/picture/" + str(resource.id))
826
827
828
@app.route("/picture/<int:id>/copy")
829
def copy_picture(id):
830
resource = db.session.get(PictureResource, id)
831
if resource is None:
832
flask.abort(404)
833
834
current_user = db.session.get(User, flask.session.get("username"))
835
if current_user is None:
836
flask.abort(401)
837
838
new_resource = PictureResource(resource.title, current_user, resource.description,
839
resource.origin_url,
840
[licence.licence_id for licence in resource.licences],
841
resource.file_format,
842
resource.nature)
843
844
for region in resource.regions:
845
db.session.add(PictureRegion(region.json, new_resource, region.object))
846
847
db.session.commit()
848
849
# Create a hard link for the new picture
850
old_path = path.join(config.DATA_PATH, "pictures", str(resource.id))
851
new_path = path.join(config.DATA_PATH, "pictures", str(new_resource.id))
852
os.link(old_path, new_path)
853
854
new_resource.width = resource.width
855
new_resource.height = resource.height
856
new_resource.copied_from = resource
857
858
db.session.commit()
859
860
return flask.redirect("/picture/" + str(new_resource.id))
861
862
863
@app.route("/gallery/<int:id>/")
864
def gallery(id):
865
gallery = db.session.get(Gallery, id)
866
if gallery is None:
867
flask.abort(404)
868
869
current_user = db.session.get(User, flask.session.get("username"))
870
871
have_permission = current_user and (current_user == gallery.owner or current_user.admin or UserInGallery.query.filter_by(user=current_user, gallery=gallery).first())
872
873
return flask.render_template("gallery.html", gallery=gallery,
874
have_permission=have_permission)
875
876
877
@app.route("/create-gallery")
878
def create_gallery():
879
if "username" not in flask.session:
880
flask.flash("Log in to create galleries.")
881
return flask.redirect("/accounts")
882
883
return flask.render_template("create-gallery.html")
884
885
886
@app.route("/create-gallery", methods=["POST"])
887
def create_gallery_post():
888
if not flask.session.get("username"):
889
flask.abort(401)
890
891
if not flask.request.form.get("title"):
892
flask.flash("Enter a title")
893
return flask.redirect(flask.request.url)
894
895
description = flask.request.form.get("description", "")
896
897
gallery = Gallery(flask.request.form["title"], description,
898
db.session.get(User, flask.session["username"]))
899
db.session.add(gallery)
900
db.session.commit()
901
902
return flask.redirect("/gallery/" + str(gallery.id))
903
904
905
@app.route("/gallery/<int:id>/add-picture", methods=["POST"])
906
def gallery_add_picture(id):
907
gallery = db.session.get(Gallery, id)
908
if gallery is None:
909
flask.abort(404)
910
911
if "username" not in flask.session:
912
flask.abort(401)
913
914
if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
915
flask.abort(403)
916
917
picture_id = flask.request.form.get("picture_id")
918
if "/" in picture_id: # also allow full URLs
919
picture_id = picture_id.rstrip("/").rpartition("/")[1]
920
if not picture_id:
921
flask.flash("Select a picture")
922
return flask.redirect("/gallery/" + str(gallery.id))
923
picture_id = int(picture_id)
924
925
picture = db.session.get(PictureResource, picture_id)
926
if picture is None:
927
flask.flash("Invalid picture")
928
return flask.redirect("/gallery/" + str(gallery.id))
929
930
if PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first():
931
flask.flash("This picture is already in the gallery")
932
return flask.redirect("/gallery/" + str(gallery.id))
933
934
db.session.add(PictureInGallery(picture, gallery))
935
936
db.session.commit()
937
938
return flask.redirect("/gallery/" + str(gallery.id))
939
940
941
@app.route("/gallery/<int:id>/remove-picture", methods=["POST"])
942
def gallery_remove_picture(id):
943
gallery = db.session.get(Gallery, id)
944
if gallery is None:
945
flask.abort(404)
946
947
if "username" not in flask.session:
948
flask.abort(401)
949
950
current_user = db.session.get(User, flask.session.get("username"))
951
952
if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
953
flask.abort(403)
954
955
picture_id = int(flask.request.form.get("picture_id"))
956
957
picture = db.session.get(PictureResource, picture_id)
958
if picture is None:
959
flask.flash("Invalid picture")
960
return flask.redirect("/gallery/" + str(gallery.id))
961
962
picture_in_gallery = PictureInGallery.query.filter_by(resource=picture,
963
gallery=gallery).first()
964
if picture_in_gallery is None:
965
flask.flash("This picture isn't in the gallery")
966
return flask.redirect("/gallery/" + str(gallery.id))
967
968
db.session.delete(picture_in_gallery)
969
970
db.session.commit()
971
972
return flask.redirect("/gallery/" + str(gallery.id))
973
974
975
@app.route("/gallery/<int:id>/add-pictures-from-query", methods=["POST"])
976
def gallery_add_from_query(id):
977
gallery = db.session.get(Gallery, id)
978
if gallery is None:
979
flask.abort(404)
980
981
if "username" not in flask.session:
982
flask.abort(401)
983
984
if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
985
flask.abort(403)
986
987
query_yaml = flask.request.form.get("query", "")
988
989
yaml_parser = yaml.YAML()
990
query_data = yaml_parser.load(query_yaml) or {}
991
query = get_picture_query(query_data)
992
993
pictures = query.all()
994
995
count = 0
996
997
for picture in pictures:
998
if not PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first():
999
db.session.add(PictureInGallery(picture, gallery))
1000
count += 1
1001
1002
db.session.commit()
1003
1004
flask.flash(f"Added {count} pictures to the gallery")
1005
1006
return flask.redirect("/gallery/" + str(gallery.id))
1007
1008
1009
@app.route("/gallery/<int:id>/users")
1010
def gallery_users(id):
1011
gallery = db.session.get(Gallery, id)
1012
if gallery is None:
1013
flask.abort(404)
1014
1015
current_user = db.session.get(User, flask.session.get("username"))
1016
have_permission = current_user and (current_user == gallery.owner or current_user.admin)
1017
1018
return flask.render_template("gallery-users.html", gallery=gallery,
1019
have_permission=have_permission)
1020
1021
1022
@app.route("/gallery/<int:id>/edit")
1023
def edit_gallery(id):
1024
gallery = db.session.get(Gallery, id)
1025
if gallery is None:
1026
flask.abort(404)
1027
1028
current_user = db.session.get(User, flask.session.get("username"))
1029
if current_user is None:
1030
flask.abort(401)
1031
1032
if current_user != gallery.owner and not current_user.admin:
1033
flask.abort(403)
1034
1035
return flask.render_template("edit-gallery.html", gallery=gallery)
1036
1037
1038
@app.route("/gallery/<int:id>/edit", methods=["POST"])
1039
def edit_gallery_post(id):
1040
gallery = db.session.get(Gallery, id)
1041
if gallery is None:
1042
flask.abort(404)
1043
1044
current_user = db.session.get(User, flask.session.get("username"))
1045
if current_user is None:
1046
flask.abort(401)
1047
1048
if current_user != gallery.owner and not current_user.admin:
1049
flask.abort(403)
1050
1051
title = flask.request.form["title"]
1052
description = flask.request.form.get("description")
1053
1054
if not title:
1055
flask.flash("Enter a title")
1056
return flask.redirect(flask.request.url)
1057
1058
if not description:
1059
description = ""
1060
1061
gallery.title = title
1062
gallery.description = description
1063
1064
db.session.commit()
1065
1066
return flask.redirect("/gallery/" + str(gallery.id))
1067
1068
1069
@app.route("/gallery/<int:id>/users/add", methods=["POST"])
1070
def gallery_add_user(id):
1071
gallery = db.session.get(Gallery, id)
1072
if gallery is None:
1073
flask.abort(404)
1074
1075
current_user = db.session.get(User, flask.session.get("username"))
1076
if current_user is None:
1077
flask.abort(401)
1078
1079
if current_user != gallery.owner and not current_user.admin:
1080
flask.abort(403)
1081
1082
username = flask.request.form.get("username")
1083
if username == gallery.owner_name:
1084
flask.flash("The owner is already in the gallery")
1085
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1086
1087
user = db.session.get(User, username)
1088
if user is None:
1089
flask.flash("User not found")
1090
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1091
1092
if UserInGallery.query.filter_by(user=user, gallery=gallery).first():
1093
flask.flash("User is already in the gallery")
1094
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1095
1096
db.session.add(UserInGallery(user, gallery))
1097
1098
db.session.commit()
1099
1100
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1101
1102
1103
@app.route("/gallery/<int:id>/users/remove", methods=["POST"])
1104
def gallery_remove_user(id):
1105
gallery = db.session.get(Gallery, id)
1106
if gallery is None:
1107
flask.abort(404)
1108
1109
current_user = db.session.get(User, flask.session.get("username"))
1110
if current_user is None:
1111
flask.abort(401)
1112
1113
if current_user != gallery.owner and not current_user.admin:
1114
flask.abort(403)
1115
1116
username = flask.request.form.get("username")
1117
user = db.session.get(User, username)
1118
if user is None:
1119
flask.flash("User not found")
1120
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1121
1122
user_in_gallery = UserInGallery.query.filter_by(user=user, gallery=gallery).first()
1123
if user_in_gallery is None:
1124
flask.flash("User is not in the gallery")
1125
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1126
1127
db.session.delete(user_in_gallery)
1128
1129
db.session.commit()
1130
1131
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1132
1133
1134
class APIError(Exception):
1135
def __init__(self, status_code, message):
1136
self.status_code = status_code
1137
self.message = message
1138
1139
1140
def get_picture_query(query_data):
1141
query = db.session.query(PictureResource)
1142
1143
requirement_conditions = {
1144
"has_object": lambda value: PictureResource.regions.any(
1145
PictureRegion.object_id.in_(value)),
1146
"nature": lambda value: PictureResource.nature_id.in_(value),
1147
"licence": lambda value: PictureResource.licences.any(
1148
PictureLicence.licence_id.in_(value)),
1149
"author": lambda value: PictureResource.author_name.in_(value),
1150
"title": lambda value: PictureResource.title.ilike(value),
1151
"description": lambda value: PictureResource.description.ilike(value),
1152
"origin_url": lambda value: db.func.lower(db.func.substr(
1153
PictureResource.origin_url,
1154
db.func.length(db.func.split_part(PictureResource.origin_url, "://", 1)) + 4
1155
)).in_(value),
1156
"above_width": lambda value: PictureResource.width >= value,
1157
"below_width": lambda value: PictureResource.width <= value,
1158
"above_height": lambda value: PictureResource.height >= value,
1159
"below_height": lambda value: PictureResource.height <= value,
1160
"before_date": lambda value: PictureResource.timestamp <= datetime.utcfromtimestamp(
1161
value),
1162
"after_date": lambda value: PictureResource.timestamp >= datetime.utcfromtimestamp(
1163
value),
1164
"in_gallery": lambda value: PictureResource.galleries.any(PictureInGallery.gallery_id.in_(value)),
1165
"above_rating": lambda value: db.select(db.func.coalesce(db.func.avg(PictureRating.rating), 5)).where(PictureRating.resource_id == PictureResource.id).scalar_subquery() >= value,
1166
"below_rating": lambda value: db.select(db.func.coalesce(db.func.avg(PictureRating.rating), 0)).where(PictureRating.resource_id == PictureResource.id).scalar_subquery() <= value,
1167
"above_rating_count": lambda value: db.select(db.func.count(PictureRating.id)).where(PictureRating.resource_id == PictureResource.id).correlate(PictureResource).scalar_subquery() >= value,
1168
"below_rating_count": lambda value: db.select(db.func.count(PictureRating.id)).where(PictureRating.resource_id == PictureResource.id).correlate(PictureResource).scalar_subquery() <= value,
1169
"above_region_count": lambda value: db.select(db.func.count(PictureRegion.id)).where(PictureRegion.resource_id == PictureResource.id).correlate(PictureResource).scalar_subquery() >= value,
1170
"below_region_count": lambda value: db.select(db.func.count(PictureRegion.id)).where(PictureRegion.resource_id == PictureResource.id).correlate(PictureResource).scalar_subquery() <= value,
1171
"copied_from": lambda value: PictureResource.copied_from_id.in_(value),
1172
}
1173
1174
if "want" in query_data:
1175
for i in query_data["want"]:
1176
if len(i) != 1:
1177
raise APIError(400, "Each requirement must have exactly one key")
1178
requirement, value = list(i.items())[0]
1179
if requirement not in requirement_conditions:
1180
raise APIError(400, f"Unknown requirement type: {requirement}")
1181
1182
condition = requirement_conditions[requirement]
1183
query = query.filter(condition(value))
1184
if "exclude" in query_data:
1185
for i in query_data["exclude"]:
1186
if len(i) != 1:
1187
raise APIError(400, "Each exclusion must have exactly one key")
1188
requirement, value = list(i.items())[0]
1189
if requirement not in requirement_conditions:
1190
raise APIError(400, f"Unknown requirement type: {requirement}")
1191
1192
condition = requirement_conditions[requirement]
1193
query = query.filter(~condition(value))
1194
if not query_data.get("include_obsolete", False):
1195
query = query.filter(PictureResource.replaced_by_id.is_(None))
1196
1197
return query
1198
1199
1200
@app.route("/query-pictures")
1201
def graphical_query_pictures():
1202
return flask.render_template("graphical-query-pictures.html")
1203
1204
1205
@app.route("/query-pictures-results")
1206
def graphical_query_pictures_results():
1207
query_yaml = flask.request.args.get("query", "")
1208
yaml_parser = yaml.YAML()
1209
query_data = yaml_parser.load(query_yaml) or {}
1210
try:
1211
query = get_picture_query(query_data)
1212
except APIError as e:
1213
flask.abort(e.status_code)
1214
1215
page = int(flask.request.args.get("page", 1))
1216
per_page = int(flask.request.args.get("per_page", 16))
1217
1218
resources = query.paginate(page=page, per_page=per_page)
1219
1220
return flask.render_template("graphical-query-pictures-results.html", resources=resources,
1221
query=query_yaml,
1222
page_number=page, page_length=per_page,
1223
num_pages=resources.pages,
1224
prev_page=resources.prev_num, next_page=resources.next_num)
1225
1226
1227
@app.route("/raw/picture/<int:id>")
1228
def raw_picture(id):
1229
resource = db.session.get(PictureResource, id)
1230
if resource is None:
1231
flask.abort(404)
1232
1233
response = flask.send_from_directory(path.join(config.DATA_PATH, "pictures"),
1234
str(resource.id))
1235
response.mimetype = resource.file_format
1236
1237
return response
1238
1239
1240
@app.route("/object/")
1241
def graphical_object_types():
1242
return flask.render_template("object-types.html", objects=PictureObject.query.all())
1243
1244
1245
@app.route("/api/object-types")
1246
def object_types():
1247
objects = db.session.query(PictureObject).all()
1248
return flask.jsonify({object.id: object.description for object in objects})
1249
1250
1251
@app.route("/api/query-pictures", methods=["POST"]) # sadly GET can't have a body
1252
def query_pictures():
1253
offset = int(flask.request.args.get("offset", 0))
1254
limit = int(flask.request.args.get("limit", 16))
1255
ordering = flask.request.args.get("ordering", "date-desc")
1256
1257
yaml_parser = yaml.YAML()
1258
query_data = yaml_parser.load(flask.request.data) or {}
1259
try:
1260
query = get_picture_query(query_data)
1261
except APIError as e:
1262
return flask.jsonify({"error": e.message}), e.status_code
1263
1264
rating_count_subquery = db.select(db.func.count(PictureRating.id)).where(
1265
PictureRating.resource_id == PictureResource.id).scalar_subquery()
1266
region_count_subquery = db.select(db.func.count(PictureRegion.id)).where(
1267
PictureRegion.resource_id == PictureResource.id).scalar_subquery()
1268
rating_subquery = db.select(db.func.coalesce(db.func.avg(PictureRating.rating), 0)).where(
1269
PictureRating.resource_id == PictureResource.id).scalar_subquery()
1270
1271
match ordering:
1272
case "date-desc":
1273
query = query.order_by(PictureResource.timestamp.desc())
1274
case "date-asc":
1275
query = query.order_by(PictureResource.timestamp.asc())
1276
case "title-asc":
1277
query = query.order_by(PictureResource.title.asc())
1278
case "title-desc":
1279
query = query.order_by(PictureResource.title.desc())
1280
case "random":
1281
query = query.order_by(db.func.random())
1282
case "number-regions-desc":
1283
query = query.order_by(region_count_subquery.desc())
1284
case "number-regions-asc":
1285
query = query.order_by(region_count_subquery.asc())
1286
case "rating-desc":
1287
query = query.order_by(rating_subquery.desc())
1288
case "rating-asc":
1289
query = query.order_by(rating_subquery.asc())
1290
case "number-ratings-desc":
1291
query = query.order_by(rating_count_subquery.desc())
1292
case "number-ratings-asc":
1293
query = query.order_by(rating_count_subquery.asc())
1294
1295
query = query.offset(offset).limit(limit)
1296
resources = query.all()
1297
1298
json_response = {
1299
"date_generated": datetime.utcnow().timestamp(),
1300
"resources": [],
1301
"offset": offset,
1302
"limit": limit,
1303
}
1304
1305
json_resources = json_response["resources"]
1306
1307
for resource in resources:
1308
json_resource = {
1309
"id": resource.id,
1310
"title": resource.title,
1311
"description": resource.description,
1312
"timestamp": resource.timestamp.timestamp(),
1313
"origin_url": resource.origin_url,
1314
"author": resource.author_name,
1315
"file_format": resource.file_format,
1316
"width": resource.width,
1317
"height": resource.height,
1318
"nature": resource.nature_id,
1319
"licences": [licence.licence_id for licence in resource.licences],
1320
"replaces": resource.replaces_id,
1321
"replaced_by": resource.replaced_by_id,
1322
"regions": [],
1323
"download": config.ROOT_URL + flask.url_for("raw_picture", id=resource.id),
1324
}
1325
for region in resource.regions:
1326
json_resource["regions"].append({
1327
"object": region.object_id,
1328
"type": region.json["type"],
1329
"shape": region.json["shape"],
1330
})
1331
1332
json_resources.append(json_resource)
1333
1334
return flask.jsonify(json_response)
1335
1336
1337
@app.route("/api/picture/<int:id>/")
1338
def api_picture(id):
1339
resource = db.session.get(PictureResource, id)
1340
if resource is None:
1341
flask.abort(404)
1342
1343
json_resource = {
1344
"id": resource.id,
1345
"title": resource.title,
1346
"description": resource.description,
1347
"timestamp": resource.timestamp.timestamp(),
1348
"origin_url": resource.origin_url,
1349
"author": resource.author_name,
1350
"file_format": resource.file_format,
1351
"width": resource.width,
1352
"height": resource.height,
1353
"nature": resource.nature_id,
1354
"licences": [licence.licence_id for licence in resource.licences],
1355
"replaces": resource.replaces_id,
1356
"replaced_by": resource.replaced_by_id,
1357
"regions": [],
1358
"download": config.ROOT_URL + flask.url_for("raw_picture", id=resource.id),
1359
"rating_average": resource.average_rating,
1360
"rating_count": resource.rating_totals,
1361
}
1362
for region in resource.regions:
1363
json_resource["regions"].append({
1364
"object": region.object_id,
1365
"type": region.json["type"],
1366
"shape": region.json["shape"],
1367
})
1368
1369
return flask.jsonify(json_resource)
1370
1371
1372
@app.route("/api/licence/")
1373
def api_licences():
1374
licences = db.session.query(Licence).all()
1375
json_licences = {
1376
licence.id: {
1377
"title": licence.title,
1378
"free": licence.free,
1379
"pinned": licence.pinned,
1380
} for licence in licences
1381
}
1382
1383
return flask.jsonify(json_licences)
1384
1385
1386
@app.route("/api/licence/<id>/")
1387
def api_licence(id):
1388
licence = db.session.get(Licence, id)
1389
if licence is None:
1390
flask.abort(404)
1391
1392
json_licence = {
1393
"id": licence.id,
1394
"title": licence.title,
1395
"description": licence.description,
1396
"info_url": licence.info_url,
1397
"legalese_url": licence.url,
1398
"free": licence.free,
1399
"logo_url": licence.logo_url,
1400
"pinned": licence.pinned,
1401
}
1402
1403
return flask.jsonify(json_licence)
1404
1405
1406
@app.route("/api/nature/")
1407
def api_natures():
1408
natures = db.session.query(PictureNature).all()
1409
json_natures = {
1410
nature.id: nature.description for nature in natures
1411
}
1412
1413
return flask.jsonify(json_natures)
1414
1415
1416
@app.route("/api/user/")
1417
def api_users():
1418
offset = int(flask.request.args.get("offset", 0))
1419
limit = int(flask.request.args.get("limit", 16))
1420
1421
users = db.session.query(User).offset(offset).limit(limit).all()
1422
1423
json_users = {
1424
user.username: {
1425
"admin": user.admin,
1426
} for user in users
1427
}
1428
1429
return flask.jsonify(json_users)
1430
1431
1432
@app.route("/api/user/<username>/")
1433
def api_user(username):
1434
user = db.session.get(User, username)
1435
if user is None:
1436
flask.abort(404)
1437
1438
json_user = {
1439
"username": user.username,
1440
"admin": user.admin,
1441
"joined": user.joined_timestamp.timestamp(),
1442
}
1443
1444
return flask.jsonify(json_user)
1445
1446
1447
@app.route("/api/login", methods=["POST"])
1448
def api_login():
1449
username = flask.request.json["username"]
1450
password = flask.request.json["password"]
1451
1452
user = db.session.get(User, username)
1453
1454
if user is None:
1455
return flask.jsonify({"error": "This username is not registered. To prevent spam, you must use the HTML interface to register."}), 401
1456
1457
if not bcrypt.check_password_hash(user.password_hashed, password):
1458
return flask.jsonify({"error": "Incorrect password"}), 401
1459
1460
flask.session["username"] = username
1461
1462
return flask.jsonify({"message": "You have been logged in. Your HTTP client must support cookies to use features of this API that require authentication."})
1463
1464
1465
@app.route("/api/logout", methods=["POST"])
1466
def api_logout():
1467
flask.session.pop("username", None)
1468
return flask.jsonify({"message": "You have been logged out."})
1469
1470
1471
@app.route("/api/upload", methods=["POST"])
1472
def api_upload():
1473
if "username" not in flask.session:
1474
return flask.jsonify({"error": "You must be logged in to upload pictures"}), 401
1475
1476
json_ = json.loads(flask.request.form["json"])
1477
title = json_["title"]
1478
description = json_.get("description", "")
1479
origin_url = json_.get("origin_url", "")
1480
author = db.session.get(User, flask.session["username"])
1481
licence_ids = json_["licence"]
1482
nature_id = json_["nature"]
1483
file = flask.request.files["file"]
1484
1485
if not file or not file.filename:
1486
return flask.jsonify({"error": "An image file must be uploaded"}), 400
1487
1488
if not file.mimetype.startswith("image/") or file.mimetype == "image/svg+xml":
1489
return flask.jsonify({"error": "Only bitmap images are supported"}), 400
1490
1491
if not title:
1492
return flask.jsonify({"error": "Give a title"}), 400
1493
1494
if not description:
1495
description = ""
1496
1497
if not nature_id:
1498
return flask.jsonify({"error": "Give a picture type"}), 400
1499
1500
if not licence_ids:
1501
return flask.jsonify({"error": "Give licences"}), 400
1502
1503
licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]
1504
if not any(licence.free for licence in licences):
1505
return flask.jsonify({"error": "Use at least one free licence"}), 400
1506
1507
resource = PictureResource(title, author, description, origin_url, licence_ids,
1508
file.mimetype,
1509
db.session.get(PictureNature, nature_id))
1510
db.session.add(resource)
1511
db.session.commit()
1512
file.save(path.join(config.DATA_PATH, "pictures", str(resource.id)))
1513
pil_image = Image.open(path.join(config.DATA_PATH, "pictures", str(resource.id)))
1514
resource.width, resource.height = pil_image.size
1515
db.session.commit()
1516
1517
if json_.get("annotations"):
1518
try:
1519
resource.put_annotations(json_["annotations"])
1520
db.session.commit()
1521
except json.JSONDecodeError:
1522
return flask.jsonify({"error": "Invalid annotations"}), 400
1523
1524
return flask.jsonify({"message": "Picture uploaded successfully", "id": resource.id})
1525
1526
1527
@app.route("/api/picture/<int:id>/update", methods=["POST"])
1528
def api_update_picture(id):
1529
resource = db.session.get(PictureResource, id)
1530
if resource is None:
1531
return flask.jsonify({"error": "Picture not found"}), 404
1532
current_user = db.session.get(User, flask.session.get("username"))
1533
if current_user is None:
1534
return flask.jsonify({"error": "You must be logged in to edit pictures"}), 401
1535
if resource.author != current_user and not current_user.admin:
1536
return flask.jsonify({"error": "You are not the author of this picture"}), 403
1537
1538
title = flask.request.json.get("title", resource.title)
1539
description = flask.request.json.get("description", resource.description)
1540
origin_url = flask.request.json.get("origin_url", resource.origin_url)
1541
licence_ids = flask.request.json.get("licence", [licence.licence_id for licence in resource.licences])
1542
nature_id = flask.request.json.get("nature", resource.nature_id)
1543
1544
if not title:
1545
return flask.jsonify({"error": "Give a title"}), 400
1546
1547
if not description:
1548
description = ""
1549
1550
if not nature_id:
1551
return flask.jsonify({"error": "Give a picture type"}), 400
1552
1553
if not licence_ids:
1554
return flask.jsonify({"error": "Give licences"}), 400
1555
1556
licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]
1557
1558
if not any(licence.free for licence in licences):
1559
return flask.jsonify({"error": "Use at least one free licence"}), 400
1560
1561
resource.title = title
1562
resource.description = description
1563
resource.origin_url = origin_url
1564
resource.licences = licences
1565
resource.nature = db.session.get(PictureNature, nature_id)
1566
1567
db.session.commit()
1568
1569
return flask.jsonify({"message": "Picture updated successfully"})
1570
1571
1572
@app.route("/api/picture/<int:id>/rate", methods=["POST"])
1573
def api_rate_picture(id):
1574
resource = db.session.get(PictureResource, id)
1575
if resource is None:
1576
flask.abort(404)
1577
1578
current_user = db.session.get(User, flask.session.get("username"))
1579
if current_user is None:
1580
flask.abort(401)
1581
1582
rating = int(flask.request.json.get("rating"))
1583
1584
if not rating:
1585
# Delete the existing rating
1586
if PictureRating.query.filter_by(resource=resource, user=current_user).first():
1587
db.session.delete(PictureRating.query.filter_by(resource=resource,
1588
user=current_user).first())
1589
db.session.commit()
1590
1591
return flask.jsonify({"message": "Existing rating removed"})
1592
1593
if not 1 <= rating <= 5:
1594
flask.flash("Invalid rating")
1595
return flask.jsonify({"error": "Invalid rating"}), 400
1596
1597
if PictureRating.query.filter_by(resource=resource, user=current_user).first():
1598
PictureRating.query.filter_by(resource=resource, user=current_user).first().rating = rating
1599
else:
1600
# Create a new rating
1601
db.session.add(PictureRating(resource, current_user, rating))
1602
1603
db.session.commit()
1604
1605
return flask.jsonify({"message": "Rating saved"})
1606
1607
1608
@app.route("/api/gallery/<int:id>/")
1609
def api_gallery(id):
1610
gallery = db.session.get(Gallery, id)
1611
if gallery is None:
1612
flask.abort(404)
1613
1614
json_gallery = {
1615
"id": gallery.id,
1616
"title": gallery.title,
1617
"description": gallery.description,
1618
"owner": gallery.owner_name,
1619
"users": [user.username for user in gallery.users],
1620
}
1621
1622
return flask.jsonify(json_gallery)
1623
1624
1625
@app.route("/api/gallery/<int:id>/edit", methods=["POST"])
1626
def api_edit_gallery(id):
1627
gallery = db.session.get(Gallery, id)
1628
if gallery is None:
1629
flask.abort(404)
1630
1631
current_user = db.session.get(User, flask.session.get("username"))
1632
if current_user is None:
1633
flask.abort(401)
1634
1635
if current_user != gallery.owner and not current_user.admin:
1636
flask.abort(403)
1637
1638
title = flask.request.json.get("title", gallery.title)
1639
description = flask.request.json.get("description", gallery.description)
1640
1641
if not title:
1642
return flask.jsonify({"error": "Give a title"}), 400
1643
1644
if not description:
1645
description = ""
1646
1647
gallery.title = title
1648
gallery.description = description
1649
1650
db.session.commit()
1651
1652
return flask.jsonify({"message": "Gallery updated successfully"})
1653
1654
1655
@app.route("/api/new-gallery", methods=["POST"])
1656
def api_new_gallery():
1657
if "username" not in flask.session:
1658
return flask.jsonify({"error": "You must be logged in to create galleries"}), 401
1659
1660
title = flask.request.json.get("title")
1661
description = flask.request.json.get("description", "")
1662
1663
if not title:
1664
return flask.jsonify({"error": "Give a title"}), 400
1665
1666
gallery = Gallery(title, description, db.session.get(User, flask.session["username"]))
1667
db.session.add(gallery)
1668
db.session.commit()
1669
1670
return flask.jsonify({"message": "Gallery created successfully", "id": gallery.id})
1671
1672
1673
@app.route("/api/gallery/<int:id>/add-picture", methods=["POST"])
1674
def api_gallery_add_picture(id):
1675
gallery = db.session.get(Gallery, id)
1676
if gallery is None:
1677
flask.abort(404)
1678
1679
if "username" not in flask.session:
1680
return flask.jsonify({"error": "You must be logged in to add pictures to galleries"}), 401
1681
1682
current_user = db.session.get(User, flask.session.get("username"))
1683
1684
if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
1685
return flask.jsonify({"error": "You do not have permission to add pictures to this gallery"}), 403
1686
1687
picture_id = flask.request.json.get("picture_id")
1688
1689
try:
1690
picture_id = int(picture_id)
1691
except ValueError:
1692
return flask.jsonify({"error": "Invalid picture ID"}), 400
1693
1694
picture = db.session.get(PictureResource, picture_id)
1695
if picture is None:
1696
return flask.jsonify({"error": "The picture doesn't exist"}), 404
1697
1698
if PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first():
1699
return flask.jsonify({"error": "This picture is already in the gallery"}), 400
1700
1701
db.session.add(PictureInGallery(picture, gallery))
1702
1703
db.session.commit()
1704
1705
return flask.jsonify({"message": "Picture added to gallery"})
1706
1707
1708
@app.route("/api/gallery/<int:id>/remove-picture", methods=["POST"])
1709
def api_gallery_remove_picture(id):
1710
gallery = db.session.get(Gallery, id)
1711
if gallery is None:
1712
flask.abort(404)
1713
1714
if "username" not in flask.session:
1715
return flask.jsonify({"error": "You must be logged in to remove pictures from galleries"}), 401
1716
1717
current_user = db.session.get(User, flask.session.get("username"))
1718
1719
if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
1720
return flask.jsonify({"error": "You do not have permission to remove pictures from this gallery"}), 403
1721
1722
picture_id = flask.request.json.get("picture_id")
1723
1724
try:
1725
picture_id = int(picture_id)
1726
except ValueError:
1727
return flask.jsonify({"error": "Invalid picture ID"}), 400
1728
1729
picture = db.session.get(PictureResource, picture_id)
1730
if picture is None:
1731
return flask.jsonify({"error": "The picture doesn't exist"}), 404
1732
1733
picture_in_gallery = PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first()
1734
if picture_in_gallery is None:
1735
return flask.jsonify({"error": "This picture isn't in the gallery"}), 400
1736
1737
db.session.delete(picture_in_gallery)
1738
1739
db.session.commit()
1740
1741
return flask.jsonify({"message": "Picture removed from gallery"})
1742
1743
1744
@app.route("/api/gallery/<int:id>/users/add", methods=["POST"])
1745
def api_gallery_add_user(id):
1746
gallery = db.session.get(Gallery, id)
1747
if gallery is None:
1748
flask.abort(404)
1749
1750
current_user = db.session.get(User, flask.session.get("username"))
1751
if current_user is None:
1752
flask.abort(401)
1753
1754
if current_user != gallery.owner and not current_user.admin:
1755
flask.abort(403)
1756
1757
username = flask.request.json.get("username")
1758
if username == gallery.owner_name:
1759
return flask.jsonify({"error": "The owner cannot be added to trusted users"}), 400
1760
1761
user = db.session.get(User, username)
1762
if user is None:
1763
return flask.jsonify({"error": "User not found"}), 404
1764
1765
if UserInGallery.query.filter_by(user=user, gallery=gallery).first():
1766
return flask.jsonify({"error": "User is already in the gallery"}), 400
1767
1768
db.session.add(UserInGallery(user, gallery))
1769
1770
db.session.commit()
1771
1772
return flask.jsonify({"message": "User added to gallery"})
1773
1774
1775
@app.route("/api/gallery/<int:id>/users/remove", methods=["POST"])
1776
def api_gallery_remove_user(id):
1777
gallery = db.session.get(Gallery, id)
1778
if gallery is None:
1779
flask.abort(404)
1780
1781
current_user = db.session.get(User, flask.session.get("username"))
1782
if current_user is None:
1783
flask.abort(401)
1784
1785
if current_user != gallery.owner and not current_user.admin:
1786
flask.abort(403)
1787
1788
username = flask.request.json.get("username")
1789
user = db.session.get(User, username)
1790
if user is None:
1791
return flask.jsonify({"error": "User not found"}), 404
1792
1793
user_in_gallery = UserInGallery.query.filter_by(user=user, gallery=gallery).first()
1794
if user_in_gallery is None:
1795
return flask.jsonify({"error": "User is not in the gallery"}), 400
1796
1797
db.session.delete(user_in_gallery)
1798
1799
db.session.commit()
1800
1801
return flask.jsonify({"message": "User removed from gallery"})
1802
1803