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