Merge branch 'feature/alex/poll-merged' of github.com:MITx/mitx into feature/alex/poll-merged
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -28,4 +28,5 @@ nosetests.xml
|
||||
cover_html/
|
||||
.idea/
|
||||
.redcar/
|
||||
chromedriver.log
|
||||
chromedriver.log
|
||||
ghostdriver.log
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.8.7-p371
|
||||
1.9.3-p374
|
||||
2
Gemfile
2
Gemfile
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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$')
|
||||
@@ -42,8 +44,20 @@ def edit_the_name_of_a_policy_key(step):
|
||||
|
||||
@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):
|
||||
@@ -99,29 +113,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, '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 +146,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))
|
||||
@@ -165,16 +183,16 @@ def assert_entries(css, expected_values):
|
||||
|
||||
|
||||
def click_save():
|
||||
css = ".save-button"
|
||||
css = "a.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)
|
||||
# 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_at(css)
|
||||
|
||||
|
||||
def fill_last_field(value):
|
||||
|
||||
@@ -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,))
|
||||
|
||||
assert_true(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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -265,7 +265,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):
|
||||
ms = modulestore('direct')
|
||||
cs = contentstore()
|
||||
|
||||
import_from_xml(ms, 'common/test/data/', ['full'])
|
||||
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
|
||||
|
||||
root_dir = path(mkdtemp_clean())
|
||||
|
||||
course = ms.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
|
||||
|
||||
ms.update_metadata(location, course.metadata)
|
||||
|
||||
print 'Exporting to tempdir = {0}'.format(root_dir)
|
||||
|
||||
# export out to a tempdir
|
||||
bExported = False
|
||||
try:
|
||||
export_to_xml(ms, cs, location, root_dir, 'test_export')
|
||||
bExported = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.assertTrue(bExported)
|
||||
|
||||
class ContentStoreTest(ModuleStoreTestCase):
|
||||
"""
|
||||
|
||||
@@ -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")
|
||||
@@ -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
6
cms/one_time_startup.py
Normal 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)
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -148,6 +149,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
|
||||
@@ -326,7 +334,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 ========
|
||||
|
||||
@@ -460,6 +488,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.
|
||||
@@ -473,7 +503,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
|
||||
@@ -486,8 +516,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)
|
||||
@@ -498,17 +529,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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(__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):
|
||||
"""
|
||||
@@ -740,6 +754,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)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
@@ -925,11 +978,12 @@ class EditAGeneInput(InputTypeBase):
|
||||
@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):
|
||||
@@ -943,3 +997,111 @@ class EditAGeneInput(InputTypeBase):
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
70
common/lib/capa/capa/templates/annotationinput.html
Normal file
70
common/lib/capa/capa/templates/annotationinput.html
Normal 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,37 +1,40 @@
|
||||
<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"> </span>
|
||||
<div class="indicator_container">
|
||||
% if input_type == 'checkbox' or not value:
|
||||
% 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
|
||||
|
||||
<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'
|
||||
if status == 'incorrect':
|
||||
correctness = 'incorrect'
|
||||
%>
|
||||
class="choicegroup_${correctness}"
|
||||
% 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>
|
||||
|
||||
</form>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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'})
|
||||
|
||||
|
||||
@@ -482,27 +482,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 +586,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)
|
||||
|
||||
@@ -906,3 +906,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))
|
||||
|
||||
@@ -48,6 +48,7 @@ setup(
|
||||
"about = xmodule.html_module:AboutDescriptor",
|
||||
"wrapper = xmodule.wrapper_module:WrapperDescriptor",
|
||||
"graphical_slider_tool = xmodule.gst_module:GraphicalSliderToolDescriptor",
|
||||
"annotatable = xmodule.annotatable_module:AnnotatableDescriptor",
|
||||
"foldit = xmodule.foldit_module:FolditDescriptor",
|
||||
]
|
||||
}
|
||||
|
||||
131
common/lib/xmodule/xmodule/annotatable_module.py
Normal file
131
common/lib/xmodule/xmodule/annotatable_module.py
Normal 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"
|
||||
|
||||
@@ -113,8 +113,6 @@ class CapaModule(XModule):
|
||||
|
||||
if self.graceperiod is not None and due_date:
|
||||
self.close_date = due_date + self.graceperiod
|
||||
#log.debug("Then parsed " + grace_period_string +
|
||||
# " to closing date" + str(self.close_date))
|
||||
else:
|
||||
self.close_date = due_date
|
||||
|
||||
@@ -209,122 +207,176 @@ 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('<', '<')
|
||||
msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<', '<')
|
||||
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:
|
||||
# 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 = self.new_lcp(None)
|
||||
self.set_state_from_lcp()
|
||||
|
||||
# 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: # Couldn't do it. Give up
|
||||
log.exception("Unable to generate html from LoncapaProblem")
|
||||
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('<', '<')
|
||||
msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<', '<')
|
||||
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 = self.new_lcp(None)
|
||||
self.set_state_from_lcp()
|
||||
|
||||
# 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: # Couldn't do it. Give up
|
||||
log.exception("Unable to generate html from LoncapaProblem")
|
||||
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.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.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 (not self.force_save_button) 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,
|
||||
@@ -357,6 +409,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:
|
||||
@@ -379,11 +432,8 @@ class CapaModule(XModule):
|
||||
datetime.datetime.utcnow() > self.close_date)
|
||||
|
||||
def closed(self):
|
||||
''' Is the student prevented from submitting answers? '''
|
||||
if self.max_attempts is None:
|
||||
return False
|
||||
|
||||
if self.attempts >= self.max_attempts:
|
||||
''' Is the student still allowed to submit answers? '''
|
||||
if self.max_attempts is not None and self.attempts >= self.max_attempts:
|
||||
return True
|
||||
if self.is_past_due():
|
||||
return True
|
||||
@@ -494,22 +544,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
|
||||
|
||||
@@ -529,7 +617,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()
|
||||
@@ -642,7 +730,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()
|
||||
@@ -676,7 +769,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):
|
||||
|
||||
@@ -175,7 +175,7 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
no_grade = Boolean(help="True if this course isn't graded", default=False, scope=Scope.settings)
|
||||
disable_progress_graph = Boolean(help="True if this course shouldn't display the progress graph", default=False, scope=Scope.settings)
|
||||
pdf_textbooks = List(help="List of dictionaries containing pdf_textbook configuration", default=None, scope=Scope.settings)
|
||||
remote_gradebook = String(scope=Scope.settings, default='')
|
||||
remote_gradebook = Object(scope=Scope.settings, default={})
|
||||
allow_anonymous = Boolean(scope=Scope.settings, default=True)
|
||||
allow_anonymous_to_peers = Boolean(scope=Scope.settings, default=False)
|
||||
has_children = True
|
||||
|
||||
169
common/lib/xmodule/xmodule/css/annotatable/display.scss
Normal file
169
common/lib/xmodule/xmodule/css/annotatable/display.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +97,7 @@ class FolditModule(XModule):
|
||||
|
||||
showbasic = (self.show_basic_score.lower() == "true")
|
||||
showleader = (self.show_leaderboard.lower() == "true")
|
||||
|
||||
context = {
|
||||
'due': self.due,
|
||||
'success': self.is_complete(),
|
||||
|
||||
35
common/lib/xmodule/xmodule/js/fixtures/annotatable.html
Normal file
35
common/lib/xmodule/xmodule/js/fixtures/annotatable.html
Normal 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>
|
||||
|
||||
@@ -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)
|
||||
197
common/lib/xmodule/xmodule/js/src/annotatable/display.coffee
Normal file
197
common/lib/xmodule/xmodule/js/src/annotatable/display.coffee
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -75,6 +75,11 @@ class SequenceModule(XModule):
|
||||
raise NotFoundError('Unexpected dispatch type')
|
||||
|
||||
def render(self):
|
||||
# If we're rendering this sequence, but no position is set yet,
|
||||
# default the position to the first element
|
||||
if self.position is None:
|
||||
self.position = 1
|
||||
|
||||
if self.rendered:
|
||||
return
|
||||
## Returns a set of all types of all sub-children
|
||||
|
||||
@@ -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><annotation></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: []
|
||||
@@ -28,22 +28,36 @@ 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"),
|
||||
xblock_model_data=lambda descriptor: descriptor._model_data,
|
||||
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"),
|
||||
xblock_model_data=lambda descriptor: descriptor._model_data,
|
||||
anonymous_student_id='student',
|
||||
open_ended_grading_interface= open_ended_grading_interface
|
||||
)
|
||||
|
||||
|
||||
class ModelsTest(unittest.TestCase):
|
||||
|
||||
129
common/lib/xmodule/xmodule/tests/test_annotatable_module.py
Normal file
129
common/lib/xmodule/xmodule/tests/test_annotatable_module.py
Normal 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)
|
||||
@@ -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,7 +76,6 @@ 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",
|
||||
@@ -87,13 +103,14 @@ class CapaFactory(object):
|
||||
# since everything else is a string.
|
||||
model_data['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, descriptor, model_data)
|
||||
system = test_system()
|
||||
system.render_template = Mock(return_value="<div>Test Template HTML</div>")
|
||||
module = CapaModule(system, location, descriptor, model_data)
|
||||
|
||||
if correct:
|
||||
# TODO: probably better to actually set the internal state properly, but...
|
||||
@@ -127,6 +144,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.
|
||||
@@ -170,6 +189,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
max_attempts="1",
|
||||
attempts="0",
|
||||
due=self.yesterday_str)
|
||||
|
||||
self.assertTrue(after_due_date.answer_available())
|
||||
|
||||
|
||||
@@ -274,3 +294,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)
|
||||
|
||||
@@ -54,8 +54,9 @@ class OpenEndedChildTest(unittest.TestCase):
|
||||
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):
|
||||
@@ -69,7 +70,7 @@ class OpenEndedChildTest(unittest.TestCase):
|
||||
|
||||
|
||||
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, "")
|
||||
|
||||
|
||||
@@ -106,7 +107,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"
|
||||
@@ -125,7 +126,7 @@ class OpenEndedChildTest(unittest.TestCase):
|
||||
|
||||
|
||||
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['child_state'], OpenEndedChild.INITIAL)
|
||||
|
||||
@@ -182,12 +183,14 @@ 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',
|
||||
@@ -195,7 +198,7 @@ class OpenEndedModuleTest(unittest.TestCase):
|
||||
'grader_id': '1',
|
||||
'score': 3}
|
||||
qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat)
|
||||
student_info = {'anonymous_student_id': test_system.anonymous_student_id,
|
||||
student_info = {'anonymous_student_id': self.test_system.anonymous_student_id,
|
||||
'submission_time': qtime}
|
||||
contents = {
|
||||
'feedback': get['feedback'],
|
||||
@@ -205,7 +208,7 @@ class OpenEndedModuleTest(unittest.TestCase):
|
||||
'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,7 +219,7 @@ 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,
|
||||
student_info = {'anonymous_student_id': self.test_system.anonymous_student_id,
|
||||
'submission_time': qtime}
|
||||
contents = self.openendedmodule.payload.copy()
|
||||
contents.update({
|
||||
@@ -224,7 +227,7 @@ class OpenEndedModuleTest(unittest.TestCase):
|
||||
'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)
|
||||
|
||||
@@ -238,7 +241,7 @@ class OpenEndedModuleTest(unittest.TestCase):
|
||||
}
|
||||
get = {'queuekey': "abcd",
|
||||
'xqueue_body': score_msg}
|
||||
self.openendedmodule.update_score(get, test_system)
|
||||
self.openendedmodule.update_score(get, self.test_system)
|
||||
|
||||
def update_score_single(self):
|
||||
self.openendedmodule.new_history_entry("New Entry")
|
||||
@@ -261,11 +264,11 @@ class OpenEndedModuleTest(unittest.TestCase):
|
||||
}
|
||||
get = {'queuekey': "abcd",
|
||||
'xqueue_body': json.dumps(score_msg)}
|
||||
self.openendedmodule.update_score(get, test_system)
|
||||
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)
|
||||
@@ -336,7 +339,16 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
|
||||
descriptor = Mock()
|
||||
|
||||
def setUp(self):
|
||||
self.combinedoe = CombinedOpenEndedV1Module(test_system, self.location, self.definition, self.descriptor, static_data = self.static_data, metadata=self.metadata, instance_state={})
|
||||
self.test_system = test_system()
|
||||
# TODO: this constructor call is definitely wrong, but neither branch
|
||||
# of the merge matches the module constructor. Someone (Vik?) should fix this.
|
||||
self.combinedoe = CombinedOpenEndedV1Module(self.test_system,
|
||||
self.location,
|
||||
self.definition,
|
||||
self.descriptor,
|
||||
static_data=self.static_data,
|
||||
metadata=self.metadata,
|
||||
instance_state={})
|
||||
|
||||
def test_get_tag_name(self):
|
||||
name = self.combinedoe.get_tag_name("<t>Tag</t>")
|
||||
|
||||
@@ -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)
|
||||
@@ -80,7 +83,7 @@ class ConditionalModuleTest(unittest.TestCase):
|
||||
location = descriptor
|
||||
descriptor = self.modulestore.get_instance(course.id, location, depth=None)
|
||||
location = descriptor.location
|
||||
return descriptor.xmodule(test_system)
|
||||
return descriptor.xmodule(self.test_system)
|
||||
|
||||
# edx - HarvardX
|
||||
# cond_test - ER22x
|
||||
@@ -88,8 +91,8 @@ class ConditionalModuleTest(unittest.TestCase):
|
||||
|
||||
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
|
||||
|
||||
@@ -51,13 +51,13 @@ class SelfAssessmentTest(unittest.TestCase):
|
||||
'skip_basic_checks' : False,
|
||||
}
|
||||
|
||||
self.module = SelfAssessmentModule(test_system, self.location,
|
||||
self.module = SelfAssessmentModule(test_system(), self.location,
|
||||
self.definition,
|
||||
self.descriptor,
|
||||
static_data)
|
||||
|
||||
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):
|
||||
@@ -80,10 +80,11 @@ 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.child_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.child_state, self.module.DONE)
|
||||
|
||||
|
||||
@@ -92,7 +93,8 @@ class SelfAssessmentTest(unittest.TestCase):
|
||||
self.assertEqual(self.module.child_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.child_state, self.module.DONE)
|
||||
|
||||
@@ -135,7 +135,7 @@ class TimeLimitDescriptor(XMLEditingDescriptor, XmlDescriptor):
|
||||
if system.error_tracker is not None:
|
||||
system.error_tracker("ERROR: " + str(e))
|
||||
continue
|
||||
return {'children': children}
|
||||
return {}, children
|
||||
|
||||
def definition_to_xml(self, resource_fs):
|
||||
xml_object = etree.Element('timelimit')
|
||||
|
||||
@@ -389,7 +389,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
|
||||
|
||||
for key, value in self.xml_attributes.items():
|
||||
if key not in self.metadata_to_strip:
|
||||
|
||||
@@ -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
|
||||
|
||||
BIN
common/static/images/partially-correct-icon.png
Normal file
BIN
common/static/images/partially-correct-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
97
common/static/js/capa/annotationinput.js
Normal file
97
common/static/js/capa/annotationinput.js
Normal 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);
|
||||
@@ -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');
|
||||
|
||||
@@ -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
BIN
common/static/js/capa/genex/clear.cache.gif
Normal file
BIN
common/static/js/capa/genex/clear.cache.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 B |
109
common/static/js/capa/genex/genex.css
Normal file
109
common/static/js/capa/genex/genex.css
Normal file
@@ -0,0 +1,109 @@
|
||||
.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 {
|
||||
}
|
||||
.gwt-DialogBox .dialogMiddleLeft {
|
||||
}
|
||||
.gwt-DialogBox .dialogMiddleRight {
|
||||
}
|
||||
.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 {
|
||||
}
|
||||
.gwt-DialogBox .dialogTopRight {
|
||||
}
|
||||
.gwt-DialogBox .dialogBottomLeft {
|
||||
}
|
||||
.gwt-DialogBox .dialogBottomRight {
|
||||
}
|
||||
* 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;
|
||||
}
|
||||
18
common/static/js/capa/genex/genex.nocache.js
Normal file
18
common/static/js/capa/genex/genex.nocache.js
Normal 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='026A6180B5959B8660E084245FEE5E9E',Rb='1F433010E1134C95BF6CB43F552F3019',Sb='2DDA730EDABB80B88A6B0DFA3AFEACA2',Tb='4EEB1DCF4B30D366C27968D1B5C0BD04',Ub='5033ABB047340FB9346B622E2CC7107D',Wb=':',pb='::',dc='<script defer="defer">genex.onInjectionDone(\'genex\')<\/script>',hb='<script id="',sb='=',$='?',ub='Bad handler "',Vb='DF3D3A7FAEE63D711CF2D95BDB3F538C',cc='DOMContentLoaded',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([Fb],Qb);G([Lb],Rb);G([Hb],Sb);G([Jb],Tb);G([Db],Ub);G([Ib],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();
|
||||
365
common/static/js/capa/genex/hosted.html
Normal file
365
common/static/js/capa/genex/hosted.html
Normal 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,"&")
|
||||
.replace(/</g,"<")
|
||||
.replace(/>/g,">")
|
||||
.replace(/\'/g, "'")
|
||||
.replace(/\"/g,""");
|
||||
}
|
||||
|
||||
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>
|
||||
@@ -550,15 +550,84 @@ If you want to customize the courseware tabs displayed for your course, specify
|
||||
*********
|
||||
Textbooks
|
||||
*********
|
||||
Support is currently provided for image-based and PDF-based textbooks.
|
||||
Support is currently provided for image-based and PDF-based textbooks. In addition to enabling the display of textbooks in tabs (see above), specific information about the location of textbook content must be configured.
|
||||
|
||||
Image-based Textbooks
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
=====================
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
Image-based textbooks are configured at the course level in the XML markup. Here is an example:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<course>
|
||||
<textbook title="Textbook 1" book_url="https://www.example.com/textbook_1/" />
|
||||
<textbook title="Textbook 2" book_url="https://www.example.com/textbook_2/" />
|
||||
<chapter url_name="Overview">
|
||||
<chapter url_name="First week">
|
||||
</course>
|
||||
|
||||
|
||||
Each `textbook` element is displayed on a different tab. The `title` attribute is used as the tab's name, and the `book_url` attribute points to the remote directory that contains the images of the text. Note the trailing slash on the end of the `book_url` attribute.
|
||||
|
||||
The images must be stored in the same directory as the `book_url`, with filenames matching `pXXX.png`, where `XXX` is a three-digit number representing the page number (with leading zeroes as necessary). Pages start at `p001.png`.
|
||||
|
||||
Each textbook must also have its own table of contents. This is read from the `book_url` location, by appending `toc.xml`. This file contains a `table_of_contents` parent element, with `entry` elements nested below it. Each `entry` has attributes for `name`, `page_label`, and `page`, as well as an optional `chapter` attribute. An arbitrary number of levels of nesting of `entry` elements within other `entry` elements is supported, but you're likely to only want two levels. The `page` represents the actual page to link to, while the `page_label` matches the displayed page number on that page. Here's an example:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<table_of_contents>
|
||||
<entry page="1" page_label="i" name="Title" />
|
||||
<entry page="2" page_label="ii" name="Preamble">
|
||||
<entry page="2" page_label="ii" name="Copyright"/>
|
||||
<entry page="3" page_label="iii" name="Brief Contents"/>
|
||||
<entry page="5" page_label="v" name="Contents"/>
|
||||
<entry page="9" page_label="1" name="About the Authors"/>
|
||||
<entry page="10" page_label="2" name="Acknowledgments"/>
|
||||
<entry page="11" page_label="3" name="Dedication"/>
|
||||
<entry page="12" page_label="4" name="Preface"/>
|
||||
</entry>
|
||||
<entry page="15" page_label="7" name="Introduction to edX" chapter="1">
|
||||
<entry page="15" page_label="7" name="edX in the Modern World"/>
|
||||
<entry page="18" page_label="10" name="The edX Method"/>
|
||||
<entry page="18" page_label="10" name="A Description of edX"/>
|
||||
<entry page="29" page_label="21" name="A Brief History of edX"/>
|
||||
<entry page="51" page_label="43" name="Introduction to edX"/>
|
||||
<entry page="56" page_label="48" name="Endnotes"/>
|
||||
</entry>
|
||||
<entry page="73" page_label="65" name="Art and Photo Credits" chapter="30">
|
||||
<entry page="73" page_label="65" name="Molecular Models"/>
|
||||
<entry page="73" page_label="65" name="Photo Credits"/>
|
||||
</entry>
|
||||
<entry page="77" page_label="69" name="Index" />
|
||||
</table_of_contents>
|
||||
|
||||
|
||||
Linking from Content
|
||||
--------------------
|
||||
|
||||
It is possible to add links to specific pages in a textbook by using a URL that encodes the index of the textbook and the page number. The URL is of the form `/course/book/${bookindex}/$page}`. If the page is omitted from the URL, the first page is assumed.
|
||||
|
||||
You can use a `customtag` to create a template for such links. For example, you can create a `book` template in the `customtag` directory, containing:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<img src="/static/images/icons/textbook_icon.png"/> More information given in <a href="/course/book/${book}/${page}">the text</a>.
|
||||
|
||||
The course content can then link to page 25 using the `customtag` element:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<customtag book="0" page="25" impl="book"/>
|
||||
|
||||
TBD.
|
||||
|
||||
PDF-based Textbooks
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
===================
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
PDF-based textbooks are configured at the course level in the policy file. The JSON markup consists of an array of maps, with each map corresponding to a separate textbook. There are two styles to presenting PDF-based material. The first way is as a single PDF on a tab, which requires only a tab title and a URL for configuration. A second way permits the display of multiple PDFs that should be displayed together on a single view. For this view, a side panel of links is available on the left, allowing selection of a particular PDF to view.
|
||||
|
||||
@@ -566,20 +635,51 @@ PDF-based textbooks are configured at the course level in the policy file. The
|
||||
|
||||
"pdf_textbooks": [
|
||||
{"tab_title": "Textbook 1",
|
||||
"url": "https://www.example.com/book1.pdf" },
|
||||
"url": "https://www.example.com/thiscourse/book1/book1.pdf" },
|
||||
{"tab_title": "Textbook 2",
|
||||
"chapters": [
|
||||
{ "title": "Chapter 1", "url": "https://www.example.com/Chapter1.pdf" },
|
||||
{ "title": "Chapter 2", "url": "https://www.example.com/Chapter2.pdf" },
|
||||
{ "title": "Chapter 3", "url": "https://www.example.com/Chapter3.pdf" },
|
||||
{ "title": "Chapter 4", "url": "https://www.example.com/Chapter4.pdf" },
|
||||
{ "title": "Chapter 5", "url": "https://www.example.com/Chapter5.pdf" },
|
||||
{ "title": "Chapter 6", "url": "https://www.example.com/Chapter6.pdf" },
|
||||
{ "title": "Chapter 7", "url": "https://www.example.com/Chapter7.pdf" }
|
||||
{ "title": "Chapter 1", "url": "https://www.example.com/thiscourse/book2/Chapter1.pdf" },
|
||||
{ "title": "Chapter 2", "url": "https://www.example.com/thiscourse/book2/Chapter2.pdf" },
|
||||
{ "title": "Chapter 3", "url": "https://www.example.com/thiscourse/book2/Chapter3.pdf" },
|
||||
{ "title": "Chapter 4", "url": "https://www.example.com/thiscourse/book2/Chapter4.pdf" },
|
||||
{ "title": "Chapter 5", "url": "https://www.example.com/thiscourse/book2/Chapter5.pdf" },
|
||||
{ "title": "Chapter 6", "url": "https://www.example.com/thiscourse/book2/Chapter6.pdf" },
|
||||
{ "title": "Chapter 7", "url": "https://www.example.com/thiscourse/book2/Chapter7.pdf" }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
Some notes:
|
||||
|
||||
* It is not a good idea to include a top-level URL and chapter-level URLs in the same textbook configuration.
|
||||
|
||||
Linking from Content
|
||||
--------------------
|
||||
|
||||
It is possible to add links to specific pages in a textbook by using a URL that encodes the index of the textbook, the chapter (if chapters are used), and the page number. For a book with no chapters, the URL is of the form `/course/pdfbook/${bookindex}/$page}`. For a book with chapters, use `/course/pdfbook/${bookindex}/chapter/${chapter}/${page}`. If the page is omitted from the URL, the first page is assumed.
|
||||
|
||||
For example, for the book with no chapters configured above, page 25 can be reached using the URL `/course/pdfbook/0/25`. Reaching page 19 in the third chapter of the second book is accomplished with `/course/pdfbook/1/chapter/3/19`.
|
||||
|
||||
You can use a `customtag` to create a template for such links. For example, you can create a `pdfbook` template in the `customtag` directory, containing:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<img src="/static/images/icons/textbook_icon.png"/> More information given in <a href="/course/pdfbook/${book}/${page}">the text</a>.
|
||||
|
||||
And a `pdfchapter` template containing:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<img src="/static/images/icons/textbook_icon.png"/> More information given in <a href="/course/pdfbook/${book}/chapter/${chapter}/${page}">the text</a>.
|
||||
|
||||
The example pages can then be linked using the `customtag` element:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<customtag book="0" page="25" impl="pdfbook"/>
|
||||
<customtag book="1" chapter="3" page="19" impl="pdfchapter"/>
|
||||
|
||||
|
||||
*************************************
|
||||
Other file locations (info and about)
|
||||
*************************************
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
<problem display_name="Drag and drop demos chem features: drag and drop icons or labels
|
||||
to proper positions." attempts="10">
|
||||
|
||||
<customresponse>
|
||||
<text>
|
||||
<h4>[Simple grading example: draggables on draggables]</h4><br/>
|
||||
<h4>Describe carbon molecule in LCAO-MO.</h4><br/>
|
||||
<br/>
|
||||
</text>
|
||||
|
||||
<drag_and_drop_input img="/static/images/images_list/lcao-mo/lcao-mo.jpg" target_outline="true" >
|
||||
|
||||
<!-- filled bond -->
|
||||
<draggable id="up_and_down" icon="/static/images/images_list/lcao-mo/u_d.png" can_reuse="true" />
|
||||
<!-- up bond -->
|
||||
<draggable id="up" icon="/static/images/images_list/lcao-mo/up.png" can_reuse="true" />
|
||||
|
||||
<draggable id="s" icon="/static/images/images_list/lcao-mo/orbital_single.png" label="s orbital" can_reuse="true" >
|
||||
<target id="1" x="0" y="0" w="32" h="32"/>
|
||||
</draggable>
|
||||
|
||||
<draggable id="p" icon="/static/images/images_list/lcao-mo/orbital_triple.png" can_reuse="true" >
|
||||
<target id="1" x="0" y="0" w="32" h="32"/>
|
||||
<target id="2" x="34" y="0" w="32" h="32"/>
|
||||
<target id="3" x="68" y="0" w="32" h="32"/>
|
||||
</draggable>
|
||||
|
||||
<!-- positions of electrons and electron pairs -->
|
||||
<target id="s_l" x="130" y="360" w="32" h="32"/>
|
||||
<target id="s_r" x="505" y="360" w="32" h="32"/>
|
||||
<target id="p_l" x="80" y="100" w="100" h="32"/>
|
||||
<target id="p_r" x="465" y="100" w="100" h="32"/>
|
||||
|
||||
</drag_and_drop_input>
|
||||
|
||||
<answer type="loncapa/python"><![CDATA[
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['p'],
|
||||
'targets': ['p_l', 'p_r'],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['s'],
|
||||
'targets': ['s_l', 's_r'],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['up_and_down'],
|
||||
'targets': [
|
||||
's_l[s][1]', 's_r[s][1]'
|
||||
],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['up'],
|
||||
'targets': [
|
||||
'p_l[p][1]', 'p_l[p][3]', 'p_r[p][1]', 'p_r[p][3]'
|
||||
],
|
||||
'rule': 'unordered_equal'
|
||||
}
|
||||
]
|
||||
|
||||
if draganddrop.grade(submission[0], correct_answer):
|
||||
correct = ['correct']
|
||||
else:
|
||||
correct = ['incorrect']
|
||||
]]></answer>
|
||||
</customresponse>
|
||||
|
||||
<customresponse>
|
||||
<text>
|
||||
<h4>[Complex grading example: draggables on draggables]</h4><br/>
|
||||
<h4>Describe carbon molecule in LCAO-MO.</h4>
|
||||
<br/>
|
||||
</text>
|
||||
|
||||
<drag_and_drop_input img="/static/images/images_list/lcao-mo/lcao-mo-clean.jpg" target_outline="true" >
|
||||
|
||||
<!-- filled bond -->
|
||||
<draggable id="up_and_down" icon="/static/images/images_list/lcao-mo/u_d.png" can_reuse="true" />
|
||||
<!-- up bond -->
|
||||
<draggable id="up" icon="/static/images/images_list/lcao-mo/up.png" can_reuse="true" />
|
||||
|
||||
<!-- images that should not be dragged -->
|
||||
<draggable id="down" icon="/static/images/images_list/lcao-mo/d.png" can_reuse="true" />
|
||||
|
||||
<draggable id="s" icon="/static/images/images_list/lcao-mo/orbital_single.png" label="s orbital" can_reuse="true" >
|
||||
<target id="1" x="0" y="0" w="32" h="32"/>
|
||||
</draggable>
|
||||
|
||||
<draggable id="p" icon="/static/images/images_list/lcao-mo/orbital_triple.png" can_reuse="true" label="p orbital" >
|
||||
<target id="1" x="0" y="0" w="32" h="32"/>
|
||||
<target id="2" x="34" y="0" w="32" h="32"/>
|
||||
<target id="3" x="68" y="0" w="32" h="32"/>
|
||||
</draggable>
|
||||
|
||||
<draggable id="s-sigma" icon="/static/images/images_list/lcao-mo/orbital_single.png" label="s-sigma orbital" can_reuse="true" >
|
||||
<target id="1" x="0" y="0" w="32" h="32"/>
|
||||
</draggable>
|
||||
|
||||
<draggable id="s-sigma*" icon="/static/images/images_list/lcao-mo/orbital_single.png" label="s-sigma* orbital" can_reuse="true" >
|
||||
<target id="1" x="0" y="0" w="32" h="32"/>
|
||||
</draggable>
|
||||
|
||||
<draggable id="p-pi" icon="/static/images/images_list/lcao-mo/orbital_double.png" label="p-pi orbital" can_reuse="true" >
|
||||
<target id="1" x="0" y="0" w="32" h="32"/>
|
||||
<target id="2" x="34" y="0" w="32" h="32"/>
|
||||
</draggable>
|
||||
|
||||
<draggable id="p-sigma" icon="/static/images/images_list/lcao-mo/orbital_single.png" label="p-sigma orbital" can_reuse="true" >
|
||||
<target id="1" x="0" y="0" w="32" h="32"/>
|
||||
</draggable>
|
||||
|
||||
<draggable id="p-pi*" icon="/static/images/images_list/lcao-mo/orbital_double.png" label="p-pi* orbital" can_reuse="true" >
|
||||
<target id="1" x="0" y="0" w="32" h="32"/>
|
||||
<target id="2" x="34" y="0" w="32" h="32"/>
|
||||
</draggable>
|
||||
|
||||
<draggable id="p-sigma*" icon="/static/images/images_list/lcao-mo/orbital_single.png" label="p-sigma* orbital" can_reuse="true" >
|
||||
<target id="1" x="0" y="0" w="32" h="32"/>
|
||||
</draggable>
|
||||
|
||||
<!-- positions of electrons and electron pairs -->
|
||||
<target id="s-left-target" x="130" y="360" w="32" h="32"/>
|
||||
<target id="s-right-target" x="505" y="360" w="32" h="32"/>
|
||||
<target id="s-sigma-target" x="315" y="425" w="32" h="32"/>
|
||||
<target id="s-sigma*-target" x="315" y="290" w="32" h="32"/>
|
||||
<target id="p-left-target" x="80" y="100" w="100" h="32"/>
|
||||
<target id="p-right-target" x="480" y="100" w="100" h="32"/>
|
||||
<target id="p-pi-target" x="300" y="220" w="66" h="32"/>
|
||||
<target id="p-sigma-target" x="315" y="170" w="32" h="32"/>
|
||||
<target id="p-pi*-target" x="300" y="40" w="66" h="32"/>
|
||||
<target id="p-sigma*-target" x="315" y="0" w="32" h="32"/>
|
||||
|
||||
</drag_and_drop_input>
|
||||
|
||||
<answer type="loncapa/python"><![CDATA[
|
||||
correct_answer = [
|
||||
{'draggables': ['p'], 'targets': ['p-left-target', 'p-right-target'], 'rule': 'unordered_equal'},
|
||||
{'draggables': ['s'], 'targets': ['s-left-target', 's-right-target'], 'rule': 'unordered_equal'},
|
||||
{'draggables': ['s-sigma'], 'targets': ['s-sigma-target'], 'rule': 'exact'},
|
||||
{'draggables': ['s-sigma*'], 'targets': ['s-sigma*-target'], 'rule': 'exact'},
|
||||
{'draggables': ['p-pi'], 'targets': ['p-pi-target'], 'rule': 'exact'},
|
||||
{'draggables': ['p-sigma'], 'targets': ['p-sigma-target'], 'rule': 'exact'},
|
||||
{'draggables': ['p-pi*'], 'targets': ['p-pi*-target'], 'rule': 'exact'},
|
||||
{'draggables': ['p-sigma*'], 'targets': ['p-sigma*-target'], 'rule': 'exact'},
|
||||
{
|
||||
'draggables': ['up_and_down'],
|
||||
'targets': ['s-left-target[s][1]', 's-right-target[s][1]', 's-sigma-target[s-sigma][1]', 's-sigma*-target[s-sigma*][1]', 'p-pi-target[p-pi][1]', 'p-pi-target[p-pi][2]'],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['up'],
|
||||
'targets': ['p-left-target[p][1]', 'p-left-target[p][2]', 'p-right-target[p][2]', 'p-right-target[p][3]',],
|
||||
'rule': 'unordered_equal'
|
||||
}
|
||||
]
|
||||
|
||||
if draganddrop.grade(submission[0], correct_answer):
|
||||
correct = ['correct']
|
||||
else:
|
||||
correct = ['incorrect']
|
||||
]]></answer>
|
||||
</customresponse>
|
||||
|
||||
<customresponse>
|
||||
<text>
|
||||
<h4>[Complex grading example: no draggables on draggables]</h4><br/>
|
||||
<h4>Describe carbon molecule in LCAO-MO.</h4>
|
||||
<br/>
|
||||
</text>
|
||||
|
||||
<drag_and_drop_input img="/static/images/images_list/lcao-mo/lcao-mo.jpg" target_outline="true">
|
||||
|
||||
<!-- filled bond -->
|
||||
<draggable id="1" icon="/static/images/images_list/lcao-mo/u_d.png" can_reuse="true" />
|
||||
|
||||
<!-- up bond -->
|
||||
<draggable id="7" icon="/static/images/images_list/lcao-mo/up.png" can_reuse="true" />
|
||||
|
||||
<!-- sigma -->
|
||||
<draggable id="11" icon="/static/images/images_list/lcao-mo/sigma.png" can_reuse="true" />
|
||||
|
||||
<!-- sigma* -->
|
||||
<draggable id="13" icon="/static/images/images_list/lcao-mo/sigma_s.png" can_reuse="true" />
|
||||
|
||||
<!-- pi -->
|
||||
<draggable id="15" icon="/static/images/images_list/lcao-mo/pi.png" can_reuse="true" />
|
||||
|
||||
<!-- pi* -->
|
||||
<draggable id="16" icon="/static/images/images_list/lcao-mo/pi_s.png" can_reuse="true" />
|
||||
|
||||
<!-- images that should not be dragged -->
|
||||
<draggable id="17" icon="/static/images/images_list/lcao-mo/d.png" can_reuse="true" />
|
||||
|
||||
<!-- positions of electrons and electron pairs -->
|
||||
<target id="s_left" x="130" y="360" w="32" h="32"/>
|
||||
<target id="s_right" x="505" y="360" w="32" h="32"/>
|
||||
<target id="s_sigma" x="320" y="425" w="32" h="32"/>
|
||||
<target id="s_sigma_star" x="320" y="290" w="32" h="32"/>
|
||||
<target id="p_left_1" x="80" y="100" w="32" h="32"/>
|
||||
<target id="p_left_2" x="125" y="100" w="32" h="32"/>
|
||||
<target id="p_left_3" x="175" y="100" w="32" h="32"/>
|
||||
<target id="p_right_1" x="465" y="100" w="32" h="32"/>
|
||||
<target id="p_right_2" x="515" y="100" w="32" h="32"/>
|
||||
<target id="p_right_3" x="560" y="100" w="32" h="32"/>
|
||||
<target id="p_pi_1" x="290" y="220" w="32" h="32"/>
|
||||
<target id="p_pi_2" x="335" y="220" w="32" h="32"/>
|
||||
<target id="p_sigma" x="315" y="170" w="32" h="32"/>
|
||||
<target id="p_pi_star_1" x="290" y="40" w="32" h="32"/>
|
||||
<target id="p_pi_star_2" x="340" y="40" w="32" h="32"/>
|
||||
<target id="p_sigma_star" x="315" y="0" w="32" h="32"/>
|
||||
|
||||
<!-- positions of names of energy levels -->
|
||||
<target id="s_sigma_name" x="400" y="425" w="32" h="32"/>
|
||||
<target id="s_sigma_star_name" x="400" y="290" w="32" h="32"/>
|
||||
<target id="p_pi_name" x="400" y="220" w="32" h="32"/>
|
||||
<target id="p_sigma_name" x="400" y="170" w="32" h="32"/>
|
||||
<target id="p_pi_star_name" x="400" y="40" w="32" h="32"/>
|
||||
<target id="p_sigma_star_name" x="400" y="0" w="32" h="32"/>
|
||||
|
||||
</drag_and_drop_input>
|
||||
|
||||
<answer type="loncapa/python"><![CDATA[
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['1'],
|
||||
'targets': [
|
||||
's_left', 's_right', 's_sigma', 's_sigma_star', 'p_pi_1', 'p_pi_2'
|
||||
],
|
||||
'rule': 'exact'
|
||||
}, {
|
||||
'draggables': ['7'],
|
||||
'targets': ['p_left_1', 'p_left_2', 'p_right_2','p_right_3'],
|
||||
'rule': 'exact'
|
||||
}, {
|
||||
'draggables': ['11'],
|
||||
'targets': ['s_sigma_name', 'p_sigma_name'],
|
||||
'rule': 'exact'
|
||||
}, {
|
||||
'draggables': ['13'],
|
||||
'targets': ['s_sigma_star_name', 'p_sigma_star_name'],
|
||||
'rule': 'exact'
|
||||
}, {
|
||||
'draggables': ['15'],
|
||||
'targets': ['p_pi_name'],
|
||||
'rule': 'exact'
|
||||
}, {
|
||||
'draggables': ['16'],
|
||||
'targets': ['p_pi_star_name'],
|
||||
'rule': 'exact'
|
||||
}]
|
||||
|
||||
if draganddrop.grade(submission[0], correct_answer):
|
||||
correct = ['correct']
|
||||
else:
|
||||
correct = ['incorrect']
|
||||
]]></answer>
|
||||
</customresponse>
|
||||
|
||||
</problem>
|
||||
@@ -83,9 +83,58 @@ the slider.
|
||||
If no targets are provided, then a draggable can be dragged and placed anywhere
|
||||
on the base image.
|
||||
|
||||
correct answer format
|
||||
Targets on draggables
|
||||
---------------------
|
||||
|
||||
Sometimes it is not enough to have targets only on the base image, and all of the
|
||||
draggables on these targets. If a complex problem exists where a draggable must
|
||||
become itself a target (or many targets), then the following extended syntax
|
||||
can be used: ::
|
||||
|
||||
<draggable {attribute list}>
|
||||
<target {attribute list} />
|
||||
<target {attribute list} />
|
||||
<target {attribute list} />
|
||||
...
|
||||
</draggable>
|
||||
|
||||
The attribute list in the tags above ('draggable' and 'target') is the same as for
|
||||
normal 'draggable' and 'target' tags. The only difference is when you will be
|
||||
specifying inner target position coordinates. Using the 'x' and 'y' attributes you
|
||||
are setting the offset of the inner target from the upper-left corner of the
|
||||
parent draggable (that contains the inner target).
|
||||
|
||||
Limitations of targets on draggables
|
||||
------------------------------------
|
||||
|
||||
1.) Currently there is a limitation to the level of nesting of targets.
|
||||
|
||||
Even though you can pile up a large number of draggables on targets that themselves
|
||||
are on draggables, the Drag and Drop instance will be graded only in the case if
|
||||
there is a maximum of two levels of targets. The first level are the "base" targets.
|
||||
They are attached to the base image. The second level are the targets defined on
|
||||
draggables.
|
||||
|
||||
2.) Another limitation is that the target bounds are not checked against
|
||||
other targets.
|
||||
|
||||
For now, it is the responsibility of the person who is constructing the course
|
||||
material to make sure that there is no overlapping of targets. It is also preferable
|
||||
that targets on draggables are smaller than the actual parent draggable. Technically
|
||||
this is not necessary, but from the usability perspective it is desirable.
|
||||
|
||||
3.) You can have targets on draggables only in the case when there are base targets
|
||||
defined (base targets are attached to the base image).
|
||||
|
||||
If you do not have base targets, then you can only have a single level of nesting
|
||||
(draggables on the base image). In this case the client side will be reporting (x,y)
|
||||
positions of each draggables on the base image.
|
||||
|
||||
Correct answer format
|
||||
---------------------
|
||||
|
||||
(NOTE: For specifying answers for targets on draggables please see next section.)
|
||||
|
||||
There are two correct answer formats: short and long
|
||||
If short from correct answer is mapping of 'draggable_id' to 'target_id'::
|
||||
|
||||
@@ -180,7 +229,7 @@ Rules are: exact, anyof, unordered_equal, anyof+number, unordered_equal+number
|
||||
'rule': 'unordered_equal'
|
||||
}]
|
||||
|
||||
- And sometimes you want to allow drag only two 'b' draggables, in these case you sould use 'anyof+number' of 'unordered_equal+number' rule::
|
||||
- And sometimes you want to allow drag only two 'b' draggables, in these case you should use 'anyof+number' of 'unordered_equal+number' rule::
|
||||
|
||||
correct_answer = [
|
||||
{
|
||||
@@ -204,6 +253,54 @@ for same number of draggables, anyof is equal to unordered_equal
|
||||
|
||||
If we have can_reuse=true, than one must use only long form of correct answer.
|
||||
|
||||
Answer format for targets on draggables
|
||||
---------------------------------------
|
||||
|
||||
As with the cases described above, an answer must provide precise positioning for
|
||||
each draggable (on which targets it must reside). In the case when a draggable must
|
||||
be placed on a target that itself is on a draggable, then the answer must contain
|
||||
the chain of target-draggable-target. It is best to understand this on an example.
|
||||
|
||||
Suppose we have three draggables - 'up', 's', and 'p'. Draggables 's', and 'p' have targets
|
||||
on themselves. More specifically, 'p' has three targets - '1', '2', and '3'. The first
|
||||
requirement is that 's', and 'p' are positioned on specific targets on the base image.
|
||||
The second requirement is that draggable 'up' is positioned on specific targets of
|
||||
draggable 'p'. Below is an excerpt from a problem.::
|
||||
|
||||
<draggable id="up" icon="/static/images/images_list/lcao-mo/up.png" can_reuse="true" />
|
||||
|
||||
<draggable id="s" icon="/static/images/images_list/lcao-mo/orbital_single.png" label="s orbital" can_reuse="true" >
|
||||
<target id="1" x="0" y="0" w="32" h="32"/>
|
||||
</draggable>
|
||||
|
||||
<draggable id="p" icon="/static/images/images_list/lcao-mo/orbital_triple.png" can_reuse="true" label="p orbital" >
|
||||
<target id="1" x="0" y="0" w="32" h="32"/>
|
||||
<target id="2" x="34" y="0" w="32" h="32"/>
|
||||
<target id="3" x="68" y="0" w="32" h="32"/>
|
||||
</draggable>
|
||||
|
||||
...
|
||||
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['p'],
|
||||
'targets': ['p-left-target', 'p-right-target'],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['s'],
|
||||
'targets': ['s-left-target', 's-right-target'],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['up'],
|
||||
'targets': ['p-left-target[p][1]', 'p-left-target[p][2]', 'p-right-target[p][2]', 'p-right-target[p][3]',],
|
||||
'rule': 'unordered_equal'
|
||||
}
|
||||
]
|
||||
|
||||
Note that it is a requirement to specify rules for all draggables, even if some draggable gets included
|
||||
in more than one chain.
|
||||
|
||||
Grading logic
|
||||
-------------
|
||||
@@ -321,3 +418,8 @@ Draggables can be reused
|
||||
------------------------
|
||||
|
||||
.. literalinclude:: drag-n-drop-demo2.xml
|
||||
|
||||
Examples of targets on draggables
|
||||
------------------------
|
||||
|
||||
.. literalinclude:: drag-n-drop-demo3.xml
|
||||
|
||||
@@ -3,4 +3,3 @@
|
||||
-e git://github.com/MITx/django-pipeline.git#egg=django-pipeline
|
||||
-e git://github.com/MITx/django-wiki.git@e2e84558#egg=django-wiki
|
||||
-e git://github.com/dementrock/pystache_custom.git@776973740bdaad83a3b029f96e415a7d1e8bec2f#egg=pystache_custom-dev
|
||||
-e git://github.com/MITx/dogapi.git@003a4fc9#egg=dogapi
|
||||
|
||||
@@ -16,7 +16,6 @@ from django.views.decorators.csrf import csrf_exempt
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
from capa.xqueue_interface import XQueueInterface
|
||||
from capa.chem import chemcalc
|
||||
from courseware.access import has_access
|
||||
from mitxmako.shortcuts import render_to_string
|
||||
from models import StudentModule
|
||||
@@ -446,42 +445,6 @@ def modx_dispatch(request, dispatch, location, course_id):
|
||||
return HttpResponse(ajax_return)
|
||||
|
||||
|
||||
def preview_chemcalc(request):
|
||||
"""
|
||||
Render an html preview of a chemical formula or equation. The fact that
|
||||
this is here is a bit of hack. See the note in lms/urls.py about why it's
|
||||
here. (Victor is to blame.)
|
||||
|
||||
request should be a GET, with a key 'formula' and value 'some formula string'.
|
||||
|
||||
Returns a json dictionary:
|
||||
{
|
||||
'preview' : 'the-preview-html' or ''
|
||||
'error' : 'the-error' or ''
|
||||
}
|
||||
"""
|
||||
if request.method != "GET":
|
||||
raise Http404
|
||||
|
||||
result = {'preview': '',
|
||||
'error': ''}
|
||||
formula = request.GET.get('formula')
|
||||
if formula is None:
|
||||
result['error'] = "No formula specified."
|
||||
|
||||
return HttpResponse(json.dumps(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 HttpResponse(json.dumps(result))
|
||||
|
||||
|
||||
def get_score_bucket(grade, max_grade):
|
||||
"""
|
||||
|
||||
259
lms/djangoapps/courseware/tests/test_tabs.py
Normal file
259
lms/djangoapps/courseware/tests/test_tabs.py
Normal file
@@ -0,0 +1,259 @@
|
||||
from django.test import TestCase
|
||||
from mock import MagicMock
|
||||
|
||||
import courseware.tabs as tabs
|
||||
|
||||
from django.test.utils import override_settings
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
|
||||
class ProgressTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.mockuser1 = MagicMock()
|
||||
self.mockuser0 = MagicMock()
|
||||
self.course = MagicMock()
|
||||
self.mockuser1.is_authenticated.return_value = True
|
||||
self.mockuser0.is_authenticated.return_value = False
|
||||
self.course.id = 'edX/full/6.002_Spring_2012'
|
||||
self.tab = {'name': 'same'}
|
||||
self.active_page1 = 'progress'
|
||||
self.active_page0 = 'stagnation'
|
||||
|
||||
def test_progress(self):
|
||||
|
||||
self.assertEqual(tabs._progress(self.tab, self.mockuser0, self.course,
|
||||
self.active_page0), [])
|
||||
|
||||
self.assertEqual(tabs._progress(self.tab, self.mockuser1, self.course,
|
||||
self.active_page1)[0].name, 'same')
|
||||
|
||||
self.assertEqual(tabs._progress(self.tab, self.mockuser1, self.course,
|
||||
self.active_page1)[0].link,
|
||||
reverse('progress', args = [self.course.id]))
|
||||
|
||||
self.assertEqual(tabs._progress(self.tab, self.mockuser1, self.course,
|
||||
self.active_page0)[0].is_active, False)
|
||||
|
||||
self.assertEqual(tabs._progress(self.tab, self.mockuser1, self.course,
|
||||
self.active_page1)[0].is_active, True)
|
||||
|
||||
|
||||
class WikiTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.user = MagicMock()
|
||||
self.course = MagicMock()
|
||||
self.course.id = 'edX/full/6.002_Spring_2012'
|
||||
self.tab = {'name': 'same'}
|
||||
self.active_page1 = 'wiki'
|
||||
self.active_page0 = 'miki'
|
||||
|
||||
@override_settings(WIKI_ENABLED=True)
|
||||
def test_wiki_enabled(self):
|
||||
|
||||
self.assertEqual(tabs._wiki(self.tab, self.user,
|
||||
self.course, self.active_page1)[0].name,
|
||||
'same')
|
||||
|
||||
self.assertEqual(tabs._wiki(self.tab, self.user,
|
||||
self.course, self.active_page1)[0].link,
|
||||
reverse('course_wiki', args=[self.course.id]))
|
||||
|
||||
self.assertEqual(tabs._wiki(self.tab, self.user,
|
||||
self.course, self.active_page1)[0].is_active,
|
||||
True)
|
||||
|
||||
self.assertEqual(tabs._wiki(self.tab, self.user,
|
||||
self.course, self.active_page0)[0].is_active,
|
||||
False)
|
||||
|
||||
@override_settings(WIKI_ENABLED=False)
|
||||
def test_wiki_enabled_false(self):
|
||||
|
||||
self.assertEqual(tabs._wiki(self.tab, self.user,
|
||||
self.course, self.active_page1), [])
|
||||
|
||||
|
||||
class ExternalLinkTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.user = MagicMock()
|
||||
self.course = MagicMock()
|
||||
self.tabby = {'name': 'same', 'link': 'blink'}
|
||||
self.active_page0 = None
|
||||
self.active_page00 = True
|
||||
|
||||
def test_external_link(self):
|
||||
|
||||
self.assertEqual(tabs._external_link(self.tabby, self.user,
|
||||
self.course, self.active_page0)[0].name,
|
||||
'same')
|
||||
|
||||
self.assertEqual(tabs._external_link(self.tabby, self.user,
|
||||
self.course, self.active_page0)[0].link,
|
||||
'blink')
|
||||
|
||||
self.assertEqual(tabs._external_link(self.tabby, self.user,
|
||||
self.course, self.active_page0)[0].is_active,
|
||||
False)
|
||||
|
||||
self.assertEqual(tabs._external_link(self.tabby, self.user,
|
||||
self.course, self.active_page00)[0].is_active,
|
||||
False)
|
||||
|
||||
|
||||
class StaticTabTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.user = MagicMock()
|
||||
self.course = MagicMock()
|
||||
self.tabby = {'name': 'same', 'url_slug': 'schmug'}
|
||||
self.course.id = 'edX/full/6.002_Spring_2012'
|
||||
self.active_page1 = 'static_tab_schmug'
|
||||
self.active_page0 = 'static_tab_schlug'
|
||||
|
||||
def test_static_tab(self):
|
||||
|
||||
self.assertEqual(tabs._static_tab(self.tabby, self.user,
|
||||
self.course, self.active_page1)[0].name,
|
||||
'same')
|
||||
|
||||
self.assertEqual(tabs._static_tab(self.tabby, self.user,
|
||||
self.course, self.active_page1)[0].link,
|
||||
reverse('static_tab', args = [self.course.id,
|
||||
self.tabby['url_slug']]))
|
||||
|
||||
self.assertEqual(tabs._static_tab(self.tabby, self.user,
|
||||
self.course, self.active_page1)[0].is_active,
|
||||
True)
|
||||
|
||||
|
||||
self.assertEqual(tabs._static_tab(self.tabby, self.user,
|
||||
self.course, self.active_page0)[0].is_active,
|
||||
False)
|
||||
|
||||
|
||||
class TextbooksTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.mockuser1 = MagicMock()
|
||||
self.mockuser0 = MagicMock()
|
||||
self.course = MagicMock()
|
||||
self.tab = MagicMock()
|
||||
A = MagicMock()
|
||||
T = MagicMock()
|
||||
self.mockuser1.is_authenticated.return_value = True
|
||||
self.mockuser0.is_authenticated.return_value = False
|
||||
self.course.id = 'edX/full/6.002_Spring_2012'
|
||||
self.active_page0 = 'textbook/0'
|
||||
self.active_page1 = 'textbook/1'
|
||||
self.active_pageX = 'you_shouldnt_be_seein_this'
|
||||
A.title = 'Algebra'
|
||||
T.title = 'Topology'
|
||||
self.course.textbooks = [A, T]
|
||||
|
||||
@override_settings(MITX_FEATURES={'ENABLE_TEXTBOOK': True})
|
||||
def test_textbooks1(self):
|
||||
|
||||
self.assertEqual(tabs._textbooks(self.tab, self.mockuser1,
|
||||
self.course, self.active_page0)[0].name,
|
||||
'Algebra')
|
||||
|
||||
self.assertEqual(tabs._textbooks(self.tab, self.mockuser1,
|
||||
self.course, self.active_page0)[0].link,
|
||||
reverse('book', args=[self.course.id, 0]))
|
||||
|
||||
self.assertEqual(tabs._textbooks(self.tab, self.mockuser1,
|
||||
self.course, self.active_page0)[0].is_active,
|
||||
True)
|
||||
|
||||
self.assertEqual(tabs._textbooks(self.tab, self.mockuser1,
|
||||
self.course, self.active_pageX)[0].is_active,
|
||||
False)
|
||||
|
||||
self.assertEqual(tabs._textbooks(self.tab, self.mockuser1,
|
||||
self.course, self.active_page1)[1].name,
|
||||
'Topology')
|
||||
|
||||
self.assertEqual(tabs._textbooks(self.tab, self.mockuser1,
|
||||
self.course, self.active_page1)[1].link,
|
||||
reverse('book', args=[self.course.id, 1]))
|
||||
|
||||
self.assertEqual(tabs._textbooks(self.tab, self.mockuser1,
|
||||
self.course, self.active_page1)[1].is_active,
|
||||
True)
|
||||
|
||||
self.assertEqual(tabs._textbooks(self.tab, self.mockuser1,
|
||||
self.course, self.active_pageX)[1].is_active,
|
||||
False)
|
||||
|
||||
@override_settings(MITX_FEATURES={'ENABLE_TEXTBOOK': False})
|
||||
def test_textbooks0(self):
|
||||
|
||||
self.assertEqual(tabs._textbooks(self.tab, self.mockuser1,
|
||||
self.course, self.active_pageX), [])
|
||||
|
||||
self.assertEqual(tabs._textbooks(self.tab, self.mockuser0,
|
||||
self.course, self.active_pageX), [])
|
||||
|
||||
class KeyCheckerTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.expected_keys1 = ['a', 'b']
|
||||
self.expected_keys0 = ['a', 'v', 'g']
|
||||
self.dictio = {'a': 1, 'b': 2, 'c': 3}
|
||||
|
||||
def test_key_checker(self):
|
||||
|
||||
self.assertIsNone(tabs.key_checker(self.expected_keys1)(self.dictio))
|
||||
self.assertRaises(tabs.InvalidTabsException,
|
||||
tabs.key_checker(self.expected_keys0), self.dictio)
|
||||
|
||||
|
||||
class NullValidatorTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.d = {}
|
||||
|
||||
def test_null_validator(self):
|
||||
|
||||
self.assertIsNone(tabs.null_validator(self.d))
|
||||
|
||||
|
||||
class ValidateTabsTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.courses = [MagicMock() for i in range(0,5)]
|
||||
|
||||
self.courses[0].tabs = None
|
||||
|
||||
self.courses[1].tabs = [{'type':'courseware'}, {'type': 'fax'}]
|
||||
|
||||
self.courses[2].tabs = [{'type':'shadow'}, {'type': 'course_info'}]
|
||||
|
||||
self.courses[3].tabs = [{'type':'courseware'},{'type':'course_info', 'name': 'alice'},
|
||||
{'type': 'wiki', 'name':'alice'}, {'type':'discussion', 'name': 'alice'},
|
||||
{'type':'external_link', 'name': 'alice', 'link':'blink'},
|
||||
{'type':'textbooks'}, {'type':'progress', 'name': 'alice'},
|
||||
{'type':'static_tab', 'name':'alice', 'url_slug':'schlug'},
|
||||
{'type': 'staff_grading'}]
|
||||
|
||||
self.courses[4].tabs = [{'type':'courseware'},{'type': 'course_info'}, {'type': 'flying'}]
|
||||
|
||||
|
||||
def test_validate_tabs(self):
|
||||
|
||||
self.assertIsNone(tabs.validate_tabs(self.courses[0]))
|
||||
self.assertRaises(tabs.InvalidTabsException, tabs.validate_tabs, self.courses[1])
|
||||
self.assertRaises(tabs.InvalidTabsException, tabs.validate_tabs, self.courses[2])
|
||||
self.assertIsNone(tabs.validate_tabs(self.courses[3]))
|
||||
self.assertRaises(tabs.InvalidTabsException, tabs.validate_tabs, self.courses[4])
|
||||
55
lms/djangoapps/django_comment_client/tests/test_models.py
Normal file
55
lms/djangoapps/django_comment_client/tests/test_models.py
Normal file
@@ -0,0 +1,55 @@
|
||||
import django_comment_client.models as models
|
||||
import django_comment_client.permissions as permissions
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class RoleClassTestCase(TestCase):
|
||||
def setUp(self):
|
||||
# For course ID, syntax edx/classname/classdate is important
|
||||
# because xmodel.course_module.id_to_location looks for a string to split
|
||||
|
||||
self.course_id = "edX/toy/2012_Fall"
|
||||
self.student_role = models.Role.objects.get_or_create(name="Student", \
|
||||
course_id=self.course_id)[0]
|
||||
self.student_role.add_permission("delete_thread")
|
||||
self.student_2_role = models.Role.objects.get_or_create(name="Student", \
|
||||
course_id=self.course_id)[0]
|
||||
self.TA_role = models.Role.objects.get_or_create(name="Community TA",\
|
||||
course_id=self.course_id)[0]
|
||||
self.course_id_2 = "edx/6.002x/2012_Fall"
|
||||
self.TA_role_2 = models.Role.objects.get_or_create(name="Community TA",\
|
||||
course_id=self.course_id_2)[0]
|
||||
class Dummy():
|
||||
def render_template():
|
||||
pass
|
||||
d = {"data": {
|
||||
"textbooks": [],
|
||||
'wiki_slug': True,
|
||||
}
|
||||
}
|
||||
|
||||
def testHasPermission(self):
|
||||
# Whenever you add a permission to student_role,
|
||||
# Roles with the same FORUM_ROLE in same class also receives the same
|
||||
# permission.
|
||||
# Is this desirable behavior?
|
||||
self.assertTrue(self.student_role.has_permission("delete_thread"))
|
||||
self.assertTrue(self.student_2_role.has_permission("delete_thread"))
|
||||
self.assertFalse(self.TA_role.has_permission("delete_thread"))
|
||||
|
||||
def testInheritPermissions(self):
|
||||
|
||||
self.TA_role.inherit_permissions(self.student_role)
|
||||
self.assertTrue(self.TA_role.has_permission("delete_thread"))
|
||||
# Despite being from 2 different courses, TA_role_2 can still inherit
|
||||
# permissions from TA_role without error
|
||||
self.TA_role_2.inherit_permissions(self.TA_role)
|
||||
|
||||
|
||||
class PermissionClassTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.permission = permissions.Permission.objects.get_or_create(name="test")[0]
|
||||
|
||||
def testUnicode(self):
|
||||
self.assertEqual(str(self.permission), "test")
|
||||
@@ -3,26 +3,40 @@ import random
|
||||
import collections
|
||||
|
||||
from django.test import TestCase
|
||||
from mock import MagicMock
|
||||
from django.test.utils import override_settings
|
||||
import django.core.urlresolvers as urlresolvers
|
||||
|
||||
import django_comment_client.mustache_helpers as mustache_helpers
|
||||
|
||||
|
||||
class PluralizeTestCase(TestCase):
|
||||
|
||||
def test_pluralize(self):
|
||||
self.text1 = '0 goat'
|
||||
self.text2 = '1 goat'
|
||||
self.text3 = '7 goat'
|
||||
self.content = 'unused argument'
|
||||
self.assertEqual(mustache_helpers.pluralize(self.content, self.text1), 'goats')
|
||||
self.assertEqual(mustache_helpers.pluralize(self.content, self.text2), 'goat')
|
||||
self.assertEqual(mustache_helpers.pluralize(self.content, self.text3), 'goats')
|
||||
#########################################################################################
|
||||
|
||||
|
||||
class CloseThreadTextTestCase(TestCase):
|
||||
class PluralizeTest(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.text1 = '0 goat'
|
||||
self.text2 = '1 goat'
|
||||
self.text3 = '7 goat'
|
||||
self.content = 'unused argument'
|
||||
|
||||
def test_pluralize(self):
|
||||
self.assertEqual(mustache_helpers.pluralize(self.content, self.text1), 'goats')
|
||||
self.assertEqual(mustache_helpers.pluralize(self.content, self.text2), 'goat')
|
||||
self.assertEqual(mustache_helpers.pluralize(self.content, self.text3), 'goats')
|
||||
|
||||
#########################################################################################
|
||||
|
||||
|
||||
class CloseThreadTextTest(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.contentClosed = {'closed': True}
|
||||
self.contentOpen = {'closed': False}
|
||||
|
||||
def test_close_thread_text(self):
|
||||
self.assertEqual(mustache_helpers.close_thread_text(self.contentClosed), 'Re-open thread')
|
||||
self.assertEqual(mustache_helpers.close_thread_text(self.contentOpen), 'Close thread')
|
||||
|
||||
#########################################################################################
|
||||
|
||||
def test_close_thread_text(self):
|
||||
self.contentClosed = {'closed': True}
|
||||
self.contentOpen = {'closed': False}
|
||||
self.assertEqual(mustache_helpers.close_thread_text(self.contentClosed), 'Re-open thread')
|
||||
self.assertEqual(mustache_helpers.close_thread_text(self.contentOpen), 'Close thread')
|
||||
|
||||
@@ -195,7 +195,7 @@ def initialize_discussion_info(course):
|
||||
sort_key = module.sort_key
|
||||
category = " / ".join([x.strip() for x in category.split("/")])
|
||||
last_category = category.split("/")[-1]
|
||||
discussion_id_map[id] = {"location": location, "title": last_category + " / " + title}
|
||||
discussion_id_map[id] = {"location": module.location, "title": last_category + " / " + title}
|
||||
unexpanded_category_map[category].append({"title": title, "id": id,
|
||||
"sort_key": sort_key, "start_date": module.lms.start})
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ def save_scores(user, puzzle_scores):
|
||||
current_score=current_score,
|
||||
best_score=best_score,
|
||||
score_version=score_version)
|
||||
obj.save()
|
||||
obj.save()
|
||||
|
||||
score_responses.append({'PuzzleID': puzzle_id,
|
||||
'Status': 'Success'})
|
||||
|
||||
6
lms/one_time_startup.py
Normal file
6
lms/one_time_startup.py
Normal 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)
|
||||
29
lms/templates/annotatable.html
Normal file
29
lms/templates/annotatable.html
Normal file
@@ -0,0 +1,29 @@
|
||||
<div class="annotatable-wrapper">
|
||||
<div class="annotatable-header">
|
||||
% if display_name is not UNDEFINED and display_name is not None:
|
||||
<div class="annotatable-title">${display_name}</div>
|
||||
% endif
|
||||
</div>
|
||||
|
||||
% if instructions_html is not UNDEFINED and instructions_html is not None:
|
||||
<div class="annotatable-section shaded">
|
||||
<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">
|
||||
${instructions_html}
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
<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 class="annotatable-section-body annotatable-content">
|
||||
${content_html}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -57,9 +57,9 @@ function goto( mode)
|
||||
<section class="instructor-dashboard-content">
|
||||
<h1>Instructor Dashboard</h1>
|
||||
|
||||
<h2>[ <a href="#" onclick="goto('Grades');" class="${modeflag.get('Grades')}">Grades</a> |
|
||||
<h2>[ <a href="#" onclick="goto('Grades');" class="${modeflag.get('Grades')}">Grades</a> |
|
||||
%if settings.MITX_FEATURES.get('ENABLE_PSYCHOMETRICS'):
|
||||
<a href="#" onclick="goto('Psychometrics');" class="${modeflag.get('Psychometrics')}">Psychometrics</a> |
|
||||
<a href="#" onclick="goto('Psychometrics');" class="${modeflag.get('Psychometrics')}">Psychometrics</a> |
|
||||
%endif
|
||||
<a href="#" onclick="goto('Admin');" class="${modeflag.get('Admin')}">Admin</a> |
|
||||
<a href="#" onclick="goto('Forum Admin');" class="${modeflag.get('Forum Admin')}">Forum Admin</a> |
|
||||
@@ -75,7 +75,7 @@ function goto( mode)
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="${ csrf_token }">
|
||||
<input type="hidden" name="idash_mode" value="">
|
||||
|
||||
##-----------------------------------------------------------------------------
|
||||
##-----------------------------------------------------------------------------
|
||||
%if modeflag.get('Grades'):
|
||||
|
||||
%if offline_grade_log:
|
||||
@@ -111,9 +111,9 @@ function goto( mode)
|
||||
<hr width="40%" style="align:left">
|
||||
|
||||
%if settings.MITX_FEATURES.get('REMOTE_GRADEBOOK_URL','') and instructor_access:
|
||||
|
||||
|
||||
<%
|
||||
rg = course.metadata.get('remote_gradebook',{})
|
||||
rg = course.remote_gradebook
|
||||
%>
|
||||
|
||||
<h3>Export grades to remote gradebook</h3>
|
||||
@@ -157,7 +157,7 @@ function goto( mode)
|
||||
|
||||
%endif
|
||||
|
||||
##-----------------------------------------------------------------------------
|
||||
##-----------------------------------------------------------------------------
|
||||
%if modeflag.get('Psychometrics'):
|
||||
|
||||
<p>Select a problem and an action:
|
||||
@@ -178,7 +178,7 @@ function goto( mode)
|
||||
|
||||
%endif
|
||||
|
||||
##-----------------------------------------------------------------------------
|
||||
##-----------------------------------------------------------------------------
|
||||
%if modeflag.get('Admin'):
|
||||
%if instructor_access:
|
||||
<hr width="40%" style="align:left">
|
||||
@@ -208,7 +208,7 @@ function goto( mode)
|
||||
%endif
|
||||
%endif
|
||||
|
||||
##-----------------------------------------------------------------------------
|
||||
##-----------------------------------------------------------------------------
|
||||
%if modeflag.get('Forum Admin'):
|
||||
%if instructor_access:
|
||||
<hr width="40%" style="align:left">
|
||||
@@ -225,7 +225,7 @@ function goto( mode)
|
||||
<input type="submit" name="action" value="List course forum moderators">
|
||||
<input type="submit" name="action" value="List course forum community TAs">
|
||||
<p>
|
||||
<input type="text" name="forummoderator">
|
||||
<input type="text" name="forummoderator">
|
||||
<input type="submit" name="action" value="Remove forum moderator">
|
||||
<input type="submit" name="action" value="Add forum moderator">
|
||||
<input type="submit" name="action" value="Remove forum community TA">
|
||||
@@ -236,7 +236,7 @@ function goto( mode)
|
||||
%endif
|
||||
%endif
|
||||
|
||||
##-----------------------------------------------------------------------------
|
||||
##-----------------------------------------------------------------------------
|
||||
%if modeflag.get('Enrollment'):
|
||||
|
||||
<hr width="40%" style="align:left">
|
||||
@@ -249,9 +249,9 @@ function goto( mode)
|
||||
<hr width="40%" style="align:left">
|
||||
|
||||
%if settings.MITX_FEATURES.get('REMOTE_GRADEBOOK_URL','') and instructor_access:
|
||||
|
||||
|
||||
<%
|
||||
rg = course.metadata.get('remote_gradebook',{})
|
||||
rg = course.remote_gradebook
|
||||
%>
|
||||
|
||||
<p>Pull enrollment from remote gradebook</p>
|
||||
@@ -264,7 +264,7 @@ function goto( mode)
|
||||
<input type="submit" name="action" value="Overload enrollment list using remote gradebook">
|
||||
<input type="submit" name="action" value="Merge enrollment list with remote gradebook">
|
||||
<hr width="40%" style="align:left">
|
||||
|
||||
|
||||
%endif
|
||||
|
||||
<p>Add students: enter emails, separated by new lines or commas;</p>
|
||||
@@ -273,21 +273,21 @@ function goto( mode)
|
||||
|
||||
%endif
|
||||
|
||||
##-----------------------------------------------------------------------------
|
||||
##-----------------------------------------------------------------------------
|
||||
|
||||
%if modeflag.get('Data'):
|
||||
<hr width="40%" style="align:left">
|
||||
<p>
|
||||
<input type="submit" name="action" value="Download CSV of all student profile data">
|
||||
</p>
|
||||
<p> Problem urlname:
|
||||
<p> Problem urlname:
|
||||
<input type="text" name="problem_to_dump" size="40">
|
||||
<input type="submit" name="action" value="Download CSV of all responses to problem">
|
||||
</p>
|
||||
<hr width="40%" style="align:left">
|
||||
%endif
|
||||
|
||||
##-----------------------------------------------------------------------------
|
||||
|
||||
##-----------------------------------------------------------------------------
|
||||
|
||||
%if modeflag.get('Manage Groups'):
|
||||
%if instructor_access:
|
||||
@@ -313,12 +313,12 @@ function goto( mode)
|
||||
%endif
|
||||
|
||||
</form>
|
||||
##-----------------------------------------------------------------------------
|
||||
##-----------------------------------------------------------------------------
|
||||
|
||||
%if msg:
|
||||
<p></p><p>${msg}</p>
|
||||
%endif
|
||||
##-----------------------------------------------------------------------------
|
||||
##-----------------------------------------------------------------------------
|
||||
|
||||
%if datatable and modeflag.get('Psychometrics') is None:
|
||||
|
||||
@@ -344,7 +344,7 @@ function goto( mode)
|
||||
</p>
|
||||
%endif
|
||||
|
||||
##-----------------------------------------------------------------------------
|
||||
##-----------------------------------------------------------------------------
|
||||
%if modeflag.get('Psychometrics'):
|
||||
|
||||
%for plot in plots:
|
||||
@@ -365,12 +365,12 @@ function goto( mode)
|
||||
%endfor
|
||||
|
||||
%endif
|
||||
|
||||
##-----------------------------------------------------------------------------
|
||||
|
||||
##-----------------------------------------------------------------------------
|
||||
## always show msg
|
||||
|
||||
|
||||
##-----------------------------------------------------------------------------
|
||||
##-----------------------------------------------------------------------------
|
||||
%if modeflag.get('Admin'):
|
||||
% if course_errors is not UNDEFINED:
|
||||
<h2>Course errors</h2>
|
||||
@@ -392,7 +392,7 @@ function goto( mode)
|
||||
%endif
|
||||
</div>
|
||||
% endif
|
||||
%endif
|
||||
%endif
|
||||
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -7,15 +7,15 @@
|
||||
<div class="options">
|
||||
<input type="checkbox" name="follow" class="discussion-follow" class="discussion-follow" id="new-post-follow" checked><label for="new-post-follow">follow this post</label>
|
||||
<br>
|
||||
% if course.metadata.get("allow_anonymous", True):
|
||||
% if course.allow_anonymous:
|
||||
<input type="checkbox" name="anonymous" class="discussion-anonymous" id="new-post-anonymous"><label for="new-post-anonymous">post anonymously</label>
|
||||
%elif course.metadata.get("allow_anonymous_to_peers", False):
|
||||
%elif course.allow_anonymous_to_peers:
|
||||
<input type="checkbox" name="anonymous_to_peers" class="discussion-anonymous-to-peers" id="new-post-anonymous-to-peers"><label for="new-post-anonymous-to-peers">post anonymously to classmates</label>
|
||||
%endif
|
||||
%if is_course_cohorted:
|
||||
<div class="form-group-label choose-cohort">
|
||||
Make visible to:
|
||||
<select class="group-filter-select new-post-group" name = "group_id">
|
||||
<select class="group-filter-select new-post-group" name = "group_id">
|
||||
<option value="">All Groups</option>
|
||||
%if is_moderator:
|
||||
%for c in cohorts:
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
|
||||
<article class="new-post-article">
|
||||
<div class="inner-wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<form class="new-post-form">
|
||||
<div class="left-column">
|
||||
<label>Create new post about:</label>
|
||||
@@ -41,18 +41,18 @@
|
||||
<div class="options">
|
||||
<input type="checkbox" name="follow" class="discussion-follow" class="discussion-follow" id="new-post-follow" checked><label for="new-post-follow">follow this post</label>
|
||||
<br>
|
||||
% if course.metadata.get("allow_anonymous", True):
|
||||
% if course.allow_anonymous:
|
||||
<input type="checkbox" name="anonymous" class="discussion-anonymous" id="new-post-anonymous"><label for="new-post-anonymous">post anonymously</label>
|
||||
%elif course.metadata.get("allow_anonymous_to_peers", False):
|
||||
%elif course.allow_anonymous_to_peers:
|
||||
<input type="checkbox" name="anonymous_to_peers" class="discussion-anonymous-to-peers" id="new-post-anonymous-to-peers"><label for="new-post-anonymous-to-peers">post anonymously to classmates</label>
|
||||
%endif
|
||||
%if is_course_cohorted and is_moderator:
|
||||
<div class="form-group-label choose-cohort">
|
||||
Make visible to:
|
||||
<select class="group-filter-select new-post-group" name = "group_id">
|
||||
<select class="group-filter-select new-post-group" name = "group_id">
|
||||
<option value="">All Groups</option>
|
||||
%for c in cohorts:
|
||||
<option value="${c.id}"
|
||||
<option value="${c.id}"
|
||||
%if user_cohort and str(user_cohort) == str(c.id):
|
||||
selected
|
||||
%endif
|
||||
|
||||
@@ -92,6 +92,7 @@ category = ${category | h}
|
||||
|
||||
<script type="text/javascript">
|
||||
// assumes courseware.html's loaded this method.
|
||||
$(function () {
|
||||
setup_debug('${element_id}',
|
||||
%if edit_link:
|
||||
'${edit_link}',
|
||||
|
||||
11
lms/urls.py
11
lms/urls.py
@@ -3,6 +3,9 @@ from django.conf.urls import patterns, include, url
|
||||
from django.contrib import admin
|
||||
from django.conf.urls.static import static
|
||||
from django.views.generic import RedirectView
|
||||
|
||||
from . import one_time_startup
|
||||
|
||||
import django.contrib.auth.views
|
||||
|
||||
# Uncomment the next two lines to enable the admin:
|
||||
@@ -224,14 +227,6 @@ if settings.COURSEWARE_ENABLED:
|
||||
'courseware.module_render.modx_dispatch',
|
||||
name='modx_dispatch'),
|
||||
|
||||
# TODO (vshnayder): This is a hack. It creates a direct connection from
|
||||
# the LMS to capa functionality, and really wants to go through the
|
||||
# input types system so that previews can be context-specific.
|
||||
# Unfortunately, we don't have time to think through the right way to do
|
||||
# that (and implement it), and it's not a terrible thing to provide a
|
||||
# generic chemical-equation rendering service.
|
||||
url(r'^preview/chemcalc', 'courseware.module_render.preview_chemcalc',
|
||||
name='preview_chemcalc'),
|
||||
|
||||
# Software Licenses
|
||||
|
||||
|
||||
@@ -58,3 +58,4 @@ ipython==0.13.1
|
||||
xmltodict==0.4.1
|
||||
paramiko==1.9.0
|
||||
Pillow==1.7.8
|
||||
dogapi==1.2.1
|
||||
|
||||
Reference in New Issue
Block a user