Skip to content

Commit 629762e

Browse files
siddhant3030claude
andauthored
last modified added (#1321)
* last modified added * PR comments changes * Add merge migration for 0158 conflict Resolves conflicting leaf nodes between 0158_allow_null_period_end_on_snapshot (from main) and 0158_merge_20260415_1814 (last_modified_by feature). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * added one more test case --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9594f2a commit 629762e

7 files changed

Lines changed: 145 additions & 14 deletions

File tree

ddpui/api/report_api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ def update_snapshot(request, snapshot_id: int, payload: SnapshotUpdate):
155155
"""Update a snapshot"""
156156
orguser: OrgUser = request.orguser
157157
try:
158-
snapshot = ReportService.update_snapshot(snapshot_id, orguser.org, payload)
158+
snapshot = ReportService.update_snapshot(snapshot_id, payload, orguser)
159159
return api_response(
160160
success=True,
161161
data=SnapshotUpdateResponse.from_model(snapshot),

ddpui/core/reports/report_service.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -448,9 +448,9 @@ def get_snapshot(snapshot_id: int, org: Org) -> ReportSnapshot:
448448
SnapshotNotFoundError: If snapshot doesn't exist or doesn't belong to org
449449
"""
450450
try:
451-
return ReportSnapshot.objects.select_related("created_by__user").get(
452-
id=snapshot_id, org=org
453-
)
451+
return ReportSnapshot.objects.select_related(
452+
"created_by__user", "last_modified_by__user"
453+
).get(id=snapshot_id, org=org)
454454
except ReportSnapshot.DoesNotExist:
455455
raise SnapshotNotFoundError(snapshot_id)
456456

@@ -515,6 +515,9 @@ def get_snapshot_view_data(snapshot_id: int, org: Org) -> Dict[str, Any]:
515515
"created_at": snapshot.created_at,
516516
"updated_at": snapshot.updated_at,
517517
"created_by": snapshot.created_by.user.email if snapshot.created_by else None,
518+
"last_modified_by": (
519+
snapshot.last_modified_by.user.email if snapshot.last_modified_by else None
520+
),
518521
"dashboard_title": snapshot.frozen_dashboard.get("title", ""),
519522
"dashboard_id": snapshot.frozen_dashboard.get("dashboard_id"),
520523
}
@@ -526,24 +529,25 @@ def get_snapshot_view_data(snapshot_id: int, org: Org) -> Dict[str, Any]:
526529
}
527530

528531
@staticmethod
529-
def update_snapshot(snapshot_id: int, org: Org, data: SnapshotUpdate) -> ReportSnapshot:
532+
def update_snapshot(snapshot_id: int, data: SnapshotUpdate, orguser: OrgUser) -> ReportSnapshot:
530533
"""Update mutable fields on a snapshot.
531534
532535
Args:
533536
snapshot_id: The snapshot ID to update
534-
org: The organization to filter by
535537
data: Validated update payload
538+
orguser: The user making the update
536539
537540
Returns:
538541
ReportSnapshot: The updated snapshot instance
539542
540543
Raises:
541544
SnapshotNotFoundError: If snapshot doesn't exist or doesn't belong to org
542545
"""
543-
snapshot = ReportService.get_snapshot(snapshot_id, org)
546+
snapshot = ReportService.get_snapshot(snapshot_id, orguser.org)
544547
if data.summary is not None:
545548
snapshot.summary = data.summary
546-
snapshot.save(update_fields=["summary"])
549+
snapshot.last_modified_by = orguser
550+
snapshot.save(update_fields=["summary", "last_modified_by", "updated_at"])
547551
return snapshot
548552

549553
@staticmethod
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Generated by Django 4.2 on 2026-04-15 13:05
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
6+
7+
class Migration(migrations.Migration):
8+
dependencies = [
9+
("ddpui", "0156_add_comment_snapshot_index"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="reportsnapshot",
15+
name="last_modified_by",
16+
field=models.ForeignKey(
17+
blank=True,
18+
help_text="User who last modified the summary",
19+
null=True,
20+
on_delete=django.db.models.deletion.SET_NULL,
21+
related_name="modified_snapshots",
22+
to="ddpui.orguser",
23+
),
24+
),
25+
]
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Generated by Django 4.2 on 2026-04-15 18:14
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("ddpui", "0157_add_last_modified_by_to_report_snapshot"),
9+
("ddpui", "0157_orgdbt_is_repo_managed_by_system"),
10+
]
11+
12+
operations = []
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Generated by Django 4.2 on 2026-05-04 09:10
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("ddpui", "0158_allow_null_period_end_on_snapshot"),
9+
("ddpui", "0158_merge_20260415_1814"),
10+
]
11+
12+
operations = []

ddpui/models/report.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,14 @@ class ReportSnapshot(models.Model):
7777
null=True,
7878
help_text="User who created this snapshot",
7979
)
80+
last_modified_by = models.ForeignKey(
81+
OrgUser,
82+
on_delete=models.SET_NULL,
83+
null=True,
84+
blank=True,
85+
related_name="modified_snapshots",
86+
help_text="User who last modified the summary",
87+
)
8088
org = models.ForeignKey(Org, on_delete=models.CASCADE)
8189
created_at = models.DateTimeField(auto_now_add=True)
8290
updated_at = models.DateTimeField(auto_now=True)

ddpui/tests/core/reports/test_report_service.py

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -510,27 +510,71 @@ def test_component_with_missing_chart_id(self, orguser, org):
510510
class TestUpdateSnapshot:
511511
"""Tests for ReportService.update_snapshot"""
512512

513-
def test_update_summary(self, sample_snapshot, org):
513+
def test_update_summary(self, sample_snapshot, org, orguser):
514514
"""Updating 'summary' succeeds"""
515515
data = SnapshotUpdate(summary="New summary")
516-
updated = ReportService.update_snapshot(sample_snapshot.id, org, data)
516+
updated = ReportService.update_snapshot(sample_snapshot.id, data, orguser)
517517
assert updated.summary == "New summary"
518518

519-
def test_update_not_found_raises(self, org):
519+
def test_update_not_found_raises(self, org, orguser):
520520
"""Updating nonexistent snapshot raises SnapshotNotFoundError"""
521521
data = SnapshotUpdate(summary="test")
522522
with pytest.raises(SnapshotNotFoundError):
523-
ReportService.update_snapshot(99999, org, data)
523+
ReportService.update_snapshot(99999, data, orguser)
524524

525-
def test_update_with_none_value_skips(self, sample_snapshot, org):
525+
def test_update_with_none_value_skips(self, sample_snapshot, org, orguser):
526526
"""Updating with None value does not change the field"""
527527
sample_snapshot.summary = "Original"
528528
sample_snapshot.save(update_fields=["summary"])
529529

530530
data = SnapshotUpdate(summary=None)
531-
updated = ReportService.update_snapshot(sample_snapshot.id, org, data)
531+
updated = ReportService.update_snapshot(sample_snapshot.id, data, orguser)
532532
assert updated.summary == "Original"
533533

534+
def test_update_sets_last_modified_by(self, sample_snapshot, org, orguser):
535+
"""Updating summary sets last_modified_by to the editing user"""
536+
assert sample_snapshot.last_modified_by is None
537+
538+
data = SnapshotUpdate(summary="Edited summary")
539+
updated = ReportService.update_snapshot(sample_snapshot.id, data, orguser)
540+
541+
assert updated.last_modified_by == orguser
542+
assert updated.last_modified_by.user.email == orguser.user.email
543+
544+
def test_update_tracks_different_modifier(self, sample_snapshot, org, other_orguser):
545+
"""last_modified_by reflects the user who made the latest edit"""
546+
data = SnapshotUpdate(summary="Edited by other user")
547+
updated = ReportService.update_snapshot(sample_snapshot.id, data, other_orguser)
548+
549+
assert updated.last_modified_by == other_orguser
550+
assert updated.last_modified_by.user.email == other_orguser.user.email
551+
552+
def test_update_none_summary_does_not_set_last_modified_by(self, sample_snapshot, org, orguser):
553+
"""When summary is None (no change), last_modified_by is not updated"""
554+
assert sample_snapshot.last_modified_by is None
555+
556+
data = SnapshotUpdate(summary=None)
557+
updated = ReportService.update_snapshot(sample_snapshot.id, data, orguser)
558+
559+
assert updated.last_modified_by is None
560+
561+
def test_creator_writes_then_other_user_edits(
562+
self, sample_snapshot, org, orguser, other_orguser
563+
):
564+
"""Creator writes the initial summary, then another user edits it"""
565+
# Creator writes the first summary
566+
data = SnapshotUpdate(summary="Initial summary by creator")
567+
updated = ReportService.update_snapshot(sample_snapshot.id, data, orguser)
568+
assert updated.summary == "Initial summary by creator"
569+
assert updated.last_modified_by == orguser
570+
571+
# A different user edits the summary
572+
data = SnapshotUpdate(summary="Revised by another user")
573+
updated = ReportService.update_snapshot(sample_snapshot.id, data, other_orguser)
574+
assert updated.summary == "Revised by another user"
575+
assert updated.last_modified_by == other_orguser
576+
assert updated.created_by == orguser # creator unchanged
577+
534578

535579
# ================================================================================
536580
# Test delete_snapshot
@@ -653,6 +697,32 @@ def test_warehouse_discovered_column_injects_chart_filters(
653697
col_names = [f["column"] for f in filters]
654698
assert "updated_at" in col_names
655699

700+
def test_view_data_includes_last_modified_by_null(
701+
self, mock_org_warehouse_model, mock_factory, sample_snapshot, org
702+
):
703+
"""report_metadata includes last_modified_by as None for unedited snapshots"""
704+
mock_org_warehouse_model.objects.filter.return_value.first.return_value = MagicMock()
705+
mock_factory.get_warehouse_client.return_value = MagicMock()
706+
707+
view_data = ReportService.get_snapshot_view_data(sample_snapshot.id, org)
708+
rm = view_data["report_metadata"]
709+
assert "last_modified_by" in rm
710+
assert rm["last_modified_by"] is None
711+
712+
def test_view_data_includes_last_modified_by_email(
713+
self, mock_org_warehouse_model, mock_factory, sample_snapshot, org, orguser
714+
):
715+
"""report_metadata includes last_modified_by email after an update"""
716+
mock_org_warehouse_model.objects.filter.return_value.first.return_value = MagicMock()
717+
mock_factory.get_warehouse_client.return_value = MagicMock()
718+
719+
data = SnapshotUpdate(summary="Updated summary")
720+
ReportService.update_snapshot(sample_snapshot.id, data, orguser)
721+
722+
view_data = ReportService.get_snapshot_view_data(sample_snapshot.id, org)
723+
rm = view_data["report_metadata"]
724+
assert rm["last_modified_by"] == orguser.user.email
725+
656726
def test_not_found(self, mock_org_warehouse_model, mock_factory, org):
657727
"""Viewing nonexistent snapshot raises SnapshotNotFoundError"""
658728
with pytest.raises(SnapshotNotFoundError):

0 commit comments

Comments
 (0)