Skip to content

Commit 7e83fb4

Browse files
chore(contributor_team): add validation message when archive non empty
contributor team.
1 parent 1eb1388 commit 7e83fb4

File tree

7 files changed

+158
-3
lines changed

7 files changed

+158
-3
lines changed

apps/common/admin.py

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,83 @@
1-
# Register your models here.
1+
import typing
2+
from datetime import datetime
3+
4+
from django.contrib import admin
5+
from django.db import models
6+
from django.http import HttpRequest
7+
8+
from apps.common.models import UserResource
9+
10+
DjangoModel = typing.TypeVar("DjangoModel", bound=models.Model)
11+
12+
13+
class UserResourceAdmin(admin.ModelAdmin):
14+
@typing.override
15+
def get_readonly_fields(self, *args, **kwargs):
16+
readonly_fields = super().get_readonly_fields(*args, **kwargs) # type: ignore[reportAttributeAccessIssue]
17+
return [
18+
# To maintain order
19+
*dict.fromkeys(
20+
[
21+
*readonly_fields,
22+
"created_at",
23+
"created_by",
24+
"modified_at",
25+
"modified_by",
26+
],
27+
),
28+
]
29+
30+
@typing.override
31+
def save_model(self, request, obj, form, change):
32+
if not change:
33+
obj.created_by = request.user
34+
obj.modified_by = request.user
35+
super().save_model(request, obj, form, change) # type: ignore[reportAttributeAccessIssue]
36+
37+
@typing.override
38+
def save_formset(self, request, form, formset, change) -> None:
39+
if not issubclass(formset.model, UserResource):
40+
return super().save_formset(request, form, formset, change)
41+
# https://docs.djangoproject.com/en/4.2/ref/contrib/admin/#django.contrib.admin.ModelAdmin.save_formset
42+
instances = formset.save(commit=False)
43+
for obj in formset.deleted_objects:
44+
obj.delete()
45+
for instance in instances:
46+
# UserResource changes
47+
if instance.pk is None:
48+
instance.created_by = request.user
49+
instance.modified_by = request.user
50+
instance.save()
51+
return None
52+
53+
@typing.override
54+
def get_queryset(self, request: HttpRequest) -> models.QuerySet[DjangoModel]:
55+
return super().get_queryset(request).select_related("created_by", "modified_by")
56+
57+
58+
class ArchivableResourceAdmin(UserResourceAdmin, admin.ModelAdmin):
59+
@typing.override
60+
def get_readonly_fields(self, *args, **kwargs):
61+
readonly_fields = super().get_readonly_fields(*args, **kwargs) # type: ignore[reportAttributeAccessIssue]
62+
return [
63+
*dict.fromkeys(
64+
[
65+
*readonly_fields,
66+
"archived_by",
67+
"archived_at",
68+
],
69+
),
70+
]
71+
72+
@typing.override
73+
def save_model(self, request, obj, form, change):
74+
if not change:
75+
obj.created_by = request.user
76+
obj.modified_by = request.user
77+
if obj.is_archived:
78+
obj.archived_by = request.user
79+
obj.archived_at = datetime.now()
80+
else:
81+
obj.archived_by = None
82+
obj.archived_at = None
83+
super().save_model(request, obj, form, change) # type: ignore[reportAttributeAccessIssue]

apps/contributor/admin.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from django.contrib import admin
22
from djangoql.admin import DjangoQLSearchMixin
33

4+
from apps.common.admin import ArchivableResourceAdmin
5+
46
from .models import ContributorTeam, ContributorUser, ContributorUserGroup, ContributorUserGroupMembership
57

68

@@ -28,7 +30,7 @@ class ContributorUserInline(admin.TabularInline):
2830

2931

3032
@admin.register(ContributorTeam)
31-
class ContributorTeamAdmin(DjangoQLSearchMixin, admin.ModelAdmin):
33+
class ContributorTeamAdmin(ArchivableResourceAdmin, DjangoQLSearchMixin, admin.ModelAdmin):
3234
inlines = [ContributorUserInline]
3335
list_display = (
3436
"name",

apps/contributor/factories.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from ulid import ULID
99

1010
from .models import (
11+
ContributorTeam,
1112
ContributorUser,
1213
ContributorUserGroup,
1314
ContributorUserGroupMembership,
@@ -46,10 +47,19 @@ class Meta:
4647
model = ContributorUserGroupMembershipLog
4748

4849

50+
class ContributorTeamFactory(DjangoModelFactory):
51+
class Meta:
52+
model = ContributorTeam
53+
54+
client_id = factory.LazyFunction(lambda: str(ULID()))
55+
name = factory.Sequence(lambda n: f"Contributor User Team {n}")
56+
57+
4958
# NOTE: Make sure to add type hints for each factory class defined below
5059
# NOTE: This needs to be at the end of this file
5160
if typing.TYPE_CHECKING:
5261
ContributorUserFactory: type[DjangoModelFactory[ContributorUser]]
62+
ContributorTeamFactory: type[DjangoModelFactory[ContributorTeam]]
5363
ContributorUserGroupFactory: type[DjangoModelFactory[ContributorUserGroup]]
5464
ContributorUserGroupMembershipFactory: type[DjangoModelFactory[ContributorUserGroupMembership]]
5565
ContributorUserGroupMembershipLogFactory: type[DjangoModelFactory[ContributorUserGroupMembershipLog]]

apps/contributor/models.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
# pyright: reportUninitializedInstanceVariable=false
22
import typing
33

4+
from django.core.exceptions import ValidationError
45
from django.db import models
6+
from django.utils.translation import gettext
57
from django_choices_field import IntegerChoicesField
68

79
from apps.common.models import ArchivableResource, UserResource
@@ -84,3 +86,11 @@ class ContributorTeam(ArchivableResource, UserResource): # type: ignore[reportI
8486
@typing.override
8587
def __str__(self):
8688
return self.name
89+
90+
@typing.override
91+
def clean(self):
92+
super().clean()
93+
if self.pk and self.is_archived and ContributorUser.objects.filter(team_id=self.pk).exists():
94+
raise ValidationError(
95+
{"is_archived": gettext("Cannot archive a team that still has team members.")},
96+
)
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import typing
2+
3+
import pytest # type: ignore[reportMissingImports]
4+
from django.contrib.admin.sites import AdminSite
5+
from django.core.exceptions import ValidationError
6+
7+
from apps.contributor.admin import ContributorTeamAdmin
8+
from apps.contributor.factories import ContributorTeamFactory, ContributorUserFactory
9+
from apps.contributor.models import ContributorTeam
10+
from apps.user.factories import UserFactory
11+
from apps.user.models import User
12+
from main.tests import TestCase
13+
14+
15+
class MockRequest:
16+
def __init__(self, user: User):
17+
self.user = user
18+
19+
20+
class TestContributorTeam(TestCase):
21+
@typing.override
22+
@classmethod
23+
def setUpClass(cls):
24+
super().setUpClass()
25+
cls.user = UserFactory.create()
26+
cls.user_resource_kwargs = dict(
27+
created_by=cls.user,
28+
modified_by=cls.user,
29+
)
30+
cls.site = AdminSite()
31+
cls.admin = ContributorTeamAdmin(ContributorTeam, cls.site)
32+
cls.contributor_team = ContributorTeamFactory.create(**cls.user_resource_kwargs)
33+
cls.contributor_user = ContributorUserFactory.create(
34+
user_id="test_id",
35+
team=cls.contributor_team,
36+
)
37+
38+
def test_cannot_archive_team_with_members(self):
39+
self.contributor_team.is_archived = True
40+
with pytest.raises(ValidationError):
41+
self.contributor_team.clean()
42+
43+
def test_archive_team(self):
44+
request = MockRequest(user=self.user)
45+
self.force_login(request.user)
46+
self.contributor_user.delete()
47+
self.contributor_team.is_archived = True
48+
self.admin.save_model(request, self.contributor_team, form=None, change=True) # type: ignore[reportArgumentType]
49+
assert self.contributor_team.is_archived is True
50+
assert self.contributor_team.archived_by == self.user

utils/common.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ def validate_imagery_url(url: str, *, support_quadkey: bool | None = True):
3636

3737

3838
def validate_ulid(val: str):
39+
# TODO: add suggestion for ULID value for local development (use settings.debug)
3940
if val == "":
4041
raise ValidationError(
4142
gettext("Empty string is not a valid ULID value"),

0 commit comments

Comments
 (0)