Skip to content

Commit 2ea3790

Browse files
authored
Merge pull request #379 from maykinmedia/fix/378-crash-on-zaak-null
[#378] Prevent crash if reviewItem has zaak = null
2 parents f879475 + 758c0f3 commit 2ea3790

File tree

8 files changed

+115
-21
lines changed

8 files changed

+115
-21
lines changed

backend/src/openarchiefbeheer/destruction/api/serializers.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -458,8 +458,9 @@ def create(self, validated_data: dict) -> DestructionListReview:
458458
class DestructionListItemReviewSerializer(serializers.ModelSerializer):
459459
zaak = serializers.SerializerMethodField(
460460
help_text=_(
461-
"In the case that the zaak has already been deleted, only the URL field will be returned."
462-
)
461+
"In the case that the zaak has already been deleted, this field will be null."
462+
),
463+
allow_null=True,
463464
)
464465

465466
class Meta:

backend/src/openarchiefbeheer/destruction/tests/e2e/features/test_feature_list_process_review.py

+35
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,38 @@ def create_data():
161161
await self.then.path_should_be(page, "/destruction-lists/00000000-0000-0000-0000-000000000000")
162162

163163
await self.then.zaaktype_filters_are(page, ["ZAAKTYPE-01 (ZAAKTYPE-01)"])
164+
165+
@tag("gh-378")
166+
async def test_zaak_removed_outside_process(self):
167+
@sync_to_async
168+
def create_data():
169+
record_manager = UserFactory.create(username="Record Manager", password="ANic3Password", role__can_start_destruction=True)
170+
171+
zaken = ZaakFactory.create_batch(2)
172+
list = DestructionListFactory.create(
173+
author=record_manager,
174+
assignee=record_manager,
175+
status=ListStatus.changes_requested,
176+
uuid="00000000-0000-0000-0000-000000000000",
177+
name="Destruction list to process",
178+
)
179+
item1 = DestructionListItemFactory.create(destruction_list=list, zaak=zaken[0])
180+
item2 = DestructionListItemFactory.create(destruction_list=list, zaak=zaken[1])
181+
182+
review = DestructionListReviewFactory.create(destruction_list=list, decision=ReviewDecisionChoices.rejected)
183+
DestructionListItemReviewFactory.create(destruction_list=list, destruction_list_item=item1, review=review)
184+
DestructionListItemReviewFactory.create(destruction_list=list, destruction_list_item=item2, review=review)
185+
186+
# Simulate the zaak being deleted by *something else*
187+
item1.zaak.delete()
188+
189+
async with browser_page() as page:
190+
await self.given.data_exists(create_data)
191+
await self.when.record_manager_logs_in(page)
192+
await self.then.path_should_be(page, "/destruction-lists")
193+
194+
await self.when.user_clicks_button(page, "Destruction list to process")
195+
196+
await self.then.path_should_be(page, "/destruction-lists/00000000-0000-0000-0000-000000000000")
197+
await self.then.page_should_contain_text(page, "Opnieuw indienen")
198+
await self.then.this_number_of_zaken_should_be_visible(page, 1)

backend/src/openarchiefbeheer/destruction/tests/e2e/features/test_feature_list_review.py

+36
Original file line numberDiff line numberDiff line change
@@ -270,3 +270,39 @@ def create_data():
270270
await self.then.path_should_be(page, "/destruction-lists/00000000-0000-0000-0000-000000000000/review")
271271

272272
await self.then.zaaktype_filters_are(page, ["ZAAKTYPE-01 (ZAAKTYPE-01)", "ZAAKTYPE-02 (ZAAKTYPE-02)"])
273+
274+
@tag("gh-378")
275+
async def test_zaak_removed_outside_process(self):
276+
@sync_to_async
277+
def create_data():
278+
record_manager = UserFactory.create(role__can_start_destruction=True)
279+
reviewer = UserFactory.create(username="Beoordelaar", password="ANic3Password", role__can_review_destruction=True)
280+
281+
zaken = ZaakFactory.create_batch(2)
282+
list = DestructionListFactory.create(
283+
author=record_manager,
284+
assignee=reviewer,
285+
status=ListStatus.ready_to_review,
286+
uuid="00000000-0000-0000-0000-000000000000",
287+
name="Destruction list to review",
288+
)
289+
item1 = DestructionListItemFactory.create(destruction_list=list, zaak=zaken[0])
290+
item2 = DestructionListItemFactory.create(destruction_list=list, zaak=zaken[1])
291+
292+
review = DestructionListReviewFactory.create(destruction_list=list, decision=ReviewDecisionChoices.rejected)
293+
DestructionListItemReviewFactory.create(destruction_list=list, destruction_list_item=item1, review=review)
294+
DestructionListItemReviewFactory.create(destruction_list=list, destruction_list_item=item2, review=review)
295+
296+
# Simulate the zaak being deleted by *something else*
297+
item1.zaak.delete()
298+
299+
async with browser_page() as page:
300+
await self.given.data_exists(create_data)
301+
await self.when.reviewer_logs_in(page)
302+
await self.then.path_should_be(page, "/destruction-lists")
303+
304+
await self.when.user_clicks_button(page, "Destruction list to review")
305+
306+
await self.then.path_should_be(page, "/destruction-lists/00000000-0000-0000-0000-000000000000/review")
307+
await self.then.page_should_contain_text(page, "Accorderen")
308+
await self.then.this_number_of_zaken_should_be_visible(page, 1)

backend/src/openarchiefbeheer/zaken/admin.py

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
@admin.register(Zaak)
1111
class ZaakAdmin(admin.ModelAdmin):
12+
search_fields = ("identificatie", "uuid")
1213

1314
def get_urls(self):
1415
urls = super().get_urls()

frontend/src/fixtures/reviewItem.ts

+7-5
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
import { ReviewItem } from "../lib/api/review";
1+
import { ReviewItemWithZaak } from "../lib/api/review";
22
import { createArrayFactory, createObjectFactory } from "./factory";
33
import { zaakFactory, zakenFactory } from "./zaak";
44

5-
const FIXTURE_REVIEW_ITEM: ReviewItem = {
5+
const FIXTURE_REVIEW_ITEM: ReviewItemWithZaak = {
66
pk: 1,
77
zaak: zaakFactory(),
88
feedback: "Deze niet",
99
};
1010

11-
const FIXTURE_REVIEW_ITEMS: ReviewItem[] = [
11+
const FIXTURE_REVIEW_ITEMS: ReviewItemWithZaak[] = [
1212
FIXTURE_REVIEW_ITEM,
1313
{
1414
pk: 2,
@@ -22,7 +22,9 @@ const FIXTURE_REVIEW_ITEMS: ReviewItem[] = [
2222
},
2323
];
2424

25-
const reviewItemFactory = createObjectFactory<ReviewItem>(FIXTURE_REVIEW_ITEM);
26-
const reviewItemsFactory = createArrayFactory<ReviewItem>(FIXTURE_REVIEW_ITEMS);
25+
const reviewItemFactory =
26+
createObjectFactory<ReviewItemWithZaak>(FIXTURE_REVIEW_ITEM);
27+
const reviewItemsFactory =
28+
createArrayFactory<ReviewItemWithZaak>(FIXTURE_REVIEW_ITEMS);
2729

2830
export { reviewItemFactory, reviewItemsFactory };

frontend/src/lib/api/review.ts

+6
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ export type ZaakReview = {
1919
};
2020

2121
export type ReviewItem = {
22+
pk: number;
23+
zaak: Zaak | null;
24+
feedback: string;
25+
};
26+
27+
export type ReviewItemWithZaak = {
2228
pk: number;
2329
zaak: Zaak;
2430
feedback: string;

frontend/src/pages/destructionlist/detail/DestructionListDetail.loader.ts

+15-9
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
import { listSelectielijstKlasseChoices } from "../../../lib/api/private";
1616
import {
1717
Review,
18-
ReviewItem,
18+
ReviewItemWithZaak,
1919
getLatestReview,
2020
listReviewItems,
2121
} from "../../../lib/api/review";
@@ -46,7 +46,7 @@ export interface DestructionListDetailContext {
4646
user: User;
4747

4848
review: Review | null;
49-
reviewItems: ReviewItem[] | null;
49+
reviewItems: ReviewItemWithZaak[] | null;
5050

5151
selectieLijstKlasseChoicesMap: Record<string, Option[]> | null;
5252
}
@@ -83,15 +83,21 @@ export const destructionListDetailLoader = loginRequired(
8383
})
8484
: null;
8585

86+
// #378 - If for some unfortunate reason a zaak has been deleted outside of the process,
87+
// item.zaak can be null
88+
const reviewItemsWithZaak = reviewItems
89+
? (reviewItems.filter((item) => !!item.zaak) as ReviewItemWithZaak[])
90+
: reviewItems;
91+
8692
/**
8793
* Fetch selectable zaken: empty array if review collected OR all zaken not in another destruction list.
8894
* FIXME: Accept no/implement real pagination?
8995
*/
9096
const getDestructionListItems =
9197
async (): Promise<PaginatedDestructionListItems> =>
92-
reviewItems
98+
reviewItemsWithZaak
9399
? {
94-
count: reviewItems.length,
100+
count: reviewItemsWithZaak.length,
95101
next: null,
96102
previous: null,
97103
results: [],
@@ -122,13 +128,13 @@ export const destructionListDetailLoader = loginRequired(
122128
* reviewItems ? await listSelectieLijstKlasseChoices({}) : null,
123129
*/
124130
const getReviewItems = () =>
125-
reviewItems
131+
reviewItemsWithZaak
126132
? cacheMemo(
127133
"selectieLijstKlasseChoicesMap",
128134
async () =>
129135
Object.fromEntries(
130136
await Promise.all(
131-
reviewItems.map(async (ri) => {
137+
reviewItemsWithZaak.map(async (ri) => {
132138
const choices = await listSelectielijstKlasseChoices({
133139
zaak: ri.zaak.url,
134140
});
@@ -137,12 +143,12 @@ export const destructionListDetailLoader = loginRequired(
137143
),
138144
),
139145
// @ts-expect-error - Params not used in function but in case key only.
140-
reviewItems.map((ri) => ri.pk),
146+
reviewItemsWithZaak.map((ri) => ri.pk),
141147
)
142148
: null;
143149

144150
const getSelectableZaken = () =>
145-
reviewItems || destructionList.status === "ready_to_delete"
151+
reviewItemsWithZaak || destructionList.status === "ready_to_delete"
146152
? ({
147153
count: 0,
148154
next: null,
@@ -196,7 +202,7 @@ export const destructionListDetailLoader = loginRequired(
196202
user,
197203

198204
review: review,
199-
reviewItems: reviewItems,
205+
reviewItems: reviewItemsWithZaak,
200206

201207
selectieLijstKlasseChoicesMap,
202208
};

frontend/src/pages/destructionlist/review/DestructionListReview.loader.ts

+12-5
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
} from "../../../lib/api/destructionLists";
99
import {
1010
Review,
11-
ReviewItem,
11+
ReviewItemWithZaak,
1212
getLatestReview,
1313
listReviewItems,
1414
} from "../../../lib/api/review";
@@ -37,7 +37,7 @@ export type DestructionListReviewContext = {
3737

3838
paginatedZaken: PaginatedZaken;
3939
review: Review;
40-
reviewItems?: ReviewItem[];
40+
reviewItems?: ReviewItemWithZaak[];
4141
reviewResponse?: ReviewResponse;
4242
reviewers: User[];
4343

@@ -94,8 +94,15 @@ export const destructionListReviewLoader = loginRequired(
9494
storageKey,
9595
);
9696

97-
const zakenOnPage = reviewItems?.length
98-
? reviewItems.map((ri) => ri.zaak.url as string)
97+
// #378 - If for some unfortunate reason a zaak has been deleted outside of the process,
98+
// item.zaak can be null
99+
// TODO refactor: This code is the same as for the DestructionListDetail loader.
100+
const reviewItemsWithZaak = reviewItems
101+
? (reviewItems.filter((item) => !!item.zaak) as ReviewItemWithZaak[])
102+
: reviewItems;
103+
104+
const zakenOnPage = reviewItemsWithZaak?.length
105+
? reviewItemsWithZaak.map((ri) => ri.zaak.url as string)
99106
: zaken.results.map((z) => z.url as string);
100107

101108
const approvedZaakUrlsOnPagePromise = await Promise.all(
@@ -123,7 +130,7 @@ export const destructionListReviewLoader = loginRequired(
123130

124131
paginatedZaken: zaken,
125132
review: latestReview,
126-
reviewItems,
133+
reviewItems: reviewItemsWithZaak,
127134
reviewResponse,
128135
reviewers,
129136

0 commit comments

Comments
 (0)