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