Skip to content

Commit 4a9b204

Browse files
authored
Merge pull request #5 from maykinmedia/feature/create-list-endpoint
Add endpoint for list creation
2 parents bd918f9 + 8a07e49 commit 4a9b204

File tree

10 files changed

+586
-4
lines changed

10 files changed

+586
-4
lines changed

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@ class UserSerializer(serializers.ModelSerializer):
1919

2020
class Meta:
2121
model = User
22-
fields = ("username", "first_name", "last_name", "email", "role")
22+
fields = ("pk", "username", "first_name", "last_name", "email", "role")

backend/src/openarchiefbeheer/api/urls.py

+16-3
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,16 @@
55
SpectacularJSONAPIView,
66
SpectacularRedocView,
77
)
8+
from rest_framework import routers
89

910
from openarchiefbeheer.accounts.api.views import ReviewersView
11+
from openarchiefbeheer.destruction.api.viewsets import DestructionListViewSet
1012

1113
app_name = "api"
1214

15+
router = routers.DefaultRouter(trailing_slash=False)
16+
router.register(r"destruction-lists", DestructionListViewSet)
17+
1318

1419
urlpatterns = [
1520
# API documentation
@@ -40,8 +45,16 @@
4045
),
4146
# Actual endpoints
4247
path(
43-
"v1/zaken/",
44-
include("openarchiefbeheer.api.zaken.urls", namespace="zaken"),
48+
"v1/",
49+
include(
50+
[
51+
path(
52+
"zaken/",
53+
include("openarchiefbeheer.api.zaken.urls", namespace="zaken"),
54+
),
55+
path("reviewers/", ReviewersView.as_view(), name="reviewers"),
56+
path("", include(router.urls)),
57+
]
58+
),
4559
),
46-
path("v1/reviewers/", ReviewersView.as_view(), name="reviewers"),
4760
]

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

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from django.utils.translation import gettext_lazy as _
2+
3+
from rest_framework import permissions
4+
5+
6+
class CanStartDestructionPermission(permissions.BasePermission):
7+
message = _("You are not allowed to create a destruction list.")
8+
9+
def has_permission(self, request, view):
10+
return request.user.role.can_start_destruction
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
from django.db.models import Q
2+
from django.utils.translation import gettext_lazy as _
3+
4+
from rest_framework import serializers
5+
from rest_framework.exceptions import ValidationError
6+
7+
from openarchiefbeheer.accounts.api.serializers import UserSerializer
8+
9+
from ..constants import ListItemStatus
10+
from ..models import DestructionList, DestructionListAssignee, DestructionListItem
11+
12+
13+
class DestructionListAssigneeSerializer(serializers.ModelSerializer):
14+
class Meta:
15+
model = DestructionListAssignee
16+
fields = ("user", "order")
17+
18+
19+
class DestructionListItemSerializer(serializers.ModelSerializer):
20+
class Meta:
21+
model = DestructionListItem
22+
fields = (
23+
"zaak",
24+
"extra_zaak_data",
25+
)
26+
27+
def validate(self, attrs: dict) -> dict:
28+
if DestructionListItem.objects.filter(
29+
Q(~Q(status=ListItemStatus.removed), zaak=attrs["zaak"])
30+
).exists():
31+
raise ValidationError(
32+
{
33+
"zaak": _(
34+
"This case was already included in another destruction list and was not exempt during the review process."
35+
)
36+
}
37+
)
38+
39+
return attrs
40+
41+
42+
class DestructionListSerializer(serializers.ModelSerializer):
43+
assignees = DestructionListAssigneeSerializer(many=True)
44+
items = DestructionListItemSerializer(many=True)
45+
author = UserSerializer(read_only=True)
46+
47+
class Meta:
48+
model = DestructionList
49+
fields = (
50+
"name",
51+
"author",
52+
"contains_sensitive_info",
53+
"assignees",
54+
"items",
55+
)
56+
57+
def create(self, validated_data: dict) -> DestructionList:
58+
assignees_data = validated_data.pop("assignees")
59+
items_data = validated_data.pop("items")
60+
61+
validated_data["author"] = self.context["request"].user
62+
destruction_list = DestructionList.objects.create(**validated_data)
63+
64+
DestructionListItem.objects.bulk_create(
65+
[
66+
DestructionListItem(**{**item, "destruction_list": destruction_list})
67+
for item in items_data
68+
]
69+
)
70+
assignees = DestructionListAssignee.objects.bulk_create(
71+
[
72+
DestructionListAssignee(
73+
**{**assignee, "destruction_list": destruction_list}
74+
)
75+
for assignee in assignees_data
76+
]
77+
)
78+
79+
destruction_list.assign(assignees[0])
80+
return destruction_list
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from django.db import transaction
2+
3+
from rest_framework import mixins, viewsets
4+
from rest_framework.permissions import IsAuthenticated
5+
6+
from ..models import DestructionList
7+
from .permissions import CanStartDestructionPermission
8+
from .serializers import DestructionListSerializer
9+
10+
11+
class DestructionListViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet):
12+
serializer_class = DestructionListSerializer
13+
queryset = DestructionList.objects.all()
14+
15+
def get_permissions(self):
16+
if self.action == "create":
17+
permission_classes = [IsAuthenticated & CanStartDestructionPermission]
18+
else:
19+
permission_classes = [IsAuthenticated]
20+
return [permission() for permission in permission_classes]
21+
22+
@transaction.atomic
23+
def create(self, request, *args, **kwargs):
24+
# TODO log creation
25+
return super().create(request, *args, **kwargs)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Generated by Django 4.2.11 on 2024-05-01 14:25
2+
3+
from django.conf import settings
4+
from django.db import migrations, models
5+
import django.db.models.deletion
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12+
("destruction", "0001_initial"),
13+
]
14+
15+
operations = [
16+
migrations.CreateModel(
17+
name="DestructionListAssignee",
18+
fields=[
19+
(
20+
"id",
21+
models.AutoField(
22+
auto_created=True,
23+
primary_key=True,
24+
serialize=False,
25+
verbose_name="ID",
26+
),
27+
),
28+
(
29+
"order",
30+
models.PositiveIntegerField(
31+
db_index=True, editable=False, verbose_name="order"
32+
),
33+
),
34+
(
35+
"assigned_on",
36+
models.DateTimeField(
37+
blank=True, null=True, verbose_name="assigned on"
38+
),
39+
),
40+
(
41+
"destruction_list",
42+
models.ForeignKey(
43+
on_delete=django.db.models.deletion.CASCADE,
44+
related_name="assignees",
45+
to="destruction.destructionlist",
46+
verbose_name="destruction list",
47+
),
48+
),
49+
(
50+
"user",
51+
models.ForeignKey(
52+
help_text="The user assigned to the destruction list.",
53+
on_delete=django.db.models.deletion.PROTECT,
54+
to=settings.AUTH_USER_MODEL,
55+
verbose_name="user",
56+
),
57+
),
58+
],
59+
options={
60+
"verbose_name": "destruction list assignee",
61+
"verbose_name_plural": "destruction list assignees",
62+
"ordering": ("order",),
63+
"abstract": False,
64+
"unique_together": {("destruction_list", "user")},
65+
},
66+
),
67+
]

backend/src/openarchiefbeheer/destruction/models.py

+58
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1+
from django.conf import settings
2+
from django.core.mail import send_mail
13
from django.db import models
4+
from django.utils import timezone
25
from django.utils.translation import gettext_lazy as _
36

7+
from ordered_model.models import OrderedModel
8+
49
from openarchiefbeheer.destruction.constants import ListItemStatus, ListStatus
510

611

@@ -61,6 +66,10 @@ class Meta:
6166
def __str__(self):
6267
return self.name
6368

69+
@staticmethod
70+
def assign(assignee: "DestructionListAssignee") -> None:
71+
assignee.assign()
72+
6473

6574
class DestructionListItem(models.Model):
6675
destruction_list = models.ForeignKey(
@@ -96,3 +105,52 @@ class Meta:
96105

97106
def __str__(self):
98107
return f"{self.destruction_list}: {self.zaak}"
108+
109+
110+
class DestructionListAssignee(OrderedModel):
111+
destruction_list = models.ForeignKey(
112+
DestructionList,
113+
on_delete=models.CASCADE,
114+
related_name="assignees",
115+
verbose_name=_("destruction list"),
116+
)
117+
user = models.ForeignKey(
118+
"accounts.User",
119+
on_delete=models.PROTECT,
120+
verbose_name=_("user"),
121+
help_text=_("The user assigned to the destruction list."),
122+
)
123+
assigned_on = models.DateTimeField(_("assigned on"), blank=True, null=True)
124+
125+
class Meta(OrderedModel.Meta):
126+
verbose_name = _("destruction list assignee")
127+
verbose_name_plural = _("destruction list assignees")
128+
unique_together = ("destruction_list", "user")
129+
130+
def __str__(self):
131+
return f"{self.user} ({self.destruction_list}, {self.order})"
132+
133+
def assign(self) -> None:
134+
# TODO Log assignment
135+
self.destruction_list.assignee = self.user
136+
self.assigned_on = timezone.now()
137+
138+
self.destruction_list.save()
139+
self.save()
140+
141+
self.notify()
142+
143+
# TODO refine what we want to do with notifications
144+
def notify(self) -> None:
145+
if not self.user.email:
146+
return
147+
148+
is_reviewer = self.user != self.destruction_list.author
149+
if is_reviewer:
150+
send_mail(
151+
_("Destruction list review request"),
152+
_("There is a destruction list review request for you."),
153+
settings.DEFAULT_FROM_EMAIL,
154+
[self.user.email],
155+
fail_silently=False,
156+
)

0 commit comments

Comments
 (0)