Add WorkNote model and integrate updates into WorkLog functionality

This commit is contained in:
Yaro Kasear 2025-07-09 15:39:55 -05:00
parent 58f8a040b7
commit 39a8e64c90
6 changed files with 123 additions and 18 deletions

View file

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

View file

@ -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(id={self.id}, start_time={self.start_time}, end_time={self.end_time}, " \
f"notes={repr(self.notes)}, complete={self.complete}, followup={self.followup}, " \
@ -49,6 +66,7 @@ class WorkLog(db.Model):
'start_time': self.start_time.isoformat() if self.start_time else None,
'end_time': self.end_time.isoformat() if self.end_time else None,
'notes': self.notes,
'updates': [note.serialize() for note in self.updates or []],
'complete': self.complete,
'followup': self.followup,
'contact_id': self.contact_id,
@ -60,14 +78,27 @@ class WorkLog(db.Model):
def from_dict(cls, data: dict[str, Any]) -> "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")
)
work_item_id=data.get("work_item_id"),
updates=updates
)

View file

@ -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"<WorkNote(id={self.id}), log_id={self.work_log_id}, ts={self.timestamp}, content={preview!r}>"
def serialize(self) -> dict:
return {
'id': self.id,
'work_log_id': self.work_log_id,
'timestamp': self.timestamp.isoformat(),
'content': self.content
}

View file

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

View file

@ -88,6 +88,7 @@
</div>
</div>
</div>
{#
<div class="row">
<div class="col-12">
<label for="notes" class="form-label">Notes</label>
@ -95,6 +96,18 @@
rows="15">{{ log.notes if log.notes else '' }}</textarea>
</div>
</div>
#}
<label class="form-label">Updates</label>
{% for update in log.updates %}
<div class="row">
<div class="col">
<div class="input-group mb-3">
<span class="input-group-text">{{ update.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</span>
<textarea name="update{{ loop.index0 }}" id="" class="form-control" rows="5" data-note-id="{{ update.id if update.id }}">{{ update.content }}</textarea>
</div>
</div>
</div>
{% endfor %}
</div>
</nav>
{% 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 {

View file

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