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 • 36.96 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
if resource.replaces:
595
resource.replaces.replaced_by = None
596
if resource.replaced_by:
597
resource.replaced_by.replaces = None
598
resource.copied_from = None
599
for copy in resource.copies:
600
copy.copied_from = None
601
db.session.delete(resource)
602
db.session.commit()
603
604
return flask.redirect("/")
605
606
607
@app.route("/picture/<int:id>/mark-replacement", methods=["POST"])
608
def mark_picture_replacement(id):
609
resource = db.session.get(PictureResource, id)
610
if resource is None:
611
flask.abort(404)
612
613
current_user = db.session.get(User, flask.session.get("username"))
614
if current_user is None:
615
flask.abort(401)
616
617
if resource.copied_from.author != current_user and not current_user.admin:
618
flask.abort(403)
619
620
resource.copied_from.replaced_by = resource
621
resource.replaces = resource.copied_from
622
623
db.session.commit()
624
625
return flask.redirect("/picture/" + str(resource.copied_from.id))
626
627
628
@app.route("/picture/<int:id>/remove-replacement", methods=["POST"])
629
def remove_picture_replacement(id):
630
resource = db.session.get(PictureResource, id)
631
if resource is None:
632
flask.abort(404)
633
634
current_user = db.session.get(User, flask.session.get("username"))
635
if current_user is None:
636
flask.abort(401)
637
638
if resource.author != current_user and not current_user.admin:
639
flask.abort(403)
640
641
resource.replaced_by.replaces = None
642
resource.replaced_by = None
643
644
db.session.commit()
645
646
return flask.redirect("/picture/" + str(resource.id))
647
648
649
@app.route("/picture/<int:id>/edit-metadata")
650
def edit_picture(id):
651
resource = db.session.get(PictureResource, id)
652
if resource is None:
653
flask.abort(404)
654
655
current_user = db.session.get(User, flask.session.get("username"))
656
if current_user is None:
657
flask.abort(401)
658
659
if resource.author != current_user and not current_user.admin:
660
flask.abort(403)
661
662
licences = Licence.query.order_by(Licence.free.desc(), Licence.pinned.desc(), Licence.title).all()
663
664
types = PictureNature.query.all()
665
666
return flask.render_template("edit-picture.html", resource=resource, licences=licences, types=types,
667
PictureLicence=PictureLicence)
668
669
670
@app.route("/picture/<int:id>/edit-metadata", methods=["POST"])
671
def edit_picture_post(id):
672
resource = db.session.get(PictureResource, id)
673
if resource is None:
674
flask.abort(404)
675
676
current_user = db.session.get(User, flask.session.get("username"))
677
if current_user is None:
678
flask.abort(401)
679
680
if resource.author != current_user and not current_user.admin:
681
flask.abort(403)
682
683
title = flask.request.form["title"]
684
description = flask.request.form["description"]
685
origin_url = flask.request.form["origin_url"]
686
licence_ids = flask.request.form.getlist("licence")
687
nature_id = flask.request.form["nature"]
688
689
if not title:
690
flask.flash("Enter a title")
691
return flask.redirect(flask.request.url)
692
693
if not description:
694
description = ""
695
696
if not nature_id:
697
flask.flash("Select a picture type")
698
return flask.redirect(flask.request.url)
699
700
if not licence_ids:
701
flask.flash("Select licences")
702
return flask.redirect(flask.request.url)
703
704
licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids]
705
if not any(licence.free for licence in licences):
706
flask.flash("Select at least one free licence")
707
return flask.redirect(flask.request.url)
708
709
resource.title = title
710
resource.description = description
711
resource.origin_url = origin_url
712
for licence_id in licence_ids:
713
joiner = PictureLicence(resource, db.session.get(Licence, licence_id))
714
db.session.add(joiner)
715
resource.nature = db.session.get(PictureNature, nature_id)
716
717
db.session.commit()
718
719
return flask.redirect("/picture/" + str(resource.id))
720
721
722
@app.route("/picture/<int:id>/copy")
723
def copy_picture(id):
724
resource = db.session.get(PictureResource, id)
725
if resource is None:
726
flask.abort(404)
727
728
current_user = db.session.get(User, flask.session.get("username"))
729
if current_user is None:
730
flask.abort(401)
731
732
new_resource = PictureResource(resource.title, current_user, resource.description, resource.origin_url,
733
[licence.licence_id for licence in resource.licences], resource.file_format,
734
resource.nature)
735
736
for region in resource.regions:
737
db.session.add(PictureRegion(region.json, new_resource, region.object))
738
739
db.session.commit()
740
741
# Create a hard link for the new picture
742
old_path = path.join(config.DATA_PATH, "pictures", str(resource.id))
743
new_path = path.join(config.DATA_PATH, "pictures", str(new_resource.id))
744
os.link(old_path, new_path)
745
746
new_resource.width = resource.width
747
new_resource.height = resource.height
748
new_resource.copied_from = resource
749
750
db.session.commit()
751
752
return flask.redirect("/picture/" + str(new_resource.id))
753
754
755
@app.route("/gallery/<int:id>/")
756
def gallery(id):
757
gallery = db.session.get(Gallery, id)
758
if gallery is None:
759
flask.abort(404)
760
761
return flask.render_template("gallery.html", gallery=gallery)
762
763
764
@app.route("/create-gallery")
765
def create_gallery():
766
if "username" not in flask.session:
767
flask.flash("Log in to create galleries.")
768
return flask.redirect("/accounts")
769
770
return flask.render_template("create-gallery.html")
771
772
773
@app.route("/create-gallery", methods=["POST"])
774
def create_gallery_post():
775
if not flask.session.get("username"):
776
flask.abort(401)
777
778
if not flask.request.form.get("title"):
779
flask.flash("Enter a title")
780
return flask.redirect(flask.request.url)
781
782
description = flask.request.form.get("description", "")
783
784
gallery = Gallery(flask.request.form["title"], description, db.session.get(User, flask.session["username"]))
785
db.session.add(gallery)
786
db.session.commit()
787
788
return flask.redirect("/gallery/" + str(gallery.id))
789
790
791
@app.route("/gallery/<int:id>/add-picture", methods=["POST"])
792
def gallery_add_picture(id):
793
gallery = db.session.get(Gallery, id)
794
if gallery is None:
795
flask.abort(404)
796
797
if "username" not in flask.session:
798
flask.abort(401)
799
800
if flask.session["username"] != gallery.owner_name and not db.session.get(User, flask.session["username"]).admin:
801
flask.abort(403)
802
803
picture_id = flask.request.form.get("picture_id")
804
if "/" in picture_id: # also allow full URLs
805
picture_id = picture_id.rstrip("/").rpartition("/")[1]
806
if not picture_id:
807
flask.flash("Select a picture")
808
return flask.redirect("/gallery/" + str(gallery.id))
809
picture_id = int(picture_id)
810
811
picture = db.session.get(PictureResource, picture_id)
812
if picture is None:
813
flask.flash("Invalid picture")
814
return flask.redirect("/gallery/" + str(gallery.id))
815
816
if PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first():
817
flask.flash("This picture is already in the gallery")
818
return flask.redirect("/gallery/" + str(gallery.id))
819
820
db.session.add(PictureInGallery(picture, gallery))
821
822
db.session.commit()
823
824
return flask.redirect("/gallery/" + str(gallery.id))
825
826
827
def get_picture_query(query_data):
828
query = db.session.query(PictureResource)
829
830
requirement_conditions = {
831
"has_object": lambda value: PictureResource.regions.any(
832
PictureRegion.object_id.in_(value)),
833
"nature": lambda value: PictureResource.nature_id.in_(value),
834
"licence": lambda value: PictureResource.licences.any(
835
PictureLicence.licence_id.in_(value)),
836
"author": lambda value: PictureResource.author_name.in_(value),
837
"title": lambda value: PictureResource.title.ilike(value),
838
"description": lambda value: PictureResource.description.ilike(value),
839
"origin_url": lambda value: db.func.lower(db.func.substr(
840
PictureResource.origin_url,
841
db.func.length(db.func.split_part(PictureResource.origin_url, "://", 1)) + 4
842
)).in_(value),
843
"above_width": lambda value: PictureResource.width >= value,
844
"below_width": lambda value: PictureResource.width <= value,
845
"above_height": lambda value: PictureResource.height >= value,
846
"below_height": lambda value: PictureResource.height <= value,
847
"before_date": lambda value: PictureResource.timestamp <= datetime.utcfromtimestamp(
848
value),
849
"after_date": lambda value: PictureResource.timestamp >= datetime.utcfromtimestamp(
850
value)
851
}
852
if "want" in query_data:
853
for i in query_data["want"]:
854
requirement, value = list(i.items())[0]
855
condition = requirement_conditions.get(requirement)
856
if condition:
857
query = query.filter(condition(value))
858
if "exclude" in query_data:
859
for i in query_data["exclude"]:
860
requirement, value = list(i.items())[0]
861
condition = requirement_conditions.get(requirement)
862
if condition:
863
query = query.filter(~condition(value))
864
if not query_data.get("include_obsolete", False):
865
query = query.filter(PictureResource.replaced_by_id.is_(None))
866
867
return query
868
869
870
@app.route("/query-pictures")
871
def graphical_query_pictures():
872
return flask.render_template("graphical-query-pictures.html")
873
874
875
@app.route("/query-pictures-results")
876
def graphical_query_pictures_results():
877
query_yaml = flask.request.args.get("query", "")
878
yaml_parser = yaml.YAML()
879
query_data = yaml_parser.load(query_yaml) or {}
880
query = get_picture_query(query_data)
881
882
page = int(flask.request.args.get("page", 1))
883
per_page = int(flask.request.args.get("per_page", 16))
884
885
resources = query.paginate(page=page, per_page=per_page)
886
887
return flask.render_template("graphical-query-pictures-results.html", resources=resources, query=query_yaml,
888
page_number=page, page_length=per_page, num_pages=resources.pages,
889
prev_page=resources.prev_num, next_page=resources.next_num)
890
891
892
@app.route("/raw/picture/<int:id>")
893
def raw_picture(id):
894
resource = db.session.get(PictureResource, id)
895
if resource is None:
896
flask.abort(404)
897
898
response = flask.send_from_directory(path.join(config.DATA_PATH, "pictures"), str(resource.id))
899
response.mimetype = resource.file_format
900
901
return response
902
903
904
@app.route("/object/")
905
def graphical_object_types():
906
return flask.render_template("object-types.html", objects=PictureObject.query.all())
907
908
909
@app.route("/api/object-types")
910
def object_types():
911
objects = db.session.query(PictureObject).all()
912
return flask.jsonify({object.id: object.description for object in objects})
913
914
915
@app.route("/api/query-pictures", methods=["POST"]) # sadly GET can't have a body
916
def query_pictures():
917
offset = int(flask.request.args.get("offset", 0))
918
limit = int(flask.request.args.get("limit", 16))
919
ordering = flask.request.args.get("ordering", "date-desc")
920
921
yaml_parser = yaml.YAML()
922
query_data = yaml_parser.load(flask.request.data) or {}
923
query = get_picture_query(query_data)
924
925
match ordering:
926
case "date-desc":
927
query = query.order_by(PictureResource.timestamp.desc())
928
case "date-asc":
929
query = query.order_by(PictureResource.timestamp.asc())
930
case "title-asc":
931
query = query.order_by(PictureResource.title.asc())
932
case "title-desc":
933
query = query.order_by(PictureResource.title.desc())
934
case "random":
935
query = query.order_by(db.func.random())
936
case "number-regions-desc":
937
query = query.order_by(db.func.count(PictureResource.regions).desc())
938
case "number-regions-asc":
939
query = query.order_by(db.func.count(PictureResource.regions).asc())
940
941
query = query.offset(offset).limit(limit)
942
resources = query.all()
943
944
json_response = {
945
"date_generated": datetime.utcnow().timestamp(),
946
"resources": [],
947
"offset": offset,
948
"limit": limit,
949
}
950
951
json_resources = json_response["resources"]
952
953
for resource in resources:
954
json_resource = {
955
"id": resource.id,
956
"title": resource.title,
957
"description": resource.description,
958
"timestamp": resource.timestamp.timestamp(),
959
"origin_url": resource.origin_url,
960
"author": resource.author_name,
961
"file_format": resource.file_format,
962
"width": resource.width,
963
"height": resource.height,
964
"nature": resource.nature_id,
965
"licences": [licence.licence_id for licence in resource.licences],
966
"replaces": resource.replaces_id,
967
"replaced_by": resource.replaced_by_id,
968
"regions": [],
969
"download": config.ROOT_URL + flask.url_for("raw_picture", id=resource.id),
970
}
971
for region in resource.regions:
972
json_resource["regions"].append({
973
"object": region.object_id,
974
"type": region.json["type"],
975
"shape": region.json["shape"],
976
})
977
978
json_resources.append(json_resource)
979
980
return flask.jsonify(json_response)
981
982
983
@app.route("/api/picture/<int:id>/")
984
def api_picture(id):
985
resource = db.session.get(PictureResource, id)
986
if resource is None:
987
flask.abort(404)
988
989
json_resource = {
990
"id": resource.id,
991
"title": resource.title,
992
"description": resource.description,
993
"timestamp": resource.timestamp.timestamp(),
994
"origin_url": resource.origin_url,
995
"author": resource.author_name,
996
"file_format": resource.file_format,
997
"width": resource.width,
998
"height": resource.height,
999
"nature": resource.nature_id,
1000
"licences": [licence.licence_id for licence in resource.licences],
1001
"replaces": resource.replaces_id,
1002
"replaced_by": resource.replaced_by_id,
1003
"regions": [],
1004
"download": config.ROOT_URL + flask.url_for("raw_picture", id=resource.id),
1005
}
1006
for region in resource.regions:
1007
json_resource["regions"].append({
1008
"object": region.object_id,
1009
"type": region.json["type"],
1010
"shape": region.json["shape"],
1011
})
1012
1013
return flask.jsonify(json_resource)
1014
1015
1016
@app.route("/api/licence/")
1017
def api_licences():
1018
licences = db.session.query(Licence).all()
1019
json_licences = {
1020
licence.id: {
1021
"title": licence.title,
1022
"free": licence.free,
1023
"pinned": licence.pinned,
1024
} for licence in licences
1025
}
1026
1027
return flask.jsonify(json_licences)
1028
1029
1030
@app.route("/api/licence/<id>/")
1031
def api_licence(id):
1032
licence = db.session.get(Licence, id)
1033
if licence is None:
1034
flask.abort(404)
1035
1036
json_licence = {
1037
"id": licence.id,
1038
"title": licence.title,
1039
"description": licence.description,
1040
"info_url": licence.info_url,
1041
"legalese_url": licence.url,
1042
"free": licence.free,
1043
"logo_url": licence.logo_url,
1044
"pinned": licence.pinned,
1045
}
1046
1047
return flask.jsonify(json_licence)
1048
1049
1050
@app.route("/api/nature/")
1051
def api_natures():
1052
natures = db.session.query(PictureNature).all()
1053
json_natures = {
1054
nature.id: nature.description for nature in natures
1055
}
1056
1057
return flask.jsonify(json_natures)
1058
1059
1060
@app.route("/api/user/")
1061
def api_users():
1062
offset = int(flask.request.args.get("offset", 0))
1063
limit = int(flask.request.args.get("limit", 16))
1064
1065
users = db.session.query(User).offset(offset).limit(limit).all()
1066
1067
json_users = {
1068
user.username: {
1069
"admin": user.admin,
1070
} for user in users
1071
}
1072
1073
return flask.jsonify(json_users)
1074
1075
1076
@app.route("/api/user/<username>/")
1077
def api_user(username):
1078
user = db.session.get(User, username)
1079
if user is None:
1080
flask.abort(404)
1081
1082
json_user = {
1083
"username": user.username,
1084
"admin": user.admin,
1085
"joined": user.joined_timestamp.timestamp(),
1086
}
1087
1088
return flask.jsonify(json_user)
1089
1090