Commit cce65ce9 by Peter Fogg

Merge branch 'master' of github.com:edx/edx-platform into peter-fogg/single-click-video-creation

parents 68581610 babe46e6
...@@ -72,3 +72,4 @@ Giulio Gratta <giulio@giuliogratta.com> ...@@ -72,3 +72,4 @@ Giulio Gratta <giulio@giuliogratta.com>
David Baumgold <david@davidbaumgold.com> David Baumgold <david@davidbaumgold.com>
Jason Bau <jbau@stanford.edu> Jason Bau <jbau@stanford.edu>
Frances Botsford <frances@edx.org> Frances Botsford <frances@edx.org>
Slater Victoroff <slater.r.victoroff@gmail.com>
This is edX, a platform for online course delivery. The project is primarily This is the main edX platform which consists of LMS and Studio.
written in [Python](http://python.org/), using the
[Django](https://www.djangoproject.com/) framework. We also use some See [code.edx.org](http://code.edx.org/) for other parts of the edX code base.
[Ruby](http://www.ruby-lang.org/) and some [NodeJS](http://nodejs.org/).
Installation Installation
============ ============
The installation process is a bit messy at the moment. Here's a high-level
overview of what you should do to get started.
**TLDR:** There is a `scripts/create-dev-env.sh` script that will attempt to set all There is a `scripts/create-dev-env.sh` that will attempt to set up a development
of this up for you. If you're in a hurry, run that script. Otherwise, I suggest environment.
that you understand what the script is doing, and why, by reading this document.
If you want to better understand what the script is doing, keep reading.
Directory Hierarchy Directory Hierarchy
------------------- -------------------
This code assumes that it is checked out in a directory that has three sibling This code assumes that it is checked out in a directory that has three sibling
directories: `data` (used for XML course data), `db` (used to hold a directories: `data` (used for XML course data), `db` (used to hold a
[sqlite](https://sqlite.org/) database), and `log` (used to hold logs). If you [sqlite](https://sqlite.org/) database), and `log` (used to hold logs). If you
...@@ -77,6 +76,7 @@ environment), and Node has a library installer called ...@@ -77,6 +76,7 @@ environment), and Node has a library installer called
Once you've got your languages and virtual environments set up, install Once you've got your languages and virtual environments set up, install
the libraries like so: the libraries like so:
$ pip install -r requirements/edx/pre.txt
$ pip install -r requirements/edx/base.txt $ pip install -r requirements/edx/base.txt
$ pip install -r requirements/edx/post.txt $ pip install -r requirements/edx/post.txt
$ bundle install $ bundle install
...@@ -144,10 +144,28 @@ in the `data` directory, instead of in Mongo. To run this older version, run: ...@@ -144,10 +144,28 @@ in the `data` directory, instead of in Mongo. To run this older version, run:
$ rake lms $ rake lms
Further Documentation License
===================== -------
Once you've got your project up and running, you can check out the `docs`
directory to see more documentation about how edX is structured. The code in this repository is licensed under version 3 of the AGPL unless
otherwise noted.
Please see ``LICENSE.txt`` for details.
How to Contribute
-----------------
Contributions are very welcome. The easiest way is to fork this repo, and then
make a pull request from your fork. The first time you make a pull request, you
may be asked to sign a Contributor Agreement.
Reporting Security Issues
-------------------------
Please do not report security issues in public. Please email security@edx.org
Mailing List and IRC Channel
----------------------------
You can discuss this code on the [edx-code Google Group](https://groups.google.com/forum/#!forum/edx-code) or in the
`edx-code` IRC channel on Freenode.
...@@ -27,3 +27,59 @@ def click_component_from_menu(instance_id, expected_css): ...@@ -27,3 +27,59 @@ def click_component_from_menu(instance_id, expected_css):
assert_equal(1, len(world.css_find(elem_css))) assert_equal(1, len(world.css_find(elem_css)))
world.css_click(elem_css) world.css_click(elem_css)
assert_equal(1, len(world.css_find(expected_css))) assert_equal(1, len(world.css_find(expected_css)))
@world.absorb
def edit_component_and_select_settings():
world.css_click('a.edit-button')
world.css_click('#settings-mode')
@world.absorb
def verify_setting_entry(setting, display_name, value, explicitly_set):
assert_equal(display_name, setting.find_by_css('.setting-label')[0].value)
assert_equal(value, setting.find_by_css('.setting-input')[0].value)
settingClearButton = setting.find_by_css('.setting-clear')[0]
assert_equal(explicitly_set, settingClearButton.has_class('active'))
assert_equal(not explicitly_set, settingClearButton.has_class('inactive'))
@world.absorb
def verify_all_setting_entries(expected_entries):
settings = world.browser.find_by_css('.wrapper-comp-setting')
assert_equal(len(expected_entries), len(settings))
for (counter, setting) in enumerate(settings):
world.verify_setting_entry(
setting, expected_entries[counter][0],
expected_entries[counter][1], expected_entries[counter][2]
)
@world.absorb
def save_component_and_reopen(step):
world.css_click("a.save-button")
# We have a known issue that modifications are still shown within the edit window after cancel (though)
# they are not persisted. Refresh the browser to make sure the changes WERE persisted after Save.
reload_the_page(step)
edit_component_and_select_settings()
@world.absorb
def cancel_component(step):
world.css_click("a.cancel-button")
# We have a known issue that modifications are still shown within the edit window after cancel (though)
# they are not persisted. Refresh the browser to make sure the changes were not persisted.
reload_the_page(step)
@world.absorb
def revert_setting_entry(label):
get_setting_entry(label).find_by_css('.setting-clear')[0].click()
@world.absorb
def get_setting_entry(label):
settings = world.browser.find_by_css('.wrapper-comp-setting')
for setting in settings:
if setting.find_by_css('.setting-label')[0].value == label:
return setting
return None
Feature: Discussion Component Editor
As a course author, I want to be able to create discussion components.
Scenario: User can view metadata
Given I have created a Discussion Tag
And I edit and select Settings
Then I see three alphabetized settings and their expected values
Scenario: User can modify display name
Given I have created a Discussion Tag
And I edit and select Settings
Then I can modify the display name
And my display name change is persisted on save
# disable missing docstring
#pylint: disable=C0111
from lettuce import world, step
@step('I have created a Discussion Tag$')
def i_created_discussion_tag(step):
world.create_component_instance(
step, '.large-discussion-icon',
'i4x://edx/templates/discussion/Discussion_Tag',
'.xmodule_DiscussionModule'
)
@step('I see three alphabetized settings and their expected values$')
def i_see_only_the_settings_and_values(step):
world.verify_all_setting_entries(
[
['Category', "Week 1", True],
['Display Name', "Discussion Tag", True],
['Subcategory', "Topic-Level Student-Visible Label", True]
])
Feature: HTML Editor
As a course author, I want to be able to create HTML blocks.
Scenario: User can view metadata
Given I have created a Blank HTML Page
And I edit and select Settings
Then I see only the HTML display name setting
Scenario: User can modify display name
Given I have created a Blank HTML Page
And I edit and select Settings
Then I can modify the display name
And my display name change is persisted on save
# disable missing docstring
#pylint: disable=C0111
from lettuce import world, step
@step('I have created a Blank HTML Page$')
def i_created_blank_html_page(step):
world.create_component_instance(
step, '.large-html-icon', 'i4x://edx/templates/html/Blank_HTML_Page',
'.xmodule_HtmlModule'
)
@step('I see only the HTML display name setting$')
def i_see_only_the_html_display_name(step):
world.verify_all_setting_entries([['Display Name', "Blank HTML Page", True]])
Feature: Problem Editor
As a course author, I want to be able to create problems and edit their settings.
Scenario: User can view metadata
Given I have created a Blank Common Problem
And I edit and select Settings
Then I see five alphabetized settings and their expected values
And Edit High Level Source is not visible
Scenario: User can modify String values
Given I have created a Blank Common Problem
And I edit and select Settings
Then I can modify the display name
And my display name change is persisted on save
Scenario: User can specify special characters in String values
Given I have created a Blank Common Problem
And I edit and select Settings
Then I can specify special characters in the display name
And my special characters and persisted on save
Scenario: User can revert display name to unset
Given I have created a Blank Common Problem
And I edit and select Settings
Then I can revert the display name to unset
And my display name is unset on save
Scenario: User can select values in a Select
Given I have created a Blank Common Problem
And I edit and select Settings
Then I can select Per Student for Randomization
And my change to randomization is persisted
And I can revert to the default value for randomization
Scenario: User can modify float input values
Given I have created a Blank Common Problem
And I edit and select Settings
Then I can set the weight to "3.5"
And my change to weight is persisted
And I can revert to the default value of unset for weight
Scenario: User cannot type letters in float number field
Given I have created a Blank Common Problem
And I edit and select Settings
Then if I set the weight to "abc", it remains unset
Scenario: User cannot type decimal values integer number field
Given I have created a Blank Common Problem
And I edit and select Settings
Then if I set the max attempts to "2.34", it displays initially as "234", and is persisted as "234"
Scenario: User cannot type out of range values in an integer number field
Given I have created a Blank Common Problem
And I edit and select Settings
Then if I set the max attempts to "-3", it displays initially as "-3", and is persisted as "1"
Scenario: Settings changes are not saved on Cancel
Given I have created a Blank Common Problem
And I edit and select Settings
Then I can set the weight to "3.5"
And I can modify the display name
Then If I press Cancel my changes are not persisted
Scenario: Edit High Level source is available for LaTeX problem
Given I have created a LaTeX Problem
And I edit and select Settings
Then Edit High Level Source is visible
# disable missing docstring
#pylint: disable=C0111
from lettuce import world, step
from nose.tools import assert_equal
DISPLAY_NAME = "Display Name"
MAXIMUM_ATTEMPTS = "Maximum Attempts"
PROBLEM_WEIGHT = "Problem Weight"
RANDOMIZATION = 'Randomization'
SHOW_ANSWER = "Show Answer"
############### ACTIONS ####################
@step('I have created a Blank Common Problem$')
def i_created_blank_common_problem(step):
world.create_component_instance(
step,
'.large-problem-icon',
'i4x://edx/templates/problem/Blank_Common_Problem',
'.xmodule_CapaModule'
)
@step('I edit and select Settings$')
def i_edit_and_select_settings(step):
world.edit_component_and_select_settings()
@step('I see five alphabetized settings and their expected values$')
def i_see_five_settings_with_values(step):
world.verify_all_setting_entries(
[
[DISPLAY_NAME, "Blank Common Problem", True],
[MAXIMUM_ATTEMPTS, "", False],
[PROBLEM_WEIGHT, "", False],
[RANDOMIZATION, "Never", True],
[SHOW_ANSWER, "Finished", True]
])
@step('I can modify the display name')
def i_can_modify_the_display_name(step):
world.get_setting_entry(DISPLAY_NAME).find_by_css('.setting-input')[0].fill('modified')
verify_modified_display_name()
@step('my display name change is persisted on save')
def my_display_name_change_is_persisted_on_save(step):
world.save_component_and_reopen(step)
verify_modified_display_name()
@step('I can specify special characters in the display name')
def i_can_modify_the_display_name_with_special_chars(step):
world.get_setting_entry(DISPLAY_NAME).find_by_css('.setting-input')[0].fill("updated ' \" &")
verify_modified_display_name_with_special_chars()
@step('my special characters and persisted on save')
def special_chars_persisted_on_save(step):
world.save_component_and_reopen(step)
verify_modified_display_name_with_special_chars()
@step('I can revert the display name to unset')
def can_revert_display_name_to_unset(step):
world.revert_setting_entry(DISPLAY_NAME)
verify_unset_display_name()
@step('my display name is unset on save')
def my_display_name_is_persisted_on_save(step):
world.save_component_and_reopen(step)
verify_unset_display_name()
@step('I can select Per Student for Randomization')
def i_can_select_per_student_for_randomization(step):
world.browser.select(RANDOMIZATION, "Per Student")
verify_modified_randomization()
@step('my change to randomization is persisted')
def my_change_to_randomization_is_persisted(step):
world.save_component_and_reopen(step)
verify_modified_randomization()
@step('I can revert to the default value for randomization')
def i_can_revert_to_default_for_randomization(step):
world.revert_setting_entry(RANDOMIZATION)
world.save_component_and_reopen(step)
world.verify_setting_entry(world.get_setting_entry(RANDOMIZATION), RANDOMIZATION, "Always", False)
@step('I can set the weight to "(.*)"?')
def i_can_set_weight(step, weight):
set_weight(weight)
verify_modified_weight()
@step('my change to weight is persisted')
def my_change_to_weight_is_persisted(step):
world.save_component_and_reopen(step)
verify_modified_weight()
@step('I can revert to the default value of unset for weight')
def i_can_revert_to_default_for_unset_weight(step):
world.revert_setting_entry(PROBLEM_WEIGHT)
world.save_component_and_reopen(step)
world.verify_setting_entry(world.get_setting_entry(PROBLEM_WEIGHT), PROBLEM_WEIGHT, "", False)
@step('if I set the weight to "(.*)", it remains unset')
def set_the_weight_to_abc(step, bad_weight):
set_weight(bad_weight)
# We show the clear button immediately on type, hence the "True" here.
world.verify_setting_entry(world.get_setting_entry(PROBLEM_WEIGHT), PROBLEM_WEIGHT, "", True)
world.save_component_and_reopen(step)
# But no change was actually ever sent to the model, so on reopen, explicitly_set is False
world.verify_setting_entry(world.get_setting_entry(PROBLEM_WEIGHT), PROBLEM_WEIGHT, "", False)
@step('if I set the max attempts to "(.*)", it displays initially as "(.*)", and is persisted as "(.*)"')
def set_the_max_attempts(step, max_attempts_set, max_attempts_displayed, max_attempts_persisted):
world.get_setting_entry(MAXIMUM_ATTEMPTS).find_by_css('.setting-input')[0].fill(max_attempts_set)
world.verify_setting_entry(world.get_setting_entry(MAXIMUM_ATTEMPTS), MAXIMUM_ATTEMPTS, max_attempts_displayed, True)
world.save_component_and_reopen(step)
world.verify_setting_entry(world.get_setting_entry(MAXIMUM_ATTEMPTS), MAXIMUM_ATTEMPTS, max_attempts_persisted, True)
@step('Edit High Level Source is not visible')
def edit_high_level_source_not_visible(step):
verify_high_level_source(step, False)
@step('Edit High Level Source is visible')
def edit_high_level_source_visible(step):
verify_high_level_source(step, True)
@step('If I press Cancel my changes are not persisted')
def cancel_does_not_save_changes(step):
world.cancel_component(step)
step.given("I edit and select Settings")
step.given("I see five alphabetized settings and their expected values")
@step('I have created a LaTeX Problem')
def create_latex_problem(step):
world.click_new_component_button(step, '.large-problem-icon')
# Go to advanced tab (waiting for the tab to be visible)
world.css_find('#ui-id-2')
world.css_click('#ui-id-2')
world.click_component_from_menu("i4x://edx/templates/problem/Problem_Written_in_LaTeX", '.xmodule_CapaModule')
def verify_high_level_source(step, visible):
assert_equal(visible, world.is_css_present('.launch-latex-compiler'))
world.cancel_component(step)
assert_equal(visible, world.is_css_present('.upload-button'))
def verify_modified_weight():
world.verify_setting_entry(world.get_setting_entry(PROBLEM_WEIGHT), PROBLEM_WEIGHT, "3.5", True)
def verify_modified_randomization():
world.verify_setting_entry(world.get_setting_entry(RANDOMIZATION), RANDOMIZATION, "Per Student", True)
def verify_modified_display_name():
world.verify_setting_entry(world.get_setting_entry(DISPLAY_NAME), DISPLAY_NAME, 'modified', True)
def verify_modified_display_name_with_special_chars():
world.verify_setting_entry(world.get_setting_entry(DISPLAY_NAME), DISPLAY_NAME, "updated ' \" &", True)
def verify_unset_display_name():
world.verify_setting_entry(world.get_setting_entry(DISPLAY_NAME), DISPLAY_NAME, '', False)
def set_weight(weight):
world.get_setting_entry(PROBLEM_WEIGHT).find_by_css('.setting-input')[0].fill(weight)
...@@ -10,9 +10,7 @@ from nose.tools import assert_equal ...@@ -10,9 +10,7 @@ from nose.tools import assert_equal
@step('I have opened a new course section in Studio$') @step('I have opened a new course section in Studio$')
def i_have_opened_a_new_course_section(step): def i_have_opened_a_new_course_section(step):
world.clear_courses() open_new_course()
log_into_studio()
create_a_course()
add_section() add_section()
......
Feature: Video Component Editor
As a course author, I want to be able to create video components.
Scenario: User can view metadata
Given I have created a Video component
And I edit and select Settings
Then I see only the Video display name setting
Scenario: User can modify display name
Given I have created a Video component
And I edit and select Settings
Then I can modify the display name
And my display name change is persisted on save
# disable missing docstring
#pylint: disable=C0111
from lettuce import world, step
@step('I see only the video display name setting$')
def i_see_only_the_video_display_name(step):
world.verify_all_setting_entries([['Display Name', "default", True]])
...@@ -3,4 +3,4 @@ Feature: Video Component ...@@ -3,4 +3,4 @@ Feature: Video Component
Scenario: Autoplay is disabled in Studio Scenario: Autoplay is disabled in Studio
Given I have created a Video component Given I have created a Video component
Then when I view it it does not autoplay Then when I view the video it does not have autoplay enabled
\ No newline at end of file
#pylint: disable=C0111 #pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from common import *
############### ACTIONS #################### ############### ACTIONS ####################
@step('when I view it it does not autoplay') @step('when I view the video it does not have autoplay enabled')
def does_not_autoplay(step): def does_not_autoplay(step):
assert world.css_find('.video')[0]['data-autoplay'] == 'False' assert world.css_find('.video')[0]['data-autoplay'] == 'False'
assert world.css_find('.video_control')[0].has_class('play') assert world.css_find('.video_control')[0].has_class('play')
...@@ -34,6 +34,8 @@ from xmodule.course_module import CourseDescriptor ...@@ -34,6 +34,8 @@ from xmodule.course_module import CourseDescriptor
from xmodule.seq_module import SequenceDescriptor from xmodule.seq_module import SequenceDescriptor
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from contentstore.views.component import ADVANCED_COMPONENT_TYPES
from django_comment_common.utils import are_permissions_roles_seeded from django_comment_common.utils import are_permissions_roles_seeded
TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE) TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
...@@ -75,6 +77,31 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -75,6 +77,31 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.client = Client() self.client = Client()
self.client.login(username=uname, password=password) self.client.login(username=uname, password=password)
def test_advanced_components_in_edit_unit(self):
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
store.update_metadata(course.location, own_metadata(course))
# just pick one vertical
descriptor = store.get_items(Location('i4x', 'edX', 'simple', 'vertical', None, None))[0]
resp = self.client.get(reverse('edit_unit', kwargs={'location': descriptor.location.url()}))
self.assertEqual(resp.status_code, 200)
# 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)
def check_edit_unit(self, test_course_name): def check_edit_unit(self, test_course_name):
import_from_xml(modulestore('direct'), 'common/test/data/', [test_course_name]) import_from_xml(modulestore('direct'), 'common/test/data/', [test_course_name])
......
...@@ -111,6 +111,18 @@ class AuthTestCase(ContentStoreTestCase): ...@@ -111,6 +111,18 @@ class AuthTestCase(ContentStoreTestCase):
# Now login should work # Now login should work
self.login(self.email, self.pw) self.login(self.email, self.pw)
def test_login_link_on_activation_age(self):
self.create_account(self.username, self.email, self.pw)
# we want to test the rendering of the activation page when the user isn't logged in
self.client.logout()
resp = self._activate_user(self.email)
self.assertEqual(resp.status_code, 200)
# check the the HTML has links to the right login page. Note that this is merely a content
# check and thus could be fragile should the wording change on this page
expected = 'You can now <a href="' + reverse('login') + '">login</a>.'
self.assertIn(expected, resp.content)
def test_private_pages_auth(self): def test_private_pages_auth(self):
"""Make sure pages that do require login work.""" """Make sure pages that do require login work."""
auth_pages = ( auth_pages = (
......
...@@ -42,7 +42,7 @@ COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video'] ...@@ -42,7 +42,7 @@ COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video']
OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"] OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"]
NOTE_COMPONENT_TYPES = ['notes'] NOTE_COMPONENT_TYPES = ['notes']
ADVANCED_COMPONENT_TYPES = ['annotatable', 'word_cloud'] + OPEN_ENDED_COMPONENT_TYPES + NOTE_COMPONENT_TYPES ADVANCED_COMPONENT_TYPES = ['annotatable', 'word_cloud', 'videoalpha'] + OPEN_ENDED_COMPONENT_TYPES + NOTE_COMPONENT_TYPES
ADVANCED_COMPONENT_CATEGORY = 'advanced' ADVANCED_COMPONENT_CATEGORY = 'advanced'
ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules' ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules'
...@@ -149,8 +149,7 @@ def edit_unit(request, location): ...@@ -149,8 +149,7 @@ def edit_unit(request, location):
component_templates[category].append(( component_templates[category].append((
template.display_name_with_default, template.display_name_with_default,
template.location.url(), template.location.url(),
hasattr(template, 'markdown') and template.markdown is not None, hasattr(template, 'markdown') and template.markdown is not None
template.cms.empty,
)) ))
components = [ components = [
......
...@@ -226,7 +226,8 @@ PIPELINE_JS = { ...@@ -226,7 +226,8 @@ PIPELINE_JS = {
rooted_glob(PROJECT_ROOT / 'static/', 'coffee/src/**/*.js') rooted_glob(PROJECT_ROOT / 'static/', 'coffee/src/**/*.js')
) + ['js/hesitate.js', 'js/base.js', ) + ['js/hesitate.js', 'js/base.js',
'js/models/feedback.js', 'js/views/feedback.js', 'js/models/feedback.js', 'js/views/feedback.js',
'js/models/section.js', 'js/views/section.js'], 'js/models/section.js', 'js/views/section.js',
'js/models/metadata_model.js', 'js/views/metadata_editor_view.js'],
'output_filename': 'js/cms-application.js', 'output_filename': 'js/cms-application.js',
'test_order': 0 'test_order': 0
}, },
......
../../../templates/js/metadata-editor.underscore
\ No newline at end of file
../../../templates/js/metadata-number-entry.underscore
\ No newline at end of file
../../../templates/js/metadata-option-entry.underscore
\ No newline at end of file
../../../templates/js/metadata-string-entry.underscore
\ No newline at end of file
describe "CMS.Models.Metadata", ->
it "knows when the value has not been modified", ->
model = new CMS.Models.Metadata(
{'value': 'original', 'explicitly_set': false})
expect(model.isModified()).toBeFalsy()
model = new CMS.Models.Metadata(
{'value': 'original', 'explicitly_set': true})
model.setValue('original')
expect(model.isModified()).toBeFalsy()
it "knows when the value has been modified", ->
model = new CMS.Models.Metadata(
{'value': 'original', 'explicitly_set': false})
model.setValue('original')
expect(model.isModified()).toBeTruthy()
model = new CMS.Models.Metadata(
{'value': 'original', 'explicitly_set': true})
model.setValue('modified')
expect(model.isModified()).toBeTruthy()
it "tracks when values have been explicitly set", ->
model = new CMS.Models.Metadata(
{'value': 'original', 'explicitly_set': false})
expect(model.isExplicitlySet()).toBeFalsy()
model.setValue('original')
expect(model.isExplicitlySet()).toBeTruthy()
it "has both 'display value' and a 'value' methods", ->
model = new CMS.Models.Metadata(
{'value': 'default', 'explicitly_set': false})
expect(model.getValue()).toBeNull
expect(model.getDisplayValue()).toBe('default')
model.setValue('modified')
expect(model.getValue()).toBe('modified')
expect(model.getDisplayValue()).toBe('modified')
it "has a clear method for reverting to the default", ->
model = new CMS.Models.Metadata(
{'value': 'original', 'default_value' : 'default', 'explicitly_set': true})
model.clear()
expect(model.getValue()).toBeNull
expect(model.getDisplayValue()).toBe('default')
expect(model.isExplicitlySet()).toBeFalsy()
it "has a getter for field name", ->
model = new CMS.Models.Metadata({'field_name': 'foo'})
expect(model.getFieldName()).toBe('foo')
it "has a getter for options", ->
model = new CMS.Models.Metadata({'options': ['foo', 'bar']})
expect(model.getOptions()).toEqual(['foo', 'bar'])
it "has a getter for type", ->
model = new CMS.Models.Metadata({'type': 'Integer'})
expect(model.getType()).toBe(CMS.Models.Metadata.INTEGER_TYPE)
describe "Test Metadata Editor", ->
editorTemplate = readFixtures('metadata-editor.underscore')
numberEntryTemplate = readFixtures('metadata-number-entry.underscore')
stringEntryTemplate = readFixtures('metadata-string-entry.underscore')
optionEntryTemplate = readFixtures('metadata-option-entry.underscore')
beforeEach ->
setFixtures($("<script>", {id: "metadata-editor-tpl", type: "text/template"}).text(editorTemplate))
appendSetFixtures($("<script>", {id: "metadata-number-entry", type: "text/template"}).text(numberEntryTemplate))
appendSetFixtures($("<script>", {id: "metadata-string-entry", type: "text/template"}).text(stringEntryTemplate))
appendSetFixtures($("<script>", {id: "metadata-option-entry", type: "text/template"}).text(optionEntryTemplate))
genericEntry = {
default_value: 'default value',
display_name: "Display Name",
explicitly_set: true,
field_name: "display_name",
help: "Specifies the name for this component.",
inheritable: false,
options: [],
type: CMS.Models.Metadata.GENERIC_TYPE,
value: "Word cloud"
}
selectEntry = {
default_value: "answered",
display_name: "Show Answer",
explicitly_set: false,
field_name: "show_answer",
help: "When should you show the answer",
inheritable: true,
options: [
{"display_name": "Always", "value": "always"},
{"display_name": "Answered", "value": "answered"},
{"display_name": "Never", "value": "never"}
],
type: CMS.Models.Metadata.SELECT_TYPE,
value: "always"
}
integerEntry = {
default_value: 5,
display_name: "Inputs",
explicitly_set: false,
field_name: "num_inputs",
help: "Number of text boxes for student to input words/sentences.",
inheritable: false,
options: {min: 1},
type: CMS.Models.Metadata.INTEGER_TYPE,
value: 5
}
floatEntry = {
default_value: 2.7,
display_name: "Weight",
explicitly_set: true,
field_name: "weight",
help: "Weight for this problem",
inheritable: true,
options: {min: 1.3, max:100.2, step:0.1},
type: CMS.Models.Metadata.FLOAT_TYPE,
value: 10.2
}
# Test for the editor that creates the individual views.
describe "CMS.Views.Metadata.Editor creates editors for each field", ->
beforeEach ->
@model = new CMS.Models.MetadataCollection(
[
integerEntry,
floatEntry,
selectEntry,
genericEntry,
{
default_value: null,
display_name: "Unknown",
explicitly_set: true,
field_name: "unknown_type",
help: "Mystery property.",
inheritable: false,
options: [
{"display_name": "Always", "value": "always"},
{"display_name": "Answered", "value": "answered"},
{"display_name": "Never", "value": "never"}],
type: "unknown type",
value: null
}
]
)
it "creates child views on initialize, and sorts them alphabetically", ->
view = new CMS.Views.Metadata.Editor({collection: @model})
childModels = view.collection.models
expect(childModels.length).toBe(5)
childViews = view.$el.find('.setting-input')
expect(childViews.length).toBe(5)
verifyEntry = (index, display_name, type) ->
expect(childModels[index].get('display_name')).toBe(display_name)
expect(childViews[index].type).toBe(type)
verifyEntry(0, 'Display Name', 'text')
verifyEntry(1, 'Inputs', 'number')
verifyEntry(2, 'Show Answer', 'select-one')
verifyEntry(3, 'Unknown', 'text')
verifyEntry(4, 'Weight', 'number')
it "returns its display name", ->
view = new CMS.Views.Metadata.Editor({collection: @model})
expect(view.getDisplayName()).toBe("Word cloud")
it "returns an empty string if there is no display name property with a valid value", ->
view = new CMS.Views.Metadata.Editor({collection: new CMS.Models.MetadataCollection()})
expect(view.getDisplayName()).toBe("")
view = new CMS.Views.Metadata.Editor({collection: new CMS.Models.MetadataCollection([
{
default_value: null,
display_name: "Display Name",
explicitly_set: false,
field_name: "display_name",
help: "",
inheritable: false,
options: [],
type: CMS.Models.Metadata.GENERIC_TYPE,
value: null
}])
})
expect(view.getDisplayName()).toBe("")
it "has no modified values by default", ->
view = new CMS.Views.Metadata.Editor({collection: @model})
expect(view.getModifiedMetadataValues()).toEqual({})
it "returns modified values only", ->
view = new CMS.Views.Metadata.Editor({collection: @model})
childModels = view.collection.models
childModels[0].setValue('updated display name')
childModels[1].setValue(20)
expect(view.getModifiedMetadataValues()).toEqual({
display_name : 'updated display name',
num_inputs: 20
})
# Tests for individual views.
assertInputType = (view, expectedType) ->
input = view.$el.find('.setting-input')
expect(input.length).toBe(1)
expect(input[0].type).toBe(expectedType)
assertValueInView = (view, expectedValue) ->
expect(view.getValueFromEditor()).toBe(expectedValue)
assertCanUpdateView = (view, newValue) ->
view.setValueInEditor(newValue)
expect(view.getValueFromEditor()).toBe(newValue)
assertClear = (view, modelValue, editorValue=modelValue) ->
view.clear()
expect(view.model.getValue()).toBe(null)
expect(view.model.getDisplayValue()).toBe(modelValue)
expect(view.getValueFromEditor()).toBe(editorValue)
assertUpdateModel = (view, originalValue, newValue) ->
view.setValueInEditor(newValue)
expect(view.model.getValue()).toBe(originalValue)
view.updateModel()
expect(view.model.getValue()).toBe(newValue)
describe "CMS.Views.Metadata.String is a basic string input with clear functionality", ->
beforeEach ->
model = new CMS.Models.Metadata(genericEntry)
@view = new CMS.Views.Metadata.String({model: model})
it "uses a text input type", ->
assertInputType(@view, 'text')
it "returns the intial value upon initialization", ->
assertValueInView(@view, 'Word cloud')
it "can update its value in the view", ->
assertCanUpdateView(@view, "updated ' \" &")
it "has a clear method to revert to the model default", ->
assertClear(@view, 'default value')
it "has an update model method", ->
assertUpdateModel(@view, 'Word cloud', 'updated')
describe "CMS.Views.Metadata.Option is an option input type with clear functionality", ->
beforeEach ->
model = new CMS.Models.Metadata(selectEntry)
@view = new CMS.Views.Metadata.Option({model: model})
it "uses a select input type", ->
assertInputType(@view, 'select-one')
it "returns the intial value upon initialization", ->
assertValueInView(@view, 'always')
it "can update its value in the view", ->
assertCanUpdateView(@view, "never")
it "has a clear method to revert to the model default", ->
assertClear(@view, 'answered')
it "has an update model method", ->
assertUpdateModel(@view, null, 'never')
it "does not update to a value that is not an option", ->
@view.setValueInEditor("not an option")
expect(@view.getValueFromEditor()).toBe('always')
describe "CMS.Views.Metadata.Number supports integer or float type and has clear functionality", ->
beforeEach ->
integerModel = new CMS.Models.Metadata(integerEntry)
@integerView = new CMS.Views.Metadata.Number({model: integerModel})
floatModel = new CMS.Models.Metadata(floatEntry)
@floatView = new CMS.Views.Metadata.Number({model: floatModel})
it "uses a number input type", ->
assertInputType(@integerView, 'number')
assertInputType(@floatView, 'number')
it "returns the intial value upon initialization", ->
assertValueInView(@integerView, '5')
assertValueInView(@floatView, '10.2')
it "can update its value in the view", ->
assertCanUpdateView(@integerView, "12")
assertCanUpdateView(@floatView, "-2.4")
it "has a clear method to revert to the model default", ->
assertClear(@integerView, 5, '5')
assertClear(@floatView, 2.7, '2.7')
it "has an update model method", ->
assertUpdateModel(@integerView, null, '90')
assertUpdateModel(@floatView, 10.2, '-9.5')
it "knows the difference between integer and float", ->
expect(@integerView.isIntegerField()).toBeTruthy()
expect(@floatView.isIntegerField()).toBeFalsy()
it "sets attribtues related to min, max, and step", ->
verifyAttributes = (view, min, step, max=null) ->
inputEntry = view.$el.find('input')
expect(Number(inputEntry.attr('min'))).toEqual(min)
expect(Number(inputEntry.attr('step'))).toEqual(step)
if max is not null
expect(Number(inputEntry.attr('max'))).toEqual(max)
verifyAttributes(@integerView, 1, 1)
verifyAttributes(@floatView, 1.3, .1, 100.2)
it "corrects values that are out of range", ->
verifyValueAfterChanged = (view, value, expectedResult) ->
view.setValueInEditor(value)
view.changed()
expect(view.getValueFromEditor()).toBe(expectedResult)
verifyValueAfterChanged(@integerView, '-4', '1')
verifyValueAfterChanged(@integerView, '1', '1')
verifyValueAfterChanged(@integerView, '0', '1')
verifyValueAfterChanged(@integerView, '3001', '3001')
verifyValueAfterChanged(@floatView, '-4', '1.3')
verifyValueAfterChanged(@floatView, '1.3', '1.3')
verifyValueAfterChanged(@floatView, '1.2', '1.3')
verifyValueAfterChanged(@floatView, '100.2', '100.2')
verifyValueAfterChanged(@floatView, '100.3', '100.2')
it "disallows invalid characters", ->
verifyValueAfterKeyPressed = (view, character, reject) ->
event = {
type : 'keypress',
which : character.charCodeAt(0),
keyCode: character.charCodeAt(0),
preventDefault : () -> 'no op'
}
spyOn(event, 'preventDefault')
view.$el.find('input').trigger(event)
if (reject)
expect(event.preventDefault).toHaveBeenCalled()
else
expect(event.preventDefault).not.toHaveBeenCalled()
verifyDisallowedChars = (view) ->
verifyValueAfterKeyPressed(view, 'a', true)
verifyValueAfterKeyPressed(view, '.', view.isIntegerField())
verifyValueAfterKeyPressed(view, '[', true)
verifyValueAfterKeyPressed(view, '@', true)
for i in [0...9]
verifyValueAfterKeyPressed(view, String(i), false)
verifyDisallowedChars(@integerView)
verifyDisallowedChars(@floatView)
...@@ -73,13 +73,3 @@ describe "CMS.Views.ModuleEdit", -> ...@@ -73,13 +73,3 @@ describe "CMS.Views.ModuleEdit", ->
expect(XModule.loadModule).toHaveBeenCalled() expect(XModule.loadModule).toHaveBeenCalled()
expect(XModule.loadModule.mostRecentCall.args[0]).toBe($('.xmodule_display')) expect(XModule.loadModule.mostRecentCall.args[0]).toBe($('.xmodule_display'))
describe "changedMetadata", ->
it "returns empty if no metadata loaded", ->
expect(@moduleEdit.changedMetadata()).toEqual({})
it "returns only changed values", ->
@moduleEdit.originalMetadata = {'foo', 'bar'}
spyOn(@moduleEdit, 'metadata').andReturn({'a': '', 'b': 'before', 'c': ''})
@moduleEdit.loadEdit()
@moduleEdit.metadata.andReturn({'a': '', 'b': 'after', 'd': 'only_after'})
expect(@moduleEdit.changedMetadata()).toEqual({'b' : 'after', 'd' : 'only_after'})
class CMS.Views.ModuleEdit extends Backbone.View class CMS.Views.ModuleEdit extends Backbone.View
tagName: 'li' tagName: 'li'
className: 'component' className: 'component'
editorMode: 'editor-mode'
events: events:
"click .component-editor .cancel-button": 'clickCancelButton' "click .component-editor .cancel-button": 'clickCancelButton'
"click .component-editor .save-button": 'clickSaveButton' "click .component-editor .save-button": 'clickSaveButton'
"click .component-actions .edit-button": 'clickEditButton' "click .component-actions .edit-button": 'clickEditButton'
"click .component-actions .delete-button": 'onDelete' "click .component-actions .delete-button": 'onDelete'
"click .mode a": 'clickModeButton'
initialize: -> initialize: ->
@onDelete = @options.onDelete @onDelete = @options.onDelete
...@@ -20,29 +22,30 @@ class CMS.Views.ModuleEdit extends Backbone.View ...@@ -20,29 +22,30 @@ class CMS.Views.ModuleEdit extends Backbone.View
loadEdit: -> loadEdit: ->
if not @module if not @module
@module = XModule.loadModule(@$el.find('.xmodule_edit')) @module = XModule.loadModule(@$el.find('.xmodule_edit'))
@originalMetadata = @metadata() # At this point, metadata-edit.html will be loaded, and the metadata (as JSON) is available.
metadataEditor = @$el.find('.metadata_edit')
metadata: -> metadataData = metadataEditor.data('metadata')
# cdodge: package up metadata which is separated into a number of input fields models = [];
# there's probably a better way to do this, but at least this lets me continue to move onwards for key of metadataData
_metadata = {} models.push(metadataData[key])
@metadataEditor = new CMS.Views.Metadata.Editor({
$metadata = @$component_editor().find('.metadata_edit') el: metadataEditor,
collection: new CMS.Models.MetadataCollection(models)
if $metadata })
# walk through the set of elments which have the 'xmetadata_name' attribute and
# build up a object to pass back to the server on the subsequent POST # Need to update set "active" class on data editor if there is one.
_metadata[$(el).data("metadata-name")] = el.value for el in $('[data-metadata-name]', $metadata) # If we are only showing settings, hide the data editor controls and update settings accordingly.
if @hasDataEditor()
return _metadata @selectMode(@editorMode)
else
@hideDataEditor()
title = interpolate(gettext('<em>Editing:</em> %s'),
[@metadataEditor.getDisplayName()])
@$el.find('.component-name').html(title)
changedMetadata: -> changedMetadata: ->
currentMetadata = @metadata() return @metadataEditor.getModifiedMetadataValues()
changedMetadata = {}
for key of currentMetadata
if currentMetadata[key] != @originalMetadata[key]
changedMetadata[key] = currentMetadata[key]
return changedMetadata
cloneTemplate: (parent, template) -> cloneTemplate: (parent, template) ->
$.post("/clone_item", { $.post("/clone_item", {
...@@ -77,7 +80,7 @@ class CMS.Views.ModuleEdit extends Backbone.View ...@@ -77,7 +80,7 @@ class CMS.Views.ModuleEdit extends Backbone.View
@render() @render()
@$el.removeClass('editing') @$el.removeClass('editing')
).fail( -> ).fail( ->
showToastMessage("There was an error saving your changes. Please try again.", null, 3) showToastMessage(gettext("There was an error saving your changes. Please try again."), null, 3)
) )
clickCancelButton: (event) -> clickCancelButton: (event) ->
...@@ -96,3 +99,38 @@ class CMS.Views.ModuleEdit extends Backbone.View ...@@ -96,3 +99,38 @@ class CMS.Views.ModuleEdit extends Backbone.View
$modalCover.show().addClass('is-fixed') $modalCover.show().addClass('is-fixed')
@$component_editor().slideDown(150) @$component_editor().slideDown(150)
@loadEdit() @loadEdit()
clickModeButton: (event) ->
event.preventDefault()
if not @hasDataEditor()
return
@selectMode(event.currentTarget.parentElement.id)
hasDataEditor: =>
return @$el.find('.wrapper-comp-editor').length > 0
selectMode: (mode) =>
dataEditor = @$el.find('.wrapper-comp-editor')
settingsEditor = @$el.find('.wrapper-comp-settings')
editorModeButton = @$el.find('#editor-mode').find("a")
settingsModeButton = @$el.find('#settings-mode').find("a")
if mode == @editorMode
# Because of CodeMirror editor, cannot hide the data editor when it is first loaded. Therefore
# we have to use a class of is-inactive instead of is-active.
dataEditor.removeClass('is-inactive')
editorModeButton.addClass('is-set')
settingsEditor.removeClass('is-active')
settingsModeButton.removeClass('is-set')
else
dataEditor.addClass('is-inactive')
editorModeButton.removeClass('is-set')
settingsEditor.addClass('is-active')
settingsModeButton.addClass('is-set')
hideDataEditor: =>
editorModeButtonParent = @$el.find('#editor-mode')
editorModeButtonParent.addClass('inactive-mode')
editorModeButtonParent.removeClass('active-mode')
@$el.find('.wrapper-comp-settings').addClass('is-active')
@$el.find('#settings-mode').find("a").addClass('is-set')
/**
* Model used for metadata setting editors. This model does not do its own saving,
* as that is done by module_edit.coffee.
*/
CMS.Models.Metadata = Backbone.Model.extend({
defaults: {
"field_name": null,
"display_name": null,
"value" : null,
"explicitly_set": null,
"default_value" : null,
"options" : null,
"type" : null
},
initialize: function() {
this.original_value = this.get('value');
this.original_explicitly_set = this.get('explicitly_set');
},
/**
* Returns true if the stored value is different, or if the "explicitly_set"
* property has changed.
*/
isModified : function() {
if (!this.get('explicitly_set') && !this.original_explicitly_set) {
return false;
}
if (this.get('explicitly_set') && this.original_explicitly_set) {
return this.get('value') !== this.original_value;
}
return true;
},
/**
* Returns true if a non-default/non-inherited value has been set.
*/
isExplicitlySet: function() {
return this.get('explicitly_set');
},
/**
* The value, as shown in the UI. This may be an inherited or default value.
*/
getDisplayValue : function () {
return this.get('value');
},
/**
* The value, as should be returned to the server. if 'isExplicitlySet'
* returns false, this method returns null to indicate that the value
* is not set at this level.
*/
getValue: function() {
return this.get('explicitly_set') ? this.get('value') : null;
},
/**
* Sets the displayed value.
*/
setValue: function (value) {
this.set({
explicitly_set: true,
value: value
});
},
/**
* Returns the field name, which should be used for persisting the metadata
* field to the server.
*/
getFieldName: function () {
return this.get('field_name');
},
/**
* Returns the options. This may be a array of possible values, or an object
* with properties like "max", "min" and "step".
*/
getOptions: function () {
return this.get('options');
},
/**
* Returns the type of this metadata field. Possible values are SELECT_TYPE,
* INTEGER_TYPE, and FLOAT_TYPE, GENERIC_TYPE.
*/
getType: function() {
return this.get('type');
},
/**
* Reverts the value to the default_value specified at construction, and updates the
* explicitly_set property.
*/
clear: function() {
this.set({
explicitly_set: false,
value: this.get('default_value')
});
}
});
CMS.Models.MetadataCollection = Backbone.Collection.extend({
model : CMS.Models.Metadata,
comparator: "display_name"
});
CMS.Models.Metadata.SELECT_TYPE = "Select";
CMS.Models.Metadata.INTEGER_TYPE = "Integer";
CMS.Models.Metadata.FLOAT_TYPE = "Float";
CMS.Models.Metadata.GENERIC_TYPE = "Generic";
if (!CMS.Views['Metadata']) CMS.Views.Metadata = {};
CMS.Views.Metadata.Editor = Backbone.View.extend({
// Model is CMS.Models.MetadataCollection,
initialize : function() {
var tpl = $("#metadata-editor-tpl").text();
if(!tpl) {
console.error("Couldn't load metadata editor template");
}
this.template = _.template(tpl);
this.$el.html(this.template({numEntries: this.collection.length}));
var counter = 0;
var self = this;
this.collection.each(
function (model) {
var data = {
el: self.$el.find('.metadata_entry')[counter++],
model: model
};
if (model.getType() === CMS.Models.Metadata.SELECT_TYPE) {
new CMS.Views.Metadata.Option(data);
}
else if (model.getType() === CMS.Models.Metadata.INTEGER_TYPE ||
model.getType() === CMS.Models.Metadata.FLOAT_TYPE) {
new CMS.Views.Metadata.Number(data);
}
else {
// Everything else is treated as GENERIC_TYPE, which uses String editor.
new CMS.Views.Metadata.String(data);
}
});
},
/**
* Returns the just the modified metadata values, in the format used to persist to the server.
*/
getModifiedMetadataValues: function () {
var modified_values = {};
this.collection.each(
function (model) {
if (model.isModified()) {
modified_values[model.getFieldName()] = model.getValue();
}
}
);
return modified_values;
},
/**
* Returns a display name for the component related to this metadata. This method looks to see
* if there is a metadata entry called 'display_name', and if so, it returns its value. If there
* is no such entry, or if display_name does not have a value set, it returns an empty string.
*/
getDisplayName: function () {
var displayName = '';
this.collection.each(
function (model) {
if (model.get('field_name') === 'display_name') {
var displayNameValue = model.get('value');
// It is possible that there is no display name value set. In that case, return empty string.
displayName = displayNameValue ? displayNameValue : '';
}
}
);
return displayName;
}
});
CMS.Views.Metadata.AbstractEditor = Backbone.View.extend({
// Model is CMS.Models.Metadata.
initialize : function() {
var self = this;
var templateName = _.result(this, 'templateName');
// Backbone model cid is only unique within the collection.
this.uniqueId = _.uniqueId(templateName + "_");
var tpl = document.getElementById(templateName).text;
if(!tpl) {
console.error("Couldn't load template: " + templateName);
}
this.template = _.template(tpl);
this.$el.html(this.template({model: this.model, uniqueId: this.uniqueId}));
this.listenTo(this.model, 'change', this.render);
this.render();
},
/**
* The ID/name of the template. Subclasses must override this.
*/
templateName: '',
/**
* Returns the value currently displayed in the editor/view. Subclasses should implement this method.
*/
getValueFromEditor : function () {},
/**
* Sets the value currently displayed in the editor/view. Subclasses should implement this method.
*/
setValueInEditor : function (value) {},
/**
* Sets the value in the model, using the value currently displayed in the view.
*/
updateModel: function () {
this.model.setValue(this.getValueFromEditor());
},
/**
* Clears the value currently set in the model (reverting to the default).
*/
clear: function () {
this.model.clear();
},
/**
* Shows the clear button, if it is not already showing.
*/
showClearButton: function() {
if (!this.$el.hasClass('is-set')) {
this.$el.addClass('is-set');
this.getClearButton().removeClass('inactive');
this.getClearButton().addClass('active');
}
},
/**
* Returns the clear button.
*/
getClearButton: function () {
return this.$el.find('.setting-clear');
},
/**
* Renders the editor, updating the value displayed in the view, as well as the state of
* the clear button.
*/
render: function () {
if (!this.template) return;
this.setValueInEditor(this.model.getDisplayValue());
if (this.model.isExplicitlySet()) {
this.showClearButton();
}
else {
this.$el.removeClass('is-set');
this.getClearButton().addClass('inactive');
this.getClearButton().removeClass('active');
}
return this;
}
});
CMS.Views.Metadata.String = CMS.Views.Metadata.AbstractEditor.extend({
events : {
"change input" : "updateModel",
"keypress .setting-input" : "showClearButton" ,
"click .setting-clear" : "clear"
},
templateName: "metadata-string-entry",
getValueFromEditor : function () {
return this.$el.find('#' + this.uniqueId).val();
},
setValueInEditor : function (value) {
this.$el.find('input').val(value);
}
});
CMS.Views.Metadata.Number = CMS.Views.Metadata.AbstractEditor.extend({
events : {
"change input" : "updateModel",
"keypress .setting-input" : "keyPressed",
"change .setting-input" : "changed",
"click .setting-clear" : "clear"
},
render: function () {
CMS.Views.Metadata.AbstractEditor.prototype.render.apply(this);
if (!this.initialized) {
var numToString = function (val) {
return val.toFixed(4);
};
var min = "min";
var max = "max";
var step = "step";
var options = this.model.getOptions();
if (options.hasOwnProperty(min)) {
this.min = Number(options[min]);
this.$el.find('input').attr(min, numToString(this.min));
}
if (options.hasOwnProperty(max)) {
this.max = Number(options[max]);
this.$el.find('input').attr(max, numToString(this.max));
}
var stepValue = undefined;
if (options.hasOwnProperty(step)) {
// Parse step and convert to String. Polyfill doesn't like float values like ".1" (expects "0.1").
stepValue = numToString(Number(options[step]));
}
else if (this.isIntegerField()) {
stepValue = "1";
}
if (stepValue !== undefined) {
this.$el.find('input').attr(step, stepValue);
}
// Manually runs polyfill for input number types to correct for Firefox non-support.
// inputNumber will be undefined when unit test is running.
if ($.fn.inputNumber) {
this.$el.find('.setting-input-number').inputNumber();
}
this.initialized = true;
}
return this;
},
templateName: "metadata-number-entry",
getValueFromEditor : function () {
return this.$el.find('#' + this.uniqueId).val();
},
setValueInEditor : function (value) {
this.$el.find('input').val(value);
},
/**
* Returns true if this view is restricted to integers, as opposed to floating points values.
*/
isIntegerField : function () {
return this.model.getType() === 'Integer';
},
keyPressed: function (e) {
this.showClearButton();
// This first filtering if statement is take from polyfill to prevent
// non-numeric input (for browsers that don't use polyfill because they DO have a number input type).
var _ref, _ref1;
if (((_ref = e.keyCode) !== 8 && _ref !== 9 && _ref !== 35 && _ref !== 36 && _ref !== 37 && _ref !== 39) &&
((_ref1 = e.which) !== 45 && _ref1 !== 46 && _ref1 !== 48 && _ref1 !== 49 && _ref1 !== 50 && _ref1 !== 51
&& _ref1 !== 52 && _ref1 !== 53 && _ref1 !== 54 && _ref1 !== 55 && _ref1 !== 56 && _ref1 !== 57)) {
e.preventDefault();
}
// For integers, prevent decimal points.
if (this.isIntegerField() && e.keyCode === 46) {
e.preventDefault();
}
},
changed: function () {
// Limit value to the range specified by min and max (necessary for browsers that aren't using polyfill).
var value = this.getValueFromEditor();
if ((this.max !== undefined) && value > this.max) {
value = this.max;
} else if ((this.min != undefined) && value < this.min) {
value = this.min;
}
this.setValueInEditor(value);
this.updateModel();
}
});
CMS.Views.Metadata.Option = CMS.Views.Metadata.AbstractEditor.extend({
events : {
"change select" : "updateModel",
"click .setting-clear" : "clear"
},
templateName: "metadata-option-entry",
getValueFromEditor : function () {
var selectedText = this.$el.find('#' + this.uniqueId).find(":selected").text();
var selectedValue;
_.each(this.model.getOptions(), function (modelValue) {
if (modelValue === selectedText) {
selectedValue = modelValue;
}
else if (modelValue['display_name'] === selectedText) {
selectedValue = modelValue['value'];
}
});
return selectedValue;
},
setValueInEditor : function (value) {
// Value here is the json value as used by the field. The choice may instead be showing display names.
// Find the display name matching the value passed in.
_.each(this.model.getOptions(), function (modelValue) {
if (modelValue['value'] === value) {
value = modelValue['display_name'];
}
});
this.$el.find('#' + this.uniqueId + " option").filter(function() {
return $(this).text() === value;
}).prop('selected', true);
}
});
...@@ -814,7 +814,7 @@ hr.divide { ...@@ -814,7 +814,7 @@ hr.divide {
line-height: 26px; line-height: 26px;
color: $white; color: $white;
pointer-events: none; pointer-events: none;
opacity: 0; opacity: 0.0;
&:after { &:after {
content: '▾'; content: '▾';
......
...@@ -149,11 +149,11 @@ abbr[title] { ...@@ -149,11 +149,11 @@ abbr[title] {
margin-left: 20px; margin-left: 20px;
} }
li { li {
opacity: .8; opacity: 0.8;
&:ui-state-active { &:ui-state-active {
background-color: rgba(255, 255, 255, .3); background-color: rgba(255, 255, 255, .3);
opacity: 1; opacity: 1.0;
font-weight: 400; font-weight: 400;
} }
a:focus { a:focus {
......
...@@ -95,12 +95,12 @@ ...@@ -95,12 +95,12 @@
// bounce in // bounce in
@mixin bounceIn { @mixin bounceIn {
0% { 0% {
opacity: 0; opacity: 0.0;
@include transform(scale(0.3)); @include transform(scale(0.3));
} }
50% { 50% {
opacity: 1; opacity: 1.0;
@include transform(scale(1.05)); @include transform(scale(1.05));
} }
...@@ -128,12 +128,12 @@ ...@@ -128,12 +128,12 @@
// bounce in // bounce in
@mixin bounceOut { @mixin bounceOut {
0% { 0% {
opacity: 0; opacity: 0.0;
@include transform(scale(0.3)); @include transform(scale(0.3));
} }
50% { 50% {
opacity: 1; opacity: 1.0;
@include transform(scale(1.05)); @include transform(scale(1.05));
} }
...@@ -146,12 +146,12 @@ ...@@ -146,12 +146,12 @@
} }
50% { 50% {
opacity: 1; opacity: 1.0;
@include transform(scale(1.05)); @include transform(scale(1.05));
} }
100% { 100% {
opacity: 0; opacity: 0.0;
@include transform(scale(0.3)); @include transform(scale(0.3));
} }
} }
......
...@@ -124,7 +124,6 @@ code { ...@@ -124,7 +124,6 @@ code {
.CodeMirror { .CodeMirror {
font-size: 13px; font-size: 13px;
border: 1px solid $darkGrey;
background: #fff; background: #fff;
} }
......
...@@ -243,7 +243,7 @@ ...@@ -243,7 +243,7 @@
left: -7px; left: -7px;
top: 47px; top: 47px;
width: 140px; width: 140px;
opacity: 0; opacity: 0.0;
pointer-events: none; pointer-events: none;
} }
...@@ -558,7 +558,7 @@ body.signin .nav-not-signedin-signup { ...@@ -558,7 +558,7 @@ body.signin .nav-not-signedin-signup {
.wrapper-nav-sub { .wrapper-nav-sub {
@include transition (opacity 1.0s ease-in-out 0s); @include transition (opacity 1.0s ease-in-out 0s);
opacity: 0; opacity: 0.0;
pointer-events: none; pointer-events: none;
&.is-shown { &.is-shown {
......
...@@ -627,7 +627,7 @@ ...@@ -627,7 +627,7 @@
pointer-events: none; pointer-events: none;
.prompt { .prompt {
opacity: 0; opacity: 0.0;
} }
} }
......
...@@ -254,7 +254,7 @@ body.course.checklists { ...@@ -254,7 +254,7 @@ body.course.checklists {
.task-support { .task-support {
@extend .t-copy-sub2; @extend .t-copy-sub2;
@include transition(opacity .15s .25s ease-in-out); @include transition(opacity .15s .25s ease-in-out);
opacity: 0; opacity: 0.0;
pointer-events: none; pointer-events: none;
} }
} }
...@@ -267,7 +267,7 @@ body.course.checklists { ...@@ -267,7 +267,7 @@ body.course.checklists {
float: right; float: right;
width: flex-grid(2,9); width: flex-grid(2,9);
margin: ($baseline/2) 0 0 flex-gutter(); margin: ($baseline/2) 0 0 flex-gutter();
opacity: 0; opacity: 0.0;
pointer-events: none; pointer-events: none;
text-align: right; text-align: right;
......
...@@ -59,7 +59,7 @@ body.dashboard { ...@@ -59,7 +59,7 @@ body.dashboard {
top: 15px; top: 15px;
right: $baseline; right: $baseline;
padding: ($baseline/4) ($baseline/2); padding: ($baseline/4) ($baseline/2);
opacity: 0; opacity: 0.0;
pointer-events: none; pointer-events: none;
&:hover { &:hover {
......
...@@ -162,7 +162,7 @@ body.index { ...@@ -162,7 +162,7 @@ body.index {
position: absolute; position: absolute;
bottom: -30px; bottom: -30px;
right: ($baseline/2); right: ($baseline/2);
opacity: 0; opacity: 0.0;
[class^="icon-"] { [class^="icon-"] {
@include font-size(18); @include font-size(18);
......
...@@ -386,6 +386,11 @@ body.course.settings { ...@@ -386,6 +386,11 @@ body.course.settings {
#course-overview { #course-overview {
height: ($baseline*20); height: ($baseline*20);
} }
//adds back in CodeMirror border removed due to Unit page styling of component editors
.CodeMirror {
border: 1px solid $gray-l2;
}
} }
// specific fields - video // specific fields - video
...@@ -698,7 +703,7 @@ body.course.settings { ...@@ -698,7 +703,7 @@ body.course.settings {
.tip { .tip {
@include transition (opacity 0.5s ease-in-out 0s); @include transition (opacity 0.5s ease-in-out 0s);
opacity: 0; opacity: 0.0;
position: absolute; position: absolute;
bottom: ($baseline*1.25); bottom: ($baseline*1.25);
} }
...@@ -713,7 +718,7 @@ body.course.settings { ...@@ -713,7 +718,7 @@ body.course.settings {
input.error { input.error {
& + .tip { & + .tip {
opacity: 0; opacity: 0.0;
} }
} }
} }
......
...@@ -41,38 +41,23 @@ body.course.static-pages { ...@@ -41,38 +41,23 @@ body.course.static-pages {
@include edit-box; @include edit-box;
@include box-shadow(none); @include box-shadow(none);
display: none; display: none;
padding: 20px; padding: 0;
border-radius: 2px 2px 0 0; border-radius: 2px 2px 0 0;
.metadata_edit { //Overrides general edit-box mixin
margin-bottom: 20px; .row {
font-size: 13px; margin-bottom: 0px;
li {
margin-bottom: 10px;
}
label {
display: inline-block;
margin-right: 10px;
}
}
h3 {
margin-bottom: 10px;
font-size: 18px;
font-weight: 700;
} }
h5 { // This duplicates the styling from Unit page editing
margin-bottom: 8px; .module-actions {
color: #fff; @include box-shadow(inset 0 1px 1px $shadow);
font-weight: 700; padding: 0px 0 10px 10px;
} background-color: $gray-l6;
.save-button { .save-button {
margin-top: 10px; margin: ($baseline/2) 8px 0 0;
margin: 15px 8px 0 0; }
} }
} }
} }
...@@ -215,3 +200,4 @@ body.course.static-pages { ...@@ -215,3 +200,4 @@ body.course.static-pages {
outline: 0; outline: 0;
} }
} }
...@@ -24,7 +24,7 @@ body.course.unit { ...@@ -24,7 +24,7 @@ body.course.unit {
border: none; border: none;
.rendered-component { .rendered-component {
padding: 0 20px; padding: 0 $baseline;
} }
} }
} }
...@@ -32,7 +32,7 @@ body.course.unit { ...@@ -32,7 +32,7 @@ body.course.unit {
.unit-body { .unit-body {
.unit-name-input { .unit-name-input {
padding: 20px 40px; padding: $baseline 2*$baseline;
label { label {
display: block; display: block;
...@@ -73,15 +73,15 @@ body.course.unit { ...@@ -73,15 +73,15 @@ body.course.unit {
letter-spacing: 1px; letter-spacing: 1px;
text-transform: uppercase; text-transform: uppercase;
} }
// ====================
// Component List Meta
.components { .components {
> li { > li {
position: relative; position: relative;
z-index: 10; z-index: 10;
margin: 20px 40px; margin: $baseline 2*$baseline;
.title { .title {
margin: 0 0 15px 0; margin: 0 0 15px 0;
...@@ -91,23 +91,26 @@ body.course.unit { ...@@ -91,23 +91,26 @@ body.course.unit {
} }
} }
// ====================
// New Components
&.new-component-item { &.new-component-item {
margin: 20px 0px; margin: $baseline 0px;
border-top: 1px solid $mediumGrey; border-top: 1px solid $mediumGrey;
box-shadow: 0 2px 1px rgba(182, 182, 182, 0.75) inset; box-shadow: 0 2px 1px rgba(182, 182, 182, 0.75) inset;
background-color: $lightGrey; background-color: $lightGrey;
margin-bottom: 0px; margin-bottom: 0px;
padding-bottom: 20px; padding-bottom: $baseline;
.new-component-button { .new-component-button {
display: block; display: block;
padding: 20px; padding: $baseline;
text-align: center; text-align: center;
color: #edf1f5; color: #edf1f5;
} }
h5 { h5 {
margin: 20px 0px; margin: $baseline 0px;
color: #fff; color: #fff;
font-weight: 600; font-weight: 600;
font-size: 18px; font-size: 18px;
...@@ -132,7 +135,7 @@ body.course.unit { ...@@ -132,7 +135,7 @@ body.course.unit {
height: 100px; height: 100px;
color: #fff; color: #fff;
margin-right: 15px; margin-right: 15px;
margin-bottom: 20px; margin-bottom: $baseline;
border-radius: 8px; border-radius: 8px;
font-size: 15px; font-size: 15px;
line-height: 14px; line-height: 14px;
...@@ -144,7 +147,7 @@ body.course.unit { ...@@ -144,7 +147,7 @@ body.course.unit {
bottom: 5px; bottom: 5px;
left: 0; left: 0;
width: 100%; width: 100%;
padding: 10px; padding: $baseline/2;
@include box-sizing(border-box); @include box-sizing(border-box);
color: #fff; color: #fff;
} }
...@@ -153,7 +156,7 @@ body.course.unit { ...@@ -153,7 +156,7 @@ body.course.unit {
.new-component-templates { .new-component-templates {
display: none; display: none;
margin: 20px 40px 20px 40px; margin: $baseline 2*$baseline;
border-radius: 3px; border-radius: 3px;
border: 1px solid $mediumGrey; border: 1px solid $mediumGrey;
background-color: #fff; background-color: #fff;
...@@ -161,7 +164,7 @@ body.course.unit { ...@@ -161,7 +164,7 @@ body.course.unit {
@include clearfix; @include clearfix;
.cancel-button { .cancel-button {
margin: 20px 0px 10px 10px; margin: $baseline 0px $baseline/2 $baseline/2;
@include white-button; @include white-button;
} }
...@@ -171,9 +174,9 @@ body.course.unit { ...@@ -171,9 +174,9 @@ body.course.unit {
// specific menu types // specific menu types
&.new-component-problem { &.new-component-problem {
padding-bottom:10px; padding-bottom: $baseline/2;
.ss-icon, .editor-indicator { [class^="icon-"], .editor-indicator {
display: inline-block; display: inline-block;
} }
...@@ -208,7 +211,7 @@ body.course.unit { ...@@ -208,7 +211,7 @@ body.course.unit {
@include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 -1px 0 rgba(0, 0, 0, 0.2) inset); @include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 -1px 0 rgba(0, 0, 0, 0.2) inset);
li:first-child { li:first-child {
margin-left: 20px; margin-left: $baseline;
} }
li { li {
...@@ -219,21 +222,21 @@ body.course.unit { ...@@ -219,21 +222,21 @@ body.course.unit {
@include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0)); @include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
background-color: tint($lightBluishGrey, 10%); background-color: tint($lightBluishGrey, 10%);
@include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 -1px 0 rgba(0, 0, 0, 0.2) inset); @include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 -1px 0 rgba(0, 0, 0, 0.2) inset);
opacity:.8; opacity: 0.8;
&:hover { &:hover {
opacity:1; opacity: 0.9;
background-color: tint($lightBluishGrey, 20%); background-color: tint($lightBluishGrey, 20%);
} }
&.ui-state-active { &.ui-state-active {
border: 0px; border: 0px;
@include active; @include active;
opacity:1; opacity: 1.0;
} }
} }
a{ a {
display: block; display: block;
padding: 15px 25px; padding: 15px 25px;
font-size: 15px; font-size: 15px;
...@@ -280,14 +283,14 @@ body.course.unit { ...@@ -280,14 +283,14 @@ body.course.unit {
a { a {
@include clearfix(); @include clearfix();
display: block; display: block;
padding: 7px 20px; padding: 7px $baseline;
border-bottom: none; border-bottom: none;
font-weight: 500; font-weight: 500;
.name { .name {
float: left; float: left;
.ss-icon { [class^="icon-"] {
@include transition(opacity .15s); @include transition(opacity .15s);
display: inline-block; display: inline-block;
top: 1px; top: 1px;
...@@ -308,14 +311,14 @@ body.course.unit { ...@@ -308,14 +311,14 @@ body.course.unit {
opacity: 0.3; opacity: 0.3;
} }
.ss-icon, .editor-indicator { [class^="icon-"], .editor-indicator {
display: none; display: none;
} }
&:hover { &:hover {
color: #fff; color: #fff;
.ss-icon { [class^="icon-"] {
opacity: 1.0; opacity: 1.0;
} }
...@@ -355,6 +358,9 @@ body.course.unit { ...@@ -355,6 +358,9 @@ body.course.unit {
} }
} }
// ====================
// Component Drag and Drop, Non-Edit Module Rendering, Styling
.component { .component {
border: 1px solid $lightBluishGrey2; border: 1px solid $lightBluishGrey2;
border-radius: 3px; border-radius: 3px;
...@@ -401,7 +407,7 @@ body.course.unit { ...@@ -401,7 +407,7 @@ body.course.unit {
} }
.xmodule_display { .xmodule_display {
padding: 40px 20px 20px; padding: 2*$baseline $baseline $baseline;
overflow-x: auto; overflow-x: auto;
h1 { h1 {
...@@ -409,36 +415,24 @@ body.course.unit { ...@@ -409,36 +415,24 @@ body.course.unit {
margin-left: 0; margin-left: 0;
} }
} }
// ====================
// Component Editing
.wrapper-component-editor { .wrapper-component-editor {
z-index: 9999; z-index: 9999;
position: relative; position: relative;
background: $lightBluishGrey2; background: $white;
} }
.component-editor { .component-editor {
@include edit-box; @include edit-box;
@include box-shadow(none); @include box-shadow(none);
display: none; display: none;
padding: 20px; padding: 0;
border-radius: 2px 2px 0 0; border-radius: 2px 2px 0 0;
.metadata_edit {
margin-bottom: 20px;
font-size: 13px;
li {
margin-bottom: 10px;
}
label {
display: inline-block;
margin-right: 10px;
}
}
h3 { h3 {
margin-bottom: 10px; margin-bottom: $baseline/2;
font-size: 18px; font-size: 18px;
font-weight: 700; font-weight: 700;
} }
...@@ -449,16 +443,313 @@ body.course.unit { ...@@ -449,16 +443,313 @@ body.course.unit {
font-weight: 700; font-weight: 700;
} }
.row {
margin-bottom: 0px;
}
// Module Actions, also used for Static Pages
.module-actions {
@include box-shadow(inset 0 1px 1px $shadow);
padding: 0 0 $baseline $baseline;
background-color: $gray-l6;
.save-button { .save-button {
margin-top: 10px; margin: ($baseline/2) 8px 0 0;
margin: 15px 8px 0 0; }
}
}
}
}
// Edit Header (Component Name, Mode-Editor, Mode-Settings)
.component-edit-header {
@include box-sizing(border-box);
padding: 18px 0 18px $baseline;
top: 0;
right: 0;
background-color: $blue;
border-bottom: 1px solid $blue-d2;
color: $white;
//Component Name
.component-name {
@extend .t-copy-sub1;
width: 50%;
color: $white;
font-weight: 600;
em {
display: inline-block;
margin-right: ($baseline/4);
font-weight: 400;
color: $white;
}
}
//Nav-Edit Modes
.nav-edit-modes {
list-style: none;
right: 0;
top: 0;
position: absolute;
padding: 12px ($baseline*0.75);
.mode {
display: inline-block;
margin-left: 8px;
&.inactive-mode{
display: none;
}
&.active-mode a {
@include blue-button;
&.is-set {
@include transition(box-shadow 0.5 ease-in-out);
@include linear-gradient($blue, $blue);
color: $blue-d1;
box-shadow: inset 0 1px 2px 1px $shadow-l1;
background-color: $blue-d4;
cursor: default;
&:hover {
box-shadow: inset 0 1px 2px 1px $shadow;
}
} }
} }
} }
}
}
// Editor Wrapper
.wrapper-comp-editor {
display: block;
// Because the editor may be a CodeMirror editor (which must be visible at the time it is created
// in order for it to properly initialize), we must toggle "is-inactive" instead of the more common "is-active".
&.is-inactive {
display: none;
}
}
// Settings Wrapper
.wrapper-comp-settings {
display: none;
&.is-active {
display: block;
}
//settings-list
.list-input.settings-list {
margin: 0;
padding: 0;
list-style: none;
overflow: auto;
max-height: 400px;
//chrome scrollbar visibility correction
&::-webkit-scrollbar {
-webkit-appearance: none;
width: 11px;
height: 11px;
}
&::-webkit-scrollbar-thumb {
border-radius: 8px;
border: 2px solid $gray-l2;
background-color: rgba(0, 0, 0, .5);
}
//component-setting-entry
.field.comp-setting-entry {
background-color: $white;
padding: $baseline;
border-bottom: 1px solid $gray-l2;
opacity: 0.7;
&:last-child {
//margin-bottom: 0;
}
//no required component settings currently
&.required {
label {
//font-weight: 600;
}
label:after {
//margin-left: ($baseline/4);
//content: "*";
}
}
&:hover {
@include transition(opacity 0.25s ease-in-out);
opacity: 1.0;
}
&.is-set {
opacity: 1.0;
background-color: $white;
.setting-input {
color: $blue-l1;
}
}
.wrapper-comp-setting{
display: inline-block;
min-width: 300px;
width: 45%;
top: 0;
vertical-align: top;
margin-bottom:5px;
position: relative;
}
label.setting-label {
@extend .t-copy-sub1;
@include transition(color, 0.15s, ease-in-out);
font-weight: 400;
vertical-align: middle;
display: inline-block;
position: relative;
left: 0;
min-width: 100px;
width: 35%;
.unit-settings { &.is-focused {
color: $blue;
}
}
input, select, input[type="number"] {
@include placeholder($gray-l4);
@include font-size(16);
@include size(100%,100%);
padding: ($baseline/2);
min-width: 100px;
width: 45%;
//&.long {
//
//}
//&.short {
//}
//&.error {
// border-color: $red;
//}
//&:focus {
// + .tip {
// color: $gray;
// }
border-radius: 3px;
border: 1px solid $gray-l2;
text-overflow: ellipsis;
}
input[type="number"] {
width: 38.5%;
@include box-shadow(0 1px 2px $shadow-l1 inset);
//For webkit browsers which render number fields differently, make input wider.
-moz-column-width: {
width: 32%;
}
&:active {
background-color: #FFFCF1;
}
}
select {
//@include box-shadow(0 1px 2px $shadow-l1 inset);
&:focus {
@include box-shadow(0 0 1px $shadow);
@include transition(opacity 0.25s ease-in-out);
background-color: $yellow;
}
&:active {
background-color: $yellow;
}
}
.action.setting-clear {
@include font-size(11);
color: $gray;
width: 25px;
height: 25px;
vertical-align: middle;
padding: 5px;
border-radius: 50%;
margin: 0 $baseline/2;
box-shadow: none;
text-shadow: none;
border: 1px solid $gray-l1;
background-color: $gray-l4;
&:hover {
@include box-shadow(0 1px 1px $shadow);
@include transition(opacity 0.15s ease-in-out);
background-color: $blue-s3;
border: 1px solid $blue-s3;
color: $white;
}
&.inactive {
visibility: hidden;
}
}
.tip.setting-help {
@include font-size(12);
display: inline-block;
font-color: $gray-l6;
min-width: 260px;
width: 50%;
vertical-align: top;
}
}
}
}
// ====================
// Editing Units from Courseware
body.unit {
.component {
padding-top: 30px;
.component-actions {
@include box-sizing(border-box);
position: absolute;
width: 100%;
padding: 15px;
top: 0;
left: 0;
border-bottom: 1px solid $lightBluishGrey2;
background: $lightGrey;
}
&.editing {
padding-top: 0;
}
}
}
// ====================
// Unit Page Sidebar
.unit-settings {
.window-contents { .window-contents {
padding: 10px 20px; padding: $baseline/2 $baseline;
} }
.unit-actions { .unit-actions {
...@@ -468,7 +759,7 @@ body.course.unit { ...@@ -468,7 +759,7 @@ body.course.unit {
.published-alert { .published-alert {
display: none; display: none;
padding: 10px; padding: $baseline/2;
border: 1px solid #edbd3c; border: 1px solid #edbd3c;
border-radius: 3px; border-radius: 3px;
background: #fbf6e1; background: #fbf6e1;
...@@ -494,7 +785,7 @@ body.course.unit { ...@@ -494,7 +785,7 @@ body.course.unit {
.preview-button, .view-button { .preview-button, .view-button {
@include white-button; @include white-button;
margin-bottom: 10px; margin-bottom: $baseline/2;
} }
.publish-button { .publish-button {
...@@ -514,12 +805,12 @@ body.course.unit { ...@@ -514,12 +805,12 @@ body.course.unit {
.publish-button, .publish-button,
.view-button { .view-button {
font-size: 11px; font-size: 11px;
margin-top: 10px; margin-top: $baseline/2;
padding: 6px 15px 8px; padding: 6px 15px 8px;
} }
} }
.unit-history { .unit-history {
&.collapsed { &.collapsed {
h4 { h4 {
border-bottom: none; border-bottom: none;
...@@ -536,7 +827,7 @@ body.course.unit { ...@@ -536,7 +827,7 @@ body.course.unit {
li { li {
display: block; display: block;
padding: 6px 8px 8px 10px; padding: 6px 8px 8px $baseline/2;
background: #edf1f5; background: #edf1f5;
font-size: 12px; font-size: 12px;
...@@ -561,13 +852,13 @@ body.course.unit { ...@@ -561,13 +852,13 @@ body.course.unit {
} }
} }
} }
} }
.unit-location { .unit-location {
.url { .url {
width: 100%;
margin-bottom: 10px;
@include box-shadow(none); @include box-shadow(none);
width: 100%;
margin-bottom: $baseline/2;
} }
.draft-tag, .draft-tag,
...@@ -581,6 +872,7 @@ body.course.unit { ...@@ -581,6 +872,7 @@ body.course.unit {
@include tree-view; @include tree-view;
.section-item { .section-item {
@include box-sizing(border-box);
display: inline-block; display: inline-block;
width: 100%; width: 100%;
font-size: 11px; font-size: 11px;
...@@ -588,16 +880,15 @@ body.course.unit { ...@@ -588,16 +880,15 @@ body.course.unit {
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
@include box-sizing(border-box);
} }
ol { ol {
.section-item { .section-item {
padding-left: 20px; padding-left: $baseline;
} }
.new-unit-item { .new-unit-item {
margin-left: 20px; margin-left: $baseline;
} }
} }
...@@ -607,13 +898,13 @@ body.course.unit { ...@@ -607,13 +898,13 @@ body.course.unit {
} }
.new-unit-item { .new-unit-item {
margin: 0 0 10px 41px; margin: 0 0 $baseline 41px;
}
} }
} }
} }
}
.edit-state-draft { .edit-state-draft {
.visibility, .visibility,
.edit-draft-message, .edit-draft-message,
...@@ -624,9 +915,9 @@ body.course.unit { ...@@ -624,9 +915,9 @@ body.course.unit {
.published-alert { .published-alert {
display: block; display: block;
} }
} }
.edit-state-public { .edit-state-public {
.delete-draft, .delete-draft,
.component-actions, .component-actions,
.new-component-item, .new-component-item,
...@@ -643,9 +934,9 @@ body.course.unit { ...@@ -643,9 +934,9 @@ body.course.unit {
.drag-handle { .drag-handle {
display: none !important; display: none !important;
} }
} }
.edit-state-private { .edit-state-private {
.delete-draft, .delete-draft,
.publish-draft, .publish-draft,
.editing-draft-alert, .editing-draft-alert,
...@@ -653,28 +944,24 @@ body.course.unit { ...@@ -653,28 +944,24 @@ body.course.unit {
.view-button { .view-button {
display: none; display: none;
} }
}
} }
// ====================
// editing units from courseware // Latex Compiler
body.unit { .launch-latex-compiler {
background-color: $white;
padding: $baseline/2 0 $baseline/2 $baseline;
border-bottom: 1px solid $gray-l2;
opacity: 0.8;
.component {
padding-top: 30px;
.component-actions { &:hover {
@include box-sizing(border-box); @include transition(opacity 0.25s ease-in-out);
position: absolute; opacity: 1.0s;
width: 100%;
padding: 15px;
top: 0;
left: 0;
border-bottom: 1px solid $lightBluishGrey2;
background: $lightGrey;
} }
}
&.editing { // hides latex compiler button if settings mode is-active
padding-top: 0; div.wrapper-comp-editor.is-inactive ~ div.launch-latex-compiler{
} display: none;
}
} }
...@@ -212,6 +212,7 @@ body.course.updates { ...@@ -212,6 +212,7 @@ body.course.updates {
@include edit-box; @include edit-box;
position: absolute; position: absolute;
right: 0; right: 0;
top: 0;
z-index: 10001; z-index: 10001;
width: 800px; width: 800px;
padding: 30px; padding: 30px;
......
...@@ -61,6 +61,8 @@ ...@@ -61,6 +61,8 @@
<div class="wrapper wrapper-view"> <div class="wrapper wrapper-view">
<%include file="widgets/header.html" /> <%include file="widgets/header.html" />
## remove this block after advanced settings notification is rewritten
<%block name="view_alerts"></%block>
<div id="page-alert"></div> <div id="page-alert"></div>
<%block name="content"></%block> <%block name="content"></%block>
...@@ -72,9 +74,13 @@ ...@@ -72,9 +74,13 @@
<%include file="widgets/footer.html" /> <%include file="widgets/footer.html" />
<%include file="widgets/tender.html" /> <%include file="widgets/tender.html" />
## remove this block after advanced settings notification is rewritten
<%block name="view_notifications"></%block>
<div id="page-notification"></div> <div id="page-notification"></div>
</div> </div>
## remove this block after advanced settings notification is rewritten
<%block name="view_prompts"></%block>
<div id="page-prompt"></div> <div id="page-prompt"></div>
<%block name="jsextra"></%block> <%block name="jsextra"></%block>
</body> </body>
......
<%! from django.utils.translation import ugettext as _ %>
<%namespace name='static' file='static_content.html'/>
<script type="text/javascript" src="${static.url('js/models/metadata_model.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/metadata_editor_view.js')}"></script>
<script src="${static.url('js/vendor/html5-input-polyfills/number-polyfill.js')}"></script>
<link rel="stylesheet" type="text/css" href="${static.url('css/vendor/html5-input-polyfills/number-polyfill.css')}" />
<div class="wrapper wrapper-component-editor"> <div class="wrapper wrapper-component-editor">
<div class="component-editor"> <div class="component-editor">
<div class="component-edit-header">
<span class="component-name"></span>
<ul class="nav-edit-modes">
<li id="editor-mode" class="mode active-mode" aria-controls="editor-tab" role="tab">
<a href="#">${_("Editor")}</a>
</li>
<li id="settings-mode" class="mode active-mode" aria-controls="settings-tab" role="tab">
<a href="#">${_("Settings")}</a>
</li>
</ul>
</div> <!-- Editor Header -->
<div class="component-edit-modes">
<div class="module-editor"> <div class="module-editor">
${editor} ${editor}
</div> </div>
<div class="row module-actions">
<a href="#" class="save-button">Save</a>
<a href="#" class="cancel-button">Cancel</a>
</div> </div>
<div class="row module-actions">
<a href="#" class="save-button">${_("Save")}</a>
<a href="#" class="cancel-button">${_("Cancel")}</a>
</div> <!-- Module Actions-->
</div> </div>
</div> </div>
<div class="component-actions"> <div class="component-actions">
<a href="#" class="edit-button standard"><span class="edit-icon"></span>Edit</a> <a href="#" class="edit-button standard"><span class="edit-icon"></span>${_("Edit")}</a>
<a href="#" class="delete-button standard"><span class="delete-icon"></span>Delete</a> <a href="#" class="delete-button standard"><span class="delete-icon"></span>${_("Delete")}</a>
</div> </div>
<a data-tooltip="Drag to reorder" href="#" class="drag-handle"></a> <a data-tooltip='${_("Drag to reorder")}' href="#" class="drag-handle"></a>
${preview} ${preview}
<ul class="list-input settings-list">
<% _.each(_.range(numEntries), function() { %>
<li class="field comp-setting-entry metadata_entry" id="settings-listing">
</li>
<% }) %>
</ul>
<div class="wrapper-comp-setting">
<label class="label setting-label" for="<%= uniqueId %>"><%= model.get('display_name') %></label>
<input class="input setting-input setting-input-number" type="number" id="<%= uniqueId %>" value='<%= model.get("value") %>'/>
<button class="action setting-clear inactive" type="button" name="setting-clear" value="<%= gettext("Clear") %>" data-tooltip="<%= gettext("Clear") %>">
<i class="icon-undo"></i><span class="sr">"<%= gettext("Clear Value") %>"</span>
</button>
</div>
<span class="tip setting-help"><%= model.get('help') %></span>
<div class="wrapper-comp-setting">
<label class="label setting-label" for="<%= uniqueId %>"><%= model.get('display_name') %></label>
<select class="input setting-input" id="<%= uniqueId %>" name="<%= model.get('display_name') %>">
<% _.each(model.get('options'), function(option) { %>
<% if (option.display_name !== undefined) { %>
<option value="<%= option['display_name'] %>"><%= option['display_name'] %></option>
<% } else { %>
<option value="<%= option %>"><%= option %></option>
<% } %>
<% }) %>
</select>
<button class="action setting-clear inactive" type="button" name="setting-clear" value="<%= gettext("Clear") %>" data-tooltip="<%= gettext("Clear") %>">
<i class="icon-undo"></i><span class="sr">"<%= gettext("Clear Value") %>"</span>
</button>
</div>
<span class="tip setting-help"><%= model.get('help') %></span>
<div class="wrapper-comp-setting">
<label class="label setting-label" for="<%= uniqueId %>"><%= model.get('display_name') %></label>
<input class="input setting-input" type="text" id="<%= uniqueId %>" value='<%= model.get("value") %>'/>
<button class="action setting-clear inactive" type="button" name="setting-clear" value="<%= gettext("Clear") %>" data-tooltip="<%= gettext("Clear") %>">
<i class="icon-undo"></i><span class="sr">"<%= gettext("Clear Value") %>"</span>
</button>
</div>
<span class="tip setting-help"><%= model.get('help') %></span>
...@@ -3,6 +3,12 @@ ...@@ -3,6 +3,12 @@
<%namespace name='static' file='../static_content.html'/> <%namespace name='static' file='../static_content.html'/>
%if not user_logged_in:
<%block name="bodyclass">
not-signedin
</%block>
%endif
<%block name="content"> <%block name="content">
<section class="container activation"> <section class="container activation">
...@@ -18,7 +24,7 @@ ...@@ -18,7 +24,7 @@
%if user_logged_in: %if user_logged_in:
Visit your <a href="/">dashboard</a> to see your courses. Visit your <a href="/">dashboard</a> to see your courses.
%else: %else:
You can now <a href="#login-modal" rel="leanModal">login</a>. You can now <a href="${reverse('login')}">login</a>.
%endif %endif
</p> </p>
</section> </section>
......
...@@ -87,23 +87,14 @@ ...@@ -87,23 +87,14 @@
% endif % endif
<div class="tab current" id="tab1"> <div class="tab current" id="tab1">
<ul class="new-component-template"> <ul class="new-component-template">
% for name, location, has_markdown, is_empty in templates: % for name, location, has_markdown in templates:
% if has_markdown or type != "problem": % if has_markdown or type != "problem":
% if is_empty:
<li class="editor-md empty">
<a href="#" data-location="${location}" id="${location}">
<span class="name"> ${name}</span>
</a>
</li>
% else:
<li class="editor-md"> <li class="editor-md">
<a href="#" data-location="${location}" id="${location}"> <a href="#" id="${location}" data-location="${location}">
<span class="name"> ${name}</span> <span class="name"> ${name}</span>
</a> </a>
</li> </li>
% endif % endif
% endif
%endfor %endfor
</ul> </ul>
...@@ -111,24 +102,14 @@ ...@@ -111,24 +102,14 @@
% if type == "problem": % if type == "problem":
<div class="tab" id="tab2"> <div class="tab" id="tab2">
<ul class="new-component-template"> <ul class="new-component-template">
% for name, location, has_markdown, is_empty in templates: % for name, location, has_markdown in templates:
% if not has_markdown: % if not has_markdown:
% if is_empty:
<li class="editor-manual empty">
<a href="#" data-location="${location}" id="${location}">
<span class="name">${name}</span>
</a>
</li>
% else:
<li class="editor-manual"> <li class="editor-manual">
<a href="#" data-location="${location}" id="${location}"> <a href="#" id="${location}" data-location="${location}">
<span class="name"> ${name}</span> <span class="name"> ${name}</span>
</a> </a>
</li> </li>
% endif % endif
% endif
% endfor % endfor
</ul> </ul>
</div> </div>
......
<%include file="metadata-edit.html" /> <%! from django.utils.translation import ugettext as _ %>
<section class="html-editor editor">
<div class="wrapper-comp-editor" id="editor-tab">
<section class="html-editor editor">
<ul class="editor-tabs"> <ul class="editor-tabs">
<li><a href="#" class="visual-tab tab current" data-tab="visual">Visual</a></li> <li><a href="#" class="visual-tab tab current" data-tab="visual">${_("Visual")}</a></li>
<li><a href="#" class="html-tab tab" data-tab="advanced">HTML</a></li> <li><a href="#" class="html-tab tab" data-tab="advanced">${_("HTML")}</a></li>
</ul> </ul>
<div class="row"> <div class="row">
<textarea class="tiny-mce">${data | h}</textarea> <textarea class="tiny-mce">${data | h}</textarea>
<textarea name="" class="edit-box">${data | h}</textarea> <textarea name="" class="edit-box">${data | h}</textarea>
</div> </div>
</section> </section>
</div>
<%include file="metadata-edit.html" />
<%! from django.utils.translation import ugettext as _ %>
<%namespace name='static' file='../static_content.html'/>
<% <%
import hashlib import hashlib
from xmodule.fields import StringyInteger, StringyFloat import copy
import json
hlskey = hashlib.md5(module.location.url()).hexdigest() hlskey = hashlib.md5(module.location.url()).hexdigest()
%> %>
<section class="metadata_edit">
<ul> ## js templates
% for field_name, field_value in editable_metadata_fields.items(): <script id="metadata-editor-tpl" type="text/template">
<li> <%static:include path="js/metadata-editor.underscore" />
% if field_name == 'source_code': </script>
% if field_value['explicitly_set'] is True:
<a href="#hls-modal-${hlskey}" style="color:yellow;" id="hls-trig-${hlskey}" >Edit High Level Source</a> <script id="metadata-number-entry" type="text/template">
% endif <%static:include path="js/metadata-number-entry.underscore" />
% else: </script>
<label>${field_value['field'].display_name}:</label>
<input type='text' data-metadata-name='${field_value["field"].display_name}' <script id="metadata-string-entry" type="text/template">
## This is a hack to keep current behavior for weight and attempts (empty will parse OK as unset). <%static:include path="js/metadata-string-entry.underscore" />
## This hack will go away with our custom editors. </script>
% if field_value["value"] == None and (isinstance(field_value["field"], StringyFloat) or isinstance(field_value["field"], StringyInteger)):
value = '' <script id="metadata-option-entry" type="text/template">
% else: <%static:include path="js/metadata-option-entry.underscore" />
value='${field_value["field"].to_json(field_value["value"])}' </script>
% endif
size='60' /> <% showHighLevelSource='source_code' in editable_metadata_fields and editable_metadata_fields['source_code']['explicitly_set'] %>
## Change to True to see all the information being passed through. <% metadata_field_copy = copy.copy(editable_metadata_fields) %>
% if False: ## Delete 'source_code' field (if it exists) so metadata editor view does not attempt to render it.
<label>Help: ${field_value['field'].help}</label> % if 'source_code' in editable_metadata_fields:
<label>Type: ${type(field_value['field']).__name__}</label> ## source-edit.html needs access to the 'source_code' value, so delete from a copy.
<label>Inheritable: ${field_value['inheritable']}</label> <% del metadata_field_copy['source_code'] %>
<label>Showing inherited value: ${field_value['inheritable'] and not field_value['explicitly_set']}</label> % endif
<label>Explicitly set: ${field_value['explicitly_set']}</label>
<label>Default value: ${field_value['default_value']}</label> % if showHighLevelSource:
% if field_value['field'].values: <div class="launch-latex-compiler">
<label>Possible values:</label> <a href="#hls-modal-${hlskey}" id="hls-trig-${hlskey}">${_("Launch Latex Source Compiler")}</a>
% for value in field_value['field'].values: </div>
<label>${value}</label>
% endfor
% endif
% endif
% endif
</li>
% endfor
</ul>
% if 'source_code' in editable_metadata_fields and editable_metadata_fields['source_code']['explicitly_set']:
<%include file="source-edit.html" /> <%include file="source-edit.html" />
% endif % endif
</section> <div class="wrapper-comp-settings metadata_edit" id="settings-tab" data-metadata='${json.dumps(metadata_field_copy) | h}'/>
\ No newline at end of file
<%include file="metadata-edit.html" /> <div class="wrapper-comp-editor" id="editor-tab">
<section class="combinedopenended-editor editor"> <section class="combinedopenended-editor editor">
<div class="row"> <div class="row">
%if enable_markdown: %if enable_markdown:
...@@ -93,3 +93,5 @@ ...@@ -93,3 +93,5 @@
</div> </div>
</article> </article>
</script> </script>
</div>
<%include file="metadata-edit.html" />
<%include file="metadata-edit.html" /> <%! from django.utils.translation import ugettext as _ %>
<div class="wrapper-comp-editor" id="editor-tab">
<section class="problem-editor editor"> <section class="problem-editor editor">
<div class="row"> <div class="row">
%if enable_markdown: %if enable_markdown:
<div class="editor-bar"> <div class="editor-bar">
<ul class="format-buttons"> <ul class="format-buttons">
<li><a href="#" class="header-button" data-tooltip="Heading 1"><span <li><a href="#" class="header-button" data-tooltip='${_("Heading 1")}'><span
class="problem-editor-icon heading1"></span></a></li> class="problem-editor-icon heading1"></span></a></li>
<li><a href="#" class="multiple-choice-button" data-tooltip="Multiple Choice"><span <li><a href="#" class="multiple-choice-button" data-tooltip='${_("Multiple Choice")}'><span
class="problem-editor-icon multiple-choice"></span></a></li> class="problem-editor-icon multiple-choice"></span></a></li>
<li><a href="#" class="checks-button" data-tooltip="Checkboxes"><span <li><a href="#" class="checks-button" data-tooltip='${_("Checkboxes")}'><span
class="problem-editor-icon checks"></span></a></li> class="problem-editor-icon checks"></span></a></li>
<li><a href="#" class="string-button" data-tooltip="Text Input"><span <li><a href="#" class="string-button" data-tooltip='${_("Text Input")}'><span
class="problem-editor-icon string"></span></a></li> class="problem-editor-icon string"></span></a></li>
<li><a href="#" class="number-button" data-tooltip="Numerical Input"><span <li><a href="#" class="number-button" data-tooltip='${_("Numerical Input")}'><span
class="problem-editor-icon number"></span></a></li> class="problem-editor-icon number"></span></a></li>
<li><a href="#" class="dropdown-button" data-tooltip="Dropdown"><span <li><a href="#" class="dropdown-button" data-tooltip='${_("Dropdown")}'><span
class="problem-editor-icon dropdown"></span></a></li> class="problem-editor-icon dropdown"></span></a></li>
<li><a href="#" class="explanation-button" data-tooltip="Explanation"><span <li><a href="#" class="explanation-button" data-tooltip='${_("Explanation")}'><span
class="problem-editor-icon explanation"></span></a></li> class="problem-editor-icon explanation"></span></a></li>
</ul> </ul>
<ul class="editor-tabs"> <ul class="editor-tabs">
<li><a href="#" class="xml-tab advanced-toggle" data-tab="xml">Advanced Editor</a></li> <li><a href="#" class="xml-tab advanced-toggle" data-tab="xml">${_("Advanced Editor")}</a></li>
<li><a href="#" class="cheatsheet-toggle" data-tooltip="Toggle Cheatsheet">?</a></li> <li><a href="#" class="cheatsheet-toggle" data-tooltip='${_("Toggle Cheatsheet")}'>?</a></li>
</ul> </ul>
</div> </div>
<textarea class="markdown-box">${markdown | h}</textarea> <textarea class="markdown-box">${markdown | h}</textarea>
...@@ -34,7 +36,7 @@ ...@@ -34,7 +36,7 @@
<article class="simple-editor-cheatsheet"> <article class="simple-editor-cheatsheet">
<div class="cheatsheet-wrapper"> <div class="cheatsheet-wrapper">
<div class="row"> <div class="row">
<h6>Heading 1</h6> <h6>${_("Heading 1")}</h6>
<div class="col sample heading-1"> <div class="col sample heading-1">
<img src="/static/img/header-example.png" /> <img src="/static/img/header-example.png" />
</div> </div>
...@@ -45,7 +47,7 @@ ...@@ -45,7 +47,7 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<h6>Multiple Choice</h6> <h6>${_("Multiple Choice")}</h6>
<div class="col sample multiple-choice"> <div class="col sample multiple-choice">
<img src="/static/img/choice-example.png" /> <img src="/static/img/choice-example.png" />
</div> </div>
...@@ -56,7 +58,7 @@ ...@@ -56,7 +58,7 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<h6>Checkboxes</h6> <h6>${_("Checkboxes")}</h6>
<div class="col sample check-multiple"> <div class="col sample check-multiple">
<img src="/static/img/multi-example.png" /> <img src="/static/img/multi-example.png" />
</div> </div>
...@@ -67,7 +69,7 @@ ...@@ -67,7 +69,7 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<h6>Text Input</h6> <h6>${_("Text Input")}</h6>
<div class="col sample string-response"> <div class="col sample string-response">
<img src="/static/img/string-example.png" /> <img src="/static/img/string-example.png" />
</div> </div>
...@@ -76,7 +78,7 @@ ...@@ -76,7 +78,7 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<h6>Numerical Input</h6> <h6>${_("Numerical Input")}</h6>
<div class="col sample numerical-response"> <div class="col sample numerical-response">
<img src="/static/img/number-example.png" /> <img src="/static/img/number-example.png" />
</div> </div>
...@@ -85,7 +87,7 @@ ...@@ -85,7 +87,7 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<h6>Dropdown</h6> <h6>${_("Dropdown")}</h6>
<div class="col sample option-reponse"> <div class="col sample option-reponse">
<img src="/static/img/select-example.png" /> <img src="/static/img/select-example.png" />
</div> </div>
...@@ -94,7 +96,7 @@ ...@@ -94,7 +96,7 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<h6>Explanation</h6> <h6>${_("Explanation")}</h6>
<div class="col sample explanation"> <div class="col sample explanation">
<img src="/static/img/explanation-example.png" /> <img src="/static/img/explanation-example.png" />
</div> </div>
...@@ -105,3 +107,5 @@ ...@@ -105,3 +107,5 @@
</div> </div>
</article> </article>
</script> </script>
</div>
<%include file="metadata-edit.html" />
<%include file="metadata-edit.html" /> <div class="wrapper-comp-editor" id="editor-tab">
<section class="raw-edit"> <section class="raw-edit">
<textarea name="" class="edit-box" rows="8" cols="40">${data | h}</textarea> <textarea name="" class="edit-box" rows="8" cols="40">${data | h}</textarea>
</section> </section>
</div>
<%include file="metadata-edit.html" />
...@@ -29,7 +29,6 @@ ...@@ -29,7 +29,6 @@
</ul> </ul>
</section> </section>
<%include file="metadata-edit.html" />
<div class="content"> <div class="content">
<section class="modules"> <section class="modules">
<ol> <ol>
...@@ -50,5 +49,6 @@ ...@@ -50,5 +49,6 @@
</ol> </ol>
</section> </section>
</div> </div>
<%include file="metadata-edit.html" />
</section> </section>
...@@ -28,4 +28,4 @@ class CmsNamespace(Namespace): ...@@ -28,4 +28,4 @@ class CmsNamespace(Namespace):
""" """
published_date = DateTuple(help="Date when the module was published", scope=Scope.settings) published_date = DateTuple(help="Date when the module was published", scope=Scope.settings)
published_by = String(help="Id of the user who published this module", scope=Scope.settings) published_by = String(help="Id of the user who published this module", scope=Scope.settings)
empty = StringyBoolean(help="Whether this is an empty template", scope=Scope.settings, default=False)
...@@ -13,6 +13,7 @@ class UserFactory(sf.UserFactory): ...@@ -13,6 +13,7 @@ class UserFactory(sf.UserFactory):
""" """
User account for lms / cms User account for lms / cms
""" """
FACTORY_DJANGO_GET_OR_CREATE = ('username',)
pass pass
...@@ -21,6 +22,7 @@ class UserProfileFactory(sf.UserProfileFactory): ...@@ -21,6 +22,7 @@ class UserProfileFactory(sf.UserProfileFactory):
""" """
Demographics etc for the User Demographics etc for the User
""" """
FACTORY_DJANGO_GET_OR_CREATE = ('user',)
pass pass
...@@ -29,6 +31,7 @@ class RegistrationFactory(sf.RegistrationFactory): ...@@ -29,6 +31,7 @@ class RegistrationFactory(sf.RegistrationFactory):
""" """
Activation key for registering the user account Activation key for registering the user account
""" """
FACTORY_DJANGO_GET_OR_CREATE = ('user',)
pass pass
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
from lettuce import world from lettuce import world
import time import time
from urllib import quote_plus from urllib import quote_plus
from selenium.common.exceptions import WebDriverException from selenium.common.exceptions import WebDriverException, StaleElementReferenceException
from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support.ui import WebDriverWait
...@@ -63,7 +63,7 @@ def css_click(css_selector): ...@@ -63,7 +63,7 @@ def css_click(css_selector):
# Occassionally, MathJax or other JavaScript can cover up # Occassionally, MathJax or other JavaScript can cover up
# an element temporarily. # an element temporarily.
# If this happens, wait a second, then try again # If this happens, wait a second, then try again
time.sleep(1) world.wait(1)
world.browser.find_by_css(css_selector).click() world.browser.find_by_css(css_selector).click()
...@@ -80,6 +80,14 @@ def css_click_at(css, x=10, y=10): ...@@ -80,6 +80,14 @@ def css_click_at(css, x=10, y=10):
@world.absorb @world.absorb
def id_click(elem_id):
"""
Perform a click on an element as specified by its id
"""
world.css_click('#%s' % elem_id)
@world.absorb
def css_fill(css_selector, text): def css_fill(css_selector, text):
world.browser.find_by_css(css_selector).first.fill(text) world.browser.find_by_css(css_selector).first.fill(text)
...@@ -94,6 +102,11 @@ def css_text(css_selector): ...@@ -94,6 +102,11 @@ def css_text(css_selector):
# Wait for the css selector to appear # Wait for the css selector to appear
if world.is_css_present(css_selector): if world.is_css_present(css_selector):
try:
return world.browser.find_by_css(css_selector).first.text
except StaleElementReferenceException:
# The DOM was still redrawing. Wait a second and try again.
world.wait(1)
return world.browser.find_by_css(css_selector).first.text return world.browser.find_by_css(css_selector).first.text
else: else:
return "" return ""
......
...@@ -209,30 +209,3 @@ def accepts(request, media_type): ...@@ -209,30 +209,3 @@ def accepts(request, media_type):
accept = parse_accept_header(request.META.get("HTTP_ACCEPT", "")) accept = parse_accept_header(request.META.get("HTTP_ACCEPT", ""))
return media_type in [t for (t, p, q) in accept] return media_type in [t for (t, p, q) in accept]
def debug_request(request):
"""Return a pretty printed version of the request"""
return HttpResponse("""<html>
<h1>request:</h1>
<pre>{0}</pre>
<h1>request.GET</h1>:
<pre>{1}</pre>
<h1>request.POST</h1>:
<pre>{2}</pre>
<h1>request.REQUEST</h1>:
<pre>{3}</pre>
</html>
""".format(
pprint.pformat(request),
pprint.pformat(dict(request.GET)),
pprint.pformat(dict(request.POST)),
pprint.pformat(dict(request.REQUEST)),
))
# .coveragerc for common/lib/calc
[run]
data_file = reports/common/lib/calc/.coverage
source = common/lib/calc
branch = true
[report]
ignore_errors = True
[html]
title = Calc Python Test Coverage Report
directory = reports/common/lib/calc/cover
[xml]
output = reports/common/lib/calc/coverage.xml
...@@ -144,6 +144,8 @@ def evaluator(variables, functions, string, cs=False): ...@@ -144,6 +144,8 @@ def evaluator(variables, functions, string, cs=False):
return x return x
def parallel(x): # Parallel resistors [ 1 2 ] => 2/3 def parallel(x): # Parallel resistors [ 1 2 ] => 2/3
# convert from pyparsing.ParseResults, which doesn't support '0 in x'
x = list(x)
if len(x) == 1: if len(x) == 1:
return x[0] return x[0]
if 0 in x: if 0 in x:
...@@ -180,8 +182,8 @@ def evaluator(variables, functions, string, cs=False): ...@@ -180,8 +182,8 @@ def evaluator(variables, functions, string, cs=False):
number_part = Word(nums) number_part = Word(nums)
# 0.33 or 7 or .34 # 0.33 or 7 or .34 or 16.
inner_number = (number_part + Optional("." + number_part)) | ("." + number_part) inner_number = (number_part + Optional("." + Optional(number_part))) | ("." + number_part)
# 0.33k or -17 # 0.33k or -17
number = (Optional(minus | plus) + inner_number number = (Optional(minus | plus) + inner_number
...@@ -230,27 +232,3 @@ def evaluator(variables, functions, string, cs=False): ...@@ -230,27 +232,3 @@ def evaluator(variables, functions, string, cs=False):
expr << Optional((plus | minus)) + term + ZeroOrMore((plus | minus) + term) # -5 + 4 - 3 expr << Optional((plus | minus)) + term + ZeroOrMore((plus | minus) + term) # -5 + 4 - 3
expr = expr.setParseAction(sum_parse_action) expr = expr.setParseAction(sum_parse_action)
return (expr + stringEnd).parseString(string)[0] return (expr + stringEnd).parseString(string)[0]
if __name__ == '__main__':
variables = {'R1': 2.0, 'R3': 4.0}
functions = {'sin': numpy.sin, 'cos': numpy.cos}
print "X", evaluator(variables, functions, "10000||sin(7+5)-6k")
print "X", evaluator(variables, functions, "13")
print evaluator({'R1': 2.0, 'R3': 4.0}, {}, "13")
print evaluator({'e1': 1, 'e2': 1.0, 'R3': 7, 'V0': 5, 'R5': 15, 'I1': 1, 'R4': 6}, {}, "e2")
print evaluator({'a': 2.2997471478310274, 'k': 9, 'm': 8, 'x': 0.66009498411213041}, {}, "5")
print evaluator({}, {}, "-1")
print evaluator({}, {}, "-(7+5)")
print evaluator({}, {}, "-0.33")
print evaluator({}, {}, "-.33")
print evaluator({}, {}, "5+1*j")
print evaluator({}, {}, "j||1")
print evaluator({}, {}, "e^(j*pi)")
print evaluator({}, {}, "fact(5)")
print evaluator({}, {}, "factorial(5)")
try:
print evaluator({}, {}, "5+7 QWSEKO")
except UndefinedVariable:
print "Successfully caught undefined variable"
"""
Unit tests for calc.py
"""
import unittest
import numpy
import calc
from pyparsing import ParseException
class EvaluatorTest(unittest.TestCase):
"""
Run tests for calc.evaluator
Go through all functionalities as specifically as possible--
work from number input to functions and complex expressions
Also test custom variable substitutions (i.e.
`evaluator({'x':3.0},{}, '3*x')`
gives 9.0) and more.
"""
def test_number_input(self):
"""
Test different kinds of float inputs
See also
test_trailing_period (slightly different)
test_exponential_answer
test_si_suffix
"""
easy_eval = lambda x: calc.evaluator({}, {}, x)
self.assertEqual(easy_eval("13"), 13)
self.assertEqual(easy_eval("3.14"), 3.14)
self.assertEqual(easy_eval(".618033989"), 0.618033989)
self.assertEqual(easy_eval("-13"), -13)
self.assertEqual(easy_eval("-3.14"), -3.14)
self.assertEqual(easy_eval("-.618033989"), -0.618033989)
def test_period(self):
"""
The string '.' should not evaluate to anything.
"""
self.assertRaises(ParseException, calc.evaluator, {}, {}, '.')
self.assertRaises(ParseException, calc.evaluator, {}, {}, '1+.')
def test_trailing_period(self):
"""
Test that things like '4.' will be 4 and not throw an error
"""
try:
self.assertEqual(4.0, calc.evaluator({}, {}, '4.'))
except ParseException:
self.fail("'4.' is a valid input, but threw an exception")
def test_exponential_answer(self):
"""
Test for correct interpretation of scientific notation
"""
answer = 50
correct_responses = ["50", "50.0", "5e1", "5e+1",
"50e0", "50.0e0", "500e-1"]
incorrect_responses = ["", "3.9", "4.1", "0", "5.01e1"]
for input_str in correct_responses:
result = calc.evaluator({}, {}, input_str)
fail_msg = "Expected '{0}' to equal {1}".format(
input_str, answer)
self.assertEqual(answer, result, msg=fail_msg)
for input_str in incorrect_responses:
result = calc.evaluator({}, {}, input_str)
fail_msg = "Expected '{0}' to not equal {1}".format(
input_str, answer)
self.assertNotEqual(answer, result, msg=fail_msg)
def test_si_suffix(self):
"""
Test calc.py's unique functionality of interpreting si 'suffixes'.
For instance 'k' stand for 'kilo-' so '1k' should be 1,000
"""
test_mapping = [('4.2%', 0.042), ('2.25k', 2250), ('8.3M', 8300000),
('9.9G', 9.9e9), ('1.2T', 1.2e12), ('7.4c', 0.074),
('5.4m', 0.0054), ('8.7u', 0.0000087),
('5.6n', 5.6e-9), ('4.2p', 4.2e-12)]
for (expr, answer) in test_mapping:
tolerance = answer * 1e-6 # Make rel. tolerance, because of floats
fail_msg = "Failure in testing suffix '{0}': '{1}' was not {2}"
fail_msg = fail_msg.format(expr[-1], expr, answer)
self.assertAlmostEqual(calc.evaluator({}, {}, expr), answer,
delta=tolerance, msg=fail_msg)
def test_operator_sanity(self):
"""
Test for simple things like '5+2' and '5/2'
"""
var1 = 5.0
var2 = 2.0
operators = [('+', 7), ('-', 3), ('*', 10), ('/', 2.5), ('^', 25)]
for (operator, answer) in operators:
input_str = "{0} {1} {2}".format(var1, operator, var2)
result = calc.evaluator({}, {}, input_str)
fail_msg = "Failed on operator '{0}': '{1}' was not {2}".format(
operator, input_str, answer)
self.assertEqual(answer, result, msg=fail_msg)
def test_raises_zero_division_err(self):
"""
Ensure division by zero gives an error
"""
self.assertRaises(ZeroDivisionError, calc.evaluator,
{}, {}, '1/0')
self.assertRaises(ZeroDivisionError, calc.evaluator,
{}, {}, '1/0.0')
self.assertRaises(ZeroDivisionError, calc.evaluator,
{'x': 0.0}, {}, '1/x')
def test_parallel_resistors(self):
"""
Test the parallel resistor operator ||
The formula is given by
a || b || c ...
= 1 / (1/a + 1/b + 1/c + ...)
It is the resistance of a parallel circuit of resistors with resistance
a, b, c, etc&. See if this evaulates correctly.
"""
self.assertEqual(calc.evaluator({}, {}, '1||1'), 0.5)
self.assertEqual(calc.evaluator({}, {}, '1||1||2'), 0.4)
self.assertEqual(calc.evaluator({}, {}, "j||1"), 0.5 + 0.5j)
def test_parallel_resistors_with_zero(self):
"""
Check the behavior of the || operator with 0
"""
self.assertTrue(numpy.isnan(calc.evaluator({}, {}, '0||1')))
self.assertTrue(numpy.isnan(calc.evaluator({}, {}, '0.0||1')))
self.assertTrue(numpy.isnan(calc.evaluator({'x': 0.0}, {}, 'x||1')))
def assert_function_values(self, fname, ins, outs, tolerance=1e-3):
"""
Helper function to test many values at once
Test the accuracy of evaluator's use of the function given by fname
Specifically, the equality of `fname(ins[i])` against outs[i].
This is used later to test a whole bunch of f(x) = y at a time
"""
for (arg, val) in zip(ins, outs):
input_str = "{0}({1})".format(fname, arg)
result = calc.evaluator({}, {}, input_str)
fail_msg = "Failed on function {0}: '{1}' was not {2}".format(
fname, input_str, val)
self.assertAlmostEqual(val, result, delta=tolerance, msg=fail_msg)
def test_trig_functions(self):
"""
Test the trig functions provided in calc.py
which are: sin, cos, tan, arccos, arcsin, arctan
"""
angles = ['-pi/4', '0', 'pi/6', 'pi/5', '5*pi/4', '9*pi/4', '1 + j']
sin_values = [-0.707, 0, 0.5, 0.588, -0.707, 0.707, 1.298 + 0.635j]
cos_values = [0.707, 1, 0.866, 0.809, -0.707, 0.707, 0.834 - 0.989j]
tan_values = [-1, 0, 0.577, 0.727, 1, 1, 0.272 + 1.084j]
# Cannot test tan(pi/2) b/c pi/2 is a float and not precise...
self.assert_function_values('sin', angles, sin_values)
self.assert_function_values('cos', angles, cos_values)
self.assert_function_values('tan', angles, tan_values)
# Include those where the real part is between -pi/2 and pi/2
arcsin_inputs = ['-0.707', '0', '0.5', '0.588', '1.298 + 0.635*j']
arcsin_angles = [-0.785, 0, 0.524, 0.629, 1 + 1j]
self.assert_function_values('arcsin', arcsin_inputs, arcsin_angles)
# Rather than throwing an exception, numpy.arcsin gives nan
# self.assertTrue(numpy.isnan(calc.evaluator({}, {}, 'arcsin(-1.1)')))
# self.assertTrue(numpy.isnan(calc.evaluator({}, {}, 'arcsin(1.1)')))
# Disabled for now because they are giving a runtime warning... :-/
# Include those where the real part is between 0 and pi
arccos_inputs = ['1', '0.866', '0.809', '0.834-0.989*j']
arccos_angles = [0, 0.524, 0.628, 1 + 1j]
self.assert_function_values('arccos', arccos_inputs, arccos_angles)
# self.assertTrue(numpy.isnan(calc.evaluator({}, {}, 'arccos(-1.1)')))
# self.assertTrue(numpy.isnan(calc.evaluator({}, {}, 'arccos(1.1)')))
# Has the same range as arcsin
arctan_inputs = ['-1', '0', '0.577', '0.727', '0.272 + 1.084*j']
arctan_angles = arcsin_angles
self.assert_function_values('arctan', arctan_inputs, arctan_angles)
def test_other_functions(self):
"""
Test the non-trig functions provided in calc.py
Specifically:
sqrt, log10, log2, ln, abs,
fact, factorial
"""
# Test sqrt
self.assert_function_values('sqrt',
[0, 1, 2, 1024], # -1
[0, 1, 1.414, 32]) # 1j
# sqrt(-1) is NAN not j (!!).
# Test logs
self.assert_function_values('log10',
[0.1, 1, 3.162, 1000000, '1+j'],
[-1, 0, 0.5, 6, 0.151 + 0.341j])
self.assert_function_values('log2',
[0.5, 1, 1.414, 1024, '1+j'],
[-1, 0, 0.5, 10, 0.5 + 1.133j])
self.assert_function_values('ln',
[0.368, 1, 1.649, 2.718, 42, '1+j'],
[-1, 0, 0.5, 1, 3.738, 0.347 + 0.785j])
# Test abs
self.assert_function_values('abs', [-1, 0, 1, 'j'], [1, 0, 1, 1])
# Test factorial
fact_inputs = [0, 1, 3, 7]
fact_values = [1, 1, 6, 5040]
self.assert_function_values('fact', fact_inputs, fact_values)
self.assert_function_values('factorial', fact_inputs, fact_values)
self.assertRaises(ValueError, calc.evaluator, {}, {}, "fact(-1)")
self.assertRaises(ValueError, calc.evaluator, {}, {}, "fact(0.5)")
self.assertRaises(ValueError, calc.evaluator, {}, {}, "factorial(-1)")
self.assertRaises(ValueError, calc.evaluator, {}, {}, "factorial(0.5)")
def test_constants(self):
"""
Test the default constants provided in calc.py
which are: j (complex number), e, pi, k, c, T, q
"""
# Of the form ('expr', python value, tolerance (or None for exact))
default_variables = [('j', 1j, None),
('e', 2.7183, 1e-3),
('pi', 3.1416, 1e-3),
# c = speed of light
('c', 2.998e8, 1e5),
# 0 deg C = T Kelvin
('T', 298.15, 0.01),
# Note k = scipy.constants.k = 1.3806488e-23
('k', 1.3806488e-23, 1e-26),
# Note q = scipy.constants.e = 1.602176565e-19
('q', 1.602176565e-19, 1e-22)]
for (variable, value, tolerance) in default_variables:
fail_msg = "Failed on constant '{0}', not within bounds".format(
variable)
result = calc.evaluator({}, {}, variable)
if tolerance is None:
self.assertEqual(value, result, msg=fail_msg)
else:
self.assertAlmostEqual(value, result,
delta=tolerance, msg=fail_msg)
def test_complex_expression(self):
"""
Calculate combinations of operators and default functions
"""
self.assertAlmostEqual(
calc.evaluator({}, {}, "(2^2+1.0)/sqrt(5e0)*5-1"),
10.180,
delta=1e-3)
self.assertAlmostEqual(
calc.evaluator({}, {}, "1+1/(1+1/(1+1/(1+1)))"),
1.6,
delta=1e-3)
self.assertAlmostEqual(
calc.evaluator({}, {}, "10||sin(7+5)"),
-0.567, delta=0.01)
self.assertAlmostEqual(calc.evaluator({}, {}, "sin(e)"),
0.41, delta=0.01)
self.assertAlmostEqual(calc.evaluator({}, {}, "k*T/q"),
0.025, delta=1e-3)
self.assertAlmostEqual(calc.evaluator({}, {}, "e^(j*pi)"),
-1, delta=1e-5)
def test_simple_vars(self):
"""
Substitution of variables into simple equations
"""
variables = {'x': 9.72, 'y': 7.91, 'loooooong': 6.4}
# Should not change value of constant
# even with different numbers of variables...
self.assertEqual(calc.evaluator({'x': 9.72}, {}, '13'), 13)
self.assertEqual(calc.evaluator({'x': 9.72, 'y': 7.91}, {}, '13'), 13)
self.assertEqual(calc.evaluator(variables, {}, '13'), 13)
# Easy evaluation
self.assertEqual(calc.evaluator(variables, {}, 'x'), 9.72)
self.assertEqual(calc.evaluator(variables, {}, 'y'), 7.91)
self.assertEqual(calc.evaluator(variables, {}, 'loooooong'), 6.4)
# Test a simple equation
self.assertAlmostEqual(calc.evaluator(variables, {}, '3*x-y'),
21.25, delta=0.01) # = 3 * 9.72 - 7.91
self.assertAlmostEqual(calc.evaluator(variables, {}, 'x*y'),
76.89, delta=0.01)
self.assertEqual(calc.evaluator({'x': 9.72, 'y': 7.91}, {}, "13"), 13)
self.assertEqual(calc.evaluator(variables, {}, "13"), 13)
self.assertEqual(
calc.evaluator({
'a': 2.2997471478310274, 'k': 9, 'm': 8,
'x': 0.66009498411213041},
{}, "5"),
5)
def test_variable_case_sensitivity(self):
"""
Test the case sensitivity flag and corresponding behavior
"""
self.assertEqual(
calc.evaluator({'R1': 2.0, 'R3': 4.0}, {}, "r1*r3"),
8.0)
variables = {'t': 1.0}
self.assertEqual(calc.evaluator(variables, {}, "t"), 1.0)
self.assertEqual(calc.evaluator(variables, {}, "T"), 1.0)
self.assertEqual(calc.evaluator(variables, {}, "t", cs=True), 1.0)
# Recall 'T' is a default constant, with value 298.15
self.assertAlmostEqual(calc.evaluator(variables, {}, "T", cs=True),
298, delta=0.2)
def test_simple_funcs(self):
"""
Subsitution of custom functions
"""
variables = {'x': 4.712}
functions = {'id': lambda x: x}
self.assertEqual(calc.evaluator({}, functions, 'id(2.81)'), 2.81)
self.assertEqual(calc.evaluator({}, functions, 'id(2.81)'), 2.81)
self.assertEqual(calc.evaluator(variables, functions, 'id(x)'), 4.712)
functions.update({'f': numpy.sin})
self.assertAlmostEqual(calc.evaluator(variables, functions, 'f(x)'),
-1, delta=1e-3)
def test_function_case_sensitivity(self):
"""
Test the case sensitivity of functions
"""
functions = {'f': lambda x: x,
'F': lambda x: x + 1}
# Test case insensitive evaluation
# Both evaulations should call the same function
self.assertEqual(calc.evaluator({}, functions, 'f(6)'),
calc.evaluator({}, functions, 'F(6)'))
# Test case sensitive evaluation
self.assertNotEqual(calc.evaluator({}, functions, 'f(6)', cs=True),
calc.evaluator({}, functions, 'F(6)', cs=True))
def test_undefined_vars(self):
"""
Check to see if the evaluator catches undefined variables
"""
variables = {'R1': 2.0, 'R3': 4.0}
self.assertRaises(calc.UndefinedVariable, calc.evaluator,
{}, {}, "5+7 QWSEKO")
self.assertRaises(calc.UndefinedVariable, calc.evaluator,
{'r1': 5}, {}, "r1+r2")
self.assertRaises(calc.UndefinedVariable, calc.evaluator,
variables, {}, "r1*r3", cs=True)
...@@ -469,6 +469,7 @@ class LoncapaProblem(object): ...@@ -469,6 +469,7 @@ class LoncapaProblem(object):
random_seed=self.seed, random_seed=self.seed,
python_path=python_path, python_path=python_path,
cache=self.system.cache, cache=self.system.cache,
slug=self.problem_id,
) )
except Exception as err: except Exception as err:
log.exception("Error while execing script code: " + all_code) log.exception("Error while execing script code: " + all_code)
......
...@@ -140,6 +140,8 @@ class LoncapaResponse(object): ...@@ -140,6 +140,8 @@ class LoncapaResponse(object):
self.context = context self.context = context
self.system = system self.system = system
self.id = xml.get('id')
for abox in inputfields: for abox in inputfields:
if abox.tag not in self.allowed_inputfields: if abox.tag not in self.allowed_inputfields:
msg = "%s: cannot have input field %s" % ( msg = "%s: cannot have input field %s" % (
...@@ -286,7 +288,7 @@ class LoncapaResponse(object): ...@@ -286,7 +288,7 @@ class LoncapaResponse(object):
} }
try: try:
safe_exec.safe_exec(code, globals_dict, python_path=self.context['python_path']) safe_exec.safe_exec(code, globals_dict, python_path=self.context['python_path'], slug=self.id)
except Exception as err: except Exception as err:
msg = 'Error %s in evaluating hint function %s' % (err, hintfn) msg = 'Error %s in evaluating hint function %s' % (err, hintfn)
msg += "\nSee XML source line %s" % getattr( msg += "\nSee XML source line %s" % getattr(
...@@ -935,7 +937,6 @@ class CustomResponse(LoncapaResponse): ...@@ -935,7 +937,6 @@ class CustomResponse(LoncapaResponse):
# if <customresponse> has an "expect" (or "answer") attribute then save # if <customresponse> has an "expect" (or "answer") attribute then save
# that # that
self.expect = xml.get('expect') or xml.get('answer') self.expect = xml.get('expect') or xml.get('answer')
self.myid = xml.get('id')
log.debug('answer_ids=%s' % self.answer_ids) log.debug('answer_ids=%s' % self.answer_ids)
...@@ -972,7 +973,7 @@ class CustomResponse(LoncapaResponse): ...@@ -972,7 +973,7 @@ class CustomResponse(LoncapaResponse):
'ans': ans, 'ans': ans,
} }
globals_dict.update(kwargs) globals_dict.update(kwargs)
safe_exec.safe_exec(code, globals_dict, python_path=self.context['python_path']) safe_exec.safe_exec(code, globals_dict, python_path=self.context['python_path'], slug=self.id)
return globals_dict['cfn_return'] return globals_dict['cfn_return']
return check_function return check_function
...@@ -981,7 +982,7 @@ class CustomResponse(LoncapaResponse): ...@@ -981,7 +982,7 @@ class CustomResponse(LoncapaResponse):
if not self.code: if not self.code:
if answer is None: if answer is None:
log.error("[courseware.capa.responsetypes.customresponse] missing" log.error("[courseware.capa.responsetypes.customresponse] missing"
" code checking script! id=%s" % self.myid) " code checking script! id=%s" % self.id)
self.code = '' self.code = ''
else: else:
answer_src = answer.get('src') answer_src = answer.get('src')
...@@ -1034,7 +1035,7 @@ class CustomResponse(LoncapaResponse): ...@@ -1034,7 +1035,7 @@ class CustomResponse(LoncapaResponse):
# note that this doesn't help the "cfn" version - only the exec version # note that this doesn't help the "cfn" version - only the exec version
self.context.update({ self.context.update({
# my ID # my ID
'response_id': self.myid, 'response_id': self.id,
# expected answer (if given as attribute) # expected answer (if given as attribute)
'expect': self.expect, 'expect': self.expect,
...@@ -1089,7 +1090,7 @@ class CustomResponse(LoncapaResponse): ...@@ -1089,7 +1090,7 @@ class CustomResponse(LoncapaResponse):
# exec the check function # exec the check function
if isinstance(self.code, basestring): if isinstance(self.code, basestring):
try: try:
safe_exec.safe_exec(self.code, self.context, cache=self.system.cache) safe_exec.safe_exec(self.code, self.context, cache=self.system.cache, slug=self.id)
except Exception as err: except Exception as err:
self._handle_exec_exception(err) self._handle_exec_exception(err)
...@@ -1813,7 +1814,7 @@ class SchematicResponse(LoncapaResponse): ...@@ -1813,7 +1814,7 @@ class SchematicResponse(LoncapaResponse):
] ]
self.context.update({'submission': submission}) self.context.update({'submission': submission})
try: try:
safe_exec.safe_exec(self.code, self.context, cache=self.system.cache) safe_exec.safe_exec(self.code, self.context, cache=self.system.cache, slug=self.id)
except Exception as err: except Exception as err:
msg = 'Error %s in evaluating SchematicResponse' % err msg = 'Error %s in evaluating SchematicResponse' % err
raise ResponseError(msg) raise ResponseError(msg)
......
...@@ -71,7 +71,7 @@ def update_hash(hasher, obj): ...@@ -71,7 +71,7 @@ def update_hash(hasher, obj):
@statsd.timed('capa.safe_exec.time') @statsd.timed('capa.safe_exec.time')
def safe_exec(code, globals_dict, random_seed=None, python_path=None, cache=None): def safe_exec(code, globals_dict, random_seed=None, python_path=None, cache=None, slug=None):
""" """
Execute python code safely. Execute python code safely.
...@@ -87,6 +87,9 @@ def safe_exec(code, globals_dict, random_seed=None, python_path=None, cache=None ...@@ -87,6 +87,9 @@ def safe_exec(code, globals_dict, random_seed=None, python_path=None, cache=None
to cache the execution, taking into account the code, the values of the globals, to cache the execution, taking into account the code, the values of the globals,
and the random seed. and the random seed.
`slug` is an arbitrary string, a description that's meaningful to the
caller, that will be used in log messages.
""" """
# Check the cache for a previous result. # Check the cache for a previous result.
if cache: if cache:
...@@ -112,7 +115,7 @@ def safe_exec(code, globals_dict, random_seed=None, python_path=None, cache=None ...@@ -112,7 +115,7 @@ def safe_exec(code, globals_dict, random_seed=None, python_path=None, cache=None
try: try:
codejail_safe_exec( codejail_safe_exec(
code_prolog + LAZY_IMPORTS + code, globals_dict, code_prolog + LAZY_IMPORTS + code, globals_dict,
python_path=python_path, python_path=python_path, slug=slug,
) )
except SafeExecException as e: except SafeExecException as e:
emsg = e.message emsg = e.message
......
...@@ -10,7 +10,6 @@ import random ...@@ -10,7 +10,6 @@ import random
import unittest import unittest
import textwrap import textwrap
import mock import mock
import textwrap
from . import new_loncapa_problem, test_system from . import new_loncapa_problem, test_system
...@@ -223,7 +222,6 @@ class SymbolicResponseTest(ResponseTest): ...@@ -223,7 +222,6 @@ class SymbolicResponseTest(ResponseTest):
for (input_str, input_mathml) in incorrect_inputs: for (input_str, input_mathml) in incorrect_inputs:
self._assert_symbolic_grade(problem, input_str, input_mathml, 'incorrect') self._assert_symbolic_grade(problem, input_str, input_mathml, 'incorrect')
def test_complex_number_grade(self): def test_complex_number_grade(self):
problem = self.build_problem(math_display=True, problem = self.build_problem(math_display=True,
expect="[[cos(theta),i*sin(theta)],[i*sin(theta),cos(theta)]]", expect="[[cos(theta),i*sin(theta)],[i*sin(theta),cos(theta)]]",
...@@ -323,7 +321,7 @@ class SymbolicResponseTest(ResponseTest): ...@@ -323,7 +321,7 @@ class SymbolicResponseTest(ResponseTest):
dynamath_input, dynamath_input,
expected_correctness): expected_correctness):
input_dict = {'1_2_1': str(student_input), input_dict = {'1_2_1': str(student_input),
'1_2_1_dynamath': str(dynamath_input) } '1_2_1_dynamath': str(dynamath_input)}
correct_map = problem.grade_answers(input_dict) correct_map = problem.grade_answers(input_dict)
...@@ -349,10 +347,18 @@ class OptionResponseTest(ResponseTest): ...@@ -349,10 +347,18 @@ class OptionResponseTest(ResponseTest):
class FormulaResponseTest(ResponseTest): class FormulaResponseTest(ResponseTest):
"""
Test the FormulaResponse class
"""
from response_xml_factory import FormulaResponseXMLFactory from response_xml_factory import FormulaResponseXMLFactory
xml_factory_class = FormulaResponseXMLFactory xml_factory_class = FormulaResponseXMLFactory
def test_grade(self): def test_grade(self):
"""
Test basic functionality of FormulaResponse
Specifically, if it can understand equivalence of formulae
"""
# Sample variables x and y in the range [-10, 10] # Sample variables x and y in the range [-10, 10]
sample_dict = {'x': (-10, 10), 'y': (-10, 10)} sample_dict = {'x': (-10, 10), 'y': (-10, 10)}
...@@ -373,6 +379,9 @@ class FormulaResponseTest(ResponseTest): ...@@ -373,6 +379,9 @@ class FormulaResponseTest(ResponseTest):
self.assert_grade(problem, input_formula, "incorrect") self.assert_grade(problem, input_formula, "incorrect")
def test_hint(self): def test_hint(self):
"""
Test the hint-giving functionality of FormulaResponse
"""
# Sample variables x and y in the range [-10, 10] # Sample variables x and y in the range [-10, 10]
sample_dict = {'x': (-10, 10), 'y': (-10, 10)} sample_dict = {'x': (-10, 10), 'y': (-10, 10)}
...@@ -401,6 +410,10 @@ class FormulaResponseTest(ResponseTest): ...@@ -401,6 +410,10 @@ class FormulaResponseTest(ResponseTest):
'Try including the variable x') 'Try including the variable x')
def test_script(self): def test_script(self):
"""
Test if python script can be used to generate answers
"""
# Calculate the answer using a script # Calculate the answer using a script
script = "calculated_ans = 'x+x'" script = "calculated_ans = 'x+x'"
...@@ -419,7 +432,9 @@ class FormulaResponseTest(ResponseTest): ...@@ -419,7 +432,9 @@ class FormulaResponseTest(ResponseTest):
self.assert_grade(problem, '3*x', 'incorrect') self.assert_grade(problem, '3*x', 'incorrect')
def test_parallel_resistors(self): def test_parallel_resistors(self):
"""Test parallel resistors""" """
Test parallel resistors
"""
sample_dict = {'R1': (10, 10), 'R2': (2, 2), 'R3': (5, 5), 'R4': (1, 1)} sample_dict = {'R1': (10, 10), 'R2': (2, 2), 'R3': (5, 5), 'R4': (1, 1)}
# Test problem # Test problem
...@@ -440,8 +455,11 @@ class FormulaResponseTest(ResponseTest): ...@@ -440,8 +455,11 @@ class FormulaResponseTest(ResponseTest):
self.assert_grade(problem, input_formula, "incorrect") self.assert_grade(problem, input_formula, "incorrect")
def test_default_variables(self): def test_default_variables(self):
"""Test the default variables provided in common/lib/capa/capa/calc.py""" """
# which are: j (complex number), e, pi, k, c, T, q Test the default variables provided in calc.py
which are: j (complex number), e, pi, k, c, T, q
"""
# Sample x in the range [-10,10] # Sample x in the range [-10,10]
sample_dict = {'x': (-10, 10)} sample_dict = {'x': (-10, 10)}
...@@ -464,11 +482,14 @@ class FormulaResponseTest(ResponseTest): ...@@ -464,11 +482,14 @@ class FormulaResponseTest(ResponseTest):
msg="Failed on variable {0}; the given, incorrect answer was {1} but graded 'correct'".format(var, incorrect)) msg="Failed on variable {0}; the given, incorrect answer was {1} but graded 'correct'".format(var, incorrect))
def test_default_functions(self): def test_default_functions(self):
"""Test the default functions provided in common/lib/capa/capa/calc.py""" """
# which are: sin, cos, tan, sqrt, log10, log2, ln, Test the default functions provided in common/lib/capa/capa/calc.py
# arccos, arcsin, arctan, abs,
# fact, factorial
which are:
sin, cos, tan, sqrt, log10, log2, ln,
arccos, arcsin, arctan, abs,
fact, factorial
"""
w = random.randint(3, 10) w = random.randint(3, 10)
sample_dict = {'x': (-10, 10), # Sample x in the range [-10,10] sample_dict = {'x': (-10, 10), # Sample x in the range [-10,10]
'y': (1, 10), # Sample y in the range [1,10] - logs, arccos need positive inputs 'y': (1, 10), # Sample y in the range [1,10] - logs, arccos need positive inputs
...@@ -496,8 +517,10 @@ class FormulaResponseTest(ResponseTest): ...@@ -496,8 +517,10 @@ class FormulaResponseTest(ResponseTest):
msg="Failed on function {0}; the given, incorrect answer was {1} but graded 'correct'".format(func, incorrect)) msg="Failed on function {0}; the given, incorrect answer was {1} but graded 'correct'".format(func, incorrect))
def test_grade_infinity(self): def test_grade_infinity(self):
# This resolves a bug where a problem with relative tolerance would """
# pass with any arbitrarily large student answer. Test that a large input on a problem with relative tolerance isn't
erroneously marked as correct.
"""
sample_dict = {'x': (1, 2)} sample_dict = {'x': (1, 2)}
...@@ -514,8 +537,9 @@ class FormulaResponseTest(ResponseTest): ...@@ -514,8 +537,9 @@ class FormulaResponseTest(ResponseTest):
self.assert_grade(problem, input_formula, "incorrect") self.assert_grade(problem, input_formula, "incorrect")
def test_grade_nan(self): def test_grade_nan(self):
# Attempt to produce a value which causes the student's answer to be """
# evaluated to nan. See if this is resolved correctly. Test that expressions that evaluate to NaN are not marked as correct.
"""
sample_dict = {'x': (1, 2)} sample_dict = {'x': (1, 2)}
...@@ -532,6 +556,18 @@ class FormulaResponseTest(ResponseTest): ...@@ -532,6 +556,18 @@ class FormulaResponseTest(ResponseTest):
input_formula = "x + 0*1e999" input_formula = "x + 0*1e999"
self.assert_grade(problem, input_formula, "incorrect") self.assert_grade(problem, input_formula, "incorrect")
def test_raises_zero_division_err(self):
"""
See if division by zero raises an error.
"""
sample_dict = {'x': (1, 2)}
problem = self.build_problem(sample_dict=sample_dict,
num_samples=10,
tolerance="1%",
answer="x") # Answer doesn't matter
input_dict = {'1_2_1': '1/0'}
self.assertRaises(StudentInputError, problem.grade_answers, input_dict)
class StringResponseTest(ResponseTest): class StringResponseTest(ResponseTest):
from response_xml_factory import StringResponseXMLFactory from response_xml_factory import StringResponseXMLFactory
...@@ -592,7 +628,7 @@ class StringResponseTest(ResponseTest): ...@@ -592,7 +628,7 @@ class StringResponseTest(ResponseTest):
problem = self.build_problem( problem = self.build_problem(
answer="Michigan", answer="Michigan",
hintfn="gimme_a_hint", hintfn="gimme_a_hint",
script = textwrap.dedent(""" script=textwrap.dedent("""
def gimme_a_hint(answer_ids, student_answers, new_cmap, old_cmap): def gimme_a_hint(answer_ids, student_answers, new_cmap, old_cmap):
aid = answer_ids[0] aid = answer_ids[0]
answer = student_answers[aid] answer = student_answers[aid]
...@@ -898,6 +934,14 @@ class NumericalResponseTest(ResponseTest): ...@@ -898,6 +934,14 @@ class NumericalResponseTest(ResponseTest):
incorrect_responses = ["", "3.9", "4.1", "0", "5.01e1"] incorrect_responses = ["", "3.9", "4.1", "0", "5.01e1"]
self.assert_multiple_grade(problem, correct_responses, incorrect_responses) self.assert_multiple_grade(problem, correct_responses, incorrect_responses)
def test_raises_zero_division_err(self):
"""See if division by zero is handled correctly"""
problem = self.build_problem(question_text="What 5 * 10?",
explanation="The answer is 50",
answer="5e+1") # Answer doesn't matter
input_dict = {'1_2_1': '1/0'}
self.assertRaises(StudentInputError, problem.grade_answers, input_dict)
class CustomResponseTest(ResponseTest): class CustomResponseTest(ResponseTest):
from response_xml_factory import CustomResponseXMLFactory from response_xml_factory import CustomResponseXMLFactory
......
...@@ -4,5 +4,5 @@ setup( ...@@ -4,5 +4,5 @@ setup(
name="capa", name="capa",
version="0.1", version="0.1",
packages=find_packages(exclude=["tests"]), packages=find_packages(exclude=["tests"]),
install_requires=["distribute==0.6.28"], install_requires=["distribute>=0.6.28"],
) )
...@@ -66,22 +66,51 @@ class ComplexEncoder(json.JSONEncoder): ...@@ -66,22 +66,51 @@ class ComplexEncoder(json.JSONEncoder):
class CapaFields(object): class CapaFields(object):
attempts = StringyInteger(help="Number of attempts taken by the student on this problem", default=0, scope=Scope.user_state) attempts = StringyInteger(help="Number of attempts taken by the student on this problem", default=0, scope=Scope.user_state)
max_attempts = StringyInteger(help="Maximum number of attempts that a student is allowed", scope=Scope.settings) max_attempts = StringyInteger(
display_name="Maximum Attempts",
help="Defines the number of times a student can try to answer this problem. If the value is not set, infinite attempts are allowed.",
values={"min": 1}, scope=Scope.settings
)
due = Date(help="Date that this problem is due by", scope=Scope.settings) due = Date(help="Date that this problem is due by", scope=Scope.settings)
graceperiod = Timedelta(help="Amount of time after the due date that submissions will be accepted", scope=Scope.settings) graceperiod = Timedelta(help="Amount of time after the due date that submissions will be accepted", scope=Scope.settings)
showanswer = String(help="When to show the problem answer to the student", scope=Scope.settings, default="closed", showanswer = String(
values=["answered", "always", "attempted", "closed", "never"]) display_name="Show Answer",
help="Defines when to show the answer to the problem. A default value can be set in Advanced Settings.",
scope=Scope.settings, default="closed",
values=[
{"display_name": "Always", "value": "always"},
{"display_name": "Answered", "value": "answered"},
{"display_name": "Attempted", "value": "attempted"},
{"display_name": "Closed", "value": "closed"},
{"display_name": "Finished", "value": "finished"},
{"display_name": "Past Due", "value": "past_due"},
{"display_name": "Never", "value": "never"}]
)
force_save_button = Boolean(help="Whether to force the save button to appear on the page", scope=Scope.settings, default=False) force_save_button = Boolean(help="Whether to force the save button to appear on the page", scope=Scope.settings, default=False)
rerandomize = Randomization(help="When to rerandomize the problem", default="always", scope=Scope.settings) rerandomize = Randomization(
display_name="Randomization", help="Defines how often inputs are randomized when a student loads the problem. This setting only applies to problems that can have randomly generated numeric values. A default value can be set in Advanced Settings.",
default="always", scope=Scope.settings, values=[{"display_name": "Always", "value": "always"},
{"display_name": "On Reset", "value": "onreset"},
{"display_name": "Never", "value": "never"},
{"display_name": "Per Student", "value": "per_student"}]
)
data = String(help="XML data for the problem", scope=Scope.content) data = String(help="XML data for the problem", scope=Scope.content)
correct_map = Object(help="Dictionary with the correctness of current student answers", scope=Scope.user_state, default={}) correct_map = Object(help="Dictionary with the correctness of current student answers", scope=Scope.user_state, default={})
input_state = Object(help="Dictionary for maintaining the state of inputtypes", scope=Scope.user_state) input_state = Object(help="Dictionary for maintaining the state of inputtypes", scope=Scope.user_state)
student_answers = Object(help="Dictionary with the current student responses", scope=Scope.user_state) student_answers = Object(help="Dictionary with the current student responses", scope=Scope.user_state)
done = Boolean(help="Whether the student has answered the problem", scope=Scope.user_state) done = Boolean(help="Whether the student has answered the problem", scope=Scope.user_state)
seed = StringyInteger(help="Random seed for this student", scope=Scope.user_state) seed = StringyInteger(help="Random seed for this student", scope=Scope.user_state)
weight = StringyFloat(help="How much to weight this problem by", scope=Scope.settings) weight = StringyFloat(
display_name="Problem Weight",
help="Defines the number of points each problem is worth. If the value is not set, each response field in the problem is worth one point.",
values={"min": 0, "step": .1},
scope=Scope.settings
)
markdown = String(help="Markdown source of this module", scope=Scope.settings) markdown = String(help="Markdown source of this module", scope=Scope.settings)
source_code = String(help="Source code for LaTeX and Word problems. This feature is not well-supported.", scope=Scope.settings) source_code = String(
help="Source code for LaTeX and Word problems. This feature is not well-supported.",
scope=Scope.settings
)
class CapaModule(CapaFields, XModule): class CapaModule(CapaFields, XModule):
......
...@@ -5,7 +5,7 @@ from pkg_resources import resource_string ...@@ -5,7 +5,7 @@ from pkg_resources import resource_string
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
from .x_module import XModule from .x_module import XModule
from xblock.core import Integer, Scope, String, Boolean, List from xblock.core import Integer, Scope, String, List
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor
from collections import namedtuple from collections import namedtuple
from .fields import Date, StringyFloat, StringyInteger, StringyBoolean from .fields import Date, StringyFloat, StringyInteger, StringyBoolean
...@@ -48,27 +48,49 @@ class VersionInteger(Integer): ...@@ -48,27 +48,49 @@ class VersionInteger(Integer):
class CombinedOpenEndedFields(object): class CombinedOpenEndedFields(object):
display_name = String(help="Display name for this module", default="Open Ended Grading", scope=Scope.settings) display_name = String(
display_name="Display Name",
help="This name appears in the horizontal navigation at the top of the page.",
default="Open Ended Grading", scope=Scope.settings
)
current_task_number = StringyInteger(help="Current task that the student is on.", default=0, scope=Scope.user_state) current_task_number = StringyInteger(help="Current task that the student is on.", default=0, scope=Scope.user_state)
task_states = List(help="List of state dictionaries of each task within this module.", scope=Scope.user_state) task_states = List(help="List of state dictionaries of each task within this module.", scope=Scope.user_state)
state = String(help="Which step within the current task that the student is on.", default="initial", state = String(help="Which step within the current task that the student is on.", default="initial",
scope=Scope.user_state) scope=Scope.user_state)
student_attempts = StringyInteger(help="Number of attempts taken by the student on this problem", default=0, student_attempts = StringyInteger(help="Number of attempts taken by the student on this problem", default=0,
scope=Scope.user_state) scope=Scope.user_state)
ready_to_reset = StringyBoolean(help="If the problem is ready to be reset or not.", default=False, ready_to_reset = StringyBoolean(
scope=Scope.user_state) help="If the problem is ready to be reset or not.", default=False,
attempts = StringyInteger(help="Maximum number of attempts that a student is allowed.", default=1, scope=Scope.settings) scope=Scope.user_state
is_graded = StringyBoolean(help="Whether or not the problem is graded.", default=False, scope=Scope.settings) )
accept_file_upload = StringyBoolean(help="Whether or not the problem accepts file uploads.", default=False, attempts = StringyInteger(
scope=Scope.settings) display_name="Maximum Attempts",
skip_spelling_checks = StringyBoolean(help="Whether or not to skip initial spelling checks.", default=True, help="The number of times the student can try to answer this problem.", default=1,
scope=Scope.settings) scope=Scope.settings, values = {"min" : 1 }
)
is_graded = StringyBoolean(display_name="Graded", help="Whether or not the problem is graded.", default=False, scope=Scope.settings)
accept_file_upload = StringyBoolean(
display_name="Allow File Uploads",
help="Whether or not the student can submit files as a response.", default=False, scope=Scope.settings
)
skip_spelling_checks = StringyBoolean(
display_name="Disable Quality Filter",
help="If False, the Quality Filter is enabled and submissions with poor spelling, short length, or poor grammar will not be peer reviewed.",
default=False, scope=Scope.settings
)
due = Date(help="Date that this problem is due by", default=None, scope=Scope.settings) due = Date(help="Date that this problem is due by", default=None, scope=Scope.settings)
graceperiod = String(help="Amount of time after the due date that submissions will be accepted", default=None, graceperiod = String(
scope=Scope.settings) help="Amount of time after the due date that submissions will be accepted",
default=None,
scope=Scope.settings
)
version = VersionInteger(help="Current version number", default=DEFAULT_VERSION, scope=Scope.settings) version = VersionInteger(help="Current version number", default=DEFAULT_VERSION, scope=Scope.settings)
data = String(help="XML data for the problem", scope=Scope.content) data = String(help="XML data for the problem", scope=Scope.content)
weight = StringyFloat(help="How much to weight this problem by", scope=Scope.settings) weight = StringyFloat(
display_name="Problem Weight",
help="Defines the number of points each problem is worth. If the value is not set, each problem is worth one point.",
scope=Scope.settings, values = {"min" : 0 , "step": ".1"}
)
markdown = String(help="Markdown source of this module", scope=Scope.settings) markdown = String(help="Markdown source of this module", scope=Scope.settings)
...@@ -244,6 +266,6 @@ class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor): ...@@ -244,6 +266,6 @@ class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor):
def non_editable_metadata_fields(self): def non_editable_metadata_fields(self):
non_editable_fields = super(CombinedOpenEndedDescriptor, self).non_editable_metadata_fields non_editable_fields = super(CombinedOpenEndedDescriptor, self).non_editable_metadata_fields
non_editable_fields.extend([CombinedOpenEndedDescriptor.due, CombinedOpenEndedDescriptor.graceperiod, non_editable_fields.extend([CombinedOpenEndedDescriptor.due, CombinedOpenEndedDescriptor.graceperiod,
CombinedOpenEndedDescriptor.markdown]) CombinedOpenEndedDescriptor.markdown, CombinedOpenEndedDescriptor.version])
return non_editable_fields return non_editable_fields
...@@ -10,8 +10,6 @@ ...@@ -10,8 +10,6 @@
position: relative; position: relative;
@include linear-gradient(top, #d4dee8, #c9d5e2); @include linear-gradient(top, #d4dee8, #c9d5e2);
padding: 5px; padding: 5px;
border: 1px solid #3c3c3c;
border-radius: 3px 3px 0 0;
border-bottom-color: #a5aaaf; border-bottom-color: #a5aaaf;
@include clearfix; @include clearfix;
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
.advanced-toggle { .advanced-toggle {
@include white-button; @include white-button;
height: auto; height: auto;
margin-top: -1px; margin-top: -4px;
padding: 3px 9px; padding: 3px 9px;
font-size: 12px; font-size: 12px;
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
width: 21px; width: 21px;
height: 21px; height: 21px;
padding: 0; padding: 0;
margin: 0 5px 0 15px; margin: -1px 5px 0 15px;
border-radius: 22px; border-radius: 22px;
border: 1px solid #a5aaaf; border: 1px solid #a5aaaf;
background: #e5ecf3; background: #e5ecf3;
...@@ -99,6 +99,13 @@ ...@@ -99,6 +99,13 @@
} }
} }
.problem-editor {
// adding padding to simple editor only - adjacent selector is needed since there are no toggles for CodeMirror
.markdown-box+.CodeMirror {
padding: 10px;
}
}
.problem-editor-icon { .problem-editor-icon {
display: inline-block; display: inline-block;
width: 26px; width: 26px;
......
...@@ -170,7 +170,7 @@ nav.sequence-nav { ...@@ -170,7 +170,7 @@ nav.sequence-nav {
font-family: $sans-serif; font-family: $sans-serif;
line-height: lh(); line-height: lh();
left: 0px; left: 0px;
opacity: 0; opacity: 0.0;
padding: 6px; padding: 6px;
position: absolute; position: absolute;
top: 48px; top: 48px;
...@@ -204,7 +204,7 @@ nav.sequence-nav { ...@@ -204,7 +204,7 @@ nav.sequence-nav {
p { p {
display: block; display: block;
margin-top: 4px; margin-top: 4px;
opacity: 1; opacity: 1.0;
} }
} }
} }
...@@ -248,12 +248,12 @@ nav.sequence-nav { ...@@ -248,12 +248,12 @@ nav.sequence-nav {
} }
&:hover { &:hover {
opacity: .5; opacity: 0.5;
} }
&.disabled { &.disabled {
cursor: normal; cursor: normal;
opacity: .4; opacity: 0.4;
} }
} }
} }
...@@ -320,12 +320,12 @@ nav.sequence-bottom { ...@@ -320,12 +320,12 @@ nav.sequence-bottom {
outline: 0; outline: 0;
&:hover { &:hover {
opacity: .5; opacity: 0.5;
background-position: center 15px; background-position: center 15px;
} }
&.disabled { &.disabled {
opacity: .4; opacity: 0.4;
} }
&:focus { &:focus {
......
...@@ -41,7 +41,7 @@ div.video { ...@@ -41,7 +41,7 @@ div.video {
&:hover { &:hover {
ul, div { ul, div {
opacity: 1; opacity: 1.0;
} }
} }
...@@ -158,7 +158,7 @@ div.video { ...@@ -158,7 +158,7 @@ div.video {
ol.video_speeds { ol.video_speeds {
display: block; display: block;
opacity: 1; opacity: 1.0;
padding: 0; padding: 0;
margin: 0; margin: 0;
list-style: none; list-style: none;
...@@ -208,7 +208,7 @@ div.video { ...@@ -208,7 +208,7 @@ div.video {
} }
&:hover, &:active, &:focus { &:hover, &:active, &:focus {
opacity: 1; opacity: 1.0;
background-color: #444; background-color: #444;
} }
} }
...@@ -221,7 +221,7 @@ div.video { ...@@ -221,7 +221,7 @@ div.video {
border: 1px solid #000; border: 1px solid #000;
bottom: 46px; bottom: 46px;
display: none; display: none;
opacity: 0; opacity: 0.0;
position: absolute; position: absolute;
width: 133px; width: 133px;
z-index: 10; z-index: 10;
...@@ -264,7 +264,7 @@ div.video { ...@@ -264,7 +264,7 @@ div.video {
&.open { &.open {
.volume-slider-container { .volume-slider-container {
display: block; display: block;
opacity: 1; opacity: 1.0;
} }
} }
...@@ -302,7 +302,7 @@ div.video { ...@@ -302,7 +302,7 @@ div.video {
border: 1px solid #000; border: 1px solid #000;
bottom: 46px; bottom: 46px;
display: none; display: none;
opacity: 0; opacity: 0.0;
position: absolute; position: absolute;
width: 45px; width: 45px;
height: 125px; height: 125px;
...@@ -395,7 +395,7 @@ div.video { ...@@ -395,7 +395,7 @@ div.video {
font-weight: 800; font-weight: 800;
line-height: 46px; //height of play pause buttons line-height: 46px; //height of play pause buttons
margin-left: 0; margin-left: 0;
opacity: 1; opacity: 1.0;
padding: 0 lh(.5); padding: 0 lh(.5);
position: relative; position: relative;
text-indent: -9999px; text-indent: -9999px;
...@@ -410,7 +410,7 @@ div.video { ...@@ -410,7 +410,7 @@ div.video {
} }
&.off { &.off {
opacity: .7; opacity: 0.7;
} }
} }
} }
...@@ -418,7 +418,7 @@ div.video { ...@@ -418,7 +418,7 @@ div.video {
&:hover section.video-controls { &:hover section.video-controls {
ul, div { ul, div {
opacity: 1; opacity: 1.0;
} }
div.slider { div.slider {
......
...@@ -41,7 +41,7 @@ div.video { ...@@ -41,7 +41,7 @@ div.video {
&:hover { &:hover {
ul, div { ul, div {
opacity: 1; opacity: 1.0;
} }
} }
...@@ -158,7 +158,7 @@ div.video { ...@@ -158,7 +158,7 @@ div.video {
ol.video_speeds { ol.video_speeds {
display: block; display: block;
opacity: 1; opacity: 1.0;
padding: 0; padding: 0;
margin: 0; margin: 0;
list-style: none; list-style: none;
...@@ -208,7 +208,7 @@ div.video { ...@@ -208,7 +208,7 @@ div.video {
} }
&:hover, &:active, &:focus { &:hover, &:active, &:focus {
opacity: 1; opacity: 1.0;
background-color: #444; background-color: #444;
} }
} }
...@@ -221,7 +221,7 @@ div.video { ...@@ -221,7 +221,7 @@ div.video {
border: 1px solid #000; border: 1px solid #000;
bottom: 46px; bottom: 46px;
display: none; display: none;
opacity: 0; opacity: 0.0;
position: absolute; position: absolute;
width: 133px; width: 133px;
z-index: 10; z-index: 10;
...@@ -264,7 +264,7 @@ div.video { ...@@ -264,7 +264,7 @@ div.video {
&.open { &.open {
.volume-slider-container { .volume-slider-container {
display: block; display: block;
opacity: 1; opacity: 1.0;
} }
} }
...@@ -302,7 +302,7 @@ div.video { ...@@ -302,7 +302,7 @@ div.video {
border: 1px solid #000; border: 1px solid #000;
bottom: 46px; bottom: 46px;
display: none; display: none;
opacity: 0; opacity: 0.0;
position: absolute; position: absolute;
width: 45px; width: 45px;
height: 125px; height: 125px;
...@@ -395,7 +395,7 @@ div.video { ...@@ -395,7 +395,7 @@ div.video {
font-weight: 800; font-weight: 800;
line-height: 46px; //height of play pause buttons line-height: 46px; //height of play pause buttons
margin-left: 0; margin-left: 0;
opacity: 1; opacity: 1.0;
padding: 0 lh(.5); padding: 0 lh(.5);
position: relative; position: relative;
text-indent: -9999px; text-indent: -9999px;
...@@ -410,7 +410,7 @@ div.video { ...@@ -410,7 +410,7 @@ div.video {
} }
&.off { &.off {
opacity: .7; opacity: 0.7;
} }
} }
} }
...@@ -418,7 +418,7 @@ div.video { ...@@ -418,7 +418,7 @@ div.video {
&:hover section.video-controls { &:hover section.video-controls {
ul, div { ul, div {
opacity: 1; opacity: 1.0;
} }
div.slider { div.slider {
......
...@@ -8,8 +8,16 @@ from xblock.core import String, Scope ...@@ -8,8 +8,16 @@ from xblock.core import String, Scope
class DiscussionFields(object): class DiscussionFields(object):
discussion_id = String(scope=Scope.settings) discussion_id = String(scope=Scope.settings)
discussion_category = String(scope=Scope.settings) discussion_category = String(
discussion_target = String(scope=Scope.settings) display_name="Category",
help="A category name for the discussion. This name appears in the left pane of the discussion forum for the course.",
scope=Scope.settings
)
discussion_target = String(
display_name="Subcategory",
help="A subcategory name for the discussion. This name appears in the left pane of the discussion forum for the course.",
scope=Scope.settings
)
sort_key = String(scope=Scope.settings) sort_key = String(scope=Scope.settings)
......
"""
Modules that get shown to the users when an error has occured while
loading or rendering other modules
"""
import hashlib import hashlib
import logging import logging
import json import json
...@@ -22,12 +27,19 @@ log = logging.getLogger(__name__) ...@@ -22,12 +27,19 @@ log = logging.getLogger(__name__)
class ErrorFields(object): class ErrorFields(object):
"""
XBlock fields used by the ErrorModules
"""
contents = String(scope=Scope.content) contents = String(scope=Scope.content)
error_msg = String(scope=Scope.content) error_msg = String(scope=Scope.content)
display_name = String(scope=Scope.settings) display_name = String(scope=Scope.settings)
class ErrorModule(ErrorFields, XModule): class ErrorModule(ErrorFields, XModule):
"""
Module that gets shown to staff when there has been an error while
loading or rendering other modules
"""
def get_html(self): def get_html(self):
'''Show an error to staff. '''Show an error to staff.
...@@ -42,6 +54,10 @@ class ErrorModule(ErrorFields, XModule): ...@@ -42,6 +54,10 @@ class ErrorModule(ErrorFields, XModule):
class NonStaffErrorModule(ErrorFields, XModule): class NonStaffErrorModule(ErrorFields, XModule):
"""
Module that gets shown to students when there has been an error while
loading or rendering other modules
"""
def get_html(self): def get_html(self):
'''Show an error to a student. '''Show an error to a student.
TODO (vshnayder): proper style, divs, etc. TODO (vshnayder): proper style, divs, etc.
...@@ -61,7 +77,7 @@ class ErrorDescriptor(ErrorFields, JSONEditingDescriptor): ...@@ -61,7 +77,7 @@ class ErrorDescriptor(ErrorFields, JSONEditingDescriptor):
module_class = ErrorModule module_class = ErrorModule
@classmethod @classmethod
def _construct(self, system, contents, error_msg, location): def _construct(cls, system, contents, error_msg, location):
if location.name is None: if location.name is None:
location = location._replace( location = location._replace(
...@@ -80,7 +96,7 @@ class ErrorDescriptor(ErrorFields, JSONEditingDescriptor): ...@@ -80,7 +96,7 @@ class ErrorDescriptor(ErrorFields, JSONEditingDescriptor):
'contents': contents, 'contents': contents,
'display_name': 'Error: ' + location.name 'display_name': 'Error: ' + location.name
} }
return ErrorDescriptor( return cls(
system, system,
location, location,
model_data, model_data,
......
...@@ -289,6 +289,9 @@ class @CombinedOpenEnded ...@@ -289,6 +289,9 @@ class @CombinedOpenEnded
if @child_type == "openended" if @child_type == "openended"
@submit_button.hide() @submit_button.hide()
@queueing() @queueing()
if @task_number==1 and @task_count==1
@grader_status = $('.grader-status')
@grader_status.html("<p>Response submitted for scoring.</p>")
else if @child_state == 'post_assessment' else if @child_state == 'post_assessment'
if @child_type=="openended" if @child_type=="openended"
@skip_button.show() @skip_button.show()
...@@ -311,6 +314,8 @@ class @CombinedOpenEnded ...@@ -311,6 +314,8 @@ class @CombinedOpenEnded
if @task_number<@task_count if @task_number<@task_count
@next_problem() @next_problem()
else else
if @task_number==1 and @task_count==1
@show_combined_rubric_current()
@show_results_current() @show_results_current()
@reset_button.show() @reset_button.show()
......
...@@ -268,7 +268,7 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -268,7 +268,7 @@ class MongoModuleStore(ModuleStoreBase):
query = {'_id.org': location.org, query = {'_id.org': location.org,
'_id.course': location.course, '_id.course': location.course,
'_id.category': {'$in': ['course', 'chapter', 'sequential', 'vertical', '_id.category': {'$in': ['course', 'chapter', 'sequential', 'vertical',
'wrapper', 'problemset', 'conditional']} 'wrapper', 'problemset', 'conditional', 'randomize']}
} }
# we just want the Location, children, and inheritable metadata # we just want the Location, children, and inheritable metadata
record_filter = {'_id': 1, 'definition.children': 1} record_filter = {'_id': 1, 'definition.children': 1}
......
...@@ -85,7 +85,7 @@ class MockControllerQueryService(object): ...@@ -85,7 +85,7 @@ class MockControllerQueryService(object):
def __init__(self, config, system): def __init__(self, config, system):
pass pass
def check_if_name_is_unique(self, **params): def check_if_name_is_unique(self, *args, **kwargs):
""" """
Mock later if needed. Stub function for now. Mock later if needed. Stub function for now.
@param params: @param params:
...@@ -93,7 +93,7 @@ class MockControllerQueryService(object): ...@@ -93,7 +93,7 @@ class MockControllerQueryService(object):
""" """
pass pass
def check_for_eta(self, **params): def check_for_eta(self, *args, **kwargs):
""" """
Mock later if needed. Stub function for now. Mock later if needed. Stub function for now.
@param params: @param params:
...@@ -101,19 +101,19 @@ class MockControllerQueryService(object): ...@@ -101,19 +101,19 @@ class MockControllerQueryService(object):
""" """
pass pass
def check_combined_notifications(self, **params): def check_combined_notifications(self, *args, **kwargs):
combined_notifications = '{"flagged_submissions_exist": false, "version": 1, "new_student_grading_to_view": false, "success": true, "staff_needs_to_grade": false, "student_needs_to_peer_grade": true, "overall_need_to_check": true}' combined_notifications = '{"flagged_submissions_exist": false, "version": 1, "new_student_grading_to_view": false, "success": true, "staff_needs_to_grade": false, "student_needs_to_peer_grade": true, "overall_need_to_check": true}'
return combined_notifications return combined_notifications
def get_grading_status_list(self, **params): def get_grading_status_list(self, *args, **kwargs):
grading_status_list = '{"version": 1, "problem_list": [{"problem_name": "Science Question -- Machine Assessed", "grader_type": "NA", "eta_available": true, "state": "Waiting to be Graded", "eta": 259200, "location": "i4x://MITx/oe101x/combinedopenended/Science_SA_ML"}, {"problem_name": "Humanities Question -- Peer Assessed", "grader_type": "NA", "eta_available": true, "state": "Waiting to be Graded", "eta": 259200, "location": "i4x://MITx/oe101x/combinedopenended/Humanities_SA_Peer"}], "success": true}' grading_status_list = '{"version": 1, "problem_list": [{"problem_name": "Science Question -- Machine Assessed", "grader_type": "NA", "eta_available": true, "state": "Waiting to be Graded", "eta": 259200, "location": "i4x://MITx/oe101x/combinedopenended/Science_SA_ML"}, {"problem_name": "Humanities Question -- Peer Assessed", "grader_type": "NA", "eta_available": true, "state": "Waiting to be Graded", "eta": 259200, "location": "i4x://MITx/oe101x/combinedopenended/Humanities_SA_Peer"}], "success": true}'
return grading_status_list return grading_status_list
def get_flagged_problem_list(self, **params): def get_flagged_problem_list(self, *args, **kwargs):
flagged_problem_list = '{"version": 1, "success": false, "error": "No flagged submissions exist for course: MITx/oe101x/2012_Fall"}' flagged_problem_list = '{"version": 1, "success": false, "error": "No flagged submissions exist for course: MITx/oe101x/2012_Fall"}'
return flagged_problem_list return flagged_problem_list
def take_action_on_flags(self, **params): def take_action_on_flags(self, *args, **kwargs):
""" """
Mock later if needed. Stub function for now. Mock later if needed. Stub function for now.
@param params: @param params:
......
...@@ -10,7 +10,7 @@ from .x_module import XModule ...@@ -10,7 +10,7 @@ from .x_module import XModule
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from .timeinfo import TimeInfo from .timeinfo import TimeInfo
from xblock.core import Object, Integer, Boolean, String, Scope from xblock.core import Object, String, Scope
from xmodule.fields import Date, StringyFloat, StringyInteger, StringyBoolean from xmodule.fields import Date, StringyFloat, StringyInteger, StringyBoolean
from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService
...@@ -22,24 +22,43 @@ USE_FOR_SINGLE_LOCATION = False ...@@ -22,24 +22,43 @@ USE_FOR_SINGLE_LOCATION = False
LINK_TO_LOCATION = "" LINK_TO_LOCATION = ""
TRUE_DICT = [True, "True", "true", "TRUE"] TRUE_DICT = [True, "True", "true", "TRUE"]
MAX_SCORE = 1 MAX_SCORE = 1
IS_GRADED = True IS_GRADED = False
EXTERNAL_GRADER_NO_CONTACT_ERROR = "Failed to contact external graders. Please notify course staff." EXTERNAL_GRADER_NO_CONTACT_ERROR = "Failed to contact external graders. Please notify course staff."
class PeerGradingFields(object): class PeerGradingFields(object):
use_for_single_location = StringyBoolean(help="Whether to use this for a single location or as a panel.", use_for_single_location = StringyBoolean(
default=USE_FOR_SINGLE_LOCATION, scope=Scope.settings) display_name="Show Single Problem",
link_to_location = String(help="The location this problem is linked to.", default=LINK_TO_LOCATION, help='When True, only the single problem specified by "Link to Problem Location" is shown. '
scope=Scope.settings) 'When False, a panel is displayed with all problems available for peer grading.',
is_graded = StringyBoolean(help="Whether or not this module is scored.", default=IS_GRADED, scope=Scope.settings) default=USE_FOR_SINGLE_LOCATION, scope=Scope.settings
)
link_to_location = String(
display_name="Link to Problem Location",
help='The location of the problem being graded. Only used when "Show Single Problem" is True.',
default=LINK_TO_LOCATION, scope=Scope.settings
)
is_graded = StringyBoolean(
display_name="Graded",
help='Defines whether the student gets credit for grading this problem. Only used when "Show Single Problem" is True.',
default=IS_GRADED, scope=Scope.settings
)
due_date = Date(help="Due date that should be displayed.", default=None, scope=Scope.settings) due_date = Date(help="Due date that should be displayed.", default=None, scope=Scope.settings)
grace_period_string = String(help="Amount of grace to give on the due date.", default=None, scope=Scope.settings) grace_period_string = String(help="Amount of grace to give on the due date.", default=None, scope=Scope.settings)
max_grade = StringyInteger(help="The maximum grade that a student can receieve for this problem.", default=MAX_SCORE, max_grade = StringyInteger(
scope=Scope.settings) help="The maximum grade that a student can receive for this problem.", default=MAX_SCORE,
student_data_for_location = Object(help="Student data for a given peer grading problem.", scope=Scope.settings, values={"min": 0}
scope=Scope.user_state) )
weight = StringyFloat(help="How much to weight this problem by", scope=Scope.settings) student_data_for_location = Object(
help="Student data for a given peer grading problem.",
scope=Scope.user_state
)
weight = StringyFloat(
display_name="Problem Weight",
help="Defines the number of points each problem is worth. If the value is not set, each problem is worth one point.",
scope=Scope.settings, values={"min": 0, "step": ".1"}
)
class PeerGradingModule(PeerGradingFields, XModule): class PeerGradingModule(PeerGradingFields, XModule):
...@@ -590,3 +609,11 @@ class PeerGradingDescriptor(PeerGradingFields, RawDescriptor): ...@@ -590,3 +609,11 @@ class PeerGradingDescriptor(PeerGradingFields, RawDescriptor):
#Specify whether or not to pass in open ended interface #Specify whether or not to pass in open ended interface
needs_open_ended_interface = True needs_open_ended_interface = True
@property
def non_editable_metadata_fields(self):
non_editable_fields = super(PeerGradingDescriptor, self).non_editable_metadata_fields
non_editable_fields.extend([PeerGradingFields.due_date, PeerGradingFields.grace_period_string,
PeerGradingFields.max_grade])
return non_editable_fields
--- ---
metadata: metadata:
display_name: Open Ended Response display_name: Open Ended Response
attempts: 1
is_graded: False
version: 1
skip_spelling_checks: False
accept_file_upload: False
weight: ""
markdown: "" markdown: ""
data: | data: |
<combinedopenended> <combinedopenended>
......
--- ---
metadata: metadata:
display_name: Blank HTML Page display_name: Blank HTML Page
empty: True
data: | data: |
......
--- ---
metadata: metadata:
display_name: Peer Grading Interface display_name: Peer Grading Interface
use_for_single_location: False
link_to_location: None
is_graded: False
max_grade: 1 max_grade: 1
weight: ""
data: | data: |
<peergrading> <peergrading>
</peergrading> </peergrading>
......
...@@ -3,9 +3,7 @@ ...@@ -3,9 +3,7 @@
metadata: metadata:
display_name: Circuit Schematic Builder display_name: Circuit Schematic Builder
rerandomize: never rerandomize: never
showanswer: always showanswer: finished
weight: ""
attempts: ""
data: | data: |
<problem > <problem >
Please make a voltage divider that splits the provided voltage evenly. Please make a voltage divider that splits the provided voltage evenly.
......
...@@ -2,9 +2,7 @@ ...@@ -2,9 +2,7 @@
metadata: metadata:
display_name: Custom Python-Evaluated Input display_name: Custom Python-Evaluated Input
rerandomize: never rerandomize: never
showanswer: always showanswer: finished
weight: ""
attempts: ""
data: | data: |
<problem> <problem>
<p> <p>
......
...@@ -2,11 +2,8 @@ ...@@ -2,11 +2,8 @@
metadata: metadata:
display_name: Blank Common Problem display_name: Blank Common Problem
rerandomize: never rerandomize: never
showanswer: always showanswer: finished
markdown: "" markdown: ""
weight: ""
empty: True
attempts: ""
data: | data: |
<problem> <problem>
</problem> </problem>
......
...@@ -2,10 +2,7 @@ ...@@ -2,10 +2,7 @@
metadata: metadata:
display_name: Blank Advanced Problem display_name: Blank Advanced Problem
rerandomize: never rerandomize: never
showanswer: always showanswer: finished
weight: ""
attempts: ""
empty: True
data: | data: |
<problem> <problem>
</problem> </problem>
......
...@@ -2,9 +2,7 @@ ...@@ -2,9 +2,7 @@
metadata: metadata:
display_name: Math Expression Input display_name: Math Expression Input
rerandomize: never rerandomize: never
showanswer: always showanswer: finished
weight: ""
attempts: ""
data: | data: |
<problem> <problem>
<p> <p>
......
...@@ -2,9 +2,7 @@ ...@@ -2,9 +2,7 @@
metadata: metadata:
display_name: Image Mapped Input display_name: Image Mapped Input
rerandomize: never rerandomize: never
showanswer: always showanswer: finished
weight: ""
attempts: ""
data: | data: |
<problem> <problem>
<p> <p>
......
...@@ -2,9 +2,7 @@ ...@@ -2,9 +2,7 @@
metadata: metadata:
display_name: Multiple Choice display_name: Multiple Choice
rerandomize: never rerandomize: never
showanswer: always showanswer: finished
weight: ""
attempts: ""
markdown: markdown:
"A multiple choice problem presents radio buttons for student input. Students can only select a single "A multiple choice problem presents radio buttons for student input. Students can only select a single
option presented. Multiple Choice questions have been the subject of many areas of research due to the early option presented. Multiple Choice questions have been the subject of many areas of research due to the early
......
...@@ -2,9 +2,7 @@ ...@@ -2,9 +2,7 @@
metadata: metadata:
display_name: Numerical Input display_name: Numerical Input
rerandomize: never rerandomize: never
showanswer: always showanswer: finished
weight: ""
attempts: ""
markdown: markdown:
"A numerical input problem accepts a line of text input from the "A numerical input problem accepts a line of text input from the
student, and evaluates the input for correctness based on its student, and evaluates the input for correctness based on its
......
...@@ -2,9 +2,7 @@ ...@@ -2,9 +2,7 @@
metadata: metadata:
display_name: Dropdown display_name: Dropdown
rerandomize: never rerandomize: never
showanswer: always showanswer: finished
weight: ""
attempts: ""
markdown: markdown:
"Dropdown problems give a limited set of options for students to respond with, and present those options "Dropdown problems give a limited set of options for students to respond with, and present those options
in a format that encourages them to search for a specific answer rather than being immediately presented in a format that encourages them to search for a specific answer rather than being immediately presented
......
...@@ -2,9 +2,7 @@ ...@@ -2,9 +2,7 @@
metadata: metadata:
display_name: Text Input display_name: Text Input
rerandomize: never rerandomize: never
showanswer: always showanswer: finished
weight: ""
attempts: ""
# Note, the extra newlines are needed to make the yaml parser add blank lines instead of folding # Note, the extra newlines are needed to make the yaml parser add blank lines instead of folding
markdown: markdown:
"A text input problem accepts a line of text from the "A text input problem accepts a line of text from the
......
--- ---
metadata: metadata:
display_name: Word cloud display_name: Word cloud
version: 1
num_inputs: 5
num_top_words: 250
display_student_percents: True
data: {} data: {}
children: [] children: []
"""
Tests for ErrorModule and NonStaffErrorModule
"""
import unittest
from xmodule.tests import test_system
import xmodule.error_module as error_module
class TestErrorModule(unittest.TestCase):
"""
Tests for ErrorModule and ErrorDescriptor
"""
def setUp(self):
self.system = test_system()
self.org = "org"
self.course = "course"
self.fake_xml = "<problem />"
self.broken_xml = "<problem>"
self.error_msg = "Error"
def test_error_module_create(self):
descriptor = error_module.ErrorDescriptor.from_xml(
self.fake_xml, self.system, self.org, self.course)
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)
class TestNonStaffErrorModule(TestErrorModule):
"""
Tests for NonStaffErrorModule and NonStaffErrorDescriptor
"""
def test_non_staff_error_module_create(self):
descriptor = error_module.NonStaffErrorDescriptor.from_xml(
self.fake_xml, self.system, self.org, self.course)
self.assertTrue(isinstance(descriptor, error_module.NonStaffErrorDescriptor))
def test_non_staff_error_module_rendering(self):
descriptor = error_module.NonStaffErrorDescriptor.from_xml(
self.fake_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)
...@@ -460,8 +460,8 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -460,8 +460,8 @@ class ImportTestCase(BaseCourseTestCase):
) )
module = modulestore.get_instance(course.id, location) module = modulestore.get_instance(course.id, location)
self.assertEqual(len(module.get_children()), 0) self.assertEqual(len(module.get_children()), 0)
self.assertEqual(module.num_inputs, '5') self.assertEqual(module.num_inputs, 5)
self.assertEqual(module.num_top_words, '250') self.assertEqual(module.num_top_words, 250)
def test_cohort_config(self): def test_cohort_config(self):
""" """
......
# disable missing docstring
#pylint: disable=C0111
from xmodule.x_module import XModuleFields from xmodule.x_module import XModuleFields
from xblock.core import Scope, String, Object from xblock.core import Scope, String, Object, Boolean
from xmodule.fields import Date, StringyInteger from xmodule.fields import Date, StringyInteger, StringyFloat
from xmodule.xml_module import XmlDescriptor from xmodule.xml_module import XmlDescriptor
import unittest import unittest
from . import test_system from .import test_system
from mock import Mock from mock import Mock
class CrazyJsonString(String):
def to_json(self, value):
return value + " JSON"
class TestFields(object): class TestFields(object):
# Will be returned by editable_metadata_fields. # Will be returned by editable_metadata_fields.
max_attempts = StringyInteger(scope=Scope.settings, default=1000) max_attempts = StringyInteger(scope=Scope.settings, default=1000, values={'min': 1, 'max': 10})
# Will not be returned by editable_metadata_fields because filtered out by non_editable_metadata_fields. # Will not be returned by editable_metadata_fields because filtered out by non_editable_metadata_fields.
due = Date(scope=Scope.settings) due = Date(scope=Scope.settings)
# Will not be returned by editable_metadata_fields because is not Scope.settings. # Will not be returned by editable_metadata_fields because is not Scope.settings.
student_answers = Object(scope=Scope.user_state) student_answers = Object(scope=Scope.user_state)
# Will be returned, and can override the inherited value from XModule. # Will be returned, and can override the inherited value from XModule.
display_name = String(scope=Scope.settings, default='local default') display_name = String(scope=Scope.settings, default='local default', display_name='Local Display Name',
help='local help')
# Used for testing select type, effect of to_json method
string_select = CrazyJsonString(
scope=Scope.settings,
default='default value',
values=[{'display_name': 'first', 'value': 'value a'},
{'display_name': 'second', 'value': 'value b'}]
)
# Used for testing select type
float_select = StringyFloat(scope=Scope.settings, default=.999, values=[1.23, 0.98])
# Used for testing float type
float_non_select = StringyFloat(scope=Scope.settings, default=.999, values={'min': 0, 'step': .3})
# Used for testing that Booleans get mapped to select type
boolean_select = Boolean(scope=Scope.settings)
class EditableMetadataFieldsTest(unittest.TestCase): class EditableMetadataFieldsTest(unittest.TestCase):
def test_display_name_field(self): def test_display_name_field(self):
editable_fields = self.get_xml_editable_fields({}) editable_fields = self.get_xml_editable_fields({})
# Tests that the xblock fields (currently tags and name) get filtered out. # Tests that the xblock fields (currently tags and name) get filtered out.
# Also tests that xml_attributes is filtered out of XmlDescriptor. # Also tests that xml_attributes is filtered out of XmlDescriptor.
self.assertEqual(1, len(editable_fields), "Expected only 1 editable field for xml descriptor.") self.assertEqual(1, len(editable_fields), "Expected only 1 editable field for xml descriptor.")
self.assert_field_values(editable_fields, 'display_name', XModuleFields.display_name, self.assert_field_values(
explicitly_set=False, inheritable=False, value=None, default_value=None) editable_fields, 'display_name', XModuleFields.display_name,
explicitly_set=False, inheritable=False, value=None, default_value=None
)
def test_override_default(self): def test_override_default(self):
# Tests that explicitly_set is correct when a value overrides the default (not inheritable). # Tests that explicitly_set is correct when a value overrides the default (not inheritable).
editable_fields = self.get_xml_editable_fields({'display_name': 'foo'}) editable_fields = self.get_xml_editable_fields({'display_name': 'foo'})
self.assert_field_values(editable_fields, 'display_name', XModuleFields.display_name, self.assert_field_values(
explicitly_set=True, inheritable=False, value='foo', default_value=None) editable_fields, 'display_name', XModuleFields.display_name,
explicitly_set=True, inheritable=False, value='foo', default_value=None
)
def test_additional_field(self): def test_integer_field(self):
descriptor = self.get_descriptor({'max_attempts' : '7'}) descriptor = self.get_descriptor({'max_attempts': '7'})
editable_fields = descriptor.editable_metadata_fields editable_fields = descriptor.editable_metadata_fields
self.assertEqual(2, len(editable_fields)) self.assertEqual(6, len(editable_fields))
self.assert_field_values(editable_fields, 'max_attempts', TestFields.max_attempts, self.assert_field_values(
explicitly_set=True, inheritable=False, value=7, default_value=1000) editable_fields, 'max_attempts', TestFields.max_attempts,
self.assert_field_values(editable_fields, 'display_name', XModuleFields.display_name, explicitly_set=True, inheritable=False, value=7, default_value=1000, type='Integer',
explicitly_set=False, inheritable=False, value='local default', default_value='local default') options=TestFields.max_attempts.values
)
self.assert_field_values(
editable_fields, 'display_name', TestFields.display_name,
explicitly_set=False, inheritable=False, value='local default', default_value='local default'
)
editable_fields = self.get_descriptor({}).editable_metadata_fields editable_fields = self.get_descriptor({}).editable_metadata_fields
self.assert_field_values(editable_fields, 'max_attempts', TestFields.max_attempts, self.assert_field_values(
explicitly_set=False, inheritable=False, value=1000, default_value=1000) editable_fields, 'max_attempts', TestFields.max_attempts,
explicitly_set=False, inheritable=False, value=1000, default_value=1000, type='Integer',
options=TestFields.max_attempts.values
)
def test_inherited_field(self): def test_inherited_field(self):
model_val = {'display_name' : 'inherited'} model_val = {'display_name': 'inherited'}
descriptor = self.get_descriptor(model_val) descriptor = self.get_descriptor(model_val)
# Mimic an inherited value for display_name (inherited and inheritable are the same in this case). # Mimic an inherited value for display_name (inherited and inheritable are the same in this case).
descriptor._inherited_metadata = model_val descriptor._inherited_metadata = model_val
descriptor._inheritable_metadata = model_val descriptor._inheritable_metadata = model_val
editable_fields = descriptor.editable_metadata_fields editable_fields = descriptor.editable_metadata_fields
self.assert_field_values(editable_fields, 'display_name', XModuleFields.display_name, self.assert_field_values(
explicitly_set=False, inheritable=True, value='inherited', default_value='inherited') editable_fields, 'display_name', TestFields.display_name,
explicitly_set=False, inheritable=True, value='inherited', default_value='inherited'
)
descriptor = self.get_descriptor({'display_name' : 'explicit'}) descriptor = self.get_descriptor({'display_name': 'explicit'})
# Mimic the case where display_name WOULD have been inherited, except we explicitly set it. # Mimic the case where display_name WOULD have been inherited, except we explicitly set it.
descriptor._inheritable_metadata = {'display_name' : 'inheritable value'} descriptor._inheritable_metadata = {'display_name': 'inheritable value'}
descriptor._inherited_metadata = {} descriptor._inherited_metadata = {}
editable_fields = descriptor.editable_metadata_fields editable_fields = descriptor.editable_metadata_fields
self.assert_field_values(editable_fields, 'display_name', XModuleFields.display_name, self.assert_field_values(
explicitly_set=True, inheritable=True, value='explicit', default_value='inheritable value') editable_fields, 'display_name', TestFields.display_name,
explicitly_set=True, inheritable=True, value='explicit', default_value='inheritable value'
)
def test_type_and_options(self):
# test_display_name_field verifies that a String field is of type "Generic".
# test_integer_field verifies that a StringyInteger field is of type "Integer".
descriptor = self.get_descriptor({})
editable_fields = descriptor.editable_metadata_fields
# Tests for select
self.assert_field_values(
editable_fields, 'string_select', TestFields.string_select,
explicitly_set=False, inheritable=False, value='default value', default_value='default value',
type='Select', options=[{'display_name': 'first', 'value': 'value a JSON'},
{'display_name': 'second', 'value': 'value b JSON'}]
)
self.assert_field_values(
editable_fields, 'float_select', TestFields.float_select,
explicitly_set=False, inheritable=False, value=.999, default_value=.999,
type='Select', options=[1.23, 0.98]
)
self.assert_field_values(
editable_fields, 'boolean_select', TestFields.boolean_select,
explicitly_set=False, inheritable=False, value=None, default_value=None,
type='Select', options=[{'display_name': "True", "value": True}, {'display_name': "False", "value": False}]
)
# Test for float
self.assert_field_values(
editable_fields, 'float_non_select', TestFields.float_non_select,
explicitly_set=False, inheritable=False, value=.999, default_value=.999,
type='Float', options={'min': 0, 'step': .3}
)
# Start of helper methods # Start of helper methods
def get_xml_editable_fields(self, model_data): def get_xml_editable_fields(self, model_data):
...@@ -73,7 +145,6 @@ class EditableMetadataFieldsTest(unittest.TestCase): ...@@ -73,7 +145,6 @@ class EditableMetadataFieldsTest(unittest.TestCase):
def get_descriptor(self, model_data): def get_descriptor(self, model_data):
class TestModuleDescriptor(TestFields, XmlDescriptor): class TestModuleDescriptor(TestFields, XmlDescriptor):
@property @property
def non_editable_metadata_fields(self): def non_editable_metadata_fields(self):
non_editable_fields = super(TestModuleDescriptor, self).non_editable_metadata_fields non_editable_fields = super(TestModuleDescriptor, self).non_editable_metadata_fields
...@@ -84,10 +155,19 @@ class EditableMetadataFieldsTest(unittest.TestCase): ...@@ -84,10 +155,19 @@ class EditableMetadataFieldsTest(unittest.TestCase):
system.render_template = Mock(return_value="<div>Test Template HTML</div>") system.render_template = Mock(return_value="<div>Test Template HTML</div>")
return TestModuleDescriptor(system=system, location=None, model_data=model_data) return TestModuleDescriptor(system=system, location=None, model_data=model_data)
def assert_field_values(self, editable_fields, name, field, explicitly_set, inheritable, value, default_value): def assert_field_values(self, editable_fields, name, field, explicitly_set, inheritable, value, default_value,
type='Generic', options=[]):
test_field = editable_fields[name] test_field = editable_fields[name]
self.assertEqual(field, test_field['field'])
self.assertEqual(field.name, test_field['field_name'])
self.assertEqual(field.display_name, test_field['display_name'])
self.assertEqual(field.help, test_field['help'])
self.assertEqual(field.to_json(value), test_field['value'])
self.assertEqual(field.to_json(default_value), test_field['default_value'])
self.assertEqual(options, test_field['options'])
self.assertEqual(type, test_field['type'])
self.assertEqual(explicitly_set, test_field['explicitly_set']) self.assertEqual(explicitly_set, test_field['explicitly_set'])
self.assertEqual(inheritable, test_field['inheritable']) self.assertEqual(inheritable, test_field['inheritable'])
self.assertEqual(value, test_field['value'])
self.assertEqual(default_value, test_field['default_value'])
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
generate and view word cloud. generate and view word cloud.
On the client side we show: On the client side we show:
If student does not yet anwered - `num_inputs` numbers of text inputs. If student does not yet answered - `num_inputs` numbers of text inputs.
If student have answered - words he entered and cloud. If student have answered - words he entered and cloud.
""" """
...@@ -14,7 +14,8 @@ from xmodule.raw_module import RawDescriptor ...@@ -14,7 +14,8 @@ from xmodule.raw_module import RawDescriptor
from xmodule.editing_module import MetadataOnlyEditingDescriptor from xmodule.editing_module import MetadataOnlyEditingDescriptor
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xblock.core import Scope, String, Object, Boolean, List, Integer from xblock.core import Scope, Object, Boolean, List
from fields import StringyBoolean, StringyInteger
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -31,22 +32,23 @@ def pretty_bool(value): ...@@ -31,22 +32,23 @@ def pretty_bool(value):
class WordCloudFields(object): class WordCloudFields(object):
"""XFields for word cloud.""" """XFields for word cloud."""
display_name = String( num_inputs = StringyInteger(
help="Display name for this module", display_name="Inputs",
scope=Scope.settings help="Number of text boxes available for students to input words/sentences.",
)
num_inputs = Integer(
help="Number of inputs.",
scope=Scope.settings, scope=Scope.settings,
default=5 default=5,
values={"min": 1}
) )
num_top_words = Integer( num_top_words = StringyInteger(
help="Number of max words, which will be displayed.", display_name="Maximum Words",
help="Maximum number of words to be displayed in generated word cloud.",
scope=Scope.settings, scope=Scope.settings,
default=250 default=250,
values={"min": 1}
) )
display_student_percents = Boolean( display_student_percents = StringyBoolean(
help="Display usage percents for each word?", display_name="Show Percents",
help="Statistics are shown for entered words near that word.",
scope=Scope.settings, scope=Scope.settings,
default=True default=True
) )
...@@ -205,7 +207,7 @@ class WordCloudModule(WordCloudFields, XModule): ...@@ -205,7 +207,7 @@ class WordCloudModule(WordCloudFields, XModule):
# Update top_words. # Update top_words.
self.top_words = self.top_dict( self.top_words = self.top_dict(
temp_all_words, temp_all_words,
int(self.num_top_words) self.num_top_words
) )
# Save all_words in database. # Save all_words in database.
...@@ -226,7 +228,7 @@ class WordCloudModule(WordCloudFields, XModule): ...@@ -226,7 +228,7 @@ class WordCloudModule(WordCloudFields, XModule):
'element_id': self.location.html_id(), 'element_id': self.location.html_id(),
'element_class': self.location.category, 'element_class': self.location.category,
'ajax_url': self.system.ajax_url, 'ajax_url': self.system.ajax_url,
'num_inputs': int(self.num_inputs), 'num_inputs': self.num_inputs,
'submitted': self.submitted 'submitted': self.submitted
} }
self.content = self.system.render_template('word_cloud.html', context) self.content = self.system.render_template('word_cloud.html', context)
......
import logging import logging
import copy
import yaml import yaml
import os import os
...@@ -9,7 +10,7 @@ from pkg_resources import resource_listdir, resource_string, resource_isdir ...@@ -9,7 +10,7 @@ from pkg_resources import resource_listdir, resource_string, resource_isdir
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from xblock.core import XBlock, Scope, String from xblock.core import XBlock, Scope, String, Integer, Float
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -80,7 +81,8 @@ class HTMLSnippet(object): ...@@ -80,7 +81,8 @@ class HTMLSnippet(object):
class XModuleFields(object): class XModuleFields(object):
display_name = String( display_name = String(
help="Display name for this module", display_name="Display Name",
help="This name appears in the horizontal navigation at the top of the page.",
scope=Scope.settings, scope=Scope.settings,
default=None default=None
) )
...@@ -458,7 +460,6 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock): ...@@ -458,7 +460,6 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
""" """
return False return False
# ================================= JSON PARSING =========================== # ================================= JSON PARSING ===========================
@staticmethod @staticmethod
def load_from_json(json_data, system, default_class=None): def load_from_json(json_data, system, default_class=None):
...@@ -625,7 +626,7 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock): ...@@ -625,7 +626,7 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
""" """
inherited_metadata = getattr(self, '_inherited_metadata', {}) inherited_metadata = getattr(self, '_inherited_metadata', {})
inheritable_metadata = getattr(self, '_inheritable_metadata', {}) inheritable_metadata = getattr(self, '_inheritable_metadata', {})
metadata = {} metadata_fields = {}
for field in self.fields: for field in self.fields:
if field.scope != Scope.settings or field in self.non_editable_metadata_fields: if field.scope != Scope.settings or field in self.non_editable_metadata_fields:
...@@ -641,13 +642,39 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock): ...@@ -641,13 +642,39 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
if field.name in inherited_metadata: if field.name in inherited_metadata:
explicitly_set = False explicitly_set = False
metadata[field.name] = {'field': field, # We support the following editors:
'value': value, # 1. A select editor for fields with a list of possible values (includes Booleans).
'default_value': default_value, # 2. Number editors for integers and floats.
# 3. A generic string editor for anything else (editing JSON representation of the value).
type = "Generic"
values = [] if field.values is None else copy.deepcopy(field.values)
if isinstance(values, tuple):
values = list(values)
if isinstance(values, list):
if len(values) > 0:
type = "Select"
for index, choice in enumerate(values):
json_choice = copy.deepcopy(choice)
if isinstance(json_choice, dict) and 'value' in json_choice:
json_choice['value'] = field.to_json(json_choice['value'])
else:
json_choice = field.to_json(json_choice)
values[index] = json_choice
elif isinstance(field, Integer):
type = "Integer"
elif isinstance(field, Float):
type = "Float"
metadata_fields[field.name] = {'field_name': field.name,
'type': type,
'display_name': field.display_name,
'value': field.to_json(value),
'options': values,
'default_value': field.to_json(default_value),
'inheritable': inheritable, 'inheritable': inheritable,
'explicitly_set': explicitly_set } 'explicitly_set': explicitly_set,
'help': field.help}
return metadata return metadata_fields
class DescriptorSystem(object): class DescriptorSystem(object):
......
/* HTML5 Number polyfill | Jonathan Stipe | https://github.com/jonstipe/number-polyfill*/
div.number-spin-btn-container {
display: inline-block;
position: absolute;
vertical-align: middle;
margin: 0 0 0 3px;
padding: 0;
left: 74%;
top: 6px;
}
div.number-spin-btn {
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
border-width: 2px;
border-color: #ededed #777777 #777777 #ededed;
border-style: solid;
background-color: #eeeeee;
width: 1em;
font-size: 14px; }
div.number-spin-btn:hover {
/* added blue hover color */
background-color: rgb(85, 151, 221);
cursor: pointer; }
div.number-spin-btn:active {
border-width: 2px;
border-color: #5e5e5e #d8d8d8 #d8d8d8 #5e5e5e;
border-style: solid;
background-color: #999999; }
div.number-spin-btn-up {
border-bottom-width: 1px;
-moz-border-radius: 0px;
-webkit-border-radius: 0px;
border-radius: 0px;
font-size: 14px; }
div.number-spin-btn-up:before {
border-width: 0 0.3em 0.3em 0.3em;
border-color: transparent transparent black transparent;
top: 25%; }
div.number-spin-btn-up:active {
border-bottom-width: 1px; }
div.number-spin-btn-up:active:before {
border-bottom-color: white;
top: 26%;
left: 51%; }
div.number-spin-btn-down {
border-top-width: 1px;
-moz-border-radius: 0px 0px 3px 3px;
-webkit-border-radius: 0px 0px 3px 3px;
border-radius: 0px 0px 3px 3px; }
div.number-spin-btn-down:before {
border-width: 0.3em 0.3em 0 0.3em;
border-color: black transparent transparent transparent;
top: 75%; }
div.number-spin-btn-down:active {
border-top-width: 1px; }
div.number-spin-btn-down:active:before {
border-top-color: white;
top: 76%;
left: 51%; }
div.number-spin-btn-up:before,
div.number-spin-btn-down:before {
content: "";
width: 0;
height: 0;
border-style: solid;
position: absolute;
left: 50%;
margin: -0.15em 0 0 -0.3em;
padding: 0; }
input:disabled + div.number-spin-btn-container > div.number-spin-btn-up:active, input:disabled + div.number-spin-btn-container > div.number-spin-btn-down:active {
border-color: #ededed #777777 #777777 #ededed;
border-style: solid;
background-color: #cccccc; }
input:disabled + div.number-spin-btn-container > div.number-spin-btn-up:before, input:disabled + div.number-spin-btn-container > div.number-spin-btn-up:active:before {
border-bottom-color: #999999;
top: 25%;
left: 50%; }
input:disabled + div.number-spin-btn-container > div.number-spin-btn-down:before, input:disabled + div.number-spin-btn-container > div.number-spin-btn-down:active:before {
border-top-color: #999999;
top: 75%;
left: 50%; }
// Generated by CoffeeScript 1.4.0
/*
HTML5 Number polyfill | Jonathan Stipe | https://github.com/jonstipe/number-polyfill
*/
(function() {
(function($) {
var i;
i = document.createElement("input");
i.setAttribute("type", "number");
if (i.type === "text") {
$.fn.inputNumber = function() {
var clipValues, decrement, domMouseScrollHandler, extractNumDecimalDigits, getParams, increment, matchStep, mouseWheelHandler;
getParams = function(elem) {
var $elem, max, min, step, val;
$elem = $(elem);
step = $elem.attr('step');
min = $elem.attr('min');
max = $elem.attr('max');
val = parseFloat($elem.val());
step = /^-?\d+(?:\.\d+)?$/.test(step) ? parseFloat(step) : null;
min = /^-?\d+(?:\.\d+)?$/.test(min) ? parseFloat(min) : null;
max = /^-?\d+(?:\.\d+)?$/.test(max) ? parseFloat(max) : null;
if (isNaN(val)) {
val = min || 0;
}
return {
min: min,
max: max,
step: step,
val: val
};
};
clipValues = function(value, min, max) {
if ((max != null) && value > max) {
return max;
} else if ((min != null) && value < min) {
return min;
} else {
return value;
}
};
extractNumDecimalDigits = function(input) {
var num, raisedNum;
if (input != null) {
num = 0;
raisedNum = input;
while (raisedNum !== Math.round(raisedNum)) {
num += 1;
raisedNum = input * Math.pow(10, num);
}
return num;
} else {
return 0;
}
};
matchStep = function(value, min, max, step) {
var mod, raiseTo, raisedMod, raisedStep, raisedStepDown, raisedStepUp, raisedValue, stepDecimalDigits, stepDown, stepUp;
stepDecimalDigits = extractNumDecimalDigits(step);
if (step == null) {
return value;
} else if (stepDecimalDigits === 0) {
mod = (value - (min || 0)) % step;
if (mod === 0) {
return value;
} else {
stepDown = value - mod;
stepUp = stepDown + step;
if ((stepUp > max) || ((value - stepDown) < (stepUp - value))) {
return stepDown;
} else {
return stepUp;
}
}
} else {
raiseTo = Math.pow(10, stepDecimalDigits);
raisedStep = step * raiseTo;
raisedMod = (value - (min || 0)) * raiseTo % raisedStep;
if (raisedMod === 0) {
return value;
} else {
raisedValue = value * raiseTo;
raisedStepDown = raisedValue - raisedMod;
raisedStepUp = raisedStepDown + raisedStep;
if (((raisedStepUp / raiseTo) > max) || ((raisedValue - raisedStepDown) < (raisedStepUp - raisedValue))) {
return raisedStepDown / raiseTo;
} else {
return raisedStepUp / raiseTo;
}
}
}
};
increment = function(elem) {
var newVal, params, raiseTo;
if (!$(elem).is(":disabled")) {
params = getParams(elem);
raiseTo = Math.pow(10, Math.max(extractNumDecimalDigits(params['val']), extractNumDecimalDigits(params['step'])));
newVal = (Math.round(params['val'] * raiseTo) + Math.round((params['step'] || 1) * raiseTo)) / raiseTo;
if ((params['max'] != null) && newVal > params['max']) {
newVal = params['max'];
}
newVal = matchStep(newVal, params['min'], params['max'], params['step']);
$(elem).val(newVal).change();
}
return null;
};
decrement = function(elem) {
var newVal, params, raiseTo;
if (!$(elem).is(":disabled")) {
params = getParams(elem);
raiseTo = Math.pow(10, Math.max(extractNumDecimalDigits(params['val']), extractNumDecimalDigits(params['step'])));
newVal = (Math.round(params['val'] * raiseTo) - Math.round((params['step'] || 1) * raiseTo)) / raiseTo;
if ((params['min'] != null) && newVal < params['min']) {
newVal = params['min'];
}
newVal = matchStep(newVal, params['min'], params['max'], params['step']);
$(elem).val(newVal).change();
}
return null;
};
domMouseScrollHandler = function(e) {
e.preventDefault();
if (e.originalEvent.detail < 0) {
increment(this);
} else {
decrement(this);
}
return null;
};
mouseWheelHandler = function(e) {
e.preventDefault();
if (e.originalEvent.wheelDelta > 0) {
increment(this);
} else {
decrement(this);
}
return null;
};
$(this).filter('input[type="number"]').each(function() {
var $downBtn, $elem, $upBtn, attrMutationCallback, attrObserver, btnContainer, downBtn, elem, halfHeight, upBtn;
elem = this;
$elem = $(elem);
halfHeight = ($elem.outerHeight() / 6) + 'px';
upBtn = document.createElement('div');
downBtn = document.createElement('div');
$upBtn = $(upBtn);
$downBtn = $(downBtn);
btnContainer = document.createElement('div');
$upBtn.addClass('number-spin-btn number-spin-btn-up').css('height', halfHeight);
$downBtn.addClass('number-spin-btn number-spin-btn-down').css('height', halfHeight);
btnContainer.appendChild(upBtn);
btnContainer.appendChild(downBtn);
$(btnContainer).addClass('number-spin-btn-container').insertAfter(elem);
$elem.on({
focus: function(e) {
$elem.on({
DOMMouseScroll: domMouseScrollHandler,
mousewheel: mouseWheelHandler
});
return null;
},
blur: function(e) {
$elem.off({
DOMMouseScroll: domMouseScrollHandler,
mousewheel: mouseWheelHandler
});
return null;
},
keypress: function(e) {
var _ref, _ref1;
if (e.keyCode === 38) {
increment(this);
} else if (e.keyCode === 40) {
decrement(this);
} else if (((_ref = e.keyCode) !== 8 && _ref !== 9 && _ref !== 35 && _ref !== 36 && _ref !== 37 && _ref !== 39) && ((_ref1 = e.which) !== 45 && _ref1 !== 46 && _ref1 !== 48 && _ref1 !== 49 && _ref1 !== 50 && _ref1 !== 51 && _ref1 !== 52 && _ref1 !== 53 && _ref1 !== 54 && _ref1 !== 55 && _ref1 !== 56 && _ref1 !== 57)) {
e.preventDefault();
}
return null;
},
change: function(e) {
var newVal, params;
if (e.originalEvent != null) {
params = getParams(this);
newVal = clipValues(params['val'], params['min'], params['max']);
newVal = matchStep(newVal, params['min'], params['max'], params['step'], params['stepDecimal']);
$(this).val(newVal);
}
return null;
}
});
$upBtn.on("mousedown", function(e) {
var releaseFunc, timeoutFunc;
increment(elem);
timeoutFunc = function(elem, incFunc) {
incFunc(elem);
$elem.data("timeoutID", window.setTimeout(timeoutFunc, 10, elem, incFunc));
return null;
};
releaseFunc = function(e) {
window.clearTimeout($elem.data("timeoutID"));
$(document).off('mouseup', releaseFunc);
$upBtn.off('mouseleave', releaseFunc);
return null;
};
$(document).on('mouseup', releaseFunc);
$upBtn.on('mouseleave', releaseFunc);
$elem.data("timeoutID", window.setTimeout(timeoutFunc, 700, elem, increment));
return null;
});
$downBtn.on("mousedown", function(e) {
var releaseFunc, timeoutFunc;
decrement(elem);
timeoutFunc = function(elem, decFunc) {
decFunc(elem);
$elem.data("timeoutID", window.setTimeout(timeoutFunc, 10, elem, decFunc));
return null;
};
releaseFunc = function(e) {
window.clearTimeout($elem.data("timeoutID"));
$(document).off('mouseup', releaseFunc);
$downBtn.off('mouseleave', releaseFunc);
return null;
};
$(document).on('mouseup', releaseFunc);
$downBtn.on('mouseleave', releaseFunc);
$elem.data("timeoutID", window.setTimeout(timeoutFunc, 700, elem, decrement));
return null;
});
$elem.css("textAlign", 'left');
if ($elem.css("opacity") !== "1") {
$(btnContainer).css("opacity", $elem.css("opacity"));
}
if ($elem.css("visibility") !== "visible") {
$(btnContainer).css("visibility", $elem.css("visibility"));
}
if (elem.style.display !== "") {
$(btnContainer).css("display", $elem.css("display"));
}
if ((typeof WebKitMutationObserver !== "undefined" && WebKitMutationObserver !== null) || (typeof MutationObserver !== "undefined" && MutationObserver !== null)) {
attrMutationCallback = function(mutations, observer) {
var mutation, _i, _len;
for (_i = 0, _len = mutations.length; _i < _len; _i++) {
mutation = mutations[_i];
if (mutation.type === "attributes") {
if (mutation.attributeName === "class") {
$(btnContainer).removeClass(mutation.oldValue).addClass(elem.className);
} else if (mutation.attributeName === "style") {
$(btnContainer).css({
"opacity": elem.style.opacity,
"visibility": elem.style.visibility,
"display": elem.style.display
});
}
}
}
return null;
};
attrObserver = (typeof WebKitMutationObserver !== "undefined" && WebKitMutationObserver !== null) ? new WebKitMutationObserver(attrMutationCallback) : ((typeof MutationObserver !== "undefined" && MutationObserver !== null) ? new MutationObserver(attrMutationCallback) : null);
attrObserver.observe(elem, {
attributes: true,
attributeOldValue: true,
attributeFilter: ["class", "style"]
});
} else if (typeof MutationEvent !== "undefined" && MutationEvent !== null) {
$elem.on("DOMAttrModified", function(evt) {
if (evt.originalEvent.attrName === "class") {
$(btnContainer).removeClass(evt.originalEvent.prevValue).addClass(evt.originalEvent.newValue);
} else if (evt.originalEvent.attrName === "style") {
$(btnContainer).css({
"display": elem.style.display,
"visibility": elem.style.visibility,
"opacity": elem.style.opacity
});
}
return null;
});
}
return null;
});
return $(this);
};
$(function() {
$('input[type="number"]').inputNumber();
return null;
});
null;
} else {
$.fn.inputNumber = function() {
return $(this);
};
null;
}
return null;
})(jQuery);
}).call(this);
This source diff could not be displayed because it is too large. You can view the blob instead.
34d1996e44f78168a73297217b3a0973c2ae90e1
\ No newline at end of file
...@@ -25,8 +25,8 @@ ...@@ -25,8 +25,8 @@
/* Layout */ /* Layout */
.studioSkin table.mceLayout {border:0;} .studioSkin table.mceLayout {border:0;}
.studioSkin table.mceLayout tr.mceFirst td {border-top:1px solid #3c3c3c;} .studioSkin table.mceLayout tr.mceFirst td {border-top: 1px solid #D1DCE6; border-left: none; border-right:none;}
.studioSkin table.mceLayout tr.mceLast td {border-bottom:1px solid #3c3c3c;} .studioSkin table.mceLayout tr.mceLast td {border-bottom:none;}
.studioSkin table.mceToolbar, .studioSkin tr.mceFirst .mceToolbar tr td, .studioSkin tr.mceLast .mceToolbar tr td {border:0; margin:0; padding:0;} .studioSkin table.mceToolbar, .studioSkin tr.mceFirst .mceToolbar tr td, .studioSkin tr.mceLast .mceToolbar tr td {border:0; margin:0; padding:0;}
.studioSkin td.mceToolbar { .studioSkin td.mceToolbar {
background: -webkit-linear-gradient(top, #d4dee8, #c9d5e2); background: -webkit-linear-gradient(top, #d4dee8, #c9d5e2);
...@@ -36,11 +36,11 @@ ...@@ -36,11 +36,11 @@
background: linear-gradient(top, #d4dee8, #c9d5e2); background: linear-gradient(top, #d4dee8, #c9d5e2);
border: 1px solid #3c3c3c; border: 1px solid #3c3c3c;
border-bottom-color: #a5aaaf; border-bottom-color: #a5aaaf;
border-radius: 3px 3px 0 0; border-radius: 0;
padding: 10px 10px 9px; padding: 10px 10px 9px;
vertical-align: top; vertical-align: top;
} }
.studioSkin .mceIframeContainer {border: 1px solid #3c3c3c; border-top: none;} .studioSkin .mceIframeContainer {border: 1px solid white; border-top: none;}
.studioSkin .mceStatusbar {background:#F0F0EE; font-size:9pt; line-height:16px; overflow:visible; color:#000; display:block; height:20px} .studioSkin .mceStatusbar {background:#F0F0EE; font-size:9pt; line-height:16px; overflow:visible; color:#000; display:block; height:20px}
.studioSkin .mceStatusbar div {float:left; margin:2px} .studioSkin .mceStatusbar div {float:left; margin:2px}
.studioSkin .mceStatusbar a.mceResize {display:block; float:right; background:url(../../img/studio-icons.png) -800px 0; width:20px; height:20px; cursor:se-resize; outline:0} .studioSkin .mceStatusbar a.mceResize {display:block; float:right; background:url(../../img/studio-icons.png) -800px 0; width:20px; height:20px; cursor:se-resize; outline:0}
......
This directory contains some high level documentation for the code. We should strive to keep it up-to-date, but don't take it as the absolute truth. This directory contains some high level documentation for the code.
A good place to start is 'overview.md' WARNING: much of this is out-of-date. It stil may be helpful, though.
# Documentation for edX code (mitx repo) # Documentation for edX code (edx-platform repo)
This document explains the general structure of the edX platform, and defines some of the acronyms and terms you'll see flying around in the code. This document explains the general structure of the edX platform, and defines some of the acronyms and terms you'll see flying around in the code.
......
...@@ -115,6 +115,11 @@ xmodule can be tested independently, with this: ...@@ -115,6 +115,11 @@ xmodule can be tested independently, with this:
rake test_common/lib/xmodule rake test_common/lib/xmodule
other module level tests include
* `rake test_common/lib/capa`
* `rake test_common/lib/calc`
To run a single django test class: To run a single django test class:
rake test_lms[courseware.tests.tests:testViewAuth] rake test_lms[courseware.tests.tests:testViewAuth]
......
...@@ -20,7 +20,7 @@ logger = getLogger(__name__) ...@@ -20,7 +20,7 @@ logger = getLogger(__name__)
TEST_COURSE_ORG = 'edx' TEST_COURSE_ORG = 'edx'
TEST_COURSE_NAME = 'Test Course' TEST_COURSE_NAME = 'Test Course'
TEST_SECTION_NAME = "Problem" TEST_SECTION_NAME = 'Test Section'
@step(u'The course "([^"]*)" exists$') @step(u'The course "([^"]*)" exists$')
......
...@@ -3,4 +3,4 @@ Feature: Video component ...@@ -3,4 +3,4 @@ Feature: Video component
Scenario: Autoplay is enabled in LMS Scenario: Autoplay is enabled in LMS
Given the course has a Video component Given the course has a Video component
Then when I view it it does autoplay Then when I view the video it has autoplay enabled
\ No newline at end of file
#pylint: disable=C0111 #pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from common import * from lettuce.django import django_url
from common import TEST_COURSE_NAME, TEST_SECTION_NAME, i_am_registered_for_the_course, section_location
############### ACTIONS #################### ############### ACTIONS ####################
@step('when I view it it does autoplay') @step('when I view the video it has autoplay enabled')
def does_autoplay(step): def does_autoplay(step):
assert(world.css_find('.video')[0]['data-autoplay'] == 'True') assert(world.css_find('.video')[0]['data-autoplay'] == 'True')
...@@ -18,7 +18,7 @@ def view_video(step): ...@@ -18,7 +18,7 @@ def view_video(step):
i_am_registered_for_the_course(step, coursename) i_am_registered_for_the_course(step, coursename)
# Make sure we have a video # Make sure we have a video
video = add_video_to_course(coursename) add_video_to_course(coursename)
chapter_name = TEST_SECTION_NAME.replace(" ", "_") chapter_name = TEST_SECTION_NAME.replace(" ", "_")
section_name = chapter_name section_name = chapter_name
url = django_url('/courses/edx/Test_Course/Test_Course/courseware/%s/%s' % url = django_url('/courses/edx/Test_Course/Test_Course/courseware/%s/%s' %
......
...@@ -10,6 +10,7 @@ from mock import MagicMock, patch, Mock ...@@ -10,6 +10,7 @@ from mock import MagicMock, patch, Mock
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.http import HttpResponse from django.http import HttpResponse
from django.conf import settings
from mitxmako.shortcuts import render_to_string from mitxmako.shortcuts import render_to_string
from xmodule.open_ended_grading_classes import peer_grading_service, controller_query_service from xmodule.open_ended_grading_classes import peer_grading_service, controller_query_service
...@@ -31,7 +32,6 @@ from xmodule.tests import test_util_open_ended ...@@ -31,7 +32,6 @@ from xmodule.tests import test_util_open_ended
from courseware.tests import factories from courseware.tests import factories
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class TestStaffGradingService(LoginEnrollmentTestCase): class TestStaffGradingService(LoginEnrollmentTestCase):
''' '''
...@@ -310,8 +310,7 @@ class TestPanel(LoginEnrollmentTestCase): ...@@ -310,8 +310,7 @@ class TestPanel(LoginEnrollmentTestCase):
found_module, peer_grading_module = views.find_peer_grading_module(self.course) found_module, peer_grading_module = views.find_peer_grading_module(self.course)
self.assertTrue(found_module) self.assertTrue(found_module)
@patch('xmodule.open_ended_grading_classes.controller_query_service.ControllerQueryService', @patch('open_ended_grading.views.controller_qs', controller_query_service.MockControllerQueryService(settings.OPEN_ENDED_GRADING_INTERFACE, views.system))
controller_query_service.MockControllerQueryService)
def test_problem_list(self): def test_problem_list(self):
""" """
Ensure that the problem list from the grading controller server can be rendered properly locally Ensure that the problem list from the grading controller server can be rendered properly locally
...@@ -319,4 +318,4 @@ class TestPanel(LoginEnrollmentTestCase): ...@@ -319,4 +318,4 @@ class TestPanel(LoginEnrollmentTestCase):
""" """
request = Mock(user=self.user) request = Mock(user=self.user)
response = views.student_problem_list(request, self.course.id) response = views.student_problem_list(request, self.course.id)
self.assertTrue(isinstance(response, HttpResponse)) self.assertRegexpMatches(response.content, "Here are a list of open ended problems for this course.")
...@@ -36,6 +36,7 @@ system = ModuleSystem( ...@@ -36,6 +36,7 @@ system = ModuleSystem(
replace_urls=None, replace_urls=None,
xblock_model_data={} xblock_model_data={}
) )
controller_qs = ControllerQueryService(settings.OPEN_ENDED_GRADING_INTERFACE, system) controller_qs = ControllerQueryService(settings.OPEN_ENDED_GRADING_INTERFACE, system)
""" """
......
"""
This config file runs the simplest dev environment using sqlite, and db-based
sessions. Assumes structure:
/envroot/
/db # This is where it'll write the database file
/mitx # The location of this repo
/log # Where we're going to write log files
"""
# 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 .common import *
from logsettings import get_logger_config
import os
DEBUG = True
INSTALLED_APPS = [
app
for app
in INSTALLED_APPS
]
# Nose Test Runner
INSTALLED_APPS += ['django_nose']
#NOSE_ARGS = ['--cover-erase', '--with-xunit', '--with-xcoverage', '--cover-html', '--cover-inclusive']
NOSE_ARGS = ['--cover-erase', '--with-xunit', '--cover-html', '--cover-inclusive']
for app in os.listdir(PROJECT_ROOT / 'djangoapps'):
NOSE_ARGS += ['--cover-package', app]
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
# Local Directories
TEST_ROOT = path("test_root")
COURSES_ROOT = TEST_ROOT / "data"
DATA_DIR = COURSES_ROOT
MAKO_TEMPLATES['course'] = [DATA_DIR]
MAKO_TEMPLATES['sections'] = [DATA_DIR / 'sections']
MAKO_TEMPLATES['custom_tags'] = [DATA_DIR / 'custom_tags']
MAKO_TEMPLATES['main'] = [PROJECT_ROOT / 'templates',
DATA_DIR / 'info',
DATA_DIR / 'problems']
LOGGING = get_logger_config(TEST_ROOT / "log",
logging_env="dev",
tracking_filename="tracking.log",
debug=True)
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': PROJECT_ROOT / "db" / "mitx.db",
}
}
CACHES = {
# This is the cache used for most things.
# In staging/prod envs, the sessions also live here.
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'mitx_loc_mem_cache',
'KEY_FUNCTION': 'util.memcache.safe_key',
},
# The general cache is what you get if you use our util.cache. It's used for
# things like caching the course.xml file for different A/B test groups.
# We set it to be a DummyCache to force reloading of course.xml in dev.
# In staging environments, we would grab VERSION from data uploaded by the
# push process.
'general': {
'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
'KEY_PREFIX': 'general',
'VERSION': 4,
'KEY_FUNCTION': 'util.memcache.safe_key',
}
}
# Dummy secret key for dev
SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd'
############################ FILE UPLOADS (for discussion forums) #############################
DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
MEDIA_ROOT = PROJECT_ROOT / "uploads"
MEDIA_URL = "/static/uploads/"
STATICFILES_DIRS.append(("uploads", MEDIA_ROOT))
FILE_UPLOAD_TEMP_DIR = PROJECT_ROOT / "uploads"
FILE_UPLOAD_HANDLERS = (
'django.core.files.uploadhandler.MemoryFileUploadHandler',
'django.core.files.uploadhandler.TemporaryFileUploadHandler',
)
b154ce99fb5c8d413ba769e8cc0df94ed674c3f4
\ No newline at end of file
2b8c58b098bdb17f9ddcbb2098f94c50fdcedf60
\ No newline at end of file
7d8b9879f7e5b859910edba7249661eedd3fcf37
\ No newline at end of file
caf8b43337faa75cef5da5cd090010215a67b1bd
\ No newline at end of file
b4d043bb1ca4a8815d4a388a2c9d96038211417b
\ No newline at end of file
6718f0c6e851376b5478baff94e1f1f4449bd938
\ No newline at end of file
...@@ -85,8 +85,8 @@ ...@@ -85,8 +85,8 @@
} }
@-webkit-keyframes fadeIn { @-webkit-keyframes fadeIn {
0% { opacity: 0; } 0% { opacity: 0.0; }
100% { opacity: 1; } 100% { opacity: 1.0; }
} }
...@@ -736,11 +736,11 @@ body.discussion { ...@@ -736,11 +736,11 @@ body.discussion {
&.is-open { &.is-open {
.browse-topic-drop-btn span { .browse-topic-drop-btn span {
opacity: 1; opacity: 1.0;
} }
.browse-topic-drop-icon { .browse-topic-drop-icon {
opacity: 0; opacity: 0.0;
} }
&.is-dropped { &.is-dropped {
...@@ -788,7 +788,7 @@ body.discussion { ...@@ -788,7 +788,7 @@ body.discussion {
&::-webkit-input-placeholder, &::-webkit-input-placeholder,
&:-moz-placeholder, &:-moz-placeholder,
&:-ms-input-placeholder { &:-ms-input-placeholder {
opacity: 1; opacity: 1.0;
} }
} }
} }
...@@ -818,7 +818,7 @@ body.discussion { ...@@ -818,7 +818,7 @@ body.discussion {
line-height: 58px; line-height: 58px;
color: #333; color: #333;
text-shadow: 0 1px 0 rgba(255, 255, 255, .8); text-shadow: 0 1px 0 rgba(255, 255, 255, .8);
opacity: 0; opacity: 0.0;
@include transition(opacity .2s); @include transition(opacity .2s);
} }
} }
...@@ -833,7 +833,7 @@ body.discussion { ...@@ -833,7 +833,7 @@ body.discussion {
height: 16px; height: 16px;
margin-left: -12px; margin-left: -12px;
background: url(../images/browse-icon.png) no-repeat; background: url(../images/browse-icon.png) no-repeat;
opacity: 1; opacity: 1.0;
@include transition(none); @include transition(none);
} }
...@@ -967,7 +967,7 @@ body.discussion { ...@@ -967,7 +967,7 @@ body.discussion {
&::-webkit-input-placeholder, &::-webkit-input-placeholder,
&:-moz-placeholder, &:-moz-placeholder,
&:-ms-input-placeholder { &:-ms-input-placeholder {
opacity: 0; opacity: 0.0;
@include transition(opacity .2s); @include transition(opacity .2s);
} }
...@@ -2454,7 +2454,7 @@ body.discussion { ...@@ -2454,7 +2454,7 @@ body.discussion {
font-style: italic; font-style: italic;
cursor:pointer; cursor:pointer;
margin-right: 10px; margin-right: 10px;
opacity:.8; opacity: 0.8;
span { span {
cursor: pointer; cursor: pointer;
...@@ -2462,7 +2462,7 @@ body.discussion { ...@@ -2462,7 +2462,7 @@ body.discussion {
&:hover { &:hover {
@include transition(opacity .2s); @include transition(opacity .2s);
opacity: 1; opacity: 1.0;
} }
} }
...@@ -2475,7 +2475,7 @@ body.discussion { ...@@ -2475,7 +2475,7 @@ body.discussion {
top:-13px; top:-13px;
margin-right:35px; margin-right:35px;
margin-top:13px; margin-top:13px;
opacity: 1; opacity: 1.0;
} }
.notpinned .icon { .notpinned .icon {
...@@ -2523,11 +2523,11 @@ display:none; ...@@ -2523,11 +2523,11 @@ display:none;
padding-right: 5px; padding-right: 5px;
font-style: italic; font-style: italic;
cursor:pointer; cursor:pointer;
opacity:.8; opacity: 0.8;
&:hover { &:hover {
@include transition(opacity .2s); @include transition(opacity .2s);
opacity: 1; opacity: 1.0;
} }
} }
......
...@@ -9,12 +9,12 @@ ...@@ -9,12 +9,12 @@
@mixin home-header-pop-up-keyframes { @mixin home-header-pop-up-keyframes {
0% { 0% {
opacity: 0; opacity: 0.0;
top: 300px; top: 300px;
//@include transform(scale(0.9)); //@include transform(scale(0.9));
} }
45% { 45% {
opacity: 1; opacity: 1.0;
} }
65% { 65% {
top: -40px; top: -40px;
...@@ -43,19 +43,19 @@ ...@@ -43,19 +43,19 @@
@mixin title-appear-keyframes { @mixin title-appear-keyframes {
0% { 0% {
opacity: 0; opacity: 0.0;
top: 60px; top: 60px;
@include transform(scale(0.9)); @include transform(scale(0.9));
} }
20% { 20% {
opacity: 1; opacity: 1.0;
} }
27% { // this % of total-time should be ~ 1.25s 27% { // this % of total-time should be ~ 1.25s
top: 40px; top: 40px;
@include transform(scale(1)); @include transform(scale(1));
} }
90% { // this % of total-time is when 2nd half of animation starts 90% { // this % of total-time is when 2nd half of animation starts
opacity: 1; opacity: 1.0;
top: 40px; top: 40px;
@include transform(scale(1)); @include transform(scale(1));
} }
...@@ -79,24 +79,24 @@ ...@@ -79,24 +79,24 @@
@mixin home-appear-keyframes { @mixin home-appear-keyframes {
0% { 0% {
opacity: 0; opacity: 0.0;
top: 60px; top: 60px;
@include transform(scale(0.9)); @include transform(scale(0.9));
} }
20% { 20% {
opacity: 1; opacity: 1.0;
} }
30% { // this % of total-time should be ~ 1.25s 30% { // this % of total-time should be ~ 1.25s
top: 40px; top: 40px;
@include transform(scale(1)); @include transform(scale(1));
} }
80% { // this % of total-time is when 2nd half of animation starts 80% { // this % of total-time is when 2nd half of animation starts
opacity: 1; opacity: 1.0;
top: 40px; top: 40px;
@include transform(scale(1)); @include transform(scale(1));
} }
100% { 100% {
opacity: 0; opacity: 0.0;
top: 60px; top: 60px;
@include transform(scale(0.7)); @include transform(scale(0.7));
} }
...@@ -117,10 +117,10 @@ ...@@ -117,10 +117,10 @@
@mixin edx-appear-keyframes { @mixin edx-appear-keyframes {
0% { 0% {
opacity: 0; opacity: 0.0;
} }
100% { 100% {
opacity: 1; opacity: 1.0;
} }
} }
...@@ -231,7 +231,7 @@ ...@@ -231,7 +231,7 @@
opacity: 0.9; opacity: 0.9;
} }
80% { 80% {
opacity: 1; opacity: 1.0;
} }
100% { 100% {
bottom: 0px; bottom: 0px;
......
...@@ -38,7 +38,7 @@ div.book-wrapper { ...@@ -38,7 +38,7 @@ div.book-wrapper {
line-height: 2.1em; line-height: 2.1em;
text-align: right; text-align: right;
color: #9a9a9a; color: #9a9a9a;
opacity: 0; opacity: 0.0;
@include transition(opacity .15s); @include transition(opacity .15s);
} }
...@@ -55,7 +55,7 @@ div.book-wrapper { ...@@ -55,7 +55,7 @@ div.book-wrapper {
background-color: transparent; background-color: transparent;
.page-number { .page-number {
opacity: 1; opacity: 1.0;
} }
} }
} }
...@@ -119,7 +119,7 @@ div.book-wrapper { ...@@ -119,7 +119,7 @@ div.book-wrapper {
@include box-sizing(border-box); @include box-sizing(border-box);
display: table; display: table;
height: 100%; height: 100%;
opacity: 0; opacity: 0.0;
filter: alpha(opacity=0); filter: alpha(opacity=0);
text-indent: -9999px; text-indent: -9999px;
@include transition; @include transition;
...@@ -127,7 +127,7 @@ div.book-wrapper { ...@@ -127,7 +127,7 @@ div.book-wrapper {
width: 100%; width: 100%;
&:hover { &:hover {
opacity: 1; opacity: 1.0;
filter: alpha(opacity=100); filter: alpha(opacity=100);
} }
} }
......
...@@ -61,7 +61,7 @@ section.course-index { ...@@ -61,7 +61,7 @@ section.course-index {
span.ui-icon { span.ui-icon {
left: 0; left: 0;
background-image: url("/static/images/ui-icons_222222_256x240.png"); background-image: url("/static/images/ui-icons_222222_256x240.png");
opacity: .3; opacity: 0.3;
} }
} }
} }
...@@ -146,7 +146,7 @@ section.course-index { ...@@ -146,7 +146,7 @@ section.course-index {
@include box-shadow(inset 0 1px 14px 0 rgba(0,0,0, 0.1)); @include box-shadow(inset 0 1px 14px 0 rgba(0,0,0, 0.1));
&:after { &:after {
opacity: 1; opacity: 1.0;
right: 15px; right: 15px;
} }
} }
...@@ -174,7 +174,7 @@ section.course-index { ...@@ -174,7 +174,7 @@ section.course-index {
background: $sidebar-active-image; background: $sidebar-active-image;
&:after { &:after {
opacity: 1; opacity: 1.0;
right: 15px; right: 15px;
} }
......
...@@ -27,7 +27,7 @@ div.calc-main { ...@@ -27,7 +27,7 @@ div.calc-main {
width: 16px; width: 16px;
&:hover { &:hover {
opacity: .8; opacity: 0.8;
} }
&.closed { &.closed {
...@@ -136,7 +136,7 @@ div.calc-main { ...@@ -136,7 +136,7 @@ div.calc-main {
&.shown { &.shown {
display: block; display: block;
opacity: 1; opacity: 1.0;
} }
dt { dt {
......
...@@ -26,7 +26,7 @@ header.global { ...@@ -26,7 +26,7 @@ header.global {
} }
h2 { h2 {
opacity: 1; opacity: 1.0;
} }
} }
...@@ -51,7 +51,7 @@ header.global { ...@@ -51,7 +51,7 @@ header.global {
text-decoration: none; text-decoration: none;
&::before { &::before {
opacity: 1; opacity: 1.0;
} }
.name { .name {
......
...@@ -429,7 +429,7 @@ ...@@ -429,7 +429,7 @@
&:hover { &:hover {
.sharing-message { .sharing-message {
opacity: 1; opacity: 1.0;
top: 56px; top: 56px;
} }
} }
...@@ -470,7 +470,7 @@ ...@@ -470,7 +470,7 @@
width: 44px; width: 44px;
&:hover { &:hover {
opacity: 1; opacity: 1.0;
} }
img { img {
...@@ -514,7 +514,7 @@ ...@@ -514,7 +514,7 @@
&:hover { &:hover {
.icon { .icon {
opacity: 1; opacity: 1.0;
} }
} }
......
...@@ -74,7 +74,7 @@ ...@@ -74,7 +74,7 @@
&:hover { &:hover {
.title .icon { .title .icon {
opacity: 1; opacity: 1.0;
} }
} }
......
...@@ -53,7 +53,7 @@ ...@@ -53,7 +53,7 @@
@include box-sizing(border-box); @include box-sizing(border-box);
@include inline-block; @include inline-block;
left: 0px; left: 0px;
opacity: 1; opacity: 1.0;
padding: 20px 30px; padding: 20px 30px;
top: 0px; top: 0px;
@include transition(all, 0.2s, linear); @include transition(all, 0.2s, linear);
...@@ -312,7 +312,7 @@ ...@@ -312,7 +312,7 @@
text-decoration: none; text-decoration: none;
&::before { &::before {
opacity: 1; opacity: 1.0;
} }
.name { .name {
......
...@@ -117,7 +117,7 @@ ...@@ -117,7 +117,7 @@
.info-link { .info-link {
color: $link-color; color: $link-color;
opacity: 1; opacity: 1.0;
} }
h2 { h2 {
......
...@@ -300,8 +300,29 @@ ...@@ -300,8 +300,29 @@
} }
} }
} }
#help_wrapper {
padding: 0 ($baseline*1.5) ($baseline*1.5) ($baseline*1.5);
header {
margin-bottom: $baseline;
padding-right: 0;
padding-left: 0;
}
}
.tip {
font-size: 12px;
display: block;
color: $dark-gray;
}
} }
.leanModal_box { .leanModal_box {
@extend .modal; @extend .modal;
} }
...@@ -20,17 +20,15 @@ ...@@ -20,17 +20,15 @@
<% <%
discussion_link = get_discussion_link(course) if course else None discussion_link = get_discussion_link(course) if course else None
%> %>
% if discussion_link:
<p> <p>For <strong>questions on course lectures, homework, tools, or materials for this course</strong>, post in the
Have a course-specific question? <a href="${discussion_link}" target="_blank"/>course discussion forum</a>.
<a href="${discussion_link}" target="_blank"/>
Post it on the course forums.
</a>
</p> </p>
<hr> <p>Have <strong>general questions about edX</strong>? You can find lots of helpful information in the edX
% endif <a href="/help" target="_blank">FAQ</a>.</p>
<p>Have a general question about edX? <a href="/help" target="_blank">Check the FAQ</a>.</p> <p>Have a <strong>question about something specific</strong>? You can contact the edX general
support team directly:</p>
<hr> <hr>
<div class="help-buttons"> <div class="help-buttons">
...@@ -58,9 +56,10 @@ discussion_link = get_discussion_link(course) if course else None ...@@ -58,9 +56,10 @@ discussion_link = get_discussion_link(course) if course else None
<label data-field="email">E-mail*</label> <label data-field="email">E-mail*</label>
<input name="email" type="text"> <input name="email" type="text">
% endif % endif
<label data-field="subject">Subject*</label> <label data-field="subject">Briefly describe your issue*</label>
<input name="subject" type="text"> <input name="subject" type="text">
<label data-field="details">Details*</label> <label data-field="details">Tell us the details*
<span class="tip">Include error messages, steps which lead to the issue, etc</span></label>
<textarea name="details"></textarea> <textarea name="details"></textarea>
<input name="tag" type="hidden"> <input name="tag" type="hidden">
<div class="submit"> <div class="submit">
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
<div class="content"> <div class="content">
<div class="log-in-form"> <div class="log-in-form">
<h2>Log in to your courses</h2> <h2>Log in to your courses</h2>
<form id="login_form" data-remote="true" method="post" action="/login"> <form id="login_form" data-remote="true" method="post" action="/login_ajax">
<div class="row"> <div class="row">
<label>Email</label> <label>Email</label>
<input name="email" type="email" class="email-field" tabindex="1"> <input name="email" type="email" class="email-field" tabindex="1">
......
...@@ -3,22 +3,7 @@ ...@@ -3,22 +3,7 @@
% endif % endif
%if settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']: %if settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']:
<div id="stub_out_video_for_testing"> <div id="stub_out_video_for_testing"></div>
<div class="video" data-autoplay="${settings.MITX_FEATURES['AUTOPLAY_VIDEOS']}">
<div class="slider"></div>
<div>
<ul class="vcr">
<li><a class="video_control" href="#"></a></li>
<li>
<div class="vidtime">0:00 / 0:00</div>
</li>
</ul>
<div class="secondary-controls">
<a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a>
</div>
</div>
</div>
</div>
%else: %else:
<div <div
id="video_${id}" id="video_${id}"
......
...@@ -330,13 +330,6 @@ if settings.DEBUG or settings.MITX_FEATURES.get('ENABLE_DJANGO_ADMIN_SITE'): ...@@ -330,13 +330,6 @@ if settings.DEBUG or settings.MITX_FEATURES.get('ENABLE_DJANGO_ADMIN_SITE'):
## Jasmine and admin ## Jasmine and admin
urlpatterns += (url(r'^admin/', include(admin.site.urls)),) urlpatterns += (url(r'^admin/', include(admin.site.urls)),)
if settings.DEBUG:
# Originally added to allow debugging issues when prod is
# mysteriously different from staging (specifically missing get
# parameters in certain cases), but removing from prod because
# it's a security risk.
urlpatterns += (url(r'^debug_request$', 'util.views.debug_request'),)
if settings.MITX_FEATURES.get('AUTH_USE_OPENID'): if settings.MITX_FEATURES.get('AUTH_USE_OPENID'):
urlpatterns += ( urlpatterns += (
url(r'^openid/login/$', 'django_openid_auth.views.login_begin', name='openid-login'), url(r'^openid/login/$', 'django_openid_auth.views.login_begin', name='openid-login'),
......
...@@ -10,11 +10,14 @@ end ...@@ -10,11 +10,14 @@ end
# the ENV_TOKENS to the templating context. # the ENV_TOKENS to the templating context.
def preprocess_with_mako(filename) def preprocess_with_mako(filename)
# simple command-line invocation of Mako engine # simple command-line invocation of Mako engine
# cdodge: the .gsub() are used to translate true->True and false->False to make the generated
# python actually valid python. This is just a short term hack to unblock the release train
# until a real fix can be made by people who know this better
mako = "from mako.template import Template;" + mako = "from mako.template import Template;" +
"print Template(filename=\"#{filename}\")" + "print Template(filename=\"#{filename}\")" +
# Total hack. It works because a Python dict literal has # Total hack. It works because a Python dict literal has
# the same format as a JSON object. # the same format as a JSON object.
".render(env=#{ENV_TOKENS.to_json});" ".render(env=#{ENV_TOKENS.to_json.gsub("true","True").gsub("false","False")});"
# strip off the .mako extension # strip off the .mako extension
output_filename = filename.chomp(File.extname(filename)) output_filename = filename.chomp(File.extname(filename))
......
...@@ -4,7 +4,7 @@ beautifulsoup4==4.1.3 ...@@ -4,7 +4,7 @@ beautifulsoup4==4.1.3
beautifulsoup==3.2.1 beautifulsoup==3.2.1
boto==2.6.0 boto==2.6.0
celery==3.0.19 celery==3.0.19
distribute==0.6.28 distribute>=0.6.28
django-celery==3.0.17 django-celery==3.0.17
django-countries==1.5 django-countries==1.5
django-followit==0.0.3 django-followit==0.0.3
...@@ -19,7 +19,7 @@ django-sekizai==0.6.1 ...@@ -19,7 +19,7 @@ django-sekizai==0.6.1
django-ses==0.4.1 django-ses==0.4.1
django-storages==1.1.5 django-storages==1.1.5
django-threaded-multihost==1.4-1 django-threaded-multihost==1.4-1
django==1.4.3 django==1.4.5
feedparser==5.1.3 feedparser==5.1.3
fs==0.4.0 fs==0.4.0
GitPython==0.3.2.RC1 GitPython==0.3.2.RC1
......
...@@ -9,4 +9,4 @@ ...@@ -9,4 +9,4 @@
# Our libraries: # Our libraries:
-e git+https://github.com/edx/XBlock.git@2144a25d#egg=XBlock -e git+https://github.com/edx/XBlock.git@2144a25d#egg=XBlock
-e git+https://github.com/edx/codejail.git@874361f#egg=codejail -e git+https://github.com/edx/codejail.git@5fb5fa0#egg=codejail
python-software-properties python-software-properties
pkg-config pkg-config
gfortran
libatlas-dev
libblas-dev
liblapack-dev
liblapack3gf
curl curl
git git
python-virtualenv python-virtualenv
python-scipy
python-numpy
build-essential build-essential
python-dev python-dev
gfortran gfortran
liblapack-dev
libfreetype6-dev libfreetype6-dev
libpng12-dev libpng12-dev
libjpeg-dev libjpeg-dev
...@@ -14,6 +20,7 @@ libxml2-dev ...@@ -14,6 +20,7 @@ libxml2-dev
libxslt-dev libxslt-dev
yui-compressor yui-compressor
graphviz graphviz
libgraphviz-dev
graphviz-dev graphviz-dev
mysql-server mysql-server
libmysqlclient-dev libmysqlclient-dev
...@@ -23,3 +30,7 @@ libreadline6-dev ...@@ -23,3 +30,7 @@ libreadline6-dev
mongodb mongodb
nodejs nodejs
coffeescript coffeescript
mysql
libmysqlclient-dev
virtualenvwrapper
libgeos-ruby1.8
#!/usr/bin/env bash #!/usr/bin/env bash
#Exit if any commands return a non-zero status
set -e set -e
# posix compliant sanity check # posix compliant sanity check
...@@ -27,10 +29,17 @@ EOL ...@@ -27,10 +29,17 @@ EOL
} }
#Setting error color to red before reset
error() { error() {
printf '\E[31m'; echo "$@"; printf '\E[0m' printf '\E[31m'; echo "$@"; printf '\E[0m'
} }
#Setting warning color to magenta before reset
warning() {
printf '\E[35m'; echo "$@"; printf '\E[0m'
}
#Setting output color to cyan before reset
output() { output() {
printf '\E[36m'; echo "$@"; printf '\E[0m' printf '\E[36m'; echo "$@"; printf '\E[0m'
} }
...@@ -51,7 +60,7 @@ EO ...@@ -51,7 +60,7 @@ EO
info() { info() {
cat<<EO cat<<EO
MITx base dir : $BASE edX base dir : $BASE
Python virtualenv dir : $PYTHON_DIR Python virtualenv dir : $PYTHON_DIR
Ruby RVM dir : $RUBY_DIR Ruby RVM dir : $RUBY_DIR
Ruby ver : $RUBY_VER Ruby ver : $RUBY_VER
...@@ -59,36 +68,31 @@ info() { ...@@ -59,36 +68,31 @@ info() {
EO EO
} }
change_git_push_defaults() {
#Set git push defaults to upstream rather than master
output "Changing git defaults"
git config --global push.default upstream
}
clone_repos() { clone_repos() {
cd "$BASE"
if [[ -d "$BASE/mitx/.git" ]]; then change_git_push_defaults
output "Pulling mitx"
cd "$BASE/mitx"
git pull
else
output "Cloning mitx"
if [[ -d "$BASE/mitx" ]]; then
mv "$BASE/mitx" "${BASE}/mitx.bak.$$"
fi
git clone git@github.com:MITx/mitx.git
fi
# By default, dev environments start with a copy of 6.002x
cd "$BASE" cd "$BASE"
mkdir -p "$BASE/data"
REPO="content-mit-6002x" if [[ -d "$BASE/edx-platform/.git" ]]; then
if [[ -d "$BASE/data/$REPO/.git" ]]; then output "Pulling edx platform"
output "Pulling $REPO" cd "$BASE/edx-platform"
cd "$BASE/data/$REPO"
git pull git pull
else else
output "Cloning $REPO" output "Cloning edx platform"
if [[ -d "$BASE/data/$REPO" ]]; then if [[ -d "$BASE/edx-platform" ]]; then
mv "$BASE/data/$REPO" "${BASE}/data/$REPO.bak.$$" output "Creating backup for existing edx platform"
mv "$BASE/edx-platform" "${BASE}/edx-platform.bak.$$"
fi fi
cd "$BASE/data" git clone https://github.com/edx/edx-platform.git
git clone git@github.com:MITx/$REPO
fi fi
} }
...@@ -98,7 +102,7 @@ clone_repos() { ...@@ -98,7 +102,7 @@ clone_repos() {
PROG=${0##*/} PROG=${0##*/}
# Adjust this to wherever you'd like to place the codebase # Adjust this to wherever you'd like to place the codebase
BASE="${PROJECT_HOME:-$HOME}/mitx_all" BASE="${PROJECT_HOME:-$HOME}/edx_all"
# Use a sensible default (~/.virtualenvs) for your Python virtualenvs # Use a sensible default (~/.virtualenvs) for your Python virtualenvs
# unless you've already got one set up with virtualenvwrapper. # unless you've already got one set up with virtualenvwrapper.
...@@ -161,7 +165,7 @@ done ...@@ -161,7 +165,7 @@ done
cat<<EO cat<<EO
This script will setup a local MITx environment, this This script will setup a local edX environment, this
includes includes
* Django * Django
...@@ -202,9 +206,31 @@ case `uname -s` in ...@@ -202,9 +206,31 @@ case `uname -s` in
distro=`lsb_release -cs` distro=`lsb_release -cs`
case $distro in case $distro in
maya|lisa|natty|oneiric|precise|quantal) wheezy|jessie|maya|olivia|nadia|precise|quantal)
warning "Debian support is not fully debugged. Assuming you have standard
development packages already working like scipy rvm, the
installation should go fine, but this is still a work in progress.
Please report issues you have and let us know if you are able to figure
out any workarounds or solutions
Press return to continue or control-C to abort"
read dummy
sudo apt-get install git ;;
squeeze|lisa|katya|oneiric|natty|raring)
warning "It seems like you're using $distro which has been deprecated.
While we don't technically support this release, the install
script will probably still work.
Raring requires an install of rvm to work correctly as the raring
package manager does not yet include a package for rvm
Press return to continue or control-C to abort"
read dummy
sudo apt-get install git sudo apt-get install git
;; ;;
*) *)
error "Unsupported distribution - $distro" error "Unsupported distribution - $distro"
exit 1 exit 1
...@@ -241,7 +267,7 @@ EO ...@@ -241,7 +267,7 @@ EO
;; ;;
*) *)
error "Unsupported platform" error "Unsupported platform. Try switching to either Mac or a Debian-based linux distribution (Ubuntu, Debian, or Mint)"
exit 1 exit 1
;; ;;
esac esac
...@@ -251,11 +277,18 @@ esac ...@@ -251,11 +277,18 @@ esac
clone_repos clone_repos
# Sanity check to make sure the repo layout hasn't changed
if [[ -d $BASE/edx-platform/scripts ]]; then
output "Installing system-level dependencies"
bash $BASE/edx-platform/scripts/install-system-req.sh
else
error "It appears that our directory structure has changed and somebody failed to update this script.
raise an issue on Github and someone should fix it."
exit 1
fi
# Install system-level dependencies # Install system-level dependencies
bash $BASE/mitx/install-system-req.sh
output "Installing RVM, Ruby, and required gems" output "Installing RVM, Ruby, and required gems"
# If we're not installing RVM in the default location, then we'll do some # If we're not installing RVM in the default location, then we'll do some
...@@ -271,7 +304,22 @@ if [ "$HOME/.rvm" != $RUBY_DIR ]; then ...@@ -271,7 +304,22 @@ if [ "$HOME/.rvm" != $RUBY_DIR ]; then
fi fi
fi fi
curl -sL get.rvm.io | bash -s -- --version 1.15.7 # rvm has issues in debian family, this is taken from stack overflow
case `uname -s` in
Darwin)
curl -sL get.rvm.io | bash -s -- --version 1.15.7
;;
squeeze|wheezy|jessie|maya|lisa|olivia|nadia|natty|oneiric|precise|quantal|raring)
warning "Setting up rvm on linux. This is a known pain point. If the script fails here
refer to the following stack overflow question:
http://stackoverflow.com/questions/9056008/installed-ruby-1-9-3-with-rvm-but-command-line-doesnt-show-ruby-v/9056395#9056395"
sudo apt-get --purge remove ruby-rvm
sudo rm -rf /usr/share/ruby-rvm /etc/rvmrc /etc/profile.d/rvm.sh
curl -sL https://get.rvm.io | bash -s stable --ruby --autolibs=enable --autodotfiles
;;
esac
# Ensure we have RVM available as a shell function so that it can mess # Ensure we have RVM available as a shell function so that it can mess
# with the environment and set everything up properly. The RVM install # with the environment and set everything up properly. The RVM install
...@@ -294,8 +342,8 @@ case `uname -s` in ...@@ -294,8 +342,8 @@ case `uname -s` in
esac esac
# Let the repo override the version of Ruby to install # Let the repo override the version of Ruby to install
if [[ -r $BASE/mitx/.ruby-version ]]; then if [[ -r $BASE/edx-platform/.ruby-version ]]; then
RUBY_VER=`cat $BASE/mitx/.ruby-version` RUBY_VER=`cat $BASE/edx-platform/.ruby-version`
fi fi
# Current stable version of RVM (1.19.0) requires the following to build Ruby: # Current stable version of RVM (1.19.0) requires the following to build Ruby:
...@@ -311,14 +359,15 @@ fi ...@@ -311,14 +359,15 @@ fi
# any required libs are missing. # any required libs are missing.
LESS="-E" rvm install $RUBY_VER --with-readline LESS="-E" rvm install $RUBY_VER --with-readline
# Create the "mitx" gemset # Create the "edx" gemset
rvm use "$RUBY_VER@mitx" --create rvm use "$RUBY_VER@edx-platform" --create
rvm rubygems latest
output "Installing gem bundler" output "Installing gem bundler"
gem install bundler gem install bundler
output "Installing ruby packages" output "Installing ruby packages"
bundle install --gemfile $BASE/mitx/Gemfile bundle install --gemfile $BASE/edx-platform/Gemfile
# Install Python virtualenv # Install Python virtualenv
...@@ -338,20 +387,33 @@ export WORKON_HOME=$PYTHON_DIR ...@@ -338,20 +387,33 @@ export WORKON_HOME=$PYTHON_DIR
# Load in the mkvirtualenv function if needed # Load in the mkvirtualenv function if needed
if [[ `type -t mkvirtualenv` != "function" ]]; then if [[ `type -t mkvirtualenv` != "function" ]]; then
case `uname -s` in
Darwin)
source `which virtualenvwrapper.sh` source `which virtualenvwrapper.sh`
;;
squeeze|wheezy|jessie|maya|lisa|olivia|nadia|natty|oneiric|precise|quantal|raring)
if [[ -f "/etc/bash_completion.d/virtualenvwrapper" ]]; then
source /etc/bash_completion.d/virtualenvwrapper
else
error "Could not find virtualenvwrapper"
exit 1
fi
;;
esac
fi fi
# Create MITx virtualenv and link it to repo # Create edX virtualenv and link it to repo
# virtualenvwrapper automatically sources the activation script # virtualenvwrapper automatically sources the activation script
if [[ $systempkgs ]]; then if [[ $systempkgs ]]; then
mkvirtualenv -a "$BASE/mitx" --system-site-packages mitx || { mkvirtualenv -a "$HOME/.virtualenvs" --system-site-packages edx-platform || {
error "mkvirtualenv exited with a non-zero error" error "mkvirtualenv exited with a non-zero error"
return 1 return 1
} }
else else
# default behavior for virtualenv>1.7 is # default behavior for virtualenv>1.7 is
# --no-site-packages # --no-site-packages
mkvirtualenv -a "$BASE/mitx" mitx || { mkvirtualenv -a "$HOME/.virtualenvs" edx-platform || {
error "mkvirtualenv exited with a non-zero error" error "mkvirtualenv exited with a non-zero error"
return 1 return 1
} }
...@@ -380,10 +442,30 @@ if [[ -n $compile ]]; then ...@@ -380,10 +442,30 @@ if [[ -n $compile ]]; then
rm -rf numpy-${NUMPY_VER} scipy-${SCIPY_VER} rm -rf numpy-${NUMPY_VER} scipy-${SCIPY_VER}
fi fi
# building correct version of distribute from source
DISTRIBUTE_VER="0.6.28"
output "Building Distribute"
SITE_PACKAGES="$HOME/.virtualenvs/edx-platform/lib/python2.7/site-packages"
cd "$SITE_PACKAGES"
curl -O http://pypi.python.org/packages/source/d/distribute/distribute-${DISTRIBUTE_VER}.tar.gz
tar -xzvf distribute-${DISTRIBUTE_VER}.tar.gz
cd distribute-${DISTRIBUTE_VER}
python setup.py install
cd ..
rm distribute-${DISTRIBUTE_VER}.tar.gz
DISTRIBUTE_VERSION=`pip freeze | grep distribute`
if [[ "$DISTRIBUTE_VERSION" == "distribute==0.6.28" ]]; then
output "Distribute successfully installed"
else
error "Distribute failed to build correctly. This script requires a working version of Distribute 0.6.28 in your virtualenv's python installation"
exit 1
fi
case `uname -s` in case `uname -s` in
Darwin) Darwin)
# on mac os x get the latest distribute and pip # on mac os x get the latest distribute and pip
curl http://python-distribute.org/distribute_setup.py | python
pip install -U pip pip install -U pip
# need latest pytz before compiling numpy and scipy # need latest pytz before compiling numpy and scipy
pip install -U pytz pip install -U pytz
...@@ -395,18 +477,29 @@ case `uname -s` in ...@@ -395,18 +477,29 @@ case `uname -s` in
;; ;;
esac esac
output "Installing MITx pre-requirements" output "Installing edX pre-requirements"
pip install -r $BASE/mitx/pre-requirements.txt pip install -r $BASE/edx-platform/requirements/edx/pre.txt
output "Installing MITx requirements" output "Installing edX requirements"
# Need to be in the mitx dir to get the paths to local modules right # Install prereqs
cd $BASE/mitx cd $BASE/edx-platform
pip install -r requirements.txt rvm use $RUBY_VER
rake install_prereqs
# Final dependecy
output "Finishing Touches"
cd $BASE
pip install argcomplete
cd $BASE/edx-platform
bundle install
mkdir "$BASE/log" || true mkdir "$BASE/log" || true
mkdir "$BASE/db" || true mkdir "$BASE/db" || true
mkdir "$BASE/data" || true
rake django-admin[syncdb]
rake django-admin[migrate]
rake django-admin[update-templates]
# Configure Git # Configure Git
output "Fixing your git default settings" output "Fixing your git default settings"
......
...@@ -16,10 +16,11 @@ output() { ...@@ -16,10 +16,11 @@ output() {
### START ### START
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" SELF_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
BREW_FILE=$DIR/"brew-formulas.txt" REQUIREMENTS_DIR="$SELF_DIR/../requirements/system"
APT_REPOS_FILE=$DIR/"apt-repos.txt" BREW_FILE=$REQUIREMENTS_DIR/"mac_os_x/brew-formulas.txt"
APT_PKGS_FILE=$DIR/"apt-packages.txt" APT_REPOS_FILE=$REQUIREMENTS_DIR/"ubuntu/apt-repos.txt"
APT_PKGS_FILE=$REQUIREMENTS_DIR/"ubuntu/apt-packages.txt"
case `uname -s` in case `uname -s` in
[Ll]inux) [Ll]inux)
...@@ -30,8 +31,9 @@ case `uname -s` in ...@@ -30,8 +31,9 @@ case `uname -s` in
distro=`lsb_release -cs` distro=`lsb_release -cs`
case $distro in case $distro in
maya|lisa|natty|oneiric|precise|quantal) #Tries to install the same
output "Installing Ubuntu requirements" squeeze|wheezy|jessie|maya|lisa|olivia|nadia|natty|oneiric|precise|quantal|raring)
output "Installing Debian family requirements"
# DEBIAN_FRONTEND=noninteractive is required for silent mysql-server installation # DEBIAN_FRONTEND=noninteractive is required for silent mysql-server installation
export DEBIAN_FRONTEND=noninteractive export DEBIAN_FRONTEND=noninteractive
...@@ -39,7 +41,10 @@ case `uname -s` in ...@@ -39,7 +41,10 @@ case `uname -s` in
# add repositories # add repositories
cat $APT_REPOS_FILE | xargs -n 1 sudo add-apt-repository -y cat $APT_REPOS_FILE | xargs -n 1 sudo add-apt-repository -y
sudo apt-get -y update sudo apt-get -y update
sudo apt-get -y install gfortran
sudo apt-get -y install graphviz libgraphviz-dev graphviz-dev
sudo apt-get -y install libatlas-dev libblas-dev
sudo apt-get -y install ruby-rvm
# install packages listed in APT_PKGS_FILE # install packages listed in APT_PKGS_FILE
cat $APT_PKGS_FILE | xargs sudo apt-get -y install cat $APT_PKGS_FILE | xargs sudo apt-get -y install
;; ;;
...@@ -70,10 +75,13 @@ EO ...@@ -70,10 +75,13 @@ EO
output "Installing OSX requirements" output "Installing OSX requirements"
if [[ ! -r $BREW_FILE ]]; then if [[ ! -r $BREW_FILE ]]; then
error "$BREW_FILE does not exist, needed to install brew" error "$BREW_FILE does not exist, please include the brew formulas file in the requirements/system/mac_os_x directory"
exit 1 exit 1
fi fi
# for some reason openssl likes to be installed by itself first
brew install openssl
# brew errors if the package is already installed # brew errors if the package is already installed
for pkg in $(cat $BREW_FILE); do for pkg in $(cat $BREW_FILE); do
grep $pkg <(brew list) &>/dev/null || { grep $pkg <(brew list) &>/dev/null || {
......
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