Skip to content

Commit b285267

Browse files
authored
fix(submission): show New Document badge for entity and clear on review (#1104)
- Remove isStaff gate so the New Document badge renders on both the staff and proponent views, matching the New Version badge - Clear is_updated when a document is Verified/Acknowledged before acknowledgement so both badges hide after review - Fix broken Withdraw/Accept Update button layout by removing the fixed 25% header width in SentRequestCollapsible - Update StatusCell and is_updated-flag tests to cover new behavior
1 parent 3e7ab30 commit b285267

9 files changed

Lines changed: 421 additions & 8 deletions

File tree

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Manual package deletion
2+
3+
`delete_package.sql` defines a Postgres function `delete_package(p_package_id)`
4+
that removes a package and **all** related rows in one atomic transaction.
5+
6+
## Install
7+
8+
```bash
9+
psql "$DATABASE_URL" -f scripts/delete_package.sql
10+
```
11+
12+
## Run
13+
14+
```sql
15+
SELECT * FROM delete_package(123);
16+
```
17+
18+
It returns a single row with the number of rows removed per table and a
19+
`s3_keys_to_delete` array.
20+
21+
## What it deletes
22+
23+
Removed automatically by `ON DELETE CASCADE` when the `packages` row goes:
24+
25+
| Table | FK |
26+
|---|---|
27+
| `package_metadata` | `package_id` |
28+
| `update_requests` | `submission_package_id` |
29+
| `internal_staff_documents` | `package_id` |
30+
| `items` | `package_id` |
31+
| `submission_reviews` | `items.id` |
32+
| `submission_review_entries` | `submission_reviews.id` |
33+
| `submission_item_notes` | `items.id` |
34+
| `submissions` | `items.id` |
35+
36+
Removed explicitly by the function because they are **not** cascaded:
37+
38+
| Table | How it is linked |
39+
|---|---|
40+
| `submitted_forms` | `submissions.submitted_form_id` points to it |
41+
| `submitted_documents` | `submissions.submitted_document_id` points to it |
42+
| `geo_data_uploads` | no FK; `raw_s3_key` matches a document url |
43+
| `activity_logs` | value-keyed: `PACKAGE` -> `package_versions.original_package_id`, `SUBMISSION` -> submission id |
44+
| `email_queue` | `entity_type='PACKAGE'`, `entity_id` = package id |
45+
| `package_versions` | `packages.version_id` points to it (only deleted if no other package shares it) |
46+
47+
## Important caveats
48+
49+
- **S3 is not touched.** SQL cannot delete object storage. The function returns
50+
`s3_keys_to_delete` (internal staff docs, submitted document urls, and the
51+
raw/preview/standard tiers of matching geo uploads). Delete those keys from S3
52+
separately after the transaction commits.
53+
- **Atomic.** The whole function runs in one transaction; any error rolls back
54+
every delete.
55+
- **Preview first.** To see what a delete would touch without committing:
56+
57+
```sql
58+
BEGIN;
59+
SELECT * FROM delete_package(123);
60+
ROLLBACK;
61+
```
62+
63+
- **Version lineage.** `package_versions` is only removed when no other package
64+
still references that `version_id`, so sibling versions are preserved.
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
-- =============================================================================
2+
-- delete_package(p_package_id)
3+
--
4+
-- Manually and completely removes a single package and every related row.
5+
--
6+
-- WHY THIS IS NOT JUST "DELETE FROM packages WHERE id = ...":
7+
-- Several child tables ARE wired with ON DELETE CASCADE and would be removed
8+
-- automatically (package_metadata, items -> submission_reviews ->
9+
-- submission_review_entries / submission_item_notes / submissions,
10+
-- update_requests, internal_staff_documents).
11+
--
12+
-- But these related rows are NOT cascaded and a plain delete would orphan them:
13+
-- * submitted_forms - submissions.submitted_form_id points TO it (no cascade)
14+
-- * submitted_documents - submissions.submitted_document_id points TO it (no cascade)
15+
-- * geo_data_uploads - no FK at all; linked by raw_s3_key == document url
16+
-- * activity_logs - keyed by value, not FK:
17+
-- PACKAGE -> package_versions.original_package_id
18+
-- SUBMISSION -> submission id
19+
-- * email_queue - entity_type='PACKAGE', entity_id = package id
20+
-- * package_versions - packages.version_id points TO it (no cascade)
21+
--
22+
-- This function deletes everything in FK-safe order inside one transaction
23+
-- (the whole function body runs atomically; any error rolls it all back).
24+
--
25+
-- NOTE: geo_data_uploads also have S3 artifacts (raw/preview/standard keys).
26+
-- This function removes the DB rows only. It RETURNS the affected S3 keys
27+
-- so the caller can clean up object storage. SQL cannot delete from S3.
28+
--
29+
-- USAGE:
30+
-- SELECT * FROM delete_package(123);
31+
--
32+
-- Returns one row: counts per table + the S3 keys that still need deletion.
33+
-- =============================================================================
34+
35+
CREATE OR REPLACE FUNCTION delete_package(p_package_id INTEGER)
36+
RETURNS TABLE (
37+
deleted_package_id INTEGER,
38+
items_deleted INTEGER,
39+
submissions_deleted INTEGER,
40+
submitted_forms_deleted INTEGER,
41+
submitted_documents_deleted INTEGER,
42+
geo_uploads_deleted INTEGER,
43+
reviews_deleted INTEGER,
44+
review_entries_deleted INTEGER,
45+
item_notes_deleted INTEGER,
46+
update_requests_deleted INTEGER,
47+
internal_docs_deleted INTEGER,
48+
package_metadata_deleted INTEGER,
49+
activity_logs_deleted INTEGER,
50+
email_queue_deleted INTEGER,
51+
package_versions_deleted INTEGER,
52+
s3_keys_to_delete TEXT[]
53+
)
54+
LANGUAGE plpgsql
55+
AS $$
56+
DECLARE
57+
v_item_ids INTEGER[];
58+
v_submission_ids INTEGER[];
59+
v_review_ids INTEGER[];
60+
v_form_ids INTEGER[];
61+
v_document_ids INTEGER[];
62+
v_document_urls TEXT[];
63+
v_version_id INTEGER;
64+
v_original_package_id INTEGER;
65+
66+
v_items_deleted INTEGER := 0;
67+
v_submissions_deleted INTEGER := 0;
68+
v_forms_deleted INTEGER := 0;
69+
v_documents_deleted INTEGER := 0;
70+
v_geo_deleted INTEGER := 0;
71+
v_reviews_deleted INTEGER := 0;
72+
v_review_entries_deleted INTEGER := 0;
73+
v_item_notes_deleted INTEGER := 0;
74+
v_update_requests_deleted INTEGER := 0;
75+
v_internal_docs_deleted INTEGER := 0;
76+
v_package_metadata_deleted INTEGER := 0;
77+
v_activity_logs_deleted INTEGER := 0;
78+
v_email_queue_deleted INTEGER := 0;
79+
v_package_versions_deleted INTEGER := 0;
80+
v_package_logs_deleted INTEGER := 0;
81+
82+
v_s3_keys TEXT[] := ARRAY[]::TEXT[];
83+
BEGIN
84+
-- Guard: package must exist.
85+
IF NOT EXISTS (SELECT 1 FROM packages WHERE id = p_package_id) THEN
86+
RAISE EXCEPTION 'Package % does not exist', p_package_id;
87+
END IF;
88+
89+
-- -------------------------------------------------------------------------
90+
-- 1. Resolve the id graph BEFORE deleting anything.
91+
-- -------------------------------------------------------------------------
92+
SELECT version_id INTO v_version_id FROM packages WHERE id = p_package_id;
93+
94+
IF v_version_id IS NOT NULL THEN
95+
SELECT original_package_id INTO v_original_package_id
96+
FROM package_versions WHERE id = v_version_id;
97+
END IF;
98+
99+
-- Items under this package.
100+
SELECT array_agg(id) INTO v_item_ids
101+
FROM items WHERE package_id = p_package_id;
102+
v_item_ids := COALESCE(v_item_ids, ARRAY[]::INTEGER[]);
103+
104+
-- Submissions under those items.
105+
IF array_length(v_item_ids, 1) IS NOT NULL THEN
106+
SELECT array_agg(id) INTO v_submission_ids
107+
FROM submissions WHERE item_id = ANY(v_item_ids);
108+
109+
SELECT array_agg(id) INTO v_review_ids
110+
FROM submission_reviews WHERE item_id = ANY(v_item_ids);
111+
END IF;
112+
v_submission_ids := COALESCE(v_submission_ids, ARRAY[]::INTEGER[]);
113+
v_review_ids := COALESCE(v_review_ids, ARRAY[]::INTEGER[]);
114+
115+
-- Forms and documents referenced by those submissions.
116+
IF array_length(v_submission_ids, 1) IS NOT NULL THEN
117+
SELECT array_agg(DISTINCT submitted_form_id)
118+
INTO v_form_ids
119+
FROM submissions
120+
WHERE id = ANY(v_submission_ids) AND submitted_form_id IS NOT NULL;
121+
122+
SELECT array_agg(DISTINCT submitted_document_id)
123+
INTO v_document_ids
124+
FROM submissions
125+
WHERE id = ANY(v_submission_ids) AND submitted_document_id IS NOT NULL;
126+
END IF;
127+
v_form_ids := COALESCE(v_form_ids, ARRAY[]::INTEGER[]);
128+
v_document_ids := COALESCE(v_document_ids, ARRAY[]::INTEGER[]);
129+
130+
-- Document URLs (used to find geo uploads and to report S3 cleanup).
131+
IF array_length(v_document_ids, 1) IS NOT NULL THEN
132+
SELECT array_agg(url) INTO v_document_urls
133+
FROM submitted_documents WHERE id = ANY(v_document_ids);
134+
END IF;
135+
v_document_urls := COALESCE(v_document_urls, ARRAY[]::TEXT[]);
136+
137+
-- Collect S3 keys from internal staff documents for the caller to clean up.
138+
SELECT COALESCE(array_agg(url), ARRAY[]::TEXT[])
139+
INTO v_s3_keys
140+
FROM internal_staff_documents
141+
WHERE package_id = p_package_id AND url IS NOT NULL;
142+
143+
-- Add document urls to the S3 cleanup report.
144+
v_s3_keys := v_s3_keys || v_document_urls;
145+
146+
-- Add geo upload S3 tiers (raw/preview/standard) for matching documents.
147+
IF array_length(v_document_urls, 1) IS NOT NULL THEN
148+
SELECT v_s3_keys
149+
|| COALESCE(array_agg(raw_s3_key) FILTER (WHERE raw_s3_key IS NOT NULL), ARRAY[]::TEXT[])
150+
|| COALESCE(array_agg(preview_s3_key) FILTER (WHERE preview_s3_key IS NOT NULL), ARRAY[]::TEXT[])
151+
|| COALESCE(array_agg(standard_s3_key) FILTER (WHERE standard_s3_key IS NOT NULL), ARRAY[]::TEXT[])
152+
INTO v_s3_keys
153+
FROM geo_data_uploads
154+
WHERE raw_s3_key = ANY(v_document_urls);
155+
END IF;
156+
157+
-- -------------------------------------------------------------------------
158+
-- 2. Delete value-keyed / non-cascaded tables that reference the package
159+
-- or its children, from the leaves inward.
160+
-- -------------------------------------------------------------------------
161+
162+
-- 2a. geo_data_uploads (no FK; matched by document url == raw_s3_key).
163+
IF array_length(v_document_urls, 1) IS NOT NULL THEN
164+
DELETE FROM geo_data_uploads WHERE raw_s3_key = ANY(v_document_urls);
165+
GET DIAGNOSTICS v_geo_deleted = ROW_COUNT;
166+
END IF;
167+
168+
-- 2b. activity_logs for the submissions (entity_type = 'SUBMISSION').
169+
IF array_length(v_submission_ids, 1) IS NOT NULL THEN
170+
DELETE FROM activity_logs
171+
WHERE entity_type = 'SUBMISSION' AND entity_id = ANY(v_submission_ids);
172+
GET DIAGNOSTICS v_activity_logs_deleted = ROW_COUNT;
173+
END IF;
174+
175+
-- 2c. activity_logs for the package.
176+
-- Historically keyed by package_versions.original_package_id; older rows
177+
-- may still be keyed by the raw package id. Clear both to be safe.
178+
DELETE FROM activity_logs
179+
WHERE entity_type = 'PACKAGE'
180+
AND entity_id IN (p_package_id, COALESCE(v_original_package_id, -1));
181+
GET DIAGNOSTICS v_package_logs_deleted = ROW_COUNT;
182+
v_activity_logs_deleted := v_activity_logs_deleted + v_package_logs_deleted;
183+
184+
-- 2d. email_queue for the package.
185+
DELETE FROM email_queue
186+
WHERE entity_type = 'PACKAGE' AND entity_id = p_package_id;
187+
GET DIAGNOSTICS v_email_queue_deleted = ROW_COUNT;
188+
189+
-- -------------------------------------------------------------------------
190+
-- 3. Delete the package. ON DELETE CASCADE removes, in order:
191+
-- package_metadata, update_requests, internal_staff_documents,
192+
-- items -> submission_reviews -> submission_review_entries,
193+
-- submission_item_notes, submissions.
194+
-- We capture counts first so the return value is accurate.
195+
-- -------------------------------------------------------------------------
196+
SELECT count(*) INTO v_package_metadata_deleted FROM package_metadata WHERE package_id = p_package_id;
197+
SELECT count(*) INTO v_update_requests_deleted FROM update_requests WHERE submission_package_id = p_package_id;
198+
SELECT count(*) INTO v_internal_docs_deleted FROM internal_staff_documents WHERE package_id = p_package_id;
199+
v_items_deleted := COALESCE(array_length(v_item_ids, 1), 0);
200+
v_submissions_deleted := COALESCE(array_length(v_submission_ids, 1), 0);
201+
v_reviews_deleted := COALESCE(array_length(v_review_ids, 1), 0);
202+
v_forms_deleted := COALESCE(array_length(v_form_ids, 1), 0);
203+
v_documents_deleted := COALESCE(array_length(v_document_ids, 1), 0);
204+
205+
IF array_length(v_review_ids, 1) IS NOT NULL THEN
206+
SELECT count(*) INTO v_review_entries_deleted
207+
FROM submission_review_entries WHERE review_id = ANY(v_review_ids);
208+
END IF;
209+
IF array_length(v_item_ids, 1) IS NOT NULL THEN
210+
SELECT count(*) INTO v_item_notes_deleted
211+
FROM submission_item_notes WHERE item_id = ANY(v_item_ids);
212+
END IF;
213+
214+
DELETE FROM packages WHERE id = p_package_id;
215+
216+
-- -------------------------------------------------------------------------
217+
-- 4. Now that submissions are gone, remove the forms/documents they
218+
-- pointed to (these FKs point TO those tables, so they survive the
219+
-- cascade and must be deleted explicitly, after the package).
220+
-- -------------------------------------------------------------------------
221+
IF array_length(v_document_ids, 1) IS NOT NULL THEN
222+
DELETE FROM submitted_documents WHERE id = ANY(v_document_ids);
223+
GET DIAGNOSTICS v_documents_deleted = ROW_COUNT;
224+
END IF;
225+
226+
IF array_length(v_form_ids, 1) IS NOT NULL THEN
227+
DELETE FROM submitted_forms WHERE id = ANY(v_form_ids);
228+
GET DIAGNOSTICS v_forms_deleted = ROW_COUNT;
229+
END IF;
230+
231+
-- -------------------------------------------------------------------------
232+
-- 5. package_versions lineage row.
233+
-- Only delete it if no OTHER package still references this version id
234+
-- (a version row can be shared across the version lineage).
235+
-- -------------------------------------------------------------------------
236+
IF v_version_id IS NOT NULL THEN
237+
IF NOT EXISTS (SELECT 1 FROM packages WHERE version_id = v_version_id) THEN
238+
DELETE FROM package_versions WHERE id = v_version_id;
239+
GET DIAGNOSTICS v_package_versions_deleted = ROW_COUNT;
240+
END IF;
241+
END IF;
242+
243+
-- -------------------------------------------------------------------------
244+
-- 6. Return the report.
245+
-- -------------------------------------------------------------------------
246+
deleted_package_id := p_package_id;
247+
items_deleted := v_items_deleted;
248+
submissions_deleted := v_submissions_deleted;
249+
submitted_forms_deleted := v_forms_deleted;
250+
submitted_documents_deleted := v_documents_deleted;
251+
geo_uploads_deleted := v_geo_deleted;
252+
reviews_deleted := v_reviews_deleted;
253+
review_entries_deleted := v_review_entries_deleted;
254+
item_notes_deleted := v_item_notes_deleted;
255+
update_requests_deleted := v_update_requests_deleted;
256+
internal_docs_deleted := v_internal_docs_deleted;
257+
package_metadata_deleted := v_package_metadata_deleted;
258+
activity_logs_deleted := v_activity_logs_deleted;
259+
email_queue_deleted := v_email_queue_deleted;
260+
package_versions_deleted := v_package_versions_deleted;
261+
s3_keys_to_delete := v_s3_keys;
262+
263+
RETURN NEXT;
264+
END;
265+
$$;

‎submit-api/src/submit_api/services/submission/__init__.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,12 @@ def update_submission_status(cls, submission_id, status):
220220

221221
submission.status = status
222222

223+
# Clear the New Version / New Document badge once a staff member
224+
# verifies or acknowledges the document. The badge is driven by
225+
# is_updated, so resetting it here hides the badge after review.
226+
if SubmissionStatus[status] in (SubmissionStatus.VERIFIED, SubmissionStatus.ACKNOWLEDGED):
227+
submission.is_updated = False
228+
223229
session.add(submission)
224230
session.flush()
225231
# Only update package status if it's not already ACKNOWLEDGED

0 commit comments

Comments
 (0)