-
Notifications
You must be signed in to change notification settings - Fork 6
CDD-3481: Export Users to CSV #3310
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
itsthatianguy
wants to merge
10
commits into
main
Choose a base branch
from
feature/cdd-3481-export-users-csv
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
31cb4c4
Exporting users to csvfile
itsthatianguy 10529ab
Audit logging and timestamp filename
itsthatianguy a51d286
Formatting and empty column fix
itsthatianguy 722485a
Fixed dependencies
itsthatianguy 4e4df20
Merge branch 'main' into feature/cdd-3481-export-users-csv
itsthatianguy e1a1b30
Sonarqube fix
itsthatianguy c92024d
Tidy up
itsthatianguy 90e7ff9
Merge branch 'main' into feature/cdd-3481-export-users-csv
itsthatianguy a34c0a4
Merge branch 'main' into feature/cdd-3481-export-users-csv
sahmed06 7e42209
Merge branch 'main' into feature/cdd-3481-export-users-csv
itsthatianguy File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| from cms.auth_content.exporters.user_exporter import ( | ||
| generate_user_permission_sets_csv_rows, | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import csv | ||
|
|
||
|
|
||
| class Echo: | ||
| @staticmethod | ||
| def write(value): | ||
| return value | ||
|
|
||
|
|
||
| def generate_user_permission_sets_csv_rows(users): | ||
| writer = csv.writer(Echo()) | ||
| yield writer.writerow( | ||
| ["User ID", "Permission Set", "Gives Global Access", "Permissions"] | ||
| ) | ||
| for user in users: | ||
| permission_sets = user.permission_sets.all() | ||
| if permission_sets: | ||
| for ps in permission_sets: | ||
| yield writer.writerow( | ||
| [user.user_id, ps.display_name, ps.global_access, ps.name] | ||
| ) | ||
| else: | ||
| yield writer.writerow([user.user_id, "", "", ""]) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| import logging | ||
| from datetime import datetime | ||
|
|
||
| from django.http import StreamingHttpResponse | ||
| from django.views.decorators.http import require_http_methods | ||
|
|
||
| from cms.auth_content.exporters import generate_user_permission_sets_csv_rows | ||
| from cms.auth_content.models.users import User | ||
|
|
||
| audit_logger = logging.getLogger("audit") | ||
|
|
||
|
|
||
| @require_http_methods(["GET"]) | ||
| def export_user_permission_sets_csv(request): | ||
| user_id = ( | ||
| request.user.id | ||
| if request.user and request.user.is_authenticated | ||
| else "anonymous" | ||
| ) | ||
| audit_logger.info( | ||
| "User permission sets exported to CSV", | ||
| extra={ | ||
| "user": user_id, | ||
| "action": "CSV EXPORT", | ||
| "target": "Users and permissions", | ||
| }, | ||
| ) | ||
|
|
||
| users = User.objects.with_permission_sets() | ||
| response = StreamingHttpResponse( | ||
| generate_user_permission_sets_csv_rows(users), | ||
| content_type="text/csv", | ||
| ) | ||
| timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") | ||
| response["Content-Disposition"] = ( | ||
| f'attachment; filename="dashboard_cms_users_{timestamp}.csv"' | ||
| ) | ||
| return response |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
Empty file.
72 changes: 72 additions & 0 deletions
72
tests/integration/cms/auth_content/exporters/test_user_exporter.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| import uuid | ||
| from django.test import TestCase | ||
|
|
||
| from cms.auth_content.models.users import User | ||
| from cms.auth_content.models.permission_sets import PermissionSet | ||
| from cms.auth_content.exporters.user_exporter import ( | ||
| generate_user_permission_sets_csv_rows, | ||
| ) | ||
|
|
||
|
|
||
| class TestUserCsvExporter(TestCase): | ||
| def test_user_with_no_permission_sets_exports_as_blank_row(self): | ||
| user = User.objects.create(user_id=uuid.uuid4()) | ||
|
|
||
| rows = list(generate_user_permission_sets_csv_rows(User.objects.all())) | ||
|
|
||
| self.assertEqual(len(rows), 2) | ||
| self.assertEqual( | ||
| rows[0], "User ID,Permission Set,Gives Global Access,Permissions\r\n" | ||
| ) | ||
| self.assertEqual(rows[1], f"{str(user.user_id)},,,\r\n") | ||
|
|
||
| def test_export_with_multiple_users_and_permissions(self): | ||
| # Results are sorted by user id, so hardcoding easier to determine uuids to ensure test order | ||
| user_without_permissions = User.objects.create( | ||
| user_id="11111111-1111-1111-1111-111111111111" | ||
| ) | ||
| user_with_one_permission_set = User.objects.create( | ||
| user_id="22222222-2222-2222-2222-222222222222" | ||
| ) | ||
| user_with_two_permission_sets = User.objects.create( | ||
| user_id="33333333-3333-3333-3333-333333333333" | ||
| ) | ||
| global_access_permission_set = PermissionSet.objects.create( | ||
| display_name="Global", | ||
| theme="-1", | ||
| sub_theme="-1", | ||
| topic="-1", | ||
| metric="-1", | ||
| geography_type="-1", | ||
| ) | ||
| limited_access_permission_set = PermissionSet.objects.create( | ||
| display_name="Limited", | ||
| theme="11", | ||
| sub_theme="-1", | ||
| topic="-1", | ||
| metric="-1", | ||
| geography_type="-1", | ||
| ) | ||
| user_with_one_permission_set.permission_sets.add(limited_access_permission_set) | ||
| user_with_two_permission_sets.permission_sets.add( | ||
| global_access_permission_set, limited_access_permission_set | ||
| ) | ||
| rows = list(generate_user_permission_sets_csv_rows(User.objects.all())) | ||
|
|
||
| self.assertEqual(len(rows), 5) | ||
| self.assertEqual( | ||
| rows[0], "User ID,Permission Set,Gives Global Access,Permissions\r\n" | ||
| ) | ||
| self.assertEqual(rows[1], f"{str(user_without_permissions.user_id)},,,\r\n") | ||
| self.assertEqual( | ||
| rows[2], | ||
| f"{str(user_with_one_permission_set.user_id)},Limited,False,Theme: 11 | Sub-theme: * (All) | Topic: * (All) | Metric: * (All) | Geography Type: * (All)\r\n", | ||
| ) | ||
| self.assertEqual( | ||
| rows[3], | ||
| f"{str(user_with_two_permission_sets.user_id)},Global,True,Theme: * (All) | Sub-theme: * (All) | Topic: * (All) | Metric: * (All) | Geography Type: * (All)\r\n", | ||
| ) | ||
| self.assertEqual( | ||
| rows[4], | ||
| f"{str(user_with_two_permission_sets.user_id)},Limited,False,Theme: 11 | Sub-theme: * (All) | Topic: * (All) | Metric: * (All) | Geography Type: * (All)\r\n", | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| import freezegun | ||
| from unittest import mock | ||
| import uuid | ||
| from django.test import TestCase | ||
| from django.urls import reverse | ||
|
|
||
| from cms.auth_content.models.users import User | ||
|
|
||
|
|
||
| class TestExportUserPermissionsView(TestCase): | ||
| def setUp(self): | ||
| self.mock_logger = mock.patch("cms.auth_content.views.audit_logger").start() | ||
| self.addCleanup(mock.patch.stopall) | ||
| self.superuser = self._create_superuser() | ||
| self.client.force_login(self.superuser) | ||
|
|
||
| @staticmethod | ||
| def _create_superuser(): | ||
| from django.contrib.auth import get_user_model | ||
|
|
||
| AdminUser = get_user_model() | ||
| return AdminUser.objects.create_superuser( | ||
| username="admin", email="admin@example.com", password="password" | ||
| ) | ||
|
|
||
| def test_export_requires_login(self): | ||
| self.client.logout() | ||
| url = reverse("export_user_permission_sets_csv") | ||
|
|
||
| response = self.client.get(url) | ||
|
|
||
| self.assertEqual(response.status_code, 302) | ||
|
|
||
| @freezegun.freeze_time("2026-08-17 12:00:00") | ||
| def test_export_user_permissions_view_response(self): | ||
| User.objects.create(user_id=uuid.uuid4()) | ||
| url = reverse("export_user_permission_sets_csv") | ||
|
|
||
| response = self.client.get(url) | ||
|
|
||
| self.assertEqual(response.status_code, 200) | ||
| self.assertEqual(response["Content-Type"], "text/csv") | ||
| self.assertIn( | ||
| 'attachment; filename="dashboard_cms_users_20260817-120000.csv"', | ||
| response["Content-Disposition"], | ||
| ) | ||
|
|
||
| def test_exporting_users_creates_audit_log(self): | ||
| User.objects.create(user_id=uuid.uuid4()) | ||
| url = reverse("export_user_permission_sets_csv") | ||
|
|
||
| response = self.client.get(url) | ||
|
|
||
| self.assertEqual(response.status_code, 200) | ||
| self.mock_logger.info.assert_called_once() | ||
| _, kwargs = self.mock_logger.info.call_args | ||
| self.assertEqual(kwargs["extra"]["user"], self.superuser.id) | ||
| self.assertEqual(kwargs["extra"]["action"], "CSV EXPORT") | ||
| self.assertEqual(kwargs["extra"]["target"], "Users and permissions") |
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| from cms.auth_content.models.permission_sets import PermissionSet | ||
|
|
||
|
|
||
| class TestPermissionSet: | ||
| def test_global_access_property_with_global_access(self): | ||
| permission_set = PermissionSet() | ||
| permission_set.theme = "-1" | ||
| permission_set.geography_type = "-1" | ||
| assert permission_set.global_access == True | ||
|
|
||
| def test_global_access_property_without_global_access(self): | ||
| permission_set = PermissionSet() | ||
| permission_set.theme = "-1" | ||
| permission_set.geography_type = "1" | ||
| assert permission_set.global_access == False | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just a question - where does the combination of theme and geography_type come from? To determine global access or not |
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potentially add a test for no users to test the CSV headers generation? Or is this suitably covered by checking
every time?