Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
10ad29f3
Commit
10ad29f3
authored
Jun 21, 2016
by
Renzo Lucioni
Committed by
GitHub
Jun 21, 2016
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #12757 from edx/renzo/link-to-detail
Link program listing cards to detail pages
parents
38a25446
9cd7c932
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
305 additions
and
231 deletions
+305
-231
common/djangoapps/student/tests/tests.py
+1
-1
lms/djangoapps/learner_dashboard/tests/test_programs.py
+262
-206
lms/djangoapps/learner_dashboard/views.py
+10
-13
lms/static/js/learner_dashboard/models/program_model.js
+1
-1
lms/static/js/spec/learner_dashboard/program_card_view_spec.js
+2
-2
lms/templates/learner_dashboard/empty_programs_list.underscore
+0
-1
lms/templates/learner_dashboard/program_card.underscore
+3
-4
openedx/core/djangoapps/credentials/tests/factories.py
+1
-1
openedx/core/djangoapps/credentials/tests/test_utils.py
+4
-2
openedx/core/djangoapps/programs/utils.py
+21
-0
No files found.
common/djangoapps/student/tests/tests.py
View file @
10ad29f3
...
...
@@ -891,7 +891,7 @@ 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
the ProgramsDataMixin
.
# TODO: Clean up these tests so that they use
program factories
.
@attr
(
'shard_3'
)
@unittest.skipUnless
(
settings
.
ROOT_URLCONF
==
'lms.urls'
,
'Test only valid in lms'
)
@ddt.ddt
...
...
lms/djangoapps/learner_dashboard/tests/test_programs.py
View file @
10ad29f3
# -*- coding: utf-8 -*-
"""
Unit tests covering the program listing and detail pages.
"""
import
datetime
import
json
import
re
import
unittest
from
urlparse
import
urljoin
from
bs4
import
BeautifulSoup
from
django.conf
import
settings
from
django.core.urlresolvers
import
reverse
from
django.test
import
override_settings
,
TestCase
from
django.test
import
override_settings
from
django.utils.text
import
slugify
from
edx_oauth2_provider.tests.factories
import
ClientFactory
import
httpretty
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
Credentials
DataMixin
,
Credentials
ApiConfigMixin
from
openedx.core.djangoapps.credentials.tests.mixins
import
CredentialsApiConfigMixin
from
openedx.core.djangoapps.programs.models
import
ProgramsApiConfig
from
openedx.core.djangoapps.programs.tests
import
factories
from
openedx.core.djangoapps.programs.tests.mixins
import
(
ProgramsApiConfigMixin
,
ProgramsDataMixin
)
from
student.models
import
CourseEnrollment
from
student.tests.factories
import
UserFactory
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
,
SharedModuleStoreTestCase
from
openedx.core.djangoapps.programs.tests
import
factories
as
programs_factories
from
openedx.core.djangoapps.programs.tests.mixins
import
ProgramsApiConfigMixin
from
openedx.core.djangoapps.programs.utils
import
get_display_category
from
student.tests.factories
import
UserFactory
,
CourseEnrollmentFactory
from
xmodule.modulestore.tests.django_utils
import
SharedModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
@httpretty.activate
@override_settings
(
MKTG_URLS
=
{
'ROOT'
:
'https://www.example.com'
})
@unittest.skipUnless
(
settings
.
ROOT_URLCONF
==
'lms.urls'
,
'Test only valid in lms'
)
@override_settings
(
MKTG_URLS
=
{
'ROOT'
:
'http://edx.org'
})
class
TestProgramListing
(
ModuleStoreTestCase
,
ProgramsApiConfigMixin
,
ProgramsDataMixin
,
CredentialsDataMixin
,
CredentialsApiConfigMixin
):
"""
Unit tests for getting the list of programs enrolled by a logged in user
"""
PASSWORD
=
'test'
class
TestProgramListing
(
ProgramsApiConfigMixin
,
CredentialsApiConfigMixin
,
SharedModuleStoreTestCase
):
"""Unit tests for the program listing page."""
maxDiff
=
None
password
=
'test'
url
=
reverse
(
'program_listing_view'
)
@classmethod
def
setUpClass
(
cls
):
super
(
TestProgramListing
,
cls
)
.
setUpClass
()
for
name
in
[
ProgramsApiConfig
.
OAUTH2_CLIENT_NAME
,
CredentialsApiConfig
.
OAUTH2_CLIENT_NAME
]:
ClientFactory
(
name
=
name
,
client_type
=
CONFIDENTIAL
)
cls
.
course
=
CourseFactory
()
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
.
first_program
=
programs_factories
.
Program
(
organizations
=
[
organization
],
course_codes
=
[
course_code
]
)
cls
.
second_program
=
programs_factories
.
Program
(
organizations
=
[
organization
],
course_codes
=
[
course_code
]
)
cls
.
data
=
sorted
([
cls
.
first_program
,
cls
.
second_program
],
key
=
cls
.
program_sort_key
)
cls
.
marketing_root
=
urljoin
(
settings
.
MKTG_URLS
.
get
(
'ROOT'
),
'xseries'
)
.
rstrip
(
'/'
)
def
setUp
(
self
):
super
(
TestProgramListing
,
self
)
.
setUp
()
self
.
user
=
UserFactory
()
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
password
)
@classmethod
def
program_sort_key
(
cls
,
program
):
"""
Add a student
Helper function used to sort dictionaries representing programs.
"""
super
(
TestProgramListing
,
self
)
.
setUp
()
ClientFactory
(
name
=
CredentialsApiConfig
.
OAUTH2_CLIENT_NAME
,
client_type
=
CONFIDENTIAL
)
ClientFactory
(
name
=
ProgramsApiConfig
.
OAUTH2_CLIENT_NAME
,
client_type
=
CONFIDENTIAL
)
self
.
student
=
UserFactory
()
return
program
[
'id'
]
def
_create_course_and_enroll
(
self
,
student
,
org
,
course
,
run
):
def
credential_sort_key
(
self
,
credential
):
"""
Creates a course and associated enrollment
.
Helper function used to sort dictionaries representing credentials
.
"""
course_location
=
locator
.
CourseLocator
(
org
,
course
,
run
)
course
=
CourseFactory
.
create
(
org
=
course_location
.
org
,
number
=
course_location
.
course
,
run
=
course_location
.
run
try
:
return
credential
[
'certificate_url'
]
except
KeyError
:
return
credential
[
'credential_url'
]
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
mock_credentials_api
(
self
,
data
):
"""Helper for mocking out Credentials API URLs."""
self
.
assertTrue
(
httpretty
.
is_enabled
(),
msg
=
'httpretty must be enabled to mock Credentials API calls.'
)
url
=
'{base}/user_credentials/?username={username}'
.
format
(
base
=
CredentialsApiConfig
.
current
()
.
internal_api_url
.
strip
(
'/'
),
username
=
self
.
user
.
username
)
enrollment
=
CourseEnrollment
.
enroll
(
student
,
course
.
id
)
enrollment
.
created
=
datetime
.
datetime
(
2000
,
12
,
31
,
0
,
0
,
0
,
0
)
enrollment
.
save
(
)
body
=
json
.
dumps
({
'results'
:
data
}
)
httpretty
.
register_uri
(
httpretty
.
GET
,
url
,
body
=
body
,
content_type
=
'application/json'
)
def
_get_program_url
(
self
,
marketing_slug
):
def
load_serialized_data
(
self
,
response
,
key
):
"""
Helper function to get the program card url
Extract and deserialize serialized data from the response.
"""
return
urljoin
(
settings
.
MKTG_URLS
.
get
(
'ROOT'
),
'xseries'
+
'/{}'
)
.
format
(
marketing_slug
)
pattern
=
re
.
compile
(
r'{key}: (?P<data>\[.*\])'
.
format
(
key
=
key
))
match
=
pattern
.
search
(
response
.
content
)
serialized
=
match
.
group
(
'data'
)
def
_setup_and_get_program
(
self
):
return
json
.
loads
(
serialized
)
def
assert_dict_contains_subset
(
self
,
superset
,
subset
):
"""
The core function to setup the mock program api,
then call the django test client to get the actual program listing page
make sure the request suceeds and make sure x_series_url is on the page
Verify that the dict superset contains the dict subset.
Works like assertDictContainsSubset, deprecated since Python 3.2.
See: https://docs.python.org/2.7/library/unittest.html#unittest.TestCase.assertDictContainsSubset.
"""
self
.
mock_programs_api
()
self
.
client
.
login
(
username
=
self
.
student
.
username
,
password
=
self
.
PASSWORD
)
response
=
self
.
client
.
get
(
self
.
url
)
x_series_url
=
urljoin
(
settings
.
MKTG_URLS
.
get
(
'ROOT'
),
'xseries'
)
self
.
assertContains
(
response
,
x_series_url
)
return
response
superset_keys
=
set
(
superset
.
keys
())
subset_keys
=
set
(
subset
.
keys
())
intersection
=
{
key
:
superset
[
key
]
for
key
in
superset_keys
&
subset_keys
}
self
.
assertEqual
(
subset
,
intersection
)
def
_get_program_checklist
(
self
,
program_id
):
def
test_login_required
(
self
):
"""
The convenience function to get all the program related page element we would like to check against
Verify that login is required to access the page.
"""
return
[
self
.
PROGRAM_NAMES
[
program_id
],
self
.
_get_program_url
(
self
.
PROGRAMS_API_RESPONSE
[
'results'
][
program_id
][
'marketing_slug'
]),
self
.
PROGRAMS_API_RESPONSE
[
'results'
][
program_id
][
'organizations'
][
0
][
'display_name'
],
]
def
_assert_progress_data_present
(
self
,
response
):
"""Verify that progress data is present."""
self
.
assertContains
(
response
,
'userProgress'
)
@httpretty.activate
def
test_get_program_with_no_enrollment
(
self
):
self
.
create_programs_config
()
response
=
self
.
_setup_and_get_program
()
for
program_element
in
self
.
_get_program_checklist
(
0
):
self
.
assertNotContains
(
response
,
program_element
)
for
program_element
in
self
.
_get_program_checklist
(
1
):
self
.
assertNotContains
(
response
,
program_element
)
@httpretty.activate
def
test_get_one_program
(
self
):
self
.
create_programs_config
()
self
.
_create_course_and_enroll
(
self
.
student
,
*
self
.
COURSE_KEYS
[
0
]
.
split
(
'/'
))
response
=
self
.
_setup_and_get_program
()
for
program_element
in
self
.
_get_program_checklist
(
0
):
self
.
assertContains
(
response
,
program_element
)
for
program_element
in
self
.
_get_program_checklist
(
1
):
self
.
assertNotContains
(
response
,
program_element
)
self
.
mock_programs_api
(
self
.
data
)
self
.
_assert_progress_data_present
(
response
)
self
.
client
.
logout
(
)
@httpretty.activate
def
test_get_both_program
(
self
):
self
.
create_programs_config
()
self
.
_create_course_and_enroll
(
self
.
student
,
*
self
.
COURSE_KEYS
[
0
]
.
split
(
'/'
))
self
.
_create_course_and_enroll
(
self
.
student
,
*
self
.
COURSE_KEYS
[
5
]
.
split
(
'/'
))
response
=
self
.
_setup_and_get_program
()
for
program_element
in
self
.
_get_program_checklist
(
0
):
self
.
assertContains
(
response
,
program_element
)
for
program_element
in
self
.
_get_program_checklist
(
1
):
self
.
assertContains
(
response
,
program_element
)
response
=
self
.
client
.
get
(
self
.
url
)
self
.
assertRedirects
(
response
,
'{}?next={}'
.
format
(
reverse
(
'signin_user'
),
self
.
url
)
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
password
)
self
.
_assert_progress_data_present
(
response
)
response
=
self
.
client
.
get
(
self
.
url
)
self
.
assertEqual
(
response
.
status_code
,
200
)
def
test_get_programs_dashboard_not_enabled
(
self
):
def
test_404_if_disabled
(
self
):
"""
Verify that the page 404s if disabled.
"""
self
.
create_programs_config
(
program_listing_enabled
=
False
)
self
.
client
.
login
(
username
=
self
.
student
.
username
,
password
=
self
.
PASSWORD
)
response
=
self
.
client
.
get
(
self
.
url
)
self
.
assertEqual
(
response
.
status_code
,
404
)
def
test_xseries_advertise_disabled
(
self
):
self
.
create_programs_config
(
xseries_ad_enabled
=
False
)
self
.
client
.
login
(
username
=
self
.
student
.
username
,
password
=
self
.
PASSWORD
)
def
test_empty_state
(
self
):
"""
Verify that the response contains no programs data when no programs are engaged.
"""
self
.
create_programs_config
()
self
.
mock_programs_api
(
self
.
data
)
response
=
self
.
client
.
get
(
self
.
url
)
x_series_url
=
urljoin
(
settings
.
MKTG_URLS
.
get
(
'ROOT'
),
'xseries'
)
self
.
assertNotContains
(
response
,
x_series_url
)
self
.
assertContains
(
response
,
'programsData: []'
)
def
test_get_programs_not_logged_in
(
self
):
def
test_programs_listed
(
self
):
"""
Verify that the response contains accurate programs data when programs are engaged.
"""
self
.
create_programs_config
()
self
.
mock_programs_api
(
self
.
data
)
CourseEnrollmentFactory
(
user
=
self
.
user
,
course_id
=
self
.
course
.
id
)
# pylint: disable=no-member
response
=
self
.
client
.
get
(
self
.
url
)
actual
=
self
.
load_serialized_data
(
response
,
'programsData'
)
actual
=
sorted
(
actual
,
key
=
self
.
program_sort_key
)
self
.
assertRedirects
(
response
,
'{}?next={}'
.
format
(
reverse
(
'signin_user'
),
self
.
url
)
)
for
index
,
actual_program
in
enumerate
(
actual
):
expected_program
=
self
.
data
[
index
]
def
_expected_progam_credentials_data
(
self
):
self
.
assert_dict_contains_subset
(
actual_program
,
expected_program
)
self
.
assertEqual
(
actual_program
[
'display_category'
],
get_display_category
(
expected_program
)
)
def
test_toggle_xseries_advertising
(
self
):
"""
Dry method for getting expected program credentials response data.
Verify that when XSeries advertising is disabled, no link to the marketing site
appears in the response (and vice versa).
"""
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'
:
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_xseries_certificates_with_data
(
self
):
# Verify the URL is present when advertising is enabled.
self
.
create_programs_config
()
self
.
mock_programs_api
(
self
.
data
)
response
=
self
.
client
.
get
(
self
.
url
)
self
.
assertContains
(
response
,
self
.
marketing_root
)
# Verify the URL is missing when advertising is disabled.
self
.
create_programs_config
(
xseries_ad_enabled
=
False
)
response
=
self
.
client
.
get
(
self
.
url
)
self
.
assertNotContains
(
response
,
self
.
marketing_root
)
def
test_links_to_detail_pages
(
self
):
"""
Verify that links to detail pages are present when enabled, instead of
links to the marketing site.
"""
self
.
create_programs_config
()
self
.
create_credentials_config
(
is_learner_issuance_enabled
=
True
)
self
.
mock_programs_api
(
self
.
data
)
CourseEnrollmentFactory
(
user
=
self
.
user
,
course_id
=
self
.
course
.
id
)
# pylint: disable=no-member
response
=
self
.
client
.
get
(
self
.
url
)
actual
=
self
.
load_serialized_data
(
response
,
'programsData'
)
actual
=
sorted
(
actual
,
key
=
self
.
program_sort_key
)
for
index
,
actual_program
in
enumerate
(
actual
):
expected_program
=
self
.
data
[
index
]
self
.
client
.
login
(
username
=
self
.
student
.
username
,
password
=
self
.
PASSWORD
)
base
=
reverse
(
'program_details_view'
,
args
=
[
expected_program
[
'id'
]])
.
rstrip
(
'/'
)
slug
=
slugify
(
expected_program
[
'name'
])
self
.
assertEqual
(
actual_program
[
'detail_url'
],
'{}/{}'
.
format
(
base
,
slug
)
)
# mock programs and credentials apis
self
.
mock_programs_api
()
self
.
mock_credentials_api
(
self
.
student
,
data
=
self
.
CREDENTIALS_API_RESPONSE
,
reset_url
=
False
)
# Verify that links to the marketing site are present when detail pages are disabled.
self
.
create_programs_config
(
program_details_enabled
=
False
)
response
=
self
.
client
.
get
(
reverse
(
"program_listing_view"
))
for
certificate
in
self
.
_expected_credentials_data
():
self
.
assertContains
(
response
,
certificate
[
'display_name'
])
self
.
assertContains
(
response
,
certificate
[
'credential_url'
])
response
=
self
.
client
.
get
(
self
.
url
)
actual
=
self
.
load_serialized_data
(
response
,
'programsData'
)
actual
=
sorted
(
actual
,
key
=
self
.
program_sort_key
)
self
.
assertContains
(
response
,
'images/xseries-certificate-visual.png'
)
for
index
,
actual_program
in
enumerate
(
actual
):
expected_program
=
self
.
data
[
index
]
@httpretty.activate
def
test_get_xseries_certificates_without_data
(
self
):
self
.
assertEqual
(
actual_program
[
'detail_url'
],
'{}/{}'
.
format
(
self
.
marketing_root
,
expected_program
[
'marketing_slug'
])
)
def
test_certificates_listed
(
self
):
"""
Verify that the response contains accurate certificate data when certificates are available.
"""
self
.
create_programs_config
()
self
.
create_credentials_config
(
is_learner_issuance_enabled
=
True
)
self
.
client
.
login
(
username
=
self
.
student
.
username
,
password
=
self
.
PASSWORD
)
self
.
mock_programs_api
(
self
.
data
)
first_credential
=
credentials_factories
.
UserCredential
(
username
=
self
.
user
.
username
,
credential
=
credentials_factories
.
ProgramCredential
(
program_id
=
self
.
first_program
[
'id'
]
)
)
second_credential
=
credentials_factories
.
UserCredential
(
username
=
self
.
user
.
username
,
credential
=
credentials_factories
.
ProgramCredential
(
program_id
=
self
.
second_program
[
'id'
]
)
)
credentials_data
=
sorted
([
first_credential
,
second_credential
],
key
=
self
.
credential_sort_key
)
# mock programs and credentials apis
self
.
mock_programs_api
()
self
.
mock_credentials_api
(
self
.
student
,
data
=
{
"results"
:
[]},
reset_url
=
False
)
self
.
mock_credentials_api
(
credentials_data
)
response
=
self
.
client
.
get
(
reverse
(
"program_listing_view"
))
for
certificate
in
self
.
_expected_credentials_data
():
self
.
assertNotContains
(
response
,
certificate
[
'display_name'
])
self
.
assertNotContains
(
response
,
certificate
[
'credential_url'
])
response
=
self
.
client
.
get
(
self
.
url
)
actual
=
self
.
load_serialized_data
(
response
,
'certificatesData'
)
actual
=
sorted
(
actual
,
key
=
self
.
credential_sort_key
)
for
index
,
actual_credential
in
enumerate
(
actual
):
expected_credential
=
credentials_data
[
index
]
self
.
assertEqual
(
# TODO: certificate_url is needlessly transformed to credential_url. (╯°□°)╯︵ ┻━┻
# Clean this up!
actual_credential
[
'credential_url'
],
expected_credential
[
'certificate_url'
]
)
@httpretty.activate
@override_settings
(
MKTG_URLS
=
{
'ROOT'
:
'http
://edx.org
'
})
@override_settings
(
MKTG_URLS
=
{
'ROOT'
:
'http
s://www.example.com
'
})
@unittest.skipUnless
(
settings
.
ROOT_URLCONF
==
'lms.urls'
,
'Test only valid in lms'
)
class
TestProgramDetails
(
ProgramsApiConfigMixin
,
SharedModuleStoreTestCase
):
"""
Unit tests for the program details page
"""
"""Unit tests for the program details page."""
program_id
=
123
password
=
'test'
url
=
reverse
(
'program_details_view'
,
args
=
[
program_id
])
@classmethod
def
setUpClass
(
cls
):
super
(
TestProgramDetails
,
cls
)
.
setUpClass
()
cls
.
course
=
CourseFactory
()
ClientFactory
(
name
=
ProgramsApiConfig
.
OAUTH2_CLIENT_NAME
,
client_type
=
CONFIDENTIAL
)
course
=
CourseFactory
()
organization
=
programs_factories
.
Organization
()
run_mode
=
programs_factories
.
RunMode
(
course_key
=
unicode
(
course
.
id
))
# pylint: disable=no-member
course_code
=
programs_factories
.
CourseCode
(
run_modes
=
[
run_mode
])
cls
.
data
=
programs_factories
.
Program
(
organizations
=
[
organization
],
course_codes
=
[
course_code
]
)
def
setUp
(
self
):
super
(
TestProgramDetails
,
self
)
.
setUp
()
self
.
details_page
=
reverse
(
'program_details_view'
,
args
=
[
self
.
program_id
])
self
.
user
=
UserFactory
()
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
password
)
ClientFactory
(
name
=
ProgramsApiConfig
.
OAUTH2_CLIENT_NAME
,
client_type
=
CONFIDENTIAL
)
self
.
organization
=
factories
.
Organization
()
self
.
run_mode
=
factories
.
RunMode
(
course_key
=
unicode
(
self
.
course
.
id
))
# pylint: disable=no-member
self
.
course_code
=
factories
.
CourseCode
(
run_modes
=
[
self
.
run_mode
])
self
.
data
=
factories
.
Program
(
organizations
=
[
self
.
organization
],
course_codes
=
[
self
.
course_code
]
)
def
_mock_programs_api
(
self
,
data
,
status
=
200
):
def
mock_programs_api
(
self
,
data
,
status
=
200
):
"""Helper for mocking out Programs API URLs."""
self
.
assertTrue
(
httpretty
.
is_enabled
(),
msg
=
'httpretty must be enabled to mock Programs API calls.'
)
...
...
@@ -281,15 +337,15 @@ class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase):
content_type
=
'application/json'
,
)
def
_
assert_program_data_present
(
self
,
response
):
def
assert_program_data_present
(
self
,
response
):
"""Verify that program data is present."""
self
.
assertContains
(
response
,
'programData'
)
self
.
assertContains
(
response
,
'urls'
)
self
.
assertContains
(
response
,
'program_listing_url'
)
self
.
assertContains
(
response
,
self
.
data
[
'name'
])
self
.
_
assert_programs_tab_present
(
response
)
self
.
assert_programs_tab_present
(
response
)
def
_
assert_programs_tab_present
(
self
,
response
):
def
assert_programs_tab_present
(
self
,
response
):
"""Verify that the programs tab is present in the nav."""
soup
=
BeautifulSoup
(
response
.
content
,
'html.parser'
)
self
.
assertTrue
(
...
...
@@ -301,20 +357,20 @@ class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase):
Verify that login is required to access the page.
"""
self
.
create_programs_config
()
self
.
_
mock_programs_api
(
self
.
data
)
self
.
mock_programs_api
(
self
.
data
)
self
.
client
.
logout
()
response
=
self
.
client
.
get
(
self
.
details_page
)
response
=
self
.
client
.
get
(
self
.
url
)
self
.
assertRedirects
(
response
,
'{}?next={}'
.
format
(
reverse
(
'signin_user'
),
self
.
details_page
)
'{}?next={}'
.
format
(
reverse
(
'signin_user'
),
self
.
url
)
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
password
)
response
=
self
.
client
.
get
(
self
.
details_page
)
self
.
_
assert_program_data_present
(
response
)
response
=
self
.
client
.
get
(
self
.
url
)
self
.
assert_program_data_present
(
response
)
def
test_404_if_disabled
(
self
):
"""
...
...
@@ -322,33 +378,33 @@ class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase):
"""
self
.
create_programs_config
(
program_details_enabled
=
False
)
response
=
self
.
client
.
get
(
self
.
details_page
)
self
.
assertEqual
s
(
response
.
status_code
,
404
)
response
=
self
.
client
.
get
(
self
.
url
)
self
.
assertEqual
(
response
.
status_code
,
404
)
def
test_
page_routing
(
self
):
"""Verify that the page
can be hit with or without a program name in the URL
."""
def
test_
404_if_no_data
(
self
):
"""Verify that the page
404s if no program data is found
."""
self
.
create_programs_config
()
self
.
_mock_programs_api
(
self
.
data
)
response
=
self
.
client
.
get
(
self
.
details_page
)
self
.
_assert_program_data_present
(
response
)
self
.
mock_programs_api
(
self
.
data
,
status
=
404
)
response
=
self
.
client
.
get
(
self
.
url
)
self
.
assertEqual
(
response
.
status_code
,
404
)
response
=
self
.
client
.
get
(
self
.
details_page
+
'program_name/'
)
self
.
_assert_program_data_present
(
response
)
httpretty
.
reset
()
response
=
self
.
client
.
get
(
self
.
details_page
+
'program_name/invalid/'
)
self
.
assertEquals
(
response
.
status_code
,
404
)
self
.
mock_programs_api
({})
response
=
self
.
client
.
get
(
self
.
url
)
self
.
assertEqual
(
response
.
status_code
,
404
)
def
test_
404_if_no_data
(
self
):
"""Verify that the page
404s if no program data is found
."""
def
test_
page_routing
(
self
):
"""Verify that the page
can be hit with or without a program name in the URL
."""
self
.
create_programs_config
()
self
.
mock_programs_api
(
self
.
data
)
self
.
_mock_programs_api
(
self
.
data
,
status
=
404
)
response
=
self
.
client
.
get
(
self
.
details_page
)
self
.
assertEquals
(
response
.
status_code
,
404
)
response
=
self
.
client
.
get
(
self
.
url
)
self
.
assert_program_data_present
(
response
)
httpretty
.
reset
()
response
=
self
.
client
.
get
(
self
.
url
+
'program_name/'
)
self
.
assert_program_data_present
(
response
)
self
.
_mock_programs_api
({})
response
=
self
.
client
.
get
(
self
.
details_page
)
self
.
assertEquals
(
response
.
status_code
,
404
)
response
=
self
.
client
.
get
(
self
.
url
+
'program_name/invalid/'
)
self
.
assertEqual
(
response
.
status_code
,
404
)
lms/djangoapps/learner_dashboard/views.py
View file @
10ad29f3
...
...
@@ -21,28 +21,26 @@ from lms.djangoapps.learner_dashboard.utils import (
@require_GET
def
view_programs
(
request
):
"""View programs in which the user is engaged."""
show_program_listing
=
ProgramsApiConfig
.
current
()
.
show_program_listing
if
not
show_program_listing
:
programs_config
=
ProgramsApiConfig
.
current
()
if
not
programs_config
.
show_program_listing
:
raise
Http404
meter
=
utils
.
ProgramProgressMeter
(
request
.
user
)
programs
=
meter
.
engaged_programs
# TODO: Pull 'xseries' string from configuration model.
marketing_root
=
urljoin
(
settings
.
MKTG_URLS
.
get
(
'ROOT'
),
'xseries'
)
.
strip
(
'/'
)
marketing_root
=
urljoin
(
settings
.
MKTG_URLS
.
get
(
'ROOT'
),
'xseries'
)
.
rstrip
(
'/'
)
for
program
in
programs
:
program
[
'detail_url'
]
=
utils
.
get_program_detail_url
(
program
,
marketing_root
)
program
[
'display_category'
]
=
utils
.
get_display_category
(
program
)
program
[
'marketing_url'
]
=
'{root}/{slug}'
.
format
(
root
=
marketing_root
,
slug
=
program
[
'marketing_slug'
]
)
context
=
{
'programs'
:
programs
,
'progress'
:
meter
.
progress
,
'xseries_url'
:
marketing_root
if
ProgramsApiConfig
.
current
()
.
show_xseries_ad
else
None
,
'xseries_url'
:
marketing_root
if
programs_config
.
show_xseries_ad
else
None
,
'nav_hidden'
:
True
,
'show_program_listing'
:
show_program_listing
,
'show_program_listing'
:
programs_config
.
show_program_listing
,
'credentials'
:
get_programs_credentials
(
request
.
user
,
category
=
'xseries'
),
'disable_courseware_js'
:
True
,
'uses_pattern_library'
:
True
...
...
@@ -55,8 +53,8 @@ def view_programs(request):
@require_GET
def
program_details
(
request
,
program_id
):
"""View details about a specific program."""
show_program_details
=
ProgramsApiConfig
.
current
()
.
show_program_details
if
not
show_program_details
:
programs_config
=
ProgramsApiConfig
.
current
()
if
not
programs_config
.
show_program_details
:
raise
Http404
program_data
=
utils
.
get_programs
(
request
.
user
,
program_id
=
program_id
)
...
...
@@ -65,7 +63,6 @@ def program_details(request, program_id):
raise
Http404
program_data
=
utils
.
supplement_program_data
(
program_data
,
request
.
user
)
show_program_listing
=
ProgramsApiConfig
.
current
()
.
show_program_listing
urls
=
{
'program_listing_url'
:
reverse
(
'program_listing_view'
),
...
...
@@ -77,7 +74,7 @@ def program_details(request, program_id):
context
=
{
'program_data'
:
program_data
,
'urls'
:
urls
,
'show_program_listing'
:
show_program_listing
,
'show_program_listing'
:
programs_config
.
show_program_listing
,
'nav_hidden'
:
True
,
'disable_courseware_js'
:
True
,
'uses_pattern_library'
:
True
...
...
lms/static/js/learner_dashboard/models/program_model.js
View file @
10ad29f3
...
...
@@ -15,7 +15,7 @@
type
:
data
.
display_category
+
' Program'
,
subtitle
:
data
.
subtitle
,
organizations
:
data
.
organizations
,
marketingUrl
:
data
.
marketing
_url
,
detailUrl
:
data
.
detail
_url
,
smallBannerUrl
:
data
.
banner_image_urls
.
w348h116
,
mediumBannerUrl
:
data
.
banner_image_urls
.
w435h145
,
largeBannerUrl
:
data
.
banner_image_urls
.
w726h242
,
...
...
lms/static/js/spec/learner_dashboard/program_card_view_spec.js
View file @
10ad29f3
...
...
@@ -28,7 +28,7 @@ define([
modified
:
'2016-03-25T13:45:21.220732Z'
,
marketing_slug
:
'p_2?param=haha&test=b'
,
id
:
146
,
marketing_url
:
'http://www.edx.org/xseries/p_2?param=haha&test=b
'
,
detail_url
:
'http://courses.edx.org/dashboard/programs/1/foo
'
,
banner_image_urls
:
{
w348h116
:
'http://www.edx.org/images/test1'
,
w435h145
:
'http://www.edx.org/images/test2'
,
...
...
@@ -55,7 +55,7 @@ define([
expect
(
$card
.
find
(
'.title'
).
html
().
trim
()).
toEqual
(
program
.
name
);
expect
(
$card
.
find
(
'.category span'
).
html
().
trim
()).
toEqual
(
'XSeries Program'
);
expect
(
$card
.
find
(
'.organization'
).
html
().
trim
()).
toEqual
(
program
.
organizations
[
0
].
key
);
expect
(
$card
.
find
(
'.card-link'
).
attr
(
'href'
)).
toEqual
(
program
.
marketing
_url
);
expect
(
$card
.
find
(
'.card-link'
).
attr
(
'href'
)).
toEqual
(
program
.
detail
_url
);
};
beforeEach
(
function
()
{
...
...
lms/templates/learner_dashboard/empty_programs_list.underscore
View file @
10ad29f3
...
...
@@ -5,4 +5,3 @@
<span><%- gettext('Explore XSeries Programs') %></span>
</a>
</section>
lms/templates/learner_dashboard/program_card.underscore
View file @
10ad29f3
<div class="text-section">
<h3 id="program-<%- id %>" class="title hd-3"><%- gettext(name) %></h3>
<div class="meta-info grid-container">
...
...
@@ -10,7 +9,7 @@
</div>
<% if (progress) { %>
<p class="certificate-status">
<a href="<%-
marketing
Url %>" class="status-text secondary" aria-describedby="program-<%- id %>"><%= interpolate(
<a href="<%-
detail
Url %>" class="status-text secondary" aria-describedby="program-<%- id %>"><%= interpolate(
ngettext(
'%(count)s course is in progress.',
'%(count)s courses are in progress.',
...
...
@@ -19,7 +18,7 @@
{count: progress.total.in_progress}, true
) %></a>
<a href="<%-
marketing
Url %>" class="status-text secondary" aria-describedby="program-<%- id %>"><%= interpolate(
<a href="<%-
detail
Url %>" class="status-text secondary" aria-describedby="program-<%- id %>"><%= interpolate(
ngettext(
'%(count)s course has not been started.',
'%(count)s courses have not been started.',
...
...
@@ -42,7 +41,7 @@
<div class="bar not-started"></div>
</div>
<% } %>
<a href="<%-
marketing
Url %>" class="card-link">
<a href="<%-
detail
Url %>" class="card-link">
<div class="banner-image-container">
<picture>
<source srcset="<%- smallBannerUrl %>" media="(max-width: <%- breakpoints.max.tiny %>)">
...
...
openedx/core/djangoapps/credentials/tests/factories.py
View file @
10ad29f3
...
...
@@ -14,7 +14,7 @@ class UserCredential(factory.Factory):
username
=
FuzzyText
(
prefix
=
'user_'
)
status
=
'awarded'
uuid
=
FuzzyText
(
prefix
=
'uuid_'
)
certificate_url
=
'http=//credentials.edx.org/credentials/dummy-uuid'
certificate_url
=
FuzzyText
(
prefix
=
'https://www.example.com/credentials/'
)
credential
=
{}
...
...
openedx/core/djangoapps/credentials/tests/test_utils.py
View file @
10ad29f3
...
...
@@ -49,12 +49,14 @@ class TestCredentialsRetrieval(ProgramsApiConfigMixin, CredentialsApiConfigMixin
factories
.
UserCredential
(
id
=
1
,
username
=
'test'
,
credential
=
factories
.
ProgramCredential
()
credential
=
factories
.
ProgramCredential
(),
certificate_url
=
self
.
CREDENTIALS_API_RESPONSE
[
'results'
][
0
][
'certificate_url'
],
),
factories
.
UserCredential
(
id
=
2
,
username
=
'test'
,
credential
=
factories
.
ProgramCredential
()
credential
=
factories
.
ProgramCredential
(),
certificate_url
=
self
.
CREDENTIALS_API_RESPONSE
[
'results'
][
1
][
'certificate_url'
],
)
]
...
...
openedx/core/djangoapps/programs/utils.py
View file @
10ad29f3
...
...
@@ -6,6 +6,7 @@ import logging
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
...
...
@@ -128,6 +129,26 @@ 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.
Arguments:
program (dict): Representation of a program.
marketing_root (str): Root URL used to build links to XSeries marketing pages.
Returns:
str, a link to program details
"""
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'
]
return
'{base}/{slug}'
.
format
(
base
=
base
,
slug
=
slug
)
def
get_display_category
(
program
):
""" Given the program, return the category of the program for display
Arguments:
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment