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