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