Add image upload functionality and enhance inventory template with image rendering
This commit is contained in:
parent
ca3417c269
commit
a2b035f522
8 changed files with 84 additions and 15 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,4 +1,6 @@
|
|||
**/__pycache__/
|
||||
inventory/static/uploads/*
|
||||
!inventory/static/uploads/.gitkeep
|
||||
.venv/
|
||||
.env
|
||||
*.db
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"])
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
0
inventory/static/uploads/.gitkeep
Normal file
0
inventory/static/uploads/.gitkeep
Normal file
22
inventory/templates/fragments/_image_fragment.html
Normal file
22
inventory/templates/fragments/_image_fragment.html
Normal 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 %}
|
|
@ -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",
|
||||
|
|
|
@ -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 %}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue