Add WorkNote model and integrate updates into WorkLog functionality
This commit is contained in:
parent
58f8a040b7
commit
39a8e64c90
6 changed files with 123 additions and 18 deletions
|
@ -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"
|
||||
]
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
35
inventory/models/work_note.py
Normal file
35
inventory/models/work_note.py
Normal 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
|
||||
}
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue