|
43 | 43 | DASHBOARD_REVISION_RETENTION_LIMIT = 10 |
44 | 44 |
|
45 | 45 |
|
| 46 | +def _take_dashboard_snapshot( |
| 47 | + organization: Organization, |
| 48 | + dashboard: Dashboard | dict[Any, Any] | None, |
| 49 | + user: Any, |
| 50 | +) -> dict[str, Any] | None: |
| 51 | + """ |
| 52 | + Serialize the current dashboard state as a snapshot, or return None if the |
| 53 | + revisions feature is disabled or the dashboard has no DB record to snapshot. |
| 54 | +
|
| 55 | + Must be called outside any transaction.atomic block because the serializer |
| 56 | + makes hybrid-cloud RPC calls (user_service.serialize_many) that cannot run |
| 57 | + inside a transaction. |
| 58 | + """ |
| 59 | + if not isinstance(dashboard, Dashboard): |
| 60 | + return None |
| 61 | + if not features.has(REVISIONS_FEATURE, organization, actor=user): |
| 62 | + return None |
| 63 | + return serialize(dashboard, user) |
| 64 | + |
| 65 | + |
| 66 | +def _save_dashboard_revision( |
| 67 | + dashboard: Dashboard, |
| 68 | + user: Any, |
| 69 | + snapshot: dict[str, Any], |
| 70 | +) -> None: |
| 71 | + """ |
| 72 | + Create a DashboardRevision for the given snapshot and prune any revisions |
| 73 | + beyond the retention limit. Must be called inside a transaction.atomic block. |
| 74 | + """ |
| 75 | + revision = DashboardRevision.objects.create( |
| 76 | + dashboard=dashboard, |
| 77 | + created_by_id=user.id if user.is_authenticated else None, |
| 78 | + title=dashboard.title, |
| 79 | + snapshot=snapshot, |
| 80 | + snapshot_schema_version=DASHBOARD_REVISION_SNAPSHOT_SCHEMA_VERSION, |
| 81 | + ) |
| 82 | + old_revision_ids = list( |
| 83 | + DashboardRevision.objects.filter(dashboard=dashboard) |
| 84 | + .exclude(id=revision.id) |
| 85 | + .order_by("-date_added") |
| 86 | + .values_list("id", flat=True)[DASHBOARD_REVISION_RETENTION_LIMIT - 1 :] |
| 87 | + ) |
| 88 | + if old_revision_ids: |
| 89 | + DashboardRevision.objects.filter(id__in=old_revision_ids).delete() |
| 90 | + |
| 91 | + |
46 | 92 | class OrganizationDashboardBase(OrganizationEndpoint): |
47 | 93 | owner = ApiOwner.DASHBOARDS |
48 | 94 | permission_classes = (OrganizationDashboardsPermission,) |
@@ -212,32 +258,12 @@ def put( |
212 | 258 | {"detail": "Cannot change the title of prebuilt Dashboards."}, status=409 |
213 | 259 | ) |
214 | 260 |
|
215 | | - # Serialize outside the transaction to avoid making RPC calls inside a transaction. |
216 | | - # The snapshot captures the dashboard state before the update is applied. |
217 | | - create_revision = features.has( |
218 | | - REVISIONS_FEATURE, organization, actor=request.user |
219 | | - ) and isinstance(dashboard, Dashboard) |
220 | | - snapshot = serialize(dashboard, request.user) if create_revision else None |
| 261 | + snapshot = _take_dashboard_snapshot(organization, dashboard, request.user) |
221 | 262 |
|
222 | 263 | try: |
223 | 264 | with transaction.atomic(router.db_for_write(DashboardTombstone)): |
224 | | - if create_revision and snapshot is not None: |
225 | | - revision = DashboardRevision.objects.create( |
226 | | - dashboard=dashboard, |
227 | | - created_by_id=(request.user.id if request.user.is_authenticated else None), |
228 | | - title=dashboard.title, |
229 | | - snapshot=snapshot, |
230 | | - snapshot_schema_version=DASHBOARD_REVISION_SNAPSHOT_SCHEMA_VERSION, |
231 | | - ) |
232 | | - # Keep only the most recent revisions; delete the rest |
233 | | - old_revision_ids = list( |
234 | | - DashboardRevision.objects.filter(dashboard=dashboard) |
235 | | - .exclude(id=revision.id) |
236 | | - .order_by("-date_added") |
237 | | - .values_list("id", flat=True)[DASHBOARD_REVISION_RETENTION_LIMIT - 1 :] |
238 | | - ) |
239 | | - if old_revision_ids: |
240 | | - DashboardRevision.objects.filter(id__in=old_revision_ids).delete() |
| 265 | + if snapshot is not None: |
| 266 | + _save_dashboard_revision(dashboard, request.user, snapshot) |
241 | 267 | serializer.save() |
242 | 268 | if tombstone: |
243 | 269 | DashboardTombstone.objects.get_or_create( |
|
0 commit comments