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