Merge branch 'danielli/pyfs' of https://github.com/edx/edx-platform into danielli/pyfs
This commit is contained in:
10
.jshintrc
10
.jshintrc
@@ -22,12 +22,12 @@
|
||||
"nonbsp" : true, // Warns about "non-breaking whitespace" characters.
|
||||
"nonew" : true, // Prohibits the use of constructor functions for side-effects.
|
||||
"plusplus" : false, // Prohibits the use of unary increment and decrement operators.
|
||||
"quotmark" : "single", // Enforces the consistency of quotation marks used throughout your code. It accepts three values: true, "single", and "double".
|
||||
"quotmark" : false, // Enforces the consistency of quotation marks used throughout your code. It accepts three values: true, "single", and "double".
|
||||
"undef" : true, // Prohibits the use of explicitly undeclared variables.
|
||||
"unused" : true, // Warns when you define and never use your variables.
|
||||
"strict" : true, // Requires all functions to run in ECMAScript 5's strict mode.
|
||||
"trailing" : true, // Makes it an error to leave a trailing whitespace in your code.
|
||||
"maxlen" : 80, // Lets you set the maximum length of a line.
|
||||
"maxlen" : 120, // Lets you set the maximum length of a line.
|
||||
//"maxparams" : 4, // Lets you set the max number of formal parameters allowed per function.
|
||||
//"maxdepth" : 4, // Lets you control how nested do you want your blocks to be.
|
||||
//"maxstatements" : 4, // Lets you set the max number of statements allowed per function.
|
||||
@@ -59,7 +59,7 @@
|
||||
"shadow" : false, // Suppresses warnings about variable shadowing i.e. declaring a variable that had been already declared somewhere in the outer scope.
|
||||
"sub" : false, // Suppresses warnings about using [] notation when it can be expressed in dot notation.
|
||||
"supernew" : false, // Suppresses warnings about "weird" constructions like new function () { ... } and new Object;.
|
||||
"validthis" : true, // Suppresses warnings about possible strict violations when the code is running in strict mode and you use this in a non-constructor function.
|
||||
"validthis" : true, // Suppresses warnings about possible strict violations when the code is running in strict mode and you use this in a non-constructor function.
|
||||
"noyield" : false, // Suppresses warnings about generator functions with no yield statement in them.
|
||||
|
||||
|
||||
@@ -73,11 +73,11 @@
|
||||
// The rest should remain `false`. Please see explanation for the "predef" parameter below.
|
||||
"couch" : false, // Defines globals exposed by CouchDB.
|
||||
"dojo" : false, // Defines globals exposed by the Dojo Toolkit
|
||||
"jquery" : false, // Defines globals exposed by the jQuery JavaScript library.
|
||||
"jquery" : false, // Defines globals exposed by the jQuery JavaScript library.
|
||||
"mootools" : false, // Defines globals exposed by the MooTools JavaScript framework.
|
||||
"node" : false, // Defines globals available when your code is running inside of the Node runtime environment.
|
||||
"nonstandard" : false, // Defines non-standard but widely adopted globals such as escape and unescape.
|
||||
"phantom" : false, // Defines globals available when your core is running inside of the PhantomJS runtime environment.
|
||||
"phantom" : false, // Defines globals available when your core is running inside of the PhantomJS runtime environment.
|
||||
"prototypejs" : false, // Defines globals exposed by the Prototype JavaScript framework.
|
||||
"rhino" : false, // Defines globals available when your code is running inside of the Rhino runtime environment.
|
||||
"worker" : false, // Defines globals available when your code is running inside of a Web Worker.
|
||||
|
||||
5
AUTHORS
5
AUTHORS
@@ -169,4 +169,7 @@ Clinton Blackburn <cblackburn@edx.org>
|
||||
Dennis Jen <djen@edx.org>
|
||||
Filippo Valsorda <hi@filippo.io>
|
||||
Ivica Ceraj <ceraj@mit.edu>
|
||||
Jason Zhu <fmyzjs@gmail.com>
|
||||
Jason Zhu <fmyzjs@gmail.com>
|
||||
Marceau Cnudde <marceau.cnudde@gmail.com>
|
||||
Braden MacDonald <mail@bradenm.com>
|
||||
Jonathan Piacenti <kelketek@gmail.com>
|
||||
|
||||
@@ -5,6 +5,8 @@ These are notable changes in edx-platform. This is a rolling list of changes,
|
||||
in roughly chronological order, most recent first. Add your entries at or near
|
||||
the top. Include a label indicating the component affected.
|
||||
|
||||
LMS: Do not allow individual due dates to be earlier than the normal due date. LMS-6563
|
||||
|
||||
Blades: Course teams can turn off Chinese Caching from Studio. BLD-1207
|
||||
|
||||
LMS: Instructors can request and see content of previous bulk emails sent in the instructor dashboard.
|
||||
|
||||
@@ -17,60 +17,6 @@ Feature: CMS Transcripts
|
||||
# one stored on YouTube
|
||||
# t_not_exist - this file does not exist on YouTube; it exists locally
|
||||
|
||||
#1
|
||||
Scenario: Check input error messages
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
|
||||
#User inputs html5 links with equal extension
|
||||
And I enter a "123.webm" source to field number 1
|
||||
And I enter a "456.webm" source to field number 2
|
||||
Then I see error message "file_type"
|
||||
# Currently we are working with 2nd field. It means, that if 2nd field
|
||||
# contain incorrect value, 1st and 3rd fields should be disabled until
|
||||
# 2nd field will be filled by correct correct value
|
||||
And I expect 1, 3 inputs are disabled
|
||||
When I clear fields
|
||||
And I expect inputs are enabled
|
||||
|
||||
#User input URL with incorrect format
|
||||
And I enter a "http://link.c" source to field number 1
|
||||
Then I see error message "url_format"
|
||||
# Currently we are working with 1st field. It means, that if 1st field
|
||||
# contain incorrect value, 2nd and 3rd fields should be disabled until
|
||||
# 1st field will be filled by correct correct value
|
||||
And I expect 2, 3 inputs are disabled
|
||||
|
||||
#User input URL with incorrect format
|
||||
And I enter a "http://goo.gl/pxxZrg" source to field number 1
|
||||
And I enter a "http://goo.gl/pxxZrg" source to field number 2
|
||||
Then I see error message "links_duplication"
|
||||
And I expect 1, 3 inputs are disabled
|
||||
|
||||
And I clear fields
|
||||
And I expect inputs are enabled
|
||||
|
||||
And I enter a "http://youtu.be/t_not_exist" source to field number 1
|
||||
Then I do not see error message
|
||||
And I expect inputs are enabled
|
||||
|
||||
#2
|
||||
Scenario: Testing interaction with test youtube server
|
||||
Given I have created a Video component with subtitles
|
||||
And I edit the component
|
||||
# first part of url will be substituted by mock_youtube_server address
|
||||
# for t__eq_exist id server will respond with transcripts
|
||||
And I enter a "http://youtu.be/t__eq_exist" source to field number 1
|
||||
Then I see status message "not found on edx"
|
||||
# t__eq_exist subs locally not presented at this moment
|
||||
And I see button "import"
|
||||
|
||||
# for t_not_exist id server will respond with 404
|
||||
And I enter a "http://youtu.be/t_not_exist" source to field number 1
|
||||
Then I see status message "not found"
|
||||
And I do not see button "import"
|
||||
And I see button "disabled_download_to_edit"
|
||||
|
||||
#3
|
||||
Scenario: Youtube id only: check "not found" and "import" states
|
||||
Given I have created a Video component with subtitles
|
||||
@@ -93,66 +39,6 @@ Feature: CMS Transcripts
|
||||
And I see button "download_to_edit"
|
||||
And I see value "t__eq_exist" in the field "Default Timed Transcript"
|
||||
|
||||
#4
|
||||
Scenario: Youtube id only: check "Found" state
|
||||
Given I have created a Video component with subtitles "t_not_exist"
|
||||
And I edit the component
|
||||
|
||||
And I enter a "http://youtu.be/t_not_exist" source to field number 1
|
||||
Then I see status message "found"
|
||||
And I see value "t_not_exist" in the field "Default Timed Transcript"
|
||||
|
||||
#5
|
||||
Scenario: Youtube id only: check "Found" state when user sets youtube_id with local and server subs and they are equal
|
||||
|
||||
Given I have created a Video component with subtitles "t__eq_exist"
|
||||
And I edit the component
|
||||
|
||||
And I enter a "http://youtu.be/t__eq_exist" source to field number 1
|
||||
And I see status message "found"
|
||||
And I see value "t__eq_exist" in the field "Default Timed Transcript"
|
||||
|
||||
#6
|
||||
Scenario: Youtube id only: check "Found" state when user sets youtube_id with local and server subs and they are not equal
|
||||
Given I have created a Video component with subtitles "t_neq_exist"
|
||||
And I edit the component
|
||||
|
||||
And I enter a "http://youtu.be/t_neq_exist" source to field number 1
|
||||
And I see status message "replace"
|
||||
And I see button "replace"
|
||||
And I click transcript button "replace"
|
||||
And I see status message "found"
|
||||
And I see value "t_neq_exist" in the field "Default Timed Transcript"
|
||||
|
||||
#7
|
||||
Scenario: html5 source only: check "Not Found" state
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
|
||||
And I enter a "t_not_exist.mp4" source to field number 1
|
||||
Then I see status message "not found"
|
||||
And I see value "" in the field "Default Timed Transcript"
|
||||
|
||||
#8
|
||||
Scenario: html5 source only: check "Found" state
|
||||
Given I have created a Video component with subtitles "t_not_exist"
|
||||
And I edit the component
|
||||
|
||||
And I enter a "t_not_exist.mp4" source to field number 1
|
||||
Then I see status message "found"
|
||||
And I see value "t_not_exist" in the field "Default Timed Transcript"
|
||||
|
||||
#9
|
||||
Scenario: User sets youtube_id w/o server but with local subs and one html5 link w/o subs
|
||||
Given I have created a Video component with subtitles "t_not_exist"
|
||||
And I edit the component
|
||||
|
||||
And I enter a "http://youtu.be/t_not_exist" source to field number 1
|
||||
Then I see status message "found"
|
||||
|
||||
And I enter a "test_video_name.mp4" source to field number 2
|
||||
Then I see status message "found"
|
||||
And I see value "t_not_exist" in the field "Default Timed Transcript"
|
||||
|
||||
# Disabled 1/29/14 due to flakiness observed in master
|
||||
#10
|
||||
@@ -170,214 +56,7 @@ Feature: CMS Transcripts
|
||||
# Then I see status message "found"
|
||||
# And I see value "t__eq_exist" in the field "Default Timed Transcript"
|
||||
|
||||
#11
|
||||
Scenario: User sets youtube_id w/o local but with server subs and one html5 link w/o transcripts w/o import action, then another one html5 link w/o transcripts
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
|
||||
And I enter a "http://youtu.be/t__eq_exist" source to field number 1
|
||||
Then I see status message "not found on edx"
|
||||
And I see button "import"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
|
||||
And I enter a "t_not_exist.mp4" source to field number 2
|
||||
Then I see status message "not found on edx"
|
||||
And I see button "import"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
|
||||
And I enter a "t_not_exist.webm" source to field number 3
|
||||
Then I see status message "not found on edx"
|
||||
And I see button "import"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
|
||||
#12
|
||||
Scenario: Entering youtube (no importing), and 2 html5 sources without transcripts - "Not Found"
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
And I enter a "http://youtu.be/t_not_exist" source to field number 1
|
||||
Then I see status message "not found"
|
||||
And I see button "disabled_download_to_edit"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
And I enter a "t_not_exist.mp4" source to field number 2
|
||||
Then I see status message "not found"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
And I see button "disabled_download_to_edit"
|
||||
And I enter a "t_not_exist.webm" source to field number 3
|
||||
Then I see status message "not found"
|
||||
And I see button "disabled_download_to_edit"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
|
||||
#13
|
||||
Scenario: Entering youtube with imported transcripts, and 2 html5 sources without transcripts - "Found"
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
|
||||
And I enter a "http://youtu.be/t__eq_exist" source to field number 1
|
||||
Then I see status message "not found on edx"
|
||||
And I see button "import"
|
||||
And I click transcript button "import"
|
||||
Then I see status message "found"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
|
||||
And I enter a "t_not_exist.mp4" source to field number 2
|
||||
Then I see status message "found"
|
||||
And I see button "download_to_edit"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
|
||||
And I enter a "t_not_exist.webm" source to field number 3
|
||||
Then I see status message "found"
|
||||
And I see button "download_to_edit"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
|
||||
#14
|
||||
Scenario: Entering youtube w/o transcripts - html5 w/o transcripts - html5 with transcripts
|
||||
Given I have created a Video component with subtitles "t_neq_exist"
|
||||
And I edit the component
|
||||
|
||||
And I enter a "http://youtu.be/t_not_exist" source to field number 1
|
||||
Then I see status message "not found"
|
||||
And I see button "disabled_download_to_edit"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
|
||||
And I enter a "t_not_exist.mp4" source to field number 2
|
||||
Then I see status message "not found"
|
||||
And I see button "disabled_download_to_edit"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
|
||||
And I enter a "t_neq_exist.webm" source to field number 3
|
||||
Then I see status message "found"
|
||||
And I see button "download_to_edit"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
|
||||
#15
|
||||
Scenario: Entering youtube w/o imported transcripts - html5 w/o transcripts w/o import - html5 with transcripts
|
||||
Given I have created a Video component with subtitles "t_neq_exist"
|
||||
And I edit the component
|
||||
|
||||
And I enter a "http://youtu.be/t__eq_exist" source to field number 1
|
||||
Then I see status message "not found on edx"
|
||||
And I see button "import"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
|
||||
And I enter a "t_not_exist.mp4" source to field number 2
|
||||
Then I see status message "not found on edx"
|
||||
And I see button "import"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
|
||||
And I enter a "t_neq_exist.webm" source to field number 3
|
||||
Then I see status message "not found on edx"
|
||||
And I see button "import"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
|
||||
#16
|
||||
Scenario: Entering youtube w/o imported transcripts - html5 with transcripts - html5 w/o transcripts w/o import
|
||||
Given I have created a Video component with subtitles "t_neq_exist"
|
||||
And I edit the component
|
||||
|
||||
And I enter a "http://youtu.be/t__eq_exist" source to field number 1
|
||||
Then I see status message "not found on edx"
|
||||
And I see button "import"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
|
||||
And I enter a "t_neq_exist.mp4" source to field number 2
|
||||
Then I see status message "not found on edx"
|
||||
And I see button "import"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
|
||||
And I enter a "t_not_exist.webm" source to field number 3
|
||||
Then I see status message "not found on edx"
|
||||
And I see button "import"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
|
||||
#17
|
||||
Scenario: Entering youtube with imported transcripts - html5 with transcripts - html5 w/o transcripts
|
||||
Given I have created a Video component with subtitles "t_neq_exist"
|
||||
And I edit the component
|
||||
|
||||
And I enter a "http://youtu.be/t__eq_exist" source to field number 1
|
||||
Then I see status message "not found on edx"
|
||||
And I see button "import"
|
||||
And I click transcript button "import"
|
||||
Then I see status message "found"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
|
||||
And I enter a "t_neq_exist.mp4" source to field number 2
|
||||
Then I see status message "found"
|
||||
And I see button "download_to_edit"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
|
||||
And I enter a "t_not_exist.webm" source to field number 3
|
||||
Then I see status message "found"
|
||||
And I see button "download_to_edit"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
|
||||
#18
|
||||
Scenario: Entering youtube with imported transcripts - html5 w/o transcripts - html5 with transcripts
|
||||
Given I have created a Video component with subtitles "t_neq_exist"
|
||||
And I edit the component
|
||||
|
||||
And I enter a "http://youtu.be/t__eq_exist" source to field number 1
|
||||
Then I see status message "not found on edx"
|
||||
And I see button "import"
|
||||
And I click transcript button "import"
|
||||
Then I see status message "found"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
|
||||
And I enter a "t_not_exist.mp4" source to field number 2
|
||||
Then I see status message "found"
|
||||
And I see button "download_to_edit"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
|
||||
And I enter a "t_neq_exist.webm" source to field number 3
|
||||
Then I see status message "found"
|
||||
And I see button "download_to_edit"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
|
||||
#19
|
||||
Scenario: Entering html5 with transcripts - upload - youtube w/o transcripts
|
||||
Given I have created a Video component with subtitles "t__eq_exist"
|
||||
And I edit the component
|
||||
|
||||
And I enter a "t__eq_exist.mp4" source to field number 1
|
||||
Then I see status message "found"
|
||||
And I see button "download_to_edit"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
And I upload the transcripts file "uk_transcripts.srt"
|
||||
Then I see status message "uploaded_successfully"
|
||||
And I see button "download_to_edit"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
And I see value "t__eq_exist" in the field "Default Timed Transcript"
|
||||
|
||||
And I enter a "http://youtu.be/t_not_exist" source to field number 2
|
||||
Then I see status message "found"
|
||||
And I see button "download_to_edit"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
|
||||
And I enter a "uk_transcripts.webm" source to field number 3
|
||||
Then I see status message "found"
|
||||
|
||||
#20
|
||||
Scenario: Enter 2 HTML5 sources with transcripts, they are not the same, choose
|
||||
Given I have created a Video component with subtitles "t_not_exist"
|
||||
And I edit the component
|
||||
|
||||
And I enter a "uk_transcripts.mp4" source to field number 1
|
||||
Then I see status message "not found"
|
||||
And I see button "download_to_edit"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
And I upload the transcripts file "uk_transcripts.srt"
|
||||
Then I see status message "uploaded_successfully"
|
||||
And I see value "uk_transcripts" in the field "Default Timed Transcript"
|
||||
|
||||
And I enter a "t_not_exist.webm" source to field number 2
|
||||
Then I see status message "replace"
|
||||
|
||||
And I see choose button "uk_transcripts.mp4" number 1
|
||||
And I see choose button "t_not_exist.webm" number 2
|
||||
And I click transcript button "choose" number 2
|
||||
And I see value "uk_transcripts|t_not_exist" in the field "Default Timed Transcript"
|
||||
|
||||
# Flaky test fails occasionally in master. https://edx-wiki.atlassian.net/browse/BLD-927
|
||||
# Flaky test fails occasionally in master. https://openedx.atlassian.net/browse/BLD-892
|
||||
#21
|
||||
#Scenario: Work with 1 field only: Enter HTML5 source with transcripts - save - > change it to another one HTML5 source w/o #transcripts - click on use existing - > change it to another one HTML5 source w/o transcripts - click on use existing
|
||||
# Given I have created a Video component with subtitles "t_not_exist"
|
||||
@@ -404,87 +83,6 @@ Feature: CMS Transcripts
|
||||
# And I click transcript button "use_existing"
|
||||
# And I see value "video_name_3" in the field "Default Timed Transcript"
|
||||
|
||||
#22
|
||||
Scenario: Work with 1 field only: Enter HTML5 source with transcripts - save -> change it to another one HTML5 source w/o transcripts - click on use existing -> change it to another one HTML5 source w/o transcripts - do not click on use existing -> change it to another one HTML5 source w/o transcripts - click on use existing
|
||||
Given I have created a Video component with subtitles "t_not_exist"
|
||||
And I edit the component
|
||||
|
||||
And I enter a "t_not_exist.mp4" source to field number 1
|
||||
Then I see status message "found"
|
||||
And I see button "download_to_edit"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
And I see value "t_not_exist" in the field "Default Timed Transcript"
|
||||
|
||||
And I save changes
|
||||
And I edit the component
|
||||
|
||||
And I enter a "video_name_2.mp4" source to field number 1
|
||||
Then I see status message "use existing"
|
||||
And I see button "use_existing"
|
||||
And I click transcript button "use_existing"
|
||||
And I see value "video_name_2" in the field "Default Timed Transcript"
|
||||
|
||||
And I enter a "video_name_3.mp4" source to field number 1
|
||||
Then I see status message "use existing"
|
||||
And I see button "use_existing"
|
||||
|
||||
And I enter a "video_name_4.mp4" source to field number 1
|
||||
Then I see status message "use existing"
|
||||
And I see button "use_existing"
|
||||
And I click transcript button "use_existing"
|
||||
And I see value "video_name_4" in the field "Default Timed Transcript"
|
||||
|
||||
#23
|
||||
Scenario: Work with 2 fields: Enter HTML5 source with transcripts - save -> change it to another one HTML5 source w/o transcripts - do not click on use existing -> add another one HTML5 source w/o transcripts - click on use existing
|
||||
Given I have created a Video component with subtitles "t_not_exist"
|
||||
And I edit the component
|
||||
|
||||
And I enter a "t_not_exist.mp4" source to field number 1
|
||||
Then I see status message "found"
|
||||
And I see button "download_to_edit"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
|
||||
And I save changes
|
||||
And I edit the component
|
||||
|
||||
And I enter a "video_name_2.mp4" source to field number 1
|
||||
Then I see status message "use existing"
|
||||
And I see button "use_existing"
|
||||
|
||||
And I enter a "video_name_3.webm" source to field number 2
|
||||
Then I see status message "use existing"
|
||||
And I see button "use_existing"
|
||||
And I click transcript button "use_existing"
|
||||
And I see value "video_name_2|video_name_3" in the field "Default Timed Transcript"
|
||||
|
||||
#24 Uploading subtitles with different file name than file
|
||||
Scenario: File name and name of subs are different
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
|
||||
And I enter a "video_name_1.mp4" source to field number 1
|
||||
And I see status message "not found"
|
||||
And I upload the transcripts file "uk_transcripts.srt"
|
||||
Then I see status message "uploaded_successfully"
|
||||
And I see value "video_name_1" in the field "Default Timed Transcript"
|
||||
|
||||
And I save changes
|
||||
Then when I view the video it does show the captions
|
||||
|
||||
And I edit the component
|
||||
Then I see status message "found"
|
||||
|
||||
#25
|
||||
# Video can have filled item.sub, but doesn't have subs file.
|
||||
# In this case, after changing this video by another one without subs
|
||||
# `Not found` message should appear ( not `use existing`).
|
||||
Scenario: Video w/o subs - another video w/o subs - Not found message
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
|
||||
And I enter a "video_name_1.mp4" source to field number 1
|
||||
Then I see status message "not found"
|
||||
|
||||
#26
|
||||
Scenario: Subtitles are copied for every html5 video source
|
||||
Given I have created a Video component
|
||||
|
||||
@@ -1,241 +0,0 @@
|
||||
@shard_1 @requires_stub_youtube
|
||||
Feature: CMS Video Component Editor
|
||||
As a course author, I want to be able to create video components
|
||||
|
||||
# 1
|
||||
Scenario: User can view Video metadata
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
Then I see the correct video settings and default values
|
||||
|
||||
# 2
|
||||
# Safari has trouble saving values on Sauce
|
||||
@skip_safari
|
||||
Scenario: User can modify Video display name
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
Then I can modify video display name
|
||||
And my video display name change is persisted on save
|
||||
|
||||
# 3
|
||||
# Sauce Labs cannot delete cookies
|
||||
@skip_sauce
|
||||
Scenario: Captions are hidden when "transcript display" is false
|
||||
Given I have created a Video component with subtitles
|
||||
And I have set "transcript display" to False
|
||||
Then when I view the video it does not show the captions
|
||||
|
||||
# 4
|
||||
# Sauce Labs cannot delete cookies
|
||||
@skip_sauce
|
||||
Scenario: Captions are shown when "transcript display" is true
|
||||
Given I have created a Video component with subtitles
|
||||
And I have set "transcript display" to True
|
||||
Then when I view the video it does show the captions
|
||||
|
||||
# 5
|
||||
Scenario: Translations uploading works correctly
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I upload transcript file "chinese_transcripts.srt" for "zh" language code
|
||||
And I save changes
|
||||
Then when I view the video it does show the captions
|
||||
And I see "好 各位同学" text in the captions
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I see translations for "zh"
|
||||
And I upload transcript file "uk_transcripts.srt" for "uk" language code
|
||||
And I save changes
|
||||
Then when I view the video it does show the captions
|
||||
And I see "好 各位同学" text in the captions
|
||||
And video language menu has "uk, zh" translations
|
||||
|
||||
# 6
|
||||
Scenario: User can upload transcript file with > 1mb size
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I upload transcript file "1mb_transcripts.srt" for "uk" language code
|
||||
And I save changes
|
||||
Then when I view the video it does show the captions
|
||||
And I see "Привіт, edX вітає вас." text in the captions
|
||||
|
||||
# 7
|
||||
Scenario: Translations downloading works correctly w/ preliminary saving
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I upload transcript files:
|
||||
|lang_code|filename |
|
||||
|uk |uk_transcripts.srt |
|
||||
|zh |chinese_transcripts.srt|
|
||||
And I save changes
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I see translations for "uk, zh"
|
||||
And video language menu has "uk, zh" translations
|
||||
Then I can download transcript for "zh" language code, that contains text "好 各位同学"
|
||||
And I can download transcript for "uk" language code, that contains text "Привіт, edX вітає вас."
|
||||
|
||||
# 8
|
||||
Scenario: Translations downloading works correctly w/o preliminary saving
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I upload transcript files:
|
||||
|lang_code|filename |
|
||||
|uk |uk_transcripts.srt |
|
||||
|zh |chinese_transcripts.srt|
|
||||
Then I can download transcript for "zh" language code, that contains text "好 各位同学"
|
||||
And I can download transcript for "uk" language code, that contains text "Привіт, edX вітає вас."
|
||||
|
||||
# 9
|
||||
Scenario: Translations removing works correctly w/ preliminary saving
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I upload transcript files:
|
||||
|lang_code|filename |
|
||||
|uk |uk_transcripts.srt |
|
||||
|zh |chinese_transcripts.srt|
|
||||
And I save changes
|
||||
Then when I view the video it does show the captions
|
||||
And I see "Привіт, edX вітає вас." text in the captions
|
||||
And video language menu has "uk, zh" translations
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I see translations for "uk, zh"
|
||||
Then I remove translation for "uk" language code
|
||||
And I save changes
|
||||
Then when I view the video it does show the captions
|
||||
And I see "好 各位同学" text in the captions
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I see translations for "zh"
|
||||
Then I remove translation for "zh" language code
|
||||
And I save changes
|
||||
Then when I view the video it does not show the captions
|
||||
|
||||
# 10
|
||||
Scenario: Translations removing works correctly w/o preliminary saving
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I upload transcript file "uk_transcripts.srt" for "uk" language code
|
||||
And I see translations for "uk"
|
||||
Then I remove translation for "uk" language code
|
||||
And I save changes
|
||||
Then when I view the video it does not show the captions
|
||||
|
||||
# 11
|
||||
Scenario: Translations clearing works correctly w/ preliminary saving
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I upload transcript files:
|
||||
|lang_code|filename |
|
||||
|uk |uk_transcripts.srt |
|
||||
|zh |chinese_transcripts.srt|
|
||||
And I save changes
|
||||
Then when I view the video it does show the captions
|
||||
And I see "Привіт, edX вітає вас." text in the captions
|
||||
And video language menu has "uk, zh" translations
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I see translations for "uk, zh"
|
||||
And I click button "Clear"
|
||||
And I save changes
|
||||
Then when I view the video it does not show the captions
|
||||
|
||||
# 12
|
||||
Scenario: Translations clearing works correctly w/o preliminary saving
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I upload transcript files:
|
||||
|lang_code|filename |
|
||||
|uk |uk_transcripts.srt |
|
||||
|zh |chinese_transcripts.srt|
|
||||
And I click button "Clear"
|
||||
And I save changes
|
||||
Then when I view the video it does not show the captions
|
||||
|
||||
# 13
|
||||
Scenario: User cannot upload translations in sjson format
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I click button "Add"
|
||||
And I choose "uk" language code
|
||||
And I try to upload transcript file "uk_transcripts.sjson"
|
||||
Then I see validation error "Only SRT files can be uploaded. Please select a file ending in .srt to upload."
|
||||
|
||||
# 14
|
||||
Scenario: User can easy replace the translation by another one w/ preliminary saving
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I upload transcript file "chinese_transcripts.srt" for "zh" language code
|
||||
And I save changes
|
||||
Then when I view the video it does show the captions
|
||||
And I see "好 各位同学" text in the captions
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I see translations for "zh"
|
||||
And I replace transcript file for "zh" language code by "uk_transcripts.srt"
|
||||
And I save changes
|
||||
Then when I view the video it does show the captions
|
||||
And I see "Привіт, edX вітає вас." text in the captions
|
||||
|
||||
# 15
|
||||
Scenario: User can easy replace the translation by another one w/o preliminary saving
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I upload transcript file "chinese_transcripts.srt" for "zh" language code
|
||||
And I see translations for "zh"
|
||||
And I replace transcript file for "zh" language code by "uk_transcripts.srt"
|
||||
And I save changes
|
||||
Then when I view the video it does show the captions
|
||||
And I see "Привіт, edX вітає вас." text in the captions
|
||||
|
||||
# 16
|
||||
Scenario: Upload "zh" file "A" -> Remove "zh" -> Upload "zh" file "B"
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I upload transcript file "chinese_transcripts.srt" for "zh" language code
|
||||
And I see translations for "zh"
|
||||
Then I remove translation for "zh" language code
|
||||
And I upload transcript file "uk_transcripts.srt" for "zh" language code
|
||||
And I save changes
|
||||
Then when I view the video it does show the captions
|
||||
And I see "Привіт, edX вітає вас." text in the captions
|
||||
|
||||
# 17
|
||||
Scenario: User cannot select the same language twice
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I click button "Add"
|
||||
And I choose "zh" language code
|
||||
And I click button "Add"
|
||||
Then I cannot choose "zh" language code
|
||||
|
||||
# 18
|
||||
Scenario: User can see table of content at the first position
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I upload transcript files:
|
||||
|lang_code|filename |
|
||||
|uk |uk_transcripts.srt |
|
||||
|table |chinese_transcripts.srt|
|
||||
And I save changes
|
||||
Then when I view the video it does show the captions
|
||||
And I see "好 各位同学" text in the captions
|
||||
And video language menu has "table, uk" translations
|
||||
And I see video language with code "table" at position "0"
|
||||
|
||||
@@ -4,7 +4,10 @@ This file contains celery tasks for contentstore views
|
||||
|
||||
from celery.task import task
|
||||
from django.contrib.auth.models import User
|
||||
import json
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.course_module import CourseFields
|
||||
|
||||
from xmodule.modulestore.exceptions import DuplicateCourseError, ItemNotFoundError
|
||||
from course_action_state.models import CourseRerunState
|
||||
from contentstore.utils import initialize_permissions
|
||||
@@ -17,9 +20,10 @@ def rerun_course(source_course_key_string, destination_course_key_string, user_i
|
||||
Reruns a course in a new celery task.
|
||||
"""
|
||||
try:
|
||||
# deserialize the keys
|
||||
# deserialize the payload
|
||||
source_course_key = CourseKey.from_string(source_course_key_string)
|
||||
destination_course_key = CourseKey.from_string(destination_course_key_string)
|
||||
fields = deserialize_fields(fields) if fields else None
|
||||
|
||||
# use the split modulestore as the store for the rerun course,
|
||||
# as the Mongo modulestore doesn't support multiple runs of the same course.
|
||||
@@ -32,13 +36,11 @@ def rerun_course(source_course_key_string, destination_course_key_string, user_i
|
||||
|
||||
# update state: Succeeded
|
||||
CourseRerunState.objects.succeeded(course_key=destination_course_key)
|
||||
|
||||
return "succeeded"
|
||||
|
||||
except DuplicateCourseError as exc:
|
||||
# do NOT delete the original course, only update the status
|
||||
CourseRerunState.objects.failed(course_key=destination_course_key, exception=exc)
|
||||
|
||||
return "duplicate course"
|
||||
|
||||
# catch all exceptions so we can update the state and properly cleanup the course.
|
||||
@@ -54,3 +56,10 @@ def rerun_course(source_course_key_string, destination_course_key_string, user_i
|
||||
pass
|
||||
|
||||
return "exception: " + unicode(exc)
|
||||
|
||||
|
||||
def deserialize_fields(json_fields):
|
||||
fields = json.loads(json_fields)
|
||||
for field_name, value in fields.iteritems():
|
||||
fields[field_name] = getattr(CourseFields, field_name).from_json(value)
|
||||
return fields
|
||||
@@ -1,9 +1,15 @@
|
||||
"""
|
||||
Unit tests for cloning a course between the same and different module stores.
|
||||
"""
|
||||
import json
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore import ModuleStoreEnum, EdxJSONEncoder
|
||||
from contentstore.tests.utils import CourseTestCase
|
||||
from contentstore.tasks import rerun_course
|
||||
from contentstore.views.access import has_course_access
|
||||
from course_action_state.models import CourseRerunState
|
||||
from course_action_state.managers import CourseRerunUIStateManager
|
||||
from mock import patch, Mock
|
||||
|
||||
|
||||
class CloneCourseTest(CourseTestCase):
|
||||
@@ -28,14 +34,57 @@ class CloneCourseTest(CourseTestCase):
|
||||
# 3. clone course (mongo -> split)
|
||||
with self.store.default_store(ModuleStoreEnum.Type.split):
|
||||
split_course3_id = CourseLocator(
|
||||
org="edx3", course="split3", run="2013_Fall", branch=ModuleStoreEnum.BranchName.draft
|
||||
org="edx3", course="split3", run="2013_Fall"
|
||||
)
|
||||
self.store.clone_course(mongo_course2_id, split_course3_id, self.user.id)
|
||||
self.assertCoursesEqual(mongo_course2_id, split_course3_id)
|
||||
|
||||
# 4. clone course (split -> split)
|
||||
split_course4_id = CourseLocator(
|
||||
org="edx4", course="split4", run="2013_Fall", branch=ModuleStoreEnum.BranchName.draft
|
||||
org="edx4", course="split4", run="2013_Fall"
|
||||
)
|
||||
self.store.clone_course(split_course3_id, split_course4_id, self.user.id)
|
||||
self.assertCoursesEqual(split_course3_id, split_course4_id)
|
||||
|
||||
def test_rerun_course(self):
|
||||
"""
|
||||
Unit tests for :meth: `contentstore.tasks.rerun_course`
|
||||
"""
|
||||
mongo_course1_id = self.import_and_populate_course()
|
||||
|
||||
# rerun from mongo into split
|
||||
split_course3_id = CourseLocator(
|
||||
org="edx3", course="split3", run="rerun_test"
|
||||
)
|
||||
# Mark the action as initiated
|
||||
fields = {'display_name': 'rerun'}
|
||||
CourseRerunState.objects.initiated(mongo_course1_id, split_course3_id, self.user, fields['display_name'])
|
||||
result = rerun_course.delay(unicode(mongo_course1_id), unicode(split_course3_id), self.user.id,
|
||||
json.dumps(fields, cls=EdxJSONEncoder))
|
||||
self.assertEqual(result.get(), "succeeded")
|
||||
self.assertTrue(has_course_access(self.user, split_course3_id), "Didn't grant access")
|
||||
rerun_state = CourseRerunState.objects.find_first(course_key=split_course3_id)
|
||||
self.assertEqual(rerun_state.state, CourseRerunUIStateManager.State.SUCCEEDED)
|
||||
|
||||
# try creating rerunning again to same name and ensure it generates error
|
||||
result = rerun_course.delay(unicode(mongo_course1_id), unicode(split_course3_id), self.user.id)
|
||||
self.assertEqual(result.get(), "duplicate course")
|
||||
# the below will raise an exception if the record doesn't exist
|
||||
CourseRerunState.objects.find_first(
|
||||
course_key=split_course3_id,
|
||||
state=CourseRerunUIStateManager.State.FAILED
|
||||
)
|
||||
|
||||
# try to hit the generic exception catch
|
||||
with patch('xmodule.modulestore.split_mongo.mongo_connection.MongoConnection.insert_course_index', Mock(side_effect=Exception)):
|
||||
split_course4_id = CourseLocator(org="edx3", course="split3", run="rerun_fail")
|
||||
fields = {'display_name': 'total failure'}
|
||||
CourseRerunState.objects.initiated(split_course3_id, split_course4_id, self.user, fields['display_name'])
|
||||
result = rerun_course.delay(unicode(split_course3_id), unicode(split_course4_id), self.user.id,
|
||||
json.dumps(fields, cls=EdxJSONEncoder))
|
||||
self.assertIn("exception: ", result.get())
|
||||
self.assertIsNone(self.store.get_course(split_course4_id), "Didn't delete course after error")
|
||||
CourseRerunState.objects.find_first(
|
||||
course_key=split_course4_id,
|
||||
state=CourseRerunUIStateManager.State.FAILED
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import copy
|
||||
import mock
|
||||
import shutil
|
||||
import lxml
|
||||
|
||||
from datetime import timedelta
|
||||
from fs.osfs import OSFS
|
||||
@@ -47,6 +48,9 @@ from student.roles import CourseCreatorRole, CourseInstructorRole
|
||||
from opaque_keys import InvalidKeyError
|
||||
from contentstore.tests.utils import get_url
|
||||
from course_action_state.models import CourseRerunState, CourseRerunUIStateManager
|
||||
|
||||
from unittest import skipIf
|
||||
|
||||
from course_action_state.managers import CourseActionStateItemNotFoundError
|
||||
|
||||
|
||||
@@ -1197,7 +1201,7 @@ class ContentStoreTest(ContentStoreTestCase):
|
||||
resp = self._show_course_overview(course.id)
|
||||
self.assertContains(
|
||||
resp,
|
||||
'<article class="outline outline-course" data-locator="{locator}" data-course-key="{course_key}">'.format(
|
||||
'<article class="outline outline-complex outline-course" data-locator="{locator}" data-course-key="{course_key}">'.format(
|
||||
locator='i4x://MITx/999/course/Robot_Super_Course',
|
||||
course_key='MITx/999/Robot_Super_Course',
|
||||
),
|
||||
@@ -1580,31 +1584,33 @@ class RerunCourseTest(ContentStoreTestCase):
|
||||
json_resp = parse_json(response)
|
||||
self.assertNotIn('ErrMsg', json_resp)
|
||||
destination_course_key = CourseKey.from_string(json_resp['destination_course_key'])
|
||||
|
||||
return destination_course_key
|
||||
|
||||
def create_unsucceeded_course_action_html(self, course_key):
|
||||
"""Creates html fragment that is created for the given course_key in the unsucceeded course action section"""
|
||||
# TODO Update this once the Rerun UI LMS-11011 is implemented.
|
||||
return '<div class="unsucceeded-course-action" href="/course/{}"'.format(course_key)
|
||||
def get_course_listing_elements(self, html, course_key):
|
||||
"""Returns the elements in the course listing section of html that have the given course_key"""
|
||||
return html.cssselect('.course-item[data-course-key="{}"]'.format(unicode(course_key)))
|
||||
|
||||
def get_unsucceeded_course_action_elements(self, html, course_key):
|
||||
"""Returns the elements in the unsucceeded course action section that have the given course_key"""
|
||||
return html.cssselect('.courses-processing li[data-course-key="{}"]'.format(unicode(course_key)))
|
||||
|
||||
def assertInCourseListing(self, course_key):
|
||||
"""
|
||||
Asserts that the given course key is in the accessible course listing section of the html
|
||||
and NOT in the unsucceeded course action section of the html.
|
||||
"""
|
||||
course_listing_html = self.client.get_html('/course/')
|
||||
self.assertIn(course_key.run, course_listing_html.content)
|
||||
self.assertNotIn(self.create_unsucceeded_course_action_html(course_key), course_listing_html.content)
|
||||
course_listing = lxml.html.fromstring(self.client.get_html('/course/').content)
|
||||
self.assertEqual(len(self.get_course_listing_elements(course_listing, course_key)), 1)
|
||||
self.assertEqual(len(self.get_unsucceeded_course_action_elements(course_listing, course_key)), 0)
|
||||
|
||||
def assertInUnsucceededCourseActions(self, course_key):
|
||||
"""
|
||||
Asserts that the given course key is in the unsucceeded course action section of the html
|
||||
and NOT in the accessible course listing section of the html.
|
||||
"""
|
||||
course_listing_html = self.client.get_html('/course/')
|
||||
self.assertNotIn(course_key.run, course_listing_html.content)
|
||||
# TODO Verify the course is in the unsucceeded listing once LMS-11011 is implemented.
|
||||
course_listing = lxml.html.fromstring(self.client.get_html('/course/').content)
|
||||
self.assertEqual(len(self.get_course_listing_elements(course_listing, course_key)), 0)
|
||||
self.assertEqual(len(self.get_unsucceeded_course_action_elements(course_listing, course_key)), 1)
|
||||
|
||||
def test_rerun_course_success(self):
|
||||
|
||||
@@ -1615,6 +1621,7 @@ class RerunCourseTest(ContentStoreTestCase):
|
||||
rerun_state = CourseRerunState.objects.find_first(course_key=destination_course_key)
|
||||
expected_states = {
|
||||
'state': CourseRerunUIStateManager.State.SUCCEEDED,
|
||||
'display_name': self.destination_course_data['display_name'],
|
||||
'source_course_key': source_course.id,
|
||||
'course_key': destination_course_key,
|
||||
'should_display': True,
|
||||
@@ -1629,6 +1636,7 @@ class RerunCourseTest(ContentStoreTestCase):
|
||||
self.assertInCourseListing(source_course.id)
|
||||
self.assertInCourseListing(destination_course_key)
|
||||
|
||||
@skipIf(not settings.FEATURES.get('ALLOW_COURSE_RERUNS', False), "ALLOW_COURSE_RERUNS are not enabled")
|
||||
def test_rerun_course_fail_no_source_course(self):
|
||||
existent_course_key = CourseFactory.create().id
|
||||
non_existent_course_key = CourseLocator("org", "non_existent_course", "non_existent_run")
|
||||
@@ -1646,7 +1654,7 @@ class RerunCourseTest(ContentStoreTestCase):
|
||||
self.assertInCourseListing(existent_course_key)
|
||||
|
||||
# Verify that the failed course is NOT in the course listings
|
||||
self.assertInUnsucceededCourseActions(non_existent_course_key)
|
||||
self.assertInUnsucceededCourseActions(destination_course_key)
|
||||
|
||||
def test_rerun_course_fail_duplicate_course(self):
|
||||
existent_course_key = CourseFactory.create().id
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
"""
|
||||
Test view handler for rerun (and eventually create)
|
||||
"""
|
||||
from django.test.client import RequestFactory
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from student.roles import CourseInstructorRole, CourseStaffRole
|
||||
from student.tests.factories import UserFactory
|
||||
from contentstore.tests.utils import AjaxEnabledTestClient, parse_json
|
||||
from datetime import datetime
|
||||
from xmodule.course_module import CourseFields
|
||||
|
||||
|
||||
class TestCourseListing(ModuleStoreTestCase):
|
||||
"""
|
||||
Unit tests for getting the list of courses for a logged in user
|
||||
"""
|
||||
def setUp(self):
|
||||
"""
|
||||
Add a user and a course
|
||||
"""
|
||||
super(TestCourseListing, self).setUp()
|
||||
# create and log in a staff user.
|
||||
# create and log in a non-staff user
|
||||
self.user = UserFactory()
|
||||
self.factory = RequestFactory()
|
||||
self.client = AjaxEnabledTestClient()
|
||||
self.client.login(username=self.user.username, password='test')
|
||||
|
||||
source_course = CourseFactory.create(
|
||||
org='origin',
|
||||
number='the_beginning',
|
||||
run='first',
|
||||
display_name='the one and only',
|
||||
start=datetime.utcnow()
|
||||
)
|
||||
self.source_course_key = source_course.id
|
||||
|
||||
for role in [CourseInstructorRole, CourseStaffRole]:
|
||||
role(self.source_course_key).add_users(self.user)
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
Reverse the setup
|
||||
"""
|
||||
self.client.logout()
|
||||
ModuleStoreTestCase.tearDown(self)
|
||||
|
||||
def test_rerun(self):
|
||||
"""
|
||||
Just testing the functionality the view handler adds over the tasks tested in test_clone_course
|
||||
"""
|
||||
response = self.client.ajax_post('/course/', {
|
||||
'source_course_key': unicode(self.source_course_key),
|
||||
'org': self.source_course_key.org, 'course': self.source_course_key.course, 'run': 'copy',
|
||||
'display_name': 'not the same old name',
|
||||
})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = parse_json(response)
|
||||
dest_course_key = CourseKey.from_string(data['destination_course_key'])
|
||||
|
||||
self.assertEqual(dest_course_key.run, 'copy')
|
||||
dest_course = self.store.get_course(dest_course_key)
|
||||
self.assertEqual(dest_course.start, CourseFields.start.default)
|
||||
@@ -329,7 +329,9 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
|
||||
# simulate initiation of course actions
|
||||
for course in courses_in_progress:
|
||||
CourseRerunState.objects.initiated(sourse_course_key, destination_course_key=course.id, user=self.user)
|
||||
CourseRerunState.objects.initiated(
|
||||
sourse_course_key, destination_course_key=course.id, user=self.user, display_name="test course"
|
||||
)
|
||||
|
||||
# verify return values
|
||||
for method in (_accessible_courses_list_from_groups, _accessible_courses_list):
|
||||
|
||||
@@ -315,3 +315,111 @@ class ReleaseDateSourceTest(CourseTestCase):
|
||||
"""Tests a sequential's release date being set by itself"""
|
||||
self._update_release_dates(self.date_one, self.date_two, self.date_two)
|
||||
self._verify_release_date_source(self.sequential, self.sequential)
|
||||
|
||||
|
||||
class StaffLockTest(CourseTestCase):
|
||||
"""Base class for testing staff lock functions."""
|
||||
|
||||
def setUp(self):
|
||||
super(StaffLockTest, self).setUp()
|
||||
|
||||
self.chapter = ItemFactory.create(category='chapter', parent_location=self.course.location)
|
||||
self.sequential = ItemFactory.create(category='sequential', parent_location=self.chapter.location)
|
||||
self.vertical = ItemFactory.create(category='vertical', parent_location=self.sequential.location)
|
||||
self.orphan = ItemFactory.create(category='vertical', parent_location=self.sequential.location)
|
||||
|
||||
# Read again so that children lists are accurate
|
||||
self.chapter = self.store.get_item(self.chapter.location)
|
||||
self.sequential = self.store.get_item(self.sequential.location)
|
||||
self.vertical = self.store.get_item(self.vertical.location)
|
||||
|
||||
# Orphan the orphaned xblock
|
||||
self.sequential.children = [self.vertical.location]
|
||||
self.sequential = self.store.update_item(self.sequential, ModuleStoreEnum.UserID.test)
|
||||
|
||||
def _set_staff_lock(self, xblock, is_locked):
|
||||
"""If is_locked is True, xblock is staff locked. Otherwise, the xblock staff lock field is removed."""
|
||||
field = xblock.fields['visible_to_staff_only']
|
||||
if is_locked:
|
||||
field.write_to(xblock, True)
|
||||
else:
|
||||
field.delete_from(xblock)
|
||||
return self.store.update_item(xblock, ModuleStoreEnum.UserID.test)
|
||||
|
||||
def _update_staff_locks(self, chapter_locked, sequential_locked, vertical_locked):
|
||||
"""
|
||||
Sets the staff lock on the chapter, sequential, and vertical
|
||||
If the corresponding argument is False, then the field is deleted from the xblock
|
||||
"""
|
||||
self.chapter = self._set_staff_lock(self.chapter, chapter_locked)
|
||||
self.sequential = self._set_staff_lock(self.sequential, sequential_locked)
|
||||
self.vertical = self._set_staff_lock(self.vertical, vertical_locked)
|
||||
|
||||
|
||||
class StaffLockSourceTest(StaffLockTest):
|
||||
"""Tests for finding the source of an xblock's staff lock."""
|
||||
|
||||
def _verify_staff_lock_source(self, item, expected_source):
|
||||
"""Helper to verify that the staff lock source of a given item matches the expected source"""
|
||||
source = utils.find_staff_lock_source(item)
|
||||
self.assertEqual(source.location, expected_source.location)
|
||||
self.assertTrue(source.visible_to_staff_only)
|
||||
|
||||
def test_chapter_source_for_vertical(self):
|
||||
"""Tests a vertical's staff lock being set by its chapter"""
|
||||
self._update_staff_locks(True, False, False)
|
||||
self._verify_staff_lock_source(self.vertical, self.chapter)
|
||||
|
||||
def test_sequential_source_for_vertical(self):
|
||||
"""Tests a vertical's staff lock being set by its sequential"""
|
||||
self._update_staff_locks(True, True, False)
|
||||
self._verify_staff_lock_source(self.vertical, self.sequential)
|
||||
self._update_staff_locks(False, True, False)
|
||||
self._verify_staff_lock_source(self.vertical, self.sequential)
|
||||
|
||||
def test_vertical_source_for_vertical(self):
|
||||
"""Tests a vertical's staff lock being set by itself"""
|
||||
self._update_staff_locks(True, True, True)
|
||||
self._verify_staff_lock_source(self.vertical, self.vertical)
|
||||
self._update_staff_locks(False, True, True)
|
||||
self._verify_staff_lock_source(self.vertical, self.vertical)
|
||||
self._update_staff_locks(False, False, True)
|
||||
self._verify_staff_lock_source(self.vertical, self.vertical)
|
||||
|
||||
def test_orphan_has_no_source(self):
|
||||
"""Tests that a orphaned xblock has no staff lock source"""
|
||||
self.assertIsNone(utils.find_staff_lock_source(self.orphan))
|
||||
|
||||
def test_no_source_for_vertical(self):
|
||||
"""Tests a vertical with no staff lock set anywhere"""
|
||||
self._update_staff_locks(False, False, False)
|
||||
self.assertIsNone(utils.find_staff_lock_source(self.vertical))
|
||||
|
||||
|
||||
class InheritedStaffLockTest(StaffLockTest):
|
||||
"""Tests for determining if an xblock inherits a staff lock."""
|
||||
|
||||
def test_no_inheritance(self):
|
||||
"""Tests that a locked or unlocked vertical with no locked ancestors does not have an inherited lock"""
|
||||
self._update_staff_locks(False, False, False)
|
||||
self.assertFalse(utils.ancestor_has_staff_lock(self.vertical))
|
||||
self._update_staff_locks(False, False, True)
|
||||
self.assertFalse(utils.ancestor_has_staff_lock(self.vertical))
|
||||
|
||||
def test_inheritance_in_locked_section(self):
|
||||
"""Tests that a locked or unlocked vertical in a locked section has an inherited lock"""
|
||||
self._update_staff_locks(True, False, False)
|
||||
self.assertTrue(utils.ancestor_has_staff_lock(self.vertical))
|
||||
self._update_staff_locks(True, False, True)
|
||||
self.assertTrue(utils.ancestor_has_staff_lock(self.vertical))
|
||||
|
||||
def test_inheritance_in_locked_subsection(self):
|
||||
"""Tests that a locked or unlocked vertical in a locked subsection has an inherited lock"""
|
||||
self._update_staff_locks(False, True, False)
|
||||
self.assertTrue(utils.ancestor_has_staff_lock(self.vertical))
|
||||
self._update_staff_locks(False, True, True)
|
||||
self.assertTrue(utils.ancestor_has_staff_lock(self.vertical))
|
||||
|
||||
def test_no_inheritance_for_orphan(self):
|
||||
"""Tests that an orphaned xblock does not inherit staff lock"""
|
||||
self.assertFalse(utils.ancestor_has_staff_lock(self.orphan))
|
||||
|
||||
@@ -257,13 +257,14 @@ class CourseTestCase(ModuleStoreTestCase):
|
||||
)
|
||||
|
||||
for course1_item in course1_items:
|
||||
course2_item_location = course1_item.location.map_into_course(course2_id)
|
||||
if course1_item.location.category == 'course':
|
||||
course1_item_loc = course1_item.location
|
||||
course2_item_loc = course2_id.make_usage_key(course1_item_loc.block_type, course1_item_loc.block_id)
|
||||
if course1_item_loc.block_type == 'course':
|
||||
# mongo uses the run as the name, split uses 'course'
|
||||
store = self.store._get_modulestore_for_courseid(course2_id) # pylint: disable=protected-access
|
||||
new_name = 'course' if isinstance(store, SplitMongoModuleStore) else course2_item_location.run
|
||||
course2_item_location = course2_item_location.replace(name=new_name)
|
||||
course2_item = self.store.get_item(course2_item_location)
|
||||
new_name = 'course' if isinstance(store, SplitMongoModuleStore) else course2_item_loc.run
|
||||
course2_item_loc = course2_item_loc.replace(name=new_name)
|
||||
course2_item = self.store.get_item(course2_item_loc)
|
||||
|
||||
try:
|
||||
# compare published state
|
||||
@@ -278,7 +279,7 @@ class CourseTestCase(ModuleStoreTestCase):
|
||||
c1_state,
|
||||
c2_state,
|
||||
"Publish states not equal: course item {} in state {} != course item {} in state {}".format(
|
||||
course1_item.location, c1_state, course2_item.location, c2_state
|
||||
course1_item_loc, c1_state, course2_item.location, c2_state
|
||||
)
|
||||
)
|
||||
|
||||
@@ -296,11 +297,9 @@ class CourseTestCase(ModuleStoreTestCase):
|
||||
expected_children = []
|
||||
for course1_item_child in course1_item.children:
|
||||
expected_children.append(
|
||||
course1_item_child.map_into_course(course2_id)
|
||||
course2_id.make_usage_key(course1_item_child.block_type, course1_item_child.block_id)
|
||||
)
|
||||
# also process course2_children just in case they have version guids
|
||||
course2_children = [child.version_agnostic() for child in course2_item.children]
|
||||
self.assertEqual(expected_children, course2_children)
|
||||
self.assertEqual(expected_children, course2_item.children)
|
||||
|
||||
# compare assets
|
||||
content_store = self.store.contentstore
|
||||
|
||||
@@ -146,7 +146,7 @@ def get_lms_link_for_about_page(course_key):
|
||||
def course_image_url(course):
|
||||
"""Returns the image url for the course."""
|
||||
loc = StaticContent.compute_location(course.location.course_key, course.course_image)
|
||||
path = loc.to_deprecated_string()
|
||||
path = StaticContent.serialize_asset_key_with_slash(loc)
|
||||
return path
|
||||
|
||||
|
||||
@@ -197,6 +197,44 @@ def find_release_date_source(xblock):
|
||||
return find_release_date_source(parent)
|
||||
|
||||
|
||||
def find_staff_lock_source(xblock):
|
||||
"""
|
||||
Returns the xblock responsible for setting this xblock's staff lock, or None if the xblock is not staff locked.
|
||||
If this xblock is explicitly locked, return it, otherwise find the ancestor which sets this xblock's staff lock.
|
||||
"""
|
||||
|
||||
# Stop searching if this xblock has explicitly set its own staff lock
|
||||
if xblock.fields['visible_to_staff_only'].is_set_on(xblock):
|
||||
return xblock
|
||||
|
||||
# Stop searching at the section level
|
||||
if xblock.category == 'chapter':
|
||||
return None
|
||||
|
||||
parent_location = modulestore().get_parent_location(xblock.location,
|
||||
revision=ModuleStoreEnum.RevisionOption.draft_preferred)
|
||||
# Orphaned xblocks set their own staff lock
|
||||
if not parent_location:
|
||||
return None
|
||||
|
||||
parent = modulestore().get_item(parent_location)
|
||||
return find_staff_lock_source(parent)
|
||||
|
||||
|
||||
def ancestor_has_staff_lock(xblock, parent_xblock=None):
|
||||
"""
|
||||
Returns True iff one of xblock's ancestors has staff lock.
|
||||
Can avoid mongo query by passing in parent_xblock.
|
||||
"""
|
||||
if parent_xblock is None:
|
||||
parent_location = modulestore().get_parent_location(xblock.location,
|
||||
revision=ModuleStoreEnum.RevisionOption.draft_preferred)
|
||||
if not parent_location:
|
||||
return False
|
||||
parent_xblock = modulestore().get_item(parent_location)
|
||||
return parent_xblock.visible_to_staff_only
|
||||
|
||||
|
||||
def add_extra_panel_tab(tab_type, course):
|
||||
"""
|
||||
Used to add the panel tab to a course if it does not exist.
|
||||
|
||||
@@ -277,7 +277,7 @@ def _get_asset_json(display_name, date, location, thumbnail_location, locked):
|
||||
"""
|
||||
Helper method for formatting the asset information to send to client.
|
||||
"""
|
||||
asset_url = _add_slash(location.to_deprecated_string())
|
||||
asset_url = StaticContent.serialize_asset_key_with_slash(location)
|
||||
external_url = settings.LMS_BASE + asset_url
|
||||
return {
|
||||
'display_name': display_name,
|
||||
@@ -285,14 +285,8 @@ def _get_asset_json(display_name, date, location, thumbnail_location, locked):
|
||||
'url': asset_url,
|
||||
'external_url': external_url,
|
||||
'portable_url': StaticContent.get_static_path_from_location(location),
|
||||
'thumbnail': _add_slash(unicode(thumbnail_location)) if thumbnail_location else None,
|
||||
'thumbnail': StaticContent.serialize_asset_key_with_slash(thumbnail_location) if thumbnail_location else None,
|
||||
'locked': locked,
|
||||
# Needed for Backbone delete/update.
|
||||
'id': unicode(location)
|
||||
}
|
||||
|
||||
|
||||
def _add_slash(url):
|
||||
if not url.startswith('/'):
|
||||
url = '/' + url # TODO - re-address this once LMS-11198 is tackled.
|
||||
return url
|
||||
|
||||
@@ -5,29 +5,31 @@ import json
|
||||
import random
|
||||
import string # pylint: disable=W0402
|
||||
import logging
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
import django.utils
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
from django.conf import settings
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import HttpResponseBadRequest, HttpResponseNotFound, HttpResponse
|
||||
from util.json_request import JsonResponse
|
||||
from util.date_utils import get_default_time_display
|
||||
from edxmako.shortcuts import render_to_response
|
||||
|
||||
from xmodule.course_module import DEFAULT_START_DATE
|
||||
from xmodule.error_module import ErrorDescriptor
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.tabs import PDFTextbookTabs
|
||||
from xmodule.partitions.partitions import UserPartition, Group
|
||||
|
||||
from xmodule.modulestore import EdxJSONEncoder
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError, DuplicateCourseError
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.locations import Location
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
from contentstore.course_info_model import get_course_updates, update_course_updates, delete_course_update
|
||||
from contentstore.utils import (
|
||||
add_instructor,
|
||||
@@ -45,7 +47,6 @@ from models.settings.course_grading import CourseGradingModel
|
||||
from models.settings.course_metadata import CourseMetadata
|
||||
from util.json_request import expect_json
|
||||
from util.string_utils import _has_non_ascii_characters
|
||||
|
||||
from .access import has_course_access
|
||||
from .component import (
|
||||
OPEN_ENDED_COMPONENT_TYPES,
|
||||
@@ -54,10 +55,8 @@ from .component import (
|
||||
SPLIT_TEST_COMPONENT_TYPE,
|
||||
ADVANCED_COMPONENT_TYPES,
|
||||
)
|
||||
from .tasks import rerun_course
|
||||
from contentstore.tasks import rerun_course
|
||||
from .item import create_xblock_info
|
||||
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from course_creators.views import get_course_creator_status, add_user_with_status_unrequested
|
||||
from contentstore import utils
|
||||
from student.roles import (
|
||||
@@ -66,11 +65,12 @@ from student.roles import (
|
||||
from student import auth
|
||||
from course_action_state.models import CourseRerunState, CourseRerunUIStateManager
|
||||
from course_action_state.managers import CourseActionStateItemNotFoundError
|
||||
|
||||
from microsite_configuration import microsite
|
||||
from xmodule.course_module import CourseFields
|
||||
|
||||
|
||||
__all__ = ['course_info_handler', 'course_handler', 'course_info_update_handler',
|
||||
'course_rerun_handler',
|
||||
'settings_handler',
|
||||
'grading_handler',
|
||||
'advanced_settings_handler',
|
||||
@@ -232,6 +232,29 @@ def course_handler(request, course_key_string=None):
|
||||
return HttpResponseNotFound()
|
||||
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
@require_http_methods(["GET"])
|
||||
def course_rerun_handler(request, course_key_string):
|
||||
"""
|
||||
The restful handler for course reruns.
|
||||
GET
|
||||
html: return html page with form to rerun a course for the given course id
|
||||
"""
|
||||
# Only global staff (PMs) are able to rerun courses during the soft launch
|
||||
if not GlobalStaff().has_user(request.user):
|
||||
raise PermissionDenied()
|
||||
course_key = CourseKey.from_string(course_key_string)
|
||||
course_module = _get_course_module(course_key, request.user, depth=3)
|
||||
if request.method == 'GET':
|
||||
return render_to_response('course-create-rerun.html', {
|
||||
'source_course_key': course_key,
|
||||
'display_name': course_module.display_name,
|
||||
'user': request.user,
|
||||
'course_creator_status': _get_course_creator_status(request.user),
|
||||
'allow_unicode_course_id': settings.FEATURES.get('ALLOW_UNICODE_COURSE_ID', False)
|
||||
})
|
||||
|
||||
def _course_outline_json(request, course_module):
|
||||
"""
|
||||
Returns a JSON representation of the course module and recursively all of its children.
|
||||
@@ -263,14 +286,14 @@ def _accessible_courses_list(request):
|
||||
return has_course_access(request.user, course.id)
|
||||
|
||||
courses = filter(course_filter, modulestore().get_courses())
|
||||
unsucceeded_course_actions = [
|
||||
in_process_course_actions = [
|
||||
course for course in
|
||||
CourseRerunState.objects.find_all(
|
||||
exclude_args={'state': CourseRerunUIStateManager.State.SUCCEEDED}, should_display=True
|
||||
)
|
||||
if has_course_access(request.user, course.course_key)
|
||||
]
|
||||
return courses, unsucceeded_course_actions
|
||||
return courses, in_process_course_actions
|
||||
|
||||
|
||||
def _accessible_courses_list_from_groups(request):
|
||||
@@ -278,7 +301,7 @@ def _accessible_courses_list_from_groups(request):
|
||||
List all courses available to the logged in user by reversing access group names
|
||||
"""
|
||||
courses_list = {}
|
||||
unsucceeded_course_actions = []
|
||||
in_process_course_actions = []
|
||||
|
||||
instructor_courses = UserBasedRole(request.user, CourseInstructorRole.ROLE).courses_with_role()
|
||||
staff_courses = UserBasedRole(request.user, CourseStaffRole.ROLE).courses_with_role()
|
||||
@@ -291,7 +314,7 @@ def _accessible_courses_list_from_groups(request):
|
||||
raise AccessListFallback
|
||||
if course_key not in courses_list:
|
||||
# check for any course action state for this course
|
||||
unsucceeded_course_actions.extend(
|
||||
in_process_course_actions.extend(
|
||||
CourseRerunState.objects.find_all(
|
||||
exclude_args={'state': CourseRerunUIStateManager.State.SUCCEEDED},
|
||||
should_display=True,
|
||||
@@ -308,7 +331,7 @@ def _accessible_courses_list_from_groups(request):
|
||||
# ignore deleted or errored courses
|
||||
courses_list[course_key] = course
|
||||
|
||||
return courses_list.values(), unsucceeded_course_actions
|
||||
return courses_list.values(), in_process_course_actions
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -321,46 +344,75 @@ def course_listing(request):
|
||||
"""
|
||||
if GlobalStaff().has_user(request.user):
|
||||
# user has global access so no need to get courses from django groups
|
||||
courses, unsucceeded_course_actions = _accessible_courses_list(request)
|
||||
courses, in_process_course_actions = _accessible_courses_list(request)
|
||||
else:
|
||||
try:
|
||||
courses, unsucceeded_course_actions = _accessible_courses_list_from_groups(request)
|
||||
courses, in_process_course_actions = _accessible_courses_list_from_groups(request)
|
||||
except AccessListFallback:
|
||||
# user have some old groups or there was some error getting courses from django groups
|
||||
# so fallback to iterating through all courses
|
||||
courses, unsucceeded_course_actions = _accessible_courses_list(request)
|
||||
courses, in_process_course_actions = _accessible_courses_list(request)
|
||||
|
||||
def format_course_for_view(course):
|
||||
"""
|
||||
return tuple of the data which the view requires for each course
|
||||
Return a dict of the data which the view requires for each course
|
||||
"""
|
||||
return (
|
||||
course.display_name,
|
||||
reverse_course_url('course_handler', course.id),
|
||||
get_lms_link_for_item(course.location),
|
||||
course.display_org_with_default,
|
||||
course.display_number_with_default,
|
||||
course.location.name
|
||||
)
|
||||
return {
|
||||
'display_name': course.display_name,
|
||||
'course_key': unicode(course.location.course_key),
|
||||
'url': reverse_course_url('course_handler', course.id),
|
||||
'lms_link': get_lms_link_for_item(course.location),
|
||||
'rerun_link': _get_rerun_link_for_item(course.id),
|
||||
'org': course.display_org_with_default,
|
||||
'number': course.display_number_with_default,
|
||||
'run': course.location.run
|
||||
}
|
||||
|
||||
# remove any courses in courses that are also in the unsucceeded_course_actions list
|
||||
unsucceeded_action_course_keys = [uca.course_key for uca in unsucceeded_course_actions]
|
||||
def format_in_process_course_view(uca):
|
||||
"""
|
||||
Return a dict of the data which the view requires for each unsucceeded course
|
||||
"""
|
||||
return {
|
||||
'display_name': uca.display_name,
|
||||
'course_key': unicode(uca.course_key),
|
||||
'org': uca.course_key.org,
|
||||
'number': uca.course_key.course,
|
||||
'run': uca.course_key.run,
|
||||
'is_failed': True if uca.state == CourseRerunUIStateManager.State.FAILED else False,
|
||||
'is_in_progress': True if uca.state == CourseRerunUIStateManager.State.IN_PROGRESS else False,
|
||||
'dismiss_link':
|
||||
reverse_course_url('course_notifications_handler', uca.course_key, kwargs={
|
||||
'action_state_id': uca.id,
|
||||
}) if uca.state == CourseRerunUIStateManager.State.FAILED else ''
|
||||
}
|
||||
|
||||
# remove any courses in courses that are also in the in_process_course_actions list
|
||||
in_process_action_course_keys = [uca.course_key for uca in in_process_course_actions]
|
||||
courses = [
|
||||
format_course_for_view(c)
|
||||
for c in courses
|
||||
if not isinstance(c, ErrorDescriptor) and (c.id not in unsucceeded_action_course_keys)
|
||||
if not isinstance(c, ErrorDescriptor) and (c.id not in in_process_action_course_keys)
|
||||
]
|
||||
|
||||
in_process_course_actions = [format_in_process_course_view(uca) for uca in in_process_course_actions]
|
||||
|
||||
return render_to_response('index.html', {
|
||||
'courses': courses,
|
||||
'unsucceeded_course_actions': unsucceeded_course_actions,
|
||||
'in_process_course_actions': in_process_course_actions,
|
||||
'user': request.user,
|
||||
'request_course_creator_url': reverse('contentstore.views.request_course_creator'),
|
||||
'course_creator_status': _get_course_creator_status(request.user),
|
||||
'allow_unicode_course_id': settings.FEATURES.get('ALLOW_UNICODE_COURSE_ID', False)
|
||||
'rerun_creator_status': GlobalStaff().has_user(request.user),
|
||||
'allow_unicode_course_id': settings.FEATURES.get('ALLOW_UNICODE_COURSE_ID', False),
|
||||
'allow_course_reruns': settings.FEATURES.get('ALLOW_COURSE_RERUNS', False)
|
||||
})
|
||||
|
||||
|
||||
def _get_rerun_link_for_item(course_key):
|
||||
""" Returns the rerun link for the given course key. """
|
||||
return reverse_course_url('course_rerun_handler', course_key)
|
||||
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def course_index(request, course_key):
|
||||
@@ -376,6 +428,8 @@ def course_index(request, course_key):
|
||||
sections = course_module.get_children()
|
||||
course_structure = _course_outline_json(request, course_module)
|
||||
locator_to_show = request.REQUEST.get('show', None)
|
||||
course_release_date = get_default_time_display(course_module.start) if course_module.start != DEFAULT_START_DATE else _("Unscheduled")
|
||||
settings_url = reverse_course_url('settings_handler', course_key)
|
||||
|
||||
try:
|
||||
current_action = CourseRerunState.objects.find_first(course_key=course_key, should_display=True)
|
||||
@@ -392,6 +446,12 @@ def course_index(request, course_key):
|
||||
CourseGradingModel.fetch(course_key).graders
|
||||
),
|
||||
'rerun_notification_id': current_action.id if current_action else None,
|
||||
'course_release_date': course_release_date,
|
||||
'settings_url': settings_url,
|
||||
'notification_dismiss_url':
|
||||
reverse_course_url('course_notifications_handler', current_action.course_key, kwargs={
|
||||
'action_state_id': current_action.id,
|
||||
}) if current_action else None,
|
||||
})
|
||||
|
||||
|
||||
@@ -448,24 +508,28 @@ def _create_or_rerun_course(request):
|
||||
|
||||
try:
|
||||
org = request.json.get('org')
|
||||
number = request.json.get('number')
|
||||
course = request.json.get('number', request.json.get('course'))
|
||||
display_name = request.json.get('display_name')
|
||||
# force the start date for reruns and allow us to override start via the client
|
||||
start = request.json.get('start', CourseFields.start.default)
|
||||
run = request.json.get('run')
|
||||
|
||||
# allow/disable unicode characters in course_id according to settings
|
||||
if not settings.FEATURES.get('ALLOW_UNICODE_COURSE_ID'):
|
||||
if _has_non_ascii_characters(org) or _has_non_ascii_characters(number) or _has_non_ascii_characters(run):
|
||||
if _has_non_ascii_characters(org) or _has_non_ascii_characters(course) or _has_non_ascii_characters(run):
|
||||
return JsonResponse(
|
||||
{'error': _('Special characters not allowed in organization, course number, and course run.')},
|
||||
status=400
|
||||
)
|
||||
|
||||
fields = {'display_name': display_name} if display_name is not None else {}
|
||||
fields = {'start': start}
|
||||
if display_name is not None:
|
||||
fields['display_name'] = display_name
|
||||
|
||||
if 'source_course_key' in request.json:
|
||||
return _rerun_course(request, org, number, run, fields)
|
||||
return _rerun_course(request, org, course, run, fields)
|
||||
else:
|
||||
return _create_new_course(request, org, number, run, fields)
|
||||
return _create_new_course(request, org, course, run, fields)
|
||||
|
||||
except DuplicateCourseError:
|
||||
return JsonResponse({
|
||||
@@ -539,7 +603,7 @@ def _rerun_course(request, org, number, run, fields):
|
||||
with store.default_store('split'):
|
||||
destination_course_key = store.make_course_key(org, number, run)
|
||||
|
||||
# verify org course and run don't already exist
|
||||
# verify org course and run don't already exist
|
||||
if store.has_course(destination_course_key, ignore_case=True):
|
||||
raise DuplicateCourseError(source_course_key, destination_course_key)
|
||||
|
||||
@@ -548,10 +612,11 @@ def _rerun_course(request, org, number, run, fields):
|
||||
add_instructor(destination_course_key, request.user, request.user)
|
||||
|
||||
# Mark the action as initiated
|
||||
CourseRerunState.objects.initiated(source_course_key, destination_course_key, request.user)
|
||||
CourseRerunState.objects.initiated(source_course_key, destination_course_key, request.user, fields['display_name'])
|
||||
|
||||
# Rerun the course as a new celery task
|
||||
rerun_course.delay(unicode(source_course_key), unicode(destination_course_key), request.user.id, fields)
|
||||
json_fields = json.dumps(fields, cls=EdxJSONEncoder)
|
||||
rerun_course.delay(unicode(source_course_key), unicode(destination_course_key), request.user.id, json_fields)
|
||||
|
||||
# Return course listing page
|
||||
return JsonResponse({
|
||||
@@ -1069,8 +1134,8 @@ class GroupConfiguration(object):
|
||||
"""
|
||||
if not self.configuration.get("name"):
|
||||
raise GroupConfigurationsValidationError(_("must have name of the configuration"))
|
||||
if len(self.configuration.get('groups', [])) < 2:
|
||||
raise GroupConfigurationsValidationError(_("must have at least two groups"))
|
||||
if len(self.configuration.get('groups', [])) < 1:
|
||||
raise GroupConfigurationsValidationError(_("must have at least one group"))
|
||||
|
||||
def generate_id(self, used_ids):
|
||||
"""
|
||||
|
||||
@@ -11,7 +11,7 @@ import json
|
||||
from collections import OrderedDict
|
||||
from functools import partial
|
||||
from static_replace import replace_static_urls
|
||||
from xmodule_modifiers import wrap_xblock
|
||||
from xmodule_modifiers import wrap_xblock, request_token
|
||||
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.contrib.auth.decorators import login_required
|
||||
@@ -24,21 +24,22 @@ from xblock.fragment import Fragment
|
||||
|
||||
import xmodule
|
||||
from xmodule.tabs import StaticTab, CourseTabList
|
||||
from xmodule.modulestore import ModuleStoreEnum, PublishState
|
||||
from xmodule.modulestore import ModuleStoreEnum, PublishState, EdxJSONEncoder
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
|
||||
from xmodule.modulestore.inheritance import own_metadata
|
||||
from xmodule.modulestore.draft_and_published import DIRECT_ONLY_CATEGORIES
|
||||
from xmodule.x_module import PREVIEW_VIEWS, STUDIO_VIEW, STUDENT_VIEW
|
||||
|
||||
from xmodule.course_module import DEFAULT_START_DATE
|
||||
from contentstore.utils import find_release_date_source
|
||||
from django.contrib.auth.models import User
|
||||
from util.date_utils import get_default_time_display
|
||||
|
||||
from util.json_request import expect_json, JsonResponse
|
||||
|
||||
from .access import has_course_access
|
||||
from contentstore.utils import is_currently_visible_to_students
|
||||
from contentstore.utils import find_release_date_source, find_staff_lock_source, is_currently_visible_to_students, \
|
||||
ancestor_has_staff_lock
|
||||
from contentstore.views.helpers import is_unit, xblock_studio_url, xblock_primary_child_category, \
|
||||
xblock_type_display_name, get_parent_xblock
|
||||
from contentstore.views.preview import get_preview_fragment
|
||||
@@ -180,6 +181,7 @@ def xblock_handler(request, usage_key_string):
|
||||
content_type="text/plain"
|
||||
)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@require_http_methods(("GET"))
|
||||
@login_required
|
||||
@@ -206,7 +208,12 @@ def xblock_view_handler(request, usage_key_string, view_name):
|
||||
|
||||
# wrap the generated fragment in the xmodule_editor div so that the javascript
|
||||
# can bind to it correctly
|
||||
xblock.runtime.wrappers.append(partial(wrap_xblock, 'StudioRuntime', usage_id_serializer=unicode))
|
||||
xblock.runtime.wrappers.append(partial(
|
||||
wrap_xblock,
|
||||
'StudioRuntime',
|
||||
usage_id_serializer=unicode,
|
||||
request_token=request_token(request),
|
||||
))
|
||||
|
||||
if view_name == STUDIO_VIEW:
|
||||
try:
|
||||
@@ -376,10 +383,10 @@ def _save_xblock(user, xblock, data=None, children=None, metadata=None, nullout=
|
||||
if grader_type is not None:
|
||||
result.update(CourseGradingModel.update_section_grader_type(xblock, grader_type, user))
|
||||
|
||||
# If publish is set to 'republish' and this item has previously been published, then this
|
||||
# new item should be republished. This is used by staff locking to ensure that changing the draft
|
||||
# value of the staff lock will also update the published version.
|
||||
if publish == 'republish':
|
||||
# If publish is set to 'republish' and this item is not in direct only categories and has previously been published,
|
||||
# then this item should be republished. This is used by staff locking to ensure that changing the draft
|
||||
# value of the staff lock will also update the published version, but only at the unit level.
|
||||
if publish == 'republish' and xblock.category not in DIRECT_ONLY_CATEGORIES:
|
||||
published = modulestore().compute_publish_state(xblock) != PublishState.private
|
||||
if published:
|
||||
publish = 'make_public'
|
||||
@@ -390,7 +397,7 @@ def _save_xblock(user, xblock, data=None, children=None, metadata=None, nullout=
|
||||
modulestore().publish(xblock.location, user.id)
|
||||
|
||||
# Note that children aren't being returned until we have a use case.
|
||||
return JsonResponse(result)
|
||||
return JsonResponse(result, encoder=EdxJSONEncoder)
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -443,7 +450,7 @@ def _create_item(request):
|
||||
# if we add one then we need to also add it to the policy information (i.e. metadata)
|
||||
# we should remove this once we can break this reference from the course to static tabs
|
||||
if category == 'static_tab':
|
||||
display_name = display_name or _("Empty") # Prevent name being None
|
||||
display_name = display_name or _("Empty") # Prevent name being None
|
||||
course = store.get_course(dest_usage_key.course_key)
|
||||
course.tabs.append(
|
||||
StaticTab(
|
||||
@@ -629,7 +636,7 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
|
||||
return None
|
||||
|
||||
is_xblock_unit = is_unit(xblock, parent_xblock)
|
||||
is_unit_with_changes = is_xblock_unit and modulestore().has_changes(xblock)
|
||||
has_changes = modulestore().has_changes(xblock)
|
||||
|
||||
if graders is None:
|
||||
graders = CourseGradingModel.fetch(xblock.location.course_key).graders
|
||||
@@ -648,6 +655,10 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
|
||||
|
||||
# Treat DEFAULT_START_DATE as a magic number that means the release date has not been set
|
||||
release_date = get_default_time_display(xblock.start) if xblock.start != DEFAULT_START_DATE else None
|
||||
if xblock.category != 'course':
|
||||
visibility_state = _compute_visibility_state(xblock, child_info, is_xblock_unit and has_changes)
|
||||
else:
|
||||
visibility_state = None
|
||||
published = modulestore().compute_publish_state(xblock) != PublishState.private
|
||||
|
||||
xblock_info = {
|
||||
@@ -657,16 +668,18 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
|
||||
"edited_on": get_default_time_display(xblock.subtree_edited_on) if xblock.subtree_edited_on else None,
|
||||
"published": published,
|
||||
"published_on": get_default_time_display(xblock.published_date) if xblock.published_date else None,
|
||||
'studio_url': xblock_studio_url(xblock, parent_xblock),
|
||||
"studio_url": xblock_studio_url(xblock, parent_xblock),
|
||||
"released_to_students": datetime.now(UTC) > xblock.start,
|
||||
"release_date": release_date,
|
||||
"visibility_state": _compute_visibility_state(xblock, child_info, is_unit_with_changes) if not xblock.category == 'course' else None,
|
||||
"visibility_state": visibility_state,
|
||||
"has_explicit_staff_lock": xblock.fields['visible_to_staff_only'].is_set_on(xblock),
|
||||
"start": xblock.fields['start'].to_json(xblock.start),
|
||||
"graded": xblock.graded,
|
||||
"due_date": get_default_time_display(xblock.due),
|
||||
"due": xblock.fields['due'].to_json(xblock.due),
|
||||
"format": xblock.format,
|
||||
"course_graders": json.dumps([grader.get('type') for grader in graders]),
|
||||
"has_changes": has_changes,
|
||||
}
|
||||
if data is not None:
|
||||
xblock_info["data"] = data
|
||||
@@ -676,16 +689,31 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
|
||||
xblock_info['ancestor_info'] = _create_xblock_ancestor_info(xblock, course_outline)
|
||||
if child_info:
|
||||
xblock_info['child_info'] = child_info
|
||||
# Currently, 'edited_by', 'published_by', and 'release_date_from', and 'has_changes' are only used by the
|
||||
if visibility_state == VisibilityState.staff_only:
|
||||
xblock_info["ancestor_has_staff_lock"] = ancestor_has_staff_lock(xblock, parent_xblock)
|
||||
else:
|
||||
xblock_info["ancestor_has_staff_lock"] = False
|
||||
|
||||
# Currently, 'edited_by', 'published_by', and 'release_date_from' are only used by the
|
||||
# container page when rendering a unit. Since they are expensive to compute, only include them for units
|
||||
# that are not being rendered on the course outline.
|
||||
if is_xblock_unit and not course_outline:
|
||||
xblock_info["edited_by"] = safe_get_username(xblock.subtree_edited_by)
|
||||
xblock_info["published_by"] = safe_get_username(xblock.published_by)
|
||||
xblock_info["currently_visible_to_students"] = is_currently_visible_to_students(xblock)
|
||||
xblock_info['has_changes'] = is_unit_with_changes
|
||||
if release_date:
|
||||
xblock_info["release_date_from"] = _get_release_date_from(xblock)
|
||||
if visibility_state == VisibilityState.staff_only:
|
||||
xblock_info["staff_lock_from"] = _get_staff_lock_from(xblock)
|
||||
else:
|
||||
xblock_info["staff_lock_from"] = None
|
||||
if course_outline:
|
||||
if xblock_info["has_explicit_staff_lock"]:
|
||||
xblock_info["staff_only_message"] = True
|
||||
elif child_info and child_info["children"]:
|
||||
xblock_info["staff_only_message"] = all([child["staff_only_message"] for child in child_info["children"]])
|
||||
else:
|
||||
xblock_info["staff_only_message"] = False
|
||||
|
||||
return xblock_info
|
||||
|
||||
@@ -813,9 +841,21 @@ def _get_release_date_from(xblock):
|
||||
"""
|
||||
Returns a string representation of the section or subsection that sets the xblock's release date
|
||||
"""
|
||||
source = find_release_date_source(xblock)
|
||||
# Translators: this will be a part of the release date message.
|
||||
# For example, 'Released: Jul 02, 2014 at 4:00 UTC with Section "Week 1"'
|
||||
return _xblock_type_and_display_name(find_release_date_source(xblock))
|
||||
|
||||
|
||||
def _get_staff_lock_from(xblock):
|
||||
"""
|
||||
Returns a string representation of the section or subsection that sets the xblock's release date
|
||||
"""
|
||||
source = find_staff_lock_source(xblock)
|
||||
return _xblock_type_and_display_name(source) if source else None
|
||||
|
||||
|
||||
def _xblock_type_and_display_name(xblock):
|
||||
"""
|
||||
Returns a string representation of the xblock's type and display name
|
||||
"""
|
||||
return _('{section_or_subsection} "{display_name}"').format(
|
||||
section_or_subsection=xblock_type_display_name(source),
|
||||
display_name=source.display_name_with_default)
|
||||
section_or_subsection=xblock_type_display_name(xblock),
|
||||
display_name=xblock.display_name_with_default)
|
||||
|
||||
@@ -9,7 +9,7 @@ from django.http import Http404, HttpResponseBadRequest
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from edxmako.shortcuts import render_to_string
|
||||
|
||||
from xmodule_modifiers import replace_static_urls, wrap_xblock, wrap_fragment
|
||||
from xmodule_modifiers import replace_static_urls, wrap_xblock, wrap_fragment, request_token
|
||||
from xmodule.x_module import PREVIEW_VIEWS, STUDENT_VIEW, AUTHOR_VIEW
|
||||
from xmodule.error_module import ErrorDescriptor
|
||||
from xmodule.exceptions import NotFoundError, ProcessingError
|
||||
@@ -123,7 +123,13 @@ def _preview_module_system(request, descriptor):
|
||||
|
||||
wrappers = [
|
||||
# This wrapper wraps the module in the template specified above
|
||||
partial(wrap_xblock, 'PreviewRuntime', display_name_only=display_name_only, usage_id_serializer=unicode),
|
||||
partial(
|
||||
wrap_xblock,
|
||||
'PreviewRuntime',
|
||||
display_name_only=display_name_only,
|
||||
usage_id_serializer=unicode,
|
||||
request_token=request_token(request)
|
||||
),
|
||||
|
||||
# This wrapper replaces urls in the output that start with /static
|
||||
# with the correct course-specific url for the static content
|
||||
|
||||
@@ -3,6 +3,7 @@ Unit tests for getting the list of courses and the course outline.
|
||||
"""
|
||||
import json
|
||||
import lxml
|
||||
import datetime
|
||||
|
||||
from contentstore.tests.utils import CourseTestCase
|
||||
from contentstore.utils import reverse_course_url, add_instructor
|
||||
@@ -10,6 +11,8 @@ from contentstore.views.access import has_course_access
|
||||
from contentstore.views.course import course_outline_initial_state
|
||||
from contentstore.views.item import create_xblock_info, VisibilityState
|
||||
from course_action_state.models import CourseRerunState
|
||||
from util.date_utils import get_default_time_display
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
@@ -273,3 +276,35 @@ class TestCourseOutline(CourseTestCase):
|
||||
expanded_locators = initial_state['expanded_locators']
|
||||
self.assertIn(unicode(self.sequential.location), expanded_locators)
|
||||
self.assertIn(unicode(self.vertical.location), expanded_locators)
|
||||
|
||||
def test_start_date_on_page(self):
|
||||
"""
|
||||
Verify that the course start date is included on the course outline page.
|
||||
"""
|
||||
def _get_release_date(response):
|
||||
"""Return the release date from the course page"""
|
||||
parsed_html = lxml.html.fromstring(response.content)
|
||||
return parsed_html.find_class('course-status')[0].find_class('status-release-value')[0].text_content()
|
||||
|
||||
def _assert_settings_link_present(response):
|
||||
"""
|
||||
Asserts there's a course settings link on the course page by the course release date.
|
||||
"""
|
||||
parsed_html = lxml.html.fromstring(response.content)
|
||||
settings_link = parsed_html.find_class('course-status')[0].find_class('action-edit')[0].find('a')
|
||||
self.assertIsNotNone(settings_link)
|
||||
self.assertEqual(settings_link.get('href'), reverse_course_url('settings_handler', self.course.id))
|
||||
|
||||
outline_url = reverse_course_url('course_handler', self.course.id)
|
||||
response = self.client.get(outline_url, {}, HTTP_ACCEPT='text/html')
|
||||
|
||||
# A course with the default release date should display as "Unscheduled"
|
||||
self.assertEqual(_get_release_date(response), 'Unscheduled')
|
||||
_assert_settings_link_present(response)
|
||||
|
||||
self.course.start = datetime.datetime(2014, 1, 1)
|
||||
modulestore().update_item(self.course, ModuleStoreEnum.UserID.test)
|
||||
response = self.client.get(outline_url, {}, HTTP_ACCEPT='text/html')
|
||||
|
||||
self.assertEqual(_get_release_date(response), get_default_time_display(self.course.start))
|
||||
_assert_settings_link_present(response)
|
||||
|
||||
@@ -7,7 +7,6 @@ from contentstore.utils import reverse_course_url, reverse_usage_url
|
||||
from contentstore.views.component import SPLIT_TEST_COMPONENT_TYPE
|
||||
from contentstore.views.course import GroupConfiguration
|
||||
from contentstore.tests.utils import CourseTestCase
|
||||
from util.testing import UrlResetMixin
|
||||
from xmodule.partitions.partitions import Group, UserPartition
|
||||
from xmodule.modulestore.tests.factories import ItemFactory
|
||||
from xmodule.split_test_module import ValidationMessage, ValidationMessageType
|
||||
@@ -121,13 +120,11 @@ class GroupConfigurationsBaseTestCase(object):
|
||||
{u'name': u'Group B'},
|
||||
],
|
||||
},
|
||||
# must have at least two groups
|
||||
# must have at least one group
|
||||
{
|
||||
u'name': u'Test name',
|
||||
u'description': u'Test description',
|
||||
u'groups': [
|
||||
{u'name': u'Group A'},
|
||||
],
|
||||
u'groups': [],
|
||||
},
|
||||
# an empty json
|
||||
{},
|
||||
@@ -167,11 +164,10 @@ class GroupConfigurationsBaseTestCase(object):
|
||||
|
||||
|
||||
# pylint: disable=no-member
|
||||
class GroupConfigurationsListHandlerTestCase(UrlResetMixin, CourseTestCase, GroupConfigurationsBaseTestCase, HelperMethods):
|
||||
class GroupConfigurationsListHandlerTestCase(CourseTestCase, GroupConfigurationsBaseTestCase, HelperMethods):
|
||||
"""
|
||||
Test cases for group_configurations_list_handler.
|
||||
"""
|
||||
@patch.dict("django.conf.settings.FEATURES", {"ENABLE_GROUP_CONFIGURATIONS": True})
|
||||
def setUp(self):
|
||||
"""
|
||||
Set up GroupConfigurationsListHandlerTestCase.
|
||||
@@ -263,14 +259,13 @@ class GroupConfigurationsListHandlerTestCase(UrlResetMixin, CourseTestCase, Grou
|
||||
|
||||
|
||||
# pylint: disable=no-member
|
||||
class GroupConfigurationsDetailHandlerTestCase(UrlResetMixin, CourseTestCase, GroupConfigurationsBaseTestCase, HelperMethods):
|
||||
class GroupConfigurationsDetailHandlerTestCase(CourseTestCase, GroupConfigurationsBaseTestCase, HelperMethods):
|
||||
"""
|
||||
Test cases for group_configurations_detail_handler.
|
||||
"""
|
||||
|
||||
ID = 0
|
||||
|
||||
@patch.dict("django.conf.settings.FEATURES", {"ENABLE_GROUP_CONFIGURATIONS": True})
|
||||
def setUp(self):
|
||||
"""
|
||||
Set up GroupConfigurationsDetailHandlerTestCase.
|
||||
@@ -422,12 +417,11 @@ class GroupConfigurationsDetailHandlerTestCase(UrlResetMixin, CourseTestCase, Gr
|
||||
|
||||
|
||||
# pylint: disable=no-member
|
||||
class GroupConfigurationsUsageInfoTestCase(UrlResetMixin, CourseTestCase, HelperMethods):
|
||||
class GroupConfigurationsUsageInfoTestCase(CourseTestCase, HelperMethods):
|
||||
"""
|
||||
Tests for usage information of configurations.
|
||||
"""
|
||||
|
||||
@patch.dict("django.conf.settings.FEATURES", {"ENABLE_GROUP_CONFIGURATIONS": True})
|
||||
def setUp(self):
|
||||
super(GroupConfigurationsUsageInfoTestCase, self).setUp()
|
||||
|
||||
@@ -544,7 +538,6 @@ class GroupConfigurationsValidationTestCase(CourseTestCase, HelperMethods):
|
||||
"""
|
||||
Tests for validation in Group Configurations.
|
||||
"""
|
||||
@patch.dict("django.conf.settings.FEATURES", {"ENABLE_GROUP_CONFIGURATIONS": True})
|
||||
def setUp(self):
|
||||
super(GroupConfigurationsValidationTestCase, self).setUp()
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"""Tests for items views."""
|
||||
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
import ddt
|
||||
from unittest import skipUnless
|
||||
|
||||
from mock import patch
|
||||
from pytz import UTC
|
||||
@@ -12,14 +13,14 @@ from django.http import Http404
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.core.urlresolvers import reverse
|
||||
from contentstore.utils import reverse_usage_url
|
||||
from contentstore.utils import reverse_usage_url, reverse_course_url
|
||||
from contentstore.views.preview import StudioUserService
|
||||
|
||||
from contentstore.views.component import (
|
||||
component_handler, get_component_templates
|
||||
)
|
||||
|
||||
from contentstore.views.item import create_xblock_info, ALWAYS, VisibilityState
|
||||
from contentstore.views.item import create_xblock_info, ALWAYS, VisibilityState, _xblock_type_and_display_name
|
||||
from contentstore.tests.utils import CourseTestCase
|
||||
from student.tests.factories import UserFactory
|
||||
from xmodule.capa_module import CapaDescriptor
|
||||
@@ -179,6 +180,53 @@ class GetItem(ItemTest):
|
||||
self.assertIn('Zooming', html)
|
||||
|
||||
|
||||
def test_split_test_edited(self):
|
||||
"""
|
||||
Test that rename of a group changes display name of child vertical.
|
||||
"""
|
||||
self.course.user_partitions = [UserPartition(
|
||||
0, 'first_partition', 'First Partition',
|
||||
[Group("0", 'alpha'), Group("1", 'beta')]
|
||||
)]
|
||||
self.store.update_item(self.course, self.user.id)
|
||||
root_usage_key = self._create_vertical()
|
||||
resp = self.create_xblock(category='split_test', parent_usage_key=root_usage_key)
|
||||
split_test_usage_key = self.response_usage_key(resp)
|
||||
self.client.ajax_post(
|
||||
reverse_usage_url("xblock_handler", split_test_usage_key),
|
||||
data={'metadata': {'user_partition_id': str(0)}}
|
||||
)
|
||||
html, __ = self._get_container_preview(split_test_usage_key)
|
||||
self.assertIn('alpha', html)
|
||||
self.assertIn('beta', html)
|
||||
|
||||
# Rename groups in group configuration
|
||||
GROUP_CONFIGURATION_JSON = {
|
||||
u'id': 0,
|
||||
u'name': u'first_partition',
|
||||
u'description': u'First Partition',
|
||||
u'version': 1,
|
||||
u'groups': [
|
||||
{u'id': 0, u'name': u'New_NAME_A', u'version': 1},
|
||||
{u'id': 1, u'name': u'New_NAME_B', u'version': 1},
|
||||
],
|
||||
}
|
||||
|
||||
response = self.client.put(
|
||||
reverse_course_url('group_configurations_detail_handler', self.course.id, kwargs={'group_configuration_id': 0}),
|
||||
data=json.dumps(GROUP_CONFIGURATION_JSON),
|
||||
content_type="application/json",
|
||||
HTTP_ACCEPT="application/json",
|
||||
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
|
||||
)
|
||||
self.assertEqual(response.status_code, 201)
|
||||
html, __ = self._get_container_preview(split_test_usage_key)
|
||||
self.assertNotIn('alpha', html)
|
||||
self.assertNotIn('beta', html)
|
||||
self.assertIn('New_NAME_A', html)
|
||||
self.assertIn('New_NAME_B', html)
|
||||
|
||||
|
||||
class DeleteItem(ItemTest):
|
||||
"""Tests for '/xblock' DELETE url."""
|
||||
def test_delete_static_page(self):
|
||||
@@ -259,7 +307,8 @@ class TestCreateItem(ItemTest):
|
||||
|
||||
# Check that its name is not None
|
||||
new_tab = self.get_item_from_modulestore(usage_key)
|
||||
self.assertEquals(new_tab.display_name, 'Empty')
|
||||
self.assertEquals(new_tab.display_name, 'Empty')
|
||||
|
||||
|
||||
class TestDuplicateItem(ItemTest):
|
||||
"""
|
||||
@@ -620,6 +669,20 @@ class TestEditItem(ItemTest):
|
||||
)
|
||||
self.assertEqual(published.display_name, new_display_name_2)
|
||||
|
||||
def test_direct_only_categories_not_republished(self):
|
||||
"""Verify that republish is ignored for items in DIRECT_ONLY_CATEGORIES"""
|
||||
# Create a vertical child with published and unpublished versions.
|
||||
# If the parent sequential is not re-published, then the child problem should also not be re-published.
|
||||
resp = self.create_xblock(parent_usage_key=self.seq_usage_key, display_name='vertical', category='vertical')
|
||||
vertical_usage_key = self.response_usage_key(resp)
|
||||
vertical_update_url = reverse_usage_url('xblock_handler', vertical_usage_key)
|
||||
self.client.ajax_post(vertical_update_url, data={'publish': 'make_public'})
|
||||
self.client.ajax_post(vertical_update_url, data={'metadata': {'display_name': 'New Display Name'}})
|
||||
|
||||
self._verify_published_with_draft(self.seq_usage_key)
|
||||
self.client.ajax_post(self.seq_update_url, data={'publish': 'republish'})
|
||||
self._verify_published_with_draft(self.seq_usage_key)
|
||||
|
||||
def _make_draft_content_different_from_published(self):
|
||||
"""
|
||||
Helper method to create different draft and published versions of a problem.
|
||||
@@ -806,8 +869,8 @@ class TestEditSplitModule(ItemTest):
|
||||
vertical_1 = self.get_item_from_modulestore(split_test.children[1], verify_is_draft=True)
|
||||
self.assertEqual("vertical", vertical_0.category)
|
||||
self.assertEqual("vertical", vertical_1.category)
|
||||
self.assertEqual("alpha", vertical_0.display_name)
|
||||
self.assertEqual("beta", vertical_1.display_name)
|
||||
self.assertEqual("Group ID 0", vertical_0.display_name)
|
||||
self.assertEqual("Group ID 1", vertical_1.display_name)
|
||||
|
||||
# Verify that the group_id_to_child mapping is correct.
|
||||
self.assertEqual(2, len(split_test.group_id_to_child))
|
||||
@@ -932,7 +995,7 @@ class TestEditSplitModule(ItemTest):
|
||||
|
||||
# group_id_to_child and children have not changed yet.
|
||||
split_test = self._assert_children(2)
|
||||
group_id_to_child = split_test.group_id_to_child
|
||||
group_id_to_child = split_test.group_id_to_child.copy()
|
||||
self.assertEqual(2, len(group_id_to_child))
|
||||
|
||||
# Test environment and Studio use different module systems
|
||||
@@ -1274,10 +1337,13 @@ class TestXBlockPublishingInfo(ItemTest):
|
||||
"""
|
||||
Creates a child xblock for the given parent.
|
||||
"""
|
||||
return ItemFactory.create(
|
||||
child = ItemFactory.create(
|
||||
parent_location=parent.location, category=category, display_name=display_name,
|
||||
user_id=self.user.id, publish_item=publish_item, visible_to_staff_only=staff_only
|
||||
user_id=self.user.id, publish_item=publish_item
|
||||
)
|
||||
if staff_only:
|
||||
self._enable_staff_only(child.location)
|
||||
return child
|
||||
|
||||
def _get_child_xblock_info(self, xblock_info, index):
|
||||
"""
|
||||
@@ -1297,6 +1363,17 @@ class TestXBlockPublishingInfo(ItemTest):
|
||||
include_children_predicate=ALWAYS,
|
||||
)
|
||||
|
||||
def _get_xblock_outline_info(self, location):
|
||||
"""
|
||||
Returns the xblock info for the specified location as neeeded for the course outline page.
|
||||
"""
|
||||
return create_xblock_info(
|
||||
modulestore().get_item(location),
|
||||
include_child_info=True,
|
||||
include_children_predicate=ALWAYS,
|
||||
course_outline=True
|
||||
)
|
||||
|
||||
def _set_release_date(self, location, start):
|
||||
"""
|
||||
Sets the release date for the specified xblock.
|
||||
@@ -1305,12 +1382,12 @@ class TestXBlockPublishingInfo(ItemTest):
|
||||
xblock.start = start
|
||||
self.store.update_item(xblock, self.user.id)
|
||||
|
||||
def _set_staff_only(self, location, staff_only):
|
||||
def _enable_staff_only(self, location):
|
||||
"""
|
||||
Sets staff only for the specified xblock.
|
||||
Enables staff only for the specified xblock.
|
||||
"""
|
||||
xblock = modulestore().get_item(location)
|
||||
xblock.visible_to_staff_only = staff_only
|
||||
xblock.visible_to_staff_only = True
|
||||
self.store.update_item(xblock, self.user.id)
|
||||
|
||||
def _set_display_name(self, location, display_name):
|
||||
@@ -1321,22 +1398,50 @@ class TestXBlockPublishingInfo(ItemTest):
|
||||
xblock.display_name = display_name
|
||||
self.store.update_item(xblock, self.user.id)
|
||||
|
||||
def _verify_visibility_state(self, xblock_info, expected_state, path=None):
|
||||
def _verify_xblock_info_state(self, xblock_info, xblock_info_field, expected_state, path=None, should_equal=True):
|
||||
"""
|
||||
Verify the publish state of an item in the xblock_info. If no path is provided
|
||||
then the root item will be verified.
|
||||
Verify the state of an xblock_info field. If no path is provided then the root item will be verified.
|
||||
If should_equal is True, assert that the current state matches the expected state, otherwise assert that they
|
||||
do not match.
|
||||
"""
|
||||
if path:
|
||||
direct_child_xblock_info = self._get_child_xblock_info(xblock_info, path[0])
|
||||
remaining_path = path[1:] if len(path) > 1 else None
|
||||
self._verify_visibility_state(direct_child_xblock_info, expected_state, remaining_path)
|
||||
self._verify_xblock_info_state(direct_child_xblock_info, xblock_info_field, expected_state, remaining_path, should_equal)
|
||||
else:
|
||||
self.assertEqual(xblock_info['visibility_state'], expected_state)
|
||||
if should_equal:
|
||||
self.assertEqual(xblock_info[xblock_info_field], expected_state)
|
||||
else:
|
||||
self.assertNotEqual(xblock_info[xblock_info_field], expected_state)
|
||||
|
||||
def _verify_has_staff_only_message(self, xblock_info, expected_state, path=None):
|
||||
"""
|
||||
Verify the staff_only_message field of xblock_info.
|
||||
"""
|
||||
self._verify_xblock_info_state(xblock_info, 'staff_only_message', expected_state, path)
|
||||
|
||||
def _verify_visibility_state(self, xblock_info, expected_state, path=None, should_equal=True):
|
||||
"""
|
||||
Verify the publish state of an item in the xblock_info.
|
||||
"""
|
||||
self._verify_xblock_info_state(xblock_info, 'visibility_state', expected_state, path, should_equal)
|
||||
|
||||
def _verify_explicit_staff_lock_state(self, xblock_info, expected_state, path=None, should_equal=True):
|
||||
"""
|
||||
Verify the explicit staff lock state of an item in the xblock_info.
|
||||
"""
|
||||
self._verify_xblock_info_state(xblock_info, 'has_explicit_staff_lock', expected_state, path, should_equal)
|
||||
|
||||
def _verify_staff_lock_from_state(self, xblock_info, expected_state, path=None, should_equal=True):
|
||||
"""
|
||||
Verify the staff_lock_from state of an item in the xblock_info.
|
||||
"""
|
||||
self._verify_xblock_info_state(xblock_info, 'staff_lock_from', expected_state, path, should_equal)
|
||||
|
||||
def test_empty_chapter(self):
|
||||
empty_chapter = self._create_child(self.course, 'chapter', "Empty Chapter")
|
||||
xblock_info = self._get_xblock_info(empty_chapter.location)
|
||||
self.assertEqual(xblock_info['visibility_state'], VisibilityState.unscheduled)
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.unscheduled)
|
||||
|
||||
def test_empty_sequential(self):
|
||||
chapter = self._create_child(self.course, 'chapter', "Test Chapter")
|
||||
@@ -1416,16 +1521,83 @@ class TestXBlockPublishingInfo(ItemTest):
|
||||
# Finally verify the state of the chapter
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.ready)
|
||||
|
||||
def test_staff_only(self):
|
||||
chapter = self._create_child(self.course, 'chapter', "Test Chapter")
|
||||
def test_staff_only_section(self):
|
||||
"""
|
||||
Tests that an explicitly staff-locked section and all of its children are visible to staff only.
|
||||
"""
|
||||
chapter = self._create_child(self.course, 'chapter', "Test Chapter", staff_only=True)
|
||||
sequential = self._create_child(chapter, 'sequential', "Test Sequential")
|
||||
unit = self._create_child(sequential, 'vertical', "Published Unit")
|
||||
self._set_staff_only(unit.location, True)
|
||||
self._create_child(sequential, 'vertical', "Unit")
|
||||
xblock_info = self._get_xblock_info(chapter.location)
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.staff_only)
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.staff_only, path=self.FIRST_SUBSECTION_PATH)
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.staff_only, path=self.FIRST_UNIT_PATH)
|
||||
|
||||
self._verify_explicit_staff_lock_state(xblock_info, True)
|
||||
self._verify_explicit_staff_lock_state(xblock_info, False, path=self.FIRST_SUBSECTION_PATH)
|
||||
self._verify_explicit_staff_lock_state(xblock_info, False, path=self.FIRST_UNIT_PATH)
|
||||
|
||||
self._verify_staff_lock_from_state(xblock_info, _xblock_type_and_display_name(chapter), path=self.FIRST_UNIT_PATH)
|
||||
|
||||
def test_no_staff_only_section(self):
|
||||
"""
|
||||
Tests that a section with a staff-locked subsection and a visible subsection is not staff locked itself.
|
||||
"""
|
||||
chapter = self._create_child(self.course, 'chapter', "Test Chapter")
|
||||
self._create_child(chapter, 'sequential', "Test Visible Sequential")
|
||||
self._create_child(chapter, 'sequential', "Test Staff Locked Sequential", staff_only=True)
|
||||
xblock_info = self._get_xblock_info(chapter.location)
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.staff_only, should_equal=False)
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.staff_only, path=[0], should_equal=False)
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.staff_only, path=[1])
|
||||
|
||||
def test_staff_only_subsection(self):
|
||||
"""
|
||||
Tests that an explicitly staff-locked subsection and all of its children are visible to staff only.
|
||||
In this case the parent section is also visible to staff only because all of its children are staff only.
|
||||
"""
|
||||
chapter = self._create_child(self.course, 'chapter', "Test Chapter")
|
||||
sequential = self._create_child(chapter, 'sequential', "Test Sequential", staff_only=True)
|
||||
self._create_child(sequential, 'vertical', "Unit")
|
||||
xblock_info = self._get_xblock_info(chapter.location)
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.staff_only)
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.staff_only, path=self.FIRST_SUBSECTION_PATH)
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.staff_only, path=self.FIRST_UNIT_PATH)
|
||||
|
||||
self._verify_explicit_staff_lock_state(xblock_info, False)
|
||||
self._verify_explicit_staff_lock_state(xblock_info, True, path=self.FIRST_SUBSECTION_PATH)
|
||||
self._verify_explicit_staff_lock_state(xblock_info, False, path=self.FIRST_UNIT_PATH)
|
||||
|
||||
self._verify_staff_lock_from_state(xblock_info, _xblock_type_and_display_name(sequential), path=self.FIRST_UNIT_PATH)
|
||||
|
||||
def test_no_staff_only_subsection(self):
|
||||
"""
|
||||
Tests that a subsection with a staff-locked unit and a visible unit is not staff locked itself.
|
||||
"""
|
||||
chapter = self._create_child(self.course, 'chapter', "Test Chapter")
|
||||
sequential = self._create_child(chapter, 'sequential', "Test Sequential")
|
||||
self._create_child(sequential, 'vertical', "Unit")
|
||||
self._create_child(sequential, 'vertical', "Locked Unit", staff_only=True)
|
||||
xblock_info = self._get_xblock_info(chapter.location)
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.staff_only, self.FIRST_SUBSECTION_PATH, should_equal=False)
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.staff_only, self.FIRST_UNIT_PATH, should_equal=False)
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.staff_only, self.SECOND_UNIT_PATH)
|
||||
|
||||
def test_staff_only_unit(self):
|
||||
chapter = self._create_child(self.course, 'chapter', "Test Chapter")
|
||||
sequential = self._create_child(chapter, 'sequential', "Test Sequential")
|
||||
unit = self._create_child(sequential, 'vertical', "Unit", staff_only=True)
|
||||
xblock_info = self._get_xblock_info(chapter.location)
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.staff_only)
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.staff_only, path=self.FIRST_SUBSECTION_PATH)
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.staff_only, path=self.FIRST_UNIT_PATH)
|
||||
|
||||
self._verify_explicit_staff_lock_state(xblock_info, False)
|
||||
self._verify_explicit_staff_lock_state(xblock_info, False, path=self.FIRST_SUBSECTION_PATH)
|
||||
self._verify_explicit_staff_lock_state(xblock_info, True, path=self.FIRST_UNIT_PATH)
|
||||
|
||||
self._verify_staff_lock_from_state(xblock_info, _xblock_type_and_display_name(unit), path=self.FIRST_UNIT_PATH)
|
||||
|
||||
def test_unscheduled_section_with_live_subsection(self):
|
||||
chapter = self._create_child(self.course, 'chapter', "Test Chapter")
|
||||
sequential = self._create_child(chapter, 'sequential', "Test Sequential")
|
||||
@@ -1450,3 +1622,27 @@ class TestXBlockPublishingInfo(ItemTest):
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.live, path=self.FIRST_SUBSECTION_PATH)
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.live, path=self.FIRST_UNIT_PATH)
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.staff_only, path=self.SECOND_UNIT_PATH)
|
||||
|
||||
def test_locked_section_staff_only_message(self):
|
||||
"""
|
||||
Tests that a locked section has a staff only message and its descendants do not.
|
||||
"""
|
||||
chapter = self._create_child(self.course, 'chapter', "Test Chapter", staff_only=True)
|
||||
sequential = self._create_child(chapter, 'sequential', "Test Sequential")
|
||||
self._create_child(sequential, 'vertical', "Unit")
|
||||
xblock_info = self._get_xblock_outline_info(chapter.location)
|
||||
self._verify_has_staff_only_message(xblock_info, True)
|
||||
self._verify_has_staff_only_message(xblock_info, False, path=self.FIRST_SUBSECTION_PATH)
|
||||
self._verify_has_staff_only_message(xblock_info, False, path=self.FIRST_UNIT_PATH)
|
||||
|
||||
def test_locked_unit_staff_only_message(self):
|
||||
"""
|
||||
Tests that a lone locked unit has a staff only message along with its ancestors.
|
||||
"""
|
||||
chapter = self._create_child(self.course, 'chapter', "Test Chapter")
|
||||
sequential = self._create_child(chapter, 'sequential', "Test Sequential")
|
||||
self._create_child(sequential, 'vertical', "Unit", staff_only=True)
|
||||
xblock_info = self._get_xblock_outline_info(chapter.location)
|
||||
self._verify_has_staff_only_message(xblock_info, True)
|
||||
self._verify_has_staff_only_message(xblock_info, True, path=self.FIRST_SUBSECTION_PATH)
|
||||
self._verify_has_staff_only_message(xblock_info, True, path=self.FIRST_UNIT_PATH)
|
||||
|
||||
@@ -86,14 +86,18 @@ class CourseDetails(object):
|
||||
temploc = course_key.make_usage_key('about', about_key)
|
||||
store = modulestore()
|
||||
if data is None:
|
||||
store.delete_item(temploc, user.id)
|
||||
try:
|
||||
store.delete_item(temploc, user.id)
|
||||
# Ignore an attempt to delete an item that doesn't exist
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
about_item = store.get_item(temploc)
|
||||
except ItemNotFoundError:
|
||||
about_item = store.create_xblock(course.runtime, course.id, 'about', about_key)
|
||||
about_item.data = data
|
||||
store.update_item(about_item, user.id)
|
||||
store.update_item(about_item, user.id, allow_not_found=True)
|
||||
|
||||
@classmethod
|
||||
def update_from_json(cls, course_key, jsondict, user):
|
||||
|
||||
@@ -24,6 +24,7 @@ class CourseMetadata(object):
|
||||
'graded',
|
||||
'hide_from_toc',
|
||||
'pdf_textbooks',
|
||||
'user_partitions',
|
||||
'name', # from xblock
|
||||
'tags', # from xblock
|
||||
'visible_to_staff_only'
|
||||
|
||||
@@ -18,6 +18,9 @@ DEBUG = True
|
||||
import logging
|
||||
logging.basicConfig(filename=TEST_ROOT / "log" / "cms_acceptance.log", level=logging.ERROR)
|
||||
|
||||
# set root logger level
|
||||
logging.getLogger().setLevel(logging.ERROR)
|
||||
|
||||
import os
|
||||
|
||||
|
||||
|
||||
@@ -243,6 +243,7 @@ if 'DATADOG_API' in AUTH_TOKENS:
|
||||
DATADOG['api_key'] = AUTH_TOKENS['DATADOG_API']
|
||||
|
||||
# Celery Broker
|
||||
CELERY_ALWAYS_EAGER = ENV_TOKENS.get("CELERY_ALWAYS_EAGER", False)
|
||||
CELERY_BROKER_TRANSPORT = ENV_TOKENS.get("CELERY_BROKER_TRANSPORT", "")
|
||||
CELERY_BROKER_HOSTNAME = ENV_TOKENS.get("CELERY_BROKER_HOSTNAME", "")
|
||||
CELERY_BROKER_VHOST = ENV_TOKENS.get("CELERY_BROKER_VHOST", "")
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"CELERY_ALWAYS_EAGER": true,
|
||||
"CELERY_BROKER_HOSTNAME": "localhost",
|
||||
"CELERY_BROKER_TRANSPORT": "amqp",
|
||||
"CERT_QUEUE": "certificates",
|
||||
@@ -70,7 +71,8 @@
|
||||
"PREVIEW_LMS_BASE": "localhost:8003",
|
||||
"SUBDOMAIN_BRANDING": false,
|
||||
"SUBDOMAIN_COURSE_LISTINGS": false,
|
||||
"ALLOW_ALL_ADVANCED_COMPONENTS": true
|
||||
"ALLOW_ALL_ADVANCED_COMPONENTS": true,
|
||||
"ALLOW_COURSE_RERUNS": true
|
||||
},
|
||||
"FEEDBACK_SUBMISSION_EMAIL": "",
|
||||
"GITHUB_REPO_ROOT": "** OVERRIDDEN **",
|
||||
|
||||
@@ -104,9 +104,6 @@ FEATURES = {
|
||||
# Turn off Advanced Security by default
|
||||
'ADVANCED_SECURITY': False,
|
||||
|
||||
# Toggles Group Configuration editing functionality
|
||||
'ENABLE_GROUP_CONFIGURATIONS': os.environ.get('FEATURE_GROUP_CONFIGURATIONS'),
|
||||
|
||||
# Modulestore to use for new courses
|
||||
'DEFAULT_STORE_FOR_NEW_COURSE': 'mongo',
|
||||
}
|
||||
|
||||
@@ -165,6 +165,7 @@ DEBUG_TOOLBAR_MONGO_STACKTRACES = False
|
||||
|
||||
# Enable URL that shows information about the status of variuous services
|
||||
FEATURES['ENABLE_SERVICE_STATUS'] = True
|
||||
FEATURES['ALLOW_COURSE_RERUNS'] = True
|
||||
|
||||
############################# SEGMENT-IO ##################################
|
||||
|
||||
|
||||
@@ -40,6 +40,10 @@ FEATURES['ALLOW_ALL_ADVANCED_COMPONENTS'] = True
|
||||
# By default don't use a worker, execute tasks as if they were local functions
|
||||
CELERY_ALWAYS_EAGER = True
|
||||
|
||||
################################ COURSE RERUNS ################################
|
||||
|
||||
FEATURES['ALLOW_COURSE_RERUNS'] = True
|
||||
|
||||
################################ DEBUG TOOLBAR ################################
|
||||
INSTALLED_APPS += ('debug_toolbar', 'debug_toolbar_mongo')
|
||||
MIDDLEWARE_CLASSES += ('debug_toolbar.middleware.DebugToolbarMiddleware',)
|
||||
|
||||
@@ -21,6 +21,12 @@ from uuid import uuid4
|
||||
# import settings from LMS for consistent behavior with CMS
|
||||
from lms.envs.test import (WIKI_ENABLED, PLATFORM_NAME, SITE_NAME)
|
||||
|
||||
# mongo connection settings
|
||||
MONGO_PORT_NUM = int(os.environ.get('EDXAPP_TEST_MONGO_PORT', '27017'))
|
||||
MONGO_HOST = os.environ.get('EDXAPP_TEST_MONGO_HOST', 'localhost')
|
||||
|
||||
THIS_UUID = uuid4().hex[:5]
|
||||
|
||||
# Nose Test Runner
|
||||
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
|
||||
|
||||
@@ -87,15 +93,18 @@ update_module_store_settings(
|
||||
},
|
||||
doc_store_settings={
|
||||
'db': 'test_xmodule',
|
||||
'collection': 'test_modulestore{0}'.format(uuid4().hex[:5]),
|
||||
'host': MONGO_HOST,
|
||||
'port': MONGO_PORT_NUM,
|
||||
'collection': 'test_modulestore{0}'.format(THIS_UUID),
|
||||
},
|
||||
)
|
||||
|
||||
CONTENTSTORE = {
|
||||
'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore',
|
||||
'DOC_STORE_CONFIG': {
|
||||
'host': 'localhost',
|
||||
'host': MONGO_HOST,
|
||||
'db': 'test_xcontent',
|
||||
'port': MONGO_PORT_NUM,
|
||||
'collection': 'dont_trip',
|
||||
},
|
||||
# allow for additional options that can be keyed on a name, e.g. 'trashcan'
|
||||
|
||||
@@ -233,6 +233,8 @@ define([
|
||||
"js/spec/views/pages/container_subviews_spec",
|
||||
"js/spec/views/pages/group_configurations_spec",
|
||||
"js/spec/views/pages/course_outline_spec",
|
||||
"js/spec/views/pages/course_rerun_spec",
|
||||
"js/spec/views/pages/index_spec",
|
||||
|
||||
"js/spec/views/modals/base_modal_spec",
|
||||
"js/spec/views/modals/edit_xblock_spec",
|
||||
|
||||
@@ -1,21 +1,29 @@
|
||||
require(["domReady", "jquery", "underscore", "js/utils/cancel_on_escape"],
|
||||
function (domReady, $, _, CancelOnEscape) {
|
||||
define(["domReady", "jquery", "underscore", "js/utils/cancel_on_escape", "js/views/utils/create_course_utils",
|
||||
"js/views/utils/view_utils"],
|
||||
function (domReady, $, _, CancelOnEscape, CreateCourseUtilsFactory, ViewUtils) {
|
||||
var CreateCourseUtils = CreateCourseUtilsFactory({
|
||||
name: '.new-course-name',
|
||||
org: '.new-course-org',
|
||||
number: '.new-course-number',
|
||||
run: '.new-course-run',
|
||||
save: '.new-course-save',
|
||||
errorWrapper: '.wrap-error',
|
||||
errorMessage: '#course_creation_error',
|
||||
tipError: 'span.tip-error',
|
||||
error: '.error',
|
||||
allowUnicode: '.allow-unicode-course-id'
|
||||
}, {
|
||||
shown: 'is-shown',
|
||||
showing: 'is-showing',
|
||||
hiding: 'is-hiding',
|
||||
disabled: 'is-disabled',
|
||||
error: 'error'
|
||||
});
|
||||
|
||||
var saveNewCourse = function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
// One final check for empty values
|
||||
var errors = _.reduce(
|
||||
['.new-course-name', '.new-course-org', '.new-course-number', '.new-course-run'],
|
||||
function (acc, ele) {
|
||||
var $ele = $(ele);
|
||||
var error = validateRequiredField($ele.val());
|
||||
setNewCourseFieldInErr($ele.parent('li'), error);
|
||||
return error ? true : acc;
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
if (errors) {
|
||||
if (CreateCourseUtils.hasInvalidRequiredFields()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -25,29 +33,19 @@ require(["domReady", "jquery", "underscore", "js/utils/cancel_on_escape"],
|
||||
var number = $newCourseForm.find('.new-course-number').val();
|
||||
var run = $newCourseForm.find('.new-course-run').val();
|
||||
|
||||
analytics.track('Created a Course', {
|
||||
'org': org,
|
||||
'number': number,
|
||||
'display_name': display_name,
|
||||
'run': run
|
||||
});
|
||||
course_info = {
|
||||
org: org,
|
||||
number: number,
|
||||
display_name: display_name,
|
||||
run: run
|
||||
};
|
||||
|
||||
$.postJSON('/course/', {
|
||||
'org': org,
|
||||
'number': number,
|
||||
'display_name': display_name,
|
||||
'run': run
|
||||
},
|
||||
function (data) {
|
||||
if (data.url !== undefined) {
|
||||
window.location = data.url;
|
||||
} else if (data.ErrMsg !== undefined) {
|
||||
$('.wrap-error').addClass('is-shown');
|
||||
$('#course_creation_error').html('<p>' + data.ErrMsg + '</p>');
|
||||
$('.new-course-save').addClass('is-disabled');
|
||||
}
|
||||
}
|
||||
);
|
||||
analytics.track('Created a Course', course_info);
|
||||
CreateCourseUtils.createCourse(course_info, function (errorMessage) {
|
||||
$('.wrap-error').addClass('is-shown');
|
||||
$('#course_creation_error').html('<p>' + errorMessage + '</p>');
|
||||
$('.new-course-save').addClass('is-disabled');
|
||||
});
|
||||
};
|
||||
|
||||
var cancelNewCourse = function (e) {
|
||||
@@ -78,91 +76,20 @@ require(["domReady", "jquery", "underscore", "js/utils/cancel_on_escape"],
|
||||
$cancelButton.bind('click', cancelNewCourse);
|
||||
CancelOnEscape($cancelButton);
|
||||
|
||||
// Check that a course (org, number, run) doesn't use any special characters
|
||||
var validateCourseItemEncoding = function (item) {
|
||||
var required = validateRequiredField(item);
|
||||
if (required) {
|
||||
return required;
|
||||
}
|
||||
if ($('.allow-unicode-course-id').val() === 'True'){
|
||||
if (/\s/g.test(item)) {
|
||||
return gettext('Please do not use any spaces in this field.');
|
||||
}
|
||||
}
|
||||
else{
|
||||
if (item !== encodeURIComponent(item)) {
|
||||
return gettext('Please do not use any spaces or special characters in this field.');
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
// Ensure that org/course_num/run < 65 chars.
|
||||
var validateTotalCourseItemsLength = function () {
|
||||
var totalLength = _.reduce(
|
||||
['.new-course-org', '.new-course-number', '.new-course-run'],
|
||||
function (sum, ele) {
|
||||
return sum + $(ele).val().length;
|
||||
}, 0
|
||||
);
|
||||
if (totalLength > 65) {
|
||||
$('.wrap-error').addClass('is-shown');
|
||||
$('#course_creation_error').html('<p>' + gettext('The combined length of the organization, course number, and course run fields cannot be more than 65 characters.') + '</p>');
|
||||
$('.new-course-save').addClass('is-disabled');
|
||||
}
|
||||
else {
|
||||
$('.wrap-error').removeClass('is-shown');
|
||||
}
|
||||
};
|
||||
|
||||
// Handle validation asynchronously
|
||||
_.each(
|
||||
['.new-course-org', '.new-course-number', '.new-course-run'],
|
||||
function (ele) {
|
||||
var $ele = $(ele);
|
||||
$ele.on('keyup', function (event) {
|
||||
// Don't bother showing "required field" error when
|
||||
// the user tabs into a new field; this is distracting
|
||||
// and unnecessary
|
||||
if (event.keyCode === 9) {
|
||||
return;
|
||||
}
|
||||
var error = validateCourseItemEncoding($ele.val());
|
||||
setNewCourseFieldInErr($ele.parent('li'), error);
|
||||
validateTotalCourseItemsLength();
|
||||
});
|
||||
}
|
||||
);
|
||||
var $name = $('.new-course-name');
|
||||
$name.on('keyup', function () {
|
||||
var error = validateRequiredField($name.val());
|
||||
setNewCourseFieldInErr($name.parent('li'), error);
|
||||
validateTotalCourseItemsLength();
|
||||
});
|
||||
CreateCourseUtils.configureHandlers();
|
||||
};
|
||||
|
||||
var validateRequiredField = function (msg) {
|
||||
return msg.length === 0 ? gettext('Required field.') : '';
|
||||
};
|
||||
|
||||
var setNewCourseFieldInErr = function (el, msg) {
|
||||
if(msg) {
|
||||
el.addClass('error');
|
||||
el.children('span.tip-error').addClass('is-showing').removeClass('is-hiding').text(msg);
|
||||
$('.new-course-save').addClass('is-disabled');
|
||||
}
|
||||
else {
|
||||
el.removeClass('error');
|
||||
el.children('span.tip-error').addClass('is-hiding').removeClass('is-showing');
|
||||
// One "error" div is always present, but hidden or shown
|
||||
if($('.error').length === 1) {
|
||||
$('.new-course-save').removeClass('is-disabled');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
domReady(function () {
|
||||
var onReady = function () {
|
||||
$('.new-course-button').bind('click', addNewCourse);
|
||||
});
|
||||
$('.dismiss-button').bind('click', ViewUtils.deleteNotificationHandler(function () {
|
||||
ViewUtils.reload();
|
||||
}));
|
||||
$('.action-reload').bind('click', ViewUtils.reload);
|
||||
};
|
||||
|
||||
domReady(onReady);
|
||||
|
||||
return {
|
||||
onReady: onReady
|
||||
};
|
||||
});
|
||||
|
||||
@@ -80,14 +80,14 @@ function(Backbone, _, str, gettext, GroupModel, GroupCollection) {
|
||||
validate: function(attrs) {
|
||||
if (!_.str.trim(attrs.name)) {
|
||||
return {
|
||||
message: gettext('Group Configuration name is required'),
|
||||
message: gettext('Group Configuration name is required.'),
|
||||
attributes: {name: true}
|
||||
};
|
||||
}
|
||||
|
||||
if (attrs.groups.length < 2) {
|
||||
if (attrs.groups.length < 1) {
|
||||
return {
|
||||
message: gettext('There must be at least two groups'),
|
||||
message: gettext('There must be at least one group.'),
|
||||
attributes: { groups: true }
|
||||
};
|
||||
} else {
|
||||
@@ -100,7 +100,7 @@ function(Backbone, _, str, gettext, GroupModel, GroupCollection) {
|
||||
});
|
||||
if (!_.isEmpty(invalidGroups)) {
|
||||
return {
|
||||
message: gettext('All groups must have a name'),
|
||||
message: gettext('All groups must have a name.'),
|
||||
attributes: { groups: invalidGroups }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ function(Backbone, _, str, ModuleUtils) {
|
||||
*/
|
||||
"visibility_state": null,
|
||||
/**
|
||||
* True iff the release date of the xblock is in the past.
|
||||
* True if the release date of the xblock is in the past.
|
||||
*/
|
||||
'released_to_students': null,
|
||||
/**
|
||||
@@ -102,7 +102,25 @@ function(Backbone, _, str, ModuleUtils) {
|
||||
/**
|
||||
* The same as `due_date` but as an ISO-formatted date string.
|
||||
*/
|
||||
'due': null
|
||||
'due': null,
|
||||
/**
|
||||
* True iff this xblock is explicitly staff locked.
|
||||
*/
|
||||
'has_explicit_staff_lock': null,
|
||||
/**
|
||||
* True iff this any of this xblock's ancestors are staff locked.
|
||||
*/
|
||||
'ancestor_has_staff_lock': null,
|
||||
/**
|
||||
* The xblock which is determining the staff lock value. For instance, for a unit,
|
||||
* this will either be the parent subsection or the grandparent section.
|
||||
* This can be null if the xblock has no inherited staff lock.
|
||||
*/
|
||||
'staff_lock_from': null,
|
||||
/**
|
||||
* True iff this xblock should display a "Contains staff only content" message.
|
||||
*/
|
||||
'staff_only_message': null
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
@@ -135,6 +153,10 @@ function(Backbone, _, str, ModuleUtils) {
|
||||
return childInfo && childInfo.children.length > 0;
|
||||
},
|
||||
|
||||
isPublishable: function(){
|
||||
return !this.get('published') || this.get('has_changes');
|
||||
},
|
||||
|
||||
/**
|
||||
* Return a list of convenience methods to check affiliation to the category.
|
||||
* @return {Array}
|
||||
@@ -157,7 +179,7 @@ function(Backbone, _, str, ModuleUtils) {
|
||||
* @return {Boolean}
|
||||
*/
|
||||
isEditableOnCourseOutline: function() {
|
||||
return this.isSequential() || this.isChapter();
|
||||
return this.isSequential() || this.isChapter() || this.isVertical();
|
||||
}
|
||||
});
|
||||
return XBlockInfo;
|
||||
|
||||
@@ -183,15 +183,14 @@ define([
|
||||
expect(model.isValid()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('requires at least two groups', function() {
|
||||
it('requires at least one group', function() {
|
||||
var group1 = new GroupModel({ name: 'Group A' }),
|
||||
group2 = new GroupModel({ name: 'Group B' }),
|
||||
model = new GroupConfigurationModel({ name: 'foo' });
|
||||
|
||||
model.get('groups').reset([group1]);
|
||||
model.get('groups').reset([]);
|
||||
expect(model.isValid()).toBeFalsy();
|
||||
|
||||
model.get('groups').add(group2);
|
||||
model.get('groups').add(group1);
|
||||
expect(model.isValid()).toBeTruthy();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
define(['backbone', 'js/models/xblock_info'],
|
||||
function(Backbone, XBlockInfo) {
|
||||
function(Backbone, XBlockInfo) {
|
||||
describe('XblockInfo isEditableOnCourseOutline', function() {
|
||||
it('works correct', function() {
|
||||
expect(new XBlockInfo({'category': 'chapter'}).isEditableOnCourseOutline()).toBe(true);
|
||||
expect(new XBlockInfo({'category': 'course'}).isEditableOnCourseOutline()).toBe(false);
|
||||
expect(new XBlockInfo({'category': 'sequential'}).isEditableOnCourseOutline()).toBe(true);
|
||||
expect(new XBlockInfo({'category': 'vertical'}).isEditableOnCourseOutline()).toBe(false);
|
||||
expect(new XBlockInfo({'category': 'vertical'}).isEditableOnCourseOutline()).toBe(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -381,7 +381,7 @@ define([
|
||||
this.view.$('form').submit();
|
||||
// See error message
|
||||
expect(this.view.$(SELECTORS.errorMessage)).toContainText(
|
||||
'Group Configuration name is required'
|
||||
'Group Configuration name is required.'
|
||||
);
|
||||
// No request
|
||||
expect(requests.length).toBe(0);
|
||||
|
||||
@@ -71,8 +71,8 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers
|
||||
refreshed = true;
|
||||
};
|
||||
modal = showModal(requests, mockXBlockEditorHtml, { refresh: refresh });
|
||||
modal.runtime.notify('save', { state: 'start' });
|
||||
modal.runtime.notify('save', { state: 'end' });
|
||||
modal.editorView.notifyRuntime('save', { state: 'start' });
|
||||
modal.editorView.notifyRuntime('save', { state: 'end' });
|
||||
expect(edit_helpers.isShowingModal(modal)).toBeFalsy();
|
||||
expect(refreshed).toBeTruthy();
|
||||
});
|
||||
@@ -84,7 +84,7 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers
|
||||
refreshed = true;
|
||||
};
|
||||
modal = showModal(requests, mockXBlockEditorHtml, { refresh: refresh });
|
||||
modal.runtime.notify('cancel');
|
||||
modal.editorView.notifyRuntime('cancel');
|
||||
expect(edit_helpers.isShowingModal(modal)).toBeFalsy();
|
||||
expect(refreshed).toBeFalsy();
|
||||
});
|
||||
|
||||
@@ -7,6 +7,8 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
|
||||
model, containerPage, requests, initialDisplayName,
|
||||
mockContainerPage = readFixtures('mock/mock-container-page.underscore'),
|
||||
mockContainerXBlockHtml = readFixtures('mock/mock-container-xblock.underscore'),
|
||||
mockBadContainerXBlockHtml = readFixtures('mock/mock-bad-javascript-container-xblock.underscore'),
|
||||
mockBadXBlockContainerXBlockHtml = readFixtures('mock/mock-bad-xblock-container-xblock.underscore'),
|
||||
mockUpdatedContainerXBlockHtml = readFixtures('mock/mock-updated-container-xblock.underscore'),
|
||||
mockXBlockEditorHtml = readFixtures('mock/mock-xblock-editor.underscore');
|
||||
|
||||
@@ -15,6 +17,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
|
||||
|
||||
edit_helpers.installEditTemplates();
|
||||
edit_helpers.installTemplate('xblock-string-field-editor');
|
||||
edit_helpers.installTemplate('container-message');
|
||||
appendSetFixtures(mockContainerPage);
|
||||
|
||||
edit_helpers.installMockXBlock({
|
||||
@@ -83,6 +86,18 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
|
||||
expect(containerPage.$('.ui-loading')).toHaveClass('is-hidden');
|
||||
});
|
||||
|
||||
it('can show an xblock with broken JavaScript', function() {
|
||||
renderContainerPage(this, mockBadContainerXBlockHtml);
|
||||
expect(containerPage.$('.wrapper-xblock .level-nesting')).not.toHaveClass('is-hidden');
|
||||
expect(containerPage.$('.ui-loading')).toHaveClass('is-hidden');
|
||||
});
|
||||
|
||||
it('can show an xblock with an invalid XBlock', function() {
|
||||
renderContainerPage(this, mockBadXBlockContainerXBlockHtml);
|
||||
expect(containerPage.$('.wrapper-xblock .level-nesting')).not.toHaveClass('is-hidden');
|
||||
expect(containerPage.$('.ui-loading')).toHaveClass('is-hidden');
|
||||
});
|
||||
|
||||
it('inline edits the display name when performing a new action', function() {
|
||||
renderContainerPage(this, mockContainerXBlockHtml, {
|
||||
action: 'new'
|
||||
@@ -138,6 +153,9 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
|
||||
resources: []
|
||||
});
|
||||
|
||||
// Respond to the subsequent xblock info fetch request.
|
||||
create_sinon.respondWithJson(requests, {"display_name": updatedDisplayName});
|
||||
|
||||
// Expect the title to have been updated
|
||||
expect(displayNameElement.text().trim()).toBe(updatedDisplayName);
|
||||
});
|
||||
@@ -177,6 +195,18 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
|
||||
});
|
||||
expect(edit_helpers.isShowingModal()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('can show an edit modal for a child xblock with broken JavaScript', function() {
|
||||
var editButtons;
|
||||
renderContainerPage(this, mockBadContainerXBlockHtml);
|
||||
editButtons = containerPage.$('.wrapper-xblock .edit-button');
|
||||
editButtons[0].click();
|
||||
create_sinon.respondWithJson(requests, {
|
||||
html: mockXBlockEditorHtml,
|
||||
resources: []
|
||||
});
|
||||
expect(edit_helpers.isShowingModal()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Editing an xmodule", function() {
|
||||
@@ -268,10 +298,10 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
|
||||
clickDelete(componentIndex);
|
||||
create_sinon.respondWithJson(requests, {});
|
||||
|
||||
// first request contains given component's id (to delete the component)
|
||||
expect(requests[requests.length - 2].url).toMatch(
|
||||
new RegExp("locator-component-" + GROUP_TO_TEST + (componentIndex + 1))
|
||||
);
|
||||
// second to last request contains given component's id (to delete the component)
|
||||
create_sinon.expectJsonRequest(requests, 'DELETE',
|
||||
'/xblock/locator-component-' + GROUP_TO_TEST + (componentIndex + 1),
|
||||
null, requests.length - 2);
|
||||
|
||||
// final request to refresh the xblock info
|
||||
create_sinon.expectJsonRequest(requests, 'GET', '/xblock/locator-container');
|
||||
@@ -302,6 +332,18 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
|
||||
deleteComponentWithSuccess(NUM_COMPONENTS_PER_GROUP - 1);
|
||||
});
|
||||
|
||||
it("can delete an xblock with broken JavaScript", function() {
|
||||
renderContainerPage(this, mockBadContainerXBlockHtml);
|
||||
containerPage.$('.delete-button').first().click();
|
||||
edit_helpers.confirmPrompt(promptSpy);
|
||||
create_sinon.respondWithJson(requests, {});
|
||||
// expect the second to last request to be a delete of the xblock
|
||||
create_sinon.expectJsonRequest(requests, 'DELETE', '/xblock/locator-broken-javascript',
|
||||
null, requests.length - 2);
|
||||
// expect the last request to be a fetch of the xblock info for the parent container
|
||||
create_sinon.expectJsonRequest(requests, 'GET', '/xblock/locator-container');
|
||||
});
|
||||
|
||||
it('does not delete when clicking No in prompt', function () {
|
||||
var numRequests;
|
||||
|
||||
@@ -387,6 +429,15 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
|
||||
duplicateComponentWithSuccess(NUM_COMPONENTS_PER_GROUP - 1);
|
||||
});
|
||||
|
||||
it("can duplicate an xblock with broken JavaScript", function() {
|
||||
renderContainerPage(this, mockBadContainerXBlockHtml);
|
||||
containerPage.$('.duplicate-button').first().click();
|
||||
create_sinon.expectJsonRequest(requests, 'POST', '/xblock/', {
|
||||
'duplicate_source_locator': 'locator-broken-javascript',
|
||||
'parent_locator': 'locator-container'
|
||||
});
|
||||
});
|
||||
|
||||
it('shows a notification when duplicating', function () {
|
||||
var notificationSpy = edit_helpers.createNotificationSpy();
|
||||
renderContainerPage(this, mockContainerXBlockHtml);
|
||||
|
||||
@@ -124,6 +124,8 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
|
||||
lastDraftCss = ".wrapper-last-draft",
|
||||
releaseDateTitleCss = ".wrapper-release .title",
|
||||
releaseDateContentCss = ".wrapper-release .copy",
|
||||
releaseDateDateCss = ".wrapper-release .copy .release-date",
|
||||
releaseDateWithCss = ".wrapper-release .copy .release-with",
|
||||
promptSpies, sendDiscardChangesToServer, verifyPublishingBitUnscheduled;
|
||||
|
||||
sendDiscardChangesToServer = function() {
|
||||
@@ -342,8 +344,8 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
|
||||
release_date: "Jul 02, 2014 at 14:20 UTC", release_date_from: 'Section "Week 1"'
|
||||
});
|
||||
expect(containerPage.$(releaseDateTitleCss).text()).toContain("Scheduled:");
|
||||
expect(containerPage.$(releaseDateContentCss).text()).
|
||||
toContain('Jul 02, 2014 at 14:20 UTC with Section "Week 1"');
|
||||
expect(containerPage.$(releaseDateDateCss).text()).toContain('Jul 02, 2014 at 14:20 UTC');
|
||||
expect(containerPage.$(releaseDateWithCss).text()).toContain('with Section "Week 1"');
|
||||
});
|
||||
|
||||
it('renders correctly when released', function () {
|
||||
@@ -353,8 +355,8 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
|
||||
release_date: "Jul 02, 2014 at 14:20 UTC", release_date_from: 'Section "Week 1"'
|
||||
});
|
||||
expect(containerPage.$(releaseDateTitleCss).text()).toContain("Released:");
|
||||
expect(containerPage.$(releaseDateContentCss).text()).
|
||||
toContain('Jul 02, 2014 at 14:20 UTC with Section "Week 1"');
|
||||
expect(containerPage.$(releaseDateDateCss).text()).toContain('Jul 02, 2014 at 14:20 UTC');
|
||||
expect(containerPage.$(releaseDateWithCss).text()).toContain('with Section "Week 1"');
|
||||
});
|
||||
|
||||
it('renders correctly when the release date is not set', function () {
|
||||
@@ -375,20 +377,22 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
|
||||
});
|
||||
containerPage.xblockPublisher.render();
|
||||
expect(containerPage.$(releaseDateTitleCss).text()).toContain("Release:");
|
||||
expect(containerPage.$(releaseDateContentCss).text()).
|
||||
toContain('Jul 02, 2014 at 14:20 UTC with Section "Week 1"');
|
||||
expect(containerPage.$(releaseDateDateCss).text()).toContain('Jul 02, 2014 at 14:20 UTC');
|
||||
expect(containerPage.$(releaseDateWithCss).text()).toContain('with Section "Week 1"');
|
||||
});
|
||||
});
|
||||
|
||||
describe("Content Visibility", function () {
|
||||
var requestStaffOnly, verifyStaffOnly, promptSpy,
|
||||
var requestStaffOnly, verifyStaffOnly, verifyExplicitStaffOnly, verifyImplicitStaffOnly, promptSpy,
|
||||
visibilityTitleCss = '.wrapper-visibility .title';
|
||||
|
||||
requestStaffOnly = function(isStaffOnly) {
|
||||
var newVisibilityState;
|
||||
|
||||
containerPage.$('.action-staff-lock').click();
|
||||
|
||||
// If removing the staff lock, click 'Yes' to confirm
|
||||
if (!isStaffOnly) {
|
||||
// If removing explicit staff lock with no implicit staff lock, click 'Yes' to confirm
|
||||
if (!isStaffOnly && !containerPage.model.get('ancestor_has_staff_lock')) {
|
||||
edit_helpers.confirmPrompt(promptSpy);
|
||||
}
|
||||
|
||||
@@ -403,24 +407,46 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
|
||||
visible_to_staff_only: isStaffOnly ? true : null
|
||||
}
|
||||
});
|
||||
|
||||
create_sinon.expectJsonRequest(requests, 'GET', '/xblock/locator-container');
|
||||
if (isStaffOnly || containerPage.model.get('ancestor_has_staff_lock')) {
|
||||
newVisibilityState = VisibilityState.staffOnly;
|
||||
} else {
|
||||
newVisibilityState = VisibilityState.live;
|
||||
}
|
||||
create_sinon.respondWithJson(requests, createXBlockInfo({
|
||||
published: containerPage.model.get('published'),
|
||||
visibility_state: isStaffOnly ? VisibilityState.staffOnly : VisibilityState.live,
|
||||
has_explicit_staff_lock: isStaffOnly,
|
||||
visibility_state: newVisibilityState,
|
||||
release_date: "Jul 02, 2000 at 14:20 UTC"
|
||||
}));
|
||||
};
|
||||
|
||||
verifyStaffOnly = function(isStaffOnly) {
|
||||
if (isStaffOnly) {
|
||||
expect(containerPage.$('.action-staff-lock i')).toHaveClass('icon-check');
|
||||
expect(containerPage.$('.wrapper-visibility .copy').text()).toBe('Staff Only');
|
||||
expect(containerPage.$('.wrapper-visibility .copy').text()).toContain('Staff Only');
|
||||
expect(containerPage.$(bitPublishingCss)).toHaveClass(staffOnlyClass);
|
||||
expect(containerPage.$(bitPublishingCss)).toHaveClass(scheduledClass);
|
||||
} else {
|
||||
expect(containerPage.$('.wrapper-visibility .copy').text().trim()).toBe('Staff and Students');
|
||||
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(staffOnlyClass);
|
||||
verifyExplicitStaffOnly(false);
|
||||
verifyImplicitStaffOnly(false);
|
||||
}
|
||||
};
|
||||
|
||||
verifyExplicitStaffOnly = function(isStaffOnly) {
|
||||
if (isStaffOnly) {
|
||||
expect(containerPage.$('.action-staff-lock i')).toHaveClass('icon-check');
|
||||
} else {
|
||||
expect(containerPage.$('.action-staff-lock i')).toHaveClass('icon-check-empty');
|
||||
expect(containerPage.$('.wrapper-visibility .copy').text()).toBe('Staff and Students');
|
||||
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(staffOnlyClass);
|
||||
}
|
||||
};
|
||||
|
||||
verifyImplicitStaffOnly = function(isStaffOnly) {
|
||||
if (isStaffOnly) {
|
||||
expect(containerPage.$('.wrapper-visibility .inherited-from')).toExist();
|
||||
} else {
|
||||
expect(containerPage.$('.wrapper-visibility .inherited-from')).not.toExist();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -444,36 +470,79 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
|
||||
published: true,
|
||||
has_changes: true
|
||||
});
|
||||
expect(containerPage.$(visibilityTitleCss).text()).toContain('Will Be Visible To')
|
||||
expect(containerPage.$(visibilityTitleCss).text()).toContain('Will Be Visible To');
|
||||
});
|
||||
|
||||
it("can be set to staff only", function() {
|
||||
it("can be explicitly set to staff only", function() {
|
||||
renderContainerPage(this, mockContainerXBlockHtml);
|
||||
requestStaffOnly(true);
|
||||
verifyExplicitStaffOnly(true);
|
||||
verifyImplicitStaffOnly(false);
|
||||
verifyStaffOnly(true);
|
||||
});
|
||||
|
||||
it("can remove staff only setting", function() {
|
||||
it("can be implicitly set to staff only", function() {
|
||||
renderContainerPage(this, mockContainerXBlockHtml, {
|
||||
visibility_state: VisibilityState.staffOnly,
|
||||
ancestor_has_staff_lock: true,
|
||||
staff_lock_from: "Section Foo"
|
||||
});
|
||||
verifyImplicitStaffOnly(true);
|
||||
verifyExplicitStaffOnly(false);
|
||||
verifyStaffOnly(true);
|
||||
});
|
||||
|
||||
it("can be explicitly and implicitly set to staff only", function() {
|
||||
renderContainerPage(this, mockContainerXBlockHtml, {
|
||||
visibility_state: VisibilityState.staffOnly,
|
||||
ancestor_has_staff_lock: true,
|
||||
staff_lock_from: "Section Foo"
|
||||
});
|
||||
requestStaffOnly(true);
|
||||
// explicit staff lock overrides the display of implicit staff lock
|
||||
verifyImplicitStaffOnly(false);
|
||||
verifyExplicitStaffOnly(true);
|
||||
verifyStaffOnly(true);
|
||||
});
|
||||
|
||||
it("can remove explicit staff only setting without having implicit staff only", function() {
|
||||
promptSpy = edit_helpers.createPromptSpy();
|
||||
renderContainerPage(this, mockContainerXBlockHtml, {
|
||||
visibility_state: VisibilityState.staffOnly,
|
||||
release_date: "Jul 02, 2000 at 14:20 UTC"
|
||||
has_explicit_staff_lock: true,
|
||||
ancestor_has_staff_lock: false
|
||||
});
|
||||
requestStaffOnly(false);
|
||||
verifyStaffOnly(false);
|
||||
});
|
||||
|
||||
it("can remove explicit staff only setting while having implicit staff only", function() {
|
||||
promptSpy = edit_helpers.createPromptSpy();
|
||||
renderContainerPage(this, mockContainerXBlockHtml, {
|
||||
visibility_state: VisibilityState.staffOnly,
|
||||
ancestor_has_staff_lock: true,
|
||||
has_explicit_staff_lock: true,
|
||||
staff_lock_from: "Section Foo"
|
||||
});
|
||||
requestStaffOnly(false);
|
||||
verifyExplicitStaffOnly(false);
|
||||
verifyImplicitStaffOnly(true);
|
||||
verifyStaffOnly(true);
|
||||
});
|
||||
|
||||
it("does not refresh if removing staff only is canceled", function() {
|
||||
var requestCount;
|
||||
promptSpy = edit_helpers.createPromptSpy();
|
||||
renderContainerPage(this, mockContainerXBlockHtml, {
|
||||
visibility_state: VisibilityState.staffOnly,
|
||||
release_date: "Jul 02, 2000 at 14:20 UTC"
|
||||
has_explicit_staff_lock: true,
|
||||
ancestor_has_staff_lock: false
|
||||
});
|
||||
requestCount = requests.length;
|
||||
containerPage.$('.action-staff-lock').click();
|
||||
edit_helpers.confirmPrompt(promptSpy, true); // Click 'No' to cancel
|
||||
expect(requests.length).toBe(requestCount);
|
||||
verifyExplicitStaffOnly(true);
|
||||
verifyStaffOnly(true);
|
||||
});
|
||||
|
||||
|
||||
@@ -5,14 +5,15 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
|
||||
describe("CourseOutlinePage", function() {
|
||||
var createCourseOutlinePage, displayNameInput, model, outlinePage, requests,
|
||||
getItemsOfType, getItemHeaders, verifyItemsExpanded, expandItemsAndVerifyState, collapseItemsAndVerifyState,
|
||||
createMockCourseJSON, createMockSectionJSON, createMockSubsectionJSON,
|
||||
mockCourseJSON, mockEmptyCourseJSON, mockSingleSectionCourseJSON,
|
||||
mockOutlinePage = readFixtures('mock/mock-course-outline-page.underscore');
|
||||
createMockCourseJSON, createMockSectionJSON, createMockSubsectionJSON, verifyTypePublishable,
|
||||
mockCourseJSON, mockEmptyCourseJSON, mockSingleSectionCourseJSON, createMockVerticalJSON,
|
||||
mockOutlinePage = readFixtures('mock/mock-course-outline-page.underscore'),
|
||||
mockRerunNotification = readFixtures('mock/mock-course-rerun-notification.underscore');
|
||||
|
||||
createMockCourseJSON = function(id, displayName, children) {
|
||||
return {
|
||||
id: id,
|
||||
display_name: displayName,
|
||||
createMockCourseJSON = function(options, children) {
|
||||
return $.extend(true, {}, {
|
||||
id: 'mock-course',
|
||||
display_name: 'Mock Course',
|
||||
category: 'course',
|
||||
studio_url: '/course/slashes:MockCourse',
|
||||
is_container: true,
|
||||
@@ -20,35 +21,39 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
|
||||
published: true,
|
||||
edited_on: 'Jul 02, 2014 at 20:56 UTC',
|
||||
edited_by: 'MockUser',
|
||||
has_explicit_staff_lock: false,
|
||||
child_info: {
|
||||
display_name: 'Section',
|
||||
category: 'chapter',
|
||||
children: children
|
||||
display_name: 'Section',
|
||||
children: []
|
||||
}
|
||||
};
|
||||
}, options, {child_info: {children: children}});
|
||||
};
|
||||
createMockSectionJSON = function(id, displayName, children) {
|
||||
return {
|
||||
id: id,
|
||||
|
||||
createMockSectionJSON = function(options, children) {
|
||||
return $.extend(true, {}, {
|
||||
id: 'mock-section',
|
||||
display_name: 'Mock Section',
|
||||
category: 'chapter',
|
||||
display_name: displayName,
|
||||
studio_url: '/course/slashes:MockCourse',
|
||||
is_container: true,
|
||||
has_changes: false,
|
||||
published: true,
|
||||
edited_on: 'Jul 02, 2014 at 20:56 UTC',
|
||||
edited_by: 'MockUser',
|
||||
has_explicit_staff_lock: false,
|
||||
child_info: {
|
||||
category: 'sequential',
|
||||
display_name: 'Subsection',
|
||||
children: children
|
||||
children: []
|
||||
}
|
||||
};
|
||||
}, options, {child_info: {children: children}});
|
||||
};
|
||||
createMockSubsectionJSON = function(id, displayName, children) {
|
||||
return {
|
||||
id: id,
|
||||
display_name: displayName,
|
||||
|
||||
createMockSubsectionJSON = function(options, children) {
|
||||
return $.extend(true, {}, {
|
||||
id: 'mock-subsection',
|
||||
display_name: 'Mock Subsection',
|
||||
category: 'sequential',
|
||||
studio_url: '/course/slashes:MockCourse',
|
||||
is_container: true,
|
||||
@@ -57,12 +62,28 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
|
||||
edited_on: 'Jul 02, 2014 at 20:56 UTC',
|
||||
edited_by: 'MockUser',
|
||||
course_graders: '["Lab", "Howework"]',
|
||||
has_explicit_staff_lock: false,
|
||||
child_info: {
|
||||
category: 'vertical',
|
||||
display_name: 'Unit',
|
||||
children: children
|
||||
children: []
|
||||
}
|
||||
};
|
||||
}, options, {child_info: {children: children}});
|
||||
};
|
||||
|
||||
createMockVerticalJSON = function(options) {
|
||||
return $.extend(true, {}, {
|
||||
id: 'mock-unit',
|
||||
display_name: 'Mock Unit',
|
||||
category: 'vertical',
|
||||
studio_url: '/container/mock-unit',
|
||||
is_container: true,
|
||||
has_changes: false,
|
||||
published: true,
|
||||
visibility_state: 'unscheduled',
|
||||
edited_on: 'Jul 02, 2014 at 20:56 UTC',
|
||||
edited_by: 'MockUser'
|
||||
}, options);
|
||||
};
|
||||
|
||||
getItemsOfType = function(type) {
|
||||
@@ -105,40 +126,100 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
|
||||
return outlinePage;
|
||||
};
|
||||
|
||||
verifyTypePublishable = function (type, getMockCourseJSON) {
|
||||
var createCourseOutlinePageAndShowUnit, verifyPublishButton;
|
||||
|
||||
createCourseOutlinePageAndShowUnit = function (test, courseJSON, createOnly) {
|
||||
outlinePage = createCourseOutlinePage.apply(this, arguments);
|
||||
if (type === 'unit') {
|
||||
expandItemsAndVerifyState('subsection');
|
||||
}
|
||||
};
|
||||
|
||||
verifyPublishButton = function (test, courseJSON, createOnly) {
|
||||
createCourseOutlinePageAndShowUnit.apply(this, arguments);
|
||||
expect(getItemHeaders(type).find('.publish-button')).toExist();
|
||||
};
|
||||
|
||||
it('can be published', function() {
|
||||
var mockCourseJSON = getMockCourseJSON({
|
||||
has_changes: true
|
||||
});
|
||||
createCourseOutlinePageAndShowUnit(this, mockCourseJSON);
|
||||
getItemHeaders(type).find('.publish-button').click();
|
||||
$(".wrapper-modal-window .action-publish").click();
|
||||
create_sinon.expectJsonRequest(requests, 'POST', '/xblock/mock-' + type, {
|
||||
publish : 'make_public'
|
||||
});
|
||||
expect(requests[0].requestHeaders['X-HTTP-Method-Override']).toBe('PATCH');
|
||||
create_sinon.respondWithJson(requests, {});
|
||||
create_sinon.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section');
|
||||
});
|
||||
|
||||
it('should show publish button if it is not published and not changed', function() {
|
||||
var mockCourseJSON = getMockCourseJSON({
|
||||
has_changes: false,
|
||||
published: false
|
||||
});
|
||||
verifyPublishButton(this, mockCourseJSON);
|
||||
});
|
||||
|
||||
it('should show publish button if it is published and changed', function() {
|
||||
var mockCourseJSON = getMockCourseJSON({
|
||||
has_changes: true,
|
||||
published: true
|
||||
});
|
||||
verifyPublishButton(this, mockCourseJSON);
|
||||
});
|
||||
|
||||
it('should show publish button if it is not published, but changed', function() {
|
||||
var mockCourseJSON = getMockCourseJSON({
|
||||
has_changes: true,
|
||||
published: false
|
||||
});
|
||||
verifyPublishButton(this, mockCourseJSON);
|
||||
});
|
||||
|
||||
it('should hide publish button if it is not changed, but published', function() {
|
||||
var mockCourseJSON = getMockCourseJSON({
|
||||
has_changes: false,
|
||||
published: true
|
||||
});
|
||||
createCourseOutlinePageAndShowUnit(this, mockCourseJSON);
|
||||
expect(getItemHeaders(type).find('.publish-button')).not.toExist();
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
view_helpers.installMockAnalytics();
|
||||
view_helpers.installViewTemplates();
|
||||
view_helpers.installTemplate('course-outline');
|
||||
view_helpers.installTemplate('xblock-string-field-editor');
|
||||
view_helpers.installTemplate('modal-button');
|
||||
view_helpers.installTemplate('basic-modal');
|
||||
view_helpers.installTemplate('edit-outline-item-modal');
|
||||
view_helpers.installTemplates([
|
||||
'course-outline', 'xblock-string-field-editor', 'modal-button',
|
||||
'basic-modal', 'course-outline-modal', 'release-date-editor',
|
||||
'due-date-editor', 'grading-editor', 'publish-editor',
|
||||
'staff-lock-editor'
|
||||
]);
|
||||
appendSetFixtures(mockOutlinePage);
|
||||
mockCourseJSON = createMockCourseJSON('mock-course', 'Mock Course', [
|
||||
createMockSectionJSON('mock-section', 'Mock Section', [
|
||||
createMockSubsectionJSON('mock-subsection', 'Mock Subsection', [{
|
||||
id: 'mock-unit',
|
||||
display_name: 'Mock Unit',
|
||||
category: 'vertical',
|
||||
studio_url: '/container/mock-unit',
|
||||
is_container: true,
|
||||
has_changes: false,
|
||||
published: true,
|
||||
visibility_state: 'unscheduled',
|
||||
edited_on: 'Jul 02, 2014 at 20:56 UTC',
|
||||
edited_by: 'MockUser'
|
||||
}])
|
||||
mockCourseJSON = createMockCourseJSON({}, [
|
||||
createMockSectionJSON({}, [
|
||||
createMockSubsectionJSON({}, [
|
||||
createMockVerticalJSON()
|
||||
])
|
||||
])
|
||||
]);
|
||||
mockEmptyCourseJSON = createMockCourseJSON('mock-course', 'Mock Course', []);
|
||||
mockSingleSectionCourseJSON = createMockCourseJSON('mock-course', 'Mock Course', [
|
||||
createMockSectionJSON('mock-section', 'Mock Section', [])
|
||||
mockEmptyCourseJSON = createMockCourseJSON();
|
||||
mockSingleSectionCourseJSON = createMockCourseJSON({}, [
|
||||
createMockSectionJSON()
|
||||
]);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
view_helpers.removeMockAnalytics();
|
||||
edit_helpers.cancelModalIfShowing();
|
||||
// Clean up after the $.datepicker
|
||||
$("#start_date").datepicker( "destroy" );
|
||||
$("#due_date").datepicker( "destroy" );
|
||||
$('.ui-datepicker').remove();
|
||||
});
|
||||
|
||||
describe('Initial display', function() {
|
||||
@@ -163,6 +244,18 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
|
||||
});
|
||||
});
|
||||
|
||||
describe("Rerun notification", function () {
|
||||
it("can be dismissed", function () {
|
||||
appendSetFixtures(mockRerunNotification);
|
||||
createCourseOutlinePage(this, mockEmptyCourseJSON);
|
||||
expect($('.wrapper-alert-announcement')).not.toHaveClass('is-hidden');
|
||||
$('.dismiss-button').click();
|
||||
create_sinon.expectJsonRequest(requests, 'DELETE', 'dummy_dismiss_url');
|
||||
create_sinon.respondToDelete(requests);
|
||||
expect($('.wrapper-alert-announcement')).toHaveClass('is-hidden');
|
||||
});
|
||||
});
|
||||
|
||||
describe("Button bar", function() {
|
||||
it('can add a section', function() {
|
||||
createCourseOutlinePage(this, mockEmptyCourseJSON);
|
||||
@@ -198,7 +291,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
|
||||
// Expect the UI to just fetch the new section and repaint it
|
||||
create_sinon.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section-2');
|
||||
create_sinon.respondWithJson(requests,
|
||||
createMockSectionJSON('mock-section-2', 'Mock Section 2', []));
|
||||
createMockSectionJSON({id: 'mock-section-2', display_name: 'Mock Section 2'}));
|
||||
sectionElements = getItemsOfType('section');
|
||||
expect(sectionElements.length).toBe(2);
|
||||
expect($(sectionElements[0]).data('locator')).toEqual('mock-section');
|
||||
@@ -266,9 +359,9 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
|
||||
|
||||
it('can be deleted', function() {
|
||||
var promptSpy = view_helpers.createPromptSpy(), requestCount;
|
||||
createCourseOutlinePage(this, createMockCourseJSON('mock-course', 'Mock Course', [
|
||||
createMockSectionJSON('mock-section', 'Mock Section', []),
|
||||
createMockSectionJSON('mock-section-2', 'Mock Section 2', [])
|
||||
createCourseOutlinePage(this, createMockCourseJSON({}, [
|
||||
createMockSectionJSON(),
|
||||
createMockSectionJSON({id: 'mock-section-2', display_name: 'Mock Section 2'})
|
||||
]));
|
||||
getItemHeaders('section').find('.delete-button').first().click();
|
||||
view_helpers.confirmPrompt(promptSpy);
|
||||
@@ -353,42 +446,34 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
|
||||
outlinePage.$('.section-header-actions .configure-button').click();
|
||||
$("#start_date").val("1/2/2015");
|
||||
// Section release date can't be cleared.
|
||||
expect($(".edit-outline-item-modal .action-clear")).not.toExist();
|
||||
expect($(".wrapper-modal-window .action-clear")).not.toExist();
|
||||
|
||||
// Section does not contain due_date or grading type selector
|
||||
expect($("due_date")).not.toExist();
|
||||
expect($("grading_format")).not.toExist();
|
||||
|
||||
$(".edit-outline-item-modal .action-save").click();
|
||||
|
||||
// Staff lock controls are always visible
|
||||
expect($("#staff_lock")).toExist();
|
||||
$(".wrapper-modal-window .action-save").click();
|
||||
create_sinon.expectJsonRequest(requests, 'POST', '/xblock/mock-section', {
|
||||
"metadata":{
|
||||
"start":"2015-01-02T00:00:00.000Z",
|
||||
"start":"2015-01-02T00:00:00.000Z"
|
||||
}
|
||||
});
|
||||
expect(requests[0].requestHeaders['X-HTTP-Method-Override']).toBe('PATCH');
|
||||
|
||||
// This is the response for the change operation.
|
||||
create_sinon.respondWithJson(requests, {});
|
||||
var mockResponseSectionJSON = $.extend(true, {},
|
||||
createMockSectionJSON('mock-section', 'Mock Section', [
|
||||
createMockSubsectionJSON('mock-subsection', 'Mock Subsection', [{
|
||||
id: 'mock-unit',
|
||||
display_name: 'Mock Unit',
|
||||
category: 'vertical',
|
||||
studio_url: '/container/mock-unit',
|
||||
is_container: true,
|
||||
has_changes: true,
|
||||
published: false,
|
||||
edited_on: 'Jul 02, 2014 at 20:56 UTC',
|
||||
edited_by: 'MockUser'
|
||||
}
|
||||
var mockResponseSectionJSON = createMockSectionJSON({
|
||||
release_date: 'Jan 02, 2015 at 00:00 UTC'
|
||||
}, [
|
||||
createMockSubsectionJSON({}, [
|
||||
createMockVerticalJSON({
|
||||
has_changes: true,
|
||||
published: false
|
||||
})
|
||||
])
|
||||
]),
|
||||
{
|
||||
release_date: 'Jan 02, 2015 at 00:00 UTC',
|
||||
}
|
||||
);
|
||||
]);
|
||||
create_sinon.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section')
|
||||
expect(requests.length).toBe(2);
|
||||
// This is the response for the subsequent fetch operation for the section.
|
||||
@@ -396,6 +481,46 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
|
||||
|
||||
expect($(".outline-section .status-release-value")).toContainText("Jan 02, 2015 at 00:00 UTC");
|
||||
});
|
||||
|
||||
verifyTypePublishable('section', function (options) {
|
||||
return createMockCourseJSON({}, [
|
||||
createMockSectionJSON(options, [
|
||||
createMockSubsectionJSON({}, [
|
||||
createMockVerticalJSON()
|
||||
])
|
||||
])
|
||||
]);
|
||||
});
|
||||
|
||||
it('can display a publish modal with a list of unpublished subsections and units', function () {
|
||||
var mockCourseJSON = createMockCourseJSON({}, [
|
||||
createMockSectionJSON({has_changes: true}, [
|
||||
createMockSubsectionJSON({has_changes: true}, [
|
||||
createMockVerticalJSON(),
|
||||
createMockVerticalJSON({has_changes: true, display_name: 'Unit 100'}),
|
||||
createMockVerticalJSON({published: false, display_name: 'Unit 50'})
|
||||
]),
|
||||
createMockSubsectionJSON({has_changes: true}, [
|
||||
createMockVerticalJSON({has_changes: true, display_name: 'Unit 1'})
|
||||
]),
|
||||
createMockSubsectionJSON({}, [createMockVerticalJSON])
|
||||
]),
|
||||
createMockSectionJSON({has_changes: true}, [
|
||||
createMockSubsectionJSON({has_changes: true}, [
|
||||
createMockVerticalJSON({has_changes: true}),
|
||||
])
|
||||
])
|
||||
]), modalWindow;
|
||||
|
||||
createCourseOutlinePage(this, mockCourseJSON, false);
|
||||
getItemHeaders('section').first().find('.publish-button').click();
|
||||
modalWindow = $('.wrapper-modal-window');
|
||||
expect(modalWindow.find('.outline-unit').length).toBe(3);
|
||||
expect(_.compact(_.map(modalWindow.find('.outline-unit').text().split("\n"), $.trim))).toEqual(
|
||||
['Unit 100', 'Unit 50', 'Unit 1']
|
||||
)
|
||||
expect(modalWindow.find('.outline-subsection').length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Subsection", function() {
|
||||
@@ -405,42 +530,33 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
|
||||
return getItemHeaders('subsection').find('.wrapper-xblock-field');
|
||||
};
|
||||
|
||||
setEditModalValues = function (start_date, due_date, grading_type) {
|
||||
setEditModalValues = function (start_date, due_date, grading_type, is_locked) {
|
||||
$("#start_date").val(start_date);
|
||||
$("#due_date").val(due_date);
|
||||
$("#grading_type").val(grading_type);
|
||||
}
|
||||
$("#staff_lock").prop('checked', is_locked);
|
||||
};
|
||||
|
||||
// Contains hard-coded dates because dates are presented in different formats.
|
||||
var mockServerValuesJson = $.extend(true, {},
|
||||
createMockSectionJSON('mock-section', 'Mock Section', [
|
||||
createMockSubsectionJSON('mock-subsection', 'Mock Subsection', [{
|
||||
id: 'mock-unit',
|
||||
display_name: 'Mock Unit',
|
||||
category: 'vertical',
|
||||
studio_url: '/container/mock-unit',
|
||||
is_container: true,
|
||||
has_changes: true,
|
||||
published: false,
|
||||
edited_on: 'Jul 02, 2014 at 20:56 UTC',
|
||||
edited_by: 'MockUser'
|
||||
}
|
||||
var mockServerValuesJson = createMockSectionJSON({
|
||||
release_date: 'Jan 01, 2970 at 05:00 UTC'
|
||||
}, [
|
||||
createMockSubsectionJSON({
|
||||
graded: true,
|
||||
due_date: 'Jul 10, 2014 at 00:00 UTC',
|
||||
release_date: 'Jul 09, 2014 at 00:00 UTC',
|
||||
start: "2014-07-09T00:00:00Z",
|
||||
format: "Lab",
|
||||
due: "2014-07-10T00:00:00Z",
|
||||
has_explicit_staff_lock: true,
|
||||
staff_only_message: true
|
||||
}, [
|
||||
createMockVerticalJSON({
|
||||
has_changes: true,
|
||||
published: false
|
||||
})
|
||||
])
|
||||
]),
|
||||
{
|
||||
release_date: 'Jan 01, 2970 at 05:00 UTC',
|
||||
child_info: { //Section child_info
|
||||
children: [{ // Section children
|
||||
graded: true,
|
||||
due_date: 'Jul 10, 2014 at 00:00 UTC',
|
||||
release_date: 'Jul 09, 2014 at 00:00 UTC',
|
||||
start: "2014-07-09T00:00:00Z",
|
||||
format: "Lab",
|
||||
due: "2014-07-10T00:00:00Z"
|
||||
}]
|
||||
}
|
||||
}
|
||||
);
|
||||
]);
|
||||
|
||||
it('can be deleted', function() {
|
||||
var promptSpy = view_helpers.createPromptSpy();
|
||||
@@ -483,9 +599,12 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
|
||||
create_sinon.respondWithJson(requests, { });
|
||||
// This is the response for the subsequent fetch operation for the section.
|
||||
create_sinon.respondWithJson(requests,
|
||||
createMockSectionJSON('mock-section', 'Mock Section', [
|
||||
createMockSubsectionJSON('mock-subsection', updatedDisplayName, [])
|
||||
]));
|
||||
createMockSectionJSON({}, [
|
||||
createMockSubsectionJSON({
|
||||
display_name: updatedDisplayName
|
||||
})
|
||||
])
|
||||
);
|
||||
// Find the display name again in the refreshed DOM and verify it
|
||||
displayNameWrapper = getItemHeaders('subsection').find('.wrapper-xblock-field');
|
||||
view_helpers.verifyInlineEditChange(displayNameWrapper, updatedDisplayName);
|
||||
@@ -504,11 +623,13 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
|
||||
it('can be edited', function() {
|
||||
createCourseOutlinePage(this, mockCourseJSON, false);
|
||||
outlinePage.$('.outline-subsection .configure-button').click();
|
||||
setEditModalValues("7/9/2014", "7/10/2014", "Lab");
|
||||
$(".edit-outline-item-modal .action-save").click();
|
||||
setEditModalValues("7/9/2014", "7/10/2014", "Lab", true);
|
||||
$(".wrapper-modal-window .action-save").click();
|
||||
create_sinon.expectJsonRequest(requests, 'POST', '/xblock/mock-subsection', {
|
||||
"graderType":"Lab",
|
||||
"publish": "republish",
|
||||
"metadata":{
|
||||
"visible_to_staff_only": true,
|
||||
"start":"2014-07-09T00:00:00.000Z",
|
||||
"due":"2014-07-10T00:00:00.000Z"
|
||||
}
|
||||
@@ -525,19 +646,21 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
|
||||
expect($(".outline-subsection .status-release-value")).toContainText("Jul 09, 2014 at 00:00 UTC");
|
||||
expect($(".outline-subsection .status-grading-date")).toContainText("Due: Jul 10, 2014 at 00:00 UTC");
|
||||
expect($(".outline-subsection .status-grading-value")).toContainText("Lab");
|
||||
expect($(".outline-subsection .status-message-copy")).toContainText("Contains staff only content");
|
||||
|
||||
expect($(".outline-item .outline-subsection .status-grading-value")).toContainText("Lab");
|
||||
outlinePage.$('.outline-item .outline-subsection .configure-button').click();
|
||||
expect($("#start_date").val()).toBe('7/9/2014');
|
||||
expect($("#due_date").val()).toBe('7/10/2014');
|
||||
expect($("#grading_type").val()).toBe('Lab');
|
||||
expect($("#staff_lock").is(":checked")).toBe(true);
|
||||
});
|
||||
|
||||
it('release date, due date and grading type can be cleared.', function() {
|
||||
it('release date, due date, grading type, and staff lock can be cleared.', function() {
|
||||
createCourseOutlinePage(this, mockCourseJSON, false);
|
||||
outlinePage.$('.outline-item .outline-subsection .configure-button').click();
|
||||
setEditModalValues("7/9/2014", "7/10/2014", "Lab");
|
||||
$(".edit-outline-item-modal .action-save").click();
|
||||
setEditModalValues("7/9/2014", "7/10/2014", "Lab", true);
|
||||
$(".wrapper-modal-window .action-save").click();
|
||||
|
||||
// This is the response for the change operation.
|
||||
create_sinon.respondWithJson(requests, {});
|
||||
@@ -547,32 +670,69 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
|
||||
expect($(".outline-subsection .status-release-value")).toContainText("Jul 09, 2014 at 00:00 UTC");
|
||||
expect($(".outline-subsection .status-grading-date")).toContainText("Due: Jul 10, 2014 at 00:00 UTC");
|
||||
expect($(".outline-subsection .status-grading-value")).toContainText("Lab");
|
||||
expect($(".outline-subsection .status-message-copy")).toContainText("Contains staff only content");
|
||||
|
||||
outlinePage.$('.outline-subsection .configure-button').click();
|
||||
expect($("#start_date").val()).toBe('7/9/2014');
|
||||
expect($("#due_date").val()).toBe('7/10/2014');
|
||||
expect($("#grading_type").val()).toBe('Lab');
|
||||
expect($("#staff_lock").is(":checked")).toBe(true);
|
||||
|
||||
$(".edit-outline-item-modal .scheduled-date-input .action-clear").click();
|
||||
$(".edit-outline-item-modal .due-date-input .action-clear").click();
|
||||
$(".wrapper-modal-window .scheduled-date-input .action-clear").click();
|
||||
$(".wrapper-modal-window .due-date-input .action-clear").click();
|
||||
expect($("#start_date").val()).toBe('');
|
||||
expect($("#due_date").val()).toBe('');
|
||||
|
||||
$("#grading_type").val('notgraded');
|
||||
$("#staff_lock").prop('checked', false);
|
||||
|
||||
$(".edit-outline-item-modal .action-save").click();
|
||||
$(".wrapper-modal-window .action-save").click();
|
||||
|
||||
// This is the response for the change operation.
|
||||
create_sinon.respondWithJson(requests, {});
|
||||
// This is the response for the subsequent fetch operation.
|
||||
create_sinon.respondWithJson(requests,
|
||||
createMockSectionJSON('mock-section', 'Mock Section', [
|
||||
createMockSubsectionJSON('mock-subsection', 'Mock Subsection', [])
|
||||
])
|
||||
createMockSectionJSON({}, [createMockSubsectionJSON()])
|
||||
);
|
||||
expect($(".outline-subsection .status-release-value")).not.toContainText("Jul 09, 2014 at 00:00 UTC");
|
||||
expect($(".outline-subsection .status-grading-date")).not.toExist();
|
||||
expect($(".outline-subsection .status-grading-value")).not.toExist();
|
||||
expect($(".outline-subsection .status-message-copy")).not.toContainText("Contains staff only content");
|
||||
});
|
||||
|
||||
verifyTypePublishable('subsection', function (options) {
|
||||
return createMockCourseJSON({}, [
|
||||
createMockSectionJSON({}, [
|
||||
createMockSubsectionJSON(options, [
|
||||
createMockVerticalJSON()
|
||||
])
|
||||
])
|
||||
]);
|
||||
});
|
||||
|
||||
it('can display a publish modal with a list of unpublished units', function () {
|
||||
var mockCourseJSON = createMockCourseJSON({}, [
|
||||
createMockSectionJSON({has_changes: true}, [
|
||||
createMockSubsectionJSON({has_changes: true}, [
|
||||
createMockVerticalJSON(),
|
||||
createMockVerticalJSON({has_changes: true, display_name: "Unit 100"}),
|
||||
createMockVerticalJSON({published: false, display_name: "Unit 50"})
|
||||
]),
|
||||
createMockSubsectionJSON({has_changes: true}, [
|
||||
createMockVerticalJSON({has_changes: true})
|
||||
]),
|
||||
createMockSubsectionJSON({}, [createMockVerticalJSON])
|
||||
])
|
||||
]), modalWindow;
|
||||
|
||||
createCourseOutlinePage(this, mockCourseJSON, false);
|
||||
getItemHeaders('subsection').first().find('.publish-button').click();
|
||||
modalWindow = $('.wrapper-modal-window');
|
||||
expect(modalWindow.find('.outline-unit').length).toBe(2);
|
||||
expect(_.compact(_.map(modalWindow.find('.outline-unit').text().split("\n"), $.trim))).toEqual(
|
||||
['Unit 100', 'Unit 50']
|
||||
)
|
||||
expect(modalWindow.find('.outline-subsection')).not.toExist();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -598,6 +758,16 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
|
||||
unitAnchor = getItemsOfType('unit').find('.unit-title a');
|
||||
expect(unitAnchor.attr('href')).toBe('/container/mock-unit');
|
||||
});
|
||||
|
||||
verifyTypePublishable('unit', function (options) {
|
||||
return createMockCourseJSON({}, [
|
||||
createMockSectionJSON({}, [
|
||||
createMockSubsectionJSON({}, [
|
||||
createMockVerticalJSON(options)
|
||||
])
|
||||
])
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Date and Time picker", function() {
|
||||
|
||||
205
cms/static/js/spec/views/pages/course_rerun_spec.js
Normal file
205
cms/static/js/spec/views/pages/course_rerun_spec.js
Normal file
@@ -0,0 +1,205 @@
|
||||
define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers", "js/views/course_rerun",
|
||||
"js/views/utils/create_course_utils", "js/views/utils/view_utils", "jquery.simulate"],
|
||||
function ($, create_sinon, view_helpers, CourseRerunUtils, CreateCourseUtilsFactory, ViewUtils) {
|
||||
describe("Create course rerun page", function () {
|
||||
var selectors = {
|
||||
org: '.rerun-course-org',
|
||||
number: '.rerun-course-number',
|
||||
run: '.rerun-course-run',
|
||||
name: '.rerun-course-name',
|
||||
tipError: 'span.tip-error',
|
||||
save: '.rerun-course-save',
|
||||
cancel: '.rerun-course-cancel',
|
||||
errorWrapper: '.wrapper-error',
|
||||
errorMessage: '#course_rerun_error',
|
||||
error: '.error',
|
||||
allowUnicode: '.allow-unicode-course-id'
|
||||
},
|
||||
classes = {
|
||||
shown: 'is-shown',
|
||||
showing: 'is-showing',
|
||||
hiding: 'is-hidden',
|
||||
hidden: 'is-hidden',
|
||||
error: 'error',
|
||||
disabled: 'is-disabled',
|
||||
processing: 'is-processing'
|
||||
},
|
||||
mockCreateCourseRerunHTML = readFixtures('mock/mock-create-course-rerun.underscore');
|
||||
|
||||
var CreateCourseUtils = CreateCourseUtilsFactory(selectors, classes);
|
||||
|
||||
var fillInFields = function (org, number, run, name) {
|
||||
$(selectors.org).val(org);
|
||||
$(selectors.number).val(number);
|
||||
$(selectors.run).val(run);
|
||||
$(selectors.name).val(name);
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
view_helpers.installMockAnalytics();
|
||||
window.source_course_key = 'test_course_key';
|
||||
appendSetFixtures(mockCreateCourseRerunHTML);
|
||||
CourseRerunUtils.onReady();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
view_helpers.removeMockAnalytics();
|
||||
delete window.source_course_key;
|
||||
});
|
||||
|
||||
describe("Field validation", function () {
|
||||
it("returns a message for an empty string", function () {
|
||||
var message = CreateCourseUtils.validateRequiredField('');
|
||||
expect(message).not.toBe('');
|
||||
});
|
||||
|
||||
it("does not return a message for a non empty string", function () {
|
||||
var message = CreateCourseUtils.validateRequiredField('edX');
|
||||
expect(message).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe("Error messages", function () {
|
||||
var setErrorMessage = function(selector, message) {
|
||||
var element = $(selector).parent();
|
||||
CreateCourseUtils.setNewCourseFieldInErr(element, message);
|
||||
return element;
|
||||
};
|
||||
|
||||
var type = function (input, value) {
|
||||
input.val(value);
|
||||
input.simulate("keyup", { keyCode: $.simulate.keyCode.SPACE });
|
||||
};
|
||||
|
||||
it("shows an error message", function () {
|
||||
var element = setErrorMessage(selectors.org, 'error message');
|
||||
expect(element).toHaveClass(classes.error);
|
||||
expect(element.children(selectors.tipError)).not.toHaveClass(classes.hidden);
|
||||
expect(element.children(selectors.tipError)).toContainText('error message');
|
||||
});
|
||||
|
||||
it("hides an error message", function () {
|
||||
var element = setErrorMessage(selectors.org, '');
|
||||
expect(element).not.toHaveClass(classes.error);
|
||||
expect(element.children(selectors.tipError)).toHaveClass(classes.hidden);
|
||||
});
|
||||
|
||||
it("disables the save button", function () {
|
||||
setErrorMessage(selectors.org, 'error message');
|
||||
expect($(selectors.save)).toHaveClass(classes.disabled);
|
||||
});
|
||||
|
||||
it("enables the save button when all errors are removed", function () {
|
||||
setErrorMessage(selectors.org, 'error message 1');
|
||||
setErrorMessage(selectors.number, 'error message 2');
|
||||
expect($(selectors.save)).toHaveClass(classes.disabled);
|
||||
setErrorMessage(selectors.org, '');
|
||||
setErrorMessage(selectors.number, '');
|
||||
expect($(selectors.save)).not.toHaveClass(classes.disabled);
|
||||
});
|
||||
|
||||
it("does not enable the save button when errors remain", function () {
|
||||
setErrorMessage(selectors.org, 'error message 1');
|
||||
setErrorMessage(selectors.number, 'error message 2');
|
||||
expect($(selectors.save)).toHaveClass(classes.disabled);
|
||||
setErrorMessage(selectors.org, '');
|
||||
expect($(selectors.save)).toHaveClass(classes.disabled);
|
||||
});
|
||||
|
||||
it("shows an error message when non URL characters are entered", function () {
|
||||
var input = $(selectors.org);
|
||||
expect(input.parent()).not.toHaveClass(classes.error);
|
||||
type(input, "%");
|
||||
expect(input.parent()).toHaveClass(classes.error);
|
||||
});
|
||||
|
||||
it("does not show an error message when tabbing into a field", function () {
|
||||
var input = $(selectors.number);
|
||||
input.val('');
|
||||
expect(input.parent()).not.toHaveClass(classes.error);
|
||||
input.simulate("keyup", { keyCode: $.simulate.keyCode.TAB });
|
||||
expect(input.parent()).not.toHaveClass(classes.error);
|
||||
});
|
||||
|
||||
it("shows an error message when a required field is empty", function () {
|
||||
var input = $(selectors.org);
|
||||
input.val('');
|
||||
expect(input.parent()).not.toHaveClass(classes.error);
|
||||
input.simulate("keyup", { keyCode: $.simulate.keyCode.ENTER });
|
||||
expect(input.parent()).toHaveClass(classes.error);
|
||||
});
|
||||
|
||||
it("shows an error message when spaces are entered and unicode is allowed", function () {
|
||||
var input = $(selectors.org);
|
||||
$(selectors.allowUnicode).val('True');
|
||||
expect(input.parent()).not.toHaveClass(classes.error);
|
||||
type(input, ' ');
|
||||
expect(input.parent()).toHaveClass(classes.error);
|
||||
});
|
||||
|
||||
it("shows an error message when total length exceeds 65 characters", function () {
|
||||
expect($(selectors.errorWrapper)).not.toHaveClass(classes.shown);
|
||||
type($(selectors.org), 'ThisIsAVeryLongNameThatWillExceedTheSixtyFiveCharacterLimit');
|
||||
type($(selectors.number), 'ThisIsAVeryLongNameThatWillExceedTheSixtyFiveCharacterLimit');
|
||||
type($(selectors.run), 'ThisIsAVeryLongNameThatWillExceedTheSixtyFiveCharacterLimit');
|
||||
expect($(selectors.errorWrapper)).toHaveClass(classes.shown);
|
||||
});
|
||||
|
||||
describe("Name field", function () {
|
||||
it("does not show an error message when non URL characters are entered", function () {
|
||||
var input = $(selectors.name);
|
||||
expect(input.parent()).not.toHaveClass(classes.error);
|
||||
type(input, "%");
|
||||
expect(input.parent()).not.toHaveClass(classes.error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("saves course reruns", function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
var redirectSpy = spyOn(ViewUtils, 'redirect')
|
||||
fillInFields('DemoX', 'DM101', '2014', 'Demo course');
|
||||
$(selectors.save).click();
|
||||
create_sinon.expectJsonRequest(requests, 'POST', '/course/', {
|
||||
source_course_key: 'test_course_key',
|
||||
org: 'DemoX',
|
||||
number: 'DM101',
|
||||
run: '2014',
|
||||
display_name: 'Demo course'
|
||||
});
|
||||
expect($(selectors.save)).toHaveClass(classes.disabled);
|
||||
expect($(selectors.save)).toHaveClass(classes.processing);
|
||||
expect($(selectors.cancel)).toHaveClass(classes.hidden);
|
||||
create_sinon.respondWithJson(requests, {
|
||||
url: 'dummy_test_url'
|
||||
});
|
||||
expect(redirectSpy).toHaveBeenCalledWith('dummy_test_url');
|
||||
});
|
||||
|
||||
it("displays an error when saving fails", function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
fillInFields('DemoX', 'DM101', '2014', 'Demo course');
|
||||
$(selectors.save).click();
|
||||
create_sinon.respondWithJson(requests, {
|
||||
ErrMsg: 'error message'
|
||||
});
|
||||
expect($(selectors.errorWrapper)).not.toHaveClass(classes.hidden);
|
||||
expect($(selectors.errorWrapper)).toContainText('error message');
|
||||
expect($(selectors.save)).not.toHaveClass(classes.processing);
|
||||
expect($(selectors.cancel)).not.toHaveClass(classes.hidden);
|
||||
});
|
||||
|
||||
it("does not save if there are validation errors", function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
fillInFields('DemoX', 'DM101', '', 'Demo course');
|
||||
$(selectors.save).click();
|
||||
expect(requests.length).toBe(0);
|
||||
});
|
||||
|
||||
it("can be canceled", function () {
|
||||
var redirectSpy = spyOn(ViewUtils, 'redirect');
|
||||
$(selectors.cancel).click();
|
||||
expect(redirectSpy).toHaveBeenCalledWith('/course/');
|
||||
});
|
||||
});
|
||||
});
|
||||
65
cms/static/js/spec/views/pages/index_spec.js
Normal file
65
cms/static/js/spec/views/pages/index_spec.js
Normal file
@@ -0,0 +1,65 @@
|
||||
define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers", "js/index",
|
||||
"js/views/utils/view_utils"],
|
||||
function ($, create_sinon, view_helpers, IndexUtils, ViewUtils) {
|
||||
describe("Course listing page", function () {
|
||||
var mockIndexPageHTML = readFixtures('mock/mock-index-page.underscore'), fillInFields;
|
||||
|
||||
var fillInFields = function (org, number, run, name) {
|
||||
$('.new-course-org').val(org);
|
||||
$('.new-course-number').val(number);
|
||||
$('.new-course-run').val(run);
|
||||
$('.new-course-name').val(name);
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
view_helpers.installMockAnalytics();
|
||||
appendSetFixtures(mockIndexPageHTML);
|
||||
IndexUtils.onReady();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
view_helpers.removeMockAnalytics();
|
||||
delete window.source_course_key;
|
||||
});
|
||||
|
||||
it("can dismiss notifications", function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
var reloadSpy = spyOn(ViewUtils, 'reload');
|
||||
$('.dismiss-button').click();
|
||||
create_sinon.expectJsonRequest(requests, 'DELETE', 'dummy_dismiss_url');
|
||||
create_sinon.respondToDelete(requests);
|
||||
expect(reloadSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("saves new courses", function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
var redirectSpy = spyOn(ViewUtils, 'redirect');
|
||||
$('.new-course-button').click()
|
||||
fillInFields('DemoX', 'DM101', '2014', 'Demo course');
|
||||
$('.new-course-save').click();
|
||||
create_sinon.expectJsonRequest(requests, 'POST', '/course/', {
|
||||
org: 'DemoX',
|
||||
number: 'DM101',
|
||||
run: '2014',
|
||||
display_name: 'Demo course'
|
||||
});
|
||||
create_sinon.respondWithJson(requests, {
|
||||
url: 'dummy_test_url'
|
||||
});
|
||||
expect(redirectSpy).toHaveBeenCalledWith('dummy_test_url');
|
||||
});
|
||||
|
||||
it("displays an error when saving fails", function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
$('.new-course-button').click();
|
||||
fillInFields('DemoX', 'DM101', '2014', 'Demo course');
|
||||
$('.new-course-save').click();
|
||||
create_sinon.respondWithJson(requests, {
|
||||
ErrMsg: 'error message'
|
||||
});
|
||||
expect($('.wrap-error')).toHaveClass('is-shown');
|
||||
expect($('#course_creation_error')).toContainText('error message');
|
||||
expect($('.new-course-save')).toHaveClass('is-disabled');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -260,6 +260,10 @@ define(["jquery", "jquery.ui", "underscore", "gettext", "js/views/feedback_notif
|
||||
},
|
||||
|
||||
expandElement: function (ele) {
|
||||
// Verify all children of the element are rendered.
|
||||
var ensureChildrenRendered = ele.data('ensureChildrenRendered');
|
||||
if (_.isFunction(ensureChildrenRendered)) { ensureChildrenRendered(); }
|
||||
// Update classes.
|
||||
ele.removeClass(this.collapsedClass);
|
||||
ele.find('.expand-collapse').first().removeClass('expand').addClass('collapse');
|
||||
},
|
||||
@@ -354,7 +358,8 @@ define(["jquery", "jquery.ui", "underscore", "gettext", "js/views/feedback_notif
|
||||
handleClass: null,
|
||||
droppableClass: null,
|
||||
parentLocationSelector: null,
|
||||
refresh: null
|
||||
refresh: null,
|
||||
ensureChildrenRendered: null
|
||||
}, options);
|
||||
|
||||
if ($(element).data('droppable-class') !== options.droppableClass) {
|
||||
@@ -362,7 +367,8 @@ define(["jquery", "jquery.ui", "underscore", "gettext", "js/views/feedback_notif
|
||||
'droppable-class': options.droppableClass,
|
||||
'parent-location-selector': options.parentLocationSelector,
|
||||
'child-selector': options.type,
|
||||
'refresh': options.refresh
|
||||
'refresh': options.refresh,
|
||||
'ensureChildrenRendered': options.ensureChildrenRendered
|
||||
});
|
||||
|
||||
draggable = new Draggabilly(element, {
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
define(["jquery", "underscore", "js/views/xblock", "js/utils/module", "gettext", "js/views/feedback_notification",
|
||||
"jquery.ui"], // The container view uses sortable, which is provided by jquery.ui.
|
||||
function ($, _, XBlockView, ModuleUtils, gettext, NotificationView) {
|
||||
var reorderableClass = '.reorderable-container',
|
||||
sortableInitializedClass = '.ui-sortable',
|
||||
studioXBlockWrapperClass = '.studio-xblock-wrapper';
|
||||
var studioXBlockWrapperClass = '.studio-xblock-wrapper';
|
||||
|
||||
var ContainerView = XBlockView.extend({
|
||||
// Store the request token of the first xblock on the page (which we know was rendered by Studio when
|
||||
// the page was generated). Use that request token to filter out user-defined HTML in any
|
||||
// child xblocks within the page.
|
||||
requestToken: "",
|
||||
|
||||
xblockReady: function () {
|
||||
XBlockView.prototype.xblockReady.call(this);
|
||||
var reorderableContainer = this.$(reorderableClass),
|
||||
alreadySortable = this.$(sortableInitializedClass),
|
||||
newParent,
|
||||
oldParent,
|
||||
self = this;
|
||||
var reorderableClass, reorderableContainer,
|
||||
newParent, oldParent, self = this;
|
||||
|
||||
alreadySortable.sortable("destroy");
|
||||
this.requestToken = this.$('div.xblock').first().data('request-token');
|
||||
reorderableClass = this.makeRequestSpecificSelector('.reorderable-container');
|
||||
|
||||
reorderableContainer = this.$(reorderableClass);
|
||||
reorderableContainer.sortable({
|
||||
handle: '.drag-handle',
|
||||
|
||||
@@ -123,7 +124,12 @@ define(["jquery", "underscore", "js/views/xblock", "js/utils/module", "gettext",
|
||||
},
|
||||
|
||||
refresh: function() {
|
||||
var sortableInitializedClass = this.makeRequestSpecificSelector('.reorderable-container.ui-sortable');
|
||||
this.$(sortableInitializedClass).sortable('refresh');
|
||||
},
|
||||
|
||||
makeRequestSpecificSelector: function(selector) {
|
||||
return 'div.xblock[data-request-token="' + this.requestToken + '"] > ' + selector;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -8,9 +8,12 @@
|
||||
* - changes cause a refresh of the entire section rather than just the view for the changed xblock
|
||||
* - adding units will automatically redirect to the unit page rather than showing them inline
|
||||
*/
|
||||
define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_utils",
|
||||
"js/models/xblock_outline_info", "js/views/modals/edit_outline_item", "js/utils/drag_and_drop"],
|
||||
function($, _, XBlockOutlineView, ViewUtils, XBlockOutlineInfo, EditSectionXBlockModal, ContentDragger) {
|
||||
define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_utils", "js/views/utils/xblock_utils",
|
||||
"js/models/xblock_outline_info", "js/views/modals/course_outline_modals", "js/utils/drag_and_drop"],
|
||||
function(
|
||||
$, _, XBlockOutlineView, ViewUtils, XBlockViewUtils,
|
||||
XBlockOutlineInfo, CourseOutlineModalsFactory, ContentDragger
|
||||
) {
|
||||
|
||||
var CourseOutlineView = XBlockOutlineView.extend({
|
||||
// takes XBlockOutlineInfo as a model
|
||||
@@ -144,12 +147,30 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_
|
||||
},
|
||||
|
||||
editXBlock: function() {
|
||||
var modal = new EditSectionXBlockModal({
|
||||
model: this.model,
|
||||
onSave: this.refresh.bind(this)
|
||||
var modal = CourseOutlineModalsFactory.getModal('edit', this.model, {
|
||||
onSave: this.refresh.bind(this),
|
||||
parentInfo: this.parentInfo,
|
||||
xblockType: XBlockViewUtils.getXBlockType(
|
||||
this.model.get('category'), this.parentView.model, true
|
||||
)
|
||||
});
|
||||
|
||||
modal.show();
|
||||
if (modal) {
|
||||
modal.show();
|
||||
}
|
||||
},
|
||||
|
||||
publishXBlock: function() {
|
||||
var modal = CourseOutlineModalsFactory.getModal('publish', this.model, {
|
||||
onSave: this.refresh.bind(this),
|
||||
xblockType: XBlockViewUtils.getXBlockType(
|
||||
this.model.get('category'), this.parentView.model, true
|
||||
)
|
||||
});
|
||||
|
||||
if (modal) {
|
||||
modal.show();
|
||||
}
|
||||
},
|
||||
|
||||
addButtonActions: function(element) {
|
||||
@@ -158,6 +179,10 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_
|
||||
event.preventDefault();
|
||||
this.editXBlock();
|
||||
}.bind(this));
|
||||
element.find('.publish-button').click(function(event) {
|
||||
event.preventDefault();
|
||||
this.publishXBlock();
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
makeContentDraggable: function(element) {
|
||||
@@ -167,7 +192,8 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_
|
||||
handleClass: '.section-drag-handle',
|
||||
droppableClass: 'ol.list-sections',
|
||||
parentLocationSelector: 'article.outline',
|
||||
refresh: this.refreshWithCollapsedState.bind(this)
|
||||
refresh: this.refreshWithCollapsedState.bind(this),
|
||||
ensureChildrenRendered: this.ensureChildrenRendered.bind(this)
|
||||
});
|
||||
}
|
||||
else if ($(element).hasClass("outline-subsection")) {
|
||||
@@ -176,7 +202,8 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_
|
||||
handleClass: '.subsection-drag-handle',
|
||||
droppableClass: 'ol.list-subsections',
|
||||
parentLocationSelector: 'li.outline-section',
|
||||
refresh: this.refreshWithCollapsedState.bind(this)
|
||||
refresh: this.refreshWithCollapsedState.bind(this),
|
||||
ensureChildrenRendered: this.ensureChildrenRendered.bind(this)
|
||||
});
|
||||
}
|
||||
else if ($(element).hasClass("outline-unit")) {
|
||||
@@ -185,7 +212,8 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_
|
||||
handleClass: '.unit-drag-handle',
|
||||
droppableClass: 'ol.list-units',
|
||||
parentLocationSelector: 'li.outline-subsection',
|
||||
refresh: this.refreshWithCollapsedState.bind(this)
|
||||
refresh: this.refreshWithCollapsedState.bind(this),
|
||||
ensureChildrenRendered: this.ensureChildrenRendered.bind(this)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
87
cms/static/js/views/course_rerun.js
Normal file
87
cms/static/js/views/course_rerun.js
Normal file
@@ -0,0 +1,87 @@
|
||||
define(["domReady", "jquery", "underscore", "js/views/utils/create_course_utils", "js/views/utils/view_utils"],
|
||||
function (domReady, $, _, CreateCourseUtilsFactory, ViewUtils) {
|
||||
var CreateCourseUtils = CreateCourseUtilsFactory({
|
||||
name: '.rerun-course-name',
|
||||
org: '.rerun-course-org',
|
||||
number: '.rerun-course-number',
|
||||
run: '.rerun-course-run',
|
||||
save: '.rerun-course-save',
|
||||
errorWrapper: '.wrapper-error',
|
||||
errorMessage: '#course_rerun_error',
|
||||
tipError: 'span.tip-error',
|
||||
error: '.error',
|
||||
allowUnicode: '.allow-unicode-course-id'
|
||||
}, {
|
||||
shown: 'is-shown',
|
||||
showing: 'is-showing',
|
||||
hiding: 'is-hidden',
|
||||
disabled: 'is-disabled',
|
||||
error: 'error'
|
||||
});
|
||||
|
||||
var saveRerunCourse = function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (CreateCourseUtils.hasInvalidRequiredFields()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var $newCourseForm = $(this).closest('#rerun-course-form');
|
||||
var display_name = $newCourseForm.find('.rerun-course-name').val();
|
||||
var org = $newCourseForm.find('.rerun-course-org').val();
|
||||
var number = $newCourseForm.find('.rerun-course-number').val();
|
||||
var run = $newCourseForm.find('.rerun-course-run').val();
|
||||
|
||||
course_info = {
|
||||
source_course_key: source_course_key,
|
||||
org: org,
|
||||
number: number,
|
||||
display_name: display_name,
|
||||
run: run
|
||||
};
|
||||
|
||||
analytics.track('Reran a Course', course_info);
|
||||
CreateCourseUtils.createCourse(course_info, function (errorMessage) {
|
||||
$('.wrapper-error').addClass('is-shown').removeClass('is-hidden');
|
||||
$('#course_rerun_error').html('<p>' + errorMessage + '</p>');
|
||||
$('.rerun-course-save').addClass('is-disabled').removeClass('is-processing').html(gettext('Create Re-run'));
|
||||
$('.action-cancel').removeClass('is-hidden');
|
||||
});
|
||||
|
||||
// Go into creating re-run state
|
||||
$('.rerun-course-save').addClass('is-disabled').addClass('is-processing').html(
|
||||
'<i class="icon icon-refresh icon-spin"></i>' + gettext('Processing Re-run Request')
|
||||
);
|
||||
$('.action-cancel').addClass('is-hidden');
|
||||
};
|
||||
|
||||
var cancelRerunCourse = function (e) {
|
||||
e.preventDefault();
|
||||
// Clear out existing fields and errors
|
||||
$('.rerun-course-run').val('');
|
||||
$('#course_rerun_error').html('');
|
||||
$('wrapper-error').removeClass('is-shown').addClass('is-hidden');
|
||||
$('.rerun-course-save').off('click');
|
||||
ViewUtils.redirect('/course/');
|
||||
};
|
||||
|
||||
var onReady = function () {
|
||||
var $cancelButton = $('.rerun-course-cancel');
|
||||
var $courseRun = $('.rerun-course-run');
|
||||
$courseRun.focus().select();
|
||||
$('.rerun-course-save').on('click', saveRerunCourse);
|
||||
$cancelButton.bind('click', cancelRerunCourse);
|
||||
$('.cancel-button').bind('click', cancelRerunCourse);
|
||||
|
||||
CreateCourseUtils.configureHandlers();
|
||||
};
|
||||
|
||||
domReady(onReady);
|
||||
|
||||
// Return these functions so that they can be tested
|
||||
return {
|
||||
saveRerunCourse: saveRerunCourse,
|
||||
cancelRerunCourse: cancelRerunCourse,
|
||||
onReady: onReady
|
||||
};
|
||||
});
|
||||
379
cms/static/js/views/modals/course_outline_modals.js
Normal file
379
cms/static/js/views/modals/course_outline_modals.js
Normal file
@@ -0,0 +1,379 @@
|
||||
/**
|
||||
* The CourseOutlineXBlockModal is a Backbone view that shows an editor in a modal window.
|
||||
* It has nested views: for release date, due date and grading format.
|
||||
* It is invoked using the editXBlock method and uses xblock_info as a model,
|
||||
* and upon save parent invokes refresh function that fetches updated model and
|
||||
* re-renders edited course outline.
|
||||
*/
|
||||
define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
|
||||
'js/views/modals/base_modal', 'date', 'js/views/utils/xblock_utils',
|
||||
'js/utils/date_utils'
|
||||
], function(
|
||||
$, Backbone, _, gettext, BaseView, BaseModal, date, XBlockViewUtils, DateUtils
|
||||
) {
|
||||
'use strict';
|
||||
var CourseOutlineXBlockModal, SettingsXBlockModal, PublishXBlockModal, AbstractEditor, BaseDateEditor,
|
||||
ReleaseDateEditor, DueDateEditor, GradingEditor, PublishEditor, StaffLockEditor;
|
||||
|
||||
CourseOutlineXBlockModal = BaseModal.extend({
|
||||
events : {
|
||||
'click .action-save': 'save'
|
||||
},
|
||||
|
||||
options: $.extend({}, BaseModal.prototype.options, {
|
||||
modalName: 'course-outline',
|
||||
modalType: 'edit-settings',
|
||||
addSaveButton: true,
|
||||
modalSize: 'med',
|
||||
viewSpecificClasses: 'confirm',
|
||||
editors: []
|
||||
}),
|
||||
|
||||
initialize: function() {
|
||||
BaseModal.prototype.initialize.call(this);
|
||||
this.events = $.extend({}, BaseModal.prototype.events, this.events);
|
||||
this.template = this.loadTemplate('course-outline-modal');
|
||||
this.options.title = this.getTitle();
|
||||
},
|
||||
|
||||
afterRender: function () {
|
||||
BaseModal.prototype.afterRender.call(this);
|
||||
this.initializeEditors();
|
||||
},
|
||||
|
||||
initializeEditors: function () {
|
||||
this.options.editors = _.map(this.options.editors, function (Editor) {
|
||||
return new Editor({
|
||||
parentElement: this.$('.modal-section'),
|
||||
model: this.model,
|
||||
xblockType: this.options.xblockType
|
||||
});
|
||||
}, this);
|
||||
},
|
||||
|
||||
getTitle: function () {
|
||||
return '';
|
||||
},
|
||||
|
||||
getIntroductionMessage: function () {
|
||||
return '';
|
||||
},
|
||||
|
||||
getContentHtml: function() {
|
||||
return this.template(this.getContext());
|
||||
},
|
||||
|
||||
save: function(event) {
|
||||
event.preventDefault();
|
||||
var requestData = this.getRequestData();
|
||||
if (!_.isEqual(requestData, { metadata: {} })) {
|
||||
XBlockViewUtils.updateXBlockFields(this.model, requestData, {
|
||||
success: this.options.onSave
|
||||
});
|
||||
}
|
||||
this.hide();
|
||||
},
|
||||
|
||||
/**
|
||||
* Return context for the modal.
|
||||
* @return {Object}
|
||||
*/
|
||||
getContext: function () {
|
||||
return $.extend({
|
||||
xblockInfo: this.model,
|
||||
introductionMessage: this.getIntroductionMessage()
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Return request data.
|
||||
* @return {Object}
|
||||
*/
|
||||
getRequestData: function () {
|
||||
var requestData = _.map(this.options.editors, function (editor) {
|
||||
return editor.getRequestData();
|
||||
});
|
||||
|
||||
return $.extend.apply(this, [true, {}].concat(requestData));
|
||||
}
|
||||
});
|
||||
|
||||
SettingsXBlockModal = CourseOutlineXBlockModal.extend({
|
||||
getTitle: function () {
|
||||
return interpolate(
|
||||
gettext('%(display_name)s Settings'),
|
||||
{ display_name: this.model.get('display_name') }, true
|
||||
);
|
||||
},
|
||||
|
||||
getIntroductionMessage: function () {
|
||||
return interpolate(
|
||||
gettext('Change the settings for %(display_name)s'),
|
||||
{ display_name: this.model.get('display_name') }, true
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
PublishXBlockModal = CourseOutlineXBlockModal.extend({
|
||||
events : {
|
||||
'click .action-publish': 'save'
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
CourseOutlineXBlockModal.prototype.initialize.call(this);
|
||||
if (this.options.xblockType) {
|
||||
this.options.modalName = 'bulkpublish-' + this.options.xblockType;
|
||||
}
|
||||
},
|
||||
|
||||
getTitle: function () {
|
||||
return interpolate(
|
||||
gettext('Publish %(display_name)s'),
|
||||
{ display_name: this.model.get('display_name') }, true
|
||||
);
|
||||
},
|
||||
|
||||
getIntroductionMessage: function () {
|
||||
return interpolate(
|
||||
gettext('Publish all unpublished changes for this %(item)s?'),
|
||||
{ item: this.options.xblockType }, true
|
||||
);
|
||||
},
|
||||
|
||||
addActionButtons: function() {
|
||||
this.addActionButton('publish', gettext('Publish'), true);
|
||||
this.addActionButton('cancel', gettext('Cancel'));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
AbstractEditor = BaseView.extend({
|
||||
tagName: 'section',
|
||||
templateName: null,
|
||||
initialize: function() {
|
||||
this.template = this.loadTemplate(this.templateName);
|
||||
this.parentElement = this.options.parentElement;
|
||||
this.render();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var html = this.template($.extend({}, {
|
||||
xblockInfo: this.model,
|
||||
xblockType: this.options.xblockType
|
||||
}, this.getContext()));
|
||||
|
||||
this.$el.html(html);
|
||||
this.parentElement.append(this.$el);
|
||||
},
|
||||
|
||||
getContext: function () {
|
||||
return {};
|
||||
},
|
||||
|
||||
getRequestData: function () {
|
||||
return {};
|
||||
}
|
||||
});
|
||||
|
||||
BaseDateEditor = AbstractEditor.extend({
|
||||
// Attribute name in the model, should be defined in children classes.
|
||||
fieldName: null,
|
||||
|
||||
events : {
|
||||
'click .clear-date': 'clearValue'
|
||||
},
|
||||
|
||||
afterRender: function () {
|
||||
AbstractEditor.prototype.afterRender.call(this);
|
||||
this.$('input.date').datepicker({'dateFormat': 'm/d/yy'});
|
||||
this.$('input.time').timepicker({
|
||||
'timeFormat' : 'H:i',
|
||||
'forceRoundTime': true
|
||||
});
|
||||
if (this.model.get(this.fieldName)) {
|
||||
DateUtils.setDate(
|
||||
this.$('input.date'), this.$('input.time'),
|
||||
this.model.get(this.fieldName)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
DueDateEditor = BaseDateEditor.extend({
|
||||
fieldName: 'due',
|
||||
templateName: 'due-date-editor',
|
||||
className: 'modal-section-content has-actions due-date-input grading-due-date',
|
||||
|
||||
getValue: function () {
|
||||
return DateUtils.getDate(this.$('#due_date'), this.$('#due_time'));
|
||||
},
|
||||
|
||||
clearValue: function (event) {
|
||||
event.preventDefault();
|
||||
this.$('#due_time, #due_date').val('');
|
||||
},
|
||||
|
||||
getRequestData: function () {
|
||||
return {
|
||||
metadata: {
|
||||
'due': this.getValue()
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
ReleaseDateEditor = BaseDateEditor.extend({
|
||||
fieldName: 'start',
|
||||
templateName: 'release-date-editor',
|
||||
className: 'edit-settings-release scheduled-date-input',
|
||||
startingReleaseDate: null,
|
||||
|
||||
afterRender: function () {
|
||||
BaseDateEditor.prototype.afterRender.call(this);
|
||||
// Store the starting date and time so that we can determine if the user
|
||||
// actually changed it when "Save" is pressed.
|
||||
this.startingReleaseDate = this.getValue();
|
||||
},
|
||||
|
||||
getValue: function () {
|
||||
return DateUtils.getDate(this.$('#start_date'), this.$('#start_time'));
|
||||
},
|
||||
|
||||
clearValue: function (event) {
|
||||
event.preventDefault();
|
||||
this.$('#start_time, #start_date').val('');
|
||||
},
|
||||
|
||||
getRequestData: function () {
|
||||
var newReleaseDate = this.getValue();
|
||||
if (JSON.stringify(newReleaseDate) === JSON.stringify(this.startingReleaseDate)) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
metadata: {
|
||||
'start': newReleaseDate
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
GradingEditor = AbstractEditor.extend({
|
||||
templateName: 'grading-editor',
|
||||
className: 'edit-settings-grading',
|
||||
|
||||
afterRender: function () {
|
||||
AbstractEditor.prototype.afterRender.call(this);
|
||||
this.setValue(this.model.get('format'));
|
||||
},
|
||||
|
||||
setValue: function (value) {
|
||||
this.$('#grading_type').val(value);
|
||||
},
|
||||
|
||||
getValue: function () {
|
||||
return this.$('#grading_type').val();
|
||||
},
|
||||
|
||||
getRequestData: function () {
|
||||
return {
|
||||
'graderType': this.getValue()
|
||||
};
|
||||
},
|
||||
|
||||
getContext: function () {
|
||||
return {
|
||||
graderTypes: JSON.parse(this.model.get('course_graders'))
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
PublishEditor = AbstractEditor.extend({
|
||||
templateName: 'publish-editor',
|
||||
className: 'edit-settings-publish',
|
||||
getRequestData: function () {
|
||||
return {
|
||||
publish: 'make_public'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
StaffLockEditor = AbstractEditor.extend({
|
||||
templateName: 'staff-lock-editor',
|
||||
className: 'edit-staff-lock',
|
||||
isModelLocked: function() {
|
||||
return this.model.get('has_explicit_staff_lock');
|
||||
},
|
||||
|
||||
isAncestorLocked: function() {
|
||||
return this.model.get('ancestor_has_staff_lock');
|
||||
},
|
||||
|
||||
afterRender: function () {
|
||||
AbstractEditor.prototype.afterRender.call(this);
|
||||
this.setLock(this.isModelLocked());
|
||||
},
|
||||
|
||||
setLock: function(value) {
|
||||
this.$('#staff_lock').prop('checked', value);
|
||||
},
|
||||
|
||||
isLocked: function() {
|
||||
return this.$('#staff_lock').is(':checked');
|
||||
},
|
||||
|
||||
hasChanges: function() {
|
||||
return this.isModelLocked() != this.isLocked();
|
||||
},
|
||||
|
||||
getRequestData: function() {
|
||||
return this.hasChanges() ? {
|
||||
publish: 'republish',
|
||||
metadata: {
|
||||
visible_to_staff_only: this.isLocked() ? true : null
|
||||
}
|
||||
} : {};
|
||||
},
|
||||
|
||||
getContext: function () {
|
||||
return {
|
||||
hasExplicitStaffLock: this.isModelLocked(),
|
||||
ancestorLocked: this.isAncestorLocked()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
getModal: function (type, xblockInfo, options) {
|
||||
if (type === 'edit') {
|
||||
return this.getEditModal(xblockInfo, options);
|
||||
} else if (type === 'publish') {
|
||||
return this.getPublishModal(xblockInfo, options);
|
||||
}
|
||||
},
|
||||
|
||||
getEditModal: function (xblockInfo, options) {
|
||||
var editors = [];
|
||||
|
||||
if (xblockInfo.isChapter()) {
|
||||
editors = [ReleaseDateEditor, StaffLockEditor];
|
||||
} else if (xblockInfo.isSequential()) {
|
||||
editors = [ReleaseDateEditor, GradingEditor, DueDateEditor, StaffLockEditor];
|
||||
} else if (xblockInfo.isVertical()) {
|
||||
editors = [StaffLockEditor];
|
||||
}
|
||||
|
||||
return new SettingsXBlockModal($.extend({
|
||||
editors: editors,
|
||||
model: xblockInfo
|
||||
}, options));
|
||||
},
|
||||
|
||||
getPublishModal: function (xblockInfo, options) {
|
||||
return new PublishXBlockModal($.extend({
|
||||
editors: [PublishEditor],
|
||||
model: xblockInfo
|
||||
}, options));
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -1,244 +0,0 @@
|
||||
/**
|
||||
* The EditSectionXBlockModal is a Backbone view that shows an editor in a modal window.
|
||||
* It has nested views: for release date, due date and grading format.
|
||||
* It is invoked using the editXBlock method and uses xblock_info as a model,
|
||||
* and upon save parent invokes refresh function that fetches updated model and
|
||||
* re-renders edited course outline.
|
||||
*/
|
||||
define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/modals/base_modal',
|
||||
'date', 'js/views/utils/xblock_utils', 'js/utils/date_utils'
|
||||
],
|
||||
function(
|
||||
$, Backbone, _, gettext, BaseModal, date, XBlockViewUtils, DateUtils
|
||||
) {
|
||||
'use strict';
|
||||
var EditSectionXBlockModal, BaseDateView, ReleaseDateView, DueDateView,
|
||||
GradingView;
|
||||
|
||||
EditSectionXBlockModal = BaseModal.extend({
|
||||
events : {
|
||||
'click .action-save': 'save',
|
||||
'click .action-modes a': 'changeMode'
|
||||
},
|
||||
|
||||
options: $.extend({}, BaseModal.prototype.options, {
|
||||
modalName: 'edit-outline-item',
|
||||
modalType: 'edit-settings',
|
||||
addSaveButton: true,
|
||||
modalSize: 'med',
|
||||
viewSpecificClasses: 'confirm'
|
||||
}),
|
||||
|
||||
initialize: function() {
|
||||
BaseModal.prototype.initialize.call(this);
|
||||
this.events = _.extend({}, BaseModal.prototype.events, this.events);
|
||||
this.template = this.loadTemplate('edit-outline-item-modal');
|
||||
this.options.title = this.getTitle();
|
||||
this.initializeComponents();
|
||||
},
|
||||
|
||||
getTitle: function () {
|
||||
if (this.model.isChapter() || this.model.isSequential()) {
|
||||
return _.template(
|
||||
gettext('<%= sectionName %> Settings'),
|
||||
{sectionName: this.model.get('display_name')});
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
|
||||
getContentHtml: function() {
|
||||
return this.template(this.getContext());
|
||||
},
|
||||
|
||||
afterRender: function() {
|
||||
BaseModal.prototype.render.apply(this, arguments);
|
||||
this.invokeComponentMethod('afterRender');
|
||||
},
|
||||
|
||||
save: function(event) {
|
||||
event.preventDefault();
|
||||
var requestData = _.extend({}, this.getRequestData(), {
|
||||
metadata: this.getMetadata()
|
||||
});
|
||||
XBlockViewUtils.updateXBlockFields(this.model, requestData, {
|
||||
success: this.options.onSave
|
||||
});
|
||||
this.hide();
|
||||
},
|
||||
|
||||
/**
|
||||
* Call the method on each value in the list. If the element of the
|
||||
* list doesn't have such a method it will be skipped.
|
||||
* @param {String} methodName The method name needs to be called.
|
||||
* @return {Object}
|
||||
*/
|
||||
invokeComponentMethod: function (methodName) {
|
||||
var values = _.map(this.components, function (component) {
|
||||
if (_.isFunction(component[methodName])) {
|
||||
return component[methodName].call(component);
|
||||
}
|
||||
});
|
||||
|
||||
return _.extend.apply(this, [{}].concat(values));
|
||||
},
|
||||
|
||||
/**
|
||||
* Return context for the modal.
|
||||
* @return {Object}
|
||||
*/
|
||||
getContext: function () {
|
||||
return _.extend({
|
||||
xblockInfo: this.model
|
||||
}, this.invokeComponentMethod('getContext'));
|
||||
},
|
||||
|
||||
/**
|
||||
* Return request data.
|
||||
* @return {Object}
|
||||
*/
|
||||
getRequestData: function () {
|
||||
return this.invokeComponentMethod('getRequestData');
|
||||
},
|
||||
|
||||
/**
|
||||
* Return metadata for the XBlock.
|
||||
* @return {Object}
|
||||
*/
|
||||
getMetadata: function () {
|
||||
return this.invokeComponentMethod('getMetadata');
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize internal components.
|
||||
*/
|
||||
initializeComponents: function () {
|
||||
this.components = [];
|
||||
this.components.push(
|
||||
new ReleaseDateView({
|
||||
selector: '.scheduled-date-input',
|
||||
parentView: this,
|
||||
model: this.model
|
||||
})
|
||||
);
|
||||
|
||||
if (this.model.isSequential()) {
|
||||
this.components.push(
|
||||
new DueDateView({
|
||||
selector: '.due-date-input',
|
||||
parentView: this,
|
||||
model: this.model
|
||||
}),
|
||||
new GradingView({
|
||||
selector: '.edit-settings-grading',
|
||||
parentView: this,
|
||||
model: this.model
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
BaseDateView = Backbone.View.extend({
|
||||
// Attribute name in the model, should be defined in children classes.
|
||||
fieldName: null,
|
||||
|
||||
events : {
|
||||
'click .clear-date': 'clearValue'
|
||||
},
|
||||
|
||||
afterRender: function () {
|
||||
this.setElement(this.options.parentView.$(this.options.selector).get(0));
|
||||
this.$('input.date').datepicker({'dateFormat': 'm/d/yy'});
|
||||
this.$('input.time').timepicker({
|
||||
'timeFormat' : 'H:i',
|
||||
'forceRoundTime': true
|
||||
});
|
||||
if (this.model.get(this.fieldName)) {
|
||||
DateUtils.setDate(
|
||||
this.$('input.date'), this.$('input.time'),
|
||||
this.model.get(this.fieldName)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
DueDateView = BaseDateView.extend({
|
||||
fieldName: 'due',
|
||||
|
||||
getValue: function () {
|
||||
return DateUtils.getDate(this.$('#due_date'), this.$('#due_time'));
|
||||
},
|
||||
|
||||
clearValue: function (event) {
|
||||
event.preventDefault();
|
||||
this.$('#due_time, #due_date').val('');
|
||||
},
|
||||
|
||||
getMetadata: function () {
|
||||
return {
|
||||
'due': this.getValue()
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
ReleaseDateView = BaseDateView.extend({
|
||||
fieldName: 'start',
|
||||
startingReleaseDate: null,
|
||||
|
||||
afterRender: function () {
|
||||
BaseDateView.prototype.afterRender.call(this);
|
||||
// Store the starting date and time so that we can determine if the user
|
||||
// actually changed it when "Save" is pressed.
|
||||
this.startingReleaseDate = this.getValue();
|
||||
},
|
||||
|
||||
getValue: function () {
|
||||
return DateUtils.getDate(this.$('#start_date'), this.$('#start_time'));
|
||||
},
|
||||
|
||||
clearValue: function (event) {
|
||||
event.preventDefault();
|
||||
this.$('#start_time, #start_date').val('');
|
||||
},
|
||||
|
||||
getMetadata: function () {
|
||||
var newReleaseDate = this.getValue();
|
||||
if (JSON.stringify(newReleaseDate) === JSON.stringify(this.startingReleaseDate)) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
'start': newReleaseDate
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
GradingView = Backbone.View.extend({
|
||||
afterRender: function () {
|
||||
this.setElement(this.options.parentView.$(this.options.selector).get(0));
|
||||
this.setValue(this.model.get('format'));
|
||||
},
|
||||
|
||||
setValue: function (value) {
|
||||
this.$('#grading_type').val(value);
|
||||
},
|
||||
|
||||
getValue: function () {
|
||||
return this.$('#grading_type').val();
|
||||
},
|
||||
|
||||
getRequestData: function () {
|
||||
return {
|
||||
'graderType': this.getValue()
|
||||
};
|
||||
},
|
||||
|
||||
getContext: function () {
|
||||
return {
|
||||
graderTypes: JSON.parse(this.model.get('course_graders'))
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return EditSectionXBlockModal;
|
||||
});
|
||||
@@ -65,15 +65,10 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal", "js/vie
|
||||
|
||||
onDisplayXBlock: function() {
|
||||
var editorView = this.editorView,
|
||||
title = this.getTitle(),
|
||||
xblock = editorView.xblock,
|
||||
runtime = xblock.runtime;
|
||||
title = this.getTitle();
|
||||
|
||||
// Notify the runtime that the modal has been shown
|
||||
if (runtime) {
|
||||
this.runtime = runtime;
|
||||
runtime.notify('modal-shown', this);
|
||||
}
|
||||
editorView.notifyRuntime('modal-shown', this);
|
||||
|
||||
// Update the modal's header
|
||||
if (editorView.hasCustomTabs()) {
|
||||
@@ -93,7 +88,7 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal", "js/vie
|
||||
// If the xblock is not using custom buttons then choose which buttons to show
|
||||
if (!editorView.hasCustomButtons()) {
|
||||
// If the xblock does not support save then disable the save button
|
||||
if (!xblock.save) {
|
||||
if (!editorView.xblock.save) {
|
||||
this.disableSave();
|
||||
}
|
||||
this.getActionBar().show();
|
||||
@@ -175,9 +170,7 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal", "js/vie
|
||||
BaseModal.prototype.hide.call(this);
|
||||
|
||||
// Notify the runtime that the modal has been hidden
|
||||
if (this.runtime) {
|
||||
this.runtime.notify('modal-hidden');
|
||||
}
|
||||
this.editorView.notifyRuntime('modal-hidden');
|
||||
},
|
||||
|
||||
findXBlockInfo: function(xblockWrapperElement, defaultXBlockInfo) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/feedback_notification",
|
||||
"js/utils/cancel_on_escape", "js/utils/date_utils", "js/utils/module"],
|
||||
"js/utils/cancel_on_escape", "js/utils/date_utils", "js/utils/module", "js/views/utils/view_utils"],
|
||||
function (domReady, $, ui, _, gettext, NotificationView, CancelOnEscape,
|
||||
DateUtils, ModuleUtils) {
|
||||
DateUtils, ModuleUtils, ViewUtils) {
|
||||
|
||||
var modalSelector = '.edit-section-publish-settings';
|
||||
|
||||
@@ -222,6 +222,10 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
|
||||
$('.toggle-button-sections').bind('click', toggleSections);
|
||||
$('.expand-collapse').bind('click', toggleSubmodules);
|
||||
|
||||
$('.dismiss-button').bind('click', ViewUtils.deleteNotificationHandler(function () {
|
||||
$('.wrapper-alert-announcement').remove();
|
||||
}));
|
||||
|
||||
var $body = $('body');
|
||||
$body.on('click', '.section-published-date .edit-release-date', editSectionPublishDate);
|
||||
$body.on('click', '.edit-section-publish-settings .action-save', saveSetSectionScheduleDate);
|
||||
|
||||
@@ -9,6 +9,7 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
|
||||
function ($, _, gettext, BasePage, ViewUtils, ContainerView, XBlockView, AddXBlockComponent,
|
||||
EditXBlockModal, XBlockInfo, XBlockStringFieldEditor, ContainerSubviews, UnitOutlineView,
|
||||
XBlockUtils) {
|
||||
'use strict';
|
||||
var XBlockContainerPage = BasePage.extend({
|
||||
// takes XBlockInfo as a model
|
||||
|
||||
@@ -88,14 +89,22 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
|
||||
|
||||
// Render the xblock
|
||||
xblockView.render({
|
||||
success: function() {
|
||||
xblockView.xblock.runtime.notify("page-shown", self);
|
||||
done: function() {
|
||||
// Show the xblock and hide the loading indicator
|
||||
xblockView.$el.removeClass(hiddenCss);
|
||||
self.renderAddXBlockComponents();
|
||||
self.onXBlockRefresh(xblockView);
|
||||
self.refreshDisplayName();
|
||||
loadingElement.addClass(hiddenCss);
|
||||
|
||||
// Notify the runtime that the page has been successfully shown
|
||||
xblockView.notifyRuntime('page-shown', self);
|
||||
|
||||
// Render the add buttons
|
||||
self.renderAddXBlockComponents();
|
||||
|
||||
// Refresh the views now that the xblock is visible
|
||||
self.onXBlockRefresh(xblockView);
|
||||
unitLocationTree.removeClass(hiddenCss);
|
||||
|
||||
// Re-enable Backbone events for any updated DOM elements
|
||||
self.delegateEvents();
|
||||
}
|
||||
});
|
||||
@@ -109,11 +118,6 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
|
||||
return this.xblockView.model.urlRoot;
|
||||
},
|
||||
|
||||
refreshDisplayName: function() {
|
||||
var displayName = this.$('.xblock-header .header-details .xblock-display-name').first().text().trim();
|
||||
this.model.set('display_name', displayName);
|
||||
},
|
||||
|
||||
onXBlockRefresh: function(xblockView) {
|
||||
this.addButtonActions(xblockView.$el);
|
||||
this.xblockView.refresh();
|
||||
@@ -159,6 +163,10 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
|
||||
});
|
||||
},
|
||||
|
||||
createPlaceholderElement: function() {
|
||||
return $("<div/>", { class: "studio-xblock-wrapper" });
|
||||
},
|
||||
|
||||
createComponent: function(template, target) {
|
||||
// A placeholder element is created in the correct location for the new xblock
|
||||
// and then onNewXBlock will replace it with a rendering of the xblock. Note that
|
||||
@@ -168,7 +176,7 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
|
||||
buttonPanel = target.closest('.add-xblock-component'),
|
||||
listPanel = buttonPanel.prev(),
|
||||
scrollOffset = ViewUtils.getScrollOffset(buttonPanel),
|
||||
placeholderElement = $('<div class="studio-xblock-wrapper"></div>').appendTo(listPanel),
|
||||
placeholderElement = this.createPlaceholderElement().appendTo(listPanel),
|
||||
requestData = _.extend(template, {
|
||||
parent_locator: parentLocator
|
||||
});
|
||||
@@ -189,7 +197,7 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
|
||||
ViewUtils.runOperationShowingMessage(gettext('Duplicating…'),
|
||||
function() {
|
||||
var scrollOffset = ViewUtils.getScrollOffset(xblockElement),
|
||||
placeholderElement = $('<div class="studio-xblock-wrapper"></div>').insertAfter(xblockElement),
|
||||
placeholderElement = self.createPlaceholderElement().insertAfter(xblockElement),
|
||||
parentElement = self.findXBlockElement(parent),
|
||||
requestData = {
|
||||
duplicate_source_locator: xblockElement.data('locator'),
|
||||
@@ -217,10 +225,13 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
|
||||
onDelete: function(xblockElement) {
|
||||
// get the parent so we can remove this component from its parent.
|
||||
var xblockView = this.xblockView,
|
||||
xblock = xblockView.xblock,
|
||||
parent = this.findXBlockElement(xblockElement.parent());
|
||||
xblockElement.remove();
|
||||
xblock.runtime.notify('deleted-child', parent.data('locator'));
|
||||
|
||||
// Inform the runtime that the child has been deleted in case
|
||||
// other views are listening to deletion events.
|
||||
xblockView.notifyRuntime('deleted-child', parent.data('locator'));
|
||||
|
||||
// Update publish and last modified information from the server.
|
||||
this.model.fetch();
|
||||
},
|
||||
|
||||
@@ -100,7 +100,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
|
||||
|
||||
onSync: function(model) {
|
||||
if (ViewUtils.hasChangedAttributes(model, [
|
||||
'has_changes', 'published', 'edited_on', 'edited_by', 'visibility_state'
|
||||
'has_changes', 'published', 'edited_on', 'edited_by', 'visibility_state', 'has_explicit_staff_lock'
|
||||
])) {
|
||||
this.render();
|
||||
}
|
||||
@@ -118,7 +118,9 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
|
||||
publishedBy: this.model.get('published_by'),
|
||||
released: this.model.get('released_to_students'),
|
||||
releaseDate: this.model.get('release_date'),
|
||||
releaseDateFrom: this.model.get('release_date_from')
|
||||
releaseDateFrom: this.model.get('release_date_from'),
|
||||
hasExplicitStaffLock: this.model.get('has_explicit_staff_lock'),
|
||||
staffLockFrom: this.model.get('staff_lock_from')
|
||||
}));
|
||||
|
||||
return this;
|
||||
@@ -161,12 +163,13 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
|
||||
},
|
||||
|
||||
toggleStaffLock: function (e) {
|
||||
var xblockInfo = this.model, self=this, enableStaffLock,
|
||||
var xblockInfo = this.model, self=this, enableStaffLock, hasInheritedStaffLock,
|
||||
saveAndPublishStaffLock, revertCheckBox;
|
||||
if (e && e.preventDefault) {
|
||||
e.preventDefault();
|
||||
}
|
||||
enableStaffLock = xblockInfo.get('visibility_state') !== VisibilityState.staffOnly;
|
||||
enableStaffLock = !xblockInfo.get('has_explicit_staff_lock');
|
||||
hasInheritedStaffLock = xblockInfo.get('ancestor_has_staff_lock');
|
||||
|
||||
revertCheckBox = function() {
|
||||
self.checkStaffLock(!enableStaffLock);
|
||||
@@ -189,8 +192,14 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
|
||||
};
|
||||
|
||||
this.checkStaffLock(enableStaffLock);
|
||||
if (enableStaffLock) {
|
||||
ViewUtils.runOperationShowingMessage(gettext('Hiding Unit from Students…'),
|
||||
if (enableStaffLock && !hasInheritedStaffLock) {
|
||||
ViewUtils.runOperationShowingMessage(gettext('Hiding from Students…'),
|
||||
_.bind(saveAndPublishStaffLock, self));
|
||||
} else if (enableStaffLock && hasInheritedStaffLock) {
|
||||
ViewUtils.runOperationShowingMessage(gettext('Explicitly Hiding from Students…'),
|
||||
_.bind(saveAndPublishStaffLock, self));
|
||||
} else if (!enableStaffLock && hasInheritedStaffLock) {
|
||||
ViewUtils.runOperationShowingMessage(gettext('Inheriting Student Visibility…'),
|
||||
_.bind(saveAndPublishStaffLock, self));
|
||||
} else {
|
||||
ViewUtils.confirmThenRunOperation(gettext("Make Visible to Students"),
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* This page is used to show the user an outline of the course.
|
||||
*/
|
||||
define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views/utils/xblock_utils",
|
||||
"js/views/course_outline"],
|
||||
function ($, _, gettext, BasePage, XBlockViewUtils, CourseOutlineView) {
|
||||
"js/views/course_outline", "js/views/utils/view_utils"],
|
||||
function ($, _, gettext, BasePage, XBlockViewUtils, CourseOutlineView, ViewUtils) {
|
||||
var expandedLocators, CourseOutlinePage;
|
||||
|
||||
CourseOutlinePage = BasePage.extend({
|
||||
@@ -25,6 +25,9 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
|
||||
self.outlineView.handleAddEvent(event);
|
||||
});
|
||||
this.model.on('change', this.setCollapseExpandVisibility, this);
|
||||
$('.dismiss-button').bind('click', ViewUtils.deleteNotificationHandler(function () {
|
||||
$('.wrapper-alert-announcement').removeClass('is-shown').addClass('is-hidden')
|
||||
}));
|
||||
},
|
||||
|
||||
setCollapseExpandVisibility: function() {
|
||||
|
||||
151
cms/static/js/views/utils/create_course_utils.js
Normal file
151
cms/static/js/views/utils/create_course_utils.js
Normal file
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* Provides utilities for validating courses during creation, for both new courses and reruns.
|
||||
*/
|
||||
define(["jquery", "underscore", "gettext", "js/views/utils/view_utils"],
|
||||
function ($, _, gettext, ViewUtils) {
|
||||
return function (selectors, classes) {
|
||||
var validateRequiredField, validateCourseItemEncoding, validateTotalCourseItemsLength, setNewCourseFieldInErr,
|
||||
hasInvalidRequiredFields, createCourse, validateFilledFields, configureHandlers;
|
||||
|
||||
validateRequiredField = function (msg) {
|
||||
return msg.length === 0 ? gettext('Required field.') : '';
|
||||
};
|
||||
|
||||
// Check that a course (org, number, run) doesn't use any special characters
|
||||
validateCourseItemEncoding = function (item) {
|
||||
var required = validateRequiredField(item);
|
||||
if (required) {
|
||||
return required;
|
||||
}
|
||||
if ($(selectors.allowUnicode).val() === 'True') {
|
||||
if (/\s/g.test(item)) {
|
||||
return gettext('Please do not use any spaces in this field.');
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (item !== encodeURIComponent(item)) {
|
||||
return gettext('Please do not use any spaces or special characters in this field.');
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
// Ensure that org/course_num/run < 65 chars.
|
||||
validateTotalCourseItemsLength = function () {
|
||||
var totalLength = _.reduce(
|
||||
[selectors.org, selectors.number, selectors.run],
|
||||
function (sum, ele) {
|
||||
return sum + $(ele).val().length;
|
||||
}, 0
|
||||
);
|
||||
if (totalLength > 65) {
|
||||
$(selectors.errorWrapper).addClass(classes.shown).removeClass(classes.hiding);
|
||||
$(selectors.errorMessage).html('<p>' + gettext('The combined length of the organization, course number, and course run fields cannot be more than 65 characters.') + '</p>');
|
||||
$(selectors.save).addClass(classes.disabled);
|
||||
}
|
||||
else {
|
||||
$(selectors.errorWrapper).removeClass(classes.shown).addClass(classes.hiding);
|
||||
}
|
||||
};
|
||||
|
||||
setNewCourseFieldInErr = function (el, msg) {
|
||||
if (msg) {
|
||||
el.addClass(classes.error);
|
||||
el.children(selectors.tipError).addClass(classes.showing).removeClass(classes.hiding).text(msg);
|
||||
$(selectors.save).addClass(classes.disabled);
|
||||
}
|
||||
else {
|
||||
el.removeClass(classes.error);
|
||||
el.children(selectors.tipError).addClass(classes.hiding).removeClass(classes.showing);
|
||||
// One "error" div is always present, but hidden or shown
|
||||
if ($(selectors.error).length === 1) {
|
||||
$(selectors.save).removeClass(classes.disabled);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// One final check for empty values
|
||||
hasInvalidRequiredFields = function () {
|
||||
return _.reduce(
|
||||
[selectors.name, selectors.org, selectors.number, selectors.run],
|
||||
function (acc, ele) {
|
||||
var $ele = $(ele);
|
||||
var error = validateRequiredField($ele.val());
|
||||
setNewCourseFieldInErr($ele.parent(), error);
|
||||
return error ? true : acc;
|
||||
},
|
||||
false
|
||||
);
|
||||
};
|
||||
|
||||
createCourse = function (courseInfo, errorHandler) {
|
||||
$.postJSON(
|
||||
'/course/',
|
||||
courseInfo,
|
||||
function (data) {
|
||||
if (data.url !== undefined) {
|
||||
ViewUtils.redirect(data.url);
|
||||
} else if (data.ErrMsg !== undefined) {
|
||||
errorHandler(data.ErrMsg);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Ensure that all fields are not empty
|
||||
validateFilledFields = function () {
|
||||
return _.reduce(
|
||||
[selectors.org, selectors.number, selectors.run, selectors.name],
|
||||
function (acc, ele) {
|
||||
var $ele = $(ele);
|
||||
return $ele.val().length !== 0 ? acc : false;
|
||||
},
|
||||
true
|
||||
);
|
||||
};
|
||||
|
||||
// Handle validation asynchronously
|
||||
configureHandlers = function () {
|
||||
_.each(
|
||||
[selectors.org, selectors.number, selectors.run],
|
||||
function (ele) {
|
||||
var $ele = $(ele);
|
||||
$ele.on('keyup', function (event) {
|
||||
// Don't bother showing "required field" error when
|
||||
// the user tabs into a new field; this is distracting
|
||||
// and unnecessary
|
||||
if (event.keyCode === 9) {
|
||||
return;
|
||||
}
|
||||
var error = validateCourseItemEncoding($ele.val());
|
||||
setNewCourseFieldInErr($ele.parent(), error);
|
||||
validateTotalCourseItemsLength();
|
||||
if (!validateFilledFields()) {
|
||||
$(selectors.save).addClass(classes.disabled);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
var $name = $(selectors.name);
|
||||
$name.on('keyup', function () {
|
||||
var error = validateRequiredField($name.val());
|
||||
setNewCourseFieldInErr($name.parent(), error);
|
||||
validateTotalCourseItemsLength();
|
||||
if (!validateFilledFields()) {
|
||||
$(selectors.save).addClass(classes.disabled);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
validateRequiredField: validateRequiredField,
|
||||
validateCourseItemEncoding: validateCourseItemEncoding,
|
||||
validateTotalCourseItemsLength: validateTotalCourseItemsLength,
|
||||
setNewCourseFieldInErr: setNewCourseFieldInErr,
|
||||
hasInvalidRequiredFields: hasInvalidRequiredFields,
|
||||
createCourse: createCourse,
|
||||
validateFilledFields: validateFilledFields,
|
||||
configureHandlers: configureHandlers
|
||||
};
|
||||
};
|
||||
});
|
||||
@@ -5,7 +5,7 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js
|
||||
function ($, _, gettext, NotificationView, PromptView) {
|
||||
var toggleExpandCollapse, showLoadingIndicator, hideLoadingIndicator, confirmThenRunOperation,
|
||||
runOperationShowingMessage, disableElementWhileRunning, getScrollOffset, setScrollOffset,
|
||||
setScrollTop, redirect, hasChangedAttributes;
|
||||
setScrollTop, redirect, reload, hasChangedAttributes, deleteNotificationHandler;
|
||||
|
||||
/**
|
||||
* Toggles the expanded state of the current element.
|
||||
@@ -94,6 +94,21 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a handler that removes a notification, both dismissing it and deleting it from the database.
|
||||
* @param callback function to call when deletion succeeds
|
||||
*/
|
||||
deleteNotificationHandler = function(callback) {
|
||||
return function (event) {
|
||||
event.preventDefault();
|
||||
$.ajax({
|
||||
url: $(this).data('dismiss-link'),
|
||||
type: 'DELETE',
|
||||
success: callback
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Performs an animated scroll so that the window has the specified scroll top.
|
||||
* @param scrollTop The desired scroll top for the window.
|
||||
@@ -132,6 +147,13 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js
|
||||
window.location = url;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reloads the page. This is broken out as its own function for unit testing.
|
||||
*/
|
||||
reload = function() {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if a model has changes to at least one of the specified attributes.
|
||||
* @param model The model in question.
|
||||
@@ -158,10 +180,12 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js
|
||||
'confirmThenRunOperation': confirmThenRunOperation,
|
||||
'runOperationShowingMessage': runOperationShowingMessage,
|
||||
'disableElementWhileRunning': disableElementWhileRunning,
|
||||
'deleteNotificationHandler': deleteNotificationHandler,
|
||||
'setScrollTop': setScrollTop,
|
||||
'getScrollOffset': getScrollOffset,
|
||||
'setScrollOffset': setScrollOffset,
|
||||
'redirect': redirect,
|
||||
'reload': reload,
|
||||
'hasChangedAttributes': hasChangedAttributes
|
||||
};
|
||||
});
|
||||
|
||||
@@ -165,6 +165,18 @@ define(["jquery", "underscore", "gettext", "js/views/utils/view_utils", "js/util
|
||||
return listType;
|
||||
};
|
||||
|
||||
getXBlockType = function(category, parentInfo, translate) {
|
||||
var xblockType = category;
|
||||
if (category === 'chapter') {
|
||||
xblockType = translate ? gettext('section') : 'section';
|
||||
} else if (category === 'sequential') {
|
||||
xblockType = translate ? gettext('subsection') : 'subsection';
|
||||
} else if (category === 'vertical' && (!parentInfo || parentInfo.get('category') === 'sequential')) {
|
||||
xblockType = translate ? gettext('unit') : 'unit';
|
||||
}
|
||||
return xblockType;
|
||||
};
|
||||
|
||||
return {
|
||||
'VisibilityState': VisibilityState,
|
||||
'addXBlock': addXBlock,
|
||||
@@ -172,6 +184,7 @@ define(["jquery", "underscore", "gettext", "js/views/utils/view_utils", "js/util
|
||||
'updateXBlockField': updateXBlockField,
|
||||
'getXBlockVisibilityClass': getXBlockVisibilityClass,
|
||||
'getXBlockListTypeClass': getXBlockListTypeClass,
|
||||
'updateXBlockFields': updateXBlockFields
|
||||
'updateXBlockFields': updateXBlockFields,
|
||||
'getXBlockType': getXBlockType
|
||||
};
|
||||
});
|
||||
|
||||
@@ -29,34 +29,58 @@ define(["jquery", "underscore", "js/views/baseview", "xblock/runtime.v1"],
|
||||
var self = this,
|
||||
wrapper = this.$el,
|
||||
xblockElement,
|
||||
success = options ? options.success : null,
|
||||
successCallback = options ? options.success || options.done : null,
|
||||
errorCallback = options ? options.error || options.done : null,
|
||||
xblock,
|
||||
fragmentsRendered;
|
||||
|
||||
fragmentsRendered = this.renderXBlockFragment(fragment, wrapper);
|
||||
fragmentsRendered.done(function() {
|
||||
fragmentsRendered.always(function() {
|
||||
xblockElement = self.$('.xblock').first();
|
||||
xblock = XBlock.initializeBlock(xblockElement);
|
||||
self.xblock = xblock;
|
||||
self.xblockReady(xblock);
|
||||
if (success) {
|
||||
success(xblock);
|
||||
try {
|
||||
xblock = XBlock.initializeBlock(xblockElement);
|
||||
self.xblock = xblock;
|
||||
self.xblockReady(xblock);
|
||||
if (successCallback) {
|
||||
successCallback(xblock);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e.stack);
|
||||
// Add 'xblock-initialization-failed' class to every xblock
|
||||
self.$('.xblock').addClass('xblock-initialization-failed');
|
||||
|
||||
// If the xblock was rendered but failed then still call xblockReady to allow
|
||||
// drag-and-drop to be initialized.
|
||||
if (xblockElement) {
|
||||
self.xblockReady(null);
|
||||
}
|
||||
if (errorCallback) {
|
||||
errorCallback();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* This method is called upon successful rendering of an xblock.
|
||||
* Sends a notification event to the runtime, if one is available. Note that the runtime
|
||||
* is only available once the xblock has been rendered and successfully initialized.
|
||||
* @param eventName The name of the event to be fired.
|
||||
* @param data The data to be passed to any listener's of the event.
|
||||
*/
|
||||
xblockReady: function(xblock) {
|
||||
// Do nothing
|
||||
notifyRuntime: function(eventName, data) {
|
||||
var runtime = this.xblock && this.xblock.runtime;
|
||||
if (runtime) {
|
||||
runtime.notify(eventName, data);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if the specified xblock has children.
|
||||
* This method is called upon successful rendering of an xblock. Note that the xblock
|
||||
* may have thrown JavaScript errors after rendering in which case the xblock parameter
|
||||
* will be null.
|
||||
*/
|
||||
hasChildXBlocks: function() {
|
||||
return this.$('.wrapper-xblock').length > 0;
|
||||
xblockReady: function(xblock) {
|
||||
// Do nothing
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -77,9 +101,16 @@ define(["jquery", "underscore", "js/views/baseview", "xblock/runtime.v1"],
|
||||
}
|
||||
|
||||
// Render the HTML first as the scripts might depend upon it, and then
|
||||
// asynchronously add the resources to the page.
|
||||
this.updateHtml(element, html);
|
||||
return this.addXBlockFragmentResources(resources);
|
||||
// asynchronously add the resources to the page. Any errors that are thrown
|
||||
// by included scripts are logged to the console but are then ignored assuming
|
||||
// that at least the rendered HTML will be in place.
|
||||
try {
|
||||
this.updateHtml(element, html);
|
||||
return this.addXBlockFragmentResources(resources);
|
||||
} catch(e) {
|
||||
console.error(e.stack);
|
||||
return $.Deferred().resolve();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -106,7 +137,7 @@ define(["jquery", "underscore", "js/views/baseview", "xblock/runtime.v1"],
|
||||
numResources = resources.length;
|
||||
deferred = $.Deferred();
|
||||
applyResource = function(index) {
|
||||
var hash, resource, head, value, promise;
|
||||
var hash, resource, value, promise;
|
||||
if (index >= numResources) {
|
||||
deferred.resolve();
|
||||
return;
|
||||
|
||||
@@ -57,9 +57,9 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
|
||||
var xblockInfo = this.model,
|
||||
childInfo = xblockInfo.get('child_info'),
|
||||
parentInfo = this.parentInfo,
|
||||
xblockType = this.getXBlockType(this.model.get('category'), this.parentInfo),
|
||||
xblockTypeDisplayName = this.getXBlockType(this.model.get('category'), this.parentInfo, true),
|
||||
parentType = parentInfo ? this.getXBlockType(parentInfo.get('category')) : null,
|
||||
xblockType = XBlockViewUtils.getXBlockType(this.model.get('category'), this.parentInfo),
|
||||
xblockTypeDisplayName = XBlockViewUtils.getXBlockType(this.model.get('category'), this.parentInfo, true),
|
||||
parentType = parentInfo ? XBlockViewUtils.getXBlockType(parentInfo.get('category')) : null,
|
||||
addChildName = null,
|
||||
defaultNewChildName = null,
|
||||
html,
|
||||
@@ -78,12 +78,14 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
|
||||
xblockType: xblockType,
|
||||
xblockTypeDisplayName: xblockTypeDisplayName,
|
||||
parentType: parentType,
|
||||
childType: childInfo ? this.getXBlockType(childInfo.category, xblockInfo) : null,
|
||||
childType: childInfo ? XBlockViewUtils.getXBlockType(childInfo.category, xblockInfo) : null,
|
||||
childCategory: childInfo ? childInfo.category : null,
|
||||
addChildLabel: addChildName,
|
||||
defaultNewChildName: defaultNewChildName,
|
||||
isCollapsed: isCollapsed,
|
||||
includesChildren: this.shouldRenderChildren()
|
||||
includesChildren: this.shouldRenderChildren(),
|
||||
hasExplicitStaffLock: this.model.get('has_explicit_staff_lock'),
|
||||
staffOnlyMessage: this.model.get('staff_only_message')
|
||||
});
|
||||
if (this.parentInfo) {
|
||||
this.setElement($(html));
|
||||
@@ -147,10 +149,17 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
|
||||
}
|
||||
}
|
||||
// Ensure that the children have been rendered before expanding
|
||||
if (this.shouldRenderChildren() && !this.renderedChildren) {
|
||||
this.ensureChildrenRendered();
|
||||
BaseView.prototype.toggleExpandCollapse.call(this, event);
|
||||
},
|
||||
|
||||
/**
|
||||
* Verifies that the children are rendered (if they should be).
|
||||
*/
|
||||
ensureChildrenRendered: function() {
|
||||
if (!this.renderedChildren && this.shouldRenderChildren()) {
|
||||
this.renderChildren();
|
||||
}
|
||||
BaseView.prototype.toggleExpandCollapse.call(this, event);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -184,18 +193,6 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
|
||||
});
|
||||
},
|
||||
|
||||
getXBlockType: function(category, parentInfo, translate) {
|
||||
var xblockType = category;
|
||||
if (category === 'chapter') {
|
||||
xblockType = translate ? gettext('section') : 'section';
|
||||
} else if (category === 'sequential') {
|
||||
xblockType = translate ? gettext('subsection') : 'subsection';
|
||||
} else if (category === 'vertical' && (!parentInfo || parentInfo.get('category') === 'sequential')) {
|
||||
xblockType = translate ? gettext('unit') : 'unit';
|
||||
}
|
||||
return xblockType;
|
||||
},
|
||||
|
||||
onSync: function(event) {
|
||||
if (ViewUtils.hasChangedAttributes(this.model, ['visibility_state', 'child_info', 'display_name'])) {
|
||||
this.onXBlockChange();
|
||||
@@ -266,7 +263,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
|
||||
var self = this,
|
||||
parentView = this.parentView;
|
||||
event.preventDefault();
|
||||
var xblockType = this.getXBlockType(this.model.get('category'), parentView.model, true);
|
||||
var xblockType = XBlockViewUtils.getXBlockType(this.model.get('category'), parentView.model, true);
|
||||
XBlockViewUtils.deleteXBlock(this.model, xblockType).done(function() {
|
||||
if (parentView) {
|
||||
parentView.onChildDeleted(self, event);
|
||||
|
||||
@@ -240,6 +240,282 @@ p, ul, ol, dl {
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// layout - basic
|
||||
.wrapper-view {
|
||||
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// layout - basic page header
|
||||
.wrapper-mast {
|
||||
margin: ($baseline*1.5) 0 0 0;
|
||||
padding: 0 $baseline;
|
||||
position: relative;
|
||||
|
||||
.mast, .metadata {
|
||||
@include clearfix();
|
||||
position: relative;
|
||||
max-width: $fg-max-width;
|
||||
min-width: $fg-min-width;
|
||||
width: flex-grid(12);
|
||||
margin: 0 auto $baseline auto;
|
||||
color: $gray-d2;
|
||||
}
|
||||
|
||||
.mast {
|
||||
border-bottom: 1px solid $gray-l4;
|
||||
padding-bottom: ($baseline/2);
|
||||
|
||||
// layout with actions
|
||||
.page-header {
|
||||
width: flex-grid(12);
|
||||
}
|
||||
|
||||
// layout with actions
|
||||
&.has-actions {
|
||||
@include clearfix();
|
||||
|
||||
.page-header {
|
||||
float: left;
|
||||
width: flex-grid(6,12);
|
||||
margin-right: flex-gutter();
|
||||
}
|
||||
|
||||
.nav-actions {
|
||||
position: relative;
|
||||
bottom: -($baseline*0.75);
|
||||
float: right;
|
||||
width: flex-grid(6,12);
|
||||
text-align: right;
|
||||
|
||||
.nav-item {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-right: ($baseline/2);
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// buttons
|
||||
.button {
|
||||
padding: ($baseline/4) ($baseline/2) ($baseline/3) ($baseline/2);
|
||||
}
|
||||
|
||||
.new-button {
|
||||
|
||||
}
|
||||
|
||||
.view-button {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// layout with actions
|
||||
&.has-subtitle {
|
||||
|
||||
.nav-actions {
|
||||
bottom: -($baseline*1.5);
|
||||
}
|
||||
}
|
||||
|
||||
// layout with navigation
|
||||
&.has-navigation {
|
||||
|
||||
.nav-actions {
|
||||
bottom: -($baseline*1.5);
|
||||
}
|
||||
|
||||
.navigation-link {
|
||||
@extend %cont-truncated;
|
||||
display: inline-block;
|
||||
vertical-align: bottom; // correct for extra padding in FF
|
||||
max-width: 250px;
|
||||
|
||||
&.navigation-current {
|
||||
@extend %ui-disabled;
|
||||
color: $gray;
|
||||
max-width: 250px;
|
||||
|
||||
&:before {
|
||||
color: $gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navigation-link:before {
|
||||
content: " / ";
|
||||
margin: ($baseline/4);
|
||||
color: $gray;
|
||||
|
||||
&:hover {
|
||||
color: $gray;
|
||||
}
|
||||
}
|
||||
|
||||
.navigation .navigation-link:first-child:before {
|
||||
content: "";
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CASE: wizard-based mast
|
||||
.mast-wizard {
|
||||
|
||||
.page-header-sub {
|
||||
@extend %t-title4;
|
||||
color: $gray;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.page-header-super {
|
||||
@extend %t-title4;
|
||||
float: left;
|
||||
width: flex-grid(12,12);
|
||||
margin-top: ($baseline/2);
|
||||
border-top: 1px solid $gray-l4;
|
||||
padding-top: ($baseline/2);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
// page metadata/action bar
|
||||
.metadata {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// layout - basic page content
|
||||
.wrapper-content {
|
||||
margin: 0;
|
||||
padding: 0 $baseline;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content {
|
||||
@include clearfix();
|
||||
@extend %t-copy-base;
|
||||
max-width: $fg-max-width;
|
||||
min-width: $fg-min-width;
|
||||
width: flex-grid(12);
|
||||
margin: 0 auto;
|
||||
color: $gray-d2;
|
||||
|
||||
header {
|
||||
position: relative;
|
||||
margin-bottom: $baseline;
|
||||
border-bottom: 1px solid $gray-l4;
|
||||
padding-bottom: ($baseline/2);
|
||||
|
||||
.title-sub {
|
||||
@extend %t-copy-sub1;
|
||||
display: block;
|
||||
margin: 0;
|
||||
color: $gray-l2;
|
||||
}
|
||||
|
||||
.title-1 {
|
||||
@extend %t-title3;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-weight: 600;
|
||||
color: $gray-d3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content-primary, .content-supplementary {
|
||||
@include box-sizing(border-box);
|
||||
}
|
||||
|
||||
// layout - primary content
|
||||
.content-primary {
|
||||
|
||||
.title-1 {
|
||||
@extend %t-title3;
|
||||
}
|
||||
|
||||
.title-2 {
|
||||
@extend %t-title4;
|
||||
margin: 0 0 ($baseline/2) 0;
|
||||
}
|
||||
|
||||
.title-3 {
|
||||
@extend %t-title6;
|
||||
margin: 0 0 ($baseline/2) 0;
|
||||
}
|
||||
|
||||
header {
|
||||
@include clearfix();
|
||||
|
||||
.title-2 {
|
||||
width: flex-grid(5, 12);
|
||||
margin: 0 flex-gutter() 0 0;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.tip {
|
||||
@extend %t-copy-sub2;
|
||||
width: flex-grid(7, 12);
|
||||
float: right;
|
||||
margin-top: ($baseline/2);
|
||||
text-align: right;
|
||||
color: $gray-l2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// layout - supplemental content
|
||||
.content-supplementary {
|
||||
|
||||
> section {
|
||||
margin: 0 0 $baseline 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// layout - grandfathered
|
||||
.main-wrapper {
|
||||
position: relative;
|
||||
margin: 0 ($baseline*2);
|
||||
}
|
||||
|
||||
.inner-wrapper {
|
||||
@include clearfix();
|
||||
position: relative;
|
||||
max-width: 1280px;
|
||||
margin: auto;
|
||||
|
||||
> article {
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
|
||||
.main-column {
|
||||
clear: both;
|
||||
float: left;
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
float: right;
|
||||
width: 28%;
|
||||
}
|
||||
|
||||
.left {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
|
||||
@@ -107,6 +107,23 @@ div.wrapper-comp-editor.is-inactive ~ div.launch-latex-compiler{
|
||||
|
||||
// ====================
|
||||
|
||||
// JQuery UI tabs font-weight override
|
||||
.ui-tabs-nav {
|
||||
|
||||
.ui-state-default {
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// xmodule editor tab font-weight override
|
||||
.xmodule_edit.xmodule_VideoDescriptor .editor-with-tabs .editor-tabs .inner_tab_wrap a.tab {
|
||||
font-weight: normal !important;
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// TODOs:
|
||||
|
||||
// * font-weight syncing
|
||||
|
||||
@@ -170,6 +170,7 @@ $color-staff-only: $black;
|
||||
|
||||
$color-heading-base: $gray-d2;
|
||||
$color-copy-base: $gray-l1;
|
||||
$color-copy-emphasized: $gray-d2;
|
||||
|
||||
// ====================
|
||||
|
||||
|
||||
@@ -133,6 +133,27 @@
|
||||
}
|
||||
}
|
||||
|
||||
// white secondary button
|
||||
%btn-secondary-white {
|
||||
@extend %ui-btn-secondary;
|
||||
border-color: $white-t2;
|
||||
color: $white-t3;
|
||||
|
||||
&:hover, &:active {
|
||||
border-color: $white;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&.current, &.active {
|
||||
background: $gray-d2;
|
||||
color: $gray-l5;
|
||||
|
||||
&:hover, &:active {
|
||||
background: $gray-d2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// green secondary button
|
||||
%btn-secondary-green {
|
||||
@extend %ui-btn-secondary;
|
||||
@@ -213,17 +234,6 @@
|
||||
|
||||
// ====================
|
||||
|
||||
// calls-to-action
|
||||
|
||||
// ====================
|
||||
|
||||
// specific buttons - view live
|
||||
%view-live-button {
|
||||
@extend %t-action4;
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// UI: element actions list
|
||||
|
||||
%actions-list {
|
||||
|
||||
@@ -9,13 +9,14 @@ textarea.text {
|
||||
@include box-sizing(border-box);
|
||||
@include linear-gradient($gray-l5, $white);
|
||||
@extend %t-copy-sub2;
|
||||
@extend %t-demi-strong;
|
||||
padding: 6px 8px 8px;
|
||||
border: 1px solid $gray-l2;
|
||||
border-radius: 2px;
|
||||
background-color: $gray-l5;
|
||||
box-shadow: inset 0 1px 2px $shadow-l1;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
color: $baseFontColor;
|
||||
color: $color-copy-emphasized;
|
||||
outline: 0;
|
||||
|
||||
&::-webkit-input-placeholder,
|
||||
@@ -59,6 +60,46 @@ textarea.text {
|
||||
// forms - additional UI
|
||||
form {
|
||||
|
||||
// CASE: cosmetic checkbox input
|
||||
.checkbox-cosmetic {
|
||||
|
||||
.input-checkbox-checked, .input-checkbox-unchecked, .label {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.input-checkbox-checked, .input-checkbox-unchecked {
|
||||
width: $baseline;
|
||||
}
|
||||
|
||||
.input-checkbox {
|
||||
@extend %cont-text-sr;
|
||||
|
||||
// CASE: unchecked
|
||||
~ label .input-checkbox-checked {
|
||||
display: none;
|
||||
}
|
||||
|
||||
~ label .input-checkbox-unchecked {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
// CASE: checked
|
||||
&:checked {
|
||||
|
||||
~ label .input-checkbox-checked {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
~ label .input-checkbox-unchecked {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// CASE: file input
|
||||
input[type=file] {
|
||||
@extend %t-copy-sub1;
|
||||
}
|
||||
@@ -97,28 +138,10 @@ form {
|
||||
}
|
||||
}
|
||||
|
||||
// ELEM: form wrapper
|
||||
.wrapper-create-element {
|
||||
height: 0;
|
||||
margin-bottom: $baseline;
|
||||
opacity: 0.0;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
|
||||
&.animate {
|
||||
@include transition(opacity $tmg-f1 ease-in-out 0s, height $tmg-f1 ease-in-out 0s);
|
||||
}
|
||||
|
||||
&.is-shown {
|
||||
height: auto; // define a specific height for the animating version of this UI to work properly
|
||||
opacity: 1.0;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// ELEM: form
|
||||
// form styling for creating a new content item (course, user, textbook)
|
||||
form[class^="create-"] {
|
||||
// TODO: refactor this into a placeholder to extend.
|
||||
.form-create {
|
||||
@extend %ui-window;
|
||||
|
||||
.title {
|
||||
@@ -213,12 +236,19 @@ form[class^="create-"] {
|
||||
|
||||
.tip {
|
||||
@extend %t-copy-sub2;
|
||||
@include transition(color, 0.15s, ease-in-out);
|
||||
@include transition(color 0.15s ease-in-out);
|
||||
|
||||
|
||||
display: block;
|
||||
margin-top: ($baseline/4);
|
||||
color: $gray-l3;
|
||||
}
|
||||
|
||||
.tip-note {
|
||||
display: block;
|
||||
margin-top: ($baseline/4);
|
||||
}
|
||||
|
||||
.tip-error {
|
||||
display: none;
|
||||
float: none;
|
||||
@@ -325,7 +355,6 @@ form[class^="create-"] {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// form - inline xblock name edit on unit, container, outline
|
||||
|
||||
// TOOD: abstract this out into a Sass placeholder
|
||||
@@ -362,6 +391,25 @@ form[class^="create-"] {
|
||||
}
|
||||
}
|
||||
|
||||
// ELEM: form wrapper
|
||||
.wrapper-create-element {
|
||||
height: 0;
|
||||
margin-bottom: $baseline;
|
||||
opacity: 0.0;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
|
||||
&.animate {
|
||||
@include transition(opacity $tmg-f1 ease-in-out 0s, height $tmg-f1 ease-in-out 0s);
|
||||
}
|
||||
|
||||
&.is-shown {
|
||||
height: auto; // define a specific height for the animating version of this UI to work properly
|
||||
opacity: 1.0;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// forms - grandfathered
|
||||
|
||||
@@ -141,6 +141,26 @@
|
||||
}
|
||||
}
|
||||
|
||||
// CASE: wizard-based mast
|
||||
.mast-wizard {
|
||||
|
||||
.page-header-sub {
|
||||
@extend %t-title4;
|
||||
color: $gray;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.page-header-super {
|
||||
@extend %t-title4;
|
||||
float: left;
|
||||
width: flex-grid(12,12);
|
||||
margin-top: ($baseline/2);
|
||||
border-top: 1px solid $gray-l4;
|
||||
padding-top: ($baseline/2);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
// page metadata/action bar
|
||||
.metadata {
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
.title {
|
||||
@extend %t-title5;
|
||||
@extend %t-strong;
|
||||
@extend %t-demi-strong;
|
||||
margin: ($baseline/4) ($baseline/2) ($baseline/2) ($baseline/2);
|
||||
color: $black;
|
||||
}
|
||||
@@ -54,7 +54,7 @@
|
||||
|
||||
// sections within a modal
|
||||
.modal-section {
|
||||
margin-bottom: ($baseline/2);
|
||||
margin-bottom: ($baseline*0.75);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
@@ -234,8 +234,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
// edit outline item settings
|
||||
.edit-outline-item-modal {
|
||||
// outline: edit item settings
|
||||
.wrapper-modal-window-bulkpublish-section,
|
||||
.wrapper-modal-window-bulkpublish-subsection,
|
||||
.wrapper-modal-window-bulkpublish-unit,
|
||||
.course-outline-modal {
|
||||
|
||||
.list-fields {
|
||||
|
||||
@@ -245,12 +248,6 @@
|
||||
margin-right: ($baseline/2);
|
||||
margin-bottom: ($baseline/4);
|
||||
|
||||
|
||||
// TODO: refactor the _forms.scss partial to allow for this area to inherit from it
|
||||
label, input, textarea {
|
||||
display: block;
|
||||
}
|
||||
|
||||
label {
|
||||
@extend %t-copy-sub1;
|
||||
@extend %t-strong;
|
||||
@@ -288,6 +285,27 @@
|
||||
.due-time {
|
||||
width: ($baseline*7);
|
||||
}
|
||||
|
||||
.tip {
|
||||
@extend %t-copy-sub1;
|
||||
@include transition(color, 0.15s, ease-in-out);
|
||||
display: block;
|
||||
margin-top: ($baseline/4);
|
||||
color: $gray-l2;
|
||||
}
|
||||
|
||||
.tip-warning {
|
||||
color: $gray-d2;
|
||||
}
|
||||
}
|
||||
|
||||
// CASE: type-based input
|
||||
.field-text {
|
||||
|
||||
// TODO: refactor the _forms.scss partial to allow for this area to inherit from it
|
||||
label, input, textarea {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
// CASE: select input
|
||||
@@ -305,15 +323,52 @@
|
||||
.input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// CASE: checkbox input
|
||||
.field-checkbox {
|
||||
|
||||
.label, label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// UI: grading section
|
||||
.edit-settings-grading {
|
||||
|
||||
.grading-type {
|
||||
margin-bottom: $baseline;
|
||||
}
|
||||
}
|
||||
|
||||
// UI: staff lock section
|
||||
.edit-staff-lock {
|
||||
|
||||
.checkbox-cosmetic .input-checkbox {
|
||||
@extend %cont-text-sr;
|
||||
|
||||
// CASE: unchecked
|
||||
~ .tip-warning {
|
||||
display: block;
|
||||
}
|
||||
|
||||
// CASE: checked
|
||||
&:checked {
|
||||
|
||||
~ .tip-warning {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// needed to override poorly scoped margin-bottom on any label element in a view (from _forms.scss)
|
||||
.checkbox-cosmetic .label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// xblock custom actions
|
||||
|
||||
@@ -198,7 +198,7 @@
|
||||
a {
|
||||
@include clearfix();
|
||||
@include transition(none);
|
||||
@extend %t-strong;
|
||||
@extend %t-demi-strong;
|
||||
display: block;
|
||||
border: 0px;
|
||||
padding: 7px $baseline;
|
||||
@@ -263,7 +263,6 @@
|
||||
|
||||
// outline UI
|
||||
// --------------------
|
||||
|
||||
// outline: utilities
|
||||
$outline-indent-width: $baseline;
|
||||
|
||||
@@ -294,82 +293,20 @@ $outline-indent-width: $baseline;
|
||||
}
|
||||
|
||||
|
||||
// UI: section
|
||||
%outline-section {
|
||||
@include transition(border-left-width $tmg-f2 linear 0s, border-left-color $tmg-f2 linear 0s, padding-left $tmg-f2 linear 0s);
|
||||
border-left: 1px solid $color-draft;
|
||||
margin-bottom: $baseline;
|
||||
padding: ($baseline/4) ($baseline/2) ($baseline/2) ($baseline/2);
|
||||
%outline-item-status {
|
||||
@extend %t-copy-sub2;
|
||||
@extend %t-strong;
|
||||
color: $color-copy-base;
|
||||
|
||||
// STATE: is-collapsed
|
||||
&.is-collapsed {
|
||||
border-left-width: ($baseline/4);
|
||||
padding-left: $baseline;
|
||||
|
||||
// CASE: is ready to be live
|
||||
&.is-ready {
|
||||
border-left-color: $color-ready;
|
||||
}
|
||||
|
||||
// CASE: is live
|
||||
&.is-live {
|
||||
border-left-color: $color-live;
|
||||
}
|
||||
|
||||
// CASE: has staff-only content
|
||||
&.is-staff-only {
|
||||
border-left-color: $color-staff-only;
|
||||
}
|
||||
|
||||
// CASE: has unpublished content
|
||||
&.has-warnings {
|
||||
border-left-color: $color-warning;
|
||||
}
|
||||
|
||||
// CASE: has errors
|
||||
&.has-errors {
|
||||
border-left-color: $color-error;
|
||||
}
|
||||
.icon {
|
||||
@extend %t-icon5;
|
||||
margin-right: ($baseline/4);
|
||||
}
|
||||
}
|
||||
|
||||
// UI: subsection
|
||||
%outline-subsection {
|
||||
@include transition(border-left-color $tmg-f2 linear 0s);
|
||||
margin-bottom: ($baseline/2);
|
||||
border: 1px solid $gray-l4;
|
||||
border-left: ($baseline/4) solid $color-draft;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
padding: ($baseline/4) ($baseline/2) ($baseline/2) ($baseline/2);
|
||||
|
||||
// CASE: is ready to be live
|
||||
&.is-ready {
|
||||
border-left-color: $color-ready;
|
||||
}
|
||||
|
||||
// CASE: is live
|
||||
&.is-live {
|
||||
border-left-color: $color-live;
|
||||
}
|
||||
|
||||
// CASE: is presented for staff only
|
||||
&.is-staff-only {
|
||||
border-left-color: $color-staff-only;
|
||||
}
|
||||
|
||||
// CASE: has unpublished content
|
||||
&.has-warnings {
|
||||
border-left-color: $color-warning;
|
||||
}
|
||||
|
||||
// CASE: has errors
|
||||
&.has-errors {
|
||||
border-left-color: $color-error;
|
||||
}
|
||||
}
|
||||
|
||||
%outline-item {
|
||||
// outline UI - complex
|
||||
// --------------------
|
||||
%outline-complex-item {
|
||||
|
||||
// UI: item title
|
||||
.item-title {
|
||||
@@ -424,154 +361,326 @@ $outline-indent-width: $baseline;
|
||||
}
|
||||
}
|
||||
|
||||
%outline-item-status {
|
||||
@extend %t-copy-sub2;
|
||||
@extend %t-strong;
|
||||
color: $color-copy-base;
|
||||
|
||||
.icon {
|
||||
@extend %t-icon5;
|
||||
margin-right: ($baseline/4);
|
||||
}
|
||||
}
|
||||
|
||||
// outline: sections
|
||||
.outline-section {
|
||||
@extend %ui-window;
|
||||
@extend %outline-item;
|
||||
@extend %outline-section;
|
||||
|
||||
// header - title
|
||||
.section-title {
|
||||
@extend %t-title5;
|
||||
@extend %t-strong;
|
||||
color: $color-heading-base;
|
||||
}
|
||||
|
||||
// status
|
||||
.section-status {
|
||||
@extend %outline-item-status;
|
||||
}
|
||||
|
||||
// status - release
|
||||
.status-release {
|
||||
@include transition(opacity $tmg-f2 ease-in-out 0s);
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
// status - grading
|
||||
.status-grading {
|
||||
@include transition(opacity $tmg-f2 ease-in-out 0s);
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.status-grading-value {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.status-grading-date {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-left: ($baseline/4);
|
||||
}
|
||||
|
||||
// status - message
|
||||
.status-message {
|
||||
margin-top: ($baseline/2);
|
||||
border-top: 1px solid $gray-l4;
|
||||
padding-top: ($baseline/4);
|
||||
|
||||
.icon {
|
||||
margin-right: ($baseline/4);
|
||||
}
|
||||
}
|
||||
|
||||
.status-message-copy {
|
||||
display: inline-block;
|
||||
color: $color-heading-base;
|
||||
}
|
||||
|
||||
// STATE: hover/active
|
||||
&:hover, &:active {
|
||||
|
||||
// status - release
|
||||
> .section-status .status-release {
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// outline: subsections
|
||||
.outline-subsection {
|
||||
@extend %outline-item;
|
||||
@extend %outline-subsection;
|
||||
// outline UI - simple
|
||||
// --------------------
|
||||
%outline-simple-item {
|
||||
border: 1px solid $gray-l4;
|
||||
border-left: ($baseline/4) solid $color-draft;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
padding: ($baseline*0.75);
|
||||
|
||||
// STATE: hover/active
|
||||
&:hover, &:active {
|
||||
box-shadow: 0 1px 1px $shadow-l2;
|
||||
// CASE: last-child in UI
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
// STATE: is-collapsed
|
||||
&.is-collapsed {
|
||||
.item-title a {
|
||||
color: $color-heading-base;
|
||||
|
||||
&:hover {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// header - title
|
||||
.subsection-title {
|
||||
@extend %t-title6;
|
||||
color: $color-heading-base;
|
||||
}
|
||||
|
||||
// status
|
||||
.subsection-status {
|
||||
@extend %outline-item-status;
|
||||
}
|
||||
// CASE: complex outline
|
||||
.outline-complex {
|
||||
|
||||
// STATE: hover/active
|
||||
&:hover, &:active {
|
||||
// outline: sections
|
||||
.outline-section {
|
||||
@include transition(border-left-width $tmg-f2 linear 0s, border-left-color $tmg-f2 linear 0s, padding-left $tmg-f2 linear 0s);
|
||||
@extend %ui-window;
|
||||
@extend %outline-complex-item;
|
||||
border-left: 1px solid $color-draft;
|
||||
margin-bottom: $baseline;
|
||||
padding: ($baseline/4) ($baseline/2) ($baseline/2) ($baseline/2);
|
||||
|
||||
// STATE: is-collapsed
|
||||
&.is-collapsed {
|
||||
border-left-width: ($baseline/4);
|
||||
padding-left: $baseline;
|
||||
|
||||
// CASE: is ready to be live
|
||||
&.is-ready {
|
||||
border-left-color: $color-ready;
|
||||
}
|
||||
|
||||
// CASE: is live
|
||||
&.is-live {
|
||||
border-left-color: $color-live;
|
||||
}
|
||||
|
||||
// CASE: has staff-only content
|
||||
&.is-staff-only {
|
||||
border-left-color: $color-staff-only;
|
||||
}
|
||||
|
||||
// CASE: has unpublished content
|
||||
&.has-warnings {
|
||||
border-left-color: $color-warning;
|
||||
}
|
||||
|
||||
// CASE: has errors
|
||||
&.has-errors {
|
||||
border-left-color: $color-error;
|
||||
}
|
||||
}
|
||||
|
||||
// header - title
|
||||
.section-title {
|
||||
@extend %t-title5;
|
||||
@extend %t-strong;
|
||||
color: $color-heading-base;
|
||||
}
|
||||
|
||||
// status
|
||||
.section-status {
|
||||
@extend %outline-item-status;
|
||||
}
|
||||
|
||||
// status - release
|
||||
> .subsection-status .status-release {
|
||||
opacity: 1.0;
|
||||
.status-release {
|
||||
@include transition(opacity $tmg-f2 ease-in-out 0s);
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
// status - message
|
||||
.status-message {
|
||||
margin-top: ($baseline/2);
|
||||
border-top: 1px solid $gray-l4;
|
||||
padding-top: ($baseline/4);
|
||||
|
||||
.icon {
|
||||
margin-right: ($baseline/4);
|
||||
}
|
||||
}
|
||||
|
||||
.status-message-copy {
|
||||
display: inline-block;
|
||||
color: $color-heading-base;
|
||||
}
|
||||
|
||||
// STATE: hover/active
|
||||
&:hover, &:active {
|
||||
|
||||
// status - release
|
||||
> .section-status .status-release {
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// outline: subsections
|
||||
.outline-subsection {
|
||||
@include transition(border-left-color $tmg-f2 linear 0s);
|
||||
@extend %outline-complex-item;
|
||||
margin-bottom: ($baseline/2);
|
||||
border: 1px solid $gray-l4;
|
||||
border-left: ($baseline/4) solid $color-draft;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
padding: ($baseline*0.75);
|
||||
|
||||
|
||||
// CASE: is ready to be live
|
||||
&.is-ready {
|
||||
border-left-color: $color-ready;
|
||||
}
|
||||
|
||||
// CASE: is live
|
||||
&.is-live {
|
||||
border-left-color: $color-live;
|
||||
}
|
||||
|
||||
// CASE: is presented for staff only
|
||||
&.is-staff-only {
|
||||
border-left-color: $color-staff-only;
|
||||
}
|
||||
|
||||
// CASE: has unpublished content
|
||||
&.has-warnings {
|
||||
border-left-color: $color-warning;
|
||||
}
|
||||
|
||||
// CASE: has errors
|
||||
&.has-errors {
|
||||
border-left-color: $color-error;
|
||||
}
|
||||
|
||||
// STATE: hover/active
|
||||
&:hover, &:active {
|
||||
box-shadow: 0 1px 1px $shadow-l2;
|
||||
}
|
||||
|
||||
// STATE: is-collapsed
|
||||
&.is-collapsed {
|
||||
|
||||
}
|
||||
|
||||
// header - title
|
||||
.subsection-title {
|
||||
@extend %t-title6;
|
||||
color: $color-heading-base;
|
||||
}
|
||||
|
||||
// status
|
||||
.subsection-status {
|
||||
@extend %outline-item-status;
|
||||
}
|
||||
|
||||
// STATE: hover/active
|
||||
&:hover, &:active {
|
||||
|
||||
// status - release
|
||||
> .subsection-status .status-release {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
// status - grading
|
||||
> .subsection-status .status-grading {
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
// status - grading
|
||||
> .subsection-status .status-grading {
|
||||
opacity: 1.0;
|
||||
.status-grading {
|
||||
@include transition(opacity $tmg-f2 ease-in-out 0s);
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.status-grading-value {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.status-grading-date {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-left: ($baseline/4);
|
||||
}
|
||||
}
|
||||
|
||||
// outline: units
|
||||
.outline-unit {
|
||||
@extend %outline-complex-item;
|
||||
margin-bottom: ($baseline/2);
|
||||
border: 1px solid $gray-l4;
|
||||
padding: ($baseline/4) ($baseline/2);
|
||||
|
||||
// header - title
|
||||
.unit-title {
|
||||
@extend %t-title7;
|
||||
color: $color-heading-base;
|
||||
}
|
||||
|
||||
.unit-status {
|
||||
@extend %outline-item-status;
|
||||
}
|
||||
|
||||
// STATE: hover/active
|
||||
&:hover, &:active {
|
||||
box-shadow: 0 1px 1px $shadow-l2;
|
||||
|
||||
// status - release
|
||||
.unit-status .status-release {
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// outline: units
|
||||
.outline-unit {
|
||||
@extend %outline-item;
|
||||
margin-bottom: ($baseline/2);
|
||||
border: 1px solid $gray-l4;
|
||||
padding: ($baseline/4) ($baseline/2);
|
||||
// CASE: simple outline
|
||||
.outline-simple {
|
||||
|
||||
// header - title
|
||||
.unit-title {
|
||||
@extend %t-title7;
|
||||
color: $color-heading-base;
|
||||
}
|
||||
// outline: sections
|
||||
.outline-section {
|
||||
@extend %outline-simple-item;
|
||||
margin-bottom: $baseline;
|
||||
padding: ($baseline/2);
|
||||
|
||||
.unit-status {
|
||||
@extend %outline-item-status;
|
||||
}
|
||||
// header - title
|
||||
.section-title {
|
||||
@extend %t-title5;
|
||||
@extend %t-strong;
|
||||
color: $color-heading-base;
|
||||
}
|
||||
|
||||
// STATE: hover/active
|
||||
&:hover, &:active {
|
||||
box-shadow: 0 1px 1px $shadow-l2;
|
||||
// status
|
||||
.section-status {
|
||||
@extend %outline-item-status;
|
||||
}
|
||||
|
||||
// status - release
|
||||
.unit-status .status-release {
|
||||
opacity: 1.0;
|
||||
.status-release {
|
||||
@include transition(opacity $tmg-f2 ease-in-out 0s);
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
// status - grading
|
||||
.status-grading {
|
||||
@include transition(opacity $tmg-f2 ease-in-out 0s);
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.status-grading-value {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.status-grading-date {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-left: ($baseline/4);
|
||||
}
|
||||
|
||||
// status - message
|
||||
.status-message {
|
||||
margin-top: ($baseline/2);
|
||||
border-top: 1px solid $gray-l4;
|
||||
padding-top: ($baseline/4);
|
||||
|
||||
.icon {
|
||||
margin-right: ($baseline/4);
|
||||
}
|
||||
}
|
||||
|
||||
.status-message-copy {
|
||||
display: inline-block;
|
||||
color: $color-heading-base;
|
||||
}
|
||||
}
|
||||
|
||||
// outline: subsections
|
||||
.outline-subsection {
|
||||
@extend %outline-simple-item;
|
||||
margin-bottom: ($baseline/2);
|
||||
padding: ($baseline/2);
|
||||
|
||||
// header - title
|
||||
.subsection-title {
|
||||
@extend %t-title6;
|
||||
color: $color-heading-base;
|
||||
}
|
||||
|
||||
// status
|
||||
.subsection-status {
|
||||
@extend %outline-item-status;
|
||||
}
|
||||
}
|
||||
|
||||
// outline: units
|
||||
.outline-unit {
|
||||
@extend %outline-simple-item;
|
||||
margin-bottom: ($baseline/4);
|
||||
padding: ($baseline/4) ($baseline/2);
|
||||
|
||||
// header - title
|
||||
.unit-title {
|
||||
@extend %t-title7;
|
||||
color: $color-heading-base;
|
||||
}
|
||||
|
||||
.unit-status {
|
||||
@extend %outline-item-status;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -527,7 +527,7 @@
|
||||
&.wrapper-alert-warning {
|
||||
box-shadow: 0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $orange;
|
||||
|
||||
[class^="icon"] {
|
||||
.alert-symbol {
|
||||
color: $orange;
|
||||
}
|
||||
}
|
||||
@@ -535,7 +535,7 @@
|
||||
&.wrapper-alert-error {
|
||||
box-shadow: 0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $red-l1;
|
||||
|
||||
[class^="icon"] {
|
||||
.alert-symbol {
|
||||
color: $red-l1;
|
||||
}
|
||||
}
|
||||
@@ -543,7 +543,7 @@
|
||||
&.wrapper-alert-confirmation {
|
||||
box-shadow: 0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $green;
|
||||
|
||||
[class^="icon"] {
|
||||
.alert-symbol {
|
||||
color: $green;
|
||||
}
|
||||
}
|
||||
@@ -551,7 +551,7 @@
|
||||
&.wrapper-alert-announcement {
|
||||
box-shadow: 0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $blue;
|
||||
|
||||
[class^="icon"] {
|
||||
.alert-symbol {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
@@ -559,7 +559,7 @@
|
||||
&.wrapper-alert-step-required {
|
||||
box-shadow: 0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $pink;
|
||||
|
||||
[class^="icon"] {
|
||||
.alert-symbol {
|
||||
color: $pink;
|
||||
}
|
||||
}
|
||||
@@ -579,11 +579,11 @@
|
||||
@extend %t-strong;
|
||||
}
|
||||
|
||||
[class^="icon"], .copy {
|
||||
.alert-symbol, .copy {
|
||||
float: left;
|
||||
}
|
||||
|
||||
[class^="icon"] {
|
||||
.alert-symbol {
|
||||
@include transition (color 0.50s ease-in-out 0s);
|
||||
@extend %t-icon3;
|
||||
width: flex-grid(1, 12);
|
||||
@@ -605,7 +605,7 @@
|
||||
// with actions
|
||||
&.has-actions {
|
||||
|
||||
[class^="icon"] {
|
||||
.alert-symbol {
|
||||
width: flex-grid(1, 12);
|
||||
}
|
||||
|
||||
@@ -667,6 +667,29 @@
|
||||
background: $gray-d1;
|
||||
}
|
||||
}
|
||||
|
||||
// with dismiss (to sunset action-alert-clos)
|
||||
.action-dismiss {
|
||||
|
||||
.button {
|
||||
@extend %btn-secondary-white;
|
||||
padding:($baseline/4) ($baseline/2);
|
||||
}
|
||||
|
||||
.icon,.button-copy {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.icon {
|
||||
@extend %t-icon4;
|
||||
margin-right: ($baseline/4);
|
||||
}
|
||||
|
||||
.button-copy {
|
||||
@extend %t-copy-sub1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
@@ -243,6 +243,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
// learn more (aka external help button)
|
||||
.external-help-button {
|
||||
@extend %ui-btn-flat-outline;
|
||||
@extend %sizing;
|
||||
}
|
||||
|
||||
// actions
|
||||
.list-actions {
|
||||
@extend %cont-no-list;
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
%t-strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
%t-demi-strong {
|
||||
font-weight: 500;
|
||||
}
|
||||
%t-regular {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@@ -497,18 +497,21 @@
|
||||
&:first-child {
|
||||
border-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
&.is-set {
|
||||
opacity: 1.0;
|
||||
background-color: $white;
|
||||
|
||||
.setting-input {
|
||||
color: $blue-l1;
|
||||
// STATE: hover & focus
|
||||
&:hover, &:focus {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
&.is-set {
|
||||
opacity: 1.0;
|
||||
background-color: $white;
|
||||
|
||||
.setting-input {
|
||||
color: $blue-l1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
@import 'views/dashboard';
|
||||
@import 'views/export';
|
||||
@import 'views/index';
|
||||
@import 'views/course-create';
|
||||
@import 'views/import';
|
||||
@import 'views/outline';
|
||||
@import 'views/settings';
|
||||
|
||||
@@ -150,6 +150,10 @@
|
||||
.user {
|
||||
@extend %t-strong;
|
||||
}
|
||||
|
||||
.user {
|
||||
@extend %cont-text-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper-release {
|
||||
@@ -157,6 +161,11 @@
|
||||
.release-date {
|
||||
@extend %t-strong;
|
||||
}
|
||||
|
||||
.release-with {
|
||||
@extend %t-title8;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper-visibility {
|
||||
@@ -170,6 +179,13 @@
|
||||
margin-left: ($baseline/4);
|
||||
color: $gray-d1;
|
||||
}
|
||||
|
||||
.inherited-from {
|
||||
@extend %t-title8;
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
.wrapper-pub-actions {
|
||||
@@ -213,6 +229,10 @@
|
||||
.user {
|
||||
@extend %t-strong;
|
||||
}
|
||||
|
||||
.user {
|
||||
@extend %cont-text-wrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,8 +244,10 @@
|
||||
.wrapper-unit-id {
|
||||
|
||||
.unit-id-value {
|
||||
@extend %cont-text-wrap;
|
||||
@extend %t-copy-sub1;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tip {
|
||||
@@ -237,29 +259,9 @@
|
||||
}
|
||||
|
||||
.wrapper-unit-tree-location {
|
||||
// tree location-specific styles should go here
|
||||
|
||||
.outline-section{
|
||||
box-shadow: none;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.outline-subsection {
|
||||
border-left: 1px solid $gray-l4;
|
||||
padding: ($baseline/2);
|
||||
|
||||
.subsection-header {
|
||||
margin-bottom: ($baseline/2);
|
||||
}
|
||||
}
|
||||
|
||||
.item-title a {
|
||||
color: $color-heading-base;
|
||||
|
||||
&:hover {
|
||||
color: $blue;
|
||||
}
|
||||
.item-title {
|
||||
@extend %cont-text-wrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
122
cms/static/sass/views/_course-create.scss
Normal file
122
cms/static/sass/views/_course-create.scss
Normal file
@@ -0,0 +1,122 @@
|
||||
// studio - views - course creation page
|
||||
// ====================
|
||||
|
||||
.view-course-create {
|
||||
|
||||
// basic layout
|
||||
// --------------------
|
||||
.content-primary, .content-supplementary {
|
||||
@include box-sizing(border-box);
|
||||
float: left;
|
||||
}
|
||||
|
||||
.content-primary {
|
||||
width: flex-grid(9, 12);
|
||||
margin-right: flex-gutter();
|
||||
}
|
||||
|
||||
.content-supplementary {
|
||||
width: flex-grid(3, 12);
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
// header/masthead
|
||||
// --------------------
|
||||
.mast .page-header-super {
|
||||
|
||||
.course-original-title-id, .course-original-title {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.course-original-title-id {
|
||||
@extend %t-title5;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// course re-run form
|
||||
// --------------------
|
||||
.rerun-course {
|
||||
|
||||
.row {
|
||||
@include clearfix();
|
||||
margin-bottom: ($baseline*0.75);
|
||||
}
|
||||
|
||||
.column {
|
||||
float: left;
|
||||
width: 48%;
|
||||
}
|
||||
|
||||
.column:first-child {
|
||||
margin-right: 4%;
|
||||
}
|
||||
|
||||
label {
|
||||
@extend %t-title7;
|
||||
display: block;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.rerun-course-org,
|
||||
.rerun-course-number,
|
||||
.rerun-course-name,
|
||||
.rerun-course-run {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rerun-course-name {
|
||||
@extend %t-title5;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.rerun-course-save {
|
||||
@include blue-button;
|
||||
|
||||
.icon {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: ($baseline/4);
|
||||
}
|
||||
}
|
||||
|
||||
.rerun-course-cancel {
|
||||
@include white-button;
|
||||
}
|
||||
|
||||
.item-details {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.wrap-error {
|
||||
@include transition(opacity $tmg-f2 ease 0s);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.wrap-error.is-shown {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.message-status {
|
||||
display: block;
|
||||
margin-bottom: 0;
|
||||
padding: ($baseline*.5) ($baseline*1.5) 8px ($baseline*1.5);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
// NOTE: override for modern button styling until all buttons (in _forms.scss) can be converted
|
||||
.actions {
|
||||
|
||||
.action-primary {
|
||||
@include blue-button;
|
||||
@extend %t-action2;
|
||||
}
|
||||
|
||||
.action-secondary {
|
||||
@include grey-button;
|
||||
@extend %t-action2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -236,7 +236,7 @@
|
||||
@extend %t-strong;
|
||||
margin: ($baseline/2);
|
||||
|
||||
[class^="icon-"] {
|
||||
.icon {
|
||||
margin-right: ($baseline/4);
|
||||
}
|
||||
}
|
||||
@@ -294,120 +294,307 @@
|
||||
// ELEM: course listings
|
||||
.courses {
|
||||
margin: $baseline 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
@extend %t-title6;
|
||||
margin-bottom: $baseline;
|
||||
border-bottom: 1px solid $gray-l3;
|
||||
padding-bottom: ($baseline/2);
|
||||
color: $gray-l2;
|
||||
}
|
||||
|
||||
.title {
|
||||
@extend %t-title6;
|
||||
margin-bottom: $baseline;
|
||||
border-bottom: 1px solid $gray-l3;
|
||||
padding-bottom: ($baseline/2);
|
||||
color: $gray-l2;
|
||||
}
|
||||
}
|
||||
|
||||
.list-courses {
|
||||
margin-top: $baseline;
|
||||
border-radius: 3px;
|
||||
border: 1px solid $gray;
|
||||
border: 1px solid $gray-l2;
|
||||
background: $white;
|
||||
box-shadow: 0 1px 2px $shadow-l1;
|
||||
box-shadow: 0 1px 1px $shadow-l1;
|
||||
|
||||
.course-item {
|
||||
@include box-sizing(border-box);
|
||||
width: flex-grid(9, 9);
|
||||
position: relative;
|
||||
border-bottom: 1px solid $gray-l1;
|
||||
padding: $baseline;
|
||||
li:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// STATE: hover/focus
|
||||
&:hover {
|
||||
background: $paleYellow;
|
||||
|
||||
.course-actions .view-live-button {
|
||||
opacity: 1.0;
|
||||
pointer-events: auto;
|
||||
}
|
||||
// UI: course wrappers (needed for status messages)
|
||||
.wrapper-course {
|
||||
|
||||
.course-title {
|
||||
color: $orange-d1;
|
||||
}
|
||||
// CASE: has status
|
||||
&.has-status {
|
||||
|
||||
.course-metadata {
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
.course-link, .course-actions {
|
||||
.course-status {
|
||||
@include box-sizing(border-box);
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
// encompassing course link
|
||||
.course-link {
|
||||
@extend %ui-depth2;
|
||||
width: flex-grid(7, 9);
|
||||
margin-right: flex-gutter();
|
||||
}
|
||||
|
||||
// course title
|
||||
.course-title {
|
||||
@extend %t-title4;
|
||||
@extend %t-light;
|
||||
margin: 0 ($baseline*2) ($baseline/4) 0;
|
||||
}
|
||||
|
||||
// course metadata
|
||||
.course-metadata {
|
||||
@extend %t-copy-sub1;
|
||||
@include transition(opacity $tmg-f1 ease-in-out 0);
|
||||
color: $gray;
|
||||
opacity: 0.75;
|
||||
|
||||
.metadata-item {
|
||||
display: inline-block;
|
||||
|
||||
&:after {
|
||||
content: "/";
|
||||
margin-left: ($baseline/10);
|
||||
margin-right: ($baseline/10);
|
||||
color: $gray-l4;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
@extend %cont-text-sr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.course-actions {
|
||||
@extend %ui-depth3;
|
||||
position: static;
|
||||
width: flex-grid(2, 9);
|
||||
width: flex-grid(3, 9);
|
||||
padding-right: ($baseline/2);
|
||||
text-align: right;
|
||||
|
||||
// view live button
|
||||
.view-live-button {
|
||||
@extend %ui-depth3;
|
||||
@include transition(opacity $tmg-f2 ease-in-out 0);
|
||||
@include box-sizing(border-box);
|
||||
padding: ($baseline/2);
|
||||
opacity: 0.0;
|
||||
pointer-events: none;
|
||||
.value {
|
||||
|
||||
&:hover {
|
||||
opacity: 1.0;
|
||||
pointer-events: auto;
|
||||
.copy, .icon {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.icon {
|
||||
@extend %t-icon4;
|
||||
margin-right: ($baseline/2);
|
||||
}
|
||||
|
||||
.copy {
|
||||
@extend %t-copy-sub1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
.status-message {
|
||||
@extend %t-copy-sub1;
|
||||
background-color: $gray-l5;
|
||||
box-shadow: 0 2px 2px 0 $shadow inset;
|
||||
padding: ($baseline*0.75) $baseline;
|
||||
|
||||
&.has-actions {
|
||||
|
||||
.copy, .status-actions {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.copy {
|
||||
width: 65%;
|
||||
margin: 0 $baseline 0 0;
|
||||
}
|
||||
|
||||
.status-actions {
|
||||
width: 30%;
|
||||
text-align: right;
|
||||
|
||||
.button {
|
||||
@extend %btn-secondary-white;
|
||||
padding:($baseline/4) ($baseline/2);
|
||||
}
|
||||
|
||||
.icon,.button-copy {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.icon {
|
||||
@extend %t-icon4;
|
||||
margin-right: ($baseline/4);
|
||||
}
|
||||
|
||||
.button-copy {
|
||||
@extend %t-copy-sub1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UI: individual course listings
|
||||
.course-item {
|
||||
@include box-sizing(border-box);
|
||||
width: flex-grid(9, 9);
|
||||
position: relative;
|
||||
border-bottom: 1px solid $gray-l2;
|
||||
padding: $baseline;
|
||||
|
||||
// STATE: hover/focus
|
||||
&:hover {
|
||||
background: $paleYellow;
|
||||
|
||||
.course-actions {
|
||||
opacity: 1.0;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.course-title {
|
||||
color: $orange-d1;
|
||||
}
|
||||
|
||||
.course-metadata {
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
.course-link, .course-actions {
|
||||
@include box-sizing(border-box);
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
// encompassing course link
|
||||
.course-link {
|
||||
@extend %ui-depth2;
|
||||
width: flex-grid(6, 9);
|
||||
margin-right: flex-gutter();
|
||||
}
|
||||
|
||||
// course title
|
||||
.course-title {
|
||||
@extend %t-title4;
|
||||
margin: 0 ($baseline*2) ($baseline/4) 0;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
// course metadata
|
||||
.course-metadata {
|
||||
@extend %t-copy-sub1;
|
||||
@include transition(opacity $tmg-f1 ease-in-out 0);
|
||||
color: $gray;
|
||||
opacity: 0.75;
|
||||
|
||||
.metadata-item {
|
||||
display: inline-block;
|
||||
|
||||
&:after {
|
||||
content: "/";
|
||||
margin-left: ($baseline/10);
|
||||
margin-right: ($baseline/10);
|
||||
color: $gray-l4;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
@extend %cont-text-sr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.course-actions {
|
||||
@include transition(opacity $tmg-f2 ease-in-out 0);
|
||||
@extend %ui-depth3;
|
||||
position: static;
|
||||
width: flex-grid(3, 9);
|
||||
text-align: right;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
|
||||
.action {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: ($baseline/2);
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
@extend %t-action3;
|
||||
}
|
||||
|
||||
// view live button
|
||||
.view-button {
|
||||
@include box-sizing(border-box);
|
||||
padding: ($baseline/2);
|
||||
}
|
||||
|
||||
// course re-run button
|
||||
.action-rerun {
|
||||
margin-right: $baseline;
|
||||
}
|
||||
|
||||
.rerun-button {
|
||||
font-weight: 600;
|
||||
// TODO: sync up button styling and add secondary style here
|
||||
}
|
||||
}
|
||||
|
||||
// CASE: is processing
|
||||
&.is-processing {
|
||||
|
||||
.course-status .value {
|
||||
color: $gray-l2;
|
||||
}
|
||||
}
|
||||
|
||||
// CASE: has an error
|
||||
&.has-error {
|
||||
|
||||
.course-status {
|
||||
color: $red; // TODO: abstract this out to an error-based color variable
|
||||
}
|
||||
|
||||
~ .status-message {
|
||||
background: $red-l1; // TODO: abstract this out to an error-based color variable
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
// CASE: last course in listing
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// CASE: courses that are being processed
|
||||
.courses-processing {
|
||||
margin-bottom: ($baseline*2);
|
||||
border-bottom: 1px solid $gray-l3;
|
||||
padding-bottom: ($baseline*2);
|
||||
|
||||
// TODO: abstract this case out better with normal course listings
|
||||
.list-courses {
|
||||
border: none;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.wrapper-course {
|
||||
@extend %ui-window;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.course-item {
|
||||
border: none;
|
||||
|
||||
// STATE: hover/focus
|
||||
&:hover {
|
||||
background: inherit;
|
||||
|
||||
.course-title {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// course details (replacement for course-link when a course cannot be linked)
|
||||
.course-details {
|
||||
@extend %ui-depth2;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: flex-grid(6, 9);
|
||||
margin-right: flex-gutter();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// ELEM: new user form
|
||||
.wrapper-create-course {
|
||||
|
||||
@@ -494,6 +681,5 @@
|
||||
margin-bottom: 0;
|
||||
padding: ($baseline*.5) ($baseline*1.5) 8px ($baseline*1.5);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.incontext-editor-action-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.incontext-editor-open-action {
|
||||
@include transition(opacity $tmg-f1 ease-in-out 0);
|
||||
opacity: 0.0;
|
||||
@@ -171,14 +175,316 @@
|
||||
@extend %expand-collapse;
|
||||
}
|
||||
|
||||
// course status
|
||||
// --------------------
|
||||
.course-status {
|
||||
margin-bottom: $baseline;
|
||||
|
||||
.status-release {
|
||||
@extend %t-copy-base;
|
||||
display: inline-block;
|
||||
color: $color-copy-base;
|
||||
}
|
||||
|
||||
.status-release-label,
|
||||
.status-release-value,
|
||||
.status-actions {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.status-release-label {
|
||||
margin-right: ($baseline/4);
|
||||
}
|
||||
|
||||
.status-release-value {
|
||||
@extend %t-strong;
|
||||
}
|
||||
|
||||
.status-actions {
|
||||
@extend %actions-list;
|
||||
@include transition(opacity $tmg-f1 ease-in-out 0);
|
||||
margin-left: ($baseline/4);
|
||||
opacity: 0.0;
|
||||
}
|
||||
|
||||
// STATE: hover
|
||||
&:hover {
|
||||
|
||||
.status-actions {
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// outline
|
||||
// --------------------
|
||||
.outline {
|
||||
|
||||
// UI: simple version of the outline
|
||||
.outline-simple {
|
||||
|
||||
}
|
||||
|
||||
// UI: complex version of the outline
|
||||
.outline-complex {
|
||||
|
||||
.outline-content {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
// outline: items
|
||||
.outline-item {
|
||||
|
||||
// CASE: expand/collapse-able
|
||||
&.is-collapsible {
|
||||
|
||||
// only select the current item's toggle expansion controls
|
||||
&:nth-child(1) .ui-toggle-expansion, &:nth-child(1) .item-title {
|
||||
|
||||
// STATE: hover/active
|
||||
&:hover, &:active {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-dragging {
|
||||
@include transition-property(none);
|
||||
}
|
||||
}
|
||||
|
||||
// item: title
|
||||
.item-title {
|
||||
|
||||
// STATE: is-editable
|
||||
&.is-editable {
|
||||
|
||||
// editor
|
||||
+ .editor {
|
||||
display: block;
|
||||
|
||||
.item-edit-title {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// STATE: drag and drop
|
||||
.drop-target-prepend .draggable-drop-indicator-initial {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
// STATE: was dropped
|
||||
&.was-dropped {
|
||||
border-color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
// outline: sections
|
||||
// --------------------
|
||||
.outline-section {
|
||||
padding: ($baseline*0.75) $baseline ($baseline*0.75) ($baseline + 4);
|
||||
|
||||
// header
|
||||
.section-header {
|
||||
@extend %outline-item-header;
|
||||
|
||||
.incontext-editor-input {
|
||||
@extend %t-strong;
|
||||
@extend %t-title5;
|
||||
}
|
||||
}
|
||||
|
||||
.section-header-details {
|
||||
float: left;
|
||||
width: flex-grid(6, 9);
|
||||
|
||||
.icon, .wrapper-section-title {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: ($baseline/4);
|
||||
}
|
||||
|
||||
.wrapper-section-title {
|
||||
width: flex-grid(5, 6);
|
||||
line-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.section-header-actions {
|
||||
float: right;
|
||||
width: flex-grid(3, 9);
|
||||
margin-top: -($baseline/4);
|
||||
text-align: right;
|
||||
|
||||
.actions-list {
|
||||
@extend %actions-list;
|
||||
@extend %t-action2;
|
||||
}
|
||||
}
|
||||
|
||||
// in-context actions
|
||||
.incontext-editor-action-wrapper {
|
||||
top: -($baseline/20);
|
||||
}
|
||||
|
||||
// status
|
||||
.section-status {
|
||||
margin: 0 0 0 ($outline-indent-width*1.25);
|
||||
}
|
||||
|
||||
// content
|
||||
.section-content {
|
||||
@extend %outline-item-content-shown;
|
||||
}
|
||||
|
||||
// CASE: is-collapsible
|
||||
&.is-collapsible {
|
||||
@extend %ui-expand-collapse;
|
||||
|
||||
.ui-toggle-expansion {
|
||||
@extend %t-icon3;
|
||||
color: $gray-l3;
|
||||
}
|
||||
}
|
||||
|
||||
// STATE: is-collapsed
|
||||
&.is-collapsed {
|
||||
|
||||
.section-content {
|
||||
@extend %outline-item-content-hidden;
|
||||
}
|
||||
}
|
||||
|
||||
// STATE: drag and drop - was dropped
|
||||
&.was-dropped {
|
||||
border-left-color: $ui-action-primary-color-focus;
|
||||
}
|
||||
}
|
||||
|
||||
// outline: subsections
|
||||
// --------------------
|
||||
.list-subsections {
|
||||
margin: $baseline 0 0 0;
|
||||
}
|
||||
|
||||
.outline-subsection {
|
||||
padding: ($baseline*0.75);
|
||||
|
||||
// header
|
||||
.subsection-header {
|
||||
@extend %outline-item-header;
|
||||
|
||||
.incontext-editor-input {
|
||||
@extend %t-title6;
|
||||
}
|
||||
}
|
||||
|
||||
.subsection-header-details {
|
||||
float: left;
|
||||
width: flex-grid(6, 9);
|
||||
|
||||
.icon, .wrapper-subsection-title {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: ($baseline/4);
|
||||
}
|
||||
|
||||
.wrapper-subsection-title {
|
||||
width: flex-grid(5, 6);
|
||||
margin-top: -($baseline/10);
|
||||
line-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.subsection-header-actions {
|
||||
float: right;
|
||||
width: flex-grid(3, 9);
|
||||
margin-top: -($baseline/4);
|
||||
text-align: right;
|
||||
|
||||
.actions-list {
|
||||
@extend %actions-list;
|
||||
@extend %t-action2;
|
||||
margin-right: ($baseline/2);
|
||||
}
|
||||
}
|
||||
|
||||
// in-context actions
|
||||
.incontext-editor-action-wrapper {
|
||||
top: -($baseline/10);
|
||||
}
|
||||
|
||||
// status
|
||||
.subsection-status {
|
||||
margin: 0 0 0 $outline-indent-width;
|
||||
}
|
||||
|
||||
// content
|
||||
.subsection-content {
|
||||
@extend %outline-item-content-shown;
|
||||
}
|
||||
|
||||
// CASE: is-collapsible
|
||||
&.is-collapsible {
|
||||
@extend %ui-expand-collapse;
|
||||
|
||||
.ui-toggle-expansion {
|
||||
@extend %t-icon4;
|
||||
color: $gray-l3;
|
||||
}
|
||||
}
|
||||
|
||||
// STATE: is-collapsed
|
||||
&.is-collapsed {
|
||||
|
||||
.subsection-content {
|
||||
@extend %outline-item-content-hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// outline: units
|
||||
// --------------------
|
||||
.list-units {
|
||||
margin: $baseline 0 0 0;
|
||||
}
|
||||
|
||||
.outline-unit {
|
||||
@include transition(margin $tmg-f2 linear 0s); // needed for drag and drop transitions
|
||||
margin-left: $outline-indent-width;
|
||||
|
||||
// header
|
||||
.unit-header {
|
||||
@extend %outline-item-header;
|
||||
}
|
||||
|
||||
.unit-header-details {
|
||||
float: left;
|
||||
width: flex-grid(6, 9);
|
||||
margin-top: ($baseline/4);
|
||||
}
|
||||
|
||||
.unit-header-actions {
|
||||
float: right;
|
||||
width: flex-grid(3, 9);
|
||||
margin-top: -($baseline/10);
|
||||
text-align: right;
|
||||
|
||||
.actions-list {
|
||||
@extend %actions-list;
|
||||
@extend %t-action2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add/new items
|
||||
.add-item {
|
||||
margin-top: ($baseline*0.75);
|
||||
@@ -209,248 +515,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// outline: items
|
||||
.outline-item {
|
||||
|
||||
// CASE: expand/collapse-able
|
||||
&.is-collapsible {
|
||||
|
||||
// only select the current item's toggle expansion controls
|
||||
&:nth-child(1) .ui-toggle-expansion, &:nth-child(1) .item-title {
|
||||
|
||||
// STATE: hover/active
|
||||
&:hover, &:active {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-dragging {
|
||||
@include transition-property(none);
|
||||
}
|
||||
}
|
||||
|
||||
// item: title
|
||||
.item-title {
|
||||
|
||||
// STATE: is-editable
|
||||
&.is-editable {
|
||||
|
||||
// editor
|
||||
+ .editor {
|
||||
display: block;
|
||||
|
||||
.item-edit-title {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// STATE: drag and drop
|
||||
.drop-target-prepend .draggable-drop-indicator-initial {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
// STATE: was dropped
|
||||
&.was-dropped {
|
||||
border-color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
// outline: sections
|
||||
// --------------------
|
||||
.outline-section {
|
||||
padding: ($baseline*0.75) $baseline ($baseline*0.75) ($baseline + 4);
|
||||
|
||||
// header
|
||||
.section-header {
|
||||
@extend %outline-item-header;
|
||||
|
||||
.incontext-editor-input {
|
||||
@extend %t-strong;
|
||||
@extend %t-title5;
|
||||
}
|
||||
}
|
||||
|
||||
.section-header-details {
|
||||
float: left;
|
||||
width: flex-grid(6, 9);
|
||||
|
||||
.icon, .wrapper-section-title {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: ($baseline/4);
|
||||
}
|
||||
|
||||
.wrapper-section-title {
|
||||
width: flex-grid(5, 6);
|
||||
line-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.section-header-actions {
|
||||
float: right;
|
||||
width: flex-grid(3, 9);
|
||||
margin-top: -($baseline/4);
|
||||
text-align: right;
|
||||
|
||||
.actions-list {
|
||||
@extend %actions-list;
|
||||
@extend %t-action2;
|
||||
}
|
||||
}
|
||||
|
||||
// status
|
||||
.section-status {
|
||||
margin: 0 0 0 ($outline-indent-width*1.25);
|
||||
}
|
||||
|
||||
// content
|
||||
.section-content {
|
||||
@extend %outline-item-content-shown;
|
||||
}
|
||||
|
||||
// CASE: is-collapsible
|
||||
&.is-collapsible {
|
||||
@extend %ui-expand-collapse;
|
||||
|
||||
.ui-toggle-expansion {
|
||||
@extend %t-icon3;
|
||||
color: $gray-l3;
|
||||
}
|
||||
}
|
||||
|
||||
// STATE: is-collapsed
|
||||
&.is-collapsed {
|
||||
|
||||
.section-content {
|
||||
@extend %outline-item-content-hidden;
|
||||
}
|
||||
}
|
||||
|
||||
// STATE: drag and drop - was dropped
|
||||
&.was-dropped {
|
||||
border-left-color: $ui-action-primary-color-focus;
|
||||
}
|
||||
}
|
||||
|
||||
// outline: subsections
|
||||
// --------------------
|
||||
.list-subsections {
|
||||
margin: $baseline 0 0 0;
|
||||
}
|
||||
|
||||
.outline-subsection {
|
||||
padding: ($baseline*0.75);
|
||||
|
||||
// header
|
||||
.subsection-header {
|
||||
@extend %outline-item-header;
|
||||
|
||||
.incontext-editor-input {
|
||||
@extend %t-title6;
|
||||
}
|
||||
}
|
||||
|
||||
.subsection-header-details {
|
||||
float: left;
|
||||
width: flex-grid(6, 9);
|
||||
|
||||
.icon, .wrapper-subsection-title {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: ($baseline/4);
|
||||
}
|
||||
|
||||
.wrapper-subsection-title {
|
||||
width: flex-grid(5, 6);
|
||||
margin-top: -($baseline/10);
|
||||
line-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.subsection-header-actions {
|
||||
float: right;
|
||||
width: flex-grid(3, 9);
|
||||
margin-top: -($baseline/4);
|
||||
text-align: right;
|
||||
|
||||
.actions-list {
|
||||
@extend %actions-list;
|
||||
@extend %t-action2;
|
||||
margin-right: ($baseline/2);
|
||||
}
|
||||
}
|
||||
|
||||
// status
|
||||
.subsection-status {
|
||||
margin: 0 0 0 $outline-indent-width;
|
||||
}
|
||||
|
||||
// content
|
||||
.subsection-content {
|
||||
@extend %outline-item-content-shown;
|
||||
}
|
||||
|
||||
// CASE: is-collapsible
|
||||
&.is-collapsible {
|
||||
@extend %ui-expand-collapse;
|
||||
|
||||
.ui-toggle-expansion {
|
||||
@extend %t-icon4;
|
||||
color: $gray-l3;
|
||||
}
|
||||
}
|
||||
|
||||
// STATE: is-collapsed
|
||||
&.is-collapsed {
|
||||
|
||||
.subsection-content {
|
||||
@extend %outline-item-content-hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// outline: units
|
||||
// --------------------
|
||||
.list-units {
|
||||
margin: $baseline 0 0 0;
|
||||
}
|
||||
|
||||
.outline-unit {
|
||||
@include transition(margin $tmg-f2 linear 0s); // needed for drag and drop transitions
|
||||
margin-left: $outline-indent-width;
|
||||
|
||||
// header
|
||||
.unit-header {
|
||||
@extend %outline-item-header;
|
||||
}
|
||||
|
||||
.unit-header-details {
|
||||
float: left;
|
||||
width: flex-grid(6, 9);
|
||||
margin-top: ($baseline/4);
|
||||
}
|
||||
|
||||
.unit-header-actions {
|
||||
float: right;
|
||||
width: flex-grid(3, 9);
|
||||
margin-top: -($baseline/10);
|
||||
text-align: right;
|
||||
|
||||
.actions-list {
|
||||
@extend %actions-list;
|
||||
@extend %t-action2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// UI: drag and drop: section
|
||||
// --------------------
|
||||
@@ -530,4 +594,147 @@
|
||||
bottom: -($baseline/2);
|
||||
}
|
||||
}
|
||||
|
||||
// outline: edit item settings
|
||||
.wrapper-modal-window-bulkpublish-section,
|
||||
.wrapper-modal-window-bulkpublish-subsection,
|
||||
.wrapper-modal-window-bulkpublish-unit,
|
||||
.course-outline-modal {
|
||||
|
||||
.list-fields {
|
||||
|
||||
.field {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-right: ($baseline/2);
|
||||
margin-bottom: ($baseline/4);
|
||||
|
||||
|
||||
// TODO: refactor the _forms.scss partial to allow for this area to inherit from it
|
||||
label, input, textarea {
|
||||
display: block;
|
||||
}
|
||||
|
||||
label {
|
||||
@extend %t-copy-sub1;
|
||||
@include transition(color $tmg-f3 ease-in-out 0s);
|
||||
margin: 0 0 ($baseline/4) 0;
|
||||
font-weight: 600;
|
||||
|
||||
&.is-focused {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
input, textarea {
|
||||
@extend %t-copy-base;
|
||||
@include transition(all $tmg-f2 ease-in-out 0s);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: ($baseline/2);
|
||||
|
||||
// CASE: long length
|
||||
&.long {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// CASE: short length
|
||||
&.short {
|
||||
width: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
// CASE: specific release + due times/dates
|
||||
.start-date,
|
||||
.start-time,
|
||||
.due-date,
|
||||
.due-time {
|
||||
width: ($baseline*7);
|
||||
}
|
||||
}
|
||||
|
||||
// CASE: select input
|
||||
.field-select {
|
||||
|
||||
.label, .input {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-right: ($baseline/2);
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.edit-settings-grading {
|
||||
|
||||
.grading-type {
|
||||
margin-bottom: $baseline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// outline: bulk publishing items
|
||||
.bulkpublish-section-modal,
|
||||
.bulkpublish-subsection-modal,
|
||||
.bulkpublish-unit-modal {
|
||||
|
||||
.modal-introduction {
|
||||
color: $gray-d2;
|
||||
}
|
||||
|
||||
.modal-section .outline-bulkpublish {
|
||||
max-height: ($baseline*20);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.outline-section,
|
||||
.outline-subsection {
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.outline-subsection {
|
||||
margin-bottom: $baseline;
|
||||
padding-right: ($baseline/4);
|
||||
}
|
||||
|
||||
.outline-subsection .subsection-title {
|
||||
@extend %t-title8;
|
||||
margin-bottom: ($baseline/4);
|
||||
font-weight: 600;
|
||||
color: $gray-l2;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.outline-unit .unit-title, .outline-unit .unit-status {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.outline-unit .unit-title {
|
||||
@extend %t-title7;
|
||||
color: $color-heading-base;
|
||||
}
|
||||
|
||||
.outline-unit .unit-status {
|
||||
@extend %t-copy-sub2;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// it is the only element there
|
||||
.bulkpublish-unit-modal {
|
||||
.modal-introduction {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -343,7 +343,9 @@
|
||||
<% online_help_token = self.online_help_token() if hasattr(self, 'online_help_token') else None %>
|
||||
<%include file="widgets/header.html" args="online_help_token=online_help_token" />
|
||||
|
||||
<div id="page-alert"></div>
|
||||
<div id="page-alert">
|
||||
<%block name="page_alert"></%block>
|
||||
</div>
|
||||
|
||||
<div id="content">
|
||||
<%block name="content"></%block>
|
||||
|
||||
@@ -144,7 +144,7 @@ templates = ["basic-modal", "modal-button", "edit-xblock-modal",
|
||||
</div>
|
||||
<div class="wrapper-unit-tree-location bar-mod-content">
|
||||
<h5 class="title">${_("Location in Course Outline")}</h5>
|
||||
<div class="wrapper-unit-overview">
|
||||
<div class="wrapper-unit-overview outline outline-simple">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
158
cms/templates/course-create-rerun.html
Normal file
158
cms/templates/course-create-rerun.html
Normal file
@@ -0,0 +1,158 @@
|
||||
<%inherit file="base.html" />
|
||||
<%def name="online_help_token()"><% return "course_rerun" %></%def>
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
|
||||
<%block name="title">${_("Create a Course Rerun of:")}</%block>
|
||||
<%block name="bodyclass">is-signedin view-course-create view-course-create-rerun</%block>
|
||||
|
||||
<%block name="jsextra">
|
||||
<script type="text/javascript">
|
||||
require(["domReady!", "jquery", "jquery.form", "js/views/course_rerun"], function(doc, $) {
|
||||
|
||||
});
|
||||
</script>
|
||||
<script type="text/javascript">
|
||||
var source_course_key = "${source_course_key}"
|
||||
</script>
|
||||
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
<div id="content">
|
||||
<div class="wrapper-mast wrapper">
|
||||
<header class="mast mast-wizard has-actions">
|
||||
<h1 class="page-header">
|
||||
<span class="page-header-sub">${_("Create a re-run of a course")}</span>
|
||||
</h1>
|
||||
|
||||
<nav class="nav-actions">
|
||||
<h3 class="sr">Page Actions</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a href="" class="button cancel-button">${_("Cancel")}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<h2 class="page-header-super course-original">
|
||||
<span class="sr">${_("You are creating a re-run from:")}</span>
|
||||
<span class="course-original-title-id">${source_course_key.org} ${source_course_key.course} ${source_course_key.run}</span>
|
||||
<span class="course-original-title">${display_name}</span>
|
||||
</h2>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-content wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<section class="content">
|
||||
<article class="content-primary">
|
||||
<div class="introduction">
|
||||
<div class="copy">
|
||||
<p>
|
||||
${_("Provide identifying information for this re-run of the course. The original course is not affected in any way by a re-run.")}
|
||||
<strong>${_("Note: Together, the organization, course number, and course run must uniquely identify this new course instance.")}</strong>
|
||||
<p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-rerun-course">
|
||||
<form class="form-create rerun-course course-info" id="rerun-course-form" name="rerun-course-form">
|
||||
<!-- NOTE: this element's contents should be only included when they are needed and not kept in the DOM for all states -->
|
||||
<div class="wrapper-error is-hidden">
|
||||
<div id="course_rerun_error" name="course_rerun_error" class="message message-status error" role="alert">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-form">
|
||||
<fieldset>
|
||||
<legend class="sr">${_("Required Information to Create a re-run of a course")}</legend>
|
||||
|
||||
<ol class="list-input">
|
||||
<li class="field text required" id="field-course-name">
|
||||
<label for="rerun-course-name">${_("Course Name")}</label>
|
||||
<input class="rerun-course-name" id="rerun-course-name" type="text" name="rerun-course-name" aria-required="true" value="${display_name}" placeholder="${_('e.g. Introduction to Computer Science')}" />
|
||||
<span class="tip">
|
||||
${_("The public display name for the new course. (This name is often the same as the original course name.)")}
|
||||
</span>
|
||||
<span class="tip tip-error is-hidden"></span>
|
||||
</li>
|
||||
<li class="field text required" id="field-organization">
|
||||
<label for="rerun-course-org">${_("Organization")}</label>
|
||||
<input class="rerun-course-org" id="rerun-course-org" type="text" name="rerun-course-org" aria-required="true" value="${source_course_key.org}" placeholder="${_('e.g. UniversityX or OrganizationX')}" />
|
||||
<span class="tip">
|
||||
${_("The name of the organization sponsoring the new course. (This name is often the same as the original organization name.)")}
|
||||
<strong class="tip-note" class="tip-note">${_("Note: No spaces or special characters are allowed.")}</strong>
|
||||
</span>
|
||||
<span class="tip tip-error is-hidden"></span>
|
||||
</li>
|
||||
|
||||
<li class="row">
|
||||
<div class="column field text required" id="field-course-number">
|
||||
<label for="rerun-course-number">${_("Course Number")}</label>
|
||||
<input class="rerun-course-number" id="rerun-course-number" type="text" name="rerun-course-number" aria-required="true" value="${source_course_key.course}" placeholder="${_('e.g. CS101')}" />
|
||||
<span class="tip">
|
||||
${_("The unique number that identifies the new course within the organization. (This number is often the same as the original course number.)")}
|
||||
<strong class="tip-note" class="tip-note">${_("Note: No spaces or special characters are allowed.")}</strong>
|
||||
</span>
|
||||
<span class="tip tip-error is-hidden"></span>
|
||||
</div>
|
||||
|
||||
<div class="column field text required" id="field-course-run">
|
||||
<label for="rerun-course-run">${_("Course Run")}</label>
|
||||
<input class="rerun-course-run" id="rerun-course-run" type="text" name="rerun-course-run" aria-required="true"placeholder="${_('e.g. 2014_T1')}" />
|
||||
<span class="tip">
|
||||
${_("The term in which the new course will run. (This value is often different than the original course run value.)")}
|
||||
<strong class="tip-note" class="tip-note">${_("Note: No spaces or special characters are allowed.")}</strong>
|
||||
</span>
|
||||
<span class="tip tip-error is-hidden"></span>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<input type="hidden" value="${allow_unicode_course_id}" class="allow-unicode-course-id" /> <!-- TODO: need to add support for allow_unicode_course_id in real view's template -->
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button type="submit" class="action action-primary rerun-course-save is-disabled">${_('Create Re-run')}</button>
|
||||
<button type="button" class="action action-secondary action-cancel rerun-course-cancel">${_('Cancel')}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</article>
|
||||
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<div class="bit">
|
||||
<h3 class="title-3">${_("When will my course re-run start?")}</h3>
|
||||
<ul class="list-details">
|
||||
<li class="item-detail">${_("The new course is set to start on January 1, 2030 at midnight (UTC).")}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="bit">
|
||||
<h3 class="title-3">${_("What transfers from the original course?")}</h3>
|
||||
<ul class="list-details">
|
||||
<li class="item-detail">${_("The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.")}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="bit">
|
||||
<h3 class="title-3">${_("What does not transfer from the original course?")}</h3>
|
||||
<ul class="list-details">
|
||||
<li class="item-detail">${_("You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.")}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="bit external-help">
|
||||
<a href="${get_online_help_info(online_help_token())['doc_url']}" target="_blank" class="button external-help-button">${_("Learn more about Course Re-runs")}</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
</section>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</%block>
|
||||
@@ -29,13 +29,38 @@ from contentstore.utils import reverse_usage_url
|
||||
|
||||
<%block name="header_extras">
|
||||
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
|
||||
% for template_name in ['course-outline', 'xblock-string-field-editor', 'basic-modal', 'modal-button', 'edit-outline-item-modal']:
|
||||
% for template_name in ['course-outline', 'xblock-string-field-editor', 'basic-modal', 'modal-button', 'course-outline-modal', 'due-date-editor', 'release-date-editor', 'grading-editor', 'publish-editor', 'staff-lock-editor']:
|
||||
<script type="text/template" id="${template_name}-tpl">
|
||||
<%static:include path="js/${template_name}.underscore" />
|
||||
</script>
|
||||
% endfor
|
||||
</%block>
|
||||
|
||||
<%block name="page_alert">
|
||||
%if notification_dismiss_url is not None:
|
||||
<div class="wrapper wrapper-alert wrapper-alert-announcement is-shown">
|
||||
<div class="alert announcement has-actions">
|
||||
<i class="alert-symbol icon-bullhorn"></i>
|
||||
|
||||
<div class="copy">
|
||||
<h2 class="title title-3">${_("This course was created as a re-run. Some manual configuration is needed.")}</h2>
|
||||
|
||||
<p>${_("No course content is currently visible, and no students are enrolled. Be sure to review and reset all dates, including the Course Start Date; set up the course team; review course updates and other assets for dated material; and seed the discussions and wiki.")}</p>
|
||||
</div>
|
||||
|
||||
<ul class="nav-actions">
|
||||
<li class="action action-dismiss">
|
||||
<a href="#" class="button dismiss-button" data-dismiss-link='${notification_dismiss_url}'>
|
||||
<i class="icon icon-remove-sign"></i>
|
||||
<span class="button-copy">${_("Dimiss")}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
%endif
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
<div class="wrapper-mast wrapper">
|
||||
<header class="mast has-actions has-subtitle">
|
||||
@@ -70,11 +95,28 @@ from contentstore.utils import reverse_usage_url
|
||||
<div class="wrapper-content wrapper">
|
||||
<section class="content">
|
||||
<article class="content-primary" role="main">
|
||||
<div class="course-status">
|
||||
<div class="status-release">
|
||||
<h2 class="status-release-label">${_("Course Start Date:")}</h2>
|
||||
<p class="status-release-value">${course_release_date}</p>
|
||||
|
||||
<ul class="status-actions">
|
||||
<li class="action-item action-edit">
|
||||
<a href="${settings_url}" class="edit-button action-button" data-tooltip="${_("Edit Start Date")}">
|
||||
<i class="icon-pencil"></i>
|
||||
<span class="action-button-text sr">${_("Edit Start Date")}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-dnd">
|
||||
<%
|
||||
course_locator = context_course.location
|
||||
%>
|
||||
<article class="outline outline-course" data-locator="${course_locator}" data-course-key="${course_locator.course_key}">
|
||||
<h2 class="sr">${_("Course Outline")}</h2>
|
||||
<article class="outline outline-complex outline-course" data-locator="${course_locator}" data-course-key="${course_locator.course_key}">
|
||||
</article>
|
||||
</div>
|
||||
<div class="ui-loading">
|
||||
@@ -83,13 +125,20 @@ from contentstore.utils import reverse_usage_url
|
||||
</article>
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<div class="bit">
|
||||
<h3 class="title-3">${_("What can I do on this page?")}</h3>
|
||||
<p>${_("You can create new sections and subsections, set the release date for sections, and create new units in existing subsections. You can set the assignment type for subsections that are to be graded, and you can open a subsection for further editing.")}</p>
|
||||
<h3 class="title-3">${_("Creating your course organization")}</h3>
|
||||
<p>${_("You add sections, subsections, and units directly in the outline.")}</p>
|
||||
<p>${_("Create a section, then add subsections and units. Open a unit to add course components.")}</p>
|
||||
<h3 class="title-3">${_("Reorganizing your course")}</h3>
|
||||
<p>${_("Drag sections, subsections, and units to new locations in the outline.")}</p>
|
||||
<h3 class="title-3">${_("Setting release dates and grading policies")}</h3>
|
||||
<p>${_("Select the Configure icon for a section or subsection to set its release date. When you configure a subsection, you can also set the grading policy and due date.")}</p>
|
||||
<h3 class="title-3">${_("Changing the content students see")}</h3>
|
||||
<p>${_("To publish draft content, select the Publish icon for a section, subsection, or unit.")}</p>
|
||||
<p>${_("To hide content from students, select the Configure icon for a section, subsection, or unit, then select {em_start}Hide from students{em_end}.").format(em_start='<strong>', em_end="</strong>")}</p>
|
||||
|
||||
<p>${_("In addition, you can drag and drop sections, subsections, and units to reorganize your course.")}</p>
|
||||
<p><a href="${get_online_help_info(online_help_token())['doc_url']}" target="_blank">${_("Learn More")}</a></p>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
</div>
|
||||
<footer></footer>
|
||||
</%block>
|
||||
|
||||
@@ -77,7 +77,7 @@ require(["domReady!", "jquery", "jquery.form", "js/index"], function(doc, $) {
|
||||
|
||||
% if course_creator_status=='granted':
|
||||
<div class="wrapper-create-element wrapper-create-course">
|
||||
<form class="create-course course-info" id="create-course-form" name="create-course-form">
|
||||
<form class="form-create create-course course-info" id="create-course-form" name="create-course-form">
|
||||
<div class="wrap-error">
|
||||
<div id="course_creation_error" name="course_creation_error" class="message message-status message-status error" role="alert">
|
||||
<p>${_("Please correct the highlighted fields below.")}</p>
|
||||
@@ -123,7 +123,7 @@ require(["domReady!", "jquery", "jquery.form", "js/index"], function(doc, $) {
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<input type="hidden" value="${ allow_unicode_course_id }" class="allow-unicode-course-id" />
|
||||
<input type="hidden" value="${allow_unicode_course_id}" class="allow-unicode-course-id" />
|
||||
<input type="submit" value="${_('Create')}" class="action action-primary new-course-save" />
|
||||
<input type="button" value="${_('Cancel')}" class="action action-secondary action-cancel new-course-cancel" />
|
||||
</div>
|
||||
@@ -131,35 +131,139 @@ require(["domReady!", "jquery", "jquery.form", "js/index"], function(doc, $) {
|
||||
</div>
|
||||
% endif
|
||||
|
||||
<!-- STATE: processing courses -->
|
||||
%if allow_course_reruns and rerun_creator_status and len(in_process_course_actions) > 0:
|
||||
<div class="courses courses-processing">
|
||||
<h3 class="title">Courses Being Processed</h3>
|
||||
|
||||
<ul class="list-courses">
|
||||
%for course_info in sorted(in_process_course_actions, key=lambda s: s['display_name'].lower() if s['display_name'] is not None else ''):
|
||||
<!-- STATE: re-run is processing -->
|
||||
%if course_info['is_in_progress']:
|
||||
<li class="wrapper-course has-status" data-course-key="${course_info['course_key']}">
|
||||
<div class="course-item course-rerun is-processing">
|
||||
<div class="course-details" href="#">
|
||||
<h3 class="course-title">${course_info['display_name']}</h3>
|
||||
|
||||
<div class="course-metadata">
|
||||
<span class="course-org metadata-item">
|
||||
<span class="label">${_("Organization:")}</span> <span class="value">${course_info['org']}</span>
|
||||
</span>
|
||||
<span class="course-num metadata-item">
|
||||
<span class="label">${_("Course Number:")}</span>
|
||||
<span class="value">${course_info['number']}</span>
|
||||
</span>
|
||||
<span class="course-run metadata-item">
|
||||
<span class="label">${_("Course Run:")}</span> <span class="value">${course_info['run']}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dl class="course-status">
|
||||
<dt class="label sr">This re-run processing status:</dt>
|
||||
<dd class="value">
|
||||
<i class="icon icon-refresh icon-spin"></i>
|
||||
<span class="copy">Configuring as re-run</span>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="status-message">
|
||||
<p class="copy">${_('The new course will be added to your course list in 5-10 minutes. Return to this page or <a href="#" class="action-reload">refresh it</a> to update the course list. The new course will need some manual configuration.')}</p>
|
||||
</div>
|
||||
</li>
|
||||
%endif
|
||||
|
||||
<!-- - - - -->
|
||||
|
||||
<!-- STATE: re-run has error -->
|
||||
%if course_info['is_failed']:
|
||||
<li class="wrapper-course has-status" data-course-key="${course_info['course_key']}">
|
||||
<div class="course-item course-rerun has-error">
|
||||
<div class="course-details" href="#">
|
||||
<h3 class="course-title">${course_info['display_name']}</h3>
|
||||
|
||||
<div class="course-metadata">
|
||||
<span class="course-org metadata-item">
|
||||
<span class="label">${_("Organization:")}</span> <span class="value">${course_info['org']}</span>
|
||||
</span>
|
||||
<span class="course-num metadata-item">
|
||||
<span class="label">${_("Course Number:")}</span>
|
||||
<span class="value">${course_info['number']}</span>
|
||||
</span>
|
||||
<span class="course-run metadata-item">
|
||||
<span class="label">${_("Course Run:")}</span> <span class="value">${course_info['run']}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dl class="course-status">
|
||||
<dt class="label sr">This re-run processing status:</dt>
|
||||
<dd class="value">
|
||||
<i class="icon icon-warning-sign"></i>
|
||||
<span class="copy">Configuration Error</span>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="status-message has-actions">
|
||||
<p class="copy">${_("A system error occurred while your course was being processed. Please go to the original course to try the re-run again, or contact your PM for assistance.")}</p>
|
||||
|
||||
<ul class="status-actions">
|
||||
<li class="action action-dismiss">
|
||||
<a href="#" class="button dismiss-button" data-dismiss-link="${course_info['dismiss_link']}">
|
||||
<i class="icon icon-remove-sign"></i>
|
||||
<span class="button-copy">${_("Dismiss")}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
%endif
|
||||
%endfor
|
||||
</ul>
|
||||
</div>
|
||||
%endif
|
||||
|
||||
%if len(courses) > 0:
|
||||
<div class="courses">
|
||||
<ul class="list-courses">
|
||||
%for course, url, lms_link, org, num, run in sorted(courses, key=lambda s: s[0].lower() if s[0] is not None else ''):
|
||||
<li class="course-item">
|
||||
<a class="course-link" href="${url}">
|
||||
<h3 class="course-title">${course}</h3>
|
||||
%for course_info in sorted(courses, key=lambda s: s['display_name'].lower() if s['display_name'] is not None else ''):
|
||||
<li class="course-item" data-course-key="${course_info['course_key']}">
|
||||
<a class="course-link" href="${course_info['url']}">
|
||||
<h3 class="course-title">${course_info['display_name']}</h3>
|
||||
|
||||
<div class="course-metadata">
|
||||
<span class="course-org metadata-item">
|
||||
<span class="label">${_("Organization:")}</span> <span class="value">${org}</span>
|
||||
<span class="label">${_("Organization:")}</span> <span class="value">${course_info['org']}</span>
|
||||
</span>
|
||||
<span class="course-num metadata-item">
|
||||
<span class="label">${_("Course Number:")}</span>
|
||||
<span class="value">${num}</span>
|
||||
<span class="value">${course_info['number']}</span>
|
||||
</span>
|
||||
<span class="course-run metadata-item">
|
||||
<span class="label">${_("Course Run:")}</span> <span class="value">${run}</span>
|
||||
<span class="label">${_("Course Run:")}</span> <span class="value">${course_info['run']}</span>
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<ul class="item-actions course-actions">
|
||||
<li class="action">
|
||||
<a href="${lms_link}" rel="external" class="button view-button view-live-button">${_("View Live")}</a>
|
||||
% if allow_course_reruns and rerun_creator_status and course_creator_status=='granted':
|
||||
<li class="action action-rerun">
|
||||
<a href="${course_info['rerun_link']}" class="button rerun-button">${_("Re-run Course")}</a>
|
||||
</li>
|
||||
% endif
|
||||
<li class="action action-view">
|
||||
<a href="${course_info['lms_link']}" rel="external" class="button view-button">${_("View Live")}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
%endfor
|
||||
% if allow_course_reruns and rerun_creator_status and course_creator_status=='granted':
|
||||
<script type="text/javascript">
|
||||
$('.course-item').addClass('can-rerun');
|
||||
</script>
|
||||
% endif
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
7
cms/templates/js/course-outline-modal.underscore
Normal file
7
cms/templates/js/course-outline-modal.underscore
Normal file
@@ -0,0 +1,7 @@
|
||||
<div class="xblock-editor" data-locator="<%= xblockInfo.get('id') %>" data-course-key="<%= xblockInfo.get('courseKey') %>">
|
||||
<div class="message modal-introduction">
|
||||
<p><%= introductionMessage %></p>
|
||||
</div>
|
||||
<div class="modal-section"></div>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
<%
|
||||
var category = xblockInfo.get('category');
|
||||
var releasedToStudents = xblockInfo.get('released_to_students');
|
||||
var visibilityState = xblockInfo.get('visibility_state');
|
||||
var published = xblockInfo.get('published');
|
||||
|
||||
var statusMessage = null;
|
||||
var statusType = null;
|
||||
if (visibilityState === 'staff_only') {
|
||||
if (staffOnlyMessage) {
|
||||
statusType = 'staff-only';
|
||||
statusMessage = gettext('Contains staff only content');
|
||||
} else if (visibilityState === 'needs_attention') {
|
||||
if (category === 'vertical') {
|
||||
if (xblockInfo.isVertical()) {
|
||||
statusType = 'warning';
|
||||
if (published && releasedToStudents) {
|
||||
statusMessage = gettext('Unpublished changes to live content');
|
||||
@@ -63,6 +62,14 @@ if (xblockInfo.get('graded')) {
|
||||
|
||||
<div class="<%= xblockType %>-header-actions">
|
||||
<ul class="actions-list">
|
||||
<% if (xblockInfo.isPublishable()) { %>
|
||||
<li class="action-item action-publish">
|
||||
<a href="#" data-tooltip="<%= gettext('Publish') %>" class="publish-button action-button">
|
||||
<i class="icon icon-upload-alt"></i>
|
||||
<span class="sr action-button-text"><%= gettext('Publish') %></span>
|
||||
</a>
|
||||
</li>
|
||||
<% } %>
|
||||
<% if (xblockInfo.isEditableOnCourseOutline()) { %>
|
||||
<li class="action-item action-configure">
|
||||
<a href="#" data-tooltip="<%= gettext('Configure') %>" class="configure-button action-button">
|
||||
@@ -141,7 +148,7 @@ if (xblockInfo.get('graded')) {
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<% } else if (category !== 'vertical') { %>
|
||||
<% } else if (!xblockInfo.isVertical()) { %>
|
||||
<div class="outline-content <%= xblockType %>-content">
|
||||
<ol class="<%= typeListClass %> is-sortable">
|
||||
<li class="ui-splint ui-splint-indicator">
|
||||
|
||||
22
cms/templates/js/due-date-editor.underscore
Normal file
22
cms/templates/js/due-date-editor.underscore
Normal file
@@ -0,0 +1,22 @@
|
||||
<ul class="list-fields list-input datepair date-setter">
|
||||
<li class="field field-text field-due-date">
|
||||
<label for="due_date"><%= gettext('Due Date:') %></label>
|
||||
<input type="text" id="due_date" name="due_date" value=""
|
||||
placeholder="MM/DD/YYYY" class="due-date date input input-text" autocomplete="off"/>
|
||||
</li>
|
||||
|
||||
<li class="field field-text field-due-time">
|
||||
<label for="due_time"><%= gettext('Due Time in UTC:') %></label>
|
||||
<input type="text" id="due_time" name="due_time" value=""
|
||||
placeholder="HH:MM" class="due-time time input input-text" autocomplete="off" />
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="list-actions">
|
||||
<li class="action-item">
|
||||
<a href="#" data-tooltip="<%= gettext('Clear Grading Due Date') %>" class="clear-date action-button action-clear">
|
||||
<i class="icon-undo"></i>
|
||||
<span class="sr"><%= gettext('Clear Grading Due Date') %></span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -1,89 +0,0 @@
|
||||
<div class="xblock-editor" data-locator="<%= xblockInfo.get('id') %>" data-course-key="<%= xblockInfo.get('courseKey') %>">
|
||||
<div class="message modal-introduction">
|
||||
<% if (xblockInfo.isChapter() || xblockInfo.isSequential()) { %>
|
||||
<p>
|
||||
<%= interpolate(gettext("Change the settings for %(display_name)s"), {display_name: xblockInfo.get('display_name')}, true) %>
|
||||
</p>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<form class="edit-settings-form" action="#">
|
||||
<div class="modal-section edit-settings-release scheduled-date-input">
|
||||
<h3 class="modal-section-title"><%= gettext('Release Date and Time') %></h3>
|
||||
<div class="modal-section-content has-actions">
|
||||
<ul class="list-fields list-input datepair">
|
||||
<li class="field field-text field-start-date field-release-date">
|
||||
<label for="start_date" class="label"><%= gettext('Release Date:') %></label>
|
||||
<input type="text" id="start_date" name="start_date"
|
||||
value=""
|
||||
placeholder="MM/DD/YYYY" class="start-date release-date date input input-text" autocomplete="off" />
|
||||
</li>
|
||||
<li class="field field-text field-start-time field-release-time">
|
||||
<label for="start_time" class="label"><%= gettext('Release Time in UTC:') %></label>
|
||||
<input type="text" id="start_time" name="start_time"
|
||||
value=""
|
||||
placeholder="HH:MM" class="start-time release-time time input input-text" autocomplete="off" />
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<% if (xblockInfo.isSequential()) { %>
|
||||
<ul class="list-actions">
|
||||
<li class="action-item">
|
||||
<a href="#" data-tooltip="<%= gettext('Clear Release Date/Time') %>" class="clear-date action-button action-clear">
|
||||
<i class="icon-undo"></i>
|
||||
<span class="sr"><%= gettext('Clear Release Date/Time') %></span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if (xblockInfo.isSequential()) { %>
|
||||
<div class="modal-section edit-settings-grading">
|
||||
<h3 class="modal-section-title"><%= gettext('Grading') %></h3>
|
||||
|
||||
<div class="modal-section-content grading-type">
|
||||
<ul class="list-fields list-input">
|
||||
<li class="field field-grading-type field-select">
|
||||
<label for="grading_type" class="label"><%= gettext('Grade as:') %></label>
|
||||
<select class="input" id="grading_type">
|
||||
<option value="notgraded"><%= gettext('Not Graded') %></option>
|
||||
<% _.each(graderTypes, function(grader) { %>
|
||||
<option value="<%= grader %>"><%= grader %></option>
|
||||
<% }); %>
|
||||
</select>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="modal-section-content has-actions due-date-input grading-due-date">
|
||||
<ul class="list-fields list-input datepair date-setter">
|
||||
<li class="field field-text field-due-date">
|
||||
<label for="due_date"><%= gettext('Due Date:') %></label>
|
||||
<input type="text" id="due_date" name="due_date"
|
||||
value=""
|
||||
placeholder="MM/DD/YYYY" class="due-date date input input-text" autocomplete="off"/>
|
||||
</li>
|
||||
|
||||
<li class="field field-text field-due-time">
|
||||
<label for="due_time"><%= gettext('Due Time in UTC:') %></label>
|
||||
<input type="text" id="due_time" name="due_time"
|
||||
value=""
|
||||
placeholder="HH:MM" class="due-time time input input-text" autocomplete="off" />
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="list-actions">
|
||||
<li class="action-item">
|
||||
<a href="#" data-tooltip="<%= gettext('Clear Grading Due Date') %>" class="clear-date action-button action-clear">
|
||||
<i class="icon-undo"></i>
|
||||
<span class="sr"><%= gettext('Clear Grading Due Date') %></span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
</form>
|
||||
</div>
|
||||
14
cms/templates/js/grading-editor.underscore
Normal file
14
cms/templates/js/grading-editor.underscore
Normal file
@@ -0,0 +1,14 @@
|
||||
<h3 class="modal-section-title"><%= gettext('Grading') %></h3>
|
||||
<div class="modal-section-content grading-type">
|
||||
<ul class="list-fields list-input">
|
||||
<li class="field field-grading-type field-select">
|
||||
<label for="grading_type" class="label"><%= gettext('Grade as:') %></label>
|
||||
<select class="input" id="grading_type">
|
||||
<option value="notgraded"><%= gettext('Not Graded') %></option>
|
||||
<% _.each(graderTypes, function(grader) { %>
|
||||
<option value="<%= grader %>"><%= grader %></option>
|
||||
<% }); %>
|
||||
</select>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -0,0 +1,54 @@
|
||||
<header class="xblock-header">
|
||||
<div class="xblock-header-primary">
|
||||
<div class="header-details">
|
||||
<span class="xblock-display-name">Test Container</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<ul class="actions-list">
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<article class="xblock-render">
|
||||
<div class="xblock" data-locator="locator-container"
|
||||
data-init="MockXBlock" data-runtime-class="StudioRuntime" data-runtime-version="1">
|
||||
<ol class="reorderable-container">
|
||||
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-broken-javascript">
|
||||
<section class="wrapper-xblock level-element">
|
||||
<header class="xblock-header">
|
||||
<div class="xblock-header-primary">
|
||||
<div class="header-actions">
|
||||
<ul class="actions-list">
|
||||
<li class="action-item action-edit">
|
||||
<a href="#" class="edit-button action-button"></a>
|
||||
</li>
|
||||
<li class="action-item action-duplicate">
|
||||
<a href="#" class="duplicate-button action-button"></a>
|
||||
</li>
|
||||
<li class="action-item action-delete">
|
||||
<a href="#" class="delete-button action-button"></a>
|
||||
</li>
|
||||
<li class="action-item action-drag">
|
||||
<span data-tooltip="Drag to reorder" class="drag-handle action"></span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<article class="xblock-render">
|
||||
<div class="xblock xblock-student_view xmodule_display xmodule_HtmlModule"
|
||||
data-runtime-class="PreviewRuntime" data-init="XBlockToXModuleShim"
|
||||
data-request-token="5efb4488272611e48053080027880ca6" data-runtime-version="1"
|
||||
data-usage-id="i4x:;_;_edX;_mock"
|
||||
data-type="HTMLModule" data-block-type="html">
|
||||
<script type="text/javascript">
|
||||
noSuchVariable.noSuchFunction();
|
||||
</script>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</article>
|
||||
@@ -0,0 +1,51 @@
|
||||
<header class="xblock-header">
|
||||
<div class="xblock-header-primary">
|
||||
<div class="header-details">
|
||||
<span class="xblock-display-name">Test Container</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<ul class="actions-list">
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<article class="xblock-render">
|
||||
<div class="xblock" data-locator="locator-container"
|
||||
data-init="MockXBlock" data-runtime-class="StudioRuntime" data-runtime-version="1">
|
||||
<ol class="reorderable-container">
|
||||
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-broken-javascript">
|
||||
<section class="wrapper-xblock level-element">
|
||||
<header class="xblock-header">
|
||||
<div class="xblock-header-primary">
|
||||
<div class="header-actions">
|
||||
<ul class="actions-list">
|
||||
<li class="action-item action-edit">
|
||||
<a href="#" class="edit-button action-button"></a>
|
||||
</li>
|
||||
<li class="action-item action-duplicate">
|
||||
<a href="#" class="duplicate-button action-button"></a>
|
||||
</li>
|
||||
<li class="action-item action-delete">
|
||||
<a href="#" class="delete-button action-button"></a>
|
||||
</li>
|
||||
<li class="action-item action-drag">
|
||||
<span data-tooltip="Drag to reorder" class="drag-handle action"></span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<article class="xblock-render">
|
||||
<div class="xblock xblock-student_view xmodule_display xmodule_HtmlModule"
|
||||
data-runtime-class="InvalidRuntime" data-init="XBlockToXModuleShim"
|
||||
data-request-token="5efb4488272611e48053080027880ca6" data-runtime-version="1"
|
||||
data-usage-id="i4x:;_;_edX;_mock"
|
||||
data-type="HTMLModule" data-block-type="html">
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</article>
|
||||
@@ -10,11 +10,11 @@
|
||||
</div>
|
||||
</header>
|
||||
<article class="xblock-render">
|
||||
<div class="xblock" data-locator="locator-container"
|
||||
<div class="xblock" data-locator="locator-container" data-request-token="page-render-token"
|
||||
data-init="MockXBlock" data-runtime-class="StudioRuntime" data-runtime-version="1">
|
||||
<ol class="reorderable-container">
|
||||
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-group-A">
|
||||
<section class="wrapper-xblock level-nesting" data-locator="locator-group-A">
|
||||
<section class="wrapper-xblock level-nesting">
|
||||
<header class="xblock-header">
|
||||
<div class="xblock-header-primary">
|
||||
<div class="header-details">
|
||||
@@ -35,11 +35,10 @@
|
||||
</header>
|
||||
|
||||
<article class="xblock-render">
|
||||
<div class="xblock">
|
||||
<div class="xblock" data-request-token="page-render-token">
|
||||
<ol class="reorderable-container">
|
||||
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-A1">
|
||||
<section class="wrapper-xblock level-element"
|
||||
data-locator="locator-component-A1">
|
||||
<section class="wrapper-xblock level-element">
|
||||
<header class="xblock-header">
|
||||
<div class="xblock-header-primary">
|
||||
<div class="header-actions">
|
||||
@@ -64,9 +63,7 @@
|
||||
</section>
|
||||
</li>
|
||||
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-A2">
|
||||
<section class="wrapper-xblock level-element"
|
||||
data-locator="locator-component-A2">
|
||||
|
||||
<section class="wrapper-xblock level-element">
|
||||
<header class="xblock-header">
|
||||
<div class="header-actions">
|
||||
<div class="xblock-header-primary">
|
||||
@@ -91,8 +88,7 @@
|
||||
</section>
|
||||
</li>
|
||||
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-A3">
|
||||
<section class="wrapper-xblock level-element"
|
||||
data-locator="locator-component-A3">
|
||||
<section class="wrapper-xblock level-element">
|
||||
<header class="xblock-header">
|
||||
<div class="xblock-header-primary">
|
||||
<div class="header-actions">
|
||||
@@ -123,7 +119,7 @@
|
||||
</section>
|
||||
</li>
|
||||
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-group-B">
|
||||
<section class="wrapper-xblock level-nesting" data-locator="locator-group-B">
|
||||
<section class="wrapper-xblock level-nesting">
|
||||
<header class="xblock-header">
|
||||
<div class="xblock-header-primary">
|
||||
<div class="header-details">
|
||||
@@ -144,12 +140,10 @@
|
||||
</header>
|
||||
|
||||
<article class="xblock-render">
|
||||
<div class="xblock">
|
||||
<div class="xblock" data-request-token="page-render-token">
|
||||
<ol class="reorderable-container">
|
||||
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-B1">
|
||||
<section class="wrapper-xblock level-element"
|
||||
data-locator="locator-component-B1">
|
||||
|
||||
<section class="wrapper-xblock level-element">
|
||||
<header class="xblock-header">
|
||||
<div class="xblock-header-primary">
|
||||
<div class="header-actions">
|
||||
@@ -174,9 +168,7 @@
|
||||
</section>
|
||||
</li>
|
||||
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-B2">
|
||||
<section class="wrapper-xblock level-element"
|
||||
data-locator="locator-component-B2">
|
||||
|
||||
<section class="wrapper-xblock level-element">
|
||||
<header class="xblock-header">
|
||||
<div class="xblock-header-primary">
|
||||
<div class="header-actions">
|
||||
@@ -201,9 +193,7 @@
|
||||
</section>
|
||||
</li>
|
||||
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-B3">
|
||||
<section class="wrapper-xblock level-element"
|
||||
data-locator="locator-component-B3">
|
||||
|
||||
<section class="wrapper-xblock level-element">
|
||||
<header class="xblock-header">
|
||||
<div class="xblock-header-primary">
|
||||
<div class="header-actions">
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<div id="page-alert">
|
||||
<div class="wrapper wrapper-alert wrapper-alert-announcement is-shown">
|
||||
<div class="alert announcement has-actions">
|
||||
<i class="alert-symbol icon-bullhorn"></i>
|
||||
|
||||
<div class="copy">
|
||||
<h2 class="title title-3">This course was created as a re-run. Some manual configuration is needed.</h2>
|
||||
|
||||
<p>No course content is currently visible, and no students are enrolled. Be sure to review and reset all
|
||||
dates, including the Course Start Date; set up the course team; review course updates and other
|
||||
assets for dated material; and seed the discussions and wiki.</p>
|
||||
</div>
|
||||
|
||||
<ul class="nav-actions">
|
||||
<li class="action action-dismiss">
|
||||
<a href="#" class="button dismiss-button" data-dismiss-link="dummy_dismiss_url">
|
||||
<i class="icon icon-remove-sign"></i>
|
||||
<span class="button-copy">Dimiss</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
116
cms/templates/js/mock/mock-create-course-rerun.underscore
Normal file
116
cms/templates/js/mock/mock-create-course-rerun.underscore
Normal file
@@ -0,0 +1,116 @@
|
||||
<div id="content">
|
||||
<div class="wrapper-mast wrapper">
|
||||
<header class="mast mast-wizard has-actions">
|
||||
<h1 class="page-header">
|
||||
<span class="page-header-sub">Create a re-run of a course</span>
|
||||
</h1>
|
||||
|
||||
<nav class="nav-actions">
|
||||
<h3 class="sr">Page Actions</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a href="" class="button cancel-button">Cancel</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<h2 class="page-header-super course-original">
|
||||
<span class="sr">You are creating a re-run from:</span>
|
||||
<span class="course-original-title-id">edX Open_DemoX 2014_T1</span>
|
||||
<span class="course-original-title">edX Demonstration Course</span>
|
||||
</h2>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-content wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<section class="content">
|
||||
<article class="content-primary">
|
||||
<div class="introduction">
|
||||
<div class="copy">
|
||||
<p>
|
||||
Provide identifying information for this re-run of the course. The original course is not affected in any way by a re-run.
|
||||
<strong>Note: Together, the organization, course number, and course run must uniquely identify this new course instance.</strong>
|
||||
<p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-rerun-course">
|
||||
<form class="form-create rerun-course course-info" id="rerun-course-form"
|
||||
name="rerun-course-form">
|
||||
<div class="wrapper-error is-hidden">
|
||||
<div id="course_rerun_error" name="course_rerun_error"
|
||||
class="message message-status error" role="alert">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-form">
|
||||
<fieldset>
|
||||
<legend class="sr">Required Information to Create a re-run of a course</legend>
|
||||
|
||||
<ol class="list-input">
|
||||
<li class="field text required" id="field-course-name">
|
||||
<label for="rerun-course-name">Course Name</label>
|
||||
<input class="rerun-course-name" id="rerun-course-name" type="text"
|
||||
name="rerun-course-name" aria-required="true" value="edX Demonstration Course"
|
||||
placeholder="e.g. Introduction to Computer Science"/>
|
||||
<span class="tip">
|
||||
The public display name for the new course. (This name is often the same as the original course name.)
|
||||
</span>
|
||||
<span class="tip tip-error is-hidden"></span>
|
||||
</li>
|
||||
<li class="field text required" id="field-organization">
|
||||
<label for="rerun-course-org">Organization</label>
|
||||
<input class="rerun-course-org" id="rerun-course-org" type="text"
|
||||
name="rerun-course-org" aria-required="true"
|
||||
value="edX"
|
||||
placeholder="e.g. UniversityX or OrganizationX"/>
|
||||
<span class="tip">
|
||||
The name of the organization sponsoring the new course. (This name is often the same as the original organization name.)
|
||||
<strong class="tip-note" class="tip-note">Note: No spaces or special characters are allowed.</strong>
|
||||
</span>
|
||||
<span class="tip tip-error is-hidden"></span>
|
||||
</li>
|
||||
|
||||
<li class="row">
|
||||
<div class="column field text required" id="field-course-number">
|
||||
<label for="rerun-course-number">Course Number</label>
|
||||
<input class="rerun-course-number" id="rerun-course-number" type="text"
|
||||
name="rerun-course-number" aria-required="true"
|
||||
value="Open_DemoX" placeholder="e.g. CS101"/>
|
||||
<span class="tip">
|
||||
The unique number that identifies the new course within the organization. (This number is often the same as the original course number.)
|
||||
<strong class="tip-note" class="tip-note">Note: No spaces or special characters are allowed.</strong>
|
||||
</span>
|
||||
<span class="tip tip-error is-hidden"></span>
|
||||
</div>
|
||||
|
||||
<div class="column field text required" id="field-course-run">
|
||||
<label for="rerun-course-run">Course Run</label>
|
||||
<input class="rerun-course-run" id="rerun-course-run" type="text"
|
||||
name="rerun-course-run" aria-required="true"
|
||||
placeholder="e.g. 2014_T1"/>
|
||||
<span class="tip">
|
||||
The term in which the new course will run. (This value is often different than the original course run value.)
|
||||
<strong class="tip-note" class="tip-note">Note: No spaces or special characters are allowed.</strong>
|
||||
</span>
|
||||
<span class="tip tip-error is-hidden"></span>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<input type="hidden" value="false" class="allow-unicode-course-id"/>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button type="submit" class="action action-primary rerun-course-save is-disabled">Create Re-run</button>
|
||||
<button type="button" class="action action-secondary action-cancel rerun-course-cancel">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
168
cms/templates/js/mock/mock-index-page.underscore
Normal file
168
cms/templates/js/mock/mock-index-page.underscore
Normal file
@@ -0,0 +1,168 @@
|
||||
<div class="wrapper-mast wrapper">
|
||||
<header class="mast has-actions">
|
||||
<h1 class="page-header">My Courses</h1>
|
||||
<nav class="nav-actions">
|
||||
<h3 class="sr">Page Actions</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="button new-button new-course-button"><i class="icon-plus icon-inline"></i>
|
||||
New Course</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-content wrapper">
|
||||
<section class="content">
|
||||
<article class="content-primary" role="main">
|
||||
|
||||
<div class="introduction">
|
||||
<h2 class="title">Welcome, user!</h2>
|
||||
<div class="copy">
|
||||
<p>Here are all of the courses you currently have access to in Studio:</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-create-element wrapper-create-course">
|
||||
<form class="form-create create-course course-info" id="create-course-form" name="create-course-form">
|
||||
<div class="wrap-error">
|
||||
<div id="course_creation_error" name="course_creation_error" class="message message-status message-status error" role="alert">
|
||||
<p>Please correct the highlighted fields below.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-form">
|
||||
<h3 class="title">Create a New Course</h3>
|
||||
|
||||
<fieldset>
|
||||
<legend class="sr">Required Information to Create a New Course</legend>
|
||||
|
||||
<ol class="list-input">
|
||||
<li class="field text required" id="field-course-name">
|
||||
<label for="new-course-name">Course Name</label>
|
||||
<input class="new-course-name" id="new-course-name" type="text" name="new-course-name" aria-required="true" placeholder="e.g. Introduction to Computer Science" />
|
||||
<span class="tip">The public display name for your course. This cannot be changed, but you can set a different display name in Advanced Settings later.</span>
|
||||
<span class="tip tip-error is-hiding"></span>
|
||||
</li>
|
||||
<li class="field text required" id="field-organization">
|
||||
<label for="new-course-org">Organization</label>
|
||||
<input class="new-course-org" id="new-course-org" type="text" name="new-course-org" aria-required="true" placeholder="e.g. UniversityX or OrganizationX" />
|
||||
<span class="tip">The name of the organization sponsoring the course. <strong>Note: This is part of your course URL, so no spaces or special characters are allowed.</strong> This cannot be changed, but you can set a different display name in Advanced Settings later.</span>
|
||||
<span class="tip tip-error is-hiding"></span>
|
||||
</li>
|
||||
|
||||
<li class="field text required" id="field-course-number">
|
||||
<label for="new-course-number">Course Number</label>
|
||||
<input class="new-course-number" id="new-course-number" type="text" name="new-course-number" aria-required="true" placeholder="e.g. CS101" />
|
||||
<span class="tip">The unique number that identifies your course within your organization. <strong>Note: This is part of your course URL, so no spaces or special characters are allowed and it cannot be changed.</strong></span>
|
||||
<span class="tip tip-error is-hiding"></span>
|
||||
</li>
|
||||
|
||||
<li class="field text required" id="field-course-run">
|
||||
<label for="new-course-run">Course Run</label>
|
||||
<input class="new-course-run" id="new-course-run" type="text" name="new-course-run" aria-required="true"placeholder="e.g. 2014_T1" />
|
||||
<span class="tip">The term in which your course will run. <strong>Note: This is part of your course URL, so no spaces or special characters are allowed and it cannot be changed.</strong></span>
|
||||
<span class="tip tip-error is-hiding"></span>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<input type="hidden" value="true" class="allow-unicode-course-id" />
|
||||
<input type="submit" value="Create" class="action action-primary new-course-save" />
|
||||
<input type="button" value="Cancel" class="action action-secondary action-cancel new-course-cancel" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- STATE: processing courses -->
|
||||
<div class="courses courses-processing">
|
||||
<h3 class="title">Courses Being Processed</h3>
|
||||
|
||||
<ul class="list-courses">
|
||||
<!-- STATE: re-run is processing -->
|
||||
<li class="wrapper-course has-status" data-test-unsucceeded="edX/DM101/2014">
|
||||
<div class="course-item course-rerun is-processing">
|
||||
<div class="course-details" href="#">
|
||||
<h3 class="course-title">Demo Course</h3>
|
||||
|
||||
<div class="course-metadata">
|
||||
<span class="course-org metadata-item">
|
||||
<span class="label">Organization:</span> <span class="value">edX</span>
|
||||
</span>
|
||||
<span class="course-num metadata-item">
|
||||
<span class="label">Course Number:</span>
|
||||
<span class="value">DM101</span>
|
||||
</span>
|
||||
<span class="course-run metadata-item">
|
||||
<span class="label">Course Run:</span> <span class="value">2014</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dl class="course-status">
|
||||
<dt class="label sr">This re-run processing status:</dt>
|
||||
<dd class="value">
|
||||
<i class="icon icon-refresh icon-spin"></i>
|
||||
<span class="copy">Configuring as re-run</span>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="status-message">
|
||||
<p class="copy">The new course will be added to your course list in 5-10 minutes. Return to this page or refresh it to update the course list. The new course will need some manual configuration.</p>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<!-- - - - -->
|
||||
|
||||
<!-- STATE: re-run has error -->
|
||||
<li class="wrapper-course has-status" data-test-unsucceeded="edX/DM102/2014">
|
||||
<div class="course-item course-rerun has-error">
|
||||
<div class="course-details" href="#">
|
||||
<h3 class="course-title">Demo Course 2</h3>
|
||||
|
||||
<div class="course-metadata">
|
||||
<span class="course-org metadata-item">
|
||||
<span class="label">Organization:</span> <span class="value">edX</span>
|
||||
</span>
|
||||
<span class="course-num metadata-item">
|
||||
<span class="label">Course Number:</span>
|
||||
<span class="value">DM102</span>
|
||||
</span>
|
||||
<span class="course-run metadata-item">
|
||||
<span class="label">Course Run:</span> <span class="value">2014</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dl class="course-status">
|
||||
<dt class="label sr">This re-run processing status:</dt>
|
||||
<dd class="value">
|
||||
<i class="icon icon-warning-sign"></i>
|
||||
<span class="copy">Configuration Error</span>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="status-message has-actions">
|
||||
<p class="copy">A system error occurred while your course was being processed. Please go to the original course to try the re-run again, or contact your PM for assistance.</p>
|
||||
|
||||
<ul class="status-actions">
|
||||
<li class="action action-dismiss">
|
||||
<a href="#" class="button dismiss-button" data-dismiss-link="dummy_dismiss_url">
|
||||
<i class="icon icon-remove-sign"></i>
|
||||
<span class="button-copy">Dismiss</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
</div>
|
||||
@@ -1,11 +1,17 @@
|
||||
<div class="xblock xblock-studio_view xmodule_edit xmodule_WrapperDescriptor" data-runtime-class="StudioRuntime" data-init="XBlockToXModuleShim" data-runtime-version="1" data-usage-id="i4x:;_;_AndyA;_ABT101;_wrapper;_wrapper_l1_poll" data-type="VerticalDescriptor" tabindex="0">
|
||||
<div class="xblock xblock-studio_view xmodule_edit xmodule_WrapperDescriptor" data-runtime-class="StudioRuntime"
|
||||
data-init="XBlockToXModuleShim" data-runtime-version="1" data-usage-id="i4x:;_;_edX;_mock"
|
||||
data-type="VerticalDescriptor" tabindex="0">
|
||||
<div class="wrapper-comp-editor is-active" id="editor-tab" data-base-asset-url="/c4x/AndyA/ABT101/asset/">
|
||||
<section class="editor-with-tabs">
|
||||
<div class="edit-header">
|
||||
<span class="component-name"></span>
|
||||
<ul class="editor-tabs">
|
||||
<li class="inner_tab_wrap"><a href="#tab-i4x-testCourse-video-84c6bf5dc2a24bc7996771eb7a1a4ad1-0" class="tab current">Basic</a></li>
|
||||
<li class="inner_tab_wrap"><a href="#tab-i4x-testCourse-video-84c6bf5dc2a24bc7996771eb7a1a4ad1-1" class="tab">Advanced</a></li>
|
||||
<li class="inner_tab_wrap">
|
||||
<a href="#tab-i4x-testCourse-video-84c6bf5dc2a24bc7996771eb7a1a4ad1-0" class="tab current">Basic</a>
|
||||
</li>
|
||||
<li class="inner_tab_wrap">
|
||||
<a href="#tab-i4x-testCourse-video-84c6bf5dc2a24bc7996771eb7a1a4ad1-1" class="tab">Advanced</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tabs-wrapper">
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<div class="xblock xblock-studio_view xmodule_edit xmodule_WrapperDescriptor" data-runtime-class="StudioRuntime" data-init="XBlockToXModuleShim" data-runtime-version="1" data-usage-id="i4x:;_;_AndyA;_ABT101;_wrapper;_wrapper_l1_poll" data-type="MockDescriptor" tabindex="0">
|
||||
<div class="xblock xblock-studio_view xmodule_edit xmodule_WrapperDescriptor" data-runtime-class="StudioRuntime"
|
||||
data-init="XBlockToXModuleShim" data-runtime-version="1" data-usage-id="i4x:;_;_edX;_mock"
|
||||
data-type="MockDescriptor" tabindex="0">
|
||||
<div class="wrapper-comp-editor is-active" id="editor-tab" data-base-asset-url="/c4x/AndyA/ABT101/asset/">
|
||||
</div>
|
||||
<section class="sequence-edit">
|
||||
@@ -24,7 +26,8 @@
|
||||
|
||||
</script>
|
||||
|
||||
<div class="wrapper-comp-settings metadata_edit" id="settings-tab" data-metadata='{"display_name": {"default_value": null, "explicitly_set": true, "display_name": "Display Name", "help": "This name appears in the horizontal navigation at the top of the page.", "type": "Generic", "value": "Poll Question", "field_name": "display_name", "options": []}, "due": {"default_value": null, "explicitly_set": false, "display_name": "due", "help": "Date that this problem is due by", "type": "Generic", "value": null, "field_name": "due", "options": []}}'/>
|
||||
<div class="wrapper-comp-settings metadata_edit" id="settings-tab"
|
||||
data-metadata='{"display_name": {"default_value": null, "explicitly_set": true, "display_name": "Display Name", "help": "This name appears in the horizontal navigation at the top of the page.", "type": "Generic", "value": "Poll Question", "field_name": "display_name", "options": []}, "due": {"default_value": null, "explicitly_set": false, "display_name": "due", "help": "Date that this problem is due by", "type": "Generic", "value": null, "field_name": "due", "options": []}}'/>
|
||||
<textarea data-metadata-name="custom_field">Custom Value</textarea>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
38
cms/templates/js/publish-editor.underscore
Normal file
38
cms/templates/js/publish-editor.underscore
Normal file
@@ -0,0 +1,38 @@
|
||||
<% if (!xblockInfo.isVertical()) { %>
|
||||
<div class="modal-section-content">
|
||||
<div class="outline outline-simple outline-bulkpublish">
|
||||
<% if (xblockInfo.isChapter()) { %>
|
||||
<ol class="list-subsections">
|
||||
<% _.each(xblockInfo.get('child_info').children, function(subsection) { %>
|
||||
<% if (subsection.isPublishable()) { %>
|
||||
<li class="outline-item outline-subsection">
|
||||
<h4 class="subsection-title item-title"><%= subsection.get('display_name') %></h4>
|
||||
<div class="subsection-content">
|
||||
<ol class="list-units">
|
||||
<% _.each(subsection.get('child_info').children, function(unit) { %>
|
||||
<% if (unit.isPublishable()) { %>
|
||||
<li class="outline-item outline-unit">
|
||||
<span class="unit-title item-title"><%= unit.get('display_name') %></span>
|
||||
</li>
|
||||
<% } %>
|
||||
<% }); %>
|
||||
</ol>
|
||||
</div>
|
||||
</li>
|
||||
<% } %>
|
||||
<% }); %>
|
||||
</ol>
|
||||
<% } else { %>
|
||||
<ol class="list-units">
|
||||
<% _.each(xblockInfo.get('child_info').children, function(unit) { %>
|
||||
<% if (unit.isPublishable()) { %>
|
||||
<li class="outline-item outline-unit">
|
||||
<span class="unit-title item-title"><%= unit.get('display_name') %></span>
|
||||
</li>
|
||||
<% } %>
|
||||
<% }); %>
|
||||
</ol>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
@@ -46,10 +46,11 @@ var visibleToStaffOnly = visibilityState === 'staff_only';
|
||||
<h5 class="title"><%= releaseLabel %></h5>
|
||||
<p class="copy">
|
||||
<% if (releaseDate) { %>
|
||||
<% var message = gettext("%(release_date)s with %(section_or_subsection)s") %>
|
||||
<%= interpolate(message, {
|
||||
release_date: '<span class="release-date">' + releaseDate + '</span>',
|
||||
section_or_subsection: '<span class="release-with">' + releaseDateFrom + '</span>' }, true) %>
|
||||
<span class="release-date"><%= releaseDate %></span>
|
||||
<span class="release-with">
|
||||
<%= interpolate(gettext('with %(release_date_from)s'), { release_date_from: releaseDateFrom }, true) %>
|
||||
</span>
|
||||
|
||||
<% } else { %>
|
||||
<%= gettext("Unscheduled") %>
|
||||
<% } %>
|
||||
@@ -65,13 +66,20 @@ var visibleToStaffOnly = visibilityState === 'staff_only';
|
||||
<% } %>
|
||||
</h5>
|
||||
<% if (visibleToStaffOnly) { %>
|
||||
<p class="copy"><%= gettext("Staff Only") %></p>
|
||||
<p class="copy">
|
||||
<%= gettext("Staff Only") %>
|
||||
<% if (!hasExplicitStaffLock) { %>
|
||||
<span class="inherited-from">
|
||||
<%= interpolate(gettext("with %(section_or_subsection)s"),{ section_or_subsection: staffLockFrom }, true) %>
|
||||
</span>
|
||||
<% } %>
|
||||
</p>
|
||||
<% } else { %>
|
||||
<p class="copy"><%= gettext("Staff and Students") %></p>
|
||||
<% } %>
|
||||
<p class="action-inline">
|
||||
<a href="" class="action-staff-lock" role="button" aria-pressed="<%= visibleToStaffOnly %>">
|
||||
<% if (visibleToStaffOnly) { %>
|
||||
<a href="" class="action-staff-lock" role="button" aria-pressed="<%= hasExplicitStaffLock %>">
|
||||
<% if (hasExplicitStaffLock) { %>
|
||||
<i class="icon-check"></i>
|
||||
<% } else { %>
|
||||
<i class="icon-check-empty"></i>
|
||||
|
||||
28
cms/templates/js/release-date-editor.underscore
Normal file
28
cms/templates/js/release-date-editor.underscore
Normal file
@@ -0,0 +1,28 @@
|
||||
<h3 class="modal-section-title"><%= gettext('Release Date and Time') %></h3>
|
||||
<div class="modal-section-content has-actions">
|
||||
<ul class="list-fields list-input datepair">
|
||||
<li class="field field-text field-start-date field-release-date">
|
||||
<label for="start_date" class="label"><%= gettext('Release Date:') %></label>
|
||||
<input type="text" id="start_date" name="start_date"
|
||||
value=""
|
||||
placeholder="MM/DD/YYYY" class="start-date release-date date input input-text" autocomplete="off" />
|
||||
</li>
|
||||
<li class="field field-text field-start-time field-release-time">
|
||||
<label for="start_time" class="label"><%= gettext('Release Time in UTC:') %></label>
|
||||
<input type="text" id="start_time" name="start_time"
|
||||
value=""
|
||||
placeholder="HH:MM" class="start-time release-time time input input-text" autocomplete="off" />
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<% if (xblockInfo.isSequential()) { %>
|
||||
<ul class="list-actions">
|
||||
<li class="action-item">
|
||||
<a href="#" data-tooltip="<%= gettext('Clear Release Date/Time') %>" class="clear-date action-button action-clear">
|
||||
<i class="icon-undo"></i>
|
||||
<span class="sr"><%= gettext('Clear Release Date/Time') %></span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<% } %>
|
||||
</div>
|
||||
35
cms/templates/js/staff-lock-editor.underscore
Normal file
35
cms/templates/js/staff-lock-editor.underscore
Normal file
@@ -0,0 +1,35 @@
|
||||
<form>
|
||||
<h3 class="modal-section-title"><%= gettext('Student Visibility') %></h3>
|
||||
<div class="modal-section-content staff-lock">
|
||||
<ul class="list-fields list-input">
|
||||
<li class="field field-checkbox checkbox-cosmetic">
|
||||
<input type="checkbox" id="staff_lock" name="staff_lock" class="input input-checkbox" />
|
||||
<label for="staff_lock" class="label">
|
||||
<i class="icon-check input-checkbox-checked"></i>
|
||||
<i class="icon-check-empty input-checkbox-unchecked"></i>
|
||||
<%= gettext('Hide from students') %>
|
||||
</label>
|
||||
|
||||
<% if (hasExplicitStaffLock && !ancestorLocked) { %>
|
||||
<p class="tip tip-warning">
|
||||
<% if (xblockInfo.isVertical()) { %>
|
||||
<%= gettext('If the unit was previously published and released to students, any changes you made to the unit when it was hidden will now be visible to students.') %>
|
||||
<% } else { %>
|
||||
<% var message = gettext('If you make this %(xblockType)s visible to students, students will be able to see its content after the release date has passed and you have published the unit(s).'); %>
|
||||
<%= interpolate(message, { xblockType: xblockType }, true) %>
|
||||
<% } %>
|
||||
</p>
|
||||
|
||||
<p class="tip tip-warning">
|
||||
<% if (xblockInfo.isChapter()) { %>
|
||||
<%= gettext('Any subsections or units that are explicitly hidden from students will remain hidden after you clear this option for the section.') %>
|
||||
<% } %>
|
||||
<% if (xblockInfo.isSequential()) { %>
|
||||
<%= gettext('Any units that are explicitly hidden from students will remain hidden after you clear this option for the subsection.') %>
|
||||
<% } %>
|
||||
</p>
|
||||
<% } %>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</form>
|
||||
@@ -24,8 +24,8 @@ from django.utils.translation import ugettext as _
|
||||
|
||||
<ol class="list-input">
|
||||
<li class="field text required" id="field-email">
|
||||
<label for="email">${_("Email Address")}</label>
|
||||
<input id="email" type="email" name="email" placeholder="e.g. jane.doe@gmail.com" />
|
||||
<label for="email">${_("E-mail")}</label>
|
||||
<input id="email" type="email" name="email" placeholder="${_('example: username@domain.com')}"/>
|
||||
</li>
|
||||
|
||||
<li class="field text required" id="field-password">
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<article class="content-primary" role="main">
|
||||
%if allow_actions:
|
||||
<div class="wrapper-create-element animate wrapper-create-user">
|
||||
<form class="create-user" id="create-user-form" name="create-user-form">
|
||||
<form class="form-create create-user" id="create-user-form" name="create-user-form">
|
||||
<div class="wrapper-form">
|
||||
<h3 class="title">${_("Add a User to Your Course's Team")}</h3>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
<ol class="list-input">
|
||||
<li class="field text required create-user-email">
|
||||
<label for="user-email-input">${_("User's Email Address")}</label>
|
||||
<input id="user-email-input" name="user-email" type="text" placeholder="${_('e.g. jane.doe@gmail.com')}" value="">
|
||||
<input id="user-email-input" name="user-email" type="text" placeholder="${_('example: username@domain.com')}" value="">
|
||||
<span class="tip tip-stacked">${_("Please provide the email address of the course staff member you'd like to add")}</span>
|
||||
</li>
|
||||
</ol>
|
||||
@@ -147,7 +147,7 @@
|
||||
<div class="bit">
|
||||
<h3 class="title-3">${_("Course Team Roles")}</h3>
|
||||
<p>${_("Course team members, or staff, are course co-authors. They have full writing and editing privileges on all course content.")}</p>
|
||||
<p>${_("Admins are course team members who can add and remove other course team members.")}</p>
|
||||
<p>${_("Admins are course team members who can add and remove other course team members.")}</p>
|
||||
</div>
|
||||
|
||||
% if user_is_instuctor and len(instructors) == 1:
|
||||
|
||||
@@ -27,18 +27,18 @@
|
||||
|
||||
<ol class="list-input">
|
||||
<li class="field text required" id="field-email">
|
||||
<label for="email">${_("Email Address")}</label>
|
||||
<input id="email" type="email" name="email" placeholder="e.g. jane.doe@gmail.com" />
|
||||
<label for="email">${_("E-mail")}</label>
|
||||
<input id="email" type="email" name="email" placeholder="example: username@domain.com" />
|
||||
</li>
|
||||
|
||||
<li class="field text required" id="field-name">
|
||||
<label for="name">${_("Full Name")}</label>
|
||||
<input id="name" type="text" name="name" placeholder="e.g. Jane Doe" />
|
||||
<input id="name" type="text" name="name" placeholder="example: Jane Doe" />
|
||||
</li>
|
||||
|
||||
<li class="field text required" id="field-username">
|
||||
<label for="username">${_("Public Username")}</label>
|
||||
<input id="username" type="text" name="username" placeholder="e.g. janedoe" />
|
||||
<input id="username" type="text" name="username" placeholder="example: JaneDoe" />
|
||||
<span class="tip tip-stacked">${_("This will be used in public discussions with your courses and in our edX101 support forums")}</span>
|
||||
</li>
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user