More image changes. Delete and replacement logic.
This commit is contained in:
parent
7d96839af8
commit
48ad5847b9
9 changed files with 67 additions and 30 deletions
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
)
|
|
@ -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,
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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__():
|
||||
|
|
|
@ -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})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
})();
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue