Commit acef28c7 by Renzo Lucioni Committed by GitHub

Merge pull request #14259 from edx/ECOM-6535

ECOM-6535 Change celery task to only pull from catalog and not programs api
parents a432f15c 05b46182
......@@ -4,18 +4,15 @@ from django.test import TestCase
import mock
from edx_oauth2_provider.tests.factories import AccessTokenFactory, ClientFactory
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin
from student.tests.factories import UserFactory
class IssueProgramCertificatesViewTests(TestCase, ProgramsApiConfigMixin):
class IssueProgramCertificatesViewTests(TestCase):
password = 'password'
def setUp(self):
super(IssueProgramCertificatesViewTests, self).setUp()
self.create_programs_config()
self.path = reverse('support:programs-certify')
self.user = UserFactory(password=self.password, is_staff=True)
self.data = {'username': self.user.username}
......@@ -57,12 +54,6 @@ class IssueProgramCertificatesViewTests(TestCase, ProgramsApiConfigMixin):
self._verify_response(403)
def test_certification_disabled(self):
"""Verify that the endpoint returns a 400 when program certification is disabled."""
self.create_programs_config(enable_certification=False)
self._verify_response(400)
def test_username_required(self):
"""Verify that the endpoint returns a 400 when a username isn't provided."""
self.data.pop('username')
......
......@@ -6,7 +6,6 @@ from rest_framework.authentication import SessionAuthentication
from rest_framework.response import Response
from rest_framework_oauth.authentication import OAuth2Authentication
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.programs.tasks.v1.tasks import award_program_certificates
......@@ -38,12 +37,6 @@ class IssueProgramCertificatesView(views.APIView):
permission_classes = (permissions.IsAuthenticated, permissions.IsAdminUser,)
def post(self, request):
if not ProgramsApiConfig.current().is_certification_enabled:
return Response(
{'error': 'Program certification is disabled.'},
status=status.HTTP_400_BAD_REQUEST
)
username = request.data.get('username')
if username:
log.info('Enqueuing program certification task for user [%s]', username)
......
......@@ -9,6 +9,7 @@ class CatalogIntegrationMixin(object):
'enabled': True,
'internal_api_url': 'https://catalog-internal.example.com/api/v1/',
'cache_ttl': 0,
'service_username': 'lms_catalog_service_user'
}
def create_catalog_integration(self, **kwargs):
......
......@@ -151,7 +151,7 @@ def munge_catalog_program(catalog_program):
# The Programs schema only supports one organization here.
'display_name': course['owners'][0]['name'],
'key': course['owners'][0]['key']
},
} if course['owners'] else {},
'run_modes': [
{
'course_key': run['key'],
......
......@@ -9,6 +9,8 @@ from django.db import models
from config_models.models import ConfigurationModel
API_VERSION = 'v2'
class CredentialsApiConfig(ConfigurationModel):
"""
......@@ -55,14 +57,14 @@ class CredentialsApiConfig(ConfigurationModel):
"""
Generate a URL based on internal service URL and API version number.
"""
return urljoin(self.internal_service_url, '/api/v1/')
return urljoin(self.internal_service_url, '/api/{}/'.format(API_VERSION))
@property
def public_api_url(self):
"""
Generate a URL based on public service URL and API version number.
"""
return urljoin(self.public_service_url, '/api/v1/')
return urljoin(self.public_service_url, '/api/{}/'.format(API_VERSION))
@property
def is_learner_issuance_enabled(self):
......
"""Factories for generating fake credentials-related data."""
import uuid
import factory
from factory.fuzzy import FuzzyText
......@@ -26,7 +28,7 @@ class ProgramCredential(factory.Factory):
model = dict
credential_id = factory.Sequence(lambda n: n)
program_id = factory.Sequence(lambda n: n)
program_uuid = factory.LazyAttribute(lambda obj: str(uuid.uuid4()))
class CourseCredential(factory.Factory):
......
......@@ -37,16 +37,12 @@ class CredentialsDataMixin(object):
factories.UserCredential(
id=1,
username='test',
credential=factories.ProgramCredential(
program_id=1
)
credential=factories.ProgramCredential()
),
factories.UserCredential(
id=2,
username='test',
credential=factories.ProgramCredential(
program_id=2
)
credential=factories.ProgramCredential()
),
factories.UserCredential(
id=3,
......@@ -99,8 +95,7 @@ class CredentialsDataMixin(object):
self.assertTrue(httpretty.is_enabled(), msg='httpretty must be enabled to mock Credentials API calls.')
internal_api_url = CredentialsApiConfig.current().internal_api_url.strip('/')
url = internal_api_url + '/user_credentials/?username=' + user.username
url = internal_api_url + '/credentials/?status=awarded&username=' + user.username
if reset_url:
httpretty.reset()
......@@ -110,7 +105,7 @@ class CredentialsDataMixin(object):
body = json.dumps(data)
if is_next_page:
next_page_url = internal_api_url + '/user_credentials/?page=2&username=' + user.username
next_page_url = internal_api_url + '/credentials/?page=2&status=awarded&username=' + user.username
self.CREDENTIALS_NEXT_API_RESPONSE['next'] = next_page_url
next_page_body = json.dumps(self.CREDENTIALS_NEXT_API_RESPONSE)
httpretty.register_uri(
......
......@@ -17,11 +17,11 @@ class TestCredentialsApiConfig(CredentialsApiConfigMixin, TestCase):
self.assertEqual(
credentials_config.internal_api_url,
credentials_config.internal_service_url.strip('/') + '/api/v1/')
credentials_config.internal_service_url.strip('/') + '/api/v2/')
self.assertEqual(
credentials_config.public_api_url,
credentials_config.public_service_url.strip('/') + '/api/v1/')
credentials_config.public_service_url.strip('/') + '/api/v2/')
def test_is_learner_issuance_enabled(self):
"""
......
......@@ -2,8 +2,8 @@
from __future__ import unicode_literals
import logging
from openedx.core.djangoapps.catalog.utils import get_programs
from openedx.core.djangoapps.credentials.models import CredentialsApiConfig
from openedx.core.djangoapps.programs.utils import get_programs_for_credentials
from openedx.core.lib.edx_api_utils import get_edx_api_data
......@@ -19,18 +19,41 @@ def get_user_credentials(user):
service.
"""
credential_configuration = CredentialsApiConfig.current()
user_query = {'username': user.username}
user_query = {'status': 'awarded', 'username': user.username}
# Bypass caching for staff users, who may be generating credentials and
# want to see them displayed immediately.
use_cache = credential_configuration.is_cache_enabled and not user.is_staff
cache_key = credential_configuration.CACHE_KEY + '.' + user.username if use_cache else None
credentials = get_edx_api_data(
credential_configuration, user, 'user_credentials', querystring=user_query, cache_key=cache_key
credential_configuration, user, 'credentials', querystring=user_query, cache_key=cache_key
)
return credentials
def get_programs_for_credentials(user, programs_credentials):
""" Given a user and an iterable of credentials, get corresponding programs
data and return it as a list of dictionaries.
Arguments:
user (User): The user to authenticate as for requesting programs.
programs_credentials (list): List of credentials awarded to the user
for completion of a program.
Returns:
list, containing programs dictionaries.
"""
certified_programs = []
programs = get_programs(user)
for program in programs:
for credential in programs_credentials:
if program['uuid'] == credential['credential']['program_uuid']:
program['credential_url'] = credential['certificate_url']
certified_programs.append(program)
return certified_programs
def get_user_program_credentials(user):
"""Given a user, get the list of all program credentials earned and returns
list of dictionaries containing related programs data.
......@@ -55,7 +78,7 @@ def get_user_program_credentials(user):
programs_credentials = []
for credential in credentials:
try:
if 'program_id' in credential['credential'] and credential['status'] == 'awarded':
if 'program_uuid' in credential['credential']:
programs_credentials.append(credential)
except KeyError:
log.exception('Invalid credential structure: %r', credential)
......@@ -84,7 +107,7 @@ def get_programs_credentials(user):
for program in programs_credentials:
try:
program_data = {
'display_name': program['name'],
'display_name': program['title'],
'subtitle': program['subtitle'],
'credential_url': program['credential_url'],
}
......
......@@ -2,15 +2,16 @@
from collections import namedtuple
import logging
from django.contrib.auth.models import User
from django.core.management import BaseCommand, CommandError
from django.db.models import Q
from opaque_keys.edx.keys import CourseKey
from provider.oauth2.models import Client
from certificates.models import GeneratedCertificate, CertificateStatuses # pylint: disable=import-error
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.catalog.models import CatalogIntegration
from openedx.core.djangoapps.programs.tasks.v1.tasks import award_program_certificates
from openedx.core.djangoapps.programs.utils import get_programs
from openedx.core.djangoapps.catalog.utils import get_programs
# TODO: Log to console, even with debug mode disabled?
......@@ -39,20 +40,17 @@ class Command(BaseCommand):
)
def handle(self, *args, **options):
programs_config = ProgramsApiConfig.current()
self.client = Client.objects.get(name=programs_config.OAUTH2_CLIENT_NAME)
if self.client.user is None:
msg = (
'No user is associated with the {} OAuth2 client. '
'A service user is necessary to make requests to the Programs API. '
'No tasks have been enqueued. '
'Associate a user with the client and try again.'
).format(programs_config.OAUTH2_CLIENT_NAME)
raise CommandError(msg)
catalog_config = CatalogIntegration.current()
try:
user = User.objects.get(username=catalog_config.service_username)
except:
raise CommandError(
'User with username [{}] not found. '
'A service user is required to run this command.'.format(catalog_config.service_username)
)
self._load_run_modes()
self._load_run_modes(user)
logger.info('Looking for users who may be eligible for a program certificate.')
......@@ -85,9 +83,9 @@ class Command(BaseCommand):
failed
)
def _load_run_modes(self):
def _load_run_modes(self, user):
"""Find all run modes which are part of a program."""
programs = get_programs(self.client.user)
programs = get_programs(user)
self.run_modes = self._flatten(programs)
def _flatten(self, programs):
......
......@@ -35,10 +35,11 @@ def handle_course_cert_awarded(sender, user, course_key, mode, status, **kwargs)
"""
# Import here instead of top of file since this module gets imported before
# the programs app is loaded, resulting in a Django deprecation warning.
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
# the credentials app is loaded, resulting in a Django deprecation warning.
from openedx.core.djangoapps.credentials.models import CredentialsApiConfig
if not ProgramsApiConfig.current().is_certification_enabled:
# Avoid scheduling new tasks if certification is disabled.
if not CredentialsApiConfig.current().is_learner_issuance_enabled:
return
# schedule background task to process
......
......@@ -11,7 +11,6 @@ from provider.oauth2.models import Client
from openedx.core.djangoapps.credentials.models import CredentialsApiConfig
from openedx.core.djangoapps.credentials.utils import get_user_credentials
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.programs.utils import ProgramProgressMeter
from openedx.core.lib.token_utils import JwtBuilder
......@@ -19,6 +18,11 @@ from openedx.core.lib.token_utils import JwtBuilder
LOGGER = get_task_logger(__name__)
# Under cms the following setting is not defined, leading to errors during tests.
ROUTING_KEY = getattr(settings, 'CREDENTIALS_GENERATION_ROUTING_KEY', None)
# Maximum number of retries before giving up on awarding credentials.
# For reference, 11 retries with exponential backoff yields a maximum waiting
# time of 2047 seconds (about 30 minutes). Setting this to None could yield
# unwanted behavior: infinite retries.
MAX_RETRIES = 11
def get_api_client(api_config, student):
......@@ -26,7 +30,7 @@ def get_api_client(api_config, student):
Create and configure an API client for authenticated HTTP requests.
Args:
api_config: ProgramsApiConfig or CredentialsApiConfig object
api_config: CredentialsApiConfig object
student: User object as whom to authenticate to the API
Returns:
......@@ -58,17 +62,16 @@ def get_completed_programs(student):
student (User): Representing the student whose completed programs to check for.
Returns:
list of program ids
list of program UUIDs
"""
meter = ProgramProgressMeter(student)
meter = ProgramProgressMeter(student, use_catalog=True)
return meter.completed_programs
def get_awarded_certificate_programs(student):
def get_certified_programs(student):
"""
Find the ids of all the programs for which the student has already been awarded
Find the UUIDs of all the programs for which the student has already been awarded
a certificate.
Args:
......@@ -76,17 +79,17 @@ def get_awarded_certificate_programs(student):
User object representing the student
Returns:
ids of the programs for which the student has been awarded a certificate
UUIDs of the programs for which the student has been awarded a certificate
"""
return [
credential['credential']['program_id']
for credential in get_user_credentials(student)
if 'program_id' in credential['credential'] and credential['status'] == 'awarded'
]
certified_programs = []
for credential in get_user_credentials(student):
if 'program_uuid' in credential['credential']:
certified_programs.append(credential['credential']['program_uuid'])
return certified_programs
def award_program_certificate(client, username, program_id):
def award_program_certificate(client, username, program_uuid):
"""
Issue a new certificate of completion to the given student for the given program.
......@@ -95,16 +98,16 @@ def award_program_certificate(client, username, program_id):
credentials API client (EdxRestApiClient)
username:
The username of the student
program_id:
id of the completed program
program_uuid:
uuid of the completed program
Returns:
None
"""
client.user_credentials.post({
client.credentials.post({
'username': username,
'credential': {'program_id': program_id},
'credential': {'program_uuid': program_uuid},
'attributes': []
})
......@@ -134,24 +137,18 @@ def award_program_certificates(self, username):
"""
LOGGER.info('Running task award_program_certificates for username %s', username)
config = ProgramsApiConfig.current()
countdown = 2 ** self.request.retries
# If either programs or credentials config models are disabled for this
# 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 not config.is_certification_enabled:
LOGGER.warning(
'Task award_program_certificates cannot be executed when program certification is disabled in API config',
)
raise self.retry(countdown=countdown, max_retries=config.max_retries)
if not CredentialsApiConfig.current().is_learner_issuance_enabled:
LOGGER.warning(
'Task award_program_certificates cannot be executed when credentials issuance is disabled in API config',
)
raise self.retry(countdown=countdown, max_retries=config.max_retries)
raise self.retry(countdown=countdown, max_retries=MAX_RETRIES)
try:
try:
......@@ -161,8 +158,8 @@ def award_program_certificates(self, username):
# Don't retry for this case - just conclude the task.
return
program_ids = get_completed_programs(student)
if not program_ids:
program_uuids = get_completed_programs(student)
if not program_uuids:
# 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)
......@@ -170,11 +167,11 @@ def award_program_certificates(self, username):
# Determine which program certificates the user has already been
# awarded, if any.
existing_program_ids = get_awarded_certificate_programs(student)
existing_program_uuids = get_certified_programs(student)
except Exception as exc: # pylint: disable=broad-except
LOGGER.exception('Failed to determine program certificates to be awarded for user %s', username)
raise self.retry(exc=exc, countdown=countdown, max_retries=config.max_retries)
raise self.retry(exc=exc, countdown=countdown, max_retries=MAX_RETRIES)
# For each completed program for which the student doesn't already have a
# certificate, award one now.
......@@ -182,8 +179,8 @@ 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_ids = sorted(list(set(program_ids) - set(existing_program_ids)))
if new_program_ids:
new_program_uuids = sorted(list(set(program_uuids) - set(existing_program_uuids)))
if new_program_uuids:
try:
credentials_client = get_api_client(
CredentialsApiConfig.current(),
......@@ -192,22 +189,22 @@ def award_program_certificates(self, username):
except Exception as exc: # pylint: disable=broad-except
LOGGER.exception('Failed to create a credentials API client to award program certificates')
# Retry because a misconfiguration could be fixed
raise self.retry(exc=exc, countdown=countdown, max_retries=config.max_retries)
raise self.retry(exc=exc, countdown=countdown, max_retries=MAX_RETRIES)
retry = False
for program_id in new_program_ids:
for program_uuid in new_program_uuids:
try:
award_program_certificate(credentials_client, username, program_id)
LOGGER.info('Awarded certificate for program %s to user %s', program_id, username)
award_program_certificate(credentials_client, username, program_uuid)
LOGGER.info('Awarded certificate for program %s to user %s', program_uuid, username)
except Exception: # pylint: disable=broad-except
# keep trying to award other certs, but retry the whole task to fix any missing entries
LOGGER.exception('Failed to award certificate for program %s to user %s', program_id, username)
LOGGER.exception('Failed to award certificate for program %s to user %s', program_uuid, username)
retry = True
if retry:
# N.B. This logic assumes that this task is idempotent
LOGGER.info('Retrying task to award failed certificates to user %s', username)
raise self.retry(countdown=countdown, max_retries=config.max_retries)
raise self.retry(countdown=countdown, max_retries=MAX_RETRIES)
else:
LOGGER.info('User %s is not eligible for any new program certificates', username)
......
......@@ -4,17 +4,15 @@ import json
import ddt
from django.core.management import call_command, CommandError
from django.test import TestCase
from edx_oauth2_provider.tests.factories import ClientFactory
import httpretty
import mock
from provider.constants import CONFIDENTIAL
from certificates.models import CertificateStatuses # pylint: disable=import-error
from lms.djangoapps.certificates.api import MODES
from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFactory
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.programs.tests import factories
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin
from openedx.core.djangoapps.catalog.tests.mixins import CatalogIntegrationMixin
from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin
from openedx.core.djangolib.testing.utils import skip_unless_lms
from student.tests.factories import UserFactory
......@@ -26,7 +24,7 @@ COMMAND_MODULE = 'openedx.core.djangoapps.programs.management.commands.backpopul
@httpretty.activate
@mock.patch(COMMAND_MODULE + '.award_program_certificates.delay')
@skip_unless_lms
class BackpopulateProgramCredentialsTests(ProgramsApiConfigMixin, TestCase):
class BackpopulateProgramCredentialsTests(CatalogIntegrationMixin, CredentialsApiConfigMixin, TestCase):
"""Tests for the backpopulate_program_credentials management command."""
course_id, alternate_course_id = 'org/course/run', 'org/alternate/run'
......@@ -35,24 +33,20 @@ class BackpopulateProgramCredentialsTests(ProgramsApiConfigMixin, TestCase):
self.alice = UserFactory()
self.bob = UserFactory()
self.oauth2_user = UserFactory()
self.oauth2_client = ClientFactory(name=ProgramsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL)
# Disable certification to prevent the task from being triggered when
# setting up test data (i.e., certificates with a passing status), thereby
# skewing mock call counts.
self.create_programs_config(enable_certification=False)
self.create_credentials_config(enable_learner_issuance=False)
def _link_oauth2_user(self):
"""Helper to link user and OAuth2 client."""
self.oauth2_client.user = self.oauth2_user
self.oauth2_client.save() # pylint: disable=no-member
self.catalog_integration = self.create_catalog_integration()
self.service_user = UserFactory(username=self.catalog_integration.service_username)
def _mock_programs_api(self, data):
"""Helper for mocking out Programs API URLs."""
self.assertTrue(httpretty.is_enabled(), msg='httpretty must be enabled to mock Programs API calls.')
"""Helper for mocking out Catalog API URLs."""
self.assertTrue(httpretty.is_enabled(), msg='httpretty must be enabled to mock Catalog API calls.')
url = ProgramsApiConfig.current().internal_api_url.strip('/') + '/programs/'
url = self.catalog_integration.internal_api_url.strip('/') + '/programs/'
body = json.dumps({'results': data})
httpretty.register_uri(httpretty.GET, url, body=body, content_type='application/json')
......@@ -71,7 +65,6 @@ class BackpopulateProgramCredentialsTests(ProgramsApiConfigMixin, TestCase):
),
]
self._mock_programs_api(data)
self._link_oauth2_user()
GeneratedCertificateFactory(
user=self.alice,
......@@ -141,7 +134,6 @@ class BackpopulateProgramCredentialsTests(ProgramsApiConfigMixin, TestCase):
def test_handle_flatten(self, data, mock_task):
"""Verify that program structures are flattened correctly."""
self._mock_programs_api(data)
self._link_oauth2_user()
GeneratedCertificateFactory(
user=self.alice,
......@@ -179,7 +171,6 @@ class BackpopulateProgramCredentialsTests(ProgramsApiConfigMixin, TestCase):
),
]
self._mock_programs_api(data)
self._link_oauth2_user()
GeneratedCertificateFactory(
user=self.alice,
......@@ -215,7 +206,6 @@ class BackpopulateProgramCredentialsTests(ProgramsApiConfigMixin, TestCase):
),
]
self._mock_programs_api(data)
self._link_oauth2_user()
GeneratedCertificateFactory(
user=self.alice,
......@@ -248,7 +238,6 @@ class BackpopulateProgramCredentialsTests(ProgramsApiConfigMixin, TestCase):
),
]
self._mock_programs_api(data)
self._link_oauth2_user()
passing_status = CertificateStatuses.downloadable
failing_status = CertificateStatuses.notpassing
......@@ -276,27 +265,10 @@ class BackpopulateProgramCredentialsTests(ProgramsApiConfigMixin, TestCase):
mock_task.assert_called_once_with(self.alice.username)
def test_handle_unlinked_oauth2_user(self, mock_task):
"""Verify that the command fails when no user is associated with the OAuth2 client."""
data = [
factories.Program(
organizations=[factories.Organization()],
course_codes=[
factories.CourseCode(run_modes=[
factories.RunMode(course_key=self.course_id),
]),
]
),
]
self._mock_programs_api(data)
GeneratedCertificateFactory(
user=self.alice,
course_id=self.course_id,
mode=MODES.verified,
status=CertificateStatuses.downloadable,
)
def test_handle_missing_service_user(self, mock_task):
"""Verify that the command fails when no service user exists."""
self.catalog_integration = self.create_catalog_integration(service_username='test')
with self.assertRaises(CommandError):
call_command('backpopulate_program_credentials')
......@@ -323,7 +295,6 @@ class BackpopulateProgramCredentialsTests(ProgramsApiConfigMixin, TestCase):
),
]
self._mock_programs_api(data)
self._link_oauth2_user()
GeneratedCertificateFactory(
user=self.alice,
......
......@@ -10,15 +10,18 @@ from student.tests.factories import UserFactory
from openedx.core.djangoapps.signals.signals import COURSE_CERT_AWARDED
from openedx.core.djangoapps.programs.signals import handle_course_cert_awarded
from openedx.core.djangolib.testing.utils import skip_unless_lms
TEST_USERNAME = 'test-user'
@attr(shard=2)
# The credentials app isn't installed for the CMS.
@skip_unless_lms
@mock.patch('openedx.core.djangoapps.programs.tasks.v1.tasks.award_program_certificates.delay')
@mock.patch(
'openedx.core.djangoapps.programs.models.ProgramsApiConfig.is_certification_enabled',
'openedx.core.djangoapps.credentials.models.CredentialsApiConfig.is_learner_issuance_enabled',
new_callable=mock.PropertyMock,
return_value=False,
)
......@@ -40,7 +43,7 @@ class CertAwardedReceiverTest(TestCase):
status='test-status',
)
def test_signal_received(self, mock_is_certification_enabled, mock_task): # pylint: disable=unused-argument
def test_signal_received(self, mock_is_learner_issuance_enabled, mock_task): # pylint: disable=unused-argument
"""
Ensures the receiver function is invoked when COURSE_CERT_AWARDED is
sent.
......@@ -50,24 +53,26 @@ class CertAwardedReceiverTest(TestCase):
known to take place inside the function.
"""
COURSE_CERT_AWARDED.send(**self.signal_kwargs)
self.assertEqual(mock_is_certification_enabled.call_count, 1)
self.assertEqual(mock_is_learner_issuance_enabled.call_count, 1)
def test_programs_disabled(self, mock_is_certification_enabled, mock_task):
def test_programs_disabled(self, mock_is_learner_issuance_enabled, mock_task):
"""
Ensures that the receiver function does nothing when the programs API
Ensures that the receiver function does nothing when the credentials API
configuration is not enabled.
"""
handle_course_cert_awarded(**self.signal_kwargs)
self.assertEqual(mock_is_certification_enabled.call_count, 1)
self.assertEqual(mock_is_learner_issuance_enabled.call_count, 1)
self.assertEqual(mock_task.call_count, 0)
def test_programs_enabled(self, mock_is_certification_enabled, mock_task):
def test_programs_enabled(self, mock_is_learner_issuance_enabled, mock_task):
"""
Ensures that the receiver function invokes the expected celery task
when the programs API configuration is enabled.
when the credentials API configuration is enabled.
"""
mock_is_certification_enabled.return_value = True
mock_is_learner_issuance_enabled.return_value = True
handle_course_cert_awarded(**self.signal_kwargs)
self.assertEqual(mock_is_certification_enabled.call_count, 1)
self.assertEqual(mock_is_learner_issuance_enabled.call_count, 1)
self.assertEqual(mock_task.call_count, 1)
self.assertEqual(mock_task.call_args[0], (TEST_USERNAME,))
......@@ -21,7 +21,6 @@ from pytz import utc
from lms.djangoapps.certificates.api import MODES
from lms.djangoapps.commerce.tests.test_utils import update_commerce_config
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.credentials.tests import factories as credentials_factories
from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin, CredentialsDataMixin
from openedx.core.djangoapps.programs import utils
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
......@@ -58,27 +57,6 @@ class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin, Credential
cache.clear()
def _expected_progam_credentials_data(self):
"""
Dry method for getting expected program credentials response data.
"""
return [
credentials_factories.UserCredential(
id=1,
username='test',
credential=credentials_factories.ProgramCredential(
program_id=1
)
),
credentials_factories.UserCredential(
id=2,
username='test',
credential=credentials_factories.ProgramCredential(
program_id=2
)
)
]
def test_get_programs(self):
"""Verify programs data can be retrieved."""
self.create_programs_config()
......@@ -141,52 +119,6 @@ class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin, Credential
actual = utils.get_programs(self.user)
self.assertEqual(actual, [])
def test_get_program_for_certificates(self):
"""Verify programs data can be retrieved and parsed correctly for certificates."""
self.create_programs_config()
self.mock_programs_api()
program_credentials_data = self._expected_progam_credentials_data()
actual = utils.get_programs_for_credentials(self.user, program_credentials_data)
expected = self.PROGRAMS_API_RESPONSE['results'][:2]
expected[0]['credential_url'] = program_credentials_data[0]['certificate_url']
expected[1]['credential_url'] = program_credentials_data[1]['certificate_url']
self.assertEqual(len(actual), 2)
self.assertEqual(actual, expected)
def test_get_program_for_certificates_no_data(self):
"""Verify behavior when no programs data is found for the user."""
self.create_programs_config()
self.create_credentials_config()
self.mock_programs_api(data={'results': []})
program_credentials_data = self._expected_progam_credentials_data()
actual = utils.get_programs_for_credentials(self.user, program_credentials_data)
self.assertEqual(actual, [])
def test_get_program_for_certificates_id_not_exist(self):
"""Verify behavior when no program with the given program_id in
credentials exists.
"""
self.create_programs_config()
self.create_credentials_config()
self.mock_programs_api()
credential_data = [
{
"id": 1,
"username": "test",
"credential": {
"credential_id": 1,
"program_id": 100
},
"status": "awarded",
"credential_url": "www.example.com"
}
]
actual = utils.get_programs_for_credentials(self.user, credential_data)
self.assertEqual(actual, [])
@skip_unless_lms
class GetProgramsByRunTests(TestCase):
......
# -*- coding: utf-8 -*-
"""Helper functions for working with Programs."""
import datetime
import logging
from urlparse import urljoin
from django.conf import settings
......@@ -26,13 +25,11 @@ from util.date_utils import strftime_localized
from util.organizations_helpers import get_organization_by_short_name
log = logging.getLogger(__name__)
# The datetime module's strftime() methods require a year >= 1900.
DEFAULT_ENROLLMENT_START_DATE = datetime.datetime(1900, 1, 1, tzinfo=utc)
def get_programs(user, program_id=None):
def get_programs(user, program_id=None, use_catalog=False):
"""Given a user, get programs from the Programs service.
Returned value is cached depending on user permissions. Staff users making requests
......@@ -49,49 +46,25 @@ def get_programs(user, program_id=None):
list of dict, representing programs returned by the Programs service.
dict, if a specific program is requested.
"""
programs_config = ProgramsApiConfig.current()
# Bypass caching for staff users, who may be creating Programs and want
# to see them displayed immediately.
cache_key = programs_config.CACHE_KEY if programs_config.is_cache_enabled and not user.is_staff else None
programs = get_edx_api_data(programs_config, user, 'programs', resource_id=program_id, cache_key=cache_key)
# Mix in munged MicroMasters data from the catalog.
if not program_id:
programs += [
munge_catalog_program(micromaster) for micromaster in get_catalog_programs(user, type='MicroMasters')
]
return programs
if use_catalog:
programs = [munge_catalog_program(program) for program in get_catalog_programs(user)]
else:
programs_config = ProgramsApiConfig.current()
# Bypass caching for staff users, who may be creating Programs and want
# to see them displayed immediately.
cache_key = programs_config.CACHE_KEY if programs_config.is_cache_enabled and not user.is_staff else None
def get_programs_for_credentials(user, programs_credentials):
""" Given a user and an iterable of credentials, get corresponding programs
data and return it as a list of dictionaries.
programs = get_edx_api_data(programs_config, user, 'programs', resource_id=program_id, cache_key=cache_key)
Arguments:
user (User): The user to authenticate as for requesting programs.
programs_credentials (list): List of credentials awarded to the user
for completion of a program.
# Mix in munged MicroMasters data from the catalog.
if not program_id:
programs += [
munge_catalog_program(micromaster) for micromaster in get_catalog_programs(user, type='MicroMasters')
]
Returns:
list, containing programs dictionaries.
"""
certificate_programs = []
programs = get_programs(user)
if not programs:
log.debug('No programs for user %d.', user.id)
return certificate_programs
for program in programs:
for credential in programs_credentials:
if program['id'] == credential['credential']['program_id']:
program['credential_url'] = credential['certificate_url']
certificate_programs.append(program)
return certificate_programs
return programs
def get_programs_by_run(programs, enrollments):
......@@ -147,7 +120,6 @@ def attach_program_detail_url(programs):
for program in programs:
base = reverse('program_details_view', kwargs={'program_id': program['id']}).rstrip('/')
slug = slugify(program['name'])
program['detail_url'] = '{base}/{slug}'.format(base=base, slug=slug)
return programs
......@@ -182,13 +154,14 @@ class ProgramProgressMeter(object):
Keyword Arguments:
enrollments (list): List of the user's enrollments.
"""
def __init__(self, user, enrollments=None):
def __init__(self, user, enrollments=None, use_catalog=False):
self.user = user
self.enrollments = enrollments
self.course_ids = None
self.course_certs = None
self.use_catalog = use_catalog
self.programs = attach_program_detail_url(get_programs(self.user))
self.programs = attach_program_detail_url(get_programs(self.user, use_catalog=use_catalog))
def engaged_programs(self, by_run=False):
"""Derive a list of programs in which the given user is engaged.
......@@ -279,7 +252,6 @@ class ProgramProgressMeter(object):
bool, whether the course code is complete.
"""
self.course_certs = self.course_certs or get_completed_courses(self.user)
return any(self._parse(run_mode) in self.course_certs for run_mode in course_code['run_modes'])
def _is_course_code_in_progress(self, course_code):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment