Merge branch 'master' of github.com:edx/edx-platform into bugfix/ichuang/make-edit-link-use-static-asset-path
This commit is contained in:
1
AUTHORS
1
AUTHORS
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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$')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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:
|
||||
|
||||
21
cms/djangoapps/contentstore/features/course_import.py
Normal file
21
cms/djangoapps/contentstore/features/course_import.py
Normal 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)
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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$')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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))
|
||||
@@ -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,
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
12
cms/envs/dev_shared_preview.py
Normal file
12
cms/envs/dev_shared_preview.py
Normal 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"
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
@@ -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"]
|
||||
@@ -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')
|
||||
|
||||
@@ -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>
|
||||
|
||||
377
cms/static/coffee/spec/views/overview_spec.coffee
Normal file
377
cms/static/coffee/spec/views/overview_spec.coffee
Normal 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')
|
||||
@@ -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
|
||||
|
||||
@@ -92,7 +92,6 @@ define ["backbone", "jquery", "underscore", "gettext", "xmodule",
|
||||
title: gettext('Saving…')
|
||||
saving.show()
|
||||
@model.save(data).done( =>
|
||||
# # showToastMessage("Your changes have been saved.", null, 3)
|
||||
@module = null
|
||||
@render()
|
||||
@$el.removeClass('editing')
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
161
cms/static/js/index.js
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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
32
cms/static/js/sock.js
Normal 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);
|
||||
});
|
||||
});
|
||||
17
cms/static/js/utils/cancel_on_escape.js
Normal file
17
cms/static/js/utils/cancel_on_escape.js
Normal 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;
|
||||
});
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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…')
|
||||
});
|
||||
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…')
|
||||
});
|
||||
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;
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
1
cms/static/sass/_mixins-inherited.scss
Symbolic link
1
cms/static/sass/_mixins-inherited.scss
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../common/static/sass/_mixins-inherited.scss
|
||||
@@ -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
|
||||
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -712,7 +712,7 @@
|
||||
|
||||
// notification showing/hiding
|
||||
.wrapper-notification {
|
||||
bottom: -($notification-height);
|
||||
bottom: -($ui-notification-height);
|
||||
|
||||
// varying animations
|
||||
&.is-shown {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -233,13 +233,6 @@
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.signup {
|
||||
|
||||
}
|
||||
|
||||
.signin {
|
||||
|
||||
#field-password {
|
||||
position: relative;
|
||||
|
||||
8
cms/static/sass/views/_import.scss
vendored
8
cms/static/sass/views/_import.scss
vendored
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
26
cms/templates/html_error.html
Normal file
26
cms/templates/html_error.html
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
1
cms/templates/widgets/_ui-dnd-indicator-after.html
Normal file
1
cms/templates/widgets/_ui-dnd-indicator-after.html
Normal file
@@ -0,0 +1 @@
|
||||
<span class="draggable-drop-indicator draggable-drop-indicator-after"><i class="icon-caret-right"></i></span>
|
||||
1
cms/templates/widgets/_ui-dnd-indicator-before.html
Normal file
1
cms/templates/widgets/_ui-dnd-indicator-before.html
Normal file
@@ -0,0 +1 @@
|
||||
<span class="draggable-drop-indicator draggable-drop-indicator-before"><i class="icon-caret-right"></i></span>
|
||||
1
cms/templates/widgets/_ui-dnd-indicator-initial.html
Normal file
1
cms/templates/widgets/_ui-dnd-indicator-initial.html
Normal file
@@ -0,0 +1 @@
|
||||
<span class="draggable-drop-indicator draggable-drop-indicator-initial"><i class="icon-caret-right"></i></span>
|
||||
@@ -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}');
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'),
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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$')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}">
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user