diff --git a/openedx/core/djangoapps/programs/tasks/v1/tasks.py b/openedx/core/djangoapps/programs/tasks/v1/tasks.py index cba2465766..5b8fb3e7d0 100644 --- a/openedx/core/djangoapps/programs/tasks/v1/tasks.py +++ b/openedx/core/djangoapps/programs/tasks/v1/tasks.py @@ -16,6 +16,7 @@ from openedx.core.djangoapps.certificates.api import available_date_for_certific from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.credentials.models import CredentialsApiConfig from openedx.core.djangoapps.credentials.utils import get_credentials, get_credentials_api_client +from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangoapps.programs.utils import ProgramProgressMeter LOGGER = get_task_logger(__name__) @@ -123,9 +124,13 @@ def award_program_certificates(self, username): """ LOGGER.info('Running task award_program_certificates for username %s', username) + 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 + return 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, @@ -156,6 +161,10 @@ def award_program_certificates(self, username): # Determine which program certificates the user has already been awarded, if any. existing_program_uuids = get_certified_programs(student) + # 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))) + except Exception as exc: LOGGER.exception('Failed to determine program certificates to be awarded for user %s', username) raise self.retry(exc=exc, countdown=countdown, max_retries=MAX_RETRIES) @@ -166,7 +175,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(completed_programs.keys()) - set(existing_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( diff --git a/openedx/core/djangoapps/programs/tasks/v1/tests/test_tasks.py b/openedx/core/djangoapps/programs/tasks/v1/tests/test_tasks.py index 9e5994be71..bba5fcb94f 100644 --- a/openedx/core/djangoapps/programs/tasks/v1/tests/test_tasks.py +++ b/openedx/core/djangoapps/programs/tasks/v1/tests/test_tasks.py @@ -2,6 +2,7 @@ Tests for programs celery tasks. """ import json +import logging from datetime import datetime, timedelta import ddt import httpretty @@ -21,10 +22,11 @@ from openedx.core.djangoapps.certificates.config import waffle from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin from openedx.core.djangoapps.programs.tasks.v1 import tasks -from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory +from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory, SiteConfigurationFactory from openedx.core.djangolib.testing.utils import skip_unless_lms from student.tests.factories import UserFactory +log = logging.getLogger(__name__) CREDENTIALS_INTERNAL_SERVICE_URL = 'https://credentials.example.com' TASKS_MODULE = 'openedx.core.djangoapps.programs.tasks.v1.tasks' @@ -123,7 +125,7 @@ class AwardProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiCo self.create_credentials_config() self.student = UserFactory.create(username='test-student') self.site = SiteFactory() - + self.site_configuration = SiteConfigurationFactory(site=self.site) self.catalog_integration = self.create_catalog_integration() ClientFactory.create(name='credentials') UserFactory.create(username=settings.CREDENTIALS_SERVICE_USERNAME) @@ -170,6 +172,42 @@ class AwardProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiCo 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 + @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, + mock_get_completed_programs, + mock_get_certified_programs, + mock_award_program_certificate, + ): + """ + Checks that the Credentials API is used to award certificates for + the proper programs and those program will be skipped which are provided + by 'programs_without_certificates' list in site configuration. + """ + # all completed programs + mock_get_completed_programs.return_value = {1: 1, 2: 2, 3: 3, 4: 4} + + # already awarded programs + mock_get_certified_programs.return_value = [1] + + # programs to be skipped + self.site_configuration.values = { + "programs_without_certificates": [2] + } + self.site_configuration.save() + mocked_get_current_site_configuration.return_value = self.site_configuration + + # programs which are expected to be awarded. + # (completed_programs - (already_awarded+programs + to_be_skipped_programs) + 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] + 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'), ) @@ -219,6 +257,25 @@ class AwardProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiCo self.assertFalse(mock_get_certified_programs.called) self.assertFalse(mock_award_program_certificate.called) + @mock.patch('openedx.core.djangoapps.site_configuration.helpers.get_value') + def test_programs_without_certificates( + self, + mock_get_value, + mock_get_completed_programs, + mock_get_certified_programs, + mock_award_program_certificate + ): + """ + Checks that the task will be aborted without further action if there exists a list + programs_without_certificates with ["ALL"] value in site configuration. + """ + mock_get_value.return_value = ["ALL"] + mock_get_completed_programs.return_value = {1: 1, 2: 2} + tasks.award_program_certificates.delay(self.student.username).get() + self.assertFalse(mock_get_completed_programs.called) + self.assertFalse(mock_get_certified_programs.called) + self.assertFalse(mock_award_program_certificate.called) + def _make_side_effect(self, side_effects): """ DRY helper. Returns a side effect function for use with mocks that