From 7030f413c3c023db1180463a0d37d1cdcd592359 Mon Sep 17 00:00:00 2001 From: "Sean R. Abraham" Date: Sun, 9 Feb 2025 16:01:29 -0500 Subject: [PATCH] add "lastModified" to Incident entity, add column in DataTable Before this change, we were figuring out last modification time on the client side. That was being calculated incorrectly, since we weren't passing system report entries into the Incidents view (exclude_system_entries = True). This moves over that computation to the server. Also, this adds a "Modified" column to the Incidents view. I'm theorizing that people might find this useful; possibly more useful than "Created". It could especially be useful as a sort order, so that active Incidents are at the top and older ones fall to the bottom. NOTE that if we want to remove this DataTables column in the future, we should still keep the server-side piece of this commit, and we should still use the new "last_modified" field in the API to know when an Incident was last modified. (finally, there's one little tweak in here for document titles, making them more concise) --- .../incidents_template/template.xhtml | 2 ++ src/ims/element/static/ims.js | 12 ++----- src/ims/element/static/incidents.js | 35 +++++++++---------- src/ims/model/_incident.py | 5 ++- src/ims/model/json/_incident.py | 2 ++ src/ims/model/json/test/json_helpers.py | 1 + src/ims/model/strategies.py | 14 ++++---- src/ims/store/_db.py | 13 ++++++- src/ims/store/test/incident.py | 7 ++++ 9 files changed, 56 insertions(+), 35 deletions(-) diff --git a/src/ims/element/incident/incidents_template/template.xhtml b/src/ims/element/incident/incidents_template/template.xhtml index 46f23000b..4d4292ecd 100644 --- a/src/ims/element/incident/incidents_template/template.xhtml +++ b/src/ims/element/incident/incidents_template/template.xhtml @@ -166,6 +166,7 @@ # Created + Modified State Rangers Location @@ -178,6 +179,7 @@ # Created + Modified State Rangers Location diff --git a/src/ims/element/static/ims.js b/src/ims/element/static/ims.js index 2c3b7e66e..d16cf1c76 100644 --- a/src/ims/element/static/ims.js +++ b/src/ims/element/static/ims.js @@ -505,10 +505,7 @@ function incidentAsString(incident) { if (incident.number == null) { return "New Incident"; } - return ( - "IMS #" + incident.number + ": " + - summarizeIncident(incident) - ); + return `#${incident.number}: ${summarizeIncident(incident)} (${incident.event})`; } @@ -517,11 +514,8 @@ function fieldReportAsString(report) { if (report.number == null) { return "New Field Report"; } - return ( - "FR #" + report.number + - " (" + fieldReportAuthor(report) + "): " + - summarizeFieldReport(report) - ); + return `FR #${report.number} (${fieldReportAuthor(report)}): ` + + `${summarizeFieldReport(report)} (${report.event})`; } diff --git a/src/ims/element/static/incidents.js b/src/ims/element/static/incidents.js index 3ffb657f1..b2de3256f 100644 --- a/src/ims/element/static/incidents.js +++ b/src/ims/element/static/incidents.js @@ -242,13 +242,20 @@ function initDataTables() { "render": renderDate, }, { // 2 + "name": "incident_last_modified", + "className": "incident_last_modified text-center", + "data": "last_modified", + "defaultContent": null, + "render": renderDate, + }, + { // 3 "name": "incident_state", "className": "incident_state text-center", "data": "state", "defaultContent": null, "render": renderState, }, - { // 3 + { // 4 "name": "incident_ranger_handles", "className": "incident_ranger_handles", "data": "ranger_handles", @@ -256,14 +263,14 @@ function initDataTables() { "render": renderSafeSorted, "width": "6em", }, - { // 4 + { // 5 "name": "incident_location", "className": "incident_location", "data": "location", "defaultContent": "", "render": renderLocation, }, - { // 5 + { // 6 "name": "incident_types", "className": "incident_types", "data": "incident_types", @@ -271,7 +278,7 @@ function initDataTables() { "render": renderSafeSorted, "width": "5em", }, - { // 6 + { // 7 "name": "incident_summary", "className": "incident_summary", "data": "summary", @@ -295,6 +302,11 @@ function initDataTables() { "title", fullDateTime.format(Date.parse(incident.created)), ); + row.getElementsByClassName("incident_last_modified")[0] + .setAttribute( + "title", + fullDateTime.format(Date.parse(incident.last_modified)), + ); }, }); } @@ -435,19 +447,6 @@ function initSearchField() { // function initSearch() { - function modifiedAfter(incident, timestamp) { - if (timestamp < Date.parse(incident.created)) { - return true; - } - - for (const entry of incident.report_entries??[]) { - if (timestamp < Date.parse(entry.created)) { - return true; - } - } - - return false; - } $.fn.dataTable.ext.search.push( function(settings, rowData, rowIndex) { @@ -472,7 +471,7 @@ function initSearch() { if ( _showModifiedAfter != null && - ! modifiedAfter(incident, _showModifiedAfter) + Date.parse(incident.last_modified) < _showModifiedAfter ) { return false } diff --git a/src/ims/model/_incident.py b/src/ims/model/_incident.py index f8e00f853..ac36e6d42 100644 --- a/src/ims/model/_incident.py +++ b/src/ims/model/_incident.py @@ -23,7 +23,7 @@ from collections.abc import Iterable, Sequence from datetime import datetime as DateTime -from attrs import field, frozen +from attrs import converters, field, frozen from ims.ext.attr import sorted_tuple @@ -53,6 +53,9 @@ class Incident(ReplaceMixIn): eventID: str number: int created: DateTime = field(converter=normalizeDateTime) + lastModified: DateTime | None = field( + converter=converters.optional(normalizeDateTime) + ) state: IncidentState priority: IncidentPriority summary: str | None diff --git a/src/ims/model/json/_incident.py b/src/ims/model/json/_incident.py index a175a62a4..f657404df 100644 --- a/src/ims/model/json/_incident.py +++ b/src/ims/model/json/_incident.py @@ -47,6 +47,7 @@ class IncidentJSONKey(Enum): eventID = "event" number = "number" created = "created" + lastModified = "last_modified" state = "state" priority = "priority" summary = "summary" @@ -65,6 +66,7 @@ class IncidentJSONType(Enum): eventID = str number = int created = DateTime + lastModified = DateTime | None state = IncidentState priority = IncidentPriority summary = str | None diff --git a/src/ims/model/json/test/json_helpers.py b/src/ims/model/json/test/json_helpers.py index c470d6441..0a0b438b5 100644 --- a/src/ims/model/json/test/json_helpers.py +++ b/src/ims/model/json/test/json_helpers.py @@ -128,6 +128,7 @@ def jsonFromIncident(incident: Incident) -> dict[str, Any]: "event": jsonSerialize(incident.eventID), "number": jsonSerialize(incident.number), "created": jsonSerialize(incident.created), + "last_modified": jsonSerialize(incident.lastModified), "state": jsonSerialize(incident.state), "priority": jsonSerialize(incident.priority), "summary": jsonSerialize(incident.summary), diff --git a/src/ims/model/strategies.py b/src/ims/model/strategies.py index 7e2fcff23..3dc6ae24d 100644 --- a/src/ims/model/strategies.py +++ b/src/ims/model/strategies.py @@ -392,21 +392,23 @@ def incidents( types = [t.name for t in draw(lists(incidentTypes()))] + created = draw(dateTimes(beforeNow=beforeNow, fromNow=fromNow)) + entries = draw( + lists(reportEntries(automatic=automatic, beforeNow=beforeNow, fromNow=fromNow)) + ) + lastModified = max(re.created for re in entries) if entries else created return Incident( eventID=event.id, number=number, - created=draw(dateTimes(beforeNow=beforeNow, fromNow=fromNow)), + created=created, + lastModified=lastModified, state=draw(incidentStates()), priority=draw(incidentPriorities()), summary=draw(incidentSummaries()), location=draw(locations()), rangerHandles=draw(lists(rangerHandles())), incidentTypes=types, - reportEntries=draw( - lists( - reportEntries(automatic=automatic, beforeNow=beforeNow, fromNow=fromNow) - ) - ), + reportEntries=entries, fieldReportNumbers=frozenset(), ) diff --git a/src/ims/store/_db.py b/src/ims/store/_db.py index 542499beb..5125449ab 100644 --- a/src/ims/store/_db.py +++ b/src/ims/store/_db.py @@ -659,11 +659,17 @@ def _fetchIncidents( int(val) for val in loads(str(row["FIELD_REPORT_NUMBERS"])) ] incidentNumber = cast(int, row["NUMBER"]) + + lastModified = self.fromDateTimeValue(row["CREATED"]) + if reportEntries[incidentNumber]: + lastModified = max(re.created for re in reportEntries[incidentNumber]) + results.append( Incident( eventID=eventID, number=incidentNumber, created=self.fromDateTimeValue(row["CREATED"]), + lastModified=lastModified, state=self.fromIncidentStateValue(row["STATE"]), priority=self.fromPriorityValue(row["PRIORITY"]), summary=cast(str | None, row["SUMMARY"]), @@ -715,7 +721,7 @@ def notFound() -> NoReturn: txn.execute(self.query.incident_reportEntries.text, parameters) - reportEntries = ( + reportEntries = tuple( ReportEntry( id=cast(int, row["ID"]), created=self.fromDateTimeValue(row["CREATED"]), @@ -728,6 +734,10 @@ def notFound() -> NoReturn: if row["TEXT"] ) + lastModified = self.fromDateTimeValue(row["CREATED"]) + if reportEntries: + lastModified = max(re.created for re in reportEntries) + # FIXME: This is because schema thinks concentric is an int if row["LOCATION_CONCENTRIC"] is None: concentric = None @@ -742,6 +752,7 @@ def notFound() -> NoReturn: eventID=eventID, number=incidentNumber, created=self.fromDateTimeValue(row["CREATED"]), + lastModified=lastModified, state=self.fromIncidentStateValue(row["STATE"]), priority=self.fromPriorityValue(row["PRIORITY"]), summary=cast(str | None, row["SUMMARY"]), diff --git a/src/ims/store/test/incident.py b/src/ims/store/test/incident.py index 03fd594b9..ee76e6bf2 100644 --- a/src/ims/store/test/incident.py +++ b/src/ims/store/test/incident.py @@ -56,6 +56,7 @@ eventID=anEvent.id, number=0, created=DateTime.now(UTC) + TimeDelta(seconds=1), + lastModified=DateTime.now(UTC) + TimeDelta(seconds=2), state=IncidentState.new, priority=IncidentPriority.normal, summary="A thing happened", @@ -70,6 +71,7 @@ eventID=anEvent.id, number=1, created=DateTime.now(UTC) + TimeDelta(seconds=2), + lastModified=DateTime.now(UTC) + TimeDelta(seconds=3), state=IncidentState.new, priority=IncidentPriority.normal, summary="A thing happened", @@ -84,6 +86,7 @@ eventID=anEvent2.id, number=325, created=DateTime.now(UTC) + TimeDelta(seconds=3), + lastModified=DateTime.now(UTC) + TimeDelta(seconds=4), state=IncidentState.new, priority=IncidentPriority.normal, summary="Another thing happened 🙂", @@ -884,6 +887,10 @@ def assertIncidentsEqual( if store.dateTimesEqual(valueA, valueB): continue messages.append(f"{name} delta: {valueA - valueB}") + elif name == "lastModified": + # this field is calculated on read, and shouldn't be equal + # to what was written + continue elif name == "reportEntries": if store.reportEntriesEqual(valueA, valueB, ignoreAutomatic): continue