More image changes. Delete and replacement logic.

This commit is contained in:
Yaro Kasear 2025-07-11 14:03:16 -05:00
parent 7d96839af8
commit 48ad5847b9
9 changed files with 67 additions and 30 deletions

View file

@ -1,5 +1,6 @@
from flask import Flask
from flask import Flask, current_app
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.engine.url import make_url
import logging
import os
@ -13,6 +14,14 @@ if not logger.handlers:
handler.setFormatter(logging.Formatter('%(asctime)s [%(levelname)s] %(message)s'))
logger.addHandler(handler)
def is_in_memory_sqlite():
uri = current_app.config.get("SQLALCHEMY_DATABASE_URI")
if not uri:
return False
url = make_url(uri)
print(url, url.database)
return url.get_backend_name() == "sqlite" and url.database == ":memory:"
def create_app():
from config import Config
app = Flask(__name__)
@ -23,7 +32,8 @@ def create_app():
with app.app_context():
from . import models
# db.create_all()
if is_in_memory_sqlite():
db.create_all()
from .routes import main
from .routes.images import image_bp

View file

@ -3,5 +3,5 @@ from .. import db
worklog_images = db.Table(
'worklog_images',
db.Column('worklog_id', db.Integer, db.ForeignKey('work_log.id'), primary_key=True),
db.Column('image_id', db.Integer, db.ForeignKey('images.id'), primary_key=True),
db.Column('image_id', db.Integer, db.ForeignKey('images.id', ondelete='CASCADE'), primary_key=True),
)

View file

@ -1,6 +1,6 @@
from typing import Any, List, Optional, TYPE_CHECKING
from inventory.models.image import Image
from .image import Image
if TYPE_CHECKING:
from .brands import Brand
from .items import Item
@ -34,14 +34,14 @@ class Inventory(db.Model, ImageAttachable):
location_id: Mapped[Optional[str]] = mapped_column(ForeignKey("rooms.id"))
barcode: Mapped[Optional[str]] = mapped_column(Unicode(255))
shared: Mapped[Optional[bool]] = mapped_column(Boolean, server_default=text('((0))'))
image_id: Mapped[Optional[int]] = mapped_column(ForeignKey('images.id'), nullable=True)
image_id: Mapped[Optional[int]] = mapped_column(ForeignKey('images.id', ondelete='SET NULL'), nullable=True)
location: Mapped[Optional['Room']] = relationship('Room', back_populates='inventory')
owner = relationship('User', back_populates='inventory')
brand: Mapped[Optional['Brand']] = relationship('Brand', back_populates='inventory')
item: Mapped['Item'] = relationship('Item', back_populates='inventory')
work_logs: Mapped[List['WorkLog']] = relationship('WorkLog', back_populates='work_item')
image: Mapped[Optional['Image']] = relationship('Image', back_populates='inventory')
image: Mapped[Optional['Image']] = relationship('Image', back_populates='inventory', passive_deletes=True)
def __init__(self, timestamp: datetime.datetime, condition: str, type_id: Optional[int] = None,
name: Optional[str] = None, serial: Optional[str] = None,

View file

@ -21,14 +21,14 @@ class User(db.Model, ImageAttachable):
first_name: Mapped[Optional[str]] = mapped_column(Unicode(255), nullable=True)
location_id: Mapped[Optional[int]] = mapped_column(ForeignKey("rooms.id"), nullable=True)
supervisor_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("users.id"))
image_id: Mapped[Optional[int]] = mapped_column(ForeignKey('images.id'), nullable=True)
image_id: Mapped[Optional[int]] = mapped_column(ForeignKey('images.id', ondelete='SET NULL'), nullable=True)
supervisor: Mapped[Optional['User']] = relationship('User', remote_side='User.id', back_populates='subordinates')
subordinates: Mapped[List['User']] = relationship('User', back_populates='supervisor')
work_logs: Mapped[List['WorkLog']] = relationship('WorkLog', back_populates='contact')
location: Mapped[Optional['Room']] = relationship('Room', back_populates='users')
inventory: Mapped[List['Inventory']] = relationship('Inventory', back_populates='owner')
image: Mapped[Optional['Image']] = relationship('Image', back_populates='user')
image: Mapped[Optional['Image']] = relationship('Image', back_populates='user', passive_deletes=True)
@property
def full_name(self) -> str:

View file

@ -35,7 +35,7 @@ class WorkLog(db.Model, ImageAttachable):
cascade='all, delete-orphan',
order_by='WorkNote.timestamp'
)
images: Mapped[List['Image']] = relationship('Image', secondary=worklog_images, back_populates='worklogs')
images: Mapped[List['Image']] = relationship('Image', secondary=worklog_images, back_populates='worklogs', passive_deletes=True)
def __init__(
self,

View file

@ -1,4 +1,5 @@
import hashlib
import os
from flask import url_for
from werkzeug.utils import secure_filename
@ -69,16 +70,14 @@ worklog_headers = {
def link(text, endpoint, **values):
return {"text": text, "url": url_for(endpoint, **values)}
def generate_hashed_filename(file_storage, model_name: str) -> str:
# Hash file contents
file_bytes = file_storage.read()
sha = hashlib.sha256(file_bytes).hexdigest()
# Reset the stream so Flask can read it again later
file_storage.stream.seek(0)
def generate_hashed_filename(file) -> str:
content = file.read()
file.seek(0) # Reset after reading
original_name = secure_filename(file_storage.filename)
return f"{model_name}/{sha}_{original_name}"
hash = hashlib.sha256(content).hexdigest()
ext = os.path.splitext(file.filename)[1]
return f"{hash}_{file.filename}"
def get_image_attachable_class_by_name(name: str):
for cls in ImageAttachable.__subclasses__():

View file

@ -12,19 +12,11 @@ image_bp = Blueprint("image_api", __name__)
def save_image(file, model: str) -> str:
assert current_app.static_folder
hashed_name = generate_hashed_filename(file, model)
rel_path = posixpath.join("uploads", "images", hashed_name)
abs_path = os.path.join(current_app.static_folder, "uploads", "images", rel_path)
filename = generate_hashed_filename(file)
rel_path = posixpath.join("uploads", "images", model, filename)
abs_path = os.path.join(current_app.static_folder, 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))
os.makedirs(os.path.dirname(abs_path), exist_ok=True)
file.save(abs_path)
return rel_path
@ -86,3 +78,11 @@ def delete_image(image_id):
image = db.session.get(Image, image_id)
if not image:
return jsonify({"success": False, "error": "Image not found"})
abs_path = os.path.join(current_app.static_folder, image.filename.replace("\\", "/"))
if os.path.exists(abs_path):
os.remove(abs_path)
db.session.delete(image)
db.session.commit()
return jsonify({"success": True})

View file

@ -84,8 +84,26 @@ const ImageWidget = (() => {
});
}
function deleteImage(inventoryId, imageId) {
if (!confirm("Are you sure you want to delete this image?")) return;
fetch(`/api/images/${imageId}`, {
method: "DELETE"
}).then(response => response.json()).then(data => {
if (data.success) {
renderToast({ message: "Image deleted.", type: "success" });
location.reload(); // Update view
} else {
renderToast({ message: `Failed to delete: ${data.error}`, type: "danger" });
}
}).catch(err => {
renderToast({ message: `Error deleting image: ${err}`, type: "danger" });
});
}
return {
submitImageUpload
submitImageUpload,
deleteImage
}
})();

View file

@ -12,6 +12,16 @@
<div class="modal-body text-center">
<img src="{{ url_for('static', filename=image.filename) }}" alt="Image of ID {{ id }}" class="img-fluid">
</div>
<div class="modal-footer justify-content-between">
<button class="btn btn-danger" onclick="ImageWidget.deleteImage('{{ id }}', '{{ image.id }}')">
{{ icons.render_icon('trash') }}
</button>
<label class="btn btn-secondary mb0">
{{ icons.render_icon('upload') }}
<input type="file" class="d-none" onchange="ImageWidget.submitImageUpload('{{ id }}')"
id="image-upload-input-{{ id }}">
</label>
</div>
</div>
</div>
</div>