Send program cert visible_date

When sending a program cert to Credentials, also send along a
calculated visible_date along with it.

LEARNER-6262
This commit is contained in:
Diana Huang
2018-08-30 16:00:34 -04:00
committed by Michael Terry
parent 05debb8d54
commit 755ebc8c7f
6 changed files with 207 additions and 157 deletions

View File

@@ -29,6 +29,7 @@ MAX_RETRIES = 11
PROGRAM_CERTIFICATE = 'program'
COURSE_CERTIFICATE = 'course-run'
VISIBLE_DATE_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
def get_completed_programs(site, student):
@@ -40,11 +41,11 @@ def get_completed_programs(site, student):
student (User): Representing the student whose completed programs to check for.
Returns:
list of program UUIDs
dict of {program_UUIDs: visible_dates}
"""
meter = ProgramProgressMeter(site, student)
return meter.completed_programs
return meter.completed_programs_with_available_dates
def get_certified_programs(student):
@@ -66,7 +67,7 @@ def get_certified_programs(student):
return certified_programs
def award_program_certificate(client, username, program_uuid):
def award_program_certificate(client, username, program_uuid, visible_date):
"""
Issue a new certificate of completion to the given student for the given program.
@@ -77,6 +78,8 @@ def award_program_certificate(client, username, program_uuid):
The username of the student
program_uuid:
uuid of the completed program
visible_date:
when the program credential should be visible to user
Returns:
None
@@ -88,7 +91,12 @@ def award_program_certificate(client, username, program_uuid):
'type': PROGRAM_CERTIFICATE,
'program_uuid': program_uuid
},
'attributes': []
'attributes': [
{
'name': 'visible_date',
'value': visible_date.strftime(VISIBLE_DATE_FORMAT)
}
]
})
@@ -136,10 +144,10 @@ def award_program_certificates(self, username):
LOGGER.exception('Task award_program_certificates was called with invalid username %s', username)
# Don't retry for this case - just conclude the task.
return
program_uuids = []
completed_programs = {}
for site in Site.objects.all():
program_uuids.extend(get_completed_programs(site, student))
if not program_uuids:
completed_programs.update(get_completed_programs(site, student))
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('Task award_program_certificates was called for user %s with no completed programs', username)
@@ -158,7 +166,7 @@ def award_program_certificates(self, username):
# 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(program_uuids) - set(existing_program_uuids)))
new_program_uuids = sorted(list(set(completed_programs.keys()) - set(existing_program_uuids)))
if new_program_uuids:
try:
credentials_client = get_credentials_api_client(
@@ -171,8 +179,9 @@ def award_program_certificates(self, username):
failed_program_certificate_award_attempts = []
for program_uuid in new_program_uuids:
visible_date = completed_programs[program_uuid]
try:
award_program_certificate(credentials_client, username, program_uuid)
award_program_certificate(credentials_client, username, program_uuid, visible_date)
LOGGER.info('Awarded certificate for program %s to user %s', program_uuid, username)
except exceptions.HttpNotFoundError:
LOGGER.exception(
@@ -237,7 +246,7 @@ def post_course_certificate(client, username, certificate, visible_date):
'attributes': [
{
'name': 'visible_date',
'value': visible_date.strftime('%Y-%m-%dT%H:%M:%SZ')
'value': visible_date.strftime(VISIBLE_DATE_FORMAT)
}
]
})

View File

@@ -89,7 +89,7 @@ class AwardProgramCertificateTestCase(TestCase):
'http://test-server/credentials/',
)
tasks.award_program_certificate(test_client, test_username, 123)
tasks.award_program_certificate(test_client, test_username, 123, datetime(2010, 5, 30))
expected_body = {
'username': test_username,
@@ -97,7 +97,12 @@ class AwardProgramCertificateTestCase(TestCase):
'program_uuid': 123,
'type': tasks.PROGRAM_CERTIFICATE,
},
'attributes': []
'attributes': [
{
'name': 'visible_date',
'value': '2010-05-30T00:00:00Z',
}
]
}
self.assertEqual(json.loads(httpretty.last_request().body), expected_body)
@@ -154,7 +159,7 @@ class AwardProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiCo
Checks that the Credentials API is used to award certificates for
the proper programs.
"""
mock_get_completed_programs.return_value = [1, 2, 3]
mock_get_completed_programs.return_value = {1: 1, 2: 2, 3: 3}
mock_get_certified_programs.return_value = already_awarded_program_uuids
tasks.award_program_certificates.delay(self.student.username).get()
@@ -162,6 +167,9 @@ class AwardProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiCo
actual_program_uuids = [call[0][2] for call in mock_award_program_certificate.call_args_list]
self.assertEqual(actual_program_uuids, expected_awarded_program_uuids)
actual_visible_dates = [call[0][3] for call in mock_award_program_certificate.call_args_list]
self.assertEqual(actual_visible_dates, expected_awarded_program_uuids) # program uuids are same as mock dates
@ddt.data(
('credentials', 'enable_learner_issuance'),
)
@@ -205,7 +213,7 @@ class AwardProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiCo
Checks that the task will be aborted without further action if there
are no programs for which to award a certificate.
"""
mock_get_completed_programs.return_value = []
mock_get_completed_programs.return_value = {}
tasks.award_program_certificates.delay(self.student.username).get()
self.assertTrue(mock_get_completed_programs.called)
self.assertFalse(mock_get_certified_programs.called)
@@ -244,7 +252,7 @@ class AwardProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiCo
successfully awarded certs are logged as INFO and warning is logged
for failed requests if there are retries available.
"""
mock_get_completed_programs.return_value = [1, 2]
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])
@@ -288,7 +296,7 @@ class AwardProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiCo
transient API errors) will cause the task to be failed and queued for
retry.
"""
mock_get_completed_programs.return_value = [1, 2]
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])
tasks.award_program_certificates.delay(self.student.username).get()
@@ -306,7 +314,7 @@ class AwardProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiCo
"""
exception = exceptions.HttpClientError()
exception.response = mock.Mock(status_code=429)
mock_get_completed_programs.return_value = [1, 2]
mock_get_completed_programs.return_value = {1: 1, 2: 2}
mock_award_program_certificate.side_effect = self._make_side_effect(
[exception, None]
)
@@ -326,7 +334,7 @@ class AwardProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiCo
"""
exception = exceptions.HttpNotFoundError()
exception.response = mock.Mock(status_code=404)
mock_get_completed_programs.return_value = [1, 2]
mock_get_completed_programs.return_value = {1: 1, 2: 2}
mock_award_program_certificate.side_effect = self._make_side_effect(
[exception, None]
)
@@ -346,7 +354,7 @@ class AwardProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiCo
"""
exception = exceptions.HttpClientError()
exception.response = mock.Mock(status_code=418)
mock_get_completed_programs.return_value = [1, 2]
mock_get_completed_programs.return_value = {1: 1, 2: 2}
mock_award_program_certificate.side_effect = self._make_side_effect(
[exception, None]
)