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