Merge branch 'master' into will/diff-cover-integration
This commit is contained in:
@@ -1 +1 @@
|
||||
mitx
|
||||
edx-platform
|
||||
|
||||
2
AUTHORS
2
AUTHORS
@@ -72,3 +72,5 @@ Giulio Gratta <giulio@giuliogratta.com>
|
||||
David Baumgold <david@davidbaumgold.com>
|
||||
Jason Bau <jbau@stanford.edu>
|
||||
Frances Botsford <frances@edx.org>
|
||||
Jonah Stanley <Jonah_Stanley@brown.edu>
|
||||
Slater Victoroff <slater.r.victoroff@gmail.com>
|
||||
|
||||
46
README.md
46
README.md
@@ -1,19 +1,18 @@
|
||||
This is edX, a platform for online course delivery. The project is primarily
|
||||
written in [Python](http://python.org/), using the
|
||||
[Django](https://www.djangoproject.com/) framework. We also use some
|
||||
[Ruby](http://www.ruby-lang.org/) and some [NodeJS](http://nodejs.org/).
|
||||
This is the main edX platform which consists of LMS and Studio.
|
||||
|
||||
See [code.edx.org](http://code.edx.org/) for other parts of the edX code base.
|
||||
|
||||
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
|
||||
of this up for you. If you're in a hurry, run that script. Otherwise, I suggest
|
||||
that you understand what the script is doing, and why, by reading this document.
|
||||
There is a `scripts/create-dev-env.sh` that will attempt to set up a development
|
||||
environment.
|
||||
|
||||
If you want to better understand what the script is doing, keep reading.
|
||||
|
||||
Directory Hierarchy
|
||||
-------------------
|
||||
|
||||
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
|
||||
[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
|
||||
Once you've got your languages and virtual environments set up, install
|
||||
the libraries like so:
|
||||
|
||||
$ pip install -r requirements/edx/pre.txt
|
||||
$ pip install -r requirements/edx/base.txt
|
||||
$ pip install -r requirements/edx/post.txt
|
||||
$ bundle install
|
||||
@@ -111,7 +111,7 @@ CMS templates. Fortunately, `rake` will do all of this for you! Just run:
|
||||
|
||||
$ rake django-admin[syncdb]
|
||||
$ rake django-admin[migrate]
|
||||
$ rake django-admin[update_templates]
|
||||
$ rake cms:update_templates
|
||||
|
||||
If you are running these commands using the [`zsh`](http://www.zsh.org/) shell,
|
||||
zsh will assume that you are doing
|
||||
@@ -144,10 +144,28 @@ in the `data` directory, instead of in Mongo. To run this older version, run:
|
||||
|
||||
$ rake lms
|
||||
|
||||
Further Documentation
|
||||
=====================
|
||||
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.
|
||||
License
|
||||
-------
|
||||
|
||||
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.
|
||||
|
||||
@@ -5,8 +5,6 @@ from lettuce import world, step
|
||||
from nose.tools import assert_true
|
||||
from nose.tools import assert_equal
|
||||
|
||||
from xmodule.modulestore.django import _MODULESTORES, modulestore
|
||||
from xmodule.templates import update_templates
|
||||
from auth.authz import get_user_by_email
|
||||
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
@@ -50,31 +48,31 @@ def i_press_the_category_delete_icon(step, category):
|
||||
|
||||
@step('I have opened a new course in Studio$')
|
||||
def i_have_opened_a_new_course(step):
|
||||
open_new_course()
|
||||
|
||||
|
||||
####### HELPER FUNCTIONS ##############
|
||||
def open_new_course():
|
||||
world.clear_courses()
|
||||
log_into_studio()
|
||||
create_a_course()
|
||||
|
||||
|
||||
####### HELPER FUNCTIONS ##############
|
||||
def create_studio_user(
|
||||
uname='robot',
|
||||
email='robot+studio@edx.org',
|
||||
password='test',
|
||||
is_staff=False):
|
||||
studio_user = world.UserFactory.build(
|
||||
studio_user = world.UserFactory(
|
||||
username=uname,
|
||||
email=email,
|
||||
password=password,
|
||||
is_staff=is_staff)
|
||||
studio_user.set_password(password)
|
||||
studio_user.save()
|
||||
|
||||
registration = world.RegistrationFactory(user=studio_user)
|
||||
registration.register(studio_user)
|
||||
registration.activate()
|
||||
|
||||
user_profile = world.UserProfileFactory(user=studio_user)
|
||||
|
||||
|
||||
def fill_in_course_info(
|
||||
name='Robot Super Course',
|
||||
@@ -153,4 +151,13 @@ def set_date_and_time(date_css, desired_date, time_css, desired_time):
|
||||
world.css_fill(time_css, desired_time)
|
||||
e = world.css_find(time_css).first
|
||||
e._element.send_keys(Keys.TAB)
|
||||
time.sleep(float(1))
|
||||
time.sleep(float(1))
|
||||
|
||||
|
||||
@step('I have created a Video component$')
|
||||
def i_created_a_video_component(step):
|
||||
world.create_component_instance(
|
||||
step, '.large-video-icon',
|
||||
'i4x://edx/templates/video/default',
|
||||
'.xmodule_VideoModule'
|
||||
)
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# disable missing docstring
|
||||
#pylint: disable=C0111
|
||||
|
||||
from lettuce import world
|
||||
from nose.tools import assert_equal
|
||||
from terrain.steps import reload_the_page
|
||||
|
||||
|
||||
@world.absorb
|
||||
def create_component_instance(step, component_button_css, instance_id, expected_css):
|
||||
click_new_component_button(step, component_button_css)
|
||||
click_component_from_menu(instance_id, expected_css)
|
||||
|
||||
|
||||
@world.absorb
|
||||
def click_new_component_button(step, component_button_css):
|
||||
step.given('I have opened a new course section in Studio')
|
||||
step.given('I have added a new subsection')
|
||||
step.given('I expand the first section')
|
||||
world.css_click('a.new-unit-item')
|
||||
world.css_click(component_button_css)
|
||||
|
||||
|
||||
@world.absorb
|
||||
def click_component_from_menu(instance_id, expected_css):
|
||||
elem_css = "a[data-location='%s']" % instance_id
|
||||
assert_equal(1, len(world.css_find(elem_css)))
|
||||
world.css_click(elem_css)
|
||||
assert_equal(1, len(world.css_find(expected_css)))
|
||||
|
||||
@world.absorb
|
||||
def edit_component_and_select_settings():
|
||||
world.css_click('a.edit-button')
|
||||
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
|
||||
@@ -47,12 +47,6 @@ def i_see_the_course_in_my_courses(step):
|
||||
assert world.css_has_text(course_css, 'Robot Super Course')
|
||||
|
||||
|
||||
@step('the course is loaded$')
|
||||
def course_is_loaded(step):
|
||||
class_css = 'a.class-name'
|
||||
assert world.css_has_text(course_css, 'Robot Super Cousre')
|
||||
|
||||
|
||||
@step('I am on the "([^"]*)" tab$')
|
||||
def i_am_on_tab(step, tab_name):
|
||||
header_css = 'div.inner-wrapper h1'
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
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
|
||||
23
cms/djangoapps/contentstore/features/discussion-editor.py
Normal file
23
cms/djangoapps/contentstore/features/discussion-editor.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# 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]
|
||||
])
|
||||
13
cms/djangoapps/contentstore/features/html-editor.feature
Normal file
13
cms/djangoapps/contentstore/features/html-editor.feature
Normal file
@@ -0,0 +1,13 @@
|
||||
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
|
||||
17
cms/djangoapps/contentstore/features/html-editor.py
Normal file
17
cms/djangoapps/contentstore/features/html-editor.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# 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]])
|
||||
67
cms/djangoapps/contentstore/features/problem-editor.feature
Normal file
67
cms/djangoapps/contentstore/features/problem-editor.feature
Normal file
@@ -0,0 +1,67 @@
|
||||
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
|
||||
187
cms/djangoapps/contentstore/features/problem-editor.py
Normal file
187
cms/djangoapps/contentstore/features/problem-editor.py
Normal file
@@ -0,0 +1,187 @@
|
||||
# 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)
|
||||
@@ -112,7 +112,7 @@ def all_sections_are_expanded(step):
|
||||
|
||||
|
||||
@step(u'all sections are collapsed$')
|
||||
def all_sections_are_expanded(step):
|
||||
def all_sections_are_collapsed(step):
|
||||
subsection_locator = 'div.subsection-list'
|
||||
subsections = world.css_find(subsection_locator)
|
||||
for s in subsections:
|
||||
|
||||
@@ -10,9 +10,7 @@ from nose.tools import assert_equal
|
||||
|
||||
@step('I have opened a new course section in Studio$')
|
||||
def i_have_opened_a_new_course_section(step):
|
||||
world.clear_courses()
|
||||
log_into_studio()
|
||||
create_a_course()
|
||||
open_new_course()
|
||||
add_section()
|
||||
|
||||
|
||||
|
||||
13
cms/djangoapps/contentstore/features/video-editor.feature
Normal file
13
cms/djangoapps/contentstore/features/video-editor.feature
Normal file
@@ -0,0 +1,13 @@
|
||||
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
|
||||
9
cms/djangoapps/contentstore/features/video-editor.py
Normal file
9
cms/djangoapps/contentstore/features/video-editor.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# 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]])
|
||||
6
cms/djangoapps/contentstore/features/video.feature
Normal file
6
cms/djangoapps/contentstore/features/video.feature
Normal file
@@ -0,0 +1,6 @@
|
||||
Feature: Video Component
|
||||
As a course author, I want to be able to view my created videos in Studio.
|
||||
|
||||
Scenario: Autoplay is disabled in Studio
|
||||
Given I have created a Video component
|
||||
Then when I view the video it does not have autoplay enabled
|
||||
11
cms/djangoapps/contentstore/features/video.py
Normal file
11
cms/djangoapps/contentstore/features/video.py
Normal file
@@ -0,0 +1,11 @@
|
||||
#pylint: disable=C0111
|
||||
|
||||
from lettuce import world, step
|
||||
|
||||
############### ACTIONS ####################
|
||||
|
||||
|
||||
@step('when I view the video it does not have autoplay enabled')
|
||||
def does_not_autoplay(step):
|
||||
assert world.css_find('.video')[0]['data-autoplay'] == 'False'
|
||||
assert world.css_find('.video_control')[0].has_class('play')
|
||||
@@ -34,6 +34,8 @@ from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.seq_module import SequenceDescriptor
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
|
||||
from contentstore.views.component import ADVANCED_COMPONENT_TYPES
|
||||
|
||||
from django_comment_common.utils import are_permissions_roles_seeded
|
||||
|
||||
TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
|
||||
@@ -75,6 +77,31 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
self.client = Client()
|
||||
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):
|
||||
import_from_xml(modulestore('direct'), 'common/test/data/', [test_course_name])
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import mock
|
||||
import collections
|
||||
import copy
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
|
||||
@@ -11,11 +12,28 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
class LMSLinksTestCase(TestCase):
|
||||
""" Tests for LMS links. """
|
||||
def about_page_test(self):
|
||||
""" Get URL for about page. """
|
||||
""" Get URL for about page, no marketing site """
|
||||
# default for ENABLE_MKTG_SITE is False.
|
||||
self.assertEquals(self.get_about_page_link(), "//localhost:8000/courses/mitX/101/test/about")
|
||||
|
||||
@override_settings(MKTG_URLS={'ROOT': 'dummy-root'})
|
||||
def about_page_marketing_site_test(self):
|
||||
""" Get URL for about page, marketing root present. """
|
||||
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': True}):
|
||||
self.assertEquals(self.get_about_page_link(), "//dummy-root/courses/mitX/101/test/about")
|
||||
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': False}):
|
||||
self.assertEquals(self.get_about_page_link(), "//localhost:8000/courses/mitX/101/test/about")
|
||||
|
||||
@override_settings(LMS_BASE=None)
|
||||
def about_page_no_lms_base_test(self):
|
||||
""" No LMS_BASE, nor is ENABLE_MKTG_SITE True """
|
||||
self.assertEquals(self.get_about_page_link(), None)
|
||||
|
||||
def get_about_page_link(self):
|
||||
""" create mock course and return the about page link """
|
||||
location = 'i4x', 'mitX', '101', 'course', 'test'
|
||||
utils.get_course_id = mock.Mock(return_value="mitX/101/test")
|
||||
link = utils.get_lms_link_for_about_page(location)
|
||||
self.assertEquals(link, "//localhost:8000/courses/mitX/101/test/about")
|
||||
return utils.get_lms_link_for_about_page(location)
|
||||
|
||||
def lms_link_test(self):
|
||||
""" Tests get_lms_link_for_item. """
|
||||
|
||||
@@ -111,6 +111,18 @@ class AuthTestCase(ContentStoreTestCase):
|
||||
# Now login should work
|
||||
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):
|
||||
"""Make sure pages that do require login work."""
|
||||
auth_pages = (
|
||||
|
||||
@@ -107,9 +107,18 @@ def get_lms_link_for_about_page(location):
|
||||
"""
|
||||
Returns the url to the course about page from the location tuple.
|
||||
"""
|
||||
if settings.LMS_BASE is not None:
|
||||
lms_link = "//{lms_base}/courses/{course_id}/about".format(
|
||||
lms_base=settings.LMS_BASE,
|
||||
if settings.MITX_FEATURES.get('ENABLE_MKTG_SITE', False):
|
||||
# Root will be "www.edx.org". The complete URL will still not be exactly correct,
|
||||
# but redirects exist from www.edx.org to get to the drupal course about page URL.
|
||||
about_base = settings.MKTG_URLS.get('ROOT')
|
||||
elif settings.LMS_BASE is not None:
|
||||
about_base = settings.LMS_BASE
|
||||
else:
|
||||
about_base = None
|
||||
|
||||
if about_base is not None:
|
||||
lms_link = "//{about_base_url}/courses/{course_id}/about".format(
|
||||
about_base_url=about_base,
|
||||
course_id=get_course_id(location)
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -42,7 +42,7 @@ COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video']
|
||||
|
||||
OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"]
|
||||
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_POLICY_KEY = 'advanced_modules'
|
||||
|
||||
@@ -149,8 +149,7 @@ def edit_unit(request, location):
|
||||
component_templates[category].append((
|
||||
template.display_name_with_default,
|
||||
template.location.url(),
|
||||
hasattr(template, 'markdown') and template.markdown is not None,
|
||||
template.cms.empty,
|
||||
hasattr(template, 'markdown') and template.markdown is not None
|
||||
))
|
||||
|
||||
components = [
|
||||
|
||||
@@ -40,7 +40,10 @@ MITX_FEATURES = {
|
||||
'SEGMENT_IO': True,
|
||||
|
||||
# Enable URL that shows information about the status of various services
|
||||
'ENABLE_SERVICE_STATUS': False
|
||||
'ENABLE_SERVICE_STATUS': False,
|
||||
|
||||
# Don't autoplay videos for course authors
|
||||
'AUTOPLAY_VIDEOS': False
|
||||
}
|
||||
ENABLE_JASMINE = False
|
||||
|
||||
@@ -223,7 +226,8 @@ PIPELINE_JS = {
|
||||
rooted_glob(PROJECT_ROOT / 'static/', 'coffee/src/**/*.js')
|
||||
) + ['js/hesitate.js', 'js/base.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',
|
||||
'test_order': 0
|
||||
},
|
||||
@@ -319,6 +323,7 @@ INSTALLED_APPS = (
|
||||
'track',
|
||||
|
||||
# For asset pipelining
|
||||
'mitxmako',
|
||||
'pipeline',
|
||||
'staticfiles',
|
||||
'static_replace',
|
||||
|
||||
1
cms/static/coffee/fixtures/metadata-editor.underscore
Symbolic link
1
cms/static/coffee/fixtures/metadata-editor.underscore
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../templates/js/metadata-editor.underscore
|
||||
1
cms/static/coffee/fixtures/metadata-number-entry.underscore
Symbolic link
1
cms/static/coffee/fixtures/metadata-number-entry.underscore
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../templates/js/metadata-number-entry.underscore
|
||||
1
cms/static/coffee/fixtures/metadata-option-entry.underscore
Symbolic link
1
cms/static/coffee/fixtures/metadata-option-entry.underscore
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../templates/js/metadata-option-entry.underscore
|
||||
1
cms/static/coffee/fixtures/metadata-string-entry.underscore
Symbolic link
1
cms/static/coffee/fixtures/metadata-string-entry.underscore
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../templates/js/metadata-string-entry.underscore
|
||||
58
cms/static/coffee/spec/models/metadata_spec.coffee
Normal file
58
cms/static/coffee/spec/models/metadata_spec.coffee
Normal file
@@ -0,0 +1,58 @@
|
||||
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)
|
||||
|
||||
300
cms/static/coffee/spec/views/metadata_edit_spec.coffee
Normal file
300
cms/static/coffee/spec/views/metadata_edit_spec.coffee
Normal file
@@ -0,0 +1,300 @@
|
||||
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", ->
|
||||
expect(XModule.loadModule).toHaveBeenCalled()
|
||||
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'})
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
class CMS.Views.ModuleEdit extends Backbone.View
|
||||
tagName: 'li'
|
||||
className: 'component'
|
||||
editorMode: 'editor-mode'
|
||||
|
||||
events:
|
||||
"click .component-editor .cancel-button": 'clickCancelButton'
|
||||
"click .component-editor .save-button": 'clickSaveButton'
|
||||
"click .component-actions .edit-button": 'clickEditButton'
|
||||
"click .component-actions .delete-button": 'onDelete'
|
||||
"click .mode a": 'clickModeButton'
|
||||
|
||||
initialize: ->
|
||||
@onDelete = @options.onDelete
|
||||
@@ -20,29 +22,30 @@ class CMS.Views.ModuleEdit extends Backbone.View
|
||||
loadEdit: ->
|
||||
if not @module
|
||||
@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')
|
||||
metadataData = metadataEditor.data('metadata')
|
||||
models = [];
|
||||
for key of metadataData
|
||||
models.push(metadataData[key])
|
||||
@metadataEditor = new CMS.Views.Metadata.Editor({
|
||||
el: metadataEditor,
|
||||
collection: new CMS.Models.MetadataCollection(models)
|
||||
})
|
||||
|
||||
metadata: ->
|
||||
# cdodge: package up metadata which is separated into a number of input fields
|
||||
# there's probably a better way to do this, but at least this lets me continue to move onwards
|
||||
_metadata = {}
|
||||
# Need to update set "active" class on data editor if there is one.
|
||||
# If we are only showing settings, hide the data editor controls and update settings accordingly.
|
||||
if @hasDataEditor()
|
||||
@selectMode(@editorMode)
|
||||
else
|
||||
@hideDataEditor()
|
||||
|
||||
$metadata = @$component_editor().find('.metadata_edit')
|
||||
|
||||
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
|
||||
_metadata[$(el).data("metadata-name")] = el.value for el in $('[data-metadata-name]', $metadata)
|
||||
|
||||
return _metadata
|
||||
title = interpolate(gettext('<em>Editing:</em> %s'),
|
||||
[@metadataEditor.getDisplayName()])
|
||||
@$el.find('.component-name').html(title)
|
||||
|
||||
changedMetadata: ->
|
||||
currentMetadata = @metadata()
|
||||
changedMetadata = {}
|
||||
for key of currentMetadata
|
||||
if currentMetadata[key] != @originalMetadata[key]
|
||||
changedMetadata[key] = currentMetadata[key]
|
||||
return changedMetadata
|
||||
return @metadataEditor.getModifiedMetadataValues()
|
||||
|
||||
cloneTemplate: (parent, template) ->
|
||||
$.post("/clone_item", {
|
||||
@@ -77,7 +80,7 @@ class CMS.Views.ModuleEdit extends Backbone.View
|
||||
@render()
|
||||
@$el.removeClass('editing')
|
||||
).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) ->
|
||||
@@ -96,3 +99,38 @@ class CMS.Views.ModuleEdit extends Backbone.View
|
||||
$modalCover.show().addClass('is-fixed')
|
||||
@$component_editor().slideDown(150)
|
||||
@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')
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 342 B After Width: | Height: | Size: 633 B |
113
cms/static/js/models/metadata_model.js
Normal file
113
cms/static/js/models/metadata_model.js
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* 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";
|
||||
312
cms/static/js/views/metadata_editor_view.js
Normal file
312
cms/static/js/views/metadata_editor_view.js
Normal file
@@ -0,0 +1,312 @@
|
||||
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 {
|
||||
line-height: 26px;
|
||||
color: $white;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
|
||||
&:after {
|
||||
content: '▾';
|
||||
|
||||
@@ -149,11 +149,11 @@ abbr[title] {
|
||||
margin-left: 20px;
|
||||
}
|
||||
li {
|
||||
opacity: .8;
|
||||
opacity: 0.8;
|
||||
|
||||
&:ui-state-active {
|
||||
background-color: rgba(255, 255, 255, .3);
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
font-weight: 400;
|
||||
}
|
||||
a:focus {
|
||||
|
||||
@@ -95,12 +95,12 @@
|
||||
// bounce in
|
||||
@mixin bounceIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
@include transform(scale(0.3));
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
@include transform(scale(1.05));
|
||||
}
|
||||
|
||||
@@ -128,12 +128,12 @@
|
||||
// bounce in
|
||||
@mixin bounceOut {
|
||||
0% {
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
@include transform(scale(0.3));
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
@include transform(scale(1.05));
|
||||
}
|
||||
|
||||
@@ -146,12 +146,12 @@
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
@include transform(scale(1.05));
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
@include transform(scale(0.3));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +124,6 @@ code {
|
||||
|
||||
.CodeMirror {
|
||||
font-size: 13px;
|
||||
border: 1px solid $darkGrey;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
|
||||
@@ -243,7 +243,7 @@
|
||||
left: -7px;
|
||||
top: 47px;
|
||||
width: 140px;
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -558,7 +558,7 @@ body.signin .nav-not-signedin-signup {
|
||||
|
||||
.wrapper-nav-sub {
|
||||
@include transition (opacity 1.0s ease-in-out 0s);
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
pointer-events: none;
|
||||
|
||||
&.is-shown {
|
||||
|
||||
@@ -627,7 +627,7 @@
|
||||
pointer-events: none;
|
||||
|
||||
.prompt {
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -254,7 +254,7 @@ body.course.checklists {
|
||||
.task-support {
|
||||
@extend .t-copy-sub2;
|
||||
@include transition(opacity .15s .25s ease-in-out);
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
@@ -267,7 +267,7 @@ body.course.checklists {
|
||||
float: right;
|
||||
width: flex-grid(2,9);
|
||||
margin: ($baseline/2) 0 0 flex-gutter();
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
pointer-events: none;
|
||||
text-align: right;
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ body.dashboard {
|
||||
top: 15px;
|
||||
right: $baseline;
|
||||
padding: ($baseline/4) ($baseline/2);
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
pointer-events: none;
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -162,7 +162,7 @@ body.index {
|
||||
position: absolute;
|
||||
bottom: -30px;
|
||||
right: ($baseline/2);
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
|
||||
[class^="icon-"] {
|
||||
@include font-size(18);
|
||||
|
||||
@@ -21,7 +21,7 @@ body.course.settings {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.message-status {
|
||||
.message-status {
|
||||
display: none;
|
||||
@include border-top-radius(2px);
|
||||
@include box-sizing(border-box);
|
||||
@@ -386,6 +386,11 @@ body.course.settings {
|
||||
#course-overview {
|
||||
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
|
||||
@@ -698,7 +703,7 @@ body.course.settings {
|
||||
|
||||
.tip {
|
||||
@include transition (opacity 0.5s ease-in-out 0s);
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
position: absolute;
|
||||
bottom: ($baseline*1.25);
|
||||
}
|
||||
@@ -713,7 +718,7 @@ body.course.settings {
|
||||
input.error {
|
||||
|
||||
& + .tip {
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,38 +41,23 @@ body.course.static-pages {
|
||||
@include edit-box;
|
||||
@include box-shadow(none);
|
||||
display: none;
|
||||
padding: 20px;
|
||||
padding: 0;
|
||||
border-radius: 2px 2px 0 0;
|
||||
|
||||
.metadata_edit {
|
||||
margin-bottom: 20px;
|
||||
font-size: 13px;
|
||||
//Overrides general edit-box mixin
|
||||
.row {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 10px;
|
||||
// This duplicates the styling from Unit page editing
|
||||
.module-actions {
|
||||
@include box-shadow(inset 0 1px 1px $shadow);
|
||||
padding: 0px 0 10px 10px;
|
||||
background-color: $gray-l6;
|
||||
|
||||
.save-button {
|
||||
margin: ($baseline/2) 8px 0 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 10px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin-bottom: 8px;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
margin-top: 10px;
|
||||
margin: 15px 8px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -215,3 +200,4 @@ body.course.static-pages {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ body.course.unit {
|
||||
border: none;
|
||||
|
||||
.rendered-component {
|
||||
padding: 0 20px;
|
||||
padding: 0 $baseline;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ body.course.unit {
|
||||
.unit-body {
|
||||
|
||||
.unit-name-input {
|
||||
padding: 20px 40px;
|
||||
padding: $baseline 2*$baseline;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
@@ -73,15 +73,15 @@ body.course.unit {
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
// ====================
|
||||
|
||||
// Component List Meta
|
||||
.components {
|
||||
|
||||
> li {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
margin: 20px 40px;
|
||||
|
||||
|
||||
margin: $baseline 2*$baseline;
|
||||
|
||||
.title {
|
||||
margin: 0 0 15px 0;
|
||||
@@ -91,23 +91,26 @@ body.course.unit {
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// New Components
|
||||
&.new-component-item {
|
||||
margin: 20px 0px;
|
||||
margin: $baseline 0px;
|
||||
border-top: 1px solid $mediumGrey;
|
||||
box-shadow: 0 2px 1px rgba(182, 182, 182, 0.75) inset;
|
||||
background-color: $lightGrey;
|
||||
margin-bottom: 0px;
|
||||
padding-bottom: 20px;
|
||||
padding-bottom: $baseline;
|
||||
|
||||
.new-component-button {
|
||||
display: block;
|
||||
padding: 20px;
|
||||
padding: $baseline;
|
||||
text-align: center;
|
||||
color: #edf1f5;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin: 20px 0px;
|
||||
margin: $baseline 0px;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
@@ -132,7 +135,7 @@ body.course.unit {
|
||||
height: 100px;
|
||||
color: #fff;
|
||||
margin-right: 15px;
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: $baseline;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
line-height: 14px;
|
||||
@@ -144,7 +147,7 @@ body.course.unit {
|
||||
bottom: 5px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
padding: $baseline/2;
|
||||
@include box-sizing(border-box);
|
||||
color: #fff;
|
||||
}
|
||||
@@ -153,7 +156,7 @@ body.course.unit {
|
||||
|
||||
.new-component-templates {
|
||||
display: none;
|
||||
margin: 20px 40px 20px 40px;
|
||||
margin: $baseline 2*$baseline;
|
||||
border-radius: 3px;
|
||||
border: 1px solid $mediumGrey;
|
||||
background-color: #fff;
|
||||
@@ -161,7 +164,7 @@ body.course.unit {
|
||||
@include clearfix;
|
||||
|
||||
.cancel-button {
|
||||
margin: 20px 0px 10px 10px;
|
||||
margin: $baseline 0px $baseline/2 $baseline/2;
|
||||
@include white-button;
|
||||
}
|
||||
|
||||
@@ -171,9 +174,9 @@ body.course.unit {
|
||||
|
||||
// specific menu types
|
||||
&.new-component-problem {
|
||||
padding-bottom:10px;
|
||||
padding-bottom: $baseline/2;
|
||||
|
||||
.ss-icon, .editor-indicator {
|
||||
[class^="icon-"], .editor-indicator {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
li:first-child {
|
||||
margin-left: 20px;
|
||||
margin-left: $baseline;
|
||||
}
|
||||
|
||||
li {
|
||||
@@ -219,21 +222,21 @@ body.course.unit {
|
||||
@include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
|
||||
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);
|
||||
opacity:.8;
|
||||
opacity: 0.8;
|
||||
|
||||
&:hover {
|
||||
opacity:1;
|
||||
opacity: 0.9;
|
||||
background-color: tint($lightBluishGrey, 20%);
|
||||
}
|
||||
|
||||
&.ui-state-active {
|
||||
border: 0px;
|
||||
@include active;
|
||||
opacity:1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
a{
|
||||
a {
|
||||
display: block;
|
||||
padding: 15px 25px;
|
||||
font-size: 15px;
|
||||
@@ -280,14 +283,14 @@ body.course.unit {
|
||||
a {
|
||||
@include clearfix();
|
||||
display: block;
|
||||
padding: 7px 20px;
|
||||
padding: 7px $baseline;
|
||||
border-bottom: none;
|
||||
font-weight: 500;
|
||||
|
||||
.name {
|
||||
float: left;
|
||||
|
||||
.ss-icon {
|
||||
[class^="icon-"] {
|
||||
@include transition(opacity .15s);
|
||||
display: inline-block;
|
||||
top: 1px;
|
||||
@@ -308,14 +311,14 @@ body.course.unit {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.ss-icon, .editor-indicator {
|
||||
[class^="icon-"], .editor-indicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
|
||||
.ss-icon {
|
||||
[class^="icon-"] {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
@@ -355,6 +358,9 @@ body.course.unit {
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// Component Drag and Drop, Non-Edit Module Rendering, Styling
|
||||
.component {
|
||||
border: 1px solid $lightBluishGrey2;
|
||||
border-radius: 3px;
|
||||
@@ -401,7 +407,7 @@ body.course.unit {
|
||||
}
|
||||
|
||||
.xmodule_display {
|
||||
padding: 40px 20px 20px;
|
||||
padding: 2*$baseline $baseline $baseline;
|
||||
overflow-x: auto;
|
||||
|
||||
h1 {
|
||||
@@ -409,36 +415,24 @@ body.course.unit {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
// ====================
|
||||
|
||||
// Component Editing
|
||||
.wrapper-component-editor {
|
||||
z-index: 9999;
|
||||
position: relative;
|
||||
background: $lightBluishGrey2;
|
||||
background: $white;
|
||||
}
|
||||
|
||||
.component-editor {
|
||||
@include edit-box;
|
||||
@include box-shadow(none);
|
||||
display: none;
|
||||
padding: 20px;
|
||||
padding: 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 {
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: $baseline/2;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
@@ -449,214 +443,286 @@ body.course.unit {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
margin-top: 10px;
|
||||
margin: 15px 8px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.unit-settings {
|
||||
.window-contents {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.unit-actions {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.published-alert {
|
||||
display: none;
|
||||
padding: 10px;
|
||||
border: 1px solid #edbd3c;
|
||||
border-radius: 3px;
|
||||
background: #fbf6e1;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
|
||||
div {
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
margin-right: 7px;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 12px;
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-button, .view-button {
|
||||
@include white-button;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.publish-button {
|
||||
@include orange-button;
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.delete-draft {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.delete-button,
|
||||
.preview-button,
|
||||
.publish-button,
|
||||
.view-button {
|
||||
font-size: 11px;
|
||||
margin-top: 10px;
|
||||
padding: 6px 15px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.unit-history {
|
||||
&.collapsed {
|
||||
h4 {
|
||||
border-bottom: none;
|
||||
border-radius: 3px;
|
||||
.row {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.window-contents {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
// 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;
|
||||
|
||||
ol {
|
||||
border: 1px solid #ced2db;
|
||||
|
||||
li {
|
||||
display: block;
|
||||
padding: 6px 8px 8px 10px;
|
||||
background: #edf1f5;
|
||||
font-size: 12px;
|
||||
|
||||
&:hover {
|
||||
background: #fffcf1;
|
||||
|
||||
.item-actions {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&.checked {
|
||||
background: #d1dae3;
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
margin-right: 7px;
|
||||
.save-button {
|
||||
margin: ($baseline/2) 8px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.unit-location {
|
||||
.url {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
@include box-shadow(none);
|
||||
}
|
||||
|
||||
.draft-tag,
|
||||
.hidden-tag,
|
||||
.private-tag,
|
||||
.has-new-draft-tag {
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.window-contents > ol {
|
||||
@include tree-view;
|
||||
|
||||
.section-item {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
font-size: 11px;
|
||||
padding: 2px 8px 4px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@include box-sizing(border-box);
|
||||
}
|
||||
|
||||
ol {
|
||||
.section-item {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.new-unit-item {
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
ol ol {
|
||||
.section-item {
|
||||
padding-left: 34px;
|
||||
}
|
||||
|
||||
.new-unit-item {
|
||||
margin: 0 0 10px 41px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.edit-state-draft {
|
||||
.visibility,
|
||||
|
||||
.edit-draft-message,
|
||||
.view-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.published-alert {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-state-public {
|
||||
.delete-draft,
|
||||
.component-actions,
|
||||
.new-component-item,
|
||||
.editing-draft-alert,
|
||||
.publish-draft-message,
|
||||
.preview-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.published-alert {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-state-private {
|
||||
.delete-draft,
|
||||
.publish-draft,
|
||||
.editing-draft-alert,
|
||||
.create-draft,
|
||||
.view-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// editing units from courseware
|
||||
// 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%;
|
||||
|
||||
&.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 {
|
||||
@@ -678,3 +744,224 @@ body.unit {
|
||||
}
|
||||
}
|
||||
}
|
||||
// ====================
|
||||
|
||||
// Unit Page Sidebar
|
||||
.unit-settings {
|
||||
.window-contents {
|
||||
padding: $baseline/2 $baseline;
|
||||
}
|
||||
|
||||
.unit-actions {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.published-alert {
|
||||
display: none;
|
||||
padding: $baseline/2;
|
||||
border: 1px solid #edbd3c;
|
||||
border-radius: 3px;
|
||||
background: #fbf6e1;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
|
||||
div {
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
margin-right: 7px;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 12px;
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-button, .view-button {
|
||||
@include white-button;
|
||||
margin-bottom: $baseline/2;
|
||||
}
|
||||
|
||||
.publish-button {
|
||||
@include orange-button;
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.delete-draft {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.delete-button,
|
||||
.preview-button,
|
||||
.publish-button,
|
||||
.view-button {
|
||||
font-size: 11px;
|
||||
margin-top: $baseline/2;
|
||||
padding: 6px 15px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.unit-history {
|
||||
&.collapsed {
|
||||
h4 {
|
||||
border-bottom: none;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.window-contents {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
border: 1px solid #ced2db;
|
||||
|
||||
li {
|
||||
display: block;
|
||||
padding: 6px 8px 8px $baseline/2;
|
||||
background: #edf1f5;
|
||||
font-size: 12px;
|
||||
|
||||
&:hover {
|
||||
background: #fffcf1;
|
||||
|
||||
.item-actions {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&.checked {
|
||||
background: #d1dae3;
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
margin-right: 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.unit-location {
|
||||
.url {
|
||||
@include box-shadow(none);
|
||||
width: 100%;
|
||||
margin-bottom: $baseline/2;
|
||||
}
|
||||
|
||||
.draft-tag,
|
||||
.hidden-tag,
|
||||
.private-tag,
|
||||
.has-new-draft-tag {
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.window-contents > ol {
|
||||
@include tree-view;
|
||||
|
||||
.section-item {
|
||||
@include box-sizing(border-box);
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
font-size: 11px;
|
||||
padding: 2px 8px 4px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
ol {
|
||||
.section-item {
|
||||
padding-left: $baseline;
|
||||
}
|
||||
|
||||
.new-unit-item {
|
||||
margin-left: $baseline;
|
||||
}
|
||||
}
|
||||
|
||||
ol ol {
|
||||
.section-item {
|
||||
padding-left: 34px;
|
||||
}
|
||||
|
||||
.new-unit-item {
|
||||
margin: 0 0 $baseline 41px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.edit-state-draft {
|
||||
.visibility,
|
||||
|
||||
.edit-draft-message,
|
||||
.view-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.published-alert {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-state-public {
|
||||
.delete-draft,
|
||||
.component-actions,
|
||||
.new-component-item,
|
||||
.editing-draft-alert,
|
||||
.publish-draft-message,
|
||||
.preview-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.published-alert {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-state-private {
|
||||
.delete-draft,
|
||||
.publish-draft,
|
||||
.editing-draft-alert,
|
||||
.create-draft,
|
||||
.view-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
// ====================
|
||||
|
||||
// Latex Compiler
|
||||
.launch-latex-compiler {
|
||||
background-color: $white;
|
||||
padding: $baseline/2 0 $baseline/2 $baseline;
|
||||
border-bottom: 1px solid $gray-l2;
|
||||
opacity: 0.8;
|
||||
|
||||
|
||||
&:hover {
|
||||
@include transition(opacity 0.25s ease-in-out);
|
||||
opacity: 1.0s;
|
||||
}
|
||||
}
|
||||
|
||||
// hides latex compiler button if settings mode is-active
|
||||
div.wrapper-comp-editor.is-inactive ~ div.launch-latex-compiler{
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -212,6 +212,7 @@ body.course.updates {
|
||||
@include edit-box;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 10001;
|
||||
width: 800px;
|
||||
padding: 30px;
|
||||
|
||||
@@ -61,6 +61,8 @@
|
||||
<div class="wrapper wrapper-view">
|
||||
<%include file="widgets/header.html" />
|
||||
|
||||
## remove this block after advanced settings notification is rewritten
|
||||
<%block name="view_alerts"></%block>
|
||||
<div id="page-alert"></div>
|
||||
|
||||
<%block name="content"></%block>
|
||||
@@ -72,9 +74,13 @@
|
||||
<%include file="widgets/footer.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>
|
||||
|
||||
## remove this block after advanced settings notification is rewritten
|
||||
<%block name="view_prompts"></%block>
|
||||
<div id="page-prompt"></div>
|
||||
<%block name="jsextra"></%block>
|
||||
</body>
|
||||
|
||||
@@ -1,19 +1,41 @@
|
||||
<%! 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="component-editor">
|
||||
<div class="module-editor">
|
||||
${editor}
|
||||
</div>
|
||||
<div class="row module-actions">
|
||||
<a href="#" class="save-button">Save</a>
|
||||
<a href="#" class="cancel-button">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
${editor}
|
||||
</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 class="component-actions">
|
||||
<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="edit-button standard"><span class="edit-icon"></span>${_("Edit")}</a>
|
||||
<a href="#" class="delete-button standard"><span class="delete-icon"></span>${_("Delete")}</a>
|
||||
</div>
|
||||
<a data-tooltip="Drag to reorder" href="#" class="drag-handle"></a>
|
||||
<a data-tooltip='${_("Drag to reorder")}' href="#" class="drag-handle"></a>
|
||||
${preview}
|
||||
|
||||
|
||||
6
cms/templates/js/metadata-editor.underscore
Normal file
6
cms/templates/js/metadata-editor.underscore
Normal file
@@ -0,0 +1,6 @@
|
||||
<ul class="list-input settings-list">
|
||||
<% _.each(_.range(numEntries), function() { %>
|
||||
<li class="field comp-setting-entry metadata_entry" id="settings-listing">
|
||||
</li>
|
||||
<% }) %>
|
||||
</ul>
|
||||
8
cms/templates/js/metadata-number-entry.underscore
Normal file
8
cms/templates/js/metadata-number-entry.underscore
Normal file
@@ -0,0 +1,8 @@
|
||||
<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>
|
||||
16
cms/templates/js/metadata-option-entry.underscore
Normal file
16
cms/templates/js/metadata-option-entry.underscore
Normal file
@@ -0,0 +1,16 @@
|
||||
<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>
|
||||
8
cms/templates/js/metadata-string-entry.underscore
Normal file
8
cms/templates/js/metadata-string-entry.underscore
Normal file
@@ -0,0 +1,8 @@
|
||||
<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 @@
|
||||
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
|
||||
%if not user_logged_in:
|
||||
<%block name="bodyclass">
|
||||
not-signedin
|
||||
</%block>
|
||||
%endif
|
||||
|
||||
<%block name="content">
|
||||
<section class="container activation">
|
||||
|
||||
@@ -18,7 +24,7 @@
|
||||
%if user_logged_in:
|
||||
Visit your <a href="/">dashboard</a> to see your courses.
|
||||
%else:
|
||||
You can now <a href="#login-modal" rel="leanModal">login</a>.
|
||||
You can now <a href="${reverse('login')}">login</a>.
|
||||
%endif
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -78,22 +78,13 @@
|
||||
% endif
|
||||
<div class="tab current" id="tab1">
|
||||
<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 is_empty:
|
||||
<li class="editor-md empty">
|
||||
<a href="#" data-location="${location}">
|
||||
<span class="name"> ${name}</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
% else:
|
||||
<li class="editor-md">
|
||||
<a href="#" data-location="${location}">
|
||||
<span class="name"> ${name}</span>
|
||||
</a>
|
||||
</li>
|
||||
% endif
|
||||
<li class="editor-md">
|
||||
<a href="#" id="${location}" data-location="${location}">
|
||||
<span class="name"> ${name}</span>
|
||||
</a>
|
||||
</li>
|
||||
% endif
|
||||
|
||||
%endfor
|
||||
@@ -102,23 +93,13 @@
|
||||
% if type == "problem":
|
||||
<div class="tab" id="tab2">
|
||||
<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 is_empty:
|
||||
<li class="editor-manual empty">
|
||||
<a href="#" data-location="${location}">
|
||||
<span class="name">${name}</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
% else:
|
||||
<li class="editor-manual">
|
||||
<a href="#" data-location="${location}">
|
||||
<span class="name"> ${name}</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
% endif
|
||||
<li class="editor-manual">
|
||||
<a href="#" id="${location}" data-location="${location}">
|
||||
<span class="name"> ${name}</span>
|
||||
</a>
|
||||
</li>
|
||||
% endif
|
||||
% endfor
|
||||
</ul>
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
|
||||
<div class="wrapper-comp-editor" id="editor-tab">
|
||||
<section class="html-editor editor">
|
||||
<ul class="editor-tabs">
|
||||
<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>
|
||||
</ul>
|
||||
|
||||
<div class="row">
|
||||
<textarea class="tiny-mce">${data | h}</textarea>
|
||||
<textarea name="" class="edit-box">${data | h}</textarea>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<%include file="metadata-edit.html" />
|
||||
<section class="html-editor editor">
|
||||
<ul class="editor-tabs">
|
||||
<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>
|
||||
</ul>
|
||||
|
||||
<div class="row">
|
||||
<textarea class="tiny-mce">${data | h}</textarea>
|
||||
<textarea name="" class="edit-box">${data | h}</textarea>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,49 +1,43 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
|
||||
<%
|
||||
import hashlib
|
||||
from xmodule.fields import StringyInteger, StringyFloat
|
||||
import copy
|
||||
import json
|
||||
hlskey = hashlib.md5(module.location.url()).hexdigest()
|
||||
%>
|
||||
<section class="metadata_edit">
|
||||
<ul>
|
||||
% for field_name, field_value in editable_metadata_fields.items():
|
||||
<li>
|
||||
% if field_name == 'source_code':
|
||||
% if field_value['explicitly_set'] is True:
|
||||
<a href="#hls-modal-${hlskey}" style="color:yellow;" id="hls-trig-${hlskey}" >Edit High Level Source</a>
|
||||
% endif
|
||||
% else:
|
||||
<label>${field_value['field'].display_name}:</label>
|
||||
<input type='text' data-metadata-name='${field_value["field"].display_name}'
|
||||
## This is a hack to keep current behavior for weight and attempts (empty will parse OK as unset).
|
||||
## This hack will go away with our custom editors.
|
||||
% if field_value["value"] == None and (isinstance(field_value["field"], StringyFloat) or isinstance(field_value["field"], StringyInteger)):
|
||||
value = ''
|
||||
% else:
|
||||
value='${field_value["field"].to_json(field_value["value"])}'
|
||||
% endif
|
||||
size='60' />
|
||||
## Change to True to see all the information being passed through.
|
||||
% if False:
|
||||
<label>Help: ${field_value['field'].help}</label>
|
||||
<label>Type: ${type(field_value['field']).__name__}</label>
|
||||
<label>Inheritable: ${field_value['inheritable']}</label>
|
||||
<label>Showing inherited value: ${field_value['inheritable'] and not field_value['explicitly_set']}</label>
|
||||
<label>Explicitly set: ${field_value['explicitly_set']}</label>
|
||||
<label>Default value: ${field_value['default_value']}</label>
|
||||
% if field_value['field'].values:
|
||||
<label>Possible values:</label>
|
||||
% for value in field_value['field'].values:
|
||||
<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']:
|
||||
## js templates
|
||||
<script id="metadata-editor-tpl" type="text/template">
|
||||
<%static:include path="js/metadata-editor.underscore" />
|
||||
</script>
|
||||
|
||||
<script id="metadata-number-entry" type="text/template">
|
||||
<%static:include path="js/metadata-number-entry.underscore" />
|
||||
</script>
|
||||
|
||||
<script id="metadata-string-entry" type="text/template">
|
||||
<%static:include path="js/metadata-string-entry.underscore" />
|
||||
</script>
|
||||
|
||||
<script id="metadata-option-entry" type="text/template">
|
||||
<%static:include path="js/metadata-option-entry.underscore" />
|
||||
</script>
|
||||
|
||||
<% showHighLevelSource='source_code' in editable_metadata_fields and editable_metadata_fields['source_code']['explicitly_set'] %>
|
||||
<% metadata_field_copy = copy.copy(editable_metadata_fields) %>
|
||||
## Delete 'source_code' field (if it exists) so metadata editor view does not attempt to render it.
|
||||
% if 'source_code' in editable_metadata_fields:
|
||||
## source-edit.html needs access to the 'source_code' value, so delete from a copy.
|
||||
<% del metadata_field_copy['source_code'] %>
|
||||
% endif
|
||||
|
||||
% if showHighLevelSource:
|
||||
<div class="launch-latex-compiler">
|
||||
<a href="#hls-modal-${hlskey}" id="hls-trig-${hlskey}">${_("Launch Latex Source Compiler")}</a>
|
||||
</div>
|
||||
<%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}'/>
|
||||
@@ -1,4 +1,4 @@
|
||||
<%include file="metadata-edit.html" />
|
||||
<div class="wrapper-comp-editor" id="editor-tab">
|
||||
<section class="combinedopenended-editor editor">
|
||||
<div class="row">
|
||||
%if enable_markdown:
|
||||
@@ -93,3 +93,5 @@
|
||||
</div>
|
||||
</article>
|
||||
</script>
|
||||
</div>
|
||||
<%include file="metadata-edit.html" />
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
<%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">
|
||||
<div class="row">
|
||||
%if enable_markdown:
|
||||
<div class="editor-bar">
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
</ul>
|
||||
<ul class="editor-tabs">
|
||||
<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="xml-tab advanced-toggle" data-tab="xml">${_("Advanced Editor")}</a></li>
|
||||
<li><a href="#" class="cheatsheet-toggle" data-tooltip='${_("Toggle Cheatsheet")}'>?</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<textarea class="markdown-box">${markdown | h}</textarea>
|
||||
@@ -34,7 +36,7 @@
|
||||
<article class="simple-editor-cheatsheet">
|
||||
<div class="cheatsheet-wrapper">
|
||||
<div class="row">
|
||||
<h6>Heading 1</h6>
|
||||
<h6>${_("Heading 1")}</h6>
|
||||
<div class="col sample heading-1">
|
||||
<img src="/static/img/header-example.png" />
|
||||
</div>
|
||||
@@ -45,7 +47,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h6>Multiple Choice</h6>
|
||||
<h6>${_("Multiple Choice")}</h6>
|
||||
<div class="col sample multiple-choice">
|
||||
<img src="/static/img/choice-example.png" />
|
||||
</div>
|
||||
@@ -56,7 +58,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h6>Checkboxes</h6>
|
||||
<h6>${_("Checkboxes")}</h6>
|
||||
<div class="col sample check-multiple">
|
||||
<img src="/static/img/multi-example.png" />
|
||||
</div>
|
||||
@@ -67,7 +69,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h6>Text Input</h6>
|
||||
<h6>${_("Text Input")}</h6>
|
||||
<div class="col sample string-response">
|
||||
<img src="/static/img/string-example.png" />
|
||||
</div>
|
||||
@@ -76,7 +78,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h6>Numerical Input</h6>
|
||||
<h6>${_("Numerical Input")}</h6>
|
||||
<div class="col sample numerical-response">
|
||||
<img src="/static/img/number-example.png" />
|
||||
</div>
|
||||
@@ -85,7 +87,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h6>Dropdown</h6>
|
||||
<h6>${_("Dropdown")}</h6>
|
||||
<div class="col sample option-reponse">
|
||||
<img src="/static/img/select-example.png" />
|
||||
</div>
|
||||
@@ -94,7 +96,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h6>Explanation</h6>
|
||||
<h6>${_("Explanation")}</h6>
|
||||
<div class="col sample explanation">
|
||||
<img src="/static/img/explanation-example.png" />
|
||||
</div>
|
||||
@@ -105,3 +107,5 @@
|
||||
</div>
|
||||
</article>
|
||||
</script>
|
||||
</div>
|
||||
<%include file="metadata-edit.html" />
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<div class="wrapper-comp-editor" id="editor-tab">
|
||||
<section class="raw-edit">
|
||||
<textarea name="" class="edit-box" rows="8" cols="40">${data | h}</textarea>
|
||||
</section>
|
||||
</div>
|
||||
<%include file="metadata-edit.html" />
|
||||
<section class="raw-edit">
|
||||
<textarea name="" class="edit-box" rows="8" cols="40">${data | h}</textarea>
|
||||
</section>
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<%include file="metadata-edit.html" />
|
||||
<div class="content">
|
||||
<section class="modules">
|
||||
<ol>
|
||||
@@ -50,5 +49,6 @@
|
||||
</ol>
|
||||
</section>
|
||||
</div>
|
||||
<%include file="metadata-edit.html" />
|
||||
</section>
|
||||
|
||||
|
||||
@@ -28,4 +28,4 @@ class CmsNamespace(Namespace):
|
||||
"""
|
||||
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)
|
||||
empty = StringyBoolean(help="Whether this is an empty template", scope=Scope.settings, default=False)
|
||||
|
||||
|
||||
0
common/djangoapps/mitxmako/management/__init__.py
Normal file
0
common/djangoapps/mitxmako/management/__init__.py
Normal file
@@ -0,0 +1,65 @@
|
||||
"""
|
||||
Preprocess templatized asset files, enabling asset authors to use
|
||||
Python/Django inside of Sass and CoffeeScript. This preprocessing
|
||||
will happen before the invocation of the asset compiler (currently
|
||||
handled by the asset Rakefile).
|
||||
|
||||
For this to work, assets need to be named with the appropriate
|
||||
template extension (e.g., .mako for Mako templates). Currently Mako
|
||||
is the only template engine supported.
|
||||
"""
|
||||
import os
|
||||
|
||||
from django.core.management.base import NoArgsCommand
|
||||
from django.conf import settings
|
||||
|
||||
from mako.template import Template
|
||||
|
||||
class Command(NoArgsCommand):
|
||||
"""
|
||||
Basic management command to preprocess asset template files.
|
||||
"""
|
||||
|
||||
help = "Preprocess asset template files to ready them for compilation."
|
||||
|
||||
def handle_noargs(self, **options):
|
||||
"""
|
||||
Walk over all of the static files directories specified in the
|
||||
settings file, looking for asset template files (indicated by
|
||||
a file extension like .mako).
|
||||
"""
|
||||
for staticfiles_dir in getattr(settings, "STATICFILES_DIRS", []):
|
||||
# Cribbed from the django-staticfiles app at:
|
||||
# https://github.com/jezdez/django-staticfiles/blob/develop/staticfiles/finders.py#L52
|
||||
if isinstance(staticfiles_dir, (list, tuple)):
|
||||
prefix, staticfiles_dir = staticfiles_dir
|
||||
|
||||
# Walk over the current static files directory tree,
|
||||
# preprocessing files that have a template extension.
|
||||
for root, dirs, files in os.walk(staticfiles_dir):
|
||||
for filename in files:
|
||||
outfile, extension = os.path.splitext(filename)
|
||||
# We currently only handle Mako templates
|
||||
if extension == ".mako":
|
||||
self.__preprocess(os.path.join(root, filename),
|
||||
os.path.join(root, outfile))
|
||||
|
||||
|
||||
def __context(self):
|
||||
"""
|
||||
Return a dict that contains all of the available context
|
||||
variables to the asset template.
|
||||
"""
|
||||
# TODO: do we need to include anything else?
|
||||
# TODO: do this with the django-settings-context-processor
|
||||
return { "THEME_NAME" : getattr(settings, "THEME_NAME", None) }
|
||||
|
||||
|
||||
def __preprocess(self, infile, outfile):
|
||||
"""
|
||||
Run `infile` through the Mako template engine, storing the
|
||||
result in `outfile`.
|
||||
"""
|
||||
with open(outfile, "w") as _outfile:
|
||||
_outfile.write(Template(filename=str(infile)).render(env=self.__context()))
|
||||
|
||||
@@ -13,6 +13,7 @@ class UserFactory(sf.UserFactory):
|
||||
"""
|
||||
User account for lms / cms
|
||||
"""
|
||||
FACTORY_DJANGO_GET_OR_CREATE = ('username',)
|
||||
pass
|
||||
|
||||
|
||||
@@ -21,6 +22,7 @@ class UserProfileFactory(sf.UserProfileFactory):
|
||||
"""
|
||||
Demographics etc for the User
|
||||
"""
|
||||
FACTORY_DJANGO_GET_OR_CREATE = ('user',)
|
||||
pass
|
||||
|
||||
|
||||
@@ -29,6 +31,7 @@ class RegistrationFactory(sf.RegistrationFactory):
|
||||
"""
|
||||
Activation key for registering the user account
|
||||
"""
|
||||
FACTORY_DJANGO_GET_OR_CREATE = ('user',)
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@@ -129,9 +129,12 @@ def should_have_link_with_id_and_text(step, link_id, text):
|
||||
assert_equals(link.text, text)
|
||||
|
||||
|
||||
@step(r'should see "(.*)" (?:somewhere|anywhere) in (?:the|this) page')
|
||||
def should_see_in_the_page(step, text):
|
||||
assert_in(text, world.css_text('body'))
|
||||
@step(r'should( not)? see "(.*)" (?:somewhere|anywhere) (?:in|on) (?:the|this) page')
|
||||
def should_see_in_the_page(step, doesnt_appear, text):
|
||||
if doesnt_appear:
|
||||
assert world.browser.is_text_not_present(text, wait_time=5)
|
||||
else:
|
||||
assert world.browser.is_text_present(text, wait_time=5)
|
||||
|
||||
|
||||
@step('I am logged in$')
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
from lettuce import world
|
||||
import time
|
||||
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.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
@@ -32,8 +32,13 @@ def url_equals(url):
|
||||
|
||||
|
||||
@world.absorb
|
||||
def is_css_present(css_selector):
|
||||
return world.browser.is_element_present_by_css(css_selector, wait_time=4)
|
||||
def is_css_present(css_selector, wait_time=5):
|
||||
return world.browser.is_element_present_by_css(css_selector, wait_time=wait_time)
|
||||
|
||||
|
||||
@world.absorb
|
||||
def is_css_not_present(css_selector, wait_time=5):
|
||||
return world.browser.is_element_not_present_by_css(css_selector, wait_time=wait_time)
|
||||
|
||||
|
||||
@world.absorb
|
||||
@@ -42,11 +47,11 @@ def css_has_text(css_selector, text):
|
||||
|
||||
|
||||
@world.absorb
|
||||
def css_find(css):
|
||||
def css_find(css, wait_time=5):
|
||||
def is_visible(driver):
|
||||
return EC.visibility_of_element_located((By.CSS_SELECTOR, css,))
|
||||
|
||||
world.browser.is_element_present_by_css(css, 5)
|
||||
world.browser.is_element_present_by_css(css, wait_time=wait_time)
|
||||
wait_for(is_visible)
|
||||
return world.browser.find_by_css(css)
|
||||
|
||||
@@ -56,6 +61,7 @@ def css_click(css_selector):
|
||||
"""
|
||||
Perform a click on a CSS selector, retrying if it initially fails
|
||||
"""
|
||||
assert is_css_present(css_selector)
|
||||
try:
|
||||
world.browser.find_by_css(css_selector).click()
|
||||
|
||||
@@ -63,7 +69,7 @@ def css_click(css_selector):
|
||||
# Occassionally, MathJax or other JavaScript can cover up
|
||||
# an element temporarily.
|
||||
# If this happens, wait a second, then try again
|
||||
time.sleep(1)
|
||||
world.wait(1)
|
||||
world.browser.find_by_css(css_selector).click()
|
||||
|
||||
|
||||
@@ -79,8 +85,17 @@ def css_click_at(css, x=10, y=10):
|
||||
e.action_chains.perform()
|
||||
|
||||
|
||||
@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):
|
||||
assert is_css_present(css_selector)
|
||||
world.browser.find_by_css(css_selector).first.fill(text)
|
||||
|
||||
|
||||
@@ -94,13 +109,19 @@ def css_text(css_selector):
|
||||
|
||||
# Wait for the css selector to appear
|
||||
if world.is_css_present(css_selector):
|
||||
return world.browser.find_by_css(css_selector).first.text
|
||||
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
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
@world.absorb
|
||||
def css_visible(css_selector):
|
||||
assert is_css_present(css_selector)
|
||||
return world.browser.find_by_css(css_selector).visible
|
||||
|
||||
|
||||
|
||||
@@ -209,30 +209,3 @@ def accepts(request, media_type):
|
||||
accept = parse_accept_header(request.META.get("HTTP_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)),
|
||||
))
|
||||
|
||||
15
common/lib/calc/.coveragerc
Normal file
15
common/lib/calc/.coveragerc
Normal file
@@ -0,0 +1,15 @@
|
||||
# .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):
|
||||
return x
|
||||
|
||||
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:
|
||||
return x[0]
|
||||
if 0 in x:
|
||||
@@ -180,8 +182,8 @@ def evaluator(variables, functions, string, cs=False):
|
||||
|
||||
number_part = Word(nums)
|
||||
|
||||
# 0.33 or 7 or .34
|
||||
inner_number = (number_part + Optional("." + number_part)) | ("." + number_part)
|
||||
# 0.33 or 7 or .34 or 16.
|
||||
inner_number = (number_part + Optional("." + Optional(number_part))) | ("." + number_part)
|
||||
|
||||
# 0.33k or -17
|
||||
number = (Optional(minus | plus) + inner_number
|
||||
@@ -230,27 +232,3 @@ def evaluator(variables, functions, string, cs=False):
|
||||
expr << Optional((plus | minus)) + term + ZeroOrMore((plus | minus) + term) # -5 + 4 - 3
|
||||
expr = expr.setParseAction(sum_parse_action)
|
||||
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"
|
||||
|
||||
377
common/lib/calc/tests/test_calc.py
Normal file
377
common/lib/calc/tests/test_calc.py
Normal file
@@ -0,0 +1,377 @@
|
||||
"""
|
||||
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)
|
||||
@@ -10,7 +10,6 @@ import random
|
||||
import unittest
|
||||
import textwrap
|
||||
import mock
|
||||
import textwrap
|
||||
|
||||
from . import new_loncapa_problem, test_system
|
||||
|
||||
@@ -190,7 +189,7 @@ class SymbolicResponseTest(ResponseTest):
|
||||
|
||||
def test_grade_single_input(self):
|
||||
problem = self.build_problem(math_display=True,
|
||||
expect="2*x+3*y")
|
||||
expect="2*x+3*y")
|
||||
|
||||
# Correct answers
|
||||
correct_inputs = [
|
||||
@@ -223,7 +222,6 @@ class SymbolicResponseTest(ResponseTest):
|
||||
for (input_str, input_mathml) in incorrect_inputs:
|
||||
self._assert_symbolic_grade(problem, input_str, input_mathml, 'incorrect')
|
||||
|
||||
|
||||
def test_complex_number_grade(self):
|
||||
problem = self.build_problem(math_display=True,
|
||||
expect="[[cos(theta),i*sin(theta)],[i*sin(theta),cos(theta)]]",
|
||||
@@ -241,7 +239,7 @@ class SymbolicResponseTest(ResponseTest):
|
||||
# Correct answer
|
||||
with mock.patch.object(requests, 'post') as mock_post:
|
||||
|
||||
# Simulate what the LaTeX-to-MathML server would
|
||||
# Simulate what the LaTeX-to-MathML server would
|
||||
# send for the correct response input
|
||||
mock_post.return_value.text = correct_snuggletex_response
|
||||
|
||||
@@ -323,7 +321,7 @@ class SymbolicResponseTest(ResponseTest):
|
||||
dynamath_input,
|
||||
expected_correctness):
|
||||
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)
|
||||
|
||||
@@ -349,10 +347,18 @@ class OptionResponseTest(ResponseTest):
|
||||
|
||||
|
||||
class FormulaResponseTest(ResponseTest):
|
||||
"""
|
||||
Test the FormulaResponse class
|
||||
"""
|
||||
from response_xml_factory import FormulaResponseXMLFactory
|
||||
xml_factory_class = FormulaResponseXMLFactory
|
||||
|
||||
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_dict = {'x': (-10, 10), 'y': (-10, 10)}
|
||||
|
||||
@@ -373,6 +379,9 @@ class FormulaResponseTest(ResponseTest):
|
||||
self.assert_grade(problem, input_formula, "incorrect")
|
||||
|
||||
def test_hint(self):
|
||||
"""
|
||||
Test the hint-giving functionality of FormulaResponse
|
||||
"""
|
||||
# Sample variables x and y in the range [-10, 10]
|
||||
sample_dict = {'x': (-10, 10), 'y': (-10, 10)}
|
||||
|
||||
@@ -401,6 +410,10 @@ class FormulaResponseTest(ResponseTest):
|
||||
'Try including the variable x')
|
||||
|
||||
def test_script(self):
|
||||
"""
|
||||
Test if python script can be used to generate answers
|
||||
"""
|
||||
|
||||
# Calculate the answer using a script
|
||||
script = "calculated_ans = 'x+x'"
|
||||
|
||||
@@ -419,7 +432,9 @@ class FormulaResponseTest(ResponseTest):
|
||||
self.assert_grade(problem, '3*x', 'incorrect')
|
||||
|
||||
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)}
|
||||
|
||||
# Test problem
|
||||
@@ -440,8 +455,11 @@ class FormulaResponseTest(ResponseTest):
|
||||
self.assert_grade(problem, input_formula, "incorrect")
|
||||
|
||||
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_dict = {'x': (-10, 10)}
|
||||
@@ -464,11 +482,14 @@ class FormulaResponseTest(ResponseTest):
|
||||
msg="Failed on variable {0}; the given, incorrect answer was {1} but graded 'correct'".format(var, incorrect))
|
||||
|
||||
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,
|
||||
# arccos, arcsin, arctan, abs,
|
||||
# fact, factorial
|
||||
"""
|
||||
Test the default functions provided in common/lib/capa/capa/calc.py
|
||||
|
||||
which are:
|
||||
sin, cos, tan, sqrt, log10, log2, ln,
|
||||
arccos, arcsin, arctan, abs,
|
||||
fact, factorial
|
||||
"""
|
||||
w = random.randint(3, 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
|
||||
@@ -496,8 +517,10 @@ class FormulaResponseTest(ResponseTest):
|
||||
msg="Failed on function {0}; the given, incorrect answer was {1} but graded 'correct'".format(func, incorrect))
|
||||
|
||||
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)}
|
||||
|
||||
@@ -514,8 +537,9 @@ class FormulaResponseTest(ResponseTest):
|
||||
self.assert_grade(problem, input_formula, "incorrect")
|
||||
|
||||
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)}
|
||||
|
||||
@@ -532,6 +556,18 @@ class FormulaResponseTest(ResponseTest):
|
||||
input_formula = "x + 0*1e999"
|
||||
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):
|
||||
from response_xml_factory import StringResponseXMLFactory
|
||||
@@ -592,7 +628,7 @@ class StringResponseTest(ResponseTest):
|
||||
problem = self.build_problem(
|
||||
answer="Michigan",
|
||||
hintfn="gimme_a_hint",
|
||||
script = textwrap.dedent("""
|
||||
script=textwrap.dedent("""
|
||||
def gimme_a_hint(answer_ids, student_answers, new_cmap, old_cmap):
|
||||
aid = answer_ids[0]
|
||||
answer = student_answers[aid]
|
||||
@@ -898,6 +934,14 @@ class NumericalResponseTest(ResponseTest):
|
||||
incorrect_responses = ["", "3.9", "4.1", "0", "5.01e1"]
|
||||
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):
|
||||
from response_xml_factory import CustomResponseXMLFactory
|
||||
@@ -947,8 +991,8 @@ class CustomResponseTest(ResponseTest):
|
||||
#
|
||||
# 'answer_given' is the answer the student gave (if there is just one input)
|
||||
# or an ordered list of answers (if there are multiple inputs)
|
||||
#
|
||||
# The function should return a dict of the form
|
||||
#
|
||||
# The function should return a dict of the form
|
||||
# { 'ok': BOOL, 'msg': STRING }
|
||||
#
|
||||
script = textwrap.dedent("""
|
||||
|
||||
@@ -4,5 +4,5 @@ setup(
|
||||
name="capa",
|
||||
version="0.1",
|
||||
packages=find_packages(exclude=["tests"]),
|
||||
install_requires=["distribute==0.6.28"],
|
||||
install_requires=["distribute>=0.6.28"],
|
||||
)
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
(Originally written by Ike.)
|
||||
|
||||
At a high level, the main challenges of checking symbolic math expressions are (1) making sure the expression is mathematically legal, and (2) simplifying the expression for comparison with what is expected.
|
||||
|
||||
(1) Generation (and testing) of legal input is done by using MathJax to provide input math in an XML format known as Presentation MathML (PMathML). Such expressions typeset correctly, but may not be mathematically legal, like "5 / (1 = 2)". The PMathML is converted into "Content MathML" (CMathML), which is by definition mathematically legal, using an XSLT 2.0 stylesheet, via a module in SnuggleTeX. CMathML is then converted into a sympy expression. This work is all done in `lms/lib/symmath/formula.py`
|
||||
|
||||
(2) Simplifying the expression and checking against what is expected is done by using sympy, and a set of heuristics based on options flags provided by the problem author. For example, the problem author may specify that the expected expression is a matrix, in which case the dimensionality of the input expression is checked. Other options include specifying that the comparison be checked numerically in addition to symbolically. The checking is done in stages, first with no simplification, then with increasing levels of testing; if a match is found at any stage, then an "ok" is returned. Helpful messages are also returned, eg if the input expression is of a different type than the expected. This work is all done in `lms/lib/symmath/symmath_check.py`
|
||||
|
||||
Links:
|
||||
|
||||
SnuggleTex: http://www2.ph.ed.ac.uk/snuggletex/documentation/overview-and-features.html
|
||||
MathML: http://www.w3.org/TR/MathML2/overview.html
|
||||
SymPy: http://sympy.org/en/index.html
|
||||
@@ -4,6 +4,7 @@ setup(
|
||||
name="sandbox-packages",
|
||||
version="0.1.1",
|
||||
packages=[
|
||||
"loncapa",
|
||||
"verifiers",
|
||||
],
|
||||
py_modules=[
|
||||
|
||||
10
common/lib/symmath/setup.py
Normal file
10
common/lib/symmath/setup.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
name="symmath",
|
||||
version="0.1",
|
||||
packages=["symmath"],
|
||||
install_requires=[
|
||||
"sympy",
|
||||
],
|
||||
)
|
||||
30
common/lib/symmath/symmath/README.md
Normal file
30
common/lib/symmath/symmath/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
(Originally written by Ike.)
|
||||
|
||||
At a high level, the main challenges of checking symbolic math expressions are
|
||||
(1) making sure the expression is mathematically legal, and (2) simplifying the
|
||||
expression for comparison with what is expected.
|
||||
|
||||
(1) Generation (and testing) of legal input is done by using MathJax to provide
|
||||
input math in an XML format known as Presentation MathML (PMathML). Such
|
||||
expressions typeset correctly, but may not be mathematically legal, like "5 /
|
||||
(1 = 2)". The PMathML is converted into "Content MathML" (CMathML), which is
|
||||
by definition mathematically legal, using an XSLT 2.0 stylesheet, via a module
|
||||
in SnuggleTeX. CMathML is then converted into a sympy expression. This work is
|
||||
all done in `symmath/formula.py`.
|
||||
|
||||
(2) Simplifying the expression and checking against what is expected is done by
|
||||
using sympy, and a set of heuristics based on options flags provided by the
|
||||
problem author. For example, the problem author may specify that the expected
|
||||
expression is a matrix, in which case the dimensionality of the input
|
||||
expression is checked. Other options include specifying that the comparison be
|
||||
checked numerically in addition to symbolically. The checking is done in
|
||||
stages, first with no simplification, then with increasing levels of testing;
|
||||
if a match is found at any stage, then an "ok" is returned. Helpful messages
|
||||
are also returned, eg if the input expression is of a different type than the
|
||||
expected. This work is all done in `symmath/symmath_check.py`.
|
||||
|
||||
Links:
|
||||
|
||||
SnuggleTex: http://www2.ph.ed.ac.uk/snuggletex/documentation/overview-and-features.html
|
||||
MathML: http://www.w3.org/TR/MathML2/overview.html
|
||||
SymPy: http://sympy.org/en/index.html
|
||||
@@ -66,22 +66,51 @@ class ComplexEncoder(json.JSONEncoder):
|
||||
|
||||
class CapaFields(object):
|
||||
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)
|
||||
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",
|
||||
values=["answered", "always", "attempted", "closed", "never"])
|
||||
showanswer = String(
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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):
|
||||
|
||||
@@ -5,7 +5,7 @@ from pkg_resources import resource_string
|
||||
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
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 collections import namedtuple
|
||||
from .fields import Date, StringyFloat, StringyInteger, StringyBoolean
|
||||
@@ -48,27 +48,49 @@ class VersionInteger(Integer):
|
||||
|
||||
|
||||
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)
|
||||
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",
|
||||
scope=Scope.user_state)
|
||||
student_attempts = StringyInteger(help="Number of attempts taken by the student on this problem", default=0,
|
||||
scope=Scope.user_state)
|
||||
ready_to_reset = StringyBoolean(help="If the problem is ready to be reset or not.", default=False,
|
||||
scope=Scope.user_state)
|
||||
attempts = StringyInteger(help="Maximum number of attempts that a student is allowed.", default=1, scope=Scope.settings)
|
||||
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,
|
||||
scope=Scope.settings)
|
||||
skip_spelling_checks = StringyBoolean(help="Whether or not to skip initial spelling checks.", default=True,
|
||||
scope=Scope.settings)
|
||||
ready_to_reset = StringyBoolean(
|
||||
help="If the problem is ready to be reset or not.", default=False,
|
||||
scope=Scope.user_state
|
||||
)
|
||||
attempts = StringyInteger(
|
||||
display_name="Maximum Attempts",
|
||||
help="The number of times the student can try to answer this problem.", default=1,
|
||||
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)
|
||||
graceperiod = String(help="Amount of time after the due date that submissions will be accepted", default=None,
|
||||
scope=Scope.settings)
|
||||
graceperiod = String(
|
||||
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)
|
||||
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)
|
||||
|
||||
|
||||
@@ -244,6 +266,6 @@ class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor):
|
||||
def non_editable_metadata_fields(self):
|
||||
non_editable_fields = super(CombinedOpenEndedDescriptor, self).non_editable_metadata_fields
|
||||
non_editable_fields.extend([CombinedOpenEndedDescriptor.due, CombinedOpenEndedDescriptor.graceperiod,
|
||||
CombinedOpenEndedDescriptor.markdown])
|
||||
CombinedOpenEndedDescriptor.markdown, CombinedOpenEndedDescriptor.version])
|
||||
return non_editable_fields
|
||||
|
||||
|
||||
@@ -10,8 +10,6 @@
|
||||
position: relative;
|
||||
@include linear-gradient(top, #d4dee8, #c9d5e2);
|
||||
padding: 5px;
|
||||
border: 1px solid #3c3c3c;
|
||||
border-radius: 3px 3px 0 0;
|
||||
border-bottom-color: #a5aaaf;
|
||||
@include clearfix;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
.advanced-toggle {
|
||||
@include white-button;
|
||||
height: auto;
|
||||
margin-top: -1px;
|
||||
margin-top: -4px;
|
||||
padding: 3px 9px;
|
||||
font-size: 12px;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
color: $darkGrey !important;
|
||||
pointer-events: none;
|
||||
cursor: none;
|
||||
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 0 !important;
|
||||
}
|
||||
@@ -27,7 +27,7 @@
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
padding: 0;
|
||||
margin: 0 5px 0 15px;
|
||||
margin: -1px 5px 0 15px;
|
||||
border-radius: 22px;
|
||||
border: 1px solid #a5aaaf;
|
||||
background: #e5ecf3;
|
||||
@@ -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 {
|
||||
display: inline-block;
|
||||
width: 26px;
|
||||
|
||||
@@ -170,7 +170,7 @@ nav.sequence-nav {
|
||||
font-family: $sans-serif;
|
||||
line-height: lh();
|
||||
left: 0px;
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
padding: 6px;
|
||||
position: absolute;
|
||||
top: 48px;
|
||||
@@ -204,7 +204,7 @@ nav.sequence-nav {
|
||||
p {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -248,12 +248,12 @@ nav.sequence-nav {
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: .5;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
cursor: normal;
|
||||
opacity: .4;
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -320,12 +320,12 @@ nav.sequence-bottom {
|
||||
outline: 0;
|
||||
|
||||
&:hover {
|
||||
opacity: .5;
|
||||
opacity: 0.5;
|
||||
background-position: center 15px;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: .4;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
|
||||
@@ -41,7 +41,7 @@ div.video {
|
||||
|
||||
&:hover {
|
||||
ul, div {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ div.video {
|
||||
|
||||
ol.video_speeds {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
@@ -208,7 +208,7 @@ div.video {
|
||||
}
|
||||
|
||||
&:hover, &:active, &:focus {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
background-color: #444;
|
||||
}
|
||||
}
|
||||
@@ -221,7 +221,7 @@ div.video {
|
||||
border: 1px solid #000;
|
||||
bottom: 46px;
|
||||
display: none;
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
position: absolute;
|
||||
width: 133px;
|
||||
z-index: 10;
|
||||
@@ -264,7 +264,7 @@ div.video {
|
||||
&.open {
|
||||
.volume-slider-container {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,7 +302,7 @@ div.video {
|
||||
border: 1px solid #000;
|
||||
bottom: 46px;
|
||||
display: none;
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
position: absolute;
|
||||
width: 45px;
|
||||
height: 125px;
|
||||
@@ -395,7 +395,7 @@ div.video {
|
||||
font-weight: 800;
|
||||
line-height: 46px; //height of play pause buttons
|
||||
margin-left: 0;
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
padding: 0 lh(.5);
|
||||
position: relative;
|
||||
text-indent: -9999px;
|
||||
@@ -410,7 +410,7 @@ div.video {
|
||||
}
|
||||
|
||||
&.off {
|
||||
opacity: .7;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -418,7 +418,7 @@ div.video {
|
||||
|
||||
&:hover section.video-controls {
|
||||
ul, div {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
div.slider {
|
||||
|
||||
@@ -41,7 +41,7 @@ div.video {
|
||||
|
||||
&:hover {
|
||||
ul, div {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ div.video {
|
||||
|
||||
ol.video_speeds {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
@@ -208,7 +208,7 @@ div.video {
|
||||
}
|
||||
|
||||
&:hover, &:active, &:focus {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
background-color: #444;
|
||||
}
|
||||
}
|
||||
@@ -221,7 +221,7 @@ div.video {
|
||||
border: 1px solid #000;
|
||||
bottom: 46px;
|
||||
display: none;
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
position: absolute;
|
||||
width: 133px;
|
||||
z-index: 10;
|
||||
@@ -264,7 +264,7 @@ div.video {
|
||||
&.open {
|
||||
.volume-slider-container {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,7 +302,7 @@ div.video {
|
||||
border: 1px solid #000;
|
||||
bottom: 46px;
|
||||
display: none;
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
position: absolute;
|
||||
width: 45px;
|
||||
height: 125px;
|
||||
@@ -395,7 +395,7 @@ div.video {
|
||||
font-weight: 800;
|
||||
line-height: 46px; //height of play pause buttons
|
||||
margin-left: 0;
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
padding: 0 lh(.5);
|
||||
position: relative;
|
||||
text-indent: -9999px;
|
||||
@@ -410,7 +410,7 @@ div.video {
|
||||
}
|
||||
|
||||
&.off {
|
||||
opacity: .7;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -418,7 +418,7 @@ div.video {
|
||||
|
||||
&:hover section.video-controls {
|
||||
ul, div {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
div.slider {
|
||||
|
||||
@@ -8,8 +8,16 @@ from xblock.core import String, Scope
|
||||
|
||||
class DiscussionFields(object):
|
||||
discussion_id = String(scope=Scope.settings)
|
||||
discussion_category = String(scope=Scope.settings)
|
||||
discussion_target = String(scope=Scope.settings)
|
||||
discussion_category = String(
|
||||
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)
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
"""
|
||||
Modules that get shown to the users when an error has occured while
|
||||
loading or rendering other modules
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
import json
|
||||
@@ -22,12 +27,19 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ErrorFields(object):
|
||||
"""
|
||||
XBlock fields used by the ErrorModules
|
||||
"""
|
||||
contents = String(scope=Scope.content)
|
||||
error_msg = String(scope=Scope.content)
|
||||
display_name = String(scope=Scope.settings)
|
||||
|
||||
|
||||
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):
|
||||
'''Show an error to staff.
|
||||
@@ -42,6 +54,10 @@ class ErrorModule(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):
|
||||
'''Show an error to a student.
|
||||
TODO (vshnayder): proper style, divs, etc.
|
||||
@@ -61,7 +77,7 @@ class ErrorDescriptor(ErrorFields, JSONEditingDescriptor):
|
||||
module_class = ErrorModule
|
||||
|
||||
@classmethod
|
||||
def _construct(self, system, contents, error_msg, location):
|
||||
def _construct(cls, system, contents, error_msg, location):
|
||||
|
||||
if location.name is None:
|
||||
location = location._replace(
|
||||
@@ -80,7 +96,7 @@ class ErrorDescriptor(ErrorFields, JSONEditingDescriptor):
|
||||
'contents': contents,
|
||||
'display_name': 'Error: ' + location.name
|
||||
}
|
||||
return ErrorDescriptor(
|
||||
return cls(
|
||||
system,
|
||||
location,
|
||||
model_data,
|
||||
|
||||
@@ -16,6 +16,8 @@ log = logging.getLogger(__name__)
|
||||
|
||||
class FolditFields(object):
|
||||
# default to what Spring_7012x uses
|
||||
required_level_half_credit = Integer(default=3, scope=Scope.settings)
|
||||
required_sublevel_half_credit = Integer(default=5, scope=Scope.settings)
|
||||
required_level = Integer(default=4, scope=Scope.settings)
|
||||
required_sublevel = Integer(default=5, scope=Scope.settings)
|
||||
due = Date(help="Date that this problem is due by", scope=Scope.settings)
|
||||
@@ -36,6 +38,8 @@ class FolditModule(FolditFields, XModule):
|
||||
<foldit show_basic_score="true"
|
||||
required_level="4"
|
||||
required_sublevel="3"
|
||||
required_level_half_credit="2"
|
||||
required_sublevel_half_credit="3"
|
||||
show_leaderboard="false"/>
|
||||
"""
|
||||
|
||||
@@ -57,6 +61,22 @@ class FolditModule(FolditFields, XModule):
|
||||
self.due_time)
|
||||
return complete
|
||||
|
||||
def is_half_complete(self):
|
||||
"""
|
||||
Did the user reach the required level for half credit?
|
||||
|
||||
Ideally this would be more flexible than just 0, 0.5, or 1 credit. On
|
||||
the other hand, the xml attributes for specifying more specific
|
||||
cut-offs and partial grades can get more confusing.
|
||||
"""
|
||||
from foldit.models import PuzzleComplete
|
||||
complete = PuzzleComplete.is_level_complete(
|
||||
self.system.anonymous_student_id,
|
||||
self.required_level_half_credit,
|
||||
self.required_sublevel_half_credit,
|
||||
self.due_time)
|
||||
return complete
|
||||
|
||||
def completed_puzzles(self):
|
||||
"""
|
||||
Return a list of puzzles that this user has completed, as an array of
|
||||
@@ -139,9 +159,18 @@ class FolditModule(FolditFields, XModule):
|
||||
|
||||
def get_score(self):
|
||||
"""
|
||||
0 / 1 based on whether student has gotten far enough.
|
||||
0 if required_level_half_credit - required_sublevel_half_credit not
|
||||
reached.
|
||||
0.5 if required_level_half_credit and required_sublevel_half_credit
|
||||
reached.
|
||||
1 if requred_level and required_sublevel reached.
|
||||
"""
|
||||
score = 1 if self.is_complete() else 0
|
||||
if self.is_complete():
|
||||
score = 1
|
||||
elif self.is_half_complete():
|
||||
score = 0.5
|
||||
else:
|
||||
score = 0
|
||||
return {'score': score,
|
||||
'total': self.max_score()}
|
||||
|
||||
|
||||
@@ -289,6 +289,9 @@ class @CombinedOpenEnded
|
||||
if @child_type == "openended"
|
||||
@submit_button.hide()
|
||||
@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'
|
||||
if @child_type=="openended"
|
||||
@skip_button.show()
|
||||
@@ -311,6 +314,8 @@ class @CombinedOpenEnded
|
||||
if @task_number<@task_count
|
||||
@next_problem()
|
||||
else
|
||||
if @task_number==1 and @task_count==1
|
||||
@show_combined_rubric_current()
|
||||
@show_results_current()
|
||||
@reset_button.show()
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ class @VideoPlayer extends Subview
|
||||
at: 'top center'
|
||||
|
||||
onReady: (event) =>
|
||||
unless onTouchBasedDevice()
|
||||
unless onTouchBasedDevice() or $('.video:first').data('autoplay') == 'False'
|
||||
$('.video-load-complete:first').data('video').player.play()
|
||||
|
||||
onStateChange: (event) =>
|
||||
|
||||
@@ -85,7 +85,7 @@ class MockControllerQueryService(object):
|
||||
def __init__(self, config, system):
|
||||
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.
|
||||
@param params:
|
||||
@@ -93,7 +93,7 @@ class MockControllerQueryService(object):
|
||||
"""
|
||||
pass
|
||||
|
||||
def check_for_eta(self, **params):
|
||||
def check_for_eta(self, *args, **kwargs):
|
||||
"""
|
||||
Mock later if needed. Stub function for now.
|
||||
@param params:
|
||||
@@ -101,19 +101,19 @@ class MockControllerQueryService(object):
|
||||
"""
|
||||
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}'
|
||||
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}'
|
||||
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"}'
|
||||
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.
|
||||
@param params:
|
||||
|
||||
@@ -10,7 +10,7 @@ from .x_module import XModule
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
from xmodule.modulestore.django import modulestore
|
||||
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.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService
|
||||
@@ -22,24 +22,43 @@ USE_FOR_SINGLE_LOCATION = False
|
||||
LINK_TO_LOCATION = ""
|
||||
TRUE_DICT = [True, "True", "true", "TRUE"]
|
||||
MAX_SCORE = 1
|
||||
IS_GRADED = True
|
||||
IS_GRADED = False
|
||||
|
||||
EXTERNAL_GRADER_NO_CONTACT_ERROR = "Failed to contact external graders. Please notify course staff."
|
||||
|
||||
|
||||
class PeerGradingFields(object):
|
||||
use_for_single_location = StringyBoolean(help="Whether to use this for a single location or as a panel.",
|
||||
default=USE_FOR_SINGLE_LOCATION, scope=Scope.settings)
|
||||
link_to_location = String(help="The location this problem is linked to.", default=LINK_TO_LOCATION,
|
||||
scope=Scope.settings)
|
||||
is_graded = StringyBoolean(help="Whether or not this module is scored.", default=IS_GRADED, scope=Scope.settings)
|
||||
use_for_single_location = StringyBoolean(
|
||||
display_name="Show Single Problem",
|
||||
help='When True, only the single problem specified by "Link to Problem Location" is shown. '
|
||||
'When False, a panel is displayed with all problems available for peer grading.',
|
||||
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)
|
||||
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,
|
||||
scope=Scope.settings)
|
||||
student_data_for_location = Object(help="Student data for a given peer grading problem.",
|
||||
scope=Scope.user_state)
|
||||
weight = StringyFloat(help="How much to weight this problem by", scope=Scope.settings)
|
||||
max_grade = StringyInteger(
|
||||
help="The maximum grade that a student can receive for this problem.", default=MAX_SCORE,
|
||||
scope=Scope.settings, values={"min": 0}
|
||||
)
|
||||
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):
|
||||
@@ -590,3 +609,11 @@ class PeerGradingDescriptor(PeerGradingFields, RawDescriptor):
|
||||
|
||||
#Specify whether or not to pass in open ended interface
|
||||
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
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user