fix: improve Celery task that sends certificate availability date data to Credentials IDA

[APER-1941]

We are aware of a product issue that causes a `certificate_available_date` (CAD) to be set for self-paced courses (and thus copied to the course-run's (course) certificate configuration) that causes an issue with learners' Program Records to be inaccurate. The stored CAD in Credentials is causing these certificates to be marked as "unearned" on the Program Record in Credentials, as the IDA believes the learner should *not* have access to them yet (but these certificates are available in the LMS).

A management command was recently introduced in Studio that can be used to clean/remove the `certificate_available_date` data from a course-run in Mongo. These updates aren't making it to the Credentials IDA because of an issue with our logic in the `update_certificate_visible_date_on_course_update` Celery task. This task assumes that we only want to send updates for *Instructor-Paced* courses that have a Certificate Display Behavior set to `end_with_date`. In reality, we need updates to pass to Credentials for _some_ self-paced courses with bad data.

This PR hopes to update our infrastructure to allow these updates to flow to Credentials.

* Improve logging for failed requests to the Credentials IDA's `course_certificates` endpoint when updating a course certificate configuration.
* Update docstrings and comments where appropriate
* Split the logic of the update_certificate_visible_date_on_course_update task into two tasks. The former task will continue to _just_ handle visible_date attribute updates. The latter (new) task will be dedicated to making the REST API call that updates the `certificate_available_date` data in Credentials.
* Update the `handle_course_cert_date_change` function wqhen the COURSE_CERT_DATE_CHANGE signal is received to queue both the "visible_date" and "certificate available date" Celery tasks.
* Update existing tests for the task changes.
This commit is contained in:
Justin Hynes
2022-09-27 14:58:50 -04:00
parent dc5b14b723
commit 8fd59044f9
4 changed files with 342 additions and 96 deletions

View File

@@ -179,32 +179,29 @@ def handle_course_cert_revoked(sender, user, course_key, mode, status, **kwargs)
@receiver(COURSE_CERT_DATE_CHANGE, dispatch_uid='course_certificate_date_change_handler')
def handle_course_cert_date_change(sender, course_key, **kwargs): # lint-amnesty, pylint: disable=unused-argument
"""
If course is updated and the certificate_available_date is changed,
schedule a celery task to update visible_date for all certificates
within course.
If a course-run's `certificate_available_date` is updated, schedule a celery task to update the `visible_date`
attribute of all (course) credentials awarded in the Credentials service.
Args:
course_key (CourseLocator): refers to the course whose certificate_available_date was updated.
Returns:
None
course_key(CourseLocator): refers to the course whose certificate_available_date was updated.
"""
# Import here instead of top of file since this module gets imported before
# the credentials app is loaded, resulting in a Django deprecation warning.
# Import here instead of top of file since this module gets imported before the credentials app is loaded, resulting
# in a Django deprecation warning.
from openedx.core.djangoapps.credentials.models import CredentialsApiConfig
# Avoid scheduling new tasks if certification is disabled.
# Avoid scheduling new tasks if we're not using the Credentials IDA
if not CredentialsApiConfig.current().is_learner_issuance_enabled:
LOGGER.warning(
f"Skipping handling of COURSE_CERT_DATE_CHANGE for course {course_key}. Use of the Credentials service is "
"disabled."
)
return
# schedule background task to process
LOGGER.info(
'handling COURSE_CERT_DATE_CHANGE for course %s',
course_key,
)
LOGGER.info(f"Handling COURSE_CERT_DATE_CHANGE for course {course_key}")
# import here, because signal is registered at startup, but items in tasks are not yet loaded
from openedx.core.djangoapps.programs.tasks import update_certificate_visible_date_on_course_update
from openedx.core.djangoapps.programs.tasks import update_certificate_available_date_on_course_update
# update the awarded credentials `visible_date` attribute in the Credentials service after a date update
update_certificate_visible_date_on_course_update.delay(str(course_key))
# update the (course) certificate configuration in the Credentials service after a date update
update_certificate_available_date_on_course_update.delay(str(course_key))

View File

@@ -7,7 +7,7 @@ from celery import shared_task
from celery.exceptions import MaxRetriesExceededError
from celery.utils.log import get_task_logger
from django.conf import settings
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
from django.contrib.auth import get_user_model
from django.contrib.sites.models import Site
from django.core.exceptions import ObjectDoesNotExist
from edx_django_utils.monitoring import set_code_owner_attribute
@@ -28,6 +28,8 @@ from openedx.core.djangoapps.credentials.utils import (
from openedx.core.djangoapps.programs.utils import ProgramProgressMeter
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
User = get_user_model()
LOGGER = get_task_logger(__name__)
# Maximum number of retries before giving up on awarding credentials.
# For reference, 11 retries with exponential backoff yields a maximum waiting
@@ -290,14 +292,21 @@ def award_program_certificates(self, username): # lint-amnesty, pylint: disable
def post_course_certificate_configuration(client, cert_config, certificate_available_date=None):
"""
POST to a course_certificates endpoint.
Make a POST request to the Credentials IDA's `course_certificates` endpoint (/api/v2/course_certificates/). This
endpoint manages the course certificate configurations within the Credentials IDA.
POST a configuration for a course certificate and the date the certificate will be available.
Args:
client(Session): An authenticated Credentials API Client
cert_config(Dict): A dictionary containing course metadata (course-run key and mode as Strings) important to the
Course Certificate Configuration.
certificate_available_date(Str): The desired Certificate Available Date for the Course Certificate Configuration
in the form of an ISO 8601 DateTime String.
"""
credentials_api_base_url = get_credentials_api_base_url()
api_url = urljoin(f"{credentials_api_base_url}/", "course_certificates/")
credentials_api_url = urljoin(f"{credentials_api_base_url}/", "course_certificates/")
response = client.post(
api_url,
credentials_api_url,
json={
"course_id": cert_config['course_id'],
"certificate_type": cert_config['mode'],
@@ -305,6 +314,17 @@ def post_course_certificate_configuration(client, cert_config, certificate_avail
"is_active": True
}
)
# Sometimes helpful error context is swallowed when calling `raise_for_status()`. We try to print out any additional
# error details here in the hope that it will save someone time when debugging an issue.
#
# Also... even though this endpoint does an `update_or_create()` on the Credentials side, it always passes back a
# 201 on a successful call.
if response.status_code != 201:
LOGGER.error(
"Error creating or updating a course certificate configuration in the Credentials IDA. Additional details: "
f"{response.text}"
)
response.raise_for_status()
@@ -358,8 +378,8 @@ def update_credentials_course_certificate_configuration_available_date(
not provided, it will be None.
"""
LOGGER.info(
f"Running task update_credentials_course_certificate_configuration_available_date for course {course_key} \
with certificate_available_date {certificate_available_date}"
f"Running task `update_credentials_course_certificate_configuration_available_date` for course {course_key} "
f"with certificate_available_date {certificate_available_date}"
)
course_key = str(course_key)
course_modes = CourseMode.objects.filter(course_id=course_key)
@@ -389,11 +409,13 @@ def update_credentials_course_certificate_configuration_available_date(
@set_code_owner_attribute
def award_course_certificate(self, username, course_run_key):
"""
This task is designed to be called whenever a student GeneratedCertificate is updated.
This task is designed to be called whenever a student GeneratedCertificate is updated, or when a course-run's
`certificate_available_date` value is updated.
It can be called independently for a username and a course_run, but is invoked on each GeneratedCertificate.save.
If this function is moved, make sure to update it's entry in
EXPLICIT_QUEUES in the settings files so it runs in the correct queue.
If this function is moved, make sure to update it's entry in EXPLICIT_QUEUES in the settings files so it runs in the
correct queue.
Arguments:
username (str): The user to award the Credentials course cert to
@@ -417,7 +439,6 @@ def award_course_certificate(self, username, course_run_key):
# feature, it may indicate a condition where processing of such tasks
# has been temporarily disabled. Since this is a recoverable situation,
# mark this task for retry instead of failing it altogether.
if not CredentialsApiConfig.current().is_learner_issuance_enabled:
error_msg = (
"Task award_course_certificate cannot be executed when credentials issuance is disabled in API config"
@@ -473,13 +494,12 @@ def award_course_certificate(self, username, course_run_key):
f"{course_key} with a visible date of {visible_date}"
)
# If the certificate has an associated CertificateDateOverride, send
# it along
# If the certificate has an associated CertificateDateOverride, send it along
try:
date_override = certificate.date_override.date
LOGGER.info(
"Task award_course_certificate will award certificate for "
f"course {course_key} with a date override of {date_override}"
"Task award_course_certificate will award certificate for course {course_key} with a date override "
f"of {date_override}"
)
except ObjectDoesNotExist:
date_override = None
@@ -706,64 +726,121 @@ def revoke_program_certificates(self, username, course_key): # lint-amnesty, py
@set_code_owner_attribute
def update_certificate_visible_date_on_course_update(self, course_key):
"""
This task is designed to be called whenever a course is updated with
certificate_available_date so that visible_date is updated on credential
service as well.
This task is designed to be called whenever a course-run's `certificate_available_date` is updated.
It will get all users within the course that have a certificate and call
the credentials API to update all these certificates visible_date value
to keep certificates in sync on both sides.
When executed, this task will first get a list of all learners within the course-run that have earned a certificate.
Next, we will enqueue an additional `award_course_certificate` task for each learner in this list. These subtasks
will be responsible for updating the `visible_date` attribute on each certificate the Credentials IDA knows about.
If this function is moved, make sure to update it's entry in
EXPLICIT_QUEUES in the settings files so it runs in the correct queue.
If this function is moved, make sure to update it's entry in EXPLICIT_QUEUES in the settings files so it runs in the
correct queue.
Arguments:
course_key (str): The course identifier
Returns:
None
course_key(str): The course identifier
"""
countdown = 2 ** self.request.retries
# If the credentials config model is disabled for this
# feature, it may indicate a condition where processing of such tasks
# has been temporarily disabled. Since this is a recoverable situation,
# mark this task for retry instead of failing it altogether.
# If the CredentialsApiConfig configuration model is disabled for this feature, it may indicate a condition where
# processing of such tasks has been temporarily disabled. Since this is a recoverable situation, mark this task for
# retry instead of failing it.
if not CredentialsApiConfig.current().is_learner_issuance_enabled:
error_msg = (
"Task update_certificate_visible_date_on_course_update cannot be executed when credentials issuance is "
"disabled in API config"
"Cannot execute the `update_certificate_visible_date_on_course_update` task. Issuing user credentials "
"through the Credentials IDA is disabled."
)
LOGGER.info(error_msg)
LOGGER.warning(error_msg)
exception = MaxRetriesExceededError(
f"Failed to update certificate availability date for course {course_key}. Reason: {error_msg}"
f"Failed to update the `visible_date` attribute for certificates in course {course_key}. Reason: "
f"{error_msg}"
)
raise self.retry(exc=exception, countdown=countdown, max_retries=MAX_RETRIES)
# Retrieve a list of all usernames of learners who have a certificate record in this course-run. The
# Credentials IDA REST API still requires a username as the main identifier for the learner.
users_with_certificates_in_course = (
GeneratedCertificate
.eligible_available_certificates
.filter(course_id=course_key)
.values_list('user__username', flat=True)
)
LOGGER.info(
f"Resending course certificates for learners in course {course_key} to the Credentials service. Queueing "
f"{len(users_with_certificates_in_course)} `award_course_certificate` tasks."
)
for user in users_with_certificates_in_course:
award_course_certificate.delay(user, str(course_key))
@shared_task(bind=True, ignore_result=True)
@set_code_owner_attribute
def update_certificate_available_date_on_course_update(self, course_key):
"""
This task is designed to be called whenever a course-run's `certificate_available_date` is updated.
When executed, this task will determine if we need to enqueue an
`update_credentials_course_certificate_configuration_available_date` task associated with the specified course-run
key from this task. If so, this subtask is responsible for making a REST API call to the Credentials IDA to update
the specified course-run's Course Certificate configuration with the new `certificate_available_date` value.
Args:
course_key(str): The course identifier
"""
countdown = 2 ** self.request.retries
# If the CredentialsApiConfig configuration model is disabled for this feature, it may indicate a condition where
# processing of such tasks has been temporarily disabled. Since this is a recoverable situation, mark this task for
# retry instead of failing it.
if not CredentialsApiConfig.current().is_learner_issuance_enabled:
error_msg = (
"Cannot execute the `update_certificate_visible_date_on_course_update` task. Issuing user credentials "
"through the Credentials IDA is disabled."
)
LOGGER.warning(error_msg)
exception = MaxRetriesExceededError(
"Failed to update the `certificate_available_date` in the Credentials service for course-run "
f"{course_key}. Reason: {error_msg}"
)
raise self.retry(exc=exception, countdown=countdown, max_retries=MAX_RETRIES)
# Update the CourseCertificate configuration in Credentials with the new
# certificate_available_date if:
# - The course is not self paced, AND
# - The certificates_display_behavior is "end_with_date"
course_overview = CourseOverview.get_from_id(course_key)
# Update the Credentials service's CourseCertificate configuration with the new `certificate_available_date` if:
# - The course-run is instructor-paced, AND
# - The `certificates_display_behavior` is set to "end_with_date",
if (
course_overview and
course_overview.self_paced is False and
course_overview.certificates_display_behavior == CertificatesDisplayBehaviors.END_WITH_DATE
):
LOGGER.info(
f"Queueing task to update the `certificate_available_date` of course-run {course_key} to "
f"[{course_overview.certificate_available_date}] in the Credentials service"
)
update_credentials_course_certificate_configuration_available_date.delay(
str(course_key),
str(course_overview.certificate_available_date)
)
# This code will update the visible_date in Credentials; we have moved away
# from relying on visible_date in favor of the above, but this still runs
# and visible_date is still updated
users_with_certificates_in_course = GeneratedCertificate.eligible_available_certificates.filter(
course_id=course_key
).values_list('user__username', flat=True)
LOGGER.info(
"Task update_certificate_visible_date_on_course_update resending course certificates "
f"for {len(users_with_certificates_in_course)} users in course {course_key}."
)
for user in users_with_certificates_in_course:
award_course_certificate.delay(user, str(course_key))
# OR,
# - The course-run is self-paced, AND
# - The `certificate_available_date` is (now) None. (This task will be executed after an update to the course
# overview)
# There are times when the CourseCertificate configuration of a self-paced course-run in Credentials can become
# associated with a `certificate_available_date`. This ends up causing learners' certificate to be incorrectly
# hidden. This is due to the Credentials IDA not understanding the concept of course pacing. Thus, we need a way
# to remove this value from self-paced courses in Credentials.
elif (
course_overview and
course_overview.self_paced is True and
course_overview.certificate_available_date is None
):
LOGGER.info(
"Queueing task to remove the `certificate_available_date` in the Credentials service for course-run "
f"{course_key}"
)
update_credentials_course_certificate_configuration_available_date.delay(str(course_key), None)
# ELSE, we don't meet the criteria to update the course cert config in the Credentials IDA
else:
LOGGER.warning(
f"Skipping update of the `certificate_available_date` for course {course_key} in the Credentials service. "
"This course-run does not meet the required criteria for an update."
)

View File

@@ -235,6 +235,7 @@ class CertRevokedReceiverTest(TestCase):
@skip_unless_lms
@mock.patch('openedx.core.djangoapps.programs.tasks.update_certificate_visible_date_on_course_update.delay')
@mock.patch('openedx.core.djangoapps.programs.tasks.update_certificate_available_date_on_course_update.delay')
@mock.patch(
'openedx.core.djangoapps.credentials.models.CredentialsApiConfig.is_learner_issuance_enabled',
new_callable=mock.PropertyMock,
@@ -256,35 +257,39 @@ class CourseCertAvailableDateChangedReceiverTest(TestCase):
'available_date': datetime.datetime.now()
}
def test_signal_received(self, mock_is_learner_issuance_enabled, mock_task): # pylint: disable=unused-argument
def test_signal_received(
self,
mock_is_learner_issuance_enabled,
mock_visible_date_task,
mock_cad_task
): # pylint: disable=unused-argument
"""
Ensures the receiver function is invoked when COURSE_CERT_DATE_CHANGE is
sent.
Ensures the receiver function is invoked when COURSE_CERT_DATE_CHANGE is sent.
Suboptimal: because we cannot mock the receiver function itself (due
to the way django signals work), we mock a configuration call that is
known to take place inside the function.
Suboptimal: because we cannot mock the receiver function itself (due to the way django signals work), we mock a
configuration call that is known to take place inside the function.
"""
COURSE_CERT_DATE_CHANGE.send(**self.signal_kwargs)
assert mock_is_learner_issuance_enabled.call_count == 1
def test_programs_disabled(self, mock_is_learner_issuance_enabled, mock_task):
def test_programs_disabled(self, mock_is_learner_issuance_enabled, mock_visible_date_task, mock_cad_task):
"""
Ensures that the receiver function does nothing when the credentials API
configuration is not enabled.
Ensures that the receiver function does nothing when the credentials API configuration is not enabled.
"""
handle_course_cert_date_change(**self.signal_kwargs)
assert mock_is_learner_issuance_enabled.call_count == 1
assert mock_task.call_count == 0
assert mock_visible_date_task.call_count == 0
assert mock_cad_task.call_count == 0
def test_programs_enabled(self, mock_is_learner_issuance_enabled, mock_task):
def test_programs_enabled(self, mock_is_learner_issuance_enabled, mock_visible_date_task, mock_cad_task):
"""
Ensures that the receiver function invokes the expected celery task
when the credentials API configuration is enabled.
Ensures that the receiver function invokes the expected celery task when the credentials API configuration is
enabled.
"""
mock_is_learner_issuance_enabled.return_value = True
handle_course_cert_date_change(**self.signal_kwargs)
assert mock_is_learner_issuance_enabled.call_count == 1
assert mock_task.call_count == 1
assert mock_visible_date_task.call_count == 1
assert mock_cad_task.call_count == 1

View File

@@ -18,7 +18,8 @@ from django.conf import settings
from django.test import TestCase, override_settings
from edx_rest_api_client.auth import SuppliedJwtAuth
from requests.exceptions import HTTPError
from xmodule.data import CertificatesDisplayBehaviors # lint-amnesty, pylint: disable=wrong-import-order
from testfixtures import LogCapture
from xmodule.data import CertificatesDisplayBehaviors
from common.djangoapps.course_modes.tests.factories import CourseModeFactory
from common.djangoapps.student.tests.factories import UserFactory
@@ -953,11 +954,9 @@ class RevokeProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiC
@skip_unless_lms
@override_settings(CREDENTIALS_SERVICE_USERNAME='test-service-username')
@mock.patch(TASKS_MODULE + '.get_credentials_api_client')
class UpdateCredentialsCourseCertificateConfigurationAvailableDateTestCase(TestCase):
"""
Tests for the update_credentials_course_certificate_configuration_available_date
function
Tests for the update_credentials_course_certificate_configuration_available_date function
"""
def setUp(self):
super().setUp()
@@ -970,8 +969,7 @@ class UpdateCredentialsCourseCertificateConfigurationAvailableDateTestCase(TestC
self.course_id = self.course.id
self.credentials_worker = UserFactory(username='test-service-username')
# pylint: disable=W0613
def test_update_course_cert_available_date(self, mock_client):
def test_update_course_cert_available_date(self):
with mock.patch(TASKS_MODULE + '.post_course_certificate_configuration') as update_posted:
tasks.update_credentials_course_certificate_configuration_available_date(
self.course_id,
@@ -979,8 +977,7 @@ class UpdateCredentialsCourseCertificateConfigurationAvailableDateTestCase(TestC
)
update_posted.assert_called_once()
# pylint: disable=W0613
def test_course_with_two_paid_modes(self, mock_client):
def test_course_with_two_paid_modes(self):
CourseModeFactory.create(course_id=self.course.id, mode_slug='professional')
with mock.patch(TASKS_MODULE + '.post_course_certificate_configuration') as update_posted:
tasks.update_credentials_course_certificate_configuration_available_date(
@@ -995,8 +992,8 @@ class PostCourseCertificateConfigurationTestCase(TestCase):
"""
Test the post_course_certificate_configuration function
"""
def setUp(self): # lint-amnesty, pylint: disable=super-method-not-called
def setUp(self):
super().setUp()
self.certificate = {
'mode': 'verified',
'course_id': 'testCourse',
@@ -1029,3 +1026,173 @@ class PostCourseCertificateConfigurationTestCase(TestCase):
}
last_request_body = httpretty.last_request().body.decode('utf-8')
assert json.loads(last_request_body) == expected_body
@skip_unless_lms
class UpdateCertificateVisibleDatesOnCourseUpdateTestCase(CredentialsApiConfigMixin, TestCase):
"""
Tests for the `update_certificate_visible_date_on_course_update` task.
"""
def setUp(self):
super().setUp()
self.credentials_api_config = self.create_credentials_config(enabled=False)
# setup course
self.course = CourseOverviewFactory.create()
# setup users
self.student1 = UserFactory.create(username='test-student1')
self.student2 = UserFactory.create(username='test-student2')
self.student3 = UserFactory.create(username='test-student3')
# award certificates to users in course we created
self.certificate_student1 = GeneratedCertificateFactory.create(
user=self.student1,
mode='verified',
course_id=self.course.id,
status='downloadable'
)
self.certificate_student2 = GeneratedCertificateFactory.create(
user=self.student2,
mode='verified',
course_id=self.course.id,
status='downloadable'
)
self.certificate_student3 = GeneratedCertificateFactory.create(
user=self.student3,
mode='verified',
course_id=self.course.id,
status='downloadable'
)
def tearDown(self):
super().tearDown()
self.credentials_api_config = self.create_credentials_config(enabled=False)
def test_update_visible_dates_but_credentials_config_disabled(self):
"""
This test verifies the behavior of the `update_certificate_visible_date_on_course_update` task when the
CredentialsApiConfig is disabled.
If the system is configured to _not_ use the Credentials IDA, we should expect this task to eventually throw an
exception when the max number of retries has reached.
"""
with pytest.raises(MaxRetriesExceededError):
tasks.update_certificate_visible_date_on_course_update(self.course.id) # pylint: disable=no-value-for-parameter
def test_update_visible_dates(self):
"""
Happy path test.
This test verifies the behavior of the `update_certificate_visible_date_on_course_update` task. This test
verifies attempts by the system to queue a number of `award_course_certificate` tasks to ensure the
`visible_date` attribute is updated on all eligible course certificates.
"""
# enable the CredentialsApiConfig to issue certificates using the Credentials service
self.credentials_api_config.enabled = True
self.credentials_api_config.enable_learner_issuance = True
with mock.patch(f"{TASKS_MODULE}.award_course_certificate.delay") as award_course_cert:
tasks.update_certificate_visible_date_on_course_update(self.course.id) # pylint: disable=no-value-for-parameter
assert award_course_cert.call_count == 3
@skip_unless_lms
class UpdateCertificateAvailableDateOnCourseUpdateTestCase(CredentialsApiConfigMixin, TestCase):
"""
Tests for the `update_certificate_available_date_on_course_update` task.
"""
def setUp(self):
super().setUp()
self.credentials_api_config = self.create_credentials_config(enabled=False)
def tearDown(self):
super().tearDown()
self.credentials_api_config = self.create_credentials_config(enabled=False)
def test_update_certificate_available_date_but_credentials_config_disabled(self):
"""
This test verifies the behavior of the `UpdateCertificateAvailableDateOnCourseUpdateTestCase` task when the
CredentialsApiConfig is disabled.
If the system is configured to _not_ use the Credentials IDA, we should expect this task to eventually throw an
exception when the max number of retries has reached.
"""
course = CourseOverviewFactory.create()
with pytest.raises(MaxRetriesExceededError):
tasks.update_certificate_available_date_on_course_update(course.id) # pylint: disable=no-value-for-parameter
def test_update_certificate_available_date_with_self_paced_course(self):
"""
Happy path test.
This test verifies that we are queueing a `update_credentials_course_certificate_configuration_available_date`
task with the expected arguments when removing a "certificate available date" from a course cert config in
Credentials.
"""
self.credentials_api_config.enabled = True
self.credentials_api_config.enable_learner_issuance = True
course = CourseOverviewFactory.create(
self_paced=True,
certificate_available_date=None
)
with mock.patch(
f"{TASKS_MODULE}.update_credentials_course_certificate_configuration_available_date.delay"
) as update_credentials_course_cert_config:
tasks.update_certificate_available_date_on_course_update(course.id) # pylint: disable=no-value-for-parameter
update_credentials_course_cert_config.assert_called_once_with(str(course.id), None)
def test_update_certificate_available_date_with_instructor_paced_course(self):
"""
Happy path test.
This test verifies that we are queueing a `update_credentials_course_certificate_configuration_available_date`
task with the expected arguments when updating a "certificate available date" from a course cert config in
Credentials.
"""
self.credentials_api_config.enabled = True
self.credentials_api_config.enable_learner_issuance = True
available_date = datetime.now(pytz.UTC) + timedelta(days=1)
course = CourseOverviewFactory.create(
self_paced=False,
certificate_available_date=available_date,
certificates_display_behavior=CertificatesDisplayBehaviors.END_WITH_DATE
)
with mock.patch(
f"{TASKS_MODULE}.update_credentials_course_certificate_configuration_available_date.delay"
) as update_credentials_course_cert_config:
tasks.update_certificate_available_date_on_course_update(course.id) # pylint: disable=no-value-for-parameter
update_credentials_course_cert_config.assert_called_once_with(str(course.id), str(available_date))
def test_update_certificate_available_date_with_expect_no_update(self):
"""
This test verifies that we do _not_ queue a task to update the course certificate configuration in Credentials
if the course-run does not meet the required criteria.
"""
self.credentials_api_config.enabled = True
self.credentials_api_config.enable_learner_issuance = True
available_date = datetime.now(pytz.UTC) + timedelta(days=1)
course = CourseOverviewFactory.create(
self_paced=False,
certificate_available_date=available_date,
certificates_display_behavior=CertificatesDisplayBehaviors.EARLY_NO_INFO
)
expected_message = (
f"Skipping update of the `certificate_available_date` for course {course.id} in the Credentials service. "
"This course-run does not meet the required criteria for an update."
)
with LogCapture(level=logging.WARNING) as log_capture:
tasks.update_certificate_available_date_on_course_update(course.id) # pylint: disable=no-value-for-parameter
assert len(log_capture.records) == 1
assert log_capture.records[0].getMessage() == expected_message