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