MB-1167: calling course certificates api from LMS (#27552)
* [feat] calling course certificates api from LMS Now that CourseCertificates in credentials have a field for the available_date, we need to make sure we are always updating that field when it changes in studio. This PR adds a call to a new Credentials API that will update the field each time the date change signal fires.
This commit is contained in:
@@ -277,6 +277,19 @@ def award_program_certificates(self, username): # lint-amnesty, pylint: disable
|
||||
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):
|
||||
"""
|
||||
POST a configuration for a course certificate and the date the certificate
|
||||
will be available
|
||||
"""
|
||||
client.course_certificates.post({
|
||||
"course_id": cert_config['course_id'],
|
||||
"certificate_type": cert_config['mode'],
|
||||
"certificate_available_date": certificate_available_date,
|
||||
"is_active": True
|
||||
})
|
||||
|
||||
|
||||
def post_course_certificate(client, username, certificate, visible_date):
|
||||
"""
|
||||
POST a certificate that has been updated to Credentials
|
||||
@@ -298,6 +311,51 @@ def post_course_certificate(client, username, certificate, visible_date):
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=W0613
|
||||
@shared_task(bind=True, ignore_result=True)
|
||||
@set_code_owner_attribute
|
||||
def update_credentials_course_certificate_configuration_available_date(
|
||||
self,
|
||||
course_key,
|
||||
certificate_available_date=None
|
||||
):
|
||||
"""
|
||||
This task will update the course certificate configuration's available date. This is different from the
|
||||
"visable_date" attribute. This date will always either be the available date that is set in studio for
|
||||
a given course, or it will be None.
|
||||
|
||||
Arguments:
|
||||
course_run_key (str): The course run key to award the certificate for
|
||||
certificate_available_date (str): A string representation of the datetime for when to make the certificate
|
||||
available to the user. If not provided, it will be none.
|
||||
"""
|
||||
LOGGER.info(
|
||||
f"Running task update_credentials_course_certificate_configuration_available_date for course {course_key}"
|
||||
)
|
||||
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]
|
||||
if len(modes) != 1:
|
||||
LOGGER.exception(
|
||||
f'Either course {course_key} has no certificate mode or multiple modes. Task failed.'
|
||||
)
|
||||
return
|
||||
|
||||
credentials_client = get_credentials_api_client(
|
||||
User.objects.get(username=settings.CREDENTIALS_SERVICE_USERNAME),
|
||||
)
|
||||
cert_config = {
|
||||
'course_id': course_key,
|
||||
'mode': modes[0],
|
||||
}
|
||||
post_course_certificate_configuration(
|
||||
client=credentials_client,
|
||||
cert_config=cert_config,
|
||||
certificate_available_date=certificate_available_date
|
||||
)
|
||||
|
||||
|
||||
@shared_task(bind=True, ignore_result=True)
|
||||
@set_code_owner_attribute
|
||||
def award_course_certificate(self, username, course_run_key, certificate_available_date=None):
|
||||
@@ -639,7 +697,11 @@ def update_certificate_visible_date_on_course_update(self, course_key, certifica
|
||||
f"Failed to update certificate availability date for course {course_key}. Reason: {error_msg}"
|
||||
)
|
||||
raise self.retry(exc=exception, countdown=countdown, max_retries=MAX_RETRIES)
|
||||
|
||||
# Always update the course certificate with the new certificate available date
|
||||
update_credentials_course_certificate_configuration_available_date.delay(
|
||||
str(course_key),
|
||||
certificate_available_date
|
||||
)
|
||||
users_with_certificates_in_course = GeneratedCertificate.eligible_available_certificates.filter(
|
||||
course_id=course_key
|
||||
).values_list('user__username', flat=True)
|
||||
|
||||
@@ -8,9 +8,9 @@ import logging
|
||||
from datetime import datetime, timedelta
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
import ddt
|
||||
import httpretty
|
||||
import pytest
|
||||
import pytz
|
||||
from celery.exceptions import MaxRetriesExceededError
|
||||
from django.conf import settings
|
||||
@@ -19,6 +19,8 @@ from edx_rest_api_client import exceptions
|
||||
from edx_rest_api_client.client import EdxRestApiClient
|
||||
from waffle.testutils import override_switch
|
||||
|
||||
from common.djangoapps.course_modes.tests.factories import CourseModeFactory
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFactory
|
||||
from openedx.core.djangoapps.catalog.tests.mixins import CatalogIntegrationMixin
|
||||
from openedx.core.djangoapps.certificates.config import waffle
|
||||
@@ -28,7 +30,6 @@ from openedx.core.djangoapps.oauth_dispatch.tests.factories import ApplicationFa
|
||||
from openedx.core.djangoapps.programs import tasks
|
||||
from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory, SiteFactory
|
||||
from openedx.core.djangolib.testing.utils import skip_unless_lms
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -915,3 +916,80 @@ class RevokeProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiC
|
||||
assert mock_exception.called
|
||||
assert mock_get_api_client.call_count == (tasks.MAX_RETRIES + 1)
|
||||
assert not mock_revoke_program_certificate.called
|
||||
|
||||
|
||||
@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
|
||||
"""
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.course = CourseOverviewFactory.create(
|
||||
certificate_available_date=datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
)
|
||||
CourseModeFactory.create(course_id=self.course.id, mode_slug='verified')
|
||||
CourseModeFactory.create(course_id=self.course.id, mode_slug='audit')
|
||||
self.available_date = self.course.certificate_available_date
|
||||
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):
|
||||
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
|
||||
)
|
||||
update_posted.assert_called_once()
|
||||
|
||||
# pylint: disable=W0613
|
||||
def test_course_with_two_paid_modes(self, mock_client):
|
||||
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(
|
||||
self.course_id,
|
||||
self.available_date
|
||||
)
|
||||
update_posted.assert_not_called()
|
||||
|
||||
|
||||
@skip_unless_lms
|
||||
class PostCourseCertificateConfigurationTestCase(TestCase):
|
||||
"""
|
||||
Test the post_course_certificate_configuration function
|
||||
"""
|
||||
|
||||
def setUp(self): # lint-amnesty, pylint: disable=super-method-not-called
|
||||
self.certificate = {
|
||||
'mode': 'verified',
|
||||
'course_id': 'testCourse',
|
||||
}
|
||||
|
||||
@httpretty.activate
|
||||
def test_post_course_certificate_configuration(self):
|
||||
"""
|
||||
Ensure the correct API call gets made
|
||||
"""
|
||||
test_client = EdxRestApiClient('http://test-server', jwt='test-token')
|
||||
|
||||
httpretty.register_uri(
|
||||
httpretty.POST,
|
||||
'http://test-server/course_certificates/',
|
||||
)
|
||||
|
||||
available_date = datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
|
||||
tasks.post_course_certificate_configuration(test_client, self.certificate, available_date)
|
||||
|
||||
expected_body = {
|
||||
"course_id": 'testCourse',
|
||||
"certificate_type": 'verified',
|
||||
"certificate_available_date": available_date,
|
||||
"is_active": True
|
||||
}
|
||||
last_request_body = httpretty.last_request().body.decode('utf-8')
|
||||
assert json.loads(last_request_body) == expected_body
|
||||
|
||||
Reference in New Issue
Block a user