app.py
Python script, ASCII text executable
1import json 2import os 3import mimetypes 4import flask 5import ruamel.yaml as yaml 6import sqlalchemy.dialects.postgresql 7import config 8import markdown 9 10from datetime import datetime 11from os import path 12from flask_sqlalchemy import SQLAlchemy 13from flask_bcrypt import Bcrypt 14from flask_migrate import Migrate, current 15from urllib.parse import urlencode 16from PIL import Image 17from sqlalchemy.sql.functions import current_user 18 19app = flask.Flask(__name__) 20bcrypt = Bcrypt(app) 21 22app.config["SQLALCHEMY_DATABASE_URI"] = config.DB_URI 23app.config["SECRET_KEY"] = config.DB_PASSWORD 24 25db = SQLAlchemy(app) 26migrate = Migrate(app, db) 27 28 29@app.template_filter("split") 30def split(value, separator=None, maxsplit=-1): 31return value.split(separator, maxsplit) 32 33 34@app.template_filter("median") 35def median(value): 36value = list(value) # prevent generators 37return sorted(value)[len(value) // 2] 38 39 40@app.template_filter("set") 41def set_filter(value): 42return set(value) 43 44 45@app.template_global() 46def modify_query(**new_values): 47args = flask.request.args.copy() 48for key, value in new_values.items(): 49args[key] = value 50 51return f"{flask.request.path}?{urlencode(args)}" 52 53 54@app.context_processor 55def default_variables(): 56return { 57"current_user": db.session.get(User, flask.session.get("username")), 58} 59 60 61with app.app_context(): 62class User(db.Model): 63username = db.Column(db.String(32), unique=True, nullable=False, primary_key=True) 64password_hashed = db.Column(db.String(60), nullable=False) 65admin = db.Column(db.Boolean, nullable=False, default=False, server_default="false") 66pictures = db.relationship("PictureResource", back_populates="author") 67joined_timestamp = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) 68galleries = db.relationship("Gallery", back_populates="owner") 69galleries_joined = db.relationship("UserInGallery", back_populates="user") 70ratings = db.relationship("PictureRating", back_populates="user") 71 72def __init__(self, username, password): 73self.username = username 74self.password_hashed = bcrypt.generate_password_hash(password).decode("utf-8") 75 76@property 77def formatted_name(self): 78if self.admin: 79return self.username + "*" 80return self.username 81 82 83class Licence(db.Model): 84id = db.Column(db.String(64), primary_key=True) # SPDX identifier 85title = db.Column(db.UnicodeText, nullable=False) # the official name of the licence 86description = db.Column(db.UnicodeText, 87nullable=False) # brief description of its permissions and restrictions 88info_url = db.Column(db.String(1024), 89nullable=False) # the URL to a page with general information about the licence 90url = db.Column(db.String(1024), 91nullable=True) # the URL to a page with the full text of the licence and more information 92pictures = db.relationship("PictureLicence", back_populates="licence") 93free = db.Column(db.Boolean, nullable=False, 94default=False) # whether the licence is free or not 95logo_url = db.Column(db.String(1024), nullable=True) # URL to the logo of the licence 96pinned = db.Column(db.Boolean, nullable=False, 97default=False) # whether the licence should be shown at the top of the list 98 99def __init__(self, id, title, description, info_url, url, free, logo_url=None, 100pinned=False): 101self.id = id 102self.title = title 103self.description = description 104self.info_url = info_url 105self.url = url 106self.free = free 107self.logo_url = logo_url 108self.pinned = pinned 109 110 111class PictureLicence(db.Model): 112id = db.Column(db.Integer, primary_key=True, autoincrement=True) 113 114resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id")) 115licence_id = db.Column(db.String(64), db.ForeignKey("licence.id")) 116 117resource = db.relationship("PictureResource", back_populates="licences") 118licence = db.relationship("Licence", back_populates="pictures") 119 120def __init__(self, resource, licence): 121self.resource = resource 122self.licence = licence 123 124 125class Resource(db.Model): 126__abstract__ = True 127 128id = db.Column(db.Integer, primary_key=True, autoincrement=True) 129title = db.Column(db.UnicodeText, nullable=False) 130description = db.Column(db.UnicodeText, nullable=False) 131timestamp = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) 132origin_url = db.Column(db.String(2048), 133nullable=True) # should be left empty if it's original or the source is unknown but public domain 134 135 136class PictureNature(db.Model): 137# Examples: 138# "photo", "paper-scan", "2d-art-photo", "sculpture-photo", "computer-3d", "computer-painting", 139# "computer-line-art", "diagram", "infographic", "text", "map", "chart-graph", "screen-capture", 140# "screen-photo", "pattern", "collage", "ai", and so on 141id = db.Column(db.String(64), primary_key=True) 142description = db.Column(db.UnicodeText, nullable=False) 143resources = db.relationship("PictureResource", back_populates="nature") 144 145def __init__(self, id, description): 146self.id = id 147self.description = description 148 149 150class PictureObjectInheritance(db.Model): 151parent_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"), 152primary_key=True) 153child_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"), 154primary_key=True) 155 156parent = db.relationship("PictureObject", foreign_keys=[parent_id], 157back_populates="child_links") 158child = db.relationship("PictureObject", foreign_keys=[child_id], 159back_populates="parent_links") 160 161def __init__(self, parent, child): 162self.parent = parent 163self.child = child 164 165 166class PictureObject(db.Model): 167id = db.Column(db.String(64), primary_key=True) 168description = db.Column(db.UnicodeText, nullable=False) 169 170child_links = db.relationship("PictureObjectInheritance", 171foreign_keys=[PictureObjectInheritance.parent_id], 172back_populates="parent") 173parent_links = db.relationship("PictureObjectInheritance", 174foreign_keys=[PictureObjectInheritance.child_id], 175back_populates="child") 176 177def __init__(self, id, description, parents): 178self.id = id 179self.description = description 180if parents: 181for parent in parents: 182db.session.add(PictureObjectInheritance(parent, self)) 183 184 185class PictureRegion(db.Model): 186# This is for picture region annotations 187id = db.Column(db.Integer, primary_key=True, autoincrement=True) 188json = db.Column(sqlalchemy.dialects.postgresql.JSONB, nullable=False) 189 190resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), 191nullable=False) 192object_id = db.Column(db.String(64), db.ForeignKey("picture_object.id"), nullable=True) 193 194resource = db.relationship("PictureResource", backref="regions") 195object = db.relationship("PictureObject", backref="regions") 196 197def __init__(self, json, resource, object): 198self.json = json 199self.resource = resource 200self.object = object 201 202 203class PictureResource(Resource): 204# This is only for bitmap pictures. Vectors will be stored under a different model 205# File name is the ID in the picture directory under data, without an extension 206file_format = db.Column(db.String(64), nullable=False) # MIME type 207width = db.Column(db.Integer, nullable=False) 208height = db.Column(db.Integer, nullable=False) 209nature_id = db.Column(db.String(32), db.ForeignKey("picture_nature.id"), nullable=True) 210author_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 211author = db.relationship("User", back_populates="pictures") 212 213nature = db.relationship("PictureNature", back_populates="resources") 214 215replaces_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), nullable=True) 216replaced_by_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), 217nullable=True) 218 219replaces = db.relationship("PictureResource", remote_side="PictureResource.id", 220foreign_keys=[replaces_id], back_populates="replaced_by", 221post_update=True) 222replaced_by = db.relationship("PictureResource", remote_side="PictureResource.id", 223foreign_keys=[replaced_by_id], post_update=True) 224 225copied_from_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), 226nullable=True) 227copied_from = db.relationship("PictureResource", remote_side="PictureResource.id", 228backref="copies", foreign_keys=[copied_from_id]) 229 230licences = db.relationship("PictureLicence", back_populates="resource") 231galleries = db.relationship("PictureInGallery", back_populates="resource") 232ratings = db.relationship("PictureRating", back_populates="resource") 233 234def __init__(self, title, author, description, origin_url, licence_ids, mime, 235nature=None): 236self.title = title 237self.author = author 238self.description = description 239self.origin_url = origin_url 240self.file_format = mime 241self.width = self.height = 0 242self.nature = nature 243db.session.add(self) 244db.session.commit() 245for licence_id in licence_ids: 246joiner = PictureLicence(self, db.session.get(Licence, licence_id)) 247db.session.add(joiner) 248 249def put_annotations(self, json): 250# Delete all previous annotations 251db.session.query(PictureRegion).filter_by(resource_id=self.id).delete() 252 253for region in json: 254object_id = region["object"] 255picture_object = db.session.get(PictureObject, object_id) 256 257region_data = { 258"type": region["type"], 259"shape": region["shape"], 260} 261 262region_row = PictureRegion(region_data, self, picture_object) 263db.session.add(region_row) 264 265@property 266def average_rating(self): 267if not self.ratings: 268return None 269return db.session.query(db.func.avg(PictureRating.rating)).filter_by(resource=self).scalar() 270 271@property 272def rating_totals(self): 273all_ratings = db.session.query(PictureRating.rating).filter_by(resource=self) 274return {rating: all_ratings.filter_by(rating=rating).count() for rating in range(1, 6)} 275 276@property 277def stars(self): 278if not self.ratings: 279return 0 280average = self.average_rating 281whole_stars = int(average) 282partial_star = average - whole_stars 283 284return [100] * whole_stars + [int(partial_star * 100)] + [0] * (4 - whole_stars) 285 286 287class PictureInGallery(db.Model): 288id = db.Column(db.Integer, primary_key=True, autoincrement=True) 289resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), 290nullable=False) 291gallery_id = db.Column(db.Integer, db.ForeignKey("gallery.id"), nullable=False) 292 293resource = db.relationship("PictureResource") 294gallery = db.relationship("Gallery") 295 296def __init__(self, resource, gallery): 297self.resource = resource 298self.gallery = gallery 299 300 301class UserInGallery(db.Model): 302id = db.Column(db.Integer, primary_key=True, autoincrement=True) 303username = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 304gallery_id = db.Column(db.Integer, db.ForeignKey("gallery.id"), nullable=False) 305 306user = db.relationship("User") 307gallery = db.relationship("Gallery") 308 309def __init__(self, user, gallery): 310self.user = user 311self.gallery = gallery 312 313 314class Gallery(db.Model): 315id = db.Column(db.Integer, primary_key=True, autoincrement=True) 316title = db.Column(db.UnicodeText, nullable=False) 317description = db.Column(db.UnicodeText, nullable=False) 318pictures = db.relationship("PictureInGallery", back_populates="gallery") 319owner_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 320owner = db.relationship("User", back_populates="galleries") 321users = db.relationship("UserInGallery", back_populates="gallery") 322 323def __init__(self, title, description, owner): 324self.title = title 325self.description = description 326self.owner = owner 327 328 329class PictureRating(db.Model): 330id = db.Column(db.Integer, primary_key=True, autoincrement=True) 331resource_id = db.Column(db.Integer, db.ForeignKey("picture_resource.id"), nullable=False) 332username = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 333rating = db.Column(db.Integer, db.CheckConstraint("rating >= 1 AND rating <= 5"), 334nullable=False) 335 336resource = db.relationship("PictureResource", back_populates="ratings") 337user = db.relationship("User", back_populates="ratings") 338 339def __init__(self, resource, user, rating): 340self.resource = resource 341self.user = user 342self.rating = rating 343 344 345@app.route("/") 346def index(): 347return flask.render_template("home.html", resources=PictureResource.query.order_by( 348db.func.random()).limit(10).all()) 349 350 351@app.route("/accounts/") 352def accounts(): 353return flask.render_template("login.html") 354 355 356@app.route("/login", methods=["POST"]) 357def login(): 358username = flask.request.form["username"] 359password = flask.request.form["password"] 360 361user = db.session.get(User, username) 362 363if user is None: 364flask.flash("This username is not registered.") 365return flask.redirect("/accounts") 366 367if not bcrypt.check_password_hash(user.password_hashed, password): 368flask.flash("Incorrect password.") 369return flask.redirect("/accounts") 370 371flask.flash("You have been logged in.") 372 373flask.session["username"] = username 374return flask.redirect("/") 375 376 377@app.route("/logout") 378def logout(): 379flask.session.pop("username", None) 380flask.flash("You have been logged out.") 381return flask.redirect("/") 382 383 384@app.route("/signup", methods=["POST"]) 385def signup(): 386username = flask.request.form["username"] 387password = flask.request.form["password"] 388 389if db.session.get(User, username) is not None: 390flask.flash("This username is already taken.") 391return flask.redirect("/accounts") 392 393if set(username) > set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"): 394flask.flash( 395"Usernames can only contain the Latin alphabet, digits, hyphens, and underscores.") 396return flask.redirect("/accounts") 397 398if len(username) < 3 or len(username) > 32: 399flask.flash("Usernames must be between 3 and 32 characters long.") 400return flask.redirect("/accounts") 401 402if len(password) < 6: 403flask.flash("Passwords must be at least 6 characters long.") 404return flask.redirect("/accounts") 405 406user = User(username, password) 407db.session.add(user) 408db.session.commit() 409 410flask.session["username"] = username 411 412flask.flash("You have been registered and logged in.") 413 414return flask.redirect("/") 415 416 417@app.route("/profile", defaults={"username": None}) 418@app.route("/profile/<username>") 419def profile(username): 420if username is None: 421if "username" in flask.session: 422return flask.redirect("/profile/" + flask.session["username"]) 423else: 424flask.flash("Please log in to perform this action.") 425return flask.redirect("/accounts") 426 427user = db.session.get(User, username) 428if user is None: 429flask.abort(404) 430 431return flask.render_template("profile.html", user=user) 432 433 434@app.route("/object/<id>") 435def has_object(id): 436object_ = db.session.get(PictureObject, id) 437if object_ is None: 438flask.abort(404) 439 440query = db.session.query(PictureResource).join(PictureRegion).filter( 441PictureRegion.object_id == id) 442 443page = int(flask.request.args.get("page", 1)) 444per_page = int(flask.request.args.get("per_page", 16)) 445 446resources = query.paginate(page=page, per_page=per_page) 447 448return flask.render_template("object.html", object=object_, resources=resources, 449page_number=page, 450page_length=per_page, num_pages=resources.pages, 451prev_page=resources.prev_num, 452next_page=resources.next_num, PictureRegion=PictureRegion) 453 454 455@app.route("/upload") 456def upload(): 457if "username" not in flask.session: 458flask.flash("Log in to upload pictures.") 459return flask.redirect("/accounts") 460 461licences = Licence.query.order_by(Licence.free.desc(), Licence.pinned.desc(), 462Licence.title).all() 463 464types = PictureNature.query.all() 465 466return flask.render_template("upload.html", licences=licences, types=types) 467 468 469@app.route("/upload", methods=["POST"]) 470def upload_post(): 471title = flask.request.form["title"] 472description = flask.request.form["description"] 473origin_url = flask.request.form["origin_url"] 474author = db.session.get(User, flask.session.get("username")) 475licence_ids = flask.request.form.getlist("licence") 476nature_id = flask.request.form["nature"] 477 478if author is None: 479flask.abort(401) 480 481file = flask.request.files["file"] 482 483if not file or not file.filename: 484flask.flash("Select a file") 485return flask.redirect(flask.request.url) 486 487if not file.mimetype.startswith("image/") or file.mimetype == "image/svg+xml": 488flask.flash("Only images are supported") 489return flask.redirect(flask.request.url) 490 491if not title: 492flask.flash("Enter a title") 493return flask.redirect(flask.request.url) 494 495if not description: 496description = "" 497 498if not nature_id: 499flask.flash("Select a picture type") 500return flask.redirect(flask.request.url) 501 502if not licence_ids: 503flask.flash("Select licences") 504return flask.redirect(flask.request.url) 505 506licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids] 507if not any(licence.free for licence in licences): 508flask.flash("Select at least one free licence") 509return flask.redirect(flask.request.url) 510 511resource = PictureResource(title, author, description, origin_url, licence_ids, 512file.mimetype, 513db.session.get(PictureNature, nature_id)) 514db.session.add(resource) 515db.session.commit() 516file.save(path.join(config.DATA_PATH, "pictures", str(resource.id))) 517pil_image = Image.open(path.join(config.DATA_PATH, "pictures", str(resource.id))) 518resource.width, resource.height = pil_image.size 519db.session.commit() 520 521if flask.request.form.get("annotations"): 522try: 523resource.put_annotations(json.loads(flask.request.form.get("annotations"))) 524db.session.commit() 525except json.JSONDecodeError: 526flask.flash("Invalid annotations") 527 528flask.flash("Picture uploaded successfully") 529 530return flask.redirect("/picture/" + str(resource.id)) 531 532 533@app.route("/picture/<int:id>/") 534def picture(id): 535resource = db.session.get(PictureResource, id) 536if resource is None: 537flask.abort(404) 538 539image = Image.open(path.join(config.DATA_PATH, "pictures", str(resource.id))) 540 541current_user = db.session.get(User, flask.session.get("username")) 542have_permission = current_user and (current_user == resource.author or current_user.admin) 543 544own_rating = None 545if current_user: 546own_rating = PictureRating.query.filter_by(resource=resource, user=current_user).first() 547 548return flask.render_template("picture.html", resource=resource, 549file_extension=mimetypes.guess_extension(resource.file_format), 550size=image.size, copies=resource.copies, 551have_permission=have_permission, own_rating=own_rating) 552 553 554@app.route("/picture/<int:id>/annotate") 555def annotate_picture(id): 556resource = db.session.get(PictureResource, id) 557if resource is None: 558flask.abort(404) 559 560current_user = db.session.get(User, flask.session.get("username")) 561if current_user is None: 562flask.abort(401) 563 564if resource.author != current_user and not current_user.admin: 565flask.abort(403) 566 567return flask.render_template("picture-annotation.html", resource=resource, 568file_extension=mimetypes.guess_extension(resource.file_format)) 569 570 571@app.route("/picture/<int:id>/put-annotations-form") 572def put_annotations_form(id): 573resource = db.session.get(PictureResource, id) 574if resource is None: 575flask.abort(404) 576 577current_user = db.session.get(User, flask.session.get("username")) 578if current_user is None: 579flask.abort(401) 580 581if resource.author != current_user and not current_user.admin: 582flask.abort(403) 583 584return flask.render_template("put-annotations-form.html", resource=resource) 585 586 587@app.route("/picture/<int:id>/put-annotations-form", methods=["POST"]) 588def put_annotations_form_post(id): 589resource = db.session.get(PictureResource, id) 590if resource is None: 591flask.abort(404) 592 593current_user = db.session.get(User, flask.session.get("username")) 594if current_user is None: 595flask.abort(401) 596 597if resource.author != current_user and not current_user.admin: 598flask.abort(403) 599 600resource.put_annotations(json.loads(flask.request.form["annotations"])) 601 602db.session.commit() 603 604return flask.redirect("/picture/" + str(resource.id)) 605 606 607@app.route("/picture/<int:id>/save-annotations", methods=["POST"]) 608@app.route("/api/picture/<int:id>/put-annotations", methods=["POST"]) 609def save_annotations(id): 610resource = db.session.get(PictureResource, id) 611if resource is None: 612flask.abort(404) 613 614current_user = db.session.get(User, flask.session.get("username")) 615if resource.author != current_user and not current_user.admin: 616flask.abort(403) 617 618resource.put_annotations(flask.request.json) 619 620db.session.commit() 621 622response = flask.make_response() 623response.status_code = 204 624return response 625 626 627@app.route("/picture/<int:id>/get-annotations") 628@app.route("/api/picture/<int:id>/api/get-annotations") 629def get_annotations(id): 630resource = db.session.get(PictureResource, id) 631if resource is None: 632flask.abort(404) 633 634regions = db.session.query(PictureRegion).filter_by(resource_id=id).all() 635 636regions_json = [] 637 638for region in regions: 639regions_json.append({ 640"object": region.object_id, 641"type": region.json["type"], 642"shape": region.json["shape"], 643}) 644 645return flask.jsonify(regions_json) 646 647 648@app.route("/picture/<int:id>/delete") 649def delete_picture(id): 650resource = db.session.get(PictureResource, id) 651if resource is None: 652flask.abort(404) 653 654current_user = db.session.get(User, flask.session.get("username")) 655if current_user is None: 656flask.abort(401) 657 658if resource.author != current_user and not current_user.admin: 659flask.abort(403) 660 661PictureLicence.query.filter_by(resource=resource).delete() 662PictureRegion.query.filter_by(resource=resource).delete() 663PictureInGallery.query.filter_by(resource=resource).delete() 664PictureRating.query.filter_by(resource=resource).delete() 665if resource.replaces: 666resource.replaces.replaced_by = None 667if resource.replaced_by: 668resource.replaced_by.replaces = None 669resource.copied_from = None 670for copy in resource.copies: 671copy.copied_from = None 672db.session.delete(resource) 673db.session.commit() 674 675return flask.redirect("/") 676 677 678@app.route("/picture/<int:id>/mark-replacement", methods=["POST"]) 679def mark_picture_replacement(id): 680resource = db.session.get(PictureResource, id) 681if resource is None: 682flask.abort(404) 683 684current_user = db.session.get(User, flask.session.get("username")) 685if current_user is None: 686flask.abort(401) 687 688if resource.copied_from.author != current_user and not current_user.admin: 689flask.abort(403) 690 691resource.copied_from.replaced_by = resource 692resource.replaces = resource.copied_from 693 694db.session.commit() 695 696return flask.redirect("/picture/" + str(resource.copied_from.id)) 697 698 699@app.route("/picture/<int:id>/remove-replacement", methods=["POST"]) 700def remove_picture_replacement(id): 701resource = db.session.get(PictureResource, id) 702if resource is None: 703flask.abort(404) 704 705current_user = db.session.get(User, flask.session.get("username")) 706if current_user is None: 707flask.abort(401) 708 709if resource.author != current_user and not current_user.admin: 710flask.abort(403) 711 712resource.replaced_by.replaces = None 713resource.replaced_by = None 714 715db.session.commit() 716 717return flask.redirect("/picture/" + str(resource.id)) 718 719 720@app.route("/picture/<int:id>/edit-metadata") 721def edit_picture(id): 722resource = db.session.get(PictureResource, id) 723if resource is None: 724flask.abort(404) 725 726current_user = db.session.get(User, flask.session.get("username")) 727if current_user is None: 728flask.abort(401) 729 730if resource.author != current_user and not current_user.admin: 731flask.abort(403) 732 733licences = Licence.query.order_by(Licence.free.desc(), Licence.pinned.desc(), 734Licence.title).all() 735 736types = PictureNature.query.all() 737 738return flask.render_template("edit-picture.html", resource=resource, licences=licences, 739types=types, 740PictureLicence=PictureLicence) 741 742 743@app.route("/picture/<int:id>/rate", methods=["POST"]) 744def rate_picture(id): 745resource = db.session.get(PictureResource, id) 746if resource is None: 747flask.abort(404) 748 749current_user = db.session.get(User, flask.session.get("username")) 750if current_user is None: 751flask.abort(401) 752 753rating = int(flask.request.form.get("rating")) 754 755if not rating: 756# Delete the existing rating 757if PictureRating.query.filter_by(resource=resource, user=current_user).first(): 758db.session.delete(PictureRating.query.filter_by(resource=resource, 759user=current_user).first()) 760db.session.commit() 761 762return flask.redirect("/picture/" + str(resource.id)) 763 764if not 1 <= rating <= 5: 765flask.flash("Invalid rating") 766return flask.redirect("/picture/" + str(resource.id)) 767 768if PictureRating.query.filter_by(resource=resource, user=current_user).first(): 769PictureRating.query.filter_by(resource=resource, user=current_user).first().rating = rating 770else: 771# Create a new rating 772db.session.add(PictureRating(resource, current_user, rating)) 773 774db.session.commit() 775 776return flask.redirect("/picture/" + str(resource.id)) 777 778 779@app.route("/picture/<int:id>/edit-metadata", methods=["POST"]) 780def edit_picture_post(id): 781resource = db.session.get(PictureResource, id) 782if resource is None: 783flask.abort(404) 784 785current_user = db.session.get(User, flask.session.get("username")) 786if current_user is None: 787flask.abort(401) 788 789if resource.author != current_user and not current_user.admin: 790flask.abort(403) 791 792title = flask.request.form["title"] 793description = flask.request.form["description"] 794origin_url = flask.request.form["origin_url"] 795licence_ids = flask.request.form.getlist("licence") 796nature_id = flask.request.form["nature"] 797 798if not title: 799flask.flash("Enter a title") 800return flask.redirect(flask.request.url) 801 802if not description: 803description = "" 804 805if not nature_id: 806flask.flash("Select a picture type") 807return flask.redirect(flask.request.url) 808 809if not licence_ids: 810flask.flash("Select licences") 811return flask.redirect(flask.request.url) 812 813licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids] 814if not any(licence.free for licence in licences): 815flask.flash("Select at least one free licence") 816return flask.redirect(flask.request.url) 817 818resource.title = title 819resource.description = description 820resource.origin_url = origin_url 821for licence_id in licence_ids: 822joiner = PictureLicence(resource, db.session.get(Licence, licence_id)) 823db.session.add(joiner) 824resource.nature = db.session.get(PictureNature, nature_id) 825 826db.session.commit() 827 828return flask.redirect("/picture/" + str(resource.id)) 829 830 831@app.route("/picture/<int:id>/copy") 832def copy_picture(id): 833resource = db.session.get(PictureResource, id) 834if resource is None: 835flask.abort(404) 836 837current_user = db.session.get(User, flask.session.get("username")) 838if current_user is None: 839flask.abort(401) 840 841new_resource = PictureResource(resource.title, current_user, resource.description, 842resource.origin_url, 843[licence.licence_id for licence in resource.licences], 844resource.file_format, 845resource.nature) 846 847for region in resource.regions: 848db.session.add(PictureRegion(region.json, new_resource, region.object)) 849 850db.session.commit() 851 852# Create a hard link for the new picture 853old_path = path.join(config.DATA_PATH, "pictures", str(resource.id)) 854new_path = path.join(config.DATA_PATH, "pictures", str(new_resource.id)) 855os.link(old_path, new_path) 856 857new_resource.width = resource.width 858new_resource.height = resource.height 859new_resource.copied_from = resource 860 861db.session.commit() 862 863return flask.redirect("/picture/" + str(new_resource.id)) 864 865 866@app.route("/gallery/<int:id>/") 867def gallery(id): 868gallery = db.session.get(Gallery, id) 869if gallery is None: 870flask.abort(404) 871 872current_user = db.session.get(User, flask.session.get("username")) 873 874have_permission = current_user and (current_user == gallery.owner or current_user.admin or UserInGallery.query.filter_by(user=current_user, gallery=gallery).first()) 875 876return flask.render_template("gallery.html", gallery=gallery, 877have_permission=have_permission) 878 879 880@app.route("/create-gallery") 881def create_gallery(): 882if "username" not in flask.session: 883flask.flash("Log in to create galleries.") 884return flask.redirect("/accounts") 885 886return flask.render_template("create-gallery.html") 887 888 889@app.route("/create-gallery", methods=["POST"]) 890def create_gallery_post(): 891if not flask.session.get("username"): 892flask.abort(401) 893 894if not flask.request.form.get("title"): 895flask.flash("Enter a title") 896return flask.redirect(flask.request.url) 897 898description = flask.request.form.get("description", "") 899 900gallery = Gallery(flask.request.form["title"], description, 901db.session.get(User, flask.session["username"])) 902db.session.add(gallery) 903db.session.commit() 904 905return flask.redirect("/gallery/" + str(gallery.id)) 906 907 908@app.route("/gallery/<int:id>/add-picture", methods=["POST"]) 909def gallery_add_picture(id): 910gallery = db.session.get(Gallery, id) 911if gallery is None: 912flask.abort(404) 913 914if "username" not in flask.session: 915flask.abort(401) 916 917if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first(): 918flask.abort(403) 919 920picture_id = flask.request.form.get("picture_id") 921if "/" in picture_id: # also allow full URLs 922picture_id = picture_id.rstrip("/").rpartition("/")[1] 923if not picture_id: 924flask.flash("Select a picture") 925return flask.redirect("/gallery/" + str(gallery.id)) 926picture_id = int(picture_id) 927 928picture = db.session.get(PictureResource, picture_id) 929if picture is None: 930flask.flash("Invalid picture") 931return flask.redirect("/gallery/" + str(gallery.id)) 932 933if PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first(): 934flask.flash("This picture is already in the gallery") 935return flask.redirect("/gallery/" + str(gallery.id)) 936 937db.session.add(PictureInGallery(picture, gallery)) 938 939db.session.commit() 940 941return flask.redirect("/gallery/" + str(gallery.id)) 942 943 944@app.route("/gallery/<int:id>/remove-picture", methods=["POST"]) 945def gallery_remove_picture(id): 946gallery = db.session.get(Gallery, id) 947if gallery is None: 948flask.abort(404) 949 950if "username" not in flask.session: 951flask.abort(401) 952 953current_user = db.session.get(User, flask.session.get("username")) 954 955if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first(): 956flask.abort(403) 957 958picture_id = int(flask.request.form.get("picture_id")) 959 960picture = db.session.get(PictureResource, picture_id) 961if picture is None: 962flask.flash("Invalid picture") 963return flask.redirect("/gallery/" + str(gallery.id)) 964 965picture_in_gallery = PictureInGallery.query.filter_by(resource=picture, 966gallery=gallery).first() 967if picture_in_gallery is None: 968flask.flash("This picture isn't in the gallery") 969return flask.redirect("/gallery/" + str(gallery.id)) 970 971db.session.delete(picture_in_gallery) 972 973db.session.commit() 974 975return flask.redirect("/gallery/" + str(gallery.id)) 976 977 978@app.route("/gallery/<int:id>/add-pictures-from-query", methods=["POST"]) 979def gallery_add_from_query(id): 980gallery = db.session.get(Gallery, id) 981if gallery is None: 982flask.abort(404) 983 984if "username" not in flask.session: 985flask.abort(401) 986 987if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first(): 988flask.abort(403) 989 990query_yaml = flask.request.form.get("query", "") 991 992yaml_parser = yaml.YAML() 993query_data = yaml_parser.load(query_yaml) or {} 994query = get_picture_query(query_data) 995 996pictures = query.all() 997 998count = 0 999 1000for picture in pictures: 1001if not PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first(): 1002db.session.add(PictureInGallery(picture, gallery)) 1003count += 1 1004 1005db.session.commit() 1006 1007flask.flash(f"Added {count} pictures to the gallery") 1008 1009return flask.redirect("/gallery/" + str(gallery.id)) 1010 1011 1012@app.route("/gallery/<int:id>/users") 1013def gallery_users(id): 1014gallery = db.session.get(Gallery, id) 1015if gallery is None: 1016flask.abort(404) 1017 1018current_user = db.session.get(User, flask.session.get("username")) 1019have_permission = current_user and (current_user == gallery.owner or current_user.admin) 1020 1021return flask.render_template("gallery-users.html", gallery=gallery, 1022have_permission=have_permission) 1023 1024 1025@app.route("/gallery/<int:id>/edit") 1026def edit_gallery(id): 1027gallery = db.session.get(Gallery, id) 1028if gallery is None: 1029flask.abort(404) 1030 1031current_user = db.session.get(User, flask.session.get("username")) 1032if current_user is None: 1033flask.abort(401) 1034 1035if current_user != gallery.owner and not current_user.admin: 1036flask.abort(403) 1037 1038return flask.render_template("edit-gallery.html", gallery=gallery) 1039 1040 1041@app.route("/gallery/<int:id>/edit", methods=["POST"]) 1042def edit_gallery_post(id): 1043gallery = db.session.get(Gallery, id) 1044if gallery is None: 1045flask.abort(404) 1046 1047current_user = db.session.get(User, flask.session.get("username")) 1048if current_user is None: 1049flask.abort(401) 1050 1051if current_user != gallery.owner and not current_user.admin: 1052flask.abort(403) 1053 1054title = flask.request.form["title"] 1055description = flask.request.form.get("description") 1056 1057if not title: 1058flask.flash("Enter a title") 1059return flask.redirect(flask.request.url) 1060 1061if not description: 1062description = "" 1063 1064gallery.title = title 1065gallery.description = description 1066 1067db.session.commit() 1068 1069return flask.redirect("/gallery/" + str(gallery.id)) 1070 1071 1072@app.route("/gallery/<int:id>/users/add", methods=["POST"]) 1073def gallery_add_user(id): 1074gallery = db.session.get(Gallery, id) 1075if gallery is None: 1076flask.abort(404) 1077 1078current_user = db.session.get(User, flask.session.get("username")) 1079if current_user is None: 1080flask.abort(401) 1081 1082if current_user != gallery.owner and not current_user.admin: 1083flask.abort(403) 1084 1085username = flask.request.form.get("username") 1086if username == gallery.owner_name: 1087flask.flash("The owner is already in the gallery") 1088return flask.redirect("/gallery/" + str(gallery.id) + "/users") 1089 1090user = db.session.get(User, username) 1091if user is None: 1092flask.flash("User not found") 1093return flask.redirect("/gallery/" + str(gallery.id) + "/users") 1094 1095if UserInGallery.query.filter_by(user=user, gallery=gallery).first(): 1096flask.flash("User is already in the gallery") 1097return flask.redirect("/gallery/" + str(gallery.id) + "/users") 1098 1099db.session.add(UserInGallery(user, gallery)) 1100 1101db.session.commit() 1102 1103return flask.redirect("/gallery/" + str(gallery.id) + "/users") 1104 1105 1106@app.route("/gallery/<int:id>/users/remove", methods=["POST"]) 1107def gallery_remove_user(id): 1108gallery = db.session.get(Gallery, id) 1109if gallery is None: 1110flask.abort(404) 1111 1112current_user = db.session.get(User, flask.session.get("username")) 1113if current_user is None: 1114flask.abort(401) 1115 1116if current_user != gallery.owner and not current_user.admin: 1117flask.abort(403) 1118 1119username = flask.request.form.get("username") 1120user = db.session.get(User, username) 1121if user is None: 1122flask.flash("User not found") 1123return flask.redirect("/gallery/" + str(gallery.id) + "/users") 1124 1125user_in_gallery = UserInGallery.query.filter_by(user=user, gallery=gallery).first() 1126if user_in_gallery is None: 1127flask.flash("User is not in the gallery") 1128return flask.redirect("/gallery/" + str(gallery.id) + "/users") 1129 1130db.session.delete(user_in_gallery) 1131 1132db.session.commit() 1133 1134return flask.redirect("/gallery/" + str(gallery.id) + "/users") 1135 1136 1137class APIError(Exception): 1138def __init__(self, status_code, message): 1139self.status_code = status_code 1140self.message = message 1141 1142 1143def get_picture_query(query_data): 1144query = db.session.query(PictureResource) 1145 1146descendants_cte = ( 1147db.select(PictureObject.id) 1148.where(PictureObject.id == PictureRegion.object_id) 1149.cte(name="descendants_cte", recursive=True) 1150) 1151 1152descendants_cte = descendants_cte.union_all( 1153db.select(PictureObjectInheritance.child_id) 1154.where(PictureObjectInheritance.parent_id == descendants_cte.c.id) 1155) 1156 1157requirement_conditions = { 1158# Has an object with the ID in the given list 1159"has_object": lambda value: PictureResource.regions.any( 1160PictureRegion.object_id.in_(value)), 1161# Has an object with the ID in the given list, or a subtype of it 1162"has": lambda value: PictureResource.regions.any( 1163PictureRegion.object_id.in_( 1164db.select(descendants_cte.c.id) 1165) 1166), 1167 1168"nature": lambda value: PictureResource.nature_id.in_(value), 1169"licence": lambda value: PictureResource.licences.any( 1170PictureLicence.licence_id.in_(value)), 1171"author": lambda value: PictureResource.author_name.in_(value), 1172"title": lambda value: PictureResource.title.ilike(value), 1173"description": lambda value: PictureResource.description.ilike(value), 1174"origin_url": lambda value: db.func.lower(db.func.substr( 1175PictureResource.origin_url, 1176db.func.length(db.func.split_part(PictureResource.origin_url, "://", 1)) + 4 1177)).in_(value), 1178"above_width": lambda value: PictureResource.width >= value, 1179"below_width": lambda value: PictureResource.width <= value, 1180"above_height": lambda value: PictureResource.height >= value, 1181"below_height": lambda value: PictureResource.height <= value, 1182"before_date": lambda value: PictureResource.timestamp <= datetime.utcfromtimestamp( 1183value), 1184"after_date": lambda value: PictureResource.timestamp >= datetime.utcfromtimestamp( 1185value), 1186"in_gallery": lambda value: PictureResource.galleries.any(PictureInGallery.gallery_id.in_(value)), 1187"above_rating": lambda value: db.select(db.func.coalesce(db.func.avg(PictureRating.rating), 5)).where(PictureRating.resource_id == PictureResource.id).scalar_subquery() >= value, 1188"below_rating": lambda value: db.select(db.func.coalesce(db.func.avg(PictureRating.rating), 0)).where(PictureRating.resource_id == PictureResource.id).scalar_subquery() <= value, 1189"above_rating_count": lambda value: db.select(db.func.count(PictureRating.id)).where(PictureRating.resource_id == PictureResource.id).correlate(PictureResource).scalar_subquery() >= value, 1190"below_rating_count": lambda value: db.select(db.func.count(PictureRating.id)).where(PictureRating.resource_id == PictureResource.id).correlate(PictureResource).scalar_subquery() <= value, 1191"above_region_count": lambda value: db.select(db.func.count(PictureRegion.id)).where(PictureRegion.resource_id == PictureResource.id).correlate(PictureResource).scalar_subquery() >= value, 1192"below_region_count": lambda value: db.select(db.func.count(PictureRegion.id)).where(PictureRegion.resource_id == PictureResource.id).correlate(PictureResource).scalar_subquery() <= value, 1193"copied_from": lambda value: PictureResource.copied_from_id.in_(value), 1194} 1195 1196if "want" in query_data: 1197for i in query_data["want"]: 1198if len(i) != 1: 1199raise APIError(400, "Each requirement must have exactly one key") 1200requirement, value = list(i.items())[0] 1201if requirement not in requirement_conditions: 1202raise APIError(400, f"Unknown requirement type: {requirement}") 1203 1204condition = requirement_conditions[requirement] 1205query = query.filter(condition(value)) 1206if "exclude" in query_data: 1207for i in query_data["exclude"]: 1208if len(i) != 1: 1209raise APIError(400, "Each exclusion must have exactly one key") 1210requirement, value = list(i.items())[0] 1211if requirement not in requirement_conditions: 1212raise APIError(400, f"Unknown requirement type: {requirement}") 1213 1214condition = requirement_conditions[requirement] 1215query = query.filter(~condition(value)) 1216if not query_data.get("include_obsolete", False): 1217query = query.filter(PictureResource.replaced_by_id.is_(None)) 1218 1219return query 1220 1221 1222@app.route("/query-pictures") 1223def graphical_query_pictures(): 1224return flask.render_template("graphical-query-pictures.html") 1225 1226 1227@app.route("/query-pictures-results") 1228def graphical_query_pictures_results(): 1229query_yaml = flask.request.args.get("query", "") 1230yaml_parser = yaml.YAML() 1231query_data = yaml_parser.load(query_yaml) or {} 1232try: 1233query = get_picture_query(query_data) 1234except APIError as e: 1235flask.abort(e.status_code) 1236 1237page = int(flask.request.args.get("page", 1)) 1238per_page = int(flask.request.args.get("per_page", 16)) 1239 1240resources = query.paginate(page=page, per_page=per_page) 1241 1242return flask.render_template("graphical-query-pictures-results.html", resources=resources, 1243query=query_yaml, 1244page_number=page, page_length=per_page, 1245num_pages=resources.pages, 1246prev_page=resources.prev_num, next_page=resources.next_num) 1247 1248 1249@app.route("/raw/picture/<int:id>") 1250def raw_picture(id): 1251resource = db.session.get(PictureResource, id) 1252if resource is None: 1253flask.abort(404) 1254 1255response = flask.send_from_directory(path.join(config.DATA_PATH, "pictures"), 1256str(resource.id)) 1257response.mimetype = resource.file_format 1258 1259return response 1260 1261 1262@app.route("/object/") 1263def graphical_object_types(): 1264return flask.render_template("object-types.html", objects=PictureObject.query.all()) 1265 1266 1267@app.route("/api/object-types") 1268def object_types(): 1269objects = db.session.query(PictureObject).all() 1270return flask.jsonify({object.id: object.description for object in objects}) 1271 1272 1273@app.route("/api/query-pictures", methods=["POST"]) # sadly GET can't have a body 1274def query_pictures(): 1275offset = int(flask.request.args.get("offset", 0)) 1276limit = int(flask.request.args.get("limit", 16)) 1277ordering = flask.request.args.get("ordering", "date-desc") 1278 1279yaml_parser = yaml.YAML() 1280query_data = yaml_parser.load(flask.request.data) or {} 1281try: 1282query = get_picture_query(query_data) 1283except APIError as e: 1284return flask.jsonify({"error": e.message}), e.status_code 1285 1286rating_count_subquery = db.select(db.func.count(PictureRating.id)).where( 1287PictureRating.resource_id == PictureResource.id).scalar_subquery() 1288region_count_subquery = db.select(db.func.count(PictureRegion.id)).where( 1289PictureRegion.resource_id == PictureResource.id).scalar_subquery() 1290rating_subquery = db.select(db.func.coalesce(db.func.avg(PictureRating.rating), 0)).where( 1291PictureRating.resource_id == PictureResource.id).scalar_subquery() 1292 1293match ordering: 1294case "date-desc": 1295query = query.order_by(PictureResource.timestamp.desc()) 1296case "date-asc": 1297query = query.order_by(PictureResource.timestamp.asc()) 1298case "title-asc": 1299query = query.order_by(PictureResource.title.asc()) 1300case "title-desc": 1301query = query.order_by(PictureResource.title.desc()) 1302case "random": 1303query = query.order_by(db.func.random()) 1304case "number-regions-desc": 1305query = query.order_by(region_count_subquery.desc()) 1306case "number-regions-asc": 1307query = query.order_by(region_count_subquery.asc()) 1308case "rating-desc": 1309query = query.order_by(rating_subquery.desc()) 1310case "rating-asc": 1311query = query.order_by(rating_subquery.asc()) 1312case "number-ratings-desc": 1313query = query.order_by(rating_count_subquery.desc()) 1314case "number-ratings-asc": 1315query = query.order_by(rating_count_subquery.asc()) 1316 1317query = query.offset(offset).limit(limit) 1318resources = query.all() 1319 1320json_response = { 1321"date_generated": datetime.utcnow().timestamp(), 1322"resources": [], 1323"offset": offset, 1324"limit": limit, 1325} 1326 1327json_resources = json_response["resources"] 1328 1329for resource in resources: 1330json_resource = { 1331"id": resource.id, 1332"title": resource.title, 1333"description": resource.description, 1334"timestamp": resource.timestamp.timestamp(), 1335"origin_url": resource.origin_url, 1336"author": resource.author_name, 1337"file_format": resource.file_format, 1338"width": resource.width, 1339"height": resource.height, 1340"nature": resource.nature_id, 1341"licences": [licence.licence_id for licence in resource.licences], 1342"replaces": resource.replaces_id, 1343"replaced_by": resource.replaced_by_id, 1344"regions": [], 1345"download": config.ROOT_URL + flask.url_for("raw_picture", id=resource.id), 1346} 1347for region in resource.regions: 1348json_resource["regions"].append({ 1349"object": region.object_id, 1350"type": region.json["type"], 1351"shape": region.json["shape"], 1352}) 1353 1354json_resources.append(json_resource) 1355 1356return flask.jsonify(json_response) 1357 1358 1359@app.route("/api/picture/<int:id>/") 1360def api_picture(id): 1361resource = db.session.get(PictureResource, id) 1362if resource is None: 1363flask.abort(404) 1364 1365json_resource = { 1366"id": resource.id, 1367"title": resource.title, 1368"description": resource.description, 1369"timestamp": resource.timestamp.timestamp(), 1370"origin_url": resource.origin_url, 1371"author": resource.author_name, 1372"file_format": resource.file_format, 1373"width": resource.width, 1374"height": resource.height, 1375"nature": resource.nature_id, 1376"licences": [licence.licence_id for licence in resource.licences], 1377"replaces": resource.replaces_id, 1378"replaced_by": resource.replaced_by_id, 1379"regions": [], 1380"download": config.ROOT_URL + flask.url_for("raw_picture", id=resource.id), 1381"rating_average": resource.average_rating, 1382"rating_count": resource.rating_totals, 1383} 1384for region in resource.regions: 1385json_resource["regions"].append({ 1386"object": region.object_id, 1387"type": region.json["type"], 1388"shape": region.json["shape"], 1389}) 1390 1391return flask.jsonify(json_resource) 1392 1393 1394@app.route("/api/licence/") 1395def api_licences(): 1396licences = db.session.query(Licence).all() 1397json_licences = { 1398licence.id: { 1399"title": licence.title, 1400"free": licence.free, 1401"pinned": licence.pinned, 1402} for licence in licences 1403} 1404 1405return flask.jsonify(json_licences) 1406 1407 1408@app.route("/api/licence/<id>/") 1409def api_licence(id): 1410licence = db.session.get(Licence, id) 1411if licence is None: 1412flask.abort(404) 1413 1414json_licence = { 1415"id": licence.id, 1416"title": licence.title, 1417"description": licence.description, 1418"info_url": licence.info_url, 1419"legalese_url": licence.url, 1420"free": licence.free, 1421"logo_url": licence.logo_url, 1422"pinned": licence.pinned, 1423} 1424 1425return flask.jsonify(json_licence) 1426 1427 1428@app.route("/api/nature/") 1429def api_natures(): 1430natures = db.session.query(PictureNature).all() 1431json_natures = { 1432nature.id: nature.description for nature in natures 1433} 1434 1435return flask.jsonify(json_natures) 1436 1437 1438@app.route("/api/user/") 1439def api_users(): 1440offset = int(flask.request.args.get("offset", 0)) 1441limit = int(flask.request.args.get("limit", 16)) 1442 1443users = db.session.query(User).offset(offset).limit(limit).all() 1444 1445json_users = { 1446user.username: { 1447"admin": user.admin, 1448} for user in users 1449} 1450 1451return flask.jsonify(json_users) 1452 1453 1454@app.route("/api/user/<username>/") 1455def api_user(username): 1456user = db.session.get(User, username) 1457if user is None: 1458flask.abort(404) 1459 1460json_user = { 1461"username": user.username, 1462"admin": user.admin, 1463"joined": user.joined_timestamp.timestamp(), 1464} 1465 1466return flask.jsonify(json_user) 1467 1468 1469@app.route("/api/login", methods=["POST"]) 1470def api_login(): 1471username = flask.request.json["username"] 1472password = flask.request.json["password"] 1473 1474user = db.session.get(User, username) 1475 1476if user is None: 1477return flask.jsonify({"error": "This username is not registered. To prevent spam, you must use the HTML interface to register."}), 401 1478 1479if not bcrypt.check_password_hash(user.password_hashed, password): 1480return flask.jsonify({"error": "Incorrect password"}), 401 1481 1482flask.session["username"] = username 1483 1484return flask.jsonify({"message": "You have been logged in. Your HTTP client must support cookies to use features of this API that require authentication."}) 1485 1486 1487@app.route("/api/logout", methods=["POST"]) 1488def api_logout(): 1489flask.session.pop("username", None) 1490return flask.jsonify({"message": "You have been logged out."}) 1491 1492 1493@app.route("/api/upload", methods=["POST"]) 1494def api_upload(): 1495if "username" not in flask.session: 1496return flask.jsonify({"error": "You must be logged in to upload pictures"}), 401 1497 1498json_ = json.loads(flask.request.form["json"]) 1499title = json_["title"] 1500description = json_.get("description", "") 1501origin_url = json_.get("origin_url", "") 1502author = db.session.get(User, flask.session["username"]) 1503licence_ids = json_["licence"] 1504nature_id = json_["nature"] 1505file = flask.request.files["file"] 1506 1507if not file or not file.filename: 1508return flask.jsonify({"error": "An image file must be uploaded"}), 400 1509 1510if not file.mimetype.startswith("image/") or file.mimetype == "image/svg+xml": 1511return flask.jsonify({"error": "Only bitmap images are supported"}), 400 1512 1513if not title: 1514return flask.jsonify({"error": "Give a title"}), 400 1515 1516if not description: 1517description = "" 1518 1519if not nature_id: 1520return flask.jsonify({"error": "Give a picture type"}), 400 1521 1522if not licence_ids: 1523return flask.jsonify({"error": "Give licences"}), 400 1524 1525licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids] 1526if not any(licence.free for licence in licences): 1527return flask.jsonify({"error": "Use at least one free licence"}), 400 1528 1529resource = PictureResource(title, author, description, origin_url, licence_ids, 1530file.mimetype, 1531db.session.get(PictureNature, nature_id)) 1532db.session.add(resource) 1533db.session.commit() 1534file.save(path.join(config.DATA_PATH, "pictures", str(resource.id))) 1535pil_image = Image.open(path.join(config.DATA_PATH, "pictures", str(resource.id))) 1536resource.width, resource.height = pil_image.size 1537db.session.commit() 1538 1539if json_.get("annotations"): 1540try: 1541resource.put_annotations(json_["annotations"]) 1542db.session.commit() 1543except json.JSONDecodeError: 1544return flask.jsonify({"error": "Invalid annotations"}), 400 1545 1546return flask.jsonify({"message": "Picture uploaded successfully", "id": resource.id}) 1547 1548 1549@app.route("/api/picture/<int:id>/update", methods=["POST"]) 1550def api_update_picture(id): 1551resource = db.session.get(PictureResource, id) 1552if resource is None: 1553return flask.jsonify({"error": "Picture not found"}), 404 1554current_user = db.session.get(User, flask.session.get("username")) 1555if current_user is None: 1556return flask.jsonify({"error": "You must be logged in to edit pictures"}), 401 1557if resource.author != current_user and not current_user.admin: 1558return flask.jsonify({"error": "You are not the author of this picture"}), 403 1559 1560title = flask.request.json.get("title", resource.title) 1561description = flask.request.json.get("description", resource.description) 1562origin_url = flask.request.json.get("origin_url", resource.origin_url) 1563licence_ids = flask.request.json.get("licence", [licence.licence_id for licence in resource.licences]) 1564nature_id = flask.request.json.get("nature", resource.nature_id) 1565 1566if not title: 1567return flask.jsonify({"error": "Give a title"}), 400 1568 1569if not description: 1570description = "" 1571 1572if not nature_id: 1573return flask.jsonify({"error": "Give a picture type"}), 400 1574 1575if not licence_ids: 1576return flask.jsonify({"error": "Give licences"}), 400 1577 1578licences = [db.session.get(Licence, licence_id) for licence_id in licence_ids] 1579 1580if not any(licence.free for licence in licences): 1581return flask.jsonify({"error": "Use at least one free licence"}), 400 1582 1583resource.title = title 1584resource.description = description 1585resource.origin_url = origin_url 1586resource.licences = licences 1587resource.nature = db.session.get(PictureNature, nature_id) 1588 1589db.session.commit() 1590 1591return flask.jsonify({"message": "Picture updated successfully"}) 1592 1593 1594@app.route("/api/picture/<int:id>/rate", methods=["POST"]) 1595def api_rate_picture(id): 1596resource = db.session.get(PictureResource, id) 1597if resource is None: 1598flask.abort(404) 1599 1600current_user = db.session.get(User, flask.session.get("username")) 1601if current_user is None: 1602flask.abort(401) 1603 1604rating = int(flask.request.json.get("rating")) 1605 1606if not rating: 1607# Delete the existing rating 1608if PictureRating.query.filter_by(resource=resource, user=current_user).first(): 1609db.session.delete(PictureRating.query.filter_by(resource=resource, 1610user=current_user).first()) 1611db.session.commit() 1612 1613return flask.jsonify({"message": "Existing rating removed"}) 1614 1615if not 1 <= rating <= 5: 1616flask.flash("Invalid rating") 1617return flask.jsonify({"error": "Invalid rating"}), 400 1618 1619if PictureRating.query.filter_by(resource=resource, user=current_user).first(): 1620PictureRating.query.filter_by(resource=resource, user=current_user).first().rating = rating 1621else: 1622# Create a new rating 1623db.session.add(PictureRating(resource, current_user, rating)) 1624 1625db.session.commit() 1626 1627return flask.jsonify({"message": "Rating saved"}) 1628 1629 1630@app.route("/api/gallery/<int:id>/") 1631def api_gallery(id): 1632gallery = db.session.get(Gallery, id) 1633if gallery is None: 1634flask.abort(404) 1635 1636json_gallery = { 1637"id": gallery.id, 1638"title": gallery.title, 1639"description": gallery.description, 1640"owner": gallery.owner_name, 1641"users": [user.username for user in gallery.users], 1642} 1643 1644return flask.jsonify(json_gallery) 1645 1646 1647@app.route("/api/gallery/<int:id>/edit", methods=["POST"]) 1648def api_edit_gallery(id): 1649gallery = db.session.get(Gallery, id) 1650if gallery is None: 1651flask.abort(404) 1652 1653current_user = db.session.get(User, flask.session.get("username")) 1654if current_user is None: 1655flask.abort(401) 1656 1657if current_user != gallery.owner and not current_user.admin: 1658flask.abort(403) 1659 1660title = flask.request.json.get("title", gallery.title) 1661description = flask.request.json.get("description", gallery.description) 1662 1663if not title: 1664return flask.jsonify({"error": "Give a title"}), 400 1665 1666if not description: 1667description = "" 1668 1669gallery.title = title 1670gallery.description = description 1671 1672db.session.commit() 1673 1674return flask.jsonify({"message": "Gallery updated successfully"}) 1675 1676 1677@app.route("/api/new-gallery", methods=["POST"]) 1678def api_new_gallery(): 1679if "username" not in flask.session: 1680return flask.jsonify({"error": "You must be logged in to create galleries"}), 401 1681 1682title = flask.request.json.get("title") 1683description = flask.request.json.get("description", "") 1684 1685if not title: 1686return flask.jsonify({"error": "Give a title"}), 400 1687 1688gallery = Gallery(title, description, db.session.get(User, flask.session["username"])) 1689db.session.add(gallery) 1690db.session.commit() 1691 1692return flask.jsonify({"message": "Gallery created successfully", "id": gallery.id}) 1693 1694 1695@app.route("/api/gallery/<int:id>/add-picture", methods=["POST"]) 1696def api_gallery_add_picture(id): 1697gallery = db.session.get(Gallery, id) 1698if gallery is None: 1699flask.abort(404) 1700 1701if "username" not in flask.session: 1702return flask.jsonify({"error": "You must be logged in to add pictures to galleries"}), 401 1703 1704current_user = db.session.get(User, flask.session.get("username")) 1705 1706if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first(): 1707return flask.jsonify({"error": "You do not have permission to add pictures to this gallery"}), 403 1708 1709picture_id = flask.request.json.get("picture_id") 1710 1711try: 1712picture_id = int(picture_id) 1713except ValueError: 1714return flask.jsonify({"error": "Invalid picture ID"}), 400 1715 1716picture = db.session.get(PictureResource, picture_id) 1717if picture is None: 1718return flask.jsonify({"error": "The picture doesn't exist"}), 404 1719 1720if PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first(): 1721return flask.jsonify({"error": "This picture is already in the gallery"}), 400 1722 1723db.session.add(PictureInGallery(picture, gallery)) 1724 1725db.session.commit() 1726 1727return flask.jsonify({"message": "Picture added to gallery"}) 1728 1729 1730@app.route("/api/gallery/<int:id>/remove-picture", methods=["POST"]) 1731def api_gallery_remove_picture(id): 1732gallery = db.session.get(Gallery, id) 1733if gallery is None: 1734flask.abort(404) 1735 1736if "username" not in flask.session: 1737return flask.jsonify({"error": "You must be logged in to remove pictures from galleries"}), 401 1738 1739current_user = db.session.get(User, flask.session.get("username")) 1740 1741if flask.session["username"] != gallery.owner_name and not current_user.admin and not UserInGallery.query.filter_by(user=current_user, gallery=gallery).first(): 1742return flask.jsonify({"error": "You do not have permission to remove pictures from this gallery"}), 403 1743 1744picture_id = flask.request.json.get("picture_id") 1745 1746try: 1747picture_id = int(picture_id) 1748except ValueError: 1749return flask.jsonify({"error": "Invalid picture ID"}), 400 1750 1751picture = db.session.get(PictureResource, picture_id) 1752if picture is None: 1753return flask.jsonify({"error": "The picture doesn't exist"}), 404 1754 1755picture_in_gallery = PictureInGallery.query.filter_by(resource=picture, gallery=gallery).first() 1756if picture_in_gallery is None: 1757return flask.jsonify({"error": "This picture isn't in the gallery"}), 400 1758 1759db.session.delete(picture_in_gallery) 1760 1761db.session.commit() 1762 1763return flask.jsonify({"message": "Picture removed from gallery"}) 1764 1765 1766@app.route("/api/gallery/<int:id>/users/add", methods=["POST"]) 1767def api_gallery_add_user(id): 1768gallery = db.session.get(Gallery, id) 1769if gallery is None: 1770flask.abort(404) 1771 1772current_user = db.session.get(User, flask.session.get("username")) 1773if current_user is None: 1774flask.abort(401) 1775 1776if current_user != gallery.owner and not current_user.admin: 1777flask.abort(403) 1778 1779username = flask.request.json.get("username") 1780if username == gallery.owner_name: 1781return flask.jsonify({"error": "The owner cannot be added to trusted users"}), 400 1782 1783user = db.session.get(User, username) 1784if user is None: 1785return flask.jsonify({"error": "User not found"}), 404 1786 1787if UserInGallery.query.filter_by(user=user, gallery=gallery).first(): 1788return flask.jsonify({"error": "User is already in the gallery"}), 400 1789 1790db.session.add(UserInGallery(user, gallery)) 1791 1792db.session.commit() 1793 1794return flask.jsonify({"message": "User added to gallery"}) 1795 1796 1797@app.route("/api/gallery/<int:id>/users/remove", methods=["POST"]) 1798def api_gallery_remove_user(id): 1799gallery = db.session.get(Gallery, id) 1800if gallery is None: 1801flask.abort(404) 1802 1803current_user = db.session.get(User, flask.session.get("username")) 1804if current_user is None: 1805flask.abort(401) 1806 1807if current_user != gallery.owner and not current_user.admin: 1808flask.abort(403) 1809 1810username = flask.request.json.get("username") 1811user = db.session.get(User, username) 1812if user is None: 1813return flask.jsonify({"error": "User not found"}), 404 1814 1815user_in_gallery = UserInGallery.query.filter_by(user=user, gallery=gallery).first() 1816if user_in_gallery is None: 1817return flask.jsonify({"error": "User is not in the gallery"}), 400 1818 1819db.session.delete(user_in_gallery) 1820 1821db.session.commit() 1822 1823return flask.jsonify({"message": "User removed from gallery"}) 1824 1825