Skip to content

Commit

Permalink
Merge pull request #38 from edx/alangsto/add_signal_receivers
Browse files Browse the repository at this point in the history
feat: add signal receivers for edx_name_affirmation
  • Loading branch information
alangsto authored Aug 30, 2021
2 parents e7a7751 + 6a520c1 commit 96bd37d
Show file tree
Hide file tree
Showing 16 changed files with 646 additions and 79 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ Change Log
Unreleased
~~~~~~~~~~

[0.8.0] - 2021-08-30
~~~~~~~~~~~~~~~~~~~~
* Add signal receivers for IDV and proctoring attempts

[0.7.0] - 2021-08-26
~~~~~~~~~~~~~~~~~~~~
* Add verified_name_enabled and use_verified_name_for_certs to the GET response of VerifiedNameHistoryView.
Expand Down
2 changes: 1 addition & 1 deletion edx_name_affirmation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
Django app housing name affirmation logic.
"""

__version__ = '0.7.0'
__version__ = '0.8.0'

default_app_config = 'edx_name_affirmation.apps.EdxNameAffirmationConfig' # pylint: disable=invalid-name
15 changes: 15 additions & 0 deletions edx_name_affirmation/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,20 @@ class EdxNameAffirmationConfig(AppConfig):
'regex': '^api/',
'relative_path': 'urls',
}
},
'signals_config': {
'lms.djangoapp': {
'relative_path': 'handlers',
'receivers': [
{
'receiver_func_name': 'idv_attempt_handler',
'signal_path': 'lms.djangoapps.verify_student.signals.idv_update_signal',
},
{
'receiver_func_name': 'proctoring_attempt_handler',
'signal_path': 'edx_proctoring.signals.exam_attempt_status_signal',
}
],
}
}
}
186 changes: 186 additions & 0 deletions edx_name_affirmation/handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
# pylint: disable=logging-format-interpolation

""""
Signal handlers for IDV and proctoring service signals
"""

import logging

from django.contrib.auth import get_user_model

from edx_name_affirmation.models import VerifiedName
from edx_name_affirmation.statuses import VerifiedNameStatus
from edx_name_affirmation.toggles import is_verified_name_enabled

User = get_user_model()

log = logging.getLogger(__name__)


def idv_attempt_handler(attempt_id, user_id, status, full_name, profile_name, **kwargs):
"""
Receiver for IDV attempt updates
Args:
attempt_id(int): ID associated with the IDV attempt
user_id(int): ID associated with the IDV attempt's user
status(str): status in IDV language for the IDV attempt
full_name(str): name to be used as verified name
profile_name(str): user's current profile name
"""
if not is_verified_name_enabled():
return

trigger_status = VerifiedNameStatus.trigger_state_change_from_idv(status)
verified_names = VerifiedName.objects.filter(user__id=user_id, verified_name=full_name).order_by('-created')
if verified_names:
# if there are VerifiedName objects, we want to update existing entries
# for each attempt with no attempt id (either proctoring or idv), update attempt id
updated_for_attempt_id = verified_names.filter(
proctored_exam_attempt_id=None,
verification_attempt_id=None
).update(verification_attempt_id=attempt_id)

if updated_for_attempt_id:
log.info(
'Updated VerifiedNames for user={user_id} to verification_attempt_id={attempt_id}'.format(
user_id=user_id,
attempt_id=attempt_id,
)
)

# then for all matching attempt ids, update the status
if trigger_status:
verified_names.filter(
verification_attempt_id=attempt_id,
proctored_exam_attempt_id=None
).update(status=trigger_status)

log.info(
'Updated VerifiedNames for user={user_id} with verification_attempt_id={attempt_id} to '
'have status={status}'.format(
user_id=user_id,
attempt_id=attempt_id,
status=trigger_status
)
)
else:
# otherwise if there are no entries, we want to create one.
user = User.objects.get(id=user_id)
verified_name = VerifiedName.objects.create(
user=user,
verified_name=full_name,
profile_name=profile_name,
verification_attempt_id=attempt_id,
status=(trigger_status if trigger_status else VerifiedNameStatus.PENDING),
)
log.error(
'Created VerifiedName for user={user_id} to have status={status} '
'and verification_attempt_id={attempt_id}, because no matching '
'attempt_id or verified_name were found.'.format(
user_id=user_id,
attempt_id=attempt_id,
status=verified_name.status
)
)


def proctoring_attempt_handler(
attempt_id,
user_id,
status,
full_name,
profile_name,
is_practice_exam,
is_proctored,
backend_supports_onboarding,
**kwargs
):
"""
Receiver for proctored exam attempt updates.
Args:
attempt_id(int): ID associated with the proctored exam attempt
user_id(int): ID associated with the proctored exam attempt's user
status(str): status in proctoring language for the proctored exam attempt
full_name(str): name to be used as verified name
profile_name(str): user's current profile name
is_practice_exam(boolean): if the exam attempt is for a practice exam
is_proctored(boolean): if the exam attempt is for a proctored exam
backend_supports_onboarding(boolean): if the exam attempt is for an exam with a backend that supports onboarding
"""
if not is_verified_name_enabled():
return

# We only care about updates from onboarding exams, or from non-practice proctored exams with a backend that
# does not support onboarding. This is because those two event types are guaranteed to contain verification events,
# whereas timed exams and proctored exams with a backend that does support onboarding are not guaranteed
is_onboarding_exam = is_practice_exam and is_proctored and backend_supports_onboarding
reviewable_proctored_exam = is_proctored and not is_practice_exam and not backend_supports_onboarding
if not (is_onboarding_exam or reviewable_proctored_exam):
return

# check if approved VerifiedName already exists for the user
verified_name = VerifiedName.objects.filter(
user__id=user_id,
status=VerifiedNameStatus.APPROVED
).order_by('-created').first()
if verified_name:
approved_verified_name = verified_name.verified_name
is_full_name_approved = approved_verified_name == full_name
if not is_full_name_approved:
log.warning(
'Full name for proctored_exam_attempt_id={attempt_id} is not equal '
'to the most recent verified name verified_name_id={name_id}.'.format(
attempt_id=attempt_id,
name_id=verified_name.id
)
)
return

trigger_status = VerifiedNameStatus.trigger_state_change_from_proctoring(status)

verified_name = VerifiedName.objects.filter(
user__id=user_id,
proctored_exam_attempt_id=attempt_id
).order_by('-created').first()
if verified_name:
# if a verified name for the given attempt ID exists, update it if the status should trigger a transition
if trigger_status:
verified_name.status = trigger_status
verified_name.save()
log.info(
'Updated VerifiedName for user={user_id} with proctored_exam_attempt_id={attempt_id} '
'to have status={status}'.format(
user_id=user_id,
attempt_id=attempt_id,
status=trigger_status
)
)
else:
if full_name and profile_name:
# if they do not already have an approved VerifiedName, create one
user = User.objects.get(id=user_id)
VerifiedName.objects.create(
user=user,
verified_name=full_name,
proctored_exam_attempt_id=attempt_id,
status=(trigger_status if trigger_status else VerifiedNameStatus.PENDING),
profile_name=profile_name
)
log.info(
'Created VerifiedName for user={user_id} to have status={status} '
'and proctored_exam_attempt_id={attempt_id}'.format(
user_id=user_id,
attempt_id=attempt_id,
status=trigger_status
)
)
else:
log.error(
'Cannot create VerifiedName for user={user_id} for proctored_exam_attempt_id={attempt_id} '
'because neither profile name nor full name were provided'.format(
user_id=user_id,
attempt_id=attempt_id,
)
)
32 changes: 32 additions & 0 deletions edx_name_affirmation/statuses.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,35 @@ class VerifiedNameStatus(str, Enum):
SUBMITTED = "submitted"
APPROVED = "approved"
DENIED = "denied"

@classmethod
def trigger_state_change_from_idv(cls, idv_status):
"""
Return the translated IDV status if it should trigger a state transition, otherwise return None
"""
# mapping from an idv status (key) to it's associated verified name status (value). We only want to
# include idv statuses that would cause a status transition for a verified name
idv_state_transition_mapping = {
'created': cls.PENDING,
'submitted': cls.SUBMITTED,
'approved': cls.APPROVED,
'denied': cls.DENIED
}

return idv_state_transition_mapping.get(idv_status, None)

@classmethod
def trigger_state_change_from_proctoring(cls, proctoring_status):
"""
Return the translated proctoring status if it should trigger a state transition, otherwise return None
"""
# mapping from an proctoring status (key) to it's associated verified name status (value). We only want to
# include proctoring statuses that would cause a status transition for a verified name
proctoring_state_transition_mapping = {
'created': cls.PENDING,
'submitted': cls.SUBMITTED,
'verified': cls.APPROVED,
'rejected': cls.DENIED
}

return proctoring_state_transition_mapping.get(proctoring_status, None)
Loading

0 comments on commit 96bd37d

Please sign in to comment.