Commit 757ce61d by ichuang

Merge branch 'master' of github.com:edx/edx-platform into…

Merge branch 'master' of github.com:edx/edx-platform into bugfix/ichuang/make-edit-link-use-static-asset-path
parents d0d18f86 dbfc38df
...@@ -89,3 +89,4 @@ Akshay Jagadeesh <akjags@gmail.com> ...@@ -89,3 +89,4 @@ Akshay Jagadeesh <akjags@gmail.com>
Nick Parlante <nick.parlante@cs.stanford.edu> Nick Parlante <nick.parlante@cs.stanford.edu>
Marko Seric <marko.seric@math.uzh.ch> Marko Seric <marko.seric@math.uzh.ch>
Felipe Montoya <felipe.montoya@edunext.co> 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, ...@@ -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 in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected. 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: 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: Refactor and clean student dashboard templates.
LMS: Fix issue with CourseMode expiration dates LMS: Fix issue with CourseMode expiration dates
...@@ -22,6 +35,8 @@ Studio: Switched to loading Javascript using require.js ...@@ -22,6 +35,8 @@ Studio: Switched to loading Javascript using require.js
Studio: Better feedback during the course import process 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. LMS: Add split testing functionality for internal use.
CMS: Add edit_course_tabs management command, providing a primitive CMS: Add edit_course_tabs management command, providing a primitive
...@@ -73,6 +88,11 @@ Common: Allow instructors to input complicated expressions as answers to ...@@ -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 `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. 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 LMS: Enable beta instructor dashboard. The beta dashboard is a rearchitecture
of the existing instructor dashboard and is available by clicking a link at of the existing instructor dashboard and is available by clicking a link at
the top right of the existing dashboard. the top right of the existing dashboard.
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
# pylint: disable=W0621 # pylint: disable=W0621
from lettuce import world, step 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 auth.authz import get_user_by_email, get_course_groupname_for_role
from django.conf import settings from django.conf import settings
...@@ -224,14 +224,50 @@ def i_enabled_the_advanced_module(step, module): ...@@ -224,14 +224,50 @@ def i_enabled_the_advanced_module(step, module):
press_the_notification_button(step, 'Save') press_the_notification_button(step, 'Save')
@step('I have clicked the new unit button') @world.absorb
def open_new_unit(step): def create_course_with_unit():
step.given('I have opened a new course section in Studio') """
step.given('I have added a new subsection') Prepare for tests by creating a course with a section, subsection, and unit.
step.given('I expand the first section') Performs the following:
old_url = world.browser.url Clear out all courseware
world.css_click('a.new-unit-item') Create a course with a section, subsection, and unit
world.wait_for(lambda x: world.browser.url != old_url) 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') @step('the save notification button is disabled')
...@@ -267,9 +303,9 @@ def confirm_the_prompt(step): ...@@ -267,9 +303,9 @@ def confirm_the_prompt(step):
assert_false(world.css_find(btn_css).visible) assert_false(world.css_find(btn_css).visible)
@step(u'I am shown a (.*)$') @step(u'I am shown a prompt$')
def i_am_shown_a_notification(step, notification_type): def i_am_shown_a_notification(step):
assert world.is_css_present('.wrapper-%s' % notification_type) assert world.is_css_present('.wrapper-prompt')
def type_in_codemirror(index, text): def type_in_codemirror(index, text):
......
...@@ -80,9 +80,3 @@ Feature: CMS.Component Adding ...@@ -80,9 +80,3 @@ Feature: CMS.Component Adding
And I add a "Blank Advanced Problem" "Advanced Problem" component And I add a "Blank Advanced Problem" "Advanced Problem" component
And I delete all components And I delete all components
Then I see no 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 @@ ...@@ -2,43 +2,19 @@
#pylint: disable=W0621 #pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from nose.tools import assert_true, assert_in, assert_equal # pylint: disable=E0611 from nose.tools import assert_true, assert_in # 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)
@step(u'I add this type of single step component:$') @step(u'I add this type of single step component:$')
def add_a_single_step_component(step): def add_a_single_step_component(step):
world.wait_for_xmodule()
for step_hash in step.hashes: for step_hash in step.hashes:
component = step_hash['Component'] component = step_hash['Component']
assert_in(component, ['Discussion', 'Video']) 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:$') @step(u'I see this type of single step component:$')
...@@ -53,51 +29,24 @@ def see_a_single_step_component(step): ...@@ -53,51 +29,24 @@ def see_a_single_step_component(step):
@step(u'I add this type of( Advanced)? (HTML|Problem) component:$') @step(u'I add this type of( Advanced)? (HTML|Problem) component:$')
def add_a_multi_step_component(step, is_advanced, category): 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: for step_hash in step.hashes:
css_selector = 'a[data-type="{}"]'.format(category) world.create_component_instance(
world.css_click(css_selector) step=step,
world.wait_for_invisible(css_selector) category='{}'.format(category.lower()),
component_type=step_hash['Component'],
if is_advanced: is_advanced=bool(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)
@step(u'I see (HTML|Problem) components in this order:') @step(u'I see (HTML|Problem) components in this order:')
def see_a_multi_step_component(step, category): 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): for idx, step_hash in enumerate(step.hashes):
if category == 'HTML': if category == 'HTML':
html_matcher = { html_matcher = {
'Text': 'Text':
...@@ -107,9 +56,11 @@ def see_a_multi_step_component(step, category): ...@@ -107,9 +56,11 @@ def see_a_multi_step_component(step, category):
'E-text Written in LaTeX': 'E-text Written in LaTeX':
'<h2>Example: E-text page</h2>', '<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: 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$') @step(u'I add a "([^"]*)" "([^"]*)" component$')
......
...@@ -2,30 +2,35 @@ ...@@ -2,30 +2,35 @@
#pylint: disable=C0111 #pylint: disable=C0111
from lettuce import world 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 from terrain.steps import reload_the_page
@world.absorb @world.absorb
def create_component_instance(step, component_button_css, category, def create_component_instance(step, category, component_type=None, is_advanced=False):
expected_css, boilerplate=None, """
has_multiple_templates=True): Create a new component in a Unit.
click_new_component_button(step, component_button_css)
if category in ('problem', 'html'):
def animation_done(_driver): Parameters
script = "$('div.new-component').css('display')" ----------
return world.browser.evaluate_script(script) == 'none' 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'])
world.wait_for(animation_done) component_button_css = '.large-{}-icon'.format(category.lower())
world.css_click(component_button_css)
if has_multiple_templates: if category in ('problem', 'html'):
click_component_from_menu(category, boilerplate, expected_css) world.wait_for_invisible(component_button_css)
click_component_from_menu(category, component_type, is_advanced)
if category in ('video',): if category == 'problem':
world.wait_for_xmodule() expected_css = 'section.xmodule_CapaModule'
else:
expected_css = 'section.xmodule_{}Module'.format(category.title())
assert_true(world.is_css_present(expected_css)) assert_true(world.is_css_present(expected_css))
...@@ -33,29 +38,53 @@ def create_component_instance(step, component_button_css, category, ...@@ -33,29 +38,53 @@ def create_component_instance(step, component_button_css, category,
@world.absorb @world.absorb
def click_new_component_button(step, component_button_css): def click_new_component_button(step, component_button_css):
step.given('I have clicked the new unit button') 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.css_click(component_button_css)
@world.absorb def _click_advanced():
def click_component_from_menu(category, boilerplate, expected_css): 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 Find the link with the specified text. There should be one and only one.
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.
""" """
if boilerplate:
elem_css = "a[data-category='{}'][data-boilerplate='{}']".format(category, boilerplate) # The tab shows links for the given category
else: links = world.css_find('div.new-component-{} a'.format(category))
elem_css = "a[data-category='{}']:not([data-boilerplate])".format(category)
elements = world.css_find(elem_css) # Find the link whose text matches what you're looking for
assert_equal(len(elements), 1) matched_links = [link for link in links if link.text == component_type]
world.css_click(elem_css)
# 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 @world.absorb
......
...@@ -58,20 +58,3 @@ Feature: CMS.Course Overview ...@@ -58,20 +58,3 @@ Feature: CMS.Course Overview
And I click the "Expand All Sections" link And I click the "Expand All Sections" link
Then I see the "Collapse All Sections" link Then I see the "Collapse All Sections" link
And all sections are expanded 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): ...@@ -91,8 +91,7 @@ def i_expand_a_section(step):
@step(u'I see the "([^"]*)" link$') @step(u'I see the "([^"]*)" link$')
def i_see_the_span_with_text(step, text): def i_see_the_span_with_text(step, text):
span_locator = '.toggle-button-sections span' span_locator = '.toggle-button-sections span'
assert_true(world.is_css_present(span_locator)) assert_true(world.css_has_value(span_locator, text))
assert_equal(world.css_value(span_locator), text)
assert_true(world.css_visible(span_locator)) assert_true(world.css_visible(span_locator))
...@@ -128,10 +127,10 @@ def change_grading_status(step): ...@@ -128,10 +127,10 @@ def change_grading_status(step):
@step(u'I reorder subsections') @step(u'I reorder subsections')
def reorder_subsections(_step): def reorder_subsections(_step):
draggable_css = 'a.drag-handle' draggable_css = '.subsection-drag-handle'
ele = world.css_find(draggable_css).first ele = world.css_find(draggable_css).first
ele.action_chains.drag_and_drop_by_offset( ele.action_chains.drag_and_drop_by_offset(
ele._element, ele._element,
30, 0,
0 25
).perform() ).perform()
...@@ -151,9 +151,10 @@ def i_see_new_course_image(_step): ...@@ -151,9 +151,10 @@ def i_see_new_course_image(_step):
assert len(images) == 1 assert len(images) == 1
img = images[0] img = images[0]
expected_src = '/c4x/MITx/999/asset/image.jpg' expected_src = '/c4x/MITx/999/asset/image.jpg'
# Don't worry about the domain in the URL # Don't worry about the domain in the URL
assert img['src'].endswith(expected_src), "Was looking for {expected}, found {actual}".format( success_func = lambda _: img['src'].endswith(expected_src)
expected=expected_src, actual=img['src']) world.wait_for(success_func)
@step('the image URL should be present in the field') @step('the image URL should be present in the field')
......
...@@ -50,8 +50,8 @@ def other_delete_self(_step): ...@@ -50,8 +50,8 @@ def other_delete_self(_step):
@step(u'I make "([^"]*)" a course team admin') @step(u'I make "([^"]*)" a course team admin')
def make_course_team_admin(_step, name): def make_course_team_admin(_step, name):
admin_btn_css = '.user-item[data-email="{email}"] .user-actions .add-admin-role'.format( admin_btn_css = '.user-item[data-email="{name}@edx.org"] .user-actions .add-admin-role'.format(
email=name+'@edx.org') name=name)
world.css_click(admin_btn_css) world.css_click(admin_btn_css)
...@@ -80,8 +80,8 @@ def see_course(_step, do_not_see, gender='self'): ...@@ -80,8 +80,8 @@ def see_course(_step, do_not_see, gender='self'):
@step(u'"([^"]*)" should( not)? be marked as an admin') @step(u'"([^"]*)" should( not)? be marked as an admin')
def marked_as_admin(_step, name, not_marked_admin): def marked_as_admin(_step, name, not_marked_admin):
flag_css = '.user-item[data-email="{email}"] .flag-role.flag-role-admin'.format( flag_css = '.user-item[data-email="{name}@edx.org"] .flag-role.flag-role-admin'.format(
email=name+'@edx.org') name=name)
if not_marked_admin: if not_marked_admin:
assert world.is_css_not_present(flag_css) assert world.is_css_not_present(flag_css)
else: else:
......
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 @@ ...@@ -2,7 +2,7 @@
Feature: CMS.Discussion Component Editor Feature: CMS.Discussion Component Editor
As a course author, I want to be able to create discussion components. 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 Given I have created a Discussion Tag
And I edit and select Settings And I edit and select Settings
Then I see three alphabetized settings and their expected values Then I see three alphabetized settings and their expected values
...@@ -14,7 +14,3 @@ Feature: CMS.Discussion Component Editor ...@@ -14,7 +14,3 @@ Feature: CMS.Discussion Component Editor
And I edit and select Settings And I edit and select Settings
Then I can modify the display name Then I can modify the display name
And my display name change is persisted on save 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 ...@@ -6,11 +6,10 @@ from lettuce import world, step
@step('I have created a Discussion Tag$') @step('I have created a Discussion Tag$')
def i_created_discussion_tag(step): def i_created_discussion_tag(step):
world.create_course_with_unit()
world.create_component_instance( world.create_component_instance(
step, '.large-discussion-icon', step=step,
'discussion', category='discussion',
'.xmodule_DiscussionModule',
has_multiple_templates=False
) )
...@@ -22,12 +21,3 @@ def i_see_only_the_settings_and_values(step): ...@@ -22,12 +21,3 @@ def i_see_only_the_settings_and_values(step):
['Display Name', "Discussion", False], ['Display Name', "Discussion", False],
['Subcategory', "Topic-Level Student-Visible Label", 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 ...@@ -59,6 +59,17 @@ Feature: CMS.Course Grading
And I go back to the main course page And I go back to the main course page
Then I do see the assignment name "New Type" 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 Scenario: Settings are only persisted when saved
Given I have opened a new course in Studio Given I have opened a new course in Studio
And I have populated the course And I have populated the course
......
...@@ -106,6 +106,22 @@ def add_assignment_type(step, new_name): ...@@ -106,6 +106,22 @@ def add_assignment_type(step, new_name):
new_assignment._element.send_keys(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') @step(u'I have populated the course')
def populate_course(step): def populate_course(step):
step.given('I have added a new section') step.given('I have added a new section')
...@@ -164,7 +180,7 @@ def cannot_edit_fail(_step): ...@@ -164,7 +180,7 @@ def cannot_edit_fail(_step):
def i_change_grace_period(_step, grace_period): def i_change_grace_period(_step, grace_period):
grace_period_css = '#course-grading-graceperiod' grace_period_css = '#course-grading-graceperiod'
ele = world.css_find(grace_period_css).first ele = world.css_find(grace_period_css).first
# Sometimes it takes a moment for the JavaScript # Sometimes it takes a moment for the JavaScript
# to populate the field. If we don't wait for # to populate the field. If we don't wait for
# this to happen, then we can end up with # this to happen, then we can end up with
......
...@@ -6,9 +6,11 @@ from lettuce import world, step ...@@ -6,9 +6,11 @@ from lettuce import world, step
@step('I have created a Blank HTML Page$') @step('I have created a Blank HTML Page$')
def i_created_blank_html_page(step): def i_created_blank_html_page(step):
world.create_course_with_unit()
world.create_component_instance( world.create_component_instance(
step, '.large-html-icon', 'html', step=step,
'.xmodule_HtmlModule' category='html',
component_type='Text'
) )
...@@ -18,11 +20,10 @@ def i_see_only_the_html_display_name(step): ...@@ -18,11 +20,10 @@ def i_see_only_the_html_display_name(step):
@step('I have created an E-text Written in LaTeX$') @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( world.create_component_instance(
step, step=step,
'.large-html-icon', category='html',
'html', component_type='E-text Written in LaTeX'
'.xmodule_HtmlModule',
'latex_html.yaml'
) )
...@@ -89,3 +89,13 @@ Feature: CMS.Problem Editor ...@@ -89,3 +89,13 @@ Feature: CMS.Problem Editor
When I edit and compile the High Level Source When I edit and compile the High Level Source
Then my change to the High Level Source is persisted Then my change to the High Level Source is persisted
And when I view the High Level Source I see my changes 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
# disable missing docstring # disable missing docstring
#pylint: disable=C0111 #pylint: disable=C0111
import json
from lettuce import world, step from lettuce import world, step
from nose.tools import assert_equal, assert_true # pylint: disable=E0611 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" DISPLAY_NAME = "Display Name"
MAXIMUM_ATTEMPTS = "Maximum Attempts" MAXIMUM_ATTEMPTS = "Maximum Attempts"
...@@ -14,17 +17,16 @@ SHOW_ANSWER = "Show Answer" ...@@ -14,17 +17,16 @@ SHOW_ANSWER = "Show Answer"
@step('I have created a Blank Common Problem$') @step('I have created a Blank Common Problem$')
def i_created_blank_common_problem(step): def i_created_blank_common_problem(step):
world.create_course_with_unit()
world.create_component_instance( world.create_component_instance(
step, step=step,
'.large-problem-icon', category='problem',
'problem', component_type='Blank Common Problem'
'.xmodule_CapaModule',
'blank_common.yaml'
) )
@step('I edit and select Settings$') @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() world.edit_component_and_select_settings()
...@@ -41,7 +43,7 @@ def i_see_advanced_settings_with_values(step): ...@@ -41,7 +43,7 @@ def i_see_advanced_settings_with_values(step):
@step('I can modify the display name') @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 # 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). # (to confirm that we don't throw an error because it is of the wrong type).
index = world.get_setting_entry_index(DISPLAY_NAME) index = world.get_setting_entry_index(DISPLAY_NAME)
...@@ -58,7 +60,7 @@ def my_display_name_change_is_persisted_on_save(step): ...@@ -58,7 +60,7 @@ def my_display_name_change_is_persisted_on_save(step):
@step('I can specify special characters in the display name') @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) index = world.get_setting_entry_index(DISPLAY_NAME)
world.css_fill('.wrapper-comp-setting .setting-input', "updated ' \" &", index=index) world.css_fill('.wrapper-comp-setting .setting-input', "updated ' \" &", index=index)
if world.is_firefox(): if world.is_firefox():
...@@ -73,7 +75,7 @@ def special_chars_persisted_on_save(step): ...@@ -73,7 +75,7 @@ def special_chars_persisted_on_save(step):
@step('I can revert the display name to unset') @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) world.revert_setting_entry(DISPLAY_NAME)
verify_unset_display_name() verify_unset_display_name()
...@@ -85,7 +87,7 @@ def my_display_name_is_persisted_on_save(step): ...@@ -85,7 +87,7 @@ def my_display_name_is_persisted_on_save(step):
@step('I can select Per Student for Randomization') @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") world.browser.select(RANDOMIZATION, "Per Student")
verify_modified_randomization() verify_modified_randomization()
...@@ -104,7 +106,7 @@ def i_can_revert_to_default_for_randomization(step): ...@@ -104,7 +106,7 @@ def i_can_revert_to_default_for_randomization(step):
@step('I can set the weight to "(.*)"?') @step('I can set the weight to "(.*)"?')
def i_can_set_weight(step, weight): def i_can_set_weight(_step, weight):
set_weight(weight) set_weight(weight)
verify_modified_weight() verify_modified_weight()
...@@ -164,25 +166,24 @@ def cancel_does_not_save_changes(step): ...@@ -164,25 +166,24 @@ def cancel_does_not_save_changes(step):
@step('I have created a LaTeX Problem') @step('I have created a LaTeX Problem')
def create_latex_problem(step): def create_latex_problem(step):
world.click_new_component_button(step, '.large-problem-icon') world.create_course_with_unit()
world.create_component_instance(
def animation_done(_driver): step=step,
return world.browser.evaluate_script("$('div.new-component').css('display')") == 'none' category='problem',
world.wait_for(animation_done) component_type='Problem Written in LaTeX',
# Go to advanced tab. is_advanced=True
world.css_click('#ui-id-2') )
world.click_component_from_menu("problem", "latex_problem.yaml", '.xmodule_CapaModule')
@step('I edit and compile the High Level Source') @step('I edit and compile the High Level Source')
def edit_latex_source(step): def edit_latex_source(_step):
open_high_level_source() open_high_level_source()
type_in_codemirror(1, "hi") type_in_codemirror(1, "hi")
world.css_click('.hls-compile') world.css_click('.hls-compile')
@step('my change to the High Level Source is persisted') @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): def verify_text(driver):
css_sel = '.problem div>span' css_sel = '.problem div>span'
return world.css_text(css_sel) == 'hi' return world.css_text(css_sel) == 'hi'
...@@ -191,11 +192,53 @@ def high_level_source_persisted(step): ...@@ -191,11 +192,53 @@ def high_level_source_persisted(step):
@step('I view the High Level Source I see my changes') @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() open_high_level_source()
assert_equal('hi', world.css_value('.source-edit-box')) 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): def verify_high_level_source_links(step, visible):
if visible: if visible:
assert_true(world.is_css_present('.launch-latex-compiler'), assert_true(world.is_css_present('.launch-latex-compiler'),
......
...@@ -5,8 +5,6 @@ from lettuce import world, step ...@@ -5,8 +5,6 @@ from lettuce import world, step
from common import * from common import *
from nose.tools import assert_equal # pylint: disable=E0611 from nose.tools import assert_equal # pylint: disable=E0611
############### ACTIONS ####################
@step('I click the New Section link$') @step('I click the New Section link$')
def i_click_new_section_link(_step): def i_click_new_section_link(_step):
...@@ -53,9 +51,6 @@ def i_see_a_mini_notification(_step, _type): ...@@ -53,9 +51,6 @@ def i_see_a_mini_notification(_step, _type):
assert world.is_css_present(saving_css) assert world.is_css_present(saving_css)
############ ASSERTIONS ###################
@step('I see my section on the Courseware page$') @step('I see my section on the Courseware page$')
def i_see_my_section_on_the_courseware_page(_step): def i_see_my_section_on_the_courseware_page(_step):
see_my_section_on_the_courseware_page('My Section') see_my_section_on_the_courseware_page('My Section')
...@@ -125,8 +120,6 @@ def the_section_release_date_is_updated(_step): ...@@ -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') assert_equal(status_text, 'Will Release: 12/25/2013 at 00:00 UTC')
############ HELPER METHODS ###################
def save_section_name(name): def save_section_name(name):
name_css = '.new-section-name' name_css = '.new-section-name'
save_css = '.new-section-name-save' save_css = '.new-section-name-save'
......
...@@ -47,7 +47,7 @@ def name_textbook(_step, name): ...@@ -47,7 +47,7 @@ def name_textbook(_step, name):
@step(u'I name the (first|second|third) chapter "([^"]*)"') @step(u'I name the (first|second|third) chapter "([^"]*)"')
def name_chapter(_step, ordinal, name): def name_chapter(_step, ordinal, name):
index = ["first", "second", "third"].index(ordinal) 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) world.css_fill(input_css, name)
if world.is_firefox(): if world.is_firefox():
world.trigger_event(input_css) world.trigger_event(input_css)
...@@ -56,7 +56,7 @@ def name_chapter(_step, ordinal, name): ...@@ -56,7 +56,7 @@ def name_chapter(_step, ordinal, name):
@step(u'I type in "([^"]*)" for the (first|second|third) chapter asset') @step(u'I type in "([^"]*)" for the (first|second|third) chapter asset')
def asset_chapter(_step, name, ordinal): def asset_chapter(_step, name, ordinal):
index = ["first", "second", "third"].index(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) world.css_fill(input_css, name)
if world.is_firefox(): if world.is_firefox():
world.trigger_event(input_css) world.trigger_event(input_css)
...@@ -65,7 +65,7 @@ def asset_chapter(_step, name, ordinal): ...@@ -65,7 +65,7 @@ def asset_chapter(_step, name, ordinal):
@step(u'I click the Upload Asset link for the (first|second|third) chapter') @step(u'I click the Upload Asset link for the (first|second|third) chapter')
def click_upload_asset(_step, ordinal): def click_upload_asset(_step, ordinal):
index = ["first", "second", "third"].index(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) world.css_click(button_css)
......
...@@ -191,7 +191,7 @@ def view_asset(_step, status): ...@@ -191,7 +191,7 @@ def view_asset(_step, status):
# Note that world.visit would trigger a 403 error instead of displaying "Unauthorized" # Note that world.visit would trigger a 403 error instead of displaying "Unauthorized"
# Instead, we can drop back into the selenium driver get command. # Instead, we can drop back into the selenium driver get command.
world.browser.driver.get(url) 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$') @step('I see a confirmation that the file was deleted$')
......
...@@ -15,12 +15,17 @@ Feature: CMS.Video Component Editor ...@@ -15,12 +15,17 @@ Feature: CMS.Video Component Editor
Then I can modify the display name Then I can modify the display name
And my video display name change is persisted on save 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 # Sauce Labs cannot delete cookies
@skip_sauce # @skip_sauce
Scenario: Captions are hidden when "show captions" is false #Scenario: Captions are hidden when "show captions" is false
Given I have created a Video component with subtitles # Given I have created a Video component with subtitles
And I have set "show captions" to False # And I have set "show captions" to False
Then when I view the video it does not show the captions # Then when I view the video it does not show the captions
# Sauce Labs cannot delete cookies # Sauce Labs cannot delete cookies
@skip_sauce @skip_sauce
......
...@@ -2,28 +2,33 @@ ...@@ -2,28 +2,33 @@
Feature: CMS.Video Component Feature: CMS.Video Component
As a course author, I want to be able to view my created videos in Studio. 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 # Video Alpha Features will work in Firefox only when Firefox is the active window
Scenario: Autoplay is disabled in Studio Scenario: Autoplay is disabled in Studio
Given I have created a Video component Given I have created a Video component
Then when I view the video it does not have autoplay enabled Then when I view the video it does not have autoplay enabled
# 2
Scenario: Creating a video takes a single click Scenario: Creating a video takes a single click
Given I have clicked the new unit button Given I have clicked the new unit button
Then creating a video takes a single click Then creating a video takes a single click
# 3
# Sauce Labs cannot delete cookies # Sauce Labs cannot delete cookies
@skip_sauce # @skip_sauce
Scenario: Captions are hidden correctly #Scenario: Captions are hidden correctly
Given I have created a Video component with subtitles # Given I have created a Video component with subtitles
And I have hidden captions # And I have hidden captions
Then when I view the video it does not show the captions # Then when I view the video it does not show the captions
# 4
# Sauce Labs cannot delete cookies # Sauce Labs cannot delete cookies
@skip_sauce @skip_sauce
Scenario: Captions are shown correctly Scenario: Captions are shown correctly
Given I have created a Video component with subtitles Given I have created a Video component with subtitles
Then when I view the video it does show the captions Then when I view the video it does show the captions
# 5
# Sauce Labs cannot delete cookies # Sauce Labs cannot delete cookies
@skip_sauce @skip_sauce
Scenario: Captions are toggled correctly Scenario: Captions are toggled correctly
...@@ -31,7 +36,36 @@ Feature: CMS.Video Component ...@@ -31,7 +36,36 @@ Feature: CMS.Video Component
And I have toggled captions And I have toggled captions
Then when I view the video it does show the captions Then when I view the video it does show the captions
# 6
Scenario: Video data is shown correctly Scenario: Video data is shown correctly
Given I have created a video with only XML data Given I have created a video with only XML data
And I reload the page And I reload the page
Then the correct Youtube video is shown 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 ...@@ -4,14 +4,18 @@ from lettuce import world, step
from xmodule.modulestore import Location from xmodule.modulestore import Location
from contentstore.utils import get_modulestore from contentstore.utils import get_modulestore
BUTTONS = {
'CC': '.hide-subtitles',
'volume': '.volume',
}
@step('I have created a Video component$') @step('I have created a Video component$')
def i_created_a_video_component(step): def i_created_a_video_component(step):
world.create_course_with_unit()
world.create_component_instance( world.create_component_instance(
step, '.large-video-icon', step=step,
'video', category='video',
'.xmodule_VideoModule',
has_multiple_templates=False
) )
...@@ -19,6 +23,7 @@ def i_created_a_video_component(step): ...@@ -19,6 +23,7 @@ def i_created_a_video_component(step):
def i_created_a_video_with_subs(_step): def i_created_a_video_with_subs(_step):
_step.given('I have created a Video component with subtitles "OEoXaMPEzfM"') _step.given('I have created a Video component with subtitles "OEoXaMPEzfM"')
@step('I have created a Video component with subtitles "([^"]*)"$') @step('I have created a Video component with subtitles "([^"]*)"$')
def i_created_a_video_with_subs_with_name(_step, sub_id): def i_created_a_video_with_subs_with_name(_step, sub_id):
_step.given('I have created a Video component') _step.given('I have created a Video component')
...@@ -115,3 +120,37 @@ def the_youtube_video_is_shown(_step): ...@@ -115,3 +120,37 @@ def the_youtube_video_is_shown(_step):
world.wait_for_xmodule() world.wait_for_xmodule()
ele = world.css_find('.video').first ele = world.css_find('.video').first
assert ele['data-streams'].split(':')[1] == world.scenario_dict['YOUTUBE_ID'] 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')
"""
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): ...@@ -792,7 +792,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
source_location.tag, source_location.org, source_location.course, 'html', 'nonportable']) source_location.tag, source_location.org, source_location.course, 'html', 'nonportable'])
html_module = module_store.get_instance(source_location.course_id, html_module_location) 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( new_data = html_module.data.replace('/static/', '/c4x/{0}/{1}/asset/'.format(
source_location.org, source_location.course)) source_location.org, source_location.course))
module_store.update_item(html_module_location, new_data) module_store.update_item(html_module_location, new_data)
...@@ -1273,6 +1273,47 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -1273,6 +1273,47 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# export out to a tempdir # export out to a tempdir
export_to_xml(module_store, content_store, location, root_dir, 'test_export') 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) @override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE, MODULESTORE=TEST_MODULESTORE)
class ContentStoreTest(ModuleStoreTestCase): class ContentStoreTest(ModuleStoreTestCase):
...@@ -1484,7 +1525,7 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -1484,7 +1525,7 @@ class ContentStoreTest(ModuleStoreTestCase):
resp = self.client.get(reverse('course_index', kwargs=data)) resp = self.client.get(reverse('course_index', kwargs=data))
self.assertContains( self.assertContains(
resp, 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, status_code=200,
html=True html=True
) )
...@@ -1588,14 +1629,7 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -1588,14 +1629,7 @@ class ContentStoreTest(ModuleStoreTestCase):
'name': loc.name})) 'name': loc.name}))
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
# static_pages # asset_index
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
resp = self.client.get(reverse('asset_index', resp = self.client.get(reverse('asset_index',
kwargs={'org': loc.org, kwargs={'org': loc.org,
'course': loc.course, 'course': loc.course,
......
...@@ -3,7 +3,7 @@ import mock ...@@ -3,7 +3,7 @@ import mock
from django.test import TestCase from django.test import TestCase
from django.core.urlresolvers import reverse 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): class CMSLogTest(TestCase):
......
...@@ -9,13 +9,13 @@ from .checklist import * ...@@ -9,13 +9,13 @@ from .checklist import *
from .component import * from .component import *
from .course import * from .course import *
from .error import * from .error import *
from .helpers import *
from .item import * from .item import *
from .import_export import * from .import_export import *
from .preview import * from .preview import *
from .public import * from .public import *
from .user import * from .user import *
from .tabs import * from .tabs import *
from .requests import *
try: try:
from .dev import * from .dev import *
except ImportError: except ImportError:
......
...@@ -26,7 +26,7 @@ from contentstore.utils import (get_modulestore, get_lms_link_for_item, ...@@ -26,7 +26,7 @@ from contentstore.utils import (get_modulestore, get_lms_link_for_item,
from models.settings.course_grading import CourseGradingModel from models.settings.course_grading import CourseGradingModel
from .requests import _xmodule_recurse from .helpers import _xmodule_recurse
from .access import has_access from .access import has_access
from xmodule.x_module import XModuleDescriptor from xmodule.x_module import XModuleDescriptor
from xblock.plugin import PluginMissingError from xblock.plugin import PluginMissingError
......
...@@ -193,6 +193,7 @@ def import_course(request, org, course, name): ...@@ -193,6 +193,7 @@ def import_course(request, org, course, name):
if not dirpath: if not dirpath:
return JsonResponse( return JsonResponse(
{ {
'ErrMsg': _('Could not find the course.xml file in the package.'), 'ErrMsg': _('Could not find the course.xml file in the package.'),
'Stage': 2 'Stage': 2
}, },
......
...@@ -12,7 +12,7 @@ from xmodule.modulestore.inheritance import own_metadata ...@@ -12,7 +12,7 @@ from xmodule.modulestore.inheritance import own_metadata
from util.json_request import expect_json, JsonResponse from util.json_request import expect_json, JsonResponse
from ..utils import get_modulestore from ..utils import get_modulestore
from .access import has_access from .access import has_access
from .requests import _xmodule_recurse from .helpers import _xmodule_recurse
from xmodule.x_module import XModuleDescriptor from xmodule.x_module import XModuleDescriptor
__all__ = ['save_item', 'create_item', 'delete_item'] __all__ = ['save_item', 'create_item', 'delete_item']
......
...@@ -6,7 +6,7 @@ from django.conf import settings ...@@ -6,7 +6,7 @@ from django.conf import settings
from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.contrib.auth.decorators import login_required 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_modifiers import replace_static_urls, wrap_xmodule
from xmodule.error_module import ErrorDescriptor from xmodule.error_module import ErrorDescriptor
...@@ -22,7 +22,7 @@ from util.sandboxing import can_execute_unsafe_code ...@@ -22,7 +22,7 @@ from util.sandboxing import can_execute_unsafe_code
import static_replace import static_replace
from .session_kv_store import SessionKeyValueStore from .session_kv_store import SessionKeyValueStore
from .requests import render_from_lms from .helpers import render_from_lms
from .access import has_access from .access import has_access
from ..utils import get_course_for_item from ..utils import get_course_for_item
...@@ -79,9 +79,17 @@ def preview_component(request, location): ...@@ -79,9 +79,17 @@ def preview_component(request, location):
# can bind to it correctly # can bind to it correctly
component.runtime.wrappers.append(partial(wrap_xmodule, 'xmodule_edit.html')) 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', { return render_to_response('component.html', {
'preview': get_preview_html(request, component, 0), '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): ...@@ -95,11 +103,6 @@ def preview_module_system(request, preview_id, descriptor):
descriptor: An XModuleDescriptor 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 course_id = get_course_for_item(descriptor.location).location.course_id
if descriptor.location.category == 'static_tab': if descriptor.location.category == 'static_tab':
...@@ -118,7 +121,6 @@ def preview_module_system(request, preview_id, descriptor): ...@@ -118,7 +121,6 @@ def preview_module_system(request, preview_id, descriptor):
debug=True, debug=True,
replace_urls=partial(static_replace.replace_static_urls, data_directory=None, course_id=course_id), replace_urls=partial(static_replace.replace_static_urls, data_directory=None, course_id=course_id),
user=request.user, user=request.user,
xmodule_field_data=preview_field_data,
can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)), can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)),
mixins=settings.XBLOCK_MIXINS, mixins=settings.XBLOCK_MIXINS,
course_id=course_id, course_id=course_id,
...@@ -136,7 +138,8 @@ def preview_module_system(request, preview_id, descriptor): ...@@ -136,7 +138,8 @@ def preview_module_system(request, preview_id, descriptor):
getattr(descriptor, 'data_dir', descriptor.location.course), getattr(descriptor, 'data_dir', descriptor.location.course),
course_id=descriptor.location.org + '/' + descriptor.location.course + '/BOGUS_RUN_REPLACE_WHEN_AVAILABLE', 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): ...@@ -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 preview_id (str): An identifier specifying which preview this module is used for
descriptor: An XModuleDescriptor descriptor: An XModuleDescriptor
""" """
system = preview_module_system(request, preview_id, descriptor) student_data = DbModel(SessionKeyValueStore(request))
try: descriptor.bind_for_student(
module = descriptor.xmodule(system) preview_module_system(request, preview_id, descriptor),
except: lms_field_data(descriptor._field_data, student_data), # pylint: disable=protected-access
log.debug("Unable to load preview module", exc_info=True) )
module = ErrorDescriptor.from_descriptor( return descriptor
descriptor,
error_msg=exc_info_to_str(sys.exc_info())
).xmodule(system)
return module
def get_preview_html(request, descriptor, idx): def get_preview_html(request, descriptor, idx):
...@@ -167,4 +165,8 @@ def get_preview_html(request, descriptor, idx): ...@@ -167,4 +165,8 @@ def get_preview_html(request, descriptor, idx):
specified by the descriptor and idx. specified by the descriptor and idx.
""" """
module = load_preview_module(request, str(idx), descriptor) 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 ...@@ -14,10 +14,9 @@ from xmodule.modulestore.inheritance import own_metadata
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from ..utils import get_course_for_item, get_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): def initialize_course_tabs(course):
...@@ -126,20 +125,6 @@ def edit_tabs(request, org, course, coursename): ...@@ -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. # "primitive" tab edit functions driven by the command line.
# These should be replaced/deleted by a more capable GUI someday. # These should be replaced/deleted by a more capable GUI someday.
# Note that the command line UI identifies the tabs with 1-based # Note that the command line UI identifies the tabs with 1-based
......
...@@ -12,20 +12,24 @@ from .common import * ...@@ -12,20 +12,24 @@ from .common import *
from logsettings import get_logger_config from logsettings import get_logger_config
import os 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_VARIANT specifies name of the variant used, which decides what JSON
# Service variants apply config differences via env and auth JSON files, # configuration files are read during startup.
# the names of which correspond to the variant.
SERVICE_VARIANT = os.environ.get('SERVICE_VARIANT', None) SERVICE_VARIANT = os.environ.get('SERVICE_VARIANT', None)
# when not variant is specified we attempt to load an unvaried # CONFIG_ROOT specifies the directory where the JSON configuration
# config set. # files are expected to be found. If not specified, use the project
CONFIG_PREFIX = "" # 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 ################################ ############### ALWAYS THE SAME ################################
DEBUG = False DEBUG = False
TEMPLATE_DEBUG = False TEMPLATE_DEBUG = False
...@@ -77,7 +81,7 @@ CELERY_QUEUES = { ...@@ -77,7 +81,7 @@ CELERY_QUEUES = {
############# NON-SECURE ENV CONFIG ############################## ############# NON-SECURE ENV CONFIG ##############################
# Things like server locations, ports, etc. # 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) ENV_TOKENS = json.load(env_file)
EMAIL_BACKEND = ENV_TOKENS.get('EMAIL_BACKEND', EMAIL_BACKEND) EMAIL_BACKEND = ENV_TOKENS.get('EMAIL_BACKEND', EMAIL_BACKEND)
...@@ -134,7 +138,7 @@ if "TRACKING_IGNORE_URL_PATTERNS" in ENV_TOKENS: ...@@ -134,7 +138,7 @@ if "TRACKING_IGNORE_URL_PATTERNS" in ENV_TOKENS:
################ SECURE AUTH ITEMS ############################### ################ SECURE AUTH ITEMS ###############################
# Secret things: passwords, access keys, etc. # 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) AUTH_TOKENS = json.load(auth_file)
# If Segment.io key specified, load it and turn on Segment.io if the feature flag is set # If Segment.io key specified, load it and turn on Segment.io if the feature flag is set
......
...@@ -218,6 +218,11 @@ STATICFILES_DIRS = [ ...@@ -218,6 +218,11 @@ STATICFILES_DIRS = [
TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 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 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_I18N = False
USE_L10N = True USE_L10N = True
......
...@@ -9,6 +9,7 @@ from .common import * ...@@ -9,6 +9,7 @@ from .common import *
from logsettings import get_logger_config from logsettings import get_logger_config
DEBUG = True DEBUG = True
USE_I18N = True
TEMPLATE_DEBUG = DEBUG TEMPLATE_DEBUG = DEBUG
LOGGING = get_logger_config(ENV_ROOT / "log", LOGGING = get_logger_config(ENV_ROOT / "log",
logging_env="dev", logging_env="dev",
......
"""
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"
requirejs.config({ requirejs.config({
paths: { paths: {
"gettext": "xmodule_js/common_static/js/test/i18n", "gettext": "xmodule_js/common_static/js/test/i18n",
"mustache": "xmodule_js/common_static/js/vendor/mustache", "mustache": "xmodule_js/common_static/js/vendor/mustache",
"codemirror": "xmodule_js/common_static/js/vendor/CodeMirror/codemirror", "codemirror": "xmodule_js/common_static/js/vendor/CodeMirror/codemirror",
...@@ -22,7 +22,7 @@ requirejs.config({ ...@@ -22,7 +22,7 @@ requirejs.config({
"underscore.string": "xmodule_js/common_static/js/vendor/underscore.string.min", "underscore.string": "xmodule_js/common_static/js/vendor/underscore.string.min",
"backbone": "xmodule_js/common_static/js/vendor/backbone-min", "backbone": "xmodule_js/common_static/js/vendor/backbone-min",
"backbone.associations": "xmodule_js/common_static/js/vendor/backbone-associations-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", "tinymce": "xmodule_js/common_static/js/vendor/tiny_mce/tiny_mce",
"jquery.tinymce": "xmodule_js/common_static/js/vendor/tiny_mce/jquery.tinymce", "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", "mathjax": "https://edx-static.s3.amazonaws.com/mathjax-MathJax-727332c/MathJax.js?config=TeX-MML-AM_HTMLorMML-full",
...@@ -32,9 +32,11 @@ requirejs.config({ ...@@ -32,9 +32,11 @@ requirejs.config({
"squire": "xmodule_js/common_static/js/vendor/Squire", "squire": "xmodule_js/common_static/js/vendor/Squire",
"jasmine-stealth": "xmodule_js/common_static/js/vendor/jasmine-stealth", "jasmine-stealth": "xmodule_js/common_static/js/vendor/jasmine-stealth",
"jasmine.async": "xmodule_js/common_static/js/vendor/jasmine.async", "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" "coffee/src/ajax_prefix": "xmodule_js/common_static/coffee/src/ajax_prefix"
}, }
shim: { shim: {
"gettext": { "gettext": {
exports: "gettext" exports: "gettext"
...@@ -100,6 +102,9 @@ requirejs.config({ ...@@ -100,6 +102,9 @@ requirejs.config({
deps: ["backbone"], deps: ["backbone"],
exports: "Backbone.Associations" exports: "Backbone.Associations"
}, },
"youtube": {
exports: "YT"
},
"codemirror": { "codemirror": {
exports: "CodeMirror" exports: "CodeMirror"
}, },
...@@ -139,12 +144,14 @@ define([ ...@@ -139,12 +144,14 @@ define([
"coffee/spec/models/course_spec", "coffee/spec/models/metadata_spec", "coffee/spec/models/course_spec", "coffee/spec/models/metadata_spec",
"coffee/spec/models/module_spec", "coffee/spec/models/section_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/settings_grading_spec", "coffee/spec/models/textbook_spec",
"coffee/spec/models/upload_spec", "coffee/spec/models/upload_spec",
"coffee/spec/views/section_spec", "coffee/spec/views/section_spec",
"coffee/spec/views/course_info_spec", "coffee/spec/views/feedback_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/metadata_edit_spec", "coffee/spec/views/module_edit_spec",
"coffee/spec/views/overview_spec",
"coffee/spec/views/textbook_spec", "coffee/spec/views/upload_spec", "coffee/spec/views/textbook_spec", "coffee/spec/views/upload_spec",
# these tests are run separate in the cms-squire suite, due to process # these tests are run separate in the cms-squire suite, due to process
......
...@@ -22,7 +22,7 @@ requirejs.config({ ...@@ -22,7 +22,7 @@ requirejs.config({
"underscore.string": "xmodule_js/common_static/js/vendor/underscore.string.min", "underscore.string": "xmodule_js/common_static/js/vendor/underscore.string.min",
"backbone": "xmodule_js/common_static/js/vendor/backbone-min", "backbone": "xmodule_js/common_static/js/vendor/backbone-min",
"backbone.associations": "xmodule_js/common_static/js/vendor/backbone-associations-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", "tinymce": "xmodule_js/common_static/js/vendor/tiny_mce/tiny_mce",
"jquery.tinymce": "xmodule_js/common_static/js/vendor/tiny_mce/jquery.tinymce", "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", "mathjax": "https://edx-static.s3.amazonaws.com/mathjax-MathJax-727332c/MathJax.js?config=TeX-MML-AM_HTMLorMML-full",
...@@ -100,6 +100,9 @@ requirejs.config({ ...@@ -100,6 +100,9 @@ requirejs.config({
deps: ["backbone"], deps: ["backbone"],
exports: "Backbone.Associations" exports: "Backbone.Associations"
}, },
"youtube": {
exports: "YT"
},
"codemirror": { "codemirror": {
exports: "CodeMirror" exports: "CodeMirror"
}, },
......
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)
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 ...@@ -10,6 +10,7 @@ define ["js/views/course_info_handout", "js/views/course_info_update", "js/model
</div> </div>
<div class="sidebar window course-handouts" id="course-handouts-view"></div> <div class="sidebar window course-handouts" id="course-handouts-view"></div>
</div> </div>
<div class="modal-cover"></div>
""" """
beforeEach -> beforeEach ->
...@@ -45,13 +46,56 @@ define ["js/views/course_info_handout", "js/views/course_info_update", "js/model ...@@ -45,13 +46,56 @@ define ["js/views/course_info_handout", "js/views/course_info_update", "js/model
preventDefault : () -> 'no op' preventDefault : () -> 'no op'
} }
@createNewUpdate = () -> @createNewUpdate = (text) ->
# Edit button is not in the template under test (it is in parent HTML). # Edit button is not in the template under test (it is in parent HTML).
# Therefore call onNew directly. # Therefore call onNew directly.
@courseInfoEdit.onNew(@event) @courseInfoEdit.onNew(@event)
spyOn(@courseInfoEdit.$codeMirror, 'getValue').andReturn('/static/image.jpg') spyOn(@courseInfoEdit.$codeMirror, 'getValue').andReturn(text)
@courseInfoEdit.$el.find('.save-button').click() @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 -> afterEach ->
@xhrRestore() @xhrRestore()
...@@ -75,19 +119,30 @@ define ["js/views/course_info_handout", "js/views/course_info_update", "js/model ...@@ -75,19 +119,30 @@ define ["js/views/course_info_handout", "js/views/course_info_update", "js/model
it "does rewrite links for preview", -> it "does rewrite links for preview", ->
# Create a new update. # Create a new update.
@createNewUpdate() @createNewUpdate('/static/image.jpg')
# Verify the link is rewritten for preview purposes. # Verify the link is rewritten for preview purposes.
previewContents = @courseInfoEdit.$el.find('.update-contents').html() previewContents = @courseInfoEdit.$el.find('.update-contents').html()
expect(previewContents).toEqual('base-asset-url/image.jpg') expect(previewContents).toEqual('base-asset-url/image.jpg')
it "shows static links in edit mode", -> it "shows static links in edit mode", ->
@createNewUpdate() @createNewUpdate('/static/image.jpg')
# Click edit and verify CodeMirror contents. # Click edit and verify CodeMirror contents.
@courseInfoEdit.$el.find('.edit-button').click() @courseInfoEdit.$el.find('.edit-button').click()
expect(@courseInfoEdit.$codeMirror.getValue()).toEqual('/static/image.jpg') 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", -> describe "Course Handouts", ->
handoutsTemplate = readFixtures('course_info_handouts.underscore') handoutsTemplate = readFixtures('course_info_handouts.underscore')
......
...@@ -5,7 +5,6 @@ define ["coffee/src/views/module_edit", "xmodule"], (ModuleEdit) -> ...@@ -5,7 +5,6 @@ define ["coffee/src/views/module_edit", "xmodule"], (ModuleEdit) ->
@stubModule = jasmine.createSpy("Module") @stubModule = jasmine.createSpy("Module")
@stubModule.id = 'stub-id' @stubModule.id = 'stub-id'
setFixtures """ setFixtures """
<li class="component" id="stub-id"> <li class="component" id="stub-id">
<div class="component-editor"> <div class="component-editor">
...@@ -19,7 +18,7 @@ define ["coffee/src/views/module_edit", "xmodule"], (ModuleEdit) -> ...@@ -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="edit-button"><span class="edit-icon white"></span>Edit</a>
<a href="#" class="delete-button"><span class="delete-icon white"></span>Delete</a> <a href="#" class="delete-button"><span class="delete-icon white"></span>Delete</a>
</div> </div>
<a href="#" class="drag-handle"></a> <span class="drag-handle"></span>
<section class="xmodule_display xmodule_stub" data-type="StubModule"> <section class="xmodule_display xmodule_stub" data-type="StubModule">
<div id="stub-module-content"/> <div id="stub-module-content"/>
</section> </section>
......
define ["jquery", "underscore.string", "backbone", "js/views/feedback_notification", "jquery.cookie"], define ["domReady", "jquery", "underscore.string", "backbone", "gettext",
($, str, Backbone, NotificationView) -> "js/views/feedback_notification", "jquery.cookie"],
(domReady, $, str, Backbone, gettext, NotificationView) ->
AjaxPrefix.addAjaxPrefix jQuery, -> AjaxPrefix.addAjaxPrefix jQuery, ->
$("meta[name='path_prefix']").attr('content') $("meta[name='path_prefix']").attr('content')
...@@ -36,5 +37,5 @@ define ["jquery", "underscore.string", "backbone", "js/views/feedback_notificati ...@@ -36,5 +37,5 @@ define ["jquery", "underscore.string", "backbone", "js/views/feedback_notificati
if onTouchBasedDevice() if onTouchBasedDevice()
$('body').addClass 'touch-based-device' $('body').addClass 'touch-based-device'
$(main) domReady(main)
return main return main
...@@ -92,7 +92,6 @@ define ["backbone", "jquery", "underscore", "gettext", "xmodule", ...@@ -92,7 +92,6 @@ define ["backbone", "jquery", "underscore", "gettext", "xmodule",
title: gettext('Saving&hellip;') title: gettext('Saving&hellip;')
saving.show() saving.show()
@model.save(data).done( => @model.save(data).done( =>
# # showToastMessage("Your changes have been saved.", null, 3)
@module = null @module = null
@render() @render()
@$el.removeClass('editing') @$el.removeClass('editing')
......
/*
* 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;
});
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({ ...@@ -10,13 +10,13 @@ var CourseGrader = Backbone.Model.extend({
}, },
parse : function(attrs) { parse : function(attrs) {
if (attrs['weight']) { if (attrs['weight']) {
if (!_.isNumber(attrs.weight)) attrs.weight = parseInt(attrs.weight, 10); attrs.weight = parseInt(attrs.weight, 10);
} }
if (attrs['min_count']) { 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 (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; return attrs;
}, },
......
...@@ -17,10 +17,10 @@ var CourseGradingPolicy = Backbone.Model.extend({ ...@@ -17,10 +17,10 @@ var CourseGradingPolicy = Backbone.Model.extend({
// interesting race condition: if {parse:true} when newing, then parse called before .attributes created // interesting race condition: if {parse:true} when newing, then parse called before .attributes created
if (this.attributes && this.has('graders')) { if (this.attributes && this.has('graders')) {
graderCollection = this.get('graders'); graderCollection = this.get('graders');
graderCollection.reset(attributes.graders); graderCollection.reset(attributes.graders, {parse:true});
} }
else { else {
graderCollection = new CourseGraderCollection(attributes.graders); graderCollection = new CourseGraderCollection(attributes.graders, {parse:true});
graderCollection.course_location = attributes['course_location'] || this.get('course_location'); graderCollection.course_location = attributes['course_location'] || this.get('course_location');
} }
attributes.graders = graderCollection; attributes.graders = graderCollection;
......
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);
});
});
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", ...@@ -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"], "js/views/feedback_prompt", "js/views/feedback_notification", "js/views/course_info_helper"],
function(Backbone, _, CodeMirror, CourseUpdateModel, PromptView, NotificationView, CourseInfoHelper) { function(Backbone, _, CodeMirror, CourseUpdateModel, PromptView, NotificationView, CourseInfoHelper) {
var $modalCover = $(".modal-cover");
var CourseInfoUpdateView = Backbone.View.extend({ var CourseInfoUpdateView = Backbone.View.extend({
// collection is CourseUpdateCollection // collection is CourseUpdateCollection
events: { events: {
...@@ -18,6 +17,8 @@ define(["backbone", "underscore", "codemirror", "js/models/course_update", ...@@ -18,6 +17,8 @@ define(["backbone", "underscore", "codemirror", "js/models/course_update",
this.render(); this.render();
// when the client refetches the updates as a whole, re-render them // when the client refetches the updates as a whole, re-render them
this.listenTo(this.collection, 'reset', this.render); this.listenTo(this.collection, 'reset', this.render);
this.$modalCover = $(".modal-cover");
}, },
render: function () { render: function () {
...@@ -63,8 +64,8 @@ define(["backbone", "underscore", "codemirror", "js/models/course_update", ...@@ -63,8 +64,8 @@ define(["backbone", "underscore", "codemirror", "js/models/course_update",
$newForm.addClass('editing'); $newForm.addClass('editing');
this.$currentPost = $newForm.closest('li'); this.$currentPost = $newForm.closest('li');
$modalCover.show(); this.$modalCover.show();
$modalCover.bind('click', function() { this.$modalCover.bind('click', function() {
self.closeEditor(true); self.closeEditor(true);
}); });
...@@ -120,9 +121,9 @@ define(["backbone", "underscore", "codemirror", "js/models/course_update", ...@@ -120,9 +121,9 @@ define(["backbone", "underscore", "codemirror", "js/models/course_update",
this.$codeMirror = CourseInfoHelper.editWithCodeMirror( this.$codeMirror = CourseInfoHelper.editWithCodeMirror(
targetModel, 'content', self.options['base_asset_url'], $textArea.get(0)); targetModel, 'content', self.options['base_asset_url'], $textArea.get(0));
$modalCover.show(); this.$modalCover.show();
$modalCover.bind('click', function() { this.$modalCover.bind('click', function() {
self.closeEditor(self); self.closeEditor(false);
}); });
}, },
...@@ -197,8 +198,8 @@ define(["backbone", "underscore", "codemirror", "js/models/course_update", ...@@ -197,8 +198,8 @@ define(["backbone", "underscore", "codemirror", "js/models/course_update",
this.$currentPost.find('.CodeMirror').remove(); this.$currentPost.find('.CodeMirror').remove();
} }
$modalCover.unbind('click'); this.$modalCover.unbind('click');
$modalCover.hide(); this.$modalCover.hide();
this.$codeMirror = null; this.$codeMirror = null;
}, },
......
define(["js/views/validation", "underscore", "jquery", "js/views/settings/grader"], define(["js/views/validation", "underscore", "jquery", "jquery.ui", "js/views/settings/grader"],
function(ValidatingView, _, $, GraderView) { function(ValidatingView, _, $, ui, GraderView) {
var GradingView = ValidatingView.extend({ var GradingView = ValidatingView.extend({
// Model class is CMS.Models.Settings.CourseGradingPolicy // Model class is CMS.Models.Settings.CourseGradingPolicy
......
...@@ -28,7 +28,6 @@ prepend_path: cms/static ...@@ -28,7 +28,6 @@ prepend_path: cms/static
# Paths to library JavaScript files (optional) # Paths to library JavaScript files (optional)
lib_paths: lib_paths:
- coffee/spec/setup_require.js
- xmodule_js/common_static/js/vendor/require.js - xmodule_js/common_static/js/vendor/require.js
- xmodule_js/common_static/coffee/src/ajax_prefix.js - xmodule_js/common_static/coffee/src/ajax_prefix.js
- xmodule_js/common_static/js/src/utility.js - xmodule_js/common_static/js/src/utility.js
...@@ -51,6 +50,10 @@ lib_paths: ...@@ -51,6 +50,10 @@ lib_paths:
- xmodule_js/common_static/js/vendor/CodeMirror/codemirror.js - xmodule_js/common_static/js/vendor/CodeMirror/codemirror.js
- xmodule_js/src/xmodule.js - xmodule_js/src/xmodule.js
- xmodule_js/common_static/js/test/i18n.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 # Paths to source JavaScript files
src_paths: src_paths:
......
...@@ -28,7 +28,6 @@ prepend_path: cms/static ...@@ -28,7 +28,6 @@ prepend_path: cms/static
# Paths to library JavaScript files (optional) # Paths to library JavaScript files (optional)
lib_paths: lib_paths:
- coffee/spec/setup_require.js
- xmodule_js/common_static/js/vendor/require.js - xmodule_js/common_static/js/vendor/require.js
- xmodule_js/common_static/coffee/src/ajax_prefix.js - xmodule_js/common_static/coffee/src/ajax_prefix.js
- xmodule_js/common_static/js/src/utility.js - xmodule_js/common_static/js/src/utility.js
......
...@@ -528,9 +528,9 @@ p, ul, ol, dl { ...@@ -528,9 +528,9 @@ p, ul, ol, dl {
.new-subsection-item, .new-subsection-item,
.new-policy-item { .new-policy-item {
@include grey-button; @include grey-button;
margin: 5px 8px; @include font-size(10);
padding: 3px 10px 4px 10px; margin: ($baseline/2);
font-size: 10px; padding: 3px ($baseline/2) 4px ($baseline/2);
.new-folder-icon, .new-folder-icon,
.new-policy-icon, .new-policy-icon,
......
../../../common/static/sass/_mixins-inherited.scss
\ No newline at end of file
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
// ==================== // ====================
// view - dashboard // view - dashboard
body.dashboard { .view-dashboard {
// elements - authorship controls // elements - authorship controls
.wrapper-authorshiprights { .wrapper-authorshiprights {
...@@ -22,6 +22,35 @@ body.dashboard { ...@@ -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 // yes we have no boldness today - need to fix the resets
body strong, body strong,
...@@ -29,12 +58,13 @@ body b { ...@@ -29,12 +58,13 @@ body b {
font-weight: 700; 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; ...@@ -173,12 +173,14 @@ $tmg-f3: 0.125s;
// ==================== // ====================
// specific UI // specific UI
$notification-height: ($baseline*10); $ui-notification-height: ($baseline*10);
$ui-update-color: $blue-l4;
// ==================== // ====================
// inherited // inherited
$baseFontColor: $gray-d2; $baseFontColor: $gray-d2;
$lighter-base-font-color: rgb(100,100,100);
$offBlack: #3c3c3c; $offBlack: #3c3c3c;
$green: #108614; $green: #108614;
$lightGrey: #edf1f5; $lightGrey: #edf1f5;
...@@ -195,6 +197,17 @@ $lightBluishGrey: rgb(197, 207, 223); ...@@ -195,6 +197,17 @@ $lightBluishGrey: rgb(197, 207, 223);
$lightBluishGrey2: rgb(213, 220, 228); $lightBluishGrey2: rgb(213, 220, 228);
$error-red: rgb(253, 87, 87); $error-red: rgb(253, 87, 87);
//carryover from LMS for xmodules
$sidebar-color: rgb(246, 246, 246);
// type // type
$sans-serif: $f-sans-serif; $sans-serif: $f-sans-serif;
$body-line-height: golden-ratio(.875em, 1); $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 @@ ...@@ -140,22 +140,22 @@
} }
90% { 90% {
@include transform(translateY(-($notification-height))); @include transform(translateY(-($ui-notification-height)));
} }
100% { 100% {
@include transform(translateY(-($notification-height*0.99))); @include transform(translateY(-($ui-notification-height*0.99)));
} }
} }
// notifications slide down // notifications slide down
@include keyframes(notificationSlideDown) { @include keyframes(notificationSlideDown) {
0% { 0% {
@include transform(translateY(-($notification-height*0.99))); @include transform(translateY(-($ui-notification-height*0.99)));
} }
10% { 10% {
@include transform(translateY(-($notification-height))); @include transform(translateY(-($ui-notification-height)));
} }
100% { 100% {
...@@ -211,3 +211,39 @@ ...@@ -211,3 +211,39 @@
%anim-bounceOut { %anim-bounceOut {
@include animation(bounceOut $tmg-f1 ease-in-out 1); @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);
}
\ No newline at end of file
...@@ -200,3 +200,83 @@ ...@@ -200,3 +200,83 @@
%view-live-button { %view-live-button {
@extend %t-action4; @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 @@ ...@@ -712,7 +712,7 @@
// notification showing/hiding // notification showing/hiding
.wrapper-notification { .wrapper-notification {
bottom: -($notification-height); bottom: -($ui-notification-height);
// varying animations // varying animations
&.is-shown { &.is-shown {
......
// tender help/support widget // tender help/support widget
// ==================== // ====================
// UI: hiding the default tender help "tag" element
#tender_toggler {
display: none;
}
#tender_frame, #tender_window { #tender_frame, #tender_window {
background-image: none !important; background-image: none !important;
background: none; background: none;
......
...@@ -233,13 +233,6 @@ ...@@ -233,13 +233,6 @@
} }
} }
}
.signup {
}
.signin {
#field-password { #field-password {
position: relative; position: relative;
......
...@@ -157,16 +157,8 @@ ...@@ -157,16 +157,8 @@
// CASE: has actions // CASE: has actions
&.has-actions { &.has-actions {
.status-detail {
width: flex-grid(5,9);
}
.list-actions { .list-actions {
display: none; display: none;
width: flex-grid(3,9);
float: right;
margin-left: flex-gutter();
text-align: right;
.action-primary { .action-primary {
@extend %btn-primary-blue; @extend %btn-primary-blue;
......
...@@ -400,4 +400,21 @@ ...@@ -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 { ...@@ -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 { ...@@ -415,6 +429,19 @@ body.course.unit,.view-unit {
margin-left: 0; 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 // Component Editing
......
...@@ -33,6 +33,7 @@ ...@@ -33,6 +33,7 @@
window.baseUrl = "${settings.STATIC_URL}"; window.baseUrl = "${settings.STATIC_URL}";
var require = { var require = {
baseUrl: baseUrl, baseUrl: baseUrl,
waitSeconds: 60,
paths: { paths: {
"domReady": "js/vendor/domReady", "domReady": "js/vendor/domReady",
"gettext": "/i18n", "gettext": "/i18n",
...@@ -60,12 +61,18 @@ var require = { ...@@ -60,12 +61,18 @@ var require = {
"underscore.string": "js/vendor/underscore.string.min", "underscore.string": "js/vendor/underscore.string.min",
"backbone": "js/vendor/backbone-min", "backbone": "js/vendor/backbone-min",
"backbone.associations": "js/vendor/backbone-associations-min", "backbone.associations": "js/vendor/backbone-associations-min",
"youtube": "js/load_youtube",
"tinymce": "js/vendor/tiny_mce/tiny_mce", "tinymce": "js/vendor/tiny_mce/tiny_mce",
"jquery.tinymce": "js/vendor/tiny_mce/jquery.tinymce", "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", "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: { shim: {
"gettext": { "gettext": {
...@@ -136,6 +143,9 @@ var require = { ...@@ -136,6 +143,9 @@ var require = {
deps: ["backbone"], deps: ["backbone"],
exports: "Backbone.Associations" exports: "Backbone.Associations"
}, },
"youtube": {
exports: "YT"
},
"codemirror": { "codemirror": {
exports: "CodeMirror" exports: "CodeMirror"
}, },
...@@ -147,16 +157,27 @@ var require = { ...@@ -147,16 +157,27 @@ var require = {
}, },
"mathjax": { "mathjax": {
exports: "MathJax" exports: "MathJax"
},
"coffee/src/main": {
deps: ["coffee/src/ajax_prefix"]
},
"coffee/src/logger": {
exports: "Logger",
deps: ["coffee/src/ajax_prefix"]
} }
}, },
// load these automatically // load jquery and gettext automatically
deps: ["js/base", "coffee/src/main", "datepair"] deps: ["jquery", "gettext"],
// we need "datepair" because it dynamically modifies the page when it is loaded -- yuck! 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>
<script type="text/javascript" src="${static.url("js/vendor/require.js")}"></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 ## js templates
<script id="system-feedback-tpl" type="text/template"> <script id="system-feedback-tpl" type="text/template">
...@@ -187,6 +208,9 @@ require(['js/models/course'], function(Course) { ...@@ -187,6 +208,9 @@ require(['js/models/course'], function(Course) {
<%block name="content"></%block> <%block name="content"></%block>
% if user.is_authenticated(): % if user.is_authenticated():
<script type="text/javascript">
require(['js/sock']);
</script>
<%include file="widgets/sock.html" /> <%include file="widgets/sock.html" />
% endif % endif
......
...@@ -31,6 +31,6 @@ ...@@ -31,6 +31,6 @@
<a href="#" class="edit-button standard"><span class="edit-icon"></span>${_("Edit")}</a> <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> <a href="#" class="delete-button standard"><span class="delete-icon"></span>${_("Delete")}</a>
</div> </div>
<a data-tooltip='${_("Drag to reorder")}' href="#" class="drag-handle"></a> <span data-tooltip='${_("Drag to reorder")}' class="drag-handle"></span>
${preview} ${preview}
...@@ -21,9 +21,11 @@ ...@@ -21,9 +21,11 @@
<label>${_("Display Name:")}</label> <label>${_("Display Name:")}</label>
<input type="text" value="${subsection.display_name_with_default | h}" class="subsection-display-name-input" data-metadata-name="display_name"/> <input type="text" value="${subsection.display_name_with_default | h}" class="subsection-display-name-input" data-metadata-name="display_name"/>
</div> </div>
<div class="sortable-unit-list"> <div class="wrapper-dnd">
<label>${_("Units:")}</label> <div class="sortable-unit-list">
${units.enum_units(subsection, subsection_units=subsection_units)} <label>${_("Units:")}</label>
${units.enum_units(subsection, subsection_units=subsection_units)}
</div>
</div> </div>
</article> </article>
</div> </div>
...@@ -112,9 +114,8 @@ require(["domReady!", "jquery", "js/models/location", "js/views/overview_assignm ...@@ -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 // 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. // but we really should change that behavior.
if (!window.graderTypes) { 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.course_location = new Location('${parent_location}');
window.graderTypes.reset(${course_graders|n});
} }
$(".gradable-status").each(function(index, ele) { $(".gradable-status").each(function(index, ele) {
......
<%! 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 @@ ...@@ -111,13 +111,13 @@
<div class="status-detail"> <div class="status-detail">
<h3 class="title">${_("Success")}</h3> <h3 class="title">${_("Success")}</h3>
<p class="copy">${_("Your imported content has now been integrated into this course")}</p> <p class="copy">${_("Your imported content has now been integrated into this course")}</p>
</div>
<ul class="list-actions"> <ul class="list-actions">
<li class="item-action"> <li class="item-action">
<a href="${successful_import_redirect_url}" class="action action-primary">${_("View Updated Outline")}</a> <a href="${successful_import_redirect_url}" class="action action-primary">${_("View Updated Outline")}</a>
</li> </li>
</ul> </ul>
</div>
</li> </li>
</ol> </ol>
</div> </div>
......
...@@ -7,29 +7,26 @@ ...@@ -7,29 +7,26 @@
<%block name="jsextra"> <%block name="jsextra">
<script type="text/javascript"> <script type="text/javascript">
require(['jquery', 'jquery.form'], function($) { require(["domReady!", "jquery", "jquery.form", "js/index"], function(doc, $) {
$(document).ready(function () { // showing/hiding creation rights UI
$('.show-creationrights').click(function(e){
// showing/hiding creation rights UI (e).preventDefault();
$('.show-creationrights').click(function(e){ $(this).closest('.wrapper-creationrights').toggleClass('is-shown').find('.ui-toggle-control').toggleClass('current');
(e).preventDefault(); });
$(this).closest('.wrapper-creationrights').toggleClass('is-shown').find('.ui-toggle-control').toggleClass('current');
});
var reloadPage = function () { var reloadPage = function () {
location.reload(); location.reload();
}; };
var showError = function () { var showError = function () {
$('#request-coursecreator-submit').toggleClass('has-error').find('.label').text('Sorry, there was error with your request'); $('#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-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){ $('#request-coursecreator-submit').click(function(e){
$(this).toggleClass('is-disabled is-submitting').find('.label').text('Submitting Your Request'); $(this).toggleClass('is-disabled is-submitting').find('.label').text('Submitting Your Request');
});
}); });
}); });
</script> </script>
......
...@@ -25,9 +25,8 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v ...@@ -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 // 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. // but we really should change that behavior.
if (!window.graderTypes) { 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.course_location = new Location('${parent_location}');
window.graderTypes.reset(${course_graders|n});
} }
$(".gradable-status").each(function(index, ele) { $(".gradable-status").each(function(index, ele) {
...@@ -82,7 +81,7 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v ...@@ -82,7 +81,7 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
</div> </div>
<div class="item-actions"> <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="${_('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> </div>
</header> </header>
</section> </section>
...@@ -138,74 +137,90 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v ...@@ -138,74 +137,90 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
<div class="main-wrapper"> <div class="main-wrapper">
<div class="inner-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="item-actions"> <div class="wrapper-dnd">
<a href="#" data-tooltip="${_('Delete this section')}" class="delete-button delete-section-button"><span class="delete-icon"></span></a> <article class="courseware-overview" data-id="${context_course.location.url()}">
<a href="#" data-tooltip="${_('Drag to reorder')}" class="drag-handle"></a> % for section in sections:
</div> <section class="courseware-section branch is-draggable" data-id="${section.location}" data-parent-id="${context_course.location.url()}">
</header>
<div class="subsection-list"> <%include file="widgets/_ui-dnd-indicator-before.html" />
<div class="list-header">
<a href="#" class="new-subsection-item" data-category="${new_subsection_category}"> <header>
<span class="new-folder-icon"></span>${_("New Subsection")} <a href="#" data-tooltip="${_('Expand/collapse this section')}" class="expand-collapse-icon collapse"></a>
</a>
</div> <div class="item-details" data-id="${section.location}">
<ol data-section-id="${section.location.url()}"> <h3 class="section-name" data-name="${section.display_name_with_default | h}"></h3>
% for subsection in section.get_children(): <div class="section-published-date">
<li class="branch collapsed id-holder" data-id="${subsection.location}"> <%
<div class="section-item"> if section.start is not None:
<div class="details"> start_date_str = section.start.strftime('%m/%d/%Y')
<a href="#" data-tooltip="${_('Expand/collapse this subsection')}" class="expand-collapse-icon expand"></a> start_time_str = section.start.strftime('%H:%M')
<a href="${reverse('edit_subsection', args=[subsection.location])}"> else:
<span class="folder-icon"></span> start_date_str = ''
<span class="subsection-name"><span class="subsection-name-value">${subsection.display_name_with_default}</span></span> start_time_str = ''
</a> %>
</div> %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="gradable-status" data-initial-status="${subsection.format if subsection.format is not None else _('Not Graded')}"> <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> </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>
<div class="item-actions"> <%include file="widgets/_ui-dnd-indicator-after.html" />
<a href="#" data-tooltip="${_('Delete this subsection')}" class="delete-button delete-subsection-button"><span class="delete-icon"></span></a> </section>
<a href="#" data-tooltip="${_('Drag to reorder')}" class="drag-handle"></a> % endfor
</div> </article>
</div> </div>
${units.enum_units(subsection)}
</li>
% endfor
</ol>
</div>
</section>
% endfor
</article>
</div> </div>
</div> </div>
<footer></footer> <footer></footer>
......
...@@ -17,6 +17,10 @@ ...@@ -17,6 +17,10 @@
</script> </script>
<script type="text/javascript"> <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"], require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/settings/main"],
function(doc, $, CourseDetailsModel, MainView) { function(doc, $, CourseDetailsModel, MainView) {
// hilighting labels when fields are focused in // hilighting labels when fields are focused in
...@@ -37,8 +41,6 @@ require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/s ...@@ -37,8 +41,6 @@ require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/s
}, },
reset: true reset: true
}); });
CMS.URL.UPLOAD_ASSET = '${upload_asset_url}';
}); });
</script> </script>
</%block> </%block>
......
<%! 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>
<span class="draggable-drop-indicator draggable-drop-indicator-after"><i class="icon-caret-right"></i></span>
<span class="draggable-drop-indicator draggable-drop-indicator-before"><i class="icon-caret-right"></i></span>
<span class="draggable-drop-indicator draggable-drop-indicator-initial"><i class="icon-caret-right"></i></span>
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
</section> </section>
<script type = "text/javascript"> <script type = "text/javascript">
require(["jquery", "codemirror/stex"], function($) { require(["jquery", "jquery.leanModal", "codemirror/stex"], function($) {
hlstrig = $('#hls-trig-${hlskey}'); hlstrig = $('#hls-trig-${hlskey}');
hlsmodal = $('#hls-modal-${hlskey}'); hlsmodal = $('#hls-modal-${hlskey}');
......
...@@ -5,13 +5,16 @@ ...@@ -5,13 +5,16 @@
This def will enumerate through a passed in subsection and list all of the units 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)"> <%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: if subsection_units is None:
subsection_units = subsection.get_children() subsection_units = subsection.get_children()
%> %>
% for unit in subsection_units: % 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) unit_state = compute_unit_state(unit)
if unit.location == selected: if unit.location == selected:
...@@ -27,13 +30,17 @@ This def will enumerate through a passed in subsection and list all of the units ...@@ -27,13 +30,17 @@ This def will enumerate through a passed in subsection and list all of the units
% if actions: % if actions:
<div class="item-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="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> </div>
% endif % endif
</div> </div>
<%include file="_ui-dnd-indicator-after.html" />
</li> </li>
% endfor % endfor
<li> <li>
<%include file="_ui-dnd-indicator-initial.html" />
<a href="#" class="new-unit-item" data-category="${new_unit_category}" data-parent="${subsection.location}"> <a href="#" class="new-unit-item" data-category="${new_unit_category}" data-parent="${subsection.location}">
<span class="new-unit-icon"></span>New Unit <span class="new-unit-icon"></span>New Unit
</a> </a>
......
...@@ -70,9 +70,6 @@ urlpatterns = ('', # nopep8 ...@@ -70,9 +70,6 @@ urlpatterns = ('', # nopep8
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/(?P<category>[^/]+)/(?P<name>[^/]+)/gradeas.*$', url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/(?P<category>[^/]+)/(?P<name>[^/]+)/gradeas.*$',
'contentstore.views.assignment_type_update', name='assignment_type_update'), '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>[^/]+)$', url(r'^edit_tabs/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$',
'contentstore.views.edit_tabs', name='edit_tabs'), 'contentstore.views.edit_tabs', name='edit_tabs'),
......
...@@ -42,6 +42,6 @@ class CeleryConfigTest(unittest.TestCase): ...@@ -42,6 +42,6 @@ class CeleryConfigTest(unittest.TestCase):
# We don't know the other dict values exactly, # We don't know the other dict values exactly,
# but we can assert that they take the right form # but we can assert that they take the right form
self.assertTrue(isinstance(result_dict['task_id'], unicode)) self.assertIsInstance(result_dict['task_id'], unicode)
self.assertTrue(isinstance(result_dict['time'], float)) self.assertIsInstance(result_dict['time'], float)
self.assertTrue(result_dict['time'] > 0.0) self.assertTrue(result_dict['time'] > 0.0)
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
# pylint: disable=W0621 # pylint: disable=W0621
from lettuce import world 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 student.models import CourseEnrollment
from xmodule.modulestore.django import editable_modulestore from xmodule.modulestore.django import editable_modulestore
from xmodule.contentstore.django import contentstore from xmodule.contentstore.django import contentstore
...@@ -51,6 +51,21 @@ def register_by_course_id(course_id, username='robot', password='test', is_staff ...@@ -51,6 +51,21 @@ def register_by_course_id(course_id, username='robot', password='test', is_staff
@world.absorb @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(): def clear_courses():
# Flush and initialize the module store # Flush and initialize the module store
# Note that if your test module gets in some weird state # Note that if your test module gets in some weird state
......
...@@ -11,7 +11,6 @@ ...@@ -11,7 +11,6 @@
# Disable the "unused argument" warning because lettuce uses "step" # Disable the "unused argument" warning because lettuce uses "step"
#pylint: disable=W0613 #pylint: disable=W0613
import re
from lettuce import world, step from lettuce import world, step
from .course_helpers import * from .course_helpers import *
from .ui_helpers import * from .ui_helpers import *
...@@ -23,32 +22,15 @@ logger = getLogger(__name__) ...@@ -23,32 +22,15 @@ logger = getLogger(__name__)
@step(r'I wait (?:for )?"(\d+\.?\d*)" seconds?$') @step(r'I wait (?:for )?"(\d+\.?\d*)" seconds?$')
def wait(step, seconds): def wait_for_seconds(step, seconds):
world.wait(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$') @step('I reload the page$')
def reload_the_page(step): def reload_the_page(step):
world.wait_for_ajax_complete() world.wait_for_ajax_complete()
world.browser.reload() world.browser.reload()
requirements = None world.wait_for_js_to_load()
for test, req in REQUIREJS_WAIT.items():
if test.search(world.browser.url):
requirements = req
break
world.wait_for_requirejs(requirements)
@step('I press the browser back button$') @step('I press the browser back button$')
...@@ -163,9 +145,9 @@ def should_see_in_the_page(step, doesnt_appear, text): ...@@ -163,9 +145,9 @@ def should_see_in_the_page(step, doesnt_appear, text):
else: else:
multiplier = 1 multiplier = 1
if doesnt_appear: 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: 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$') @step('I am logged in$')
......
...@@ -4,11 +4,13 @@ ...@@ -4,11 +4,13 @@
from lettuce import world from lettuce import world
import time import time
import json import json
import re
import platform import platform
from textwrap import dedent from textwrap import dedent
from urllib import quote_plus from urllib import quote_plus
from selenium.common.exceptions import ( from selenium.common.exceptions import (
WebDriverException, TimeoutException, StaleElementReferenceException) WebDriverException, TimeoutException,
StaleElementReferenceException)
from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support.ui import WebDriverWait
...@@ -16,11 +18,50 @@ from lettuce.django import django_url ...@@ -16,11 +18,50 @@ from lettuce.django import django_url
from nose.tools import assert_true # pylint: disable=E0611 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 @world.absorb
def wait(seconds): def wait(seconds):
time.sleep(float(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 # Selenium's `execute_async_script` function pauses Selenium's execution
# until the browser calls a specific Javascript callback; in effect, # until the browser calls a specific Javascript callback; in effect,
# Selenium goes to sleep until the JS callback function wakes it back up again. # Selenium goes to sleep until the JS callback function wakes it back up again.
...@@ -28,8 +69,6 @@ def wait(seconds): ...@@ -28,8 +69,6 @@ def wait(seconds):
# passed to this callback get returned from the `execute_async_script` # passed to this callback get returned from the `execute_async_script`
# function, which allows the JS to communicate information back to Python. # 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 # Ref: https://selenium.googlecode.com/svn/trunk/docs/api/dotnet/html/M_OpenQA_Selenium_IJavaScriptExecutor_ExecuteAsyncScript.htm
@world.absorb @world.absorb
def wait_for_js_variable_truthy(variable): def wait_for_js_variable_truthy(variable):
""" """
...@@ -37,7 +76,7 @@ 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 environment until the given variable is defined and truthy. This process
guards against page reloads, and seamlessly retries on the next page. guards against page reloads, and seamlessly retries on the next page.
""" """
js = """ javascript = """
var callback = arguments[arguments.length - 1]; var callback = arguments[arguments.length - 1];
var unloadHandler = function() {{ var unloadHandler = function() {{
callback("unload"); callback("unload");
...@@ -56,7 +95,13 @@ def wait_for_js_variable_truthy(variable): ...@@ -56,7 +95,13 @@ def wait_for_js_variable_truthy(variable):
}}, 10); }}, 10);
""".format(variable=variable) """.format(variable=variable)
for _ in range(5): # 5 attempts max 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": if result == "unload":
# we ran this on the wrong page. Wait a bit, and try again, when the # we ran this on the wrong page. Wait a bit, and try again, when the
# browser has loaded the next page. # browser has loaded the next page.
...@@ -105,7 +150,7 @@ def wait_for_requirejs(dependencies=None): ...@@ -105,7 +150,7 @@ def wait_for_requirejs(dependencies=None):
if dependencies[0] != "jquery": if dependencies[0] != "jquery":
dependencies.insert(0, "jquery") dependencies.insert(0, "jquery")
js = """ javascript = """
var callback = arguments[arguments.length - 1]; var callback = arguments[arguments.length - 1];
if(window.require) {{ if(window.require) {{
requirejs.onError = callback; requirejs.onError = callback;
...@@ -126,19 +171,33 @@ def wait_for_requirejs(dependencies=None): ...@@ -126,19 +171,33 @@ def wait_for_requirejs(dependencies=None):
}} }}
""".format(deps=json.dumps(dependencies)) """.format(deps=json.dumps(dependencies))
for _ in range(5): # 5 attempts max 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": if result == "unload":
# we ran this on the wrong page. Wait a bit, and try again, when the # we ran this on the wrong page. Wait a bit, and try again, when the
# browser has loaded the next page. # browser has loaded the next page.
world.wait(1) world.wait(1)
continue continue
elif result not in (None, True, False): elif result not in (None, True, False):
# we got a require.js error # We got a require.js error
msg = "Error loading dependencies: type={0} modules={1}".format( # Sometimes requireJS will throw an error with requireType=require
result['requireType'], result['requireModules']) # This doesn't seem to cause problems on the page, so we ignore it
err = RequireJSError(msg) if result['requireType'] == 'require':
err.error = result world.wait(1)
raise err 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: else:
return result return result
...@@ -153,7 +212,7 @@ def wait_for_ajax_complete(): ...@@ -153,7 +212,7 @@ def wait_for_ajax_complete():
keeps track of this information, go here: keeps track of this information, go here:
http://stackoverflow.com/questions/3148225/jquery-active-function#3148506 http://stackoverflow.com/questions/3148225/jquery-active-function#3148506
""" """
js = """ javascript = """
var callback = arguments[arguments.length - 1]; var callback = arguments[arguments.length - 1];
if(!window.jQuery) {callback(false);} if(!window.jQuery) {callback(false);}
var intervalID = setInterval(function() { var intervalID = setInterval(function() {
...@@ -163,13 +222,13 @@ def wait_for_ajax_complete(): ...@@ -163,13 +222,13 @@ def wait_for_ajax_complete():
} }
}, 100); }, 100);
""" """
world.browser.driver.execute_async_script(dedent(js)) world.browser.driver.execute_async_script(dedent(javascript))
@world.absorb @world.absorb
def visit(url): def visit(url):
world.browser.visit(django_url(url)) world.browser.visit(django_url(url))
wait_for_requirejs() wait_for_js_to_load()
@world.absorb @world.absorb
...@@ -238,11 +297,11 @@ def css_has_value(css_selector, value, index=0): ...@@ -238,11 +297,11 @@ def css_has_value(css_selector, value, index=0):
@world.absorb @world.absorb
def wait_for(func, timeout=5): def wait_for(func, timeout=5):
WebDriverWait( WebDriverWait(
driver=world.browser.driver, driver=world.browser.driver,
timeout=timeout, timeout=timeout,
ignored_exceptions=(StaleElementReferenceException) ignored_exceptions=(StaleElementReferenceException)
).until(func) ).until(func)
@world.absorb @world.absorb
...@@ -336,17 +395,15 @@ def css_click(css_selector, index=0, wait_time=30): ...@@ -336,17 +395,15 @@ def css_click(css_selector, index=0, wait_time=30):
This method will return True if the click worked. This method will return True if the click worked.
""" """
wait_for_clickable(css_selector, timeout=wait_time) wait_for_clickable(css_selector, timeout=wait_time)
assert_true(world.css_find(css_selector)[index].visible, assert_true(
msg="Element {}[{}] is present but not visible".format(css_selector, index)) 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 result = retry_on_exception(lambda: world.css_find(css_selector)[index].click())
# another element might be on top of it. In this case, try if result:
# clicking in the upper left corner. wait_for_js_to_load()
try: return result
return retry_on_exception(lambda: world.css_find(css_selector)[index].click())
except WebDriverException:
return css_click_at(css_selector, index=index)
@world.absorb @world.absorb
...@@ -362,22 +419,6 @@ def css_check(css_selector, index=0, wait_time=30): ...@@ -362,22 +419,6 @@ def css_check(css_selector, index=0, wait_time=30):
@world.absorb @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): def select_option(name, value, index=0, wait_time=30):
''' '''
A method to select an option A method to select an option
...@@ -406,6 +447,7 @@ def css_fill(css_selector, text, index=0): ...@@ -406,6 +447,7 @@ def css_fill(css_selector, text, index=0):
@world.absorb @world.absorb
def click_link(partial_text, index=0): def click_link(partial_text, index=0):
retry_on_exception(lambda: world.browser.find_link_by_partial_text(partial_text)[index].click()) retry_on_exception(lambda: world.browser.find_link_by_partial_text(partial_text)[index].click())
wait_for_js_to_load()
@world.absorb @world.absorb
...@@ -512,7 +554,6 @@ def retry_on_exception(func, max_attempts=5, ignored_exceptions=StaleElementRefe ...@@ -512,7 +554,6 @@ def retry_on_exception(func, max_attempts=5, ignored_exceptions=StaleElementRefe
while attempt < max_attempts: while attempt < max_attempts:
try: try:
return func() return func()
break
except ignored_exceptions: except ignored_exceptions:
world.wait(1) world.wait(1)
attempt += 1 attempt += 1
......
...@@ -11,7 +11,7 @@ from pymongo.errors import PyMongoError ...@@ -11,7 +11,7 @@ from pymongo.errors import PyMongoError
from track.backends import BaseBackend from track.backends import BaseBackend
log = logging.getLogger('track.backends.mongodb') log = logging.getLogger(__name__)
class MongoBackend(BaseBackend): class MongoBackend(BaseBackend):
...@@ -64,14 +64,17 @@ class MongoBackend(BaseBackend): ...@@ -64,14 +64,17 @@ class MongoBackend(BaseBackend):
**extra **extra
) )
self.collection = self.connection[db_name][collection_name] database = self.connection[db_name]
if user or password: if user or password:
self.collection.database.authenticate(user, password) database.authenticate(user, password)
self.collection = database[collection_name]
self._create_indexes() self._create_indexes()
def _create_indexes(self): def _create_indexes(self):
"""Ensures the proper fields are indexed"""
# WARNING: The collection will be locked during the index # WARNING: The collection will be locked during the index
# creation. If the collection has a large number of # creation. If the collection has a large number of
# documents in it, the operation can take a long time. # documents in it, the operation can take a long time.
...@@ -83,8 +86,12 @@ class MongoBackend(BaseBackend): ...@@ -83,8 +86,12 @@ class MongoBackend(BaseBackend):
self.collection.ensure_index('event_type') self.collection.ensure_index('event_type')
def send(self, event): def send(self, event):
"""Insert the event in to the Mongo collection"""
try: try:
self.collection.insert(event, manipulate=False) self.collection.insert(event, manipulate=False)
except PyMongoError: 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' msg = 'Error inserting to MongoDB event tracker backend'
log.exception(msg) log.exception(msg)
...@@ -134,7 +134,7 @@ def add_histogram(user, block, view, frag, context): # pylint: disable=unused-a ...@@ -134,7 +134,7 @@ def add_histogram(user, block, view, frag, context): # pylint: disable=unused-a
return frag return frag
block_id = block.id block_id = block.id
if block.descriptor.has_score: if block.has_score:
histogram = grade_histogram(block_id) histogram = grade_histogram(block_id)
render_histogram = len(histogram) > 0 render_histogram = len(histogram) > 0
else: else:
...@@ -142,7 +142,7 @@ def add_histogram(user, block, view, frag, context): # pylint: disable=unused-a ...@@ -142,7 +142,7 @@ def add_histogram(user, block, view, frag, context): # pylint: disable=unused-a
render_histogram = False render_histogram = False
if settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'): 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 osfs = block.system.filestore
if filename is not None and osfs.exists(filename): if filename is not None and osfs.exists(filename):
# if original, unmangled filename exists then use it (github # 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 ...@@ -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 # TODO (ichuang): use _has_access_descriptor.can_load in lms.courseware.access, instead of now>mstart comparison here
now = datetime.datetime.now(UTC()) now = datetime.datetime.now(UTC())
is_released = "unknown" is_released = "unknown"
mstart = block.descriptor.start mstart = block.start
if mstart is not None: if mstart is not None:
is_released = "<font color='red'>Yes!</font>" if (now > mstart) else "<font color='green'>Not yet</font>" 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()], 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, 'location': block.location,
'xqa_key': block.xqa_key, 'xqa_key': block.xqa_key,
'source_file': source_file, 'source_file': source_file,
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
<div class="drag_and_drop_problem_json" id="drag_and_drop_json_${id}" <div class="drag_and_drop_problem_json" id="drag_and_drop_json_${id}"
style="display:none;">${drag_and_drop_json}</div> 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': % if status == 'unsubmitted':
<div class="unanswered" id="status_${id}"> <div class="unanswered" id="status_${id}">
......
...@@ -951,7 +951,7 @@ class DragAndDropTest(unittest.TestCase): ...@@ -951,7 +951,7 @@ class DragAndDropTest(unittest.TestCase):
''' '''
def test_rendering(self): def test_rendering(self):
path_to_images = '/static/images/' path_to_images = '/dummy-static/images/'
xml_str = """ xml_str = """
<drag_and_drop_input id="prob_1_2" img="{path}about_1.png" target_outline="false"> <drag_and_drop_input id="prob_1_2" img="{path}about_1.png" target_outline="false">
...@@ -978,15 +978,15 @@ class DragAndDropTest(unittest.TestCase): ...@@ -978,15 +978,15 @@ class DragAndDropTest(unittest.TestCase):
user_input = { # order matters, for string comparison user_input = { # order matters, for string comparison
"target_outline": "false", "target_outline": "false",
"base_image": "/static/images/about_1.png", "base_image": "/dummy-static/images/about_1.png",
"draggables": [ "draggables": [
{"can_reuse": "", "label": "Label 1", "id": "1", "icon": "", "target_fields": []}, {"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": "cc", "id": "name_with_icon", "icon": "/dummy-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": "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": "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": "Mute", "id": "2", "icon": "/dummy-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": "spinner", "id": "name_label_icon3", "icon": "/dummy-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": "Star", "id": "name4", "icon": "/dummy-static/images/volume.png", "can_reuse": "", "target_fields": []},
{"can_reuse": "", "label": "Label3", "id": "7", "icon": "", "can_reuse": "", "target_fields": []}], {"can_reuse": "", "label": "Label3", "id": "7", "icon": "", "can_reuse": "", "target_fields": []}],
"one_per_target": "True", "one_per_target": "True",
"targets": [ "targets": [
......
...@@ -15,7 +15,7 @@ from capa.responsetypes import StudentInputError, \ ...@@ -15,7 +15,7 @@ from capa.responsetypes import StudentInputError, \
ResponseError, LoncapaProblemError ResponseError, LoncapaProblemError
from capa.util import convert_files_to_filenames from capa.util import convert_files_to_filenames
from .progress import Progress 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.raw_module import RawDescriptor
from xmodule.exceptions import NotFoundError, ProcessingError from xmodule.exceptions import NotFoundError, ProcessingError
from xblock.fields import Scope, String, Boolean, Dict, Integer, Float from xblock.fields import Scope, String, Boolean, Dict, Integer, Float
...@@ -884,6 +884,8 @@ class CapaModule(CapaFields, XModule): ...@@ -884,6 +884,8 @@ class CapaModule(CapaFields, XModule):
'max_value': score['total'], 'max_value': score['total'],
}) })
return {'grade': score['score'], 'max_grade': score['total']}
def check_problem(self, data): def check_problem(self, data):
""" """
Checks whether answers to a problem are correct Checks whether answers to a problem are correct
...@@ -951,7 +953,7 @@ class CapaModule(CapaFields, XModule): ...@@ -951,7 +953,7 @@ class CapaModule(CapaFields, XModule):
return {'success': msg} return {'success': msg}
raise raise
self.publish_grade() published_grade = self.publish_grade()
# success = correct if ALL questions in this problem are correct # success = correct if ALL questions in this problem are correct
success = 'correct' success = 'correct'
...@@ -961,6 +963,8 @@ class CapaModule(CapaFields, XModule): ...@@ -961,6 +963,8 @@ class CapaModule(CapaFields, XModule):
# NOTE: We are logging both full grading and queued-grading submissions. In the latter, # NOTE: We are logging both full grading and queued-grading submissions. In the latter,
# 'success' will always be incorrect # '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['correct_map'] = correct_map.get_dict()
event_info['success'] = success event_info['success'] = success
event_info['attempts'] = self.attempts event_info['attempts'] = self.attempts
...@@ -1193,3 +1197,33 @@ class CapaDescriptor(CapaFields, RawDescriptor): ...@@ -1193,3 +1197,33 @@ class CapaDescriptor(CapaFields, RawDescriptor):
CapaDescriptor.force_save_button, CapaDescriptor.markdown, CapaDescriptor.force_save_button, CapaDescriptor.markdown,
CapaDescriptor.text_customization]) CapaDescriptor.text_customization])
return non_editable_fields 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): ...@@ -496,7 +496,7 @@ class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor):
metadata_translations = { metadata_translations = {
'is_graded': 'graded', 'is_graded': 'graded',
'attempts': 'max_attempts', 'attempts': 'max_attempts',
} }
def get_context(self): def get_context(self):
_context = RawDescriptor.get_context(self) _context = RawDescriptor.get_context(self)
......
...@@ -18,6 +18,7 @@ log = logging.getLogger('mitx.' + __name__) ...@@ -18,6 +18,7 @@ log = logging.getLogger('mitx.' + __name__)
class ConditionalFields(object): class ConditionalFields(object):
has_children = True
show_tag_list = List(help="Poll answers", scope=Scope.content) show_tag_list = List(help="Poll answers", scope=Scope.content)
...@@ -148,7 +149,7 @@ class ConditionalModule(ConditionalFields, XModule): ...@@ -148,7 +149,7 @@ class ConditionalModule(ConditionalFields, XModule):
context) context)
return json.dumps({'html': [html], 'message': bool(message)}) 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}) return json.dumps({'html': html})
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment