Skip to content

Commit

Permalink
Merge pull request #55 from edx/alangsto/add_celery
Browse files Browse the repository at this point in the history
feat: use celery for django signal receivers
  • Loading branch information
alangsto authored Sep 23, 2021
2 parents 72f4224 + 95b1a26 commit faab25d
Show file tree
Hide file tree
Showing 19 changed files with 465 additions and 209 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
~~~~~~~~~~

[1.0.0] - 2021-09-23
~~~~~~~~~~~~~~~~~~~~~
* Move signal receiver logic into celery task

[0.11.0] - 2021-09-15
~~~~~~~~~~~~~~~~~~~~~
* Add name change validator
Expand Down
8 changes: 8 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ upgrade: ## update the requirements/*.txt files with the latest packages satisfy
# Let tox control the Django version for tests
sed '/^[dD]jango==/d' requirements/test.txt > requirements/test.tmp
mv requirements/test.tmp requirements/test.txt
grep -e "^amqp==\|^anyjson==\|^billiard==\|^celery==\|^kombu==\|^click-didyoumean==\|^click-repl==\|^click==\|^prompt-toolkit==\|^vine==" requirements/base.txt > requirements/celery50.txt
sed -i.tmp '/^amqp==/d' requirements/test.txt
sed -i.tmp '/^anyjson==/d' requirements/test.txt
sed -i.tmp '/^billiard==/d' requirements/test.txt
sed -i.tmp '/^celery==/d' requirements/test.txt
sed -i.tmp '/^kombu==/d' requirements/test.txt
sed -i.tmp '/^vine==/d' requirements/test.txt
rm requirements/*.txt.tmp

quality-python: ## Run python linters
tox -e quality
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.11.0'
__version__ = '1.0.0'

default_app_config = 'edx_name_affirmation.apps.EdxNameAffirmationConfig' # pylint: disable=invalid-name
149 changes: 12 additions & 137 deletions edx_name_affirmation/handlers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# pylint: disable=logging-format-interpolation
"""
Name Affirmation signal handlers
"""
Expand All @@ -12,7 +11,7 @@
from edx_name_affirmation.models import VerifiedName
from edx_name_affirmation.signals import VERIFIED_NAME_APPROVED
from edx_name_affirmation.statuses import VerifiedNameStatus
from edx_name_affirmation.toggles import is_verified_name_enabled
from edx_name_affirmation.tasks import idv_update_verified_name, proctoring_update_verified_name

User = get_user_model()

Expand Down Expand Up @@ -43,66 +42,7 @@ def idv_attempt_handler(attempt_id, user_id, status, photo_id_name, full_name, *
photo_id_name(str): name to be used as verified name
full_name(str): user's pending name change or 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=photo_id_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_name_qs = verified_names.filter(
verification_attempt_id=attempt_id,
proctored_exam_attempt_id=None
)

# Individually update to ensure that post_save signals send
for verified_name_obj in verified_name_qs:
verified_name_obj.status = trigger_status
verified_name_obj.save()

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=photo_id_name,
profile_name=full_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
)
)
idv_update_verified_name.delay(attempt_id, user_id, status, photo_id_name, full_name)


def proctoring_attempt_handler(
Expand All @@ -129,78 +69,13 @@ def proctoring_attempt_handler(
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,
)
)
proctoring_update_verified_name.delay(
attempt_id,
user_id,
status,
full_name,
profile_name,
is_practice_exam,
is_proctored,
backend_supports_onboarding
)
179 changes: 179 additions & 0 deletions edx_name_affirmation/tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
# pylint: disable=logging-format-interpolation
"""
Name affirmation celery tasks
"""

import logging

from celery import shared_task
from edx_django_utils.monitoring import set_code_owner_attribute

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__)


@shared_task
@set_code_owner_attribute
def idv_update_verified_name(attempt_id, user_id, status, photo_id_name, full_name):
"""
Celery task for updating a verified name based on an IDV attempt
"""
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=photo_id_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_name_qs = verified_names.filter(
verification_attempt_id=attempt_id,
proctored_exam_attempt_id=None
)

# Individually update to ensure that post_save signals send
for verified_name_obj in verified_name_qs:
verified_name_obj.status = trigger_status
verified_name_obj.save()

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=photo_id_name,
profile_name=full_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
)
)


@shared_task
@set_code_owner_attribute
def proctoring_update_verified_name(
attempt_id,
user_id,
status,
full_name,
profile_name,
is_practice_exam,
is_proctored,
backend_supports_onboarding
):
"""
Celery task for updating a verified name based on a proctoring attempt
"""
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,
)
)
Loading

0 comments on commit faab25d

Please sign in to comment.