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