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 • 60.05 kiB
Python script, ASCII text executable
        
            
1
import json
2
from datetime import datetime
3
from email.policy import default
4
from time import perf_counter
5
6
import flask
7
from flask_sqlalchemy import SQLAlchemy
8
from flask_bcrypt import Bcrypt
9
from flask_httpauth import HTTPBasicAuth
10
from markupsafe import escape, Markup
11
from flask_migrate import Migrate, current
12
from jinja2_fragments.flask import render_block
13
from sqlalchemy import false
14
from sqlalchemy.orm import backref
15
import sqlalchemy.dialects.postgresql
16
from os import path
17
import os
18
from urllib.parse import urlencode
19
import mimetypes
20
import ruamel.yaml as yaml
21
22
from PIL import Image
23
from sqlalchemy.orm.persistence import post_update
24
from sqlalchemy.sql.functions import current_user
25
26
import config
27
import markdown
28
29
app = flask.Flask(__name__)
30
bcrypt = Bcrypt(app)
31
32
app.config["SQLALCHEMY_DATABASE_URI"] = config.DB_URI
33
app.config["SECRET_KEY"] = config.DB_PASSWORD
34
35
db = SQLAlchemy(app)
36
migrate = Migrate(app, db)
37
38
39
@app.template_filter("split")
40
def split(value, separator=None, maxsplit=-1):
41
return value.split(separator, maxsplit)
42
43
44
@app.template_filter("median")
45
def median(value):
46
value = list(value) # prevent generators
47
return sorted(value)[len(value) // 2]
48
49
50
@app.template_filter("set")
51
def set_filter(value):
52
return set(value)
53
54
55
@app.template_global()
56
def modify_query(**new_values):
57
args = flask.request.args.copy()
58
for key, value in new_values.items():
59
args[key] = value
60
61
return f"{flask.request.path}?{urlencode(args)}"
62
63
64
@app.context_processor
65
def default_variables():
66
return {
67
"current_user": db.session.get(User, flask.session.get("username")),
68
}
69
70
71
with app.app_context():
72
class User(db.Model):
73
username = db.Column(db.String(32), unique=True, nullable=False, primary_key=True)
74
password_hashed = db.Column(db.String(60), nullable=False)
75
admin = db.Column(db.Boolean, nullable=False, default=False, server_default="false")
76
pictures = db.relationship("PictureResource", back_populates="author")
77
joined_timestamp = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
78
galleries = db.relationship("Gallery", back_populates="owner")
79
galleries_joined = db.relationship("UserInGallery", back_populates="user")
80
ratings = db.relationship("PictureRating", back_populates="user")
81
82
def __init__(self, username, password):
83
self.username = username
84
self.password_hashed = bcrypt.generate_password_hash(password).decode("utf-8")
85
86
@property
87
def formatted_name(self):
88
if self.admin:
89
return self.username + "*"
90
return self.username
91
92
93
class Licence(db.Model):
94
id = db.Column(db.String(64), primary_key=True) # SPDX identifier
95
title = db.Column(db.UnicodeText, nullable=False) # the official name of the licence
96
description = db.Column(db.UnicodeText,
97
nullable=False) # brief description of its permissions and restrictions
98
info_url = db.Column(db.String(1024),
99
nullable=False) # the URL to a page with general information about the licence
100
url = db.Column(db.String(1024),
101
nullable=True) # the URL to a page with the full text of the licence and more information
102
pictures = db.relationship("PictureLicence", back_populates="licence")
103
free = db.Column(db.Boolean, nullable=False,
104
default=False) # whether the licence is free or not
105
logo_url = db.Column(db.String(1024), nullable=True) # URL to the logo of the licence
106
pinned = db.Column(db.Boolean, nullable=False,
107
default=False) # whether the licence should be shown at the top of the list
108
109
def __init__(self, id, title, description, info_url, url, free, logo_url=None,
110
pinned=False):
111
self.id = id
112
self.title = title
113
self.description = description
114
self.info_url = info_url
115
self.url = url
116
self.free = free
117
self.logo_url = logo_url
118
self.pinned = pinned
119
120
121
class PictureLicence(db.Model):
122
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
123
124
resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"))
125
licence_id = db.Column(db.String(64), db.ForeignKey("licence.id"))
126
127
resource = db.relationship("PictureResource", back_populates="licences")
128
licence = db.relationship("Licence", back_populates="pictures")
129
130
def __init__(self, resource, licence):
131
self.resource = resource
132
self.licence = licence
133
134
135
class Resource(db.Model):
136
__abstract__ = True
137
138
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
139
title = db.Column(db.UnicodeText, nullable=False)
140
description = db.Column(db.UnicodeText, nullable=False)
141
timestamp = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
142
origin_url = db.Column(db.String(2048),
143
nullable=True) # should be left empty if it's original or the source is unknown but public domain
144
145
146
class PictureNature(db.Model):
147
# Examples:
148
# "photo", "paper-scan", "2d-art-photo", "sculpture-photo", "computer-3d", "computer-painting",
149
# "computer-line-art", "diagram", "infographic", "text", "map", "chart-graph", "screen-capture",
150
# "screen-photo", "pattern", "collage", "ai", and so on
151
id = db.Column(db.String(64), primary_key=True)
152
description = db.Column(db.UnicodeText, nullable=False)
153
resources = db.relationship("PictureResource", back_populates="nature")
154
155
def __init__(self, id, description):
156
self.id = id
157
self.description = description
158
159
160
class PictureObjectInheritance(db.Model):
161
parent_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"),
162
primary_key=True)
163
child_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"),
164
primary_key=True)
165
166
parent = db.relationship("PictureObject", foreign_keys=[parent_id],
167
back_populates="child_links")
168
child = db.relationship("PictureObject", foreign_keys=[child_id],
169
back_populates="parent_links")
170
171
def __init__(self, parent, child):
172
self.parent = parent
173
self.child = child
174
175
176
class PictureObject(db.Model):
177
id = db.Column(db.String(64), primary_key=True)
178
description = db.Column(db.UnicodeText, nullable=False)
179
180
child_links = db.relationship("PictureObjectInheritance",
181
foreign_keys=[PictureObjectInheritance.parent_id],
182
back_populates="parent")
183
parent_links = db.relationship("PictureObjectInheritance",
184
foreign_keys=[PictureObjectInheritance.child_id],
185
back_populates="child")
186
187
def __init__(self, id, description):
188
self.id = id
189
self.description = description
190
191
192
class PictureRegion(db.Model):
193
# This is for picture region annotations
194
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
195
json = db.Column(sqlalchemy.dialects.postgresql.JSONB, nullable=False)
196
197
resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"),
198
nullable=False)
199
object_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"), nullable=True)
200
201
resource = db.relationship("PictureResource", backref="regions")
202
object = db.relationship("PictureObject", backref="regions")
203
204
def __init__(self, json, resource, object):
205
self.json = json
206
self.resource = resource
207
self.object = object
208
209
210
class PictureResource(Resource):
211
# This is only for bitmap pictures. Vectors will be stored under a different model
212
# File name is the ID in the picture directory under data, without an extension
213
file_format = db.Column(db.String(64), nullable=False) # MIME type
214
width = db.Column(db.Integer, nullable=False)
215
height = db.Column(db.Integer, nullable=False)
216
nature_id = db.Column(db.String(32), db.ForeignKey("picture_nature.id"), nullable=True)
217
author_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
218
author = db.relationship("User", back_populates="pictures")
219
220
nature = db.relationship("PictureNature", back_populates="resources")
221
222
replaces_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), nullable=True)
223
replaced_by_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"),
224
nullable=True)
225
226
replaces = db.relationship("PictureResource", remote_side="PictureResource.id",
227
foreign_keys=[replaces_id], back_populates="replaced_by",
228
post_update=True)
229
replaced_by = db.relationship("PictureResource", remote_side="PictureResource.id",
230
foreign_keys=[replaced_by_id], post_update=True)
231
232
copied_from_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"),
233
nullable=True)
234
copied_from = db.relationship("PictureResource", remote_side="PictureResource.id",
235
backref="copies", foreign_keys=[copied_from_id])
236
237
licences = db.relationship("PictureLicence", back_populates="resource")
238
galleries = db.relationship("PictureInGallery", back_populates="resource")
239
ratings = db.relationship("PictureRating", back_populates="resource")
240
241
def __init__(self, title, author, description, origin_url, licence_ids, mime,
242
nature=None):
243
self.title = title
244
self.author = author
245
self.description = description
246
self.origin_url = origin_url
247
self.file_format = mime
248
self.width = self.height = 0
249
self.nature = nature
250
db.session.add(self)
251
db.session.commit()
252
for licence_id in licence_ids:
253
joiner = PictureLicence(self, db.session.get(Licence, licence_id))
254
db.session.add(joiner)
255
256
def put_annotations(self, json):
257
# Delete all previous annotations
258
db.session.query(PictureRegion).filter_by(resource_id=self.id).delete()
259
260
for region in json:
261
object_id = region["object"]
262
picture_object = db.session.get(PictureObject, object_id)
263
264
region_data = {
265
"type": region["type"],
266
"shape": region["shape"],
267
}
268
269
region_row = PictureRegion(region_data, self, picture_object)
270
db.session.add(region_row)
271
272
@property
273
def average_rating(self):
274
if not self.ratings:
275
return None
276
return db.session.query(db.func.avg(PictureRating.rating)).filter_by(resource=self).scalar()
277
278
@property
279
def rating_totals(self):
280
all_ratings = db.session.query(PictureRating.rating).filter_by(resource=self)
281
return {rating: all_ratings.filter_by(rating=rating).count() for rating in range(1, 6)}
282
283
@property
284
def stars(self):
285
if not self.ratings:
286
return 0
287
average = self.average_rating
288
whole_stars = int(average)
289
partial_star = average - whole_stars
290
291
return [100] * whole_stars + [int(partial_star * 100)] + [0] * (4 - whole_stars)
292
293
294
class PictureInGallery(db.Model):
295
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
296
resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"),
297
nullable=False)
298
gallery_id = db.Column(db.Integer, db.ForeignKey("gallery.id"), nullable=False)
299
300
resource = db.relationship("PictureResource")
301
gallery = db.relationship("Gallery")
302
303
def __init__(self, resource, gallery):
304
self.resource = resource
305
self.gallery = gallery
306
307
308
class UserInGallery(db.Model):
309
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
310
username = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
311
gallery_id = db.Column(db.Integer, db.ForeignKey("gallery.id"), nullable=False)
312
313
user = db.relationship("User")
314
gallery = db.relationship("Gallery")
315
316
def __init__(self, user, gallery):
317
self.user = user
318
self.gallery = gallery
319
320
321
class Gallery(db.Model):
322
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
323
title = db.Column(db.UnicodeText, nullable=False)
324
description = db.Column(db.UnicodeText, nullable=False)
325
pictures = db.relationship("PictureInGallery", back_populates="gallery")
326
owner_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
327
owner = db.relationship("User", back_populates="galleries")
328
users = db.relationship("UserInGallery", back_populates="gallery")
329
330
def __init__(self, title, description, owner):
331
self.title = title
332
self.description = description
333
self.owner = owner
334
335
336
class PictureRating(db.Model):
337
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
338
resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), nullable=False)
339
username = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
340
rating = db.Column(db.Integer, db.CheckConstraint("rating >= 1 AND rating <= 5"),
341
nullable=False)
342
343
resource = db.relationship("PictureResource", back_populates="ratings")
344
user = db.relationship("User", back_populates="ratings")
345
346
def __init__(self, resource, user, rating):
347
self.resource = resource
348
self.user = user
349
self.rating = rating
350
351
352
@app.route("/")
353
def index():
354
return flask.render_template("home.html", resources=PictureResource.query.order_by(
355
db.func.random()).limit(10).all())
356
357
358
@app.route("/accounts/")
359
def accounts():
360
return flask.render_template("login.html")
361
362
363
@app.route("/login", methods=["POST"])
364
def login():
365
username = flask.request.form["username"]
366
password = flask.request.form["password"]
367
368
user = db.session.get(User, username)
369
370
if user is None:
371
flask.flash("This username is not registered.")
372
return flask.redirect("/accounts")
373
374
if not bcrypt.check_password_hash(user.password_hashed, password):
375
flask.flash("Incorrect password.")
376
return flask.redirect("/accounts")
377
378
flask.flash("You have been logged in.")
379
380
flask.session["username"] = username
381
return flask.redirect("/")
382
383
384
@app.route("/logout")
385
def logout():
386
flask.session.pop("username", None)
387
flask.flash("You have been logged out.")
388
return flask.redirect("/")
389
390
391
@app.route("/signup", methods=["POST"])
392
def signup():
393
username = flask.request.form["username"]
394
password = flask.request.form["password"]
395
396
if db.session.get(User, username) is not None:
397
flask.flash("This username is already taken.")
398
return flask.redirect("/accounts")
399
400
if set(username) > set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"):
401
flask.flash(
402
"Usernames can only contain the Latin alphabet, digits, hyphens, and underscores.")
403
return flask.redirect("/accounts")
404
405
if len(username) < 3 or len(username) > 32:
406
flask.flash("Usernames must be between 3 and 32 characters long.")
407
return flask.redirect("/accounts")
408
409
if len(password) < 6:
410
flask.flash("Passwords must be at least 6 characters long.")
411
return flask.redirect("/accounts")
412
413
user = User(username, password)
414
db.session.add(user)
415
db.session.commit()
416
417
flask.session["username"] = username
418
419
flask.flash("You have been registered and logged in.")
420
421
return flask.redirect("/")
422
423
424
@app.route("/profile", defaults={"username": None})
425
@app.route("/profile/<username>")
426
def profile(username):
427
if username is None:
428
if "username" in flask.session:
429
return flask.redirect("/profile/" + flask.session["username"])
430
else:
431
flask.flash("Please log in to perform this action.")
432
return flask.redirect("/accounts")
433
434
user = db.session.get(User, username)
435
if user is None:
436
flask.abort(404)
437
438
return flask.render_template("profile.html", user=user)
439
440
441
@app.route("/object/<id>")
442
def has_object(id):
443
object_ = db.session.get(PictureObject, id)
444
if object_ is None:
445
flask.abort(404)
446
447
query = db.session.query(PictureResource).join(PictureRegion).filter(
448
PictureRegion.object_id == id)
449
450
page = int(flask.request.args.get("page", 1))
451
per_page = int(flask.request.args.get("per_page", 16))
452
453
resources = query.paginate(page=page, per_page=per_page)
454
455
return flask.render_template("object.html", object=object_, resources=resources,
456
page_number=page,
457
page_length=per_page, num_pages=resources.pages,
458
prev_page=resources.prev_num,
459
next_page=resources.next_num, PictureRegion=PictureRegion)
460
461
462
@app.route("/upload")
463
def upload():
464
if "username" not in flask.session:
465
flask.flash("Log in to upload pictures.")
466
return flask.redirect("/accounts")
467
468
licences = Licence.query.order_by(Licence.free.desc(), Licence.pinned.desc(),
469
Licence.title).all()
470
471
types = PictureNature.query.all()
472
473
return flask.render_template("upload.html", licences=licences, types=types)
474
475
476
@app.route("/upload", methods=["POST"])
477
def upload_post():
478
title = flask.request.form["title"]
479
description = flask.request.form["description"]
480
origin_url = flask.request.form["origin_url"]
481
author = db.session.get(User, flask.session.get("username"))
482
licence_ids = flask.request.form.getlist("licence")
483
nature_id = flask.request.form["nature"]
484
485
if author is None:
486
flask.abort(401)
487
488
file = flask.request.files["file"]
489
490
if not file or not file.filename:
491
flask.flash("Select a file")
492
return flask.redirect(flask.request.url)
493
494
if not file.mimetype.startswith("image/") or file.mimetype == "image/svg+xml":
495
flask.flash("Only images are supported")
496
return flask.redirect(flask.request.url)
497
498
if not title:
499
flask.flash("Enter a title")
500
return flask.redirect(flask.request.url)
501
502
if not description:
503
description = ""
504
505
if not nature_id:
506
flask.flash("Select a picture type")
507
return flask.redirect(flask.request.url)
508
509
if not licence_ids:
510
flask.flash("Select licences")
511
return flask.redirect(flask.request.url)
512
513
licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]
514
if not any(licence.free for licence in licences):
515
flask.flash("Select at least one free licence")
516
return flask.redirect(flask.request.url)
517
518
resource = PictureResource(title, author, description, origin_url, licence_ids,
519
file.mimetype,
520
db.session.get(PictureNature, nature_id))
521
db.session.add(resource)
522
db.session.commit()
523
file.save(path.join(config.DATA_PATH, "pictures", str(resource.id)))
524
pil_image = Image.open(path.join(config.DATA_PATH, "pictures", str(resource.id)))
525
resource.width, resource.height = pil_image.size
526
db.session.commit()
527
528
if flask.request.form.get("annotations"):
529
try:
530
resource.put_annotations(json.loads(flask.request.form.get("annotations")))
531
db.session.commit()
532
except json.JSONDecodeError:
533
flask.flash("Invalid annotations")
534
535
flask.flash("Picture uploaded successfully")
536
537
return flask.redirect("/picture/" + str(resource.id))
538
539
540
@app.route("/picture/<int:id>/")
541
def picture(id):
542
resource = db.session.get(PictureResource, id)
543
if resource is None:
544
flask.abort(404)
545
546
image = Image.open(path.join(config.DATA_PATH, "pictures", str(resource.id)))
547
548
current_user = db.session.get(User, flask.session.get("username"))
549
have_permission = current_user and (current_user == resource.author or current_user.admin)
550
551
own_rating = None
552
if current_user:
553
own_rating = PictureRating.query.filter_by(resource=resource, user=current_user).first()
554
555
return flask.render_template("picture.html", resource=resource,
556
file_extension=mimetypes.guess_extension(resource.file_format),
557
size=image.size, copies=resource.copies,
558
have_permission=have_permission, own_rating=own_rating)
559
560
561
@app.route("/picture/<int:id>/annotate")
562
def annotate_picture(id):
563
resource = db.session.get(PictureResource, id)
564
if resource is None:
565
flask.abort(404)
566
567
current_user = db.session.get(User, flask.session.get("username"))
568
if current_user is None:
569
flask.abort(401)
570
if resource.author != current_user and not current_user.admin:
571
flask.abort(403)
572
573
return flask.render_template("picture-annotation.html", resource=resource,
574
file_extension=mimetypes.guess_extension(resource.file_format))
575
576
577
@app.route("/picture/<int:id>/put-annotations-form")
578
def put_annotations_form(id):
579
resource = db.session.get(PictureResource, id)
580
if resource is None:
581
flask.abort(404)
582
583
current_user = db.session.get(User, flask.session.get("username"))
584
if current_user is None:
585
flask.abort(401)
586
587
if resource.author != current_user and not current_user.admin:
588
flask.abort(403)
589
590
return flask.render_template("put-annotations-form.html", resource=resource)
591
592
593
@app.route("/picture/<int:id>/put-annotations-form", methods=["POST"])
594
def put_annotations_form_post(id):
595
resource = db.session.get(PictureResource, id)
596
if resource is None:
597
flask.abort(404)
598
599
current_user = db.session.get(User, flask.session.get("username"))
600
if current_user is None:
601
flask.abort(401)
602
603
if resource.author != current_user and not current_user.admin:
604
flask.abort(403)
605
606
resource.put_annotations(json.loads(flask.request.form["annotations"]))
607
608
db.session.commit()
609
610
return flask.redirect("/picture/" + str(resource.id))
611
612
613
@app.route("/picture/<int:id>/save-annotations", methods=["POST"])
614
@app.route("/api/picture/<int:id>/put-annotations", methods=["POST"])
615
def save_annotations(id):
616
resource = db.session.get(PictureResource, id)
617
if resource is None:
618
flask.abort(404)
619
620
current_user = db.session.get(User, flask.session.get("username"))
621
if resource.author != current_user and not current_user.admin:
622
flask.abort(403)
623
624
resource.put_annotations(flask.request.json)
625
626
db.session.commit()
627
628
response = flask.make_response()
629
response.status_code = 204
630
return response
631
632
633
@app.route("/picture/<int:id>/get-annotations")
634
@app.route("/api/picture/<int:id>/api/get-annotations")
635
def get_annotations(id):
636
resource = db.session.get(PictureResource, id)
637
if resource is None:
638
flask.abort(404)
639
640
regions = db.session.query(PictureRegion).filter_by(resource_id=id).all()
641
642
regions_json = []
643
644
for region in regions:
645
regions_json.append({
646
"object": region.object_id,
647
"type": region.json["type"],
648
"shape": region.json["shape"],
649
})
650
651
return flask.jsonify(regions_json)
652
653
654
@app.route("/picture/<int:id>/delete")
655
def delete_picture(id):
656
resource = db.session.get(PictureResource, id)
657
if resource is None:
658
flask.abort(404)
659
660
current_user = db.session.get(User, flask.session.get("username"))
661
if current_user is None:
662
flask.abort(401)
663
664
if resource.author != current_user and not current_user.admin:
665
flask.abort(403)
666
667
PictureLicence.query.filter_by(resource=resource).delete()
668
PictureRegion.query.filter_by(resource=resource).delete()
669
PictureInGallery.query.filter_by(resource=resource).delete()
670
if resource.replaces:
671
resource.replaces.replaced_by = None
672
if resource.replaced_by:
673
resource.replaced_by.replaces = None
674
resource.copied_from = None
675
for copy in resource.copies:
676
copy.copied_from = None
677
db.session.delete(resource)
678
db.session.commit()
679
680
return flask.redirect("/")
681
682
683
@app.route("/picture/<int:id>/mark-replacement", methods=["POST"])
684
def mark_picture_replacement(id):
685
resource = db.session.get(PictureResource, id)
686
if resource is None:
687
flask.abort(404)
688
689
current_user = db.session.get(User, flask.session.get("username"))
690
if current_user is None:
691
flask.abort(401)
692
693
if resource.copied_from.author != current_user and not current_user.admin:
694
flask.abort(403)
695
696
resource.copied_from.replaced_by = resource
697
resource.replaces = resource.copied_from
698
699
db.session.commit()
700
701
return flask.redirect("/picture/" + str(resource.copied_from.id))
702
703
704
@app.route("/picture/<int:id>/remove-replacement", methods=["POST"])
705
def remove_picture_replacement(id):
706
resource = db.session.get(PictureResource, id)
707
if resource is None:
708
flask.abort(404)
709
710
current_user = db.session.get(User, flask.session.get("username"))
711
if current_user is None:
712
flask.abort(401)
713
714
if resource.author != current_user and not current_user.admin:
715
flask.abort(403)
716
717
resource.replaced_by.replaces = None
718
resource.replaced_by = None
719
720
db.session.commit()
721
722
return flask.redirect("/picture/" + str(resource.id))
723
724
725
@app.route("/picture/<int:id>/edit-metadata")
726
def edit_picture(id):
727
resource = db.session.get(PictureResource, id)
728
if resource is None:
729
flask.abort(404)
730
731
current_user = db.session.get(User, flask.session.get("username"))
732
if current_user is None:
733
flask.abort(401)
734
735
if resource.author != current_user and not current_user.admin:
736
flask.abort(403)
737
738
licences = Licence.query.order_by(Licence.free.desc(), Licence.pinned.desc(),
739
Licence.title).all()
740
741
types = PictureNature.query.all()
742
743
return flask.render_template("edit-picture.html", resource=resource, licences=licences,
744
types=types,
745
PictureLicence=PictureLicence)
746
747
748
@app.route("/picture/<int:id>/rate", methods=["POST"])
749
def rate_picture(id):
750
resource = db.session.get(PictureResource, id)
751
if resource is None:
752
flask.abort(404)
753
754
current_user = db.session.get(User, flask.session.get("username"))
755
if current_user is None:
756
flask.abort(401)
757
758
rating = int(flask.request.form.get("rating"))
759
760
if not rating:
761
# Delete the existing rating
762
if PictureRating.query.filter_by(resource=resource, user=current_user).first():
763
db.session.delete(PictureRating.query.filter_by(resource=resource,
764
user=current_user).first())
765
db.session.commit()
766
767
return flask.redirect("/picture/" + str(resource.id))
768
769
if not 1 <= rating <= 5:
770
flask.flash("Invalid rating")
771
return flask.redirect("/picture/" + str(resource.id))
772
773
if PictureRating.query.filter_by(resource=resource, user=current_user).first():
774
PictureRating.query.filter_by(resource=resource, user=current_user).first().rating = rating
775
else:
776
# Create a new rating
777
db.session.add(PictureRating(resource, current_user, rating))
778
779
db.session.commit()
780
781
return flask.redirect("/picture/" + str(resource.id))
782
783
784
@app.route("/picture/<int:id>/edit-metadata", methods=["POST"])
785
def edit_picture_post(id):
786
resource = db.session.get(PictureResource, id)
787
if resource is None:
788
flask.abort(404)
789
790
current_user = db.session.get(User, flask.session.get("username"))
791
if current_user is None:
792
flask.abort(401)
793
794
if resource.author != current_user and not current_user.admin:
795
flask.abort(403)
796
797
title = flask.request.form["title"]
798
description = flask.request.form["description"]
799
origin_url = flask.request.form["origin_url"]
800
licence_ids = flask.request.form.getlist("licence")
801
nature_id = flask.request.form["nature"]
802
803
if not title:
804
flask.flash("Enter a title")
805
return flask.redirect(flask.request.url)
806
807
if not description:
808
description = ""
809
810
if not nature_id:
811
flask.flash("Select a picture type")
812
return flask.redirect(flask.request.url)
813
814
if not licence_ids:
815
flask.flash("Select licences")
816
return flask.redirect(flask.request.url)
817
818
licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]
819
if not any(licence.free for licence in licences):
820
flask.flash("Select at least one free licence")
821
return flask.redirect(flask.request.url)
822
823
resource.title = title
824
resource.description = description
825
resource.origin_url = origin_url
826
for licence_id in licence_ids:
827
joiner = PictureLicence(resource, db.session.get(Licence, licence_id))
828
db.session.add(joiner)
829
resource.nature = db.session.get(PictureNature, nature_id)
830
831
db.session.commit()
832
833
return flask.redirect("/picture/" + str(resource.id))
834
835
836
@app.route("/picture/<int:id>/copy")
837
def copy_picture(id):
838
resource = db.session.get(PictureResource, id)
839
if resource is None:
840
flask.abort(404)
841
842
current_user = db.session.get(User, flask.session.get("username"))
843
if current_user is None:
844
flask.abort(401)
845
846
new_resource = PictureResource(resource.title, current_user, resource.description,
847
resource.origin_url,
848
[licence.licence_id for licence in resource.licences],
849
resource.file_format,
850
resource.nature)
851
852
for region in resource.regions:
853
db.session.add(PictureRegion(region.json, new_resource, region.object))
854
855
db.session.commit()
856
857
# Create a hard link for the new picture
858
old_path = path.join(config.DATA_PATH, "pictures", str(resource.id))
859
new_path = path.join(config.DATA_PATH, "pictures", str(new_resource.id))
860
os.link(old_path, new_path)
861
862
new_resource.width = resource.width
863
new_resource.height = resource.height
864
new_resource.copied_from = resource
865
866
db.session.commit()
867
868
return flask.redirect("/picture/" + str(new_resource.id))
869
870
871
@app.route("/gallery/<int:id>/")
872
def gallery(id):
873
gallery = db.session.get(Gallery, id)
874
if gallery is None:
875
flask.abort(404)
876
877
current_user = db.session.get(User, flask.session.get("username"))
878
879
have_permission = current_user and (current_user == gallery.owner or current_user.admin or UserInGallery.query.filter_by(user=current_user, gallery=gallery).first())
880
881
return flask.render_template("gallery.html", gallery=gallery,
882
have_permission=have_permission)
883
884
885
@app.route("/create-gallery")
886
def create_gallery():
887
if "username" not in flask.session:
888
flask.flash("Log in to create galleries.")
889
return flask.redirect("/accounts")
890
891
return flask.render_template("create-gallery.html")
892
893
894
@app.route("/create-gallery", methods=["POST"])
895
def create_gallery_post():
896
if not flask.session.get("username"):
897
flask.abort(401)
898
899
if not flask.request.form.get("title"):
900
flask.flash("Enter a title")
901
return flask.redirect(flask.request.url)
902
903
description = flask.request.form.get("description", "")
904
905
gallery = Gallery(flask.request.form["title"], description,
906
db.session.get(User, flask.session["username"]))
907
db.session.add(gallery)
908
db.session.commit()
909
910
return flask.redirect("/gallery/" + str(gallery.id))
911
912
913
@app.route("/gallery/<int:id>/add-picture", methods=["POST"])
914
def gallery_add_picture(id):
915
gallery = db.session.get(Gallery, id)
916
if gallery is None:
917
flask.abort(404)
918
919
if "username" not in flask.session:
920
flask.abort(401)
921
922
if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
923
flask.abort(403)
924
925
picture_id = flask.request.form.get("picture_id")
926
if "/" in picture_id: # also allow full URLs
927
picture_id = picture_id.rstrip("/").rpartition("/")[1]
928
if not picture_id:
929
flask.flash("Select a picture")
930
return flask.redirect("/gallery/" + str(gallery.id))
931
picture_id = int(picture_id)
932
933
picture = db.session.get(PictureResource, picture_id)
934
if picture is None:
935
flask.flash("Invalid picture")
936
return flask.redirect("/gallery/" + str(gallery.id))
937
938
if PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first():
939
flask.flash("This picture is already in the gallery")
940
return flask.redirect("/gallery/" + str(gallery.id))
941
942
db.session.add(PictureInGallery(picture, gallery))
943
944
db.session.commit()
945
946
return flask.redirect("/gallery/" + str(gallery.id))
947
948
949
@app.route("/gallery/<int:id>/remove-picture", methods=["POST"])
950
def gallery_remove_picture(id):
951
gallery = db.session.get(Gallery, id)
952
if gallery is None:
953
flask.abort(404)
954
955
if "username" not in flask.session:
956
flask.abort(401)
957
958
current_user = db.session.get(User, flask.session.get("username"))
959
960
if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
961
flask.abort(403)
962
963
picture_id = int(flask.request.form.get("picture_id"))
964
965
picture = db.session.get(PictureResource, picture_id)
966
if picture is None:
967
flask.flash("Invalid picture")
968
return flask.redirect("/gallery/" + str(gallery.id))
969
970
picture_in_gallery = PictureInGallery.query.filter_by(resource=picture,
971
gallery=gallery).first()
972
if picture_in_gallery is None:
973
flask.flash("This picture isn't in the gallery")
974
return flask.redirect("/gallery/" + str(gallery.id))
975
976
db.session.delete(picture_in_gallery)
977
978
db.session.commit()
979
980
return flask.redirect("/gallery/" + str(gallery.id))
981
982
983
@app.route("/gallery/<int:id>/add-pictures-from-query", methods=["POST"])
984
def gallery_add_from_query(id):
985
gallery = db.session.get(Gallery, id)
986
if gallery is None:
987
flask.abort(404)
988
989
if "username" not in flask.session:
990
flask.abort(401)
991
992
if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
993
flask.abort(403)
994
995
query_yaml = flask.request.form.get("query", "")
996
997
yaml_parser = yaml.YAML()
998
query_data = yaml_parser.load(query_yaml) or {}
999
query = get_picture_query(query_data)
1000
1001
pictures = query.all()
1002
1003
count = 0
1004
1005
for picture in pictures:
1006
if not PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first():
1007
db.session.add(PictureInGallery(picture, gallery))
1008
count += 1
1009
1010
db.session.commit()
1011
1012
flask.flash(f"Added {count} pictures to the gallery")
1013
1014
return flask.redirect("/gallery/" + str(gallery.id))
1015
1016
1017
@app.route("/gallery/<int:id>/users")
1018
def gallery_users(id):
1019
gallery = db.session.get(Gallery, id)
1020
if gallery is None:
1021
flask.abort(404)
1022
1023
current_user = db.session.get(User, flask.session.get("username"))
1024
have_permission = current_user and (current_user == gallery.owner or current_user.admin)
1025
1026
return flask.render_template("gallery-users.html", gallery=gallery,
1027
have_permission=have_permission)
1028
1029
1030
@app.route("/gallery/<int:id>/edit")
1031
def edit_gallery(id):
1032
gallery = db.session.get(Gallery, id)
1033
if gallery is None:
1034
flask.abort(404)
1035
1036
current_user = db.session.get(User, flask.session.get("username"))
1037
if current_user is None:
1038
flask.abort(401)
1039
1040
if current_user != gallery.owner and not current_user.admin:
1041
flask.abort(403)
1042
1043
return flask.render_template("edit-gallery.html", gallery=gallery)
1044
1045
1046
@app.route("/gallery/<int:id>/edit", methods=["POST"])
1047
def edit_gallery_post(id):
1048
gallery = db.session.get(Gallery, id)
1049
if gallery is None:
1050
flask.abort(404)
1051
1052
current_user = db.session.get(User, flask.session.get("username"))
1053
if current_user is None:
1054
flask.abort(401)
1055
1056
if current_user != gallery.owner and not current_user.admin:
1057
flask.abort(403)
1058
1059
title = flask.request.form["title"]
1060
description = flask.request.form.get("description")
1061
1062
if not title:
1063
flask.flash("Enter a title")
1064
return flask.redirect(flask.request.url)
1065
1066
if not description:
1067
description = ""
1068
1069
gallery.title = title
1070
gallery.description = description
1071
1072
db.session.commit()
1073
1074
return flask.redirect("/gallery/" + str(gallery.id))
1075
1076
1077
@app.route("/gallery/<int:id>/users/add", methods=["POST"])
1078
def gallery_add_user(id):
1079
gallery = db.session.get(Gallery, id)
1080
if gallery is None:
1081
flask.abort(404)
1082
1083
current_user = db.session.get(User, flask.session.get("username"))
1084
if current_user is None:
1085
flask.abort(401)
1086
1087
if current_user != gallery.owner and not current_user.admin:
1088
flask.abort(403)
1089
1090
username = flask.request.form.get("username")
1091
if username == gallery.owner_name:
1092
flask.flash("The owner is already in the gallery")
1093
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1094
1095
user = db.session.get(User, username)
1096
if user is None:
1097
flask.flash("User not found")
1098
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1099
1100
if UserInGallery.query.filter_by(user=user, gallery=gallery).first():
1101
flask.flash("User is already in the gallery")
1102
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1103
1104
db.session.add(UserInGallery(user, gallery))
1105
1106
db.session.commit()
1107
1108
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1109
1110
1111
@app.route("/gallery/<int:id>/users/remove", methods=["POST"])
1112
def gallery_remove_user(id):
1113
gallery = db.session.get(Gallery, id)
1114
if gallery is None:
1115
flask.abort(404)
1116
1117
current_user = db.session.get(User, flask.session.get("username"))
1118
if current_user is None:
1119
flask.abort(401)
1120
1121
if current_user != gallery.owner and not current_user.admin:
1122
flask.abort(403)
1123
1124
username = flask.request.form.get("username")
1125
user = db.session.get(User, username)
1126
if user is None:
1127
flask.flash("User not found")
1128
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1129
1130
user_in_gallery = UserInGallery.query.filter_by(user=user, gallery=gallery).first()
1131
if user_in_gallery is None:
1132
flask.flash("User is not in the gallery")
1133
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1134
1135
db.session.delete(user_in_gallery)
1136
1137
db.session.commit()
1138
1139
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1140
1141
1142
class APIError(Exception):
1143
def __init__(self, status_code, message):
1144
self.status_code = status_code
1145
self.message = message
1146
1147
1148
def get_picture_query(query_data):
1149
query = db.session.query(PictureResource)
1150
1151
requirement_conditions = {
1152
"has_object": lambda value: PictureResource.regions.any(
1153
PictureRegion.object_id.in_(value)),
1154
"nature": lambda value: PictureResource.nature_id.in_(value),
1155
"licence": lambda value: PictureResource.licences.any(
1156
PictureLicence.licence_id.in_(value)),
1157
"author": lambda value: PictureResource.author_name.in_(value),
1158
"title": lambda value: PictureResource.title.ilike(value),
1159
"description": lambda value: PictureResource.description.ilike(value),
1160
"origin_url": lambda value: db.func.lower(db.func.substr(
1161
PictureResource.origin_url,
1162
db.func.length(db.func.split_part(PictureResource.origin_url, "://", 1)) + 4
1163
)).in_(value),
1164
"above_width": lambda value: PictureResource.width >= value,
1165
"below_width": lambda value: PictureResource.width <= value,
1166
"above_height": lambda value: PictureResource.height >= value,
1167
"below_height": lambda value: PictureResource.height <= value,
1168
"before_date": lambda value: PictureResource.timestamp <= datetime.utcfromtimestamp(
1169
value),
1170
"after_date": lambda value: PictureResource.timestamp >= datetime.utcfromtimestamp(
1171
value),
1172
"in_gallery": lambda value: PictureResource.galleries.any(PictureInGallery.gallery_id.in_(value)),
1173
}
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"})
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"})
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