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 flask_sqlalchemy import SQLAlchemy
|
||||||
|
from sqlalchemy.engine.url import make_url
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
@ -13,6 +14,14 @@ if not logger.handlers:
|
||||||
handler.setFormatter(logging.Formatter('%(asctime)s [%(levelname)s] %(message)s'))
|
handler.setFormatter(logging.Formatter('%(asctime)s [%(levelname)s] %(message)s'))
|
||||||
logger.addHandler(handler)
|
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():
|
def create_app():
|
||||||
from config import Config
|
from config import Config
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
@ -23,7 +32,8 @@ def create_app():
|
||||||
|
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
from . import models
|
from . import models
|
||||||
# db.create_all()
|
if is_in_memory_sqlite():
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
from .routes import main
|
from .routes import main
|
||||||
from .routes.images import image_bp
|
from .routes.images import image_bp
|
||||||
|
|
|
@ -3,5 +3,5 @@ from .. import db
|
||||||
worklog_images = db.Table(
|
worklog_images = db.Table(
|
||||||
'worklog_images',
|
'worklog_images',
|
||||||
db.Column('worklog_id', db.Integer, db.ForeignKey('work_log.id'), primary_key=True),
|
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 typing import Any, List, Optional, TYPE_CHECKING
|
||||||
|
|
||||||
from inventory.models.image import Image
|
from .image import Image
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .brands import Brand
|
from .brands import Brand
|
||||||
from .items import Item
|
from .items import Item
|
||||||
|
@ -34,14 +34,14 @@ class Inventory(db.Model, ImageAttachable):
|
||||||
location_id: Mapped[Optional[str]] = mapped_column(ForeignKey("rooms.id"))
|
location_id: Mapped[Optional[str]] = mapped_column(ForeignKey("rooms.id"))
|
||||||
barcode: Mapped[Optional[str]] = mapped_column(Unicode(255))
|
barcode: Mapped[Optional[str]] = mapped_column(Unicode(255))
|
||||||
shared: Mapped[Optional[bool]] = mapped_column(Boolean, server_default=text('((0))'))
|
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')
|
location: Mapped[Optional['Room']] = relationship('Room', back_populates='inventory')
|
||||||
owner = relationship('User', back_populates='inventory')
|
owner = relationship('User', back_populates='inventory')
|
||||||
brand: Mapped[Optional['Brand']] = relationship('Brand', back_populates='inventory')
|
brand: Mapped[Optional['Brand']] = relationship('Brand', back_populates='inventory')
|
||||||
item: Mapped['Item'] = relationship('Item', back_populates='inventory')
|
item: Mapped['Item'] = relationship('Item', back_populates='inventory')
|
||||||
work_logs: Mapped[List['WorkLog']] = relationship('WorkLog', back_populates='work_item')
|
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,
|
def __init__(self, timestamp: datetime.datetime, condition: str, type_id: Optional[int] = None,
|
||||||
name: Optional[str] = None, serial: Optional[str] = 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)
|
first_name: Mapped[Optional[str]] = mapped_column(Unicode(255), nullable=True)
|
||||||
location_id: Mapped[Optional[int]] = mapped_column(ForeignKey("rooms.id"), 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"))
|
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')
|
supervisor: Mapped[Optional['User']] = relationship('User', remote_side='User.id', back_populates='subordinates')
|
||||||
subordinates: Mapped[List['User']] = relationship('User', back_populates='supervisor')
|
subordinates: Mapped[List['User']] = relationship('User', back_populates='supervisor')
|
||||||
work_logs: Mapped[List['WorkLog']] = relationship('WorkLog', back_populates='contact')
|
work_logs: Mapped[List['WorkLog']] = relationship('WorkLog', back_populates='contact')
|
||||||
location: Mapped[Optional['Room']] = relationship('Room', back_populates='users')
|
location: Mapped[Optional['Room']] = relationship('Room', back_populates='users')
|
||||||
inventory: Mapped[List['Inventory']] = relationship('Inventory', back_populates='owner')
|
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
|
@property
|
||||||
def full_name(self) -> str:
|
def full_name(self) -> str:
|
||||||
|
|
|
@ -35,7 +35,7 @@ class WorkLog(db.Model, ImageAttachable):
|
||||||
cascade='all, delete-orphan',
|
cascade='all, delete-orphan',
|
||||||
order_by='WorkNote.timestamp'
|
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__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import os
|
||||||
|
|
||||||
from flask import url_for
|
from flask import url_for
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
|
@ -69,16 +70,14 @@ worklog_headers = {
|
||||||
def link(text, endpoint, **values):
|
def link(text, endpoint, **values):
|
||||||
return {"text": text, "url": url_for(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
|
def generate_hashed_filename(file) -> str:
|
||||||
file_storage.stream.seek(0)
|
content = file.read()
|
||||||
|
file.seek(0) # Reset after reading
|
||||||
|
|
||||||
original_name = secure_filename(file_storage.filename)
|
hash = hashlib.sha256(content).hexdigest()
|
||||||
return f"{model_name}/{sha}_{original_name}"
|
ext = os.path.splitext(file.filename)[1]
|
||||||
|
return f"{hash}_{file.filename}"
|
||||||
|
|
||||||
def get_image_attachable_class_by_name(name: str):
|
def get_image_attachable_class_by_name(name: str):
|
||||||
for cls in ImageAttachable.__subclasses__():
|
for cls in ImageAttachable.__subclasses__():
|
||||||
|
|
|
@ -12,19 +12,11 @@ image_bp = Blueprint("image_api", __name__)
|
||||||
def save_image(file, model: str) -> str:
|
def save_image(file, model: str) -> str:
|
||||||
assert current_app.static_folder
|
assert current_app.static_folder
|
||||||
|
|
||||||
hashed_name = generate_hashed_filename(file, model)
|
filename = generate_hashed_filename(file)
|
||||||
rel_path = posixpath.join("uploads", "images", hashed_name)
|
rel_path = posixpath.join("uploads", "images", model, filename)
|
||||||
abs_path = os.path.join(current_app.static_folder, "uploads", "images", rel_path)
|
abs_path = os.path.join(current_app.static_folder, rel_path)
|
||||||
|
|
||||||
dir_path = os.path.dirname(abs_path)
|
os.makedirs(os.path.dirname(abs_path), exist_ok=True)
|
||||||
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)
|
file.save(abs_path)
|
||||||
return rel_path
|
return rel_path
|
||||||
|
@ -86,3 +78,11 @@ def delete_image(image_id):
|
||||||
image = db.session.get(Image, image_id)
|
image = db.session.get(Image, image_id)
|
||||||
if not image:
|
if not image:
|
||||||
return jsonify({"success": False, "error": "Image not found"})
|
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 {
|
return {
|
||||||
submitImageUpload
|
submitImageUpload,
|
||||||
|
deleteImage
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,16 @@
|
||||||
<div class="modal-body text-center">
|
<div class="modal-body text-center">
|
||||||
<img src="{{ url_for('static', filename=image.filename) }}" alt="Image of ID {{ id }}" class="img-fluid">
|
<img src="{{ url_for('static', filename=image.filename) }}" alt="Image of ID {{ id }}" class="img-fluid">
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue