Merge pull request #13170 from edx/renzo/dashboard-programs
Display programs of all categories on the student dashboard
This commit is contained in:
@@ -16,9 +16,8 @@ from openedx.core.lib.token_utils import JwtBuilder
|
||||
class ProgramAuthoringView(View):
|
||||
"""View rendering a template which hosts the Programs authoring app.
|
||||
|
||||
The Programs authoring app is a Backbone SPA maintained in a separate repository.
|
||||
The app handles its own routing and provides a UI which can be used to create and
|
||||
publish new Programs (e.g, XSeries).
|
||||
The Programs authoring app is a Backbone SPA. The app handles its own routing
|
||||
and provides a UI which can be used to create and publish new Programs.
|
||||
"""
|
||||
|
||||
@method_decorator(login_required)
|
||||
|
||||
@@ -2,53 +2,56 @@
|
||||
"""
|
||||
Miscellaneous tests for the student app.
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
import logging
|
||||
import unittest
|
||||
import ddt
|
||||
from datetime import datetime, timedelta
|
||||
from urlparse import urljoin
|
||||
|
||||
import pytz
|
||||
from markupsafe import escape
|
||||
from mock import Mock, patch
|
||||
from nose.plugins.attrib import attr
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from pyquery import PyQuery as pq
|
||||
|
||||
import ddt
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User, AnonymousUser
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import TestCase
|
||||
from django.test.client import Client
|
||||
from edx_oauth2_provider.tests.factories import ClientFactory
|
||||
import httpretty
|
||||
from markupsafe import escape
|
||||
from mock import Mock, patch
|
||||
from nose.plugins.attrib import attr
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from provider.constants import CONFIDENTIAL
|
||||
from pyquery import PyQuery as pq
|
||||
import pytz
|
||||
|
||||
from bulk_email.models import Optout # pylint: disable=import-error
|
||||
from certificates.models import CertificateStatuses # pylint: disable=import-error
|
||||
from certificates.tests.factories import GeneratedCertificateFactory # pylint: disable=import-error
|
||||
from config_models.models import cache
|
||||
from course_modes.models import CourseMode
|
||||
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
|
||||
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
|
||||
from openedx.core.djangoapps.programs.tests import factories as programs_factories
|
||||
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin
|
||||
import shoppingcart # pylint: disable=import-error
|
||||
from student.models import (
|
||||
anonymous_id_for_user, user_by_anonymous_id, CourseEnrollment,
|
||||
unique_id_for_user, LinkedInAddToProfileConfiguration, UserAttribute
|
||||
)
|
||||
from student.tests.factories import UserFactory, CourseModeFactory, CourseEnrollmentFactory
|
||||
from student.views import (
|
||||
process_survey_link,
|
||||
_cert_info,
|
||||
complete_course_mode_info,
|
||||
_get_course_programs
|
||||
)
|
||||
from student.tests.factories import UserFactory, CourseModeFactory
|
||||
from util.testing import EventTestMixin
|
||||
from util.model_utils import USER_SETTINGS_CHANGED_EVENT_NAME
|
||||
from util.testing import EventTestMixin
|
||||
from xmodule.modulestore.tests.django_utils import (
|
||||
ModuleStoreTestCase,
|
||||
ModuleStoreEnum,
|
||||
SharedModuleStoreTestCase,
|
||||
)
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, ModuleStoreEnum
|
||||
|
||||
# These imports refer to lms djangoapps.
|
||||
# Their testcases are only run under lms.
|
||||
from bulk_email.models import Optout # pylint: disable=import-error
|
||||
from certificates.models import CertificateStatuses # pylint: disable=import-error
|
||||
from certificates.tests.factories import GeneratedCertificateFactory # pylint: disable=import-error
|
||||
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
|
||||
import shoppingcart # pylint: disable=import-error
|
||||
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin
|
||||
|
||||
# Explicitly import the cache from ConfigurationModel so we can reset it after each test
|
||||
from config_models.models import cache
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -889,276 +892,95 @@ class AnonymousLookupTable(ModuleStoreTestCase):
|
||||
self.assertEqual(anonymous_id, anonymous_id_for_user(self.user, course2.id, save=False))
|
||||
|
||||
|
||||
# TODO: Clean up these tests so that they use program factories and don't mention XSeries!
|
||||
@attr(shard=3)
|
||||
@httpretty.activate
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
@ddt.ddt
|
||||
class DashboardTestXSeriesPrograms(ModuleStoreTestCase, ProgramsApiConfigMixin):
|
||||
"""
|
||||
Tests for dashboard for xseries program courses. Enroll student into
|
||||
programs and then try different combinations to see xseries upsell
|
||||
messages are appearing.
|
||||
"""
|
||||
class RelatedProgramsTests(ProgramsApiConfigMixin, SharedModuleStoreTestCase):
|
||||
"""Tests verifying that related programs appear on the course dashboard."""
|
||||
url = None
|
||||
maxDiff = None
|
||||
password = 'test'
|
||||
related_programs_preface = 'Related Programs'
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(RelatedProgramsTests, cls).setUpClass()
|
||||
|
||||
cls.user = UserFactory()
|
||||
cls.course = CourseFactory()
|
||||
cls.enrollment = CourseEnrollmentFactory(user=cls.user, course_id=cls.course.id) # pylint: disable=no-member
|
||||
ClientFactory(name=ProgramsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL)
|
||||
|
||||
cls.organization = programs_factories.Organization()
|
||||
run_mode = programs_factories.RunMode(course_key=unicode(cls.course.id)) # pylint: disable=no-member
|
||||
course_code = programs_factories.CourseCode(run_modes=[run_mode])
|
||||
|
||||
cls.programs = [
|
||||
programs_factories.Program(
|
||||
organizations=[cls.organization],
|
||||
course_codes=[course_code]
|
||||
) for __ in range(2)
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
super(DashboardTestXSeriesPrograms, self).setUp()
|
||||
super(RelatedProgramsTests, self).setUp()
|
||||
|
||||
self.user = UserFactory.create(username="jack", email="jack@fake.edx.org", password='test')
|
||||
self.course_1 = CourseFactory.create()
|
||||
self.course_2 = CourseFactory.create()
|
||||
self.course_3 = CourseFactory.create()
|
||||
self.program_name = 'Testing Program'
|
||||
self.category = 'XSeries'
|
||||
self.url = reverse('dashboard')
|
||||
|
||||
CourseModeFactory.create(
|
||||
course_id=self.course_1.id,
|
||||
mode_slug='verified',
|
||||
mode_display_name='Verified',
|
||||
expiration_datetime=datetime.now(pytz.UTC) + timedelta(days=1)
|
||||
self.create_programs_config()
|
||||
self.client.login(username=self.user.username, password=self.password)
|
||||
|
||||
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.')
|
||||
|
||||
url = ProgramsApiConfig.current().internal_api_url.strip('/') + '/programs/'
|
||||
body = json.dumps({'results': data})
|
||||
|
||||
httpretty.register_uri(httpretty.GET, url, body=body, content_type='application/json')
|
||||
|
||||
def assert_related_programs(self, response, are_programs_present=True):
|
||||
"""Assertion for verifying response contents."""
|
||||
assertion = getattr(self, 'assert{}Contains'.format('' if are_programs_present else 'Not'))
|
||||
|
||||
for program in self.programs:
|
||||
assertion(response, self.expected_link_text(program))
|
||||
|
||||
assertion(response, self.related_programs_preface)
|
||||
|
||||
def expected_link_text(self, program):
|
||||
"""Construct expected dashboard link text."""
|
||||
return '{name} {category}'.format(name=program['name'], category=program['category'])
|
||||
|
||||
def test_related_programs_listed(self):
|
||||
"""Verify that related programs are listed when the programs API returns data."""
|
||||
self.mock_programs_api(self.programs)
|
||||
|
||||
response = self.client.get(self.url)
|
||||
self.assert_related_programs(response)
|
||||
|
||||
def test_no_data_no_programs(self):
|
||||
"""Verify that related programs aren't listed if the programs API returns no data."""
|
||||
self.mock_programs_api([])
|
||||
|
||||
response = self.client.get(self.url)
|
||||
self.assert_related_programs(response, are_programs_present=False)
|
||||
|
||||
def test_unrelated_program_not_listed(self):
|
||||
"""Verify that unrelated programs don't appear in the listing."""
|
||||
run_mode = programs_factories.RunMode(course_key='some/nonexistent/run')
|
||||
course_code = programs_factories.CourseCode(run_modes=[run_mode])
|
||||
|
||||
unrelated_program = programs_factories.Program(
|
||||
organizations=[self.organization],
|
||||
course_codes=[course_code]
|
||||
)
|
||||
self.client = Client()
|
||||
cache.clear()
|
||||
|
||||
def _create_program_data(self, data):
|
||||
"""Dry method to create testing programs data."""
|
||||
programs = {}
|
||||
_id = 0
|
||||
self.mock_programs_api(self.programs + [unrelated_program])
|
||||
|
||||
for course, program_status in data:
|
||||
programs[unicode(course)] = [{
|
||||
'id': _id,
|
||||
'category': self.category,
|
||||
'organization': {'display_name': 'Test Organization 1', 'key': 'edX'},
|
||||
'marketing_slug': 'fake-marketing-slug-xseries-1',
|
||||
'status': program_status,
|
||||
'course_codes': [
|
||||
{
|
||||
'display_name': 'Demo XSeries Program 1',
|
||||
'key': unicode(course),
|
||||
'run_modes': [{'sku': '', 'mode_slug': 'ABC', 'course_key': unicode(course)}]
|
||||
},
|
||||
{
|
||||
'display_name': 'Demo XSeries Program 2',
|
||||
'key': 'edx/demo/course_2',
|
||||
'run_modes': [{'sku': '', 'mode_slug': 'ABC', 'course_key': 'edx/demo/course_2'}]
|
||||
},
|
||||
{
|
||||
'display_name': 'Demo XSeries Program 3',
|
||||
'key': 'edx/demo/course_3',
|
||||
'run_modes': [{'sku': '', 'mode_slug': 'ABC', 'course_key': 'edx/demo/course_3'}]
|
||||
}
|
||||
],
|
||||
'subtitle': 'sub',
|
||||
'name': self.program_name
|
||||
}]
|
||||
|
||||
_id += 1
|
||||
|
||||
return programs
|
||||
|
||||
@ddt.data(
|
||||
('active', [{'sku': ''}, {'sku': ''}, {'sku': ''}, {'sku': ''}], 'marketing-slug-1'),
|
||||
('active', [{'sku': ''}, {'sku': ''}, {'sku': ''}], 'marketing-slug-2'),
|
||||
('active', [], ''),
|
||||
('unpublished', [{'sku': ''}, {'sku': ''}, {'sku': ''}, {'sku': ''}], 'marketing-slug-3'),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_get_xseries_programs_method(self, program_status, course_codes, marketing_slug):
|
||||
"""Verify that program data is parsed correctly for a given course"""
|
||||
with patch('student.views.get_programs_for_dashboard') as mock_data:
|
||||
mock_data.return_value = {
|
||||
u'edx/demox/Run_1': [{
|
||||
'id': 0,
|
||||
'category': self.category,
|
||||
'organization': {'display_name': 'Test Organization 1', 'key': 'edX'},
|
||||
'marketing_slug': marketing_slug,
|
||||
'status': program_status,
|
||||
'course_codes': course_codes,
|
||||
'subtitle': 'sub',
|
||||
'name': self.program_name
|
||||
}]
|
||||
}
|
||||
parse_data = _get_course_programs(
|
||||
self.user, [
|
||||
u'edx/demox/Run_1', u'valid/edX/Course'
|
||||
]
|
||||
)
|
||||
|
||||
if program_status == 'unpublished':
|
||||
self.assertEqual({}, parse_data)
|
||||
else:
|
||||
self.assertEqual(
|
||||
{
|
||||
u'edx/demox/Run_1': {
|
||||
'category': self.category,
|
||||
'course_program_list': [{
|
||||
'program_id': 0,
|
||||
'course_count': len(course_codes),
|
||||
'display_name': self.program_name,
|
||||
'program_marketing_url': urljoin(
|
||||
settings.MKTG_URLS.get('ROOT'), 'xseries' + '/{}'
|
||||
).format(marketing_slug)
|
||||
}]
|
||||
}
|
||||
},
|
||||
parse_data
|
||||
)
|
||||
|
||||
def test_program_courses_on_dashboard_without_configuration(self):
|
||||
"""If programs configuration is disabled then the xseries upsell messages
|
||||
will not appear on student dashboard.
|
||||
"""
|
||||
CourseEnrollment.enroll(self.user, self.course_1.id)
|
||||
self.client.login(username="jack", password="test")
|
||||
with patch('student.views.get_programs_for_dashboard') as mock_method:
|
||||
mock_method.return_value = self._create_program_data([])
|
||||
response = self.client.get(reverse('dashboard'))
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertIn('Pursue a Certificate of Achievement to highlight', response.content)
|
||||
self._assert_responses(response, 0)
|
||||
|
||||
@ddt.data('verified', 'honor')
|
||||
def test_modes_program_courses_on_dashboard_with_configuration(self, course_mode):
|
||||
"""Test that if program configuration is enabled than student can only
|
||||
see those courses with xseries upsell messages which are active in
|
||||
xseries programs.
|
||||
"""
|
||||
CourseEnrollment.enroll(self.user, self.course_1.id, mode=course_mode)
|
||||
CourseEnrollment.enroll(self.user, self.course_2.id, mode=course_mode)
|
||||
|
||||
self.client.login(username="jack", password="test")
|
||||
self.create_programs_config()
|
||||
|
||||
with patch('student.views.get_programs_for_dashboard') as mock_data:
|
||||
mock_data.return_value = self._create_program_data(
|
||||
[(self.course_1.id, 'active'), (self.course_2.id, 'unpublished')]
|
||||
)
|
||||
response = self.client.get(reverse('dashboard'))
|
||||
# count total courses appearing on student dashboard
|
||||
self.assertContains(response, 'course-container', 2)
|
||||
self._assert_responses(response, 1)
|
||||
|
||||
# for verified enrollment view the program detail button will have
|
||||
# the class 'base-btn'
|
||||
# for other modes view the program detail button will have have the
|
||||
# class border-btn
|
||||
if course_mode == 'verified':
|
||||
self.assertIn('xseries-base-btn', response.content)
|
||||
else:
|
||||
self.assertIn('xseries-border-btn', response.content)
|
||||
|
||||
@patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
|
||||
@ddt.data((-2, -1), (-1, 1), (1, 2))
|
||||
@ddt.unpack
|
||||
def test_start_end_offsets(self, start_days_offset, end_days_offset):
|
||||
"""Test that the xseries upsell messaging displays whether the course
|
||||
has not yet started, is in session, or has already ended.
|
||||
"""
|
||||
self.course_1.start = datetime.now(pytz.UTC) + timedelta(days=start_days_offset)
|
||||
self.course_1.end = datetime.now(pytz.UTC) + timedelta(days=end_days_offset)
|
||||
self.update_course(self.course_1, self.user.id)
|
||||
CourseEnrollment.enroll(self.user, self.course_1.id, mode='verified')
|
||||
|
||||
self.client.login(username="jack", password="test")
|
||||
self.create_programs_config()
|
||||
|
||||
with patch(
|
||||
'student.views.get_programs_for_dashboard',
|
||||
return_value=self._create_program_data([(self.course_1.id, 'active')])
|
||||
) as mock_get_programs:
|
||||
response = self.client.get(reverse('dashboard'))
|
||||
# ensure that our course id was included in the API call regardless of start/end dates
|
||||
__, course_ids = mock_get_programs.call_args[0]
|
||||
self.assertEqual(list(course_ids), [self.course_1.id])
|
||||
# count total courses appearing on student dashboard
|
||||
self._assert_responses(response, 1)
|
||||
|
||||
@ddt.data(
|
||||
('unpublished', 'unpublished', 'unpublished', 0),
|
||||
('active', 'unpublished', 'unpublished', 1),
|
||||
('active', 'active', 'unpublished', 2),
|
||||
('active', 'active', 'active', 3),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_different_programs_on_dashboard(self, status_1, status_2, status_3, program_count):
|
||||
"""Test the upsell on student dashboard with different programs
|
||||
statuses.
|
||||
"""
|
||||
|
||||
CourseEnrollment.enroll(self.user, self.course_1.id, mode='verified')
|
||||
CourseEnrollment.enroll(self.user, self.course_2.id, mode='honor')
|
||||
CourseEnrollment.enroll(self.user, self.course_3.id, mode='honor')
|
||||
|
||||
self.client.login(username="jack", password="test")
|
||||
self.create_programs_config()
|
||||
|
||||
with patch('student.views.get_programs_for_dashboard') as mock_data:
|
||||
mock_data.return_value = self._create_program_data(
|
||||
[(self.course_1.id, status_1),
|
||||
(self.course_2.id, status_2),
|
||||
(self.course_3.id, status_3)]
|
||||
)
|
||||
|
||||
response = self.client.get(reverse('dashboard'))
|
||||
# count total courses appearing on student dashboard
|
||||
self.assertContains(response, 'course-container', 3)
|
||||
self._assert_responses(response, program_count)
|
||||
|
||||
@patch('student.views.log.warning')
|
||||
@ddt.data('', 'course_codes', 'marketing_slug', 'name')
|
||||
def test_program_courses_with_invalid_data(self, key_remove, log_warn):
|
||||
"""Test programs with invalid responses."""
|
||||
|
||||
CourseEnrollment.enroll(self.user, self.course_1.id)
|
||||
self.client.login(username="jack", password="test")
|
||||
self.create_programs_config()
|
||||
|
||||
program_data = self._create_program_data([(self.course_1.id, 'active')])
|
||||
for program in program_data[unicode(self.course_1.id)]:
|
||||
if key_remove and key_remove in program:
|
||||
del program[key_remove]
|
||||
|
||||
with patch('student.views.get_programs_for_dashboard') as mock_data:
|
||||
mock_data.return_value = program_data
|
||||
|
||||
response = self.client.get(reverse('dashboard'))
|
||||
|
||||
# if data is invalid then warning log will be recorded.
|
||||
if key_remove:
|
||||
log_warn.assert_called_with(
|
||||
'Program structure is invalid, skipping display: %r', program_data[
|
||||
unicode(self.course_1.id)
|
||||
][0]
|
||||
)
|
||||
# verify that no programs related upsell messages appear on the
|
||||
# student dashboard.
|
||||
self._assert_responses(response, 0)
|
||||
else:
|
||||
# in case of valid data all upsell messages will appear on dashboard.
|
||||
self._assert_responses(response, 1)
|
||||
|
||||
# verify that only normal courses (non-programs courses) appear on
|
||||
# the student dashboard.
|
||||
self.assertContains(response, 'course-container', 1)
|
||||
self.assertIn('Pursue a Certificate of Achievement to highlight', response.content)
|
||||
|
||||
def _assert_responses(self, response, count):
|
||||
"""Dry method to compare different programs related upsell messages,
|
||||
classes.
|
||||
"""
|
||||
self.assertContains(response, 'label-xseries-association', count)
|
||||
self.assertContains(response, 'btn xseries-', count)
|
||||
|
||||
self.assertContains(response, '{category} Program Course'.format(category=self.category), count)
|
||||
self.assertContains(
|
||||
response,
|
||||
'{category} Program: Interested in more courses in this subject?'.format(category=self.category),
|
||||
count
|
||||
)
|
||||
self.assertContains(response, 'View {category} Details'.format(category=self.category), count)
|
||||
|
||||
self.assertContains(response, 'This course is 1 of 3 courses in the', count)
|
||||
self.assertContains(response, self.program_name, count * 2)
|
||||
response = self.client.get(self.url)
|
||||
self.assert_related_programs(response)
|
||||
self.assertNotContains(response, unrelated_program['name'])
|
||||
|
||||
|
||||
class UserAttributeTests(TestCase):
|
||||
|
||||
@@ -120,8 +120,8 @@ from notification_prefs.views import enable_notifications
|
||||
|
||||
from openedx.core.djangoapps.credit.email_utils import get_credit_provider_display_names, make_providers_strings
|
||||
from openedx.core.djangoapps.user_api.preferences import api as preferences_api
|
||||
from openedx.core.djangoapps.programs.utils import get_programs_for_dashboard
|
||||
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
|
||||
from openedx.core.djangoapps.programs import utils as programs_utils
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.djangoapps.theming import helpers as theming_helpers
|
||||
|
||||
@@ -609,10 +609,11 @@ def dashboard(request):
|
||||
and has_access(request.user, 'view_courseware_with_prerequisites', enrollment.course_overview)
|
||||
)
|
||||
|
||||
# Get any programs associated with courses being displayed.
|
||||
# This is passed along in the template context to allow rendering of
|
||||
# program-related information on the dashboard.
|
||||
course_programs = _get_course_programs(user, [enrollment.course_id for enrollment in course_enrollments])
|
||||
# Find programs associated with courses being displayed. This information
|
||||
# is passed in the template context to allow rendering of program-related
|
||||
# information on the dashboard.
|
||||
meter = programs_utils.ProgramProgressMeter(user, enrollments=course_enrollments)
|
||||
programs_by_run = meter.engaged_programs(by_run=True)
|
||||
|
||||
# Construct a dictionary of course mode information
|
||||
# used to render the course list. We re-use the course modes dict
|
||||
@@ -736,9 +737,9 @@ def dashboard(request):
|
||||
'order_history_list': order_history_list,
|
||||
'courses_requirements_not_met': courses_requirements_not_met,
|
||||
'nav_hidden': True,
|
||||
'course_programs': course_programs,
|
||||
'disable_courseware_js': True,
|
||||
'programs_by_run': programs_by_run,
|
||||
'show_program_listing': ProgramsApiConfig.current().show_program_listing,
|
||||
'disable_courseware_js': True,
|
||||
}
|
||||
|
||||
ecommerce_service = EcommerceService()
|
||||
@@ -2478,44 +2479,6 @@ def change_email_settings(request):
|
||||
return JsonResponse({"success": True})
|
||||
|
||||
|
||||
def _get_course_programs(user, user_enrolled_courses): # pylint: disable=invalid-name
|
||||
"""Build a dictionary of program data required for display on the student dashboard.
|
||||
|
||||
Given a user and an iterable of course keys, find all programs relevant to the
|
||||
user and return them in a dictionary keyed by course key.
|
||||
|
||||
Arguments:
|
||||
user (User): The user to authenticate as when requesting programs.
|
||||
user_enrolled_courses (list): List of course keys representing the courses in which
|
||||
the given user has active enrollments.
|
||||
|
||||
Returns:
|
||||
dict, containing programs keyed by course.
|
||||
"""
|
||||
course_programs = get_programs_for_dashboard(user, user_enrolled_courses)
|
||||
programs_data = {}
|
||||
|
||||
for course_key, programs in course_programs.viewitems():
|
||||
for program in programs:
|
||||
if program.get('status') == 'active' and program.get('category') == 'XSeries':
|
||||
try:
|
||||
programs_for_course = programs_data.setdefault(course_key, {})
|
||||
programs_for_course.setdefault('course_program_list', []).append({
|
||||
'course_count': len(program['course_codes']),
|
||||
'display_name': program['name'],
|
||||
'program_id': program['id'],
|
||||
'program_marketing_url': urljoin(
|
||||
settings.MKTG_URLS.get('ROOT'),
|
||||
'xseries' + '/{}'
|
||||
).format(program['marketing_slug'])
|
||||
})
|
||||
programs_for_course['category'] = program.get('category')
|
||||
except KeyError:
|
||||
log.warning('Program structure is invalid, skipping display: %r', program)
|
||||
|
||||
return programs_data
|
||||
|
||||
|
||||
class LogoutView(TemplateView):
|
||||
"""
|
||||
Logs out user and redirects.
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
"""Learner dashboard views"""
|
||||
from urlparse import urljoin
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import Http404
|
||||
@@ -23,19 +20,13 @@ def program_listing(request):
|
||||
raise Http404
|
||||
|
||||
meter = utils.ProgramProgressMeter(request.user)
|
||||
programs = meter.engaged_programs
|
||||
|
||||
marketing_url = urljoin(settings.MKTG_URLS.get('ROOT'), programs_config.marketing_path).rstrip('/')
|
||||
|
||||
for program in programs:
|
||||
program['detail_url'] = utils.get_program_detail_url(program, marketing_url)
|
||||
|
||||
context = {
|
||||
'credentials': get_programs_credentials(request.user),
|
||||
'disable_courseware_js': True,
|
||||
'marketing_url': marketing_url,
|
||||
'marketing_url': utils.get_program_marketing_url(programs_config),
|
||||
'nav_hidden': True,
|
||||
'programs': programs,
|
||||
'programs': meter.engaged_programs(),
|
||||
'progress': meter.progress,
|
||||
'show_program_listing': programs_config.show_program_listing,
|
||||
'uses_pattern_library': True,
|
||||
|
||||
@@ -20,17 +20,6 @@ var edx = edx || {};
|
||||
return properties;
|
||||
};
|
||||
|
||||
// Generate object to be passed with programs events
|
||||
edx.dashboard.generateProgramProperties = function(element) {
|
||||
var $el = $(element);
|
||||
|
||||
return {
|
||||
category: 'dashboard',
|
||||
course_id: $el.closest('.course-container').find('.info-course-id').html(),
|
||||
program_id: $el.data('program-id')
|
||||
};
|
||||
};
|
||||
|
||||
// Emit an event when the 'course title link' is clicked.
|
||||
edx.dashboard.trackCourseTitleClicked = function($courseTitleLink, properties) {
|
||||
var trackProperty = properties || edx.dashboard.generateTrackProperties;
|
||||
@@ -92,24 +81,6 @@ var edx = edx || {};
|
||||
);
|
||||
};
|
||||
|
||||
// Emit an event when the 'View XSeries Details' button is clicked
|
||||
edx.dashboard.trackXseriesBtnClicked = function($xseriesBtn, properties) {
|
||||
var trackProperty = properties || edx.dashboard.generateProgramProperties;
|
||||
window.analytics.trackLink(
|
||||
$xseriesBtn,
|
||||
'edx.bi.dashboard.xseries_cta_message.clicked',
|
||||
trackProperty
|
||||
);
|
||||
};
|
||||
|
||||
edx.dashboard.xseriesTrackMessages = function() {
|
||||
$('.xseries-action .btn').each(function(i, element) {
|
||||
var data = edx.dashboard.generateProgramProperties($(element));
|
||||
|
||||
window.analytics.track('edx.bi.dashboard.xseries_cta_message.viewed', data);
|
||||
});
|
||||
};
|
||||
|
||||
$(document).ready(function() {
|
||||
if (!window.analytics) {
|
||||
return;
|
||||
@@ -120,7 +91,5 @@ var edx = edx || {};
|
||||
edx.dashboard.trackCourseOptionDropdownClicked($('.wrapper-action-more'));
|
||||
edx.dashboard.trackLearnVerifiedLinkClicked($('.verified-info'));
|
||||
edx.dashboard.trackFindCourseBtnClicked($('.btn-find-courses'));
|
||||
edx.dashboard.trackXseriesBtnClicked($('.xseries-action .btn'));
|
||||
edx.dashboard.xseriesTrackMessages();
|
||||
});
|
||||
})(jQuery);
|
||||
|
||||
@@ -27,10 +27,6 @@
|
||||
|
||||
|
||||
<div class="course-container">
|
||||
<div class="label-xseries-association">
|
||||
<span class="xseries-icon" aria-hidden="true"></span>
|
||||
<p class="message-copy">XSeries Program Course</p>
|
||||
</div>
|
||||
<div class="course honor">
|
||||
<section class="details" aria-labelledby="course-details-heading">
|
||||
<h2 class="hd hd-2 sr" id="course-details-heading">Course details</h2>
|
||||
@@ -96,32 +92,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="message message-status is-shown credit-message">
|
||||
<div class="xseries-action">
|
||||
<div class="message-copy xseries-msg">
|
||||
<p><b class="message-copy-bold">XSeries Program: Interested in more courses in this subject?</b></p>
|
||||
<p></p>
|
||||
<p class="message-copy">
|
||||
This course is 1 of 3 courses in the <a href="https://www.edx.org/xseries/water-management">Water Management</a> XSeries.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<a class="btn xseries-border-btn" href="https://www.edx.org/xseries/water-management" target="_blank"
|
||||
data-program-id="xseries007">
|
||||
<span class="action-xseries-icon" aria-hidden="true"></span>
|
||||
<span>View XSeries Details</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</ul>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
<div class="course-container">
|
||||
<div class="label-xseries-association">
|
||||
<span class="xseries-icon" aria-hidden="true"></span>
|
||||
<p class="message-copy">XSeries Program Course</p>
|
||||
</div>
|
||||
<div class="course honor">
|
||||
<div class="details">
|
||||
<div class="wrapper-course-image" aria-hidden="true">
|
||||
@@ -186,22 +161,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="message message-status is-shown credit-message">
|
||||
<div class="xseries-action">
|
||||
<div class="message-copy xseries-msg">
|
||||
<p><b class="message-copy-bold">XSeries Program: Interested in more courses in this subject?</b></p>
|
||||
<p class="message-copy">
|
||||
This course is 1 of 3 courses in the <a href="https://www.edx.org/xseries/water-management">Water Management</a> XSeries.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<a class="btn xseries-border-btn" href="https://www.edx.org/xseries/water-management" target="_blank"
|
||||
data-program-id="xseries007">
|
||||
<span class="action-xseries-icon" aria-hidden="true"></span>
|
||||
<span>View XSeries Details</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</ul>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@@ -92,31 +92,6 @@
|
||||
property
|
||||
);
|
||||
});
|
||||
|
||||
it('sends an analytics event when the user clicks the \'View XSeries Details\' button', function() {
|
||||
var $xseries = $('.xseries-action .btn');
|
||||
window.edx.dashboard.trackXseriesBtnClicked(
|
||||
$xseries,
|
||||
window.edx.dashboard.generateProgramProperties);
|
||||
|
||||
expect(window.analytics.trackLink).toHaveBeenCalledWith(
|
||||
$xseries,
|
||||
'edx.bi.dashboard.xseries_cta_message.clicked',
|
||||
window.edx.dashboard.generateProgramProperties
|
||||
);
|
||||
});
|
||||
|
||||
it('sends an analytics event when xseries messages are present in the DOM on page load', function() {
|
||||
window.edx.dashboard.xseriesTrackMessages();
|
||||
expect(window.analytics.track).toHaveBeenCalledWith(
|
||||
'edx.bi.dashboard.xseries_cta_message.viewed',
|
||||
{
|
||||
category: 'dashboard',
|
||||
course_id: 'CTB3365DWx',
|
||||
program_id: 'xseries007'
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}).call(this, window.define);
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
// Uses the Pattern Library
|
||||
|
||||
@import 'elements/banners';
|
||||
@import 'elements/program-card';
|
||||
@import 'elements/course-card';
|
||||
@import 'elements/icons';
|
||||
@import 'views/program-list';
|
||||
@import 'elements/program-card';
|
||||
@import 'elements-v2/icons';
|
||||
@import 'views/program-details';
|
||||
@import 'views/program-list';
|
||||
|
||||
@@ -13,11 +13,12 @@
|
||||
@import 'base/base';
|
||||
|
||||
// base - elements
|
||||
@import 'elements/typography';
|
||||
@import 'elements/controls';
|
||||
@import 'elements/creative-commons';
|
||||
@import 'elements/icons';
|
||||
@import 'elements/navigation';
|
||||
@import 'elements/pagination';
|
||||
@import 'elements/creative-commons';
|
||||
@import 'elements/typography';
|
||||
|
||||
// shared - course
|
||||
@import 'shared/fields';
|
||||
|
||||
34
lms/static/sass/elements-v2/_icons.scss
Normal file
34
lms/static/sass/elements-v2/_icons.scss
Normal file
@@ -0,0 +1,34 @@
|
||||
.xseries-icon {
|
||||
background: url('#{$static-path}/images/programs/xseries-icon.svg') no-repeat;
|
||||
}
|
||||
|
||||
.micromasters-icon {
|
||||
margin-top: $baseline * 0.05;
|
||||
background: url('#{$static-path}/images/programs/micromasters-icon.svg') no-repeat;
|
||||
}
|
||||
|
||||
.certificate-body {
|
||||
// Use the ampersand to reference parent selectors.
|
||||
.certificate-icon & {
|
||||
@include float(left);
|
||||
@include margin-right($baseline*0.4);
|
||||
|
||||
margin-top: ($baseline/10);
|
||||
width: 23px;
|
||||
height: 20px;
|
||||
padding: 2px;
|
||||
background-color: $white;
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.green-icon & {
|
||||
fill: palette(success, text);
|
||||
border-color: palette(success, text);
|
||||
}
|
||||
|
||||
.blue-icon & {
|
||||
fill: palette(primary, dark);
|
||||
border-color: palette(primary, dark);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,8 @@
|
||||
.certificate-icon .certificate-body {
|
||||
@include float(left);
|
||||
@include margin-right($baseline*0.4);
|
||||
|
||||
margin-top: ($baseline/10);
|
||||
width: 23px;
|
||||
height: 20px;
|
||||
padding: 2px;
|
||||
background-color: $white;
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
.xseries-icon {
|
||||
background: url('#{$static-path}/images/programs/xseries-icon.svg') no-repeat;
|
||||
}
|
||||
|
||||
.green-certificate-icon .certificate-body {
|
||||
fill: palette(success, accent);
|
||||
border-color: palette(success, accent);
|
||||
}
|
||||
|
||||
.blue-certificate-icon .certificate-body {
|
||||
fill: palette(primary, dark);
|
||||
border-color: palette(primary, dark);
|
||||
.micromasters-icon {
|
||||
margin-top: $baseline * 0.05;
|
||||
background: url('#{$static-path}/images/programs/micromasters-icon.svg') no-repeat;
|
||||
}
|
||||
|
||||
@@ -97,15 +97,6 @@
|
||||
width: ($baseline*0.7);
|
||||
height: ($baseline*0.7);
|
||||
}
|
||||
|
||||
.xseries-icon{
|
||||
background: url('#{$static-path}/images/programs/xseries-icon.svg') no-repeat;
|
||||
}
|
||||
|
||||
.micromasters-icon{
|
||||
margin-top: $baseline * 0.05;
|
||||
background: url('#{$static-path}/images/programs/micromasters-icon.svg') no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
.hd-3 {
|
||||
|
||||
@@ -62,32 +62,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper-xseries-certificates{
|
||||
@include float(right);
|
||||
@include margin-left(flex-gutter());
|
||||
width: flex-grid(3);
|
||||
|
||||
.title{
|
||||
@extend %t-title7;
|
||||
@extend %t-weight4;
|
||||
}
|
||||
|
||||
ul{
|
||||
@include padding-left(0);
|
||||
margin-top: ($baseline/2);
|
||||
}
|
||||
|
||||
li{
|
||||
@include line-height(20);
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.copy {
|
||||
@extend %t-copy-sub1;
|
||||
margin-top: ($baseline/2);
|
||||
}
|
||||
}
|
||||
|
||||
.profile-sidebar {
|
||||
background: transparent;
|
||||
@include float(right);
|
||||
@@ -304,31 +278,11 @@
|
||||
border-bottom: 4px solid $border-color-l4;
|
||||
padding-bottom: $baseline;
|
||||
|
||||
.course-container{
|
||||
.course-container {
|
||||
border: 1px solid $border-color-l4;
|
||||
border-radius: 3px;
|
||||
|
||||
// CASE: Xseries associated course
|
||||
.label-xseries-association{
|
||||
@include margin($baseline/2, $baseline/5, 0, $baseline/2);
|
||||
|
||||
.xseries-icon{
|
||||
@include float(left);
|
||||
|
||||
@include margin-right($baseline*0.4);
|
||||
background: url('#{$static-path}/images/icon-sm-xseries-black.png') no-repeat;
|
||||
background-color: transparent;
|
||||
|
||||
width: ($baseline*1.1);
|
||||
height: ($baseline*1.1);
|
||||
}
|
||||
|
||||
.message-copy{
|
||||
padding-top: ($baseline/5);
|
||||
@extend %t-action3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
border-bottom: none;
|
||||
@@ -860,100 +814,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.xseries-action{
|
||||
.xseries-msg{
|
||||
@include float(left);
|
||||
width: flex-grid(9, 12);
|
||||
}
|
||||
|
||||
.message-copy{
|
||||
@extend %t-demi-strong;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.message-copy-bold{
|
||||
@extend %t-strong;
|
||||
}
|
||||
|
||||
.xseries-border-btn {
|
||||
@extend %btn-pl-black-border;
|
||||
@include float(right);
|
||||
position: relative;
|
||||
@include left(10px);
|
||||
padding: ($baseline*0.4) ($baseline*0.6);
|
||||
background-image: none ;
|
||||
text-shadow: none;
|
||||
box-shadow: none;
|
||||
text-transform: none;
|
||||
|
||||
.action-xseries-icon{
|
||||
@include float(left);
|
||||
display: inline;
|
||||
|
||||
@include margin-right($baseline*0.4);
|
||||
background: url('#{$static-path}/images/icon-sm-xseries-black.png') no-repeat;
|
||||
background-color: transparent;
|
||||
|
||||
width: ($baseline*1.1);
|
||||
height: ($baseline*1.1);
|
||||
}
|
||||
&:hover,
|
||||
&:focus {
|
||||
|
||||
.action-xseries-icon{
|
||||
@include float(left);
|
||||
display: inline;
|
||||
|
||||
@include margin-right($baseline*0.4);
|
||||
background: url('#{$static-path}/images/icon-sm-xseries-white.png') no-repeat;
|
||||
background-color: transparent;
|
||||
|
||||
width: ($baseline*1.1);
|
||||
height: ($baseline*1.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.xseries-base-btn {
|
||||
@extend %btn-pl-black-base;
|
||||
@include float(right);
|
||||
position: relative;
|
||||
@include left(10px);
|
||||
padding: ($baseline*0.4) ($baseline*0.6);
|
||||
background-image: none ;
|
||||
text-shadow: none;
|
||||
box-shadow: none;
|
||||
text-transform: none;
|
||||
|
||||
.action-xseries-icon{
|
||||
@include float(left);
|
||||
display: inline;
|
||||
|
||||
@include margin-right($baseline*0.4);
|
||||
background: url('#{$static-path}/images/icon-sm-xseries-white.png') no-repeat;
|
||||
background-color: transparent;
|
||||
|
||||
width: ($baseline*1.1);
|
||||
height: ($baseline*1.1);
|
||||
}
|
||||
&:hover,
|
||||
&:focus {
|
||||
|
||||
.action-xseries-icon {
|
||||
@include float(left);
|
||||
display: inline;
|
||||
|
||||
@include margin-right($baseline*0.4);
|
||||
background: url('#{$static-path}/images/icon-sm-xseries-black.png') no-repeat;
|
||||
background-color: transparent;
|
||||
|
||||
width: ($baseline*1.1);
|
||||
height: ($baseline*1.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
|
||||
.action {
|
||||
@@ -1129,6 +989,46 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.message-related-programs {
|
||||
background: none;
|
||||
border: none;
|
||||
margin-top: ($baseline/4);
|
||||
padding-bottom: 0;
|
||||
|
||||
.related-programs-preface {
|
||||
@include float(left);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
ul {
|
||||
display: inline;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
@include float(left);
|
||||
display: inline;
|
||||
padding: 0 0.5em;
|
||||
border-right: 1px solid;
|
||||
|
||||
.category-icon {
|
||||
@include float(left);
|
||||
@include margin-right($baseline/4);
|
||||
margin-top: ($baseline/10);
|
||||
background-color: transparent;
|
||||
background-size: 100%;
|
||||
width: ($baseline*0.7);
|
||||
height: ($baseline*0.7);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove separator from last list item.
|
||||
li:last-child {
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// TYPE: pre-requisites
|
||||
.prerequisites {
|
||||
@include clearfix;
|
||||
|
||||
@@ -98,8 +98,8 @@ from openedx.core.djangolib.markup import HTML, Text
|
||||
<% is_course_blocked = (enrollment.course_id in block_courses) %>
|
||||
<% course_verification_status = verification_status_by_course.get(enrollment.course_id, {}) %>
|
||||
<% course_requirements = courses_requirements_not_met.get(enrollment.course_id) %>
|
||||
<% course_program_info = course_programs.get(unicode(enrollment.course_id)) %>
|
||||
<%include file = 'dashboard/_dashboard_course_listing.html' args="course_overview=enrollment.course_overview, enrollment=enrollment, show_courseware_link=show_courseware_link, cert_status=cert_status, can_unenroll=can_unenroll, credit_status=credit_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, show_refund_option=show_refund_option, is_paid_course=is_paid_course, is_course_blocked=is_course_blocked, verification_status=course_verification_status, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings, user=user, course_program_info=course_program_info" />
|
||||
<% related_programs = programs_by_run.get(unicode(enrollment.course_id)) %>
|
||||
<%include file = 'dashboard/_dashboard_course_listing.html' args="course_overview=enrollment.course_overview, enrollment=enrollment, show_courseware_link=show_courseware_link, cert_status=cert_status, can_unenroll=can_unenroll, credit_status=credit_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, show_refund_option=show_refund_option, is_paid_course=is_paid_course, is_course_blocked=is_course_blocked, verification_status=course_verification_status, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings, user=user, related_programs=related_programs" />
|
||||
% endfor
|
||||
|
||||
</ul>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<%page args="course_overview, enrollment, show_courseware_link, cert_status, can_unenroll, credit_status, show_email_settings, course_mode_info, show_refund_option, is_paid_course, is_course_blocked, verification_status, course_requirements, dashboard_index, share_settings, course_program_info" expression_filter="h"/>
|
||||
<%page args="course_overview, enrollment, show_courseware_link, cert_status, can_unenroll, credit_status, show_email_settings, course_mode_info, show_refund_option, is_paid_course, is_course_blocked, verification_status, course_requirements, dashboard_index, share_settings, related_programs" expression_filter="h"/>
|
||||
|
||||
<%!
|
||||
import urllib
|
||||
@@ -53,12 +53,6 @@ from student.helpers import (
|
||||
<% mode_class = '' %>
|
||||
% endif
|
||||
<div class="course-container">
|
||||
% if course_program_info and course_program_info.get('category')=='XSeries':
|
||||
<div class="label-xseries-association">
|
||||
<span class="xseries-icon" aria-hidden="true"></span>
|
||||
<p class="message-copy">${_("{category} Program Course").format(category=course_program_info['category'])}</p>
|
||||
</div>
|
||||
% endif
|
||||
<article class="course${mode_class}">
|
||||
<% course_target = reverse('info', args=[unicode(course_overview.id)]) %>
|
||||
<section class="details" aria-labelledby="details-heading-${course_overview.number}">
|
||||
@@ -283,6 +277,20 @@ from student.helpers import (
|
||||
</section>
|
||||
<footer class="wrapper-messages-primary">
|
||||
<ul class="messages-list">
|
||||
% if related_programs:
|
||||
<div class="message message-related-programs is-shown">
|
||||
<span class="related-programs-preface">${_('Related Programs')}:</span>
|
||||
<ul>
|
||||
% for program in related_programs:
|
||||
<li>
|
||||
<span class="category-icon ${program['category'].lower()}-icon" aria-hidden="true"></span>
|
||||
<span><a href="${program['detail_url']}">${'{name} {category}'.format(name=program['name'], category=program['category'])}</a></span>
|
||||
</li>
|
||||
% endfor
|
||||
</ul>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
% if course_overview.may_certify() and cert_status:
|
||||
<%include file='_dashboard_certificate_information.html' args='cert_status=cert_status,course_overview=course_overview, enrollment=enrollment, reverify_link=reverify_link'/>
|
||||
% endif
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
<%page expression_filter="h" args="program_data, enrollment_mode, category" />
|
||||
<%!
|
||||
from django.utils.translation import ugettext as _
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
%>
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
<div class="message message-status is-shown credit-message">
|
||||
<div class="xseries-action">
|
||||
<div class="message-copy xseries-msg">
|
||||
<p class="message-copy-bold">
|
||||
${_("{category} Program: Interested in more courses in this subject?").format(category=category)}
|
||||
</p>
|
||||
<p class="message-copy">
|
||||
${Text(_("This course is 1 of {course_count} courses in the {link_start}{program_display_name}{link_end} {program_category}.")).format(
|
||||
course_count=program_data['course_count'],
|
||||
link_start=HTML('<a href="{}">').format(program_data['program_marketing_url']),
|
||||
link_end=HTML('</a>'),
|
||||
program_display_name=program_data['display_name'],
|
||||
program_category=category,
|
||||
)}
|
||||
</p>
|
||||
|
||||
</div>
|
||||
<%
|
||||
xseries_btn_class = "xseries-border-btn"
|
||||
if enrollment_mode == "verified":
|
||||
xseries_btn_class = "xseries-base-btn";
|
||||
%>
|
||||
<a class="btn ${xseries_btn_class}" href="${program_data['program_marketing_url']}" target="_blank"
|
||||
data-program-id="${program_data['program_id']}" >
|
||||
<span class="sr">${program_data['display_name']}</span>
|
||||
<span class="action-xseries-icon" aria-hidden="true"></span>
|
||||
${_("View {category} Details").format(category=category)}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,12 +1,12 @@
|
||||
<div class="message col-12 md-col-8">
|
||||
<% // safe-lint: disable=underscore-not-escaped %>
|
||||
<span class="certificate-icon green-certificate-icon" aria-hidden="true"><%= certificateSvg %></span>
|
||||
<span class="certificate-icon green-icon" aria-hidden="true"><%= certificateSvg %></span>
|
||||
<span class="card-msg"><%- gettext('Congratulations! You have earned a certificate for this course.') %></span>
|
||||
</div>
|
||||
<div class="action col-12 md-col-4">
|
||||
<a href="<%- certificate_url %>" class="btn-brand cta-secondary">
|
||||
<% // safe-lint: disable=underscore-not-escaped %>
|
||||
<span class="certificate-icon blue-certificate-icon" aria-hidden="true"><%= certificateSvg %></span>
|
||||
<span class="certificate-icon blue-icon" aria-hidden="true"><%= certificateSvg %></span>
|
||||
<%- gettext('View Certificate') %>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<div class="message col-12 md-col-8">
|
||||
<% // safe-lint: disable=underscore-not-escaped %>
|
||||
<span class="certificate-icon green-certificate-icon" aria-hidden="true"><%= certificateSvg %></span>
|
||||
<span class="certificate-icon green-icon" aria-hidden="true"><%= certificateSvg %></span>
|
||||
<span class="card-msg"><%- gettext('You need a certificate in this course to be eligible for a program certificate.') %></span>
|
||||
</div>
|
||||
<div class="action col-12 md-col-4">
|
||||
<a href="<%- upgrade_url %>" class="btn-brand cta-primary">
|
||||
<% // safe-lint: disable=underscore-not-escaped %>
|
||||
<span class="certificate-icon green-certificate-icon" aria-hidden="true"><%= certificateSvg %></span>
|
||||
<span class="certificate-icon green-icon" aria-hidden="true"><%= certificateSvg %></span>
|
||||
<%- gettext('Upgrade Now') %>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ from django.db import models
|
||||
from config_models.models import ConfigurationModel
|
||||
|
||||
|
||||
# TODO: To be simplified as part of ECOM-5136.
|
||||
class ProgramsApiConfig(ConfigurationModel):
|
||||
"""
|
||||
Manages configuration for connecting to the Programs service and using its
|
||||
@@ -29,7 +30,6 @@ class ProgramsApiConfig(ConfigurationModel):
|
||||
)
|
||||
)
|
||||
|
||||
# TODO: The property below is obsolete. Delete at the earliest safe moment. See ECOM-4995
|
||||
authoring_app_js_path = models.CharField(
|
||||
verbose_name=_("Path to authoring app's JS"),
|
||||
max_length=255,
|
||||
@@ -39,7 +39,6 @@ class ProgramsApiConfig(ConfigurationModel):
|
||||
)
|
||||
)
|
||||
|
||||
# TODO: The property below is obsolete. Delete at the earliest safe moment. See ECOM-4995
|
||||
authoring_app_css_path = models.CharField(
|
||||
verbose_name=_("Path to authoring app's CSS"),
|
||||
max_length=255,
|
||||
@@ -81,7 +80,6 @@ class ProgramsApiConfig(ConfigurationModel):
|
||||
)
|
||||
)
|
||||
|
||||
# TODO: Remove unused field.
|
||||
xseries_ad_enabled = models.BooleanField(
|
||||
verbose_name=_("Do we want to show xseries program advertising"),
|
||||
default=False
|
||||
@@ -116,14 +114,6 @@ class ProgramsApiConfig(ConfigurationModel):
|
||||
"""Whether responses from the Programs API will be cached."""
|
||||
return self.cache_ttl > 0
|
||||
|
||||
@property
|
||||
def is_student_dashboard_enabled(self):
|
||||
"""
|
||||
Indicates whether LMS dashboard functionality related to Programs should
|
||||
be enabled or not.
|
||||
"""
|
||||
return self.enabled and self.enable_student_dashboard
|
||||
|
||||
@property
|
||||
def is_studio_tab_enabled(self):
|
||||
"""
|
||||
|
||||
@@ -14,7 +14,7 @@ class Program(factory.Factory):
|
||||
name = FuzzyText(prefix='Program ')
|
||||
subtitle = FuzzyText(prefix='Subtitle ')
|
||||
category = 'FooBar'
|
||||
status = 'unpublished'
|
||||
status = 'active'
|
||||
marketing_slug = FuzzyText(prefix='slug_')
|
||||
organizations = []
|
||||
course_codes = []
|
||||
|
||||
@@ -16,7 +16,6 @@ class ProgramsApiConfigMixin(object):
|
||||
'internal_service_url': 'http://internal.programs.org/',
|
||||
'public_service_url': 'http://public.programs.org/',
|
||||
'cache_ttl': 0,
|
||||
'enable_student_dashboard': True,
|
||||
'enable_studio_tab': True,
|
||||
'enable_certification': True,
|
||||
'program_listing_enabled': True,
|
||||
|
||||
@@ -36,20 +36,6 @@ class TestProgramsApiConfig(ProgramsApiConfigMixin, TestCase):
|
||||
programs_config = self.create_programs_config(cache_ttl=cache_ttl)
|
||||
self.assertEqual(programs_config.is_cache_enabled, is_cache_enabled)
|
||||
|
||||
def test_is_student_dashboard_enabled(self, _mock_cache):
|
||||
"""
|
||||
Verify that the property controlling display on the student dashboard is only True
|
||||
when configuration is enabled and all required configuration is provided.
|
||||
"""
|
||||
programs_config = self.create_programs_config(enabled=False)
|
||||
self.assertFalse(programs_config.is_student_dashboard_enabled)
|
||||
|
||||
programs_config = self.create_programs_config(enable_student_dashboard=False)
|
||||
self.assertFalse(programs_config.is_student_dashboard_enabled)
|
||||
|
||||
programs_config = self.create_programs_config()
|
||||
self.assertTrue(programs_config.is_student_dashboard_enabled)
|
||||
|
||||
def test_is_studio_tab_enabled(self, _mock_cache):
|
||||
"""
|
||||
Verify that the property controlling display of the Studio tab is only True
|
||||
|
||||
@@ -12,9 +12,11 @@ from django.core.urlresolvers import reverse
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from django.utils import timezone
|
||||
from django.utils.text import slugify
|
||||
import httpretty
|
||||
import mock
|
||||
from nose.plugins.attrib import attr
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from edx_oauth2_provider.tests.factories import ClientFactory
|
||||
from provider.constants import CONFIDENTIAL
|
||||
|
||||
@@ -141,36 +143,6 @@ class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin, Credential
|
||||
actual = utils.get_programs(self.user)
|
||||
self.assertEqual(actual, [])
|
||||
|
||||
def test_get_programs_for_dashboard(self):
|
||||
"""Verify programs data can be retrieved and parsed correctly."""
|
||||
self.create_programs_config()
|
||||
self.mock_programs_api()
|
||||
|
||||
actual = utils.get_programs_for_dashboard(self.user, self.COURSE_KEYS)
|
||||
expected = {}
|
||||
for program in self.PROGRAMS_API_RESPONSE['results']:
|
||||
for course_code in program['course_codes']:
|
||||
for run in course_code['run_modes']:
|
||||
course_key = run['course_key']
|
||||
expected.setdefault(course_key, []).append(program)
|
||||
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
def test_get_programs_for_dashboard_dashboard_display_disabled(self):
|
||||
"""Verify behavior when student dashboard display is disabled."""
|
||||
self.create_programs_config(enable_student_dashboard=False)
|
||||
|
||||
actual = utils.get_programs_for_dashboard(self.user, self.COURSE_KEYS)
|
||||
self.assertEqual(actual, {})
|
||||
|
||||
def test_get_programs_for_dashboard_no_data(self):
|
||||
"""Verify behavior when no programs data is found for the user."""
|
||||
self.create_programs_config()
|
||||
self.mock_programs_api(data={'results': []})
|
||||
|
||||
actual = utils.get_programs_for_dashboard(self.user, self.COURSE_KEYS)
|
||||
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()
|
||||
@@ -218,6 +190,78 @@ class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin, Credential
|
||||
self.assertEqual(actual, [])
|
||||
|
||||
|
||||
@skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
class GetProgramsByRunTests(TestCase):
|
||||
"""Tests verifying that programs are inverted correctly."""
|
||||
maxDiff = None
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(GetProgramsByRunTests, cls).setUpClass()
|
||||
|
||||
cls.user = UserFactory()
|
||||
|
||||
course_keys = [
|
||||
CourseKey.from_string('some/course/run'),
|
||||
CourseKey.from_string('some/other/run'),
|
||||
]
|
||||
|
||||
cls.enrollments = [CourseEnrollmentFactory(user=cls.user, course_id=c) for c in course_keys]
|
||||
cls.course_ids = [unicode(c) for c in course_keys]
|
||||
|
||||
organization = factories.Organization()
|
||||
joint_programs = sorted([
|
||||
factories.Program(
|
||||
organizations=[organization],
|
||||
course_codes=[
|
||||
factories.CourseCode(run_modes=[
|
||||
factories.RunMode(course_key=cls.course_ids[0]),
|
||||
]),
|
||||
]
|
||||
) for __ in range(2)
|
||||
], key=lambda p: p['name'])
|
||||
|
||||
cls.programs = joint_programs + [
|
||||
factories.Program(
|
||||
organizations=[organization],
|
||||
course_codes=[
|
||||
factories.CourseCode(run_modes=[
|
||||
factories.RunMode(course_key=cls.course_ids[1]),
|
||||
]),
|
||||
]
|
||||
),
|
||||
factories.Program(
|
||||
organizations=[organization],
|
||||
course_codes=[
|
||||
factories.CourseCode(run_modes=[
|
||||
factories.RunMode(course_key='yet/another/run'),
|
||||
]),
|
||||
]
|
||||
),
|
||||
]
|
||||
|
||||
def test_get_programs_by_run(self):
|
||||
"""Verify that programs are organized by run ID."""
|
||||
programs_by_run, course_ids = utils.get_programs_by_run(self.programs, self.enrollments)
|
||||
|
||||
self.assertEqual(programs_by_run[self.course_ids[0]], self.programs[:2])
|
||||
self.assertEqual(programs_by_run[self.course_ids[1]], self.programs[2:3])
|
||||
|
||||
self.assertEqual(course_ids, self.course_ids)
|
||||
|
||||
def test_no_programs(self):
|
||||
"""Verify that the utility can cope with missing programs data."""
|
||||
programs_by_run, course_ids = utils.get_programs_by_run([], self.enrollments)
|
||||
self.assertEqual(programs_by_run, {})
|
||||
self.assertEqual(course_ids, self.course_ids)
|
||||
|
||||
def test_no_enrollments(self):
|
||||
"""Verify that the utility can cope with missing enrollment data."""
|
||||
programs_by_run, course_ids = utils.get_programs_by_run(self.programs, [])
|
||||
self.assertEqual(programs_by_run, {})
|
||||
self.assertEqual(course_ids, [])
|
||||
|
||||
|
||||
@skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
class GetCompletedCoursesTestCase(TestCase):
|
||||
"""
|
||||
@@ -297,6 +341,14 @@ class TestProgramProgressMeter(ProgramsApiConfigMixin, TestCase):
|
||||
"""Construct a list containing the display names of the indicated course codes."""
|
||||
return [program['course_codes'][cc]['display_name'] for cc in course_codes]
|
||||
|
||||
def _attach_detail_url(self, programs):
|
||||
"""Add expected detail URLs to a list of program dicts."""
|
||||
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)
|
||||
|
||||
def test_no_enrollments(self):
|
||||
"""Verify behavior when programs exist, but no relevant enrollments do."""
|
||||
data = [
|
||||
@@ -311,7 +363,7 @@ class TestProgramProgressMeter(ProgramsApiConfigMixin, TestCase):
|
||||
|
||||
meter = utils.ProgramProgressMeter(self.user)
|
||||
|
||||
self.assertEqual(meter.engaged_programs, [])
|
||||
self.assertEqual(meter.engaged_programs(), [])
|
||||
self._assert_progress(meter)
|
||||
self.assertEqual(meter.completed_programs, [])
|
||||
|
||||
@@ -322,7 +374,7 @@ class TestProgramProgressMeter(ProgramsApiConfigMixin, TestCase):
|
||||
self._create_enrollments('org/course/run')
|
||||
meter = utils.ProgramProgressMeter(self.user)
|
||||
|
||||
self.assertEqual(meter.engaged_programs, [])
|
||||
self.assertEqual(meter.engaged_programs(), [])
|
||||
self._assert_progress(meter)
|
||||
self.assertEqual(meter.completed_programs, [])
|
||||
|
||||
@@ -353,8 +405,9 @@ class TestProgramProgressMeter(ProgramsApiConfigMixin, TestCase):
|
||||
self._create_enrollments(course_id)
|
||||
meter = utils.ProgramProgressMeter(self.user)
|
||||
|
||||
self._attach_detail_url(data)
|
||||
program = data[0]
|
||||
self.assertEqual(meter.engaged_programs, [program])
|
||||
self.assertEqual(meter.engaged_programs(), [program])
|
||||
self._assert_progress(
|
||||
meter,
|
||||
factories.Progress(
|
||||
@@ -399,8 +452,9 @@ class TestProgramProgressMeter(ProgramsApiConfigMixin, TestCase):
|
||||
self._create_enrollments(second_course_id, first_course_id)
|
||||
meter = utils.ProgramProgressMeter(self.user)
|
||||
|
||||
self._attach_detail_url(data)
|
||||
programs = data[:2]
|
||||
self.assertEqual(meter.engaged_programs, programs)
|
||||
self.assertEqual(meter.engaged_programs(), programs)
|
||||
self._assert_progress(
|
||||
meter,
|
||||
factories.Progress(id=programs[0]['id'], in_progress=self._extract_names(programs[0], 0)),
|
||||
@@ -414,7 +468,8 @@ class TestProgramProgressMeter(ProgramsApiConfigMixin, TestCase):
|
||||
appearing in multiple programs.
|
||||
"""
|
||||
shared_course_id, solo_course_id = 'org/shared-course/run', 'org/solo-course/run'
|
||||
data = [
|
||||
|
||||
joint_programs = sorted([
|
||||
factories.Program(
|
||||
organizations=[factories.Organization()],
|
||||
course_codes=[
|
||||
@@ -422,15 +477,10 @@ class TestProgramProgressMeter(ProgramsApiConfigMixin, TestCase):
|
||||
factories.RunMode(course_key=shared_course_id),
|
||||
]),
|
||||
]
|
||||
),
|
||||
factories.Program(
|
||||
organizations=[factories.Organization()],
|
||||
course_codes=[
|
||||
factories.CourseCode(run_modes=[
|
||||
factories.RunMode(course_key=shared_course_id),
|
||||
]),
|
||||
]
|
||||
),
|
||||
) for __ in range(2)
|
||||
], key=lambda p: p['name'])
|
||||
|
||||
data = joint_programs + [
|
||||
factories.Program(
|
||||
organizations=[factories.Organization()],
|
||||
course_codes=[
|
||||
@@ -446,14 +496,16 @@ class TestProgramProgressMeter(ProgramsApiConfigMixin, TestCase):
|
||||
]
|
||||
),
|
||||
]
|
||||
|
||||
self._mock_programs_api(data)
|
||||
|
||||
# Enrollment for the shared course ID created last (most recently).
|
||||
self._create_enrollments(solo_course_id, shared_course_id)
|
||||
meter = utils.ProgramProgressMeter(self.user)
|
||||
|
||||
self._attach_detail_url(data)
|
||||
programs = data[:3]
|
||||
self.assertEqual(meter.engaged_programs, programs)
|
||||
self.assertEqual(meter.engaged_programs(), programs)
|
||||
self._assert_progress(
|
||||
meter,
|
||||
factories.Progress(id=programs[0]['id'], in_progress=self._extract_names(programs[0], 0)),
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
"""Helper functions for working with Programs."""
|
||||
import datetime
|
||||
import logging
|
||||
from urlparse import urljoin
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.text import slugify
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
import pytz
|
||||
@@ -52,63 +53,6 @@ def get_programs(user, program_id=None):
|
||||
return get_edx_api_data(programs_config, user, 'programs', resource_id=program_id, cache_key=cache_key)
|
||||
|
||||
|
||||
def flatten_programs(programs, course_ids):
|
||||
"""Flatten the result returned by the Programs API.
|
||||
|
||||
Arguments:
|
||||
programs (list): Serialized programs
|
||||
course_ids (list): Course IDs to key on.
|
||||
|
||||
Returns:
|
||||
dict, programs keyed by course ID
|
||||
"""
|
||||
flattened = {}
|
||||
|
||||
for program in programs:
|
||||
try:
|
||||
for course_code in program['course_codes']:
|
||||
for run in course_code['run_modes']:
|
||||
run_id = run['course_key']
|
||||
if run_id in course_ids:
|
||||
flattened.setdefault(run_id, []).append(program)
|
||||
except KeyError:
|
||||
log.exception('Unable to parse Programs API response: %r', program)
|
||||
|
||||
return flattened
|
||||
|
||||
|
||||
def get_programs_for_dashboard(user, course_keys):
|
||||
"""Build a dictionary of programs, keyed by course.
|
||||
|
||||
Given a user and an iterable of course keys, find all the programs relevant
|
||||
to the user's dashboard and return them in a dictionary keyed by course key.
|
||||
|
||||
Arguments:
|
||||
user (User): The user to authenticate as when requesting programs.
|
||||
course_keys (list): List of course keys representing the courses in which
|
||||
the given user has active enrollments.
|
||||
|
||||
Returns:
|
||||
dict, containing programs keyed by course. Empty if programs cannot be retrieved.
|
||||
"""
|
||||
programs_config = ProgramsApiConfig.current()
|
||||
course_programs = {}
|
||||
|
||||
if not programs_config.is_student_dashboard_enabled:
|
||||
log.debug('Display of programs on the student dashboard is disabled.')
|
||||
return course_programs
|
||||
|
||||
programs = get_programs(user)
|
||||
if not programs:
|
||||
log.debug('No programs found for the user with ID %d.', user.id)
|
||||
return course_programs
|
||||
|
||||
course_ids = [unicode(c) for c in course_keys]
|
||||
course_programs = flatten_programs(programs, course_ids)
|
||||
|
||||
return course_programs
|
||||
|
||||
|
||||
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.
|
||||
@@ -137,24 +81,71 @@ def get_programs_for_credentials(user, programs_credentials):
|
||||
return certificate_programs
|
||||
|
||||
|
||||
def get_program_detail_url(program, marketing_root):
|
||||
"""Construct the URL to be used when linking to program details.
|
||||
def get_programs_by_run(programs, enrollments):
|
||||
"""Intersect programs and enrollments.
|
||||
|
||||
Builds a dictionary of program dict lists keyed by course ID. The resulting dictionary
|
||||
is suitable for use in applications where programs must be filtered by the course
|
||||
runs they contain (e.g., student dashboard).
|
||||
|
||||
Arguments:
|
||||
program (dict): Representation of a program.
|
||||
marketing_root (str): Root URL used to build links to program marketing pages.
|
||||
programs (list): Containing dictionaries representing programs.
|
||||
enrollments (list): Enrollments from which course IDs to key on can be extracted.
|
||||
|
||||
Returns:
|
||||
str, a link to program details
|
||||
tuple, dict of programs keyed by course ID and list of course IDs themselves
|
||||
"""
|
||||
if ProgramsApiConfig.current().show_program_details:
|
||||
base = reverse('program_details_view', kwargs={'program_id': program['id']}).rstrip('/')
|
||||
slug = slugify(program['name'])
|
||||
else:
|
||||
base = marketing_root.rstrip('/')
|
||||
slug = program['marketing_slug']
|
||||
programs_by_run = {}
|
||||
# enrollment.course_id is really a course key (╯ಠ_ಠ)╯︵ ┻━┻
|
||||
course_ids = [unicode(e.course_id) for e in enrollments]
|
||||
|
||||
return '{base}/{slug}'.format(base=base, slug=slug)
|
||||
for program in programs:
|
||||
for course_code in program['course_codes']:
|
||||
for run in course_code['run_modes']:
|
||||
run_id = run['course_key']
|
||||
if run_id in course_ids:
|
||||
program_list = programs_by_run.setdefault(run_id, list())
|
||||
if program not in program_list:
|
||||
program_list.append(program)
|
||||
|
||||
# Sort programs by name for consistent presentation.
|
||||
for program_list in programs_by_run.itervalues():
|
||||
program_list.sort(key=lambda p: p['name'])
|
||||
|
||||
return programs_by_run, course_ids
|
||||
|
||||
|
||||
def get_program_marketing_url(programs_config):
|
||||
"""Build a URL to be used when linking to program details on a marketing site."""
|
||||
return urljoin(settings.MKTG_URLS.get('ROOT'), programs_config.marketing_path).rstrip('/')
|
||||
|
||||
|
||||
def attach_program_detail_url(programs):
|
||||
"""Extend program representations by attaching a URL to be used when linking to program details.
|
||||
|
||||
Facilitates the building of context to be passed to templates containing program data.
|
||||
|
||||
Arguments:
|
||||
programs (list): Containing dicts representing programs.
|
||||
|
||||
Returns:
|
||||
list, containing extended program dicts
|
||||
"""
|
||||
programs_config = ProgramsApiConfig.current()
|
||||
marketing_url = get_program_marketing_url(programs_config)
|
||||
|
||||
for program in programs:
|
||||
if programs_config.show_program_details:
|
||||
base = reverse('program_details_view', kwargs={'program_id': program['id']}).rstrip('/')
|
||||
slug = slugify(program['name'])
|
||||
else:
|
||||
# TODO: Remove. Learners should always be sent to the LMS' program details page.
|
||||
base = marketing_url
|
||||
slug = program['marketing_slug']
|
||||
|
||||
program['detail_url'] = '{base}/{slug}'.format(base=base, slug=slug)
|
||||
|
||||
return programs
|
||||
|
||||
|
||||
def get_completed_courses(student):
|
||||
@@ -182,35 +173,40 @@ class ProgramProgressMeter(object):
|
||||
|
||||
Arguments:
|
||||
user (User): The user for which to find programs.
|
||||
|
||||
Keyword Arguments:
|
||||
enrollments (list): List of the user's enrollments.
|
||||
"""
|
||||
def __init__(self, user):
|
||||
def __init__(self, user, enrollments=None):
|
||||
self.user = user
|
||||
self.enrollments = enrollments
|
||||
self.course_ids = None
|
||||
self.course_certs = None
|
||||
|
||||
self.programs = get_programs(self.user)
|
||||
self.course_certs = get_completed_courses(self.user)
|
||||
self.programs = attach_program_detail_url(get_programs(self.user))
|
||||
|
||||
@cached_property
|
||||
def engaged_programs(self):
|
||||
def engaged_programs(self, by_run=False):
|
||||
"""Derive a list of programs in which the given user is engaged.
|
||||
|
||||
Returns:
|
||||
list of program dicts, ordered by most recent enrollment.
|
||||
list of program dicts, ordered by most recent enrollment,
|
||||
or dict of programs, keyed by course ID.
|
||||
"""
|
||||
enrollments = CourseEnrollment.enrollments_for_user(self.user)
|
||||
enrollments = sorted(enrollments, key=lambda e: e.created, reverse=True)
|
||||
# enrollment.course_id is really a course key ಠ_ಠ
|
||||
self.course_ids = [unicode(e.course_id) for e in enrollments]
|
||||
self.enrollments = self.enrollments or list(CourseEnrollment.enrollments_for_user(self.user))
|
||||
self.enrollments.sort(key=lambda e: e.created, reverse=True)
|
||||
|
||||
flattened = flatten_programs(self.programs, self.course_ids)
|
||||
programs_by_run, self.course_ids = get_programs_by_run(self.programs, self.enrollments)
|
||||
|
||||
engaged_programs = []
|
||||
if by_run:
|
||||
return programs_by_run
|
||||
|
||||
programs = []
|
||||
for course_id in self.course_ids:
|
||||
for program in flattened.get(course_id, []):
|
||||
if program not in engaged_programs:
|
||||
engaged_programs.append(program)
|
||||
for program in programs_by_run.get(course_id, []):
|
||||
if program not in programs:
|
||||
programs.append(program)
|
||||
|
||||
return engaged_programs
|
||||
return programs
|
||||
|
||||
@property
|
||||
def progress(self):
|
||||
@@ -221,7 +217,7 @@ class ProgramProgressMeter(object):
|
||||
towards completing a program.
|
||||
"""
|
||||
progress = []
|
||||
for program in self.engaged_programs:
|
||||
for program in self.engaged_programs():
|
||||
completed, in_progress, not_started = [], [], []
|
||||
|
||||
for course_code in program['course_codes']:
|
||||
@@ -277,6 +273,8 @@ class ProgramProgressMeter(object):
|
||||
Returns:
|
||||
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):
|
||||
|
||||
@@ -1877,6 +1877,7 @@ class TestGoogleRegistrationView(
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
class UpdateEmailOptInTestCase(UserAPITestCase, SharedModuleStoreTestCase):
|
||||
"""Tests the UpdateEmailOptInPreference view. """
|
||||
|
||||
|
||||
@@ -99,8 +99,8 @@ from openedx.core.djangoapps.theming import helpers as theming_helpers
|
||||
<% is_course_blocked = (enrollment.course_id in block_courses) %>
|
||||
<% course_verification_status = verification_status_by_course.get(enrollment.course_id, {}) %>
|
||||
<% course_requirements = courses_requirements_not_met.get(enrollment.course_id) %>
|
||||
<% course_program_info = course_programs.get(unicode(enrollment.course_id)) %>
|
||||
<%include file = 'dashboard/_dashboard_course_listing.html' args="course_overview=enrollment.course_overview, enrollment=enrollment, show_courseware_link=show_courseware_link, cert_status=cert_status, can_unenroll=can_unenroll, credit_status=credit_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, show_refund_option=show_refund_option, is_paid_course=is_paid_course, is_course_blocked=is_course_blocked, verification_status=course_verification_status, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings, user=user, course_program_info=course_program_info" />
|
||||
<% related_programs = programs_by_run.get(unicode(enrollment.course_id)) %>
|
||||
<%include file = 'dashboard/_dashboard_course_listing.html' args="course_overview=enrollment.course_overview, enrollment=enrollment, show_courseware_link=show_courseware_link, cert_status=cert_status, can_unenroll=can_unenroll, credit_status=credit_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, show_refund_option=show_refund_option, is_paid_course=is_paid_course, is_course_blocked=is_course_blocked, verification_status=course_verification_status, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings, user=user, related_programs=related_programs" />
|
||||
% endfor
|
||||
|
||||
</ul>
|
||||
|
||||
Reference in New Issue
Block a user