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()
from .routes import main
from .routes.photos import photo_bp
from .routes.images import image_bp
app.register_blueprint(main)
app.register_blueprint(photo_bp)
app.register_blueprint(image_bp)
return app

View file

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

View file

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

View file

@ -1,7 +1,7 @@
from .. import db
worklog_photos = db.Table(
'worklog_photos',
worklog_images = db.Table(
'worklog_images',
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 inventory.models.photo import Photo
from inventory.models.image import Image
if TYPE_CHECKING:
from .brands import Brand
from .items import Item
from .work_log import WorkLog
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.orm import Mapped, mapped_column, relationship
import datetime
from . import db
from .photo import PhotoAttachable
from .image import ImageAttachable
class Inventory(db.Model, PhotoAttachable):
class Inventory(db.Model, ImageAttachable):
__tablename__ = 'inventory'
__table_args__ = (
Index('Inventory$Barcode', 'barcode'),
@ -34,14 +34,14 @@ class Inventory(db.Model, PhotoAttachable):
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))'))
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')
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')
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,
name: Optional[str] = None, serial: Optional[str] = None,
@ -128,5 +128,5 @@ class Inventory(db.Model, PhotoAttachable):
shared=bool(data.get("shared", False))
)
def attach_photo(self, photo: Photo) -> None:
self.photo = photo
def attach_image(self, image: Image) -> None:
self.image = image

View file

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

View file

@ -1,7 +1,7 @@
from typing import Optional, Any, List, TYPE_CHECKING
if TYPE_CHECKING:
from .inventory import Inventory
from .photo import Photo
from .image import Image
from .users import User
from .work_note import WorkNote
@ -10,11 +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 .image import ImageAttachable
from .image_links import worklog_images
from .work_note import WorkNote
class WorkLog(db.Model, PhotoAttachable):
class WorkLog(db.Model, ImageAttachable):
__tablename__ = 'work_log'
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',
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__(
self,

View file

@ -5,7 +5,7 @@ from werkzeug.utils import secure_filename
from ..models import Inventory
from ..models.photo import PhotoAttachable
from ..models.image import ImageAttachable
inventory_headers = {
"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)
return f"{model_name}/{sha}_{original_name}"
def get_photo_attachable_class_by_name(name: str):
for cls in PhotoAttachable.__subclasses__():
def get_image_attachable_class_by_name(name: str):
for cls in ImageAttachable.__subclasses__():
if getattr(cls, '__tablename__', None) == name:
return cls
return None

View file

@ -3,18 +3,18 @@ import posixpath
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 ..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
hashed_name = generate_hashed_filename(file, model)
rel_path = posixpath.join("uploads", "photos", hashed_name)
abs_path = os.path.join(current_app.static_folder, "uploads", "photos", rel_path)
rel_path = posixpath.join("uploads", "images", hashed_name)
abs_path = os.path.join(current_app.static_folder, "uploads", "images", rel_path)
dir_path = os.path.dirname(abs_path)
if not os.path.exists(dir_path):
@ -29,8 +29,8 @@ def save_photo(file, model: str) -> str:
file.save(abs_path)
return rel_path
@photo_bp.route("/api/photos", methods=["POST"])
def upload_photo():
@image_bp.route("/api/images", methods=["POST"])
def upload_image():
file = request.files.get("file")
model = request.form.get("model")
model_id = request.form.get("model_id")
@ -39,9 +39,9 @@ def upload_photo():
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)
ModelClass = get_image_attachable_class_by_name(model)
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:
model_id = int(model_id)
@ -49,34 +49,40 @@ def upload_photo():
return jsonify({"success": False, "error": "model_id must be an integer"}), 400
# Save file
rel_path = save_photo(file, model)
rel_path = save_image(file, model)
print(rel_path)
# Create Photo row
photo = Photo(filename=rel_path, caption=caption)
db.session.add(photo)
# Create Image row
image = Image(filename=rel_path, caption=caption)
db.session.add(image)
# Attach photo to model
# Attach image 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)
target.attach_image(image)
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"])
def get_photo(photo_id: int):
photo = db.session.get(Photo, photo_id)
if not photo:
return jsonify({"success": False, "error": f"No photo found with ID {photo_id}"}), 404
@image_bp.route("/api/images/<int:image_id>", methods=["GET"])
def get_image(image_id: int):
image = db.session.get(Image, image_id)
if not image:
return jsonify({"success": False, "error": f"No image found with ID {image_id}"}), 404
return jsonify({
"success": True,
"id": photo.id,
"filename": photo.filename,
"caption": photo.caption,
"timestamp": photo.timestamp.isoformat() if photo.timestamp else None,
"url": f"/static/{photo.filename}"
"id": image.id,
"filename": image.filename,
"caption": image.caption,
"timestamp": image.timestamp.isoformat() if image.timestamp else None,
"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);
const formData = new FormData(form);
fetch("/api/photos", {
fetch("/api/images", {
method: "POST",
body: formData
}).then(async response => {
@ -76,7 +76,7 @@ const ImageWidget = (() => {
}
return response.json();
}).then(data => {
renderToast({ message: `Photo uploaded.`, type: "success" });
renderToast({ message: `Image uploaded.`, type: "success" });
location.reload();
}).catch(err => {
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>
<input type="text" class="form-control-plaintext" value="{{ item.identifier }}" readonly>
</div>
<div class="col text-center">
{{ images.render_image(item.id, item.photo) }}
</div>
</div>
<div class="row">
<div class="col-4">
@ -131,6 +128,9 @@
entry_route='worklog_entry', title='Work Log') }}
</div>
{% endif %}
<div class="col mt-5">
{{ images.render_image(item.id, item.image) }}
</div>
</div>
</div>
{% endblock %}