Commit 0fb11365 by dcadams

Merge branch 'master' of github.com:edx/edx-platform into feature-dcadams-usermanagement

parents a8ec1ca9 468dfe34
......@@ -74,3 +74,4 @@ Jason Bau <jbau@stanford.edu>
Frances Botsford <frances@edx.org>
Jonah Stanley <Jonah_Stanley@brown.edu>
Slater Victoroff <slater.r.victoroff@gmail.com>
Peter Fogg <peter.p.fogg@gmail.com>
......@@ -11,8 +11,6 @@ Feature: Advanced (manual) course policy
Given I am on the Advanced Course Settings page in Studio
Then the settings are alphabetized
# Skipped because Ubuntu ChromeDriver cannot click notification "Cancel"
@skip
Scenario: Test cancel editing key value
Given I am on the Advanced Course Settings page in Studio
When I edit the value of a policy key
......@@ -21,8 +19,6 @@ Feature: Advanced (manual) course policy
And I reload the page
Then the policy key value is unchanged
# Skipped because Ubuntu ChromeDriver cannot click notification "Save"
@skip
Scenario: Test editing key value
Given I am on the Advanced Course Settings page in Studio
When I edit the value of a policy key and save
......@@ -30,8 +26,6 @@ Feature: Advanced (manual) course policy
And I reload the page
Then the policy key value is changed
# Skipped because Ubuntu ChromeDriver cannot edit CodeMirror input
@skip
Scenario: Test how multi-line input appears
Given I am on the Advanced Course Settings page in Studio
When I create a JSON object as a value
......@@ -39,8 +33,6 @@ Feature: Advanced (manual) course policy
And I reload the page
Then it is displayed as formatted
# Skipped because Ubuntu ChromeDriver cannot edit CodeMirror input
@skip
Scenario: Test automatic quoting of non-JSON values
Given I am on the Advanced Course Settings page in Studio
When I create a non-JSON value not in quotes
......
......@@ -42,8 +42,9 @@ def edit_the_value_of_a_policy_key(step):
It is hard to figure out how to get into the CodeMirror
area, so cheat and do it from the policy key field :)
"""
e = world.css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)]
e._element.send_keys(Keys.TAB, Keys.END, Keys.ARROW_LEFT, ' ', 'X')
world.css_find(".CodeMirror")[get_index_of(DISPLAY_NAME_KEY)].click()
g = world.css_find("div.CodeMirror.CodeMirror-focused > div > textarea")
g._element.send_keys(Keys.ARROW_LEFT, ' ', 'X')
@step(u'I edit the value of a policy key and save$')
......@@ -123,10 +124,12 @@ def get_display_name_value():
def change_display_name_value(step, new_value):
e = world.css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)]
world.css_find(".CodeMirror")[get_index_of(DISPLAY_NAME_KEY)].click()
g = world.css_find("div.CodeMirror.CodeMirror-focused > div > textarea")
display_name = get_display_name_value()
for count in range(len(display_name)):
e._element.send_keys(Keys.TAB, Keys.END, Keys.BACK_SPACE)
g._element.send_keys(Keys.END, Keys.BACK_SPACE)
# Must delete "" before typing the JSON value
e._element.send_keys(Keys.TAB, Keys.END, Keys.BACK_SPACE, Keys.BACK_SPACE, new_value)
g._element.send_keys(Keys.END, Keys.BACK_SPACE, Keys.BACK_SPACE, new_value)
press_the_notification_button(step, "Save")
......@@ -161,3 +161,11 @@ def i_created_a_video_component(step):
'i4x://edx/templates/video/default',
'.xmodule_VideoModule'
)
@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')
world.css_click('a.new-unit-item')
......@@ -14,20 +14,27 @@ def create_component_instance(step, component_button_css, instance_id, expected_
@world.absorb
def click_new_component_button(step, component_button_css):
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')
world.css_click('a.new-unit-item')
step.given('I have clicked the new unit button')
world.css_click(component_button_css)
@world.absorb
def click_component_from_menu(instance_id, expected_css):
"""
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.
"""
elem_css = "a[data-location='%s']" % instance_id
assert_equal(1, len(world.css_find(elem_css)))
world.css_click(elem_css)
elements = world.css_find(elem_css)
assert(len(elements) == 1)
if elements[0]['id'] == instance_id: # If this is a component with multiple templates
world.css_click(elem_css)
assert_equal(1, len(world.css_find(expected_css)))
@world.absorb
def edit_component_and_select_settings():
world.css_click('a.edit-button')
......
......@@ -11,3 +11,7 @@ Feature: 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
......@@ -21,3 +21,10 @@ def i_see_only_the_settings_and_values(step):
['Display Name', "Discussion Tag", True],
['Subcategory', "Topic-Level Student-Visible Label", True]
])
@step('creating a discussion takes a single click')
def discussion_takes_a_single_click(step):
assert(not world.is_css_present('.xmodule_DiscussionModule'))
world.css_click("a[data-location='i4x://edx/templates/discussion/Discussion_Tag']")
assert(world.is_css_present('.xmodule_DiscussionModule'))
......@@ -4,3 +4,7 @@ Feature: Video Component
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
Scenario: Creating a video takes a single click
Given I have clicked the new unit button
Then creating a video takes a single click
......@@ -9,3 +9,10 @@ from lettuce import world, step
def does_not_autoplay(step):
assert world.css_find('.video')[0]['data-autoplay'] == 'False'
assert world.css_find('.video_control')[0].has_class('play')
@step('creating a video takes a single click')
def video_takes_a_single_click(step):
assert(not world.is_css_present('.xmodule_VideoModule'))
world.css_click("a[data-location='i4x://edx/templates/video/default']")
assert(world.is_css_present('.xmodule_VideoModule'))
......@@ -77,14 +77,25 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.client = Client()
self.client.login(username=uname, password=password)
def test_advanced_components_in_edit_unit(self):
def check_components_on_page(self, component_types, expected_types):
"""
Ensure that the right types end up on the page.
component_types is the list of advanced components.
expected_types is the list of elements that should appear on the page.
expected_types and component_types should be similar, but not
exactly the same -- for example, 'videoalpha' in
component_types should cause 'Video Alpha' to be present.
"""
store = modulestore('direct')
import_from_xml(store, 'common/test/data/', ['simple'])
course = store.get_item(Location(['i4x', 'edX', 'simple',
'course', '2012_Fall', None]), depth=None)
course.advanced_modules = ADVANCED_COMPONENT_TYPES
course.advanced_modules = component_types
store.update_metadata(course.location, own_metadata(course))
......@@ -94,13 +105,20 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
resp = self.client.get(reverse('edit_unit', kwargs={'location': descriptor.location.url()}))
self.assertEqual(resp.status_code, 200)
for expected in expected_types:
self.assertIn(expected, resp.content)
def test_advanced_components_in_edit_unit(self):
# This could be made better, but for now let's just assert that we see the advanced modules mentioned in the page
# response HTML
self.assertIn('Video Alpha', resp.content)
self.assertIn('Word cloud', resp.content)
self.assertIn('Annotation', resp.content)
self.assertIn('Open Ended Response', resp.content)
self.assertIn('Peer Grading Interface', resp.content)
self.check_components_on_page(ADVANCED_COMPONENT_TYPES, ['Video Alpha',
'Word cloud',
'Annotation',
'Open Ended Response',
'Peer Grading Interface'])
def test_advanced_components_require_two_clicks(self):
self.check_components_on_page(['videoalpha'], ['Video Alpha'])
def check_edit_unit(self, test_course_name):
import_from_xml(modulestore('direct'), 'common/test/data/', [test_course_name])
......
......@@ -91,6 +91,12 @@ CACHES = ENV_TOKENS['CACHES']
SESSION_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN')
# allow for environments to specify what cookie name our login subsystem should use
# this is to fix a bug regarding simultaneous logins between edx.org and edge.edx.org which can
# happen with some browsers (e.g. Firefox)
if ENV_TOKENS.get('SESSION_COOKIE_NAME', None):
SESSION_COOKIE_NAME = ENV_TOKENS.get('SESSION_COOKIE_NAME')
#Email overrides
DEFAULT_FROM_EMAIL = ENV_TOKENS.get('DEFAULT_FROM_EMAIL', DEFAULT_FROM_EMAIL)
DEFAULT_FEEDBACK_EMAIL = ENV_TOKENS.get('DEFAULT_FEEDBACK_EMAIL', DEFAULT_FEEDBACK_EMAIL)
......
class CMS.Views.UnitEdit extends Backbone.View
events:
'click .new-component .new-component-type a': 'showComponentTemplates'
'click .new-component .new-component-type a.multiple-templates': 'showComponentTemplates'
'click .new-component .new-component-type a.single-template': 'saveNewComponent'
'click .new-component .cancel-button': 'closeNewComponent'
'click .new-component-templates .new-component-template a': 'saveNewComponent'
'click .new-component-templates .cancel-button': 'closeNewComponent'
......
......@@ -53,9 +53,15 @@
<div class="new-component">
<h5>Add New Component</h5>
<ul class="new-component-type">
% for type in sorted(component_templates.keys()):
% for type, templates in sorted(component_templates.items()):
<li>
<a href="#" data-type="${type}">
% if type == 'advanced' or len(templates) > 1:
<a href="#" class="multiple-templates" data-type="${type}">
% else:
% for _, location, _ in templates:
<a href="#" class="single-template" data-type="${type}" data-location="${location}">
% endfor
% endif
<span class="large-template-icon large-${type}-icon"></span>
<span class="name">${type}</span>
</a>
......@@ -64,50 +70,52 @@
</ul>
</div>
% for type, templates in sorted(component_templates.items()):
% if len(templates) > 1 or type == 'advanced':
<div class="new-component-templates new-component-${type}">
% if type == "problem":
<div class="tab-group tabs">
<ul class="problem-type-tabs nav-tabs">
<li class="current">
<a class="link-tab" href="#tab1">Common Problem Types</a>
</li>
<li>
<a class="link-tab" href="#tab2">Advanced</a>
</li>
</ul>
% endif
<div class="tab current" id="tab1">
<ul class="new-component-template">
% for name, location, has_markdown in templates:
% if has_markdown or type != "problem":
<li class="editor-md">
<a href="#" id="${location}" data-location="${location}">
<span class="name"> ${name}</span>
</a>
</li>
% endif
%endfor
</ul>
</div>
% if type == "problem":
<div class="tab" id="tab2">
<ul class="new-component-template">
% for name, location, has_markdown in templates:
% if not has_markdown:
<li class="editor-manual">
<a href="#" id="${location}" data-location="${location}">
<span class="name"> ${name}</span>
</a>
</li>
% endif
% endfor
<div class="tab-group tabs">
<ul class="problem-type-tabs nav-tabs">
<li class="current">
<a class="link-tab" href="#tab1">Common Problem Types</a>
</li>
<li>
<a class="link-tab" href="#tab2">Advanced</a>
</li>
</ul>
</div>
</div>
% endif
<a href="#" class="cancel-button">Cancel</a>
</div>
% endif
<div class="tab current" id="tab1">
<ul class="new-component-template">
% for name, location, has_markdown in templates:
% if has_markdown or type != "problem":
<li class="editor-md">
<a href="#" id="${location}" data-location="${location}">
<span class="name"> ${name}</span>
</a>
</li>
% endif
%endfor
</ul>
</div>
% if type == "problem":
<div class="tab" id="tab2">
<ul class="new-component-template">
% for name, location, has_markdown in templates:
% if not has_markdown:
<li class="editor-manual">
<a href="#" id="${location}" data-location="${location}">
<span class="name"> ${name}</span>
</a>
</li>
% endif
% endfor
</ul>
</div>
</div>
% endif
<a href="#" class="cancel-button">Cancel</a>
</div>
% endif
% endfor
</li>
</ol>
......
......@@ -44,7 +44,11 @@ class MakoLoader(object):
if source.startswith("## mako\n"):
# This is a mako template
template = Template(filename=file_path, module_directory=self.module_directory, uri=template_name)
template = Template(filename=file_path,
module_directory=self.module_directory,
input_encoding='utf-8',
output_encoding='utf-8',
uri=template_name)
return template, None
else:
# This is a regular template
......
......@@ -14,6 +14,7 @@ from django.core.management.base import NoArgsCommand
from django.conf import settings
from mako.template import Template
import textwrap
class Command(NoArgsCommand):
"""
......@@ -61,5 +62,12 @@ class Command(NoArgsCommand):
result in `outfile`.
"""
with open(outfile, "w") as _outfile:
_outfile.write(textwrap.dedent("""\
/*
* This file is dynamically generated and ignored by Git.
* DO NOT MAKE CHANGES HERE. Instead, go edit its template:
* %s
*/
""" % infile))
_outfile.write(Template(filename=str(infile)).render(env=self.__context()))
......@@ -87,8 +87,8 @@ def reset_data(scenario):
LOGGER.debug("Flushing the test database...")
call_command('flush', interactive=False)
@after.each_scenario
# Uncomment below to trigger a screenshot on error
# @after.each_scenario
def screenshot_on_error(scenario):
"""
Save a screenshot to help with debugging.
......
......@@ -121,7 +121,7 @@ class ErrorDescriptor(ErrorFields, JSONEditingDescriptor):
def from_descriptor(cls, descriptor, error_msg='Error not available'):
return cls._construct(
descriptor.system,
descriptor._model_data,
str(descriptor),
error_msg,
location=descriptor.location,
)
......
......@@ -32,6 +32,8 @@ class HtmlModule(HtmlFields, XModule):
css = {'scss': [resource_string(__name__, 'css/html/display.scss')]}
def get_html(self):
if self.system.anonymous_student_id:
return self.data.replace("%%USER_ID%%", self.system.anonymous_student_id)
return self.data
......
......@@ -4,6 +4,9 @@ Tests for ErrorModule and NonStaffErrorModule
import unittest
from xmodule.tests import test_system
import xmodule.error_module as error_module
from xmodule.modulestore import Location
from xmodule.x_module import XModuleDescriptor
from mock import MagicMock
class TestErrorModule(unittest.TestCase):
......@@ -14,22 +17,33 @@ class TestErrorModule(unittest.TestCase):
self.system = test_system()
self.org = "org"
self.course = "course"
self.fake_xml = "<problem />"
self.location = Location(['i4x', self.org, self.course, None, None])
self.valid_xml = "<problem />"
self.broken_xml = "<problem>"
self.error_msg = "Error"
def test_error_module_create(self):
def test_error_module_xml_rendering(self):
descriptor = error_module.ErrorDescriptor.from_xml(
self.fake_xml, self.system, self.org, self.course)
self.valid_xml, self.system, self.org, self.course, self.error_msg)
self.assertTrue(isinstance(descriptor, error_module.ErrorDescriptor))
def test_error_module_rendering(self):
descriptor = error_module.ErrorDescriptor.from_xml(
self.fake_xml, self.system, self.org, self.course, self.error_msg)
module = descriptor.xmodule(self.system)
rendered_html = module.get_html()
self.assertIn(self.error_msg, rendered_html)
self.assertIn(self.fake_xml, rendered_html)
self.assertIn(self.valid_xml, rendered_html)
def test_error_module_from_descriptor(self):
descriptor = MagicMock([XModuleDescriptor],
system=self.system,
location=self.location,
_model_data=self.valid_xml)
error_descriptor = error_module.ErrorDescriptor.from_descriptor(
descriptor, self.error_msg)
self.assertTrue(isinstance(error_descriptor, error_module.ErrorDescriptor))
module = error_descriptor.xmodule(self.system)
rendered_html = module.get_html()
self.assertIn(self.error_msg, rendered_html)
self.assertIn(str(descriptor), rendered_html)
class TestNonStaffErrorModule(TestErrorModule):
......@@ -39,13 +53,27 @@ class TestNonStaffErrorModule(TestErrorModule):
def test_non_staff_error_module_create(self):
descriptor = error_module.NonStaffErrorDescriptor.from_xml(
self.fake_xml, self.system, self.org, self.course)
self.valid_xml, self.system, self.org, self.course)
self.assertTrue(isinstance(descriptor, error_module.NonStaffErrorDescriptor))
def test_non_staff_error_module_rendering(self):
def test_from_xml_render(self):
descriptor = error_module.NonStaffErrorDescriptor.from_xml(
self.fake_xml, self.system, self.org, self.course)
self.valid_xml, self.system, self.org, self.course)
module = descriptor.xmodule(self.system)
rendered_html = module.get_html()
self.assertNotIn(self.error_msg, rendered_html)
self.assertNotIn(self.fake_xml, rendered_html)
self.assertNotIn(self.valid_xml, rendered_html)
def test_error_module_from_descriptor(self):
descriptor = MagicMock([XModuleDescriptor],
system=self.system,
location=self.location,
_model_data=self.valid_xml)
error_descriptor = error_module.NonStaffErrorDescriptor.from_descriptor(
descriptor, self.error_msg)
self.assertTrue(isinstance(error_descriptor, error_module.ErrorDescriptor))
module = error_descriptor.xmodule(self.system)
rendered_html = module.get_html()
self.assertNotIn(self.error_msg, rendered_html)
self.assertNotIn(str(descriptor), rendered_html)
import unittest
from mock import Mock
from xmodule.html_module import HtmlModule
from xmodule.modulestore import Location
from . import test_system
class HtmlModuleSubstitutionTestCase(unittest.TestCase):
location = Location(["i4x", "edX", "toy", "html", "simple_html"])
descriptor = Mock()
def test_substitution_works(self):
sample_xml = '''%%USER_ID%%'''
module_data = {'data': sample_xml}
module_system = test_system()
module = HtmlModule(module_system, self.location, self.descriptor, module_data)
self.assertEqual(module.get_html(), str(module_system.anonymous_student_id))
def test_substitution_without_magic_string(self):
sample_xml = '''
<html>
<p>Hi USER_ID!11!</p>
</html>
'''
module_data = {'data': sample_xml}
module = HtmlModule(test_system(), self.location, self.descriptor, module_data)
self.assertEqual(module.get_html(), sample_xml)
def test_substitution_without_anonymous_student_id(self):
sample_xml = '''%%USER_ID%%'''
module_data = {'data': sample_xml}
module_system = test_system()
module_system.anonymous_student_id = None
module = HtmlModule(module_system, self.location, self.descriptor, module_data)
self.assertEqual(module.get_html(), sample_xml)
......@@ -335,9 +335,8 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours
else:
err_descriptor_class = NonStaffErrorDescriptor
err_descriptor = err_descriptor_class.from_xml(
str(descriptor), descriptor.system,
org=descriptor.location.org, course=descriptor.location.course,
err_descriptor = err_descriptor_class.from_descriptor(
descriptor,
error_msg=exc_info_to_str(sys.exc_info())
)
......
......@@ -102,6 +102,12 @@ with open(ENV_ROOT / CONFIG_PREFIX + "env.json") as env_file:
SITE_NAME = ENV_TOKENS['SITE_NAME']
SESSION_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN')
# allow for environments to specify what cookie name our login subsystem should use
# this is to fix a bug regarding simultaneous logins between edx.org and edge.edx.org which can
# happen with some browsers (e.g. Firefox)
if ENV_TOKENS.get('SESSION_COOKIE_NAME', None):
SESSION_COOKIE_NAME = ENV_TOKENS.get('SESSION_COOKIE_NAME')
BOOK_URL = ENV_TOKENS['BOOK_URL']
MEDIA_URL = ENV_TOKENS['MEDIA_URL']
LOG_DIR = ENV_TOKENS['LOG_DIR']
......
......@@ -62,7 +62,8 @@ nav.course-material {
header.global.slim {
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1));
height: 50px;
height: auto;
padding: 5px 0 10px 0;
border-bottom: 1px solid $outer-border-color;
background: $white;
......@@ -106,16 +107,15 @@ header.global.slim {
padding-top: 5px;
}
h1.logo {
margin-left: 13px;
margin-right: 20px;
h1.logo {
margin: 0 10px 0 13px;
padding-right: 20px;
&:before {
@extend .faded-vertical-divider;
content: "";
display: block;
height: 40px;
height: 35px;
position: absolute;
right: 3px;
top: 0;
......@@ -126,12 +126,16 @@ header.global.slim {
@extend .faded-vertical-divider-light;
content: "";
display: block;
height: 40px;
height: 35px;
position: absolute;
right: 0px;
right: 0;
top: 0;
width: 1px;
}
img {
height: 30px;
}
}
.nav-global {
......@@ -147,6 +151,7 @@ header.global.slim {
color: #777;
letter-spacing: 0;
margin-top: 9px;
margin-bottom: 0;
text-transform: none;
text-shadow: 0 1px 0 #fff;
white-space: nowrap;
......@@ -168,4 +173,4 @@ header.global.slim {
font-weight: bold;
letter-spacing: 0;
}
}
\ No newline at end of file
}
......@@ -39,7 +39,7 @@ site_status_msg = get_site_status_msg(course_id)
<h1 class="logo">
<a href="${marketing_link('ROOT')}">
<img src="${static.url(branding.get_logo_url(request.META.get('HTTP_HOST')))}"/>
<img src="${static.url(branding.get_logo_url(request.META.get('HTTP_HOST')))}" alt="edX home" />
</a></h1>
% if course:
......
......@@ -27,10 +27,11 @@ def coffee_cmd(watch=false, debug=false)
#
# Ref: https://github.com/joyent/node/issues/2479
#
# Instead, watch 50 files per process in parallel
# Rather than watching all of the directories in one command
# watch each static files subdirectory separately
cmds = []
Dir['*/static/**/*.coffee'].each_slice(50) do |coffee_files|
cmds << "node_modules/.bin/coffee --watch --compile #{coffee_files.join(' ')}"
['lms/static/coffee', 'cms/static/coffee', 'common/static/coffee', 'common/static/xmodule'].each do |coffee_folder|
cmds << "node_modules/.bin/coffee --watch --compile #{coffee_folder}"
end
cmds
else
......@@ -119,12 +120,15 @@ namespace :assets do
namespace :sass do
# In watch mode, sass doesn't immediately compile out of date files,
# so force a recompile first
task :_watch => 'assets:sass:debug'
# Also force xmodule files to be generated before we start watching anything
task :_watch => ['assets:sass:debug', 'assets:xmodule']
multitask :debug => 'assets:xmodule:debug'
end
multitask :coffee => 'assets:xmodule'
namespace :coffee do
# Force xmodule files to be generated before we start watching anything
task :_watch => 'assets:xmodule'
multitask :debug => 'assets:xmodule:debug'
end
end
......
numpy==1.6.2
networkx==1.7
sympy==0.7.1
\ No newline at end of file
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