Add image upload functionality and enhance inventory template with image rendering

This commit is contained in:
Yaro Kasear 2025-07-11 11:48:53 -05:00
parent ca3417c269
commit a2b035f522
8 changed files with 84 additions and 15 deletions

2
.gitignore vendored
View file

@ -1,4 +1,6 @@
**/__pycache__/
inventory/static/uploads/*
!inventory/static/uploads/.gitkeep
.venv/
.env
*.db

View file

@ -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

View file

@ -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"])

View file

@ -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;

View file

View file

@ -0,0 +1,22 @@
{% import "fragments/_icon_fragment.html" as icons %}
{% macro render_image(id, image=None) %}
<!-- Image fragment -->
<div class="image-slot text-center">
{% if image %}
<img src="{{ url_for('static', filename=image.filename) }}" alt="Photo of ID {{ id }}" class="img-thumbnail"
style="max-height: 256px;">
{% else %}
<a href="#" class="link-secondary" onclick="document.getElementById('image-upload-input-{{ id }}').click(); return false;">
{{ icons.render_icon('image', 256) }}
</a>
<form method="POST" enctype="multipart/form-data" id="image-upload-form-{{ id }}" class="d-none">
<input type="file" id="image-upload-input-{{ id }}" name="file"
onchange="ImageWidget.submitImageUpload('{{ id }}')">
<input type="hidden" name="model" value="inventory">
<input type="hidden" name="model_id" value="{{ id }}">
<input type="hidden" name="caption" value="Uploaded via UI">
</form>
{% endif %}
</div>
{% endmacro %}

View file

@ -15,15 +15,18 @@
<input type="hidden" id="inventoryId" value="{{ item.id }}">
<div class="container">
<div class="row">
<div class="col-6">
<div class="row align-items-center">
<div class="col">
<label for="timestamp" class="form-label">Date Entered</label>
<input type="date" class="form-control" name="timestamp" value="{{ item.timestamp.date().isoformat() }}">
</div>
<div class="col-6">
<div class="col">
<label for="identifier" class="form-label">Identifier</label>
<input type="text" class="form-control-plaintext" value="{{ item.identifier }}" readonly>
</div>
<div class="col text-center">
{{ images.render_image(item.id, item.photo) }}
</div>
</div>
<div class="row">
<div class="col-4">
@ -115,11 +118,6 @@
</div>
<div class="row">
<div class="col p-3">
{#
<label for="notes" class="form-label">Notes & Comments</label>
<textarea name="notes" id="notes" class="form-control"
rows="10">{{ item.notes if item.notes else '' }}</textarea>
#}
{{ editor.render_editor(
id = "notes",
title = "Notes & Comments",

View file

@ -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 %}