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 • 65.31 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("home.html", resources=PictureResource.query.filter_by(replaced_by=None).order_by(
348
db.func.random()).limit(10).all())
349
350
351
@app.route("/info/")
352
def usage_guide():
353
with open("help/usage.md") as f:
354
return flask.render_template("help.html", content=markdown.markdown2html(f.read()))
355
356
357
@app.route("/accounts/")
358
def accounts():
359
return flask.render_template("login.html")
360
361
362
@app.route("/login", methods=["POST"])
363
def login():
364
username = flask.request.form["username"]
365
password = flask.request.form["password"]
366
367
user = db.session.get(User, username)
368
369
if user is None:
370
flask.flash("This username is not registered.")
371
return flask.redirect("/accounts")
372
373
if not bcrypt.check_password_hash(user.password_hashed, password):
374
flask.flash("Incorrect password.")
375
return flask.redirect("/accounts")
376
377
flask.flash("You have been logged in.")
378
379
flask.session["username"] = username
380
return flask.redirect("/")
381
382
383
@app.route("/logout")
384
def logout():
385
flask.session.pop("username", None)
386
flask.flash("You have been logged out.")
387
return flask.redirect("/")
388
389
390
@app.route("/signup", methods=["POST"])
391
def signup():
392
username = flask.request.form["username"]
393
password = flask.request.form["password"]
394
395
if db.session.get(User, username) is not None:
396
flask.flash("This username is already taken.")
397
return flask.redirect("/accounts")
398
399
if set(username) > set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"):
400
flask.flash(
401
"Usernames can only contain the Latin alphabet, digits, hyphens, and underscores.")
402
return flask.redirect("/accounts")
403
404
if len(username) < 3 or len(username) > 32:
405
flask.flash("Usernames must be between 3 and 32 characters long.")
406
return flask.redirect("/accounts")
407
408
if len(password) < 6:
409
flask.flash("Passwords must be at least 6 characters long.")
410
return flask.redirect("/accounts")
411
412
user = User(username, password)
413
db.session.add(user)
414
db.session.commit()
415
416
flask.session["username"] = username
417
418
flask.flash("You have been registered and logged in.")
419
420
return flask.redirect("/")
421
422
423
@app.route("/profile", defaults={"username": None})
424
@app.route("/profile/<username>")
425
def profile(username):
426
if username is None:
427
if "username" in flask.session:
428
return flask.redirect("/profile/" + flask.session["username"])
429
else:
430
flask.flash("Please log in to perform this action.")
431
return flask.redirect("/accounts")
432
433
user = db.session.get(User, username)
434
if user is None:
435
flask.abort(404)
436
437
return flask.render_template("profile.html", user=user)
438
439
440
@app.route("/object/<id>")
441
def has_object(id):
442
object_ = db.session.get(PictureObject, id)
443
if object_ is None:
444
flask.abort(404)
445
446
descendants_cte = (
447
db.select(PictureObject.id)
448
.where(PictureObject.id == id)
449
.cte(name="descendants_cte", recursive=True)
450
)
451
452
descendants_cte = descendants_cte.union_all(
453
db.select(PictureObjectInheritance.child_id)
454
.where(PictureObjectInheritance.parent_id == descendants_cte.c.id)
455
)
456
457
query = db.session.query(PictureResource).filter(
458
PictureResource.regions.any(
459
PictureRegion.object_id.in_(
460
db.select(descendants_cte.c.id)
461
)
462
)
463
)
464
465
page = int(flask.request.args.get("page", 1))
466
per_page = int(flask.request.args.get("per_page", 16))
467
468
resources = query.paginate(page=page, per_page=per_page)
469
470
return flask.render_template("object.html", object=object_, resources=resources,
471
page_number=page,
472
page_length=per_page, num_pages=resources.pages,
473
prev_page=resources.prev_num,
474
next_page=resources.next_num, PictureRegion=PictureRegion)
475
476
477
@app.route("/upload")
478
def upload():
479
if "username" not in flask.session:
480
flask.flash("Log in to upload pictures.")
481
return flask.redirect("/accounts")
482
483
licences = Licence.query.order_by(Licence.free.desc(), Licence.pinned.desc(),
484
Licence.title).all()
485
486
types = PictureNature.query.all()
487
488
return flask.render_template("upload.html", licences=licences, types=types)
489
490
491
@app.route("/upload", methods=["POST"])
492
def upload_post():
493
title = flask.request.form["title"]
494
description = flask.request.form["description"]
495
origin_url = flask.request.form["origin_url"]
496
author = db.session.get(User, flask.session.get("username"))
497
licence_ids = flask.request.form.getlist("licence")
498
nature_id = flask.request.form["nature"]
499
500
if author is None:
501
flask.abort(401)
502
503
file = flask.request.files["file"]
504
505
if not file or not file.filename:
506
flask.flash("Select a file")
507
return flask.redirect(flask.request.url)
508
509
if not file.mimetype.startswith("image/") or file.mimetype == "image/svg+xml":
510
flask.flash("Only images are supported")
511
return flask.redirect(flask.request.url)
512
513
if not title:
514
flask.flash("Enter a title")
515
return flask.redirect(flask.request.url)
516
517
if not description:
518
description = ""
519
520
if not nature_id:
521
flask.flash("Select a picture type")
522
return flask.redirect(flask.request.url)
523
524
if not licence_ids:
525
flask.flash("Select licences")
526
return flask.redirect(flask.request.url)
527
528
licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]
529
if not any(licence.free for licence in licences):
530
flask.flash("Select at least one free licence")
531
return flask.redirect(flask.request.url)
532
533
resource = PictureResource(title, author, description, origin_url, licence_ids,
534
file.mimetype,
535
db.session.get(PictureNature, nature_id))
536
db.session.add(resource)
537
db.session.commit()
538
file.save(path.join(config.DATA_PATH, "pictures", str(resource.id)))
539
pil_image = Image.open(path.join(config.DATA_PATH, "pictures", str(resource.id)))
540
resource.width, resource.height = pil_image.size
541
db.session.commit()
542
543
if flask.request.form.get("annotations"):
544
try:
545
resource.put_annotations(json.loads(flask.request.form.get("annotations")))
546
db.session.commit()
547
except json.JSONDecodeError:
548
flask.flash("Invalid annotations")
549
550
flask.flash("Picture uploaded successfully")
551
552
return flask.redirect("/picture/" + str(resource.id))
553
554
555
@app.route("/picture/<int:id>/")
556
def picture(id):
557
resource = db.session.get(PictureResource, id)
558
if resource is None:
559
flask.abort(404)
560
561
image = Image.open(path.join(config.DATA_PATH, "pictures", str(resource.id)))
562
563
current_user = db.session.get(User, flask.session.get("username"))
564
have_permission = current_user and (current_user == resource.author or current_user.admin)
565
566
own_rating = None
567
if current_user:
568
own_rating = PictureRating.query.filter_by(resource=resource, user=current_user).first()
569
570
return flask.render_template("picture.html", resource=resource,
571
file_extension=mimetypes.guess_extension(resource.file_format),
572
size=image.size, copies=resource.copies,
573
have_permission=have_permission, own_rating=own_rating)
574
575
576
@app.route("/picture/<int:id>/annotate")
577
def annotate_picture(id):
578
resource = db.session.get(PictureResource, id)
579
if resource is None:
580
flask.abort(404)
581
582
current_user = db.session.get(User, flask.session.get("username"))
583
if current_user is None:
584
flask.abort(401)
585
586
if resource.author != current_user and not current_user.admin:
587
flask.abort(403)
588
589
return flask.render_template("picture-annotation.html", resource=resource,
590
file_extension=mimetypes.guess_extension(resource.file_format))
591
592
593
@app.route("/picture/<int:id>/put-annotations-form")
594
def put_annotations_form(id):
595
resource = db.session.get(PictureResource, id)
596
if resource is None:
597
flask.abort(404)
598
599
current_user = db.session.get(User, flask.session.get("username"))
600
if current_user is None:
601
flask.abort(401)
602
603
if resource.author != current_user and not current_user.admin:
604
flask.abort(403)
605
606
return flask.render_template("put-annotations-form.html", resource=resource)
607
608
609
@app.route("/picture/<int:id>/put-annotations-form", methods=["POST"])
610
def put_annotations_form_post(id):
611
resource = db.session.get(PictureResource, id)
612
if resource is None:
613
flask.abort(404)
614
615
current_user = db.session.get(User, flask.session.get("username"))
616
if current_user is None:
617
flask.abort(401)
618
619
if resource.author != current_user and not current_user.admin:
620
flask.abort(403)
621
622
resource.put_annotations(json.loads(flask.request.form["annotations"]))
623
624
db.session.commit()
625
626
return flask.redirect("/picture/" + str(resource.id))
627
628
629
@app.route("/picture/<int:id>/save-annotations", methods=["POST"])
630
@app.route("/api/picture/<int:id>/put-annotations", methods=["POST"])
631
def save_annotations(id):
632
resource = db.session.get(PictureResource, id)
633
if resource is None:
634
flask.abort(404)
635
636
current_user = db.session.get(User, flask.session.get("username"))
637
if resource.author != current_user and not current_user.admin:
638
flask.abort(403)
639
640
resource.put_annotations(flask.request.json)
641
642
db.session.commit()
643
644
response = flask.make_response()
645
response.status_code = 204
646
return response
647
648
649
@app.route("/picture/<int:id>/get-annotations")
650
@app.route("/api/picture/<int:id>/api/get-annotations")
651
def get_annotations(id):
652
resource = db.session.get(PictureResource, id)
653
if resource is None:
654
flask.abort(404)
655
656
regions = db.session.query(PictureRegion).filter_by(resource_id=id).all()
657
658
regions_json = []
659
660
for region in regions:
661
regions_json.append({
662
"object": region.object_id,
663
"type": region.json["type"],
664
"shape": region.json["shape"],
665
})
666
667
return flask.jsonify(regions_json)
668
669
670
@app.route("/picture/<int:id>/delete")
671
def delete_picture(id):
672
resource = db.session.get(PictureResource, id)
673
if resource is None:
674
flask.abort(404)
675
676
current_user = db.session.get(User, flask.session.get("username"))
677
if current_user is None:
678
flask.abort(401)
679
680
if resource.author != current_user and not current_user.admin:
681
flask.abort(403)
682
683
PictureLicence.query.filter_by(resource=resource).delete()
684
PictureRegion.query.filter_by(resource=resource).delete()
685
PictureInGallery.query.filter_by(resource=resource).delete()
686
PictureRating.query.filter_by(resource=resource).delete()
687
if resource.replaces:
688
resource.replaces.replaced_by = None
689
if resource.replaced_by:
690
resource.replaced_by.replaces = None
691
resource.copied_from = None
692
for copy in resource.copies:
693
copy.copied_from = None
694
db.session.delete(resource)
695
db.session.commit()
696
697
return flask.redirect("/")
698
699
700
@app.route("/picture/<int:id>/mark-replacement", methods=["POST"])
701
def mark_picture_replacement(id):
702
resource = db.session.get(PictureResource, id)
703
if resource is None:
704
flask.abort(404)
705
706
current_user = db.session.get(User, flask.session.get("username"))
707
if current_user is None:
708
flask.abort(401)
709
710
if resource.copied_from.author != current_user and not current_user.admin:
711
flask.abort(403)
712
713
resource.copied_from.replaced_by = resource
714
resource.replaces = resource.copied_from
715
716
db.session.commit()
717
718
return flask.redirect("/picture/" + str(resource.copied_from.id))
719
720
721
@app.route("/picture/<int:id>/remove-replacement", methods=["POST"])
722
def remove_picture_replacement(id):
723
resource = db.session.get(PictureResource, id)
724
if resource is None:
725
flask.abort(404)
726
727
current_user = db.session.get(User, flask.session.get("username"))
728
if current_user is None:
729
flask.abort(401)
730
731
if resource.author != current_user and not current_user.admin:
732
flask.abort(403)
733
734
resource.replaced_by.replaces = None
735
resource.replaced_by = None
736
737
db.session.commit()
738
739
return flask.redirect("/picture/" + str(resource.id))
740
741
742
@app.route("/picture/<int:id>/edit-metadata")
743
def edit_picture(id):
744
resource = db.session.get(PictureResource, id)
745
if resource is None:
746
flask.abort(404)
747
748
current_user = db.session.get(User, flask.session.get("username"))
749
if current_user is None:
750
flask.abort(401)
751
752
if resource.author != current_user and not current_user.admin:
753
flask.abort(403)
754
755
licences = Licence.query.order_by(Licence.free.desc(), Licence.pinned.desc(),
756
Licence.title).all()
757
758
types = PictureNature.query.all()
759
760
return flask.render_template("edit-picture.html", resource=resource, licences=licences,
761
types=types,
762
PictureLicence=PictureLicence)
763
764
765
@app.route("/picture/<int:id>/rate", methods=["POST"])
766
def rate_picture(id):
767
resource = db.session.get(PictureResource, id)
768
if resource is None:
769
flask.abort(404)
770
771
current_user = db.session.get(User, flask.session.get("username"))
772
if current_user is None:
773
flask.abort(401)
774
775
rating = int(flask.request.form.get("rating"))
776
777
if not rating:
778
# Delete the existing rating
779
if PictureRating.query.filter_by(resource=resource, user=current_user).first():
780
db.session.delete(PictureRating.query.filter_by(resource=resource,
781
user=current_user).first())
782
db.session.commit()
783
784
return flask.redirect("/picture/" + str(resource.id))
785
786
if not 1 <= rating <= 5:
787
flask.flash("Invalid rating")
788
return flask.redirect("/picture/" + str(resource.id))
789
790
if PictureRating.query.filter_by(resource=resource, user=current_user).first():
791
PictureRating.query.filter_by(resource=resource, user=current_user).first().rating = rating
792
else:
793
# Create a new rating
794
db.session.add(PictureRating(resource, current_user, rating))
795
796
db.session.commit()
797
798
return flask.redirect("/picture/" + str(resource.id))
799
800
801
@app.route("/picture/<int:id>/edit-metadata", methods=["POST"])
802
def edit_picture_post(id):
803
resource = db.session.get(PictureResource, id)
804
if resource is None:
805
flask.abort(404)
806
807
current_user = db.session.get(User, flask.session.get("username"))
808
if current_user is None:
809
flask.abort(401)
810
811
if resource.author != current_user and not current_user.admin:
812
flask.abort(403)
813
814
title = flask.request.form["title"]
815
description = flask.request.form["description"]
816
origin_url = flask.request.form["origin_url"]
817
licence_ids = flask.request.form.getlist("licence")
818
nature_id = flask.request.form["nature"]
819
820
if not title:
821
flask.flash("Enter a title")
822
return flask.redirect(flask.request.url)
823
824
if not description:
825
description = ""
826
827
if not nature_id:
828
flask.flash("Select a picture type")
829
return flask.redirect(flask.request.url)
830
831
if not licence_ids:
832
flask.flash("Select licences")
833
return flask.redirect(flask.request.url)
834
835
licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]
836
if not any(licence.free for licence in licences):
837
flask.flash("Select at least one free licence")
838
return flask.redirect(flask.request.url)
839
840
resource.title = title
841
resource.description = description
842
resource.origin_url = origin_url
843
for licence_id in licence_ids:
844
joiner = PictureLicence(resource, db.session.get(Licence, licence_id))
845
db.session.add(joiner)
846
resource.nature = db.session.get(PictureNature, nature_id)
847
848
db.session.commit()
849
850
return flask.redirect("/picture/" + str(resource.id))
851
852
853
@app.route("/picture/<int:id>/copy")
854
def copy_picture(id):
855
resource = db.session.get(PictureResource, id)
856
if resource is None:
857
flask.abort(404)
858
859
current_user = db.session.get(User, flask.session.get("username"))
860
if current_user is None:
861
flask.abort(401)
862
863
new_resource = PictureResource(resource.title, current_user, resource.description,
864
resource.origin_url,
865
[licence.licence_id for licence in resource.licences],
866
resource.file_format,
867
resource.nature)
868
869
for region in resource.regions:
870
db.session.add(PictureRegion(region.json, new_resource, region.object))
871
872
db.session.commit()
873
874
# Create a hard link for the new picture
875
old_path = path.join(config.DATA_PATH, "pictures", str(resource.id))
876
new_path = path.join(config.DATA_PATH, "pictures", str(new_resource.id))
877
os.link(old_path, new_path)
878
879
new_resource.width = resource.width
880
new_resource.height = resource.height
881
new_resource.copied_from = resource
882
883
db.session.commit()
884
885
return flask.redirect("/picture/" + str(new_resource.id))
886
887
888
@app.route("/gallery/<int:id>/")
889
def gallery(id):
890
gallery = db.session.get(Gallery, id)
891
if gallery is None:
892
flask.abort(404)
893
894
current_user = db.session.get(User, flask.session.get("username"))
895
896
have_permission = current_user and (current_user == gallery.owner or current_user.admin or UserInGallery.query.filter_by(user=current_user, gallery=gallery).first())
897
have_extended_permission = current_user and (current_user == gallery.owner or current_user.admin)
898
899
return flask.render_template("gallery.html", gallery=gallery,
900
have_permission=have_permission,
901
have_extended_permission=have_extended_permission)
902
903
904
@app.route("/create-gallery")
905
def create_gallery():
906
if "username" not in flask.session:
907
flask.flash("Log in to create galleries.")
908
return flask.redirect("/accounts")
909
910
return flask.render_template("create-gallery.html")
911
912
913
@app.route("/create-gallery", methods=["POST"])
914
def create_gallery_post():
915
if not flask.session.get("username"):
916
flask.abort(401)
917
918
if not flask.request.form.get("title"):
919
flask.flash("Enter a title")
920
return flask.redirect(flask.request.url)
921
922
description = flask.request.form.get("description", "")
923
924
gallery = Gallery(flask.request.form["title"], description,
925
db.session.get(User, flask.session["username"]))
926
db.session.add(gallery)
927
db.session.commit()
928
929
return flask.redirect("/gallery/" + str(gallery.id))
930
931
932
@app.route("/gallery/<int:id>/add-picture", methods=["POST"])
933
def gallery_add_picture(id):
934
gallery = db.session.get(Gallery, id)
935
if gallery is None:
936
flask.abort(404)
937
938
if "username" not in flask.session:
939
flask.abort(401)
940
941
if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
942
flask.abort(403)
943
944
picture_id = flask.request.form.get("picture_id")
945
if "/" in picture_id: # also allow full URLs
946
picture_id = picture_id.rstrip("/").rpartition("/")[1]
947
if not picture_id:
948
flask.flash("Select a picture")
949
return flask.redirect("/gallery/" + str(gallery.id))
950
picture_id = int(picture_id)
951
952
picture = db.session.get(PictureResource, picture_id)
953
if picture is None:
954
flask.flash("Invalid picture")
955
return flask.redirect("/gallery/" + str(gallery.id))
956
957
if PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first():
958
flask.flash("This picture is already in the gallery")
959
return flask.redirect("/gallery/" + str(gallery.id))
960
961
db.session.add(PictureInGallery(picture, gallery))
962
963
db.session.commit()
964
965
return flask.redirect("/gallery/" + str(gallery.id))
966
967
968
@app.route("/gallery/<int:id>/remove-picture", methods=["POST"])
969
def gallery_remove_picture(id):
970
gallery = db.session.get(Gallery, id)
971
if gallery is None:
972
flask.abort(404)
973
974
if "username" not in flask.session:
975
flask.abort(401)
976
977
current_user = db.session.get(User, flask.session.get("username"))
978
979
if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
980
flask.abort(403)
981
982
picture_id = int(flask.request.form.get("picture_id"))
983
984
picture = db.session.get(PictureResource, picture_id)
985
if picture is None:
986
flask.flash("Invalid picture")
987
return flask.redirect("/gallery/" + str(gallery.id))
988
989
picture_in_gallery = PictureInGallery.query.filter_by(resource=picture,
990
gallery=gallery).first()
991
if picture_in_gallery is None:
992
flask.flash("This picture isn't in the gallery")
993
return flask.redirect("/gallery/" + str(gallery.id))
994
995
db.session.delete(picture_in_gallery)
996
997
db.session.commit()
998
999
return flask.redirect("/gallery/" + str(gallery.id))
1000
1001
1002
@app.route("/gallery/<int:id>/add-pictures-from-query", methods=["POST"])
1003
def gallery_add_from_query(id):
1004
gallery = db.session.get(Gallery, id)
1005
if gallery is None:
1006
flask.abort(404)
1007
1008
if "username" not in flask.session:
1009
flask.abort(401)
1010
1011
if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
1012
flask.abort(403)
1013
1014
query_yaml = flask.request.form.get("query", "")
1015
1016
yaml_parser = yaml.YAML()
1017
query_data = yaml_parser.load(query_yaml) or {}
1018
query = get_picture_query(query_data)
1019
1020
pictures = query.all()
1021
1022
count = 0
1023
1024
for picture in pictures:
1025
if not PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first():
1026
db.session.add(PictureInGallery(picture, gallery))
1027
count += 1
1028
1029
db.session.commit()
1030
1031
flask.flash(f"Added {count} pictures to the gallery")
1032
1033
return flask.redirect("/gallery/" + str(gallery.id))
1034
1035
1036
@app.route("/gallery/<int:id>/users")
1037
def gallery_users(id):
1038
gallery = db.session.get(Gallery, id)
1039
if gallery is None:
1040
flask.abort(404)
1041
1042
current_user = db.session.get(User, flask.session.get("username"))
1043
have_permission = current_user and (current_user == gallery.owner or current_user.admin)
1044
1045
return flask.render_template("gallery-users.html", gallery=gallery,
1046
have_permission=have_permission)
1047
1048
1049
@app.route("/gallery/<int:id>/edit")
1050
def edit_gallery(id):
1051
gallery = db.session.get(Gallery, id)
1052
if gallery is None:
1053
flask.abort(404)
1054
1055
current_user = db.session.get(User, flask.session.get("username"))
1056
if current_user is None:
1057
flask.abort(401)
1058
1059
if current_user != gallery.owner and not current_user.admin:
1060
flask.abort(403)
1061
1062
return flask.render_template("edit-gallery.html", gallery=gallery)
1063
1064
1065
@app.route("/gallery/<int:id>/edit", methods=["POST"])
1066
def edit_gallery_post(id):
1067
gallery = db.session.get(Gallery, id)
1068
if gallery is None:
1069
flask.abort(404)
1070
1071
current_user = db.session.get(User, flask.session.get("username"))
1072
if current_user is None:
1073
flask.abort(401)
1074
1075
if current_user != gallery.owner and not current_user.admin:
1076
flask.abort(403)
1077
1078
title = flask.request.form["title"]
1079
description = flask.request.form.get("description")
1080
1081
if not title:
1082
flask.flash("Enter a title")
1083
return flask.redirect(flask.request.url)
1084
1085
if not description:
1086
description = ""
1087
1088
gallery.title = title
1089
gallery.description = description
1090
1091
db.session.commit()
1092
1093
return flask.redirect("/gallery/" + str(gallery.id))
1094
1095
1096
@app.route("/gallery/<int:id>/users/add", methods=["POST"])
1097
def gallery_add_user(id):
1098
gallery = db.session.get(Gallery, id)
1099
if gallery is None:
1100
flask.abort(404)
1101
1102
current_user = db.session.get(User, flask.session.get("username"))
1103
if current_user is None:
1104
flask.abort(401)
1105
1106
if current_user != gallery.owner and not current_user.admin:
1107
flask.abort(403)
1108
1109
username = flask.request.form.get("username")
1110
if username == gallery.owner_name:
1111
flask.flash("The owner is already in the gallery")
1112
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1113
1114
user = db.session.get(User, username)
1115
if user is None:
1116
flask.flash("User not found")
1117
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1118
1119
if UserInGallery.query.filter_by(user=user, gallery=gallery).first():
1120
flask.flash("User is already in the gallery")
1121
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1122
1123
db.session.add(UserInGallery(user, gallery))
1124
1125
db.session.commit()
1126
1127
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1128
1129
1130
@app.route("/gallery/<int:id>/users/remove", methods=["POST"])
1131
def gallery_remove_user(id):
1132
gallery = db.session.get(Gallery, id)
1133
if gallery is None:
1134
flask.abort(404)
1135
1136
current_user = db.session.get(User, flask.session.get("username"))
1137
if current_user is None:
1138
flask.abort(401)
1139
1140
if current_user != gallery.owner and not current_user.admin:
1141
flask.abort(403)
1142
1143
username = flask.request.form.get("username")
1144
user = db.session.get(User, username)
1145
if user is None:
1146
flask.flash("User not found")
1147
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1148
1149
user_in_gallery = UserInGallery.query.filter_by(user=user, gallery=gallery).first()
1150
if user_in_gallery is None:
1151
flask.flash("User is not in the gallery")
1152
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1153
1154
db.session.delete(user_in_gallery)
1155
1156
db.session.commit()
1157
1158
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1159
1160
1161
class APIError(Exception):
1162
def __init__(self, status_code, message):
1163
self.status_code = status_code
1164
self.message = message
1165
1166
1167
def get_picture_query(query_data):
1168
query = db.session.query(PictureResource)
1169
1170
def has_condition(id):
1171
descendants_cte = (
1172
db.select(PictureObject.id)
1173
.where(PictureObject.id == id)
1174
.cte(name=f"descendants_cte_{id}", recursive=True)
1175
)
1176
1177
descendants_cte = descendants_cte.union_all(
1178
db.select(PictureObjectInheritance.child_id)
1179
.where(PictureObjectInheritance.parent_id == descendants_cte.c.id)
1180
)
1181
1182
return PictureResource.regions.any(
1183
PictureRegion.object_id.in_(
1184
db.select(descendants_cte.c.id)
1185
)
1186
)
1187
1188
requirement_conditions = {
1189
# Has an object with the ID in the given list
1190
"has_object": lambda value: PictureResource.regions.any(
1191
PictureRegion.object_id.in_(value)),
1192
# Has an object with the ID in the given list, or a subtype of it
1193
"has": lambda value: db.or_(*[has_condition(id) for id in value]),
1194
"nature": lambda value: PictureResource.nature_id.in_(value),
1195
"licence": lambda value: PictureResource.licences.any(
1196
PictureLicence.licence_id.in_(value)),
1197
"author": lambda value: PictureResource.author_name.in_(value),
1198
"title": lambda value: PictureResource.title.ilike(value),
1199
"description": lambda value: PictureResource.description.ilike(value),
1200
"origin_url": lambda value: db.func.lower(db.func.substr(
1201
PictureResource.origin_url,
1202
db.func.length(db.func.split_part(PictureResource.origin_url, "://", 1)) + 4
1203
)).in_(value),
1204
"above_width": lambda value: PictureResource.width >= value,
1205
"below_width": lambda value: PictureResource.width <= value,
1206
"above_height": lambda value: PictureResource.height >= value,
1207
"below_height": lambda value: PictureResource.height <= value,
1208
"before_date": lambda value: PictureResource.timestamp <= datetime.utcfromtimestamp(
1209
value),
1210
"after_date": lambda value: PictureResource.timestamp >= datetime.utcfromtimestamp(
1211
value),
1212
"in_gallery": lambda value: PictureResource.galleries.any(PictureInGallery.gallery_id.in_(value)),
1213
"above_rating": lambda value: db.select(db.func.coalesce(db.func.avg(PictureRating.rating), 5)).where(PictureRating.resource_id == PictureResource.id).scalar_subquery() >= value,
1214
"below_rating": lambda value: db.select(db.func.coalesce(db.func.avg(PictureRating.rating), 0)).where(PictureRating.resource_id == PictureResource.id).scalar_subquery() <= value,
1215
"above_rating_count": lambda value: db.select(db.func.count(PictureRating.id)).where(PictureRating.resource_id == PictureResource.id).correlate(PictureResource).scalar_subquery() >= value,
1216
"below_rating_count": lambda value: db.select(db.func.count(PictureRating.id)).where(PictureRating.resource_id == PictureResource.id).correlate(PictureResource).scalar_subquery() <= value,
1217
"above_region_count": lambda value: db.select(db.func.count(PictureRegion.id)).where(PictureRegion.resource_id == PictureResource.id).correlate(PictureResource).scalar_subquery() >= value,
1218
"below_region_count": lambda value: db.select(db.func.count(PictureRegion.id)).where(PictureRegion.resource_id == PictureResource.id).correlate(PictureResource).scalar_subquery() <= value,
1219
"copied_from": lambda value: PictureResource.copied_from_id.in_(value),
1220
}
1221
1222
if "want" in query_data:
1223
for i in query_data["want"]:
1224
if len(i) != 1:
1225
raise APIError(400, "Each requirement must have exactly one key")
1226
requirement, value = list(i.items())[0]
1227
if requirement not in requirement_conditions:
1228
raise APIError(400, f"Unknown requirement type: {requirement}")
1229
1230
condition = requirement_conditions[requirement]
1231
query = query.filter(condition(value))
1232
if "exclude" in query_data:
1233
for i in query_data["exclude"]:
1234
if len(i) != 1:
1235
raise APIError(400, "Each exclusion must have exactly one key")
1236
requirement, value = list(i.items())[0]
1237
if requirement not in requirement_conditions:
1238
raise APIError(400, f"Unknown requirement type: {requirement}")
1239
1240
condition = requirement_conditions[requirement]
1241
query = query.filter(~condition(value))
1242
if not query_data.get("include_obsolete", False):
1243
query = query.filter(PictureResource.replaced_by_id.is_(None))
1244
1245
return query
1246
1247
1248
@app.route("/query-pictures")
1249
def graphical_query_pictures():
1250
return flask.render_template("graphical-query-pictures.html")
1251
1252
1253
@app.route("/query-pictures-results")
1254
def graphical_query_pictures_results():
1255
query_yaml = flask.request.args.get("query", "")
1256
yaml_parser = yaml.YAML()
1257
query_data = yaml_parser.load(query_yaml) or {}
1258
try:
1259
query = get_picture_query(query_data)
1260
except APIError as e:
1261
flask.abort(e.status_code)
1262
1263
page = int(flask.request.args.get("page", 1))
1264
per_page = int(flask.request.args.get("per_page", 16))
1265
1266
resources = query.paginate(page=page, per_page=per_page)
1267
1268
return flask.render_template("graphical-query-pictures-results.html", resources=resources,
1269
query=query_yaml,
1270
page_number=page, page_length=per_page,
1271
num_pages=resources.pages,
1272
prev_page=resources.prev_num, next_page=resources.next_num)
1273
1274
1275
@app.route("/raw/picture/<int:id>")
1276
def raw_picture(id):
1277
resource = db.session.get(PictureResource, id)
1278
if resource is None:
1279
flask.abort(404)
1280
1281
response = flask.send_from_directory(path.join(config.DATA_PATH, "pictures"),
1282
str(resource.id))
1283
response.mimetype = resource.file_format
1284
1285
return response
1286
1287
1288
@app.route("/object/")
1289
def graphical_object_types():
1290
return flask.render_template("object-types.html", objects=PictureObject.query.all())
1291
1292
1293
@app.route("/api/object-types")
1294
def object_types():
1295
objects = db.session.query(PictureObject).all()
1296
return flask.jsonify({object.id: object.description for object in objects})
1297
1298
1299
@app.route("/api/query-pictures", methods=["POST"]) # sadly GET can't have a body
1300
def query_pictures():
1301
offset = int(flask.request.args.get("offset", 0))
1302
limit = int(flask.request.args.get("limit", 16))
1303
ordering = flask.request.args.get("ordering", "date-desc")
1304
1305
yaml_parser = yaml.YAML()
1306
query_data = yaml_parser.load(flask.request.data) or {}
1307
try:
1308
query = get_picture_query(query_data)
1309
except APIError as e:
1310
return flask.jsonify({"error": e.message}), e.status_code
1311
1312
rating_count_subquery = db.select(db.func.count(PictureRating.id)).where(
1313
PictureRating.resource_id == PictureResource.id).scalar_subquery()
1314
region_count_subquery = db.select(db.func.count(PictureRegion.id)).where(
1315
PictureRegion.resource_id == PictureResource.id).scalar_subquery()
1316
rating_subquery = db.select(db.func.coalesce(db.func.avg(PictureRating.rating), 0)).where(
1317
PictureRating.resource_id == PictureResource.id).scalar_subquery()
1318
1319
match ordering:
1320
case "date-desc":
1321
query = query.order_by(PictureResource.timestamp.desc())
1322
case "date-asc":
1323
query = query.order_by(PictureResource.timestamp.asc())
1324
case "title-asc":
1325
query = query.order_by(PictureResource.title.asc())
1326
case "title-desc":
1327
query = query.order_by(PictureResource.title.desc())
1328
case "random":
1329
query = query.order_by(db.func.random())
1330
case "number-regions-desc":
1331
query = query.order_by(region_count_subquery.desc())
1332
case "number-regions-asc":
1333
query = query.order_by(region_count_subquery.asc())
1334
case "rating-desc":
1335
query = query.order_by(rating_subquery.desc())
1336
case "rating-asc":
1337
query = query.order_by(rating_subquery.asc())
1338
case "number-ratings-desc":
1339
query = query.order_by(rating_count_subquery.desc())
1340
case "number-ratings-asc":
1341
query = query.order_by(rating_count_subquery.asc())
1342
1343
query = query.offset(offset).limit(limit)
1344
resources = query.all()
1345
1346
json_response = {
1347
"date_generated": datetime.utcnow().timestamp(),
1348
"resources": [],
1349
"offset": offset,
1350
"limit": limit,
1351
}
1352
1353
json_resources = json_response["resources"]
1354
1355
for resource in resources:
1356
json_resource = {
1357
"id": resource.id,
1358
"title": resource.title,
1359
"description": resource.description,
1360
"timestamp": resource.timestamp.timestamp(),
1361
"origin_url": resource.origin_url,
1362
"author": resource.author_name,
1363
"file_format": resource.file_format,
1364
"width": resource.width,
1365
"height": resource.height,
1366
"nature": resource.nature_id,
1367
"licences": [licence.licence_id for licence in resource.licences],
1368
"replaces": resource.replaces_id,
1369
"replaced_by": resource.replaced_by_id,
1370
"regions": [],
1371
"download": config.ROOT_URL + flask.url_for("raw_picture", id=resource.id),
1372
}
1373
for region in resource.regions:
1374
json_resource["regions"].append({
1375
"object": region.object_id,
1376
"type": region.json["type"],
1377
"shape": region.json["shape"],
1378
})
1379
1380
json_resources.append(json_resource)
1381
1382
return flask.jsonify(json_response)
1383
1384
1385
@app.route("/api/picture/<int:id>/")
1386
def api_picture(id):
1387
resource = db.session.get(PictureResource, id)
1388
if resource is None:
1389
flask.abort(404)
1390
1391
json_resource = {
1392
"id": resource.id,
1393
"title": resource.title,
1394
"description": resource.description,
1395
"timestamp": resource.timestamp.timestamp(),
1396
"origin_url": resource.origin_url,
1397
"author": resource.author_name,
1398
"file_format": resource.file_format,
1399
"width": resource.width,
1400
"height": resource.height,
1401
"nature": resource.nature_id,
1402
"licences": [licence.licence_id for licence in resource.licences],
1403
"replaces": resource.replaces_id,
1404
"replaced_by": resource.replaced_by_id,
1405
"regions": [],
1406
"download": config.ROOT_URL + flask.url_for("raw_picture", id=resource.id),
1407
"rating_average": resource.average_rating,
1408
"rating_count": resource.rating_totals,
1409
}
1410
for region in resource.regions:
1411
json_resource["regions"].append({
1412
"object": region.object_id,
1413
"type": region.json["type"],
1414
"shape": region.json["shape"],
1415
})
1416
1417
return flask.jsonify(json_resource)
1418
1419
1420
@app.route("/api/licence/")
1421
def api_licences():
1422
licences = db.session.query(Licence).all()
1423
json_licences = {
1424
licence.id: {
1425
"title": licence.title,
1426
"free": licence.free,
1427
"pinned": licence.pinned,
1428
} for licence in licences
1429
}
1430
1431
return flask.jsonify(json_licences)
1432
1433
1434
@app.route("/api/licence/<id>/")
1435
def api_licence(id):
1436
licence = db.session.get(Licence, id)
1437
if licence is None:
1438
flask.abort(404)
1439
1440
json_licence = {
1441
"id": licence.id,
1442
"title": licence.title,
1443
"description": licence.description,
1444
"info_url": licence.info_url,
1445
"legalese_url": licence.url,
1446
"free": licence.free,
1447
"logo_url": licence.logo_url,
1448
"pinned": licence.pinned,
1449
}
1450
1451
return flask.jsonify(json_licence)
1452
1453
1454
@app.route("/api/nature/")
1455
def api_natures():
1456
natures = db.session.query(PictureNature).all()
1457
json_natures = {
1458
nature.id: nature.description for nature in natures
1459
}
1460
1461
return flask.jsonify(json_natures)
1462
1463
1464
@app.route("/api/user/")
1465
def api_users():
1466
offset = int(flask.request.args.get("offset", 0))
1467
limit = int(flask.request.args.get("limit", 16))
1468
1469
users = db.session.query(User).offset(offset).limit(limit).all()
1470
1471
json_users = {
1472
user.username: {
1473
"admin": user.admin,
1474
} for user in users
1475
}
1476
1477
return flask.jsonify(json_users)
1478
1479
1480
@app.route("/api/user/<username>/")
1481
def api_user(username):
1482
user = db.session.get(User, username)
1483
if user is None:
1484
flask.abort(404)
1485
1486
json_user = {
1487
"username": user.username,
1488
"admin": user.admin,
1489
"joined": user.joined_timestamp.timestamp(),
1490
}
1491
1492
return flask.jsonify(json_user)
1493
1494
1495
@app.route("/api/login", methods=["POST"])
1496
def api_login():
1497
username = flask.request.json["username"]
1498
password = flask.request.json["password"]
1499
1500
user = db.session.get(User, username)
1501
1502
if user is None:
1503
return flask.jsonify({"error": "This username is not registered. To prevent spam, you must use the HTML interface to register."}), 401
1504
1505
if not bcrypt.check_password_hash(user.password_hashed, password):
1506
return flask.jsonify({"error": "Incorrect password"}), 401
1507
1508
flask.session["username"] = username
1509
1510
return flask.jsonify({"message": "You have been logged in. Your HTTP client must support cookies to use features of this API that require authentication."})
1511
1512
1513
@app.route("/api/logout", methods=["POST"])
1514
def api_logout():
1515
flask.session.pop("username", None)
1516
return flask.jsonify({"message": "You have been logged out."})
1517
1518
1519
@app.route("/api/upload", methods=["POST"])
1520
def api_upload():
1521
if "username" not in flask.session:
1522
return flask.jsonify({"error": "You must be logged in to upload pictures"}), 401
1523
1524
json_ = json.loads(flask.request.form["json"])
1525
title = json_["title"]
1526
description = json_.get("description", "")
1527
origin_url = json_.get("origin_url", "")
1528
author = db.session.get(User, flask.session["username"])
1529
licence_ids = json_["licence"]
1530
nature_id = json_["nature"]
1531
file = flask.request.files["file"]
1532
1533
if not file or not file.filename:
1534
return flask.jsonify({"error": "An image file must be uploaded"}), 400
1535
1536
if not file.mimetype.startswith("image/") or file.mimetype == "image/svg+xml":
1537
return flask.jsonify({"error": "Only bitmap images are supported"}), 400
1538
1539
if not title:
1540
return flask.jsonify({"error": "Give a title"}), 400
1541
1542
if not description:
1543
description = ""
1544
1545
if not nature_id:
1546
return flask.jsonify({"error": "Give a picture type"}), 400
1547
1548
if not licence_ids:
1549
return flask.jsonify({"error": "Give licences"}), 400
1550
1551
licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]
1552
if not any(licence.free for licence in licences):
1553
return flask.jsonify({"error": "Use at least one free licence"}), 400
1554
1555
resource = PictureResource(title, author, description, origin_url, licence_ids,
1556
file.mimetype,
1557
db.session.get(PictureNature, nature_id))
1558
db.session.add(resource)
1559
db.session.commit()
1560
file.save(path.join(config.DATA_PATH, "pictures", str(resource.id)))
1561
pil_image = Image.open(path.join(config.DATA_PATH, "pictures", str(resource.id)))
1562
resource.width, resource.height = pil_image.size
1563
db.session.commit()
1564
1565
if json_.get("annotations"):
1566
try:
1567
resource.put_annotations(json_["annotations"])
1568
db.session.commit()
1569
except json.JSONDecodeError:
1570
return flask.jsonify({"error": "Invalid annotations"}), 400
1571
1572
return flask.jsonify({"message": "Picture uploaded successfully", "id": resource.id})
1573
1574
1575
@app.route("/api/picture/<int:id>/update", methods=["POST"])
1576
def api_update_picture(id):
1577
resource = db.session.get(PictureResource, id)
1578
if resource is None:
1579
return flask.jsonify({"error": "Picture not found"}), 404
1580
current_user = db.session.get(User, flask.session.get("username"))
1581
if current_user is None:
1582
return flask.jsonify({"error": "You must be logged in to edit pictures"}), 401
1583
if resource.author != current_user and not current_user.admin:
1584
return flask.jsonify({"error": "You are not the author of this picture"}), 403
1585
1586
title = flask.request.json.get("title", resource.title)
1587
description = flask.request.json.get("description", resource.description)
1588
origin_url = flask.request.json.get("origin_url", resource.origin_url)
1589
licence_ids = flask.request.json.get("licence", [licence.licence_id for licence in resource.licences])
1590
nature_id = flask.request.json.get("nature", resource.nature_id)
1591
1592
if not title:
1593
return flask.jsonify({"error": "Give a title"}), 400
1594
1595
if not description:
1596
description = ""
1597
1598
if not nature_id:
1599
return flask.jsonify({"error": "Give a picture type"}), 400
1600
1601
if not licence_ids:
1602
return flask.jsonify({"error": "Give licences"}), 400
1603
1604
licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]
1605
1606
if not any(licence.free for licence in licences):
1607
return flask.jsonify({"error": "Use at least one free licence"}), 400
1608
1609
resource.title = title
1610
resource.description = description
1611
resource.origin_url = origin_url
1612
resource.licences = licences
1613
resource.nature = db.session.get(PictureNature, nature_id)
1614
1615
db.session.commit()
1616
1617
return flask.jsonify({"message": "Picture updated successfully"})
1618
1619
1620
@app.route("/api/picture/<int:id>/rate", methods=["POST"])
1621
def api_rate_picture(id):
1622
resource = db.session.get(PictureResource, id)
1623
if resource is None:
1624
flask.abort(404)
1625
1626
current_user = db.session.get(User, flask.session.get("username"))
1627
if current_user is None:
1628
flask.abort(401)
1629
1630
rating = int(flask.request.json.get("rating", 0))
1631
1632
if not rating:
1633
# Delete the existing rating
1634
if PictureRating.query.filter_by(resource=resource, user=current_user).first():
1635
db.session.delete(PictureRating.query.filter_by(resource=resource,
1636
user=current_user).first())
1637
db.session.commit()
1638
1639
return flask.jsonify({"message": "Existing rating removed"})
1640
1641
if not 1 <= rating <= 5:
1642
flask.flash("Invalid rating")
1643
return flask.jsonify({"error": "Invalid rating"}), 400
1644
1645
if PictureRating.query.filter_by(resource=resource, user=current_user).first():
1646
PictureRating.query.filter_by(resource=resource, user=current_user).first().rating = rating
1647
else:
1648
# Create a new rating
1649
db.session.add(PictureRating(resource, current_user, rating))
1650
1651
db.session.commit()
1652
1653
return flask.jsonify({"message": "Rating saved"})
1654
1655
1656
@app.route("/api/gallery/<int:id>/")
1657
def api_gallery(id):
1658
gallery = db.session.get(Gallery, id)
1659
if gallery is None:
1660
flask.abort(404)
1661
1662
json_gallery = {
1663
"id": gallery.id,
1664
"title": gallery.title,
1665
"description": gallery.description,
1666
"owner": gallery.owner_name,
1667
"users": [user.username for user in gallery.users],
1668
}
1669
1670
return flask.jsonify(json_gallery)
1671
1672
1673
@app.route("/api/gallery/<int:id>/edit", methods=["POST"])
1674
def api_edit_gallery(id):
1675
gallery = db.session.get(Gallery, id)
1676
if gallery is None:
1677
flask.abort(404)
1678
1679
current_user = db.session.get(User, flask.session.get("username"))
1680
if current_user is None:
1681
flask.abort(401)
1682
1683
if current_user != gallery.owner and not current_user.admin:
1684
flask.abort(403)
1685
1686
title = flask.request.json.get("title", gallery.title)
1687
description = flask.request.json.get("description", gallery.description)
1688
1689
if not title:
1690
return flask.jsonify({"error": "Give a title"}), 400
1691
1692
if not description:
1693
description = ""
1694
1695
gallery.title = title
1696
gallery.description = description
1697
1698
db.session.commit()
1699
1700
return flask.jsonify({"message": "Gallery updated successfully"})
1701
1702
1703
@app.route("/api/new-gallery", methods=["POST"])
1704
def api_new_gallery():
1705
if "username" not in flask.session:
1706
return flask.jsonify({"error": "You must be logged in to create galleries"}), 401
1707
1708
title = flask.request.json.get("title")
1709
description = flask.request.json.get("description", "")
1710
1711
if not title:
1712
return flask.jsonify({"error": "Give a title"}), 400
1713
1714
gallery = Gallery(title, description, db.session.get(User, flask.session["username"]))
1715
db.session.add(gallery)
1716
db.session.commit()
1717
1718
return flask.jsonify({"message": "Gallery created successfully", "id": gallery.id})
1719
1720
1721
@app.route("/api/gallery/<int:id>/add-picture", methods=["POST"])
1722
def api_gallery_add_picture(id):
1723
gallery = db.session.get(Gallery, id)
1724
if gallery is None:
1725
flask.abort(404)
1726
1727
if "username" not in flask.session:
1728
return flask.jsonify({"error": "You must be logged in to add pictures to galleries"}), 401
1729
1730
current_user = db.session.get(User, flask.session.get("username"))
1731
1732
if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
1733
return flask.jsonify({"error": "You do not have permission to add pictures to this gallery"}), 403
1734
1735
picture_id = flask.request.json.get("picture_id")
1736
1737
try:
1738
picture_id = int(picture_id)
1739
except ValueError:
1740
return flask.jsonify({"error": "Invalid picture ID"}), 400
1741
1742
picture = db.session.get(PictureResource, picture_id)
1743
if picture is None:
1744
return flask.jsonify({"error": "The picture doesn't exist"}), 404
1745
1746
if PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first():
1747
return flask.jsonify({"error": "This picture is already in the gallery"}), 400
1748
1749
db.session.add(PictureInGallery(picture, gallery))
1750
1751
db.session.commit()
1752
1753
return flask.jsonify({"message": "Picture added to gallery"})
1754
1755
1756
@app.route("/api/gallery/<int:id>/remove-picture", methods=["POST"])
1757
def api_gallery_remove_picture(id):
1758
gallery = db.session.get(Gallery, id)
1759
if gallery is None:
1760
flask.abort(404)
1761
1762
if "username" not in flask.session:
1763
return flask.jsonify({"error": "You must be logged in to remove pictures from galleries"}), 401
1764
1765
current_user = db.session.get(User, flask.session.get("username"))
1766
1767
if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
1768
return flask.jsonify({"error": "You do not have permission to remove pictures from this gallery"}), 403
1769
1770
picture_id = flask.request.json.get("picture_id")
1771
1772
try:
1773
picture_id = int(picture_id)
1774
except ValueError:
1775
return flask.jsonify({"error": "Invalid picture ID"}), 400
1776
1777
picture = db.session.get(PictureResource, picture_id)
1778
if picture is None:
1779
return flask.jsonify({"error": "The picture doesn't exist"}), 404
1780
1781
picture_in_gallery = PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first()
1782
if picture_in_gallery is None:
1783
return flask.jsonify({"error": "This picture isn't in the gallery"}), 400
1784
1785
db.session.delete(picture_in_gallery)
1786
1787
db.session.commit()
1788
1789
return flask.jsonify({"message": "Picture removed from gallery"})
1790
1791
1792
@app.route("/api/gallery/<int:id>/users/add", methods=["POST"])
1793
def api_gallery_add_user(id):
1794
gallery = db.session.get(Gallery, id)
1795
if gallery is None:
1796
flask.abort(404)
1797
1798
current_user = db.session.get(User, flask.session.get("username"))
1799
if current_user is None:
1800
flask.abort(401)
1801
1802
if current_user != gallery.owner and not current_user.admin:
1803
flask.abort(403)
1804
1805
username = flask.request.json.get("username")
1806
if username == gallery.owner_name:
1807
return flask.jsonify({"error": "The owner cannot be added to trusted users"}), 400
1808
1809
user = db.session.get(User, username)
1810
if user is None:
1811
return flask.jsonify({"error": "User not found"}), 404
1812
1813
if UserInGallery.query.filter_by(user=user, gallery=gallery).first():
1814
return flask.jsonify({"error": "User is already in the gallery"}), 400
1815
1816
db.session.add(UserInGallery(user, gallery))
1817
1818
db.session.commit()
1819
1820
return flask.jsonify({"message": "User added to gallery"})
1821
1822
1823
@app.route("/api/gallery/<int:id>/users/remove", methods=["POST"])
1824
def api_gallery_remove_user(id):
1825
gallery = db.session.get(Gallery, id)
1826
if gallery is None:
1827
flask.abort(404)
1828
1829
current_user = db.session.get(User, flask.session.get("username"))
1830
if current_user is None:
1831
flask.abort(401)
1832
1833
if current_user != gallery.owner and not current_user.admin:
1834
flask.abort(403)
1835
1836
username = flask.request.json.get("username")
1837
user = db.session.get(User, username)
1838
if user is None:
1839
return flask.jsonify({"error": "User not found"}), 404
1840
1841
user_in_gallery = UserInGallery.query.filter_by(user=user, gallery=gallery).first()
1842
if user_in_gallery is None:
1843
return flask.jsonify({"error": "User is not in the gallery"}), 400
1844
1845
db.session.delete(user_in_gallery)
1846
1847
db.session.commit()
1848
1849
return flask.jsonify({"message": "User removed from gallery"})
1850
1851
1852
@app.route("/api/gallery/<int:id>/delete", methods=["POST"])
1853
def api_delete_gallery(id):
1854
gallery = db.session.get(Gallery, id)
1855
if gallery is None:
1856
flask.abort(404)
1857
1858
current_user = db.session.get(User, flask.session.get("username"))
1859
if current_user is None:
1860
flask.abort(401)
1861
1862
if current_user != gallery.owner and not current_user.admin:
1863
flask.abort(403)
1864
1865
for picture_in_gallery in gallery.pictures:
1866
db.session.delete(picture_in_gallery)
1867
1868
for user_in_gallery in gallery.users:
1869
db.session.delete(user_in_gallery)
1870
1871
db.session.delete(gallery)
1872
1873
db.session.commit()
1874
1875
return flask.jsonify({"message": "Gallery deleted"})
1876
1877