Merge branch 'danielli/pyfs' of https://github.com/edx/edx-platform into danielli/pyfs

This commit is contained in:
swdanielli
2014-08-25 15:40:06 -04:00
604 changed files with 121021 additions and 44470 deletions

View File

@@ -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.

View File

@@ -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>

View File

@@ -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.

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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
)

View File

@@ -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

View File

@@ -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)

View File

@@ -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):

View File

@@ -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))

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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):
"""

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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()

View File

@@ -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)

View File

@@ -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):

View File

@@ -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'

View File

@@ -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

View File

@@ -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", "")

View File

@@ -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 **",

View File

@@ -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',
}

View File

@@ -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 ##################################

View File

@@ -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',)

View File

@@ -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'

View File

@@ -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",

View File

@@ -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
};
});

View File

@@ -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 }
};
}

View File

@@ -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;

View File

@@ -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();
});

View File

@@ -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);
});
});
}

View File

@@ -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);

View File

@@ -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();
});

View File

@@ -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);

View File

@@ -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);
});

View File

@@ -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() {

View 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/');
});
});
});

View 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');
});
});
});

View File

@@ -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, {

View File

@@ -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;
}
});

View File

@@ -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)
});
}
}

View 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
};
});

View 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));
}
};
});

View File

@@ -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;
});

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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&hellip;'),
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();
},

View File

@@ -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&hellip;'),
if (enableStaffLock && !hasInheritedStaffLock) {
ViewUtils.runOperationShowingMessage(gettext('Hiding from Students&hellip;'),
_.bind(saveAndPublishStaffLock, self));
} else if (enableStaffLock && hasInheritedStaffLock) {
ViewUtils.runOperationShowingMessage(gettext('Explicitly Hiding from Students&hellip;'),
_.bind(saveAndPublishStaffLock, self));
} else if (!enableStaffLock && hasInheritedStaffLock) {
ViewUtils.runOperationShowingMessage(gettext('Inheriting Student Visibility&hellip;'),
_.bind(saveAndPublishStaffLock, self));
} else {
ViewUtils.confirmThenRunOperation(gettext("Make Visible to Students"),

View File

@@ -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() {

View 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
};
};
});

View File

@@ -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
};
});

View File

@@ -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
};
});

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;
}
// ====================

View File

@@ -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

View File

@@ -170,6 +170,7 @@ $color-staff-only: $black;
$color-heading-base: $gray-d2;
$color-copy-base: $gray-l1;
$color-copy-emphasized: $gray-d2;
// ====================

View File

@@ -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 {

View File

@@ -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

View File

@@ -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 {

View File

@@ -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

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}
// ====================

View File

@@ -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;

View File

@@ -10,6 +10,9 @@
%t-strong {
font-weight: 600;
}
%t-demi-strong {
font-weight: 500;
}
%t-regular {
font-weight: 400;
}

View File

@@ -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;
}
}
}

View File

@@ -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';

View File

@@ -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;
}
}
}

View 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;
}
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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>

View File

@@ -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>

View 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>

View File

@@ -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>

View File

@@ -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>

View 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>

View File

@@ -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">

View 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>

View File

@@ -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>

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>

View 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>

View 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>

View File

@@ -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">

View File

@@ -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='{&#34;display_name&#34;: {&#34;default_value&#34;: null, &#34;explicitly_set&#34;: true, &#34;display_name&#34;: &#34;Display Name&#34;, &#34;help&#34;: &#34;This name appears in the horizontal navigation at the top of the page.&#34;, &#34;type&#34;: &#34;Generic&#34;, &#34;value&#34;: &#34;Poll Question&#34;, &#34;field_name&#34;: &#34;display_name&#34;, &#34;options&#34;: []}, &#34;due&#34;: {&#34;default_value&#34;: null, &#34;explicitly_set&#34;: false, &#34;display_name&#34;: &#34;due&#34;, &#34;help&#34;: &#34;Date that this problem is due by&#34;, &#34;type&#34;: &#34;Generic&#34;, &#34;value&#34;: null, &#34;field_name&#34;: &#34;due&#34;, &#34;options&#34;: []}}'/>
<div class="wrapper-comp-settings metadata_edit" id="settings-tab"
data-metadata='{&#34;display_name&#34;: {&#34;default_value&#34;: null, &#34;explicitly_set&#34;: true, &#34;display_name&#34;: &#34;Display Name&#34;, &#34;help&#34;: &#34;This name appears in the horizontal navigation at the top of the page.&#34;, &#34;type&#34;: &#34;Generic&#34;, &#34;value&#34;: &#34;Poll Question&#34;, &#34;field_name&#34;: &#34;display_name&#34;, &#34;options&#34;: []}, &#34;due&#34;: {&#34;default_value&#34;: null, &#34;explicitly_set&#34;: false, &#34;display_name&#34;: &#34;due&#34;, &#34;help&#34;: &#34;Date that this problem is due by&#34;, &#34;type&#34;: &#34;Generic&#34;, &#34;value&#34;: null, &#34;field_name&#34;: &#34;due&#34;, &#34;options&#34;: []}}'/>
<textarea data-metadata-name="custom_field">Custom Value</textarea>
</section>
</div>

View 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>
<% } %>

View File

@@ -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>

View 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>

View 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>

View File

@@ -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">

View File

@@ -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:

View File

@@ -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