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