Commit 84a5475d by Brian Talbot

studio - sass cleanup: resolving conflicts with local master merge

parents 13a7e905 3b0aed5d
...@@ -29,4 +29,5 @@ cover_html/ ...@@ -29,4 +29,5 @@ cover_html/
.idea/ .idea/
.redcar/ .redcar/
chromedriver.log chromedriver.log
/nbproject
ghostdriver.log ghostdriver.log
...@@ -33,7 +33,15 @@ load-plugins= ...@@ -33,7 +33,15 @@ load-plugins=
# can either give multiple identifier separated by comma (,) or put this option # can either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where # multiple time (only on the command line, not in the configuration file where
# it should appear only once). # it should appear only once).
disable=E1102,W0142 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)
# R0904: Too many public methods
W0141,W0142,R0201,R0901,R0902,R0903,R0904
[REPORTS] [REPORTS]
...@@ -97,7 +105,7 @@ bad-functions=map,filter,apply,input ...@@ -97,7 +105,7 @@ bad-functions=map,filter,apply,input
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Regular expression which should only match correct module level names # Regular expression which should only match correct module level names
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__)|log)$ const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__)|log|urlpatterns)$
# Regular expression which should only match correct class names # Regular expression which should only match correct class names
class-rgx=[A-Z_][a-zA-Z0-9]+$ class-rgx=[A-Z_][a-zA-Z0-9]+$
...@@ -106,7 +114,7 @@ class-rgx=[A-Z_][a-zA-Z0-9]+$ ...@@ -106,7 +114,7 @@ class-rgx=[A-Z_][a-zA-Z0-9]+$
function-rgx=[a-z_][a-z0-9_]{2,30}$ function-rgx=[a-z_][a-z0-9_]{2,30}$
# Regular expression which should only match correct method names # Regular expression which should only match correct method names
method-rgx=[a-z_][a-z0-9_]{2,30}$ method-rgx=([a-z_][a-z0-9_]{2,60}|setUp|set[Uu]pClass|tearDown|tear[Dd]ownClass|assert[A-Z]\w*)$
# Regular expression which should only match correct instance attribute names # Regular expression which should only match correct instance attribute names
attr-rgx=[a-z_][a-z0-9_]{2,30}$ attr-rgx=[a-z_][a-z0-9_]{2,30}$
......
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from lxml import html from lxml import html, etree
import re import re
from django.http import HttpResponseBadRequest from django.http import HttpResponseBadRequest
import logging import logging
...@@ -26,9 +26,9 @@ def get_course_updates(location): ...@@ -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. # purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
try: try:
course_html_parsed = html.fromstring(course_updates.definition['data']) course_html_parsed = etree.fromstring(course_updates.data)
except: except etree.XMLSyntaxError:
course_html_parsed = html.fromstring("<ol></ol>") course_html_parsed = etree.fromstring("<ol></ol>")
# Confirm that root is <ol>, iterate over <li>, pull out <h2> subs and then rest of val # Confirm that root is <ol>, iterate over <li>, pull out <h2> subs and then rest of val
course_upd_collection = [] course_upd_collection = []
...@@ -60,13 +60,13 @@ def update_course_updates(location, update, passed_id=None): ...@@ -60,13 +60,13 @@ def update_course_updates(location, update, passed_id=None):
try: try:
course_updates = modulestore('direct').get_item(location) course_updates = modulestore('direct').get_item(location)
except ItemNotFoundError: 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. # purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
try: try:
course_html_parsed = html.fromstring(course_updates.definition['data']) course_html_parsed = etree.fromstring(course_updates.data)
except: except etree.XMLSyntaxError:
course_html_parsed = html.fromstring("<ol></ol>") course_html_parsed = etree.fromstring("<ol></ol>")
# No try/catch b/c failure generates an error back to client # 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>') 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): ...@@ -85,13 +85,12 @@ def update_course_updates(location, update, passed_id=None):
passed_id = course_updates.location.url() + "/" + str(idx) passed_id = course_updates.location.url() + "/" + str(idx)
# update db record # update db record
course_updates.definition['data'] = html.tostring(course_html_parsed) course_updates.data = etree.tostring(course_html_parsed)
modulestore('direct').update_item(location, course_updates.definition['data']) modulestore('direct').update_item(location, course_updates.data)
return {"id": passed_id,
"date": update['date'],
"content": update['content']}
return {"id" : passed_id,
"date" : update['date'],
"content" :update['content']}
def delete_course_update(location, update, passed_id): def delete_course_update(location, update, passed_id):
""" """
...@@ -99,19 +98,19 @@ 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. Returns the resulting course_updates b/c their ids change.
""" """
if not passed_id: if not passed_id:
return HttpResponseBadRequest return HttpResponseBadRequest()
try: try:
course_updates = modulestore('direct').get_item(location) course_updates = modulestore('direct').get_item(location)
except ItemNotFoundError: except ItemNotFoundError:
return HttpResponseBadRequest return HttpResponseBadRequest()
# TODO use delete_blank_text parser throughout and cache as a static var in a class # 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. # purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
try: try:
course_html_parsed = html.fromstring(course_updates.definition['data']) course_html_parsed = etree.fromstring(course_updates.data)
except: except etree.XMLSyntaxError:
course_html_parsed = html.fromstring("<ol></ol>") course_html_parsed = etree.fromstring("<ol></ol>")
if course_html_parsed.tag == 'ol': if course_html_parsed.tag == 'ol':
# ??? Should this use the id in the json or in the url or does it matter? # ??? 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): ...@@ -122,9 +121,9 @@ def delete_course_update(location, update, passed_id):
course_html_parsed.remove(element_to_delete) course_html_parsed.remove(element_to_delete)
# update db record # update db record
course_updates.definition['data'] = html.tostring(course_html_parsed) course_updates.data = etree.tostring(course_html_parsed)
store = modulestore('direct') store = modulestore('direct')
store.update_item(location, course_updates.definition['data']) store.update_item(location, course_updates.data)
return get_course_updates(location) return get_course_updates(location)
......
...@@ -2,53 +2,41 @@ Feature: Advanced (manual) course policy ...@@ -2,53 +2,41 @@ Feature: Advanced (manual) course policy
In order to specify course policy settings for which no custom user interface exists 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 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 Given I have opened a new course in Studio
When I select the Advanced Settings When I select the Advanced Settings
Then I see only the display name Then I see default advanced settings
@skip-phantom Scenario: Add new entries, and they appear alphabetically after save
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
Given I am on the Advanced Course Settings page in Studio Given I am on the Advanced Course Settings page in Studio
When I edit the name of a policy key Then the settings are alphabetized
And I press the "Save" notification button
Then the policy key name is changed
Scenario: Test cancel editing key value Scenario: Test cancel editing key value
Given I am on the Advanced Course Settings page in Studio Given I am on the Advanced Course Settings page in Studio
When I edit the value of a policy key When I edit the value of a policy key
And I press the "Cancel" notification button And I press the "Cancel" notification button
Then the policy key value is unchanged 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 Scenario: Test editing key value
Given I am on the Advanced Course Settings page in Studio Given I am on the Advanced Course Settings page in Studio
When I edit the value of a policy key When I edit the value of a policy key
And I press the "Save" notification button And I press the "Save" notification button
Then the policy key value is changed 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 And I reload the page
Then they are alphabetized Then the policy key value is changed
Scenario: Test how multi-line input appears Scenario: Test how multi-line input appears
Given I am on the Advanced Course Settings page in Studio 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 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 lettuce import world, step
from common import * from common import *
import time import time
from terrain.steps import reload_the_page
from selenium.common.exceptions import WebDriverException from selenium.common.exceptions import WebDriverException
from selenium.webdriver.support import expected_conditions as EC 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 ...@@ -11,6 +12,10 @@ http://selenium.googlecode.com/svn/trunk/docs/api/py/webdriver/selenium.webdrive
""" """
from selenium.webdriver.common.keys import Keys 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 #################### ############### ACTIONS ####################
@step('I select the Advanced Settings$') @step('I select the Advanced Settings$')
...@@ -20,7 +25,6 @@ def i_select_advanced_settings(step): ...@@ -20,7 +25,6 @@ def i_select_advanced_settings(step):
css_click(expand_icon_css) css_click(expand_icon_css)
link_css = 'li.nav-course-settings-advanced a' link_css = 'li.nav-course-settings-advanced a'
css_click(link_css) css_click(link_css)
# world.browser.click_link_by_text('Advanced Settings')
@step('I am on the Advanced Course Settings page in Studio$') @step('I am on the Advanced Course Settings page in Studio$')
...@@ -29,35 +33,27 @@ def i_am_on_advanced_course_settings(step): ...@@ -29,35 +33,27 @@ def i_am_on_advanced_course_settings(step):
step.given('I select the Advanced Settings') 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$') @step(u'I press the "([^"]*)" notification button$')
def press_the_notification_button(step, name): def press_the_notification_button(step, name):
def is_visible(driver): def is_visible(driver):
return EC.visibility_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,)) # def is_invisible(driver):
# return EC.invisibility_of_element_located((By.CSS_SELECTOR,css,))
css = 'a.%s-button' % name.lower() css = 'a.%s-button' % name.lower()
wait_for(is_visible) 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$') @step(u'I edit the value of a policy key$')
def edit_the_value_of_a_policy_key(step): def edit_the_value_of_a_policy_key(step):
...@@ -65,133 +61,86 @@ 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 It is hard to figure out how to get into the CodeMirror
area, so cheat and do it from the policy key field :) area, so cheat and do it from the policy key field :)
""" """
policy_key_css = 'input.policy-key' e = css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)]
e = css_find(policy_key_css).first
e._element.send_keys(Keys.TAB, Keys.END, Keys.ARROW_LEFT, ' ', 'X') e._element.send_keys(Keys.TAB, Keys.END, Keys.ARROW_LEFT, ' ', 'X')
@step('I delete the display name$') @step('I create a JSON object as a value$')
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$')
def create_JSON_object(step): def create_JSON_object(step):
create_entry("json", '{"key": "value", "key_2": "value_2"}') change_display_name_value(step, '{"key": "value", "key_2": "value_2"}')
click_save()
############### RESULTS #################### @step('I create a non-JSON value not in quotes$')
@step('I see only the display name$') def create_value_not_in_quotes(step):
def i_see_only_display_name(step): change_display_name_value(step, 'quote me')
assert_policy_entries(["display_name"], ['"Robot Super Course"'])
@step('there are no advanced policy settings$') ############### RESULTS ####################
def no_policy_settings(step): @step('I see default advanced settings$')
keys_css = 'input.policy-key' def i_see_default_advanced_settings(step):
val_css = 'textarea.json' # Test only a few of the existing properties (there are around 34 of them)
k = world.browser.is_element_not_present_by_css(keys_css, 5) assert_policy_entries(
v = world.browser.is_element_not_present_by_css(val_css, 5) ["advanced_modules", DISPLAY_NAME_KEY, "show_calculator"], ["[]", DISPLAY_NAME_VALUE, "false"])
assert_true(k)
assert_true(v)
@step('they are alphabetized$') @step('the settings are alphabetized$')
def they_are_alphabetized(step): 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$') @step('it is displayed as formatted$')
def it_is_formatted(step): 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('it is displayed as a string')
@step(u'the policy key name is changed$') def it_is_formatted(step):
def the_policy_key_name_is_changed(step): assert_policy_entries([DISPLAY_NAME_KEY], ['"quote me"'])
policy_key_css = 'input.policy-key'
val = css_find(policy_key_css).first.value
assert_equal(val, 'display_name_new')
@step(u'the policy key value is unchanged$') @step(u'the policy key value is unchanged$')
def the_policy_key_value_is_unchanged(step): def the_policy_key_value_is_unchanged(step):
policy_value_css = 'li.course-advanced-policy-list-item div.value textarea' assert_equal(get_display_name_value(), DISPLAY_NAME_VALUE)
val = css_find(policy_value_css).first.value
assert_equal(val, '"Robot Super Course"')
@step(u'the policy key value is changed$') @step(u'the policy key value is changed$')
def the_policy_key_value_is_unchanged(step): def the_policy_key_value_is_changed(step):
policy_value_css = 'li.course-advanced-policy-list-item div.value textarea' assert_equal(get_display_name_value(), '"Robot Super Course X"')
val = css_find(policy_value_css).first.value
assert_equal(val, '"Robot Super Course X"')
############# HELPERS ############### ############# 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): def assert_policy_entries(expected_keys, expected_values):
assert_entries('.key input.policy-key', expected_keys) for counter in range(len(expected_keys)):
assert_entries('textarea.json', expected_values) 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): def get_index_of(expected_key):
webElements = css_find(css) for counter in range(len(css_find(KEY_CSS))):
assert_equal(len(expected_values), len(webElements)) # Sometimes get stale reference if I hold on to the array of elements
# Sometimes get stale reference if I hold on to the array of elements key = css_find(KEY_CSS)[counter].value
for counter in range(len(expected_values)): if key == expected_key:
assert_equal(expected_values[counter], css_find(css)[counter].value) return counter
return -1
def click_save():
css = "a.save-button" def get_display_name_value():
css_click_at(css) index = get_index_of(DISPLAY_NAME_KEY)
return css_find(VALUE_CSS)[index].value
def fill_last_field(value): def change_display_name_value(step, new_value):
newValue = css_find('#__new_advanced_key__ input').first e = css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)]
newValue.fill(value) 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 ...@@ -7,7 +7,7 @@ from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore from xmodule.contentstore.django import contentstore
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.course_module import CourseDescriptor 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 from auth.authz import _delete_course_group
......
...@@ -15,10 +15,10 @@ def get_module_info(store, location, parent_location=None, rewrite_static_links= ...@@ -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']) template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty'])
module = store.clone_item(template_location, location) module = store.clone_item(template_location, location)
data = module.definition['data'] data = module.data
if rewrite_static_links: if rewrite_static_links:
data = replace_static_urls( data = replace_static_urls(
module.definition['data'], module.data,
None, None,
course_namespace=Location([ course_namespace=Location([
module.location.tag, module.location.tag,
...@@ -32,7 +32,8 @@ def get_module_info(store, location, parent_location=None, rewrite_static_links= ...@@ -32,7 +32,8 @@ def get_module_info(store, location, parent_location=None, rewrite_static_links=
return { return {
'id': module.location.url(), 'id': module.location.url(),
'data': data, '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): ...@@ -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 # 'apply' the submitted metadata, so we don't end up deleting system metadata
if post_data.get('metadata') is not None: if post_data.get('metadata') is not None:
posted_metadata = post_data['metadata'] posted_metadata = post_data['metadata']
# update existing metadata with submitted metadata (which can be partial) # 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' # 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 # 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 # and therefore should not be user-editable, so we should accept them back from the client
if metadata_key in module.system_metadata_fields: if metadata_key in module.system_metadata_fields:
del posted_metadata[metadata_key] del posted_metadata[metadata_key]
elif posted_metadata[metadata_key] is None: elif posted_metadata[metadata_key] is None:
# remove both from passed in collection as well as the collection read in from the modulestore # remove both from passed in collection as well as the collection read in from the modulestore
if metadata_key in module.metadata: if metadata_key in module._model_data:
del module.metadata[metadata_key] del module._model_data[metadata_key]
del posted_metadata[metadata_key] del posted_metadata[metadata_key]
else:
# overlay the new metadata over the modulestore sourced collection to support partial updates module._model_data[metadata_key] = value
module.metadata.update(posted_metadata)
# commit to datastore # 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 ...@@ -6,15 +6,16 @@ from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from path import path from path import path
from tempdir import mkdtemp_clean from tempdir import mkdtemp_clean
from datetime import timedelta
import json import json
from fs.osfs import OSFS from fs.osfs import OSFS
import copy import copy
from json import loads from json import loads
from django.contrib.auth.models import User 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.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore import Location from xmodule.modulestore import Location
...@@ -25,6 +26,7 @@ from xmodule.contentstore.django import contentstore ...@@ -25,6 +26,7 @@ from xmodule.contentstore.django import contentstore
from xmodule.templates import update_templates from xmodule.templates import update_templates
from xmodule.modulestore.xml_exporter import export_to_xml from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.xml_importer import import_from_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.capa_module import CapaDescriptor
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
...@@ -109,10 +111,10 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -109,10 +111,10 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter','Week_1', None])) chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter','Week_1', None]))
# make sure the parent no longer points to the child object which was deleted # 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'), self.client.post(reverse('delete_item'),
json.dumps({'id': sequential.location.url(), 'delete_children':'true'}), json.dumps({'id': sequential.location.url(), 'delete_children': 'true', 'delete_all_versions': 'true'}),
"application/json") "application/json")
found = False found = False
...@@ -127,9 +129,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -127,9 +129,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter','Week_1', None])) chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter','Week_1', None]))
# make sure the parent no longer points to the child object which was deleted # 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): def test_about_overrides(self):
''' '''
...@@ -139,11 +141,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -139,11 +141,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
import_from_xml(modulestore(), 'common/test/data/', ['full']) import_from_xml(modulestore(), 'common/test/data/', ['full'])
module_store = modulestore('direct') module_store = modulestore('direct')
effort = module_store.get_item(Location(['i4x', 'edX', 'full', 'about', 'effort', None])) 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 # this one should be in a non-override folder
effort = module_store.get_item(Location(['i4x', 'edX', 'full', 'about', 'end_date', None])) effort = module_store.get_item(Location(['i4x', 'edX', 'full', 'about', 'end_date', None]))
self.assertEqual(effort.definition['data'], 'TBD') self.assertEqual(effort.data, 'TBD')
def test_remove_hide_progress_tab(self): def test_remove_hide_progress_tab(self):
import_from_xml(modulestore(), 'common/test/data/', ['full']) import_from_xml(modulestore(), 'common/test/data/', ['full'])
...@@ -153,7 +155,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -153,7 +155,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012') source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
course = module_store.get_item(source_location) 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): def test_clone_course(self):
...@@ -246,7 +248,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -246,7 +248,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# compare what's on disk compared to what we have in our course # compare what's on disk compared to what we have in our course
with fs.open('grading_policy.json', 'r') as grading_policy: with fs.open('grading_policy.json', 'r') as grading_policy:
on_disk = loads(grading_policy.read()) 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 #check for policy.json
self.assertTrue(fs.exists('policy.json')) self.assertTrue(fs.exists('policy.json'))
...@@ -255,7 +257,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -255,7 +257,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
with fs.open('policy.json', 'r') as course_policy: with fs.open('policy.json', 'r') as course_policy:
on_disk = loads(course_policy.read()) on_disk = loads(course_policy.read())
self.assertIn('course/6.002_Spring_2012', on_disk) 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 # remove old course
delete_course(module_store, content_store, location) delete_course(module_store, content_store, location)
...@@ -302,10 +304,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -302,10 +304,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
course = module_store.get_item(location) 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 # 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) print 'Exporting to tempdir = {0}'.format(root_dir)
...@@ -473,21 +476,20 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -473,21 +476,20 @@ class ContentStoreTest(ModuleStoreTestCase):
self.assertIsInstance(problem, CapaDescriptor, "New problem is not a CapaDescriptor") self.assertIsInstance(problem, CapaDescriptor, "New problem is not a CapaDescriptor")
context = problem.get_context() context = problem.get_context()
self.assertIn('markdown', context, "markdown is missing from 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") self.assertNotIn('markdown', problem.editable_metadata_fields, "Markdown slipped into the editable metadata fields")
def test_import_metadata_with_attempts_empty_string(self): def test_import_metadata_with_attempts_empty_string(self):
import_from_xml(modulestore(), 'common/test/data/', ['simple']) import_from_xml(modulestore(), 'common/test/data/', ['simple'])
module_store = modulestore('direct') module_store = modulestore('direct')
did_load_item = False did_load_item = False
try: try:
module_store.get_item(Location(['i4x', 'edX', 'simple', 'problem', 'ps01-simple', None])) module_store.get_item(Location(['i4x', 'edX', 'simple', 'problem', 'ps01-simple', None]))
did_load_item = True did_load_item = True
except ItemNotFoundError: except ItemNotFoundError:
pass pass
# make sure we found the item (e.g. it didn't error while loading) # 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): def test_metadata_inheritance(self):
import_from_xml(modulestore(), 'common/test/data/', ['full']) import_from_xml(modulestore(), 'common/test/data/', ['full'])
...@@ -499,8 +501,7 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -499,8 +501,7 @@ class ContentStoreTest(ModuleStoreTestCase):
# let's assert on the metadata_inheritance on an existing vertical # let's assert on the metadata_inheritance on an existing vertical
for vertical in verticals: for vertical in verticals:
self.assertIn('xqa_key', vertical.metadata) self.assertEqual(course.lms.xqa_key, vertical.lms.xqa_key)
self.assertEqual(course.metadata['xqa_key'], vertical.metadata['xqa_key'])
self.assertGreater(len(verticals), 0) self.assertGreater(len(verticals), 0)
...@@ -510,36 +511,33 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -510,36 +511,33 @@ class ContentStoreTest(ModuleStoreTestCase):
# crate a new module and add it as a child to a vertical # crate a new module and add it as a child to a vertical
module_store.clone_item(source_template_location, new_component_location) module_store.clone_item(source_template_location, new_component_location)
parent = verticals[0] 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 # flush the cache
module_store.get_cached_metadata_inheritance_tree(new_component_location, -1) module_store.get_cached_metadata_inheritance_tree(new_component_location, -1)
new_module = module_store.get_item(new_component_location) new_module = module_store.get_item(new_component_location)
# check for grace period definition which should be defined at the course level # 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.lms.xqa_key, new_module.lms.xqa_key)
self.assertEqual(course.metadata['xqa_key'], new_module.metadata['xqa_key'])
# #
# now let's define an override at the leaf node level # now let's define an override at the leaf node level
# #
new_module.metadata['graceperiod'] = '1 day' new_module.lms.graceperiod = timedelta(1)
module_store.update_metadata(new_module.location, new_module.metadata) module_store.update_metadata(new_module.location, own_metadata(new_module))
# flush the cache and refetch # flush the cache and refetch
module_store.get_cached_metadata_inheritance_tree(new_component_location, -1) module_store.get_cached_metadata_inheritance_tree(new_component_location, -1)
new_module = module_store.get_item(new_component_location) new_module = module_store.get_item(new_component_location)
self.assertIn('graceperiod', new_module.metadata) self.assertEqual(timedelta(1), new_module.lms.graceperiod)
self.assertEqual('1 day', new_module.metadata['graceperiod'])
class TemplateTestCase(ModuleStoreTestCase): class TemplateTestCase(ModuleStoreTestCase):
def test_template_cleanup(self): def test_template_cleanup(self):
module_store = modulestore('direct') module_store = modulestore('direct')
# insert a bogus template in the store # insert a bogus template in the store
...@@ -562,4 +560,3 @@ class TemplateTestCase(ModuleStoreTestCase): ...@@ -562,4 +560,3 @@ class TemplateTestCase(ModuleStoreTestCase):
asserted = True asserted = True
self.assertTrue(asserted) self.assertTrue(asserted)
...@@ -10,16 +10,16 @@ from django.core.urlresolvers import reverse ...@@ -10,16 +10,16 @@ from django.core.urlresolvers import reverse
from django.utils.timezone import UTC from django.utils.timezone import UTC
from xmodule.modulestore import Location from xmodule.modulestore import Location
from cms.djangoapps.models.settings.course_details import (CourseDetails, from models.settings.course_details import (CourseDetails,
CourseSettingsEncoder) CourseSettingsEncoder)
from cms.djangoapps.models.settings.course_grading import CourseGradingModel from models.settings.course_grading import CourseGradingModel
from cms.djangoapps.contentstore.utils import get_modulestore from contentstore.utils import get_modulestore
from django.test import TestCase from django.test import TestCase
from utils import ModuleStoreTestCase from .utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory 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.xml_importer import import_from_xml
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
...@@ -246,8 +246,9 @@ class CourseGradingTest(CourseTestCase): ...@@ -246,8 +246,9 @@ class CourseGradingTest(CourseTestCase):
altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__) altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "cutoff add D") 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__) 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") self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "4 hour grace period")
def test_update_grader_from_json(self): def test_update_grader_from_json(self):
...@@ -286,31 +287,31 @@ class CourseMetadataEditingTest(CourseTestCase): ...@@ -286,31 +287,31 @@ class CourseMetadataEditingTest(CourseTestCase):
def test_update_from_json(self): def test_update_from_json(self):
test_model = CourseMetadata.update_from_json(self.course_location, test_model = CourseMetadata.update_from_json(self.course_location,
{ "a" : 1, { "advertised_start" : "start A",
"b_a_c_h" : { "c" : "test" }, "testcenter_info" : { "c" : "test" },
"test_text" : "a text string"}) "days_early_for_beta" : 2})
self.update_check(test_model) self.update_check(test_model)
# try fresh fetch to ensure persistence # try fresh fetch to ensure persistence
test_model = CourseMetadata.fetch(self.course_location) test_model = CourseMetadata.fetch(self.course_location)
self.update_check(test_model) self.update_check(test_model)
# now change some of the existing metadata # now change some of the existing metadata
test_model = CourseMetadata.update_from_json(self.course_location, test_model = CourseMetadata.update_from_json(self.course_location,
{ "a" : 2, { "advertised_start" : "start B",
"display_name" : "jolly roger"}) "display_name" : "jolly roger"})
self.assertIn('display_name', test_model, 'Missing editable metadata field') self.assertIn('display_name', test_model, 'Missing editable metadata field')
self.assertEqual(test_model['display_name'], 'jolly roger', "not expected value") self.assertEqual(test_model['display_name'], 'jolly roger', "not expected value")
self.assertIn('a', test_model, 'Missing revised a metadata field') self.assertIn('advertised_start', test_model, 'Missing revised advertised_start metadata field')
self.assertEqual(test_model['a'], 2, "a not expected value") self.assertEqual(test_model['advertised_start'], 'start B', "advertised_start not expected value")
def update_check(self, test_model): def update_check(self, test_model):
self.assertIn('display_name', test_model, 'Missing editable metadata field') self.assertIn('display_name', test_model, 'Missing editable metadata field')
self.assertEqual(test_model['display_name'], 'Robot Super Course', "not expected value") self.assertEqual(test_model['display_name'], 'Robot Super Course', "not expected value")
self.assertIn('a', test_model, 'Missing new a metadata field') self.assertIn('advertised_start', test_model, 'Missing new advertised_start metadata field')
self.assertEqual(test_model['a'], 1, "a not expected value") self.assertEqual(test_model['advertised_start'], 'start A', "advertised_start not expected value")
self.assertIn('b_a_c_h', test_model, 'Missing b_a_c_h metadata field') self.assertIn('testcenter_info', test_model, 'Missing testcenter_info metadata field')
self.assertDictEqual(test_model['b_a_c_h'], { "c" : "test" }, "b_a_c_h not expected value") self.assertDictEqual(test_model['testcenter_info'], { "c" : "test" }, "testcenter_info not expected value")
self.assertIn('test_text', test_model, 'Missing test_text metadata field') self.assertIn('days_early_for_beta', test_model, 'Missing days_early_for_beta metadata field')
self.assertEqual(test_model['test_text'], "a text string", "test_text not expected value") self.assertEqual(test_model['days_early_for_beta'], 2, "days_early_for_beta not expected value")
def test_delete_key(self): def test_delete_key(self):
...@@ -321,5 +322,5 @@ class CourseMetadataEditingTest(CourseTestCase): ...@@ -321,5 +322,5 @@ class CourseMetadataEditingTest(CourseTestCase):
self.assertEqual(test_model['display_name'], 'Testing', "not expected value") self.assertEqual(test_model['display_name'], 'Testing', "not expected value")
self.assertIn('rerandomize', test_model, 'Missing rerandomize metadata field') self.assertIn('rerandomize', test_model, 'Missing rerandomize metadata field')
# check for deletion effectiveness # check for deletion effectiveness
self.assertNotIn('showanswer', test_model, 'showanswer field still in') self.assertEqual('closed', test_model['showanswer'], 'showanswer field still in')
self.assertNotIn('xqa_key', test_model, 'xqa_key field still in') self.assertEqual(None, test_model['xqa_key'], 'xqa_key field still in')
\ No newline at end of file
from cms.djangoapps.contentstore.tests.test_course_settings import CourseTestCase from contentstore.tests.test_course_settings import CourseTestCase
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
import json import json
......
from cms.djangoapps.contentstore import utils from contentstore import utils
import mock import mock
from django.test import TestCase from django.test import TestCase
......
...@@ -8,7 +8,7 @@ import json ...@@ -8,7 +8,7 @@ import json
from fs.osfs import OSFS from fs.osfs import OSFS
import copy import copy
from cms.djangoapps.contentstore.utils import get_modulestore from contentstore.utils import get_modulestore
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.store_utilities import clone_course from xmodule.modulestore.store_utilities import clone_course
...@@ -24,7 +24,7 @@ from xmodule.course_module import CourseDescriptor ...@@ -24,7 +24,7 @@ from xmodule.course_module import CourseDescriptor
from xmodule.seq_module import SequenceDescriptor from xmodule.seq_module import SequenceDescriptor
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory 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): class ContentStoreTestCase(ModuleStoreTestCase):
......
...@@ -39,10 +39,10 @@ def get_course_location_for_item(location): ...@@ -39,10 +39,10 @@ def get_course_location_for_item(location):
# make sure we found exactly one match on this above course search # make sure we found exactly one match on this above course search
found_cnt = len(courses) found_cnt = len(courses)
if found_cnt == 0: 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: 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 location = courses[0].location
...@@ -136,7 +136,7 @@ def compute_unit_state(unit): ...@@ -136,7 +136,7 @@ def compute_unit_state(unit):
'private' content is editabled and not visible in the LMS 'private' content is editabled and not visible in the LMS
""" """
if unit.metadata.get('is_draft', False): if unit.cms.is_draft:
try: try:
modulestore('direct').get_item(unit.location) modulestore('direct').get_item(unit.location)
return UnitState.draft return UnitState.draft
......
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.inheritance import own_metadata
import json import json
from json.encoder import JSONEncoder from json.encoder import JSONEncoder
import time import time
from contentstore.utils import get_modulestore from contentstore.utils import get_modulestore
from util.converters import jsdate_to_time, time_to_date from util.converters import jsdate_to_time, time_to_date
from cms.djangoapps.models.settings import course_grading from models.settings import course_grading
from cms.djangoapps.contentstore.utils import update_item from contentstore.utils import update_item
import re import re
import logging import logging
...@@ -43,25 +44,25 @@ class CourseDetails(object): ...@@ -43,25 +44,25 @@ class CourseDetails(object):
temploc = course_location._replace(category='about', name='syllabus') temploc = course_location._replace(category='about', name='syllabus')
try: try:
course.syllabus = get_modulestore(temploc).get_item(temploc).definition['data'] course.syllabus = get_modulestore(temploc).get_item(temploc).data
except ItemNotFoundError: except ItemNotFoundError:
pass pass
temploc = temploc._replace(name='overview') temploc = temploc._replace(name='overview')
try: try:
course.overview = get_modulestore(temploc).get_item(temploc).definition['data'] course.overview = get_modulestore(temploc).get_item(temploc).data
except ItemNotFoundError: except ItemNotFoundError:
pass pass
temploc = temploc._replace(name='effort') temploc = temploc._replace(name='effort')
try: try:
course.effort = get_modulestore(temploc).get_item(temploc).definition['data'] course.effort = get_modulestore(temploc).get_item(temploc).data
except ItemNotFoundError: except ItemNotFoundError:
pass pass
temploc = temploc._replace(name='video') temploc = temploc._replace(name='video')
try: 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) course.intro_video = CourseDetails.parse_video_tag(raw_video)
except ItemNotFoundError: except ItemNotFoundError:
pass pass
...@@ -116,7 +117,7 @@ class CourseDetails(object): ...@@ -116,7 +117,7 @@ class CourseDetails(object):
descriptor.enrollment_end = converted descriptor.enrollment_end = converted
if dirty: 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 # 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. # 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): ...@@ -133,7 +134,6 @@ class CourseDetails(object):
recomposed_video_tag = CourseDetails.recompose_video_tag(jsondict['intro_video']) recomposed_video_tag = CourseDetails.recompose_video_tag(jsondict['intro_video'])
update_item(temploc, recomposed_video_tag) 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 # 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 # it persisted correctly
return CourseDetails.fetch(course_location) return CourseDetails.fetch(course_location)
......
...@@ -2,6 +2,7 @@ from xmodule.modulestore import Location ...@@ -2,6 +2,7 @@ from xmodule.modulestore import Location
from contentstore.utils import get_modulestore from contentstore.utils import get_modulestore
import re import re
from util import converters from util import converters
from datetime import timedelta
class CourseGradingModel(object): class CourseGradingModel(object):
...@@ -91,7 +92,7 @@ class CourseGradingModel(object): ...@@ -91,7 +92,7 @@ class CourseGradingModel(object):
descriptor.raw_grader = graders_parsed descriptor.raw_grader = graders_parsed
descriptor.grade_cutoffs = jsondict['grade_cutoffs'] 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']) CourseGradingModel.update_grace_period_from_json(course_location, jsondict['grace_period'])
return CourseGradingModel.fetch(course_location) return CourseGradingModel.fetch(course_location)
...@@ -119,7 +120,7 @@ class CourseGradingModel(object): ...@@ -119,7 +120,7 @@ class CourseGradingModel(object):
else: else:
descriptor.raw_grader.append(grader) 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]) return CourseGradingModel.jsonize_grader(index, descriptor.raw_grader[index])
...@@ -134,7 +135,7 @@ class CourseGradingModel(object): ...@@ -134,7 +135,7 @@ class CourseGradingModel(object):
descriptor = get_modulestore(course_location).get_item(course_location) descriptor = get_modulestore(course_location).get_item(course_location)
descriptor.grade_cutoffs = cutoffs 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 return cutoffs
...@@ -156,11 +157,11 @@ class CourseGradingModel(object): ...@@ -156,11 +157,11 @@ class CourseGradingModel(object):
graceperiodjson = graceperiodjson['grace_period'] graceperiodjson = graceperiodjson['grace_period']
# lms requires these to be in a fixed order # 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 = get_modulestore(course_location).get_item(course_location)
descriptor.metadata['graceperiod'] = grace_rep descriptor.lms.graceperiod = grace_timedelta
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata) get_modulestore(course_location).update_metadata(course_location, descriptor._model_data._kvs._metadata)
@staticmethod @staticmethod
def delete_grader(course_location, index): def delete_grader(course_location, index):
...@@ -176,7 +177,7 @@ class CourseGradingModel(object): ...@@ -176,7 +177,7 @@ class CourseGradingModel(object):
del descriptor.raw_grader[index] del descriptor.raw_grader[index]
# force propagation to definition # force propagation to definition
descriptor.raw_grader = descriptor.raw_grader 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 # NOTE cannot delete cutoffs. May be useful to reset
@staticmethod @staticmethod
...@@ -189,7 +190,7 @@ class CourseGradingModel(object): ...@@ -189,7 +190,7 @@ class CourseGradingModel(object):
descriptor = get_modulestore(course_location).get_item(course_location) descriptor = get_modulestore(course_location).get_item(course_location)
descriptor.grade_cutoffs = descriptor.defaut_grading_policy['GRADE_CUTOFFS'] 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 return descriptor.grade_cutoffs
...@@ -202,8 +203,8 @@ class CourseGradingModel(object): ...@@ -202,8 +203,8 @@ class CourseGradingModel(object):
course_location = Location(course_location) course_location = Location(course_location)
descriptor = get_modulestore(course_location).get_item(course_location) descriptor = get_modulestore(course_location).get_item(course_location)
if 'graceperiod' in descriptor.metadata: del descriptor.metadata['graceperiod'] del descriptor.lms.graceperiod
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata) get_modulestore(course_location).update_metadata(course_location, descriptor._model_data._kvs._metadata)
@staticmethod @staticmethod
def get_section_grader_type(location): def get_section_grader_type(location):
...@@ -212,7 +213,7 @@ class CourseGradingModel(object): ...@@ -212,7 +213,7 @@ class CourseGradingModel(object):
descriptor = get_modulestore(location).get_item(location) descriptor = get_modulestore(location).get_item(location)
return { 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, "location": location,
"id": 99 # just an arbitrary value to "id": 99 # just an arbitrary value to
} }
...@@ -224,23 +225,41 @@ class CourseGradingModel(object): ...@@ -224,23 +225,41 @@ class CourseGradingModel(object):
descriptor = get_modulestore(location).get_item(location) descriptor = get_modulestore(location).get_item(location)
if 'graderType' in jsondict and jsondict['graderType'] != u"Not Graded": if 'graderType' in jsondict and jsondict['graderType'] != u"Not Graded":
descriptor.metadata['format'] = jsondict.get('graderType') descriptor.lms.format = jsondict.get('graderType')
descriptor.metadata['graded'] = True descriptor.lms.graded = True
else: else:
if 'format' in descriptor.metadata: del descriptor.metadata['format'] del descriptor.lms.format
if 'graded' in descriptor.metadata: del descriptor.metadata['graded'] del descriptor.lms.graded
get_modulestore(location).update_metadata(location, descriptor.metadata) get_modulestore(location).update_metadata(location, descriptor._model_data._kvs._metadata)
@staticmethod @staticmethod
def convert_set_grace_period(descriptor): def convert_set_grace_period(descriptor):
# 5 hours 59 minutes 59 seconds => { hours: 5, minutes : 59, seconds : 59} # 5 hours 59 minutes 59 seconds => converted to iso format
rawgrace = descriptor.metadata.get('graceperiod', None) rawgrace = descriptor.lms.graceperiod
if rawgrace: if rawgrace:
parsedgrace = {str(key): int(val) for (val, key) in re.findall('\s*(\d+)\s*(\w+)', rawgrace)} hours_from_days = rawgrace.days*24
return parsedgrace seconds = rawgrace.seconds
else: return None 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 @staticmethod
def parse_grader(json_grader): def parse_grader(json_grader):
......
from xmodule.modulestore import Location from xmodule.modulestore import Location
from contentstore.utils import get_modulestore from contentstore.utils import get_modulestore
from xmodule.x_module import XModuleDescriptor from xmodule.x_module import XModuleDescriptor
from xmodule.modulestore.inheritance import own_metadata
from xblock.core import Scope
class CourseMetadata(object): class CourseMetadata(object):
...@@ -8,8 +10,7 @@ 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. 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. 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']
FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start', 'end', 'enrollment_start', 'enrollment_end', 'tabs', 'graceperiod', '__new_advanced_key__']
@classmethod @classmethod
def fetch(cls, course_location): def fetch(cls, course_location):
...@@ -23,17 +24,20 @@ class CourseMetadata(object): ...@@ -23,17 +24,20 @@ class CourseMetadata(object):
descriptor = get_modulestore(course_location).get_item(course_location) descriptor = get_modulestore(course_location).get_item(course_location)
for k, v in descriptor.metadata.iteritems(): for field in descriptor.fields + descriptor.lms.fields:
if k not in cls.FILTERED_LIST: if field.scope != Scope.settings:
course[k] = v continue
if field.name not in cls.FILTERED_LIST:
course[field.name] = field.read_from(descriptor)
return course return course
@classmethod @classmethod
def update_from_json(cls, course_location, jsondict): def update_from_json(cls, course_location, jsondict):
""" """
Decode the json into CourseMetadata and save any changed attrs to the db. Decode the json into CourseMetadata and save any changed attrs to the db.
Ensures none of the fields are in the blacklist. Ensures none of the fields are in the blacklist.
""" """
descriptor = get_modulestore(course_location).get_item(course_location) descriptor = get_modulestore(course_location).get_item(course_location)
...@@ -42,12 +46,18 @@ class CourseMetadata(object): ...@@ -42,12 +46,18 @@ class CourseMetadata(object):
for k, v in jsondict.iteritems(): for k, v in jsondict.iteritems():
# should it be an error if one of the filtered list items is in the payload? # 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 dirty = True
descriptor.metadata[k] = v setattr(descriptor.lms, k, v)
if dirty: 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 # 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 # it persisted correctly
...@@ -61,10 +71,11 @@ class CourseMetadata(object): ...@@ -61,10 +71,11 @@ class CourseMetadata(object):
descriptor = get_modulestore(course_location).get_item(course_location) descriptor = get_modulestore(course_location).get_item(course_location)
for key in payload['deleteKeys']: for key in payload['deleteKeys']:
if key in descriptor.metadata: if hasattr(descriptor, key):
del descriptor.metadata[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) return cls.fetch(course_location)
\ No newline at end of file
...@@ -96,6 +96,13 @@ CACHES = { ...@@ -96,6 +96,13 @@ CACHES = {
'KEY_PREFIX': 'general', 'KEY_PREFIX': 'general',
'VERSION': 4, 'VERSION': 4,
'KEY_FUNCTION': 'util.memcache.safe_key', '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 = { ...@@ -98,6 +98,13 @@ CACHES = {
'KEY_PREFIX': 'general', 'KEY_PREFIX': 'general',
'VERSION': 4, 'VERSION': 4,
'KEY_FUNCTION': 'util.memcache.safe_key', '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 dogapi import dog_http_api, dog_stats_api
from django.conf import settings 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'): if hasattr(settings, 'DATADOG_API'):
dog_http_api.api_key = settings.DATADOG_API dog_http_api.api_key = settings.DATADOG_API
......
<li class="field-group course-advanced-policy-list-item"> <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> <label for="<%= keyUniqueId %>">Policy Key:</label>
<input type="text" class="short policy-key" id="<%= keyUniqueId %>" value="<%= key %>" /> <input readonly title="This field is disabled: policy keys cannot be edited." 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>
</div> </div>
<div class="field text value"> <div class="field text value">
<label for="<%= valueUniqueId %>">Policy Value:</label> <label for="<%= valueUniqueId %>">Policy Value:</label>
<textarea class="json text" id="<%= valueUniqueId %>"><%= value %></textarea> <textarea class="json text" id="<%= valueUniqueId %>"><%= value %></textarea>
</div> </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> </li>
\ No newline at end of file
if (!CMS.Models['Settings']) CMS.Models.Settings = {}; if (!CMS.Models['Settings']) CMS.Models.Settings = {};
CMS.Models.Settings.Advanced = Backbone.Model.extend({ 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: { defaults: {
// the properties are whatever the user types in (in addition to whatever comes originally from the server) // 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 // which keys to send as the deleted keys on next save
deleteKeys : [], deleteKeys : [],
blacklistKeys : [], // an array which the controller should populate directly for now [static not instance based]
validate: function (attrs) { validate: function (attrs) {
var errors = {}; // Keys can no longer be edited. We are currently not validating values.
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;
}, },
save : function (attrs, options) { save : function (attrs, options) {
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
if (typeof window.templateLoader == 'function') return; if (typeof window.templateLoader == 'function') return;
var templateLoader = { var templateLoader = {
templateVersion: "0.0.15", templateVersion: "0.0.16",
templates: {}, templates: {},
loadRemoteTemplate: function(templateName, filename, callback) { loadRemoteTemplate: function(templateName, filename, callback) {
if (!this.templates[templateName]) { if (!this.templates[templateName]) {
......
...@@ -6,14 +6,6 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ ...@@ -6,14 +6,6 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
// Model class is CMS.Models.Settings.Advanced // Model class is CMS.Models.Settings.Advanced
events : { 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", 'focus :input' : "focusInput",
'blur :input' : "blurInput" 'blur :input' : "blurInput"
// TODO enable/disable save based on validation (currently enabled whenever there are changes) // 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({ ...@@ -95,16 +87,11 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
mirror.setValue(stringValue); mirror.setValue(stringValue);
} catch(quotedE) { } catch(quotedE) {
// TODO: validation error // TODO: validation error
console.log("Error with JSON, even after converting to String."); // console.log("Error with JSON, even after converting to String.");
console.log(quotedE); // console.log(quotedE);
JSONValue = undefined; JSONValue = undefined;
} }
} }
else {
// TODO: validation error
console.log("Error with JSON, but will not convert to String.");
console.log(e);
}
} }
if (JSONValue !== undefined) { if (JSONValue !== undefined) {
self.clearValidationErrors(); self.clearValidationErrors();
...@@ -113,7 +100,6 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ ...@@ -113,7 +100,6 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
} }
}); });
}, },
showMessage: function (type) { showMessage: function (type) {
this.$el.find(".message-status").removeClass("is-shown"); this.$el.find(".message-status").removeClass("is-shown");
if (type) { if (type) {
...@@ -128,56 +114,19 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ ...@@ -128,56 +114,19 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
else { else {
// This is the case of the page first rendering, or when Cancel is pressed. // This is the case of the page first rendering, or when Cancel is pressed.
this.hideSaveCancelButtons(); this.hideSaveCancelButtons();
this.toggleNewButton(true);
} }
}, },
showSaveCancelButtons: function(event) { showSaveCancelButtons: function(event) {
if (!this.buttonsVisible) { 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"); this.$el.find(".message-status").removeClass("is-shown");
$('.wrapper-notification').addClass('is-shown'); $('.wrapper-notification').addClass('is-shown');
this.buttonsVisible = true; this.buttonsVisible = true;
} }
}, },
hideSaveCancelButtons: function() { hideSaveCancelButtons: function() {
$('.wrapper-notification').removeClass('is-shown'); $('.wrapper-notification').removeClass('is-shown');
this.buttonsVisible = false; 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) { saveView : function(event) {
// TODO one last verification scan: // TODO one last verification scan:
// call validateKey on each to ensure proper format // call validateKey on each to ensure proper format
...@@ -201,102 +150,15 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ ...@@ -201,102 +150,15 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
error : CMS.ServerError 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) { renderTemplate: function (key, value) {
var newKeyId = _.uniqueId('policy_key_'), var newKeyId = _.uniqueId('policy_key_'),
newEle = this.template({ key : key, value : JSON.stringify(value, null, 4), newEle = this.template({ key : key, value : JSON.stringify(value, null, 4),
keyUniqueId: newKeyId, valueUniqueId: _.uniqueId('policy_value_')}); keyUniqueId: newKeyId, valueUniqueId: _.uniqueId('policy_value_')});
this.fieldToSelectorMap[(_.isEmpty(key) ? this.model.new_key : key)] = newKeyId; this.fieldToSelectorMap[key] = newKeyId;
this.selectorToField[newKeyId] = (_.isEmpty(key) ? this.model.new_key : key); this.selectorToField[newKeyId] = key;
return newEle; return newEle;
}, },
focusInput : function(event) { focusInput : function(event) {
$(event.target).prev().addClass("is-focused"); $(event.target).prev().addClass("is-focused");
}, },
......
...@@ -240,13 +240,9 @@ body.course.settings { ...@@ -240,13 +240,9 @@ body.course.settings {
// not editable fields // not editable fields
.field.is-not-editable { .field.is-not-editable {
label, .label { & label.is-focused {
color: $gray-l3; color: $gray-d2;
}
input {
opacity: 0.25;
} }
} }
......
...@@ -6,10 +6,10 @@ ...@@ -6,10 +6,10 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title> <title>
<%block name="title"></%block> | <%block name="title"></%block> |
% if context_course: % if context_course:
<% ctx_loc = context_course.location %> <% ctx_loc = context_course.location %>
${context_course.display_name} | ${context_course.display_name_with_default} |
% endif % endif
edX Studio edX Studio
</title> </title>
...@@ -22,7 +22,7 @@ ...@@ -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('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-symbolicons-block.css')}" />
<link rel="stylesheet" type="text/css" href="${static.url('css/vendor/symbolset.ss-standard.css')}" /> <link rel="stylesheet" type="text/css" href="${static.url('css/vendor/symbolset.ss-standard.css')}" />
<%block name="header_extras"></%block> <%block name="header_extras"></%block>
</head> </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 @@ ...@@ -22,7 +22,7 @@
<article class="subsection-body window" data-id="${subsection.location}"> <article class="subsection-body window" data-id="${subsection.location}">
<div class="subsection-name-input"> <div class="subsection-name-input">
<label>Display Name:</label> <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>
<div class="sortable-unit-list"> <div class="sortable-unit-list">
<label>Units:</label> <label>Units:</label>
...@@ -40,7 +40,7 @@ ...@@ -40,7 +40,7 @@
<a href="#" class="save-button">Save</a> <a href="#" class="save-button">Save</a>
<a href="#" class="cancel-button">Cancel</a> <a href="#" class="cancel-button">Cancel</a>
<a href="#" class="delete-icon remove-policy-data"></a> <a href="#" class="delete-icon remove-policy-data"></a>
</li> </li>
</div> </div>
<div class="sidebar"> <div class="sidebar">
...@@ -51,28 +51,28 @@ ...@@ -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> <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"> <div class="datepair" data-language="javascript">
<% <%
start_date = datetime.fromtimestamp(mktime(subsection.start)) if subsection.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.start)) if parent_item.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_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> </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: % 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: % 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 % 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 % endif
</div> </div>
<div class="row gradable"> <div class="row gradable">
<label>Graded as:</label> <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>
<div class="due-date-input row"> <div class="due-date-input row">
<label>Due date:</label> <label>Due date:</label>
<a href="#" class="set-date">Set a due date</a> <a href="#" class="set-date">Set a due date</a>
...@@ -80,9 +80,9 @@ ...@@ -80,9 +80,9 @@
<p class="date-description"> <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 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"/> <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> <a href="#" class="remove-date">Remove due date</a>
</p> </p>
...@@ -110,7 +110,7 @@ ...@@ -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/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/models/settings/course_grading_policy.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/overview.js')}"></script> <script type="text/javascript" src="${static.url('js/views/overview.js')}"></script>
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() { $(document).ready(function() {
...@@ -128,7 +128,7 @@ ...@@ -128,7 +128,7 @@
window.graderTypes.course_location = new CMS.Models.Location('${parent_location}'); window.graderTypes.course_location = new CMS.Models.Location('${parent_location}');
window.graderTypes.reset(${course_graders|n}); window.graderTypes.reset(${course_graders|n});
} }
$(".gradable-status").each(function(index, ele) { $(".gradable-status").each(function(index, ele) {
var gradeView = new CMS.Views.OverviewAssignmentGrader({ var gradeView = new CMS.Views.OverviewAssignmentGrader({
el : ele, el : ele,
......
...@@ -21,14 +21,14 @@ ...@@ -21,14 +21,14 @@
<div class="description"> <div class="description">
<p><strong>Importing a new course will delete all content currently associated with your course <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> 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, <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> re-importing your course could cause the loss of student data associated with those problems.</p>
</div> </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"> <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> <h2>Course to import:</h2>
<p class="error-block"></p> <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> <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="file" name="course-data" class="file-input">
<input type="submit" value="Replace my course with the one above" class="submit-button"> <input type="submit" value="Replace my course with the one above" class="submit-button">
...@@ -45,13 +45,13 @@ ...@@ -45,13 +45,13 @@
<%block name="jsextra"> <%block name="jsextra">
<script> <script>
(function() { (function() {
var bar = $('.progress-bar'); var bar = $('.progress-bar');
var fill = $('.progress-fill'); var fill = $('.progress-fill');
var percent = $('.percent'); var percent = $('.percent');
var status = $('#status'); var status = $('#status');
var submitBtn = $('.submit-button'); var submitBtn = $('.submit-button');
$('form').ajaxForm({ $('form').ajaxForm({
beforeSend: function() { beforeSend: function() {
status.empty(); status.empty();
...@@ -76,7 +76,7 @@ $('form').ajaxForm({ ...@@ -76,7 +76,7 @@ $('form').ajaxForm({
submitBtn.show(); submitBtn.show();
bar.hide(); bar.hide();
} }
}); });
})(); })();
</script> </script>
</%block> </%block>
\ No newline at end of file
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
<div>${module_type}</div> <div>${module_type}</div>
<div> <div>
% for template in module_templates: % 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 % endfor
</div> </div>
</div> </div>
......
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
window.graderTypes.course_location = new CMS.Models.Location('${parent_location}'); window.graderTypes.course_location = new CMS.Models.Location('${parent_location}');
window.graderTypes.reset(${course_graders|n}); window.graderTypes.reset(${course_graders|n});
} }
$(".gradable-status").each(function(index, ele) { $(".gradable-status").each(function(index, ele) {
var gradeView = new CMS.Views.OverviewAssignmentGrader({ var gradeView = new CMS.Views.OverviewAssignmentGrader({
el : ele, el : ele,
...@@ -40,7 +40,7 @@ ...@@ -40,7 +40,7 @@
}); });
}); });
}); });
</script> </script>
</%block> </%block>
...@@ -146,7 +146,7 @@ ...@@ -146,7 +146,7 @@
<div class="main-wrapper"> <div class="main-wrapper">
<div class="inner-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: % for section in sections:
<section class="courseware-section branch" data-id="${section.location}"> <section class="courseware-section branch" data-id="${section.location}">
<header> <header>
...@@ -154,16 +154,16 @@ ...@@ -154,16 +154,16 @@
<div class="item-details" data-id="${section.location}"> <div class="item-details" data-id="${section.location}">
<h3 class="section-name"> <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"> <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="submit" class="save-button edit-section-name-save" value="Save" />
<input type="button" class="cancel-button edit-section-name-cancel" value="Cancel" /> <input type="button" class="cancel-button edit-section-name-cancel" value="Cancel" />
</form> </form>
</h3> </h3>
<div class="section-published-date"> <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_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 '' start_time_str = start_date.strftime('%H:%M') if start_date is not None else ''
%> %>
...@@ -174,9 +174,9 @@ ...@@ -174,9 +174,9 @@
<span class="published-status"><strong>Will Release:</strong> ${start_date_str} at ${start_time_str}</span> <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> <a href="#" class="edit-button" data-date="${start_date_str}" data-time="${start_time_str}" data-id="${section.location}">Edit</a>
%endif %endif
</div> </div>
</div> </div>
<div class="item-actions"> <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="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> <a href="#" data-tooltip="Drag to reorder" class="drag-handle"></a>
...@@ -196,15 +196,15 @@ ...@@ -196,15 +196,15 @@
<a href="#" data-tooltip="Expand/collapse this subsection" class="expand-collapse-icon expand"></a> <a href="#" data-tooltip="Expand/collapse this subsection" class="expand-collapse-icon expand"></a>
<a href="${reverse('edit_subsection', args=[subsection.location])}"> <a href="${reverse('edit_subsection', args=[subsection.location])}">
<span class="folder-icon"></span> <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> </a>
</div> </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>
<div class="item-actions"> <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> <a href="#" data-tooltip="Drag to reorder" class="drag-handle"></a>
</div> </div>
</div> </div>
......
...@@ -70,17 +70,17 @@ from contentstore import utils ...@@ -70,17 +70,17 @@ from contentstore import utils
<ol class="list-input"> <ol class="list-input">
<li class="field text is-not-editable" id="field-course-organization"> <li class="field text is-not-editable" id="field-course-organization">
<label for="course-organization">Organization</label> <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>
<li class="field text is-not-editable" id="field-course-number"> <li class="field text is-not-editable" id="field-course-number">
<label for="course-number">Course Number</label> <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>
<li class="field text is-not-editable" id="field-course-name"> <li class="field text is-not-editable" id="field-course-name">
<label for="course-name">Course Name</label> <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> </li>
</ol> </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> <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 () { ...@@ -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 // 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}); 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))}"; 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({ var editor = new CMS.Views.Settings.Advanced({
...@@ -61,18 +60,11 @@ editor.render(); ...@@ -61,18 +60,11 @@ editor.render();
<span class="tip">Manually Edit Course Policy Values (JSON Key / Value pairs)</span> <span class="tip">Manually Edit Course Policy Values (JSON Key / Value pairs)</span>
</header> </header>
<p class="instructions"><strong>Warning</strong>: Add only manual policy data that you are familiar <p class="instructions"><strong>Warning</strong>: Do not modify these policies unless you are familiar with their purpose.</p>
with.</p>
<ul class="list-input course-advanced-policy-list enum"> <ul class="list-input course-advanced-policy-list enum">
</ul> </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> </section>
</form> </form>
</article> </article>
...@@ -80,9 +72,9 @@ editor.render(); ...@@ -80,9 +72,9 @@ editor.render();
<aside class="content-supplementary" role="complimentary"> <aside class="content-supplementary" role="complimentary">
<div class="bit"> <div class="bit">
<h3 class="title-3">How will these settings be used?</h3> <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>
<div class="bit"> <div class="bit">
...@@ -110,7 +102,7 @@ editor.render(); ...@@ -110,7 +102,7 @@ editor.render();
<i class="ss-icon ss-symbolicons-block icon icon-warning">&#x26A0;</i> <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 <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>
<div class="actions"> <div class="actions">
......
...@@ -44,12 +44,12 @@ ...@@ -44,12 +44,12 @@
</div> </div>
<div class="main-column"> <div class="main-column">
<article class="unit-body window"> <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"> <ol class="components">
% for id in components: % for id in components:
<li class="component" data-id="${id}"/> <li class="component" data-id="${id}"/>
% endfor % endfor
<li class="new-component-item adding"> <li class="new-component-item adding">
<div class="new-component"> <div class="new-component">
<h5>Add New Component</h5> <h5>Add New Component</h5>
<ul class="new-component-type"> <ul class="new-component-type">
...@@ -86,7 +86,7 @@ ...@@ -86,7 +86,7 @@
<span class="name"> ${name}</span> <span class="name"> ${name}</span>
</a> </a>
</li> </li>
% else: % else:
<li class="editor-md"> <li class="editor-md">
<a href="#" data-location="${location}"> <a href="#" data-location="${location}">
...@@ -95,7 +95,7 @@ ...@@ -95,7 +95,7 @@
</li> </li>
% endif % endif
% endif % endif
%endfor %endfor
</ul> </ul>
</div> </div>
...@@ -103,20 +103,20 @@ ...@@ -103,20 +103,20 @@
<div class="tab" id="tab2"> <div class="tab" id="tab2">
<ul class="new-component-template"> <ul class="new-component-template">
% for name, location, has_markdown, is_empty in templates: % for name, location, has_markdown, is_empty in templates:
% if not has_markdown: % if not has_markdown:
% if is_empty: % if is_empty:
<li class="editor-manual empty"> <li class="editor-manual empty">
<a href="#" data-location="${location}"> <a href="#" data-location="${location}">
<span class="name">${name}</span> <span class="name">${name}</span>
</a> </a>
</li> </li>
% else: % else:
<li class="editor-manual"> <li class="editor-manual">
<a href="#" data-location="${location}"> <a href="#" data-location="${location}">
<span class="name"> ${name}</span> <span class="name"> ${name}</span>
</a> </a>
</li> </li>
% endif % endif
% endif % endif
...@@ -147,13 +147,13 @@ ...@@ -147,13 +147,13 @@
<div class="row published-alert"> <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="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> <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"> <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: % if release_date is not None:
on <strong>${release_date}</strong> on <strong>${release_date}</strong>
% endif % endif
with the subsection <a href="${reverse('edit_subsection', kwargs={'location': subsection.location})}">"${subsection.display_name}"</a></p> with the subsection <a href="${reverse('edit_subsection', kwargs={'location': subsection.location})}">"${subsection.display_name_with_default}"</a></p>
</div> </div>
<div class="row unit-actions"> <div class="row unit-actions">
<a href="#" class="delete-draft delete-button">Delete Draft</a> <a href="#" class="delete-draft delete-button">Delete Draft</a>
...@@ -168,18 +168,18 @@ ...@@ -168,18 +168,18 @@
<div><input type="text" class="url" value="/courseware/${section.url_name}/${subsection.url_name}" disabled /></div> <div><input type="text" class="url" value="/courseware/${section.url_name}/${subsection.url_name}" disabled /></div>
<ol> <ol>
<li> <li>
<a href="#" class="section-item">${section.display_name}</a> <a href="#" class="section-item">${section.display_name_with_default}</a>
<ol> <ol>
<li> <li>
<a href="${reverse('edit_subsection', args=[subsection.location])}" class="section-item"> <a href="${reverse('edit_subsection', args=[subsection.location])}" class="section-item">
<span class="folder-icon"></span> <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> </a>
${units.enum_units(subsection, actions=False, selected=unit.location)} ${units.enum_units(subsection, actions=False, selected=unit.location)}
</li> </li>
</ol> </ol>
</li> </li>
</ol> </ol>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -5,24 +5,24 @@ ...@@ -5,24 +5,24 @@
<div class="wrapper wrapper-left "> <div class="wrapper wrapper-left ">
<h1 class="branding"><a href="/">edX Studio</a></h1> <h1 class="branding"><a href="/">edX Studio</a></h1>
% if context_course: % if context_course:
<% ctx_loc = context_course.location %> <% ctx_loc = context_course.location %>
<div class="info-course"> <div class="info-course">
<h2 class="sr">Current Course:</h2> <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))}"> <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-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> </a>
</div> </div>
<nav class="nav-course primary nav-dropdown" role="navigation"> <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> <ol>
<li class="nav-item nav-course-courseware"> <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> <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="wrapper wrapper-nav-sub">
<div class="nav-sub"> <div class="nav-sub">
<ul> <ul>
...@@ -32,12 +32,12 @@ ...@@ -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> <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> </ul>
</div> </div>
</div> </div>
</li> </li>
<li class="nav-item nav-course-settings"> <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> <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="wrapper wrapper-nav-sub">
<div class="nav-sub"> <div class="nav-sub">
<ul> <ul>
...@@ -47,12 +47,12 @@ ...@@ -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> <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> </ul>
</div> </div>
</div> </div>
</li> </li>
<li class="nav-item nav-course-tools"> <li class="nav-item nav-course-tools">
<h3 class="title">Tools <i class="ss-icon ss-symbolicons-block icon-expand">&#x25BE;</i></h3> <h3 class="title">Tools <i class="ss-icon ss-symbolicons-block icon-expand">&#x25BE;</i></h3>
<div class="wrapper wrapper-nav-sub"> <div class="wrapper wrapper-nav-sub">
<div class="nav-sub"> <div class="nav-sub">
<ul> <ul>
...@@ -66,16 +66,16 @@ ...@@ -66,16 +66,16 @@
</nav> </nav>
% endif % endif
</div> </div>
<div class="wrapper wrapper-right"> <div class="wrapper wrapper-right">
% if user.is_authenticated(): % if user.is_authenticated():
<nav class="nav-account nav-is-signedin nav-dropdown"> <nav class="nav-account nav-is-signedin nav-dropdown">
<h2 class="sr">Currently logged in as:</h2> <h2 class="sr">Currently logged in as:</h2>
<ol> <ol>
<li class="nav-item nav-account-username"> <li class="nav-item nav-account-username">
<a href="#" class="title"> <a href="#" class="title">
<span class="account-username"> <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 } ${ user.username }
</span> </span>
<i class="ss-icon ss-symbolicons-block icon-expand">&#x25BE;</i> <i class="ss-icon ss-symbolicons-block icon-expand">&#x25BE;</i>
...@@ -111,7 +111,7 @@ ...@@ -111,7 +111,7 @@
</li> </li>
</ol> </ol>
</nav> </nav>
% endif % endif
</div> </div>
</header> </header>
</div> </div>
\ No newline at end of file
% if metadata:
<% <%
import hashlib import hashlib
hlskey = hashlib.md5(module.location.url()).hexdigest() hlskey = hashlib.md5(module.location.url()).hexdigest()
%> %>
<section class="metadata_edit"> <section class="metadata_edit">
<ul> <ul>
% for keyname in editable_metadata_fields: % for field_name, field_value in editable_metadata_fields.items():
<li> <li>
% if keyname=='source_code': % if field_name == 'source_code':
<a href="#hls-modal-${hlskey}" style="color:yellow;" id="hls-trig-${hlskey}" >Edit High Level Source</a> <a href="#hls-modal-${hlskey}" style="color:yellow;" id="hls-trig-${hlskey}" >Edit High Level Source</a>
% else: % else:
<label>${keyname}:</label> <label>${field_name}:</label>
<input type='text' data-metadata-name='${keyname}' value='${metadata[keyname]}' size='60' /> <input type='text' data-metadata-name='${field_name}' value='${field_value}' size='60' />
% endif % endif
</li> </li>
% endfor % endfor
</ul> </ul>
...@@ -22,4 +21,3 @@ ...@@ -22,4 +21,3 @@
% endif % endif
</section> </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 @@ ...@@ -40,7 +40,7 @@
<a href="#" class="module-edit" <a href="#" class="module-edit"
data-id="${child.location.url()}" data-id="${child.location.url()}"
data-type="${child.js_module_name}" 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> <a href="#" class="draggable">handle</a>
</li> </li>
%endfor %endfor
......
...@@ -22,7 +22,7 @@ This def will enumerate through a passed in subsection and list all of the units ...@@ -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}"> <div class="section-item ${selected_class}">
<a href="${reverse('edit_unit', args=[unit.location])}" class="${unit_state}-item"> <a href="${reverse('edit_unit', args=[unit.location])}" class="${unit_state}-item">
<span class="${unit.category}-icon"></span> <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> </a>
% if actions: % if actions:
<div class="item-actions"> <div class="item-actions">
...@@ -39,7 +39,7 @@ This def will enumerate through a passed in subsection and list all of the units ...@@ -39,7 +39,7 @@ This def will enumerate through a passed in subsection and list all of the units
</a> </a>
</li> </li>
</ol> </ol>
</%def> </%def>
import datetime
from xblock.core import Namespace, Boolean, Scope, ModelType, String
class StringyBoolean(Boolean):
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):
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): ...@@ -76,7 +76,7 @@ class TestCohorts(django.test.TestCase):
"id": to_id(name)}) "id": to_id(name)})
for name in discussions) for name in discussions)
course.metadata["discussion_topics"] = topics course.discussion_topics = topics
d = {"cohorted": cohorted} d = {"cohorted": cohorted}
if cohorted_discussions is not None: if cohorted_discussions is not None:
...@@ -88,7 +88,7 @@ class TestCohorts(django.test.TestCase): ...@@ -88,7 +88,7 @@ class TestCohorts(django.test.TestCase):
if auto_cohort_groups is not None: if auto_cohort_groups is not None:
d["auto_cohort_groups"] = auto_cohort_groups d["auto_cohort_groups"] = auto_cohort_groups
course.metadata["cohort_config"] = d course.cohort_config = d
def setUp(self): def setUp(self):
......
...@@ -4,7 +4,7 @@ import os ...@@ -4,7 +4,7 @@ import os
from django.test.utils import override_settings from django.test.utils import override_settings
from tempfile import NamedTemporaryFile 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 # Get a name where we can put test files
TMP_FILE = NamedTemporaryFile(delete=False) TMP_FILE = NamedTemporaryFile(delete=False)
......
...@@ -44,9 +44,8 @@ from collections import namedtuple ...@@ -44,9 +44,8 @@ from collections import namedtuple
from courseware.courses import get_courses, sort_by_announcement from courseware.courses import get_courses, sort_by_announcement
from courseware.access import has_access from courseware.access import has_access
from courseware.models import StudentModuleCache
from courseware.views import get_module_for_descriptor, jump_to 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 from statsd import statsd
...@@ -318,7 +317,7 @@ def change_enrollment(request): ...@@ -318,7 +317,7 @@ def change_enrollment(request):
if not has_access(user, course, 'enroll'): if not has_access(user, course, 'enroll'):
return {'success': False, return {'success': False,
'error': 'enrollment in {} not allowed at this time' '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("/") org, course_num, run = course_id.split("/")
statsd.increment("common.student.enrollment", statsd.increment("common.student.enrollment",
...@@ -1071,14 +1070,14 @@ def accept_name_change(request): ...@@ -1071,14 +1070,14 @@ def accept_name_change(request):
@csrf_exempt @csrf_exempt
def test_center_login(request): 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. # which contains the error code describing the exceptional condition.
def makeErrorURL(error_url, error_code): def makeErrorURL(error_url, error_code):
log.error("generating error URL with error code {}".format(error_code)) log.error("generating error URL with error code {}".format(error_code))
return "{}?code={}".format(error_url, 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 # 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") error_url = request.POST.get("errorURL")
# TODO: check that the parameters have not been tampered with, by comparing the code provided by Pearson # 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): ...@@ -1089,12 +1088,12 @@ def test_center_login(request):
# calculate SHA for query string # calculate SHA for query string
# TODO: figure out how to get the original query string, so we can hash it and compare. # TODO: figure out how to get the original query string, so we can hash it and compare.
if 'clientCandidateID' not in request.POST: if 'clientCandidateID' not in request.POST:
return HttpResponseRedirect(makeErrorURL(error_url, "missingClientCandidateID")); return HttpResponseRedirect(makeErrorURL(error_url, "missingClientCandidateID"));
client_candidate_id = request.POST.get("clientCandidateID") client_candidate_id = request.POST.get("clientCandidateID")
# TODO: check remaining parameters, and maybe at least log if they're not matching # TODO: check remaining parameters, and maybe at least log if they're not matching
# expected values.... # expected values....
# registration_id = request.POST.get("registrationID") # registration_id = request.POST.get("registrationID")
...@@ -1108,12 +1107,12 @@ def test_center_login(request): ...@@ -1108,12 +1107,12 @@ def test_center_login(request):
return HttpResponseRedirect(makeErrorURL(error_url, "invalidClientCandidateID")); return HttpResponseRedirect(makeErrorURL(error_url, "invalidClientCandidateID"));
# find testcenter_registration that matches the provided exam code: # find testcenter_registration that matches the provided exam code:
# Note that we could rely in future on either the registrationId or the 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, # 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. # while we currently have no record of RegistrationID values at all.
if 'vueExamSeriesCode' not in request.POST: if 'vueExamSeriesCode' not in request.POST:
# we are not allowed to make up a new error code, according to Pearson, # 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 # so instead of "missingExamSeriesCode", we use a valid one that is
# inaccurate but at least distinct. (Sigh.) # inaccurate but at least distinct. (Sigh.)
log.error("missing exam series code for cand ID {}".format(client_candidate_id)) log.error("missing exam series code for cand ID {}".format(client_candidate_id))
return HttpResponseRedirect(makeErrorURL(error_url, "missingPartnerID")); return HttpResponseRedirect(makeErrorURL(error_url, "missingPartnerID"));
...@@ -1127,11 +1126,11 @@ def test_center_login(request): ...@@ -1127,11 +1126,11 @@ def test_center_login(request):
if not registrations: if not registrations:
log.error("not able to find exam registration for exam {} and cand ID {}".format(exam_series_code, client_candidate_id)) 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")); return HttpResponseRedirect(makeErrorURL(error_url, "noTestsAssigned"));
# TODO: figure out what to do if there are more than one registrations.... # TODO: figure out what to do if there are more than one registrations....
# for now, just take the first... # for now, just take the first...
registration = registrations[0] registration = registrations[0]
course_id = registration.course_id course_id = registration.course_id
course = course_from_id(course_id) # assume it will be found.... course = course_from_id(course_id) # assume it will be found....
if not course: if not course:
...@@ -1149,19 +1148,19 @@ def test_center_login(request): ...@@ -1149,19 +1148,19 @@ def test_center_login(request):
if not timelimit_descriptor: 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)) 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")); return HttpResponseRedirect(makeErrorURL(error_url, "missingClientProgram"));
timelimit_module_cache = StudentModuleCache.cache_for_descriptor_descendents(course_id, testcenteruser.user, timelimit_module_cache = ModelDataCache.cache_for_descriptor_descendents(course_id, testcenteruser.user,
timelimit_descriptor, depth=None) timelimit_descriptor, depth=None)
timelimit_module = get_module_for_descriptor(request.user, request, timelimit_descriptor, timelimit_module = get_module_for_descriptor(request.user, request, timelimit_descriptor,
timelimit_module_cache, course_id, position=None) timelimit_module_cache, course_id, position=None)
if not timelimit_module.category == 'timelimit': 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)) 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")); return HttpResponseRedirect(makeErrorURL(error_url, "missingClientProgram"));
if timelimit_module and timelimit_module.has_ended: 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)) 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")); return HttpResponseRedirect(makeErrorURL(error_url, "allTestsTaken"));
# check if we need to provide an accommodation: # check if we need to provide an accommodation:
time_accommodation_mapping = {'ET12ET' : 'ADDHALFTIME', time_accommodation_mapping = {'ET12ET' : 'ADDHALFTIME',
'ET30MN' : 'ADD30MIN', 'ET30MN' : 'ADD30MIN',
...@@ -1174,27 +1173,24 @@ def test_center_login(request): ...@@ -1174,27 +1173,24 @@ def test_center_login(request):
# special, hard-coded client ID used by Pearson shell for testing: # special, hard-coded client ID used by Pearson shell for testing:
if client_candidate_id == "edX003671291147": if client_candidate_id == "edX003671291147":
time_accommodation_code = 'TESTING' time_accommodation_code = 'TESTING'
if time_accommodation_code: if time_accommodation_code:
timelimit_module.accommodation_code = 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)) log.info("cand {} on exam {} for course {}: receiving accommodation {}".format(client_candidate_id, exam_series_code, course_id, time_accommodation_code))
# UGLY HACK!!! # 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 # backend annotation on the user object, indicating which backend
# against which the user was authenticated. We're authenticating here # against which the user was authenticated. We're authenticating here
# against the registration entry, and assuming that the request given # against the registration entry, and assuming that the request given
# this information is correct, we allow the user to be logged in # this information is correct, we allow the user to be logged in
# without a password. This could all be formalized in a backend object # 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. # TODO: (brian) create a backend class to do this.
# testcenteruser.user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__) # testcenteruser.user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__)
testcenteruser.user.backend = "%s.%s" % ("TestcenterAuthenticationModule", "TestcenterAuthenticationClass") testcenteruser.user.backend = "%s.%s" % ("TestcenterAuthenticationModule", "TestcenterAuthenticationClass")
login(request, testcenteruser.user) login(request, testcenteruser.user)
# And start the test: # And start the test:
return jump_to(request, course_id, location) return jump_to(request, course_id, location)
......
...@@ -7,13 +7,14 @@ from xmodule.modulestore.django import modulestore ...@@ -7,13 +7,14 @@ from xmodule.modulestore.django import modulestore
from time import gmtime from time import gmtime
from uuid import uuid4 from uuid import uuid4
from xmodule.timeparse import stringify_time from xmodule.timeparse import stringify_time
from xmodule.modulestore.inheritance import own_metadata
class GroupFactory(Factory): class GroupFactory(Factory):
FACTORY_FOR = Group FACTORY_FOR = Group
name = 'staff_MITx/999/Robot_Super_Course' name = 'staff_MITx/999/Robot_Super_Course'
class UserProfileFactory(Factory): class UserProfileFactory(Factory):
FACTORY_FOR = UserProfile FACTORY_FOR = UserProfile
...@@ -81,18 +82,17 @@ class XModuleCourseFactory(Factory): ...@@ -81,18 +82,17 @@ class XModuleCourseFactory(Factory):
# This metadata code was copied from cms/djangoapps/contentstore/views.py # This metadata code was copied from cms/djangoapps/contentstore/views.py
if display_name is not None: 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.lms.start = gmtime()
new_course.metadata['start'] = stringify_time(gmtime())
new_course.tabs = [{"type": "courseware"}, new_course.tabs = [{"type": "courseware"},
{"type": "course_info", "name": "Course Info"}, {"type": "course_info", "name": "Course Info"},
{"type": "discussion", "name": "Discussion"}, {"type": "discussion", "name": "Discussion"},
{"type": "wiki", "name": "Wiki"}, {"type": "wiki", "name": "Wiki"},
{"type": "progress", "name": "Progress"}] {"type": "progress", "name": "Progress"}]
# Update the data in the mongo datastore # 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 return new_course
...@@ -139,17 +139,14 @@ class XModuleItemFactory(Factory): ...@@ -139,17 +139,14 @@ class XModuleItemFactory(Factory):
new_item = store.clone_item(template, dest_location) 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 # replace the display name with an optional parameter passed in from the caller
if display_name is not None: 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: 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 return new_item
......
from lettuce import world, step from lettuce import world, step
from factories import * from .factories import *
from lettuce.django import django_url from lettuce.django import django_url
from django.contrib.auth.models import User from django.contrib.auth.models import User
from student.models import CourseEnrollment from student.models import CourseEnrollment
......
...@@ -33,7 +33,7 @@ def wrap_xmodule(get_html, module, template, context=None): ...@@ -33,7 +33,7 @@ def wrap_xmodule(get_html, module, template, context=None):
def _get_html(): def _get_html():
context.update({ context.update({
'content': get_html(), '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__, 'class_': module.__class__.__name__,
'module_name': module.js_module_name 'module_name': module.js_module_name
}) })
...@@ -108,42 +108,25 @@ def add_histogram(get_html, module, user): ...@@ -108,42 +108,25 @@ def add_histogram(get_html, module, user):
histogram = grade_histogram(module_id) histogram = grade_histogram(module_id)
render_histogram = len(histogram) > 0 render_histogram = len(histogram) > 0
# TODO (ichuang): Remove after fall 2012 LMS migration done source_file = module.lms.source_file # source used to generate the problem XML, eg latex or word
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
# useful to indicate to staff if problem has been released or not # 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 # TODO (ichuang): use _has_access_descriptor.can_load in lms.courseware.access, instead of now>mstart comparison here
now = time.gmtime() now = time.gmtime()
is_released = "unknown" is_released = "unknown"
mstart = getattr(module.descriptor, 'start') mstart = module.descriptor.lms.start
if mstart is not None: if mstart is not None:
is_released = "<font color='red'>Yes!</font>" if (now > mstart) else "<font color='green'>Not yet</font>" is_released = "<font color='red'>Yes!</font>" if (now > mstart) else "<font color='green'>Not yet</font>"
staff_context = {'definition': module.definition.get('data'), staff_context = {'fields': [(field.name, getattr(module, field.name)) for field in module.fields],
'metadata': json.dumps(module.metadata, indent=4), 'lms_fields': [(field.name, getattr(module.lms, field.name)) for field in module.lms.fields],
'location': module.location, 'location': module.location,
'xqa_key': module.metadata.get('xqa_key', ''), 'xqa_key': module.lms.xqa_key,
'source_file': source_file, 'source_file': source_file,
'source_url': '%s/%s/tree/master/%s' % (giturl, data_dir, source_file),
'category': str(module.__class__.__name__), 'category': str(module.__class__.__name__),
# Template uses element_id in js function names, so can't allow dashes # Template uses element_id in js function names, so can't allow dashes
'element_id': module.location.html_id().replace('-', '_'), 'element_id': module.location.html_id().replace('-', '_'),
'edit_link': edit_link,
'user': user, 'user': user,
'xqa_server': settings.MITX_FEATURES.get('USE_XQA_SERVER', 'http://xqa:server@content-qa.mitx.mit.edu/xqa'), 'xqa_server': settings.MITX_FEATURES.get('USE_XQA_SERVER', 'http://xqa:server@content-qa.mitx.mit.edu/xqa'),
'histogram': json.dumps(histogram), 'histogram': json.dumps(histogram),
......
...@@ -39,11 +39,11 @@ import verifiers ...@@ -39,11 +39,11 @@ import verifiers
import verifiers.draganddrop import verifiers.draganddrop
import calc import calc
from correctmap import CorrectMap from .correctmap import CorrectMap
import eia import eia
import inputtypes import inputtypes
import customrender import customrender
from util import contextualize_text, convert_files_to_filenames from .util import contextualize_text, convert_files_to_filenames
import xqueue_interface import xqueue_interface
# to be replaced with auto-registering # to be replaced with auto-registering
...@@ -78,7 +78,7 @@ global_context = {'random': random, ...@@ -78,7 +78,7 @@ global_context = {'random': random,
# These should be removed from HTML output, including all subelements # These should be removed from HTML output, including all subelements
html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup", "openendedparam", "openendedrubric"] html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup", "openendedparam", "openendedrubric"]
log = logging.getLogger('mitx.' + __name__) log = logging.getLogger(__name__)
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
# main class for this module # main class for this module
...@@ -108,6 +108,8 @@ class LoncapaProblem(object): ...@@ -108,6 +108,8 @@ class LoncapaProblem(object):
self.do_reset() self.do_reset()
self.problem_id = id self.problem_id = id
self.system = system self.system = system
if self.system is None:
raise Exception()
self.seed = seed self.seed = seed
if state: if state:
......
...@@ -12,8 +12,8 @@ from path import path ...@@ -12,8 +12,8 @@ from path import path
from cStringIO import StringIO from cStringIO import StringIO
from collections import defaultdict from collections import defaultdict
from calc import UndefinedVariable from .calc import UndefinedVariable
from capa_problem import LoncapaProblem from .capa_problem import LoncapaProblem
from mako.lookup import TemplateLookup from mako.lookup import TemplateLookup
logging.basicConfig(format="%(levelname)s %(message)s") logging.basicConfig(format="%(levelname)s %(message)s")
......
...@@ -2,7 +2,7 @@ import codecs ...@@ -2,7 +2,7 @@ import codecs
from fractions import Fraction from fractions import Fraction
import unittest 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) render_to_html, chemical_equations_equal)
import miller import miller
...@@ -277,7 +277,6 @@ class Test_Render_Equations(unittest.TestCase): ...@@ -277,7 +277,6 @@ class Test_Render_Equations(unittest.TestCase):
def test_render9(self): def test_render9(self):
s = "5[Ni(NH3)4]^2+ + 5/2SO4^2-" s = "5[Ni(NH3)4]^2+ + 5/2SO4^2-"
#import ipdb; ipdb.set_trace()
out = render_to_html(s) 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>' 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') log(out + ' ------- ' + correct, 'html')
......
...@@ -47,7 +47,7 @@ class CorrectMap(object): ...@@ -47,7 +47,7 @@ class CorrectMap(object):
queuestate=None, **kwargs): queuestate=None, **kwargs):
if answer_id is not None: if answer_id is not None:
self.cmap[answer_id] = {'correctness': correctness, self.cmap[str(answer_id)] = {'correctness': correctness,
'npoints': npoints, 'npoints': npoints,
'msg': msg, 'msg': msg,
'hint': hint, 'hint': hint,
......
...@@ -6,7 +6,7 @@ These tags do not have state, so they just get passed the system (for access to ...@@ -6,7 +6,7 @@ These tags do not have state, so they just get passed the system (for access to
and the xml element. and the xml element.
""" """
from registry import TagRegistry from .registry import TagRegistry
import logging import logging
import re import re
...@@ -15,9 +15,9 @@ import json ...@@ -15,9 +15,9 @@ import json
from lxml import etree from lxml import etree
import xml.sax.saxutils as saxutils 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() registry = TagRegistry()
......
...@@ -47,10 +47,10 @@ import sys ...@@ -47,10 +47,10 @@ import sys
import os import os
import pyparsing import pyparsing
from registry import TagRegistry from .registry import TagRegistry
from capa.chem import chemcalc from capa.chem import chemcalc
log = logging.getLogger('mitx.' + __name__) log = logging.getLogger(__name__)
######################################################################### #########################################################################
...@@ -857,6 +857,10 @@ class DragAndDropInput(InputTypeBase): ...@@ -857,6 +857,10 @@ class DragAndDropInput(InputTypeBase):
if tag_type == 'draggable' and not self.no_labels: if tag_type == 'draggable' and not self.no_labels:
dic['label'] = dic['label'] or dic['id'] 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 return dic
# add labels to images?: # add labels to images?:
......
...@@ -28,15 +28,15 @@ from collections import namedtuple ...@@ -28,15 +28,15 @@ from collections import namedtuple
from shapely.geometry import Point, MultiPoint from shapely.geometry import Point, MultiPoint
# specific library imports # specific library imports
from calc import evaluator, UndefinedVariable from .calc import evaluator, UndefinedVariable
from correctmap import CorrectMap from .correctmap import CorrectMap
from datetime import datetime from datetime import datetime
from util import * from .util import *
from lxml import etree from lxml import etree
from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful Soup!!! FIXME? from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful Soup!!! FIXME?
import xqueue_interface import xqueue_interface
log = logging.getLogger('mitx.' + __name__) log = logging.getLogger(__name__)
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
...@@ -231,16 +231,14 @@ class LoncapaResponse(object): ...@@ -231,16 +231,14 @@ class LoncapaResponse(object):
# hint specified by function? # hint specified by function?
hintfn = hintgroup.get('hintfn') hintfn = hintgroup.get('hintfn')
if hintfn: if hintfn:
''' # Hint is determined by a function defined in the <script> context; evaluate
Hint is determined by a function defined in the <script> context; evaluate # that function to obtain list of hint, hintmode for each answer_id.
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) # The function should take arguments (answer_ids, student_answers, new_cmap, old_cmap)
and it should modify new_cmap as appropriate. # and it should modify new_cmap as appropriate.
We may extend this in the future to add another argument which provides a # We may extend this in the future to add another argument which provides a
callback procedure to a social hint generation system. # callback procedure to a social hint generation system.
'''
if not hintfn in self.context: if not hintfn in self.context:
msg = 'missing specified hint function %s in script context' % hintfn msg = 'missing specified hint function %s in script context' % hintfn
msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline', '<unavailable>') msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline', '<unavailable>')
...@@ -329,7 +327,7 @@ class LoncapaResponse(object): ...@@ -329,7 +327,7 @@ class LoncapaResponse(object):
""" Render a <div> for a message that applies to the entire response. """ Render a <div> for a message that applies to the entire response.
*response_msg* is a string, which may contain XHTML markup *response_msg* is a string, which may contain XHTML markup
Returns an etree element representing the response message <div> """ Returns an etree element representing the response message <div> """
# First try wrapping the text in a <div> and parsing # First try wrapping the text in a <div> and parsing
# it as an XHTML tree # it as an XHTML tree
...@@ -872,7 +870,7 @@ class CustomResponse(LoncapaResponse): ...@@ -872,7 +870,7 @@ class CustomResponse(LoncapaResponse):
Custom response. The python code to be run should be in <answer>...</answer> Custom response. The python code to be run should be in <answer>...</answer>
or in a <script>...</script> or in a <script>...</script>
''' '''
snippets = [{'snippet': """<customresponse> snippets = [{'snippet': r"""<customresponse>
<text> <text>
<br/> <br/>
Suppose that \(I(t)\) rises from \(0\) to \(I_S\) at a time \(t_0 \neq 0\) Suppose that \(I(t)\) rises from \(0\) to \(I_S\) at a time \(t_0 \neq 0\)
...@@ -1104,7 +1102,7 @@ def sympy_check2(): ...@@ -1104,7 +1102,7 @@ def sympy_check2():
# the form: # the form:
# {'overall_message': STRING, # {'overall_message': STRING,
# 'input_list': [{ 'ok': BOOLEAN, 'msg': STRING }, ...] } # 'input_list': [{ 'ok': BOOLEAN, 'msg': STRING }, ...] }
# #
# This allows the function to return an 'overall message' # This allows the function to return an 'overall message'
# that applies to the entire problem, as well as correct/incorrect # that applies to the entire problem, as well as correct/incorrect
# status and messages for individual inputs # status and messages for individual inputs
...@@ -1197,7 +1195,7 @@ class SymbolicResponse(CustomResponse): ...@@ -1197,7 +1195,7 @@ class SymbolicResponse(CustomResponse):
""" """
Symbolic math response checking, using symmath library. 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) \] <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/> and give the resulting \(2\times 2\) matrix: <br/>
<symbolicresponse answer=""> <symbolicresponse answer="">
...@@ -1988,7 +1986,7 @@ class AnnotationResponse(LoncapaResponse): ...@@ -1988,7 +1986,7 @@ class AnnotationResponse(LoncapaResponse):
for inputfield in self.inputfields: for inputfield in self.inputfields:
option_scoring = dict([(option['id'], { option_scoring = dict([(option['id'], {
'correctness': choices.get(option['choice']), 'correctness': choices.get(option['choice']),
'points': scoring.get(option['choice']) 'points': scoring.get(option['choice'])
}) for option in self._find_options(inputfield) ]) }) for option in self._find_options(inputfield) ])
......
...@@ -7,7 +7,7 @@ import json ...@@ -7,7 +7,7 @@ import json
import mock import mock
from capa.capa_problem import LoncapaProblem from capa.capa_problem import LoncapaProblem
from response_xml_factory import StringResponseXMLFactory, CustomResponseXMLFactory from .response_xml_factory import StringResponseXMLFactory, CustomResponseXMLFactory
from . import test_system from . import test_system
class CapaHtmlRenderTest(unittest.TestCase): class CapaHtmlRenderTest(unittest.TestCase):
......
...@@ -557,14 +557,14 @@ class DragAndDropTest(unittest.TestCase): ...@@ -557,14 +557,14 @@ class DragAndDropTest(unittest.TestCase):
"target_outline": "false", "target_outline": "false",
"base_image": "/static/images/about_1.png", "base_image": "/static/images/about_1.png",
"draggables": [ "draggables": [
{"can_reuse": "", "label": "Label 1", "id": "1", "icon": ""}, {"can_reuse": "", "label": "Label 1", "id": "1", "icon": "", "target_fields": []},
{"can_reuse": "", "label": "cc", "id": "name_with_icon", "icon": "/static/images/cc.jpg", }, {"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": ""}, {"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": ""}, {"can_reuse": "", "label": "Label2", "id": "5", "icon": "", "can_reuse": "", "target_fields": []},
{"can_reuse": "", "label": "Mute", "id": "2", "icon": "/static/images/mute.png", "can_reuse": ""}, {"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": ""}, {"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": ""}, {"can_reuse": "", "label": "Star", "id": "name4", "icon": "/static/images/volume.png", "can_reuse": "", "target_fields": []},
{"can_reuse": "", "label": "Label3", "id": "7", "icon": "", "can_reuse": ""}], {"can_reuse": "", "label": "Label3", "id": "7", "icon": "", "can_reuse": "", "target_fields": []}],
"one_per_target": "True", "one_per_target": "True",
"targets": [ "targets": [
{"y": "90", "x": "210", "id": "t1", "w": "90", "h": "90"}, {"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. ...@@ -27,6 +27,49 @@ values are (x,y) coordinates of centers of dragged images.
import json 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 PositionsCompare(list):
""" Class for comparing positions. """ Class for comparing positions.
...@@ -116,37 +159,36 @@ class DragAndDrop(object): ...@@ -116,37 +159,36 @@ class DragAndDrop(object):
# Number of draggables in user_groups may be differ that in # Number of draggables in user_groups may be differ that in
# correct_groups, that is incorrect, except special case with 'number' # 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 # 'number' rule special case
# for reusable draggables we may get in self.user_groups # 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']} # {'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 # if '+number' is in rule - do not remove duplicates and strip
# '+number' from rule # '+number' from rule
current_rule = self.correct_positions[groupname].keys()[0] current_rule = self.correct_positions[index].keys()[0]
if 'number' in current_rule: 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 # 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', '') 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 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 return False
# Check that in every group, for rule of that group, user positions of # Check that in every group, for rule of that group, user positions of
# every element are equal with correct positions # every element are equal with correct positions
for groupname in self.correct_groups: for index, _ in enumerate(self.correct_groups):
rules_executed = 0 rules_executed = 0
for rule in ('exact', 'anyof', 'unordered_equal'): for rule in ('exact', 'anyof', 'unordered_equal'):
# every group has only one rule # 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 rules_executed += 1
if not self.compare_positions( if not self.compare_positions(
self.correct_positions[groupname][rule], self.correct_positions[index][rule],
self.user_positions[groupname]['user'], flag=rule): self.user_positions[index]['user'], flag=rule):
return False return False
if not rules_executed: # no correct rules for current group if not rules_executed: # no correct rules for current group
# probably xml content mistake - wrong rules names # probably xml content mistake - wrong rules names
...@@ -248,7 +290,7 @@ class DragAndDrop(object): ...@@ -248,7 +290,7 @@ class DragAndDrop(object):
correct_answer = {'name4': 't1', correct_answer = {'name4': 't1',
'name_with_icon': 't1', 'name_with_icon': 't1',
'5': 't2', '5': 't2',
'7':'t2'} '7': 't2'}
It is draggable_name: dragable_position mapping. It is draggable_name: dragable_position mapping.
...@@ -284,24 +326,25 @@ class DragAndDrop(object): ...@@ -284,24 +326,25 @@ class DragAndDrop(object):
Args: Args:
user_answer: json user_answer: json
correct_answer: dict or list correct_answer: dict or list
""" """
self.correct_groups = dict() # correct groups from xml self.correct_groups = [] # Correct groups from xml.
self.correct_positions = dict() # correct positions for comparing self.correct_positions = [] # Correct positions for comparing.
self.user_groups = dict() # will be populated from user answer self.user_groups = [] # Will be populated from user answer.
self.user_positions = dict() # 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): if isinstance(correct_answer, dict):
tmp = [] tmp = []
for key, value in correct_answer.items(): for key, value in correct_answer.items():
tmp_dict = {'draggables': [], 'targets': [], 'rule': 'exact'} tmp.append({
tmp_dict['draggables'].append(key) 'draggables': [key],
tmp_dict['targets'].append(value) 'targets': [value],
tmp.append(tmp_dict) 'rule': 'exact'})
correct_answer = tmp correct_answer = tmp
# Convert string `user_answer` to object.
user_answer = json.loads(user_answer) user_answer = json.loads(user_answer)
# This dictionary will hold a key for each draggable the user placed on # This dictionary will hold a key for each draggable the user placed on
...@@ -309,27 +352,32 @@ class DragAndDrop(object): ...@@ -309,27 +352,32 @@ class DragAndDrop(object):
# correct_answer entries. If the draggable is mentioned in at least one # correct_answer entries. If the draggable is mentioned in at least one
# correct_answer entry, the value is False. # correct_answer entry, the value is False.
# default to consider every user answer excess until proven otherwise. # default to consider every user answer excess until proven otherwise.
self.excess_draggables = dict((users_draggable.keys()[0],True) self.excess_draggables = dict((users_draggable.keys()[0],True)
for users_draggable in user_answer['draggables']) for users_draggable in user_answer)
# create identical data structures from user answer and correct answer # Convert nested `user_answer` to flat format.
for i in xrange(0, len(correct_answer)): user_answer = flat_user_answer(user_answer)
groupname = str(i)
self.correct_groups[groupname] = correct_answer[i]['draggables'] # Create identical data structures from user answer and correct answer.
self.correct_positions[groupname] = {correct_answer[i]['rule']: for answer in correct_answer:
correct_answer[i]['targets']} user_groups_data = []
self.user_groups[groupname] = [] user_positions_data = []
self.user_positions[groupname] = {'user': []} for draggable_dict in user_answer:
for draggable_dict in user_answer['draggables']: # Draggable_dict is 1-to-1 {draggable_name: position}.
# draggable_dict is 1-to-1 {draggable_name: position}
draggable_name = draggable_dict.keys()[0] draggable_name = draggable_dict.keys()[0]
if draggable_name in self.correct_groups[groupname]: if draggable_name in answer['draggables']:
self.user_groups[groupname].append(draggable_name) user_groups_data.append(draggable_name)
self.user_positions[groupname]['user'].append( user_positions_data.append(
draggable_dict[draggable_name]) draggable_dict[draggable_name])
# proved that this is not excess # proved that this is not excess
self.excess_draggables[draggable_name] = False 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): def grade(user_input, correct_answer):
""" Creates DragAndDrop instance from user_input and correct_answer and """ Creates DragAndDrop instance from user_input and correct_answer and
calls DragAndDrop.grade for grading. calls DragAndDrop.grade for grading.
......
...@@ -7,7 +7,7 @@ import logging ...@@ -7,7 +7,7 @@ import logging
import requests import requests
log = logging.getLogger('mitx.' + __name__) log = logging.getLogger(__name__)
dateformat = '%Y%m%d%H%M%S' dateformat = '%Y%m%d%H%M%S'
......
...@@ -4,5 +4,5 @@ setup( ...@@ -4,5 +4,5 @@ setup(
name="capa", name="capa",
version="0.1", version="0.1",
packages=find_packages(exclude=["tests"]), packages=find_packages(exclude=["tests"]),
install_requires=['distribute==0.6.34', 'pyparsing==1.5.6'], install_requires=['distribute==0.6.30', 'pyparsing==1.5.6'],
) )
...@@ -28,6 +28,7 @@ setup( ...@@ -28,6 +28,7 @@ setup(
"image = xmodule.backcompat_module:TranslateCustomTagDescriptor", "image = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"error = xmodule.error_module:ErrorDescriptor", "error = xmodule.error_module:ErrorDescriptor",
"peergrading = xmodule.peer_grading_module:PeerGradingDescriptor", "peergrading = xmodule.peer_grading_module:PeerGradingDescriptor",
"poll_question = xmodule.poll_module:PollDescriptor",
"problem = xmodule.capa_module:CapaDescriptor", "problem = xmodule.capa_module:CapaDescriptor",
"problemset = xmodule.seq_module:SequenceDescriptor", "problemset = xmodule.seq_module:SequenceDescriptor",
"randomize = xmodule.randomize_module:RandomizeDescriptor", "randomize = xmodule.randomize_module:RandomizeDescriptor",
...@@ -45,6 +46,7 @@ setup( ...@@ -45,6 +46,7 @@ setup(
"static_tab = xmodule.html_module:StaticTabDescriptor", "static_tab = xmodule.html_module:StaticTabDescriptor",
"custom_tag_template = xmodule.raw_module:RawDescriptor", "custom_tag_template = xmodule.raw_module:RawDescriptor",
"about = xmodule.html_module:AboutDescriptor", "about = xmodule.html_module:AboutDescriptor",
"wrapper = xmodule.wrapper_module:WrapperDescriptor",
"graphical_slider_tool = xmodule.gst_module:GraphicalSliderToolDescriptor", "graphical_slider_tool = xmodule.gst_module:GraphicalSliderToolDescriptor",
"annotatable = xmodule.annotatable_module:AnnotatableDescriptor", "annotatable = xmodule.annotatable_module:AnnotatableDescriptor",
"foldit = xmodule.foldit_module:FolditDescriptor", "foldit = xmodule.foldit_module:FolditDescriptor",
......
import json
import random import random
import logging import logging
from lxml import etree from lxml import etree
...@@ -7,6 +6,7 @@ from xmodule.x_module import XModule ...@@ -7,6 +6,7 @@ from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
from xmodule.xml_module import XmlDescriptor from xmodule.xml_module import XmlDescriptor
from xmodule.exceptions import InvalidDefinitionError from xmodule.exceptions import InvalidDefinitionError
from xblock.core import String, Scope, Object, BlockScope
DEFAULT = "_DEFAULT_GROUP" DEFAULT = "_DEFAULT_GROUP"
...@@ -31,29 +31,42 @@ def group_from_value(groups, v): ...@@ -31,29 +31,42 @@ def group_from_value(groups, v):
return g 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 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): def __init__(self, *args, **kwargs):
XModule.__init__(self, system, location, definition, descriptor, instance_state, shared_state, **kwargs) XModule.__init__(self, *args, **kwargs)
if shared_state is None:
if self.group is None:
self.group = group_from_value( self.group = group_from_value(
self.definition['data']['group_portions'].items(), self.group_portions.items(),
random.uniform(0, 1) random.uniform(0, 1)
) )
else:
shared_state = json.loads(shared_state)
self.group = shared_state['group']
def get_shared_state(self): @property
return json.dumps({'group': self.group}) 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): 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] return [desc for desc in self.descriptor.get_children() if desc.location.url() in active_locations]
def displayable_items(self): def displayable_items(self):
...@@ -64,43 +77,11 @@ class ABTestModule(XModule): ...@@ -64,43 +77,11 @@ class ABTestModule(XModule):
# TODO (cpennington): Use Groups should be a first class object, rather than being # TODO (cpennington): Use Groups should be a first class object, rather than being
# managed by ABTests # managed by ABTests
class ABTestDescriptor(RawDescriptor, XmlDescriptor): class ABTestDescriptor(ABTestFields, RawDescriptor, XmlDescriptor):
module_class = ABTestModule module_class = ABTestModule
template_dir_name = "abtest" 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 @classmethod
def definition_from_xml(cls, xml_object, system): def definition_from_xml(cls, xml_object, system):
""" """
...@@ -118,19 +99,16 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor): ...@@ -118,19 +99,16 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor):
"ABTests must specify an experiment. Not found in:\n{xml}" "ABTests must specify an experiment. Not found in:\n{xml}"
.format(xml=etree.tostring(xml_object, pretty_print=True))) .format(xml=etree.tostring(xml_object, pretty_print=True)))
definition = { group_portions = {}
'data': { group_content = {}
'experiment': experiment, children = []
'group_portions': {},
'group_content': {DEFAULT: []},
},
'children': []}
for group in xml_object: for group in xml_object:
if group.tag == 'default': if group.tag == 'default':
name = DEFAULT name = DEFAULT
else: else:
name = group.get('name') 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 = [] child_content_urls = []
for child in group: for child in group:
...@@ -140,29 +118,33 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor): ...@@ -140,29 +118,33 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor):
log.exception("Unable to load child when parsing ABTest. Continuing...") log.exception("Unable to load child when parsing ABTest. Continuing...")
continue continue
definition['data']['group_content'][name] = child_content_urls group_content[name] = child_content_urls
definition['children'].extend(child_content_urls) children.extend(child_content_urls)
default_portion = 1 - sum( 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: if default_portion < 0:
raise InvalidDefinitionError("ABTest portions must add up to less than or equal to 1") raise InvalidDefinitionError("ABTest portions must add up to less than or equal to 1")
definition['data']['group_portions'][DEFAULT] = default_portion group_portions[DEFAULT] = default_portion
definition['children'].sort() children.sort()
return definition return {
'group_portions': group_portions,
'group_content': group_content,
}, children
def definition_to_xml(self, resource_fs): def definition_to_xml(self, resource_fs):
xml_object = etree.Element('abtest') xml_object = etree.Element('abtest')
xml_object.set('experiment', self.definition['data']['experiment']) xml_object.set('experiment', self.experiment)
for name, group in self.definition['data']['group_content'].items(): for name, group in self.group_content.items():
if name == DEFAULT: if name == DEFAULT:
group_elem = etree.SubElement(xml_object, 'default') group_elem = etree.SubElement(xml_object, 'default')
else: else:
group_elem = etree.SubElement(xml_object, 'group', attrib={ group_elem = etree.SubElement(xml_object, 'group', attrib={
'portion': str(self.definition['data']['group_portions'][name]), 'portion': str(self.group_portions[name]),
'name': name, 'name': name,
}) })
...@@ -172,6 +154,5 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor): ...@@ -172,6 +154,5 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor):
return xml_object return xml_object
def has_dynamic_children(self): def has_dynamic_children(self):
return True return True
...@@ -5,13 +5,17 @@ from pkg_resources import resource_string, resource_listdir ...@@ -5,13 +5,17 @@ from pkg_resources import resource_string, resource_listdir
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor 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 xmodule.contentstore.content import StaticContent
from xblock.core import Scope, String
log = logging.getLogger(__name__) 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'), js = {'coffee': [resource_string(__name__, 'js/src/javascript_loader.coffee'),
resource_string(__name__, 'js/src/collapsible.coffee'), resource_string(__name__, 'js/src/collapsible.coffee'),
resource_string(__name__, 'js/src/html/display.coffee'), resource_string(__name__, 'js/src/html/display.coffee'),
...@@ -22,6 +26,17 @@ class AnnotatableModule(XModule): ...@@ -22,6 +26,17 @@ class AnnotatableModule(XModule):
css = {'scss': [resource_string(__name__, 'css/annotatable/display.scss')]} css = {'scss': [resource_string(__name__, 'css/annotatable/display.scss')]}
icon_class = 'annotatable' 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): def _get_annotation_class_attr(self, index, el):
""" Returns a dict with the CSS class attribute to set on the annotation """ Returns a dict with the CSS class attribute to set on the annotation
and an XML key to delete from the element. and an XML key to delete from the element.
...@@ -103,7 +118,7 @@ class AnnotatableModule(XModule): ...@@ -103,7 +118,7 @@ class AnnotatableModule(XModule):
def get_html(self): def get_html(self):
""" Renders parameters to template. """ """ Renders parameters to template. """
context = { context = {
'display_name': self.display_name, 'display_name': self.display_name_with_default,
'element_id': self.element_id, 'element_id': self.element_id,
'instructions_html': self.instructions, 'instructions_html': self.instructions,
'content_html': self._render_content() 'content_html': self._render_content()
...@@ -111,19 +126,8 @@ class AnnotatableModule(XModule): ...@@ -111,19 +126,8 @@ class AnnotatableModule(XModule):
return self.system.render_template('annotatable.html', context) 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 module_class = AnnotatableModule
stores_state = True stores_state = True
template_dir_name = "annotatable" template_dir_name = "annotatable"
......
""" """
These modules exist to translate old format XML into newer, semantic forms 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 lxml import etree
from functools import wraps from functools import wraps
import logging import logging
......
...@@ -6,19 +6,47 @@ from pkg_resources import resource_string ...@@ -6,19 +6,47 @@ from pkg_resources import resource_string
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
from .x_module import XModule from .x_module import XModule
from xblock.core import Integer, Scope, BlockScope, ModelType, String, Boolean, Object, Float, List
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
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
VERSION_TUPLES = ( VERSION_TUPLES = (
('1', CombinedOpenEndedV1Descriptor, CombinedOpenEndedV1Module), ('1', CombinedOpenEndedV1Descriptor, CombinedOpenEndedV1Module, V1_SETTINGS_ATTRIBUTES, V1_STUDENT_ATTRIBUTES),
) )
DEFAULT_VERSION = 1 DEFAULT_VERSION = 1
DEFAULT_VERSION = str(DEFAULT_VERSION) DEFAULT_VERSION = str(DEFAULT_VERSION)
class CombinedOpenEndedModule(XModule): 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 = Integer(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). This is a module that encapsulates all open ended grading (self assessment, peer assessment, etc).
It transitions between problems, and support arbitrary ordering. It transitions between problems, and support arbitrary ordering.
...@@ -49,6 +77,8 @@ class CombinedOpenEndedModule(XModule): ...@@ -49,6 +77,8 @@ class CombinedOpenEndedModule(XModule):
INTERMEDIATE_DONE = 'intermediate_done' INTERMEDIATE_DONE = 'intermediate_done'
DONE = 'done' DONE = 'done'
icon_class = 'problem'
js = {'coffee': [resource_string(__name__, 'js/src/combinedopenended/display.coffee'), js = {'coffee': [resource_string(__name__, 'js/src/combinedopenended/display.coffee'),
resource_string(__name__, 'js/src/collapsible.coffee'), resource_string(__name__, 'js/src/collapsible.coffee'),
resource_string(__name__, 'js/src/javascript_loader.coffee'), resource_string(__name__, 'js/src/javascript_loader.coffee'),
...@@ -57,11 +87,8 @@ class CombinedOpenEndedModule(XModule): ...@@ -57,11 +87,8 @@ class CombinedOpenEndedModule(XModule):
css = {'scss': [resource_string(__name__, 'css/combinedopenended/display.scss')]} css = {'scss': [resource_string(__name__, 'css/combinedopenended/display.scss')]}
def __init__(self, system, location, definition, descriptor, def __init__(self, system, location, descriptor, model_data):
instance_state=None, shared_state=None, **kwargs): XModule.__init__(self, system, location, descriptor, model_data)
XModule.__init__(self, system, location, definition, descriptor,
instance_state, shared_state, **kwargs)
""" """
Definition file should have one or many task blocks, a rubric block, and a prompt block: Definition file should have one or many task blocks, a rubric block, and a prompt block:
...@@ -100,25 +127,15 @@ class CombinedOpenEndedModule(XModule): ...@@ -100,25 +127,15 @@ class CombinedOpenEndedModule(XModule):
self.system = system self.system = system
self.system.set('location', location) self.system.set('location', location)
# Load instance state if self.task_states is None:
if instance_state is not None: self.task_states = []
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] versions = [i[0] for i in VERSION_TUPLES]
descriptors = [i[1] for i in VERSION_TUPLES] descriptors = [i[1] for i in VERSION_TUPLES]
modules = [i[2] for i in VERSION_TUPLES] modules = [i[2] for i in VERSION_TUPLES]
settings_attributes = [i[3] for i in VERSION_TUPLES]
student_attributes = [i[4] for i in VERSION_TUPLES]
version_error_string = "Could not find version {0}, using version {1} instead"
try: try:
version_index = versions.index(self.version) version_index = versions.index(self.version)
...@@ -128,22 +145,31 @@ class CombinedOpenEndedModule(XModule): ...@@ -128,22 +145,31 @@ class CombinedOpenEndedModule(XModule):
self.version = DEFAULT_VERSION self.version = DEFAULT_VERSION
version_index = versions.index(self.version) version_index = versions.index(self.version)
self.student_attributes = student_attributes[version_index]
self.settings_attributes = settings_attributes[version_index]
attributes = self.student_attributes + self.settings_attributes
static_data = { static_data = {
'rewrite_content_links': self.rewrite_content_links, 'rewrite_content_links': self.rewrite_content_links,
} }
instance_state = {k: getattr(self, k) for k in attributes}
self.child_descriptor = descriptors[version_index](self.system) self.child_descriptor = descriptors[version_index](self.system)
self.child_definition = descriptors[version_index].definition_from_xml(etree.fromstring(definition['data']), self.child_definition = descriptors[version_index].definition_from_xml(etree.fromstring(self.data), self.system)
self.system)
self.child_module = modules[version_index](self.system, location, self.child_definition, self.child_descriptor, self.child_module = modules[version_index](self.system, location, self.child_definition, self.child_descriptor,
instance_state=json.dumps(instance_state), metadata=self.metadata, instance_state=instance_state, static_data=static_data, attributes=attributes)
static_data=static_data) self.save_instance_data()
def get_html(self): 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): 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): def get_instance_state(self):
return self.child_module.get_instance_state() return self.child_module.get_instance_state()
...@@ -151,8 +177,8 @@ class CombinedOpenEndedModule(XModule): ...@@ -151,8 +177,8 @@ class CombinedOpenEndedModule(XModule):
def get_score(self): def get_score(self):
return self.child_module.get_score() return self.child_module.get_score()
def max_score(self): #def max_score(self):
return self.child_module.max_score() # return self.child_module.max_score()
def get_progress(self): def get_progress(self):
return self.child_module.get_progress() return self.child_module.get_progress()
...@@ -161,12 +187,14 @@ class CombinedOpenEndedModule(XModule): ...@@ -161,12 +187,14 @@ class CombinedOpenEndedModule(XModule):
def due_date(self): def due_date(self):
return self.child_module.due_date return self.child_module.due_date
@property def save_instance_data(self):
def display_name(self): for attribute in self.student_attributes:
return self.child_module.display_name 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 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 ...@@ -3,35 +3,38 @@ from pkg_resources import resource_string, resource_listdir
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor 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': js = {'coffee':
[resource_string(__name__, 'js/src/time.coffee'), [resource_string(__name__, 'js/src/time.coffee'),
resource_string(__name__, 'js/src/discussion/display.coffee')] resource_string(__name__, 'js/src/discussion/display.coffee')]
} }
js_module_name = "InlineDiscussion" js_module_name = "InlineDiscussion"
def get_html(self): def get_html(self):
context = { context = {
'discussion_id': self.discussion_id, 'discussion_id': self.discussion_id,
} }
return self.system.render_template('discussion/_discussion_module.html', context) 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): class DiscussionDescriptor(DiscussionFields, RawDescriptor):
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):
module_class = DiscussionModule module_class = DiscussionModule
template_dir_name = "discussion" 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 pkg_resources import resource_string
from xmodule.mako_module import MakoModuleDescriptor from xmodule.mako_module import MakoModuleDescriptor
from xblock.core import Scope, String
import logging import logging
log = logging.getLogger(__name__) 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 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. perform any validation on its definition---just passes it along to the browser.
...@@ -20,7 +25,7 @@ class EditingDescriptor(MakoModuleDescriptor): ...@@ -20,7 +25,7 @@ class EditingDescriptor(MakoModuleDescriptor):
def get_context(self): def get_context(self):
_context = MakoModuleDescriptor.get_context(self) _context = MakoModuleDescriptor.get_context(self)
# Add our specific template information (the raw data body) # Add our specific template information (the raw data body)
_context.update({'data': self.definition.get('data', '')}) _context.update({'data': self.data})
return _context return _context
......
...@@ -8,6 +8,7 @@ from xmodule.x_module import XModule ...@@ -8,6 +8,7 @@ from xmodule.x_module import XModule
from xmodule.editing_module import JSONEditingDescriptor from xmodule.editing_module import JSONEditingDescriptor
from xmodule.errortracker import exc_info_to_str from xmodule.errortracker import exc_info_to_str
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xblock.core import String, Scope
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -20,7 +21,14 @@ log = logging.getLogger(__name__) ...@@ -20,7 +21,14 @@ log = logging.getLogger(__name__)
# decides whether to create a staff or not-staff module. # 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): def get_html(self):
'''Show an error to staff. '''Show an error to staff.
TODO (vshnayder): proper style, divs, etc. TODO (vshnayder): proper style, divs, etc.
...@@ -28,12 +36,12 @@ class ErrorModule(XModule): ...@@ -28,12 +36,12 @@ class ErrorModule(XModule):
# staff get to see all the details # staff get to see all the details
return self.system.render_template('module-error.html', { return self.system.render_template('module-error.html', {
'staff_access': True, 'staff_access': True,
'data': self.definition['data']['contents'], 'data': self.contents,
'error': self.definition['data']['error_msg'], 'error': self.error_msg,
}) })
class NonStaffErrorModule(XModule): class NonStaffErrorModule(ErrorFields, XModule):
def get_html(self): def get_html(self):
'''Show an error to a student. '''Show an error to a student.
TODO (vshnayder): proper style, divs, etc. TODO (vshnayder): proper style, divs, etc.
...@@ -46,7 +54,7 @@ class NonStaffErrorModule(XModule): ...@@ -46,7 +54,7 @@ class NonStaffErrorModule(XModule):
}) })
class ErrorDescriptor(JSONEditingDescriptor): class ErrorDescriptor(ErrorFields, JSONEditingDescriptor):
""" """
Module that provides a raw editing view of broken xml. Module that provides a raw editing view of broken xml.
""" """
...@@ -66,26 +74,22 @@ class ErrorDescriptor(JSONEditingDescriptor): ...@@ -66,26 +74,22 @@ class ErrorDescriptor(JSONEditingDescriptor):
name=hashlib.sha1(contents).hexdigest() 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 # 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( return ErrorDescriptor(
system, system,
definition, location,
location=location, model_data,
metadata=metadata
) )
def get_context(self): def get_context(self):
return { return {
'module': self, 'module': self,
'data': self.definition['data']['contents'], 'data': self.contents,
} }
@classmethod @classmethod
...@@ -101,10 +105,7 @@ class ErrorDescriptor(JSONEditingDescriptor): ...@@ -101,10 +105,7 @@ class ErrorDescriptor(JSONEditingDescriptor):
def from_descriptor(cls, descriptor, error_msg='Error not available'): def from_descriptor(cls, descriptor, error_msg='Error not available'):
return cls._construct( return cls._construct(
descriptor.system, descriptor.system,
json.dumps({ descriptor._model_data,
'definition': descriptor.definition,
'metadata': descriptor.metadata,
}, indent=4),
error_msg, error_msg,
location=descriptor.location, location=descriptor.location,
) )
...@@ -148,14 +149,14 @@ class ErrorDescriptor(JSONEditingDescriptor): ...@@ -148,14 +149,14 @@ class ErrorDescriptor(JSONEditingDescriptor):
files, etc. That would just get re-wrapped on import. files, etc. That would just get re-wrapped on import.
''' '''
try: try:
xml = etree.fromstring(self.definition['data']['contents']) xml = etree.fromstring(self.contents)
return etree.tostring(xml, encoding='unicode') return etree.tostring(xml, encoding='unicode')
except etree.XMLSyntaxError: except etree.XMLSyntaxError:
# still not valid. # still not valid.
root = etree.Element('error') root = etree.Element('error')
root.text = self.definition['data']['contents'] root.text = self.contents
err_node = etree.SubElement(root, 'error_msg') 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') 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 ...@@ -7,17 +7,27 @@ from pkg_resources import resource_string
from xmodule.editing_module import EditingDescriptor from xmodule.editing_module import EditingDescriptor
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.xml_module import XmlDescriptor from xmodule.xml_module import XmlDescriptor
from xblock.core import Scope, Integer, String
log = logging.getLogger(__name__) 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')]} css = {'scss': [resource_string(__name__, 'css/foldit/leaderboard.scss')]}
def __init__(self, system, location, definition, descriptor, def __init__(self, *args, **kwargs):
instance_state=None, shared_state=None, **kwargs): XModule.__init__(self, *args, **kwargs)
XModule.__init__(self, system, location, definition, descriptor,
instance_state, shared_state, **kwargs)
""" """
Example: Example:
...@@ -26,25 +36,17 @@ class FolditModule(XModule): ...@@ -26,25 +36,17 @@ class FolditModule(XModule):
required_sublevel="3" required_sublevel="3"
show_leaderboard="false"/> 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(): def parse_due_date():
""" """
Pull out the date, or None Pull out the date, or None
""" """
s = self.metadata.get("due") s = self.due
if s: if s:
return parser.parse(s) return parser.parse(s)
else: else:
return None return None
self.due_str = self.metadata.get("due", "None") self.due_time = parse_due_date()
self.due = parse_due_date()
def is_complete(self): def is_complete(self):
""" """
...@@ -59,7 +61,7 @@ class FolditModule(XModule): ...@@ -59,7 +61,7 @@ class FolditModule(XModule):
self.system.anonymous_student_id, self.system.anonymous_student_id,
self.required_level, self.required_level,
self.required_sublevel, self.required_sublevel,
self.due) self.due_time)
return complete return complete
def completed_puzzles(self): def completed_puzzles(self):
...@@ -87,7 +89,7 @@ class FolditModule(XModule): ...@@ -87,7 +89,7 @@ class FolditModule(XModule):
from foldit.models import Score from foldit.models import Score
leaders = [(e['username'], e['score']) for e in Score.get_tops_n(10)] leaders = [(e['username'], e['score']) for e in Score.get_tops_n(10)]
leaders.sort(key=lambda x: x[1]) leaders.sort(key=lambda x: -x[1])
return leaders return leaders
...@@ -99,11 +101,11 @@ class FolditModule(XModule): ...@@ -99,11 +101,11 @@ class FolditModule(XModule):
self.required_level, self.required_level,
self.required_sublevel) self.required_sublevel)
showbasic = (self.metadata.get("show_basic_score", "").lower() == "true") showbasic = (self.show_basic_score.lower() == "true")
showleader = (self.metadata.get("show_leaderboard", "").lower() == "true") showleader = (self.show_leaderboard.lower() == "true")
context = { context = {
'due': self.due_str, 'due': self.due,
'success': self.is_complete(), 'success': self.is_complete(),
'goal_level': goal_level, 'goal_level': goal_level,
'completed': self.completed_puzzles(), 'completed': self.completed_puzzles(),
...@@ -125,7 +127,7 @@ class FolditModule(XModule): ...@@ -125,7 +127,7 @@ class FolditModule(XModule):
self.required_sublevel) self.required_sublevel)
context = { context = {
'due': self.due_str, 'due': self.due,
'success': self.is_complete(), 'success': self.is_complete(),
'goal_level': goal_level, 'goal_level': goal_level,
'completed': self.completed_puzzles(), 'completed': self.completed_puzzles(),
...@@ -155,7 +157,7 @@ class FolditModule(XModule): ...@@ -155,7 +157,7 @@ class FolditModule(XModule):
class FolditDescriptor(XmlDescriptor, EditingDescriptor): class FolditDescriptor(FolditFields, XmlDescriptor, EditingDescriptor):
""" """
Module for adding Foldit problems to courses Module for adding Foldit problems to courses
""" """
...@@ -176,7 +178,8 @@ class FolditDescriptor(XmlDescriptor, EditingDescriptor): ...@@ -176,7 +178,8 @@ class FolditDescriptor(XmlDescriptor, EditingDescriptor):
@classmethod @classmethod
def definition_from_xml(cls, xml_object, system): def definition_from_xml(cls, xml_object, system):
""" return ({}, [])
Get the xml_object's attributes.
""" def definition_to_xml(self):
return {'metadata': xml_object.attrib} xml_object = etree.Element('foldit')
return xml_object
...@@ -14,12 +14,18 @@ from xmodule.xml_module import XmlDescriptor ...@@ -14,12 +14,18 @@ from xmodule.xml_module import XmlDescriptor
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.stringify import stringify_children from xmodule.stringify import stringify_children
from pkg_resources import resource_string from pkg_resources import resource_string
from xblock.core import String, Scope
log = logging.getLogger(__name__) 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 ''' Graphical-Slider-Tool Module
''' '''
...@@ -43,15 +49,6 @@ class GraphicalSliderToolModule(XModule): ...@@ -43,15 +49,6 @@ class GraphicalSliderToolModule(XModule):
} }
js_module_name = "GraphicalSliderTool" 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): def get_html(self):
""" Renders parameters to template. """ """ Renders parameters to template. """
...@@ -60,14 +57,14 @@ class GraphicalSliderToolModule(XModule): ...@@ -60,14 +57,14 @@ class GraphicalSliderToolModule(XModule):
self.html_class = self.location.category self.html_class = self.location.category
self.configuration_json = self.build_configuration_json() self.configuration_json = self.build_configuration_json()
params = { params = {
'gst_html': self.substitute_controls(self.definition['render']), 'gst_html': self.substitute_controls(self.render),
'element_id': self.html_id, 'element_id': self.html_id,
'element_class': self.html_class, 'element_class': self.html_class,
'configuration_json': self.configuration_json 'configuration_json': self.configuration_json
} }
self.content = self.system.render_template( content = self.system.render_template(
'graphical_slider_tool.html', params) 'graphical_slider_tool.html', params)
return self.content return content
def substitute_controls(self, html_string): def substitute_controls(self, html_string):
""" Substitutes control elements (slider, textbox and plot) in """ Substitutes control elements (slider, textbox and plot) in
...@@ -139,10 +136,10 @@ class GraphicalSliderToolModule(XModule): ...@@ -139,10 +136,10 @@ class GraphicalSliderToolModule(XModule):
# <root> added for interface compatibility with xmltodict.parse # <root> added for interface compatibility with xmltodict.parse
# class added for javascript's part purposes # class added for javascript's part purposes
return json.dumps(xmltodict.parse('<root class="' + self.html_class + 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 module_class = GraphicalSliderToolModule
template_dir_name = 'graphical_slider_tool' template_dir_name = 'graphical_slider_tool'
...@@ -177,14 +174,14 @@ class GraphicalSliderToolDescriptor(MakoModuleDescriptor, XmlDescriptor): ...@@ -177,14 +174,14 @@ class GraphicalSliderToolDescriptor(MakoModuleDescriptor, XmlDescriptor):
return { return {
'render': parse('render'), 'render': parse('render'),
'configuration': parse('configuration') 'configuration': parse('configuration')
} }, []
def definition_to_xml(self, resource_fs): def definition_to_xml(self, resource_fs):
'''Return an xml element representing this definition.''' '''Return an xml element representing this definition.'''
xml_object = etree.Element('graphical_slider_tool') xml_object = etree.Element('graphical_slider_tool')
def add_child(k): 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) child_node = etree.fromstring(child_str)
xml_object.append(child_node) xml_object.append(child_node)
......
...@@ -7,10 +7,9 @@ from lxml import etree ...@@ -7,10 +7,9 @@ from lxml import etree
from path import path from path import path
from pkg_resources import resource_string 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.editing_module import EditingDescriptor
from xmodule.html_checker import check_html from xmodule.html_checker import check_html
from xmodule.modulestore import Location
from xmodule.stringify import stringify_children from xmodule.stringify import stringify_children
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.xml_module import XmlDescriptor, name_to_pathname from xmodule.xml_module import XmlDescriptor, name_to_pathname
...@@ -18,7 +17,11 @@ 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") 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'), js = {'coffee': [resource_string(__name__, 'js/src/javascript_loader.coffee'),
resource_string(__name__, 'js/src/collapsible.coffee'), resource_string(__name__, 'js/src/collapsible.coffee'),
resource_string(__name__, 'js/src/html/display.coffee') resource_string(__name__, 'js/src/html/display.coffee')
...@@ -28,17 +31,10 @@ class HtmlModule(XModule): ...@@ -28,17 +31,10 @@ class HtmlModule(XModule):
css = {'scss': [resource_string(__name__, 'css/html/display.scss')]} css = {'scss': [resource_string(__name__, 'css/html/display.scss')]}
def get_html(self): def get_html(self):
return self.html return self.data
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']
class HtmlDescriptor(XmlDescriptor, EditingDescriptor): class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor):
""" """
Module for putting raw html in a course Module for putting raw html in a course
""" """
...@@ -91,7 +87,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor): ...@@ -91,7 +87,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
if filename is None: if filename is None:
definition_xml = copy.deepcopy(xml_object) definition_xml = copy.deepcopy(xml_object)
cls.clean_metadata_from_xml(definition_xml) cls.clean_metadata_from_xml(definition_xml)
return {'data': stringify_children(definition_xml)} return {'data': stringify_children(definition_xml)}, []
else: else:
# html is special. cls.filename_extension is 'xml', but # html is special. cls.filename_extension is 'xml', but
# if 'filename' is in the definition, that means to load # if 'filename' is in the definition, that means to load
...@@ -105,8 +101,6 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor): ...@@ -105,8 +101,6 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
filepath = "{base}/{name}.html".format(base=base, name=filename) filepath = "{base}/{name}.html".format(base=base, name=filename)
#log.debug("looking for html file for {0} at {1}".format(location, filepath)) #log.debug("looking for html file for {0} at {1}".format(location, filepath))
# VS[compat] # VS[compat]
# TODO (cpennington): If the file doesn't exist at the right path, # 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 # give the class a chance to fix it up. The file will be written out
...@@ -135,7 +129,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor): ...@@ -135,7 +129,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
# for Fall 2012 LMS migration: keep filename (and unmangled filename) # for Fall 2012 LMS migration: keep filename (and unmangled filename)
definition['filename'] = [filepath, filename] definition['filename'] = [filepath, filename]
return definition return definition, []
except (ResourceNotFoundError) as err: except (ResourceNotFoundError) as err:
msg = 'Unable to load file contents at path {0}: {1} '.format( msg = 'Unable to load file contents at path {0}: {1} '.format(
...@@ -151,19 +145,18 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor): ...@@ -151,19 +145,18 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
string to filename.html. string to filename.html.
''' '''
try: try:
return etree.fromstring(self.definition['data']) return etree.fromstring(self.data)
except etree.XMLSyntaxError: except etree.XMLSyntaxError:
pass pass
# Not proper format. Write html to file, return an empty tag # Not proper format. Write html to file, return an empty tag
pathname = name_to_pathname(self.url_name) pathname = name_to_pathname(self.url_name)
pathdir = path(pathname).dirname()
filepath = u'{category}/{pathname}.html'.format(category=self.category, filepath = u'{category}/{pathname}.html'.format(category=self.category,
pathname=pathname) pathname=pathname)
resource_fs.makedir(os.path.dirname(filepath), recursive=True, allow_recreate=True) resource_fs.makedir(os.path.dirname(filepath), recursive=True, allow_recreate=True)
with resource_fs.open(filepath, 'w') as file: 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 # write out the relative name
relname = path(pathname).basename() relname = path(pathname).basename()
...@@ -175,8 +168,11 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor): ...@@ -175,8 +168,11 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
@property @property
def editable_metadata_fields(self): def editable_metadata_fields(self):
"""Remove any metadata from the editable fields which have their own editor or shouldn't be edited by user.""" """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 subset = super(HtmlDescriptor, self).editable_metadata_fields
if field not in ['empty']]
if 'empty' in subset:
del subset['empty']
return subset return subset
......
class @Conditional class @Conditional
constructor: (element) -> constructor: (element, callerElId) ->
@el = $(element).find('.conditional-wrapper') @el = $(element).find('.conditional-wrapper')
@id = @el.data('problem-id')
@element_id = @el.attr('id')
@url = @el.data('url')
@render()
$: (selector) -> @callerElId = callerElId
$(selector, @el)
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) => @url = @el.data('url')
if response.progress_changed @render(element)
@el.attr progress: response.progress_status
@el.trigger('progressChanged')
render: (content) -> render: (element) ->
if content
@el.html(content)
XModule.loadModules(@el)
else
$.postWithPrefix "#{@url}/conditional_get", (response) => $.postWithPrefix "#{@url}/conditional_get", (response) =>
@el.html(response.html) @el.html ''
XModule.loadModules(@el) @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 ...@@ -56,7 +56,7 @@ class @Sequence
element.removeClass('progress-none') element.removeClass('progress-none')
.removeClass('progress-some') .removeClass('progress-some')
.removeClass('progress-done') .removeClass('progress-done')
switch progress switch progress
when 'none' then element.addClass('progress-none') when 'none' then element.addClass('progress-none')
when 'in_progress' then element.addClass('progress-some') when 'in_progress' then element.addClass('progress-some')
...@@ -65,6 +65,11 @@ class @Sequence ...@@ -65,6 +65,11 @@ class @Sequence
toggleArrows: => toggleArrows: =>
@$('.sequence-nav-buttons a').unbind('click') @$('.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 if @position == 1
@$('.sequence-nav-buttons .prev a').addClass('disabled') @$('.sequence-nav-buttons .prev a').addClass('disabled')
else else
...@@ -105,8 +110,8 @@ class @Sequence ...@@ -105,8 +110,8 @@ class @Sequence
if (1 <= new_position) and (new_position <= @num_contents) if (1 <= new_position) and (new_position <= @num_contents)
Logger.log "seq_goto", old: @position, new: new_position, id: @id 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 # for queued submissions, see ../capa/display.coffee
if window.queuePollerID if window.queuePollerID
window.clearTimeout(window.queuePollerID) window.clearTimeout(window.queuePollerID)
......
...@@ -4,7 +4,6 @@ class @Video ...@@ -4,7 +4,6 @@ class @Video
@id = @el.attr('id').replace(/video_/, '') @id = @el.attr('id').replace(/video_/, '')
@start = @el.data('start') @start = @el.data('start')
@end = @el.data('end') @end = @el.data('end')
@caption_data_dir = @el.data('caption-data-dir')
@caption_asset_path = @el.data('caption-asset-path') @caption_asset_path = @el.data('caption-asset-path')
@show_captions = @el.data('show-captions') == "true" @show_captions = @el.data('show-captions') == "true"
window.player = null 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 from .x_module import XModuleDescriptor, DescriptorSystem
import logging from .modulestore.inheritance import own_metadata
class MakoDescriptorSystem(DescriptorSystem): class MakoDescriptorSystem(DescriptorSystem):
...@@ -21,21 +21,21 @@ class MakoModuleDescriptor(XModuleDescriptor): ...@@ -21,21 +21,21 @@ class MakoModuleDescriptor(XModuleDescriptor):
the descriptor as the `module` parameter to that template 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: if getattr(system, 'render_template', None) is None:
raise TypeError('{system} must have a render_template function' raise TypeError('{system} must have a render_template function'
' in order to use a MakoDescriptor'.format( ' in order to use a MakoDescriptor'.format(
system=system)) system=system))
super(MakoModuleDescriptor, self).__init__(system, definition, **kwargs) super(MakoModuleDescriptor, self).__init__(system, location, model_data)
def get_context(self): def get_context(self):
""" """
Return the context to render the mako template with Return the context to render the mako template with
""" """
return {'module': self, return {
'metadata': self.metadata, 'module': self,
'editable_metadata_fields': self.editable_metadata_fields 'editable_metadata_fields': self.editable_metadata_fields,
} }
def get_html(self): def get_html(self):
return self.system.render_template( return self.system.render_template(
...@@ -44,6 +44,10 @@ class MakoModuleDescriptor(XModuleDescriptor): ...@@ -44,6 +44,10 @@ class MakoModuleDescriptor(XModuleDescriptor):
# cdodge: encapsulate a means to expose "editable" metadata fields (i.e. not internal system metadata) # cdodge: encapsulate a means to expose "editable" metadata fields (i.e. not internal system metadata)
@property @property
def editable_metadata_fields(self): def editable_metadata_fields(self):
subset = [name for name in self.metadata.keys() if name not in self.system_metadata_fields and fields = {}
name not in self._inherited_metadata] for field, value in own_metadata(self).items():
return subset if field in self.system_metadata_fields:
continue
fields[field] = value
return fields
...@@ -423,6 +423,7 @@ class ModuleStoreBase(ModuleStore): ...@@ -423,6 +423,7 @@ class ModuleStoreBase(ModuleStore):
Set up the error-tracking logic. Set up the error-tracking logic.
''' '''
self._location_errors = {} # location -> ErrorLog self._location_errors = {} # location -> ErrorLog
self.metadata_inheritance_cache = None
def _get_errorlog(self, location): def _get_errorlog(self, location):
""" """
......
...@@ -33,11 +33,12 @@ def modulestore(name='default'): ...@@ -33,11 +33,12 @@ def modulestore(name='default'):
class_ = load_function(settings.MODULESTORE[name]['ENGINE']) class_ = load_function(settings.MODULESTORE[name]['ENGINE'])
options = {} options = {}
options.update(settings.MODULESTORE[name]['OPTIONS']) options.update(settings.MODULESTORE[name]['OPTIONS'])
for key in FUNCTION_KEYS: for key in FUNCTION_KEYS:
if key in options: if key in options:
options[key] = load_function(options[key]) options[key] = load_function(options[key])
_MODULESTORES[name] = class_( _MODULESTORES[name] = class_(
**options **options
) )
......
...@@ -15,11 +15,11 @@ def as_draft(location): ...@@ -15,11 +15,11 @@ def as_draft(location):
def wrap_draft(item): def wrap_draft(item):
""" """
Sets `item.metadata['is_draft']` to `True` if the item is a Sets `item.cms.is_draft` to `True` if the item is a
draft, and false otherwise. Sets the item's location to the draft, and `False` otherwise. Sets the item's location to the
non-draft location in either case non-draft location in either case
""" """
item.metadata['is_draft'] = item.location.revision == DRAFT item.cms.is_draft = item.location.revision == DRAFT
item.location = item.location._replace(revision=None) item.location = item.location._replace(revision=None)
return item return item
...@@ -118,7 +118,7 @@ class DraftModuleStore(ModuleStoreBase): ...@@ -118,7 +118,7 @@ class DraftModuleStore(ModuleStoreBase):
""" """
draft_loc = as_draft(location) draft_loc = as_draft(location)
draft_item = self.get_item(location) draft_item = self.get_item(location)
if not draft_item.metadata['is_draft']: if not draft_item.cms.is_draft:
self.clone_item(location, draft_loc) self.clone_item(location, draft_loc)
return super(DraftModuleStore, self).update_item(draft_loc, data) return super(DraftModuleStore, self).update_item(draft_loc, data)
...@@ -133,7 +133,7 @@ class DraftModuleStore(ModuleStoreBase): ...@@ -133,7 +133,7 @@ class DraftModuleStore(ModuleStoreBase):
""" """
draft_loc = as_draft(location) draft_loc = as_draft(location)
draft_item = self.get_item(location) draft_item = self.get_item(location)
if not draft_item.metadata['is_draft']: if not draft_item.cms.is_draft:
self.clone_item(location, draft_loc) self.clone_item(location, draft_loc)
return super(DraftModuleStore, self).update_children(draft_loc, children) return super(DraftModuleStore, self).update_children(draft_loc, children)
...@@ -149,7 +149,7 @@ class DraftModuleStore(ModuleStoreBase): ...@@ -149,7 +149,7 @@ class DraftModuleStore(ModuleStoreBase):
draft_loc = as_draft(location) draft_loc = as_draft(location)
draft_item = self.get_item(location) draft_item = self.get_item(location)
if not draft_item.metadata['is_draft']: if not draft_item.cms.is_draft:
self.clone_item(location, draft_loc) self.clone_item(location, draft_loc)
if 'is_draft' in metadata: if 'is_draft' in metadata:
...@@ -179,13 +179,11 @@ class DraftModuleStore(ModuleStoreBase): ...@@ -179,13 +179,11 @@ class DraftModuleStore(ModuleStoreBase):
Save a current draft to the underlying modulestore Save a current draft to the underlying modulestore
""" """
draft = self.get_item(location) draft = self.get_item(location)
metadata = {} draft.cms.published_date = datetime.utcnow()
metadata.update(draft.metadata) draft.cms.published_by = published_by_id
metadata['published_date'] = tuple(datetime.utcnow().timetuple()) super(DraftModuleStore, self).update_item(location, draft._model_data._kvs._data)
metadata['published_by'] = published_by_id super(DraftModuleStore, self).update_children(location, draft._model_data._kvs._children)
super(DraftModuleStore, self).update_item(location, draft.definition.get('data', {})) super(DraftModuleStore, self).update_metadata(location, draft._model_data._kvs._metadata)
super(DraftModuleStore, self).update_children(location, draft.definition.get('children', []))
super(DraftModuleStore, self).update_metadata(location, metadata)
self.delete_item(location) self.delete_item(location)
def unpublish(self, location): def unpublish(self, location):
......
from xblock.core import Scope
# A list of metadata that this module can inherit from its parent module
INHERITABLE_METADATA = (
'graded', 'start', 'due', 'graceperiod', 'showanswer', 'rerandomize',
# TODO (ichuang): used for Fall 2012 xqa server access
'xqa_key',
# How many days early to show a course element to beta testers (float)
# intended to be set per-course, but can be overridden in for specific
# elements. Can be a float.
'days_early_for_beta'
)
def compute_inherited_metadata(descriptor):
"""Given a descriptor, traverse all of its descendants and do metadata
inheritance. Should be called on a CourseDescriptor after importing a
course.
NOTE: This means that there is no such thing as lazy loading at the
moment--this accesses all the children."""
for child in descriptor.get_children():
inherit_metadata(child, descriptor._model_data)
compute_inherited_metadata(child)
def inherit_metadata(descriptor, model_data):
"""
Updates this module with metadata inherited from a containing module.
Only metadata specified in self.inheritable_metadata will
be inherited
"""
if not hasattr(descriptor, '_inherited_metadata'):
setattr(descriptor, '_inherited_metadata', {})
# Set all inheritable metadata from kwargs that are
# in self.inheritable_metadata and aren't already set in metadata
for attr in INHERITABLE_METADATA:
if attr not in descriptor._model_data and attr in model_data:
descriptor._inherited_metadata[attr] = model_data[attr]
descriptor._model_data[attr] = model_data[attr]
def own_metadata(module):
"""
Return a dictionary that contains only non-inherited field keys,
mapped to their values
"""
inherited_metadata = getattr(module, '_inherited_metadata', {})
metadata = {}
for field in module.fields + module.lms.fields:
# Only save metadata that wasn't inherited
if field.scope != Scope.settings:
continue
if field.name in inherited_metadata and module._model_data.get(field.name) == inherited_metadata.get(field.name):
continue
if field.name not in module._model_data:
continue
try:
metadata[field.name] = module._model_data[field.name]
except KeyError:
# Ignore any missing keys in _model_data
pass
return metadata
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