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