From 9efa7770f8c06bc37174e1a7da3503eb761953d6 Mon Sep 17 00:00:00 2001 From: Clinton Blackburn Date: Fri, 21 Jul 2017 14:43:31 -0400 Subject: [PATCH] Resolved multi-tenant program issues A site must be passed so the system knows which URL to use to contact the Discovery Service. --- common/djangoapps/student/views.py | 2 +- lms/djangoapps/courseware/views/views.py | 2 +- lms/djangoapps/learner_dashboard/views.py | 4 +- .../djangoapps/catalog/tests/test_utils.py | 27 +++++++---- openedx/core/djangoapps/catalog/utils.py | 15 ++++-- .../backpopulate_program_credentials.py | 13 ++++-- .../djangoapps/programs/tasks/v1/tasks.py | 19 ++++---- .../programs/tasks/v1/tests/test_tasks.py | 6 ++- .../djangoapps/programs/tests/test_signals.py | 4 +- .../djangoapps/programs/tests/test_utils.py | 46 ++++++++++--------- openedx/core/djangoapps/programs/utils.py | 7 +-- .../site_configuration/tests/factories.py | 2 + 12 files changed, 85 insertions(+), 62 deletions(-) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 8b49a5e735..e4799a44d4 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -739,7 +739,7 @@ def dashboard(request): # Find programs associated with course runs being displayed. This information # is passed in the template context to allow rendering of program-related # information on the dashboard. - meter = ProgramProgressMeter(user, enrollments=course_enrollments) + meter = ProgramProgressMeter(request.site, user, enrollments=course_enrollments) inverted_programs = meter.invert_programs() # Construct a dictionary of course mode information diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index 1e2e5bda0e..b6e54558a3 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -838,7 +838,7 @@ def program_marketing(request, program_uuid): """ Display the program marketing page. """ - program_data = get_programs(uuid=program_uuid) + program_data = get_programs(request.site, uuid=program_uuid) if not program_data: raise Http404 diff --git a/lms/djangoapps/learner_dashboard/views.py b/lms/djangoapps/learner_dashboard/views.py index e54ab41236..e166ee1ea0 100644 --- a/lms/djangoapps/learner_dashboard/views.py +++ b/lms/djangoapps/learner_dashboard/views.py @@ -25,7 +25,7 @@ def program_listing(request): if not programs_config.enabled: raise Http404 - meter = ProgramProgressMeter(request.user) + meter = ProgramProgressMeter(request.site, request.user) context = { 'disable_courseware_js': True, @@ -48,7 +48,7 @@ def program_details(request, program_uuid): if not programs_config.enabled: raise Http404 - meter = ProgramProgressMeter(request.user, uuid=program_uuid) + meter = ProgramProgressMeter(request.site, request.user, uuid=program_uuid) program_data = meter.programs[0] if not program_data: diff --git a/openedx/core/djangoapps/catalog/tests/test_utils.py b/openedx/core/djangoapps/catalog/tests/test_utils.py index a27bca56e2..d02166a658 100644 --- a/openedx/core/djangoapps/catalog/tests/test_utils.py +++ b/openedx/core/djangoapps/catalog/tests/test_utils.py @@ -1,13 +1,13 @@ """Tests covering utilities for integrating with the catalog service.""" # pylint: disable=missing-docstring import copy -import uuid import ddt import mock from django.contrib.auth import get_user_model from django.core.cache import cache from django.test import TestCase, override_settings +from student.tests.factories import UserFactory from openedx.core.djangoapps.catalog.cache import PROGRAM_CACHE_KEY_TPL, PROGRAM_UUIDS_CACHE_KEY from openedx.core.djangoapps.catalog.models import CatalogIntegration @@ -19,8 +19,8 @@ from openedx.core.djangoapps.catalog.utils import ( get_programs, get_programs_with_type ) +from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_unless_lms -from student.tests.factories import UserFactory UTILS_MODULE = 'openedx.core.djangoapps.catalog.utils' User = get_user_model() # pylint: disable=invalid-name @@ -32,6 +32,10 @@ User = get_user_model() # pylint: disable=invalid-name class TestGetPrograms(CacheIsolationTestCase): ENABLED_CACHES = ['default'] + def setUp(self): + super(TestGetPrograms, self).setUp() + self.site = SiteFactory() + def test_get_many(self, mock_warning, mock_info): programs = ProgramFactory.create_batch(3) @@ -43,7 +47,7 @@ class TestGetPrograms(CacheIsolationTestCase): # When called before UUIDs are cached, the function should return an # empty list and log a warning. - self.assertEqual(get_programs(), []) + self.assertEqual(get_programs(self.site), []) mock_warning.assert_called_once_with('Failed to get program UUIDs from the cache.') mock_warning.reset_mock() @@ -54,7 +58,7 @@ class TestGetPrograms(CacheIsolationTestCase): None ) - actual_programs = get_programs() + actual_programs = get_programs(self.site) # The 2 cached programs should be returned while info and warning # messages should be logged for the missing one. @@ -82,7 +86,7 @@ class TestGetPrograms(CacheIsolationTestCase): } cache.set_many(all_programs, None) - actual_programs = get_programs() + actual_programs = get_programs(self.site) # All 3 programs should be returned. self.assertEqual( @@ -116,7 +120,7 @@ class TestGetPrograms(CacheIsolationTestCase): mock_cache.get.return_value = [program['uuid'] for program in programs] mock_cache.get_many.side_effect = fake_get_many - actual_programs = get_programs() + actual_programs = get_programs(self.site) # All 3 cached programs should be returned. An info message should be # logged about the one that was initially missing, but the code should @@ -136,7 +140,7 @@ class TestGetPrograms(CacheIsolationTestCase): expected_program = ProgramFactory() expected_uuid = expected_program['uuid'] - self.assertEqual(get_programs(uuid=expected_uuid), None) + self.assertEqual(get_programs(self.site, uuid=expected_uuid), None) mock_warning.assert_called_once_with( 'Failed to get details for program {uuid} from the cache.'.format(uuid=expected_uuid) ) @@ -148,7 +152,7 @@ class TestGetPrograms(CacheIsolationTestCase): None ) - actual_program = get_programs(uuid=expected_uuid) + actual_program = get_programs(self.site, uuid=expected_uuid) self.assertEqual(actual_program, expected_program) self.assertFalse(mock_warning.called) @@ -156,6 +160,9 @@ class TestGetPrograms(CacheIsolationTestCase): @skip_unless_lms @ddt.ddt class TestGetProgramsWithType(TestCase): + def setUp(self): + super(TestGetProgramsWithType, self).setUp() + self.site = SiteFactory() @mock.patch(UTILS_MODULE + '.get_programs') @mock.patch(UTILS_MODULE + '.get_program_types') @@ -176,7 +183,7 @@ class TestGetProgramsWithType(TestCase): mock_get_programs.return_value = programs mock_get_program_types.return_value = program_types - actual = get_programs_with_type() + actual = get_programs_with_type(self.site) self.assertEqual(actual, programs_with_program_type) @ddt.data(False, True) @@ -202,7 +209,7 @@ class TestGetProgramsWithType(TestCase): mock_get_programs.return_value = programs mock_get_program_types.return_value = program_types - actual = get_programs_with_type(include_hidden=include_hidden) + actual = get_programs_with_type(self.site, include_hidden=include_hidden) self.assertEqual(actual, programs_with_program_type) diff --git a/openedx/core/djangoapps/catalog/utils.py b/openedx/core/djangoapps/catalog/utils.py index 9d0b4d24f4..6ba15dd028 100644 --- a/openedx/core/djangoapps/catalog/utils.py +++ b/openedx/core/djangoapps/catalog/utils.py @@ -14,7 +14,6 @@ from openedx.core.djangoapps.catalog.cache import ( SITE_PROGRAM_UUIDS_CACHE_KEY_TPL ) from openedx.core.djangoapps.catalog.models import CatalogIntegration -from openedx.core.djangoapps.theming.helpers import get_current_site from openedx.core.lib.edx_api_utils import get_edx_api_data from openedx.core.lib.token_utils import JwtBuilder @@ -35,11 +34,14 @@ def create_catalog_api_client(user, site=None): return EdxRestApiClient(url, jwt=jwt) -def get_programs(uuid=None): +def get_programs(site, uuid=None): """Read programs from the cache. The cache is populated by a management command, cache_programs. + Arguments: + site (Site): django.contrib.sites.models object + Keyword Arguments: uuid (string): UUID identifying a specific program to read from the cache. @@ -56,7 +58,7 @@ def get_programs(uuid=None): return program if waffle.switch_is_active('get-multitenant-programs'): - uuids = cache.get(SITE_PROGRAM_UUIDS_CACHE_KEY_TPL.format(domain=get_current_site().domain), []) + uuids = cache.get(SITE_PROGRAM_UUIDS_CACHE_KEY_TPL.format(domain=site.domain), []) else: uuids = cache.get(PROGRAM_UUIDS_CACHE_KEY, []) if not uuids: @@ -121,13 +123,16 @@ def get_program_types(name=None): return [] -def get_programs_with_type(include_hidden=True): +def get_programs_with_type(site, include_hidden=True): """ Return the list of programs. You can filter the types of programs returned by using the optional include_hidden parameter. By default hidden programs will be included. The program dict is updated with the fully serialized program type. + Arguments: + site (Site): django.contrib.sites.models object + Keyword Arguments: include_hidden (bool): whether to include hidden programs @@ -135,7 +140,7 @@ def get_programs_with_type(include_hidden=True): list of dict, representing the active programs. """ programs_with_type = [] - programs = get_programs() + programs = get_programs(site) if programs: program_types = {program_type['name']: program_type for program_type in get_program_types()} diff --git a/openedx/core/djangoapps/programs/management/commands/backpopulate_program_credentials.py b/openedx/core/djangoapps/programs/management/commands/backpopulate_program_credentials.py index c9cb763fd2..61121ae77e 100644 --- a/openedx/core/djangoapps/programs/management/commands/backpopulate_program_credentials.py +++ b/openedx/core/djangoapps/programs/management/commands/backpopulate_program_credentials.py @@ -1,18 +1,17 @@ """Management command for backpopulating missing program credentials.""" -from collections import namedtuple import logging +from collections import namedtuple -from django.contrib.auth.models import User +from django.contrib.sites.models import Site from django.core.management import BaseCommand from django.db.models import Q from opaque_keys.edx.keys import CourseKey -from certificates.models import GeneratedCertificate, CertificateStatuses # pylint: disable=import-error +from certificates.models import CertificateStatuses, GeneratedCertificate # pylint: disable=import-error from course_modes.models import CourseMode from openedx.core.djangoapps.catalog.utils import get_programs from openedx.core.djangoapps.programs.tasks.v1.tasks import award_program_certificates - # TODO: Log to console, even with debug mode disabled? logger = logging.getLogger(__name__) # pylint: disable=invalid-name CourseRun = namedtuple('CourseRun', ['key', 'type']) @@ -73,7 +72,11 @@ class Command(BaseCommand): def _load_course_runs(self): """Find all course runs which are part of a program.""" - programs = get_programs() + programs = [] + for site in Site.objects.all(): + logger.info('Loading programs from the catalog for site %s.', site.domain) + programs.extend(get_programs(site)) + self.course_runs = self._flatten(programs) def _flatten(self, programs): diff --git a/openedx/core/djangoapps/programs/tasks/v1/tasks.py b/openedx/core/djangoapps/programs/tasks/v1/tasks.py index 81e27163c9..8570401236 100644 --- a/openedx/core/djangoapps/programs/tasks/v1/tasks.py +++ b/openedx/core/djangoapps/programs/tasks/v1/tasks.py @@ -5,6 +5,7 @@ from celery import task from celery.utils.log import get_task_logger # pylint: disable=no-name-in-module, import-error from django.conf import settings from django.contrib.auth.models import User +from django.contrib.sites.models import Site from django.core.exceptions import ImproperlyConfigured from edx_rest_api_client import exceptions from edx_rest_api_client.client import EdxRestApiClient @@ -55,18 +56,19 @@ def get_api_client(api_config, student): return EdxRestApiClient(api_config.internal_api_url, jwt=jwt) -def get_completed_programs(student): +def get_completed_programs(site, student): """ Given a set of completed courses, determine which programs are completed. Args: + site (Site): Site for which data should be retrieved. student (User): Representing the student whose completed programs to check for. Returns: list of program UUIDs """ - meter = ProgramProgressMeter(student) + meter = ProgramProgressMeter(site, student) return meter.completed_programs @@ -80,7 +82,7 @@ def get_certified_programs(student): User object representing the student Returns: - UUIDs of the programs for which the student has been awarded a certificate + str[]: UUIDs of the programs for which the student has been awarded a certificate """ certified_programs = [] @@ -129,8 +131,7 @@ def award_program_certificates(self, username): student. Args: - username: - The username of the student + username (str): The username of the student Returns: None @@ -158,16 +159,16 @@ 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 = get_completed_programs(student) + program_uuids = [] + for site in Site.objects.all(): + program_uuids.extend(get_completed_programs(site, 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) return - # Determine which program certificates the user has already been - # awarded, if any. + # Determine which program certificates the user has already been awarded, if any. existing_program_uuids = get_certified_programs(student) except Exception as exc: # pylint: disable=broad-except 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 1960bb4e42..a2dfb2788d 100644 --- a/openedx/core/djangoapps/programs/tasks/v1/tests/test_tasks.py +++ b/openedx/core/djangoapps/programs/tasks/v1/tests/test_tasks.py @@ -16,6 +16,7 @@ from edx_rest_api_client.client import EdxRestApiClient from openedx.core.djangoapps.catalog.tests.mixins import CatalogIntegrationMixin 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.djangolib.testing.utils import skip_unless_lms from student.tests.factories import UserFactory @@ -130,6 +131,7 @@ class AwardProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiCo super(AwardProgramCertificatesTestCase, self).setUp() self.create_credentials_config() self.student = UserFactory.create(username='test-student') + self.site = SiteFactory() self.catalog_integration = self.create_catalog_integration() ClientFactory.create(name='credentials') @@ -146,7 +148,7 @@ class AwardProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiCo programs. """ tasks.award_program_certificates.delay(self.student.username).get() - mock_get_completed_programs.assert_called_once_with(self.student) + mock_get_completed_programs.assert_called(self.site, self.student) @ddt.data( ([1], [2, 3]), @@ -282,7 +284,7 @@ class AwardProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiCo """ mock_get_completed_programs.side_effect = self._make_side_effect([Exception('boom'), None]) tasks.award_program_certificates.delay(self.student.username).get() - self.assertEqual(mock_get_completed_programs.call_count, 2) + self.assertEqual(mock_get_completed_programs.call_count, 3) def test_retry_on_credentials_api_errors( self, diff --git a/openedx/core/djangoapps/programs/tests/test_signals.py b/openedx/core/djangoapps/programs/tests/test_signals.py index 30950b27c5..94c4845bde 100644 --- a/openedx/core/djangoapps/programs/tests/test_signals.py +++ b/openedx/core/djangoapps/programs/tests/test_signals.py @@ -10,8 +10,8 @@ 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 - +from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory +from openedx.core.djangolib.testing.utils import skip_unless_lms, get_mock_request TEST_USERNAME = 'test-user' diff --git a/openedx/core/djangoapps/programs/tests/test_utils.py b/openedx/core/djangoapps/programs/tests/test_utils.py index 0b5fbe2019..582d051024 100644 --- a/openedx/core/djangoapps/programs/tests/test_utils.py +++ b/openedx/core/djangoapps/programs/tests/test_utils.py @@ -33,6 +33,7 @@ from openedx.core.djangoapps.programs.utils import ( ProgramMarketingDataExtender, get_certificates, ) +from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory from openedx.core.djangolib.testing.utils import skip_unless_lms from student.tests.factories import AnonymousUserFactory, UserFactory, CourseEnrollmentFactory from util.date_utils import strftime_localized @@ -55,6 +56,7 @@ class TestProgramProgressMeter(TestCase): super(TestProgramProgressMeter, self).setUp() self.user = UserFactory() + self.site = SiteFactory() def _create_enrollments(self, *course_run_ids): """Variadic helper used to create course run enrollments.""" @@ -92,7 +94,7 @@ class TestProgramProgressMeter(TestCase): data = [ProgramFactory()] mock_get_programs.return_value = data - meter = ProgramProgressMeter(self.user) + meter = ProgramProgressMeter(self.site, self.user) self.assertEqual(meter.engaged_programs, []) self._assert_progress(meter) @@ -104,7 +106,7 @@ class TestProgramProgressMeter(TestCase): course_run_id = generate_course_run_key() self._create_enrollments(course_run_id) - meter = ProgramProgressMeter(self.user) + meter = ProgramProgressMeter(self.site, self.user) self.assertEqual(meter.engaged_programs, []) self._assert_progress(meter) @@ -129,7 +131,7 @@ class TestProgramProgressMeter(TestCase): mock_get_programs.return_value = data self._create_enrollments(course_run_key) - meter = ProgramProgressMeter(self.user) + meter = ProgramProgressMeter(self.site, self.user) self._attach_detail_url(data) program = data[0] @@ -159,7 +161,7 @@ class TestProgramProgressMeter(TestCase): self._create_enrollments(course_run_key) - meter = ProgramProgressMeter(self.user) + meter = ProgramProgressMeter(self.site, self.user) program = data[0] expected = [ @@ -195,7 +197,7 @@ class TestProgramProgressMeter(TestCase): mode=CourseMode.NO_ID_PROFESSIONAL_MODE ) - meter = ProgramProgressMeter(self.user) + meter = ProgramProgressMeter(self.site, self.user) program = data[0] expected = [ @@ -236,7 +238,7 @@ class TestProgramProgressMeter(TestCase): CourseEnrollmentFactory(user=self.user, course_id=course_run_key, mode='audit') - meter = ProgramProgressMeter(self.user) + meter = ProgramProgressMeter(self.site, self.user) program = data[0] expected = [ @@ -278,7 +280,7 @@ class TestProgramProgressMeter(TestCase): # The creation time of the enrollments matters to the test. We want # the first_course_run_key to represent the newest enrollment. self._create_enrollments(older_course_run_key, newer_course_run_key) - meter = ProgramProgressMeter(self.user) + meter = ProgramProgressMeter(self.site, self.user) self._attach_detail_url(data) programs = data[:2] @@ -323,7 +325,7 @@ class TestProgramProgressMeter(TestCase): # Enrollment for the shared course run created last (most recently). self._create_enrollments(solo_course_run_key, shared_course_run_key) - meter = ProgramProgressMeter(self.user) + meter = ProgramProgressMeter(self.site, self.user) self._attach_detail_url(data) programs = data[:3] @@ -354,13 +356,13 @@ class TestProgramProgressMeter(TestCase): mock_get_programs.return_value = data # No enrollments, no programs in progress. - meter = ProgramProgressMeter(self.user) + meter = ProgramProgressMeter(self.site, self.user) self._assert_progress(meter) self.assertEqual(meter.completed_programs, []) # One enrollment, one program in progress. self._create_enrollments(first_course_run_key) - meter = ProgramProgressMeter(self.user) + meter = ProgramProgressMeter(self.site, self.user) program, program_uuid = data[0], data[0]['uuid'] self._assert_progress( meter, @@ -370,7 +372,7 @@ class TestProgramProgressMeter(TestCase): # Two enrollments, all courses in progress. self._create_enrollments(second_course_run_key) - meter = ProgramProgressMeter(self.user) + meter = ProgramProgressMeter(self.site, self.user) self._assert_progress( meter, ProgressFactory(uuid=program_uuid, in_progress=2) @@ -381,7 +383,7 @@ class TestProgramProgressMeter(TestCase): mock_completed_course_runs.return_value = [ {'course_run_id': first_course_run_key, 'type': MODES.verified}, ] - meter = ProgramProgressMeter(self.user) + meter = ProgramProgressMeter(self.site, self.user) self._assert_progress( meter, ProgressFactory(uuid=program_uuid, completed=1, in_progress=1) @@ -393,7 +395,7 @@ class TestProgramProgressMeter(TestCase): {'course_run_id': first_course_run_key, 'type': MODES.verified}, {'course_run_id': second_course_run_key, 'type': MODES.honor}, ] - meter = ProgramProgressMeter(self.user) + meter = ProgramProgressMeter(self.site, self.user) self._assert_progress( meter, ProgressFactory(uuid=program_uuid, completed=1, in_progress=1) @@ -405,7 +407,7 @@ class TestProgramProgressMeter(TestCase): {'course_run_id': first_course_run_key, 'type': MODES.verified}, {'course_run_id': second_course_run_key, 'type': MODES.verified}, ] - meter = ProgramProgressMeter(self.user) + meter = ProgramProgressMeter(self.site, self.user) self._assert_progress( meter, ProgressFactory(uuid=program_uuid, completed=2) @@ -436,7 +438,7 @@ class TestProgramProgressMeter(TestCase): mock_completed_course_runs.return_value = [ {'course_run_id': course_run_key, 'type': MODES.honor}, ] - meter = ProgramProgressMeter(self.user) + meter = ProgramProgressMeter(self.site, self.user) program, program_uuid = data[0], data[0]['uuid'] self._assert_progress( @@ -449,7 +451,7 @@ class TestProgramProgressMeter(TestCase): """Verify that programs with no courses do not count as completed.""" program = ProgramFactory() program['courses'] = [] - meter = ProgramProgressMeter(self.user) + meter = ProgramProgressMeter(self.site, self.user) program_complete = meter._is_program_complete(program) self.assertFalse(program_complete) @@ -469,7 +471,7 @@ class TestProgramProgressMeter(TestCase): course_run_keys.append(course_run['key']) # Verify that no programs are complete. - meter = ProgramProgressMeter(self.user) + meter = ProgramProgressMeter(self.site, self.user) self.assertEqual(meter.completed_programs, []) # Complete all programs. @@ -480,7 +482,7 @@ class TestProgramProgressMeter(TestCase): ] # Verify that all programs are complete. - meter = ProgramProgressMeter(self.user) + meter = ProgramProgressMeter(self.site, self.user) self.assertEqual(meter.completed_programs, program_uuids) @mock.patch(UTILS_MODULE + '.certificate_api.get_certificates_for_user') @@ -494,7 +496,7 @@ class TestProgramProgressMeter(TestCase): self._make_certificate_result(status='unknown', course_key='unknown-course'), ] - meter = ProgramProgressMeter(self.user) + meter = ProgramProgressMeter(self.site, self.user) self.assertEqual( meter.completed_course_runs, [ @@ -517,7 +519,7 @@ class TestProgramProgressMeter(TestCase): mock_get_programs.return_value = [program] # Verify that the test program is not complete. - meter = ProgramProgressMeter(self.user) + meter = ProgramProgressMeter(self.site, self.user) self.assertEqual(meter.completed_programs, []) # Grant a 'no-id-professional' certificate for one of the course runs, @@ -527,7 +529,7 @@ class TestProgramProgressMeter(TestCase): ] # Verify that the program is complete. - meter = ProgramProgressMeter(self.user) + meter = ProgramProgressMeter(self.site, self.user) self.assertEqual(meter.completed_programs, [program['uuid']]) @mock.patch(UTILS_MODULE + '.ProgramProgressMeter.completed_course_runs', new_callable=mock.PropertyMock) @@ -543,7 +545,7 @@ class TestProgramProgressMeter(TestCase): program = ProgramFactory(courses=[course]) mock_get_programs.return_value = [program] self._create_enrollments(course_run_key) - meter = ProgramProgressMeter(self.user) + meter = ProgramProgressMeter(self.site, self.user) mock_completed_course_runs.return_value = [{'course_run_id': course_run_key, 'type': 'verified'}] self.assertEqual(meter._is_course_complete(course), True) diff --git a/openedx/core/djangoapps/programs/utils.py b/openedx/core/djangoapps/programs/utils.py index c663ac3bbc..7619d2c4d1 100644 --- a/openedx/core/djangoapps/programs/utils.py +++ b/openedx/core/djangoapps/programs/utils.py @@ -70,7 +70,8 @@ class ProgramProgressMeter(object): will only inspect this one program, not all programs the user may be engaged with. """ - def __init__(self, user, enrollments=None, uuid=None): + def __init__(self, site, user, enrollments=None, uuid=None): + self.site = site self.user = user self.enrollments = enrollments or list(CourseEnrollment.enrollments_for_user(self.user)) @@ -89,9 +90,9 @@ class ProgramProgressMeter(object): self.course_run_ids.append(enrollment_id) if uuid: - self.programs = [get_programs(uuid=uuid)] + self.programs = [get_programs(self.site, uuid=uuid)] else: - self.programs = attach_program_detail_url(get_programs()) + self.programs = attach_program_detail_url(get_programs(self.site)) def invert_programs(self): """Intersect programs and enrollments. diff --git a/openedx/core/djangoapps/site_configuration/tests/factories.py b/openedx/core/djangoapps/site_configuration/tests/factories.py index f9277d1a41..0ce22f5e74 100644 --- a/openedx/core/djangoapps/site_configuration/tests/factories.py +++ b/openedx/core/djangoapps/site_configuration/tests/factories.py @@ -26,5 +26,7 @@ class SiteFactory(DjangoModelFactory): model = Site django_get_or_create = ('domain',) + # TODO These should be generated. Otherwise, code that creates multiple Site + # objects will only end up with a single Site since domain has a unique constraint. domain = 'testserver.fake' name = 'testserver.fake'