@@ -17,6 +17,8 @@ the Check/Final Check buttons with keys: custom_check and custom_final_check
|
||||
LMS: Add PaidCourseRegistration mode, where payment is required before course
|
||||
registration.
|
||||
|
||||
Studio: Switched to loading Javascript using require.js
|
||||
|
||||
LMS: Add split testing functionality for internal use.
|
||||
|
||||
CMS: Add edit_course_tabs management command, providing a primitive
|
||||
@@ -36,7 +38,7 @@ new post dropdown as well as response and comment area labeling.
|
||||
LMS: enhanced shib support, including detection of linked shib account
|
||||
at login page and support for the ?next= GET parameter.
|
||||
|
||||
LMS: Experimental feature using the ICE change tracker JS pkg to allow peer
|
||||
LMS: Experimental feature using the ICE change tracker JS pkg to allow peer
|
||||
assessors to edit the original submitter's work.
|
||||
|
||||
LMS: Fixed a bug that caused links from forum user profile pages to
|
||||
@@ -341,4 +343,4 @@ Common: Allow setting of authentication session cookie name.
|
||||
LMS: Option to email students when enroll/un-enroll them.
|
||||
|
||||
Blades: Added WAI-ARIA markup to the video player controls. These are now fully
|
||||
accessible by screen readers.
|
||||
accessible by screen readers.
|
||||
|
||||
@@ -16,6 +16,11 @@ def i_select_advanced_settings(step):
|
||||
world.click_course_settings()
|
||||
link_css = 'li.nav-course-settings-advanced a'
|
||||
world.css_click(link_css)
|
||||
world.wait_for_requirejs(
|
||||
["jquery", "js/models/course", "js/models/settings/advanced",
|
||||
"js/views/settings/advanced", "codemirror"])
|
||||
# this shouldn't be necessary, but we experience sporadic failures otherwise
|
||||
world.wait(1)
|
||||
|
||||
|
||||
@step('I am on the Advanced Course Settings page in Studio$')
|
||||
@@ -91,8 +96,10 @@ def assert_policy_entries(expected_keys, expected_values):
|
||||
index = get_index_of(key)
|
||||
assert_false(index == -1, "Could not find key: {key}".format(key=key))
|
||||
found_value = world.css_find(VALUE_CSS)[index].value
|
||||
assert_equal(value, found_value,
|
||||
"Expected {} to have value {} but found {}".format(key, value, found_value))
|
||||
assert_equal(
|
||||
value, found_value,
|
||||
"Expected {} to have value {} but found {}".format(key, value, found_value)
|
||||
)
|
||||
|
||||
|
||||
def get_index_of(expected_key):
|
||||
@@ -116,4 +123,6 @@ def change_display_name_value(step, new_value):
|
||||
|
||||
def change_value(step, key, new_value):
|
||||
type_in_codemirror(get_index_of(key), new_value)
|
||||
world.wait(0.5)
|
||||
press_the_notification_button(step, "Save")
|
||||
world.wait_for_ajax_complete()
|
||||
|
||||
@@ -9,7 +9,8 @@ Feature: CMS.Course checklists
|
||||
Scenario: A course author can mark tasks as complete
|
||||
Given I have opened Checklists
|
||||
Then I can check and uncheck tasks in a checklist
|
||||
And They are correctly selected after reloading the page
|
||||
And I reload the page
|
||||
Then the tasks are correctly selected
|
||||
|
||||
# There are issues getting link to be active in browsers other than chrome
|
||||
@skip_firefox
|
||||
|
||||
@@ -45,11 +45,11 @@ def i_can_check_and_uncheck_tasks(step):
|
||||
verifyChecklist2Status(2, 7, 29)
|
||||
|
||||
|
||||
@step('They are correctly selected after reloading the page$')
|
||||
def tasks_correctly_selected_after_reload(step):
|
||||
reload_the_page(step)
|
||||
@step('the tasks are correctly selected$')
|
||||
def tasks_correctly_selected(step):
|
||||
verifyChecklist2Status(2, 7, 29)
|
||||
# verify that task 7 is still selected by toggling its checkbox state and making sure that it deselects
|
||||
world.browser.execute_script("window.scrollBy(0,1000)")
|
||||
toggleTask(1, 6)
|
||||
verifyChecklist2Status(1, 7, 14)
|
||||
|
||||
@@ -109,13 +109,15 @@ def toggleTask(checklist, task):
|
||||
# TODO: figure out a way to do this in phantom and firefox
|
||||
# For now we will mark the scenerios that use this method as skipped
|
||||
def clickActionLink(checklist, task, actionText):
|
||||
# toggle checklist item to make sure that the link button is showing
|
||||
toggleTask(checklist, task)
|
||||
action_link = world.css_find('#course-checklist' + str(checklist) + ' a')[task]
|
||||
|
||||
# text will be empty initially, wait for it to populate
|
||||
def verify_action_link_text(driver):
|
||||
return world.css_text('#course-checklist' + str(checklist) + ' a', index=task) == actionText
|
||||
actualText = world.css_text('#course-checklist' + str(checklist) + ' a', index=task)
|
||||
if actualText == actionText:
|
||||
return True
|
||||
else:
|
||||
# toggle checklist item to make sure that the link button is showing
|
||||
toggleTask(checklist, task)
|
||||
return False
|
||||
|
||||
world.wait_for(verify_action_link_text)
|
||||
world.css_click('#course-checklist' + str(checklist) + ' a', index=task)
|
||||
|
||||
@@ -90,6 +90,7 @@ def press_the_notification_button(_step, name):
|
||||
world.browser.execute_script("$('{}').click()".format(btn_css))
|
||||
else:
|
||||
world.css_click(btn_css)
|
||||
world.wait_for_ajax_complete()
|
||||
|
||||
|
||||
@step('I change the "(.*)" field to "(.*)"$')
|
||||
@@ -244,7 +245,9 @@ def open_new_unit(step):
|
||||
step.given('I have opened a new course section in Studio')
|
||||
step.given('I have added a new subsection')
|
||||
step.given('I expand the first section')
|
||||
old_url = world.browser.url
|
||||
world.css_click('a.new-unit-item')
|
||||
world.wait_for(lambda x: world.browser.url != old_url)
|
||||
|
||||
|
||||
@step('the save notification button is disabled')
|
||||
@@ -298,6 +301,7 @@ def type_in_codemirror(index, text):
|
||||
g._element.send_keys(text)
|
||||
if world.is_firefox():
|
||||
world.trigger_event('div.CodeMirror', index=index, event='blur')
|
||||
world.wait_for_ajax_complete()
|
||||
|
||||
|
||||
def upload_file(filename):
|
||||
|
||||
@@ -18,13 +18,22 @@ def add_unit(step):
|
||||
user = create_studio_user(is_staff=False)
|
||||
add_course_author(user, course)
|
||||
log_into_studio()
|
||||
css_selectors = ['a.course-link', 'div.section-item a.expand-collapse-icon', 'a.new-unit-item']
|
||||
world.wait_for_requirejs([
|
||||
"jquery", "js/models/course", "coffee/src/models/module",
|
||||
"coffee/src/views/unit", "jquery.ui",
|
||||
])
|
||||
world.wait_for_mathjax()
|
||||
css_selectors = [
|
||||
'a.course-link', 'div.section-item a.expand-collapse-icon',
|
||||
'a.new-unit-item',
|
||||
]
|
||||
for selector in css_selectors:
|
||||
world.css_click(selector)
|
||||
|
||||
|
||||
@step(u'I add this type of single step component:$')
|
||||
def add_a_single_step_component(step):
|
||||
world.wait_for_xmodule()
|
||||
for step_hash in step.hashes:
|
||||
component = step_hash['Component']
|
||||
assert_in(component, ['Discussion', 'Video'])
|
||||
@@ -67,6 +76,7 @@ def add_a_multi_step_component(step, is_advanced, category):
|
||||
def click_link():
|
||||
link.click()
|
||||
|
||||
world.wait_for_xmodule()
|
||||
category = category.lower()
|
||||
for step_hash in step.hashes:
|
||||
css_selector = 'a[data-type="{}"]'.format(category)
|
||||
@@ -103,7 +113,7 @@ def see_a_multi_step_component(step, category):
|
||||
|
||||
|
||||
@step(u'I add a "([^"]*)" "([^"]*)" component$')
|
||||
def add_component_catetory(step, component, category):
|
||||
def add_component_category(step, component, category):
|
||||
assert category in ('single step', 'HTML', 'Problem', 'Advanced Problem')
|
||||
given_string = 'I add this type of {} component:'.format(category)
|
||||
step.given('{}\n{}\n{}'.format(given_string, '|Component|', '|{}|'.format(component)))
|
||||
@@ -111,6 +121,7 @@ def add_component_catetory(step, component, category):
|
||||
|
||||
@step(u'I delete all components$')
|
||||
def delete_all_components(step):
|
||||
world.wait_for_xmodule()
|
||||
delete_btn_css = 'a.delete-button'
|
||||
prompt_css = 'div#prompt-warning'
|
||||
btn_css = '{} a.button.action-primary'.format(prompt_css)
|
||||
@@ -118,7 +129,8 @@ def delete_all_components(step):
|
||||
count = len(world.css_find('ol.components li.component'))
|
||||
for _ in range(int(count)):
|
||||
world.css_click(delete_btn_css)
|
||||
assert_true(world.is_css_present('{}.is-shown'.format(prompt_css)),
|
||||
assert_true(
|
||||
world.is_css_present('{}.is-shown'.format(prompt_css)),
|
||||
msg='Waiting for the confirmation prompt to be shown')
|
||||
|
||||
# Pressing the button via css was not working reliably for the last component
|
||||
|
||||
@@ -20,16 +20,21 @@ def create_component_instance(step, component_button_css, category,
|
||||
if has_multiple_templates:
|
||||
click_component_from_menu(category, boilerplate, expected_css)
|
||||
|
||||
if category in ('video',):
|
||||
world.wait_for_xmodule()
|
||||
|
||||
assert_equal(
|
||||
1,
|
||||
len(world.css_find(expected_css)),
|
||||
"Component instance with css {css} was not created successfully".format(css=expected_css))
|
||||
|
||||
|
||||
|
||||
@world.absorb
|
||||
def click_new_component_button(step, component_button_css):
|
||||
step.given('I have clicked the new unit button')
|
||||
world.wait_for_requirejs(
|
||||
["jquery", "js/models/course", "coffee/src/models/module",
|
||||
"coffee/src/views/unit", "jquery.ui"])
|
||||
world.css_click(component_button_css)
|
||||
|
||||
|
||||
@@ -50,6 +55,7 @@ def click_component_from_menu(category, boilerplate, expected_css):
|
||||
assert_equal(len(elements), 1)
|
||||
world.css_click(elem_css)
|
||||
|
||||
|
||||
@world.absorb
|
||||
def edit_component_and_select_settings():
|
||||
world.wait_for(lambda _driver: world.css_visible('a.edit-button'))
|
||||
@@ -107,6 +113,7 @@ def verify_all_setting_entries(expected_entries):
|
||||
@world.absorb
|
||||
def save_component_and_reopen(step):
|
||||
world.css_click("a.save-button")
|
||||
world.wait_for_ajax_complete()
|
||||
# We have a known issue that modifications are still shown within the edit window after cancel (though)
|
||||
# they are not persisted. Refresh the browser to make sure the changes WERE persisted after Save.
|
||||
reload_the_page(step)
|
||||
@@ -136,6 +143,7 @@ def get_setting_entry(label):
|
||||
return None
|
||||
return world.retry_on_exception(get_setting)
|
||||
|
||||
|
||||
@world.absorb
|
||||
def get_setting_entry_index(label):
|
||||
def get_index():
|
||||
|
||||
@@ -9,7 +9,8 @@ Feature: CMS.Course Settings
|
||||
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
|
||||
And I reload the page
|
||||
Then I see the set dates
|
||||
|
||||
# IE has trouble with saving information
|
||||
@skip_internetexplorer
|
||||
@@ -17,7 +18,8 @@ Feature: CMS.Course Settings
|
||||
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
|
||||
And I reload the page
|
||||
Then I see cleared dates
|
||||
|
||||
# IE has trouble with saving information
|
||||
@skip_internetexplorer
|
||||
@@ -26,7 +28,8 @@ Feature: CMS.Course Settings
|
||||
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
|
||||
And I reload the page
|
||||
And the previously set start date is shown
|
||||
|
||||
# IE has trouble with saving information
|
||||
# Safari gets CSRF token errors
|
||||
@@ -37,7 +40,8 @@ Feature: CMS.Course Settings
|
||||
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
|
||||
And I reload the page
|
||||
Then my new course start date is shown
|
||||
|
||||
# Safari does not save + refresh properly through sauce labs
|
||||
@skip_safari
|
||||
@@ -45,7 +49,8 @@ Feature: CMS.Course Settings
|
||||
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
|
||||
And I reload the page
|
||||
Then I do not see the changes
|
||||
|
||||
# Safari does not save + refresh properly through sauce labs
|
||||
@skip_safari
|
||||
|
||||
@@ -31,6 +31,9 @@ def test_i_select_schedule_and_details(step):
|
||||
world.click_course_settings()
|
||||
link_css = 'li.nav-course-settings-schedule a'
|
||||
world.css_click(link_css)
|
||||
world.wait_for_requirejs(
|
||||
["jquery", "js/models/course",
|
||||
"js/models/settings/course_details", "js/views/settings/main"])
|
||||
|
||||
|
||||
@step('I have set course dates$')
|
||||
@@ -51,12 +54,6 @@ def test_and_i_set_course_dates(step):
|
||||
set_date_or_time(ENROLLMENT_END_TIME_CSS, DUMMY_TIME)
|
||||
|
||||
|
||||
@step('Then I see the set dates on refresh$')
|
||||
def test_then_i_see_the_set_dates_on_refresh(step):
|
||||
reload_the_page(step)
|
||||
i_see_the_set_dates()
|
||||
|
||||
|
||||
@step('And I clear all the dates except start$')
|
||||
def test_and_i_clear_all_the_dates_except_start(step):
|
||||
set_date_or_time(COURSE_END_DATE_CSS, '')
|
||||
@@ -64,9 +61,8 @@ def test_and_i_clear_all_the_dates_except_start(step):
|
||||
set_date_or_time(ENROLLMENT_END_DATE_CSS, '')
|
||||
|
||||
|
||||
@step('Then I see cleared dates on refresh$')
|
||||
def test_then_i_see_cleared_dates_on_refresh(step):
|
||||
reload_the_page(step)
|
||||
@step('Then I see cleared dates$')
|
||||
def test_then_i_see_cleared_dates(step):
|
||||
verify_date_or_time(COURSE_END_DATE_CSS, '')
|
||||
verify_date_or_time(ENROLLMENT_START_DATE_CSS, '')
|
||||
verify_date_or_time(ENROLLMENT_END_DATE_CSS, '')
|
||||
@@ -92,9 +88,8 @@ def test_i_receive_a_warning_about_course_start_date(step):
|
||||
assert_true('error' in world.css_find(COURSE_START_TIME_CSS).first._element.get_attribute('class'))
|
||||
|
||||
|
||||
@step('The previously set start date is shown on refresh$')
|
||||
def test_the_previously_set_start_date_is_shown_on_refresh(step):
|
||||
reload_the_page(step)
|
||||
@step('the previously set start date is shown$')
|
||||
def test_the_previously_set_start_date_is_shown(step):
|
||||
verify_date_or_time(COURSE_START_DATE_CSS, '12/20/2013')
|
||||
verify_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME)
|
||||
|
||||
@@ -118,9 +113,8 @@ def test_the_warning_about_course_start_date_goes_away(step):
|
||||
assert_false('error' in world.css_find(COURSE_START_TIME_CSS).first._element.get_attribute('class'))
|
||||
|
||||
|
||||
@step('My new course start date is shown on refresh$')
|
||||
def test_my_new_course_start_date_is_shown_on_refresh(step):
|
||||
reload_the_page(step)
|
||||
@step('my new course start date is shown$')
|
||||
def new_course_start_date_is_shown(step):
|
||||
verify_date_or_time(COURSE_START_DATE_CSS, '12/22/2013')
|
||||
# Time should have stayed from before attempt to clear date.
|
||||
verify_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME)
|
||||
@@ -134,16 +128,6 @@ def test_i_change_fields(step):
|
||||
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>")
|
||||
@@ -168,11 +152,8 @@ def i_see_new_course_image(_step):
|
||||
img = images[0]
|
||||
expected_src = '/c4x/MITx/999/asset/image.jpg'
|
||||
# Don't worry about the domain in the URL
|
||||
try:
|
||||
assert img['src'].endswith(expected_src)
|
||||
except AssertionError as e:
|
||||
e.args += ('Was looking for {}'.format(expected_src), 'Found {}'.format(img['src']))
|
||||
raise
|
||||
assert img['src'].endswith(expected_src), "Was looking for {expected}, found {actual}".format(
|
||||
expected=expected_src, actual=img['src'])
|
||||
|
||||
|
||||
@step('the image URL should be present in the field')
|
||||
@@ -200,7 +181,9 @@ def verify_date_or_time(css, date_or_time):
|
||||
assert_equal(date_or_time, world.css_value(css))
|
||||
|
||||
|
||||
def i_see_the_set_dates():
|
||||
@step('I do not see the changes')
|
||||
@step('I see the set dates')
|
||||
def i_see_the_set_dates(_step):
|
||||
"""
|
||||
Ensure that each field has the value set in `test_and_i_set_course_dates`.
|
||||
"""
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
from lettuce import world, step
|
||||
from common import *
|
||||
from terrain.steps import reload_the_page
|
||||
from selenium.common.exceptions import InvalidElementStateException
|
||||
from selenium.common.exceptions import (
|
||||
InvalidElementStateException, WebDriverException)
|
||||
from nose.tools import assert_in, assert_not_in # pylint: disable=E0611
|
||||
|
||||
|
||||
@@ -134,7 +135,7 @@ def change_grade_range(_step, range_name):
|
||||
def i_see_highest_grade_range(_step, range_name):
|
||||
range_css = 'span.letter-grade'
|
||||
grade = world.css_find(range_css).first
|
||||
assert grade.value == range_name
|
||||
assert grade.value == range_name, "{0} != {1}".format(grade.value, range_name)
|
||||
|
||||
|
||||
@step(u'I cannot edit the "Fail" grade range$')
|
||||
@@ -142,12 +143,18 @@ def cannot_edit_fail(_step):
|
||||
range_css = 'span.letter-grade'
|
||||
ranges = world.css_find(range_css)
|
||||
assert len(ranges) == 2
|
||||
assert ranges.last.value != 'Failure'
|
||||
|
||||
# try to change the grade range -- this should throw an exception
|
||||
try:
|
||||
ranges.last.value = 'Failure'
|
||||
assert False, "Should not be able to edit failing range"
|
||||
except InvalidElementStateException:
|
||||
except (InvalidElementStateException):
|
||||
pass # We should get this exception on failing to edit the element
|
||||
|
||||
# check to be sure that nothing has changed
|
||||
ranges = world.css_find(range_css)
|
||||
assert len(ranges) == 2
|
||||
assert ranges.last.value != 'Failure'
|
||||
|
||||
|
||||
@step(u'I change the grace period to "(.*)"$')
|
||||
|
||||
@@ -142,8 +142,9 @@ def set_the_max_attempts(step, max_attempts_set):
|
||||
if world.is_firefox():
|
||||
world.trigger_event('.wrapper-comp-setting .setting-input', index=index)
|
||||
world.save_component_and_reopen(step)
|
||||
value = int(world.css_value('input.setting-input', index=index))
|
||||
assert value >= 0
|
||||
value = world.css_value('input.setting-input', index=index)
|
||||
assert value != "", "max attempts is blank"
|
||||
assert int(value) >= 0
|
||||
|
||||
|
||||
@step('Edit High Level Source is not visible')
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
from lettuce import world, step
|
||||
from django.conf import settings
|
||||
from common import upload_file
|
||||
from nose.tools import assert_equal
|
||||
|
||||
TEST_ROOT = settings.COMMON_TEST_DATA_ROOT
|
||||
|
||||
@@ -82,20 +83,23 @@ def save_textbook(_step):
|
||||
|
||||
@step(u'I should see a textbook named "([^"]*)" with a chapter path containing "([^"]*)"')
|
||||
def check_textbook(_step, textbook_name, chapter_name):
|
||||
title = world.css_find(".textbook h3.textbook-title")
|
||||
chapter = world.css_find(".textbook .wrap-textbook p")
|
||||
assert title.text == textbook_name, "{} != {}".format(title.text, textbook_name)
|
||||
assert chapter.text == chapter_name, "{} != {}".format(chapter.text, chapter_name)
|
||||
title = world.css_text(".textbook h3.textbook-title", index=0)
|
||||
chapter = world.css_text(".textbook .wrap-textbook p", index=0)
|
||||
assert_equal(title, textbook_name)
|
||||
assert_equal(chapter, chapter_name)
|
||||
|
||||
|
||||
@step(u'I should see a textbook named "([^"]*)" with (\d+) chapters')
|
||||
def check_textbook_chapters(_step, textbook_name, num_chapters_str):
|
||||
num_chapters = int(num_chapters_str)
|
||||
title = world.css_find(".textbook .view-textbook h3.textbook-title")
|
||||
toggle = world.css_find(".textbook .view-textbook .chapter-toggle")
|
||||
assert title.text == textbook_name, "{} != {}".format(title.text, textbook_name)
|
||||
assert toggle.text == "{num} PDF Chapters".format(num=num_chapters), \
|
||||
"Expected {num} chapters, found {real}".format(num=num_chapters, real=toggle.text)
|
||||
title = world.css_text(".textbook .view-textbook h3.textbook-title", index=0)
|
||||
toggle_text = world.css_text(".textbook .view-textbook .chapter-toggle", index=0)
|
||||
assert_equal(title, textbook_name)
|
||||
assert_equal(
|
||||
toggle_text,
|
||||
"{num} PDF Chapters".format(num=num_chapters),
|
||||
"Expected {num} chapters, found {real}".format(num=num_chapters, real=toggle_text)
|
||||
)
|
||||
|
||||
|
||||
@step(u'I click the textbook chapters')
|
||||
|
||||
@@ -10,7 +10,7 @@ import random
|
||||
import os
|
||||
from django.contrib.auth.models import User
|
||||
from student.models import CourseEnrollment
|
||||
from nose.tools import assert_equal, assert_not_equal # pylint: disable=E0611
|
||||
from nose.tools import assert_equal, assert_not_equal # pylint: disable=E0611
|
||||
|
||||
TEST_ROOT = settings.COMMON_TEST_DATA_ROOT
|
||||
ASSET_NAMES_CSS = 'td.name-col > span.title > a.filename'
|
||||
@@ -79,7 +79,7 @@ def check_upload(_step, file_name):
|
||||
@step(u'The url for the file "([^"]*)" is valid$')
|
||||
def check_url(_step, file_name):
|
||||
r = get_file(file_name)
|
||||
assert_equal(r.status_code , 200)
|
||||
assert_equal(r.status_code, 200)
|
||||
|
||||
|
||||
@step(u'I delete the file "([^"]*)"$')
|
||||
@@ -89,6 +89,8 @@ def delete_file(_step, file_name):
|
||||
delete_css = "a.remove-asset-button"
|
||||
world.css_click(delete_css, index=index)
|
||||
|
||||
world.wait_for_present(".wrapper-prompt.is-shown")
|
||||
world.wait(0.2) # wait for css animation
|
||||
prompt_confirm_css = 'li.nav-item > a.action-primary'
|
||||
world.css_click(prompt_confirm_css)
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ def set_show_captions(step, setting):
|
||||
|
||||
@step('when I view the video it (.*) show the captions$')
|
||||
def shows_captions(_step, show_captions):
|
||||
world.wait_for_js_variable_truthy("Video")
|
||||
world.wait(0.5)
|
||||
if show_captions == 'does not':
|
||||
assert world.is_css_present('div.video.closed')
|
||||
else:
|
||||
@@ -48,6 +50,6 @@ def correct_video_settings(_step):
|
||||
def video_name_persisted(step):
|
||||
world.css_click('a.save-button')
|
||||
reload_the_page(step)
|
||||
world.wait_for_xmodule()
|
||||
world.edit_component()
|
||||
world.verify_setting_entry(world.get_setting_entry('Display Name'), 'Display Name', '3.4', True)
|
||||
|
||||
|
||||
@@ -33,4 +33,5 @@ Feature: CMS.Video Component
|
||||
|
||||
Scenario: Video data is shown correctly
|
||||
Given I have created a video with only XML data
|
||||
And I reload the page
|
||||
Then the correct Youtube video is shown
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#pylint: disable=C0111
|
||||
|
||||
from lettuce import world, step
|
||||
from terrain.steps import reload_the_page
|
||||
from xmodule.modulestore import Location
|
||||
from contentstore.utils import get_modulestore
|
||||
|
||||
@@ -32,6 +31,7 @@ def i_created_a_video_with_subs_with_name(_step, sub_id):
|
||||
|
||||
# Return to the video
|
||||
world.visit(video_url)
|
||||
world.wait_for_xmodule()
|
||||
|
||||
|
||||
@step('I have uploaded subtitles "([^"]*)"$')
|
||||
@@ -46,6 +46,7 @@ def i_have_uploaded_subtitles(_step, sub_id):
|
||||
|
||||
@step('when I view the (.*) it does not have autoplay enabled$')
|
||||
def does_not_autoplay(_step, video_type):
|
||||
world.wait_for_xmodule()
|
||||
assert world.css_find('.%s' % video_type)[0]['data-autoplay'] == 'False'
|
||||
assert world.css_has_class('.video_control', 'play')
|
||||
|
||||
@@ -66,6 +67,7 @@ def i_edit_the_component(_step):
|
||||
|
||||
@step('I have (hidden|toggled) captions$')
|
||||
def hide_or_show_captions(step, shown):
|
||||
world.wait_for_xmodule()
|
||||
button_css = 'a.hide-subtitles'
|
||||
if shown == 'hidden':
|
||||
world.css_click(button_css)
|
||||
@@ -107,12 +109,9 @@ def xml_only_video(step):
|
||||
data='<video youtube="1.00:%s"></video>' % youtube_id
|
||||
)
|
||||
|
||||
# Refresh to see the new video
|
||||
reload_the_page(step)
|
||||
|
||||
|
||||
@step('The correct Youtube video is shown$')
|
||||
def the_youtube_video_is_shown(_step):
|
||||
world.wait_for_xmodule()
|
||||
ele = world.css_find('.video').first
|
||||
assert ele['data-streams'].split(':')[1] == world.scenario_dict['YOUTUBE_ID']
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
import json
|
||||
|
||||
|
||||
class AssetsTestCase(CourseTestCase):
|
||||
def setUp(self):
|
||||
super(AssetsTestCase, self).setUp()
|
||||
@@ -50,7 +51,7 @@ class AssetsToyCourseTestCase(CourseTestCase):
|
||||
|
||||
resp = self.client.get(url)
|
||||
# Test a small portion of the asset data passed to the client.
|
||||
self.assertContains(resp, "new CMS.Models.AssetCollection([{")
|
||||
self.assertContains(resp, "new AssetCollection([{")
|
||||
self.assertContains(resp, "/c4x/edX/toy/asset/handouts_sample_handout.txt")
|
||||
|
||||
|
||||
|
||||
@@ -90,7 +90,10 @@ def save_item(request):
|
||||
if value is None:
|
||||
field.delete_from(existing_item)
|
||||
else:
|
||||
value = field.from_json(value)
|
||||
try:
|
||||
value = field.from_json(value)
|
||||
except ValueError:
|
||||
return JsonResponse({"error": "Invalid data"}, 400)
|
||||
field.write_to(existing_item, value)
|
||||
# Save the data that we've just changed to the underlying
|
||||
# MongoKeyValueStore before we update the mongo datastore.
|
||||
|
||||
@@ -108,6 +108,7 @@ def preview_module_system(request, preview_id, descriptor):
|
||||
wrapper_template = 'xmodule_display.html'
|
||||
|
||||
return ModuleSystem(
|
||||
static_url=settings.STATIC_URL,
|
||||
ajax_url=reverse('preview_dispatch', args=[preview_id, descriptor.location.url(), '']).rstrip('/'),
|
||||
# TODO (cpennington): Do we want to track how instructors are using the preview problems?
|
||||
track_function=lambda event_type, event: None,
|
||||
|
||||
@@ -32,6 +32,7 @@ from lms.xblock.mixin import LmsBlockMixin
|
||||
from cms.xmodule_namespace import CmsBlockMixin
|
||||
from xmodule.modulestore.inheritance import InheritanceMixin
|
||||
from xmodule.x_module import XModuleMixin
|
||||
from dealer.git import git
|
||||
|
||||
############################ FEATURE CONFIGURATION #############################
|
||||
|
||||
@@ -69,6 +70,7 @@ ENABLE_JASMINE = False
|
||||
PROJECT_ROOT = path(__file__).abspath().dirname().dirname() # /mitx/cms
|
||||
REPO_ROOT = PROJECT_ROOT.dirname()
|
||||
COMMON_ROOT = REPO_ROOT / "common"
|
||||
LMS_ROOT = REPO_ROOT / "lms"
|
||||
ENV_ROOT = REPO_ROOT.dirname() # virtualenv dir /mitx is in
|
||||
|
||||
GITHUB_REPO_ROOT = ENV_ROOT / "data"
|
||||
@@ -88,7 +90,8 @@ MAKO_TEMPLATES = {}
|
||||
MAKO_TEMPLATES['main'] = [
|
||||
PROJECT_ROOT / 'templates',
|
||||
COMMON_ROOT / 'templates',
|
||||
COMMON_ROOT / 'djangoapps' / 'pipeline_mako' / 'templates'
|
||||
COMMON_ROOT / 'djangoapps' / 'pipeline_mako' / 'templates',
|
||||
COMMON_ROOT / 'djangoapps' / 'pipeline_js' / 'templates',
|
||||
]
|
||||
|
||||
for namespace, template_dirs in lms.envs.common.MAKO_TEMPLATES.iteritems():
|
||||
@@ -107,7 +110,8 @@ TEMPLATE_CONTEXT_PROCESSORS = (
|
||||
'django.core.context_processors.static',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
'django.contrib.auth.context_processors.auth', # this is required for admin
|
||||
'django.core.context_processors.csrf'
|
||||
'django.core.context_processors.csrf',
|
||||
'dealer.contrib.django.staff.context_processor', # access git revision
|
||||
)
|
||||
|
||||
# use the ratelimit backend to prevent brute force attacks
|
||||
@@ -197,13 +201,14 @@ ADMINS = ()
|
||||
MANAGERS = ADMINS
|
||||
|
||||
# Static content
|
||||
STATIC_URL = '/static/'
|
||||
STATIC_URL = '/static/' + git.revision + "/"
|
||||
ADMIN_MEDIA_PREFIX = '/static/admin/'
|
||||
STATIC_ROOT = ENV_ROOT / "staticfiles"
|
||||
STATIC_ROOT = ENV_ROOT / "staticfiles" / git.revision
|
||||
|
||||
STATICFILES_DIRS = [
|
||||
COMMON_ROOT / "static",
|
||||
PROJECT_ROOT / "static",
|
||||
LMS_ROOT / "static",
|
||||
|
||||
# This is how you would use the textbook images locally
|
||||
# ("book", ENV_ROOT / "book_images")
|
||||
@@ -245,42 +250,39 @@ PIPELINE_CSS = {
|
||||
# test_order: Determines the position of this chunk of javascript on
|
||||
# the jasmine test page
|
||||
PIPELINE_JS = {
|
||||
'main': {
|
||||
'source_filenames': sorted(
|
||||
rooted_glob(COMMON_ROOT / 'static/', 'coffee/src/**/*.js') +
|
||||
rooted_glob(PROJECT_ROOT / 'static/', 'coffee/src/**/*.js')
|
||||
) + ['js/hesitate.js', 'js/base.js', 'js/views/feedback.js',
|
||||
'js/models/course.js',
|
||||
'js/models/section.js', 'js/views/section.js',
|
||||
'js/models/metadata_model.js', 'js/views/metadata_editor_view.js',
|
||||
'js/models/uploads.js', 'js/views/uploads.js',
|
||||
'js/models/textbook.js', 'js/views/textbook.js',
|
||||
'js/src/utility.js',
|
||||
'js/models/settings/course_grading_policy.js',
|
||||
'js/models/asset.js', 'js/models/assets.js',
|
||||
'js/views/assets.js',
|
||||
'js/views/assets_view.js', 'js/views/asset_view.js'],
|
||||
'output_filename': 'js/cms-application.js',
|
||||
'test_order': 0
|
||||
},
|
||||
'module-js': {
|
||||
'source_filenames': (
|
||||
rooted_glob(COMMON_ROOT / 'static/', 'xmodule/descriptors/js/*.js') +
|
||||
rooted_glob(COMMON_ROOT / 'static/', 'xmodule/modules/js/*.js')
|
||||
rooted_glob(COMMON_ROOT / 'static/', 'xmodule/modules/js/*.js') +
|
||||
rooted_glob(COMMON_ROOT / 'static/', 'coffee/src/discussion/*.js')
|
||||
),
|
||||
'output_filename': 'js/cms-modules.js',
|
||||
'test_order': 1
|
||||
},
|
||||
}
|
||||
|
||||
PIPELINE_COMPILERS = (
|
||||
'pipeline.compilers.coffee.CoffeeScriptCompiler',
|
||||
)
|
||||
|
||||
PIPELINE_CSS_COMPRESSOR = None
|
||||
PIPELINE_JS_COMPRESSOR = None
|
||||
|
||||
STATICFILES_IGNORE_PATTERNS = (
|
||||
"sass/*",
|
||||
"coffee/*",
|
||||
"*.py",
|
||||
"*.pyc"
|
||||
# it would be nice if we could do, for example, "**/*.scss",
|
||||
# but these strings get passed down to the `fnmatch` module,
|
||||
# which doesn't support that. :(
|
||||
# http://docs.python.org/2/library/fnmatch.html
|
||||
"sass/*.scss",
|
||||
"sass/*/*.scss",
|
||||
"sass/*/*/*.scss",
|
||||
"sass/*/*/*/*.scss",
|
||||
"coffee/*.coffee",
|
||||
"coffee/*/*.coffee",
|
||||
"coffee/*/*/*.coffee",
|
||||
"coffee/*/*/*/*.coffee",
|
||||
)
|
||||
|
||||
PIPELINE_YUI_BINARY = 'yui-compressor'
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
jasmine.getFixtures().fixturesPath += 'coffee/fixtures'
|
||||
|
||||
# Stub jQuery.cookie
|
||||
@stubCookies =
|
||||
csrftoken: "stubCSRFToken"
|
||||
|
||||
jQuery.cookie = (key, value) =>
|
||||
if value?
|
||||
@stubCookies[key] = value
|
||||
else
|
||||
@stubCookies[key]
|
||||
|
||||
# Path Jasmine's `it` method to raise an error when the test is not defined.
|
||||
# This is helpful when writing the specs first before writing the test.
|
||||
@it = (desc, func) ->
|
||||
if func?
|
||||
jasmine.getEnv().it(desc, func)
|
||||
else
|
||||
jasmine.getEnv().it desc, ->
|
||||
throw "test is undefined"
|
||||
154
cms/static/coffee/spec/main.coffee
Normal file
154
cms/static/coffee/spec/main.coffee
Normal file
@@ -0,0 +1,154 @@
|
||||
requirejs.config({
|
||||
paths: {
|
||||
"gettext": "xmodule_js/common_static/js/test/i18n",
|
||||
"mustache": "xmodule_js/common_static/js/vendor/mustache",
|
||||
"codemirror": "xmodule_js/common_static/js/vendor/CodeMirror/codemirror",
|
||||
"jquery": "xmodule_js/common_static/js/vendor/jquery.min",
|
||||
"jquery.ui": "xmodule_js/common_static/js/vendor/jquery-ui.min",
|
||||
"jquery.form": "xmodule_js/common_static/js/vendor/jquery.form",
|
||||
"jquery.markitup": "xmodule_js/common_static/js/vendor/markitup/jquery.markitup",
|
||||
"jquery.leanModal": "xmodule_js/common_static/js/vendor/jquery.leanModal.min",
|
||||
"jquery.smoothScroll": "xmodule_js/common_static/js/vendor/jquery.smooth-scroll.min",
|
||||
"jquery.scrollTo": "xmodule_js/common_static/js/vendor/jquery.scrollTo-1.4.2-min",
|
||||
"jquery.timepicker": "xmodule_js/common_static/js/vendor/timepicker/jquery.timepicker",
|
||||
"jquery.cookie": "xmodule_js/common_static/js/vendor/jquery.cookie",
|
||||
"jquery.qtip": "xmodule_js/common_static/js/vendor/jquery.qtip.min",
|
||||
"jquery.fileupload": "xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.fileupload",
|
||||
"jquery.iframe-transport": "xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.iframe-transport",
|
||||
"jquery.inputnumber": "xmodule_js/common_static/js/vendor/html5-input-polyfills/number-polyfill",
|
||||
"datepair": "xmodule_js/common_static/js/vendor/timepicker/datepair",
|
||||
"date": "xmodule_js/common_static/js/vendor/date",
|
||||
"underscore": "xmodule_js/common_static/js/vendor/underscore-min",
|
||||
"underscore.string": "xmodule_js/common_static/js/vendor/underscore.string.min",
|
||||
"backbone": "xmodule_js/common_static/js/vendor/backbone-min",
|
||||
"backbone.associations": "xmodule_js/common_static/js/vendor/backbone-associations-min",
|
||||
"youtube": "xmodule_js/common_static/js/load_youtube",
|
||||
"tinymce": "xmodule_js/common_static/js/vendor/tiny_mce/tiny_mce",
|
||||
"jquery.tinymce": "xmodule_js/common_static/js/vendor/tiny_mce/jquery.tinymce",
|
||||
"mathjax": "https://edx-static.s3.amazonaws.com/mathjax-MathJax-727332c/MathJax.js?config=TeX-MML-AM_HTMLorMML-full",
|
||||
"xmodule": "xmodule_js/src/xmodule",
|
||||
"utility": "xmodule_js/common_static/js/src/utility",
|
||||
"sinon": "xmodule_js/common_static/js/vendor/sinon-1.7.1",
|
||||
"squire": "xmodule_js/common_static/js/vendor/Squire",
|
||||
"jasmine-stealth": "xmodule_js/common_static/js/vendor/jasmine-stealth",
|
||||
"jasmine.async": "xmodule_js/common_static/js/vendor/jasmine.async",
|
||||
|
||||
"coffee/src/ajax_prefix": "xmodule_js/common_static/coffee/src/ajax_prefix"
|
||||
},
|
||||
shim: {
|
||||
"gettext": {
|
||||
exports: "gettext"
|
||||
},
|
||||
"date": {
|
||||
exports: "Date"
|
||||
},
|
||||
"jquery.ui": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.ui"
|
||||
},
|
||||
"jquery.form": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.ajaxForm"
|
||||
},
|
||||
"jquery.markitup": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.markitup"
|
||||
},
|
||||
"jquery.leanModal": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.leanModal"
|
||||
},
|
||||
"jquery.smoothScroll": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.smoothScroll"
|
||||
},
|
||||
"jquery.scrollTo": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.scrollTo"
|
||||
},
|
||||
"jquery.cookie": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.cookie"
|
||||
},
|
||||
"jquery.qtip": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.qtip"
|
||||
},
|
||||
"jquery.fileupload": {
|
||||
deps: ["jquery.iframe-transport"],
|
||||
exports: "jQuery.fn.fileupload"
|
||||
},
|
||||
"jquery.inputnumber": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.inputNumber"
|
||||
},
|
||||
"jquery.tinymce": {
|
||||
deps: ["jquery", "tinymce"],
|
||||
exports: "jQuery.fn.tinymce"
|
||||
},
|
||||
"datepair": {
|
||||
deps: ["jquery.ui", "jquery.timepicker"]
|
||||
},
|
||||
"underscore": {
|
||||
exports: "_"
|
||||
},
|
||||
"backbone": {
|
||||
deps: ["underscore", "jquery"],
|
||||
exports: "Backbone"
|
||||
},
|
||||
"backbone.associations": {
|
||||
deps: ["backbone"],
|
||||
exports: "Backbone.Associations"
|
||||
},
|
||||
"codemirror": {
|
||||
exports: "CodeMirror"
|
||||
},
|
||||
"tinymce": {
|
||||
exports: "tinymce"
|
||||
},
|
||||
"mathjax": {
|
||||
exports: "MathJax"
|
||||
},
|
||||
"xmodule": {
|
||||
exports: "XModule"
|
||||
},
|
||||
"sinon": {
|
||||
exports: "sinon"
|
||||
},
|
||||
"jasmine-stealth": {
|
||||
deps: ["jasmine"]
|
||||
},
|
||||
"jasmine.async": {
|
||||
deps: ["jasmine"],
|
||||
exports: "AsyncSpec"
|
||||
},
|
||||
|
||||
"coffee/src/main": {
|
||||
deps: ["coffee/src/ajax_prefix"]
|
||||
},
|
||||
"coffee/src/ajax_prefix": {
|
||||
deps: ["jquery"]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
jasmine.getFixtures().fixturesPath += 'coffee/fixtures'
|
||||
|
||||
define([
|
||||
"coffee/spec/main_spec",
|
||||
|
||||
"coffee/spec/models/course_spec", "coffee/spec/models/metadata_spec",
|
||||
"coffee/spec/models/module_spec", "coffee/spec/models/section_spec",
|
||||
"coffee/spec/models/settings_grading_spec", "coffee/spec/models/textbook_spec",
|
||||
"coffee/spec/models/upload_spec",
|
||||
|
||||
"coffee/spec/views/section_spec",
|
||||
"coffee/spec/views/course_info_spec", "coffee/spec/views/feedback_spec",
|
||||
"coffee/spec/views/metadata_edit_spec", "coffee/spec/views/module_edit_spec",
|
||||
"coffee/spec/views/textbook_spec", "coffee/spec/views/upload_spec",
|
||||
|
||||
# these tests are run separate in the cms-squire suite, due to process
|
||||
# isolation issues with Squire.js
|
||||
# "coffee/spec/views/assets_spec"
|
||||
])
|
||||
|
||||
@@ -1,58 +1,55 @@
|
||||
describe "CMS", ->
|
||||
beforeEach ->
|
||||
CMS.unbind()
|
||||
require ["jquery", "backbone", "coffee/src/main", "sinon", "jasmine-stealth"],
|
||||
($, Backbone, main, sinon) ->
|
||||
describe "CMS", ->
|
||||
it "should initialize URL", ->
|
||||
expect(window.CMS.URL).toBeDefined()
|
||||
|
||||
it "should initialize Models", ->
|
||||
expect(CMS.Models).toBeDefined()
|
||||
describe "main helper", ->
|
||||
beforeEach ->
|
||||
@previousAjaxSettings = $.extend(true, {}, $.ajaxSettings)
|
||||
spyOn($, "cookie")
|
||||
$.cookie.when("csrftoken").thenReturn("stubCSRFToken")
|
||||
main()
|
||||
|
||||
it "should initialize Views", ->
|
||||
expect(CMS.Views).toBeDefined()
|
||||
afterEach ->
|
||||
$.ajaxSettings = @previousAjaxSettings
|
||||
|
||||
describe "main helper", ->
|
||||
beforeEach ->
|
||||
@previousAjaxSettings = $.extend(true, {}, $.ajaxSettings)
|
||||
window.stubCookies["csrftoken"] = "stubCSRFToken"
|
||||
$(document).ready()
|
||||
it "turn on Backbone emulateHTTP", ->
|
||||
expect(Backbone.emulateHTTP).toBeTruthy()
|
||||
|
||||
afterEach ->
|
||||
$.ajaxSettings = @previousAjaxSettings
|
||||
it "setup AJAX CSRF token", ->
|
||||
expect($.ajaxSettings.headers["X-CSRFToken"]).toEqual("stubCSRFToken")
|
||||
|
||||
it "turn on Backbone emulateHTTP", ->
|
||||
expect(Backbone.emulateHTTP).toBeTruthy()
|
||||
describe "AJAX Errors", ->
|
||||
tpl = readFixtures('system-feedback.underscore')
|
||||
|
||||
it "setup AJAX CSRF token", ->
|
||||
expect($.ajaxSettings.headers["X-CSRFToken"]).toEqual("stubCSRFToken")
|
||||
beforeEach ->
|
||||
setFixtures($("<script>", {id: "system-feedback-tpl", type: "text/template"}).text(tpl))
|
||||
appendSetFixtures(sandbox({id: "page-notification"}))
|
||||
@requests = requests = []
|
||||
@xhr = sinon.useFakeXMLHttpRequest()
|
||||
@xhr.onCreate = (xhr) -> requests.push(xhr)
|
||||
|
||||
describe "AJAX Errors", ->
|
||||
tpl = readFixtures('system-feedback.underscore')
|
||||
afterEach ->
|
||||
@xhr.restore()
|
||||
|
||||
beforeEach ->
|
||||
setFixtures($("<script>", {id: "system-feedback-tpl", type: "text/template"}).text(tpl))
|
||||
appendSetFixtures(sandbox({id: "page-notification"}))
|
||||
@requests = requests = []
|
||||
@xhr = sinon.useFakeXMLHttpRequest()
|
||||
@xhr.onCreate = (xhr) -> requests.push(xhr)
|
||||
it "successful AJAX request does not pop an error notification", ->
|
||||
expect($("#page-notification")).toBeEmpty()
|
||||
$.ajax("/test")
|
||||
expect($("#page-notification")).toBeEmpty()
|
||||
@requests[0].respond(200)
|
||||
expect($("#page-notification")).toBeEmpty()
|
||||
|
||||
afterEach ->
|
||||
@xhr.restore()
|
||||
it "AJAX request with error should pop an error notification", ->
|
||||
$.ajax("/test")
|
||||
@requests[0].respond(500)
|
||||
expect($("#page-notification")).not.toBeEmpty()
|
||||
expect($("#page-notification")).toContain('div.wrapper-notification-error')
|
||||
|
||||
it "successful AJAX request does not pop an error notification", ->
|
||||
expect($("#page-notification")).toBeEmpty()
|
||||
$.ajax("/test")
|
||||
expect($("#page-notification")).toBeEmpty()
|
||||
@requests[0].respond(200)
|
||||
expect($("#page-notification")).toBeEmpty()
|
||||
|
||||
it "AJAX request with error should pop an error notification", ->
|
||||
$.ajax("/test")
|
||||
@requests[0].respond(500)
|
||||
expect($("#page-notification")).not.toBeEmpty()
|
||||
expect($("#page-notification")).toContain('div.wrapper-notification-error')
|
||||
|
||||
it "can override AJAX request with error so it does not pop an error notification", ->
|
||||
$.ajax
|
||||
url: "/test"
|
||||
notifyOnError: false
|
||||
@requests[0].respond(500)
|
||||
expect($("#page-notification")).toBeEmpty()
|
||||
it "can override AJAX request with error so it does not pop an error notification", ->
|
||||
$.ajax
|
||||
url: "/test"
|
||||
notifyOnError: false
|
||||
@requests[0].respond(500)
|
||||
expect($("#page-notification")).toBeEmpty()
|
||||
|
||||
|
||||
140
cms/static/coffee/spec/main_squire.coffee
Normal file
140
cms/static/coffee/spec/main_squire.coffee
Normal file
@@ -0,0 +1,140 @@
|
||||
requirejs.config({
|
||||
paths: {
|
||||
"gettext": "xmodule_js/common_static/js/test/i18n",
|
||||
"mustache": "xmodule_js/common_static/js/vendor/mustache",
|
||||
"codemirror": "xmodule_js/common_static/js/vendor/CodeMirror/codemirror",
|
||||
"jquery": "xmodule_js/common_static/js/vendor/jquery.min",
|
||||
"jquery.ui": "xmodule_js/common_static/js/vendor/jquery-ui.min",
|
||||
"jquery.form": "xmodule_js/common_static/js/vendor/jquery.form",
|
||||
"jquery.markitup": "xmodule_js/common_static/js/vendor/markitup/jquery.markitup",
|
||||
"jquery.leanModal": "xmodule_js/common_static/js/vendor/jquery.leanModal.min",
|
||||
"jquery.smoothScroll": "xmodule_js/common_static/js/vendor/jquery.smooth-scroll.min",
|
||||
"jquery.scrollTo": "xmodule_js/common_static/js/vendor/jquery.scrollTo-1.4.2-min",
|
||||
"jquery.timepicker": "xmodule_js/common_static/js/vendor/timepicker/jquery.timepicker",
|
||||
"jquery.cookie": "xmodule_js/common_static/js/vendor/jquery.cookie",
|
||||
"jquery.qtip": "xmodule_js/common_static/js/vendor/jquery.qtip.min",
|
||||
"jquery.fileupload": "xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.fileupload",
|
||||
"jquery.iframe-transport": "xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.iframe-transport",
|
||||
"jquery.inputnumber": "xmodule_js/common_static/js/vendor/html5-input-polyfills/number-polyfill",
|
||||
"datepair": "xmodule_js/common_static/js/vendor/timepicker/datepair",
|
||||
"date": "xmodule_js/common_static/js/vendor/date",
|
||||
"underscore": "xmodule_js/common_static/js/vendor/underscore-min",
|
||||
"underscore.string": "xmodule_js/common_static/js/vendor/underscore.string.min",
|
||||
"backbone": "xmodule_js/common_static/js/vendor/backbone-min",
|
||||
"backbone.associations": "xmodule_js/common_static/js/vendor/backbone-associations-min",
|
||||
"youtube": "xmodule_js/common_static/js/load_youtube",
|
||||
"tinymce": "xmodule_js/common_static/js/vendor/tiny_mce/tiny_mce",
|
||||
"jquery.tinymce": "xmodule_js/common_static/js/vendor/tiny_mce/jquery.tinymce",
|
||||
"mathjax": "https://edx-static.s3.amazonaws.com/mathjax-MathJax-727332c/MathJax.js?config=TeX-MML-AM_HTMLorMML-full",
|
||||
"xmodule": "xmodule_js/src/xmodule",
|
||||
"utility": "xmodule_js/common_static/js/src/utility",
|
||||
"sinon": "xmodule_js/common_static/js/vendor/sinon-1.7.1",
|
||||
"squire": "xmodule_js/common_static/js/vendor/Squire",
|
||||
"jasmine-stealth": "xmodule_js/common_static/js/vendor/jasmine-stealth",
|
||||
"jasmine.async": "xmodule_js/common_static/js/vendor/jasmine.async",
|
||||
|
||||
"coffee/src/ajax_prefix": "xmodule_js/common_static/coffee/src/ajax_prefix"
|
||||
},
|
||||
shim: {
|
||||
"gettext": {
|
||||
exports: "gettext"
|
||||
},
|
||||
"date": {
|
||||
exports: "Date"
|
||||
},
|
||||
"jquery.ui": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.ui"
|
||||
},
|
||||
"jquery.form": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.ajaxForm"
|
||||
},
|
||||
"jquery.markitup": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.markitup"
|
||||
},
|
||||
"jquery.leanModal": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.leanModal"
|
||||
},
|
||||
"jquery.smoothScroll": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.smoothScroll"
|
||||
},
|
||||
"jquery.scrollTo": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.scrollTo"
|
||||
},
|
||||
"jquery.cookie": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.cookie"
|
||||
},
|
||||
"jquery.qtip": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.qtip"
|
||||
},
|
||||
"jquery.fileupload": {
|
||||
deps: ["jquery.iframe-transport"],
|
||||
exports: "jQuery.fn.fileupload"
|
||||
},
|
||||
"jquery.inputnumber": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.inputNumber"
|
||||
},
|
||||
"jquery.tinymce": {
|
||||
deps: ["jquery", "tinymce"],
|
||||
exports: "jQuery.fn.tinymce"
|
||||
},
|
||||
"datepair": {
|
||||
deps: ["jquery.ui", "jquery.timepicker"]
|
||||
},
|
||||
"underscore": {
|
||||
exports: "_"
|
||||
},
|
||||
"backbone": {
|
||||
deps: ["underscore", "jquery"],
|
||||
exports: "Backbone"
|
||||
},
|
||||
"backbone.associations": {
|
||||
deps: ["backbone"],
|
||||
exports: "Backbone.Associations"
|
||||
},
|
||||
"codemirror": {
|
||||
exports: "CodeMirror"
|
||||
},
|
||||
"tinymce": {
|
||||
exports: "tinymce"
|
||||
},
|
||||
"mathjax": {
|
||||
exports: "MathJax"
|
||||
},
|
||||
"xmodule": {
|
||||
exports: "XModule"
|
||||
},
|
||||
"sinon": {
|
||||
exports: "sinon"
|
||||
},
|
||||
"jasmine-stealth": {
|
||||
deps: ["jasmine"]
|
||||
},
|
||||
"jasmine.async": {
|
||||
deps: ["jasmine"],
|
||||
exports: "AsyncSpec"
|
||||
},
|
||||
|
||||
"coffee/src/main": {
|
||||
deps: ["coffee/src/ajax_prefix"]
|
||||
},
|
||||
"coffee/src/ajax_prefix": {
|
||||
deps: ["jquery"]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
jasmine.getFixtures().fixturesPath += 'coffee/fixtures'
|
||||
|
||||
define([
|
||||
"coffee/spec/views/assets_spec"
|
||||
])
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
describe "CMS.Models.Course", ->
|
||||
describe "basic", ->
|
||||
beforeEach ->
|
||||
@model = new CMS.Models.Course({
|
||||
define ["js/models/course"], (Course) ->
|
||||
describe "Course", ->
|
||||
describe "basic", ->
|
||||
beforeEach ->
|
||||
@model = new Course({
|
||||
name: "Greek Hero"
|
||||
})
|
||||
})
|
||||
|
||||
it "should take a name argument", ->
|
||||
expect(@model.get("name")).toEqual("Greek Hero")
|
||||
it "should take a name argument", ->
|
||||
expect(@model.get("name")).toEqual("Greek Hero")
|
||||
|
||||
@@ -1,58 +1,59 @@
|
||||
describe "CMS.Models.Metadata", ->
|
||||
it "knows when the value has not been modified", ->
|
||||
model = new CMS.Models.Metadata(
|
||||
{'value': 'original', 'explicitly_set': false})
|
||||
expect(model.isModified()).toBeFalsy()
|
||||
define ["js/models/metadata"], (Metadata) ->
|
||||
describe "Metadata", ->
|
||||
it "knows when the value has not been modified", ->
|
||||
model = new Metadata(
|
||||
{'value': 'original', 'explicitly_set': false})
|
||||
expect(model.isModified()).toBeFalsy()
|
||||
|
||||
model = new CMS.Models.Metadata(
|
||||
{'value': 'original', 'explicitly_set': true})
|
||||
model.setValue('original')
|
||||
expect(model.isModified()).toBeFalsy()
|
||||
model = new Metadata(
|
||||
{'value': 'original', 'explicitly_set': true})
|
||||
model.setValue('original')
|
||||
expect(model.isModified()).toBeFalsy()
|
||||
|
||||
it "knows when the value has been modified", ->
|
||||
model = new CMS.Models.Metadata(
|
||||
{'value': 'original', 'explicitly_set': false})
|
||||
model.setValue('original')
|
||||
expect(model.isModified()).toBeTruthy()
|
||||
it "knows when the value has been modified", ->
|
||||
model = new Metadata(
|
||||
{'value': 'original', 'explicitly_set': false})
|
||||
model.setValue('original')
|
||||
expect(model.isModified()).toBeTruthy()
|
||||
|
||||
model = new CMS.Models.Metadata(
|
||||
{'value': 'original', 'explicitly_set': true})
|
||||
model.setValue('modified')
|
||||
expect(model.isModified()).toBeTruthy()
|
||||
model = new Metadata(
|
||||
{'value': 'original', 'explicitly_set': true})
|
||||
model.setValue('modified')
|
||||
expect(model.isModified()).toBeTruthy()
|
||||
|
||||
it "tracks when values have been explicitly set", ->
|
||||
model = new CMS.Models.Metadata(
|
||||
{'value': 'original', 'explicitly_set': false})
|
||||
expect(model.isExplicitlySet()).toBeFalsy()
|
||||
model.setValue('original')
|
||||
expect(model.isExplicitlySet()).toBeTruthy()
|
||||
it "tracks when values have been explicitly set", ->
|
||||
model = new Metadata(
|
||||
{'value': 'original', 'explicitly_set': false})
|
||||
expect(model.isExplicitlySet()).toBeFalsy()
|
||||
model.setValue('original')
|
||||
expect(model.isExplicitlySet()).toBeTruthy()
|
||||
|
||||
it "has both 'display value' and a 'value' methods", ->
|
||||
model = new CMS.Models.Metadata(
|
||||
{'value': 'default', 'explicitly_set': false})
|
||||
expect(model.getValue()).toBeNull
|
||||
expect(model.getDisplayValue()).toBe('default')
|
||||
model.setValue('modified')
|
||||
expect(model.getValue()).toBe('modified')
|
||||
expect(model.getDisplayValue()).toBe('modified')
|
||||
it "has both 'display value' and a 'value' methods", ->
|
||||
model = new Metadata(
|
||||
{'value': 'default', 'explicitly_set': false})
|
||||
expect(model.getValue()).toBeNull
|
||||
expect(model.getDisplayValue()).toBe('default')
|
||||
model.setValue('modified')
|
||||
expect(model.getValue()).toBe('modified')
|
||||
expect(model.getDisplayValue()).toBe('modified')
|
||||
|
||||
it "has a clear method for reverting to the default", ->
|
||||
model = new CMS.Models.Metadata(
|
||||
{'value': 'original', 'default_value' : 'default', 'explicitly_set': true})
|
||||
model.clear()
|
||||
expect(model.getValue()).toBeNull
|
||||
expect(model.getDisplayValue()).toBe('default')
|
||||
expect(model.isExplicitlySet()).toBeFalsy()
|
||||
it "has a clear method for reverting to the default", ->
|
||||
model = new Metadata(
|
||||
{'value': 'original', 'default_value' : 'default', 'explicitly_set': true})
|
||||
model.clear()
|
||||
expect(model.getValue()).toBeNull
|
||||
expect(model.getDisplayValue()).toBe('default')
|
||||
expect(model.isExplicitlySet()).toBeFalsy()
|
||||
|
||||
it "has a getter for field name", ->
|
||||
model = new CMS.Models.Metadata({'field_name': 'foo'})
|
||||
expect(model.getFieldName()).toBe('foo')
|
||||
it "has a getter for field name", ->
|
||||
model = new Metadata({'field_name': 'foo'})
|
||||
expect(model.getFieldName()).toBe('foo')
|
||||
|
||||
it "has a getter for options", ->
|
||||
model = new CMS.Models.Metadata({'options': ['foo', 'bar']})
|
||||
expect(model.getOptions()).toEqual(['foo', 'bar'])
|
||||
it "has a getter for options", ->
|
||||
model = new Metadata({'options': ['foo', 'bar']})
|
||||
expect(model.getOptions()).toEqual(['foo', 'bar'])
|
||||
|
||||
it "has a getter for type", ->
|
||||
model = new CMS.Models.Metadata({'type': 'Integer'})
|
||||
expect(model.getType()).toBe(CMS.Models.Metadata.INTEGER_TYPE)
|
||||
it "has a getter for type", ->
|
||||
model = new Metadata({'type': 'Integer'})
|
||||
expect(model.getType()).toBe(Metadata.INTEGER_TYPE)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
describe "CMS.Models.Module", ->
|
||||
it "set the correct URL", ->
|
||||
expect(new CMS.Models.Module().url).toEqual("/save_item")
|
||||
define ["coffee/src/models/module"], (Module) ->
|
||||
describe "Module", ->
|
||||
it "set the correct URL", ->
|
||||
expect(new Module().url).toEqual("/save_item")
|
||||
|
||||
it "set the correct default", ->
|
||||
expect(new CMS.Models.Module().defaults).toEqual(undefined)
|
||||
it "set the correct default", ->
|
||||
expect(new Module().defaults).toEqual(undefined)
|
||||
|
||||
@@ -1,53 +1,53 @@
|
||||
describe "CMS.Models.Section", ->
|
||||
describe "basic", ->
|
||||
beforeEach ->
|
||||
@model = new CMS.Models.Section({
|
||||
define ["js/models/section", "sinon"], (Section, sinon) ->
|
||||
describe "Section", ->
|
||||
describe "basic", ->
|
||||
beforeEach ->
|
||||
@model = new Section({
|
||||
id: 42,
|
||||
name: "Life, the Universe, and Everything"
|
||||
})
|
||||
|
||||
it "should take an id argument", ->
|
||||
expect(@model.get("id")).toEqual(42)
|
||||
|
||||
it "should take a name argument", ->
|
||||
expect(@model.get("name")).toEqual("Life, the Universe, and Everything")
|
||||
|
||||
it "should have a URL set", ->
|
||||
expect(@model.url).toEqual("/save_item")
|
||||
|
||||
it "should serialize to JSON correctly", ->
|
||||
expect(@model.toJSON()).toEqual({
|
||||
id: 42,
|
||||
name: "Life, the Universe, and Everything"
|
||||
})
|
||||
|
||||
it "should take an id argument", ->
|
||||
expect(@model.get("id")).toEqual(42)
|
||||
|
||||
it "should take a name argument", ->
|
||||
expect(@model.get("name")).toEqual("Life, the Universe, and Everything")
|
||||
|
||||
it "should have a URL set", ->
|
||||
expect(@model.url).toEqual("/save_item")
|
||||
|
||||
it "should serialize to JSON correctly", ->
|
||||
expect(@model.toJSON()).toEqual({
|
||||
id: 42,
|
||||
metadata: {
|
||||
metadata:
|
||||
{
|
||||
display_name: "Life, the Universe, and Everything"
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe "XHR", ->
|
||||
beforeEach ->
|
||||
spyOn(CMS.Models.Section.prototype, 'showNotification')
|
||||
spyOn(CMS.Models.Section.prototype, 'hideNotification')
|
||||
@model = new CMS.Models.Section({
|
||||
id: 42,
|
||||
name: "Life, the Universe, and Everything"
|
||||
})
|
||||
@requests = requests = []
|
||||
@xhr = sinon.useFakeXMLHttpRequest()
|
||||
@xhr.onCreate = (xhr) -> requests.push(xhr)
|
||||
describe "XHR", ->
|
||||
beforeEach ->
|
||||
spyOn(Section.prototype, 'showNotification')
|
||||
spyOn(Section.prototype, 'hideNotification')
|
||||
@model = new Section({
|
||||
id: 42,
|
||||
name: "Life, the Universe, and Everything"
|
||||
})
|
||||
@requests = requests = []
|
||||
@xhr = sinon.useFakeXMLHttpRequest()
|
||||
@xhr.onCreate = (xhr) -> requests.push(xhr)
|
||||
|
||||
afterEach ->
|
||||
@xhr.restore()
|
||||
|
||||
it "show/hide a notification when it saves to the server", ->
|
||||
@model.save()
|
||||
expect(CMS.Models.Section.prototype.showNotification).toHaveBeenCalled()
|
||||
@requests[0].respond(200)
|
||||
expect(CMS.Models.Section.prototype.hideNotification).toHaveBeenCalled()
|
||||
|
||||
it "don't hide notification when saving fails", ->
|
||||
# this is handled by the global AJAX error handler
|
||||
@model.save()
|
||||
@requests[0].respond(500)
|
||||
expect(CMS.Models.Section.prototype.hideNotification).not.toHaveBeenCalled()
|
||||
afterEach ->
|
||||
@xhr.restore()
|
||||
|
||||
it "show/hide a notification when it saves to the server", ->
|
||||
@model.save()
|
||||
expect(Section.prototype.showNotification).toHaveBeenCalled()
|
||||
@requests[0].respond(200)
|
||||
expect(Section.prototype.hideNotification).toHaveBeenCalled()
|
||||
|
||||
it "don't hide notification when saving fails", ->
|
||||
# this is handled by the global AJAX error handler
|
||||
@model.save()
|
||||
@requests[0].respond(500)
|
||||
expect(Section.prototype.hideNotification).not.toHaveBeenCalled()
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
describe "CMS.Models.Settings.CourseGradingPolicy", ->
|
||||
beforeEach ->
|
||||
@model = new CMS.Models.Settings.CourseGradingPolicy()
|
||||
define ["js/models/settings/course_grading_policy"], (CourseGradingPolicy) ->
|
||||
describe "CourseGradingPolicy", ->
|
||||
beforeEach ->
|
||||
@model = new CourseGradingPolicy()
|
||||
|
||||
describe "parse", ->
|
||||
it "sets a null grace period to 00:00", ->
|
||||
attrs = @model.parse(grace_period: null)
|
||||
expect(attrs.grace_period).toEqual(
|
||||
hours: 0,
|
||||
minutes: 0
|
||||
)
|
||||
describe "parse", ->
|
||||
it "sets a null grace period to 00:00", ->
|
||||
attrs = @model.parse(grace_period: null)
|
||||
expect(attrs.grace_period).toEqual(
|
||||
hours: 0,
|
||||
minutes: 0
|
||||
)
|
||||
|
||||
describe "parseGracePeriod", ->
|
||||
it "parses a time in HH:MM format", ->
|
||||
time = @model.parseGracePeriod("07:19")
|
||||
expect(time).toEqual(
|
||||
hours: 7,
|
||||
minutes: 19
|
||||
)
|
||||
describe "parseGracePeriod", ->
|
||||
it "parses a time in HH:MM format", ->
|
||||
time = @model.parseGracePeriod("07:19")
|
||||
expect(time).toEqual(
|
||||
hours: 7,
|
||||
minutes: 19
|
||||
)
|
||||
|
||||
it "returns null on an incorrectly formatted string", ->
|
||||
expect(@model.parseGracePeriod("asdf")).toBe(null)
|
||||
expect(@model.parseGracePeriod("7:19")).toBe(null)
|
||||
expect(@model.parseGracePeriod("1000:00")).toBe(null)
|
||||
it "returns null on an incorrectly formatted string", ->
|
||||
expect(@model.parseGracePeriod("asdf")).toBe(null)
|
||||
expect(@model.parseGracePeriod("7:19")).toBe(null)
|
||||
expect(@model.parseGracePeriod("1000:00")).toBe(null)
|
||||
|
||||
@@ -1,198 +1,202 @@
|
||||
beforeEach ->
|
||||
@addMatchers
|
||||
toBeInstanceOf: (expected) ->
|
||||
return @actual instanceof expected
|
||||
define ["backbone", "js/models/textbook", "js/collections/textbook", "js/models/chapter", "js/collections/chapter", "coffee/src/main"],
|
||||
(Backbone, Textbook, TextbookSet, Chapter, ChapterSet, main) ->
|
||||
|
||||
|
||||
describe "CMS.Models.Textbook", ->
|
||||
beforeEach ->
|
||||
@model = new CMS.Models.Textbook()
|
||||
|
||||
describe "Basic", ->
|
||||
it "should have an empty name by default", ->
|
||||
expect(@model.get("name")).toEqual("")
|
||||
|
||||
it "should not show chapters by default", ->
|
||||
expect(@model.get("showChapters")).toBeFalsy()
|
||||
|
||||
it "should have a ChapterSet with one chapter by default", ->
|
||||
chapters = @model.get("chapters")
|
||||
expect(chapters).toBeInstanceOf(CMS.Collections.ChapterSet)
|
||||
expect(chapters.length).toEqual(1)
|
||||
expect(chapters.at(0).isEmpty()).toBeTruthy()
|
||||
|
||||
it "should be empty by default", ->
|
||||
expect(@model.isEmpty()).toBeTruthy()
|
||||
|
||||
it "should have a URL set", ->
|
||||
expect(@model.url()).toBeTruthy()
|
||||
|
||||
it "should be able to reset itself", ->
|
||||
@model.set("name", "foobar")
|
||||
@model.reset()
|
||||
expect(@model.get("name")).toEqual("")
|
||||
|
||||
it "should not be dirty by default", ->
|
||||
expect(@model.isDirty()).toBeFalsy()
|
||||
|
||||
it "should be dirty after it's been changed", ->
|
||||
@model.set("name", "foobar")
|
||||
expect(@model.isDirty()).toBeTruthy()
|
||||
|
||||
it "should not be dirty after calling setOriginalAttributes", ->
|
||||
@model.set("name", "foobar")
|
||||
@model.setOriginalAttributes()
|
||||
expect(@model.isDirty()).toBeFalsy()
|
||||
|
||||
describe "Input/Output", ->
|
||||
deepAttributes = (obj) ->
|
||||
if obj instanceof Backbone.Model
|
||||
deepAttributes(obj.attributes)
|
||||
else if obj instanceof Backbone.Collection
|
||||
obj.map(deepAttributes);
|
||||
else if _.isArray(obj)
|
||||
_.map(obj, deepAttributes);
|
||||
else if _.isObject(obj)
|
||||
attributes = {};
|
||||
for own prop, val of obj
|
||||
attributes[prop] = deepAttributes(val)
|
||||
attributes
|
||||
else
|
||||
obj
|
||||
|
||||
it "should match server model to client model", ->
|
||||
serverModelSpec = {
|
||||
"tab_title": "My Textbook",
|
||||
"chapters": [
|
||||
{"title": "Chapter 1", "url": "/ch1.pdf"},
|
||||
{"title": "Chapter 2", "url": "/ch2.pdf"},
|
||||
]
|
||||
}
|
||||
clientModelSpec = {
|
||||
"name": "My Textbook",
|
||||
"showChapters": false,
|
||||
"editing": false,
|
||||
"chapters": [{
|
||||
"name": "Chapter 1",
|
||||
"asset_path": "/ch1.pdf",
|
||||
"order": 1
|
||||
}, {
|
||||
"name": "Chapter 2",
|
||||
"asset_path": "/ch2.pdf",
|
||||
"order": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
model = new CMS.Models.Textbook(serverModelSpec, {parse: true})
|
||||
expect(deepAttributes(model)).toEqual(clientModelSpec)
|
||||
expect(model.toJSON()).toEqual(serverModelSpec)
|
||||
|
||||
describe "Validation", ->
|
||||
it "requires a name", ->
|
||||
model = new CMS.Models.Textbook({name: ""})
|
||||
expect(model.isValid()).toBeFalsy()
|
||||
|
||||
it "requires at least one chapter", ->
|
||||
model = new CMS.Models.Textbook({name: "foo"})
|
||||
model.get("chapters").reset()
|
||||
expect(model.isValid()).toBeFalsy()
|
||||
|
||||
it "requires a valid chapter", ->
|
||||
chapter = new CMS.Models.Chapter()
|
||||
chapter.isValid = -> false
|
||||
model = new CMS.Models.Textbook({name: "foo"})
|
||||
model.get("chapters").reset([chapter])
|
||||
expect(model.isValid()).toBeFalsy()
|
||||
|
||||
it "requires all chapters to be valid", ->
|
||||
chapter1 = new CMS.Models.Chapter()
|
||||
chapter1.isValid = -> true
|
||||
chapter2 = new CMS.Models.Chapter()
|
||||
chapter2.isValid = -> false
|
||||
model = new CMS.Models.Textbook({name: "foo"})
|
||||
model.get("chapters").reset([chapter1, chapter2])
|
||||
expect(model.isValid()).toBeFalsy()
|
||||
|
||||
it "can pass validation", ->
|
||||
chapter = new CMS.Models.Chapter()
|
||||
chapter.isValid = -> true
|
||||
model = new CMS.Models.Textbook({name: "foo"})
|
||||
model.get("chapters").reset([chapter])
|
||||
expect(model.isValid()).toBeTruthy()
|
||||
@addMatchers
|
||||
toBeInstanceOf: (expected) ->
|
||||
return @actual instanceof expected
|
||||
|
||||
|
||||
describe "CMS.Collections.TextbookSet", ->
|
||||
beforeEach ->
|
||||
CMS.URL.TEXTBOOKS = "/textbooks"
|
||||
@collection = new CMS.Collections.TextbookSet()
|
||||
describe "Textbook model", ->
|
||||
beforeEach ->
|
||||
main()
|
||||
@model = new Textbook()
|
||||
|
||||
afterEach ->
|
||||
delete CMS.URL.TEXTBOOKS
|
||||
describe "Basic", ->
|
||||
it "should have an empty name by default", ->
|
||||
expect(@model.get("name")).toEqual("")
|
||||
|
||||
it "should have a url set", ->
|
||||
expect(@collection.url()).toEqual("/textbooks")
|
||||
it "should not show chapters by default", ->
|
||||
expect(@model.get("showChapters")).toBeFalsy()
|
||||
|
||||
it "can call save", ->
|
||||
spyOn(@collection, "sync")
|
||||
@collection.save()
|
||||
expect(@collection.sync).toHaveBeenCalledWith("update", @collection, undefined)
|
||||
it "should have a ChapterSet with one chapter by default", ->
|
||||
chapters = @model.get("chapters")
|
||||
expect(chapters).toBeInstanceOf(ChapterSet)
|
||||
expect(chapters.length).toEqual(1)
|
||||
expect(chapters.at(0).isEmpty()).toBeTruthy()
|
||||
|
||||
it "should be empty by default", ->
|
||||
expect(@model.isEmpty()).toBeTruthy()
|
||||
|
||||
it "should have a URL set", ->
|
||||
expect(@model.url()).toBeTruthy()
|
||||
|
||||
it "should be able to reset itself", ->
|
||||
@model.set("name", "foobar")
|
||||
@model.reset()
|
||||
expect(@model.get("name")).toEqual("")
|
||||
|
||||
it "should not be dirty by default", ->
|
||||
expect(@model.isDirty()).toBeFalsy()
|
||||
|
||||
it "should be dirty after it's been changed", ->
|
||||
@model.set("name", "foobar")
|
||||
expect(@model.isDirty()).toBeTruthy()
|
||||
|
||||
it "should not be dirty after calling setOriginalAttributes", ->
|
||||
@model.set("name", "foobar")
|
||||
@model.setOriginalAttributes()
|
||||
expect(@model.isDirty()).toBeFalsy()
|
||||
|
||||
describe "Input/Output", ->
|
||||
deepAttributes = (obj) ->
|
||||
if obj instanceof Backbone.Model
|
||||
deepAttributes(obj.attributes)
|
||||
else if obj instanceof Backbone.Collection
|
||||
obj.map(deepAttributes);
|
||||
else if _.isArray(obj)
|
||||
_.map(obj, deepAttributes);
|
||||
else if _.isObject(obj)
|
||||
attributes = {};
|
||||
for own prop, val of obj
|
||||
attributes[prop] = deepAttributes(val)
|
||||
attributes
|
||||
else
|
||||
obj
|
||||
|
||||
it "should match server model to client model", ->
|
||||
serverModelSpec = {
|
||||
"tab_title": "My Textbook",
|
||||
"chapters": [
|
||||
{"title": "Chapter 1", "url": "/ch1.pdf"},
|
||||
{"title": "Chapter 2", "url": "/ch2.pdf"},
|
||||
]
|
||||
}
|
||||
clientModelSpec = {
|
||||
"name": "My Textbook",
|
||||
"showChapters": false,
|
||||
"editing": false,
|
||||
"chapters": [{
|
||||
"name": "Chapter 1",
|
||||
"asset_path": "/ch1.pdf",
|
||||
"order": 1
|
||||
}, {
|
||||
"name": "Chapter 2",
|
||||
"asset_path": "/ch2.pdf",
|
||||
"order": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
model = new Textbook(serverModelSpec, {parse: true})
|
||||
expect(deepAttributes(model)).toEqual(clientModelSpec)
|
||||
expect(model.toJSON()).toEqual(serverModelSpec)
|
||||
|
||||
describe "Validation", ->
|
||||
it "requires a name", ->
|
||||
model = new Textbook({name: ""})
|
||||
expect(model.isValid()).toBeFalsy()
|
||||
|
||||
it "requires at least one chapter", ->
|
||||
model = new Textbook({name: "foo"})
|
||||
model.get("chapters").reset()
|
||||
expect(model.isValid()).toBeFalsy()
|
||||
|
||||
it "requires a valid chapter", ->
|
||||
chapter = new Chapter()
|
||||
chapter.isValid = -> false
|
||||
model = new Textbook({name: "foo"})
|
||||
model.get("chapters").reset([chapter])
|
||||
expect(model.isValid()).toBeFalsy()
|
||||
|
||||
it "requires all chapters to be valid", ->
|
||||
chapter1 = new Chapter()
|
||||
chapter1.isValid = -> true
|
||||
chapter2 = new Chapter()
|
||||
chapter2.isValid = -> false
|
||||
model = new Textbook({name: "foo"})
|
||||
model.get("chapters").reset([chapter1, chapter2])
|
||||
expect(model.isValid()).toBeFalsy()
|
||||
|
||||
it "can pass validation", ->
|
||||
chapter = new Chapter()
|
||||
chapter.isValid = -> true
|
||||
model = new Textbook({name: "foo"})
|
||||
model.get("chapters").reset([chapter])
|
||||
expect(model.isValid()).toBeTruthy()
|
||||
|
||||
|
||||
describe "CMS.Models.Chapter", ->
|
||||
beforeEach ->
|
||||
@model = new CMS.Models.Chapter()
|
||||
describe "Textbook collection", ->
|
||||
beforeEach ->
|
||||
CMS.URL.TEXTBOOKS = "/textbooks"
|
||||
@collection = new TextbookSet()
|
||||
|
||||
describe "Basic", ->
|
||||
it "should have a name by default", ->
|
||||
expect(@model.get("name")).toEqual("")
|
||||
afterEach ->
|
||||
delete CMS.URL.TEXTBOOKS
|
||||
|
||||
it "should have an asset_path by default", ->
|
||||
expect(@model.get("asset_path")).toEqual("")
|
||||
it "should have a url set", ->
|
||||
expect(@collection.url()).toEqual("/textbooks")
|
||||
|
||||
it "should have an order by default", ->
|
||||
expect(@model.get("order")).toEqual(1)
|
||||
|
||||
it "should be empty by default", ->
|
||||
expect(@model.isEmpty()).toBeTruthy()
|
||||
|
||||
describe "Validation", ->
|
||||
it "requires a name", ->
|
||||
model = new CMS.Models.Chapter({name: "", asset_path: "a.pdf"})
|
||||
expect(model.isValid()).toBeFalsy()
|
||||
|
||||
it "requires an asset_path", ->
|
||||
model = new CMS.Models.Chapter({name: "a", asset_path: ""})
|
||||
expect(model.isValid()).toBeFalsy()
|
||||
|
||||
it "can pass validation", ->
|
||||
model = new CMS.Models.Chapter({name: "a", asset_path: "a.pdf"})
|
||||
expect(model.isValid()).toBeTruthy()
|
||||
it "can call save", ->
|
||||
spyOn(@collection, "sync")
|
||||
@collection.save()
|
||||
expect(@collection.sync).toHaveBeenCalledWith("update", @collection, undefined)
|
||||
|
||||
|
||||
describe "CMS.Collections.ChapterSet", ->
|
||||
beforeEach ->
|
||||
@collection = new CMS.Collections.ChapterSet()
|
||||
describe "Chapter model", ->
|
||||
beforeEach ->
|
||||
@model = new Chapter()
|
||||
|
||||
it "is empty by default", ->
|
||||
expect(@collection.isEmpty()).toBeTruthy()
|
||||
describe "Basic", ->
|
||||
it "should have a name by default", ->
|
||||
expect(@model.get("name")).toEqual("")
|
||||
|
||||
it "is empty if all chapters are empty", ->
|
||||
@collection.add([{}, {}, {}])
|
||||
expect(@collection.isEmpty()).toBeTruthy()
|
||||
it "should have an asset_path by default", ->
|
||||
expect(@model.get("asset_path")).toEqual("")
|
||||
|
||||
it "is not empty if a chapter is not empty", ->
|
||||
@collection.add([{}, {name: "full"}, {}])
|
||||
expect(@collection.isEmpty()).toBeFalsy()
|
||||
it "should have an order by default", ->
|
||||
expect(@model.get("order")).toEqual(1)
|
||||
|
||||
it "should have a nextOrder function", ->
|
||||
expect(@collection.nextOrder()).toEqual(1)
|
||||
@collection.add([{}])
|
||||
expect(@collection.nextOrder()).toEqual(2)
|
||||
@collection.add([{}])
|
||||
expect(@collection.nextOrder()).toEqual(3)
|
||||
# verify that it doesn't just return an incrementing value each time
|
||||
expect(@collection.nextOrder()).toEqual(3)
|
||||
# try going back one
|
||||
@collection.remove(@collection.last())
|
||||
expect(@collection.nextOrder()).toEqual(2)
|
||||
it "should be empty by default", ->
|
||||
expect(@model.isEmpty()).toBeTruthy()
|
||||
|
||||
describe "Validation", ->
|
||||
it "requires a name", ->
|
||||
model = new Chapter({name: "", asset_path: "a.pdf"})
|
||||
expect(model.isValid()).toBeFalsy()
|
||||
|
||||
it "requires an asset_path", ->
|
||||
model = new Chapter({name: "a", asset_path: ""})
|
||||
expect(model.isValid()).toBeFalsy()
|
||||
|
||||
it "can pass validation", ->
|
||||
model = new Chapter({name: "a", asset_path: "a.pdf"})
|
||||
expect(model.isValid()).toBeTruthy()
|
||||
|
||||
|
||||
describe "Chapter collection", ->
|
||||
beforeEach ->
|
||||
@collection = new ChapterSet()
|
||||
|
||||
it "is empty by default", ->
|
||||
expect(@collection.isEmpty()).toBeTruthy()
|
||||
|
||||
it "is empty if all chapters are empty", ->
|
||||
@collection.add([{}, {}, {}])
|
||||
expect(@collection.isEmpty()).toBeTruthy()
|
||||
|
||||
it "is not empty if a chapter is not empty", ->
|
||||
@collection.add([{}, {name: "full"}, {}])
|
||||
expect(@collection.isEmpty()).toBeFalsy()
|
||||
|
||||
it "should have a nextOrder function", ->
|
||||
expect(@collection.nextOrder()).toEqual(1)
|
||||
@collection.add([{}])
|
||||
expect(@collection.nextOrder()).toEqual(2)
|
||||
@collection.add([{}])
|
||||
expect(@collection.nextOrder()).toEqual(3)
|
||||
# verify that it doesn't just return an incrementing value each time
|
||||
expect(@collection.nextOrder()).toEqual(3)
|
||||
# try going back one
|
||||
@collection.remove(@collection.last())
|
||||
expect(@collection.nextOrder()).toEqual(2)
|
||||
|
||||
@@ -1,56 +1,58 @@
|
||||
describe "CMS.Models.FileUpload", ->
|
||||
beforeEach ->
|
||||
@model = new CMS.Models.FileUpload()
|
||||
define ["js/models/uploads"], (FileUpload) ->
|
||||
|
||||
it "is unfinished by default", ->
|
||||
expect(@model.get("finished")).toBeFalsy()
|
||||
describe "FileUpload", ->
|
||||
beforeEach ->
|
||||
@model = new FileUpload()
|
||||
|
||||
it "is not uploading by default", ->
|
||||
expect(@model.get("uploading")).toBeFalsy()
|
||||
it "is unfinished by default", ->
|
||||
expect(@model.get("finished")).toBeFalsy()
|
||||
|
||||
it "is valid by default", ->
|
||||
expect(@model.isValid()).toBeTruthy()
|
||||
it "is not uploading by default", ->
|
||||
expect(@model.get("uploading")).toBeFalsy()
|
||||
|
||||
it "is invalid for text files by default", ->
|
||||
file = {"type": "text/plain"}
|
||||
@model.set("selectedFile", file);
|
||||
expect(@model.isValid()).toBeFalsy()
|
||||
it "is valid by default", ->
|
||||
expect(@model.isValid()).toBeTruthy()
|
||||
|
||||
it "is invalid for PNG files by default", ->
|
||||
file = {"type": "image/png"}
|
||||
@model.set("selectedFile", file);
|
||||
expect(@model.isValid()).toBeFalsy()
|
||||
it "is invalid for text files by default", ->
|
||||
file = {"type": "text/plain"}
|
||||
@model.set("selectedFile", file);
|
||||
expect(@model.isValid()).toBeFalsy()
|
||||
|
||||
it "can accept a file type when explicitly set", ->
|
||||
file = {"type": "image/png"}
|
||||
@model.set("mimeTypes": ["image/png"])
|
||||
@model.set("selectedFile", file)
|
||||
expect(@model.isValid()).toBeTruthy()
|
||||
it "is invalid for PNG files by default", ->
|
||||
file = {"type": "image/png"}
|
||||
@model.set("selectedFile", file);
|
||||
expect(@model.isValid()).toBeFalsy()
|
||||
|
||||
it "can accept multiple file types", ->
|
||||
file = {"type": "image/gif"}
|
||||
@model.set("mimeTypes": ["image/png", "image/jpeg", "image/gif"])
|
||||
@model.set("selectedFile", file)
|
||||
expect(@model.isValid()).toBeTruthy()
|
||||
it "can accept a file type when explicitly set", ->
|
||||
file = {"type": "image/png"}
|
||||
@model.set("mimeTypes": ["image/png"])
|
||||
@model.set("selectedFile", file)
|
||||
expect(@model.isValid()).toBeTruthy()
|
||||
|
||||
describe "fileTypes", ->
|
||||
it "returns a list of the uploader's file types", ->
|
||||
@model.set('mimeTypes', ['image/png', 'application/json'])
|
||||
expect(@model.fileTypes()).toEqual(['PNG', 'JSON'])
|
||||
it "can accept multiple file types", ->
|
||||
file = {"type": "image/gif"}
|
||||
@model.set("mimeTypes": ["image/png", "image/jpeg", "image/gif"])
|
||||
@model.set("selectedFile", file)
|
||||
expect(@model.isValid()).toBeTruthy()
|
||||
|
||||
describe "formatValidTypes", ->
|
||||
it "returns a map of formatted file types and extensions", ->
|
||||
@model.set('mimeTypes', ['image/png', 'image/jpeg', 'application/json'])
|
||||
formatted = @model.formatValidTypes()
|
||||
expect(formatted).toEqual(
|
||||
fileTypes: 'PNG, JPEG or JSON',
|
||||
fileExtensions: '.png, .jpeg or .json'
|
||||
)
|
||||
describe "fileTypes", ->
|
||||
it "returns a list of the uploader's file types", ->
|
||||
@model.set('mimeTypes', ['image/png', 'application/json'])
|
||||
expect(@model.fileTypes()).toEqual(['PNG', 'JSON'])
|
||||
|
||||
it "does not format with only one mime type", ->
|
||||
@model.set('mimeTypes', ['application/pdf'])
|
||||
formatted = @model.formatValidTypes()
|
||||
expect(formatted).toEqual(
|
||||
fileTypes: 'PDF',
|
||||
fileExtensions: '.pdf'
|
||||
)
|
||||
describe "formatValidTypes", ->
|
||||
it "returns a map of formatted file types and extensions", ->
|
||||
@model.set('mimeTypes', ['image/png', 'image/jpeg', 'application/json'])
|
||||
formatted = @model.formatValidTypes()
|
||||
expect(formatted).toEqual(
|
||||
fileTypes: 'PNG, JPEG or JSON',
|
||||
fileExtensions: '.png, .jpeg or .json'
|
||||
)
|
||||
|
||||
it "does not format with only one mime type", ->
|
||||
@model.set('mimeTypes', ['application/pdf'])
|
||||
formatted = @model.formatValidTypes()
|
||||
expect(formatted).toEqual(
|
||||
fileTypes: 'PDF',
|
||||
fileExtensions: '.pdf'
|
||||
)
|
||||
|
||||
61
cms/static/coffee/spec/setup_require.coffee
Normal file
61
cms/static/coffee/spec/setup_require.coffee
Normal file
@@ -0,0 +1,61 @@
|
||||
require =
|
||||
baseUrl: "/suite/cms/include"
|
||||
paths:
|
||||
"jquery": "xmodule_js/common_static/js/vendor/jquery.min",
|
||||
"jquery.ui" : "xmodule_js/common_static/js/vendor/jquery-ui.min",
|
||||
"jquery.cookie": "xmodule_js/common_static/js/vendor/jquery.cookie",
|
||||
"underscore": "xmodule_js/common_static/js/vendor/underscore-min",
|
||||
"underscore.string": "xmodule_js/common_static/js/vendor/underscore.string.min",
|
||||
"backbone": "xmodule_js/common_static/js/vendor/backbone-min",
|
||||
"backbone.associations": "xmodule_js/common_static/js/vendor/backbone-associations-min",
|
||||
"jquery.timepicker": "xmodule_js/common_static/js/vendor/timepicker/jquery.timepicker",
|
||||
"jquery.leanModal": "xmodule_js/common_static/js/vendor/jquery.leanModal.min",
|
||||
"jquery.scrollTo": "xmodule_js/common_static/js/vendor/jquery.scrollTo-1.4.2-min",
|
||||
"jquery.flot": "xmodule_js/common_static/js/vendor/flot/jquery.flot.min",
|
||||
"jquery.form": "xmodule_js/common_static/js/vendor/jquery.form",
|
||||
"jquery.inputnumber": "xmodule_js/common_static/js/vendor/html5-input-polyfills/number-polyfill",
|
||||
"sinon": "xmodule_js/common_static/js/vendor/sinon-1.7.1",
|
||||
"xmodule": "xmodule_js/src/xmodule",
|
||||
"gettext": "xmodule_js/common_static/js/test/i18n",
|
||||
"utility": "xmodule_js/common_static/js/src/utility",
|
||||
"codemirror": "xmodule_js/common_static/js/vendor/CodeMirror/codemirror"
|
||||
shim:
|
||||
"gettext":
|
||||
exports: "gettext"
|
||||
"jquery.ui":
|
||||
deps: ["jquery"]
|
||||
exports: "jQuery.ui"
|
||||
"jquery.form":
|
||||
deps: ["jquery"]
|
||||
exports: "jQuery.fn.ajaxForm"
|
||||
"jquery.inputnumber":
|
||||
deps: ["jquery"]
|
||||
exports: "jQuery.fn.inputNumber"
|
||||
"jquery.leanModal":
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.leanModal"
|
||||
"jquery.cookie":
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.cookie"
|
||||
"jquery.scrollTo":
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.scrollTo"
|
||||
"jquery.flot":
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.plot"
|
||||
"underscore":
|
||||
exports: "_"
|
||||
"backbone":
|
||||
deps: ["underscore", "jquery"],
|
||||
exports: "Backbone"
|
||||
"backbone.associations":
|
||||
deps: ["backbone"],
|
||||
exports: "Backbone.Associations"
|
||||
"xmodule":
|
||||
exports: "XModule"
|
||||
"sinon":
|
||||
exports: "sinon"
|
||||
"codemirror":
|
||||
exports: "CodeMirror"
|
||||
# load these automatically
|
||||
deps: ["js/base", "coffee/src/main"]
|
||||
@@ -1,162 +1,227 @@
|
||||
feedbackTpl = readFixtures('system-feedback.underscore')
|
||||
assetTpl = readFixtures('asset.underscore')
|
||||
define ["jasmine", "sinon", "squire"],
|
||||
(jasmine, sinon, Squire) ->
|
||||
|
||||
describe "CMS.Views.Asset", ->
|
||||
beforeEach ->
|
||||
setFixtures($("<script>", {id: "asset-tpl", type: "text/template"}).text(assetTpl))
|
||||
appendSetFixtures($("<script>", {id: "system-feedback-tpl", type: "text/template"}).text(feedbackTpl))
|
||||
appendSetFixtures(sandbox({id: "page-prompt"}))
|
||||
@model = new CMS.Models.Asset({display_name: "test asset", url: 'actual_asset_url', portable_url: 'portable_url', date_added: 'date', thumbnail: null, id: 'id'})
|
||||
spyOn(@model, "destroy").andCallThrough()
|
||||
spyOn(@model, "save").andCallThrough()
|
||||
feedbackTpl = readFixtures('system-feedback.underscore')
|
||||
assetTpl = readFixtures('asset.underscore')
|
||||
|
||||
@collection = new CMS.Models.AssetCollection([@model])
|
||||
@collection.url = "update-asset-url"
|
||||
@view = new CMS.Views.Asset({model: @model})
|
||||
|
||||
@promptSpies = spyOnConstructor(CMS.Views.Prompt, "Warning", ["show", "hide"])
|
||||
@promptSpies.show.andReturn(@promptSpies)
|
||||
|
||||
describe "Basic", ->
|
||||
it "should render properly", ->
|
||||
@view.render()
|
||||
expect(@view.$el).toContainText("test asset")
|
||||
|
||||
it "should pop a delete confirmation when the delete button is clicked", ->
|
||||
@view.render().$(".remove-asset-button").click()
|
||||
expect(@promptSpies.constructor).toHaveBeenCalled()
|
||||
ctorOptions = @promptSpies.constructor.mostRecentCall.args[0]
|
||||
expect(ctorOptions.title).toMatch('Delete File Confirmation')
|
||||
# hasn't actually been removed
|
||||
expect(@model.destroy).not.toHaveBeenCalled()
|
||||
expect(@collection).toContain(@model)
|
||||
|
||||
describe "AJAX", ->
|
||||
describe "Asset view", ->
|
||||
beforeEach ->
|
||||
setFixtures($("<script>", {id: "asset-tpl", type: "text/template"}).text(assetTpl))
|
||||
appendSetFixtures($("<script>", {id: "system-feedback-tpl", type: "text/template"}).text(feedbackTpl))
|
||||
appendSetFixtures(sandbox({id: "page-prompt"}))
|
||||
|
||||
@promptSpies = jasmine.createSpyObj('Prompt.Warning', ["constructor", "show", "hide"])
|
||||
@promptSpies.constructor.andReturn(@promptSpies)
|
||||
@promptSpies.show.andReturn(@promptSpies)
|
||||
|
||||
@confirmationSpies = jasmine.createSpyObj('Notification.Confirmation', ["constructor", "show"])
|
||||
@confirmationSpies.constructor.andReturn(@confirmationSpies)
|
||||
@confirmationSpies.show.andReturn(@confirmationSpies)
|
||||
|
||||
@savingSpies = jasmine.createSpyObj('Notification.Mini', ["constructor", "show", "hide"])
|
||||
@savingSpies.constructor.andReturn(@savingSpies)
|
||||
@savingSpies.show.andReturn(@savingSpies)
|
||||
|
||||
@injector = new Squire()
|
||||
@injector.mock("js/views/feedback_prompt", {
|
||||
"Warning": @promptSpies.constructor
|
||||
})
|
||||
@injector.mock("js/views/feedback_notification", {
|
||||
"Confirmation": @confirmationSpies.constructor,
|
||||
"Mini": @savingSpies.constructor
|
||||
})
|
||||
runs =>
|
||||
@injector.require ["js/models/asset", "js/collections/asset", "js/views/asset"],
|
||||
(AssetModel, AssetCollection, AssetView) =>
|
||||
@model = new AssetModel
|
||||
display_name: "test asset"
|
||||
url: 'actual_asset_url'
|
||||
portable_url: 'portable_url'
|
||||
date_added: 'date'
|
||||
thumbnail: null
|
||||
id: 'id'
|
||||
spyOn(@model, "destroy").andCallThrough()
|
||||
spyOn(@model, "save").andCallThrough()
|
||||
|
||||
@collection = new AssetCollection([@model])
|
||||
@collection.url = "update-asset-url"
|
||||
@view = new AssetView({model: @model})
|
||||
|
||||
waitsFor (=> @view), "AssetView was not created", 1000
|
||||
|
||||
afterEach ->
|
||||
@injector.clean()
|
||||
@injector.remove()
|
||||
|
||||
describe "Basic", ->
|
||||
it "should render properly", ->
|
||||
@view.render()
|
||||
expect(@view.$el).toContainText("test asset")
|
||||
|
||||
it "should pop a delete confirmation when the delete button is clicked", ->
|
||||
@view.render().$(".remove-asset-button").click()
|
||||
expect(@promptSpies.constructor).toHaveBeenCalled()
|
||||
ctorOptions = @promptSpies.constructor.mostRecentCall.args[0]
|
||||
expect(ctorOptions.title).toMatch('Delete File Confirmation')
|
||||
# hasn't actually been removed
|
||||
expect(@model.destroy).not.toHaveBeenCalled()
|
||||
expect(@collection).toContain(@model)
|
||||
|
||||
describe "AJAX", ->
|
||||
beforeEach ->
|
||||
@requests = requests = []
|
||||
@xhr = sinon.useFakeXMLHttpRequest()
|
||||
@xhr.onCreate = (xhr) -> requests.push(xhr)
|
||||
|
||||
afterEach ->
|
||||
@xhr.restore()
|
||||
|
||||
it "should destroy itself on confirmation", ->
|
||||
@view.render().$(".remove-asset-button").click()
|
||||
ctorOptions = @promptSpies.constructor.mostRecentCall.args[0]
|
||||
# run the primary function to indicate confirmation
|
||||
ctorOptions.actions.primary.click(@promptSpies)
|
||||
# AJAX request has been sent, but not yet returned
|
||||
expect(@model.destroy).toHaveBeenCalled()
|
||||
expect(@requests.length).toEqual(1)
|
||||
expect(@confirmationSpies.constructor).not.toHaveBeenCalled()
|
||||
expect(@collection.contains(@model)).toBeTruthy()
|
||||
# return a success response
|
||||
@requests[0].respond(200)
|
||||
expect(@confirmationSpies.constructor).toHaveBeenCalled()
|
||||
expect(@confirmationSpies.show).toHaveBeenCalled()
|
||||
savingOptions = @confirmationSpies.constructor.mostRecentCall.args[0]
|
||||
expect(savingOptions.title).toMatch("Your file has been deleted.")
|
||||
expect(@collection.contains(@model)).toBeFalsy()
|
||||
|
||||
it "should not destroy itself if server errors", ->
|
||||
@view.render().$(".remove-asset-button").click()
|
||||
ctorOptions = @promptSpies.constructor.mostRecentCall.args[0]
|
||||
# run the primary function to indicate confirmation
|
||||
ctorOptions.actions.primary.click(@promptSpies)
|
||||
# AJAX request has been sent, but not yet returned
|
||||
expect(@model.destroy).toHaveBeenCalled()
|
||||
# return an error response
|
||||
@requests[0].respond(404)
|
||||
expect(@confirmationSpies.constructor).not.toHaveBeenCalled()
|
||||
expect(@collection.contains(@model)).toBeTruthy()
|
||||
|
||||
it "should lock the asset on confirmation", ->
|
||||
@view.render().$(".lock-checkbox").click()
|
||||
# AJAX request has been sent, but not yet returned
|
||||
expect(@model.save).toHaveBeenCalled()
|
||||
expect(@requests.length).toEqual(1)
|
||||
expect(@savingSpies.constructor).toHaveBeenCalled()
|
||||
expect(@savingSpies.show).toHaveBeenCalled()
|
||||
savingOptions = @savingSpies.constructor.mostRecentCall.args[0]
|
||||
expect(savingOptions.title).toMatch("Saving...")
|
||||
expect(@model.get("locked")).toBeFalsy()
|
||||
# return a success response
|
||||
@requests[0].respond(200)
|
||||
expect(@savingSpies.hide).toHaveBeenCalled()
|
||||
expect(@model.get("locked")).toBeTruthy()
|
||||
|
||||
it "should not lock the asset if server errors", ->
|
||||
@view.render().$(".lock-checkbox").click()
|
||||
# return an error response
|
||||
@requests[0].respond(404)
|
||||
# Don't call hide because that closes the notification showing the server error.
|
||||
expect(@savingSpies.hide).not.toHaveBeenCalled()
|
||||
expect(@model.get("locked")).toBeFalsy()
|
||||
|
||||
describe "Assets view", ->
|
||||
beforeEach ->
|
||||
setFixtures($("<script>", {id: "asset-tpl", type: "text/template"}).text(assetTpl))
|
||||
window.analytics = jasmine.createSpyObj('analytics', ['track'])
|
||||
window.course_location_analytics = jasmine.createSpy()
|
||||
appendSetFixtures(sandbox({id: "asset_table_body"}))
|
||||
|
||||
@promptSpies = jasmine.createSpyObj('Prompt.Warning', ["constructor", "show", "hide"])
|
||||
@promptSpies.constructor.andReturn(@promptSpies)
|
||||
@promptSpies.show.andReturn(@promptSpies)
|
||||
|
||||
@injector = new Squire()
|
||||
@injector.mock("js/views/feedback_prompt", {
|
||||
"Warning": @promptSpies.constructor
|
||||
})
|
||||
|
||||
runs =>
|
||||
@injector.require ["js/models/asset", "js/collections/asset", "js/views/assets"],
|
||||
(AssetModel, AssetCollection, AssetsView) =>
|
||||
@AssetModel = AssetModel
|
||||
@collection = new AssetCollection [
|
||||
display_name: "test asset 1"
|
||||
url: 'actual_asset_url_1'
|
||||
portable_url: 'portable_url_1'
|
||||
date_added: 'date_1'
|
||||
thumbnail: null
|
||||
id: 'id_1'
|
||||
,
|
||||
display_name: "test asset 2"
|
||||
url: 'actual_asset_url_2'
|
||||
portable_url: 'portable_url_2'
|
||||
date_added: 'date_2'
|
||||
thumbnail: null
|
||||
id: 'id_2'
|
||||
]
|
||||
@collection.url = "update-asset-url"
|
||||
@view = new AssetsView
|
||||
collection: @collection
|
||||
el: $('#asset_table_body')
|
||||
|
||||
waitsFor (=> @view), "AssetView was not created", 1000
|
||||
|
||||
$.ajax()
|
||||
@requests = requests = []
|
||||
@xhr = sinon.useFakeXMLHttpRequest()
|
||||
@xhr.onCreate = (xhr) -> requests.push(xhr)
|
||||
|
||||
@confirmationSpies = spyOnConstructor(CMS.Views.Notification, "Confirmation", ["show"])
|
||||
@confirmationSpies.show.andReturn(@confirmationSpies)
|
||||
|
||||
@savingSpies = spyOnConstructor(CMS.Views.Notification, "Mini", ["show", "hide"])
|
||||
@savingSpies.show.andReturn(@savingSpies)
|
||||
|
||||
afterEach ->
|
||||
@xhr.restore()
|
||||
delete window.analytics
|
||||
delete window.course_location_analytics
|
||||
|
||||
it "should destroy itself on confirmation", ->
|
||||
@view.render().$(".remove-asset-button").click()
|
||||
ctorOptions = @promptSpies.constructor.mostRecentCall.args[0]
|
||||
# run the primary function to indicate confirmation
|
||||
ctorOptions.actions.primary.click(@promptSpies)
|
||||
# AJAX request has been sent, but not yet returned
|
||||
expect(@model.destroy).toHaveBeenCalled()
|
||||
expect(@requests.length).toEqual(1)
|
||||
expect(@confirmationSpies.constructor).not.toHaveBeenCalled()
|
||||
expect(@collection.contains(@model)).toBeTruthy()
|
||||
# return a success response
|
||||
@requests[0].respond(200)
|
||||
expect(@confirmationSpies.constructor).toHaveBeenCalled()
|
||||
expect(@confirmationSpies.show).toHaveBeenCalled()
|
||||
savingOptions = @confirmationSpies.constructor.mostRecentCall.args[0]
|
||||
expect(savingOptions.title).toMatch("Your file has been deleted.")
|
||||
expect(@collection.contains(@model)).toBeFalsy()
|
||||
@injector.clean()
|
||||
@injector.remove()
|
||||
|
||||
it "should not destroy itself if server errors", ->
|
||||
@view.render().$(".remove-asset-button").click()
|
||||
ctorOptions = @promptSpies.constructor.mostRecentCall.args[0]
|
||||
# run the primary function to indicate confirmation
|
||||
ctorOptions.actions.primary.click(@promptSpies)
|
||||
# AJAX request has been sent, but not yet returned
|
||||
expect(@model.destroy).toHaveBeenCalled()
|
||||
# return an error response
|
||||
@requests[0].respond(404)
|
||||
expect(@confirmationSpies.constructor).not.toHaveBeenCalled()
|
||||
expect(@collection.contains(@model)).toBeTruthy()
|
||||
describe "Basic", ->
|
||||
it "should render both assets", ->
|
||||
@view.render()
|
||||
expect(@view.$el).toContainText("test asset 1")
|
||||
expect(@view.$el).toContainText("test asset 2")
|
||||
|
||||
it "should lock the asset on confirmation", ->
|
||||
@view.render().$(".lock-checkbox").click()
|
||||
# AJAX request has been sent, but not yet returned
|
||||
expect(@model.save).toHaveBeenCalled()
|
||||
expect(@requests.length).toEqual(1)
|
||||
expect(@savingSpies.constructor).toHaveBeenCalled()
|
||||
expect(@savingSpies.show).toHaveBeenCalled()
|
||||
savingOptions = @savingSpies.constructor.mostRecentCall.args[0]
|
||||
expect(savingOptions.title).toMatch("Saving...")
|
||||
expect(@model.get("locked")).toBeFalsy()
|
||||
# return a success response
|
||||
@requests[0].respond(200)
|
||||
expect(@savingSpies.hide).toHaveBeenCalled()
|
||||
expect(@model.get("locked")).toBeTruthy()
|
||||
it "should remove the deleted asset from the view", ->
|
||||
# Delete the 2nd asset with success from server.
|
||||
@view.render().$(".remove-asset-button")[1].click()
|
||||
@promptSpies.constructor.mostRecentCall.args[0].actions.primary.click(@promptSpies)
|
||||
req.respond(200) for req in @requests
|
||||
expect(@view.$el).toContainText("test asset 1")
|
||||
expect(@view.$el).not.toContainText("test asset 2")
|
||||
|
||||
it "should not lock the asset if server errors", ->
|
||||
@view.render().$(".lock-checkbox").click()
|
||||
# return an error response
|
||||
@requests[0].respond(404)
|
||||
# Don't call hide because that closes the notification showing the server error.
|
||||
expect(@savingSpies.hide).not.toHaveBeenCalled()
|
||||
expect(@model.get("locked")).toBeFalsy()
|
||||
it "does not remove asset if deletion failed", ->
|
||||
# Delete the 2nd asset, but mimic a failure from the server.
|
||||
@view.render().$(".remove-asset-button")[1].click()
|
||||
@promptSpies.constructor.mostRecentCall.args[0].actions.primary.click(@promptSpies)
|
||||
req.respond(404) for req in @requests
|
||||
expect(@view.$el).toContainText("test asset 1")
|
||||
expect(@view.$el).toContainText("test asset 2")
|
||||
|
||||
describe "CMS.Views.Assets", ->
|
||||
beforeEach ->
|
||||
setFixtures($("<script>", {id: "asset-tpl", type: "text/template"}).text(assetTpl))
|
||||
window.analytics = jasmine.createSpyObj('analytics', ['track'])
|
||||
window.course_location_analytics = jasmine.createSpy()
|
||||
appendSetFixtures(sandbox({id: "asset_table_body"}))
|
||||
@collection = new CMS.Models.AssetCollection(
|
||||
[
|
||||
{display_name: "test asset 1", url: 'actual_asset_url_1', portable_url: 'portable_url_1', date_added: 'date_1', thumbnail: null, id: 'id_1'},
|
||||
{display_name: "test asset 2", url: 'actual_asset_url_2', portable_url: 'portable_url_2', date_added: 'date_2', thumbnail: null, id: 'id_2'}
|
||||
])
|
||||
@collection.url = "update-asset-url"
|
||||
@view = new CMS.Views.Assets({collection: @collection, el: $('#asset_table_body')})
|
||||
it "adds an asset if asset does not already exist", ->
|
||||
@view.render()
|
||||
model = new @AssetModel
|
||||
display_name: "new asset"
|
||||
url: 'new_actual_asset_url'
|
||||
portable_url: 'portable_url'
|
||||
date_added: 'date'
|
||||
thumbnail: null
|
||||
id: 'idx'
|
||||
@view.addAsset(model)
|
||||
expect(@view.$el).toContainText("new asset")
|
||||
expect(@collection.models.indexOf(model)).toBe(0)
|
||||
expect(@collection.models.length).toBe(3)
|
||||
|
||||
@promptSpies = spyOnConstructor(CMS.Views.Prompt, "Warning", ["show", "hide"])
|
||||
@promptSpies.show.andReturn(@promptSpies)
|
||||
|
||||
@requests = requests = []
|
||||
@xhr = sinon.useFakeXMLHttpRequest()
|
||||
@xhr.onCreate = (xhr) -> requests.push(xhr)
|
||||
|
||||
afterEach ->
|
||||
delete window.analytics
|
||||
delete window.course_location_analytics
|
||||
|
||||
describe "Basic", ->
|
||||
it "should render both assets", ->
|
||||
@view.render()
|
||||
expect(@view.$el).toContainText("test asset 1")
|
||||
expect(@view.$el).toContainText("test asset 2")
|
||||
|
||||
it "should remove the deleted asset from the view", ->
|
||||
# Delete the 2nd asset with success from server.
|
||||
@view.render().$(".remove-asset-button")[1].click()
|
||||
@promptSpies.constructor.mostRecentCall.args[0].actions.primary.click(@promptSpies)
|
||||
@requests[0].respond(200)
|
||||
expect(@view.$el).toContainText("test asset 1")
|
||||
expect(@view.$el).not.toContainText("test asset 2")
|
||||
|
||||
it "does not remove asset if deletion failed", ->
|
||||
# Delete the 2nd asset, but mimic a failure from the server.
|
||||
@view.render().$(".remove-asset-button")[1].click()
|
||||
@promptSpies.constructor.mostRecentCall.args[0].actions.primary.click(@promptSpies)
|
||||
@requests[0].respond(404)
|
||||
expect(@view.$el).toContainText("test asset 1")
|
||||
expect(@view.$el).toContainText("test asset 2")
|
||||
|
||||
it "adds an asset if asset does not already exist", ->
|
||||
@view.render()
|
||||
model = new CMS.Models.Asset({display_name: "new asset", url: 'new_actual_asset_url', portable_url: 'portable_url', date_added: 'date', thumbnail: null, id: 'idx'})
|
||||
@view.addAsset(model)
|
||||
expect(@view.$el).toContainText("new asset")
|
||||
expect(@collection.models.indexOf(model)).toBe(0)
|
||||
expect(@collection.models.length).toBe(3)
|
||||
|
||||
it "does not add an asset if asset already exists", ->
|
||||
@view.render()
|
||||
spyOn(@collection, "add").andCallThrough()
|
||||
model = @collection.models[1]
|
||||
@view.addAsset(model)
|
||||
expect(@collection.add).not.toHaveBeenCalled()
|
||||
it "does not add an asset if asset already exists", ->
|
||||
@view.render()
|
||||
spyOn(@collection, "add").andCallThrough()
|
||||
model = @collection.models[1]
|
||||
@view.addAsset(model)
|
||||
expect(@collection.add).not.toHaveBeenCalled()
|
||||
|
||||
@@ -1,144 +1,148 @@
|
||||
courseInfoPage = """
|
||||
<div class="course-info-wrapper">
|
||||
<div class="main-column window">
|
||||
<article class="course-updates" id="course-update-view">
|
||||
<ol class="update-list" id="course-update-list"></ol>
|
||||
</article>
|
||||
</div>
|
||||
<div class="sidebar window course-handouts" id="course-handouts-view"></div>
|
||||
</div>
|
||||
"""
|
||||
define ["js/views/course_info_handout", "js/views/course_info_update", "js/models/module_info", "js/collections/course_update", "sinon"],
|
||||
(CourseInfoHandoutsView, CourseInfoUpdateView, ModuleInfo, CourseUpdateCollection, sinon) ->
|
||||
|
||||
commonSetup = () ->
|
||||
window.analytics = jasmine.createSpyObj('analytics', ['track'])
|
||||
window.course_location_analytics = jasmine.createSpy()
|
||||
window.courseUpdatesXhr = sinon.useFakeXMLHttpRequest()
|
||||
requests = []
|
||||
window.courseUpdatesXhr.onCreate = (xhr) -> requests.push(xhr)
|
||||
return requests
|
||||
|
||||
commonCleanup = () ->
|
||||
window.courseUpdatesXhr.restore()
|
||||
delete window.analytics
|
||||
delete window.course_location_analytics
|
||||
|
||||
describe "Course Updates", ->
|
||||
courseInfoTemplate = readFixtures('course_info_update.underscore')
|
||||
courseInfoPage = """
|
||||
<div class="course-info-wrapper">
|
||||
<div class="main-column window">
|
||||
<article class="course-updates" id="course-update-view">
|
||||
<ol class="update-list" id="course-update-list"></ol>
|
||||
</article>
|
||||
</div>
|
||||
<div class="sidebar window course-handouts" id="course-handouts-view"></div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
beforeEach ->
|
||||
setFixtures($("<script>", {id: "course_info_update-tpl", type: "text/template"}).text(courseInfoTemplate))
|
||||
appendSetFixtures courseInfoPage
|
||||
window.analytics = jasmine.createSpyObj('analytics', ['track'])
|
||||
window.course_location_analytics = jasmine.createSpy()
|
||||
|
||||
@collection = new CMS.Models.CourseUpdateCollection()
|
||||
@courseInfoEdit = new CMS.Views.ClassInfoUpdateView({
|
||||
el: $('.course-updates'),
|
||||
collection: @collection,
|
||||
base_asset_url : 'base-asset-url/'
|
||||
})
|
||||
afterEach ->
|
||||
delete window.analytics
|
||||
delete window.course_location_analytics
|
||||
|
||||
@courseInfoEdit.render()
|
||||
describe "Course Updates", ->
|
||||
courseInfoTemplate = readFixtures('course_info_update.underscore')
|
||||
|
||||
@event = {
|
||||
preventDefault : () -> 'no op'
|
||||
}
|
||||
beforeEach ->
|
||||
setFixtures($("<script>", {id: "course_info_update-tpl", type: "text/template"}).text(courseInfoTemplate))
|
||||
appendSetFixtures courseInfoPage
|
||||
|
||||
@createNewUpdate = () ->
|
||||
# Edit button is not in the template under test (it is in parent HTML).
|
||||
# Therefore call onNew directly.
|
||||
courseUpdatesXhr = sinon.useFakeXMLHttpRequest()
|
||||
@courseUpdatesRequests = requests = []
|
||||
courseUpdatesXhr.onCreate = (xhr) -> requests.push(xhr)
|
||||
@xhrRestore = courseUpdatesXhr.restore
|
||||
|
||||
@collection = new CourseUpdateCollection()
|
||||
@courseInfoEdit = new CourseInfoUpdateView({
|
||||
el: $('.course-updates'),
|
||||
collection: @collection,
|
||||
base_asset_url : 'base-asset-url/'
|
||||
})
|
||||
|
||||
@courseInfoEdit.render()
|
||||
|
||||
@event = {
|
||||
preventDefault : () -> 'no op'
|
||||
}
|
||||
|
||||
@createNewUpdate = () ->
|
||||
# Edit button is not in the template under test (it is in parent HTML).
|
||||
# Therefore call onNew directly.
|
||||
@courseInfoEdit.onNew(@event)
|
||||
spyOn(@courseInfoEdit.$codeMirror, 'getValue').andReturn('/static/image.jpg')
|
||||
@courseInfoEdit.$el.find('.save-button').click()
|
||||
|
||||
afterEach ->
|
||||
@xhrRestore()
|
||||
|
||||
it "does not rewrite links on save", ->
|
||||
# Create a new update, verifying that the model is created
|
||||
# in the collection and save is called.
|
||||
expect(@collection.isEmpty()).toBeTruthy()
|
||||
@courseInfoEdit.onNew(@event)
|
||||
expect(@collection.length).toEqual(1)
|
||||
model = @collection.at(0)
|
||||
spyOn(model, "save").andCallThrough()
|
||||
spyOn(@courseInfoEdit.$codeMirror, 'getValue').andReturn('/static/image.jpg')
|
||||
|
||||
# Click the "Save button."
|
||||
@courseInfoEdit.$el.find('.save-button').click()
|
||||
expect(model.save).toHaveBeenCalled()
|
||||
|
||||
@requests = commonSetup()
|
||||
# Verify content sent to server does not have rewritten links.
|
||||
contentSaved = JSON.parse(@courseUpdatesRequests[@courseUpdatesRequests.length - 1].requestBody).content
|
||||
expect(contentSaved).toEqual('/static/image.jpg')
|
||||
|
||||
afterEach ->
|
||||
commonCleanup()
|
||||
it "does rewrite links for preview", ->
|
||||
# Create a new update.
|
||||
@createNewUpdate()
|
||||
|
||||
it "does not rewrite links on save", ->
|
||||
# Create a new update, verifying that the model is created
|
||||
# in the collection and save is called.
|
||||
expect(@collection.isEmpty()).toBeTruthy()
|
||||
@courseInfoEdit.onNew(@event)
|
||||
expect(@collection.length).toEqual(1)
|
||||
model = @collection.at(0)
|
||||
spyOn(model, "save").andCallThrough()
|
||||
spyOn(@courseInfoEdit.$codeMirror, 'getValue').andReturn('/static/image.jpg')
|
||||
# Verify the link is rewritten for preview purposes.
|
||||
previewContents = @courseInfoEdit.$el.find('.update-contents').html()
|
||||
expect(previewContents).toEqual('base-asset-url/image.jpg')
|
||||
|
||||
# Click the "Save button."
|
||||
@courseInfoEdit.$el.find('.save-button').click()
|
||||
expect(model.save).toHaveBeenCalled()
|
||||
it "shows static links in edit mode", ->
|
||||
@createNewUpdate()
|
||||
|
||||
# Verify content sent to server does not have rewritten links.
|
||||
contentSaved = JSON.parse(this.requests[0].requestBody).content
|
||||
expect(contentSaved).toEqual('/static/image.jpg')
|
||||
|
||||
it "does rewrite links for preview", ->
|
||||
# Create a new update.
|
||||
@createNewUpdate()
|
||||
|
||||
# Verify the link is rewritten for preview purposes.
|
||||
previewContents = @courseInfoEdit.$el.find('.update-contents').html()
|
||||
expect(previewContents).toEqual('base-asset-url/image.jpg')
|
||||
|
||||
it "shows static links in edit mode", ->
|
||||
@createNewUpdate()
|
||||
|
||||
# Click edit and verify CodeMirror contents.
|
||||
@courseInfoEdit.$el.find('.edit-button').click()
|
||||
expect(@courseInfoEdit.$codeMirror.getValue()).toEqual('/static/image.jpg')
|
||||
# Click edit and verify CodeMirror contents.
|
||||
@courseInfoEdit.$el.find('.edit-button').click()
|
||||
expect(@courseInfoEdit.$codeMirror.getValue()).toEqual('/static/image.jpg')
|
||||
|
||||
|
||||
describe "Course Handouts", ->
|
||||
handoutsTemplate = readFixtures('course_info_handouts.underscore')
|
||||
describe "Course Handouts", ->
|
||||
handoutsTemplate = readFixtures('course_info_handouts.underscore')
|
||||
|
||||
beforeEach ->
|
||||
setFixtures($("<script>", {id: "course_info_handouts-tpl", type: "text/template"}).text(handoutsTemplate))
|
||||
appendSetFixtures courseInfoPage
|
||||
beforeEach ->
|
||||
setFixtures($("<script>", {id: "course_info_handouts-tpl", type: "text/template"}).text(handoutsTemplate))
|
||||
appendSetFixtures courseInfoPage
|
||||
|
||||
@model = new CMS.Models.ModuleInfo({
|
||||
id: 'handouts-id',
|
||||
data: '/static/fromServer.jpg'
|
||||
})
|
||||
courseHandoutsXhr = sinon.useFakeXMLHttpRequest()
|
||||
@handoutsRequests = requests = []
|
||||
courseHandoutsXhr.onCreate = (xhr) -> requests.push(xhr)
|
||||
@handoutsXhrRestore = courseHandoutsXhr.restore
|
||||
|
||||
@handoutsEdit = new CMS.Views.ClassInfoHandoutsView({
|
||||
el: $('#course-handouts-view'),
|
||||
model: @model,
|
||||
base_asset_url: 'base-asset-url/'
|
||||
});
|
||||
@model = new ModuleInfo({
|
||||
id: 'handouts-id',
|
||||
data: '/static/fromServer.jpg'
|
||||
})
|
||||
|
||||
@handoutsEdit.render()
|
||||
@handoutsEdit = new CourseInfoHandoutsView({
|
||||
el: $('#course-handouts-view'),
|
||||
model: @model,
|
||||
base_asset_url: 'base-asset-url/'
|
||||
});
|
||||
|
||||
@requests = commonSetup()
|
||||
@handoutsEdit.render()
|
||||
|
||||
afterEach ->
|
||||
commonCleanup()
|
||||
afterEach ->
|
||||
@handoutsXhrRestore()
|
||||
|
||||
it "does not rewrite links on save", ->
|
||||
# Enter something in the handouts section, verifying that the model is saved
|
||||
# when "Save" is clicked.
|
||||
@handoutsEdit.$el.find('.edit-button').click()
|
||||
spyOn(@handoutsEdit.$codeMirror, 'getValue').andReturn('/static/image.jpg')
|
||||
spyOn(@model, "save").andCallThrough()
|
||||
@handoutsEdit.$el.find('.save-button').click()
|
||||
expect(@model.save).toHaveBeenCalled()
|
||||
it "does not rewrite links on save", ->
|
||||
# Enter something in the handouts section, verifying that the model is saved
|
||||
# when "Save" is clicked.
|
||||
@handoutsEdit.$el.find('.edit-button').click()
|
||||
spyOn(@handoutsEdit.$codeMirror, 'getValue').andReturn('/static/image.jpg')
|
||||
spyOn(@model, "save").andCallThrough()
|
||||
@handoutsEdit.$el.find('.save-button').click()
|
||||
expect(@model.save).toHaveBeenCalled()
|
||||
|
||||
contentSaved = JSON.parse(this.requests[0].requestBody).data
|
||||
expect(contentSaved).toEqual('/static/image.jpg')
|
||||
contentSaved = JSON.parse(@handoutsRequests[@handoutsRequests.length - 1].requestBody).data
|
||||
expect(contentSaved).toEqual('/static/image.jpg')
|
||||
|
||||
it "does rewrite links in initial content", ->
|
||||
expect(@handoutsEdit.$preview.html().trim()).toBe('base-asset-url/fromServer.jpg')
|
||||
it "does rewrite links in initial content", ->
|
||||
expect(@handoutsEdit.$preview.html().trim()).toBe('base-asset-url/fromServer.jpg')
|
||||
|
||||
it "does rewrite links after edit", ->
|
||||
# Edit handouts and save.
|
||||
@handoutsEdit.$el.find('.edit-button').click()
|
||||
spyOn(@handoutsEdit.$codeMirror, 'getValue').andReturn('/static/image.jpg')
|
||||
@handoutsEdit.$el.find('.save-button').click()
|
||||
it "does rewrite links after edit", ->
|
||||
# Edit handouts and save.
|
||||
@handoutsEdit.$el.find('.edit-button').click()
|
||||
spyOn(@handoutsEdit.$codeMirror, 'getValue').andReturn('/static/image.jpg')
|
||||
@handoutsEdit.$el.find('.save-button').click()
|
||||
|
||||
# Verify preview text.
|
||||
expect(@handoutsEdit.$preview.html().trim()).toBe('base-asset-url/image.jpg')
|
||||
# Verify preview text.
|
||||
expect(@handoutsEdit.$preview.html().trim()).toBe('base-asset-url/image.jpg')
|
||||
|
||||
it "shows static links in edit mode", ->
|
||||
# Click edit and verify CodeMirror contents.
|
||||
@handoutsEdit.$el.find('.edit-button').click()
|
||||
expect(@handoutsEdit.$codeMirror.getValue().trim()).toEqual('/static/fromServer.jpg')
|
||||
it "shows static links in edit mode", ->
|
||||
# Click edit and verify CodeMirror contents.
|
||||
@handoutsEdit.$el.find('.edit-button').click()
|
||||
expect(@handoutsEdit.$codeMirror.getValue().trim()).toEqual('/static/fromServer.jpg')
|
||||
|
||||
|
||||
@@ -1,301 +1,305 @@
|
||||
tpl = readFixtures('system-feedback.underscore')
|
||||
define ["jquery", "js/views/feedback", "js/views/feedback_notification", "js/views/feedback_alert",
|
||||
"js/views/feedback_prompt", "sinon"],
|
||||
($, SystemFeedback, NotificationView, AlertView, PromptView, sinon) ->
|
||||
|
||||
beforeEach ->
|
||||
setFixtures(sandbox({id: "page-alert"}))
|
||||
appendSetFixtures(sandbox({id: "page-notification"}))
|
||||
appendSetFixtures(sandbox({id: "page-prompt"}))
|
||||
appendSetFixtures($("<script>", {id: "system-feedback-tpl", type: "text/template"}).text(tpl))
|
||||
@addMatchers
|
||||
toBeShown: ->
|
||||
@actual.hasClass("is-shown") and not @actual.hasClass("is-hiding")
|
||||
toBeHiding: ->
|
||||
@actual.hasClass("is-hiding") and not @actual.hasClass("is-shown")
|
||||
toContainText: (text) ->
|
||||
# remove this when we upgrade jasmine-jquery
|
||||
trimmedText = $.trim(@actual.text())
|
||||
if text and $.isFunction(text.test)
|
||||
return text.test(trimmedText)
|
||||
else
|
||||
return trimmedText.indexOf(text) != -1;
|
||||
toHaveBeenPrevented: ->
|
||||
# remove this when we upgrade jasmine-jquery
|
||||
eventName = @actual.eventName
|
||||
selector = @actual.selector
|
||||
@message = ->
|
||||
[
|
||||
"Expected event #{eventName} to have been prevented on #{selector}",
|
||||
"Expected event #{eventName} not to have been prevented on #{selector}"
|
||||
]
|
||||
return jasmine.JQuery.events.wasPrevented(selector, eventName)
|
||||
tpl = readFixtures('system-feedback.underscore')
|
||||
|
||||
describe "CMS.Views.SystemFeedback", ->
|
||||
beforeEach ->
|
||||
@options =
|
||||
title: "Portal"
|
||||
message: "Welcome to the Aperture Science Computer-Aided Enrichment Center"
|
||||
# it will be interesting to see when this.render is called, so lets spy on it
|
||||
@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()
|
||||
setFixtures(sandbox({id: "page-alert"}))
|
||||
appendSetFixtures(sandbox({id: "page-notification"}))
|
||||
appendSetFixtures(sandbox({id: "page-prompt"}))
|
||||
appendSetFixtures($("<script>", {id: "system-feedback-tpl", type: "text/template"}).text(tpl))
|
||||
@addMatchers
|
||||
toBeShown: ->
|
||||
@actual.hasClass("is-shown") and not @actual.hasClass("is-hiding")
|
||||
toBeHiding: ->
|
||||
@actual.hasClass("is-hiding") and not @actual.hasClass("is-shown")
|
||||
toContainText: (text) ->
|
||||
# remove this when we upgrade jasmine-jquery
|
||||
trimmedText = $.trim(@actual.text())
|
||||
if text and $.isFunction(text.test)
|
||||
return text.test(trimmedText)
|
||||
else
|
||||
return trimmedText.indexOf(text) != -1;
|
||||
toHaveBeenPrevented: ->
|
||||
# remove this when we upgrade jasmine-jquery
|
||||
eventName = @actual.eventName
|
||||
selector = @actual.selector
|
||||
@message = ->
|
||||
[
|
||||
"Expected event #{eventName} to have been prevented on #{selector}",
|
||||
"Expected event #{eventName} not to have been prevented on #{selector}"
|
||||
]
|
||||
return jasmine.JQuery.events.wasPrevented(selector, eventName)
|
||||
|
||||
afterEach ->
|
||||
@clock.restore()
|
||||
describe "SystemFeedback", ->
|
||||
beforeEach ->
|
||||
@options =
|
||||
title: "Portal"
|
||||
message: "Welcome to the Aperture Science Computer-Aided Enrichment Center"
|
||||
# it will be interesting to see when this.render is called, so lets spy on it
|
||||
@renderSpy = spyOn(AlertView.Confirmation.prototype, 'render').andCallThrough()
|
||||
@showSpy = spyOn(AlertView.Confirmation.prototype, 'show').andCallThrough()
|
||||
@hideSpy = spyOn(AlertView.Confirmation.prototype, 'hide').andCallThrough()
|
||||
@clock = sinon.useFakeTimers()
|
||||
|
||||
it "requires a type and an intent", ->
|
||||
neither = =>
|
||||
new CMS.Views.SystemFeedback(@options)
|
||||
noType = =>
|
||||
options = $.extend({}, @options)
|
||||
options.intent = "confirmation"
|
||||
new CMS.Views.SystemFeedback(options)
|
||||
noIntent = =>
|
||||
options = $.extend({}, @options)
|
||||
options.type = "alert"
|
||||
new CMS.Views.SystemFeedback(options)
|
||||
both = =>
|
||||
options = $.extend({}, @options)
|
||||
options.type = "alert"
|
||||
options.intent = "confirmation"
|
||||
new CMS.Views.SystemFeedback(options)
|
||||
afterEach ->
|
||||
@clock.restore()
|
||||
|
||||
expect(neither).toThrow()
|
||||
expect(noType).toThrow()
|
||||
expect(noIntent).toThrow()
|
||||
expect(both).not.toThrow()
|
||||
it "requires a type and an intent", ->
|
||||
neither = =>
|
||||
new SystemFeedback(@options)
|
||||
noType = =>
|
||||
options = $.extend({}, @options)
|
||||
options.intent = "confirmation"
|
||||
new SystemFeedback(options)
|
||||
noIntent = =>
|
||||
options = $.extend({}, @options)
|
||||
options.type = "alert"
|
||||
new SystemFeedback(options)
|
||||
both = =>
|
||||
options = $.extend({}, @options)
|
||||
options.type = "alert"
|
||||
options.intent = "confirmation"
|
||||
new SystemFeedback(options)
|
||||
|
||||
# for simplicity, we'll use CMS.Views.Alert.Confirmation from here on,
|
||||
# which extends and proxies to CMS.Views.SystemFeedback
|
||||
expect(neither).toThrow()
|
||||
expect(noType).toThrow()
|
||||
expect(noIntent).toThrow()
|
||||
expect(both).not.toThrow()
|
||||
|
||||
it "does not show on initalize", ->
|
||||
view = new CMS.Views.Alert.Confirmation(@options)
|
||||
expect(@renderSpy).not.toHaveBeenCalled()
|
||||
expect(@showSpy).not.toHaveBeenCalled()
|
||||
# for simplicity, we'll use AlertView.Confirmation from here on,
|
||||
# which extends and proxies to SystemFeedback
|
||||
|
||||
it "renders the template", ->
|
||||
view = new CMS.Views.Alert.Confirmation(@options)
|
||||
view.show()
|
||||
it "does not show on initalize", ->
|
||||
view = new AlertView.Confirmation(@options)
|
||||
expect(@renderSpy).not.toHaveBeenCalled()
|
||||
expect(@showSpy).not.toHaveBeenCalled()
|
||||
|
||||
expect(view.$(".action-close")).toBeDefined()
|
||||
expect(view.$('.wrapper')).toBeShown()
|
||||
expect(view.$el).toContainText(@options.title)
|
||||
expect(view.$el).toContainText(@options.message)
|
||||
it "renders the template", ->
|
||||
view = new AlertView.Confirmation(@options)
|
||||
view.show()
|
||||
|
||||
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()
|
||||
expect(view.$(".action-close")).toBeDefined()
|
||||
expect(view.$('.wrapper')).toBeShown()
|
||||
expect(view.$el).toContainText(@options.title)
|
||||
expect(view.$el).toContainText(@options.message)
|
||||
|
||||
describe "CMS.Views.Prompt", ->
|
||||
# for some reason, expect($("body")) blows up the test runner, so this test
|
||||
# just exercises the Prompt rather than asserting on anything. Best I can
|
||||
# do for now. :(
|
||||
it "changes class on body", ->
|
||||
# expect($("body")).not.toHaveClass("prompt-is-shown")
|
||||
view = new CMS.Views.Prompt.Confirmation({
|
||||
title: "Portal"
|
||||
message: "Welcome to the Aperture Science Computer-Aided Enrichment Center"
|
||||
})
|
||||
# expect($("body")).toHaveClass("prompt-is-shown")
|
||||
view.hide()
|
||||
# expect($("body")).not.toHaveClass("prompt-is-shown")
|
||||
it "close button sends a .hide() message", ->
|
||||
view = new AlertView.Confirmation(@options).show()
|
||||
view.$(".action-close").click()
|
||||
expect(@hideSpy).toHaveBeenCalled()
|
||||
@clock.tick(900)
|
||||
expect(view.$('.wrapper')).toBeHiding()
|
||||
|
||||
describe "CMS.Views.Notification.Mini", ->
|
||||
beforeEach ->
|
||||
@view = new CMS.Views.Notification.Mini()
|
||||
describe "PromptView", ->
|
||||
# for some reason, expect($("body")) blows up the test runner, so this test
|
||||
# just exercises the Prompt rather than asserting on anything. Best I can
|
||||
# do for now. :(
|
||||
it "changes class on body", ->
|
||||
# expect($("body")).not.toHaveClass("prompt-is-shown")
|
||||
view = new PromptView.Confirmation({
|
||||
title: "Portal"
|
||||
message: "Welcome to the Aperture Science Computer-Aided Enrichment Center"
|
||||
})
|
||||
# expect($("body")).toHaveClass("prompt-is-shown")
|
||||
view.hide()
|
||||
# expect($("body")).not.toHaveClass("prompt-is-shown")
|
||||
|
||||
it "should have minShown set to 1250 by default", ->
|
||||
expect(@view.options.minShown).toEqual(1250)
|
||||
describe "NotificationView.Mini", ->
|
||||
beforeEach ->
|
||||
@view = new NotificationView.Mini()
|
||||
|
||||
it "should have closeIcon set to false by default", ->
|
||||
expect(@view.options.closeIcon).toBeFalsy()
|
||||
it "should have minShown set to 1250 by default", ->
|
||||
expect(@view.options.minShown).toEqual(1250)
|
||||
|
||||
describe "CMS.Views.SystemFeedback click events", ->
|
||||
beforeEach ->
|
||||
@primaryClickSpy = jasmine.createSpy('primaryClick')
|
||||
@secondaryClickSpy = jasmine.createSpy('secondaryClick')
|
||||
@view = new CMS.Views.Notification.Warning(
|
||||
title: "Unsaved",
|
||||
message: "Your content is currently Unsaved.",
|
||||
actions:
|
||||
primary:
|
||||
text: "Save",
|
||||
class: "save-button",
|
||||
click: @primaryClickSpy
|
||||
secondary:
|
||||
text: "Revert",
|
||||
class: "cancel-button",
|
||||
click: @secondaryClickSpy
|
||||
)
|
||||
@view.show()
|
||||
it "should have closeIcon set to false by default", ->
|
||||
expect(@view.options.closeIcon).toBeFalsy()
|
||||
|
||||
it "should trigger the primary event on a primary click", ->
|
||||
@view.$(".action-primary").click()
|
||||
expect(@primaryClickSpy).toHaveBeenCalled()
|
||||
expect(@secondaryClickSpy).not.toHaveBeenCalled()
|
||||
describe "SystemFeedback click events", ->
|
||||
beforeEach ->
|
||||
@primaryClickSpy = jasmine.createSpy('primaryClick')
|
||||
@secondaryClickSpy = jasmine.createSpy('secondaryClick')
|
||||
@view = new NotificationView.Warning(
|
||||
title: "Unsaved",
|
||||
message: "Your content is currently Unsaved.",
|
||||
actions:
|
||||
primary:
|
||||
text: "Save",
|
||||
class: "save-button",
|
||||
click: @primaryClickSpy
|
||||
secondary:
|
||||
text: "Revert",
|
||||
class: "cancel-button",
|
||||
click: @secondaryClickSpy
|
||||
)
|
||||
@view.show()
|
||||
|
||||
it "should trigger the secondary event on a secondary click", ->
|
||||
@view.$(".action-secondary").click()
|
||||
expect(@secondaryClickSpy).toHaveBeenCalled()
|
||||
expect(@primaryClickSpy).not.toHaveBeenCalled()
|
||||
it "should trigger the primary event on a primary click", ->
|
||||
@view.$(".action-primary").click()
|
||||
expect(@primaryClickSpy).toHaveBeenCalled()
|
||||
expect(@secondaryClickSpy).not.toHaveBeenCalled()
|
||||
|
||||
it "should apply class to primary action", ->
|
||||
expect(@view.$(".action-primary")).toHaveClass("save-button")
|
||||
it "should trigger the secondary event on a secondary click", ->
|
||||
@view.$(".action-secondary").click()
|
||||
expect(@secondaryClickSpy).toHaveBeenCalled()
|
||||
expect(@primaryClickSpy).not.toHaveBeenCalled()
|
||||
|
||||
it "should apply class to secondary action", ->
|
||||
expect(@view.$(".action-secondary")).toHaveClass("cancel-button")
|
||||
it "should apply class to primary action", ->
|
||||
expect(@view.$(".action-primary")).toHaveClass("save-button")
|
||||
|
||||
it "should preventDefault on primary action", ->
|
||||
spyOnEvent(".action-primary", "click")
|
||||
@view.$(".action-primary").click()
|
||||
expect("click").toHaveBeenPreventedOn(".action-primary")
|
||||
it "should apply class to secondary action", ->
|
||||
expect(@view.$(".action-secondary")).toHaveClass("cancel-button")
|
||||
|
||||
it "should preventDefault on secondary action", ->
|
||||
spyOnEvent(".action-secondary", "click")
|
||||
@view.$(".action-secondary").click()
|
||||
expect("click").toHaveBeenPreventedOn(".action-secondary")
|
||||
it "should preventDefault on primary action", ->
|
||||
spyOnEvent(".action-primary", "click")
|
||||
@view.$(".action-primary").click()
|
||||
expect("click").toHaveBeenPreventedOn(".action-primary")
|
||||
|
||||
describe "CMS.Views.SystemFeedback not preventing events", ->
|
||||
beforeEach ->
|
||||
@clickSpy = jasmine.createSpy('clickSpy')
|
||||
@view = new CMS.Views.Alert.Confirmation(
|
||||
title: "It's all good"
|
||||
message: "No reason for this alert"
|
||||
actions:
|
||||
primary:
|
||||
text: "Whatever"
|
||||
click: @clickSpy
|
||||
preventDefault: false
|
||||
)
|
||||
@view.show()
|
||||
it "should preventDefault on secondary action", ->
|
||||
spyOnEvent(".action-secondary", "click")
|
||||
@view.$(".action-secondary").click()
|
||||
expect("click").toHaveBeenPreventedOn(".action-secondary")
|
||||
|
||||
it "should not preventDefault", ->
|
||||
spyOnEvent(".action-primary", "click")
|
||||
@view.$(".action-primary").click()
|
||||
expect("click").not.toHaveBeenPreventedOn(".action-primary")
|
||||
expect(@clickSpy).toHaveBeenCalled()
|
||||
describe "SystemFeedback not preventing events", ->
|
||||
beforeEach ->
|
||||
@clickSpy = jasmine.createSpy('clickSpy')
|
||||
@view = new AlertView.Confirmation(
|
||||
title: "It's all good"
|
||||
message: "No reason for this alert"
|
||||
actions:
|
||||
primary:
|
||||
text: "Whatever"
|
||||
click: @clickSpy
|
||||
preventDefault: false
|
||||
)
|
||||
@view.show()
|
||||
|
||||
describe "CMS.Views.SystemFeedback multiple secondary actions", ->
|
||||
beforeEach ->
|
||||
@secondarySpyOne = jasmine.createSpy('secondarySpyOne')
|
||||
@secondarySpyTwo = jasmine.createSpy('secondarySpyTwo')
|
||||
@view = new CMS.Views.Notification.Warning(
|
||||
title: "No Primary",
|
||||
message: "Pick a secondary action",
|
||||
actions:
|
||||
secondary: [
|
||||
{
|
||||
text: "Option One"
|
||||
class: "option-one"
|
||||
click: @secondarySpyOne
|
||||
}, {
|
||||
text: "Option Two"
|
||||
class: "option-two"
|
||||
click: @secondarySpyTwo
|
||||
}
|
||||
]
|
||||
)
|
||||
@view.show()
|
||||
it "should not preventDefault", ->
|
||||
spyOnEvent(".action-primary", "click")
|
||||
@view.$(".action-primary").click()
|
||||
expect("click").not.toHaveBeenPreventedOn(".action-primary")
|
||||
expect(@clickSpy).toHaveBeenCalled()
|
||||
|
||||
it "should render both", ->
|
||||
expect(@view.el).toContain(".action-secondary.option-one")
|
||||
expect(@view.el).toContain(".action-secondary.option-two")
|
||||
expect(@view.el).not.toContain(".action-secondary.option-one.option-two")
|
||||
expect(@view.$(".action-secondary.option-one")).toContainText("Option One")
|
||||
expect(@view.$(".action-secondary.option-two")).toContainText("Option Two")
|
||||
describe "SystemFeedback multiple secondary actions", ->
|
||||
beforeEach ->
|
||||
@secondarySpyOne = jasmine.createSpy('secondarySpyOne')
|
||||
@secondarySpyTwo = jasmine.createSpy('secondarySpyTwo')
|
||||
@view = new NotificationView.Warning(
|
||||
title: "No Primary",
|
||||
message: "Pick a secondary action",
|
||||
actions:
|
||||
secondary: [
|
||||
{
|
||||
text: "Option One"
|
||||
class: "option-one"
|
||||
click: @secondarySpyOne
|
||||
}, {
|
||||
text: "Option Two"
|
||||
class: "option-two"
|
||||
click: @secondarySpyTwo
|
||||
}
|
||||
]
|
||||
)
|
||||
@view.show()
|
||||
|
||||
it "should differentiate clicks (1)", ->
|
||||
@view.$(".option-one").click()
|
||||
expect(@secondarySpyOne).toHaveBeenCalled()
|
||||
expect(@secondarySpyTwo).not.toHaveBeenCalled()
|
||||
it "should render both", ->
|
||||
expect(@view.el).toContain(".action-secondary.option-one")
|
||||
expect(@view.el).toContain(".action-secondary.option-two")
|
||||
expect(@view.el).not.toContain(".action-secondary.option-one.option-two")
|
||||
expect(@view.$(".action-secondary.option-one")).toContainText("Option One")
|
||||
expect(@view.$(".action-secondary.option-two")).toContainText("Option Two")
|
||||
|
||||
it "should differentiate clicks (2)", ->
|
||||
@view.$(".option-two").click()
|
||||
expect(@secondarySpyOne).not.toHaveBeenCalled()
|
||||
expect(@secondarySpyTwo).toHaveBeenCalled()
|
||||
it "should differentiate clicks (1)", ->
|
||||
@view.$(".option-one").click()
|
||||
expect(@secondarySpyOne).toHaveBeenCalled()
|
||||
expect(@secondarySpyTwo).not.toHaveBeenCalled()
|
||||
|
||||
describe "CMS.Views.Notification minShown and maxShown", ->
|
||||
beforeEach ->
|
||||
@showSpy = spyOn(CMS.Views.Notification.Confirmation.prototype, 'show')
|
||||
@showSpy.andCallThrough()
|
||||
@hideSpy = spyOn(CMS.Views.Notification.Confirmation.prototype, 'hide')
|
||||
@hideSpy.andCallThrough()
|
||||
@clock = sinon.useFakeTimers()
|
||||
it "should differentiate clicks (2)", ->
|
||||
@view.$(".option-two").click()
|
||||
expect(@secondarySpyOne).not.toHaveBeenCalled()
|
||||
expect(@secondarySpyTwo).toHaveBeenCalled()
|
||||
|
||||
afterEach ->
|
||||
@clock.restore()
|
||||
describe "NotificationView minShown and maxShown", ->
|
||||
beforeEach ->
|
||||
@showSpy = spyOn(NotificationView.Confirmation.prototype, 'show')
|
||||
@showSpy.andCallThrough()
|
||||
@hideSpy = spyOn(NotificationView.Confirmation.prototype, 'hide')
|
||||
@hideSpy.andCallThrough()
|
||||
@clock = sinon.useFakeTimers()
|
||||
|
||||
it "should not have minShown or maxShown by default", ->
|
||||
view = new CMS.Views.Notification.Confirmation()
|
||||
expect(view.options.minShown).toEqual(0)
|
||||
expect(view.options.maxShown).toEqual(Infinity)
|
||||
afterEach ->
|
||||
@clock.restore()
|
||||
|
||||
it "a minShown view should not hide too quickly", ->
|
||||
view = new CMS.Views.Notification.Confirmation({minShown: 1000})
|
||||
view.show()
|
||||
expect(view.$('.wrapper')).toBeShown()
|
||||
it "should not have minShown or maxShown by default", ->
|
||||
view = new NotificationView.Confirmation()
|
||||
expect(view.options.minShown).toEqual(0)
|
||||
expect(view.options.maxShown).toEqual(Infinity)
|
||||
|
||||
# call hide() on it, but the minShown should prevent it from hiding right away
|
||||
view.hide()
|
||||
expect(view.$('.wrapper')).toBeShown()
|
||||
it "a minShown view should not hide too quickly", ->
|
||||
view = new NotificationView.Confirmation({minShown: 1000})
|
||||
view.show()
|
||||
expect(view.$('.wrapper')).toBeShown()
|
||||
|
||||
# wait for the minShown timeout to expire, and check again
|
||||
@clock.tick(1001)
|
||||
expect(view.$('.wrapper')).toBeHiding()
|
||||
# call hide() on it, but the minShown should prevent it from hiding right away
|
||||
view.hide()
|
||||
expect(view.$('.wrapper')).toBeShown()
|
||||
|
||||
it "a maxShown view should hide by itself", ->
|
||||
view = new CMS.Views.Notification.Confirmation({maxShown: 1000})
|
||||
view.show()
|
||||
expect(view.$('.wrapper')).toBeShown()
|
||||
# wait for the minShown timeout to expire, and check again
|
||||
@clock.tick(1001)
|
||||
expect(view.$('.wrapper')).toBeHiding()
|
||||
|
||||
# wait for the maxShown timeout to expire, and check again
|
||||
@clock.tick(1001)
|
||||
expect(view.$('.wrapper')).toBeHiding()
|
||||
it "a maxShown view should hide by itself", ->
|
||||
view = new NotificationView.Confirmation({maxShown: 1000})
|
||||
view.show()
|
||||
expect(view.$('.wrapper')).toBeShown()
|
||||
|
||||
it "a minShown view can stay visible longer", ->
|
||||
view = new CMS.Views.Notification.Confirmation({minShown: 1000})
|
||||
view.show()
|
||||
expect(view.$('.wrapper')).toBeShown()
|
||||
# wait for the maxShown timeout to expire, and check again
|
||||
@clock.tick(1001)
|
||||
expect(view.$('.wrapper')).toBeHiding()
|
||||
|
||||
# wait for the minShown timeout to expire, and check again
|
||||
@clock.tick(1001)
|
||||
expect(@hideSpy).not.toHaveBeenCalled()
|
||||
expect(view.$('.wrapper')).toBeShown()
|
||||
it "a minShown view can stay visible longer", ->
|
||||
view = new NotificationView.Confirmation({minShown: 1000})
|
||||
view.show()
|
||||
expect(view.$('.wrapper')).toBeShown()
|
||||
|
||||
# can now hide immediately
|
||||
view.hide()
|
||||
expect(view.$('.wrapper')).toBeHiding()
|
||||
# wait for the minShown timeout to expire, and check again
|
||||
@clock.tick(1001)
|
||||
expect(@hideSpy).not.toHaveBeenCalled()
|
||||
expect(view.$('.wrapper')).toBeShown()
|
||||
|
||||
it "a maxShown view can hide early", ->
|
||||
view = new CMS.Views.Notification.Confirmation({maxShown: 1000})
|
||||
view.show()
|
||||
expect(view.$('.wrapper')).toBeShown()
|
||||
# can now hide immediately
|
||||
view.hide()
|
||||
expect(view.$('.wrapper')).toBeHiding()
|
||||
|
||||
# wait 50 milliseconds, and hide it early
|
||||
@clock.tick(50)
|
||||
view.hide()
|
||||
expect(view.$('.wrapper')).toBeHiding()
|
||||
it "a maxShown view can hide early", ->
|
||||
view = new NotificationView.Confirmation({maxShown: 1000})
|
||||
view.show()
|
||||
expect(view.$('.wrapper')).toBeShown()
|
||||
|
||||
# wait for timeout to expire, make sure it doesn't do anything weird
|
||||
@clock.tick(1000)
|
||||
expect(view.$('.wrapper')).toBeHiding()
|
||||
# wait 50 milliseconds, and hide it early
|
||||
@clock.tick(50)
|
||||
view.hide()
|
||||
expect(view.$('.wrapper')).toBeHiding()
|
||||
|
||||
it "a view can have both maxShown and minShown", ->
|
||||
view = new CMS.Views.Notification.Confirmation({minShown: 1000, maxShown: 2000})
|
||||
view.show()
|
||||
# wait for timeout to expire, make sure it doesn't do anything weird
|
||||
@clock.tick(1000)
|
||||
expect(view.$('.wrapper')).toBeHiding()
|
||||
|
||||
# can't hide early
|
||||
@clock.tick(50)
|
||||
view.hide()
|
||||
expect(view.$('.wrapper')).toBeShown()
|
||||
@clock.tick(1000)
|
||||
expect(view.$('.wrapper')).toBeHiding()
|
||||
it "a view can have both maxShown and minShown", ->
|
||||
view = new NotificationView.Confirmation({minShown: 1000, maxShown: 2000})
|
||||
view.show()
|
||||
|
||||
# show it again, and let it hide automatically
|
||||
view.show()
|
||||
@clock.tick(1050)
|
||||
expect(view.$('.wrapper')).toBeShown()
|
||||
@clock.tick(1000)
|
||||
expect(view.$('.wrapper')).toBeHiding()
|
||||
# can't hide early
|
||||
@clock.tick(50)
|
||||
view.hide()
|
||||
expect(view.$('.wrapper')).toBeShown()
|
||||
@clock.tick(1000)
|
||||
expect(view.$('.wrapper')).toBeHiding()
|
||||
|
||||
# show it again, and let it hide automatically
|
||||
view.show()
|
||||
@clock.tick(1050)
|
||||
expect(view.$('.wrapper')).toBeShown()
|
||||
@clock.tick(1000)
|
||||
expect(view.$('.wrapper')).toBeHiding()
|
||||
|
||||
@@ -1,360 +1,363 @@
|
||||
verifyInputType = (input, expectedType) ->
|
||||
# Some browsers (e.g. FireFox) do not support the "number"
|
||||
# input type. We can accept a "text" input instead
|
||||
# and still get acceptable behavior in the UI.
|
||||
if expectedType == 'number' and input.type != 'number'
|
||||
expectedType = 'text'
|
||||
expect(input.type).toBe(expectedType)
|
||||
define ["js/models/metadata", "js/collections/metadata", "js/views/metadata", "coffee/src/main"],
|
||||
(MetadataModel, MetadataCollection, MetadataView, main) ->
|
||||
verifyInputType = (input, expectedType) ->
|
||||
# Some browsers (e.g. FireFox) do not support the "number"
|
||||
# input type. We can accept a "text" input instead
|
||||
# and still get acceptable behavior in the UI.
|
||||
if expectedType == 'number' and input.type != 'number'
|
||||
expectedType = 'text'
|
||||
expect(input.type).toBe(expectedType)
|
||||
|
||||
describe "Test Metadata Editor", ->
|
||||
editorTemplate = readFixtures('metadata-editor.underscore')
|
||||
numberEntryTemplate = readFixtures('metadata-number-entry.underscore')
|
||||
stringEntryTemplate = readFixtures('metadata-string-entry.underscore')
|
||||
optionEntryTemplate = readFixtures('metadata-option-entry.underscore')
|
||||
listEntryTemplate = readFixtures('metadata-list-entry.underscore')
|
||||
describe "Test Metadata Editor", ->
|
||||
editorTemplate = readFixtures('metadata-editor.underscore')
|
||||
numberEntryTemplate = readFixtures('metadata-number-entry.underscore')
|
||||
stringEntryTemplate = readFixtures('metadata-string-entry.underscore')
|
||||
optionEntryTemplate = readFixtures('metadata-option-entry.underscore')
|
||||
listEntryTemplate = readFixtures('metadata-list-entry.underscore')
|
||||
|
||||
beforeEach ->
|
||||
setFixtures($("<script>", {id: "metadata-editor-tpl", type: "text/template"}).text(editorTemplate))
|
||||
appendSetFixtures($("<script>", {id: "metadata-number-entry", type: "text/template"}).text(numberEntryTemplate))
|
||||
appendSetFixtures($("<script>", {id: "metadata-string-entry", type: "text/template"}).text(stringEntryTemplate))
|
||||
appendSetFixtures($("<script>", {id: "metadata-option-entry", type: "text/template"}).text(optionEntryTemplate))
|
||||
appendSetFixtures($("<script>", {id: "metadata-list-entry", type: "text/template"}).text(listEntryTemplate))
|
||||
|
||||
genericEntry = {
|
||||
default_value: 'default value',
|
||||
display_name: "Display Name",
|
||||
explicitly_set: true,
|
||||
field_name: "display_name",
|
||||
help: "Specifies the name for this component.",
|
||||
options: [],
|
||||
type: CMS.Models.Metadata.GENERIC_TYPE,
|
||||
value: "Word cloud"
|
||||
}
|
||||
|
||||
selectEntry = {
|
||||
default_value: "answered",
|
||||
display_name: "Show Answer",
|
||||
explicitly_set: false,
|
||||
field_name: "show_answer",
|
||||
help: "When should you show the answer",
|
||||
options: [
|
||||
{"display_name": "Always", "value": "always"},
|
||||
{"display_name": "Answered", "value": "answered"},
|
||||
{"display_name": "Never", "value": "never"}
|
||||
],
|
||||
type: CMS.Models.Metadata.SELECT_TYPE,
|
||||
value: "always"
|
||||
}
|
||||
|
||||
integerEntry = {
|
||||
default_value: 5,
|
||||
display_name: "Inputs",
|
||||
explicitly_set: false,
|
||||
field_name: "num_inputs",
|
||||
help: "Number of text boxes for student to input words/sentences.",
|
||||
options: {min: 1},
|
||||
type: CMS.Models.Metadata.INTEGER_TYPE,
|
||||
value: 5
|
||||
}
|
||||
|
||||
floatEntry = {
|
||||
default_value: 2.7,
|
||||
display_name: "Weight",
|
||||
explicitly_set: true,
|
||||
field_name: "weight",
|
||||
help: "Weight for this problem",
|
||||
options: {min: 1.3, max:100.2, step:0.1},
|
||||
type: CMS.Models.Metadata.FLOAT_TYPE,
|
||||
value: 10.2
|
||||
}
|
||||
|
||||
listEntry = {
|
||||
default_value: ["a thing", "another thing"],
|
||||
display_name: "List",
|
||||
explicitly_set: false,
|
||||
field_name: "list",
|
||||
help: "A list of things.",
|
||||
options: [],
|
||||
type: CMS.Models.Metadata.LIST_TYPE,
|
||||
value: ["the first display value", "the second"]
|
||||
}
|
||||
|
||||
# Test for the editor that creates the individual views.
|
||||
describe "CMS.Views.Metadata.Editor creates editors for each field", ->
|
||||
beforeEach ->
|
||||
@model = new CMS.Models.MetadataCollection(
|
||||
[
|
||||
integerEntry,
|
||||
floatEntry,
|
||||
selectEntry,
|
||||
genericEntry,
|
||||
{
|
||||
default_value: null,
|
||||
display_name: "Unknown",
|
||||
explicitly_set: true,
|
||||
field_name: "unknown_type",
|
||||
help: "Mystery property.",
|
||||
options: [
|
||||
{"display_name": "Always", "value": "always"},
|
||||
{"display_name": "Answered", "value": "answered"},
|
||||
{"display_name": "Never", "value": "never"}],
|
||||
type: "unknown type",
|
||||
value: null
|
||||
},
|
||||
listEntry
|
||||
]
|
||||
)
|
||||
|
||||
it "creates child views on initialize, and sorts them alphabetically", ->
|
||||
view = new CMS.Views.Metadata.Editor({collection: @model})
|
||||
childModels = view.collection.models
|
||||
expect(childModels.length).toBe(6)
|
||||
# Be sure to check list view as well as other input types
|
||||
childViews = view.$el.find('.setting-input, .list-settings')
|
||||
expect(childViews.length).toBe(6)
|
||||
|
||||
verifyEntry = (index, display_name, type) ->
|
||||
expect(childModels[index].get('display_name')).toBe(display_name)
|
||||
verifyInputType(childViews[index], type)
|
||||
|
||||
verifyEntry(0, 'Display Name', 'text')
|
||||
verifyEntry(1, 'Inputs', 'number')
|
||||
verifyEntry(2, 'List', '')
|
||||
verifyEntry(3, 'Show Answer', 'select-one')
|
||||
verifyEntry(4, 'Unknown', 'text')
|
||||
verifyEntry(5, 'Weight', 'number')
|
||||
|
||||
it "returns its display name", ->
|
||||
view = new CMS.Views.Metadata.Editor({collection: @model})
|
||||
expect(view.getDisplayName()).toBe("Word cloud")
|
||||
|
||||
it "returns an empty string if there is no display name property with a valid value", ->
|
||||
view = new CMS.Views.Metadata.Editor({collection: new CMS.Models.MetadataCollection()})
|
||||
expect(view.getDisplayName()).toBe("")
|
||||
|
||||
view = new CMS.Views.Metadata.Editor({collection: new CMS.Models.MetadataCollection([
|
||||
{
|
||||
default_value: null,
|
||||
display_name: "Display Name",
|
||||
explicitly_set: false,
|
||||
field_name: "display_name",
|
||||
help: "",
|
||||
options: [],
|
||||
type: CMS.Models.Metadata.GENERIC_TYPE,
|
||||
value: null
|
||||
|
||||
}])
|
||||
})
|
||||
expect(view.getDisplayName()).toBe("")
|
||||
|
||||
it "has no modified values by default", ->
|
||||
view = new CMS.Views.Metadata.Editor({collection: @model})
|
||||
expect(view.getModifiedMetadataValues()).toEqual({})
|
||||
|
||||
it "returns modified values only", ->
|
||||
view = new CMS.Views.Metadata.Editor({collection: @model})
|
||||
childModels = view.collection.models
|
||||
childModels[0].setValue('updated display name')
|
||||
childModels[1].setValue(20)
|
||||
expect(view.getModifiedMetadataValues()).toEqual({
|
||||
display_name : 'updated display name',
|
||||
num_inputs: 20
|
||||
})
|
||||
|
||||
# Tests for individual views.
|
||||
assertInputType = (view, expectedType) ->
|
||||
input = view.$el.find('.setting-input')
|
||||
expect(input.length).toEqual(1)
|
||||
verifyInputType(input[0], expectedType)
|
||||
|
||||
assertValueInView = (view, expectedValue) ->
|
||||
expect(view.getValueFromEditor()).toEqual(expectedValue)
|
||||
|
||||
assertCanUpdateView = (view, newValue) ->
|
||||
view.setValueInEditor(newValue)
|
||||
expect(view.getValueFromEditor()).toEqual(newValue)
|
||||
|
||||
assertClear = (view, modelValue, editorValue=modelValue) ->
|
||||
view.clear()
|
||||
expect(view.model.getValue()).toBe(null)
|
||||
expect(view.model.getDisplayValue()).toEqual(modelValue)
|
||||
expect(view.getValueFromEditor()).toEqual(editorValue)
|
||||
|
||||
assertUpdateModel = (view, originalValue, newValue) ->
|
||||
view.setValueInEditor(newValue)
|
||||
expect(view.model.getValue()).toEqual(originalValue)
|
||||
view.updateModel()
|
||||
expect(view.model.getValue()).toEqual(newValue)
|
||||
|
||||
describe "CMS.Views.Metadata.String is a basic string input with clear functionality", ->
|
||||
beforeEach ->
|
||||
model = new CMS.Models.Metadata(genericEntry)
|
||||
@view = new CMS.Views.Metadata.String({model: model})
|
||||
|
||||
it "uses a text input type", ->
|
||||
assertInputType(@view, 'text')
|
||||
|
||||
it "returns the intial value upon initialization", ->
|
||||
assertValueInView(@view, 'Word cloud')
|
||||
|
||||
it "can update its value in the view", ->
|
||||
assertCanUpdateView(@view, "updated ' \" &")
|
||||
|
||||
it "has a clear method to revert to the model default", ->
|
||||
assertClear(@view, 'default value')
|
||||
|
||||
it "has an update model method", ->
|
||||
assertUpdateModel(@view, 'Word cloud', 'updated')
|
||||
|
||||
describe "CMS.Views.Metadata.Option is an option input type with clear functionality", ->
|
||||
beforeEach ->
|
||||
model = new CMS.Models.Metadata(selectEntry)
|
||||
@view = new CMS.Views.Metadata.Option({model: model})
|
||||
|
||||
it "uses a select input type", ->
|
||||
assertInputType(@view, 'select-one')
|
||||
|
||||
it "returns the intial value upon initialization", ->
|
||||
assertValueInView(@view, 'always')
|
||||
|
||||
it "can update its value in the view", ->
|
||||
assertCanUpdateView(@view, "never")
|
||||
|
||||
it "has a clear method to revert to the model default", ->
|
||||
assertClear(@view, 'answered')
|
||||
|
||||
it "has an update model method", ->
|
||||
assertUpdateModel(@view, null, 'never')
|
||||
|
||||
it "does not update to a value that is not an option", ->
|
||||
@view.setValueInEditor("not an option")
|
||||
expect(@view.getValueFromEditor()).toBe('always')
|
||||
|
||||
describe "CMS.Views.Metadata.Number supports integer or float type and has clear functionality", ->
|
||||
beforeEach ->
|
||||
integerModel = new CMS.Models.Metadata(integerEntry)
|
||||
@integerView = new CMS.Views.Metadata.Number({model: integerModel})
|
||||
|
||||
floatModel = new CMS.Models.Metadata(floatEntry)
|
||||
@floatView = new CMS.Views.Metadata.Number({model: floatModel})
|
||||
|
||||
it "uses a number input type", ->
|
||||
assertInputType(@integerView, 'number')
|
||||
assertInputType(@floatView, 'number')
|
||||
|
||||
it "returns the intial value upon initialization", ->
|
||||
assertValueInView(@integerView, '5')
|
||||
assertValueInView(@floatView, '10.2')
|
||||
|
||||
it "can update its value in the view", ->
|
||||
assertCanUpdateView(@integerView, "12")
|
||||
assertCanUpdateView(@floatView, "-2.4")
|
||||
|
||||
it "has a clear method to revert to the model default", ->
|
||||
assertClear(@integerView, 5, '5')
|
||||
assertClear(@floatView, 2.7, '2.7')
|
||||
|
||||
it "has an update model method", ->
|
||||
assertUpdateModel(@integerView, null, '90')
|
||||
assertUpdateModel(@floatView, 10.2, '-9.5')
|
||||
|
||||
it "knows the difference between integer and float", ->
|
||||
expect(@integerView.isIntegerField()).toBeTruthy()
|
||||
expect(@floatView.isIntegerField()).toBeFalsy()
|
||||
|
||||
it "sets attribtues related to min, max, and step", ->
|
||||
verifyAttributes = (view, min, step, max=null) ->
|
||||
inputEntry = view.$el.find('input')
|
||||
expect(Number(inputEntry.attr('min'))).toEqual(min)
|
||||
expect(Number(inputEntry.attr('step'))).toEqual(step)
|
||||
if max is not null
|
||||
expect(Number(inputEntry.attr('max'))).toEqual(max)
|
||||
|
||||
verifyAttributes(@integerView, 1, 1)
|
||||
verifyAttributes(@floatView, 1.3, .1, 100.2)
|
||||
|
||||
it "corrects values that are out of range", ->
|
||||
verifyValueAfterChanged = (view, value, expectedResult) ->
|
||||
view.setValueInEditor(value)
|
||||
view.changed()
|
||||
expect(view.getValueFromEditor()).toBe(expectedResult)
|
||||
|
||||
verifyValueAfterChanged(@integerView, '-4', '1')
|
||||
verifyValueAfterChanged(@integerView, '1', '1')
|
||||
verifyValueAfterChanged(@integerView, '0', '1')
|
||||
verifyValueAfterChanged(@integerView, '3001', '3001')
|
||||
|
||||
verifyValueAfterChanged(@floatView, '-4', '1.3')
|
||||
verifyValueAfterChanged(@floatView, '1.3', '1.3')
|
||||
verifyValueAfterChanged(@floatView, '1.2', '1.3')
|
||||
verifyValueAfterChanged(@floatView, '100.2', '100.2')
|
||||
verifyValueAfterChanged(@floatView, '100.3', '100.2')
|
||||
|
||||
it "disallows invalid characters", ->
|
||||
verifyValueAfterKeyPressed = (view, character, reject) ->
|
||||
event = {
|
||||
type : 'keypress',
|
||||
which : character.charCodeAt(0),
|
||||
keyCode: character.charCodeAt(0),
|
||||
preventDefault : () -> 'no op'
|
||||
}
|
||||
spyOn(event, 'preventDefault')
|
||||
view.$el.find('input').trigger(event)
|
||||
if (reject)
|
||||
expect(event.preventDefault).toHaveBeenCalled()
|
||||
else
|
||||
expect(event.preventDefault).not.toHaveBeenCalled()
|
||||
|
||||
verifyDisallowedChars = (view) ->
|
||||
verifyValueAfterKeyPressed(view, 'a', true)
|
||||
verifyValueAfterKeyPressed(view, '.', view.isIntegerField())
|
||||
verifyValueAfterKeyPressed(view, '[', true)
|
||||
verifyValueAfterKeyPressed(view, '@', true)
|
||||
|
||||
for i in [0...9]
|
||||
verifyValueAfterKeyPressed(view, String(i), false)
|
||||
|
||||
verifyDisallowedChars(@integerView)
|
||||
verifyDisallowedChars(@floatView)
|
||||
|
||||
describe "CMS.Views.Metadata.List allows the user to enter an ordered list of strings", ->
|
||||
beforeEach ->
|
||||
listModel = new CMS.Models.Metadata(listEntry)
|
||||
@listView = new CMS.Views.Metadata.List({model: listModel})
|
||||
@el = @listView.$el
|
||||
setFixtures($("<script>", {id: "metadata-editor-tpl", type: "text/template"}).text(editorTemplate))
|
||||
appendSetFixtures($("<script>", {id: "metadata-number-entry", type: "text/template"}).text(numberEntryTemplate))
|
||||
appendSetFixtures($("<script>", {id: "metadata-string-entry", type: "text/template"}).text(stringEntryTemplate))
|
||||
appendSetFixtures($("<script>", {id: "metadata-option-entry", type: "text/template"}).text(optionEntryTemplate))
|
||||
appendSetFixtures($("<script>", {id: "metadata-list-entry", type: "text/template"}).text(listEntryTemplate))
|
||||
|
||||
it "returns the initial value upon initialization", ->
|
||||
assertValueInView(@listView, ['the first display value', 'the second'])
|
||||
genericEntry = {
|
||||
default_value: 'default value',
|
||||
display_name: "Display Name",
|
||||
explicitly_set: true,
|
||||
field_name: "display_name",
|
||||
help: "Specifies the name for this component.",
|
||||
options: [],
|
||||
type: MetadataModel.GENERIC_TYPE,
|
||||
value: "Word cloud"
|
||||
}
|
||||
|
||||
it "updates its value correctly", ->
|
||||
assertCanUpdateView(@listView, ['a new item', 'another new item', 'a third'])
|
||||
selectEntry = {
|
||||
default_value: "answered",
|
||||
display_name: "Show Answer",
|
||||
explicitly_set: false,
|
||||
field_name: "show_answer",
|
||||
help: "When should you show the answer",
|
||||
options: [
|
||||
{"display_name": "Always", "value": "always"},
|
||||
{"display_name": "Answered", "value": "answered"},
|
||||
{"display_name": "Never", "value": "never"}
|
||||
],
|
||||
type: MetadataModel.SELECT_TYPE,
|
||||
value: "always"
|
||||
}
|
||||
|
||||
it "has a clear method to revert to the model default", ->
|
||||
assertClear(@listView, ['a thing', 'another thing'])
|
||||
integerEntry = {
|
||||
default_value: 5,
|
||||
display_name: "Inputs",
|
||||
explicitly_set: false,
|
||||
field_name: "num_inputs",
|
||||
help: "Number of text boxes for student to input words/sentences.",
|
||||
options: {min: 1},
|
||||
type: MetadataModel.INTEGER_TYPE,
|
||||
value: 5
|
||||
}
|
||||
|
||||
it "has an update model method", ->
|
||||
assertUpdateModel(@listView, null, ['a new value'])
|
||||
floatEntry = {
|
||||
default_value: 2.7,
|
||||
display_name: "Weight",
|
||||
explicitly_set: true,
|
||||
field_name: "weight",
|
||||
help: "Weight for this problem",
|
||||
options: {min: 1.3, max:100.2, step:0.1},
|
||||
type: MetadataModel.FLOAT_TYPE,
|
||||
value: 10.2
|
||||
}
|
||||
|
||||
it "can add an entry", ->
|
||||
expect(@listView.model.get('value').length).toEqual(2)
|
||||
@el.find('.create-setting').click()
|
||||
expect(@el.find('input.input').length).toEqual(3)
|
||||
listEntry = {
|
||||
default_value: ["a thing", "another thing"],
|
||||
display_name: "List",
|
||||
explicitly_set: false,
|
||||
field_name: "list",
|
||||
help: "A list of things.",
|
||||
options: [],
|
||||
type: MetadataModel.LIST_TYPE,
|
||||
value: ["the first display value", "the second"]
|
||||
}
|
||||
|
||||
it "can remove an entry", ->
|
||||
expect(@listView.model.get('value').length).toEqual(2)
|
||||
@el.find('.remove-setting').first().click()
|
||||
expect(@listView.model.get('value').length).toEqual(1)
|
||||
# Test for the editor that creates the individual views.
|
||||
describe "MetadataView.Editor creates editors for each field", ->
|
||||
beforeEach ->
|
||||
@model = new MetadataCollection(
|
||||
[
|
||||
integerEntry,
|
||||
floatEntry,
|
||||
selectEntry,
|
||||
genericEntry,
|
||||
{
|
||||
default_value: null,
|
||||
display_name: "Unknown",
|
||||
explicitly_set: true,
|
||||
field_name: "unknown_type",
|
||||
help: "Mystery property.",
|
||||
options: [
|
||||
{"display_name": "Always", "value": "always"},
|
||||
{"display_name": "Answered", "value": "answered"},
|
||||
{"display_name": "Never", "value": "never"}],
|
||||
type: "unknown type",
|
||||
value: null
|
||||
},
|
||||
listEntry
|
||||
]
|
||||
)
|
||||
|
||||
it "only allows one blank entry at a time", ->
|
||||
expect(@el.find('input').length).toEqual(2)
|
||||
@el.find('.create-setting').click()
|
||||
@el.find('.create-setting').click()
|
||||
expect(@el.find('input').length).toEqual(3)
|
||||
it "creates child views on initialize, and sorts them alphabetically", ->
|
||||
view = new MetadataView.Editor({collection: @model})
|
||||
childModels = view.collection.models
|
||||
expect(childModels.length).toBe(6)
|
||||
# Be sure to check list view as well as other input types
|
||||
childViews = view.$el.find('.setting-input, .list-settings')
|
||||
expect(childViews.length).toBe(6)
|
||||
|
||||
it "re-enables the add setting button after entering a new value", ->
|
||||
expect(@el.find('input').length).toEqual(2)
|
||||
@el.find('.create-setting').click()
|
||||
expect(@el.find('.create-setting')).toHaveClass('is-disabled')
|
||||
@el.find('input').last().val('third setting')
|
||||
@el.find('input').last().trigger('input')
|
||||
expect(@el.find('.create-setting')).not.toHaveClass('is-disabled')
|
||||
verifyEntry = (index, display_name, type) ->
|
||||
expect(childModels[index].get('display_name')).toBe(display_name)
|
||||
verifyInputType(childViews[index], type)
|
||||
|
||||
verifyEntry(0, 'Display Name', 'text')
|
||||
verifyEntry(1, 'Inputs', 'number')
|
||||
verifyEntry(2, 'List', '')
|
||||
verifyEntry(3, 'Show Answer', 'select-one')
|
||||
verifyEntry(4, 'Unknown', 'text')
|
||||
verifyEntry(5, 'Weight', 'number')
|
||||
|
||||
it "returns its display name", ->
|
||||
view = new MetadataView.Editor({collection: @model})
|
||||
expect(view.getDisplayName()).toBe("Word cloud")
|
||||
|
||||
it "returns an empty string if there is no display name property with a valid value", ->
|
||||
view = new MetadataView.Editor({collection: new MetadataCollection()})
|
||||
expect(view.getDisplayName()).toBe("")
|
||||
|
||||
view = new MetadataView.Editor({collection: new MetadataCollection([
|
||||
{
|
||||
default_value: null,
|
||||
display_name: "Display Name",
|
||||
explicitly_set: false,
|
||||
field_name: "display_name",
|
||||
help: "",
|
||||
options: [],
|
||||
type: MetadataModel.GENERIC_TYPE,
|
||||
value: null
|
||||
|
||||
}])
|
||||
})
|
||||
expect(view.getDisplayName()).toBe("")
|
||||
|
||||
it "has no modified values by default", ->
|
||||
view = new MetadataView.Editor({collection: @model})
|
||||
expect(view.getModifiedMetadataValues()).toEqual({})
|
||||
|
||||
it "returns modified values only", ->
|
||||
view = new MetadataView.Editor({collection: @model})
|
||||
childModels = view.collection.models
|
||||
childModels[0].setValue('updated display name')
|
||||
childModels[1].setValue(20)
|
||||
expect(view.getModifiedMetadataValues()).toEqual({
|
||||
display_name : 'updated display name',
|
||||
num_inputs: 20
|
||||
})
|
||||
|
||||
# Tests for individual views.
|
||||
assertInputType = (view, expectedType) ->
|
||||
input = view.$el.find('.setting-input')
|
||||
expect(input.length).toEqual(1)
|
||||
verifyInputType(input[0], expectedType)
|
||||
|
||||
assertValueInView = (view, expectedValue) ->
|
||||
expect(view.getValueFromEditor()).toEqual(expectedValue)
|
||||
|
||||
assertCanUpdateView = (view, newValue) ->
|
||||
view.setValueInEditor(newValue)
|
||||
expect(view.getValueFromEditor()).toEqual(newValue)
|
||||
|
||||
assertClear = (view, modelValue, editorValue=modelValue) ->
|
||||
view.clear()
|
||||
expect(view.model.getValue()).toBe(null)
|
||||
expect(view.model.getDisplayValue()).toEqual(modelValue)
|
||||
expect(view.getValueFromEditor()).toEqual(editorValue)
|
||||
|
||||
assertUpdateModel = (view, originalValue, newValue) ->
|
||||
view.setValueInEditor(newValue)
|
||||
expect(view.model.getValue()).toEqual(originalValue)
|
||||
view.updateModel()
|
||||
expect(view.model.getValue()).toEqual(newValue)
|
||||
|
||||
describe "MetadataView.String is a basic string input with clear functionality", ->
|
||||
beforeEach ->
|
||||
model = new MetadataModel(genericEntry)
|
||||
@view = new MetadataView.String({model: model})
|
||||
|
||||
it "uses a text input type", ->
|
||||
assertInputType(@view, 'text')
|
||||
|
||||
it "returns the intial value upon initialization", ->
|
||||
assertValueInView(@view, 'Word cloud')
|
||||
|
||||
it "can update its value in the view", ->
|
||||
assertCanUpdateView(@view, "updated ' \" &")
|
||||
|
||||
it "has a clear method to revert to the model default", ->
|
||||
assertClear(@view, 'default value')
|
||||
|
||||
it "has an update model method", ->
|
||||
assertUpdateModel(@view, 'Word cloud', 'updated')
|
||||
|
||||
describe "MetadataView.Option is an option input type with clear functionality", ->
|
||||
beforeEach ->
|
||||
model = new MetadataModel(selectEntry)
|
||||
@view = new MetadataView.Option({model: model})
|
||||
|
||||
it "uses a select input type", ->
|
||||
assertInputType(@view, 'select-one')
|
||||
|
||||
it "returns the intial value upon initialization", ->
|
||||
assertValueInView(@view, 'always')
|
||||
|
||||
it "can update its value in the view", ->
|
||||
assertCanUpdateView(@view, "never")
|
||||
|
||||
it "has a clear method to revert to the model default", ->
|
||||
assertClear(@view, 'answered')
|
||||
|
||||
it "has an update model method", ->
|
||||
assertUpdateModel(@view, null, 'never')
|
||||
|
||||
it "does not update to a value that is not an option", ->
|
||||
@view.setValueInEditor("not an option")
|
||||
expect(@view.getValueFromEditor()).toBe('always')
|
||||
|
||||
describe "MetadataView.Number supports integer or float type and has clear functionality", ->
|
||||
beforeEach ->
|
||||
integerModel = new MetadataModel(integerEntry)
|
||||
@integerView = new MetadataView.Number({model: integerModel})
|
||||
|
||||
floatModel = new MetadataModel(floatEntry)
|
||||
@floatView = new MetadataView.Number({model: floatModel})
|
||||
|
||||
it "uses a number input type", ->
|
||||
assertInputType(@integerView, 'number')
|
||||
assertInputType(@floatView, 'number')
|
||||
|
||||
it "returns the intial value upon initialization", ->
|
||||
assertValueInView(@integerView, '5')
|
||||
assertValueInView(@floatView, '10.2')
|
||||
|
||||
it "can update its value in the view", ->
|
||||
assertCanUpdateView(@integerView, "12")
|
||||
assertCanUpdateView(@floatView, "-2.4")
|
||||
|
||||
it "has a clear method to revert to the model default", ->
|
||||
assertClear(@integerView, 5, '5')
|
||||
assertClear(@floatView, 2.7, '2.7')
|
||||
|
||||
it "has an update model method", ->
|
||||
assertUpdateModel(@integerView, null, '90')
|
||||
assertUpdateModel(@floatView, 10.2, '-9.5')
|
||||
|
||||
it "knows the difference between integer and float", ->
|
||||
expect(@integerView.isIntegerField()).toBeTruthy()
|
||||
expect(@floatView.isIntegerField()).toBeFalsy()
|
||||
|
||||
it "sets attribtues related to min, max, and step", ->
|
||||
verifyAttributes = (view, min, step, max=null) ->
|
||||
inputEntry = view.$el.find('input')
|
||||
expect(Number(inputEntry.attr('min'))).toEqual(min)
|
||||
expect(Number(inputEntry.attr('step'))).toEqual(step)
|
||||
if max is not null
|
||||
expect(Number(inputEntry.attr('max'))).toEqual(max)
|
||||
|
||||
verifyAttributes(@integerView, 1, 1)
|
||||
verifyAttributes(@floatView, 1.3, .1, 100.2)
|
||||
|
||||
it "corrects values that are out of range", ->
|
||||
verifyValueAfterChanged = (view, value, expectedResult) ->
|
||||
view.setValueInEditor(value)
|
||||
view.changed()
|
||||
expect(view.getValueFromEditor()).toBe(expectedResult)
|
||||
|
||||
verifyValueAfterChanged(@integerView, '-4', '1')
|
||||
verifyValueAfterChanged(@integerView, '1', '1')
|
||||
verifyValueAfterChanged(@integerView, '0', '1')
|
||||
verifyValueAfterChanged(@integerView, '3001', '3001')
|
||||
|
||||
verifyValueAfterChanged(@floatView, '-4', '1.3')
|
||||
verifyValueAfterChanged(@floatView, '1.3', '1.3')
|
||||
verifyValueAfterChanged(@floatView, '1.2', '1.3')
|
||||
verifyValueAfterChanged(@floatView, '100.2', '100.2')
|
||||
verifyValueAfterChanged(@floatView, '100.3', '100.2')
|
||||
|
||||
it "disallows invalid characters", ->
|
||||
verifyValueAfterKeyPressed = (view, character, reject) ->
|
||||
event = {
|
||||
type : 'keypress',
|
||||
which : character.charCodeAt(0),
|
||||
keyCode: character.charCodeAt(0),
|
||||
preventDefault : () -> 'no op'
|
||||
}
|
||||
spyOn(event, 'preventDefault')
|
||||
view.$el.find('input').trigger(event)
|
||||
if (reject)
|
||||
expect(event.preventDefault).toHaveBeenCalled()
|
||||
else
|
||||
expect(event.preventDefault).not.toHaveBeenCalled()
|
||||
|
||||
verifyDisallowedChars = (view) ->
|
||||
verifyValueAfterKeyPressed(view, 'a', true)
|
||||
verifyValueAfterKeyPressed(view, '.', view.isIntegerField())
|
||||
verifyValueAfterKeyPressed(view, '[', true)
|
||||
verifyValueAfterKeyPressed(view, '@', true)
|
||||
|
||||
for i in [0...9]
|
||||
verifyValueAfterKeyPressed(view, String(i), false)
|
||||
|
||||
verifyDisallowedChars(@integerView)
|
||||
verifyDisallowedChars(@floatView)
|
||||
|
||||
describe "MetadataView.List allows the user to enter an ordered list of strings", ->
|
||||
beforeEach ->
|
||||
listModel = new MetadataModel(listEntry)
|
||||
@listView = new MetadataView.List({model: listModel})
|
||||
@el = @listView.$el
|
||||
main()
|
||||
|
||||
it "returns the initial value upon initialization", ->
|
||||
assertValueInView(@listView, ['the first display value', 'the second'])
|
||||
|
||||
it "updates its value correctly", ->
|
||||
assertCanUpdateView(@listView, ['a new item', 'another new item', 'a third'])
|
||||
|
||||
it "has a clear method to revert to the model default", ->
|
||||
assertClear(@listView, ['a thing', 'another thing'])
|
||||
|
||||
it "has an update model method", ->
|
||||
assertUpdateModel(@listView, null, ['a new value'])
|
||||
|
||||
it "can add an entry", ->
|
||||
expect(@listView.model.get('value').length).toEqual(2)
|
||||
@el.find('.create-setting').click()
|
||||
expect(@el.find('input.input').length).toEqual(3)
|
||||
|
||||
it "can remove an entry", ->
|
||||
expect(@listView.model.get('value').length).toEqual(2)
|
||||
@el.find('.remove-setting').first().click()
|
||||
expect(@listView.model.get('value').length).toEqual(1)
|
||||
|
||||
it "only allows one blank entry at a time", ->
|
||||
expect(@el.find('input').length).toEqual(2)
|
||||
@el.find('.create-setting').click()
|
||||
@el.find('.create-setting').click()
|
||||
expect(@el.find('input').length).toEqual(3)
|
||||
|
||||
it "re-enables the add setting button after entering a new value", ->
|
||||
expect(@el.find('input').length).toEqual(2)
|
||||
@el.find('.create-setting').click()
|
||||
expect(@el.find('.create-setting')).toHaveClass('is-disabled')
|
||||
@el.find('input').last().val('third setting')
|
||||
@el.find('input').last().trigger('input')
|
||||
expect(@el.find('.create-setting')).not.toHaveClass('is-disabled')
|
||||
|
||||
@@ -1,75 +1,76 @@
|
||||
describe "CMS.Views.ModuleEdit", ->
|
||||
beforeEach ->
|
||||
@stubModule = jasmine.createSpy("CMS.Models.Module")
|
||||
@stubModule.id = 'stub-id'
|
||||
define ["coffee/src/views/module_edit", "xmodule"], (ModuleEdit) ->
|
||||
|
||||
|
||||
setFixtures """
|
||||
<li class="component" id="stub-id">
|
||||
<div class="component-editor">
|
||||
<div class="module-editor">
|
||||
${editor}
|
||||
</div>
|
||||
<a href="#" class="save-button">Save</a>
|
||||
<a href="#" class="cancel-button">Cancel</a>
|
||||
</div>
|
||||
<div class="component-actions">
|
||||
<a href="#" class="edit-button"><span class="edit-icon white"></span>Edit</a>
|
||||
<a href="#" class="delete-button"><span class="delete-icon white"></span>Delete</a>
|
||||
</div>
|
||||
<a href="#" class="drag-handle"></a>
|
||||
<section class="xmodule_display xmodule_stub" data-type="StubModule">
|
||||
<div id="stub-module-content"/>
|
||||
</section>
|
||||
</li>
|
||||
"""
|
||||
spyOn($.fn, 'load').andReturn(@moduleData)
|
||||
|
||||
@moduleEdit = new CMS.Views.ModuleEdit(
|
||||
el: $(".component")
|
||||
model: @stubModule
|
||||
onDelete: jasmine.createSpy()
|
||||
)
|
||||
CMS.unbind()
|
||||
|
||||
describe "class definition", ->
|
||||
it "sets the correct tagName", ->
|
||||
expect(@moduleEdit.tagName).toEqual("li")
|
||||
|
||||
it "sets the correct className", ->
|
||||
expect(@moduleEdit.className).toEqual("component")
|
||||
|
||||
describe "methods", ->
|
||||
describe "initialize", ->
|
||||
describe "ModuleEdit", ->
|
||||
beforeEach ->
|
||||
spyOn(CMS.Views.ModuleEdit.prototype, 'render')
|
||||
@moduleEdit = new CMS.Views.ModuleEdit(
|
||||
@stubModule = jasmine.createSpy("Module")
|
||||
@stubModule.id = 'stub-id'
|
||||
|
||||
|
||||
setFixtures """
|
||||
<li class="component" id="stub-id">
|
||||
<div class="component-editor">
|
||||
<div class="module-editor">
|
||||
${editor}
|
||||
</div>
|
||||
<a href="#" class="save-button">Save</a>
|
||||
<a href="#" class="cancel-button">Cancel</a>
|
||||
</div>
|
||||
<div class="component-actions">
|
||||
<a href="#" class="edit-button"><span class="edit-icon white"></span>Edit</a>
|
||||
<a href="#" class="delete-button"><span class="delete-icon white"></span>Delete</a>
|
||||
</div>
|
||||
<a href="#" class="drag-handle"></a>
|
||||
<section class="xmodule_display xmodule_stub" data-type="StubModule">
|
||||
<div id="stub-module-content"/>
|
||||
</section>
|
||||
</li>
|
||||
"""
|
||||
spyOn($.fn, 'load').andReturn(@moduleData)
|
||||
|
||||
@moduleEdit = new ModuleEdit(
|
||||
el: $(".component")
|
||||
model: @stubModule
|
||||
onDelete: jasmine.createSpy()
|
||||
)
|
||||
|
||||
it "renders the module editor", ->
|
||||
expect(@moduleEdit.render).toHaveBeenCalled()
|
||||
describe "class definition", ->
|
||||
it "sets the correct tagName", ->
|
||||
expect(@moduleEdit.tagName).toEqual("li")
|
||||
|
||||
describe "render", ->
|
||||
beforeEach ->
|
||||
spyOn(@moduleEdit, 'loadDisplay')
|
||||
spyOn(@moduleEdit, 'delegateEvents')
|
||||
@moduleEdit.render()
|
||||
it "sets the correct className", ->
|
||||
expect(@moduleEdit.className).toEqual("component")
|
||||
|
||||
it "loads the module preview and editor via ajax on the view element", ->
|
||||
expect(@moduleEdit.$el.load).toHaveBeenCalledWith("/preview_component/#{@moduleEdit.model.id}", jasmine.any(Function))
|
||||
@moduleEdit.$el.load.mostRecentCall.args[1]()
|
||||
expect(@moduleEdit.loadDisplay).toHaveBeenCalled()
|
||||
expect(@moduleEdit.delegateEvents).toHaveBeenCalled()
|
||||
describe "methods", ->
|
||||
describe "initialize", ->
|
||||
beforeEach ->
|
||||
spyOn(ModuleEdit.prototype, 'render')
|
||||
@moduleEdit = new ModuleEdit(
|
||||
el: $(".component")
|
||||
model: @stubModule
|
||||
onDelete: jasmine.createSpy()
|
||||
)
|
||||
|
||||
describe "loadDisplay", ->
|
||||
beforeEach ->
|
||||
spyOn(XModule, 'loadModule')
|
||||
@moduleEdit.loadDisplay()
|
||||
it "renders the module editor", ->
|
||||
expect(@moduleEdit.render).toHaveBeenCalled()
|
||||
|
||||
it "loads the .xmodule-display inside the module editor", ->
|
||||
expect(XModule.loadModule).toHaveBeenCalled()
|
||||
expect(XModule.loadModule.mostRecentCall.args[0]).toBe($('.xmodule_display'))
|
||||
describe "render", ->
|
||||
beforeEach ->
|
||||
spyOn(@moduleEdit, 'loadDisplay')
|
||||
spyOn(@moduleEdit, 'delegateEvents')
|
||||
@moduleEdit.render()
|
||||
|
||||
it "loads the module preview and editor via ajax on the view element", ->
|
||||
expect(@moduleEdit.$el.load).toHaveBeenCalledWith("/preview_component/#{@moduleEdit.model.id}", jasmine.any(Function))
|
||||
@moduleEdit.$el.load.mostRecentCall.args[1]()
|
||||
expect(@moduleEdit.loadDisplay).toHaveBeenCalled()
|
||||
expect(@moduleEdit.delegateEvents).toHaveBeenCalled()
|
||||
|
||||
describe "loadDisplay", ->
|
||||
beforeEach ->
|
||||
spyOn(XModule, 'loadModule')
|
||||
@moduleEdit.loadDisplay()
|
||||
|
||||
it "loads the .xmodule-display inside the module editor", ->
|
||||
expect(XModule.loadModule).toHaveBeenCalled()
|
||||
expect(XModule.loadModule.mostRecentCall.args[0]).toBe($('.xmodule_display'))
|
||||
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
describe "Course Overview", ->
|
||||
|
||||
beforeEach ->
|
||||
_.each ["/static/js/vendor/date.js", "/static/js/vendor/timepicker/jquery.timepicker.js", "/jsi18n/"], (path) ->
|
||||
appendSetFixtures """
|
||||
<script type="text/javascript" src="#{path}"></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>
|
||||
"""
|
||||
|
||||
appendSetFixtures """
|
||||
<section class="courseware-section branch" data-id="a-location-goes-here">
|
||||
<li class="branch collapsed id-holder" data-id="an-id-goes-here">
|
||||
<a href="#" class="delete-section-button"></a>
|
||||
</li>
|
||||
</section>
|
||||
"""
|
||||
|
||||
spyOn(window, 'saveSetSectionScheduleDate').andCallThrough()
|
||||
# Have to do this here, as it normally gets bound in document.ready()
|
||||
$('a.save-button').click(saveSetSectionScheduleDate)
|
||||
$('a.delete-section-button').click(deleteSection)
|
||||
$(".edit-subsection-publish-settings .start-date").datepicker()
|
||||
|
||||
@notificationSpy = spyOn(CMS.Views.Notification.Mini.prototype, 'show').andCallThrough()
|
||||
window.analytics = jasmine.createSpyObj('analytics', ['track'])
|
||||
window.course_location_analytics = jasmine.createSpy()
|
||||
@xhr = sinon.useFakeXMLHttpRequest()
|
||||
requests = @requests = []
|
||||
@xhr.onCreate = (req) -> requests.push(req)
|
||||
|
||||
afterEach ->
|
||||
delete window.analytics
|
||||
delete window.course_location_analytics
|
||||
@notificationSpy.reset()
|
||||
|
||||
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()
|
||||
|
||||
it "should delete model when delete is clicked", ->
|
||||
deleteSpy = spyOn(window, '_deleteItem').andCallThrough()
|
||||
$('a.delete-section-button').click()
|
||||
$('a.action-primary').click()
|
||||
expect(deleteSpy).toHaveBeenCalled()
|
||||
expect(@requests[0].url).toEqual('/delete_item')
|
||||
|
||||
it "should not delete model when cancel is clicked", ->
|
||||
deleteSpy = spyOn(window, '_deleteItem').andCallThrough()
|
||||
$('a.delete-section-button').click()
|
||||
$('a.action-secondary').click()
|
||||
expect(@requests.length).toEqual(0)
|
||||
|
||||
it "should show a confirmation on delete", ->
|
||||
$('a.delete-section-button').click()
|
||||
$('a.action-primary').click()
|
||||
expect(@notificationSpy).toHaveBeenCalled()
|
||||
@@ -1,85 +1,87 @@
|
||||
describe "CMS.Views.SectionShow", ->
|
||||
describe "Basic", ->
|
||||
beforeEach ->
|
||||
spyOn(CMS.Views.SectionShow.prototype, "switchToEditView")
|
||||
.andCallThrough()
|
||||
@model = new CMS.Models.Section({
|
||||
id: 42
|
||||
name: "Life, the Universe, and Everything"
|
||||
})
|
||||
@view = new CMS.Views.SectionShow({model: @model})
|
||||
@view.render()
|
||||
define ["js/models/section", "js/views/section_show", "js/views/section_edit", "sinon"], (Section, SectionShow, SectionEdit, sinon) ->
|
||||
|
||||
it "should contain the model name", ->
|
||||
expect(@view.$el).toHaveText(@model.get('name'))
|
||||
describe "SectionShow", ->
|
||||
describe "Basic", ->
|
||||
beforeEach ->
|
||||
spyOn(SectionShow.prototype, "switchToEditView")
|
||||
.andCallThrough()
|
||||
@model = new Section({
|
||||
id: 42
|
||||
name: "Life, the Universe, and Everything"
|
||||
})
|
||||
@view = new SectionShow({model: @model})
|
||||
@view.render()
|
||||
|
||||
it "should call switchToEditView when clicked", ->
|
||||
@view.$el.click()
|
||||
expect(@view.switchToEditView).toHaveBeenCalled()
|
||||
it "should contain the model name", ->
|
||||
expect(@view.$el).toHaveText(@model.get('name'))
|
||||
|
||||
it "should pass the same element to SectionEdit when switching views", ->
|
||||
spyOn(CMS.Views.SectionEdit.prototype, 'initialize').andCallThrough()
|
||||
@view.switchToEditView()
|
||||
expect(CMS.Views.SectionEdit.prototype.initialize).toHaveBeenCalled()
|
||||
expect(CMS.Views.SectionEdit.prototype.initialize.mostRecentCall.args[0].el).toEqual(@view.el)
|
||||
it "should call switchToEditView when clicked", ->
|
||||
@view.$el.click()
|
||||
expect(@view.switchToEditView).toHaveBeenCalled()
|
||||
|
||||
describe "CMS.Views.SectionEdit", ->
|
||||
describe "Basic", ->
|
||||
tpl = readFixtures('section-name-edit.underscore')
|
||||
feedback_tpl = readFixtures('system-feedback.underscore')
|
||||
it "should pass the same element to SectionEdit when switching views", ->
|
||||
spyOn(SectionEdit.prototype, 'initialize').andCallThrough()
|
||||
@view.switchToEditView()
|
||||
expect(SectionEdit.prototype.initialize).toHaveBeenCalled()
|
||||
expect(SectionEdit.prototype.initialize.mostRecentCall.args[0].el).toEqual(@view.el)
|
||||
|
||||
beforeEach ->
|
||||
setFixtures($("<script>", {id: "section-name-edit-tpl", type: "text/template"}).text(tpl))
|
||||
appendSetFixtures($("<script>", {id: "system-feedback-tpl", type: "text/template"}).text(feedback_tpl))
|
||||
spyOn(CMS.Views.SectionEdit.prototype, "switchToShowView")
|
||||
.andCallThrough()
|
||||
spyOn(CMS.Views.SectionEdit.prototype, "showInvalidMessage")
|
||||
.andCallThrough()
|
||||
window.analytics = jasmine.createSpyObj('analytics', ['track'])
|
||||
window.course_location_analytics = jasmine.createSpy()
|
||||
@requests = requests = []
|
||||
@xhr = sinon.useFakeXMLHttpRequest()
|
||||
@xhr.onCreate = (xhr) -> requests.push(xhr)
|
||||
describe "SectionEdit", ->
|
||||
describe "Basic", ->
|
||||
tpl = readFixtures('section-name-edit.underscore')
|
||||
feedback_tpl = readFixtures('system-feedback.underscore')
|
||||
|
||||
@model = new CMS.Models.Section({
|
||||
id: 42
|
||||
name: "Life, the Universe, and Everything"
|
||||
})
|
||||
@view = new CMS.Views.SectionEdit({model: @model})
|
||||
@view.render()
|
||||
beforeEach ->
|
||||
setFixtures($("<script>", {id: "section-name-edit-tpl", type: "text/template"}).text(tpl))
|
||||
appendSetFixtures($("<script>", {id: "system-feedback-tpl", type: "text/template"}).text(feedback_tpl))
|
||||
spyOn(SectionEdit.prototype, "switchToShowView")
|
||||
.andCallThrough()
|
||||
spyOn(SectionEdit.prototype, "showInvalidMessage")
|
||||
.andCallThrough()
|
||||
window.analytics = jasmine.createSpyObj('analytics', ['track'])
|
||||
window.course_location_analytics = jasmine.createSpy()
|
||||
@requests = requests = []
|
||||
@xhr = sinon.useFakeXMLHttpRequest()
|
||||
@xhr.onCreate = (xhr) -> requests.push(xhr)
|
||||
|
||||
afterEach ->
|
||||
@xhr.restore()
|
||||
delete window.analytics
|
||||
delete window.course_location_analytics
|
||||
@model = new Section({
|
||||
id: 42
|
||||
name: "Life, the Universe, and Everything"
|
||||
})
|
||||
@view = new SectionEdit({model: @model})
|
||||
@view.render()
|
||||
|
||||
it "should have the model name as the default text value", ->
|
||||
expect(@view.$("input[type=text]").val()).toEqual(@model.get('name'))
|
||||
afterEach ->
|
||||
@xhr.restore()
|
||||
delete window.analytics
|
||||
delete window.course_location_analytics
|
||||
|
||||
it "should call switchToShowView when cancel button is clicked", ->
|
||||
@view.$("input.cancel-button").click()
|
||||
expect(@view.switchToShowView).toHaveBeenCalled()
|
||||
it "should have the model name as the default text value", ->
|
||||
expect(@view.$("input[type=text]").val()).toEqual(@model.get('name'))
|
||||
|
||||
it "should save model when save button is clicked", ->
|
||||
spyOn(@model, 'save')
|
||||
@view.$("input[type=submit]").click()
|
||||
expect(@model.save).toHaveBeenCalled()
|
||||
it "should call switchToShowView when cancel button is clicked", ->
|
||||
@view.$("input.cancel-button").click()
|
||||
expect(@view.switchToShowView).toHaveBeenCalled()
|
||||
|
||||
it "should call switchToShowView when save() is successful", ->
|
||||
@view.$("input[type=submit]").click()
|
||||
@requests[0].respond(200)
|
||||
expect(@view.switchToShowView).toHaveBeenCalled()
|
||||
it "should save model when save button is clicked", ->
|
||||
spyOn(@model, 'save')
|
||||
@view.$("input[type=submit]").click()
|
||||
expect(@model.save).toHaveBeenCalled()
|
||||
|
||||
it "should call showInvalidMessage when validation is unsuccessful", ->
|
||||
spyOn(@model, 'validate').andReturn("BLARRGH")
|
||||
@view.$("input[type=submit]").click()
|
||||
expect(@view.showInvalidMessage).toHaveBeenCalledWith(
|
||||
jasmine.any(Object), "BLARRGH", jasmine.any(Object))
|
||||
expect(@view.switchToShowView).not.toHaveBeenCalled()
|
||||
it "should call switchToShowView when save() is successful", ->
|
||||
@view.$("input[type=submit]").click()
|
||||
@requests[0].respond(200)
|
||||
expect(@view.switchToShowView).toHaveBeenCalled()
|
||||
|
||||
it "should not save when validation is unsuccessful", ->
|
||||
spyOn(@model, 'validate').andReturn("BLARRGH")
|
||||
@view.$("input[type=text]").val("changed")
|
||||
@view.$("input[type=submit]").click()
|
||||
expect(@model.get('name')).not.toEqual("changed")
|
||||
it "should call showInvalidMessage when validation is unsuccessful", ->
|
||||
spyOn(@model, 'validate').andReturn("BLARRGH")
|
||||
@view.$("input[type=submit]").click()
|
||||
expect(@view.showInvalidMessage).toHaveBeenCalledWith(
|
||||
jasmine.any(Object), "BLARRGH", jasmine.any(Object))
|
||||
expect(@view.switchToShowView).not.toHaveBeenCalled()
|
||||
|
||||
it "should not save when validation is unsuccessful", ->
|
||||
spyOn(@model, 'validate').andReturn("BLARRGH")
|
||||
@view.$("input[type=text]").val("changed")
|
||||
@view.$("input[type=submit]").click()
|
||||
expect(@model.get('name')).not.toEqual("changed")
|
||||
|
||||
|
||||
@@ -1,313 +1,318 @@
|
||||
feedbackTpl = readFixtures('system-feedback.underscore')
|
||||
|
||||
beforeEach ->
|
||||
# remove this when we upgrade jasmine-jquery
|
||||
@addMatchers
|
||||
toContainText: (text) ->
|
||||
trimmedText = $.trim(@actual.text())
|
||||
if text and $.isFunction(text.test)
|
||||
return text.test(trimmedText)
|
||||
else
|
||||
return trimmedText.indexOf(text) != -1;
|
||||
|
||||
describe "CMS.Views.ShowTextbook", ->
|
||||
tpl = readFixtures('show-textbook.underscore')
|
||||
define ["js/models/textbook", "js/models/chapter", "js/collections/chapter", "js/models/section",
|
||||
"js/collections/textbook", "js/views/show_textbook", "js/views/edit_textbook", "js/views/list_textbooks",
|
||||
"js/views/edit_chapter", "js/views/feedback_prompt", "js/views/feedback_notification",
|
||||
"sinon", "jasmine-stealth"],
|
||||
(Textbook, Chapter, ChapterSet, Section, TextbookSet, ShowTextbook, EditTextbook, ListTexbook, EditChapter, Prompt, Notification, sinon) ->
|
||||
feedbackTpl = readFixtures('system-feedback.underscore')
|
||||
|
||||
beforeEach ->
|
||||
setFixtures($("<script>", {id: "show-textbook-tpl", type: "text/template"}).text(tpl))
|
||||
appendSetFixtures($("<script>", {id: "system-feedback-tpl", type: "text/template"}).text(feedbackTpl))
|
||||
appendSetFixtures(sandbox({id: "page-notification"}))
|
||||
appendSetFixtures(sandbox({id: "page-prompt"}))
|
||||
@model = new CMS.Models.Textbook({name: "Life Sciences", id: "0life-sciences"})
|
||||
spyOn(@model, "destroy").andCallThrough()
|
||||
@collection = new CMS.Collections.TextbookSet([@model])
|
||||
@view = new CMS.Views.ShowTextbook({model: @model})
|
||||
# remove this when we upgrade jasmine-jquery
|
||||
@addMatchers
|
||||
toContainText: (text) ->
|
||||
trimmedText = $.trim(@actual.text())
|
||||
if text and $.isFunction(text.test)
|
||||
return text.test(trimmedText)
|
||||
else
|
||||
return trimmedText.indexOf(text) != -1;
|
||||
|
||||
@promptSpies = spyOnConstructor(CMS.Views.Prompt, "Warning", ["show", "hide"])
|
||||
@promptSpies.show.andReturn(@promptSpies)
|
||||
window.section = new CMS.Models.Section({
|
||||
id: "5",
|
||||
name: "Course Name",
|
||||
url_name: "course_name",
|
||||
org: "course_org",
|
||||
num: "course_num",
|
||||
revision: "course_rev"
|
||||
});
|
||||
|
||||
afterEach ->
|
||||
delete window.section
|
||||
|
||||
describe "Basic", ->
|
||||
it "should render properly", ->
|
||||
@view.render()
|
||||
expect(@view.$el).toContainText("Life Sciences")
|
||||
|
||||
it "should set the 'editing' property on the model when the edit button is clicked", ->
|
||||
@view.render().$(".edit").click()
|
||||
expect(@model.get("editing")).toBeTruthy()
|
||||
|
||||
it "should pop a delete confirmation when the delete button is clicked", ->
|
||||
@view.render().$(".delete").click()
|
||||
expect(@promptSpies.constructor).toHaveBeenCalled()
|
||||
ctorOptions = @promptSpies.constructor.mostRecentCall.args[0]
|
||||
expect(ctorOptions.title).toMatch(/Life Sciences/)
|
||||
# hasn't actually been removed
|
||||
expect(@model.destroy).not.toHaveBeenCalled()
|
||||
expect(@collection).toContain(@model)
|
||||
|
||||
it "should show chapters appropriately", ->
|
||||
@model.get("chapters").add([{}, {}, {}])
|
||||
@model.set('showChapters', false)
|
||||
@view.render().$(".show-chapters").click()
|
||||
expect(@model.get('showChapters')).toBeTruthy()
|
||||
|
||||
it "should hide chapters appropriately", ->
|
||||
@model.get("chapters").add([{}, {}, {}])
|
||||
@model.set('showChapters', true)
|
||||
@view.render().$(".hide-chapters").click()
|
||||
expect(@model.get('showChapters')).toBeFalsy()
|
||||
|
||||
describe "AJAX", ->
|
||||
beforeEach ->
|
||||
@requests = requests = []
|
||||
@xhr = sinon.useFakeXMLHttpRequest()
|
||||
@xhr.onCreate = (xhr) -> requests.push(xhr)
|
||||
|
||||
@savingSpies = spyOnConstructor(CMS.Views.Notification, "Mini",
|
||||
["show", "hide"])
|
||||
@savingSpies.show.andReturn(@savingSpies)
|
||||
|
||||
afterEach ->
|
||||
@xhr.restore()
|
||||
|
||||
it "should destroy itself on confirmation", ->
|
||||
@view.render().$(".delete").click()
|
||||
ctorOptions = @promptSpies.constructor.mostRecentCall.args[0]
|
||||
# run the primary function to indicate confirmation
|
||||
ctorOptions.actions.primary.click(@promptSpies)
|
||||
# AJAX request has been sent, but not yet returned
|
||||
expect(@model.destroy).toHaveBeenCalled()
|
||||
expect(@requests.length).toEqual(1)
|
||||
expect(@savingSpies.constructor).toHaveBeenCalled()
|
||||
expect(@savingSpies.show).toHaveBeenCalled()
|
||||
expect(@savingSpies.hide).not.toHaveBeenCalled()
|
||||
savingOptions = @savingSpies.constructor.mostRecentCall.args[0]
|
||||
expect(savingOptions.title).toMatch(/Deleting/)
|
||||
# return a success response
|
||||
@requests[0].respond(200)
|
||||
expect(@savingSpies.hide).toHaveBeenCalled()
|
||||
expect(@collection.contains(@model)).toBeFalsy()
|
||||
|
||||
describe "CMS.Views.EditTextbook", ->
|
||||
describe "Basic", ->
|
||||
tpl = readFixtures('edit-textbook.underscore')
|
||||
chapterTpl = readFixtures('edit-chapter.underscore')
|
||||
describe "ShowTextbook", ->
|
||||
tpl = readFixtures('show-textbook.underscore')
|
||||
|
||||
beforeEach ->
|
||||
setFixtures($("<script>", {id: "edit-textbook-tpl", type: "text/template"}).text(tpl))
|
||||
appendSetFixtures($("<script>", {id: "edit-chapter-tpl", type: "text/template"}).text(chapterTpl))
|
||||
setFixtures($("<script>", {id: "show-textbook-tpl", type: "text/template"}).text(tpl))
|
||||
appendSetFixtures($("<script>", {id: "system-feedback-tpl", type: "text/template"}).text(feedbackTpl))
|
||||
appendSetFixtures(sandbox({id: "page-notification"}))
|
||||
appendSetFixtures(sandbox({id: "page-prompt"}))
|
||||
@model = new CMS.Models.Textbook({name: "Life Sciences", editing: true})
|
||||
spyOn(@model, 'save')
|
||||
@collection = new CMS.Collections.TextbookSet()
|
||||
@model = new Textbook({name: "Life Sciences", id: "0life-sciences"})
|
||||
spyOn(@model, "destroy").andCallThrough()
|
||||
@collection = new TextbookSet([@model])
|
||||
@view = new ShowTextbook({model: @model})
|
||||
|
||||
@promptSpies = spyOnConstructor(Prompt, "Warning", ["show", "hide"])
|
||||
@promptSpies.show.andReturn(@promptSpies)
|
||||
window.section = new Section({
|
||||
id: "5",
|
||||
name: "Course Name",
|
||||
url_name: "course_name",
|
||||
org: "course_org",
|
||||
num: "course_num",
|
||||
revision: "course_rev"
|
||||
});
|
||||
|
||||
afterEach ->
|
||||
delete window.section
|
||||
|
||||
describe "Basic", ->
|
||||
it "should render properly", ->
|
||||
@view.render()
|
||||
expect(@view.$el).toContainText("Life Sciences")
|
||||
|
||||
it "should set the 'editing' property on the model when the edit button is clicked", ->
|
||||
@view.render().$(".edit").click()
|
||||
expect(@model.get("editing")).toBeTruthy()
|
||||
|
||||
it "should pop a delete confirmation when the delete button is clicked", ->
|
||||
@view.render().$(".delete").click()
|
||||
expect(@promptSpies.constructor).toHaveBeenCalled()
|
||||
ctorOptions = @promptSpies.constructor.mostRecentCall.args[0]
|
||||
expect(ctorOptions.title).toMatch(/Life Sciences/)
|
||||
# hasn't actually been removed
|
||||
expect(@model.destroy).not.toHaveBeenCalled()
|
||||
expect(@collection).toContain(@model)
|
||||
|
||||
it "should show chapters appropriately", ->
|
||||
@model.get("chapters").add([{}, {}, {}])
|
||||
@model.set('showChapters', false)
|
||||
@view.render().$(".show-chapters").click()
|
||||
expect(@model.get('showChapters')).toBeTruthy()
|
||||
|
||||
it "should hide chapters appropriately", ->
|
||||
@model.get("chapters").add([{}, {}, {}])
|
||||
@model.set('showChapters', true)
|
||||
@view.render().$(".hide-chapters").click()
|
||||
expect(@model.get('showChapters')).toBeFalsy()
|
||||
|
||||
describe "AJAX", ->
|
||||
beforeEach ->
|
||||
@requests = requests = []
|
||||
@xhr = sinon.useFakeXMLHttpRequest()
|
||||
@xhr.onCreate = (xhr) -> requests.push(xhr)
|
||||
|
||||
@savingSpies = spyOnConstructor(Notification, "Mini",
|
||||
["show", "hide"])
|
||||
@savingSpies.show.andReturn(@savingSpies)
|
||||
|
||||
afterEach ->
|
||||
@xhr.restore()
|
||||
|
||||
it "should destroy itself on confirmation", ->
|
||||
@view.render().$(".delete").click()
|
||||
ctorOptions = @promptSpies.constructor.mostRecentCall.args[0]
|
||||
# run the primary function to indicate confirmation
|
||||
ctorOptions.actions.primary.click(@promptSpies)
|
||||
# AJAX request has been sent, but not yet returned
|
||||
expect(@model.destroy).toHaveBeenCalled()
|
||||
expect(@requests.length).toEqual(1)
|
||||
expect(@savingSpies.constructor).toHaveBeenCalled()
|
||||
expect(@savingSpies.show).toHaveBeenCalled()
|
||||
expect(@savingSpies.hide).not.toHaveBeenCalled()
|
||||
savingOptions = @savingSpies.constructor.mostRecentCall.args[0]
|
||||
expect(savingOptions.title).toMatch(/Deleting/)
|
||||
# return a success response
|
||||
@requests[0].respond(200)
|
||||
expect(@savingSpies.hide).toHaveBeenCalled()
|
||||
expect(@collection.contains(@model)).toBeFalsy()
|
||||
|
||||
describe "EditTextbook", ->
|
||||
describe "Basic", ->
|
||||
tpl = readFixtures('edit-textbook.underscore')
|
||||
chapterTpl = readFixtures('edit-chapter.underscore')
|
||||
|
||||
beforeEach ->
|
||||
setFixtures($("<script>", {id: "edit-textbook-tpl", type: "text/template"}).text(tpl))
|
||||
appendSetFixtures($("<script>", {id: "edit-chapter-tpl", type: "text/template"}).text(chapterTpl))
|
||||
appendSetFixtures($("<script>", {id: "system-feedback-tpl", type: "text/template"}).text(feedbackTpl))
|
||||
appendSetFixtures(sandbox({id: "page-notification"}))
|
||||
appendSetFixtures(sandbox({id: "page-prompt"}))
|
||||
@model = new Textbook({name: "Life Sciences", editing: true})
|
||||
spyOn(@model, 'save')
|
||||
@collection = new TextbookSet()
|
||||
@collection.add(@model)
|
||||
@view = new EditTextbook({model: @model})
|
||||
spyOn(@view, 'render').andCallThrough()
|
||||
|
||||
it "should render properly", ->
|
||||
@view.render()
|
||||
expect(@view.$("input[name=textbook-name]").val()).toEqual("Life Sciences")
|
||||
|
||||
it "should allow you to create new empty chapters", ->
|
||||
@view.render()
|
||||
numChapters = @model.get("chapters").length
|
||||
@view.$(".action-add-chapter").click()
|
||||
expect(@model.get("chapters").length).toEqual(numChapters+1)
|
||||
expect(@model.get("chapters").last().isEmpty()).toBeTruthy()
|
||||
|
||||
it "should save properly", ->
|
||||
@view.render()
|
||||
@view.$("input[name=textbook-name]").val("starfish")
|
||||
@view.$("input[name=chapter1-name]").val("wallflower")
|
||||
@view.$("input[name=chapter1-asset-path]").val("foobar")
|
||||
@view.$("form").submit()
|
||||
expect(@model.get("name")).toEqual("starfish")
|
||||
chapter = @model.get("chapters").first()
|
||||
expect(chapter.get("name")).toEqual("wallflower")
|
||||
expect(chapter.get("asset_path")).toEqual("foobar")
|
||||
expect(@model.save).toHaveBeenCalled()
|
||||
|
||||
it "should not save on invalid", ->
|
||||
@view.render()
|
||||
@view.$("input[name=textbook-name]").val("")
|
||||
@view.$("input[name=chapter1-asset-path]").val("foobar.pdf")
|
||||
@view.$("form").submit()
|
||||
expect(@model.validationError).toBeTruthy()
|
||||
expect(@model.save).not.toHaveBeenCalled()
|
||||
|
||||
it "does not save on cancel", ->
|
||||
@model.get("chapters").add([{name: "a", asset_path: "b"}])
|
||||
@view.render()
|
||||
@view.$("input[name=textbook-name]").val("starfish")
|
||||
@view.$("input[name=chapter1-asset-path]").val("foobar.pdf")
|
||||
@view.$(".action-cancel").click()
|
||||
expect(@model.get("name")).not.toEqual("starfish")
|
||||
chapter = @model.get("chapters").first()
|
||||
expect(chapter.get("asset_path")).not.toEqual("foobar")
|
||||
expect(@model.save).not.toHaveBeenCalled()
|
||||
|
||||
it "should be possible to correct validation errors", ->
|
||||
@view.render()
|
||||
@view.$("input[name=textbook-name]").val("")
|
||||
@view.$("input[name=chapter1-asset-path]").val("foobar.pdf")
|
||||
@view.$("form").submit()
|
||||
expect(@model.validationError).toBeTruthy()
|
||||
expect(@model.save).not.toHaveBeenCalled()
|
||||
@view.$("input[name=textbook-name]").val("starfish")
|
||||
@view.$("input[name=chapter1-name]").val("foobar")
|
||||
@view.$("form").submit()
|
||||
expect(@model.validationError).toBeFalsy()
|
||||
expect(@model.save).toHaveBeenCalled()
|
||||
|
||||
it "removes all empty chapters on cancel if the model has a non-empty chapter", ->
|
||||
chapters = @model.get("chapters")
|
||||
chapters.at(0).set("name", "non-empty")
|
||||
@model.setOriginalAttributes()
|
||||
@view.render()
|
||||
chapters.add([{}, {}, {}]) # add three empty chapters
|
||||
expect(chapters.length).toEqual(4)
|
||||
@view.$(".action-cancel").click()
|
||||
expect(chapters.length).toEqual(1)
|
||||
expect(chapters.first().get('name')).toEqual("non-empty")
|
||||
|
||||
it "removes all empty chapters on cancel except one if the model has no non-empty chapters", ->
|
||||
chapters = @model.get("chapters")
|
||||
@view.render()
|
||||
chapters.add([{}, {}, {}]) # add three empty chapters
|
||||
expect(chapters.length).toEqual(4)
|
||||
@view.$(".action-cancel").click()
|
||||
expect(chapters.length).toEqual(1)
|
||||
|
||||
|
||||
# describe "ListTextbooks", ->
|
||||
# noTextbooksTpl = readFixtures("no-textbooks.underscore")
|
||||
#
|
||||
# beforeEach ->
|
||||
# setFixtures($("<script>", {id: "no-textbooks-tpl", type: "text/template"}).text(noTextbooksTpl))
|
||||
# appendSetFixtures($("<script>", {id: "system-feedback-tpl", type: "text/template"}).text(feedbackTpl))
|
||||
# @showSpies = spyOnConstructor("ShowTextbook", ["render"])
|
||||
# @showSpies.render.andReturn(@showSpies) # equivalent of `return this`
|
||||
# showEl = $("<li>")
|
||||
# @showSpies.$el = showEl
|
||||
# @showSpies.el = showEl.get(0)
|
||||
# @editSpies = spyOnConstructor("EditTextbook", ["render"])
|
||||
# editEl = $("<li>")
|
||||
# @editSpies.render.andReturn(@editSpies)
|
||||
# @editSpies.$el = editEl
|
||||
# @editSpies.el= editEl.get(0)
|
||||
#
|
||||
# @collection = new TextbookSet
|
||||
# @view = new ListTextbooks({collection: @collection})
|
||||
# @view.render()
|
||||
#
|
||||
# it "should render the empty template if there are no textbooks", ->
|
||||
# expect(@view.$el).toContainText("You haven't added any textbooks to this course yet")
|
||||
# expect(@view.$el).toContain(".new-button")
|
||||
# expect(@showSpies.constructor).not.toHaveBeenCalled()
|
||||
# expect(@editSpies.constructor).not.toHaveBeenCalled()
|
||||
#
|
||||
# it "should render ShowTextbook views by default if no textbook is being edited", ->
|
||||
# # add three empty textbooks to the collection
|
||||
# @collection.add([{}, {}, {}])
|
||||
# # reset spies due to re-rendering on collection modification
|
||||
# @showSpies.constructor.reset()
|
||||
# @editSpies.constructor.reset()
|
||||
# # render once and test
|
||||
# @view.render()
|
||||
#
|
||||
# expect(@view.$el).not.toContainText(
|
||||
# "You haven't added any textbooks to this course yet")
|
||||
# expect(@showSpies.constructor).toHaveBeenCalled()
|
||||
# expect(@showSpies.constructor.calls.length).toEqual(3);
|
||||
# expect(@editSpies.constructor).not.toHaveBeenCalled()
|
||||
#
|
||||
# it "should render an EditTextbook view for a textbook being edited", ->
|
||||
# # add three empty textbooks to the collection: the first and third
|
||||
# # should be shown, and the second should be edited
|
||||
# @collection.add([{editing: false}, {editing: true}, {editing: false}])
|
||||
# editing = @collection.at(1)
|
||||
# expect(editing.get("editing")).toBeTruthy()
|
||||
# # reset spies
|
||||
# @showSpies.constructor.reset()
|
||||
# @editSpies.constructor.reset()
|
||||
# # render once and test
|
||||
# @view.render()
|
||||
#
|
||||
# expect(@showSpies.constructor).toHaveBeenCalled()
|
||||
# expect(@showSpies.constructor.calls.length).toEqual(2)
|
||||
# expect(@showSpies.constructor).not.toHaveBeenCalledWith({model: editing})
|
||||
# expect(@editSpies.constructor).toHaveBeenCalled()
|
||||
# expect(@editSpies.constructor.calls.length).toEqual(1)
|
||||
# expect(@editSpies.constructor).toHaveBeenCalledWith({model: editing})
|
||||
#
|
||||
# it "should add a new textbook when the new-button is clicked", ->
|
||||
# # reset spies
|
||||
# @showSpies.constructor.reset()
|
||||
# @editSpies.constructor.reset()
|
||||
# # test
|
||||
# @view.$(".new-button").click()
|
||||
#
|
||||
# expect(@collection.length).toEqual(1)
|
||||
# expect(@view.$el).toContain(@editSpies.$el)
|
||||
# expect(@view.$el).not.toContain(@showSpies.$el)
|
||||
|
||||
|
||||
describe "EditChapter", ->
|
||||
tpl = readFixtures("edit-chapter.underscore")
|
||||
|
||||
beforeEach ->
|
||||
setFixtures($("<script>", {id: "edit-chapter-tpl", type: "text/template"}).text(tpl))
|
||||
appendSetFixtures($("<script>", {id: "system-feedback-tpl", type: "text/template"}).text(feedbackTpl))
|
||||
@model = new Chapter
|
||||
name: "Chapter 1"
|
||||
asset_path: "/ch1.pdf"
|
||||
@collection = new ChapterSet()
|
||||
@collection.add(@model)
|
||||
@view = new CMS.Views.EditTextbook({model: @model})
|
||||
spyOn(@view, 'render').andCallThrough()
|
||||
@view = new EditChapter({model: @model})
|
||||
spyOn(@view, "remove").andCallThrough()
|
||||
CMS.URL.UPLOAD_ASSET = "/upload"
|
||||
window.section = new Section({name: "abcde"})
|
||||
|
||||
it "should render properly", ->
|
||||
afterEach ->
|
||||
delete CMS.URL.UPLOAD_ASSET
|
||||
delete window.section
|
||||
|
||||
it "can render", ->
|
||||
@view.render()
|
||||
expect(@view.$("input[name=textbook-name]").val()).toEqual("Life Sciences")
|
||||
expect(@view.$("input.chapter-name").val()).toEqual("Chapter 1")
|
||||
expect(@view.$("input.chapter-asset-path").val()).toEqual("/ch1.pdf")
|
||||
|
||||
it "should allow you to create new empty chapters", ->
|
||||
it "can delete itself", ->
|
||||
@view.render().$(".action-close").click()
|
||||
expect(@collection.length).toEqual(0)
|
||||
expect(@view.remove).toHaveBeenCalled()
|
||||
|
||||
# it "can open an upload dialog", ->
|
||||
# uploadSpies = spyOnConstructor("UploadDialog", ["show", "el"])
|
||||
# uploadSpies.show.andReturn(uploadSpies)
|
||||
#
|
||||
# @view.render().$(".action-upload").click()
|
||||
# ctorOptions = uploadSpies.constructor.mostRecentCall.args[0]
|
||||
# expect(ctorOptions.model.get('title')).toMatch(/abcde/)
|
||||
# expect(typeof ctorOptions.onSuccess).toBe('function')
|
||||
# expect(uploadSpies.show).toHaveBeenCalled()
|
||||
|
||||
it "saves content when opening upload dialog", ->
|
||||
@view.render()
|
||||
numChapters = @model.get("chapters").length
|
||||
@view.$(".action-add-chapter").click()
|
||||
expect(@model.get("chapters").length).toEqual(numChapters+1)
|
||||
expect(@model.get("chapters").last().isEmpty()).toBeTruthy()
|
||||
|
||||
it "should save properly", ->
|
||||
@view.render()
|
||||
@view.$("input[name=textbook-name]").val("starfish")
|
||||
@view.$("input[name=chapter1-name]").val("wallflower")
|
||||
@view.$("input[name=chapter1-asset-path]").val("foobar")
|
||||
@view.$("form").submit()
|
||||
expect(@model.get("name")).toEqual("starfish")
|
||||
chapter = @model.get("chapters").first()
|
||||
expect(chapter.get("name")).toEqual("wallflower")
|
||||
expect(chapter.get("asset_path")).toEqual("foobar")
|
||||
expect(@model.save).toHaveBeenCalled()
|
||||
|
||||
it "should not save on invalid", ->
|
||||
@view.render()
|
||||
@view.$("input[name=textbook-name]").val("")
|
||||
@view.$("input[name=chapter1-asset-path]").val("foobar.pdf")
|
||||
@view.$("form").submit()
|
||||
expect(@model.validationError).toBeTruthy()
|
||||
expect(@model.save).not.toHaveBeenCalled()
|
||||
|
||||
it "does not save on cancel", ->
|
||||
@model.get("chapters").add([{name: "a", asset_path: "b"}])
|
||||
@view.render()
|
||||
@view.$("input[name=textbook-name]").val("starfish")
|
||||
@view.$("input[name=chapter1-asset-path]").val("foobar.pdf")
|
||||
@view.$(".action-cancel").click()
|
||||
expect(@model.get("name")).not.toEqual("starfish")
|
||||
chapter = @model.get("chapters").first()
|
||||
expect(chapter.get("asset_path")).not.toEqual("foobar")
|
||||
expect(@model.save).not.toHaveBeenCalled()
|
||||
|
||||
it "should be possible to correct validation errors", ->
|
||||
@view.render()
|
||||
@view.$("input[name=textbook-name]").val("")
|
||||
@view.$("input[name=chapter1-asset-path]").val("foobar.pdf")
|
||||
@view.$("form").submit()
|
||||
expect(@model.validationError).toBeTruthy()
|
||||
expect(@model.save).not.toHaveBeenCalled()
|
||||
@view.$("input[name=textbook-name]").val("starfish")
|
||||
@view.$("input[name=chapter1-name]").val("foobar")
|
||||
@view.$("form").submit()
|
||||
expect(@model.validationError).toBeFalsy()
|
||||
expect(@model.save).toHaveBeenCalled()
|
||||
|
||||
it "removes all empty chapters on cancel if the model has a non-empty chapter", ->
|
||||
chapters = @model.get("chapters")
|
||||
chapters.at(0).set("name", "non-empty")
|
||||
@model.setOriginalAttributes()
|
||||
@view.render()
|
||||
chapters.add([{}, {}, {}]) # add three empty chapters
|
||||
expect(chapters.length).toEqual(4)
|
||||
@view.$(".action-cancel").click()
|
||||
expect(chapters.length).toEqual(1)
|
||||
expect(chapters.first().get('name')).toEqual("non-empty")
|
||||
|
||||
it "removes all empty chapters on cancel except one if the model has no non-empty chapters", ->
|
||||
chapters = @model.get("chapters")
|
||||
@view.render()
|
||||
chapters.add([{}, {}, {}]) # add three empty chapters
|
||||
expect(chapters.length).toEqual(4)
|
||||
@view.$(".action-cancel").click()
|
||||
expect(chapters.length).toEqual(1)
|
||||
|
||||
|
||||
describe "CMS.Views.ListTextbooks", ->
|
||||
noTextbooksTpl = readFixtures("no-textbooks.underscore")
|
||||
|
||||
beforeEach ->
|
||||
setFixtures($("<script>", {id: "no-textbooks-tpl", type: "text/template"}).text(noTextbooksTpl))
|
||||
appendSetFixtures($("<script>", {id: "system-feedback-tpl", type: "text/template"}).text(feedbackTpl))
|
||||
@showSpies = spyOnConstructor(CMS.Views, "ShowTextbook", ["render"])
|
||||
@showSpies.render.andReturn(@showSpies) # equivalent of `return this`
|
||||
showEl = $("<li>")
|
||||
@showSpies.$el = showEl
|
||||
@showSpies.el = showEl.get(0)
|
||||
@editSpies = spyOnConstructor(CMS.Views, "EditTextbook", ["render"])
|
||||
editEl = $("<li>")
|
||||
@editSpies.render.andReturn(@editSpies)
|
||||
@editSpies.$el = editEl
|
||||
@editSpies.el= editEl.get(0)
|
||||
|
||||
@collection = new CMS.Collections.TextbookSet
|
||||
@view = new CMS.Views.ListTextbooks({collection: @collection})
|
||||
@view.render()
|
||||
|
||||
it "should render the empty template if there are no textbooks", ->
|
||||
expect(@view.$el).toContainText("You haven't added any textbooks to this course yet")
|
||||
expect(@view.$el).toContain(".new-button")
|
||||
expect(@showSpies.constructor).not.toHaveBeenCalled()
|
||||
expect(@editSpies.constructor).not.toHaveBeenCalled()
|
||||
|
||||
it "should render ShowTextbook views by default if no textbook is being edited", ->
|
||||
# add three empty textbooks to the collection
|
||||
@collection.add([{}, {}, {}])
|
||||
# reset spies due to re-rendering on collection modification
|
||||
@showSpies.constructor.reset()
|
||||
@editSpies.constructor.reset()
|
||||
# render once and test
|
||||
@view.render()
|
||||
|
||||
expect(@view.$el).not.toContainText(
|
||||
"You haven't added any textbooks to this course yet")
|
||||
expect(@showSpies.constructor).toHaveBeenCalled()
|
||||
expect(@showSpies.constructor.calls.length).toEqual(3);
|
||||
expect(@editSpies.constructor).not.toHaveBeenCalled()
|
||||
|
||||
it "should render an EditTextbook view for a textbook being edited", ->
|
||||
# add three empty textbooks to the collection: the first and third
|
||||
# should be shown, and the second should be edited
|
||||
@collection.add([{editing: false}, {editing: true}, {editing: false}])
|
||||
editing = @collection.at(1)
|
||||
expect(editing.get("editing")).toBeTruthy()
|
||||
# reset spies
|
||||
@showSpies.constructor.reset()
|
||||
@editSpies.constructor.reset()
|
||||
# render once and test
|
||||
@view.render()
|
||||
|
||||
expect(@showSpies.constructor).toHaveBeenCalled()
|
||||
expect(@showSpies.constructor.calls.length).toEqual(2)
|
||||
expect(@showSpies.constructor).not.toHaveBeenCalledWith({model: editing})
|
||||
expect(@editSpies.constructor).toHaveBeenCalled()
|
||||
expect(@editSpies.constructor.calls.length).toEqual(1)
|
||||
expect(@editSpies.constructor).toHaveBeenCalledWith({model: editing})
|
||||
|
||||
it "should add a new textbook when the new-button is clicked", ->
|
||||
# reset spies
|
||||
@showSpies.constructor.reset()
|
||||
@editSpies.constructor.reset()
|
||||
# test
|
||||
@view.$(".new-button").click()
|
||||
|
||||
expect(@collection.length).toEqual(1)
|
||||
expect(@view.$el).toContain(@editSpies.$el)
|
||||
expect(@view.$el).not.toContain(@showSpies.$el)
|
||||
|
||||
|
||||
describe "CMS.Views.EditChapter", ->
|
||||
tpl = readFixtures("edit-chapter.underscore")
|
||||
|
||||
beforeEach ->
|
||||
setFixtures($("<script>", {id: "edit-chapter-tpl", type: "text/template"}).text(tpl))
|
||||
appendSetFixtures($("<script>", {id: "system-feedback-tpl", type: "text/template"}).text(feedbackTpl))
|
||||
@model = new CMS.Models.Chapter
|
||||
name: "Chapter 1"
|
||||
asset_path: "/ch1.pdf"
|
||||
@collection = new CMS.Collections.ChapterSet()
|
||||
@collection.add(@model)
|
||||
@view = new CMS.Views.EditChapter({model: @model})
|
||||
spyOn(@view, "remove").andCallThrough()
|
||||
CMS.URL.UPLOAD_ASSET = "/upload"
|
||||
window.section = new CMS.Models.Section({name: "abcde"})
|
||||
|
||||
afterEach ->
|
||||
delete CMS.URL.UPLOAD_ASSET
|
||||
delete window.section
|
||||
|
||||
it "can render", ->
|
||||
@view.render()
|
||||
expect(@view.$("input.chapter-name").val()).toEqual("Chapter 1")
|
||||
expect(@view.$("input.chapter-asset-path").val()).toEqual("/ch1.pdf")
|
||||
|
||||
it "can delete itself", ->
|
||||
@view.render().$(".action-close").click()
|
||||
expect(@collection.length).toEqual(0)
|
||||
expect(@view.remove).toHaveBeenCalled()
|
||||
|
||||
it "can open an upload dialog", ->
|
||||
uploadSpies = spyOnConstructor(CMS.Views, "UploadDialog", ["show", "el"])
|
||||
uploadSpies.show.andReturn(uploadSpies)
|
||||
|
||||
@view.render().$(".action-upload").click()
|
||||
ctorOptions = uploadSpies.constructor.mostRecentCall.args[0]
|
||||
expect(ctorOptions.model.get('title')).toMatch(/abcde/)
|
||||
expect(typeof ctorOptions.onSuccess).toBe('function')
|
||||
expect(uploadSpies.show).toHaveBeenCalled()
|
||||
|
||||
it "saves content when opening upload dialog", ->
|
||||
@view.render()
|
||||
@view.$("input.chapter-name").val("rainbows")
|
||||
@view.$("input.chapter-asset-path").val("unicorns")
|
||||
@view.$(".action-upload").click()
|
||||
expect(@model.get("name")).toEqual("rainbows")
|
||||
expect(@model.get("asset_path")).toEqual("unicorns")
|
||||
@view.$("input.chapter-name").val("rainbows")
|
||||
@view.$("input.chapter-asset-path").val("unicorns")
|
||||
@view.$(".action-upload").click()
|
||||
expect(@model.get("name")).toEqual("rainbows")
|
||||
expect(@model.get("asset_path")).toEqual("unicorns")
|
||||
|
||||
@@ -1,114 +1,116 @@
|
||||
feedbackTpl = readFixtures('system-feedback.underscore')
|
||||
define ["js/models/uploads", "js/views/uploads", "js/models/chapter", "sinon"], (FileUpload, UploadDialog, Chapter, sinon) ->
|
||||
|
||||
describe "CMS.Views.UploadDialog", ->
|
||||
tpl = readFixtures("upload-dialog.underscore")
|
||||
feedbackTpl = readFixtures('system-feedback.underscore')
|
||||
|
||||
beforeEach ->
|
||||
setFixtures($("<script>", {id: "upload-dialog-tpl", type: "text/template"}).text(tpl))
|
||||
appendSetFixtures($("<script>", {id: "system-feedback-tpl", type: "text/template"}).text(feedbackTpl))
|
||||
CMS.URL.UPLOAD_ASSET = "/upload"
|
||||
describe "UploadDialog", ->
|
||||
tpl = readFixtures("upload-dialog.underscore")
|
||||
|
||||
@model = new CMS.Models.FileUpload(
|
||||
mimeTypes: ['application/pdf']
|
||||
)
|
||||
@dialogResponse = dialogResponse = []
|
||||
@view = new CMS.Views.UploadDialog(
|
||||
model: @model,
|
||||
onSuccess: (response) =>
|
||||
dialogResponse.push(response.response)
|
||||
)
|
||||
spyOn(@view, 'remove').andCallThrough()
|
||||
|
||||
# create mock file input, so that we aren't subject to browser restrictions
|
||||
@mockFiles = []
|
||||
mockFileInput = jasmine.createSpy('mockFileInput')
|
||||
mockFileInput.files = @mockFiles
|
||||
jqMockFileInput = jasmine.createSpyObj('jqMockFileInput', ['get', 'replaceWith'])
|
||||
jqMockFileInput.get.andReturn(mockFileInput)
|
||||
realMethod = @view.$
|
||||
spyOn(@view, "$").andCallFake (selector) ->
|
||||
if selector == "input[type=file]"
|
||||
jqMockFileInput
|
||||
else
|
||||
realMethod.apply(this, arguments)
|
||||
|
||||
afterEach ->
|
||||
delete CMS.URL.UPLOAD_ASSET
|
||||
|
||||
describe "Basic", ->
|
||||
it "should be shown by default", ->
|
||||
expect(@view.options.shown).toBeTruthy()
|
||||
|
||||
it "should render without a file selected", ->
|
||||
@view.render()
|
||||
expect(@view.$el).toContain("input[type=file]")
|
||||
expect(@view.$(".action-upload")).toHaveClass("disabled")
|
||||
|
||||
it "should render with a PDF selected", ->
|
||||
file = {name: "fake.pdf", "type": "application/pdf"}
|
||||
@mockFiles.push(file)
|
||||
@model.set("selectedFile", file)
|
||||
@view.render()
|
||||
expect(@view.$el).toContain("input[type=file]")
|
||||
expect(@view.$el).not.toContain("#upload_error")
|
||||
expect(@view.$(".action-upload")).not.toHaveClass("disabled")
|
||||
|
||||
it "should render an error with an invalid file type selected", ->
|
||||
file = {name: "fake.png", "type": "image/png"}
|
||||
@mockFiles.push(file)
|
||||
@model.set("selectedFile", file)
|
||||
@view.render()
|
||||
expect(@view.$el).toContain("input[type=file]")
|
||||
expect(@view.$el).toContain("#upload_error")
|
||||
expect(@view.$(".action-upload")).toHaveClass("disabled")
|
||||
|
||||
it "adds body class on show()", ->
|
||||
@view.show()
|
||||
expect(@view.options.shown).toBeTruthy()
|
||||
# can't test: this blows up the spec runner
|
||||
# expect($("body")).toHaveClass("dialog-is-shown")
|
||||
|
||||
it "removes body class on hide()", ->
|
||||
@view.hide()
|
||||
expect(@view.options.shown).toBeFalsy()
|
||||
# can't test: this blows up the spec runner
|
||||
# expect($("body")).not.toHaveClass("dialog-is-shown")
|
||||
|
||||
describe "Uploads", ->
|
||||
beforeEach ->
|
||||
@requests = requests = []
|
||||
@xhr = sinon.useFakeXMLHttpRequest()
|
||||
@xhr.onCreate = (xhr) -> requests.push(xhr)
|
||||
@clock = sinon.useFakeTimers()
|
||||
setFixtures($("<script>", {id: "upload-dialog-tpl", type: "text/template"}).text(tpl))
|
||||
appendSetFixtures($("<script>", {id: "system-feedback-tpl", type: "text/template"}).text(feedbackTpl))
|
||||
CMS.URL.UPLOAD_ASSET = "/upload"
|
||||
|
||||
@model = new FileUpload(
|
||||
mimeTypes: ['application/pdf']
|
||||
)
|
||||
@dialogResponse = dialogResponse = []
|
||||
@view = new UploadDialog(
|
||||
model: @model,
|
||||
onSuccess: (response) =>
|
||||
dialogResponse.push(response.response)
|
||||
)
|
||||
spyOn(@view, 'remove').andCallThrough()
|
||||
|
||||
# create mock file input, so that we aren't subject to browser restrictions
|
||||
@mockFiles = []
|
||||
mockFileInput = jasmine.createSpy('mockFileInput')
|
||||
mockFileInput.files = @mockFiles
|
||||
jqMockFileInput = jasmine.createSpyObj('jqMockFileInput', ['get', 'replaceWith'])
|
||||
jqMockFileInput.get.andReturn(mockFileInput)
|
||||
realMethod = @view.$
|
||||
spyOn(@view, "$").andCallFake (selector) ->
|
||||
if selector == "input[type=file]"
|
||||
jqMockFileInput
|
||||
else
|
||||
realMethod.apply(this, arguments)
|
||||
|
||||
afterEach ->
|
||||
@xhr.restore()
|
||||
@clock.restore()
|
||||
delete CMS.URL.UPLOAD_ASSET
|
||||
|
||||
it "can upload correctly", ->
|
||||
@view.upload()
|
||||
expect(@model.get("uploading")).toBeTruthy()
|
||||
expect(@requests.length).toEqual(1)
|
||||
request = @requests[0]
|
||||
expect(request.url).toEqual("/upload")
|
||||
expect(request.method).toEqual("POST")
|
||||
describe "Basic", ->
|
||||
it "should be shown by default", ->
|
||||
expect(@view.options.shown).toBeTruthy()
|
||||
|
||||
request.respond(200, {"Content-Type": "application/json"},
|
||||
'{"response": "dummy_response"}')
|
||||
expect(@model.get("uploading")).toBeFalsy()
|
||||
expect(@model.get("finished")).toBeTruthy()
|
||||
expect(@dialogResponse.pop()).toEqual("dummy_response")
|
||||
it "should render without a file selected", ->
|
||||
@view.render()
|
||||
expect(@view.$el).toContain("input[type=file]")
|
||||
expect(@view.$(".action-upload")).toHaveClass("disabled")
|
||||
|
||||
it "can handle upload errors", ->
|
||||
@view.upload()
|
||||
@requests[0].respond(500)
|
||||
expect(@model.get("title")).toMatch(/error/)
|
||||
expect(@view.remove).not.toHaveBeenCalled()
|
||||
it "should render with a PDF selected", ->
|
||||
file = {name: "fake.pdf", "type": "application/pdf"}
|
||||
@mockFiles.push(file)
|
||||
@model.set("selectedFile", file)
|
||||
@view.render()
|
||||
expect(@view.$el).toContain("input[type=file]")
|
||||
expect(@view.$el).not.toContain("#upload_error")
|
||||
expect(@view.$(".action-upload")).not.toHaveClass("disabled")
|
||||
|
||||
it "removes itself after two seconds on successful upload", ->
|
||||
@view.upload()
|
||||
@requests[0].respond(200, {"Content-Type": "application/json"},
|
||||
'{"response": "dummy_response"}')
|
||||
expect(@view.remove).not.toHaveBeenCalled()
|
||||
@clock.tick(2001)
|
||||
expect(@view.remove).toHaveBeenCalled()
|
||||
it "should render an error with an invalid file type selected", ->
|
||||
file = {name: "fake.png", "type": "image/png"}
|
||||
@mockFiles.push(file)
|
||||
@model.set("selectedFile", file)
|
||||
@view.render()
|
||||
expect(@view.$el).toContain("input[type=file]")
|
||||
expect(@view.$el).toContain("#upload_error")
|
||||
expect(@view.$(".action-upload")).toHaveClass("disabled")
|
||||
|
||||
it "adds body class on show()", ->
|
||||
@view.show()
|
||||
expect(@view.options.shown).toBeTruthy()
|
||||
# can't test: this blows up the spec runner
|
||||
# expect($("body")).toHaveClass("dialog-is-shown")
|
||||
|
||||
it "removes body class on hide()", ->
|
||||
@view.hide()
|
||||
expect(@view.options.shown).toBeFalsy()
|
||||
# can't test: this blows up the spec runner
|
||||
# expect($("body")).not.toHaveClass("dialog-is-shown")
|
||||
|
||||
describe "Uploads", ->
|
||||
beforeEach ->
|
||||
@requests = requests = []
|
||||
@xhr = sinon.useFakeXMLHttpRequest()
|
||||
@xhr.onCreate = (xhr) -> requests.push(xhr)
|
||||
@clock = sinon.useFakeTimers()
|
||||
|
||||
afterEach ->
|
||||
@xhr.restore()
|
||||
@clock.restore()
|
||||
|
||||
it "can upload correctly", ->
|
||||
@view.upload()
|
||||
expect(@model.get("uploading")).toBeTruthy()
|
||||
expect(@requests.length).toEqual(1)
|
||||
request = @requests[0]
|
||||
expect(request.url).toEqual("/upload")
|
||||
expect(request.method).toEqual("POST")
|
||||
|
||||
request.respond(200, {"Content-Type": "application/json"},
|
||||
'{"response": "dummy_response"}')
|
||||
expect(@model.get("uploading")).toBeFalsy()
|
||||
expect(@model.get("finished")).toBeTruthy()
|
||||
expect(@dialogResponse.pop()).toEqual("dummy_response")
|
||||
|
||||
it "can handle upload errors", ->
|
||||
@view.upload()
|
||||
@requests[0].respond(500)
|
||||
expect(@model.get("title")).toMatch(/error/)
|
||||
expect(@view.remove).not.toHaveBeenCalled()
|
||||
|
||||
it "removes itself after two seconds on successful upload", ->
|
||||
@view.upload()
|
||||
@requests[0].respond(200, {"Content-Type": "application/json"},
|
||||
'{"response": "dummy_response"}')
|
||||
expect(@view.remove).not.toHaveBeenCalled()
|
||||
@clock.tick(2001)
|
||||
expect(@view.remove).toHaveBeenCalled()
|
||||
|
||||
@@ -1,39 +1,40 @@
|
||||
AjaxPrefix.addAjaxPrefix(jQuery, -> CMS.prefix)
|
||||
|
||||
@CMS =
|
||||
Models: {}
|
||||
Views: {}
|
||||
Collections: {}
|
||||
URL: {}
|
||||
|
||||
prefix: $("meta[name='path_prefix']").attr('content')
|
||||
|
||||
_.extend CMS, Backbone.Events
|
||||
|
||||
$ ->
|
||||
Backbone.emulateHTTP = true
|
||||
|
||||
$.ajaxSetup
|
||||
headers : { 'X-CSRFToken': $.cookie 'csrftoken' }
|
||||
dataType: 'json'
|
||||
|
||||
$(document).ajaxError (event, jqXHR, ajaxSettings, thrownError) ->
|
||||
if ajaxSettings.notifyOnError is false
|
||||
return
|
||||
if jqXHR.responseText
|
||||
try
|
||||
message = JSON.parse(jqXHR.responseText).error
|
||||
catch error
|
||||
message = _.str.truncate(jqXHR.responseText, 300)
|
||||
else
|
||||
message = gettext("This may be happening because of an error with our server or your internet connection. Try refreshing the page or making sure you are online.")
|
||||
msg = new CMS.Views.Notification.Error(
|
||||
"title": gettext("Studio's having trouble saving your work")
|
||||
"message": message
|
||||
)
|
||||
msg.show()
|
||||
define ["jquery", "underscore.string", "backbone", "js/views/feedback_notification", "jquery.cookie"],
|
||||
($, str, Backbone, NotificationView) ->
|
||||
AjaxPrefix.addAjaxPrefix jQuery, ->
|
||||
$("meta[name='path_prefix']").attr('content')
|
||||
|
||||
window.CMS = window.CMS or {}
|
||||
CMS.URL = CMS.URL or {}
|
||||
window.onTouchBasedDevice = ->
|
||||
navigator.userAgent.match /iPhone|iPod|iPad/i
|
||||
|
||||
$('body').addClass 'touch-based-device' if onTouchBasedDevice()
|
||||
_.extend CMS, Backbone.Events
|
||||
|
||||
main = ->
|
||||
Backbone.emulateHTTP = true
|
||||
|
||||
$.ajaxSetup
|
||||
headers : { 'X-CSRFToken': $.cookie 'csrftoken' }
|
||||
dataType: 'json'
|
||||
|
||||
$(document).ajaxError (event, jqXHR, ajaxSettings, thrownError) ->
|
||||
if ajaxSettings.notifyOnError is false
|
||||
return
|
||||
if jqXHR.responseText
|
||||
try
|
||||
message = JSON.parse(jqXHR.responseText).error
|
||||
catch error
|
||||
message = str.truncate(jqXHR.responseText, 300)
|
||||
else
|
||||
message = gettext("This may be happening because of an error with our server or your internet connection. Try refreshing the page or making sure you are online.")
|
||||
msg = new NotificationView.Error(
|
||||
"title": gettext("Studio's having trouble saving your work")
|
||||
"message": message
|
||||
)
|
||||
msg.show()
|
||||
|
||||
if onTouchBasedDevice()
|
||||
$('body').addClass 'touch-based-device'
|
||||
|
||||
$(main)
|
||||
return main
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
class CMS.Models.Module extends Backbone.Model
|
||||
url: '/save_item'
|
||||
define ["backbone"], (Backbone) ->
|
||||
class Module extends Backbone.Model
|
||||
url: '/save_item'
|
||||
|
||||
|
||||
@@ -1,150 +1,153 @@
|
||||
class CMS.Views.ModuleEdit extends Backbone.View
|
||||
tagName: 'li'
|
||||
className: 'component'
|
||||
editorMode: 'editor-mode'
|
||||
define ["backbone", "jquery", "underscore", "gettext", "xmodule",
|
||||
"js/views/feedback_notification", "js/views/metadata", "js/collections/metadata"
|
||||
"jquery.inputnumber"],
|
||||
(Backbone, $, _, gettext, XModule, NotificationView, MetadataView, MetadataCollection) ->
|
||||
class ModuleEdit extends Backbone.View
|
||||
tagName: 'li'
|
||||
className: 'component'
|
||||
editorMode: 'editor-mode'
|
||||
|
||||
events:
|
||||
"click .component-editor .cancel-button": 'clickCancelButton'
|
||||
"click .component-editor .save-button": 'clickSaveButton'
|
||||
"click .component-actions .edit-button": 'clickEditButton'
|
||||
"click .component-actions .delete-button": 'onDelete'
|
||||
"click .mode a": 'clickModeButton'
|
||||
events:
|
||||
"click .component-editor .cancel-button": 'clickCancelButton'
|
||||
"click .component-editor .save-button": 'clickSaveButton'
|
||||
"click .component-actions .edit-button": 'clickEditButton'
|
||||
"click .component-actions .delete-button": 'onDelete'
|
||||
"click .mode a": 'clickModeButton'
|
||||
|
||||
initialize: ->
|
||||
@onDelete = @options.onDelete
|
||||
@render()
|
||||
initialize: ->
|
||||
@onDelete = @options.onDelete
|
||||
@render()
|
||||
|
||||
$component_editor: => @$el.find('.component-editor')
|
||||
$component_editor: => @$el.find('.component-editor')
|
||||
|
||||
loadDisplay: ->
|
||||
XModule.loadModule(@$el.find('.xmodule_display'))
|
||||
loadDisplay: ->
|
||||
XModule.loadModule(@$el.find('.xmodule_display'))
|
||||
|
||||
loadEdit: ->
|
||||
if not @module
|
||||
@module = XModule.loadModule(@$el.find('.xmodule_edit'))
|
||||
# At this point, metadata-edit.html will be loaded, and the metadata (as JSON) is available.
|
||||
metadataEditor = @$el.find('.metadata_edit')
|
||||
metadataData = metadataEditor.data('metadata')
|
||||
models = [];
|
||||
for key of metadataData
|
||||
models.push(metadataData[key])
|
||||
@metadataEditor = new CMS.Views.Metadata.Editor({
|
||||
el: metadataEditor,
|
||||
collection: new CMS.Models.MetadataCollection(models)
|
||||
})
|
||||
loadEdit: ->
|
||||
if not @module
|
||||
@module = XModule.loadModule(@$el.find('.xmodule_edit'))
|
||||
# At this point, metadata-edit.html will be loaded, and the metadata (as JSON) is available.
|
||||
metadataEditor = @$el.find('.metadata_edit')
|
||||
metadataData = metadataEditor.data('metadata')
|
||||
models = [];
|
||||
for key of metadataData
|
||||
models.push(metadataData[key])
|
||||
@metadataEditor = new MetadataView.Editor({
|
||||
el: metadataEditor,
|
||||
collection: new MetadataCollection(models)
|
||||
})
|
||||
|
||||
# Need to update set "active" class on data editor if there is one.
|
||||
# If we are only showing settings, hide the data editor controls and update settings accordingly.
|
||||
if @hasDataEditor()
|
||||
@selectMode(@editorMode)
|
||||
else
|
||||
@hideDataEditor()
|
||||
# Need to update set "active" class on data editor if there is one.
|
||||
# If we are only showing settings, hide the data editor controls and update settings accordingly.
|
||||
if @hasDataEditor()
|
||||
@selectMode(@editorMode)
|
||||
else
|
||||
@hideDataEditor()
|
||||
|
||||
title = interpolate(gettext('<em>Editing:</em> %s'),
|
||||
[@metadataEditor.getDisplayName()])
|
||||
@$el.find('.component-name').html(title)
|
||||
title = interpolate(gettext('<em>Editing:</em> %s'),
|
||||
[@metadataEditor.getDisplayName()])
|
||||
@$el.find('.component-name').html(title)
|
||||
|
||||
customMetadata: ->
|
||||
# Hack to support metadata fields that aren't part of the metadata editor (ie, LaTeX high level source).
|
||||
# Walk through the set of elements which have the 'data-metadata_name' attribute and
|
||||
# build up an object to pass back to the server on the subsequent POST.
|
||||
# Note that these values will always be sent back on POST, even if they did not actually change.
|
||||
_metadata = {}
|
||||
_metadata[$(el).data("metadata-name")] = el.value for el in $('[data-metadata-name]', @$component_editor())
|
||||
return _metadata
|
||||
customMetadata: ->
|
||||
# Hack to support metadata fields that aren't part of the metadata editor (ie, LaTeX high level source).
|
||||
# Walk through the set of elements which have the 'data-metadata_name' attribute and
|
||||
# build up an object to pass back to the server on the subsequent POST.
|
||||
# Note that these values will always be sent back on POST, even if they did not actually change.
|
||||
_metadata = {}
|
||||
_metadata[$(el).data("metadata-name")] = el.value for el in $('[data-metadata-name]', @$component_editor())
|
||||
return _metadata
|
||||
|
||||
changedMetadata: ->
|
||||
return _.extend(@metadataEditor.getModifiedMetadataValues(), @customMetadata())
|
||||
changedMetadata: ->
|
||||
return _.extend(@metadataEditor.getModifiedMetadataValues(), @customMetadata())
|
||||
|
||||
createItem: (parent, payload) ->
|
||||
payload.parent_location = parent
|
||||
$.post(
|
||||
"/create_item"
|
||||
payload
|
||||
(data) =>
|
||||
@model.set(id: data.id)
|
||||
@$el.data('id', data.id)
|
||||
@render()
|
||||
)
|
||||
|
||||
render: ->
|
||||
if @model.id
|
||||
@$el.load("/preview_component/#{@model.id}", =>
|
||||
@loadDisplay()
|
||||
@delegateEvents()
|
||||
createItem: (parent, payload) ->
|
||||
payload.parent_location = parent
|
||||
$.post(
|
||||
"/create_item"
|
||||
payload
|
||||
(data) =>
|
||||
@model.set(id: data.id)
|
||||
@$el.data('id', data.id)
|
||||
@render()
|
||||
)
|
||||
|
||||
clickSaveButton: (event) =>
|
||||
event.preventDefault()
|
||||
data = @module.save()
|
||||
render: ->
|
||||
if @model.id
|
||||
@$el.load("/preview_component/#{@model.id}", =>
|
||||
@loadDisplay()
|
||||
@delegateEvents()
|
||||
)
|
||||
|
||||
analytics.track "Saved Module",
|
||||
course: course_location_analytics
|
||||
id: _this.model.id
|
||||
clickSaveButton: (event) =>
|
||||
event.preventDefault()
|
||||
data = @module.save()
|
||||
|
||||
data.metadata = _.extend(data.metadata || {}, @changedMetadata())
|
||||
@hideModal()
|
||||
saving = new CMS.Views.Notification.Mini
|
||||
title: gettext('Saving…')
|
||||
saving.show()
|
||||
@model.save(data).done( =>
|
||||
# # showToastMessage("Your changes have been saved.", null, 3)
|
||||
@module = null
|
||||
@render()
|
||||
analytics.track "Saved Module",
|
||||
course: course_location_analytics
|
||||
id: _this.model.id
|
||||
|
||||
data.metadata = _.extend(data.metadata || {}, @changedMetadata())
|
||||
@hideModal()
|
||||
saving = new NotificationView.Mini
|
||||
title: gettext('Saving…')
|
||||
saving.show()
|
||||
@model.save(data).done( =>
|
||||
# # showToastMessage("Your changes have been saved.", null, 3)
|
||||
@module = null
|
||||
@render()
|
||||
@$el.removeClass('editing')
|
||||
saving.hide()
|
||||
)
|
||||
|
||||
clickCancelButton: (event) ->
|
||||
event.preventDefault()
|
||||
@$el.removeClass('editing')
|
||||
saving.hide()
|
||||
).fail( ->
|
||||
showToastMessage(gettext("There was an error saving your changes. Please try again."), null, 3)
|
||||
)
|
||||
@$component_editor().slideUp(150)
|
||||
@hideModal()
|
||||
|
||||
clickCancelButton: (event) ->
|
||||
event.preventDefault()
|
||||
@$el.removeClass('editing')
|
||||
@$component_editor().slideUp(150)
|
||||
@hideModal()
|
||||
hideModal: ->
|
||||
$modalCover = $(".modal-cover")
|
||||
$modalCover.hide()
|
||||
$modalCover.removeClass('is-fixed')
|
||||
|
||||
hideModal: ->
|
||||
$modalCover.hide()
|
||||
$modalCover.removeClass('is-fixed')
|
||||
clickEditButton: (event) ->
|
||||
event.preventDefault()
|
||||
@$el.addClass('editing')
|
||||
$(".modal-cover").show().addClass('is-fixed')
|
||||
@$component_editor().slideDown(150)
|
||||
@loadEdit()
|
||||
|
||||
clickEditButton: (event) ->
|
||||
event.preventDefault()
|
||||
@$el.addClass('editing')
|
||||
$modalCover.show().addClass('is-fixed')
|
||||
@$component_editor().slideDown(150)
|
||||
@loadEdit()
|
||||
clickModeButton: (event) ->
|
||||
event.preventDefault()
|
||||
if not @hasDataEditor()
|
||||
return
|
||||
@selectMode(event.currentTarget.parentElement.id)
|
||||
|
||||
clickModeButton: (event) ->
|
||||
event.preventDefault()
|
||||
if not @hasDataEditor()
|
||||
return
|
||||
@selectMode(event.currentTarget.parentElement.id)
|
||||
hasDataEditor: =>
|
||||
return @$el.find('.wrapper-comp-editor').length > 0
|
||||
|
||||
hasDataEditor: =>
|
||||
return @$el.find('.wrapper-comp-editor').length > 0
|
||||
selectMode: (mode) =>
|
||||
dataEditor = @$el.find('.wrapper-comp-editor')
|
||||
settingsEditor = @$el.find('.wrapper-comp-settings')
|
||||
editorModeButton = @$el.find('#editor-mode').find("a")
|
||||
settingsModeButton = @$el.find('#settings-mode').find("a")
|
||||
|
||||
selectMode: (mode) =>
|
||||
dataEditor = @$el.find('.wrapper-comp-editor')
|
||||
settingsEditor = @$el.find('.wrapper-comp-settings')
|
||||
editorModeButton = @$el.find('#editor-mode').find("a")
|
||||
settingsModeButton = @$el.find('#settings-mode').find("a")
|
||||
if mode == @editorMode
|
||||
# Because of CodeMirror editor, cannot hide the data editor when it is first loaded. Therefore
|
||||
# we have to use a class of is-inactive instead of is-active.
|
||||
dataEditor.removeClass('is-inactive')
|
||||
editorModeButton.addClass('is-set')
|
||||
settingsEditor.removeClass('is-active')
|
||||
settingsModeButton.removeClass('is-set')
|
||||
else
|
||||
dataEditor.addClass('is-inactive')
|
||||
editorModeButton.removeClass('is-set')
|
||||
settingsEditor.addClass('is-active')
|
||||
settingsModeButton.addClass('is-set')
|
||||
|
||||
if mode == @editorMode
|
||||
# Because of CodeMirror editor, cannot hide the data editor when it is first loaded. Therefore
|
||||
# we have to use a class of is-inactive instead of is-active.
|
||||
dataEditor.removeClass('is-inactive')
|
||||
editorModeButton.addClass('is-set')
|
||||
settingsEditor.removeClass('is-active')
|
||||
settingsModeButton.removeClass('is-set')
|
||||
else
|
||||
dataEditor.addClass('is-inactive')
|
||||
editorModeButton.removeClass('is-set')
|
||||
settingsEditor.addClass('is-active')
|
||||
settingsModeButton.addClass('is-set')
|
||||
|
||||
hideDataEditor: =>
|
||||
editorModeButtonParent = @$el.find('#editor-mode')
|
||||
editorModeButtonParent.addClass('inactive-mode')
|
||||
editorModeButtonParent.removeClass('active-mode')
|
||||
@$el.find('.wrapper-comp-settings').addClass('is-active')
|
||||
@$el.find('#settings-mode').find("a").addClass('is-set')
|
||||
hideDataEditor: =>
|
||||
editorModeButtonParent = @$el.find('#editor-mode')
|
||||
editorModeButtonParent.addClass('inactive-mode')
|
||||
editorModeButtonParent.removeClass('active-mode')
|
||||
@$el.find('.wrapper-comp-settings').addClass('is-active')
|
||||
@$el.find('#settings-mode').find("a").addClass('is-set')
|
||||
|
||||
@@ -1,94 +1,96 @@
|
||||
class CMS.Views.TabsEdit extends Backbone.View
|
||||
define ["jquery", "jquery.ui", "backbone", "js/views/feedback_prompt", "js/views/feedback_notification", "coffee/src/models/module", "coffee/src/views/module_edit"],
|
||||
($, ui, Backbone, PromptView, NotificationView, ModuleModel, ModuleEditView) ->
|
||||
class TabsEdit extends Backbone.View
|
||||
|
||||
initialize: =>
|
||||
@$('.component').each((idx, element) =>
|
||||
new CMS.Views.ModuleEdit(
|
||||
el: element,
|
||||
onDelete: @deleteTab,
|
||||
model: new CMS.Models.Module(
|
||||
id: $(element).data('id'),
|
||||
)
|
||||
)
|
||||
)
|
||||
initialize: =>
|
||||
@$('.component').each((idx, element) =>
|
||||
new ModuleEditView(
|
||||
el: element,
|
||||
onDelete: @deleteTab,
|
||||
model: new ModuleModel(
|
||||
id: $(element).data('id'),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@options.mast.find('.new-tab').on('click', @addNewTab)
|
||||
@$('.components').sortable(
|
||||
handle: '.drag-handle'
|
||||
update: @tabMoved
|
||||
helper: 'clone'
|
||||
opacity: '0.5'
|
||||
placeholder: 'component-placeholder'
|
||||
forcePlaceholderSize: true
|
||||
axis: 'y'
|
||||
items: '> .component'
|
||||
)
|
||||
@options.mast.find('.new-tab').on('click', @addNewTab)
|
||||
@$('.components').sortable(
|
||||
handle: '.drag-handle'
|
||||
update: @tabMoved
|
||||
helper: 'clone'
|
||||
opacity: '0.5'
|
||||
placeholder: 'component-placeholder'
|
||||
forcePlaceholderSize: true
|
||||
axis: 'y'
|
||||
items: '> .component'
|
||||
)
|
||||
|
||||
tabMoved: (event, ui) =>
|
||||
tabs = []
|
||||
@$('.component').each((idx, element) =>
|
||||
tabs.push($(element).data('id'))
|
||||
)
|
||||
tabMoved: (event, ui) =>
|
||||
tabs = []
|
||||
@$('.component').each((idx, element) =>
|
||||
tabs.push($(element).data('id'))
|
||||
)
|
||||
|
||||
analytics.track "Reordered Static Pages",
|
||||
course: course_location_analytics
|
||||
analytics.track "Reordered Static Pages",
|
||||
course: course_location_analytics
|
||||
|
||||
$.ajax({
|
||||
type:'POST',
|
||||
url: '/reorder_static_tabs',
|
||||
data: JSON.stringify({
|
||||
tabs : tabs
|
||||
}),
|
||||
contentType: 'application/json'
|
||||
})
|
||||
$.ajax({
|
||||
type:'POST',
|
||||
url: '/reorder_static_tabs',
|
||||
data: JSON.stringify({
|
||||
tabs : tabs
|
||||
}),
|
||||
contentType: 'application/json'
|
||||
})
|
||||
|
||||
addNewTab: (event) =>
|
||||
event.preventDefault()
|
||||
addNewTab: (event) =>
|
||||
event.preventDefault()
|
||||
|
||||
editor = new CMS.Views.ModuleEdit(
|
||||
onDelete: @deleteTab
|
||||
model: new CMS.Models.Module()
|
||||
)
|
||||
editor = new ModuleEditView(
|
||||
onDelete: @deleteTab
|
||||
model: new ModuleModel()
|
||||
)
|
||||
|
||||
$('.new-component-item').before(editor.$el)
|
||||
editor.$el.addClass('new')
|
||||
setTimeout(=>
|
||||
editor.$el.removeClass('new')
|
||||
, 500)
|
||||
$('.new-component-item').before(editor.$el)
|
||||
editor.$el.addClass('new')
|
||||
setTimeout(=>
|
||||
editor.$el.removeClass('new')
|
||||
, 500)
|
||||
|
||||
editor.createItem(
|
||||
@model.get('id'),
|
||||
{category: 'static_tab'}
|
||||
)
|
||||
editor.createItem(
|
||||
@model.get('id'),
|
||||
{category: 'static_tab'}
|
||||
)
|
||||
|
||||
analytics.track "Added Static Page",
|
||||
course: course_location_analytics
|
||||
analytics.track "Added Static Page",
|
||||
course: course_location_analytics
|
||||
|
||||
deleteTab: (event) =>
|
||||
confirm = new CMS.Views.Prompt.Warning
|
||||
title: gettext('Delete Component Confirmation')
|
||||
message: gettext('Are you sure you want to delete this component? This action cannot be undone.')
|
||||
actions:
|
||||
primary:
|
||||
text: gettext("OK")
|
||||
click: (view) ->
|
||||
view.hide()
|
||||
$component = $(event.currentTarget).parents('.component')
|
||||
deleteTab: (event) =>
|
||||
confirm = new PromptView.Warning
|
||||
title: gettext('Delete Component Confirmation')
|
||||
message: gettext('Are you sure you want to delete this component? This action cannot be undone.')
|
||||
actions:
|
||||
primary:
|
||||
text: gettext("OK")
|
||||
click: (view) ->
|
||||
view.hide()
|
||||
$component = $(event.currentTarget).parents('.component')
|
||||
|
||||
analytics.track "Deleted Static Page",
|
||||
course: course_location_analytics
|
||||
id: $component.data('id')
|
||||
deleting = new CMS.Views.Notification.Mini
|
||||
title: gettext('Deleting…')
|
||||
deleting.show()
|
||||
$.post('/delete_item', {
|
||||
id: $component.data('id')
|
||||
}, =>
|
||||
$component.remove()
|
||||
deleting.hide()
|
||||
)
|
||||
secondary: [
|
||||
text: gettext('Cancel')
|
||||
click: (view) ->
|
||||
view.hide()
|
||||
]
|
||||
confirm.show()
|
||||
analytics.track "Deleted Static Page",
|
||||
course: course_location_analytics
|
||||
id: $component.data('id')
|
||||
deleting = new NotificationView.Mini
|
||||
title: gettext('Deleting…')
|
||||
deleting.show()
|
||||
$.post('/delete_item', {
|
||||
id: $component.data('id')
|
||||
}, =>
|
||||
$component.remove()
|
||||
deleting.hide()
|
||||
)
|
||||
secondary: [
|
||||
text: gettext('Cancel')
|
||||
click: (view) ->
|
||||
view.hide()
|
||||
]
|
||||
confirm.show()
|
||||
|
||||
@@ -1,271 +1,274 @@
|
||||
class CMS.Views.UnitEdit extends Backbone.View
|
||||
events:
|
||||
'click .new-component .new-component-type a.multiple-templates': 'showComponentTemplates'
|
||||
'click .new-component .new-component-type a.single-template': 'saveNewComponent'
|
||||
'click .new-component .cancel-button': 'closeNewComponent'
|
||||
'click .new-component-templates .new-component-template a': 'saveNewComponent'
|
||||
'click .new-component-templates .cancel-button': 'closeNewComponent'
|
||||
'click .delete-draft': 'deleteDraft'
|
||||
'click .create-draft': 'createDraft'
|
||||
'click .publish-draft': 'publishDraft'
|
||||
'change .visibility-select': 'setVisibility'
|
||||
define ["jquery", "jquery.ui", "gettext", "backbone",
|
||||
"js/views/feedback_notification", "js/views/feedback_prompt",
|
||||
"coffee/src/models/module", "coffee/src/views/module_edit"],
|
||||
($, ui, gettext, Backbone, NotificationView, PromptView, ModuleModel, ModuleEditView) ->
|
||||
class UnitEditView extends Backbone.View
|
||||
events:
|
||||
'click .new-component .new-component-type a.multiple-templates': 'showComponentTemplates'
|
||||
'click .new-component .new-component-type a.single-template': 'saveNewComponent'
|
||||
'click .new-component .cancel-button': 'closeNewComponent'
|
||||
'click .new-component-templates .new-component-template a': 'saveNewComponent'
|
||||
'click .new-component-templates .cancel-button': 'closeNewComponent'
|
||||
'click .delete-draft': 'deleteDraft'
|
||||
'click .create-draft': 'createDraft'
|
||||
'click .publish-draft': 'publishDraft'
|
||||
'change .visibility-select': 'setVisibility'
|
||||
|
||||
initialize: =>
|
||||
@visibilityView = new CMS.Views.UnitEdit.Visibility(
|
||||
el: @$('.visibility-select')
|
||||
model: @model
|
||||
)
|
||||
initialize: =>
|
||||
@visibilityView = new UnitEditView.Visibility(
|
||||
el: @$('.visibility-select')
|
||||
model: @model
|
||||
)
|
||||
|
||||
@locationView = new CMS.Views.UnitEdit.LocationState(
|
||||
el: @$('.section-item.editing a')
|
||||
model: @model
|
||||
)
|
||||
@locationView = new UnitEditView.LocationState(
|
||||
el: @$('.section-item.editing a')
|
||||
model: @model
|
||||
)
|
||||
|
||||
@nameView = new CMS.Views.UnitEdit.NameEdit(
|
||||
el: @$('.unit-name-input')
|
||||
model: @model
|
||||
)
|
||||
@nameView = new UnitEditView.NameEdit(
|
||||
el: @$('.unit-name-input')
|
||||
model: @model
|
||||
)
|
||||
|
||||
@model.on('change:state', @render)
|
||||
@model.on('change:state', @render)
|
||||
|
||||
@$newComponentItem = @$('.new-component-item')
|
||||
@$newComponentTypePicker = @$('.new-component')
|
||||
@$newComponentTemplatePickers = @$('.new-component-templates')
|
||||
@$newComponentButton = @$('.new-component-button')
|
||||
@$newComponentItem = @$('.new-component-item')
|
||||
@$newComponentTypePicker = @$('.new-component')
|
||||
@$newComponentTemplatePickers = @$('.new-component-templates')
|
||||
@$newComponentButton = @$('.new-component-button')
|
||||
|
||||
@$('.components').sortable(
|
||||
handle: '.drag-handle'
|
||||
update: (event, ui) =>
|
||||
analytics.track "Reordered Components",
|
||||
course: course_location_analytics
|
||||
id: unit_location_analytics
|
||||
@$('.components').sortable(
|
||||
handle: '.drag-handle'
|
||||
update: (event, ui) =>
|
||||
analytics.track "Reordered Components",
|
||||
course: course_location_analytics
|
||||
id: unit_location_analytics
|
||||
|
||||
payload = children : @components()
|
||||
saving = new CMS.Views.Notification.Mini
|
||||
title: gettext('Saving…')
|
||||
saving.show()
|
||||
options = success : =>
|
||||
@model.unset('children')
|
||||
saving.hide()
|
||||
@model.save(payload, options)
|
||||
helper: 'clone'
|
||||
opacity: '0.5'
|
||||
placeholder: 'component-placeholder'
|
||||
forcePlaceholderSize: true
|
||||
axis: 'y'
|
||||
items: '> .component'
|
||||
)
|
||||
payload = children : @components()
|
||||
saving = new NotificationView.Mini
|
||||
title: gettext('Saving…')
|
||||
saving.show()
|
||||
options = success : =>
|
||||
@model.unset('children')
|
||||
saving.hide()
|
||||
@model.save(payload, options)
|
||||
helper: 'clone'
|
||||
opacity: '0.5'
|
||||
placeholder: 'component-placeholder'
|
||||
forcePlaceholderSize: true
|
||||
axis: 'y'
|
||||
items: '> .component'
|
||||
)
|
||||
|
||||
@$('.component').each((idx, element) =>
|
||||
new CMS.Views.ModuleEdit(
|
||||
el: element,
|
||||
onDelete: @deleteComponent,
|
||||
model: new CMS.Models.Module(
|
||||
id: $(element).data('id'),
|
||||
)
|
||||
)
|
||||
)
|
||||
@$('.component').each (idx, element) =>
|
||||
new ModuleEditView
|
||||
el: element,
|
||||
onDelete: @deleteComponent,
|
||||
model: new ModuleModel
|
||||
id: $(element).data('id')
|
||||
|
||||
showComponentTemplates: (event) =>
|
||||
event.preventDefault()
|
||||
showComponentTemplates: (event) =>
|
||||
event.preventDefault()
|
||||
|
||||
type = $(event.currentTarget).data('type')
|
||||
@$newComponentTypePicker.slideUp(250)
|
||||
@$(".new-component-#{type}").slideDown(250)
|
||||
$('html, body').animate({
|
||||
scrollTop: @$(".new-component-#{type}").offset().top
|
||||
}, 500)
|
||||
type = $(event.currentTarget).data('type')
|
||||
@$newComponentTypePicker.slideUp(250)
|
||||
@$(".new-component-#{type}").slideDown(250)
|
||||
$('html, body').animate({
|
||||
scrollTop: @$(".new-component-#{type}").offset().top
|
||||
}, 500)
|
||||
|
||||
closeNewComponent: (event) =>
|
||||
event.preventDefault()
|
||||
closeNewComponent: (event) =>
|
||||
event.preventDefault()
|
||||
|
||||
@$newComponentTypePicker.slideDown(250)
|
||||
@$newComponentTemplatePickers.slideUp(250)
|
||||
@$newComponentItem.removeClass('adding')
|
||||
@$newComponentItem.find('.rendered-component').remove()
|
||||
@$newComponentTypePicker.slideDown(250)
|
||||
@$newComponentTemplatePickers.slideUp(250)
|
||||
@$newComponentItem.removeClass('adding')
|
||||
@$newComponentItem.find('.rendered-component').remove()
|
||||
|
||||
saveNewComponent: (event) =>
|
||||
event.preventDefault()
|
||||
saveNewComponent: (event) =>
|
||||
event.preventDefault()
|
||||
|
||||
editor = new CMS.Views.ModuleEdit(
|
||||
onDelete: @deleteComponent
|
||||
model: new CMS.Models.Module()
|
||||
)
|
||||
editor = new ModuleEditView(
|
||||
onDelete: @deleteComponent
|
||||
model: new ModuleModel()
|
||||
)
|
||||
|
||||
@$newComponentItem.before(editor.$el)
|
||||
@$newComponentItem.before(editor.$el)
|
||||
|
||||
editor.createItem(
|
||||
@$el.data('id'),
|
||||
$(event.currentTarget).data()
|
||||
)
|
||||
editor.createItem(
|
||||
@$el.data('id'),
|
||||
$(event.currentTarget).data()
|
||||
)
|
||||
|
||||
analytics.track "Added a Component",
|
||||
course: course_location_analytics
|
||||
unit_id: unit_location_analytics
|
||||
type: $(event.currentTarget).data('location')
|
||||
analytics.track "Added a Component",
|
||||
course: course_location_analytics
|
||||
unit_id: unit_location_analytics
|
||||
type: $(event.currentTarget).data('location')
|
||||
|
||||
@closeNewComponent(event)
|
||||
@closeNewComponent(event)
|
||||
|
||||
components: => @$('.component').map((idx, el) -> $(el).data('id')).get()
|
||||
components: => @$('.component').map((idx, el) -> $(el).data('id')).get()
|
||||
|
||||
wait: (value) =>
|
||||
@$('.unit-body').toggleClass("waiting", value)
|
||||
wait: (value) =>
|
||||
@$('.unit-body').toggleClass("waiting", value)
|
||||
|
||||
render: =>
|
||||
if @model.hasChanged('state')
|
||||
@$el.toggleClass("edit-state-#{@model.previous('state')} edit-state-#{@model.get('state')}")
|
||||
@wait(false)
|
||||
render: =>
|
||||
if @model.hasChanged('state')
|
||||
@$el.toggleClass("edit-state-#{@model.previous('state')} edit-state-#{@model.get('state')}")
|
||||
@wait(false)
|
||||
|
||||
saveDraft: =>
|
||||
@model.save()
|
||||
saveDraft: =>
|
||||
@model.save()
|
||||
|
||||
deleteComponent: (event) =>
|
||||
event.preventDefault()
|
||||
msg = new CMS.Views.Prompt.Warning(
|
||||
title: gettext('Delete this component?'),
|
||||
message: gettext('Deleting this component is permanent and cannot be undone.'),
|
||||
actions:
|
||||
primary:
|
||||
text: gettext('Yes, delete this component'),
|
||||
click: (view) =>
|
||||
view.hide()
|
||||
deleting = new CMS.Views.Notification.Mini
|
||||
title: gettext('Deleting…'),
|
||||
deleting.show()
|
||||
$component = $(event.currentTarget).parents('.component')
|
||||
$.post('/delete_item', {
|
||||
id: $component.data('id')
|
||||
}, =>
|
||||
deleting.hide()
|
||||
analytics.track "Deleted a Component",
|
||||
course: course_location_analytics
|
||||
unit_id: unit_location_analytics
|
||||
deleteComponent: (event) =>
|
||||
event.preventDefault()
|
||||
msg = new PromptView.Warning(
|
||||
title: gettext('Delete this component?'),
|
||||
message: gettext('Deleting this component is permanent and cannot be undone.'),
|
||||
actions:
|
||||
primary:
|
||||
text: gettext('Yes, delete this component'),
|
||||
click: (view) =>
|
||||
view.hide()
|
||||
deleting = new NotificationView.Mini
|
||||
title: gettext('Deleting…'),
|
||||
deleting.show()
|
||||
$component = $(event.currentTarget).parents('.component')
|
||||
$.post('/delete_item', {
|
||||
id: $component.data('id')
|
||||
}, =>
|
||||
deleting.hide()
|
||||
analytics.track "Deleted a Component",
|
||||
course: course_location_analytics
|
||||
unit_id: unit_location_analytics
|
||||
id: $component.data('id')
|
||||
|
||||
$component.remove()
|
||||
# b/c we don't vigilantly keep children up to date
|
||||
# get rid of it before it hurts someone
|
||||
# sorry for the js, i couldn't figure out the coffee equivalent
|
||||
`_this.model.save({children: _this.components()},
|
||||
{success: function(model) {
|
||||
model.unset('children');
|
||||
}}
|
||||
);`
|
||||
)
|
||||
secondary:
|
||||
text: gettext('Cancel'),
|
||||
click: (view) ->
|
||||
view.hide()
|
||||
)
|
||||
msg.show()
|
||||
$component.remove()
|
||||
# b/c we don't vigilantly keep children up to date
|
||||
# get rid of it before it hurts someone
|
||||
# sorry for the js, i couldn't figure out the coffee equivalent
|
||||
`_this.model.save({children: _this.components()},
|
||||
{success: function(model) {
|
||||
model.unset('children');
|
||||
}}
|
||||
);`
|
||||
)
|
||||
secondary:
|
||||
text: gettext('Cancel'),
|
||||
click: (view) ->
|
||||
view.hide()
|
||||
)
|
||||
msg.show()
|
||||
|
||||
deleteDraft: (event) ->
|
||||
@wait(true)
|
||||
deleteDraft: (event) ->
|
||||
@wait(true)
|
||||
|
||||
$.post('/delete_item', {
|
||||
id: @$el.data('id')
|
||||
delete_children: true
|
||||
}, =>
|
||||
analytics.track "Deleted Draft",
|
||||
$.post('/delete_item', {
|
||||
id: @$el.data('id')
|
||||
delete_children: true
|
||||
}, =>
|
||||
analytics.track "Deleted Draft",
|
||||
course: course_location_analytics
|
||||
unit_id: unit_location_analytics
|
||||
|
||||
window.location.reload()
|
||||
)
|
||||
|
||||
createDraft: (event) ->
|
||||
@wait(true)
|
||||
|
||||
$.post('/create_draft', {
|
||||
id: @$el.data('id')
|
||||
}, =>
|
||||
analytics.track "Created Draft",
|
||||
course: course_location_analytics
|
||||
unit_id: unit_location_analytics
|
||||
|
||||
@model.set('state', 'draft')
|
||||
)
|
||||
|
||||
publishDraft: (event) ->
|
||||
@wait(true)
|
||||
@saveDraft()
|
||||
|
||||
$.post('/publish_draft', {
|
||||
id: @$el.data('id')
|
||||
}, =>
|
||||
analytics.track "Published Draft",
|
||||
course: course_location_analytics
|
||||
unit_id: unit_location_analytics
|
||||
|
||||
@model.set('state', 'public')
|
||||
)
|
||||
|
||||
setVisibility: (event) ->
|
||||
if @$('.visibility-select').val() == 'private'
|
||||
target_url = '/unpublish_unit'
|
||||
visibility = "private"
|
||||
else
|
||||
target_url = '/publish_draft'
|
||||
visibility = "public"
|
||||
|
||||
@wait(true)
|
||||
|
||||
$.post(target_url, {
|
||||
id: @$el.data('id')
|
||||
}, =>
|
||||
analytics.track "Set Unit Visibility",
|
||||
course: course_location_analytics
|
||||
unit_id: unit_location_analytics
|
||||
visibility: visibility
|
||||
|
||||
@model.set('state', @$('.visibility-select').val())
|
||||
)
|
||||
|
||||
class UnitEditView.NameEdit extends Backbone.View
|
||||
events:
|
||||
'change .unit-display-name-input': 'saveName'
|
||||
|
||||
initialize: =>
|
||||
@model.on('change:metadata', @render)
|
||||
@model.on('change:state', @setEnabled)
|
||||
@setEnabled()
|
||||
@saveName
|
||||
@$spinner = $('<span class="spinner-in-field-icon"></span>');
|
||||
|
||||
render: =>
|
||||
@$('.unit-display-name-input').val(@model.get('metadata').display_name)
|
||||
|
||||
setEnabled: =>
|
||||
disabled = @model.get('state') == 'public'
|
||||
if disabled
|
||||
@$('.unit-display-name-input').attr('disabled', true)
|
||||
else
|
||||
@$('.unit-display-name-input').removeAttr('disabled')
|
||||
|
||||
saveName: =>
|
||||
# Treat the metadata dictionary as immutable
|
||||
metadata = $.extend({}, @model.get('metadata'))
|
||||
metadata.display_name = @$('.unit-display-name-input').val()
|
||||
@model.save(metadata: metadata)
|
||||
# Update name shown in the right-hand side location summary.
|
||||
$('.unit-location .editing .unit-name').html(metadata.display_name)
|
||||
analytics.track "Edited Unit Name",
|
||||
course: course_location_analytics
|
||||
unit_id: unit_location_analytics
|
||||
|
||||
window.location.reload()
|
||||
)
|
||||
|
||||
createDraft: (event) ->
|
||||
@wait(true)
|
||||
|
||||
$.post('/create_draft', {
|
||||
id: @$el.data('id')
|
||||
}, =>
|
||||
analytics.track "Created Draft",
|
||||
course: course_location_analytics
|
||||
unit_id: unit_location_analytics
|
||||
|
||||
@model.set('state', 'draft')
|
||||
)
|
||||
|
||||
publishDraft: (event) ->
|
||||
@wait(true)
|
||||
@saveDraft()
|
||||
|
||||
$.post('/publish_draft', {
|
||||
id: @$el.data('id')
|
||||
}, =>
|
||||
analytics.track "Published Draft",
|
||||
course: course_location_analytics
|
||||
unit_id: unit_location_analytics
|
||||
|
||||
@model.set('state', 'public')
|
||||
)
|
||||
|
||||
setVisibility: (event) ->
|
||||
if @$('.visibility-select').val() == 'private'
|
||||
target_url = '/unpublish_unit'
|
||||
visibility = "private"
|
||||
else
|
||||
target_url = '/publish_draft'
|
||||
visibility = "public"
|
||||
|
||||
@wait(true)
|
||||
|
||||
$.post(target_url, {
|
||||
id: @$el.data('id')
|
||||
}, =>
|
||||
analytics.track "Set Unit Visibility",
|
||||
course: course_location_analytics
|
||||
unit_id: unit_location_analytics
|
||||
visibility: visibility
|
||||
|
||||
@model.set('state', @$('.visibility-select').val())
|
||||
)
|
||||
|
||||
class CMS.Views.UnitEdit.NameEdit extends Backbone.View
|
||||
events:
|
||||
'change .unit-display-name-input': 'saveName'
|
||||
|
||||
initialize: =>
|
||||
@model.on('change:metadata', @render)
|
||||
@model.on('change:state', @setEnabled)
|
||||
@setEnabled()
|
||||
@saveName
|
||||
@$spinner = $('<span class="spinner-in-field-icon"></span>');
|
||||
|
||||
render: =>
|
||||
@$('.unit-display-name-input').val(@model.get('metadata').display_name)
|
||||
|
||||
setEnabled: =>
|
||||
disabled = @model.get('state') == 'public'
|
||||
if disabled
|
||||
@$('.unit-display-name-input').attr('disabled', true)
|
||||
else
|
||||
@$('.unit-display-name-input').removeAttr('disabled')
|
||||
|
||||
saveName: =>
|
||||
# Treat the metadata dictionary as immutable
|
||||
metadata = $.extend({}, @model.get('metadata'))
|
||||
metadata.display_name = @$('.unit-display-name-input').val()
|
||||
@model.save(metadata: metadata)
|
||||
# Update name shown in the right-hand side location summary.
|
||||
$('.unit-location .editing .unit-name').html(metadata.display_name)
|
||||
analytics.track "Edited Unit Name",
|
||||
course: course_location_analytics
|
||||
unit_id: unit_location_analytics
|
||||
display_name: metadata.display_name
|
||||
display_name: metadata.display_name
|
||||
|
||||
|
||||
class CMS.Views.UnitEdit.LocationState extends Backbone.View
|
||||
initialize: =>
|
||||
@model.on('change:state', @render)
|
||||
class UnitEditView.LocationState extends Backbone.View
|
||||
initialize: =>
|
||||
@model.on('change:state', @render)
|
||||
|
||||
render: =>
|
||||
@$el.toggleClass("#{@model.previous('state')}-item #{@model.get('state')}-item")
|
||||
render: =>
|
||||
@$el.toggleClass("#{@model.previous('state')}-item #{@model.get('state')}-item")
|
||||
|
||||
class CMS.Views.UnitEdit.Visibility extends Backbone.View
|
||||
initialize: =>
|
||||
@model.on('change:state', @render)
|
||||
@render()
|
||||
class UnitEditView.Visibility extends Backbone.View
|
||||
initialize: =>
|
||||
@model.on('change:state', @render)
|
||||
@render()
|
||||
|
||||
render: =>
|
||||
@$el.val(@model.get('state'))
|
||||
render: =>
|
||||
@$el.val(@model.get('state'))
|
||||
|
||||
return UnitEditView
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
if (!window.CmsUtils) window.CmsUtils = {};
|
||||
require(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js/views/feedback_prompt",
|
||||
"jquery.ui", "jquery.timepicker", "jquery.leanModal", "jquery.form"],
|
||||
function($, _, gettext, NotificationView, PromptView) {
|
||||
|
||||
var $body;
|
||||
var $modal;
|
||||
@@ -13,13 +15,8 @@ var $newComponentButton;
|
||||
$(document).ready(function() {
|
||||
$body = $('body');
|
||||
$modal = $('.history-modal');
|
||||
$modalCover = $('<div class="modal-cover">');
|
||||
// cdodge: this looks funny, but on AWS instances, this base.js get's wrapped in a separate scope as part of Django static
|
||||
// pipelining (note, this doesn't happen on local runtimes). So if we set it on window, when we can access it from other
|
||||
// scopes (namely the course-info tab)
|
||||
window.$modalCover = $modalCover;
|
||||
$modalCover = $('.modal-cover');
|
||||
|
||||
$body.append($modalCover);
|
||||
$newComponentItem = $('.new-component-item');
|
||||
$newComponentTypePicker = $('.new-component');
|
||||
$newComponentTemplatePickers = $('.new-component-templates');
|
||||
@@ -95,7 +92,7 @@ $(document).ready(function() {
|
||||
$('a[rel*="view"][href^="#"]').bind('click', smoothScrollLink);
|
||||
|
||||
// tender feedback window scrolling
|
||||
$('a.show-tender').bind('click', window.CmsUtils.smoothScrollTop);
|
||||
$('a.show-tender').bind('click', smoothScrollTop);
|
||||
|
||||
// toggling footer additional support
|
||||
$('.cta-show-sock').bind('click', toggleSock);
|
||||
@@ -169,10 +166,7 @@ function smoothScrollLink(e) {
|
||||
});
|
||||
}
|
||||
|
||||
// On AWS instances, this base.js gets wrapped in a separate scope as part of Django static
|
||||
// pipelining (note, this doesn't happen on local runtimes). So if we set it on window,
|
||||
// when we can access it from other scopes (namely Course Advanced Settings).
|
||||
window.CmsUtils.smoothScrollTop = function(e) {
|
||||
function smoothScrollTop(e) {
|
||||
(e).preventDefault();
|
||||
|
||||
$.smoothScroll({
|
||||
@@ -189,11 +183,6 @@ function linkNewWindow(e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// On AWS instances, base.js gets wrapped in a separate scope as part of Django static
|
||||
// pipelining (note, this doesn't happen on local runtimes). So if we set it on window,
|
||||
// when we can access it from other scopes (namely the checklists)
|
||||
window.cmsLinkNewWindow = linkNewWindow;
|
||||
|
||||
function toggleSections(e) {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -378,7 +367,7 @@ function deleteSection(e) {
|
||||
}
|
||||
|
||||
function _deleteItem($el, type) {
|
||||
var confirm = new CMS.Views.Prompt.Warning({
|
||||
var confirm = new PromptView.Warning({
|
||||
title: gettext('Delete this ' + type + '?'),
|
||||
message: gettext('Deleting this ' + type + ' is permanent and cannot be undone.'),
|
||||
actions: {
|
||||
@@ -394,7 +383,7 @@ function _deleteItem($el, type) {
|
||||
'id': id
|
||||
});
|
||||
|
||||
var deleting = new CMS.Views.Notification.Mini({
|
||||
var deleting = new NotificationView.Mini({
|
||||
title: gettext('Deleting…')
|
||||
});
|
||||
deleting.show();
|
||||
@@ -429,7 +418,7 @@ function hideModal(e) {
|
||||
// of the editor. Users must press Cancel or Save to exit the editor.
|
||||
// module_edit adds and removes the "is-fixed" class.
|
||||
if (!$modalCover.hasClass("is-fixed")) {
|
||||
$modal.hide();
|
||||
$(".modal, .edit-subsection-publish-settings").hide();
|
||||
$modalCover.hide();
|
||||
}
|
||||
}
|
||||
@@ -833,7 +822,7 @@ function saveSetSectionScheduleDate(e) {
|
||||
'start': datetime
|
||||
});
|
||||
|
||||
var saving = new CMS.Views.Notification.Mini({
|
||||
var saving = new NotificationView.Mini({
|
||||
title: gettext("Saving…")
|
||||
});
|
||||
saving.show();
|
||||
@@ -874,3 +863,5 @@ function saveSetSectionScheduleDate(e) {
|
||||
saving.hide();
|
||||
});
|
||||
}
|
||||
|
||||
}); // end require()
|
||||
|
||||
6
cms/static/js/collections/asset.js
Normal file
6
cms/static/js/collections/asset.js
Normal file
@@ -0,0 +1,6 @@
|
||||
define(["backbone", "js/models/asset"], function(Backbone, AssetModel){
|
||||
var AssetCollection = Backbone.Collection.extend({
|
||||
model : AssetModel
|
||||
});
|
||||
return AssetCollection;
|
||||
});
|
||||
14
cms/static/js/collections/chapter.js
Normal file
14
cms/static/js/collections/chapter.js
Normal file
@@ -0,0 +1,14 @@
|
||||
define(["backbone", "js/models/chapter"], function(Backbone, ChapterModel) {
|
||||
var ChapterCollection = Backbone.Collection.extend({
|
||||
model: ChapterModel,
|
||||
comparator: "order",
|
||||
nextOrder: function() {
|
||||
if(!this.length) return 1;
|
||||
return this.last().get('order') + 1;
|
||||
},
|
||||
isEmpty: function() {
|
||||
return this.length === 0 || this.every(function(m) { return m.isEmpty(); });
|
||||
}
|
||||
});
|
||||
return ChapterCollection;
|
||||
});
|
||||
23
cms/static/js/collections/checklist.js
Normal file
23
cms/static/js/collections/checklist.js
Normal file
@@ -0,0 +1,23 @@
|
||||
define(["backbone", "underscore", "js/models/checklist"],
|
||||
function(Backbone, _, ChecklistModel) {
|
||||
var ChecklistCollection = Backbone.Collection.extend({
|
||||
model : ChecklistModel,
|
||||
|
||||
parse: function(response) {
|
||||
_.each(response,
|
||||
function( element, idx ) {
|
||||
element.id = idx;
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
// Disable caching so the browser back button will work (checklists have links to other
|
||||
// places within Studio).
|
||||
fetch: function (options) {
|
||||
options.cache = false;
|
||||
return Backbone.Collection.prototype.fetch.call(this, options);
|
||||
}
|
||||
});
|
||||
return ChecklistCollection;
|
||||
});
|
||||
15
cms/static/js/collections/course_grader.js
Normal file
15
cms/static/js/collections/course_grader.js
Normal file
@@ -0,0 +1,15 @@
|
||||
define(["backbone", "js/models/settings/course_grader"], function(Backbone, CourseGrader) {
|
||||
|
||||
var CourseGraderCollection = Backbone.Collection.extend({
|
||||
model : CourseGrader,
|
||||
course_location : null, // must be set to a Location object
|
||||
url : function() {
|
||||
return '/' + this.course_location.get('org') + "/" + this.course_location.get('course') + '/settings-grading/' + this.course_location.get('name') + '/';
|
||||
},
|
||||
sumWeights : function() {
|
||||
return this.reduce(function(subtotal, grader) { return subtotal + grader.get('weight'); }, 0);
|
||||
}
|
||||
});
|
||||
|
||||
return CourseGraderCollection;
|
||||
}); // end define()
|
||||
6
cms/static/js/collections/course_relative.js
Normal file
6
cms/static/js/collections/course_relative.js
Normal file
@@ -0,0 +1,6 @@
|
||||
define(["backbone", "js/models/course_relative"], function(Backbone, CourseRelativeModel) {
|
||||
var CourseRelativeCollection = Backbone.Collection.extend({
|
||||
model: CourseRelativeModel
|
||||
});
|
||||
return CourseRelativeCollection;
|
||||
});
|
||||
12
cms/static/js/collections/course_update.js
Normal file
12
cms/static/js/collections/course_update.js
Normal file
@@ -0,0 +1,12 @@
|
||||
define(["backbone", "js/models/course_update"], function(Backbone, CourseUpdateModel) {
|
||||
/*
|
||||
The intitializer of this collection must set id to the update's location.url and courseLocation to the course's location. Must pass the
|
||||
collection of updates as [{ date : "month day", content : "html"}]
|
||||
*/
|
||||
var CourseUpdateCollection = Backbone.Collection.extend({
|
||||
url : function() {return this.urlbase + "course_info/updates/";},
|
||||
|
||||
model : CourseUpdateModel
|
||||
});
|
||||
return CourseUpdateCollection;
|
||||
});
|
||||
7
cms/static/js/collections/metadata.js
Normal file
7
cms/static/js/collections/metadata.js
Normal file
@@ -0,0 +1,7 @@
|
||||
define(["backbone", "js/models/metadata"], function(Backbone, MetadataModel) {
|
||||
var MetadataCollection = Backbone.Collection.extend({
|
||||
model : MetadataModel,
|
||||
comparator: "display_name"
|
||||
});
|
||||
return MetadataCollection;
|
||||
});
|
||||
11
cms/static/js/collections/textbook.js
Normal file
11
cms/static/js/collections/textbook.js
Normal file
@@ -0,0 +1,11 @@
|
||||
define(["backbone", "js/models/textbook"],
|
||||
function(Backbone, TextbookModel) {
|
||||
var TextbookCollection = Backbone.Collection.extend({
|
||||
model: TextbookModel,
|
||||
url: function() { return CMS.URL.TEXTBOOKS; },
|
||||
save: function(options) {
|
||||
return this.sync('update', this, options);
|
||||
}
|
||||
});
|
||||
return TextbookCollection;
|
||||
});
|
||||
@@ -12,37 +12,41 @@
|
||||
* NOTE: if something outside of this wants to cancel the event, invoke cachedhesitation.untrigger(null | anything);
|
||||
*/
|
||||
|
||||
CMS.HesitateEvent = function(executeOnTimeOut, cancelSelector, onlyOnce) {
|
||||
this.executeOnTimeOut = executeOnTimeOut;
|
||||
this.cancelSelector = cancelSelector;
|
||||
this.timeoutEventId = null;
|
||||
this.originalEvent = null;
|
||||
this.onlyOnce = (onlyOnce === true);
|
||||
};
|
||||
define(["jquery"], function($) {
|
||||
var HesitateEvent = function(executeOnTimeOut, cancelSelector, onlyOnce) {
|
||||
this.executeOnTimeOut = executeOnTimeOut;
|
||||
this.cancelSelector = cancelSelector;
|
||||
this.timeoutEventId = null;
|
||||
this.originalEvent = null;
|
||||
this.onlyOnce = (onlyOnce === true);
|
||||
};
|
||||
|
||||
CMS.HesitateEvent.DURATION = 800;
|
||||
HesitateEvent.DURATION = 800;
|
||||
|
||||
CMS.HesitateEvent.prototype.trigger = function(event) {
|
||||
if (event.data.timeoutEventId == null) {
|
||||
event.data.timeoutEventId = window.setTimeout(
|
||||
function() { event.data.fireEvent(event); },
|
||||
CMS.HesitateEvent.DURATION);
|
||||
event.data.originalEvent = event;
|
||||
$(event.data.originalEvent.delegateTarget).on(event.data.cancelSelector, event.data, event.data.untrigger);
|
||||
}
|
||||
};
|
||||
HesitateEvent.prototype.trigger = function(event) {
|
||||
if (event.data.timeoutEventId == null) {
|
||||
event.data.timeoutEventId = window.setTimeout(
|
||||
function() { event.data.fireEvent(event); },
|
||||
HesitateEvent.DURATION);
|
||||
event.data.originalEvent = event;
|
||||
$(event.data.originalEvent.delegateTarget).on(event.data.cancelSelector, event.data, event.data.untrigger);
|
||||
}
|
||||
};
|
||||
|
||||
CMS.HesitateEvent.prototype.fireEvent = function(event) {
|
||||
event.data.timeoutEventId = null;
|
||||
$(event.data.originalEvent.delegateTarget).off(event.data.cancelSelector, event.data.untrigger);
|
||||
if (event.data.onlyOnce) $(event.data.originalEvent.delegateTarget).off(event.data.originalEvent.type, event.data.trigger);
|
||||
event.data.executeOnTimeOut(event.data.originalEvent);
|
||||
};
|
||||
HesitateEvent.prototype.fireEvent = function(event) {
|
||||
event.data.timeoutEventId = null;
|
||||
$(event.data.originalEvent.delegateTarget).off(event.data.cancelSelector, event.data.untrigger);
|
||||
if (event.data.onlyOnce) $(event.data.originalEvent.delegateTarget).off(event.data.originalEvent.type, event.data.trigger);
|
||||
event.data.executeOnTimeOut(event.data.originalEvent);
|
||||
};
|
||||
|
||||
CMS.HesitateEvent.prototype.untrigger = function(event) {
|
||||
if (event.data.timeoutEventId) {
|
||||
window.clearTimeout(event.data.timeoutEventId);
|
||||
$(event.data.originalEvent.delegateTarget).off(event.data.cancelSelector, event.data.untrigger);
|
||||
}
|
||||
event.data.timeoutEventId = null;
|
||||
};
|
||||
HesitateEvent.prototype.untrigger = function(event) {
|
||||
if (event.data.timeoutEventId) {
|
||||
window.clearTimeout(event.data.timeoutEventId);
|
||||
$(event.data.originalEvent.delegateTarget).off(event.data.cancelSelector, event.data.untrigger);
|
||||
}
|
||||
event.data.timeoutEventId = null;
|
||||
};
|
||||
|
||||
return HesitateEvent;
|
||||
});
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
/**
|
||||
* Simple model for an asset.
|
||||
*/
|
||||
CMS.Models.Asset = Backbone.Model.extend({
|
||||
define(["backbone"], function(Backbone) {
|
||||
/**
|
||||
* Simple model for an asset.
|
||||
*/
|
||||
var Asset = Backbone.Model.extend({
|
||||
defaults: {
|
||||
display_name: "",
|
||||
thumbnail: "",
|
||||
date_added: "",
|
||||
url: "",
|
||||
portable_url: "",
|
||||
locked: false
|
||||
display_name: "",
|
||||
thumbnail: "",
|
||||
date_added: "",
|
||||
url: "",
|
||||
portable_url: "",
|
||||
locked: false
|
||||
}
|
||||
});
|
||||
return Asset;
|
||||
});
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
CMS.Models.AssetCollection = Backbone.Collection.extend({
|
||||
model : CMS.Models.Asset
|
||||
});
|
||||
27
cms/static/js/models/assignment_grade.js
Normal file
27
cms/static/js/models/assignment_grade.js
Normal file
@@ -0,0 +1,27 @@
|
||||
define(["backbone", "underscore", "js/models/location"], function(Backbone, _, Location) {
|
||||
var AssignmentGrade = Backbone.Model.extend({
|
||||
defaults : {
|
||||
graderType : null, // the type label (string). May be "Not Graded" which implies None. I'd like to use id but that's ephemeral
|
||||
location : null // A location object
|
||||
},
|
||||
initialize : function(attrs) {
|
||||
if (attrs['assignmentUrl']) {
|
||||
this.set('location', new Location(attrs['assignmentUrl'], {parse: true}));
|
||||
}
|
||||
},
|
||||
parse : function(attrs) {
|
||||
if (attrs && attrs['location']) {
|
||||
attrs.location = new Location(attrs['location'], {parse: true});
|
||||
}
|
||||
},
|
||||
urlRoot : function() {
|
||||
if (this.has('location')) {
|
||||
var location = this.get('location');
|
||||
return '/' + location.get('org') + "/" + location.get('course') + '/' + location.get('category') + '/'
|
||||
+ location.get('name') + '/gradeas/';
|
||||
}
|
||||
else return "";
|
||||
}
|
||||
});
|
||||
return AssignmentGrade;
|
||||
});
|
||||
52
cms/static/js/models/chapter.js
Normal file
52
cms/static/js/models/chapter.js
Normal file
@@ -0,0 +1,52 @@
|
||||
define(["backbone", "backbone.associations"], function(Backbone) {
|
||||
var Chapter = Backbone.AssociatedModel.extend({
|
||||
defaults: function() {
|
||||
return {
|
||||
name: "",
|
||||
asset_path: "",
|
||||
order: this.collection ? this.collection.nextOrder() : 1
|
||||
};
|
||||
},
|
||||
isEmpty: function() {
|
||||
return !this.get('name') && !this.get('asset_path');
|
||||
},
|
||||
parse: function(response) {
|
||||
if("title" in response && !("name" in response)) {
|
||||
response.name = response.title;
|
||||
delete response.title;
|
||||
}
|
||||
if("url" in response && !("asset_path" in response)) {
|
||||
response.asset_path = response.url;
|
||||
delete response.url;
|
||||
}
|
||||
return response;
|
||||
},
|
||||
toJSON: function() {
|
||||
return {
|
||||
title: this.get('name'),
|
||||
url: this.get('asset_path')
|
||||
};
|
||||
},
|
||||
// NOTE: validation functions should return non-internationalized error
|
||||
// messages. The messages will be passed through gettext in the template.
|
||||
validate: function(attrs, options) {
|
||||
if(!attrs.name && !attrs.asset_path) {
|
||||
return {
|
||||
message: "Chapter name and asset_path are both required",
|
||||
attributes: {name: true, asset_path: true}
|
||||
};
|
||||
} else if(!attrs.name) {
|
||||
return {
|
||||
message: "Chapter name is required",
|
||||
attributes: {name: true}
|
||||
};
|
||||
} else if (!attrs.asset_path) {
|
||||
return {
|
||||
message: "asset_path is required",
|
||||
attributes: {asset_path: true}
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
return Chapter;
|
||||
});
|
||||
5
cms/static/js/models/checklist.js
Normal file
5
cms/static/js/models/checklist.js
Normal file
@@ -0,0 +1,5 @@
|
||||
define(["backbone"], function(Backbone) {
|
||||
var Checklist = Backbone.Model.extend({
|
||||
});
|
||||
return Checklist;
|
||||
});
|
||||
@@ -1,24 +0,0 @@
|
||||
// Model for checklists_view.js.
|
||||
CMS.Models.Checklist = Backbone.Model.extend({
|
||||
});
|
||||
|
||||
CMS.Models.ChecklistCollection = Backbone.Collection.extend({
|
||||
model : CMS.Models.Checklist,
|
||||
|
||||
parse: function(response) {
|
||||
_.each(response,
|
||||
function( element, idx ) {
|
||||
element.id = idx;
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
// Disable caching so the browser back button will work (checklists have links to other
|
||||
// places within Studio).
|
||||
fetch: function (options) {
|
||||
options.cache = false;
|
||||
return Backbone.Collection.prototype.fetch.call(this, options);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
CMS.Models.Course = Backbone.Model.extend({
|
||||
defaults: {
|
||||
"name": ""
|
||||
},
|
||||
validate: function(attrs, options) {
|
||||
if (!attrs.name) {
|
||||
return gettext("You must specify a name");
|
||||
define(['backbone'], function(Backbone){
|
||||
var Course = Backbone.Model.extend({
|
||||
defaults: {
|
||||
"name": ""
|
||||
},
|
||||
validate: function(attrs, options) {
|
||||
if (!attrs.name) {
|
||||
return gettext("You must specify a name");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return Course;
|
||||
});
|
||||
|
||||
@@ -1,36 +1,16 @@
|
||||
// single per course holds the updates and handouts
|
||||
CMS.Models.CourseInfo = Backbone.Model.extend({
|
||||
// This model class is not suited for restful operations and is considered just a server side initialized container
|
||||
url: '',
|
||||
define(["backbone"], function(Backbone) {
|
||||
// single per course holds the updates and handouts
|
||||
var CourseInfo = Backbone.Model.extend({
|
||||
// This model class is not suited for restful operations and is considered just a server side initialized container
|
||||
url: '',
|
||||
|
||||
defaults: {
|
||||
"courseId": "", // the location url
|
||||
"updates" : null, // UpdateCollection
|
||||
"handouts": null // HandoutCollection
|
||||
},
|
||||
defaults: {
|
||||
"courseId": "", // the location url
|
||||
"updates" : null, // UpdateCollection
|
||||
"handouts": null // HandoutCollection
|
||||
},
|
||||
|
||||
idAttribute : "courseId"
|
||||
idAttribute : "courseId"
|
||||
});
|
||||
return CourseInfo;
|
||||
});
|
||||
|
||||
// course update -- biggest kludge here is the lack of a real id to map updates to originals
|
||||
CMS.Models.CourseUpdate = Backbone.Model.extend({
|
||||
defaults: {
|
||||
"date" : $.datepicker.formatDate('MM d, yy', new Date()),
|
||||
"content" : ""
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
The intitializer of this collection must set id to the update's location.url and courseLocation to the course's location. Must pass the
|
||||
collection of updates as [{ date : "month day", content : "html"}]
|
||||
*/
|
||||
CMS.Models.CourseUpdateCollection = Backbone.Collection.extend({
|
||||
url : function() {return this.urlbase + "course_info/updates/";},
|
||||
|
||||
model : CMS.Models.CourseUpdate
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,68 +1,9 @@
|
||||
CMS.Models.Location = Backbone.Model.extend({
|
||||
defaults: {
|
||||
tag: "",
|
||||
org: "",
|
||||
course: "",
|
||||
category: "",
|
||||
name: ""
|
||||
},
|
||||
toUrl: function(overrides) {
|
||||
return
|
||||
(overrides && overrides['tag'] ? overrides['tag'] : this.get('tag')) + "://" +
|
||||
(overrides && overrides['org'] ? overrides['org'] : this.get('org')) + "/" +
|
||||
(overrides && overrides['course'] ? overrides['course'] : this.get('course')) + "/" +
|
||||
(overrides && overrides['category'] ? overrides['category'] : this.get('category')) + "/" +
|
||||
(overrides && overrides['name'] ? overrides['name'] : this.get('name')) + "/";
|
||||
},
|
||||
_tagPattern : /[^:]+/g,
|
||||
_fieldPattern : new RegExp('[^/]+','g'),
|
||||
|
||||
parse: function(payload) {
|
||||
if (_.isArray(payload)) {
|
||||
return {
|
||||
tag: payload[0],
|
||||
org: payload[1],
|
||||
course: payload[2],
|
||||
category: payload[3],
|
||||
name: payload[4]
|
||||
};
|
||||
}
|
||||
else if (_.isString(payload)) {
|
||||
this._tagPattern.lastIndex = 0; // odd regex behavior requires this to be reset sometimes
|
||||
var foundTag = this._tagPattern.exec(payload);
|
||||
if (foundTag) {
|
||||
this._fieldPattern.lastIndex = this._tagPattern.lastIndex + 1; // skip over the colon
|
||||
return {
|
||||
tag: foundTag[0],
|
||||
org: this.getNextField(payload),
|
||||
course: this.getNextField(payload),
|
||||
category: this.getNextField(payload),
|
||||
name: this.getNextField(payload)
|
||||
}
|
||||
}
|
||||
else return null;
|
||||
}
|
||||
else {
|
||||
return payload;
|
||||
}
|
||||
},
|
||||
getNextField : function(payload) {
|
||||
try {
|
||||
return this._fieldPattern.exec(payload)[0];
|
||||
}
|
||||
catch (err) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
CMS.Models.CourseRelative = Backbone.Model.extend({
|
||||
defaults: {
|
||||
course_location : null, // must never be null, but here to doc the field
|
||||
idx : null // the index making it unique in the containing collection (no implied sort)
|
||||
}
|
||||
});
|
||||
|
||||
CMS.Models.CourseRelativeCollection = Backbone.Collection.extend({
|
||||
model : CMS.Models.CourseRelative
|
||||
define(["backbone"], function(Backbone) {
|
||||
var CourseRelative = Backbone.Model.extend({
|
||||
defaults: {
|
||||
course_location : null, // must never be null, but here to doc the field
|
||||
idx : null // the index making it unique in the containing collection (no implied sort)
|
||||
}
|
||||
});
|
||||
return CourseRelative;
|
||||
});
|
||||
|
||||
10
cms/static/js/models/course_update.js
Normal file
10
cms/static/js/models/course_update.js
Normal file
@@ -0,0 +1,10 @@
|
||||
define(["backbone", "jquery", "jquery.ui"], function(Backbone, $) {
|
||||
// course update -- biggest kludge here is the lack of a real id to map updates to originals
|
||||
var CourseUpdate = Backbone.Model.extend({
|
||||
defaults: {
|
||||
"date" : $.datepicker.formatDate('MM d, yy', new Date()),
|
||||
"content" : ""
|
||||
}
|
||||
});
|
||||
return CourseUpdate;
|
||||
}); // end define()
|
||||
60
cms/static/js/models/location.js
Normal file
60
cms/static/js/models/location.js
Normal file
@@ -0,0 +1,60 @@
|
||||
define(["backbone", "underscore"], function(Backbone, _) {
|
||||
var Location = Backbone.Model.extend({
|
||||
defaults: {
|
||||
tag: "",
|
||||
org: "",
|
||||
course: "",
|
||||
category: "",
|
||||
name: ""
|
||||
},
|
||||
toUrl: function(overrides) {
|
||||
return
|
||||
(overrides && overrides['tag'] ? overrides['tag'] : this.get('tag')) + "://" +
|
||||
(overrides && overrides['org'] ? overrides['org'] : this.get('org')) + "/" +
|
||||
(overrides && overrides['course'] ? overrides['course'] : this.get('course')) + "/" +
|
||||
(overrides && overrides['category'] ? overrides['category'] : this.get('category')) + "/" +
|
||||
(overrides && overrides['name'] ? overrides['name'] : this.get('name')) + "/";
|
||||
},
|
||||
_tagPattern : /[^:]+/g,
|
||||
_fieldPattern : new RegExp('[^/]+','g'),
|
||||
|
||||
parse: function(payload) {
|
||||
if (_.isArray(payload)) {
|
||||
return {
|
||||
tag: payload[0],
|
||||
org: payload[1],
|
||||
course: payload[2],
|
||||
category: payload[3],
|
||||
name: payload[4]
|
||||
};
|
||||
}
|
||||
else if (_.isString(payload)) {
|
||||
this._tagPattern.lastIndex = 0; // odd regex behavior requires this to be reset sometimes
|
||||
var foundTag = this._tagPattern.exec(payload);
|
||||
if (foundTag) {
|
||||
this._fieldPattern.lastIndex = this._tagPattern.lastIndex + 1; // skip over the colon
|
||||
return {
|
||||
tag: foundTag[0],
|
||||
org: this.getNextField(payload),
|
||||
course: this.getNextField(payload),
|
||||
category: this.getNextField(payload),
|
||||
name: this.getNextField(payload)
|
||||
};
|
||||
}
|
||||
else return null;
|
||||
}
|
||||
else {
|
||||
return payload;
|
||||
}
|
||||
},
|
||||
getNextField : function(payload) {
|
||||
try {
|
||||
return this._fieldPattern.exec(payload)[0];
|
||||
}
|
||||
catch (err) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
});
|
||||
return Location;
|
||||
});
|
||||
112
cms/static/js/models/metadata.js
Normal file
112
cms/static/js/models/metadata.js
Normal file
@@ -0,0 +1,112 @@
|
||||
define(["backbone"], function(Backbone) {
|
||||
/**
|
||||
* Model used for metadata setting editors. This model does not do its own saving,
|
||||
* as that is done by module_edit.coffee.
|
||||
*/
|
||||
var Metadata = Backbone.Model.extend({
|
||||
defaults: {
|
||||
"field_name": null,
|
||||
"display_name": null,
|
||||
"value" : null,
|
||||
"explicitly_set": null,
|
||||
"default_value" : null,
|
||||
"options" : null,
|
||||
"type" : null
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
this.original_value = this.get('value');
|
||||
this.original_explicitly_set = this.get('explicitly_set');
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if the stored value is different, or if the "explicitly_set"
|
||||
* property has changed.
|
||||
*/
|
||||
isModified : function() {
|
||||
if (!this.get('explicitly_set') && !this.original_explicitly_set) {
|
||||
return false;
|
||||
}
|
||||
if (this.get('explicitly_set') && this.original_explicitly_set) {
|
||||
return this.get('value') !== this.original_value;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if a non-default/non-inherited value has been set.
|
||||
*/
|
||||
isExplicitlySet: function() {
|
||||
return this.get('explicitly_set');
|
||||
},
|
||||
|
||||
/**
|
||||
* The value, as shown in the UI. This may be an inherited or default value.
|
||||
*/
|
||||
getDisplayValue : function () {
|
||||
return this.get('value');
|
||||
},
|
||||
|
||||
/**
|
||||
* The value, as should be returned to the server. if 'isExplicitlySet'
|
||||
* returns false, this method returns null to indicate that the value
|
||||
* is not set at this level.
|
||||
*/
|
||||
getValue: function() {
|
||||
return this.get('explicitly_set') ? this.get('value') : null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the displayed value.
|
||||
*/
|
||||
setValue: function (value) {
|
||||
this.set({
|
||||
explicitly_set: true,
|
||||
value: value
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the field name, which should be used for persisting the metadata
|
||||
* field to the server.
|
||||
*/
|
||||
getFieldName: function () {
|
||||
return this.get('field_name');
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the options. This may be a array of possible values, or an object
|
||||
* with properties like "max", "min" and "step".
|
||||
*/
|
||||
getOptions: function () {
|
||||
return this.get('options');
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the type of this metadata field. Possible values are SELECT_TYPE,
|
||||
* INTEGER_TYPE, and FLOAT_TYPE, GENERIC_TYPE.
|
||||
*/
|
||||
getType: function() {
|
||||
return this.get('type');
|
||||
},
|
||||
|
||||
/**
|
||||
* Reverts the value to the default_value specified at construction, and updates the
|
||||
* explicitly_set property.
|
||||
*/
|
||||
clear: function() {
|
||||
this.set({
|
||||
explicitly_set: false,
|
||||
value: this.get('default_value')
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Metadata.SELECT_TYPE = "Select";
|
||||
Metadata.INTEGER_TYPE = "Integer";
|
||||
Metadata.FLOAT_TYPE = "Float";
|
||||
Metadata.GENERIC_TYPE = "Generic";
|
||||
Metadata.LIST_TYPE = "List";
|
||||
|
||||
return Metadata;
|
||||
});
|
||||
@@ -1,114 +0,0 @@
|
||||
/**
|
||||
* Model used for metadata setting editors. This model does not do its own saving,
|
||||
* as that is done by module_edit.coffee.
|
||||
*/
|
||||
CMS.Models.Metadata = Backbone.Model.extend({
|
||||
|
||||
defaults: {
|
||||
"field_name": null,
|
||||
"display_name": null,
|
||||
"value" : null,
|
||||
"explicitly_set": null,
|
||||
"default_value" : null,
|
||||
"options" : null,
|
||||
"type" : null
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
this.original_value = this.get('value');
|
||||
this.original_explicitly_set = this.get('explicitly_set');
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if the stored value is different, or if the "explicitly_set"
|
||||
* property has changed.
|
||||
*/
|
||||
isModified : function() {
|
||||
if (!this.get('explicitly_set') && !this.original_explicitly_set) {
|
||||
return false;
|
||||
}
|
||||
if (this.get('explicitly_set') && this.original_explicitly_set) {
|
||||
return this.get('value') !== this.original_value;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if a non-default/non-inherited value has been set.
|
||||
*/
|
||||
isExplicitlySet: function() {
|
||||
return this.get('explicitly_set');
|
||||
},
|
||||
|
||||
/**
|
||||
* The value, as shown in the UI. This may be an inherited or default value.
|
||||
*/
|
||||
getDisplayValue : function () {
|
||||
return this.get('value');
|
||||
},
|
||||
|
||||
/**
|
||||
* The value, as should be returned to the server. if 'isExplicitlySet'
|
||||
* returns false, this method returns null to indicate that the value
|
||||
* is not set at this level.
|
||||
*/
|
||||
getValue: function() {
|
||||
return this.get('explicitly_set') ? this.get('value') : null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the displayed value.
|
||||
*/
|
||||
setValue: function (value) {
|
||||
this.set({
|
||||
explicitly_set: true,
|
||||
value: value
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the field name, which should be used for persisting the metadata
|
||||
* field to the server.
|
||||
*/
|
||||
getFieldName: function () {
|
||||
return this.get('field_name');
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the options. This may be a array of possible values, or an object
|
||||
* with properties like "max", "min" and "step".
|
||||
*/
|
||||
getOptions: function () {
|
||||
return this.get('options');
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the type of this metadata field. Possible values are SELECT_TYPE,
|
||||
* INTEGER_TYPE, and FLOAT_TYPE, GENERIC_TYPE.
|
||||
*/
|
||||
getType: function() {
|
||||
return this.get('type');
|
||||
},
|
||||
|
||||
/**
|
||||
* Reverts the value to the default_value specified at construction, and updates the
|
||||
* explicitly_set property.
|
||||
*/
|
||||
clear: function() {
|
||||
this.set({
|
||||
explicitly_set: false,
|
||||
value: this.get('default_value')
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
CMS.Models.MetadataCollection = Backbone.Collection.extend({
|
||||
model : CMS.Models.Metadata,
|
||||
comparator: "display_name"
|
||||
});
|
||||
|
||||
CMS.Models.Metadata.SELECT_TYPE = "Select";
|
||||
CMS.Models.Metadata.INTEGER_TYPE = "Integer";
|
||||
CMS.Models.Metadata.FLOAT_TYPE = "Float";
|
||||
CMS.Models.Metadata.GENERIC_TYPE = "Generic";
|
||||
CMS.Models.Metadata.LIST_TYPE = "List";
|
||||
@@ -1,10 +1,13 @@
|
||||
CMS.Models.ModuleInfo = Backbone.Model.extend({
|
||||
url: function() {return "/module_info/" + this.id;},
|
||||
define(["backbone"], function(Backbone) {
|
||||
var ModuleInfo = Backbone.Model.extend({
|
||||
url: function() {return "/module_info/" + this.id;},
|
||||
|
||||
defaults: {
|
||||
"id": null,
|
||||
"data": null,
|
||||
"metadata" : null,
|
||||
"children" : null
|
||||
}
|
||||
defaults: {
|
||||
"id": null,
|
||||
"data": null,
|
||||
"metadata" : null,
|
||||
"children" : null
|
||||
}
|
||||
});
|
||||
return ModuleInfo;
|
||||
});
|
||||
|
||||
@@ -1,35 +1,38 @@
|
||||
CMS.Models.Section = Backbone.Model.extend({
|
||||
defaults: {
|
||||
"name": ""
|
||||
},
|
||||
validate: function(attrs, options) {
|
||||
if (!attrs.name) {
|
||||
return gettext("You must specify a name");
|
||||
}
|
||||
},
|
||||
url: "/save_item",
|
||||
toJSON: function() {
|
||||
return {
|
||||
id: this.get("id"),
|
||||
metadata: {
|
||||
display_name: this.get("name")
|
||||
define(["backbone", "gettext", "js/views/feedback_notification"], function(Backbone, gettext, NotificationView) {
|
||||
var Section = Backbone.Model.extend({
|
||||
defaults: {
|
||||
"name": ""
|
||||
},
|
||||
validate: function(attrs, options) {
|
||||
if (!attrs.name) {
|
||||
return gettext("You must specify a name");
|
||||
}
|
||||
};
|
||||
},
|
||||
initialize: function() {
|
||||
this.listenTo(this, "request", this.showNotification);
|
||||
this.listenTo(this, "sync", this.hideNotification);
|
||||
},
|
||||
showNotification: function() {
|
||||
if(!this.msg) {
|
||||
this.msg = new CMS.Views.Notification.Mini({
|
||||
title: gettext("Saving…")
|
||||
});
|
||||
},
|
||||
url: "/save_item",
|
||||
toJSON: function() {
|
||||
return {
|
||||
id: this.get("id"),
|
||||
metadata: {
|
||||
display_name: this.get("name")
|
||||
}
|
||||
};
|
||||
},
|
||||
initialize: function() {
|
||||
this.listenTo(this, "request", this.showNotification);
|
||||
this.listenTo(this, "sync", this.hideNotification);
|
||||
},
|
||||
showNotification: function() {
|
||||
if(!this.msg) {
|
||||
this.msg = new NotificationView.Mini({
|
||||
title: gettext("Saving…")
|
||||
});
|
||||
}
|
||||
this.msg.show();
|
||||
},
|
||||
hideNotification: function() {
|
||||
if(!this.msg) { return; }
|
||||
this.msg.hide();
|
||||
}
|
||||
this.msg.show();
|
||||
},
|
||||
hideNotification: function() {
|
||||
if(!this.msg) { return; }
|
||||
this.msg.hide();
|
||||
}
|
||||
});
|
||||
return Section;
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
if (!CMS.Models['Settings']) CMS.Models.Settings = {};
|
||||
define(["backbone"], function(Backbone) {
|
||||
|
||||
CMS.Models.Settings.Advanced = Backbone.Model.extend({
|
||||
var Advanced = Backbone.Model.extend({
|
||||
|
||||
defaults: {
|
||||
// the properties are whatever the user types in (in addition to whatever comes originally from the server)
|
||||
@@ -21,3 +21,6 @@ CMS.Models.Settings.Advanced = Backbone.Model.extend({
|
||||
Backbone.Model.prototype.save.call(this, attrs, options);
|
||||
}
|
||||
});
|
||||
|
||||
return Advanced;
|
||||
}); // end define()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
if (!CMS.Models['Settings']) CMS.Models.Settings = new Object();
|
||||
define(["backbone", "underscore", "gettext", "js/models/location"], function(Backbone, _, gettext, Location) {
|
||||
|
||||
CMS.Models.Settings.CourseDetails = Backbone.Model.extend({
|
||||
var CourseDetails = Backbone.Model.extend({
|
||||
defaults: {
|
||||
location : null, // the course's Location model, required
|
||||
start_date: null, // maps to 'start'
|
||||
@@ -18,7 +18,7 @@ CMS.Models.Settings.CourseDetails = Backbone.Model.extend({
|
||||
// When init'g from html script, ensure you pass {parse: true} as an option (2nd arg to reset)
|
||||
parse: function(attributes) {
|
||||
if (attributes['course_location']) {
|
||||
attributes.location = new CMS.Models.Location(attributes.course_location, {parse:true});
|
||||
attributes.location = new Location(attributes.course_location, {parse:true});
|
||||
}
|
||||
if (attributes['start_date']) {
|
||||
attributes.start_date = new Date(attributes.start_date);
|
||||
@@ -81,3 +81,7 @@ CMS.Models.Settings.CourseDetails = Backbone.Model.extend({
|
||||
else return "";
|
||||
}
|
||||
});
|
||||
|
||||
return CourseDetails;
|
||||
|
||||
}); // end define()
|
||||
|
||||
74
cms/static/js/models/settings/course_grader.js
Normal file
74
cms/static/js/models/settings/course_grader.js
Normal file
@@ -0,0 +1,74 @@
|
||||
define(["backbone", "underscore", "gettext"], function(Backbone, _, gettext) {
|
||||
|
||||
var CourseGrader = Backbone.Model.extend({
|
||||
defaults: {
|
||||
"type" : "", // must be unique w/in collection (ie. w/in course)
|
||||
"min_count" : 1,
|
||||
"drop_count" : 0,
|
||||
"short_label" : "", // what to use in place of type if space is an issue
|
||||
"weight" : 0 // int 0..100
|
||||
},
|
||||
parse : function(attrs) {
|
||||
if (attrs['weight']) {
|
||||
if (!_.isNumber(attrs.weight)) attrs.weight = parseInt(attrs.weight, 10);
|
||||
}
|
||||
if (attrs['min_count']) {
|
||||
if (!_.isNumber(attrs.min_count)) attrs.min_count = parseInt(attrs.min_count, 10);
|
||||
}
|
||||
if (attrs['drop_count']) {
|
||||
if (!_.isNumber(attrs.drop_count)) attrs.drop_count = parseInt(attrs.drop_count, 10);
|
||||
}
|
||||
return attrs;
|
||||
},
|
||||
validate : function(attrs) {
|
||||
var errors = {};
|
||||
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.cid != this.cid) && (other.get('type') == attrs['type']);}, this);
|
||||
if (existing) {
|
||||
errors.type = gettext("There's already another assignment type with this name.");
|
||||
}
|
||||
}
|
||||
}
|
||||
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 = 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
|
||||
// or figure out a holistic way to balance the vals across the whole
|
||||
// if ((this.collection.sumWeights() + attrs.weight - this.get('weight')) > 100)
|
||||
// errors.weight = "The weights cannot add to more than 100.";
|
||||
}
|
||||
}}
|
||||
if (_.has(attrs, 'min_count')) {
|
||||
if (!isFinite(attrs.min_count) || /\D+/.test(attrs.min_count)) {
|
||||
errors.min_count = gettext("Please enter an integer.");
|
||||
}
|
||||
else attrs.min_count = parseInt(attrs.min_count, 10);
|
||||
}
|
||||
if (_.has(attrs, 'drop_count')) {
|
||||
if (!isFinite(attrs.drop_count) || /\D+/.test(attrs.drop_count)) {
|
||||
errors.drop_count = gettext("Please enter an integer.");
|
||||
}
|
||||
else attrs.drop_count = parseInt(attrs.drop_count, 10);
|
||||
}
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
return CourseGrader;
|
||||
}); // end define()
|
||||
@@ -1,6 +1,7 @@
|
||||
if (!CMS.Models['Settings']) CMS.Models.Settings = new Object();
|
||||
define(["backbone", "js/models/location", "js/collections/course_grader"],
|
||||
function(Backbone, Location, CourseGraderCollection) {
|
||||
|
||||
CMS.Models.Settings.CourseGradingPolicy = Backbone.Model.extend({
|
||||
var CourseGradingPolicy = Backbone.Model.extend({
|
||||
defaults : {
|
||||
course_location : null,
|
||||
graders : null, // CourseGraderCollection
|
||||
@@ -9,7 +10,7 @@ CMS.Models.Settings.CourseGradingPolicy = Backbone.Model.extend({
|
||||
},
|
||||
parse: function(attributes) {
|
||||
if (attributes['course_location']) {
|
||||
attributes.course_location = new CMS.Models.Location(attributes.course_location, {parse:true});
|
||||
attributes.course_location = new Location(attributes.course_location, {parse:true});
|
||||
}
|
||||
if (attributes['graders']) {
|
||||
var graderCollection;
|
||||
@@ -19,7 +20,7 @@ CMS.Models.Settings.CourseGradingPolicy = Backbone.Model.extend({
|
||||
graderCollection.reset(attributes.graders);
|
||||
}
|
||||
else {
|
||||
graderCollection = new CMS.Models.Settings.CourseGraderCollection(attributes.graders);
|
||||
graderCollection = new CourseGraderCollection(attributes.graders);
|
||||
graderCollection.course_location = attributes['course_location'] || this.get('course_location');
|
||||
}
|
||||
attributes.graders = graderCollection;
|
||||
@@ -74,83 +75,5 @@ CMS.Models.Settings.CourseGradingPolicy = Backbone.Model.extend({
|
||||
}
|
||||
});
|
||||
|
||||
CMS.Models.Settings.CourseGrader = Backbone.Model.extend({
|
||||
defaults: {
|
||||
"type" : "", // must be unique w/in collection (ie. w/in course)
|
||||
"min_count" : 1,
|
||||
"drop_count" : 0,
|
||||
"short_label" : "", // what to use in place of type if space is an issue
|
||||
"weight" : 0 // int 0..100
|
||||
},
|
||||
parse : function(attrs) {
|
||||
if (attrs['weight']) {
|
||||
if (!_.isNumber(attrs.weight)) attrs.weight = parseInt(attrs.weight);
|
||||
}
|
||||
if (attrs['min_count']) {
|
||||
if (!_.isNumber(attrs.min_count)) attrs.min_count = parseInt(attrs.min_count);
|
||||
}
|
||||
if (attrs['drop_count']) {
|
||||
if (!_.isNumber(attrs.drop_count)) attrs.drop_count = parseInt(attrs.drop_count);
|
||||
}
|
||||
return attrs;
|
||||
},
|
||||
validate : function(attrs) {
|
||||
var errors = {};
|
||||
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.cid != this.cid) && (other.get('type') == attrs['type']);}, this);
|
||||
if (existing) {
|
||||
errors.type = gettext("There's already another assignment type with this name.");
|
||||
}
|
||||
}
|
||||
}
|
||||
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 = 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
|
||||
// or figure out a wholistic way to balance the vals across the whole
|
||||
// if ((this.collection.sumWeights() + attrs.weight - this.get('weight')) > 100)
|
||||
// errors.weight = "The weights cannot add to more than 100.";
|
||||
}
|
||||
}}
|
||||
if (_.has(attrs, 'min_count')) {
|
||||
if (!isFinite(attrs.min_count) || /\D+/.test(attrs.min_count)) {
|
||||
errors.min_count = gettext("Please enter an integer.");
|
||||
}
|
||||
else attrs.min_count = parseInt(attrs.min_count);
|
||||
}
|
||||
if (_.has(attrs, 'drop_count')) {
|
||||
if (!isFinite(attrs.drop_count) || /\D+/.test(attrs.drop_count)) {
|
||||
errors.drop_count = gettext("Please enter an integer.");
|
||||
}
|
||||
else attrs.drop_count = parseInt(attrs.drop_count);
|
||||
}
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
CMS.Models.Settings.CourseGraderCollection = Backbone.Collection.extend({
|
||||
model : CMS.Models.Settings.CourseGrader,
|
||||
course_location : null, // must be set to a Location object
|
||||
url : function() {
|
||||
return '/' + this.course_location.get('org') + "/" + this.course_location.get('course') + '/settings-grading/' + this.course_location.get('name') + '/';
|
||||
},
|
||||
sumWeights : function() {
|
||||
return this.reduce(function(subtotal, grader) { return subtotal + grader.get('weight'); }, 0);
|
||||
}
|
||||
});
|
||||
return CourseGradingPolicy;
|
||||
}); // end define()
|
||||
|
||||
@@ -1,158 +1,95 @@
|
||||
CMS.Models.Textbook = Backbone.AssociatedModel.extend({
|
||||
defaults: function() {
|
||||
return {
|
||||
name: "",
|
||||
chapters: new CMS.Collections.ChapterSet([{}]),
|
||||
showChapters: false,
|
||||
editing: false
|
||||
};
|
||||
},
|
||||
relations: [{
|
||||
type: Backbone.Many,
|
||||
key: "chapters",
|
||||
relatedModel: "CMS.Models.Chapter",
|
||||
collectionType: "CMS.Collections.ChapterSet"
|
||||
}],
|
||||
initialize: function() {
|
||||
this.setOriginalAttributes();
|
||||
return this;
|
||||
},
|
||||
setOriginalAttributes: function() {
|
||||
this._originalAttributes = this.parse(this.toJSON());
|
||||
},
|
||||
reset: function() {
|
||||
this.set(this._originalAttributes, {parse: true});
|
||||
},
|
||||
isDirty: function() {
|
||||
return !_.isEqual(this._originalAttributes, this.parse(this.toJSON()));
|
||||
},
|
||||
isEmpty: function() {
|
||||
return !this.get('name') && this.get('chapters').isEmpty();
|
||||
},
|
||||
url: function() {
|
||||
if(this.isNew()) {
|
||||
return CMS.URL.TEXTBOOKS + "/new";
|
||||
} else {
|
||||
return CMS.URL.TEXTBOOKS + "/" + this.id;
|
||||
}
|
||||
},
|
||||
parse: function(response) {
|
||||
var ret = $.extend(true, {}, response);
|
||||
if("tab_title" in ret && !("name" in ret)) {
|
||||
ret.name = ret.tab_title;
|
||||
delete ret.tab_title;
|
||||
}
|
||||
if("url" in ret && !("chapters" in ret)) {
|
||||
ret.chapters = {"url": ret.url};
|
||||
delete ret.url;
|
||||
}
|
||||
_.each(ret.chapters, function(chapter, i) {
|
||||
chapter.order = chapter.order || i+1;
|
||||
});
|
||||
return ret;
|
||||
},
|
||||
toJSON: function() {
|
||||
return {
|
||||
tab_title: this.get('name'),
|
||||
chapters: this.get('chapters').toJSON()
|
||||
};
|
||||
},
|
||||
// NOTE: validation functions should return non-internationalized error
|
||||
// messages. The messages will be passed through gettext in the template.
|
||||
validate: function(attrs, options) {
|
||||
if (!attrs.name) {
|
||||
define(["backbone", "underscore", "js/models/chapter", "js/collections/chapter", "backbone.associations"],
|
||||
function(Backbone, _, ChapterModel, ChapterCollection) {
|
||||
|
||||
var Textbook = Backbone.AssociatedModel.extend({
|
||||
defaults: function() {
|
||||
return {
|
||||
message: "Textbook name is required",
|
||||
attributes: {name: true}
|
||||
name: "",
|
||||
chapters: new ChapterCollection([{}]),
|
||||
showChapters: false,
|
||||
editing: false
|
||||
};
|
||||
}
|
||||
if (attrs.chapters.length === 0) {
|
||||
return {
|
||||
message: "Please add at least one chapter",
|
||||
attributes: {chapters: true}
|
||||
};
|
||||
} else {
|
||||
// validate all chapters
|
||||
var invalidChapters = [];
|
||||
attrs.chapters.each(function(chapter) {
|
||||
if(!chapter.isValid()) {
|
||||
invalidChapters.push(chapter);
|
||||
}
|
||||
},
|
||||
relations: [{
|
||||
type: Backbone.Many,
|
||||
key: "chapters",
|
||||
relatedModel: ChapterModel,
|
||||
collectionType: ChapterCollection
|
||||
}],
|
||||
initialize: function() {
|
||||
this.setOriginalAttributes();
|
||||
return this;
|
||||
},
|
||||
setOriginalAttributes: function() {
|
||||
this._originalAttributes = this.parse(this.toJSON());
|
||||
},
|
||||
reset: function() {
|
||||
this.set(this._originalAttributes, {parse: true});
|
||||
},
|
||||
isDirty: function() {
|
||||
return !_.isEqual(this._originalAttributes, this.parse(this.toJSON()));
|
||||
},
|
||||
isEmpty: function() {
|
||||
return !this.get('name') && this.get('chapters').isEmpty();
|
||||
},
|
||||
url: function() {
|
||||
if(this.isNew()) {
|
||||
return CMS.URL.TEXTBOOKS + "/new";
|
||||
} else {
|
||||
return CMS.URL.TEXTBOOKS + "/" + this.id;
|
||||
}
|
||||
},
|
||||
parse: function(response) {
|
||||
var ret = $.extend(true, {}, response);
|
||||
if("tab_title" in ret && !("name" in ret)) {
|
||||
ret.name = ret.tab_title;
|
||||
delete ret.tab_title;
|
||||
}
|
||||
if("url" in ret && !("chapters" in ret)) {
|
||||
ret.chapters = {"url": ret.url};
|
||||
delete ret.url;
|
||||
}
|
||||
_.each(ret.chapters, function(chapter, i) {
|
||||
chapter.order = chapter.order || i+1;
|
||||
});
|
||||
if(!_.isEmpty(invalidChapters)) {
|
||||
return ret;
|
||||
},
|
||||
toJSON: function() {
|
||||
return {
|
||||
tab_title: this.get('name'),
|
||||
chapters: this.get('chapters').toJSON()
|
||||
};
|
||||
},
|
||||
// NOTE: validation functions should return non-internationalized error
|
||||
// messages. The messages will be passed through gettext in the template.
|
||||
validate: function(attrs, options) {
|
||||
if (!attrs.name) {
|
||||
return {
|
||||
message: "All chapters must have a name and asset",
|
||||
attributes: {chapters: invalidChapters}
|
||||
message: "Textbook name is required",
|
||||
attributes: {name: true}
|
||||
};
|
||||
}
|
||||
if (attrs.chapters.length === 0) {
|
||||
return {
|
||||
message: "Please add at least one chapter",
|
||||
attributes: {chapters: true}
|
||||
};
|
||||
} else {
|
||||
// validate all chapters
|
||||
var invalidChapters = [];
|
||||
attrs.chapters.each(function(chapter) {
|
||||
if(!chapter.isValid()) {
|
||||
invalidChapters.push(chapter);
|
||||
}
|
||||
});
|
||||
if(!_.isEmpty(invalidChapters)) {
|
||||
return {
|
||||
message: "All chapters must have a name and asset",
|
||||
attributes: {chapters: invalidChapters}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return Textbook;
|
||||
});
|
||||
CMS.Collections.TextbookSet = Backbone.Collection.extend({
|
||||
model: CMS.Models.Textbook,
|
||||
url: function() { return CMS.URL.TEXTBOOKS; },
|
||||
save: function(options) {
|
||||
return this.sync('update', this, options);
|
||||
}
|
||||
});
|
||||
CMS.Models.Chapter = Backbone.AssociatedModel.extend({
|
||||
defaults: function() {
|
||||
return {
|
||||
name: "",
|
||||
asset_path: "",
|
||||
order: this.collection ? this.collection.nextOrder() : 1
|
||||
};
|
||||
},
|
||||
isEmpty: function() {
|
||||
return !this.get('name') && !this.get('asset_path');
|
||||
},
|
||||
parse: function(response) {
|
||||
if("title" in response && !("name" in response)) {
|
||||
response.name = response.title;
|
||||
delete response.title;
|
||||
}
|
||||
if("url" in response && !("asset_path" in response)) {
|
||||
response.asset_path = response.url;
|
||||
delete response.url;
|
||||
}
|
||||
return response;
|
||||
},
|
||||
toJSON: function() {
|
||||
return {
|
||||
title: this.get('name'),
|
||||
url: this.get('asset_path')
|
||||
};
|
||||
},
|
||||
// NOTE: validation functions should return non-internationalized error
|
||||
// messages. The messages will be passed through gettext in the template.
|
||||
validate: function(attrs, options) {
|
||||
if(!attrs.name && !attrs.asset_path) {
|
||||
return {
|
||||
message: "Chapter name and asset_path are both required",
|
||||
attributes: {name: true, asset_path: true}
|
||||
};
|
||||
} else if(!attrs.name) {
|
||||
return {
|
||||
message: "Chapter name is required",
|
||||
attributes: {name: true}
|
||||
};
|
||||
} else if (!attrs.asset_path) {
|
||||
return {
|
||||
message: "asset_path is required",
|
||||
attributes: {asset_path: true}
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
CMS.Collections.ChapterSet = Backbone.Collection.extend({
|
||||
model: CMS.Models.Chapter,
|
||||
comparator: "order",
|
||||
nextOrder: function() {
|
||||
if(!this.length) return 1;
|
||||
return this.last().get('order') + 1;
|
||||
},
|
||||
isEmpty: function() {
|
||||
return this.length === 0 || this.every(function(m) { return m.isEmpty(); });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
CMS.Models.FileUpload = Backbone.Model.extend({
|
||||
define(["backbone", "underscore", "gettext"], function(Backbone, _, gettext) {
|
||||
|
||||
var FileUpload = Backbone.Model.extend({
|
||||
defaults: {
|
||||
"title": "",
|
||||
"message": "",
|
||||
@@ -57,3 +59,6 @@ CMS.Models.FileUpload = Backbone.Model.extend({
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return FileUpload;
|
||||
}); // end define()
|
||||
|
||||
91
cms/static/js/views/asset.js
Normal file
91
cms/static/js/views/asset.js
Normal file
@@ -0,0 +1,91 @@
|
||||
define(["backbone", "underscore", "gettext", "js/views/feedback_prompt", "js/views/feedback_notification"],
|
||||
function(Backbone, _, gettext, PromptView, NotificationView) {
|
||||
var AssetView = Backbone.View.extend({
|
||||
initialize: function() {
|
||||
this.template = _.template($("#asset-tpl").text());
|
||||
this.listenTo(this.model, "change:locked", this.updateLockState);
|
||||
},
|
||||
tagName: "tr",
|
||||
events: {
|
||||
"click .remove-asset-button": "confirmDelete",
|
||||
"click .lock-checkbox": "lockAsset"
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var uniqueId = _.uniqueId('lock_asset_');
|
||||
this.$el.html(this.template({
|
||||
display_name: this.model.get('display_name'),
|
||||
thumbnail: this.model.get('thumbnail'),
|
||||
date_added: this.model.get('date_added'),
|
||||
url: this.model.get('url'),
|
||||
portable_url: this.model.get('portable_url'),
|
||||
uniqueId: uniqueId
|
||||
}));
|
||||
this.updateLockState();
|
||||
return this;
|
||||
},
|
||||
|
||||
updateLockState: function () {
|
||||
var locked_class = "is-locked";
|
||||
|
||||
// Add a class of "locked" to the tr element if appropriate,
|
||||
// and toggle locked state of hidden checkbox.
|
||||
if (this.model.get('locked')) {
|
||||
this.$el.addClass(locked_class);
|
||||
this.$el.find('.lock-checkbox').attr('checked','checked');
|
||||
}
|
||||
else {
|
||||
this.$el.removeClass(locked_class);
|
||||
this.$el.find('.lock-checkbox').removeAttr('checked');
|
||||
}
|
||||
},
|
||||
|
||||
confirmDelete: function(e) {
|
||||
if(e && e.preventDefault) { e.preventDefault(); }
|
||||
var asset = this.model, collection = this.model.collection;
|
||||
new PromptView.Warning({
|
||||
title: gettext("Delete File Confirmation"),
|
||||
message: gettext("Are you sure you wish to delete this item. It cannot be reversed!\n\nAlso any content that links/refers to this item will no longer work (e.g. broken images and/or links)"),
|
||||
actions: {
|
||||
primary: {
|
||||
text: gettext("Delete"),
|
||||
click: function (view) {
|
||||
view.hide();
|
||||
asset.destroy({
|
||||
wait: true, // Don't remove the asset from the collection until successful.
|
||||
success: function () {
|
||||
new NotificationView.Confirmation({
|
||||
title: gettext("Your file has been deleted."),
|
||||
closeIcon: false,
|
||||
maxShown: 2000
|
||||
}).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
secondary: {
|
||||
text: gettext("Cancel"),
|
||||
click: function (view) {
|
||||
view.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
}).show();
|
||||
},
|
||||
|
||||
lockAsset: function(e) {
|
||||
var asset = this.model;
|
||||
var saving = new NotificationView.Mini({
|
||||
title: gettext("Saving…")
|
||||
}).show();
|
||||
asset.save({'locked': !asset.get('locked')}, {
|
||||
wait: true, // This means we won't re-render until we get back the success state.
|
||||
success: function() {
|
||||
saving.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return AssetView;
|
||||
}); // end define()
|
||||
@@ -1,93 +0,0 @@
|
||||
CMS.Views.Asset = Backbone.View.extend({
|
||||
initialize: function() {
|
||||
this.template = _.template($("#asset-tpl").text());
|
||||
this.listenTo(this.model, "change:locked", this.updateLockState);
|
||||
},
|
||||
|
||||
tagName: "tr",
|
||||
|
||||
events: {
|
||||
"click .remove-asset-button": "confirmDelete",
|
||||
"click .lock-checkbox": "lockAsset"
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var uniqueId = _.uniqueId('lock_asset_');
|
||||
|
||||
this.$el.html(this.template({
|
||||
display_name: this.model.get('display_name'),
|
||||
thumbnail: this.model.get('thumbnail'),
|
||||
date_added: this.model.get('date_added'),
|
||||
url: this.model.get('url'),
|
||||
portable_url: this.model.get('portable_url'),
|
||||
uniqueId: uniqueId}));
|
||||
|
||||
this.updateLockState();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
updateLockState: function () {
|
||||
var locked_class = "is-locked";
|
||||
|
||||
// Add a class of "locked" to the tr element if appropriate,
|
||||
// and toggle locked state of hidden checkbox.
|
||||
if (this.model.get('locked')) {
|
||||
this.$el.addClass(locked_class);
|
||||
this.$el.find('.lock-checkbox').attr('checked','checked');
|
||||
}
|
||||
else {
|
||||
this.$el.removeClass(locked_class);
|
||||
this.$el.find('.lock-checkbox').removeAttr('checked');
|
||||
}
|
||||
},
|
||||
|
||||
confirmDelete: function(e) {
|
||||
if(e && e.preventDefault) { e.preventDefault(); }
|
||||
var asset = this.model;
|
||||
new CMS.Views.Prompt.Warning({
|
||||
title: gettext("Delete File Confirmation"),
|
||||
message: gettext("Are you sure you wish to delete this item. It cannot be reversed!\n\nAlso any content that links/refers to this item will no longer work (e.g. broken images and/or links)"),
|
||||
actions: {
|
||||
primary: {
|
||||
text: gettext("Delete"),
|
||||
click: function (view) {
|
||||
view.hide();
|
||||
asset.destroy({
|
||||
wait: true, // Don't remove the asset from the collection until successful.
|
||||
success: function () {
|
||||
new CMS.Views.Notification.Confirmation({
|
||||
title: gettext("Your file has been deleted."),
|
||||
closeIcon: false,
|
||||
maxShown: 2000
|
||||
}).show()
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
secondary: [
|
||||
{
|
||||
text: gettext("Cancel"),
|
||||
click: function (view) {
|
||||
view.hide();
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}).show();
|
||||
},
|
||||
|
||||
lockAsset: function(e) {
|
||||
var asset = this.model;
|
||||
var saving = new CMS.Views.Notification.Mini({
|
||||
title: gettext("Saving…")
|
||||
}).show();
|
||||
asset.save({'locked': !asset.get('locked')}, {
|
||||
wait: true, // This means we won't re-render until we get back the success state.
|
||||
success: function() {
|
||||
saving.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,90 +1,50 @@
|
||||
// This code is temporarily moved out of asset_index.html
|
||||
// to fix AWS pipelining issues. We can move it back after RequireJS is integrated.
|
||||
$(document).ready(function() {
|
||||
$('.uploads .upload-button').bind('click', showUploadModal);
|
||||
$('.upload-modal .close-button').bind('click', hideModal);
|
||||
$('.upload-modal .choose-file-button').bind('click', showFileSelectionMenu);
|
||||
define(["backbone", "js/views/asset"], function(Backbone, AssetView) {
|
||||
|
||||
var AssetsView = Backbone.View.extend({
|
||||
// takes AssetCollection as model
|
||||
|
||||
initialize : function() {
|
||||
this.listenTo(this.collection, 'destroy', this.handleDestroy);
|
||||
this.render();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.$el.empty();
|
||||
|
||||
var self = this;
|
||||
this.collection.each(
|
||||
function(asset) {
|
||||
var view = new AssetView({model: asset});
|
||||
self.$el.append(view.render().el);
|
||||
});
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
handleDestroy: function(model, collection, options) {
|
||||
var index = options.index;
|
||||
this.$el.children().eq(index).remove();
|
||||
|
||||
analytics.track('Deleted Asset', {
|
||||
'course': course_location_analytics,
|
||||
'id': model.get('url')
|
||||
});
|
||||
},
|
||||
|
||||
addAsset: function (model) {
|
||||
// If asset is not already being shown, add it.
|
||||
if (this.collection.findWhere({'url': model.get('url')}) === undefined) {
|
||||
this.collection.add(model, {at: 0});
|
||||
var view = new AssetView({model: model});
|
||||
this.$el.prepend(view.render().el);
|
||||
|
||||
analytics.track('Uploaded a File', {
|
||||
'course': course_location_analytics,
|
||||
'asset_url': model.get('url')
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var showUploadModal = function (e) {
|
||||
e.preventDefault();
|
||||
resetUploadModal();
|
||||
// $modal has to be global for hideModal to work.
|
||||
$modal = $('.upload-modal').show();
|
||||
$('.file-input').bind('change', startUpload);
|
||||
$('.upload-modal .file-chooser').fileupload({
|
||||
dataType: 'json',
|
||||
type: 'POST',
|
||||
maxChunkSize: 100 * 1000 * 1000, // 100 MB
|
||||
autoUpload: true,
|
||||
progressall: function(e, data) {
|
||||
var percentComplete = parseInt((100 * data.loaded) / data.total, 10);
|
||||
showUploadFeedback(e, percentComplete);
|
||||
},
|
||||
maxFileSize: 100 * 1000 * 1000, // 100 MB
|
||||
maxNumberofFiles: 100,
|
||||
add: function(e, data) {
|
||||
data.process().done(function () {
|
||||
data.submit();
|
||||
});
|
||||
},
|
||||
done: function(e, data) {
|
||||
displayFinishedUpload(data.result);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
$modalCover.show();
|
||||
};
|
||||
|
||||
var showFileSelectionMenu = function(e) {
|
||||
e.preventDefault();
|
||||
$('.file-input').click();
|
||||
};
|
||||
|
||||
var startUpload = function (e) {
|
||||
var file = e.target.value;
|
||||
|
||||
$('.upload-modal h1').html(gettext('Uploading…'));
|
||||
$('.upload-modal .file-name').html(file.substring(file.lastIndexOf("\\") + 1));
|
||||
$('.upload-modal .choose-file-button').hide();
|
||||
$('.upload-modal .progress-bar').removeClass('loaded').show();
|
||||
};
|
||||
|
||||
var resetUploadModal = function () {
|
||||
$('.file-input').unbind('change', startUpload);
|
||||
|
||||
// Reset modal so it no longer displays information about previously
|
||||
// completed uploads.
|
||||
var percentVal = '0%';
|
||||
$('.upload-modal .progress-fill').width(percentVal);
|
||||
$('.upload-modal .progress-fill').html(percentVal);
|
||||
$('.upload-modal .progress-bar').hide();
|
||||
|
||||
$('.upload-modal .file-name').show();
|
||||
$('.upload-modal .file-name').html('');
|
||||
$('.upload-modal .choose-file-button').html(gettext('Choose File'));
|
||||
$('.upload-modal .embeddable-xml-input').val('');
|
||||
$('.upload-modal .embeddable').hide();
|
||||
};
|
||||
|
||||
var showUploadFeedback = function (event, percentComplete) {
|
||||
var percentVal = percentComplete + '%';
|
||||
$('.upload-modal .progress-fill').width(percentVal);
|
||||
$('.upload-modal .progress-fill').html(percentVal);
|
||||
};
|
||||
|
||||
var displayFinishedUpload = function (resp) {
|
||||
var asset = resp.asset;
|
||||
|
||||
$('.upload-modal h1').html(gettext('Upload New File'));
|
||||
$('.upload-modal .embeddable-xml-input').val(asset.portable_url);
|
||||
$('.upload-modal .embeddable').show();
|
||||
$('.upload-modal .file-name').hide();
|
||||
$('.upload-modal .progress-fill').html(resp.msg);
|
||||
$('.upload-modal .choose-file-button').html(gettext('Load Another File')).show();
|
||||
$('.upload-modal .progress-fill').width('100%');
|
||||
|
||||
// TODO remove setting on window object after RequireJS.
|
||||
window.assetsView.addAsset(new CMS.Models.Asset(asset));
|
||||
};
|
||||
return AssetsView;
|
||||
}); // end define();
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
CMS.Views.Assets = Backbone.View.extend({
|
||||
// takes CMS.Models.AssetCollection as model
|
||||
|
||||
initialize : function() {
|
||||
this.listenTo(this.collection, 'destroy', this.handleDestroy);
|
||||
this.render();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.$el.empty();
|
||||
|
||||
var self = this;
|
||||
this.collection.each(
|
||||
function(asset) {
|
||||
var view = new CMS.Views.Asset({model: asset});
|
||||
self.$el.append(view.render().el);
|
||||
});
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
handleDestroy: function(model, collection, options) {
|
||||
var index = options.index;
|
||||
this.$el.children().eq(index).remove();
|
||||
|
||||
analytics.track('Deleted Asset', {
|
||||
'course': course_location_analytics,
|
||||
'id': model.get('url')
|
||||
});
|
||||
},
|
||||
|
||||
addAsset: function (model) {
|
||||
// If asset is not already being shown, add it.
|
||||
if (this.collection.findWhere({'url': model.get('url')}) === undefined) {
|
||||
this.collection.add(model, {at: 0});
|
||||
var view = new CMS.Views.Asset({model: model});
|
||||
this.$el.prepend(view.render().el);
|
||||
|
||||
analytics.track('Uploaded a File', {
|
||||
'course': course_location_analytics,
|
||||
'asset_url': model.get('url')
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
93
cms/static/js/views/checklist.js
Normal file
93
cms/static/js/views/checklist.js
Normal file
@@ -0,0 +1,93 @@
|
||||
define(["backbone", "underscore", "jquery"], function(Backbone, _, $) {
|
||||
var ChecklistView = Backbone.View.extend({
|
||||
// takes CMS.Models.Checklists as model
|
||||
|
||||
events : {
|
||||
'click .course-checklist .checklist-title' : "toggleChecklist",
|
||||
'click .course-checklist .task input' : "toggleTask",
|
||||
'click a[rel="external"]' : "popup"
|
||||
},
|
||||
|
||||
initialize : function() {
|
||||
var self = this;
|
||||
this.template = _.template($("#checklist-tpl").text());
|
||||
this.collection.fetch({
|
||||
reset: true,
|
||||
complete: function() {
|
||||
self.render();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
// catch potential outside call before template loaded
|
||||
if (!this.template) return this;
|
||||
|
||||
this.$el.empty();
|
||||
|
||||
var self = this;
|
||||
_.each(this.collection.models,
|
||||
function(checklist, index) {
|
||||
self.$el.append(self.renderTemplate(checklist, index));
|
||||
});
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
renderTemplate: function (checklist, index) {
|
||||
var checklistItems = checklist.attributes['items'];
|
||||
var itemsChecked = 0;
|
||||
_.each(checklistItems,
|
||||
function(checklist) {
|
||||
if (checklist['is_checked']) {
|
||||
itemsChecked +=1;
|
||||
}
|
||||
});
|
||||
var percentChecked = Math.round((itemsChecked/checklistItems.length)*100);
|
||||
return this.template({
|
||||
checklistIndex : index,
|
||||
checklistShortDescription : checklist.attributes['short_description'],
|
||||
items: checklistItems,
|
||||
itemsChecked: itemsChecked,
|
||||
percentChecked: percentChecked});
|
||||
},
|
||||
|
||||
toggleChecklist : function(e) {
|
||||
e.preventDefault();
|
||||
$(e.target).closest('.course-checklist').toggleClass('is-collapsed');
|
||||
},
|
||||
|
||||
toggleTask : function (e) {
|
||||
var self = this;
|
||||
|
||||
var completed = 'is-completed';
|
||||
var $checkbox = $(e.target);
|
||||
var $task = $checkbox.closest('.task');
|
||||
$task.toggleClass(completed);
|
||||
|
||||
var checklist_index = $checkbox.data('checklist');
|
||||
var task_index = $checkbox.data('task');
|
||||
var model = this.collection.at(checklist_index);
|
||||
model.attributes.items[task_index].is_checked = $task.hasClass(completed);
|
||||
|
||||
model.save({},
|
||||
{
|
||||
success : function() {
|
||||
var updatedTemplate = self.renderTemplate(model, checklist_index);
|
||||
self.$el.find('#course-checklist'+checklist_index).first().replaceWith(updatedTemplate);
|
||||
|
||||
analytics.track('Toggled a Checklist Task', {
|
||||
'course': course_location_analytics,
|
||||
'task': model.attributes.items[task_index].short_description,
|
||||
'state': model.attributes.items[task_index].is_checked
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
popup: function(e) {
|
||||
e.preventDefault();
|
||||
window.open($(e.target).attr('href'));
|
||||
}
|
||||
});
|
||||
return ChecklistView;
|
||||
});
|
||||
@@ -1,80 +0,0 @@
|
||||
if (!CMS.Views['Checklists']) CMS.Views.Checklists = {};
|
||||
|
||||
CMS.Views.Checklists = Backbone.View.extend({
|
||||
// takes CMS.Models.Checklists as model
|
||||
|
||||
events : {
|
||||
'click .course-checklist .checklist-title' : "toggleChecklist",
|
||||
'click .course-checklist .task input' : "toggleTask",
|
||||
'click a[rel="external"]' : window.cmsLinkNewWindow
|
||||
},
|
||||
|
||||
initialize : function() {
|
||||
this.template = _.template($("#checklist-tpl").text());
|
||||
this.listenTo(this.collection, 'reset', this.render);
|
||||
this.render();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.$el.empty();
|
||||
|
||||
var self = this;
|
||||
_.each(this.collection.models,
|
||||
function(checklist, index) {
|
||||
self.$el.append(self.renderTemplate(checklist, index));
|
||||
});
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
renderTemplate: function (checklist, index) {
|
||||
var checklistItems = checklist.attributes['items'];
|
||||
var itemsChecked = 0;
|
||||
_.each(checklistItems,
|
||||
function(checklist) {
|
||||
if (checklist['is_checked']) {
|
||||
itemsChecked +=1;
|
||||
}
|
||||
});
|
||||
var percentChecked = Math.round((itemsChecked/checklistItems.length)*100);
|
||||
return this.template({
|
||||
checklistIndex : index,
|
||||
checklistShortDescription : checklist.attributes['short_description'],
|
||||
items: checklistItems,
|
||||
itemsChecked: itemsChecked,
|
||||
percentChecked: percentChecked});
|
||||
},
|
||||
|
||||
toggleChecklist : function(e) {
|
||||
e.preventDefault();
|
||||
$(e.target).closest('.course-checklist').toggleClass('is-collapsed');
|
||||
},
|
||||
|
||||
toggleTask : function (e) {
|
||||
var self = this;
|
||||
|
||||
var completed = 'is-completed';
|
||||
var $checkbox = $(e.target);
|
||||
var $task = $checkbox.closest('.task');
|
||||
$task.toggleClass(completed);
|
||||
|
||||
var checklist_index = $checkbox.data('checklist');
|
||||
var task_index = $checkbox.data('task');
|
||||
var model = this.collection.at(checklist_index);
|
||||
model.attributes.items[task_index].is_checked = $task.hasClass(completed);
|
||||
|
||||
model.save({},
|
||||
{
|
||||
success : function() {
|
||||
var updatedTemplate = self.renderTemplate(model, checklist_index);
|
||||
self.$el.find('#course-checklist'+checklist_index).first().replaceWith(updatedTemplate);
|
||||
|
||||
analytics.track('Toggled a Checklist Task', {
|
||||
'course': course_location_analytics,
|
||||
'task': model.attributes.items[task_index].short_description,
|
||||
'state': model.attributes.items[task_index].is_checked
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,41 +1,24 @@
|
||||
define(["backbone", "js/views/course_info_update", "js/views/course_info_handout"],
|
||||
function(Backbone, CourseInfoUpdateView, CourseInfoHandoutView) {
|
||||
/* this view should own everything on the page which has controls effecting its operation
|
||||
generate other views for the individual editors.
|
||||
The render here adds views for each update/handout by delegating to their collections but does not
|
||||
generate any html for the surrounding page.
|
||||
*/
|
||||
|
||||
var editWithCodeMirror = function(model, contentName, baseAssetUrl, textArea) {
|
||||
var content = rewriteStaticLinks(model.get(contentName), baseAssetUrl, '/static/');
|
||||
model.set(contentName, content);
|
||||
var $codeMirror = CodeMirror.fromTextArea(textArea, {
|
||||
mode: "text/html",
|
||||
lineNumbers: true,
|
||||
lineWrapping: true
|
||||
});
|
||||
$codeMirror.setValue(content);
|
||||
$codeMirror.clearHistory();
|
||||
return $codeMirror;
|
||||
};
|
||||
|
||||
var changeContentToPreview = function (model, contentName, baseAssetUrl) {
|
||||
var content = rewriteStaticLinks(model.get(contentName), '/static/', baseAssetUrl);
|
||||
model.set(contentName, content);
|
||||
return content;
|
||||
};
|
||||
|
||||
CMS.Views.CourseInfoEdit = Backbone.View.extend({
|
||||
var CourseInfoEdit = Backbone.View.extend({
|
||||
// takes CMS.Models.CourseInfo as model
|
||||
tagName: 'div',
|
||||
|
||||
render: function() {
|
||||
// instantiate the ClassInfoUpdateView and delegate the proper dom to it
|
||||
new CMS.Views.ClassInfoUpdateView({
|
||||
new CourseInfoUpdateView({
|
||||
el: $('body.updates'),
|
||||
collection: this.model.get('updates'),
|
||||
base_asset_url: this.model.get('base_asset_url')
|
||||
});
|
||||
|
||||
new CMS.Views.ClassInfoHandoutsView({
|
||||
new CourseInfoHandoutView({
|
||||
el: this.$('#course-handouts-view'),
|
||||
model: this.model.get('handouts'),
|
||||
base_asset_url: this.model.get('base_asset_url')
|
||||
@@ -43,318 +26,6 @@ CMS.Views.CourseInfoEdit = Backbone.View.extend({
|
||||
return this;
|
||||
}
|
||||
});
|
||||
return CourseInfoEdit;
|
||||
|
||||
CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
|
||||
// collection is CourseUpdateCollection
|
||||
events: {
|
||||
"click .new-update-button" : "onNew",
|
||||
"click #course-update-view .save-button" : "onSave",
|
||||
"click #course-update-view .cancel-button" : "onCancel",
|
||||
"click .post-actions > .edit-button" : "onEdit",
|
||||
"click .post-actions > .delete-button" : "onDelete"
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
this.template = _.template($("#course_info_update-tpl").text());
|
||||
this.render();
|
||||
// when the client refetches the updates as a whole, re-render them
|
||||
this.listenTo(this.collection, 'reset', this.render);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
// iterate over updates and create views for each using the template
|
||||
var updateEle = this.$el.find("#course-update-list");
|
||||
// remove and then add all children
|
||||
$(updateEle).empty();
|
||||
var self = this;
|
||||
this.collection.each(function (update) {
|
||||
try {
|
||||
changeContentToPreview(update, 'content', self.options['base_asset_url'])
|
||||
var newEle = self.template({ updateModel : update });
|
||||
$(updateEle).append(newEle);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
this.$el.find(".new-update-form").hide();
|
||||
this.$el.find('.date').datepicker({ 'dateFormat': 'MM d, yy' });
|
||||
return this;
|
||||
},
|
||||
|
||||
onNew: function(event) {
|
||||
event.preventDefault();
|
||||
var self = this;
|
||||
// create new obj, insert into collection, and render this one ele overriding the hidden attr
|
||||
var newModel = new CMS.Models.CourseUpdate();
|
||||
this.collection.add(newModel, {at : 0});
|
||||
|
||||
var $newForm = $(this.template({ updateModel : newModel }));
|
||||
|
||||
var updateEle = this.$el.find("#course-update-list");
|
||||
$(updateEle).prepend($newForm);
|
||||
|
||||
var $textArea = $newForm.find(".new-update-content").first();
|
||||
this.$codeMirror = CodeMirror.fromTextArea($textArea.get(0), {
|
||||
mode: "text/html",
|
||||
lineNumbers: true,
|
||||
lineWrapping: true
|
||||
});
|
||||
|
||||
$newForm.addClass('editing');
|
||||
this.$currentPost = $newForm.closest('li');
|
||||
|
||||
window.$modalCover.show();
|
||||
window.$modalCover.bind('click', function() {
|
||||
self.closeEditor(true);
|
||||
});
|
||||
|
||||
$('.date').datepicker('destroy');
|
||||
$('.date').datepicker({ 'dateFormat': 'MM d, yy' });
|
||||
},
|
||||
|
||||
onSave: function(event) {
|
||||
event.preventDefault();
|
||||
var targetModel = this.eventModel(event);
|
||||
targetModel.set({ date : this.dateEntry(event).val(), content : this.$codeMirror.getValue() });
|
||||
// push change to display, hide the editor, submit the change
|
||||
var saving = new CMS.Views.Notification.Mini({
|
||||
title: gettext('Saving…')
|
||||
});
|
||||
saving.show();
|
||||
var ele = this.modelDom(event);
|
||||
targetModel.save({}, {
|
||||
success: function() {
|
||||
saving.hide();
|
||||
},
|
||||
error: function() {
|
||||
ele.remove();
|
||||
}
|
||||
});
|
||||
this.closeEditor();
|
||||
|
||||
analytics.track('Saved Course Update', {
|
||||
'course': course_location_analytics,
|
||||
'date': this.dateEntry(event).val()
|
||||
});
|
||||
},
|
||||
|
||||
onCancel: function(event) {
|
||||
event.preventDefault();
|
||||
// change editor contents back to model values and hide the editor
|
||||
$(this.editor(event)).hide();
|
||||
// If the model was never created (user created a new update, then pressed Cancel),
|
||||
// we wish to remove it from the DOM.
|
||||
var targetModel = this.eventModel(event);
|
||||
this.closeEditor(!targetModel.id);
|
||||
},
|
||||
|
||||
onEdit: function(event) {
|
||||
event.preventDefault();
|
||||
var self = this;
|
||||
this.$currentPost = $(event.target).closest('li');
|
||||
this.$currentPost.addClass('editing');
|
||||
|
||||
$(this.editor(event)).show();
|
||||
var $textArea = this.$currentPost.find(".new-update-content").first();
|
||||
var targetModel = this.eventModel(event);
|
||||
this.$codeMirror = editWithCodeMirror(targetModel, 'content', self.options['base_asset_url'], $textArea.get(0));
|
||||
|
||||
window.$modalCover.show();
|
||||
window.$modalCover.bind('click', function() {
|
||||
self.closeEditor(self);
|
||||
});
|
||||
},
|
||||
|
||||
onDelete: function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
var self = this;
|
||||
var targetModel = this.eventModel(event);
|
||||
var confirm = new CMS.Views.Prompt.Warning({
|
||||
title: gettext('Are you sure you want to delete this update?'),
|
||||
message: gettext('This action cannot be undone.'),
|
||||
actions: {
|
||||
primary: {
|
||||
text: gettext('OK'),
|
||||
click: function () {
|
||||
analytics.track('Deleted Course Update', {
|
||||
'course': course_location_analytics,
|
||||
'date': self.dateEntry(event).val()
|
||||
});
|
||||
self.modelDom(event).remove();
|
||||
var deleting = new CMS.Views.Notification.Mini({
|
||||
title: gettext('Deleting…')
|
||||
});
|
||||
deleting.show();
|
||||
targetModel.destroy({
|
||||
success: function (model, response) {
|
||||
self.collection.fetch({
|
||||
success: function() {
|
||||
self.render();
|
||||
deleting.hide();
|
||||
},
|
||||
reset: true
|
||||
});
|
||||
}
|
||||
});
|
||||
confirm.hide();
|
||||
}
|
||||
},
|
||||
secondary: {
|
||||
text: gettext('Cancel'),
|
||||
click: function() {
|
||||
confirm.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
confirm.show();
|
||||
},
|
||||
|
||||
closeEditor: function(removePost) {
|
||||
var targetModel = this.collection.get(this.$currentPost.attr('name'));
|
||||
|
||||
if(removePost) {
|
||||
this.$currentPost.remove();
|
||||
}
|
||||
else {
|
||||
// close the modal and insert the appropriate data
|
||||
this.$currentPost.removeClass('editing');
|
||||
this.$currentPost.find('.date-display').html(targetModel.get('date'));
|
||||
this.$currentPost.find('.date').val(targetModel.get('date'));
|
||||
|
||||
var content = changeContentToPreview(targetModel, 'content', this.options['base_asset_url'])
|
||||
try {
|
||||
// just in case the content causes an error (embedded js errors)
|
||||
this.$currentPost.find('.update-contents').html(content);
|
||||
this.$currentPost.find('.new-update-content').val(content);
|
||||
} catch (e) {
|
||||
// ignore but handle rest of page
|
||||
}
|
||||
this.$currentPost.find('form').hide();
|
||||
this.$currentPost.find('.CodeMirror').remove();
|
||||
}
|
||||
|
||||
window.$modalCover.unbind('click');
|
||||
window.$modalCover.hide();
|
||||
this.$codeMirror = null;
|
||||
},
|
||||
|
||||
// Dereferencing from events to screen elements
|
||||
eventModel: function(event) {
|
||||
// not sure if it should be currentTarget or delegateTarget
|
||||
return this.collection.get($(event.currentTarget).attr("name"));
|
||||
},
|
||||
|
||||
modelDom: function(event) {
|
||||
return $(event.currentTarget).closest("li");
|
||||
},
|
||||
|
||||
editor: function(event) {
|
||||
var li = $(event.currentTarget).closest("li");
|
||||
if (li) return $(li).find("form").first();
|
||||
},
|
||||
|
||||
dateEntry: function(event) {
|
||||
var li = $(event.currentTarget).closest("li");
|
||||
if (li) return $(li).find(".date").first();
|
||||
},
|
||||
|
||||
contentEntry: function(event) {
|
||||
return $(event.currentTarget).closest("li").find(".new-update-content").first();
|
||||
},
|
||||
|
||||
dateDisplay: function(event) {
|
||||
return $(event.currentTarget).closest("li").find("#date-display").first();
|
||||
},
|
||||
|
||||
contentDisplay: function(event) {
|
||||
return $(event.currentTarget).closest("li").find(".update-contents").first();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// the handouts view is dumb right now; it needs tied to a model and all that jazz
|
||||
CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
|
||||
// collection is CourseUpdateCollection
|
||||
events: {
|
||||
"click .save-button" : "onSave",
|
||||
"click .cancel-button" : "onCancel",
|
||||
"click .edit-button" : "onEdit"
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
this.template = _.template($("#course_info_handouts-tpl").text());
|
||||
var self = this;
|
||||
this.model.fetch({
|
||||
complete: function() {
|
||||
self.render();
|
||||
},
|
||||
reset: true
|
||||
});
|
||||
},
|
||||
|
||||
render: function () {
|
||||
changeContentToPreview(this.model, 'data', this.options['base_asset_url'])
|
||||
|
||||
this.$el.html(
|
||||
$(this.template( {
|
||||
model: this.model
|
||||
})
|
||||
)
|
||||
);
|
||||
this.$preview = this.$el.find('.handouts-content');
|
||||
this.$form = this.$el.find(".edit-handouts-form");
|
||||
this.$editor = this.$form.find('.handouts-content-editor');
|
||||
this.$form.hide();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
onEdit: function(event) {
|
||||
var self = this;
|
||||
this.$editor.val(this.$preview.html());
|
||||
this.$form.show();
|
||||
|
||||
this.$codeMirror = editWithCodeMirror(self.model, 'data', self.options['base_asset_url'], this.$editor.get(0));
|
||||
|
||||
window.$modalCover.show();
|
||||
window.$modalCover.bind('click', function() {
|
||||
self.closeEditor();
|
||||
});
|
||||
},
|
||||
|
||||
onSave: function(event) {
|
||||
this.model.set('data', this.$codeMirror.getValue());
|
||||
var saving = new CMS.Views.Notification.Mini({
|
||||
title: gettext('Saving…')
|
||||
});
|
||||
saving.show();
|
||||
this.model.save({}, {
|
||||
success: function() {
|
||||
saving.hide();
|
||||
}
|
||||
});
|
||||
this.render();
|
||||
this.$form.hide();
|
||||
this.closeEditor();
|
||||
|
||||
analytics.track('Saved Course Handouts', {
|
||||
'course': course_location_analytics
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
onCancel: function(event) {
|
||||
this.$form.hide();
|
||||
this.closeEditor();
|
||||
},
|
||||
|
||||
closeEditor: function() {
|
||||
this.$form.hide();
|
||||
window.$modalCover.unbind('click');
|
||||
window.$modalCover.hide();
|
||||
this.$form.find('.CodeMirror').remove();
|
||||
this.$codeMirror = null;
|
||||
}
|
||||
});
|
||||
}); // end define()
|
||||
|
||||
92
cms/static/js/views/course_info_handout.js
Normal file
92
cms/static/js/views/course_info_handout.js
Normal file
@@ -0,0 +1,92 @@
|
||||
define(["backbone", "underscore", "codemirror", "js/views/feedback_notification", "js/views/course_info_helper"],
|
||||
function(Backbone, _, CodeMirror, NotificationView, CourseInfoHelper) {
|
||||
|
||||
var $modalCover = $(".modal-cover");
|
||||
// the handouts view is dumb right now; it needs tied to a model and all that jazz
|
||||
var CourseInfoHandoutsView = Backbone.View.extend({
|
||||
// collection is CourseUpdateCollection
|
||||
events: {
|
||||
"click .save-button" : "onSave",
|
||||
"click .cancel-button" : "onCancel",
|
||||
"click .edit-button" : "onEdit"
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
this.template = _.template($("#course_info_handouts-tpl").text());
|
||||
var self = this;
|
||||
this.model.fetch({
|
||||
complete: function() {
|
||||
self.render();
|
||||
},
|
||||
reset: true
|
||||
});
|
||||
},
|
||||
|
||||
render: function () {
|
||||
CourseInfoHelper.changeContentToPreview(
|
||||
this.model, 'data', this.options['base_asset_url']);
|
||||
|
||||
this.$el.html(
|
||||
$(this.template({
|
||||
model: this.model
|
||||
}))
|
||||
);
|
||||
this.$preview = this.$el.find('.handouts-content');
|
||||
this.$form = this.$el.find(".edit-handouts-form");
|
||||
this.$editor = this.$form.find('.handouts-content-editor');
|
||||
this.$form.hide();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
onEdit: function(event) {
|
||||
var self = this;
|
||||
this.$editor.val(this.$preview.html());
|
||||
this.$form.show();
|
||||
|
||||
this.$codeMirror = CourseInfoHelper.editWithCodeMirror(
|
||||
self.model, 'data', self.options['base_asset_url'], this.$editor.get(0));
|
||||
|
||||
$modalCover.show();
|
||||
$modalCover.bind('click', function() {
|
||||
self.closeEditor();
|
||||
});
|
||||
},
|
||||
|
||||
onSave: function(event) {
|
||||
this.model.set('data', this.$codeMirror.getValue());
|
||||
var saving = new NotificationView.Mini({
|
||||
title: gettext('Saving…')
|
||||
});
|
||||
saving.show();
|
||||
this.model.save({}, {
|
||||
success: function() {
|
||||
saving.hide();
|
||||
}
|
||||
});
|
||||
this.render();
|
||||
this.$form.hide();
|
||||
this.closeEditor();
|
||||
|
||||
analytics.track('Saved Course Handouts', {
|
||||
'course': course_location_analytics
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
onCancel: function(event) {
|
||||
this.$form.hide();
|
||||
this.closeEditor();
|
||||
},
|
||||
|
||||
closeEditor: function() {
|
||||
this.$form.hide();
|
||||
$modalCover.unbind('click');
|
||||
$modalCover.hide();
|
||||
this.$form.find('.CodeMirror').remove();
|
||||
this.$codeMirror = null;
|
||||
}
|
||||
});
|
||||
|
||||
return CourseInfoHandoutsView;
|
||||
}); // end define()
|
||||
24
cms/static/js/views/course_info_helper.js
Normal file
24
cms/static/js/views/course_info_helper.js
Normal file
@@ -0,0 +1,24 @@
|
||||
define(["codemirror", "utility"],
|
||||
function(CodeMirror) {
|
||||
var editWithCodeMirror = function(model, contentName, baseAssetUrl, textArea) {
|
||||
var content = rewriteStaticLinks(model.get(contentName), baseAssetUrl, '/static/');
|
||||
model.set(contentName, content);
|
||||
var $codeMirror = CodeMirror.fromTextArea(textArea, {
|
||||
mode: "text/html",
|
||||
lineNumbers: true,
|
||||
lineWrapping: true
|
||||
});
|
||||
$codeMirror.setValue(content);
|
||||
$codeMirror.clearHistory();
|
||||
return $codeMirror;
|
||||
};
|
||||
|
||||
var changeContentToPreview = function (model, contentName, baseAssetUrl) {
|
||||
var content = rewriteStaticLinks(model.get(contentName), '/static/', baseAssetUrl);
|
||||
model.set(contentName, content);
|
||||
return content;
|
||||
};
|
||||
|
||||
return {'editWithCodeMirror': editWithCodeMirror, 'changeContentToPreview': changeContentToPreview};
|
||||
}
|
||||
);
|
||||
240
cms/static/js/views/course_info_update.js
Normal file
240
cms/static/js/views/course_info_update.js
Normal file
@@ -0,0 +1,240 @@
|
||||
define(["backbone", "underscore", "codemirror", "js/models/course_update",
|
||||
"js/views/feedback_prompt", "js/views/feedback_notification", "js/views/course_info_helper"],
|
||||
function(Backbone, _, CodeMirror, CourseUpdateModel, PromptView, NotificationView, CourseInfoHelper) {
|
||||
|
||||
var $modalCover = $(".modal-cover");
|
||||
var CourseInfoUpdateView = Backbone.View.extend({
|
||||
// collection is CourseUpdateCollection
|
||||
events: {
|
||||
"click .new-update-button" : "onNew",
|
||||
"click #course-update-view .save-button" : "onSave",
|
||||
"click #course-update-view .cancel-button" : "onCancel",
|
||||
"click .post-actions > .edit-button" : "onEdit",
|
||||
"click .post-actions > .delete-button" : "onDelete"
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
this.template = _.template($("#course_info_update-tpl").text());
|
||||
this.render();
|
||||
// when the client refetches the updates as a whole, re-render them
|
||||
this.listenTo(this.collection, 'reset', this.render);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
// iterate over updates and create views for each using the template
|
||||
var updateEle = this.$el.find("#course-update-list");
|
||||
// remove and then add all children
|
||||
$(updateEle).empty();
|
||||
var self = this;
|
||||
this.collection.each(function (update) {
|
||||
try {
|
||||
CourseInfoHelper.changeContentToPreview(
|
||||
update, 'content', self.options['base_asset_url']);
|
||||
var newEle = self.template({ updateModel : update });
|
||||
$(updateEle).append(newEle);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
this.$el.find(".new-update-form").hide();
|
||||
this.$el.find('.date').datepicker({ 'dateFormat': 'MM d, yy' });
|
||||
return this;
|
||||
},
|
||||
|
||||
onNew: function(event) {
|
||||
event.preventDefault();
|
||||
var self = this;
|
||||
// create new obj, insert into collection, and render this one ele overriding the hidden attr
|
||||
var newModel = new CourseUpdateModel();
|
||||
this.collection.add(newModel, {at : 0});
|
||||
|
||||
var $newForm = $(this.template({ updateModel : newModel }));
|
||||
|
||||
var updateEle = this.$el.find("#course-update-list");
|
||||
$(updateEle).prepend($newForm);
|
||||
|
||||
var $textArea = $newForm.find(".new-update-content").first();
|
||||
this.$codeMirror = CodeMirror.fromTextArea($textArea.get(0), {
|
||||
mode: "text/html",
|
||||
lineNumbers: true,
|
||||
lineWrapping: true
|
||||
});
|
||||
|
||||
$newForm.addClass('editing');
|
||||
this.$currentPost = $newForm.closest('li');
|
||||
|
||||
$modalCover.show();
|
||||
$modalCover.bind('click', function() {
|
||||
self.closeEditor(true);
|
||||
});
|
||||
|
||||
$('.date').datepicker('destroy');
|
||||
$('.date').datepicker({ 'dateFormat': 'MM d, yy' });
|
||||
},
|
||||
|
||||
onSave: function(event) {
|
||||
event.preventDefault();
|
||||
var targetModel = this.eventModel(event);
|
||||
targetModel.set({ date : this.dateEntry(event).val(), content : this.$codeMirror.getValue() });
|
||||
// push change to display, hide the editor, submit the change
|
||||
var saving = new NotificationView.Mini({
|
||||
title: gettext('Saving…')
|
||||
});
|
||||
saving.show();
|
||||
var ele = this.modelDom(event);
|
||||
targetModel.save({}, {
|
||||
success: function() {
|
||||
saving.hide();
|
||||
},
|
||||
error: function() {
|
||||
ele.remove();
|
||||
}
|
||||
});
|
||||
this.closeEditor();
|
||||
|
||||
analytics.track('Saved Course Update', {
|
||||
'course': course_location_analytics,
|
||||
'date': this.dateEntry(event).val()
|
||||
});
|
||||
},
|
||||
|
||||
onCancel: function(event) {
|
||||
event.preventDefault();
|
||||
// change editor contents back to model values and hide the editor
|
||||
$(this.editor(event)).hide();
|
||||
// If the model was never created (user created a new update, then pressed Cancel),
|
||||
// we wish to remove it from the DOM.
|
||||
var targetModel = this.eventModel(event);
|
||||
this.closeEditor(!targetModel.id);
|
||||
},
|
||||
|
||||
onEdit: function(event) {
|
||||
event.preventDefault();
|
||||
var self = this;
|
||||
this.$currentPost = $(event.target).closest('li');
|
||||
this.$currentPost.addClass('editing');
|
||||
|
||||
$(this.editor(event)).show();
|
||||
var $textArea = this.$currentPost.find(".new-update-content").first();
|
||||
var targetModel = this.eventModel(event);
|
||||
this.$codeMirror = CourseInfoHelper.editWithCodeMirror(
|
||||
targetModel, 'content', self.options['base_asset_url'], $textArea.get(0));
|
||||
|
||||
$modalCover.show();
|
||||
$modalCover.bind('click', function() {
|
||||
self.closeEditor(self);
|
||||
});
|
||||
},
|
||||
|
||||
onDelete: function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
var self = this;
|
||||
var targetModel = this.eventModel(event);
|
||||
var confirm = new PromptView.Warning({
|
||||
title: gettext('Are you sure you want to delete this update?'),
|
||||
message: gettext('This action cannot be undone.'),
|
||||
actions: {
|
||||
primary: {
|
||||
text: gettext('OK'),
|
||||
click: function () {
|
||||
analytics.track('Deleted Course Update', {
|
||||
'course': course_location_analytics,
|
||||
'date': self.dateEntry(event).val()
|
||||
});
|
||||
self.modelDom(event).remove();
|
||||
var deleting = new NotificationView.Mini({
|
||||
title: gettext('Deleting…')
|
||||
});
|
||||
deleting.show();
|
||||
targetModel.destroy({
|
||||
success: function (model, response) {
|
||||
self.collection.fetch({
|
||||
success: function() {
|
||||
self.render();
|
||||
deleting.hide();
|
||||
},
|
||||
reset: true
|
||||
});
|
||||
}
|
||||
});
|
||||
confirm.hide();
|
||||
}
|
||||
},
|
||||
secondary: {
|
||||
text: gettext('Cancel'),
|
||||
click: function() {
|
||||
confirm.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
confirm.show();
|
||||
},
|
||||
|
||||
closeEditor: function(removePost) {
|
||||
var targetModel = this.collection.get(this.$currentPost.attr('name'));
|
||||
|
||||
if(removePost) {
|
||||
this.$currentPost.remove();
|
||||
}
|
||||
else {
|
||||
// close the modal and insert the appropriate data
|
||||
this.$currentPost.removeClass('editing');
|
||||
this.$currentPost.find('.date-display').html(targetModel.get('date'));
|
||||
this.$currentPost.find('.date').val(targetModel.get('date'));
|
||||
|
||||
var content = CourseInfoHelper.changeContentToPreview(
|
||||
targetModel, 'content', this.options['base_asset_url']);
|
||||
try {
|
||||
// just in case the content causes an error (embedded js errors)
|
||||
this.$currentPost.find('.update-contents').html(content);
|
||||
this.$currentPost.find('.new-update-content').val(content);
|
||||
} catch (e) {
|
||||
// ignore but handle rest of page
|
||||
}
|
||||
this.$currentPost.find('form').hide();
|
||||
this.$currentPost.find('.CodeMirror').remove();
|
||||
}
|
||||
|
||||
$modalCover.unbind('click');
|
||||
$modalCover.hide();
|
||||
this.$codeMirror = null;
|
||||
},
|
||||
|
||||
// Dereferencing from events to screen elements
|
||||
eventModel: function(event) {
|
||||
// not sure if it should be currentTarget or delegateTarget
|
||||
return this.collection.get($(event.currentTarget).attr("name"));
|
||||
},
|
||||
|
||||
modelDom: function(event) {
|
||||
return $(event.currentTarget).closest("li");
|
||||
},
|
||||
|
||||
editor: function(event) {
|
||||
var li = $(event.currentTarget).closest("li");
|
||||
if (li) return $(li).find("form").first();
|
||||
},
|
||||
|
||||
dateEntry: function(event) {
|
||||
var li = $(event.currentTarget).closest("li");
|
||||
if (li) return $(li).find(".date").first();
|
||||
},
|
||||
|
||||
contentEntry: function(event) {
|
||||
return $(event.currentTarget).closest("li").find(".new-update-content").first();
|
||||
},
|
||||
|
||||
dateDisplay: function(event) {
|
||||
return $(event.currentTarget).closest("li").find("#date-display").first();
|
||||
},
|
||||
|
||||
contentDisplay: function(event) {
|
||||
return $(event.currentTarget).closest("li").find(".update-contents").first();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
return CourseInfoUpdateView;
|
||||
}); // end define()
|
||||
77
cms/static/js/views/edit_chapter.js
Normal file
77
cms/static/js/views/edit_chapter.js
Normal file
@@ -0,0 +1,77 @@
|
||||
define(["backbone", "underscore", "underscore.string", "jquery", "gettext", "js/models/uploads", "js/views/uploads"],
|
||||
function(Backbone, _, str, $, gettext, FileUploadModel, UploadDialogView) {
|
||||
_.str = str; // used in template
|
||||
var EditChapter = Backbone.View.extend({
|
||||
initialize: function() {
|
||||
this.template = _.template($("#edit-chapter-tpl").text());
|
||||
this.listenTo(this.model, "change", this.render);
|
||||
},
|
||||
tagName: "li",
|
||||
className: function() {
|
||||
return "field-group chapter chapter" + this.model.get('order');
|
||||
},
|
||||
render: function() {
|
||||
this.$el.html(this.template({
|
||||
name: this.model.escape('name'),
|
||||
asset_path: this.model.escape('asset_path'),
|
||||
order: this.model.get('order'),
|
||||
error: this.model.validationError
|
||||
}));
|
||||
return this;
|
||||
},
|
||||
events: {
|
||||
"change .chapter-name": "changeName",
|
||||
"change .chapter-asset-path": "changeAssetPath",
|
||||
"click .action-close": "removeChapter",
|
||||
"click .action-upload": "openUploadDialog",
|
||||
"submit": "uploadAsset"
|
||||
},
|
||||
changeName: function(e) {
|
||||
if(e && e.preventDefault) { e.preventDefault(); }
|
||||
this.model.set({
|
||||
name: this.$(".chapter-name").val()
|
||||
}, {silent: true});
|
||||
return this;
|
||||
},
|
||||
changeAssetPath: function(e) {
|
||||
if(e && e.preventDefault) { e.preventDefault(); }
|
||||
this.model.set({
|
||||
asset_path: this.$(".chapter-asset-path").val()
|
||||
}, {silent: true});
|
||||
return this;
|
||||
},
|
||||
removeChapter: function(e) {
|
||||
if(e && e.preventDefault) { e.preventDefault(); }
|
||||
this.model.collection.remove(this.model);
|
||||
return this.remove();
|
||||
},
|
||||
openUploadDialog: function(e) {
|
||||
if(e && e.preventDefault) { e.preventDefault(); }
|
||||
this.model.set({
|
||||
name: this.$("input.chapter-name").val(),
|
||||
asset_path: this.$("input.chapter-asset-path").val()
|
||||
});
|
||||
var msg = new FileUploadModel({
|
||||
title: _.template(gettext("Upload a new PDF to “<%= name %>”"),
|
||||
{name: section.escape('name')}),
|
||||
message: "Files must be in PDF format.",
|
||||
mimeTypes: ['application/pdf']
|
||||
});
|
||||
var that = this;
|
||||
var view = new UploadDialogView({
|
||||
model: msg,
|
||||
onSuccess: function(response) {
|
||||
var options = {};
|
||||
if(!that.model.get('name')) {
|
||||
options.name = response.asset.displayname;
|
||||
}
|
||||
options.asset_path = response.asset.url;
|
||||
that.model.set(options);
|
||||
}
|
||||
});
|
||||
$(".wrapper-view").after(view.show().el);
|
||||
}
|
||||
});
|
||||
|
||||
return EditChapter;
|
||||
});
|
||||
94
cms/static/js/views/edit_textbook.js
Normal file
94
cms/static/js/views/edit_textbook.js
Normal file
@@ -0,0 +1,94 @@
|
||||
define(["backbone", "underscore", "jquery", "js/views/edit_chapter", "js/views/feedback_notification"],
|
||||
function(Backbone, _, $, EditChapterView, NotificationView) {
|
||||
var EditTextbook = Backbone.View.extend({
|
||||
initialize: function() {
|
||||
this.template = _.template($("#edit-textbook-tpl").text());
|
||||
this.listenTo(this.model, "invalid", this.render);
|
||||
var chapters = this.model.get('chapters');
|
||||
this.listenTo(chapters, "add", this.addOne);
|
||||
this.listenTo(chapters, "reset", this.addAll);
|
||||
this.listenTo(chapters, "all", this.render);
|
||||
},
|
||||
tagName: "section",
|
||||
className: "textbook",
|
||||
render: function() {
|
||||
this.$el.html(this.template({
|
||||
name: this.model.escape('name'),
|
||||
error: this.model.validationError
|
||||
}));
|
||||
this.addAll();
|
||||
return this;
|
||||
},
|
||||
events: {
|
||||
"change input[name=textbook-name]": "setName",
|
||||
"submit": "setAndClose",
|
||||
"click .action-cancel": "cancel",
|
||||
"click .action-add-chapter": "createChapter"
|
||||
},
|
||||
addOne: function(chapter) {
|
||||
var view = new EditChapterView({model: chapter});
|
||||
this.$("ol.chapters").append(view.render().el);
|
||||
return this;
|
||||
},
|
||||
addAll: function() {
|
||||
this.model.get('chapters').each(this.addOne, this);
|
||||
},
|
||||
createChapter: function(e) {
|
||||
if(e && e.preventDefault) { e.preventDefault(); }
|
||||
this.setValues();
|
||||
this.model.get('chapters').add([{}]);
|
||||
},
|
||||
setName: function(e) {
|
||||
if(e && e.preventDefault) { e.preventDefault(); }
|
||||
this.model.set("name", this.$("#textbook-name-input").val(), {silent: true});
|
||||
},
|
||||
setValues: function() {
|
||||
this.setName();
|
||||
var that = this;
|
||||
_.each(this.$("li"), function(li, i) {
|
||||
var chapter = that.model.get('chapters').at(i);
|
||||
if(!chapter) { return; }
|
||||
chapter.set({
|
||||
"name": $(".chapter-name", li).val(),
|
||||
"asset_path": $(".chapter-asset-path", li).val()
|
||||
});
|
||||
});
|
||||
return this;
|
||||
},
|
||||
setAndClose: function(e) {
|
||||
if(e && e.preventDefault) { e.preventDefault(); }
|
||||
this.setValues();
|
||||
if(!this.model.isValid()) { return; }
|
||||
var saving = new NotificationView.Mini({
|
||||
title: gettext("Saving") + "…"
|
||||
}).show();
|
||||
var that = this;
|
||||
this.model.save({}, {
|
||||
success: function() {
|
||||
that.model.setOriginalAttributes();
|
||||
that.close();
|
||||
},
|
||||
complete: function() {
|
||||
saving.hide();
|
||||
}
|
||||
});
|
||||
},
|
||||
cancel: function(e) {
|
||||
if(e && e.preventDefault) { e.preventDefault(); }
|
||||
this.model.reset();
|
||||
return this.close();
|
||||
},
|
||||
close: function() {
|
||||
var textbooks = this.model.collection;
|
||||
this.remove();
|
||||
if(this.model.isNew()) {
|
||||
// if the textbook has never been saved, remove it
|
||||
textbooks.remove(this.model);
|
||||
}
|
||||
// don't forget to tell the model that it's no longer being edited
|
||||
this.model.set("editing", false);
|
||||
return this;
|
||||
}
|
||||
});
|
||||
return EditTextbook;
|
||||
});
|
||||
@@ -1,207 +1,141 @@
|
||||
CMS.Views.SystemFeedback = Backbone.View.extend({
|
||||
options: {
|
||||
title: "",
|
||||
message: "",
|
||||
intent: null, // "warning", "confirmation", "error", "announcement", "step-required", etc
|
||||
type: null, // "alert", "notification", or "prompt": set by subclass
|
||||
shown: true, // is this view currently being shown?
|
||||
icon: true, // should we render an icon related to the message intent?
|
||||
closeIcon: true, // should we render a close button in the top right corner?
|
||||
minShown: 0, // length of time after this view has been shown before it can be hidden (milliseconds)
|
||||
maxShown: Infinity // length of time after this view has been shown before it will be automatically hidden (milliseconds)
|
||||
define(["backbone", "underscore", "underscore.string", "jquery"], function(Backbone, _, str, $) {
|
||||
var SystemFeedback = Backbone.View.extend({
|
||||
options: {
|
||||
title: "",
|
||||
message: "",
|
||||
intent: null, // "warning", "confirmation", "error", "announcement", "step-required", etc
|
||||
type: null, // "alert", "notification", or "prompt": set by subclass
|
||||
shown: true, // is this view currently being shown?
|
||||
icon: true, // should we render an icon related to the message intent?
|
||||
closeIcon: true, // should we render a close button in the top right corner?
|
||||
minShown: 0, // length of time after this view has been shown before it can be hidden (milliseconds)
|
||||
maxShown: Infinity // length of time after this view has been shown before it will be automatically hidden (milliseconds)
|
||||
|
||||
/* Could also have an "actions" hash: here is an example demonstrating
|
||||
the expected structure. For each action, by default the framework
|
||||
will call preventDefault on the click event before the function is
|
||||
run; to make it not do that, just pass `preventDefault: false` in
|
||||
the action object.
|
||||
/* Could also have an "actions" hash: here is an example demonstrating
|
||||
the expected structure. For each action, by default the framework
|
||||
will call preventDefault on the click event before the function is
|
||||
run; to make it not do that, just pass `preventDefault: false` in
|
||||
the action object.
|
||||
|
||||
actions: {
|
||||
primary: {
|
||||
"text": "Save",
|
||||
"class": "action-save",
|
||||
"click": function(view) {
|
||||
// do something when Save is clicked
|
||||
}
|
||||
},
|
||||
secondary: [
|
||||
{
|
||||
"text": "Cancel",
|
||||
"class": "action-cancel",
|
||||
"click": function(view) {}
|
||||
}, {
|
||||
"text": "Discard Changes",
|
||||
"class": "action-discard",
|
||||
"click": function(view) {}
|
||||
}
|
||||
]
|
||||
}
|
||||
*/
|
||||
},
|
||||
initialize: function() {
|
||||
if(!this.options.type) {
|
||||
throw "SystemFeedback: type required (given " +
|
||||
JSON.stringify(this.options) + ")";
|
||||
}
|
||||
if(!this.options.intent) {
|
||||
throw "SystemFeedback: intent required (given " +
|
||||
JSON.stringify(this.options) + ")";
|
||||
}
|
||||
var tpl = $("#system-feedback-tpl").text();
|
||||
if(!tpl) {
|
||||
console.error("Couldn't load system-feedback template");
|
||||
}
|
||||
this.template = _.template(tpl);
|
||||
this.setElement($("#page-"+this.options.type));
|
||||
// handle single "secondary" action
|
||||
if (this.options.actions && this.options.actions.secondary &&
|
||||
!_.isArray(this.options.actions.secondary)) {
|
||||
this.options.actions.secondary = [this.options.actions.secondary];
|
||||
}
|
||||
return this;
|
||||
},
|
||||
// public API: show() and hide()
|
||||
show: function() {
|
||||
clearTimeout(this.hideTimeout);
|
||||
this.options.shown = true;
|
||||
this.shownAt = new Date();
|
||||
this.render();
|
||||
if($.isNumeric(this.options.maxShown)) {
|
||||
this.hideTimeout = setTimeout(_.bind(this.hide, this),
|
||||
this.options.maxShown);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
hide: function() {
|
||||
if(this.shownAt && $.isNumeric(this.options.minShown) &&
|
||||
this.options.minShown > new Date() - this.shownAt)
|
||||
{
|
||||
actions: {
|
||||
primary: {
|
||||
"text": "Save",
|
||||
"class": "action-save",
|
||||
"click": function(view) {
|
||||
// do something when Save is clicked
|
||||
}
|
||||
},
|
||||
secondary: [
|
||||
{
|
||||
"text": "Cancel",
|
||||
"class": "action-cancel",
|
||||
"click": function(view) {}
|
||||
}, {
|
||||
"text": "Discard Changes",
|
||||
"class": "action-discard",
|
||||
"click": function(view) {}
|
||||
}
|
||||
]
|
||||
}
|
||||
*/
|
||||
},
|
||||
initialize: function() {
|
||||
if(!this.options.type) {
|
||||
throw "SystemFeedback: type required (given " +
|
||||
JSON.stringify(this.options) + ")";
|
||||
}
|
||||
if(!this.options.intent) {
|
||||
throw "SystemFeedback: intent required (given " +
|
||||
JSON.stringify(this.options) + ")";
|
||||
}
|
||||
var tpl = $("#system-feedback-tpl").text();
|
||||
if(!tpl) {
|
||||
console.error("Couldn't load system-feedback template");
|
||||
}
|
||||
this.template = _.template(tpl);
|
||||
this.setElement($("#page-"+this.options.type));
|
||||
// handle single "secondary" action
|
||||
if (this.options.actions && this.options.actions.secondary &&
|
||||
!_.isArray(this.options.actions.secondary)) {
|
||||
this.options.actions.secondary = [this.options.actions.secondary];
|
||||
}
|
||||
return this;
|
||||
},
|
||||
// public API: show() and hide()
|
||||
show: function() {
|
||||
clearTimeout(this.hideTimeout);
|
||||
this.hideTimeout = setTimeout(_.bind(this.hide, this),
|
||||
this.options.minShown - (new Date() - this.shownAt));
|
||||
} else {
|
||||
this.options.shown = false;
|
||||
delete this.shownAt;
|
||||
this.options.shown = true;
|
||||
this.shownAt = new Date();
|
||||
this.render();
|
||||
if($.isNumeric(this.options.maxShown)) {
|
||||
this.hideTimeout = setTimeout(_.bind(this.hide, this),
|
||||
this.options.maxShown);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
hide: function() {
|
||||
if(this.shownAt && $.isNumeric(this.options.minShown) &&
|
||||
this.options.minShown > new Date() - this.shownAt)
|
||||
{
|
||||
clearTimeout(this.hideTimeout);
|
||||
this.hideTimeout = setTimeout(_.bind(this.hide, this),
|
||||
this.options.minShown - (new Date() - this.shownAt));
|
||||
} else {
|
||||
this.options.shown = false;
|
||||
delete this.shownAt;
|
||||
this.render();
|
||||
}
|
||||
return this;
|
||||
},
|
||||
// the rest of the API should be considered semi-private
|
||||
events: {
|
||||
"click .action-close": "hide",
|
||||
"click .action-primary": "primaryClick",
|
||||
"click .action-secondary": "secondaryClick"
|
||||
},
|
||||
render: function() {
|
||||
// there can be only one active view of a given type at a time: only
|
||||
// one alert, only one notification, only one prompt. Therefore, we'll
|
||||
// use a singleton approach.
|
||||
var singleton = SystemFeedback["active_"+this.options.type];
|
||||
if(singleton && singleton !== this) {
|
||||
singleton.stopListening();
|
||||
singleton.undelegateEvents();
|
||||
}
|
||||
this.$el.html(this.template(this.options));
|
||||
SystemFeedback["active_"+this.options.type] = this;
|
||||
return this;
|
||||
},
|
||||
primaryClick: function(event) {
|
||||
var actions = this.options.actions;
|
||||
if(!actions) { return; }
|
||||
var primary = actions.primary;
|
||||
if(!primary) { return; }
|
||||
if(primary.preventDefault !== false) {
|
||||
event.preventDefault();
|
||||
}
|
||||
if(primary.click) {
|
||||
primary.click.call(event.target, this, event);
|
||||
}
|
||||
},
|
||||
secondaryClick: function(event) {
|
||||
var actions = this.options.actions;
|
||||
if(!actions) { return; }
|
||||
var secondaryList = actions.secondary;
|
||||
if(!secondaryList) { return; }
|
||||
// which secondary action was clicked?
|
||||
var i = 0; // default to the first secondary action (easier for testing)
|
||||
if(event && event.target) {
|
||||
i = _.indexOf(this.$(".action-secondary"), event.target);
|
||||
}
|
||||
var secondary = secondaryList[i];
|
||||
if(secondary.preventDefault !== false) {
|
||||
event.preventDefault();
|
||||
}
|
||||
if(secondary.click) {
|
||||
secondary.click.call(event.target, this, event);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
},
|
||||
// the rest of the API should be considered semi-private
|
||||
events: {
|
||||
"click .action-close": "hide",
|
||||
"click .action-primary": "primaryClick",
|
||||
"click .action-secondary": "secondaryClick"
|
||||
},
|
||||
render: function() {
|
||||
// there can be only one active view of a given type at a time: only
|
||||
// one alert, only one notification, only one prompt. Therefore, we'll
|
||||
// use a singleton approach.
|
||||
var parent = CMS.Views[_.str.capitalize(this.options.type)];
|
||||
if(parent && parent.active && parent.active !== this) {
|
||||
parent.active.stopListening();
|
||||
parent.active.undelegateEvents();
|
||||
}
|
||||
this.$el.html(this.template(this.options));
|
||||
parent.active = this;
|
||||
return this;
|
||||
},
|
||||
primaryClick: function(event) {
|
||||
var actions = this.options.actions;
|
||||
if(!actions) { return; }
|
||||
var primary = actions.primary;
|
||||
if(!primary) { return; }
|
||||
if(primary.preventDefault !== false) {
|
||||
event.preventDefault();
|
||||
}
|
||||
if(primary.click) {
|
||||
primary.click.call(event.target, this, event);
|
||||
}
|
||||
},
|
||||
secondaryClick: function(event) {
|
||||
var actions = this.options.actions;
|
||||
if(!actions) { return; }
|
||||
var secondaryList = actions.secondary;
|
||||
if(!secondaryList) { return; }
|
||||
// which secondary action was clicked?
|
||||
var i = 0; // default to the first secondary action (easier for testing)
|
||||
if(event && event.target) {
|
||||
i = _.indexOf(this.$(".action-secondary"), event.target);
|
||||
}
|
||||
var secondary = secondaryList[i];
|
||||
if(secondary.preventDefault !== false) {
|
||||
event.preventDefault();
|
||||
}
|
||||
if(secondary.click) {
|
||||
secondary.click.call(event.target, this, event);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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, {
|
||||
type: "notification",
|
||||
closeIcon: false
|
||||
})
|
||||
});
|
||||
CMS.Views.Prompt = CMS.Views.SystemFeedback.extend({
|
||||
options: $.extend({}, CMS.Views.SystemFeedback.prototype.options, {
|
||||
type: "prompt",
|
||||
closeIcon: false,
|
||||
icon: false
|
||||
}),
|
||||
render: function() {
|
||||
if(!window.$body) { window.$body = $(document.body); }
|
||||
if(this.options.shown) {
|
||||
$body.addClass('prompt-is-shown');
|
||||
} else {
|
||||
$body.removeClass('prompt-is-shown');
|
||||
}
|
||||
// super() in Javascript has awkward syntax :(
|
||||
return CMS.Views.SystemFeedback.prototype.render.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
|
||||
// create CMS.Views.Alert.Warning, CMS.Views.Notification.Confirmation,
|
||||
// CMS.Views.Prompt.StepRequired, etc
|
||||
var capitalCamel, types, intents;
|
||||
capitalCamel = _.compose(_.str.capitalize, _.str.camelize);
|
||||
types = ["alert", "notification", "prompt"];
|
||||
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
|
||||
var klass, subklass;
|
||||
klass = CMS.Views[capitalCamel(type)];
|
||||
subklass = klass.extend({
|
||||
options: $.extend({}, klass.prototype.options, {
|
||||
type: type,
|
||||
intent: intent
|
||||
})
|
||||
});
|
||||
klass[capitalCamel(intent)] = subklass;
|
||||
});
|
||||
return SystemFeedback;
|
||||
});
|
||||
|
||||
// set more sensible defaults for Notification-Mini views
|
||||
var miniOptions = CMS.Views.Notification.Mini.prototype.options;
|
||||
miniOptions.minShown = 1250;
|
||||
miniOptions.closeIcon = false;
|
||||
|
||||
37
cms/static/js/views/feedback_alert.js
Normal file
37
cms/static/js/views/feedback_alert.js
Normal file
@@ -0,0 +1,37 @@
|
||||
define(["jquery", "underscore", "underscore.string", "js/views/feedback"], function($, _, str, SystemFeedbackView) {
|
||||
var Alert = SystemFeedbackView.extend({
|
||||
options: $.extend({}, SystemFeedbackView.prototype.options, {
|
||||
type: "alert"
|
||||
}),
|
||||
slide_speed: 900,
|
||||
show: function() {
|
||||
SystemFeedbackView.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(SystemFeedbackView.prototype.hide, this, arguments),
|
||||
this.slideSpeed);
|
||||
}
|
||||
});
|
||||
|
||||
// create Alert.Warning, Alert.Confirmation, etc
|
||||
var capitalCamel, intents;
|
||||
capitalCamel = _.compose(str.capitalize, str.camelize);
|
||||
intents = ["warning", "error", "confirmation", "announcement", "step-required", "help", "mini"];
|
||||
_.each(intents, function(intent) {
|
||||
var subclass;
|
||||
subclass = Alert.extend({
|
||||
options: $.extend({}, Alert.prototype.options, {
|
||||
intent: intent
|
||||
})
|
||||
});
|
||||
Alert[capitalCamel(intent)] = subclass;
|
||||
});
|
||||
|
||||
return Alert;
|
||||
});
|
||||
29
cms/static/js/views/feedback_notification.js
Normal file
29
cms/static/js/views/feedback_notification.js
Normal file
@@ -0,0 +1,29 @@
|
||||
define(["jquery", "underscore", "underscore.string", "js/views/feedback"], function($, _, str, SystemFeedbackView) {
|
||||
var Notification = SystemFeedbackView.extend({
|
||||
options: $.extend({}, SystemFeedbackView.prototype.options, {
|
||||
type: "notification",
|
||||
closeIcon: false
|
||||
})
|
||||
});
|
||||
|
||||
// create Notification.Warning, Notification.Confirmation, etc
|
||||
var capitalCamel, intents;
|
||||
capitalCamel = _.compose(str.capitalize, str.camelize);
|
||||
intents = ["warning", "error", "confirmation", "announcement", "step-required", "help", "mini"];
|
||||
_.each(intents, function(intent) {
|
||||
var subclass;
|
||||
subclass = Notification.extend({
|
||||
options: $.extend({}, Notification.prototype.options, {
|
||||
intent: intent
|
||||
})
|
||||
});
|
||||
Notification[capitalCamel(intent)] = subclass;
|
||||
});
|
||||
|
||||
// set more sensible defaults for Notification.Mini views
|
||||
var miniOptions = Notification.Mini.prototype.options;
|
||||
miniOptions.minShown = 1250;
|
||||
miniOptions.closeIcon = false;
|
||||
|
||||
return Notification;
|
||||
});
|
||||
35
cms/static/js/views/feedback_prompt.js
Normal file
35
cms/static/js/views/feedback_prompt.js
Normal file
@@ -0,0 +1,35 @@
|
||||
define(["jquery", "underscore", "underscore.string", "js/views/feedback"], function($, _, str, SystemFeedbackView) {
|
||||
var Prompt = SystemFeedbackView.extend({
|
||||
options: $.extend({}, SystemFeedbackView.prototype.options, {
|
||||
type: "prompt",
|
||||
closeIcon: false,
|
||||
icon: false
|
||||
}),
|
||||
render: function() {
|
||||
if(!window.$body) { window.$body = $(document.body); }
|
||||
if(this.options.shown) {
|
||||
$body.addClass('prompt-is-shown');
|
||||
} else {
|
||||
$body.removeClass('prompt-is-shown');
|
||||
}
|
||||
// super() in Javascript has awkward syntax :(
|
||||
return SystemFeedbackView.prototype.render.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
|
||||
// create Prompt.Warning, Prompt.Confirmation, etc
|
||||
var capitalCamel, intents;
|
||||
capitalCamel = _.compose(str.capitalize, str.camelize);
|
||||
intents = ["warning", "error", "confirmation", "announcement", "step-required", "help", "mini"];
|
||||
_.each(intents, function(intent) {
|
||||
var subclass;
|
||||
subclass = Prompt.extend({
|
||||
options: $.extend({}, Prompt.prototype.options, {
|
||||
intent: intent
|
||||
})
|
||||
});
|
||||
Prompt[capitalCamel(intent)] = subclass;
|
||||
});
|
||||
|
||||
return Prompt;
|
||||
});
|
||||
@@ -1,99 +0,0 @@
|
||||
CMS.Models.AssignmentGrade = Backbone.Model.extend({
|
||||
defaults : {
|
||||
graderType : null, // the type label (string). May be "Not Graded" which implies None. I'd like to use id but that's ephemeral
|
||||
location : null // A location object
|
||||
},
|
||||
initialize : function(attrs) {
|
||||
if (attrs['assignmentUrl']) {
|
||||
this.set('location', new CMS.Models.Location(attrs['assignmentUrl'], {parse: true}));
|
||||
}
|
||||
},
|
||||
parse : function(attrs) {
|
||||
if (attrs && attrs['location']) {
|
||||
attrs.location = new CMS.Models.Location(attrs['location'], {parse: true});
|
||||
}
|
||||
},
|
||||
urlRoot : function() {
|
||||
if (this.has('location')) {
|
||||
var location = this.get('location');
|
||||
return '/' + location.get('org') + "/" + location.get('course') + '/' + location.get('category') + '/'
|
||||
+ location.get('name') + '/gradeas/';
|
||||
}
|
||||
else return "";
|
||||
}
|
||||
});
|
||||
|
||||
CMS.Views.OverviewAssignmentGrader = Backbone.View.extend({
|
||||
// instantiate w/ { graders : CourseGraderCollection, el : <the gradable-status div> }
|
||||
events : {
|
||||
"click .menu-toggle" : "showGradeMenu",
|
||||
"click .menu li" : "selectGradeType"
|
||||
},
|
||||
initialize : function() {
|
||||
// call template w/ {assignmentType : formatname, graders : CourseGraderCollection instance }
|
||||
this.template = _.template(
|
||||
// TODO move to a template file
|
||||
'<h4 class="status-label"><%= assignmentType %></h4>' +
|
||||
'<a data-tooltip="Mark/unmark this subsection as graded" class="menu-toggle" href="#">' +
|
||||
'<% if (!hideSymbol) {%><i class="icon-ok"></i><%};%>' +
|
||||
'</a>' +
|
||||
'<ul class="menu">' +
|
||||
'<% graders.each(function(option) { %>' +
|
||||
'<li><a <% if (option.get("type") == assignmentType) {%>class="is-selected" <%}%> href="#"><%= option.get("type") %></a></li>' +
|
||||
'<% }) %>' +
|
||||
'<li><a class="gradable-status-notgraded" href="#">Not Graded</a></li>' +
|
||||
'</ul>');
|
||||
this.assignmentGrade = new CMS.Models.AssignmentGrade({
|
||||
assignmentUrl : this.$el.closest('.id-holder').data('id'),
|
||||
graderType : this.$el.data('initial-status')});
|
||||
// TODO throw exception if graders is null
|
||||
this.graders = this.options['graders'];
|
||||
var cachethis = this;
|
||||
// defining here to get closure around this
|
||||
this.removeMenu = function(e) {
|
||||
e.preventDefault();
|
||||
cachethis.$el.removeClass('is-active');
|
||||
$(document).off('click', cachethis.removeMenu);
|
||||
};
|
||||
this.hideSymbol = this.options['hideSymbol'];
|
||||
this.render();
|
||||
},
|
||||
render : function() {
|
||||
this.$el.html(this.template({ assignmentType : this.assignmentGrade.get('graderType'), graders : this.graders,
|
||||
hideSymbol : this.hideSymbol }));
|
||||
if (this.assignmentGrade.has('graderType') && this.assignmentGrade.get('graderType') != "Not Graded") {
|
||||
this.$el.addClass('is-set');
|
||||
}
|
||||
else {
|
||||
this.$el.removeClass('is-set');
|
||||
}
|
||||
},
|
||||
showGradeMenu : function(e) {
|
||||
e.preventDefault();
|
||||
// I sure hope this doesn't break anything but it's needed to keep the removeMenu from activating
|
||||
e.stopPropagation();
|
||||
// nasty global event trap :-(
|
||||
$(document).on('click', this.removeMenu);
|
||||
this.$el.addClass('is-active');
|
||||
},
|
||||
selectGradeType : function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
this.removeMenu(e);
|
||||
|
||||
var saving = new CMS.Views.Notification.Mini({
|
||||
title: gettext('Saving…')
|
||||
});
|
||||
saving.show();
|
||||
|
||||
// TODO I'm not happy with this string fetch via the html for what should be an id. I'd rather use the id attr
|
||||
// of the CourseGradingPolicy model or null for Not Graded (NOTE, change template's if check for is-selected accordingly)
|
||||
this.assignmentGrade.save(
|
||||
'graderType',
|
||||
$(e.target).text(),
|
||||
{success: function () { saving.hide(); }}
|
||||
);
|
||||
|
||||
this.render();
|
||||
}
|
||||
});
|
||||
42
cms/static/js/views/list_textbooks.js
Normal file
42
cms/static/js/views/list_textbooks.js
Normal file
@@ -0,0 +1,42 @@
|
||||
define(["backbone", "underscore", "jquery", "js/views/edit_textbook", "js/views/show_textbook"],
|
||||
function(Backbone, _, $, EditTextbookView, ShowTextbookView) {
|
||||
var ListTextbooks = Backbone.View.extend({
|
||||
initialize: function() {
|
||||
this.emptyTemplate = _.template($("#no-textbooks-tpl").text());
|
||||
this.listenTo(this.collection, 'all', this.render);
|
||||
this.listenTo(this.collection, 'destroy', this.handleDestroy);
|
||||
},
|
||||
tagName: "div",
|
||||
className: "textbooks-list",
|
||||
render: function() {
|
||||
var textbooks = this.collection;
|
||||
if(textbooks.length === 0) {
|
||||
this.$el.html(this.emptyTemplate());
|
||||
} else {
|
||||
this.$el.empty();
|
||||
var that = this;
|
||||
textbooks.each(function(textbook) {
|
||||
var view;
|
||||
if (textbook.get("editing")) {
|
||||
view = new EditTextbookView({model: textbook});
|
||||
} else {
|
||||
view = new ShowTextbookView({model: textbook});
|
||||
}
|
||||
that.$el.append(view.render().el);
|
||||
});
|
||||
}
|
||||
return this;
|
||||
},
|
||||
events: {
|
||||
"click .new-button": "addOne"
|
||||
},
|
||||
addOne: function(e) {
|
||||
if(e && e.preventDefault) { e.preventDefault(); }
|
||||
this.collection.add([{editing: true}]);
|
||||
},
|
||||
handleDestroy: function(model, collection, options) {
|
||||
collection.remove(model);
|
||||
}
|
||||
});
|
||||
return ListTextbooks;
|
||||
});
|
||||
376
cms/static/js/views/metadata.js
Normal file
376
cms/static/js/views/metadata.js
Normal file
@@ -0,0 +1,376 @@
|
||||
|
||||
define(["backbone", "underscore", "js/models/metadata"], function(Backbone, _, MetadataModel) {
|
||||
var Metadata = {};
|
||||
|
||||
Metadata.Editor = Backbone.View.extend({
|
||||
|
||||
// Model is CMS.Models.MetadataCollection,
|
||||
initialize : function() {
|
||||
var tpl = $("#metadata-editor-tpl").text();
|
||||
if(!tpl) {
|
||||
console.error("Couldn't load metadata editor template");
|
||||
}
|
||||
this.template = _.template(tpl);
|
||||
|
||||
this.$el.html(this.template({numEntries: this.collection.length}));
|
||||
var counter = 0;
|
||||
|
||||
var self = this;
|
||||
this.collection.each(
|
||||
function (model) {
|
||||
var data = {
|
||||
el: self.$el.find('.metadata_entry')[counter++],
|
||||
model: model
|
||||
};
|
||||
if (model.getType() === MetadataModel.SELECT_TYPE) {
|
||||
new Metadata.Option(data);
|
||||
}
|
||||
else if (model.getType() === MetadataModel.INTEGER_TYPE ||
|
||||
model.getType() === MetadataModel.FLOAT_TYPE) {
|
||||
new Metadata.Number(data);
|
||||
}
|
||||
else if(model.getType() === MetadataModel.LIST_TYPE) {
|
||||
new Metadata.List(data);
|
||||
}
|
||||
else {
|
||||
// Everything else is treated as GENERIC_TYPE, which uses String editor.
|
||||
new Metadata.String(data);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the just the modified metadata values, in the format used to persist to the server.
|
||||
*/
|
||||
getModifiedMetadataValues: function () {
|
||||
var modified_values = {};
|
||||
this.collection.each(
|
||||
function (model) {
|
||||
if (model.isModified()) {
|
||||
modified_values[model.getFieldName()] = model.getValue();
|
||||
}
|
||||
}
|
||||
);
|
||||
return modified_values;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a display name for the component related to this metadata. This method looks to see
|
||||
* if there is a metadata entry called 'display_name', and if so, it returns its value. If there
|
||||
* is no such entry, or if display_name does not have a value set, it returns an empty string.
|
||||
*/
|
||||
getDisplayName: function () {
|
||||
var displayName = '';
|
||||
this.collection.each(
|
||||
function (model) {
|
||||
if (model.get('field_name') === 'display_name') {
|
||||
var displayNameValue = model.get('value');
|
||||
// It is possible that there is no display name value set. In that case, return empty string.
|
||||
displayName = displayNameValue ? displayNameValue : '';
|
||||
}
|
||||
}
|
||||
);
|
||||
return displayName;
|
||||
}
|
||||
});
|
||||
|
||||
Metadata.AbstractEditor = Backbone.View.extend({
|
||||
|
||||
// Model is MetadataModel
|
||||
initialize : function() {
|
||||
var self = this;
|
||||
var templateName = _.result(this, 'templateName');
|
||||
// Backbone model cid is only unique within the collection.
|
||||
this.uniqueId = _.uniqueId(templateName + "_");
|
||||
|
||||
var tpl = document.getElementById(templateName).text;
|
||||
if(!tpl) {
|
||||
console.error("Couldn't load template: " + templateName);
|
||||
}
|
||||
this.template = _.template(tpl);
|
||||
this.$el.html(this.template({model: this.model, uniqueId: this.uniqueId}));
|
||||
this.listenTo(this.model, 'change', this.render);
|
||||
this.render();
|
||||
},
|
||||
|
||||
/**
|
||||
* The ID/name of the template. Subclasses must override this.
|
||||
*/
|
||||
templateName: '',
|
||||
|
||||
/**
|
||||
* Returns the value currently displayed in the editor/view. Subclasses should implement this method.
|
||||
*/
|
||||
getValueFromEditor : function () {},
|
||||
|
||||
/**
|
||||
* Sets the value currently displayed in the editor/view. Subclasses should implement this method.
|
||||
*/
|
||||
setValueInEditor : function (value) {},
|
||||
|
||||
/**
|
||||
* Sets the value in the model, using the value currently displayed in the view.
|
||||
*/
|
||||
updateModel: function () {
|
||||
this.model.setValue(this.getValueFromEditor());
|
||||
},
|
||||
|
||||
/**
|
||||
* Clears the value currently set in the model (reverting to the default).
|
||||
*/
|
||||
clear: function () {
|
||||
this.model.clear();
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows the clear button, if it is not already showing.
|
||||
*/
|
||||
showClearButton: function() {
|
||||
if (!this.$el.hasClass('is-set')) {
|
||||
this.$el.addClass('is-set');
|
||||
this.getClearButton().removeClass('inactive');
|
||||
this.getClearButton().addClass('active');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the clear button.
|
||||
*/
|
||||
getClearButton: function () {
|
||||
return this.$el.find('.setting-clear');
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders the editor, updating the value displayed in the view, as well as the state of
|
||||
* the clear button.
|
||||
*/
|
||||
render: function () {
|
||||
if (!this.template) return;
|
||||
|
||||
this.setValueInEditor(this.model.getDisplayValue());
|
||||
|
||||
if (this.model.isExplicitlySet()) {
|
||||
this.showClearButton();
|
||||
}
|
||||
else {
|
||||
this.$el.removeClass('is-set');
|
||||
this.getClearButton().addClass('inactive');
|
||||
this.getClearButton().removeClass('active');
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
Metadata.String = Metadata.AbstractEditor.extend({
|
||||
|
||||
events : {
|
||||
"change input" : "updateModel",
|
||||
"keypress .setting-input" : "showClearButton" ,
|
||||
"click .setting-clear" : "clear"
|
||||
},
|
||||
|
||||
templateName: "metadata-string-entry",
|
||||
|
||||
getValueFromEditor : function () {
|
||||
return this.$el.find('#' + this.uniqueId).val();
|
||||
},
|
||||
|
||||
setValueInEditor : function (value) {
|
||||
this.$el.find('input').val(value);
|
||||
}
|
||||
});
|
||||
|
||||
Metadata.Number = Metadata.AbstractEditor.extend({
|
||||
|
||||
events : {
|
||||
"change input" : "updateModel",
|
||||
"keypress .setting-input" : "keyPressed",
|
||||
"change .setting-input" : "changed",
|
||||
"click .setting-clear" : "clear"
|
||||
},
|
||||
|
||||
render: function () {
|
||||
Metadata.AbstractEditor.prototype.render.apply(this);
|
||||
if (!this.initialized) {
|
||||
var numToString = function (val) {
|
||||
return val.toFixed(4);
|
||||
};
|
||||
var min = "min";
|
||||
var max = "max";
|
||||
var step = "step";
|
||||
var options = this.model.getOptions();
|
||||
if (options.hasOwnProperty(min)) {
|
||||
this.min = Number(options[min]);
|
||||
this.$el.find('input').attr(min, numToString(this.min));
|
||||
}
|
||||
if (options.hasOwnProperty(max)) {
|
||||
this.max = Number(options[max]);
|
||||
this.$el.find('input').attr(max, numToString(this.max));
|
||||
}
|
||||
var stepValue = undefined;
|
||||
if (options.hasOwnProperty(step)) {
|
||||
// Parse step and convert to String. Polyfill doesn't like float values like ".1" (expects "0.1").
|
||||
stepValue = numToString(Number(options[step]));
|
||||
}
|
||||
else if (this.isIntegerField()) {
|
||||
stepValue = "1";
|
||||
}
|
||||
if (stepValue !== undefined) {
|
||||
this.$el.find('input').attr(step, stepValue);
|
||||
}
|
||||
|
||||
// Manually runs polyfill for input number types to correct for Firefox non-support.
|
||||
// inputNumber will be undefined when unit test is running.
|
||||
if ($.fn.inputNumber) {
|
||||
this.$el.find('.setting-input-number').inputNumber();
|
||||
}
|
||||
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
templateName: "metadata-number-entry",
|
||||
|
||||
getValueFromEditor : function () {
|
||||
return this.$el.find('#' + this.uniqueId).val();
|
||||
},
|
||||
|
||||
setValueInEditor : function (value) {
|
||||
this.$el.find('input').val(value);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if this view is restricted to integers, as opposed to floating points values.
|
||||
*/
|
||||
isIntegerField : function () {
|
||||
return this.model.getType() === 'Integer';
|
||||
},
|
||||
|
||||
keyPressed: function (e) {
|
||||
this.showClearButton();
|
||||
// This first filtering if statement is take from polyfill to prevent
|
||||
// non-numeric input (for browsers that don't use polyfill because they DO have a number input type).
|
||||
var _ref, _ref1;
|
||||
if (((_ref = e.keyCode) !== 8 && _ref !== 9 && _ref !== 35 && _ref !== 36 && _ref !== 37 && _ref !== 39) &&
|
||||
((_ref1 = e.which) !== 45 && _ref1 !== 46 && _ref1 !== 48 && _ref1 !== 49 && _ref1 !== 50 && _ref1 !== 51
|
||||
&& _ref1 !== 52 && _ref1 !== 53 && _ref1 !== 54 && _ref1 !== 55 && _ref1 !== 56 && _ref1 !== 57)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
// For integers, prevent decimal points.
|
||||
if (this.isIntegerField() && e.keyCode === 46) {
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
|
||||
changed: function () {
|
||||
// Limit value to the range specified by min and max (necessary for browsers that aren't using polyfill).
|
||||
var value = this.getValueFromEditor();
|
||||
if ((this.max !== undefined) && value > this.max) {
|
||||
value = this.max;
|
||||
} else if ((this.min != undefined) && value < this.min) {
|
||||
value = this.min;
|
||||
}
|
||||
this.setValueInEditor(value);
|
||||
this.updateModel();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Metadata.Option = Metadata.AbstractEditor.extend({
|
||||
|
||||
events : {
|
||||
"change select" : "updateModel",
|
||||
"click .setting-clear" : "clear"
|
||||
},
|
||||
|
||||
templateName: "metadata-option-entry",
|
||||
|
||||
getValueFromEditor : function () {
|
||||
var selectedText = this.$el.find('#' + this.uniqueId).find(":selected").text();
|
||||
var selectedValue;
|
||||
_.each(this.model.getOptions(), function (modelValue) {
|
||||
if (modelValue === selectedText) {
|
||||
selectedValue = modelValue;
|
||||
}
|
||||
else if (modelValue['display_name'] === selectedText) {
|
||||
selectedValue = modelValue['value'];
|
||||
}
|
||||
});
|
||||
return selectedValue;
|
||||
},
|
||||
|
||||
setValueInEditor : function (value) {
|
||||
// Value here is the json value as used by the field. The choice may instead be showing display names.
|
||||
// Find the display name matching the value passed in.
|
||||
_.each(this.model.getOptions(), function (modelValue) {
|
||||
if (modelValue['value'] === value) {
|
||||
value = modelValue['display_name'];
|
||||
}
|
||||
});
|
||||
this.$el.find('#' + this.uniqueId + " option").filter(function() {
|
||||
return $(this).text() === value;
|
||||
}).prop('selected', true);
|
||||
}
|
||||
});
|
||||
|
||||
Metadata.List = Metadata.AbstractEditor.extend({
|
||||
|
||||
events : {
|
||||
"click .setting-clear" : "clear",
|
||||
"keypress .setting-input" : "showClearButton",
|
||||
"change input" : "updateModel",
|
||||
"input input" : "enableAdd",
|
||||
"click .create-setting" : "addEntry",
|
||||
"click .remove-setting" : "removeEntry"
|
||||
},
|
||||
|
||||
templateName: "metadata-list-entry",
|
||||
|
||||
getValueFromEditor: function () {
|
||||
return _.map(
|
||||
this.$el.find('li input'),
|
||||
function (ele) { return ele.value.trim(); }
|
||||
).filter(_.identity);
|
||||
},
|
||||
|
||||
setValueInEditor: function (value) {
|
||||
var list = this.$el.find('ol');
|
||||
list.empty();
|
||||
_.each(value, function(ele, index) {
|
||||
var template = _.template(
|
||||
'<li class="list-settings-item">' +
|
||||
'<input type="text" class="input" value="<%= ele %>">' +
|
||||
'<a href="#" class="remove-action remove-setting" data-index="<%= index %>"><i class="icon-remove-sign"></i><span class="sr">Remove</span></a>' +
|
||||
'</li>'
|
||||
);
|
||||
list.append($(template({'ele': ele, 'index': index})));
|
||||
});
|
||||
},
|
||||
|
||||
addEntry: function(event) {
|
||||
event.preventDefault();
|
||||
// We don't call updateModel here since it's bound to the
|
||||
// change event
|
||||
var list = this.model.get('value') || [];
|
||||
this.setValueInEditor(list.concat(['']))
|
||||
this.$el.find('.create-setting').addClass('is-disabled');
|
||||
},
|
||||
|
||||
removeEntry: function(event) {
|
||||
event.preventDefault();
|
||||
var entry = $(event.currentTarget).siblings().val();
|
||||
this.setValueInEditor(_.without(this.model.get('value'), entry));
|
||||
this.updateModel();
|
||||
this.$el.find('.create-setting').removeClass('is-disabled');
|
||||
},
|
||||
|
||||
enableAdd: function() {
|
||||
this.$el.find('.create-setting').removeClass('is-disabled');
|
||||
}
|
||||
});
|
||||
|
||||
return Metadata;
|
||||
});
|
||||
@@ -1,371 +0,0 @@
|
||||
if (!CMS.Views['Metadata']) CMS.Views.Metadata = {};
|
||||
|
||||
CMS.Views.Metadata.Editor = Backbone.View.extend({
|
||||
|
||||
// Model is CMS.Models.MetadataCollection,
|
||||
initialize : function() {
|
||||
var tpl = $("#metadata-editor-tpl").text();
|
||||
if(!tpl) {
|
||||
console.error("Couldn't load metadata editor template");
|
||||
}
|
||||
this.template = _.template(tpl);
|
||||
|
||||
this.$el.html(this.template({numEntries: this.collection.length}));
|
||||
var counter = 0;
|
||||
|
||||
var self = this;
|
||||
this.collection.each(
|
||||
function (model) {
|
||||
var data = {
|
||||
el: self.$el.find('.metadata_entry')[counter++],
|
||||
model: model
|
||||
};
|
||||
if (model.getType() === CMS.Models.Metadata.SELECT_TYPE) {
|
||||
new CMS.Views.Metadata.Option(data);
|
||||
}
|
||||
else if (model.getType() === CMS.Models.Metadata.INTEGER_TYPE ||
|
||||
model.getType() === CMS.Models.Metadata.FLOAT_TYPE) {
|
||||
new CMS.Views.Metadata.Number(data);
|
||||
}
|
||||
else if(model.getType() === CMS.Models.Metadata.LIST_TYPE) {
|
||||
new CMS.Views.Metadata.List(data);
|
||||
}
|
||||
else {
|
||||
// Everything else is treated as GENERIC_TYPE, which uses String editor.
|
||||
new CMS.Views.Metadata.String(data);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the just the modified metadata values, in the format used to persist to the server.
|
||||
*/
|
||||
getModifiedMetadataValues: function () {
|
||||
var modified_values = {};
|
||||
this.collection.each(
|
||||
function (model) {
|
||||
if (model.isModified()) {
|
||||
modified_values[model.getFieldName()] = model.getValue();
|
||||
}
|
||||
}
|
||||
);
|
||||
return modified_values;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a display name for the component related to this metadata. This method looks to see
|
||||
* if there is a metadata entry called 'display_name', and if so, it returns its value. If there
|
||||
* is no such entry, or if display_name does not have a value set, it returns an empty string.
|
||||
*/
|
||||
getDisplayName: function () {
|
||||
var displayName = '';
|
||||
this.collection.each(
|
||||
function (model) {
|
||||
if (model.get('field_name') === 'display_name') {
|
||||
var displayNameValue = model.get('value');
|
||||
// It is possible that there is no display name value set. In that case, return empty string.
|
||||
displayName = displayNameValue ? displayNameValue : '';
|
||||
}
|
||||
}
|
||||
);
|
||||
return displayName;
|
||||
}
|
||||
});
|
||||
|
||||
CMS.Views.Metadata.AbstractEditor = Backbone.View.extend({
|
||||
|
||||
// Model is CMS.Models.Metadata.
|
||||
initialize : function() {
|
||||
var self = this;
|
||||
var templateName = _.result(this, 'templateName');
|
||||
// Backbone model cid is only unique within the collection.
|
||||
this.uniqueId = _.uniqueId(templateName + "_");
|
||||
|
||||
var tpl = document.getElementById(templateName).text;
|
||||
if(!tpl) {
|
||||
console.error("Couldn't load template: " + templateName);
|
||||
}
|
||||
this.template = _.template(tpl);
|
||||
this.$el.html(this.template({model: this.model, uniqueId: this.uniqueId}));
|
||||
this.listenTo(this.model, 'change', this.render);
|
||||
this.render();
|
||||
},
|
||||
|
||||
/**
|
||||
* The ID/name of the template. Subclasses must override this.
|
||||
*/
|
||||
templateName: '',
|
||||
|
||||
/**
|
||||
* Returns the value currently displayed in the editor/view. Subclasses should implement this method.
|
||||
*/
|
||||
getValueFromEditor : function () {},
|
||||
|
||||
/**
|
||||
* Sets the value currently displayed in the editor/view. Subclasses should implement this method.
|
||||
*/
|
||||
setValueInEditor : function (value) {},
|
||||
|
||||
/**
|
||||
* Sets the value in the model, using the value currently displayed in the view.
|
||||
*/
|
||||
updateModel: function () {
|
||||
this.model.setValue(this.getValueFromEditor());
|
||||
},
|
||||
|
||||
/**
|
||||
* Clears the value currently set in the model (reverting to the default).
|
||||
*/
|
||||
clear: function () {
|
||||
this.model.clear();
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows the clear button, if it is not already showing.
|
||||
*/
|
||||
showClearButton: function() {
|
||||
if (!this.$el.hasClass('is-set')) {
|
||||
this.$el.addClass('is-set');
|
||||
this.getClearButton().removeClass('inactive');
|
||||
this.getClearButton().addClass('active');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the clear button.
|
||||
*/
|
||||
getClearButton: function () {
|
||||
return this.$el.find('.setting-clear');
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders the editor, updating the value displayed in the view, as well as the state of
|
||||
* the clear button.
|
||||
*/
|
||||
render: function () {
|
||||
if (!this.template) return;
|
||||
|
||||
this.setValueInEditor(this.model.getDisplayValue());
|
||||
|
||||
if (this.model.isExplicitlySet()) {
|
||||
this.showClearButton();
|
||||
}
|
||||
else {
|
||||
this.$el.removeClass('is-set');
|
||||
this.getClearButton().addClass('inactive');
|
||||
this.getClearButton().removeClass('active');
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
CMS.Views.Metadata.String = CMS.Views.Metadata.AbstractEditor.extend({
|
||||
|
||||
events : {
|
||||
"change input" : "updateModel",
|
||||
"keypress .setting-input" : "showClearButton" ,
|
||||
"click .setting-clear" : "clear"
|
||||
},
|
||||
|
||||
templateName: "metadata-string-entry",
|
||||
|
||||
getValueFromEditor : function () {
|
||||
return this.$el.find('#' + this.uniqueId).val();
|
||||
},
|
||||
|
||||
setValueInEditor : function (value) {
|
||||
this.$el.find('input').val(value);
|
||||
}
|
||||
});
|
||||
|
||||
CMS.Views.Metadata.Number = CMS.Views.Metadata.AbstractEditor.extend({
|
||||
|
||||
events : {
|
||||
"change input" : "updateModel",
|
||||
"keypress .setting-input" : "keyPressed",
|
||||
"change .setting-input" : "changed",
|
||||
"click .setting-clear" : "clear"
|
||||
},
|
||||
|
||||
render: function () {
|
||||
CMS.Views.Metadata.AbstractEditor.prototype.render.apply(this);
|
||||
if (!this.initialized) {
|
||||
var numToString = function (val) {
|
||||
return val.toFixed(4);
|
||||
};
|
||||
var min = "min";
|
||||
var max = "max";
|
||||
var step = "step";
|
||||
var options = this.model.getOptions();
|
||||
if (options.hasOwnProperty(min)) {
|
||||
this.min = Number(options[min]);
|
||||
this.$el.find('input').attr(min, numToString(this.min));
|
||||
}
|
||||
if (options.hasOwnProperty(max)) {
|
||||
this.max = Number(options[max]);
|
||||
this.$el.find('input').attr(max, numToString(this.max));
|
||||
}
|
||||
var stepValue = undefined;
|
||||
if (options.hasOwnProperty(step)) {
|
||||
// Parse step and convert to String. Polyfill doesn't like float values like ".1" (expects "0.1").
|
||||
stepValue = numToString(Number(options[step]));
|
||||
}
|
||||
else if (this.isIntegerField()) {
|
||||
stepValue = "1";
|
||||
}
|
||||
if (stepValue !== undefined) {
|
||||
this.$el.find('input').attr(step, stepValue);
|
||||
}
|
||||
|
||||
// Manually runs polyfill for input number types to correct for Firefox non-support.
|
||||
// inputNumber will be undefined when unit test is running.
|
||||
if ($.fn.inputNumber) {
|
||||
this.$el.find('.setting-input-number').inputNumber();
|
||||
}
|
||||
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
templateName: "metadata-number-entry",
|
||||
|
||||
getValueFromEditor : function () {
|
||||
return this.$el.find('#' + this.uniqueId).val();
|
||||
},
|
||||
|
||||
setValueInEditor : function (value) {
|
||||
this.$el.find('input').val(value);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if this view is restricted to integers, as opposed to floating points values.
|
||||
*/
|
||||
isIntegerField : function () {
|
||||
return this.model.getType() === 'Integer';
|
||||
},
|
||||
|
||||
keyPressed: function (e) {
|
||||
this.showClearButton();
|
||||
// This first filtering if statement is take from polyfill to prevent
|
||||
// non-numeric input (for browsers that don't use polyfill because they DO have a number input type).
|
||||
var _ref, _ref1;
|
||||
if (((_ref = e.keyCode) !== 8 && _ref !== 9 && _ref !== 35 && _ref !== 36 && _ref !== 37 && _ref !== 39) &&
|
||||
((_ref1 = e.which) !== 45 && _ref1 !== 46 && _ref1 !== 48 && _ref1 !== 49 && _ref1 !== 50 && _ref1 !== 51
|
||||
&& _ref1 !== 52 && _ref1 !== 53 && _ref1 !== 54 && _ref1 !== 55 && _ref1 !== 56 && _ref1 !== 57)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
// For integers, prevent decimal points.
|
||||
if (this.isIntegerField() && e.keyCode === 46) {
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
|
||||
changed: function () {
|
||||
// Limit value to the range specified by min and max (necessary for browsers that aren't using polyfill).
|
||||
var value = this.getValueFromEditor();
|
||||
if ((this.max !== undefined) && value > this.max) {
|
||||
value = this.max;
|
||||
} else if ((this.min != undefined) && value < this.min) {
|
||||
value = this.min;
|
||||
}
|
||||
this.setValueInEditor(value);
|
||||
this.updateModel();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
CMS.Views.Metadata.Option = CMS.Views.Metadata.AbstractEditor.extend({
|
||||
|
||||
events : {
|
||||
"change select" : "updateModel",
|
||||
"click .setting-clear" : "clear"
|
||||
},
|
||||
|
||||
templateName: "metadata-option-entry",
|
||||
|
||||
getValueFromEditor : function () {
|
||||
var selectedText = this.$el.find('#' + this.uniqueId).find(":selected").text();
|
||||
var selectedValue;
|
||||
_.each(this.model.getOptions(), function (modelValue) {
|
||||
if (modelValue === selectedText) {
|
||||
selectedValue = modelValue;
|
||||
}
|
||||
else if (modelValue['display_name'] === selectedText) {
|
||||
selectedValue = modelValue['value'];
|
||||
}
|
||||
});
|
||||
return selectedValue;
|
||||
},
|
||||
|
||||
setValueInEditor : function (value) {
|
||||
// Value here is the json value as used by the field. The choice may instead be showing display names.
|
||||
// Find the display name matching the value passed in.
|
||||
_.each(this.model.getOptions(), function (modelValue) {
|
||||
if (modelValue['value'] === value) {
|
||||
value = modelValue['display_name'];
|
||||
}
|
||||
});
|
||||
this.$el.find('#' + this.uniqueId + " option").filter(function() {
|
||||
return $(this).text() === value;
|
||||
}).prop('selected', true);
|
||||
}
|
||||
});
|
||||
|
||||
CMS.Views.Metadata.List = CMS.Views.Metadata.AbstractEditor.extend({
|
||||
|
||||
events : {
|
||||
"click .setting-clear" : "clear",
|
||||
"keypress .setting-input" : "showClearButton",
|
||||
"change input" : "updateModel",
|
||||
"input input" : "enableAdd",
|
||||
"click .create-setting" : "addEntry",
|
||||
"click .remove-setting" : "removeEntry"
|
||||
},
|
||||
|
||||
templateName: "metadata-list-entry",
|
||||
|
||||
getValueFromEditor: function () {
|
||||
return _.map(
|
||||
this.$el.find('li input'),
|
||||
function (ele) { return ele.value.trim(); }
|
||||
).filter(_.identity);
|
||||
},
|
||||
|
||||
setValueInEditor: function (value) {
|
||||
var list = this.$el.find('ol');
|
||||
list.empty();
|
||||
_.each(value, function(ele, index) {
|
||||
var template = _.template(
|
||||
'<li class="list-settings-item">' +
|
||||
'<input type="text" class="input" value="<%= ele %>">' +
|
||||
'<a href="#" class="remove-action remove-setting" data-index="<%= index %>"><i class="icon-remove-sign"></i><span class="sr">Remove</span></a>' +
|
||||
'</li>'
|
||||
);
|
||||
list.append($(template({'ele': ele, 'index': index})));
|
||||
});
|
||||
},
|
||||
|
||||
addEntry: function(event) {
|
||||
event.preventDefault();
|
||||
// We don't call updateModel here since it's bound to the
|
||||
// change event
|
||||
var list = this.model.get('value') || [];
|
||||
this.setValueInEditor(list.concat(['']))
|
||||
this.$el.find('.create-setting').addClass('is-disabled');
|
||||
},
|
||||
|
||||
removeEntry: function(event) {
|
||||
event.preventDefault();
|
||||
var entry = $(event.currentTarget).siblings().val();
|
||||
this.setValueInEditor(_.without(this.model.get('value'), entry));
|
||||
this.updateModel();
|
||||
this.$el.find('.create-setting').removeClass('is-disabled');
|
||||
},
|
||||
|
||||
enableAdd: function() {
|
||||
this.$el.find('.create-setting').removeClass('is-disabled');
|
||||
}
|
||||
});
|
||||
@@ -1,3 +1,6 @@
|
||||
require(["jquery", "jquery.ui", "gettext", "js/hesitate", "js/views/feedback_notification"],
|
||||
function($, ui, gettext, HesitateEvent, NotificationView) {
|
||||
|
||||
$(document).ready(function() {
|
||||
// making the unit list draggable. Note: sortable didn't work b/c it considered
|
||||
// drop points which the user hovered over as destinations and proactively changed
|
||||
@@ -64,10 +67,10 @@ $(document).ready(function() {
|
||||
|
||||
});
|
||||
|
||||
CMS.HesitateEvent.toggleXpandHesitation = null;
|
||||
HesitateEvent.toggleXpandHesitation = null;
|
||||
function initiateHesitate(event, ui) {
|
||||
CMS.HesitateEvent.toggleXpandHesitation = new CMS.HesitateEvent(expandSection, 'dragLeave', true);
|
||||
$('.collapsed').on('dragEnter', CMS.HesitateEvent.toggleXpandHesitation, CMS.HesitateEvent.toggleXpandHesitation.trigger);
|
||||
HesitateEvent.toggleXpandHesitation = new HesitateEvent(expandSection, 'dragLeave', true);
|
||||
$('.collapsed').on('dragEnter', HesitateEvent.toggleXpandHesitation, HesitateEvent.toggleXpandHesitation.trigger);
|
||||
$('.collapsed, .unit, .id-holder').each(function() {
|
||||
this.proportions = {width : this.offsetWidth, height : this.offsetHeight };
|
||||
// reset b/c these were holding values from aborts
|
||||
@@ -152,10 +155,10 @@ function generateCheckHoverState(selectorsToOpen, selectorsToShove) {
|
||||
}
|
||||
|
||||
function removeHesitate(event, ui) {
|
||||
$('.collapsed').off('dragEnter', CMS.HesitateEvent.toggleXpandHesitation.trigger);
|
||||
$('.collapsed').off('dragEnter', HesitateEvent.toggleXpandHesitation.trigger);
|
||||
$('.ui-dragging-pushdown').removeClass('ui-dragging-pushdown');
|
||||
$('.ui-dragging-pushup').removeClass('ui-dragging-pushup');
|
||||
CMS.HesitateEvent.toggleXpandHesitation = null;
|
||||
HesitateEvent.toggleXpandHesitation = null;
|
||||
}
|
||||
|
||||
function expandSection(event) {
|
||||
@@ -225,7 +228,7 @@ function _handleReorder(event, ui, parentIdField, childrenSelector) {
|
||||
ui.draggable.attr("style", "position:relative;"); // STYLE hack too
|
||||
children.push(ui.draggable.data('id'));
|
||||
}
|
||||
var saving = new CMS.Views.Notification.Mini({
|
||||
var saving = new NotificationView.Mini({
|
||||
title: gettext('Saving…')
|
||||
});
|
||||
saving.show();
|
||||
@@ -242,4 +245,4 @@ function _handleReorder(event, ui, parentIdField, childrenSelector) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
}); // end define()
|
||||
|
||||
78
cms/static/js/views/overview_assignment_grader.js
Normal file
78
cms/static/js/views/overview_assignment_grader.js
Normal file
@@ -0,0 +1,78 @@
|
||||
define(["backbone", "underscore", "gettext", "js/models/assignment_grade", "js/views/feedback_notification"],
|
||||
function(Backbone, _, gettext, AssignmentGrade, NotificationView) {
|
||||
var OverviewAssignmentGrader = Backbone.View.extend({
|
||||
// instantiate w/ { graders : CourseGraderCollection, el : <the gradable-status div> }
|
||||
events : {
|
||||
"click .menu-toggle" : "showGradeMenu",
|
||||
"click .menu li" : "selectGradeType"
|
||||
},
|
||||
initialize : function() {
|
||||
// call template w/ {assignmentType : formatname, graders : CourseGraderCollection instance }
|
||||
this.template = _.template(
|
||||
// TODO move to a template file
|
||||
'<h4 class="status-label"><%= assignmentType %></h4>' +
|
||||
'<a data-tooltip="Mark/unmark this subsection as graded" class="menu-toggle" href="#">' +
|
||||
'<% if (!hideSymbol) {%><i class="icon-ok"></i><%};%>' +
|
||||
'</a>' +
|
||||
'<ul class="menu">' +
|
||||
'<% graders.each(function(option) { %>' +
|
||||
'<li><a <% if (option.get("type") == assignmentType) {%>class="is-selected" <%}%> href="#"><%= option.get("type") %></a></li>' +
|
||||
'<% }) %>' +
|
||||
'<li><a class="gradable-status-notgraded" href="#">Not Graded</a></li>' +
|
||||
'</ul>');
|
||||
this.assignmentGrade = new AssignmentGrade({
|
||||
assignmentUrl : this.$el.closest('.id-holder').data('id'),
|
||||
graderType : this.$el.data('initial-status')});
|
||||
// TODO throw exception if graders is null
|
||||
this.graders = this.options['graders'];
|
||||
var cachethis = this;
|
||||
// defining here to get closure around this
|
||||
this.removeMenu = function(e) {
|
||||
e.preventDefault();
|
||||
cachethis.$el.removeClass('is-active');
|
||||
$(document).off('click', cachethis.removeMenu);
|
||||
};
|
||||
this.hideSymbol = this.options['hideSymbol'];
|
||||
this.render();
|
||||
},
|
||||
render : function() {
|
||||
this.$el.html(this.template({ assignmentType : this.assignmentGrade.get('graderType'), graders : this.graders,
|
||||
hideSymbol : this.hideSymbol }));
|
||||
if (this.assignmentGrade.has('graderType') && this.assignmentGrade.get('graderType') != "Not Graded") {
|
||||
this.$el.addClass('is-set');
|
||||
}
|
||||
else {
|
||||
this.$el.removeClass('is-set');
|
||||
}
|
||||
},
|
||||
showGradeMenu : function(e) {
|
||||
e.preventDefault();
|
||||
// I sure hope this doesn't break anything but it's needed to keep the removeMenu from activating
|
||||
e.stopPropagation();
|
||||
// nasty global event trap :-(
|
||||
$(document).on('click', this.removeMenu);
|
||||
this.$el.addClass('is-active');
|
||||
},
|
||||
selectGradeType : function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
this.removeMenu(e);
|
||||
|
||||
var saving = new NotificationView.Mini({
|
||||
title: gettext('Saving') + '…'
|
||||
});
|
||||
saving.show();
|
||||
|
||||
// TODO I'm not happy with this string fetch via the html for what should be an id. I'd rather use the id attr
|
||||
// of the CourseGradingPolicy model or null for Not Graded (NOTE, change template's if check for is-selected accordingly)
|
||||
this.assignmentGrade.save(
|
||||
'graderType',
|
||||
$(e.target).text(),
|
||||
{success: function () { saving.hide(); }}
|
||||
);
|
||||
|
||||
this.render();
|
||||
}
|
||||
});
|
||||
return OverviewAssignmentGrader;
|
||||
});
|
||||
@@ -1,85 +0,0 @@
|
||||
CMS.Views.SectionShow = Backbone.View.extend({
|
||||
template: _.template('<span data-tooltip="<%= gettext("Edit this section\'s name") %>" class="section-name-span"><%= name %></span>'),
|
||||
render: function() {
|
||||
var attrs = {
|
||||
name: this.model.escape('name')
|
||||
};
|
||||
this.$el.html(this.template(attrs));
|
||||
this.delegateEvents();
|
||||
return this;
|
||||
},
|
||||
events: {
|
||||
"click": "switchToEditView"
|
||||
},
|
||||
switchToEditView: function() {
|
||||
if(!this.editView) {
|
||||
this.editView = new CMS.Views.SectionEdit({
|
||||
model: this.model, el: this.el, showView: this});
|
||||
}
|
||||
this.undelegateEvents();
|
||||
this.editView.render();
|
||||
}
|
||||
});
|
||||
|
||||
CMS.Views.SectionEdit = Backbone.View.extend({
|
||||
render: function() {
|
||||
var attrs = {
|
||||
name: this.model.escape('name')
|
||||
};
|
||||
this.$el.html(this.template(attrs));
|
||||
this.delegateEvents();
|
||||
return this;
|
||||
},
|
||||
initialize: function() {
|
||||
this.template = _.template($("#section-name-edit-tpl").text());
|
||||
this.listenTo(this.model, "invalid", this.showInvalidMessage);
|
||||
this.render();
|
||||
},
|
||||
events: {
|
||||
"click .save-button": "saveName",
|
||||
"submit": "saveName",
|
||||
"click .cancel-button": "switchToShowView"
|
||||
},
|
||||
saveName: function(e) {
|
||||
if (e) { e.preventDefault(); }
|
||||
var name = this.$("input[type=text]").val();
|
||||
var that = this;
|
||||
this.model.save("name", name, {
|
||||
success: function() {
|
||||
analytics.track('Edited Section Name', {
|
||||
'course': course_location_analytics,
|
||||
'display_name': that.model.get('name'),
|
||||
'id': that.model.get('id')
|
||||
});
|
||||
that.switchToShowView();
|
||||
}
|
||||
});
|
||||
},
|
||||
switchToShowView: function() {
|
||||
if(!this.showView) {
|
||||
this.showView = new CMS.Views.SectionShow({
|
||||
model: this.model, el: this.el, editView: this});
|
||||
}
|
||||
this.undelegateEvents();
|
||||
this.stopListening();
|
||||
this.showView.render();
|
||||
},
|
||||
showInvalidMessage: function(model, error, options) {
|
||||
model.set("name", model.previous("name"));
|
||||
var that = this;
|
||||
var prompt = new CMS.Views.Prompt.Error({
|
||||
title: gettext("Your change could not be saved"),
|
||||
message: error,
|
||||
actions: {
|
||||
primary: {
|
||||
text: gettext("Return and resolve this issue"),
|
||||
click: function(view) {
|
||||
view.hide();
|
||||
that.$("input[type=text]").focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
prompt.show();
|
||||
}
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user