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
dda0e03f
Commit
dda0e03f
authored
Feb 09, 2017
by
Renzo Lucioni
Committed by
GitHub
Feb 09, 2017
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #14488 from edx/renzo/finish-catalog-transition
Finish transition to catalog for program data
parents
c0e45249
0e06e905
Show whitespace changes
Inline
Side-by-side
Showing
25 changed files
with
676 additions
and
742 deletions
+676
-742
common/djangoapps/student/views.py
+2
-6
lms/djangoapps/learner_dashboard/tests/test_programs.py
+1
-8
lms/djangoapps/learner_dashboard/views.py
+3
-7
lms/static/js/learner_dashboard/models/course_card_model.js
+99
-63
lms/static/js/learner_dashboard/models/program_model.js
+7
-7
lms/static/js/learner_dashboard/views/course_enroll_view.js
+12
-14
lms/static/js/learner_dashboard/views/program_card_view.js
+9
-14
lms/static/js/learner_dashboard/views/program_details_view.js
+1
-1
lms/static/js/spec/learner_dashboard/collection_list_view_spec.js
+82
-50
lms/static/js/spec/learner_dashboard/course_card_view_spec.js
+67
-64
lms/static/js/spec/learner_dashboard/course_enroll_view_spec.js
+160
-93
lms/static/js/spec/learner_dashboard/program_card_view_spec.js
+56
-36
lms/static/js/spec/learner_dashboard/program_details_header_spec.js
+40
-21
lms/templates/dashboard.html
+1
-1
lms/templates/dashboard/_dashboard_course_listing.html
+2
-8
lms/templates/learner_dashboard/course_card.underscore
+4
-4
lms/templates/learner_dashboard/course_enroll.underscore
+9
-9
lms/templates/learner_dashboard/program_card.underscore
+16
-16
lms/templates/learner_dashboard/program_header_view.underscore
+9
-9
openedx/core/djangoapps/catalog/tests/test_utils.py
+0
-58
openedx/core/djangoapps/catalog/utils.py
+0
-58
openedx/core/djangoapps/programs/tests/factories.py
+3
-6
openedx/core/djangoapps/programs/tests/test_utils.py
+53
-117
openedx/core/djangoapps/programs/utils.py
+39
-71
themes/edx.org/lms/templates/dashboard.html
+1
-1
No files found.
common/djangoapps/student/views.py
View file @
dda0e03f
...
...
@@ -122,15 +122,14 @@ import newrelic_custom_metrics
# Note that this lives in LMS, so this dependency should be refactored.
from
notification_prefs.views
import
enable_notifications
from
openedx.core.djangoapps.catalog.utils
import
get_programs_with_type_logo
from
openedx.core.djangoapps.credit.email_utils
import
get_credit_provider_display_names
,
make_providers_strings
from
openedx.core.djangoapps.lang_pref
import
LANGUAGE_KEY
from
openedx.core.djangoapps.catalog.utils
import
munge_catalog_program
from
openedx.core.djangoapps.programs.models
import
ProgramsApiConfig
from
openedx.core.djangoapps.programs.utils
import
ProgramProgressMeter
from
openedx.core.djangoapps.site_configuration
import
helpers
as
configuration_helpers
from
openedx.core.djangoapps.theming
import
helpers
as
theming_helpers
from
openedx.core.djangoapps.user_api.preferences
import
api
as
preferences_api
from
openedx.core.djangoapps.catalog.utils
import
get_programs_with_type_logo
log
=
logging
.
getLogger
(
"edx.student"
)
...
...
@@ -670,9 +669,6 @@ def dashboard(request):
meter
=
ProgramProgressMeter
(
user
,
enrollments
=
course_enrollments
)
inverted_programs
=
meter
.
invert_programs
()
for
program_list
in
inverted_programs
.
itervalues
():
program_list
[:]
=
[
munge_catalog_program
(
program
)
for
program
in
program_list
]
# Construct a dictionary of course mode information
# used to render the course list. We re-use the course modes dict
# we loaded earlier to avoid hitting the database.
...
...
@@ -795,7 +791,7 @@ def dashboard(request):
'order_history_list'
:
order_history_list
,
'courses_requirements_not_met'
:
courses_requirements_not_met
,
'nav_hidden'
:
True
,
'
programs_by_run
'
:
inverted_programs
,
'
inverted_programs
'
:
inverted_programs
,
'show_program_listing'
:
ProgramsApiConfig
.
current
()
.
show_program_listing
,
'disable_courseware_js'
:
True
,
'display_course_modes_on_dashboard'
:
enable_verified_certificates
and
display_course_modes_on_dashboard
,
...
...
lms/djangoapps/learner_dashboard/tests/test_programs.py
View file @
dda0e03f
...
...
@@ -16,7 +16,6 @@ import mock
from
openedx.core.djangoapps.catalog.tests.factories
import
ProgramFactory
,
CourseFactory
,
CourseRunFactory
from
openedx.core.djangoapps.catalog.tests.mixins
import
CatalogIntegrationMixin
from
openedx.core.djangoapps.catalog.utils
import
munge_catalog_program
from
openedx.core.djangoapps.credentials.tests.factories
import
UserCredential
,
ProgramCredential
from
openedx.core.djangoapps.credentials.tests.mixins
import
CredentialsApiConfigMixin
from
openedx.core.djangoapps.programs.tests.mixins
import
ProgramsApiConfigMixin
...
...
@@ -64,13 +63,7 @@ class TestProgramListing(ProgramsApiConfigMixin, CredentialsApiConfigMixin, Shar
"""
Helper function used to sort dictionaries representing programs.
"""
try
:
return
program
[
'title'
]
except
:
# pylint: disable=bare-except
# This is here temporarily because programs are still being munged
# to look like they came from the programs service before going out
# to the front end.
return
program
[
'name'
]
def
credential_sort_key
(
self
,
credential
):
"""
...
...
@@ -157,7 +150,7 @@ class TestProgramListing(ProgramsApiConfigMixin, CredentialsApiConfigMixin, Shar
actual
=
sorted
(
actual
,
key
=
self
.
program_sort_key
)
for
index
,
actual_program
in
enumerate
(
actual
):
expected_program
=
munge_catalog_program
(
self
.
data
[
index
])
expected_program
=
self
.
data
[
index
]
self
.
assert_dict_contains_subset
(
actual_program
,
expected_program
)
def
test_program_discovery
(
self
,
mock_get_programs
):
...
...
lms/djangoapps/learner_dashboard/views.py
View file @
dda0e03f
...
...
@@ -6,12 +6,11 @@ from django.views.decorators.http import require_GET
from
edxmako.shortcuts
import
render_to_response
from
lms.djangoapps.learner_dashboard.utils
import
strip_course_id
,
FAKE_COURSE_KEY
from
openedx.core.djangoapps.catalog.utils
import
get_programs
,
munge_catalog_program
from
openedx.core.djangoapps.catalog.utils
import
get_programs
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
(
get_program_marketing_url
,
munge_progress_map
,
ProgramProgressMeter
,
ProgramDataExtender
,
)
...
...
@@ -27,16 +26,14 @@ def program_listing(request):
raise
Http404
meter
=
ProgramProgressMeter
(
request
.
user
)
engaged_programs
=
[
munge_catalog_program
(
program
)
for
program
in
meter
.
engaged_programs
]
progress
=
[
munge_progress_map
(
progress_map
)
for
progress_map
in
meter
.
progress
]
context
=
{
'credentials'
:
get_programs_credentials
(
request
.
user
),
'disable_courseware_js'
:
True
,
'marketing_url'
:
get_program_marketing_url
(
programs_config
),
'nav_hidden'
:
True
,
'programs'
:
engaged_programs
,
'progress'
:
progress
,
'programs'
:
meter
.
engaged_programs
,
'progress'
:
meter
.
progress
,
'show_program_listing'
:
programs_config
.
show_program_listing
,
'uses_pattern_library'
:
True
,
}
...
...
@@ -56,7 +53,6 @@ def program_details(request, program_uuid):
if
not
program_data
:
raise
Http404
program_data
=
munge_catalog_program
(
program_data
)
program_data
=
ProgramDataExtender
(
program_data
,
request
.
user
)
.
extend
()
urls
=
{
...
...
lms/static/js/learner_dashboard/models/course_card_model.js
View file @
dda0e03f
...
...
@@ -5,60 +5,93 @@
'use strict'
;
define
([
'backbone'
,
'underscore'
,
'jquery'
,
'edx-ui-toolkit/js/utils/date-utils'
],
function
(
Backbone
,
DateUtils
)
{
function
(
Backbone
,
_
,
$
,
DateUtils
)
{
return
Backbone
.
Model
.
extend
({
initialize
:
function
(
data
)
{
if
(
data
)
{
this
.
context
=
data
;
this
.
setActive
RunMode
(
this
.
getRunMode
(
data
.
run_mode
s
),
data
.
user_preferences
);
this
.
setActive
CourseRun
(
this
.
getCourseRun
(
data
.
course_run
s
),
data
.
user_preferences
);
}
},
getUnselectedRunMode
:
function
(
runModes
)
{
if
(
runModes
&&
runModes
.
length
>
0
)
{
return
{
course_image_url
:
runModes
[
0
].
course_image_url
,
marketing_url
:
runModes
[
0
].
marketing_url
,
is_enrollment_open
:
runModes
[
0
].
is_enrollment_open
};
getCourseRun
:
function
(
courseRuns
)
{
var
enrolledCourseRun
=
_
.
findWhere
(
courseRuns
,
{
is_enrolled
:
true
}),
openEnrollmentCourseRuns
=
this
.
getEnrollableCourseRuns
(),
desiredCourseRun
;
// We populate our model by looking at the course runs.
if
(
enrolledCourseRun
)
{
// If the learner is already enrolled in a course run, return that one.
desiredCourseRun
=
enrolledCourseRun
;
}
else
if
(
openEnrollmentCourseRuns
.
length
>
0
)
{
if
(
openEnrollmentCourseRuns
.
length
===
1
)
{
desiredCourseRun
=
openEnrollmentCourseRuns
[
0
];
}
else
{
desiredCourseRun
=
this
.
getUnselectedCourseRun
(
openEnrollmentCourseRuns
);
}
}
else
{
desiredCourseRun
=
this
.
getUnselectedCourseRun
(
courseRuns
);
}
return
{}
;
return
desiredCourseRun
;
},
getRunMode
:
function
(
runModes
)
{
var
enrolled_mode
=
_
.
findWhere
(
runModes
,
{
is_enrolled
:
true
}),
openEnrollmentRunModes
=
this
.
getEnrollableRunModes
(),
desiredRunMode
;
// We populate our model by looking at the run modes.
if
(
enrolled_mode
)
{
// If the learner is already enrolled in a run mode, return that one.
desiredRunMode
=
enrolled_mode
;
}
else
if
(
openEnrollmentRunModes
.
length
>
0
)
{
if
(
openEnrollmentRunModes
.
length
===
1
)
{
desiredRunMode
=
openEnrollmentRunModes
[
0
];
getUnselectedCourseRun
:
function
(
courseRuns
)
{
var
unselectedRun
=
{},
courseRun
,
courseImageUrl
;
if
(
courseRuns
&&
courseRuns
.
length
>
0
)
{
courseRun
=
courseRuns
[
0
];
if
(
courseRun
.
hasOwnProperty
(
'image'
))
{
courseImageUrl
=
courseRun
.
image
.
src
;
}
else
{
desiredRunMode
=
this
.
getUnselectedRunMode
(
openEnrollmentRunModes
);
// The course_image_url property is attached by setActiveCourseRun.
// If that hasn't been called, it won't be present yet.
courseImageUrl
=
courseRun
.
course_image_url
;
}
}
else
{
desiredRunMode
=
this
.
getUnselectedRunMode
(
runModes
);
$
.
extend
(
unselectedRun
,
{
course_image_url
:
courseImageUrl
,
marketing_url
:
courseRun
.
marketing_url
,
is_enrollment_open
:
courseRun
.
is_enrollment_open
});
}
return
desiredRunMode
;
return
unselectedRun
;
},
getEnrollableRunModes
:
function
()
{
return
_
.
where
(
this
.
context
.
run_modes
,
{
getEnrollableCourseRuns
:
function
()
{
var
rawCourseRuns
,
enrollableCourseRuns
;
rawCourseRuns
=
_
.
where
(
this
.
context
.
course_runs
,
{
is_enrollment_open
:
true
,
is_enrolled
:
false
,
is_course_ended
:
false
});
// Deep copy to avoid mutating this.context.
enrollableCourseRuns
=
$
.
extend
(
true
,
[],
rawCourseRuns
);
// These are raw course runs from the server. The start
// dates are ISO-8601 formatted strings that need to be
// prepped for display.
_
.
each
(
enrollableCourseRuns
,
(
function
(
courseRun
)
{
// eslint-disable-next-line no-param-reassign
courseRun
.
start_date
=
this
.
formatDate
(
courseRun
.
start
);
}).
bind
(
this
));
return
enrollableCourseRuns
;
},
getUpcoming
RunMode
s
:
function
()
{
return
_
.
where
(
this
.
context
.
run_mode
s
,
{
getUpcoming
CourseRun
s
:
function
()
{
return
_
.
where
(
this
.
context
.
course_run
s
,
{
is_enrollment_open
:
false
,
is_enrolled
:
false
,
is_course_ended
:
false
...
...
@@ -82,51 +115,54 @@
return
DateUtils
.
localize
(
context
);
},
setActiveRunMode
:
function
(
runMode
,
userPreferences
)
{
var
startDateString
;
if
(
runMode
)
{
if
(
runMode
.
advertised_start
!==
undefined
&&
runMode
.
advertised_start
!==
'None'
)
{
startDateString
=
runMode
.
advertised_start
;
setActiveCourseRun
:
function
(
courseRun
,
userPreferences
)
{
var
startDateString
,
courseImageUrl
;
if
(
courseRun
)
{
if
(
courseRun
.
advertised_start
!==
undefined
&&
courseRun
.
advertised_start
!==
'None'
)
{
startDateString
=
courseRun
.
advertised_start
;
}
else
{
startDateString
=
this
.
formatDate
(
runMode
.
start_date
,
userPreferences
);
startDateString
=
this
.
formatDate
(
courseRun
.
start
,
userPreferences
);
}
if
(
courseRun
.
hasOwnProperty
(
'image'
))
{
courseImageUrl
=
courseRun
.
image
.
src
;
}
else
{
courseImageUrl
=
courseRun
.
course_image_url
;
}
this
.
set
({
certificate_url
:
runMode
.
certificate_url
,
course_image_url
:
runMode
.
course_image_url
||
''
,
course_key
:
runMode
.
course_key
,
course_url
:
runMode
.
course_url
||
''
,
display_name
:
this
.
context
.
display_name
,
end_date
:
this
.
formatDate
(
runMode
.
end_date
,
userPreferences
),
enrollable_run_modes
:
this
.
getEnrollableRunModes
(),
is_course_ended
:
runMode
.
is_course_ended
,
is_enrolled
:
runMode
.
is_enrolled
,
is_enrollment_open
:
runMode
.
is_enrollment_open
,
key
:
this
.
context
.
key
,
marketing_url
:
runMode
.
marketing_url
,
mode_slug
:
runMode
.
mode_slug
,
run_key
:
runMode
.
run_key
,
certificate_url
:
courseRun
.
certificate_url
,
course_image_url
:
courseImageUrl
||
''
,
course_run_key
:
courseRun
.
key
,
course_url
:
courseRun
.
course_url
||
''
,
title
:
this
.
context
.
title
,
end_date
:
this
.
formatDate
(
courseRun
.
end
,
userPreferences
),
enrollable_course_runs
:
this
.
getEnrollableCourseRuns
(),
is_course_ended
:
courseRun
.
is_course_ended
,
is_enrolled
:
courseRun
.
is_enrolled
,
is_enrollment_open
:
courseRun
.
is_enrollment_open
,
course_key
:
this
.
context
.
key
,
marketing_url
:
courseRun
.
marketing_url
,
mode_slug
:
courseRun
.
type
,
start_date
:
startDateString
,
upcoming_
run_modes
:
this
.
getUpcomingRunMode
s
(),
upgrade_url
:
runMode
.
upgrade_url
upcoming_
course_runs
:
this
.
getUpcomingCourseRun
s
(),
upgrade_url
:
courseRun
.
upgrade_url
});
}
},
setUnselected
:
function
()
{
// Called to reset the model back to the unselected state.
var
unselected
Mode
=
this
.
getUnselectedRunMode
(
this
.
get
(
'enrollable_run_mode
s'
));
this
.
setActive
RunMode
(
unselectedMode
);
var
unselected
CourseRun
=
this
.
getUnselectedCourseRun
(
this
.
get
(
'enrollable_course_run
s'
));
this
.
setActive
CourseRun
(
unselectedCourseRun
);
},
update
Run
:
function
(
r
unKey
)
{
var
selected
Run
=
_
.
findWhere
(
this
.
get
(
'run_modes'
),
{
run_key
:
r
unKey
});
if
(
selectedRun
)
{
this
.
setActive
RunMode
(
selected
Run
);
update
CourseRun
:
function
(
courseR
unKey
)
{
var
selected
CourseRun
=
_
.
findWhere
(
this
.
get
(
'course_runs'
),
{
key
:
courseR
unKey
});
if
(
selected
Course
Run
)
{
this
.
setActive
CourseRun
(
selectedCourse
Run
);
}
}
});
...
...
lms/static/js/learner_dashboard/models/program_model.js
View file @
dda0e03f
...
...
@@ -11,17 +11,17 @@
initialize
:
function
(
data
)
{
if
(
data
)
{
this
.
set
({
name
:
data
.
nam
e
,
category
:
data
.
category
,
title
:
data
.
titl
e
,
type
:
data
.
type
,
subtitle
:
data
.
subtitle
,
organizations
:
data
.
organizations
,
authoring_organizations
:
data
.
authoring_
organizations
,
detailUrl
:
data
.
detail_url
,
smallBannerUrl
:
data
.
banner_image_urls
.
w348h116
,
mediumBannerUrl
:
data
.
banner_image_urls
.
w435h145
,
largeBannerUrl
:
data
.
banner_image_urls
.
w726h242
,
xsmallBannerUrl
:
data
.
banner_image
[
'x-small'
].
url
,
smallBannerUrl
:
data
.
banner_image
.
small
.
url
,
mediumBannerUrl
:
data
.
banner_image
.
medium
.
url
,
breakpoints
:
{
max
:
{
tiny
:
'320px'
,
xsmall
:
'320px'
,
small
:
'540px'
,
medium
:
'768px'
,
large
:
'979px'
...
...
lms/static/js/learner_dashboard/views/course_enroll_view.js
View file @
dda0e03f
...
...
@@ -21,7 +21,7 @@
events
:
{
'click .enroll-button'
:
'handleEnroll'
,
'change .run-select'
:
'handleRunSelect'
'change .run-select'
:
'handle
Course
RunSelect'
},
initialize
:
function
(
options
)
{
...
...
@@ -45,12 +45,12 @@
handleEnroll
:
function
()
{
// Enrollment click event handled here
if
(
!
this
.
model
.
get
(
'course_key'
))
{
if
(
!
this
.
model
.
get
(
'course_
run_
key'
))
{
this
.
$
(
'.select-error'
).
css
(
'visibility'
,
'visible'
);
}
else
if
(
!
this
.
model
.
get
(
'is_enrolled'
))
{
// actually enroll
// Create the enrollment.
this
.
enrollModel
.
save
({
course_id
:
this
.
model
.
get
(
'course_key'
)
course_id
:
this
.
model
.
get
(
'course_
run_
key'
)
},
{
success
:
_
.
bind
(
this
.
enrollSuccess
,
this
),
error
:
_
.
bind
(
this
.
enrollError
,
this
)
...
...
@@ -58,24 +58,22 @@
}
},
handleRunSelect
:
function
(
event
)
{
var
runKey
;
if
(
event
.
target
)
{
runKey
=
$
(
event
.
target
).
val
();
if
(
runKey
)
{
this
.
model
.
updateRun
(
runKey
);
handleCourseRunSelect
:
function
(
event
)
{
var
courseRunKey
=
$
(
event
.
target
).
val
();
if
(
courseRunKey
)
{
this
.
model
.
updateCourseRun
(
courseRunKey
);
}
else
{
// Set back the unselected states
this
.
model
.
setUnselected
();
}
}
},
enrollSuccess
:
function
()
{
var
course
Key
=
this
.
model
.
get
(
'course
_key'
);
var
course
RunKey
=
this
.
model
.
get
(
'course_run
_key'
);
if
(
this
.
trackSelectionUrl
)
{
// Go to track selection page
this
.
redirect
(
this
.
trackSelectionUrl
+
courseKey
);
this
.
redirect
(
this
.
trackSelectionUrl
+
course
Run
Key
);
}
else
{
this
.
model
.
set
({
is_enrolled
:
true
...
...
@@ -98,7 +96,7 @@
* This can occur, for example, when a course does not
* have a free enrollment mode, so we can't auto-enroll.
*/
this
.
redirect
(
this
.
trackSelectionUrl
+
this
.
model
.
get
(
'course_key'
));
this
.
redirect
(
this
.
trackSelectionUrl
+
this
.
model
.
get
(
'course_
run_
key'
));
}
},
...
...
lms/static/js/learner_dashboard/views/program_card_view.js
View file @
dda0e03f
...
...
@@ -22,7 +22,7 @@
attributes
:
function
()
{
return
{
'aria-labelledby'
:
'program-'
+
this
.
model
.
get
(
'id'
),
'aria-labelledby'
:
'program-'
+
this
.
model
.
get
(
'
uu
id'
),
'role'
:
'group'
};
},
...
...
@@ -33,14 +33,14 @@
this
.
progressCollection
=
data
.
context
.
progressCollection
;
if
(
this
.
progressCollection
)
{
this
.
progressModel
=
this
.
progressCollection
.
findWhere
({
id
:
this
.
model
.
get
(
'
id'
)
uuid
:
this
.
model
.
get
(
'uu
id'
)
});
}
this
.
render
();
},
render
:
function
()
{
var
orgList
=
_
.
map
(
this
.
model
.
get
(
'organizations'
),
function
(
org
)
{
var
orgList
=
_
.
map
(
this
.
model
.
get
(
'
authoring_
organizations'
),
function
(
org
)
{
return
gettext
(
org
.
key
);
}),
data
=
$
.
extend
(
...
...
@@ -56,7 +56,7 @@
postRender
:
function
()
{
// Add describedby to parent only if progess is present
if
(
this
.
progressModel
)
{
this
.
$el
.
attr
(
'aria-describedby'
,
'status-'
+
this
.
model
.
get
(
'id'
));
this
.
$el
.
attr
(
'aria-describedby'
,
'status-'
+
this
.
model
.
get
(
'
uu
id'
));
}
if
(
navigator
.
userAgent
.
indexOf
(
'MSIE'
)
!==
-
1
||
...
...
@@ -73,19 +73,14 @@
var
progress
=
this
.
progressModel
?
this
.
progressModel
.
toJSON
()
:
false
;
if
(
progress
)
{
progress
.
total
=
{
completed
:
progress
.
completed
.
length
,
in_progress
:
progress
.
in_progress
.
length
,
not_started
:
progress
.
not_started
.
length
};
progress
.
total
.
courses
=
progress
.
total
.
completed
+
progress
.
total
.
in_progress
+
progress
.
total
.
not_started
;
progress
.
total
=
progress
.
completed
+
progress
.
in_progress
+
progress
.
not_started
;
progress
.
percentage
=
{
completed
:
this
.
getWidth
(
progress
.
total
.
completed
,
progress
.
total
.
courses
),
in_progress
:
this
.
getWidth
(
progress
.
total
.
in_progress
,
progress
.
total
.
courses
)
completed
:
this
.
getWidth
(
progress
.
completed
,
progress
.
total
),
in_progress
:
this
.
getWidth
(
progress
.
in_progress
,
progress
.
total
)
};
}
...
...
lms/static/js/learner_dashboard/views/program_details_view.js
View file @
dda0e03f
...
...
@@ -33,7 +33,7 @@
this
.
options
=
options
;
this
.
programModel
=
new
Backbone
.
Model
(
this
.
options
.
programData
);
this
.
courseCardCollection
=
new
CourseCardCollection
(
this
.
programModel
.
get
(
'course
_code
s'
),
this
.
programModel
.
get
(
'courses'
),
this
.
options
.
userPreferences
);
this
.
render
();
...
...
lms/static/js/spec/learner_dashboard/collection_list_view_spec.js
View file @
dda0e03f
...
...
@@ -5,8 +5,7 @@ define([
'js/learner_dashboard/collections/program_collection'
,
'js/learner_dashboard/views/collection_list_view'
,
'js/learner_dashboard/collections/program_progress_collection'
],
function
(
Backbone
,
$
,
ProgramCardView
,
ProgramCollection
,
CollectionListView
,
ProgressCollection
)
{
],
function
(
Backbone
,
$
,
ProgramCardView
,
ProgramCollection
,
CollectionListView
,
ProgressCollection
)
{
'use strict'
;
/* jslint maxlen: 500 */
...
...
@@ -17,62 +16,90 @@ define([
context
=
{
programsData
:
[
{
category
:
'xseries'
,
status
:
'active'
,
subtitle
:
'program 1'
,
name
:
'test program 1'
,
organizations
:
[
{
display_name
:
'edX'
,
key
:
'edx'
}
],
created
:
'2016-03-03T19:18:50.061136Z'
,
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'
,
banner_image_urls
:
{
w348h116
:
'http://www.edx.org/images/org1/test1'
,
w435h145
:
'http://www.edx.org/images/org1/test2'
,
w726h242
:
'http://www.edx.org/images/org1/test3'
uuid
:
'a87e5eac-3c93-45a1-a8e1-4c79ca8401c8'
,
title
:
'Food Security and Sustainability'
,
subtitle
:
'Learn how to feed all people in the world in a sustainable way.'
,
type
:
'XSeries'
,
detail_url
:
'https://www.edx.org/foo/bar'
,
banner_image
:
{
medium
:
{
height
:
242
,
width
:
726
,
url
:
'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.medium.jpg'
},
'x-small'
:
{
height
:
116
,
width
:
348
,
url
:
'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.x-small.jpg'
},
small
:
{
height
:
145
,
width
:
435
,
url
:
'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.small.jpg'
},
large
:
{
height
:
480
,
width
:
1440
,
url
:
'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.large.jpg'
}
},
authoring_organizations
:
[
{
category
:
'xseries'
,
status
:
'active'
,
subtitle
:
'fda'
,
name
:
'fda'
,
organizations
:
[
uuid
:
'0c6e5fa2-96e8-40b2-9ebe-c8b0df2a3b22'
,
key
:
'WageningenX'
,
name
:
'Wageningen University & Research'
}
]
},
{
display_name
:
'edX'
,
key
:
'edx'
uuid
:
'91d144d2-1bb1-4afe-90df-d5cff63fa6e2'
,
title
:
'edX Course Creator'
,
subtitle
:
'Become an expert in creating courses for the edX platform.'
,
type
:
'XSeries'
,
detail_url
:
'https://www.edx.org/foo/bar'
,
banner_image
:
{
medium
:
{
height
:
242
,
width
:
726
,
url
:
'https://example.com/91d144d2-1bb1-4afe-90df-d5cff63fa6e2.medium.jpg'
},
'x-small'
:
{
height
:
116
,
width
:
348
,
url
:
'https://example.com/91d144d2-1bb1-4afe-90df-d5cff63fa6e2.x-small.jpg'
},
small
:
{
height
:
145
,
width
:
435
,
url
:
'https://example.com/91d144d2-1bb1-4afe-90df-d5cff63fa6e2.small.jpg'
},
large
:
{
height
:
480
,
width
:
1440
,
url
:
'https://example.com/91d144d2-1bb1-4afe-90df-d5cff63fa6e2.large.jpg'
}
],
created
:
'2016-03-09T14:30:41.484848Z'
,
modified
:
'2016-03-09T14:30:52.840898Z'
,
marketing_slug
:
'gdaf'
,
id
:
147
,
marketing_url
:
'http://www.edx.org/xseries/gdaf'
,
banner_image_urls
:
{
w348h116
:
'http://www.edx.org/images/org2/test1'
,
w435h145
:
'http://www.edx.org/images/org2/test2'
,
w726h242
:
'http://www.edx.org/images/org2/test3'
},
authoring_organizations
:
[
{
uuid
:
'4f8cb2c9-589b-4d1e-88c1-b01a02db3a9c'
,
key
:
'edX'
,
name
:
'edX'
}
]
}
],
userProgress
:
[
{
id
:
146
,
completed
:
[
'courses'
,
'the'
,
'user'
,
'completed'
]
,
in_progress
:
[
'in'
,
'progress'
]
,
not_started
:
[
'courses'
,
'not'
,
'yet'
,
'started'
]
uuid
:
'a87e5eac-3c93-45a1-a8e1-4c79ca8401c8'
,
completed
:
4
,
in_progress
:
2
,
not_started
:
4
},
{
id
:
147
,
completed
:
[
'Course 1'
]
,
in_progress
:
[]
,
not_started
:
[
'Course 2'
,
'Course 3'
,
'Course 4'
]
uuid
:
'91d144d2-1bb1-4afe-90df-d5cff63fa6e2'
,
completed
:
1
,
in_progress
:
0
,
not_started
:
3
}
]
};
...
...
@@ -105,7 +132,8 @@ define([
var
$cards
=
view
.
$el
.
find
(
'.program-card'
);
expect
(
$cards
.
length
).
toBe
(
2
);
$cards
.
each
(
function
(
index
,
el
)
{
expect
(
$
(
el
).
find
(
'.title'
).
html
().
trim
()).
toEqual
(
context
.
programsData
[
index
].
name
);
// eslint-disable-next-line newline-per-chained-call
expect
(
$
(
el
).
find
(
'.title'
).
html
().
trim
()).
toEqual
(
context
.
programsData
[
index
].
title
);
});
});
...
...
@@ -116,13 +144,14 @@ define([
view
=
new
CollectionListView
({
el
:
'.program-cards-container'
,
childView
:
ProgramCardView
,
context
:
{
'xseriesUrl'
:
'/programs'
},
context
:
{},
collection
:
programCollection
});
view
.
render
();
$cards
=
view
.
$el
.
find
(
'.program-card'
);
expect
(
$cards
.
length
).
toBe
(
0
);
});
it
(
'should have no title when title not provided'
,
function
()
{
var
$title
;
setFixtures
(
'<div class="test-container"><div class="program-cards-container"></div></div>'
);
...
...
@@ -132,15 +161,18 @@ define([
$title
=
view
.
$el
.
parent
().
find
(
'.collection-title'
);
expect
(
$title
.
html
()).
not
.
toBeDefined
();
});
it
(
'should display screen reader header when provided'
,
function
()
{
var
$title
,
titleContext
=
{
el
:
'h2'
,
title
:
'list start'
};
var
titleContext
=
{
el
:
'h2'
,
title
:
'list start'
},
$title
;
view
.
remove
();
setFixtures
(
'<div class="test-container"><div class="program-cards-container"></div></div>'
);
programCollection
=
new
ProgramCollection
(
context
.
programsData
);
view
=
new
CollectionListView
({
el
:
'.program-cards-container'
,
childView
:
ProgramCardView
,
context
:
{
'xseriesUrl'
:
'/programs'
}
,
context
:
context
,
collection
:
programCollection
,
titleContext
:
titleContext
});
...
...
lms/static/js/spec/learner_dashboard/course_card_view_spec.js
View file @
dda0e03f
...
...
@@ -9,12 +9,14 @@ define([
describe
(
'Course Card View'
,
function
()
{
var
view
=
null
,
courseCardModel
,
context
,
course
,
startDate
=
'Feb 28, 2017'
,
endDate
=
'May 30, 2017'
,
setupView
=
function
(
data
,
isEnrolled
)
{
var
programData
=
$
.
extend
({},
data
);
programData
.
run_mode
s
[
0
].
is_enrolled
=
isEnrolled
;
programData
.
course_run
s
[
0
].
is_enrolled
=
isEnrolled
;
setFixtures
(
'<div class="course-card card"></div>'
);
courseCardModel
=
new
CourseCardModel
(
programData
);
view
=
new
CourseCardView
({
...
...
@@ -24,48 +26,49 @@ define([
validateCourseInfoDisplay
=
function
()
{
// DRY validation for course card in enrolled state
expect
(
view
.
$
(
'.header-img'
).
attr
(
'src'
)).
toEqual
(
co
ntext
.
run_modes
[
0
].
course_image_url
);
expect
(
view
.
$
(
'.course-details .course-title-link'
).
text
().
trim
()).
toEqual
(
co
ntext
.
display_nam
e
);
expect
(
view
.
$
(
'.header-img'
).
attr
(
'src'
)).
toEqual
(
co
urse
.
course_runs
[
0
].
image
.
src
);
expect
(
view
.
$
(
'.course-details .course-title-link'
).
text
().
trim
()).
toEqual
(
co
urse
.
titl
e
);
expect
(
view
.
$
(
'.course-details .course-title-link'
).
attr
(
'href'
)).
toEqual
(
co
ntext
.
run_mode
s
[
0
].
marketing_url
co
urse
.
course_run
s
[
0
].
marketing_url
);
expect
(
view
.
$
(
'.course-details .course-text .course-key'
).
html
()).
toEqual
(
co
ntext
.
key
);
expect
(
view
.
$
(
'.course-details .course-text .course-key'
).
html
()).
toEqual
(
co
urse
.
key
);
expect
(
view
.
$
(
'.course-details .course-text .run-period'
).
html
()).
toEqual
(
context
.
run_modes
[
0
].
start_date
+
' - '
+
context
.
run_modes
[
0
].
end_d
ate
startDate
+
' - '
+
endD
ate
);
};
beforeEach
(
function
()
{
// Redefine this data prior to each test case so that tests can't
// break each other by modifying data copied by reference.
context
=
{
course_modes
:
[],
display_name
:
'Astrophysics: Exploring Exoplanets'
,
key
:
'ANU-ASTRO1x'
,
organization
:
{
display_name
:
'Australian National University'
,
key
:
'ANUx'
// NOTE: This data is redefined prior to each test case so that tests
// can't break each other by modifying data copied by reference.
course
=
{
key
:
'WageningenX+FFESx'
,
uuid
:
'9f8562eb-f99b-45c7-b437-799fd0c15b6a'
,
title
:
'Systems thinking and environmental sustainability'
,
course_runs
:
[
{
key
:
'course-v1:WageningenX+FFESx+1T2017'
,
title
:
'Food Security and Sustainability: Systems thinking and environmental sustainability'
,
image
:
{
src
:
'https://example.com/9f8562eb-f99b-45c7-b437-799fd0c15b6a.jpg'
},
run_modes
:
[{
marketing_url
:
'https://www.edx.org/course/food-security-sustainability'
,
start
:
'2017-02-28T05:00:00Z'
,
end
:
'2017-05-30T23:00:00Z'
,
enrollment_start
:
'2017-01-18T00:00:00Z'
,
enrollment_end
:
null
,
type
:
'verified'
,
certificate_url
:
''
,
course_image_url
:
'http://test.com/image1'
,
course_key
:
'course-v1:ANUx+ANU-ASTRO1x+3T2015'
,
course_started
:
true
,
course_url
:
'https://courses.example.com/courses/course-v1:edX+DemoX+Demo_Course'
,
end_date
:
'Jun 13, 2019'
,
enrollment_open_date
:
'Apr 1, 2016'
,
course_url
:
'https://courses.example.com/courses/course-v1:WageningenX+FFESx+1T2017'
,
enrollment_open_date
:
'Jan 18, 2016'
,
is_course_ended
:
false
,
is_enrolled
:
true
,
is_enrollment_open
:
true
,
marketing_url
:
'https://www.example.com/marketing/site'
,
mode_slug
:
'verified'
,
run_key
:
'2T2016'
,
start_date
:
'Apr 25, 2016'
,
upgrade_url
:
''
}]
}
]
};
setupView
(
co
ntext
,
false
);
setupView
(
co
urse
,
false
);
});
afterEach
(
function
()
{
...
...
@@ -78,7 +81,7 @@ define([
it
(
'should render the course card based on the data enrolled'
,
function
()
{
view
.
remove
();
setupView
(
co
ntext
,
true
);
setupView
(
co
urse
,
true
);
validateCourseInfoDisplay
();
});
...
...
@@ -94,11 +97,11 @@ define([
});
it
(
'should show the course advertised start date'
,
function
()
{
var
advertisedStart
=
'
This is an advertised start
'
;
co
ntext
.
run_mode
s
[
0
].
advertised_start
=
advertisedStart
;
setupView
(
co
ntext
,
false
);
var
advertisedStart
=
'
A long time ago...
'
;
co
urse
.
course_run
s
[
0
].
advertised_start
=
advertisedStart
;
setupView
(
co
urse
,
false
);
expect
(
view
.
$
(
'.course-details .course-text .run-period'
).
html
()).
toEqual
(
advertisedStart
+
' - '
+
context
.
run_modes
[
0
].
end_d
ate
advertisedStart
+
' - '
+
endD
ate
);
});
...
...
@@ -108,8 +111,8 @@ define([
expect
(
view
.
$
(
'.certificate-status'
).
length
).
toEqual
(
0
);
view
.
remove
();
co
ntext
.
run_mode
s
[
0
].
certificate_url
=
certUrl
;
setupView
(
co
ntext
,
false
);
co
urse
.
course_run
s
[
0
].
certificate_url
=
certUrl
;
setupView
(
co
urse
,
false
);
expect
(
view
.
$
(
'.certificate-status'
).
length
).
toEqual
(
1
);
expect
(
view
.
$
(
'.certificate-status .cta-secondary'
).
attr
(
'href'
)).
toEqual
(
certUrl
);
});
...
...
@@ -120,53 +123,53 @@ define([
expect
(
view
.
$
(
'.upgrade-message'
).
length
).
toEqual
(
0
);
view
.
remove
();
co
ntext
.
run_mode
s
[
0
].
upgrade_url
=
upgradeUrl
;
setupView
(
co
ntext
,
false
);
co
urse
.
course_run
s
[
0
].
upgrade_url
=
upgradeUrl
;
setupView
(
co
urse
,
false
);
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
()
{
// Verify that no empty elements are left in the DOM.
co
ntext
.
run_mode
s
[
0
].
upgrade_url
=
''
;
co
ntext
.
run_mode
s
[
0
].
certificate_url
=
''
;
setupView
(
co
ntext
,
false
);
co
urse
.
course_run
s
[
0
].
upgrade_url
=
''
;
co
urse
.
course_run
s
[
0
].
certificate_url
=
''
;
setupView
(
co
urse
,
false
);
expect
(
view
.
$
(
'.upgrade-message'
).
length
).
toEqual
(
0
);
expect
(
view
.
$
(
'.certificate-status'
).
length
).
toEqual
(
0
);
view
.
remove
();
// Verify that the upgrade message takes priority.
co
ntext
.
run_mode
s
[
0
].
upgrade_url
=
'/path/to/upgrade'
;
co
ntext
.
run_mode
s
[
0
].
certificate_url
=
'/path/to/certificate'
;
setupView
(
co
ntext
,
false
);
co
urse
.
course_run
s
[
0
].
upgrade_url
=
'/path/to/upgrade'
;
co
urse
.
course_run
s
[
0
].
certificate_url
=
'/path/to/certificate'
;
setupView
(
co
urse
,
false
);
expect
(
view
.
$
(
'.upgrade-message'
).
length
).
toEqual
(
1
);
expect
(
view
.
$
(
'.certificate-status'
).
length
).
toEqual
(
0
);
});
it
(
'should show a message if an there is an upcoming course run'
,
function
()
{
co
ntext
.
run_mode
s
[
0
].
is_enrollment_open
=
false
;
co
urse
.
course_run
s
[
0
].
is_enrollment_open
=
false
;
setupView
(
co
ntext
,
false
);
setupView
(
co
urse
,
false
);
expect
(
view
.
$
(
'.header-img'
).
attr
(
'src'
)).
toEqual
(
co
ntext
.
run_modes
[
0
].
course_image_url
);
expect
(
view
.
$
(
'.course-details .course-title'
).
text
().
trim
()).
toEqual
(
co
ntext
.
display_nam
e
);
expect
(
view
.
$
(
'.course-details .course-text .course-key'
).
html
()).
toEqual
(
co
ntext
.
key
);
expect
(
view
.
$
(
'.header-img'
).
attr
(
'src'
)).
toEqual
(
co
urse
.
course_runs
[
0
].
image
.
src
);
expect
(
view
.
$
(
'.course-details .course-title'
).
text
().
trim
()).
toEqual
(
co
urse
.
titl
e
);
expect
(
view
.
$
(
'.course-details .course-text .course-key'
).
html
()).
toEqual
(
co
urse
.
key
);
expect
(
view
.
$
(
'.course-details .course-text .run-period'
).
length
).
toBe
(
0
);
expect
(
view
.
$
(
'.no-action-message'
).
text
().
trim
()).
toBe
(
'Coming Soon'
);
expect
(
view
.
$
(
'.enrollment-open-date'
).
text
().
trim
()).
toEqual
(
context
.
run_mode
s
[
0
].
enrollment_open_date
course
.
course_run
s
[
0
].
enrollment_open_date
);
});
it
(
'should show a message if there are no
known
upcoming course runs'
,
function
()
{
co
ntext
.
run_mode
s
[
0
].
is_enrollment_open
=
false
;
co
ntext
.
run_mode
s
[
0
].
is_course_ended
=
true
;
it
(
'should show a message if there are no upcoming course runs'
,
function
()
{
co
urse
.
course_run
s
[
0
].
is_enrollment_open
=
false
;
co
urse
.
course_run
s
[
0
].
is_course_ended
=
true
;
setupView
(
co
ntext
,
false
);
setupView
(
co
urse
,
false
);
expect
(
view
.
$
(
'.header-img'
).
attr
(
'src'
)).
toEqual
(
co
ntext
.
run_modes
[
0
].
course_image_url
);
expect
(
view
.
$
(
'.course-details .course-title'
).
text
().
trim
()).
toEqual
(
co
ntext
.
display_nam
e
);
expect
(
view
.
$
(
'.course-details .course-text .course-key'
).
html
()).
toEqual
(
co
ntext
.
key
);
expect
(
view
.
$
(
'.header-img'
).
attr
(
'src'
)).
toEqual
(
co
urse
.
course_runs
[
0
].
image
.
src
);
expect
(
view
.
$
(
'.course-details .course-title'
).
text
().
trim
()).
toEqual
(
co
urse
.
titl
e
);
expect
(
view
.
$
(
'.course-details .course-text .course-key'
).
html
()).
toEqual
(
co
urse
.
key
);
expect
(
view
.
$
(
'.course-details .course-text .run-period'
).
length
).
toBe
(
0
);
expect
(
view
.
$
(
'.no-action-message'
).
text
().
trim
()).
toBe
(
'Not Currently Available'
);
expect
(
view
.
$
(
'.enrollment-opens'
).
length
).
toEqual
(
0
);
...
...
@@ -174,23 +177,23 @@ define([
it
(
'should link to the marketing site when a URL is available'
,
function
()
{
$
.
each
([
'.course-image-link'
,
'.course-title-link'
],
function
(
index
,
selector
)
{
expect
(
view
.
$
(
selector
).
attr
(
'href'
)).
toEqual
(
co
ntext
.
run_mode
s
[
0
].
marketing_url
);
expect
(
view
.
$
(
selector
).
attr
(
'href'
)).
toEqual
(
co
urse
.
course_run
s
[
0
].
marketing_url
);
});
});
it
(
'should link to the course home when no marketing URL is available'
,
function
()
{
co
ntext
.
run_mode
s
[
0
].
marketing_url
=
null
;
setupView
(
co
ntext
,
false
);
co
urse
.
course_run
s
[
0
].
marketing_url
=
null
;
setupView
(
co
urse
,
false
);
$
.
each
([
'.course-image-link'
,
'.course-title-link'
],
function
(
index
,
selector
)
{
expect
(
view
.
$
(
selector
).
attr
(
'href'
)).
toEqual
(
co
ntext
.
run_mode
s
[
0
].
course_url
);
expect
(
view
.
$
(
selector
).
attr
(
'href'
)).
toEqual
(
co
urse
.
course_run
s
[
0
].
course_url
);
});
});
it
(
'should not link to the marketing site or the course home if neither URL is available'
,
function
()
{
co
ntext
.
run_mode
s
[
0
].
marketing_url
=
null
;
co
ntext
.
run_mode
s
[
0
].
course_url
=
null
;
setupView
(
co
ntext
,
false
);
co
urse
.
course_run
s
[
0
].
marketing_url
=
null
;
co
urse
.
course_run
s
[
0
].
course_url
=
null
;
setupView
(
co
urse
,
false
);
$
.
each
([
'.course-image-link'
,
'.course-title-link'
],
function
(
index
,
selector
)
{
expect
(
view
.
$
(
selector
).
length
).
toEqual
(
0
);
...
...
lms/static/js/spec/learner_dashboard/course_enroll_view_spec.js
View file @
dda0e03f
...
...
@@ -13,75 +13,102 @@ define([
courseEnrollModel
,
urlModel
,
setupView
,
singleRunModeList
,
multiRunModeList
,
context
=
{
display_name
:
'Edx Demo course'
,
key
:
'edX+DemoX+Demo_Course'
,
organization
:
{
display_name
:
'edx.org'
,
key
:
'edX'
singleCourseRunList
,
multiCourseRunList
,
course
=
{
key
:
'WageningenX+FFESx'
,
uuid
:
'9f8562eb-f99b-45c7-b437-799fd0c15b6a'
,
title
:
'Systems thinking and environmental sustainability'
,
owners
:
[
{
uuid
:
'0c6e5fa2-96e8-40b2-9ebe-c8b0df2a3b22'
,
key
:
'WageningenX'
,
name
:
'Wageningen University & Research'
}
]
},
urls
=
{
dashboard_url
:
'/dashboard'
,
id_verification_url
:
'/verify_student/start_flow/'
,
commerce_api_url
:
'/commerce'
,
track_selection_url
:
'/select_track/course/'
};
beforeEach
(
function
()
{
// Redefine this data prior to each test case so that tests can't
// break each other by modifying data copied by reference.
singleRunModeList
=
[{
start_date
:
'Apr 25, 2016'
,
end_date
:
'Jun 13, 2016'
,
course_key
:
'course-v1:course-v1:edX+DemoX+Demo_Course'
,
course_url
:
'http://localhost:8000/courses/course-v1:edX+DemoX+Demo_Course/info'
,
course_image_url
:
'http://test.com/image1'
,
marketing_url
:
'http://test.com/image2'
,
// NOTE: This data is redefined prior to each test case so that tests
// can't break each other by modifying data copied by reference.
singleCourseRunList
=
[{
key
:
'course-v1:WageningenX+FFESx+1T2017'
,
uuid
:
'2f2edf03-79e6-4e39-aef0-65436a6ee344'
,
title
:
'Food Security and Sustainability: Systems thinking and environmental sustainability'
,
image
:
{
src
:
'https://example.com/2f2edf03-79e6-4e39-aef0-65436a6ee344.jpg'
},
marketing_url
:
'https://www.edx.org/course/food-security-sustainability-systems-wageningenx-ffesx'
,
start
:
'2017-02-28T05:00:00Z'
,
end
:
'2017-05-30T23:00:00Z'
,
enrollment_start
:
'2017-01-18T00:00:00Z'
,
enrollment_end
:
null
,
type
:
'verified'
,
certificate_url
:
''
,
course_url
:
'https://courses.example.com/courses/course-v1:edX+DemoX+Demo_Course'
,
enrollment_open_date
:
'Jan 18, 2016'
,
is_course_ended
:
false
,
mode_slug
:
'audit'
,
run_key
:
'2T2016'
,
is_enrolled
:
false
,
is_enrollment_open
:
true
is_enrollment_open
:
true
,
upgrade_url
:
''
}];
multiRunModeList
=
[{
start_date
:
'May 21, 2015'
,
end_date
:
'Sep 21, 2015'
,
course_key
:
'course-v1:course-v1:edX+DemoX+Demo_Course'
,
course_url
:
'http://localhost:8000/courses/course-v1:edX+DemoX+Demo_Course/info'
,
course_image_url
:
'http://test.com/run_2_image_1'
,
marketing_url
:
'http://test.com/run_2_image_2'
,
mode_slug
:
'verified'
,
multiCourseRunList
=
[{
key
:
'course-v1:WageningenX+FFESx+2T2016'
,
uuid
:
'9bbb7844-4848-44ab-8e20-0be6604886e9'
,
title
:
'Food Security and Sustainability: Systems thinking and environmental sustainability'
,
image
:
{
src
:
'https://example.com/9bbb7844-4848-44ab-8e20-0be6604886e9.jpg'
},
short_description
:
'Learn how to apply systems thinking to improve food production systems.'
,
marketing_url
:
'https://www.edx.org/course/food-security-sustainability-systems-wageningenx-stesx'
,
start
:
'2016-09-08T04:00:00Z'
,
end
:
'2016-11-11T00:00:00Z'
,
enrollment_start
:
null
,
enrollment_end
:
null
,
pacing_type
:
'instructor_paced'
,
type
:
'verified'
,
certificate_url
:
''
,
course_url
:
'https://courses.example.com/courses/course-v1:WageningenX+FFESx+2T2016'
,
enrollment_open_date
:
'Jan 18, 2016'
,
is_course_ended
:
false
,
run_key
:
'1T2015'
,
is_enrolled
:
false
,
is_enrollment_open
:
true
},
{
start_date
:
'Sep 22, 2015'
,
end_date
:
'Dec 28, 2015'
,
course_key
:
'course-v1:course-v1:edX+DemoX+Demo_Course'
,
course_url
:
'http://localhost:8000/courses/course-v1:edX+DemoX+Demo_Course/info'
,
course_image_url
:
'http://test.com/run_3_image_1'
,
marketing_url
:
'http://test.com/run_3_image_2'
,
key
:
'course-v1:WageningenX+FFESx+1T2017'
,
uuid
:
'2f2edf03-79e6-4e39-aef0-65436a6ee344'
,
title
:
'Food Security and Sustainability: Systems thinking and environmental sustainability'
,
image
:
{
src
:
'https://example.com/2f2edf03-79e6-4e39-aef0-65436a6ee344.jpg'
},
marketing_url
:
'https://www.edx.org/course/food-security-sustainability-systems-wageningenx-ffesx'
,
start
:
'2017-02-28T05:00:00Z'
,
end
:
'2017-05-30T23:00:00Z'
,
enrollment_start
:
'2017-01-18T00:00:00Z'
,
enrollment_end
:
null
,
type
:
'verified'
,
certificate_url
:
''
,
course_url
:
'https://courses.example.com/courses/course-v1:WageningenX+FFESx+1T2017'
,
enrollment_open_date
:
'Jan 18, 2016'
,
is_course_ended
:
false
,
mode_slug
:
'verified'
,
run_key
:
'2T2015'
,
is_enrolled
:
false
,
is_enrollment_open
:
true
}];
});
setupView
=
function
(
runModes
,
urls
)
{
co
ntext
.
run_modes
=
runMode
s
;
setupView
=
function
(
courseRuns
,
urlMap
)
{
co
urse
.
course_runs
=
courseRun
s
;
setFixtures
(
'<div class="course-actions"></div>'
);
courseCardModel
=
new
CourseCardModel
(
co
ntext
);
courseCardModel
=
new
CourseCardModel
(
co
urse
);
courseEnrollModel
=
new
CourseEnrollModel
({},
{
courseId
:
courseCardModel
.
get
(
'course_key'
)
courseId
:
courseCardModel
.
get
(
'course_
run_
key'
)
});
if
(
url
s
)
{
urlModel
=
new
Backbone
.
Model
(
url
s
);
if
(
url
Map
)
{
urlModel
=
new
Backbone
.
Model
(
url
Map
);
}
view
=
new
CourseEnrollView
({
$parentEl
:
$
(
'.course-actions'
),
...
...
@@ -99,143 +126,183 @@ define([
});
it
(
'should exist'
,
function
()
{
setupView
(
single
RunMode
List
);
setupView
(
single
CourseRun
List
);
expect
(
view
).
toBeDefined
();
});
it
(
'should render the course enroll view based on not enrolled data'
,
function
()
{
setupView
(
singleRunModeList
);
expect
(
view
.
$
(
'.enrollment-info'
).
html
().
trim
()).
toEqual
(
'not enrolled'
);
it
(
'should render the course enroll view when not enrolled'
,
function
()
{
setupView
(
singleCourseRunList
);
expect
(
view
.
$
(
'.enrollment-info'
).
html
().
trim
()).
toEqual
(
'Not Enrolled'
);
expect
(
view
.
$
(
'.enroll-button'
).
text
().
trim
()).
toEqual
(
'Enroll Now'
);
expect
(
view
.
$
(
'.run-select'
).
length
).
toBe
(
0
);
});
it
(
'should render the course enroll view
based on enrolled data
'
,
function
()
{
single
RunMode
List
[
0
].
is_enrolled
=
true
;
it
(
'should render the course enroll view
when enrolled
'
,
function
()
{
single
CourseRun
List
[
0
].
is_enrolled
=
true
;
setupView
(
single
RunMode
List
);
setupView
(
single
CourseRun
List
);
expect
(
view
.
$
(
'.enrollment-info'
).
html
().
trim
()).
toEqual
(
'enrolled'
);
expect
(
view
.
$
(
'.view-course-link'
).
attr
(
'href'
)).
toEqual
(
co
ntext
.
run_mode
s
[
0
].
course_url
);
expect
(
view
.
$
(
'.view-course-link'
).
attr
(
'href'
)).
toEqual
(
co
urse
.
course_run
s
[
0
].
course_url
);
expect
(
view
.
$
(
'.view-course-link'
).
text
().
trim
()).
toEqual
(
'View Course'
);
expect
(
view
.
$
(
'.run-select'
).
length
).
toBe
(
0
);
});
it
(
'should allow the learner to view an archived course'
,
function
()
{
// Regression test for ECOM-4974.
single
RunMode
List
[
0
].
is_enrolled
=
true
;
single
RunMode
List
[
0
].
is_enrollment_open
=
false
;
single
RunMode
List
[
0
].
is_course_ended
=
true
;
single
CourseRun
List
[
0
].
is_enrolled
=
true
;
single
CourseRun
List
[
0
].
is_enrollment_open
=
false
;
single
CourseRun
List
[
0
].
is_course_ended
=
true
;
setupView
(
single
RunMode
List
);
setupView
(
single
CourseRun
List
);
expect
(
view
.
$
(
'.view-course-link'
).
text
().
trim
()).
toEqual
(
'View Archived Course'
);
});
it
(
'should not render anything if
run modes is
empty'
,
function
()
{
it
(
'should not render anything if
course runs are
empty'
,
function
()
{
setupView
([]);
expect
(
view
.
$
(
'.enrollment-info'
).
length
).
toBe
(
0
);
expect
(
view
.
$
(
'.run-select'
).
length
).
toBe
(
0
);
expect
(
view
.
$
(
'.enroll-button'
).
length
).
toBe
(
0
);
});
it
(
'should render run selection drop down if mulitple run available'
,
function
()
{
setupView
(
multiRunModeList
);
it
(
'should render run selection dropdown if multiple course runs are available'
,
function
()
{
setupView
(
multiCourseRunList
);
expect
(
view
.
$
(
'.run-select'
).
length
).
toBe
(
1
);
expect
(
view
.
$
(
'.run-select'
).
val
()).
toEqual
(
''
);
expect
(
view
.
$
(
'.run-select option'
).
length
).
toBe
(
3
);
});
it
(
'should switch run context if dropdown selection changed'
,
function
()
{
setupView
(
multiRunModeList
);
spyOn
(
courseCardModel
,
'updateRun'
).
and
.
callThrough
();
it
(
'should switch course run context if an option is selected from the dropdown'
,
function
()
{
setupView
(
multiCourseRunList
);
spyOn
(
courseCardModel
,
'updateCourseRun'
).
and
.
callThrough
();
expect
(
view
.
$
(
'.run-select'
).
val
()).
toEqual
(
''
);
view
.
$
(
'.run-select'
).
val
(
multiRunModeList
[
1
].
run_key
);
view
.
$
(
'.run-select'
).
val
(
multiCourseRunList
[
1
].
key
);
view
.
$
(
'.run-select'
).
trigger
(
'change'
);
expect
(
view
.
$
(
'.run-select'
).
val
()).
toEqual
(
multiRunModeList
[
1
].
run_key
);
expect
(
courseCardModel
.
updateRun
)
.
toHaveBeenCalledWith
(
multiRunModeList
[
1
].
run_key
);
expect
(
courseCardModel
.
get
(
'run_key'
)).
toEqual
(
multiRunModeList
[
1
].
run_key
);
expect
(
view
.
$
(
'.run-select'
).
val
()).
toEqual
(
multiCourseRunList
[
1
].
key
);
expect
(
courseCardModel
.
updateCourseRun
)
.
toHaveBeenCalledWith
(
multiCourseRunList
[
1
].
key
);
expect
(
courseCardModel
.
get
(
'course_key'
)).
toEqual
(
course
.
key
);
});
it
(
'should enroll learner when enroll button clicked'
,
function
()
{
setupView
(
singleRunModeList
);
it
(
'should enroll learner when enroll button is clicked with one course run available'
,
function
()
{
setupView
(
singleCourseRunList
);
expect
(
view
.
$
(
'.enroll-button'
).
length
).
toBe
(
1
);
spyOn
(
courseEnrollModel
,
'save'
);
view
.
$
(
'.enroll-button'
).
click
();
expect
(
courseEnrollModel
.
save
).
toHaveBeenCalled
();
});
it
(
'should enroll learner into the updated run with button click'
,
function
()
{
setupView
(
multiRunModeList
);
it
(
'should enroll learner when enroll button is clicked with multiple course runs available'
,
function
()
{
setupView
(
multiCourseRunList
);
spyOn
(
courseEnrollModel
,
'save'
);
view
.
$
(
'.run-select'
).
val
(
multiRunModeList
[
1
].
run_key
);
view
.
$
(
'.run-select'
).
val
(
multiCourseRunList
[
1
].
key
);
view
.
$
(
'.run-select'
).
trigger
(
'change'
);
view
.
$
(
'.enroll-button'
).
click
();
expect
(
courseEnrollModel
.
save
).
toHaveBeenCalled
();
});
it
(
'should redirect to trackSelectionUrl when enrollment success for audit track'
,
function
()
{
singleRunModeList
[
0
].
is_enrolled
=
false
;
singleRunModeList
[
0
].
mode_slug
=
'audit'
;
setupView
(
singleRunModeList
,
urls
);
it
(
'should redirect to track selection when audit enrollment succeeds'
,
function
()
{
singleCourseRunList
[
0
].
is_enrolled
=
false
;
singleCourseRunList
[
0
].
mode_slug
=
'audit'
;
setupView
(
singleCourseRunList
,
urls
);
expect
(
view
.
$
(
'.enroll-button'
).
length
).
toBe
(
1
);
expect
(
view
.
trackSelectionUrl
).
toBeDefined
();
spyOn
(
view
,
'redirect'
);
view
.
enrollSuccess
();
expect
(
view
.
redirect
).
toHaveBeenCalledWith
(
view
.
trackSelectionUrl
+
courseCardModel
.
get
(
'course
_key'
));
view
.
trackSelectionUrl
+
courseCardModel
.
get
(
'course_run
_key'
));
});
it
(
'should redirect to track selection when enrollment in an unspecified mode is attempted'
,
function
()
{
singleCourseRunList
[
0
].
is_enrolled
=
false
;
singleCourseRunList
[
0
].
mode_slug
=
null
;
setupView
(
singleCourseRunList
,
urls
);
it
(
'should redirect when enrollment success for no track'
,
function
()
{
singleRunModeList
[
0
].
is_enrolled
=
false
;
singleRunModeList
[
0
].
mode_slug
=
null
;
setupView
(
singleRunModeList
,
urls
);
expect
(
view
.
$
(
'.enroll-button'
).
length
).
toBe
(
1
);
expect
(
view
.
trackSelectionUrl
).
toBeDefined
();
spyOn
(
view
,
'redirect'
);
view
.
enrollSuccess
();
expect
(
view
.
redirect
).
toHaveBeenCalledWith
(
view
.
trackSelectionUrl
+
courseCardModel
.
get
(
'course_key'
));
view
.
trackSelectionUrl
+
courseCardModel
.
get
(
'course_run_key'
)
);
});
it
(
'should not redirect when urls not provided'
,
function
()
{
singleRunModeList
[
0
].
is_enrolled
=
false
;
singleRunModeList
[
0
].
mode_slug
=
'verified'
;
setupView
(
singleRunModeList
);
it
(
'should not redirect when urls are not provided'
,
function
()
{
singleCourseRunList
[
0
].
is_enrolled
=
false
;
singleCourseRunList
[
0
].
mode_slug
=
'verified'
;
setupView
(
singleCourseRunList
);
expect
(
view
.
$
(
'.enroll-button'
).
length
).
toBe
(
1
);
expect
(
view
.
verificationUrl
).
not
.
toBeDefined
();
expect
(
view
.
dashboardUrl
).
not
.
toBeDefined
();
expect
(
view
.
trackSelectionUrl
).
not
.
toBeDefined
();
spyOn
(
view
,
'redirect'
);
view
.
enrollSuccess
();
expect
(
view
.
redirect
).
not
.
toHaveBeenCalled
();
});
it
(
'should redirect to track selection on error'
,
function
()
{
setupView
(
singleRunModeList
,
urls
);
setupView
(
singleCourseRunList
,
urls
);
expect
(
view
.
$
(
'.enroll-button'
).
length
).
toBe
(
1
);
expect
(
view
.
trackSelectionUrl
).
toBeDefined
();
spyOn
(
view
,
'redirect'
);
view
.
enrollError
(
courseEnrollModel
,
{
status
:
500
});
expect
(
view
.
redirect
).
toHaveBeenCalledWith
(
view
.
trackSelectionUrl
+
courseCardModel
.
get
(
'course_key'
));
view
.
trackSelectionUrl
+
courseCardModel
.
get
(
'course_run_key'
)
);
});
it
(
'should redirect to login on 403 error'
,
function
()
{
var
response
=
{
status
:
403
,
responseJSON
:
{
user_message_url
:
'test_url/haha'
}};
setupView
(
singleRunModeList
,
urls
);
user_message_url
:
'redirect/to/this'
}
};
setupView
(
singleCourseRunList
,
urls
);
expect
(
view
.
$
(
'.enroll-button'
).
length
).
toBe
(
1
);
expect
(
view
.
trackSelectionUrl
).
toBeDefined
();
spyOn
(
view
,
'redirect'
);
view
.
enrollError
(
courseEnrollModel
,
response
);
expect
(
view
.
redirect
).
toHaveBeenCalledWith
(
response
.
responseJSON
.
user_message_url
);
response
.
responseJSON
.
user_message_url
);
});
});
}
...
...
lms/static/js/spec/learner_dashboard/program_card_view_spec.js
View file @
dda0e03f
define
([
'backbone'
,
'underscore'
,
'jquery'
,
'js/learner_dashboard/collections/program_progress_collection'
,
'js/learner_dashboard/models/program_model'
,
'js/learner_dashboard/views/program_card_view'
],
function
(
Backbone
,
$
,
ProgressCollection
,
ProgramModel
,
ProgramCardView
)
{
],
function
(
Backbone
,
_
,
$
,
ProgressCollection
,
ProgramModel
,
ProgramCardView
)
{
'use strict'
;
/* jslint maxlen: 500 */
...
...
@@ -12,47 +13,61 @@ define([
var
view
=
null
,
programModel
,
program
=
{
category
:
'FooBar'
,
status
:
'active'
,
subtitle
:
'program 1'
,
name
:
'test program 1'
,
organizations
:
[
{
display_name
:
'edX'
,
key
:
'edx'
uuid
:
'a87e5eac-3c93-45a1-a8e1-4c79ca8401c8'
,
title
:
'Food Security and Sustainability'
,
subtitle
:
'Learn how to feed all people in the world in a sustainable way.'
,
type
:
'XSeries'
,
detail_url
:
'https://www.edx.org/foo/bar'
,
banner_image
:
{
medium
:
{
height
:
242
,
width
:
726
,
url
:
'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.medium.jpg'
},
'x-small'
:
{
height
:
116
,
width
:
348
,
url
:
'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.x-small.jpg'
},
small
:
{
height
:
145
,
width
:
435
,
url
:
'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.small.jpg'
},
large
:
{
height
:
480
,
width
:
1440
,
url
:
'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.large.jpg'
}
],
created
:
'2016-03-03T19:18:50.061136Z'
,
modified
:
'2016-03-25T13:45:21.220732Z'
,
marketing_slug
:
'p_2?param=haha&test=b'
,
id
:
146
,
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'
,
w726h242
:
'http://www.edx.org/images/test3'
},
authoring_organizations
:
[
{
uuid
:
'0c6e5fa2-96e8-40b2-9ebe-c8b0df2a3b22'
,
key
:
'WageningenX'
,
name
:
'Wageningen University & Research'
}
]
},
userProgress
=
[
{
id
:
146
,
completed
:
[
'courses'
,
'the'
,
'user'
,
'completed'
]
,
in_progress
:
[
'in'
,
'progress'
]
,
not_started
:
[
'courses'
,
'not'
,
'yet'
,
'started'
]
uuid
:
'a87e5eac-3c93-45a1-a8e1-4c79ca8401c8'
,
completed
:
4
,
in_progress
:
2
,
not_started
:
4
},
{
id
:
147
,
completed
:
[
'Course 1'
]
,
in_progress
:
[]
,
not_started
:
[
'Course 2'
,
'Course 3'
,
'Course 4'
]
uuid
:
'91d144d2-1bb1-4afe-90df-d5cff63fa6e2'
,
completed
:
1
,
in_progress
:
0
,
not_started
:
3
}
],
progressCollection
=
new
ProgressCollection
(),
cardRenders
=
function
(
$card
)
{
expect
(
$card
).
toBeDefined
();
expect
(
$card
.
find
(
'.title'
).
html
().
trim
()).
toEqual
(
program
.
nam
e
);
expect
(
$card
.
find
(
'.category span'
).
html
().
trim
()).
toEqual
(
program
.
category
);
expect
(
$card
.
find
(
'.organization'
).
html
().
trim
()).
toEqual
(
program
.
organizations
[
0
].
key
);
expect
(
$card
.
find
(
'.title'
).
html
().
trim
()).
toEqual
(
program
.
titl
e
);
expect
(
$card
.
find
(
'.category span'
).
html
().
trim
()).
toEqual
(
program
.
type
);
expect
(
$card
.
find
(
'.organization'
).
html
().
trim
()).
toEqual
(
program
.
authoring_
organizations
[
0
].
key
);
expect
(
$card
.
find
(
'.card-link'
).
attr
(
'href'
)).
toEqual
(
program
.
detail_url
);
};
...
...
@@ -87,12 +102,16 @@ define([
});
it
(
'should handle exceptions from reEvaluatePicture'
,
function
()
{
var
message
=
'Picturefill had exceptions'
;
spyOn
(
view
,
'reEvaluatePicture'
).
and
.
callFake
(
function
()
{
throw
{
name
:
'Picturefill had exceptions'
};
var
error
=
{
name
:
message
};
throw
error
;
});
view
.
reLoadBannerImage
();
expect
(
view
.
reEvaluatePicture
).
toHaveBeenCalled
();
expect
(
view
.
reLoadBannerImage
).
not
.
toThrow
(
'Picturefill had exceptions'
);
expect
(
view
.
reLoadBannerImage
).
not
.
toThrow
(
message
);
});
it
(
'should calculate the correct percentages for progress bars'
,
function
()
{
...
...
@@ -101,11 +120,12 @@ define([
});
it
(
'should display the correct completed courses message'
,
function
()
{
var
program
=
_
.
findWhere
(
userProgress
,
{
id
:
146
}),
completed
=
program
.
completed
.
length
,
total
=
completed
+
program
.
in_progress
.
length
+
program
.
not_started
.
length
;
var
program
Progress
=
_
.
findWhere
(
userProgress
,
{
uuid
:
'a87e5eac-3c93-45a1-a8e1-4c79ca8401c8'
}),
completed
=
program
Progress
.
completed
,
total
=
completed
+
program
Progress
.
in_progress
+
programProgress
.
not_started
;
expect
(
view
.
$
(
'.certificate-status .status-text'
).
not
(
'.secondary'
).
html
()).
toEqual
(
'You have earned certificates in '
+
completed
+
' of the '
+
total
+
' courses so far.'
);
expect
(
view
.
$
(
'.certificate-status .status-text'
).
not
(
'.secondary'
).
html
())
.
toEqual
(
'You have earned certificates in '
+
completed
+
' of the '
+
total
+
' courses so far.'
);
});
it
(
'should render cards if there is no progressData'
,
function
()
{
...
...
lms/static/js/spec/learner_dashboard/program_details_header_spec.js
View file @
dda0e03f
...
...
@@ -12,23 +12,42 @@ define([
program_listing_url
:
'/dashboard/programs'
},
programData
:
{
uuid
:
'12-ab'
,
name
:
'Astrophysics'
,
subtitle
:
'Learn contemporary astrophysics from the leaders in the field.'
,
category
:
'xseries'
,
organizations
:
[
{
display_name
:
'Australian National University'
,
img
:
'common/test/data/static/picture1.jpg'
,
key
:
'ANUx'
uuid
:
'a87e5eac-3c93-45a1-a8e1-4c79ca8401c8'
,
title
:
'Food Security and Sustainability'
,
subtitle
:
'Learn how to feed all people in the world in a sustainable way.'
,
type
:
'XSeries'
,
detail_url
:
'https://www.edx.org/foo/bar'
,
banner_image
:
{
medium
:
{
height
:
242
,
width
:
726
,
url
:
'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.medium.jpg'
},
'x-small'
:
{
height
:
116
,
width
:
348
,
url
:
'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.x-small.jpg'
},
small
:
{
height
:
145
,
width
:
435
,
url
:
'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.small.jpg'
},
large
:
{
height
:
480
,
width
:
1440
,
url
:
'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.large.jpg'
}
],
banner_image_urls
:
{
w1440h480
:
'common/test/data/static/picture1.jpg'
,
w726h242
:
'common/test/data/static/picture2.jpg'
,
w348h116
:
'common/test/data/static/picture3.jpg'
},
program_details_url
:
'/dashboard/programs'
authoring_organizations
:
[
{
uuid
:
'0c6e5fa2-96e8-40b2-9ebe-c8b0df2a3b22'
,
key
:
'WageningenX'
,
name
:
'Wageningen University & Research'
,
certificate_logo_image_url
:
'https://example.com/org-certificate-logo.jpg'
,
logo_image_url
:
'https://example.com/org-logo.jpg'
}
]
}
};
...
...
@@ -51,13 +70,13 @@ define([
it
(
'should render the header based on the passed in model'
,
function
()
{
var
programListUrl
=
view
.
$
(
'.breadcrumb-list .crumb:nth-of-type(2) .crumb-link'
).
attr
(
'href'
);
expect
(
view
.
$
(
'.title'
).
html
()).
toEqual
(
context
.
programData
.
nam
e
);
expect
(
view
.
$
(
'.title'
).
html
()).
toEqual
(
context
.
programData
.
titl
e
);
expect
(
view
.
$
(
'.subtitle'
).
html
()).
toEqual
(
context
.
programData
.
subtitle
);
expect
(
view
.
$
(
'.org-logo'
).
length
).
toEqual
(
context
.
programData
.
organizations
.
length
);
expect
(
view
.
$
(
'.org-logo'
).
attr
(
'src'
))
.
toEqual
(
context
.
programData
.
organizations
[
0
].
img
);
expect
(
view
.
$
(
'.org-logo'
).
attr
(
'alt'
)).
toEqual
(
context
.
programData
.
organizations
[
0
].
display_name
+
'
\'
s logo'
);
expect
(
view
.
$
(
'.org-logo'
).
length
).
toEqual
(
context
.
programData
.
authoring_
organizations
.
length
);
expect
(
view
.
$
(
'.org-logo'
).
attr
(
'src'
))
.
toEqual
(
context
.
programData
.
authoring_organizations
[
0
].
certificate_logo_image_url
);
expect
(
view
.
$
(
'.org-logo'
).
attr
(
'alt'
))
.
toEqual
(
context
.
programData
.
authoring_organizations
[
0
].
name
+
'
\'
s logo'
);
expect
(
programListUrl
).
toEqual
(
context
.
urls
.
program_listing_url
);
});
});
...
...
lms/templates/dashboard.html
View file @
dda0e03f
...
...
@@ -98,7 +98,7 @@ from openedx.core.djangolib.markup import HTML, Text
<
%
is_course_blocked =
(enrollment.course_id
in
block_courses
)
%
>
<
%
course_verification_status =
verification_status_by_course.get(enrollment.course_id,
{})
%
>
<
%
course_requirements =
courses_requirements_not_met.get(enrollment.course_id)
%
>
<
%
related_programs =
programs_by_run
.get(unicode(enrollment.course_id))
%
>
<
%
related_programs =
inverted_programs
.get(unicode(enrollment.course_id))
%
>
<
%
include
file=
'dashboard/_dashboard_course_listing.html'
args=
'course_overview=enrollment.course_overview, enrollment=enrollment, show_courseware_link=show_courseware_link, cert_status=cert_status, can_unenroll=can_unenroll, credit_status=credit_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, show_refund_option=show_refund_option, is_paid_course=is_paid_course, is_course_blocked=is_course_blocked, verification_status=course_verification_status, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings, user=user, related_programs=related_programs, display_course_modes_on_dashboard=display_course_modes_on_dashboard'
/>
% endfor
...
...
lms/templates/dashboard/_dashboard_course_listing.html
View file @
dda0e03f
...
...
@@ -300,8 +300,8 @@ from student.helpers import (
<ul>
% for program in related_programs:
<li>
<span
class=
"category-icon ${program['
category
'].lower()}-icon"
aria-hidden=
"true"
></span>
<span><a
href=
"${program['detail_url']}"
>
${u'{
name} {category}'.format(name=program['name'], category=program['category
'])}
</a></span>
<span
class=
"category-icon ${program['
type
'].lower()}-icon"
aria-hidden=
"true"
></span>
<span><a
href=
"${program['detail_url']}"
>
${u'{
title} {type}'.format(title=program['title'], type=program['type
'])}
</a></span>
</li>
% endfor
</ul>
...
...
@@ -397,12 +397,6 @@ from student.helpers import (
</div>
%endif
% if course_program_info and course_program_info.get('category'):
%for program_data in course_program_info.get('course_program_list', []):
<
%
include
file =
"_dashboard_program_info.html"
args=
"program_data=program_data, enrollment_mode=enrollment.mode, category=course_program_info['category']"
/>
%endfor
% endif
% if is_course_blocked:
<p
id=
"block-course-msg"
class=
"course-block"
>
${Text(_("You can no longer access this course because payment has not yet been received. "
...
...
lms/templates/learner_dashboard/course_card.underscore
View file @
dda0e03f
...
...
@@ -7,7 +7,7 @@
class="header-img"
src="<%- course_image_url %>"
<% // safe-lint: disable=underscore-not-escaped %>
alt="<%= interpolate(gettext('%(courseName)s Home Page.'), {courseName:
display_nam
e}, true) %>"/>
alt="<%= interpolate(gettext('%(courseName)s Home Page.'), {courseName:
titl
e}, true) %>"/>
</a>
<% } else { %>
<img class="header-img" src="<%- course_image_url %>" alt=""/>
...
...
@@ -18,10 +18,10 @@
<h3 class="course-title">
<% if ( marketing_url || course_url ) { %>
<a href="<%- marketing_url || course_url %>" class="course-title-link">
<%-
display_nam
e %>
<%-
titl
e %>
</a>
<% } else { %>
<%-
display_nam
e %>
<%-
titl
e %>
<% } %>
</h3>
<div class="course-text">
...
...
@@ -29,7 +29,7 @@
<span class="run-period"><%- start_date %> - <%- end_date %></span>
-
<% } %>
<span class="course-key"><%- key %></span>
<span class="course-key"><%-
course_
key %></span>
</div>
</div>
</div>
...
...
lms/templates/learner_dashboard/course_enroll.underscore
View file @
dda0e03f
...
...
@@ -10,9 +10,9 @@
</a>
<% } %>
<% } else { %>
<% if (enrollable_
run_mode
s.length > 0) { %>
<div class="enrollment-info"><%- gettext('
not e
nrolled') %></div>
<% if (enrollable_
run_mode
s.length > 1) { %>
<% if (enrollable_
course_run
s.length > 0) { %>
<div class="enrollment-info"><%- gettext('
Not E
nrolled') %></div>
<% if (enrollable_
course_run
s.length > 1) { %>
<div class="run-select-container">
<div class="select-error">
<%- gettext('Please select a course date') %>
...
...
@@ -24,16 +24,16 @@
<option value="" selected="selected">
<%- gettext('Choose Course Date') %>
</option>
<% _.each (enrollable_
run_modes, function(runMode
) { %>
<% _.each (enrollable_
course_runs, function(courseRun
) { %>
<option
value="<%-
runMode.run_
key %>"
<% if (
run_key === runMode.run_
key) { %>
value="<%-
courseRun.
key %>"
<% if (
key === courseRun.
key) { %>
selected="selected"
<% }%>
>
<%= interpolate(
gettext('Starts %(start)s'),
{ start:
runMode
.start_date },
{ start:
courseRun
.start_date },
true)
%>
</option>
...
...
@@ -44,14 +44,14 @@
<button type="button" class="btn-brand btn cta-primary enroll-button">
<%- gettext('Enroll Now') %>
</button>
<% } else if (upcoming_
run_mode
s.length > 0) {%>
<% } else if (upcoming_
course_run
s.length > 0) {%>
<div class="no-action-message">
<%- gettext('Coming Soon') %>
</div>
<div class="enrollment-opens">
<%- gettext('Enrollment Opens on') %>
<span class="enrollment-open-date">
<%- upcoming_
run_mode
s[0].enrollment_open_date %>
<%- upcoming_
course_run
s[0].enrollment_open_date %>
</span>
</div>
<% } else { %>
...
...
lms/templates/learner_dashboard/program_card.underscore
View file @
dda0e03f
<div class="text-section">
<h3 id="program-<%-
id %>" class="title hd-3"><%- gettext(nam
e) %></h3>
<h3 id="program-<%-
uuid %>" class="title hd-3"><%- gettext(titl
e) %></h3>
<div class="meta-info grid-container">
<div class="organization col"><%- orgList %></div>
<div class="category col col-last">
<span class="category-text"><%- gettext(
category
) %></span>
<span class="category-icon <%-
category
.toLowerCase() %>-icon" aria-hidden="true"></span>
<span class="category-text"><%- gettext(
type
) %></span>
<span class="category-icon <%-
type
.toLowerCase() %>-icon" aria-hidden="true"></span>
</div>
</div>
<% if (progress) { %>
<p class="certificate-status">
<a href="<%- detailUrl %>" class="status-text secondary" aria-describedby="program-<%- id %>"><%= interpolate(
<a href="<%- detailUrl %>" class="status-text secondary" aria-describedby="program-<%-
uu
id %>"><%= interpolate(
ngettext(
'%(count)s course is in progress.',
'%(count)s courses are in progress.',
progress.
total.
in_progress
progress.in_progress
),
{count: progress.
total.
in_progress}, true
{count: progress.in_progress}, true
) %></a>
<a href="<%- detailUrl %>" class="status-text secondary" aria-describedby="program-<%- id %>"><%= interpolate(
<a href="<%- detailUrl %>" class="status-text secondary" aria-describedby="program-<%-
uu
id %>"><%= interpolate(
ngettext(
'%(count)s course has not been started.',
'%(count)s courses have not been started.',
progress.
total.
not_started
progress.not_started
),
{count: progress.
total.
not_started}, true
{count: progress.not_started}, true
) %></a>
<span id="status-<%- id %>" class="status-text"><%= interpolate(
<span id="status-<%-
uu
id %>" class="status-text"><%= interpolate(
gettext('You have earned certificates in %(completed_courses)s of the %(total_courses)s courses so far.'),
{completed_courses: progress.
total.completed, total_courses: progress.total.courses
}, true
{completed_courses: progress.
completed, total_courses: progress.total
}, true
) %></span>
</p>
<% } %>
...
...
@@ -44,11 +44,11 @@
<a href="<%- detailUrl %>" class="card-link">
<div class="banner-image-container">
<picture>
<source srcset="<%-
smallBannerUrl %>" media="(max-width: <%- breakpoints.max.tiny
%>)">
<source srcset="<%-
medium
BannerUrl %>" media="(max-width: <%- breakpoints.max.small %>)">
<source srcset="<%-
large
BannerUrl %>" media="(max-width: <%- breakpoints.max.medium %>)">
<source srcset="<%- smallBannerUrl %>" media="(max-width: <%- breakpoints.max.large %>)">
<img class="banner-image" srcset="<%-
mediumBannerUrl %>" alt="<%= interpolate(gettext('%(programName)s Home Page.'), {programName: nam
e}, true)%>">
<source srcset="<%-
xsmallBannerUrl %>" media="(max-width: <%- breakpoints.max.xsmall
%>)">
<source srcset="<%-
small
BannerUrl %>" media="(max-width: <%- breakpoints.max.small %>)">
<source srcset="<%-
medium
BannerUrl %>" media="(max-width: <%- breakpoints.max.medium %>)">
<source srcset="<%-
x
smallBannerUrl %>" media="(max-width: <%- breakpoints.max.large %>)">
<img class="banner-image" srcset="<%-
smallBannerUrl %>" alt="<%= interpolate(gettext('%(programName)s Home Page.'), {programName: titl
e}, true)%>">
</picture>
</div>
</a>
lms/templates/learner_dashboard/program_header_view.underscore
View file @
dda0e03f
<div class="banner-background-wrapper">
<picture>
<source srcset="<%- programData.banner_image
_urls.w1440h480
%>" media="(min-width: <%- breakpoints.min.large %>)">
<source srcset="<%- programData.banner_image
_urls.w726h242
%>" media="(min-width: <%- breakpoints.min.medium %>)">
<img class="banner-background-image" srcset="<%- programData.banner_image
_urls.w348h116
%>" alt="">
<source srcset="<%- programData.banner_image
.large.url
%>" media="(min-width: <%- breakpoints.min.large %>)">
<source srcset="<%- programData.banner_image
.medium.url
%>" media="(min-width: <%- breakpoints.min.medium %>)">
<img class="banner-background-image" srcset="<%- programData.banner_image
['x-small'].url
%>" alt="">
</picture>
<div class="banner-content grid-container">
<h2 class="hd-1 title row"><%- programData.
nam
e %></h2>
<h2 class="hd-1 title row"><%- programData.
titl
e %></h2>
<p class="hd-4 subtitle row"><%- programData.subtitle %></p>
<% if (programData.organizations.length) { %>
<% if (programData.
authoring_
organizations.length) { %>
<div class="org-wrapper">
<% _.each(programData.organizations, function(org) { %>
<img src="<%- org.
img
%>" class="org-logo" alt="<%- StringUtils.interpolate(
<% _.each(programData.
authoring_
organizations, function(org) { %>
<img src="<%- org.
certificate_logo_image_url || org.logo_image_url
%>" class="org-logo" alt="<%- StringUtils.interpolate(
gettext('{organization}\'s logo'),
{organization: org.
display_
name}
{organization: org.name}
) %>">
<% }) %>
</div>
...
...
@@ -33,7 +33,7 @@
<span class="crumb-separator fa fa-chevron-right" aria-hidden="true"></span>
</li>
<li class="crumb active">
<%- programData.
nam
e %>
<%- programData.
titl
e %>
</li>
</ol>
</nav>
openedx/core/djangoapps/catalog/tests/test_utils.py
View file @
dda0e03f
...
...
@@ -13,7 +13,6 @@ from openedx.core.djangoapps.catalog.tests.factories import ProgramFactory, Prog
from
openedx.core.djangoapps.catalog.tests.mixins
import
CatalogIntegrationMixin
from
openedx.core.djangoapps.catalog.utils
import
(
get_programs
,
munge_catalog_program
,
get_program_types
,
get_programs_with_type_logo
,
)
...
...
@@ -131,63 +130,6 @@ class TestGetPrograms(CatalogIntegrationMixin, TestCase):
self
.
assertEqual
(
data
,
[])
class
TestMungeCatalogProgram
(
TestCase
):
def
setUp
(
self
):
super
(
TestMungeCatalogProgram
,
self
)
.
setUp
()
self
.
catalog_program
=
ProgramFactory
()
def
assert_munged
(
self
,
program
):
munged
=
munge_catalog_program
(
program
)
expected
=
{
'id'
:
program
[
'uuid'
],
'name'
:
program
[
'title'
],
'subtitle'
:
program
[
'subtitle'
],
'category'
:
program
[
'type'
],
'marketing_slug'
:
program
[
'marketing_slug'
],
'organizations'
:
[
{
'display_name'
:
organization
[
'name'
],
'key'
:
organization
[
'key'
]
}
for
organization
in
program
[
'authoring_organizations'
]
],
'course_codes'
:
[
{
'display_name'
:
course
[
'title'
],
'key'
:
course
[
'key'
],
'organization'
:
{
'display_name'
:
course
[
'owners'
][
0
][
'name'
],
'key'
:
course
[
'owners'
][
0
][
'key'
]
},
'run_modes'
:
[
{
'course_key'
:
course_run
[
'key'
],
'run_key'
:
CourseKey
.
from_string
(
course_run
[
'key'
])
.
run
,
'mode_slug'
:
course_run
[
'type'
],
'marketing_url'
:
course_run
[
'marketing_url'
],
}
for
course_run
in
course
[
'course_runs'
]
],
}
for
course
in
program
[
'courses'
]
],
'banner_image_urls'
:
{
'w1440h480'
:
program
[
'banner_image'
][
'large'
][
'url'
],
'w726h242'
:
program
[
'banner_image'
][
'medium'
][
'url'
],
'w435h145'
:
program
[
'banner_image'
][
'small'
][
'url'
],
'w348h116'
:
program
[
'banner_image'
][
'x-small'
][
'url'
],
},
'detail_url'
:
program
.
get
(
'detail_url'
),
}
self
.
assertEqual
(
munged
,
expected
)
def
test_munge_catalog_program
(
self
):
self
.
assert_munged
(
self
.
catalog_program
)
def
test_munge_with_detail_url
(
self
):
self
.
catalog_program
[
'detail_url'
]
=
'foo'
self
.
assert_munged
(
self
.
catalog_program
)
@skip_unless_lms
@mock.patch
(
UTILS_MODULE
+
'.get_edx_api_data'
)
class
TestGetProgramTypes
(
CatalogIntegrationMixin
,
TestCase
):
...
...
openedx/core/djangoapps/catalog/utils.py
View file @
dda0e03f
...
...
@@ -66,64 +66,6 @@ def get_programs(uuid=None, type=None): # pylint: disable=redefined-builtin
return
[]
def
munge_catalog_program
(
catalog_program
):
"""
Make a program from the catalog service look like it came from the programs service.
We want to display programs from the catalog service on the LMS. The LMS
originally retrieved all program data from the deprecated programs service.
This temporary utility is here to help incrementally swap out the backend.
Clean up of this debt is tracked by ECOM-4418.
Arguments:
catalog_program (dict): The catalog service's representation of a program.
Return:
dict, imitating the schema used by the programs service.
"""
return
{
'id'
:
catalog_program
[
'uuid'
],
'name'
:
catalog_program
[
'title'
],
'subtitle'
:
catalog_program
[
'subtitle'
],
'category'
:
catalog_program
[
'type'
],
'marketing_slug'
:
catalog_program
[
'marketing_slug'
],
'organizations'
:
[
{
'display_name'
:
organization
[
'name'
],
'key'
:
organization
[
'key'
]
}
for
organization
in
catalog_program
[
'authoring_organizations'
]
],
'course_codes'
:
[
{
'display_name'
:
course
[
'title'
],
'key'
:
course
[
'key'
],
'organization'
:
{
# The Programs schema only supports one organization here.
'display_name'
:
course
[
'owners'
][
0
][
'name'
],
'key'
:
course
[
'owners'
][
0
][
'key'
]
}
if
course
[
'owners'
]
else
{},
'run_modes'
:
[
{
'course_key'
:
course_run
[
'key'
],
'run_key'
:
CourseKey
.
from_string
(
course_run
[
'key'
])
.
run
,
'mode_slug'
:
course_run
[
'type'
],
'marketing_url'
:
course_run
[
'marketing_url'
],
}
for
course_run
in
course
[
'course_runs'
]
],
}
for
course
in
catalog_program
[
'courses'
]
],
'banner_image_urls'
:
{
'w1440h480'
:
catalog_program
[
'banner_image'
][
'large'
][
'url'
],
'w726h242'
:
catalog_program
[
'banner_image'
][
'medium'
][
'url'
],
'w435h145'
:
catalog_program
[
'banner_image'
][
'small'
][
'url'
],
'w348h116'
:
catalog_program
[
'banner_image'
][
'x-small'
][
'url'
],
},
# If a detail URL has been added, we don't want to lose it.
'detail_url'
:
catalog_program
.
get
(
'detail_url'
),
}
def
get_program_types
():
"""Retrieve all program types from the catalog service.
...
...
openedx/core/djangoapps/programs/tests/factories.py
View file @
dda0e03f
...
...
@@ -4,14 +4,11 @@ import factory
from
faker
import
Faker
fake
=
Faker
()
class
ProgressFactory
(
factory
.
Factory
):
class
Meta
(
object
):
model
=
dict
uuid
=
factory
.
Faker
(
'uuid4'
)
completed
=
[]
in_progress
=
[]
not_started
=
[]
completed
=
0
in_progress
=
0
not_started
=
0
openedx/core/djangoapps/programs/tests/test_utils.py
View file @
dda0e03f
"""Tests covering Programs utilities."""
# pylint: disable=no-member
import
datetime
import
json
import
uuid
...
...
@@ -15,7 +16,6 @@ from pytz import utc
from
lms.djangoapps.certificates.api
import
MODES
from
lms.djangoapps.commerce.tests.test_utils
import
update_commerce_config
from
openedx.core.djangoapps.catalog.utils
import
munge_catalog_program
from
openedx.core.djangoapps.catalog.tests.factories
import
(
generate_course_run_key
,
ProgramFactory
,
...
...
@@ -60,10 +60,6 @@ class TestProgramProgressMeter(TestCase):
"""Variadic helper used to verify progress calculations."""
self
.
assertEqual
(
meter
.
progress
,
list
(
progresses
))
def
_extract_titles
(
self
,
program
,
*
indices
):
"""Construct a list containing the titles of the indicated courses."""
return
[
program
[
'courses'
][
index
][
'title'
]
for
index
in
indices
]
def
_attach_detail_url
(
self
,
programs
):
"""Add expected detail URLs to a list of program dicts."""
for
program
in
programs
:
...
...
@@ -118,10 +114,7 @@ class TestProgramProgressMeter(TestCase):
self
.
assertEqual
(
meter
.
engaged_programs
,
[
program
])
self
.
_assert_progress
(
meter
,
ProgressFactory
(
uuid
=
program
[
'uuid'
],
in_progress
=
self
.
_extract_titles
(
program
,
0
)
)
ProgressFactory
(
uuid
=
program
[
'uuid'
],
in_progress
=
1
)
)
self
.
assertEqual
(
meter
.
completed_programs
,
[])
...
...
@@ -160,10 +153,7 @@ class TestProgramProgressMeter(TestCase):
self
.
assertEqual
(
meter
.
engaged_programs
,
programs
)
self
.
_assert_progress
(
meter
,
*
(
ProgressFactory
(
uuid
=
program
[
'uuid'
],
in_progress
=
self
.
_extract_titles
(
program
,
0
))
for
program
in
programs
)
*
(
ProgressFactory
(
uuid
=
program
[
'uuid'
],
in_progress
=
1
)
for
program
in
programs
)
)
self
.
assertEqual
(
meter
.
completed_programs
,
[])
...
...
@@ -208,10 +198,7 @@ class TestProgramProgressMeter(TestCase):
self
.
assertEqual
(
meter
.
engaged_programs
,
programs
)
self
.
_assert_progress
(
meter
,
*
(
ProgressFactory
(
uuid
=
program
[
'uuid'
],
in_progress
=
self
.
_extract_titles
(
program
,
0
))
for
program
in
programs
)
*
(
ProgressFactory
(
uuid
=
program
[
'uuid'
],
in_progress
=
1
)
for
program
in
programs
)
)
self
.
assertEqual
(
meter
.
completed_programs
,
[])
...
...
@@ -245,11 +232,7 @@ class TestProgramProgressMeter(TestCase):
program
,
program_uuid
=
data
[
0
],
data
[
0
][
'uuid'
]
self
.
_assert_progress
(
meter
,
ProgressFactory
(
uuid
=
program_uuid
,
in_progress
=
self
.
_extract_titles
(
program
,
0
),
not_started
=
self
.
_extract_titles
(
program
,
1
)
)
ProgressFactory
(
uuid
=
program_uuid
,
in_progress
=
1
,
not_started
=
1
)
)
self
.
assertEqual
(
meter
.
completed_programs
,
[])
...
...
@@ -258,10 +241,7 @@ class TestProgramProgressMeter(TestCase):
meter
=
ProgramProgressMeter
(
self
.
user
)
self
.
_assert_progress
(
meter
,
ProgressFactory
(
uuid
=
program_uuid
,
in_progress
=
self
.
_extract_titles
(
program
,
0
,
1
)
)
ProgressFactory
(
uuid
=
program_uuid
,
in_progress
=
2
)
)
self
.
assertEqual
(
meter
.
completed_programs
,
[])
...
...
@@ -272,11 +252,7 @@ class TestProgramProgressMeter(TestCase):
meter
=
ProgramProgressMeter
(
self
.
user
)
self
.
_assert_progress
(
meter
,
ProgressFactory
(
uuid
=
program_uuid
,
completed
=
self
.
_extract_titles
(
program
,
0
),
in_progress
=
self
.
_extract_titles
(
program
,
1
)
)
ProgressFactory
(
uuid
=
program_uuid
,
completed
=
1
,
in_progress
=
1
)
)
self
.
assertEqual
(
meter
.
completed_programs
,
[])
...
...
@@ -288,11 +264,7 @@ class TestProgramProgressMeter(TestCase):
meter
=
ProgramProgressMeter
(
self
.
user
)
self
.
_assert_progress
(
meter
,
ProgressFactory
(
uuid
=
program_uuid
,
completed
=
self
.
_extract_titles
(
program
,
0
),
in_progress
=
self
.
_extract_titles
(
program
,
1
)
)
ProgressFactory
(
uuid
=
program_uuid
,
completed
=
1
,
in_progress
=
1
)
)
self
.
assertEqual
(
meter
.
completed_programs
,
[])
...
...
@@ -304,10 +276,7 @@ class TestProgramProgressMeter(TestCase):
meter
=
ProgramProgressMeter
(
self
.
user
)
self
.
_assert_progress
(
meter
,
ProgressFactory
(
uuid
=
program_uuid
,
completed
=
self
.
_extract_titles
(
program
,
0
,
1
)
)
ProgressFactory
(
uuid
=
program_uuid
,
completed
=
2
)
)
self
.
assertEqual
(
meter
.
completed_programs
,
[
program_uuid
])
...
...
@@ -340,7 +309,7 @@ class TestProgramProgressMeter(TestCase):
program
,
program_uuid
=
data
[
0
],
data
[
0
][
'uuid'
]
self
.
_assert_progress
(
meter
,
ProgressFactory
(
uuid
=
program_uuid
,
completed
=
self
.
_extract_titles
(
program
,
0
)
)
ProgressFactory
(
uuid
=
program_uuid
,
completed
=
1
)
)
self
.
assertEqual
(
meter
.
completed_programs
,
[
program_uuid
])
...
...
@@ -418,57 +387,56 @@ class TestProgramDataExtender(ModuleStoreTestCase):
"""Tests of the program data extender utility class."""
maxDiff
=
None
sku
=
'abc123'
password
=
'test'
checkout_path
=
'/basket'
def
setUp
(
self
):
super
(
TestProgramDataExtender
,
self
)
.
setUp
()
self
.
user
=
UserFactory
()
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
password
)
self
.
course
=
ModuleStoreCourseFactory
()
self
.
course
.
start
=
datetime
.
datetime
.
now
(
utc
)
-
datetime
.
timedelta
(
days
=
1
)
self
.
course
.
end
=
datetime
.
datetime
.
now
(
utc
)
+
datetime
.
timedelta
(
days
=
1
)
self
.
course
=
self
.
update_course
(
self
.
course
,
self
.
user
.
id
)
# pylint: disable=no-member
organization
=
OrganizationFactory
()
course_run
=
CourseRunFactory
(
key
=
unicode
(
self
.
course
.
id
))
# pylint: disable=no-member
course
=
CourseFactory
(
course_runs
=
[
course_run
])
program
=
ProgramFactory
(
authoring_organizations
=
[
organization
],
courses
=
[
course
])
self
.
course
=
self
.
update_course
(
self
.
course
,
self
.
user
.
id
)
self
.
program
=
munge_catalog_program
(
program
)
self
.
c
ourse_code
=
self
.
program
[
'course_codes'
][
0
]
self
.
run_mode
=
self
.
course_code
[
'run_modes'
][
0
]
self
.
course_run
=
CourseRunFactory
(
key
=
unicode
(
self
.
course
.
id
)
)
self
.
c
atalog_course
=
CourseFactory
(
course_runs
=
[
self
.
course_run
])
self
.
program
=
ProgramFactory
(
courses
=
[
self
.
catalog_course
])
def
_assert_supplemented
(
self
,
actual
,
**
kwargs
):
"""DRY helper used to verify that program data is extended correctly."""
course_overview
=
CourseOverview
.
get_from_id
(
self
.
course
.
id
)
# pylint: disable=no-member
run_mode
=
dict
(
self
.
course_run
.
update
(
dict
(
{
'certificate_url'
:
None
,
'course_image_url'
:
course_overview
.
course_image_url
,
'course_key'
:
unicode
(
self
.
course
.
id
),
# pylint: disable=no-member
'course_url'
:
reverse
(
'course_root'
,
args
=
[
self
.
course
.
id
]),
# pylint: disable=no-member
'end_date'
:
self
.
course
.
end
.
replace
(
tzinfo
=
utc
),
'course_url'
:
reverse
(
'course_root'
,
args
=
[
self
.
course
.
id
]),
'enrollment_open_date'
:
strftime_localized
(
DEFAULT_ENROLLMENT_START_DATE
,
'SHORT_DATE'
),
'is_course_ended'
:
self
.
course
.
end
<
datetime
.
datetime
.
now
(
utc
),
'is_enrolled'
:
False
,
'is_enrollment_open'
:
True
,
'marketing_url'
:
self
.
run_mode
[
'marketing_url'
],
'mode_slug'
:
'verified'
,
'start_date'
:
self
.
course
.
start
.
replace
(
tzinfo
=
utc
),
'upgrade_url'
:
None
,
'advertised_start'
:
None
,
},
**
kwargs
)
)
self
.
c
ourse_code
[
'run_modes'
]
=
[
run_mode
]
self
.
program
[
'course
_codes'
]
=
[
self
.
course_cod
e
]
self
.
c
atalog_course
[
'course_runs'
]
=
[
self
.
course_run
]
self
.
program
[
'course
s'
]
=
[
self
.
catalog_cours
e
]
self
.
assertEqual
(
actual
,
self
.
program
)
@ddt.data
(
-
1
,
0
,
1
)
def
test_is_enrollment_open
(
self
,
days_offset
):
"""
Verify that changes to the course run end date do not affect our
assessment of the course run being open for enrollment.
"""
self
.
course
.
end
=
datetime
.
datetime
.
now
(
utc
)
+
datetime
.
timedelta
(
days
=
days_offset
)
self
.
course
=
self
.
update_course
(
self
.
course
,
self
.
user
.
id
)
data
=
ProgramDataExtender
(
self
.
program
,
self
.
user
)
.
extend
()
self
.
_assert_supplemented
(
data
)
@ddt.data
(
(
False
,
None
,
False
),
(
True
,
MODES
.
audit
,
True
),
...
...
@@ -491,7 +459,7 @@ class TestProgramDataExtender(ModuleStoreTestCase):
mock_get_mode
.
return_value
=
mock_mode
if
is_enrolled
:
CourseEnrollmentFactory
(
user
=
self
.
user
,
course_id
=
self
.
course
.
id
,
mode
=
enrolled_mode
)
# pylint: disable=no-member
CourseEnrollmentFactory
(
user
=
self
.
user
,
course_id
=
self
.
course
.
id
,
mode
=
enrolled_mode
)
data
=
ProgramDataExtender
(
self
.
program
,
self
.
user
)
.
extend
()
...
...
@@ -503,12 +471,14 @@ class TestProgramDataExtender(ModuleStoreTestCase):
@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."""
"""
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
course_id
=
self
.
course
.
id
,
mode
=
enrolled_mode
,
is_active
=
False
,
)
...
...
@@ -519,14 +489,16 @@ class TestProgramDataExtender(ModuleStoreTestCase):
@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."""
"""
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
CourseEnrollmentFactory
(
user
=
self
.
user
,
course_id
=
self
.
course
.
id
,
mode
=
MODES
.
audit
)
data
=
ProgramDataExtender
(
self
.
program
,
self
.
user
)
.
extend
()
...
...
@@ -537,14 +509,14 @@ class TestProgramDataExtender(ModuleStoreTestCase):
(
1
,
-
1
,
True
),
)
@ddt.unpack
def
test_course_enrollment_status
(
self
,
start_offset
,
end_offset
,
is_enrollment_open
):
def
test_course_
run_
enrollment_status
(
self
,
start_offset
,
end_offset
,
is_enrollment_open
):
"""
Verify that course enrollment status is reflected correctly.
Verify that course
run
enrollment status is reflected correctly.
"""
self
.
course
.
enrollment_start
=
datetime
.
datetime
.
now
(
utc
)
-
datetime
.
timedelta
(
days
=
start_offset
)
self
.
course
.
enrollment_end
=
datetime
.
datetime
.
now
(
utc
)
-
datetime
.
timedelta
(
days
=
end_offset
)
self
.
course
=
self
.
update_course
(
self
.
course
,
self
.
user
.
id
)
# pylint: disable=no-member
self
.
course
=
self
.
update_course
(
self
.
course
,
self
.
user
.
id
)
data
=
ProgramDataExtender
(
self
.
program
,
self
.
user
)
.
extend
()
...
...
@@ -555,12 +527,12 @@ class TestProgramDataExtender(ModuleStoreTestCase):
)
def
test_no_enrollment_start_date
(
self
):
"""
Verify that a closed course with no explicit enrollment start date doesn't cause an error.
Regression test for ECOM-4973.
"""
Verify that a closed course run with no explicit enrollment start date
doesn't cause an error.
Regression test for ECOM-4973.
"""
self
.
course
.
enrollment_end
=
datetime
.
datetime
.
now
(
utc
)
-
datetime
.
timedelta
(
days
=
1
)
self
.
course
=
self
.
update_course
(
self
.
course
,
self
.
user
.
id
)
# pylint: disable=no-member
self
.
course
=
self
.
update_course
(
self
.
course
,
self
.
user
.
id
)
data
=
ProgramDataExtender
(
self
.
program
,
self
.
user
)
.
extend
()
...
...
@@ -573,7 +545,10 @@ class TestProgramDataExtender(ModuleStoreTestCase):
@mock.patch
(
UTILS_MODULE
+
'.certificate_api.certificate_downloadable_status'
)
@mock.patch
(
CERTIFICATES_API_MODULE
+
'.has_html_certificates_enabled'
)
def
test_certificate_url_retrieval
(
self
,
is_uuid_available
,
mock_html_certs_enabled
,
mock_get_cert_data
):
"""Verify that the student's run mode certificate is included, when available."""
"""
Verify that the student's run mode certificate is included,
when available.
"""
test_uuid
=
uuid
.
uuid4
()
.
hex
mock_get_cert_data
.
return_value
=
{
'uuid'
:
test_uuid
}
if
is_uuid_available
else
{}
mock_html_certs_enabled
.
return_value
=
True
...
...
@@ -586,42 +561,3 @@ class TestProgramDataExtender(ModuleStoreTestCase):
)
if
is_uuid_available
else
None
self
.
_assert_supplemented
(
data
,
certificate_url
=
expected_url
)
@ddt.data
(
-
1
,
0
,
1
)
def
test_course_course_ended
(
self
,
days_offset
):
self
.
course
.
end
=
datetime
.
datetime
.
now
(
utc
)
+
datetime
.
timedelta
(
days
=
days_offset
)
self
.
course
=
self
.
update_course
(
self
.
course
,
self
.
user
.
id
)
# pylint: disable=no-member
data
=
ProgramDataExtender
(
self
.
program
,
self
.
user
)
.
extend
()
self
.
_assert_supplemented
(
data
)
@mock.patch
(
UTILS_MODULE
+
'.get_organization_by_short_name'
)
def
test_organization_logo_exists
(
self
,
mock_get_organization_by_short_name
):
""" Verify the logo image is set from the organizations api """
mock_logo_url
=
'edx/logo.png'
mock_image
=
mock
.
Mock
()
mock_image
.
url
=
mock_logo_url
mock_get_organization_by_short_name
.
return_value
=
{
'logo'
:
mock_image
}
data
=
ProgramDataExtender
(
self
.
program
,
self
.
user
)
.
extend
()
self
.
assertEqual
(
data
[
'organizations'
][
0
]
.
get
(
'img'
),
mock_logo_url
)
@mock.patch
(
UTILS_MODULE
+
'.get_organization_by_short_name'
)
def
test_organization_missing
(
self
,
mock_get_organization_by_short_name
):
""" Verify the logo image is not set if the organizations api returns None """
mock_get_organization_by_short_name
.
return_value
=
None
data
=
ProgramDataExtender
(
self
.
program
,
self
.
user
)
.
extend
()
self
.
assertEqual
(
data
[
'organizations'
][
0
]
.
get
(
'img'
),
None
)
@mock.patch
(
UTILS_MODULE
+
'.get_organization_by_short_name'
)
def
test_organization_logo_missing
(
self
,
mock_get_organization_by_short_name
):
"""
Verify the logo image is not set if the organizations api returns organization,
but the logo is not available
"""
mock_get_organization_by_short_name
.
return_value
=
{
'logo'
:
None
}
data
=
ProgramDataExtender
(
self
.
program
,
self
.
user
)
.
extend
()
self
.
assertEqual
(
data
[
'organizations'
][
0
]
.
get
(
'img'
),
None
)
openedx/core/djangoapps/programs/utils.py
View file @
dda0e03f
...
...
@@ -18,7 +18,6 @@ from openedx.core.djangoapps.content.course_overviews.models import CourseOvervi
from
openedx.core.lib.edx_api_utils
import
get_edx_api_data
from
student.models
import
CourseEnrollment
from
util.date_utils
import
strftime_localized
from
util.organizations_helpers
import
get_organization_by_short_name
# The datetime module's strftime() methods require a year >= 1900.
...
...
@@ -26,7 +25,7 @@ DEFAULT_ENROLLMENT_START_DATE = datetime.datetime(1900, 1, 1, tzinfo=utc)
def
get_program_marketing_url
(
programs_config
):
"""Build a URL
to be used when linking to program details on a
marketing site."""
"""Build a URL
used to link to programs on the
marketing site."""
return
urljoin
(
settings
.
MKTG_URLS
.
get
(
'ROOT'
),
programs_config
.
marketing_path
)
.
rstrip
(
'/'
)
...
...
@@ -47,18 +46,6 @@ def attach_program_detail_url(programs):
return
programs
def
munge_progress_map
(
progress_map
):
"""
Temporary utility for making progress maps look like they were built using
data from the deprecated programs service.
Clean up of this debt is tracked by ECOM-4418.
"""
progress_map
[
'id'
]
=
progress_map
.
pop
(
'uuid'
)
return
progress_map
class
ProgramProgressMeter
(
object
):
"""Utility for gauging a user's progress towards program completion.
...
...
@@ -139,19 +126,15 @@ class ProgramProgressMeter(object):
"""
progress
=
[]
for
program
in
self
.
engaged_programs
:
completed
,
in_progress
,
not_started
=
[],
[],
[]
completed
,
in_progress
,
not_started
=
0
,
0
,
0
for
course
in
program
[
'courses'
]:
# TODO: What are these titles used for? If they're not used by
# the front-end, pass integer counts instead.
title
=
course
[
'title'
]
if
self
.
_is_course_complete
(
course
):
completed
.
append
(
title
)
completed
+=
1
elif
self
.
_is_course_in_progress
(
course
):
in_progress
.
append
(
title
)
in_progress
+=
1
else
:
not_started
.
append
(
title
)
not_started
+=
1
progress
.
append
({
'uuid'
:
program
[
'uuid'
],
...
...
@@ -249,18 +232,18 @@ class ProgramProgressMeter(object):
# pylint: disable=missing-docstring
class
ProgramDataExtender
(
object
):
"""
Utility for extending program
course codes with CourseOverview and
CourseEnrollment
data.
Utility for extending program
data meant for the program detail page with
user-specific (e.g., CourseEnrollment)
data.
Arguments:
program_data (dict): Representation of a program. Note that this dict must
be formatted as if it was returned by the deprecated program service.
program_data (dict): Representation of a program.
user (User): The user whose enrollments to inspect.
"""
def
__init__
(
self
,
program_data
,
user
):
self
.
data
=
program_data
self
.
user
=
user
self
.
course_key
=
None
self
.
course_run_key
=
None
self
.
course_overview
=
None
self
.
enrollment_start
=
None
...
...
@@ -278,77 +261,62 @@ class ProgramDataExtender(object):
"""Returns a generator yielding method names beginning with the given prefix."""
return
(
name
for
name
in
cls
.
__dict__
if
name
.
startswith
(
prefix
))
def
_extend_organizations
(
self
):
"""Execute organization data handlers."""
for
organization
in
self
.
data
[
'organizations'
]:
self
.
_execute
(
'_attach_organization'
,
organization
)
def
_extend_run_modes
(
self
):
"""Execute run mode data handlers."""
for
course_code
in
self
.
data
[
'course_codes'
]:
for
run_mode
in
course_code
[
'run_modes'
]:
def
_extend_course_runs
(
self
):
"""Execute course run data handlers."""
for
course
in
self
.
data
[
'courses'
]:
for
course_run
in
course
[
'course_runs'
]:
# State to be shared across handlers.
self
.
course_
key
=
CourseKey
.
from_string
(
run_mode
[
'course_
key'
])
self
.
course_overview
=
CourseOverview
.
get_from_id
(
self
.
course_key
)
self
.
course_
run_key
=
CourseKey
.
from_string
(
course_run
[
'
key'
])
self
.
course_overview
=
CourseOverview
.
get_from_id
(
self
.
course_
run_
key
)
self
.
enrollment_start
=
self
.
course_overview
.
enrollment_start
or
DEFAULT_ENROLLMENT_START_DATE
self
.
_execute
(
'_attach_
run_mode'
,
run_mode
)
self
.
_execute
(
'_attach_
course_run'
,
course_run
)
def
_attach_organization_logo
(
self
,
organization
):
# TODO: Cache the results of the get_organization_by_short_name call so
# the database is hit less frequently.
org_obj
=
get_organization_by_short_name
(
organization
[
'key'
])
if
org_obj
and
org_obj
.
get
(
'logo'
):
organization
[
'img'
]
=
org_obj
[
'logo'
]
.
url
def
_attach_run_mode_certificate_url
(
self
,
run_mode
):
certificate_data
=
certificate_api
.
certificate_downloadable_status
(
self
.
user
,
self
.
course_key
)
def
_attach_course_run_certificate_url
(
self
,
run_mode
):
certificate_data
=
certificate_api
.
certificate_downloadable_status
(
self
.
user
,
self
.
course_run_key
)
certificate_uuid
=
certificate_data
.
get
(
'uuid'
)
run_mode
[
'certificate_url'
]
=
certificate_api
.
get_certificate_url
(
user_id
=
self
.
user
.
id
,
# Providing user_id allows us to fall back to PDF certificates
# if web certificates are not configured for a given course.
course_id
=
self
.
course_key
,
course_id
=
self
.
course_
run_
key
,
uuid
=
certificate_uuid
,
)
if
certificate_uuid
else
None
def
_attach_run_mode_course_image_url
(
self
,
run_mode
):
run_mode
[
'course_image_url'
]
=
self
.
course_overview
.
course_image_url
def
_attach_run_mode_course_url
(
self
,
run_mode
):
run_mode
[
'course_url'
]
=
reverse
(
'course_root'
,
args
=
[
self
.
course_key
])
def
_attach_course_run_course_url
(
self
,
run_mode
):
run_mode
[
'course_url'
]
=
reverse
(
'course_root'
,
args
=
[
self
.
course_run_key
])
def
_attach_run_mode_end_date
(
self
,
run_mode
):
run_mode
[
'end_date'
]
=
self
.
course_overview
.
end
def
_attach_run_mode_enrollment_open_date
(
self
,
run_mode
):
def
_attach_course_run_enrollment_open_date
(
self
,
run_mode
):
run_mode
[
'enrollment_open_date'
]
=
strftime_localized
(
self
.
enrollment_start
,
'SHORT_DATE'
)
def
_attach_
run_mode
_is_course_ended
(
self
,
run_mode
):
def
_attach_
course_run
_is_course_ended
(
self
,
run_mode
):
end_date
=
self
.
course_overview
.
end
or
datetime
.
datetime
.
max
.
replace
(
tzinfo
=
utc
)
run_mode
[
'is_course_ended'
]
=
end_date
<
datetime
.
datetime
.
now
(
utc
)
def
_attach_
run_mode
_is_enrolled
(
self
,
run_mode
):
run_mode
[
'is_enrolled'
]
=
CourseEnrollment
.
is_enrolled
(
self
.
user
,
self
.
course_key
)
def
_attach_
course_run
_is_enrolled
(
self
,
run_mode
):
run_mode
[
'is_enrolled'
]
=
CourseEnrollment
.
is_enrolled
(
self
.
user
,
self
.
course_
run_
key
)
def
_attach_
run_mode
_is_enrollment_open
(
self
,
run_mode
):
def
_attach_
course_run
_is_enrollment_open
(
self
,
run_mode
):
enrollment_end
=
self
.
course_overview
.
enrollment_end
or
datetime
.
datetime
.
max
.
replace
(
tzinfo
=
utc
)
run_mode
[
'is_enrollment_open'
]
=
self
.
enrollment_start
<=
datetime
.
datetime
.
now
(
utc
)
<
enrollment_end
def
_attach_run_mode_start_date
(
self
,
run_mode
):
run_mode
[
'start_date'
]
=
self
.
course_overview
.
start
def
_attach_run_mode_advertised_start
(
self
,
run_mode
):
def
_attach_course_run_advertised_start
(
self
,
run_mode
):
"""
The advertised_start is text a course author can provide to be displayed
instead of their course's start date. For example, if a course run were
to start on December 1, 2016, the author might provide 'Winter 2016' as
the advertised start.
"""
run_mode
[
'advertised_start'
]
=
self
.
course_overview
.
advertised_start
def
_attach_
run_mode
_upgrade_url
(
self
,
run_mode
):
required_mode_slug
=
run_mode
[
'
mode_slug
'
]
enrolled_mode_slug
,
_
=
CourseEnrollment
.
enrollment_mode_for_user
(
self
.
user
,
self
.
course_key
)
def
_attach_
course_run
_upgrade_url
(
self
,
run_mode
):
required_mode_slug
=
run_mode
[
'
type
'
]
enrolled_mode_slug
,
_
=
CourseEnrollment
.
enrollment_mode_for_user
(
self
.
user
,
self
.
course_
run_
key
)
is_mode_mismatch
=
required_mode_slug
!=
enrolled_mode_slug
is_upgrade_required
=
is_mode_mismatch
and
CourseEnrollment
.
is_enrolled
(
self
.
user
,
self
.
course_key
)
is_upgrade_required
=
is_mode_mismatch
and
CourseEnrollment
.
is_enrolled
(
self
.
user
,
self
.
course_
run_
key
)
if
is_upgrade_required
:
# Requires that the ecommerce service be in use.
required_mode
=
CourseMode
.
mode_for_course
(
self
.
course_key
,
required_mode_slug
)
required_mode
=
CourseMode
.
mode_for_course
(
self
.
course_
run_
key
,
required_mode_slug
)
ecommerce
=
EcommerceService
()
sku
=
getattr
(
required_mode
,
'sku'
,
None
)
...
...
themes/edx.org/lms/templates/dashboard.html
View file @
dda0e03f
...
...
@@ -100,7 +100,7 @@ from openedx.core.djangoapps.theming import helpers as theming_helpers
<
%
is_course_blocked =
(enrollment.course_id
in
block_courses
)
%
>
<
%
course_verification_status =
verification_status_by_course.get(enrollment.course_id,
{})
%
>
<
%
course_requirements =
courses_requirements_not_met.get(enrollment.course_id)
%
>
<
%
related_programs =
programs_by_run
.get(unicode(enrollment.course_id))
%
>
<
%
related_programs =
inverted_programs
.get(unicode(enrollment.course_id))
%
>
<
%
include
file =
'dashboard/_dashboard_course_listing.html'
args=
"course_overview=enrollment.course_overview, enrollment=enrollment, show_courseware_link=show_courseware_link, cert_status=cert_status, can_unenroll=can_unenroll, credit_status=credit_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, show_refund_option=show_refund_option, is_paid_course=is_paid_course, is_course_blocked=is_course_blocked, verification_status=course_verification_status, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings, user=user, related_programs=related_programs"
/>
% endfor
...
...
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