Rename "photo" to "image."

This commit is contained in:
Yaro Kasear 2025-07-11 13:01:08 -05:00
parent 84db8592cb
commit 7d96839af8
11 changed files with 78 additions and 72 deletions

View file

@ -26,8 +26,8 @@ def create_app():
# db.create_all() # db.create_all()
from .routes import main from .routes import main
from .routes.photos import photo_bp from .routes.images import image_bp
app.register_blueprint(main) app.register_blueprint(main)
app.register_blueprint(photo_bp) app.register_blueprint(image_bp)
return app return app

View file

@ -11,8 +11,8 @@ from .users import User
from .work_log import WorkLog from .work_log import WorkLog
from .rooms import Room from .rooms import Room
from .work_note import WorkNote from .work_note import WorkNote
from .photo import Photo from .image import Image
from .photo_links import worklog_photos from .image_links import worklog_images
__all__ = [ __all__ = [
"db", "db",
@ -25,6 +25,6 @@ __all__ = [
"WorkLog", "WorkLog",
"Room", "Room",
"WorkNote", "WorkNote",
"Photo", "Image",
"worklog_photos" "worklog_images"
] ]

View file

@ -10,27 +10,27 @@ from sqlalchemy import Integer, Unicode, DateTime, func
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from . import db from . import db
from .photo_links import worklog_photos from .image_links import worklog_images
class Photo(db.Model): class Image(db.Model):
__tablename__ = 'photos' __tablename__ = 'images'
id: Mapped[int] = mapped_column(Integer, primary_key=True) id: Mapped[int] = mapped_column(Integer, primary_key=True)
filename: Mapped[str] = mapped_column(Unicode(512)) filename: Mapped[str] = mapped_column(Unicode(512))
caption: Mapped[str] = mapped_column(Unicode(255), default="") caption: Mapped[str] = mapped_column(Unicode(255), default="")
timestamp: Mapped[datetime.datetime] = mapped_column(DateTime, default=datetime.datetime.now(), server_default=func.now()) timestamp: Mapped[datetime.datetime] = mapped_column(DateTime, default=datetime.datetime.now(), server_default=func.now())
inventory: Mapped[Optional['Inventory']] = relationship('Inventory', back_populates='photo') inventory: Mapped[Optional['Inventory']] = relationship('Inventory', back_populates='image')
user: Mapped[Optional['User']] = relationship('User', back_populates='photo') user: Mapped[Optional['User']] = relationship('User', back_populates='image')
worklogs: Mapped[List['WorkLog']] = relationship('WorkLog', secondary=worklog_photos, back_populates='photos') worklogs: Mapped[List['WorkLog']] = relationship('WorkLog', secondary=worklog_images, back_populates='images')
def __init__(self, filename: str, caption: Optional[str] = None): def __init__(self, filename: str, caption: Optional[str] = None):
self.filename = filename self.filename = filename
self.caption = caption or "" self.caption = caption or ""
def __repr__(self): def __repr__(self):
return f"<Photo(id={self.id}, filename={self.filename})>" return f"<Image(id={self.id}, filename={self.filename})>"
class PhotoAttachable: class ImageAttachable:
def attach_photo(self, photo: 'Photo') -> None: def attach_image(self, image: 'Image') -> None:
raise NotImplementedError("This model doesn't know how to attach photos.") raise NotImplementedError("This model doesn't know how to attach images.")

View file

@ -1,7 +1,7 @@
from .. import db from .. import db
worklog_photos = db.Table( worklog_images = db.Table(
'worklog_photos', '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('photo_id', db.Integer, db.ForeignKey('photos.id'), primary_key=True), db.Column('image_id', db.Integer, db.ForeignKey('images.id'), primary_key=True),
) )

View file

@ -1,21 +1,21 @@
from typing import Any, List, Optional, TYPE_CHECKING from typing import Any, List, Optional, TYPE_CHECKING
from inventory.models.photo import Photo from inventory.models.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
from .work_log import WorkLog from .work_log import WorkLog
from .rooms import Room from .rooms import Room
from .photo import Photo from .image import Image
from sqlalchemy import Boolean, ForeignKey, Identity, Index, Integer, Unicode, DateTime, text from sqlalchemy import Boolean, ForeignKey, Identity, Index, Integer, Unicode, DateTime, text
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
import datetime import datetime
from . import db from . import db
from .photo import PhotoAttachable from .image import ImageAttachable
class Inventory(db.Model, PhotoAttachable): class Inventory(db.Model, ImageAttachable):
__tablename__ = 'inventory' __tablename__ = 'inventory'
__table_args__ = ( __table_args__ = (
Index('Inventory$Barcode', 'barcode'), Index('Inventory$Barcode', 'barcode'),
@ -34,14 +34,14 @@ class Inventory(db.Model, PhotoAttachable):
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))'))
photo_id: Mapped[Optional[int]] = mapped_column(ForeignKey('photos.id'), nullable=True) image_id: Mapped[Optional[int]] = mapped_column(ForeignKey('images.id'), 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')
photo: Mapped[Optional['Photo']] = relationship('Photo', back_populates='inventory') image: Mapped[Optional['Image']] = relationship('Image', back_populates='inventory')
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,
@ -128,5 +128,5 @@ class Inventory(db.Model, PhotoAttachable):
shared=bool(data.get("shared", False)) shared=bool(data.get("shared", False))
) )
def attach_photo(self, photo: Photo) -> None: def attach_image(self, image: Image) -> None:
self.photo = photo self.image = image

View file

@ -3,15 +3,15 @@ if TYPE_CHECKING:
from .inventory import Inventory from .inventory import Inventory
from .rooms import Room from .rooms import Room
from .work_log import WorkLog from .work_log import WorkLog
from .photo import Photo from .image import Image
from sqlalchemy import Boolean, ForeignKey, Identity, Integer, Unicode, text from sqlalchemy import Boolean, ForeignKey, Identity, Integer, Unicode, text
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from . import db from . import db
from .photo import PhotoAttachable from .image import ImageAttachable
class User(db.Model, PhotoAttachable): class User(db.Model, ImageAttachable):
__tablename__ = 'users' __tablename__ = 'users'
id: Mapped[int] = mapped_column(Integer, Identity(start=1, increment=1), primary_key=True) id: Mapped[int] = mapped_column(Integer, Identity(start=1, increment=1), primary_key=True)
@ -21,14 +21,14 @@ class User(db.Model, PhotoAttachable):
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"))
photo_id: Mapped[Optional[int]] = mapped_column(ForeignKey('photos.id'), nullable=True) image_id: Mapped[Optional[int]] = mapped_column(ForeignKey('images.id'), 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')
photo: Mapped[Optional['Photo']] = relationship('Photo', back_populates='user') image: Mapped[Optional['Image']] = relationship('Image', back_populates='user')
@property @property
def full_name(self) -> str: def full_name(self) -> str:

View file

@ -1,7 +1,7 @@
from typing import Optional, Any, List, TYPE_CHECKING from typing import Optional, Any, List, TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from .inventory import Inventory from .inventory import Inventory
from .photo import Photo from .image import Image
from .users import User from .users import User
from .work_note import WorkNote from .work_note import WorkNote
@ -10,11 +10,11 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
import datetime import datetime
from . import db from . import db
from .photo import PhotoAttachable from .image import ImageAttachable
from .photo_links import worklog_photos from .image_links import worklog_images
from .work_note import WorkNote from .work_note import WorkNote
class WorkLog(db.Model, PhotoAttachable): class WorkLog(db.Model, ImageAttachable):
__tablename__ = 'work_log' __tablename__ = 'work_log'
id: Mapped[int] = mapped_column(Integer, Identity(start=1, increment=1), primary_key=True) id: Mapped[int] = mapped_column(Integer, Identity(start=1, increment=1), primary_key=True)
@ -35,7 +35,7 @@ class WorkLog(db.Model, PhotoAttachable):
cascade='all, delete-orphan', cascade='all, delete-orphan',
order_by='WorkNote.timestamp' order_by='WorkNote.timestamp'
) )
photos: Mapped[List['Photo']] = relationship('Photo', secondary=worklog_photos, back_populates='worklogs') images: Mapped[List['Image']] = relationship('Image', secondary=worklog_images, back_populates='worklogs')
def __init__( def __init__(
self, self,

View file

@ -5,7 +5,7 @@ from werkzeug.utils import secure_filename
from ..models import Inventory from ..models import Inventory
from ..models.photo import PhotoAttachable from ..models.image import ImageAttachable
inventory_headers = { inventory_headers = {
"Date Entered": lambda i: {"text": i.timestamp.strftime("%Y-%m-%d") if i.timestamp else None}, "Date Entered": lambda i: {"text": i.timestamp.strftime("%Y-%m-%d") if i.timestamp else None},
@ -80,8 +80,8 @@ def generate_hashed_filename(file_storage, model_name: str) -> str:
original_name = secure_filename(file_storage.filename) original_name = secure_filename(file_storage.filename)
return f"{model_name}/{sha}_{original_name}" return f"{model_name}/{sha}_{original_name}"
def get_photo_attachable_class_by_name(name: str): def get_image_attachable_class_by_name(name: str):
for cls in PhotoAttachable.__subclasses__(): for cls in ImageAttachable.__subclasses__():
if getattr(cls, '__tablename__', None) == name: if getattr(cls, '__tablename__', None) == name:
return cls return cls
return None return None

View file

@ -3,18 +3,18 @@ import posixpath
from flask import Blueprint, current_app, request, jsonify from flask import Blueprint, current_app, request, jsonify
from .helpers import generate_hashed_filename, get_photo_attachable_class_by_name from .helpers import generate_hashed_filename, get_image_attachable_class_by_name
from .. import db from .. import db
from ..models import Photo from ..models import Image
photo_bp = Blueprint("photo_api", __name__) image_bp = Blueprint("image_api", __name__)
def save_photo(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) hashed_name = generate_hashed_filename(file, model)
rel_path = posixpath.join("uploads", "photos", hashed_name) rel_path = posixpath.join("uploads", "images", hashed_name)
abs_path = os.path.join(current_app.static_folder, "uploads", "photos", rel_path) abs_path = os.path.join(current_app.static_folder, "uploads", "images", rel_path)
dir_path = os.path.dirname(abs_path) dir_path = os.path.dirname(abs_path)
if not os.path.exists(dir_path): if not os.path.exists(dir_path):
@ -29,8 +29,8 @@ def save_photo(file, model: str) -> str:
file.save(abs_path) file.save(abs_path)
return rel_path return rel_path
@photo_bp.route("/api/photos", methods=["POST"]) @image_bp.route("/api/images", methods=["POST"])
def upload_photo(): def upload_image():
file = request.files.get("file") file = request.files.get("file")
model = request.form.get("model") model = request.form.get("model")
model_id = request.form.get("model_id") model_id = request.form.get("model_id")
@ -39,9 +39,9 @@ def upload_photo():
if not file or not model or not model_id: if not file or not model or not model_id:
return jsonify({"success": False, "error": "Missing file, model, or model_id"}), 400 return jsonify({"success": False, "error": "Missing file, model, or model_id"}), 400
ModelClass = get_photo_attachable_class_by_name(model) ModelClass = get_image_attachable_class_by_name(model)
if not ModelClass: if not ModelClass:
return jsonify({"success": False, "error": f"Model '{model}' does not support photo attachments."}), 400 return jsonify({"success": False, "error": f"Model '{model}' does not support image attachments."}), 400
try: try:
model_id = int(model_id) model_id = int(model_id)
@ -49,34 +49,40 @@ def upload_photo():
return jsonify({"success": False, "error": "model_id must be an integer"}), 400 return jsonify({"success": False, "error": "model_id must be an integer"}), 400
# Save file # Save file
rel_path = save_photo(file, model) rel_path = save_image(file, model)
print(rel_path) print(rel_path)
# Create Photo row # Create Image row
photo = Photo(filename=rel_path, caption=caption) image = Image(filename=rel_path, caption=caption)
db.session.add(photo) db.session.add(image)
# Attach photo to model # Attach image to model
target = db.session.get(ModelClass, model_id) target = db.session.get(ModelClass, model_id)
if not target: if not target:
return jsonify({"success": False, "error": f"No {model} found with ID {model_id}"}), 404 return jsonify({"success": False, "error": f"No {model} found with ID {model_id}"}), 404
target.attach_photo(photo) target.attach_image(image)
db.session.commit() db.session.commit()
return jsonify({"success": True, "id": photo.id}), 201 return jsonify({"success": True, "id": image.id}), 201
@photo_bp.route("/api/photos/<int:photo_id>", methods=["GET"]) @image_bp.route("/api/images/<int:image_id>", methods=["GET"])
def get_photo(photo_id: int): def get_image(image_id: int):
photo = db.session.get(Photo, photo_id) image = db.session.get(Image, image_id)
if not photo: if not image:
return jsonify({"success": False, "error": f"No photo found with ID {photo_id}"}), 404 return jsonify({"success": False, "error": f"No image found with ID {image_id}"}), 404
return jsonify({ return jsonify({
"success": True, "success": True,
"id": photo.id, "id": image.id,
"filename": photo.filename, "filename": image.filename,
"caption": photo.caption, "caption": image.caption,
"timestamp": photo.timestamp.isoformat() if photo.timestamp else None, "timestamp": image.timestamp.isoformat() if image.timestamp else None,
"url": f"/static/{photo.filename}" "url": f"/static/{image.filename}"
}) })
@image_bp.route("/api/images/<int:image_id>", methods=["DELETE"])
def delete_image(image_id):
image = db.session.get(Image, image_id)
if not image:
return jsonify({"success": False, "error": "Image not found"})

View file

@ -59,7 +59,7 @@ const ImageWidget = (() => {
console.log(form); console.log(form);
const formData = new FormData(form); const formData = new FormData(form);
fetch("/api/photos", { fetch("/api/images", {
method: "POST", method: "POST",
body: formData body: formData
}).then(async response => { }).then(async response => {
@ -76,7 +76,7 @@ const ImageWidget = (() => {
} }
return response.json(); return response.json();
}).then(data => { }).then(data => {
renderToast({ message: `Photo uploaded.`, type: "success" }); renderToast({ message: `Image uploaded.`, type: "success" });
location.reload(); location.reload();
}).catch(err => { }).catch(err => {
const msg = typeof err === "object" && err.error ? err.error : err.toString(); const msg = typeof err === "object" && err.error ? err.error : err.toString();

View file

@ -24,9 +24,6 @@
<label for="identifier" class="form-label">Identifier</label> <label for="identifier" class="form-label">Identifier</label>
<input type="text" class="form-control-plaintext" value="{{ item.identifier }}" readonly> <input type="text" class="form-control-plaintext" value="{{ item.identifier }}" readonly>
</div> </div>
<div class="col text-center">
{{ images.render_image(item.id, item.photo) }}
</div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-4"> <div class="col-4">
@ -131,6 +128,9 @@
entry_route='worklog_entry', title='Work Log') }} entry_route='worklog_entry', title='Work Log') }}
</div> </div>
{% endif %} {% endif %}
<div class="col mt-5">
{{ images.render_image(item.id, item.image) }}
</div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}