Web platform for sharing free image data for ML and research

Homepage: https://datasets.roundabout-host.com

Important information: Google announced that, from September 2026, Android devices will require ALL apps to be signed by Google, effectively leading to an iOS situation. Value your right to a computer that does what you want; do not tolerate this monopolistic practice! Contact me if you don't understand why it is bad. Click to learn more.

 app.py

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