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 • 51.89 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
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
if resource.author != current_user and not current_user.admin:
561
flask.abort(403)
562
563
return flask.render_template("picture-annotation.html", resource=resource,
564
file_extension=mimetypes.guess_extension(resource.file_format))
565
566
567
@app.route("/picture/<int:id>/put-annotations-form")
568
def put_annotations_form(id):
569
resource = db.session.get(PictureResource, id)
570
if resource is None:
571
flask.abort(404)
572
573
current_user = db.session.get(User, flask.session.get("username"))
574
if current_user is None:
575
flask.abort(401)
576
577
if resource.author != current_user and not current_user.admin:
578
flask.abort(403)
579
580
return flask.render_template("put-annotations-form.html", resource=resource)
581
582
583
@app.route("/picture/<int:id>/put-annotations-form", methods=["POST"])
584
def put_annotations_form_post(id):
585
resource = db.session.get(PictureResource, id)
586
if resource is None:
587
flask.abort(404)
588
589
current_user = db.session.get(User, flask.session.get("username"))
590
if current_user is None:
591
flask.abort(401)
592
593
if resource.author != current_user and not current_user.admin:
594
flask.abort(403)
595
596
resource.put_annotations(json.loads(flask.request.form["annotations"]))
597
598
db.session.commit()
599
600
return flask.redirect("/picture/" + str(resource.id))
601
602
603
@app.route("/picture/<int:id>/save-annotations", methods=["POST"])
604
@app.route("/api/picture/<int:id>/put-annotations", methods=["POST"])
605
def save_annotations(id):
606
resource = db.session.get(PictureResource, id)
607
if resource is None:
608
flask.abort(404)
609
610
current_user = db.session.get(User, flask.session.get("username"))
611
if resource.author != current_user and not current_user.admin:
612
flask.abort(403)
613
614
resource.put_annotations(flask.request.json)
615
616
db.session.commit()
617
618
response = flask.make_response()
619
response.status_code = 204
620
return response
621
622
623
@app.route("/picture/<int:id>/get-annotations")
624
@app.route("/api/picture/<int:id>/api/get-annotations")
625
def get_annotations(id):
626
resource = db.session.get(PictureResource, id)
627
if resource is None:
628
flask.abort(404)
629
630
regions = db.session.query(PictureRegion).filter_by(resource_id=id).all()
631
632
regions_json = []
633
634
for region in regions:
635
regions_json.append({
636
"object": region.object_id,
637
"type": region.json["type"],
638
"shape": region.json["shape"],
639
})
640
641
return flask.jsonify(regions_json)
642
643
644
@app.route("/picture/<int:id>/delete")
645
def delete_picture(id):
646
resource = db.session.get(PictureResource, id)
647
if resource is None:
648
flask.abort(404)
649
650
current_user = db.session.get(User, flask.session.get("username"))
651
if current_user is None:
652
flask.abort(401)
653
654
if resource.author != current_user and not current_user.admin:
655
flask.abort(403)
656
657
PictureLicence.query.filter_by(resource=resource).delete()
658
PictureRegion.query.filter_by(resource=resource).delete()
659
PictureInGallery.query.filter_by(resource=resource).delete()
660
if resource.replaces:
661
resource.replaces.replaced_by = None
662
if resource.replaced_by:
663
resource.replaced_by.replaces = None
664
resource.copied_from = None
665
for copy in resource.copies:
666
copy.copied_from = None
667
db.session.delete(resource)
668
db.session.commit()
669
670
return flask.redirect("/")
671
672
673
@app.route("/picture/<int:id>/mark-replacement", methods=["POST"])
674
def mark_picture_replacement(id):
675
resource = db.session.get(PictureResource, id)
676
if resource is None:
677
flask.abort(404)
678
679
current_user = db.session.get(User, flask.session.get("username"))
680
if current_user is None:
681
flask.abort(401)
682
683
if resource.copied_from.author != current_user and not current_user.admin:
684
flask.abort(403)
685
686
resource.copied_from.replaced_by = resource
687
resource.replaces = resource.copied_from
688
689
db.session.commit()
690
691
return flask.redirect("/picture/" + str(resource.copied_from.id))
692
693
694
@app.route("/picture/<int:id>/remove-replacement", methods=["POST"])
695
def remove_picture_replacement(id):
696
resource = db.session.get(PictureResource, id)
697
if resource is None:
698
flask.abort(404)
699
700
current_user = db.session.get(User, flask.session.get("username"))
701
if current_user is None:
702
flask.abort(401)
703
704
if resource.author != current_user and not current_user.admin:
705
flask.abort(403)
706
707
resource.replaced_by.replaces = None
708
resource.replaced_by = None
709
710
db.session.commit()
711
712
return flask.redirect("/picture/" + str(resource.id))
713
714
715
@app.route("/picture/<int:id>/edit-metadata")
716
def edit_picture(id):
717
resource = db.session.get(PictureResource, id)
718
if resource is None:
719
flask.abort(404)
720
721
current_user = db.session.get(User, flask.session.get("username"))
722
if current_user is None:
723
flask.abort(401)
724
725
if resource.author != current_user and not current_user.admin:
726
flask.abort(403)
727
728
licences = Licence.query.order_by(Licence.free.desc(), Licence.pinned.desc(),
729
Licence.title).all()
730
731
types = PictureNature.query.all()
732
733
return flask.render_template("edit-picture.html", resource=resource, licences=licences,
734
types=types,
735
PictureLicence=PictureLicence)
736
737
738
@app.route("/picture/<int:id>/rate", methods=["POST"])
739
def rate_picture(id):
740
resource = db.session.get(PictureResource, id)
741
if resource is None:
742
flask.abort(404)
743
744
current_user = db.session.get(User, flask.session.get("username"))
745
if current_user is None:
746
flask.abort(401)
747
748
rating = int(flask.request.form.get("rating"))
749
750
if not rating:
751
# Delete the existing rating
752
if PictureRating.query.filter_by(resource=resource, user=current_user).first():
753
db.session.delete(PictureRating.query.filter_by(resource=resource,
754
user=current_user).first())
755
db.session.commit()
756
757
return flask.redirect("/picture/" + str(resource.id))
758
759
if not 1 <= rating <= 5:
760
flask.flash("Invalid rating")
761
return flask.redirect("/picture/" + str(resource.id))
762
763
if PictureRating.query.filter_by(resource=resource, user=current_user).first():
764
PictureRating.query.filter_by(resource=resource, user=current_user).first().rating = rating
765
else:
766
# Create a new rating
767
db.session.add(PictureRating(resource, current_user, rating))
768
769
db.session.commit()
770
771
return flask.redirect("/picture/" + str(resource.id))
772
773
774
@app.route("/picture/<int:id>/edit-metadata", methods=["POST"])
775
def edit_picture_post(id):
776
resource = db.session.get(PictureResource, id)
777
if resource is None:
778
flask.abort(404)
779
780
current_user = db.session.get(User, flask.session.get("username"))
781
if current_user is None:
782
flask.abort(401)
783
784
if resource.author != current_user and not current_user.admin:
785
flask.abort(403)
786
787
title = flask.request.form["title"]
788
description = flask.request.form["description"]
789
origin_url = flask.request.form["origin_url"]
790
licence_ids = flask.request.form.getlist("licence")
791
nature_id = flask.request.form["nature"]
792
793
if not title:
794
flask.flash("Enter a title")
795
return flask.redirect(flask.request.url)
796
797
if not description:
798
description = ""
799
800
if not nature_id:
801
flask.flash("Select a picture type")
802
return flask.redirect(flask.request.url)
803
804
if not licence_ids:
805
flask.flash("Select licences")
806
return flask.redirect(flask.request.url)
807
808
licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]
809
if not any(licence.free for licence in licences):
810
flask.flash("Select at least one free licence")
811
return flask.redirect(flask.request.url)
812
813
resource.title = title
814
resource.description = description
815
resource.origin_url = origin_url
816
for licence_id in licence_ids:
817
joiner = PictureLicence(resource, db.session.get(Licence, licence_id))
818
db.session.add(joiner)
819
resource.nature = db.session.get(PictureNature, nature_id)
820
821
db.session.commit()
822
823
return flask.redirect("/picture/" + str(resource.id))
824
825
826
@app.route("/picture/<int:id>/copy")
827
def copy_picture(id):
828
resource = db.session.get(PictureResource, id)
829
if resource is None:
830
flask.abort(404)
831
832
current_user = db.session.get(User, flask.session.get("username"))
833
if current_user is None:
834
flask.abort(401)
835
836
new_resource = PictureResource(resource.title, current_user, resource.description,
837
resource.origin_url,
838
[licence.licence_id for licence in resource.licences],
839
resource.file_format,
840
resource.nature)
841
842
for region in resource.regions:
843
db.session.add(PictureRegion(region.json, new_resource, region.object))
844
845
db.session.commit()
846
847
# Create a hard link for the new picture
848
old_path = path.join(config.DATA_PATH, "pictures", str(resource.id))
849
new_path = path.join(config.DATA_PATH, "pictures", str(new_resource.id))
850
os.link(old_path, new_path)
851
852
new_resource.width = resource.width
853
new_resource.height = resource.height
854
new_resource.copied_from = resource
855
856
db.session.commit()
857
858
return flask.redirect("/picture/" + str(new_resource.id))
859
860
861
@app.route("/gallery/<int:id>/")
862
def gallery(id):
863
gallery = db.session.get(Gallery, id)
864
if gallery is None:
865
flask.abort(404)
866
867
current_user = db.session.get(User, flask.session.get("username"))
868
869
have_permission = current_user and (current_user == gallery.owner or current_user.admin or UserInGallery.query.filter_by(user=current_user, gallery=gallery).first())
870
871
return flask.render_template("gallery.html", gallery=gallery,
872
have_permission=have_permission)
873
874
875
@app.route("/create-gallery")
876
def create_gallery():
877
if "username" not in flask.session:
878
flask.flash("Log in to create galleries.")
879
return flask.redirect("/accounts")
880
881
return flask.render_template("create-gallery.html")
882
883
884
@app.route("/create-gallery", methods=["POST"])
885
def create_gallery_post():
886
if not flask.session.get("username"):
887
flask.abort(401)
888
889
if not flask.request.form.get("title"):
890
flask.flash("Enter a title")
891
return flask.redirect(flask.request.url)
892
893
description = flask.request.form.get("description", "")
894
895
gallery = Gallery(flask.request.form["title"], description,
896
db.session.get(User, flask.session["username"]))
897
db.session.add(gallery)
898
db.session.commit()
899
900
return flask.redirect("/gallery/" + str(gallery.id))
901
902
903
@app.route("/gallery/<int:id>/add-picture", methods=["POST"])
904
def gallery_add_picture(id):
905
gallery = db.session.get(Gallery, id)
906
if gallery is None:
907
flask.abort(404)
908
909
if "username" not in flask.session:
910
flask.abort(401)
911
912
if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
913
flask.abort(403)
914
915
picture_id = flask.request.form.get("picture_id")
916
if "/" in picture_id: # also allow full URLs
917
picture_id = picture_id.rstrip("/").rpartition("/")[1]
918
if not picture_id:
919
flask.flash("Select a picture")
920
return flask.redirect("/gallery/" + str(gallery.id))
921
picture_id = int(picture_id)
922
923
picture = db.session.get(PictureResource, picture_id)
924
if picture is None:
925
flask.flash("Invalid picture")
926
return flask.redirect("/gallery/" + str(gallery.id))
927
928
if PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first():
929
flask.flash("This picture is already in the gallery")
930
return flask.redirect("/gallery/" + str(gallery.id))
931
932
db.session.add(PictureInGallery(picture, gallery))
933
934
db.session.commit()
935
936
return flask.redirect("/gallery/" + str(gallery.id))
937
938
939
@app.route("/gallery/<int:id>/remove-picture", methods=["POST"])
940
def gallery_remove_picture(id):
941
gallery = db.session.get(Gallery, id)
942
if gallery is None:
943
flask.abort(404)
944
945
if "username" not in flask.session:
946
flask.abort(401)
947
948
current_user = db.session.get(User, flask.session.get("username"))
949
950
if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
951
flask.abort(403)
952
953
picture_id = int(flask.request.form.get("picture_id"))
954
955
picture = db.session.get(PictureResource, picture_id)
956
if picture is None:
957
flask.flash("Invalid picture")
958
return flask.redirect("/gallery/" + str(gallery.id))
959
960
picture_in_gallery = PictureInGallery.query.filter_by(resource=picture,
961
gallery=gallery).first()
962
if picture_in_gallery is None:
963
flask.flash("This picture isn't in the gallery")
964
return flask.redirect("/gallery/" + str(gallery.id))
965
966
db.session.delete(picture_in_gallery)
967
968
db.session.commit()
969
970
return flask.redirect("/gallery/" + str(gallery.id))
971
972
973
@app.route("/gallery/<int:id>/add-pictures-from-query", methods=["POST"])
974
def gallery_add_from_query(id):
975
gallery = db.session.get(Gallery, id)
976
if gallery is None:
977
flask.abort(404)
978
979
if "username" not in flask.session:
980
flask.abort(401)
981
982
if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first():
983
flask.abort(403)
984
985
query_yaml = flask.request.form.get("query", "")
986
987
yaml_parser = yaml.YAML()
988
query_data = yaml_parser.load(query_yaml) or {}
989
query = get_picture_query(query_data)
990
991
pictures = query.all()
992
993
count = 0
994
995
for picture in pictures:
996
if not PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first():
997
db.session.add(PictureInGallery(picture, gallery))
998
count += 1
999
1000
db.session.commit()
1001
1002
flask.flash(f"Added {count} pictures to the gallery")
1003
1004
return flask.redirect("/gallery/" + str(gallery.id))
1005
1006
1007
@app.route("/gallery/<int:id>/users")
1008
def gallery_users(id):
1009
gallery = db.session.get(Gallery, id)
1010
if gallery is None:
1011
flask.abort(404)
1012
1013
current_user = db.session.get(User, flask.session.get("username"))
1014
have_permission = current_user and (current_user == gallery.owner or current_user.admin)
1015
1016
return flask.render_template("gallery-users.html", gallery=gallery,
1017
have_permission=have_permission)
1018
1019
1020
@app.route("/gallery/<int:id>/users/add", methods=["POST"])
1021
def gallery_add_user(id):
1022
gallery = db.session.get(Gallery, id)
1023
if gallery is None:
1024
flask.abort(404)
1025
1026
current_user = db.session.get(User, flask.session.get("username"))
1027
if current_user is None:
1028
flask.abort(401)
1029
1030
if current_user != gallery.owner and not current_user.admin:
1031
flask.abort(403)
1032
1033
username = flask.request.form.get("username")
1034
if username == gallery.owner_name:
1035
flask.flash("The owner is already in the gallery")
1036
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1037
1038
user = db.session.get(User, username)
1039
if user is None:
1040
flask.flash("User not found")
1041
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1042
1043
if UserInGallery.query.filter_by(user=user, gallery=gallery).first():
1044
flask.flash("User is already in the gallery")
1045
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1046
1047
db.session.add(UserInGallery(user, gallery))
1048
1049
db.session.commit()
1050
1051
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1052
1053
1054
@app.route("/gallery/<int:id>/users/remove", methods=["POST"])
1055
def gallery_remove_user(id):
1056
gallery = db.session.get(Gallery, id)
1057
if gallery is None:
1058
flask.abort(404)
1059
1060
current_user = db.session.get(User, flask.session.get("username"))
1061
if current_user is None:
1062
flask.abort(401)
1063
1064
if current_user != gallery.owner and not current_user.admin:
1065
flask.abort(403)
1066
1067
username = flask.request.form.get("username")
1068
user = db.session.get(User, username)
1069
if user is None:
1070
flask.flash("User not found")
1071
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1072
1073
user_in_gallery = UserInGallery.query.filter_by(user=user, gallery=gallery).first()
1074
if user_in_gallery is None:
1075
flask.flash("User is not in the gallery")
1076
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1077
1078
db.session.delete(user_in_gallery)
1079
1080
db.session.commit()
1081
1082
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1083
1084
1085
class APIError(Exception):
1086
def __init__(self, status_code, message):
1087
self.status_code = status_code
1088
self.message = message
1089
1090
1091
def get_picture_query(query_data):
1092
query = db.session.query(PictureResource)
1093
1094
requirement_conditions = {
1095
"has_object": lambda value: PictureResource.regions.any(
1096
PictureRegion.object_id.in_(value)),
1097
"nature": lambda value: PictureResource.nature_id.in_(value),
1098
"licence": lambda value: PictureResource.licences.any(
1099
PictureLicence.licence_id.in_(value)),
1100
"author": lambda value: PictureResource.author_name.in_(value),
1101
"title": lambda value: PictureResource.title.ilike(value),
1102
"description": lambda value: PictureResource.description.ilike(value),
1103
"origin_url": lambda value: db.func.lower(db.func.substr(
1104
PictureResource.origin_url,
1105
db.func.length(db.func.split_part(PictureResource.origin_url, "://", 1)) + 4
1106
)).in_(value),
1107
"above_width": lambda value: PictureResource.width >= value,
1108
"below_width": lambda value: PictureResource.width <= value,
1109
"above_height": lambda value: PictureResource.height >= value,
1110
"below_height": lambda value: PictureResource.height <= value,
1111
"before_date": lambda value: PictureResource.timestamp <= datetime.utcfromtimestamp(
1112
value),
1113
"after_date": lambda value: PictureResource.timestamp >= datetime.utcfromtimestamp(
1114
value),
1115
"in_gallery": lambda value: PictureResource.galleries.any(PictureInGallery.gallery_id.in_(value)),
1116
}
1117
if "want" in query_data:
1118
for i in query_data["want"]:
1119
if len(i) != 1:
1120
raise APIError(400, "Each requirement must have exactly one key")
1121
requirement, value = list(i.items())[0]
1122
if requirement not in requirement_conditions:
1123
raise APIError(400, f"Unknown requirement type: {requirement}")
1124
1125
condition = requirement_conditions[requirement]
1126
query = query.filter(condition(value))
1127
if "exclude" in query_data:
1128
for i in query_data["exclude"]:
1129
if len(i) != 1:
1130
raise APIError(400, "Each exclusion must have exactly one key")
1131
requirement, value = list(i.items())[0]
1132
if requirement not in requirement_conditions:
1133
raise APIError(400, f"Unknown requirement type: {requirement}")
1134
1135
condition = requirement_conditions[requirement]
1136
query = query.filter(~condition(value))
1137
if not query_data.get("include_obsolete", False):
1138
query = query.filter(PictureResource.replaced_by_id.is_(None))
1139
1140
return query
1141
1142
1143
@app.route("/query-pictures")
1144
def graphical_query_pictures():
1145
return flask.render_template("graphical-query-pictures.html")
1146
1147
1148
@app.route("/query-pictures-results")
1149
def graphical_query_pictures_results():
1150
query_yaml = flask.request.args.get("query", "")
1151
yaml_parser = yaml.YAML()
1152
query_data = yaml_parser.load(query_yaml) or {}
1153
try:
1154
query = get_picture_query(query_data)
1155
except APIError as e:
1156
flask.abort(e.status_code)
1157
1158
page = int(flask.request.args.get("page", 1))
1159
per_page = int(flask.request.args.get("per_page", 16))
1160
1161
resources = query.paginate(page=page, per_page=per_page)
1162
1163
return flask.render_template("graphical-query-pictures-results.html", resources=resources,
1164
query=query_yaml,
1165
page_number=page, page_length=per_page,
1166
num_pages=resources.pages,
1167
prev_page=resources.prev_num, next_page=resources.next_num)
1168
1169
1170
@app.route("/raw/picture/<int:id>")
1171
def raw_picture(id):
1172
resource = db.session.get(PictureResource, id)
1173
if resource is None:
1174
flask.abort(404)
1175
1176
response = flask.send_from_directory(path.join(config.DATA_PATH, "pictures"),
1177
str(resource.id))
1178
response.mimetype = resource.file_format
1179
1180
return response
1181
1182
1183
@app.route("/object/")
1184
def graphical_object_types():
1185
return flask.render_template("object-types.html", objects=PictureObject.query.all())
1186
1187
1188
@app.route("/api/object-types")
1189
def object_types():
1190
objects = db.session.query(PictureObject).all()
1191
return flask.jsonify({object.id: object.description for object in objects})
1192
1193
1194
@app.route("/api/query-pictures", methods=["POST"]) # sadly GET can't have a body
1195
def query_pictures():
1196
offset = int(flask.request.args.get("offset", 0))
1197
limit = int(flask.request.args.get("limit", 16))
1198
ordering = flask.request.args.get("ordering", "date-desc")
1199
1200
yaml_parser = yaml.YAML()
1201
query_data = yaml_parser.load(flask.request.data) or {}
1202
try:
1203
query = get_picture_query(query_data)
1204
except APIError as e:
1205
return flask.jsonify({"error": e.message}), e.status_code
1206
1207
match ordering:
1208
case "date-desc":
1209
query = query.order_by(PictureResource.timestamp.desc())
1210
case "date-asc":
1211
query = query.order_by(PictureResource.timestamp.asc())
1212
case "title-asc":
1213
query = query.order_by(PictureResource.title.asc())
1214
case "title-desc":
1215
query = query.order_by(PictureResource.title.desc())
1216
case "random":
1217
query = query.order_by(db.func.random())
1218
case "number-regions-desc":
1219
query = query.order_by(db.func.count(PictureResource.regions).desc())
1220
case "number-regions-asc":
1221
query = query.order_by(db.func.count(PictureResource.regions).asc())
1222
1223
query = query.offset(offset).limit(limit)
1224
resources = query.all()
1225
1226
json_response = {
1227
"date_generated": datetime.utcnow().timestamp(),
1228
"resources": [],
1229
"offset": offset,
1230
"limit": limit,
1231
}
1232
1233
json_resources = json_response["resources"]
1234
1235
for resource in resources:
1236
json_resource = {
1237
"id": resource.id,
1238
"title": resource.title,
1239
"description": resource.description,
1240
"timestamp": resource.timestamp.timestamp(),
1241
"origin_url": resource.origin_url,
1242
"author": resource.author_name,
1243
"file_format": resource.file_format,
1244
"width": resource.width,
1245
"height": resource.height,
1246
"nature": resource.nature_id,
1247
"licences": [licence.licence_id for licence in resource.licences],
1248
"replaces": resource.replaces_id,
1249
"replaced_by": resource.replaced_by_id,
1250
"regions": [],
1251
"download": config.ROOT_URL + flask.url_for("raw_picture", id=resource.id),
1252
}
1253
for region in resource.regions:
1254
json_resource["regions"].append({
1255
"object": region.object_id,
1256
"type": region.json["type"],
1257
"shape": region.json["shape"],
1258
})
1259
1260
json_resources.append(json_resource)
1261
1262
return flask.jsonify(json_response)
1263
1264
1265
@app.route("/api/picture/<int:id>/")
1266
def api_picture(id):
1267
resource = db.session.get(PictureResource, id)
1268
if resource is None:
1269
flask.abort(404)
1270
1271
json_resource = {
1272
"id": resource.id,
1273
"title": resource.title,
1274
"description": resource.description,
1275
"timestamp": resource.timestamp.timestamp(),
1276
"origin_url": resource.origin_url,
1277
"author": resource.author_name,
1278
"file_format": resource.file_format,
1279
"width": resource.width,
1280
"height": resource.height,
1281
"nature": resource.nature_id,
1282
"licences": [licence.licence_id for licence in resource.licences],
1283
"replaces": resource.replaces_id,
1284
"replaced_by": resource.replaced_by_id,
1285
"regions": [],
1286
"download": config.ROOT_URL + flask.url_for("raw_picture", id=resource.id),
1287
}
1288
for region in resource.regions:
1289
json_resource["regions"].append({
1290
"object": region.object_id,
1291
"type": region.json["type"],
1292
"shape": region.json["shape"],
1293
})
1294
1295
return flask.jsonify(json_resource)
1296
1297
1298
@app.route("/api/licence/")
1299
def api_licences():
1300
licences = db.session.query(Licence).all()
1301
json_licences = {
1302
licence.id: {
1303
"title": licence.title,
1304
"free": licence.free,
1305
"pinned": licence.pinned,
1306
} for licence in licences
1307
}
1308
1309
return flask.jsonify(json_licences)
1310
1311
1312
@app.route("/api/licence/<id>/")
1313
def api_licence(id):
1314
licence = db.session.get(Licence, id)
1315
if licence is None:
1316
flask.abort(404)
1317
1318
json_licence = {
1319
"id": licence.id,
1320
"title": licence.title,
1321
"description": licence.description,
1322
"info_url": licence.info_url,
1323
"legalese_url": licence.url,
1324
"free": licence.free,
1325
"logo_url": licence.logo_url,
1326
"pinned": licence.pinned,
1327
}
1328
1329
return flask.jsonify(json_licence)
1330
1331
1332
@app.route("/api/nature/")
1333
def api_natures():
1334
natures = db.session.query(PictureNature).all()
1335
json_natures = {
1336
nature.id: nature.description for nature in natures
1337
}
1338
1339
return flask.jsonify(json_natures)
1340
1341
1342
@app.route("/api/user/")
1343
def api_users():
1344
offset = int(flask.request.args.get("offset", 0))
1345
limit = int(flask.request.args.get("limit", 16))
1346
1347
users = db.session.query(User).offset(offset).limit(limit).all()
1348
1349
json_users = {
1350
user.username: {
1351
"admin": user.admin,
1352
} for user in users
1353
}
1354
1355
return flask.jsonify(json_users)
1356
1357
1358
@app.route("/api/user/<username>/")
1359
def api_user(username):
1360
user = db.session.get(User, username)
1361
if user is None:
1362
flask.abort(404)
1363
1364
json_user = {
1365
"username": user.username,
1366
"admin": user.admin,
1367
"joined": user.joined_timestamp.timestamp(),
1368
}
1369
1370
return flask.jsonify(json_user)
1371
1372
1373
@app.route("/api/login", methods=["POST"])
1374
def api_login():
1375
username = flask.request.json["username"]
1376
password = flask.request.json["password"]
1377
1378
user = db.session.get(User, username)
1379
1380
if user is None:
1381
return flask.jsonify({"error": "This username is not registered. To prevent spam, you must use the HTML interface to register."}), 401
1382
1383
if not bcrypt.check_password_hash(user.password_hashed, password):
1384
return flask.jsonify({"error": "Incorrect password"}), 401
1385
1386
flask.session["username"] = username
1387
1388
return flask.jsonify({"message": "You have been logged in. Your HTTP client must support cookies to use features of this API that require authentication."})
1389
1390
1391
@app.route("/api/logout", methods=["POST"])
1392
def api_logout():
1393
flask.session.pop("username", None)
1394
return flask.jsonify({"message": "You have been logged out."})
1395
1396
1397
@app.route("/api/upload", methods=["POST"])
1398
def api_upload():
1399
if "username" not in flask.session:
1400
return flask.jsonify({"error": "You must be logged in to upload pictures"}), 401
1401
1402
json_ = json.loads(flask.request.form["json"])
1403
title = json_["title"]
1404
description = json_.get("description", "")
1405
origin_url = json_.get("origin_url", "")
1406
author = db.session.get(User, flask.session["username"])
1407
licence_ids = json_["licence"]
1408
nature_id = json_["nature"]
1409
file = flask.request.files["file"]
1410
1411
if not file or not file.filename:
1412
return flask.jsonify({"error": "An image file must be uploaded"}), 400
1413
1414
if not file.mimetype.startswith("image/") or file.mimetype == "image/svg+xml":
1415
return flask.jsonify({"error": "Only bitmap images are supported"}), 400
1416
1417
if not title:
1418
return flask.jsonify({"error": "Give a title"}), 400
1419
1420
if not description:
1421
description = ""
1422
1423
if not nature_id:
1424
return flask.jsonify({"error": "Give a picture type"}), 400
1425
1426
if not licence_ids:
1427
return flask.jsonify({"error": "Give licences"}), 400
1428
1429
licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]
1430
if not any(licence.free for licence in licences):
1431
return flask.jsonify({"error": "Use at least one free licence"}), 400
1432
1433
resource = PictureResource(title, author, description, origin_url, licence_ids,
1434
file.mimetype,
1435
db.session.get(PictureNature, nature_id))
1436
db.session.add(resource)
1437
db.session.commit()
1438
file.save(path.join(config.DATA_PATH, "pictures", str(resource.id)))
1439
pil_image = Image.open(path.join(config.DATA_PATH, "pictures", str(resource.id)))
1440
resource.width, resource.height = pil_image.size
1441
db.session.commit()
1442
1443
if json_.get("annotations"):
1444
try:
1445
resource.put_annotations(json_["annotations"])
1446
db.session.commit()
1447
except json.JSONDecodeError:
1448
return flask.jsonify({"error": "Invalid annotations"}), 400
1449
1450
return flask.jsonify({"message": "Picture uploaded successfully"})
1451
1452
1453
@app.route("/api/picture/<int:id>/update", methods=["POST"])
1454
def api_update_picture(id):
1455
resource = db.session.get(PictureResource, id)
1456
if resource is None:
1457
return flask.jsonify({"error": "Picture not found"}), 404
1458
current_user = db.session.get(User, flask.session.get("username"))
1459
if current_user is None:
1460
return flask.jsonify({"error": "You must be logged in to edit pictures"}), 401
1461
if resource.author != current_user and not current_user.admin:
1462
return flask.jsonify({"error": "You are not the author of this picture"}), 403
1463
1464
title = flask.request.json.get("title", resource.title)
1465
description = flask.request.json.get("description", resource.description)
1466
origin_url = flask.request.json.get("origin_url", resource.origin_url)
1467
licence_ids = flask.request.json.get("licence", [licence.licence_id for licence in resource.licences])
1468
nature_id = flask.request.json.get("nature", resource.nature_id)
1469
1470
if not title:
1471
return flask.jsonify({"error": "Give a title"}), 400
1472
1473
if not description:
1474
description = ""
1475
1476
if not nature_id:
1477
return flask.jsonify({"error": "Give a picture type"}), 400
1478
1479
if not licence_ids:
1480
return flask.jsonify({"error": "Give licences"}), 400
1481
1482
licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]
1483
1484
if not any(licence.free for licence in licences):
1485
return flask.jsonify({"error": "Use at least one free licence"}), 400
1486
1487
resource.title = title
1488
resource.description = description
1489
resource.origin_url = origin_url
1490
resource.licences = licences
1491
resource.nature = db.session.get(PictureNature, nature_id)
1492
1493
db.session.commit()
1494
1495
return flask.jsonify({"message": "Picture updated successfully"})
1496
1497