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