Implement PhotoAttachable interface in Inventory, User, and WorkLog models; add photo upload API endpoint with file handling and attachment logic.
This commit is contained in:
parent
92dce08d1c
commit
e1cb99f2d1
7 changed files with 101 additions and 7 deletions
|
@ -11,8 +11,9 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|||
import datetime
|
||||
|
||||
from . import db
|
||||
from .photo import PhotoAttachable
|
||||
|
||||
class Inventory(db.Model):
|
||||
class Inventory(db.Model, PhotoAttachable):
|
||||
__tablename__ = 'inventory'
|
||||
__table_args__ = (
|
||||
Index('Inventory$Barcode', 'barcode'),
|
||||
|
|
|
@ -10,6 +10,7 @@ from sqlalchemy import Integer, Unicode, DateTime, func
|
|||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from . import db
|
||||
from .photo_links import worklog_photos
|
||||
|
||||
class Photo(db.Model):
|
||||
__tablename__ = 'photos'
|
||||
|
@ -21,7 +22,16 @@ class Photo(db.Model):
|
|||
|
||||
inventory: Mapped[Optional['Inventory']] = relationship('Inventory', back_populates='photo')
|
||||
user: Mapped[Optional['User']] = relationship('User', back_populates='photo')
|
||||
worklogs: Mapped[List['WorkLog']] = relationship('WorkLog', back_populates='photos')
|
||||
worklogs: Mapped[List['WorkLog']] = relationship('WorkLog', secondary=worklog_photos, back_populates='photos')
|
||||
|
||||
def __init__(self, filename: str, timestamp: Optional[datetime.datetime] = None, caption: Optional[str] = None):
|
||||
self.filename = filename
|
||||
self.caption = caption or ""
|
||||
self.timestamp = timestamp
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Photo(id={self.id}, filename={self.filename})>"
|
||||
|
||||
class PhotoAttachable:
|
||||
def attach_photo(self, photo: 'Photo') -> None:
|
||||
raise NotImplementedError("This model doesn't know how to attach photos.")
|
||||
|
|
|
@ -9,8 +9,9 @@ from sqlalchemy import Boolean, ForeignKey, Identity, Integer, Unicode, text
|
|||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from . import db
|
||||
from .photo import PhotoAttachable
|
||||
|
||||
class User(db.Model):
|
||||
class User(db.Model, PhotoAttachable):
|
||||
__tablename__ = 'users'
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, Identity(start=1, increment=1), primary_key=True)
|
||||
|
|
|
@ -10,10 +10,11 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|||
import datetime
|
||||
|
||||
from . import db
|
||||
from .photo import PhotoAttachable
|
||||
from .photo_links import worklog_photos
|
||||
from .work_note import WorkNote
|
||||
|
||||
class WorkLog(db.Model):
|
||||
class WorkLog(db.Model, PhotoAttachable):
|
||||
__tablename__ = 'work_log'
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, Identity(start=1, increment=1), primary_key=True)
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
import hashlib
|
||||
|
||||
from flask import url_for
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from ..models import Inventory
|
||||
|
||||
from ..models.photo import PhotoAttachable
|
||||
|
||||
inventory_headers = {
|
||||
"Date Entered": lambda i: {"text": i.timestamp.strftime("%Y-%m-%d") if i.timestamp else None},
|
||||
"Identifier": lambda i: {"text": i.identifier},
|
||||
|
@ -63,3 +68,20 @@ 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)
|
||||
|
||||
original_name = secure_filename(file_storage.filename)
|
||||
return f"{model_name}/{sha}_{original_name}"
|
||||
|
||||
def get_photo_attachable_class_by_name(name: str):
|
||||
for cls in PhotoAttachable.__subclasses__():
|
||||
if cls.__tablename__ == name:
|
||||
return cls
|
||||
return None
|
||||
|
|
58
inventory/routes/photos.py
Normal file
58
inventory/routes/photos.py
Normal file
|
@ -0,0 +1,58 @@
|
|||
import os
|
||||
|
||||
import datetime
|
||||
from flask import Blueprint, current_app, request, jsonify
|
||||
|
||||
from .helpers import generate_hashed_filename, get_photo_attachable_class_by_name
|
||||
from .. import db
|
||||
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
|
||||
abs_path = os.path.join(current_app.static_folder, rel_path)
|
||||
os.makedirs(os.path.dirname(abs_path), exist_ok=True)
|
||||
|
||||
file.save(abs_path)
|
||||
|
||||
return rel_path
|
||||
|
||||
@photo_bp.route("/api/photos", methods=["POST"])
|
||||
def upload_photo():
|
||||
file = request.files.get("file")
|
||||
model = request.form.get("model")
|
||||
model_id = request.form.get("model_id")
|
||||
caption = request.form.get("caption", "")
|
||||
|
||||
if not file or not model or not model_id:
|
||||
return jsonify({"success": False, "error": "Missing file, model, or model_id"}), 400
|
||||
|
||||
ModelClass = get_photo_attachable_class_by_name(model)
|
||||
if not ModelClass:
|
||||
return jsonify({"success": False, "error": f"Model '{model}' does not support photo attachments."}), 400
|
||||
|
||||
try:
|
||||
model_id = int(model_id)
|
||||
except ValueError:
|
||||
return jsonify({"success": False, "error": "model_id must be an integer"}), 400
|
||||
|
||||
# Save file
|
||||
rel_path = save_photo(file, model)
|
||||
|
||||
# Create Photo row
|
||||
photo = Photo(filename=rel_path, caption=caption)
|
||||
db.session.add(photo)
|
||||
|
||||
# Attach photo to model
|
||||
target = db.session.get(ModelClass, model_id)
|
||||
if not target:
|
||||
return jsonify({"success": False, "error": f"No {model} found with ID {model_id}"}), 404
|
||||
|
||||
target.attach_photo(photo)
|
||||
|
||||
db.session.commit()
|
||||
return jsonify({"success": True, "id": photo.id}), 201
|
|
@ -4,13 +4,14 @@ version = "0.1.0"
|
|||
description = "A Flask app for tracking inventory."
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"python-dotenv",
|
||||
"alembic",
|
||||
"beautifulsoup4",
|
||||
"flask",
|
||||
"flask_sqlalchemy",
|
||||
"pandas",
|
||||
"pyodbc",
|
||||
"beautifulsoup4",
|
||||
"alembic"
|
||||
"python-dotenv",
|
||||
"Werkzeug"
|
||||
]
|
||||
|
||||
[build-system]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue