Merge branch 'master' into peter/symbolic

This commit is contained in:
Peter Baratta
2013-03-14 06:31:17 -06:00
146 changed files with 9326 additions and 1147 deletions

4
.gitignore vendored
View File

@@ -28,4 +28,6 @@ nosetests.xml
cover_html/
.idea/
.redcar/
chromedriver.log
chromedriver.log
/nbproject
ghostdriver.log

View File

@@ -12,7 +12,7 @@ profile=no
# Add files or directories to the blacklist. They should be base names, not
# paths.
ignore=CVS
ignore=CVS, migrations
# Pickle collected data for later comparisons.
persistent=yes
@@ -33,7 +33,11 @@ load-plugins=
# can either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once).
disable=E1102,W0142
disable=
# W0141: Used builtin function 'map'
# W0142: Used * or ** magic
# R0903: Too few public methods (1/2)
W0141,W0142,R0903
[REPORTS]
@@ -43,7 +47,7 @@ disable=E1102,W0142
output-format=text
# Include message's id in output
include-ids=no
include-ids=yes
# Put messages in a separate file for each module / package specified on the
# command line instead of printing them on stdout. Reports (if any) will be
@@ -97,7 +101,7 @@ bad-functions=map,filter,apply,input
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Regular expression which should only match correct module level names
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__)|log|urlpatterns)$
# Regular expression which should only match correct class names
class-rgx=[A-Z_][a-zA-Z0-9]+$
@@ -106,7 +110,7 @@ class-rgx=[A-Z_][a-zA-Z0-9]+$
function-rgx=[a-z_][a-z0-9_]{2,30}$
# Regular expression which should only match correct method names
method-rgx=[a-z_][a-z0-9_]{2,30}$
method-rgx=([a-z_][a-z0-9_]{2,60}|setUp|set[Uu]pClass|tearDown|tear[Dd]ownClass|assert[A-Z]\w*)$
# Regular expression which should only match correct instance attribute names
attr-rgx=[a-z_][a-z0-9_]{2,30}$

View File

@@ -1 +1 @@
1.8.7-p371
1.9.3-p374

View File

@@ -1,4 +1,4 @@
source :rubygems
source 'https://rubygems.org'
gem 'rake', '~> 10.0.3'
gem 'sass', '3.1.15'
gem 'bourbon', '~> 1.3.6'

View File

@@ -7,6 +7,7 @@ Feature: Advanced (manual) course policy
When I select the Advanced Settings
Then I see only the display name
@skip-phantom
Scenario: Test if there are no policy settings without existing UI controls
Given I am on the Advanced Course Settings page in Studio
When I delete the display name
@@ -14,6 +15,7 @@ Feature: Advanced (manual) course policy
And I reload the page
Then there are no advanced policy settings
@skip-phantom
Scenario: Test cancel editing key name
Given I am on the Advanced Course Settings page in Studio
When I edit the name of a policy key
@@ -32,6 +34,7 @@ Feature: Advanced (manual) course policy
And I press the "Cancel" notification button
Then the policy key value is unchanged
@skip-phantom
Scenario: Test editing key value
Given I am on the Advanced Course Settings page in Studio
When I edit the value of a policy key

View File

@@ -1,9 +1,10 @@
from lettuce import world, step
from common import *
import time
from selenium.common.exceptions import WebDriverException
from selenium.webdriver.support import expected_conditions as EC
from nose.tools import assert_equal
from nose.tools import assert_true
from nose.tools import assert_true, assert_false, assert_equal
"""
http://selenium.googlecode.com/svn/trunk/docs/api/py/webdriver/selenium.webdriver.common.keys.html
@@ -19,6 +20,7 @@ def i_select_advanced_settings(step):
css_click(expand_icon_css)
link_css = 'li.nav-course-settings-advanced a'
css_click(link_css)
# world.browser.click_link_by_text('Advanced Settings')
@step('I am on the Advanced Course Settings page in Studio$')
@@ -37,13 +39,25 @@ def reload_the_page(step):
def edit_the_name_of_a_policy_key(step):
policy_key_css = 'input.policy-key'
e = css_find(policy_key_css).first
e.fill('new')
e.type('_new')
@step(u'I press the "([^"]*)" notification button$')
def press_the_notification_button(step, name):
world.browser.click_link_by_text(name)
def is_visible(driver):
return EC.visibility_of_element_located((By.CSS_SELECTOR,css,))
def is_invisible(driver):
return EC.invisibility_of_element_located((By.CSS_SELECTOR,css,))
css = 'a.%s-button' % name.lower()
wait_for(is_visible)
try:
css_click_at(css)
wait_for(is_invisible)
except WebDriverException, e:
css_click_at(css)
wait_for(is_invisible)
@step(u'I edit the value of a policy key$')
def edit_the_value_of_a_policy_key(step):
@@ -83,7 +97,12 @@ def i_see_only_display_name(step):
@step('there are no advanced policy settings$')
def no_policy_settings(step):
assert_policy_entries([], [])
keys_css = 'input.policy-key'
val_css = 'textarea.json'
k = world.browser.is_element_not_present_by_css(keys_css, 5)
v = world.browser.is_element_not_present_by_css(val_css, 5)
assert_true(k)
assert_true(v)
@step('they are alphabetized$')
@@ -99,29 +118,29 @@ def it_is_formatted(step):
@step(u'the policy key name is unchanged$')
def the_policy_key_name_is_unchanged(step):
policy_key_css = 'input.policy-key'
e = css_find(policy_key_css).first
assert_equal(e.value, 'display_name')
val = css_find(policy_key_css).first.value
assert_equal(val, 'display_name')
@step(u'the policy key name is changed$')
def the_policy_key_name_is_changed(step):
policy_key_css = 'input.policy-key'
e = css_find(policy_key_css).first
assert_equal(e.value, 'new')
val = css_find(policy_key_css).first.value
assert_equal(val, 'display_name_new')
@step(u'the policy key value is unchanged$')
def the_policy_key_value_is_unchanged(step):
policy_value_css = 'li.course-advanced-policy-list-item div.value textarea'
e = css_find(policy_value_css).first
assert_equal(e.value, '"Robot Super Course"')
val = css_find(policy_value_css).first.value
assert_equal(val, '"Robot Super Course"')
@step(u'the policy key value is changed$')
def the_policy_key_value_is_unchanged(step):
policy_value_css = 'li.course-advanced-policy-list-item div.value textarea'
e = css_find(policy_value_css).first
assert_equal(e.value, '"Robot Super Course X"')
val = css_find(policy_value_css).first.value
assert_equal(val, '"Robot Super Course X"')
############# HELPERS ###############
@@ -132,19 +151,23 @@ def create_entry(key, value):
new_key_css = 'div#__new_advanced_key__ input'
new_key_element = css_find(new_key_css).first
new_key_element.fill(key)
# For some reason have to get the instance for each command (get error that it is no longer attached to the DOM)
# Have to do all this because Selenium has a bug that fill does not remove existing text
# For some reason have to get the instance for each command
# (get error that it is no longer attached to the DOM)
# Have to do all this because Selenium fill does not remove existing text
new_value_css = 'div.CodeMirror textarea'
css_find(new_value_css).last.fill("")
css_find(new_value_css).last._element.send_keys(Keys.DELETE, Keys.DELETE)
css_find(new_value_css).last.fill(value)
# Add in a TAB key press because intermittently on ubuntu the
# last character of "value" above was not getting typed in
css_find(new_value_css).last._element.send_keys(Keys.TAB)
def delete_entry(index):
"""
Delete the nth entry where index is 0-based
"""
css = '.delete-button'
css = 'a.delete-button'
assert_true(world.browser.is_element_present_by_css(css, 5))
delete_buttons = css_find(css)
assert_true(len(delete_buttons) > index, "no delete button exists for entry " + str(index))
@@ -152,8 +175,8 @@ def delete_entry(index):
def assert_policy_entries(expected_keys, expected_values):
assert_entries('.key input', expected_keys)
assert_entries('.json', expected_values)
assert_entries('.key input.policy-key', expected_keys)
assert_entries('textarea.json', expected_values)
def assert_entries(css, expected_values):
@@ -165,16 +188,8 @@ def assert_entries(css, expected_values):
def click_save():
css = ".save-button"
def is_shown(driver):
visible = css_find(css).first.visible
if visible:
# Even when waiting for visible, this fails sporadically. Adding in a small wait.
time.sleep(float(1))
return visible
wait_for(is_shown)
css_click(css)
css = "a.save-button"
css_click_at(css)
def fill_last_field(value):

View File

@@ -3,18 +3,20 @@ from lettuce.django import django_url
from nose.tools import assert_true
from nose.tools import assert_equal
from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import WebDriverException, StaleElementReferenceException
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from terrain.factories import UserFactory, RegistrationFactory, UserProfileFactory
from terrain.factories import CourseFactory, GroupFactory
import xmodule.modulestore.django
from xmodule.modulestore.django import _MODULESTORES, modulestore
from xmodule.templates import update_templates
from auth.authz import get_user_by_email
from logging import getLogger
logger = getLogger(__name__)
########### STEP HELPERS ##############
@step('I (?:visit|access|open) the Studio homepage$')
def i_visit_the_studio_homepage(step):
# To make this go to port 8001, put
@@ -52,9 +54,8 @@ def i_have_opened_a_new_course(step):
log_into_studio()
create_a_course()
####### HELPER FUNCTIONS ##############
def create_studio_user(
uname='robot',
email='robot+studio@edx.org',
@@ -83,9 +84,9 @@ def flush_xmodule_store():
# (though it shouldn't), do this manually
# from the bash shell to drop it:
# $ mongo test_xmodule --eval "db.dropDatabase()"
xmodule.modulestore.django._MODULESTORES = {}
xmodule.modulestore.django.modulestore().collection.drop()
xmodule.templates.update_templates()
_MODULESTORES = {}
modulestore().collection.drop()
update_templates()
def assert_css_with_text(css, text):
@@ -94,8 +95,16 @@ def assert_css_with_text(css, text):
def css_click(css):
assert_true(world.browser.is_element_present_by_css(css, 5))
world.browser.find_by_css(css).first.click()
'''
First try to use the regular click method,
but if clicking in the middle of an element
doesn't work it might be that it thinks some other
element is on top of it there so click in the upper left
'''
try:
css_find(css).first.click()
except WebDriverException, e:
css_click_at(css)
def css_click_at(css, x=10, y=10):
@@ -103,8 +112,7 @@ def css_click_at(css, x=10, y=10):
A method to click at x,y coordinates of the element
rather than in the center of the element
'''
assert_true(world.browser.is_element_present_by_css(css, 5))
e = world.browser.find_by_css(css).first
e = css_find(css).first
e.action_chains.move_to_element_with_offset(e._element, x, y)
e.action_chains.click()
e.action_chains.perform()
@@ -115,11 +123,16 @@ def css_fill(css, value):
def css_find(css):
def is_visible(driver):
return EC.visibility_of_element_located((By.CSS_SELECTOR,css,))
world.browser.is_element_present_by_css(css, 5)
wait_for(is_visible)
return world.browser.find_by_css(css)
def wait_for(func):
WebDriverWait(world.browser.driver, 10).until(func)
WebDriverWait(world.browser.driver, 5).until(func)
def id_find(id):

View File

@@ -26,9 +26,10 @@ Feature: Create Section
And I save a new section release date
Then the section release date is updated
@skip-phantom
Scenario: Delete section
Given I have opened a new course in Studio
And I have added a new section
When I press the "section" delete icon
And I confirm the alert
Then the section does not exist
Then the section does not exist

View File

@@ -1,6 +1,8 @@
from lettuce import world, step
from common import *
from nose.tools import assert_equal
from selenium.webdriver.common.keys import Keys
import time
############### ACTIONS ####################
@@ -37,10 +39,14 @@ def i_save_a_new_section_release_date(step):
date_css = 'input.start-date.date.hasDatepicker'
time_css = 'input.start-time.time.ui-timepicker-input'
css_fill(date_css, '12/25/2013')
# click here to make the calendar go away
css_click(time_css)
# hit TAB to get to the time field
e = css_find(date_css).first
e._element.send_keys(Keys.TAB)
css_fill(time_css, '12:00am')
css_click('a.save-button')
e = css_find(time_css).first
e._element.send_keys(Keys.TAB)
time.sleep(float(1))
world.browser.click_link_by_text('Save')
############ ASSERTIONS ###################
@@ -106,7 +112,7 @@ def the_section_release_date_picker_not_visible(step):
def the_section_release_date_is_updated(step):
css = 'span.published-status'
status_text = world.browser.find_by_css(css).text
assert status_text == 'Will Release: 12/25/2013 at 12:00am'
assert_equal(status_text,'Will Release: 12/25/2013 at 12:00am')
############ HELPER METHODS ###################
@@ -120,4 +126,4 @@ def save_section_name(name):
def see_my_section_on_the_courseware_page(name):
section_css = 'span.section-name-span'
assert_css_with_text(section_css, name)
assert_css_with_text(section_css, name)

View File

@@ -1,4 +1,5 @@
from lettuce import world, step
from common import *
@step('I fill in the registration form$')
@@ -13,10 +14,11 @@ def i_fill_in_the_registration_form(step):
@step('I press the Create My Account button on the registration form$')
def i_press_the_button_on_the_registration_form(step):
register_form = world.browser.find_by_css('form#register_form')
submit_css = 'button#submit'
register_form.find_by_css(submit_css).click()
submit_css = 'form#register_form button#submit'
# Workaround for click not working on ubuntu
# for some unknown reason.
e = css_find(submit_css)
e.type(' ')
@step('I should see be on the studio home page$')
def i_should_see_be_on_the_studio_home_page(step):

View File

@@ -21,6 +21,7 @@ Feature: Overview Toggle Section
Then I see the "Collapse All Sections" link
And all sections are expanded
@skip-phantom
Scenario: Collapse link is not removed after last section of a course is deleted
Given I have a course with 1 section
And I navigate to the course overview page

View File

@@ -17,6 +17,7 @@ Feature: Create Subsection
And I click to edit the subsection name
Then I see the complete subsection name with a quote in the editor
@skip-phantom
Scenario: Delete a subsection
Given I have opened a new course section in Studio
And I have added a new subsection

View File

@@ -9,10 +9,8 @@ from tempdir import mkdtemp_clean
import json
from fs.osfs import OSFS
import copy
from mock import Mock
from json import dumps, loads
from json import loads
from student.models import Registration
from django.contrib.auth.models import User
from cms.djangoapps.contentstore.utils import get_modulestore
@@ -22,12 +20,11 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore import Location
from xmodule.modulestore.store_utilities import clone_course
from xmodule.modulestore.store_utilities import delete_course
from xmodule.modulestore.django import modulestore, _MODULESTORES
from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore
from xmodule.templates import update_templates
from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.templates import update_templates
from xmodule.capa_module import CapaDescriptor
from xmodule.course_module import CourseDescriptor
@@ -63,7 +60,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.client = Client()
self.client.login(username=uname, password=password)
def check_edit_unit(self, test_course_name):
import_from_xml(modulestore(), 'common/test/data/', [test_course_name])
@@ -82,8 +78,8 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
def test_static_tab_reordering(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
ms = modulestore('direct')
course = ms.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
module_store = modulestore('direct')
course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
# reverse the ordering
reverse_tabs = []
@@ -91,9 +87,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
if tab['type'] == 'static_tab':
reverse_tabs.insert(0, 'i4x://edX/full/static_tab/{0}'.format(tab['url_slug']))
resp = self.client.post(reverse('reorder_static_tabs'), json.dumps({'tabs': reverse_tabs}), "application/json")
self.client.post(reverse('reorder_static_tabs'), json.dumps({'tabs': reverse_tabs}), "application/json")
course = ms.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
# compare to make sure that the tabs information is in the expected order after the server call
course_tabs = []
@@ -103,28 +99,60 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertEqual(reverse_tabs, course_tabs)
def test_delete(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
module_store = modulestore('direct')
sequential = module_store.get_item(Location(['i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None]))
chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter','Week_1', None]))
# make sure the parent no longer points to the child object which was deleted
self.assertTrue(sequential.location.url() in chapter.definition['children'])
self.client.post(reverse('delete_item'),
json.dumps({'id': sequential.location.url(), 'delete_children':'true', 'delete_all_versions':'true'}),
"application/json")
found = False
try:
module_store.get_item(Location(['i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None]))
found = True
except ItemNotFoundError:
pass
self.assertFalse(found)
chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter','Week_1', None]))
# make sure the parent no longer points to the child object which was deleted
self.assertFalse(sequential.location.url() in chapter.definition['children'])
def test_about_overrides(self):
'''
This test case verifies that a course can use specialized override for about data, e.g. /about/Fall_2012/effort.html
while there is a base definition in /about/effort.html
'''
import_from_xml(modulestore(), 'common/test/data/', ['full'])
ms = modulestore('direct')
effort = ms.get_item(Location(['i4x', 'edX', 'full', 'about', 'effort', None]))
module_store = modulestore('direct')
effort = module_store.get_item(Location(['i4x', 'edX', 'full', 'about', 'effort', None]))
self.assertEqual(effort.definition['data'], '6 hours')
# this one should be in a non-override folder
effort = ms.get_item(Location(['i4x', 'edX', 'full', 'about', 'end_date', None]))
effort = module_store.get_item(Location(['i4x', 'edX', 'full', 'about', 'end_date', None]))
self.assertEqual(effort.definition['data'], 'TBD')
def test_remove_hide_progress_tab(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
ms = modulestore('direct')
cs = contentstore()
module_store = modulestore('direct')
content_store = contentstore()
source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
course = ms.get_item(source_location)
course = module_store.get_item(source_location)
self.assertNotIn('hide_progress_tab', course.metadata)
def test_clone_course(self):
@@ -143,19 +171,19 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
data = parse_json(resp)
self.assertEqual(data['id'], 'i4x://MITx/999/course/Robot_Super_Course')
ms = modulestore('direct')
cs = contentstore()
module_store = modulestore('direct')
content_store = contentstore()
source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
dest_location = CourseDescriptor.id_to_location('MITx/999/Robot_Super_Course')
clone_course(ms, cs, source_location, dest_location)
clone_course(module_store, content_store, source_location, dest_location)
# now loop through all the units in the course and verify that the clone can render them, which
# means the objects are at least present
items = ms.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
items = module_store.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
self.assertGreater(len(items), 0)
clone_items = ms.get_items(Location(['i4x', 'MITx', '999', 'vertical', None]))
clone_items = module_store.get_items(Location(['i4x', 'MITx', '999', 'vertical', None]))
self.assertGreater(len(clone_items), 0)
for descriptor in items:
new_loc = descriptor.location._replace(org='MITx', course='999')
@@ -166,14 +194,14 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
def test_delete_course(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
ms = modulestore('direct')
cs = contentstore()
module_store = modulestore('direct')
content_store = contentstore()
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
delete_course(ms, cs, location, commit=True)
delete_course(module_store, content_store, location, commit=True)
items = ms.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
items = module_store.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
self.assertEqual(len(items), 0)
def verify_content_existence(self, modulestore, root_dir, location, dirname, category_name, filename_suffix=''):
@@ -188,10 +216,10 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertTrue(fs.exists(item.location.name + filename_suffix))
def test_export_course(self):
ms = modulestore('direct')
cs = contentstore()
module_store = modulestore('direct')
content_store = contentstore()
import_from_xml(ms, 'common/test/data/', ['full'])
import_from_xml(module_store, 'common/test/data/', ['full'])
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
root_dir = path(mkdtemp_clean())
@@ -199,43 +227,43 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
print 'Exporting to tempdir = {0}'.format(root_dir)
# export out to a tempdir
export_to_xml(ms, cs, location, root_dir, 'test_export')
export_to_xml(module_store, content_store, location, root_dir, 'test_export')
# check for static tabs
self.verify_content_existence(ms, root_dir, location, 'tabs', 'static_tab', '.html')
self.verify_content_existence(module_store, root_dir, location, 'tabs', 'static_tab', '.html')
# check for custom_tags
self.verify_content_existence(ms, root_dir, location, 'info', 'course_info', '.html')
self.verify_content_existence(module_store, root_dir, location, 'info', 'course_info', '.html')
# check for custom_tags
self.verify_content_existence(ms, root_dir, location, 'custom_tags', 'custom_tag_template')
self.verify_content_existence(module_store, root_dir, location, 'custom_tags', 'custom_tag_template')
# check for graiding_policy.json
fs = OSFS(root_dir / 'test_export/policies/6.002_Spring_2012')
self.assertTrue(fs.exists('grading_policy.json'))
course = ms.get_item(location)
course = module_store.get_item(location)
# compare what's on disk compared to what we have in our course
with fs.open('grading_policy.json','r') as grading_policy:
on_disk = loads(grading_policy.read())
with fs.open('grading_policy.json', 'r') as grading_policy:
on_disk = loads(grading_policy.read())
self.assertEqual(on_disk, course.definition['data']['grading_policy'])
#check for policy.json
self.assertTrue(fs.exists('policy.json'))
# compare what's on disk to what we have in the course module
with fs.open('policy.json','r') as course_policy:
with fs.open('policy.json', 'r') as course_policy:
on_disk = loads(course_policy.read())
self.assertIn('course/6.002_Spring_2012', on_disk)
self.assertEqual(on_disk['course/6.002_Spring_2012'], course.metadata)
# remove old course
delete_course(ms, cs, location)
delete_course(module_store, content_store, location)
# reimport
import_from_xml(ms, root_dir, ['test_export'])
import_from_xml(module_store, root_dir, ['test_export'])
items = ms.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
items = module_store.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
self.assertGreater(len(items), 0)
for descriptor in items:
print "Checking {0}....".format(descriptor.location.url())
@@ -245,11 +273,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
shutil.rmtree(root_dir)
def test_course_handouts_rewrites(self):
ms = modulestore('direct')
cs = contentstore()
module_store = modulestore('direct')
content_store = contentstore()
# import a test course
import_from_xml(ms, 'common/test/data/', ['full'])
import_from_xml(module_store, 'common/test/data/', ['full'])
handout_location = Location(['i4x', 'edX', 'full', 'course_info', 'handouts'])
@@ -263,7 +291,33 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# note, we know the link it should be because that's what in the 'full' course in the test data
self.assertContains(resp, '/c4x/edX/full/asset/handouts_schematic_tutorial.pdf')
def test_export_course_with_unknown_metadata(self):
module_store = modulestore('direct')
content_store = contentstore()
import_from_xml(module_store, 'common/test/data/', ['full'])
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
root_dir = path(mkdtemp_clean())
course = module_store.get_item(location)
# add a bool piece of unknown metadata so we can verify we don't throw an exception
course.metadata['new_metadata'] = True
module_store.update_metadata(location, course.metadata)
print 'Exporting to tempdir = {0}'.format(root_dir)
# export out to a tempdir
exported = False
try:
export_to_xml(module_store, content_store, location, root_dir, 'test_export')
exported = True
except Exception:
pass
self.assertTrue(exported)
class ContentStoreTest(ModuleStoreTestCase):
"""
@@ -402,7 +456,7 @@ class ContentStoreTest(ModuleStoreTestCase):
def test_capa_module(self):
"""Test that a problem treats markdown specially."""
course = CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
problem_data = {
'parent_location': 'i4x://MITx/999/course/Robot_Super_Course',
@@ -424,10 +478,10 @@ class ContentStoreTest(ModuleStoreTestCase):
def test_import_metadata_with_attempts_empty_string(self):
import_from_xml(modulestore(), 'common/test/data/', ['simple'])
ms = modulestore('direct')
module_store = modulestore('direct')
did_load_item = False
try:
ms.get_item(Location(['i4x', 'edX', 'simple', 'problem', 'ps01-simple', None]))
module_store.get_item(Location(['i4x', 'edX', 'simple', 'problem', 'ps01-simple', None]))
did_load_item = True
except ItemNotFoundError:
pass
@@ -438,10 +492,10 @@ class ContentStoreTest(ModuleStoreTestCase):
def test_metadata_inheritance(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
ms = modulestore('direct')
course = ms.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
module_store = modulestore('direct')
course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
verticals = ms.get_items(['i4x', 'edX', 'full', 'vertical', None, None])
verticals = module_store.get_items(['i4x', 'edX', 'full', 'vertical', None, None])
# let's assert on the metadata_inheritance on an existing vertical
for vertical in verticals:
@@ -452,15 +506,15 @@ class ContentStoreTest(ModuleStoreTestCase):
new_component_location = Location('i4x', 'edX', 'full', 'html', 'new_component')
source_template_location = Location('i4x', 'edx', 'templates', 'html', 'Blank_HTML_Page')
# crate a new module and add it as a child to a vertical
ms.clone_item(source_template_location, new_component_location)
module_store.clone_item(source_template_location, new_component_location)
parent = verticals[0]
ms.update_children(parent.location, parent.definition.get('children', []) + [new_component_location.url()])
module_store.update_children(parent.location, parent.definition.get('children', []) + [new_component_location.url()])
# flush the cache
ms.get_cached_metadata_inheritance_tree(new_component_location, -1)
new_module = ms.get_item(new_component_location)
module_store.get_cached_metadata_inheritance_tree(new_component_location, -1)
new_module = module_store.get_item(new_component_location)
# check for grace period definition which should be defined at the course level
self.assertIn('graceperiod', new_module.metadata)
@@ -473,11 +527,11 @@ class ContentStoreTest(ModuleStoreTestCase):
# now let's define an override at the leaf node level
#
new_module.metadata['graceperiod'] = '1 day'
ms.update_metadata(new_module.location, new_module.metadata)
module_store.update_metadata(new_module.location, new_module.metadata)
# flush the cache and refetch
ms.get_cached_metadata_inheritance_tree(new_component_location, -1)
new_module = ms.get_item(new_component_location)
module_store.get_cached_metadata_inheritance_tree(new_component_location, -1)
new_module = module_store.get_item(new_component_location)
self.assertIn('graceperiod', new_module.metadata)
self.assertEqual('1 day', new_module.metadata['graceperiod'])
@@ -486,15 +540,15 @@ class ContentStoreTest(ModuleStoreTestCase):
class TemplateTestCase(ModuleStoreTestCase):
def test_template_cleanup(self):
ms = modulestore('direct')
module_store = modulestore('direct')
# insert a bogus template in the store
bogus_template_location = Location('i4x', 'edx', 'templates', 'html', 'bogus')
source_template_location = Location('i4x', 'edx', 'templates', 'html', 'Blank_HTML_Page')
ms.clone_item(source_template_location, bogus_template_location)
verify_create = ms.get_item(bogus_template_location)
module_store.clone_item(source_template_location, bogus_template_location)
verify_create = module_store.get_item(bogus_template_location)
self.assertIsNotNone(verify_create)
# now run cleanup
@@ -503,10 +557,9 @@ class TemplateTestCase(ModuleStoreTestCase):
# now try to find dangling template, it should not be in DB any longer
asserted = False
try:
verify_create = ms.get_item(bogus_template_location)
verify_create = module_store.get_item(bogus_template_location)
except ItemNotFoundError:
asserted = True
self.assertTrue(asserted)
self.assertTrue(asserted)

View File

@@ -86,12 +86,14 @@ def signup(request):
csrf_token = csrf(request)['csrf_token']
return render_to_response('signup.html', {'csrf': csrf_token})
def old_login_redirect(request):
'''
Redirect to the active login url.
'''
return redirect('login', permanent=True)
@ssl_login_shortcut
@ensure_csrf_cookie
def login_page(request):
@@ -104,6 +106,7 @@ def login_page(request):
'forgot_password_link': "//{base}/#forgot-password-modal".format(base=settings.LMS_BASE),
})
def howitworks(request):
if request.user.is_authenticated():
return index(request)
@@ -112,6 +115,7 @@ def howitworks(request):
# ==== Views for any logged-in user ==================================
@login_required
@ensure_csrf_cookie
def index(request):
@@ -145,6 +149,7 @@ def index(request):
# ==== Views with per-item permissions================================
def has_access(user, location, role=STAFF_ROLE_NAME):
'''
Return True if user allowed to access this piece of data
@@ -393,6 +398,7 @@ def preview_component(request, location):
'editor': wrap_xmodule(component.get_html, component, 'xmodule_edit.html')(),
})
@expect_json
@login_required
@ensure_csrf_cookie
@@ -636,6 +642,17 @@ def delete_item(request):
if item.location.revision is None and item.location.category == 'vertical' and delete_all_versions:
modulestore('direct').delete_item(item.location)
# cdodge: we need to remove our parent's pointer to us so that it is no longer dangling
if delete_all_versions:
parent_locs = modulestore('direct').get_parent_locations(item_loc, None)
for parent_loc in parent_locs:
parent = modulestore('direct').get_item(parent_loc)
item_url = item_loc.url()
if item_url in parent.definition["children"]:
parent.definition["children"].remove(item_url)
modulestore('direct').update_children(parent.location, parent.definition["children"])
return HttpResponse()
@@ -709,6 +726,7 @@ def create_draft(request):
return HttpResponse()
@login_required
@expect_json
def publish_draft(request):
@@ -738,6 +756,7 @@ def unpublish_unit(request):
return HttpResponse()
@login_required
@expect_json
def clone_item(request):
@@ -768,8 +787,7 @@ def clone_item(request):
return HttpResponse(json.dumps({'id': dest_location.url()}))
#@login_required
#@ensure_csrf_cookie
def upload_asset(request, org, course, coursename):
'''
cdodge: this method allows for POST uploading of files into the course asset library, which will
@@ -831,6 +849,7 @@ def upload_asset(request, org, course, coursename):
response['asset_url'] = StaticContent.get_url_path_from_location(content.location)
return response
'''
This view will return all CMS users who are editors for the specified course
'''
@@ -863,6 +882,7 @@ def create_json_response(errmsg = None):
return resp
'''
This POST-back view will add a user - specified by email - to the list of editors for
the specified course
@@ -895,6 +915,7 @@ def add_user(request, location):
return create_json_response()
'''
This POST-back view will remove a user - specified by email - from the list of editors for
the specified course
@@ -926,6 +947,7 @@ def remove_user(request, location):
def landing(request, org, course, coursename):
return render_to_response('temp-course-landing.html', {})
@login_required
@ensure_csrf_cookie
def static_pages(request, org, course, coursename):
@@ -1029,6 +1051,7 @@ def edit_tabs(request, org, course, coursename):
'components': components
})
def not_found(request):
return render_to_response('error.html', {'error': '404'})
@@ -1064,6 +1087,7 @@ def course_info(request, org, course, name, provided_id=None):
'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url()
})
@expect_json
@login_required
@ensure_csrf_cookie
@@ -1161,6 +1185,7 @@ def get_course_settings(request, org, course, name):
"section": "details"})
})
@login_required
@ensure_csrf_cookie
def course_config_graders_page(request, org, course, name):
@@ -1184,6 +1209,7 @@ def course_config_graders_page(request, org, course, name):
'course_details': json.dumps(course_details, cls=CourseSettingsEncoder)
})
@login_required
@ensure_csrf_cookie
def course_config_advanced_page(request, org, course, name):
@@ -1207,6 +1233,7 @@ def course_config_advanced_page(request, org, course, name):
'advanced_dict' : json.dumps(CourseMetadata.fetch(location)),
})
@expect_json
@login_required
@ensure_csrf_cookie
@@ -1238,6 +1265,7 @@ def course_settings_updates(request, org, course, name, section):
return HttpResponse(json.dumps(manager.update_from_json(request.POST), cls=CourseSettingsEncoder),
mimetype="application/json")
@expect_json
@login_required
@ensure_csrf_cookie
@@ -1272,7 +1300,7 @@ def course_grader_updates(request, org, course, name, grader_index=None):
return HttpResponse(json.dumps(CourseGradingModel.update_grader_from_json(Location(['i4x', org, course, 'course', name]), request.POST)),
mimetype="application/json")
## NB: expect_json failed on ["key", "key2"] and json payload
@login_required
@ensure_csrf_cookie
@@ -1363,6 +1391,7 @@ def asset_index(request, org, course, name):
def edge(request):
return render_to_response('university_profiles/edge.html', {})
@login_required
@expect_json
def create_new_course(request):
@@ -1418,6 +1447,7 @@ def create_new_course(request):
return HttpResponse(json.dumps({'id': new_course.location.url()}))
def initialize_course_tabs(course):
# set up the default tabs
# I've added this because when we add static tabs, the LMS either expects a None for the tabs list or
@@ -1435,6 +1465,7 @@ def initialize_course_tabs(course):
modulestore('direct').update_metadata(course.location.url(), course.own_metadata)
@ensure_csrf_cookie
@login_required
def import_course(request, org, course, name):
@@ -1512,6 +1543,7 @@ def import_course(request, org, course, name):
course_module.location.name])
})
@ensure_csrf_cookie
@login_required
def generate_export_course(request, org, course, name):
@@ -1563,6 +1595,7 @@ def export_course(request, org, course, name):
'successful_import_redirect_url': ''
})
def event(request):
'''
A noop to swallow the analytics call so that cms methods don't spook and poor developers looking at

View File

@@ -10,7 +10,7 @@ class CourseMetadata(object):
'''
# __new_advanced_key__ is used by client not server; so, could argue against it being here
FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start', 'end', 'enrollment_start', 'enrollment_end', 'tabs', 'graceperiod', '__new_advanced_key__']
@classmethod
def fetch(cls, course_location):
"""
@@ -18,17 +18,17 @@ class CourseMetadata(object):
"""
if not isinstance(course_location, Location):
course_location = Location(course_location)
course = {}
descriptor = get_modulestore(course_location).get_item(course_location)
for k, v in descriptor.metadata.iteritems():
if k not in cls.FILTERED_LIST:
course[k] = v
return course
@classmethod
def update_from_json(cls, course_location, jsondict):
"""
@@ -37,7 +37,7 @@ class CourseMetadata(object):
Ensures none of the fields are in the blacklist.
"""
descriptor = get_modulestore(course_location).get_item(course_location)
dirty = False
for k, v in jsondict.iteritems():
@@ -45,26 +45,26 @@ class CourseMetadata(object):
if k not in cls.FILTERED_LIST and (k not in descriptor.metadata or descriptor.metadata[k] != v):
dirty = True
descriptor.metadata[k] = v
if dirty:
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
# Could just generate and return a course obj w/o doing any db reads, but I put the reads in as a means to confirm
# it persisted correctly
return cls.fetch(course_location)
@classmethod
def delete_key(cls, course_location, payload):
'''
Remove the given metadata key(s) from the course. payload can be a single key or [key..]
'''
descriptor = get_modulestore(course_location).get_item(course_location)
for key in payload['deleteKeys']:
if key in descriptor.metadata:
del descriptor.metadata[key]
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
return cls.fetch(course_location)

View File

@@ -62,3 +62,6 @@ AWS_SECRET_ACCESS_KEY = AUTH_TOKENS["AWS_SECRET_ACCESS_KEY"]
DATABASES = AUTH_TOKENS['DATABASES']
MODULESTORE = AUTH_TOKENS['MODULESTORE']
CONTENTSTORE = AUTH_TOKENS['CONTENTSTORE']
# Datadog for events!
DATADOG_API = AUTH_TOKENS.get("DATADOG_API")

View File

@@ -4,9 +4,6 @@ This config file runs the simplest dev environment"""
from .common import *
from logsettings import get_logger_config
import logging
import sys
DEBUG = True
TEMPLATE_DEBUG = DEBUG
LOGGING = get_logger_config(ENV_ROOT / "log",
@@ -107,3 +104,36 @@ CACHE_TIMEOUT = 0
# Dummy secret key for dev
SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd'
################################ DEBUG TOOLBAR #################################
INSTALLED_APPS += ('debug_toolbar', 'debug_toolbar_mongo')
MIDDLEWARE_CLASSES += ('django_comment_client.utils.QueryCountDebugMiddleware',
'debug_toolbar.middleware.DebugToolbarMiddleware',)
INTERNAL_IPS = ('127.0.0.1',)
DEBUG_TOOLBAR_PANELS = (
'debug_toolbar.panels.version.VersionDebugPanel',
'debug_toolbar.panels.timer.TimerDebugPanel',
'debug_toolbar.panels.settings_vars.SettingsVarsDebugPanel',
'debug_toolbar.panels.headers.HeaderDebugPanel',
'debug_toolbar.panels.request_vars.RequestVarsDebugPanel',
'debug_toolbar.panels.sql.SQLDebugPanel',
'debug_toolbar.panels.signals.SignalDebugPanel',
'debug_toolbar.panels.logger.LoggingPanel',
# This is breaking Mongo updates-- Christina is investigating.
# 'debug_toolbar_mongo.panel.MongoDebugPanel',
# Enabling the profiler has a weird bug as of django-debug-toolbar==0.9.4 and
# Django=1.3.1/1.4 where requests to views get duplicated (your method gets
# hit twice). So you can uncomment when you need to diagnose performance
# problems, but you shouldn't leave it on.
# 'debug_toolbar.panels.profiling.ProfilingDebugPanel',
)
DEBUG_TOOLBAR_CONFIG = {
'INTERCEPT_REDIRECTS': False
}
# To see stacktraces for MongoDB queries, set this to True.
# Stacktraces slow down page loads drastically (for pages with lots of queries).
# DEBUG_TOOLBAR_MONGO_STACKTRACES = False

6
cms/one_time_startup.py Normal file
View File

@@ -0,0 +1,6 @@
from dogapi import dog_http_api, dog_stats_api
from django.conf import settings
if hasattr(settings, 'DATADOG_API'):
dog_http_api.api_key = settings.DATADOG_API
dog_stats_api.start(api_key=settings.DATADOG_API, statsd=True)

View File

@@ -31,7 +31,8 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
// because these are outside of this.$el, they can't be in the event hash
$('.save-button').on('click', this, this.saveView);
$('.cancel-button').on('click', this, this.revertView);
this.model.on('error', this.handleValidationError, this);
this.listenTo(this.model, 'error', CMS.ServerError);
this.listenTo(this.model, 'invalid', this.handleValidationError);
},
render: function() {
// catch potential outside call before template loaded
@@ -228,7 +229,7 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
var error = {};
error[oldKey] = 'You have already defined "' + newKey + '" in the manual policy definitions.';
error[newKey] = "You tried to enter a duplicate of this key.";
this.model.trigger("error", this.model, error);
this.model.trigger("invalid", this.model, error);
return false;
}
@@ -244,7 +245,7 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
// swap to the key which the map knows about
validation[oldKey] = validation[newKey];
}
this.model.trigger("error", this.model, validation);
this.model.trigger("invalid", this.model, validation);
// abandon update
return;
}

View File

@@ -26,7 +26,8 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
var dateIntrospect = new Date();
this.$el.find('#timezone').html("(" + dateIntrospect.getTimezone() + ")");
this.model.on('error', this.handleValidationError, this);
this.listenTo(this.model, 'error', CMS.ServerError);
this.listenTo(this.model, 'invalid', this.handleValidationError);
this.selectorToField = _.invert(this.fieldToSelectorMap);
},

View File

@@ -44,7 +44,8 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
self.render();
}
);
this.model.on('error', this.handleValidationError, this);
this.listenTo(this.model, 'error', CMS.ServerError);
this.listenTo(this.model, 'invalid', this.handleValidationError);
this.model.get('graders').on('remove', this.render, this);
this.model.get('graders').on('reset', this.render, this);
this.model.get('graders').on('add', this.render, this);
@@ -316,7 +317,8 @@ CMS.Views.Settings.GraderView = CMS.Views.ValidatingView.extend({
'blur :input' : "inputUnfocus"
},
initialize : function() {
this.model.on('error', this.handleValidationError, this);
this.listenTo(this.model, 'error', CMS.ServerError);
this.listenTo(this.model, 'invalid', this.handleValidationError);
this.selectorToField = _.invert(this.fieldToSelectorMap);
this.render();
},

View File

@@ -3,7 +3,8 @@ CMS.Views.ValidatingView = Backbone.View.extend({
// decorates the fields. Needs wiring per class, but this initialization shows how
// either have your init call this one or copy the contents
initialize : function() {
this.model.on('error', this.handleValidationError, this);
this.listenTo(this.model, 'error', CMS.ServerError);
this.listenTo(this.model, 'invalid', this.handleValidationError);
this.selectorToField = _.invert(this.fieldToSelectorMap);
},
@@ -18,20 +19,11 @@ CMS.Views.ValidatingView = Backbone.View.extend({
// which may be the subjects of validation errors
},
_cacheValidationErrors : [],
handleValidationError : function(model, error) {
// error triggered either by validation or server error
// error is object w/ fields and error strings
for (var field in error) {
var ele = this.$el.find('#' + this.fieldToSelectorMap[field]);
if (ele.length === 0) {
// check if it might a server error: note a typo in the field name
// or failure to put in a map may cause this to muffle validation errors
if (_.has(error, 'error') && _.has(error, 'responseText')) {
CMS.ServerError(model, error);
return;
}
else continue;
}
this._cacheValidationErrors.push(ele);
if ($(ele).is('div')) {
// put error on the contained inputs

View File

@@ -28,7 +28,7 @@
{{uploadDate}}
</td>
<td class="embed-col">
<input type="text" class="embeddable-xml-input" value='{{url}}' disabled>
<input type="text" class="embeddable-xml-input" value='{{url}}' readonly>
</td>
</tr>
</script>
@@ -84,7 +84,7 @@
${asset['uploadDate']}
</td>
<td class="embed-col">
<input type="text" class="embeddable-xml-input" value="${asset['url']}" disabled>
<input type="text" class="embeddable-xml-input" value="${asset['url']}" readonly>
</td>
</tr>
% endfor
@@ -115,7 +115,7 @@
</div>
<div class="embeddable">
<label>URL:</label>
<input type="text" class="embeddable-xml-input" value='' disabled>
<input type="text" class="embeddable-xml-input" value='' readonly>
</div>
<form class="file-chooser" action="${upload_asset_callback_url}"
method="post" enctype="multipart/form-data">

View File

@@ -1,5 +1,6 @@
from django.conf import settings
from django.conf.urls import patterns, include, url
from . import one_time_startup
# Uncomment the next two lines to enable the admin:
# from django.contrib import admin

View File

@@ -65,23 +65,23 @@ def is_commentable_cohorted(course_id, commentable_id):
ans))
return ans
def get_cohorted_commentables(course_id):
"""
Given a course_id return a list of strings representing cohorted commentables
"""
course = courses.get_course_by_id(course_id)
if not course.is_cohorted:
# this is the easy case :)
ans = []
else:
else:
ans = course.cohorted_discussions
return ans
def get_cohort(user, course_id):
"""
Given a django User and a course_id, return the user's cohort in that
@@ -120,7 +120,8 @@ def get_cohort(user, course_id):
return None
choices = course.auto_cohort_groups
if len(choices) == 0:
n = len(choices)
if n == 0:
# Nowhere to put user
log.warning("Course %s is auto-cohorted, but there are no"
" auto_cohort_groups specified",
@@ -128,12 +129,19 @@ def get_cohort(user, course_id):
return None
# Put user in a random group, creating it if needed
group_name = random.choice(choices)
choice = random.randrange(0, n)
group_name = choices[choice]
# Victor: we are seeing very strange behavior on prod, where almost all users
# end up in the same group. Log at INFO to try to figure out what's going on.
log.info("DEBUG: adding user {0} to cohort {1}. choice={2}".format(
user, group_name,choice))
group, created = CourseUserGroup.objects.get_or_create(
course_id=course_id,
group_type=CourseUserGroup.COHORT,
name=group_name)
user.course_groups.add(group)
return group

View File

@@ -6,7 +6,7 @@ from django.test.utils import override_settings
from course_groups.models import CourseUserGroup
from course_groups.cohorts import (get_cohort, get_course_cohorts,
is_commentable_cohorted)
is_commentable_cohorted, get_cohort_by_name)
from xmodule.modulestore.django import modulestore, _MODULESTORES
@@ -168,7 +168,7 @@ class TestCohorts(django.test.TestCase):
self.assertEquals(get_cohort(user3, course.id), None,
"No groups->no auto-cohorting")
# Now make it different
self.config_course_cohorts(course, [], cohorted=True,
auto_cohort=True,
@@ -180,6 +180,37 @@ class TestCohorts(django.test.TestCase):
"user2 should still be in originally placed cohort")
def test_auto_cohorting_randomization(self):
"""
Make sure get_cohort() randomizes properly.
"""
course = modulestore().get_course("edX/toy/2012_Fall")
self.assertEqual(course.id, "edX/toy/2012_Fall")
self.assertFalse(course.is_cohorted)
groups = ["group_{0}".format(n) for n in range(5)]
self.config_course_cohorts(course, [], cohorted=True,
auto_cohort=True,
auto_cohort_groups=groups)
# Assign 100 users to cohorts
for i in range(100):
user = User.objects.create(username="test_{0}".format(i),
email="a@b{0}.com".format(i))
get_cohort(user, course.id)
# Now make sure that the assignment was at least vaguely random:
# each cohort should have at least 1, and fewer than 50 students.
# (with 5 groups, probability of 0 users in any group is about
# .8**100= 2.0e-10)
for cohort_name in groups:
cohort = get_cohort_by_name(course.id, cohort_name)
num_users = cohort.users.count()
self.assertGreater(num_users, 1)
self.assertLess(num_users, 50)
def test_get_course_cohorts(self):
course1_id = 'a/b/c'
course2_id = 'e/f/g'

View File

@@ -2,8 +2,9 @@ import json
from datetime import datetime
from django.http import HttpResponse
from xmodule.modulestore.django import modulestore
from dogapi import dog_stats_api
@dog_stats_api.timed('edxapp.heartbeat')
def heartbeat(request):
"""
Simple view that a loadbalancer can check to verify that the app is up

View File

@@ -84,12 +84,19 @@ def replace_static_urls(text, data_directory, course_namespace=None):
if rest.endswith('?raw'):
return original
# course_namespace is not None, then use studio style urls
if course_namespace is not None and not isinstance(modulestore(), XMLModuleStore):
url = StaticContent.convert_legacy_static_url(rest, course_namespace)
# In debug mode, if we can find the url as is,
elif settings.DEBUG and finders.find(rest, True):
if settings.DEBUG and finders.find(rest, True):
return original
# if we're running with a MongoBacked store course_namespace is not None, then use studio style urls
elif course_namespace is not None and not isinstance(modulestore(), XMLModuleStore):
# first look in the static file pipeline and see if we are trying to reference
# a piece of static content which is in the mitx repo (e.g. JS associated with an xmodule)
if staticfiles_storage.exists(rest):
url = staticfiles_storage.url(rest)
else:
# if not, then assume it's courseware specific content and then look in the
# Mongo-backed database
url = StaticContent.convert_legacy_static_url(rest, course_namespace)
# Otherwise, look the file up in staticfiles_storage, and append the data directory if needed
else:
course_path = "/".join((data_directory, rest))

View File

@@ -10,6 +10,7 @@ import paramiko
import boto
dog_http_api.api_key = settings.DATADOG_API
dog_stats_api.start(api_key=settings.DATADOG_API, statsd=True)
class Command(BaseCommand):

View File

@@ -13,6 +13,7 @@ from django.core.management import call_command
def initial_setup(server):
# Launch the browser app (choose one of these below)
world.browser = Browser('chrome')
# world.browser = Browser('phantomjs')
# world.browser = Browser('firefox')

View File

@@ -183,7 +183,7 @@ def evaluator(variables, functions, string, cs=False):
# 0.33k or -17
number = (Optional(minus | plus) + inner_number
+ Optional(CaselessLiteral("E") + Optional("-") + number_part)
+ Optional(CaselessLiteral("E") + Optional((plus | minus)) + number_part)
+ Optional(number_suffix))
number = number.setParseAction(number_parse_action) # Convert to number

View File

@@ -29,6 +29,7 @@ import sys
from lxml import etree
from xml.sax.saxutils import unescape
from copy import deepcopy
import chem
import chem.chemcalc
@@ -146,6 +147,13 @@ class LoncapaProblem(object):
if not self.student_answers: # True when student_answers is an empty dict
self.set_initial_display()
# dictionary of InputType objects associated with this problem
# input_id string -> InputType object
self.inputs = {}
self.extracted_tree = self._extract_html(self.tree)
def do_reset(self):
'''
Reset internal state to unfinished, with no answers
@@ -324,7 +332,27 @@ class LoncapaProblem(object):
'''
Main method called externally to get the HTML to be rendered for this capa Problem.
'''
return contextualize_text(etree.tostring(self._extract_html(self.tree)), self.context)
html = contextualize_text(etree.tostring(self._extract_html(self.tree)), self.context)
return html
def handle_input_ajax(self, get):
'''
InputTypes can support specialized AJAX calls. Find the correct input and pass along the correct data
Also, parse out the dispatch from the get so that it can be passed onto the input type nicely
'''
# pull out the id
input_id = get['input_id']
if self.inputs[input_id]:
dispatch = get['dispatch']
return self.inputs[input_id].handle_ajax(dispatch, get)
else:
log.warning("Could not find matching input for id: %s" % problem_id)
return {}
# ======= Private Methods Below ========
@@ -458,6 +486,8 @@ class LoncapaProblem(object):
finally:
sys.path = original_path
def _extract_html(self, problemtree): # private
'''
Main (private) function which converts Problem XML tree to HTML.
@@ -471,7 +501,7 @@ class LoncapaProblem(object):
if (problemtree.tag == 'script' and problemtree.get('type')
and 'javascript' in problemtree.get('type')):
# leave javascript intact.
return problemtree
return deepcopy(problemtree)
if problemtree.tag in html_problem_semantics:
return
@@ -484,8 +514,9 @@ class LoncapaProblem(object):
msg = ''
hint = ''
hintmode = None
input_id = problemtree.get('id')
if problemid in self.correct_map:
pid = problemtree.get('id')
pid = input_id
status = self.correct_map.get_correctness(pid)
msg = self.correct_map.get_msg(pid)
hint = self.correct_map.get_hint(pid)
@@ -496,17 +527,17 @@ class LoncapaProblem(object):
value = self.student_answers[problemid]
# do the rendering
state = {'value': value,
'status': status,
'id': problemtree.get('id'),
'id': input_id,
'feedback': {'message': msg,
'hint': hint,
'hintmode': hintmode, }}
input_type_cls = inputtypes.registry.get_class_for_tag(problemtree.tag)
the_input = input_type_cls(self.system, problemtree, state)
return the_input.get_html()
# save the input type so that we can make ajax calls on it if we need to
self.inputs[input_id] = input_type_cls(self.system, problemtree, state)
return self.inputs[input_id].get_html()
# let each Response render itself
if problemtree in self.responders:

View File

@@ -95,7 +95,7 @@ class CorrectMap(object):
def is_correct(self, answer_id):
if answer_id in self.cmap:
return self.cmap[answer_id]['correctness'] == 'correct'
return self.cmap[answer_id]['correctness'] in ['correct', 'partially-correct']
return None
def is_queued(self, answer_id):
@@ -111,15 +111,14 @@ class CorrectMap(object):
return None
def get_npoints(self, answer_id):
""" Return the number of points for an answer:
If the answer is correct, return the assigned
number of points (default: 1 point)
Otherwise, return 0 points """
if self.is_correct(answer_id):
npoints = self.get_property(answer_id, 'npoints')
return npoints if npoints is not None else 1
else:
return 0
"""Return the number of points for an answer, used for partial credit."""
npoints = self.get_property(answer_id, 'npoints')
if npoints is not None:
return npoints
elif self.is_correct(answer_id):
return 1
# if not correct and no points have been assigned, return 0
return 0
def set_property(self, answer_id, property, value):
if answer_id in self.cmap:

View File

@@ -45,8 +45,10 @@ import re
import shlex # for splitting quoted strings
import sys
import os
import pyparsing
from registry import TagRegistry
from capa.chem import chemcalc
log = logging.getLogger('mitx.' + __name__)
@@ -215,6 +217,18 @@ class InputTypeBase(object):
"""
pass
def handle_ajax(self, dispatch, get):
"""
InputTypes that need to handle specialized AJAX should override this.
Input:
dispatch: a string that can be used to determine how to handle the data passed in
get: a dictionary containing the data that was sent with the ajax call
Output:
a dictionary object that can be serialized into JSON. This will be sent back to the Javascript.
"""
pass
def _get_render_context(self):
"""
@@ -352,6 +366,12 @@ class ChoiceGroup(InputTypeBase):
self.choices = self.extract_choices(self.xml)
@classmethod
def get_attributes(cls):
return [Attribute("show_correctness", "always"),
Attribute("submitted_message", "Answer received.")]
def _extra_context(self):
return {'input_type': self.html_input_type,
'choices': self.choices,
@@ -740,6 +760,45 @@ class ChemicalEquationInput(InputTypeBase):
"""
return {'previewer': '/static/js/capa/chemical_equation_preview.js', }
def handle_ajax(self, dispatch, get):
'''
Since we only have chemcalc preview this input, check to see if it
matches the corresponding dispatch and send it through if it does
'''
if dispatch == 'preview_chemcalc':
return self.preview_chemcalc(get)
return {}
def preview_chemcalc(self, get):
"""
Render an html preview of a chemical formula or equation. get should
contain a key 'formula' and value 'some formula string'.
Returns a json dictionary:
{
'preview' : 'the-preview-html' or ''
'error' : 'the-error' or ''
}
"""
result = {'preview': '',
'error': ''}
formula = get['formula']
if formula is None:
result['error'] = "No formula specified."
return result
try:
result['preview'] = chemcalc.render_to_html(formula)
except pyparsing.ParseException as p:
result['error'] = "Couldn't parse formula: {0}".format(p)
except Exception:
# this is unexpected, so log
log.warning("Error while previewing chemical formula", exc_info=True)
result['error'] = "Error while rendering preview"
return result
registry.register(ChemicalEquationInput)
#-----------------------------------------------------------------------------
@@ -909,33 +968,142 @@ registry.register(DesignProtein2dInput)
class EditAGeneInput(InputTypeBase):
"""
An input type for editing a gene. Integrates with the genex java applet.
Example:
<editagene width="800" hight="500" dna_sequence="ETAAGGCTATAACCGA" />
"""
template = "editageneinput.html"
tags = ['editageneinput']
@classmethod
def get_attributes(cls):
"""
Note: width, hight, and dna_sequencee are required.
"""
Note: width, height, and dna_sequencee are required.
"""
return [Attribute('width'),
Attribute('height'),
Attribute('dna_sequence')
Attribute('dna_sequence'),
Attribute('genex_problem_number')
]
def _extra_context(self):
"""
"""
context = {
'applet_loader': '/static/js/capa/edit-a-gene.js',
}
return context
registry.register(EditAGeneInput)
#---------------------------------------------------------------------
class AnnotationInput(InputTypeBase):
"""
Input type for annotations: students can enter some notes or other text
(currently ungraded), and then choose from a set of tags/optoins, which are graded.
Example:
<annotationinput>
<title>Annotation Exercise</title>
<text>
They are the ones who, at the public assembly, had put savage derangement [ate] into my thinking
[phrenes] |89 on that day when I myself deprived Achilles of his honorific portion [geras]
</text>
<comment>Agamemnon says that ate or 'derangement' was the cause of his actions: why could Zeus say the same thing?</comment>
<comment_prompt>Type a commentary below:</comment_prompt>
<tag_prompt>Select one tag:</tag_prompt>
<options>
<option choice="correct">ate - both a cause and an effect</option>
<option choice="incorrect">ate - a cause</option>
<option choice="partially-correct">ate - an effect</option>
</options>
</annotationinput>
# TODO: allow ordering to be randomized
"""
template = "annotationinput.html"
tags = ['annotationinput']
def setup(self):
xml = self.xml
self.debug = False # set to True to display extra debug info with input
self.return_to_annotation = True # return only works in conjunction with annotatable xmodule
self.title = xml.findtext('./title', 'Annotation Exercise')
self.text = xml.findtext('./text')
self.comment = xml.findtext('./comment')
self.comment_prompt = xml.findtext('./comment_prompt', 'Type a commentary below:')
self.tag_prompt = xml.findtext('./tag_prompt', 'Select one tag:')
self.options = self._find_options()
# Need to provide a value that JSON can parse if there is no
# student-supplied value yet.
if self.value == '':
self.value = 'null'
self._validate_options()
def _find_options(self):
''' Returns an array of dicts where each dict represents an option. '''
elements = self.xml.findall('./options/option')
return [{
'id': index,
'description': option.text,
'choice': option.get('choice')
} for (index, option) in enumerate(elements) ]
def _validate_options(self):
''' Raises a ValueError if the choice attribute is missing or invalid. '''
valid_choices = ('correct', 'partially-correct', 'incorrect')
for option in self.options:
choice = option['choice']
if choice is None:
raise ValueError('Missing required choice attribute.')
elif choice not in valid_choices:
raise ValueError('Invalid choice attribute: {0}. Must be one of: {1}'.format(choice, ', '.join(valid_choices)))
def _unpack(self, json_value):
''' Unpacks the json input state into a dict. '''
d = json.loads(json_value)
if type(d) != dict:
d = {}
comment_value = d.get('comment', '')
if not isinstance(comment_value, basestring):
comment_value = ''
options_value = d.get('options', [])
if not isinstance(options_value, list):
options_value = []
return {
'options_value': options_value,
'has_options_value': len(options_value) > 0, # for convenience
'comment_value': comment_value,
}
def _extra_context(self):
extra_context = {
'title': self.title,
'text': self.text,
'comment': self.comment,
'comment_prompt': self.comment_prompt,
'tag_prompt': self.tag_prompt,
'options': self.options,
'return_to_annotation': self.return_to_annotation,
'debug': self.debug
}
extra_context.update(self._unpack(self.value))
return extra_context
registry.register(AnnotationInput)

View File

@@ -911,7 +911,8 @@ def sympy_check2():
allowed_inputfields = ['textline', 'textbox', 'crystallography',
'chemicalequationinput', 'vsepr_input',
'drag_and_drop_input', 'editamoleculeinput',
'designprotein2dinput', 'editageneinput']
'designprotein2dinput', 'editageneinput',
'annotationinput']
def setup_response(self):
xml = self.xml
@@ -1943,6 +1944,117 @@ class ImageResponse(LoncapaResponse):
dict([(ie.get('id'), ie.get('regions')) for ie in self.ielements]))
#-----------------------------------------------------------------------------
class AnnotationResponse(LoncapaResponse):
'''
Checking of annotation responses.
The response contains both a comment (student commentary) and an option (student tag).
Only the tag is currently graded. Answers may be incorrect, partially correct, or correct.
'''
response_tag = 'annotationresponse'
allowed_inputfields = ['annotationinput']
max_inputfields = 1
default_scoring = {'incorrect': 0, 'partially-correct': 1, 'correct': 2 }
def setup_response(self):
xml = self.xml
self.scoring_map = self._get_scoring_map()
self.answer_map = self._get_answer_map()
self.maxpoints = self._get_max_points()
def get_score(self, student_answers):
''' Returns a CorrectMap for the student answer, which may include
partially correct answers.'''
student_answer = student_answers[self.answer_id]
student_option = self._get_submitted_option_id(student_answer)
scoring = self.scoring_map[self.answer_id]
is_valid = student_option is not None and student_option in scoring.keys()
(correctness, points) = ('incorrect', None)
if is_valid:
correctness = scoring[student_option]['correctness']
points = scoring[student_option]['points']
return CorrectMap(self.answer_id, correctness=correctness, npoints=points)
def get_answers(self):
return self.answer_map
def _get_scoring_map(self):
''' Returns a dict of option->scoring for each input. '''
scoring = self.default_scoring
choices = dict([(choice,choice) for choice in scoring])
scoring_map = {}
for inputfield in self.inputfields:
option_scoring = dict([(option['id'], {
'correctness': choices.get(option['choice']),
'points': scoring.get(option['choice'])
}) for option in self._find_options(inputfield) ])
scoring_map[inputfield.get('id')] = option_scoring
return scoring_map
def _get_answer_map(self):
''' Returns a dict of answers for each input.'''
answer_map = {}
for inputfield in self.inputfields:
correct_option = self._find_option_with_choice(inputfield, 'correct')
if correct_option is not None:
answer_map[inputfield.get('id')] = correct_option.get('description')
return answer_map
def _get_max_points(self):
''' Returns a dict of the max points for each input: input id -> maxpoints. '''
scoring = self.default_scoring
correct_points = scoring.get('correct')
return dict([(inputfield.get('id'), correct_points) for inputfield in self.inputfields])
def _find_options(self, inputfield):
''' Returns an array of dicts where each dict represents an option. '''
elements = inputfield.findall('./options/option')
return [{
'id': index,
'description': option.text,
'choice': option.get('choice')
} for (index, option) in enumerate(elements) ]
def _find_option_with_choice(self, inputfield, choice):
''' Returns the option with the given choice value, otherwise None. '''
for option in self._find_options(inputfield):
if option['choice'] == choice:
return option
def _unpack(self, json_value):
''' Unpacks a student response value submitted as JSON.'''
d = json.loads(json_value)
if type(d) != dict:
d = {}
comment_value = d.get('comment', '')
if not isinstance(d, basestring):
comment_value = ''
options_value = d.get('options', [])
if not isinstance(options_value, list):
options_value = []
return {
'options_value': options_value,
'comment_value': comment_value
}
def _get_submitted_option_id(self, student_answer):
''' Return the single option that was selected, otherwise None.'''
submitted = self._unpack(student_answer)
option_ids = submitted['options_value']
if len(option_ids) == 1:
return option_ids[0]
return None
#-----------------------------------------------------------------------------
# TEMPORARY: List of all response subclasses
# FIXME: To be replaced by auto-registration
@@ -1959,4 +2071,5 @@ __all__ = [CodeResponse,
ChoiceResponse,
MultipleChoiceResponse,
TrueFalseResponse,
JavascriptResponse]
JavascriptResponse,
AnnotationResponse]

View File

@@ -0,0 +1,70 @@
<form class="annotation-input">
<div class="script_placeholder" data-src="/static/js/capa/annotationinput.js"/>
<div class="annotation-header">
${title}
% if return_to_annotation:
<a class="annotation-return" href="javascript:void(0)">Return to Annotation</a><br/>
% endif
</div>
<div class="annotation-body">
<div class="block block-highlight">${text}</div>
<div class="block block-comment">${comment}</div>
<div class="block">${comment_prompt}</div>
<textarea class="comment" id="input_${id}_comment" name="input_${id}_comment">${comment_value|h}</textarea>
<div class="block">${tag_prompt}</div>
<ul class="tags">
% for option in options:
<li>
% if has_options_value:
% if all([c == 'correct' for c in option['choice'], status]):
<span class="tag-status correct" id="status_${id}"></span>
% elif all([c == 'partially-correct' for c in option['choice'], status]):
<span class="tag-status partially-correct" id="status_${id}"></span>
% elif all([c == 'incorrect' for c in option['choice'], status]):
<span class="tag-status incorrect" id="status_${id}"></span>
% endif
% endif
<span class="tag
% if option['id'] in options_value:
selected
% endif
" data-id="${option['id']}">
${option['description']}
</span>
</li>
% endfor
</ul>
% if debug:
<div class="debug-value">
Rendered with value:<br/>
<pre>${value|h}</pre>
Current input value:<br/>
<input type="text" class="value" name="input_${id}" id="input_${id}" value="${value|h}" />
</div>
% else:
<input type="hidden" class="value" name="input_${id}" id="input_${id}" value="${value|h}" />
% endif
% if status == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
% elif status == 'incomplete':
<span class="incorrect" id="status_${id}"></span>
% elif status == 'incorrect' and not has_options_value:
<span class="incorrect" id="status_${id}"></span>
% endif
<p id="answer_${id}" class="answer answer-annotation"></p>
</div>
</form>
% if msg:
<span class="message">${msg|n}</span>
% endif

View File

@@ -11,7 +11,7 @@
<div class="incorrect" id="status_${id}">
% endif
<input type="text" name="input_${id}" id="input_${id}" value="${value|h}"
<input type="text" name="input_${id}" id="input_${id}" data-input-id="${id}" value="${value|h}"
% if size:
size="${size}"
% endif

View File

@@ -1,37 +1,47 @@
<form class="choicegroup capa_inputtype" id="inputtype_${id}">
<fieldset>
% for choice_id, choice_description in choices:
<label for="input_${id}_${choice_id}">
% if choice_id in value:
<span class="indicator_container">
% if status == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
% elif status == 'correct':
<span class="correct" id="status_${id}"></span>
% elif status == 'incorrect':
<span class="incorrect" id="status_${id}"></span>
% elif status == 'incomplete':
<span class="incorrect" id="status_${id}"></span>
% endif
</span>
% else:
<span class="indicator_container">&#160;</span>
<div class="indicator_container">
% if input_type == 'checkbox' or not value:
% if status == 'unsubmitted' or show_correctness == 'never':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
% elif status == 'correct':
<span class="correct" id="status_${id}"></span>
% elif status == 'incorrect':
<span class="incorrect" id="status_${id}"></span>
% elif status == 'incomplete':
<span class="incorrect" id="status_${id}"></span>
% endif
<input type="${input_type}" name="input_${id}${name_array_suffix}" id="input_${id}_${choice_id}" value="${choice_id}"
% if choice_id in value:
checked="true"
% endif
/>
${choice_description}
</div>
</label>
<fieldset>
% endfor
<span id="answer_${id}"></span>
</fieldset>
% for choice_id, choice_description in choices:
<label for="input_${id}_${choice_id}"
% if input_type == 'radio' and choice_id in value:
<%
if status == 'correct':
correctness = 'correct'
elif status == 'incorrect':
correctness = 'incorrect'
else:
correctness = None
%>
% if correctness and not show_correctness=='never':
class="choicegroup_${correctness}"
% endif
% endif
>
<input type="${input_type}" name="input_${id}${name_array_suffix}" id="input_${id}_${choice_id}" value="${choice_id}"
% if choice_id in value:
checked="true"
% endif
/> ${choice_description} </label>
% endfor
<span id="answer_${id}"></span>
</fieldset>
% if show_correctness == "never" and (value or status not in ['unsubmitted']):
<div class="capa_alert">${submitted_message}</div>
%endif
</form>

View File

@@ -1,4 +1,5 @@
<section id="editageneinput_${id}" class="editageneinput">
<section id="editageneinput_${id}" class="editageneinput">
<div class="script_placeholder" data-src="/static/js/capa/genex/genex.nocache.js?raw"/>
<div class="script_placeholder" data-src="${applet_loader}"/>
% if status == 'unsubmitted':
@@ -8,16 +9,12 @@
% elif status == 'incorrect':
<div class="incorrect" id="status_${id}">
% elif status == 'incomplete':
<div class="incorrect" id="status_${id}">
<div class="incomplete" id="status_${id}">
% endif
<object type="application/x-java-applet" id="applet_${id}" class="applet" width="${width}" height="${height}">
<param name="archive" value="/static/applets/capa/genex.jar" />
<param name="code" value="GX.GenexApplet.class" />
<param name="DNA_SEQUENCE" value="${dna_sequence}" />
Applet failed to run. No Java plug-in was found.
</object>
<div id="genex_container"></div>
<input type="hidden" name="dna_sequence" id="dna_sequence" value ="${dna_sequence}"></input>
<input type="hidden" name="genex_problem_number" id="genex_problem_number" value ="${genex_problem_number}"></input>
<input type="hidden" name="input_${id}" id="input_${id}" value="${value|h}"/>
<p class="status">
@@ -37,3 +34,4 @@
</div>
% endif
</section>

View File

@@ -666,3 +666,36 @@ class StringResponseXMLFactory(ResponseXMLFactory):
def create_input_element(self, **kwargs):
return ResponseXMLFactory.textline_input_xml(**kwargs)
class AnnotationResponseXMLFactory(ResponseXMLFactory):
""" Factory for creating <annotationresponse> XML trees """
def create_response_element(self, **kwargs):
""" Create a <annotationresponse> element """
return etree.Element("annotationresponse")
def create_input_element(self, **kwargs):
""" Create a <annotationinput> element."""
input_element = etree.Element("annotationinput")
text_children = [
{'tag': 'title', 'text': kwargs.get('title', 'super cool annotation') },
{'tag': 'text', 'text': kwargs.get('text', 'texty text') },
{'tag': 'comment', 'text':kwargs.get('comment', 'blah blah erudite comment blah blah') },
{'tag': 'comment_prompt', 'text': kwargs.get('comment_prompt', 'type a commentary below') },
{'tag': 'tag_prompt', 'text': kwargs.get('tag_prompt', 'select one tag') }
]
for child in text_children:
etree.SubElement(input_element, child['tag']).text = child['text']
default_options = [('green', 'correct'),('eggs', 'incorrect'),('ham', 'partially-correct')]
options = kwargs.get('options', default_options)
options_element = etree.SubElement(input_element, 'options')
for (description, correctness) in options:
option_element = etree.SubElement(options_element, 'option', {'choice': correctness})
option_element.text = description
return input_element

View File

@@ -91,12 +91,12 @@ class CorrectMapTest(unittest.TestCase):
npoints=0)
# Assert that we get the expected points
# If points assigned and correct --> npoints
# If points assigned --> npoints
# If no points assigned and correct --> 1 point
# Otherwise --> 0 points
# If no points assigned and incorrect --> 0 points
self.assertEqual(self.cmap.get_npoints('1_2_1'), 5)
self.assertEqual(self.cmap.get_npoints('2_2_1'), 1)
self.assertEqual(self.cmap.get_npoints('3_2_1'), 0)
self.assertEqual(self.cmap.get_npoints('3_2_1'), 5)
self.assertEqual(self.cmap.get_npoints('4_2_1'), 0)
self.assertEqual(self.cmap.get_npoints('5_2_1'), 0)

View File

@@ -3,6 +3,7 @@ from lxml import etree
import os
import textwrap
import json
import mock
from capa.capa_problem import LoncapaProblem
@@ -11,6 +12,20 @@ from . import test_system
class CapaHtmlRenderTest(unittest.TestCase):
def test_blank_problem(self):
"""
It's important that blank problems don't break, since that's
what you start with in studio.
"""
xml_str = "<problem> </problem>"
# Create the problem
problem = LoncapaProblem(xml_str, '1', system=test_system)
# Render the HTML
rendered_html = etree.XML(problem.get_html())
# expect that we made it here without blowing up
def test_include_html(self):
# Create a test file to include
self._create_test_file('test_include.xml',
@@ -25,7 +40,7 @@ class CapaHtmlRenderTest(unittest.TestCase):
# Create the problem
problem = LoncapaProblem(xml_str, '1', system=test_system)
# Render the HTML
rendered_html = etree.XML(problem.get_html())
@@ -35,6 +50,8 @@ class CapaHtmlRenderTest(unittest.TestCase):
self.assertEqual(test_element.text, "Test include")
def test_process_outtext(self):
# Generate some XML with <startouttext /> and <endouttext />
xml_str = textwrap.dedent("""
@@ -45,7 +62,7 @@ class CapaHtmlRenderTest(unittest.TestCase):
# Create the problem
problem = LoncapaProblem(xml_str, '1', system=test_system)
# Render the HTML
rendered_html = etree.XML(problem.get_html())
@@ -64,7 +81,7 @@ class CapaHtmlRenderTest(unittest.TestCase):
# Create the problem
problem = LoncapaProblem(xml_str, '1', system=test_system)
# Render the HTML
rendered_html = etree.XML(problem.get_html())
@@ -72,6 +89,25 @@ class CapaHtmlRenderTest(unittest.TestCase):
script_element = rendered_html.find('script')
self.assertEqual(None, script_element)
def test_render_javascript(self):
# Generate some XML with a <script> tag
xml_str = textwrap.dedent("""
<problem>
<script type="text/javascript">function(){}</script>
</problem>
""")
# Create the problem
problem = LoncapaProblem(xml_str, '1', system=test_system)
# Render the HTML
rendered_html = etree.XML(problem.get_html())
# expect the javascript is still present in the rendered html
self.assertTrue("<script type=\"text/javascript\">function(){}</script>" in etree.tostring(rendered_html))
def test_render_response_xml(self):
# Generate some XML for a string response
kwargs = {'question_text': "Test question",
@@ -99,11 +135,11 @@ class CapaHtmlRenderTest(unittest.TestCase):
response_element = rendered_html.find("span")
self.assertEqual(response_element.tag, "span")
# Expect that the response <span>
# Expect that the response <span>
# that contains a <div> for the textline
textline_element = response_element.find("div")
self.assertEqual(textline_element.text, 'Input Template Render')
# Expect a child <div> for the solution
# with the rendered template
solution_element = rendered_html.find("div")
@@ -112,19 +148,21 @@ class CapaHtmlRenderTest(unittest.TestCase):
# Expect that the template renderer was called with the correct
# arguments, once for the textline input and once for
# the solution
expected_textline_context = {'status': 'unsubmitted',
'value': '',
'preprocessor': None,
'msg': '',
'inline': False,
'hidden': False,
'do_math': False,
'id': '1_2_1',
expected_textline_context = {'status': 'unsubmitted',
'value': '',
'preprocessor': None,
'msg': '',
'inline': False,
'hidden': False,
'do_math': False,
'id': '1_2_1',
'size': None}
expected_solution_context = {'id': '1_solution_1'}
expected_calls = [mock.call('textline.html', expected_textline_context),
mock.call('solutionspan.html', expected_solution_context),
mock.call('textline.html', expected_textline_context),
mock.call('solutionspan.html', expected_solution_context)]
self.assertEqual(test_system.render_template.call_args_list,
@@ -146,7 +184,7 @@ class CapaHtmlRenderTest(unittest.TestCase):
# Create the problem and render the html
problem = LoncapaProblem(xml_str, '1', system=test_system)
# Grade the problem
correctmap = problem.grade_answers({'1_2_1': 'test'})

View File

@@ -102,6 +102,8 @@ class ChoiceGroupTest(unittest.TestCase):
'choices': [('foil1', '<text>This is foil One.</text>'),
('foil2', '<text>This is foil Two.</text>'),
('foil3', 'This is foil Three.'), ],
'show_correctness': 'always',
'submitted_message': 'Answer received.',
'name_array_suffix': expected_suffix, # what is this for??
}
@@ -482,27 +484,43 @@ class ChemicalEquationTest(unittest.TestCase):
'''
Check that chemical equation inputs work.
'''
def test_rendering(self):
size = "42"
xml_str = """<chemicalequationinput id="prob_1_2" size="{size}"/>""".format(size=size)
def setUp(self):
self.size = "42"
xml_str = """<chemicalequationinput id="prob_1_2" size="{size}"/>""".format(size=self.size)
element = etree.fromstring(xml_str)
state = {'value': 'H2OYeah', }
the_input = lookup_tag('chemicalequationinput')(test_system, element, state)
self.the_input = lookup_tag('chemicalequationinput')(test_system, element, state)
context = the_input._get_render_context()
def test_rendering(self):
''' Verify that the render context matches the expected render context'''
context = self.the_input._get_render_context()
expected = {'id': 'prob_1_2',
'value': 'H2OYeah',
'status': 'unanswered',
'msg': '',
'size': size,
'size': self.size,
'previewer': '/static/js/capa/chemical_equation_preview.js',
}
self.assertEqual(context, expected)
def test_chemcalc_ajax_sucess(self):
''' Verify that using the correct dispatch and valid data produces a valid response'''
data = {'formula': "H"}
response = self.the_input.handle_ajax("preview_chemcalc", data)
self.assertTrue('preview' in response)
self.assertNotEqual(response['preview'], '')
self.assertEqual(response['error'], "")
class DragAndDropTest(unittest.TestCase):
'''
@@ -570,3 +588,65 @@ class DragAndDropTest(unittest.TestCase):
context.pop('drag_and_drop_json')
expected.pop('drag_and_drop_json')
self.assertEqual(context, expected)
class AnnotationInputTest(unittest.TestCase):
'''
Make sure option inputs work
'''
def test_rendering(self):
xml_str = '''
<annotationinput>
<title>foo</title>
<text>bar</text>
<comment>my comment</comment>
<comment_prompt>type a commentary</comment_prompt>
<tag_prompt>select a tag</tag_prompt>
<options>
<option choice="correct">x</option>
<option choice="incorrect">y</option>
<option choice="partially-correct">z</option>
</options>
</annotationinput>
'''
element = etree.fromstring(xml_str)
value = {"comment": "blah blah", "options": [1]}
json_value = json.dumps(value)
state = {
'value': json_value,
'id': 'annotation_input',
'status': 'answered'
}
tag = 'annotationinput'
the_input = lookup_tag(tag)(test_system, element, state)
context = the_input._get_render_context()
expected = {
'id': 'annotation_input',
'value': value,
'status': 'answered',
'msg': '',
'title': 'foo',
'text': 'bar',
'comment': 'my comment',
'comment_prompt': 'type a commentary',
'tag_prompt': 'select a tag',
'options': [
{'id': 0, 'description': 'x', 'choice': 'correct'},
{'id': 1, 'description': 'y', 'choice': 'incorrect'},
{'id': 2, 'description': 'z', 'choice': 'partially-correct'}
],
'value': json_value,
'options_value': value['options'],
'has_options_value': len(value['options']) > 0,
'comment_value': value['comment'],
'debug': False,
'return_to_annotation': True
}
self.maxDiff = None
self.assertDictEqual(context, expected)

View File

@@ -19,7 +19,7 @@ from capa.xqueue_interface import dateformat
class ResponseTest(unittest.TestCase):
""" Base class for tests of capa responses."""
xml_factory_class = None
def setUp(self):
@@ -43,7 +43,7 @@ class ResponseTest(unittest.TestCase):
for input_str in incorrect_answers:
result = problem.grade_answers({'1_2_1': input_str}).get_correctness('1_2_1')
self.assertEqual(result, 'incorrect',
self.assertEqual(result, 'incorrect',
msg="%s should be marked incorrect" % str(input_str))
class MultiChoiceResponseTest(ResponseTest):
@@ -61,7 +61,7 @@ class MultiChoiceResponseTest(ResponseTest):
def test_named_multiple_choice_grade(self):
problem = self.build_problem(choices=[False, True, False],
choice_names=["foil_1", "foil_2", "foil_3"])
# Ensure that we get the expected grades
self.assert_grade(problem, 'choice_foil_1', 'incorrect')
self.assert_grade(problem, 'choice_foil_2', 'correct')
@@ -117,7 +117,7 @@ class ImageResponseTest(ResponseTest):
# Anything inside the rectangle (and along the borders) is correct
# Everything else is incorrect
correct_inputs = ["[12,19]", "[10,10]", "[20,20]",
correct_inputs = ["[12,19]", "[10,10]", "[20,20]",
"[10,15]", "[20,15]", "[15,10]", "[15,20]"]
incorrect_inputs = ["[4,6]", "[25,15]", "[15,40]", "[15,4]"]
self.assert_multiple_grade(problem, correct_inputs, incorrect_inputs)
@@ -259,7 +259,7 @@ class OptionResponseTest(ResponseTest):
xml_factory_class = OptionResponseXMLFactory
def test_grade(self):
problem = self.build_problem(options=["first", "second", "third"],
problem = self.build_problem(options=["first", "second", "third"],
correct_option="second")
# Assert that we get the expected grades
@@ -374,8 +374,8 @@ class StringResponseTest(ResponseTest):
hints = [("wisconsin", "wisc", "The state capital of Wisconsin is Madison"),
("minnesota", "minn", "The state capital of Minnesota is St. Paul")]
problem = self.build_problem(answer="Michigan",
case_sensitive=False,
problem = self.build_problem(answer="Michigan",
case_sensitive=False,
hints=hints)
# We should get a hint for Wisconsin
@@ -543,7 +543,7 @@ class ChoiceResponseTest(ResponseTest):
xml_factory_class = ChoiceResponseXMLFactory
def test_radio_group_grade(self):
problem = self.build_problem(choice_type='radio',
problem = self.build_problem(choice_type='radio',
choices=[False, True, False])
# Check that we get the expected results
@@ -601,17 +601,17 @@ class NumericalResponseTest(ResponseTest):
correct_responses = ["4", "4.0", "4.00"]
incorrect_responses = ["", "3.9", "4.1", "0"]
self.assert_multiple_grade(problem, correct_responses, incorrect_responses)
def test_grade_decimal_tolerance(self):
problem = self.build_problem(question_text="What is 2 + 2 approximately?",
explanation="The answer is 4",
answer=4,
tolerance=0.1)
correct_responses = ["4.0", "4.00", "4.09", "3.91"]
correct_responses = ["4.0", "4.00", "4.09", "3.91"]
incorrect_responses = ["", "4.11", "3.89", "0"]
self.assert_multiple_grade(problem, correct_responses, incorrect_responses)
def test_grade_percent_tolerance(self):
problem = self.build_problem(question_text="What is 2 + 2 approximately?",
explanation="The answer is 4",
@@ -642,6 +642,15 @@ class NumericalResponseTest(ResponseTest):
incorrect_responses = ["", "2.11", "1.89", "0"]
self.assert_multiple_grade(problem, correct_responses, incorrect_responses)
def test_exponential_answer(self):
problem = self.build_problem(question_text="What 5 * 10?",
explanation="The answer is 50",
answer="5e+1")
correct_responses = ["50", "50.0", "5e1", "5e+1", "50e0", "500e-1"]
incorrect_responses = ["", "3.9", "4.1", "0", "5.01e1"]
self.assert_multiple_grade(problem, correct_responses, incorrect_responses)
class CustomResponseTest(ResponseTest):
from response_xml_factory import CustomResponseXMLFactory
@@ -667,7 +676,7 @@ class CustomResponseTest(ResponseTest):
# The code can also set the global overall_message (str)
# to pass a message that applies to the whole response
inline_script = textwrap.dedent("""
messages[0] = "Test Message"
messages[0] = "Test Message"
overall_message = "Overall message"
""")
problem = self.build_problem(answer=inline_script)
@@ -687,14 +696,14 @@ class CustomResponseTest(ResponseTest):
def test_function_code_single_input(self):
# For function code, we pass in these arguments:
#
#
# 'expect' is the expect attribute of the <customresponse>
#
# '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("""
@@ -727,7 +736,7 @@ class CustomResponseTest(ResponseTest):
def test_function_code_multiple_input_no_msg(self):
# Check functions also have the option of returning
# a single boolean value
# a single boolean value
# If true, mark all the inputs correct
# If false, mark all the inputs incorrect
script = textwrap.dedent("""
@@ -736,7 +745,7 @@ class CustomResponseTest(ResponseTest):
answer_given[1] == expect)
""")
problem = self.build_problem(script=script, cfn="check_func",
problem = self.build_problem(script=script, cfn="check_func",
expect="42", num_inputs=2)
# Correct answer -- expect both inputs marked correct
@@ -764,10 +773,10 @@ class CustomResponseTest(ResponseTest):
# If the <customresponse> has multiple inputs associated with it,
# the check function can return a dict of the form:
#
#
# {'overall_message': STRING,
# 'input_list': [{'ok': BOOL, 'msg': STRING}, ...] }
#
#
# 'overall_message' is displayed at the end of the response
#
# 'input_list' contains dictionaries representing the correctness
@@ -784,7 +793,7 @@ class CustomResponseTest(ResponseTest):
{'ok': check3, 'msg': 'Feedback 3'} ] }
""")
problem = self.build_problem(script=script,
problem = self.build_problem(script=script,
cfn="check_func", num_inputs=3)
# Grade the inputs (one input incorrect)
@@ -821,11 +830,11 @@ class CustomResponseTest(ResponseTest):
check1 = (int(answer_given[0]) == 1)
check2 = (int(answer_given[1]) == 2)
check3 = (int(answer_given[2]) == 3)
return {'ok': (check1 and check2 and check3),
return {'ok': (check1 and check2 and check3),
'msg': 'Message text'}
""")
problem = self.build_problem(script=script,
problem = self.build_problem(script=script,
cfn="check_func", num_inputs=3)
# Grade the inputs (one input incorrect)
@@ -862,7 +871,7 @@ class CustomResponseTest(ResponseTest):
# Expect that an exception gets raised when we check the answer
with self.assertRaises(Exception):
problem.grade_answers({'1_2_1': '42'})
def test_invalid_dict_exception(self):
# Construct a script that passes back an invalid dict format
@@ -906,3 +915,40 @@ class SchematicResponseTest(ResponseTest):
# (That is, our script verifies that the context
# is what we expect)
self.assertEqual(correct_map.get_correctness('1_2_1'), 'correct')
class AnnotationResponseTest(ResponseTest):
from response_xml_factory import AnnotationResponseXMLFactory
xml_factory_class = AnnotationResponseXMLFactory
def test_grade(self):
(correct, partially, incorrect) = ('correct', 'partially-correct', 'incorrect')
answer_id = '1_2_1'
options = (('x', correct),('y', partially),('z', incorrect))
make_answer = lambda option_ids: {answer_id: json.dumps({'options': option_ids })}
tests = [
{'correctness': correct, 'points': 2,'answers': make_answer([0]) },
{'correctness': partially, 'points': 1, 'answers': make_answer([1]) },
{'correctness': incorrect, 'points': 0, 'answers': make_answer([2]) },
{'correctness': incorrect, 'points': 0, 'answers': make_answer([0,1,2]) },
{'correctness': incorrect, 'points': 0, 'answers': make_answer([]) },
{'correctness': incorrect, 'points': 0, 'answers': make_answer('') },
{'correctness': incorrect, 'points': 0, 'answers': make_answer(None) },
{'correctness': incorrect, 'points': 0, 'answers': {answer_id: 'null' } },
]
for (index, test) in enumerate(tests):
expected_correctness = test['correctness']
expected_points = test['points']
answers = test['answers']
problem = self.build_problem(options=options)
correct_map = problem.grade_answers(answers)
actual_correctness = correct_map.get_correctness(answer_id)
actual_points = correct_map.get_npoints(answer_id)
self.assertEqual(expected_correctness, actual_correctness,
msg="%s should be marked %s" % (answer_id, expected_correctness))
self.assertEqual(expected_points, actual_points,
msg="%s should have %d points" % (answer_id, expected_points))

View File

@@ -46,6 +46,7 @@ setup(
"custom_tag_template = xmodule.raw_module:RawDescriptor",
"about = xmodule.html_module:AboutDescriptor",
"graphical_slider_tool = xmodule.gst_module:GraphicalSliderToolDescriptor",
"annotatable = xmodule.annotatable_module:AnnotatableDescriptor",
"foldit = xmodule.foldit_module:FolditDescriptor",
]
}

View File

@@ -0,0 +1,131 @@
import logging
from lxml import etree
from pkg_resources import resource_string, resource_listdir
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from xmodule.modulestore.mongo import MongoModuleStore
from xmodule.modulestore.django import modulestore
from xmodule.contentstore.content import StaticContent
log = logging.getLogger(__name__)
class AnnotatableModule(XModule):
js = {'coffee': [resource_string(__name__, 'js/src/javascript_loader.coffee'),
resource_string(__name__, 'js/src/collapsible.coffee'),
resource_string(__name__, 'js/src/html/display.coffee'),
resource_string(__name__, 'js/src/annotatable/display.coffee')],
'js': []
}
js_module_name = "Annotatable"
css = {'scss': [resource_string(__name__, 'css/annotatable/display.scss')]}
icon_class = 'annotatable'
def _get_annotation_class_attr(self, index, el):
""" Returns a dict with the CSS class attribute to set on the annotation
and an XML key to delete from the element.
"""
attr = {}
cls = ['annotatable-span', 'highlight']
highlight_key = 'highlight'
color = el.get(highlight_key)
if color is not None:
if color in self.highlight_colors:
cls.append('highlight-'+color)
attr['_delete'] = highlight_key
attr['value'] = ' '.join(cls)
return { 'class' : attr }
def _get_annotation_data_attr(self, index, el):
""" Returns a dict in which the keys are the HTML data attributes
to set on the annotation element. Each data attribute has a
corresponding 'value' and (optional) '_delete' key to specify
an XML attribute to delete.
"""
data_attrs = {}
attrs_map = {
'body': 'data-comment-body',
'title': 'data-comment-title',
'problem': 'data-problem-id'
}
for xml_key in attrs_map.keys():
if xml_key in el.attrib:
value = el.get(xml_key, '')
html_key = attrs_map[xml_key]
data_attrs[html_key] = { 'value': value, '_delete': xml_key }
return data_attrs
def _render_annotation(self, index, el):
""" Renders an annotation element for HTML output. """
attr = {}
attr.update(self._get_annotation_class_attr(index, el))
attr.update(self._get_annotation_data_attr(index, el))
el.tag = 'span'
for key in attr.keys():
el.set(key, attr[key]['value'])
if '_delete' in attr[key] and attr[key]['_delete'] is not None:
delete_key = attr[key]['_delete']
del el.attrib[delete_key]
def _render_content(self):
""" Renders annotatable content with annotation spans and returns HTML. """
xmltree = etree.fromstring(self.content)
xmltree.tag = 'div'
if 'display_name' in xmltree.attrib:
del xmltree.attrib['display_name']
index = 0
for el in xmltree.findall('.//annotation'):
self._render_annotation(index, el)
index += 1
return etree.tostring(xmltree, encoding='unicode')
def _extract_instructions(self, xmltree):
""" Removes <instructions> from the xmltree and returns them as a string, otherwise None. """
instructions = xmltree.find('instructions')
if instructions is not None:
instructions.tag = 'div'
xmltree.remove(instructions)
return etree.tostring(instructions, encoding='unicode')
return None
def get_html(self):
""" Renders parameters to template. """
context = {
'display_name': self.display_name,
'element_id': self.element_id,
'instructions_html': self.instructions,
'content_html': self._render_content()
}
return self.system.render_template('annotatable.html', context)
def __init__(self, system, location, definition, descriptor,
instance_state=None, shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition, descriptor,
instance_state, shared_state, **kwargs)
xmltree = etree.fromstring(self.definition['data'])
self.instructions = self._extract_instructions(xmltree)
self.content = etree.tostring(xmltree, encoding='unicode')
self.element_id = self.location.html_id()
self.highlight_colors = ['yellow', 'orange', 'purple', 'blue', 'green']
class AnnotatableDescriptor(RawDescriptor):
module_class = AnnotatableModule
stores_state = True
template_dir_name = "annotatable"
mako_template = "widgets/raw-edit.html"

View File

@@ -135,8 +135,8 @@ class CapaModule(XModule):
self.grace_period = None
self.close_date = self.display_due_date
max_attempts = self.metadata.get('attempts', None)
if max_attempts:
max_attempts = self.metadata.get('attempts')
if max_attempts is not None and max_attempts != '':
self.max_attempts = int(max_attempts)
else:
self.max_attempts = None
@@ -247,123 +247,177 @@ class CapaModule(XModule):
'progress': Progress.to_js_status_str(self.get_progress())
})
def check_button_name(self):
"""
Determine the name for the "check" button.
Usually it is just "Check", but if this is the student's
final attempt, change the name to "Final Check"
"""
if self.max_attempts is not None:
final_check = (self.attempts >= self.max_attempts - 1)
else:
final_check = False
return "Final Check" if final_check else "Check"
def should_show_check_button(self):
"""
Return True/False to indicate whether to show the "Check" button.
"""
submitted_without_reset = (self.is_completed() and self.rerandomize == "always")
# If the problem is closed (past due / too many attempts)
# then we do NOT show the "check" button
# Also, do not show the "check" button if we're waiting
# for the user to reset a randomized problem
if self.closed() or submitted_without_reset:
return False
else:
return True
def should_show_reset_button(self):
"""
Return True/False to indicate whether to show the "Reset" button.
"""
is_survey_question = (self.max_attempts == 0)
if self.rerandomize in ["always", "onreset"]:
# If the problem is closed (and not a survey question with max_attempts==0),
# then do NOT show the reset button.
# If the problem hasn't been submitted yet, then do NOT show
# the reset button.
if (self.closed() and not is_survey_question) or not self.is_completed():
return False
else:
return True
# Only randomized problems need a "reset" button
else:
return False
def should_show_save_button(self):
"""
Return True/False to indicate whether to show the "Save" button.
"""
# If the user has forced the save button to display,
# then show it as long as the problem is not closed
# (past due / too many attempts)
if self.force_save_button == "true":
return not self.closed()
else:
is_survey_question = (self.max_attempts == 0)
needs_reset = self.is_completed() and self.rerandomize == "always"
# If the problem is closed (and not a survey question with max_attempts==0),
# then do NOT show the reset button
# If we're waiting for the user to reset a randomized problem
# then do NOT show the reset button
if (self.closed() and not is_survey_question) or needs_reset:
return False
else:
return True
def handle_problem_html_error(self, err):
"""
Change our problem to a dummy problem containing
a warning message to display to users.
Returns the HTML to show to users
*err* is the Exception encountered while rendering the problem HTML.
"""
log.exception(err)
# TODO (vshnayder): another switch on DEBUG.
if self.system.DEBUG:
msg = (
'[courseware.capa.capa_module] <font size="+1" color="red">'
'Failed to generate HTML for problem %s</font>' %
(self.location.url()))
msg += '<p>Error:</p><p><pre>%s</pre></p>' % str(err).replace('<', '&lt;')
msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<', '&lt;')
html = msg
# We're in non-debug mode, and possibly even in production. We want
# to avoid bricking of problem as much as possible
else:
# Presumably, student submission has corrupted LoncapaProblem HTML.
# First, pull down all student answers
student_answers = self.lcp.student_answers
answer_ids = student_answers.keys()
# Some inputtypes, such as dynamath, have additional "hidden" state that
# is not exposed to the student. Keep those hidden
# TODO: Use regex, e.g. 'dynamath' is suffix at end of answer_id
hidden_state_keywords = ['dynamath']
for answer_id in answer_ids:
for hidden_state_keyword in hidden_state_keywords:
if answer_id.find(hidden_state_keyword) >= 0:
student_answers.pop(answer_id)
# Next, generate a fresh LoncapaProblem
self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(),
state=None, # Tabula rasa
seed=self.seed, system=self.system)
# Prepend a scary warning to the student
warning = '<div class="capa_reset">'\
'<h2>Warning: The problem has been reset to its initial state!</h2>'\
'The problem\'s state was corrupted by an invalid submission. ' \
'The submission consisted of:'\
'<ul>'
for student_answer in student_answers.values():
if student_answer != '':
warning += '<li>' + cgi.escape(student_answer) + '</li>'
warning += '</ul>'\
'If this error persists, please contact the course staff.'\
'</div>'
html = warning
try:
html += self.lcp.get_html()
except Exception, err: # Couldn't do it. Give up
log.exception(err)
raise
return html
def get_problem_html(self, encapsulate=True):
'''Return html for the problem. Adds check, reset, save buttons
as necessary based on the problem config and state.'''
try:
html = self.lcp.get_html()
# If we cannot construct the problem HTML,
# then generate an error message instead.
except Exception, err:
log.exception(err)
html = self.handle_problem_html_error(err)
# TODO (vshnayder): another switch on DEBUG.
if self.system.DEBUG:
msg = (
'[courseware.capa.capa_module] <font size="+1" color="red">'
'Failed to generate HTML for problem %s</font>' %
(self.location.url()))
msg += '<p>Error:</p><p><pre>%s</pre></p>' % str(err).replace('<', '&lt;')
msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<', '&lt;')
html = msg
else:
# We're in non-debug mode, and possibly even in production. We want
# to avoid bricking of problem as much as possible
# Presumably, student submission has corrupted LoncapaProblem HTML.
# First, pull down all student answers
student_answers = self.lcp.student_answers
answer_ids = student_answers.keys()
# Some inputtypes, such as dynamath, have additional "hidden" state that
# is not exposed to the student. Keep those hidden
# TODO: Use regex, e.g. 'dynamath' is suffix at end of answer_id
hidden_state_keywords = ['dynamath']
for answer_id in answer_ids:
for hidden_state_keyword in hidden_state_keywords:
if answer_id.find(hidden_state_keyword) >= 0:
student_answers.pop(answer_id)
# Next, generate a fresh LoncapaProblem
self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(),
state=None, # Tabula rasa
seed=self.seed, system=self.system)
# Prepend a scary warning to the student
warning = '<div class="capa_reset">'\
'<h2>Warning: The problem has been reset to its initial state!</h2>'\
'The problem\'s state was corrupted by an invalid submission. ' \
'The submission consisted of:'\
'<ul>'
for student_answer in student_answers.values():
if student_answer != '':
warning += '<li>' + cgi.escape(student_answer) + '</li>'
warning += '</ul>'\
'If this error persists, please contact the course staff.'\
'</div>'
html = warning
try:
html += self.lcp.get_html()
except Exception, err: # Couldn't do it. Give up
log.exception(err)
raise
# The convention is to pass the name of the check button
# if we want to show a check button, and False otherwise
# This works because non-empty strings evaluate to True
if self.should_show_check_button():
check_button = self.check_button_name()
else:
check_button = False
content = {'name': self.display_name,
'html': html,
'weight': self.descriptor.weight,
}
# We using strings as truthy values, because the terminology of the
# check button is context-specific.
# Put a "Check" button if unlimited attempts or still some left
if self.max_attempts is None or self.attempts < self.max_attempts - 1:
check_button = "Check"
else:
# Will be final check so let user know that
check_button = "Final Check"
reset_button = True
save_button = True
# If we're after deadline, or user has exhausted attempts,
# question is read-only.
if self.closed():
check_button = False
reset_button = False
save_button = False
# If attempts=0 then show just check and reset buttons; this is for survey questions using capa
if self.max_attempts==0:
check_button = False
reset_button = True
save_button = True
# User submitted a problem, and hasn't reset. We don't want
# more submissions.
if self.lcp.done and self.rerandomize == "always":
check_button = False
save_button = False
# Only show the reset button if pressing it will show different values
if self.rerandomize not in ["always", "onreset"]:
reset_button = False
# User hasn't submitted an answer yet -- we don't want resets
if not self.lcp.done:
reset_button = False
# We may not need a "save" button if infinite number of attempts and
# non-randomized. The problem author can force it. It's a bit weird for
# randomization to control this; should perhaps be cleaned up.
if (self.force_save_button == "false") and (self.max_attempts is None and self.rerandomize != "always"):
save_button = False
context = {'problem': content,
'id': self.id,
'check_button': check_button,
'reset_button': reset_button,
'save_button': save_button,
'reset_button': self.should_show_reset_button(),
'save_button': self.should_show_save_button(),
'answer_available': self.answer_available(),
'ajax_url': self.system.ajax_url,
'attempts_used': self.attempts,
@@ -396,6 +450,7 @@ class CapaModule(XModule):
'problem_save': self.save_problem,
'problem_show': self.get_answer,
'score_update': self.update_score,
'input_ajax': self.lcp.handle_input_ajax
}
if dispatch not in handlers:
@@ -419,7 +474,7 @@ class CapaModule(XModule):
def closed(self):
''' Is the student still allowed to submit answers? '''
if self.attempts == self.max_attempts:
if self.max_attempts is not None and self.attempts >= self.max_attempts:
return True
if self.is_past_due():
return True
@@ -527,22 +582,60 @@ class CapaModule(XModule):
@staticmethod
def make_dict_of_responses(get):
'''Make dictionary of student responses (aka "answers")
get is POST dictionary.
get is POST dictionary (Djano QueryDict).
The *get* dict has keys of the form 'x_y', which are mapped
to key 'y' in the returned dict. For example,
'input_1_2_3' would be mapped to '1_2_3' in the returned dict.
Some inputs always expect a list in the returned dict
(e.g. checkbox inputs). The convention is that
keys in the *get* dict that end with '[]' will always
have list values in the returned dict.
For example, if the *get* dict contains {'input_1[]': 'test' }
then the output dict would contain {'1': ['test'] }
(the value is a list).
Raises an exception if:
A key in the *get* dictionary does not contain >= 1 underscores
(e.g. "input" is invalid; "input_1" is valid)
Two keys end up with the same name in the returned dict.
(e.g. 'input_1' and 'input_1[]', which both get mapped
to 'input_1' in the returned dict)
'''
answers = dict()
for key in get:
# e.g. input_resistor_1 ==> resistor_1
_, _, name = key.partition('_')
# This allows for answers which require more than one value for
# the same form input (e.g. checkbox inputs). The convention is that
# if the name ends with '[]' (which looks like an array), then the
# answer will be an array.
if not name.endswith('[]'):
answers[name] = get[key]
# If key has no underscores, then partition
# will return (key, '', '')
# We detect this and raise an error
if not name:
raise ValueError("%s must contain at least one underscore" % str(key))
else:
name = name[:-2]
answers[name] = get.getlist(key)
# This allows for answers which require more than one value for
# the same form input (e.g. checkbox inputs). The convention is that
# if the name ends with '[]' (which looks like an array), then the
# answer will be an array.
is_list_key = name.endswith('[]')
name = name[:-2] if is_list_key else name
if is_list_key:
val = get.getlist(key)
else:
val = get[key]
# If the name already exists, then we don't want
# to override it. Raise an error instead
if name in answers:
raise ValueError("Key %s already exists in answers dict" % str(name))
else:
answers[name] = val
return answers
@@ -550,7 +643,7 @@ class CapaModule(XModule):
''' Checks whether answers to a problem are correct, and
returns a map of correct/incorrect answers:
{'success' : bool,
{'success' : 'correct' | 'incorrect' | AJAX alert msg string,
'contents' : html}
'''
event_info = dict()
@@ -609,11 +702,11 @@ class CapaModule(XModule):
# 'success' will always be incorrect
event_info['correct_map'] = correct_map.get_dict()
event_info['success'] = success
event_info['attempts'] = self.attempts
event_info['attempts'] = self.attempts
self.system.track_function('save_problem_check', event_info)
if hasattr(self.system, 'psychometrics_handler'): # update PsychometricsData using callback
self.system.psychometrics_handler(self.get_instance_state())
if hasattr(self.system, 'psychometrics_handler'): # update PsychometricsData using callback
self.system.psychometrics_handler(self.get_instance_state())
# render problem into HTML
html = self.get_problem_html(encapsulate=False)
@@ -663,7 +756,12 @@ class CapaModule(XModule):
''' Changes problem state to unfinished -- removes student answers,
and causes problem to rerender itself.
Returns problem html as { 'html' : html-string }.
Returns a dictionary of the form:
{'success': True/False,
'html': Problem HTML string }
If an error occurs, the dictionary will also have an
'error' key containing an error message.
'''
event_info = dict()
event_info['old_state'] = self.lcp.get_state()
@@ -686,6 +784,7 @@ class CapaModule(XModule):
# reset random number generator seed (note the self.lcp.get_state()
# in next line)
self.lcp.seed = None
self.lcp = LoncapaProblem(self.definition['data'],
self.location.html_id(), self.lcp.get_state(),
@@ -694,7 +793,8 @@ class CapaModule(XModule):
event_info['new_state'] = self.lcp.get_state()
self.system.track_function('reset_problem', event_info)
return {'html': self.get_problem_html(encapsulate=False)}
return { 'success': True,
'html': self.get_problem_html(encapsulate=False)}
class CapaDescriptor(RawDescriptor):

View File

@@ -10,7 +10,6 @@ from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import Comb
log = logging.getLogger("mitx.courseware")
VERSION_TUPLES = (
('1', CombinedOpenEndedV1Descriptor, CombinedOpenEndedV1Module),
)
@@ -18,6 +17,7 @@ VERSION_TUPLES = (
DEFAULT_VERSION = 1
DEFAULT_VERSION = str(DEFAULT_VERSION)
class CombinedOpenEndedModule(XModule):
"""
This is a module that encapsulates all open ended grading (self assessment, peer assessment, etc).
@@ -60,7 +60,7 @@ class CombinedOpenEndedModule(XModule):
def __init__(self, system, location, definition, descriptor,
instance_state=None, shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition, descriptor,
instance_state, shared_state, **kwargs)
instance_state, shared_state, **kwargs)
"""
Definition file should have one or many task blocks, a rubric block, and a prompt block:
@@ -129,13 +129,15 @@ class CombinedOpenEndedModule(XModule):
version_index = versions.index(self.version)
static_data = {
'rewrite_content_links' : self.rewrite_content_links,
'rewrite_content_links': self.rewrite_content_links,
}
self.child_descriptor = descriptors[version_index](self.system)
self.child_definition = descriptors[version_index].definition_from_xml(etree.fromstring(definition['data']), self.system)
self.child_definition = descriptors[version_index].definition_from_xml(etree.fromstring(definition['data']),
self.system)
self.child_module = modules[version_index](self.system, location, self.child_definition, self.child_descriptor,
instance_state = json.dumps(instance_state), metadata = self.metadata, static_data= static_data)
instance_state=json.dumps(instance_state), metadata=self.metadata,
static_data=static_data)
def get_html(self):
return self.child_module.get_html()

View File

@@ -246,7 +246,7 @@ class CourseDescriptor(SequenceDescriptor):
policy = json.loads(cls.read_grading_policy(paths, system))
except ValueError:
system.error_tracker("Unable to decode grading policy as json")
policy = None
policy = {}
# cdodge: import the grading policy information that is on disk and put into the
# descriptor 'definition' bucket as a dictionary so that it is persisted in the DB
@@ -356,7 +356,14 @@ class CourseDescriptor(SequenceDescriptor):
"""
Return the pdf_textbooks config, as a python object, or None if not specified.
"""
return self.metadata.get('pdf_textbooks')
return self.metadata.get('pdf_textbooks', [])
@property
def html_textbooks(self):
"""
Return the html_textbooks config, as a python object, or None if not specified.
"""
return self.metadata.get('html_textbooks', [])
@tabs.setter
def tabs(self, value):
@@ -398,7 +405,7 @@ class CourseDescriptor(SequenceDescriptor):
return self.metadata.get("cohort_config", {}).get(
"auto_cohort_groups", [])
@property
def top_level_discussion_topic_ids(self):
"""

View File

@@ -0,0 +1,169 @@
$border-color: #C8C8C8;
$body-font-size: em(14);
.annotatable-header {
margin-bottom: .5em;
.annotatable-title {
font-size: em(22);
text-transform: uppercase;
padding: 2px 4px;
}
}
.annotatable-section {
position: relative;
padding: .5em 1em;
border: 1px solid $border-color;
border-radius: .5em;
margin-bottom: .5em;
&.shaded { background-color: #EDEDED; }
.annotatable-section-title {
font-weight: bold;
a { font-weight: normal; }
}
.annotatable-section-body {
border-top: 1px solid $border-color;
margin-top: .5em;
padding-top: .5em;
@include clearfix;
}
ul.instructions-template {
list-style: disc;
margin-left: 4em;
b { font-weight: bold; }
i { font-style: italic; }
code {
display: inline;
white-space: pre;
font-family: Courier New, monospace;
}
}
}
.annotatable-toggle {
position: absolute;
right: 0;
margin: 2px 1em 2px 0;
&.expanded:after { content: " \2191" }
&.collapsed:after { content: " \2193" }
}
.annotatable-span {
display: inline;
cursor: pointer;
@each $highlight in (
(yellow rgba(255,255,10,0.3) rgba(255,255,10,0.9)),
(red rgba(178,19,16,0.3) rgba(178,19,16,0.9)),
(orange rgba(255,165,0,0.3) rgba(255,165,0,0.9)),
(green rgba(25,255,132,0.3) rgba(25,255,132,0.9)),
(blue rgba(35,163,255,0.3) rgba(35,163,255,0.9)),
(purple rgba(115,9,178,0.3) rgba(115,9,178,0.9))) {
$marker: nth($highlight,1);
$color: nth($highlight,2);
$selected_color: nth($highlight,3);
@if $marker == yellow {
&.highlight {
background-color: $color;
&.selected { background-color: $selected_color; }
}
}
&.highlight-#{$marker} {
background-color: $color;
&.selected { background-color: $selected_color; }
}
}
&.hide {
cursor: none;
background-color: inherit;
.annotatable-icon {
display: none;
}
}
.annotatable-comment {
display: none;
}
}
.ui-tooltip.qtip.ui-tooltip {
font-size: $body-font-size;
border: 1px solid #333;
border-radius: 1em;
background-color: rgba(0,0,0,.85);
color: #fff;
-webkit-font-smoothing: antialiased;
.ui-tooltip-titlebar {
font-size: em(16);
color: inherit;
background-color: transparent;
padding: 5px 10px;
border: none;
.ui-tooltip-title {
padding: 5px 0px;
border-bottom: 2px solid #333;
font-weight: bold;
}
.ui-tooltip-icon {
right: 10px;
background: #333;
}
.ui-state-hover {
color: inherit;
border: 1px solid #ccc;
}
}
.ui-tooltip-content {
color: inherit;
font-size: em(14);
text-align: left;
font-weight: 400;
padding: 0 10px 10px 10px;
background-color: transparent;
}
p {
color: inherit;
line-height: normal;
}
}
.ui-tooltip.qtip.ui-tooltip-annotatable {
max-width: 375px;
.ui-tooltip-content {
padding: 0 10px;
.annotatable-comment {
display: block;
margin: 0px 0px 10px 0;
max-height: 225px;
overflow: auto;
}
.annotatable-reply {
display: block;
border-top: 2px solid #333;
padding: 5px 0;
margin: 0;
text-align: center;
}
}
&:after {
content: '';
display: inline-block;
position: absolute;
bottom: -20px;
left: 50%;
height: 0;
width: 0;
margin-left: -5px;
border: 10px solid transparent;
border-top-color: rgba(0, 0, 0, .85);
}
}

View File

@@ -42,6 +42,14 @@ section.problem {
label.choicegroup_correct{
&:after{
content: url('../images/correct-icon.png');
margin-left:15px
}
}
label.choicegroup_incorrect{
&:after{
content: url('../images/incorrect-icon.png');
margin-left:15px;
}
}
@@ -52,6 +60,7 @@ section.problem {
.indicator_container {
float: left;
width: 25px;
height: 1px;
margin-right: 15px;
}
@@ -69,7 +78,7 @@ section.problem {
}
text {
display: block;
display: inline;
margin-left: 25px;
}
}
@@ -231,6 +240,15 @@ section.problem {
width: 25px;
}
&.partially-correct {
@include inline-block();
background: url('../images/partially-correct-icon.png') center center no-repeat;
height: 20px;
position: relative;
top: 6px;
width: 25px;
}
&.incorrect, &.ui-icon-close {
@include inline-block();
background: url('../images/incorrect-icon.png') center center no-repeat;
@@ -802,4 +820,91 @@ section.problem {
display: none;
}
}
.annotation-input {
$yellow: rgba(255,255,10,0.3);
border: 1px solid #ccc;
border-radius: 1em;
margin: 0 0 1em 0;
.annotation-header {
font-weight: bold;
border-bottom: 1px solid #ccc;
padding: .5em 1em;
}
.annotation-body { padding: .5em 1em; }
a.annotation-return {
float: right;
font: inherit;
font-weight: normal;
}
a.annotation-return:after { content: " \2191" }
.block, ul.tags {
margin: .5em 0;
padding: 0;
}
.block-highlight {
padding: .5em;
color: #333;
font-style: normal;
background-color: $yellow;
border: 1px solid darken($yellow, 10%);
}
.block-comment { font-style: italic; }
ul.tags {
display: block;
list-style-type: none;
margin-left: 1em;
li {
display: block;
margin: 1em 0 0 0;
position: relative;
.tag {
display: inline-block;
cursor: pointer;
border: 1px solid rgb(102,102,102);
margin-left: 40px;
&.selected {
background-color: $yellow;
}
}
.tag-status {
position: absolute;
left: 0;
}
.tag-status, .tag { padding: .25em .5em; }
}
}
textarea.comment {
$num-lines-to-show: 5;
$line-height: 1.4em;
$padding: .2em;
width: 100%;
padding: $padding (2 * $padding);
line-height: $line-height;
height: ($num-lines-to-show * $line-height) + (2*$padding) - (($line-height - 1)/2);
}
.answer-annotation { display: block; margin: 0; }
/* for debugging the input value field. enable the debug flag on the inputtype */
.debug-value {
color: #fff;
padding: 1em;
margin: 1em 0;
background-color: #999;
border: 1px solid #000;
input[type="text"] { width: 100%; }
pre { background-color: #CCC; color: #000; }
&:before {
display: block;
content: "debug input value";
text-transform: uppercase;
font-weight: bold;
font-size: 1.5em;
}
}
}
}

View File

@@ -86,7 +86,10 @@ class FolditModule(XModule):
"""
from foldit.models import Score
return [(e['username'], e['score']) for e in Score.get_tops_n(10)]
leaders = [(e['username'], e['score']) for e in Score.get_tops_n(10)]
leaders.sort(key=lambda x: -x[1])
return leaders
def get_html(self):
"""
@@ -96,8 +99,9 @@ class FolditModule(XModule):
self.required_level,
self.required_sublevel)
showbasic = (self.metadata.get("show_basic_score").lower() == "true")
showleader = (self.metadata.get("show_leaderboard").lower() == "true")
showbasic = (self.metadata.get("show_basic_score", "").lower() == "true")
showleader = (self.metadata.get("show_leaderboard", "").lower() == "true")
context = {
'due': self.due_str,
'success': self.is_complete(),

View File

@@ -0,0 +1,35 @@
<section class='xmodule_display xmodule_AnnotatableModule' data-type='Annotatable'>
<div class="annotatable-wrapper">
<div class="annotatable-header">
<div class="annotatable-title">First Annotation Exercise</div>
</div>
<div class="annotatable-section">
<div class="annotatable-section-title">
Instructions
<a class="annotatable-toggle annotatable-toggle-instructions expanded" href="javascript:void(0)">Collapse Instructions</a>
</div>
<div class="annotatable-section-body annotatable-instructions">
<div><p>The main goal of this exercise is to start practicing the art of slow reading.</p>
</div>
</div>
<div class="annotatable-section">
<div class="annotatable-section-title">
Guided Discussion
<a class="annotatable-toggle annotatable-toggle-annotations" href="javascript:void(0)">Hide Annotations</a>
</div>
</div>
<div class="annotatable-content">
|87 No, those who are really responsible are Zeus and Fate [Moira] and the Fury [Erinys] who roams in the mist. <br/>
|88 <span data-problem-id="0" data-comment-body="Agamemnon says..." class="annotatable-span highlight" data-comment-title="Your Title Here">They are the ones who</span><br/>
|100 He [= Zeus], making a formal declaration [eukhesthai], spoke up at a meeting of all the gods and said: <br/>
|101 <span data-problem-id="1" data-comment-body="When Zeus speaks..." class="annotatable-span highlight">“hear me, all gods and all goddesses,</span><br/>
|113 but he swore a great oath.
<span data-problem-id="2" data-comment-body="How is the veering off-course ..." class="annotatable-span highlight">And right then and there</span><br/>
</div>
</div>
</section>
<section class="problem"><a class="annotation-return" href="javascript:void(0)">Return to Annotation</a></section>
<section class="problem"><a class="annotation-return" href="javascript:void(0)">Return to Annotation</a></section>
<section class="problem"><a class="annotation-return" href="javascript:void(0)">Return to Annotation</a></section>

View File

@@ -0,0 +1,9 @@
describe 'Annotatable', ->
beforeEach ->
loadFixtures 'annotatable.html'
describe 'constructor', ->
el = $('.xmodule_display.xmodule_AnnotatableModule')
beforeEach ->
@annotatable = new Annotatable(el)
it 'works', ->
expect(1).toBe(1)

View File

@@ -0,0 +1,197 @@
class @Annotatable
_debug: false
# selectors for the annotatable xmodule
toggleAnnotationsSelector: '.annotatable-toggle-annotations'
toggleInstructionsSelector: '.annotatable-toggle-instructions'
instructionsSelector: '.annotatable-instructions'
sectionSelector: '.annotatable-section'
spanSelector: '.annotatable-span'
replySelector: '.annotatable-reply'
# these selectors are for responding to events from the annotation capa problem type
problemXModuleSelector: '.xmodule_CapaModule'
problemSelector: 'section.problem'
problemInputSelector: 'section.problem .annotation-input'
problemReturnSelector: 'section.problem .annotation-return'
constructor: (el) ->
console.log 'loaded Annotatable' if @_debug
@el = el
@$el = $(el)
@init()
$: (selector) ->
$(selector, @el)
init: () ->
@initEvents()
@initTips()
initEvents: () ->
# Initialize toggle handlers for the instructions and annotations sections
[@annotationsHidden, @instructionsHidden] = [false, false]
@$(@toggleAnnotationsSelector).bind 'click', @onClickToggleAnnotations
@$(@toggleInstructionsSelector).bind 'click', @onClickToggleInstructions
# Initialize handler for 'reply to annotation' events that scroll to
# the associated problem. The reply buttons are part of the tooltip
# content. It's important that the tooltips be configured to render
# as descendants of the annotation module and *not* the document.body.
@$el.delegate @replySelector, 'click', @onClickReply
# Initialize handler for 'return to annotation' events triggered from problems.
# 1) There are annotationinput capa problems rendered on the page
# 2) Each one has an embedded return link (see annotation capa problem template).
# Since the capa problem injects HTML content via AJAX, the best we can do is
# is let the click events bubble up to the body and handle them there.
$('body').delegate @problemReturnSelector, 'click', @onClickReturn
initTips: () ->
# tooltips are used to display annotations for highlighted text spans
@$(@spanSelector).each (index, el) =>
$(el).qtip(@getSpanTipOptions el)
getSpanTipOptions: (el) ->
content:
title:
text: @makeTipTitle(el)
text: @makeTipContent(el)
position:
my: 'bottom center' # of tooltip
at: 'top center' # of target
target: $(el) # where the tooltip was triggered (i.e. the annotation span)
container: @$el
adjust:
y: -5
show:
event: 'click mouseenter'
solo: true
hide:
event: 'click mouseleave'
delay: 500,
fixed: true # don't hide the tooltip if it is moused over
style:
classes: 'ui-tooltip-annotatable'
events:
show: @onShowTip
onClickToggleAnnotations: (e) => @toggleAnnotations()
onClickToggleInstructions: (e) => @toggleInstructions()
onClickReply: (e) => @replyTo(e.currentTarget)
onClickReturn: (e) => @returnFrom(e.currentTarget)
onShowTip: (event, api) =>
event.preventDefault() if @annotationsHidden
getSpanForProblemReturn: (el) ->
problem_id = $(@problemReturnSelector).index(el)
@$(@spanSelector).filter("[data-problem-id='#{problem_id}']")
getProblem: (el) ->
problem_id = @getProblemId(el)
$(@problemSelector).has(@problemInputSelector).eq(problem_id)
getProblemId: (el) ->
$(el).data('problem-id')
toggleAnnotations: () ->
hide = (@annotationsHidden = not @annotationsHidden)
@toggleAnnotationButtonText hide
@toggleSpans hide
@toggleTips hide
toggleTips: (hide) ->
visible = @findVisibleTips()
@hideTips visible
toggleAnnotationButtonText: (hide) ->
buttonText = (if hide then 'Show' else 'Hide')+' Annotations'
@$(@toggleAnnotationsSelector).text(buttonText)
toggleInstructions: () ->
hide = (@instructionsHidden = not @instructionsHidden)
@toggleInstructionsButton hide
@toggleInstructionsText hide
toggleInstructionsButton: (hide) ->
txt = (if hide then 'Expand' else 'Collapse')+' Instructions'
cls = (if hide then ['expanded', 'collapsed'] else ['collapsed','expanded'])
@$(@toggleInstructionsSelector).text(txt).removeClass(cls[0]).addClass(cls[1])
toggleInstructionsText: (hide) ->
slideMethod = (if hide then 'slideUp' else 'slideDown')
@$(@instructionsSelector)[slideMethod]()
toggleSpans: (hide) ->
@$(@spanSelector).toggleClass 'hide', hide, 250
replyTo: (buttonEl) ->
offset = -20
el = @getProblem buttonEl
if el.length > 0
@scrollTo(el, @afterScrollToProblem, offset)
else
console.log('problem not found. event: ', e) if @_debug
returnFrom: (buttonEl) ->
offset = -200
el = @getSpanForProblemReturn buttonEl
if el.length > 0
@scrollTo(el, @afterScrollToSpan, offset)
else
console.log('span not found. event:', e) if @_debug
scrollTo: (el, after, offset = -20) ->
$('html,body').scrollTo(el, {
duration: 500
onAfter: @_once => after?.call this, el
offset: offset
}) if $(el).length > 0
afterScrollToProblem: (problem_el) ->
problem_el.effect 'highlight', {}, 500
afterScrollToSpan: (span_el) ->
span_el.addClass 'selected', 400, 'swing', ->
span_el.removeClass 'selected', 400, 'swing'
makeTipContent: (el) ->
(api) =>
text = $(el).data('comment-body')
comment = @createComment(text)
problem_id = @getProblemId(el)
reply = @createReplyLink(problem_id)
$(comment).add(reply)
makeTipTitle: (el) ->
(api) =>
title = $(el).data('comment-title')
(if title then title else 'Commentary')
createComment: (text) ->
$("<div class=\"annotatable-comment\">#{text}</div>")
createReplyLink: (problem_id) ->
$("<a class=\"annotatable-reply\" href=\"javascript:void(0);\" data-problem-id=\"#{problem_id}\">Reply to Annotation</a>")
findVisibleTips: () ->
visible = []
@$(@spanSelector).each (index, el) ->
api = $(el).qtip('api')
tip = $(api?.elements.tooltip)
if tip.is(':visible')
visible.push el
visible
hideTips: (elements) ->
$(elements).qtip('hide')
_once: (fn) ->
done = false
return =>
fn.call this unless done
done = true

View File

@@ -76,6 +76,24 @@ class @Problem
# TODO: Some logic to dynamically adjust polling rate based on queuelen
window.queuePollerID = window.setTimeout(@poll, 1000)
# Use this if you want to make an ajax call on the input type object
# static method so you don't have to instantiate a Problem in order to use it
# Input:
# url: the AJAX url of the problem
# input_id: the input_id of the input you would like to make the call on
# NOTE: the id is the ${id} part of "input_${id}" during rendering
# If this function is passed the entire prefixed id, the backend may have trouble
# finding the correct input
# dispatch: string that indicates how this data should be handled by the inputtype
# callback: the function that will be called once the AJAX call has been completed.
# It will be passed a response object
@inputAjax: (url, input_id, dispatch, data, callback) ->
data['dispatch'] = dispatch
data['input_id'] = input_id
$.postWithPrefix "#{url}/input_ajax", data, callback
render: (content) ->
if content
@el.html(content)

View File

@@ -46,10 +46,10 @@ class XModuleCourseFactory(Factory):
new_course.metadata['start'] = stringify_time(gmtime())
new_course.tabs = [{"type": "courseware"},
{"type": "course_info", "name": "Course Info"},
{"type": "discussion", "name": "Discussion"},
{"type": "wiki", "name": "Wiki"},
{"type": "progress", "name": "Progress"}]
{"type": "course_info", "name": "Course Info"},
{"type": "discussion", "name": "Discussion"},
{"type": "wiki", "name": "Wiki"},
{"type": "progress", "name": "Progress"}]
# Update the data in the mongo datastore
store.update_metadata(new_course.location.url(), new_course.own_metadata)

View File

@@ -119,11 +119,11 @@ def test_equality():
# All the cleaning functions should do the same thing with these
general_pairs = [('', ''),
(' ', '_'),
('abc,', 'abc_'),
('ab fg!@//\\aj', 'ab_fg_aj'),
(u"ab\xA9", "ab_"), # no unicode allowed for now
]
(' ', '_'),
('abc,', 'abc_'),
('ab fg!@//\\aj', 'ab_fg_aj'),
(u"ab\xA9", "ab_"), # no unicode allowed for now
]
def test_clean():
@@ -131,7 +131,7 @@ def test_clean():
('a:b', 'a_b'), # no colons in non-name components
('a-b', 'a-b'), # dashes ok
('a.b', 'a.b'), # dot ok
]
]
for input, output in pairs:
assert_equals(Location.clean(input), output)
@@ -141,17 +141,17 @@ def test_clean_for_url_name():
('a:b', 'a:b'), # colons ok in names
('a-b', 'a-b'), # dashes ok in names
('a.b', 'a.b'), # dot ok in names
]
]
for input, output in pairs:
assert_equals(Location.clean_for_url_name(input), output)
def test_clean_for_html():
pairs = general_pairs + [
("a:b", "a_b"), # no colons for html use
("a-b", "a-b"), # dashes ok (though need to be replaced in various use locations. ugh.)
('a.b', 'a_b'), # no dots.
]
("a:b", "a_b"), # no colons for html use
("a-b", "a-b"), # dashes ok (though need to be replaced in various use locations. ugh.)
('a.b', 'a_b'), # no dots.
]
for input, output in pairs:
assert_equals(Location.clean_for_html(input), output)

View File

@@ -12,7 +12,7 @@ def check_path_to_location(modulestore):
("edX/toy/2012_Fall", "Overview", "Welcome", None)),
("i4x://edX/toy/chapter/Overview",
("edX/toy/2012_Fall", "Overview", None, None)),
)
)
course_id = "edX/toy/2012_Fall"
for location, expected in should_work:
@@ -20,6 +20,6 @@ def check_path_to_location(modulestore):
not_found = (
"i4x://edX/toy/video/WelcomeX", "i4x://edX/toy/course/NotHome"
)
)
for location in not_found:
assert_raises(ItemNotFoundError, path_to_location, modulestore, course_id, location)

View File

@@ -40,14 +40,15 @@ ACCEPT_FILE_UPLOAD = False
TRUE_DICT = ["True", True, "TRUE", "true"]
HUMAN_TASK_TYPE = {
'selfassessment' : "Self Assessment",
'openended' : "edX Assessment",
}
'selfassessment': "Self Assessment",
'openended': "edX Assessment",
}
#Default value that controls whether or not to skip basic spelling checks in the controller
#Metadata overrides this
SKIP_BASIC_CHECKS = False
class CombinedOpenEndedV1Module():
"""
This is a module that encapsulates all open ended grading (self assessment, peer assessment, etc).
@@ -83,7 +84,7 @@ class CombinedOpenEndedV1Module():
TEMPLATE_DIR = "combinedopenended"
def __init__(self, system, location, definition, descriptor,
instance_state=None, shared_state=None, metadata = None, static_data = None, **kwargs):
instance_state=None, shared_state=None, metadata=None, static_data=None, **kwargs):
"""
Definition file should have one or many task blocks, a rubric block, and a prompt block:
@@ -122,7 +123,7 @@ class CombinedOpenEndedV1Module():
self.metadata = metadata
self.display_name = metadata.get('display_name', "Open Ended")
self.rewrite_content_links = static_data.get('rewrite_content_links',"")
self.rewrite_content_links = static_data.get('rewrite_content_links', "")
# Load instance state
@@ -152,10 +153,10 @@ class CombinedOpenEndedV1Module():
self.skip_basic_checks = self.metadata.get('skip_spelling_checks', SKIP_BASIC_CHECKS)
display_due_date_string = self.metadata.get('due', None)
grace_period_string = self.metadata.get('graceperiod', None)
try:
self.timeinfo = TimeInfo(display_due_date_string, grace_period_string)
self.timeinfo = TimeInfo(display_due_date_string, grace_period_string)
except:
log.error("Error parsing due date information in location {0}".format(location))
raise
@@ -177,10 +178,10 @@ class CombinedOpenEndedV1Module():
'rubric': definition['rubric'],
'display_name': self.display_name,
'accept_file_upload': self.accept_file_upload,
'close_date' : self.timeinfo.close_date,
's3_interface' : self.system.s3_interface,
'skip_basic_checks' : self.skip_basic_checks,
}
'close_date': self.timeinfo.close_date,
's3_interface': self.system.s3_interface,
'skip_basic_checks': self.skip_basic_checks,
}
self.task_xml = definition['task_xml']
self.location = location
@@ -223,15 +224,15 @@ class CombinedOpenEndedV1Module():
child_modules = {
'openended': open_ended_module.OpenEndedModule,
'selfassessment': self_assessment_module.SelfAssessmentModule,
}
}
child_descriptors = {
'openended': open_ended_module.OpenEndedDescriptor,
'selfassessment': self_assessment_module.SelfAssessmentDescriptor,
}
}
children = {
'modules': child_modules,
'descriptors': child_descriptors,
}
}
return children
def setup_next_task(self, reset=False):
@@ -267,7 +268,8 @@ class CombinedOpenEndedV1Module():
self.current_task_parsed_xml = self.current_task_descriptor.definition_from_xml(etree_xml, self.system)
if current_task_state is None and self.current_task_number == 0:
self.current_task = child_task_module(self.system, self.location,
self.current_task_parsed_xml, self.current_task_descriptor, self.static_data)
self.current_task_parsed_xml, self.current_task_descriptor,
self.static_data)
self.task_states.append(self.current_task.get_instance_state())
self.state = self.ASSESSING
elif current_task_state is None and self.current_task_number > 0:
@@ -280,18 +282,20 @@ class CombinedOpenEndedV1Module():
'attempts': 0,
'created': True,
'history': [{'answer': last_response}],
})
})
self.current_task = child_task_module(self.system, self.location,
self.current_task_parsed_xml, self.current_task_descriptor, self.static_data,
instance_state=current_task_state)
self.current_task_parsed_xml, self.current_task_descriptor,
self.static_data,
instance_state=current_task_state)
self.task_states.append(self.current_task.get_instance_state())
self.state = self.ASSESSING
else:
if self.current_task_number > 0 and not reset:
current_task_state = self.overwrite_state(current_task_state)
self.current_task = child_task_module(self.system, self.location,
self.current_task_parsed_xml, self.current_task_descriptor, self.static_data,
instance_state=current_task_state)
self.current_task_parsed_xml, self.current_task_descriptor,
self.static_data,
instance_state=current_task_state)
return True
@@ -307,8 +311,8 @@ class CombinedOpenEndedV1Module():
last_response_data = self.get_last_response(self.current_task_number - 1)
current_response_data = self.get_current_attributes(self.current_task_number)
if(current_response_data['min_score_to_attempt'] > last_response_data['score']
or current_response_data['max_score_to_attempt'] < last_response_data['score']):
if (current_response_data['min_score_to_attempt'] > last_response_data['score']
or current_response_data['max_score_to_attempt'] < last_response_data['score']):
self.state = self.DONE
self.allow_reset = True
@@ -334,8 +338,8 @@ class CombinedOpenEndedV1Module():
'display_name': self.display_name,
'accept_file_upload': self.accept_file_upload,
'location': self.location,
'legend_list' : LEGEND_LIST,
}
'legend_list': LEGEND_LIST,
}
return context
@@ -404,7 +408,7 @@ class CombinedOpenEndedV1Module():
task_parsed_xml = task_descriptor.definition_from_xml(etree_xml, self.system)
task = children['modules'][task_type](self.system, self.location, task_parsed_xml, task_descriptor,
self.static_data, instance_state=task_state)
self.static_data, instance_state=task_state)
last_response = task.latest_answer()
last_score = task.latest_score()
last_post_assessment = task.latest_post_assessment(self.system)
@@ -426,10 +430,10 @@ class CombinedOpenEndedV1Module():
rubric_scores = rubric_data['rubric_scores']
grader_types = rubric_data['grader_types']
feedback_items = rubric_data['feedback_items']
feedback_dicts = rubric_data['feedback_dicts']
feedback_dicts = rubric_data['feedback_dicts']
grader_ids = rubric_data['grader_ids']
submission_ids = rubric_data['submission_ids']
elif task_type== "selfassessment":
submission_ids = rubric_data['submission_ids']
elif task_type == "selfassessment":
rubric_scores = last_post_assessment
grader_types = ['SA']
feedback_items = ['']
@@ -446,7 +450,7 @@ class CombinedOpenEndedV1Module():
human_state = task.HUMAN_NAMES[state]
else:
human_state = state
if len(grader_types)>0:
if len(grader_types) > 0:
grader_type = grader_types[0]
else:
grader_type = "IN"
@@ -468,15 +472,15 @@ class CombinedOpenEndedV1Module():
'correct': last_correctness,
'min_score_to_attempt': min_score_to_attempt,
'max_score_to_attempt': max_score_to_attempt,
'rubric_scores' : rubric_scores,
'grader_types' : grader_types,
'feedback_items' : feedback_items,
'grader_type' : grader_type,
'human_grader_type' : human_grader_name,
'feedback_dicts' : feedback_dicts,
'grader_ids' : grader_ids,
'submission_ids' : submission_ids,
}
'rubric_scores': rubric_scores,
'grader_types': grader_types,
'feedback_items': feedback_items,
'grader_type': grader_type,
'human_grader_type': human_grader_name,
'feedback_dicts': feedback_dicts,
'grader_ids': grader_ids,
'submission_ids': submission_ids,
}
return last_response_dict
def update_task_states(self):
@@ -519,20 +523,27 @@ class CombinedOpenEndedV1Module():
Output: Dictionary to be rendered via ajax that contains the result html.
"""
all_responses = []
loop_up_to_task = self.current_task_number+1
for i in xrange(0,loop_up_to_task):
loop_up_to_task = self.current_task_number + 1
for i in xrange(0, loop_up_to_task):
all_responses.append(self.get_last_response(i))
rubric_scores = [all_responses[i]['rubric_scores'] for i in xrange(0,len(all_responses)) if len(all_responses[i]['rubric_scores'])>0 and all_responses[i]['grader_types'][0] in HUMAN_GRADER_TYPE.keys()]
grader_types = [all_responses[i]['grader_types'] for i in xrange(0,len(all_responses)) if len(all_responses[i]['grader_types'])>0 and all_responses[i]['grader_types'][0] in HUMAN_GRADER_TYPE.keys()]
feedback_items = [all_responses[i]['feedback_items'] for i in xrange(0,len(all_responses)) if len(all_responses[i]['feedback_items'])>0 and all_responses[i]['grader_types'][0] in HUMAN_GRADER_TYPE.keys()]
rubric_html = self.rubric_renderer.render_combined_rubric(stringify_children(self.static_data['rubric']), rubric_scores,
grader_types, feedback_items)
rubric_scores = [all_responses[i]['rubric_scores'] for i in xrange(0, len(all_responses)) if
len(all_responses[i]['rubric_scores']) > 0 and all_responses[i]['grader_types'][
0] in HUMAN_GRADER_TYPE.keys()]
grader_types = [all_responses[i]['grader_types'] for i in xrange(0, len(all_responses)) if
len(all_responses[i]['grader_types']) > 0 and all_responses[i]['grader_types'][
0] in HUMAN_GRADER_TYPE.keys()]
feedback_items = [all_responses[i]['feedback_items'] for i in xrange(0, len(all_responses)) if
len(all_responses[i]['feedback_items']) > 0 and all_responses[i]['grader_types'][
0] in HUMAN_GRADER_TYPE.keys()]
rubric_html = self.rubric_renderer.render_combined_rubric(stringify_children(self.static_data['rubric']),
rubric_scores,
grader_types, feedback_items)
response_dict = all_responses[-1]
context = {
'results': rubric_html,
'task_name' : 'Scored Rubric',
'class_name' : 'combined-rubric-container'
'task_name': 'Scored Rubric',
'class_name': 'combined-rubric-container'
}
html = self.system.render_template('{0}/combined_open_ended_results.html'.format(self.TEMPLATE_DIR), context)
return {'html': html, 'success': True}
@@ -544,8 +555,8 @@ class CombinedOpenEndedV1Module():
Output: Dictionary to be rendered via ajax that contains the result html.
"""
context = {
'legend_list' : LEGEND_LIST,
}
'legend_list': LEGEND_LIST,
}
html = self.system.render_template('{0}/combined_open_ended_legend.html'.format(self.TEMPLATE_DIR), context)
return {'html': html, 'success': True}
@@ -556,15 +567,16 @@ class CombinedOpenEndedV1Module():
Output: Dictionary to be rendered via ajax that contains the result html.
"""
self.update_task_states()
loop_up_to_task = self.current_task_number+1
all_responses =[]
for i in xrange(0,loop_up_to_task):
loop_up_to_task = self.current_task_number + 1
all_responses = []
for i in xrange(0, loop_up_to_task):
all_responses.append(self.get_last_response(i))
context_list = []
for ri in all_responses:
for i in xrange(0,len(ri['rubric_scores'])):
feedback = ri['feedback_dicts'][i].get('feedback','')
rubric_data = self.rubric_renderer.render_rubric(stringify_children(self.static_data['rubric']), ri['rubric_scores'][i])
for i in xrange(0, len(ri['rubric_scores'])):
feedback = ri['feedback_dicts'][i].get('feedback', '')
rubric_data = self.rubric_renderer.render_rubric(stringify_children(self.static_data['rubric']),
ri['rubric_scores'][i])
if rubric_data['success']:
rubric_html = rubric_data['html']
else:
@@ -572,23 +584,23 @@ class CombinedOpenEndedV1Module():
context = {
'rubric_html': rubric_html,
'grader_type': ri['grader_type'],
'feedback' : feedback,
'grader_id' : ri['grader_ids'][i],
'submission_id' : ri['submission_ids'][i],
'feedback': feedback,
'grader_id': ri['grader_ids'][i],
'submission_id': ri['submission_ids'][i],
}
context_list.append(context)
feedback_table = self.system.render_template('{0}/open_ended_result_table.html'.format(self.TEMPLATE_DIR), {
'context_list' : context_list,
'grader_type_image_dict' : GRADER_TYPE_IMAGE_DICT,
'human_grader_types' : HUMAN_GRADER_TYPE,
'context_list': context_list,
'grader_type_image_dict': GRADER_TYPE_IMAGE_DICT,
'human_grader_types': HUMAN_GRADER_TYPE,
'rows': 50,
'cols': 50,
})
context = {
'results': feedback_table,
'task_name' : "Feedback",
'class_name' : "result-container",
}
'task_name': "Feedback",
'class_name': "result-container",
}
html = self.system.render_template('{0}/combined_open_ended_results.html'.format(self.TEMPLATE_DIR), context)
return {'html': html, 'success': True}
@@ -617,8 +629,8 @@ class CombinedOpenEndedV1Module():
'reset': self.reset,
'get_results': self.get_results,
'get_combined_rubric': self.get_rubric,
'get_status' : self.get_status_ajax,
'get_legend' : self.get_legend,
'get_status': self.get_status_ajax,
'get_legend': self.get_legend,
}
if dispatch not in handlers:
@@ -681,7 +693,7 @@ class CombinedOpenEndedV1Module():
'task_states': self.task_states,
'attempts': self.attempts,
'ready_to_reset': self.allow_reset,
}
}
return json.dumps(state)
@@ -699,11 +711,12 @@ class CombinedOpenEndedV1Module():
context = {
'status_list': status,
'grader_type_image_dict' : GRADER_TYPE_IMAGE_DICT,
'legend_list' : LEGEND_LIST,
'render_via_ajax' : render_via_ajax,
'grader_type_image_dict': GRADER_TYPE_IMAGE_DICT,
'legend_list': LEGEND_LIST,
'render_via_ajax': render_via_ajax,
}
status_html = self.system.render_template("{0}/combined_open_ended_status.html".format(self.TEMPLATE_DIR), context)
status_html = self.system.render_template("{0}/combined_open_ended_status.html".format(self.TEMPLATE_DIR),
context)
return status_html
@@ -736,7 +749,7 @@ class CombinedOpenEndedV1Module():
score_dict = {
'score': score,
'total': max_score,
}
}
return score_dict
@@ -793,7 +806,9 @@ class CombinedOpenEndedV1Descriptor(XmlDescriptor, EditingDescriptor):
for child in expected_children:
if len(xml_object.xpath(child)) == 0:
#This is a staff_facing_error
raise ValueError("Combined Open Ended definition must include at least one '{0}' tag. Contact the learning sciences group for assistance.".format(child))
raise ValueError(
"Combined Open Ended definition must include at least one '{0}' tag. Contact the learning sciences group for assistance.".format(
child))
def parse_task(k):
"""Assumes that xml_object has child k"""

View File

@@ -4,24 +4,26 @@ from lxml import etree
log = logging.getLogger(__name__)
GRADER_TYPE_IMAGE_DICT = {
'SA' : '/static/images/self_assessment_icon.png',
'PE' : '/static/images/peer_grading_icon.png',
'ML' : '/static/images/ml_grading_icon.png',
'IN' : '/static/images/peer_grading_icon.png',
'BC' : '/static/images/ml_grading_icon.png',
}
'SA': '/static/images/self_assessment_icon.png',
'PE': '/static/images/peer_grading_icon.png',
'ML': '/static/images/ml_grading_icon.png',
'IN': '/static/images/peer_grading_icon.png',
'BC': '/static/images/ml_grading_icon.png',
}
HUMAN_GRADER_TYPE = {
'SA' : 'Self-Assessment',
'PE' : 'Peer-Assessment',
'IN' : 'Instructor-Assessment',
'ML' : 'AI-Assessment',
'BC' : 'AI-Assessment',
}
'SA': 'Self-Assessment',
'PE': 'Peer-Assessment',
'IN': 'Instructor-Assessment',
'ML': 'AI-Assessment',
'BC': 'AI-Assessment',
}
DO_NOT_DISPLAY = ['BC', 'IN']
LEGEND_LIST = [{'name' : HUMAN_GRADER_TYPE[k], 'image' : GRADER_TYPE_IMAGE_DICT[k]} for k in GRADER_TYPE_IMAGE_DICT.keys() if k not in DO_NOT_DISPLAY ]
LEGEND_LIST = [{'name': HUMAN_GRADER_TYPE[k], 'image': GRADER_TYPE_IMAGE_DICT[k]} for k in GRADER_TYPE_IMAGE_DICT.keys()
if k not in DO_NOT_DISPLAY]
class RubricParsingError(Exception):
def __init__(self, msg):
@@ -29,15 +31,14 @@ class RubricParsingError(Exception):
class CombinedOpenEndedRubric(object):
TEMPLATE_DIR = "combinedopenended/openended"
def __init__ (self, system, view_only = False):
def __init__(self, system, view_only=False):
self.has_score = False
self.view_only = view_only
self.system = system
def render_rubric(self, rubric_xml, score_list = None):
def render_rubric(self, rubric_xml, score_list=None):
'''
render_rubric: takes in an xml string and outputs the corresponding
html for that xml, given the type of rubric we're generating
@@ -50,11 +51,11 @@ class CombinedOpenEndedRubric(object):
success = False
try:
rubric_categories = self.extract_categories(rubric_xml)
if score_list and len(score_list)==len(rubric_categories):
for i in xrange(0,len(rubric_categories)):
if score_list and len(score_list) == len(rubric_categories):
for i in xrange(0, len(rubric_categories)):
category = rubric_categories[i]
for j in xrange(0,len(category['options'])):
if score_list[i]==j:
for j in xrange(0, len(category['options'])):
if score_list[i] == j:
rubric_categories[i]['options'][j]['selected'] = True
rubric_scores = [cat['score'] for cat in rubric_categories]
max_scores = map((lambda cat: cat['options'][-1]['points']), rubric_categories)
@@ -63,19 +64,20 @@ class CombinedOpenEndedRubric(object):
if self.view_only:
rubric_template = '{0}/open_ended_view_only_rubric.html'.format(self.TEMPLATE_DIR)
html = self.system.render_template(rubric_template,
{'categories': rubric_categories,
'has_score': self.has_score,
'view_only': self.view_only,
'max_score': max_score,
'combined_rubric' : False
})
{'categories': rubric_categories,
'has_score': self.has_score,
'view_only': self.view_only,
'max_score': max_score,
'combined_rubric': False
})
success = True
except:
#This is a staff_facing_error
error_message = "[render_rubric] Could not parse the rubric with xml: {0}. Contact the learning sciences group for assistance.".format(rubric_xml)
error_message = "[render_rubric] Could not parse the rubric with xml: {0}. Contact the learning sciences group for assistance.".format(
rubric_xml)
log.exception(error_message)
raise RubricParsingError(error_message)
return {'success' : success, 'html' : html, 'rubric_scores' : rubric_scores}
return {'success': success, 'html': html, 'rubric_scores': rubric_scores}
def check_if_rubric_is_parseable(self, rubric_string, location, max_score_allowed, max_score):
rubric_dict = self.render_rubric(rubric_string)
@@ -83,7 +85,8 @@ class CombinedOpenEndedRubric(object):
rubric_feedback = rubric_dict['html']
if not success:
#This is a staff_facing_error
error_message = "Could not parse rubric : {0} for location {1}. Contact the learning sciences group for assistance.".format(rubric_string, location.url())
error_message = "Could not parse rubric : {0} for location {1}. Contact the learning sciences group for assistance.".format(
rubric_string, location.url())
log.error(error_message)
raise RubricParsingError(error_message)
@@ -101,7 +104,7 @@ class CombinedOpenEndedRubric(object):
if total != max_score:
#This is a staff_facing_error
error_msg = "The max score {0} for problem {1} does not match the total number of points in the rubric {2}. Contact the learning sciences group for assistance.".format(
max_score, location, total)
max_score, location, total)
log.error(error_msg)
raise RubricParsingError(error_msg)
@@ -123,12 +126,13 @@ class CombinedOpenEndedRubric(object):
for category in element:
if category.tag != 'category':
#This is a staff_facing_error
raise RubricParsingError("[extract_categories] Expected a <category> tag: got {0} instead. Contact the learning sciences group for assistance.".format(category.tag))
raise RubricParsingError(
"[extract_categories] Expected a <category> tag: got {0} instead. Contact the learning sciences group for assistance.".format(
category.tag))
else:
categories.append(self.extract_category(category))
return categories
def extract_category(self, category):
'''
construct an individual category
@@ -150,13 +154,17 @@ class CombinedOpenEndedRubric(object):
# if we are missing the score tag and we are expecting one
elif self.has_score:
#This is a staff_facing_error
raise RubricParsingError("[extract_category] Category {0} is missing a score. Contact the learning sciences group for assistance.".format(descriptionxml.text))
raise RubricParsingError(
"[extract_category] Category {0} is missing a score. Contact the learning sciences group for assistance.".format(
descriptionxml.text))
# parse description
if descriptionxml.tag != 'description':
#This is a staff_facing_error
raise RubricParsingError("[extract_category]: expected description tag, got {0} instead. Contact the learning sciences group for assistance.".format(descriptionxml.tag))
raise RubricParsingError(
"[extract_category]: expected description tag, got {0} instead. Contact the learning sciences group for assistance.".format(
descriptionxml.tag))
description = descriptionxml.text
@@ -167,7 +175,9 @@ class CombinedOpenEndedRubric(object):
for option in optionsxml:
if option.tag != 'option':
#This is a staff_facing_error
raise RubricParsingError("[extract_category]: expected option tag, got {0} instead. Contact the learning sciences group for assistance.".format(option.tag))
raise RubricParsingError(
"[extract_category]: expected option tag, got {0} instead. Contact the learning sciences group for assistance.".format(
option.tag))
else:
pointstr = option.get("points")
if pointstr:
@@ -177,13 +187,16 @@ class CombinedOpenEndedRubric(object):
points = int(pointstr)
except ValueError:
#This is a staff_facing_error
raise RubricParsingError("[extract_category]: expected points to have int, got {0} instead. Contact the learning sciences group for assistance.".format(pointstr))
raise RubricParsingError(
"[extract_category]: expected points to have int, got {0} instead. Contact the learning sciences group for assistance.".format(
pointstr))
elif autonumbering:
# use the generated one if we're in the right mode
points = cur_points
cur_points = cur_points + 1
else:
raise Exception("[extract_category]: missing points attribute. Cannot continue to auto-create points values after a points value is explicitly defined.")
raise Exception(
"[extract_category]: missing points attribute. Cannot continue to auto-create points values after a points value is explicitly defined.")
selected = score == points
optiontext = option.text
@@ -193,34 +206,34 @@ class CombinedOpenEndedRubric(object):
options = sorted(options, key=lambda option: option['points'])
CombinedOpenEndedRubric.validate_options(options)
return {'description': description, 'options': options, 'score' : score}
return {'description': description, 'options': options, 'score': score}
def render_combined_rubric(self,rubric_xml,scores,score_types,feedback_types):
success, score_tuples = CombinedOpenEndedRubric.reformat_scores_for_rendering(scores,score_types,feedback_types)
def render_combined_rubric(self, rubric_xml, scores, score_types, feedback_types):
success, score_tuples = CombinedOpenEndedRubric.reformat_scores_for_rendering(scores, score_types,
feedback_types)
rubric_categories = self.extract_categories(rubric_xml)
max_scores = map((lambda cat: cat['options'][-1]['points']), rubric_categories)
max_score = max(max_scores)
for i in xrange(0,len(rubric_categories)):
for i in xrange(0, len(rubric_categories)):
category = rubric_categories[i]
for j in xrange(0,len(category['options'])):
for j in xrange(0, len(category['options'])):
rubric_categories[i]['options'][j]['grader_types'] = []
for tuple in score_tuples:
if tuple[1] == i and tuple[2] ==j:
if tuple[1] == i and tuple[2] == j:
for grader_type in tuple[3]:
rubric_categories[i]['options'][j]['grader_types'].append(grader_type)
html = self.system.render_template('{0}/open_ended_combined_rubric.html'.format(self.TEMPLATE_DIR),
{'categories': rubric_categories,
'has_score': True,
'view_only': True,
'max_score': max_score,
'combined_rubric' : True,
'grader_type_image_dict' : GRADER_TYPE_IMAGE_DICT,
'human_grader_types' : HUMAN_GRADER_TYPE,
})
{'categories': rubric_categories,
'has_score': True,
'view_only': True,
'max_score': max_score,
'combined_rubric': True,
'grader_type_image_dict': GRADER_TYPE_IMAGE_DICT,
'human_grader_types': HUMAN_GRADER_TYPE,
})
return html
@staticmethod
def validate_options(options):
'''
@@ -228,14 +241,16 @@ class CombinedOpenEndedRubric(object):
'''
if len(options) == 0:
#This is a staff_facing_error
raise RubricParsingError("[extract_category]: no options associated with this category. Contact the learning sciences group for assistance.")
raise RubricParsingError(
"[extract_category]: no options associated with this category. Contact the learning sciences group for assistance.")
if len(options) == 1:
return
prev = options[0]['points']
for option in options[1:]:
if prev == option['points']:
#This is a staff_facing_error
raise RubricParsingError("[extract_category]: found duplicate point values between two different options. Contact the learning sciences group for assistance.")
raise RubricParsingError(
"[extract_category]: found duplicate point values between two different options. Contact the learning sciences group for assistance.")
else:
prev = option['points']
@@ -250,7 +265,7 @@ class CombinedOpenEndedRubric(object):
@return:
"""
success = False
if len(scores)==0:
if len(scores) == 0:
#This is a dev_facing_error
log.error("Score length is 0 when trying to reformat rubric scores for rendering.")
return success, ""
@@ -264,25 +279,25 @@ class CombinedOpenEndedRubric(object):
score_lists = []
score_type_list = []
feedback_type_list = []
for i in xrange(0,len(scores)):
for i in xrange(0, len(scores)):
score_cont_list = scores[i]
for j in xrange(0,len(score_cont_list)):
for j in xrange(0, len(score_cont_list)):
score_list = score_cont_list[j]
score_lists.append(score_list)
score_type_list.append(score_types[i][j])
feedback_type_list.append(feedback_types[i][j])
score_list_len = len(score_lists[0])
for i in xrange(0,len(score_lists)):
for i in xrange(0, len(score_lists)):
score_list = score_lists[i]
if len(score_list)!=score_list_len:
if len(score_list) != score_list_len:
return success, ""
score_tuples = []
for i in xrange(0,len(score_lists)):
for j in xrange(0,len(score_lists[i])):
tuple = [1,j,score_lists[i][j],[],[]]
score_tuples, tup_ind = CombinedOpenEndedRubric.check_for_tuple_matches(score_tuples,tuple)
for i in xrange(0, len(score_lists)):
for j in xrange(0, len(score_lists[i])):
tuple = [1, j, score_lists[i][j], [], []]
score_tuples, tup_ind = CombinedOpenEndedRubric.check_for_tuple_matches(score_tuples, tuple)
score_tuples[tup_ind][0] += 1
score_tuples[tup_ind][3].append(score_type_list[i])
score_tuples[tup_ind][4].append(feedback_type_list[i])
@@ -302,18 +317,12 @@ class CombinedOpenEndedRubric(object):
category = tuple[1]
score = tuple[2]
tup_ind = -1
for t in xrange(0,len(tuples)):
for t in xrange(0, len(tuples)):
if tuples[t][1] == category and tuples[t][2] == score:
tup_ind = t
break
if tup_ind == -1:
tuples.append([0,category,score,[],[]])
tup_ind = len(tuples)-1
tuples.append([0, category, score, [], []])
tup_ind = len(tuples) - 1
return tuples, tup_ind

View File

@@ -8,6 +8,7 @@ class ControllerQueryService(GradingService):
"""
Interface to staff grading backend.
"""
def __init__(self, config, system):
config['system'] = system
super(ControllerQueryService, self).__init__(config)
@@ -59,7 +60,7 @@ class ControllerQueryService(GradingService):
def get_flagged_problem_list(self, course_id):
params = {
'course_id': course_id,
}
}
response = self.get(self.flagged_problem_list_url, params)
return response
@@ -70,20 +71,21 @@ class ControllerQueryService(GradingService):
'student_id': student_id,
'submission_id': submission_id,
'action_type': action_type
}
}
response = self.post(self.take_action_on_flags_url, params)
return response
def convert_seconds_to_human_readable(seconds):
if seconds < 60:
human_string = "{0} seconds".format(seconds)
elif seconds < 60 * 60:
human_string = "{0} minutes".format(round(seconds/60,1))
elif seconds < (24*60*60):
human_string = "{0} hours".format(round(seconds/(60*60),1))
human_string = "{0} minutes".format(round(seconds / 60, 1))
elif seconds < (24 * 60 * 60):
human_string = "{0} hours".format(round(seconds / (60 * 60), 1))
else:
human_string = "{0} days".format(round(seconds/(60*60*24),1))
human_string = "{0} days".format(round(seconds / (60 * 60 * 24), 1))
eta_string = "{0}".format(human_string)
return eta_string

View File

@@ -19,6 +19,7 @@ class GradingService(object):
"""
Interface to staff grading backend.
"""
def __init__(self, config):
self.username = config['username']
self.password = config['password']
@@ -34,8 +35,8 @@ class GradingService(object):
Returns the decoded json dict of the response.
"""
response = self.session.post(self.login_url,
{'username': self.username,
'password': self.password, })
{'username': self.username,
'password': self.password, })
response.raise_for_status()
@@ -47,7 +48,7 @@ class GradingService(object):
"""
try:
op = lambda: self.session.post(url, data=data,
allow_redirects=allow_redirects)
allow_redirects=allow_redirects)
r = self._try_with_login(op)
except (RequestException, ConnectionError, HTTPError) as err:
# reraise as promised GradingServiceError, but preserve stacktrace.
@@ -63,8 +64,8 @@ class GradingService(object):
"""
log.debug(params)
op = lambda: self.session.get(url,
allow_redirects=allow_redirects,
params=params)
allow_redirects=allow_redirects,
params=params)
try:
r = self._try_with_login(op)
except (RequestException, ConnectionError, HTTPError) as err:
@@ -92,7 +93,7 @@ class GradingService(object):
r = self._login()
if r and not r.get('success'):
log.warning("Couldn't log into staff_grading backend. Response: %s",
r)
r)
# try again
response = operation()
response.raise_for_status()

View File

@@ -5,6 +5,7 @@ to send them to S3.
try:
from PIL import Image
ENABLE_PIL = True
except:
ENABLE_PIL = False
@@ -51,6 +52,7 @@ class ImageProperties(object):
"""
Class to check properties of an image and to validate if they are allowed.
"""
def __init__(self, image_data):
"""
Initializes class variables
@@ -92,7 +94,7 @@ class ImageProperties(object):
g = rgb[1]
b = rgb[2]
check_r = (r > 60)
check_g = (r * 0.4) < g < (r * 0.85)
check_g = (r * 0.4) < g < (r * 0.85)
check_b = (r * 0.2) < b < (r * 0.7)
colors_okay = check_r and check_b and check_g
except:
@@ -141,6 +143,7 @@ class URLProperties(object):
Checks to see if a URL points to acceptable content. Added to check if students are submitting reasonable
links to the peer grading image functionality of the external grading service.
"""
def __init__(self, url_string):
self.url_string = url_string
@@ -212,7 +215,7 @@ def run_image_tests(image):
success = image_properties.run_tests()
except:
log.exception("Cannot run image tests in combined open ended xmodule. May be an issue with a particular image,"
"or an issue with the deployment configuration of PIL/Pillow")
"or an issue with the deployment configuration of PIL/Pillow")
return success
@@ -252,7 +255,8 @@ def upload_to_s3(file_to_upload, keyname, s3_interface):
return True, public_url
except:
#This is a dev_facing_error
error_message = "Could not connect to S3 to upload peer grading image. Trying to utilize bucket: {0}".format(bucketname.lower())
error_message = "Could not connect to S3 to upload peer grading image. Trying to utilize bucket: {0}".format(
bucketname.lower())
log.error(error_message)
return False, error_message

View File

@@ -10,7 +10,7 @@ import logging
from lxml import etree
import capa.xqueue_interface as xqueue_interface
from xmodule.capa_module import ComplexEncoder
from xmodule.capa_module import ComplexEncoder
from xmodule.editing_module import EditingDescriptor
from xmodule.progress import Progress
from xmodule.stringify import stringify_children
@@ -77,7 +77,6 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
self.send_to_grader(self.latest_answer(), system)
self.created = False
def _parse(self, oeparam, prompt, rubric, system):
'''
Parse OpenEndedResponse XML:
@@ -104,7 +103,9 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
# response types)
except TypeError, ValueError:
#This is a dev_facing_error
log.exception("Grader payload from external open ended grading server is not a json object! Object: {0}".format(grader_payload))
log.exception(
"Grader payload from external open ended grading server is not a json object! Object: {0}".format(
grader_payload))
self.initial_display = find_with_default(oeparam, 'initial_display', '')
self.answer = find_with_default(oeparam, 'answer_display', 'No answer given.')
@@ -148,7 +149,9 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
for tag in ['feedback', 'submission_id', 'grader_id', 'score']:
if tag not in survey_responses:
#This is a student_facing_error
return {'success': False, 'msg': "Could not find needed tag {0} in the survey responses. Please try submitting again.".format(tag)}
return {'success': False,
'msg': "Could not find needed tag {0} in the survey responses. Please try submitting again.".format(
tag)}
try:
submission_id = int(survey_responses['submission_id'])
grader_id = int(survey_responses['grader_id'])
@@ -188,7 +191,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
}
(error, msg) = qinterface.send_to_queue(header=xheader,
body=json.dumps(contents))
body=json.dumps(contents))
#Convert error to a success value
success = True
@@ -222,8 +225,8 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
str(len(self.history)))
xheader = xqueue_interface.make_xheader(lms_callback_url=system.xqueue['callback_url'],
lms_key=queuekey,
queue_name=self.queue_name)
lms_key=queuekey,
queue_name=self.queue_name)
contents = self.payload.copy()
@@ -241,7 +244,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
# Submit request. When successful, 'msg' is the prior length of the queue
(error, msg) = qinterface.send_to_queue(header=xheader,
body=json.dumps(contents))
body=json.dumps(contents))
# State associated with the queueing request
queuestate = {'key': queuekey,
@@ -266,7 +269,6 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
return True
def get_answers(self):
"""
Gets and shows the answer for this problem.
@@ -300,7 +302,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
# We want to display available feedback in a particular order.
# This dictionary specifies which goes first--lower first.
priorities = { # These go at the start of the feedback
priorities = {# These go at the start of the feedback
'spelling': 0,
'grammar': 1,
# needs to be after all the other feedback
@@ -400,7 +402,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
if not response_items['success']:
return system.render_template("{0}/open_ended_error.html".format(self.TEMPLATE_DIR),
{'errors': feedback})
{'errors': feedback})
feedback_template = system.render_template("{0}/open_ended_feedback.html".format(self.TEMPLATE_DIR), {
'grader_type': response_items['grader_type'],
@@ -411,7 +413,6 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
return feedback_template, rubric_scores
def _parse_score_msg(self, score_msg, system, join_feedback=True):
"""
Grader reply is a JSON-dump of the following dict
@@ -437,13 +438,13 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
'valid': False,
'score': 0,
'feedback': '',
'rubric_scores' : [[0]],
'grader_types' : [''],
'feedback_items' : [''],
'feedback_dicts' : [{}],
'grader_ids' : [0],
'submission_ids' : [0],
}
'rubric_scores': [[0]],
'grader_types': [''],
'feedback_items': [''],
'feedback_dicts': [{}],
'grader_ids': [0],
'submission_ids': [0],
}
try:
score_result = json.loads(score_msg)
except (TypeError, ValueError):
@@ -470,7 +471,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
log.error(error_message)
fail['feedback'] = error_message
return fail
#This is to support peer grading
#This is to support peer grading
if isinstance(score_result['score'], list):
feedback_items = []
rubric_scores = []
@@ -527,12 +528,12 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
'valid': True,
'score': score,
'feedback': feedback,
'rubric_scores' : rubric_scores,
'grader_types' : grader_types,
'feedback_items' : feedback_items,
'feedback_dicts' : feedback_dicts,
'grader_ids' : grader_ids,
'submission_ids' : submission_ids,
'rubric_scores': rubric_scores,
'grader_types': grader_types,
'feedback_items': feedback_items,
'feedback_dicts': feedback_dicts,
'grader_ids': grader_ids,
'submission_ids': submission_ids,
}
def latest_post_assessment(self, system, short_feedback=False, join_feedback=True):
@@ -545,7 +546,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
return ""
feedback_dict = self._parse_score_msg(self.history[-1].get('post_assessment', ""), system,
join_feedback=join_feedback)
join_feedback=join_feedback)
if not short_feedback:
return feedback_dict['feedback'] if feedback_dict['valid'] else ''
if feedback_dict['valid']:
@@ -585,7 +586,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
#This is a dev_facing_error
log.error("Cannot find {0} in handlers in handle_ajax function for open_ended_module.py".format(dispatch))
#This is a dev_facing_error
return json.dumps({'error': 'Error handling action. Please try again.', 'success' : False})
return json.dumps({'error': 'Error handling action. Please try again.', 'success': False})
before = self.get_progress()
d = handlers[dispatch](get, system)
@@ -679,7 +680,6 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
correct = ""
previous_answer = self.initial_display
context = {
'prompt': self.prompt,
'previous_answer': previous_answer,
@@ -692,7 +692,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
'child_type': 'openended',
'correct': correct,
'accept_file_upload': self.accept_file_upload,
'eta_message' : eta_string,
'eta_message': eta_string,
}
html = system.render_template('{0}/open_ended.html'.format(self.TEMPLATE_DIR), context)
return html
@@ -723,7 +723,9 @@ class OpenEndedDescriptor(XmlDescriptor, EditingDescriptor):
for child in ['openendedparam']:
if len(xml_object.xpath(child)) != 1:
#This is a staff_facing_error
raise ValueError("Open Ended definition must include exactly one '{0}' tag. Contact the learning sciences group for assistance.".format(child))
raise ValueError(
"Open Ended definition must include exactly one '{0}' tag. Contact the learning sciences group for assistance.".format(
child))
def parse(k):
"""Assumes that xml_object has child k"""

View File

@@ -74,7 +74,7 @@ class OpenEndedChild(object):
'done': 'Done',
}
def __init__(self, system, location, definition, descriptor, static_data,
def __init__(self, system, location, definition, descriptor, static_data,
instance_state=None, shared_state=None, **kwargs):
# Load instance state
if instance_state is not None:
@@ -108,15 +108,14 @@ class OpenEndedChild(object):
self._max_score = static_data['max_score']
if system.open_ended_grading_interface:
self.peer_gs = PeerGradingService(system.open_ended_grading_interface, system)
self.controller_qs = controller_query_service.ControllerQueryService(system.open_ended_grading_interface,system)
self.controller_qs = controller_query_service.ControllerQueryService(system.open_ended_grading_interface,
system)
else:
self.peer_gs = MockPeerGradingService()
self.controller_qs = None
self.controller_qs = None
self.system = system
self.location_string = location
try:
self.location_string = self.location_string.url()
@@ -152,7 +151,8 @@ class OpenEndedChild(object):
return True, {
'success': False,
#This is a student_facing_error
'error': 'You have attempted this problem {0} times. You are allowed {1} attempts.'.format(self.attempts, self.max_attempts)
'error': 'You have attempted this problem {0} times. You are allowed {1} attempts.'.format(
self.attempts, self.max_attempts)
}
else:
return False, {}
@@ -180,8 +180,8 @@ class OpenEndedChild(object):
try:
answer = autolink_html(answer)
cleaner = Cleaner(style=True, links=True, add_nofollow=False, page_structure=True, safe_attrs_only=True,
host_whitelist=open_ended_image_submission.TRUSTED_IMAGE_DOMAINS,
whitelist_tags=set(['embed', 'iframe', 'a', 'img']))
host_whitelist=open_ended_image_submission.TRUSTED_IMAGE_DOMAINS,
whitelist_tags=set(['embed', 'iframe', 'a', 'img']))
clean_html = cleaner.clean_html(answer)
clean_html = re.sub(r'</p>$', '', re.sub(r'^<p>', '', clean_html))
except:
@@ -282,7 +282,7 @@ class OpenEndedChild(object):
"""
#This is a dev_facing_error
log.warning("Open ended child state out sync. state: %r, get: %r. %s",
self.state, get, msg)
self.state, get, msg)
#This is a student_facing_error
return {'success': False,
'error': 'The problem state got out-of-sync. Please try reloading the page.'}
@@ -308,7 +308,7 @@ class OpenEndedChild(object):
@return: Boolean correct.
"""
correct = False
if(isinstance(score, (int, long, float, complex))):
if (isinstance(score, (int, long, float, complex))):
score_ratio = int(score) / float(self.max_score())
correct = (score_ratio >= 0.66)
return correct
@@ -342,7 +342,8 @@ class OpenEndedChild(object):
try:
image_data.seek(0)
success, s3_public_url = open_ended_image_submission.upload_to_s3(image_data, image_key, self.s3_interface)
success, s3_public_url = open_ended_image_submission.upload_to_s3(image_data, image_key,
self.s3_interface)
except:
log.exception("Could not upload image to S3.")
@@ -404,9 +405,9 @@ class OpenEndedChild(object):
#In this case, an image was submitted by the student, but the image could not be uploaded to S3. Likely
#a config issue (development vs deployment). For now, just treat this as a "success"
log.exception("Student AJAX post to combined open ended xmodule indicated that it contained an image, "
"but the image was not able to be uploaded to S3. This could indicate a config"
"issue with this deployment, but it could also indicate a problem with S3 or with the"
"student image itself.")
"but the image was not able to be uploaded to S3. This could indicate a config"
"issue with this deployment, but it could also indicate a problem with S3 or with the"
"student image itself.")
overall_success = True
elif not has_file_to_upload:
#If there is no file to upload, probably the student has embedded the link in the answer text
@@ -445,7 +446,7 @@ class OpenEndedChild(object):
response = {}
#This is a student_facing_error
error_string = ("You need to peer grade {0} more in order to make another submission. "
"You have graded {1}, and {2} are required. You have made {3} successful peer grading submissions.")
"You have graded {1}, and {2} are required. You have made {3} successful peer grading submissions.")
try:
response = self.peer_gs.get_data_for_location(self.location_string, student_id)
count_graded = response['count_graded']
@@ -454,16 +455,18 @@ class OpenEndedChild(object):
success = True
except:
#This is a dev_facing_error
log.error("Could not contact external open ended graders for location {0} and student {1}".format(self.location_string,student_id))
log.error("Could not contact external open ended graders for location {0} and student {1}".format(
self.location_string, student_id))
#This is a student_facing_error
error_message = "Could not contact the graders. Please notify course staff."
return success, allowed_to_submit, error_message
if count_graded>=count_required:
if count_graded >= count_required:
return success, allowed_to_submit, ""
else:
allowed_to_submit = False
#This is a student_facing_error
error_message = error_string.format(count_required-count_graded, count_graded, count_required, student_sub_count)
error_message = error_string.format(count_required - count_graded, count_graded, count_required,
student_sub_count)
return success, allowed_to_submit, error_message
def get_eta(self):
@@ -478,7 +481,7 @@ class OpenEndedChild(object):
success = response['success']
if isinstance(success, basestring):
success = (success.lower()=="true")
success = (success.lower() == "true")
if success:
eta = controller_query_service.convert_seconds_to_human_readable(response['eta'])
@@ -487,6 +490,3 @@ class OpenEndedChild(object):
eta_string = ""
return eta_string

View File

@@ -14,6 +14,7 @@ class PeerGradingService(GradingService):
"""
Interface with the grading controller for peer grading
"""
def __init__(self, config, system):
config['system'] = system
super(PeerGradingService, self).__init__(config)
@@ -36,10 +37,11 @@ class PeerGradingService(GradingService):
def get_next_submission(self, problem_location, grader_id):
response = self.get(self.get_next_submission_url,
{'location': problem_location, 'grader_id': grader_id})
{'location': problem_location, 'grader_id': grader_id})
return self.try_to_decode(self._render_rubric(response))
def save_grade(self, location, grader_id, submission_id, score, feedback, submission_key, rubric_scores, submission_flagged):
def save_grade(self, location, grader_id, submission_id, score, feedback, submission_key, rubric_scores,
submission_flagged):
data = {'grader_id': grader_id,
'submission_id': submission_id,
'score': score,
@@ -89,6 +91,7 @@ class PeerGradingService(GradingService):
pass
return text
"""
This is a mock peer grading service that can be used for unit tests
without making actual service calls to the grading controller
@@ -122,7 +125,7 @@ class MockPeerGradingService(object):
'max_score': 4})
def save_calibration_essay(self, problem_location, grader_id,
calibration_essay_id, submission_key, score,
calibration_essay_id, submission_key, score,
feedback, rubric_scores):
return {'success': True, 'actual_score': 2}

View File

@@ -73,7 +73,6 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
html = system.render_template('{0}/self_assessment_prompt.html'.format(self.TEMPLATE_DIR), context)
return html
def handle_ajax(self, dispatch, get, system):
"""
This is called by courseware.module_render, to handle an AJAX call.
@@ -95,7 +94,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
#This is a dev_facing_error
log.error("Cannot find {0} in handlers in handle_ajax function for open_ended_module.py".format(dispatch))
#This is a dev_facing_error
return json.dumps({'error': 'Error handling action. Please try again.', 'success' : False})
return json.dumps({'error': 'Error handling action. Please try again.', 'success': False})
before = self.get_progress()
d = handlers[dispatch](get, system)
@@ -159,7 +158,6 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
return system.render_template('{0}/self_assessment_hint.html'.format(self.TEMPLATE_DIR), context)
def save_answer(self, get, system):
"""
After the answer is submitted, show the rubric.
@@ -224,7 +222,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
try:
score = int(get['assessment'])
score_list = get.getlist('score_list[]')
for i in xrange(0,len(score_list)):
for i in xrange(0, len(score_list)):
score_list[i] = int(score_list[i])
except ValueError:
#This is a dev_facing_error
@@ -268,7 +266,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
'allow_reset': self._allow_reset()}
def latest_post_assessment(self, system):
latest_post_assessment = super(SelfAssessmentModule, self).latest_post_assessment(system)
latest_post_assessment = super(SelfAssessmentModule, self).latest_post_assessment(system)
try:
rubric_scores = json.loads(latest_post_assessment)
except:
@@ -305,7 +303,9 @@ class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor):
for child in expected_children:
if len(xml_object.xpath(child)) != 1:
#This is a staff_facing_error
raise ValueError("Self assessment definition must include exactly one '{0}' tag. Contact the learning sciences group for assistance.".format(child))
raise ValueError(
"Self assessment definition must include exactly one '{0}' tag. Contact the learning sciences group for assistance.".format(
child))
def parse(k):
"""Assumes that xml_object has child k"""

View File

@@ -5,7 +5,7 @@ from lxml import etree
from datetime import datetime
from pkg_resources import resource_string
from .capa_module import ComplexEncoder
from .capa_module import ComplexEncoder
from .editing_module import EditingDescriptor
from .stringify import stringify_children
from .x_module import XModule
@@ -34,7 +34,7 @@ class PeerGradingModule(XModule):
resource_string(__name__, 'js/src/peergrading/peer_grading_problem.coffee'),
resource_string(__name__, 'js/src/collapsible.coffee'),
resource_string(__name__, 'js/src/javascript_loader.coffee'),
]}
]}
js_module_name = "PeerGrading"
css = {'scss': [resource_string(__name__, 'css/combinedopenended/display.scss')]}
@@ -42,7 +42,7 @@ class PeerGradingModule(XModule):
def __init__(self, system, location, definition, descriptor,
instance_state=None, shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition, descriptor,
instance_state, shared_state, **kwargs)
instance_state, shared_state, **kwargs)
# Load instance state
if instance_state is not None:
@@ -53,12 +53,11 @@ class PeerGradingModule(XModule):
#We need to set the location here so the child modules can use it
system.set('location', location)
self.system = system
if(self.system.open_ended_grading_interface):
if (self.system.open_ended_grading_interface):
self.peer_gs = PeerGradingService(self.system.open_ended_grading_interface, self.system)
else:
self.peer_gs = MockPeerGradingService()
self.use_for_single_location = self.metadata.get('use_for_single_location', USE_FOR_SINGLE_LOCATION)
if isinstance(self.use_for_single_location, basestring):
self.use_for_single_location = (self.use_for_single_location in TRUE_DICT)
@@ -83,14 +82,13 @@ class PeerGradingModule(XModule):
grace_period_string = self.metadata.get('graceperiod', None)
try:
self.timeinfo = TimeInfo(display_due_date_string, grace_period_string)
self.timeinfo = TimeInfo(display_due_date_string, grace_period_string)
except:
log.error("Error parsing due date information in location {0}".format(location))
raise
self.display_due_date = self.timeinfo.display_due_date
self.ajax_url = self.system.ajax_url
if not self.ajax_url.endswith("/"):
self.ajax_url = self.ajax_url + "/"
@@ -148,13 +146,13 @@ class PeerGradingModule(XModule):
'save_grade': self.save_grade,
'save_calibration_essay': self.save_calibration_essay,
'problem': self.peer_grading_problem,
}
}
if dispatch not in handlers:
#This is a dev_facing_error
log.error("Cannot find {0} in handlers in handle_ajax function for open_ended_module.py".format(dispatch))
#This is a dev_facing_error
return json.dumps({'error': 'Error handling action. Please try again.', 'success' : False})
return json.dumps({'error': 'Error handling action. Please try again.', 'success': False})
d = handlers[dispatch](get)
@@ -191,9 +189,10 @@ class PeerGradingModule(XModule):
except:
success, response = self.query_data_for_location()
if not success:
log.exception("No instance data found and could not get data from controller for loc {0} student {1}".format(
self.system.location.url(), self.system.anonymous_student_id
))
log.exception(
"No instance data found and could not get data from controller for loc {0} student {1}".format(
self.system.location.url(), self.system.anonymous_student_id
))
return None
count_graded = response['count_graded']
count_required = response['count_required']
@@ -204,7 +203,7 @@ class PeerGradingModule(XModule):
score_dict = {
'score': int(count_graded >= count_required),
'total': self.max_grade,
}
}
return score_dict
@@ -253,7 +252,7 @@ class PeerGradingModule(XModule):
.format(self.peer_gs.url, location, grader_id))
#This is a student_facing_error
return {'success': False,
'error': EXTERNAL_GRADER_NO_CONTACT_ERROR}
'error': EXTERNAL_GRADER_NO_CONTACT_ERROR}
def save_grade(self, get):
"""
@@ -271,7 +270,8 @@ class PeerGradingModule(XModule):
error: if there was an error in the submission, this is the error message
"""
required = set(['location', 'submission_id', 'submission_key', 'score', 'feedback', 'rubric_scores[]', 'submission_flagged'])
required = set(['location', 'submission_id', 'submission_key', 'score', 'feedback', 'rubric_scores[]',
'submission_flagged'])
success, message = self._check_required(get, required)
if not success:
return self._err_response(message)
@@ -287,14 +287,14 @@ class PeerGradingModule(XModule):
try:
response = self.peer_gs.save_grade(location, grader_id, submission_id,
score, feedback, submission_key, rubric_scores, submission_flagged)
score, feedback, submission_key, rubric_scores, submission_flagged)
return response
except GradingServiceError:
#This is a dev_facing_error
log.exception("""Error saving grade to open ended grading service. server url: {0}, location: {1}, submission_id:{2},
submission_key: {3}, score: {4}"""
.format(self.peer_gs.url,
location, submission_id, submission_key, score)
location, submission_id, submission_key, score)
)
#This is a student_facing_error
return {
@@ -382,7 +382,7 @@ class PeerGradingModule(XModule):
.format(self.peer_gs.url, location))
#This is a student_facing_error
return {'success': False,
'error': EXTERNAL_GRADER_NO_CONTACT_ERROR}
'error': EXTERNAL_GRADER_NO_CONTACT_ERROR}
# if we can't parse the rubric into HTML,
except etree.XMLSyntaxError:
#This is a dev_facing_error
@@ -390,7 +390,7 @@ class PeerGradingModule(XModule):
.format(rubric))
#This is a student_facing_error
return {'success': False,
'error': 'Error displaying submission. Please notify course staff.'}
'error': 'Error displaying submission. Please notify course staff.'}
def save_calibration_essay(self, get):
@@ -426,11 +426,13 @@ class PeerGradingModule(XModule):
try:
response = self.peer_gs.save_calibration_essay(location, grader_id, calibration_essay_id,
submission_key, score, feedback, rubric_scores)
submission_key, score, feedback, rubric_scores)
return response
except GradingServiceError:
#This is a dev_facing_error
log.exception("Error saving calibration grade, location: {0}, submission_id: {1}, submission_key: {2}, grader_id: {3}".format(location, submission_id, submission_key, grader_id))
log.exception(
"Error saving calibration grade, location: {0}, submission_id: {1}, submission_key: {2}, grader_id: {3}".format(
location, submission_id, submission_key, grader_id))
#This is a student_facing_error
return self._err_response('There was an error saving your score. Please notify course staff.')
@@ -440,7 +442,7 @@ class PeerGradingModule(XModule):
'''
html = self.system.render_template('peer_grading/peer_grading_closed.html', {
'use_for_single_location': self.use_for_single_location
})
})
return html
@@ -503,12 +505,11 @@ class PeerGradingModule(XModule):
problem['closed'] = True
else:
problem['closed'] = False
else:
# if we can't find the due date, assume that it doesn't have one
else:
# if we can't find the due date, assume that it doesn't have one
problem['due'] = None
problem['closed'] = False
ajax_url = self.ajax_url
html = self.system.render_template('peer_grading/peer_grading.html', {
'course_id': self.system.course_id,
@@ -519,7 +520,7 @@ class PeerGradingModule(XModule):
# Checked above
'staff_access': False,
'use_single_location': self.use_for_single_location,
})
})
return html
@@ -531,7 +532,8 @@ class PeerGradingModule(XModule):
if not self.use_for_single_location:
#This is an error case, because it must be set to use a single location to be called without get parameters
#This is a dev_facing_error
log.error("Peer grading problem in peer_grading_module called with no get parameters, but use_for_single_location is False.")
log.error(
"Peer grading problem in peer_grading_module called with no get parameters, but use_for_single_location is False.")
return {'html': "", 'success': False}
problem_location = self.link_to_location
@@ -547,7 +549,7 @@ class PeerGradingModule(XModule):
# Checked above
'staff_access': False,
'use_single_location': self.use_for_single_location,
})
})
return {'html': html, 'success': True}
@@ -560,7 +562,7 @@ class PeerGradingModule(XModule):
state = {
'student_data_for_location': self.student_data_for_location,
}
}
return json.dumps(state)
@@ -596,7 +598,9 @@ class PeerGradingDescriptor(XmlDescriptor, EditingDescriptor):
for child in expected_children:
if len(xml_object.xpath(child)) == 0:
#This is a staff_facing_error
raise ValueError("Peer grading definition must include at least one '{0}' tag. Contact the learning sciences group for assistance.".format(child))
raise ValueError(
"Peer grading definition must include at least one '{0}' tag. Contact the learning sciences group for assistance.".format(
child))
def parse_task(k):
"""Assumes that xml_object has child k"""

View File

@@ -0,0 +1,20 @@
---
metadata:
display_name: 'Annotation'
data: |
<annotatable>
<instructions>
<p>Enter your (optional) instructions for the exercise in HTML format.</p>
<p>Annotations are specified by an <code>&lt;annotation&gt;</code> tag which may may have the following attributes:</p>
<ul class="instructions-template">
<li><code>title</code> (optional). Title of the annotation. Defaults to <i>Commentary</i> if omitted.</li>
<li><code>body</code> (<b>required</b>). Text of the annotation.</li>
<li><code>problem</code> (optional). Numeric index of the problem associated with this annotation. This is a zero-based index, so the first problem on the page would have <code>problem="0"</code>.</li>
<li><code>highlight</code> (optional). Possible values: yellow, red, orange, green, blue, or purple. Defaults to yellow if this attribute is omitted.</li>
</ul>
</instructions>
<p>Add your HTML with annotation spans here.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. <annotation title="My title" body="My comment" highlight="yellow" problem="0">Ut sodales laoreet est, egestas gravida felis egestas nec.</annotation> Aenean at volutpat erat. Cras commodo viverra nibh in aliquam.</p>
<p>Nulla facilisi. <annotation body="Basic annotation example." problem="1">Pellentesque id vestibulum libero.</annotation> Suspendisse potenti. Morbi scelerisque nisi vitae felis dictum mattis. Nam sit amet magna elit. Nullam volutpat cursus est, sit amet sagittis odio vulputate et. Curabitur euismod, orci in vulputate imperdiet, augue lorem tempor purus, id aliquet augue turpis a est. Aenean a sagittis libero. Praesent fringilla pretium magna, non condimentum risus elementum nec. Pellentesque faucibus elementum pharetra. Pellentesque vitae metus eros.</p>
</annotatable>
children: []

View File

@@ -28,21 +28,35 @@ open_ended_grading_interface = {
'grading_controller' : 'grading_controller'
}
test_system = ModuleSystem(
ajax_url='courses/course_id/modx/a_location',
track_function=Mock(),
get_module=Mock(),
# "render" to just the context...
render_template=lambda template, context: str(context),
replace_urls=Mock(),
user=Mock(is_staff=False),
filestore=Mock(),
debug=True,
xqueue={'interface': None, 'callback_url': '/', 'default_queuename': 'testqueue', 'waittime': 10},
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"),
anonymous_student_id='student',
open_ended_grading_interface= open_ended_grading_interface
)
def test_system():
"""
Construct a test ModuleSystem instance.
By default, the render_template() method simply returns
the context it is passed as a string.
You can override this behavior by monkey patching:
system = test_system()
system.render_template = my_render_func
where my_render_func is a function of the form
my_render_func(template, context)
"""
return ModuleSystem(
ajax_url='courses/course_id/modx/a_location',
track_function=Mock(),
get_module=Mock(),
render_template=lambda template, context: str(context),
replace_urls=lambda html: str(html),
user=Mock(is_staff=False),
filestore=Mock(),
debug=True,
xqueue={'interface': None, 'callback_url': '/', 'default_queuename': 'testqueue', 'waittime': 10},
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"),
anonymous_student_id='student',
open_ended_grading_interface= open_ended_grading_interface
)
class ModelsTest(unittest.TestCase):

View File

@@ -0,0 +1,129 @@
"""Module annotatable tests"""
import unittest
from lxml import etree
from mock import Mock
from xmodule.annotatable_module import AnnotatableModule
from xmodule.modulestore import Location
from . import test_system
class AnnotatableModuleTestCase(unittest.TestCase):
location = Location(["i4x", "edX", "toy", "annotatable", "guided_discussion"])
sample_xml = '''
<annotatable display_name="Iliad">
<instructions>Read the text.</instructions>
<p>
<annotation body="first">Sing</annotation>,
<annotation title="goddess" body="second">O goddess</annotation>,
<annotation title="anger" body="third" highlight="blue">the anger of Achilles son of Peleus</annotation>,
that brought <i>countless</i> ills upon the Achaeans. Many a brave soul did it send
hurrying down to Hades, and many a hero did it yield a prey to dogs and
<div style="font-weight:bold"><annotation body="fourth" problem="4">vultures</annotation>, for so were the counsels
of Jove fulfilled from the day on which the son of Atreus, king of men, and great
Achilles, first fell out with one another.</div>
</p>
<annotation title="footnote" body="the end">The Iliad of Homer by Samuel Butler</annotation>
</annotatable>
'''
definition = { 'data': sample_xml }
descriptor = Mock()
instance_state = None
shared_state = None
def setUp(self):
self.annotatable = AnnotatableModule(test_system(), self.location, self.definition, self.descriptor, self.instance_state, self.shared_state)
def test_annotation_data_attr(self):
el = etree.fromstring('<annotation title="bar" body="foo" problem="0">test</annotation>')
expected_attr = {
'data-comment-body': {'value': 'foo', '_delete': 'body' },
'data-comment-title': {'value': 'bar', '_delete': 'title'},
'data-problem-id': {'value': '0', '_delete': 'problem'}
}
actual_attr = self.annotatable._get_annotation_data_attr(0, el)
self.assertTrue(type(actual_attr) is dict)
self.assertDictEqual(expected_attr, actual_attr)
def test_annotation_class_attr_default(self):
xml = '<annotation title="x" body="y" problem="0">test</annotation>'
el = etree.fromstring(xml)
expected_attr = { 'class': { 'value': 'annotatable-span highlight' } }
actual_attr = self.annotatable._get_annotation_class_attr(0, el)
self.assertTrue(type(actual_attr) is dict)
self.assertDictEqual(expected_attr, actual_attr)
def test_annotation_class_attr_with_valid_highlight(self):
xml = '<annotation title="x" body="y" problem="0" highlight="{highlight}">test</annotation>'
for color in self.annotatable.highlight_colors:
el = etree.fromstring(xml.format(highlight=color))
value = 'annotatable-span highlight highlight-{highlight}'.format(highlight=color)
expected_attr = { 'class': {
'value': value,
'_delete': 'highlight' }
}
actual_attr = self.annotatable._get_annotation_class_attr(0, el)
self.assertTrue(type(actual_attr) is dict)
self.assertDictEqual(expected_attr, actual_attr)
def test_annotation_class_attr_with_invalid_highlight(self):
xml = '<annotation title="x" body="y" problem="0" highlight="{highlight}">test</annotation>'
for invalid_color in ['rainbow', 'blink', 'invisible', '', None]:
el = etree.fromstring(xml.format(highlight=invalid_color))
expected_attr = { 'class': {
'value': 'annotatable-span highlight',
'_delete': 'highlight' }
}
actual_attr = self.annotatable._get_annotation_class_attr(0, el)
self.assertTrue(type(actual_attr) is dict)
self.assertDictEqual(expected_attr, actual_attr)
def test_render_annotation(self):
expected_html = '<span class="annotatable-span highlight highlight-yellow" data-comment-title="x" data-comment-body="y" data-problem-id="0">z</span>'
expected_el = etree.fromstring(expected_html)
actual_el = etree.fromstring('<annotation title="x" body="y" problem="0" highlight="yellow">z</annotation>')
self.annotatable._render_annotation(0, actual_el)
self.assertEqual(expected_el.tag, actual_el.tag)
self.assertEqual(expected_el.text, actual_el.text)
self.assertDictEqual(dict(expected_el.attrib), dict(actual_el.attrib))
def test_render_content(self):
content = self.annotatable._render_content()
el = etree.fromstring(content)
self.assertEqual('div', el.tag, 'root tag is a div')
expected_num_annotations = 5
actual_num_annotations = el.xpath('count(//span[contains(@class,"annotatable-span")])')
self.assertEqual(expected_num_annotations, actual_num_annotations, 'check number of annotations')
def test_get_html(self):
context = self.annotatable.get_html()
for key in ['display_name', 'element_id', 'content_html', 'instructions_html']:
self.assertIn(key, context)
def test_extract_instructions(self):
xmltree = etree.fromstring(self.sample_xml)
expected_xml = u"<div>Read the text.</div>"
actual_xml = self.annotatable._extract_instructions(xmltree)
self.assertIsNotNone(actual_xml)
self.assertEqual(expected_xml.strip(), actual_xml.strip())
xmltree = etree.fromstring('<annotatable>foo</annotatable>')
actual = self.annotatable._extract_instructions(xmltree)
self.assertIsNone(actual)

View File

@@ -1,13 +1,18 @@
import datetime
import json
from mock import Mock
from mock import Mock, MagicMock, patch
from pprint import pprint
import unittest
import random
import xmodule
import capa
from xmodule.capa_module import CapaModule
from xmodule.modulestore import Location
from lxml import etree
from django.http import QueryDict
from . import test_system
@@ -33,6 +38,18 @@ class CapaFactory(object):
CapaFactory.num += 1
return CapaFactory.num
@staticmethod
def input_key():
""" Return the input key to use when passing GET parameters """
return ("input_" + CapaFactory.answer_key())
@staticmethod
def answer_key():
""" Return the key stored in the capa problem answer dict """
return ("-".join(['i4x', 'edX', 'capa_test', 'problem',
'SampleProblem%d' % CapaFactory.num]) +
"_2_1")
@staticmethod
def create(graceperiod=None,
due=None,
@@ -59,11 +76,10 @@ class CapaFactory(object):
module.
attempts: also added to instance state. Will be converted to an int.
correct: if True, the problem will be initialized to be answered correctly.
"""
definition = {'data': CapaFactory.sample_problem_xml, }
location = Location(["i4x", "edX", "capa_test", "problem",
"SampleProblem{0}".format(CapaFactory.next_num())])
"SampleProblem%d" % CapaFactory.next_num()])
metadata = {}
if graceperiod is not None:
metadata['graceperiod'] = graceperiod
@@ -89,19 +105,14 @@ class CapaFactory(object):
# since everything else is a string.
instance_state_dict['attempts'] = int(attempts)
if correct:
# TODO: make this actually set an answer of 3.14, and mark it correct
#instance_state_dict['student_answers'] = {}
#instance_state_dict['correct_map'] = {}
pass
if len(instance_state_dict) > 0:
instance_state = json.dumps(instance_state_dict)
else:
instance_state = None
module = CapaModule(test_system, location,
system = test_system()
system.render_template = Mock(return_value="<div>Test Template HTML</div>")
module = CapaModule(system, location,
definition, descriptor,
instance_state, None, metadata=metadata)
@@ -135,6 +146,8 @@ class CapaModuleTest(unittest.TestCase):
"Factory should be creating unique names for each problem")
def test_correct(self):
"""
Check that the factory creates correct and incorrect problems properly.
@@ -178,6 +191,7 @@ class CapaModuleTest(unittest.TestCase):
max_attempts="1",
attempts="0",
due=self.yesterday_str)
self.assertTrue(after_due_date.answer_available())
@@ -282,3 +296,602 @@ class CapaModuleTest(unittest.TestCase):
due=self.yesterday_str,
graceperiod=self.two_day_delta_str)
self.assertTrue(still_in_grace.answer_available())
def test_closed(self):
# Attempts < Max attempts --> NOT closed
module = CapaFactory.create(max_attempts="1", attempts="0")
self.assertFalse(module.closed())
# Attempts < Max attempts --> NOT closed
module = CapaFactory.create(max_attempts="2", attempts="1")
self.assertFalse(module.closed())
# Attempts = Max attempts --> closed
module = CapaFactory.create(max_attempts="1", attempts="1")
self.assertTrue(module.closed())
# Attempts > Max attempts --> closed
module = CapaFactory.create(max_attempts="1", attempts="2")
self.assertTrue(module.closed())
# Max attempts = 0 --> closed
module = CapaFactory.create(max_attempts="0", attempts="2")
self.assertTrue(module.closed())
# Past due --> closed
module = CapaFactory.create(max_attempts="1", attempts="0",
due=self.yesterday_str)
self.assertTrue(module.closed())
def test_parse_get_params(self):
# We have to set up Django settings in order to use QueryDict
from django.conf import settings
settings.configure()
# Valid GET param dict
valid_get_dict = self._querydict_from_dict({'input_1': 'test',
'input_1_2': 'test',
'input_1_2_3': 'test',
'input_[]_3': 'test',
'input_4': None,
'input_5': [],
'input_6': 5})
result = CapaModule.make_dict_of_responses(valid_get_dict)
# Expect that we get a dict with "input" stripped from key names
# and that we get the same values back
for key in result.keys():
original_key = "input_" + key
self.assertTrue(original_key in valid_get_dict,
"Output dict should have key %s" % original_key)
self.assertEqual(valid_get_dict[original_key], result[key])
# Valid GET param dict with list keys
valid_get_dict = self._querydict_from_dict({'input_2[]': ['test1', 'test2']})
result = CapaModule.make_dict_of_responses(valid_get_dict)
self.assertTrue('2' in result)
self.assertEqual(['test1','test2'], result['2'])
# If we use [] at the end of a key name, we should always
# get a list, even if there's just one value
valid_get_dict = self._querydict_from_dict({'input_1[]': 'test'})
result = CapaModule.make_dict_of_responses(valid_get_dict)
self.assertEqual(result['1'], ['test'])
# If we have no underscores in the name, then the key is invalid
invalid_get_dict = self._querydict_from_dict({'input': 'test'})
with self.assertRaises(ValueError):
result = CapaModule.make_dict_of_responses(invalid_get_dict)
# Two equivalent names (one list, one non-list)
# One of the values would overwrite the other, so detect this
# and raise an exception
invalid_get_dict = self._querydict_from_dict({'input_1[]': 'test 1',
'input_1': 'test 2' })
with self.assertRaises(ValueError):
result = CapaModule.make_dict_of_responses(invalid_get_dict)
def _querydict_from_dict(self, param_dict):
""" Create a Django QueryDict from a Python dictionary """
# QueryDict objects are immutable by default, so we make
# a copy that we can update.
querydict = QueryDict('')
copyDict = querydict.copy()
for (key, val) in param_dict.items():
# QueryDicts handle lists differently from ordinary values,
# so we have to specifically tell the QueryDict that
# this is a list
if type(val) is list:
copyDict.setlist(key, val)
else:
copyDict[key] = val
return copyDict
def test_check_problem_correct(self):
module = CapaFactory.create(attempts=1)
# Simulate that all answers are marked correct, no matter
# what the input is, by patching CorrectMap.is_correct()
# Also simulate rendering the HTML
with patch('capa.correctmap.CorrectMap.is_correct') as mock_is_correct,\
patch('xmodule.capa_module.CapaModule.get_problem_html') as mock_html:
mock_is_correct.return_value = True
mock_html.return_value = "Test HTML"
# Check the problem
get_request_dict = { CapaFactory.input_key(): '3.14' }
result = module.check_problem(get_request_dict)
# Expect that the problem is marked correct
self.assertEqual(result['success'], 'correct')
# Expect that we get the (mocked) HTML
self.assertEqual(result['contents'], 'Test HTML')
# Expect that the number of attempts is incremented by 1
self.assertEqual(module.attempts, 2)
def test_check_problem_incorrect(self):
module = CapaFactory.create(attempts=0)
# Simulate marking the input incorrect
with patch('capa.correctmap.CorrectMap.is_correct') as mock_is_correct:
mock_is_correct.return_value = False
# Check the problem
get_request_dict = { CapaFactory.input_key(): '0' }
result = module.check_problem(get_request_dict)
# Expect that the problem is marked correct
self.assertEqual(result['success'], 'incorrect')
# Expect that the number of attempts is incremented by 1
self.assertEqual(module.attempts, 1)
def test_check_problem_closed(self):
module = CapaFactory.create(attempts=3)
# Problem closed -- cannot submit
# Simulate that CapaModule.closed() always returns True
with patch('xmodule.capa_module.CapaModule.closed') as mock_closed:
mock_closed.return_value = True
with self.assertRaises(xmodule.exceptions.NotFoundError):
get_request_dict = { CapaFactory.input_key(): '3.14' }
module.check_problem(get_request_dict)
# Expect that number of attempts NOT incremented
self.assertEqual(module.attempts, 3)
def test_check_problem_resubmitted_with_randomize(self):
# Randomize turned on
module = CapaFactory.create(rerandomize='always', attempts=0)
# Simulate that the problem is completed
module.lcp.done = True
# Expect that we cannot submit
with self.assertRaises(xmodule.exceptions.NotFoundError):
get_request_dict = { CapaFactory.input_key(): '3.14' }
module.check_problem(get_request_dict)
# Expect that number of attempts NOT incremented
self.assertEqual(module.attempts, 0)
def test_check_problem_resubmitted_no_randomize(self):
# Randomize turned off
module = CapaFactory.create(rerandomize='never', attempts=0)
# Simulate that the problem is completed
module.lcp.done = True
# Expect that we can submit successfully
get_request_dict = { CapaFactory.input_key(): '3.14' }
result = module.check_problem(get_request_dict)
self.assertEqual(result['success'], 'correct')
# Expect that number of attempts IS incremented
self.assertEqual(module.attempts, 1)
def test_check_problem_queued(self):
module = CapaFactory.create(attempts=1)
# Simulate that the problem is queued
with patch('capa.capa_problem.LoncapaProblem.is_queued') \
as mock_is_queued,\
patch('capa.capa_problem.LoncapaProblem.get_recentmost_queuetime') \
as mock_get_queuetime:
mock_is_queued.return_value = True
mock_get_queuetime.return_value = datetime.datetime.now()
get_request_dict = { CapaFactory.input_key(): '3.14' }
result = module.check_problem(get_request_dict)
# Expect an AJAX alert message in 'success'
self.assertTrue('You must wait' in result['success'])
# Expect that the number of attempts is NOT incremented
self.assertEqual(module.attempts, 1)
def test_check_problem_student_input_error(self):
module = CapaFactory.create(attempts=1)
# Simulate a student input exception
with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade:
mock_grade.side_effect = capa.responsetypes.StudentInputError('test error')
get_request_dict = { CapaFactory.input_key(): '3.14' }
result = module.check_problem(get_request_dict)
# Expect an AJAX alert message in 'success'
self.assertTrue('test error' in result['success'])
# Expect that the number of attempts is NOT incremented
self.assertEqual(module.attempts, 1)
def test_reset_problem(self):
module = CapaFactory.create()
# Mock the module's capa problem
# to simulate that the problem is done
mock_problem = MagicMock(capa.capa_problem.LoncapaProblem)
mock_problem.done = True
module.lcp = mock_problem
# Stub out HTML rendering
with patch('xmodule.capa_module.CapaModule.get_problem_html') as mock_html:
mock_html.return_value = "<div>Test HTML</div>"
# Reset the problem
get_request_dict = {}
result = module.reset_problem(get_request_dict)
# Expect that the request was successful
self.assertTrue('success' in result and result['success'])
# Expect that the problem HTML is retrieved
self.assertTrue('html' in result)
self.assertEqual(result['html'], "<div>Test HTML</div>")
# Expect that the problem was reset
mock_problem.do_reset.assert_called_once_with()
def test_reset_problem_closed(self):
module = CapaFactory.create()
# Simulate that the problem is closed
with patch('xmodule.capa_module.CapaModule.closed') as mock_closed:
mock_closed.return_value = True
# Try to reset the problem
get_request_dict = {}
result = module.reset_problem(get_request_dict)
# Expect that the problem was NOT reset
self.assertTrue('success' in result and not result['success'])
def test_reset_problem_not_done(self):
module = CapaFactory.create()
# Simulate that the problem is NOT done
module.lcp.done = False
# Try to reset the problem
get_request_dict = {}
result = module.reset_problem(get_request_dict)
# Expect that the problem was NOT reset
self.assertTrue('success' in result and not result['success'])
def test_save_problem(self):
module = CapaFactory.create()
# Simulate that the problem is not done (not attempted or reset)
module.lcp.done = False
# Save the problem
get_request_dict = { CapaFactory.input_key(): '3.14' }
result = module.save_problem(get_request_dict)
# Expect that answers are saved to the problem
expected_answers = { CapaFactory.answer_key(): '3.14' }
self.assertEqual(module.lcp.student_answers, expected_answers)
# Expect that the result is success
self.assertTrue('success' in result and result['success'])
def test_save_problem_closed(self):
module = CapaFactory.create()
# Simulate that the problem is NOT done (not attempted or reset)
module.lcp.done = False
# Simulate that the problem is closed
with patch('xmodule.capa_module.CapaModule.closed') as mock_closed:
mock_closed.return_value = True
# Try to save the problem
get_request_dict = { CapaFactory.input_key(): '3.14' }
result = module.save_problem(get_request_dict)
# Expect that the result is failure
self.assertTrue('success' in result and not result['success'])
def test_save_problem_submitted_with_randomize(self):
module = CapaFactory.create(rerandomize='always')
# Simulate that the problem is completed
module.lcp.done = True
# Try to save
get_request_dict = { CapaFactory.input_key(): '3.14' }
result = module.save_problem(get_request_dict)
# Expect that we cannot save
self.assertTrue('success' in result and not result['success'])
def test_save_problem_submitted_no_randomize(self):
module = CapaFactory.create(rerandomize='never')
# Simulate that the problem is completed
module.lcp.done = True
# Try to save
get_request_dict = { CapaFactory.input_key(): '3.14' }
result = module.save_problem(get_request_dict)
# Expect that we succeed
self.assertTrue('success' in result and result['success'])
def test_check_button_name(self):
# If last attempt, button name changes to "Final Check"
# Just in case, we also check what happens if we have
# more attempts than allowed.
attempts = random.randint(1, 10)
module = CapaFactory.create(attempts=attempts-1, max_attempts=attempts)
self.assertEqual(module.check_button_name(), "Final Check")
module = CapaFactory.create(attempts=attempts, max_attempts=attempts)
self.assertEqual(module.check_button_name(), "Final Check")
module = CapaFactory.create(attempts=attempts + 1, max_attempts=attempts)
self.assertEqual(module.check_button_name(), "Final Check")
# Otherwise, button name is "Check"
module = CapaFactory.create(attempts=attempts-2, max_attempts=attempts)
self.assertEqual(module.check_button_name(), "Check")
module = CapaFactory.create(attempts=attempts-3, max_attempts=attempts)
self.assertEqual(module.check_button_name(), "Check")
# If no limit on attempts, then always show "Check"
module = CapaFactory.create(attempts=attempts-3)
self.assertEqual(module.check_button_name(), "Check")
module = CapaFactory.create(attempts=0)
self.assertEqual(module.check_button_name(), "Check")
def test_should_show_check_button(self):
attempts = random.randint(1,10)
# If we're after the deadline, do NOT show check button
module = CapaFactory.create(due=self.yesterday_str)
self.assertFalse(module.should_show_check_button())
# If user is out of attempts, do NOT show the check button
module = CapaFactory.create(attempts=attempts, max_attempts=attempts)
self.assertFalse(module.should_show_check_button())
# If survey question (max_attempts = 0), do NOT show the check button
module = CapaFactory.create(max_attempts=0)
self.assertFalse(module.should_show_check_button())
# If user submitted a problem but hasn't reset,
# do NOT show the check button
# Note: we can only reset when rerandomize="always"
module = CapaFactory.create(rerandomize="always")
module.lcp.done = True
self.assertFalse(module.should_show_check_button())
# Otherwise, DO show the check button
module = CapaFactory.create()
self.assertTrue(module.should_show_check_button())
# If the user has submitted the problem
# and we do NOT have a reset button, then we can show the check button
# Setting rerandomize to "never" ensures that the reset button
# is not shown
module = CapaFactory.create(rerandomize="never")
module.lcp.done = True
self.assertTrue(module.should_show_check_button())
def test_should_show_reset_button(self):
attempts = random.randint(1,10)
# If we're after the deadline, do NOT show the reset button
module = CapaFactory.create(due=self.yesterday_str)
module.lcp.done = True
self.assertFalse(module.should_show_reset_button())
# If the user is out of attempts, do NOT show the reset button
module = CapaFactory.create(attempts=attempts, max_attempts=attempts)
module.lcp.done = True
self.assertFalse(module.should_show_reset_button())
# If we're NOT randomizing, then do NOT show the reset button
module = CapaFactory.create(rerandomize="never")
module.lcp.done = True
self.assertFalse(module.should_show_reset_button())
# If the user hasn't submitted an answer yet,
# then do NOT show the reset button
module = CapaFactory.create()
module.lcp.done = False
self.assertFalse(module.should_show_reset_button())
# Otherwise, DO show the reset button
module = CapaFactory.create()
module.lcp.done = True
self.assertTrue(module.should_show_reset_button())
# If survey question for capa (max_attempts = 0),
# DO show the reset button
module = CapaFactory.create(max_attempts=0)
module.lcp.done = True
self.assertTrue(module.should_show_reset_button())
def test_should_show_save_button(self):
attempts = random.randint(1,10)
# If we're after the deadline, do NOT show the save button
module = CapaFactory.create(due=self.yesterday_str)
module.lcp.done = True
self.assertFalse(module.should_show_save_button())
# If the user is out of attempts, do NOT show the save button
module = CapaFactory.create(attempts=attempts, max_attempts=attempts)
module.lcp.done = True
self.assertFalse(module.should_show_save_button())
# If user submitted a problem but hasn't reset, do NOT show the save button
module = CapaFactory.create(rerandomize="always")
module.lcp.done = True
self.assertFalse(module.should_show_save_button())
# Otherwise, DO show the save button
module = CapaFactory.create()
module.lcp.done = False
self.assertTrue(module.should_show_save_button())
# If we're not randomizing, then we can re-save
module = CapaFactory.create(rerandomize="never")
module.lcp.done = True
self.assertTrue(module.should_show_save_button())
# If survey question for capa (max_attempts = 0),
# DO show the save button
module = CapaFactory.create(max_attempts=0)
module.lcp.done = False
self.assertTrue(module.should_show_save_button())
def test_should_show_save_button_force_save_button(self):
# If we're after the deadline, do NOT show the save button
# even though we're forcing a save
module = CapaFactory.create(due=self.yesterday_str,
force_save_button="true")
module.lcp.done = True
self.assertFalse(module.should_show_save_button())
# If the user is out of attempts, do NOT show the save button
attempts = random.randint(1,10)
module = CapaFactory.create(attempts=attempts,
max_attempts=attempts,
force_save_button="true")
module.lcp.done = True
self.assertFalse(module.should_show_save_button())
# Otherwise, if we force the save button,
# then show it even if we would ordinarily
# require a reset first
module = CapaFactory.create(force_save_button="true",
rerandomize="always")
module.lcp.done = True
self.assertTrue(module.should_show_save_button())
def test_no_max_attempts(self):
module = CapaFactory.create(max_attempts='')
html = module.get_problem_html()
# assert that we got here without exploding
def test_get_problem_html(self):
module = CapaFactory.create()
# We've tested the show/hide button logic in other tests,
# so here we hard-wire the values
show_check_button = bool(random.randint(0,1) % 2)
show_reset_button = bool(random.randint(0,1) % 2)
show_save_button = bool(random.randint(0,1) % 2)
module.should_show_check_button = Mock(return_value=show_check_button)
module.should_show_reset_button = Mock(return_value=show_reset_button)
module.should_show_save_button = Mock(return_value=show_save_button)
# Mock the system rendering function
module.system.render_template = Mock(return_value="<div>Test Template HTML</div>")
# Patch the capa problem's HTML rendering
with patch('capa.capa_problem.LoncapaProblem.get_html') as mock_html:
mock_html.return_value = "<div>Test Problem HTML</div>"
# Render the problem HTML
html = module.get_problem_html(encapsulate=False)
# Also render the problem encapsulated in a <div>
html_encapsulated = module.get_problem_html(encapsulate=True)
# Expect that we get the rendered template back
self.assertEqual(html, "<div>Test Template HTML</div>")
# Check the rendering context
render_args,_ = module.system.render_template.call_args
self.assertEqual(len(render_args), 2)
template_name = render_args[0]
self.assertEqual(template_name, "problem.html")
context = render_args[1]
self.assertEqual(context['problem']['html'], "<div>Test Problem HTML</div>")
self.assertEqual(bool(context['check_button']), show_check_button)
self.assertEqual(bool(context['reset_button']), show_reset_button)
self.assertEqual(bool(context['save_button']), show_save_button)
# Assert that the encapsulated html contains the original html
self.assertTrue(html in html_encapsulated)
def test_get_problem_html_error(self):
"""
In production, when an error occurs with the problem HTML
rendering, a "dummy" problem is created with an error
message to display to the user.
"""
module = CapaFactory.create()
# Save the original problem so we can compare it later
original_problem = module.lcp
# Simulate throwing an exception when the capa problem
# is asked to render itself as HTML
module.lcp.get_html = Mock(side_effect=Exception("Test"))
# Stub out the test_system rendering function
module.system.render_template = Mock(return_value="<div>Test Template HTML</div>")
# Turn off DEBUG
module.system.DEBUG = False
# Try to render the module with DEBUG turned off
html = module.get_problem_html()
# Check the rendering context
render_args,_ = module.system.render_template.call_args
context = render_args[1]
self.assertTrue("error" in context['problem']['html'])
# Expect that the module has created a new dummy problem with the error
self.assertNotEqual(original_problem, module.lcp)

View File

@@ -14,6 +14,7 @@ from datetime import datetime
from . import test_system
import test_util_open_ended
"""
Tests for the various pieces of the CombinedOpenEndedGrading system
@@ -39,40 +40,37 @@ class OpenEndedChildTest(unittest.TestCase):
max_score = 1
static_data = {
'max_attempts': 20,
'prompt': prompt,
'rubric': rubric,
'max_score': max_score,
'display_name': 'Name',
'accept_file_upload': False,
'close_date': None,
's3_interface' : "",
'open_ended_grading_interface' : {},
'skip_basic_checks' : False,
}
'max_attempts': 20,
'prompt': prompt,
'rubric': rubric,
'max_score': max_score,
'display_name': 'Name',
'accept_file_upload': False,
'close_date': None,
's3_interface': "",
'open_ended_grading_interface': {},
'skip_basic_checks': False,
}
definition = Mock()
descriptor = Mock()
def setUp(self):
self.openendedchild = OpenEndedChild(test_system, self.location,
self.definition, self.descriptor, self.static_data, self.metadata)
self.test_system = test_system()
self.openendedchild = OpenEndedChild(self.test_system, self.location,
self.definition, self.descriptor, self.static_data, self.metadata)
def test_latest_answer_empty(self):
answer = self.openendedchild.latest_answer()
self.assertEqual(answer, "")
def test_latest_score_empty(self):
answer = self.openendedchild.latest_score()
self.assertEqual(answer, None)
def test_latest_post_assessment_empty(self):
answer = self.openendedchild.latest_post_assessment(test_system)
answer = self.openendedchild.latest_post_assessment(self.test_system)
self.assertEqual(answer, "")
def test_new_history_entry(self):
new_answer = "New Answer"
self.openendedchild.new_history_entry(new_answer)
@@ -98,7 +96,6 @@ class OpenEndedChildTest(unittest.TestCase):
score = self.openendedchild.latest_score()
self.assertEqual(score, 4)
def test_record_latest_post_assessment(self):
new_answer = "New Answer"
self.openendedchild.new_history_entry(new_answer)
@@ -106,7 +103,7 @@ class OpenEndedChildTest(unittest.TestCase):
post_assessment = "Post assessment"
self.openendedchild.record_latest_post_assessment(post_assessment)
self.assertEqual(post_assessment,
self.openendedchild.latest_post_assessment(test_system))
self.openendedchild.latest_post_assessment(self.test_system))
def test_get_score(self):
new_answer = "New Answer"
@@ -123,24 +120,22 @@ class OpenEndedChildTest(unittest.TestCase):
self.assertEqual(score['score'], new_score)
self.assertEqual(score['total'], self.static_data['max_score'])
def test_reset(self):
self.openendedchild.reset(test_system)
self.openendedchild.reset(self.test_system)
state = json.loads(self.openendedchild.get_instance_state())
self.assertEqual(state['state'], OpenEndedChild.INITIAL)
def test_is_last_response_correct(self):
new_answer = "New Answer"
self.openendedchild.new_history_entry(new_answer)
self.openendedchild.record_latest_score(self.static_data['max_score'])
self.assertEqual(self.openendedchild.is_last_response_correct(),
'correct')
'correct')
self.openendedchild.new_history_entry(new_answer)
self.openendedchild.record_latest_score(0)
self.assertEqual(self.openendedchild.is_last_response_correct(),
'incorrect')
'incorrect')
class OpenEndedModuleTest(unittest.TestCase):
@@ -158,18 +153,18 @@ class OpenEndedModuleTest(unittest.TestCase):
max_score = 4
static_data = {
'max_attempts': 20,
'prompt': prompt,
'rubric': rubric,
'max_score': max_score,
'display_name': 'Name',
'accept_file_upload': False,
'rewrite_content_links' : "",
'close_date': None,
's3_interface' : test_util_open_ended.S3_INTERFACE,
'open_ended_grading_interface' : test_util_open_ended.OPEN_ENDED_GRADING_INTERFACE,
'skip_basic_checks' : False,
}
'max_attempts': 20,
'prompt': prompt,
'rubric': rubric,
'max_score': max_score,
'display_name': 'Name',
'accept_file_upload': False,
'rewrite_content_links': "",
'close_date': None,
's3_interface': test_util_open_ended.S3_INTERFACE,
'open_ended_grading_interface': test_util_open_ended.OPEN_ENDED_GRADING_INTERFACE,
'skip_basic_checks': False,
}
oeparam = etree.XML('''
<openendedparam>
@@ -182,30 +177,33 @@ class OpenEndedModuleTest(unittest.TestCase):
descriptor = Mock()
def setUp(self):
test_system.location = self.location
self.test_system = test_system()
self.test_system.location = self.location
self.mock_xqueue = MagicMock()
self.mock_xqueue.send_to_queue.return_value = (None, "Message")
test_system.xqueue = {'interface': self.mock_xqueue, 'callback_url': '/', 'default_queuename': 'testqueue', 'waittime': 1}
self.openendedmodule = OpenEndedModule(test_system, self.location,
self.definition, self.descriptor, self.static_data, self.metadata)
self.test_system.xqueue = {'interface': self.mock_xqueue, 'callback_url': '/', 'default_queuename': 'testqueue',
'waittime': 1}
self.openendedmodule = OpenEndedModule(self.test_system, self.location,
self.definition, self.descriptor, self.static_data, self.metadata)
def test_message_post(self):
get = {'feedback': 'feedback text',
'submission_id': '1',
'grader_id': '1',
'score': 3}
'submission_id': '1',
'grader_id': '1',
'score': 3}
qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat)
student_info = {'anonymous_student_id': test_system.anonymous_student_id,
'submission_time': qtime}
student_info = {'anonymous_student_id': self.test_system.anonymous_student_id,
'submission_time': qtime}
contents = {
'feedback': get['feedback'],
'submission_id': int(get['submission_id']),
'grader_id': int(get['grader_id']),
'score': get['score'],
'student_info': json.dumps(student_info)
}
'feedback': get['feedback'],
'submission_id': int(get['submission_id']),
'grader_id': int(get['grader_id']),
'score': get['score'],
'student_info': json.dumps(student_info)
}
result = self.openendedmodule.message_post(get, test_system)
result = self.openendedmodule.message_post(get, self.test_system)
self.assertTrue(result['success'])
# make sure it's actually sending something we want to the queue
self.mock_xqueue.send_to_queue.assert_called_with(body=json.dumps(contents), header=ANY)
@@ -216,56 +214,56 @@ class OpenEndedModuleTest(unittest.TestCase):
def test_send_to_grader(self):
submission = "This is a student submission"
qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat)
student_info = {'anonymous_student_id': test_system.anonymous_student_id,
'submission_time': qtime}
student_info = {'anonymous_student_id': self.test_system.anonymous_student_id,
'submission_time': qtime}
contents = self.openendedmodule.payload.copy()
contents.update({
'student_info': json.dumps(student_info),
'student_response': submission,
'max_score': self.max_score
})
result = self.openendedmodule.send_to_grader(submission, test_system)
})
result = self.openendedmodule.send_to_grader(submission, self.test_system)
self.assertTrue(result)
self.mock_xqueue.send_to_queue.assert_called_with(body=json.dumps(contents), header=ANY)
def update_score_single(self):
self.openendedmodule.new_history_entry("New Entry")
score_msg = {
'correct': True,
'score': 4,
'msg': 'Grader Message',
'feedback': "Grader Feedback"
}
'correct': True,
'score': 4,
'msg': 'Grader Message',
'feedback': "Grader Feedback"
}
get = {'queuekey': "abcd",
'xqueue_body': score_msg}
self.openendedmodule.update_score(get, test_system)
'xqueue_body': score_msg}
self.openendedmodule.update_score(get, self.test_system)
def update_score_single(self):
self.openendedmodule.new_history_entry("New Entry")
feedback = {
"success": True,
"feedback": "Grader Feedback"
}
"success": True,
"feedback": "Grader Feedback"
}
score_msg = {
'correct': True,
'score': 4,
'msg': 'Grader Message',
'feedback': json.dumps(feedback),
'grader_type': 'IN',
'grader_id': '1',
'submission_id': '1',
'success': True,
'rubric_scores': [0],
'rubric_scores_complete': True,
'rubric_xml': etree.tostring(self.rubric)
}
'correct': True,
'score': 4,
'msg': 'Grader Message',
'feedback': json.dumps(feedback),
'grader_type': 'IN',
'grader_id': '1',
'submission_id': '1',
'success': True,
'rubric_scores': [0],
'rubric_scores_complete': True,
'rubric_xml': etree.tostring(self.rubric)
}
get = {'queuekey': "abcd",
'xqueue_body': json.dumps(score_msg)}
self.openendedmodule.update_score(get, test_system)
'xqueue_body': json.dumps(score_msg)}
self.openendedmodule.update_score(get, self.test_system)
def test_latest_post_assessment(self):
self.update_score_single()
assessment = self.openendedmodule.latest_post_assessment(test_system)
assessment = self.openendedmodule.latest_post_assessment(self.test_system)
self.assertFalse(assessment == '')
# check for errors
self.assertFalse('errors' in assessment)
@@ -293,18 +291,18 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
metadata = {'attempts': '10', 'max_score': max_score}
static_data = {
'max_attempts': 20,
'prompt': prompt,
'rubric': rubric,
'max_score': max_score,
'display_name': 'Name',
'accept_file_upload' : False,
'rewrite_content_links' : "",
'close_date' : "",
's3_interface' : test_util_open_ended.S3_INTERFACE,
'open_ended_grading_interface' : test_util_open_ended.OPEN_ENDED_GRADING_INTERFACE,
'skip_basic_checks' : False,
}
'max_attempts': 20,
'prompt': prompt,
'rubric': rubric,
'max_score': max_score,
'display_name': 'Name',
'accept_file_upload': False,
'rewrite_content_links': "",
'close_date': "",
's3_interface': test_util_open_ended.S3_INTERFACE,
'open_ended_grading_interface': test_util_open_ended.OPEN_ENDED_GRADING_INTERFACE,
'skip_basic_checks': False,
}
oeparam = etree.XML('''
<openendedparam>
@@ -326,17 +324,23 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
'''
task_xml2 = '''
<openended min_score_to_attempt="1" max_score_to_attempt="1">
<openendedparam>
<initial_display>Enter essay here.</initial_display>
<answer_display>This is the answer.</answer_display>
<grader_payload>{"grader_settings" : "ml_grading.conf", "problem_id" : "6.002x/Welcome/OETest"}</grader_payload>
</openendedparam>
</openended>'''
<openendedparam>
<initial_display>Enter essay here.</initial_display>
<answer_display>This is the answer.</answer_display>
<grader_payload>{"grader_settings" : "ml_grading.conf", "problem_id" : "6.002x/Welcome/OETest"}</grader_payload>
</openendedparam>
</openended>'''
definition = {'prompt': etree.XML(prompt), 'rubric': etree.XML(rubric), 'task_xml': [task_xml1, task_xml2]}
descriptor = Mock()
def setUp(self):
self.combinedoe = CombinedOpenEndedV1Module(test_system, self.location, self.definition, self.descriptor, static_data = self.static_data, metadata=self.metadata)
self.test_system = test_system()
self.combinedoe = CombinedOpenEndedV1Module(self.test_system,
self.location,
self.definition,
self.descriptor,
static_data=self.static_data,
metadata=self.metadata)
def test_get_tag_name(self):
name = self.combinedoe.get_tag_name("<t>Tag</t>")

View File

@@ -56,6 +56,9 @@ class ConditionalModuleTest(unittest.TestCase):
'''Get a dummy system'''
return DummySystem(load_error_modules)
def setUp(self):
self.test_system = test_system()
def get_course(self, name):
"""Get a test course by directory name. If there's more than one, error."""
print "Importing {0}".format(name)
@@ -85,14 +88,14 @@ class ConditionalModuleTest(unittest.TestCase):
location = descriptor.location
instance_state = instance_states.get(location.category, None)
print "inner_get_module, location=%s, inst_state=%s" % (location, instance_state)
return descriptor.xmodule_constructor(test_system)(instance_state, shared_state)
return descriptor.xmodule_constructor(self.test_system)(instance_state, shared_state)
location = Location(["i4x", "edX", "cond_test", "conditional", "condone"])
def replace_urls(text, staticfiles_prefix=None, replace_prefix='/static/', course_namespace=None):
return text
test_system.replace_urls = replace_urls
test_system.get_module = inner_get_module
self.test_system.replace_urls = replace_urls
self.test_system.get_module = inner_get_module
module = inner_get_module(location)
print "module: ", module

View File

@@ -10,8 +10,8 @@ from . import test_system
import test_util_open_ended
class SelfAssessmentTest(unittest.TestCase):
class SelfAssessmentTest(unittest.TestCase):
rubric = '''<rubric><rubric>
<category>
<description>Response Quality</description>
@@ -24,7 +24,7 @@ class SelfAssessmentTest(unittest.TestCase):
'prompt': prompt,
'submitmessage': 'Shall we submit now?',
'hintprompt': 'Consider this...',
}
}
location = Location(["i4x", "edX", "sa_test", "selfassessment",
"SampleQuestion"])
@@ -41,37 +41,38 @@ class SelfAssessmentTest(unittest.TestCase):
'attempts': 2})
static_data = {
'max_attempts': 10,
'rubric': etree.XML(self.rubric),
'prompt': self.prompt,
'max_score': 1,
'display_name': "Name",
'accept_file_upload': False,
'close_date': None,
's3_interface' : test_util_open_ended.S3_INTERFACE,
'open_ended_grading_interface' : test_util_open_ended.OPEN_ENDED_GRADING_INTERFACE,
'skip_basic_checks' : False,
}
'max_attempts': 10,
'rubric': etree.XML(self.rubric),
'prompt': self.prompt,
'max_score': 1,
'display_name': "Name",
'accept_file_upload': False,
'close_date': None,
's3_interface': test_util_open_ended.S3_INTERFACE,
'open_ended_grading_interface': test_util_open_ended.OPEN_ENDED_GRADING_INTERFACE,
'skip_basic_checks': False,
}
self.module = SelfAssessmentModule(test_system, self.location,
self.definition, self.descriptor,
static_data,
state, metadata=self.metadata)
self.module = SelfAssessmentModule(test_system(), self.location,
self.definition, self.descriptor,
static_data,
state, metadata=self.metadata)
def test_get_html(self):
html = self.module.get_html(test_system)
html = self.module.get_html(self.module.system)
self.assertTrue("This is sample prompt text" in html)
def test_self_assessment_flow(self):
responses = {'assessment': '0', 'score_list[]': ['0', '0']}
def get_fake_item(name):
return responses[name]
def get_data_for_location(self,location,student):
def get_data_for_location(self, location, student):
return {
'count_graded' : 0,
'count_required' : 0,
'student_sub_count': 0,
'count_graded': 0,
'count_required': 0,
'student_sub_count': 0,
}
mock_query_dict = MagicMock()
@@ -82,19 +83,20 @@ class SelfAssessmentTest(unittest.TestCase):
self.assertEqual(self.module.get_score()['score'], 0)
self.module.save_answer({'student_answer': "I am an answer"}, test_system)
self.module.save_answer({'student_answer': "I am an answer"},
self.module.system)
self.assertEqual(self.module.state, self.module.ASSESSING)
self.module.save_assessment(mock_query_dict, test_system)
self.module.save_assessment(mock_query_dict, self.module.system)
self.assertEqual(self.module.state, self.module.DONE)
d = self.module.reset({})
self.assertTrue(d['success'])
self.assertEqual(self.module.state, self.module.INITIAL)
# if we now assess as right, skip the REQUEST_HINT state
self.module.save_answer({'student_answer': 'answer 4'}, test_system)
self.module.save_answer({'student_answer': 'answer 4'},
self.module.system)
responses['assessment'] = '1'
self.module.save_assessment(mock_query_dict, test_system)
self.module.save_assessment(mock_query_dict, self.module.system)
self.assertEqual(self.module.state, self.module.DONE)

View File

@@ -1,14 +1,14 @@
OPEN_ENDED_GRADING_INTERFACE = {
'url' : 'http://127.0.0.1:3033/',
'username' : 'incorrect',
'password' : 'incorrect',
'staff_grading' : 'staff_grading',
'peer_grading' : 'peer_grading',
'grading_controller' : 'grading_controller'
'url': 'http://127.0.0.1:3033/',
'username': 'incorrect',
'password': 'incorrect',
'staff_grading': 'staff_grading',
'peer_grading': 'peer_grading',
'grading_controller': 'grading_controller'
}
S3_INTERFACE = {
'aws_access_key' : "",
'aws_secret_key' : "",
"aws_bucket_name" : "",
'aws_access_key': "",
'aws_secret_key': "",
"aws_bucket_name": "",
}

View File

@@ -379,7 +379,11 @@ class XmlDescriptor(XModuleDescriptor):
if attr not in self.metadata_to_strip and attr not in self.metadata_to_export_to_policy:
val = val_for_xml(attr)
#logging.debug('location.category = {0}, attr = {1}'.format(self.location.category, attr))
xml_object.set(attr, val)
try:
xml_object.set(attr, val)
except Exception, e:
logging.exception('Failed to serialize metadata attribute {0} with value {1}. This could mean data loss!!! Exception: {2}'.format(attr, val, e))
pass
if self.export_to_file():
# Write the definition to a file

View File

@@ -79,6 +79,17 @@ if Backbone?
@getContent(id).updateInfo(info)
$.extend @contentInfos, infos
pinThread: ->
pinned = @get("pinned")
@set("pinned",pinned)
@trigger "change", @
unPinThread: ->
pinned = @get("pinned")
@set("pinned",pinned)
@trigger "change", @
class @Thread extends @Content
urlMappers:
'retrieve' : -> DiscussionUtil.urlFor('retrieve_single_thread', @discussion.id, @id)
@@ -91,6 +102,8 @@ if Backbone?
'delete' : -> DiscussionUtil.urlFor('delete_thread', @id)
'follow' : -> DiscussionUtil.urlFor('follow_thread', @id)
'unfollow' : -> DiscussionUtil.urlFor('unfollow_thread', @id)
'pinThread' : -> DiscussionUtil.urlFor("pin_thread", @id)
'unPinThread' : -> DiscussionUtil.urlFor("un_pin_thread", @id)
initialize: ->
@set('thread', @)

View File

@@ -58,10 +58,31 @@ if Backbone?
@current_page = response.page
sortByDate: (thread) ->
thread.get("created_at")
#
#The comment client asks each thread for a value by which to sort the collection
#and calls this sort routine regardless of the order returned from the LMS/comments service
#so, this takes advantage of this per-thread value and returns tomorrow's date
#for pinned threads, ensuring that they appear first, (which is the intent of pinned threads)
#
if thread.get('pinned')
#use tomorrow's date
today = new Date();
new Date(today.getTime() + (24 * 60 * 60 * 1000));
else
thread.get("created_at")
sortByDateRecentFirst: (thread) ->
-(new Date(thread.get("created_at")).getTime())
#
#Same as above
#but negative to flip the order (newest first)
#
if thread.get('pinned')
#use tomorrow's date
today = new Date();
-(new Date(today.getTime() + (24 * 60 * 60 * 1000)));
else
-(new Date(thread.get("created_at")).getTime())
#return String.fromCharCode.apply(String,
# _.map(thread.get("created_at").split(""),
# ((c) -> return 0xffff - c.charChodeAt()))

View File

@@ -88,7 +88,7 @@ if Backbone?
if @$('section.discussion').length
@$('section.discussion').replaceWith($discussion)
else
$(".discussion-module").append($discussion)
@$el.append($discussion)
@newPostForm = $('.new-post-article')
@threadviews = @discussion.map (thread) ->
new DiscussionThreadInlineView el: @$("article#thread_#{thread.id}"), model: thread

View File

@@ -50,6 +50,8 @@ class @DiscussionUtil
delete_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/delete"
upvote_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/upvote"
downvote_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/downvote"
pin_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/pin"
un_pin_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/unpin"
undo_vote_for_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/unvote"
follow_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/follow"
unfollow_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/unfollow"

View File

@@ -3,6 +3,7 @@ if Backbone?
events:
"click .discussion-vote": "toggleVote"
"click .admin-pin": "togglePin"
"click .action-follow": "toggleFollowing"
"click .action-edit": "edit"
"click .action-delete": "delete"
@@ -24,6 +25,7 @@ if Backbone?
@delegateEvents()
@renderDogear()
@renderVoted()
@renderPinned()
@renderAttrs()
@$("span.timeago").timeago()
@convertMath()
@@ -41,8 +43,20 @@ if Backbone?
else
@$("[data-role=discussion-vote]").removeClass("is-cast")
renderPinned: =>
if @model.get("pinned")
@$("[data-role=thread-pin]").addClass("pinned")
@$("[data-role=thread-pin]").removeClass("notpinned")
@$(".discussion-pin .pin-label").html("Pinned")
else
@$("[data-role=thread-pin]").removeClass("pinned")
@$("[data-role=thread-pin]").addClass("notpinned")
@$(".discussion-pin .pin-label").html("Pin Thread")
updateModelDetails: =>
@renderVoted()
@renderPinned()
@$("[data-role=discussion-vote] .votes-count-number").html(@model.get("votes")["up_count"])
convertMath: ->
@@ -99,6 +113,34 @@ if Backbone?
delete: (event) ->
@trigger "thread:delete", event
togglePin: (event) ->
event.preventDefault()
if @model.get('pinned')
@unPin()
else
@pin()
pin: ->
url = @model.urlFor("pinThread")
DiscussionUtil.safeAjax
$elem: @$(".discussion-pin")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
@model.set('pinned', true)
unPin: ->
url = @model.urlFor("unPinThread")
DiscussionUtil.safeAjax
$elem: @$(".discussion-pin")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
@model.set('pinned', false)
toggleClosed: (event) ->
$elem = $(event.target)
url = @model.urlFor('close')
@@ -137,3 +179,5 @@ if Backbone?
if @model.get('username')?
params = $.extend(params, user:{username: @model.username, user_url: @model.user_url})
Mustache.render(@template, params)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,97 @@
(function () {
var debug = false;
var module = {
debug: debug,
inputSelector: '.annotation-input',
tagSelector: '.tag',
tagsSelector: '.tags',
commentSelector: 'textarea.comment',
valueSelector: 'input.value', // stash tag selections and comment here as a JSON string...
singleSelect: true,
init: function() {
var that = this;
if(this.debug) { console.log('annotation input loaded: '); }
$(this.inputSelector).each(function(index, el) {
if(!$(el).data('listening')) {
$(el).delegate(that.tagSelector, 'click', $.proxy(that.onClickTag, that));
$(el).delegate(that.commentSelector, 'change', $.proxy(that.onChangeComment, that));
$(el).data('listening', 'yes');
}
});
},
onChangeComment: function(e) {
var value_el = this.findValueEl(e.target);
var current_value = this.loadValue(value_el);
var target_value = $(e.target).val();
current_value.comment = target_value;
this.storeValue(value_el, current_value);
},
onClickTag: function(e) {
var target_el = e.target, target_value, target_index;
var value_el, current_value;
value_el = this.findValueEl(e.target);
current_value = this.loadValue(value_el);
target_value = $(e.target).data('id');
if(!$(target_el).hasClass('selected')) {
if(this.singleSelect) {
current_value.options = [target_value]
} else {
current_value.options.push(target_value);
}
} else {
if(this.singleSelect) {
current_value.options = []
} else {
target_index = current_value.options.indexOf(target_value);
if(target_index !== -1) {
current_value.options.splice(target_index, 1);
}
}
}
this.storeValue(value_el, current_value);
if(this.singleSelect) {
$(target_el).closest(this.tagsSelector)
.find(this.tagSelector)
.not(target_el)
.removeClass('selected')
}
$(target_el).toggleClass('selected');
},
findValueEl: function(target_el) {
var input_el = $(target_el).closest(this.inputSelector);
return $(this.valueSelector, input_el);
},
loadValue: function(value_el) {
var json = $(value_el).val();
var result = JSON.parse(json);
if(result === null) {
result = {};
}
if(!result.hasOwnProperty('options')) {
result.options = [];
}
if(!result.hasOwnProperty('comment')) {
result.comment = '';
}
return result;
},
storeValue: function(value_el, new_value) {
var json = JSON.stringify(new_value);
$(value_el).val(json);
}
}
module.init();
}).call(this);

View File

@@ -11,9 +11,14 @@
}
prev_id = "#" + this.id + "_preview";
preview_div = $(prev_id)
preview_div = $(prev_id);
$.get("/preview/chemcalc/", {"formula" : this.value}, create_handler(preview_div));
// find the closest parent problems-wrapper and use that url
url = $(this).closest('.problems-wrapper').data('url');
// grab the input id from the input
input_id = $(this).data('input-id')
Problem.inputAjax(url, input_id, 'preview_chemcalc', {"formula" : this.value}, create_handler(preview_div));
}
inputs = $('.chemicalequationinput input');

View File

@@ -1,27 +1,44 @@
(function () {
var timeout = 1000;
function initializeApplet(applet) {
console.log("Initializing " + applet);
waitForApplet(applet);
}
waitForGenex();
function waitForApplet(applet) {
if (applet.isActive && applet.isActive()) {
console.log("Applet is ready.");
var answerStr = applet.checkAnswer();
console.log(answerStr);
var input = $('.editageneinput input');
console.log(input);
input.val(answerStr);
} else if (timeout > 30 * 1000) {
console.error("Applet did not load on time.");
} else {
console.log("Waiting for applet...");
setTimeout(function() { waitForApplet(applet); }, timeout);
function waitForGenex() {
if (typeof(genex) !== "undefined" && genex) {
genex.onInjectionDone("genex");
}
else {
setTimeout(function() { waitForGenex(); }, timeout);
}
}
var applets = $('.editageneinput object');
applets.each(function(i, el) { initializeApplet(el); });
//NOTE:
// Genex uses six global functions:
// genexSetDNASequence (exported from GWT)
// genexSetClickEvent (exported from GWT)
// genexSetKeyEvent (exported from GWT)
// genexSetProblemNumber (exported from GWT)
//
// It calls genexIsReady with a deferred command when it has finished
// initialization and has drawn itself
// genexStoreAnswer(answer) is called when the GWT [Store Answer] button
// is clicked
genexIsReady = function() {
//Load DNA sequence
var dna_sequence = $('#dna_sequence').val();
genexSetDNASequence(dna_sequence);
//Now load mouse and keyboard handlers
genexSetClickEvent();
genexSetKeyEvent();
//Now load problem
var genex_problem_number = $('#genex_problem_number').val();
genexSetProblemNumber(genex_problem_number);
};
genexStoreAnswer = function(ans) {
var problem = $('#genex_container').parents('.problem');
var input_field = problem.find('input[type="hidden"][name!="dna_sequence"][name!="genex_problem_number"]');
input_field.val(ans);
};
}).call(this);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

View File

@@ -0,0 +1,122 @@
.genex-button {
margin-right: -8px;
height: 40px !important;
}
.genex-label {
/*font: normal normal normal 10pt/normal 'Open Sans', Verdana, Geneva, sans-serif !important;*/
/*padding: 4px 0px 0px 10px !important;*/
font-family: sans-serif !important;
font-size: 13px !important;
font-style: normal !important;
font-variant: normal !important;
font-weight: bold !important;
padding-top: 6px !important;
margin-left: 18px;
}
.gwt-HTML {
cursor: default;
overflow-x: auto !important;
overflow-y: auto !important;
background-color: rgb(248, 248, 248) !important;
}
.genex-scrollpanel {
word-wrap: normal !important;
white-space: pre !important;
}
pre, #dna-strand {
font-family: 'courier new', courier !important;
font-size: 13px !important;
font-style: normal !important;
font-variant: normal !important;
font-weight: normal !important;
border-style: none !important;
background-color: rgb(248, 248, 248) !important;
word-wrap: normal !important;
white-space: pre !important;
overflow-x: visible !important;
overflow-y: visible !important;
}
.gwt-DialogBox .Caption {
background: #F1F1F1;
padding: 4px 8px 4px 4px;
cursor: default;
font-family: Arial Unicode MS, Arial, sans-serif;
font-weight: bold;
border-bottom: 1px solid #bbbbbb;
border-top: 1px solid #D2D2D2;
}
.gwt-DialogBox .dialogContent {
}
.gwt-DialogBox .dialogMiddleCenter {
padding: 3px;
background: white;
}
.gwt-DialogBox .dialogBottomCenter {
background: url(images/hborder.png) repeat-x 0px -2945px;
-background: url(images/hborder_ie6.png) repeat-x 0px -2144px;
}
.gwt-DialogBox .dialogMiddleLeft {
background: url(images/vborder.png) repeat-y -31px 0px;
}
.gwt-DialogBox .dialogMiddleRight {
background: url(images/vborder.png) repeat-y -32px 0px;
-background: url(images/vborder_ie6.png) repeat-y -32px 0px;
}
.gwt-DialogBox .dialogTopLeftInner {
width: 10px;
height: 8px;
zoom: 1;
}
.gwt-DialogBox .dialogTopRightInner {
width: 12px;
zoom: 1;
}
.gwt-DialogBox .dialogBottomLeftInner {
width: 10px;
height: 12px;
zoom: 1;
}
.gwt-DialogBox .dialogBottomRightInner {
width: 12px;
height: 12px;
zoom: 1;
}
.gwt-DialogBox .dialogTopLeft {
background: url(images/circles.png) no-repeat -20px 0px;
-background: url(images/circles_ie6.png) no-repeat -20px 0px;
}
.gwt-DialogBox .dialogTopRight {
background: url(images/circles.png) no-repeat -28px 0px;
-background: url(images/circles_ie6.png) no-repeat -28px 0px;
}
.gwt-DialogBox .dialogBottomLeft {
background: url(images/circles.png) no-repeat 0px -36px;
-background: url(images/circles_ie6.png) no-repeat 0px -36px;
}
.gwt-DialogBox .dialogBottomRight {
background: url(images/circles.png) no-repeat -8px -36px;
-background: url(images/circles_ie6.png) no-repeat -8px -36px;
}
* html .gwt-DialogBox .dialogTopLeftInner {
width: 10px;
overflow: hidden;
}
* html .gwt-DialogBox .dialogTopRightInner {
width: 12px;
overflow: hidden;
}
* html .gwt-DialogBox .dialogBottomLeftInner {
width: 10px;
height: 12px;
overflow: hidden;
}
* html .gwt-DialogBox .dialogBottomRightInner {
width: 12px;
height: 12px;
overflow: hidden;
}

View File

@@ -0,0 +1,18 @@
function genex(){var P='',xb='" for "gwt:onLoadErrorFn"',vb='" for "gwt:onPropertyErrorFn"',ib='"><\/script>',Z='#',Xb='.cache.html',_='/',lb='//',Qb='3F4ADBED36D589545A9300A1EA686D36',Rb='73F4B6D6D466BAD6850A60128DF5B80D',Wb=':',pb='::',dc='<script defer="defer">genex.onInjectionDone(\'genex\')<\/script>',hb='<script id="',sb='=',$='?',Sb='BA18AC23ACC5016C5D0799E864BBDFFE',ub='Bad handler "',Tb='C7B18436BA03373FB13ED589C2CCF417',cc='DOMContentLoaded',Ub='E1A9A95677AFC620CAD5759B7ACC3E67',Vb='FF175D5583BDD5ACF40C7F0AFF9A374B',jb='SCRIPT',gb='__gwt_marker_genex',kb='base',cb='baseUrl',T='begin',S='bootstrap',bb='clear.cache.gif',rb='content',Y='end',Kb='gecko',Lb='gecko1_8',Q='genex',Yb='genex.css',eb='genex.nocache.js',ob='genex::',U='gwt.codesvr=',V='gwt.hosted=',W='gwt.hybrid',wb='gwt:onLoadErrorFn',tb='gwt:onPropertyErrorFn',qb='gwt:property',bc='head',Ob='hosted.html?genex',ac='href',Jb='ie6',Ib='ie8',Hb='ie9',yb='iframe',ab='img',zb="javascript:''",Zb='link',Nb='loadExternalRefs',mb='meta',Bb='moduleRequested',X='moduleStartup',Gb='msie',nb='name',Db='opera',Ab='position:absolute;width:0;height:0;border:none',$b='rel',Fb='safari',db='script',Pb='selectingPermutation',R='startup',_b='stylesheet',fb='undefined',Mb='unknown',Cb='user.agent',Eb='webkit';var m=window,n=document,o=m.__gwtStatsEvent?function(a){return m.__gwtStatsEvent(a)}:null,p=m.__gwtStatsSessionId?m.__gwtStatsSessionId:null,q,r,s,t=P,u={},v=[],w=[],x=[],y=0,z,A;o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:S,millis:(new Date).getTime(),type:T});if(!m.__gwt_stylesLoaded){m.__gwt_stylesLoaded={}}if(!m.__gwt_scriptsLoaded){m.__gwt_scriptsLoaded={}}function B(){var b=false;try{var c=m.location.search;return (c.indexOf(U)!=-1||(c.indexOf(V)!=-1||m.external&&m.external.gwtOnLoad))&&c.indexOf(W)==-1}catch(a){}B=function(){return b};return b}
function C(){if(q&&r){var b=n.getElementById(Q);var c=b.contentWindow;if(B()){c.__gwt_getProperty=function(a){return H(a)}}genex=null;c.gwtOnLoad(z,Q,t,y);o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:X,millis:(new Date).getTime(),type:Y})}}
function D(){function e(a){var b=a.lastIndexOf(Z);if(b==-1){b=a.length}var c=a.indexOf($);if(c==-1){c=a.length}var d=a.lastIndexOf(_,Math.min(c,b));return d>=0?a.substring(0,d+1):P}
function f(a){if(a.match(/^\w+:\/\//)){}else{var b=n.createElement(ab);b.src=a+bb;a=e(b.src)}return a}
function g(){var a=F(cb);if(a!=null){return a}return P}
function h(){var a=n.getElementsByTagName(db);for(var b=0;b<a.length;++b){if(a[b].src.indexOf(eb)!=-1){return e(a[b].src)}}return P}
function i(){var a;if(typeof isBodyLoaded==fb||!isBodyLoaded()){var b=gb;var c;n.write(hb+b+ib);c=n.getElementById(b);a=c&&c.previousSibling;while(a&&a.tagName!=jb){a=a.previousSibling}if(c){c.parentNode.removeChild(c)}if(a&&a.src){return e(a.src)}}return P}
function j(){var a=n.getElementsByTagName(kb);if(a.length>0){return a[a.length-1].href}return P}
function k(){var a=n.location;return a.href==a.protocol+lb+a.host+a.pathname+a.search+a.hash}
var l=g();if(l==P){l=h()}if(l==P){l=i()}if(l==P){l=j()}if(l==P&&k()){l=e(n.location.href)}l=f(l);t=l;return l}
function E(){var b=document.getElementsByTagName(mb);for(var c=0,d=b.length;c<d;++c){var e=b[c],f=e.getAttribute(nb),g;if(f){f=f.replace(ob,P);if(f.indexOf(pb)>=0){continue}if(f==qb){g=e.getAttribute(rb);if(g){var h,i=g.indexOf(sb);if(i>=0){f=g.substring(0,i);h=g.substring(i+1)}else{f=g;h=P}u[f]=h}}else if(f==tb){g=e.getAttribute(rb);if(g){try{A=eval(g)}catch(a){alert(ub+g+vb)}}}else if(f==wb){g=e.getAttribute(rb);if(g){try{z=eval(g)}catch(a){alert(ub+g+xb)}}}}}}
function F(a){var b=u[a];return b==null?null:b}
function G(a,b){var c=x;for(var d=0,e=a.length-1;d<e;++d){c=c[a[d]]||(c[a[d]]=[])}c[a[e]]=b}
function H(a){var b=w[a](),c=v[a];if(b in c){return b}var d=[];for(var e in c){d[c[e]]=e}if(A){A(a,d,b)}throw null}
var I;function J(){if(!I){I=true;var a=n.createElement(yb);a.src=zb;a.id=Q;a.style.cssText=Ab;a.tabIndex=-1;n.body.appendChild(a);o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:X,millis:(new Date).getTime(),type:Bb});a.contentWindow.location.replace(t+L)}}
w[Cb]=function(){var b=navigator.userAgent.toLowerCase();var c=function(a){return parseInt(a[1])*1000+parseInt(a[2])};if(function(){return b.indexOf(Db)!=-1}())return Db;if(function(){return b.indexOf(Eb)!=-1}())return Fb;if(function(){return b.indexOf(Gb)!=-1&&n.documentMode>=9}())return Hb;if(function(){return b.indexOf(Gb)!=-1&&n.documentMode>=8}())return Ib;if(function(){var a=/msie ([0-9]+)\.([0-9]+)/.exec(b);if(a&&a.length==3)return c(a)>=6000}())return Jb;if(function(){return b.indexOf(Kb)!=-1}())return Lb;return Mb};v[Cb]={gecko1_8:0,ie6:1,ie8:2,ie9:3,opera:4,safari:5};genex.onScriptLoad=function(){if(I){r=true;C()}};genex.onInjectionDone=function(){q=true;o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:Nb,millis:(new Date).getTime(),type:Y});C()};E();D();var K;var L;if(B()){if(m.external&&(m.external.initModule&&m.external.initModule(Q))){m.location.reload();return}L=Ob;K=P}o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:S,millis:(new Date).getTime(),type:Pb});if(!B()){try{G([Hb],Qb);G([Fb],Rb);G([Ib],Sb);G([Lb],Tb);G([Db],Ub);G([Jb],Vb);K=x[H(Cb)];var M=K.indexOf(Wb);if(M!=-1){y=Number(K.substring(M+1));K=K.substring(0,M)}L=K+Xb}catch(a){return}}var N;function O(){if(!s){s=true;if(!__gwt_stylesLoaded[Yb]){var a=n.createElement(Zb);__gwt_stylesLoaded[Yb]=a;a.setAttribute($b,_b);a.setAttribute(ac,t+Yb);n.getElementsByTagName(bc)[0].appendChild(a)}C();if(n.removeEventListener){n.removeEventListener(cc,O,false)}if(N){clearInterval(N)}}}
if(n.addEventListener){n.addEventListener(cc,function(){J();O()},false)}var N=setInterval(function(){if(/loaded|complete/.test(n.readyState)){J();O()}},50);o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:S,millis:(new Date).getTime(),type:Y});o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:Nb,millis:(new Date).getTime(),type:T});n.write(dc)}
genex();

View File

@@ -0,0 +1,365 @@
<html>
<head><script>
var $wnd = parent;
var $doc = $wnd.document;
var $moduleName, $moduleBase, $entry
,$stats = $wnd.__gwtStatsEvent ? function(a) {return $wnd.__gwtStatsEvent(a);} : null
,$sessionId = $wnd.__gwtStatsSessionId ? $wnd.__gwtStatsSessionId : null;
// Lightweight metrics
if ($stats) {
var moduleFuncName = location.search.substr(1);
var moduleFunc = $wnd[moduleFuncName];
var moduleName = moduleFunc ? moduleFunc.moduleName : "unknown";
$stats({moduleName:moduleName,sessionId:$sessionId,subSystem:'startup',evtGroup:'moduleStartup',millis:(new Date()).getTime(),type:'moduleEvalStart'});
}
var $hostedHtmlVersion="2.1";
var gwtOnLoad;
var $hosted = "localhost:9997";
function loadIframe(url) {
var topDoc = window.top.document;
// create an iframe
var iframeDiv = topDoc.createElement("div");
iframeDiv.innerHTML = "<iframe scrolling=no frameborder=0 src='" + url + "'>";
var iframe = iframeDiv.firstChild;
// mess with the iframe style a little
var iframeStyle = iframe.style;
iframeStyle.position = "absolute";
iframeStyle.borderWidth = "0";
iframeStyle.left = "0";
iframeStyle.top = "0";
iframeStyle.width = "100%";
iframeStyle.backgroundColor = "#ffffff";
iframeStyle.zIndex = "1";
iframeStyle.height = "100%";
// update the top window's document's body's style
var hostBodyStyle = window.top.document.body.style;
hostBodyStyle.margin = "0";
hostBodyStyle.height = iframeStyle.height;
hostBodyStyle.overflow = "hidden";
// insert the iframe
topDoc.body.insertBefore(iframe, topDoc.body.firstChild);
}
var ua = navigator.userAgent.toLowerCase();
if (ua.indexOf("gecko") != -1) {
// install eval wrapper on FF to avoid EvalError problem
var __eval = window.eval;
window.eval = function(s) {
return __eval(s);
}
}
if (ua.indexOf("chrome") != -1) {
// work around __gwt_ObjectId appearing in JS objects
var hop = Object.prototype.hasOwnProperty;
Object.prototype.hasOwnProperty = function(prop) {
return prop != "__gwt_ObjectId" && hop.call(this, prop);
};
// do the same in our parent as well -- see issue 4486
// NOTE: this will have to be changed when we support non-iframe-based DevMode
var hop2 = parent.Object.prototype.hasOwnProperty;
parent.Object.prototype.hasOwnProperty = function(prop) {
return prop != "__gwt_ObjectId" && hop2.call(this, prop);
};
}
// wrapper to call JS methods, which we need both to be able to supply a
// different this for method lookup and to get the exception back
function __gwt_jsInvoke(thisObj, methodName) {
try {
var args = Array.prototype.slice.call(arguments, 2);
return [0, window[methodName].apply(thisObj, args)];
} catch (e) {
return [1, e];
}
}
var __gwt_javaInvokes = [];
function __gwt_makeJavaInvoke(argCount) {
return __gwt_javaInvokes[argCount] || __gwt_doMakeJavaInvoke(argCount);
}
function __gwt_doMakeJavaInvoke(argCount) {
// IE6 won't eval() anonymous functions except as r-values
var argList = "";
for (var i = 0; i < argCount; i++) {
argList += ",p" + i;
}
var argListNoComma = argList.substring(1);
return eval(
"__gwt_javaInvokes[" + argCount + "] =\n" +
" function(thisObj, dispId" + argList + ") {\n" +
" var result = __static(dispId, thisObj" + argList + ");\n" +
" if (result[0]) {\n" +
" throw result[1];\n" +
" } else {\n" +
" return result[1];\n" +
" }\n" +
" }\n"
);
}
/*
* This is used to create tear-offs of Java methods. Each function corresponds
* to exactly one dispId, and also embeds the argument count. We get the "this"
* value from the context in which the function is being executed.
* Function-object identity is preserved by caching in a sparse array.
*/
var __gwt_tearOffs = [];
var __gwt_tearOffGenerators = [];
function __gwt_makeTearOff(proxy, dispId, argCount) {
return __gwt_tearOffs[dispId] || __gwt_doMakeTearOff(dispId, argCount);
}
function __gwt_doMakeTearOff(dispId, argCount) {
return __gwt_tearOffs[dispId] =
(__gwt_tearOffGenerators[argCount] || __gwt_doMakeTearOffGenerator(argCount))(dispId);
}
function __gwt_doMakeTearOffGenerator(argCount) {
// IE6 won't eval() anonymous functions except as r-values
var argList = "";
for (var i = 0; i < argCount; i++) {
argList += ",p" + i;
}
var argListNoComma = argList.substring(1);
return eval(
"__gwt_tearOffGenerators[" + argCount + "] =\n" +
" function(dispId) {\n" +
" return function(" + argListNoComma + ") {\n" +
" var result = __static(dispId, this" + argList + ");\n" +
" if (result[0]) {\n" +
" throw result[1];\n" +
" } else {\n" +
" return result[1];\n" +
" }\n" +
" }\n" +
" }\n"
);
}
function __gwt_makeResult(isException, result) {
return [isException, result];
}
function __gwt_disconnected() {
// Prevent double-invocation.
window.__gwt_disconnected = new Function();
// Do it in a timeout so we can be sure we have a clean stack.
window.setTimeout(__gwt_disconnected_impl, 1);
}
function __gwt_disconnected_impl() {
__gwt_displayGlassMessage('GWT Code Server Disconnected',
'Most likely, you closed GWT Development Mode. Or, you might have lost '
+ 'network connectivity. To fix this, try restarting GWT Development Mode and '
+ 'refresh this page.');
}
// Keep track of z-index to allow layering of multiple glass messages
var __gwt_glassMessageZIndex = 2147483647;
// Note this method is also used by ModuleSpace.java
function __gwt_displayGlassMessage(summary, details) {
var topWin = window.top;
var topDoc = topWin.document;
var outer = topDoc.createElement("div");
// Do not insert whitespace or outer.firstChild will get a text node.
outer.innerHTML =
'<div style="position:absolute;z-index:' + __gwt_glassMessageZIndex-- +
';left:50px;top:50px;width:600px;color:#FFF;font-family:verdana;text-align:left;">' +
'<div style="font-size:30px;font-weight:bold;">' + summary + '</div>' +
'<div style="font-size:15px;">' + details + '</div>' +
'</div>' +
'<div style="position:absolute;z-index:' + __gwt_glassMessageZIndex-- +
';left:0px;top:0px;right:0px;bottom:0px;filter:alpha(opacity=60);opacity:0.6;background-color:#000;"></div>'
;
topDoc.body.appendChild(outer);
var glass = outer.firstChild;
var glassStyle = glass.style;
// Scroll to the top and remove scrollbars.
topWin.scrollTo(0, 0);
if (topDoc.compatMode == "BackCompat") {
topDoc.body.style["overflow"] = "hidden";
} else {
topDoc.documentElement.style["overflow"] = "hidden";
}
// Steal focus.
glass.focus();
if ((navigator.userAgent.indexOf("MSIE") >= 0) && (topDoc.compatMode == "BackCompat")) {
// IE quirks mode doesn't support right/bottom, but does support this.
glassStyle.width = "125%";
glassStyle.height = "100%";
} else if (navigator.userAgent.indexOf("MSIE 6") >= 0) {
// IE6 doesn't have a real standards mode, so we have to use hacks.
glassStyle.width = "125%"; // Get past scroll bar area.
// Nasty CSS; onresize would be better but the outer window won't let us add a listener IE.
glassStyle.setExpression("height", "document.documentElement.clientHeight");
}
$doc.title = summary + " [" + $doc.title + "]";
}
function findPluginObject() {
try {
return document.getElementById('pluginObject');
} catch (e) {
return null;
}
}
function findPluginEmbed() {
try {
return document.getElementById('pluginEmbed')
} catch (e) {
return null;
}
}
function findPluginXPCOM() {
try {
return __gwt_HostedModePlugin;
} catch (e) {
return null;
}
}
gwtOnLoad = function(errFn, modName, modBase){
$moduleName = modName;
$moduleBase = modBase;
// Note that the order is important
var pluginFinders = [
findPluginXPCOM,
findPluginObject,
findPluginEmbed,
];
var topWin = window.top;
var url = topWin.location.href;
if (!topWin.__gwt_SessionID) {
var ASCII_EXCLAMATION = 33;
var ASCII_TILDE = 126;
var chars = [];
for (var i = 0; i < 16; ++i) {
chars.push(Math.floor(ASCII_EXCLAMATION
+ Math.random() * (ASCII_TILDE - ASCII_EXCLAMATION + 1)));
}
topWin.__gwt_SessionID = String.fromCharCode.apply(null, chars);
}
var plugin = null;
for (var i = 0; i < pluginFinders.length; ++i) {
try {
var maybePlugin = pluginFinders[i]();
if (maybePlugin != null && maybePlugin.init(window)) {
plugin = maybePlugin;
break;
}
} catch (e) {
}
}
if (!plugin) {
// try searching for a v1 plugin for backwards compatibility
var found = false;
for (var i = 0; i < pluginFinders.length; ++i) {
try {
plugin = pluginFinders[i]();
if (plugin != null && plugin.connect($hosted, $moduleName, window)) {
return;
}
} catch (e) {
}
}
loadIframe("http://gwt.google.com/missing-plugin");
} else {
if (plugin.connect(url, topWin.__gwt_SessionID, $hosted, $moduleName,
$hostedHtmlVersion)) {
window.onUnload = function() {
try {
// wrap in try/catch since plugins are not required to supply this
plugin.disconnect();
} catch (e) {
}
};
} else {
if (errFn) {
errFn(modName);
} else {
__gwt_displayGlassMessage(
"Plugin failed to connect to Development Mode server at " + simpleEscape($hosted),
"Follow the troubleshooting instructions at "
+ "<a href='http://code.google.com/p/google-web-toolkit/wiki/TroubleshootingOOPHM'>"
+ "http://code.google.com/p/google-web-toolkit/wiki/TroubleshootingOOPHM</a>");
}
}
}
}
function simpleEscape(originalString) {
return originalString.replace(/&/g,"&amp;")
.replace(/</g,"&lt;")
.replace(/>/g,"&gt;")
.replace(/\'/g, "&#39;")
.replace(/\"/g,"&quot;");
}
window.onunload = function() {
};
// Lightweight metrics
window.fireOnModuleLoadStart = function(className) {
$stats && $stats({moduleName:$moduleName, sessionId:$sessionId, subSystem:'startup', evtGroup:'moduleStartup', millis:(new Date()).getTime(), type:'onModuleLoadStart', className:className});
};
window.__gwt_module_id = 0;
</script></head>
<body>
<font face='arial' size='-1'>This html file is for Development Mode support.</font>
<script><!--
// Lightweight metrics
$stats && $stats({moduleName:$moduleName, sessionId:$sessionId, subSystem:'startup', evtGroup:'moduleStartup', millis:(new Date()).getTime(), type:'moduleEvalEnd'});
// OOPHM currently only supports IFrameLinker
var query = parent.location.search;
if (!findPluginXPCOM()) {
document.write('<embed id="pluginEmbed" type="application/x-gwt-hosted-mode" width="10" height="10">');
document.write('</embed>');
document.write('<object id="pluginObject" CLASSID="CLSID:1D6156B6-002B-49E7-B5CA-C138FB843B4E">');
document.write('</object>');
}
// look for the old query parameter if we don't find the new one
var idx = query.indexOf("gwt.codesvr=");
if (idx >= 0) {
idx += 12; // "gwt.codesvr=".length() == 12
} else {
idx = query.indexOf("gwt.hosted=");
if (idx >= 0) {
idx += 11; // "gwt.hosted=".length() == 11
}
}
if (idx >= 0) {
var amp = query.indexOf("&", idx);
if (amp >= 0) {
$hosted = query.substring(idx, amp);
} else {
$hosted = query.substring(idx);
}
// According to RFC 3986, some of this component's characters (e.g., ':')
// are reserved and *may* be escaped.
$hosted = decodeURIComponent($hosted);
}
query = window.location.search.substring(1);
if (query && $wnd[query]) setTimeout($wnd[query].onScriptLoad, 1);
--></script></body></html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Some files were not shown because too many files have changed in this diff Show More