Web platform for sharing free data for ML and research

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