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 • 52.21 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>/users/add", methods=["POST"])
1031
def gallery_add_user(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
username = flask.request.form.get("username")
1044
if username == gallery.owner_name:
1045
flask.flash("The owner is already in the gallery")
1046
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1047
1048
user = db.session.get(User, username)
1049
if user is None:
1050
flask.flash("User not found")
1051
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1052
1053
if UserInGallery.query.filter_by(user=user, gallery=gallery).first():
1054
flask.flash("User is already in the gallery")
1055
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1056
1057
db.session.add(UserInGallery(user, gallery))
1058
1059
db.session.commit()
1060
1061
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1062
1063
1064
@app.route("/gallery/<int:id>/users/remove", methods=["POST"])
1065
def gallery_remove_user(id):
1066
gallery = db.session.get(Gallery, id)
1067
if gallery is None:
1068
flask.abort(404)
1069
1070
current_user = db.session.get(User, flask.session.get("username"))
1071
if current_user is None:
1072
flask.abort(401)
1073
1074
if current_user != gallery.owner and not current_user.admin:
1075
flask.abort(403)
1076
1077
username = flask.request.form.get("username")
1078
user = db.session.get(User, username)
1079
if user is None:
1080
flask.flash("User not found")
1081
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1082
1083
user_in_gallery = UserInGallery.query.filter_by(user=user, gallery=gallery).first()
1084
if user_in_gallery is None:
1085
flask.flash("User is not in the gallery")
1086
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1087
1088
db.session.delete(user_in_gallery)
1089
1090
db.session.commit()
1091
1092
return flask.redirect("/gallery/" + str(gallery.id) + "/users")
1093
1094
1095
class APIError(Exception):
1096
def __init__(self, status_code, message):
1097
self.status_code = status_code
1098
self.message = message
1099
1100
1101
def get_picture_query(query_data):
1102
query = db.session.query(PictureResource)
1103
1104
requirement_conditions = {
1105
"has_object": lambda value: PictureResource.regions.any(
1106
PictureRegion.object_id.in_(value)),
1107
"nature": lambda value: PictureResource.nature_id.in_(value),
1108
"licence": lambda value: PictureResource.licences.any(
1109
PictureLicence.licence_id.in_(value)),
1110
"author": lambda value: PictureResource.author_name.in_(value),
1111
"title": lambda value: PictureResource.title.ilike(value),
1112
"description": lambda value: PictureResource.description.ilike(value),
1113
"origin_url": lambda value: db.func.lower(db.func.substr(
1114
PictureResource.origin_url,
1115
db.func.length(db.func.split_part(PictureResource.origin_url, "://", 1)) + 4
1116
)).in_(value),
1117
"above_width": lambda value: PictureResource.width >= value,
1118
"below_width": lambda value: PictureResource.width <= value,
1119
"above_height": lambda value: PictureResource.height >= value,
1120
"below_height": lambda value: PictureResource.height <= value,
1121
"before_date": lambda value: PictureResource.timestamp <= datetime.utcfromtimestamp(
1122
value),
1123
"after_date": lambda value: PictureResource.timestamp >= datetime.utcfromtimestamp(
1124
value),
1125
"in_gallery": lambda value: PictureResource.galleries.any(PictureInGallery.gallery_id.in_(value)),
1126
}
1127
if "want" in query_data:
1128
for i in query_data["want"]:
1129
if len(i) != 1:
1130
raise APIError(400, "Each requirement 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 "exclude" in query_data:
1138
for i in query_data["exclude"]:
1139
if len(i) != 1:
1140
raise APIError(400, "Each exclusion must have exactly one key")
1141
requirement, value = list(i.items())[0]
1142
if requirement not in requirement_conditions:
1143
raise APIError(400, f"Unknown requirement type: {requirement}")
1144
1145
condition = requirement_conditions[requirement]
1146
query = query.filter(~condition(value))
1147
if not query_data.get("include_obsolete", False):
1148
query = query.filter(PictureResource.replaced_by_id.is_(None))
1149
1150
return query
1151
1152
1153
@app.route("/query-pictures")
1154
def graphical_query_pictures():
1155
return flask.render_template("graphical-query-pictures.html")
1156
1157
1158
@app.route("/query-pictures-results")
1159
def graphical_query_pictures_results():
1160
query_yaml = flask.request.args.get("query", "")
1161
yaml_parser = yaml.YAML()
1162
query_data = yaml_parser.load(query_yaml) or {}
1163
try:
1164
query = get_picture_query(query_data)
1165
except APIError as e:
1166
flask.abort(e.status_code)
1167
1168
page = int(flask.request.args.get("page", 1))
1169
per_page = int(flask.request.args.get("per_page", 16))
1170
1171
resources = query.paginate(page=page, per_page=per_page)
1172
1173
return flask.render_template("graphical-query-pictures-results.html", resources=resources,
1174
query=query_yaml,
1175
page_number=page, page_length=per_page,
1176
num_pages=resources.pages,
1177
prev_page=resources.prev_num, next_page=resources.next_num)
1178
1179
1180
@app.route("/raw/picture/<int:id>")
1181
def raw_picture(id):
1182
resource = db.session.get(PictureResource, id)
1183
if resource is None:
1184
flask.abort(404)
1185
1186
response = flask.send_from_directory(path.join(config.DATA_PATH, "pictures"),
1187
str(resource.id))
1188
response.mimetype = resource.file_format
1189
1190
return response
1191
1192
1193
@app.route("/object/")
1194
def graphical_object_types():
1195
return flask.render_template("object-types.html", objects=PictureObject.query.all())
1196
1197
1198
@app.route("/api/object-types")
1199
def object_types():
1200
objects = db.session.query(PictureObject).all()
1201
return flask.jsonify({object.id: object.description for object in objects})
1202
1203
1204
@app.route("/api/query-pictures", methods=["POST"]) # sadly GET can't have a body
1205
def query_pictures():
1206
offset = int(flask.request.args.get("offset", 0))
1207
limit = int(flask.request.args.get("limit", 16))
1208
ordering = flask.request.args.get("ordering", "date-desc")
1209
1210
yaml_parser = yaml.YAML()
1211
query_data = yaml_parser.load(flask.request.data) or {}
1212
try:
1213
query = get_picture_query(query_data)
1214
except APIError as e:
1215
return flask.jsonify({"error": e.message}), e.status_code
1216
1217
match ordering:
1218
case "date-desc":
1219
query = query.order_by(PictureResource.timestamp.desc())
1220
case "date-asc":
1221
query = query.order_by(PictureResource.timestamp.asc())
1222
case "title-asc":
1223
query = query.order_by(PictureResource.title.asc())
1224
case "title-desc":
1225
query = query.order_by(PictureResource.title.desc())
1226
case "random":
1227
query = query.order_by(db.func.random())
1228
case "number-regions-desc":
1229
query = query.order_by(db.func.count(PictureResource.regions).desc())
1230
case "number-regions-asc":
1231
query = query.order_by(db.func.count(PictureResource.regions).asc())
1232
1233
query = query.offset(offset).limit(limit)
1234
resources = query.all()
1235
1236
json_response = {
1237
"date_generated": datetime.utcnow().timestamp(),
1238
"resources": [],
1239
"offset": offset,
1240
"limit": limit,
1241
}
1242
1243
json_resources = json_response["resources"]
1244
1245
for resource in resources:
1246
json_resource = {
1247
"id": resource.id,
1248
"title": resource.title,
1249
"description": resource.description,
1250
"timestamp": resource.timestamp.timestamp(),
1251
"origin_url": resource.origin_url,
1252
"author": resource.author_name,
1253
"file_format": resource.file_format,
1254
"width": resource.width,
1255
"height": resource.height,
1256
"nature": resource.nature_id,
1257
"licences": [licence.licence_id for licence in resource.licences],
1258
"replaces": resource.replaces_id,
1259
"replaced_by": resource.replaced_by_id,
1260
"regions": [],
1261
"download": config.ROOT_URL + flask.url_for("raw_picture", id=resource.id),
1262
}
1263
for region in resource.regions:
1264
json_resource["regions"].append({
1265
"object": region.object_id,
1266
"type": region.json["type"],
1267
"shape": region.json["shape"],
1268
})
1269
1270
json_resources.append(json_resource)
1271
1272
return flask.jsonify(json_response)
1273
1274
1275
@app.route("/api/picture/<int:id>/")
1276
def api_picture(id):
1277
resource = db.session.get(PictureResource, id)
1278
if resource is None:
1279
flask.abort(404)
1280
1281
json_resource = {
1282
"id": resource.id,
1283
"title": resource.title,
1284
"description": resource.description,
1285
"timestamp": resource.timestamp.timestamp(),
1286
"origin_url": resource.origin_url,
1287
"author": resource.author_name,
1288
"file_format": resource.file_format,
1289
"width": resource.width,
1290
"height": resource.height,
1291
"nature": resource.nature_id,
1292
"licences": [licence.licence_id for licence in resource.licences],
1293
"replaces": resource.replaces_id,
1294
"replaced_by": resource.replaced_by_id,
1295
"regions": [],
1296
"download": config.ROOT_URL + flask.url_for("raw_picture", id=resource.id),
1297
}
1298
for region in resource.regions:
1299
json_resource["regions"].append({
1300
"object": region.object_id,
1301
"type": region.json["type"],
1302
"shape": region.json["shape"],
1303
})
1304
1305
return flask.jsonify(json_resource)
1306
1307
1308
@app.route("/api/licence/")
1309
def api_licences():
1310
licences = db.session.query(Licence).all()
1311
json_licences = {
1312
licence.id: {
1313
"title": licence.title,
1314
"free": licence.free,
1315
"pinned": licence.pinned,
1316
} for licence in licences
1317
}
1318
1319
return flask.jsonify(json_licences)
1320
1321
1322
@app.route("/api/licence/<id>/")
1323
def api_licence(id):
1324
licence = db.session.get(Licence, id)
1325
if licence is None:
1326
flask.abort(404)
1327
1328
json_licence = {
1329
"id": licence.id,
1330
"title": licence.title,
1331
"description": licence.description,
1332
"info_url": licence.info_url,
1333
"legalese_url": licence.url,
1334
"free": licence.free,
1335
"logo_url": licence.logo_url,
1336
"pinned": licence.pinned,
1337
}
1338
1339
return flask.jsonify(json_licence)
1340
1341
1342
@app.route("/api/nature/")
1343
def api_natures():
1344
natures = db.session.query(PictureNature).all()
1345
json_natures = {
1346
nature.id: nature.description for nature in natures
1347
}
1348
1349
return flask.jsonify(json_natures)
1350
1351
1352
@app.route("/api/user/")
1353
def api_users():
1354
offset = int(flask.request.args.get("offset", 0))
1355
limit = int(flask.request.args.get("limit", 16))
1356
1357
users = db.session.query(User).offset(offset).limit(limit).all()
1358
1359
json_users = {
1360
user.username: {
1361
"admin": user.admin,
1362
} for user in users
1363
}
1364
1365
return flask.jsonify(json_users)
1366
1367
1368
@app.route("/api/user/<username>/")
1369
def api_user(username):
1370
user = db.session.get(User, username)
1371
if user is None:
1372
flask.abort(404)
1373
1374
json_user = {
1375
"username": user.username,
1376
"admin": user.admin,
1377
"joined": user.joined_timestamp.timestamp(),
1378
}
1379
1380
return flask.jsonify(json_user)
1381
1382
1383
@app.route("/api/login", methods=["POST"])
1384
def api_login():
1385
username = flask.request.json["username"]
1386
password = flask.request.json["password"]
1387
1388
user = db.session.get(User, username)
1389
1390
if user is None:
1391
return flask.jsonify({"error": "This username is not registered. To prevent spam, you must use the HTML interface to register."}), 401
1392
1393
if not bcrypt.check_password_hash(user.password_hashed, password):
1394
return flask.jsonify({"error": "Incorrect password"}), 401
1395
1396
flask.session["username"] = username
1397
1398
return flask.jsonify({"message": "You have been logged in. Your HTTP client must support cookies to use features of this API that require authentication."})
1399
1400
1401
@app.route("/api/logout", methods=["POST"])
1402
def api_logout():
1403
flask.session.pop("username", None)
1404
return flask.jsonify({"message": "You have been logged out."})
1405
1406
1407
@app.route("/api/upload", methods=["POST"])
1408
def api_upload():
1409
if "username" not in flask.session:
1410
return flask.jsonify({"error": "You must be logged in to upload pictures"}), 401
1411
1412
json_ = json.loads(flask.request.form["json"])
1413
title = json_["title"]
1414
description = json_.get("description", "")
1415
origin_url = json_.get("origin_url", "")
1416
author = db.session.get(User, flask.session["username"])
1417
licence_ids = json_["licence"]
1418
nature_id = json_["nature"]
1419
file = flask.request.files["file"]
1420
1421
if not file or not file.filename:
1422
return flask.jsonify({"error": "An image file must be uploaded"}), 400
1423
1424
if not file.mimetype.startswith("image/") or file.mimetype == "image/svg+xml":
1425
return flask.jsonify({"error": "Only bitmap images are supported"}), 400
1426
1427
if not title:
1428
return flask.jsonify({"error": "Give a title"}), 400
1429
1430
if not description:
1431
description = ""
1432
1433
if not nature_id:
1434
return flask.jsonify({"error": "Give a picture type"}), 400
1435
1436
if not licence_ids:
1437
return flask.jsonify({"error": "Give licences"}), 400
1438
1439
licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]
1440
if not any(licence.free for licence in licences):
1441
return flask.jsonify({"error": "Use at least one free licence"}), 400
1442
1443
resource = PictureResource(title, author, description, origin_url, licence_ids,
1444
file.mimetype,
1445
db.session.get(PictureNature, nature_id))
1446
db.session.add(resource)
1447
db.session.commit()
1448
file.save(path.join(config.DATA_PATH, "pictures", str(resource.id)))
1449
pil_image = Image.open(path.join(config.DATA_PATH, "pictures", str(resource.id)))
1450
resource.width, resource.height = pil_image.size
1451
db.session.commit()
1452
1453
if json_.get("annotations"):
1454
try:
1455
resource.put_annotations(json_["annotations"])
1456
db.session.commit()
1457
except json.JSONDecodeError:
1458
return flask.jsonify({"error": "Invalid annotations"}), 400
1459
1460
return flask.jsonify({"message": "Picture uploaded successfully"})
1461
1462
1463
@app.route("/api/picture/<int:id>/update", methods=["POST"])
1464
def api_update_picture(id):
1465
resource = db.session.get(PictureResource, id)
1466
if resource is None:
1467
return flask.jsonify({"error": "Picture not found"}), 404
1468
current_user = db.session.get(User, flask.session.get("username"))
1469
if current_user is None:
1470
return flask.jsonify({"error": "You must be logged in to edit pictures"}), 401
1471
if resource.author != current_user and not current_user.admin:
1472
return flask.jsonify({"error": "You are not the author of this picture"}), 403
1473
1474
title = flask.request.json.get("title", resource.title)
1475
description = flask.request.json.get("description", resource.description)
1476
origin_url = flask.request.json.get("origin_url", resource.origin_url)
1477
licence_ids = flask.request.json.get("licence", [licence.licence_id for licence in resource.licences])
1478
nature_id = flask.request.json.get("nature", resource.nature_id)
1479
1480
if not title:
1481
return flask.jsonify({"error": "Give a title"}), 400
1482
1483
if not description:
1484
description = ""
1485
1486
if not nature_id:
1487
return flask.jsonify({"error": "Give a picture type"}), 400
1488
1489
if not licence_ids:
1490
return flask.jsonify({"error": "Give licences"}), 400
1491
1492
licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]
1493
1494
if not any(licence.free for licence in licences):
1495
return flask.jsonify({"error": "Use at least one free licence"}), 400
1496
1497
resource.title = title
1498
resource.description = description
1499
resource.origin_url = origin_url
1500
resource.licences = licences
1501
resource.nature = db.session.get(PictureNature, nature_id)
1502
1503
db.session.commit()
1504
1505
return flask.jsonify({"message": "Picture updated successfully"})
1506
1507