feat: add assignments array to GET /api/authz/v1/users/ endpoint - #451
carlos-marquez-wgu wants to merge 2 commits into
Conversation
|
Thanks for the pull request, @carlos-marquez-wgu! This repository is currently maintained by Once you've gone through the following steps feel free to tag them in a comment and let them know that your changes are ready for engineering review. 🔘 Get product approvalIf you haven't already, check this list to see if your contribution needs to go through the product review process.
🔘 Provide contextTo help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:
🔘 Get a green buildIf one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green. DetailsWhere can I find more information?If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources: When can I expect my changes to be merged?Our goal is to get community contributions seen and reviewed as efficiently as possible. However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:
💡 As a result it may take up to several weeks or months to complete a review and merge your PR. |
Extend TeamMembersAPIView to return a nested assignments array per user, implementing ADR 0024 (API Contract for User-Grouped Role Assignments). Changes: - Add assignments_limit query parameter (default 3, max 10) to control the number of inline assignments per user - Add assignments array to the response with role, org, scope, scope_display_name, and permission_count per entry - Resolve scope_display_name from ContentLibrary.learning_package.title and CourseOverview.display_name via batched lookups - Rename assignation_count to assignment_count for consistency with the rest of the codebase - Add roles query parameter passthrough to the underlying API call - Add get_scope_display_name_map batch helper to api/utils.py - Add TeamMemberAssignmentInlineSerializer extending TeamMemberAssignmentSerializer with scope_display_name and without is_superadmin - Add tests for response shape, assignments_limit behavior, scope_display_name resolution (libraries, courses, missing resources, glob scopes)
dda0b39 to
594b992
Compare
|
Hi @openedx/committers-openedx-authz, this PR is ready for review |
rodmgwgu
left a comment
There was a problem hiding this comment.
Looking good overall, added some comments and things that should be reviewed. Thanks!
| ASSIGNMENTS_LIMIT_DEFAULT = 3 | ||
| ASSIGNMENTS_LIMIT_MAX = 10 | ||
|
|
||
| roles = LowercaseCharField(required=False, default=[]) |
There was a problem hiding this comment.
I think this should be a CommaSeparatedListField or similar field, otherwise this will return a string, not a list.
|
|
||
| * Add assignments array to the response of GET /api/authz/v1/users/ endpoint. | ||
| * Add `assignments_limit` query parameter (default 3, max 10) to control the number of inline assignments per user. | ||
| * Rename `assignation_count`` to `assignment_count` for consistency with the rest of the codebase. |
There was a problem hiding this comment.
nit: extra backtick in `assignation_count``
There was a problem hiding this comment.
In RST, we should use two characters (``) so it formats correctly.
| team_members = TeamMemberSerializer(users_with_assignments, many=True).data | ||
| # Collect all assignments across users for a single batch query of display names. | ||
| all_assignments = [a for uwa in users_with_assignments for a in uwa.assignments] | ||
| scope_display_name_map = get_scope_display_name_map(all_assignments) |
There was a problem hiding this comment.
This will bring the display name for all assignments for a user. For performance considerations, could we instead get the names after the pagination is done? that way we only fetch the ones we actually need
There was a problem hiding this comment.
Thanks the suggestion, I addressed this
BryanttV
left a comment
There was a problem hiding this comment.
Thanks! I tested this on my local with some test data, and it works great. Just one comment from my side.
| @@ -321,7 +330,8 @@ class TeamMemberSerializer(serializers.Serializer): # pylint: disable=abstract- | |||
| username = serializers.SerializerMethodField() | |||
| full_name = serializers.SerializerMethodField() | |||
There was a problem hiding this comment.
This is not part of the implementation, but I noticed it while testing.
Currently, we use the get_full_name() function to get the full name, but we should actually retrieve this information from the UserProfile. It is being done correctly in the UserRoleAssignmentSerializer, can we do the same for the other serializers?
There was a problem hiding this comment.
Thanks for the suggestion, @BryanttV
I'll implement it for TeamMemberSerializer.get_full_name, I see another instance that retrieves the full name happens at TeamMemberUserAssignmentSerializer.get_full_name, Would you like I update that one as well?
There was a problem hiding this comment.
Yes, please update that as well.
| def test_scope_display_name_graceful_on_course_db_error(self, mock_course_overview): | ||
| """When the CourseOverview query raises, scope_display_name falls back to empty string. | ||
|
|
||
| Setup: | ||
| Assign regular_1 a course role so there's a CourseOverviewData scope. | ||
|
|
||
| Expected result: | ||
| - The endpoint still returns 200 OK. | ||
| - scope_display_name is "" for the affected course scopes. | ||
| """ | ||
| mock_course_overview.objects.filter.side_effect = Exception("DB error") | ||
| course_scope = "course-v1:Org1+CS101+2024" | ||
|
|
||
| response = self.client.get(self.url, {"search": "regular_1"}) | ||
|
|
||
| self.assertEqual(response.status_code, status.HTTP_200_OK) | ||
| results = [r for r in response.data["results"] if r["username"] == "regular_1"] | ||
| self.assertEqual(len(results), 1) | ||
| course_assignments = [a for a in results[0]["assignments"] if a["scope"] == course_scope] | ||
| for assignment in course_assignments: | ||
| self.assertEqual(assignment["scope_display_name"], "") |
There was a problem hiding this comment.
The test doesn't assign any roles, so course_assignments is left empty and the for loop never runs. Can we update it?
There was a problem hiding this comment.
Thanks for catching this up, I updated it and added a little guard against empty assignments just in case
|
|
||
| * Add assignments array to the response of GET /api/authz/v1/users/ endpoint. | ||
| * Add `assignments_limit` query parameter (default 3, max 10) to control the number of inline assignments per user. | ||
| * Rename `assignation_count`` to `assignment_count` for consistency with the rest of the codebase. |
There was a problem hiding this comment.
In RST, we should use two characters (``) so it formats correctly.
254a6ed to
45eeab0
Compare
- Change roles field to CommaSeparatedListField in ListTeamMembersSerializer - Use UserProfile.name instead of get_full_name() in TeamMemberSerializer and TeamMemberUserAssignmentSerializer for consistency - Resolve scope display names post-pagination to avoid unnecessary DB lookups - Fix RST formatting in CHANGELOG.rst (double backticks for inline code) - Add role assignment and assertGreater guard to course DB error test
45eeab0 to
734131a
Compare
Description
Extend TeamMembersAPIView to return a nested assignments array per user, implementing ADR 0024 (API Contract for User-Grouped Role Assignments).
Changes
Manual testing
Examples
GET /api/authz/v1/users/
Request
Response body
{ "count": 6, "next": null, "previous": null, "results": [ { "username": "carlos.marquez", "full_name": "", "email": "carlos.marquez@wgu.edu", "assignment_count": 8, "assignments": [ { "role": "library_admin", "org": "test", "scope": "lib:test:LIB101", "permission_count": 11, "scope_display_name": "Carlos Testing Library" }, { "role": "course_admin", "org": "test", "scope": "course-v1:test+CARLOS102+2026_2", "permission_count": 33, "scope_display_name": "Carlos Testing Course 2" }, { "role": "course_staff", "org": "test", "scope": "course-v1:test+CARLOS102+2026_2", "permission_count": 31, "scope_display_name": "Carlos Testing Course 2" } ] }, { "username": "test.user.course_editor", "full_name": "", "email": "course_editor@example.com", "assignment_count": 1, "assignments": [ { "role": "course_editor", "org": "CORG", "scope": "course-v1:CORG+CARLOS301+2026_2", "permission_count": 22, "scope_display_name": "Carlos Testing Course 3" } ] }, { "username": "test.user.courses.manage_library_updates", "full_name": "", "email": "courses.manage_library_updates@example.com", "assignment_count": 2, "assignments": [ { "role": "course_staff", "org": "test", "scope": "course-v1:test+CARLOS101+2026_2", "permission_count": 31, "scope_display_name": "Carlos Testing Course" }, { "role": "course_staff", "org": "CORG", "scope": "course-v1:CORG+CARLOS301+2026_2", "permission_count": 31, "scope_display_name": "Carlos Testing Course 3" } ] }, { "username": "test.user.many_assignments", "full_name": "", "email": "many_assignments@example.com", "assignment_count": 14, "assignments": [ { "role": "library_author", "org": "*", "scope": "lib:*", "permission_count": 9, "scope_display_name": "" }, { "role": "library_author", "org": "CORG", "scope": "lib:CORG:*", "permission_count": 9, "scope_display_name": "" }, { "role": "library_author", "org": "EORG", "scope": "lib:EORG:*", "permission_count": 9, "scope_display_name": "" } ] }, { "username": "test.user.master_editor", "full_name": "", "email": "master_editor@example.com", "assignment_count": 6, "assignments": [ { "role": "library_admin", "org": "*", "scope": "lib:*", "permission_count": 11, "scope_display_name": "" }, { "role": "library_admin", "org": "CORG", "scope": "lib:CORG:*", "permission_count": 11, "scope_display_name": "" }, { "role": "library_admin", "org": "EORG", "scope": "lib:EORG:*", "permission_count": 11, "scope_display_name": "" } ] }, { "username": "test.user.regular_editor", "full_name": "", "email": "regular_editor@example.com", "assignment_count": 1, "assignments": [ { "role": "library_user", "org": "test", "scope": "lib:test:LIB101", "permission_count": 3, "scope_display_name": "Carlos Testing Library" } ] } ] }GET /api/authz/v1/users/?search=test.user.many_assignments&assignments_limit=50
Request
Response body
{ "count": 1, "next": null, "previous": null, "results": [ { "username": "test.user.many_assignments", "full_name": "", "email": "many_assignments@example.com", "assignment_count": 14, "assignments": [ { "role": "library_author", "org": "*", "scope": "lib:*", "permission_count": 9, "scope_display_name": "" }, { "role": "library_author", "org": "CORG", "scope": "lib:CORG:*", "permission_count": 9, "scope_display_name": "" }, { "role": "library_author", "org": "EORG", "scope": "lib:EORG:*", "permission_count": 9, "scope_display_name": "" }, { "role": "library_author", "org": "REM", "scope": "lib:REM:*", "permission_count": 9, "scope_display_name": "" }, { "role": "library_author", "org": "test", "scope": "lib:test:*", "permission_count": 9, "scope_display_name": "" }, { "role": "library_author", "org": "test", "scope": "lib:test:LIB101", "permission_count": 9, "scope_display_name": "Carlos Testing Library" }, { "role": "course_staff", "org": "*", "scope": "course-v1:*", "permission_count": 31, "scope_display_name": "" }, { "role": "course_staff", "org": "CORG", "scope": "course-v1:CORG+*", "permission_count": 31, "scope_display_name": "" }, { "role": "course_staff", "org": "CORG", "scope": "course-v1:CORG+CARLOS301+2026_2", "permission_count": 31, "scope_display_name": "Carlos Testing Course 3" }, { "role": "course_staff", "org": "EORG", "scope": "course-v1:EORG+*", "permission_count": 31, "scope_display_name": "" } ] } ] }GET /api/authz/v1/users/?search=test.user&assignments_limit=2&roles=library_author&orgs=CORG,REM&sort_by=email&order=desc
Request
Response body
{ "count": 1, "next": null, "previous": null, "results": [ { "username": "test.user.many_assignments", "full_name": "", "email": "many_assignments@example.com", "assignment_count": 2, "assignments": [ { "role": "library_author", "org": "CORG", "scope": "lib:CORG:*", "permission_count": 9, "scope_display_name": "" }, { "role": "library_author", "org": "REM", "scope": "lib:REM:*", "permission_count": 9, "scope_display_name": "" } ] } ] }Merge checklist:
AI Usage
Kiro + Claude were used to assist with the creation of the tests and code implementations while throughly and carefully guided, everything was reviewed and corrected manually by an actual person and ensured the changes were up to standard and met the closing issue requirements.
Closes #406