From 39a8e64c902a836ad0cff4b6f392f6f834df86fe Mon Sep 17 00:00:00 2001 From: Yaro Kasear Date: Wed, 9 Jul 2025 15:39:55 -0500 Subject: [PATCH] Add WorkNote model and integrate updates into WorkLog functionality --- inventory/models/__init__.py | 2 ++ inventory/models/work_log.py | 53 +++++++++++++++++++++++++------- inventory/models/work_note.py | 35 +++++++++++++++++++++ inventory/routes/worklog.py | 23 +++++++++++--- inventory/templates/worklog.html | 25 ++++++++++++++- inventory/utils/load.py | 3 +- 6 files changed, 123 insertions(+), 18 deletions(-) create mode 100644 inventory/models/work_note.py diff --git a/inventory/models/__init__.py b/inventory/models/__init__.py index 27a48a3..a7fefbb 100644 --- a/inventory/models/__init__.py +++ b/inventory/models/__init__.py @@ -10,6 +10,7 @@ from .room_functions import RoomFunction from .users import User from .work_log import WorkLog from .rooms import Room +from .work_note import WorkNote __all__ = [ "db", @@ -21,4 +22,5 @@ __all__ = [ "User", "WorkLog", "Room", + "WorkNote" ] diff --git a/inventory/models/work_log.py b/inventory/models/work_log.py index 35e022e..23ffc71 100644 --- a/inventory/models/work_log.py +++ b/inventory/models/work_log.py @@ -1,13 +1,15 @@ -from typing import Optional, Any, TYPE_CHECKING +from typing import Optional, Any, List, TYPE_CHECKING if TYPE_CHECKING: from .inventory import Inventory from .users import User + from .work_note import WorkNote -from sqlalchemy import Boolean, ForeignKeyConstraint, Identity, Integer, ForeignKey, Unicode, DateTime, text +from sqlalchemy import Boolean, Identity, Integer, ForeignKey, Unicode, DateTime, text from sqlalchemy.orm import Mapped, mapped_column, relationship import datetime from . import db +from .work_note import WorkNote class WorkLog(db.Model): __tablename__ = 'work_log' @@ -24,11 +26,25 @@ class WorkLog(db.Model): work_item: Mapped[Optional['Inventory']] = relationship('Inventory', back_populates='work_logs') contact: Mapped[Optional['User']] = relationship('User', back_populates='work_logs') + updates: Mapped[list['WorkNote']] = relationship( + 'WorkNote', + back_populates='work_log', + cascade='all, delete-orphan', + order_by='WorkNote.timestamp' + ) - def __init__(self, start_time: Optional[datetime.datetime] = None, end_time: Optional[datetime.datetime] = None, - notes: Optional[str] = None, complete: Optional[bool] = False, - followup: Optional[bool] = False, contact_id: Optional[int] = None, - analysis: Optional[bool] = False, work_item_id: Optional[int] = None): + def __init__( + self, + start_time: Optional[datetime.datetime] = None, + end_time: Optional[datetime.datetime] = None, + notes: Optional[str] = None, + complete: Optional[bool] = False, + followup: Optional[bool] = False, + contact_id: Optional[int] = None, + analysis: Optional[bool] = False, + work_item_id: Optional[int] = None, + updates: Optional[List[WorkNote]] = None + ) -> None: self.start_time = start_time self.end_time = end_time self.notes = notes @@ -37,7 +53,8 @@ class WorkLog(db.Model): self.contact_id = contact_id self.analysis = analysis self.work_item_id = work_item_id - + self.updates = updates or [] + def __repr__(self): return f" "WorkLog": start_time_str = data.get("start_time") end_time_str = data.get("end_time") - + + updates_raw = data.get("updates", []) + updates: list[WorkNote] = [] + + for u in updates_raw: + if isinstance(u, dict): + content = u.get("content", "").strip() + else: + content = str(u).strip() + + if content: + updates.append(WorkNote(content=content)) + return cls( start_time=datetime.datetime.fromisoformat(str(start_time_str)) if start_time_str else datetime.datetime.now(), end_time=datetime.datetime.fromisoformat(str(end_time_str)) if end_time_str else None, - notes=data.get("notes"), + notes=data.get("notes"), # Soon to be removed and sent to a farm upstate complete=bool(data.get("complete", False)), followup=bool(data.get("followup", False)), analysis=bool(data.get("analysis", False)), contact_id=data.get("contact_id"), - work_item_id=data.get("work_item_id") - ) \ No newline at end of file + work_item_id=data.get("work_item_id"), + updates=updates + ) diff --git a/inventory/models/work_note.py b/inventory/models/work_note.py new file mode 100644 index 0000000..b52c45d --- /dev/null +++ b/inventory/models/work_note.py @@ -0,0 +1,35 @@ +import datetime + +from sqlalchemy import ForeignKey, DateTime, UnicodeText, func +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from . import db + +class WorkNote(db.Model): + __tablename__ = 'work_note' + __table_args__ = ( + db.Index('ix_work_note_work_log_id', 'work_log_id'), + ) + + id: Mapped[int] = mapped_column(primary_key=True) + work_log_id: Mapped[int] = mapped_column(ForeignKey('work_log.id', ondelete='CASCADE'), nullable=False) + timestamp: Mapped[datetime.datetime] = mapped_column(DateTime, default=func.now(), server_default=func.now()) + content: Mapped[str] = mapped_column(UnicodeText, nullable=False) + + work_log = relationship('WorkLog', back_populates='updates') + + def __init__(self, content: str, timestamp: datetime.datetime | None = None) -> None: + self.content = content + self.timestamp = timestamp or datetime.datetime.now() + + def __repr__(self) -> str: + preview = self.content[:30].replace("\n", " ") + "..." if len(self.content) > 30 else self.content + return f"" + + def serialize(self) -> dict: + return { + 'id': self.id, + 'work_log_id': self.work_log_id, + 'timestamp': self.timestamp.isoformat(), + 'content': self.content + } \ No newline at end of file diff --git a/inventory/routes/worklog.py b/inventory/routes/worklog.py index 325c8b8..5047cc8 100644 --- a/inventory/routes/worklog.py +++ b/inventory/routes/worklog.py @@ -5,7 +5,7 @@ from flask import request, render_template, jsonify from . import main from .helpers import worklog_headers from .. import db -from ..models import WorkLog, User, Inventory +from ..models import WorkLog, User, Inventory, WorkNote from ..utils.load import eager_load_worklog_relationships, eager_load_user_relationships, eager_load_inventory_relationships @main.route("/worklog") @@ -27,12 +27,13 @@ def worklog_entry(id): except ValueError: return render_template('error.html', title='Bad ID', message='ID must be an integer.', endpoint='worklog_entry', endpoint_args={'id': -1}) - log = eager_load_worklog_relationships(db.session.query(WorkLog)).filter(WorkLog.id == id).first() + log = eager_load_worklog_relationships(db.session.query(WorkLog)).get(id) user_query = db.session.query(User).order_by(User.first_name) users = eager_load_user_relationships(user_query).all() item_query = db.session.query(Inventory) items = eager_load_inventory_relationships(item_query).all() items = sorted(items, key=lambda i: i.identifier) + print(log) if log: title = f'Work Log - Entry #{id}' @@ -92,20 +93,32 @@ def create_worklog(): def update_worklog(id): try: data = request.get_json(force=True) - print(data) - log = db.session.query(WorkLog).get(id) + log = eager_load_worklog_relationships(db.session.query(WorkLog)).get(id) if not log: return jsonify({"success": False, "error": f"Work Log with ID {id} not found."}), 404 log.start_time = datetime.datetime.fromisoformat(data.get("start_time")) if data.get("start_time") else log.start_time log.end_time = datetime.datetime.fromisoformat(data.get("end_time")) if data.get("end_time") else log.end_time - log.notes = data.get("notes", log.notes) log.complete = bool(data.get("complete", log.complete)) log.followup = bool(data.get("followup", log.followup)) log.analysis = bool(data.get("analysis", log.analysis)) log.contact_id = data.get("contact_id", log.contact_id) log.work_item_id = data.get("work_item_id", log.work_item_id) + existing = {str(note.id): note for note in log.updates} + incoming = data.get("updates", []) + new_updates = [] + + for note_data in incoming: + if isinstance(note_data, dict): + if "id" in note_data and str(note_data["id"]) in existing: + note = existing[str(note_data["id"])] + note.content = note_data.get("content", note.content) + new_updates.append(note) + elif "content" in note_data: + new_updates.append(WorkNote(content=note_data["content"])) + + log.updates[:] = new_updates # This replaces in-place db.session.commit() diff --git a/inventory/templates/worklog.html b/inventory/templates/worklog.html index e8b8cd9..5bd2209 100644 --- a/inventory/templates/worklog.html +++ b/inventory/templates/worklog.html @@ -88,6 +88,7 @@ + {#
@@ -95,6 +96,18 @@ rows="15">{{ log.notes if log.notes else '' }}
+ #} + + {% for update in log.updates %} +
+
+
+ {{ update.timestamp.strftime('%Y-%m-%d %H:%M:%S') }} + +
+
+
+ {% endfor %} {% endblock %} @@ -103,19 +116,29 @@ const saveButton = document.getElementById("saveButton"); const deleteButton = document.getElementById("deleteButton"); + const updateTextareas = Array.from(document.querySelectorAll("textarea[name^='update']")); + const updates = updateTextareas.map(el => { + const id = el.dataset.noteId; + const content = el.value; + return id ? { id, content } : { content }; + }); + if (saveButton) { saveButton.addEventListener("click", async (e) => { e.preventDefault(); + const updateTextareas = Array.from(document.querySelectorAll("textarea[name^='update']")); + const updates = updateTextareas.map(el => el.value).filter(val => val.trim() !== ''); + const payload = { start_time: document.querySelector("input[name='start']").value, end_time: document.querySelector("input[name='end']").value, - notes: document.querySelector("textarea[name='notes']").value, complete: document.querySelector("input[name='complete']").checked, analysis: document.querySelector("input[name='analysis']").checked, followup: document.querySelector("input[name='followup']").checked, contact_id: document.querySelector("select[name='contact']").value || null, work_item_id: document.querySelector("select[name='item']").value || null, + updates: updates }; try { diff --git a/inventory/utils/load.py b/inventory/utils/load.py index 8a5c8f4..26b2875 100644 --- a/inventory/utils/load.py +++ b/inventory/utils/load.py @@ -27,7 +27,8 @@ def eager_load_room_relationships(query): def eager_load_worklog_relationships(query): return query.options( joinedload(WorkLog.contact), - joinedload(WorkLog.work_item) + joinedload(WorkLog.work_item), + joinedload(WorkLog.updates) ) def chunk_list(lst, chunk_size):