From a2b035f522583decb12d5bf7da19f4ed67cf7612 Mon Sep 17 00:00:00 2001 From: Yaro Kasear Date: Fri, 11 Jul 2025 11:48:53 -0500 Subject: [PATCH] Add image upload functionality and enhance inventory template with image rendering --- .gitignore | 2 ++ inventory/models/inventory.py | 5 +++ inventory/routes/photos.py | 19 ++++++---- inventory/static/js/widget.js | 36 +++++++++++++++++++ inventory/static/uploads/.gitkeep | 0 .../templates/fragments/_image_fragment.html | 22 ++++++++++++ inventory/templates/inventory.html | 14 ++++---- inventory/templates/layout.html | 1 + 8 files changed, 84 insertions(+), 15 deletions(-) create mode 100644 inventory/static/uploads/.gitkeep create mode 100644 inventory/templates/fragments/_image_fragment.html diff --git a/.gitignore b/.gitignore index 1be1a03..29fa030 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ **/__pycache__/ +inventory/static/uploads/* +!inventory/static/uploads/.gitkeep .venv/ .env *.db diff --git a/inventory/models/inventory.py b/inventory/models/inventory.py index 0b9803e..c8c4b22 100644 --- a/inventory/models/inventory.py +++ b/inventory/models/inventory.py @@ -1,4 +1,6 @@ from typing import Any, List, Optional, TYPE_CHECKING + +from inventory.models.photo import Photo if TYPE_CHECKING: from .brands import Brand from .items import Item @@ -125,3 +127,6 @@ class Inventory(db.Model, PhotoAttachable): barcode=data.get("barcode"), shared=bool(data.get("shared", False)) ) + + def attach_photo(self, photo: Photo) -> None: + self.photo = photo diff --git a/inventory/routes/photos.py b/inventory/routes/photos.py index 3297620..4001436 100644 --- a/inventory/routes/photos.py +++ b/inventory/routes/photos.py @@ -10,17 +10,22 @@ from ..models import Photo photo_bp = Blueprint("photo_api", __name__) def save_photo(file, model: str) -> str: - # Generate unique path - rel_path = generate_hashed_filename(file, model) - - # Absolute path to save assert current_app.static_folder - abs_path = os.path.join(current_app.static_folder, rel_path) - os.makedirs(os.path.dirname(abs_path), exist_ok=True) + rel_path = generate_hashed_filename(file, model) + abs_path = os.path.join(current_app.static_folder, "uploads", "photos", rel_path) + + dir_path = os.path.dirname(abs_path) + if not os.path.exists(dir_path): + os.makedirs(dir_path, exist_ok=True) + + print("Saving file:") + print(" - Model:", model) + print(" - Relative path:", rel_path) + print(" - Absolute path:", abs_path) + print(" - Directory exists?", os.path.exists(dir_path)) file.save(abs_path) - return rel_path @photo_bp.route("/api/photos", methods=["POST"]) diff --git a/inventory/static/js/widget.js b/inventory/static/js/widget.js index bd2c239..e0dfb3e 100644 --- a/inventory/static/js/widget.js +++ b/inventory/static/js/widget.js @@ -53,6 +53,42 @@ function renderToast({ message, type = ToastConfig.defaultType, timeout = ToastC }); } +const ImageWidget = (() => { + function submitImageUpload(id) { + const form = document.getElementById(`image-upload-form-${id}`); + console.log(form); + const formData = new FormData(form); + + fetch("/api/photos", { + method: "POST", + body: formData + }).then(async response => { + if (!response.ok) { + // Try to parse JSON, fallback to text + const contentType = response.headers.get("Content-Type") || ""; + let errorDetails; + if (contentType.includes("application/json")) { + errorDetails = await response.json(); + } else { + errorDetails = { error: await response.text() }; + } + throw errorDetails; + } + return response.json(); + }).then(data => { + renderToast({ message: `Photo uploaded.`, type: "success" }); + location.reload(); + }).catch(err => { + const msg = typeof err === "object" && err.error ? err.error : err.toString(); + renderToast({ message: `Upload failed: ${msg}`, type: "danger" }); + }); + } + + return { + submitImageUpload + } +})(); + const EditorWidget = (() => { let tempIdCounter = 1; diff --git a/inventory/static/uploads/.gitkeep b/inventory/static/uploads/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/inventory/templates/fragments/_image_fragment.html b/inventory/templates/fragments/_image_fragment.html new file mode 100644 index 0000000..a2c9ecc --- /dev/null +++ b/inventory/templates/fragments/_image_fragment.html @@ -0,0 +1,22 @@ +{% import "fragments/_icon_fragment.html" as icons %} + +{% macro render_image(id, image=None) %} + +
+ {% if image %} + Photo of ID {{ id }} + {% else %} + + {{ icons.render_icon('image', 256) }} + +
+ + + + +
+ {% endif %} +
+{% endmacro %} diff --git a/inventory/templates/inventory.html b/inventory/templates/inventory.html index 545a67c..518e2f8 100644 --- a/inventory/templates/inventory.html +++ b/inventory/templates/inventory.html @@ -15,15 +15,18 @@
-
-
+
+
-
+
+
+ {{ images.render_image(item.id, item.photo) }} +
@@ -115,11 +118,6 @@
- {# - - - #} {{ editor.render_editor( id = "notes", title = "Notes & Comments", diff --git a/inventory/templates/layout.html b/inventory/templates/layout.html index 0902d5a..fb12b0a 100644 --- a/inventory/templates/layout.html +++ b/inventory/templates/layout.html @@ -2,6 +2,7 @@ {% import "fragments/_combobox_fragment.html" as combos %} {% import "fragments/_editor_fragment.html" as editor %} {% import "fragments/_icon_fragment.html" as icons %} +{% import "fragments/_image_fragment.html" as images %} {% import "fragments/_link_fragment.html" as links %} {% import "fragments/_table_fragment.html" as tables %}