diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8737534702..5bdc93da28 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,10 @@ Change Log Unreleased ---------- +[4.12.1] +--------- +* feat: unlink canvas user if not decommissioned on canvas side + [4.12.0] --------- * feat: Remove history tables for integrated channels customers configurations. diff --git a/enterprise/__init__.py b/enterprise/__init__.py index da4f333ca2..2b34b661db 100644 --- a/enterprise/__init__.py +++ b/enterprise/__init__.py @@ -2,4 +2,4 @@ Your project description goes here. """ -__version__ = "4.12.0" +__version__ = "4.12.1" diff --git a/integrated_channels/canvas/client.py b/integrated_channels/canvas/client.py index 8911ccf6ad..fe36683875 100644 --- a/integrated_channels/canvas/client.py +++ b/integrated_channels/canvas/client.py @@ -12,6 +12,7 @@ from django.apps import apps +from enterprise.models import EnterpriseCustomerUser from integrated_channels.canvas.utils import CanvasUtil # pylint: disable=cyclic-import from integrated_channels.exceptions import ClientError from integrated_channels.integrated_channel.client import IntegratedChannelApiClient, IntegratedChannelHealthStatus @@ -654,7 +655,7 @@ def _extract_integration_id(self, data): return integration_id - def _search_for_canvas_user_by_email(self, user_email): + def _search_for_canvas_user_by_email(self, user_email): # pylint: disable=inconsistent-return-statements """ Helper method to make an api call to Canvas using the user's email as a search term. @@ -687,13 +688,34 @@ def _search_for_canvas_user_by_email(self, user_email): get_users_by_email_response = rsps.json() try: - canvas_user_id = get_users_by_email_response[0]['id'] + canvas_user_id = get_users_by_email_response[0]["id"] + return canvas_user_id except (KeyError, IndexError) as error: - raise ClientError( - "No Canvas user ID found associated with email: {}".format(user_email), - HTTPStatus.NOT_FOUND.value - ) from error - return canvas_user_id + # learner is decommissioned on Canvas side - unlink it from enterprise + try: + enterprise_customer = self.enterprise_configuration.enterprise_customer + # Unlink user from related Enterprise Customer + EnterpriseCustomerUser.objects.unlink_user( + enterprise_customer=enterprise_customer, + user_email=user_email, + ) + raise ClientError( + "No Canvas user ID found associated with email: {} - User unlinked from enterprise now".format( + user_email + ), + HTTPStatus.NOT_FOUND.value, + ) from error + except Exception as e: # pylint: disable=broad-except + LOGGER.error( + generate_formatted_log( + self.enterprise_configuration.channel_code(), + self.enterprise_configuration.enterprise_customer.uuid, + None, + None, + f"Error occurred while unlinking a Canvas learner: {user_email}. " + f"Error: {e}", + ) + ) def _get_canvas_user_courses_by_id(self, user_id): """Helper method to retrieve all courses that a Canvas user is enrolled in.""" diff --git a/integrated_channels/integrated_channel/admin/__init__.py b/integrated_channels/integrated_channel/admin/__init__.py index 41042bc142..5103a8198c 100644 --- a/integrated_channels/integrated_channel/admin/__init__.py +++ b/integrated_channels/integrated_channel/admin/__init__.py @@ -128,5 +128,7 @@ class IntegratedChannelAPIRequestLogAdmin(admin.ModelAdmin): "payload", ] + list_per_page = 100 + class Meta: model = IntegratedChannelAPIRequestLogs diff --git a/tests/test_integrated_channels/test_canvas/test_client.py b/tests/test_integrated_channels/test_canvas/test_client.py index 278206286d..45ad649df3 100644 --- a/tests/test_integrated_channels/test_canvas/test_client.py +++ b/tests/test_integrated_channels/test_canvas/test_client.py @@ -16,6 +16,7 @@ from django.apps import apps +from enterprise.models import EnterpriseCustomerUser from integrated_channels.canvas.client import MESSAGE_WHEN_COURSE_WAS_DELETED, CanvasAPIClient from integrated_channels.canvas.utils import CanvasUtil from integrated_channels.exceptions import ClientError @@ -161,39 +162,27 @@ def test_expires_at_is_updated_after_session_expiry(self): canvas_api_client._create_session() # pylint: disable=protected-access assert canvas_api_client.expires_at > orig_expires_at + @responses.activate def test_search_for_canvas_user_with_400(self): """ - Test that we properly raise exceptions if the client can't find the edx user in Canvas while reporting - grades (assessment and course level reporting both use the same method of retrieval). + Test that we properly raise exception and unlink user if the client can't find the edx user in Canvas + while reporting grades (assessment and course level reporting both use the same method of retrieval). """ - with responses.RequestsMock() as rsps: - rsps.add( - responses.GET, - self.canvas_users_url, - body="[]", - status=200 - ) - canvas_api_client = CanvasAPIClient(self.enterprise_config) - - # Searching for canvas users will require the session to be created - rsps.add( - responses.POST, - self.oauth_url, - json=self._token_response(), - status=200 - ) - canvas_api_client._create_session() # pylint: disable=protected-access - - with pytest.raises(ClientError) as client_error: - canvas_api_client._search_for_canvas_user_by_email(self.canvas_email) # pylint: disable=protected-access - assert IntegratedChannelAPIRequestLogs.objects.count() == 2 - assert client_error.value.message == \ - "Course: {course_id} not found registered in Canvas for Edx " \ - "learner: {canvas_email}/Canvas learner: {canvas_user_id}.".format( - course_id=self.course_id, - canvas_email=self.canvas_email, - canvas_user_id=self.canvas_user_id - ) + responses.add( + responses.POST, self.oauth_url, json=self._token_response(), status=200 + ) + responses.add(responses.GET, self.canvas_users_url, json=[], status=200) + canvas_api_client = CanvasAPIClient(self.enterprise_config) + canvas_api_client._create_session() # pylint: disable=protected-access + assert responses.calls[0].request.url == self.oauth_url + + with mock.patch.object( + EnterpriseCustomerUser.objects, "unlink_user" + ) as unlink_user_mock: + canvas_api_client._search_for_canvas_user_by_email(self.canvas_email) # pylint: disable=protected-access + unlink_user_mock.assert_called_once() + assert len(responses.calls) == 2 + assert IntegratedChannelAPIRequestLogs.objects.count() == 2 def test_assessment_reporting_with_no_canvas_course_found(self): """