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
1210d3fc
Commit
1210d3fc
authored
Jul 02, 2016
by
Renzo Lucioni
Committed by
GitHub
Jul 02, 2016
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #12908 from edx/renzo/course-card-upgrade-cta
Add upgrade section to program detail course cards.
parents
69410948
d7381885
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
287 additions
and
90 deletions
+287
-90
lms/djangoapps/learner_dashboard/views.py
+3
-2
lms/static/js/learner_dashboard/models/course_card_model.js
+5
-4
lms/static/js/learner_dashboard/views/certificate_status_view.js
+2
-5
lms/static/js/learner_dashboard/views/course_card_view.js
+16
-3
lms/static/js/learner_dashboard/views/upgrade_message_view.js
+38
-0
lms/static/js/spec/learner_dashboard/course_card_view_spec.js
+62
-25
lms/static/sass/elements/_course-card.scss
+1
-0
lms/templates/learner_dashboard/certificate_status.underscore
+4
-2
lms/templates/learner_dashboard/course_card.underscore
+1
-0
lms/templates/learner_dashboard/upgrade_message.underscore
+12
-0
openedx/core/djangoapps/programs/tests/test_utils.py
+97
-30
openedx/core/djangoapps/programs/utils.py
+46
-19
No files found.
lms/djangoapps/learner_dashboard/views.py
View file @
1210d3fc
...
@@ -67,8 +67,9 @@ def program_details(request, program_id):
...
@@ -67,8 +67,9 @@ def program_details(request, program_id):
urls
=
{
urls
=
{
'program_listing_url'
:
reverse
(
'program_listing_view'
),
'program_listing_url'
:
reverse
(
'program_listing_view'
),
'track_selection_url'
:
strip_course_id
(
'track_selection_url'
:
strip_course_id
(
reverse
(
'course_modes_choose'
,
kwargs
=
{
'course_id'
:
FAKE_COURSE_KEY
})),
reverse
(
'course_modes_choose'
,
kwargs
=
{
'course_id'
:
FAKE_COURSE_KEY
})
'commerce_api_url'
:
reverse
(
'commerce_api:v0:baskets:create'
)
),
'commerce_api_url'
:
reverse
(
'commerce_api:v0:baskets:create'
),
}
}
context
=
{
context
=
{
...
...
lms/static/js/learner_dashboard/models/course_card_model.js
View file @
1210d3fc
...
@@ -65,17 +65,18 @@
...
@@ -65,17 +65,18 @@
course_key
:
runMode
.
course_key
,
course_key
:
runMode
.
course_key
,
course_url
:
runMode
.
course_url
||
''
,
course_url
:
runMode
.
course_url
||
''
,
display_name
:
this
.
context
.
display_name
,
display_name
:
this
.
context
.
display_name
,
start_date
:
runMode
.
start_date
,
end_date
:
runMode
.
end_date
,
end_date
:
runMode
.
end_date
,
enrollable_run_modes
:
this
.
getEnrollableRunModes
(),
enrollment_open_date
:
runMode
.
enrollment_open_date
||
''
,
is_course_ended
:
runMode
.
is_course_ended
,
is_enrolled
:
runMode
.
is_enrolled
,
is_enrolled
:
runMode
.
is_enrolled
,
is_enrollment_open
:
runMode
.
is_enrollment_open
,
is_enrollment_open
:
runMode
.
is_enrollment_open
,
key
:
this
.
context
.
key
,
key
:
this
.
context
.
key
,
marketing_url
:
runMode
.
marketing_url
||
''
,
marketing_url
:
runMode
.
marketing_url
||
''
,
is_course_ended
:
runMode
.
is_course_ended
,
mode_slug
:
runMode
.
mode_slug
,
mode_slug
:
runMode
.
mode_slug
,
run_key
:
runMode
.
run_key
,
run_key
:
runMode
.
run_key
,
enrollment_open_date
:
runMode
.
enrollment_open_date
||
''
,
start_date
:
runMode
.
start_date
,
enrollable_run_modes
:
this
.
getEnrollableRunModes
()
upgrade_url
:
runMode
.
upgrade_url
});
});
}
}
},
},
...
...
lms/static/js/learner_dashboard/views/certificate_status_view.js
View file @
1210d3fc
...
@@ -27,13 +27,10 @@
...
@@ -27,13 +27,10 @@
},
},
render
:
function
()
{
render
:
function
()
{
var
data
=
this
.
model
.
toJSON
(),
var
data
=
this
.
model
.
toJSON
();
$icons
;
data
=
$
.
extend
(
data
,
{
certificateSvg
:
this
.
iconTpl
()});
HtmlUtils
.
setHtml
(
this
.
$el
,
this
.
statusTpl
(
data
));
HtmlUtils
.
setHtml
(
this
.
$el
,
this
.
statusTpl
(
data
));
$icons
=
this
.
$
(
'.certificate-icon'
);
HtmlUtils
.
setHtml
(
$icons
,
this
.
iconTpl
());
}
}
});
});
}
}
...
...
lms/static/js/learner_dashboard/views/course_card_view.js
View file @
1210d3fc
...
@@ -7,6 +7,7 @@
...
@@ -7,6 +7,7 @@
'gettext'
,
'gettext'
,
'edx-ui-toolkit/js/utils/html-utils'
,
'edx-ui-toolkit/js/utils/html-utils'
,
'js/learner_dashboard/models/course_enroll_model'
,
'js/learner_dashboard/models/course_enroll_model'
,
'js/learner_dashboard/views/upgrade_message_view'
,
'js/learner_dashboard/views/certificate_status_view'
,
'js/learner_dashboard/views/certificate_status_view'
,
'js/learner_dashboard/views/course_enroll_view'
,
'js/learner_dashboard/views/course_enroll_view'
,
'text!../../../templates/learner_dashboard/course_card.underscore'
'text!../../../templates/learner_dashboard/course_card.underscore'
...
@@ -18,6 +19,7 @@
...
@@ -18,6 +19,7 @@
gettext
,
gettext
,
HtmlUtils
,
HtmlUtils
,
EnrollModel
,
EnrollModel
,
UpgradeMessageView
,
CertificateStatusView
,
CertificateStatusView
,
CourseEnrollView
,
CourseEnrollView
,
pageTpl
pageTpl
...
@@ -44,7 +46,8 @@
...
@@ -44,7 +46,8 @@
},
},
postRender
:
function
(){
postRender
:
function
(){
var
$certStatus
=
this
.
$
(
'.certificate-status'
);
var
$upgradeMessage
=
this
.
$
(
'.upgrade-message'
),
$certStatus
=
this
.
$
(
'.certificate-status'
);
this
.
enrollView
=
new
CourseEnrollView
({
this
.
enrollView
=
new
CourseEnrollView
({
$parentEl
:
this
.
$
(
'.course-actions'
),
$parentEl
:
this
.
$
(
'.course-actions'
),
...
@@ -53,13 +56,23 @@
...
@@ -53,13 +56,23 @@
enrollModel
:
this
.
enrollModel
enrollModel
:
this
.
enrollModel
});
});
if
(
this
.
model
.
get
(
'certificate_url'
)
)
{
if
(
this
.
model
.
get
(
'upgrade_url'
)
)
{
this
.
upgradeMessage
=
new
UpgradeMessageView
({
$el
:
$upgradeMessage
,
model
:
this
.
model
});
$certStatus
.
remove
();
}
else
if
(
this
.
model
.
get
(
'certificate_url'
)
)
{
this
.
certificateStatus
=
new
CertificateStatusView
({
this
.
certificateStatus
=
new
CertificateStatusView
({
$el
:
$certStatus
,
$el
:
$certStatus
,
model
:
this
.
model
model
:
this
.
model
});
});
$upgradeMessage
.
remove
();
}
else
{
}
else
{
// Styles are applied to the element that show if it's empty
// Styles are applied to these elements which will be visible if they're empty.
$upgradeMessage
.
remove
();
$certStatus
.
remove
();
$certStatus
.
remove
();
}
}
}
}
...
...
lms/static/js/learner_dashboard/views/upgrade_message_view.js
0 → 100644
View file @
1210d3fc
;(
function
(
define
)
{
'use strict'
;
define
([
'backbone'
,
'jquery'
,
'underscore'
,
'gettext'
,
'edx-ui-toolkit/js/utils/html-utils'
,
'text!../../../templates/learner_dashboard/upgrade_message.underscore'
,
'text!../../../templates/learner_dashboard/certificate_icon.underscore'
],
function
(
Backbone
,
$
,
_
,
gettext
,
HtmlUtils
,
upgradeMessageTpl
,
certificateIconTpl
)
{
return
Backbone
.
View
.
extend
({
messageTpl
:
HtmlUtils
.
template
(
upgradeMessageTpl
),
iconTpl
:
HtmlUtils
.
template
(
certificateIconTpl
),
initialize
:
function
(
options
)
{
this
.
$el
=
options
.
$el
;
this
.
render
();
},
render
:
function
()
{
var
data
=
this
.
model
.
toJSON
();
data
=
$
.
extend
(
data
,
{
certificateSvg
:
this
.
iconTpl
()});
HtmlUtils
.
setHtml
(
this
.
$el
,
this
.
messageTpl
(
data
));
}
});
}
);
}).
call
(
this
,
define
||
RequireJS
.
define
);
lms/static/js/spec/learner_dashboard/course_card_view_spec.js
View file @
1210d3fc
...
@@ -19,27 +19,30 @@ define([
...
@@ -19,27 +19,30 @@ define([
key
:
'ANUx'
key
:
'ANUx'
},
},
run_modes
:
[{
run_modes
:
[{
start_date
:
'Apr 25, 2016
'
,
certificate_url
:
'
'
,
end_date
:
'Jun 13, 2019
'
,
course_image_url
:
'http://test.com/image1
'
,
course_key
:
'course-v1:ANUx+ANU-ASTRO1x+3T2015'
,
course_key
:
'course-v1:ANUx+ANU-ASTRO1x+3T2015'
,
course_started
:
true
,
course_url
:
'http://localhost:8000/courses/course-v1:edX+DemoX+Demo_Course/info'
,
course_url
:
'http://localhost:8000/courses/course-v1:edX+DemoX+Demo_Course/info'
,
end_date
:
'Jun 13, 2019'
,
enrollment_open_date
:
'Mar 03, 2016'
,
is_course_ended
:
false
,
is_enrolled
:
true
,
is_enrollment_open
:
true
,
marketing_url
:
'https://www.edx.org/course/astrophysics-exploring'
,
marketing_url
:
'https://www.edx.org/course/astrophysics-exploring'
,
course_image_url
:
'http://test.com/image1'
,
mode_slug
:
'verified'
,
mode_slug
:
'verified'
,
run_key
:
'2T2016'
,
run_key
:
'2T2016'
,
course_started
:
true
,
start_date
:
'Apr 25, 2016'
,
is_enrolled
:
true
,
upgrade_url
:
''
is_course_ended
:
false
,
is_enrollment_open
:
true
,
certificate_url
:
''
,
enrollment_open_date
:
'Mar 03, 2016'
}]
}]
},
},
setupView
=
function
(
data
,
isEnrolled
){
setupView
=
function
(
data
,
isEnrolled
){
data
.
run_modes
[
0
].
is_enrolled
=
isEnrolled
;
var
programData
=
$
.
extend
({},
data
);
programData
.
run_modes
[
0
].
is_enrolled
=
isEnrolled
;
setFixtures
(
'<div class="course-card card"></div>'
);
setFixtures
(
'<div class="course-card card"></div>'
);
courseCardModel
=
new
CourseCardModel
(
d
ata
);
courseCardModel
=
new
CourseCardModel
(
programD
ata
);
view
=
new
CourseCardView
({
view
=
new
CourseCardView
({
model
:
courseCardModel
model
:
courseCardModel
});
});
...
@@ -86,37 +89,71 @@ define([
...
@@ -86,37 +89,71 @@ define([
});
});
it
(
'should only show certificate status section if a certificate has been earned'
,
function
()
{
it
(
'should only show certificate status section if a certificate has been earned'
,
function
()
{
var
data
=
context
,
var
data
=
$
.
extend
({},
context
)
,
certUrl
=
'sample-certificate'
;
certUrl
=
'sample-certificate'
;
setupView
(
context
,
false
);
expect
(
view
.
$
(
'.certificate-status'
).
length
).
toEqual
(
0
);
expect
(
view
.
$
(
'certificate-status'
).
length
).
toEqual
(
0
);
view
.
remove
();
view
.
remove
();
data
.
run_modes
[
0
].
certificate_url
=
certUrl
;
data
.
run_modes
[
0
].
certificate_url
=
certUrl
;
setupView
(
data
,
false
);
setupView
(
data
,
false
);
expect
(
view
.
$
(
'.certificate-status'
).
length
).
toEqual
(
1
);
expect
(
view
.
$
(
'.certificate-status'
).
length
).
toEqual
(
1
);
expect
(
view
.
$
(
'.certificate-status .cta-secondary'
).
attr
(
'href'
)).
toEqual
(
certUrl
);
expect
(
view
.
$
(
'.certificate-status .cta-secondary'
).
attr
(
'href'
)).
toEqual
(
certUrl
);
});
});
it
(
'should render the course card with coming soon'
,
function
(){
it
(
'should only show upgrade message section if an upgrade is required'
,
function
()
{
var
data
=
$
.
extend
({},
context
),
upgradeUrl
=
'/path/to/upgrade'
;
expect
(
view
.
$
(
'.upgrade-message'
).
length
).
toEqual
(
0
);
view
.
remove
();
view
.
remove
();
context
.
run_modes
[
0
].
is_enrollment_open
=
false
;
setupView
(
context
,
false
);
data
.
run_modes
[
0
].
upgrade_url
=
upgradeUrl
;
expect
(
view
.
$
(
'.header-img'
).
attr
(
'src'
)).
toEqual
(
context
.
run_modes
[
0
].
course_image_url
);
setupView
(
data
,
false
);
expect
(
view
.
$
(
'.course-details .course-title'
).
text
().
trim
()).
toEqual
(
context
.
display_name
);
expect
(
view
.
$
(
'.upgrade-message'
).
length
).
toEqual
(
1
);
expect
(
view
.
$
(
'.upgrade-message .cta-primary'
).
attr
(
'href'
)).
toEqual
(
upgradeUrl
);
});
it
(
'should not show both the upgrade message and certificate status sections'
,
function
()
{
var
data
=
$
.
extend
({},
context
);
// Verify that no empty elements are left in the DOM.
data
.
run_modes
[
0
].
upgrade_url
=
''
;
data
.
run_modes
[
0
].
certificate_url
=
''
;
setupView
(
data
,
false
);
expect
(
view
.
$
(
'.upgrade-message'
).
length
).
toEqual
(
0
);
expect
(
view
.
$
(
'.certificate-status'
).
length
).
toEqual
(
0
);
view
.
remove
();
// Verify that the upgrade message takes priority.
data
.
run_modes
[
0
].
upgrade_url
=
'/path/to/upgrade'
;
data
.
run_modes
[
0
].
certificate_url
=
'/path/to/certificate'
;
setupView
(
data
,
false
);
expect
(
view
.
$
(
'.upgrade-message'
).
length
).
toEqual
(
1
);
expect
(
view
.
$
(
'.certificate-status'
).
length
).
toEqual
(
0
);
});
it
(
'should render the course card with coming soon'
,
function
(){
var
data
=
$
.
extend
({},
context
);
data
.
run_modes
[
0
].
is_enrollment_open
=
false
;
setupView
(
data
,
false
);
expect
(
view
.
$
(
'.header-img'
).
attr
(
'src'
)).
toEqual
(
data
.
run_modes
[
0
].
course_image_url
);
expect
(
view
.
$
(
'.course-details .course-title'
).
text
().
trim
()).
toEqual
(
data
.
display_name
);
expect
(
view
.
$
(
'.course-details .course-title-link'
).
length
).
toBe
(
0
);
expect
(
view
.
$
(
'.course-details .course-title-link'
).
length
).
toBe
(
0
);
expect
(
view
.
$
(
'.course-details .course-text .course-key'
).
html
()).
toEqual
(
context
.
key
);
expect
(
view
.
$
(
'.course-details .course-text .course-key'
).
html
()).
toEqual
(
data
.
key
);
expect
(
view
.
$
(
'.course-details .course-text .run-period'
).
length
).
toBe
(
0
);
expect
(
view
.
$
(
'.course-details .course-text .run-period'
).
length
).
toBe
(
0
);
expect
(
view
.
$
(
'.no-action-message'
).
text
().
trim
()).
toBe
(
'Coming Soon'
);
expect
(
view
.
$
(
'.no-action-message'
).
text
().
trim
()).
toBe
(
'Coming Soon'
);
expect
(
view
.
$
(
'.enrollment-open-date'
).
text
().
trim
())
expect
(
view
.
$
(
'.enrollment-open-date'
).
text
().
trim
())
.
toEqual
(
context
.
run_modes
[
0
].
enrollment_open_date
);
.
toEqual
(
data
.
run_modes
[
0
].
enrollment_open_date
);
});
});
it
(
'should render if enrollment_open_date is not provided'
,
function
(){
it
(
'should render if enrollment_open_date is not provided'
,
function
(){
view
.
remove
();
var
data
=
$
.
extend
({},
context
);
context
.
run_modes
[
0
].
is_enrollment_open
=
true
;
delete
context
.
run_modes
[
0
].
enrollment_open_date
;
data
.
run_modes
[
0
].
is_enrollment_open
=
true
;
setupView
(
context
,
false
);
delete
data
.
run_modes
[
0
].
enrollment_open_date
;
setupView
(
data
,
false
);
validateCourseInfoDisplay
();
validateCourseInfoDisplay
();
});
});
});
});
...
...
lms/static/sass/elements/_course-card.scss
View file @
1210d3fc
...
@@ -128,6 +128,7 @@
...
@@ -128,6 +128,7 @@
}
}
}
}
.upgrade-message
,
.certificate-status
{
.certificate-status
{
border-top
:
1px
solid
palette
(
grayscale
,
x-trans
);
border-top
:
1px
solid
palette
(
grayscale
,
x-trans
);
padding-top
:
$baseline
;
padding-top
:
$baseline
;
...
...
lms/templates/learner_dashboard/certificate_status.underscore
View file @
1210d3fc
<div class="message col-12 md-col-8">
<div class="message col-12 md-col-8">
<span class="certificate-icon green-certificate-icon" aria-hidden="true"></span>
<% // safe-lint: disable=underscore-not-escaped %>
<span class="certificate-icon green-certificate-icon" aria-hidden="true"><%= certificateSvg %></span>
<span class="card-msg"><%- gettext('Congratulations! You have earned a certificate for this course.') %></span>
<span class="card-msg"><%- gettext('Congratulations! You have earned a certificate for this course.') %></span>
</div>
</div>
<div class="action col-12 md-col-4">
<div class="action col-12 md-col-4">
<a href="<%- certificate_url %>" class="btn-brand cta-secondary">
<a href="<%- certificate_url %>" class="btn-brand cta-secondary">
<span class="certificate-icon blue-certificate-icon" aria-hidden="true"></span>
<% // safe-lint: disable=underscore-not-escaped %>
<span class="certificate-icon blue-certificate-icon" aria-hidden="true"><%= certificateSvg %></span>
<%- gettext('View/Share Certificate') %>
<%- gettext('View/Share Certificate') %>
</a>
</a>
</div>
</div>
lms/templates/learner_dashboard/course_card.underscore
View file @
1210d3fc
...
@@ -39,4 +39,5 @@
...
@@ -39,4 +39,5 @@
</div>
</div>
</div>
</div>
<div class="section action-msg-view"></div>
<div class="section action-msg-view"></div>
<div class="section upgrade-message"></div>
<div class="section certificate-status"></div>
<div class="section certificate-status"></div>
lms/templates/learner_dashboard/upgrade_message.underscore
0 → 100644
View file @
1210d3fc
<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="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>
<%- gettext('Upgrade Now') %>
</a>
</div>
openedx/core/djangoapps/programs/tests/test_utils.py
View file @
1210d3fc
...
@@ -10,6 +10,7 @@ from django.conf import settings
...
@@ -10,6 +10,7 @@ from django.conf import settings
from
django.core.cache
import
cache
from
django.core.cache
import
cache
from
django.core.urlresolvers
import
reverse
from
django.core.urlresolvers
import
reverse
from
django.test
import
TestCase
from
django.test
import
TestCase
from
django.test.utils
import
override_settings
from
django.utils
import
timezone
from
django.utils
import
timezone
import
httpretty
import
httpretty
import
mock
import
mock
...
@@ -18,6 +19,7 @@ from edx_oauth2_provider.tests.factories import ClientFactory
...
@@ -18,6 +19,7 @@ from edx_oauth2_provider.tests.factories import ClientFactory
from
provider.constants
import
CONFIDENTIAL
from
provider.constants
import
CONFIDENTIAL
from
lms.djangoapps.certificates.api
import
MODES
from
lms.djangoapps.certificates.api
import
MODES
from
lms.djangoapps.commerce.tests.test_utils
import
update_commerce_config
from
openedx.core.djangoapps.content.course_overviews.models
import
CourseOverview
from
openedx.core.djangoapps.content.course_overviews.models
import
CourseOverview
from
openedx.core.djangoapps.credentials.tests
import
factories
as
credentials_factories
from
openedx.core.djangoapps.credentials.tests
import
factories
as
credentials_factories
from
openedx.core.djangoapps.credentials.tests.mixins
import
CredentialsApiConfigMixin
,
CredentialsDataMixin
from
openedx.core.djangoapps.credentials.tests.mixins
import
CredentialsApiConfigMixin
,
CredentialsDataMixin
...
@@ -34,6 +36,7 @@ from xmodule.modulestore.tests.factories import CourseFactory
...
@@ -34,6 +36,7 @@ from xmodule.modulestore.tests.factories import CourseFactory
UTILS_MODULE
=
'openedx.core.djangoapps.programs.utils'
UTILS_MODULE
=
'openedx.core.djangoapps.programs.utils'
CERTIFICATES_API_MODULE
=
'lms.djangoapps.certificates.api'
CERTIFICATES_API_MODULE
=
'lms.djangoapps.certificates.api'
ECOMMERCE_URL_ROOT
=
'http://example-ecommerce.com'
@skipUnless
(
settings
.
ROOT_URLCONF
==
'lms.urls'
,
'Test only valid in lms'
)
@skipUnless
(
settings
.
ROOT_URLCONF
==
'lms.urls'
,
'Test only valid in lms'
)
...
@@ -672,11 +675,14 @@ class TestProgramProgressMeter(ProgramsApiConfigMixin, TestCase):
...
@@ -672,11 +675,14 @@ class TestProgramProgressMeter(ProgramsApiConfigMixin, TestCase):
@ddt.ddt
@ddt.ddt
@override_settings
(
ECOMMERCE_PUBLIC_URL_ROOT
=
ECOMMERCE_URL_ROOT
)
@skipUnless
(
settings
.
ROOT_URLCONF
==
'lms.urls'
,
'Test only valid in lms'
)
@skipUnless
(
settings
.
ROOT_URLCONF
==
'lms.urls'
,
'Test only valid in lms'
)
class
TestSupplementProgramData
(
ProgramsApiConfigMixin
,
ModuleStoreTestCase
):
class
TestSupplementProgramData
(
ProgramsApiConfigMixin
,
ModuleStoreTestCase
):
"""Tests of the utility function used to supplement program data."""
"""Tests of the utility function used to supplement program data."""
password
=
'test'
maxDiff
=
None
maxDiff
=
None
sku
=
'abc123'
password
=
'test'
checkout_path
=
'/basket'
def
setUp
(
self
):
def
setUp
(
self
):
super
(
TestSupplementProgramData
,
self
)
.
setUp
()
super
(
TestSupplementProgramData
,
self
)
.
setUp
()
...
@@ -704,15 +710,18 @@ class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase):
...
@@ -704,15 +710,18 @@ class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase):
course_overview
=
CourseOverview
.
get_from_id
(
self
.
course
.
id
)
# pylint: disable=no-member
course_overview
=
CourseOverview
.
get_from_id
(
self
.
course
.
id
)
# pylint: disable=no-member
run_mode
=
dict
(
run_mode
=
dict
(
factories
.
RunMode
(
factories
.
RunMode
(
certificate_url
=
None
,
course_image_url
=
course_overview
.
course_image_url
,
course_key
=
unicode
(
self
.
course
.
id
),
# pylint: disable=no-member
course_key
=
unicode
(
self
.
course
.
id
),
# pylint: disable=no-member
course_url
=
reverse
(
'course_root'
,
args
=
[
self
.
course
.
id
]),
# pylint: disable=no-member
course_url
=
reverse
(
'course_root'
,
args
=
[
self
.
course
.
id
]),
# pylint: disable=no-member
course_image_url
=
course_overview
.
course_image_url
,
start_date
=
strftime_localized
(
self
.
course
.
start
,
'SHORT_DATE'
),
end_date
=
strftime_localized
(
self
.
course
.
end
,
'SHORT_DATE'
),
end_date
=
strftime_localized
(
self
.
course
.
end
,
'SHORT_DATE'
),
enrollment_open_date
=
None
,
is_course_ended
=
self
.
course
.
end
<
timezone
.
now
(),
is_course_ended
=
self
.
course
.
end
<
timezone
.
now
(),
is_enrolled
=
False
,
is_enrolled
=
False
,
is_enrollment_open
=
True
,
is_enrollment_open
=
True
,
marketing_url
=
''
,
marketing_url
=
None
,
start_date
=
strftime_localized
(
self
.
course
.
start
,
'SHORT_DATE'
),
upgrade_url
=
None
,
),
),
**
kwargs
**
kwargs
)
)
...
@@ -722,19 +731,72 @@ class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase):
...
@@ -722,19 +731,72 @@ class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase):
self
.
assertEqual
(
actual
,
expected
)
self
.
assertEqual
(
actual
,
expected
)
@ddt.data
(
True
,
False
)
@ddt.data
(
def
test_student_enrollment_status
(
self
,
is_enrolled
):
(
False
,
None
,
False
),
"""Verify that program data is supplemented correctly."""
(
True
,
MODES
.
audit
,
True
),
(
True
,
MODES
.
verified
,
False
),
)
@ddt.unpack
@mock.patch
(
UTILS_MODULE
+
'.CourseMode.mode_for_course'
)
def
test_student_enrollment_status
(
self
,
is_enrolled
,
enrolled_mode
,
is_upgrade_required
,
mock_get_mode
):
"""Verify that program data is supplemented with the student's enrollment status."""
expected_upgrade_url
=
'{root}/{path}?sku={sku}'
.
format
(
root
=
ECOMMERCE_URL_ROOT
,
path
=
self
.
checkout_path
.
strip
(
'/'
),
sku
=
self
.
sku
,
)
update_commerce_config
(
enabled
=
True
,
checkout_page
=
self
.
checkout_path
)
mock_mode
=
mock
.
Mock
()
mock_mode
.
sku
=
self
.
sku
mock_get_mode
.
return_value
=
mock_mode
if
is_enrolled
:
if
is_enrolled
:
CourseEnrollmentFactory
(
user
=
self
.
user
,
course_id
=
self
.
course
.
id
)
# pylint: disable=no-member
CourseEnrollmentFactory
(
user
=
self
.
user
,
course_id
=
self
.
course
.
id
,
mode
=
enrolled_mode
)
# pylint: disable=no-member
data
=
utils
.
supplement_program_data
(
self
.
program
,
self
.
user
)
data
=
utils
.
supplement_program_data
(
self
.
program
,
self
.
user
)
self
.
_assert_supplemented
(
data
,
is_enrolled
=
is_enrolled
)
self
.
_assert_supplemented
(
data
,
is_enrolled
=
is_enrolled
,
upgrade_url
=
expected_upgrade_url
if
is_upgrade_required
else
None
)
@ddt.data
(
MODES
.
audit
,
MODES
.
verified
)
def
test_inactive_enrollment_no_upgrade
(
self
,
enrolled_mode
):
"""Verify that a student with an inactive enrollment isn't encouraged to upgrade."""
update_commerce_config
(
enabled
=
True
,
checkout_page
=
self
.
checkout_path
)
CourseEnrollmentFactory
(
user
=
self
.
user
,
course_id
=
self
.
course
.
id
,
# pylint: disable=no-member
mode
=
enrolled_mode
,
is_active
=
False
,
)
data
=
utils
.
supplement_program_data
(
self
.
program
,
self
.
user
)
self
.
_assert_supplemented
(
data
)
@mock.patch
(
UTILS_MODULE
+
'.CourseMode.mode_for_course'
)
def
test_ecommerce_disabled
(
self
,
mock_get_mode
):
"""Verify that the utility can operate when the ecommerce service is disabled."""
update_commerce_config
(
enabled
=
False
,
checkout_page
=
self
.
checkout_path
)
mock_mode
=
mock
.
Mock
()
mock_mode
.
sku
=
self
.
sku
mock_get_mode
.
return_value
=
mock_mode
CourseEnrollmentFactory
(
user
=
self
.
user
,
course_id
=
self
.
course
.
id
,
mode
=
MODES
.
audit
)
# pylint: disable=no-member
data
=
utils
.
supplement_program_data
(
self
.
program
,
self
.
user
)
self
.
_assert_supplemented
(
data
,
is_enrolled
=
True
,
upgrade_url
=
None
)
@ddt.data
(
@ddt.data
(
[
1
,
1
,
False
]
,
(
1
,
1
,
False
)
,
[
1
,
-
1
,
True
]
,
(
1
,
-
1
,
True
)
,
)
)
@ddt.unpack
@ddt.unpack
def
test_course_enrollment_status
(
self
,
start_offset
,
end_offset
,
is_enrollment_open
):
def
test_course_enrollment_status
(
self
,
start_offset
,
end_offset
,
is_enrollment_open
):
...
@@ -746,13 +808,15 @@ class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase):
...
@@ -746,13 +808,15 @@ class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase):
data
=
utils
.
supplement_program_data
(
self
.
program
,
self
.
user
)
data
=
utils
.
supplement_program_data
(
self
.
program
,
self
.
user
)
if
is_enrollment_open
:
if
is_enrollment_open
:
self
.
_assert_supplemented
(
data
,
is_enrollment_open
=
is_enrollment_open
)
enrollment_open_date
=
None
else
:
else
:
self
.
_assert_supplemented
(
enrollment_open_date
=
strftime_localized
(
self
.
course
.
enrollment_start
,
'SHORT_DATE'
)
data
,
is_enrollment_open
=
is_enrollment_open
,
self
.
_assert_supplemented
(
enrollment_open_date
=
strftime_localized
(
self
.
course
.
enrollment_start
,
'SHORT_DATE'
)
data
,
)
is_enrollment_open
=
is_enrollment_open
,
enrollment_open_date
=
enrollment_open_date
,
)
@ddt.data
(
True
,
False
)
@ddt.data
(
True
,
False
)
@mock.patch
(
UTILS_MODULE
+
'.certificate_api.certificate_downloadable_status'
)
@mock.patch
(
UTILS_MODULE
+
'.certificate_api.certificate_downloadable_status'
)
...
@@ -765,11 +829,21 @@ class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase):
...
@@ -765,11 +829,21 @@ class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase):
data
=
utils
.
supplement_program_data
(
self
.
program
,
self
.
user
)
data
=
utils
.
supplement_program_data
(
self
.
program
,
self
.
user
)
if
is_uuid_available
:
expected_url
=
reverse
(
expected_url
=
reverse
(
'certificates:render_cert_by_uuid'
,
kwargs
=
{
'certificate_uuid'
:
test_uuid
})
'certificates:render_cert_by_uuid'
,
self
.
_assert_supplemented
(
data
,
certificate_url
=
expected_url
)
kwargs
=
{
'certificate_uuid'
:
test_uuid
}
else
:
)
if
is_uuid_available
else
None
self
.
_assert_supplemented
(
data
)
self
.
_assert_supplemented
(
data
,
certificate_url
=
expected_url
)
@ddt.data
(
-
1
,
0
,
1
)
def
test_course_course_ended
(
self
,
days_offset
):
self
.
course
.
end
=
timezone
.
now
()
+
datetime
.
timedelta
(
days
=
days_offset
)
self
.
course
=
self
.
update_course
(
self
.
course
,
self
.
user
.
id
)
# pylint: disable=no-member
data
=
utils
.
supplement_program_data
(
self
.
program
,
self
.
user
)
self
.
_assert_supplemented
(
data
)
@mock.patch
(
UTILS_MODULE
+
'.get_organization_by_short_name'
)
@mock.patch
(
UTILS_MODULE
+
'.get_organization_by_short_name'
)
def
test_organization_logo_exists
(
self
,
mock_get_organization_by_short_name
):
def
test_organization_logo_exists
(
self
,
mock_get_organization_by_short_name
):
...
@@ -780,6 +854,7 @@ class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase):
...
@@ -780,6 +854,7 @@ class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase):
mock_get_organization_by_short_name
.
return_value
=
{
mock_get_organization_by_short_name
.
return_value
=
{
'logo'
:
mock_image
'logo'
:
mock_image
}
}
data
=
utils
.
supplement_program_data
(
self
.
program
,
self
.
user
)
data
=
utils
.
supplement_program_data
(
self
.
program
,
self
.
user
)
self
.
assertEqual
(
data
[
'organizations'
][
0
]
.
get
(
'img'
),
mock_logo_url
)
self
.
assertEqual
(
data
[
'organizations'
][
0
]
.
get
(
'img'
),
mock_logo_url
)
...
@@ -799,11 +874,3 @@ class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase):
...
@@ -799,11 +874,3 @@ class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase):
mock_get_organization_by_short_name
.
return_value
=
{
'logo'
:
None
}
mock_get_organization_by_short_name
.
return_value
=
{
'logo'
:
None
}
data
=
utils
.
supplement_program_data
(
self
.
program
,
self
.
user
)
data
=
utils
.
supplement_program_data
(
self
.
program
,
self
.
user
)
self
.
assertEqual
(
data
[
'organizations'
][
0
]
.
get
(
'img'
),
None
)
self
.
assertEqual
(
data
[
'organizations'
][
0
]
.
get
(
'img'
),
None
)
@ddt.data
(
-
1
,
0
,
1
)
def
test_course_course_ended
(
self
,
days_offset
):
self
.
course
.
end
=
timezone
.
now
()
+
datetime
.
timedelta
(
days
=
days_offset
)
self
.
course
=
self
.
update_course
(
self
.
course
,
self
.
user
.
id
)
# pylint: disable=no-member
data
=
utils
.
supplement_program_data
(
self
.
program
,
self
.
user
)
self
.
_assert_supplemented
(
data
)
openedx/core/djangoapps/programs/utils.py
View file @
1210d3fc
...
@@ -10,7 +10,9 @@ from django.utils.text import slugify
...
@@ -10,7 +10,9 @@ from django.utils.text import slugify
from
opaque_keys.edx.keys
import
CourseKey
from
opaque_keys.edx.keys
import
CourseKey
import
pytz
import
pytz
from
course_modes.models
import
CourseMode
from
lms.djangoapps.certificates
import
api
as
certificate_api
from
lms.djangoapps.certificates
import
api
as
certificate_api
from
lms.djangoapps.commerce.utils
import
EcommerceService
from
openedx.core.djangoapps.content.course_overviews.models
import
CourseOverview
from
openedx.core.djangoapps.content.course_overviews.models
import
CourseOverview
from
openedx.core.djangoapps.programs.models
import
ProgramsApiConfig
from
openedx.core.djangoapps.programs.models
import
ProgramsApiConfig
from
openedx.core.lib.edx_api_utils
import
get_edx_api_data
from
openedx.core.lib.edx_api_utils
import
get_edx_api_data
...
@@ -322,6 +324,7 @@ class ProgramProgressMeter(object):
...
@@ -322,6 +324,7 @@ class ProgramProgressMeter(object):
return
parsed
return
parsed
# TODO: This function will benefit from being refactored as a class.
def
supplement_program_data
(
program_data
,
user
):
def
supplement_program_data
(
program_data
,
user
):
"""Supplement program course codes with CourseOverview and CourseEnrollment data.
"""Supplement program course codes with CourseOverview and CourseEnrollment data.
...
@@ -330,8 +333,8 @@ def supplement_program_data(program_data, user):
...
@@ -330,8 +333,8 @@ def supplement_program_data(program_data, user):
user (User): The user whose enrollments to inspect.
user (User): The user whose enrollments to inspect.
"""
"""
for
organization
in
program_data
[
'organizations'
]:
for
organization
in
program_data
[
'organizations'
]:
# TODO
cache the results of the get_organization_by_short_name call
# TODO
: Cache the results of the get_organization_by_short_name call so
#
so we don't have to hit database that frequently
#
the database is hit less frequently.
org_obj
=
get_organization_by_short_name
(
organization
[
'key'
])
org_obj
=
get_organization_by_short_name
(
organization
[
'key'
])
if
org_obj
and
org_obj
.
get
(
'logo'
):
if
org_obj
and
org_obj
.
get
(
'logo'
):
organization
[
'img'
]
=
org_obj
[
'logo'
]
.
url
organization
[
'img'
]
=
org_obj
[
'logo'
]
.
url
...
@@ -341,34 +344,58 @@ def supplement_program_data(program_data, user):
...
@@ -341,34 +344,58 @@ def supplement_program_data(program_data, user):
course_key
=
CourseKey
.
from_string
(
run_mode
[
'course_key'
])
course_key
=
CourseKey
.
from_string
(
run_mode
[
'course_key'
])
course_overview
=
CourseOverview
.
get_from_id
(
course_key
)
course_overview
=
CourseOverview
.
get_from_id
(
course_key
)
run_mode
[
'course_url'
]
=
reverse
(
'course_root'
,
args
=
[
course_key
])
course_url
=
reverse
(
'course_root'
,
args
=
[
course_key
])
run_mode
[
'course_image_url'
]
=
course_overview
.
course_image_url
course_image_url
=
course_overview
.
course_image_url
run_mode
[
'start_date'
]
=
course_overview
.
start_datetime_text
()
start_date_string
=
course_overview
.
start_datetime_text
()
run_mode
[
'end_date'
]
=
course_overview
.
end_datetime_text
()
end_date_string
=
course_overview
.
end_datetime_text
()
end_date
=
course_overview
.
end
or
datetime
.
datetime
.
max
.
replace
(
tzinfo
=
pytz
.
UTC
)
end_date
=
course_overview
.
end
or
datetime
.
datetime
.
max
.
replace
(
tzinfo
=
pytz
.
UTC
)
run_mode
[
'is_course_ended'
]
=
end_date
<
timezone
.
now
()
is_course_ended
=
end_date
<
timezone
.
now
()
run_mode
[
'is_enrolled'
]
=
CourseEnrollment
.
is_enrolled
(
user
,
course_key
)
is_enrolled
=
CourseEnrollment
.
is_enrolled
(
user
,
course_key
)
enrollment_start
=
course_overview
.
enrollment_start
or
datetime
.
datetime
.
min
.
replace
(
tzinfo
=
pytz
.
UTC
)
enrollment_start
=
course_overview
.
enrollment_start
or
datetime
.
datetime
.
min
.
replace
(
tzinfo
=
pytz
.
UTC
)
enrollment_end
=
course_overview
.
enrollment_end
or
datetime
.
datetime
.
max
.
replace
(
tzinfo
=
pytz
.
UTC
)
enrollment_end
=
course_overview
.
enrollment_end
or
datetime
.
datetime
.
max
.
replace
(
tzinfo
=
pytz
.
UTC
)
is_enrollment_open
=
enrollment_start
<=
timezone
.
now
()
<
enrollment_end
is_enrollment_open
=
enrollment_start
<=
timezone
.
now
()
<
enrollment_end
run_mode
[
'is_enrollment_open'
]
=
is_enrollment_open
if
not
is_enrollment_open
:
# Only render this enrollment open date if the enrollment open is in the future
run_mode
[
'enrollment_open_date'
]
=
strftime_localized
(
enrollment_start
,
'SHORT_DATE'
)
# TODO: Currently unavailable on LMS.
enrollment_open_date
=
None
if
is_enrollment_open
else
strftime_localized
(
enrollment_start
,
'SHORT_DATE'
)
run_mode
[
'marketing_url'
]
=
''
certificate_data
=
certificate_api
.
certificate_downloadable_status
(
user
,
course_key
)
certificate_data
=
certificate_api
.
certificate_downloadable_status
(
user
,
course_key
)
certificate_uuid
=
certificate_data
.
get
(
'uuid'
)
certificate_uuid
=
certificate_data
.
get
(
'uuid'
)
if
certificate_uuid
:
certificate_url
=
certificate_api
.
get_certificate_url
(
run_mode
[
'certificate_url'
]
=
certificate_api
.
get_certificate_url
(
course_id
=
course_key
,
course_id
=
course_key
,
uuid
=
certificate_uuid
,
uuid
=
certificate_uuid
,
)
if
certificate_uuid
else
None
)
required_mode_slug
=
run_mode
[
'mode_slug'
]
enrolled_mode_slug
,
_
=
CourseEnrollment
.
enrollment_mode_for_user
(
user
,
course_key
)
is_mode_mismatch
=
required_mode_slug
!=
enrolled_mode_slug
is_upgrade_required
=
is_enrolled
and
is_mode_mismatch
# Requires that the ecommerce service be in use.
required_mode
=
CourseMode
.
mode_for_course
(
course_key
,
required_mode_slug
)
ecommerce
=
EcommerceService
()
sku
=
getattr
(
required_mode
,
'sku'
,
None
)
if
ecommerce
.
is_enabled
(
user
)
and
sku
:
upgrade_url
=
ecommerce
.
checkout_page_url
(
required_mode
.
sku
)
if
is_upgrade_required
else
None
else
:
upgrade_url
=
None
run_mode
.
update
({
'certificate_url'
:
certificate_url
,
'course_image_url'
:
course_image_url
,
'course_url'
:
course_url
,
'end_date'
:
end_date_string
,
'enrollment_open_date'
:
enrollment_open_date
,
'is_course_ended'
:
is_course_ended
,
'is_enrolled'
:
is_enrolled
,
'is_enrollment_open'
:
is_enrollment_open
,
# TODO: Not currently available on LMS.
'marketing_url'
:
None
,
'start_date'
:
start_date_string
,
'upgrade_url'
:
upgrade_url
,
})
return
program_data
return
program_data
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