Commit 715aa698 by David Baumgold

Merge pull request #662 from edx/db/requirejs

requirejs in Studio
parents a47442ed d97921e6
......@@ -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
......
......@@ -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')
......
......@@ -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
#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:
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'
......
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"
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"
])
describe "CMS", ->
beforeEach ->
CMS.unbind()
it "should initialize Models", ->
expect(CMS.Models).toBeDefined()
it "should initialize Views", ->
expect(CMS.Views).toBeDefined()
require ["jquery", "backbone", "coffee/src/main", "sinon", "jasmine-stealth"],
($, Backbone, main, sinon) ->
describe "CMS", ->
it "should initialize URL", ->
expect(window.CMS.URL).toBeDefined()
describe "main helper", ->
describe "main helper", ->
beforeEach ->
@previousAjaxSettings = $.extend(true, {}, $.ajaxSettings)
window.stubCookies["csrftoken"] = "stubCSRFToken"
$(document).ready()
spyOn($, "cookie")
$.cookie.when("csrftoken").thenReturn("stubCSRFToken")
main()
afterEach ->
$.ajaxSettings = @previousAjaxSettings
......@@ -23,7 +20,7 @@ describe "main helper", ->
it "setup AJAX CSRF token", ->
expect($.ajaxSettings.headers["X-CSRFToken"]).toEqual("stubCSRFToken")
describe "AJAX Errors", ->
describe "AJAX Errors", ->
tpl = readFixtures('system-feedback.underscore')
beforeEach ->
......
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"
])
describe "CMS.Models.Course", ->
define ["js/models/course"], (Course) ->
describe "Course", ->
describe "basic", ->
beforeEach ->
@model = new CMS.Models.Course({
@model = new Course({
name: "Greek Hero"
})
......
describe "CMS.Models.Metadata", ->
define ["js/models/metadata"], (Metadata) ->
describe "Metadata", ->
it "knows when the value has not been modified", ->
model = new CMS.Models.Metadata(
model = new Metadata(
{'value': 'original', 'explicitly_set': false})
expect(model.isModified()).toBeFalsy()
model = new CMS.Models.Metadata(
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(
model = new Metadata(
{'value': 'original', 'explicitly_set': false})
model.setValue('original')
expect(model.isModified()).toBeTruthy()
model = new CMS.Models.Metadata(
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(
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(
model = new Metadata(
{'value': 'default', 'explicitly_set': false})
expect(model.getValue()).toBeNull
expect(model.getDisplayValue()).toBe('default')
......@@ -37,7 +38,7 @@ describe "CMS.Models.Metadata", ->
expect(model.getDisplayValue()).toBe('modified')
it "has a clear method for reverting to the default", ->
model = new CMS.Models.Metadata(
model = new Metadata(
{'value': 'original', 'default_value' : 'default', 'explicitly_set': true})
model.clear()
expect(model.getValue()).toBeNull
......@@ -45,14 +46,14 @@ describe "CMS.Models.Metadata", ->
expect(model.isExplicitlySet()).toBeFalsy()
it "has a getter for field name", ->
model = new CMS.Models.Metadata({'field_name': 'foo'})
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']})
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)
model = new Metadata({'type': 'Integer'})
expect(model.getType()).toBe(Metadata.INTEGER_TYPE)
describe "CMS.Models.Module", ->
define ["coffee/src/models/module"], (Module) ->
describe "Module", ->
it "set the correct URL", ->
expect(new CMS.Models.Module().url).toEqual("/save_item")
expect(new Module().url).toEqual("/save_item")
it "set the correct default", ->
expect(new CMS.Models.Module().defaults).toEqual(undefined)
expect(new Module().defaults).toEqual(undefined)
describe "CMS.Models.Section", ->
define ["js/models/section", "sinon"], (Section, sinon) ->
describe "Section", ->
describe "basic", ->
beforeEach ->
@model = new CMS.Models.Section({
@model = new Section({
id: 42,
name: "Life, the Universe, and Everything"
})
......@@ -18,16 +19,17 @@ describe "CMS.Models.Section", ->
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({
spyOn(Section.prototype, 'showNotification')
spyOn(Section.prototype, 'hideNotification')
@model = new Section({
id: 42,
name: "Life, the Universe, and Everything"
})
......@@ -40,14 +42,12 @@ describe "CMS.Models.Section", ->
it "show/hide a notification when it saves to the server", ->
@model.save()
expect(CMS.Models.Section.prototype.showNotification).toHaveBeenCalled()
expect(Section.prototype.showNotification).toHaveBeenCalled()
@requests[0].respond(200)
expect(CMS.Models.Section.prototype.hideNotification).toHaveBeenCalled()
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(CMS.Models.Section.prototype.hideNotification).not.toHaveBeenCalled()
expect(Section.prototype.hideNotification).not.toHaveBeenCalled()
describe "CMS.Models.Settings.CourseGradingPolicy", ->
define ["js/models/settings/course_grading_policy"], (CourseGradingPolicy) ->
describe "CourseGradingPolicy", ->
beforeEach ->
@model = new CMS.Models.Settings.CourseGradingPolicy()
@model = new CourseGradingPolicy()
describe "parse", ->
it "sets a null grace period to 00:00", ->
......
beforeEach ->
define ["backbone", "js/models/textbook", "js/collections/textbook", "js/models/chapter", "js/collections/chapter", "coffee/src/main"],
(Backbone, Textbook, TextbookSet, Chapter, ChapterSet, main) ->
beforeEach ->
@addMatchers
toBeInstanceOf: (expected) ->
return @actual instanceof expected
describe "CMS.Models.Textbook", ->
describe "Textbook model", ->
beforeEach ->
@model = new CMS.Models.Textbook()
main()
@model = new Textbook()
describe "Basic", ->
it "should have an empty name by default", ->
......@@ -17,7 +21,7 @@ describe "CMS.Models.Textbook", ->
it "should have a ChapterSet with one chapter by default", ->
chapters = @model.get("chapters")
expect(chapters).toBeInstanceOf(CMS.Collections.ChapterSet)
expect(chapters).toBeInstanceOf(ChapterSet)
expect(chapters.length).toEqual(1)
expect(chapters.at(0).isEmpty()).toBeTruthy()
......@@ -84,48 +88,48 @@ describe "CMS.Models.Textbook", ->
]
}
model = new CMS.Models.Textbook(serverModelSpec, {parse: true})
model = new 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: ""})
model = new Textbook({name: ""})
expect(model.isValid()).toBeFalsy()
it "requires at least one chapter", ->
model = new CMS.Models.Textbook({name: "foo"})
model = new Textbook({name: "foo"})
model.get("chapters").reset()
expect(model.isValid()).toBeFalsy()
it "requires a valid chapter", ->
chapter = new CMS.Models.Chapter()
chapter = new Chapter()
chapter.isValid = -> false
model = new CMS.Models.Textbook({name: "foo"})
model = new 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 = new Chapter()
chapter1.isValid = -> true
chapter2 = new CMS.Models.Chapter()
chapter2 = new Chapter()
chapter2.isValid = -> false
model = new CMS.Models.Textbook({name: "foo"})
model = new Textbook({name: "foo"})
model.get("chapters").reset([chapter1, chapter2])
expect(model.isValid()).toBeFalsy()
it "can pass validation", ->
chapter = new CMS.Models.Chapter()
chapter = new Chapter()
chapter.isValid = -> true
model = new CMS.Models.Textbook({name: "foo"})
model = new Textbook({name: "foo"})
model.get("chapters").reset([chapter])
expect(model.isValid()).toBeTruthy()
describe "CMS.Collections.TextbookSet", ->
describe "Textbook collection", ->
beforeEach ->
CMS.URL.TEXTBOOKS = "/textbooks"
@collection = new CMS.Collections.TextbookSet()
@collection = new TextbookSet()
afterEach ->
delete CMS.URL.TEXTBOOKS
......@@ -139,9 +143,9 @@ describe "CMS.Collections.TextbookSet", ->
expect(@collection.sync).toHaveBeenCalledWith("update", @collection, undefined)
describe "CMS.Models.Chapter", ->
describe "Chapter model", ->
beforeEach ->
@model = new CMS.Models.Chapter()
@model = new Chapter()
describe "Basic", ->
it "should have a name by default", ->
......@@ -158,21 +162,21 @@ describe "CMS.Models.Chapter", ->
describe "Validation", ->
it "requires a name", ->
model = new CMS.Models.Chapter({name: "", asset_path: "a.pdf"})
model = new Chapter({name: "", asset_path: "a.pdf"})
expect(model.isValid()).toBeFalsy()
it "requires an asset_path", ->
model = new CMS.Models.Chapter({name: "a", asset_path: ""})
model = new Chapter({name: "a", asset_path: ""})
expect(model.isValid()).toBeFalsy()
it "can pass validation", ->
model = new CMS.Models.Chapter({name: "a", asset_path: "a.pdf"})
model = new Chapter({name: "a", asset_path: "a.pdf"})
expect(model.isValid()).toBeTruthy()
describe "CMS.Collections.ChapterSet", ->
describe "Chapter collection", ->
beforeEach ->
@collection = new CMS.Collections.ChapterSet()
@collection = new ChapterSet()
it "is empty by default", ->
expect(@collection.isEmpty()).toBeTruthy()
......
describe "CMS.Models.FileUpload", ->
define ["js/models/uploads"], (FileUpload) ->
describe "FileUpload", ->
beforeEach ->
@model = new CMS.Models.FileUpload()
@model = new FileUpload()
it "is unfinished by default", ->
expect(@model.get("finished")).toBeFalsy()
......
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"]
feedbackTpl = readFixtures('system-feedback.underscore')
assetTpl = readFixtures('asset.underscore')
define ["jasmine", "sinon", "squire"],
(jasmine, sinon, Squire) ->
describe "CMS.Views.Asset", ->
feedbackTpl = readFixtures('system-feedback.underscore')
assetTpl = readFixtures('asset.underscore')
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"}))
@model = new CMS.Models.Asset({display_name: "test asset", url: 'actual_asset_url', portable_url: 'portable_url', date_added: 'date', thumbnail: null, id: 'id'})
@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 CMS.Models.AssetCollection([@model])
@collection = new AssetCollection([@model])
@collection.url = "update-asset-url"
@view = new CMS.Views.Asset({model: @model})
@view = new AssetView({model: @model})
@promptSpies = spyOnConstructor(CMS.Views.Prompt, "Warning", ["show", "hide"])
@promptSpies.show.andReturn(@promptSpies)
waitsFor (=> @view), "AssetView was not created", 1000
afterEach ->
@injector.clean()
@injector.remove()
describe "Basic", ->
it "should render properly", ->
......@@ -37,12 +73,6 @@ describe "CMS.Views.Asset", ->
@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()
......@@ -99,23 +129,49 @@ describe "CMS.Views.Asset", ->
expect(@savingSpies.hide).not.toHaveBeenCalled()
expect(@model.get("locked")).toBeFalsy()
describe "CMS.Views.Assets", ->
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"}))
@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')})
@promptSpies = spyOnConstructor(CMS.Views.Prompt, "Warning", ["show", "hide"])
@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)
......@@ -124,6 +180,9 @@ describe "CMS.Views.Assets", ->
delete window.analytics
delete window.course_location_analytics
@injector.clean()
@injector.remove()
describe "Basic", ->
it "should render both assets", ->
@view.render()
......@@ -134,7 +193,7 @@ describe "CMS.Views.Assets", ->
# 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)
req.respond(200) for req in @requests
expect(@view.$el).toContainText("test asset 1")
expect(@view.$el).not.toContainText("test asset 2")
......@@ -142,13 +201,19 @@ describe "CMS.Views.Assets", ->
# 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)
req.respond(404) for req in @requests
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'})
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)
......
courseInfoPage = """
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) ->
courseInfoPage = """
<div class="course-info-wrapper">
<div class="main-column window">
<article class="course-updates" id="course-update-view">
......@@ -9,28 +12,28 @@ courseInfoPage = """
</div>
"""
commonSetup = () ->
beforeEach ->
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()
afterEach ->
delete window.analytics
delete window.course_location_analytics
describe "Course Updates", ->
describe "Course Updates", ->
courseInfoTemplate = readFixtures('course_info_update.underscore')
beforeEach ->
setFixtures($("<script>", {id: "course_info_update-tpl", type: "text/template"}).text(courseInfoTemplate))
appendSetFixtures courseInfoPage
@collection = new CMS.Models.CourseUpdateCollection()
@courseInfoEdit = new CMS.Views.ClassInfoUpdateView({
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/'
......@@ -49,10 +52,8 @@ describe "Course Updates", ->
spyOn(@courseInfoEdit.$codeMirror, 'getValue').andReturn('/static/image.jpg')
@courseInfoEdit.$el.find('.save-button').click()
@requests = commonSetup()
afterEach ->
commonCleanup()
@xhrRestore()
it "does not rewrite links on save", ->
# Create a new update, verifying that the model is created
......@@ -69,7 +70,7 @@ describe "Course Updates", ->
expect(model.save).toHaveBeenCalled()
# Verify content sent to server does not have rewritten links.
contentSaved = JSON.parse(this.requests[0].requestBody).content
contentSaved = JSON.parse(@courseUpdatesRequests[@courseUpdatesRequests.length - 1].requestBody).content
expect(contentSaved).toEqual('/static/image.jpg')
it "does rewrite links for preview", ->
......@@ -88,19 +89,24 @@ describe "Course Updates", ->
expect(@courseInfoEdit.$codeMirror.getValue()).toEqual('/static/image.jpg')
describe "Course Handouts", ->
describe "Course Handouts", ->
handoutsTemplate = readFixtures('course_info_handouts.underscore')
beforeEach ->
setFixtures($("<script>", {id: "course_info_handouts-tpl", type: "text/template"}).text(handoutsTemplate))
appendSetFixtures courseInfoPage
@model = new CMS.Models.ModuleInfo({
courseHandoutsXhr = sinon.useFakeXMLHttpRequest()
@handoutsRequests = requests = []
courseHandoutsXhr.onCreate = (xhr) -> requests.push(xhr)
@handoutsXhrRestore = courseHandoutsXhr.restore
@model = new ModuleInfo({
id: 'handouts-id',
data: '/static/fromServer.jpg'
})
@handoutsEdit = new CMS.Views.ClassInfoHandoutsView({
@handoutsEdit = new CourseInfoHandoutsView({
el: $('#course-handouts-view'),
model: @model,
base_asset_url: 'base-asset-url/'
......@@ -108,10 +114,8 @@ describe "Course Handouts", ->
@handoutsEdit.render()
@requests = commonSetup()
afterEach ->
commonCleanup()
@handoutsXhrRestore()
it "does not rewrite links on save", ->
# Enter something in the handouts section, verifying that the model is saved
......@@ -122,7 +126,7 @@ describe "Course Handouts", ->
@handoutsEdit.$el.find('.save-button').click()
expect(@model.save).toHaveBeenCalled()
contentSaved = JSON.parse(this.requests[0].requestBody).data
contentSaved = JSON.parse(@handoutsRequests[@handoutsRequests.length - 1].requestBody).data
expect(contentSaved).toEqual('/static/image.jpg')
it "does rewrite links in initial content", ->
......
verifyInputType = (input, 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.
......@@ -6,7 +8,7 @@ verifyInputType = (input, expectedType) ->
expectedType = 'text'
expect(input.type).toBe(expectedType)
describe "Test Metadata Editor", ->
describe "Test Metadata Editor", ->
editorTemplate = readFixtures('metadata-editor.underscore')
numberEntryTemplate = readFixtures('metadata-number-entry.underscore')
stringEntryTemplate = readFixtures('metadata-string-entry.underscore')
......@@ -27,7 +29,7 @@ describe "Test Metadata Editor", ->
field_name: "display_name",
help: "Specifies the name for this component.",
options: [],
type: CMS.Models.Metadata.GENERIC_TYPE,
type: MetadataModel.GENERIC_TYPE,
value: "Word cloud"
}
......@@ -42,7 +44,7 @@ describe "Test Metadata Editor", ->
{"display_name": "Answered", "value": "answered"},
{"display_name": "Never", "value": "never"}
],
type: CMS.Models.Metadata.SELECT_TYPE,
type: MetadataModel.SELECT_TYPE,
value: "always"
}
......@@ -53,7 +55,7 @@ describe "Test Metadata Editor", ->
field_name: "num_inputs",
help: "Number of text boxes for student to input words/sentences.",
options: {min: 1},
type: CMS.Models.Metadata.INTEGER_TYPE,
type: MetadataModel.INTEGER_TYPE,
value: 5
}
......@@ -64,7 +66,7 @@ describe "Test Metadata Editor", ->
field_name: "weight",
help: "Weight for this problem",
options: {min: 1.3, max:100.2, step:0.1},
type: CMS.Models.Metadata.FLOAT_TYPE,
type: MetadataModel.FLOAT_TYPE,
value: 10.2
}
......@@ -75,14 +77,14 @@ describe "Test Metadata Editor", ->
field_name: "list",
help: "A list of things.",
options: [],
type: CMS.Models.Metadata.LIST_TYPE,
type: MetadataModel.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", ->
describe "MetadataView.Editor creates editors for each field", ->
beforeEach ->
@model = new CMS.Models.MetadataCollection(
@model = new MetadataCollection(
[
integerEntry,
floatEntry,
......@@ -106,7 +108,7 @@ describe "Test Metadata Editor", ->
)
it "creates child views on initialize, and sorts them alphabetically", ->
view = new CMS.Views.Metadata.Editor({collection: @model})
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
......@@ -125,14 +127,14 @@ describe "Test Metadata Editor", ->
verifyEntry(5, 'Weight', 'number')
it "returns its display name", ->
view = new CMS.Views.Metadata.Editor({collection: @model})
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 CMS.Views.Metadata.Editor({collection: new CMS.Models.MetadataCollection()})
view = new MetadataView.Editor({collection: new MetadataCollection()})
expect(view.getDisplayName()).toBe("")
view = new CMS.Views.Metadata.Editor({collection: new CMS.Models.MetadataCollection([
view = new MetadataView.Editor({collection: new MetadataCollection([
{
default_value: null,
display_name: "Display Name",
......@@ -140,7 +142,7 @@ describe "Test Metadata Editor", ->
field_name: "display_name",
help: "",
options: [],
type: CMS.Models.Metadata.GENERIC_TYPE,
type: MetadataModel.GENERIC_TYPE,
value: null
}])
......@@ -148,11 +150,11 @@ describe "Test Metadata Editor", ->
expect(view.getDisplayName()).toBe("")
it "has no modified values by default", ->
view = new CMS.Views.Metadata.Editor({collection: @model})
view = new MetadataView.Editor({collection: @model})
expect(view.getModifiedMetadataValues()).toEqual({})
it "returns modified values only", ->
view = new CMS.Views.Metadata.Editor({collection: @model})
view = new MetadataView.Editor({collection: @model})
childModels = view.collection.models
childModels[0].setValue('updated display name')
childModels[1].setValue(20)
......@@ -186,10 +188,10 @@ describe "Test Metadata Editor", ->
view.updateModel()
expect(view.model.getValue()).toEqual(newValue)
describe "CMS.Views.Metadata.String is a basic string input with clear functionality", ->
describe "MetadataView.String is a basic string input with clear functionality", ->
beforeEach ->
model = new CMS.Models.Metadata(genericEntry)
@view = new CMS.Views.Metadata.String({model: model})
model = new MetadataModel(genericEntry)
@view = new MetadataView.String({model: model})
it "uses a text input type", ->
assertInputType(@view, 'text')
......@@ -206,10 +208,10 @@ describe "Test Metadata Editor", ->
it "has an update model method", ->
assertUpdateModel(@view, 'Word cloud', 'updated')
describe "CMS.Views.Metadata.Option is an option input type with clear functionality", ->
describe "MetadataView.Option is an option input type with clear functionality", ->
beforeEach ->
model = new CMS.Models.Metadata(selectEntry)
@view = new CMS.Views.Metadata.Option({model: model})
model = new MetadataModel(selectEntry)
@view = new MetadataView.Option({model: model})
it "uses a select input type", ->
assertInputType(@view, 'select-one')
......@@ -230,13 +232,13 @@ describe "Test Metadata Editor", ->
@view.setValueInEditor("not an option")
expect(@view.getValueFromEditor()).toBe('always')
describe "CMS.Views.Metadata.Number supports integer or float type and has clear functionality", ->
describe "MetadataView.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})
integerModel = new MetadataModel(integerEntry)
@integerView = new MetadataView.Number({model: integerModel})
floatModel = new CMS.Models.Metadata(floatEntry)
@floatView = new CMS.Views.Metadata.Number({model: floatModel})
floatModel = new MetadataModel(floatEntry)
@floatView = new MetadataView.Number({model: floatModel})
it "uses a number input type", ->
assertInputType(@integerView, 'number')
......@@ -317,11 +319,12 @@ describe "Test Metadata Editor", ->
verifyDisallowedChars(@integerView)
verifyDisallowedChars(@floatView)
describe "CMS.Views.Metadata.List allows the user to enter an ordered list of strings", ->
describe "MetadataView.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})
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'])
......
describe "CMS.Views.ModuleEdit", ->
define ["coffee/src/views/module_edit", "xmodule"], (ModuleEdit) ->
describe "ModuleEdit", ->
beforeEach ->
@stubModule = jasmine.createSpy("CMS.Models.Module")
@stubModule = jasmine.createSpy("Module")
@stubModule.id = 'stub-id'
......@@ -25,12 +27,11 @@ describe "CMS.Views.ModuleEdit", ->
"""
spyOn($.fn, 'load').andReturn(@moduleData)
@moduleEdit = new CMS.Views.ModuleEdit(
@moduleEdit = new ModuleEdit(
el: $(".component")
model: @stubModule
onDelete: jasmine.createSpy()
)
CMS.unbind()
describe "class definition", ->
it "sets the correct tagName", ->
......@@ -42,8 +43,8 @@ describe "CMS.Views.ModuleEdit", ->
describe "methods", ->
describe "initialize", ->
beforeEach ->
spyOn(CMS.Views.ModuleEdit.prototype, 'render')
@moduleEdit = new CMS.Views.ModuleEdit(
spyOn(ModuleEdit.prototype, 'render')
@moduleEdit = new ModuleEdit(
el: $(".component")
model: @stubModule
onDelete: jasmine.createSpy()
......
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()
describe "CMS.Views.SectionShow", ->
define ["js/models/section", "js/views/section_show", "js/views/section_edit", "sinon"], (Section, SectionShow, SectionEdit, sinon) ->
describe "SectionShow", ->
describe "Basic", ->
beforeEach ->
spyOn(CMS.Views.SectionShow.prototype, "switchToEditView")
spyOn(SectionShow.prototype, "switchToEditView")
.andCallThrough()
@model = new CMS.Models.Section({
@model = new Section({
id: 42
name: "Life, the Universe, and Everything"
})
@view = new CMS.Views.SectionShow({model: @model})
@view = new SectionShow({model: @model})
@view.render()
it "should contain the model name", ->
......@@ -18,12 +20,12 @@ describe "CMS.Views.SectionShow", ->
expect(@view.switchToEditView).toHaveBeenCalled()
it "should pass the same element to SectionEdit when switching views", ->
spyOn(CMS.Views.SectionEdit.prototype, 'initialize').andCallThrough()
spyOn(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)
expect(SectionEdit.prototype.initialize).toHaveBeenCalled()
expect(SectionEdit.prototype.initialize.mostRecentCall.args[0].el).toEqual(@view.el)
describe "CMS.Views.SectionEdit", ->
describe "SectionEdit", ->
describe "Basic", ->
tpl = readFixtures('section-name-edit.underscore')
feedback_tpl = readFixtures('system-feedback.underscore')
......@@ -31,9 +33,9 @@ describe "CMS.Views.SectionEdit", ->
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")
spyOn(SectionEdit.prototype, "switchToShowView")
.andCallThrough()
spyOn(CMS.Views.SectionEdit.prototype, "showInvalidMessage")
spyOn(SectionEdit.prototype, "showInvalidMessage")
.andCallThrough()
window.analytics = jasmine.createSpyObj('analytics', ['track'])
window.course_location_analytics = jasmine.createSpy()
......@@ -41,11 +43,11 @@ describe "CMS.Views.SectionEdit", ->
@xhr = sinon.useFakeXMLHttpRequest()
@xhr.onCreate = (xhr) -> requests.push(xhr)
@model = new CMS.Models.Section({
@model = new Section({
id: 42
name: "Life, the Universe, and Everything"
})
@view = new CMS.Views.SectionEdit({model: @model})
@view = new SectionEdit({model: @model})
@view.render()
afterEach ->
......
feedbackTpl = readFixtures('system-feedback.underscore')
define ["js/models/uploads", "js/views/uploads", "js/models/chapter", "sinon"], (FileUpload, UploadDialog, Chapter, sinon) ->
describe "CMS.Views.UploadDialog", ->
feedbackTpl = readFixtures('system-feedback.underscore')
describe "UploadDialog", ->
tpl = readFixtures("upload-dialog.underscore")
beforeEach ->
......@@ -8,11 +10,11 @@ describe "CMS.Views.UploadDialog", ->
appendSetFixtures($("<script>", {id: "system-feedback-tpl", type: "text/template"}).text(feedbackTpl))
CMS.URL.UPLOAD_ASSET = "/upload"
@model = new CMS.Models.FileUpload(
@model = new FileUpload(
mimeTypes: ['application/pdf']
)
@dialogResponse = dialogResponse = []
@view = new CMS.Views.UploadDialog(
@view = new UploadDialog(
model: @model,
onSuccess: (response) =>
dialogResponse.push(response.response)
......
AjaxPrefix.addAjaxPrefix(jQuery, -> CMS.prefix)
define ["jquery", "underscore.string", "backbone", "js/views/feedback_notification", "jquery.cookie"],
($, str, Backbone, NotificationView) ->
AjaxPrefix.addAjaxPrefix jQuery, ->
$("meta[name='path_prefix']").attr('content')
@CMS =
Models: {}
Views: {}
Collections: {}
URL: {}
prefix: $("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
_.extend CMS, Backbone.Events
_.extend CMS, Backbone.Events
$ ->
main = ->
Backbone.emulateHTTP = true
$.ajaxSetup
......@@ -24,16 +24,17 @@ $ ->
try
message = JSON.parse(jqXHR.responseText).error
catch error
message = _.str.truncate(jqXHR.responseText, 300)
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(
msg = new NotificationView.Error(
"title": gettext("Studio's having trouble saving your work")
"message": message
)
msg.show()
window.onTouchBasedDevice = ->
navigator.userAgent.match /iPhone|iPod|iPad/i
if onTouchBasedDevice()
$('body').addClass 'touch-based-device'
$('body').addClass 'touch-based-device' if onTouchBasedDevice()
$(main)
return main
class CMS.Models.Module extends Backbone.Model
define ["backbone"], (Backbone) ->
class Module extends Backbone.Model
url: '/save_item'
class CMS.Views.ModuleEdit extends Backbone.View
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'
......@@ -28,9 +32,9 @@ class CMS.Views.ModuleEdit extends Backbone.View
models = [];
for key of metadataData
models.push(metadataData[key])
@metadataEditor = new CMS.Views.Metadata.Editor({
@metadataEditor = new MetadataView.Editor({
el: metadataEditor,
collection: new CMS.Models.MetadataCollection(models)
collection: new MetadataCollection(models)
})
# Need to update set "active" class on data editor if there is one.
......@@ -84,7 +88,7 @@ class CMS.Views.ModuleEdit extends Backbone.View
data.metadata = _.extend(data.metadata || {}, @changedMetadata())
@hideModal()
saving = new CMS.Views.Notification.Mini
saving = new NotificationView.Mini
title: gettext('Saving&hellip;')
saving.show()
@model.save(data).done( =>
......@@ -93,8 +97,6 @@ class CMS.Views.ModuleEdit extends Backbone.View
@render()
@$el.removeClass('editing')
saving.hide()
).fail( ->
showToastMessage(gettext("There was an error saving your changes. Please try again."), null, 3)
)
clickCancelButton: (event) ->
......@@ -104,13 +106,14 @@ class CMS.Views.ModuleEdit extends Backbone.View
@hideModal()
hideModal: ->
$modalCover = $(".modal-cover")
$modalCover.hide()
$modalCover.removeClass('is-fixed')
clickEditButton: (event) ->
event.preventDefault()
@$el.addClass('editing')
$modalCover.show().addClass('is-fixed')
$(".modal-cover").show().addClass('is-fixed')
@$component_editor().slideDown(150)
@loadEdit()
......
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(
new ModuleEditView(
el: element,
onDelete: @deleteTab,
model: new CMS.Models.Module(
model: new ModuleModel(
id: $(element).data('id'),
)
)
......@@ -44,9 +46,9 @@ class CMS.Views.TabsEdit extends Backbone.View
addNewTab: (event) =>
event.preventDefault()
editor = new CMS.Views.ModuleEdit(
editor = new ModuleEditView(
onDelete: @deleteTab
model: new CMS.Models.Module()
model: new ModuleModel()
)
$('.new-component-item').before(editor.$el)
......@@ -64,7 +66,7 @@ class CMS.Views.TabsEdit extends Backbone.View
course: course_location_analytics
deleteTab: (event) =>
confirm = new CMS.Views.Prompt.Warning
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:
......@@ -77,7 +79,7 @@ class CMS.Views.TabsEdit extends Backbone.View
analytics.track "Deleted Static Page",
course: course_location_analytics
id: $component.data('id')
deleting = new CMS.Views.Notification.Mini
deleting = new NotificationView.Mini
title: gettext('Deleting&hellip;')
deleting.show()
$.post('/delete_item', {
......
class CMS.Views.UnitEdit extends Backbone.View
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'
......@@ -11,17 +15,17 @@ class CMS.Views.UnitEdit extends Backbone.View
'change .visibility-select': 'setVisibility'
initialize: =>
@visibilityView = new CMS.Views.UnitEdit.Visibility(
@visibilityView = new UnitEditView.Visibility(
el: @$('.visibility-select')
model: @model
)
@locationView = new CMS.Views.UnitEdit.LocationState(
@locationView = new UnitEditView.LocationState(
el: @$('.section-item.editing a')
model: @model
)
@nameView = new CMS.Views.UnitEdit.NameEdit(
@nameView = new UnitEditView.NameEdit(
el: @$('.unit-name-input')
model: @model
)
......@@ -41,7 +45,7 @@ class CMS.Views.UnitEdit extends Backbone.View
id: unit_location_analytics
payload = children : @components()
saving = new CMS.Views.Notification.Mini
saving = new NotificationView.Mini
title: gettext('Saving&hellip;')
saving.show()
options = success : =>
......@@ -56,15 +60,12 @@ class CMS.Views.UnitEdit extends Backbone.View
items: '> .component'
)
@$('.component').each((idx, element) =>
new CMS.Views.ModuleEdit(
@$('.component').each (idx, element) =>
new ModuleEditView
el: element,
onDelete: @deleteComponent,
model: new CMS.Models.Module(
id: $(element).data('id'),
)
)
)
model: new ModuleModel
id: $(element).data('id')
showComponentTemplates: (event) =>
event.preventDefault()
......@@ -87,9 +88,9 @@ class CMS.Views.UnitEdit extends Backbone.View
saveNewComponent: (event) =>
event.preventDefault()
editor = new CMS.Views.ModuleEdit(
editor = new ModuleEditView(
onDelete: @deleteComponent
model: new CMS.Models.Module()
model: new ModuleModel()
)
@$newComponentItem.before(editor.$el)
......@@ -121,7 +122,7 @@ class CMS.Views.UnitEdit extends Backbone.View
deleteComponent: (event) =>
event.preventDefault()
msg = new CMS.Views.Prompt.Warning(
msg = new PromptView.Warning(
title: gettext('Delete this component?'),
message: gettext('Deleting this component is permanent and cannot be undone.'),
actions:
......@@ -129,7 +130,7 @@ class CMS.Views.UnitEdit extends Backbone.View
text: gettext('Yes, delete this component'),
click: (view) =>
view.hide()
deleting = new CMS.Views.Notification.Mini
deleting = new NotificationView.Mini
title: gettext('Deleting&hellip;'),
deleting.show()
$component = $(event.currentTarget).parents('.component')
......@@ -221,7 +222,7 @@ class CMS.Views.UnitEdit extends Backbone.View
@model.set('state', @$('.visibility-select').val())
)
class CMS.Views.UnitEdit.NameEdit extends Backbone.View
class UnitEditView.NameEdit extends Backbone.View
events:
'change .unit-display-name-input': 'saveName'
......@@ -255,17 +256,19 @@ class CMS.Views.UnitEdit.NameEdit extends Backbone.View
display_name: metadata.display_name
class CMS.Views.UnitEdit.LocationState extends Backbone.View
class UnitEditView.LocationState extends Backbone.View
initialize: =>
@model.on('change:state', @render)
render: =>
@$el.toggleClass("#{@model.previous('state')}-item #{@model.get('state')}-item")
class CMS.Views.UnitEdit.Visibility extends Backbone.View
class UnitEditView.Visibility extends Backbone.View
initialize: =>
@model.on('change:state', @render)
@render()
render: =>
@$el.val(@model.get('state'))
return UnitEditView
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&hellip;')
});
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&hellip;")
});
saving.show();
......@@ -874,3 +863,5 @@ function saveSetSectionScheduleDate(e) {
saving.hide();
});
}
}); // end require()
define(["backbone", "js/models/asset"], function(Backbone, AssetModel){
var AssetCollection = Backbone.Collection.extend({
model : AssetModel
});
return AssetCollection;
});
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;
});
// Model for checklists_view.js.
CMS.Models.Checklist = Backbone.Model.extend({
});
CMS.Models.ChecklistCollection = Backbone.Collection.extend({
model : CMS.Models.Checklist,
define(["backbone", "underscore", "js/models/checklist"],
function(Backbone, _, ChecklistModel) {
var ChecklistCollection = Backbone.Collection.extend({
model : ChecklistModel,
parse: function(response) {
_.each(response,
......@@ -20,5 +18,6 @@ CMS.Models.ChecklistCollection = Backbone.Collection.extend({
options.cache = false;
return Backbone.Collection.prototype.fetch.call(this, options);
}
});
return ChecklistCollection;
});
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()
define(["backbone", "js/models/course_relative"], function(Backbone, CourseRelativeModel) {
var CourseRelativeCollection = Backbone.Collection.extend({
model: CourseRelativeModel
});
return CourseRelativeCollection;
});
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;
});
define(["backbone", "js/models/metadata"], function(Backbone, MetadataModel) {
var MetadataCollection = Backbone.Collection.extend({
model : MetadataModel,
comparator: "display_name"
});
return MetadataCollection;
});
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) {
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) {
HesitateEvent.prototype.trigger = function(event) {
if (event.data.timeoutEventId == null) {
event.data.timeoutEventId = window.setTimeout(
function() { event.data.fireEvent(event); },
CMS.HesitateEvent.DURATION);
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) {
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) {
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;
});
/**
define(["backbone"], function(Backbone) {
/**
* Simple model for an asset.
*/
CMS.Models.Asset = Backbone.Model.extend({
var Asset = Backbone.Model.extend({
defaults: {
display_name: "",
thumbnail: "",
......@@ -10,4 +11,6 @@ CMS.Models.Asset = Backbone.Model.extend({
portable_url: "",
locked: false
}
});
return Asset;
});
CMS.Models.AssetCollection = Backbone.Collection.extend({
model : CMS.Models.Asset
});
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;
});
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;
});
define(["backbone"], function(Backbone) {
var Checklist = Backbone.Model.extend({
});
return Checklist;
});
CMS.Models.Course = Backbone.Model.extend({
define(['backbone'], function(Backbone){
var Course = Backbone.Model.extend({
defaults: {
"name": ""
},
......@@ -7,4 +8,6 @@ CMS.Models.Course = Backbone.Model.extend({
return gettext("You must specify a name");
}
}
});
return Course;
});
// single per course holds the updates and handouts
CMS.Models.CourseInfo = Backbone.Model.extend({
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: '',
......@@ -10,27 +11,6 @@ CMS.Models.CourseInfo = Backbone.Model.extend({
},
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
});
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({
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)
}
});
CMS.Models.CourseRelativeCollection = Backbone.Collection.extend({
model : CMS.Models.CourseRelative
});
return CourseRelative;
});
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()
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;
});
/**
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.
*/
CMS.Models.Metadata = Backbone.Model.extend({
var Metadata = Backbone.Model.extend({
defaults: {
"field_name": null,
"display_name": null,
......@@ -100,15 +100,13 @@ CMS.Models.Metadata = Backbone.Model.extend({
value: this.get('default_value')
});
}
});
});
CMS.Models.MetadataCollection = Backbone.Collection.extend({
model : CMS.Models.Metadata,
comparator: "display_name"
});
Metadata.SELECT_TYPE = "Select";
Metadata.INTEGER_TYPE = "Integer";
Metadata.FLOAT_TYPE = "Float";
Metadata.GENERIC_TYPE = "Generic";
Metadata.LIST_TYPE = "List";
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";
return Metadata;
});
CMS.Models.ModuleInfo = Backbone.Model.extend({
define(["backbone"], function(Backbone) {
var ModuleInfo = Backbone.Model.extend({
url: function() {return "/module_info/" + this.id;},
defaults: {
......@@ -7,4 +8,6 @@ CMS.Models.ModuleInfo = Backbone.Model.extend({
"metadata" : null,
"children" : null
}
});
return ModuleInfo;
});
CMS.Models.Section = Backbone.Model.extend({
define(["backbone", "gettext", "js/views/feedback_notification"], function(Backbone, gettext, NotificationView) {
var Section = Backbone.Model.extend({
defaults: {
"name": ""
},
......@@ -22,7 +23,7 @@ CMS.Models.Section = Backbone.Model.extend({
},
showNotification: function() {
if(!this.msg) {
this.msg = new CMS.Views.Notification.Mini({
this.msg = new NotificationView.Mini({
title: gettext("Saving&hellip;")
});
}
......@@ -32,4 +33,6 @@ CMS.Models.Section = Backbone.Model.extend({
if(!this.msg) { return; }
this.msg.hide();
}
});
return Section;
});
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()
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()
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()
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()
CMS.Models.Textbook = Backbone.AssociatedModel.extend({
define(["backbone", "underscore", "js/models/chapter", "js/collections/chapter", "backbone.associations"],
function(Backbone, _, ChapterModel, ChapterCollection) {
var Textbook = Backbone.AssociatedModel.extend({
defaults: function() {
return {
name: "",
chapters: new CMS.Collections.ChapterSet([{}]),
chapters: new ChapterCollection([{}]),
showChapters: false,
editing: false
};
......@@ -10,8 +13,8 @@ CMS.Models.Textbook = Backbone.AssociatedModel.extend({
relations: [{
type: Backbone.Many,
key: "chapters",
relatedModel: "CMS.Models.Chapter",
collectionType: "CMS.Collections.ChapterSet"
relatedModel: ChapterModel,
collectionType: ChapterCollection
}],
initialize: function() {
this.setOriginalAttributes();
......@@ -87,72 +90,6 @@ CMS.Models.Textbook = Backbone.AssociatedModel.extend({
}
}
}
});
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(); });
}
});
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()
CMS.Views.Asset = Backbone.View.extend({
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"
......@@ -13,17 +13,15 @@ CMS.Views.Asset = Backbone.View.extend({
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}));
uniqueId: uniqueId
}));
this.updateLockState();
return this;
},
......@@ -44,8 +42,8 @@ CMS.Views.Asset = Backbone.View.extend({
confirmDelete: function(e) {
if(e && e.preventDefault) { e.preventDefault(); }
var asset = this.model;
new CMS.Views.Prompt.Warning({
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: {
......@@ -56,31 +54,28 @@ CMS.Views.Asset = Backbone.View.extend({
asset.destroy({
wait: true, // Don't remove the asset from the collection until successful.
success: function () {
new CMS.Views.Notification.Confirmation({
new NotificationView.Confirmation({
title: gettext("Your file has been deleted."),
closeIcon: false,
maxShown: 2000
}).show()
}
}).show();
}
);
});
}
},
secondary: [
{
secondary: {
text: gettext("Cancel"),
click: function (view) {
view.hide();
}
}
]
}
}).show();
},
lockAsset: function(e) {
var asset = this.model;
var saving = new CMS.Views.Notification.Mini({
var saving = new NotificationView.Mini({
title: gettext("Saving&hellip;")
}).show();
asset.save({'locked': !asset.get('locked')}, {
......@@ -91,3 +86,6 @@ CMS.Views.Asset = Backbone.View.extend({
});
}
});
return AssetView;
}); // end define()
// 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);
});
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();
};
define(["backbone", "js/views/asset"], function(Backbone, AssetView) {
var showFileSelectionMenu = function(e) {
e.preventDefault();
$('.file-input').click();
};
var AssetsView = Backbone.View.extend({
// takes AssetCollection as model
var startUpload = function (e) {
var file = e.target.value;
initialize : function() {
this.listenTo(this.collection, 'destroy', this.handleDestroy);
this.render();
},
$('.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();
};
render: function() {
this.$el.empty();
var resetUploadModal = function () {
$('.file-input').unbind('change', startUpload);
var self = this;
this.collection.each(
function(asset) {
var view = new AssetView({model: asset});
self.$el.append(view.render().el);
});
// 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();
return this;
},
$('.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();
};
handleDestroy: function(model, collection, options) {
var index = options.index;
this.$el.children().eq(index).remove();
var showUploadFeedback = function (event, percentComplete) {
var percentVal = percentComplete + '%';
$('.upload-modal .progress-fill').width(percentVal);
$('.upload-modal .progress-fill').html(percentVal);
};
analytics.track('Deleted Asset', {
'course': course_location_analytics,
'id': model.get('url')
});
},
var displayFinishedUpload = function (resp) {
var asset = resp.asset;
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);
$('.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%');
analytics.track('Uploaded a File', {
'course': course_location_analytics,
'asset_url': model.get('url')
});
}
}
});
// TODO remove setting on window object after RequireJS.
window.assetsView.addAsset(new CMS.Models.Asset(asset));
};
return AssetsView;
}); // end define();
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')
});
}
}
});
if (!CMS.Views['Checklists']) CMS.Views.Checklists = {};
CMS.Views.Checklists = Backbone.View.extend({
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"]' : window.cmsLinkNewWindow
'click a[rel="external"]' : "popup"
},
initialize : function() {
var self = this;
this.template = _.template($("#checklist-tpl").text());
this.listenTo(this.collection, 'reset', this.render);
this.render();
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;
......@@ -76,5 +83,11 @@ CMS.Views.Checklists = Backbone.View.extend({
});
}
});
},
popup: function(e) {
e.preventDefault();
window.open($(e.target).attr('href'));
}
});
return ChecklistView;
});
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&hellip;')
});
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()
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};
}
);
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&hellip;')
});
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&hellip;')
});
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()
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;
});
CMS.Views.ShowTextbook = Backbone.View.extend({
initialize: function() {
this.template = _.template($("#show-textbook-tpl").text());
this.listenTo(this.model, "change", this.render);
},
tagName: "section",
className: "textbook",
events: {
"click .edit": "editTextbook",
"click .delete": "confirmDelete",
"click .show-chapters": "showChapters",
"click .hide-chapters": "hideChapters"
},
render: function() {
var attrs = $.extend({}, this.model.attributes);
attrs.bookindex = this.model.collection.indexOf(this.model);
attrs.course = window.section.attributes;
this.$el.html(this.template(attrs));
return this;
},
editTextbook: function(e) {
if(e && e.preventDefault) { e.preventDefault(); }
this.model.set("editing", true);
},
confirmDelete: function(e) {
if(e && e.preventDefault) { e.preventDefault(); }
var textbook = this.model, collection = this.model.collection;
var msg = new CMS.Views.Prompt.Warning({
title: _.template(gettext("Delete “<%= name %>”?"),
{name: textbook.escape('name')}),
message: gettext("Deleting a textbook cannot be undone and once deleted any reference to it in your courseware's navigation will also be removed."),
actions: {
primary: {
text: gettext("Delete"),
click: function(view) {
view.hide();
var delmsg = new CMS.Views.Notification.Mini({
title: gettext("Deleting&hellip;")
}).show();
textbook.destroy({
complete: function() {
delmsg.hide();
}
});
}
},
secondary: {
text: gettext("Cancel"),
click: function(view) {
view.hide();
}
}
}
}).show();
},
showChapters: function(e) {
if(e && e.preventDefault) { e.preventDefault(); }
this.model.set('showChapters', true);
},
hideChapters: function(e) {
if(e && e.preventDefault) { e.preventDefault(); }
this.model.set('showChapters', false);
}
});
CMS.Views.EditTextbook = Backbone.View.extend({
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);
......@@ -88,7 +26,7 @@ CMS.Views.EditTextbook = Backbone.View.extend({
"click .action-add-chapter": "createChapter"
},
addOne: function(chapter) {
var view = new CMS.Views.EditChapter({model: chapter});
var view = new EditChapterView({model: chapter});
this.$("ol.chapters").append(view.render().el);
return this;
},
......@@ -121,8 +59,8 @@ CMS.Views.EditTextbook = Backbone.View.extend({
if(e && e.preventDefault) { e.preventDefault(); }
this.setValues();
if(!this.model.isValid()) { return; }
var saving = new CMS.Views.Notification.Mini({
title: gettext("Saving&hellip;")
var saving = new NotificationView.Mini({
title: gettext("Saving") + "&hellip;"
}).show();
var that = this;
this.model.save({}, {
......@@ -151,109 +89,6 @@ CMS.Views.EditTextbook = Backbone.View.extend({
this.model.set("editing", false);
return this;
}
});
CMS.Views.ListTextbooks = Backbone.View.extend({
initialize: function() {
this.emptyTemplate = _.template($("#no-textbooks-tpl").text());
this.listenTo(this.collection, 'all', this.render);
},
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 CMS.Views.EditTextbook({model: textbook});
} else {
view = new CMS.Views.ShowTextbook({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}]);
}
});
CMS.Views.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 CMS.Models.FileUpload({
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 CMS.Views.UploadDialog({
model: msg,
onSuccess: function(response) {
var options = {};
if(!that.model.get('name')) {
options.name = response.asset.display_name;
}
options.asset_path = response.asset.url;
that.model.set(options);
}
});
$(".wrapper-view").after(view.show().el);
}
return EditTextbook;
});
CMS.Views.SystemFeedback = Backbone.View.extend({
define(["backbone", "underscore", "underscore.string", "jquery"], function(Backbone, _, str, $) {
var SystemFeedback = Backbone.View.extend({
options: {
title: "",
message: "",
......@@ -96,13 +97,13 @@ CMS.Views.SystemFeedback = Backbone.View.extend({
// 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();
var singleton = SystemFeedback["active_"+this.options.type];
if(singleton && singleton !== this) {
singleton.stopListening();
singleton.undelegateEvents();
}
this.$el.html(this.template(this.options));
parent.active = this;
SystemFeedback["active_"+this.options.type] = this;
return this;
},
primaryClick: function(event) {
......@@ -135,73 +136,6 @@ CMS.Views.SystemFeedback = Backbone.View.extend({
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
})
return SystemFeedback;
});
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;
});
});
// set more sensible defaults for Notification-Mini views
var miniOptions = CMS.Views.Notification.Mini.prototype.options;
miniOptions.minShown = 1250;
miniOptions.closeIcon = false;
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;
});
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;
});
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;
});
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;
});
if (!CMS.Views['Metadata']) CMS.Views.Metadata = {};
CMS.Views.Metadata.Editor = Backbone.View.extend({
define(["backbone", "underscore", "js/models/metadata"], function(Backbone, _, MetadataModel) {
var Metadata = {};
Metadata.Editor = Backbone.View.extend({
// Model is CMS.Models.MetadataCollection,
initialize : function() {
......@@ -20,19 +22,19 @@ CMS.Views.Metadata.Editor = Backbone.View.extend({
el: self.$el.find('.metadata_entry')[counter++],
model: model
};
if (model.getType() === CMS.Models.Metadata.SELECT_TYPE) {
new CMS.Views.Metadata.Option(data);
if (model.getType() === MetadataModel.SELECT_TYPE) {
new 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() === MetadataModel.INTEGER_TYPE ||
model.getType() === MetadataModel.FLOAT_TYPE) {
new Metadata.Number(data);
}
else if(model.getType() === CMS.Models.Metadata.LIST_TYPE) {
new CMS.Views.Metadata.List(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 CMS.Views.Metadata.String(data);
new Metadata.String(data);
}
});
},
......@@ -70,11 +72,11 @@ CMS.Views.Metadata.Editor = Backbone.View.extend({
);
return displayName;
}
});
});
CMS.Views.Metadata.AbstractEditor = Backbone.View.extend({
Metadata.AbstractEditor = Backbone.View.extend({
// Model is CMS.Models.Metadata.
// Model is MetadataModel
initialize : function() {
var self = this;
var templateName = _.result(this, 'templateName');
......@@ -158,9 +160,9 @@ CMS.Views.Metadata.AbstractEditor = Backbone.View.extend({
return this;
}
});
});
CMS.Views.Metadata.String = CMS.Views.Metadata.AbstractEditor.extend({
Metadata.String = Metadata.AbstractEditor.extend({
events : {
"change input" : "updateModel",
......@@ -177,9 +179,9 @@ CMS.Views.Metadata.String = CMS.Views.Metadata.AbstractEditor.extend({
setValueInEditor : function (value) {
this.$el.find('input').val(value);
}
});
});
CMS.Views.Metadata.Number = CMS.Views.Metadata.AbstractEditor.extend({
Metadata.Number = Metadata.AbstractEditor.extend({
events : {
"change input" : "updateModel",
......@@ -189,7 +191,7 @@ CMS.Views.Metadata.Number = CMS.Views.Metadata.AbstractEditor.extend({
},
render: function () {
CMS.Views.Metadata.AbstractEditor.prototype.render.apply(this);
Metadata.AbstractEditor.prototype.render.apply(this);
if (!this.initialized) {
var numToString = function (val) {
return val.toFixed(4);
......@@ -275,9 +277,9 @@ CMS.Views.Metadata.Number = CMS.Views.Metadata.AbstractEditor.extend({
this.updateModel();
}
});
});
CMS.Views.Metadata.Option = CMS.Views.Metadata.AbstractEditor.extend({
Metadata.Option = Metadata.AbstractEditor.extend({
events : {
"change select" : "updateModel",
......@@ -312,9 +314,9 @@ CMS.Views.Metadata.Option = CMS.Views.Metadata.AbstractEditor.extend({
return $(this).text() === value;
}).prop('selected', true);
}
});
});
CMS.Views.Metadata.List = CMS.Views.Metadata.AbstractEditor.extend({
Metadata.List = Metadata.AbstractEditor.extend({
events : {
"click .setting-clear" : "clear",
......@@ -368,4 +370,7 @@ CMS.Views.Metadata.List = CMS.Views.Metadata.AbstractEditor.extend({
enableAdd: function() {
this.$el.find('.create-setting').removeClass('is-disabled');
}
});
return Metadata;
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment