Commit 915f815c by Vik Paruchuri

Merge remote-tracking branch 'origin/master' into fix/vik/studio-oe

Conflicts:
	common/lib/xmodule/xmodule/combined_open_ended_module.py
parents d710d8e4 8300bb5e
......@@ -45,3 +45,4 @@ node_modules
autodeploy.properties
.ws_migrations_complete
.vagrant/
logs
[main]
host = https://www.transifex.com
[edx-studio.django-partial]
[edx-platform.django-partial]
file_filter = conf/locale/<lang>/LC_MESSAGES/django-partial.po
source_file = conf/locale/en/LC_MESSAGES/django-partial.po
source_lang = en
type = PO
[edx-studio.djangojs]
[edx-platform.djangojs]
file_filter = conf/locale/<lang>/LC_MESSAGES/djangojs.po
source_file = conf/locale/en/LC_MESSAGES/djangojs.po
source_lang = en
type = PO
[edx-studio.mako]
[edx-platform.mako]
file_filter = conf/locale/<lang>/LC_MESSAGES/mako.po
source_file = conf/locale/en/LC_MESSAGES/mako.po
source_lang = en
type = PO
[edx-studio.messages]
[edx-platform.messages]
file_filter = conf/locale/<lang>/LC_MESSAGES/messages.po
source_file = conf/locale/en/LC_MESSAGES/messages.po
source_lang = en
......
......@@ -79,4 +79,5 @@ Bethany LaPenta <lapentab@mit.edu>
Renzo Lucioni <renzolucioni@gmail.com>
Felix Sun <felixsun@mit.edu>
Adam Palay <adam@edx.org>
Ian Hoover <ihoover@edx.org>
\ No newline at end of file
Ian Hoover <ihoover@edx.org>
Mukul Goyal <miki@edx.org>
......@@ -43,11 +43,20 @@ history of background tasks for a given problem and student.
Blades: Small UX fix on capa multiple-choice problems. Make labels only
as wide as the text to reduce accidental choice selections.
Studio:
- use xblock field defaults to initialize all new instances' fields and
only use templates as override samples.
- create new instances via in memory create_xmodule and related methods rather
than cloning a db record.
- have an explicit method for making a draft copy as distinct from making a new module.
Studio: Remove XML from the video component editor. All settings are
moved to be edited as metadata.
XModule: Only write out assets files if the contents have changed.
Studio: Course settings are now saved explicitly.
XModule: Don't delete generated xmodule asset files when compiling (for
instance, when XModule provides a coffeescript file, don't delete
the associated javascript)
......
......@@ -239,7 +239,6 @@ CMS templates. Fortunately, `rake` will do all of this for you! Just run:
$ rake django-admin[syncdb]
$ rake django-admin[migrate]
$ rake cms:update_templates
If you are running these commands using the [`zsh`](http://www.zsh.org/) shell,
zsh will assume that you are doing
......
......@@ -20,8 +20,8 @@ def get_course_updates(location):
try:
course_updates = modulestore('direct').get_item(location)
except ItemNotFoundError:
template = Location(['i4x', 'edx', "templates", 'course_info', "Empty"])
course_updates = modulestore('direct').clone_item(template, Location(location))
modulestore('direct').create_and_save_xmodule(location)
course_updates = modulestore('direct').get_item(location)
# current db rep: {"_id" : locationjson, "definition" : { "data" : "<ol>[<li><h2>date</h2>content</li>]</ol>"} "metadata" : ignored}
location_base = course_updates.location.url()
......
......@@ -46,3 +46,9 @@ Feature: Advanced (manual) course policy
Then it is displayed as a string
And I reload the page
Then it is displayed as a string
Scenario: Confirmation is shown on save
Given I am on the Advanced Course Settings page in Studio
When I edit the value of a policy key
And I press the "Save" notification button
Then I see a confirmation that my changes have been saved
......@@ -2,8 +2,8 @@
#pylint: disable=W0621
from lettuce import world, step
from nose.tools import assert_false, assert_equal, assert_regexp_matches
from common import type_in_codemirror
from nose.tools import assert_false, assert_equal, assert_regexp_matches, assert_true
from common import type_in_codemirror, press_the_notification_button
KEY_CSS = '.key input.policy-key'
VALUE_CSS = 'textarea.json'
......@@ -25,20 +25,6 @@ def i_am_on_advanced_course_settings(step):
step.given('I select the Advanced Settings')
@step(u'I press the "([^"]*)" notification button$')
def press_the_notification_button(step, name):
css = 'a.action-%s' % name.lower()
# Save was clicked if either the save notification bar is gone, or we have a error notification
# overlaying it (expected in the case of typing Object into display_name).
def save_clicked():
confirmation_dismissed = world.is_css_not_present('.is-shown.wrapper-notification-warning')
error_showing = world.is_css_present('.is-shown.wrapper-notification-error')
return confirmation_dismissed or error_showing
world.css_click(css, success_condition=save_clicked)
@step(u'I edit the value of a policy key$')
def edit_the_value_of_a_policy_key(step):
type_in_codemirror(get_index_of(DISPLAY_NAME_KEY), 'X')
......@@ -113,7 +99,7 @@ def assert_policy_entries(expected_keys, expected_values):
def get_index_of(expected_key):
for counter in range(len(world.css_find(KEY_CSS))):
# Sometimes get stale reference if I hold on to the array of elements
key = world.css_find(KEY_CSS)[counter].value
key = world.css_value(KEY_CSS, index=counter)
if key == expected_key:
return counter
......@@ -122,7 +108,7 @@ def get_index_of(expected_key):
def get_display_name_value():
index = get_index_of(DISPLAY_NAME_KEY)
return world.css_find(VALUE_CSS)[index].value
return world.css_value(VALUE_CSS, index=index)
def change_display_name_value(step, new_value):
......
......@@ -61,7 +61,7 @@ def i_select_a_link_to_the_course_outline(step):
@step('I am brought to the course outline page$')
def i_am_brought_to_course_outline(step):
assert_in('Course Outline', world.css_find('.outline .page-header')[0].text)
assert_in('Course Outline', world.css_text('.outline .page-header'))
assert_equal(1, len(world.browser.windows))
......
......@@ -12,9 +12,7 @@ import time
from logging import getLogger
logger = getLogger(__name__)
_COURSE_NAME = 'Robot Super Course'
_COURSE_NUM = '999'
_COURSE_ORG = 'MITx'
from terrain.browser import reset_data
########### STEP HELPERS ##############
......@@ -55,6 +53,48 @@ def i_have_opened_a_new_course(_step):
open_new_course()
@step(u'I press the "([^"]*)" notification button$')
def press_the_notification_button(_step, name):
css = 'a.action-%s' % name.lower()
# The button was clicked if either the notification bar is gone,
# or we see an error overlaying it (expected for invalid inputs).
def button_clicked():
confirmation_dismissed = world.is_css_not_present('.is-shown.wrapper-notification-warning')
error_showing = world.is_css_present('.is-shown.wrapper-notification-error')
return confirmation_dismissed or error_showing
world.css_click(css, success_condition=button_clicked), '%s button not clicked after 5 attempts.' % name
@step('I change the "(.*)" field to "(.*)"$')
def i_change_field_to_value(_step, field, value):
field_css = '#%s' % '-'.join([s.lower() for s in field.split()])
ele = world.css_find(field_css).first
ele.fill(value)
ele._element.send_keys(Keys.ENTER)
@step('I reset the database')
def reset_the_db(_step):
"""
When running Lettuce tests using examples (i.e. "Confirmation is
shown on save" in course-settings.feature), the normal hooks
aren't called between examples. reset_data should run before each
scenario to flush the test database. When this doesn't happen we
get errors due to trying to insert a non-unique entry. So instead,
we delete the database manually. This has the effect of removing
any users and courses that have been created during the test run.
"""
reset_data(None)
@step('I see a confirmation that my changes have been saved')
def i_see_a_confirmation(step):
confirmation_css = '#alert-confirmation'
assert world.is_css_present(confirmation_css)
####### HELPER FUNCTIONS ##############
def open_new_course():
world.clear_courses()
......@@ -80,9 +120,9 @@ def create_studio_user(
def fill_in_course_info(
name=_COURSE_NAME,
org=_COURSE_ORG,
num=_COURSE_NUM):
name='Robot Super Course',
org='MITx',
num='999'):
world.css_fill('.new-course-name', name)
world.css_fill('.new-course-org', org)
world.css_fill('.new-course-number', num)
......@@ -100,21 +140,28 @@ def log_into_studio(
world.is_css_present(signin_css)
world.css_click(signin_css)
login_form = world.browser.find_by_css('form#login_form')
login_form.find_by_name('email').fill(email)
login_form.find_by_name('password').fill(password)
login_form.find_by_name('submit').click()
def fill_login_form():
login_form = world.browser.find_by_css('form#login_form')
login_form.find_by_name('email').fill(email)
login_form.find_by_name('password').fill(password)
login_form.find_by_name('submit').click()
world.retry_on_exception(fill_login_form)
assert_true(world.is_css_present('.new-course-button'))
world.scenario_dict['USER'] = get_user_by_email(email)
def create_a_course():
world.CourseFactory.create(org=_COURSE_ORG, course=_COURSE_NUM, display_name=_COURSE_NAME)
world.scenario_dict['COURSE'] = world.CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
# Add the user to the instructor group of the course
# so they will have the permissions to see it in studio
course = world.GroupFactory.create(name='instructor_MITx/{course_num}/{course_name}'.format(course_num=_COURSE_NUM, course_name=_COURSE_NAME.replace(" ", "_")))
user = get_user_by_email('robot+studio@edx.org')
course = world.GroupFactory.create(name='instructor_MITx/{}/{}'.format(world.scenario_dict['COURSE'].number,
world.scenario_dict['COURSE'].display_name.replace(" ", "_")))
if world.scenario_dict.get('USER') is None:
user = world.scenario_dict['USER']
else:
user = get_user_by_email('robot+studio@edx.org')
user.groups.add(course)
user.save()
world.browser.reload()
......@@ -161,7 +208,7 @@ def set_date_and_time(date_css, desired_date, time_css, desired_time):
def i_created_a_video_component(step):
world.create_component_instance(
step, '.large-video-icon',
'i4x://edx/templates/video/default',
'video',
'.xmodule_VideoModule'
)
......@@ -179,11 +226,18 @@ def shows_captions(step, show_captions):
# Prevent cookies from overriding course settings
world.browser.cookies.delete('hide_captions')
if show_captions == 'does not':
assert world.css_find('.video')[0].has_class('closed')
assert world.css_has_class('.video', 'closed')
else:
assert world.is_css_not_present('.video.closed')
@step('the save button is disabled$')
def save_button_disabled(step):
button_css = '.action-save'
disabled = 'is-disabled'
assert world.css_has_class(button_css, disabled)
def type_in_codemirror(index, text):
world.css_click(".CodeMirror", index=index)
g = world.css_find("div.CodeMirror.CodeMirror-focused > div > textarea")
......
......@@ -7,9 +7,9 @@ from terrain.steps import reload_the_page
@world.absorb
def create_component_instance(step, component_button_css, instance_id, expected_css):
def create_component_instance(step, component_button_css, category, expected_css, boilerplate=None):
click_new_component_button(step, component_button_css)
click_component_from_menu(instance_id, expected_css)
click_component_from_menu(category, boilerplate, expected_css)
@world.absorb
......@@ -19,7 +19,7 @@ def click_new_component_button(step, component_button_css):
@world.absorb
def click_component_from_menu(instance_id, expected_css):
def click_component_from_menu(category, boilerplate, expected_css):
"""
Creates a component from `instance_id`. For components with more
than one template, clicks on `elem_css` to create the new
......@@ -27,11 +27,13 @@ def click_component_from_menu(instance_id, expected_css):
as the user clicks the appropriate button, so we assert that the
expected component is present.
"""
elem_css = "a[data-location='%s']" % instance_id
if boilerplate:
elem_css = "a[data-category='{}'][data-boilerplate='{}']".format(category, boilerplate)
else:
elem_css = "a[data-category='{}']:not([data-boilerplate])".format(category)
elements = world.css_find(elem_css)
assert(len(elements) == 1)
if elements[0]['id'] == instance_id: # If this is a component with multiple templates
world.css_click(elem_css)
assert_equal(len(elements), 1)
world.css_click(elem_css)
assert_equal(1, len(world.css_find(expected_css)))
......
......@@ -5,15 +5,18 @@ Feature: Course Settings
Given I have opened a new course in Studio
When I select Schedule and Details
And I set course dates
And I press the "Save" notification button
Then I see the set dates on refresh
Scenario: User can clear previously set course dates (except start date)
Given I have set course dates
And I clear all the dates except start
And I press the "Save" notification button
Then I see cleared dates on refresh
Scenario: User cannot clear the course start date
Given I have set course dates
And I press the "Save" notification button
And I clear the course start date
Then I receive a warning about course start date
And The previously set start date is shown on refresh
......@@ -21,5 +24,50 @@ Feature: Course Settings
Scenario: User can correct the course start date warning
Given I have tried to clear the course start
And I have entered a new course start date
And I press the "Save" notification button
Then The warning about course start date goes away
And My new course start date is shown on refresh
Scenario: Settings are only persisted when saved
Given I have set course dates
And I press the "Save" notification button
When I change fields
Then I do not see the new changes persisted on refresh
Scenario: Settings are reset on cancel
Given I have set course dates
And I press the "Save" notification button
When I change fields
And I press the "Cancel" notification button
Then I do not see the changes
Scenario: Confirmation is shown on save
Given I have opened a new course in Studio
When I select Schedule and Details
And I change the "<field>" field to "<value>"
And I press the "Save" notification button
Then I see a confirmation that my changes have been saved
# Lettuce hooks don't get called between each example, so we need
# to run the before.each_scenario hook manually to avoid database
# errors.
And I reset the database
Examples:
| field | value |
| Course Start Time | 11:00 |
| Course Introduction Video | 4r7wHMg5Yjg |
| Course Effort | 200:00 |
# Special case because we have to type in code mirror
Scenario: Changes in Course Overview show a confirmation
Given I have opened a new course in Studio
When I select Schedule and Details
And I change the course overview
And I press the "Save" notification button
Then I see a confirmation that my changes have been saved
Scenario: User cannot save invalid settings
Given I have opened a new course in Studio
When I select Schedule and Details
And I change the "Course Start Date" field to ""
Then the save button is disabled
......@@ -4,7 +4,7 @@
from lettuce import world, step
from terrain.steps import reload_the_page
from selenium.webdriver.common.keys import Keys
import time
from common import type_in_codemirror
from nose.tools import assert_true, assert_false, assert_equal
......@@ -47,22 +47,11 @@ def test_and_i_set_course_dates(step):
set_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME)
set_date_or_time(ENROLLMENT_END_TIME_CSS, DUMMY_TIME)
pause()
@step('Then I see the set dates on refresh$')
def test_then_i_see_the_set_dates_on_refresh(step):
reload_the_page(step)
verify_date_or_time(COURSE_START_DATE_CSS, '12/20/2013')
verify_date_or_time(COURSE_END_DATE_CSS, '12/26/2013')
verify_date_or_time(ENROLLMENT_START_DATE_CSS, '12/01/2013')
verify_date_or_time(ENROLLMENT_END_DATE_CSS, '12/10/2013')
verify_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME)
# Unset times get set to 12 AM once the corresponding date has been set.
verify_date_or_time(COURSE_END_TIME_CSS, DEFAULT_TIME)
verify_date_or_time(ENROLLMENT_START_TIME_CSS, DEFAULT_TIME)
verify_date_or_time(ENROLLMENT_END_TIME_CSS, DUMMY_TIME)
i_see_the_set_dates()
@step('And I clear all the dates except start$')
......@@ -71,8 +60,6 @@ def test_and_i_clear_all_the_dates_except_start(step):
set_date_or_time(ENROLLMENT_START_DATE_CSS, '')
set_date_or_time(ENROLLMENT_END_DATE_CSS, '')
pause()
@step('Then I see cleared dates on refresh$')
def test_then_i_see_cleared_dates_on_refresh(step):
......@@ -119,7 +106,6 @@ def test_i_have_tried_to_clear_the_course_start(step):
@step('I have entered a new course start date$')
def test_i_have_entered_a_new_course_start_date(step):
set_date_or_time(COURSE_START_DATE_CSS, '12/22/2013')
pause()
@step('The warning about course start date goes away$')
......@@ -137,6 +123,30 @@ def test_my_new_course_start_date_is_shown_on_refresh(step):
verify_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME)
@step('I change fields$')
def test_i_change_fields(step):
set_date_or_time(COURSE_START_DATE_CSS, '7/7/7777')
set_date_or_time(COURSE_END_DATE_CSS, '7/7/7777')
set_date_or_time(ENROLLMENT_START_DATE_CSS, '7/7/7777')
set_date_or_time(ENROLLMENT_END_DATE_CSS, '7/7/7777')
@step('I do not see the new changes persisted on refresh$')
def test_changes_not_shown_on_refresh(step):
step.then('Then I see the set dates on refresh')
@step('I do not see the changes')
def test_i_do_not_see_changes(_step):
i_see_the_set_dates()
@step('I change the course overview')
def test_change_course_overview(_step):
type_in_codemirror(0, "<h1>Overview</h1>")
############### HELPER METHODS ####################
def set_date_or_time(css, date_or_time):
"""
......@@ -152,12 +162,20 @@ def verify_date_or_time(css, date_or_time):
"""
Verifies date or time field.
"""
assert_equal(date_or_time, world.css_find(css).first.value)
assert_equal(date_or_time, world.css_value(css))
def pause():
def i_see_the_set_dates():
"""
Must sleep briefly to allow last time save to finish,
else refresh of browser will fail.
Ensure that each field has the value set in `test_and_i_set_course_dates`.
"""
time.sleep(float(1))
verify_date_or_time(COURSE_START_DATE_CSS, '12/20/2013')
verify_date_or_time(COURSE_END_DATE_CSS, '12/26/2013')
verify_date_or_time(ENROLLMENT_START_DATE_CSS, '12/01/2013')
verify_date_or_time(ENROLLMENT_END_DATE_CSS, '12/10/2013')
verify_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME)
# Unset times get set to 12 AM once the corresponding date has been set.
verify_date_or_time(COURSE_END_TIME_CSS, DEFAULT_TIME)
verify_date_or_time(ENROLLMENT_START_TIME_CSS, DEFAULT_TIME)
verify_date_or_time(ENROLLMENT_END_TIME_CSS, DUMMY_TIME)
......@@ -2,7 +2,7 @@
#pylint: disable=W0621
from lettuce import world, step
from common import create_studio_user, log_into_studio, _COURSE_NAME
from common import create_studio_user, log_into_studio
PASSWORD = 'test'
EMAIL_EXTENSION = '@edx.org'
......@@ -47,12 +47,12 @@ def other_user_login(_step, name):
@step(u's?he does( not)? see the course on (his|her) page')
def see_course(_step, doesnt_see_course, gender):
class_css = 'span.class-name'
all_courses = world.css_find(class_css)
all_courses = world.css_find(class_css, wait_time=1)
all_names = [item.html for item in all_courses]
if doesnt_see_course:
assert not _COURSE_NAME in all_names
assert not world.scenario_dict['COURSE'].display_name in all_names
else:
assert _COURSE_NAME in all_names
assert world.scenario_dict['COURSE'].display_name in all_names
@step(u's?he cannot delete users')
......
......@@ -24,7 +24,7 @@ def add_update(_step, text):
@step(u'I should( not)? see the update "([^"]*)"$')
def check_update(_step, doesnt_see_update, text):
update_css = 'div.update-contents'
update = world.css_find(update_css)
update = world.css_find(update_css, wait_time=1)
if doesnt_see_update:
assert len(update) == 0 or not text in update.html
else:
......
......@@ -45,7 +45,7 @@ def courseware_page_has_loaded_in_studio(step):
@step('I see the course listed in My Courses$')
def i_see_the_course_in_my_courses(step):
course_css = 'span.class-name'
assert world.css_has_text(course_css, 'Robot Super Course')
assert world.css_has_text(course_css, world.scenario_dict['COURSE'].display_name)
@step('I am on the "([^"]*)" tab$')
......
......@@ -8,7 +8,7 @@ from lettuce import world, step
def i_created_discussion_tag(step):
world.create_component_instance(
step, '.large-discussion-icon',
'i4x://edx/templates/discussion/Discussion_Tag',
'discussion',
'.xmodule_DiscussionModule'
)
......@@ -17,14 +17,14 @@ def i_created_discussion_tag(step):
def i_see_only_the_settings_and_values(step):
world.verify_all_setting_entries(
[
['Category', "Week 1", True],
['Display Name', "Discussion Tag", True],
['Subcategory', "Topic-Level Student-Visible Label", True]
['Category', "Week 1", False],
['Display Name', "Discussion Tag", False],
['Subcategory', "Topic-Level Student-Visible Label", False]
])
@step('creating a discussion takes a single click')
def discussion_takes_a_single_click(step):
assert(not world.is_css_present('.xmodule_DiscussionModule'))
world.css_click("a[data-location='i4x://edx/templates/discussion/Discussion_Tag']")
world.css_click("a[data-category='discussion']")
assert(world.is_css_present('.xmodule_DiscussionModule'))
......@@ -32,6 +32,7 @@ Feature: Course Grading
And I have populated the course
And I am viewing the grading settings
When I change assignment type "Homework" to "New Type"
And I press the "Save" notification button
And I go back to the main course page
Then I do see the assignment name "New Type"
And I do not see the assignment name "Homework"
......@@ -41,6 +42,7 @@ Feature: Course Grading
And I have populated the course
And I am viewing the grading settings
When I delete the assignment type "Homework"
And I press the "Save" notification button
And I go back to the main course page
Then I do not see the assignment name "Homework"
......@@ -49,5 +51,36 @@ Feature: Course Grading
And I have populated the course
And I am viewing the grading settings
When I add a new assignment type "New Type"
And I press the "Save" notification button
And I go back to the main course page
Then I do see the assignment name "New Type"
Scenario: Settings are only persisted when saved
Given I have opened a new course in Studio
And I have populated the course
And I am viewing the grading settings
When I change assignment type "Homework" to "New Type"
Then I do not see the changes persisted on refresh
Scenario: Settings are reset on cancel
Given I have opened a new course in Studio
And I have populated the course
And I am viewing the grading settings
When I change assignment type "Homework" to "New Type"
And I press the "Cancel" notification button
Then I see the assignment type "Homework"
Scenario: Confirmation is shown on save
Given I have opened a new course in Studio
And I have populated the course
And I am viewing the grading settings
When I change assignment type "Homework" to "New Type"
And I press the "Save" notification button
Then I see a confirmation that my changes have been saved
Scenario: User cannot save invalid settings
Given I have opened a new course in Studio
And I have populated the course
And I am viewing the grading settings
When I change assignment type "Homework" to ""
Then the save button is disabled
......@@ -3,6 +3,7 @@
from lettuce import world, step
from common import *
from terrain.steps import reload_the_page
@step(u'I am viewing the grading settings')
......@@ -63,7 +64,9 @@ def change_assignment_name(step, old_name, new_name):
@step(u'I go back to the main course page')
def main_course_page(step):
main_page_link_css = 'a[href="/MITx/999/course/Robot_Super_Course"]'
main_page_link_css = 'a[href="/%s/%s/course/%s"]' % (world.scenario_dict['COURSE'].org,
world.scenario_dict['COURSE'].number,
world.scenario_dict['COURSE'].display_name.replace(' ', '_'),)
world.css_click(main_page_link_css)
......@@ -89,8 +92,8 @@ def add_assignment_type(step, new_name):
add_button_css = '.add-grading-data'
world.css_click(add_button_css)
name_id = '#course-grading-assignment-name'
f = world.css_find(name_id)[4]
f._element.send_keys(new_name)
new_assignment = world.css_find(name_id)[-1]
new_assignment._element.send_keys(new_name)
@step(u'I have populated the course')
......@@ -99,10 +102,25 @@ def populate_course(step):
step.given('I have added a new subsection')
@step(u'I do not see the changes persisted on refresh$')
def changes_not_persisted(step):
reload_the_page(step)
name_id = '#course-grading-assignment-name'
assert(world.css_value(name_id) == 'Homework')
@step(u'I see the assignment type "(.*)"$')
def i_see_the_assignment_type(_step, name):
assignment_css = '#course-grading-assignment-name'
assignments = world.css_find(assignment_css)
types = [ele['value'] for ele in assignments]
assert name in types
def get_type_index(name):
name_id = '#course-grading-assignment-name'
f = world.css_find(name_id)
for i in range(len(f)):
if f[i].value == name:
return i
all_types = world.css_find(name_id)
for index in range(len(all_types)):
if world.css_value(name_id, index=index) == name:
return index
return -1
......@@ -7,11 +7,11 @@ from lettuce import world, step
@step('I have created a Blank HTML Page$')
def i_created_blank_html_page(step):
world.create_component_instance(
step, '.large-html-icon', 'i4x://edx/templates/html/Blank_HTML_Page',
step, '.large-html-icon', 'html',
'.xmodule_HtmlModule'
)
@step('I see only the HTML display name setting$')
def i_see_only_the_html_display_name(step):
world.verify_all_setting_entries([['Display Name', "Blank HTML Page", True]])
world.verify_all_setting_entries([['Display Name', "Blank HTML Page", False]])
Feature: Problem Editor
As a course author, I want to be able to create problems and edit their settings.
@skip
Scenario: User can view metadata
Given I have created a Blank Common Problem
When I edit and select Settings
Then I see five alphabetized settings and their expected values
And Edit High Level Source is not visible
@skip
Scenario: User can modify String values
Given I have created a Blank Common Problem
When I edit and select Settings
Then I can modify the display name
And my display name change is persisted on save
@skip
Scenario: User can specify special characters in String values
Given I have created a Blank Common Problem
When I edit and select Settings
Then I can specify special characters in the display name
And my special characters and persisted on save
@skip
Scenario: User can revert display name to unset
Given I have created a Blank Common Problem
When I edit and select Settings
Then I can revert the display name to unset
And my display name is unset on save
@skip
Scenario: User can select values in a Select
Given I have created a Blank Common Problem
When I edit and select Settings
......@@ -37,7 +32,6 @@ Feature: Problem Editor
And my change to randomization is persisted
And I can revert to the default value for randomization
@skip
Scenario: User can modify float input values
Given I have created a Blank Common Problem
When I edit and select Settings
......@@ -45,25 +39,21 @@ Feature: Problem Editor
And my change to weight is persisted
And I can revert to the default value of unset for weight
@skip
Scenario: User cannot type letters in float number field
Given I have created a Blank Common Problem
When I edit and select Settings
Then if I set the weight to "abc", it remains unset
@skip
Scenario: User cannot type decimal values integer number field
Given I have created a Blank Common Problem
When I edit and select Settings
Then if I set the max attempts to "2.34", it displays initially as "234", and is persisted as "234"
@skip
Scenario: User cannot type out of range values in an integer number field
Given I have created a Blank Common Problem
When I edit and select Settings
Then if I set the max attempts to "-3", it displays initially as "-3", and is persisted as "0"
@skip
Scenario: Settings changes are not saved on Cancel
Given I have created a Blank Common Problem
When I edit and select Settings
......@@ -71,13 +61,11 @@ Feature: Problem Editor
And I can modify the display name
Then If I press Cancel my changes are not persisted
@skip
Scenario: Edit High Level source is available for LaTeX problem
Given I have created a LaTeX Problem
When I edit and select Settings
Then Edit High Level Source is visible
@skip
Scenario: High Level source is persisted for LaTeX problem (bug STUD-280)
Given I have created a LaTeX Problem
When I edit and compile the High Level Source
......
......@@ -18,8 +18,9 @@ def i_created_blank_common_problem(step):
world.create_component_instance(
step,
'.large-problem-icon',
'i4x://edx/templates/problem/Blank_Common_Problem',
'.xmodule_CapaModule'
'problem',
'.xmodule_CapaModule',
'blank_common.yaml'
)
......@@ -35,8 +36,8 @@ def i_see_five_settings_with_values(step):
[DISPLAY_NAME, "Blank Common Problem", True],
[MAXIMUM_ATTEMPTS, "", False],
[PROBLEM_WEIGHT, "", False],
[RANDOMIZATION, "Never", True],
[SHOW_ANSWER, "Finished", True]
[RANDOMIZATION, "Never", False],
[SHOW_ANSWER, "Finished", False]
])
......@@ -94,7 +95,7 @@ def my_change_to_randomization_is_persisted(step):
def i_can_revert_to_default_for_randomization(step):
world.revert_setting_entry(RANDOMIZATION)
world.save_component_and_reopen(step)
world.verify_setting_entry(world.get_setting_entry(RANDOMIZATION), RANDOMIZATION, "Always", False)
world.verify_setting_entry(world.get_setting_entry(RANDOMIZATION), RANDOMIZATION, "Never", False)
@step('I can set the weight to "(.*)"?')
......@@ -156,7 +157,7 @@ def create_latex_problem(step):
world.click_new_component_button(step, '.large-problem-icon')
# Go to advanced tab.
world.css_click('#ui-id-2')
world.click_component_from_menu("i4x://edx/templates/problem/Problem_Written_in_LaTeX", '.xmodule_CapaModule')
world.click_component_from_menu("problem", "latex_problem.yaml", '.xmodule_CapaModule')
@step('I edit and compile the High Level Source')
......@@ -169,7 +170,7 @@ def edit_latex_source(step):
@step('my change to the High Level Source is persisted')
def high_level_source_persisted(step):
def verify_text(driver):
return world.css_find('.problem').text == 'hi'
return world.css_text('.problem') == 'hi'
world.wait_for(verify_text)
......@@ -177,7 +178,7 @@ def high_level_source_persisted(step):
@step('I view the High Level Source I see my changes')
def high_level_source_in_editor(step):
open_high_level_source()
assert_equal('hi', world.css_find('.source-edit-box').value)
assert_equal('hi', world.css_value('.source-edit-box'))
def verify_high_level_source_links(step, visible):
......@@ -203,7 +204,7 @@ def verify_modified_display_name_with_special_chars():
def verify_unset_display_name():
world.verify_setting_entry(world.get_setting_entry(DISPLAY_NAME), DISPLAY_NAME, '', False)
world.verify_setting_entry(world.get_setting_entry(DISPLAY_NAME), DISPLAY_NAME, 'Blank Advanced Problem', False)
def set_weight(weight):
......
......@@ -26,6 +26,7 @@ Feature: Create Section
When I click the Edit link for the release date
And I save a new section release date
Then the section release date is updated
And I see a "saving" notification
Scenario: Delete section
Given I have opened a new course in Studio
......
......@@ -42,6 +42,12 @@ def i_save_a_new_section_release_date(_step):
world.browser.click_link_by_text('Save')
@step('I see a "saving" notification')
def i_see_a_saving_notification(step):
saving_css = '.wrapper-notification-mini'
assert world.is_css_present(saving_css)
############ ASSERTIONS ###################
......@@ -64,7 +70,7 @@ def i_click_to_edit_section_name(_step):
def i_see_complete_section_name_with_quote_in_editor(_step):
css = '.section-name-edit input[type=text]'
assert world.is_css_present(css)
assert_equal(world.browser.find_by_css(css).value, 'Section with "Quote"')
assert_equal(world.css_value(css), 'Section with "Quote"')
@step('the section does not exist$')
......@@ -79,7 +85,7 @@ def i_see_a_release_date_for_my_section(_step):
css = 'span.published-status'
assert world.is_css_present(css)
status_text = world.browser.find_by_css(css).text
status_text = world.css_text(css)
# e.g. 11/06/2012 at 16:25
msg = 'Will Release:'
......
......@@ -7,12 +7,14 @@ from common import *
@step('I fill in the registration form$')
def i_fill_in_the_registration_form(step):
register_form = world.browser.find_by_css('form#register_form')
register_form.find_by_name('email').fill('robot+studio@edx.org')
register_form.find_by_name('password').fill('test')
register_form.find_by_name('username').fill('robot-studio')
register_form.find_by_name('name').fill('Robot Studio')
register_form.find_by_name('terms_of_service').check()
def fill_in_reg_form():
register_form = world.css_find('form#register_form')
register_form.find_by_name('email').fill('robot+studio@edx.org')
register_form.find_by_name('password').fill('test')
register_form.find_by_name('username').fill('robot-studio')
register_form.find_by_name('name').fill('Robot Studio')
register_form.find_by_name('terms_of_service').check()
world.retry_on_exception(fill_in_reg_form)
@step('I press the Create My Account button on the registration form$')
......
......@@ -22,7 +22,7 @@ def have_a_course_with_1_section(step):
section = world.ItemFactory.create(parent_location=course.location)
subsection1 = world.ItemFactory.create(
parent_location=section.location,
template='i4x://edx/templates/sequential/Empty',
category='sequential',
display_name='Subsection One',)
......@@ -33,18 +33,18 @@ def have_a_course_with_two_sections(step):
section = world.ItemFactory.create(parent_location=course.location)
subsection1 = world.ItemFactory.create(
parent_location=section.location,
template='i4x://edx/templates/sequential/Empty',
category='sequential',
display_name='Subsection One',)
section2 = world.ItemFactory.create(
parent_location=course.location,
display_name='Section Two',)
subsection2 = world.ItemFactory.create(
parent_location=section2.location,
template='i4x://edx/templates/sequential/Empty',
category='sequential',
display_name='Subsection Alpha',)
subsection3 = world.ItemFactory.create(
parent_location=section2.location,
template='i4x://edx/templates/sequential/Empty',
category='sequential',
display_name='Subsection Beta',)
......@@ -92,7 +92,7 @@ def i_expand_a_section(step):
def i_see_the_span_with_text(step, text):
span_locator = '.toggle-button-sections span'
assert_true(world.is_css_present(span_locator))
assert_equal(world.css_find(span_locator).value, text)
assert_equal(world.css_value(span_locator), text)
assert_true(world.css_visible(span_locator))
......@@ -108,13 +108,13 @@ def i_do_not_see_the_span_with_text(step, text):
def all_sections_are_expanded(step):
subsection_locator = 'div.subsection-list'
subsections = world.css_find(subsection_locator)
for s in subsections:
assert_true(s.visible)
for index in range(len(subsections)):
assert_true(world.css_visible(subsection_locator, index=index))
@step(u'all sections are collapsed$')
def all_sections_are_collapsed(step):
subsection_locator = 'div.subsection-list'
subsections = world.css_find(subsection_locator)
for s in subsections:
assert_false(s.visible)
for index in range(len(subsections)):
assert_false(world.css_visible(subsection_locator, index=index))
......@@ -32,7 +32,6 @@ Feature: Create Subsection
And I reload the page
Then I see the correct dates
@skip
Scenario: Delete a subsection
Given I have opened a new course section in Studio
And I have added a new subsection
......
......@@ -50,7 +50,7 @@ def i_click_to_edit_subsection_name(step):
def i_see_complete_subsection_name_with_quote_in_editor(step):
css = '.subsection-display-name-input'
assert world.is_css_present(css)
assert_equal(world.css_find(css).value, 'Subsection With "Quote"')
assert_equal(world.css_value(css), 'Subsection With "Quote"')
@step('I have set a release date and due date in different years$')
......@@ -69,7 +69,7 @@ def i_mark_it_as_homework(step):
@step('I see it marked as Homework$')
def i_see_it_marked__as_homework(step):
assert_equal(world.css_find(".status-label").value, 'Homework')
assert_equal(world.css_value(".status-label"), 'Homework')
############ ASSERTIONS ###################
......
......@@ -7,7 +7,7 @@ from lettuce import world, step
@step('I see the correct settings and default values$')
def i_see_the_correct_settings_and_values(step):
world.verify_all_setting_entries([['Default Speed', 'OEoXaMPEzfM', False],
['Display Name', 'default', True],
['Display Name', 'Video Title', False],
['Download Track', '', False],
['Download Video', '', False],
['Show Captions', 'True', False],
......
......@@ -18,7 +18,6 @@ Feature: Video Component
Given I have created a Video component
Then when I view the video it does show the captions
@skip
Scenario: Captions are toggled correctly
Given I have created a Video component
And I have toggled captions
......
......@@ -8,13 +8,13 @@ from lettuce import world, step
@step('when I view the video it does not have autoplay enabled')
def does_not_autoplay(_step):
assert world.css_find('.video')[0]['data-autoplay'] == 'False'
assert world.css_find('.video_control')[0].has_class('play')
assert world.css_has_class('.video_control', 'play')
@step('creating a video takes a single click')
def video_takes_a_single_click(_step):
assert(not world.is_css_present('.xmodule_VideoModule'))
world.css_click("a[data-location='i4x://edx/templates/video/default']")
world.css_click("a[data-category='video']")
assert(world.is_css_present('.xmodule_VideoModule'))
......
......@@ -14,11 +14,11 @@ unnamed_modules = 0
class Command(BaseCommand):
help = 'Import the specified data directory into the default ModuleStore'
help = 'Export the specified data directory into the default ModuleStore'
def handle(self, *args, **options):
if len(args) != 2:
raise CommandError("import requires two arguments: <course location> <output path>")
raise CommandError("export requires two arguments: <course location> <output path>")
course_id = args[0]
output_path = args[1]
......@@ -30,4 +30,4 @@ class Command(BaseCommand):
root_dir = os.path.dirname(output_path)
course_dir = os.path.splitext(os.path.basename(output_path))[0]
export_to_xml(modulestore('direct'), contentstore(), location, root_dir, course_dir)
export_to_xml(modulestore('direct'), contentstore(), location, root_dir, course_dir, modulestore())
###
### Script for exporting all courseware from Mongo to a directory
###
import os
from django.core.management.base import BaseCommand, CommandError
from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore
from xmodule.course_module import CourseDescriptor
unnamed_modules = 0
class Command(BaseCommand):
help = 'Export all courses from mongo to the specified data directory'
def handle(self, *args, **options):
if len(args) != 1:
raise CommandError("export requires one argument: <output path>")
output_path = args[0]
cs = contentstore()
ms = modulestore('direct')
root_dir = output_path
courses = ms.get_courses()
print "%d courses to export:" % len(courses)
cids = [x.id for x in courses]
print cids
for course_id in cids:
print "-"*77
print "Exporting course id = {0} to {1}".format(course_id, output_path)
if 1:
try:
location = CourseDescriptor.id_to_location(course_id)
course_dir = course_id.replace('/', '...')
export_to_xml(ms, cs, location, root_dir, course_dir, modulestore())
except Exception as err:
print "="*30 + "> Oops, failed to export %s" % course_id
print "Error:"
print err
from xmodule.templates import update_templates
from xmodule.modulestore.django import modulestore
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = 'Imports and updates the Studio component templates from the code pack and put in the DB'
def handle(self, *args, **options):
update_templates(modulestore('direct'))
......@@ -3,13 +3,13 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore import Location
def get_module_info(store, location, parent_location=None, rewrite_static_links=False):
def get_module_info(store, location, rewrite_static_links=False):
try:
module = store.get_item(location)
except ItemNotFoundError:
# create a new one
template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty'])
module = store.clone_item(template_location, location)
store.create_and_save_xmodule(location)
module = store.get_item(location)
data = module.data
if rewrite_static_links:
......@@ -29,7 +29,8 @@ def get_module_info(store, location, parent_location=None, rewrite_static_links=
'id': module.location.url(),
'data': data,
# TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
'metadata': module._model_data._kvs._metadata
# what's the intent here? all metadata incl inherited & namespaced?
'metadata': module.xblock_kvs._metadata
}
......@@ -37,14 +38,11 @@ def set_module_info(store, location, post_data):
module = None
try:
module = store.get_item(location)
except:
pass
if module is None:
# new module at this location
# presume that we have an 'Empty' template
template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty'])
module = store.clone_item(template_location, location)
except ItemNotFoundError:
# new module at this location: almost always used for the course about pages; thus, no parent. (there
# are quite a handful of about page types available for a course and only the overview is pre-created)
store.create_and_save_xmodule(location)
module = store.get_item(location)
if post_data.get('data') is not None:
data = post_data['data']
......@@ -79,4 +77,4 @@ def set_module_info(store, location, post_data):
# commit to datastore
# TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
store.update_metadata(location, module._model_data._kvs._metadata)
store.update_metadata(location, module.xblock_kvs._metadata)
......@@ -14,10 +14,12 @@ from xmodule.modulestore import Location
from models.settings.course_details import (CourseDetails, CourseSettingsEncoder)
from models.settings.course_grading import CourseGradingModel
from contentstore.utils import get_modulestore
from xmodule.modulestore.tests.factories import CourseFactory
from models.settings.course_metadata import CourseMetadata
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.django import modulestore
from xmodule.fields import Date
from .utils import CourseTestCase
......@@ -35,7 +37,6 @@ class CourseDetailsTestCase(CourseTestCase):
self.assertIsNone(details.enrollment_start, "enrollment_start date somehow initialized " + str(details.enrollment_start))
self.assertIsNone(details.enrollment_end, "enrollment_end date somehow initialized " + str(details.enrollment_end))
self.assertIsNone(details.syllabus, "syllabus somehow initialized" + str(details.syllabus))
self.assertEqual(details.overview, "", "overview somehow initialized" + details.overview)
self.assertIsNone(details.intro_video, "intro_video somehow initialized" + str(details.intro_video))
self.assertIsNone(details.effort, "effort somehow initialized" + str(details.effort))
......@@ -48,7 +49,6 @@ class CourseDetailsTestCase(CourseTestCase):
self.assertIsNone(jsondetails['enrollment_start'], "enrollment_start date somehow initialized ")
self.assertIsNone(jsondetails['enrollment_end'], "enrollment_end date somehow initialized ")
self.assertIsNone(jsondetails['syllabus'], "syllabus somehow initialized")
self.assertEqual(jsondetails['overview'], "", "overview somehow initialized")
self.assertIsNone(jsondetails['intro_video'], "intro_video somehow initialized")
self.assertIsNone(jsondetails['effort'], "effort somehow initialized")
......@@ -297,9 +297,8 @@ class CourseMetadataEditingTest(CourseTestCase):
"""
def setUp(self):
CourseTestCase.setUp(self)
# add in the full class too
import_from_xml(get_modulestore(self.course.location), 'common/test/data/', ['full'])
self.fullcourse_location = Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None])
CourseFactory.create(org='edX', course='999', display_name='Robot Super Course')
self.fullcourse_location = Location(['i4x', 'edX', '999', 'course', 'Robot_Super_Course', None])
def test_fetch_initial_fields(self):
test_model = CourseMetadata.fetch(self.course.location)
......@@ -309,7 +308,7 @@ class CourseMetadataEditingTest(CourseTestCase):
test_model = CourseMetadata.fetch(self.fullcourse_location)
self.assertNotIn('graceperiod', test_model, 'blacklisted field leaked in')
self.assertIn('display_name', test_model, 'full missing editable metadata field')
self.assertEqual(test_model['display_name'], 'Testing', "not expected value")
self.assertEqual(test_model['display_name'], 'Robot Super Course', "not expected value")
self.assertIn('rerandomize', test_model, 'Missing rerandomize metadata field')
self.assertIn('showanswer', test_model, 'showanswer field ')
self.assertIn('xqa_key', test_model, 'xqa_key field ')
......@@ -349,10 +348,10 @@ class CourseMetadataEditingTest(CourseTestCase):
# ensure no harm
self.assertNotIn('graceperiod', test_model, 'blacklisted field leaked in')
self.assertIn('display_name', test_model, 'full missing editable metadata field')
self.assertEqual(test_model['display_name'], 'Testing', "not expected value")
self.assertEqual(test_model['display_name'], 'Robot Super Course', "not expected value")
self.assertIn('rerandomize', test_model, 'Missing rerandomize metadata field')
# check for deletion effectiveness
self.assertEqual('closed', test_model['showanswer'], 'showanswer field still in')
self.assertEqual('finished', test_model['showanswer'], 'showanswer field still in')
self.assertEqual(None, test_model['xqa_key'], 'xqa_key field still in')
......
......@@ -35,7 +35,6 @@ class InternationalizationTest(ModuleStoreTestCase):
self.user.save()
self.course_data = {
'template': 'i4x://edx/templates/course/Empty',
'org': 'MITx',
'number': '999',
'display_name': 'Robot Super Course',
......
from contentstore.tests.test_course_settings import CourseTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from django.core.urlresolvers import reverse
from xmodule.capa_module import CapaDescriptor
import json
from xmodule.modulestore.django import modulestore
class DeleteItem(CourseTestCase):
......@@ -11,14 +14,199 @@ class DeleteItem(CourseTestCase):
def testDeleteStaticPage(self):
# Add static tab
data = {
data = json.dumps({
'parent_location': 'i4x://mitX/333/course/Dummy_Course',
'template': 'i4x://edx/templates/static_tab/Empty'
}
'category': 'static_tab'
})
resp = self.client.post(reverse('clone_item'), data)
resp = self.client.post(reverse('create_item'), data,
content_type="application/json")
self.assertEqual(resp.status_code, 200)
# Now delete it. There was a bug that the delete was failing (static tabs do not exist in draft modulestore).
resp = self.client.post(reverse('delete_item'), resp.content, "application/json")
self.assertEqual(resp.status_code, 200)
class TestCreateItem(CourseTestCase):
"""
Test the create_item handler thoroughly
"""
def response_id(self, response):
"""
Get the id from the response payload
:param response:
"""
parsed = json.loads(response.content)
return parsed['id']
def test_create_nicely(self):
"""
Try the straightforward use cases
"""
# create a chapter
display_name = 'Nicely created'
resp = self.client.post(
reverse('create_item'),
json.dumps({
'parent_location': self.course.location.url(),
'display_name': display_name,
'category': 'chapter'
}),
content_type="application/json"
)
self.assertEqual(resp.status_code, 200)
# get the new item and check its category and display_name
chap_location = self.response_id(resp)
new_obj = modulestore().get_item(chap_location)
self.assertEqual(new_obj.category, 'chapter')
self.assertEqual(new_obj.display_name, display_name)
self.assertEqual(new_obj.location.org, self.course.location.org)
self.assertEqual(new_obj.location.course, self.course.location.course)
# get the course and ensure it now points to this one
course = modulestore().get_item(self.course.location)
self.assertIn(chap_location, course.children)
# use default display name
resp = self.client.post(
reverse('create_item'),
json.dumps({
'parent_location': chap_location,
'category': 'vertical'
}),
content_type="application/json"
)
self.assertEqual(resp.status_code, 200)
vert_location = self.response_id(resp)
# create problem w/ boilerplate
template_id = 'multiplechoice.yaml'
resp = self.client.post(
reverse('create_item'),
json.dumps({
'parent_location': vert_location,
'category': 'problem',
'boilerplate': template_id
}),
content_type="application/json"
)
self.assertEqual(resp.status_code, 200)
prob_location = self.response_id(resp)
problem = modulestore('draft').get_item(prob_location)
# ensure it's draft
self.assertTrue(problem.is_draft)
# check against the template
template = CapaDescriptor.get_template(template_id)
self.assertEqual(problem.data, template['data'])
self.assertEqual(problem.display_name, template['metadata']['display_name'])
self.assertEqual(problem.markdown, template['metadata']['markdown'])
def test_create_item_negative(self):
"""
Negative tests for create_item
"""
# non-existent boilerplate: creates a default
resp = self.client.post(
reverse('create_item'),
json.dumps(
{'parent_location': self.course.location.url(),
'category': 'problem',
'boilerplate': 'nosuchboilerplate.yaml'
}),
content_type="application/json"
)
self.assertEqual(resp.status_code, 200)
class TestEditItem(CourseTestCase):
"""
Test contentstore.views.item.save_item
"""
def response_id(self, response):
"""
Get the id from the response payload
:param response:
"""
parsed = json.loads(response.content)
return parsed['id']
def setUp(self):
""" Creates the test course structure and a couple problems to 'edit'. """
super(TestEditItem, self).setUp()
# create a chapter
display_name = 'chapter created'
resp = self.client.post(
reverse('create_item'),
json.dumps(
{'parent_location': self.course.location.url(),
'display_name': display_name,
'category': 'chapter'
}),
content_type="application/json"
)
chap_location = self.response_id(resp)
resp = self.client.post(
reverse('create_item'),
json.dumps(
{'parent_location': chap_location,
'category': 'vertical'
}),
content_type="application/json"
)
vert_location = self.response_id(resp)
# create problem w/ boilerplate
template_id = 'multiplechoice.yaml'
resp = self.client.post(
reverse('create_item'),
json.dumps({'parent_location': vert_location,
'category': 'problem',
'boilerplate': template_id
}),
content_type="application/json"
)
self.problems = [self.response_id(resp)]
def test_delete_field(self):
"""
Sending null in for a field 'deletes' it
"""
self.client.post(
reverse('save_item'),
json.dumps({
'id': self.problems[0],
'metadata': {'rerandomize': 'onreset'}
}),
content_type="application/json"
)
problem = modulestore('draft').get_item(self.problems[0])
self.assertEqual(problem.rerandomize, 'onreset')
self.client.post(
reverse('save_item'),
json.dumps({
'id': self.problems[0],
'metadata': {'rerandomize': None}
}),
content_type="application/json"
)
problem = modulestore('draft').get_item(self.problems[0])
self.assertEqual(problem.rerandomize, 'never')
def test_null_field(self):
"""
Sending null in for a field 'deletes' it
"""
problem = modulestore('draft').get_item(self.problems[0])
self.assertIsNotNone(problem.markdown)
self.client.post(
reverse('save_item'),
json.dumps({
'id': self.problems[0],
'nullout': ['markdown']
}),
content_type="application/json"
)
problem = modulestore('draft').get_item(self.problems[0])
self.assertIsNone(problem.markdown)
......@@ -54,7 +54,6 @@ class CourseTestCase(ModuleStoreTestCase):
self.client.login(username=uname, password=password)
self.course = CourseFactory.create(
template='i4x://edx/templates/course/Empty',
org='MITx',
number='999',
display_name='Robot Super Course',
......
......@@ -9,23 +9,24 @@ import copy
import logging
import re
from xmodule.modulestore.draft import DIRECT_ONLY_CATEGORIES
from django.utils.translation import ugettext as _
log = logging.getLogger(__name__)
# In order to instantiate an open ended tab automatically, need to have this data
OPEN_ENDED_PANEL = {"name": "Open Ended Panel", "type": "open_ended"}
NOTES_PANEL = {"name": "My Notes", "type": "notes"}
OPEN_ENDED_PANEL = {"name": _("Open Ended Panel"), "type": "open_ended"}
NOTES_PANEL = {"name": _("My Notes"), "type": "notes"}
EXTRA_TAB_PANELS = dict([(p['type'], p) for p in [OPEN_ENDED_PANEL, NOTES_PANEL]])
def get_modulestore(location):
def get_modulestore(category_or_location):
"""
Returns the correct modulestore to use for modifying the specified location
"""
if not isinstance(location, Location):
location = Location(location)
if isinstance(category_or_location, Location):
category_or_location = category_or_location.category
if location.category in DIRECT_ONLY_CATEGORIES:
if category_or_location in DIRECT_ONLY_CATEGORIES:
return modulestore('direct')
else:
return modulestore()
......
......@@ -7,11 +7,11 @@ from django.views.decorators.http import require_http_methods
from django_future.csrf import ensure_csrf_cookie
from mitxmako.shortcuts import render_to_response
from xmodule.modulestore import Location
from xmodule.modulestore.inheritance import own_metadata
from ..utils import get_modulestore, get_url_reverse
from .access import get_location_and_verify_access
from xmodule.course_module import CourseDescriptor
__all__ = ['get_checklists', 'update_checklist']
......@@ -28,13 +28,11 @@ def get_checklists(request, org, course, name):
modulestore = get_modulestore(location)
course_module = modulestore.get_item(location)
new_course_template = Location('i4x', 'edx', 'templates', 'course', 'Empty')
template_module = modulestore.get_item(new_course_template)
# If course was created before checklists were introduced, copy them over from the template.
copied = False
if not course_module.checklists:
course_module.checklists = template_module.checklists
course_module.checklists = CourseDescriptor.checklists.default
copied = True
checklists, modified = expand_checklist_action_urls(course_module)
......
......@@ -26,6 +26,8 @@ from models.settings.course_grading import CourseGradingModel
from .requests import _xmodule_recurse
from .access import has_access
from xmodule.x_module import XModuleDescriptor
from xblock.plugin import PluginMissingError
__all__ = ['OPEN_ENDED_COMPONENT_TYPES',
'ADVANCED_COMPONENT_POLICY_KEY',
......@@ -101,7 +103,7 @@ def edit_subsection(request, location):
return render_to_response('edit_subsection.html',
{'subsection': item,
'context_course': course,
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
'new_unit_category': 'vertical',
'lms_link': lms_link,
'preview_link': preview_link,
'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
......@@ -134,10 +136,26 @@ def edit_unit(request, location):
item = modulestore().get_item(location, depth=1)
except ItemNotFoundError:
return HttpResponseBadRequest()
lms_link = get_lms_link_for_item(item.location, course_id=course.location.course_id)
component_templates = defaultdict(list)
for category in COMPONENT_TYPES:
component_class = XModuleDescriptor.load_class(category)
# add the default template
component_templates[category].append((
component_class.display_name.default or 'Blank',
category,
False, # No defaults have markdown (hardcoded current default)
None # no boilerplate for overrides
))
# add boilerplates
for template in component_class.templates():
component_templates[category].append((
template['metadata'].get('display_name'),
category,
template['metadata'].get('markdown') is not None,
template.get('template_id')
))
# Check if there are any advanced modules specified in the course policy. These modules
# should be specified as a list of strings, where the strings are the names of the modules
......@@ -145,29 +163,29 @@ def edit_unit(request, location):
course_advanced_keys = course.advanced_modules
# Set component types according to course policy file
component_types = list(COMPONENT_TYPES)
if isinstance(course_advanced_keys, list):
course_advanced_keys = [c for c in course_advanced_keys if c in ADVANCED_COMPONENT_TYPES]
if len(course_advanced_keys) > 0:
component_types.append(ADVANCED_COMPONENT_CATEGORY)
for category in course_advanced_keys:
if category in ADVANCED_COMPONENT_TYPES:
# Do I need to allow for boilerplates or just defaults on the class? i.e., can an advanced
# have more than one entry in the menu? one for default and others for prefilled boilerplates?
try:
component_class = XModuleDescriptor.load_class(category)
component_templates['advanced'].append((
component_class.display_name.default or category,
category,
False,
None # don't override default data
))
except PluginMissingError:
# dhm: I got this once but it can happen any time the course author configures
# an advanced component which does not exist on the server. This code here merely
# prevents any authors from trying to instantiate the non-existent component type
# by not showing it in the menu
pass
else:
log.error("Improper format for course advanced keys! {0}".format(course_advanced_keys))
templates = modulestore().get_items(Location('i4x', 'edx', 'templates'))
for template in templates:
category = template.location.category
if category in course_advanced_keys:
category = ADVANCED_COMPONENT_CATEGORY
if category in component_types:
# This is a hack to create categories for different xmodules
component_templates[category].append((
template.display_name_with_default,
template.location.url(),
hasattr(template, 'markdown') and template.markdown is not None
))
components = [
component.location.url()
for component
......@@ -219,7 +237,7 @@ def edit_unit(request, location):
'subsection': containing_subsection,
'release_date': get_default_time_display(containing_subsection.lms.start) if containing_subsection.lms.start is not None else None,
'section': containing_section,
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
'new_unit_category': 'vertical',
'unit_state': unit_state,
'published_date': get_default_time_display(item.cms.published_date) if item.cms.published_date is not None else None
})
......@@ -253,7 +271,7 @@ def create_draft(request):
# This clones the existing item location to a draft location (the draft is implicit,
# because modulestore is a Draft modulestore)
modulestore().clone_item(location, location)
modulestore().convert_to_draft(location)
return HttpResponse()
......
......@@ -45,6 +45,7 @@ from .component import (
from django_comment_common.utils import seed_permissions_roles
import datetime
from django.utils.timezone import UTC
from xmodule.html_module import AboutDescriptor
__all__ = ['course_index', 'create_new_course', 'course_info',
'course_info_updates', 'get_course_settings',
'course_config_graders_page',
......@@ -82,10 +83,11 @@ def course_index(request, org, course, name):
'sections': sections,
'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
'parent_location': course.location,
'new_section_template': Location('i4x', 'edx', 'templates', 'chapter', 'Empty'),
'new_subsection_template': Location('i4x', 'edx', 'templates', 'sequential', 'Empty'), # for now they are the same, but the could be different at some point...
'new_section_category': 'chapter',
'new_subsection_category': 'sequential',
'upload_asset_callback_url': upload_asset_callback_url,
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty')
'new_unit_category': 'vertical',
'category': 'vertical'
})
......@@ -98,12 +100,6 @@ def create_new_course(request):
if not is_user_in_creator_group(request.user):
raise PermissionDenied()
# This logic is repeated in xmodule/modulestore/tests/factories.py
# so if you change anything here, you need to also change it there.
# TODO: write a test that creates two courses, one with the factory and
# the other with this method, then compare them to make sure they are
# equivalent.
template = Location(request.POST['template'])
org = request.POST.get('org')
number = request.POST.get('number')
display_name = request.POST.get('display_name')
......@@ -121,29 +117,31 @@ def create_new_course(request):
existing_course = modulestore('direct').get_item(dest_location)
except ItemNotFoundError:
pass
if existing_course is not None:
return JsonResponse({'ErrMsg': 'There is already a course defined with this name.'})
course_search_location = ['i4x', dest_location.org, dest_location.course, 'course', None]
courses = modulestore().get_items(course_search_location)
if len(courses) > 0:
return JsonResponse({'ErrMsg': 'There is already a course defined with the same organization and course number.'})
new_course = modulestore('direct').clone_item(template, dest_location)
# clone a default 'about' module as well
about_template_location = Location(['i4x', 'edx', 'templates', 'about', 'overview'])
dest_about_location = dest_location._replace(category='about', name='overview')
modulestore('direct').clone_item(about_template_location, dest_about_location)
if display_name is not None:
new_course.display_name = display_name
# set a default start date to now
new_course.start = datetime.datetime.now(UTC())
# instantiate the CourseDescriptor and then persist it
# note: no system to pass
if display_name is None:
metadata = {}
else:
metadata = {'display_name': display_name}
modulestore('direct').create_and_save_xmodule(dest_location, metadata=metadata)
new_course = modulestore('direct').get_item(dest_location)
# clone a default 'about' overview module as well
dest_about_location = dest_location.replace(category='about', name='overview')
overview_template = AboutDescriptor.get_template('overview.yaml')
modulestore('direct').create_and_save_xmodule(
dest_about_location,
system=new_course.system,
definition_data=overview_template.get('data')
)
initialize_course_tabs(new_course)
......
......@@ -13,16 +13,26 @@ from util.json_request import expect_json
from ..utils import get_modulestore
from .access import has_access
from .requests import _xmodule_recurse
from xmodule.x_module import XModuleDescriptor
__all__ = ['save_item', 'clone_item', 'delete_item']
__all__ = ['save_item', 'create_item', 'delete_item']
# cdodge: these are categories which should not be parented, they are detached from the hierarchy
DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
@login_required
@expect_json
def save_item(request):
"""
Will carry a json payload with these possible fields
:id (required): the id
:data (optional): the new value for the data
:metadata (optional): new values for the metadata fields.
Any whose values are None will be deleted not set to None! Absent ones will be left alone
:nullout (optional): which metadata fields to set to None
"""
# The nullout is a bit of a temporary copout until we can make module_edit.coffee and the metadata editors a
# little smarter and able to pass something more akin to {unset: [field, field]}
item_location = request.POST['id']
# check permissions for this user within this course
......@@ -42,30 +52,25 @@ def save_item(request):
children = request.POST['children']
store.update_children(item_location, children)
# cdodge: also commit any metadata which might have been passed along in the
# POST from the client, if it is there
# NOTE, that the postback is not the complete metadata, as there's system metadata which is
# not presented to the end-user for editing. So let's fetch the original and
# 'apply' the submitted metadata, so we don't end up deleting system metadata
if request.POST.get('metadata') is not None:
posted_metadata = request.POST['metadata']
# fetch original
# cdodge: also commit any metadata which might have been passed along
if request.POST.get('nullout') is not None or request.POST.get('metadata') is not None:
# the postback is not the complete metadata, as there's system metadata which is
# not presented to the end-user for editing. So let's fetch the original and
# 'apply' the submitted metadata, so we don't end up deleting system metadata
existing_item = modulestore().get_item(item_location)
for metadata_key in request.POST.get('nullout', []):
setattr(existing_item, metadata_key, None)
# update existing metadata with submitted metadata (which can be partial)
# IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
for metadata_key, value in posted_metadata.items():
if posted_metadata[metadata_key] is None:
# remove both from passed in collection as well as the collection read in from the modulestore
if metadata_key in existing_item._model_data:
del existing_item._model_data[metadata_key]
del posted_metadata[metadata_key]
else:
existing_item._model_data[metadata_key] = value
# IMPORTANT NOTE: if the client passed 'null' (None) for a piece of metadata that means 'remove it'. If
# the intent is to make it None, use the nullout field
for metadata_key, value in request.POST.get('metadata', {}).items():
if value is None:
delattr(existing_item, metadata_key)
else:
setattr(existing_item, metadata_key, value)
# commit to datastore
# TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
store.update_metadata(item_location, own_metadata(existing_item))
return HttpResponse()
......@@ -73,28 +78,38 @@ def save_item(request):
@login_required
@expect_json
def clone_item(request):
def create_item(request):
parent_location = Location(request.POST['parent_location'])
template = Location(request.POST['template'])
category = request.POST['category']
display_name = request.POST.get('display_name')
if not has_access(request.user, parent_location):
raise PermissionDenied()
parent = get_modulestore(template).get_item(parent_location)
dest_location = parent_location._replace(category=template.category, name=uuid4().hex)
new_item = get_modulestore(template).clone_item(template, dest_location)
parent = get_modulestore(category).get_item(parent_location)
dest_location = parent_location.replace(category=category, name=uuid4().hex)
# get the metadata, display_name, and definition from the request
metadata = {}
data = None
template_id = request.POST.get('boilerplate')
if template_id is not None:
clz = XModuleDescriptor.load_class(category)
if clz is not None:
template = clz.get_template(template_id)
if template is not None:
metadata = template.get('metadata', {})
data = template.get('data')
# replace the display name with an optional parameter passed in from the caller
if display_name is not None:
new_item.display_name = display_name
metadata['display_name'] = display_name
get_modulestore(template).update_metadata(new_item.location.url(), own_metadata(new_item))
get_modulestore(category).create_and_save_xmodule(dest_location, definition_data=data,
metadata=metadata, system=parent.system)
if new_item.location.category not in DETACHED_CATEGORIES:
get_modulestore(parent.location).update_children(parent_location, parent.children + [new_item.location.url()])
if category not in DETACHED_CATEGORIES:
get_modulestore(parent.location).update_children(parent_location, parent.children + [dest_location.url()])
return HttpResponse(json.dumps({'id': dest_location.url()}))
......
......@@ -13,7 +13,7 @@ from xmodule.modulestore.django import modulestore
from ..utils import get_course_for_item, get_modulestore
from .access import get_location_and_verify_access
__all__ = ['edit_tabs', 'reorder_static_tabs', 'static_pages', 'edit_static']
__all__ = ['edit_tabs', 'reorder_static_tabs', 'static_pages']
def initialize_course_tabs(course):
......@@ -127,7 +127,3 @@ def static_pages(request, org, course, coursename):
return render_to_response('static-pages.html', {
'context_course': course,
})
def edit_static(request, org, course, coursename):
return render_to_response('edit-static-page.html', {})
......@@ -2,6 +2,7 @@ from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse
from django.contrib.auth.decorators import login_required
from django.utils.translation import ugettext as _
from django_future.csrf import ensure_csrf_cookie
from mitxmako.shortcuts import render_to_response
......@@ -26,6 +27,7 @@ def index(request):
# filter out courses that we don't have access too
def course_filter(course):
return (has_access(request.user, course.location)
# TODO remove this condition when templates purged from db
and course.location.course != 'templates'
and course.location.org != ''
and course.location.course != ''
......@@ -33,7 +35,6 @@ def index(request):
courses = filter(course_filter, courses)
return render_to_response('index.html', {
'new_course_template': Location('i4x', 'edx', 'templates', 'course', 'Empty'),
'courses': [(course.display_name,
get_url_reverse('CourseOutline', course),
get_lms_link_for_item(course.location, course_id=course.location.course_id))
......@@ -78,7 +79,7 @@ def add_user(request, location):
if not email:
msg = {
'Status': 'Failed',
'ErrMsg': 'Please specify an email address.',
'ErrMsg': _('Please specify an email address.'),
}
return JsonResponse(msg, 400)
......@@ -92,7 +93,7 @@ def add_user(request, location):
if user is None:
msg = {
'Status': 'Failed',
'ErrMsg': "Could not find user by email address '{0}'.".format(email),
'ErrMsg': _("Could not find user by email address '{email}'.").format(email=email),
}
return JsonResponse(msg, 404)
......@@ -100,7 +101,7 @@ def add_user(request, location):
if not user.is_active:
msg = {
'Status': 'Failed',
'ErrMsg': 'User {0} has registered but has not yet activated his/her account.'.format(email),
'ErrMsg': _('User {email} has registered but has not yet activated his/her account.').format(email=email),
}
return JsonResponse(msg, 400)
......@@ -129,7 +130,7 @@ def remove_user(request, location):
if user is None:
msg = {
'Status': 'Failed',
'ErrMsg': "Could not find user by email address '{0}'.".format(email),
'ErrMsg': _("Could not find user by email address '{email}'.").format(email=email),
}
return JsonResponse(msg, 404)
......
......@@ -9,7 +9,7 @@ class CourseGradingModel(object):
"""
def __init__(self, course_descriptor):
self.course_location = course_descriptor.location
self.graders = [CourseGradingModel.jsonize_grader(i, grader) for i, grader in enumerate(course_descriptor.raw_grader)] # weights transformed to ints [0..100]
self.graders = [CourseGradingModel.jsonize_grader(i, grader) for i, grader in enumerate(course_descriptor.raw_grader)] # weights transformed to ints [0..100]
self.grade_cutoffs = course_descriptor.grade_cutoffs
self.grace_period = CourseGradingModel.convert_set_grace_period(course_descriptor)
......@@ -81,7 +81,7 @@ class CourseGradingModel(object):
Decode the json into CourseGradingModel and save any changes. Returns the modified model.
Probably not the usual path for updates as it's too coarse grained.
"""
course_location = jsondict['course_location']
course_location = Location(jsondict['course_location'])
descriptor = get_modulestore(course_location).get_item(course_location)
graders_parsed = [CourseGradingModel.parse_grader(jsonele) for jsonele in jsondict['graders']]
......@@ -89,7 +89,7 @@ class CourseGradingModel(object):
descriptor.raw_grader = graders_parsed
descriptor.grade_cutoffs = jsondict['grade_cutoffs']
get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data)
get_modulestore(course_location).update_item(course_location, descriptor.xblock_kvs._data)
CourseGradingModel.update_grace_period_from_json(course_location, jsondict['grace_period'])
return CourseGradingModel.fetch(course_location)
......@@ -209,7 +209,7 @@ class CourseGradingModel(object):
descriptor = get_modulestore(location).get_item(location)
return {"graderType": descriptor.lms.format if descriptor.lms.format is not None else 'Not Graded',
"location": location,
"id": 99 # just an arbitrary value to
"id": 99 # just an arbitrary value to
}
@staticmethod
......@@ -232,7 +232,7 @@ class CourseGradingModel(object):
# 5 hours 59 minutes 59 seconds => converted to iso format
rawgrace = descriptor.lms.graceperiod
if rawgrace:
hours_from_days = rawgrace.days*24
hours_from_days = rawgrace.days * 24
seconds = rawgrace.seconds
hours_from_seconds = int(seconds / 3600)
hours = hours_from_days + hours_from_seconds
......
......@@ -80,6 +80,8 @@ CELERY_QUEUES = {
with open(ENV_ROOT / CONFIG_PREFIX + "env.json") as env_file:
ENV_TOKENS = json.load(env_file)
EMAIL_BACKEND = ENV_TOKENS.get('EMAIL_BACKEND', EMAIL_BACKEND)
EMAIL_FILE_PATH = ENV_TOKENS.get('EMAIL_FILE_PATH', None)
LMS_BASE = ENV_TOKENS.get('LMS_BASE')
# Note that MITX_FEATURES['PREVIEW_LMS_BASE'] gets read in from the environment file.
......
......@@ -368,3 +368,5 @@ MKTG_URL_LINK_MAP = {
'HONOR': 'honor',
'PRIVACY': 'privacy_edx',
}
COURSES_WITH_UNSAFE_CODE = []
......@@ -6,15 +6,15 @@
class="course-checklist"
<% } %>
id="<%= 'course-checklist' + checklistIndex %>">
<% var widthPercentage = 'width:' + percentChecked + '%;'; %>
<span class="viz viz-checklist-status"><span class="viz value viz-checklist-status-value" style="<%= widthPercentage %>">
<span class="int"><%= percentChecked %></span>% of checklist completed</span></span>
<span class="viz viz-checklist-status"><span class="viz value viz-checklist-status-value" style="width: <%= percentChecked %>%;">
<%= _.template(gettext("{number}% of checklists completed"), {number: '<span class="int">' + percentChecked + '</span>'}, {interpolate: /\{(.+?)\}/g}) %>
</span></span>
<header>
<h3 class="checklist-title title-2 is-selectable" title="Collapse/Expand this Checklist">
<i class="icon-caret-down ui-toggle-expansion"></i>
<%= checklistShortDescription %></h3>
<span class="checklist-status status">
Tasks Completed: <span class="status-count"><%= itemsChecked %></span>/<span class="status-amount"><%= items.length %></span>
<%= gettext("Tasks Completed:") %> <span class="status-count"><%= itemsChecked %></span>/<span class="status-amount"><%= items.length %></span>
<i class="icon-ok"></i>
</span>
</header>
......@@ -47,7 +47,7 @@
<li class="action-item">
<a href="<%= item['action_url'] %>" class="action action-primary"
<% if (item['action_external']) { %>
rel="external" title="This link will open in a new browser window/tab"
rel="external" title="<%= gettext("This link will open in a new browser window/tab") %>"
<% } %>
><%= item['action_text'] %></a>
</li>
......
<a href="#" class="edit-button"><span class="edit-icon"></span>Edit</a>
<h2>Course Handouts</h2>
<h2 class="title">Course Handouts</h2>
<%if (model.get('data') != null) { %>
<div class="handouts-content">
<%= model.get('data') %>
</div>
<% } else {%>
<p>You have no handouts defined</p>
<p>${_("You have no handouts defined")}</p>
<% } %>
<form class="edit-handouts-form" style="display: block;">
<div class="row">
......
{
"static_files": [
"../jsi18n/",
"js/vendor/RequireJS.js",
"js/vendor/jquery.min.js",
"js/vendor/jquery-ui.min.js",
......
......@@ -37,6 +37,10 @@ describe "CMS.Views.SystemFeedback", ->
@renderSpy = spyOn(CMS.Views.Alert.Confirmation.prototype, 'render').andCallThrough()
@showSpy = spyOn(CMS.Views.Alert.Confirmation.prototype, 'show').andCallThrough()
@hideSpy = spyOn(CMS.Views.Alert.Confirmation.prototype, 'hide').andCallThrough()
@clock = sinon.useFakeTimers()
afterEach ->
@clock.restore()
it "requires a type and an intent", ->
neither = =>
......@@ -80,8 +84,8 @@ describe "CMS.Views.SystemFeedback", ->
it "close button sends a .hide() message", ->
view = new CMS.Views.Alert.Confirmation(@options).show()
view.$(".action-close").click()
expect(@hideSpy).toHaveBeenCalled()
@clock.tick(900)
expect(view.$('.wrapper')).toBeHiding()
describe "CMS.Views.Prompt", ->
......@@ -98,9 +102,9 @@ describe "CMS.Views.Prompt", ->
view.hide()
# expect($("body")).not.toHaveClass("prompt-is-shown")
describe "CMS.Views.Notification.Saving", ->
describe "CMS.Views.Notification.Mini", ->
beforeEach ->
@view = new CMS.Views.Notification.Saving()
@view = new CMS.Views.Notification.Mini()
it "should have minShown set to 1250 by default", ->
expect(@view.options.minShown).toEqual(1250)
......
describe "Course Overview", ->
beforeEach ->
appendSetFixtures """
<script src="/static/js/vendor/date.js"></script>
"""
appendSetFixtures """
<script type="text/javascript" src="/jsi18n/"></script>
"""
appendSetFixtures """
<div class="section-published-date">
<span class="published-status">
<strong>Will Release:</strong> 06/12/2013 at 04:00 UTC
</span>
<a href="#" class="edit-button" "="" data-date="06/12/2013" data-time="04:00" data-id="i4x://pfogg/42/chapter/d6b47f7b084f49debcaf67fe5436c8e2">Edit</a>
</div>
"""#"
appendSetFixtures """
<div class="edit-subsection-publish-settings">
<div class="settings">
<h3>Section Release Date</h3>
<div class="picker datepair">
<div class="field field-start-date">
<label for="">Release Day</label>
<input class="start-date date" type="text" name="start_date" value="04/08/1990" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
</div>
<div class="field field-start-time">
<label for="">Release Time (<abbr title="Coordinated Universal Time">UTC</abbr>)</label>
<input class="start-time time" type="text" name="start_time" value="12:00" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
</div>
<div class="description">
<p>On the date set above, this section – <strong class="section-name"></strong> – will be released to students. Any units marked private will only be visible to admins.</p>
</div>
</div>
<a href="#" class="save-button">Save</a><a href="#" class="cancel-button">Cancel</a>
</div>
</div>
"""#"
spyOn(window, 'saveSetSectionScheduleDate').andCallThrough()
# Have to do this here, as it normally gets bound in document.ready()
$('a.save-button').click(saveSetSectionScheduleDate)
@notificationSpy = spyOn(CMS.Views.Notification.Mini.prototype, 'show').andCallThrough()
window.analytics = jasmine.createSpyObj('analytics', ['track'])
window.course_location_analytics = jasmine.createSpy()
sinon.useFakeXMLHttpRequest()
afterEach ->
delete window.analytics
delete window.course_location_analytics
it "should save model when save is clicked", ->
$('a.edit-button').click()
$('a.save-button').click()
expect(saveSetSectionScheduleDate).toHaveBeenCalled()
it "should show a confirmation on save", ->
$('a.edit-button').click()
$('a.save-button').click()
expect(@notificationSpy).toHaveBeenCalled()
......@@ -73,7 +73,7 @@ describe "CMS.Views.ShowTextbook", ->
@xhr = sinon.useFakeXMLHttpRequest()
@xhr.onCreate = (xhr) -> requests.push(xhr)
@savingSpies = spyOnConstructor(CMS.Views.Notification, "Saving",
@savingSpies = spyOnConstructor(CMS.Views.Notification, "Mini",
["show", "hide"])
@savingSpies.show.andReturn(@savingSpies)
......
......@@ -56,14 +56,15 @@ class CMS.Views.ModuleEdit extends Backbone.View
changedMetadata: ->
return _.extend(@metadataEditor.getModifiedMetadataValues(), @customMetadata())
cloneTemplate: (parent, template) ->
$.post("/clone_item", {
parent_location: parent
template: template
}, (data) =>
@model.set(id: data.id)
@$el.data('id', data.id)
@render()
createItem: (parent, payload) ->
payload.parent_location = parent
$.post(
"/create_item"
payload
(data) =>
@model.set(id: data.id)
@$el.data('id', data.id)
@render()
)
render: ->
......
......@@ -55,9 +55,9 @@ class CMS.Views.TabsEdit extends Backbone.View
editor.$el.removeClass('new')
, 500)
editor.cloneTemplate(
editor.createItem(
@model.get('id'),
'i4x://edx/templates/static_tab/Empty'
{category: 'static_tab'}
)
analytics.track "Added Static Page",
......
......@@ -89,9 +89,9 @@ class CMS.Views.UnitEdit extends Backbone.View
@$newComponentItem.before(editor.$el)
editor.cloneTemplate(
editor.createItem(
@$el.data('id'),
$(event.currentTarget).data('location')
$(event.currentTarget).data()
)
analytics.track "Added a Component",
......
......@@ -79,10 +79,10 @@ $(document).ready(function() {
});
// general link management - new window/tab
$('a[rel="external"]').attr('title', 'This link will open in a new browser window/tab').bind('click', linkNewWindow);
$('a[rel="external"]').attr('title', gettext('This link will open in a new browser window/tab')).bind('click', linkNewWindow);
// general link management - lean modal window
$('a[rel="modal"]').attr('title', 'This link will open in a modal window').leanModal({
$('a[rel="modal"]').attr('title', gettext('This link will open in a modal window')).leanModal({
overlay: 0.50,
closeButton: '.action-modal-close'
});
......@@ -199,8 +199,10 @@ function toggleSections(e) {
$section = $('.courseware-section');
sectionCount = $section.length;
$button = $(this);
$labelCollapsed = $('<i class="icon-arrow-up"></i> <span class="label">Collapse All Sections</span>');
$labelExpanded = $('<i class="icon-arrow-down"></i> <span class="label">Expand All Sections</span>');
$labelCollapsed = $('<i class="icon-arrow-up"></i> <span class="label">' +
gettext('Collapse All Sections') + '</span>');
$labelExpanded = $('<i class="icon-arrow-down"></i> <span class="label">' +
gettext('Expand All Sections') + '</span>');
var buttonLabel = $button.hasClass('is-activated') ? $labelCollapsed : $labelExpanded;
$button.toggleClass('is-activated').html(buttonLabel);
......@@ -326,7 +328,7 @@ function saveSubsection() {
$changedInput = null;
},
error: function() {
showToastMessage('There has been an error while saving your changes.');
showToastMessage(gettext('There has been an error while saving your changes.'));
}
});
}
......@@ -336,7 +338,7 @@ function createNewUnit(e) {
e.preventDefault();
var parent = $(this).data('parent');
var template = $(this).data('template');
var category = $(this).data('category');
analytics.track('Created a Unit', {
'course': course_location_analytics,
......@@ -344,9 +346,9 @@ function createNewUnit(e) {
});
$.post('/clone_item', {
$.post('/create_item', {
'parent_location': parent,
'template': template,
'category': category,
'display_name': 'New Unit'
},
......@@ -372,7 +374,7 @@ function deleteSection(e) {
}
function _deleteItem($el) {
if (!confirm('Are you sure you wish to delete this item. It cannot be reversed!')) return;
if (!confirm(gettext('Are you sure you wish to delete this item. It cannot be reversed!'))) return;
var id = $el.data('id');
......@@ -549,7 +551,7 @@ function saveNewSection(e) {
var $saveButton = $(this).find('.new-section-name-save');
var parent = $saveButton.data('parent');
var template = $saveButton.data('template');
var category = $saveButton.data('category');
var display_name = $(this).find('.new-section-name').val();
analytics.track('Created a Section', {
......@@ -557,9 +559,9 @@ function saveNewSection(e) {
'display_name': display_name
});
$.post('/clone_item', {
$.post('/create_item', {
'parent_location': parent,
'template': template,
'category': category,
'display_name': display_name,
},
......@@ -593,13 +595,12 @@ function saveNewCourse(e) {
e.preventDefault();
var $newCourse = $(this).closest('.new-course');
var template = $(this).find('.new-course-save').data('template');
var org = $newCourse.find('.new-course-org').val();
var number = $newCourse.find('.new-course-number').val();
var display_name = $newCourse.find('.new-course-name').val();
if (org == '' || number == '' || display_name == '') {
alert('You must specify all fields in order to create a new course.');
alert(gettext('You must specify all fields in order to create a new course.'));
return;
}
......@@ -610,7 +611,6 @@ function saveNewCourse(e) {
});
$.post('/create_new_course', {
'template': template,
'org': org,
'number': number,
'display_name': display_name
......@@ -644,7 +644,7 @@ function addNewSubsection(e) {
var parent = $(this).parents("section.branch").data("id");
$saveButton.data('parent', parent);
$saveButton.data('template', $(this).data('template'));
$saveButton.data('category', $(this).data('category'));
$newSubsection.find('.new-subsection-form').bind('submit', saveNewSubsection);
$cancelButton.bind('click', cancelNewSubsection);
......@@ -657,7 +657,7 @@ function saveNewSubsection(e) {
e.preventDefault();
var parent = $(this).find('.new-subsection-name-save').data('parent');
var template = $(this).find('.new-subsection-name-save').data('template');
var category = $(this).find('.new-subsection-name-save').data('category');
var display_name = $(this).find('.new-subsection-name-input').val();
analytics.track('Created a Subsection', {
......@@ -666,9 +666,9 @@ function saveNewSubsection(e) {
});
$.post('/clone_item', {
$.post('/create_item', {
'parent_location': parent,
'template': template,
'category': category,
'display_name': display_name
},
......@@ -712,6 +712,10 @@ function saveSetSectionScheduleDate(e) {
'start': start
});
var saving = new CMS.Views.Notification.Mini({
title: gettext("Saving") + "&hellip;",
});
saving.show();
// call into server to commit the new order
$.ajax({
url: "/save_item",
......@@ -726,28 +730,17 @@ function saveSetSectionScheduleDate(e) {
})
}).success(function() {
var $thisSection = $('.courseware-section[data-id="' + id + '"]');
var format = gettext('<strong>Will Release:</strong> %(date)s at %(time)s UTC');
var willReleaseAt = interpolate(format, {
'date': input_date,
'time': input_time
},
true);
$thisSection.find('.section-published-date').html(
'<span class="published-status">' + willReleaseAt + '</span>' +
'<a href="#" class="edit-button" ' +
'" data-date="' + input_date +
'" data-time="' + input_time +
'" data-id="' + id + '">' + gettext('Edit') + '</a>');
$thisSection.find('.section-published-date').animate({
'background-color': 'rgb(182,37,104)'
}, 300).animate({
'background-color': '#edf1f5'
}, 300).animate({
'background-color': 'rgb(182,37,104)'
}, 300).animate({
'background-color': '#edf1f5'
}, 300);
var html = _.template(
'<span class="published-status">' +
'<strong>' + gettext("Will Release: ") + '</strong>' +
gettext("<%= date %> at <%= time %> UTC") +
'</span>' +
'<a href="#" class="edit-button" data-date="<%= date %>" data-time="<%= time %>" data-id="<%= id %>">' +
gettext("Edit") +
'</a>',
{date: input_date, time: input_time, id: id});
$thisSection.find('.section-published-date').html(html);
hideModal();
saving.hide();
});
}
......@@ -22,8 +22,8 @@ CMS.Models.Section = Backbone.Model.extend({
},
showNotification: function() {
if(!this.msg) {
this.msg = new CMS.Views.Notification.Saving({
title: gettext("Saving&hellip;")
this.msg = new CMS.Views.Notification.Mini({
title: gettext("Saving") + "&hellip;"
});
}
this.msg.show();
......
......@@ -5,8 +5,6 @@ CMS.Models.Settings.Advanced = Backbone.Model.extend({
defaults: {
// the properties are whatever the user types in (in addition to whatever comes originally from the server)
},
// which keys to send as the deleted keys on next save
deleteKeys : [],
validate: function (attrs) {
// Keys can no longer be edited. We are currently not validating values.
......@@ -18,32 +16,8 @@ CMS.Models.Settings.Advanced = Backbone.Model.extend({
// add saveSuccess to the success
var success = options.success;
options.success = function(model, resp, options) {
model.afterSave(model);
if (success) success(model, resp, options);
};
Backbone.Model.prototype.save.call(this, attrs, options);
},
afterSave : function(self) {
// remove deleted attrs
if (!_.isEmpty(self.deleteKeys)) {
// remove the to be deleted keys from the returned model
_.each(self.deleteKeys, function(key) { self.unset(key); });
// not able to do via backbone since we're not destroying the model
$.ajax({
url : self.url,
// json to and fro
contentType : "application/json",
dataType : "json",
// delete
type : 'DELETE',
// data
data : JSON.stringify({ deleteKeys : self.deleteKeys})
})
.done(function(data, status, error) {
// clear deleteKeys on success
self.deleteKeys = [];
});
}
}
});
......@@ -38,23 +38,23 @@ CMS.Models.Settings.CourseDetails = Backbone.Model.extend({
// A bit funny in that the video key validation is asynchronous; so, it won't stop the validation.
var errors = {};
if (newattrs.start_date === null) {
errors.start_date = "The course must have an assigned start date.";
errors.start_date = gettext("The course must have an assigned start date.");
}
if (newattrs.start_date && newattrs.end_date && newattrs.start_date >= newattrs.end_date) {
errors.end_date = "The course end date cannot be before the course start date.";
errors.end_date = gettext("The course end date cannot be before the course start date.");
}
if (newattrs.start_date && newattrs.enrollment_start && newattrs.start_date < newattrs.enrollment_start) {
errors.enrollment_start = "The course start date cannot be before the enrollment start date.";
errors.enrollment_start = gettext("The course start date cannot be before the enrollment start date.");
}
if (newattrs.enrollment_start && newattrs.enrollment_end && newattrs.enrollment_start >= newattrs.enrollment_end) {
errors.enrollment_end = "The enrollment start date cannot be after the enrollment end date.";
errors.enrollment_end = gettext("The enrollment start date cannot be after the enrollment end date.");
}
if (newattrs.end_date && newattrs.enrollment_end && newattrs.end_date < newattrs.enrollment_end) {
errors.enrollment_end = "The enrollment end date cannot be after the course end date.";
errors.enrollment_end = gettext("The enrollment end date cannot be after the course end date.");
}
if (newattrs.intro_video && newattrs.intro_video !== this.get('intro_video')) {
if (this._videokey_illegal_chars.exec(newattrs.intro_video)) {
errors.intro_video = "Key should only contain letters, numbers, _, or -";
errors.intro_video = gettext("Key should only contain letters, numbers, _, or -");
}
// TODO check if key points to a real video using google's youtube api
}
......@@ -63,13 +63,13 @@ CMS.Models.Settings.CourseDetails = Backbone.Model.extend({
},
_videokey_illegal_chars : /[^a-zA-Z0-9_-]/g,
save_videosource: function(newsource) {
set_videosource: function(newsource) {
// newsource either is <video youtube="speed:key, *"/> or just the "speed:key, *" string
// returns the videosource for the preview which iss the key whose speed is closest to 1
if (_.isEmpty(newsource) && !_.isEmpty(this.get('intro_video'))) this.save({'intro_video': null});
if (_.isEmpty(newsource) && !_.isEmpty(this.get('intro_video'))) this.set({'intro_video': null}, {validate: true});
// TODO remove all whitespace w/in string
else {
if (this.get('intro_video') !== newsource) this.save('intro_video', newsource);
if (this.get('intro_video') !== newsource) this.set('intro_video', newsource, {validate: true});
}
return this.videosourceSample();
......
......@@ -71,24 +71,25 @@ CMS.Models.Settings.CourseGrader = Backbone.Model.extend({
},
validate : function(attrs) {
var errors = {};
if (attrs['type']) {
if (_.has(attrs, 'type')) {
if (_.isEmpty(attrs['type'])) {
errors.type = "The assignment type must have a name.";
}
else {
// FIXME somehow this.collection is unbound sometimes. I can't track down when
var existing = this.collection && this.collection.some(function(other) { return (other != this) && (other.get('type') == attrs['type']);}, this);
var existing = this.collection && this.collection.some(function(other) { return (other.cid != this.cid) && (other.get('type') == attrs['type']);}, this);
if (existing) {
errors.type = "There's already another assignment type with this name.";
errors.type = gettext("There's already another assignment type with this name.");
}
}
}
if (attrs['weight']) {
if (!isFinite(attrs.weight) || /\D+/.test(attrs.weight)) {
errors.weight = "Please enter an integer between 0 and 100.";
if (_.has(attrs, 'weight')) {
var intWeight = parseInt(attrs.weight); // see if this ensures value saved is int
if (!isFinite(intWeight) || /\D+/.test(attrs.weight) || intWeight < 0 || intWeight > 100) {
errors.weight = gettext("Please enter an integer between 0 and 100.");
}
else {
attrs.weight = parseInt(attrs.weight); // see if this ensures value saved is int
attrs.weight = intWeight;
if (this.collection && attrs.weight > 0) {
// FIXME b/c saves don't update the models if validation fails, we should
// either revert the field value to the one in the model and make them make room
......@@ -97,20 +98,22 @@ CMS.Models.Settings.CourseGrader = Backbone.Model.extend({
// errors.weight = "The weights cannot add to more than 100.";
}
}}
if (attrs['min_count']) {
if (_.has(attrs, 'min_count')) {
if (!isFinite(attrs.min_count) || /\D+/.test(attrs.min_count)) {
errors.min_count = "Please enter an integer.";
errors.min_count = gettext("Please enter an integer.");
}
else attrs.min_count = parseInt(attrs.min_count);
}
if (attrs['drop_count']) {
if (_.has(attrs, 'drop_count')) {
if (!isFinite(attrs.drop_count) || /\D+/.test(attrs.drop_count)) {
errors.drop_count = "Please enter an integer.";
errors.drop_count = gettext("Please enter an integer.");
}
else attrs.drop_count = parseInt(attrs.drop_count);
}
if (attrs['min_count'] && attrs['drop_count'] && attrs.drop_count > attrs.min_count) {
errors.drop_count = "Cannot drop more " + attrs.type + " than will assigned.";
if (_.has(attrs, 'min_count') && _.has(attrs, 'drop_count') && attrs.drop_count > attrs.min_count) {
errors.drop_count = _.template(
gettext("Cannot drop more <% attrs.types %> than will assigned."),
attrs, {variable: 'attrs'});
}
if (!_.isEmpty(errors)) return errors;
}
......
......@@ -140,7 +140,21 @@ CMS.Views.SystemFeedback = Backbone.View.extend({
CMS.Views.Alert = CMS.Views.SystemFeedback.extend({
options: $.extend({}, CMS.Views.SystemFeedback.prototype.options, {
type: "alert"
})
}),
slide_speed: 900,
show: function() {
CMS.Views.SystemFeedback.prototype.show.apply(this, arguments);
this.$el.hide();
this.$el.slideDown(this.slide_speed);
return this;
},
hide: function () {
this.$el.slideUp({
duration: this.slide_speed
});
setTimeout(_.bind(CMS.Views.SystemFeedback.prototype.hide, this, arguments),
this.slideSpeed);
}
});
CMS.Views.Notification = CMS.Views.SystemFeedback.extend({
options: $.extend({}, CMS.Views.SystemFeedback.prototype.options, {
......@@ -171,7 +185,7 @@ CMS.Views.Prompt = CMS.Views.SystemFeedback.extend({
var capitalCamel, types, intents;
capitalCamel = _.compose(_.str.capitalize, _.str.camelize);
types = ["alert", "notification", "prompt"];
intents = ["warning", "error", "confirmation", "announcement", "step-required", "help", "saving"];
intents = ["warning", "error", "confirmation", "announcement", "step-required", "help", "mini"];
_.each(types, function(type) {
_.each(intents, function(intent) {
// "class" is a reserved word in Javascript, so use "klass" instead
......@@ -187,8 +201,7 @@ _.each(types, function(type) {
});
});
// set more sensible defaults for Notification-Saving views
var savingOptions = CMS.Views.Notification.Saving.prototype.options;
savingOptions.minShown = 1250;
savingOptions.closeIcon = false;
// set more sensible defaults for Notification-Mini views
var miniOptions = CMS.Views.Notification.Mini.prototype.options;
miniOptions.minShown = 1250;
miniOptions.closeIcon = false;
......@@ -56,9 +56,13 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
CodeMirror.fromTextArea(textarea, {
mode: "application/json", lineNumbers: false, lineWrapping: false,
onChange: function(instance, changeobj) {
instance.save()
// this event's being called even when there's no change :-(
if (instance.getValue() !== oldValue && !self.notificationBarShowing) {
self.showNotificationBar();
if (instance.getValue() !== oldValue) {
var message = gettext("Your changes will not take effect until you save your progress. Take care with key and value formatting, as validation is not implemented.");
self.showNotificationBar(message,
_.bind(self.saveView, self),
_.bind(self.revertView, self));
}
},
onFocus : function(mirror) {
......@@ -91,44 +95,11 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
}
}
if (JSONValue !== undefined) {
self.clearValidationErrors();
self.model.set(key, JSONValue, {validate: true});
self.model.set(key, JSONValue);
}
}
});
},
showNotificationBar: function() {
var self = this;
var message = gettext("Your changes will not take effect until you save your progress. Take care with key and value formatting, as validation is not implemented.")
var confirm = new CMS.Views.Notification.Warning({
title: gettext("You've Made Some Changes"),
message: message,
actions: {
primary: {
"text": gettext("Save Changes"),
"class": "action-save",
"click": function() {
self.saveView();
confirm.hide();
self.notificationBarShowing = false;
}
},
secondary: [{
"text": gettext("Cancel"),
"class": "action-cancel",
"click": function() {
self.revertView();
confirm.hide();
self.notificationBarShowing = false;
}
}]
}});
this.notificationBarShowing = true;
confirm.show();
if(this.saved) {
this.saved.hide();
}
},
saveView : function() {
// TODO one last verification scan:
// call validateKey on each to ensure proper format
......@@ -138,25 +109,20 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
{
success : function() {
self.render();
var title = gettext("Your policy changes have been saved.");
var message = gettext("Please note that validation of your policy key and value pairs is not currently in place yet. If you are having difficulties, please review your policy pairs.");
self.saved = new CMS.Views.Alert.Confirmation({
title: gettext("Your policy changes have been saved."),
message: message,
closeIcon: false
});
self.saved.show();
self.showSavedBar(title, message);
analytics.track('Saved Advanced Settings', {
'course': course_location_analytics
});
}
},
silent: true
});
},
revertView : function() {
revertView: function() {
var self = this;
this.model.deleteKeys = [];
this.model.clear({silent : true});
this.model.fetch({
success : function() { self.render(); },
success: function() { self.render(); },
reset: true
});
},
......
......@@ -3,6 +3,9 @@ if (!CMS.Views['Settings']) CMS.Views.Settings = {}; // ensure the pseudo pkg ex
CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
// Model class is CMS.Models.Settings.CourseGradingPolicy
events : {
"input input" : "updateModel",
"input textarea" : "updateModel",
// Leaving change in as fallback for older browsers
"change input" : "updateModel",
"change textarea" : "updateModel",
"change span[contenteditable=true]" : "updateDesignation",
......@@ -23,14 +26,7 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
'<% if (removable) {%><a href="#" class="remove-button">remove</a><% ;} %>' +
'</li>');
// Instrument grading scale
// convert cutoffs to inversely ordered list
var modelCutoffs = this.model.get('grade_cutoffs');
for (var cutoff in modelCutoffs) {
this.descendingCutoffs.push({designation: cutoff, cutoff: Math.round(modelCutoffs[cutoff] * 100)});
}
this.descendingCutoffs = _.sortBy(this.descendingCutoffs,
function (gradeEle) { return -gradeEle['cutoff']; });
this.setupCutoffs();
// Instrument grace period
this.$el.find('#course-grading-graceperiod').timepicker();
......@@ -45,7 +41,7 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
}
);
this.listenTo(this.model, 'invalid', this.handleValidationError);
this.model.get('graders').on('remove', this.render, this);
this.listenTo(this.model, 'change', this.showNotificationBar);
this.model.get('graders').on('reset', this.render, this);
this.model.get('graders').on('add', this.render, this);
this.selectorToField = _.invert(this.fieldToSelectorMap);
......@@ -61,11 +57,31 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
// Undo the double invocation error. At some point, fix the double invocation
$(gradelist).empty();
var gradeCollection = this.model.get('graders');
// We need to bind these events here (rather than in
// initialize), or else we can only press the delete button
// once due to the graders collection changing when we cancel
// our changes.
_.each(['change', 'remove', 'add'],
function (event) {
gradeCollection.on(event, function() {
this.showNotificationBar();
// Since the change event gets fired every time
// we type in an input field, we don't need to
// (and really shouldn't) rerender the whole view.
if(event !== 'change') {
this.render();
}
}, this);
},
this);
gradeCollection.each(function(gradeModel) {
$(gradelist).append(self.template({model : gradeModel }));
var newEle = gradelist.children().last();
var newView = new CMS.Views.Settings.GraderView({el: newEle,
model : gradeModel, collection : gradeCollection });
// Listen in order to rerender when the 'cancel' button is
// pressed
self.listenTo(newView, 'revert', _.bind(self.render, self));
});
// render the grade cutoffs
......@@ -88,9 +104,10 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
'grace_period' : 'course-grading-graceperiod'
},
setGracePeriod : function(event) {
event.data.clearValidationErrors();
var newVal = event.data.model.dateToGracePeriod($(event.currentTarget).timepicker('getTime'));
if (event.data.model.get('grace_period') != newVal) event.data.model.save('grace_period', newVal);
var self = event.data;
self.clearValidationErrors();
var newVal = self.model.dateToGracePeriod($(event.currentTarget).timepicker('getTime'));
self.model.set('grace_period', newVal, {validate: true});
},
updateModel : function(event) {
if (!this.selectorToField[event.currentTarget.id]) return;
......@@ -100,8 +117,8 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
break;
default:
this.saveIfChanged(event);
break;
this.setField(event);
break;
}
},
......@@ -220,13 +237,14 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
},
saveCutoffs: function() {
this.model.save('grade_cutoffs',
this.model.set('grade_cutoffs',
_.reduce(this.descendingCutoffs,
function(object, cutoff) {
object[cutoff['designation']] = cutoff['cutoff'] / 100.0;
return object;
},
{}));
{}),
{validate: true});
},
addNewGrade: function(e) {
......@@ -301,13 +319,45 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
},
setTopGradeLabel: function() {
this.$el.find('.grades .letter-grade').first().html(this.descendingCutoffs[0]['designation']);
},
setupCutoffs: function() {
// Instrument grading scale
// convert cutoffs to inversely ordered list
var modelCutoffs = this.model.get('grade_cutoffs');
for (var cutoff in modelCutoffs) {
this.descendingCutoffs.push({designation: cutoff, cutoff: Math.round(modelCutoffs[cutoff] * 100)});
}
this.descendingCutoffs = _.sortBy(this.descendingCutoffs,
function (gradeEle) { return -gradeEle['cutoff']; });
},
revertView: function() {
var self = this;
this.model.fetch({
success: function() {
self.descendingCutoffs = [];
self.setupCutoffs();
self.render();
self.renderCutoffBar();
},
reset: true,
silent: true});
},
showNotificationBar: function() {
// We always call showNotificationBar with the same args, just
// delegate to superclass
CMS.Views.ValidatingView.prototype.showNotificationBar.call(this,
this.save_message,
_.bind(this.saveView, this),
_.bind(this.revertView, this));
}
});
CMS.Views.Settings.GraderView = CMS.Views.ValidatingView.extend({
// Model class is CMS.Models.Settings.CourseGrader
events : {
"input input" : "updateModel",
"input textarea" : "updateModel",
// Leaving change in as fallback for older browsers
"change input" : "updateModel",
"change textarea" : "updateModel",
"click .remove-grading-data" : "deleteModel",
......@@ -331,7 +381,7 @@ CMS.Views.Settings.GraderView = CMS.Views.ValidatingView.extend({
'drop_count' : 'course-grading-assignment-droppable',
'weight' : 'course-grading-assignment-gradeweight'
},
updateModel : function(event) {
updateModel: function(event) {
// HACK to fix model sometimes losing its pointer to the collection [I think I fixed this but leaving
// this in out of paranoia. If this error ever happens, the user will get a warning that they cannot
// give 2 assignments the same name.]
......@@ -342,26 +392,27 @@ CMS.Views.Settings.GraderView = CMS.Views.ValidatingView.extend({
switch (event.currentTarget.id) {
case 'course-grading-assignment-totalassignments':
this.$el.find('#course-grading-assignment-droppable').attr('max', $(event.currentTarget).val());
this.saveIfChanged(event);
this.setField(event);
break;
case 'course-grading-assignment-name':
var oldName = this.model.get('type');
if (this.saveIfChanged(event) && !_.isEmpty(oldName)) {
// Keep the original name, until we save
this.oldName = this.oldName === undefined ? this.model.get('type') : this.oldName;
// If the name has changed, alert the user to change all subsection names.
if (this.setField(event) != this.oldName && !_.isEmpty(this.oldName)) {
// overload the error display logic
this._cacheValidationErrors.push(event.currentTarget);
$(event.currentTarget).parent().append(
this.errorTemplate({message : 'For grading to work, you must change all "' + oldName +
this.errorTemplate({message : 'For grading to work, you must change all "' + this.oldName +
'" subsections to "' + this.model.get('type') + '".'}));
}
break;
default:
this.saveIfChanged(event);
this.setField(event);
break;
}
},
deleteModel : function(e) {
this.model.destroy();
e.preventDefault();
this.collection.remove(this.model);
}
});
......@@ -26,16 +26,16 @@ CMS.Views.ShowTextbook = Backbone.View.extend({
if(e && e.preventDefault) { e.preventDefault(); }
var textbook = this.model, collection = this.model.collection;
var msg = new CMS.Views.Prompt.Warning({
title: _.str.sprintf(gettext("Delete “%s”?"),
textbook.escape('name')),
title: _.template(gettext("Delete “<%= name %>”?"),
{name: textbook.escape('name')}),
message: gettext("Deleting a textbook cannot be undone and once deleted any reference to it in your courseware's navigation will also be removed."),
actions: {
primary: {
text: gettext("Delete"),
click: function(view) {
view.hide();
var delmsg = new CMS.Views.Notification.Saving({
title: gettext("Deleting&hellip;")
var delmsg = new CMS.Views.Notification.Mini({
title: gettext("Deleting") + "&hellip;"
}).show();
textbook.destroy({
complete: function() {
......@@ -121,8 +121,8 @@ CMS.Views.EditTextbook = Backbone.View.extend({
if(e && e.preventDefault) { e.preventDefault(); }
this.setValues();
if(!this.model.isValid()) { return; }
var saving = new CMS.Views.Notification.Saving({
title: gettext("Saving&hellip;")
var saving = new CMS.Views.Notification.Mini({
title: gettext("Saving") + "&hellip;"
}).show();
var that = this;
this.model.save({}, {
......@@ -241,8 +241,8 @@ CMS.Views.EditChapter = Backbone.View.extend({
asset_path: this.$("input.chapter-asset-path").val()
});
var msg = new CMS.Models.FileUpload({
title: _.str.sprintf(gettext("Upload a new asset to %s"),
section.escape('name')),
title: _.template(gettext("Upload a new asset to “<%= name %>”"),
{name: section.escape('name')}),
message: "Files must be in PDF format."
});
var view = new CMS.Views.UploadDialog({model: msg, chapter: this.model});
......@@ -260,7 +260,7 @@ CMS.Views.UploadDialog = Backbone.View.extend({
this.listenTo(this.model, "change", this.render);
},
render: function() {
var isValid = this.model.isValid()
var isValid = this.model.isValid();
var selectedFile = this.model.get('selectedFile');
var oldInput = this.$("input[type=file]").get(0);
this.$el.html(this.template({
......
......@@ -9,6 +9,11 @@ CMS.Views.ValidatingView = Backbone.View.extend({
errorTemplate : _.template('<span class="message-error"><%= message %></span>'),
save_title: gettext("You've made some changes"),
save_message: gettext("Your changes will not take effect until you save your progress."),
error_title: gettext("You've made some changes, but there are some errors"),
error_message: gettext("Please address the errors on this page first, and then save your progress."),
events : {
"change input" : "clearValidationErrors",
"change textarea" : "clearValidationErrors"
......@@ -20,6 +25,7 @@ CMS.Views.ValidatingView = Backbone.View.extend({
_cacheValidationErrors : [],
handleValidationError : function(model, error) {
this.clearValidationErrors();
// error is object w/ fields and error strings
for (var field in error) {
var ele = this.$el.find('#' + this.fieldToSelectorMap[field]);
......@@ -27,6 +33,11 @@ CMS.Views.ValidatingView = Backbone.View.extend({
this.getInputElements(ele).addClass('error');
$(ele).parent().append(this.errorTemplate({message : error[field]}));
}
$('.wrapper-notification-warning').addClass('wrapper-notification-warning-w-errors');
$('.action-save').addClass('is-disabled');
// TODO: (pfogg) should this text fade in/out on change?
$('#notification-warning-title').text(this.error_title);
$('#notification-warning-description').text(this.error_message);
},
clearValidationErrors : function() {
......@@ -36,19 +47,20 @@ CMS.Views.ValidatingView = Backbone.View.extend({
this.getInputElements(ele).removeClass('error');
$(ele).nextAll('.message-error').remove();
}
$('.wrapper-notification-warning').removeClass('wrapper-notification-warning-w-errors');
$('.action-save').removeClass('is-disabled');
$('#notification-warning-title').text(this.save_title);
$('#notification-warning-description').text(this.save_message);
},
saveIfChanged : function(event) {
// returns true if the value changed and was thus sent to server
setField : function(event) {
// Set model field and return the new value.
this.clearValidationErrors();
var field = this.selectorToField[event.currentTarget.id];
var currentVal = this.model.get(field);
var newVal = $(event.currentTarget).val();
this.clearValidationErrors(); // curr = new if user reverts manually
if (currentVal != newVal) {
this.model.save(field, newVal);
return true;
}
else return false;
this.model.set(field, newVal);
this.model.isValid();
return newVal;
},
// these should perhaps go into a superclass but lack of event hash inheritance demotivates me
inputFocus : function(event) {
......@@ -67,5 +79,79 @@ CMS.Views.ValidatingView = Backbone.View.extend({
// put error on the contained inputs
return $(ele).find(inputElements);
}
},
showNotificationBar: function(message, primaryClick, secondaryClick) {
// Show a notification with message. primaryClick is called on
// pressing the save button, and secondaryClick (if it's
// passed, which it may not be) will be called on
// cancel. Takes care of hiding the notification bar at the
// appropriate times.
if(this.notificationBarShowing) {
return;
}
// If we've already saved something, hide the alert.
if(this.saved) {
this.saved.hide();
}
var self = this;
this.confirmation = new CMS.Views.Notification.Warning({
title: this.save_title,
message: message,
actions: {
primary: {
"text": gettext("Save Changes"),
"class": "action-save",
"click": function() {
primaryClick();
self.confirmation.hide();
self.notificationBarShowing = false;
}
},
secondary: [{
"text": gettext("Cancel"),
"class": "action-cancel",
"click": function() {
if(secondaryClick) {
secondaryClick();
}
self.model.clear({silent : true});
self.confirmation.hide();
self.notificationBarShowing = false;
}
}]
}});
this.notificationBarShowing = true;
this.confirmation.show();
// Make sure the bar is in the right state
this.model.isValid();
},
showSavedBar: function(title, message) {
var defaultTitle = gettext('Your changes have been saved.');
this.saved = new CMS.Views.Alert.Confirmation({
title: title || defaultTitle,
message: message,
closeIcon: false
});
this.saved.show();
$.smoothScroll({
offset: 0,
easing: 'swing',
speed: 1000
});
},
saveView: function() {
var self = this;
this.model.save(
{},
{
success: function() {
self.showSavedBar();
},
silent: true
}
);
}
});
......@@ -576,7 +576,7 @@ p, ul, ol, dl {
// misc
hr.divide {
@extend .text-sr;
@extend .cont-text-sr;
}
.item-details {
......@@ -824,7 +824,7 @@ hr.divide {
// basic utility
.sr {
@extend .text-sr;
@extend .cont-text-sr;
}
.fake-link {
......@@ -877,7 +877,7 @@ body.js {
text-align: center;
.label {
@extend .text-sr;
@extend .cont-text-sr;
}
[class^="icon-"] {
......
../../../common/static/sass/_mixins-inherited.scss
\ No newline at end of file
......@@ -3,7 +3,7 @@
// gray primary button
.btn-primary-gray {
@extend .btn-primary;
@extend .ui-btn-primary;
background: $gray-l1;
border-color: $gray-l2;
color: $white;
......@@ -25,7 +25,7 @@
// blue primary button
.btn-primary-blue {
@extend .btn-primary;
@extend .ui-btn-primary;
background: $blue;
border-color: $blue-s1;
color: $white;
......@@ -48,7 +48,7 @@
// green primary button
.btn-primary-green {
@extend .btn-primary;
@extend .ui-btn-primary;
background: $green;
border-color: $green;
color: $white;
......@@ -71,7 +71,7 @@
// gray secondary button
.btn-secondary-gray {
@extend .btn-secondary;
@extend .ui-btn-secondary;
border-color: $gray-l3;
color: $gray-l1;
......@@ -92,7 +92,7 @@
// blue secondary button
.btn-secondary-blue {
@extend .btn-secondary;
@extend .ui-btn-secondary;
border-color: $blue-l3;
color: $blue;
......@@ -114,7 +114,7 @@
// green secondary button
.btn-secondary-green {
@extend .btn-secondary;
@extend .ui-btn-secondary;
border-color: $green-l4;
color: $green-l2;
......@@ -148,9 +148,9 @@
// ====================
// simple dropdown button styling - should we move this elsewhere?
.btn-dd {
@extend .btn;
@extend .btn-pill;
.ui-btn-dd {
@extend .ui-btn;
@extend .ui-btn-pill;
padding:($baseline/4) ($baseline/2);
border-width: 1px;
border-style: solid;
......@@ -158,7 +158,7 @@
text-align: center;
&:hover, &:active {
@extend .fake-link;
@extend .ui-fake-link;
border-color: $gray-l3;
}
......@@ -169,8 +169,8 @@
}
// layout-based buttons - nav dd
.btn-dd-nav-primary {
@extend .btn-dd;
.ui-btn-dd-nav-primary {
@extend .ui-btn-dd;
background: $white;
border-color: $white;
color: $gray-d1;
......
......@@ -2,10 +2,10 @@
// ====================
.wrapper-header {
@extend .depth3;
box-shadow: 0 1px 2px 0 $shadow-l1;
@extend .ui-depth3;
position: relative;
width: 100%;
box-shadow: 0 1px 2px 0 $shadow-l1;
margin: 0;
padding: 0 $baseline;
background: $white;
......@@ -22,7 +22,6 @@
// ====================
// basic layout
.wrapper-l, .wrapper-r {
background: $white;
}
......@@ -76,7 +75,7 @@
.title {
@extend .t-action2;
@extend .btn-dd-nav-primary;
@extend .ui-btn-dd-nav-primary;
@include transition(all $tmg-f2 ease-in-out 0s);
.label, .icon-caret-down {
......
......@@ -2,7 +2,7 @@
// ====================
.modal-cover {
@extend .depth3;
@extend .ui-depth3;
display: none;
position: fixed;
top: 0;
......@@ -13,7 +13,7 @@
}
.modal {
@extend .depth4;
@extend .ui-depth4;
display: none;
position: fixed;
top: 60px;
......@@ -61,7 +61,7 @@
// lean modal alternative
#lean_overlay {
@extend .depth4;
@extend .ui-depth4;
position: fixed;
top: 0px;
left: 0px;
......
......@@ -5,7 +5,7 @@
nav {
ol, ul {
@extend .no-list;
@extend .cont-no-list;
}
.nav-item {
......
......@@ -10,7 +10,7 @@
.wrapper-inner {
@include linear-gradient($gray-d3 0%, $gray-d3 98%, $black 100%);
@extend .depth0;
@extend .ui-depth0;
display: none;
width: 100% !important;
border-bottom: 1px solid $white;
......@@ -19,7 +19,7 @@
// sock - actions
.list-cta {
@extend .depth1;
@extend .ui-depth1;
position: absolute;
top: -($baseline*0.75);
width: 100%;
......@@ -27,7 +27,7 @@
text-align: center;
.cta-show-sock {
@extend .btn-pill;
@extend .ui-btn-pill;
@extend .t-action4;
background: $gray-l5;
padding: ($baseline/2) $baseline;
......
......@@ -144,8 +144,8 @@
// prompts
.wrapper-prompt {
@extend .depth5;
@include transition(all $tmg-f3 ease-in-out 0s);
@extend .ui-depth5;
@include transition(all $tmg-f3 ease-in-out 0s);
position: fixed;
top: 0;
background: $black-t0;
......@@ -242,7 +242,7 @@
// notifications
.wrapper-notification {
@extend .depth5;
@extend .ui-depth5;
@include clearfix();
box-shadow: 0 -1px 3px $shadow, inset 0 3px 1px $blue;
position: fixed;
......@@ -274,7 +274,7 @@
}
}
&.wrapper-notification-saving {
&.wrapper-notification-mini {
box-shadow: 0 -1px 3px $shadow, inset 0 3px 1px $pink;
}
......@@ -434,7 +434,7 @@
}
}
&.saving {
&.mini {
[class^="icon"] {
@include animation(rotateCW $tmg-s3 linear infinite);
......@@ -444,7 +444,7 @@
}
.copy p {
@extend .text-sr;
@extend .cont-text-sr;
}
}
}
......@@ -453,7 +453,7 @@
// alerts
.wrapper-alert {
@extend .depth2;
@extend .ui-depth2;
@include box-sizing(border-box);
box-shadow: 0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $blue;
position: relative;
......@@ -599,7 +599,7 @@
text-align: center;
.label {
@extend .text-sr;
@extend .cont-text-sr;
}
[class^="icon"] {
......@@ -665,14 +665,8 @@
}
}
// alert showing/hiding
.wrapper-alert {
display: none;
&.is-shown {
display: block;
}
}
// alert showing/hiding done by jQuery
.wrapper-alert { }
// notification showing/hiding
.wrapper-notification {
......@@ -702,7 +696,7 @@ body.uxdesign.alerts {
}
.content-primary {
@extend .window;
@extend .ui-window;
width: flex-grid(12, 12);
margin-right: flex-gutter();
padding: $baseline ($baseline*1.5);
......
......@@ -14,7 +14,7 @@ body.course.checklists {
// checklists - general
.course-checklist {
@extend .window;
@extend .ui-window;
margin: 0 0 ($baseline*2) 0;
&:last-child {
......@@ -23,7 +23,7 @@ body.course.checklists {
// visual status
.viz-checklist-status {
@extend .text-hide;
@extend .cont-text-hide;
@include size(100%,($baseline/4));
position: relative;
display: block;
......@@ -40,7 +40,7 @@ body.course.checklists {
background: $green;
.int {
@extend .text-sr;
@extend .cont-text-sr;
}
}
}
......
......@@ -2,9 +2,9 @@
// ====================
body.course.export {
.export-overview {
@extend .window;
@extend .ui-window;
@include clearfix;
padding: 30px 40px;
}
......@@ -40,7 +40,7 @@ body.course.export {
}
.export-form-wrapper {
.export-form {
float: left;
width: 35%;
......@@ -122,4 +122,4 @@ body.course.export {
}
}
}
}
\ No newline at end of file
}
......@@ -2,9 +2,9 @@
// ====================
body.course.import {
.import-overview {
@extend .window;
@extend .ui-window;
@include clearfix;
padding: 30px 40px;
}
......@@ -103,4 +103,4 @@ body.course.import {
color: #fff;
line-height: 48px;
}
}
\ No newline at end of file
}
......@@ -9,7 +9,7 @@ body.course.settings {
}
.content-primary {
@extend .window;
@extend .ui-window;
width: flex-grid(9, 12);
margin-right: flex-gutter();
padding: $baseline ($baseline*1.5);
......
......@@ -171,7 +171,7 @@ body.course.static-pages {
}
.static-page-details {
@extend .window;
@extend .ui-window;
padding: 32px 40px;
.row {
......
......@@ -115,7 +115,7 @@ body.course.textbooks {
}
.delete {
@extend .btn-non;
@extend .ui-btn-non;
}
}
......@@ -188,7 +188,7 @@ body.course.textbooks {
.chapters-fields,
.textbook-fields {
@extend .no-list;
@extend .cont-no-list;
.field {
margin: 0 0 ($baseline*0.75) 0;
......@@ -320,7 +320,7 @@ body.course.textbooks {
}
.action-upload {
@extend .btn-flat-outline;
@extend .ui-btn-flat-outline;
position: absolute;
top: 3px;
right: 0;
......@@ -348,7 +348,7 @@ body.course.textbooks {
.action-add-chapter {
@extend .btn-flat-outline;
@extend .ui-btn-flat-outline;
@include font-size(16);
display: block;
width: 100%;
......@@ -365,7 +365,7 @@ body.course.textbooks {
// dialog
.wrapper-dialog {
@extend .depth5;
@extend .ui-depth5;
@include transition(all 0.05s ease-in-out);
position: fixed;
top: 0;
......
......@@ -2,12 +2,6 @@
// ====================
body.course.updates {
h2 {
margin-bottom: 24px;
font-size: 22px;
font-weight: 300;
}
.course-info-wrapper {
display: table;
......@@ -180,9 +174,10 @@ body.course.updates {
border-left: none;
background: $lightGrey;
h2 {
font-size: 18px;
font-weight: 700;
.title {
margin-bottom: 24px;
font-size: 22px;
font-weight: 300;
}
.edit-button {
......@@ -220,4 +215,4 @@ body.course.updates {
textarea {
height: 300px;
}
}
\ No newline at end of file
}
<%! from django.utils.translation import ugettext as _ %>
<%inherit file="base.html" />
<%block name="title">Page Not Found</%block>
<%block name="title">${_("Page Not Found")}</%block>
<%block name="content">
<div class="wrapper-content wrapper">
<section class="content">
<h1>Page not found</h1>
<p>The page that you were looking for was not found. Go back to the <a href="/">homepage</a> or let us know about any pages that may have been moved at <a href="mailto:technical@edx.org">technical@edx.org</a>.</p>
<h1>${_("Page not found")}</h1>
<p>${_('The page that you were looking for was not found.')}
${_('Go back to the {homepage} or let us know about any pages that may have been moved at {email}.').format(
homepage='<a href="/">homepage</a>',
email='<a href="mailto:technical@edx.org">technical@edx.org</a>')}
</p>
</section>
</div>
</%block>
\ No newline at end of file
</%block>
<%! from django.utils.translation import ugettext as _ %>
<%inherit file="base.html" />
<%block name="title">Studio Server Error</%block>
<%block name="title">${_("Studio Server Error")}</%block>
<%block name="content">
<div class="wrapper-content wrapper">
<section class="content">
<h1>The <em>Studio</em> servers encountered an error</h1>
<h1>${_("The <em>Studio</em> servers encountered an error")}</h1>
<p>
An error occurred in Studio and the page could not be loaded. Please try again in a few moments.
We've logged the error and our staff is currently working to resolve this error as soon as possible.
If the problem persists, please email us at <a href="mailto:technical@edx.org">technical@edx.org</a>.
${_("An error occurred in Studio and the page could not be loaded. Please try again in a few moments.")}
${_("We've logged the error and our staff is currently working to resolve this error as soon as possible.")}
${_('If the problem persists, please email us at {email}.').format(email='<a href="mailto:technical@edx.org">technical@edx.org</a>')}
</p>
</section>
</div>
</%block>
\ No newline at end of file
</%block>
<%! from django.utils.translation import ugettext as _ %>
<%inherit file="base.html" />
<%block name="content">
......@@ -6,9 +7,9 @@
<div>
<section class="activation">
<h1>Account already active!</h1>
<p> This account has already been activated. <a href="/signin">Log in here</a>.</p>
<h1>${_("Account already active!")}</h1>
<p>${_('This account has already been activated.')}<a href="/signin">${_("Log in here.")}</a></p>
</div>
</section>
</%block>
\ No newline at end of file
</%block>
<%! from django.utils.translation import ugettext as _ %>
<%inherit file="base.html" />
<%block name="content">
<section class="tos">
<div>
<h1>Activation Complete!</h1>
<p>Thanks for activating your account. <a href="/signin">Log in here</a>.</p>
<h1>${_("Activation Complete!")}</h1>
<p>${_('Thanks for activating your account.')}<a href="/signin">${_("Log in here.")}</a></p>
</div>
</section>
</%block>
\ No newline at end of file
</%block>
<%! from django.utils.translation import ugettext as _ %>
<%inherit file="base.html" />
<%block name="content">
<section class="tos">
<div>
<h1>Activation Invalid</h1>
<h1>${_("Activation Invalid")}</h1>
<p>Something went wrong. Check to make sure the URL you went to was
correct -- e-mail programs will sometimes split it into two
lines. If you still have issues, e-mail us to let us know what happened
at <a href="mailto:bugs@mitx.mit.edu">bugs@mitx.mit.edu</a>.</p>
<p>${_('Something went wrong. Check to make sure the URL you went to was correct -- e-mail programs will sometimes split it into two lines. If you still have issues, e-mail us to let us know what happened at {email}.').format(email='<a href="mailto:bugs@mitx.mit.edu">bugs@mitx.mit.edu</a>')}</p>
<p>Or you can go back to the <a href="/">home page</a>.</p>
<p>${_('Or you can go back to the {link_start}home page{link_end}.').format(
link_start='<a href="/">', link_end='</a>')}</p>
</div>
</section>
</%block>
\ No newline at end of file
</%block>
......@@ -2,7 +2,7 @@
<%! from django.core.urlresolvers import reverse %>
<%! from django.utils.translation import ugettext as _ %>
<%block name="bodyclass">is-signedin course uploads</%block>
<%block name="title">Files &amp; Uploads</%block>
<%block name="title">${_("Files &amp; Uploads")}</%block>
<%namespace name='static' file='static_content.html'/>
......@@ -48,7 +48,7 @@
<h3 class="sr">Page Actions</h3>
<ul>
<li class="nav-item">
<a href="#" class="button upload-button new-button"><i class="icon-plus"></i> Upload New File</a>
<a href="#" class="button upload-button new-button"><i class="icon-plus"></i> ${_("Upload New File")}</a>
</li>
</ul>
</nav>
......
## -*- coding: utf-8 -*-
<%namespace name='static' file='static_content.html'/>
<!doctype html>
......@@ -17,6 +18,7 @@
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="path_prefix" content="${MITX_ROOT_URL}">
<script type="text/javascript" src="/jsi18n/"></script>
<%static:css group='base-style'/>
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/markitup/skins/simple/style.css')}" />
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/markitup/sets/wiki/style.css')}" />
......@@ -35,7 +37,6 @@
</script>
## javascript
<script type="text/javascript" src="/jsi18n/"></script>
<script type="text/javascript" src="${static.url('js/vendor/json2.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/underscore.string.min.js')}"></script>
......
<%! from django.utils.translation import ugettext as _ %>
<%inherit file="base.html" />
<%! from django.core.urlresolvers import reverse %>
<%block name="title">Course Checklists</%block>
......@@ -30,8 +31,8 @@
<div class="wrapper-mast wrapper">
<header class="mast has-actions has-subtitle">
<h1 class="page-header">
<small class="subtitle">Tools</small>
<span class="sr">&gt; </span>Course Checklists
<small class="subtitle">${_("Tools")}</small>
<span class="sr">&gt; </span>${_("Course Checklists")}
</h1>
</header>
</div>
......@@ -40,18 +41,18 @@
<section class="content">
<article class="content-primary" role="main">
<form id="course-checklists" class="course-checklists" method="post" action="">
<h2 class="title title-3 sr">Current Checklists</h2>
<h2 class="title title-3 sr">${_("Current Checklists")}</h2>
</form>
</article>
<aside class="content-supplementary" role="complimentary">
<div class="bit">
<h3 class="title title-3">What are checklists?</h3>
<h3 class="title title-3">${_("What are checklists?")}</h3>
<p>
Running a course on edX is a complex undertaking. Course checklists are designed to help you understand and keep track of all the steps necessary to get your course ready for students.
${_("Running a course on edX is a complex undertaking. Course checklists are designed to help you understand and keep track of all the steps necessary to get your course ready for students.")}
</p>
<p>
These checklists are shared among your course team, and any changes you make are immediately visible to other members of the team and saved automatically.
${_("These checklists are shared among your course team, and any changes you make are immediately visible to other members of the team and saved automatically.")}
</p>
</div>
......
<%! from django.utils.translation import ugettext as _ %>
<%inherit file="base.html" />
<%namespace name='static' file='static_content.html'/>
<!-- TODO decode course # from context_course into title -->
<%block name="title">Course Updates</%block>
<%block name="title">${_("Course Updates")}</%block>
<%block name="bodyclass">is-signedin course course-info updates</%block>
......@@ -44,15 +45,15 @@
<div class="wrapper-mast wrapper">
<header class="mast has-actions has-subtitle">
<h1 class="page-header">
<small class="subtitle">Content</small>
<span class="sr">&gt; </span>Course Updates
<small class="subtitle">${_("Content")}</small>
<span class="sr">&gt; </span>${_("Course Updates")}
</h1>
<nav class="nav-actions">
<h3 class="sr">Page Actions</h3>
<h3 class="sr">${_('Page Actions')}</h3>
<ul>
<li class="nav-item">
<a href="#" class=" button new-button new-update-button"><i class="icon-plus"></i> New Update</a>
<a href="#" class=" button new-button new-update-button"><i class="icon-plus"></i> ${_('New Update')}</a>
</li>
</ul>
</nav>
......@@ -62,7 +63,7 @@
<div class="wrapper-content wrapper">
<section class="content">
<div class="introduction">
<p clas="copy">Course updates are announcements or notifications you want to share with your class. Other course authors have used them for important exam/date reminders, change in schedules, and to call out any important steps students need to be aware of.</p>
<p clas="copy">${_('Course updates are announcements or notifications you want to share with your class. Other course authors have used them for important exam/date reminders, change in schedules, and to call out any important steps students need to be aware of.')}</p>
</div>
</section>
</div>
......
<%inherit file="base.html" />
<%! from django.utils.translation import ugettext as _ %>
<%! from django.core.urlresolvers import reverse %>
<%block name="title">Static Pages</%block>
<%block name="bodyclass">is-signedin course pages static-pages</%block>
......@@ -19,15 +20,15 @@
<div class="wrapper-mast wrapper">
<header class="mast has-actions has-subtitle">
<h1 class="page-header">
<small class="subtitle">Content</small>
<span class="sr">&gt; </span>Static Pages
<small class="subtitle">${_("Content")}</small>
<span class="sr">&gt; </span>${_("Static Pages")}
</h1>
<nav class="nav-actions">
<h3 class="sr">Page Actions</h3>
<h3 class="sr">${_("Page Actions")}</h3>
<ul>
<li class="nav-item">
<a href="#" class="button new-button new-tab"><i class="icon-plus"></i> New Page</a>
<a href="#" class="button new-button new-tab"><i class="icon-plus"></i> ${_("New Page")}</a>
</li>
</ul>
</nav>
......@@ -37,11 +38,11 @@
<div class="wrapper-content wrapper">
<section class="content">
<div class="introduction has-links">
<p class="copy">Static Pages are additional pages that supplement your Courseware. Other course authors have used them to share a syllabus, calendar, handouts, and more.</p>
<p class="copy">${_("Static Pages are additional pages that supplement your Courseware. Other course authors have used them to share a syllabus, calendar, handouts, and more.")}</p>
<nav class="nav-introduction-supplementary">
<ul>
<li class="nav-item">
<a rel="modal" href="#preview-lms-staticpages"><i class="icon-question-sign"></i>How do Static Pages look to students in my course?</a>
<a rel="modal" href="#preview-lms-staticpages"><i class="icon-question-sign"></i>${_("How do Static Pages look to students in my course?")}</a>
</li>
</ul>
</nav>
......@@ -69,15 +70,15 @@
</div>
<div class="content-modal" id="preview-lms-staticpages">
<h3 class="title">How Static Pages are Used in Your Course</h3>
<h3 class="title">${_("How Static Pages are Used in Your Course")}</h3>
<figure>
<img src="/static/img/preview-lms-staticpages.png" alt="Preview of how Static Pages are used in your course" />
<figcaption class="description">These pages will be presented in your course's main navigation alongside Courseware, Course Info, Discussion, etc.</figcaption>
<img src="/static/img/preview-lms-staticpages.png" alt="${_('Preview of how Static Pages are used in your course')}" />
<figcaption class="description">${_("These pages will be presented in your course's main navigation alongside Courseware, Course Info, Discussion, etc.")}</figcaption>
</figure>
<a href="#" rel="view" class="action action-modal-close">
<i class="icon-remove-sign"></i>
<span class="label">close modal</span>
<span class="label">${_("close modal")}</span>
</a>
</div>
</%block>
<%! from django.utils.translation import ugettext as _ %>
<%inherit file="base.html" />
<%!
import logging
......@@ -5,7 +6,7 @@
%>
<%! from django.core.urlresolvers import reverse %>
<%block name="title">CMS Subsection</%block>
<%block name="title">${_("CMS Subsection")}</%block>
<%block name="bodyclass">is-signedin course subsection</%block>
......@@ -18,11 +19,11 @@
<div class="main-column">
<article class="subsection-body window" data-id="${subsection.location}">
<div class="subsection-name-input">
<label>Display Name:</label>
<label>${_("Display Name:")}</label>
<input type="text" value="${subsection.display_name_with_default | h}" class="subsection-display-name-input" data-metadata-name="display_name"/>
</div>
<div class="sortable-unit-list">
<label>Units:</label>
<label>${_("Units:")}</label>
${units.enum_units(subsection, subsection_units=subsection_units)}
</div>
</article>
......@@ -30,63 +31,61 @@
<div class="sidebar">
<div class="unit-settings window id-holder" data-id="${subsection.location}">
<h4 class="header">Subsection Settings</h4>
<h4 class="header">${_("Subsection Settings")}</h4>
<div class="window-contents">
<div class="scheduled-date-input row">
<div class="datepair" data-language="javascript">
<div class="field field-start-date">
<label for="start_date">Release Day</label>
<input type="text" id="start_date" name="start_date"
value="${subsection.lms.start.strftime('%m/%d/%Y') if subsection.lms.start else ''}"
<label for="start_date">${_("Release Day")}</label>
<input type="text" id="start_date" name="start_date"
value="${subsection.lms.start.strftime('%m/%d/%Y') if subsection.lms.start else ''}"
placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
</div>
<div class="field field-start-time">
<label for="start_time">Release Time (<abbr title="Coordinated Universal Time">UTC</abbr>)</label>
<input type="text" id="start_time" name="start_time"
value="${subsection.lms.start.strftime('%H:%M') if subsection.lms.start else ''}"
<label for="start_time">${_("Release Time")} (<abbr title="${_("Coordinated Universal Time")}">${_("UTC")}</abbr>)</label>
<input type="text" id="start_time" name="start_time"
value="${subsection.lms.start.strftime('%H:%M') if subsection.lms.start else ''}"
placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
</div>
</div>
% if subsection.lms.start and not almost_same_datetime(subsection.lms.start, parent_item.lms.start):
% if parent_item.lms.start is None:
<p class="notice">The date above differs from the release date of
${parent_item.display_name_with_default}, which is unset.
<p class="notice">${_("The date above differs from the release date of {name}, which is unset.").format(name=parent_item.display_name_with_default)}
% else:
<p class="notice">The date above differs from the release date of ${parent_item.display_name_with_default} –
${get_default_time_display(parent_item.lms.start)}.
<p class="notice">${_("The date above differs from the release date of {name} - {start_time}").format(name=parent_item.display_name_with_default, start_time=get_default_time_display(parent_item.lms.start))}.
% endif
<a href="#" class="sync-date no-spinner">Sync to ${parent_item.display_name_with_default}.</a></p>
<a href="#" class="sync-date no-spinner">${_("Sync to {name}.").format(name=parent_item.display_name_with_default)}</a></p>
% endif
</div>
<div class="row gradable">
<label>Graded as:</label>
<label>${_("Graded as:")}</label>
<div class="gradable-status" data-initial-status="${subsection.lms.format if subsection.lms.format is not None else 'Not Graded'}">
<div class="gradable-status" data-initial-status="${subsection.lms.format if subsection.lms.format is not None else _('Not Graded')}">
</div>
<div class="due-date-input row">
<a href="#" class="set-date">Set a due date</a>
<a href="#" class="set-date">${_("Set a due date")}</a>
<div class="datepair date-setter">
<div class="field field-start-date">
<label for="due_date">Due Day</label>
<input type="text" id="due_date" name="due_date"
value="${subsection.lms.due.strftime('%m/%d/%Y') if subsection.lms.due else ''}"
<label for="due_date">${_("Due Day")}</label>
<input type="text" id="due_date" name="due_date"
value="${subsection.lms.due.strftime('%m/%d/%Y') if subsection.lms.due else ''}"
placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
</div>
<div class="field field-start-time">
<label for="due_time">Due Time (<abbr title="Coordinated Universal Time">UTC</abbr>)</label>
<input type="text" id="due_time" name="due_time"
value="${subsection.lms.due.strftime('%H:%M') if subsection.lms.due else ''}"
<label for="due_time">${_("Due Time")} (<abbr title="${_('Coordinated Universal Time')}">UTC</abbr>)</label>
<input type="text" id="due_time" name="due_time"
value="${subsection.lms.due.strftime('%H:%M') if subsection.lms.due else ''}"
placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
</div>
<a href="#" class="remove-date">Remove due date</a>
<a href="#" class="remove-date">${_("Remove due date")}</a>
</div>
</div>
<div class="row unit-actions">
<a href="${preview_link}" target="_blank" class="preview-button">Preview Drafts</a>
<a href="${preview_link}" target="_blank" class="preview-button">${_("Preview Drafts")}</a>
%if can_view_live:
<a href="${lms_link}" target="_blank" class="preview-button">View Live</a>
<a href="${lms_link}" target="_blank" class="preview-button">${_("View Live")}</a>
%endif
</div>
</div>
......@@ -119,19 +118,19 @@
// TODO figure out whether these should be in window or someplace else or whether they're only needed as local vars
// I believe that current (New Section/New Subsection) cause full page reloads which means these aren't needed globally
// but we really should change that behavior.
if (!window.graderTypes) {
window.graderTypes = new CMS.Models.Settings.CourseGraderCollection();
window.graderTypes.course_location = new CMS.Models.Location('${parent_location}');
window.graderTypes.reset(${course_graders|n});
}
if (!window.graderTypes) {
window.graderTypes = new CMS.Models.Settings.CourseGraderCollection();
window.graderTypes.course_location = new CMS.Models.Location('${parent_location}');
window.graderTypes.reset(${course_graders|n});
}
$(".gradable-status").each(function(index, ele) {
var gradeView = new CMS.Views.OverviewAssignmentGrader({
el : ele,
graders : window.graderTypes,
hideSymbol : true
});
});
$(".gradable-status").each(function(index, ele) {
var gradeView = new CMS.Views.OverviewAssignmentGrader({
el : ele,
graders : window.graderTypes,
hideSymbol : true
});
});
})
</script>
......
Thank you for signing up for edX Studio! To activate your account,
please copy and paste this address into your web browser's
address bar:
<%! from django.utils.translation import ugettext as _ %>
${_("Thank you for signing up for edX Studio! To activate your account, please copy and paste this address into your web browser's address bar:")}
% if is_secure:
https://${ site }/activate/${ key }
......@@ -8,6 +8,4 @@ address bar:
http://${ site }/activate/${ key }
% endif
If you didn't request this, you don't need to do anything; you won't
receive any more email from us. Please do not reply to this e-mail; if
you require assistance, check the help section of the edX web site.
${_("If you didn't request this, you don't need to do anything; you won't receive any more email from us. Please do not reply to this e-mail; if you require assistance, check the help section of the edX web site.")}
Your account for edX Studio
<%! from django.utils.translation import ugettext as _ %>
${_("Your account for edX Studio")}
<%! from django.utils.translation import ugettext as _ %>
<%inherit file="base.html" />
<%! from django.core.urlresolvers import reverse %>
<%block name="bodyclass">error</%block>
<%block name="title">
% if error == '404':
404 - Page Not Found
404 - ${_("Page Not Found")}
% elif error == '500':
500 - Internal Server Error
500 - ${_("Internal Server Error")}
% endif
</%block>
<%block name="content">
<article class="error-prompt">
% if error == '404':
<h1>Hmm…</h1>
<p class="description">we can't find that page.</p>
% elif error == '500':
<h1>Oops…</h1>
<p class="description">there was a problem with the server.</p>
% endif
<a href="/" class="back-button">Back to dashboard</a>
<h1>${_("The Page You Requested Page Cannot be Found")}</h1>
<p class="description">${_("We're sorry. We couldn't find the Studio page you're looking for. You may want to return to the Studio Dashboard and try again. If you are still having problems accessing things, please feel free to {link_start}contact Studio support{link_end} for further help.").format(
link_start='<a href="http://help.edge.edx.org/discussion/new" class="show-tender" title="{title}">'.format(title=_("Use our feedback tool, Tender, to share your feedback")),
link_end='</a>',
)}</p>
% elif error == '500':
<h1>${_("The Server Encountered an Error")}</h1>
<p class="description">${_("We're sorry. There was a problem with the server while trying to process your last request. You may want to return to the Studio Dashboard or try this request again. If you are still having problems accessing things, please feel free to {link_start}contact Studio support{link_end} for further help.").format(
link_start='<a href="http://help.edge.edx.org/discussion/new" class="show-tender" title="{title}">'.format(title=_("Use our feedback tool, Tender, to share your feedback")),
link_end='</a>',
)}</p>
% endif
<a href="/" class="back-button">${_("Back to dashboard")}</a>
</article>
</%block>
\ No newline at end of file
</%block>
<%! from django.utils.translation import ugettext as _ %>
<%inherit file="base.html" />
<%namespace name='static' file='static_content.html'/>
<%! from django.core.urlresolvers import reverse %>
<%block name="title">Course Export</%block>
<%block name="title">${_("Course Export")}</%block>
<%block name="bodyclass">is-signedin course tools export</%block>
<%block name="content">
<div class="wrapper-mast wrapper">
<header class="mast has-subtitle">
<h1 class="page-header">
<small class="subtitle">Tools</small>
<span class="sr">&gt; </span>Course Export
<small class="subtitle">${_("Tools")}</small>
<span class="sr">&gt; </span>${_("Course Export")}
</h1>
</header>
</div>
......@@ -19,28 +20,29 @@
<div class="inner-wrapper">
<article class="export-overview">
<div class="description">
<h2>About Exporting Courses</h2>
<p>When exporting your course, you will receive a .tar.gz formatted file that contains the following course data:</p>
<h2>${_("About Exporting Courses")}</h2>
## Translators: ".tar.gz" is a file extension, and should not be translated
<p>${_("When exporting your course, you will receive a .tar.gz formatted file that contains the following course data:")}</p>
<ul>
<li>Course Structure (Sections and sub-section ordering)</li>
<li>Individual Units</li>
<li>Individual Problems</li>
<li>Static Pages</li>
<li>Course Assets</li>
<li>${_("Course Structure (Sections and sub-section ordering)")}</li>
<li>${_("Individual Units")}</li>
<li>${_("Individual Problems")}</li>
<li>${_("Static Pages")}</li>
<li>${_("Course Assets")}</li>
</ul>
<p>Your course export <strong>will not include</strong>: student data, forum/discussion data, course settings, certificates, grading information, or user data.</p>
<p>${_("Your course export <strong>will not include</strong>: student data, forum/discussion data, course settings, certificates, grading information, or user data.")}</p>
</div>
<!-- default state -->
<div class="export-form-wrapper">
<form action="${reverse('generate_export_course', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}" method="post" enctype="multipart/form-data" class="export-form">
<h2>Export Course:</h2>
<h2>${_("Export Course:")}</h2>
<p class="error-block"></p>
<a href="${reverse('generate_export_course', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}" class="button-export">Download Files</a>
<a href="${reverse('generate_export_course', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}" class="button-export">${_("Download Files")}</a>
</form>
</div>
......@@ -48,12 +50,12 @@
<%doc>
<div class="export-form-wrapper is-downloading">
<form action="${reverse('export_course', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}" method="post" enctype="multipart/form-data" class="export-form">
<h2>Export Course:</h2>
<h2>${_("Export Course:")}</h2>
<p class="error-block"></p>
<a href="#" class="button-export disabled">Files Downloading</a>
<p class="message-status">Download not start? <a href="#" class="text-export">Try again</a></p>
<p class="message-status">${_("Download not start?")} <a href="#" class="text-export">${_("Try again")}</a></p>
</form>
</div>
</%doc>
......
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment