Skip to content

Commit c873026

Browse files
committed
squash!: Filter by active users
1 parent c516fae commit c873026

4 files changed

Lines changed: 97 additions & 45 deletions

File tree

openedx_authz/api/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def get_user_map(usernames: list[str]) -> dict[str, User]:
2727
dict[str, User]: Dictionary mapping each username to its corresponding User object.
2828
Only users that exist in the database are included in the returned dictionary.
2929
"""
30-
users = User.objects.filter(username__in=usernames).select_related("profile")
30+
users = User.objects.filter(username__in=usernames, is_active=True).select_related("profile")
3131
return {user.username: user for user in users}
3232

3333

openedx_authz/tests/rest_api/test_views.py

Lines changed: 38 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,6 @@
2626
User = get_user_model()
2727

2828

29-
def get_user_map_without_profile(usernames: list[str]) -> dict[str, User]:
30-
"""
31-
Test version of ``get_user_map`` that doesn't use select_related('profile').
32-
33-
The generic Django User model doesn't have a profile relation,
34-
so we override this in tests to avoid FieldError.
35-
"""
36-
users = User.objects.filter(username__in=usernames)
37-
return {user.username: user for user in users}
38-
39-
4029
class ViewTestMixin(BaseRolesTestCase):
4130
"""Mixin providing common test utilities for view tests."""
4231

@@ -322,11 +311,6 @@ def setUp(self):
322311
super().setUp()
323312
self.client.force_authenticate(user=self.admin_user)
324313
self.url = reverse("openedx_authz:role-user-list")
325-
self.get_user_map_patcher = patch(
326-
"openedx_authz.rest_api.v1.views.get_user_map",
327-
side_effect=get_user_map_without_profile,
328-
)
329-
self.get_user_map_patcher.start()
330314

331315
@data(
332316
# All users
@@ -1076,12 +1060,6 @@ def setUp(self):
10761060
"""Set up test fixtures."""
10771061
super().setUp()
10781062
self.url = reverse("openedx_authz:user-list")
1079-
self.get_user_map_patcher = patch(
1080-
"openedx_authz.api.utils.get_user_map",
1081-
side_effect=get_user_map_without_profile,
1082-
)
1083-
self.get_user_map_patcher.start()
1084-
self.addCleanup(self.get_user_map_patcher.stop)
10851063

10861064
# -------------------------------------------------------------------- #
10871065
# Visibility: calling user only sees assignments it has view access to #
@@ -1340,16 +1318,6 @@ class TestTeamMemberAssignmentsAPIView(ViewTestMixin):
13401318
(requires at least one VIEW_LIBRARY_TEAM or COURSES_VIEW_COURSE_TEAM permission).
13411319
"""
13421320

1343-
def setUp(self):
1344-
"""Set up test fixtures."""
1345-
super().setUp()
1346-
self.get_user_map_patcher = patch(
1347-
"openedx_authz.api.utils.get_user_map",
1348-
side_effect=get_user_map_without_profile,
1349-
)
1350-
self.get_user_map_patcher.start()
1351-
self.addCleanup(self.get_user_map_patcher.stop)
1352-
13531321
def _url(self, username: str) -> str:
13541322
return reverse("openedx_authz:user-assignment-list", kwargs={"username": username})
13551323

@@ -2102,12 +2070,6 @@ def setUp(self):
21022070
"""Set up test fixtures."""
21032071
super().setUp()
21042072
self.url = reverse("openedx_authz:assignment-list")
2105-
self.get_user_map_patcher = patch(
2106-
"openedx_authz.api.utils.get_user_map",
2107-
side_effect=get_user_map_without_profile,
2108-
)
2109-
self.get_user_map_patcher.start()
2110-
self.addCleanup(self.get_user_map_patcher.stop)
21112073

21122074
# -------------------------------------------------------------------- #
21132075
# Visibility: calling user only sees assignments it has view access to #
@@ -2585,6 +2547,44 @@ def test_combined_scope_and_search(self):
25852547
self.assertEqual(response.status_code, status.HTTP_200_OK)
25862548
self.assertEqual(response.data["count"], 2)
25872549

2550+
# ------------------------------------------------------------------ #
2551+
# Active user filtering #
2552+
# ------------------------------------------------------------------ #
2553+
2554+
def test_inactive_users_excluded_from_results(self):
2555+
"""Role assignments for inactive users are not included in results.
2556+
2557+
Deactivating a user (is_active=False) should remove their role assignments
2558+
from the response, even though the assignments still exist in the database.
2559+
Superadmin entries are also excluded for inactive staff/superusers.
2560+
2561+
Expected result:
2562+
- Returns 200 OK.
2563+
- The inactive user's assignments do not appear in the results.
2564+
- The total count decreases by the number of assignments the inactive user had.
2565+
"""
2566+
# Baseline: admin_1 (staff) sees all 14 rows (3 superadmin + 11 role assignments)
2567+
baseline_response = self.client.get(self.url)
2568+
self.assertEqual(baseline_response.status_code, status.HTTP_200_OK)
2569+
baseline_count = baseline_response.data["count"]
2570+
2571+
# Deactivate regular_1, who has 1 role assignment in lib:Org1:LIB1
2572+
inactive_user = User.objects.get(username="regular_1")
2573+
inactive_user.is_active = False
2574+
inactive_user.save()
2575+
try:
2576+
response = self.client.get(self.url)
2577+
2578+
self.assertEqual(response.status_code, status.HTTP_200_OK)
2579+
# regular_1 had 1 role assignment → count should drop by 1
2580+
self.assertEqual(response.data["count"], baseline_count - 1)
2581+
# Confirm regular_1 is not in the results
2582+
usernames = {item["username"] for item in response.data["results"]}
2583+
self.assertNotIn("regular_1", usernames)
2584+
finally:
2585+
inactive_user.is_active = True
2586+
inactive_user.save()
2587+
25882588

25892589
@ddt
25902590
class TestAssignmentsAPIViewPermissions(ViewTestMixin):
@@ -2638,12 +2638,6 @@ def setUp(self):
26382638
"""Set up test fixtures."""
26392639
super().setUp()
26402640
self.url = reverse("openedx_authz:assignment-list")
2641-
self.get_user_map_patcher = patch(
2642-
"openedx_authz.api.utils.get_user_map",
2643-
side_effect=get_user_map_without_profile,
2644-
)
2645-
self.get_user_map_patcher.start()
2646-
self.addCleanup(self.get_user_map_patcher.stop)
26472641

26482642
# ------------------------------------------------------------------ #
26492643
# Superadmin caller #
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""Migration to add the UserProfile stub model."""
2+
3+
import django.db.models.deletion
4+
from django.conf import settings
5+
from django.db import migrations, models
6+
7+
8+
class Migration(migrations.Migration):
9+
dependencies = [
10+
("stubs", "0001_initial"),
11+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12+
]
13+
14+
operations = [
15+
migrations.CreateModel(
16+
name="UserProfile",
17+
fields=[
18+
(
19+
"id",
20+
models.BigAutoField(
21+
auto_created=True,
22+
primary_key=True,
23+
serialize=False,
24+
verbose_name="ID",
25+
),
26+
),
27+
("name", models.CharField(blank=True, default="", max_length=255)),
28+
(
29+
"user",
30+
models.OneToOneField(
31+
on_delete=django.db.models.deletion.CASCADE,
32+
related_name="profile",
33+
to=settings.AUTH_USER_MODEL,
34+
),
35+
),
36+
],
37+
),
38+
]

openedx_authz/tests/stubs/models.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,26 @@
1313
from organizations.models import Organization
1414

1515

16+
class UserProfile(models.Model):
17+
"""Stub model mimicking the Open edX UserProfile for testing purposes.
18+
19+
Provides the ``profile`` reverse relation that ``select_related('profile')``
20+
expects on the User model, along with the ``name`` field used by serializers.
21+
22+
.. no_pii:
23+
"""
24+
25+
user = models.OneToOneField(
26+
settings.AUTH_USER_MODEL,
27+
on_delete=models.CASCADE,
28+
related_name="profile",
29+
)
30+
name = models.CharField(max_length=255, blank=True, default="")
31+
32+
def __str__(self):
33+
return f"UserProfile({self.user.username})"
34+
35+
1636
class ContentLibraryManager(models.Manager):
1737
"""Manager for ContentLibrary model with helper methods."""
1838

0 commit comments

Comments
 (0)