feat: relinting

Trying with new autoformat settings.

FIXES: APER-3146
This commit is contained in:
Deborah Kaplan
2024-01-30 17:39:57 +00:00
parent bde84117c3
commit adae7e3e25
2 changed files with 128 additions and 358 deletions

View File

@@ -128,9 +128,7 @@ def award_program_certificate(client, user, program_uuid, visible_date):
"username": user.username,
"lms_user_id": user.id,
"credential": {"type": PROGRAM_CERTIFICATE, "program_uuid": program_uuid},
"attributes": [
{"name": "visible_date", "value": visible_date.strftime(DATE_FORMAT)}
],
"attributes": [{"name": "visible_date", "value": visible_date.strftime(DATE_FORMAT)}],
},
)
response.raise_for_status()
@@ -138,9 +136,7 @@ def award_program_certificate(client, user, program_uuid, visible_date):
@shared_task(bind=True, ignore_result=True)
@set_code_owner_attribute
def award_program_certificates(
self, username
): # lint-amnesty, pylint: disable=too-many-statements
def award_program_certificates(self, username): # lint-amnesty, pylint: disable=too-many-statements
"""
This task is designed to be called whenever a student's completion status
changes with respect to one or more courses (primarily, when a course
@@ -172,9 +168,7 @@ def award_program_certificates(
return self.retry(exc=exception, countdown=countdown, max_retries=MAX_RETRIES)
LOGGER.info(f"Running task award_program_certificates for username {username}")
programs_without_certificates = configuration_helpers.get_value(
"programs_without_certificates", []
)
programs_without_certificates = configuration_helpers.get_value("programs_without_certificates", [])
if programs_without_certificates:
if str(programs_without_certificates[0]).lower() == "all":
# this check will prevent unnecessary logging for partners without program certificates
@@ -187,19 +181,17 @@ def award_program_certificates(
# mark this task for retry instead of failing it altogether.
if not CredentialsApiConfig.current().is_learner_issuance_enabled:
error_msg = "Task award_program_certificates cannot be executed when credentials issuance is disabled in API config"
LOGGER.warning(error_msg)
raise _retry_with_custom_exception(
username=username, reason=error_msg, countdown=countdown
error_msg = (
"Task award_program_certificates cannot be executed when credentials issuance is disabled in API config"
)
LOGGER.warning(error_msg)
raise _retry_with_custom_exception(username=username, reason=error_msg, countdown=countdown)
try:
try:
student = User.objects.get(username=username)
except User.DoesNotExist:
LOGGER.exception(
f"Task award_program_certificates was called with invalid username {username}"
)
LOGGER.exception(f"Task award_program_certificates was called with invalid username {username}")
# Don't retry for this case - just conclude the task.
return
completed_programs = {}
@@ -208,9 +200,7 @@ def award_program_certificates(
if not completed_programs:
# No reason to continue beyond this point unless/until this
# task gets updated to support revocation of program certs.
LOGGER.info(
f"Task award_program_certificates was called for user {username} with no completed programs"
)
LOGGER.info(f"Task award_program_certificates was called for user {username} with no completed programs")
return
# Determine which program certificates the user has already been awarded, if any.
@@ -218,16 +208,12 @@ def award_program_certificates(
# we will skip all the programs which have already been awarded and we want to skip the programs
# which are exit in site configuration in 'programs_without_certificates' list.
awarded_and_skipped_program_uuids = list(
set(existing_program_uuids + list(programs_without_certificates))
)
awarded_and_skipped_program_uuids = list(set(existing_program_uuids + list(programs_without_certificates)))
except Exception as exc:
error_msg = f"Failed to determine program certificates to be awarded for user {username}. {exc}"
LOGGER.exception(error_msg)
raise _retry_with_custom_exception(
username=username, reason=error_msg, countdown=countdown
) from exc
raise _retry_with_custom_exception(username=username, reason=error_msg, countdown=countdown) from exc
# For each completed program for which the student doesn't already have a
# certificate, award one now.
@@ -235,9 +221,7 @@ def award_program_certificates(
# This logic is important, because we will retry the whole task if awarding any particular program cert fails.
#
# N.B. the list is sorted to facilitate deterministic ordering, e.g. for tests.
new_program_uuids = sorted(
list(set(completed_programs.keys()) - set(awarded_and_skipped_program_uuids))
)
new_program_uuids = sorted(list(set(completed_programs.keys()) - set(awarded_and_skipped_program_uuids)))
if new_program_uuids:
try:
credentials_client = get_credentials_api_client(
@@ -247,23 +231,15 @@ def award_program_certificates(
error_msg = "Failed to create a credentials API client to award program certificates"
LOGGER.exception(error_msg)
# Retry because a misconfiguration could be fixed
raise _retry_with_custom_exception(
username=username, reason=error_msg, countdown=countdown
) from exc
raise _retry_with_custom_exception(username=username, reason=error_msg, countdown=countdown) from exc
failed_program_certificate_award_attempts = []
for program_uuid in new_program_uuids:
visible_date = completed_programs[program_uuid]
try:
LOGGER.info(
f"Visible date for user {username} : program {program_uuid} is {visible_date}"
)
award_program_certificate(
credentials_client, student, program_uuid, visible_date
)
LOGGER.info(
f"Awarded certificate for program {program_uuid} to user {username}"
)
LOGGER.info(f"Visible date for user {username} : program {program_uuid} is {visible_date}")
award_program_certificate(credentials_client, student, program_uuid, visible_date)
LOGGER.info(f"Awarded certificate for program {program_uuid} to user {username}")
except HTTPError as exc:
if exc.response.status_code == 404:
LOGGER.exception(
@@ -290,16 +266,12 @@ def award_program_certificates(
)
except Exception: # pylint: disable=broad-except
# keep trying to award other certs, but retry the whole task to fix any missing entries
LOGGER.exception(
f"Failed to award certificate for program {program_uuid} to user {username}."
)
LOGGER.exception(f"Failed to award certificate for program {program_uuid} to user {username}.")
failed_program_certificate_award_attempts.append(program_uuid)
if failed_program_certificate_award_attempts:
# N.B. This logic assumes that this task is idempotent
LOGGER.info(
f"Retrying task to award failed certificates to user {username}"
)
LOGGER.info(f"Retrying task to award failed certificates to user {username}")
# The error message may change on each reattempt but will never be raised until
# the max number of retries have been exceeded. It is unlikely that this list
# will change by the time it reaches its maximimum number of attempts.
@@ -307,20 +279,14 @@ def award_program_certificates(
f"Failed to award certificate for user {username} "
f"for programs {failed_program_certificate_award_attempts}"
)
raise _retry_with_custom_exception(
username=username, reason=error_msg, countdown=countdown
)
raise _retry_with_custom_exception(username=username, reason=error_msg, countdown=countdown)
else:
LOGGER.info(f"User {username} is not eligible for any new program certificates")
LOGGER.info(
f"Successfully completed the task award_program_certificates for username {username}"
)
LOGGER.info(f"Successfully completed the task award_program_certificates for username {username}")
def post_course_certificate_configuration(
client, cert_config, certificate_available_date=None
):
def post_course_certificate_configuration(client, cert_config, certificate_available_date=None):
"""
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.
@@ -333,9 +299,7 @@ def post_course_certificate_configuration(
in the form of an ISO 8601 DateTime String.
"""
credentials_api_base_url = get_credentials_api_base_url()
credentials_api_url = urljoin(
f"{credentials_api_base_url}/", "course_certificates/"
)
credentials_api_url = urljoin(f"{credentials_api_base_url}/", "course_certificates/")
response = client.post(
credentials_api_url,
@@ -360,9 +324,7 @@ def post_course_certificate_configuration(
response.raise_for_status()
def post_course_certificate(
client, username, certificate, visible_date, date_override=None, org=None
):
def post_course_certificate(client, username, certificate, visible_date, date_override=None, org=None):
"""
POST a certificate that has been updated to Credentials
"""
@@ -373,20 +335,14 @@ def post_course_certificate(
api_url,
json={
"username": username,
"status": "awarded"
if certificate.is_valid()
else "revoked", # Only need the two options at this time
"status": "awarded" if certificate.is_valid() else "revoked", # Only need the two options at this time
"credential": {
"course_run_key": str(certificate.course_id),
"mode": certificate.mode,
"type": COURSE_CERTIFICATE,
},
"date_override": {"date": date_override.strftime(DATE_FORMAT)}
if date_override
else None,
"attributes": [
{"name": "visible_date", "value": visible_date.strftime(DATE_FORMAT)}
],
"date_override": {"date": date_override.strftime(DATE_FORMAT)} if date_override else None,
"attributes": [{"name": "visible_date", "value": visible_date.strftime(DATE_FORMAT)}],
},
)
response.raise_for_status()
@@ -417,15 +373,9 @@ def update_credentials_course_certificate_configuration_available_date(
course_key = str(course_key)
course_modes = CourseMode.objects.filter(course_id=course_key)
# There should only ever be one certificate relevant mode per course run
modes = [
mode.slug
for mode in course_modes
if mode.slug in CourseMode.CERTIFICATE_RELEVANT_MODES
]
modes = [mode.slug for mode in course_modes if mode.slug in CourseMode.CERTIFICATE_RELEVANT_MODES]
if len(modes) != 1:
LOGGER.exception(
f"Either course {course_key} has no certificate mode or multiple modes. Task failed."
)
LOGGER.exception(f"Either course {course_key} has no certificate mode or multiple modes. Task failed.")
return
credentials_client = get_credentials_api_client(
@@ -474,7 +424,9 @@ def award_course_certificate(self, username, course_run_key):
# 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"
error_msg = (
"Task award_course_certificate cannot be executed when credentials issuance is disabled in API " "config"
)
LOGGER.warning(error_msg)
raise _retry_with_custom_exception(
username=username,
@@ -487,9 +439,7 @@ def award_course_certificate(self, username, course_run_key):
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
LOGGER.exception(
f"Task award_course_certificate was called with invalid username {username}"
)
LOGGER.exception(f"Task award_course_certificate was called with invalid username {username}")
# Don't retry for this case - just conclude the task.
return
# Get the cert for the course key and username if it's both passing and available in professional/verified
@@ -546,9 +496,7 @@ def award_course_certificate(self, username, course_run_key):
org=course_key.org,
)
LOGGER.info(
f"Awarded certificate for course {course_key} to user {username}"
)
LOGGER.info(f"Awarded certificate for course {course_key} to user {username}")
except Exception as exc:
error_msg = f"Failed to determine course certificates to be awarded for user {username}."
LOGGER.exception(error_msg)
@@ -560,9 +508,7 @@ def award_course_certificate(self, username, course_run_key):
) from exc
def get_revokable_program_uuids(
course_specific_programs: List[Dict], student: User
) -> List[str]:
def get_revokable_program_uuids(course_specific_programs: List[Dict], student: User) -> List[str]:
"""
Get program uuids for which certificate to be revoked.
@@ -618,9 +564,7 @@ def revoke_program_certificate(client, username, program_uuid):
@shared_task(bind=True, ignore_result=True)
@set_code_owner_attribute
def revoke_program_certificates(
self, username, course_key
): # lint-amnesty, pylint: disable=too-many-statements
def revoke_program_certificates(self, username, course_key): # lint-amnesty, pylint: disable=too-many-statements
"""
This task is designed to be called whenever a student's course certificate is
revoked.
@@ -654,7 +598,9 @@ def revoke_program_certificates(
# mark this task for retry instead of failing it altogether.
if not CredentialsApiConfig.current().is_learner_issuance_enabled:
error_msg = "Task revoke_program_certificates cannot be executed when credentials issuance is disabled in API config"
error_msg = (
"Task revoke_program_certificates cannot be executed when credentials issuance is disabled in API config"
)
LOGGER.warning(error_msg)
raise _retry_with_custom_exception(
username=username,
@@ -685,13 +631,10 @@ def revoke_program_certificates(
return
# Determine which program certificates the user has already been awarded, if any.
program_uuids_to_revoke = get_revokable_program_uuids(
course_specific_programs, student
)
program_uuids_to_revoke = get_revokable_program_uuids(course_specific_programs, student)
except Exception as exc:
error_msg = (
f"Failed to determine program certificates to be revoked for user {username} "
f"with course {course_key}"
f"Failed to determine program certificates to be revoked for user {username} " f"with course {course_key}"
)
LOGGER.exception(error_msg)
raise _retry_with_custom_exception(
@@ -710,17 +653,13 @@ def revoke_program_certificates(
error_msg = "Failed to create a credentials API client to revoke program certificates"
LOGGER.exception(error_msg)
# Retry because a misconfiguration could be fixed
raise _retry_with_custom_exception(
username, course_key, reason=exc, countdown=countdown
) from exc
raise _retry_with_custom_exception(username, course_key, reason=exc, countdown=countdown) from exc
failed_program_certificate_revoke_attempts = []
for program_uuid in program_uuids_to_revoke:
try:
revoke_program_certificate(credentials_client, username, program_uuid)
LOGGER.info(
f"Revoked certificate for program {program_uuid} for user {username}"
)
LOGGER.info(f"Revoked certificate for program {program_uuid} for user {username}")
except HTTPError as exc:
if exc.response.status_code == 404:
LOGGER.exception(
@@ -742,21 +681,15 @@ def revoke_program_certificates(
countdown=rate_limit_countdown,
) from exc
else:
LOGGER.exception(
f"Unable to revoke certificate for user {username} for program {program_uuid}."
)
LOGGER.exception(f"Unable to revoke certificate for user {username} for program {program_uuid}.")
except Exception: # pylint: disable=broad-except
# keep trying to revoke other certs, but retry the whole task to fix any missing entries
LOGGER.warning(
f"Failed to revoke certificate for program {program_uuid} of user {username}."
)
LOGGER.warning(f"Failed to revoke certificate for program {program_uuid} of user {username}.")
failed_program_certificate_revoke_attempts.append(program_uuid)
if failed_program_certificate_revoke_attempts:
# N.B. This logic assumes that this task is idempotent
LOGGER.info(
f"Retrying task to revoke failed certificates to user {username}"
)
LOGGER.info(f"Retrying task to revoke failed certificates to user {username}")
# The error message may change on each reattempt but will never be raised until
# the max number of retries have been exceeded. It is unlikely that this list
# will change by the time it reaches its maximimum number of attempts.
@@ -764,15 +697,11 @@ def revoke_program_certificates(
f"Failed to revoke certificate for user {username} "
f"for programs {failed_program_certificate_revoke_attempts}"
)
raise _retry_with_custom_exception(
username, course_key, reason=error_msg, countdown=countdown
)
raise _retry_with_custom_exception(username, course_key, reason=error_msg, countdown=countdown)
else:
LOGGER.info(f"There is no program certificates for user {username} to revoke")
LOGGER.info(
f"Successfully completed the task revoke_program_certificates for username {username}"
)
LOGGER.info(f"Successfully completed the task revoke_program_certificates for username {username}")
@shared_task(bind=True, ignore_result=True)
@@ -810,11 +739,9 @@ def update_certificate_visible_date_on_course_update(self, course_key):
# 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)
)
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 "
@@ -862,8 +789,7 @@ def update_certificate_available_date_on_course_update(self, course_key):
if (
course_overview
and course_overview.self_paced is False
and course_overview.certificates_display_behavior
== CertificatesDisplayBehaviors.END_WITH_DATE
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 "
@@ -880,18 +806,12 @@ def update_certificate_available_date_on_course_update(self, course_key):
# 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
):
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
)
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(

View File

@@ -77,16 +77,12 @@ class GetAwardedCertificateProgramsTestCase(TestCase):
"""
student = UserFactory(username="test-username")
mock_get_credentials.return_value = [
self.make_credential_result(
status="awarded", credential={"program_uuid": 1}
),
self.make_credential_result(status="awarded", credential={"program_uuid": 1}),
]
result = tasks.get_certified_programs(student)
assert mock_get_credentials.call_args[0] == (student,)
assert (
mock_get_credentials.call_args[1].get("credential_type", None) == "program"
)
assert mock_get_credentials.call_args[1].get("credential_type", None) == "program"
assert result == [1]
@@ -113,9 +109,7 @@ class AwardProgramCertificateTestCase(TestCase):
"http://test-server/credentials/",
)
tasks.award_program_certificate(
test_client, student, 123, datetime(2010, 5, 30)
)
tasks.award_program_certificate(test_client, student, 123, datetime(2010, 5, 30))
expected_body = {
"username": student.username,
@@ -141,9 +135,7 @@ class AwardProgramCertificateTestCase(TestCase):
@mock.patch(TASKS_MODULE + ".get_certified_programs")
@mock.patch(TASKS_MODULE + ".get_completed_programs")
@override_settings(CREDENTIALS_SERVICE_USERNAME="test-service-username")
class AwardProgramCertificatesTestCase(
CatalogIntegrationMixin, CredentialsApiConfigMixin, TestCase
):
class AwardProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiConfigMixin, TestCase):
"""
Tests for the 'award_program_certificates' celery task.
"""
@@ -194,20 +186,14 @@ class AwardProgramCertificatesTestCase(
tasks.award_program_certificates.delay(self.student.username).get()
actual_program_uuids = [
call[0][2] for call in mock_award_program_certificate.call_args_list
]
actual_program_uuids = [call[0][2] for call in mock_award_program_certificate.call_args_list]
assert actual_program_uuids == expected_awarded_program_uuids
actual_visible_dates = [
call[0][3] for call in mock_award_program_certificate.call_args_list
]
actual_visible_dates = [call[0][3] for call in mock_award_program_certificate.call_args_list]
assert actual_visible_dates == expected_awarded_program_uuids
# program uuids are same as mock dates
@mock.patch(
"openedx.core.djangoapps.site_configuration.helpers.get_current_site_configuration"
)
@mock.patch("openedx.core.djangoapps.site_configuration.helpers.get_current_site_configuration")
def test_awarding_certs_with_skip_program_certificate(
self,
mocked_get_current_site_configuration,
@@ -236,13 +222,9 @@ class AwardProgramCertificatesTestCase(
expected_awarded_program_uuids = [3, 4]
tasks.award_program_certificates.delay(self.student.username).get()
actual_program_uuids = [
call[0][2] for call in mock_award_program_certificate.call_args_list
]
actual_program_uuids = [call[0][2] for call in mock_award_program_certificate.call_args_list]
assert actual_program_uuids == expected_awarded_program_uuids
actual_visible_dates = [
call[0][3] for call in mock_award_program_certificate.call_args_list
]
actual_visible_dates = [call[0][3] for call in mock_award_program_certificate.call_args_list]
assert actual_visible_dates == expected_awarded_program_uuids
# program uuids are same as mock dates
@@ -250,16 +232,12 @@ class AwardProgramCertificatesTestCase(
("credentials", "enable_learner_issuance"),
)
@ddt.unpack
def test_retry_if_config_disabled(
self, disabled_config_type, disabled_config_attribute, *mock_helpers
):
def test_retry_if_config_disabled(self, disabled_config_type, disabled_config_attribute, *mock_helpers):
"""
Checks that the task is aborted if any relevant api configs are
disabled.
"""
getattr(self, f"create_{disabled_config_type}_config")(
**{disabled_config_attribute: False}
)
getattr(self, f"create_{disabled_config_type}_config")(**{disabled_config_attribute: False})
with mock.patch(TASKS_MODULE + ".LOGGER.warning") as mock_warning:
with pytest.raises(MaxRetriesExceededError):
tasks.award_program_certificates.delay(self.student.username).get()
@@ -371,9 +349,7 @@ class AwardProgramCertificatesTestCase(
"""
mock_get_completed_programs.return_value = {1: 1, 2: 2}
mock_get_certified_programs.side_effect = [[], [2]]
mock_award_program_certificate.side_effect = self._make_side_effect(
[Exception("boom"), None]
)
mock_award_program_certificate.side_effect = self._make_side_effect([Exception("boom"), None])
with mock.patch(TASKS_MODULE + ".LOGGER.info") as mock_info, mock.patch(
TASKS_MODULE + ".LOGGER.exception"
@@ -386,25 +362,17 @@ class AwardProgramCertificatesTestCase(
uuid=1, username=self.student.username
)
)
mock_info.assert_any_call(
f"Awarded certificate for program {1} to user {self.student.username}"
)
mock_info.assert_any_call(
f"Awarded certificate for program {2} to user {self.student.username}"
)
mock_info.assert_any_call(f"Awarded certificate for program {1} to user {self.student.username}")
mock_info.assert_any_call(f"Awarded certificate for program {2} to user {self.student.username}")
def test_retry_on_programs_api_errors(
self, mock_get_completed_programs, *_mock_helpers
):
def test_retry_on_programs_api_errors(self, mock_get_completed_programs, *_mock_helpers):
"""
Ensures that any otherwise-unhandled errors that arise while trying
to get completed programs (e.g. network issues or other
transient API errors) will cause the task to be failed and queued for
retry.
"""
mock_get_completed_programs.side_effect = self._make_side_effect(
[Exception("boom"), None]
)
mock_get_completed_programs.side_effect = self._make_side_effect([Exception("boom"), None])
tasks.award_program_certificates.delay(self.student.username).get()
assert mock_get_completed_programs.call_count == 3
@@ -422,9 +390,7 @@ class AwardProgramCertificatesTestCase(
"""
mock_get_completed_programs.return_value = {1: 1, 2: 2}
mock_get_certified_programs.return_value = [1]
mock_get_certified_programs.side_effect = self._make_side_effect(
[Exception("boom"), None]
)
mock_get_certified_programs.side_effect = self._make_side_effect([Exception("boom"), None])
tasks.award_program_certificates.delay(self.student.username).get()
assert mock_get_certified_programs.call_count == 2
assert mock_award_program_certificate.call_count == 1
@@ -441,9 +407,7 @@ class AwardProgramCertificatesTestCase(
exception = HTTPError()
exception.response = mock.Mock(status_code=429)
mock_get_completed_programs.return_value = {1: 1, 2: 2}
mock_award_program_certificate.side_effect = self._make_side_effect(
[exception, None]
)
mock_award_program_certificate.side_effect = self._make_side_effect([exception, None])
tasks.award_program_certificates.delay(self.student.username).get()
@@ -461,9 +425,7 @@ class AwardProgramCertificatesTestCase(
exception = HTTPError()
exception.response = mock.Mock(status_code=404)
mock_get_completed_programs.return_value = {1: 1, 2: 2}
mock_award_program_certificate.side_effect = self._make_side_effect(
[exception, None]
)
mock_award_program_certificate.side_effect = self._make_side_effect([exception, None])
tasks.award_program_certificates.delay(self.student.username).get()
@@ -481,9 +443,7 @@ class AwardProgramCertificatesTestCase(
exception = HTTPError()
exception.response = mock.Mock(status_code=418)
mock_get_completed_programs.return_value = {1: 1, 2: 2}
mock_award_program_certificate.side_effect = self._make_side_effect(
[exception, None]
)
mock_award_program_certificate.side_effect = self._make_side_effect([exception, None])
tasks.award_program_certificates.delay(self.student.username).get()
@@ -525,9 +485,7 @@ class PostCourseCertificateTestCase(TestCase):
visible_date = datetime.now()
tasks.post_course_certificate(
test_client, self.student.username, self.certificate, visible_date
)
tasks.post_course_certificate(test_client, self.student.username, self.certificate, visible_date)
expected_body = {
"username": self.student.username,
@@ -541,9 +499,7 @@ class PostCourseCertificateTestCase(TestCase):
"attributes": [
{
"name": "visible_date",
"value": visible_date.strftime(
"%Y-%m-%dT%H:%M:%SZ"
), # text representation of date
"value": visible_date.strftime("%Y-%m-%dT%H:%M:%SZ"), # text representation of date
}
],
}
@@ -607,107 +563,79 @@ class AwardCourseCertificatesTestCase(CredentialsApiConfigMixin, TestCase):
"""
self.certificate.mode = mode
self.certificate.save()
tasks.award_course_certificate.delay(
self.student.username, str(self.course.id)
).get()
tasks.award_course_certificate.delay(self.student.username, str(self.course.id)).get()
call_args, _ = mock_post_course_certificate.call_args
assert call_args[1] == self.student.username
assert call_args[2] == self.certificate
assert call_args[3] == self.certificate.modified_date
def test_award_course_certificates_available_date(
self, mock_post_course_certificate
):
def test_award_course_certificates_available_date(self, mock_post_course_certificate):
"""
Tests the API POST method is called with available date when the course is not self paced
"""
self.course.self_paced = False
self.course.save()
tasks.award_course_certificate.delay(
self.student.username, str(self.course.id)
).get()
tasks.award_course_certificate.delay(self.student.username, str(self.course.id)).get()
call_args, _ = mock_post_course_certificate.call_args
assert call_args[1] == self.student.username
assert call_args[2] == self.certificate
assert call_args[3] == self.available_date
def test_award_course_certificates_override_date(
self, mock_post_course_certificate
):
def test_award_course_certificates_override_date(self, mock_post_course_certificate):
"""
Tests the API POST method is called with date override when present
"""
self._add_certificate_date_override()
tasks.award_course_certificate.delay(
self.student.username, str(self.course.id)
).get()
tasks.award_course_certificate.delay(self.student.username, str(self.course.id)).get()
call_args, _ = mock_post_course_certificate.call_args
assert call_args[1] == self.student.username
assert call_args[2] == self.certificate
assert call_args[3] == self.certificate.modified_date
assert call_args[4] == self.certificate.date_override.date
def test_award_course_cert_not_called_if_disabled(
self, mock_post_course_certificate
):
def test_award_course_cert_not_called_if_disabled(self, mock_post_course_certificate):
"""
Test that the post method is never called if the config is disabled
"""
self.create_credentials_config(enabled=False)
with mock.patch(TASKS_MODULE + ".LOGGER.warning") as mock_warning:
with pytest.raises(MaxRetriesExceededError):
tasks.award_course_certificate.delay(
self.student.username, str(self.course.id)
).get()
tasks.award_course_certificate.delay(self.student.username, str(self.course.id)).get()
assert mock_warning.called
assert not mock_post_course_certificate.called
def test_award_course_cert_not_called_if_user_not_found(
self, mock_post_course_certificate
):
def test_award_course_cert_not_called_if_user_not_found(self, mock_post_course_certificate):
"""
Test that the post method is never called if the user isn't found by username
"""
with mock.patch(TASKS_MODULE + ".LOGGER.exception") as mock_exception:
# Use a random username here since this user won't be found in the DB
tasks.award_course_certificate.delay(
"random_username", str(self.course.id)
).get()
tasks.award_course_certificate.delay("random_username", str(self.course.id)).get()
assert mock_exception.called
assert not mock_post_course_certificate.called
def test_award_course_cert_not_called_if_certificate_not_found(
self, mock_post_course_certificate
):
def test_award_course_cert_not_called_if_certificate_not_found(self, mock_post_course_certificate):
"""
Test that the post method is never called if the certificate doesn't exist for the user and course
"""
self.certificate.delete()
with mock.patch(TASKS_MODULE + ".LOGGER.exception") as mock_exception:
tasks.award_course_certificate.delay(
self.student.username, str(self.course.id)
).get()
tasks.award_course_certificate.delay(self.student.username, str(self.course.id)).get()
assert mock_exception.called
assert not mock_post_course_certificate.called
def test_award_course_cert_not_called_if_course_overview_not_found(
self, mock_post_course_certificate
):
def test_award_course_cert_not_called_if_course_overview_not_found(self, mock_post_course_certificate):
"""
Test that the post method is never called if the CourseOverview isn't found
"""
self.course.delete()
with mock.patch(TASKS_MODULE + ".LOGGER.exception") as mock_exception:
# Use the certificate course id here since the course will be deleted
tasks.award_course_certificate.delay(
self.student.username, str(self.certificate.course_id)
).get()
tasks.award_course_certificate.delay(self.student.username, str(self.certificate.course_id)).get()
assert mock_exception.called
assert not mock_post_course_certificate.called
def test_award_course_cert_not_called_if_certificated_not_verified_mode(
self, mock_post_course_certificate
):
def test_award_course_cert_not_called_if_certificated_not_verified_mode(self, mock_post_course_certificate):
"""
Test that the post method is never called if the GeneratedCertificate is an 'audit' cert
"""
@@ -717,9 +645,7 @@ class AwardCourseCertificatesTestCase(CredentialsApiConfigMixin, TestCase):
self.certificate.save()
self.create_credentials_config()
tasks.award_course_certificate.delay(
self.student.username, str(self.certificate.course_id)
).get()
tasks.award_course_certificate.delay(self.student.username, str(self.certificate.course_id)).get()
assert not mock_post_course_certificate.called
@@ -765,9 +691,7 @@ class RevokeProgramCertificateTestCase(TestCase):
@mock.patch(TASKS_MODULE + ".get_certified_programs")
@mock.patch(TASKS_MODULE + ".get_inverted_programs")
@override_settings(CREDENTIALS_SERVICE_USERNAME="test-service-username")
class RevokeProgramCertificatesTestCase(
CatalogIntegrationMixin, CredentialsApiConfigMixin, TestCase
):
class RevokeProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiConfigMixin, TestCase):
"""
Tests for the 'revoke_program_certificates' celery task.
"""
@@ -811,9 +735,7 @@ class RevokeProgramCertificatesTestCase(
Checks that the Programs API is used correctly to determine completed
programs.
"""
tasks.revoke_program_certificates.delay(
self.student.username, self.course_key
).get()
tasks.revoke_program_certificates.delay(self.student.username, self.course_key).get()
mock_get_inverted_programs.assert_any_call(self.student)
def test_revoke_program_certificate(
@@ -827,14 +749,10 @@ class RevokeProgramCertificatesTestCase(
the proper programs.
"""
expected_program_uuid = 1
mock_get_inverted_programs.return_value = {
self.course_key: [{"uuid": expected_program_uuid}]
}
mock_get_inverted_programs.return_value = {self.course_key: [{"uuid": expected_program_uuid}]}
mock_get_certified_programs.return_value = [expected_program_uuid]
tasks.revoke_program_certificates.delay(
self.student.username, self.course_key
).get()
tasks.revoke_program_certificates.delay(self.student.username, self.course_key).get()
call_args, _ = mock_revoke_program_certificate.call_args
assert call_args[1] == self.student.username
@@ -844,21 +762,15 @@ class RevokeProgramCertificatesTestCase(
("credentials", "enable_learner_issuance"),
)
@ddt.unpack
def test_retry_if_config_disabled(
self, disabled_config_type, disabled_config_attribute, *mock_helpers
):
def test_retry_if_config_disabled(self, disabled_config_type, disabled_config_attribute, *mock_helpers):
"""
Checks that the task is aborted if any relevant api configs are
disabled.
"""
getattr(self, f"create_{disabled_config_type}_config")(
**{disabled_config_attribute: False}
)
getattr(self, f"create_{disabled_config_type}_config")(**{disabled_config_attribute: False})
with mock.patch(TASKS_MODULE + ".LOGGER.warning") as mock_warning:
with pytest.raises(MaxRetriesExceededError):
tasks.revoke_program_certificates.delay(
self.student.username, self.course_key
).get()
tasks.revoke_program_certificates.delay(self.student.username, self.course_key).get()
assert mock_warning.called
for mock_helper in mock_helpers:
assert not mock_helper.called
@@ -869,9 +781,7 @@ class RevokeProgramCertificatesTestCase(
passed was not found, and that an exception is logged.
"""
with mock.patch(TASKS_MODULE + ".LOGGER.exception") as mock_exception:
tasks.revoke_program_certificates.delay(
"nonexistent-username", self.course_key
).get()
tasks.revoke_program_certificates.delay("nonexistent-username", self.course_key).get()
assert mock_exception.called
for mock_helper in mock_helpers:
assert not mock_helper.called
@@ -887,9 +797,7 @@ class RevokeProgramCertificatesTestCase(
not part of any program.
"""
mock_get_inverted_programs.return_value = {}
tasks.revoke_program_certificates.delay(
self.student.username, self.course_key
).get()
tasks.revoke_program_certificates.delay(self.student.username, self.course_key).get()
assert mock_get_inverted_programs.called
assert not mock_get_certified_programs.called
assert not mock_revoke_program_certificate.called
@@ -908,16 +816,12 @@ class RevokeProgramCertificatesTestCase(
"""
mock_get_inverted_programs.return_value = self.inverted_programs
mock_get_certified_programs.side_effect = [[1], [1, 2]]
mock_revoke_program_certificate.side_effect = self._make_side_effect(
[Exception("boom"), None]
)
mock_revoke_program_certificate.side_effect = self._make_side_effect([Exception("boom"), None])
with mock.patch(TASKS_MODULE + ".LOGGER.info") as mock_info, mock.patch(
TASKS_MODULE + ".LOGGER.warning"
) as mock_warning:
tasks.revoke_program_certificates.delay(
self.student.username, self.course_key
).get()
tasks.revoke_program_certificates.delay(self.student.username, self.course_key).get()
assert mock_revoke_program_certificate.call_count == 3
mock_warning.assert_called_once_with(
@@ -925,12 +829,8 @@ class RevokeProgramCertificatesTestCase(
uuid=1, username=self.student.username
)
)
mock_info.assert_any_call(
f"Revoked certificate for program {1} for user {self.student.username}"
)
mock_info.assert_any_call(
f"Revoked certificate for program {2} for user {self.student.username}"
)
mock_info.assert_any_call(f"Revoked certificate for program {1} for user {self.student.username}")
mock_info.assert_any_call(f"Revoked certificate for program {2} for user {self.student.username}")
def test_retry_on_credentials_api_errors(
self,
@@ -946,12 +846,8 @@ class RevokeProgramCertificatesTestCase(
"""
mock_get_inverted_programs.return_value = self.inverted_programs
mock_get_certified_programs.return_value = [1]
mock_get_certified_programs.side_effect = self._make_side_effect(
[Exception("boom"), None]
)
tasks.revoke_program_certificates.delay(
self.student.username, self.course_key
).get()
mock_get_certified_programs.side_effect = self._make_side_effect([Exception("boom"), None])
tasks.revoke_program_certificates.delay(self.student.username, self.course_key).get()
assert mock_get_certified_programs.call_count == 2
assert mock_revoke_program_certificate.call_count == 1
@@ -968,13 +864,9 @@ class RevokeProgramCertificatesTestCase(
exception.response = mock.Mock(status_code=429)
mock_get_inverted_programs.return_value = self.inverted_programs
mock_get_certified_programs.return_value = [1, 2]
mock_revoke_program_certificate.side_effect = self._make_side_effect(
[exception, None]
)
mock_revoke_program_certificate.side_effect = self._make_side_effect([exception, None])
tasks.revoke_program_certificates.delay(
self.student.username, self.course_key
).get()
tasks.revoke_program_certificates.delay(self.student.username, self.course_key).get()
assert mock_revoke_program_certificate.call_count == 3
@@ -991,13 +883,9 @@ class RevokeProgramCertificatesTestCase(
exception.response = mock.Mock(status_code=404)
mock_get_inverted_programs.return_value = self.inverted_programs
mock_get_certified_programs.return_value = [1, 2]
mock_revoke_program_certificate.side_effect = self._make_side_effect(
[exception, None]
)
mock_revoke_program_certificate.side_effect = self._make_side_effect([exception, None])
tasks.revoke_program_certificates.delay(
self.student.username, self.course_key
).get()
tasks.revoke_program_certificates.delay(self.student.username, self.course_key).get()
assert mock_revoke_program_certificate.call_count == 2
@@ -1014,13 +902,9 @@ class RevokeProgramCertificatesTestCase(
exception.response = mock.Mock(status_code=418)
mock_get_inverted_programs.return_value = self.inverted_programs
mock_get_certified_programs.return_value = [1, 2]
mock_revoke_program_certificate.side_effect = self._make_side_effect(
[exception, None]
)
mock_revoke_program_certificate.side_effect = self._make_side_effect([exception, None])
tasks.revoke_program_certificates.delay(
self.student.username, self.course_key
).get()
tasks.revoke_program_certificates.delay(self.student.username, self.course_key).get()
assert mock_revoke_program_certificate.call_count == 2
@@ -1036,16 +920,12 @@ class RevokeProgramCertificatesTestCase(
mock_get_inverted_programs.return_value = self.inverted_programs
mock_get_certified_programs.return_value = [1, 2]
with mock.patch(
TASKS_MODULE + ".get_credentials_api_client"
) as mock_get_api_client, mock.patch(
with mock.patch(TASKS_MODULE + ".get_credentials_api_client") as mock_get_api_client, mock.patch(
TASKS_MODULE + ".LOGGER.exception"
) as mock_exception:
mock_get_api_client.side_effect = Exception("boom")
with pytest.raises(MaxRetriesExceededError):
tasks.revoke_program_certificates.delay(
self.student.username, self.course_key
).get()
tasks.revoke_program_certificates.delay(self.student.username, self.course_key).get()
assert mock_exception.called
assert mock_get_api_client.call_count == (tasks.MAX_RETRIES + 1)
assert not mock_revoke_program_certificate.called
@@ -1070,9 +950,7 @@ class UpdateCredentialsCourseCertificateConfigurationAvailableDateTestCase(TestC
self.credentials_worker = UserFactory(username="test-service-username")
def test_update_course_cert_available_date(self):
with mock.patch(
TASKS_MODULE + ".post_course_certificate_configuration"
) as update_posted:
with mock.patch(TASKS_MODULE + ".post_course_certificate_configuration") as update_posted:
tasks.update_credentials_course_certificate_configuration_available_date(
self.course_id, self.available_date
)
@@ -1080,9 +958,7 @@ class UpdateCredentialsCourseCertificateConfigurationAvailableDateTestCase(TestC
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:
with mock.patch(TASKS_MODULE + ".post_course_certificate_configuration") as update_posted:
tasks.update_credentials_course_certificate_configuration_available_date(
self.course_id, self.available_date
)
@@ -1119,9 +995,7 @@ class PostCourseCertificateConfigurationTestCase(TestCase):
available_date = datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ")
tasks.post_course_certificate_configuration(
test_client, self.certificate, available_date
)
tasks.post_course_certificate_configuration(test_client, self.certificate, available_date)
expected_body = {
"course_id": "testCourse",
@@ -1134,9 +1008,7 @@ class PostCourseCertificateConfigurationTestCase(TestCase):
@skip_unless_lms
class UpdateCertificateVisibleDatesOnCourseUpdateTestCase(
CredentialsApiConfigMixin, TestCase
):
class UpdateCertificateVisibleDatesOnCourseUpdateTestCase(CredentialsApiConfigMixin, TestCase):
"""
Tests for the `update_certificate_visible_date_on_course_update` task.
"""
@@ -1183,9 +1055,7 @@ class UpdateCertificateVisibleDatesOnCourseUpdateTestCase(
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
tasks.update_certificate_visible_date_on_course_update(self.course.id) # pylint: disable=no-value-for-parameter
def test_update_visible_dates(self):
"""
@@ -1199,20 +1069,14 @@ class UpdateCertificateVisibleDatesOnCourseUpdateTestCase(
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
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
):
class UpdateCertificateAvailableDateOnCourseUpdateTestCase(CredentialsApiConfigMixin, TestCase):
"""
Tests for the `update_certificate_available_date_on_course_update` task.
"""
@@ -1236,9 +1100,7 @@ class UpdateCertificateAvailableDateOnCourseUpdateTestCase(
course = CourseOverviewFactory.create()
with pytest.raises(MaxRetriesExceededError):
tasks.update_certificate_available_date_on_course_update(
course.id
) # pylint: disable=no-value-for-parameter
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):
"""
@@ -1251,20 +1113,14 @@ class UpdateCertificateAvailableDateOnCourseUpdateTestCase(
self.credentials_api_config.enabled = True
self.credentials_api_config.enable_learner_issuance = True
course = CourseOverviewFactory.create(
self_paced=True, certificate_available_date=None
)
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
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
)
update_credentials_course_cert_config.assert_called_once_with(str(course.id), None)
def test_update_certificate_available_date_with_instructor_paced_course(self):
"""
@@ -1288,13 +1144,9 @@ class UpdateCertificateAvailableDateOnCourseUpdateTestCase(
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
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)
)
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):
"""
@@ -1318,9 +1170,7 @@ class UpdateCertificateAvailableDateOnCourseUpdateTestCase(
)
with LogCapture(level=logging.WARNING) as log_capture:
tasks.update_certificate_available_date_on_course_update(
course.id
) # pylint: disable=no-value-for-parameter
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