Skip to content

Commit 0c2da26

Browse files
authored
Merge pull request #526 from maykinmedia/feature/502-review-process-report
[#502] Add review process to destruction report
2 parents 29e81e7 + 24c8c2f commit 0c2da26

File tree

6 files changed

+180
-14
lines changed

6 files changed

+180
-14
lines changed

backend/src/openarchiefbeheer/accounts/models.py

+2-6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from django.utils.translation import gettext_lazy as _
66

77
from .managers import UserManager
8+
from .utils import format_user
89

910

1011
class User(AbstractBaseUser, PermissionsMixin):
@@ -68,9 +69,4 @@ def get_short_name(self):
6869
return self.first_name
6970

7071
def get_name_with_username(self):
71-
username = self.get_username()
72-
if self.first_name or self.last_name:
73-
full_name = self.get_full_name()
74-
return f"{full_name} ({username})"
75-
76-
return username
72+
return format_user(self)

backend/src/openarchiefbeheer/accounts/utils.py

+35-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1+
from dataclasses import dataclass
2+
from typing import TYPE_CHECKING, Union
3+
14
from django.core.exceptions import ValidationError
25
from django.utils.translation import gettext_lazy as _
36

4-
from .models import User
7+
if TYPE_CHECKING:
8+
from .models import User
59

610

711
def validate_max_permissions(
8-
current_user: User, permissions=None, groups=None, is_superuser=None
12+
current_user: "User", permissions=None, groups=None, is_superuser=None
913
):
1014
"""
1115
Validate that the ``permissions``, ``groups`` and ``superuser``-flag are
@@ -45,7 +49,7 @@ def validate_max_permissions(
4549
)
4650

4751

48-
def validate_max_user_permissions(current_user: User, target_user: User):
52+
def validate_max_user_permissions(current_user: "User", target_user: "User") -> None:
4953
"""
5054
Validate that the ``target_user`` permissions are never more than the
5155
permissions of the ``current_user``.
@@ -56,3 +60,31 @@ def validate_max_user_permissions(current_user: User, target_user: User):
5660
target_user.groups,
5761
target_user.is_superuser,
5862
)
63+
64+
65+
@dataclass
66+
class UserData:
67+
username: str
68+
first_name: str = ""
69+
last_name: str = ""
70+
71+
72+
def format_user(user: Union[dict, "User"]) -> str:
73+
if isinstance(user, dict):
74+
user = UserData(
75+
**{
76+
"username": user["username"],
77+
"first_name": user.get("first_name"),
78+
"last_name": user.get("last_name"),
79+
}
80+
)
81+
82+
if user.first_name or user.last_name:
83+
full_name = "%s %s" % (user.first_name, user.last_name)
84+
return f"{full_name.strip()} ({user.username})"
85+
86+
return user.username
87+
88+
89+
def format_user_groups(groups: list[str]) -> str:
90+
return ", ".join(groups)

backend/src/openarchiefbeheer/destruction/destruction_report.py

+36-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
from dataclasses import dataclass
22
from typing import IO
33

4-
from django.utils.translation import gettext
4+
from django.utils.translation import gettext as _
55

66
import xlsxwriter
77
from glom import glom
8+
from timeline_logger.models import TimelineLog
89
from xlsxwriter.worksheet import Worksheet
910

11+
from openarchiefbeheer.accounts.utils import format_user, format_user_groups
12+
from openarchiefbeheer.logging.logevent import destruction_list_reviewed
13+
from openarchiefbeheer.logging.utils import get_event_template, get_readable_timestamp
1014
from openarchiefbeheer.zaken.api.constants import ZAAK_METADATA_FIELDS_MAPPINGS
1115

1216
from .constants import InternalStatus
@@ -17,6 +21,33 @@
1721
class DestructionReportGenerator:
1822
destruction_list: DestructionList
1923

24+
def add_review_process_table(
25+
self, worksheet: Worksheet, start_row: int = 0
26+
) -> None:
27+
column_names = [
28+
_("Group"),
29+
_("Name"),
30+
_("Date/Time"),
31+
_("Changes"),
32+
]
33+
worksheet.write_row(start_row, 0, column_names)
34+
35+
logs = TimelineLog.objects.for_object(self.destruction_list).filter(
36+
template=get_event_template(destruction_list_reviewed),
37+
extra_data__approved=True,
38+
)
39+
for row_count, log in enumerate(logs):
40+
# Not using the FK because the user might have been deleted in the mean time
41+
data = [
42+
format_user_groups(log.extra_data["user_groups"]),
43+
format_user(log.extra_data["user"]),
44+
get_readable_timestamp(log),
45+
# This column is not useful, since we are filtering on approved reviews.
46+
# But it was specifically requested.
47+
_("Has approved"),
48+
]
49+
worksheet.write_row(start_row + row_count + 1, 0, data)
50+
2051
def add_zaken_table(self, worksheet: Worksheet, start_row: int = 0) -> None:
2152
worksheet.write_row(
2253
start_row, 0, [field["name"] for field in ZAAK_METADATA_FIELDS_MAPPINGS]
@@ -36,8 +67,10 @@ def add_zaken_table(self, worksheet: Worksheet, start_row: int = 0) -> None:
3667
def generate_destruction_report(self, file: IO) -> None:
3768
workbook = xlsxwriter.Workbook(file.name, options={"in_memory": False})
3869

39-
worksheet = workbook.add_worksheet(name=gettext("Deleted zaken"))
70+
worksheet_zaken = workbook.add_worksheet(name=_("Deleted zaken"))
71+
worksheet_review_process = workbook.add_worksheet(name=_("Review process"))
4072

41-
self.add_zaken_table(worksheet)
73+
self.add_zaken_table(worksheet_zaken)
74+
self.add_review_process_table(worksheet_review_process)
4275

4376
workbook.close()

backend/src/openarchiefbeheer/destruction/tests/test_models.py

+85-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from datetime import date, datetime
22
from unittest.mock import patch
33

4+
from django.contrib.auth.models import Group
45
from django.core.exceptions import ObjectDoesNotExist
56
from django.test import TestCase
67
from django.utils import timezone
@@ -15,16 +16,23 @@
1516
from zgw_consumers.test.factories import ServiceFactory
1617

1718
from openarchiefbeheer.config.models import ArchiveConfig
19+
from openarchiefbeheer.logging import logevent
1820
from openarchiefbeheer.utils.results_store import ResultStore
1921
from openarchiefbeheer.zaken.models import Zaak
2022
from openarchiefbeheer.zaken.tests.factories import ZaakFactory
2123

2224
from ...accounts.tests.factories import UserFactory
23-
from ..constants import InternalStatus, ListItemStatus, ListStatus
25+
from ..constants import (
26+
InternalStatus,
27+
ListItemStatus,
28+
ListStatus,
29+
ReviewDecisionChoices,
30+
)
2431
from .factories import (
2532
DestructionListCoReviewFactory,
2633
DestructionListFactory,
2734
DestructionListItemFactory,
35+
DestructionListReviewFactory,
2836
ReviewResponseFactory,
2937
)
3038

@@ -408,6 +416,82 @@ def test_generate_destruction_report(self):
408416
),
409417
)
410418

419+
def test_generate_destruction_report_review_process(self):
420+
author = UserFactory.create(post__can_start_destruction=True)
421+
reviewer = UserFactory.create(
422+
first_name="John",
423+
last_name="Doe",
424+
username="jdoe1",
425+
post__can_review_destruction=True,
426+
)
427+
reviewer_group, created = Group.objects.get_or_create(name="Reviewer")
428+
reviewer.groups.add(reviewer_group)
429+
archivist = UserFactory.create(
430+
first_name="Alice",
431+
last_name="Wonderland",
432+
username="awonderland1",
433+
post__can_review_final_list=True,
434+
)
435+
archivist_group, created = Group.objects.get_or_create(name="Archivist")
436+
archivist.groups.add(archivist_group)
437+
438+
destruction_list = DestructionListFactory.create(
439+
status=ListStatus.deleted, author=author
440+
)
441+
442+
review_reviewer_rejected = DestructionListReviewFactory.create(
443+
author=reviewer, decision=ReviewDecisionChoices.rejected
444+
)
445+
review_reviewer_accepted = DestructionListReviewFactory.create(
446+
author=reviewer, decision=ReviewDecisionChoices.accepted
447+
)
448+
review_archivist_accepted = DestructionListReviewFactory.create(
449+
author=archivist, decision=ReviewDecisionChoices.accepted
450+
)
451+
452+
logevent.destruction_list_created(
453+
destruction_list, author, reviewer
454+
) # A log with a different template (should NOT be present in the report)
455+
456+
logevent.destruction_list_reviewed(
457+
destruction_list, review_reviewer_rejected, reviewer
458+
) # A rejection (should NOT be present in the report)
459+
with freeze_time("2024-05-02T16:00:00+02:00"):
460+
logevent.destruction_list_reviewed(
461+
destruction_list, review_reviewer_accepted, reviewer
462+
)
463+
with freeze_time("2024-05-08T09:45:00+02:00"):
464+
logevent.destruction_list_reviewed(
465+
destruction_list, review_archivist_accepted, archivist
466+
)
467+
468+
destruction_list.generate_destruction_report()
469+
470+
wb = load_workbook(filename=destruction_list.destruction_report.path)
471+
sheet_deleted_zaken = wb[gettext("Review process")]
472+
rows = list(sheet_deleted_zaken.iter_rows(values_only=True))
473+
474+
self.assertEqual(len(rows), 3)
475+
476+
self.assertEqual(
477+
rows[1],
478+
(
479+
"Reviewer",
480+
"John Doe (jdoe1)",
481+
"2024-05-02 16:00+02:00",
482+
gettext("Has approved"),
483+
),
484+
)
485+
self.assertEqual(
486+
rows[2],
487+
(
488+
"Archivist",
489+
"Alice Wonderland (awonderland1)",
490+
"2024-05-08 09:45+02:00",
491+
gettext("Has approved"),
492+
),
493+
)
494+
411495
def test_zaak_creation_skipped_if_internal_status_succeeded(self):
412496
destruction_list = DestructionListFactory.create(
413497
processing_status=InternalStatus.succeeded

backend/src/openarchiefbeheer/logging/logevent.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
format_zaaktype_choices,
2020
)
2121

22+
TEMPLATE_FORMAT = "logging/%(event)s.txt"
23+
2224

2325
def _create_log(
2426
model: Model, event: str, extra_data: dict | None = None, user: User | None = None
@@ -34,7 +36,7 @@ def _create_log(
3436

3537
return TimelineLog.objects.create(
3638
content_object=model,
37-
template=f"logging/{event}.txt",
39+
template=TEMPLATE_FORMAT % {"event": event},
3840
extra_data=extra_data,
3941
user=user,
4042
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from typing import Callable
2+
3+
from django.utils import timezone
4+
5+
from timeline_logger.models import TimelineLog
6+
7+
from .logevent import TEMPLATE_FORMAT
8+
9+
10+
def get_event_template(logging_func: Callable) -> str:
11+
return TEMPLATE_FORMAT % {"event": logging_func.__name__}
12+
13+
14+
def get_readable_timestamp(
15+
log: TimelineLog, separator: str = " ", timespec: str = "minutes"
16+
) -> str:
17+
return log.timestamp.astimezone(tz=timezone.get_default_timezone()).isoformat(
18+
sep=separator, timespec=timespec
19+
)

0 commit comments

Comments
 (0)