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>
Nick Parlante <nick.parlante@cs.stanford.edu>
Marko Seric <marko.seric@math.uzh.ch>
Felipe Montoya <felipe.montoya@edunext.co>
Julia Hansbrough <julia@edx.org>
......@@ -5,8 +5,21 @@ These are notable changes in edx-platform. This is a rolling list of changes,
in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected.
Blades: Hovering over CC button in video player, when transcripts are hidden,
will cause them to show up. Moving the mouse from the CC button will auto hide
them. You can hover over the CC button and then move the mouse to the
transcripts which will allow you to select some video position in 1 click.
Blades: Add possibility to use multiple LTI tools per page.
Blades: LTI module can now load external content in a new window.
LMS: Disable data download buttons on the instructor dashboard for large courses
LMS: Ported bulk emailing to the beta instructor dashboard.
LMS: Add monitoring of bulk email subtasks to display progress on instructor dash.
LMS: Refactor and clean student dashboard templates.
LMS: Fix issue with CourseMode expiration dates
......@@ -22,6 +35,8 @@ Studio: Switched to loading Javascript using require.js
Studio: Better feedback during the course import process
Studio: Improve drag and drop on the course overview and subsection views.
LMS: Add split testing functionality for internal use.
CMS: Add edit_course_tabs management command, providing a primitive
......@@ -73,6 +88,11 @@ Common: Allow instructors to input complicated expressions as answers to
`NumericalResponse`s. Prior to the change only numbers were allowed, now any
answer from '1/3' to 'sqrt(12)*(1-1/3^2+1/5/3^2)' are valid.
Studio/LMS: Allow for 'preview' and 'published' in a single LMS instance. Use
middlware components to retain the incoming Django request and put in thread
local storage. It is recommended that all developers define a 'preview.localhost'
which maps to the same IP address as localhost in his/her HOSTS file.
LMS: Enable beta instructor dashboard. The beta dashboard is a rearchitecture
of the existing instructor dashboard and is available by clicking a link at
the top right of the existing dashboard.
......
......@@ -2,7 +2,7 @@
# pylint: disable=W0621
from lettuce import world, step
from nose.tools import assert_true, assert_equal, assert_in, assert_false # pylint: disable=E0611
from nose.tools import assert_true, assert_in, assert_false # pylint: disable=E0611
from auth.authz import get_user_by_email, get_course_groupname_for_role
from django.conf import settings
......@@ -224,14 +224,50 @@ def i_enabled_the_advanced_module(step, module):
press_the_notification_button(step, 'Save')
@step('I have clicked the new unit button')
def open_new_unit(step):
step.given('I have opened a new course section in Studio')
step.given('I have added a new subsection')
step.given('I expand the first section')
old_url = world.browser.url
world.css_click('a.new-unit-item')
world.wait_for(lambda x: world.browser.url != old_url)
@world.absorb
def create_course_with_unit():
"""
Prepare for tests by creating a course with a section, subsection, and unit.
Performs the following:
Clear out all courseware
Create a course with a section, subsection, and unit
Create a user and make that user a course author
Log the user into studio
Open the course from the dashboard
Expand the section and click on the New Unit link
The end result is the page where the user is editing the new unit
"""
world.clear_courses()
course = world.CourseFactory.create()
world.scenario_dict['COURSE'] = course
section = world.ItemFactory.create(parent_location=course.location)
world.ItemFactory.create(
parent_location=section.location,
category='sequential',
display_name='Subsection One',
)
user = create_studio_user(is_staff=False)
add_course_author(user, course)
log_into_studio()
world.css_click('a.course-link')
css_selectors = [
'div.section-item a.expand-collapse-icon', 'a.new-unit-item'
]
for selector in css_selectors:
world.css_click(selector)
world.wait_for_mathjax()
world.wait_for_xmodule()
assert world.is_css_present('ul.new-component-type')
@step('I have clicked the new unit button$')
@step(u'I am in Studio editing a new unit$')
def edit_new_unit(step):
create_course_with_unit()
@step('the save notification button is disabled')
......@@ -267,9 +303,9 @@ def confirm_the_prompt(step):
assert_false(world.css_find(btn_css).visible)
@step(u'I am shown a (.*)$')
def i_am_shown_a_notification(step, notification_type):
assert world.is_css_present('.wrapper-%s' % notification_type)
@step(u'I am shown a prompt$')
def i_am_shown_a_notification(step):
assert world.is_css_present('.wrapper-prompt')
def type_in_codemirror(index, text):
......
......@@ -80,9 +80,3 @@ Feature: CMS.Component Adding
And I add a "Blank Advanced Problem" "Advanced Problem" component
And I delete all components
Then I see no components
Scenario: I see a notification on save
Given I am in Studio editing a new unit
And I add a "Discussion" "single step" component
And I edit and save a component
Then I am shown a notification
......@@ -2,43 +2,19 @@
#pylint: disable=W0621
from lettuce import world, step
from nose.tools import assert_true, assert_in, assert_equal # pylint: disable=E0611
from common import create_studio_user, add_course_author, log_into_studio
@step(u'I am in Studio editing a new unit$')
def add_unit(step):
world.clear_courses()
course = world.CourseFactory.create()
section = world.ItemFactory.create(parent_location=course.location)
world.ItemFactory.create(
parent_location=section.location,
category='sequential',
display_name='Subsection One',)
user = create_studio_user(is_staff=False)
add_course_author(user, course)
log_into_studio()
world.wait_for_requirejs([
"jquery", "js/models/course", "coffee/src/models/module",
"coffee/src/views/unit", "jquery.ui",
])
world.wait_for_mathjax()
css_selectors = [
'a.course-link', 'div.section-item a.expand-collapse-icon',
'a.new-unit-item',
]
for selector in css_selectors:
world.css_click(selector)
from nose.tools import assert_true, assert_in # pylint: disable=E0611
@step(u'I add this type of single step component:$')
def add_a_single_step_component(step):
world.wait_for_xmodule()
for step_hash in step.hashes:
component = step_hash['Component']
assert_in(component, ['Discussion', 'Video'])
css_selector = 'a[data-type="{}"]'.format(component.lower())
world.css_click(css_selector)
world.create_component_instance(
step=step,
category='{}'.format(component.lower()),
)
@step(u'I see this type of single step component:$')
......@@ -53,51 +29,24 @@ def see_a_single_step_component(step):
@step(u'I add this type of( Advanced)? (HTML|Problem) component:$')
def add_a_multi_step_component(step, is_advanced, category):
def click_advanced():
css = 'ul.problem-type-tabs a[href="#tab2"]'
world.css_click(css)
my_css = 'ul.problem-type-tabs li.ui-state-active a[href="#tab2"]'
assert(world.css_find(my_css))
def find_matching_link():
"""
Find the link with the specified text. There should be one and only one.
"""
# The tab shows links for the given category
links = world.css_find('div.new-component-{} a'.format(category))
# Find the link whose text matches what you're looking for
matched_links = [link for link in links if link.text == step_hash['Component']]
# There should be one and only one
assert_equal(len(matched_links), 1)
return matched_links[0]
def click_link():
link.click()
world.wait_for_xmodule()
category = category.lower()
for step_hash in step.hashes:
css_selector = 'a[data-type="{}"]'.format(category)
world.css_click(css_selector)
world.wait_for_invisible(css_selector)
if is_advanced:
# Sometimes this click does not work if you go too fast.
world.retry_on_exception(click_advanced, max_attempts=5, ignored_exceptions=AssertionError)
# Retry this in case the list is empty because you tried too fast.
link = world.retry_on_exception(func=find_matching_link, ignored_exceptions=AssertionError)
# Wait for the link to be clickable. If you go too fast it is not.
world.retry_on_exception(click_link)
world.create_component_instance(
step=step,
category='{}'.format(category.lower()),
component_type=step_hash['Component'],
is_advanced=bool(is_advanced),
)
@step(u'I see (HTML|Problem) components in this order:')
def see_a_multi_step_component(step, category):
components = world.css_find('li.component section.xmodule_display')
# Wait for all components to finish rendering
selector = 'li.component section.xmodule_display'
world.wait_for(lambda _: len(world.css_find(selector)) == len(step.hashes))
for idx, step_hash in enumerate(step.hashes):
if category == 'HTML':
html_matcher = {
'Text':
......@@ -107,9 +56,11 @@ def see_a_multi_step_component(step, category):
'E-text Written in LaTeX':
'<h2>Example: E-text page</h2>',
}
assert_in(html_matcher[step_hash['Component']], components[idx].html)
actual_html = world.css_html(selector, index=idx)
assert_in(html_matcher[step_hash['Component']], actual_html)
else:
assert_in(step_hash['Component'].upper(), components[idx].text)
actual_text = world.css_text(selector, index=idx)
assert_in(step_hash['Component'].upper(), actual_text)
@step(u'I add a "([^"]*)" "([^"]*)" component$')
......
......@@ -2,30 +2,35 @@
#pylint: disable=C0111
from lettuce import world
from nose.tools import assert_equal, assert_true # pylint: disable=E0611
from nose.tools import assert_equal, assert_true, assert_in # pylint: disable=E0611
from terrain.steps import reload_the_page
@world.absorb
def create_component_instance(step, component_button_css, category,
expected_css, boilerplate=None,
has_multiple_templates=True):
click_new_component_button(step, component_button_css)
if category in ('problem', 'html'):
def create_component_instance(step, category, component_type=None, is_advanced=False):
"""
Create a new component in a Unit.
def animation_done(_driver):
script = "$('div.new-component').css('display')"
return world.browser.evaluate_script(script) == 'none'
Parameters
----------
category: component type (discussion, html, problem, video)
component_type: for components with multiple templates, the link text in the menu
is_advanced: for html and problem, is the desired component under the
advanced menu
"""
assert_in(category, ['problem', 'html', 'video', 'discussion'])
world.wait_for(animation_done)
component_button_css = '.large-{}-icon'.format(category.lower())
world.css_click(component_button_css)
if has_multiple_templates:
click_component_from_menu(category, boilerplate, expected_css)
if category in ('problem', 'html'):
world.wait_for_invisible(component_button_css)
click_component_from_menu(category, component_type, is_advanced)
if category in ('video',):
world.wait_for_xmodule()
if category == 'problem':
expected_css = 'section.xmodule_CapaModule'
else:
expected_css = 'section.xmodule_{}Module'.format(category.title())
assert_true(world.is_css_present(expected_css))
......@@ -33,29 +38,53 @@ def create_component_instance(step, component_button_css, category,
@world.absorb
def click_new_component_button(step, component_button_css):
step.given('I have clicked the new unit button')
world.wait_for_requirejs(
["jquery", "js/models/course", "coffee/src/models/module",
"coffee/src/views/unit", "jquery.ui"]
)
world.css_click(component_button_css)
@world.absorb
def click_component_from_menu(category, boilerplate, expected_css):
def _click_advanced():
css = 'ul.problem-type-tabs a[href="#tab2"]'
world.css_click(css)
my_css = 'ul.problem-type-tabs li.ui-state-active a[href="#tab2"]'
assert(world.css_find(my_css))
def _find_matching_link(category, component_type):
"""
Creates a component from `instance_id`. For components with more
than one template, clicks on `elem_css` to create the new
component. Components with only one template are created as soon
as the user clicks the appropriate button, so we assert that the
expected component is present.
Find the link with the specified text. There should be one and only one.
"""
if boilerplate:
elem_css = "a[data-category='{}'][data-boilerplate='{}']".format(category, boilerplate)
else:
elem_css = "a[data-category='{}']:not([data-boilerplate])".format(category)
elements = world.css_find(elem_css)
assert_equal(len(elements), 1)
world.css_click(elem_css)
# The tab shows links for the given category
links = world.css_find('div.new-component-{} a'.format(category))
# Find the link whose text matches what you're looking for
matched_links = [link for link in links if link.text == component_type]
# There should be one and only one
assert_equal(len(matched_links), 1)
return matched_links[0]
def click_component_from_menu(category, component_type, is_advanced):
"""
Creates a component for a category with more
than one template, i.e. HTML and Problem.
For some problem types, it is necessary to click to
the Advanced tab.
The component_type is the link text, e.g. "Blank Common Problem"
"""
if is_advanced:
# Sometimes this click does not work if you go too fast.
world.retry_on_exception(_click_advanced,
ignored_exceptions=AssertionError)
# Retry this in case the list is empty because you tried too fast.
link = world.retry_on_exception(
lambda: _find_matching_link(category, component_type),
ignored_exceptions=AssertionError
)
# Wait for the link to be clickable. If you go too fast it is not.
world.retry_on_exception(lambda: link.click())
@world.absorb
......
......@@ -58,20 +58,3 @@ Feature: CMS.Course Overview
And I click the "Expand All Sections" link
Then I see the "Collapse All Sections" link
And all sections are expanded
Scenario: Notification is shown on grading status changes
Given I have a course with 1 section
When I navigate to the course overview page
And I change an assignment's grading status
Then I am shown a notification
# Notification is not shown on reorder for IE
# Safari does not have moveMouseTo implemented
@skip_internetexplorer
@skip_safari
Scenario: Notification is shown on subsection reorder
Given I have opened a new course section in Studio
And I have added a new subsection
And I have added a new subsection
When I reorder subsections
Then I am shown a notification
......@@ -91,8 +91,7 @@ def i_expand_a_section(step):
@step(u'I see the "([^"]*)" link$')
def i_see_the_span_with_text(step, text):
span_locator = '.toggle-button-sections span'
assert_true(world.is_css_present(span_locator))
assert_equal(world.css_value(span_locator), text)
assert_true(world.css_has_value(span_locator, text))
assert_true(world.css_visible(span_locator))
......@@ -128,10 +127,10 @@ def change_grading_status(step):
@step(u'I reorder subsections')
def reorder_subsections(_step):
draggable_css = 'a.drag-handle'
draggable_css = '.subsection-drag-handle'
ele = world.css_find(draggable_css).first
ele.action_chains.drag_and_drop_by_offset(
ele._element,
30,
0
0,
25
).perform()
......@@ -151,9 +151,10 @@ def i_see_new_course_image(_step):
assert len(images) == 1
img = images[0]
expected_src = '/c4x/MITx/999/asset/image.jpg'
# Don't worry about the domain in the URL
assert img['src'].endswith(expected_src), "Was looking for {expected}, found {actual}".format(
expected=expected_src, actual=img['src'])
success_func = lambda _: img['src'].endswith(expected_src)
world.wait_for(success_func)
@step('the image URL should be present in the field')
......
......@@ -50,8 +50,8 @@ def other_delete_self(_step):
@step(u'I make "([^"]*)" a course team admin')
def make_course_team_admin(_step, name):
admin_btn_css = '.user-item[data-email="{email}"] .user-actions .add-admin-role'.format(
email=name+'@edx.org')
admin_btn_css = '.user-item[data-email="{name}@edx.org"] .user-actions .add-admin-role'.format(
name=name)
world.css_click(admin_btn_css)
......@@ -80,8 +80,8 @@ def see_course(_step, do_not_see, gender='self'):
@step(u'"([^"]*)" should( not)? be marked as an admin')
def marked_as_admin(_step, name, not_marked_admin):
flag_css = '.user-item[data-email="{email}"] .flag-role.flag-role-admin'.format(
email=name+'@edx.org')
flag_css = '.user-item[data-email="{name}@edx.org"] .flag-role.flag-role-admin'.format(
name=name)
if not_marked_admin:
assert world.is_css_not_present(flag_css)
else:
......
import os
from lettuce import world
from django.conf import settings
def import_file(filename):
world.browser.execute_script("$('input.file-input').css('display', 'block')")
path = os.path.join(settings.COMMON_TEST_DATA_ROOT, "imports", filename)
world.browser.attach_file('course-data', os.path.abspath(path))
world.css_click('input.submit-button')
# Go to course outline
world.click_course_content()
outline_css = 'li.nav-course-courseware-outline a'
world.css_click(outline_css)
def go_to_import():
menu_css = 'li.nav-course-tools'
import_css = 'li.nav-course-tools-import a'
world.css_click(menu_css)
world.css_click(import_css)
......@@ -2,7 +2,7 @@
Feature: CMS.Discussion Component Editor
As a course author, I want to be able to create discussion components.
Scenario: User can view metadata
Scenario: User can view discussion component metadata
Given I have created a Discussion Tag
And I edit and select Settings
Then I see three alphabetized settings and their expected values
......@@ -14,7 +14,3 @@ Feature: CMS.Discussion Component Editor
And I edit and select Settings
Then I can modify the display name
And my display name change is persisted on save
Scenario: Creating a discussion takes a single click
Given I have clicked the new unit button
Then creating a discussion takes a single click
......@@ -6,11 +6,10 @@ from lettuce import world, step
@step('I have created a Discussion Tag$')
def i_created_discussion_tag(step):
world.create_course_with_unit()
world.create_component_instance(
step, '.large-discussion-icon',
'discussion',
'.xmodule_DiscussionModule',
has_multiple_templates=False
step=step,
category='discussion',
)
......@@ -22,12 +21,3 @@ def i_see_only_the_settings_and_values(step):
['Display Name', "Discussion", False],
['Subcategory', "Topic-Level Student-Visible Label", False]
])
@step('creating a discussion takes a single click')
def discussion_takes_a_single_click(step):
component_css = '.xmodule_DiscussionModule'
assert world.is_css_not_present(component_css)
world.css_click("a[data-category='discussion']")
assert world.is_css_present(component_css)
......@@ -59,6 +59,17 @@ Feature: CMS.Course Grading
And I go back to the main course page
Then I do see the assignment name "New Type"
# Note that "7" is a special weight because it revealed rounding errors (STUD-826).
Scenario: Users can set weight to Assignment types
Given I have opened a new course in Studio
And I am viewing the grading settings
When I add a new assignment type "New Type"
And I set the assignment weight to "7"
And I press the "Save" notification button
Then the assignment weight is displayed as "7"
And I reload the page
Then the assignment weight is displayed as "7"
Scenario: Settings are only persisted when saved
Given I have opened a new course in Studio
And I have populated the course
......
......@@ -106,6 +106,22 @@ def add_assignment_type(step, new_name):
new_assignment._element.send_keys(new_name)
@step(u'I set the assignment weight to "([^"]*)"$')
def set_weight(step, weight):
weight_id = '#course-grading-assignment-gradeweight'
weight_field = world.css_find(weight_id)[-1]
old_weight = world.css_value(weight_id, -1)
for count in range(len(old_weight)):
weight_field._element.send_keys(Keys.END, Keys.BACK_SPACE)
weight_field._element.send_keys(weight)
@step(u'the assignment weight is displayed as "([^"]*)"$')
def verify_weight(step, weight):
weight_id = '#course-grading-assignment-gradeweight'
assert_equal(world.css_value(weight_id, -1), weight)
@step(u'I have populated the course')
def populate_course(step):
step.given('I have added a new section')
......@@ -164,7 +180,7 @@ def cannot_edit_fail(_step):
def i_change_grace_period(_step, grace_period):
grace_period_css = '#course-grading-graceperiod'
ele = world.css_find(grace_period_css).first
# Sometimes it takes a moment for the JavaScript
# to populate the field. If we don't wait for
# this to happen, then we can end up with
......
......@@ -6,9 +6,11 @@ from lettuce import world, step
@step('I have created a Blank HTML Page$')
def i_created_blank_html_page(step):
world.create_course_with_unit()
world.create_component_instance(
step, '.large-html-icon', 'html',
'.xmodule_HtmlModule'
step=step,
category='html',
component_type='Text'
)
......@@ -18,11 +20,10 @@ def i_see_only_the_html_display_name(step):
@step('I have created an E-text Written in LaTeX$')
def i_created_blank_html_page(step):
def i_created_etext_in_latex(step):
world.create_course_with_unit()
world.create_component_instance(
step,
'.large-html-icon',
'html',
'.xmodule_HtmlModule',
'latex_html.yaml'
step=step,
category='html',
component_type='E-text Written in LaTeX'
)
......@@ -89,3 +89,13 @@ Feature: CMS.Problem Editor
When I edit and compile the High Level Source
Then my change to the High Level Source is persisted
And when I view the High Level Source I see my changes
Scenario: Exceptions don't cause problem to be uneditable (bug STUD-786)
Given I have an empty course
And I go to the import page
And I import the file "get_html_exception_test.tar.gz"
When I go to the unit "Probability and BMI"
And I click on "edit a draft"
Then I see a message that says "We're having trouble rendering your component"
And I can edit the problem
# disable missing docstring
#pylint: disable=C0111
import json
from lettuce import world, step
from nose.tools import assert_equal, assert_true # pylint: disable=E0611
from common import type_in_codemirror
from common import type_in_codemirror, open_new_course
from course_import import import_file, go_to_import
DISPLAY_NAME = "Display Name"
MAXIMUM_ATTEMPTS = "Maximum Attempts"
......@@ -14,17 +17,16 @@ SHOW_ANSWER = "Show Answer"
@step('I have created a Blank Common Problem$')
def i_created_blank_common_problem(step):
world.create_course_with_unit()
world.create_component_instance(
step,
'.large-problem-icon',
'problem',
'.xmodule_CapaModule',
'blank_common.yaml'
step=step,
category='problem',
component_type='Blank Common Problem'
)
@step('I edit and select Settings$')
def i_edit_and_select_settings(step):
def i_edit_and_select_settings(_step):
world.edit_component_and_select_settings()
......@@ -41,7 +43,7 @@ def i_see_advanced_settings_with_values(step):
@step('I can modify the display name')
def i_can_modify_the_display_name(step):
def i_can_modify_the_display_name(_step):
# Verifying that the display name can be a string containing a floating point value
# (to confirm that we don't throw an error because it is of the wrong type).
index = world.get_setting_entry_index(DISPLAY_NAME)
......@@ -58,7 +60,7 @@ def my_display_name_change_is_persisted_on_save(step):
@step('I can specify special characters in the display name')
def i_can_modify_the_display_name_with_special_chars(step):
def i_can_modify_the_display_name_with_special_chars(_step):
index = world.get_setting_entry_index(DISPLAY_NAME)
world.css_fill('.wrapper-comp-setting .setting-input', "updated ' \" &", index=index)
if world.is_firefox():
......@@ -73,7 +75,7 @@ def special_chars_persisted_on_save(step):
@step('I can revert the display name to unset')
def can_revert_display_name_to_unset(step):
def can_revert_display_name_to_unset(_step):
world.revert_setting_entry(DISPLAY_NAME)
verify_unset_display_name()
......@@ -85,7 +87,7 @@ def my_display_name_is_persisted_on_save(step):
@step('I can select Per Student for Randomization')
def i_can_select_per_student_for_randomization(step):
def i_can_select_per_student_for_randomization(_step):
world.browser.select(RANDOMIZATION, "Per Student")
verify_modified_randomization()
......@@ -104,7 +106,7 @@ def i_can_revert_to_default_for_randomization(step):
@step('I can set the weight to "(.*)"?')
def i_can_set_weight(step, weight):
def i_can_set_weight(_step, weight):
set_weight(weight)
verify_modified_weight()
......@@ -164,25 +166,24 @@ def cancel_does_not_save_changes(step):
@step('I have created a LaTeX Problem')
def create_latex_problem(step):
world.click_new_component_button(step, '.large-problem-icon')
def animation_done(_driver):
return world.browser.evaluate_script("$('div.new-component').css('display')") == 'none'
world.wait_for(animation_done)
# Go to advanced tab.
world.css_click('#ui-id-2')
world.click_component_from_menu("problem", "latex_problem.yaml", '.xmodule_CapaModule')
world.create_course_with_unit()
world.create_component_instance(
step=step,
category='problem',
component_type='Problem Written in LaTeX',
is_advanced=True
)
@step('I edit and compile the High Level Source')
def edit_latex_source(step):
def edit_latex_source(_step):
open_high_level_source()
type_in_codemirror(1, "hi")
world.css_click('.hls-compile')
@step('my change to the High Level Source is persisted')
def high_level_source_persisted(step):
def high_level_source_persisted(_step):
def verify_text(driver):
css_sel = '.problem div>span'
return world.css_text(css_sel) == 'hi'
......@@ -191,11 +192,53 @@ def high_level_source_persisted(step):
@step('I view the High Level Source I see my changes')
def high_level_source_in_editor(step):
def high_level_source_in_editor(_step):
open_high_level_source()
assert_equal('hi', world.css_value('.source-edit-box'))
@step(u'I have an empty course')
def i_have_empty_course(step):
open_new_course()
@step(u'I go to the import page')
def i_go_to_import(_step):
go_to_import()
@step(u'I import the file "([^"]*)"$')
def i_import_the_file(_step, filename):
import_file(filename)
@step(u'I click on "edit a draft"$')
def i_edit_a_draft(_step):
world.css_click("a.create-draft")
@step(u'I go to the vertical "([^"]*)"$')
def i_go_to_vertical(_step, vertical):
world.css_click("span:contains('{0}')".format(vertical))
@step(u'I go to the unit "([^"]*)"$')
def i_go_to_unit(_step, unit):
loc = "window.location = $(\"span:contains('{0}')\").closest('a').attr('href')".format(unit)
world.browser.execute_script(loc)
@step(u'I see a message that says "([^"]*)"$')
def i_can_see_message(_step, msg):
msg = json.dumps(msg) # escape quotes
world.css_has_text("h2.title", msg)
@step(u'I can edit the problem$')
def i_can_edit_problem(_step):
world.edit_component()
def verify_high_level_source_links(step, visible):
if visible:
assert_true(world.is_css_present('.launch-latex-compiler'),
......
......@@ -5,8 +5,6 @@ from lettuce import world, step
from common import *
from nose.tools import assert_equal # pylint: disable=E0611
############### ACTIONS ####################
@step('I click the New Section link$')
def i_click_new_section_link(_step):
......@@ -53,9 +51,6 @@ def i_see_a_mini_notification(_step, _type):
assert world.is_css_present(saving_css)
############ ASSERTIONS ###################
@step('I see my section on the Courseware page$')
def i_see_my_section_on_the_courseware_page(_step):
see_my_section_on_the_courseware_page('My Section')
......@@ -125,8 +120,6 @@ def the_section_release_date_is_updated(_step):
assert_equal(status_text, 'Will Release: 12/25/2013 at 00:00 UTC')
############ HELPER METHODS ###################
def save_section_name(name):
name_css = '.new-section-name'
save_css = '.new-section-name-save'
......
......@@ -47,7 +47,7 @@ def name_textbook(_step, name):
@step(u'I name the (first|second|third) chapter "([^"]*)"')
def name_chapter(_step, ordinal, name):
index = ["first", "second", "third"].index(ordinal)
input_css = ".textbook .chapter{i} input.chapter-name".format(i=index+1)
input_css = ".textbook .chapter{i} input.chapter-name".format(i=index + 1)
world.css_fill(input_css, name)
if world.is_firefox():
world.trigger_event(input_css)
......@@ -56,7 +56,7 @@ def name_chapter(_step, ordinal, name):
@step(u'I type in "([^"]*)" for the (first|second|third) chapter asset')
def asset_chapter(_step, name, ordinal):
index = ["first", "second", "third"].index(ordinal)
input_css = ".textbook .chapter{i} input.chapter-asset-path".format(i=index+1)
input_css = ".textbook .chapter{i} input.chapter-asset-path".format(i=index + 1)
world.css_fill(input_css, name)
if world.is_firefox():
world.trigger_event(input_css)
......@@ -65,7 +65,7 @@ def asset_chapter(_step, name, ordinal):
@step(u'I click the Upload Asset link for the (first|second|third) chapter')
def click_upload_asset(_step, ordinal):
index = ["first", "second", "third"].index(ordinal)
button_css = ".textbook .chapter{i} .action-upload".format(i=index+1)
button_css = ".textbook .chapter{i} .action-upload".format(i=index + 1)
world.css_click(button_css)
......
......@@ -191,7 +191,7 @@ def view_asset(_step, status):
# Note that world.visit would trigger a 403 error instead of displaying "Unauthorized"
# Instead, we can drop back into the selenium driver get command.
world.browser.driver.get(url)
assert_equal(world.css_text('body'),expected_text)
assert_equal(world.css_text('body'), expected_text)
@step('I see a confirmation that the file was deleted$')
......
......@@ -15,12 +15,17 @@ Feature: CMS.Video Component Editor
Then I can modify the display name
And my video display name change is persisted on save
# Disabling this 10/7/13 due to nondeterministic behavior
# in master. The failure seems to occur when YouTube does
# not respond quickly enough, so that the video player
# doesn't load.
#
# Sauce Labs cannot delete cookies
@skip_sauce
Scenario: Captions are hidden when "show captions" is false
Given I have created a Video component with subtitles
And I have set "show captions" to False
Then when I view the video it does not show the captions
# @skip_sauce
#Scenario: Captions are hidden when "show captions" is false
# Given I have created a Video component with subtitles
# And I have set "show captions" to False
# Then when I view the video it does not show the captions
# Sauce Labs cannot delete cookies
@skip_sauce
......
......@@ -2,28 +2,33 @@
Feature: CMS.Video Component
As a course author, I want to be able to view my created videos in Studio.
# 1
# Video Alpha Features will work in Firefox only when Firefox is the active window
Scenario: Autoplay is disabled in Studio
Given I have created a Video component
Then when I view the video it does not have autoplay enabled
# 2
Scenario: Creating a video takes a single click
Given I have clicked the new unit button
Then creating a video takes a single click
# 3
# Sauce Labs cannot delete cookies
@skip_sauce
Scenario: Captions are hidden correctly
Given I have created a Video component with subtitles
And I have hidden captions
Then when I view the video it does not show the captions
# @skip_sauce
#Scenario: Captions are hidden correctly
# Given I have created a Video component with subtitles
# And I have hidden captions
# Then when I view the video it does not show the captions
# 4
# Sauce Labs cannot delete cookies
@skip_sauce
Scenario: Captions are shown correctly
Given I have created a Video component with subtitles
Then when I view the video it does show the captions
# 5
# Sauce Labs cannot delete cookies
@skip_sauce
Scenario: Captions are toggled correctly
......@@ -31,7 +36,36 @@ Feature: CMS.Video Component
And I have toggled captions
Then when I view the video it does show the captions
# 6
Scenario: Video data is shown correctly
Given I have created a video with only XML data
And I reload the page
Then the correct Youtube video is shown
# 7
# Scenario: Closed captions become visible when the mouse hovers over CC button
# Given I have created a Video component with subtitles
# And Make sure captions are closed
# Then Captions become "invisible" after 3 seconds
# And I hover over button "CC"
# Then Captions become "visible"
# And I hover over button "volume"
# Then Captions become "invisible" after 3 seconds
# 8
#Scenario: Open captions never become invisible
# Given I have created a Video component with subtitles
# And Make sure captions are open
# Then Captions are "visible"
# And I hover over button "CC"
# Then Captions are "visible"
# And I hover over button "volume"
# Then Captions are "visible"
# 9
#Scenario: Closed captions are invisible when mouse doesn't hover on CC button
# Given I have created a Video component with subtitles
# And Make sure captions are closed
# Then Captions become "invisible" after 3 seconds
# And I hover over button "volume"
# Then Captions are "invisible"
......@@ -4,14 +4,18 @@ from lettuce import world, step
from xmodule.modulestore import Location
from contentstore.utils import get_modulestore
BUTTONS = {
'CC': '.hide-subtitles',
'volume': '.volume',
}
@step('I have created a Video component$')
def i_created_a_video_component(step):
world.create_course_with_unit()
world.create_component_instance(
step, '.large-video-icon',
'video',
'.xmodule_VideoModule',
has_multiple_templates=False
step=step,
category='video',
)
......@@ -19,6 +23,7 @@ def i_created_a_video_component(step):
def i_created_a_video_with_subs(_step):
_step.given('I have created a Video component with subtitles "OEoXaMPEzfM"')
@step('I have created a Video component with subtitles "([^"]*)"$')
def i_created_a_video_with_subs_with_name(_step, sub_id):
_step.given('I have created a Video component')
......@@ -115,3 +120,37 @@ def the_youtube_video_is_shown(_step):
world.wait_for_xmodule()
ele = world.css_find('.video').first
assert ele['data-streams'].split(':')[1] == world.scenario_dict['YOUTUBE_ID']
@step('Make sure captions are (.+)$')
def set_captions_visibility_state(_step, captions_state):
if captions_state == 'closed':
if world.css_visible('.subtitles'):
world.browser.find_by_css('.hide-subtitles').click()
else:
if not world.css_visible('.subtitles'):
world.browser.find_by_css('.hide-subtitles').click()
@step('I hover over button "([^"]*)"$')
def hover_over_button(_step, button):
world.css_find(BUTTONS[button.strip()]).mouse_over()
@step('Captions (?:are|become) "([^"]*)"$')
def are_captions_visibile(_step, visibility_state):
_step.given('Captions become "{0}" after 0 seconds'.format(visibility_state))
@step('Captions (?:are|become) "([^"]*)" after (.+) seconds$')
def check_captions_visibility_state(_step, visibility_state, timeout):
timeout = int(timeout.strip())
# Captions become invisible by fading out. We must wait by a specified
# time.
world.wait(timeout)
if visibility_state == 'visible':
assert world.css_visible('.subtitles')
else:
assert not world.css_visible('.subtitles')
"""
Script for dumping course dumping the course structure
"""
from django.core.management.base import BaseCommand, CommandError
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.django import modulestore
from json import dumps
from xmodule.modulestore.inheritance import own_metadata
from django.conf import settings
filter_list = ['xml_attributes', 'checklists']
class Command(BaseCommand):
"""
The Django command for dumping course structure
"""
help = '''Write out to stdout a structural and metadata information about a course in a flat dictionary serialized
in a JSON format. This can be used for analytics.'''
def handle(self, *args, **options):
"Execute the command"
if len(args) < 2 or len(args) > 3:
raise CommandError("dump_course_structure requires two or more arguments: <location> <outfile> |<db>|")
course_id = args[0]
outfile = args[1]
# use a user-specified database name, if present
# this is useful for doing dumps from databases restored from prod backups
if len(args) == 3:
settings.MODULESTORE['direct']['OPTIONS']['db'] = args[2]
loc = CourseDescriptor.id_to_location(course_id)
store = modulestore()
course = None
try:
course = store.get_item(loc, depth=4)
except:
print('Could not find course at {0}'.format(course_id))
return
info = {}
def dump_into_dict(module, info):
filtered_metadata = dict((key, value) for key, value in own_metadata(module).iteritems()
if key not in filter_list)
info[module.location.url()] = {
'category': module.location.category,
'children': module.children if hasattr(module, 'children') else [],
'metadata': filtered_metadata
}
for child in module.get_children():
dump_into_dict(child, info)
dump_into_dict(course, info)
with open(outfile, 'w') as f:
f.write(dumps(info))
......@@ -792,7 +792,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
source_location.tag, source_location.org, source_location.course, 'html', 'nonportable'])
html_module = module_store.get_instance(source_location.course_id, html_module_location)
self.assertTrue(isinstance(html_module.data, basestring))
self.assertIsInstance(html_module.data, basestring)
new_data = html_module.data.replace('/static/', '/c4x/{0}/{1}/asset/'.format(
source_location.org, source_location.course))
module_store.update_item(html_module_location, new_data)
......@@ -1273,6 +1273,47 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# export out to a tempdir
export_to_xml(module_store, content_store, location, root_dir, 'test_export')
def test_export_course_without_content_store(self):
module_store = modulestore('direct')
content_store = contentstore()
# Create toy course
import_from_xml(module_store, 'common/test/data/', ['toy'])
location = CourseDescriptor.id_to_location('edX/toy/2012_Fall')
# Add a sequence
stub_location = Location(['i4x', 'edX', 'toy', 'sequential', 'vertical_sequential'])
sequential = module_store.get_item(stub_location)
module_store.update_children(sequential.location, sequential.children)
# Get course and export it without a content_store
course = module_store.get_item(location)
course.save()
root_dir = path(mkdtemp_clean())
print 'Exporting to tempdir = {0}'.format(root_dir)
export_to_xml(module_store, None, location, root_dir, 'test_export_no_content_store')
# Delete the course from module store and reimport it
delete_course(module_store, content_store, location, commit=True)
import_from_xml(
module_store, root_dir, ['test_export_no_content_store'],
draft_store=None,
static_content_store=None,
target_location_namespace=course.location
)
# Verify reimported course
items = module_store.get_items(stub_location)
self.assertEqual(len(items), 1)
@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE, MODULESTORE=TEST_MODULESTORE)
class ContentStoreTest(ModuleStoreTestCase):
......@@ -1484,7 +1525,7 @@ class ContentStoreTest(ModuleStoreTestCase):
resp = self.client.get(reverse('course_index', kwargs=data))
self.assertContains(
resp,
'<article class="courseware-overview" data-course-id="i4x://MITx/999/course/Robot_Super_Course">',
'<article class="courseware-overview" data-id="i4x://MITx/999/course/Robot_Super_Course">',
status_code=200,
html=True
)
......@@ -1588,14 +1629,7 @@ class ContentStoreTest(ModuleStoreTestCase):
'name': loc.name}))
self.assertEqual(resp.status_code, 200)
# static_pages
resp = self.client.get(reverse('static_pages',
kwargs={'org': loc.org,
'course': loc.course,
'coursename': loc.name}))
self.assertEqual(resp.status_code, 200)
# static_pages
# asset_index
resp = self.client.get(reverse('asset_index',
kwargs={'org': loc.org,
'course': loc.course,
......
......@@ -3,7 +3,7 @@ import mock
from django.test import TestCase
from django.core.urlresolvers import reverse
from contentstore.views.requests import event as cms_user_track
from contentstore.views.helpers import event as cms_user_track
class CMSLogTest(TestCase):
......
......@@ -9,13 +9,13 @@ from .checklist import *
from .component import *
from .course import *
from .error import *
from .helpers import *
from .item import *
from .import_export import *
from .preview import *
from .public import *
from .user import *
from .tabs import *
from .requests import *
try:
from .dev import *
except ImportError:
......
......@@ -26,7 +26,7 @@ from contentstore.utils import (get_modulestore, get_lms_link_for_item,
from models.settings.course_grading import CourseGradingModel
from .requests import _xmodule_recurse
from .helpers import _xmodule_recurse
from .access import has_access
from xmodule.x_module import XModuleDescriptor
from xblock.plugin import PluginMissingError
......
......@@ -193,6 +193,7 @@ def import_course(request, org, course, name):
if not dirpath:
return JsonResponse(
{
'ErrMsg': _('Could not find the course.xml file in the package.'),
'Stage': 2
},
......
......@@ -12,7 +12,7 @@ from xmodule.modulestore.inheritance import own_metadata
from util.json_request import expect_json, JsonResponse
from ..utils import get_modulestore
from .access import has_access
from .requests import _xmodule_recurse
from .helpers import _xmodule_recurse
from xmodule.x_module import XModuleDescriptor
__all__ = ['save_item', 'create_item', 'delete_item']
......
......@@ -6,7 +6,7 @@ from django.conf import settings
from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden
from django.core.urlresolvers import reverse
from django.contrib.auth.decorators import login_required
from mitxmako.shortcuts import render_to_response
from mitxmako.shortcuts import render_to_response, render_to_string
from xmodule_modifiers import replace_static_urls, wrap_xmodule
from xmodule.error_module import ErrorDescriptor
......@@ -22,7 +22,7 @@ from util.sandboxing import can_execute_unsafe_code
import static_replace
from .session_kv_store import SessionKeyValueStore
from .requests import render_from_lms
from .helpers import render_from_lms
from .access import has_access
from ..utils import get_course_for_item
......@@ -79,9 +79,17 @@ def preview_component(request, location):
# can bind to it correctly
component.runtime.wrappers.append(partial(wrap_xmodule, 'xmodule_edit.html'))
try:
content = component.render('studio_view').content
# catch exceptions indiscriminately, since after this point they escape the
# dungeon and surface as uneditable, unsaveable, and undeletable
# component-goblins.
except Exception as exc: # pylint: disable=W0703
content = render_to_string('html_error.html', {'message': str(exc)})
return render_to_response('component.html', {
'preview': get_preview_html(request, component, 0),
'editor': component.runtime.render(component, None, 'studio_view').content,
'editor': content
})
......@@ -95,11 +103,6 @@ def preview_module_system(request, preview_id, descriptor):
descriptor: An XModuleDescriptor
"""
def preview_field_data(descriptor):
"Helper method to create a DbModel from a descriptor"
student_data = DbModel(SessionKeyValueStore(request))
return lms_field_data(descriptor._field_data, student_data)
course_id = get_course_for_item(descriptor.location).location.course_id
if descriptor.location.category == 'static_tab':
......@@ -118,7 +121,6 @@ def preview_module_system(request, preview_id, descriptor):
debug=True,
replace_urls=partial(static_replace.replace_static_urls, data_directory=None, course_id=course_id),
user=request.user,
xmodule_field_data=preview_field_data,
can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)),
mixins=settings.XBLOCK_MIXINS,
course_id=course_id,
......@@ -136,7 +138,8 @@ def preview_module_system(request, preview_id, descriptor):
getattr(descriptor, 'data_dir', descriptor.location.course),
course_id=descriptor.location.org + '/' + descriptor.location.course + '/BOGUS_RUN_REPLACE_WHEN_AVAILABLE',
),
)
),
error_descriptor_class=ErrorDescriptor,
)
......@@ -148,17 +151,12 @@ def load_preview_module(request, preview_id, descriptor):
preview_id (str): An identifier specifying which preview this module is used for
descriptor: An XModuleDescriptor
"""
system = preview_module_system(request, preview_id, descriptor)
try:
module = descriptor.xmodule(system)
except:
log.debug("Unable to load preview module", exc_info=True)
module = ErrorDescriptor.from_descriptor(
descriptor,
error_msg=exc_info_to_str(sys.exc_info())
).xmodule(system)
return module
student_data = DbModel(SessionKeyValueStore(request))
descriptor.bind_for_student(
preview_module_system(request, preview_id, descriptor),
lms_field_data(descriptor._field_data, student_data), # pylint: disable=protected-access
)
return descriptor
def get_preview_html(request, descriptor, idx):
......@@ -167,4 +165,8 @@ def get_preview_html(request, descriptor, idx):
specified by the descriptor and idx.
"""
module = load_preview_module(request, str(idx), descriptor)
return module.runtime.render(module, None, "student_view").content
try:
content = module.render("student_view").content
except Exception as exc: # pylint: disable=W0703
content = render_to_string('html_error.html', {'message': str(exc)})
return content
......@@ -14,10 +14,9 @@ from xmodule.modulestore.inheritance import own_metadata
from xmodule.modulestore.django import modulestore
from ..utils import get_course_for_item, get_modulestore
from .access import get_location_and_verify_access
__all__ = ['edit_tabs', 'reorder_static_tabs', 'static_pages']
__all__ = ['edit_tabs', 'reorder_static_tabs']
def initialize_course_tabs(course):
......@@ -126,20 +125,6 @@ def edit_tabs(request, org, course, coursename):
})
@login_required
@ensure_csrf_cookie
def static_pages(request, org, course, coursename):
"Static pages view"
location = get_location_and_verify_access(request, org, course, coursename)
course = modulestore().get_item(location)
return render_to_response('static-pages.html', {
'context_course': course,
})
# "primitive" tab edit functions driven by the command line.
# These should be replaced/deleted by a more capable GUI someday.
# Note that the command line UI identifies the tabs with 1-based
......
......@@ -12,20 +12,24 @@ from .common import *
from logsettings import get_logger_config
import os
# specified as an environment variable. Typically this is set
# in the service's upstart script and corresponds exactly to the service name.
# Service variants apply config differences via env and auth JSON files,
# the names of which correspond to the variant.
# SERVICE_VARIANT specifies name of the variant used, which decides what JSON
# configuration files are read during startup.
SERVICE_VARIANT = os.environ.get('SERVICE_VARIANT', None)
# when not variant is specified we attempt to load an unvaried
# config set.
CONFIG_PREFIX = ""
# CONFIG_ROOT specifies the directory where the JSON configuration
# files are expected to be found. If not specified, use the project
# directory.
CONFIG_ROOT = os.environ.get('CONFIG_ROOT', ENV_ROOT)
# CONFIG_PREFIX specifies the prefix of the JSON configuration files,
# based on the service variant. If no variant is use, don't use a
# prefix.
CONFIG_PREFIX = SERVICE_VARIANT + "." if SERVICE_VARIANT else ""
if SERVICE_VARIANT:
CONFIG_PREFIX = SERVICE_VARIANT + "."
############### ALWAYS THE SAME ################################
DEBUG = False
TEMPLATE_DEBUG = False
......@@ -77,7 +81,7 @@ CELERY_QUEUES = {
############# NON-SECURE ENV CONFIG ##############################
# Things like server locations, ports, etc.
with open(ENV_ROOT / CONFIG_PREFIX + "env.json") as env_file:
with open(CONFIG_ROOT / CONFIG_PREFIX + "env.json") as env_file:
ENV_TOKENS = json.load(env_file)
EMAIL_BACKEND = ENV_TOKENS.get('EMAIL_BACKEND', EMAIL_BACKEND)
......@@ -134,7 +138,7 @@ if "TRACKING_IGNORE_URL_PATTERNS" in ENV_TOKENS:
################ SECURE AUTH ITEMS ###############################
# Secret things: passwords, access keys, etc.
with open(ENV_ROOT / CONFIG_PREFIX + "auth.json") as auth_file:
with open(CONFIG_ROOT / CONFIG_PREFIX + "auth.json") as auth_file:
AUTH_TOKENS = json.load(auth_file)
# If Segment.io key specified, load it and turn on Segment.io if the feature flag is set
......
......@@ -218,6 +218,11 @@ STATICFILES_DIRS = [
TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html
# We want i18n to be turned off in production, at least until we have full localizations.
# Thus we want the Django translation engine to be disabled. Otherwise even without
# localization files, if the user's browser is set to a language other than us-en,
# strings like "login" and "password" will be translated and the rest of the page will be
# in English, which is confusing.
USE_I18N = False
USE_L10N = True
......
......@@ -9,6 +9,7 @@ from .common import *
from logsettings import get_logger_config
DEBUG = True
USE_I18N = True
TEMPLATE_DEBUG = DEBUG
LOGGING = get_logger_config(ENV_ROOT / "log",
logging_env="dev",
......
"""
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({
paths: {
paths: {
"gettext": "xmodule_js/common_static/js/test/i18n",
"mustache": "xmodule_js/common_static/js/vendor/mustache",
"codemirror": "xmodule_js/common_static/js/vendor/CodeMirror/codemirror",
......@@ -22,7 +22,7 @@ requirejs.config({
"underscore.string": "xmodule_js/common_static/js/vendor/underscore.string.min",
"backbone": "xmodule_js/common_static/js/vendor/backbone-min",
"backbone.associations": "xmodule_js/common_static/js/vendor/backbone-associations-min",
"youtube": "xmodule_js/common_static/js/load_youtube",
"youtube": "//www.youtube.com/player_api?noext",
"tinymce": "xmodule_js/common_static/js/vendor/tiny_mce/tiny_mce",
"jquery.tinymce": "xmodule_js/common_static/js/vendor/tiny_mce/jquery.tinymce",
"mathjax": "https://edx-static.s3.amazonaws.com/mathjax-MathJax-727332c/MathJax.js?config=TeX-MML-AM_HTMLorMML-full",
......@@ -32,9 +32,11 @@ requirejs.config({
"squire": "xmodule_js/common_static/js/vendor/Squire",
"jasmine-stealth": "xmodule_js/common_static/js/vendor/jasmine-stealth",
"jasmine.async": "xmodule_js/common_static/js/vendor/jasmine.async",
"draggabilly": "xmodule_js/common_static/js/vendor/draggabilly.pkgd",
"domReady": "xmodule_js/common_static/js/vendor/domReady",
"coffee/src/ajax_prefix": "xmodule_js/common_static/coffee/src/ajax_prefix"
},
}
shim: {
"gettext": {
exports: "gettext"
......@@ -100,6 +102,9 @@ requirejs.config({
deps: ["backbone"],
exports: "Backbone.Associations"
},
"youtube": {
exports: "YT"
},
"codemirror": {
exports: "CodeMirror"
},
......@@ -139,12 +144,14 @@ define([
"coffee/spec/models/course_spec", "coffee/spec/models/metadata_spec",
"coffee/spec/models/module_spec", "coffee/spec/models/section_spec",
"coffee/spec/models/settings_course_grader_spec",
"coffee/spec/models/settings_grading_spec", "coffee/spec/models/textbook_spec",
"coffee/spec/models/upload_spec",
"coffee/spec/views/section_spec",
"coffee/spec/views/course_info_spec", "coffee/spec/views/feedback_spec",
"coffee/spec/views/metadata_edit_spec", "coffee/spec/views/module_edit_spec",
"coffee/spec/views/overview_spec",
"coffee/spec/views/textbook_spec", "coffee/spec/views/upload_spec",
# these tests are run separate in the cms-squire suite, due to process
......
......@@ -22,7 +22,7 @@ requirejs.config({
"underscore.string": "xmodule_js/common_static/js/vendor/underscore.string.min",
"backbone": "xmodule_js/common_static/js/vendor/backbone-min",
"backbone.associations": "xmodule_js/common_static/js/vendor/backbone-associations-min",
"youtube": "xmodule_js/common_static/js/load_youtube",
"youtube": "//www.youtube.com/player_api?noext",
"tinymce": "xmodule_js/common_static/js/vendor/tiny_mce/tiny_mce",
"jquery.tinymce": "xmodule_js/common_static/js/vendor/tiny_mce/jquery.tinymce",
"mathjax": "https://edx-static.s3.amazonaws.com/mathjax-MathJax-727332c/MathJax.js?config=TeX-MML-AM_HTMLorMML-full",
......@@ -100,6 +100,9 @@ requirejs.config({
deps: ["backbone"],
exports: "Backbone.Associations"
},
"youtube": {
exports: "YT"
},
"codemirror": {
exports: "CodeMirror"
},
......
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
</div>
<div class="sidebar window course-handouts" id="course-handouts-view"></div>
</div>
<div class="modal-cover"></div>
"""
beforeEach ->
......@@ -45,13 +46,56 @@ define ["js/views/course_info_handout", "js/views/course_info_update", "js/model
preventDefault : () -> 'no op'
}
@createNewUpdate = () ->
@createNewUpdate = (text) ->
# Edit button is not in the template under test (it is in parent HTML).
# Therefore call onNew directly.
@courseInfoEdit.onNew(@event)
spyOn(@courseInfoEdit.$codeMirror, 'getValue').andReturn('/static/image.jpg')
spyOn(@courseInfoEdit.$codeMirror, 'getValue').andReturn(text)
@courseInfoEdit.$el.find('.save-button').click()
@cancelNewCourseInfo = (useCancelButton) ->
spyOn(@courseInfoEdit.$modalCover, 'show').andCallThrough()
spyOn(@courseInfoEdit.$modalCover, 'hide').andCallThrough()
@courseInfoEdit.onNew(@event)
expect(@courseInfoEdit.$modalCover.show).toHaveBeenCalled()
spyOn(@courseInfoEdit.$codeMirror, 'getValue').andReturn('unsaved changes')
model = @collection.at(0)
spyOn(model, "save").andCallThrough()
cancelEditingUpdate(useCancelButton)
expect(@courseInfoEdit.$modalCover.hide).toHaveBeenCalled()
expect(model.save).not.toHaveBeenCalled()
previewContents = @courseInfoEdit.$el.find('.update-contents').html()
expect(previewContents).not.toEqual('unsaved changes')
@cancelExistingCourseInfo = (useCancelButton) ->
@createNewUpdate('existing update')
spyOn(@courseInfoEdit.$modalCover, 'show').andCallThrough()
spyOn(@courseInfoEdit.$modalCover, 'hide').andCallThrough()
@courseInfoEdit.$el.find('.edit-button').click()
expect(@courseInfoEdit.$modalCover.show).toHaveBeenCalled()
spyOn(@courseInfoEdit.$codeMirror, 'getValue').andReturn('modification')
model = @collection.at(0)
spyOn(model, "save").andCallThrough()
cancelEditingUpdate(useCancelButton)
expect(@courseInfoEdit.$modalCover.hide).toHaveBeenCalled()
expect(model.save).not.toHaveBeenCalled()
previewContents = @courseInfoEdit.$el.find('.update-contents').html()
expect(previewContents).toEqual('existing update')
cancelEditingUpdate = (update, useCancelButton) ->
if useCancelButton
update.$el.find('.cancel-button').click()
else
$('.modal-cover').click()
afterEach ->
@xhrRestore()
......@@ -75,19 +119,30 @@ define ["js/views/course_info_handout", "js/views/course_info_update", "js/model
it "does rewrite links for preview", ->
# Create a new update.
@createNewUpdate()
@createNewUpdate('/static/image.jpg')
# Verify the link is rewritten for preview purposes.
previewContents = @courseInfoEdit.$el.find('.update-contents').html()
expect(previewContents).toEqual('base-asset-url/image.jpg')
it "shows static links in edit mode", ->
@createNewUpdate()
@createNewUpdate('/static/image.jpg')
# Click edit and verify CodeMirror contents.
@courseInfoEdit.$el.find('.edit-button').click()
expect(@courseInfoEdit.$codeMirror.getValue()).toEqual('/static/image.jpg')
it "removes newly created course info on cancel", ->
@cancelNewCourseInfo(true)
it "removes newly created course info on click outside modal", ->
@cancelNewCourseInfo(false)
it "does not remove existing course info on cancel", ->
@cancelExistingCourseInfo(true)
it "does not remove existing course info on click outside modal", ->
@cancelExistingCourseInfo(false)
describe "Course Handouts", ->
handoutsTemplate = readFixtures('course_info_handouts.underscore')
......
......@@ -5,7 +5,6 @@ define ["coffee/src/views/module_edit", "xmodule"], (ModuleEdit) ->
@stubModule = jasmine.createSpy("Module")
@stubModule.id = 'stub-id'
setFixtures """
<li class="component" id="stub-id">
<div class="component-editor">
......@@ -19,7 +18,7 @@ define ["coffee/src/views/module_edit", "xmodule"], (ModuleEdit) ->
<a href="#" class="edit-button"><span class="edit-icon white"></span>Edit</a>
<a href="#" class="delete-button"><span class="delete-icon white"></span>Delete</a>
</div>
<a href="#" class="drag-handle"></a>
<span class="drag-handle"></span>
<section class="xmodule_display xmodule_stub" data-type="StubModule">
<div id="stub-module-content"/>
</section>
......
define ["jquery", "underscore.string", "backbone", "js/views/feedback_notification", "jquery.cookie"],
($, str, Backbone, NotificationView) ->
define ["domReady", "jquery", "underscore.string", "backbone", "gettext",
"js/views/feedback_notification", "jquery.cookie"],
(domReady, $, str, Backbone, gettext, NotificationView) ->
AjaxPrefix.addAjaxPrefix jQuery, ->
$("meta[name='path_prefix']").attr('content')
......@@ -36,5 +37,5 @@ define ["jquery", "underscore.string", "backbone", "js/views/feedback_notificati
if onTouchBasedDevice()
$('body').addClass 'touch-based-device'
$(main)
domReady(main)
return main
......@@ -92,7 +92,6 @@ define ["backbone", "jquery", "underscore", "gettext", "xmodule",
title: gettext('Saving&hellip;')
saving.show()
@model.save(data).done( =>
# # showToastMessage("Your changes have been saved.", null, 3)
@module = null
@render()
@$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({
},
parse : function(attrs) {
if (attrs['weight']) {
if (!_.isNumber(attrs.weight)) attrs.weight = parseInt(attrs.weight, 10);
attrs.weight = parseInt(attrs.weight, 10);
}
if (attrs['min_count']) {
if (!_.isNumber(attrs.min_count)) attrs.min_count = parseInt(attrs.min_count, 10);
attrs.min_count = parseInt(attrs.min_count, 10);
}
if (attrs['drop_count']) {
if (!_.isNumber(attrs.drop_count)) attrs.drop_count = parseInt(attrs.drop_count, 10);
attrs.drop_count = parseInt(attrs.drop_count, 10);
}
return attrs;
},
......
......@@ -17,10 +17,10 @@ var CourseGradingPolicy = Backbone.Model.extend({
// interesting race condition: if {parse:true} when newing, then parse called before .attributes created
if (this.attributes && this.has('graders')) {
graderCollection = this.get('graders');
graderCollection.reset(attributes.graders);
graderCollection.reset(attributes.graders, {parse:true});
}
else {
graderCollection = new CourseGraderCollection(attributes.graders);
graderCollection = new CourseGraderCollection(attributes.graders, {parse:true});
graderCollection.course_location = attributes['course_location'] || this.get('course_location');
}
attributes.graders = graderCollection;
......
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",
"js/views/feedback_prompt", "js/views/feedback_notification", "js/views/course_info_helper"],
function(Backbone, _, CodeMirror, CourseUpdateModel, PromptView, NotificationView, CourseInfoHelper) {
var $modalCover = $(".modal-cover");
var CourseInfoUpdateView = Backbone.View.extend({
// collection is CourseUpdateCollection
events: {
......@@ -18,6 +17,8 @@ define(["backbone", "underscore", "codemirror", "js/models/course_update",
this.render();
// when the client refetches the updates as a whole, re-render them
this.listenTo(this.collection, 'reset', this.render);
this.$modalCover = $(".modal-cover");
},
render: function () {
......@@ -63,8 +64,8 @@ define(["backbone", "underscore", "codemirror", "js/models/course_update",
$newForm.addClass('editing');
this.$currentPost = $newForm.closest('li');
$modalCover.show();
$modalCover.bind('click', function() {
this.$modalCover.show();
this.$modalCover.bind('click', function() {
self.closeEditor(true);
});
......@@ -120,9 +121,9 @@ define(["backbone", "underscore", "codemirror", "js/models/course_update",
this.$codeMirror = CourseInfoHelper.editWithCodeMirror(
targetModel, 'content', self.options['base_asset_url'], $textArea.get(0));
$modalCover.show();
$modalCover.bind('click', function() {
self.closeEditor(self);
this.$modalCover.show();
this.$modalCover.bind('click', function() {
self.closeEditor(false);
});
},
......@@ -197,8 +198,8 @@ define(["backbone", "underscore", "codemirror", "js/models/course_update",
this.$currentPost.find('.CodeMirror').remove();
}
$modalCover.unbind('click');
$modalCover.hide();
this.$modalCover.unbind('click');
this.$modalCover.hide();
this.$codeMirror = null;
},
......
define(["js/views/validation", "underscore", "jquery", "js/views/settings/grader"],
function(ValidatingView, _, $, GraderView) {
define(["js/views/validation", "underscore", "jquery", "jquery.ui", "js/views/settings/grader"],
function(ValidatingView, _, $, ui, GraderView) {
var GradingView = ValidatingView.extend({
// Model class is CMS.Models.Settings.CourseGradingPolicy
......
......@@ -28,7 +28,6 @@ prepend_path: cms/static
# Paths to library JavaScript files (optional)
lib_paths:
- coffee/spec/setup_require.js
- xmodule_js/common_static/js/vendor/require.js
- xmodule_js/common_static/coffee/src/ajax_prefix.js
- xmodule_js/common_static/js/src/utility.js
......@@ -51,6 +50,10 @@ lib_paths:
- xmodule_js/common_static/js/vendor/CodeMirror/codemirror.js
- xmodule_js/src/xmodule.js
- xmodule_js/common_static/js/test/i18n.js
- xmodule_js/common_static/js/vendor/draggabilly.pkgd.js
- xmodule_js/common_static/js/vendor/date.js
- xmodule_js/common_static/js/vendor/domReady.js
- xmodule_js/common_static/js/vendor/jquery.smooth-scroll.min.js
# Paths to source JavaScript files
src_paths:
......
......@@ -28,7 +28,6 @@ prepend_path: cms/static
# Paths to library JavaScript files (optional)
lib_paths:
- coffee/spec/setup_require.js
- xmodule_js/common_static/js/vendor/require.js
- xmodule_js/common_static/coffee/src/ajax_prefix.js
- xmodule_js/common_static/js/src/utility.js
......
......@@ -528,9 +528,9 @@ p, ul, ol, dl {
.new-subsection-item,
.new-policy-item {
@include grey-button;
margin: 5px 8px;
padding: 3px 10px 4px 10px;
font-size: 10px;
@include font-size(10);
margin: ($baseline/2);
padding: 3px ($baseline/2) 4px ($baseline/2);
.new-folder-icon,
.new-policy-icon,
......
../../../common/static/sass/_mixins-inherited.scss
\ No newline at end of file
......@@ -3,7 +3,7 @@
// ====================
// view - dashboard
body.dashboard {
.view-dashboard {
// elements - authorship controls
.wrapper-authorshiprights {
......@@ -22,6 +22,35 @@ body.dashboard {
}
}
// ====================
.view-unit {
.unit-location .draggable-drop-indicator {
display: none; //needed to not show DnD UI (UI is shared across both views)
}
}
// ====================
// needed to override ui-window styling for dragging state (outline selectors get too specific)
.courseware-section.is-dragging {
box-shadow: 0 1px 2px 0 $shadow-d1 !important;
border: 1px solid $gray-d3 !important;
}
.courseware-section.is-dragging.valid-drop {
border-color: $blue-s1 !important;
box-shadow: 0 1px 2px 0 $blue-t2 !important;
}
// ====================
// needed for poorly scoped margin rules on all content elements
.branch .sortable-unit-list {
margin-bottom: 0;
}
// yes we have no boldness today - need to fix the resets
body strong,
......@@ -29,12 +58,13 @@ body b {
font-weight: 700;
}
// known things to do (paint the fence, sand the floor, wax on/off)
// ====================
// known things to do (paint the fence, sand the floor, wax on/off):
/* known things to do (paint the fence, sand the floor, wax on/off):
* centralize and move form styling into forms.scss - cms/static/sass/views/_textbooks.scss and cms/static/sass/views/_settings.scss
* move dialogue styles into cms/static/sass/elements/_modal.scss
* use the @include placeholder Bourbon mixin (http://bourbon.io/docs/#placeholder) for any placeholder styling
// * centralize and move form styling into forms.scss - cms/static/sass/views/_textbooks.scss and cms/static/sass/views/_settings.scss
// * move dialogue styles into cms/static/sass/elements/_modal.scss
// * use the @include placeholder Bourbon mixin (http://bourbon.io/docs/#placeholder) for any placeholder styling
*/
......@@ -173,12 +173,14 @@ $tmg-f3: 0.125s;
// ====================
// specific UI
$notification-height: ($baseline*10);
$ui-notification-height: ($baseline*10);
$ui-update-color: $blue-l4;
// ====================
// inherited
$baseFontColor: $gray-d2;
$lighter-base-font-color: rgb(100,100,100);
$offBlack: #3c3c3c;
$green: #108614;
$lightGrey: #edf1f5;
......@@ -195,6 +197,17 @@ $lightBluishGrey: rgb(197, 207, 223);
$lightBluishGrey2: rgb(213, 220, 228);
$error-red: rgb(253, 87, 87);
//carryover from LMS for xmodules
$sidebar-color: rgb(246, 246, 246);
// type
$sans-serif: $f-sans-serif;
$body-line-height: golden-ratio(.875em, 1);
// carried over from LMS for xmodules
$action-primary-active-bg: #1AA1DE; // $m-blue
$very-light-text: #fff;
......@@ -140,22 +140,22 @@
}
90% {
@include transform(translateY(-($notification-height)));
@include transform(translateY(-($ui-notification-height)));
}
100% {
@include transform(translateY(-($notification-height*0.99)));
@include transform(translateY(-($ui-notification-height*0.99)));
}
}
// notifications slide down
@include keyframes(notificationSlideDown) {
0% {
@include transform(translateY(-($notification-height*0.99)));
@include transform(translateY(-($ui-notification-height*0.99)));
}
10% {
@include transform(translateY(-($notification-height)));
@include transform(translateY(-($ui-notification-height)));
}
100% {
......@@ -211,3 +211,39 @@
%anim-bounceOut {
@include animation(bounceOut $tmg-f1 ease-in-out 1);
}
// ====================
// flash
@include keyframes(flash) {
0%, 100% {
opacity: 1.0;
}
50% {
opacity: 0.0;
}
}
// canned animation - use if you want out of the box/non-customized anim
%anim-flash {
@include animation(flash $tmg-f1 ease-in-out 1);
}
// flash - double
@include keyframes(flashDouble) {
0%, 50%, 100% {
opacity: 1.0;
}
25%, 75% {
opacity: 0.0;
}
}
// canned animation - use if you want out of the box/non-customized anim
%anim-flashDouble {
@include animation(flashDouble $tmg-f1 ease-in-out 1);
}
\ No newline at end of file
......@@ -200,3 +200,83 @@
%view-live-button {
@extend %t-action4;
}
// ====================
// UI: drag handles
.drag-handle {
&:hover, &:focus {
cursor: move;
}
}
// UI: elem is draggable
.is-draggable {
@include transition(border-color $tmg-f2 ease-in-out 0, box-shadow $tmg-f2 ease-in-out 0);
position: relative;
.draggable-drop-indicator {
@extend %ui-depth3;
@include transition(opacity $tmg-f2 linear 0s);
@include size(100%, auto);
position: absolute;
border-top: 1px solid $blue-l1;
opacity: 0.0;
*[class^="icon-caret"] {
@extend %t-icon5;
position: absolute;
top: -12px;
left: -($baseline/4);
color: $blue-s1;
}
}
.draggable-drop-indicator-before {
top: -($baseline/2);
}
.draggable-drop-indicator-after {
bottom: -($baseline/2);
}
}
// UI: drag state - is dragging
.is-dragging {
@extend %ui-depth4;
left: -($baseline/4);
box-shadow: 0 1px 2px 0 $shadow-d1;
cursor: move;
opacity: 0.65;
border: 1px solid $gray-d3;
// UI: condition - valid drop
&.valid-drop {
border-color: $blue-s1;
box-shadow: 0 1px 2px 0 $blue-t2;
}
}
// UI: drag state - was dragging
.was-dragging {
@include transition(transform $tmg-f2 ease-in-out 0);
}
// UI: drag target
.drop-target {
&.drop-target-before {
> .draggable-drop-indicator-before {
opacity: 1.0;
}
}
&.drop-target-after {
> .draggable-drop-indicator-after {
opacity: 1.0;
}
}
}
......@@ -712,7 +712,7 @@
// notification showing/hiding
.wrapper-notification {
bottom: -($notification-height);
bottom: -($ui-notification-height);
// varying animations
&.is-shown {
......
// tender help/support widget
// ====================
// UI: hiding the default tender help "tag" element
#tender_toggler {
display: none;
}
#tender_frame, #tender_window {
background-image: none !important;
background: none;
......
......@@ -233,13 +233,6 @@
}
}
}
.signup {
}
.signin {
#field-password {
position: relative;
......
......@@ -157,16 +157,8 @@
// CASE: has actions
&.has-actions {
.status-detail {
width: flex-grid(5,9);
}
.list-actions {
display: none;
width: flex-grid(3,9);
float: right;
margin-left: flex-gutter();
text-align: right;
.action-primary {
@extend %btn-primary-blue;
......
......@@ -400,4 +400,21 @@
}
}
}
// UI: DnD - specific elems/cases - units
.courseware-unit {
.draggable-drop-indicator-before {
top: 0;
}
.draggable-drop-indicator-after {
bottom: 0;
}
}
// UI: DnD - specific elems/cases - empty parents initial drop indicator
.draggable-drop-indicator-initial {
display: none;
}
}
......@@ -355,6 +355,20 @@ body.course.unit,.view-unit {
}
}
.wrapper-alert-error {
margin-top: ($baseline*1.25);
box-shadow: none;
border-top: 5px solid $red-l1;
.copy,
.title {
color: $white;
}
}
}
}
......@@ -415,6 +429,19 @@ body.course.unit,.view-unit {
margin-left: 0;
}
}
// UI: DnD - specific elems/cases - unit
.courseware-unit {
// STATE: was dropped
&.was-dropped {
> .section-item {
background-color: $ui-update-color !important; // nasty, but needed for specificity
}
}
}
// ====================
// Component Editing
......
......@@ -33,6 +33,7 @@
window.baseUrl = "${settings.STATIC_URL}";
var require = {
baseUrl: baseUrl,
waitSeconds: 60,
paths: {
"domReady": "js/vendor/domReady",
"gettext": "/i18n",
......@@ -60,12 +61,18 @@ var require = {
"underscore.string": "js/vendor/underscore.string.min",
"backbone": "js/vendor/backbone-min",
"backbone.associations": "js/vendor/backbone-associations-min",
"youtube": "js/load_youtube",
"tinymce": "js/vendor/tiny_mce/tiny_mce",
"jquery.tinymce": "js/vendor/tiny_mce/jquery.tinymce",
"mathjax": "https://edx-static.s3.amazonaws.com/mathjax-MathJax-727332c/MathJax.js?config=TeX-MML-AM_HTMLorMML-full",
"xmodule": "/xmodule/xmodule",
"utility": "js/src/utility"
"utility": "js/src/utility",
"draggabilly": "js/vendor/draggabilly.pkgd",
// externally hosted files
"mathjax": "//edx-static.s3.amazonaws.com/mathjax-MathJax-727332c/MathJax.js?config=TeX-MML-AM_HTMLorMML-full",
// youtube URL does not end in ".js". We add "?noext" to the path so
// that require.js adds the ".js" to the query component of the URL,
// and leaves the path component intact.
"youtube": "//www.youtube.com/player_api?noext"
},
shim: {
"gettext": {
......@@ -136,6 +143,9 @@ var require = {
deps: ["backbone"],
exports: "Backbone.Associations"
},
"youtube": {
exports: "YT"
},
"codemirror": {
exports: "CodeMirror"
},
......@@ -147,16 +157,27 @@ var require = {
},
"mathjax": {
exports: "MathJax"
},
"coffee/src/main": {
deps: ["coffee/src/ajax_prefix"]
},
"coffee/src/logger": {
exports: "Logger",
deps: ["coffee/src/ajax_prefix"]
}
},
// load these automatically
deps: ["js/base", "coffee/src/main", "datepair"]
// we need "datepair" because it dynamically modifies the page when it is loaded -- yuck!
// load jquery and gettext automatically
deps: ["jquery", "gettext"],
callback: function() {
// load other scripts on every page, after jquery loads
require(["js/base", "coffee/src/main", "coffee/src/logger", "datepair"]);
// we need "datepair" because it dynamically modifies the page
// when it is loaded -- yuck!
}
};
</script>
<script type="text/javascript" src="${static.url("js/vendor/require.js")}"></script>
<script type="text/javascript" src="${static.url("coffee/src/logger.js")}"></script>
<script type="text/javascript" src="${static.url("coffee/src/ajax_prefix.js")}"></script>
## js templates
<script id="system-feedback-tpl" type="text/template">
......@@ -187,6 +208,9 @@ require(['js/models/course'], function(Course) {
<%block name="content"></%block>
% if user.is_authenticated():
<script type="text/javascript">
require(['js/sock']);
</script>
<%include file="widgets/sock.html" />
% endif
......
......@@ -31,6 +31,6 @@
<a href="#" class="edit-button standard"><span class="edit-icon"></span>${_("Edit")}</a>
<a href="#" class="delete-button standard"><span class="delete-icon"></span>${_("Delete")}</a>
</div>
<a data-tooltip='${_("Drag to reorder")}' href="#" class="drag-handle"></a>
<span data-tooltip='${_("Drag to reorder")}' class="drag-handle"></span>
${preview}
......@@ -21,9 +21,11 @@
<label>${_("Display Name:")}</label>
<input type="text" value="${subsection.display_name_with_default | h}" class="subsection-display-name-input" data-metadata-name="display_name"/>
</div>
<div class="sortable-unit-list">
<label>${_("Units:")}</label>
${units.enum_units(subsection, subsection_units=subsection_units)}
<div class="wrapper-dnd">
<div class="sortable-unit-list">
<label>${_("Units:")}</label>
${units.enum_units(subsection, subsection_units=subsection_units)}
</div>
</div>
</article>
</div>
......@@ -112,9 +114,8 @@ require(["domReady!", "jquery", "js/models/location", "js/views/overview_assignm
// I believe that current (New Section/New Subsection) cause full page reloads which means these aren't needed globally
// but we really should change that behavior.
if (!window.graderTypes) {
window.graderTypes = new CourseGraderCollection();
window.graderTypes = new CourseGraderCollection(${course_graders|n}, {parse:true});
window.graderTypes.course_location = new Location('${parent_location}');
window.graderTypes.reset(${course_graders|n});
}
$(".gradable-status").each(function(index, ele) {
......
<%! from django.utils.translation import ugettext as _ %>
<%! from django.core.urlresolvers import reverse %>
<%block name="content">
<div class="wrapper wrapper-alert wrapper-alert-error is-shown">
<div class="error">
<div class="copy">
<h2 class="title">
<i class="icon-warning-sign"></i>
${_("We're having trouble rendering your component")}
</h2>
<p>${_("Students will not be able to access this component. Re-edit your component to fix the error.")}</p>
% if message:
<p class="description">
${_("Error:")}
${message | h}
</p>
% endif
</div>
</div>
</%block>
......@@ -111,13 +111,13 @@
<div class="status-detail">
<h3 class="title">${_("Success")}</h3>
<p class="copy">${_("Your imported content has now been integrated into this course")}</p>
</div>
<ul class="list-actions">
<li class="item-action">
<a href="${successful_import_redirect_url}" class="action action-primary">${_("View Updated Outline")}</a>
</li>
</ul>
<ul class="list-actions">
<li class="item-action">
<a href="${successful_import_redirect_url}" class="action action-primary">${_("View Updated Outline")}</a>
</li>
</ul>
</div>
</li>
</ol>
</div>
......
......@@ -7,29 +7,26 @@
<%block name="jsextra">
<script type="text/javascript">
require(['jquery', 'jquery.form'], function($) {
$(document).ready(function () {
// showing/hiding creation rights UI
$('.show-creationrights').click(function(e){
(e).preventDefault();
$(this).closest('.wrapper-creationrights').toggleClass('is-shown').find('.ui-toggle-control').toggleClass('current');
});
require(["domReady!", "jquery", "jquery.form", "js/index"], function(doc, $) {
// showing/hiding creation rights UI
$('.show-creationrights').click(function(e){
(e).preventDefault();
$(this).closest('.wrapper-creationrights').toggleClass('is-shown').find('.ui-toggle-control').toggleClass('current');
});
var reloadPage = function () {
location.reload();
};
var reloadPage = function () {
location.reload();
};
var showError = function () {
$('#request-coursecreator-submit').toggleClass('has-error').find('.label').text('Sorry, there was error with your request');
$('#request-coursecreator-submit').find('.icon-cog').toggleClass('icon-spin');
};
var showError = function () {
$('#request-coursecreator-submit').toggleClass('has-error').find('.label').text('Sorry, there was error with your request');
$('#request-coursecreator-submit').find('.icon-cog').toggleClass('icon-spin');
};
$('#request-coursecreator').ajaxForm({error: showError, success: reloadPage});
$('#request-coursecreator').ajaxForm({error: showError, success: reloadPage});
$('#request-coursecreator-submit').click(function(e){
$(this).toggleClass('is-disabled is-submitting').find('.label').text('Submitting Your Request');
});
$('#request-coursecreator-submit').click(function(e){
$(this).toggleClass('is-disabled is-submitting').find('.label').text('Submitting Your Request');
});
});
</script>
......
......@@ -25,9 +25,8 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
// I believe that current (New Section/New Subsection) cause full page reloads which means these aren't needed globally
// but we really should change that behavior.
if (!window.graderTypes) {
window.graderTypes = new CourseGraderCollection();
window.graderTypes = new CourseGraderCollection(${course_graders|n}, {parse:true});
window.graderTypes.course_location = new Location('${parent_location}');
window.graderTypes.reset(${course_graders|n});
}
$(".gradable-status").each(function(index, ele) {
......@@ -82,7 +81,7 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
</div>
<div class="item-actions">
<a href="#" data-tooltip="${_('Delete this section')}" class="delete-button delete-section-button"><span class="delete-icon"></span></a>
<a href="#" data-tooltip="${_('Drag to re-order')}" class="drag-handle"></a>
<span data-tooltip="${_('Drag to re-order')}" class="drag-handle"></span>
</div>
</header>
</section>
......@@ -138,74 +137,90 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
<div class="main-wrapper">
<div class="inner-wrapper">
<article class="courseware-overview" data-course-id="${context_course.location.url()}">
% for section in sections:
<section class="courseware-section branch" data-id="${section.location}">
<header>
<a href="#" data-tooltip="${_('Expand/collapse this section')}" class="expand-collapse-icon collapse"></a>
<div class="item-details" data-id="${section.location}">
<h3 class="section-name" data-name="${section.display_name_with_default | h}"></h3>
<div class="section-published-date">
<%
if section.start is not None:
start_date_str = section.start.strftime('%m/%d/%Y')
start_time_str = section.start.strftime('%H:%M')
else:
start_date_str = ''
start_time_str = ''
%>
%if section.start is None:
<span class="published-status">${_("This section has not been released.")}</span>
<a href="#" class="schedule-button" data-date="" data-time="" data-id="${section.location}">${_("Schedule")}</a>
%else:
<span class="published-status"><strong>${_("Will Release:")}</strong>
${date_utils.get_default_time_display(section.start)}</span>
<a href="#" class="edit-button" data-date="${start_date_str}"
data-time="${start_time_str}" data-id="${section.location}">${_("Edit")}</a>
%endif
</div>
</div>
<div class="item-actions">
<a href="#" data-tooltip="${_('Delete this section')}" class="delete-button delete-section-button"><span class="delete-icon"></span></a>
<a href="#" data-tooltip="${_('Drag to reorder')}" class="drag-handle"></a>
</div>
</header>
<div class="subsection-list">
<div class="list-header">
<a href="#" class="new-subsection-item" data-category="${new_subsection_category}">
<span class="new-folder-icon"></span>${_("New Subsection")}
</a>
</div>
<ol data-section-id="${section.location.url()}">
% for subsection in section.get_children():
<li class="branch collapsed id-holder" data-id="${subsection.location}">
<div class="section-item">
<div class="details">
<a href="#" data-tooltip="${_('Expand/collapse this subsection')}" class="expand-collapse-icon expand"></a>
<a href="${reverse('edit_subsection', args=[subsection.location])}">
<span class="folder-icon"></span>
<span class="subsection-name"><span class="subsection-name-value">${subsection.display_name_with_default}</span></span>
</a>
</div>
<div class="wrapper-dnd">
<article class="courseware-overview" data-id="${context_course.location.url()}">
% for section in sections:
<section class="courseware-section branch is-draggable" data-id="${section.location}" data-parent-id="${context_course.location.url()}">
<%include file="widgets/_ui-dnd-indicator-before.html" />
<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="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>
${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">
<a href="#" data-tooltip="${_('Delete this subsection')}" class="delete-button delete-subsection-button"><span class="delete-icon"></span></a>
<a href="#" data-tooltip="${_('Drag to reorder')}" class="drag-handle"></a>
</div>
</div>
${units.enum_units(subsection)}
</li>
% endfor
</ol>
</div>
</section>
% endfor
</article>
<%include file="widgets/_ui-dnd-indicator-after.html" />
</section>
% endfor
</article>
</div>
</div>
</div>
<footer></footer>
......
......@@ -17,6 +17,10 @@
</script>
<script type="text/javascript">
window.CMS = window.CMS || {};
CMS.URL = CMS.URL || {};
CMS.URL.UPLOAD_ASSET = '${upload_asset_url}';
require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/settings/main"],
function(doc, $, CourseDetailsModel, MainView) {
// hilighting labels when fields are focused in
......@@ -37,8 +41,6 @@ require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/s
},
reset: true
});
CMS.URL.UPLOAD_ASSET = '${upload_asset_url}';
});
</script>
</%block>
......
<%! 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 @@
</section>
<script type = "text/javascript">
require(["jquery", "codemirror/stex"], function($) {
require(["jquery", "jquery.leanModal", "codemirror/stex"], function($) {
hlstrig = $('#hls-trig-${hlskey}');
hlsmodal = $('#hls-modal-${hlskey}');
......
......@@ -5,13 +5,16 @@
This def will enumerate through a passed in subsection and list all of the units
-->
<%def name="enum_units(subsection, actions=True, selected=None, sortable=True, subsection_units=None)">
<ol ${'class="sortable-unit-list"' if sortable else ''} data-subsection-id="${subsection.location}">
<ol ${'class="sortable-unit-list"' if sortable else ''}>
<%
if subsection_units is None:
subsection_units = subsection.get_children()
%>
% for unit in subsection_units:
<li class="leaf unit" data-id="${unit.location}">
<li class="courseware-unit leaf unit is-draggable" data-id="${unit.location}" data-parent-id="${subsection.location.url()}">
<%include file="_ui-dnd-indicator-before.html" />
<%
unit_state = compute_unit_state(unit)
if unit.location == selected:
......@@ -27,13 +30,17 @@ This def will enumerate through a passed in subsection and list all of the units
% if actions:
<div class="item-actions">
<a href="#" data-tooltip="Delete this unit" class="delete-button" data-id="${unit.location}"><span class="delete-icon"></span></a>
<a href="#" data-tooltip="Drag to sort" class="drag-handle"></a>
<span data-tooltip="Drag to sort" class="drag-handle unit-drag-handle"></span>
</div>
% endif
</div>
<%include file="_ui-dnd-indicator-after.html" />
</li>
% endfor
<li>
<%include file="_ui-dnd-indicator-initial.html" />
<a href="#" class="new-unit-item" data-category="${new_unit_category}" data-parent="${subsection.location}">
<span class="new-unit-icon"></span>New Unit
</a>
......
......@@ -70,9 +70,6 @@ urlpatterns = ('', # nopep8
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/(?P<category>[^/]+)/(?P<name>[^/]+)/gradeas.*$',
'contentstore.views.assignment_type_update', name='assignment_type_update'),
url(r'^pages/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$',
'contentstore.views.static_pages',
name='static_pages'),
url(r'^edit_tabs/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$',
'contentstore.views.edit_tabs', name='edit_tabs'),
......
......@@ -42,6 +42,6 @@ class CeleryConfigTest(unittest.TestCase):
# We don't know the other dict values exactly,
# but we can assert that they take the right form
self.assertTrue(isinstance(result_dict['task_id'], unicode))
self.assertTrue(isinstance(result_dict['time'], float))
self.assertIsInstance(result_dict['task_id'], unicode)
self.assertIsInstance(result_dict['time'], float)
self.assertTrue(result_dict['time'] > 0.0)
......@@ -2,7 +2,7 @@
# pylint: disable=W0621
from lettuce import world
from django.contrib.auth.models import User
from django.contrib.auth.models import User, Group
from student.models import CourseEnrollment
from xmodule.modulestore.django import editable_modulestore
from xmodule.contentstore.django import contentstore
......@@ -51,6 +51,21 @@ def register_by_course_id(course_id, username='robot', password='test', is_staff
@world.absorb
def add_to_course_staff(username, course_num):
"""
Add the user with `username` to the course staff group
for `course_num`.
"""
# Based on code in lms/djangoapps/courseware/access.py
group_name = "instructor_{}".format(course_num)
group, _ = Group.objects.get_or_create(name=group_name)
group.save()
user = User.objects.get(username=username)
user.groups.add(group)
@world.absorb
def clear_courses():
# Flush and initialize the module store
# Note that if your test module gets in some weird state
......
......@@ -11,7 +11,6 @@
# Disable the "unused argument" warning because lettuce uses "step"
#pylint: disable=W0613
import re
from lettuce import world, step
from .course_helpers import *
from .ui_helpers import *
......@@ -23,32 +22,15 @@ logger = getLogger(__name__)
@step(r'I wait (?:for )?"(\d+\.?\d*)" seconds?$')
def wait(step, seconds):
def wait_for_seconds(step, seconds):
world.wait(seconds)
REQUIREJS_WAIT = {
re.compile('settings-details'): [
"jquery", "js/models/course",
"js/models/settings/course_details", "js/views/settings/main"],
re.compile('settings-advanced'): [
"jquery", "js/models/course", "js/models/settings/advanced",
"js/views/settings/advanced", "codemirror"],
re.compile('edit\/.+vertical'): [
"jquery", "js/models/course", "coffee/src/models/module",
"coffee/src/views/unit", "jquery.ui"],
}
@step('I reload the page$')
def reload_the_page(step):
world.wait_for_ajax_complete()
world.browser.reload()
requirements = None
for test, req in REQUIREJS_WAIT.items():
if test.search(world.browser.url):
requirements = req
break
world.wait_for_requirejs(requirements)
world.wait_for_js_to_load()
@step('I press the browser back button$')
......@@ -163,9 +145,9 @@ def should_see_in_the_page(step, doesnt_appear, text):
else:
multiplier = 1
if doesnt_appear:
assert world.browser.is_text_not_present(text, wait_time=5*multiplier)
assert world.browser.is_text_not_present(text, wait_time=5 * multiplier)
else:
assert world.browser.is_text_present(text, wait_time=5*multiplier)
assert world.browser.is_text_present(text, wait_time=5 * multiplier)
@step('I am logged in$')
......
......@@ -4,11 +4,13 @@
from lettuce import world
import time
import json
import re
import platform
from textwrap import dedent
from urllib import quote_plus
from selenium.common.exceptions import (
WebDriverException, TimeoutException, StaleElementReferenceException)
WebDriverException, TimeoutException,
StaleElementReferenceException)
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
......@@ -16,11 +18,50 @@ from lettuce.django import django_url
from nose.tools import assert_true # pylint: disable=E0611
REQUIREJS_WAIT = {
# Settings - Schedule & Details
re.compile('^Schedule & Details Settings \|'): [
"jquery", "js/models/course",
"js/models/settings/course_details", "js/views/settings/main"],
# Settings - Advanced Settings
re.compile('^Advanced Settings \|'): [
"jquery", "js/models/course", "js/models/settings/advanced",
"js/views/settings/advanced", "codemirror"],
# Individual Unit (editing)
re.compile('^Individual Unit \|'): [
"coffee/src/models/module", "coffee/src/views/unit",
"coffee/src/views/module_edit"],
# Content - Outline
# Note that calling your org, course number, or display name, 'course' will mess this up
re.compile('^Course Outline \|'): [
"js/models/course", "js/models/location", "js/models/section",
"js/views/overview", "js/views/section_edit"],
# Dashboard
re.compile('^My Courses \|'): [
"js/sock", "gettext", "js/base",
"jquery.ui", "coffee/src/main", "underscore"],
}
@world.absorb
def wait(seconds):
time.sleep(float(seconds))
@world.absorb
def wait_for_js_to_load():
requirements = None
for test, req in REQUIREJS_WAIT.items():
if test.search(world.browser.title):
requirements = req
break
world.wait_for_requirejs(requirements)
# Selenium's `execute_async_script` function pauses Selenium's execution
# until the browser calls a specific Javascript callback; in effect,
# Selenium goes to sleep until the JS callback function wakes it back up again.
......@@ -28,8 +69,6 @@ def wait(seconds):
# passed to this callback get returned from the `execute_async_script`
# function, which allows the JS to communicate information back to Python.
# Ref: https://selenium.googlecode.com/svn/trunk/docs/api/dotnet/html/M_OpenQA_Selenium_IJavaScriptExecutor_ExecuteAsyncScript.htm
@world.absorb
def wait_for_js_variable_truthy(variable):
"""
......@@ -37,7 +76,7 @@ def wait_for_js_variable_truthy(variable):
environment until the given variable is defined and truthy. This process
guards against page reloads, and seamlessly retries on the next page.
"""
js = """
javascript = """
var callback = arguments[arguments.length - 1];
var unloadHandler = function() {{
callback("unload");
......@@ -56,7 +95,13 @@ def wait_for_js_variable_truthy(variable):
}}, 10);
""".format(variable=variable)
for _ in range(5): # 5 attempts max
result = world.browser.driver.execute_async_script(dedent(js))
try:
result = world.browser.driver.execute_async_script(dedent(javascript))
except WebDriverException as wde:
if "document unloaded while waiting for result" in wde.msg:
result = "unload"
else:
raise
if result == "unload":
# we ran this on the wrong page. Wait a bit, and try again, when the
# browser has loaded the next page.
......@@ -105,7 +150,7 @@ def wait_for_requirejs(dependencies=None):
if dependencies[0] != "jquery":
dependencies.insert(0, "jquery")
js = """
javascript = """
var callback = arguments[arguments.length - 1];
if(window.require) {{
requirejs.onError = callback;
......@@ -126,19 +171,33 @@ def wait_for_requirejs(dependencies=None):
}}
""".format(deps=json.dumps(dependencies))
for _ in range(5): # 5 attempts max
result = world.browser.driver.execute_async_script(dedent(js))
try:
result = world.browser.driver.execute_async_script(dedent(javascript))
except WebDriverException as wde:
if "document unloaded while waiting for result" in wde.msg:
result = "unload"
else:
raise
if result == "unload":
# we ran this on the wrong page. Wait a bit, and try again, when the
# browser has loaded the next page.
world.wait(1)
continue
elif result not in (None, True, False):
# we got a require.js error
msg = "Error loading dependencies: type={0} modules={1}".format(
result['requireType'], result['requireModules'])
err = RequireJSError(msg)
err.error = result
raise err
# We got a require.js error
# Sometimes requireJS will throw an error with requireType=require
# This doesn't seem to cause problems on the page, so we ignore it
if result['requireType'] == 'require':
world.wait(1)
continue
# Otherwise, fail and report the error
else:
msg = "Error loading dependencies: type={0} modules={1}".format(
result['requireType'], result['requireModules'])
err = RequireJSError(msg)
err.error = result
raise err
else:
return result
......@@ -153,7 +212,7 @@ def wait_for_ajax_complete():
keeps track of this information, go here:
http://stackoverflow.com/questions/3148225/jquery-active-function#3148506
"""
js = """
javascript = """
var callback = arguments[arguments.length - 1];
if(!window.jQuery) {callback(false);}
var intervalID = setInterval(function() {
......@@ -163,13 +222,13 @@ def wait_for_ajax_complete():
}
}, 100);
"""
world.browser.driver.execute_async_script(dedent(js))
world.browser.driver.execute_async_script(dedent(javascript))
@world.absorb
def visit(url):
world.browser.visit(django_url(url))
wait_for_requirejs()
wait_for_js_to_load()
@world.absorb
......@@ -238,11 +297,11 @@ def css_has_value(css_selector, value, index=0):
@world.absorb
def wait_for(func, timeout=5):
WebDriverWait(
driver=world.browser.driver,
timeout=timeout,
ignored_exceptions=(StaleElementReferenceException)
).until(func)
WebDriverWait(
driver=world.browser.driver,
timeout=timeout,
ignored_exceptions=(StaleElementReferenceException)
).until(func)
@world.absorb
......@@ -336,17 +395,15 @@ def css_click(css_selector, index=0, wait_time=30):
This method will return True if the click worked.
"""
wait_for_clickable(css_selector, timeout=wait_time)
assert_true(world.css_find(css_selector)[index].visible,
msg="Element {}[{}] is present but not visible".format(css_selector, index))
assert_true(
world.css_visible(css_selector, index=index),
msg="Element {}[{}] is present but not visible".format(css_selector, index)
)
# Sometimes you can't click in the center of the element, as
# another element might be on top of it. In this case, try
# clicking in the upper left corner.
try:
return retry_on_exception(lambda: world.css_find(css_selector)[index].click())
except WebDriverException:
return css_click_at(css_selector, index=index)
result = retry_on_exception(lambda: world.css_find(css_selector)[index].click())
if result:
wait_for_js_to_load()
return result
@world.absorb
......@@ -362,22 +419,6 @@ def css_check(css_selector, index=0, wait_time=30):
@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):
'''
A method to select an option
......@@ -406,6 +447,7 @@ def css_fill(css_selector, text, index=0):
@world.absorb
def click_link(partial_text, index=0):
retry_on_exception(lambda: world.browser.find_link_by_partial_text(partial_text)[index].click())
wait_for_js_to_load()
@world.absorb
......@@ -512,7 +554,6 @@ def retry_on_exception(func, max_attempts=5, ignored_exceptions=StaleElementRefe
while attempt < max_attempts:
try:
return func()
break
except ignored_exceptions:
world.wait(1)
attempt += 1
......
......@@ -11,7 +11,7 @@ from pymongo.errors import PyMongoError
from track.backends import BaseBackend
log = logging.getLogger('track.backends.mongodb')
log = logging.getLogger(__name__)
class MongoBackend(BaseBackend):
......@@ -64,14 +64,17 @@ class MongoBackend(BaseBackend):
**extra
)
self.collection = self.connection[db_name][collection_name]
database = self.connection[db_name]
if user or password:
self.collection.database.authenticate(user, password)
database.authenticate(user, password)
self.collection = database[collection_name]
self._create_indexes()
def _create_indexes(self):
"""Ensures the proper fields are indexed"""
# WARNING: The collection will be locked during the index
# creation. If the collection has a large number of
# documents in it, the operation can take a long time.
......@@ -83,8 +86,12 @@ class MongoBackend(BaseBackend):
self.collection.ensure_index('event_type')
def send(self, event):
"""Insert the event in to the Mongo collection"""
try:
self.collection.insert(event, manipulate=False)
except PyMongoError:
# The event will be lost in case of a connection error.
# pymongo will re-connect/re-authenticate automatically
# during the next event.
msg = 'Error inserting to MongoDB event tracker backend'
log.exception(msg)
......@@ -134,7 +134,7 @@ def add_histogram(user, block, view, frag, context): # pylint: disable=unused-a
return frag
block_id = block.id
if block.descriptor.has_score:
if block.has_score:
histogram = grade_histogram(block_id)
render_histogram = len(histogram) > 0
else:
......@@ -142,7 +142,7 @@ def add_histogram(user, block, view, frag, context): # pylint: disable=unused-a
render_histogram = False
if settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'):
[filepath, filename] = getattr(block.descriptor, 'xml_attributes', {}).get('filename', ['', None])
[filepath, filename] = getattr(block, 'xml_attributes', {}).get('filename', ['', None])
osfs = block.system.filestore
if filename is not None and osfs.exists(filename):
# if original, unmangled filename exists then use it (github
......@@ -163,13 +163,13 @@ def add_histogram(user, block, view, frag, context): # pylint: disable=unused-a
# TODO (ichuang): use _has_access_descriptor.can_load in lms.courseware.access, instead of now>mstart comparison here
now = datetime.datetime.now(UTC())
is_released = "unknown"
mstart = block.descriptor.start
mstart = block.start
if mstart is not None:
is_released = "<font color='red'>Yes!</font>" if (now > mstart) else "<font color='green'>Not yet</font>"
staff_context = {'fields': [(name, field.read_from(block)) for name, field in block.fields.items()],
'xml_attributes': getattr(block.descriptor, 'xml_attributes', {}),
'xml_attributes': getattr(block, 'xml_attributes', {}),
'location': block.location,
'xqa_key': block.xqa_key,
'source_file': source_file,
......
......@@ -6,7 +6,7 @@
<div class="drag_and_drop_problem_json" id="drag_and_drop_json_${id}"
style="display:none;">${drag_and_drop_json}</div>
<div class="script_placeholder" data-src="/static/js/capa/drag_and_drop.js"></div>
<div class="script_placeholder" data-src="${STATIC_URL}js/capa/drag_and_drop.js"></div>
% if status == 'unsubmitted':
<div class="unanswered" id="status_${id}">
......
......@@ -951,7 +951,7 @@ class DragAndDropTest(unittest.TestCase):
'''
def test_rendering(self):
path_to_images = '/static/images/'
path_to_images = '/dummy-static/images/'
xml_str = """
<drag_and_drop_input id="prob_1_2" img="{path}about_1.png" target_outline="false">
......@@ -978,15 +978,15 @@ class DragAndDropTest(unittest.TestCase):
user_input = { # order matters, for string comparison
"target_outline": "false",
"base_image": "/static/images/about_1.png",
"base_image": "/dummy-static/images/about_1.png",
"draggables": [
{"can_reuse": "", "label": "Label 1", "id": "1", "icon": "", "target_fields": []},
{"can_reuse": "", "label": "cc", "id": "name_with_icon", "icon": "/static/images/cc.jpg", "target_fields": []},
{"can_reuse": "", "label": "arrow-left", "id": "with_icon", "icon": "/static/images/arrow-left.png", "can_reuse": "", "target_fields": []},
{"can_reuse": "", "label": "cc", "id": "name_with_icon", "icon": "/dummy-static/images/cc.jpg", "target_fields": []},
{"can_reuse": "", "label": "arrow-left", "id": "with_icon", "icon": "/dummy-static/images/arrow-left.png", "can_reuse": "", "target_fields": []},
{"can_reuse": "", "label": "Label2", "id": "5", "icon": "", "can_reuse": "", "target_fields": []},
{"can_reuse": "", "label": "Mute", "id": "2", "icon": "/static/images/mute.png", "can_reuse": "", "target_fields": []},
{"can_reuse": "", "label": "spinner", "id": "name_label_icon3", "icon": "/static/images/spinner.gif", "can_reuse": "", "target_fields": []},
{"can_reuse": "", "label": "Star", "id": "name4", "icon": "/static/images/volume.png", "can_reuse": "", "target_fields": []},
{"can_reuse": "", "label": "Mute", "id": "2", "icon": "/dummy-static/images/mute.png", "can_reuse": "", "target_fields": []},
{"can_reuse": "", "label": "spinner", "id": "name_label_icon3", "icon": "/dummy-static/images/spinner.gif", "can_reuse": "", "target_fields": []},
{"can_reuse": "", "label": "Star", "id": "name4", "icon": "/dummy-static/images/volume.png", "can_reuse": "", "target_fields": []},
{"can_reuse": "", "label": "Label3", "id": "7", "icon": "", "can_reuse": "", "target_fields": []}],
"one_per_target": "True",
"targets": [
......
......@@ -15,7 +15,7 @@ from capa.responsetypes import StudentInputError, \
ResponseError, LoncapaProblemError
from capa.util import convert_files_to_filenames
from .progress import Progress
from xmodule.x_module import XModule
from xmodule.x_module import XModule, module_attr
from xmodule.raw_module import RawDescriptor
from xmodule.exceptions import NotFoundError, ProcessingError
from xblock.fields import Scope, String, Boolean, Dict, Integer, Float
......@@ -884,6 +884,8 @@ class CapaModule(CapaFields, XModule):
'max_value': score['total'],
})
return {'grade': score['score'], 'max_grade': score['total']}
def check_problem(self, data):
"""
Checks whether answers to a problem are correct
......@@ -951,7 +953,7 @@ class CapaModule(CapaFields, XModule):
return {'success': msg}
raise
self.publish_grade()
published_grade = self.publish_grade()
# success = correct if ALL questions in this problem are correct
success = 'correct'
......@@ -961,6 +963,8 @@ class CapaModule(CapaFields, XModule):
# NOTE: We are logging both full grading and queued-grading submissions. In the latter,
# 'success' will always be incorrect
event_info['grade'] = published_grade['grade']
event_info['max_grade'] = published_grade['max_grade']
event_info['correct_map'] = correct_map.get_dict()
event_info['success'] = success
event_info['attempts'] = self.attempts
......@@ -1193,3 +1197,33 @@ class CapaDescriptor(CapaFields, RawDescriptor):
CapaDescriptor.force_save_button, CapaDescriptor.markdown,
CapaDescriptor.text_customization])
return non_editable_fields
# Proxy to CapaModule for access to any of its attributes
answer_available = module_attr('answer_available')
check_button_name = module_attr('check_button_name')
check_problem = module_attr('check_problem')
choose_new_seed = module_attr('choose_new_seed')
closed = module_attr('closed')
get_answer = module_attr('get_answer')
get_problem = module_attr('get_problem')
get_problem_html = module_attr('get_problem_html')
get_state_for_lcp = module_attr('get_state_for_lcp')
handle_input_ajax = module_attr('handle_input_ajax')
handle_problem_html_error = module_attr('handle_problem_html_error')
handle_ungraded_response = module_attr('handle_ungraded_response')
is_attempted = module_attr('is_attempted')
is_correct = module_attr('is_correct')
is_past_due = module_attr('is_past_due')
is_submitted = module_attr('is_submitted')
lcp = module_attr('lcp')
make_dict_of_responses = module_attr('make_dict_of_responses')
new_lcp = module_attr('new_lcp')
publish_grade = module_attr('publish_grade')
rescore_problem = module_attr('rescore_problem')
reset_problem = module_attr('reset_problem')
save_problem = module_attr('save_problem')
set_state_from_lcp = module_attr('set_state_from_lcp')
should_show_check_button = module_attr('should_show_check_button')
should_show_reset_button = module_attr('should_show_reset_button')
should_show_save_button = module_attr('should_show_save_button')
update_score = module_attr('update_score')
......@@ -496,7 +496,7 @@ class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor):
metadata_translations = {
'is_graded': 'graded',
'attempts': 'max_attempts',
}
}
def get_context(self):
_context = RawDescriptor.get_context(self)
......
......@@ -18,6 +18,7 @@ log = logging.getLogger('mitx.' + __name__)
class ConditionalFields(object):
has_children = True
show_tag_list = List(help="Poll answers", scope=Scope.content)
......@@ -148,7 +149,7 @@ class ConditionalModule(ConditionalFields, XModule):
context)
return json.dumps({'html': [html], 'message': bool(message)})
html = [self.runtime.render_child(child, None, 'student_view').content for child in self.get_display_items()]
html = [child.render('student_view').content for child in self.get_display_items()]
return json.dumps({'html': html})
......
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