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