Commit fa7ed070 by Ahsan Ulhaq

Merge pull request #12468 from edx/ahsan/ECOM-4398-Course-Dashboard-Visual-Update

Course Dashboard Visual Update
parents f7e98062 a8f2de83
......@@ -122,8 +122,6 @@ from eventtracking import tracker
# Note that this lives in LMS, so this dependency should be refactored.
from notification_prefs.views import enable_notifications
# Note that this lives in openedx, so this dependency should be refactored.
from openedx.core.djangoapps.credentials.utils import get_user_program_credentials
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, get_display_category
......@@ -615,7 +613,6 @@ def dashboard(request):
# 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])
xseries_credentials = _get_xseries_credentials(user)
# Construct a dictionary of course mode information
# used to render the course list. We re-use the course modes dict
......@@ -741,7 +738,6 @@ def dashboard(request):
'nav_hidden': True,
'course_programs': course_programs,
'disable_courseware_js': True,
'xseries_credentials': xseries_credentials,
'show_program_listing': ProgramsApiConfig.current().show_program_listing,
}
......@@ -2483,34 +2479,3 @@ def _get_course_programs(user, user_enrolled_courses): # pylint: disable=invali
log.warning('Program structure is invalid, skipping display: %r', program)
return programs_data
def _get_xseries_credentials(user):
"""Return program credentials data required for display on
the learner dashboard.
Given a user, find all programs for which certificates have been earned
and return list of dictionaries of required program data.
Arguments:
user (User): user object for getting programs credentials.
Returns:
list of dict, containing data corresponding to the programs for which
the user has been awarded a credential.
"""
programs_credentials = get_user_program_credentials(user)
credentials_data = []
for program in programs_credentials:
if program.get('category') == 'xseries':
try:
program_data = {
'display_name': program['name'],
'subtitle': program['subtitle'],
'credential_url': program['credential_url'],
}
credentials_data.append(program_data)
except KeyError:
log.warning('Program structure is invalid: %r', program)
return credentials_data
......@@ -14,6 +14,7 @@ from opaque_keys.edx import locator
from provider.constants import CONFIDENTIAL
from openedx.core.djangoapps.credentials.models import CredentialsApiConfig
from openedx.core.djangoapps.credentials.tests import factories as credentials_factories
from openedx.core.djangoapps.credentials.tests.mixins import CredentialsDataMixin, CredentialsApiConfigMixin
from openedx.core.djangoapps.programs.tests.mixins import (
ProgramsApiConfigMixin,
......@@ -155,18 +156,36 @@ class TestProgramListing(
'{}?next={}'.format(reverse('signin_user'), self.url)
)
# TODO: Use a factory to generate this data.
def _expected_progam_credentials_data(self):
"""
Dry method for getting expected program credentials response data.
"""
return [
credentials_factories.UserCredential(
id=1,
username='test',
credential=credentials_factories.ProgramCredential()
),
credentials_factories.UserCredential(
id=2,
username='test',
credential=credentials_factories.ProgramCredential()
)
]
def _expected_credentials_data(self):
""" Dry method for getting expected credentials."""
program_credentials_data = self._expected_progam_credentials_data()
return [
{
"display_name": "Test Program A",
"credential_url": "http://credentials.edx.org/credentials/dummy-uuid-1/"
'display_name': self.PROGRAMS_API_RESPONSE['results'][0]['name'],
'subtitle': self.PROGRAMS_API_RESPONSE['results'][0]['subtitle'],
'credential_url':program_credentials_data[0]['certificate_url']
},
{
"display_name": "Test Program B",
"credential_url": "http://credentials.edx.org/credentials/dummy-uuid-2/"
'display_name': self.PROGRAMS_API_RESPONSE['results'][1]['name'],
'subtitle':self.PROGRAMS_API_RESPONSE['results'][1]['subtitle'],
'credential_url':program_credentials_data[1]['certificate_url']
}
]
......
......@@ -7,9 +7,10 @@ from django.views.decorators.http import require_GET
from django.http import Http404
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_display_category
from student.views import get_course_enrollments, _get_xseries_credentials
from student.views import get_course_enrollments
@login_required
......@@ -39,7 +40,7 @@ def view_programs(request):
'xseries_url': marketing_root if ProgramsApiConfig.current().show_xseries_ad else None,
'nav_hidden': True,
'show_program_listing': show_program_listing,
'credentials': _get_xseries_credentials(request.user),
'credentials': get_programs_credentials(request.user, category='xseries'),
'disable_courseware_js': True,
'uses_pattern_library': True
}
......
......@@ -19,9 +19,6 @@
@include float(right);
@include margin-left(flex-gutter());
width: flex-grid(3);
margin-top: ($baseline*2);
border-top: 3px solid $blue;
padding: $baseline 0;
.course-advertise {
@include clearfix();
......@@ -36,7 +33,7 @@
}
.ad-link {
@include text-align(center);
.btn-find-courses {
.btn-neutral {
padding-bottom: 12px;
padding-top: 12px;
}
......@@ -57,6 +54,9 @@
span {
@include margin-left($baseline*0.25);
}
.icon {
@include margin-right($baseline*0.25);
}
}
}
}
......@@ -94,8 +94,6 @@
@include margin-left(flex-gutter());
width: flex-grid(3);
margin-top: ($baseline*2);
border-top: 3px solid $blue;
padding: $baseline 0;
.user-info {
@include clearfix();
......
......@@ -175,19 +175,6 @@ from openedx.core.djangolib.markup import Text, HTML
</ul>
</section>
</section>
% if xseries_credentials:
<div class="wrapper-xseries-certificates">
<p class="title">${_("XSeries Program Certificates")}</p>
<p class="copy">${_("You have received a certificate for the following XSeries programs:")}</p>
<ul>
% for xseries_credential in xseries_credentials:
<li>
<a class="copy" href="${xseries_credential['credential_url']}">${xseries_credential['display_name']}</a>
</li>
% endfor
</ul>
</div>
% endif
</section>
</main>
......
"""Factories for generating fake credentials-related data."""
import factory
from factory.fuzzy import FuzzyText
class UserCredential(factory.Factory):
"""Factory for stubbing user credentials resources from the User Credentials
API (v1).
"""
class Meta(object):
model = dict
id = factory.Sequence(lambda n: n) # pylint: disable=invalid-name
username = FuzzyText(prefix='user_')
status = 'awarded'
uuid = FuzzyText(prefix='uuid_')
certificate_url = 'http=//credentials.edx.org/credentials/dummy-uuid'
credential = {}
class ProgramCredential(factory.Factory):
"""Factory for stubbing program credentials resources from the Program
Credentials API (v1).
"""
class Meta(object):
model = dict
credential_id = factory.Sequence(lambda n: n)
program_id = factory.Sequence(lambda n: n)
class CourseCredential(factory.Factory):
"""Factory for stubbing course credentials resources from the Course
Credentials API (v1).
"""
class Meta(object):
model = dict
course_id = 'edx/test01/2015'
credential_id = factory.Sequence(lambda n: n)
certificate_type = 'verified'
......@@ -4,6 +4,7 @@ import json
import httpretty
from openedx.core.djangoapps.credentials.models import CredentialsApiConfig
from openedx.core.djangoapps.credentials.tests import factories
class CredentialsApiConfigMixin(object):
......@@ -33,103 +34,63 @@ class CredentialsDataMixin(object):
CREDENTIALS_API_RESPONSE = {
"next": None,
"results": [
{
"id": 1,
"username": "test",
"credential": {
"credential_id": 1,
"program_id": 1
},
"status": "awarded",
"uuid": "dummy-uuid-1",
"certificate_url": "http://credentials.edx.org/credentials/dummy-uuid-1/"
},
{
"id": 2,
"username": "test",
"credential": {
"credential_id": 2,
"program_id": 2
},
"status": "awarded",
"uuid": "dummy-uuid-2",
"certificate_url": "http://credentials.edx.org/credentials/dummy-uuid-2/"
},
{
"id": 3,
"username": "test",
"credential": {
"credential_id": 3,
"program_id": 3
},
"status": "revoked",
"uuid": "dummy-uuid-3",
"certificate_url": "http://credentials.edx.org/credentials/dummy-uuid-3/"
},
{
"id": 4,
"username": "test",
"credential": {
"course_id": "edx/test01/2015",
"credential_id": 4,
"certificate_type": "honor"
},
"status": "awarded",
"uuid": "dummy-uuid-4",
"certificate_url": "http://credentials.edx.org/credentials/dummy-uuid-4/"
},
{
"id": 5,
"username": "test",
"credential": {
"course_id": "edx/test02/2015",
"credential_id": 5,
"certificate_type": "verified"
},
"status": "awarded",
"uuid": "dummy-uuid-5",
"certificate_url": "http://credentials.edx.org/credentials/dummy-uuid-5/"
},
{
"id": 6,
"username": "test",
"credential": {
"course_id": "edx/test03/2015",
"credential_id": 6,
"certificate_type": "honor"
},
"status": "revoked",
"uuid": "dummy-uuid-6",
"certificate_url": "http://credentials.edx.org/credentials/dummy-uuid-6/"
}
factories.UserCredential(
id=1,
username='test',
credential=factories.ProgramCredential(
program_id=1
)
),
factories.UserCredential(
id=2,
username='test',
credential=factories.ProgramCredential(
program_id=2
)
),
factories.UserCredential(
id=3,
status='revoked',
username='test',
credential=factories.ProgramCredential()
),
factories.UserCredential(
id=4,
username='test',
credential=factories.CourseCredential(
certificate_type='honor'
)
),
factories.UserCredential(
id=5,
username='test',
credential=factories.CourseCredential(
course_id='edx/test02/2015'
)
),
factories.UserCredential(
id=6,
username='test',
credential=factories.CourseCredential(
course_id='edx/test02/2015'
)
),
]
}
CREDENTIALS_NEXT_API_RESPONSE = {
"next": None,
"results": [
{
"id": 7,
"username": "test",
"credential": {
"credential_id": 7,
"program_id": 7
},
"status": "awarded",
"uuid": "dummy-uuid-7",
"certificate_url": "http://credentials.edx.org/credentials/dummy-uuid-7"
},
{
"id": 8,
"username": "test",
"credential": {
"credential_id": 8,
"program_id": 8
},
"status": "awarded",
"uuid": "dummy-uuid-8",
"certificate_url": "http://credentials.edx.org/credentials/dummy-uuid-8/"
}
factories.UserCredential(
id=7,
username='test',
credential=factories.ProgramCredential()
),
factories.UserCredential(
id=8,
username='test',
credential=factories.ProgramCredential()
)
]
}
......
......@@ -3,17 +3,19 @@ import unittest
from django.conf import settings
from django.core.cache import cache
from django.test import TestCase
from nose.plugins.attrib import attr
import httpretty
from edx_oauth2_provider.tests.factories import ClientFactory
from provider.constants import CONFIDENTIAL
from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin, CredentialsDataMixin
from openedx.core.djangoapps.credentials.models import CredentialsApiConfig
from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin, CredentialsDataMixin
from openedx.core.djangoapps.credentials.utils import (
get_user_credentials, get_user_program_credentials
get_user_credentials,
get_user_program_credentials,
get_programs_credentials
)
from openedx.core.djangoapps.credentials.tests import factories
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin, ProgramsDataMixin
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
......@@ -39,6 +41,39 @@ class TestCredentialsRetrieval(ProgramsApiConfigMixin, CredentialsApiConfigMixin
cache.clear()
def _expected_progam_credentials_data(self):
"""
Dry method for getting expected program credentials response data.
"""
return [
factories.UserCredential(
id=1,
username='test',
credential=factories.ProgramCredential()
),
factories.UserCredential(
id=2,
username='test',
credential=factories.ProgramCredential()
)
]
def expected_credentials_display_data(self):
""" Returns expected credentials data to be represented. """
program_credentials_data = self._expected_progam_credentials_data()
return [
{
'display_name': self.PROGRAMS_API_RESPONSE['results'][0]['name'],
'subtitle': self.PROGRAMS_API_RESPONSE['results'][0]['subtitle'],
'credential_url':program_credentials_data[0]['certificate_url']
},
{
'display_name': self.PROGRAMS_API_RESPONSE['results'][1]['name'],
'subtitle':self.PROGRAMS_API_RESPONSE['results'][1]['subtitle'],
'credential_url':program_credentials_data[1]['certificate_url']
}
]
@httpretty.activate
def test_get_user_credentials(self):
"""Verify user credentials data can be retrieve."""
......@@ -98,9 +133,10 @@ class TestCredentialsRetrieval(ProgramsApiConfigMixin, CredentialsApiConfigMixin
self.mock_credentials_api(self.user, reset_url=False)
actual = get_user_program_credentials(self.user)
program_credentials_data = self._expected_progam_credentials_data()
expected = self.PROGRAMS_API_RESPONSE['results'][:2]
expected[0]['credential_url'] = self.PROGRAMS_CREDENTIALS_DATA[0]['certificate_url']
expected[1]['credential_url'] = self.PROGRAMS_CREDENTIALS_DATA[1]['certificate_url']
expected[0]['credential_url'] = program_credentials_data[0]['certificate_url']
expected[1]['credential_url'] = program_credentials_data[1]['certificate_url']
# checking response from API is as expected
self.assertEqual(len(actual), 2)
......@@ -125,3 +161,57 @@ class TestCredentialsRetrieval(ProgramsApiConfigMixin, CredentialsApiConfigMixin
self.mock_credentials_api(self.user, data=credential_data)
actual = get_user_program_credentials(self.user)
self.assertEqual(actual, [])
@httpretty.activate
def test_get_programs_credentials(self):
""" Verify that the program credentials data required for display can
be retrieved.
"""
# create credentials and program configuration
self.create_credentials_config()
self.create_programs_config()
# Mocking the API responses from programs and credentials
self.mock_programs_api()
self.mock_credentials_api(self.user, reset_url=False)
actual = get_programs_credentials(self.user, category='xseries')
expected = self.expected_credentials_display_data()
# Checking result is as expected
self.assertEqual(len(actual), 2)
self.assertEqual(actual, expected)
@httpretty.activate
def test_get_programs_credentials_category(self):
""" Verify behaviour when program category is provided."""
# create credentials and program configuration
self.create_credentials_config()
self.create_programs_config()
# Mocking the API responses from programs and credentials
self.mock_programs_api()
self.mock_credentials_api(self.user, reset_url=False)
actual = get_programs_credentials(self.user, category='dummy_category')
expected = self.expected_credentials_display_data()
self.assertEqual(len(actual), 0)
actual = get_programs_credentials(self.user, category='xseries')
self.assertEqual(len(actual), 2)
self.assertEqual(actual, expected)
@httpretty.activate
def test_get_programs_credentials_no_category(self):
""" Verify behaviour when no program category is provided. """
self.create_credentials_config()
self.create_programs_config()
# Mocking the API responses from programs and credentials
self.mock_programs_api()
self.mock_credentials_api(self.user, reset_url=False)
actual = get_programs_credentials(self.user)
expected = self.expected_credentials_display_data()
self.assertEqual(len(actual), 2)
self.assertEqual(actual, expected)
......@@ -64,3 +64,35 @@ def get_user_program_credentials(user):
programs_credentials_data = get_programs_for_credentials(user, programs_credentials)
return programs_credentials_data
def get_programs_credentials(user, category=None):
"""Return program credentials data required for display.
Given a user, find all programs for which certificates have been earned
and return list of dictionaries of required program data.
Arguments:
user (User): user object for getting programs credentials.
category(str) : program category for getting credentials.
Returns:
list of dict, containing data corresponding to the programs for which
the user has been awarded a credential.
"""
programs_credentials = get_user_program_credentials(user)
credentials_data = []
for program in programs_credentials:
is_included = (category is None) or (program.get('category') == category)
if is_included:
try:
program_data = {
'display_name': program['name'],
'subtitle': program['subtitle'],
'credential_url': program['credential_url'],
}
credentials_data.append(program_data)
except KeyError:
log.warning('Program structure is invalid: %r', program)
return credentials_data
......@@ -100,31 +100,6 @@ class ProgramsDataMixin(object):
]
}
PROGRAMS_CREDENTIALS_DATA = [
{
"id": 1,
"username": "test",
"credential": {
"credential_id": 1,
"program_id": 1
},
"status": "awarded",
"uuid": "dummy-uuid-1",
"certificate_url": "http://credentials.edx.org/credentials/dummy-uuid-1/"
},
{
"id": 2,
"username": "test",
"credential": {
"credential_id": 2,
"program_id": 2
},
"status": "awarded",
"uuid": "dummy-uuid-2",
"certificate_url": "http://credentials.edx.org/credentials/dummy-uuid-2/"
}
]
def mock_programs_api(self, data=None, status_code=200):
"""Utility for mocking out Programs API URLs."""
self.assertTrue(httpretty.is_enabled(), msg='httpretty must be enabled to mock Programs API calls.')
......
......@@ -12,7 +12,8 @@ from edx_oauth2_provider.tests.factories import ClientFactory
from provider.constants import CONFIDENTIAL
from lms.djangoapps.certificates.api import MODES
from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin
from openedx.core.djangoapps.credentials.tests import factories as credentials_factories
from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin, CredentialsDataMixin
from openedx.core.djangoapps.programs import utils
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.programs.tests import factories
......@@ -26,7 +27,7 @@ UTILS_MODULE = 'openedx.core.djangoapps.programs.utils'
@skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@attr('shard_2')
class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin,
class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin, CredentialsDataMixin,
CredentialsApiConfigMixin, CacheIsolationTestCase):
"""Tests covering the retrieval of programs from the Programs service."""
......@@ -40,6 +41,27 @@ class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin,
cache.clear()
def _expected_progam_credentials_data(self):
"""
Dry method for getting expected program credentials response data.
"""
return [
credentials_factories.UserCredential(
id=1,
username='test',
credential=credentials_factories.ProgramCredential(
program_id=1
)
),
credentials_factories.UserCredential(
id=2,
username='test',
credential=credentials_factories.ProgramCredential(
program_id=2
)
)
]
@httpretty.activate
def test_get_programs(self):
"""Verify programs data can be retrieved."""
......@@ -152,11 +174,12 @@ class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin,
"""Verify programs data can be retrieved and parsed correctly for certificates."""
self.create_programs_config()
self.mock_programs_api()
program_credentials_data = self._expected_progam_credentials_data()
actual = utils.get_programs_for_credentials(self.user, self.PROGRAMS_CREDENTIALS_DATA)
actual = utils.get_programs_for_credentials(self.user, program_credentials_data)
expected = self.PROGRAMS_API_RESPONSE['results'][:2]
expected[0]['credential_url'] = self.PROGRAMS_CREDENTIALS_DATA[0]['certificate_url']
expected[1]['credential_url'] = self.PROGRAMS_CREDENTIALS_DATA[1]['certificate_url']
expected[0]['credential_url'] = program_credentials_data[0]['certificate_url']
expected[1]['credential_url'] = program_credentials_data[1]['certificate_url']
self.assertEqual(len(actual), 2)
self.assertEqual(actual, expected)
......@@ -167,8 +190,9 @@ class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin,
self.create_programs_config()
self.create_credentials_config()
self.mock_programs_api(data={'results': []})
program_credentials_data = self._expected_progam_credentials_data()
actual = utils.get_programs_for_credentials(self.user, self.PROGRAMS_CREDENTIALS_DATA)
actual = utils.get_programs_for_credentials(self.user, program_credentials_data)
self.assertEqual(actual, [])
@httpretty.activate
......
......@@ -163,7 +163,7 @@ from openedx.core.djangolib.markup import Text, HTML
${_("Browse recently launched courses and see what's new in your favorite subjects.")}
</div>
<div class="ad-link">
<a class="btn-find-courses" href="${marketing_link('COURSES')}">
<a class="btn-neutral" href="${marketing_link('COURSES')}">
<span class="icon fa fa-search" aria-hidden="true"></span>
${_("Explore New Courses")}
</a>
......@@ -193,19 +193,6 @@ from openedx.core.djangolib.markup import Text, HTML
</ul>
</section>
</section>
% if xseries_credentials:
<div class="wrapper-xseries-certificates">
<p class="title">${_("XSeries Program Certificates")}</p>
<p class="copy">${_("You have received a certificate for the following XSeries programs:")}</p>
<ul>
% for xseries_credential in xseries_credentials:
<li>
<a class="copy" href="${xseries_credential['credential_url']}">${xseries_credential['display_name']}</a>
</li>
% endfor
</ul>
</div>
% endif
</section>
<section id="email-settings-modal" class="modal" aria-hidden="true">
......
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