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:
Yaro Kasear 2025-07-11 09:21:31 -05:00
parent 92dce08d1c
commit e1cb99f2d1
7 changed files with 101 additions and 7 deletions

View file

@ -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'),

View file

@ -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.")

View file

@ -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)

View file

@ -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)

View file

@ -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

View 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

View file

@ -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]