Commit ed798bb9 by Renzo Lucioni

Merge pull request #12599 from edx/renzo/supplement-program-data

Supplement program data with course and enrollment data
parents 9acc82dd a8150a51
"""
Stub implementation of programs service for acceptance tests
"""
import re
import urlparse
from .http import StubHttpRequestHandler, StubHttpService
......@@ -11,10 +11,13 @@ class StubProgramsServiceHandler(StubHttpRequestHandler): # pylint: disable=mis
def do_GET(self): # pylint: disable=invalid-name, missing-docstring
pattern_handlers = {
"/api/v1/programs/$": self.get_programs_list,
r'/api/v1/programs/$': self.get_programs_list,
r'/api/v1/programs/(\d+)/$': self.get_program_details,
}
if self.match_pattern(pattern_handlers):
return
self.send_response(404, content="404 Not Found")
def match_pattern(self, pattern_handlers):
......@@ -25,7 +28,7 @@ class StubProgramsServiceHandler(StubHttpRequestHandler): # pylint: disable=mis
for pattern in pattern_handlers:
match = re.match(pattern, path)
if match:
pattern_handlers[pattern](**match.groupdict())
pattern_handlers[pattern](*match.groups())
return True
return None
......@@ -36,6 +39,13 @@ class StubProgramsServiceHandler(StubHttpRequestHandler): # pylint: disable=mis
programs = self.server.config.get('programs', [])
self.send_json_response(programs)
def get_program_details(self, program_id):
"""
Stubs a program details endpoint.
"""
program = self.server.config.get('programs.{}'.format(program_id), [])
self.send_json_response(program)
class StubProgramsService(StubHttpService): # pylint: disable=missing-docstring
HANDLER_CLASS = StubProgramsServiceHandler
"""
Tools to create programs-related data for use in bok choy tests.
"""
from collections import namedtuple
import json
import requests
from . import PROGRAMS_STUB_URL
from .config import ConfigModelFixture
from openedx.core.djangoapps.programs.tests import factories
FakeProgram = namedtuple('FakeProgram', ['name', 'status', 'org_key', 'course_id'])
class ProgramsFixture(object):
"""
Interface to set up mock responses from the Programs stub server.
"""
def install_programs(self, fake_programs):
"""
Sets the response data for the programs list endpoint.
At present, `fake_programs` must be a iterable of FakeProgram named tuples.
"""
programs = []
for program in fake_programs:
run_mode = factories.RunMode(course_key=program.course_id)
course_code = factories.CourseCode(run_modes=[run_mode])
org = factories.Organization(key=program.org_key)
program = factories.Program(
name=program.name,
status=program.status,
organizations=[org],
course_codes=[course_code]
)
programs.append(program)
api_result = {'results': programs}
def install_programs(self, programs, is_list=True):
"""Sets the response data for Programs API endpoints."""
if is_list:
key = 'programs'
api_result = {'results': programs}
else:
program = programs[0]
key = 'programs.{}'.format(program['id'])
api_result = program
requests.put(
'{}/set_config'.format(PROGRAMS_STUB_URL),
data={'programs': json.dumps(api_result)},
data={key: json.dumps(api_result)},
)
......
......@@ -24,7 +24,8 @@ class ProgramListingPage(PageObject):
class ProgramDetailsPage(PageObject):
"""Program details page."""
url = BASE_URL + '/dashboard/programs/123/program-name/'
program_id = 123
url = BASE_URL + '/dashboard/programs/{}/program-name/'.format(program_id)
def is_browser_on_page(self):
return self.q(css='.js-program-details-wrapper').present
"""Acceptance tests for LMS-hosted Programs pages"""
from nose.plugins.attrib import attr
from ...fixtures.programs import FakeProgram, ProgramsFixture, ProgramsConfigMixin
from ...fixtures.programs import ProgramsFixture, ProgramsConfigMixin
from ...fixtures.course import CourseFixture
from ..helpers import UniqueCourseTest
from ...pages.lms.auto_auth import AutoAuthPage
from ...pages.lms.programs import ProgramListingPage, ProgramDetailsPage
from openedx.core.djangoapps.programs.tests import factories
class ProgramPageBase(ProgramsConfigMixin, UniqueCourseTest):
......@@ -15,16 +16,33 @@ class ProgramPageBase(ProgramsConfigMixin, UniqueCourseTest):
self.set_programs_api_configuration(is_enabled=True)
def stub_api(self, course_id=None):
"""Stub out the programs API with fake data."""
name = 'Fake Program'
status = 'active'
org_key = self.course_info['org']
def create_program(self, program_id=None, course_id=None):
"""DRY helper for creating test program data."""
course_id = course_id if course_id else self.course_id
ProgramsFixture().install_programs([
FakeProgram(name=name, status=status, org_key=org_key, course_id=course_id),
])
run_mode = factories.RunMode(course_key=course_id)
course_code = factories.CourseCode(run_modes=[run_mode])
org = factories.Organization(key=self.course_info['org'])
if program_id:
program = factories.Program(
id=program_id,
status='active',
organizations=[org],
course_codes=[course_code]
)
else:
program = factories.Program(
status='active',
organizations=[org],
course_codes=[course_code]
)
return program
def stub_api(self, programs, is_list=True):
"""Stub out the programs API with fake data."""
ProgramsFixture().install_programs(programs, is_list=is_list)
def auth(self, enroll=True):
"""Authenticate, enrolling the user in the configured course if requested."""
......@@ -43,8 +61,10 @@ class ProgramListingPageTest(ProgramPageBase):
def test_no_enrollments(self):
"""Verify that no cards appear when the user has no enrollments."""
self.stub_api()
program = self.create_program()
self.stub_api([program])
self.auth(enroll=False)
self.listing_page.visit()
self.assertTrue(self.listing_page.is_sidebar_present)
......@@ -59,8 +79,11 @@ class ProgramListingPageTest(ProgramPageBase):
self.course_info['run'],
'other_run'
)
self.stub_api(course_id=course_id)
program = self.create_program(course_id=course_id)
self.stub_api([program])
self.auth()
self.listing_page.visit()
self.assertTrue(self.listing_page.is_sidebar_present)
......@@ -71,8 +94,10 @@ class ProgramListingPageTest(ProgramPageBase):
Verify that cards appear when the user has enrollments
which are included in at least one active program.
"""
self.stub_api()
program = self.create_program()
self.stub_api([program])
self.auth()
self.listing_page.visit()
self.assertTrue(self.listing_page.is_sidebar_present)
......@@ -87,9 +112,11 @@ class ProgramListingPageA11yTest(ProgramPageBase):
self.listing_page = ProgramListingPage(self.browser)
program = self.create_program()
self.stub_api([program])
def test_empty_a11y(self):
"""Test a11y of the page's empty state."""
self.stub_api()
self.auth(enroll=False)
self.listing_page.visit()
......@@ -100,7 +127,6 @@ class ProgramListingPageA11yTest(ProgramPageBase):
def test_cards_a11y(self):
"""Test a11y when program cards are present."""
self.stub_api()
self.auth()
self.listing_page.visit()
......@@ -118,9 +144,12 @@ class ProgramDetailsPageA11yTest(ProgramPageBase):
self.details_page = ProgramDetailsPage(self.browser)
program = self.create_program(program_id=self.details_page.program_id)
self.stub_api([program], is_list=False)
def test_a11y(self):
"""Test a11y of the page's state."""
self.auth(enroll=False)
"""Test the page's a11y compliance."""
self.auth()
self.details_page.visit()
self.details_page.a11y_audit.check_for_accessibility_errors()
......@@ -8,7 +8,7 @@ from uuid import uuid4
from ...fixtures import PROGRAMS_STUB_URL
from ...fixtures.config import ConfigModelFixture
from ...fixtures.programs import FakeProgram, ProgramsFixture, ProgramsConfigMixin
from ...fixtures.programs import ProgramsFixture, ProgramsConfigMixin
from ...pages.studio.auto_auth import AutoAuthPage
from ...pages.studio.library import LibraryEditPage
from ...pages.studio.index import DashboardPage, DashboardPageWithPrograms
......@@ -17,6 +17,7 @@ from ..helpers import (
select_option_by_text,
get_selected_option_text
)
from openedx.core.djangoapps.programs.tests import factories
class CreateLibraryTest(WebAppTest):
......@@ -111,11 +112,24 @@ class DashboardProgramsTabTest(ProgramsConfigMixin, WebAppTest):
via config, and the results of the program list should display when
the list is nonempty.
"""
test_program_values = [
FakeProgram(name='first program', status='unpublished', org_key='org1', course_id='foo/bar/baz'),
FakeProgram(name='second program', status='unpublished', org_key='org2', course_id='qux/quux/corge'),
test_program_values = [('first program', 'org1'), ('second program', 'org2')]
programs = [
factories.Program(
name=name,
organizations=[
factories.Organization(key=org),
],
course_codes=[
factories.CourseCode(run_modes=[
factories.RunMode(),
]),
]
)
for name, org in test_program_values
]
ProgramsFixture().install_programs(test_program_values)
ProgramsFixture().install_programs(programs)
self.set_programs_api_configuration(True)
......@@ -126,8 +140,7 @@ class DashboardProgramsTabTest(ProgramsConfigMixin, WebAppTest):
self.assertFalse(self.dashboard_page.is_empty_list_create_button_present())
results = self.dashboard_page.get_program_list()
expected = [(p.name, p.org_key) for p in test_program_values]
self.assertEqual(results, expected)
self.assertEqual(results, test_program_values)
def test_tab_requires_staff(self):
"""
......
......@@ -24,7 +24,7 @@ from openedx.core.djangoapps.programs.tests.mixins import (
ProgramsDataMixin)
from student.models import CourseEnrollment
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
......@@ -231,13 +231,18 @@ class TestProgramListing(
@httpretty.activate
@override_settings(MKTG_URLS={'ROOT': 'http://edx.org'})
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class TestProgramDetails(ProgramsApiConfigMixin, TestCase):
class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase):
"""
Unit tests for the program details page
"""
program_id = 123
password = 'test'
@classmethod
def setUpClass(cls):
super(TestProgramDetails, cls).setUpClass()
cls.course = CourseFactory()
def setUp(self):
super(TestProgramDetails, self).setUp()
......@@ -248,11 +253,12 @@ class TestProgramDetails(ProgramsApiConfigMixin, TestCase):
ClientFactory(name=ProgramsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL)
self.organization = factories.Organization()
self.run_mode = factories.RunMode(course_key=unicode(self.course.id)) # pylint: disable=no-member
self.course_code = factories.CourseCode(run_modes=[self.run_mode])
self.data = factories.Program(
organizations=[factories.Organization()],
course_codes=[
factories.CourseCode(run_modes=[factories.RunMode()]),
]
organizations=[self.organization],
course_codes=[self.course_code]
)
def _mock_programs_api(self):
......
......@@ -3,13 +3,13 @@ from urlparse import urljoin
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_GET
from django.http import Http404
from django.views.decorators.http import require_GET
from edxmako.shortcuts import render_to_response
from openedx.core.djangoapps.credentials.utils import get_programs_credentials
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.programs.utils import ProgramProgressMeter, get_programs, get_display_category
from openedx.core.djangoapps.programs import utils
from student.views import get_course_enrollments
......@@ -22,13 +22,13 @@ def view_programs(request):
raise Http404
enrollments = list(get_course_enrollments(request.user, None, []))
meter = ProgramProgressMeter(request.user, enrollments)
meter = utils.ProgramProgressMeter(request.user, enrollments)
programs = meter.engaged_programs
# TODO: Pull 'xseries' string from configuration model.
marketing_root = urljoin(settings.MKTG_URLS.get('ROOT'), 'xseries').strip('/')
for program in programs:
program['display_category'] = get_display_category(program)
program['display_category'] = utils.get_display_category(program)
program['marketing_url'] = '{root}/{slug}'.format(
root=marketing_root,
slug=program['marketing_slug']
......@@ -56,7 +56,8 @@ def program_details(request, program_id):
if not show_program_details:
raise Http404
program_data = get_programs(request.user, program_id=program_id)
program_data = utils.get_programs(request.user, program_id=program_id)
program_data = utils.supplement_program_data(program_data, request.user)
context = {
'program_data': program_data,
......
"""Tests covering Programs utilities."""
import copy
import datetime
import json
from unittest import skipUnless
import ddt
from django.conf import settings
from django.core.cache import cache
from django.core.urlresolvers import reverse
from django.test import TestCase
from django.utils import timezone
import httpretty
import mock
from nose.plugins.attrib import attr
......@@ -12,6 +17,7 @@ from edx_oauth2_provider.tests.factories import ClientFactory
from provider.constants import CONFIDENTIAL
from lms.djangoapps.certificates.api import MODES
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
......@@ -20,6 +26,8 @@ from openedx.core.djangoapps.programs.tests import factories
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin, ProgramsDataMixin
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
from student.tests.factories import UserFactory, CourseEnrollmentFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
UTILS_MODULE = 'openedx.core.djangoapps.programs.utils'
......@@ -597,3 +605,78 @@ class TestProgramProgressMeter(ProgramsApiConfigMixin, TestCase):
meter,
factories.Progress(id=program['id'], completed=self._extract_names(program, 0))
)
@ddt.ddt
@skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase):
"""Tests of the utility function used to supplement program data."""
password = 'test'
human_friendly_format = '%x'
maxDiff = None
def setUp(self):
super(TestSupplementProgramData, self).setUp()
self.user = UserFactory()
self.client.login(username=self.user.username, password=self.password)
ClientFactory(name=ProgramsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL)
self.course = CourseFactory()
self.course.start = timezone.now() - datetime.timedelta(days=1)
self.course.end = timezone.now() + datetime.timedelta(days=1)
self.course = self.update_course(self.course, self.user.id) # pylint: disable=no-member
self.organization = factories.Organization()
self.run_mode = factories.RunMode(course_key=unicode(self.course.id)) # pylint: disable=no-member
self.course_code = factories.CourseCode(run_modes=[self.run_mode])
self.program = factories.Program(
organizations=[self.organization],
course_codes=[self.course_code]
)
def _assert_supplemented(self, actual, is_enrolled=False, is_enrollment_open=True):
"""DRY helper used to verify that program data is extended correctly."""
course_overview = CourseOverview.get_from_id(self.course.id) # pylint: disable=no-member
run_mode = factories.RunMode(
course_key=unicode(self.course.id), # pylint: disable=no-member
course_url=reverse('course_root', args=[self.course.id]), # pylint: disable=no-member
course_image_url=course_overview.course_image_url,
start_date=self.course.start.strftime(self.human_friendly_format),
end_date=self.course.end.strftime(self.human_friendly_format),
is_enrolled=is_enrolled,
is_enrollment_open=is_enrollment_open,
marketing_url='',
)
course_code = factories.CourseCode(display_name=self.course_code['display_name'], run_modes=[run_mode])
expected = copy.deepcopy(self.program)
expected['course_codes'] = [course_code]
self.assertEqual(actual, expected)
@ddt.data(True, False)
def test_student_enrollment_status(self, is_enrolled):
"""Verify that program data is supplemented correctly."""
if is_enrolled:
CourseEnrollmentFactory(user=self.user, course_id=self.course.id) # pylint: disable=no-member
data = utils.supplement_program_data(self.program, self.user)
self._assert_supplemented(data, is_enrolled=is_enrolled)
@ddt.data(
[1, 1, False],
[1, -1, True],
)
@ddt.unpack
def test_course_enrollment_status(self, start_offset, end_offset, is_enrollment_open):
"""Verify that course enrollment status is reflected correctly."""
self.course.enrollment_start = timezone.now() - datetime.timedelta(days=start_offset)
self.course.enrollment_end = timezone.now() - datetime.timedelta(days=end_offset)
self.course = self.update_course(self.course, self.user.id) # pylint: disable=no-member
data = utils.supplement_program_data(self.program, self.user)
self._assert_supplemented(data, is_enrollment_open=is_enrollment_open)
# -*- coding: utf-8 -*-
"""Helper functions for working with Programs."""
import datetime
import logging
from django.core.urlresolvers import reverse
from django.utils import timezone
from opaque_keys.edx.keys import CourseKey
import pytz
from lms.djangoapps.certificates.api import get_certificates_for_user, is_passing_status
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.lib.edx_api_utils import get_edx_api_data
from student.models import CourseEnrollment
from xmodule.course_metadata_utils import DEFAULT_START_DATE
log = logging.getLogger(__name__)
......@@ -275,3 +284,37 @@ class ProgramProgressMeter(object):
}
return parsed
def supplement_program_data(program_data, user):
"""Supplement program course codes with CourseOverview and CourseEnrollment data.
Arguments:
program_data (dict): Representation of a program.
user (User): The user whose enrollments to inspect.
"""
for course_code in program_data['course_codes']:
for run_mode in course_code['run_modes']:
course_key = CourseKey.from_string(run_mode['course_key'])
course_overview = CourseOverview.get_from_id(course_key)
run_mode['course_url'] = reverse('course_root', args=[course_key])
run_mode['course_image_url'] = course_overview.course_image_url
human_friendly_format = '%x'
start_date = course_overview.start or DEFAULT_START_DATE
end_date = course_overview.end or datetime.datetime.max.replace(tzinfo=pytz.UTC)
run_mode['start_date'] = start_date.strftime(human_friendly_format)
run_mode['end_date'] = end_date.strftime(human_friendly_format)
run_mode['is_enrolled'] = CourseEnrollment.is_enrolled(user, course_key)
enrollment_start = course_overview.enrollment_start or datetime.datetime.min.replace(tzinfo=pytz.UTC)
enrollment_end = course_overview.enrollment_end or datetime.datetime.max.replace(tzinfo=pytz.UTC)
is_enrollment_open = enrollment_start <= timezone.now() < enrollment_end
run_mode['is_enrollment_open'] = is_enrollment_open
# TODO: Currently unavailable on LMS.
run_mode['marketing_url'] = ''
return program_data
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