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 • 68.48 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("/gallery/<int:id>/")
908
def gallery(id):
909
gallery = db.session.get(Gallery, id)
910
if gallery is None:
911
flask.abort(404)
912
913
current_user = db.session.get(User, flask.session.get("username"))
914
915
have_permission = current_user and (current_user == gallery.owner or current_user.admin or UserInGallery.query.filter_by(user=current_user, gallery=gallery).first())
916
have_extended_permission = current_user and (current_user == gallery.owner or current_user.admin)
917
918
return flask.render_template("gallery.html", gallery=gallery,
919
have_permission=have_permission,
920
have_extended_permission=have_extended_permission)
921
922
923
@app.route("/create-gallery")
924
def create_gallery():
925
if "username" not in flask.session:
926
flask.flash("Log in to create galleries.")
927
return flask.redirect("/accounts")
928
929
return flask.render_template("create-gallery.html")
930
931
932
@app.route("/create-gallery", methods=["POST"])
933
def create_gallery_post():
934
if not flask.session.get("username"):
935
flask.abort(401)
936
937
if not flask.request.form.get("title"):
938
flask.flash("Enter a title")
939
return flask.redirect(flask.request.url)
940
941
description = flask.request.form.get("description", "")
942
943
gallery = Gallery(flask.request.form["title"], description,
944
db.session.get(User, flask.session["username"]))
945
db.session.add(gallery)
946
db.session.commit()
947
948
return flask.redirect("/gallery/" + str(gallery.id))
949
950
951
@app.route("/gallery/<int:id>/add-picture", methods=["POST"])
952
def gallery_add_picture(id):
953
gallery = db.session.get(Gallery, id)
954
if gallery is None:
955
flask.abort(404)
956
957
if "username" not in flask.session:
958
flask.abort(401)
959
960
if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
961
flask.abort(403)
962
963
picture_id = flask.request.form.get("picture_id")
964
if "/" in picture_id: # also allow full URLs
965
picture_id = picture_id.rstrip("/").rpartition("/")[1]
966
if not picture_id:
967
flask.flash("Select a picture")
968
return flask.redirect("/gallery/" + str(gallery.id))
969
picture_id = int(picture_id)
970
971
picture = db.session.get(PictureResource, picture_id)
972
if picture is None:
973
flask.flash("Invalid picture")
974
return flask.redirect("/gallery/" + str(gallery.id))
975
976
if PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first():
977
flask.flash("This picture is already in the gallery")
978
return flask.redirect("/gallery/" + str(gallery.id))
979
980
db.session.add(PictureInGallery(picture, gallery))
981
982
db.session.commit()
983
984
return flask.redirect("/gallery/" + str(gallery.id))
985
986
987
@app.route("/gallery/<int:id>/remove-picture", methods=["POST"])
988
def gallery_remove_picture(id):
989
gallery = db.session.get(Gallery, id)
990
if gallery is None:
991
flask.abort(404)
992
993
if "username" not in flask.session:
994
flask.abort(401)
995
996
current_user = db.session.get(User, flask.session.get("username"))
997
998
if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
999
flask.abort(403)
1000
1001
picture_id = int(flask.request.form.get("picture_id"))
1002
1003
picture = db.session.get(PictureResource, picture_id)
1004
if picture is None:
1005
flask.flash("Invalid picture")
1006
return flask.redirect("/gallery/" + str(gallery.id))
1007
1008
picture_in_gallery = PictureInGallery.query.filter_by(resource=picture,
1009
gallery=gallery).first()
1010
if picture_in_gallery is None:
1011
flask.flash("This picture isn't in the gallery")
1012
return flask.redirect("/gallery/" + str(gallery.id))
1013
1014
db.session.delete(picture_in_gallery)
1015
1016
db.session.commit()
1017
1018
return flask.redirect("/gallery/" + str(gallery.id))
1019
1020
1021
@app.route("/gallery/<int:id>/add-pictures-from-query", methods=["POST"])
1022
def gallery_add_from_query(id):
1023
gallery = db.session.get(Gallery, id)
1024
if gallery is None:
1025
flask.abort(404)
1026
1027
if "username" not in flask.session:
1028
flask.abort(401)
1029
1030
if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
1031
flask.abort(403)
1032
1033
query_yaml = flask.request.form.get("query", "")
1034
1035
yaml_parser = yaml.YAML()
1036
query_data = yaml_parser.load(query_yaml) or {}
1037
query = get_picture_query(query_data)
1038
1039
pictures = query.all()
1040
1041
count = 0
1042
1043
for picture in pictures:
1044
if not PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first():
1045
db.session.add(PictureInGallery(picture, gallery))
1046
count += 1
1047
1048
db.session.commit()
1049
1050
flask.flash(f"Added {count} pictures to the gallery")
1051
1052
return flask.redirect("/gallery/" + str(gallery.id))
1053
1054
1055
@app.route("/gallery/<int:id>/users")
1056
def gallery_users(id):
1057
gallery = db.session.get(Gallery, id)
1058
if gallery is None:
1059
flask.abort(404)
1060
1061
current_user = db.session.get(User, flask.session.get("username"))
1062
have_permission = current_user and (current_user == gallery.owner or current_user.admin)
1063
1064
return flask.render_template("gallery-users.html", gallery=gallery,
1065
have_permission=have_permission)
1066
1067
1068
@app.route("/gallery/<int:id>/edit")
1069
def edit_gallery(id):
1070
gallery = db.session.get(Gallery, id)
1071
if gallery is None:
1072
flask.abort(404)
1073
1074
current_user = db.session.get(User, flask.session.get("username"))
1075
if current_user is None:
1076
flask.abort(401)
1077
1078
if current_user != gallery.owner and not current_user.admin:
1079
flask.abort(403)
1080
1081
return flask.render_template("edit-gallery.html", gallery=gallery)
1082
1083
1084
@app.route("/gallery/<int:id>/edit", methods=["POST"])
1085
def edit_gallery_post(id):
1086
gallery = db.session.get(Gallery, id)
1087
if gallery is None:
1088
flask.abort(404)
1089
1090
current_user = db.session.get(User, flask.session.get("username"))
1091
if current_user is None:
1092
flask.abort(401)
1093
1094
if current_user != gallery.owner and not current_user.admin:
1095
flask.abort(403)
1096
1097
title = flask.request.form["title"]
1098
description = flask.request.form.get("description")
1099
1100
if not title:
1101
flask.flash("Enter a title")
1102
return flask.redirect(flask.request.url)
1103
1104
if not description:
1105
description = ""
1106
1107
gallery.title = title
1108
gallery.description = description
1109
1110
db.session.commit()
1111
1112
return flask.redirect("/gallery/" + str(gallery.id))
1113
1114
1115
@app.route("/gallery/<int:id>/delete")
1116
def delete_gallery(id):
1117
gallery = db.session.get(Gallery, id)
1118
if gallery is None:
1119
flask.abort(404)
1120
1121
current_user = db.session.get(User, flask.session.get("username"))
1122
if current_user is None:
1123
flask.abort(401)
1124
1125
if current_user != gallery.owner and not current_user.admin:
1126
flask.abort(403)
1127
1128
PictureInGallery.query.filter_by(gallery=gallery).delete()
1129
UserInGallery.query.filter_by(gallery=gallery).delete()
1130
db.session.delete(gallery)
1131
db.session.commit()
1132
1133
return flask.redirect("/")
1134
1135
1136
@app.route("/gallery/<int:id>/users/add", methods=["POST"])
1137
def gallery_add_user(id):
1138
gallery = db.session.get(Gallery, id)
1139
if gallery is None:
1140
flask.abort(404)
1141
1142
current_user = db.session.get(User, flask.session.get("username"))
1143
if current_user is None:
1144
flask.abort(401)
1145
1146
if current_user != gallery.owner and not current_user.admin:
1147
flask.abort(403)
1148
1149
username = flask.request.form.get("username")
1150
if username == gallery.owner_name:
1151
flask.flash("The owner is already in the gallery")
1152
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1153
1154
user = db.session.get(User, username)
1155
if user is None:
1156
flask.flash("User not found")
1157
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1158
1159
if UserInGallery.query.filter_by(user=user, gallery=gallery).first():
1160
flask.flash("User is already in the gallery")
1161
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1162
1163
db.session.add(UserInGallery(user, gallery))
1164
1165
db.session.commit()
1166
1167
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1168
1169
1170
@app.route("/gallery/<int:id>/users/remove", methods=["POST"])
1171
def gallery_remove_user(id):
1172
gallery = db.session.get(Gallery, id)
1173
if gallery is None:
1174
flask.abort(404)
1175
1176
current_user = db.session.get(User, flask.session.get("username"))
1177
if current_user is None:
1178
flask.abort(401)
1179
1180
if current_user != gallery.owner and not current_user.admin:
1181
flask.abort(403)
1182
1183
username = flask.request.form.get("username")
1184
user = db.session.get(User, username)
1185
if user is None:
1186
flask.flash("User not found")
1187
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1188
1189
user_in_gallery = UserInGallery.query.filter_by(user=user, gallery=gallery).first()
1190
if user_in_gallery is None:
1191
flask.flash("User is not in the gallery")
1192
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1193
1194
db.session.delete(user_in_gallery)
1195
1196
db.session.commit()
1197
1198
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1199
1200
1201
class APIError(Exception):
1202
def __init__(self, status_code, message):
1203
self.status_code = status_code
1204
self.message = message
1205
1206
1207
def get_picture_query(query_data):
1208
query = db.session.query(PictureResource)
1209
1210
def has_condition(id):
1211
descendants_cte = (
1212
db.select(PictureObject.id)
1213
.where(PictureObject.id == id)
1214
.cte(name=f"descendants_cte_{id}", recursive=True)
1215
)
1216
1217
descendants_cte = descendants_cte.union_all(
1218
db.select(PictureObjectInheritance.child_id)
1219
.where(PictureObjectInheritance.parent_id == descendants_cte.c.id)
1220
)
1221
1222
return PictureResource.regions.any(
1223
PictureRegion.object_id.in_(
1224
db.select(descendants_cte.c.id)
1225
)
1226
)
1227
1228
requirement_conditions = {
1229
# Has an object with the ID in the given list
1230
"has_object": lambda value: PictureResource.regions.any(
1231
PictureRegion.object_id.in_(value)),
1232
# Has an object with the ID in the given list, or a subtype of it
1233
"has": lambda value: db.or_(*[has_condition(id) for id in value]),
1234
"nature": lambda value: PictureResource.nature_id.in_(value),
1235
"licence": lambda value: PictureResource.licences.any(
1236
PictureLicence.licence_id.in_(value)),
1237
"author": lambda value: PictureResource.author_name.in_(value),
1238
"title": lambda value: PictureResource.title.ilike(value),
1239
"description": lambda value: PictureResource.description.ilike(value),
1240
"origin_url": lambda value: db.func.lower(db.func.substr(
1241
PictureResource.origin_url,
1242
db.func.length(db.func.split_part(PictureResource.origin_url, "://", 1)) + 4
1243
)).in_(value),
1244
"above_width": lambda value: PictureResource.width >= value,
1245
"below_width": lambda value: PictureResource.width <= value,
1246
"above_height": lambda value: PictureResource.height >= value,
1247
"below_height": lambda value: PictureResource.height <= value,
1248
"before_date": lambda value: PictureResource.timestamp <= datetime.utcfromtimestamp(
1249
value),
1250
"after_date": lambda value: PictureResource.timestamp >= datetime.utcfromtimestamp(
1251
value),
1252
"in_gallery": lambda value: PictureResource.galleries.any(PictureInGallery.gallery_id.in_(value)),
1253
"above_rating": lambda value: db.select(db.func.coalesce(db.func.avg(PictureRating.rating), 5)).where(PictureRating.resource_id == PictureResource.id).scalar_subquery() >= value,
1254
"below_rating": lambda value: db.select(db.func.coalesce(db.func.avg(PictureRating.rating), 0)).where(PictureRating.resource_id == PictureResource.id).scalar_subquery() <= value,
1255
"above_rating_count": lambda value: db.select(db.func.count(PictureRating.id)).where(PictureRating.resource_id == PictureResource.id).correlate(PictureResource).scalar_subquery() >= value,
1256
"below_rating_count": lambda value: db.select(db.func.count(PictureRating.id)).where(PictureRating.resource_id == PictureResource.id).correlate(PictureResource).scalar_subquery() <= value,
1257
"above_region_count": lambda value: db.select(db.func.count(PictureRegion.id)).where(PictureRegion.resource_id == PictureResource.id).correlate(PictureResource).scalar_subquery() >= value,
1258
"below_region_count": lambda value: db.select(db.func.count(PictureRegion.id)).where(PictureRegion.resource_id == PictureResource.id).correlate(PictureResource).scalar_subquery() <= value,
1259
"copied_from": lambda value: PictureResource.copied_from_id.in_(value),
1260
}
1261
1262
if "want" in query_data:
1263
for i in query_data["want"]:
1264
if len(i) != 1:
1265
raise APIError(400, "Each requirement 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 "exclude" in query_data:
1273
for i in query_data["exclude"]:
1274
if len(i) != 1:
1275
raise APIError(400, "Each exclusion must have exactly one key")
1276
requirement, value = list(i.items())[0]
1277
if requirement not in requirement_conditions:
1278
raise APIError(400, f"Unknown requirement type: {requirement}")
1279
1280
condition = requirement_conditions[requirement]
1281
query = query.filter(~condition(value))
1282
if not query_data.get("include_obsolete", False):
1283
query = query.filter(PictureResource.replaced_by_id.is_(None))
1284
1285
return query
1286
1287
1288
@app.route("/query-pictures")
1289
def graphical_query_pictures():
1290
return flask.render_template("graphical-query-pictures.html")
1291
1292
1293
@app.route("/query-pictures-results")
1294
def graphical_query_pictures_results():
1295
query_yaml = flask.request.args.get("query", "")
1296
yaml_parser = yaml.YAML()
1297
query_data = yaml_parser.load(query_yaml) or {}
1298
try:
1299
query = get_picture_query(query_data)
1300
except APIError as e:
1301
flask.abort(e.status_code)
1302
1303
page = int(flask.request.args.get("page", 1))
1304
per_page = int(flask.request.args.get("per_page", 16))
1305
1306
resources = query.paginate(page=page, per_page=per_page)
1307
1308
return flask.render_template("graphical-query-pictures-results.html", resources=resources,
1309
query=query_yaml,
1310
page_number=page, page_length=per_page,
1311
num_pages=resources.pages,
1312
prev_page=resources.prev_num, next_page=resources.next_num)
1313
1314
1315
@app.route("/raw/picture/<int:id>")
1316
def raw_picture(id):
1317
resource = db.session.get(PictureResource, id)
1318
if resource is None:
1319
flask.abort(404)
1320
1321
response = flask.send_from_directory(path.join(config.DATA_PATH, "pictures"),
1322
str(resource.id))
1323
response.mimetype = resource.file_format
1324
1325
return response
1326
1327
1328
@app.route("/raw/picture-thumbnail/<int:id>")
1329
def raw_picture_thumbnail(id):
1330
resource = db.session.get(PictureResource, id)
1331
if resource is None:
1332
flask.abort(404)
1333
1334
if not path.exists(path.join(config.DATA_PATH, "picture-thumbnails", str(resource.id))):
1335
pil_image = Image.open(path.join(config.DATA_PATH, "pictures", str(resource.id)))
1336
pil_image = make_thumbnail(pil_image)
1337
pil_image.save(path.join(config.DATA_PATH, "picture-thumbnails", str(resource.id)), **config.THUMBNAIL_SAVE_OPTIONS)
1338
1339
response = flask.send_from_directory(path.join(config.DATA_PATH, "picture-thumbnails"),
1340
str(resource.id))
1341
response.mimetype = resource.file_format
1342
1343
return response
1344
1345
1346
@app.route("/object/")
1347
def graphical_object_types():
1348
return flask.render_template("object-types.html", objects=PictureObject.query.all())
1349
1350
1351
@app.route("/api/object-types")
1352
def object_types():
1353
objects = db.session.query(PictureObject).all()
1354
return flask.jsonify({object.id: object.description for object in objects})
1355
1356
1357
@app.route("/api/query-pictures", methods=["POST"]) # sadly GET can't have a body
1358
def query_pictures():
1359
offset = int(flask.request.args.get("offset", 0))
1360
limit = int(flask.request.args.get("limit", 16))
1361
ordering = flask.request.args.get("ordering", "date-desc")
1362
1363
yaml_parser = yaml.YAML()
1364
query_data = yaml_parser.load(flask.request.data) or {}
1365
try:
1366
query = get_picture_query(query_data)
1367
except APIError as e:
1368
return flask.jsonify({"error": e.message}), e.status_code
1369
1370
rating_count_subquery = db.select(db.func.count(PictureRating.id)).where(
1371
PictureRating.resource_id == PictureResource.id).scalar_subquery()
1372
region_count_subquery = db.select(db.func.count(PictureRegion.id)).where(
1373
PictureRegion.resource_id == PictureResource.id).scalar_subquery()
1374
rating_subquery = db.select(db.func.coalesce(db.func.avg(PictureRating.rating), 0)).where(
1375
PictureRating.resource_id == PictureResource.id).scalar_subquery()
1376
1377
match ordering:
1378
case "date-desc":
1379
query = query.order_by(PictureResource.timestamp.desc())
1380
case "date-asc":
1381
query = query.order_by(PictureResource.timestamp.asc())
1382
case "title-asc":
1383
query = query.order_by(PictureResource.title.asc())
1384
case "title-desc":
1385
query = query.order_by(PictureResource.title.desc())
1386
case "random":
1387
query = query.order_by(db.func.random())
1388
case "number-regions-desc":
1389
query = query.order_by(region_count_subquery.desc())
1390
case "number-regions-asc":
1391
query = query.order_by(region_count_subquery.asc())
1392
case "rating-desc":
1393
query = query.order_by(rating_subquery.desc())
1394
case "rating-asc":
1395
query = query.order_by(rating_subquery.asc())
1396
case "number-ratings-desc":
1397
query = query.order_by(rating_count_subquery.desc())
1398
case "number-ratings-asc":
1399
query = query.order_by(rating_count_subquery.asc())
1400
1401
query = query.offset(offset).limit(limit)
1402
resources = query.all()
1403
1404
json_response = {
1405
"date_generated": datetime.utcnow().timestamp(),
1406
"resources": [],
1407
"offset": offset,
1408
"limit": limit,
1409
}
1410
1411
json_resources = json_response["resources"]
1412
1413
for resource in resources:
1414
json_resource = {
1415
"id": resource.id,
1416
"title": resource.title,
1417
"description": resource.description,
1418
"timestamp": resource.timestamp.timestamp(),
1419
"origin_url": resource.origin_url,
1420
"author": resource.author_name,
1421
"file_format": resource.file_format,
1422
"width": resource.width,
1423
"height": resource.height,
1424
"nature": resource.nature_id,
1425
"licences": [licence.licence_id for licence in resource.licences],
1426
"replaces": resource.replaces_id,
1427
"replaced_by": resource.replaced_by_id,
1428
"regions": [],
1429
"download": config.ROOT_URL + flask.url_for("raw_picture", id=resource.id),
1430
}
1431
for region in resource.regions:
1432
json_resource["regions"].append({
1433
"object": region.object_id,
1434
"type": region.json["type"],
1435
"shape": region.json["shape"],
1436
})
1437
1438
json_resources.append(json_resource)
1439
1440
return flask.jsonify(json_response)
1441
1442
1443
@app.route("/api/picture/<int:id>/")
1444
def api_picture(id):
1445
resource = db.session.get(PictureResource, id)
1446
if resource is None:
1447
flask.abort(404)
1448
1449
json_resource = {
1450
"id": resource.id,
1451
"title": resource.title,
1452
"description": resource.description,
1453
"timestamp": resource.timestamp.timestamp(),
1454
"origin_url": resource.origin_url,
1455
"author": resource.author_name,
1456
"file_format": resource.file_format,
1457
"width": resource.width,
1458
"height": resource.height,
1459
"nature": resource.nature_id,
1460
"licences": [licence.licence_id for licence in resource.licences],
1461
"replaces": resource.replaces_id,
1462
"replaced_by": resource.replaced_by_id,
1463
"regions": [],
1464
"download": config.ROOT_URL + flask.url_for("raw_picture", id=resource.id),
1465
"rating_average": resource.average_rating,
1466
"rating_count": resource.rating_totals,
1467
}
1468
for region in resource.regions:
1469
json_resource["regions"].append({
1470
"object": region.object_id,
1471
"type": region.json["type"],
1472
"shape": region.json["shape"],
1473
})
1474
1475
return flask.jsonify(json_resource)
1476
1477
1478
@app.route("/api/licence/")
1479
def api_licences():
1480
licences = db.session.query(Licence).all()
1481
json_licences = {
1482
licence.id: {
1483
"title": licence.title,
1484
"free": licence.free,
1485
"pinned": licence.pinned,
1486
} for licence in licences
1487
}
1488
1489
return flask.jsonify(json_licences)
1490
1491
1492
@app.route("/api/licence/<id>/")
1493
def api_licence(id):
1494
licence = db.session.get(Licence, id)
1495
if licence is None:
1496
flask.abort(404)
1497
1498
json_licence = {
1499
"id": licence.id,
1500
"title": licence.title,
1501
"description": licence.description,
1502
"info_url": licence.info_url,
1503
"legalese_url": licence.url,
1504
"free": licence.free,
1505
"logo_url": licence.logo_url,
1506
"pinned": licence.pinned,
1507
}
1508
1509
return flask.jsonify(json_licence)
1510
1511
1512
@app.route("/api/nature/")
1513
def api_natures():
1514
natures = db.session.query(PictureNature).all()
1515
json_natures = {
1516
nature.id: nature.description for nature in natures
1517
}
1518
1519
return flask.jsonify(json_natures)
1520
1521
1522
@app.route("/api/user/")
1523
def api_users():
1524
offset = int(flask.request.args.get("offset", 0))
1525
limit = int(flask.request.args.get("limit", 16))
1526
1527
users = db.session.query(User).offset(offset).limit(limit).all()
1528
1529
json_users = {
1530
user.username: {
1531
"admin": user.admin,
1532
} for user in users
1533
}
1534
1535
return flask.jsonify(json_users)
1536
1537
1538
@app.route("/api/user/<username>/")
1539
def api_user(username):
1540
user = db.session.get(User, username)
1541
if user is None:
1542
flask.abort(404)
1543
1544
json_user = {
1545
"username": user.username,
1546
"admin": user.admin,
1547
"joined": user.joined_timestamp.timestamp(),
1548
}
1549
1550
return flask.jsonify(json_user)
1551
1552
1553
@app.route("/api/login", methods=["POST"])
1554
def api_login():
1555
username = flask.request.json["username"]
1556
password = flask.request.json["password"]
1557
1558
user = db.session.get(User, username)
1559
1560
if user is None:
1561
return flask.jsonify({"error": "This username is not registered. To prevent spam, you must use the HTML interface to register."}), 401
1562
1563
if not bcrypt.check_password_hash(user.password_hashed, password):
1564
return flask.jsonify({"error": "Incorrect password"}), 401
1565
1566
flask.session["username"] = username
1567
1568
return flask.jsonify({"message": "You have been logged in. Your HTTP client must support cookies to use features of this API that require authentication."})
1569
1570
1571
@app.route("/api/logout", methods=["POST"])
1572
def api_logout():
1573
flask.session.pop("username", None)
1574
return flask.jsonify({"message": "You have been logged out."})
1575
1576
1577
@app.route("/api/upload", methods=["POST"])
1578
def api_upload():
1579
if "username" not in flask.session:
1580
return flask.jsonify({"error": "You must be logged in to upload pictures"}), 401
1581
1582
json_ = json.loads(flask.request.form["json"])
1583
title = json_["title"]
1584
description = json_.get("description", "")
1585
origin_url = json_.get("origin_url", "")
1586
author = db.session.get(User, flask.session["username"])
1587
licence_ids = json_["licence"]
1588
nature_id = json_["nature"]
1589
file = flask.request.files["file"]
1590
1591
if not file or not file.filename:
1592
return flask.jsonify({"error": "An image file must be uploaded"}), 400
1593
1594
if not file.mimetype.startswith("image/") or file.mimetype == "image/svg+xml":
1595
return flask.jsonify({"error": "Only bitmap images are supported"}), 400
1596
1597
if not title:
1598
return flask.jsonify({"error": "Give a title"}), 400
1599
1600
if not description:
1601
description = ""
1602
1603
if not nature_id:
1604
return flask.jsonify({"error": "Give a picture type"}), 400
1605
1606
if not licence_ids:
1607
return flask.jsonify({"error": "Give licences"}), 400
1608
1609
licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]
1610
if not any(licence.free for licence in licences):
1611
return flask.jsonify({"error": "Use at least one free licence"}), 400
1612
1613
resource = PictureResource(title, author, description, origin_url, licence_ids,
1614
file.mimetype,
1615
db.session.get(PictureNature, nature_id))
1616
db.session.add(resource)
1617
db.session.commit()
1618
file.save(path.join(config.DATA_PATH, "pictures", str(resource.id)))
1619
pil_image = Image.open(path.join(config.DATA_PATH, "pictures", str(resource.id)))
1620
resource.width, resource.height = pil_image.size
1621
pil_image.thumbnail(config.THUMBNAIL_SIZE)
1622
pil_image = pil_image.convert("RGB")
1623
pil_image.save(path.join(config.DATA_PATH, "picture-thumbnails", str(resource.id)), **config.THUMBNAIL_SAVE_OPTIONS)
1624
db.session.commit()
1625
1626
if json_.get("annotations"):
1627
try:
1628
resource.put_annotations(json_["annotations"])
1629
db.session.commit()
1630
except json.JSONDecodeError:
1631
return flask.jsonify({"error": "Invalid annotations"}), 400
1632
1633
return flask.jsonify({"message": "Picture uploaded successfully", "id": resource.id})
1634
1635
1636
@app.route("/api/picture/<int:id>/update", methods=["POST"])
1637
def api_update_picture(id):
1638
resource = db.session.get(PictureResource, id)
1639
if resource is None:
1640
return flask.jsonify({"error": "Picture not found"}), 404
1641
current_user = db.session.get(User, flask.session.get("username"))
1642
if current_user is None:
1643
return flask.jsonify({"error": "You must be logged in to edit pictures"}), 401
1644
if resource.author != current_user and not current_user.admin:
1645
return flask.jsonify({"error": "You are not the author of this picture"}), 403
1646
1647
title = flask.request.json.get("title", resource.title)
1648
description = flask.request.json.get("description", resource.description)
1649
origin_url = flask.request.json.get("origin_url", resource.origin_url)
1650
licence_ids = flask.request.json.get("licence", [licence.licence_id for licence in resource.licences])
1651
nature_id = flask.request.json.get("nature", resource.nature_id)
1652
1653
if not title:
1654
return flask.jsonify({"error": "Give a title"}), 400
1655
1656
if not description:
1657
description = ""
1658
1659
if not nature_id:
1660
return flask.jsonify({"error": "Give a picture type"}), 400
1661
1662
if not licence_ids:
1663
return flask.jsonify({"error": "Give licences"}), 400
1664
1665
licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]
1666
1667
if not any(licence.free for licence in licences):
1668
return flask.jsonify({"error": "Use at least one free licence"}), 400
1669
1670
resource.title = title
1671
resource.description = description
1672
resource.origin_url = origin_url
1673
resource.licences = licences
1674
resource.nature = db.session.get(PictureNature, nature_id)
1675
1676
db.session.commit()
1677
1678
return flask.jsonify({"message": "Picture updated successfully"})
1679
1680
1681
@app.route("/api/picture/<int:id>/delete", methods=["POST"])
1682
def api_delete_picture(id):
1683
resource = db.session.get(PictureResource, id)
1684
if resource is None:
1685
return flask.jsonify({"error": "Picture not found"}), 404
1686
current_user = db.session.get(User, flask.session.get("username"))
1687
if current_user is None:
1688
return flask.jsonify({"error": "You must be logged in to delete pictures"}), 401
1689
if resource.author != current_user and not current_user.admin:
1690
return flask.jsonify({"error": "You are not the author of this picture"}), 403
1691
1692
PictureInGallery.query.filter_by(resource=resource).delete()
1693
db.session.delete(resource)
1694
db.session.commit()
1695
1696
return flask.jsonify({"message": "Picture deleted successfully"})
1697
1698
1699
@app.route("/api/picture/<int:id>/rate", methods=["POST"])
1700
def api_rate_picture(id):
1701
resource = db.session.get(PictureResource, id)
1702
if resource is None:
1703
flask.abort(404)
1704
1705
current_user = db.session.get(User, flask.session.get("username"))
1706
if current_user is None:
1707
flask.abort(401)
1708
1709
rating = int(flask.request.json.get("rating", 0))
1710
1711
if not rating:
1712
# Delete the existing rating
1713
if PictureRating.query.filter_by(resource=resource, user=current_user).first():
1714
db.session.delete(PictureRating.query.filter_by(resource=resource,
1715
user=current_user).first())
1716
db.session.commit()
1717
1718
return flask.jsonify({"message": "Existing rating removed"})
1719
1720
if not 1 <= rating <= 5:
1721
flask.flash("Invalid rating")
1722
return flask.jsonify({"error": "Invalid rating"}), 400
1723
1724
if PictureRating.query.filter_by(resource=resource, user=current_user).first():
1725
PictureRating.query.filter_by(resource=resource, user=current_user).first().rating = rating
1726
else:
1727
# Create a new rating
1728
db.session.add(PictureRating(resource, current_user, rating))
1729
1730
db.session.commit()
1731
1732
return flask.jsonify({"message": "Rating saved"})
1733
1734
1735
@app.route("/api/gallery/<int:id>/")
1736
def api_gallery(id):
1737
gallery = db.session.get(Gallery, id)
1738
if gallery is None:
1739
flask.abort(404)
1740
1741
json_gallery = {
1742
"id": gallery.id,
1743
"title": gallery.title,
1744
"description": gallery.description,
1745
"owner": gallery.owner_name,
1746
"users": [user.username for user in gallery.users],
1747
}
1748
1749
return flask.jsonify(json_gallery)
1750
1751
1752
@app.route("/api/gallery/<int:id>/edit", methods=["POST"])
1753
def api_edit_gallery(id):
1754
gallery = db.session.get(Gallery, id)
1755
if gallery is None:
1756
flask.abort(404)
1757
1758
current_user = db.session.get(User, flask.session.get("username"))
1759
if current_user is None:
1760
flask.abort(401)
1761
1762
if current_user != gallery.owner and not current_user.admin:
1763
flask.abort(403)
1764
1765
title = flask.request.json.get("title", gallery.title)
1766
description = flask.request.json.get("description", gallery.description)
1767
1768
if not title:
1769
return flask.jsonify({"error": "Give a title"}), 400
1770
1771
if not description:
1772
description = ""
1773
1774
gallery.title = title
1775
gallery.description = description
1776
1777
db.session.commit()
1778
1779
return flask.jsonify({"message": "Gallery updated successfully"})
1780
1781
1782
@app.route("/api/new-gallery", methods=["POST"])
1783
def api_new_gallery():
1784
if "username" not in flask.session:
1785
return flask.jsonify({"error": "You must be logged in to create galleries"}), 401
1786
1787
title = flask.request.json.get("title")
1788
description = flask.request.json.get("description", "")
1789
1790
if not title:
1791
return flask.jsonify({"error": "Give a title"}), 400
1792
1793
gallery = Gallery(title, description, db.session.get(User, flask.session["username"]))
1794
db.session.add(gallery)
1795
db.session.commit()
1796
1797
return flask.jsonify({"message": "Gallery created successfully", "id": gallery.id})
1798
1799
1800
@app.route("/api/gallery/<int:id>/add-picture", methods=["POST"])
1801
def api_gallery_add_picture(id):
1802
gallery = db.session.get(Gallery, id)
1803
if gallery is None:
1804
flask.abort(404)
1805
1806
if "username" not in flask.session:
1807
return flask.jsonify({"error": "You must be logged in to add pictures to galleries"}), 401
1808
1809
current_user = db.session.get(User, flask.session.get("username"))
1810
1811
if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
1812
return flask.jsonify({"error": "You do not have permission to add pictures to this gallery"}), 403
1813
1814
picture_id = flask.request.json.get("picture_id")
1815
1816
try:
1817
picture_id = int(picture_id)
1818
except ValueError:
1819
return flask.jsonify({"error": "Invalid picture ID"}), 400
1820
1821
picture = db.session.get(PictureResource, picture_id)
1822
if picture is None:
1823
return flask.jsonify({"error": "The picture doesn't exist"}), 404
1824
1825
if PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first():
1826
return flask.jsonify({"error": "This picture is already in the gallery"}), 400
1827
1828
db.session.add(PictureInGallery(picture, gallery))
1829
1830
db.session.commit()
1831
1832
return flask.jsonify({"message": "Picture added to gallery"})
1833
1834
1835
@app.route("/api/gallery/<int:id>/remove-picture", methods=["POST"])
1836
def api_gallery_remove_picture(id):
1837
gallery = db.session.get(Gallery, id)
1838
if gallery is None:
1839
flask.abort(404)
1840
1841
if "username" not in flask.session:
1842
return flask.jsonify({"error": "You must be logged in to remove pictures from galleries"}), 401
1843
1844
current_user = db.session.get(User, flask.session.get("username"))
1845
1846
if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
1847
return flask.jsonify({"error": "You do not have permission to remove pictures from this gallery"}), 403
1848
1849
picture_id = flask.request.json.get("picture_id")
1850
1851
try:
1852
picture_id = int(picture_id)
1853
except ValueError:
1854
return flask.jsonify({"error": "Invalid picture ID"}), 400
1855
1856
picture = db.session.get(PictureResource, picture_id)
1857
if picture is None:
1858
return flask.jsonify({"error": "The picture doesn't exist"}), 404
1859
1860
picture_in_gallery = PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first()
1861
if picture_in_gallery is None:
1862
return flask.jsonify({"error": "This picture isn't in the gallery"}), 400
1863
1864
db.session.delete(picture_in_gallery)
1865
1866
db.session.commit()
1867
1868
return flask.jsonify({"message": "Picture removed from gallery"})
1869
1870
1871
@app.route("/api/gallery/<int:id>/users/add", methods=["POST"])
1872
def api_gallery_add_user(id):
1873
gallery = db.session.get(Gallery, id)
1874
if gallery is None:
1875
flask.abort(404)
1876
1877
current_user = db.session.get(User, flask.session.get("username"))
1878
if current_user is None:
1879
flask.abort(401)
1880
1881
if current_user != gallery.owner and not current_user.admin:
1882
flask.abort(403)
1883
1884
username = flask.request.json.get("username")
1885
if username == gallery.owner_name:
1886
return flask.jsonify({"error": "The owner cannot be added to trusted users"}), 400
1887
1888
user = db.session.get(User, username)
1889
if user is None:
1890
return flask.jsonify({"error": "User not found"}), 404
1891
1892
if UserInGallery.query.filter_by(user=user, gallery=gallery).first():
1893
return flask.jsonify({"error": "User is already in the gallery"}), 400
1894
1895
db.session.add(UserInGallery(user, gallery))
1896
1897
db.session.commit()
1898
1899
return flask.jsonify({"message": "User added to gallery"})
1900
1901
1902
@app.route("/api/gallery/<int:id>/users/remove", methods=["POST"])
1903
def api_gallery_remove_user(id):
1904
gallery = db.session.get(Gallery, id)
1905
if gallery is None:
1906
flask.abort(404)
1907
1908
current_user = db.session.get(User, flask.session.get("username"))
1909
if current_user is None:
1910
flask.abort(401)
1911
1912
if current_user != gallery.owner and not current_user.admin:
1913
flask.abort(403)
1914
1915
username = flask.request.json.get("username")
1916
user = db.session.get(User, username)
1917
if user is None:
1918
return flask.jsonify({"error": "User not found"}), 404
1919
1920
user_in_gallery = UserInGallery.query.filter_by(user=user, gallery=gallery).first()
1921
if user_in_gallery is None:
1922
return flask.jsonify({"error": "User is not in the gallery"}), 400
1923
1924
db.session.delete(user_in_gallery)
1925
1926
db.session.commit()
1927
1928
return flask.jsonify({"message": "User removed from gallery"})
1929
1930
1931
@app.route("/api/gallery/<int:id>/delete", methods=["POST"])
1932
def api_delete_gallery(id):
1933
gallery = db.session.get(Gallery, id)
1934
if gallery is None:
1935
flask.abort(404)
1936
1937
current_user = db.session.get(User, flask.session.get("username"))
1938
if current_user is None:
1939
flask.abort(401)
1940
1941
if current_user != gallery.owner and not current_user.admin:
1942
flask.abort(403)
1943
1944
PictureInGallery.query.filter_by(gallery=gallery).delete()
1945
UserInGallery.query.filter_by(gallery=gallery).delete()
1946
db.session.delete(gallery)
1947
1948
db.session.commit()
1949
1950
return flask.jsonify({"message": "Gallery deleted"})
1951
1952