diff --git a/nau_openedx_extensions/apps.py b/nau_openedx_extensions/apps.py index 5506d3c..af54792 100644 --- a/nau_openedx_extensions/apps.py +++ b/nau_openedx_extensions/apps.py @@ -42,7 +42,6 @@ def ready(self): """ Method to perform actions after apps registry is ended """ + from nau_openedx_extensions import signals # pylint: disable=import-outside-toplevel,unused-import # noqa from nau_openedx_extensions.permissions import \ load_permissions # pylint: disable=import-outside-toplevel,unused-import # noqa - - # load_permissions() diff --git a/nau_openedx_extensions/edxapp_wrapper/backends/verify_student_v1.py b/nau_openedx_extensions/edxapp_wrapper/backends/verify_student_v1.py new file mode 100644 index 0000000..9b46d9f --- /dev/null +++ b/nau_openedx_extensions/edxapp_wrapper/backends/verify_student_v1.py @@ -0,0 +1,33 @@ +""" +Real implementation of user id verifications service. +""" +from django.contrib.auth import get_user_model +from lms.djangoapps.verify_student.models import ManualVerification # pylint: disable=import-error + + +def get_user_id_verifications(user_id, *args, **kwargs): + """ + Read the user's `ManualVerification` from the edx-platform. + + Args: + user: The user id to read the Id Verifications. + + Returns: + An enumeration of those Id Verifications + """ + user = get_user_model().objects.get(id=user_id) + return ManualVerification.objects.filter(user=user).order_by('-created_at') + + +def create_user_id_verification(user_id, *args, **kwargs): + """ + Create a new `ManualVerification` on the edx-platform. + + Args: + user: The user id that this Id verification should be created. + + Returns: + The object created + """ + user = get_user_model().objects.get(id=user_id) + ManualVerification(user=user, name=user.profile.name, *args, **kwargs).save() diff --git a/nau_openedx_extensions/edxapp_wrapper/backends/verify_student_v1_tests.py b/nau_openedx_extensions/edxapp_wrapper/backends/verify_student_v1_tests.py new file mode 100644 index 0000000..a786719 --- /dev/null +++ b/nau_openedx_extensions/edxapp_wrapper/backends/verify_student_v1_tests.py @@ -0,0 +1,28 @@ +""" +Real implementation of user id verifications service. +""" + + +def get_user_id_verifications(user_id, *args, **kwargs): # pylint: disable=unused-argument + """ + Read the user's `ManualVerification` from the edx-platform. + + Args: + user_id: The user id to read the Id Verifications. + + Returns: + An enumeration of those Id Verifications + """ + return [] + +def create_user_id_verification(user_id, *args, **kwargs): # pylint: disable=unused-argument + """ + Create a new `ManualVerification` on the edx-platform. + + Args: + user_id: The user id that this Id verification should be created. + + Returns: + The object created + """ + return None diff --git a/nau_openedx_extensions/edxapp_wrapper/verify_student.py b/nau_openedx_extensions/edxapp_wrapper/verify_student.py new file mode 100644 index 0000000..9658d2f --- /dev/null +++ b/nau_openedx_extensions/edxapp_wrapper/verify_student.py @@ -0,0 +1,29 @@ +""" +Student backend abstraction +""" +from __future__ import absolute_import, unicode_literals + +from importlib import import_module + +from django.conf import settings + + +def get_user_id_verifications(user_id, *args, **kwargs): + """ + Read the user's `ManualVerification` from the edx-platform. + """ + + backend_module = settings.NAU_VERIFY_STUDENT_MODULE + backend = import_module(backend_module) + + return backend.get_user_id_verifications(user_id, *args, **kwargs) + + +def create_user_id_verification(user_id, *args, **kwargs): + """ + Create an user Id Verification `ManualVerification` instance on the edx-platform. + """ + backend_module = settings.NAU_VERIFY_STUDENT_MODULE + backend = import_module(backend_module) + + return backend.create_user_id_verification(user_id, *args, **kwargs) diff --git a/nau_openedx_extensions/locale/en/LC_MESSAGES/django.mo b/nau_openedx_extensions/locale/en/LC_MESSAGES/django.mo index 8abd246..46d442a 100644 Binary files a/nau_openedx_extensions/locale/en/LC_MESSAGES/django.mo and b/nau_openedx_extensions/locale/en/LC_MESSAGES/django.mo differ diff --git a/nau_openedx_extensions/locale/en/LC_MESSAGES/django.po b/nau_openedx_extensions/locale/en/LC_MESSAGES/django.po index 6f4e142..007c63a 100644 --- a/nau_openedx_extensions/locale/en/LC_MESSAGES/django.po +++ b/nau_openedx_extensions/locale/en/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: equipa@nau.edu.pt\n" -"POT-Creation-Date: 2024-09-26 11:25+0100\n" +"POT-Creation-Date: 2024-10-02 13:56+0100\n" "PO-Revision-Date: 2021-02-15 15:56+0000\n" "Last-Translator: FULL NAME \n" "Language: en\n" @@ -133,11 +133,11 @@ msgid "" "out in order to obtain a certificate." msgstr "" -#: nau_openedx_extensions/settings/common.py:89 +#: nau_openedx_extensions/settings/common.py:35 msgid "Certificate" msgstr "" -#: nau_openedx_extensions/settings/common.py:90 +#: nau_openedx_extensions/settings/common.py:36 msgid "Certificate of Achievement" msgstr "" diff --git a/nau_openedx_extensions/locale/pt_PT/LC_MESSAGES/django.mo b/nau_openedx_extensions/locale/pt_PT/LC_MESSAGES/django.mo index 4311d06..b2f3646 100644 Binary files a/nau_openedx_extensions/locale/pt_PT/LC_MESSAGES/django.mo and b/nau_openedx_extensions/locale/pt_PT/LC_MESSAGES/django.mo differ diff --git a/nau_openedx_extensions/locale/pt_PT/LC_MESSAGES/django.po b/nau_openedx_extensions/locale/pt_PT/LC_MESSAGES/django.po index 67e2d3d..415ac13 100644 --- a/nau_openedx_extensions/locale/pt_PT/LC_MESSAGES/django.po +++ b/nau_openedx_extensions/locale/pt_PT/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: equipa@nau.edu.pt\n" -"POT-Creation-Date: 2024-09-26 11:25+0100\n" +"POT-Creation-Date: 2024-10-02 13:56+0100\n" "PO-Revision-Date: 2021-02-15 15:56+0000\n" "Last-Translator: Ivo Branco \n" "Language: pt_PT\n" @@ -142,11 +142,11 @@ msgstr "" "Este curso encontra-se arquivado e já não permite a realização de " "atividades para obtenção de certificado." -#: nau_openedx_extensions/settings/common.py:89 +#: nau_openedx_extensions/settings/common.py:35 msgid "Certificate" msgstr "Certificado" -#: nau_openedx_extensions/settings/common.py:90 +#: nau_openedx_extensions/settings/common.py:36 msgid "Certificate of Achievement" msgstr "Certificado de Conclusão" diff --git a/nau_openedx_extensions/settings/common.py b/nau_openedx_extensions/settings/common.py index 7ad3126..36e4995 100644 --- a/nau_openedx_extensions/settings/common.py +++ b/nau_openedx_extensions/settings/common.py @@ -31,6 +31,10 @@ def plugin_settings(settings): See: https://github.com/edx/edx-platform/blob/master/openedx/core/djangoapps/plugins/README.rst """ + # Overwrite the default certificate name + settings.CERT_NAME_SHORT = _("Certificate") + settings.CERT_NAME_LONG = _("Certificate of Achievement") + settings.NAU_CUSTOM_SAML_IDENTITY_PROVIDERS = [ { "provider_key": "nau_custom_saml_provider", @@ -84,7 +88,6 @@ def plugin_settings(settings): settings.NAU_COHORT_MODULE = ( "nau_openedx_extensions.edxapp_wrapper.backends.cohort_v1" ) - - # Overwrite the default certificate name - settings.CERT_NAME_SHORT = _("Certificate") - settings.CERT_NAME_LONG = _("Certificate of Achievement") + settings.NAU_VERIFY_STUDENT_MODULE = ( + "nau_openedx_extensions.edxapp_wrapper.backends.verify_student_v1" + ) diff --git a/nau_openedx_extensions/settings/test.py b/nau_openedx_extensions/settings/test.py index 5be662d..4edd08c 100644 --- a/nau_openedx_extensions/settings/test.py +++ b/nau_openedx_extensions/settings/test.py @@ -50,3 +50,6 @@ class SettingsClass: NAU_COHORT_MODULE = ( "nau_openedx_extensions.edxapp_wrapper.backends.cohort_v1_tests" ) +NAU_VERIFY_STUDENT_MODULE = ( + "nau_openedx_extensions.edxapp_wrapper.backends.verify_student_v1_tests" +) diff --git a/nau_openedx_extensions/signals.py b/nau_openedx_extensions/signals.py new file mode 100644 index 0000000..44e828a --- /dev/null +++ b/nau_openedx_extensions/signals.py @@ -0,0 +1,7 @@ +""" +File that contains the definition of all signals and its receivers. +""" + +from nau_openedx_extensions.verify_student.id_verification import ( # pylint: disable=unused-import + event_receiver_no_id_verify_for_enrollment_modes, +) diff --git a/nau_openedx_extensions/tests/test_verify_student.py b/nau_openedx_extensions/tests/test_verify_student.py new file mode 100644 index 0000000..81f5147 --- /dev/null +++ b/nau_openedx_extensions/tests/test_verify_student.py @@ -0,0 +1,183 @@ +""" +Tests for Id Verification of studentes. +""" + +from datetime import datetime +from unittest.mock import Mock, patch + +from django.test import TestCase, override_settings +from openedx_events.learning.data import CourseEnrollmentData, UserData +from openedx_events.learning.signals import COURSE_ENROLLMENT_CHANGED + +from nau_openedx_extensions.verify_student.id_verification import event_receiver_no_id_verify_for_enrollment_modes + + +class VerifyStudentTest(TestCase): + """ + Tests for Id Verification patch. + """ + + @patch( + "nau_openedx_extensions.verify_student.id_verification.get_user_id_verifications" + ) + @patch( + "nau_openedx_extensions.verify_student.id_verification.create_user_id_verification" + ) + def test_verify_student_create_new_verification( + self, create_user_id_verification_mock, get_user_id_verifications_mock + ): + """ + Test an enrollment mode that requires a ID Verification. + """ + user_id = 10 + get_user_id_verifications_mock.return_value = [] + COURSE_ENROLLMENT_CHANGED.connect(event_receiver_no_id_verify_for_enrollment_modes) + COURSE_ENROLLMENT_CHANGED.send_event( + enrollment=CourseEnrollmentData( + user=UserData(id=user_id, is_active=True, pii=None), + mode="verified", + course=None, + is_active=None, + creation_date=None, + ) + ) + create_user_id_verification_mock.assert_called_once() + self.assertEqual(create_user_id_verification_mock.call_args.args[0], user_id) + create_user_id_verification_mock_kwargs = ( + create_user_id_verification_mock.call_args.kwargs + ) + self.assertEqual( + create_user_id_verification_mock_kwargs, + { + **create_user_id_verification_mock_kwargs, + **{ + "status": "approved", + }, + }, + ) + self.assertEqual( + create_user_id_verification_mock_kwargs, + { + **create_user_id_verification_mock_kwargs, + **{ + "reason": "Skip id verification from nau_openedx_extensions", + }, + }, + ) + self.assertEqual( + create_user_id_verification_mock.call_args.kwargs.get( + "expiration_date" + ).year, + datetime.today().year + 100, + ) + + @patch( + "nau_openedx_extensions.verify_student.id_verification.get_user_id_verifications" + ) + @patch( + "nau_openedx_extensions.verify_student.id_verification.create_user_id_verification" + ) + def test_verify_student_enrollment_mode_not_need_id_verification_patch( + self, create_user_id_verification_mock, get_user_id_verifications_mock + ): + """ + Test a case enrollment mode that doesn't requires a ID Verification. + """ + user_id = 10 + get_user_id_verifications_mock.return_value = [] + COURSE_ENROLLMENT_CHANGED.connect(event_receiver_no_id_verify_for_enrollment_modes) + COURSE_ENROLLMENT_CHANGED.send_event( + enrollment=CourseEnrollmentData( + user=UserData(id=user_id, is_active=True, pii=None), + mode="honor", + course=None, + is_active=None, + creation_date=None, + ) + ) + create_user_id_verification_mock.assert_not_called() + + @patch( + "nau_openedx_extensions.verify_student.id_verification.get_user_id_verifications" + ) + @patch( + "nau_openedx_extensions.verify_student.id_verification.create_user_id_verification" + ) + @override_settings(NAU_NO_ID_VERIFY_FOR_ENROLLMENT_MODES="verified, somemode") + def test_verify_student_change_enrollment_modes_require_id_verification( + self, create_user_id_verification_mock, get_user_id_verifications_mock + ): + """ + Test that changing the setting `NAU_NO_ID_VERIFY_FOR_ENROLLMENT_MODES` to include a custom enrollment mode, + and test with that new custom enrollment mode, it still creates an id verification. + """ + user_id = 10 + get_user_id_verifications_mock.return_value = [] + COURSE_ENROLLMENT_CHANGED.connect(event_receiver_no_id_verify_for_enrollment_modes) + COURSE_ENROLLMENT_CHANGED.send_event( + enrollment=CourseEnrollmentData( + user=UserData(id=user_id, is_active=True, pii=None), + mode="somemode", + course=None, + is_active=None, + creation_date=None, + ) + ) + create_user_id_verification_mock.assert_called_once() + self.assertEqual(create_user_id_verification_mock.call_args.args[0], user_id) + create_user_id_verification_mock_kwargs = ( + create_user_id_verification_mock.call_args.kwargs + ) + self.assertEqual( + create_user_id_verification_mock_kwargs, + { + **create_user_id_verification_mock_kwargs, + **{ + "status": "approved", + }, + }, + ) + self.assertEqual( + create_user_id_verification_mock_kwargs, + { + **create_user_id_verification_mock_kwargs, + **{ + "reason": "Skip id verification from nau_openedx_extensions", + }, + }, + ) + self.assertEqual( + create_user_id_verification_mock.call_args.kwargs.get( + "expiration_date" + ).year, + datetime.today().year + 100, + ) + + @patch( + "nau_openedx_extensions.verify_student.id_verification.get_user_id_verifications" + ) + @patch( + "nau_openedx_extensions.verify_student.id_verification.create_user_id_verification" + ) + def test_verify_student_with_existing_id_verification( + self, create_user_id_verification_mock, get_user_id_verifications_mock + ): + """ + Test that if the user already has an id verification, it won't try to create a new one. + """ + active_at_datetime_mock = Mock() + active_at_datetime_mock.return_value = True + + user_id = 10 + get_user_id_verifications_mock.return_value = active_at_datetime_mock + COURSE_ENROLLMENT_CHANGED.connect(event_receiver_no_id_verify_for_enrollment_modes) + COURSE_ENROLLMENT_CHANGED.send_event( + enrollment=CourseEnrollmentData( + user=UserData(id=user_id, is_active=True, pii=None), + mode="verify", + course=None, + is_active=None, + creation_date=None, + ) + ) + create_user_id_verification_mock.assert_not_called() diff --git a/nau_openedx_extensions/verify_student/id_verification.py b/nau_openedx_extensions/verify_student/id_verification.py new file mode 100644 index 0000000..20ea909 --- /dev/null +++ b/nau_openedx_extensions/verify_student/id_verification.py @@ -0,0 +1,50 @@ +""" +NAU Custom code to skip Open edX ID Verification module. +""" +import logging +from datetime import datetime, timedelta + +from django.conf import settings +from django.dispatch import receiver +from openedx_events.learning.data import CourseEnrollmentData +from openedx_events.learning.signals import COURSE_ENROLLMENT_CHANGED +from pytz import UTC + +from nau_openedx_extensions.edxapp_wrapper.verify_student import create_user_id_verification, get_user_id_verifications + +log = logging.getLogger(__name__) + + +@receiver(COURSE_ENROLLMENT_CHANGED) +def event_receiver_no_id_verify_for_enrollment_modes(enrollment: CourseEnrollmentData, **kwargs): + """ + This receiver will ignore / skip the id verification of the Open edX platform. + Meaning that will create `ManualVerification` object if `enrollment_mode` is defined in the + `NAU_NO_ID_VERIFY_FOR_ENROLLMENT_MODES` setting, defaults to just the `verified` enrollment mode. + It should be configured using the Open edX signal: + `openedx_events.learning.signals.COURSE_ENROLLMENT_CHANGED` + """ + log.info("On event receiver that makes removes the need of ID Verify for some enrollment modes") + enrollment_mode = enrollment.mode + enrollment_modes_to_skip_as_str = getattr(settings, 'NAU_NO_ID_VERIFY_FOR_ENROLLMENT_MODES', 'verified') + enrollment_modes_to_skip = list(map(str.strip, enrollment_modes_to_skip_as_str.split(','))) + if enrollment_mode in enrollment_modes_to_skip: + user_id = enrollment.user.id + now = datetime.now(UTC) + + def verification_active_predicate(verification): + return verification.active_at_datetime(now) + + user_manual_verifications = get_user_id_verifications(user_id) + verification_active = next(filter(verification_active_predicate, user_manual_verifications), None) + if verification_active: + log.info("User %d already has an ID verification", user_id) + else: + expiration_date = now + timedelta(days=36500) # 100 years + log.info("Create user ID Verification for %d", user_id) + create_user_id_verification( + user_id, + status='approved', + expiration_date=expiration_date, + reason="Skip id verification from nau_openedx_extensions" + ) diff --git a/requirements/base.in b/requirements/base.in index 5e3ac27..65ca28c 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -5,3 +5,4 @@ six future; python_version < "3.0" web-fragments openedx-filters==0.7.0 +openedx-events==0.8.1 diff --git a/requirements/base.txt b/requirements/base.txt index 23d4952..45bd75b 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -11,6 +11,7 @@ django==2.2.25 # via -c requirements/constraints.txt, edx-opaque-keys edx-opaque-keys[django]==2.2.0 # via -c requirements/constraints.txt, -r requirements/base.in kombu==4.6.11 # via celery openedx-filters==0.7.0 # via -c requirements/constraints.txt, -r requirements/base.in +openedx-events==0.8.1 pbr==5.10.0 # via stevedore pymongo==4.2.0 # via edx-opaque-keys pytz==2022.2.1 # via celery, django diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 9ac3412..5e1cee2 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -3,5 +3,6 @@ celery<5.0 Django==2.2.25 edx-opaque-keys[django]==2.2.0 openedx-filters==0.7.0 +openedx-events==0.8.1 pip-tools<5.4 click==7.1.2 diff --git a/requirements/test.txt b/requirements/test.txt index c77c2cd..8f303f5 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -23,6 +23,7 @@ lazy-object-proxy==1.7.1 # via astroid markupsafe==2.1.1 # via jinja2 mccabe==0.7.0 # via pylint openedx-filters==0.7.0 # via -c requirements/constraints.txt, -r requirements/base.txt +openedx-events==0.8.1 packaging==21.3 # via pytest pbr==5.10.0 # via -r requirements/base.txt, stevedore platformdirs==2.5.2 # via pylint