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