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