Skip to content

Commit 4f9d862

Browse files
committed
Add roles to UserAssignment
Model roles as an array field on the UserAssignment record. This allows us to map users to one or more roles with a specific provider. This will be used to implement an authorization layer throughout the service.
1 parent f1d8b96 commit 4f9d862

File tree

3 files changed

+108
-31
lines changed

3 files changed

+108
-31
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Generated by Django 5.2.6 on 2025-09-29 14:01
2+
3+
import django.contrib.postgres.fields
4+
import django.core.validators
5+
from django.db import migrations, models
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
('clinics', '0017_alter_userassignment_options'),
12+
]
13+
14+
operations = [
15+
migrations.AddField(
16+
model_name='userassignment',
17+
name='roles',
18+
field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('Clinical', 'Clinical'), ('Administrative', 'Administrative')], max_length=32), default=list, help_text='Roles granted to the user for this provider.', size=None, validators=[django.core.validators.MinLengthValidator(1)]),
19+
),
20+
]

manage_breast_screening/clinics/models.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
from enum import StrEnum
44

55
from django.conf import settings
6+
from django.contrib.postgres.fields import ArrayField
7+
from django.core.validators import MinLengthValidator
68
from django.db import models
79

10+
from ..auth.models import Role
811
from ..core.models import BaseModel
912

1013

@@ -189,6 +192,24 @@ class UserAssignment(BaseModel):
189192
provider = models.ForeignKey(
190193
Provider, on_delete=models.PROTECT, related_name="assignments"
191194
)
195+
roles = ArrayField(
196+
base_field=models.CharField(
197+
max_length=32,
198+
choices=[
199+
(Role.CLINICAL.value, Role.CLINICAL.value),
200+
(Role.ADMINISTRATIVE.value, Role.ADMINISTRATIVE.value),
201+
],
202+
),
203+
default=list,
204+
validators=[MinLengthValidator(1)],
205+
help_text="Roles granted to the user for this provider.",
206+
)
207+
208+
def save(self, *args, **kwargs):
209+
if self.roles:
210+
# Remove duplicates and sort
211+
self.roles = sorted(list(set(self.roles)))
212+
super().save(*args, **kwargs)
192213

193214
class Meta:
194215
unique_together = ["user", "provider"]

manage_breast_screening/clinics/tests/test_models.py

Lines changed: 67 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import time_machine
66
from pytest_django.asserts import assertQuerySetEqual
77

8+
from manage_breast_screening.auth.models import Role
89
from manage_breast_screening.clinics import models
910

1011
from .factories import (
@@ -38,37 +39,72 @@ def test_status_filtering():
3839
assertQuerySetEqual(models.Clinic.objects.completed(), {past}, ordered=False)
3940

4041

41-
@pytest.mark.django_db
42-
def test_user_assignment_creation():
43-
assignment = UserAssignmentFactory()
44-
assert assignment.user is not None
45-
assert assignment.provider is not None
46-
assert assignment.pk is not None
47-
48-
49-
@pytest.mark.django_db
50-
def test_user_assignment_str():
51-
user = UserFactory(first_name="John", last_name="Doe")
52-
provider = ProviderFactory(name="Test Provider")
53-
assignment = UserAssignmentFactory(user=user, provider=provider)
54-
assert str(assignment) == "John Doe → Test Provider"
55-
42+
class TestUserAssignment:
43+
def test_str(self):
44+
user = UserFactory.build(first_name="John", last_name="Doe")
45+
provider = ProviderFactory.build(name="Test Provider")
46+
assignment = UserAssignmentFactory.build(user=user, provider=provider)
47+
assert str(assignment) == "John Doe → Test Provider"
5648

57-
@pytest.mark.django_db
58-
def test_user_assignment_unique_constraint():
59-
user = UserFactory()
60-
provider = ProviderFactory()
61-
UserAssignmentFactory(user=user, provider=provider)
62-
63-
with pytest.raises(Exception):
49+
@pytest.mark.django_db
50+
def test_unique_constraint(self):
51+
user = UserFactory()
52+
provider = ProviderFactory()
6453
UserAssignmentFactory(user=user, provider=provider)
6554

66-
67-
@pytest.mark.django_db
68-
def test_user_assignment_related_names():
69-
user = UserFactory()
70-
provider = ProviderFactory()
71-
assignment = UserAssignmentFactory(user=user, provider=provider)
72-
73-
assert assignment in user.assignments.all()
74-
assert assignment in provider.assignments.all()
55+
with pytest.raises(Exception):
56+
UserAssignmentFactory(user=user, provider=provider)
57+
58+
@pytest.mark.django_db
59+
def test_related_names(self):
60+
user = UserFactory()
61+
provider = ProviderFactory()
62+
assignment = UserAssignmentFactory(user=user, provider=provider)
63+
64+
assert assignment in user.assignments.all()
65+
assert assignment in provider.assignments.all()
66+
67+
@pytest.mark.django_db
68+
def test_roles_ordering(self):
69+
"""Test that roles are sorted alphabetically for consistent ordering."""
70+
user = UserFactory()
71+
provider = ProviderFactory()
72+
73+
# Create assignment with roles in reverse alphabetical order
74+
assignment = UserAssignmentFactory(
75+
user=user,
76+
provider=provider,
77+
roles=[Role.CLINICAL.value, Role.ADMINISTRATIVE.value],
78+
)
79+
80+
assert assignment.roles == [Role.ADMINISTRATIVE.value, Role.CLINICAL.value]
81+
82+
@pytest.mark.django_db
83+
def test_roles_single_role(self):
84+
"""Test that single role works correctly."""
85+
user = UserFactory()
86+
provider = ProviderFactory()
87+
88+
assignment = UserAssignmentFactory(
89+
user=user, provider=provider, roles=[Role.CLINICAL.value]
90+
)
91+
92+
assert assignment.roles == [Role.CLINICAL.value]
93+
94+
@pytest.mark.django_db
95+
def test_roles_duplicate_values(self):
96+
"""Test that duplicate roles are deduplicated and sorted on save."""
97+
user = UserFactory()
98+
provider = ProviderFactory()
99+
100+
assignment = UserAssignmentFactory(
101+
user=user,
102+
provider=provider,
103+
roles=[Role.CLINICAL.value, Role.CLINICAL.value, Role.ADMINISTRATIVE.value],
104+
)
105+
106+
# Should be sorted and deduplicated
107+
assert assignment.roles == [
108+
Role.ADMINISTRATIVE.value,
109+
Role.CLINICAL.value,
110+
]

0 commit comments

Comments
 (0)