diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5ae1e65..c7995e4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,6 +14,10 @@ Change Log Unreleased ~~~~~~~~~~ +[0.6.0] - 2021-08-11 +~~~~~~~~~~~~~~~~~~~~ +* Add name verification status field, replacing single is_verified boolean. + [0.5.0] - 2021-08-11 ~~~~~~~~~~~~~~~~~~~~ * Add API method and endpoint to return a complete list of the user's diff --git a/edx_name_affirmation/__init__.py b/edx_name_affirmation/__init__.py index 6acea29..4c3b791 100644 --- a/edx_name_affirmation/__init__.py +++ b/edx_name_affirmation/__init__.py @@ -2,6 +2,6 @@ Django app housing name affirmation logic. """ -__version__ = '0.5.0' +__version__ = '0.6.0' default_app_config = 'edx_name_affirmation.apps.EdxNameAffirmationConfig' # pylint: disable=invalid-name diff --git a/edx_name_affirmation/api.py b/edx_name_affirmation/api.py index 4fa800b..9590da8 100644 --- a/edx_name_affirmation/api.py +++ b/edx_name_affirmation/api.py @@ -11,13 +11,14 @@ VerifiedNameMultipleAttemptIds ) from edx_name_affirmation.models import VerifiedName, VerifiedNameConfig +from edx_name_affirmation.statuses import VerifiedNameStatus log = logging.getLogger(__name__) def create_verified_name( user, verified_name, profile_name, verification_attempt_id=None, - proctored_exam_attempt_id=None, is_verified=False, + proctored_exam_attempt_id=None, status=VerifiedNameStatus.PENDING, ): """ Create a new `VerifiedName` for the given user. @@ -47,9 +48,9 @@ def create_verified_name( 'external attempt IDs were given. Only one may be used. ' 'verification_attempt_id={verification_attempt_id}, ' 'proctored_exam_attempt_id={proctored_exam_attempt_id}, ' - 'is_verified={is_verified}'.format( + 'status={status}'.format( user_id=user.id, verification_attempt_id=verification_attempt_id, - proctored_exam_attempt_id=proctored_exam_attempt_id, is_verified=is_verified, + proctored_exam_attempt_id=proctored_exam_attempt_id, status=status, ) ) raise VerifiedNameMultipleAttemptIds(err_msg) @@ -60,16 +61,16 @@ def create_verified_name( profile_name=profile_name, verification_attempt_id=verification_attempt_id, proctored_exam_attempt_id=proctored_exam_attempt_id, - is_verified=is_verified, + status=status, ) log_msg = ( 'VerifiedName created for user_id={user_id}. ' 'verification_attempt_id={verification_attempt_id}, ' 'proctored_exam_attempt_id={proctored_exam_attempt_id}, ' - 'is_verified={is_verified}'.format( + 'status={status}'.format( user_id=user.id, verification_attempt_id=verification_attempt_id, - proctored_exam_attempt_id=proctored_exam_attempt_id, is_verified=is_verified, + proctored_exam_attempt_id=proctored_exam_attempt_id, status=status, ) ) log.info(log_msg) @@ -89,7 +90,7 @@ def get_verified_name(user, is_verified=False): verified_name_qs = VerifiedName.objects.filter(user=user).order_by('-created') if is_verified: - return verified_name_qs.filter(is_verified=True).first() + return verified_name_qs.filter(status=VerifiedNameStatus.APPROVED.value).first() return verified_name_qs.first() @@ -159,8 +160,8 @@ def update_verification_attempt_id(user, verification_attempt_id): log.info(log_msg) -def update_is_verified_status( - user, is_verified, verification_attempt_id=None, proctored_exam_attempt_id=None +def update_verified_name_status( + user, status, verification_attempt_id=None, proctored_exam_attempt_id=None ): """ Update the status of a VerifiedName using the linked ID verification or exam attempt ID. Only one @@ -168,7 +169,7 @@ def update_is_verified_status( Arguments: * user (User object) - * is_verified (bool) + * status (Verified Name Status) * verification_attempt_id (int) * proctored_exam_attempt_id (int) """ @@ -177,7 +178,7 @@ def update_is_verified_status( if verification_attempt_id: if proctored_exam_attempt_id: err_msg = ( - 'Attempted to update the is_verified status for a VerifiedName, but two different ' + 'Attempted to update the status for a VerifiedName, but two different ' 'attempt IDs were given. verification_attempt_id={verification_attempt_id}, ' 'proctored_exam_attempt_id={proctored_exam_attempt_id}'.format( verification_attempt_id=verification_attempt_id, @@ -190,7 +191,7 @@ def update_is_verified_status( filters['proctored_exam_attempt_id'] = proctored_exam_attempt_id else: err_msg = ( - 'Attempted to update the is_verified status for a VerifiedName, but no ' + 'Attempted to update the status for a VerifiedName, but no ' 'verification_attempt_id or proctored_exam_attempt_id was given.' ) raise VerifiedNameAttemptIdNotGiven(err_msg) @@ -199,24 +200,24 @@ def update_is_verified_status( if not verified_name_obj: err_msg = ( - 'Attempted to update is_verified={is_verified} for a VerifiedName, but one does ' + 'Attempted to update status={status} for a VerifiedName, but one does ' 'not exist for the given attempt ID. verification_attempt_id={verification_attempt_id}, ' 'proctored_exam_attempt_id={proctored_exam_attempt_id}'.format( - is_verified=is_verified, + status=status, verification_attempt_id=verification_attempt_id, proctored_exam_attempt_id=proctored_exam_attempt_id, ) ) raise VerifiedNameDoesNotExist(err_msg) - verified_name_obj.is_verified = is_verified + verified_name_obj.status = status.value verified_name_obj.save() log_msg = ( - 'Updated is_verified={is_verified} for VerifiedName belonging to user_id={user_id}. ' + 'Updated status={status} for VerifiedName belonging to user_id={user_id}. ' 'verification_attempt_id={verification_attempt_id}, ' 'proctored_exam_attempt_id={proctored_exam_attempt_id}'.format( - is_verified=is_verified, + status=status, user_id=verified_name_obj.user.id, verification_attempt_id=verification_attempt_id, proctored_exam_attempt_id=proctored_exam_attempt_id, diff --git a/edx_name_affirmation/migrations/0003_verifiedname_status.py b/edx_name_affirmation/migrations/0003_verifiedname_status.py new file mode 100644 index 0000000..04fe4fa --- /dev/null +++ b/edx_name_affirmation/migrations/0003_verifiedname_status.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.24 on 2021-08-13 12:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('edx_name_affirmation', '0002_verifiednameconfig'), + ] + + operations = [ + migrations.AddField( + model_name='verifiedname', + name='status', + field=models.CharField(choices=[('pending', 'pending'), ('submitted', 'submitted'), ('approved', 'approved'), ('denied', 'denied')], default='pending', max_length=32), + ), + ] diff --git a/edx_name_affirmation/migrations/0004_auto_20210816_0958.py b/edx_name_affirmation/migrations/0004_auto_20210816_0958.py new file mode 100644 index 0000000..2216cea --- /dev/null +++ b/edx_name_affirmation/migrations/0004_auto_20210816_0958.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.24 on 2021-08-16 09:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('edx_name_affirmation', '0003_verifiedname_status'), + ] + + operations = [ + migrations.AlterField( + model_name='verifiedname', + name='is_verified', + field=models.BooleanField(default=False, null=True), + ), + ] diff --git a/edx_name_affirmation/models.py b/edx_name_affirmation/models.py index 18583ff..4e9d621 100644 --- a/edx_name_affirmation/models.py +++ b/edx_name_affirmation/models.py @@ -8,6 +8,8 @@ from django.contrib.auth import get_user_model from django.db import models +from edx_name_affirmation.statuses import VerifiedNameStatus + User = get_user_model() @@ -30,7 +32,13 @@ class VerifiedName(TimeStampedModel): verification_attempt_id = models.PositiveIntegerField(null=True) proctored_exam_attempt_id = models.PositiveIntegerField(null=True) - is_verified = models.BooleanField(default=False) + status = models.CharField( + max_length=32, + choices=[(st.value, st.value) for st in VerifiedNameStatus], + default=VerifiedNameStatus.PENDING.value, + ) + # is_verified is being removed + is_verified = models.BooleanField(default=False, null=True) class Meta: """ Meta class for this Django model """ diff --git a/edx_name_affirmation/serializers.py b/edx_name_affirmation/serializers.py index 083de7e..8ef3680 100644 --- a/edx_name_affirmation/serializers.py +++ b/edx_name_affirmation/serializers.py @@ -17,7 +17,7 @@ class VerifiedNameSerializer(serializers.ModelSerializer): profile_name = serializers.CharField(required=True) verification_attempt_id = serializers.IntegerField(required=False, allow_null=True) proctored_exam_attempt_id = serializers.IntegerField(required=False, allow_null=True) - is_verified = serializers.BooleanField(required=False, allow_null=True) + status = serializers.CharField(required=False, allow_null=True) class Meta: """ @@ -27,7 +27,7 @@ class Meta: fields = ( "created", "username", "verified_name", "profile_name", "verification_attempt_id", - "proctored_exam_attempt_id", "is_verified" + "proctored_exam_attempt_id", "status" ) diff --git a/edx_name_affirmation/statuses.py b/edx_name_affirmation/statuses.py new file mode 100644 index 0000000..4ef4b46 --- /dev/null +++ b/edx_name_affirmation/statuses.py @@ -0,0 +1,33 @@ +""" +Statuses for edx_name_affirmation. +""" + +from enum import Enum + + +class VerifiedNameStatus(str, Enum): + """ + Possible states for the verified name. + + Pending: the verified name has been created + + Submitted: the verified name has been submitted to a verification authority + + Approved, Denied: resulting states from that authority + + This is the status of the verified name attempt, which is related to + but separate from the status of the verifying process such as IDV or proctoring. + Status changes in the verifying processes are usually more fine grained. + + For example when proctoring changes from ready to start to started the verified + name is still pending. Once proctoring is actually submitted the verified name + can be considered submitted. + + The expected lifecycle is pending -> submitted -> approved/denied. + + .. no_pii: This model has no PII. + """ + PENDING = "pending" + SUBMITTED = "submitted" + APPROVED = "approved" + DENIED = "denied" diff --git a/edx_name_affirmation/tests/test_api.py b/edx_name_affirmation/tests/test_api.py index 7260a82..4a0437c 100644 --- a/edx_name_affirmation/tests/test_api.py +++ b/edx_name_affirmation/tests/test_api.py @@ -14,8 +14,8 @@ get_verified_name, get_verified_name_history, should_use_verified_name_for_certs, - update_is_verified_status, - update_verification_attempt_id + update_verification_attempt_id, + update_verified_name_status ) from edx_name_affirmation.exceptions import ( VerifiedNameAttemptIdNotGiven, @@ -24,6 +24,7 @@ VerifiedNameMultipleAttemptIds ) from edx_name_affirmation.models import VerifiedName, VerifiedNameConfig +from edx_name_affirmation.statuses import VerifiedNameStatus User = get_user_model() @@ -58,26 +59,26 @@ def test_create_verified_name_defaults(self): self.assertEqual(verified_name_obj.user, self.user) self.assertIsNone(verified_name_obj.verification_attempt_id) self.assertIsNone(verified_name_obj.proctored_exam_attempt_id) - self.assertFalse(verified_name_obj.is_verified) + self.assertEqual(verified_name_obj.status, VerifiedNameStatus.PENDING.value) @ddt.data( - (123, None, False), - (None, 456, True), + (123, None, VerifiedNameStatus.APPROVED), + (None, 456, VerifiedNameStatus.SUBMITTED), ) @ddt.unpack def test_create_verified_name_with_optional_arguments( - self, verification_attempt_id, proctored_exam_attempt_id, is_verified, + self, verification_attempt_id, proctored_exam_attempt_id, status, ): """ Test to create a verified name with optional arguments supplied. """ verified_name_obj = self._create_verified_name( - verification_attempt_id, proctored_exam_attempt_id, is_verified, + verification_attempt_id, proctored_exam_attempt_id, status, ) self.assertEqual(verified_name_obj.verification_attempt_id, verification_attempt_id) self.assertEqual(verified_name_obj.proctored_exam_attempt_id, proctored_exam_attempt_id) - self.assertEqual(verified_name_obj.is_verified, is_verified) + self.assertEqual(verified_name_obj.status, status.value) def test_create_verified_name_two_ids(self): """ @@ -130,10 +131,10 @@ def test_get_verified_name_most_recent(self): def test_get_verified_name_only_verified(self): """ - Test that VerifiedName entries with is_verified=False are ignored if is_verified + Test that VerifiedName entries with status != approved are ignored if is_verified argument is set to True. """ - self._create_verified_name(is_verified=True) + self._create_verified_name(status=VerifiedNameStatus.APPROVED) create_verified_name(self.user, 'unverified name', 'unverified profile name') verified_name_obj = get_verified_name(self.user, True) @@ -225,11 +226,11 @@ def test_update_is_verified_status( Test that VerifiedName status can be updated with a given attempt ID. """ self._create_verified_name(verification_attempt_id, proctored_exam_attempt_id) - update_is_verified_status( - self.user, True, verification_attempt_id, proctored_exam_attempt_id, + update_verified_name_status( + self.user, VerifiedNameStatus.DENIED, verification_attempt_id, proctored_exam_attempt_id, ) verified_name_obj = get_verified_name(self.user) - self.assertTrue(verified_name_obj.is_verified) + self.assertEqual(VerifiedNameStatus.DENIED.value, verified_name_obj.status) def test_update_is_verified_no_attempt_id(self): """ @@ -237,7 +238,7 @@ def test_update_is_verified_no_attempt_id(self): ID given. """ with self.assertRaises(VerifiedNameAttemptIdNotGiven): - update_is_verified_status(self.user, True) + update_verified_name_status(self.user, True) def test_update_is_verified_multiple_attempt_ids(self): """ @@ -245,7 +246,7 @@ def test_update_is_verified_multiple_attempt_ids(self): IDs given. """ with self.assertRaises(VerifiedNameMultipleAttemptIds): - update_is_verified_status( + update_verified_name_status( self.user, True, self.VERIFICATION_ATTEMPT_ID, self.PROCTORED_EXAM_ATTEMPT_ID, ) @@ -255,17 +256,17 @@ def test_update_is_verified_does_not_exist(self): not exist for the attempt ID given. """ with self.assertRaises(VerifiedNameDoesNotExist): - update_is_verified_status(self.user, True, self.VERIFICATION_ATTEMPT_ID) + update_verified_name_status(self.user, True, self.VERIFICATION_ATTEMPT_ID) def _create_verified_name( - self, verification_attempt_id=None, proctored_exam_attempt_id=None, is_verified=False, + self, verification_attempt_id=None, proctored_exam_attempt_id=None, status=VerifiedNameStatus.PENDING, ): """ Util to create and return a VerifiedName with default names. """ create_verified_name( self.user, self.VERIFIED_NAME, self.PROFILE_NAME, verification_attempt_id, - proctored_exam_attempt_id, is_verified + proctored_exam_attempt_id, status ) return get_verified_name(self.user) diff --git a/edx_name_affirmation/tests/test_views.py b/edx_name_affirmation/tests/test_views.py index 31d5e18..a29f3e3 100644 --- a/edx_name_affirmation/tests/test_views.py +++ b/edx_name_affirmation/tests/test_views.py @@ -18,6 +18,7 @@ should_use_verified_name_for_certs ) from edx_name_affirmation.models import VerifiedNameConfig +from edx_name_affirmation.statuses import VerifiedNameStatus from edx_name_affirmation.toggles import VERIFIED_NAME_FLAG from .utils import LoggedInTestCase @@ -48,7 +49,7 @@ def tearDown(self): cache.clear() def test_verified_name(self): - verified_name = self._create_verified_name(is_verified=True) + verified_name = self._create_verified_name(status=VerifiedNameStatus.APPROVED) expected_data = self._get_expected_data(self.user, verified_name) @@ -59,7 +60,7 @@ def test_verified_name(self): @override_waffle_flag(VERIFIED_NAME_FLAG, active=True) def test_verified_name_feature_enabled(self): - verified_name = self._create_verified_name(is_verified=True) + verified_name = self._create_verified_name(status=VerifiedNameStatus.APPROVED) expected_data = self._get_expected_data(self.user, verified_name, verified_name_enabled=True) @@ -69,7 +70,7 @@ def test_verified_name_feature_enabled(self): self.assertEqual(data, expected_data) def test_verified_name_existing_config(self): - verified_name = self._create_verified_name(is_verified=True) + verified_name = self._create_verified_name() create_verified_name_config(self.user, use_verified_name_for_certs=True) expected_data = self._get_expected_data(self.user, verified_name, use_verified_name_for_certs=True) @@ -80,7 +81,7 @@ def test_verified_name_existing_config(self): def test_staff_access_verified_name(self): other_user = User(username='other_tester', email='other@test.com') other_user.save() - create_verified_name(other_user, self.VERIFIED_NAME, self.PROFILE_NAME, is_verified=True) + create_verified_name(other_user, self.VERIFIED_NAME, self.PROFILE_NAME, status=VerifiedNameStatus.APPROVED) # check that non staff access returns 403 response = self.client.get(reverse('edx_name_affirmation:verified_name'), {'username': other_user.username}) @@ -136,7 +137,7 @@ def test_post_200_if_staff(self): 'profile_name': self.PROFILE_NAME, 'verified_name': self.VERIFIED_NAME, 'proctored_exam_attempt_id': self.ATTEMPT_ID, - 'is_verified': True, + 'status': VerifiedNameStatus.APPROVED.value, } response = self.client.post( reverse('edx_name_affirmation:verified_name'), @@ -159,7 +160,7 @@ def test_post_403_non_staff(self): 'profile_name': self.PROFILE_NAME, 'verified_name': self.VERIFIED_NAME, 'verification_attempt_id': self.ATTEMPT_ID, - 'is_verified': True, + 'status': VerifiedNameStatus.APPROVED.value, } response = self.client.post( reverse('edx_name_affirmation:verified_name'), @@ -173,7 +174,7 @@ def test_post_400_invalid_serializer(self): 'profile_name': self.PROFILE_NAME, 'verified_name': self.VERIFIED_NAME, 'verification_attempt_id': 'xxyz', - 'is_verified': True + 'status': VerifiedNameStatus.APPROVED.value, } response = self.client.post( reverse('edx_name_affirmation:verified_name'), @@ -196,7 +197,7 @@ def test_post_400_two_attempt_ids(self): self.assertEqual(response.status_code, 400) def _create_verified_name( - self, verification_attempt_id=None, proctored_exam_attempt_id=None, is_verified=False, + self, verification_attempt_id=None, proctored_exam_attempt_id=None, status=VerifiedNameStatus.PENDING, ): """ Create and return a verified name object. @@ -207,7 +208,7 @@ def _create_verified_name( self.PROFILE_NAME, verification_attempt_id, proctored_exam_attempt_id, - is_verified + status ) return get_verified_name(self.user) @@ -225,7 +226,7 @@ def _get_expected_data( 'profile_name': verified_name_obj.profile_name, 'verification_attempt_id': verified_name_obj.verification_attempt_id, 'proctored_exam_attempt_id': verified_name_obj.proctored_exam_attempt_id, - 'is_verified': verified_name_obj.is_verified, + 'status': verified_name_obj.status, 'use_verified_name_for_certs': use_verified_name_for_certs, 'verified_name_enabled': verified_name_enabled } @@ -280,14 +281,14 @@ def _create_verified_name_history(self, user): 'Jonathan Doe', 'Jon Doe', verification_attempt_id=123, - is_verified=True + status=VerifiedNameStatus.APPROVED, ) create_verified_name( user, 'Jane Doe', 'Jane Doe', proctored_exam_attempt_id=456, - is_verified=False + status=VerifiedNameStatus.DENIED, ) return get_verified_name_history(user) @@ -305,7 +306,7 @@ def _get_expected_response(self, user, verified_name_history): 'profile_name': verified_name_obj.profile_name, 'verification_attempt_id': verified_name_obj.verification_attempt_id, 'proctored_exam_attempt_id': verified_name_obj.proctored_exam_attempt_id, - 'is_verified': verified_name_obj.is_verified + 'status': verified_name_obj.status } expected_response['results'].append(data) diff --git a/edx_name_affirmation/views.py b/edx_name_affirmation/views.py index f5d924e..45e354d 100644 --- a/edx_name_affirmation/views.py +++ b/edx_name_affirmation/views.py @@ -20,6 +20,7 @@ ) from edx_name_affirmation.exceptions import VerifiedNameMultipleAttemptIds from edx_name_affirmation.serializers import VerifiedNameConfigSerializer, VerifiedNameSerializer +from edx_name_affirmation.statuses import VerifiedNameStatus from edx_name_affirmation.toggles import is_verified_name_enabled @@ -48,7 +49,7 @@ class VerifiedNameView(AuthenticatedAPIView): "profile_name": "Jon Doe" "verification_attempt_id": (Optional) "proctored_exam_attempt_id": (Optional) - "is_verified": (Optional) + "status": (Optional) } HTTP GET @@ -61,7 +62,7 @@ class VerifiedNameView(AuthenticatedAPIView): "profile_name": "Jon Doe", "verification_attempt_id": 123, "proctored_exam_attempt_id": None, - "is_verified": True, + "status": "approved", "use_verified_name_for_certs": False, "verified_name_enabled": True } @@ -113,7 +114,7 @@ def post(self, request): request.data.get('profile_name'), verification_attempt_id=request.data.get('verification_attempt_id', None), proctored_exam_attempt_id=request.data.get('proctored_exam_attempt_id', None), - is_verified=request.data.get('is_verified', False) + status=request.data.get('status', VerifiedNameStatus.PENDING) ) response_status = status.HTTP_200_OK data = {}