Merge branch 'master' of github.com:edx/edx-platform into bugfix/ichuang/make-edit-link-use-static-asset-path

This commit is contained in:
ichuang
2013-10-16 22:00:13 -04:00
314 changed files with 11338 additions and 5033 deletions

View File

@@ -89,3 +89,4 @@ Akshay Jagadeesh <akjags@gmail.com>
Nick Parlante <nick.parlante@cs.stanford.edu>
Marko Seric <marko.seric@math.uzh.ch>
Felipe Montoya <felipe.montoya@edunext.co>
Julia Hansbrough <julia@edx.org>

View File

@@ -5,8 +5,21 @@ These are notable changes in edx-platform. This is a rolling list of changes,
in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected.
Blades: Hovering over CC button in video player, when transcripts are hidden,
will cause them to show up. Moving the mouse from the CC button will auto hide
them. You can hover over the CC button and then move the mouse to the
transcripts which will allow you to select some video position in 1 click.
Blades: Add possibility to use multiple LTI tools per page.
Blades: LTI module can now load external content in a new window.
LMS: Disable data download buttons on the instructor dashboard for large courses
LMS: Ported bulk emailing to the beta instructor dashboard.
LMS: Add monitoring of bulk email subtasks to display progress on instructor dash.
LMS: Refactor and clean student dashboard templates.
LMS: Fix issue with CourseMode expiration dates
@@ -22,6 +35,8 @@ Studio: Switched to loading Javascript using require.js
Studio: Better feedback during the course import process
Studio: Improve drag and drop on the course overview and subsection views.
LMS: Add split testing functionality for internal use.
CMS: Add edit_course_tabs management command, providing a primitive
@@ -73,6 +88,11 @@ Common: Allow instructors to input complicated expressions as answers to
`NumericalResponse`s. Prior to the change only numbers were allowed, now any
answer from '1/3' to 'sqrt(12)*(1-1/3^2+1/5/3^2)' are valid.
Studio/LMS: Allow for 'preview' and 'published' in a single LMS instance. Use
middlware components to retain the incoming Django request and put in thread
local storage. It is recommended that all developers define a 'preview.localhost'
which maps to the same IP address as localhost in his/her HOSTS file.
LMS: Enable beta instructor dashboard. The beta dashboard is a rearchitecture
of the existing instructor dashboard and is available by clicking a link at
the top right of the existing dashboard.

View File

@@ -2,7 +2,7 @@
# pylint: disable=W0621
from lettuce import world, step
from nose.tools import assert_true, assert_equal, assert_in, assert_false # pylint: disable=E0611
from nose.tools import assert_true, assert_in, assert_false # pylint: disable=E0611
from auth.authz import get_user_by_email, get_course_groupname_for_role
from django.conf import settings
@@ -224,14 +224,50 @@ def i_enabled_the_advanced_module(step, module):
press_the_notification_button(step, 'Save')
@step('I have clicked the new unit button')
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)
@world.absorb
def create_course_with_unit():
"""
Prepare for tests by creating a course with a section, subsection, and unit.
Performs the following:
Clear out all courseware
Create a course with a section, subsection, and unit
Create a user and make that user a course author
Log the user into studio
Open the course from the dashboard
Expand the section and click on the New Unit link
The end result is the page where the user is editing the new unit
"""
world.clear_courses()
course = world.CourseFactory.create()
world.scenario_dict['COURSE'] = course
section = world.ItemFactory.create(parent_location=course.location)
world.ItemFactory.create(
parent_location=section.location,
category='sequential',
display_name='Subsection One',
)
user = create_studio_user(is_staff=False)
add_course_author(user, course)
log_into_studio()
world.css_click('a.course-link')
css_selectors = [
'div.section-item a.expand-collapse-icon', 'a.new-unit-item'
]
for selector in css_selectors:
world.css_click(selector)
world.wait_for_mathjax()
world.wait_for_xmodule()
assert world.is_css_present('ul.new-component-type')
@step('I have clicked the new unit button$')
@step(u'I am in Studio editing a new unit$')
def edit_new_unit(step):
create_course_with_unit()
@step('the save notification button is disabled')
@@ -267,9 +303,9 @@ def confirm_the_prompt(step):
assert_false(world.css_find(btn_css).visible)
@step(u'I am shown a (.*)$')
def i_am_shown_a_notification(step, notification_type):
assert world.is_css_present('.wrapper-%s' % notification_type)
@step(u'I am shown a prompt$')
def i_am_shown_a_notification(step):
assert world.is_css_present('.wrapper-prompt')
def type_in_codemirror(index, text):

View File

@@ -80,9 +80,3 @@ Feature: CMS.Component Adding
And I add a "Blank Advanced Problem" "Advanced Problem" component
And I delete all components
Then I see no components
Scenario: I see a notification on save
Given I am in Studio editing a new unit
And I add a "Discussion" "single step" component
And I edit and save a component
Then I am shown a notification

View File

@@ -2,43 +2,19 @@
#pylint: disable=W0621
from lettuce import world, step
from nose.tools import assert_true, assert_in, assert_equal # pylint: disable=E0611
from common import create_studio_user, add_course_author, log_into_studio
@step(u'I am in Studio editing a new unit$')
def add_unit(step):
world.clear_courses()
course = world.CourseFactory.create()
section = world.ItemFactory.create(parent_location=course.location)
world.ItemFactory.create(
parent_location=section.location,
category='sequential',
display_name='Subsection One',)
user = create_studio_user(is_staff=False)
add_course_author(user, course)
log_into_studio()
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)
from nose.tools import assert_true, assert_in # pylint: disable=E0611
@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'])
css_selector = 'a[data-type="{}"]'.format(component.lower())
world.css_click(css_selector)
world.create_component_instance(
step=step,
category='{}'.format(component.lower()),
)
@step(u'I see this type of single step component:$')
@@ -53,51 +29,24 @@ def see_a_single_step_component(step):
@step(u'I add this type of( Advanced)? (HTML|Problem) component:$')
def add_a_multi_step_component(step, is_advanced, category):
def click_advanced():
css = 'ul.problem-type-tabs a[href="#tab2"]'
world.css_click(css)
my_css = 'ul.problem-type-tabs li.ui-state-active a[href="#tab2"]'
assert(world.css_find(my_css))
def find_matching_link():
"""
Find the link with the specified text. There should be one and only one.
"""
# The tab shows links for the given category
links = world.css_find('div.new-component-{} a'.format(category))
# Find the link whose text matches what you're looking for
matched_links = [link for link in links if link.text == step_hash['Component']]
# There should be one and only one
assert_equal(len(matched_links), 1)
return matched_links[0]
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)
world.css_click(css_selector)
world.wait_for_invisible(css_selector)
if is_advanced:
# Sometimes this click does not work if you go too fast.
world.retry_on_exception(click_advanced, max_attempts=5, ignored_exceptions=AssertionError)
# Retry this in case the list is empty because you tried too fast.
link = world.retry_on_exception(func=find_matching_link, ignored_exceptions=AssertionError)
# Wait for the link to be clickable. If you go too fast it is not.
world.retry_on_exception(click_link)
world.create_component_instance(
step=step,
category='{}'.format(category.lower()),
component_type=step_hash['Component'],
is_advanced=bool(is_advanced),
)
@step(u'I see (HTML|Problem) components in this order:')
def see_a_multi_step_component(step, category):
components = world.css_find('li.component section.xmodule_display')
# Wait for all components to finish rendering
selector = 'li.component section.xmodule_display'
world.wait_for(lambda _: len(world.css_find(selector)) == len(step.hashes))
for idx, step_hash in enumerate(step.hashes):
if category == 'HTML':
html_matcher = {
'Text':
@@ -107,9 +56,11 @@ def see_a_multi_step_component(step, category):
'E-text Written in LaTeX':
'<h2>Example: E-text page</h2>',
}
assert_in(html_matcher[step_hash['Component']], components[idx].html)
actual_html = world.css_html(selector, index=idx)
assert_in(html_matcher[step_hash['Component']], actual_html)
else:
assert_in(step_hash['Component'].upper(), components[idx].text)
actual_text = world.css_text(selector, index=idx)
assert_in(step_hash['Component'].upper(), actual_text)
@step(u'I add a "([^"]*)" "([^"]*)" component$')

View File

@@ -2,30 +2,35 @@
#pylint: disable=C0111
from lettuce import world
from nose.tools import assert_equal, assert_true # pylint: disable=E0611
from nose.tools import assert_equal, assert_true, assert_in # pylint: disable=E0611
from terrain.steps import reload_the_page
@world.absorb
def create_component_instance(step, component_button_css, category,
expected_css, boilerplate=None,
has_multiple_templates=True):
def create_component_instance(step, category, component_type=None, is_advanced=False):
"""
Create a new component in a Unit.
click_new_component_button(step, component_button_css)
Parameters
----------
category: component type (discussion, html, problem, video)
component_type: for components with multiple templates, the link text in the menu
is_advanced: for html and problem, is the desired component under the
advanced menu
"""
assert_in(category, ['problem', 'html', 'video', 'discussion'])
component_button_css = '.large-{}-icon'.format(category.lower())
world.css_click(component_button_css)
if category in ('problem', 'html'):
world.wait_for_invisible(component_button_css)
click_component_from_menu(category, component_type, is_advanced)
def animation_done(_driver):
script = "$('div.new-component').css('display')"
return world.browser.evaluate_script(script) == 'none'
world.wait_for(animation_done)
if has_multiple_templates:
click_component_from_menu(category, boilerplate, expected_css)
if category in ('video',):
world.wait_for_xmodule()
if category == 'problem':
expected_css = 'section.xmodule_CapaModule'
else:
expected_css = 'section.xmodule_{}Module'.format(category.title())
assert_true(world.is_css_present(expected_css))
@@ -33,29 +38,53 @@ def create_component_instance(step, component_button_css, category,
@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)
@world.absorb
def click_component_from_menu(category, boilerplate, expected_css):
def _click_advanced():
css = 'ul.problem-type-tabs a[href="#tab2"]'
world.css_click(css)
my_css = 'ul.problem-type-tabs li.ui-state-active a[href="#tab2"]'
assert(world.css_find(my_css))
def _find_matching_link(category, component_type):
"""
Creates a component from `instance_id`. For components with more
than one template, clicks on `elem_css` to create the new
component. Components with only one template are created as soon
as the user clicks the appropriate button, so we assert that the
expected component is present.
Find the link with the specified text. There should be one and only one.
"""
if boilerplate:
elem_css = "a[data-category='{}'][data-boilerplate='{}']".format(category, boilerplate)
else:
elem_css = "a[data-category='{}']:not([data-boilerplate])".format(category)
elements = world.css_find(elem_css)
assert_equal(len(elements), 1)
world.css_click(elem_css)
# The tab shows links for the given category
links = world.css_find('div.new-component-{} a'.format(category))
# Find the link whose text matches what you're looking for
matched_links = [link for link in links if link.text == component_type]
# There should be one and only one
assert_equal(len(matched_links), 1)
return matched_links[0]
def click_component_from_menu(category, component_type, is_advanced):
"""
Creates a component for a category with more
than one template, i.e. HTML and Problem.
For some problem types, it is necessary to click to
the Advanced tab.
The component_type is the link text, e.g. "Blank Common Problem"
"""
if is_advanced:
# Sometimes this click does not work if you go too fast.
world.retry_on_exception(_click_advanced,
ignored_exceptions=AssertionError)
# Retry this in case the list is empty because you tried too fast.
link = world.retry_on_exception(
lambda: _find_matching_link(category, component_type),
ignored_exceptions=AssertionError
)
# Wait for the link to be clickable. If you go too fast it is not.
world.retry_on_exception(lambda: link.click())
@world.absorb

View File

@@ -58,20 +58,3 @@ Feature: CMS.Course Overview
And I click the "Expand All Sections" link
Then I see the "Collapse All Sections" link
And all sections are expanded
Scenario: Notification is shown on grading status changes
Given I have a course with 1 section
When I navigate to the course overview page
And I change an assignment's grading status
Then I am shown a notification
# Notification is not shown on reorder for IE
# Safari does not have moveMouseTo implemented
@skip_internetexplorer
@skip_safari
Scenario: Notification is shown on subsection reorder
Given I have opened a new course section in Studio
And I have added a new subsection
And I have added a new subsection
When I reorder subsections
Then I am shown a notification

View File

@@ -91,8 +91,7 @@ def i_expand_a_section(step):
@step(u'I see the "([^"]*)" link$')
def i_see_the_span_with_text(step, text):
span_locator = '.toggle-button-sections span'
assert_true(world.is_css_present(span_locator))
assert_equal(world.css_value(span_locator), text)
assert_true(world.css_has_value(span_locator, text))
assert_true(world.css_visible(span_locator))
@@ -128,10 +127,10 @@ def change_grading_status(step):
@step(u'I reorder subsections')
def reorder_subsections(_step):
draggable_css = 'a.drag-handle'
draggable_css = '.subsection-drag-handle'
ele = world.css_find(draggable_css).first
ele.action_chains.drag_and_drop_by_offset(
ele._element,
30,
0
0,
25
).perform()

View File

@@ -151,9 +151,10 @@ def i_see_new_course_image(_step):
assert len(images) == 1
img = images[0]
expected_src = '/c4x/MITx/999/asset/image.jpg'
# Don't worry about the domain in the URL
assert img['src'].endswith(expected_src), "Was looking for {expected}, found {actual}".format(
expected=expected_src, actual=img['src'])
success_func = lambda _: img['src'].endswith(expected_src)
world.wait_for(success_func)
@step('the image URL should be present in the field')

View File

@@ -50,8 +50,8 @@ def other_delete_self(_step):
@step(u'I make "([^"]*)" a course team admin')
def make_course_team_admin(_step, name):
admin_btn_css = '.user-item[data-email="{email}"] .user-actions .add-admin-role'.format(
email=name+'@edx.org')
admin_btn_css = '.user-item[data-email="{name}@edx.org"] .user-actions .add-admin-role'.format(
name=name)
world.css_click(admin_btn_css)
@@ -80,8 +80,8 @@ def see_course(_step, do_not_see, gender='self'):
@step(u'"([^"]*)" should( not)? be marked as an admin')
def marked_as_admin(_step, name, not_marked_admin):
flag_css = '.user-item[data-email="{email}"] .flag-role.flag-role-admin'.format(
email=name+'@edx.org')
flag_css = '.user-item[data-email="{name}@edx.org"] .flag-role.flag-role-admin'.format(
name=name)
if not_marked_admin:
assert world.is_css_not_present(flag_css)
else:

View File

@@ -0,0 +1,21 @@
import os
from lettuce import world
from django.conf import settings
def import_file(filename):
world.browser.execute_script("$('input.file-input').css('display', 'block')")
path = os.path.join(settings.COMMON_TEST_DATA_ROOT, "imports", filename)
world.browser.attach_file('course-data', os.path.abspath(path))
world.css_click('input.submit-button')
# Go to course outline
world.click_course_content()
outline_css = 'li.nav-course-courseware-outline a'
world.css_click(outline_css)
def go_to_import():
menu_css = 'li.nav-course-tools'
import_css = 'li.nav-course-tools-import a'
world.css_click(menu_css)
world.css_click(import_css)

View File

@@ -2,7 +2,7 @@
Feature: CMS.Discussion Component Editor
As a course author, I want to be able to create discussion components.
Scenario: User can view metadata
Scenario: User can view discussion component metadata
Given I have created a Discussion Tag
And I edit and select Settings
Then I see three alphabetized settings and their expected values
@@ -14,7 +14,3 @@ Feature: CMS.Discussion Component Editor
And I edit and select Settings
Then I can modify the display name
And my display name change is persisted on save
Scenario: Creating a discussion takes a single click
Given I have clicked the new unit button
Then creating a discussion takes a single click

View File

@@ -6,11 +6,10 @@ from lettuce import world, step
@step('I have created a Discussion Tag$')
def i_created_discussion_tag(step):
world.create_course_with_unit()
world.create_component_instance(
step, '.large-discussion-icon',
'discussion',
'.xmodule_DiscussionModule',
has_multiple_templates=False
step=step,
category='discussion',
)
@@ -22,12 +21,3 @@ def i_see_only_the_settings_and_values(step):
['Display Name', "Discussion", False],
['Subcategory', "Topic-Level Student-Visible Label", False]
])
@step('creating a discussion takes a single click')
def discussion_takes_a_single_click(step):
component_css = '.xmodule_DiscussionModule'
assert world.is_css_not_present(component_css)
world.css_click("a[data-category='discussion']")
assert world.is_css_present(component_css)

View File

@@ -59,6 +59,17 @@ Feature: CMS.Course Grading
And I go back to the main course page
Then I do see the assignment name "New Type"
# Note that "7" is a special weight because it revealed rounding errors (STUD-826).
Scenario: Users can set weight to Assignment types
Given I have opened a new course in Studio
And I am viewing the grading settings
When I add a new assignment type "New Type"
And I set the assignment weight to "7"
And I press the "Save" notification button
Then the assignment weight is displayed as "7"
And I reload the page
Then the assignment weight is displayed as "7"
Scenario: Settings are only persisted when saved
Given I have opened a new course in Studio
And I have populated the course

View File

@@ -106,6 +106,22 @@ def add_assignment_type(step, new_name):
new_assignment._element.send_keys(new_name)
@step(u'I set the assignment weight to "([^"]*)"$')
def set_weight(step, weight):
weight_id = '#course-grading-assignment-gradeweight'
weight_field = world.css_find(weight_id)[-1]
old_weight = world.css_value(weight_id, -1)
for count in range(len(old_weight)):
weight_field._element.send_keys(Keys.END, Keys.BACK_SPACE)
weight_field._element.send_keys(weight)
@step(u'the assignment weight is displayed as "([^"]*)"$')
def verify_weight(step, weight):
weight_id = '#course-grading-assignment-gradeweight'
assert_equal(world.css_value(weight_id, -1), weight)
@step(u'I have populated the course')
def populate_course(step):
step.given('I have added a new section')
@@ -164,7 +180,7 @@ def cannot_edit_fail(_step):
def i_change_grace_period(_step, grace_period):
grace_period_css = '#course-grading-graceperiod'
ele = world.css_find(grace_period_css).first
# Sometimes it takes a moment for the JavaScript
# to populate the field. If we don't wait for
# this to happen, then we can end up with

View File

@@ -6,9 +6,11 @@ from lettuce import world, step
@step('I have created a Blank HTML Page$')
def i_created_blank_html_page(step):
world.create_course_with_unit()
world.create_component_instance(
step, '.large-html-icon', 'html',
'.xmodule_HtmlModule'
step=step,
category='html',
component_type='Text'
)
@@ -18,11 +20,10 @@ def i_see_only_the_html_display_name(step):
@step('I have created an E-text Written in LaTeX$')
def i_created_blank_html_page(step):
def i_created_etext_in_latex(step):
world.create_course_with_unit()
world.create_component_instance(
step,
'.large-html-icon',
'html',
'.xmodule_HtmlModule',
'latex_html.yaml'
step=step,
category='html',
component_type='E-text Written in LaTeX'
)

View File

@@ -89,3 +89,13 @@ Feature: CMS.Problem Editor
When I edit and compile the High Level Source
Then my change to the High Level Source is persisted
And when I view the High Level Source I see my changes
Scenario: Exceptions don't cause problem to be uneditable (bug STUD-786)
Given I have an empty course
And I go to the import page
And I import the file "get_html_exception_test.tar.gz"
When I go to the unit "Probability and BMI"
And I click on "edit a draft"
Then I see a message that says "We're having trouble rendering your component"
And I can edit the problem

View File

@@ -1,9 +1,12 @@
# disable missing docstring
#pylint: disable=C0111
import json
from lettuce import world, step
from nose.tools import assert_equal, assert_true # pylint: disable=E0611
from common import type_in_codemirror
from common import type_in_codemirror, open_new_course
from course_import import import_file, go_to_import
DISPLAY_NAME = "Display Name"
MAXIMUM_ATTEMPTS = "Maximum Attempts"
@@ -14,17 +17,16 @@ SHOW_ANSWER = "Show Answer"
@step('I have created a Blank Common Problem$')
def i_created_blank_common_problem(step):
world.create_course_with_unit()
world.create_component_instance(
step,
'.large-problem-icon',
'problem',
'.xmodule_CapaModule',
'blank_common.yaml'
step=step,
category='problem',
component_type='Blank Common Problem'
)
@step('I edit and select Settings$')
def i_edit_and_select_settings(step):
def i_edit_and_select_settings(_step):
world.edit_component_and_select_settings()
@@ -41,7 +43,7 @@ def i_see_advanced_settings_with_values(step):
@step('I can modify the display name')
def i_can_modify_the_display_name(step):
def i_can_modify_the_display_name(_step):
# Verifying that the display name can be a string containing a floating point value
# (to confirm that we don't throw an error because it is of the wrong type).
index = world.get_setting_entry_index(DISPLAY_NAME)
@@ -58,7 +60,7 @@ def my_display_name_change_is_persisted_on_save(step):
@step('I can specify special characters in the display name')
def i_can_modify_the_display_name_with_special_chars(step):
def i_can_modify_the_display_name_with_special_chars(_step):
index = world.get_setting_entry_index(DISPLAY_NAME)
world.css_fill('.wrapper-comp-setting .setting-input', "updated ' \" &", index=index)
if world.is_firefox():
@@ -73,7 +75,7 @@ def special_chars_persisted_on_save(step):
@step('I can revert the display name to unset')
def can_revert_display_name_to_unset(step):
def can_revert_display_name_to_unset(_step):
world.revert_setting_entry(DISPLAY_NAME)
verify_unset_display_name()
@@ -85,7 +87,7 @@ def my_display_name_is_persisted_on_save(step):
@step('I can select Per Student for Randomization')
def i_can_select_per_student_for_randomization(step):
def i_can_select_per_student_for_randomization(_step):
world.browser.select(RANDOMIZATION, "Per Student")
verify_modified_randomization()
@@ -104,7 +106,7 @@ def i_can_revert_to_default_for_randomization(step):
@step('I can set the weight to "(.*)"?')
def i_can_set_weight(step, weight):
def i_can_set_weight(_step, weight):
set_weight(weight)
verify_modified_weight()
@@ -164,25 +166,24 @@ def cancel_does_not_save_changes(step):
@step('I have created a LaTeX Problem')
def create_latex_problem(step):
world.click_new_component_button(step, '.large-problem-icon')
def animation_done(_driver):
return world.browser.evaluate_script("$('div.new-component').css('display')") == 'none'
world.wait_for(animation_done)
# Go to advanced tab.
world.css_click('#ui-id-2')
world.click_component_from_menu("problem", "latex_problem.yaml", '.xmodule_CapaModule')
world.create_course_with_unit()
world.create_component_instance(
step=step,
category='problem',
component_type='Problem Written in LaTeX',
is_advanced=True
)
@step('I edit and compile the High Level Source')
def edit_latex_source(step):
def edit_latex_source(_step):
open_high_level_source()
type_in_codemirror(1, "hi")
world.css_click('.hls-compile')
@step('my change to the High Level Source is persisted')
def high_level_source_persisted(step):
def high_level_source_persisted(_step):
def verify_text(driver):
css_sel = '.problem div>span'
return world.css_text(css_sel) == 'hi'
@@ -191,11 +192,53 @@ def high_level_source_persisted(step):
@step('I view the High Level Source I see my changes')
def high_level_source_in_editor(step):
def high_level_source_in_editor(_step):
open_high_level_source()
assert_equal('hi', world.css_value('.source-edit-box'))
@step(u'I have an empty course')
def i_have_empty_course(step):
open_new_course()
@step(u'I go to the import page')
def i_go_to_import(_step):
go_to_import()
@step(u'I import the file "([^"]*)"$')
def i_import_the_file(_step, filename):
import_file(filename)
@step(u'I click on "edit a draft"$')
def i_edit_a_draft(_step):
world.css_click("a.create-draft")
@step(u'I go to the vertical "([^"]*)"$')
def i_go_to_vertical(_step, vertical):
world.css_click("span:contains('{0}')".format(vertical))
@step(u'I go to the unit "([^"]*)"$')
def i_go_to_unit(_step, unit):
loc = "window.location = $(\"span:contains('{0}')\").closest('a').attr('href')".format(unit)
world.browser.execute_script(loc)
@step(u'I see a message that says "([^"]*)"$')
def i_can_see_message(_step, msg):
msg = json.dumps(msg) # escape quotes
world.css_has_text("h2.title", msg)
@step(u'I can edit the problem$')
def i_can_edit_problem(_step):
world.edit_component()
def verify_high_level_source_links(step, visible):
if visible:
assert_true(world.is_css_present('.launch-latex-compiler'),

View File

@@ -5,8 +5,6 @@ from lettuce import world, step
from common import *
from nose.tools import assert_equal # pylint: disable=E0611
############### ACTIONS ####################
@step('I click the New Section link$')
def i_click_new_section_link(_step):
@@ -53,9 +51,6 @@ def i_see_a_mini_notification(_step, _type):
assert world.is_css_present(saving_css)
############ ASSERTIONS ###################
@step('I see my section on the Courseware page$')
def i_see_my_section_on_the_courseware_page(_step):
see_my_section_on_the_courseware_page('My Section')
@@ -125,8 +120,6 @@ def the_section_release_date_is_updated(_step):
assert_equal(status_text, 'Will Release: 12/25/2013 at 00:00 UTC')
############ HELPER METHODS ###################
def save_section_name(name):
name_css = '.new-section-name'
save_css = '.new-section-name-save'

View File

@@ -47,7 +47,7 @@ def name_textbook(_step, name):
@step(u'I name the (first|second|third) chapter "([^"]*)"')
def name_chapter(_step, ordinal, name):
index = ["first", "second", "third"].index(ordinal)
input_css = ".textbook .chapter{i} input.chapter-name".format(i=index+1)
input_css = ".textbook .chapter{i} input.chapter-name".format(i=index + 1)
world.css_fill(input_css, name)
if world.is_firefox():
world.trigger_event(input_css)
@@ -56,7 +56,7 @@ def name_chapter(_step, ordinal, name):
@step(u'I type in "([^"]*)" for the (first|second|third) chapter asset')
def asset_chapter(_step, name, ordinal):
index = ["first", "second", "third"].index(ordinal)
input_css = ".textbook .chapter{i} input.chapter-asset-path".format(i=index+1)
input_css = ".textbook .chapter{i} input.chapter-asset-path".format(i=index + 1)
world.css_fill(input_css, name)
if world.is_firefox():
world.trigger_event(input_css)
@@ -65,7 +65,7 @@ def asset_chapter(_step, name, ordinal):
@step(u'I click the Upload Asset link for the (first|second|third) chapter')
def click_upload_asset(_step, ordinal):
index = ["first", "second", "third"].index(ordinal)
button_css = ".textbook .chapter{i} .action-upload".format(i=index+1)
button_css = ".textbook .chapter{i} .action-upload".format(i=index + 1)
world.css_click(button_css)

View File

@@ -191,7 +191,7 @@ def view_asset(_step, status):
# Note that world.visit would trigger a 403 error instead of displaying "Unauthorized"
# Instead, we can drop back into the selenium driver get command.
world.browser.driver.get(url)
assert_equal(world.css_text('body'),expected_text)
assert_equal(world.css_text('body'), expected_text)
@step('I see a confirmation that the file was deleted$')

View File

@@ -15,12 +15,17 @@ Feature: CMS.Video Component Editor
Then I can modify the display name
And my video display name change is persisted on save
# Disabling this 10/7/13 due to nondeterministic behavior
# in master. The failure seems to occur when YouTube does
# not respond quickly enough, so that the video player
# doesn't load.
#
# Sauce Labs cannot delete cookies
@skip_sauce
Scenario: Captions are hidden when "show captions" is false
Given I have created a Video component with subtitles
And I have set "show captions" to False
Then when I view the video it does not show the captions
# @skip_sauce
#Scenario: Captions are hidden when "show captions" is false
# Given I have created a Video component with subtitles
# And I have set "show captions" to False
# Then when I view the video it does not show the captions
# Sauce Labs cannot delete cookies
@skip_sauce

View File

@@ -2,28 +2,33 @@
Feature: CMS.Video Component
As a course author, I want to be able to view my created videos in Studio.
# 1
# Video Alpha Features will work in Firefox only when Firefox is the active window
Scenario: Autoplay is disabled in Studio
Given I have created a Video component
Then when I view the video it does not have autoplay enabled
# 2
Scenario: Creating a video takes a single click
Given I have clicked the new unit button
Then creating a video takes a single click
# 3
# Sauce Labs cannot delete cookies
@skip_sauce
Scenario: Captions are hidden correctly
Given I have created a Video component with subtitles
And I have hidden captions
Then when I view the video it does not show the captions
# @skip_sauce
#Scenario: Captions are hidden correctly
# Given I have created a Video component with subtitles
# And I have hidden captions
# Then when I view the video it does not show the captions
# 4
# Sauce Labs cannot delete cookies
@skip_sauce
Scenario: Captions are shown correctly
Given I have created a Video component with subtitles
Then when I view the video it does show the captions
# 5
# Sauce Labs cannot delete cookies
@skip_sauce
Scenario: Captions are toggled correctly
@@ -31,7 +36,36 @@ Feature: CMS.Video Component
And I have toggled captions
Then when I view the video it does show the captions
# 6
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
# 7
# Scenario: Closed captions become visible when the mouse hovers over CC button
# Given I have created a Video component with subtitles
# And Make sure captions are closed
# Then Captions become "invisible" after 3 seconds
# And I hover over button "CC"
# Then Captions become "visible"
# And I hover over button "volume"
# Then Captions become "invisible" after 3 seconds
# 8
#Scenario: Open captions never become invisible
# Given I have created a Video component with subtitles
# And Make sure captions are open
# Then Captions are "visible"
# And I hover over button "CC"
# Then Captions are "visible"
# And I hover over button "volume"
# Then Captions are "visible"
# 9
#Scenario: Closed captions are invisible when mouse doesn't hover on CC button
# Given I have created a Video component with subtitles
# And Make sure captions are closed
# Then Captions become "invisible" after 3 seconds
# And I hover over button "volume"
# Then Captions are "invisible"

View File

@@ -4,14 +4,18 @@ from lettuce import world, step
from xmodule.modulestore import Location
from contentstore.utils import get_modulestore
BUTTONS = {
'CC': '.hide-subtitles',
'volume': '.volume',
}
@step('I have created a Video component$')
def i_created_a_video_component(step):
world.create_course_with_unit()
world.create_component_instance(
step, '.large-video-icon',
'video',
'.xmodule_VideoModule',
has_multiple_templates=False
step=step,
category='video',
)
@@ -19,6 +23,7 @@ def i_created_a_video_component(step):
def i_created_a_video_with_subs(_step):
_step.given('I have created a Video component with subtitles "OEoXaMPEzfM"')
@step('I have created a Video component with subtitles "([^"]*)"$')
def i_created_a_video_with_subs_with_name(_step, sub_id):
_step.given('I have created a Video component')
@@ -115,3 +120,37 @@ 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']
@step('Make sure captions are (.+)$')
def set_captions_visibility_state(_step, captions_state):
if captions_state == 'closed':
if world.css_visible('.subtitles'):
world.browser.find_by_css('.hide-subtitles').click()
else:
if not world.css_visible('.subtitles'):
world.browser.find_by_css('.hide-subtitles').click()
@step('I hover over button "([^"]*)"$')
def hover_over_button(_step, button):
world.css_find(BUTTONS[button.strip()]).mouse_over()
@step('Captions (?:are|become) "([^"]*)"$')
def are_captions_visibile(_step, visibility_state):
_step.given('Captions become "{0}" after 0 seconds'.format(visibility_state))
@step('Captions (?:are|become) "([^"]*)" after (.+) seconds$')
def check_captions_visibility_state(_step, visibility_state, timeout):
timeout = int(timeout.strip())
# Captions become invisible by fading out. We must wait by a specified
# time.
world.wait(timeout)
if visibility_state == 'visible':
assert world.css_visible('.subtitles')
else:
assert not world.css_visible('.subtitles')

View File

@@ -1,62 +0,0 @@
"""
Script for dumping course dumping the course structure
"""
from django.core.management.base import BaseCommand, CommandError
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.django import modulestore
from json import dumps
from xmodule.modulestore.inheritance import own_metadata
from django.conf import settings
filter_list = ['xml_attributes', 'checklists']
class Command(BaseCommand):
"""
The Django command for dumping course structure
"""
help = '''Write out to stdout a structural and metadata information about a course in a flat dictionary serialized
in a JSON format. This can be used for analytics.'''
def handle(self, *args, **options):
"Execute the command"
if len(args) < 2 or len(args) > 3:
raise CommandError("dump_course_structure requires two or more arguments: <location> <outfile> |<db>|")
course_id = args[0]
outfile = args[1]
# use a user-specified database name, if present
# this is useful for doing dumps from databases restored from prod backups
if len(args) == 3:
settings.MODULESTORE['direct']['OPTIONS']['db'] = args[2]
loc = CourseDescriptor.id_to_location(course_id)
store = modulestore()
course = None
try:
course = store.get_item(loc, depth=4)
except:
print('Could not find course at {0}'.format(course_id))
return
info = {}
def dump_into_dict(module, info):
filtered_metadata = dict((key, value) for key, value in own_metadata(module).iteritems()
if key not in filter_list)
info[module.location.url()] = {
'category': module.location.category,
'children': module.children if hasattr(module, 'children') else [],
'metadata': filtered_metadata
}
for child in module.get_children():
dump_into_dict(child, info)
dump_into_dict(course, info)
with open(outfile, 'w') as f:
f.write(dumps(info))

View File

@@ -792,7 +792,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
source_location.tag, source_location.org, source_location.course, 'html', 'nonportable'])
html_module = module_store.get_instance(source_location.course_id, html_module_location)
self.assertTrue(isinstance(html_module.data, basestring))
self.assertIsInstance(html_module.data, basestring)
new_data = html_module.data.replace('/static/', '/c4x/{0}/{1}/asset/'.format(
source_location.org, source_location.course))
module_store.update_item(html_module_location, new_data)
@@ -1273,6 +1273,47 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# export out to a tempdir
export_to_xml(module_store, content_store, location, root_dir, 'test_export')
def test_export_course_without_content_store(self):
module_store = modulestore('direct')
content_store = contentstore()
# Create toy course
import_from_xml(module_store, 'common/test/data/', ['toy'])
location = CourseDescriptor.id_to_location('edX/toy/2012_Fall')
# Add a sequence
stub_location = Location(['i4x', 'edX', 'toy', 'sequential', 'vertical_sequential'])
sequential = module_store.get_item(stub_location)
module_store.update_children(sequential.location, sequential.children)
# Get course and export it without a content_store
course = module_store.get_item(location)
course.save()
root_dir = path(mkdtemp_clean())
print 'Exporting to tempdir = {0}'.format(root_dir)
export_to_xml(module_store, None, location, root_dir, 'test_export_no_content_store')
# Delete the course from module store and reimport it
delete_course(module_store, content_store, location, commit=True)
import_from_xml(
module_store, root_dir, ['test_export_no_content_store'],
draft_store=None,
static_content_store=None,
target_location_namespace=course.location
)
# Verify reimported course
items = module_store.get_items(stub_location)
self.assertEqual(len(items), 1)
@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE, MODULESTORE=TEST_MODULESTORE)
class ContentStoreTest(ModuleStoreTestCase):
@@ -1484,7 +1525,7 @@ class ContentStoreTest(ModuleStoreTestCase):
resp = self.client.get(reverse('course_index', kwargs=data))
self.assertContains(
resp,
'<article class="courseware-overview" data-course-id="i4x://MITx/999/course/Robot_Super_Course">',
'<article class="courseware-overview" data-id="i4x://MITx/999/course/Robot_Super_Course">',
status_code=200,
html=True
)
@@ -1588,14 +1629,7 @@ class ContentStoreTest(ModuleStoreTestCase):
'name': loc.name}))
self.assertEqual(resp.status_code, 200)
# static_pages
resp = self.client.get(reverse('static_pages',
kwargs={'org': loc.org,
'course': loc.course,
'coursename': loc.name}))
self.assertEqual(resp.status_code, 200)
# static_pages
# asset_index
resp = self.client.get(reverse('asset_index',
kwargs={'org': loc.org,
'course': loc.course,

View File

@@ -3,7 +3,7 @@ import mock
from django.test import TestCase
from django.core.urlresolvers import reverse
from contentstore.views.requests import event as cms_user_track
from contentstore.views.helpers import event as cms_user_track
class CMSLogTest(TestCase):

View File

@@ -9,13 +9,13 @@ from .checklist import *
from .component import *
from .course import *
from .error import *
from .helpers import *
from .item import *
from .import_export import *
from .preview import *
from .public import *
from .user import *
from .tabs import *
from .requests import *
try:
from .dev import *
except ImportError:

View File

@@ -26,7 +26,7 @@ from contentstore.utils import (get_modulestore, get_lms_link_for_item,
from models.settings.course_grading import CourseGradingModel
from .requests import _xmodule_recurse
from .helpers import _xmodule_recurse
from .access import has_access
from xmodule.x_module import XModuleDescriptor
from xblock.plugin import PluginMissingError

View File

@@ -193,6 +193,7 @@ def import_course(request, org, course, name):
if not dirpath:
return JsonResponse(
{
'ErrMsg': _('Could not find the course.xml file in the package.'),
'Stage': 2
},

View File

@@ -12,7 +12,7 @@ from xmodule.modulestore.inheritance import own_metadata
from util.json_request import expect_json, JsonResponse
from ..utils import get_modulestore
from .access import has_access
from .requests import _xmodule_recurse
from .helpers import _xmodule_recurse
from xmodule.x_module import XModuleDescriptor
__all__ = ['save_item', 'create_item', 'delete_item']

View File

@@ -6,7 +6,7 @@ from django.conf import settings
from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden
from django.core.urlresolvers import reverse
from django.contrib.auth.decorators import login_required
from mitxmako.shortcuts import render_to_response
from mitxmako.shortcuts import render_to_response, render_to_string
from xmodule_modifiers import replace_static_urls, wrap_xmodule
from xmodule.error_module import ErrorDescriptor
@@ -22,7 +22,7 @@ from util.sandboxing import can_execute_unsafe_code
import static_replace
from .session_kv_store import SessionKeyValueStore
from .requests import render_from_lms
from .helpers import render_from_lms
from .access import has_access
from ..utils import get_course_for_item
@@ -79,9 +79,17 @@ def preview_component(request, location):
# can bind to it correctly
component.runtime.wrappers.append(partial(wrap_xmodule, 'xmodule_edit.html'))
try:
content = component.render('studio_view').content
# catch exceptions indiscriminately, since after this point they escape the
# dungeon and surface as uneditable, unsaveable, and undeletable
# component-goblins.
except Exception as exc: # pylint: disable=W0703
content = render_to_string('html_error.html', {'message': str(exc)})
return render_to_response('component.html', {
'preview': get_preview_html(request, component, 0),
'editor': component.runtime.render(component, None, 'studio_view').content,
'editor': content
})
@@ -95,11 +103,6 @@ def preview_module_system(request, preview_id, descriptor):
descriptor: An XModuleDescriptor
"""
def preview_field_data(descriptor):
"Helper method to create a DbModel from a descriptor"
student_data = DbModel(SessionKeyValueStore(request))
return lms_field_data(descriptor._field_data, student_data)
course_id = get_course_for_item(descriptor.location).location.course_id
if descriptor.location.category == 'static_tab':
@@ -118,7 +121,6 @@ def preview_module_system(request, preview_id, descriptor):
debug=True,
replace_urls=partial(static_replace.replace_static_urls, data_directory=None, course_id=course_id),
user=request.user,
xmodule_field_data=preview_field_data,
can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)),
mixins=settings.XBLOCK_MIXINS,
course_id=course_id,
@@ -136,7 +138,8 @@ def preview_module_system(request, preview_id, descriptor):
getattr(descriptor, 'data_dir', descriptor.location.course),
course_id=descriptor.location.org + '/' + descriptor.location.course + '/BOGUS_RUN_REPLACE_WHEN_AVAILABLE',
),
)
),
error_descriptor_class=ErrorDescriptor,
)
@@ -148,17 +151,12 @@ def load_preview_module(request, preview_id, descriptor):
preview_id (str): An identifier specifying which preview this module is used for
descriptor: An XModuleDescriptor
"""
system = preview_module_system(request, preview_id, descriptor)
try:
module = descriptor.xmodule(system)
except:
log.debug("Unable to load preview module", exc_info=True)
module = ErrorDescriptor.from_descriptor(
descriptor,
error_msg=exc_info_to_str(sys.exc_info())
).xmodule(system)
return module
student_data = DbModel(SessionKeyValueStore(request))
descriptor.bind_for_student(
preview_module_system(request, preview_id, descriptor),
lms_field_data(descriptor._field_data, student_data), # pylint: disable=protected-access
)
return descriptor
def get_preview_html(request, descriptor, idx):
@@ -167,4 +165,8 @@ def get_preview_html(request, descriptor, idx):
specified by the descriptor and idx.
"""
module = load_preview_module(request, str(idx), descriptor)
return module.runtime.render(module, None, "student_view").content
try:
content = module.render("student_view").content
except Exception as exc: # pylint: disable=W0703
content = render_to_string('html_error.html', {'message': str(exc)})
return content

View File

@@ -14,10 +14,9 @@ from xmodule.modulestore.inheritance import own_metadata
from xmodule.modulestore.django import modulestore
from ..utils import get_course_for_item, get_modulestore
from .access import get_location_and_verify_access
__all__ = ['edit_tabs', 'reorder_static_tabs', 'static_pages']
__all__ = ['edit_tabs', 'reorder_static_tabs']
def initialize_course_tabs(course):
@@ -126,20 +125,6 @@ def edit_tabs(request, org, course, coursename):
})
@login_required
@ensure_csrf_cookie
def static_pages(request, org, course, coursename):
"Static pages view"
location = get_location_and_verify_access(request, org, course, coursename)
course = modulestore().get_item(location)
return render_to_response('static-pages.html', {
'context_course': course,
})
# "primitive" tab edit functions driven by the command line.
# These should be replaced/deleted by a more capable GUI someday.
# Note that the command line UI identifies the tabs with 1-based

View File

@@ -12,20 +12,24 @@ from .common import *
from logsettings import get_logger_config
import os
# specified as an environment variable. Typically this is set
# in the service's upstart script and corresponds exactly to the service name.
# Service variants apply config differences via env and auth JSON files,
# the names of which correspond to the variant.
# SERVICE_VARIANT specifies name of the variant used, which decides what JSON
# configuration files are read during startup.
SERVICE_VARIANT = os.environ.get('SERVICE_VARIANT', None)
# when not variant is specified we attempt to load an unvaried
# config set.
CONFIG_PREFIX = ""
# CONFIG_ROOT specifies the directory where the JSON configuration
# files are expected to be found. If not specified, use the project
# directory.
CONFIG_ROOT = os.environ.get('CONFIG_ROOT', ENV_ROOT)
# CONFIG_PREFIX specifies the prefix of the JSON configuration files,
# based on the service variant. If no variant is use, don't use a
# prefix.
CONFIG_PREFIX = SERVICE_VARIANT + "." if SERVICE_VARIANT else ""
if SERVICE_VARIANT:
CONFIG_PREFIX = SERVICE_VARIANT + "."
############### ALWAYS THE SAME ################################
DEBUG = False
TEMPLATE_DEBUG = False
@@ -77,7 +81,7 @@ CELERY_QUEUES = {
############# NON-SECURE ENV CONFIG ##############################
# Things like server locations, ports, etc.
with open(ENV_ROOT / CONFIG_PREFIX + "env.json") as env_file:
with open(CONFIG_ROOT / CONFIG_PREFIX + "env.json") as env_file:
ENV_TOKENS = json.load(env_file)
EMAIL_BACKEND = ENV_TOKENS.get('EMAIL_BACKEND', EMAIL_BACKEND)
@@ -134,7 +138,7 @@ if "TRACKING_IGNORE_URL_PATTERNS" in ENV_TOKENS:
################ SECURE AUTH ITEMS ###############################
# Secret things: passwords, access keys, etc.
with open(ENV_ROOT / CONFIG_PREFIX + "auth.json") as auth_file:
with open(CONFIG_ROOT / CONFIG_PREFIX + "auth.json") as auth_file:
AUTH_TOKENS = json.load(auth_file)
# If Segment.io key specified, load it and turn on Segment.io if the feature flag is set

View File

@@ -218,6 +218,11 @@ STATICFILES_DIRS = [
TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html
# We want i18n to be turned off in production, at least until we have full localizations.
# Thus we want the Django translation engine to be disabled. Otherwise even without
# localization files, if the user's browser is set to a language other than us-en,
# strings like "login" and "password" will be translated and the rest of the page will be
# in English, which is confusing.
USE_I18N = False
USE_L10N = True

View File

@@ -9,6 +9,7 @@ from .common import *
from logsettings import get_logger_config
DEBUG = True
USE_I18N = True
TEMPLATE_DEBUG = DEBUG
LOGGING = get_logger_config(ENV_ROOT / "log",
logging_env="dev",

View File

@@ -0,0 +1,12 @@
"""
This configuration is have localdev use a preview.localhost hostname for the preview LMS so that we can share
the same process between preview and published
"""
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .dev import *
MITX_FEATURES['PREVIEW_LMS_BASE'] = "preview.localhost:8000"

View File

@@ -1,5 +1,5 @@
requirejs.config({
paths: {
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",
@@ -22,7 +22,7 @@ requirejs.config({
"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",
"youtube": "//www.youtube.com/player_api?noext",
"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",
@@ -32,9 +32,11 @@ requirejs.config({
"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",
"draggabilly": "xmodule_js/common_static/js/vendor/draggabilly.pkgd",
"domReady": "xmodule_js/common_static/js/vendor/domReady",
"coffee/src/ajax_prefix": "xmodule_js/common_static/coffee/src/ajax_prefix"
},
}
shim: {
"gettext": {
exports: "gettext"
@@ -100,6 +102,9 @@ requirejs.config({
deps: ["backbone"],
exports: "Backbone.Associations"
},
"youtube": {
exports: "YT"
},
"codemirror": {
exports: "CodeMirror"
},
@@ -139,12 +144,14 @@ define([
"coffee/spec/models/course_spec", "coffee/spec/models/metadata_spec",
"coffee/spec/models/module_spec", "coffee/spec/models/section_spec",
"coffee/spec/models/settings_course_grader_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/overview_spec",
"coffee/spec/views/textbook_spec", "coffee/spec/views/upload_spec",
# these tests are run separate in the cms-squire suite, due to process

View File

@@ -22,7 +22,7 @@ requirejs.config({
"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",
"youtube": "//www.youtube.com/player_api?noext",
"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",
@@ -100,6 +100,9 @@ requirejs.config({
deps: ["backbone"],
exports: "Backbone.Associations"
},
"youtube": {
exports: "YT"
},
"codemirror": {
exports: "CodeMirror"
},

View File

@@ -0,0 +1,20 @@
define ["js/models/settings/course_grader"], (CourseGrader) ->
describe "CourseGraderModel", ->
describe "parseWeight", ->
it "converts a float to an integer", ->
model = new CourseGrader({weight: 7.0001, min_count: 3.67, drop_count: 1.88}, {parse:true})
expect(model.get('weight')).toBe(7)
expect(model.get('min_count')).toBe(3)
expect(model.get('drop_count')).toBe(1)
it "converts a string to an integer", ->
model = new CourseGrader({weight: '7.0001', min_count: '3.67', drop_count: '1.88'}, {parse:true})
expect(model.get('weight')).toBe(7)
expect(model.get('min_count')).toBe(3)
expect(model.get('drop_count')).toBe(1)
it "does a no-op for integers", ->
model = new CourseGrader({weight: 7, min_count: 3, drop_count: 1}, {parse:true})
expect(model.get('weight')).toBe(7)
expect(model.get('min_count')).toBe(3)
expect(model.get('drop_count')).toBe(1)

View File

@@ -1,61 +0,0 @@
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"]

View File

@@ -10,6 +10,7 @@ define ["js/views/course_info_handout", "js/views/course_info_update", "js/model
</div>
<div class="sidebar window course-handouts" id="course-handouts-view"></div>
</div>
<div class="modal-cover"></div>
"""
beforeEach ->
@@ -45,13 +46,56 @@ define ["js/views/course_info_handout", "js/views/course_info_update", "js/model
preventDefault : () -> 'no op'
}
@createNewUpdate = () ->
@createNewUpdate = (text) ->
# Edit button is not in the template under test (it is in parent HTML).
# Therefore call onNew directly.
@courseInfoEdit.onNew(@event)
spyOn(@courseInfoEdit.$codeMirror, 'getValue').andReturn('/static/image.jpg')
spyOn(@courseInfoEdit.$codeMirror, 'getValue').andReturn(text)
@courseInfoEdit.$el.find('.save-button').click()
@cancelNewCourseInfo = (useCancelButton) ->
spyOn(@courseInfoEdit.$modalCover, 'show').andCallThrough()
spyOn(@courseInfoEdit.$modalCover, 'hide').andCallThrough()
@courseInfoEdit.onNew(@event)
expect(@courseInfoEdit.$modalCover.show).toHaveBeenCalled()
spyOn(@courseInfoEdit.$codeMirror, 'getValue').andReturn('unsaved changes')
model = @collection.at(0)
spyOn(model, "save").andCallThrough()
cancelEditingUpdate(useCancelButton)
expect(@courseInfoEdit.$modalCover.hide).toHaveBeenCalled()
expect(model.save).not.toHaveBeenCalled()
previewContents = @courseInfoEdit.$el.find('.update-contents').html()
expect(previewContents).not.toEqual('unsaved changes')
@cancelExistingCourseInfo = (useCancelButton) ->
@createNewUpdate('existing update')
spyOn(@courseInfoEdit.$modalCover, 'show').andCallThrough()
spyOn(@courseInfoEdit.$modalCover, 'hide').andCallThrough()
@courseInfoEdit.$el.find('.edit-button').click()
expect(@courseInfoEdit.$modalCover.show).toHaveBeenCalled()
spyOn(@courseInfoEdit.$codeMirror, 'getValue').andReturn('modification')
model = @collection.at(0)
spyOn(model, "save").andCallThrough()
cancelEditingUpdate(useCancelButton)
expect(@courseInfoEdit.$modalCover.hide).toHaveBeenCalled()
expect(model.save).not.toHaveBeenCalled()
previewContents = @courseInfoEdit.$el.find('.update-contents').html()
expect(previewContents).toEqual('existing update')
cancelEditingUpdate = (update, useCancelButton) ->
if useCancelButton
update.$el.find('.cancel-button').click()
else
$('.modal-cover').click()
afterEach ->
@xhrRestore()
@@ -75,19 +119,30 @@ define ["js/views/course_info_handout", "js/views/course_info_update", "js/model
it "does rewrite links for preview", ->
# Create a new update.
@createNewUpdate()
@createNewUpdate('/static/image.jpg')
# Verify the link is rewritten for preview purposes.
previewContents = @courseInfoEdit.$el.find('.update-contents').html()
expect(previewContents).toEqual('base-asset-url/image.jpg')
it "shows static links in edit mode", ->
@createNewUpdate()
@createNewUpdate('/static/image.jpg')
# Click edit and verify CodeMirror contents.
@courseInfoEdit.$el.find('.edit-button').click()
expect(@courseInfoEdit.$codeMirror.getValue()).toEqual('/static/image.jpg')
it "removes newly created course info on cancel", ->
@cancelNewCourseInfo(true)
it "removes newly created course info on click outside modal", ->
@cancelNewCourseInfo(false)
it "does not remove existing course info on cancel", ->
@cancelExistingCourseInfo(true)
it "does not remove existing course info on click outside modal", ->
@cancelExistingCourseInfo(false)
describe "Course Handouts", ->
handoutsTemplate = readFixtures('course_info_handouts.underscore')

View File

@@ -5,7 +5,6 @@ define ["coffee/src/views/module_edit", "xmodule"], (ModuleEdit) ->
@stubModule = jasmine.createSpy("Module")
@stubModule.id = 'stub-id'
setFixtures """
<li class="component" id="stub-id">
<div class="component-editor">
@@ -19,7 +18,7 @@ define ["coffee/src/views/module_edit", "xmodule"], (ModuleEdit) ->
<a href="#" class="edit-button"><span class="edit-icon white"></span>Edit</a>
<a href="#" class="delete-button"><span class="delete-icon white"></span>Delete</a>
</div>
<a href="#" class="drag-handle"></a>
<span class="drag-handle"></span>
<section class="xmodule_display xmodule_stub" data-type="StubModule">
<div id="stub-module-content"/>
</section>

View File

@@ -0,0 +1,377 @@
define ["js/views/overview", "js/views/feedback_notification", "sinon", "js/base", "date", "jquery.timepicker"],
(OverviewDragger, Notification, sinon) ->
describe "Course Overview", ->
beforeEach ->
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>
"""
appendSetFixtures """
<ol>
<li class="subsection-list branch" data-id="subsection-1-id" id="subsection-1">
<ol class="sortable-unit-list" id="subsection-list-1">
<li class="unit" id="unit-1" data-id="first-unit-id" data-parent-id="subsection-1-id"></li>
<li class="unit" id="unit-2" data-id="second-unit-id" data-parent-id="subsection-1-id"></li>
<li class="unit" id="unit-3" data-id="third-unit-id" data-parent-id="subsection-1-id"></li>
</ol>
</li>
<li class="subsection-list branch" data-id="subsection-2-id" id="subsection-2">
<ol class="sortable-unit-list" id="subsection-list-2">
<li class="unit" id="unit-4" data-id="fourth-unit-id" data-parent-id="subsection-2"></li>
</ol>
</li>
<li class="subsection-list branch" data-id="subsection-3-id" id="subsection-3">
<ol class="sortable-unit-list" id="subsection-list-3">
</li>
</ol>
"""
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(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)
OverviewDragger.makeDraggable(
'.unit',
'.unit-drag-handle',
'ol.sortable-unit-list',
'li.branch, article.subsection-body'
)
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()
# Fails sporadically in Jenkins.
# it "should delete model when delete is clicked", ->
# $('a.delete-section-button').click()
# $('a.action-primary').click()
# expect(@requests[0].url).toEqual('/delete_item')
it "should not delete model when cancel is clicked", ->
$('a.delete-section-button').click()
$('a.action-secondary').click()
expect(@requests.length).toEqual(0)
# Fails sporadically in Jenkins.
# it "should show a confirmation on delete", ->
# $('a.delete-section-button').click()
# $('a.action-primary').click()
# expect(@notificationSpy).toHaveBeenCalled()
describe "findDestination", ->
it "correctly finds the drop target of a drag", ->
$ele = $('#unit-1')
$ele.offset(
top: $ele.offset().top + 10, left: $ele.offset().left
)
destination = OverviewDragger.findDestination($ele, 1)
expect(destination.ele).toBe($('#unit-2'))
expect(destination.attachMethod).toBe('before')
it "can drag and drop across section boundaries, with special handling for first element", ->
$ele = $('#unit-1')
$ele.offset(
top: $('#unit-4').offset().top + 8
left: $ele.offset().left
)
destination = OverviewDragger.findDestination($ele, 1)
expect(destination.ele).toBe($('#unit-4'))
# Dragging down into first element, we have a fudge factor makes it easier to drag at beginning.
expect(destination.attachMethod).toBe('before')
# Now past the "fudge factor".
$ele.offset(
top: $('#unit-4').offset().top + 12
left: $ele.offset().left
)
destination = OverviewDragger.findDestination($ele, 1)
expect(destination.ele).toBe($('#unit-4'))
expect(destination.attachMethod).toBe('after')
it "can drag and drop across section boundaries, with special handling for last element", ->
$ele = $('#unit-4')
$ele.offset(
top: $('#unit-3').offset().bottom + 4
left: $ele.offset().left
)
destination = OverviewDragger.findDestination($ele, -1)
expect(destination.ele).toBe($('#unit-3'))
# Dragging down up into last element, we have a fudge factor makes it easier to drag at beginning.
expect(destination.attachMethod).toBe('after')
# Now past the "fudge factor".
$ele.offset(
top: $('#unit-3').offset().top + 4
left: $ele.offset().left
)
destination = OverviewDragger.findDestination($ele, -1)
expect(destination.ele).toBe($('#unit-3'))
expect(destination.attachMethod).toBe('before')
it "can drag into an empty list", ->
$ele = $('#unit-1')
$ele.offset(
top: $('#subsection-3').offset().top + 10
left: $ele.offset().left
)
destination = OverviewDragger.findDestination($ele, 1)
expect(destination.ele).toBe($('#subsection-list-3'))
expect(destination.attachMethod).toBe('prepend')
it "reports a null destination on a failed drag", ->
$ele = $('#unit-1')
$ele.offset(
top: $ele.offset().top + 200, left: $ele.offset().left
)
destination = OverviewDragger.findDestination($ele, 1)
expect(destination).toEqual(
ele: null
attachMethod: ""
)
it "can drag into a collapsed list", ->
$('#subsection-2').addClass('collapsed')
$ele = $('#unit-2')
$ele.offset(
top: $('#subsection-2').offset().top + 3
left: $ele.offset().left
)
destination = OverviewDragger.findDestination($ele, 1)
expect(destination.ele).toBe($('#subsection-list-2'))
expect(destination.parentList).toBe($('#subsection-2'))
expect(destination.attachMethod).toBe('prepend')
describe "onDragStart", ->
it "sets the dragState to its default values", ->
expect(OverviewDragger.dragState).toEqual({})
# Call with some dummy data
OverviewDragger.onDragStart(
{element: $('#unit-1')},
null,
null
)
expect(OverviewDragger.dragState).toEqual(
dropDestination: null,
attachMethod: '',
parentList: null,
lastY: 0,
dragDirection: 0
)
it "collapses expanded elements", ->
expect($('#subsection-1')).not.toHaveClass('collapsed')
OverviewDragger.onDragStart(
{element: $('#subsection-1')},
null,
null
)
expect($('#subsection-1')).toHaveClass('collapsed')
expect($('#subsection-1')).toHaveClass('expand-on-drop')
describe "onDragMove", ->
beforeEach ->
@scrollSpy = spyOn(window, 'scrollBy').andCallThrough()
it "adds the correct CSS class to the drop destination", ->
$ele = $('#unit-1')
dragY = $ele.offset().top + 10
dragX = $ele.offset().left
$ele.offset(
top: dragY, left: dragX
)
OverviewDragger.onDragMove(
{element: $ele, dragPoint:
{y: dragY}}, '', {clientX: dragX}
)
expect($('#unit-2')).toHaveClass('drop-target drop-target-before')
expect($ele).toHaveClass('valid-drop')
it "does not add CSS class to the drop destination if out of bounds", ->
$ele = $('#unit-1')
dragY = $ele.offset().top + 10
$ele.offset(
top: dragY, left: $ele.offset().left
)
OverviewDragger.onDragMove(
{element: $ele, dragPoint:
{y: dragY}}, '', {clientX: $ele.offset().left - 3}
)
expect($('#unit-2')).not.toHaveClass('drop-target drop-target-before')
expect($ele).not.toHaveClass('valid-drop')
it "scrolls up if necessary", ->
OverviewDragger.onDragMove(
{element: $('#unit-1')}, '', {clientY: 2}
)
expect(@scrollSpy).toHaveBeenCalledWith(0, -10)
it "scrolls down if necessary", ->
OverviewDragger.onDragMove(
{element: $('#unit-1')}, '', {clientY: (window.innerHeight - 5)}
)
expect(@scrollSpy).toHaveBeenCalledWith(0, 10)
describe "onDragEnd", ->
beforeEach ->
@reorderSpy = spyOn(OverviewDragger, 'handleReorder')
afterEach ->
@reorderSpy.reset()
it "calls handleReorder on a successful drag", ->
OverviewDragger.dragState.dropDestination = $('#unit-2')
OverviewDragger.dragState.attachMethod = "before"
OverviewDragger.dragState.parentList = $('#subsection-1')
$('#unit-1').offset(
top: $('#unit-1').offset().top + 10
left: $('#unit-1').offset().left
)
OverviewDragger.onDragEnd(
{element: $('#unit-1')},
null,
{clientX: $('#unit-1').offset().left}
)
expect(@reorderSpy).toHaveBeenCalled()
it "clears out the drag state", ->
OverviewDragger.onDragEnd(
{element: $('#unit-1')},
null,
null
)
expect(OverviewDragger.dragState).toEqual({})
it "sets the element to the correct position", ->
OverviewDragger.onDragEnd(
{element: $('#unit-1')},
null,
null
)
# Chrome sets the CSS to 'auto', but Firefox uses '0px'.
expect(['0px', 'auto']).toContain($('#unit-1').css('top'))
expect(['0px', 'auto']).toContain($('#unit-1').css('left'))
it "expands an element if it was collapsed on drag start", ->
$('#subsection-1').addClass('collapsed')
$('#subsection-1').addClass('expand-on-drop')
OverviewDragger.onDragEnd(
{element: $('#subsection-1')},
null,
null
)
expect($('#subsection-1')).not.toHaveClass('collapsed')
expect($('#subsection-1')).not.toHaveClass('expand-on-drop')
it "expands a collapsed element when something is dropped in it", ->
$('#subsection-2').addClass('collapsed')
OverviewDragger.dragState.dropDestination = $('#list-2')
OverviewDragger.dragState.attachMethod = "prepend"
OverviewDragger.dragState.parentList = $('#subsection-2')
OverviewDragger.onDragEnd(
{element: $('#unit-1')},
null,
{clientX: $('#unit-1').offset().left}
)
expect($('#subsection-2')).not.toHaveClass('collapsed')
describe "AJAX", ->
beforeEach ->
@requests = requests = []
@xhr = sinon.useFakeXMLHttpRequest()
@xhr.onCreate = (xhr) -> requests.push(xhr)
@savingSpies = spyOnConstructor(Notification, "Mini",
["show", "hide"])
@savingSpies.show.andReturn(@savingSpies)
@clock = sinon.useFakeTimers()
afterEach ->
@xhr.restore()
@clock.restore()
it "should send an update on reorder", ->
OverviewDragger.dragState.dropDestination = $('#unit-4')
OverviewDragger.dragState.attachMethod = "after"
OverviewDragger.dragState.parentList = $('#subsection-2')
# Drag Unit 1 from Subsection 1 to the end of Subsection 2.
$('#unit-1').offset(
top: $('#unit-4').offset().top + 10
left: $('#unit-4').offset().left
)
OverviewDragger.onDragEnd(
{element: $('#unit-1')},
null,
{clientX: $('#unit-1').offset().left}
)
expect(@requests.length).toEqual(2)
expect(@savingSpies.constructor).toHaveBeenCalled()
expect(@savingSpies.show).toHaveBeenCalled()
expect(@savingSpies.hide).not.toHaveBeenCalled()
savingOptions = @savingSpies.constructor.mostRecentCall.args[0]
expect(savingOptions.title).toMatch(/Saving/)
expect($('#unit-1')).toHaveClass('was-dropped')
# We expect 2 requests to be sent-- the first for removing Unit 1 from Subsection 1,
# and the second for adding Unit 1 to the end of Subsection 2.
expect(@requests[0].requestBody).toEqual('{"id":"subsection-1-id","children":["second-unit-id","third-unit-id"]}')
@requests[0].respond(200)
expect(@savingSpies.hide).not.toHaveBeenCalled()
expect(@requests[1].requestBody).toEqual('{"id":"subsection-2-id","children":["fourth-unit-id","first-unit-id"]}')
@requests[1].respond(200)
expect(@savingSpies.hide).toHaveBeenCalled()
# Class is removed in a timeout.
@clock.tick(1001)
expect($('#unit-1')).not.toHaveClass('was-dropped')

View File

@@ -1,5 +1,6 @@
define ["jquery", "underscore.string", "backbone", "js/views/feedback_notification", "jquery.cookie"],
($, str, Backbone, NotificationView) ->
define ["domReady", "jquery", "underscore.string", "backbone", "gettext",
"js/views/feedback_notification", "jquery.cookie"],
(domReady, $, str, Backbone, gettext, NotificationView) ->
AjaxPrefix.addAjaxPrefix jQuery, ->
$("meta[name='path_prefix']").attr('content')
@@ -36,5 +37,5 @@ define ["jquery", "underscore.string", "backbone", "js/views/feedback_notificati
if onTouchBasedDevice()
$('body').addClass 'touch-based-device'
$(main)
domReady(main)
return main

View File

@@ -92,7 +92,6 @@ define ["backbone", "jquery", "underscore", "gettext", "xmodule",
title: gettext('Saving&hellip;')
saving.show()
@model.save(data).done( =>
# # showToastMessage("Your changes have been saved.", null, 3)
@module = null
@render()
@$el.removeClass('editing')

View File

@@ -1,6 +1,6 @@
require(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js/views/feedback_prompt",
"jquery.ui", "jquery.timepicker", "jquery.leanModal", "jquery.form", "jquery.smoothScroll"],
function($, _, gettext, NotificationView, PromptView) {
require(["domReady", "jquery", "underscore", "gettext", "js/views/feedback_notification", "js/views/feedback_prompt",
"js/utils/cancel_on_escape", "jquery.ui", "jquery.timepicker", "jquery.leanModal", "jquery.form", "jquery.smoothScroll"],
function(domReady, $, _, gettext, NotificationView, PromptView, CancelOnEscape) {
var $body;
var $modal;
@@ -12,7 +12,7 @@ var $newComponentTypePicker;
var $newComponentTemplatePickers;
var $newComponentButton;
$(document).ready(function() {
domReady(function() {
$body = $('body');
$modal = $('.history-modal');
$modalCover = $('.modal-cover');
@@ -94,9 +94,6 @@ $(document).ready(function() {
// tender feedback window scrolling
$('a.show-tender').bind('click', smoothScrollTop);
// toggling footer additional support
$('.cta-show-sock').bind('click', toggleSock);
// toggling overview section details
$(function() {
if ($('.courseware-section').length > 0) {
@@ -126,15 +123,6 @@ $(document).ready(function() {
$('.sync-date').bind('click', syncReleaseDate);
// import form setup
$('.view-import .file-input').bind('change', showImportSubmit);
$('.view-import .choose-file-button, .view-import .choose-file-button-inline').bind('click', function(e) {
e.preventDefault();
$('.view-import .file-input').click();
});
$('.new-course-button').bind('click', addNewCourse);
// section date setting
$('.set-publish-date').bind('click', setSectionScheduleDate);
$('.edit-section-start-cancel').bind('click', cancelSetSectionScheduleDate);
@@ -221,20 +209,6 @@ function editSectionPublishDate(e) {
$modalCover.show();
}
function showImportSubmit(e) {
var filepath = $(this).val();
if (filepath.substr(filepath.length - 6, 6) == 'tar.gz') {
$('.error-block').hide();
$('.file-name').html($(this).val().replace('C:\\fakepath\\', ''));
$('.file-name-block').show();
$('.view-import .choose-file-button').hide();
$('.submit-button').show();
$('.progress').show();
} else {
$('.error-block').html(gettext('File format not supported. Please upload a file with a <code>tar.gz</code> extension.')).show();
}
}
function syncReleaseDate(e) {
e.preventDefault();
$(this).closest('.notice').hide();
@@ -319,9 +293,6 @@ function saveSubsection() {
success: function() {
$spinner.delay(500).fadeOut(150);
$changedInput = null;
},
error: function() {
showToastMessage(gettext('There has been an error while saving your changes.'));
}
});
}
@@ -423,31 +394,6 @@ function hideModal(e) {
}
}
function toggleSock(e) {
e.preventDefault();
var $btnLabel = $(this).find('.copy');
var $sock = $('.wrapper-sock');
var $sockContent = $sock.find('.wrapper-inner');
$sock.toggleClass('is-shown');
$sockContent.toggle('fast');
$.smoothScroll({
offset: -200,
easing: 'swing',
speed: 1000,
scrollElement: null,
scrollTarget: $sock
});
if ($sock.hasClass('is-shown')) {
$btnLabel.text(gettext('Hide Studio Help'));
} else {
$btnLabel.text(gettext('Looking for Help with Studio?'));
}
}
function toggleSubmodules(e) {
e.preventDefault();
$(this).toggleClass('expand').toggleClass('collapse');
@@ -459,16 +405,6 @@ function setVisibility(e) {
$(e.target).closest('.option').addClass('checked');
}
function editComponent(e) {
e.preventDefault();
$(this).closest('.xmodule_edit').addClass('editing').find('.component-editor').slideDown(150);
}
function closeComponentEditor(e) {
e.preventDefault();
$(this).closest('.xmodule_edit').removeClass('editing').find('.component-editor').slideUp(150);
}
function showDateSetter(e) {
e.preventDefault();
var $block = $(this).closest('.due-date-input');
@@ -497,41 +433,7 @@ function hideAlert(e) {
$(this).closest('.wrapper-alert').removeClass('is-shown');
}
function showToastMessage(message, $button, lifespan) {
var $toast = $('<div class="toast-notification"></div>');
var $closeBtn = $('<a href="#" class="close-button">×</a>');
$toast.append($closeBtn);
var $content = $('<div class="notification-content"></div>');
$content.html(message);
$toast.append($content);
if ($button) {
$button.addClass('action-button');
$button.bind('click', hideToastMessage);
$content.append($button);
}
$closeBtn.bind('click', hideToastMessage);
if ($('.toast-notification')[0]) {
var targetY = $('.toast-notification').offset().top + $('.toast-notification').outerHeight();
$toast.css('top', (targetY + 10) + 'px');
}
$body.prepend($toast);
$toast.fadeIn(200);
if (lifespan) {
$toast.timer = setTimeout(function() {
$toast.fadeOut(300);
}, lifespan * 1000);
}
}
function hideToastMessage(e) {
e.preventDefault();
$(this).closest('.toast-notification').remove();
}
function addNewSection(e, isTemplate) {
function addNewSection(e) {
e.preventDefault();
$(e.target).addClass('disabled');
@@ -542,19 +444,9 @@ function addNewSection(e, isTemplate) {
$newSection.find('.new-section-name').focus().select();
$newSection.find('.section-name-form').bind('submit', saveNewSection);
$cancelButton.bind('click', cancelNewSection);
$body.bind('keyup', {
$cancelButton: $cancelButton
}, checkForCancel);
CancelOnEscape($cancelButton);
}
function checkForCancel(e) {
if (e.which == 27) {
$body.unbind('keyup', checkForCancel);
e.data.$cancelButton.click();
}
}
function saveNewSection(e) {
e.preventDefault();
@@ -571,7 +463,7 @@ function saveNewSection(e) {
$.post('/create_item', {
'parent_location': parent,
'category': category,
'display_name': display_name,
'display_name': display_name
},
function(data) {
@@ -585,162 +477,6 @@ function cancelNewSection(e) {
$(this).parents('section.new-section').remove();
}
function addNewCourse(e) {
e.preventDefault();
$('.new-course-button').addClass('is-disabled');
$('.new-course-save').addClass('is-disabled');
var $newCourse = $('.wrapper-create-course').addClass('is-shown');
var $cancelButton = $newCourse.find('.new-course-cancel');
var $courseName = $('.new-course-name');
$courseName.focus().select();
$('.new-course-save').on('click', saveNewCourse);
$cancelButton.bind('click', cancelNewCourse);
$body.bind('keyup', {
$cancelButton: $cancelButton
}, checkForCancel);
// Check that a course (org, number, run) doesn't use any special characters
var validateCourseItemEncoding = function(item) {
var required = validateRequiredField(item);
if(required) {
return required;
}
if(item !== encodeURIComponent(item)) {
return gettext('Please do not use any spaces or special characters in this field.');
}
return '';
};
// Ensure that org/course_num/run < 65 chars.
var validateTotalCourseItemsLength = function() {
var totalLength = _.reduce(
['.new-course-org', '.new-course-number', '.new-course-run'],
function(sum, ele) {
return sum + $(ele).val().length;
}, 0
);
if(totalLength > 65) {
$('.wrap-error').addClass('is-shown');
$('#course_creation_error').html('<p>' + gettext('The combined length of the organization, course number, and course run fields cannot be more than 65 characters.') + '</p>');
$('.new-course-save').addClass('is-disabled');
}
else {
$('.wrap-error').removeClass('is-shown');
}
};
// Handle validation asynchronously
_.each(
['.new-course-org', '.new-course-number', '.new-course-run'],
function(ele) {
var $ele = $(ele);
$ele.on('keyup', function(event) {
// Don't bother showing "required field" error when
// the user tabs into a new field; this is distracting
// and unnecessary
if(event.keyCode === 9) {
return;
}
var error = validateCourseItemEncoding($ele.val());
setNewCourseFieldInErr($ele.parent('li'), error);
validateTotalCourseItemsLength();
});
}
);
var $name = $('.new-course-name');
$name.on('keyup', function() {
var error = validateRequiredField($name.val());
setNewCourseFieldInErr($name.parent('li'), error);
validateTotalCourseItemsLength();
});
}
function validateRequiredField(msg) {
return msg.length === 0 ? gettext('Required field.') : '';
}
function setNewCourseFieldInErr(el, msg) {
if(msg) {
el.addClass('error');
el.children('span.tip-error').addClass('is-showing').removeClass('is-hiding').text(msg);
$('.new-course-save').addClass('is-disabled');
}
else {
el.removeClass('error');
el.children('span.tip-error').addClass('is-hiding').removeClass('is-showing');
// One "error" div is always present, but hidden or shown
if($('.error').length === 1) {
$('.new-course-save').removeClass('is-disabled');
}
}
};
function saveNewCourse(e) {
e.preventDefault();
// One final check for empty values
var errors = _.reduce(
['.new-course-name', '.new-course-org', '.new-course-number', '.new-course-run'],
function(acc, ele) {
var $ele = $(ele);
var error = validateRequiredField($ele.val());
setNewCourseFieldInErr($ele.parent('li'), error);
return error ? true : acc;
},
false
);
if(errors) {
return;
}
var $newCourseForm = $(this).closest('#create-course-form');
var display_name = $newCourseForm.find('.new-course-name').val();
var org = $newCourseForm.find('.new-course-org').val();
var number = $newCourseForm.find('.new-course-number').val();
var run = $newCourseForm.find('.new-course-run').val();
analytics.track('Created a Course', {
'org': org,
'number': number,
'display_name': display_name,
'run': run
});
$.post('/create_new_course', {
'org': org,
'number': number,
'display_name': display_name,
'run': run
},
function(data) {
if (data.id !== undefined) {
window.location = '/' + data.id.replace(/.*:\/\//, '');
} else if (data.ErrMsg !== undefined) {
$('.wrap-error').addClass('is-shown');
$('#course_creation_error').html('<p>' + data.ErrMsg + '</p>');
$('.new-course-save').addClass('is-disabled');
}
}
);
}
function cancelNewCourse(e) {
e.preventDefault();
$('.new-course-button').removeClass('is-disabled');
$('.wrapper-create-course').removeClass('is-shown');
// Clear out existing fields and errors
_.each(
['.new-course-name', '.new-course-org', '.new-course-number', '.new-course-run'],
function(field) {
$(field).val('');
}
);
$('#course_creation_error').html('');
$('.wrap-error').removeClass('is-shown');
$('.new-course-save').off('click');
}
function addNewSubsection(e) {
e.preventDefault();
var $section = $(this).closest('.courseware-section');
@@ -758,9 +494,7 @@ function addNewSubsection(e) {
$newSubsection.find('.new-subsection-form').bind('submit', saveNewSubsection);
$cancelButton.bind('click', cancelNewSubsection);
$body.bind('keyup', {
$cancelButton: $cancelButton
}, checkForCancel);
CancelOnEscape($cancelButton);
}
function saveNewSubsection(e) {
@@ -863,5 +597,8 @@ function saveSetSectionScheduleDate(e) {
saving.hide();
});
}
// Add to window object for unit test (overview_spec).
window.saveSetSectionScheduleDate = saveSetSectionScheduleDate;
window.deleteSection = deleteSection;
}); // end require()

View File

@@ -1,52 +0,0 @@
/*
* Create a HesitateEvent and assign it as the event to execute:
* $(el).on('mouseEnter', CMS.HesitateEvent( expand, 'mouseLeave').trigger);
* It calls the executeOnTimeOut function with the event.currentTarget after the configurable timeout IFF the cancelSelector event
* did not occur on the event.currentTarget.
*
* More specifically, when trigger is called (triggered by the event you bound it to), it starts a timer
* which the cancelSelector event will cancel or if the timer finished, it executes the executeOnTimeOut function
* passing it the original event (whose currentTarget s/b the specific ele). It never accumulates events; however, it doesn't hurt for your
* code to minimize invocations of trigger by binding to mouseEnter v mouseOver and such.
*
* NOTE: if something outside of this wants to cancel the event, invoke cachedhesitation.untrigger(null | anything);
*/
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);
};
HesitateEvent.DURATION = 800;
HesitateEvent.prototype.trigger = function(event) {
if (event.data.timeoutEventId == null) {
event.data.timeoutEventId = window.setTimeout(
function() { event.data.fireEvent(event); },
HesitateEvent.DURATION);
event.data.originalEvent = event;
$(event.data.originalEvent.delegateTarget).on(event.data.cancelSelector, event.data, event.data.untrigger);
}
};
HesitateEvent.prototype.fireEvent = function(event) {
event.data.timeoutEventId = null;
$(event.data.originalEvent.delegateTarget).off(event.data.cancelSelector, event.data.untrigger);
if (event.data.onlyOnce) $(event.data.originalEvent.delegateTarget).off(event.data.originalEvent.type, event.data.trigger);
event.data.executeOnTimeOut(event.data.originalEvent);
};
HesitateEvent.prototype.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;
});

161
cms/static/js/index.js Normal file
View File

@@ -0,0 +1,161 @@
require(["domReady", "jquery", "underscore", "js/utils/cancel_on_escape"],
function (domReady, $, _, CancelOnEscape) {
var saveNewCourse = function (e) {
e.preventDefault();
// One final check for empty values
var errors = _.reduce(
['.new-course-name', '.new-course-org', '.new-course-number', '.new-course-run'],
function (acc, ele) {
var $ele = $(ele);
var error = validateRequiredField($ele.val());
setNewCourseFieldInErr($ele.parent('li'), error);
return error ? true : acc;
},
false
);
if (errors) {
return;
}
var $newCourseForm = $(this).closest('#create-course-form');
var display_name = $newCourseForm.find('.new-course-name').val();
var org = $newCourseForm.find('.new-course-org').val();
var number = $newCourseForm.find('.new-course-number').val();
var run = $newCourseForm.find('.new-course-run').val();
analytics.track('Created a Course', {
'org': org,
'number': number,
'display_name': display_name,
'run': run
});
$.post('/create_new_course', {
'org': org,
'number': number,
'display_name': display_name,
'run': run
},
function (data) {
if (data.id !== undefined) {
window.location = '/' + data.id.replace(/.*:\/\//, '');
} else if (data.ErrMsg !== undefined) {
$('.wrap-error').addClass('is-shown');
$('#course_creation_error').html('<p>' + data.ErrMsg + '</p>');
$('.new-course-save').addClass('is-disabled');
}
}
);
};
var cancelNewCourse = function (e) {
e.preventDefault();
$('.new-course-button').removeClass('is-disabled');
$('.wrapper-create-course').removeClass('is-shown');
// Clear out existing fields and errors
_.each(
['.new-course-name', '.new-course-org', '.new-course-number', '.new-course-run'],
function (field) {
$(field).val('');
}
);
$('#course_creation_error').html('');
$('.wrap-error').removeClass('is-shown');
$('.new-course-save').off('click');
};
var addNewCourse = function (e) {
e.preventDefault();
$('.new-course-button').addClass('is-disabled');
$('.new-course-save').addClass('is-disabled');
var $newCourse = $('.wrapper-create-course').addClass('is-shown');
var $cancelButton = $newCourse.find('.new-course-cancel');
var $courseName = $('.new-course-name');
$courseName.focus().select();
$('.new-course-save').on('click', saveNewCourse);
$cancelButton.bind('click', cancelNewCourse);
CancelOnEscape($cancelButton);
// Check that a course (org, number, run) doesn't use any special characters
var validateCourseItemEncoding = function (item) {
var required = validateRequiredField(item);
if (required) {
return required;
}
if (item !== encodeURIComponent(item)) {
return gettext('Please do not use any spaces or special characters in this field.');
}
return '';
};
// Ensure that org/course_num/run < 65 chars.
var validateTotalCourseItemsLength = function () {
var totalLength = _.reduce(
['.new-course-org', '.new-course-number', '.new-course-run'],
function (sum, ele) {
return sum + $(ele).val().length;
}, 0
);
if (totalLength > 65) {
$('.wrap-error').addClass('is-shown');
$('#course_creation_error').html('<p>' + gettext('The combined length of the organization, course number, and course run fields cannot be more than 65 characters.') + '</p>');
$('.new-course-save').addClass('is-disabled');
}
else {
$('.wrap-error').removeClass('is-shown');
}
};
// Handle validation asynchronously
_.each(
['.new-course-org', '.new-course-number', '.new-course-run'],
function (ele) {
var $ele = $(ele);
$ele.on('keyup', function (event) {
// Don't bother showing "required field" error when
// the user tabs into a new field; this is distracting
// and unnecessary
if (event.keyCode === 9) {
return;
}
var error = validateCourseItemEncoding($ele.val());
setNewCourseFieldInErr($ele.parent('li'), error);
validateTotalCourseItemsLength();
});
}
);
var $name = $('.new-course-name');
$name.on('keyup', function () {
var error = validateRequiredField($name.val());
setNewCourseFieldInErr($name.parent('li'), error);
validateTotalCourseItemsLength();
});
};
var validateRequiredField = function (msg) {
return msg.length === 0 ? gettext('Required field.') : '';
};
var setNewCourseFieldInErr = function (el, msg) {
if(msg) {
el.addClass('error');
el.children('span.tip-error').addClass('is-showing').removeClass('is-hiding').text(msg);
$('.new-course-save').addClass('is-disabled');
}
else {
el.removeClass('error');
el.children('span.tip-error').addClass('is-hiding').removeClass('is-showing');
// One "error" div is always present, but hidden or shown
if($('.error').length === 1) {
$('.new-course-save').removeClass('is-disabled');
}
}
};
domReady(function () {
$('.new-course-button').bind('click', addNewCourse);
});
});

View File

@@ -10,13 +10,13 @@ var CourseGrader = Backbone.Model.extend({
},
parse : function(attrs) {
if (attrs['weight']) {
if (!_.isNumber(attrs.weight)) attrs.weight = parseInt(attrs.weight, 10);
attrs.weight = parseInt(attrs.weight, 10);
}
if (attrs['min_count']) {
if (!_.isNumber(attrs.min_count)) attrs.min_count = parseInt(attrs.min_count, 10);
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);
attrs.drop_count = parseInt(attrs.drop_count, 10);
}
return attrs;
},

View File

@@ -17,10 +17,10 @@ var CourseGradingPolicy = Backbone.Model.extend({
// interesting race condition: if {parse:true} when newing, then parse called before .attributes created
if (this.attributes && this.has('graders')) {
graderCollection = this.get('graders');
graderCollection.reset(attributes.graders);
graderCollection.reset(attributes.graders, {parse:true});
}
else {
graderCollection = new CourseGraderCollection(attributes.graders);
graderCollection = new CourseGraderCollection(attributes.graders, {parse:true});
graderCollection.course_location = attributes['course_location'] || this.get('course_location');
}
attributes.graders = graderCollection;

32
cms/static/js/sock.js Normal file
View File

@@ -0,0 +1,32 @@
require(["domReady", "jquery", "jquery.smoothScroll"],
function (domReady, $) {
var toggleSock = function (e) {
e.preventDefault();
var $btnLabel = $(this).find('.copy');
var $sock = $('.wrapper-sock');
var $sockContent = $sock.find('.wrapper-inner');
$sock.toggleClass('is-shown');
$sockContent.toggle('fast');
$.smoothScroll({
offset: -200,
easing: 'swing',
speed: 1000,
scrollElement: null,
scrollTarget: $sock
});
if ($sock.hasClass('is-shown')) {
$btnLabel.text(gettext('Hide Studio Help'));
} else {
$btnLabel.text(gettext('Looking for Help with Studio?'));
}
};
domReady(function () {
// toggling footer additional support
$('.cta-show-sock').bind('click', toggleSock);
});
});

View File

@@ -0,0 +1,17 @@
define(["jquery"], function($) {
var $body = $('body');
var checkForCancel = function (e) {
if (e.which == 27) {
$body.unbind('keyup', checkForCancel);
e.data.$cancelButton.click();
}
};
var cancelOnEscape = function (cancelButton) {
$body.bind('keyup', {
$cancelButton: cancelButton
}, checkForCancel);
};
return cancelOnEscape;
});

View File

@@ -2,7 +2,6 @@ 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: {
@@ -18,6 +17,8 @@ define(["backbone", "underscore", "codemirror", "js/models/course_update",
this.render();
// when the client refetches the updates as a whole, re-render them
this.listenTo(this.collection, 'reset', this.render);
this.$modalCover = $(".modal-cover");
},
render: function () {
@@ -63,8 +64,8 @@ define(["backbone", "underscore", "codemirror", "js/models/course_update",
$newForm.addClass('editing');
this.$currentPost = $newForm.closest('li');
$modalCover.show();
$modalCover.bind('click', function() {
this.$modalCover.show();
this.$modalCover.bind('click', function() {
self.closeEditor(true);
});
@@ -120,9 +121,9 @@ define(["backbone", "underscore", "codemirror", "js/models/course_update",
this.$codeMirror = CourseInfoHelper.editWithCodeMirror(
targetModel, 'content', self.options['base_asset_url'], $textArea.get(0));
$modalCover.show();
$modalCover.bind('click', function() {
self.closeEditor(self);
this.$modalCover.show();
this.$modalCover.bind('click', function() {
self.closeEditor(false);
});
},
@@ -197,8 +198,8 @@ define(["backbone", "underscore", "codemirror", "js/models/course_update",
this.$currentPost.find('.CodeMirror').remove();
}
$modalCover.unbind('click');
$modalCover.hide();
this.$modalCover.unbind('click');
this.$modalCover.hide();
this.$codeMirror = null;
},

View File

@@ -2,149 +2,172 @@
* Course import-related js.
*/
define(
["jquery", "underscore", "gettext"],
function($, _, gettext) {
["domReady", "jquery", "underscore", "gettext"],
function(domReady, $, _, gettext) {
"use strict";
"use strict";
/********** Private functions ************************************************/
/********** Private functions ************************************************/
/**
* Toggle the spin on the progress cog.
* @param {boolean} isSpinning Turns cog spin on if true, off otherwise.
*/
var updateCog = function (elem, isSpinning) {
var cogI = elem.find('i.icon-cog');
if (isSpinning) { cogI.addClass("icon-spin");}
else { cogI.removeClass("icon-spin");}
};
/**
* Toggle the spin on the progress cog.
* @param {boolean} isSpinning Turns cog spin on if true, off otherwise.
*/
var updateCog = function (elem, isSpinning) {
var cogI = elem.find('i.icon-cog');
if (isSpinning) { cogI.addClass("icon-spin");}
else { cogI.removeClass("icon-spin");}
};
/**
* Manipulate the DOM to reflect current status of upload.
* @param {int} stageNo Current stage.
*/
var updateStage = function (stageNo){
var all = $('ol.status-progress').children();
var prevList = all.slice(0, stageNo);
_.map(prevList, function (elem){
$(elem).
removeClass("is-not-started").
removeClass("is-started").
addClass("is-complete");
updateCog($(elem), false);
/**
* Manipulate the DOM to reflect current status of upload.
* @param {int} stageNo Current stage.
*/
var updateStage = function (stageNo){
var all = $('ol.status-progress').children();
var prevList = all.slice(0, stageNo);
_.map(prevList, function (elem){
$(elem).
removeClass("is-not-started").
removeClass("is-started").
addClass("is-complete");
updateCog($(elem), false);
});
var curList = all.eq(stageNo);
curList.removeClass("is-not-started").addClass("is-started");
updateCog(curList, true);
};
/**
* Check for import status updates every `timeout` milliseconds, and update
* the page accordingly.
* @param {string} url Url to call for status updates.
* @param {int} timeout Number of milliseconds to wait in between ajax calls
* for new updates.
* @param {int} stage Starting stage.
*/
var getStatus = function (url, timeout, stage) {
var currentStage = stage || 0;
if (CourseImport.stopGetStatus) { return ;}
updateStage(currentStage);
if (currentStage == 3 ) { return ;}
var time = timeout || 1000;
$.getJSON(url,
function (data) {
setTimeout(function () {
getStatus(url, time, data.ImportStatus);
}, time);
}
);
};
/********** Public functions *************************************************/
var CourseImport = {
/**
* Whether to stop sending AJAX requests for updates on the import
* progress.
*/
stopGetStatus: false,
/**
* Update DOM to set all stages as not-started (for retrying an upload that
* failed).
*/
clearImportDisplay: function () {
var all = $('ol.status-progress').children();
_.map(all, function (elem){
$(elem).removeClass("is-complete").
removeClass("is-started").
removeClass("has-error").
addClass("is-not-started");
$(elem).find('p.error').remove(); // remove error messages
$(elem).find('p.copy').show();
updateCog($(elem), false);
});
this.stopGetStatus = false;
},
/**
* Update DOM to set all stages as complete, and stop asking for status
* updates.
*/
displayFinishedImport: function () {
this.stopGetStatus = true;
var all = $('ol.status-progress').children();
_.map(all, function (elem){
$(elem).
removeClass("is-not-started").
removeClass("is-started").
addClass("is-complete");
updateCog($(elem), false);
});
},
/**
* Entry point for server feedback. Makes status list visible and starts
* sending requests to the server for status updates.
* @param {string} url The url to send Ajax GET requests for updates.
*/
startServerFeedback: function (url){
this.stopGetStatus = false;
$('div.wrapper-status').removeClass('is-hidden');
$('.status-info').show();
getStatus(url, 500, 0);
},
/**
* Give error message at the list element that corresponds to the stage
* where the error occurred.
* @param {int} stageNo Stage of import process at which error occured.
* @param {string} msg Error message to display.
*/
stageError: function (stageNo, msg) {
var all = $('ol.status-progress').children();
// Make all stages up to, and including, the error stage 'complete'.
var prevList = all.slice(0, stageNo + 1);
_.map(prevList, function (elem){
$(elem).
removeClass("is-not-started").
removeClass("is-started").
addClass("is-complete");
updateCog($(elem), false);
});
var message = msg || gettext("There was an error with the upload");
var elem = $('ol.status-progress').children().eq(stageNo);
elem.removeClass('is-started').addClass('has-error');
elem.find('p.copy').hide().after("<p class='copy error'>" + message + "</p>");
}
};
var showImportSubmit = function (e) {
var filepath = $(this).val();
if (filepath.substr(filepath.length - 6, 6) == 'tar.gz') {
$('.error-block').hide();
$('.file-name').html($(this).val().replace('C:\\fakepath\\', ''));
$('.file-name-block').show();
$('.view-import .choose-file-button').hide();
$('.submit-button').show();
$('.progress').show();
} else {
$('.error-block').html(gettext('File format not supported. Please upload a file with a <code>tar.gz</code> extension.')).show();
}
};
domReady(function () {
// import form setup
$('.view-import .file-input').bind('change', showImportSubmit);
$('.view-import .choose-file-button, .view-import .choose-file-button-inline').bind('click', function (e) {
e.preventDefault();
$('.view-import .file-input').click();
});
});
return CourseImport;
});
var curList = all.eq(stageNo);
curList.removeClass("is-not-started").addClass("is-started");
updateCog(curList, true);
};
/**
* Check for import status updates every `timemout` milliseconds, and update
* the page accordingly.
* @param {string} url Url to call for status updates.
* @param {int} timeout Number of milliseconds to wait in between ajax calls
* for new updates.
* @param {int} stage Starting stage.
*/
var getStatus = function (url, timeout, stage) {
var currentStage = stage || 0;
if (CourseImport.stopGetStatus) { return ;}
updateStage(currentStage);
if (currentStage == 3 ) { return ;}
var time = timeout || 1000;
$.getJSON(url,
function (data) {
setTimeout(function () {
getStatus(url, time, data.ImportStatus);
}, time);
}
);
};
/********** Public functions *************************************************/
var CourseImport = {
/**
* Whether to stop sending AJAX requests for updates on the import
* progress.
*/
stopGetStatus: false,
/**
* Update DOM to set all stages as not-started (for retrying an upload that
* failed).
*/
clearImportDisplay: function () {
var all = $('ol.status-progress').children();
_.map(all, function (elem){
$(elem).removeClass("is-complete").
removeClass("is-started").
removeClass("has-error").
addClass("is-not-started");
$(elem).find('p.error').remove(); // remove error messages
$(elem).find('p.copy').show();
updateCog($(elem), false);
});
this.stopGetStatus = false;
},
/**
* Update DOM to set all stages as complete, and stop asking for status
* updates.
*/
displayFinishedImport: function () {
this.stopGetStatus = true;
var all = $('ol.status-progress').children();
_.map(all, function (elem){
$(elem).
removeClass("is-not-started").
removeClass("is-started").
addClass("is-complete");
updateCog($(elem), false);
});
},
/**
* Entry point for server feedback. Makes status list visible and starts
* sending requests to the server for status updates.
* @param {string} url The url to send Ajax GET requests for updates.
*/
startServerFeedback: function (url){
this.stopGetStatus = false;
$('div.wrapper-status').removeClass('is-hidden');
$('.status-info').show();
getStatus(url, 500, 0);
},
/**
* Give error message at the list element that corresponds to the stage
* where the error occurred.
* @param {int} stageNo Stage of import process at which error occured.
* @param {string} msg Error message to display.
*/
stageError: function (stageNo, msg) {
var all = $('ol.status-progress').children();
// Make all stages up to, and including, the error stage 'complete'.
var prevList = all.slice(0, stageNo + 1);
_.map(prevList, function (elem){
$(elem).
removeClass("is-not-started").
removeClass("is-started").
addClass("is-complete");
updateCog($(elem), false);
});
var message = msg || gettext("There was an error with the upload");
var elem = $('ol.status-progress').children().eq(stageNo);
elem.removeClass('is-started').addClass('has-error');
elem.find('p.copy').hide().after("<p class='copy error'>" + message + "</p>");
}
};
return CourseImport;
});

View File

@@ -1,248 +1,322 @@
require(["jquery", "jquery.ui", "gettext", "js/hesitate", "js/views/feedback_notification"],
function($, ui, gettext, HesitateEvent, NotificationView) {
define(["domReady", "jquery", "jquery.ui", "gettext", "js/views/feedback_notification", "draggabilly"],
function (domReady, $, ui, gettext, NotificationView, Draggabilly) {
$(document).ready(function() {
// making the unit list draggable. Note: sortable didn't work b/c it considered
// drop points which the user hovered over as destinations and proactively changed
// the dom; so, if the user subsequently dropped at an illegal spot, the reversion
// point was the last dom change.
$('.unit').draggable({
axis: 'y',
handle: '.drag-handle',
zIndex: 999,
start: initiateHesitate,
// left 2nd arg in as inert selector b/c i was uncertain whether we'd try to get the shove up/down
// to work in the future
drag: generateCheckHoverState('.collapsed', ''),
stop: removeHesitate,
revert: "invalid"
});
var overviewDragger = {
droppableClasses: 'drop-target drop-target-prepend drop-target-before drop-target-after',
validDropClass: "valid-drop",
expandOnDropClass: "expand-on-drop",
// Subsection reordering
$('.id-holder').draggable({
axis: 'y',
handle: '.section-item .drag-handle',
zIndex: 999,
start: initiateHesitate,
drag: generateCheckHoverState('.courseware-section.collapsed', ''),
stop: removeHesitate,
revert: "invalid"
});
/*
* Determine information about where to drop the currently dragged
* element. Returns the element to attach to and the method of
* attachment ('before', 'after', or 'prepend').
*/
findDestination: function (ele, yChange) {
var eleY = ele.offset().top;
var containers = $(ele.data('droppable-class'));
// Section reordering
$('.courseware-section').draggable({
axis: 'y',
handle: 'header .drag-handle',
stack: '.courseware-section',
revert: "invalid"
});
for (var i = 0; i < containers.length; i++) {
var container = $(containers[i]);
// Exclude the 'new unit' buttons, and make sure we don't
// prepend an element to itself
var siblings = container.children().filter(function () {
return $(this).data('id') !== undefined && !$(this).is(ele);
});
// If the container is collapsed, check to see if the
// element is on top of its parent list -- don't check the
// position of the container
var parentList = container.parents(ele.data('parent-location-selector')).first();
if (parentList.hasClass('collapsed')) {
if (Math.abs(eleY - parentList.offset().top) < 10) {
return {
ele: container,
attachMethod: 'prepend',
parentList: parentList
};
}
}
// Otherwise, do check the container
else {
// If the list is empty, we should prepend to it,
// unless both elements are at the same location --
// this prevents the user from being unable to expand
// a section
var containerY = container.offset().top;
if (siblings.length == 0 &&
containerY != eleY &&
Math.abs(eleY - containerY) < 50) {
return {
ele: container,
attachMethod: 'prepend'
};
}
// Otherwise the list is populated, and we should attach before/after a sibling
else {
for (var j = 0; j < siblings.length; j++) {
var $sibling = $(siblings[j]);
var siblingY = $sibling.offset().top;
var siblingHeight = $sibling.height();
var siblingYEnd = siblingY + siblingHeight;
$('.sortable-unit-list').droppable({
accept : '.unit',
greedy: true,
tolerance: "pointer",
hoverClass: "dropover",
drop: onUnitReordered
});
$('.subsection-list > ol').droppable({
// why don't we have a more useful class for subsections than id-holder?
accept : '.id-holder', // '.unit, .id-holder',
tolerance: "pointer",
hoverClass: "dropover",
drop: onSubsectionReordered,
greedy: true
});
// Section reordering
$('.courseware-overview').droppable({
accept : '.courseware-section',
tolerance: "pointer",
drop: onSectionReordered,
greedy: true
});
// stop clicks on drag bars from doing their thing w/o stopping drag
$('.drag-handle').click(function(e) {e.preventDefault(); });
});
HesitateEvent.toggleXpandHesitation = null;
function initiateHesitate(event, ui) {
HesitateEvent.toggleXpandHesitation = new HesitateEvent(expandSection, 'dragLeave', true);
$('.collapsed').on('dragEnter', HesitateEvent.toggleXpandHesitation, HesitateEvent.toggleXpandHesitation.trigger);
$('.collapsed, .unit, .id-holder').each(function() {
this.proportions = {width : this.offsetWidth, height : this.offsetHeight };
// reset b/c these were holding values from aborts
this.isover = false;
});
}
function computeIntersection(droppable, uiHelper, y) {
/*
* Test whether y falls within the bounds of the droppable on the Y axis
*/
// NOTE: this only judges y axis intersection b/c that's all we're doing right now
// don't expand the thing being carried
if (uiHelper.is(droppable)) {
return null;
}
$.extend(droppable, {offset : $(droppable).offset()});
var t = droppable.offset.top,
b = t + droppable.proportions.height;
if (t === b) {
// probably wrong values b/c invisible at the time of caching
droppable.proportions = { width : droppable.offsetWidth, height : droppable.offsetHeight };
b = t + droppable.proportions.height;
}
// equivalent to the intersects test
return (t < y && // Bottom Half
y < b ); // Top Half
}
// NOTE: selectorsToShove is not currently being used but I left this code as it did work but not well
function generateCheckHoverState(selectorsToOpen, selectorsToShove) {
return function(event, ui) {
// copied from jquery.ui.droppable.js $.ui.ddmanager.drag & other ui.intersect
var draggable = $(this).data("ui-draggable"),
centerY = (draggable.positionAbs || draggable.position.absolute).top + (draggable.helperProportions.height / 2);
$(selectorsToOpen).each(function() {
var intersects = computeIntersection(this, ui.helper, centerY),
c = !intersects && this.isover ? "isout" : (intersects && !this.isover ? "isover" : null);
if(!c) {
return;
}
this[c] = true;
this[c === "isout" ? "isover" : "isout"] = false;
$(this).trigger(c === "isover" ? "dragEnter" : "dragLeave");
});
$(selectorsToShove).each(function() {
var intersectsBottom = computeIntersection(this, ui.helper, (draggable.positionAbs || draggable.position.absolute).top);
if ($(this).hasClass('ui-dragging-pushup')) {
if (!intersectsBottom) {
console.log('not up', $(this).data('id'));
$(this).removeClass('ui-dragging-pushup');
}
}
else if (intersectsBottom) {
console.log('up', $(this).data('id'));
$(this).addClass('ui-dragging-pushup');
}
var intersectsTop = computeIntersection(this, ui.helper,
(draggable.positionAbs || draggable.position.absolute).top + draggable.helperProportions.height);
if ($(this).hasClass('ui-dragging-pushdown')) {
if (!intersectsTop) {
console.log('not down', $(this).data('id'));
$(this).removeClass('ui-dragging-pushdown');
// Facilitate dropping into the beginning or end of a list
// (coming from opposite direction) via a "fudge factor". Math.min is for Jasmine test.
var fudge = Math.min(Math.ceil(siblingHeight / 2), 20);
// Dragging up into end of list.
if (j == siblings.length - 1 && yChange < 0 && Math.abs(eleY - siblingYEnd) <= fudge) {
return {
ele: $sibling,
attachMethod: 'after'
};
}
// Dragging down into beginning of list.
else if (j == 0 && yChange > 0 && Math.abs(eleY - siblingY) <= fudge) {
return {
ele: $sibling,
attachMethod: 'before'
};
}
else if (eleY >= siblingY && eleY <= siblingYEnd) {
return {
ele: $sibling,
attachMethod: eleY - siblingY <= siblingHeight / 2 ? 'before' : 'after'
};
}
}
}
}
}
}
else if (intersectsTop) {
console.log('down', $(this).data('id'));
$(this).addClass('ui-dragging-pushdown');
}
// Failed drag
return {
ele: null,
attachMethod: ''
}
},
});
};
}
// Information about the current drag.
dragState: {},
function removeHesitate(event, ui) {
$('.collapsed').off('dragEnter', HesitateEvent.toggleXpandHesitation.trigger);
$('.ui-dragging-pushdown').removeClass('ui-dragging-pushdown');
$('.ui-dragging-pushup').removeClass('ui-dragging-pushup');
HesitateEvent.toggleXpandHesitation = null;
}
onDragStart: function (draggie, event, pointer) {
var ele = $(draggie.element);
this.dragState = {
// Which element will be dropped into/onto on success
dropDestination: null,
// How we attach to the destination: 'before', 'after', 'prepend'
attachMethod: '',
// If dragging to an empty section, the parent section
parentList: null,
// The y location of the last dragMove event (to determine direction).
lastY: 0,
// The direction the drag is moving in (negative means up, positive down).
dragDirection: 0
};
if (!ele.hasClass('collapsed')) {
ele.addClass('collapsed');
ele.find('.expand-collapse-icon').first().addClass('expand').removeClass('collapse');
// onDragStart gets called again after the collapse, so we can't just store a variable in the dragState.
ele.addClass(this.expandOnDropClass);
}
},
function expandSection(event) {
$(event.delegateTarget).removeClass('collapsed', 400);
// don't descend to icon's on children (which aren't under first child) only to this element's icon
$(event.delegateTarget).children().first().find('.expand-collapse-icon').removeClass('expand', 400).addClass('collapse');
}
onDragMove: function (draggie, event, pointer) {
// Handle scrolling of the browser.
var scrollAmount = 0;
var dragBuffer = 10;
if (window.innerHeight - dragBuffer < pointer.clientY) {
scrollAmount = dragBuffer;
}
else if (dragBuffer > pointer.clientY) {
scrollAmount = -(dragBuffer);
}
if (scrollAmount !== 0) {
window.scrollBy(0, scrollAmount);
return;
}
function onUnitReordered(event, ui) {
// a unit's been dropped on this subsection,
// figure out where it came from and where it slots in.
_handleReorder(event, ui, 'subsection-id', 'li:.leaf');
}
var yChange = draggie.dragPoint.y - this.dragState.lastY;
if (yChange !== 0) {
this.dragState.direction = yChange;
}
this.dragState.lastY = draggie.dragPoint.y;
function onSubsectionReordered(event, ui) {
// a subsection has been dropped on this section,
// figure out where it came from and where it slots in.
_handleReorder(event, ui, 'section-id', 'li:.branch');
}
var ele = $(draggie.element);
var destinationInfo = this.findDestination(ele, this.dragState.direction);
var destinationEle = destinationInfo.ele;
this.dragState.parentList = destinationInfo.parentList;
function onSectionReordered(event, ui) {
// a section moved w/in the overall (cannot change course via this, so no parentage change possible, just order)
_handleReorder(event, ui, 'course-id', '.courseware-section');
}
// Clear out the old destination
if (this.dragState.dropDestination) {
this.dragState.dropDestination.removeClass(this.droppableClasses);
}
// Mark the new destination
if (destinationEle && this.pointerInBounds(pointer, ele)) {
ele.addClass(this.validDropClass);
destinationEle.addClass('drop-target drop-target-' + destinationInfo.attachMethod);
this.dragState.attachMethod = destinationInfo.attachMethod;
this.dragState.dropDestination = destinationEle;
}
else {
ele.removeClass(this.validDropClass);
this.dragState.attachMethod = '';
this.dragState.dropDestination = null;
}
},
function _handleReorder(event, ui, parentIdField, childrenSelector) {
// figure out where it came from and where it slots in.
var subsection_id = $(event.target).data(parentIdField);
var _els = $(event.target).children(childrenSelector);
var children = _els.map(function(idx, el) { return $(el).data('id'); }).get();
// if new to this parent, figure out which parent to remove it from and do so
if (!_.contains(children, ui.draggable.data('id'))) {
var old_parent = ui.draggable.parent();
var old_children = old_parent.children(childrenSelector).map(function(idx, el) { return $(el).data('id'); }).get();
old_children = _.without(old_children, ui.draggable.data('id'));
$.ajax({
url: "/save_item",
type: "POST",
dataType: "json",
contentType: "application/json",
data:JSON.stringify({ 'id' : old_parent.data(parentIdField), 'children' : old_children})
onDragEnd: function (draggie, event, pointer) {
var ele = $(draggie.element);
var destination = this.dragState.dropDestination;
// Clear dragging state in preparation for the next event.
if (destination) {
destination.removeClass(this.droppableClasses);
}
ele.removeClass(this.validDropClass);
// If the drag succeeded, rearrange the DOM and send the result.
if (destination && this.pointerInBounds(pointer, ele)) {
// Make sure we don't drop into a collapsed element
if (this.dragState.parentList) {
this.expandElement(this.dragState.parentList);
}
var method = this.dragState.attachMethod;
destination[method](ele);
this.handleReorder(ele);
}
// If the drag failed, send it back
else {
$('.was-dragging').removeClass('was-dragging');
ele.addClass('was-dragging');
}
if (ele.hasClass(this.expandOnDropClass)) {
this.expandElement(ele);
ele.removeClass(this.expandOnDropClass);
}
// Everything in its right place
ele.css({
top: 'auto',
left: 'auto'
});
}
else {
// staying in same parent
// remove so that the replacement in the right place doesn't double it
children = _.without(children, ui.draggable.data('id'));
}
// add to this parent (figure out where)
for (var i = 0, bump = 0; i < _els.length; i++) {
if (ui.draggable.is(_els[i])) {
bump = -1; // bump indicates that the draggable was passed in the dom but not children's list b/c
// it's not in that list
}
else if (ui.offset.top < $(_els[i]).offset().top) {
// insert at i in children and _els
ui.draggable.insertBefore($(_els[i]));
// TODO figure out correct way to have it remove the style: top:n; setting (and similar line below)
ui.draggable.attr("style", "position:relative;");
children.splice(i + bump, 0, ui.draggable.data('id'));
break;
this.dragState = {};
},
pointerInBounds: function (pointer, ele) {
return pointer.clientX >= ele.offset().left && pointer.clientX < ele.offset().left + ele.width();
},
expandElement: function (ele) {
ele.removeClass('collapsed');
ele.find('.expand-collapse-icon').first().removeClass('expand').addClass('collapse');
},
/*
* Find all parent-child changes and save them.
*/
handleReorder: function (ele) {
var parentSelector = ele.data('parent-location-selector');
var childrenSelector = ele.data('child-selector');
var newParentEle = ele.parents(parentSelector).first();
var newParentID = newParentEle.data('id');
var oldParentID = ele.data('parent-id');
// If the parent has changed, update the children of the old parent.
if (oldParentID !== newParentID) {
// Find the old parent element.
var oldParentEle = $(parentSelector).filter(function () {
return $(this).data('id') === oldParentID;
});
this.saveItem(oldParentEle, childrenSelector, function () {
ele.data('parent-id', newParentID);
});
}
}
// see if it goes at end (the above loop didn't insert it)
if (!_.contains(children, ui.draggable.data('id'))) {
$(event.target).append(ui.draggable);
ui.draggable.attr("style", "position:relative;"); // STYLE hack too
children.push(ui.draggable.data('id'));
}
var saving = new NotificationView.Mini({
title: gettext('Saving&hellip;')
});
saving.show();
$.ajax({
url: "/save_item",
type: "POST",
dataType: "json",
contentType: "application/json",
data:JSON.stringify({ 'id' : subsection_id, 'children' : children}),
success: function() {
var saving = new NotificationView.Mini({
title: gettext('Saving&hellip;')
});
saving.show();
ele.addClass('was-dropped');
// Timeout interval has to match what is in the CSS.
setTimeout(function () {
ele.removeClass('was-dropped');
}, 1000);
this.saveItem(newParentEle, childrenSelector, function () {
saving.hide();
}
});
},
/*
* Actually save the update to the server. Takes the element
* representing the parent item to save, a CSS selector to find
* its children, and a success callback.
*/
saveItem: function (ele, childrenSelector, success) {
// Find all current child IDs.
var children = _.map(
ele.find(childrenSelector),
function (child) {
return $(child).data('id');
}
);
$.ajax({
url: '/save_item',
type: 'POST',
dataType: 'json',
contentType: 'application/json',
data: JSON.stringify({
id: ele.data('id'),
children: children
}),
success: success
});
},
/*
* Make `type` draggable using `handleClass`, able to be dropped
* into `droppableClass`, and with parent type
* `parentLocationSelector`.
*/
makeDraggable: function (type, handleClass, droppableClass, parentLocationSelector) {
_.each(
$(type),
function (ele) {
// Remember data necessary to reconstruct the parent-child relationships
$(ele).data('droppable-class', droppableClass);
$(ele).data('parent-location-selector', parentLocationSelector);
$(ele).data('child-selector', type);
var draggable = new Draggabilly(ele, {
handle: handleClass,
containment: '.wrapper-dnd'
});
draggable.on('dragStart', _.bind(overviewDragger.onDragStart, overviewDragger));
draggable.on('dragMove', _.bind(overviewDragger.onDragMove, overviewDragger));
draggable.on('dragEnd', _.bind(overviewDragger.onDragEnd, overviewDragger));
}
);
}
};
domReady(function() {
// Section
overviewDragger.makeDraggable(
'.courseware-section',
'.section-drag-handle',
'.courseware-overview',
'article.courseware-overview'
);
// Subsection
overviewDragger.makeDraggable(
'.id-holder',
'.subsection-drag-handle',
'.subsection-list > ol',
'.courseware-section'
);
// Unit
overviewDragger.makeDraggable(
'.unit',
'.unit-drag-handle',
'ol.sortable-unit-list',
'li.branch, article.subsection-body'
);
});
}
}); // end define()
return overviewDragger;
});

View File

@@ -1,5 +1,5 @@
define(["js/views/validation", "underscore", "jquery", "js/views/settings/grader"],
function(ValidatingView, _, $, GraderView) {
define(["js/views/validation", "underscore", "jquery", "jquery.ui", "js/views/settings/grader"],
function(ValidatingView, _, $, ui, GraderView) {
var GradingView = ValidatingView.extend({
// Model class is CMS.Models.Settings.CourseGradingPolicy

View File

@@ -28,7 +28,6 @@ prepend_path: cms/static
# Paths to library JavaScript files (optional)
lib_paths:
- coffee/spec/setup_require.js
- xmodule_js/common_static/js/vendor/require.js
- xmodule_js/common_static/coffee/src/ajax_prefix.js
- xmodule_js/common_static/js/src/utility.js
@@ -51,6 +50,10 @@ lib_paths:
- xmodule_js/common_static/js/vendor/CodeMirror/codemirror.js
- xmodule_js/src/xmodule.js
- xmodule_js/common_static/js/test/i18n.js
- xmodule_js/common_static/js/vendor/draggabilly.pkgd.js
- xmodule_js/common_static/js/vendor/date.js
- xmodule_js/common_static/js/vendor/domReady.js
- xmodule_js/common_static/js/vendor/jquery.smooth-scroll.min.js
# Paths to source JavaScript files
src_paths:

View File

@@ -28,7 +28,6 @@ prepend_path: cms/static
# Paths to library JavaScript files (optional)
lib_paths:
- coffee/spec/setup_require.js
- xmodule_js/common_static/js/vendor/require.js
- xmodule_js/common_static/coffee/src/ajax_prefix.js
- xmodule_js/common_static/js/src/utility.js

View File

@@ -528,9 +528,9 @@ p, ul, ol, dl {
.new-subsection-item,
.new-policy-item {
@include grey-button;
margin: 5px 8px;
padding: 3px 10px 4px 10px;
font-size: 10px;
@include font-size(10);
margin: ($baseline/2);
padding: 3px ($baseline/2) 4px ($baseline/2);
.new-folder-icon,
.new-policy-icon,

View File

@@ -0,0 +1 @@
../../../common/static/sass/_mixins-inherited.scss

View File

@@ -3,7 +3,7 @@
// ====================
// view - dashboard
body.dashboard {
.view-dashboard {
// elements - authorship controls
.wrapper-authorshiprights {
@@ -22,6 +22,35 @@ body.dashboard {
}
}
// ====================
.view-unit {
.unit-location .draggable-drop-indicator {
display: none; //needed to not show DnD UI (UI is shared across both views)
}
}
// ====================
// needed to override ui-window styling for dragging state (outline selectors get too specific)
.courseware-section.is-dragging {
box-shadow: 0 1px 2px 0 $shadow-d1 !important;
border: 1px solid $gray-d3 !important;
}
.courseware-section.is-dragging.valid-drop {
border-color: $blue-s1 !important;
box-shadow: 0 1px 2px 0 $blue-t2 !important;
}
// ====================
// needed for poorly scoped margin rules on all content elements
.branch .sortable-unit-list {
margin-bottom: 0;
}
// yes we have no boldness today - need to fix the resets
body strong,
@@ -29,12 +58,13 @@ body b {
font-weight: 700;
}
// known things to do (paint the fence, sand the floor, wax on/off)
// ====================
// known things to do (paint the fence, sand the floor, wax on/off):
/* known things to do (paint the fence, sand the floor, wax on/off):
// * centralize and move form styling into forms.scss - cms/static/sass/views/_textbooks.scss and cms/static/sass/views/_settings.scss
// * move dialogue styles into cms/static/sass/elements/_modal.scss
// * use the @include placeholder Bourbon mixin (http://bourbon.io/docs/#placeholder) for any placeholder styling
* centralize and move form styling into forms.scss - cms/static/sass/views/_textbooks.scss and cms/static/sass/views/_settings.scss
* move dialogue styles into cms/static/sass/elements/_modal.scss
* use the @include placeholder Bourbon mixin (http://bourbon.io/docs/#placeholder) for any placeholder styling
*/

View File

@@ -173,12 +173,14 @@ $tmg-f3: 0.125s;
// ====================
// specific UI
$notification-height: ($baseline*10);
$ui-notification-height: ($baseline*10);
$ui-update-color: $blue-l4;
// ====================
// inherited
$baseFontColor: $gray-d2;
$lighter-base-font-color: rgb(100,100,100);
$offBlack: #3c3c3c;
$green: #108614;
$lightGrey: #edf1f5;
@@ -195,6 +197,17 @@ $lightBluishGrey: rgb(197, 207, 223);
$lightBluishGrey2: rgb(213, 220, 228);
$error-red: rgb(253, 87, 87);
//carryover from LMS for xmodules
$sidebar-color: rgb(246, 246, 246);
// type
$sans-serif: $f-sans-serif;
$body-line-height: golden-ratio(.875em, 1);
// carried over from LMS for xmodules
$action-primary-active-bg: #1AA1DE; // $m-blue
$very-light-text: #fff;

View File

@@ -140,22 +140,22 @@
}
90% {
@include transform(translateY(-($notification-height)));
@include transform(translateY(-($ui-notification-height)));
}
100% {
@include transform(translateY(-($notification-height*0.99)));
@include transform(translateY(-($ui-notification-height*0.99)));
}
}
// notifications slide down
@include keyframes(notificationSlideDown) {
0% {
@include transform(translateY(-($notification-height*0.99)));
@include transform(translateY(-($ui-notification-height*0.99)));
}
10% {
@include transform(translateY(-($notification-height)));
@include transform(translateY(-($ui-notification-height)));
}
100% {
@@ -211,3 +211,39 @@
%anim-bounceOut {
@include animation(bounceOut $tmg-f1 ease-in-out 1);
}
// ====================
// flash
@include keyframes(flash) {
0%, 100% {
opacity: 1.0;
}
50% {
opacity: 0.0;
}
}
// canned animation - use if you want out of the box/non-customized anim
%anim-flash {
@include animation(flash $tmg-f1 ease-in-out 1);
}
// flash - double
@include keyframes(flashDouble) {
0%, 50%, 100% {
opacity: 1.0;
}
25%, 75% {
opacity: 0.0;
}
}
// canned animation - use if you want out of the box/non-customized anim
%anim-flashDouble {
@include animation(flashDouble $tmg-f1 ease-in-out 1);
}

View File

@@ -200,3 +200,83 @@
%view-live-button {
@extend %t-action4;
}
// ====================
// UI: drag handles
.drag-handle {
&:hover, &:focus {
cursor: move;
}
}
// UI: elem is draggable
.is-draggable {
@include transition(border-color $tmg-f2 ease-in-out 0, box-shadow $tmg-f2 ease-in-out 0);
position: relative;
.draggable-drop-indicator {
@extend %ui-depth3;
@include transition(opacity $tmg-f2 linear 0s);
@include size(100%, auto);
position: absolute;
border-top: 1px solid $blue-l1;
opacity: 0.0;
*[class^="icon-caret"] {
@extend %t-icon5;
position: absolute;
top: -12px;
left: -($baseline/4);
color: $blue-s1;
}
}
.draggable-drop-indicator-before {
top: -($baseline/2);
}
.draggable-drop-indicator-after {
bottom: -($baseline/2);
}
}
// UI: drag state - is dragging
.is-dragging {
@extend %ui-depth4;
left: -($baseline/4);
box-shadow: 0 1px 2px 0 $shadow-d1;
cursor: move;
opacity: 0.65;
border: 1px solid $gray-d3;
// UI: condition - valid drop
&.valid-drop {
border-color: $blue-s1;
box-shadow: 0 1px 2px 0 $blue-t2;
}
}
// UI: drag state - was dragging
.was-dragging {
@include transition(transform $tmg-f2 ease-in-out 0);
}
// UI: drag target
.drop-target {
&.drop-target-before {
> .draggable-drop-indicator-before {
opacity: 1.0;
}
}
&.drop-target-after {
> .draggable-drop-indicator-after {
opacity: 1.0;
}
}
}

View File

@@ -712,7 +712,7 @@
// notification showing/hiding
.wrapper-notification {
bottom: -($notification-height);
bottom: -($ui-notification-height);
// varying animations
&.is-shown {

View File

@@ -1,6 +1,11 @@
// tender help/support widget
// ====================
// UI: hiding the default tender help "tag" element
#tender_toggler {
display: none;
}
#tender_frame, #tender_window {
background-image: none !important;
background: none;

View File

@@ -233,13 +233,6 @@
}
}
}
.signup {
}
.signin {
#field-password {
position: relative;

View File

@@ -157,16 +157,8 @@
// CASE: has actions
&.has-actions {
.status-detail {
width: flex-grid(5,9);
}
.list-actions {
display: none;
width: flex-grid(3,9);
float: right;
margin-left: flex-gutter();
text-align: right;
.action-primary {
@extend %btn-primary-blue;

View File

@@ -137,394 +137,388 @@
.courseware-section {
position: relative;
background: #fff;
border-radius: 3px;
border: 1px solid $mediumGrey;
margin-top: 15px;
padding-bottom: 12px;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
@extend %ui-window;
@include transition(background $tmg-avg ease-in-out 0);
position: relative;
margin-top: ($baseline);
padding-bottom: ($baseline/2);
&:first-child {
margin-top: 0;
}
&.collapsed {
padding-bottom: 0;
}
&.collapsed {
padding-bottom: 0;
}
label {
float: left;
line-height: 29px;
}
label {
float: left;
line-height: 29px;
}
.datepair {
float: left;
margin-left: 10px;
}
.datepair {
float: left;
margin-left: 10px;
}
.section-published-date {
position: absolute;
top: 19px;
right: 80px;
padding: 4px 10px;
border-radius: 3px;
background: $lightGrey;
text-align: right;
.section-published-date {
position: absolute;
top: 19px;
right: 80px;
padding: 4px 10px;
.published-status {
@include font-size(12);
margin-right: 15px;
strong {
font-weight: bold;
}
}
.schedule-button {
@include blue-button;
}
.edit-button {
@include blue-button;
}
.schedule-button,
.edit-button {
@include font-size(11);
padding: 3px 15px 5px;
}
}
.datepair .date,
.datepair .time {
@include font-size(13);
box-shadow: none;
padding-left: 0;
padding-right: 0;
border: none;
background: none;
font-weight: bold;
color: $blue;
cursor: pointer;
}
.datepair .date {
width: 80px;
}
.datepair .time {
width: 65px;
}
&.collapsed .subsection-list,
.collapsed .subsection-list,
.collapsed > ol {
display: none !important;
}
header {
min-height: 75px;
@include clearfix();
.item-details, .section-published-date {
}
.item-details {
display: inline-block;
padding: 20px 0 10px 0;
@include clearfix();
.section-name {
@include font-size(19);
float: left;
margin-right: 10px;
width: 350px;
font-weight: bold;
color: $blue;
}
.section-name-span {
@include transition(color $tmg-f2 linear 0s);
cursor: pointer;
&:hover {
color: $orange;
}
}
.section-name-edit {
position: relative;
width: 400px;
background: $white;
input {
@include font-size(16);
}
.save-button {
@include blue-button;
padding: 7px 20px 7px;
margin-right: 5px;
}
.cancel-button {
@include white-button;
padding: 7px 20px 7px;
}
}
.section-published-date {
float: right;
border-radius: 3px;
background: $lightGrey;
text-align: right;
.published-status {
@include font-size(12);
margin-right: 15px;
@include font-size(12);
margin-right: 15px;
strong {
font-weight: bold;
}
strong {
font-weight: bold;
}
}
.schedule-button {
@include blue-button;
@include blue-button;
}
.edit-button {
@include blue-button;
@include blue-button;
}
.schedule-button,
.edit-button {
@include font-size(11);
padding: 3px 15px 5px;
}
}
.datepair .date,
.datepair .time {
@include font-size(13);
box-shadow: none;
padding-left: 0;
padding-right: 0;
border: none;
background: none;
font-weight: bold;
color: $blue;
cursor: pointer;
}
.datepair .date {
width: 80px;
}
.datepair .time {
width: 65px;
}
&.collapsed .subsection-list,
.collapsed .subsection-list,
.collapsed > ol {
display: none !important;
}
header {
min-height: 75px;
@include clearfix();
.item-details, .section-published-date {
@include font-size(11);
padding: 0 15px 2px 15px;
}
}
.item-details {
display: inline-block;
padding: 20px 0 10px 0;
@include clearfix();
.gradable-status {
position: absolute;
top: 20px;
right: 70px;
width: 145px;
.section-name {
@include font-size(19);
float: left;
margin-right: 10px;
width: 350px;
font-weight: bold;
.status-label {
@include font-size(12);
border-radius: 3px;
position: absolute;
top: 0;
right: 2px;
display: none;
width: 100px;
padding: 10px 35px 10px 10px;
background: $lightGrey;
color: $lightGrey;
text-align: right;
font-weight: bold;
line-height: 16px;
}
.menu-toggle {
z-index: 10;
position: absolute;
top: 2px;
right: 5px;
padding: 5px;
color: $lightGrey;
&:hover, &.is-active {
color: $blue;
}
.section-name-span {
@include transition(color $tmg-f2 linear 0s);
cursor: pointer;
&:hover {
color: $orange;
}
}
.section-name-edit {
position: relative;
width: 400px;
background: $white;
input {
@include font-size(16);
}
.save-button {
@include blue-button;
padding: 7px 20px 7px;
margin-right: 5px;
}
.cancel-button {
@include white-button;
padding: 7px 20px 7px;
}
}
.section-published-date {
float: right;
border-radius: 3px;
background: $lightGrey;
.published-status {
.menu {
@include font-size(12);
margin-right: 15px;
strong {
font-weight: bold;
}
}
.schedule-button {
@include blue-button;
}
.edit-button {
@include blue-button;
}
.schedule-button,
.edit-button {
@include font-size(11);
padding: 0 15px 2px 15px;
}
}
.gradable-status {
@include transition(opacity $tmg-f2 linear 0s, display $tmg-f2 linear 0s);
border-radius: 4px;
box-shadow: 0 1px 2px rgba(0, 0, 0, .2);
z-index: 1;
display: none;
opacity: 0.0;
position: absolute;
top: 20px;
right: 70px;
width: 145px;
.status-label {
@include font-size(12);
border-radius: 3px;
position: absolute;
top: 0;
right: 2px;
display: none;
width: 100px;
padding: 10px 35px 10px 10px;
background: $lightGrey;
color: $lightGrey;
text-align: right;
font-weight: bold;
line-height: 16px;
}
.menu-toggle {
z-index: 10;
position: absolute;
top: 2px;
right: 5px;
padding: 5px;
color: $lightGrey;
&:hover, &.is-active {
color: $blue;
}
}
.menu {
@include font-size(12);
@include transition(opacity $tmg-f2 linear 0s, display $tmg-f2 linear 0s);
border-radius: 4px;
box-shadow: 0 1px 2px rgba(0, 0, 0, .2);
z-index: 1;
display: none;
opacity: 0.0;
position: absolute;
top: -1px;
left: 2px;
margin: 0;
padding: 8px 12px;
background: $white;
border: 1px solid $mediumGrey;
top: -1px;
left: 2px;
margin: 0;
padding: 8px 12px;
background: $white;
border: 1px solid $mediumGrey;
li {
width: 115px;
margin-bottom: 3px;
padding-bottom: 3px;
border-bottom: 1px solid $lightGrey;
li {
width: 115px;
margin-bottom: 3px;
padding-bottom: 3px;
border-bottom: 1px solid $lightGrey;
&:last-child {
margin-bottom: 0;
padding-bottom: 0;
border: none;
&:last-child {
margin-bottom: 0;
padding-bottom: 0;
border: none;
a {
color: $darkGrey;
}
a {
color: $darkGrey;
}
}
a {
&.is-selected {
font-weight: bold;
}
}
}
// dropdown state
&.is-active {
.menu {
z-index: 1000;
display: block;
opacity: 1.0;
}
a {
.menu-toggle {
z-index: 10000;
}
&.is-selected {
font-weight: bold;
}
}
}
// set state
&.is-set {
// dropdown state
&.is-active {
.menu-toggle {
color: $blue;
}
.status-label {
.menu {
z-index: 1000;
display: block;
color: $blue;
opacity: 1.0;
}
}
float: left;
padding: 21px 0 0;
.menu-toggle {
z-index: 10000;
}
}
// set state
&.is-set {
.menu-toggle {
color: $blue;
}
.status-label {
display: block;
color: $blue;
}
}
float: left;
padding: 21px 0 0;
}
}
.item-actions {
margin-top: 21px;
margin-right: 12px;
.item-actions {
margin-top: 21px;
margin-right: 12px;
.edit-button,
.delete-button {
margin-top: -3px;
}
}
.expand-collapse-icon {
@include transition(none);
float: left;
margin: 25px 6px 16px 16px;
&.expand {
background-position: 0 0;
}
&.collapsed {
}
}
.drag-handle {
margin-left: 11px;
}
}
h3 {
@include font-size(19);
font-weight: 700;
color: $blue;
}
.section-name-span {
@include transition(color $tmg-f2 linear 0s);
cursor: pointer;
&:hover {
color: $orange;
}
}
.section-name-form {
margin-bottom: 15px;
}
.section-name-edit {
input {
@include font-size(16);
}
.save-button {
@include blue-button;
padding: 7px 20px 7px;
margin-right: 5px;
}
.cancel-button {
@include white-button;
padding: 7px 20px 7px;
}
}
h4 {
@include font-size(12);
color: #878e9d;
strong {
font-weight: bold;
}
}
.list-header {
@include linear-gradient(top, transparent, rgba(0, 0, 0, .1));
background-color: #ced2db;
border-radius: 3px 3px 0 0;
}
.subsection-list {
margin: 0 12px;
> ol {
@include tree-view;
border-top-width: 0;
}
}
&.new-section {
header {
@include clearfix();
height: auto;
}
.expand-collapse-icon {
visibility: hidden;
}
.item-details {
padding: 25px 0 0 0;
.section-name {
float: none;
width: 100%;
.edit-button,
.delete-button {
margin-top: -3px;
}
}
.expand-collapse-icon {
@include transition(none);
float: left;
margin: 25px 6px 16px 16px;
&.expand {
background-position: 0 0;
}
&.collapsed {
}
}
.drag-handle {
margin-left: 11px;
}
}
h3 {
@include font-size(19);
font-weight: 700;
color: $blue;
}
.section-name-span {
@include transition(color $tmg-f2 linear 0s);
cursor: pointer;
&:hover {
color: $orange;
}
}
.section-name-form {
margin-bottom: 15px;
}
.section-name-edit {
input {
@include font-size(16);
}
.save-button {
@include blue-button;
padding: 7px 20px 7px;
margin-right: 5px;
}
.cancel-button {
@include white-button;
padding: 7px 20px 7px;
}
}
h4 {
@include font-size(12);
color: #878e9d;
strong {
font-weight: bold;
}
}
.list-header {
@include linear-gradient(top, transparent, rgba(0, 0, 0, .1));
background-color: #ced2db;
border-radius: 3px 3px 0 0;
}
.subsection-list {
margin: 0 12px;
> ol {
@include tree-view;
border-top-width: 0;
}
}
&.new-section {
header {
@include clearfix();
height: auto;
}
.expand-collapse-icon {
visibility: hidden;
}
.item-details {
padding: 25px 0 0 0;
.section-name {
float: none;
width: 100%;
}
}
}
}
}
.toggle-button-sections {
@@ -675,35 +669,75 @@
color: $darkGrey;
}
// sort/drag and drop
.ui-droppable {
@include transition (padding 0.5s ease-in-out 0s);
min-height: 20px;
padding: 0;
// UI: DnD - specific elems/cases - section
.courseware-section {
&.dropover {
padding: 15px 0;
}
.draggable-drop-indicator-before {
top: -($baseline/2);
}
.draggable-drop-indicator-after {
bottom: -13px;
}
// CASE: DnD - empty subsection with unit dropping
.drop-target-prepend .draggable-drop-indicator-initial {
opacity: 1.0;
}
// STATE: was dropped
&.was-dropped {
background-color: $ui-update-color;
}
}
.ui-draggable-dragging {
box-shadow: 0 1px 2px rgba(0, 0, 0, .3);
border: 1px solid $darkGrey;
opacity : 0.2;
&:hover {
opacity : 1.0;
.section-item {
background: $yellow !important;
}
}
// UI: DnD - specific elems/cases - subsection
.courseware-subsection {
// hiding unit button - temporary fix until this semantically corrected
.new-unit-item {
display: none;
.draggable-drop-indicator-before {
top: 0;
}
.draggable-drop-indicator-after {
bottom: 0;
}
// CASE: DnD - empty subsection with unit dropping
.drop-target-prepend .draggable-drop-indicator-initial {
opacity: 1.0;
}
// STATE: was dropped
&.was-dropped {
> .section-item {
background-color: $ui-update-color !important; // nasty, but needed for specificity
}
}
}
ol.ui-droppable .branch:first-child .section-item {
border-top: none;
// UI: DnD - specific elems/cases - unit
.courseware-unit {
.draggable-drop-indicator-before {
top: 0;
}
.draggable-drop-indicator-after {
bottom: 0;
}
// STATE: was dropped
&.was-dropped {
> .section-item {
background-color: $ui-update-color !important; // nasty, but needed for specificity
}
}
}
// UI: DnD - specific elems/cases - empty parents splint
.ui-splint-indicator {
position: relative;
}
}

View File

@@ -400,4 +400,21 @@
}
}
}
// UI: DnD - specific elems/cases - units
.courseware-unit {
.draggable-drop-indicator-before {
top: 0;
}
.draggable-drop-indicator-after {
bottom: 0;
}
}
// UI: DnD - specific elems/cases - empty parents initial drop indicator
.draggable-drop-indicator-initial {
display: none;
}
}

View File

@@ -355,6 +355,20 @@ body.course.unit,.view-unit {
}
}
.wrapper-alert-error {
margin-top: ($baseline*1.25);
box-shadow: none;
border-top: 5px solid $red-l1;
.copy,
.title {
color: $white;
}
}
}
}
@@ -415,6 +429,19 @@ body.course.unit,.view-unit {
margin-left: 0;
}
}
// UI: DnD - specific elems/cases - unit
.courseware-unit {
// STATE: was dropped
&.was-dropped {
> .section-item {
background-color: $ui-update-color !important; // nasty, but needed for specificity
}
}
}
// ====================
// Component Editing

View File

@@ -33,6 +33,7 @@
window.baseUrl = "${settings.STATIC_URL}";
var require = {
baseUrl: baseUrl,
waitSeconds: 60,
paths: {
"domReady": "js/vendor/domReady",
"gettext": "/i18n",
@@ -60,12 +61,18 @@ var require = {
"underscore.string": "js/vendor/underscore.string.min",
"backbone": "js/vendor/backbone-min",
"backbone.associations": "js/vendor/backbone-associations-min",
"youtube": "js/load_youtube",
"tinymce": "js/vendor/tiny_mce/tiny_mce",
"jquery.tinymce": "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/xmodule",
"utility": "js/src/utility"
"utility": "js/src/utility",
"draggabilly": "js/vendor/draggabilly.pkgd",
// externally hosted files
"mathjax": "//edx-static.s3.amazonaws.com/mathjax-MathJax-727332c/MathJax.js?config=TeX-MML-AM_HTMLorMML-full",
// youtube URL does not end in ".js". We add "?noext" to the path so
// that require.js adds the ".js" to the query component of the URL,
// and leaves the path component intact.
"youtube": "//www.youtube.com/player_api?noext"
},
shim: {
"gettext": {
@@ -136,6 +143,9 @@ var require = {
deps: ["backbone"],
exports: "Backbone.Associations"
},
"youtube": {
exports: "YT"
},
"codemirror": {
exports: "CodeMirror"
},
@@ -147,16 +157,27 @@ var require = {
},
"mathjax": {
exports: "MathJax"
},
"coffee/src/main": {
deps: ["coffee/src/ajax_prefix"]
},
"coffee/src/logger": {
exports: "Logger",
deps: ["coffee/src/ajax_prefix"]
}
},
// load these automatically
deps: ["js/base", "coffee/src/main", "datepair"]
// we need "datepair" because it dynamically modifies the page when it is loaded -- yuck!
// load jquery and gettext automatically
deps: ["jquery", "gettext"],
callback: function() {
// load other scripts on every page, after jquery loads
require(["js/base", "coffee/src/main", "coffee/src/logger", "datepair"]);
// we need "datepair" because it dynamically modifies the page
// when it is loaded -- yuck!
}
};
</script>
<script type="text/javascript" src="${static.url("js/vendor/require.js")}"></script>
<script type="text/javascript" src="${static.url("coffee/src/logger.js")}"></script>
<script type="text/javascript" src="${static.url("coffee/src/ajax_prefix.js")}"></script>
## js templates
<script id="system-feedback-tpl" type="text/template">
@@ -187,6 +208,9 @@ require(['js/models/course'], function(Course) {
<%block name="content"></%block>
% if user.is_authenticated():
<script type="text/javascript">
require(['js/sock']);
</script>
<%include file="widgets/sock.html" />
% endif

View File

@@ -31,6 +31,6 @@
<a href="#" class="edit-button standard"><span class="edit-icon"></span>${_("Edit")}</a>
<a href="#" class="delete-button standard"><span class="delete-icon"></span>${_("Delete")}</a>
</div>
<a data-tooltip='${_("Drag to reorder")}' href="#" class="drag-handle"></a>
<span data-tooltip='${_("Drag to reorder")}' class="drag-handle"></span>
${preview}

View File

@@ -21,9 +21,11 @@
<label>${_("Display Name:")}</label>
<input type="text" value="${subsection.display_name_with_default | h}" class="subsection-display-name-input" data-metadata-name="display_name"/>
</div>
<div class="sortable-unit-list">
<label>${_("Units:")}</label>
${units.enum_units(subsection, subsection_units=subsection_units)}
<div class="wrapper-dnd">
<div class="sortable-unit-list">
<label>${_("Units:")}</label>
${units.enum_units(subsection, subsection_units=subsection_units)}
</div>
</div>
</article>
</div>
@@ -112,9 +114,8 @@ require(["domReady!", "jquery", "js/models/location", "js/views/overview_assignm
// I believe that current (New Section/New Subsection) cause full page reloads which means these aren't needed globally
// but we really should change that behavior.
if (!window.graderTypes) {
window.graderTypes = new CourseGraderCollection();
window.graderTypes = new CourseGraderCollection(${course_graders|n}, {parse:true});
window.graderTypes.course_location = new Location('${parent_location}');
window.graderTypes.reset(${course_graders|n});
}
$(".gradable-status").each(function(index, ele) {

View File

@@ -0,0 +1,26 @@
<%! from django.utils.translation import ugettext as _ %>
<%! from django.core.urlresolvers import reverse %>
<%block name="content">
<div class="wrapper wrapper-alert wrapper-alert-error is-shown">
<div class="error">
<div class="copy">
<h2 class="title">
<i class="icon-warning-sign"></i>
${_("We're having trouble rendering your component")}
</h2>
<p>${_("Students will not be able to access this component. Re-edit your component to fix the error.")}</p>
% if message:
<p class="description">
${_("Error:")}
${message | h}
</p>
% endif
</div>
</div>
</%block>

View File

@@ -111,13 +111,13 @@
<div class="status-detail">
<h3 class="title">${_("Success")}</h3>
<p class="copy">${_("Your imported content has now been integrated into this course")}</p>
</div>
<ul class="list-actions">
<li class="item-action">
<a href="${successful_import_redirect_url}" class="action action-primary">${_("View Updated Outline")}</a>
</li>
</ul>
<ul class="list-actions">
<li class="item-action">
<a href="${successful_import_redirect_url}" class="action action-primary">${_("View Updated Outline")}</a>
</li>
</ul>
</div>
</li>
</ol>
</div>

View File

@@ -7,29 +7,26 @@
<%block name="jsextra">
<script type="text/javascript">
require(['jquery', 'jquery.form'], function($) {
$(document).ready(function () {
require(["domReady!", "jquery", "jquery.form", "js/index"], function(doc, $) {
// showing/hiding creation rights UI
$('.show-creationrights').click(function(e){
(e).preventDefault();
$(this).closest('.wrapper-creationrights').toggleClass('is-shown').find('.ui-toggle-control').toggleClass('current');
});
// showing/hiding creation rights UI
$('.show-creationrights').click(function(e){
(e).preventDefault();
$(this).closest('.wrapper-creationrights').toggleClass('is-shown').find('.ui-toggle-control').toggleClass('current');
});
var reloadPage = function () {
location.reload();
};
var reloadPage = function () {
location.reload();
};
var showError = function () {
$('#request-coursecreator-submit').toggleClass('has-error').find('.label').text('Sorry, there was error with your request');
$('#request-coursecreator-submit').find('.icon-cog').toggleClass('icon-spin');
};
var showError = function () {
$('#request-coursecreator-submit').toggleClass('has-error').find('.label').text('Sorry, there was error with your request');
$('#request-coursecreator-submit').find('.icon-cog').toggleClass('icon-spin');
};
$('#request-coursecreator').ajaxForm({error: showError, success: reloadPage});
$('#request-coursecreator').ajaxForm({error: showError, success: reloadPage});
$('#request-coursecreator-submit').click(function(e){
$(this).toggleClass('is-disabled is-submitting').find('.label').text('Submitting Your Request');
});
$('#request-coursecreator-submit').click(function(e){
$(this).toggleClass('is-disabled is-submitting').find('.label').text('Submitting Your Request');
});
});
</script>

View File

@@ -25,9 +25,8 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
// I believe that current (New Section/New Subsection) cause full page reloads which means these aren't needed globally
// but we really should change that behavior.
if (!window.graderTypes) {
window.graderTypes = new CourseGraderCollection();
window.graderTypes = new CourseGraderCollection(${course_graders|n}, {parse:true});
window.graderTypes.course_location = new Location('${parent_location}');
window.graderTypes.reset(${course_graders|n});
}
$(".gradable-status").each(function(index, ele) {
@@ -82,7 +81,7 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
</div>
<div class="item-actions">
<a href="#" data-tooltip="${_('Delete this section')}" class="delete-button delete-section-button"><span class="delete-icon"></span></a>
<a href="#" data-tooltip="${_('Drag to re-order')}" class="drag-handle"></a>
<span data-tooltip="${_('Drag to re-order')}" class="drag-handle"></span>
</div>
</header>
</section>
@@ -138,74 +137,90 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
<div class="main-wrapper">
<div class="inner-wrapper">
<article class="courseware-overview" data-course-id="${context_course.location.url()}">
% for section in sections:
<section class="courseware-section branch" data-id="${section.location}">
<header>
<a href="#" data-tooltip="${_('Expand/collapse this section')}" class="expand-collapse-icon collapse"></a>
<div class="item-details" data-id="${section.location}">
<h3 class="section-name" data-name="${section.display_name_with_default | h}"></h3>
<div class="section-published-date">
<%
if section.start is not None:
start_date_str = section.start.strftime('%m/%d/%Y')
start_time_str = section.start.strftime('%H:%M')
else:
start_date_str = ''
start_time_str = ''
%>
%if section.start is None:
<span class="published-status">${_("This section has not been released.")}</span>
<a href="#" class="schedule-button" data-date="" data-time="" data-id="${section.location}">${_("Schedule")}</a>
%else:
<span class="published-status"><strong>${_("Will Release:")}</strong>
${date_utils.get_default_time_display(section.start)}</span>
<a href="#" class="edit-button" data-date="${start_date_str}"
data-time="${start_time_str}" data-id="${section.location}">${_("Edit")}</a>
%endif
</div>
</div>
<div class="wrapper-dnd">
<article class="courseware-overview" data-id="${context_course.location.url()}">
% for section in sections:
<section class="courseware-section branch is-draggable" data-id="${section.location}" data-parent-id="${context_course.location.url()}">
<div class="item-actions">
<a href="#" data-tooltip="${_('Delete this section')}" class="delete-button delete-section-button"><span class="delete-icon"></span></a>
<a href="#" data-tooltip="${_('Drag to reorder')}" class="drag-handle"></a>
</div>
</header>
<div class="subsection-list">
<div class="list-header">
<a href="#" class="new-subsection-item" data-category="${new_subsection_category}">
<span class="new-folder-icon"></span>${_("New Subsection")}
</a>
</div>
<ol data-section-id="${section.location.url()}">
% for subsection in section.get_children():
<li class="branch collapsed id-holder" data-id="${subsection.location}">
<div class="section-item">
<div class="details">
<a href="#" data-tooltip="${_('Expand/collapse this subsection')}" class="expand-collapse-icon expand"></a>
<a href="${reverse('edit_subsection', args=[subsection.location])}">
<span class="folder-icon"></span>
<span class="subsection-name"><span class="subsection-name-value">${subsection.display_name_with_default}</span></span>
</a>
</div>
<%include file="widgets/_ui-dnd-indicator-before.html" />
<div class="gradable-status" data-initial-status="${subsection.format if subsection.format is not None else _('Not Graded')}">
</div>
<header>
<a href="#" data-tooltip="${_('Expand/collapse this section')}" class="expand-collapse-icon collapse"></a>
<div class="item-actions">
<a href="#" data-tooltip="${_('Delete this subsection')}" class="delete-button delete-subsection-button"><span class="delete-icon"></span></a>
<a href="#" data-tooltip="${_('Drag to reorder')}" class="drag-handle"></a>
</div>
<div class="item-details" data-id="${section.location}">
<h3 class="section-name" data-name="${section.display_name_with_default | h}"></h3>
<div class="section-published-date">
<%
if section.start is not None:
start_date_str = section.start.strftime('%m/%d/%Y')
start_time_str = section.start.strftime('%H:%M')
else:
start_date_str = ''
start_time_str = ''
%>
%if section.start is None:
<span class="published-status">${_("This section has not been released.")}</span>
<a href="#" class="schedule-button" data-date="" data-time="" data-id="${section.location}">${_("Schedule")}</a>
%else:
<span class="published-status"><strong>${_("Will Release:")}</strong>
${date_utils.get_default_time_display(section.start)}</span>
<a href="#" class="edit-button" data-date="${start_date_str}"
data-time="${start_time_str}" data-id="${section.location}">${_("Edit")}</a>
%endif
</div>
${units.enum_units(subsection)}
</li>
% endfor
</ol>
</div>
</section>
% endfor
</article>
</div>
<div class="item-actions">
<a href="#" data-tooltip="${_('Delete this section')}" class="delete-button delete-section-button"><span class="delete-icon"></span></a>
<span data-tooltip="${_('Drag to reorder')}" class="drag-handle section-drag-handle"></span>
</div>
</header>
<div class="subsection-list">
<div class="list-header">
<a href="#" class="new-subsection-item" data-category="${new_subsection_category}">
<span class="new-folder-icon"></span>${_("New Subsection")}
</a>
</div>
<ol class="sortable-subsection-list" data-id="${section.location.url()}">
% for subsection in section.get_children():
<li class="courseware-subsection branch collapsed id-holder is-draggable" data-id="${subsection.location}" data-parent-id="${section.location.url()}">
<%include file="widgets/_ui-dnd-indicator-before.html" />
<div class="section-item">
<div class="details">
<a href="#" data-tooltip="${_('Expand/collapse this subsection')}" class="expand-collapse-icon expand"></a>
<a href="${reverse('edit_subsection', args=[subsection.location])}">
<span class="folder-icon"></span>
<span class="subsection-name"><span class="subsection-name-value">${subsection.display_name_with_default}</span></span>
</a>
</div>
<div class="gradable-status" data-initial-status="${subsection.format if subsection.format is not None else _('Not Graded')}">
</div>
<div class="item-actions">
<a href="#" data-tooltip="${_('Delete this subsection')}" class="delete-button delete-subsection-button"><span class="delete-icon"></span></a>
<span data-tooltip="${_('Drag to reorder')}" class="drag-handle subsection-drag-handle"></span>
</div>
</div>
${units.enum_units(subsection)}
<%include file="widgets/_ui-dnd-indicator-after.html" />
</li>
% endfor
<li class="ui-splint ui-splint-indicator">
<%include file="widgets/_ui-dnd-indicator-initial.html" />
</li>
</ol>
</div>
<%include file="widgets/_ui-dnd-indicator-after.html" />
</section>
% endfor
</article>
</div>
</div>
</div>
<footer></footer>

View File

@@ -17,6 +17,10 @@
</script>
<script type="text/javascript">
window.CMS = window.CMS || {};
CMS.URL = CMS.URL || {};
CMS.URL.UPLOAD_ASSET = '${upload_asset_url}';
require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/settings/main"],
function(doc, $, CourseDetailsModel, MainView) {
// hilighting labels when fields are focused in
@@ -37,8 +41,6 @@ require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/s
},
reset: true
});
CMS.URL.UPLOAD_ASSET = '${upload_asset_url}';
});
</script>
</%block>

View File

@@ -1,429 +0,0 @@
<%! from django.utils.translation import ugettext as _ %>
<!-- NOTE not used currently but retained b/c it's yet-to-be-wired functionality -->
<%inherit file="base.html" />
<%block name="title">${_("Schedule and details")}</%block>
<%block name="bodyclass">is-signedin course view-settings</%block>
<%namespace name='static' file='static_content.html'/>
<%!
from contentstore import utils
%>
<%block name="jsextra">
<script type="text/javascript" src="${static.url('js/models/course_relative.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/validating_view.js')}"></script>
<script type="text/javascript">
$(document).ready(function(){
});
</script>
</%block>
<%block name="content">
<!-- -->
<div class="main-wrapper">
<div class="inner-wrapper">
<h1>${_("Settings")}</h1>
<article class="settings-overview">
<div class="settings-page-section main-column">
<section class="settings-faculty">
<h2 class="title">${_("Faculty")}</h2>
<section class="settings-faculty-members">
<header>
<h3>${_("Faculty Members")}</h3>
<span class="detail">${_("Individuals instructing and helping with this course")}</span>
</header>
<div class="row">
<div class="field enum">
<ul class="input-list course-faculty-list">
<li class="input input-existing multi course-faculty-list-item">
<div class="row row-col2">
<label for="course-faculty-1-firstname">${_("Faculty First Name:")}</label>
<div class="field">
<input type="text" class="long" id="course-faculty-1-firstname">
</div>
</div>
<div class="row row-col2">
<label for="course-faculty-1-lastname">${_("Faculty Last Name:")}</label>
<div class="field">
<input type="text" class="long" id="course-faculty-1-lastname">
</div>
</div>
<div class="row row-col2">
<label for="course-faculty-1-photo">${_("Faculty Photo")}</label>
<div class="field">
<div class="input input-existing">
<div class="current current-faculty-1-photo">
<a href="#" class="remove-item remove-faculty-photo remove-video-data"><span class="delete-icon"></span> ${_("Delete Faculty Photo")}</a>
</div>
</div>
</div>
</div>
<div class="row">
<label for="course-faculty-1-bio">${_("Faculty Bio:")}</label>
<div class="field">
<textarea class="long tall edit-box tinymce" id="course-faculty-1-bio"></textarea>
<span class="tip tip-stacked">${_("A brief description of your education, experience, and expertise")}</span>
</div>
</div>
<a href="#" class="remove-item remove-faculty-data"><span class="delete-icon"></span> ${_("Delete Faculty Member")}</a>
</li>
<li class="input multi course-faculty-list-item">
<div class="row row-col2">
<label for="course-faculty-2-firstname">${_("Faculty First Name:")}</label>
<div class="field">
<input type="text" class="long" id="course-faculty-2-firstname">
</div>
</div>
<div class="row row-col2">
<label for="course-faculty-2-lastname">${_("Faculty Last Name:")}</label>
<div class="field">
<input type="text" class="long" id="course-faculty-2-lastname">
</div>
</div>
<div class="row row-col2">
<label for="course-faculty-2-photo">${_("Faculty Photo")}</label>
<div class="field">
<div class="input">
<a href="#" class="new-item new-faculty-photo add-faculty-photo-data" id="course-faculty-2-photo">
<span class="upload-icon"></span>${_("Upload Faculty Photo")}
</a>
<span class="tip tip-inline">${_("Max size: 30KB")}</span>
</div>
</div>
</div>
<div class="row">
<label for="course-faculty-2-bio">${_("Faculty Bio:")}</label>
<div class="field">
<div clas="input">
<textarea class="long tall edit-box tinymce" id="course-faculty-2-bio"></textarea>
<span class="tip tip-stacked">${_("A brief description of your education, experience, and expertise")}</span>
</div>
</div>
</div>
</li>
</ul>
<a href="#" class="new-item new-course-faculty-item add-faculty-data">
<span class="plus-icon"></span>${_("New Faculty Member")}
</a>
</div>
</div>
</section>
</section><!-- .settings-staff -->
<section class="settings-problems">
<h2 class="title">${_("Problems")}</h2>
<section class="settings-problems-general">
<header>
<h3>${_("General Settings")}</h3>
<span class="detail">${_("Course-wide settings for all problems")}</span>
</header>
<div class="row row-col2">
<h4 class="label">${_("Problem Randomization:")}</h4>
<div class="field">
<div class="input input-radio">
<input checked="checked" type="radio" name="course-problems-general-randomization" id="course-problems-general-randomization-always" value="${_('Always')}">
<div class="copy">
<label for="course-problems-general-randomization-always">${_("Always")}</label>
<span class="tip tip-stacked">${_("<strong>randomize all</strong> problems")}</span>
</div>
</div>
<div class="input input-radio">
<input type="radio" name="course-problems-general-randomization" id="course-problems-general-randomization-never" value="${_('Never')}">
<div class="copy">
<label for="course-problems-general-randomization-never">${_("Never")}</label>
<span class="tip tip-stacked">${_("<strong>do not randomize</strong> problems")}</span>
</div>
</div>
<div class="input input-radio">
<input type="radio" name="course-problems-general-randomization" id="course-problems-general-randomization-perstudent" value="${_('Per Student')}">
<div class="copy">
<label for="course-problems-general-randomization-perstudent">${_("Per Student")}</label>
<span class="tip tip-stacked">${_("randomize problems <strong>per student</strong>")}</span>
</div>
</div>
</div>
</div>
<div class="row row-col2">
<h4 class="label">${_("Show Answers:")}</h4>
<div class="field">
<div class="input input-radio">
<input checked="checked" type="radio" name="course-problems-general-showanswer" id="course-problems-general-showanswer-always" value="${_('Always')}">
<div class="copy">
<label for="course-problems-general-showanswer-always">${_("Always")}</label>
<span class="tip tip-stacked">${_("Answers will be shown after the number of attempts has been met")}</span>
</div>
</div>
<div class="input input-radio">
<input type="radio" name="course-problems-general-showanswer" id="course-problems-general-showanswer-never" value="${_('Never')}">
<div class="copy">
<label for="course-problems-general-showanswer-never">${_("Never")}</label>
<span class="tip tip-stacked">${_("Answers will never be shown, regardless of attempts")}</span>
</div>
</div>
</div>
</div>
<div class="row row-col2">
<label for="pcourse-roblems-general-attempts">${_("Number of Attempts <br /> Allowed on Problems:")} </label>
<div class="field">
<div class="input">
<input type="text" class="short" id="course-problems-general-attempts" placeholder="0 or higher" value="0">
<span class="tip tip-stacked">${_('Students will this have this number of chances to answer a problem. To set infinite atttempts, use "0"')}</span>
</div>
</div>
</div>
</section><!-- .settings-problems-general -->
<section class="settings-problems-assignment-1 settings-extras">
<header>
<h3>[${_("Assignment Type Name")}]</h3>
</header>
<div class="row row-col2">
<h4 class="label">${_("Problem Randomization:")}</h4>
<div class="field">
<div class="input input-radio">
<input checked="checked" type="radio" name="course-problems-assignment-1-randomization" id="course-problems-assignment-1-randomization-always" value="${_('Always')}">
<div class="copy">
<label for="course-problems-assignment-1-randomization-always">${_("Always")}</label>
<span class="tip tip-stacked">${_("<strong>randomize all</strong> problems")}</span>
</div>
</div>
<div class="input input-radio">
<input type="radio" name="course-problems-assignment-1-randomization" id="course-problems-assignment-1-randomization-never" value="${_('Never')}">
<div class="copy">
<label for="course-problems-assignment-1-randomization-never">${_("Never")}</label>
<span class="tip tip-stacked">${_("<strong>do not randomize</strong> problems")}</span>
</div>
</div>
<div class="input input-radio">
<input type="radio" name="course-problems-assignment-1-randomization" id="course-problems-assignment-1-randomization-perstudent" value="${_('Per Student')}">
<div class="copy">
<label for="course-problems-assignment-1-randomization-perstudent">${_("Per Student")}</label>
<span class="tip tip-stacked">${_("randomize problems <strong>per student</strong>")}</span>
</div>
</div>
</div>
</div>
<div class="row row-col2">
<h4 class="label">${_("Show Answers:")}</h4>
<div class="field">
<div class="input input-radio">
<input checked="checked" type="radio" name="course-problems-assignment-1-showanswer" id="course-problems-assignment-1-showanswer-always" value="${_('Always')}">
<div class="copy">
<label for="course-problems-assignment-1-showanswer-always">${_("Always")}</label>
<span class="tip tip-stacked">${_("Answers will be shown after the number of attempts has been met")}</span>
</div>
</div>
<div class="input input-radio">
<input type="radio" name="course-problems-assignment-1-showanswer" id="course-problems-assignment-1-showanswer-never" value="${_('Never')}">
<div class="copy">
<label for="pcourse-roblems-assignment-1-showanswer-never">${_("Never")}</label>
<span class="tip tip-stacked">${_("Answers will never be shown, regardless of attempts")}</span>
</div>
</div>
</div>
</div>
<div class="row row-col2">
<label for="course-problems-assignment-1-attempts">${_("Number of Attempts <br /> Allowed on Problems: ")}</label>
<div class="field">
<div class="input">
<input type="text" class="short" id="course-problems-assignment-1-attempts" placeholder="${_('0 or higher')}" value="0">
<span class="tip tip-stacked">${_('Students will this have this number of chances to answer a problem. To set infinite atttempts, use "0"')}</span>
</div>
</div>
</div>
</section><!-- .settings-problems-assignment-1 -->
</section><!-- .settings-problems -->
<section class="settings-discussions">
<h2 class="title">${_("Discussions")}</h2>
<section class="settings-discussions-general">
<header>
<h3>${_("General Settings")}</h3>
<span class="detail">${_("Course-wide settings for online discussion")}</span>
</header>
<div class="row row-col2">
<h4 class="label">${_("Anonymous Discussions:")}</h4>
<div class="field">
<div class="input input-radio">
<input type="radio" name="course-discussions-anonymous" id="course-discussions-anonymous-allow" value="${_('Allow')}">
<div class="copy">
<label for="course-discussions-anonymous-allow">${_("Allow")}</label>
<span class="tip tip-stacked">${_("Students and faculty <strong>will be able to post anonymously</strong>")}</span>
</div>
</div>
<div class="input input-radio">
<input checked="checked" type="radio" name="course-discussions-anonymous" id="course-discussions-anonymous-dontallow" value="${_('Do Not Allow')}">
<div class="copy">
<label for="course-discussions-anonymous-dontallow">${_("Do not allow")}</label>
<span class="tip tip-stacked">${_("<strong>Posting anonymously is not allowed</strong>. Any previous anonymous posts <strong>will be reverted to non-anonymous</strong>")}</span>
</div>
</div>
</div>
</div>
<div class="row row-col2">
<h4 class="label">${_("Anonymous Discussions:")}</h4>
<div class="field">
<div class="input input-radio">
<input checked="checked" type="radio" name="course-discussions-anonymous" id="course-discussions-anonymous-allow" value="${_('Allow')}">
<div class="copy">
<label for="course-discussions-anonymous-allow">${_("Allow")}</label>
<span class="tip tip-stacked">${_("Students and faculty <strong>will be able to post anonymously</strong>")}</span>
</div>
</div>
<div class="input input-radio">
<input disabled="disabled" type="radio" name="course-discussions-anonymous" id="course-discussions-anonymous-dontallow" value="${_('Do Not Allow')}">
<div class="copy">
<label for="course-discussions-anonymous-dontallow">${_("Do not allow")}</label>
<span class="tip tip-stacked">${_("This option is disabled since there are previous discussions that are anonymous.")}</span>
</div>
</div>
</div>
</div>
<div class="row row-col2">
<h4 class="label">${_("Discussion Categories")}</h4>
<div class="field enum">
<ul class="input-list course-discussions-categories-list sortable">
<li class="input input-existing input-default course-discussions-categories-list-item sortable-item">
<div class="group">
<label for="course-discussions-categories-1-name">${_("Category Name:")} </label>
<input type="text" class="course-discussions-categories-name" id="course-discussions-categories-1-name" placeholder="" value="${_('General')}" disabled="disabled">
</div>
<a href="#" class="drag-handle"></a>
</li>
<li class="input input-existing input-default course-discussions-categories-list-item sortable-item">
<div class="group">
<label for="course-discussions-categories-2-name">${_("Category Name:")} </label>
<input type="text" class="course-discussions-categories-name" id="course-discussions-categories-2-name" placeholder="" value="Feedback" disabled="disabled">
</div>
<a href="#" class="drag-handle"></a>
</li>
<li class="input input-existing input-default course-discussions-categories-list-item sortable-item">
<div class="group">
<label for="course-discussions-categories-3-name">${_("Category Name:")} </label>
<input type="text" class="course-discussions-categories-name" id="course-discussions-categories-3-name" placeholder="" value="${_('Troubleshooting')}" disabled="disabled">
</div>
<a href="#" class="drag-handle"></a>
</li>
<li class="input input-existing course-discussions-categories-list-item sortable-item">
<div class="group">
<label for="course-discussions-categories-4-name">${_("Category Name:")} </label>
<input type="text" class="course-discussions-categories-name" id="course-discussions-categories-4-name" placeholder="" value="${_('Study Groups')}">
<a href="#" class="remove-item remove-course-discussions-categories-data"><span class="delete-icon"></span> ${_("Delete Category")}</a>
</div>
<a href="#" class="drag-handle"></a>
</li>
<li class="input input-existing course-discussions-categories-list-item sortable-item">
<div class="group">
<label for="course-discussions-categories-5-name">${_("Category Name:")} </label>
<input type="text" class="course-discussions-categories-name" id="course-discussions-categories-5-name" placeholder="" value="${_('Lectures')}">
</div>
<a href="#" class="remove-item remove-course-discussions-categories-data"><span class="delete-icon"></span> ${_("Delete Category")}</a>
<a href="#" class="drag-handle"></a>
</li>
<li class="input input-existing course-discussions-categories-list-item sortable-item">
<div class="group">
<label for="course-discussions-categories-6-name">${_("Category Name:")} </label>
<input type="text" class="course-discussions-categories-name" id="course-discussions-categories-6-name" placeholder="" value="${_('Labs')}">
</div>
<a href="#" class="remove-item remove-course-discussions-categories-data"><span class="delete-icon"></span> ${_("Delete Category")}</a>
<a href="#" class="drag-handle"></a>
</li>
<li class="input input-existing course-discussions-categories-list-item sortable-item">
<div class="group">
<label for="course-discussions-categories-6-name">${_("Category Name:")} </label>
<input type="text" class="course-discussions-categories-name" id="course-discussions-categories-6-name" placeholder="" value="">
</div>
<a href="#" class="remove-item remove-course-discussions-categories-data"><span class="delete-icon"></span> ${_("Delete Category")}</a>
<a href="#" class="drag-handle"></a>
</li>
</ul>
<a href="#" class="new-item new-course-discussions-categories-item add-categories-data">
<span class="plus-icon"></span>${_("New Discussion Category")}
</a>
</div>
</div>
</section><!-- .settings-discussions-general -->
</section><!-- .settings-discussions -->
</div>
</article>
</div>
</div>
<footer></footer>
</%block>

View File

@@ -1,42 +0,0 @@
<%! from django.utils.translation import ugettext as _ %>
<%inherit file="base.html" />
<%! from django.core.urlresolvers import reverse %>
<%block name="title">${_("Static Pages")}</%block>
<%block name="bodyclass">view-static-pages</%block>
<%block name="content">
<div class="main-wrapper">
<div class="inner-wrapper">
<h1>Static Pages</h1>
<div class="page-actions">
</div>
<article class="static-page-overview">
<a href="#" class="new-static-page-button wip-box"><span class="plus-icon"></span> ${_("New Static Page")}</a>
<ul class="static-page-list">
<li class="static-page-item">
<a href="#" class="page-name">${_("Course Info")}</a>
<div class="item-actions">
<a href="#" class="edit-button wip"><span class="delete-icon"></span></a>
<a href="#" class="drag-handle wip"></a>
</div>
</li>
<li class="static-page-item">
<a href="#" class="page-name">${_("Textbook")}</a>
<div class="item-actions">
<a href="#" class="edit-button wip"><span class="delete-icon"></span></a>
<a href="#" class="drag-handle wip"></a>
</div>
</li>
<li class="static-page-item">
<a href="#" class="page-name">${_("Syllabus")}</a>
<div class="item-actions">
<a href="#" class="edit-button wip"><span class="delete-icon"></span></a>
<a href="#" class="drag-handle wip"></a>
</div>
</li>
</ul>
</article>
</div>
</div>
</%block>

View File

@@ -0,0 +1 @@
<span class="draggable-drop-indicator draggable-drop-indicator-after"><i class="icon-caret-right"></i></span>

View File

@@ -0,0 +1 @@
<span class="draggable-drop-indicator draggable-drop-indicator-before"><i class="icon-caret-right"></i></span>

View File

@@ -0,0 +1 @@
<span class="draggable-drop-indicator draggable-drop-indicator-initial"><i class="icon-caret-right"></i></span>

View File

@@ -28,7 +28,7 @@
</section>
<script type = "text/javascript">
require(["jquery", "codemirror/stex"], function($) {
require(["jquery", "jquery.leanModal", "codemirror/stex"], function($) {
hlstrig = $('#hls-trig-${hlskey}');
hlsmodal = $('#hls-modal-${hlskey}');

View File

@@ -5,13 +5,16 @@
This def will enumerate through a passed in subsection and list all of the units
-->
<%def name="enum_units(subsection, actions=True, selected=None, sortable=True, subsection_units=None)">
<ol ${'class="sortable-unit-list"' if sortable else ''} data-subsection-id="${subsection.location}">
<ol ${'class="sortable-unit-list"' if sortable else ''}>
<%
if subsection_units is None:
subsection_units = subsection.get_children()
%>
% for unit in subsection_units:
<li class="leaf unit" data-id="${unit.location}">
<li class="courseware-unit leaf unit is-draggable" data-id="${unit.location}" data-parent-id="${subsection.location.url()}">
<%include file="_ui-dnd-indicator-before.html" />
<%
unit_state = compute_unit_state(unit)
if unit.location == selected:
@@ -27,13 +30,17 @@ This def will enumerate through a passed in subsection and list all of the units
% if actions:
<div class="item-actions">
<a href="#" data-tooltip="Delete this unit" class="delete-button" data-id="${unit.location}"><span class="delete-icon"></span></a>
<a href="#" data-tooltip="Drag to sort" class="drag-handle"></a>
<span data-tooltip="Drag to sort" class="drag-handle unit-drag-handle"></span>
</div>
% endif
</div>
<%include file="_ui-dnd-indicator-after.html" />
</li>
% endfor
<li>
<%include file="_ui-dnd-indicator-initial.html" />
<a href="#" class="new-unit-item" data-category="${new_unit_category}" data-parent="${subsection.location}">
<span class="new-unit-icon"></span>New Unit
</a>

View File

@@ -70,9 +70,6 @@ urlpatterns = ('', # nopep8
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/(?P<category>[^/]+)/(?P<name>[^/]+)/gradeas.*$',
'contentstore.views.assignment_type_update', name='assignment_type_update'),
url(r'^pages/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$',
'contentstore.views.static_pages',
name='static_pages'),
url(r'^edit_tabs/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$',
'contentstore.views.edit_tabs', name='edit_tabs'),

View File

@@ -42,6 +42,6 @@ class CeleryConfigTest(unittest.TestCase):
# We don't know the other dict values exactly,
# but we can assert that they take the right form
self.assertTrue(isinstance(result_dict['task_id'], unicode))
self.assertTrue(isinstance(result_dict['time'], float))
self.assertIsInstance(result_dict['task_id'], unicode)
self.assertIsInstance(result_dict['time'], float)
self.assertTrue(result_dict['time'] > 0.0)

View File

@@ -2,7 +2,7 @@
# pylint: disable=W0621
from lettuce import world
from django.contrib.auth.models import User
from django.contrib.auth.models import User, Group
from student.models import CourseEnrollment
from xmodule.modulestore.django import editable_modulestore
from xmodule.contentstore.django import contentstore
@@ -50,6 +50,21 @@ def register_by_course_id(course_id, username='robot', password='test', is_staff
CourseEnrollment.enroll(user, course_id)
@world.absorb
def add_to_course_staff(username, course_num):
"""
Add the user with `username` to the course staff group
for `course_num`.
"""
# Based on code in lms/djangoapps/courseware/access.py
group_name = "instructor_{}".format(course_num)
group, _ = Group.objects.get_or_create(name=group_name)
group.save()
user = User.objects.get(username=username)
user.groups.add(group)
@world.absorb
def clear_courses():
# Flush and initialize the module store

View File

@@ -11,7 +11,6 @@
# Disable the "unused argument" warning because lettuce uses "step"
#pylint: disable=W0613
import re
from lettuce import world, step
from .course_helpers import *
from .ui_helpers import *
@@ -23,32 +22,15 @@ logger = getLogger(__name__)
@step(r'I wait (?:for )?"(\d+\.?\d*)" seconds?$')
def wait(step, seconds):
def wait_for_seconds(step, seconds):
world.wait(seconds)
REQUIREJS_WAIT = {
re.compile('settings-details'): [
"jquery", "js/models/course",
"js/models/settings/course_details", "js/views/settings/main"],
re.compile('settings-advanced'): [
"jquery", "js/models/course", "js/models/settings/advanced",
"js/views/settings/advanced", "codemirror"],
re.compile('edit\/.+vertical'): [
"jquery", "js/models/course", "coffee/src/models/module",
"coffee/src/views/unit", "jquery.ui"],
}
@step('I reload the page$')
def reload_the_page(step):
world.wait_for_ajax_complete()
world.browser.reload()
requirements = None
for test, req in REQUIREJS_WAIT.items():
if test.search(world.browser.url):
requirements = req
break
world.wait_for_requirejs(requirements)
world.wait_for_js_to_load()
@step('I press the browser back button$')
@@ -163,9 +145,9 @@ def should_see_in_the_page(step, doesnt_appear, text):
else:
multiplier = 1
if doesnt_appear:
assert world.browser.is_text_not_present(text, wait_time=5*multiplier)
assert world.browser.is_text_not_present(text, wait_time=5 * multiplier)
else:
assert world.browser.is_text_present(text, wait_time=5*multiplier)
assert world.browser.is_text_present(text, wait_time=5 * multiplier)
@step('I am logged in$')

View File

@@ -4,11 +4,13 @@
from lettuce import world
import time
import json
import re
import platform
from textwrap import dedent
from urllib import quote_plus
from selenium.common.exceptions import (
WebDriverException, TimeoutException, StaleElementReferenceException)
WebDriverException, TimeoutException,
StaleElementReferenceException)
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
@@ -16,11 +18,50 @@ from lettuce.django import django_url
from nose.tools import assert_true # pylint: disable=E0611
REQUIREJS_WAIT = {
# Settings - Schedule & Details
re.compile('^Schedule & Details Settings \|'): [
"jquery", "js/models/course",
"js/models/settings/course_details", "js/views/settings/main"],
# Settings - Advanced Settings
re.compile('^Advanced Settings \|'): [
"jquery", "js/models/course", "js/models/settings/advanced",
"js/views/settings/advanced", "codemirror"],
# Individual Unit (editing)
re.compile('^Individual Unit \|'): [
"coffee/src/models/module", "coffee/src/views/unit",
"coffee/src/views/module_edit"],
# Content - Outline
# Note that calling your org, course number, or display name, 'course' will mess this up
re.compile('^Course Outline \|'): [
"js/models/course", "js/models/location", "js/models/section",
"js/views/overview", "js/views/section_edit"],
# Dashboard
re.compile('^My Courses \|'): [
"js/sock", "gettext", "js/base",
"jquery.ui", "coffee/src/main", "underscore"],
}
@world.absorb
def wait(seconds):
time.sleep(float(seconds))
@world.absorb
def wait_for_js_to_load():
requirements = None
for test, req in REQUIREJS_WAIT.items():
if test.search(world.browser.title):
requirements = req
break
world.wait_for_requirejs(requirements)
# Selenium's `execute_async_script` function pauses Selenium's execution
# until the browser calls a specific Javascript callback; in effect,
# Selenium goes to sleep until the JS callback function wakes it back up again.
@@ -28,8 +69,6 @@ def wait(seconds):
# passed to this callback get returned from the `execute_async_script`
# function, which allows the JS to communicate information back to Python.
# Ref: https://selenium.googlecode.com/svn/trunk/docs/api/dotnet/html/M_OpenQA_Selenium_IJavaScriptExecutor_ExecuteAsyncScript.htm
@world.absorb
def wait_for_js_variable_truthy(variable):
"""
@@ -37,7 +76,7 @@ def wait_for_js_variable_truthy(variable):
environment until the given variable is defined and truthy. This process
guards against page reloads, and seamlessly retries on the next page.
"""
js = """
javascript = """
var callback = arguments[arguments.length - 1];
var unloadHandler = function() {{
callback("unload");
@@ -56,7 +95,13 @@ def wait_for_js_variable_truthy(variable):
}}, 10);
""".format(variable=variable)
for _ in range(5): # 5 attempts max
result = world.browser.driver.execute_async_script(dedent(js))
try:
result = world.browser.driver.execute_async_script(dedent(javascript))
except WebDriverException as wde:
if "document unloaded while waiting for result" in wde.msg:
result = "unload"
else:
raise
if result == "unload":
# we ran this on the wrong page. Wait a bit, and try again, when the
# browser has loaded the next page.
@@ -105,7 +150,7 @@ def wait_for_requirejs(dependencies=None):
if dependencies[0] != "jquery":
dependencies.insert(0, "jquery")
js = """
javascript = """
var callback = arguments[arguments.length - 1];
if(window.require) {{
requirejs.onError = callback;
@@ -126,19 +171,33 @@ def wait_for_requirejs(dependencies=None):
}}
""".format(deps=json.dumps(dependencies))
for _ in range(5): # 5 attempts max
result = world.browser.driver.execute_async_script(dedent(js))
try:
result = world.browser.driver.execute_async_script(dedent(javascript))
except WebDriverException as wde:
if "document unloaded while waiting for result" in wde.msg:
result = "unload"
else:
raise
if result == "unload":
# we ran this on the wrong page. Wait a bit, and try again, when the
# browser has loaded the next page.
world.wait(1)
continue
elif result not in (None, True, False):
# we got a require.js error
msg = "Error loading dependencies: type={0} modules={1}".format(
result['requireType'], result['requireModules'])
err = RequireJSError(msg)
err.error = result
raise err
# We got a require.js error
# Sometimes requireJS will throw an error with requireType=require
# This doesn't seem to cause problems on the page, so we ignore it
if result['requireType'] == 'require':
world.wait(1)
continue
# Otherwise, fail and report the error
else:
msg = "Error loading dependencies: type={0} modules={1}".format(
result['requireType'], result['requireModules'])
err = RequireJSError(msg)
err.error = result
raise err
else:
return result
@@ -153,7 +212,7 @@ def wait_for_ajax_complete():
keeps track of this information, go here:
http://stackoverflow.com/questions/3148225/jquery-active-function#3148506
"""
js = """
javascript = """
var callback = arguments[arguments.length - 1];
if(!window.jQuery) {callback(false);}
var intervalID = setInterval(function() {
@@ -163,13 +222,13 @@ def wait_for_ajax_complete():
}
}, 100);
"""
world.browser.driver.execute_async_script(dedent(js))
world.browser.driver.execute_async_script(dedent(javascript))
@world.absorb
def visit(url):
world.browser.visit(django_url(url))
wait_for_requirejs()
wait_for_js_to_load()
@world.absorb
@@ -238,11 +297,11 @@ def css_has_value(css_selector, value, index=0):
@world.absorb
def wait_for(func, timeout=5):
WebDriverWait(
driver=world.browser.driver,
timeout=timeout,
ignored_exceptions=(StaleElementReferenceException)
).until(func)
WebDriverWait(
driver=world.browser.driver,
timeout=timeout,
ignored_exceptions=(StaleElementReferenceException)
).until(func)
@world.absorb
@@ -336,17 +395,15 @@ def css_click(css_selector, index=0, wait_time=30):
This method will return True if the click worked.
"""
wait_for_clickable(css_selector, timeout=wait_time)
assert_true(world.css_find(css_selector)[index].visible,
msg="Element {}[{}] is present but not visible".format(css_selector, index))
assert_true(
world.css_visible(css_selector, index=index),
msg="Element {}[{}] is present but not visible".format(css_selector, index)
)
# Sometimes you can't click in the center of the element, as
# another element might be on top of it. In this case, try
# clicking in the upper left corner.
try:
return retry_on_exception(lambda: world.css_find(css_selector)[index].click())
except WebDriverException:
return css_click_at(css_selector, index=index)
result = retry_on_exception(lambda: world.css_find(css_selector)[index].click())
if result:
wait_for_js_to_load()
return result
@world.absorb
@@ -361,22 +418,6 @@ def css_check(css_selector, index=0, wait_time=30):
return css_click(css_selector=css_selector, index=index, wait_time=wait_time)
@world.absorb
def css_click_at(css_selector, index=0, x_coord=10, y_coord=10, timeout=5):
'''
A method to click at x,y coordinates of the element
rather than in the center of the element
'''
wait_for_clickable(css_selector, timeout=timeout)
element = css_find(css_selector)[index]
assert_true(element.visible,
msg="Element {}[{}] is present but not visible".format(css_selector, index))
element.action_chains.move_to_element_with_offset(element._element, x_coord, y_coord)
element.action_chains.click()
element.action_chains.perform()
@world.absorb
def select_option(name, value, index=0, wait_time=30):
'''
@@ -406,6 +447,7 @@ def css_fill(css_selector, text, index=0):
@world.absorb
def click_link(partial_text, index=0):
retry_on_exception(lambda: world.browser.find_link_by_partial_text(partial_text)[index].click())
wait_for_js_to_load()
@world.absorb
@@ -512,7 +554,6 @@ def retry_on_exception(func, max_attempts=5, ignored_exceptions=StaleElementRefe
while attempt < max_attempts:
try:
return func()
break
except ignored_exceptions:
world.wait(1)
attempt += 1

View File

@@ -11,7 +11,7 @@ from pymongo.errors import PyMongoError
from track.backends import BaseBackend
log = logging.getLogger('track.backends.mongodb')
log = logging.getLogger(__name__)
class MongoBackend(BaseBackend):
@@ -64,14 +64,17 @@ class MongoBackend(BaseBackend):
**extra
)
self.collection = self.connection[db_name][collection_name]
database = self.connection[db_name]
if user or password:
self.collection.database.authenticate(user, password)
database.authenticate(user, password)
self.collection = database[collection_name]
self._create_indexes()
def _create_indexes(self):
"""Ensures the proper fields are indexed"""
# WARNING: The collection will be locked during the index
# creation. If the collection has a large number of
# documents in it, the operation can take a long time.
@@ -83,8 +86,12 @@ class MongoBackend(BaseBackend):
self.collection.ensure_index('event_type')
def send(self, event):
"""Insert the event in to the Mongo collection"""
try:
self.collection.insert(event, manipulate=False)
except PyMongoError:
# The event will be lost in case of a connection error.
# pymongo will re-connect/re-authenticate automatically
# during the next event.
msg = 'Error inserting to MongoDB event tracker backend'
log.exception(msg)

View File

@@ -134,7 +134,7 @@ def add_histogram(user, block, view, frag, context): # pylint: disable=unused-a
return frag
block_id = block.id
if block.descriptor.has_score:
if block.has_score:
histogram = grade_histogram(block_id)
render_histogram = len(histogram) > 0
else:
@@ -142,7 +142,7 @@ def add_histogram(user, block, view, frag, context): # pylint: disable=unused-a
render_histogram = False
if settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'):
[filepath, filename] = getattr(block.descriptor, 'xml_attributes', {}).get('filename', ['', None])
[filepath, filename] = getattr(block, 'xml_attributes', {}).get('filename', ['', None])
osfs = block.system.filestore
if filename is not None and osfs.exists(filename):
# if original, unmangled filename exists then use it (github
@@ -163,13 +163,13 @@ def add_histogram(user, block, view, frag, context): # pylint: disable=unused-a
# TODO (ichuang): use _has_access_descriptor.can_load in lms.courseware.access, instead of now>mstart comparison here
now = datetime.datetime.now(UTC())
is_released = "unknown"
mstart = block.descriptor.start
mstart = block.start
if mstart is not None:
is_released = "<font color='red'>Yes!</font>" if (now > mstart) else "<font color='green'>Not yet</font>"
staff_context = {'fields': [(name, field.read_from(block)) for name, field in block.fields.items()],
'xml_attributes': getattr(block.descriptor, 'xml_attributes', {}),
'xml_attributes': getattr(block, 'xml_attributes', {}),
'location': block.location,
'xqa_key': block.xqa_key,
'source_file': source_file,

View File

@@ -6,7 +6,7 @@
<div class="drag_and_drop_problem_json" id="drag_and_drop_json_${id}"
style="display:none;">${drag_and_drop_json}</div>
<div class="script_placeholder" data-src="/static/js/capa/drag_and_drop.js"></div>
<div class="script_placeholder" data-src="${STATIC_URL}js/capa/drag_and_drop.js"></div>
% if status == 'unsubmitted':
<div class="unanswered" id="status_${id}">

View File

@@ -951,7 +951,7 @@ class DragAndDropTest(unittest.TestCase):
'''
def test_rendering(self):
path_to_images = '/static/images/'
path_to_images = '/dummy-static/images/'
xml_str = """
<drag_and_drop_input id="prob_1_2" img="{path}about_1.png" target_outline="false">
@@ -978,15 +978,15 @@ class DragAndDropTest(unittest.TestCase):
user_input = { # order matters, for string comparison
"target_outline": "false",
"base_image": "/static/images/about_1.png",
"base_image": "/dummy-static/images/about_1.png",
"draggables": [
{"can_reuse": "", "label": "Label 1", "id": "1", "icon": "", "target_fields": []},
{"can_reuse": "", "label": "cc", "id": "name_with_icon", "icon": "/static/images/cc.jpg", "target_fields": []},
{"can_reuse": "", "label": "arrow-left", "id": "with_icon", "icon": "/static/images/arrow-left.png", "can_reuse": "", "target_fields": []},
{"can_reuse": "", "label": "cc", "id": "name_with_icon", "icon": "/dummy-static/images/cc.jpg", "target_fields": []},
{"can_reuse": "", "label": "arrow-left", "id": "with_icon", "icon": "/dummy-static/images/arrow-left.png", "can_reuse": "", "target_fields": []},
{"can_reuse": "", "label": "Label2", "id": "5", "icon": "", "can_reuse": "", "target_fields": []},
{"can_reuse": "", "label": "Mute", "id": "2", "icon": "/static/images/mute.png", "can_reuse": "", "target_fields": []},
{"can_reuse": "", "label": "spinner", "id": "name_label_icon3", "icon": "/static/images/spinner.gif", "can_reuse": "", "target_fields": []},
{"can_reuse": "", "label": "Star", "id": "name4", "icon": "/static/images/volume.png", "can_reuse": "", "target_fields": []},
{"can_reuse": "", "label": "Mute", "id": "2", "icon": "/dummy-static/images/mute.png", "can_reuse": "", "target_fields": []},
{"can_reuse": "", "label": "spinner", "id": "name_label_icon3", "icon": "/dummy-static/images/spinner.gif", "can_reuse": "", "target_fields": []},
{"can_reuse": "", "label": "Star", "id": "name4", "icon": "/dummy-static/images/volume.png", "can_reuse": "", "target_fields": []},
{"can_reuse": "", "label": "Label3", "id": "7", "icon": "", "can_reuse": "", "target_fields": []}],
"one_per_target": "True",
"targets": [

View File

@@ -15,7 +15,7 @@ from capa.responsetypes import StudentInputError, \
ResponseError, LoncapaProblemError
from capa.util import convert_files_to_filenames
from .progress import Progress
from xmodule.x_module import XModule
from xmodule.x_module import XModule, module_attr
from xmodule.raw_module import RawDescriptor
from xmodule.exceptions import NotFoundError, ProcessingError
from xblock.fields import Scope, String, Boolean, Dict, Integer, Float
@@ -884,6 +884,8 @@ class CapaModule(CapaFields, XModule):
'max_value': score['total'],
})
return {'grade': score['score'], 'max_grade': score['total']}
def check_problem(self, data):
"""
Checks whether answers to a problem are correct
@@ -951,7 +953,7 @@ class CapaModule(CapaFields, XModule):
return {'success': msg}
raise
self.publish_grade()
published_grade = self.publish_grade()
# success = correct if ALL questions in this problem are correct
success = 'correct'
@@ -961,6 +963,8 @@ class CapaModule(CapaFields, XModule):
# NOTE: We are logging both full grading and queued-grading submissions. In the latter,
# 'success' will always be incorrect
event_info['grade'] = published_grade['grade']
event_info['max_grade'] = published_grade['max_grade']
event_info['correct_map'] = correct_map.get_dict()
event_info['success'] = success
event_info['attempts'] = self.attempts
@@ -1193,3 +1197,33 @@ class CapaDescriptor(CapaFields, RawDescriptor):
CapaDescriptor.force_save_button, CapaDescriptor.markdown,
CapaDescriptor.text_customization])
return non_editable_fields
# Proxy to CapaModule for access to any of its attributes
answer_available = module_attr('answer_available')
check_button_name = module_attr('check_button_name')
check_problem = module_attr('check_problem')
choose_new_seed = module_attr('choose_new_seed')
closed = module_attr('closed')
get_answer = module_attr('get_answer')
get_problem = module_attr('get_problem')
get_problem_html = module_attr('get_problem_html')
get_state_for_lcp = module_attr('get_state_for_lcp')
handle_input_ajax = module_attr('handle_input_ajax')
handle_problem_html_error = module_attr('handle_problem_html_error')
handle_ungraded_response = module_attr('handle_ungraded_response')
is_attempted = module_attr('is_attempted')
is_correct = module_attr('is_correct')
is_past_due = module_attr('is_past_due')
is_submitted = module_attr('is_submitted')
lcp = module_attr('lcp')
make_dict_of_responses = module_attr('make_dict_of_responses')
new_lcp = module_attr('new_lcp')
publish_grade = module_attr('publish_grade')
rescore_problem = module_attr('rescore_problem')
reset_problem = module_attr('reset_problem')
save_problem = module_attr('save_problem')
set_state_from_lcp = module_attr('set_state_from_lcp')
should_show_check_button = module_attr('should_show_check_button')
should_show_reset_button = module_attr('should_show_reset_button')
should_show_save_button = module_attr('should_show_save_button')
update_score = module_attr('update_score')

View File

@@ -496,7 +496,7 @@ class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor):
metadata_translations = {
'is_graded': 'graded',
'attempts': 'max_attempts',
}
}
def get_context(self):
_context = RawDescriptor.get_context(self)

View File

@@ -18,6 +18,7 @@ log = logging.getLogger('mitx.' + __name__)
class ConditionalFields(object):
has_children = True
show_tag_list = List(help="Poll answers", scope=Scope.content)
@@ -148,7 +149,7 @@ class ConditionalModule(ConditionalFields, XModule):
context)
return json.dumps({'html': [html], 'message': bool(message)})
html = [self.runtime.render_child(child, None, 'student_view').content for child in self.get_display_items()]
html = [child.render('student_view').content for child in self.get_display_items()]
return json.dumps({'html': html})

Some files were not shown because too many files have changed in this diff Show More