Commit 49ee903e by cahrens

Merge branch 'master' into feature/btalbot/studio-checklists

Conflicts:
	cms/djangoapps/contentstore/views.py
	cms/static/js/template_loader.js
parents f6fb64f1 c50bf567
......@@ -29,4 +29,5 @@ cover_html/
.idea/
.redcar/
chromedriver.log
/nbproject
ghostdriver.log
......@@ -36,8 +36,12 @@ load-plugins=
disable=
# W0141: Used builtin function 'map'
# W0142: Used * or ** magic
# R0201: Method could be a function
# R0901: Too many ancestors
# R0902: Too many instance attributes
# R0903: Too few public methods (1/2)
W0141,W0142,R0903
# R0904: Too many public methods
W0141,W0142,R0201,R0901,R0902,R0903,R0904
[REPORTS]
......
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from lxml import html
from lxml import html, etree
import re
from django.http import HttpResponseBadRequest
import logging
......@@ -26,9 +26,9 @@ def get_course_updates(location):
# purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
try:
course_html_parsed = html.fromstring(course_updates.definition['data'])
except:
course_html_parsed = html.fromstring("<ol></ol>")
course_html_parsed = etree.fromstring(course_updates.data)
except etree.XMLSyntaxError:
course_html_parsed = etree.fromstring("<ol></ol>")
# Confirm that root is <ol>, iterate over <li>, pull out <h2> subs and then rest of val
course_upd_collection = []
......@@ -60,13 +60,13 @@ def update_course_updates(location, update, passed_id=None):
try:
course_updates = modulestore('direct').get_item(location)
except ItemNotFoundError:
return HttpResponseBadRequest
return HttpResponseBadRequest()
# purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
try:
course_html_parsed = html.fromstring(course_updates.definition['data'])
except:
course_html_parsed = html.fromstring("<ol></ol>")
course_html_parsed = etree.fromstring(course_updates.data)
except etree.XMLSyntaxError:
course_html_parsed = etree.fromstring("<ol></ol>")
# No try/catch b/c failure generates an error back to client
new_html_parsed = html.fromstring('<li><h2>' + update['date'] + '</h2>' + update['content'] + '</li>')
......@@ -85,13 +85,12 @@ def update_course_updates(location, update, passed_id=None):
passed_id = course_updates.location.url() + "/" + str(idx)
# update db record
course_updates.definition['data'] = html.tostring(course_html_parsed)
modulestore('direct').update_item(location, course_updates.definition['data'])
return {"id": passed_id,
"date": update['date'],
"content": update['content']}
course_updates.data = etree.tostring(course_html_parsed)
modulestore('direct').update_item(location, course_updates.data)
return {"id" : passed_id,
"date" : update['date'],
"content" :update['content']}
def delete_course_update(location, update, passed_id):
"""
......@@ -99,19 +98,19 @@ def delete_course_update(location, update, passed_id):
Returns the resulting course_updates b/c their ids change.
"""
if not passed_id:
return HttpResponseBadRequest
return HttpResponseBadRequest()
try:
course_updates = modulestore('direct').get_item(location)
except ItemNotFoundError:
return HttpResponseBadRequest
return HttpResponseBadRequest()
# TODO use delete_blank_text parser throughout and cache as a static var in a class
# purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
try:
course_html_parsed = html.fromstring(course_updates.definition['data'])
except:
course_html_parsed = html.fromstring("<ol></ol>")
course_html_parsed = etree.fromstring(course_updates.data)
except etree.XMLSyntaxError:
course_html_parsed = etree.fromstring("<ol></ol>")
if course_html_parsed.tag == 'ol':
# ??? Should this use the id in the json or in the url or does it matter?
......@@ -122,9 +121,9 @@ def delete_course_update(location, update, passed_id):
course_html_parsed.remove(element_to_delete)
# update db record
course_updates.definition['data'] = html.tostring(course_html_parsed)
course_updates.data = etree.tostring(course_html_parsed)
store = modulestore('direct')
store.update_item(location, course_updates.definition['data'])
store.update_item(location, course_updates.data)
return get_course_updates(location)
......
......@@ -2,53 +2,41 @@ Feature: Advanced (manual) course policy
In order to specify course policy settings for which no custom user interface exists
I want to be able to manually enter JSON key/value pairs
Scenario: A course author sees only display_name on a newly created course
Scenario: A course author sees default advanced settings
Given I have opened a new course in Studio
When I select the Advanced Settings
Then I see only the display name
Then I see default advanced settings
@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
Then there are no advanced policy settings
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
And I press the "Cancel" notification button
Then the policy key name is unchanged
Scenario: Test editing key name
Scenario: Add new entries, and they appear alphabetically after save
Given I am on the Advanced Course Settings page in Studio
When I edit the name of a policy key
And I press the "Save" notification button
Then the policy key name is changed
Then the settings are alphabetized
Scenario: Test cancel editing key value
Given I am on the Advanced Course Settings page in Studio
When I edit the value of a policy key
And I press the "Cancel" notification button
Then the policy key value is unchanged
And I reload the page
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
And I press the "Save" notification button
Then the policy key value is changed
Scenario: Add new entries, and they appear alphabetically after save
Given I am on the Advanced Course Settings page in Studio
When I create New Entries
Then they are alphabetized
And I reload the page
Then they are alphabetized
Then the policy key value is changed
Scenario: Test how multi-line input appears
Given I am on the Advanced Course Settings page in Studio
When I create a JSON object
When I create a JSON object as a value
Then it is displayed as formatted
And I reload the page
Then it is displayed as formatted
Scenario: Test automatic quoting of non-JSON values
Given I am on the Advanced Course Settings page in Studio
When I create a non-JSON value not in quotes
Then it is displayed as a string
And I reload the page
Then it is displayed as a string
from lettuce import world, step
from common import *
import time
from terrain.steps import reload_the_page
from selenium.common.exceptions import WebDriverException
from selenium.webdriver.support import expected_conditions as EC
......@@ -11,6 +12,10 @@ http://selenium.googlecode.com/svn/trunk/docs/api/py/webdriver/selenium.webdrive
"""
from selenium.webdriver.common.keys import Keys
KEY_CSS = '.key input.policy-key'
VALUE_CSS = 'textarea.json'
DISPLAY_NAME_KEY = "display_name"
DISPLAY_NAME_VALUE = '"Robot Super Course"'
############### ACTIONS ####################
@step('I select the Advanced Settings$')
......@@ -20,7 +25,6 @@ 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$')
......@@ -29,35 +33,27 @@ def i_am_on_advanced_course_settings(step):
step.given('I select the Advanced Settings')
# TODO: this is copied from terrain's step.py. Need to figure out how to share that code.
@step('I reload the page$')
def reload_the_page(step):
world.browser.reload()
@step(u'I edit the name of a policy key$')
def edit_the_name_of_a_policy_key(step):
policy_key_css = 'input.policy-key'
e = css_find(policy_key_css).first
e.type('_new')
@step(u'I press the "([^"]*)" notification button$')
def press_the_notification_button(step, 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,))
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)
time.sleep(float(1))
css_click_at(css)
# is_invisible is not returning a boolean, not working
# try:
# css_click_at(css)
# wait_for(is_invisible)
# except WebDriverException, e:
# css_click_at(css)
# wait_for(is_invisible)
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):
......@@ -65,133 +61,86 @@ def edit_the_value_of_a_policy_key(step):
It is hard to figure out how to get into the CodeMirror
area, so cheat and do it from the policy key field :)
"""
policy_key_css = 'input.policy-key'
e = css_find(policy_key_css).first
e = css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)]
e._element.send_keys(Keys.TAB, Keys.END, Keys.ARROW_LEFT, ' ', 'X')
@step('I delete the display name$')
def delete_the_display_name(step):
delete_entry(0)
click_save()
@step('create New Entries$')
def create_new_entries(step):
create_entry("z", "apple")
create_entry("a", "zebra")
click_save()
@step('I create a JSON object$')
@step('I create a JSON object as a value$')
def create_JSON_object(step):
create_entry("json", '{"key": "value", "key_2": "value_2"}')
click_save()
change_display_name_value(step, '{"key": "value", "key_2": "value_2"}')
############### RESULTS ####################
@step('I see only the display name$')
def i_see_only_display_name(step):
assert_policy_entries(["display_name"], ['"Robot Super Course"'])
@step('I create a non-JSON value not in quotes$')
def create_value_not_in_quotes(step):
change_display_name_value(step, 'quote me')
@step('there are no advanced policy settings$')
def no_policy_settings(step):
keys_css = 'input.policy-key'
val_css = 'textarea.json'
k = world.browser.is_element_not_present_by_css(keys_css, 5)
v = world.browser.is_element_not_present_by_css(val_css, 5)
assert_true(k)
assert_true(v)
############### RESULTS ####################
@step('I see default advanced settings$')
def i_see_default_advanced_settings(step):
# Test only a few of the existing properties (there are around 34 of them)
assert_policy_entries(
["advanced_modules", DISPLAY_NAME_KEY, "show_calculator"], ["[]", DISPLAY_NAME_VALUE, "false"])
@step('they are alphabetized$')
@step('the settings are alphabetized$')
def they_are_alphabetized(step):
assert_policy_entries(["a", "display_name", "z"], ['"zebra"', '"Robot Super Course"', '"apple"'])
key_elements = css_find(KEY_CSS)
all_keys = []
for key in key_elements:
all_keys.append(key.value)
assert_equal(sorted(all_keys), all_keys, "policy keys were not sorted")
@step('it is displayed as formatted$')
def it_is_formatted(step):
assert_policy_entries(["display_name", "json"], ['"Robot Super Course"', '{\n "key": "value",\n "key_2": "value_2"\n}'])
assert_policy_entries([DISPLAY_NAME_KEY], ['{\n "key": "value",\n "key_2": "value_2"\n}'])
@step(u'the policy key name is unchanged$')
def the_policy_key_name_is_unchanged(step):
policy_key_css = 'input.policy-key'
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'
val = css_find(policy_key_css).first.value
assert_equal(val, 'display_name_new')
@step('it is displayed as a string')
def it_is_formatted(step):
assert_policy_entries([DISPLAY_NAME_KEY], ['"quote me"'])
@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'
val = css_find(policy_value_css).first.value
assert_equal(val, '"Robot Super Course"')
assert_equal(get_display_name_value(), DISPLAY_NAME_VALUE)
@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'
val = css_find(policy_value_css).first.value
assert_equal(val, '"Robot Super Course X"')
def the_policy_key_value_is_changed(step):
assert_equal(get_display_name_value(), '"Robot Super Course X"')
############# HELPERS ###############
def create_entry(key, value):
# Scroll down the page so the button is visible
world.scroll_to_bottom()
css_click_at('a.new-advanced-policy-item', 10, 10)
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 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 = '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))
delete_buttons[index].click()
def assert_policy_entries(expected_keys, expected_values):
assert_entries('.key input.policy-key', expected_keys)
assert_entries('textarea.json', expected_values)
for counter in range(len(expected_keys)):
index = get_index_of(expected_keys[counter])
assert_false(index == -1, "Could not find key: " + expected_keys[counter])
assert_equal(expected_values[counter], css_find(VALUE_CSS)[index].value, "value is incorrect")
def assert_entries(css, expected_values):
webElements = css_find(css)
assert_equal(len(expected_values), len(webElements))
# Sometimes get stale reference if I hold on to the array of elements
for counter in range(len(expected_values)):
assert_equal(expected_values[counter], css_find(css)[counter].value)
def get_index_of(expected_key):
for counter in range(len(css_find(KEY_CSS))):
# Sometimes get stale reference if I hold on to the array of elements
key = css_find(KEY_CSS)[counter].value
if key == expected_key:
return counter
return -1
def click_save():
css = "a.save-button"
css_click_at(css)
def get_display_name_value():
index = get_index_of(DISPLAY_NAME_KEY)
return css_find(VALUE_CSS)[index].value
def fill_last_field(value):
newValue = css_find('#__new_advanced_key__ input').first
newValue.fill(value)
def change_display_name_value(step, new_value):
e = css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)]
display_name = get_display_name_value()
for count in range(len(display_name)):
e._element.send_keys(Keys.TAB, Keys.END, Keys.BACK_SPACE)
# Must delete "" before typing the JSON value
e._element.send_keys(Keys.TAB, Keys.END, Keys.BACK_SPACE, Keys.BACK_SPACE, new_value)
press_the_notification_button(step, "Save")
\ No newline at end of file
......@@ -7,7 +7,7 @@ from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore
from xmodule.modulestore import Location
from xmodule.course_module import CourseDescriptor
from prompt import query_yes_no
from .prompt import query_yes_no
from auth.authz import _delete_course_group
......
......@@ -15,10 +15,10 @@ def get_module_info(store, location, parent_location=None, rewrite_static_links=
template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty'])
module = store.clone_item(template_location, location)
data = module.definition['data']
data = module.data
if rewrite_static_links:
data = replace_static_urls(
module.definition['data'],
module.data,
None,
course_namespace=Location([
module.location.tag,
......@@ -32,7 +32,8 @@ def get_module_info(store, location, parent_location=None, rewrite_static_links=
return {
'id': module.location.url(),
'data': data,
'metadata': module.metadata
# TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
'metadata': module._model_data._kvs._metadata
}
......@@ -70,23 +71,23 @@ def set_module_info(store, location, post_data):
# 'apply' the submitted metadata, so we don't end up deleting system metadata
if post_data.get('metadata') is not None:
posted_metadata = post_data['metadata']
# update existing metadata with submitted metadata (which can be partial)
# IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
for metadata_key in posted_metadata.keys():
for metadata_key, value in posted_metadata.items():
# let's strip out any metadata fields from the postback which have been identified as system metadata
# and therefore should not be user-editable, so we should accept them back from the client
if metadata_key in module.system_metadata_fields:
del posted_metadata[metadata_key]
elif posted_metadata[metadata_key] is None:
# remove both from passed in collection as well as the collection read in from the modulestore
if metadata_key in module.metadata:
del module.metadata[metadata_key]
if metadata_key in module._model_data:
del module._model_data[metadata_key]
del posted_metadata[metadata_key]
# overlay the new metadata over the modulestore sourced collection to support partial updates
module.metadata.update(posted_metadata)
else:
module._model_data[metadata_key] = value
# commit to datastore
store.update_metadata(location, module.metadata)
# TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
store.update_metadata(location, module._model_data._kvs._metadata)
......@@ -6,15 +6,16 @@ from django.conf import settings
from django.core.urlresolvers import reverse
from path import path
from tempdir import mkdtemp_clean
from datetime import timedelta
import json
from fs.osfs import OSFS
import copy
from json import loads
from django.contrib.auth.models import User
from cms.djangoapps.contentstore.utils import get_modulestore
from contentstore.utils import get_modulestore
from utils import ModuleStoreTestCase, parse_json
from .utils import ModuleStoreTestCase, parse_json
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore import Location
......@@ -25,6 +26,7 @@ from xmodule.contentstore.django import contentstore
from xmodule.templates import update_templates
from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.inheritance import own_metadata
from xmodule.capa_module import CapaDescriptor
from xmodule.course_module import CourseDescriptor
......@@ -109,10 +111,10 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter','Week_1', None]))
# make sure the parent no longer points to the child object which was deleted
self.assertTrue(sequential.location.url() in chapter.definition['children'])
self.assertTrue(sequential.location.url() in chapter.children)
self.client.post(reverse('delete_item'),
json.dumps({'id': sequential.location.url(), 'delete_children':'true', 'delete_all_versions':'true'}),
self.client.post(reverse('delete_item'),
json.dumps({'id': sequential.location.url(), 'delete_children': 'true', 'delete_all_versions': 'true'}),
"application/json")
found = False
......@@ -127,9 +129,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter','Week_1', None]))
# make sure the parent no longer points to the child object which was deleted
self.assertFalse(sequential.location.url() in chapter.definition['children'])
self.assertFalse(sequential.location.url() in chapter.children)
def test_about_overrides(self):
'''
......@@ -139,11 +141,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
module_store = modulestore('direct')
effort = module_store.get_item(Location(['i4x', 'edX', 'full', 'about', 'effort', None]))
self.assertEqual(effort.definition['data'], '6 hours')
self.assertEqual(effort.data, '6 hours')
# this one should be in a non-override folder
effort = module_store.get_item(Location(['i4x', 'edX', 'full', 'about', 'end_date', None]))
self.assertEqual(effort.definition['data'], 'TBD')
self.assertEqual(effort.data, 'TBD')
def test_remove_hide_progress_tab(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
......@@ -153,7 +155,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
course = module_store.get_item(source_location)
self.assertNotIn('hide_progress_tab', course.metadata)
self.assertFalse(course.hide_progress_tab)
def test_clone_course(self):
......@@ -246,7 +248,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# compare what's on disk compared to what we have in our course
with fs.open('grading_policy.json', 'r') as grading_policy:
on_disk = loads(grading_policy.read())
self.assertEqual(on_disk, course.definition['data']['grading_policy'])
self.assertEqual(on_disk, course.grading_policy)
#check for policy.json
self.assertTrue(fs.exists('policy.json'))
......@@ -255,7 +257,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
with fs.open('policy.json', 'r') as course_policy:
on_disk = loads(course_policy.read())
self.assertIn('course/6.002_Spring_2012', on_disk)
self.assertEqual(on_disk['course/6.002_Spring_2012'], course.metadata)
self.assertEqual(on_disk['course/6.002_Spring_2012'], own_metadata(course))
# remove old course
delete_course(module_store, content_store, location)
......@@ -302,10 +304,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
course = module_store.get_item(location)
metadata = own_metadata(course)
# add a bool piece of unknown metadata so we can verify we don't throw an exception
course.metadata['new_metadata'] = True
metadata['new_metadata'] = True
module_store.update_metadata(location, course.metadata)
module_store.update_metadata(location, metadata)
print 'Exporting to tempdir = {0}'.format(root_dir)
......@@ -473,21 +476,20 @@ class ContentStoreTest(ModuleStoreTestCase):
self.assertIsInstance(problem, CapaDescriptor, "New problem is not a CapaDescriptor")
context = problem.get_context()
self.assertIn('markdown', context, "markdown is missing from context")
self.assertIn('markdown', problem.metadata, "markdown is missing from metadata")
self.assertNotIn('markdown', problem.editable_metadata_fields, "Markdown slipped into the editable metadata fields")
def test_import_metadata_with_attempts_empty_string(self):
import_from_xml(modulestore(), 'common/test/data/', ['simple'])
module_store = modulestore('direct')
did_load_item = False
try:
try:
module_store.get_item(Location(['i4x', 'edX', 'simple', 'problem', 'ps01-simple', None]))
did_load_item = True
except ItemNotFoundError:
pass
# make sure we found the item (e.g. it didn't error while loading)
self.assertTrue(did_load_item)
self.assertTrue(did_load_item)
def test_metadata_inheritance(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
......@@ -499,8 +501,7 @@ class ContentStoreTest(ModuleStoreTestCase):
# let's assert on the metadata_inheritance on an existing vertical
for vertical in verticals:
self.assertIn('xqa_key', vertical.metadata)
self.assertEqual(course.metadata['xqa_key'], vertical.metadata['xqa_key'])
self.assertEqual(course.lms.xqa_key, vertical.lms.xqa_key)
self.assertGreater(len(verticals), 0)
......@@ -510,36 +511,33 @@ class ContentStoreTest(ModuleStoreTestCase):
# crate a new module and add it as a child to a vertical
module_store.clone_item(source_template_location, new_component_location)
parent = verticals[0]
module_store.update_children(parent.location, parent.definition.get('children', []) + [new_component_location.url()])
module_store.update_children(parent.location, parent.children + [new_component_location.url()])
# flush the cache
module_store.get_cached_metadata_inheritance_tree(new_component_location, -1)
new_module = module_store.get_item(new_component_location)
# check for grace period definition which should be defined at the course level
self.assertIn('graceperiod', new_module.metadata)
self.assertEqual(parent.lms.graceperiod, new_module.lms.graceperiod)
self.assertEqual(parent.metadata['graceperiod'], new_module.metadata['graceperiod'])
self.assertEqual(course.metadata['xqa_key'], new_module.metadata['xqa_key'])
self.assertEqual(course.lms.xqa_key, new_module.lms.xqa_key)
#
# now let's define an override at the leaf node level
#
new_module.metadata['graceperiod'] = '1 day'
module_store.update_metadata(new_module.location, new_module.metadata)
new_module.lms.graceperiod = timedelta(1)
module_store.update_metadata(new_module.location, own_metadata(new_module))
# flush the cache and refetch
module_store.get_cached_metadata_inheritance_tree(new_component_location, -1)
new_module = module_store.get_item(new_component_location)
self.assertIn('graceperiod', new_module.metadata)
self.assertEqual('1 day', new_module.metadata['graceperiod'])
self.assertEqual(timedelta(1), new_module.lms.graceperiod)
class TemplateTestCase(ModuleStoreTestCase):
def test_template_cleanup(self):
def test_template_cleanup(self):
module_store = modulestore('direct')
# insert a bogus template in the store
......@@ -562,4 +560,3 @@ class TemplateTestCase(ModuleStoreTestCase):
asserted = True
self.assertTrue(asserted)
......@@ -10,16 +10,16 @@ from django.core.urlresolvers import reverse
from django.utils.timezone import UTC
from xmodule.modulestore import Location
from cms.djangoapps.models.settings.course_details import (CourseDetails,
from models.settings.course_details import (CourseDetails,
CourseSettingsEncoder)
from cms.djangoapps.models.settings.course_grading import CourseGradingModel
from cms.djangoapps.contentstore.utils import get_modulestore
from models.settings.course_grading import CourseGradingModel
from contentstore.utils import get_modulestore
from django.test import TestCase
from utils import ModuleStoreTestCase
from .utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from cms.djangoapps.models.settings.course_metadata import CourseMetadata
from models.settings.course_metadata import CourseMetadata
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.django import modulestore
......@@ -246,8 +246,9 @@ class CourseGradingTest(CourseTestCase):
altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "cutoff add D")
test_grader.grace_period = {'hours' : 4, 'minutes' : 5, 'seconds': 0}
test_grader.grace_period = {'hours': 4, 'minutes': 5, 'seconds': 0}
altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
print test_grader.grace_period, altered_grader.grace_period
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "4 hour grace period")
def test_update_grader_from_json(self):
......@@ -286,31 +287,31 @@ class CourseMetadataEditingTest(CourseTestCase):
def test_update_from_json(self):
test_model = CourseMetadata.update_from_json(self.course_location,
{ "a" : 1,
"b_a_c_h" : { "c" : "test" },
"test_text" : "a text string"})
{ "advertised_start" : "start A",
"testcenter_info" : { "c" : "test" },
"days_early_for_beta" : 2})
self.update_check(test_model)
# try fresh fetch to ensure persistence
test_model = CourseMetadata.fetch(self.course_location)
self.update_check(test_model)
# now change some of the existing metadata
test_model = CourseMetadata.update_from_json(self.course_location,
{ "a" : 2,
{ "advertised_start" : "start B",
"display_name" : "jolly roger"})
self.assertIn('display_name', test_model, 'Missing editable metadata field')
self.assertEqual(test_model['display_name'], 'jolly roger', "not expected value")
self.assertIn('a', test_model, 'Missing revised a metadata field')
self.assertEqual(test_model['a'], 2, "a not expected value")
self.assertIn('advertised_start', test_model, 'Missing revised advertised_start metadata field')
self.assertEqual(test_model['advertised_start'], 'start B', "advertised_start not expected value")
def update_check(self, test_model):
self.assertIn('display_name', test_model, 'Missing editable metadata field')
self.assertEqual(test_model['display_name'], 'Robot Super Course', "not expected value")
self.assertIn('a', test_model, 'Missing new a metadata field')
self.assertEqual(test_model['a'], 1, "a not expected value")
self.assertIn('b_a_c_h', test_model, 'Missing b_a_c_h metadata field')
self.assertDictEqual(test_model['b_a_c_h'], { "c" : "test" }, "b_a_c_h not expected value")
self.assertIn('test_text', test_model, 'Missing test_text metadata field')
self.assertEqual(test_model['test_text'], "a text string", "test_text not expected value")
self.assertIn('advertised_start', test_model, 'Missing new advertised_start metadata field')
self.assertEqual(test_model['advertised_start'], 'start A', "advertised_start not expected value")
self.assertIn('testcenter_info', test_model, 'Missing testcenter_info metadata field')
self.assertDictEqual(test_model['testcenter_info'], { "c" : "test" }, "testcenter_info not expected value")
self.assertIn('days_early_for_beta', test_model, 'Missing days_early_for_beta metadata field')
self.assertEqual(test_model['days_early_for_beta'], 2, "days_early_for_beta not expected value")
def test_delete_key(self):
......@@ -321,5 +322,5 @@ class CourseMetadataEditingTest(CourseTestCase):
self.assertEqual(test_model['display_name'], 'Testing', "not expected value")
self.assertIn('rerandomize', test_model, 'Missing rerandomize metadata field')
# check for deletion effectiveness
self.assertNotIn('showanswer', test_model, 'showanswer field still in')
self.assertNotIn('xqa_key', test_model, 'xqa_key field still in')
\ No newline at end of file
self.assertEqual('closed', test_model['showanswer'], 'showanswer field still in')
self.assertEqual(None, test_model['xqa_key'], 'xqa_key field still in')
from cms.djangoapps.contentstore.tests.test_course_settings import CourseTestCase
from contentstore.tests.test_course_settings import CourseTestCase
from django.core.urlresolvers import reverse
import json
......
from cms.djangoapps.contentstore import utils
from contentstore import utils
import mock
from django.test import TestCase
......
......@@ -8,7 +8,7 @@ import json
from fs.osfs import OSFS
import copy
from cms.djangoapps.contentstore.utils import get_modulestore
from contentstore.utils import get_modulestore
from xmodule.modulestore import Location
from xmodule.modulestore.store_utilities import clone_course
......@@ -24,7 +24,7 @@ from xmodule.course_module import CourseDescriptor
from xmodule.seq_module import SequenceDescriptor
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from utils import ModuleStoreTestCase, parse_json, user, registration
from .utils import ModuleStoreTestCase, parse_json, user, registration
class ContentStoreTestCase(ModuleStoreTestCase):
......
......@@ -39,10 +39,10 @@ def get_course_location_for_item(location):
# make sure we found exactly one match on this above course search
found_cnt = len(courses)
if found_cnt == 0:
raise BaseException('Could not find course at {0}'.format(course_search_location))
raise Exception('Could not find course at {0}'.format(course_search_location))
if found_cnt > 1:
raise BaseException('Found more than one course at {0}. There should only be one!!! Dump = {1}'.format(course_search_location, courses))
raise Exception('Found more than one course at {0}. There should only be one!!! Dump = {1}'.format(course_search_location, courses))
location = courses[0].location
......@@ -136,7 +136,7 @@ def compute_unit_state(unit):
'private' content is editabled and not visible in the LMS
"""
if unit.metadata.get('is_draft', False):
if unit.cms.is_draft:
try:
modulestore('direct').get_item(unit.location)
return UnitState.draft
......
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.inheritance import own_metadata
import json
from json.encoder import JSONEncoder
import time
from contentstore.utils import get_modulestore
from util.converters import jsdate_to_time, time_to_date
from cms.djangoapps.models.settings import course_grading
from cms.djangoapps.contentstore.utils import update_item
from models.settings import course_grading
from contentstore.utils import update_item
import re
import logging
......@@ -43,25 +44,25 @@ class CourseDetails(object):
temploc = course_location._replace(category='about', name='syllabus')
try:
course.syllabus = get_modulestore(temploc).get_item(temploc).definition['data']
course.syllabus = get_modulestore(temploc).get_item(temploc).data
except ItemNotFoundError:
pass
temploc = temploc._replace(name='overview')
try:
course.overview = get_modulestore(temploc).get_item(temploc).definition['data']
course.overview = get_modulestore(temploc).get_item(temploc).data
except ItemNotFoundError:
pass
temploc = temploc._replace(name='effort')
try:
course.effort = get_modulestore(temploc).get_item(temploc).definition['data']
course.effort = get_modulestore(temploc).get_item(temploc).data
except ItemNotFoundError:
pass
temploc = temploc._replace(name='video')
try:
raw_video = get_modulestore(temploc).get_item(temploc).definition['data']
raw_video = get_modulestore(temploc).get_item(temploc).data
course.intro_video = CourseDetails.parse_video_tag(raw_video)
except ItemNotFoundError:
pass
......@@ -116,7 +117,7 @@ class CourseDetails(object):
descriptor.enrollment_end = converted
if dirty:
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
get_modulestore(course_location).update_metadata(course_location, own_metadata(descriptor))
# NOTE: below auto writes to the db w/o verifying that any of the fields actually changed
# to make faster, could compare against db or could have client send over a list of which fields changed.
......@@ -133,7 +134,6 @@ class CourseDetails(object):
recomposed_video_tag = CourseDetails.recompose_video_tag(jsondict['intro_video'])
update_item(temploc, recomposed_video_tag)
# Could just generate and return a course obj w/o doing any db reads, but I put the reads in as a means to confirm
# it persisted correctly
return CourseDetails.fetch(course_location)
......
......@@ -2,6 +2,7 @@ from xmodule.modulestore import Location
from contentstore.utils import get_modulestore
import re
from util import converters
from datetime import timedelta
class CourseGradingModel(object):
......@@ -91,7 +92,7 @@ class CourseGradingModel(object):
descriptor.raw_grader = graders_parsed
descriptor.grade_cutoffs = jsondict['grade_cutoffs']
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data)
CourseGradingModel.update_grace_period_from_json(course_location, jsondict['grace_period'])
return CourseGradingModel.fetch(course_location)
......@@ -119,7 +120,7 @@ class CourseGradingModel(object):
else:
descriptor.raw_grader.append(grader)
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data)
return CourseGradingModel.jsonize_grader(index, descriptor.raw_grader[index])
......@@ -134,7 +135,7 @@ class CourseGradingModel(object):
descriptor = get_modulestore(course_location).get_item(course_location)
descriptor.grade_cutoffs = cutoffs
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data)
return cutoffs
......@@ -156,11 +157,11 @@ class CourseGradingModel(object):
graceperiodjson = graceperiodjson['grace_period']
# lms requires these to be in a fixed order
grace_rep = "{0[hours]:d} hours {0[minutes]:d} minutes {0[seconds]:d} seconds".format(graceperiodjson)
grace_timedelta = timedelta(**graceperiodjson)
descriptor = get_modulestore(course_location).get_item(course_location)
descriptor.metadata['graceperiod'] = grace_rep
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
descriptor.lms.graceperiod = grace_timedelta
get_modulestore(course_location).update_metadata(course_location, descriptor._model_data._kvs._metadata)
@staticmethod
def delete_grader(course_location, index):
......@@ -176,7 +177,7 @@ class CourseGradingModel(object):
del descriptor.raw_grader[index]
# force propagation to definition
descriptor.raw_grader = descriptor.raw_grader
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data)
# NOTE cannot delete cutoffs. May be useful to reset
@staticmethod
......@@ -189,7 +190,7 @@ class CourseGradingModel(object):
descriptor = get_modulestore(course_location).get_item(course_location)
descriptor.grade_cutoffs = descriptor.defaut_grading_policy['GRADE_CUTOFFS']
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data)
return descriptor.grade_cutoffs
......@@ -202,8 +203,8 @@ class CourseGradingModel(object):
course_location = Location(course_location)
descriptor = get_modulestore(course_location).get_item(course_location)
if 'graceperiod' in descriptor.metadata: del descriptor.metadata['graceperiod']
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
del descriptor.lms.graceperiod
get_modulestore(course_location).update_metadata(course_location, descriptor._model_data._kvs._metadata)
@staticmethod
def get_section_grader_type(location):
......@@ -212,7 +213,7 @@ class CourseGradingModel(object):
descriptor = get_modulestore(location).get_item(location)
return {
"graderType": descriptor.metadata.get('format', u"Not Graded"),
"graderType": descriptor.lms.format if descriptor.lms.format is not None else 'Not Graded',
"location": location,
"id": 99 # just an arbitrary value to
}
......@@ -224,23 +225,41 @@ class CourseGradingModel(object):
descriptor = get_modulestore(location).get_item(location)
if 'graderType' in jsondict and jsondict['graderType'] != u"Not Graded":
descriptor.metadata['format'] = jsondict.get('graderType')
descriptor.metadata['graded'] = True
descriptor.lms.format = jsondict.get('graderType')
descriptor.lms.graded = True
else:
if 'format' in descriptor.metadata: del descriptor.metadata['format']
if 'graded' in descriptor.metadata: del descriptor.metadata['graded']
del descriptor.lms.format
del descriptor.lms.graded
get_modulestore(location).update_metadata(location, descriptor.metadata)
get_modulestore(location).update_metadata(location, descriptor._model_data._kvs._metadata)
@staticmethod
def convert_set_grace_period(descriptor):
# 5 hours 59 minutes 59 seconds => { hours: 5, minutes : 59, seconds : 59}
rawgrace = descriptor.metadata.get('graceperiod', None)
# 5 hours 59 minutes 59 seconds => converted to iso format
rawgrace = descriptor.lms.graceperiod
if rawgrace:
parsedgrace = {str(key): int(val) for (val, key) in re.findall('\s*(\d+)\s*(\w+)', rawgrace)}
return parsedgrace
else: return None
hours_from_days = rawgrace.days*24
seconds = rawgrace.seconds
hours_from_seconds = int(seconds / 3600)
hours = hours_from_days + hours_from_seconds
seconds -= hours_from_seconds * 3600
minutes = int(seconds / 60)
seconds -= minutes * 60
graceperiod = {'hours': 0, 'minutes': 0, 'seconds': 0}
if hours > 0:
graceperiod['hours'] = hours
if minutes > 0:
graceperiod['minutes'] = minutes
if seconds > 0:
graceperiod['seconds'] = seconds
return graceperiod
else:
return None
@staticmethod
def parse_grader(json_grader):
......
from xmodule.modulestore import Location
from contentstore.utils import get_modulestore
from xmodule.x_module import XModuleDescriptor
from xmodule.modulestore.inheritance import own_metadata
from xblock.core import Scope
class CourseMetadata(object):
......@@ -8,8 +10,7 @@ class CourseMetadata(object):
For CRUD operations on metadata fields which do not have specific editors on the other pages including any user generated ones.
The objects have no predefined attrs but instead are obj encodings of the editable metadata.
'''
# __new_advanced_key__ is used by client not server; so, could argue against it being here
FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start', 'end', 'enrollment_start', 'enrollment_end', 'tabs', 'graceperiod', '__new_advanced_key__']
FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start', 'end', 'enrollment_start', 'enrollment_end', 'tabs', 'graceperiod']
@classmethod
def fetch(cls, course_location):
......@@ -23,17 +24,20 @@ class CourseMetadata(object):
descriptor = get_modulestore(course_location).get_item(course_location)
for k, v in descriptor.metadata.iteritems():
if k not in cls.FILTERED_LIST:
course[k] = v
for field in descriptor.fields + descriptor.lms.fields:
if field.scope != Scope.settings:
continue
if field.name not in cls.FILTERED_LIST:
course[field.name] = field.read_from(descriptor)
return course
@classmethod
def update_from_json(cls, course_location, jsondict):
"""
Decode the json into CourseMetadata and save any changed attrs to the db.
Ensures none of the fields are in the blacklist.
"""
descriptor = get_modulestore(course_location).get_item(course_location)
......@@ -42,12 +46,18 @@ class CourseMetadata(object):
for k, v in jsondict.iteritems():
# should it be an error if one of the filtered list items is in the payload?
if k not in cls.FILTERED_LIST and (k not in descriptor.metadata or descriptor.metadata[k] != v):
if k in cls.FILTERED_LIST:
continue
if hasattr(descriptor, k) and getattr(descriptor, k) != v:
dirty = True
setattr(descriptor, k, v)
elif hasattr(descriptor.lms, k) and getattr(descriptor.lms, k) != k:
dirty = True
descriptor.metadata[k] = v
setattr(descriptor.lms, k, v)
if dirty:
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
get_modulestore(course_location).update_metadata(course_location, own_metadata(descriptor))
# Could just generate and return a course obj w/o doing any db reads, but I put the reads in as a means to confirm
# it persisted correctly
......@@ -61,10 +71,11 @@ class CourseMetadata(object):
descriptor = get_modulestore(course_location).get_item(course_location)
for key in payload['deleteKeys']:
if key in descriptor.metadata:
del descriptor.metadata[key]
if hasattr(descriptor, key):
delattr(descriptor, key)
elif hasattr(descriptor.lms, key):
delattr(descriptor.lms, key)
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
get_modulestore(course_location).update_metadata(course_location, own_metadata(descriptor))
return cls.fetch(course_location)
\ No newline at end of file
......@@ -96,6 +96,13 @@ CACHES = {
'KEY_PREFIX': 'general',
'VERSION': 4,
'KEY_FUNCTION': 'util.memcache.safe_key',
},
'mongo_metadata_inheritance': {
'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
'LOCATION': '/var/tmp/mongo_metadata_inheritance',
'TIMEOUT': 300,
'KEY_FUNCTION': 'util.memcache.safe_key',
}
}
......
......@@ -98,6 +98,13 @@ CACHES = {
'KEY_PREFIX': 'general',
'VERSION': 4,
'KEY_FUNCTION': 'util.memcache.safe_key',
},
'mongo_metadata_inheritance': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': '/var/tmp/mongo_metadata_inheritance',
'TIMEOUT': 300,
'KEY_FUNCTION': 'util.memcache.safe_key',
}
}
......
from dogapi import dog_http_api, dog_stats_api
from django.conf import settings
from xmodule.modulestore.django import modulestore
from django.core.cache import get_cache, InvalidCacheBackendError
cache = get_cache('mongo_metadata_inheritance')
for store_name in settings.MODULESTORE:
store = modulestore(store_name)
store.metadata_inheritance_cache = cache
if hasattr(settings, 'DATADOG_API'):
dog_http_api.api_key = settings.DATADOG_API
......
<li class="field-group course-advanced-policy-list-item">
<div class="field text key" id="<%= (_.isEmpty(key) ? '__new_advanced_key__' : key) %>">
<div class="field is-not-editable text key" id="<%= key %>">
<label for="<%= keyUniqueId %>">Policy Key:</label>
<input type="text" class="short policy-key" id="<%= keyUniqueId %>" value="<%= key %>" />
<span class="tip tip-stacked">Keys are case sensitive and cannot contain spaces or start with a number</span>
<input readonly title="This field is disabled: policy keys cannot be edited." type="text" class="short policy-key" id="<%= keyUniqueId %>" value="<%= key %>" />
</div>
<div class="field text value">
<label for="<%= valueUniqueId %>">Policy Value:</label>
<textarea class="json text" id="<%= valueUniqueId %>"><%= value %></textarea>
</div>
<div class="actions">
<a href="#" class="button delete-button standard remove-item remove-grading-data"><span class="delete-icon"></span>Delete</a>
</div>
</li>
\ No newline at end of file
{
"js_files": [
"/static/js/vendor/RequireJS.js",
"/static/js/vendor/jquery.min.js",
"/static/js/vendor/jquery-ui.min.js",
"/static/js/vendor/jquery.ui.draggable.js",
"/static/js/vendor/jquery.cookie.js",
"/static/js/vendor/json2.js",
"/static/js/vendor/underscore-min.js",
"/static/js/vendor/backbone-min.js"
"static_files": [
"js/vendor/RequireJS.js",
"js/vendor/jquery.min.js",
"js/vendor/jquery-ui.min.js",
"js/vendor/jquery.ui.draggable.js",
"js/vendor/jquery.cookie.js",
"js/vendor/json2.js",
"js/vendor/underscore-min.js",
"js/vendor/backbone-min.js"
]
}
if (!CMS.Models['Settings']) CMS.Models.Settings = {};
CMS.Models.Settings.Advanced = Backbone.Model.extend({
// the key for a newly added policy-- before the user has entered a key value
new_key : "__new_advanced_key__",
defaults: {
// the properties are whatever the user types in (in addition to whatever comes originally from the server)
},
// which keys to send as the deleted keys on next save
deleteKeys : [],
blacklistKeys : [], // an array which the controller should populate directly for now [static not instance based]
validate: function (attrs) {
var errors = {};
for (var key in attrs) {
if (key === this.new_key || _.isEmpty(key)) {
errors[key] = "A key must be entered.";
}
else if (_.contains(this.blacklistKeys, key)) {
errors[key] = key + " is a reserved keyword or can be edited on another screen";
}
}
if (!_.isEmpty(errors)) return errors;
// Keys can no longer be edited. We are currently not validating values.
},
save : function (attrs, options) {
......
......@@ -6,14 +6,6 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
// Model class is CMS.Models.Settings.Advanced
events : {
'click .delete-button' : "deleteEntry",
'click .new-button' : "addEntry",
// update model on changes
'change .policy-key' : "updateKey",
// keypress to catch alpha keys and backspace/delete on some browsers
'keypress .policy-key' : "showSaveCancelButtons",
// keyup to catch backspace/delete reliably
'keyup .policy-key' : "showSaveCancelButtons",
'focus :input' : "focusInput",
'blur :input' : "blurInput"
// TODO enable/disable save based on validation (currently enabled whenever there are changes)
......@@ -95,16 +87,11 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
mirror.setValue(stringValue);
} catch(quotedE) {
// TODO: validation error
console.log("Error with JSON, even after converting to String.");
console.log(quotedE);
// console.log("Error with JSON, even after converting to String.");
// console.log(quotedE);
JSONValue = undefined;
}
}
else {
// TODO: validation error
console.log("Error with JSON, but will not convert to String.");
console.log(e);
}
}
if (JSONValue !== undefined) {
self.clearValidationErrors();
......@@ -113,7 +100,6 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
}
});
},
showMessage: function (type) {
this.$el.find(".message-status").removeClass("is-shown");
if (type) {
......@@ -128,56 +114,19 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
else {
// This is the case of the page first rendering, or when Cancel is pressed.
this.hideSaveCancelButtons();
this.toggleNewButton(true);
}
},
showSaveCancelButtons: function(event) {
if (!this.buttonsVisible) {
if (event && (event.type === 'keypress' || event.type === 'keyup')) {
// check whether it's really an altering event: note, String.fromCharCode(keyCode) will
// give positive values for control/command/option-letter combos; so, don't use it
if (!((event.charCode && String.fromCharCode(event.charCode) !== "") ||
// 8 = backspace, 46 = delete
event.keyCode === 8 || event.keyCode === 46)) return;
}
this.$el.find(".message-status").removeClass("is-shown");
$('.wrapper-notification').addClass('is-shown');
this.buttonsVisible = true;
}
},
hideSaveCancelButtons: function() {
$('.wrapper-notification').removeClass('is-shown');
this.buttonsVisible = false;
},
toggleNewButton: function (enable) {
var newButton = this.$el.find(".new-button");
if (enable) {
newButton.removeClass('disabled');
}
else {
newButton.addClass('disabled');
}
},
deleteEntry : function(event) {
event.preventDefault();
// find out which entry
var li$ = $(event.currentTarget).closest('li');
// Not data b/c the validation view uses it for a selector
var key = $('.key', li$).attr('id');
delete this.selectorToField[this.fieldToSelectorMap[key]];
delete this.fieldToSelectorMap[key];
if (key !== this.model.new_key) {
this.model.deleteKeys.push(key);
this.model.unset(key);
}
li$.remove();
this.showSaveCancelButtons();
},
saveView : function(event) {
// TODO one last verification scan:
// call validateKey on each to ensure proper format
......@@ -201,102 +150,15 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
error : CMS.ServerError
});
},
addEntry : function() {
var listEle$ = this.$el.find('.course-advanced-policy-list');
var newEle = this.renderTemplate("", "");
listEle$.append(newEle);
// need to re-find b/c replaceWith seems to copy rather than use the specific ele instance
var policyValueDivs = this.$el.find('#' + this.model.new_key).closest('li').find('.json');
// only 1 but hey, let's take advantage of the context mechanism
_.each(policyValueDivs, this.attachJSONEditor, this);
this.toggleNewButton(false);
},
updateKey : function(event) {
var parentElement = $(event.currentTarget).closest('.key');
// old key: either the key as in the model or new_key.
// That is, it doesn't change as the val changes until val is accepted.
var oldKey = parentElement.attr('id');
// TODO: validation of keys with spaces. For now at least trim strings to remove initial and
// trailing whitespace
var newKey = $.trim($(event.currentTarget).val());
if (oldKey !== newKey) {
// TODO: is it OK to erase other validation messages?
this.clearValidationErrors();
if (!this.validateKey(oldKey, newKey)) return;
if (this.model.has(newKey)) {
var error = {};
error[oldKey] = 'You have already defined "' + newKey + '" in the manual policy definitions.';
error[newKey] = "You tried to enter a duplicate of this key.";
this.model.trigger("invalid", this.model, error);
return false;
}
// explicitly call validate to determine whether to proceed (relying on triggered error means putting continuation in the success
// method which is uglier I think?)
var newEntryModel = {};
// set the new key's value to the old one's
newEntryModel[newKey] = (oldKey === this.model.new_key ? '' : this.model.get(oldKey));
var validation = this.model.validate(newEntryModel);
if (validation) {
if (_.has(validation, newKey)) {
// swap to the key which the map knows about
validation[oldKey] = validation[newKey];
}
this.model.trigger("invalid", this.model, validation);
// abandon update
return;
}
// Now safe to actually do the update
this.model.set(newEntryModel);
// update maps
var selector = this.fieldToSelectorMap[oldKey];
this.selectorToField[selector] = newKey;
this.fieldToSelectorMap[newKey] = selector;
delete this.fieldToSelectorMap[oldKey];
if (oldKey !== this.model.new_key) {
// mark the old key for deletion and delete from field maps
this.model.deleteKeys.push(oldKey);
this.model.unset(oldKey) ;
}
else {
// id for the new entry will now be the key value. Enable new entry button.
this.toggleNewButton(true);
}
// check for newkey being the name of one which was previously deleted in this session
var wasDeleting = this.model.deleteKeys.indexOf(newKey);
if (wasDeleting >= 0) {
this.model.deleteKeys.splice(wasDeleting, 1);
}
// Update the ID to the new value.
parentElement.attr('id', newKey);
}
},
validateKey : function(oldKey, newKey) {
// model validation can't handle malformed keys nor notice if 2 fields have same key; so, need to add that chk here
// TODO ensure there's no spaces or illegal chars (note some checking for spaces currently done in model's
// validate method.
return true;
},
renderTemplate: function (key, value) {
var newKeyId = _.uniqueId('policy_key_'),
newEle = this.template({ key : key, value : JSON.stringify(value, null, 4),
keyUniqueId: newKeyId, valueUniqueId: _.uniqueId('policy_value_')});
this.fieldToSelectorMap[(_.isEmpty(key) ? this.model.new_key : key)] = newKeyId;
this.selectorToField[newKeyId] = (_.isEmpty(key) ? this.model.new_key : key);
this.fieldToSelectorMap[key] = newKeyId;
this.selectorToField[newKeyId] = key;
return newEle;
},
focusInput : function(event) {
$(event.target).prev().addClass("is-focused");
},
......
......@@ -406,6 +406,21 @@ textarea.text {
@include linear-gradient($paleYellow, tint($paleYellow, 90%));
outline: 0;
}
&[disabled] {
border-color: $gray-l4;
color: $gray-l2;
}
&[readonly] {
border-color: $gray-l4;
color: $gray-l1;
&:focus {
@include linear-gradient($lightGrey, tint($lightGrey, 90%));
outline: 0;
}
}
}
// forms - specific
......
......@@ -239,13 +239,9 @@ body.course.settings {
// not editable fields
.field.is-not-editable {
label, .label {
color: $gray-l3;
}
input {
opacity: 0.25;
& label.is-focused {
color: $gray-d2;
}
}
......
<%inherit file="base.html" />
<%block name="title">Page Not Found</%block>
<%block name="content">
<div class="wrapper-content wrapper">
<section class="content">
<h1>Page not found</h1>
<p>The page that you were looking for was not found. Go back to the <a href="/">homepage</a> or let us know about any pages that may have been moved at <a href="mailto:technical@edx.org">technical@edx.org</a>.</p>
</section>
</div>
</%block>
\ No newline at end of file
<%inherit file="base.html" />
<%block name="title">Server Error</%block>
<%block name="content">
<div class="wrapper-content wrapper">
<section class="content">
<h1>Currently the <em>edX</em> servers are down</h1>
<p>Our staff is currently working to get the site back up as soon as possible. Please email us at <a href="mailto:technical@edx.org">technical@edx.org</a> to report any problems or downtime.</p>
</section>
</div>
</%block>
\ No newline at end of file
......@@ -6,10 +6,10 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>
<%block name="title"></%block> |
<%block name="title"></%block> |
% if context_course:
<% ctx_loc = context_course.location %>
${context_course.display_name} |
${context_course.display_name_with_default} |
% endif
edX Studio
</title>
......@@ -22,7 +22,7 @@
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/markitup/sets/wiki/style.css')}" />
<link rel="stylesheet" type="text/css" href="${static.url('css/vendor/symbolset.ss-symbolicons-block.css')}" />
<link rel="stylesheet" type="text/css" href="${static.url('css/vendor/symbolset.ss-standard.css')}" />
<%block name="header_extras"></%block>
</head>
......
<%inherit file="base.html" />
<%include file="widgets/header.html"/>
<%block name="content">
<section class="main-container">
<%include file="widgets/navigation.html"/>
<section class="main-content">
</section>
</section>
</%block>
......@@ -22,7 +22,7 @@
<article class="subsection-body window" data-id="${subsection.location}">
<div class="subsection-name-input">
<label>Display Name:</label>
<input type="text" value="${subsection.display_name | h}" class="subsection-display-name-input" data-metadata-name="display_name"/>
<input type="text" value="${subsection.display_name_with_default | h}" class="subsection-display-name-input" data-metadata-name="display_name"/>
</div>
<div class="sortable-unit-list">
<label>Units:</label>
......@@ -40,7 +40,7 @@
<a href="#" class="save-button">Save</a>
<a href="#" class="cancel-button">Cancel</a>
<a href="#" class="delete-icon remove-policy-data"></a>
</li>
</li>
</div>
<div class="sidebar">
......@@ -51,28 +51,28 @@
<label>Release date:<!-- <span class="description">Determines when this subsection and the units within it will be released publicly.</span>--></label>
<div class="datepair" data-language="javascript">
<%
start_date = datetime.fromtimestamp(mktime(subsection.start)) if subsection.start is not None else None
parent_start_date = datetime.fromtimestamp(mktime(parent_item.start)) if parent_item.start is not None else None
start_date = datetime.fromtimestamp(mktime(subsection.lms.start)) if subsection.lms.start is not None else None
parent_start_date = datetime.fromtimestamp(mktime(parent_item.lms.start)) if parent_item.lms.start is not None else None
%>
<input type="text" id="start_date" name="start_date" value="${start_date.strftime('%m/%d/%Y') if start_date is not None else ''}" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
<input type="text" id="start_time" name="start_time" value="${start_date.strftime('%H:%M') if start_date is not None else ''}" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
<input type="text" id="start_time" name="start_time" value="${start_date.strftime('%H:%M') if start_date is not None else ''}" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
</div>
% if subsection.start != parent_item.start and subsection.start:
% if subsection.lms.start != parent_item.lms.start and subsection.lms.start:
% if parent_start_date is None:
<p class="notice">The date above differs from the release date of ${parent_item.display_name}, which is unset.
<p class="notice">The date above differs from the release date of ${parent_item.display_name_with_default}, which is unset.
% else:
<p class="notice">The date above differs from the release date of ${parent_item.display_name} – ${parent_start_date.strftime('%m/%d/%Y')} at ${parent_start_date.strftime('%H:%M')}.
<p class="notice">The date above differs from the release date of ${parent_item.display_name_with_default} – ${parent_start_date.strftime('%m/%d/%Y')} at ${parent_start_date.strftime('%H:%M')}.
% endif
<a href="#" class="sync-date no-spinner">Sync to ${parent_item.display_name}.</a></p>
<a href="#" class="sync-date no-spinner">Sync to ${parent_item.display_name_with_default}.</a></p>
% endif
</div>
<div class="row gradable">
<label>Graded as:</label>
<div class="gradable-status" data-initial-status="${subsection.metadata.get('format', 'Not Graded')}">
<div class="gradable-status" data-initial-status="${subsection.lms.format if subsection.lms.format is not None else 'Not Graded'}">
</div>
<div class="due-date-input row">
<label>Due date:</label>
<a href="#" class="set-date">Set a due date</a>
......@@ -80,9 +80,9 @@
<p class="date-description">
<%
# due date uses it own formatting for stringifying the date. As with capa_module.py, there's a utility module available for us to use
due_date = dateutil.parser.parse(subsection.metadata.get('due')) if 'due' in subsection.metadata else None
due_date = dateutil.parser.parse(subsection.lms.due) if subsection.lms.due else None
%>
<input type="text" id="due_date" name="due_date" value="${due_date.strftime('%m/%d/%Y') if due_date is not None else ''}" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
<input type="text" id="due_date" name="due_date" value="${due_date.strftime('%m/%d/%Y') if due_date is not None else ''}" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
<input type="text" id="due_time" name="due_time" value="${due_date.strftime('%H:%M') if due_date is not None else ''}" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
<a href="#" class="remove-date">Remove due date</a>
</p>
......@@ -110,7 +110,7 @@
<script type="text/javascript" src="${static.url('js/views/grader-select-view.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/settings/course_grading_policy.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/overview.js')}"></script>
<script type="text/javascript">
$(document).ready(function() {
......@@ -128,7 +128,7 @@
window.graderTypes.course_location = new CMS.Models.Location('${parent_location}');
window.graderTypes.reset(${course_graders|n});
}
$(".gradable-status").each(function(index, ele) {
var gradeView = new CMS.Views.OverviewAssignmentGrader({
el : ele,
......
......@@ -21,14 +21,14 @@
<div class="description">
<p><strong>Importing a new course will delete all content currently associated with your course
and replace it with the contents of the uploaded file.</strong></p>
<p>File uploads must be gzipped tar files (.tar.gz or .tgz) containing, at a minimum, a <code>course.xml</code> file.</p>
<p>File uploads must be gzipped tar files (.tar.gz) containing, at a minimum, a <code>course.xml</code> file.</p>
<p>Please note that if your course has any problems with auto-generated <code>url_name</code> nodes,
re-importing your course could cause the loss of student data associated with those problems.</p>
</div>
<form action="${reverse('import_course', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}" method="post" enctype="multipart/form-data" class="import-form">
<h2>Course to import:</h2>
<p class="error-block"></p>
<a href="#" class="choose-file-button">Choose File</a>
<a href="#" class="choose-file-button">Choose File</a>
<p class="file-name-block"><span class="file-name"></span><a href="#" class="choose-file-button-inline">change</a></p>
<input type="file" name="course-data" class="file-input">
<input type="submit" value="Replace my course with the one above" class="submit-button">
......@@ -45,13 +45,13 @@
<%block name="jsextra">
<script>
(function() {
var bar = $('.progress-bar');
var fill = $('.progress-fill');
var percent = $('.percent');
var status = $('#status');
var submitBtn = $('.submit-button');
$('form').ajaxForm({
beforeSend: function() {
status.empty();
......@@ -76,7 +76,7 @@ $('form').ajaxForm({
submitBtn.show();
bar.hide();
}
});
})();
});
})();
</script>
</%block>
\ No newline at end of file
......@@ -8,7 +8,7 @@
<div>${module_type}</div>
<div>
% for template in module_templates:
<a class="save" data-template-id="${template.location.url()}">${template.display_name}</a>
<a class="save" data-template-id="${template.location.url()}">${template.display_name_with_default}</a>
% endfor
</div>
</div>
......
......@@ -32,7 +32,7 @@
window.graderTypes.course_location = new CMS.Models.Location('${parent_location}');
window.graderTypes.reset(${course_graders|n});
}
$(".gradable-status").each(function(index, ele) {
var gradeView = new CMS.Views.OverviewAssignmentGrader({
el : ele,
......@@ -40,7 +40,7 @@
});
});
});
</script>
</%block>
......@@ -146,7 +146,7 @@
<div class="main-wrapper">
<div class="inner-wrapper">
<article class="courseware-overview" data-course-id="${context_course.location.url()}">
<article class="courseware-overview" data-course-id="${context_course.location.url()}">
% for section in sections:
<section class="courseware-section branch" data-id="${section.location}">
<header>
......@@ -154,16 +154,16 @@
<div class="item-details" data-id="${section.location}">
<h3 class="section-name">
<span data-tooltip="Edit this section's name" class="section-name-span">${section.display_name}</span>
<span data-tooltip="Edit this section's name" class="section-name-span">${section.display_name_with_default}</span>
<form class="section-name-edit" style="display:none">
<input type="text" value="${section.display_name | h}" class="edit-section-name" autocomplete="off"/>
<input type="text" value="${section.display_name_with_default | h}" class="edit-section-name" autocomplete="off"/>
<input type="submit" class="save-button edit-section-name-save" value="Save" />
<input type="button" class="cancel-button edit-section-name-cancel" value="Cancel" />
</form>
</h3>
<div class="section-published-date">
<%
start_date = datetime.fromtimestamp(mktime(section.start)) if section.start is not None else None
start_date = datetime.fromtimestamp(mktime(section.lms.start)) if section.lms.start is not None else None
start_date_str = start_date.strftime('%m/%d/%Y') if start_date is not None else ''
start_time_str = start_date.strftime('%H:%M') if start_date is not None else ''
%>
......@@ -174,9 +174,9 @@
<span class="published-status"><strong>Will Release:</strong> ${start_date_str} at ${start_time_str}</span>
<a href="#" class="edit-button" data-date="${start_date_str}" data-time="${start_time_str}" data-id="${section.location}">Edit</a>
%endif
</div>
</div>
</div>
<div class="item-actions">
<a href="#" data-tooltip="Delete this section" class="delete-button delete-section-button"><span class="delete-icon"></span></a>
<a href="#" data-tooltip="Drag to reorder" class="drag-handle"></a>
......@@ -196,15 +196,15 @@
<a href="#" data-tooltip="Expand/collapse this subsection" class="expand-collapse-icon expand"></a>
<a href="${reverse('edit_subsection', args=[subsection.location])}">
<span class="folder-icon"></span>
<span class="subsection-name"><span class="subsection-name-value">${subsection.display_name}</span></span>
<span class="subsection-name"><span class="subsection-name-value">${subsection.display_name_with_default}</span></span>
</a>
</div>
<div class="gradable-status" data-initial-status="${subsection.metadata.get('format', 'Not Graded')}">
<div class="gradable-status" data-initial-status="${subsection.lms.format if section.lms.format is not None else 'Not Graded'}">
</div>
<div class="item-actions">
<a href="#" data-tooltip="Delete this subsection" class="delete-button delete-subsection-button"><span class="delete-icon"></span></a>
<a href="#" data-tooltip="Delete this subsection" class="delete-button delete-subsection-button"><span class="delete-icon"></span></a>
<a href="#" data-tooltip="Drag to reorder" class="drag-handle"></a>
</div>
</div>
......
......@@ -70,17 +70,17 @@ from contentstore import utils
<ol class="list-input">
<li class="field text is-not-editable" id="field-course-organization">
<label for="course-organization">Organization</label>
<input type="text" class="long" id="course-organization" value="[Course Organization]" disabled="disabled" />
<input title="This field is disabled: this information cannot be changed." type="text" class="long" id="course-organization" value="[Course Organization]" readonly />
</li>
<li class="field text is-not-editable" id="field-course-number">
<label for="course-number">Course Number</label>
<input type="text" class="short" id="course-number" value="[Course No.]" disabled="disabled">
<input title="This field is disabled: this information cannot be changed." type="text" class="short" id="course-number" value="[Course No.]" readonly>
</li>
<li class="field text is-not-editable" id="field-course-name">
<label for="course-name">Course Name</label>
<input type="text" class="long" id="course-name" value="[Course Name]" disabled="disabled" />
<input title="This field is disabled: this information cannot be changed." type="text" class="long" id="course-name" value="[Course Name]" readonly />
</li>
</ol>
<span class="tip tip-stacked">These are used in <a rel="external" href="${utils.get_lms_link_for_about_page(course_location)}" />your course URL</a>, and cannot be changed</span>
......
......@@ -21,7 +21,6 @@ $(document).ready(function () {
// proactively populate advanced b/c it has the filtered list and doesn't really follow the model pattern
var advancedModel = new CMS.Models.Settings.Advanced(${advanced_dict | n}, {parse: true});
advancedModel.blacklistKeys = ${advanced_blacklist | n};
advancedModel.url = "${reverse('course_advanced_settings_updates', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}";
var editor = new CMS.Views.Settings.Advanced({
......@@ -61,18 +60,11 @@ editor.render();
<span class="tip">Manually Edit Course Policy Values (JSON Key / Value pairs)</span>
</header>
<p class="instructions"><strong>Warning</strong>: Add only manual policy data that you are familiar
with.</p>
<p class="instructions"><strong>Warning</strong>: Do not modify these policies unless you are familiar with their purpose.</p>
<ul class="list-input course-advanced-policy-list enum">
</ul>
<div class="actions">
<a href="#" class="button new-button new-advanced-policy-item add-policy-data">
<span class="plus-icon white"></span>New Manual Policy
</a>
</div>
</section>
</form>
</article>
......@@ -80,9 +72,9 @@ editor.render();
<aside class="content-supplementary" role="complimentary">
<div class="bit">
<h3 class="title-3">How will these settings be used?</h3>
<p>Manual policies are JSON-based key and value pairs that allow you add additional settings which edX Studio will use when generating your course.</p>
<p>Manual policies are JSON-based key and value pairs that give you control over specific course settings that edX Studio will use when displaying and running your course.</p>
<p>Any policies you define here will override any other information you've defined elsewhere in Studio. With this in mind, please be very careful and do not add policies that you are unfamiliar with (both their purpose and their syntax).</p>
<p>Any policies you modify here will override any other information you've defined elsewhere in Studio. With this in mind, please be very careful and do not edit policies that you are unfamiliar with (both their purpose and their syntax).</p>
</div>
<div class="bit">
......@@ -110,7 +102,7 @@ editor.render();
<i class="ss-icon ss-symbolicons-block icon icon-warning">&#x26A0;</i>
<p><strong>Note: </strong>Your changes will not take effect until you <strong>save your
progress</strong>. Take care with key and value formatting, as validation is <strong>not implemented</strong>.</p>
progress</strong>. Take care with policy value formatting, as validation is <strong>not implemented</strong>.</p>
</div>
<div class="actions">
......
......@@ -44,12 +44,12 @@
</div>
<div class="main-column">
<article class="unit-body window">
<p class="unit-name-input"><label>Display Name:</label><input type="text" value="${unit.display_name | h}" class="unit-display-name-input" /></p>
<p class="unit-name-input"><label>Display Name:</label><input type="text" value="${unit.display_name_with_default | h}" class="unit-display-name-input" /></p>
<ol class="components">
% for id in components:
<li class="component" data-id="${id}"/>
% endfor
<li class="new-component-item adding">
<li class="new-component-item adding">
<div class="new-component">
<h5>Add New Component</h5>
<ul class="new-component-type">
......@@ -86,7 +86,7 @@
<span class="name"> ${name}</span>
</a>
</li>
% else:
<li class="editor-md">
<a href="#" data-location="${location}">
......@@ -95,7 +95,7 @@
</li>
% endif
% endif
%endfor
</ul>
</div>
......@@ -103,20 +103,20 @@
<div class="tab" id="tab2">
<ul class="new-component-template">
% for name, location, has_markdown, is_empty in templates:
% if not has_markdown:
% if not has_markdown:
% if is_empty:
<li class="editor-manual empty">
<a href="#" data-location="${location}">
<span class="name">${name}</span>
</a>
</li>
% else:
<li class="editor-manual">
<a href="#" data-location="${location}">
<span class="name"> ${name}</span>
</a>
</li>
% endif
% endif
......@@ -147,13 +147,13 @@
<div class="row published-alert">
<p class="edit-draft-message">This unit has been published. To make changes, you must <a href="#" class="create-draft">edit a draft</a>.</p>
<p class="publish-draft-message">This is a draft of the published unit. To update the live version, you must <a href="#" class="publish-draft">replace it with this draft</a>.</p>
</div>
</div>
<div class="row status">
<p>This unit is scheduled to be released to <strong>students</strong>
<p>This unit is scheduled to be released to <strong>students</strong>
% if release_date is not None:
on <strong>${release_date}</strong>
% endif
with the subsection <a href="${reverse('edit_subsection', kwargs={'location': subsection.location})}">"${subsection.display_name}"</a></p>
% endif
with the subsection <a href="${reverse('edit_subsection', kwargs={'location': subsection.location})}">"${subsection.display_name_with_default}"</a></p>
</div>
<div class="row unit-actions">
<a href="#" class="delete-draft delete-button">Delete Draft</a>
......@@ -168,18 +168,18 @@
<div><input type="text" class="url" value="/courseware/${section.url_name}/${subsection.url_name}" disabled /></div>
<ol>
<li>
<a href="#" class="section-item">${section.display_name}</a>
<a href="#" class="section-item">${section.display_name_with_default}</a>
<ol>
<li>
<a href="${reverse('edit_subsection', args=[subsection.location])}" class="section-item">
<span class="folder-icon"></span>
<span class="subsection-name"><span class="subsection-name-value">${subsection.display_name}</span></span>
<span class="subsection-name"><span class="subsection-name-value">${subsection.display_name_with_default}</span></span>
</a>
${units.enum_units(subsection, actions=False, selected=unit.location)}
</li>
</ol>
</li>
</ol>
</ol>
</div>
</div>
</div>
......
......@@ -5,24 +5,24 @@
<div class="wrapper wrapper-left ">
<h1 class="branding"><a href="/">edX Studio</a></h1>
% if context_course:
<% ctx_loc = context_course.location %>
<div class="info-course">
<h2 class="sr">Current Course:</h2>
<a href="${reverse('course_index', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">
<span class="course-org">${ctx_loc.org}</span><span class="course-number">${ctx_loc.course}</span>
<span class="course-title" title="${context_course.display_name}">${context_course.display_name}</span>
<span class="course-title" title="${context_course.display_name_with_default}">${context_course.display_name_with_default}</span>
</a>
</div>
<nav class="nav-course primary nav-dropdown" role="navigation">
<h2 class="sr">${context_course.display_name}'s Navigation:</h2>
<h2 class="sr">${context_course.display_name_with_default}'s Navigation:</h2>
<ol>
<li class="nav-item nav-course-courseware">
<h3 class="title"><span class="label-prefix">Course </span>Content <i class="ss-icon ss-symbolicons-block icon-expand">&#x25BE;</i></h3>
<div class="wrapper wrapper-nav-sub">
<div class="nav-sub">
<ul>
......@@ -32,12 +32,12 @@
<li class="nav-item nav-course-courseware-uploads"><a href="${reverse('asset_index', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">Files &amp; Uploads</a></li>
</ul>
</div>
</div>
</div>
</li>
<li class="nav-item nav-course-settings">
<h3 class="title"><span class="label-prefix">Course </span>Settings <i class="ss-icon ss-symbolicons-block icon-expand">&#x25BE;</i></h3>
<div class="wrapper wrapper-nav-sub">
<div class="nav-sub">
<ul>
......@@ -47,12 +47,12 @@
<li class="nav-item nav-course-settings-advanced"><a href="${reverse('course_advanced_settings', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">Advanced Settings</a></li>
</ul>
</div>
</div>
</div>
</li>
<li class="nav-item nav-course-tools">
<h3 class="title">Tools <i class="ss-icon ss-symbolicons-block icon-expand">&#x25BE;</i></h3>
<div class="wrapper wrapper-nav-sub">
<div class="nav-sub">
<ul>
......@@ -67,16 +67,16 @@
</nav>
% endif
</div>
<div class="wrapper wrapper-right">
% if user.is_authenticated():
% if user.is_authenticated():
<nav class="nav-account nav-is-signedin nav-dropdown">
<h2 class="sr">Currently logged in as:</h2>
<ol>
<li class="nav-item nav-account-username">
<a href="#" class="title">
<span class="account-username">
<i class="ss-icon ss-symbolicons-standard icon-user">&#x1F464;</i>
<i class="ss-icon ss-symbolicons-standard icon-user">&#x1F464;</i>
${ user.username }
</span>
<i class="ss-icon ss-symbolicons-block icon-expand">&#x25BE;</i>
......@@ -112,7 +112,7 @@
</li>
</ol>
</nav>
% endif
% endif
</div>
</header>
</div>
\ No newline at end of file
</div>
% if metadata:
<%
import hashlib
hlskey = hashlib.md5(module.location.url()).hexdigest()
%>
<section class="metadata_edit">
<ul>
% for keyname in editable_metadata_fields:
% for field_name, field_value in editable_metadata_fields.items():
<li>
% if keyname=='source_code':
<a href="#hls-modal-${hlskey}" style="color:yellow;" id="hls-trig-${hlskey}" >Edit High Level Source</a>
% else:
<label>${keyname}:</label>
<input type='text' data-metadata-name='${keyname}' value='${metadata[keyname]}' size='60' />
% endif
% if field_name == 'source_code':
<a href="#hls-modal-${hlskey}" style="color:yellow;" id="hls-trig-${hlskey}" >Edit High Level Source</a>
% else:
<label>${field_name}:</label>
<input type='text' data-metadata-name='${field_name}' value='${field_value}' size='60' />
% endif
</li>
% endfor
</ul>
......@@ -22,4 +21,3 @@
% endif
</section>
% endif
<section class="cal">
<header class="wip">
<ul class="actions">
<li><a href="#">Timeline view</a></li>
<li><a href="#">Multi-Module edit</a></li>
</ul>
<ul>
<li>
<h2>Sort:</h2>
<select>
<option value="">Linear Order</option>
<option value="">Recently Modified</option>
<option value="">Type</option>
<option value="">Alphabetically</option>
</select>
</li>
<li>
<h2>Filter:</h2>
<select>
<option value="">All content</option>
<option value="">Videos</option>
<option value="">Problems</option>
<option value="">Labs</option>
<option value="">Tutorials</option>
<option value="">HTML</option>
</select>
<a href="#" class="more">More</a>
</li>
<li>
<a href="#">Hide goals</a>
</li>
<li class="search">
<input type="search" name="" id="" value="" placeholder="Search" />
</li>
</ul>
</header>
<ol id="weeks">
% for week in weeks:
<li class="week" data-id="${week.location.url()}">
<header>
<h1><a href="#" class="week-edit">${week.url_name}</a></h1>
<ul>
% if 'goals' in week.metadata:
% for goal in week.metadata['goals']:
<li class="goal editable">${goal}</li>
% endfor
% else:
<li class="goal editable">Please create a learning goal for this week</li>
% endif
</ul>
</header>
<ul class="modules">
% for module in week.get_children():
<li class="module"
data-id="${module.location.url()}"
data-type="${module.js_module_name}"
data-preview-type="${module.module_class.js_module_name}">
<a href="#" class="module-edit">${module.display_name}</a>
</li>
% endfor
<%include file="module-dropdown.html"/>
</ul>
</li>
%endfor
</ol>
<section class="new-section">
<a href="#" class="wip" >+ Add New Section</a>
<section class="hidden">
<form>
<ul>
<li>
<input type="text" name="" id="" placeholder="Section title" />
</li>
<li>
<select>
<option>Blank</option>
<option>6.002x</option>
<option>6.00x</option>
</select>
</li>
<li>
<input type="submit" value="Save and edit week" class="edit-week" />
<div>
<a href="#" class="close">Save without edit</a>
<a href="#" class="close">cancel</a>
</div>
</li>
</ul>
</form>
</section>
</section>
</section>
......@@ -40,7 +40,7 @@
<a href="#" class="module-edit"
data-id="${child.location.url()}"
data-type="${child.js_module_name}"
data-preview-type="${child.module_class.js_module_name}">${child.display_name}</a>
data-preview-type="${child.module_class.js_module_name}">${child.display_name_with_default}</a>
<a href="#" class="draggable">handle</a>
</li>
%endfor
......
......@@ -22,7 +22,7 @@ This def will enumerate through a passed in subsection and list all of the units
<div class="section-item ${selected_class}">
<a href="${reverse('edit_unit', args=[unit.location])}" class="${unit_state}-item">
<span class="${unit.category}-icon"></span>
<span class="unit-name">${unit.display_name}</span>
<span class="unit-name">${unit.display_name_with_default}</span>
</a>
% if actions:
<div class="item-actions">
......@@ -39,7 +39,7 @@ This def will enumerate through a passed in subsection and list all of the units
</a>
</li>
</ol>
</%def>
</%def>
......@@ -107,3 +107,9 @@ if settings.ENABLE_JASMINE:
urlpatterns = urlpatterns + (url(r'^_jasmine/', include('django_jasmine.urls')),)
urlpatterns = patterns(*urlpatterns)
#Custom error pages
handler404 = 'contentstore.views.render_404'
handler500 = 'contentstore.views.render_500'
"""
Namespace defining common fields used by Studio for all blocks
"""
import datetime
from xblock.core import Namespace, Boolean, Scope, ModelType, String
class StringyBoolean(Boolean):
"""
Reads strings from JSON as booleans.
If the string is 'true' (case insensitive), then return True,
otherwise False.
JSON values that aren't strings are returned as is
"""
def from_json(self, value):
if isinstance(value, basestring):
return value.lower() == 'true'
return value
class DateTuple(ModelType):
"""
ModelType that stores datetime objects as time tuples
"""
def from_json(self, value):
return datetime.datetime(*value[0:6])
def to_json(self, value):
if value is None:
return None
return list(value.timetuple())
class CmsNamespace(Namespace):
"""
Namespace with fields common to all blocks in Studio
"""
is_draft = Boolean(help="Whether this module is a draft", default=False, scope=Scope.settings)
published_date = DateTuple(help="Date when the module was published", scope=Scope.settings)
published_by = String(help="Id of the user who published this module", scope=Scope.settings)
empty = StringyBoolean(help="Whether this is an empty template", scope=Scope.settings, default=False)
......@@ -76,7 +76,7 @@ class TestCohorts(django.test.TestCase):
"id": to_id(name)})
for name in discussions)
course.metadata["discussion_topics"] = topics
course.discussion_topics = topics
d = {"cohorted": cohorted}
if cohorted_discussions is not None:
......@@ -88,7 +88,7 @@ class TestCohorts(django.test.TestCase):
if auto_cohort_groups is not None:
d["auto_cohort_groups"] = auto_cohort_groups
course.metadata["cohort_config"] = d
course.cohort_config = d
def setUp(self):
......
......@@ -4,7 +4,7 @@ import os
from django.test.utils import override_settings
from tempfile import NamedTemporaryFile
from status import get_site_status_msg
from .status import get_site_status_msg
# Get a name where we can put test files
TMP_FILE = NamedTemporaryFile(delete=False)
......
......@@ -44,9 +44,8 @@ from collections import namedtuple
from courseware.courses import get_courses, sort_by_announcement
from courseware.access import has_access
from courseware.models import StudentModuleCache
from courseware.views import get_module_for_descriptor, jump_to
from courseware.module_render import get_instance_module
from courseware.model_data import ModelDataCache
from statsd import statsd
......@@ -318,7 +317,7 @@ def change_enrollment(request):
if not has_access(user, course, 'enroll'):
return {'success': False,
'error': 'enrollment in {} not allowed at this time'
.format(course.display_name)}
.format(course.display_name_with_default)}
org, course_num, run = course_id.split("/")
statsd.increment("common.student.enrollment",
......@@ -1071,14 +1070,14 @@ def accept_name_change(request):
@csrf_exempt
def test_center_login(request):
# errors are returned by navigating to the error_url, adding a query parameter named "code"
# errors are returned by navigating to the error_url, adding a query parameter named "code"
# which contains the error code describing the exceptional condition.
def makeErrorURL(error_url, error_code):
log.error("generating error URL with error code {}".format(error_code))
return "{}?code={}".format(error_url, error_code);
# get provided error URL, which will be used as a known prefix for returning error messages to the
# Pearson shell.
# Pearson shell.
error_url = request.POST.get("errorURL")
# TODO: check that the parameters have not been tampered with, by comparing the code provided by Pearson
......@@ -1089,12 +1088,12 @@ def test_center_login(request):
# calculate SHA for query string
# TODO: figure out how to get the original query string, so we can hash it and compare.
if 'clientCandidateID' not in request.POST:
return HttpResponseRedirect(makeErrorURL(error_url, "missingClientCandidateID"));
client_candidate_id = request.POST.get("clientCandidateID")
# TODO: check remaining parameters, and maybe at least log if they're not matching
# expected values....
# registration_id = request.POST.get("registrationID")
......@@ -1108,12 +1107,12 @@ def test_center_login(request):
return HttpResponseRedirect(makeErrorURL(error_url, "invalidClientCandidateID"));
# find testcenter_registration that matches the provided exam code:
# Note that we could rely in future on either the registrationId or the exam code,
# or possibly both. But for now we know what to do with an ExamSeriesCode,
# Note that we could rely in future on either the registrationId or the exam code,
# or possibly both. But for now we know what to do with an ExamSeriesCode,
# while we currently have no record of RegistrationID values at all.
if 'vueExamSeriesCode' not in request.POST:
# we are not allowed to make up a new error code, according to Pearson,
# so instead of "missingExamSeriesCode", we use a valid one that is
# we are not allowed to make up a new error code, according to Pearson,
# so instead of "missingExamSeriesCode", we use a valid one that is
# inaccurate but at least distinct. (Sigh.)
log.error("missing exam series code for cand ID {}".format(client_candidate_id))
return HttpResponseRedirect(makeErrorURL(error_url, "missingPartnerID"));
......@@ -1127,11 +1126,11 @@ def test_center_login(request):
if not registrations:
log.error("not able to find exam registration for exam {} and cand ID {}".format(exam_series_code, client_candidate_id))
return HttpResponseRedirect(makeErrorURL(error_url, "noTestsAssigned"));
# TODO: figure out what to do if there are more than one registrations....
# for now, just take the first...
registration = registrations[0]
course_id = registration.course_id
course = course_from_id(course_id) # assume it will be found....
if not course:
......@@ -1149,19 +1148,19 @@ def test_center_login(request):
if not timelimit_descriptor:
log.error("cand {} on exam {} for course {}: descriptor not found for location {}".format(client_candidate_id, exam_series_code, course_id, location))
return HttpResponseRedirect(makeErrorURL(error_url, "missingClientProgram"));
timelimit_module_cache = StudentModuleCache.cache_for_descriptor_descendents(course_id, testcenteruser.user,
timelimit_descriptor, depth=None)
timelimit_module = get_module_for_descriptor(request.user, request, timelimit_descriptor,
timelimit_module_cache = ModelDataCache.cache_for_descriptor_descendents(course_id, testcenteruser.user,
timelimit_descriptor, depth=None)
timelimit_module = get_module_for_descriptor(request.user, request, timelimit_descriptor,
timelimit_module_cache, course_id, position=None)
if not timelimit_module.category == 'timelimit':
log.error("cand {} on exam {} for course {}: non-timelimit module at location {}".format(client_candidate_id, exam_series_code, course_id, location))
return HttpResponseRedirect(makeErrorURL(error_url, "missingClientProgram"));
if timelimit_module and timelimit_module.has_ended:
log.warning("cand {} on exam {} for course {}: test already over at {}".format(client_candidate_id, exam_series_code, course_id, timelimit_module.ending_at))
return HttpResponseRedirect(makeErrorURL(error_url, "allTestsTaken"));
# check if we need to provide an accommodation:
time_accommodation_mapping = {'ET12ET' : 'ADDHALFTIME',
'ET30MN' : 'ADD30MIN',
......@@ -1174,27 +1173,24 @@ def test_center_login(request):
# special, hard-coded client ID used by Pearson shell for testing:
if client_candidate_id == "edX003671291147":
time_accommodation_code = 'TESTING'
if time_accommodation_code:
timelimit_module.accommodation_code = time_accommodation_code
instance_module = get_instance_module(course_id, testcenteruser.user, timelimit_module, timelimit_module_cache)
instance_module.state = timelimit_module.get_instance_state()
instance_module.save()
log.info("cand {} on exam {} for course {}: receiving accommodation {}".format(client_candidate_id, exam_series_code, course_id, time_accommodation_code))
# UGLY HACK!!!
# Login assumes that authentication has occurred, and that there is a
# Login assumes that authentication has occurred, and that there is a
# backend annotation on the user object, indicating which backend
# against which the user was authenticated. We're authenticating here
# against the registration entry, and assuming that the request given
# this information is correct, we allow the user to be logged in
# without a password. This could all be formalized in a backend object
# that does the above checking.
# that does the above checking.
# TODO: (brian) create a backend class to do this.
# testcenteruser.user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__)
testcenteruser.user.backend = "%s.%s" % ("TestcenterAuthenticationModule", "TestcenterAuthenticationClass")
# testcenteruser.user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__)
testcenteruser.user.backend = "%s.%s" % ("TestcenterAuthenticationModule", "TestcenterAuthenticationClass")
login(request, testcenteruser.user)
# And start the test:
return jump_to(request, course_id, location)
......
......@@ -7,13 +7,14 @@ from xmodule.modulestore.django import modulestore
from time import gmtime
from uuid import uuid4
from xmodule.timeparse import stringify_time
from xmodule.modulestore.inheritance import own_metadata
class GroupFactory(Factory):
FACTORY_FOR = Group
name = 'staff_MITx/999/Robot_Super_Course'
class UserProfileFactory(Factory):
FACTORY_FOR = UserProfile
......@@ -81,18 +82,17 @@ class XModuleCourseFactory(Factory):
# This metadata code was copied from cms/djangoapps/contentstore/views.py
if display_name is not None:
new_course.metadata['display_name'] = display_name
new_course.display_name = display_name
new_course.metadata['data_dir'] = uuid4().hex
new_course.metadata['start'] = stringify_time(gmtime())
new_course.lms.start = gmtime()
new_course.tabs = [{"type": "courseware"},
{"type": "course_info", "name": "Course Info"},
{"type": "course_info", "name": "Course Info"},
{"type": "discussion", "name": "Discussion"},
{"type": "wiki", "name": "Wiki"},
{"type": "progress", "name": "Progress"}]
# Update the data in the mongo datastore
store.update_metadata(new_course.location.url(), new_course.own_metadata)
store.update_metadata(new_course.location.url(), own_metadata(new_course))
return new_course
......@@ -139,17 +139,14 @@ class XModuleItemFactory(Factory):
new_item = store.clone_item(template, dest_location)
# TODO: This needs to be deleted when we have proper storage for static content
new_item.metadata['data_dir'] = parent.metadata['data_dir']
# replace the display name with an optional parameter passed in from the caller
if display_name is not None:
new_item.metadata['display_name'] = display_name
new_item.display_name = display_name
store.update_metadata(new_item.location.url(), new_item.own_metadata)
store.update_metadata(new_item.location.url(), own_metadata(new_item))
if new_item.location.category not in DETACHED_CATEGORIES:
store.update_children(parent_location, parent.definition.get('children', []) + [new_item.location.url()])
store.update_children(parent_location, parent.children + [new_item.location.url()])
return new_item
......
from lettuce import world, step
from factories import *
from .factories import *
from lettuce.django import django_url
from django.contrib.auth.models import User
from student.models import CourseEnrollment
......
......@@ -33,7 +33,7 @@ def wrap_xmodule(get_html, module, template, context=None):
def _get_html():
context.update({
'content': get_html(),
'display_name': module.metadata.get('display_name') if module.metadata is not None else None,
'display_name': module.display_name,
'class_': module.__class__.__name__,
'module_name': module.js_module_name
})
......@@ -108,42 +108,25 @@ def add_histogram(get_html, module, user):
histogram = grade_histogram(module_id)
render_histogram = len(histogram) > 0
# TODO (ichuang): Remove after fall 2012 LMS migration done
if settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'):
[filepath, filename] = module.definition.get('filename', ['', None])
osfs = module.system.filestore
if filename is not None and osfs.exists(filename):
# if original, unmangled filename exists then use it (github
# doesn't like symlinks)
filepath = filename
data_dir = osfs.root_path.rsplit('/')[-1]
giturl = module.metadata.get('giturl', 'https://github.com/MITx')
edit_link = "%s/%s/tree/master/%s" % (giturl, data_dir, filepath)
else:
edit_link = False
# Need to define all the variables that are about to be used
giturl = ""
data_dir = ""
source_file = module.metadata.get('source_file', '') # source used to generate the problem XML, eg latex or word
source_file = module.lms.source_file # source used to generate the problem XML, eg latex or word
# useful to indicate to staff if problem has been released or not
# TODO (ichuang): use _has_access_descriptor.can_load in lms.courseware.access, instead of now>mstart comparison here
now = time.gmtime()
is_released = "unknown"
mstart = getattr(module.descriptor, 'start')
mstart = module.descriptor.lms.start
if mstart is not None:
is_released = "<font color='red'>Yes!</font>" if (now > mstart) else "<font color='green'>Not yet</font>"
staff_context = {'definition': module.definition.get('data'),
'metadata': json.dumps(module.metadata, indent=4),
staff_context = {'fields': [(field.name, getattr(module, field.name)) for field in module.fields],
'lms_fields': [(field.name, getattr(module.lms, field.name)) for field in module.lms.fields],
'location': module.location,
'xqa_key': module.metadata.get('xqa_key', ''),
'xqa_key': module.lms.xqa_key,
'source_file': source_file,
'source_url': '%s/%s/tree/master/%s' % (giturl, data_dir, source_file),
'category': str(module.__class__.__name__),
# Template uses element_id in js function names, so can't allow dashes
'element_id': module.location.html_id().replace('-', '_'),
'edit_link': edit_link,
'user': user,
'xqa_server': settings.MITX_FEATURES.get('USE_XQA_SERVER', 'http://xqa:server@content-qa.mitx.mit.edu/xqa'),
'histogram': json.dumps(histogram),
......
......@@ -39,11 +39,11 @@ import verifiers
import verifiers.draganddrop
import calc
from correctmap import CorrectMap
from .correctmap import CorrectMap
import eia
import inputtypes
import customrender
from util import contextualize_text, convert_files_to_filenames
from .util import contextualize_text, convert_files_to_filenames
import xqueue_interface
# to be replaced with auto-registering
......@@ -78,7 +78,7 @@ global_context = {'random': random,
# These should be removed from HTML output, including all subelements
html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup", "openendedparam", "openendedrubric"]
log = logging.getLogger('mitx.' + __name__)
log = logging.getLogger(__name__)
#-----------------------------------------------------------------------------
# main class for this module
......@@ -108,6 +108,8 @@ class LoncapaProblem(object):
self.do_reset()
self.problem_id = id
self.system = system
if self.system is None:
raise Exception()
self.seed = seed
if state:
......
......@@ -12,8 +12,8 @@ from path import path
from cStringIO import StringIO
from collections import defaultdict
from calc import UndefinedVariable
from capa_problem import LoncapaProblem
from .calc import UndefinedVariable
from .capa_problem import LoncapaProblem
from mako.lookup import TemplateLookup
logging.basicConfig(format="%(levelname)s %(message)s")
......
......@@ -2,7 +2,7 @@ import codecs
from fractions import Fraction
import unittest
from chemcalc import (compare_chemical_expression, divide_chemical_expression,
from .chemcalc import (compare_chemical_expression, divide_chemical_expression,
render_to_html, chemical_equations_equal)
import miller
......@@ -277,7 +277,6 @@ class Test_Render_Equations(unittest.TestCase):
def test_render9(self):
s = "5[Ni(NH3)4]^2+ + 5/2SO4^2-"
#import ipdb; ipdb.set_trace()
out = render_to_html(s)
correct = u'<span class="math">5[Ni(NH<sub>3</sub>)<sub>4</sub>]<sup>2+</sup>+<sup>5</sup>&frasl;<sub>2</sub>SO<sub>4</sub><sup>2-</sup></span>'
log(out + ' ------- ' + correct, 'html')
......
......@@ -47,7 +47,7 @@ class CorrectMap(object):
queuestate=None, **kwargs):
if answer_id is not None:
self.cmap[answer_id] = {'correctness': correctness,
self.cmap[str(answer_id)] = {'correctness': correctness,
'npoints': npoints,
'msg': msg,
'hint': hint,
......
......@@ -6,7 +6,7 @@ These tags do not have state, so they just get passed the system (for access to
and the xml element.
"""
from registry import TagRegistry
from .registry import TagRegistry
import logging
import re
......@@ -15,9 +15,9 @@ import json
from lxml import etree
import xml.sax.saxutils as saxutils
from registry import TagRegistry
from .registry import TagRegistry
log = logging.getLogger('mitx.' + __name__)
log = logging.getLogger(__name__)
registry = TagRegistry()
......
......@@ -47,10 +47,10 @@ import sys
import os
import pyparsing
from registry import TagRegistry
from .registry import TagRegistry
from capa.chem import chemcalc
log = logging.getLogger('mitx.' + __name__)
log = logging.getLogger(__name__)
#########################################################################
......@@ -857,6 +857,10 @@ class DragAndDropInput(InputTypeBase):
if tag_type == 'draggable' and not self.no_labels:
dic['label'] = dic['label'] or dic['id']
if tag_type == 'draggable':
dic['target_fields'] = [parse(target, 'target') for target in
tag.iterchildren('target')]
return dic
# add labels to images?:
......
......@@ -28,15 +28,15 @@ from collections import namedtuple
from shapely.geometry import Point, MultiPoint
# specific library imports
from calc import evaluator, UndefinedVariable
from correctmap import CorrectMap
from .calc import evaluator, UndefinedVariable
from .correctmap import CorrectMap
from datetime import datetime
from util import *
from .util import *
from lxml import etree
from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful Soup!!! FIXME?
import xqueue_interface
log = logging.getLogger('mitx.' + __name__)
log = logging.getLogger(__name__)
#-----------------------------------------------------------------------------
......@@ -231,16 +231,14 @@ class LoncapaResponse(object):
# hint specified by function?
hintfn = hintgroup.get('hintfn')
if hintfn:
'''
Hint is determined by a function defined in the <script> context; evaluate
that function to obtain list of hint, hintmode for each answer_id.
# Hint is determined by a function defined in the <script> context; evaluate
# that function to obtain list of hint, hintmode for each answer_id.
The function should take arguments (answer_ids, student_answers, new_cmap, old_cmap)
and it should modify new_cmap as appropriate.
# The function should take arguments (answer_ids, student_answers, new_cmap, old_cmap)
# and it should modify new_cmap as appropriate.
We may extend this in the future to add another argument which provides a
callback procedure to a social hint generation system.
'''
# We may extend this in the future to add another argument which provides a
# callback procedure to a social hint generation system.
if not hintfn in self.context:
msg = 'missing specified hint function %s in script context' % hintfn
msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline', '<unavailable>')
......@@ -329,7 +327,7 @@ class LoncapaResponse(object):
""" Render a <div> for a message that applies to the entire response.
*response_msg* is a string, which may contain XHTML markup
Returns an etree element representing the response message <div> """
# First try wrapping the text in a <div> and parsing
# it as an XHTML tree
......@@ -872,7 +870,7 @@ class CustomResponse(LoncapaResponse):
Custom response. The python code to be run should be in <answer>...</answer>
or in a <script>...</script>
'''
snippets = [{'snippet': """<customresponse>
snippets = [{'snippet': r"""<customresponse>
<text>
<br/>
Suppose that \(I(t)\) rises from \(0\) to \(I_S\) at a time \(t_0 \neq 0\)
......@@ -1104,7 +1102,7 @@ def sympy_check2():
# the form:
# {'overall_message': STRING,
# 'input_list': [{ 'ok': BOOLEAN, 'msg': STRING }, ...] }
#
#
# This allows the function to return an 'overall message'
# that applies to the entire problem, as well as correct/incorrect
# status and messages for individual inputs
......@@ -1197,7 +1195,7 @@ class SymbolicResponse(CustomResponse):
"""
Symbolic math response checking, using symmath library.
"""
snippets = [{'snippet': '''<problem>
snippets = [{'snippet': r'''<problem>
<text>Compute \[ \exp\left(-i \frac{\theta}{2} \left[ \begin{matrix} 0 & 1 \\ 1 & 0 \end{matrix} \right] \right) \]
and give the resulting \(2\times 2\) matrix: <br/>
<symbolicresponse answer="">
......@@ -1988,7 +1986,7 @@ class AnnotationResponse(LoncapaResponse):
for inputfield in self.inputfields:
option_scoring = dict([(option['id'], {
'correctness': choices.get(option['choice']),
'correctness': choices.get(option['choice']),
'points': scoring.get(option['choice'])
}) for option in self._find_options(inputfield) ])
......
......@@ -7,7 +7,7 @@ import json
import mock
from capa.capa_problem import LoncapaProblem
from response_xml_factory import StringResponseXMLFactory, CustomResponseXMLFactory
from .response_xml_factory import StringResponseXMLFactory, CustomResponseXMLFactory
from . import test_system
class CapaHtmlRenderTest(unittest.TestCase):
......
......@@ -557,14 +557,14 @@ class DragAndDropTest(unittest.TestCase):
"target_outline": "false",
"base_image": "/static/images/about_1.png",
"draggables": [
{"can_reuse": "", "label": "Label 1", "id": "1", "icon": ""},
{"can_reuse": "", "label": "cc", "id": "name_with_icon", "icon": "/static/images/cc.jpg", },
{"can_reuse": "", "label": "arrow-left", "id": "with_icon", "icon": "/static/images/arrow-left.png", "can_reuse": ""},
{"can_reuse": "", "label": "Label2", "id": "5", "icon": "", "can_reuse": ""},
{"can_reuse": "", "label": "Mute", "id": "2", "icon": "/static/images/mute.png", "can_reuse": ""},
{"can_reuse": "", "label": "spinner", "id": "name_label_icon3", "icon": "/static/images/spinner.gif", "can_reuse": ""},
{"can_reuse": "", "label": "Star", "id": "name4", "icon": "/static/images/volume.png", "can_reuse": ""},
{"can_reuse": "", "label": "Label3", "id": "7", "icon": "", "can_reuse": ""}],
{"can_reuse": "", "label": "Label 1", "id": "1", "icon": "", "target_fields": []},
{"can_reuse": "", "label": "cc", "id": "name_with_icon", "icon": "/static/images/cc.jpg", "target_fields": []},
{"can_reuse": "", "label": "arrow-left", "id": "with_icon", "icon": "/static/images/arrow-left.png", "can_reuse": "", "target_fields": []},
{"can_reuse": "", "label": "Label2", "id": "5", "icon": "", "can_reuse": "", "target_fields": []},
{"can_reuse": "", "label": "Mute", "id": "2", "icon": "/static/images/mute.png", "can_reuse": "", "target_fields": []},
{"can_reuse": "", "label": "spinner", "id": "name_label_icon3", "icon": "/static/images/spinner.gif", "can_reuse": "", "target_fields": []},
{"can_reuse": "", "label": "Star", "id": "name4", "icon": "/static/images/volume.png", "can_reuse": "", "target_fields": []},
{"can_reuse": "", "label": "Label3", "id": "7", "icon": "", "can_reuse": "", "target_fields": []}],
"one_per_target": "True",
"targets": [
{"y": "90", "x": "210", "id": "t1", "w": "90", "h": "90"},
......
from calc import evaluator, UndefinedVariable
from .calc import evaluator, UndefinedVariable
#-----------------------------------------------------------------------------
#
......
......@@ -27,6 +27,49 @@ values are (x,y) coordinates of centers of dragged images.
import json
def flat_user_answer(user_answer):
"""
Convert nested `user_answer` to flat format.
{'up': {'first': {'p': 'p_l'}}}
to
{'up': 'p_l[p][first]'}
"""
def parse_user_answer(answer):
key = answer.keys()[0]
value = answer.values()[0]
if isinstance(value, dict):
# Make complex value:
# Example:
# Create like 'p_l[p][first]' from {'first': {'p': 'p_l'}
complex_value_list = []
v_value = value
while isinstance(v_value, dict):
v_key = v_value.keys()[0]
v_value = v_value.values()[0]
complex_value_list.append(v_key)
complex_value = '{0}'.format(v_value)
for i in reversed(complex_value_list):
complex_value = '{0}[{1}]'.format(complex_value, i)
res = {key: complex_value}
return res
else:
return answer
result = []
for answer in user_answer:
parse_answer = parse_user_answer(answer)
result.append(parse_answer)
return result
class PositionsCompare(list):
""" Class for comparing positions.
......@@ -116,37 +159,36 @@ class DragAndDrop(object):
# Number of draggables in user_groups may be differ that in
# correct_groups, that is incorrect, except special case with 'number'
for groupname, draggable_ids in self.correct_groups.items():
for index, draggable_ids in enumerate(self.correct_groups):
# 'number' rule special case
# for reusable draggables we may get in self.user_groups
# {'1': [u'2', u'2', u'2'], '0': [u'1', u'1'], '2': [u'3']}
# if '+number' is in rule - do not remove duplicates and strip
# '+number' from rule
current_rule = self.correct_positions[groupname].keys()[0]
current_rule = self.correct_positions[index].keys()[0]
if 'number' in current_rule:
rule_values = self.correct_positions[groupname][current_rule]
rule_values = self.correct_positions[index][current_rule]
# clean rule, do not do clean duplicate items
self.correct_positions[groupname].pop(current_rule, None)
self.correct_positions[index].pop(current_rule, None)
parsed_rule = current_rule.replace('+', '').replace('number', '')
self.correct_positions[groupname][parsed_rule] = rule_values
self.correct_positions[index][parsed_rule] = rule_values
else: # remove dublicates
self.user_groups[groupname] = list(set(self.user_groups[groupname]))
self.user_groups[index] = list(set(self.user_groups[index]))
if sorted(draggable_ids) != sorted(self.user_groups[groupname]):
if sorted(draggable_ids) != sorted(self.user_groups[index]):
return False
# Check that in every group, for rule of that group, user positions of
# every element are equal with correct positions
for groupname in self.correct_groups:
for index, _ in enumerate(self.correct_groups):
rules_executed = 0
for rule in ('exact', 'anyof', 'unordered_equal'):
# every group has only one rule
if self.correct_positions[groupname].get(rule, None):
if self.correct_positions[index].get(rule, None):
rules_executed += 1
if not self.compare_positions(
self.correct_positions[groupname][rule],
self.user_positions[groupname]['user'], flag=rule):
self.correct_positions[index][rule],
self.user_positions[index]['user'], flag=rule):
return False
if not rules_executed: # no correct rules for current group
# probably xml content mistake - wrong rules names
......@@ -248,7 +290,7 @@ class DragAndDrop(object):
correct_answer = {'name4': 't1',
'name_with_icon': 't1',
'5': 't2',
'7':'t2'}
'7': 't2'}
It is draggable_name: dragable_position mapping.
......@@ -284,24 +326,25 @@ class DragAndDrop(object):
Args:
user_answer: json
correct_answer: dict or list
correct_answer: dict or list
"""
self.correct_groups = dict() # correct groups from xml
self.correct_positions = dict() # correct positions for comparing
self.user_groups = dict() # will be populated from user answer
self.user_positions = dict() # will be populated from user answer
self.correct_groups = [] # Correct groups from xml.
self.correct_positions = [] # Correct positions for comparing.
self.user_groups = [] # Will be populated from user answer.
self.user_positions = [] # Will be populated from user answer.
# convert from dict answer format to list format
# Convert from dict answer format to list format.
if isinstance(correct_answer, dict):
tmp = []
for key, value in correct_answer.items():
tmp_dict = {'draggables': [], 'targets': [], 'rule': 'exact'}
tmp_dict['draggables'].append(key)
tmp_dict['targets'].append(value)
tmp.append(tmp_dict)
tmp.append({
'draggables': [key],
'targets': [value],
'rule': 'exact'})
correct_answer = tmp
# Convert string `user_answer` to object.
user_answer = json.loads(user_answer)
# This dictionary will hold a key for each draggable the user placed on
......@@ -309,27 +352,32 @@ class DragAndDrop(object):
# correct_answer entries. If the draggable is mentioned in at least one
# correct_answer entry, the value is False.
# default to consider every user answer excess until proven otherwise.
self.excess_draggables = dict((users_draggable.keys()[0],True)
for users_draggable in user_answer['draggables'])
# create identical data structures from user answer and correct answer
for i in xrange(0, len(correct_answer)):
groupname = str(i)
self.correct_groups[groupname] = correct_answer[i]['draggables']
self.correct_positions[groupname] = {correct_answer[i]['rule']:
correct_answer[i]['targets']}
self.user_groups[groupname] = []
self.user_positions[groupname] = {'user': []}
for draggable_dict in user_answer['draggables']:
# draggable_dict is 1-to-1 {draggable_name: position}
self.excess_draggables = dict((users_draggable.keys()[0],True)
for users_draggable in user_answer)
# Convert nested `user_answer` to flat format.
user_answer = flat_user_answer(user_answer)
# Create identical data structures from user answer and correct answer.
for answer in correct_answer:
user_groups_data = []
user_positions_data = []
for draggable_dict in user_answer:
# Draggable_dict is 1-to-1 {draggable_name: position}.
draggable_name = draggable_dict.keys()[0]
if draggable_name in self.correct_groups[groupname]:
self.user_groups[groupname].append(draggable_name)
self.user_positions[groupname]['user'].append(
if draggable_name in answer['draggables']:
user_groups_data.append(draggable_name)
user_positions_data.append(
draggable_dict[draggable_name])
# proved that this is not excess
self.excess_draggables[draggable_name] = False
self.correct_groups.append(answer['draggables'])
self.correct_positions.append({answer['rule']: answer['targets']})
self.user_groups.append(user_groups_data)
self.user_positions.append({'user': user_positions_data})
def grade(user_input, correct_answer):
""" Creates DragAndDrop instance from user_input and correct_answer and
calls DragAndDrop.grade for grading.
......
......@@ -7,7 +7,7 @@ import logging
import requests
log = logging.getLogger('mitx.' + __name__)
log = logging.getLogger(__name__)
dateformat = '%Y%m%d%H%M%S'
......
......@@ -4,5 +4,5 @@ setup(
name="capa",
version="0.1",
packages=find_packages(exclude=["tests"]),
install_requires=['distribute==0.6.34', 'pyparsing==1.5.6'],
install_requires=['distribute==0.6.30', 'pyparsing==1.5.6'],
)
......@@ -28,6 +28,7 @@ setup(
"image = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"error = xmodule.error_module:ErrorDescriptor",
"peergrading = xmodule.peer_grading_module:PeerGradingDescriptor",
"poll_question = xmodule.poll_module:PollDescriptor",
"problem = xmodule.capa_module:CapaDescriptor",
"problemset = xmodule.seq_module:SequenceDescriptor",
"randomize = xmodule.randomize_module:RandomizeDescriptor",
......@@ -45,6 +46,7 @@ setup(
"static_tab = xmodule.html_module:StaticTabDescriptor",
"custom_tag_template = xmodule.raw_module:RawDescriptor",
"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",
......
import json
import random
import logging
from lxml import etree
......@@ -7,6 +6,7 @@ from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from xmodule.xml_module import XmlDescriptor
from xmodule.exceptions import InvalidDefinitionError
from xblock.core import String, Scope, Object, BlockScope
DEFAULT = "_DEFAULT_GROUP"
......@@ -31,29 +31,42 @@ def group_from_value(groups, v):
return g
class ABTestModule(XModule):
class ABTestFields(object):
group_portions = Object(help="What proportions of students should go in each group", default={DEFAULT: 1}, scope=Scope.content)
group_assignments = Object(help="What group this user belongs to", scope=Scope.student_preferences, default={})
group_content = Object(help="What content to display to each group", scope=Scope.content, default={DEFAULT: []})
experiment = String(help="Experiment that this A/B test belongs to", scope=Scope.content)
has_children = True
class ABTestModule(ABTestFields, XModule):
"""
Implements an A/B test with an aribtrary number of competing groups
"""
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)
if shared_state is None:
def __init__(self, *args, **kwargs):
XModule.__init__(self, *args, **kwargs)
if self.group is None:
self.group = group_from_value(
self.definition['data']['group_portions'].items(),
self.group_portions.items(),
random.uniform(0, 1)
)
else:
shared_state = json.loads(shared_state)
self.group = shared_state['group']
def get_shared_state(self):
return json.dumps({'group': self.group})
@property
def group(self):
return self.group_assignments.get(self.experiment)
@group.setter
def group(self, value):
self.group_assignments[self.experiment] = value
@group.deleter
def group(self):
del self.group_assignments[self.experiment]
def get_child_descriptors(self):
active_locations = set(self.definition['data']['group_content'][self.group])
active_locations = set(self.group_content[self.group])
return [desc for desc in self.descriptor.get_children() if desc.location.url() in active_locations]
def displayable_items(self):
......@@ -64,43 +77,11 @@ class ABTestModule(XModule):
# TODO (cpennington): Use Groups should be a first class object, rather than being
# managed by ABTests
class ABTestDescriptor(RawDescriptor, XmlDescriptor):
class ABTestDescriptor(ABTestFields, RawDescriptor, XmlDescriptor):
module_class = ABTestModule
template_dir_name = "abtest"
def __init__(self, system, definition=None, **kwargs):
"""
definition is a dictionary with the following layout:
{'data': {
'experiment': 'the name of the experiment',
'group_portions': {
'group_a': 0.1,
'group_b': 0.2
},
'group_contents': {
'group_a': [
'url://for/content/module/1',
'url://for/content/module/2',
],
'group_b': [
'url://for/content/module/3',
],
DEFAULT: [
'url://for/default/content/1'
]
}
},
'children': [
'url://for/content/module/1',
'url://for/content/module/2',
'url://for/content/module/3',
'url://for/default/content/1',
]}
"""
kwargs['shared_state_key'] = definition['data']['experiment']
RawDescriptor.__init__(self, system, definition, **kwargs)
@classmethod
def definition_from_xml(cls, xml_object, system):
"""
......@@ -118,19 +99,16 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor):
"ABTests must specify an experiment. Not found in:\n{xml}"
.format(xml=etree.tostring(xml_object, pretty_print=True)))
definition = {
'data': {
'experiment': experiment,
'group_portions': {},
'group_content': {DEFAULT: []},
},
'children': []}
group_portions = {}
group_content = {}
children = []
for group in xml_object:
if group.tag == 'default':
name = DEFAULT
else:
name = group.get('name')
definition['data']['group_portions'][name] = float(group.get('portion', 0))
group_portions[name] = float(group.get('portion', 0))
child_content_urls = []
for child in group:
......@@ -140,29 +118,33 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor):
log.exception("Unable to load child when parsing ABTest. Continuing...")
continue
definition['data']['group_content'][name] = child_content_urls
definition['children'].extend(child_content_urls)
group_content[name] = child_content_urls
children.extend(child_content_urls)
default_portion = 1 - sum(
portion for (name, portion) in definition['data']['group_portions'].items())
portion for (name, portion) in group_portions.items()
)
if default_portion < 0:
raise InvalidDefinitionError("ABTest portions must add up to less than or equal to 1")
definition['data']['group_portions'][DEFAULT] = default_portion
definition['children'].sort()
group_portions[DEFAULT] = default_portion
children.sort()
return definition
return {
'group_portions': group_portions,
'group_content': group_content,
}, children
def definition_to_xml(self, resource_fs):
xml_object = etree.Element('abtest')
xml_object.set('experiment', self.definition['data']['experiment'])
for name, group in self.definition['data']['group_content'].items():
xml_object.set('experiment', self.experiment)
for name, group in self.group_content.items():
if name == DEFAULT:
group_elem = etree.SubElement(xml_object, 'default')
else:
group_elem = etree.SubElement(xml_object, 'group', attrib={
'portion': str(self.definition['data']['group_portions'][name]),
'portion': str(self.group_portions[name]),
'name': name,
})
......@@ -172,6 +154,5 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor):
return xml_object
def has_dynamic_children(self):
return True
......@@ -5,13 +5,17 @@ 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
from xblock.core import Scope, String
log = logging.getLogger(__name__)
class AnnotatableModule(XModule):
class AnnotatableFields(object):
data = String(help="XML data for the annotation", scope=Scope.content)
class AnnotatableModule(AnnotatableFields, 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'),
......@@ -22,6 +26,17 @@ class AnnotatableModule(XModule):
css = {'scss': [resource_string(__name__, 'css/annotatable/display.scss')]}
icon_class = 'annotatable'
def __init__(self, *args, **kwargs):
XModule.__init__(self, *args, **kwargs)
xmltree = etree.fromstring(self.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']
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.
......@@ -103,7 +118,7 @@ class AnnotatableModule(XModule):
def get_html(self):
""" Renders parameters to template. """
context = {
'display_name': self.display_name,
'display_name': self.display_name_with_default,
'element_id': self.element_id,
'instructions_html': self.instructions,
'content_html': self._render_content()
......@@ -111,19 +126,8 @@ class AnnotatableModule(XModule):
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):
class AnnotatableDescriptor(AnnotatableFields, RawDescriptor):
module_class = AnnotatableModule
stores_state = True
template_dir_name = "annotatable"
......
"""
These modules exist to translate old format XML into newer, semantic forms
"""
from x_module import XModuleDescriptor
from .x_module import XModuleDescriptor
from lxml import etree
from functools import wraps
import logging
......
......@@ -6,19 +6,72 @@ from pkg_resources import resource_string
from xmodule.raw_module import RawDescriptor
from .x_module import XModule
from xblock.core import Integer, Scope, BlockScope, ModelType, String, Boolean, Object, Float, List
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor
from collections import namedtuple
log = logging.getLogger("mitx.courseware")
VERSION_TUPLES = (
('1', CombinedOpenEndedV1Descriptor, CombinedOpenEndedV1Module),
)
V1_SETTINGS_ATTRIBUTES = ["display_name", "attempts", "is_graded", "accept_file_upload",
"skip_spelling_checks", "due", "graceperiod", "max_score"]
V1_STUDENT_ATTRIBUTES = ["current_task_number", "task_states", "state",
"student_attempts", "ready_to_reset"]
V1_ATTRIBUTES = V1_SETTINGS_ATTRIBUTES + V1_STUDENT_ATTRIBUTES
VersionTuple = namedtuple('VersionTuple', ['descriptor', 'module', 'settings_attributes', 'student_attributes'])
VERSION_TUPLES = {
1: VersionTuple(CombinedOpenEndedV1Descriptor, CombinedOpenEndedV1Module, V1_SETTINGS_ATTRIBUTES,
V1_STUDENT_ATTRIBUTES),
}
DEFAULT_VERSION = 1
DEFAULT_VERSION = str(DEFAULT_VERSION)
class CombinedOpenEndedModule(XModule):
class VersionInteger(Integer):
"""
A model type that converts from strings to integers when reading from json.
Also does error checking to see if version is correct or not.
"""
def from_json(self, value):
try:
value = int(value)
if value not in VERSION_TUPLES:
version_error_string = "Could not find version {0}, using version {1} instead"
log.error(version_error_string.format(value, DEFAULT_VERSION))
value = DEFAULT_VERSION
except:
value = DEFAULT_VERSION
return value
class CombinedOpenEndedFields(object):
display_name = String(help="Display name for this module", default="Open Ended Grading", scope=Scope.settings)
current_task_number = Integer(help="Current task that the student is on.", default=0, scope=Scope.student_state)
task_states = List(help="List of state dictionaries of each task within this module.", scope=Scope.student_state)
state = String(help="Which step within the current task that the student is on.", default="initial",
scope=Scope.student_state)
student_attempts = Integer(help="Number of attempts taken by the student on this problem", default=0,
scope=Scope.student_state)
ready_to_reset = Boolean(help="If the problem is ready to be reset or not.", default=False,
scope=Scope.student_state)
attempts = Integer(help="Maximum number of attempts that a student is allowed.", default=1, scope=Scope.settings)
is_graded = Boolean(help="Whether or not the problem is graded.", default=False, scope=Scope.settings)
accept_file_upload = Boolean(help="Whether or not the problem accepts file uploads.", default=False,
scope=Scope.settings)
skip_spelling_checks = Boolean(help="Whether or not to skip initial spelling checks.", default=True,
scope=Scope.settings)
due = String(help="Date that this problem is due by", default=None, scope=Scope.settings)
graceperiod = String(help="Amount of time after the due date that submissions will be accepted", default=None,
scope=Scope.settings)
max_score = Integer(help="Maximum score for the problem.", default=1, scope=Scope.settings)
version = VersionInteger(help="Current version number", default=DEFAULT_VERSION, scope=Scope.settings)
data = String(help="XML data for the problem", scope=Scope.content)
class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule):
"""
This is a module that encapsulates all open ended grading (self assessment, peer assessment, etc).
It transitions between problems, and support arbitrary ordering.
......@@ -49,6 +102,8 @@ class CombinedOpenEndedModule(XModule):
INTERMEDIATE_DONE = 'intermediate_done'
DONE = 'done'
icon_class = 'problem'
js = {'coffee': [resource_string(__name__, 'js/src/combinedopenended/display.coffee'),
resource_string(__name__, 'js/src/collapsible.coffee'),
resource_string(__name__, 'js/src/javascript_loader.coffee'),
......@@ -57,11 +112,8 @@ class CombinedOpenEndedModule(XModule):
css = {'scss': [resource_string(__name__, 'css/combinedopenended/display.scss')]}
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)
def __init__(self, system, location, descriptor, model_data):
XModule.__init__(self, system, location, descriptor, model_data)
"""
Definition file should have one or many task blocks, a rubric block, and a prompt block:
......@@ -100,50 +152,37 @@ class CombinedOpenEndedModule(XModule):
self.system = system
self.system.set('location', location)
# Load instance state
if instance_state is not None:
instance_state = json.loads(instance_state)
else:
instance_state = {}
self.version = self.metadata.get('version', DEFAULT_VERSION)
version_error_string = "Version of combined open ended module {0} is not correct. Going with version {1}"
if not isinstance(self.version, basestring):
try:
self.version = str(self.version)
except:
#This is a dev_facing_error
log.info(version_error_string.format(self.version, DEFAULT_VERSION))
self.version = DEFAULT_VERSION
versions = [i[0] for i in VERSION_TUPLES]
descriptors = [i[1] for i in VERSION_TUPLES]
modules = [i[2] for i in VERSION_TUPLES]
if self.task_states is None:
self.task_states = []
try:
version_index = versions.index(self.version)
except:
#This is a dev_facing_error
log.error(version_error_string.format(self.version, DEFAULT_VERSION))
self.version = DEFAULT_VERSION
version_index = versions.index(self.version)
version_tuple = VERSION_TUPLES[self.version]
self.student_attributes = version_tuple.student_attributes
self.settings_attributes = version_tuple.settings_attributes
attributes = self.student_attributes + self.settings_attributes
static_data = {
'rewrite_content_links': self.rewrite_content_links,
}
self.child_descriptor = descriptors[version_index](self.system)
self.child_definition = descriptors[version_index].definition_from_xml(etree.fromstring(definition['data']),
self.system)
self.child_module = modules[version_index](self.system, location, self.child_definition, self.child_descriptor,
instance_state=json.dumps(instance_state), metadata=self.metadata,
static_data=static_data)
instance_state = {k: getattr(self, k) for k in attributes}
self.child_descriptor = version_tuple.descriptor(self.system)
self.child_definition = version_tuple.descriptor.definition_from_xml(etree.fromstring(self.data), self.system)
self.child_module = version_tuple.module(self.system, location, self.child_definition, self.child_descriptor,
instance_state=instance_state, static_data=static_data,
attributes=attributes)
self.save_instance_data()
def get_html(self):
return self.child_module.get_html()
self.save_instance_data()
return_value = self.child_module.get_html()
return return_value
def handle_ajax(self, dispatch, get):
return self.child_module.handle_ajax(dispatch, get)
self.save_instance_data()
return_value = self.child_module.handle_ajax(dispatch, get)
self.save_instance_data()
return return_value
def get_instance_state(self):
return self.child_module.get_instance_state()
......@@ -151,8 +190,8 @@ class CombinedOpenEndedModule(XModule):
def get_score(self):
return self.child_module.get_score()
def max_score(self):
return self.child_module.max_score()
#def max_score(self):
# return self.child_module.max_score()
def get_progress(self):
return self.child_module.get_progress()
......@@ -161,12 +200,14 @@ class CombinedOpenEndedModule(XModule):
def due_date(self):
return self.child_module.due_date
@property
def display_name(self):
return self.child_module.display_name
def save_instance_data(self):
for attribute in self.student_attributes:
child_attr = getattr(self.child_module, attribute)
if child_attr != getattr(self, attribute):
setattr(self, attribute, getattr(self.child_module, attribute))
class CombinedOpenEndedDescriptor(RawDescriptor):
class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor):
"""
Module for adding combined open ended questions
"""
......
section.poll_question {
@media print {
display: block;
width: auto;
padding: 0;
canvas, img {
page-break-inside: avoid;
}
}
.inline {
display: inline;
}
h3 {
margin-top: 0;
margin-bottom: 15px;
color: #fe57a1;
font-size: 1.9em;
&.problem-header {
section.staff {
margin-top: 30px;
font-size: 80%;
}
}
@media print {
display: block;
width: auto;
border-right: 0;
}
}
p {
text-align: justify;
font-weight: bold;
}
.poll_answer {
margin-bottom: 20px;
&.short {
clear: both;
}
.question {
height: auto;
clear: both;
min-height: 30px;
&.short {
clear: none;
width: 30%;
display: inline;
float: left;
}
.button {
-webkit-appearance: none;
-webkit-background-clip: padding-box;
-webkit-border-image: none;
-webkit-box-align: center;
-webkit-box-shadow: rgb(255, 255, 255) 0px 1px 0px 0px inset;
-webkit-font-smoothing: antialiased;
-webkit-rtl-ordering: logical;
-webkit-user-select: text;
-webkit-writing-mode: horizontal-tb;
background-clip: padding-box;
background-color: rgb(238, 238, 238);
background-image: -webkit-linear-gradient(top, rgb(238, 238, 238), rgb(210, 210, 210));
border-bottom-color: rgb(202, 202, 202);
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
border-bottom-style: solid;
border-bottom-width: 1px;
border-left-color: rgb(202, 202, 202);
border-left-style: solid;
border-left-width: 1px;
border-right-color: rgb(202, 202, 202);
border-right-style: solid;
border-right-width: 1px;
border-top-color: rgb(202, 202, 202);
border-top-left-radius: 3px;
border-top-right-radius: 3px;
border-top-style: solid;
border-top-width: 1px;
box-shadow: rgb(255, 255, 255) 0px 1px 0px 0px inset;
box-sizing: border-box;
color: rgb(51, 51, 51);
cursor: pointer;
/* display: inline-block; */
display: inline;
float: left;
font-family: 'Open Sans', Verdana, Geneva, sans-serif;
font-size: 13px;
font-style: normal;
font-variant: normal;
font-weight: bold;
letter-spacing: normal;
line-height: 25.59375px;
margin-bottom: 15px;
margin: 0px;
padding: 0px;
text-align: center;
text-decoration: none;
text-indent: 0px;
text-shadow: rgb(248, 248, 248) 0px 1px 0px;
text-transform: none;
vertical-align: top;
white-space: pre-line;
width: 25px;
height: 25px;
word-spacing: 0px;
writing-mode: lr-tb;
}
.button.answered {
-webkit-box-shadow: rgb(97, 184, 225) 0px 1px 0px 0px inset;
background-color: rgb(29, 157, 217);
background-image: -webkit-linear-gradient(top, rgb(29, 157, 217), rgb(14, 124, 176));
border-bottom-color: rgb(13, 114, 162);
border-left-color: rgb(13, 114, 162);
border-right-color: rgb(13, 114, 162);
border-top-color: rgb(13, 114, 162);
box-shadow: rgb(97, 184, 225) 0px 1px 0px 0px inset;
color: rgb(255, 255, 255);
text-shadow: rgb(7, 103, 148) 0px 1px 0px;
}
.text {
display: inline;
float: left;
width: 80%;
text-align: left;
min-height: 30px;
margin-left: 20px;
height: auto;
margin-bottom: 20px;
cursor: pointer;
&.short {
width: 100px;
}
}
}
.stats {
min-height: 40px;
margin-top: 20px;
clear: both;
&.short {
margin-top: 0;
clear: none;
display: inline;
float: right;
width: 70%;
}
.bar {
width: 75%;
height: 20px;
border: 1px solid black;
display: inline;
float: left;
margin-right: 10px;
&.short {
width: 65%;
height: 20px;
margin-top: 3px;
}
.percent {
background-color: gray;
width: 0px;
height: 20px;
&.short { }
}
}
.number {
width: 80px;
display: inline;
float: right;
height: 28px;
text-align: right;
&.short {
width: 120px;
height: auto;
}
}
}
}
.poll_answer.answered {
-webkit-box-shadow: rgb(97, 184, 225) 0px 1px 0px 0px inset;
background-color: rgb(29, 157, 217);
background-image: -webkit-linear-gradient(top, rgb(29, 157, 217), rgb(14, 124, 176));
border-bottom-color: rgb(13, 114, 162);
border-left-color: rgb(13, 114, 162);
border-right-color: rgb(13, 114, 162);
border-top-color: rgb(13, 114, 162);
box-shadow: rgb(97, 184, 225) 0px 1px 0px 0px inset;
color: rgb(255, 255, 255);
text-shadow: rgb(7, 103, 148) 0px 1px 0px;
}
.button.reset-button {
clear: both;
float: right;
}
}
......@@ -3,35 +3,38 @@ from pkg_resources import resource_string, resource_listdir
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from xblock.core import String, Scope
import json
class DiscussionFields(object):
discussion_id = String(scope=Scope.settings)
discussion_category = String(scope=Scope.settings)
discussion_target = String(scope=Scope.settings)
sort_key = String(scope=Scope.settings)
class DiscussionModule(XModule):
class DiscussionModule(DiscussionFields, XModule):
js = {'coffee':
[resource_string(__name__, 'js/src/time.coffee'),
resource_string(__name__, 'js/src/discussion/display.coffee')]
}
js_module_name = "InlineDiscussion"
def get_html(self):
context = {
'discussion_id': self.discussion_id,
}
return self.system.render_template('discussion/_discussion_module.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)
if isinstance(instance_state, str):
instance_state = json.loads(instance_state)
xml_data = etree.fromstring(definition['data'])
self.discussion_id = xml_data.attrib['id']
self.title = xml_data.attrib['for']
self.discussion_category = xml_data.attrib['discussion_category']
class DiscussionDescriptor(RawDescriptor):
class DiscussionDescriptor(DiscussionFields, RawDescriptor):
module_class = DiscussionModule
template_dir_name = "discussion"
# The discussion XML format uses `id` and `for` attributes,
# but these would overload other module attributes, so we prefix them
# for actual use in the code
metadata_translations = dict(RawDescriptor.metadata_translations)
metadata_translations['id'] = 'discussion_id'
metadata_translations['for'] = 'discussion_target'
from pkg_resources import resource_string
from xmodule.mako_module import MakoModuleDescriptor
from xblock.core import Scope, String
import logging
log = logging.getLogger(__name__)
class EditingDescriptor(MakoModuleDescriptor):
class EditingFields(object):
data = String(scope=Scope.content, default='')
class EditingDescriptor(EditingFields, MakoModuleDescriptor):
"""
Module that provides a raw editing view of its data and children. It does not
perform any validation on its definition---just passes it along to the browser.
......@@ -20,7 +25,7 @@ class EditingDescriptor(MakoModuleDescriptor):
def get_context(self):
_context = MakoModuleDescriptor.get_context(self)
# Add our specific template information (the raw data body)
_context.update({'data': self.definition.get('data', '')})
_context.update({'data': self.data})
return _context
......
......@@ -8,6 +8,7 @@ from xmodule.x_module import XModule
from xmodule.editing_module import JSONEditingDescriptor
from xmodule.errortracker import exc_info_to_str
from xmodule.modulestore import Location
from xblock.core import String, Scope
log = logging.getLogger(__name__)
......@@ -20,7 +21,14 @@ log = logging.getLogger(__name__)
# decides whether to create a staff or not-staff module.
class ErrorModule(XModule):
class ErrorFields(object):
contents = String(scope=Scope.content)
error_msg = String(scope=Scope.content)
display_name = String(scope=Scope.settings)
class ErrorModule(ErrorFields, XModule):
def get_html(self):
'''Show an error to staff.
TODO (vshnayder): proper style, divs, etc.
......@@ -28,12 +36,12 @@ class ErrorModule(XModule):
# staff get to see all the details
return self.system.render_template('module-error.html', {
'staff_access': True,
'data': self.definition['data']['contents'],
'error': self.definition['data']['error_msg'],
'data': self.contents,
'error': self.error_msg,
})
class NonStaffErrorModule(XModule):
class NonStaffErrorModule(ErrorFields, XModule):
def get_html(self):
'''Show an error to a student.
TODO (vshnayder): proper style, divs, etc.
......@@ -46,7 +54,7 @@ class NonStaffErrorModule(XModule):
})
class ErrorDescriptor(JSONEditingDescriptor):
class ErrorDescriptor(ErrorFields, JSONEditingDescriptor):
"""
Module that provides a raw editing view of broken xml.
"""
......@@ -66,26 +74,22 @@ class ErrorDescriptor(JSONEditingDescriptor):
name=hashlib.sha1(contents).hexdigest()
)
definition = {
'data': {
'error_msg': str(error_msg),
'contents': contents,
}
}
# real metadata stays in the content, but add a display name
metadata = {'display_name': 'Error: ' + location.name}
model_data = {
'error_msg': str(error_msg),
'contents': contents,
'display_name': 'Error: ' + location.name
}
return ErrorDescriptor(
system,
definition,
location=location,
metadata=metadata
location,
model_data,
)
def get_context(self):
return {
'module': self,
'data': self.definition['data']['contents'],
'data': self.contents,
}
@classmethod
......@@ -101,10 +105,7 @@ class ErrorDescriptor(JSONEditingDescriptor):
def from_descriptor(cls, descriptor, error_msg='Error not available'):
return cls._construct(
descriptor.system,
json.dumps({
'definition': descriptor.definition,
'metadata': descriptor.metadata,
}, indent=4),
descriptor._model_data,
error_msg,
location=descriptor.location,
)
......@@ -148,14 +149,14 @@ class ErrorDescriptor(JSONEditingDescriptor):
files, etc. That would just get re-wrapped on import.
'''
try:
xml = etree.fromstring(self.definition['data']['contents'])
xml = etree.fromstring(self.contents)
return etree.tostring(xml, encoding='unicode')
except etree.XMLSyntaxError:
# still not valid.
root = etree.Element('error')
root.text = self.definition['data']['contents']
root.text = self.contents
err_node = etree.SubElement(root, 'error_msg')
err_node.text = self.definition['data']['error_msg']
err_node.text = self.error_msg
return etree.tostring(root, encoding='unicode')
......
import time
import logging
import re
from datetime import timedelta
from xblock.core import ModelType
log = logging.getLogger(__name__)
class Date(ModelType):
time_format = "%Y-%m-%dT%H:%M"
def from_json(self, value):
"""
Parse an optional metadata key containing a time: if present, complain
if it doesn't parse.
Return None if not present or invalid.
"""
if value is None:
return None
try:
return time.strptime(value, self.time_format)
except ValueError as e:
msg = "Field {0} has bad value '{1}': '{2}'".format(
self._name, value, e)
log.warning(msg)
return None
def to_json(self, value):
"""
Convert a time struct to a string
"""
if value is None:
return None
return time.strftime(self.time_format, value)
TIMEDELTA_REGEX = re.compile(r'^((?P<days>\d+?) day(?:s?))?(\s)?((?P<hours>\d+?) hour(?:s?))?(\s)?((?P<minutes>\d+?) minute(?:s)?)?(\s)?((?P<seconds>\d+?) second(?:s)?)?$')
class Timedelta(ModelType):
def from_json(self, time_str):
"""
time_str: A string with the following components:
<D> day[s] (optional)
<H> hour[s] (optional)
<M> minute[s] (optional)
<S> second[s] (optional)
Returns a datetime.timedelta parsed from the string
"""
parts = TIMEDELTA_REGEX.match(time_str)
if not parts:
return
parts = parts.groupdict()
time_params = {}
for (name, param) in parts.iteritems():
if param:
time_params[name] = int(param)
return timedelta(**time_params)
def to_json(self, value):
values = []
for attr in ('days', 'hours', 'minutes', 'seconds'):
cur_value = getattr(value, attr, 0)
if cur_value > 0:
values.append("%d %s" % (cur_value, attr))
return ' '.join(values)
\ No newline at end of file
......@@ -7,17 +7,27 @@ from pkg_resources import resource_string
from xmodule.editing_module import EditingDescriptor
from xmodule.x_module import XModule
from xmodule.xml_module import XmlDescriptor
from xblock.core import Scope, Integer, String
log = logging.getLogger(__name__)
class FolditModule(XModule):
class FolditFields(object):
# default to what Spring_7012x uses
required_level = Integer(default=4, scope=Scope.settings)
required_sublevel = Integer(default=5, scope=Scope.settings)
due = String(help="Date that this problem is due by", scope=Scope.settings, default='')
show_basic_score = String(scope=Scope.settings, default='false')
show_leaderboard = String(scope=Scope.settings, default='false')
class FolditModule(FolditFields, XModule):
css = {'scss': [resource_string(__name__, 'css/foldit/leaderboard.scss')]}
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)
def __init__(self, *args, **kwargs):
XModule.__init__(self, *args, **kwargs)
"""
Example:
......@@ -26,25 +36,17 @@ class FolditModule(XModule):
required_sublevel="3"
show_leaderboard="false"/>
"""
req_level = self.metadata.get("required_level")
req_sublevel = self.metadata.get("required_sublevel")
# default to what Spring_7012x uses
self.required_level = req_level if req_level else 4
self.required_sublevel = req_sublevel if req_sublevel else 5
def parse_due_date():
"""
Pull out the date, or None
"""
s = self.metadata.get("due")
s = self.due
if s:
return parser.parse(s)
else:
return None
self.due_str = self.metadata.get("due", "None")
self.due = parse_due_date()
self.due_time = parse_due_date()
def is_complete(self):
"""
......@@ -59,7 +61,7 @@ class FolditModule(XModule):
self.system.anonymous_student_id,
self.required_level,
self.required_sublevel,
self.due)
self.due_time)
return complete
def completed_puzzles(self):
......@@ -99,11 +101,11 @@ class FolditModule(XModule):
self.required_level,
self.required_sublevel)
showbasic = (self.metadata.get("show_basic_score", "").lower() == "true")
showleader = (self.metadata.get("show_leaderboard", "").lower() == "true")
showbasic = (self.show_basic_score.lower() == "true")
showleader = (self.show_leaderboard.lower() == "true")
context = {
'due': self.due_str,
'due': self.due,
'success': self.is_complete(),
'goal_level': goal_level,
'completed': self.completed_puzzles(),
......@@ -125,7 +127,7 @@ class FolditModule(XModule):
self.required_sublevel)
context = {
'due': self.due_str,
'due': self.due,
'success': self.is_complete(),
'goal_level': goal_level,
'completed': self.completed_puzzles(),
......@@ -155,7 +157,7 @@ class FolditModule(XModule):
class FolditDescriptor(XmlDescriptor, EditingDescriptor):
class FolditDescriptor(FolditFields, XmlDescriptor, EditingDescriptor):
"""
Module for adding Foldit problems to courses
"""
......@@ -176,7 +178,8 @@ class FolditDescriptor(XmlDescriptor, EditingDescriptor):
@classmethod
def definition_from_xml(cls, xml_object, system):
"""
Get the xml_object's attributes.
"""
return {'metadata': xml_object.attrib}
return ({}, [])
def definition_to_xml(self):
xml_object = etree.Element('foldit')
return xml_object
......@@ -14,12 +14,18 @@ from xmodule.xml_module import XmlDescriptor
from xmodule.x_module import XModule
from xmodule.stringify import stringify_children
from pkg_resources import resource_string
from xblock.core import String, Scope
log = logging.getLogger(__name__)
class GraphicalSliderToolModule(XModule):
class GraphicalSliderToolFields(object):
render = String(scope=Scope.content)
configuration = String(scope=Scope.content)
class GraphicalSliderToolModule(GraphicalSliderToolFields, XModule):
''' Graphical-Slider-Tool Module
'''
......@@ -43,15 +49,6 @@ class GraphicalSliderToolModule(XModule):
}
js_module_name = "GraphicalSliderTool"
def __init__(self, system, location, definition, descriptor, instance_state=None,
shared_state=None, **kwargs):
"""
For XML file format please look at documentation. TODO - receive
information where to store XML documentation.
"""
XModule.__init__(self, system, location, definition, descriptor,
instance_state, shared_state, **kwargs)
def get_html(self):
""" Renders parameters to template. """
......@@ -60,14 +57,14 @@ class GraphicalSliderToolModule(XModule):
self.html_class = self.location.category
self.configuration_json = self.build_configuration_json()
params = {
'gst_html': self.substitute_controls(self.definition['render']),
'gst_html': self.substitute_controls(self.render),
'element_id': self.html_id,
'element_class': self.html_class,
'configuration_json': self.configuration_json
}
self.content = self.system.render_template(
content = self.system.render_template(
'graphical_slider_tool.html', params)
return self.content
return content
def substitute_controls(self, html_string):
""" Substitutes control elements (slider, textbox and plot) in
......@@ -139,10 +136,10 @@ class GraphicalSliderToolModule(XModule):
# <root> added for interface compatibility with xmltodict.parse
# class added for javascript's part purposes
return json.dumps(xmltodict.parse('<root class="' + self.html_class +
'">' + self.definition['configuration'] + '</root>'))
'">' + self.configuration + '</root>'))
class GraphicalSliderToolDescriptor(MakoModuleDescriptor, XmlDescriptor):
class GraphicalSliderToolDescriptor(GraphicalSliderToolFields, MakoModuleDescriptor, XmlDescriptor):
module_class = GraphicalSliderToolModule
template_dir_name = 'graphical_slider_tool'
......@@ -177,14 +174,14 @@ class GraphicalSliderToolDescriptor(MakoModuleDescriptor, XmlDescriptor):
return {
'render': parse('render'),
'configuration': parse('configuration')
}
}, []
def definition_to_xml(self, resource_fs):
'''Return an xml element representing this definition.'''
xml_object = etree.Element('graphical_slider_tool')
def add_child(k):
child_str = '<{tag}>{body}</{tag}>'.format(tag=k, body=self.definition[k])
child_str = '<{tag}>{body}</{tag}>'.format(tag=k, body=getattr(self, k))
child_node = etree.fromstring(child_str)
xml_object.append(child_node)
......
......@@ -7,10 +7,9 @@ from lxml import etree
from path import path
from pkg_resources import resource_string
from xmodule.contentstore.content import XASSET_SRCREF_PREFIX, StaticContent
from xblock.core import Scope, String
from xmodule.editing_module import EditingDescriptor
from xmodule.html_checker import check_html
from xmodule.modulestore import Location
from xmodule.stringify import stringify_children
from xmodule.x_module import XModule
from xmodule.xml_module import XmlDescriptor, name_to_pathname
......@@ -18,7 +17,11 @@ from xmodule.xml_module import XmlDescriptor, name_to_pathname
log = logging.getLogger("mitx.courseware")
class HtmlModule(XModule):
class HtmlFields(object):
data = String(help="Html contents to display for this module", scope=Scope.content)
class HtmlModule(HtmlFields, 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')
......@@ -28,17 +31,10 @@ class HtmlModule(XModule):
css = {'scss': [resource_string(__name__, 'css/html/display.scss')]}
def get_html(self):
return self.html
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)
self.html = self.definition['data']
return self.data
class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor):
"""
Module for putting raw html in a course
"""
......@@ -91,7 +87,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
if filename is None:
definition_xml = copy.deepcopy(xml_object)
cls.clean_metadata_from_xml(definition_xml)
return {'data': stringify_children(definition_xml)}
return {'data': stringify_children(definition_xml)}, []
else:
# html is special. cls.filename_extension is 'xml', but
# if 'filename' is in the definition, that means to load
......@@ -105,8 +101,6 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
filepath = "{base}/{name}.html".format(base=base, name=filename)
#log.debug("looking for html file for {0} at {1}".format(location, filepath))
# VS[compat]
# TODO (cpennington): If the file doesn't exist at the right path,
# give the class a chance to fix it up. The file will be written out
......@@ -135,7 +129,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
# for Fall 2012 LMS migration: keep filename (and unmangled filename)
definition['filename'] = [filepath, filename]
return definition
return definition, []
except (ResourceNotFoundError) as err:
msg = 'Unable to load file contents at path {0}: {1} '.format(
......@@ -151,19 +145,18 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
string to filename.html.
'''
try:
return etree.fromstring(self.definition['data'])
return etree.fromstring(self.data)
except etree.XMLSyntaxError:
pass
# Not proper format. Write html to file, return an empty tag
pathname = name_to_pathname(self.url_name)
pathdir = path(pathname).dirname()
filepath = u'{category}/{pathname}.html'.format(category=self.category,
pathname=pathname)
resource_fs.makedir(os.path.dirname(filepath), recursive=True, allow_recreate=True)
with resource_fs.open(filepath, 'w') as file:
file.write(self.definition['data'].encode('utf-8'))
file.write(self.data.encode('utf-8'))
# write out the relative name
relname = path(pathname).basename()
......@@ -175,8 +168,11 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
@property
def editable_metadata_fields(self):
"""Remove any metadata from the editable fields which have their own editor or shouldn't be edited by user."""
subset = [field for field in super(HtmlDescriptor,self).editable_metadata_fields
if field not in ['empty']]
subset = super(HtmlDescriptor, self).editable_metadata_fields
if 'empty' in subset:
del subset['empty']
return subset
......
class @Conditional
constructor: (element) ->
constructor: (element, callerElId) ->
@el = $(element).find('.conditional-wrapper')
@id = @el.data('problem-id')
@element_id = @el.attr('id')
@url = @el.data('url')
@render()
$: (selector) ->
$(selector, @el)
@callerElId = callerElId
if callerElId isnt undefined
dependencies = @el.data('depends')
if (typeof dependencies is 'string') and (dependencies.length > 0) and (dependencies.indexOf(callerElId) is -1)
return
updateProgress: (response) =>
if response.progress_changed
@el.attr progress: response.progress_status
@el.trigger('progressChanged')
@url = @el.data('url')
@render(element)
render: (content) ->
if content
@el.html(content)
XModule.loadModules(@el)
else
render: (element) ->
$.postWithPrefix "#{@url}/conditional_get", (response) =>
@el.html(response.html)
XModule.loadModules(@el)
@el.html ''
@el.append(i) for i in response.html
parentEl = $(element).parent()
parentId = parentEl.attr 'id'
if response.message is false
if parentId.indexOf('vert') is 0
parentEl.hide()
else
$(element).hide()
else
if parentId.indexOf('vert') is 0
parentEl.show()
else
$(element).show()
XModule.loadModules @el
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
// define() functions from Require JS available inside the anonymous function.
(function (requirejs, require, define) {
define('logme', [], function () {
var debugMode;
// debugMode can be one of the following:
//
// true - All messages passed to logme will be written to the internal
// browser console.
// false - Suppress all output to the internal browser console.
//
// Obviously, if anywhere there is a direct console.log() call, we can't do
// anything about it. That's why use logme() - it will allow to turn off
// the output of debug information with a single change to a variable.
debugMode = true;
return logme;
/*
* function: logme
*
* A helper function that provides logging facilities. We don't want
* to call console.log() directly, because sometimes it is not supported
* by the browser. Also when everything is routed through this function.
* the logging output can be easily turned off.
*
* logme() supports multiple parameters. Each parameter will be passed to
* console.log() function separately.
*
*/
function logme() {
var i;
if (
(typeof debugMode === 'undefined') ||
(debugMode !== true) ||
(typeof window.console === 'undefined')
) {
return;
}
for (i = 0; i < arguments.length; i++) {
window.console.log(arguments[i]);
}
} // End-of: function logme
});
// End of wrapper for RequireJS. As you can see, we are passing
// namespaced Require JS variables to an anonymous function. Within
// it, you can use the standard requirejs(), require(), and define()
// functions as if they were in the global namespace.
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)
window.Poll = function (el) {
RequireJS.require(['PollMain'], function (PollMain) {
new PollMain(el);
});
};
......@@ -56,7 +56,7 @@ class @Sequence
element.removeClass('progress-none')
.removeClass('progress-some')
.removeClass('progress-done')
switch progress
when 'none' then element.addClass('progress-none')
when 'in_progress' then element.addClass('progress-some')
......@@ -65,6 +65,11 @@ class @Sequence
toggleArrows: =>
@$('.sequence-nav-buttons a').unbind('click')
if @contents.length == 0
@$('.sequence-nav-buttons .prev a').addClass('disabled')
@$('.sequence-nav-buttons .next a').addClass('disabled')
return
if @position == 1
@$('.sequence-nav-buttons .prev a').addClass('disabled')
else
......@@ -105,8 +110,8 @@ class @Sequence
if (1 <= new_position) and (new_position <= @num_contents)
Logger.log "seq_goto", old: @position, new: new_position, id: @id
# On Sequence chage, destroy any existing polling thread
# On Sequence chage, destroy any existing polling thread
# for queued submissions, see ../capa/display.coffee
if window.queuePollerID
window.clearTimeout(window.queuePollerID)
......
......@@ -4,7 +4,6 @@ class @Video
@id = @el.attr('id').replace(/video_/, '')
@start = @el.data('start')
@end = @el.data('end')
@caption_data_dir = @el.data('caption-data-dir')
@caption_asset_path = @el.data('caption-asset-path')
@show_captions = @el.data('show-captions') == "true"
window.player = null
......
class @WrapperDescriptor extends XModule.Descriptor
constructor: (@element) ->
console.log 'WrapperDescriptor'
@$items = $(@element).find(".vert-mod")
@$items.sortable(
update: (event, ui) => @update()
)
save: ->
children: $('.vert-mod li', @element).map((idx, el) -> $(el).data('id')).toArray()
from x_module import XModuleDescriptor, DescriptorSystem
import logging
from .x_module import XModuleDescriptor, DescriptorSystem
from .modulestore.inheritance import own_metadata
class MakoDescriptorSystem(DescriptorSystem):
......@@ -21,21 +21,21 @@ class MakoModuleDescriptor(XModuleDescriptor):
the descriptor as the `module` parameter to that template
"""
def __init__(self, system, definition=None, **kwargs):
def __init__(self, system, location, model_data):
if getattr(system, 'render_template', None) is None:
raise TypeError('{system} must have a render_template function'
' in order to use a MakoDescriptor'.format(
system=system))
super(MakoModuleDescriptor, self).__init__(system, definition, **kwargs)
super(MakoModuleDescriptor, self).__init__(system, location, model_data)
def get_context(self):
"""
Return the context to render the mako template with
"""
return {'module': self,
'metadata': self.metadata,
'editable_metadata_fields': self.editable_metadata_fields
}
return {
'module': self,
'editable_metadata_fields': self.editable_metadata_fields,
}
def get_html(self):
return self.system.render_template(
......@@ -44,6 +44,10 @@ class MakoModuleDescriptor(XModuleDescriptor):
# cdodge: encapsulate a means to expose "editable" metadata fields (i.e. not internal system metadata)
@property
def editable_metadata_fields(self):
subset = [name for name in self.metadata.keys() if name not in self.system_metadata_fields and
name not in self._inherited_metadata]
return subset
fields = {}
for field, value in own_metadata(self).items():
if field in self.system_metadata_fields:
continue
fields[field] = value
return fields
......@@ -423,6 +423,7 @@ class ModuleStoreBase(ModuleStore):
Set up the error-tracking logic.
'''
self._location_errors = {} # location -> ErrorLog
self.metadata_inheritance_cache = None
def _get_errorlog(self, location):
"""
......
......@@ -33,11 +33,12 @@ def modulestore(name='default'):
class_ = load_function(settings.MODULESTORE[name]['ENGINE'])
options = {}
options.update(settings.MODULESTORE[name]['OPTIONS'])
for key in FUNCTION_KEYS:
if key in options:
options[key] = load_function(options[key])
_MODULESTORES[name] = class_(
**options
)
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment