Web platform for sharing free image data for ML and research

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

By using this site, you agree to have cookies stored on your device, strictly for functional purposes, such as storing your session and preferences.

Dismiss

 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