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
7330ff99
Commit
7330ff99
authored
Apr 07, 2017
by
Matthew Piatetsky
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add notification to program detail view
ECOM-7385
parent
6431d0f6
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
212 additions
and
22 deletions
+212
-22
lms/static/js/learner_dashboard/views/course_card_view_2017.js
+11
-1
lms/static/js/learner_dashboard/views/expired_notification_view.js
+33
-0
lms/static/js/learner_dashboard/views/program_header_view_2017.js
+3
-3
lms/static/lms/js/build.js
+1
-0
lms/static/sass/views/_program-details.scss
+26
-2
lms/templates/learner_dashboard/course_card_2017.underscore
+1
-1
lms/templates/learner_dashboard/expired_notification.underscore
+2
-0
openedx/core/djangoapps/programs/tests/test_utils.py
+42
-1
openedx/core/djangoapps/programs/utils.py
+93
-14
No files found.
lms/static/js/learner_dashboard/views/course_card_view_2017.js
View file @
7330ff99
...
@@ -9,6 +9,7 @@
...
@@ -9,6 +9,7 @@
'js/learner_dashboard/models/course_enroll_model'
,
'js/learner_dashboard/models/course_enroll_model'
,
'js/learner_dashboard/views/upgrade_message_view_2017'
,
'js/learner_dashboard/views/upgrade_message_view_2017'
,
'js/learner_dashboard/views/certificate_status_view_2017'
,
'js/learner_dashboard/views/certificate_status_view_2017'
,
'js/learner_dashboard/views/expired_notification_view'
,
'js/learner_dashboard/views/course_enroll_view_2017'
,
'js/learner_dashboard/views/course_enroll_view_2017'
,
'text!../../../templates/learner_dashboard/course_card_2017.underscore'
'text!../../../templates/learner_dashboard/course_card_2017.underscore'
],
],
...
@@ -21,6 +22,7 @@
...
@@ -21,6 +22,7 @@
EnrollModel
,
EnrollModel
,
UpgradeMessageView
,
UpgradeMessageView
,
CertificateStatusView
,
CertificateStatusView
,
ExpiredNotificationView
,
CourseEnrollView
,
CourseEnrollView
,
pageTpl
pageTpl
)
{
)
{
...
@@ -50,7 +52,8 @@
...
@@ -50,7 +52,8 @@
postRender
:
function
()
{
postRender
:
function
()
{
var
$upgradeMessage
=
this
.
$
(
'.upgrade-message'
),
var
$upgradeMessage
=
this
.
$
(
'.upgrade-message'
),
$certStatus
=
this
.
$
(
'.certificate-status'
);
$certStatus
=
this
.
$
(
'.certificate-status'
),
$expiredNotification
=
this
.
$
(
'.expired-notification'
);
this
.
enrollView
=
new
CourseEnrollView
({
this
.
enrollView
=
new
CourseEnrollView
({
$parentEl
:
this
.
$
(
'.course-actions'
),
$parentEl
:
this
.
$
(
'.course-actions'
),
...
@@ -78,6 +81,13 @@
...
@@ -78,6 +81,13 @@
$upgradeMessage
.
remove
();
$upgradeMessage
.
remove
();
$certStatus
.
remove
();
$certStatus
.
remove
();
}
}
if
(
this
.
model
.
get
(
'expired'
))
{
this
.
expiredNotification
=
new
ExpiredNotificationView
({
$el
:
$expiredNotification
,
model
:
this
.
model
});
}
}
}
});
});
}
}
...
...
lms/static/js/learner_dashboard/views/expired_notification_view.js
0 → 100644
View file @
7330ff99
(
function
(
define
)
{
'use strict'
;
define
([
'backbone'
,
'jquery'
,
'underscore'
,
'gettext'
,
'edx-ui-toolkit/js/utils/html-utils'
,
'text!../../../templates/learner_dashboard/expired_notification.underscore'
],
function
(
Backbone
,
$
,
_
,
gettext
,
HtmlUtils
,
expiredNotificationTpl
)
{
return
Backbone
.
View
.
extend
({
expiredNotificationTpl
:
HtmlUtils
.
template
(
expiredNotificationTpl
),
initialize
:
function
(
options
)
{
this
.
$el
=
options
.
$el
;
this
.
render
();
},
render
:
function
()
{
var
data
=
this
.
model
.
toJSON
();
HtmlUtils
.
setHtml
(
this
.
$el
,
this
.
expiredNotificationTpl
(
data
));
}
});
}
);
}).
call
(
this
,
define
||
RequireJS
.
define
);
lms/static/js/learner_dashboard/views/program_header_view_2017.js
View file @
7330ff99
...
@@ -5,9 +5,9 @@
...
@@ -5,9 +5,9 @@
'jquery'
,
'jquery'
,
'edx-ui-toolkit/js/utils/html-utils'
,
'edx-ui-toolkit/js/utils/html-utils'
,
'text!../../../templates/learner_dashboard/program_header_view_2017.underscore'
,
'text!../../../templates/learner_dashboard/program_header_view_2017.underscore'
,
'text!
/static
/images/programs/micromasters-program-details.svg'
,
'text!
../../..
/images/programs/micromasters-program-details.svg'
,
'text!
/static
/images/programs/xseries-program-details.svg'
,
'text!
../../..
/images/programs/xseries-program-details.svg'
,
'text!
/static
/images/programs/professional-certificate-program-details.svg'
'text!
../../..
/images/programs/professional-certificate-program-details.svg'
],
],
function
(
Backbone
,
$
,
HtmlUtils
,
pageTpl
,
MicroMastersLogo
,
function
(
Backbone
,
$
,
HtmlUtils
,
pageTpl
,
MicroMastersLogo
,
XSeriesLogo
,
ProfessionalCertificateLogo
)
{
XSeriesLogo
,
ProfessionalCertificateLogo
)
{
...
...
lms/static/lms/js/build.js
View file @
7330ff99
...
@@ -31,6 +31,7 @@
...
@@ -31,6 +31,7 @@
'js/groups/views/cohorts_dashboard_factory'
,
'js/groups/views/cohorts_dashboard_factory'
,
'js/header_factory'
,
'js/header_factory'
,
'js/learner_dashboard/program_details_factory'
,
'js/learner_dashboard/program_details_factory'
,
'js/learner_dashboard/program_details_factory_2017'
,
'js/learner_dashboard/program_list_factory'
,
'js/learner_dashboard/program_list_factory'
,
'js/search/course/course_search_factory'
,
'js/search/course/course_search_factory'
,
'js/search/dashboard/dashboard_search_factory'
,
'js/search/dashboard/dashboard_search_factory'
,
...
...
lms/static/sass/views/_program-details.scss
View file @
7330ff99
...
@@ -357,7 +357,7 @@
...
@@ -357,7 +357,7 @@
margin-bottom
:
10px
;
margin-bottom
:
10px
;
@media
(
min-width
:
$bp-screen-md
)
{
@media
(
min-width
:
$bp-screen-md
)
{
height
:
100px
;
height
:
auto
;
}
}
.section
{
.section
{
...
@@ -366,7 +366,7 @@
...
@@ -366,7 +366,7 @@
margin-right
:
40px
;
margin-right
:
40px
;
margin-left
:
15px
;
margin-left
:
15px
;
@media
(
min-width
:
$bp-screen-
md
)
{
@media
(
min-width
:
$bp-screen-
sm
)
{
margin-left
:
20px
;
margin-left
:
20px
;
}
}
...
@@ -496,5 +496,29 @@
...
@@ -496,5 +496,29 @@
}
}
}
}
.expired-notification
{
display
:
inline-block
;
padding-top
:
5px
;
width
:
300px
;
@media
(
min-width
:
$bp-screen-sm
)
{
padding-top
:
10px
;
width
:
500px
;
}
@media
(
min-width
:
$bp-screen-md
)
{
width
:
initial
;
}
}
.expired-icon
{
float
:
left
;
color
:
palette
(
primary
,
dark
);
}
.expired-text
{
overflow
:
hidden
;
padding-left
:
10px
;
}
}
}
}
}
lms/templates/learner_dashboard/course_card_2017.underscore
View file @
7330ff99
...
@@ -25,4 +25,4 @@
...
@@ -25,4 +25,4 @@
</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 upgrade-message"></div>
<div class="section expired-notification"></div>
lms/templates/learner_dashboard/expired_notification.underscore
0 → 100644
View file @
7330ff99
<div class="expired-icon"><span class="fa fa-info-circle fa-lg" aria-hidden="true"></span></div>
<div class="expired-text">You enrolled in this course but did not earn the certificate required to complete this program.</div>
openedx/core/djangoapps/programs/tests/test_utils.py
View file @
7330ff99
...
@@ -40,6 +40,7 @@ CERTIFICATES_API_MODULE = 'lms.djangoapps.certificates.api'
...
@@ -40,6 +40,7 @@ CERTIFICATES_API_MODULE = 'lms.djangoapps.certificates.api'
ECOMMERCE_URL_ROOT
=
'https://example-ecommerce.com'
ECOMMERCE_URL_ROOT
=
'https://example-ecommerce.com'
@ddt.ddt
@attr
(
shard
=
2
)
@attr
(
shard
=
2
)
@skip_unless_lms
@skip_unless_lms
@mock.patch
(
UTILS_MODULE
+
'.get_programs'
)
@mock.patch
(
UTILS_MODULE
+
'.get_programs'
)
...
@@ -53,7 +54,7 @@ class TestProgramProgressMeter(TestCase):
...
@@ -53,7 +54,7 @@ class TestProgramProgressMeter(TestCase):
def
_create_enrollments
(
self
,
*
course_run_ids
):
def
_create_enrollments
(
self
,
*
course_run_ids
):
"""Variadic helper used to create course run enrollments."""
"""Variadic helper used to create course run enrollments."""
for
course_run_id
in
course_run_ids
:
for
course_run_id
in
course_run_ids
:
CourseEnrollmentFactory
(
user
=
self
.
user
,
course_id
=
course_run_id
)
CourseEnrollmentFactory
(
user
=
self
.
user
,
course_id
=
course_run_id
,
mode
=
'verified'
)
def
_assert_progress
(
self
,
meter
,
*
progresses
):
def
_assert_progress
(
self
,
meter
,
*
progresses
):
"""Variadic helper used to verify progress calculations."""
"""Variadic helper used to verify progress calculations."""
...
@@ -150,6 +151,46 @@ class TestProgramProgressMeter(TestCase):
...
@@ -150,6 +151,46 @@ class TestProgramProgressMeter(TestCase):
self
.
assertEqual
(
meter
.
progress
(
count_only
=
False
),
expected
)
self
.
assertEqual
(
meter
.
progress
(
count_only
=
False
),
expected
)
@ddt.data
(
1
,
-
1
)
def
test_in_progress_course_upgrade_deadline_check
(
self
,
modifier
,
mock_get_programs
):
"""
Verify that if the user's enrollment is not of the same type as the course run,
the course will only count as in progress if there is another available seat with
the right type, where the upgrade deadline has not expired.
"""
course_run_key
=
generate_course_run_key
()
now
=
datetime
.
datetime
.
now
(
utc
)
date_modifier
=
modifier
*
datetime
.
timedelta
(
days
=
1
)
seat_with_upgrade_deadline
=
SeatFactory
(
type
=
'test'
,
upgrade_deadline
=
str
(
now
+
date_modifier
))
enrolled_seat
=
SeatFactory
(
type
=
'verified'
)
seats
=
[
seat_with_upgrade_deadline
,
enrolled_seat
]
data
=
[
ProgramFactory
(
courses
=
[
CourseFactory
(
course_runs
=
[
CourseRunFactory
(
key
=
course_run_key
,
type
=
'test'
,
seats
=
seats
),
]),
]
)
]
mock_get_programs
.
return_value
=
data
self
.
_create_enrollments
(
course_run_key
)
meter
=
ProgramProgressMeter
(
self
.
user
)
program
=
data
[
0
]
expected
=
[
ProgressFactory
(
uuid
=
program
[
'uuid'
],
completed
=
0
,
in_progress
=
1
if
modifier
==
1
else
0
,
not_started
=
1
if
modifier
==
-
1
else
0
)
]
self
.
assertEqual
(
meter
.
progress
(
count_only
=
True
),
expected
)
def
test_mutiple_program_engagement
(
self
,
mock_get_programs
):
def
test_mutiple_program_engagement
(
self
,
mock_get_programs
):
"""
"""
Verify that correct programs are returned in the correct order when the
Verify that correct programs are returned in the correct order when the
...
...
openedx/core/djangoapps/programs/utils.py
View file @
7330ff99
# -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
"""Helper functions for working with Programs."""
"""Helper functions for working with Programs."""
from
collections
import
defaultdict
from
collections
import
defaultdict
from
copy
import
deepcopy
import
datetime
import
datetime
from
urlparse
import
urljoin
from
urlparse
import
urljoin
...
@@ -69,8 +70,14 @@ class ProgramProgressMeter(object):
...
@@ -69,8 +70,14 @@ class ProgramProgressMeter(object):
self
.
enrollments
=
enrollments
or
list
(
CourseEnrollment
.
enrollments_for_user
(
self
.
user
))
self
.
enrollments
=
enrollments
or
list
(
CourseEnrollment
.
enrollments_for_user
(
self
.
user
))
self
.
enrollments
.
sort
(
key
=
lambda
e
:
e
.
created
,
reverse
=
True
)
self
.
enrollments
.
sort
(
key
=
lambda
e
:
e
.
created
,
reverse
=
True
)
# enrollment.course_id is really a CourseKey (╯ಠ_ಠ)╯︵ ┻━┻
self
.
enrolled_run_modes
=
{}
self
.
course_run_ids
=
[
unicode
(
e
.
course_id
)
for
e
in
self
.
enrollments
]
self
.
course_run_ids
=
[]
for
enrollment
in
self
.
enrollments
:
# enrollment.course_id is really a CourseKey (╯ಠ_ಠ)╯︵ ┻━┻
enrollment_id
=
unicode
(
enrollment
.
course_id
)
self
.
enrolled_run_modes
[
enrollment_id
]
=
enrollment
.
mode
# We can't use dict.keys() for this because the course run ids need to be ordered
self
.
course_run_ids
.
append
(
enrollment_id
)
if
uuid
:
if
uuid
:
self
.
programs
=
[
get_programs
(
uuid
=
uuid
)]
self
.
programs
=
[
get_programs
(
uuid
=
uuid
)]
...
@@ -127,6 +134,44 @@ class ProgramProgressMeter(object):
...
@@ -127,6 +134,44 @@ class ProgramProgressMeter(object):
return
programs
return
programs
def
_is_course_in_progress
(
self
,
now
,
course
):
"""Check if course qualifies as in progress as part of the program.
A course is considered to be in progress if a user is enrolled in a run
of the correct mode or a run of the correct mode is still available for enrollment.
Arguments:
now (datetime): datetime for now
course (dict): Containing nested course runs.
Returns:
bool, indicating whether the course is in progress.
"""
# Part 1: Check if any of the seats you are enrolled in qualify this course as in progress
enrolled_runs
=
[
run
for
run
in
course
[
'course_runs'
]
if
run
[
'key'
]
in
self
.
course_run_ids
]
# Check if the user is enrolled in the required mode for the run
runs_with_required_mode
=
[
run
for
run
in
enrolled_runs
if
run
[
'type'
]
==
self
.
enrolled_run_modes
[
run
[
'key'
]]
]
if
runs_with_required_mode
:
# Check if the runs you are enrolled in with the right mode are not failed
not_failed_runs
=
[
run
for
run
in
runs_with_required_mode
if
run
not
in
self
.
failed_course_runs
]
if
not_failed_runs
:
return
True
# Part 2: Check if any of the seats you are not enrolled in
# in the runs you are enrolled in qualify this course as in progress
upgrade_deadlines
=
[]
for
run
in
enrolled_runs
:
for
seat
in
run
[
'seats'
]:
if
seat
[
'type'
]
==
run
[
'type'
]
and
run
[
'type'
]
!=
self
.
enrolled_run_modes
[
run
[
'key'
]]:
upgrade_deadlines
.
append
(
seat
[
'upgrade_deadline'
])
course_still_upgradeable
=
any
(
(
deadline
is
not
None
)
and
(
parse
(
deadline
)
>
now
)
for
deadline
in
upgrade_deadlines
)
return
course_still_upgradeable
def
progress
(
self
,
programs
=
None
,
count_only
=
True
):
def
progress
(
self
,
programs
=
None
,
count_only
=
True
):
"""Gauge a user's progress towards program completion.
"""Gauge a user's progress towards program completion.
...
@@ -142,21 +187,29 @@ class ProgramProgressMeter(object):
...
@@ -142,21 +187,29 @@ class ProgramProgressMeter(object):
list of dict, each containing information about a user's progress
list of dict, each containing information about a user's progress
towards completing a program.
towards completing a program.
"""
"""
now
=
datetime
.
datetime
.
now
(
utc
)
progress
=
[]
progress
=
[]
programs
=
programs
or
self
.
engaged_programs
programs
=
programs
or
self
.
engaged_programs
for
program
in
programs
:
for
program
in
programs
:
program_copy
=
deepcopy
(
program
)
completed
,
in_progress
,
not_started
=
[],
[],
[]
completed
,
in_progress
,
not_started
=
[],
[],
[]
for
course
in
program
[
'courses'
]:
for
course
in
program
_copy
[
'courses'
]:
if
self
.
_is_course_complete
(
course
):
if
self
.
_is_course_complete
(
course
):
completed
.
append
(
course
)
completed
.
append
(
course
)
elif
self
.
_is_course_in_progress
(
course
):
elif
self
.
_is_course_enrolled
(
course
):
in_progress
.
append
(
course
)
course_in_progress
=
self
.
_is_course_in_progress
(
now
,
course
)
if
course_in_progress
:
in_progress
.
append
(
course
)
else
:
course
[
'expired'
]
=
not
course_in_progress
not_started
.
append
(
course
)
else
:
else
:
not_started
.
append
(
course
)
not_started
.
append
(
course
)
progress
.
append
({
progress
.
append
({
'uuid'
:
program
[
'uuid'
],
'uuid'
:
program
_copy
[
'uuid'
],
'completed'
:
len
(
completed
)
if
count_only
else
completed
,
'completed'
:
len
(
completed
)
if
count_only
else
completed
,
'in_progress'
:
len
(
in_progress
)
if
count_only
else
in_progress
,
'in_progress'
:
len
(
in_progress
)
if
count_only
else
in_progress
,
'not_started'
:
len
(
not_started
)
if
count_only
else
not_started
,
'not_started'
:
len
(
not_started
)
if
count_only
else
not_started
,
...
@@ -226,17 +279,43 @@ class ProgramProgressMeter(object):
...
@@ -226,17 +279,43 @@ class ProgramProgressMeter(object):
Returns:
Returns:
list of dicts, each representing a course run certificate
list of dicts, each representing a course run certificate
"""
"""
return
self
.
course_runs_with_state
[
'completed'
]
@cached_property
def
failed_course_runs
(
self
):
"""
Determine which course runs have been failed by the user.
Returns:
list of dicts, each a course run ID
"""
return
[
run
[
'course_run_id'
]
for
run
in
self
.
course_runs_with_state
[
'failed'
]]
@cached_property
def
course_runs_with_state
(
self
):
"""
Determine which course runs have been completed and failed by the user.
Returns:
dict with a list of completed and failed runs
"""
course_run_certificates
=
certificate_api
.
get_certificates_for_user
(
self
.
user
.
username
)
course_run_certificates
=
certificate_api
.
get_certificates_for_user
(
self
.
user
.
username
)
return
[
completed_runs
,
failed_runs
=
[],
[]
{
'course_run_id'
:
unicode
(
certificate
[
'course_key'
]),
'type'
:
certificate
[
'type'
]}
for
certificate
in
course_run_certificates
:
for
certificate
in
course_run_certificates
course_data
=
{
if
certificate_api
.
is_passing_status
(
certificate
[
'status'
])
'course_run_id'
:
unicode
(
certificate
[
'course_key'
]),
]
'type'
:
certificate
[
'type'
]
}
if
certificate_api
.
is_passing_status
(
certificate
[
'status'
]):
completed_runs
.
append
(
course_data
)
else
:
failed_runs
.
append
(
course_data
)
return
{
'completed'
:
completed_runs
,
'failed'
:
failed_runs
}
def
_is_course_
in_progress
(
self
,
course
):
def
_is_course_
enrolled
(
self
,
course
):
"""Check if a user is
in the process of completing
a course.
"""Check if a user is
enrolled in
a course.
A user is considered to be
in the process of completing
a course if
A user is considered to be
enrolled in
a course if
they're enrolled in any of the nested course runs.
they're enrolled in any of the nested course runs.
Arguments:
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