studio - alerts: resolving local master merge conflcits

This commit is contained in:
Brian Talbot
2013-03-18 10:58:16 -04:00
287 changed files with 7924 additions and 4448 deletions

View File

@@ -1,7 +1,7 @@
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from lxml import html
from lxml import html, etree
import re
from django.http import HttpResponseBadRequest
import logging
@@ -26,9 +26,9 @@ def get_course_updates(location):
# purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
try:
course_html_parsed = html.fromstring(course_updates.definition['data'])
except:
course_html_parsed = html.fromstring("<ol></ol>")
course_html_parsed = etree.fromstring(course_updates.data)
except etree.XMLSyntaxError:
course_html_parsed = etree.fromstring("<ol></ol>")
# Confirm that root is <ol>, iterate over <li>, pull out <h2> subs and then rest of val
course_upd_collection = []
@@ -60,13 +60,13 @@ def update_course_updates(location, update, passed_id=None):
try:
course_updates = modulestore('direct').get_item(location)
except ItemNotFoundError:
return HttpResponseBadRequest
return HttpResponseBadRequest()
# purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
try:
course_html_parsed = html.fromstring(course_updates.definition['data'])
except:
course_html_parsed = html.fromstring("<ol></ol>")
course_html_parsed = etree.fromstring(course_updates.data)
except etree.XMLSyntaxError:
course_html_parsed = etree.fromstring("<ol></ol>")
# No try/catch b/c failure generates an error back to client
new_html_parsed = html.fromstring('<li><h2>' + update['date'] + '</h2>' + update['content'] + '</li>')
@@ -85,13 +85,12 @@ def update_course_updates(location, update, passed_id=None):
passed_id = course_updates.location.url() + "/" + str(idx)
# update db record
course_updates.definition['data'] = html.tostring(course_html_parsed)
modulestore('direct').update_item(location, course_updates.definition['data'])
return {"id": passed_id,
"date": update['date'],
"content": update['content']}
course_updates.data = etree.tostring(course_html_parsed)
modulestore('direct').update_item(location, course_updates.data)
return {"id" : passed_id,
"date" : update['date'],
"content" :update['content']}
def delete_course_update(location, update, passed_id):
"""
@@ -99,19 +98,19 @@ def delete_course_update(location, update, passed_id):
Returns the resulting course_updates b/c their ids change.
"""
if not passed_id:
return HttpResponseBadRequest
return HttpResponseBadRequest()
try:
course_updates = modulestore('direct').get_item(location)
except ItemNotFoundError:
return HttpResponseBadRequest
return HttpResponseBadRequest()
# TODO use delete_blank_text parser throughout and cache as a static var in a class
# purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
try:
course_html_parsed = html.fromstring(course_updates.definition['data'])
except:
course_html_parsed = html.fromstring("<ol></ol>")
course_html_parsed = etree.fromstring(course_updates.data)
except etree.XMLSyntaxError:
course_html_parsed = etree.fromstring("<ol></ol>")
if course_html_parsed.tag == 'ol':
# ??? Should this use the id in the json or in the url or does it matter?
@@ -122,9 +121,9 @@ def delete_course_update(location, update, passed_id):
course_html_parsed.remove(element_to_delete)
# update db record
course_updates.definition['data'] = html.tostring(course_html_parsed)
course_updates.data = etree.tostring(course_html_parsed)
store = modulestore('direct')
store.update_item(location, course_updates.definition['data'])
store.update_item(location, course_updates.data)
return get_course_updates(location)

View File

@@ -2,53 +2,41 @@ Feature: Advanced (manual) course policy
In order to specify course policy settings for which no custom user interface exists
I want to be able to manually enter JSON key/value pairs
Scenario: A course author sees only display_name on a newly created course
Scenario: A course author sees default advanced settings
Given I have opened a new course in Studio
When I select the Advanced Settings
Then I see only the display name
Then I see default advanced settings
@skip-phantom
Scenario: Test if there are no policy settings without existing UI controls
Scenario: Add new entries, and they appear alphabetically after save
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
When I edit the name of a policy key
And I press the "Save" notification button
Then the policy key name is changed
Then the settings are alphabetized
Scenario: Test cancel editing key value
Given I am on the Advanced Course Settings page in Studio
When I edit the value of a policy key
And I press the "Cancel" notification button
Then the policy key value is unchanged
And I reload the page
Then the policy key value is unchanged
@skip-phantom
Scenario: Test editing key value
Given I am on the Advanced Course Settings page in Studio
When I edit the value of a policy key
And I press the "Save" notification button
Then the policy key value is changed
Scenario: Add new entries, and they appear alphabetically after save
Given I am on the Advanced Course Settings page in Studio
When I create New Entries
Then they are alphabetized
And I reload the page
Then they are alphabetized
Then the policy key value is changed
Scenario: Test how multi-line input appears
Given I am on the Advanced Course Settings page in Studio
When I create a JSON object
When I create a JSON object as a value
Then it is displayed as formatted
And I reload the page
Then it is displayed as formatted
Scenario: Test automatic quoting of non-JSON values
Given I am on the Advanced Course Settings page in Studio
When I create a non-JSON value not in quotes
Then it is displayed as a string
And I reload the page
Then it is displayed as a string

View File

@@ -1,6 +1,7 @@
from lettuce import world, step
from common import *
import time
from terrain.steps import reload_the_page
from selenium.common.exceptions import WebDriverException
from selenium.webdriver.support import expected_conditions as EC
@@ -11,6 +12,10 @@ http://selenium.googlecode.com/svn/trunk/docs/api/py/webdriver/selenium.webdrive
"""
from selenium.webdriver.common.keys import Keys
KEY_CSS = '.key input.policy-key'
VALUE_CSS = 'textarea.json'
DISPLAY_NAME_KEY = "display_name"
DISPLAY_NAME_VALUE = '"Robot Super Course"'
############### ACTIONS ####################
@step('I select the Advanced Settings$')
@@ -20,7 +25,6 @@ def i_select_advanced_settings(step):
css_click(expand_icon_css)
link_css = 'li.nav-course-settings-advanced a'
css_click(link_css)
# world.browser.click_link_by_text('Advanced Settings')
@step('I am on the Advanced Course Settings page in Studio$')
@@ -29,35 +33,27 @@ def i_am_on_advanced_course_settings(step):
step.given('I select the Advanced Settings')
# TODO: this is copied from terrain's step.py. Need to figure out how to share that code.
@step('I reload the page$')
def reload_the_page(step):
world.browser.reload()
@step(u'I edit the name of a policy key$')
def edit_the_name_of_a_policy_key(step):
policy_key_css = 'input.policy-key'
e = css_find(policy_key_css).first
e.type('_new')
@step(u'I press the "([^"]*)" notification button$')
def press_the_notification_button(step, name):
def is_visible(driver):
return EC.visibility_of_element_located((By.CSS_SELECTOR,css,))
def is_invisible(driver):
return EC.invisibility_of_element_located((By.CSS_SELECTOR,css,))
return EC.visibility_of_element_located((By.CSS_SELECTOR, css,))
# def is_invisible(driver):
# return EC.invisibility_of_element_located((By.CSS_SELECTOR,css,))
css = 'a.%s-button' % name.lower()
wait_for(is_visible)
time.sleep(float(1))
css_click_at(css)
# is_invisible is not returning a boolean, not working
# try:
# css_click_at(css)
# wait_for(is_invisible)
# except WebDriverException, e:
# css_click_at(css)
# wait_for(is_invisible)
try:
css_click_at(css)
wait_for(is_invisible)
except WebDriverException, e:
css_click_at(css)
wait_for(is_invisible)
@step(u'I edit the value of a policy key$')
def edit_the_value_of_a_policy_key(step):
@@ -65,133 +61,86 @@ def edit_the_value_of_a_policy_key(step):
It is hard to figure out how to get into the CodeMirror
area, so cheat and do it from the policy key field :)
"""
policy_key_css = 'input.policy-key'
e = css_find(policy_key_css).first
e = css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)]
e._element.send_keys(Keys.TAB, Keys.END, Keys.ARROW_LEFT, ' ', 'X')
@step('I delete the display name$')
def delete_the_display_name(step):
delete_entry(0)
click_save()
@step('create New Entries$')
def create_new_entries(step):
create_entry("z", "apple")
create_entry("a", "zebra")
click_save()
@step('I create a JSON object$')
@step('I create a JSON object as a value$')
def create_JSON_object(step):
create_entry("json", '{"key": "value", "key_2": "value_2"}')
click_save()
change_display_name_value(step, '{"key": "value", "key_2": "value_2"}')
@step('I create a non-JSON value not in quotes$')
def create_value_not_in_quotes(step):
change_display_name_value(step, 'quote me')
############### RESULTS ####################
@step('I see only the display name$')
def i_see_only_display_name(step):
assert_policy_entries(["display_name"], ['"Robot Super Course"'])
@step('I see default advanced settings$')
def i_see_default_advanced_settings(step):
# Test only a few of the existing properties (there are around 34 of them)
assert_policy_entries(
["advanced_modules", DISPLAY_NAME_KEY, "show_calculator"], ["[]", DISPLAY_NAME_VALUE, "false"])
@step('there are no advanced policy settings$')
def no_policy_settings(step):
keys_css = 'input.policy-key'
val_css = 'textarea.json'
k = world.browser.is_element_not_present_by_css(keys_css, 5)
v = world.browser.is_element_not_present_by_css(val_css, 5)
assert_true(k)
assert_true(v)
@step('they are alphabetized$')
@step('the settings are alphabetized$')
def they_are_alphabetized(step):
assert_policy_entries(["a", "display_name", "z"], ['"zebra"', '"Robot Super Course"', '"apple"'])
key_elements = css_find(KEY_CSS)
all_keys = []
for key in key_elements:
all_keys.append(key.value)
assert_equal(sorted(all_keys), all_keys, "policy keys were not sorted")
@step('it is displayed as formatted$')
def it_is_formatted(step):
assert_policy_entries(["display_name", "json"], ['"Robot Super Course"', '{\n "key": "value",\n "key_2": "value_2"\n}'])
assert_policy_entries([DISPLAY_NAME_KEY], ['{\n "key": "value",\n "key_2": "value_2"\n}'])
@step(u'the policy key name is unchanged$')
def the_policy_key_name_is_unchanged(step):
policy_key_css = 'input.policy-key'
val = css_find(policy_key_css).first.value
assert_equal(val, 'display_name')
@step(u'the policy key name is changed$')
def the_policy_key_name_is_changed(step):
policy_key_css = 'input.policy-key'
val = css_find(policy_key_css).first.value
assert_equal(val, 'display_name_new')
@step('it is displayed as a string')
def it_is_formatted(step):
assert_policy_entries([DISPLAY_NAME_KEY], ['"quote me"'])
@step(u'the policy key value is unchanged$')
def the_policy_key_value_is_unchanged(step):
policy_value_css = 'li.course-advanced-policy-list-item div.value textarea'
val = css_find(policy_value_css).first.value
assert_equal(val, '"Robot Super Course"')
assert_equal(get_display_name_value(), DISPLAY_NAME_VALUE)
@step(u'the policy key value is changed$')
def the_policy_key_value_is_unchanged(step):
policy_value_css = 'li.course-advanced-policy-list-item div.value textarea'
val = css_find(policy_value_css).first.value
assert_equal(val, '"Robot Super Course X"')
def the_policy_key_value_is_changed(step):
assert_equal(get_display_name_value(), '"Robot Super Course X"')
############# HELPERS ###############
def create_entry(key, value):
# Scroll down the page so the button is visible
world.scroll_to_bottom()
css_click_at('a.new-advanced-policy-item', 10, 10)
new_key_css = 'div#__new_advanced_key__ input'
new_key_element = css_find(new_key_css).first
new_key_element.fill(key)
# For some reason have to get the instance for each command
# (get error that it is no longer attached to the DOM)
# Have to do all this because Selenium fill does not remove existing text
new_value_css = 'div.CodeMirror textarea'
css_find(new_value_css).last.fill("")
css_find(new_value_css).last._element.send_keys(Keys.DELETE, Keys.DELETE)
css_find(new_value_css).last.fill(value)
# Add in a TAB key press because intermittently on ubuntu the
# last character of "value" above was not getting typed in
css_find(new_value_css).last._element.send_keys(Keys.TAB)
def delete_entry(index):
"""
Delete the nth entry where index is 0-based
"""
css = 'a.delete-button'
assert_true(world.browser.is_element_present_by_css(css, 5))
delete_buttons = css_find(css)
assert_true(len(delete_buttons) > index, "no delete button exists for entry " + str(index))
delete_buttons[index].click()
def assert_policy_entries(expected_keys, expected_values):
assert_entries('.key input.policy-key', expected_keys)
assert_entries('textarea.json', expected_values)
for counter in range(len(expected_keys)):
index = get_index_of(expected_keys[counter])
assert_false(index == -1, "Could not find key: " + expected_keys[counter])
assert_equal(expected_values[counter], css_find(VALUE_CSS)[index].value, "value is incorrect")
def assert_entries(css, expected_values):
webElements = css_find(css)
assert_equal(len(expected_values), len(webElements))
# Sometimes get stale reference if I hold on to the array of elements
for counter in range(len(expected_values)):
assert_equal(expected_values[counter], css_find(css)[counter].value)
def get_index_of(expected_key):
for counter in range(len(css_find(KEY_CSS))):
# Sometimes get stale reference if I hold on to the array of elements
key = css_find(KEY_CSS)[counter].value
if key == expected_key:
return counter
return -1
def click_save():
css = "a.save-button"
css_click_at(css)
def get_display_name_value():
index = get_index_of(DISPLAY_NAME_KEY)
return css_find(VALUE_CSS)[index].value
def fill_last_field(value):
newValue = css_find('#__new_advanced_key__ input').first
newValue.fill(value)
def change_display_name_value(step, new_value):
e = css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)]
display_name = get_display_name_value()
for count in range(len(display_name)):
e._element.send_keys(Keys.TAB, Keys.END, Keys.BACK_SPACE)
# Must delete "" before typing the JSON value
e._element.send_keys(Keys.TAB, Keys.END, Keys.BACK_SPACE, Keys.BACK_SPACE, new_value)
press_the_notification_button(step, "Save")

View File

@@ -7,7 +7,7 @@ from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore
from xmodule.modulestore import Location
from xmodule.course_module import CourseDescriptor
from prompt import query_yes_no
from .prompt import query_yes_no
from auth.authz import _delete_course_group

View File

@@ -15,10 +15,10 @@ def get_module_info(store, location, parent_location=None, rewrite_static_links=
template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty'])
module = store.clone_item(template_location, location)
data = module.definition['data']
data = module.data
if rewrite_static_links:
data = replace_static_urls(
module.definition['data'],
module.data,
None,
course_namespace=Location([
module.location.tag,
@@ -32,7 +32,8 @@ def get_module_info(store, location, parent_location=None, rewrite_static_links=
return {
'id': module.location.url(),
'data': data,
'metadata': module.metadata
# TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
'metadata': module._model_data._kvs._metadata
}
@@ -70,23 +71,23 @@ def set_module_info(store, location, post_data):
# 'apply' the submitted metadata, so we don't end up deleting system metadata
if post_data.get('metadata') is not None:
posted_metadata = post_data['metadata']
# update existing metadata with submitted metadata (which can be partial)
# IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
for metadata_key in posted_metadata.keys():
for metadata_key, value in posted_metadata.items():
# let's strip out any metadata fields from the postback which have been identified as system metadata
# and therefore should not be user-editable, so we should accept them back from the client
if metadata_key in module.system_metadata_fields:
del posted_metadata[metadata_key]
elif posted_metadata[metadata_key] is None:
# remove both from passed in collection as well as the collection read in from the modulestore
if metadata_key in module.metadata:
del module.metadata[metadata_key]
if metadata_key in module._model_data:
del module._model_data[metadata_key]
del posted_metadata[metadata_key]
# overlay the new metadata over the modulestore sourced collection to support partial updates
module.metadata.update(posted_metadata)
else:
module._model_data[metadata_key] = value
# commit to datastore
store.update_metadata(location, module.metadata)
# TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
store.update_metadata(location, module._model_data._kvs._metadata)

View File

@@ -6,15 +6,16 @@ from django.conf import settings
from django.core.urlresolvers import reverse
from path import path
from tempdir import mkdtemp_clean
from datetime import timedelta
import json
from fs.osfs import OSFS
import copy
from json import loads
from django.contrib.auth.models import User
from cms.djangoapps.contentstore.utils import get_modulestore
from contentstore.utils import get_modulestore
from utils import ModuleStoreTestCase, parse_json
from .utils import ModuleStoreTestCase, parse_json
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore import Location
@@ -25,6 +26,7 @@ from xmodule.contentstore.django import contentstore
from xmodule.templates import update_templates
from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.inheritance import own_metadata
from xmodule.capa_module import CapaDescriptor
from xmodule.course_module import CourseDescriptor
@@ -109,10 +111,10 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter','Week_1', None]))
# make sure the parent no longer points to the child object which was deleted
self.assertTrue(sequential.location.url() in chapter.definition['children'])
self.assertTrue(sequential.location.url() in chapter.children)
self.client.post(reverse('delete_item'),
json.dumps({'id': sequential.location.url(), 'delete_children':'true'}),
self.client.post(reverse('delete_item'),
json.dumps({'id': sequential.location.url(), 'delete_children': 'true', 'delete_all_versions': 'true'}),
"application/json")
found = False
@@ -127,9 +129,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter','Week_1', None]))
# make sure the parent no longer points to the child object which was deleted
self.assertFalse(sequential.location.url() in chapter.definition['children'])
self.assertFalse(sequential.location.url() in chapter.children)
def test_about_overrides(self):
'''
@@ -139,11 +141,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
module_store = modulestore('direct')
effort = module_store.get_item(Location(['i4x', 'edX', 'full', 'about', 'effort', None]))
self.assertEqual(effort.definition['data'], '6 hours')
self.assertEqual(effort.data, '6 hours')
# this one should be in a non-override folder
effort = module_store.get_item(Location(['i4x', 'edX', 'full', 'about', 'end_date', None]))
self.assertEqual(effort.definition['data'], 'TBD')
self.assertEqual(effort.data, 'TBD')
def test_remove_hide_progress_tab(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
@@ -153,7 +155,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
course = module_store.get_item(source_location)
self.assertNotIn('hide_progress_tab', course.metadata)
self.assertFalse(course.hide_progress_tab)
def test_clone_course(self):
@@ -246,7 +248,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# compare what's on disk compared to what we have in our course
with fs.open('grading_policy.json', 'r') as grading_policy:
on_disk = loads(grading_policy.read())
self.assertEqual(on_disk, course.definition['data']['grading_policy'])
self.assertEqual(on_disk, course.grading_policy)
#check for policy.json
self.assertTrue(fs.exists('policy.json'))
@@ -255,7 +257,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
with fs.open('policy.json', 'r') as course_policy:
on_disk = loads(course_policy.read())
self.assertIn('course/6.002_Spring_2012', on_disk)
self.assertEqual(on_disk['course/6.002_Spring_2012'], course.metadata)
self.assertEqual(on_disk['course/6.002_Spring_2012'], own_metadata(course))
# remove old course
delete_course(module_store, content_store, location)
@@ -302,10 +304,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
course = module_store.get_item(location)
metadata = own_metadata(course)
# add a bool piece of unknown metadata so we can verify we don't throw an exception
course.metadata['new_metadata'] = True
metadata['new_metadata'] = True
module_store.update_metadata(location, course.metadata)
module_store.update_metadata(location, metadata)
print 'Exporting to tempdir = {0}'.format(root_dir)
@@ -473,21 +476,20 @@ class ContentStoreTest(ModuleStoreTestCase):
self.assertIsInstance(problem, CapaDescriptor, "New problem is not a CapaDescriptor")
context = problem.get_context()
self.assertIn('markdown', context, "markdown is missing from context")
self.assertIn('markdown', problem.metadata, "markdown is missing from metadata")
self.assertNotIn('markdown', problem.editable_metadata_fields, "Markdown slipped into the editable metadata fields")
def test_import_metadata_with_attempts_empty_string(self):
import_from_xml(modulestore(), 'common/test/data/', ['simple'])
module_store = modulestore('direct')
did_load_item = False
try:
try:
module_store.get_item(Location(['i4x', 'edX', 'simple', 'problem', 'ps01-simple', None]))
did_load_item = True
except ItemNotFoundError:
pass
# make sure we found the item (e.g. it didn't error while loading)
self.assertTrue(did_load_item)
self.assertTrue(did_load_item)
def test_metadata_inheritance(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
@@ -499,8 +501,7 @@ class ContentStoreTest(ModuleStoreTestCase):
# let's assert on the metadata_inheritance on an existing vertical
for vertical in verticals:
self.assertIn('xqa_key', vertical.metadata)
self.assertEqual(course.metadata['xqa_key'], vertical.metadata['xqa_key'])
self.assertEqual(course.lms.xqa_key, vertical.lms.xqa_key)
self.assertGreater(len(verticals), 0)
@@ -510,36 +511,33 @@ class ContentStoreTest(ModuleStoreTestCase):
# crate a new module and add it as a child to a vertical
module_store.clone_item(source_template_location, new_component_location)
parent = verticals[0]
module_store.update_children(parent.location, parent.definition.get('children', []) + [new_component_location.url()])
module_store.update_children(parent.location, parent.children + [new_component_location.url()])
# flush the cache
module_store.get_cached_metadata_inheritance_tree(new_component_location, -1)
new_module = module_store.get_item(new_component_location)
# check for grace period definition which should be defined at the course level
self.assertIn('graceperiod', new_module.metadata)
self.assertEqual(parent.lms.graceperiod, new_module.lms.graceperiod)
self.assertEqual(parent.metadata['graceperiod'], new_module.metadata['graceperiod'])
self.assertEqual(course.metadata['xqa_key'], new_module.metadata['xqa_key'])
self.assertEqual(course.lms.xqa_key, new_module.lms.xqa_key)
#
# now let's define an override at the leaf node level
#
new_module.metadata['graceperiod'] = '1 day'
module_store.update_metadata(new_module.location, new_module.metadata)
new_module.lms.graceperiod = timedelta(1)
module_store.update_metadata(new_module.location, own_metadata(new_module))
# flush the cache and refetch
module_store.get_cached_metadata_inheritance_tree(new_component_location, -1)
new_module = module_store.get_item(new_component_location)
self.assertIn('graceperiod', new_module.metadata)
self.assertEqual('1 day', new_module.metadata['graceperiod'])
self.assertEqual(timedelta(1), new_module.lms.graceperiod)
class TemplateTestCase(ModuleStoreTestCase):
def test_template_cleanup(self):
def test_template_cleanup(self):
module_store = modulestore('direct')
# insert a bogus template in the store
@@ -562,4 +560,3 @@ class TemplateTestCase(ModuleStoreTestCase):
asserted = True
self.assertTrue(asserted)

View File

@@ -10,16 +10,16 @@ from django.core.urlresolvers import reverse
from django.utils.timezone import UTC
from xmodule.modulestore import Location
from cms.djangoapps.models.settings.course_details import (CourseDetails,
from models.settings.course_details import (CourseDetails,
CourseSettingsEncoder)
from cms.djangoapps.models.settings.course_grading import CourseGradingModel
from cms.djangoapps.contentstore.utils import get_modulestore
from models.settings.course_grading import CourseGradingModel
from contentstore.utils import get_modulestore
from django.test import TestCase
from utils import ModuleStoreTestCase
from .utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from cms.djangoapps.models.settings.course_metadata import CourseMetadata
from models.settings.course_metadata import CourseMetadata
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.django import modulestore
@@ -246,8 +246,9 @@ class CourseGradingTest(CourseTestCase):
altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "cutoff add D")
test_grader.grace_period = {'hours' : 4, 'minutes' : 5, 'seconds': 0}
test_grader.grace_period = {'hours': 4, 'minutes': 5, 'seconds': 0}
altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
print test_grader.grace_period, altered_grader.grace_period
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "4 hour grace period")
def test_update_grader_from_json(self):
@@ -286,31 +287,31 @@ class CourseMetadataEditingTest(CourseTestCase):
def test_update_from_json(self):
test_model = CourseMetadata.update_from_json(self.course_location,
{ "a" : 1,
"b_a_c_h" : { "c" : "test" },
"test_text" : "a text string"})
{ "advertised_start" : "start A",
"testcenter_info" : { "c" : "test" },
"days_early_for_beta" : 2})
self.update_check(test_model)
# try fresh fetch to ensure persistence
test_model = CourseMetadata.fetch(self.course_location)
self.update_check(test_model)
# now change some of the existing metadata
test_model = CourseMetadata.update_from_json(self.course_location,
{ "a" : 2,
{ "advertised_start" : "start B",
"display_name" : "jolly roger"})
self.assertIn('display_name', test_model, 'Missing editable metadata field')
self.assertEqual(test_model['display_name'], 'jolly roger', "not expected value")
self.assertIn('a', test_model, 'Missing revised a metadata field')
self.assertEqual(test_model['a'], 2, "a not expected value")
self.assertIn('advertised_start', test_model, 'Missing revised advertised_start metadata field')
self.assertEqual(test_model['advertised_start'], 'start B', "advertised_start not expected value")
def update_check(self, test_model):
self.assertIn('display_name', test_model, 'Missing editable metadata field')
self.assertEqual(test_model['display_name'], 'Robot Super Course', "not expected value")
self.assertIn('a', test_model, 'Missing new a metadata field')
self.assertEqual(test_model['a'], 1, "a not expected value")
self.assertIn('b_a_c_h', test_model, 'Missing b_a_c_h metadata field')
self.assertDictEqual(test_model['b_a_c_h'], { "c" : "test" }, "b_a_c_h not expected value")
self.assertIn('test_text', test_model, 'Missing test_text metadata field')
self.assertEqual(test_model['test_text'], "a text string", "test_text not expected value")
self.assertIn('advertised_start', test_model, 'Missing new advertised_start metadata field')
self.assertEqual(test_model['advertised_start'], 'start A', "advertised_start not expected value")
self.assertIn('testcenter_info', test_model, 'Missing testcenter_info metadata field')
self.assertDictEqual(test_model['testcenter_info'], { "c" : "test" }, "testcenter_info not expected value")
self.assertIn('days_early_for_beta', test_model, 'Missing days_early_for_beta metadata field')
self.assertEqual(test_model['days_early_for_beta'], 2, "days_early_for_beta not expected value")
def test_delete_key(self):
@@ -321,5 +322,5 @@ class CourseMetadataEditingTest(CourseTestCase):
self.assertEqual(test_model['display_name'], 'Testing', "not expected value")
self.assertIn('rerandomize', test_model, 'Missing rerandomize metadata field')
# check for deletion effectiveness
self.assertNotIn('showanswer', test_model, 'showanswer field still in')
self.assertNotIn('xqa_key', test_model, 'xqa_key field still in')
self.assertEqual('closed', test_model['showanswer'], 'showanswer field still in')
self.assertEqual(None, test_model['xqa_key'], 'xqa_key field still in')

View File

@@ -1,4 +1,4 @@
from cms.djangoapps.contentstore.tests.test_course_settings import CourseTestCase
from contentstore.tests.test_course_settings import CourseTestCase
from django.core.urlresolvers import reverse
import json

View File

@@ -1,4 +1,4 @@
from cms.djangoapps.contentstore import utils
from contentstore import utils
import mock
from django.test import TestCase

View File

@@ -8,7 +8,7 @@ import json
from fs.osfs import OSFS
import copy
from cms.djangoapps.contentstore.utils import get_modulestore
from contentstore.utils import get_modulestore
from xmodule.modulestore import Location
from xmodule.modulestore.store_utilities import clone_course
@@ -24,7 +24,7 @@ from xmodule.course_module import CourseDescriptor
from xmodule.seq_module import SequenceDescriptor
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from utils import ModuleStoreTestCase, parse_json, user, registration
from .utils import ModuleStoreTestCase, parse_json, user, registration
class ContentStoreTestCase(ModuleStoreTestCase):

View File

@@ -39,10 +39,10 @@ def get_course_location_for_item(location):
# make sure we found exactly one match on this above course search
found_cnt = len(courses)
if found_cnt == 0:
raise BaseException('Could not find course at {0}'.format(course_search_location))
raise Exception('Could not find course at {0}'.format(course_search_location))
if found_cnt > 1:
raise BaseException('Found more than one course at {0}. There should only be one!!! Dump = {1}'.format(course_search_location, courses))
raise Exception('Found more than one course at {0}. There should only be one!!! Dump = {1}'.format(course_search_location, courses))
location = courses[0].location
@@ -136,7 +136,7 @@ def compute_unit_state(unit):
'private' content is editabled and not visible in the LMS
"""
if unit.metadata.get('is_draft', False):
if unit.cms.is_draft:
try:
modulestore('direct').get_item(unit.location)
return UnitState.draft

View File

@@ -28,11 +28,15 @@ from django.conf import settings
from xmodule.modulestore import Location
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
from xmodule.modulestore.inheritance import own_metadata
from xblock.core import Scope
from xblock.runtime import KeyValueStore, DbModel, InvalidScopeError
from xmodule.x_module import ModuleSystem
from xmodule.error_module import ErrorDescriptor
from xmodule.errortracker import exc_info_to_str
import static_replace
from external_auth.views import ssl_login_shortcut
from xmodule.modulestore.mongo import MongoUsage
from mitxmako.shortcuts import render_to_response, render_to_string
from xmodule.modulestore.django import modulestore
@@ -54,12 +58,12 @@ from contentstore.course_info_model import get_course_updates,\
from cache_toolbox.core import del_cached_content
from xmodule.timeparse import stringify_time
from contentstore.module_info_model import get_module_info, set_module_info
from cms.djangoapps.models.settings.course_details import CourseDetails,\
from models.settings.course_details import CourseDetails,\
CourseSettingsEncoder
from cms.djangoapps.models.settings.course_grading import CourseGradingModel
from cms.djangoapps.contentstore.utils import get_modulestore
from models.settings.course_grading import CourseGradingModel
from contentstore.utils import get_modulestore
from django.shortcuts import redirect
from cms.djangoapps.models.settings.course_metadata import CourseMetadata
from models.settings.course_metadata import CourseMetadata
# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
@@ -110,7 +114,7 @@ def login_page(request):
def howitworks(request):
if request.user.is_authenticated():
return index(request)
else:
else:
return render_to_response('howitworks.html', {})
def ux_alerts(request):
@@ -138,7 +142,7 @@ def index(request):
return render_to_response('index.html', {
'new_course_template': Location('i4x', 'edx', 'templates', 'course', 'Empty'),
'courses': [(course.metadata.get('display_name'),
'courses': [(course.display_name,
reverse('course_index', args=[
course.location.org,
course.location.course,
@@ -242,8 +246,13 @@ def edit_subsection(request, location):
# remove all metadata from the generic dictionary that is presented in a more normalized UI
policy_metadata = dict((key, value) for key, value in item.metadata.iteritems()
if key not in ['display_name', 'start', 'due', 'format'] and key not in item.system_metadata_fields)
policy_metadata = dict(
(field.name, field.read_from(item))
for field
in item.fields
if field.name not in ['display_name', 'start', 'due', 'format'] and
field.scope == Scope.settings
)
can_view_live = False
subsection_units = item.get_children()
@@ -296,8 +305,7 @@ def edit_unit(request, location):
# Check if there are any advanced modules specified in the course policy. These modules
# should be specified as a list of strings, where the strings are the names of the modules
# in ADVANCED_COMPONENT_TYPES that should be enabled for the course.
course_metadata = CourseMetadata.fetch(course.location)
course_advanced_keys = course_metadata.get(ADVANCED_COMPONENT_POLICY_KEY, [])
course_advanced_keys = course.advanced_modules
# Set component types according to course policy file
component_types = list(COMPONENT_TYPES)
@@ -318,10 +326,10 @@ def edit_unit(request, location):
if category in component_types:
#This is a hack to create categories for different xmodules
component_templates[category].append((
template.display_name,
template.display_name_with_default,
template.location.url(),
'markdown' in template.metadata,
'empty' in template.metadata
hasattr(template, 'markdown') and template.markdown is not None,
template.cms.empty,
))
components = [
@@ -365,11 +373,6 @@ def edit_unit(request, location):
unit_state = compute_unit_state(item)
try:
published_date = time.strftime('%B %d, %Y', item.metadata.get('published_date'))
except TypeError:
published_date = None
return render_to_response('unit.html', {
'context_course': course,
'active_tab': 'courseware',
@@ -380,11 +383,11 @@ def edit_unit(request, location):
'draft_preview_link': preview_lms_link,
'published_preview_link': lms_link,
'subsection': containing_subsection,
'release_date': get_date_display(datetime.fromtimestamp(time.mktime(containing_subsection.start))) if containing_subsection.start is not None else None,
'release_date': get_date_display(datetime.fromtimestamp(time.mktime(containing_subsection.lms.start))) if containing_subsection.lms.start is not None else None,
'section': containing_section,
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
'unit_state': unit_state,
'published_date': published_date,
'published_date': item.cms.published_date.strftime('%B %d, %Y') if item.cms.published_date is not None else None,
})
@@ -449,9 +452,8 @@ def preview_dispatch(request, preview_id, location, dispatch=None):
dispatch: The action to execute
"""
instance_state, shared_state = load_preview_state(request, preview_id, location)
descriptor = modulestore().get_item(location)
instance = load_preview_module(request, preview_id, descriptor, instance_state, shared_state)
instance = load_preview_module(request, preview_id, descriptor)
# Let the module handle the AJAX
try:
ajax_return = instance.handle_ajax(dispatch, request.POST)
@@ -462,46 +464,9 @@ def preview_dispatch(request, preview_id, location, dispatch=None):
log.exception("error processing ajax call")
raise
save_preview_state(request, preview_id, location, instance.get_instance_state(), instance.get_shared_state())
return HttpResponse(ajax_return)
def load_preview_state(request, preview_id, location):
"""
Load the state of a preview module from the request
preview_id (str): An identifier specifying which preview this module is used for
location: The Location of the module to dispatch to
"""
if 'preview_states' not in request.session:
request.session['preview_states'] = defaultdict(dict)
instance_state = request.session['preview_states'][preview_id, location].get('instance')
shared_state = request.session['preview_states'][preview_id, location].get('shared')
return instance_state, shared_state
def save_preview_state(request, preview_id, location, instance_state, shared_state):
"""
Save the state of a preview module to the request
preview_id (str): An identifier specifying which preview this module is used for
location: The Location of the module to dispatch to
instance_state: The instance state to save
shared_state: The shared state to save
"""
if 'preview_states' not in request.session:
request.session['preview_states'] = defaultdict(dict)
# request.session doesn't notice indirect changes; so, must set its dict w/ every change to get
# it to persist: http://www.djangobook.com/en/2.0/chapter14.html
preview_states = request.session['preview_states']
preview_states[preview_id, location]['instance'] = instance_state
preview_states[preview_id, location]['shared'] = shared_state
request.session['preview_states'] = preview_states # make session mgmt notice the update
def render_from_lms(template_name, dictionary, context=None, namespace='main'):
"""
Render a template using the LMS MAKO_TEMPLATES
@@ -509,6 +474,33 @@ def render_from_lms(template_name, dictionary, context=None, namespace='main'):
return render_to_string(template_name, dictionary, context, namespace="lms." + namespace)
class SessionKeyValueStore(KeyValueStore):
def __init__(self, request, model_data):
self._model_data = model_data
self._session = request.session
def get(self, key):
try:
return self._model_data[key.field_name]
except (KeyError, InvalidScopeError):
return self._session[tuple(key)]
def set(self, key, value):
try:
self._model_data[key.field_name] = value
except (KeyError, InvalidScopeError):
self._session[tuple(key)] = value
def delete(self, key):
try:
del self._model_data[key.field_name]
except (KeyError, InvalidScopeError):
del self._session[tuple(key)]
def has(self, key):
return key in self._model_data or key in self._session
def preview_module_system(request, preview_id, descriptor):
"""
Returns a ModuleSystem for the specified descriptor that is specialized for
@@ -519,6 +511,14 @@ def preview_module_system(request, preview_id, descriptor):
descriptor: An XModuleDescriptor
"""
def preview_model_data(descriptor):
return DbModel(
SessionKeyValueStore(request, descriptor._model_data),
descriptor.module_class,
preview_id,
MongoUsage(preview_id, descriptor.location.url()),
)
return ModuleSystem(
ajax_url=reverse('preview_dispatch', args=[preview_id, descriptor.location.url(), '']).rstrip('/'),
# TODO (cpennington): Do we want to track how instructors are using the preview problems?
@@ -529,6 +529,7 @@ def preview_module_system(request, preview_id, descriptor):
debug=True,
replace_urls=partial(static_replace.replace_static_urls, data_directory=None, course_namespace=descriptor.location),
user=request.user,
xblock_model_data=preview_model_data,
)
@@ -541,11 +542,11 @@ def get_preview_module(request, preview_id, descriptor):
preview_id (str): An identifier specifying which preview this module is used for
location: A Location
"""
instance_state, shared_state = descriptor.get_sample_state()[0]
return load_preview_module(request, preview_id, descriptor, instance_state, shared_state)
return load_preview_module(request, preview_id, descriptor)
def load_preview_module(request, preview_id, descriptor, instance_state, shared_state):
def load_preview_module(request, preview_id, descriptor):
"""
Return a preview XModule instantiated from the supplied descriptor, instance_state, and shared_state
@@ -557,12 +558,13 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_
"""
system = preview_module_system(request, preview_id, descriptor)
try:
module = descriptor.xmodule_constructor(system)(instance_state, shared_state)
module = descriptor.xmodule(system)
except:
log.debug("Unable to load preview module", exc_info=True)
module = ErrorDescriptor.from_descriptor(
descriptor,
error_msg=exc_info_to_str(sys.exc_info())
).xmodule_constructor(system)(None, None)
).xmodule(system)
# cdodge: Special case
if module.location.category == 'static_tab':
@@ -580,11 +582,9 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_
module.get_html = replace_static_urls(
module.get_html,
module.metadata.get('data_dir', module.location.course),
getattr(module, 'data_dir', module.location.course),
course_namespace=Location([module.location.tag, module.location.org, module.location.course, None, None])
)
save_preview_state(request, preview_id, descriptor.location.url(),
module.get_instance_state(), module.get_shared_state())
return module
@@ -598,7 +598,7 @@ def get_module_previews(request, descriptor):
"""
preview_html = []
for idx, (instance_state, shared_state) in enumerate(descriptor.get_sample_state()):
module = load_preview_module(request, str(idx), descriptor, instance_state, shared_state)
module = load_preview_module(request, str(idx), descriptor)
preview_html.append(module.get_html())
return preview_html
@@ -646,15 +646,17 @@ def delete_item(request):
modulestore('direct').delete_item(item.location)
# cdodge: we need to remove our parent's pointer to us so that it is no longer dangling
if delete_all_versions:
parent_locs = modulestore('direct').get_parent_locations(item_loc, None)
parent_locs = modulestore('direct').get_parent_locations(item_loc, None)
for parent_loc in parent_locs:
parent = modulestore('direct').get_item(parent_loc)
item_url = item_loc.url()
if item_url in parent.definition["children"]:
parent.definition["children"].remove(item_url)
modulestore('direct').update_children(parent.location, parent.definition["children"])
for parent_loc in parent_locs:
parent = modulestore('direct').get_item(parent_loc)
item_url = item_loc.url()
if item_url in parent.children:
children = parent.children
children.remove(item_url)
parent.children = children
modulestore('direct').update_children(parent.location, parent.children)
return HttpResponse()
@@ -693,7 +695,7 @@ def save_item(request):
# update existing metadata with submitted metadata (which can be partial)
# IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
for metadata_key in posted_metadata.keys():
for metadata_key, value in posted_metadata.items():
# let's strip out any metadata fields from the postback which have been identified as system metadata
# and therefore should not be user-editable, so we should accept them back from the client
@@ -701,15 +703,15 @@ def save_item(request):
del posted_metadata[metadata_key]
elif posted_metadata[metadata_key] is None:
# remove both from passed in collection as well as the collection read in from the modulestore
if metadata_key in existing_item.metadata:
del existing_item.metadata[metadata_key]
if metadata_key in existing_item._model_data:
del existing_item._model_data[metadata_key]
del posted_metadata[metadata_key]
# overlay the new metadata over the modulestore sourced collection to support partial updates
existing_item.metadata.update(posted_metadata)
else:
existing_item._model_data[metadata_key] = value
# commit to datastore
store.update_metadata(item_location, existing_item.metadata)
# TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
store.update_metadata(item_location, own_metadata(existing_item))
return HttpResponse()
@@ -776,17 +778,14 @@ def clone_item(request):
new_item = get_modulestore(template).clone_item(template, dest_location)
# TODO: This needs to be deleted when we have proper storage for static content
new_item.metadata['data_dir'] = parent.metadata['data_dir']
# replace the display name with an optional parameter passed in from the caller
if display_name is not None:
new_item.metadata['display_name'] = display_name
new_item.display_name = display_name
get_modulestore(template).update_metadata(new_item.location.url(), new_item.own_metadata)
get_modulestore(template).update_metadata(new_item.location.url(), own_metadata(new_item))
if new_item.location.category not in DETACHED_CATEGORIES:
get_modulestore(parent.location).update_children(parent_location, parent.definition.get('children', []) + [new_item.location.url()])
get_modulestore(parent.location).update_children(parent_location, parent.children + [new_item.location.url()])
return HttpResponse(json.dumps({'id': dest_location.url()}))
@@ -1005,7 +1004,7 @@ def reorder_static_tabs(request):
for tab in course.tabs:
if tab['type'] == 'static_tab':
reordered_tabs.append({'type': 'static_tab',
'name': tab_items[static_tab_idx].metadata.get('display_name'),
'name': tab_items[static_tab_idx].display_name,
'url_slug': tab_items[static_tab_idx].location.name})
static_tab_idx += 1
else:
@@ -1014,7 +1013,7 @@ def reorder_static_tabs(request):
# OK, re-assemble the static tabs in the new order
course.tabs = reordered_tabs
modulestore('direct').update_metadata(course.location, course.metadata)
modulestore('direct').update_metadata(course.location, own_metadata(course))
return HttpResponse()
@@ -1232,7 +1231,6 @@ def course_config_advanced_page(request, org, course, name):
return render_to_response('settings_advanced.html', {
'context_course': course_module,
'course_location' : location,
'advanced_blacklist' : json.dumps(CourseMetadata.FILTERED_LIST),
'advanced_dict' : json.dumps(CourseMetadata.fetch(location)),
})
@@ -1315,7 +1313,7 @@ def course_advanced_updates(request, org, course, name):
org, course: Attributes of the Location for the item to edit
"""
location = ['i4x', org, course, 'course', name]
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
@@ -1325,7 +1323,7 @@ def course_advanced_updates(request, org, course, name):
real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
else:
real_method = request.method
if real_method == 'GET':
return HttpResponse(json.dumps(CourseMetadata.fetch(location)), mimetype="application/json")
elif real_method == 'DELETE':
@@ -1436,13 +1434,10 @@ def create_new_course(request):
new_course = modulestore('direct').clone_item(template, dest_location)
if display_name is not None:
new_course.metadata['display_name'] = display_name
# we need a 'data_dir' for legacy reasons
new_course.metadata['data_dir'] = uuid4().hex
new_course.display_name = display_name
# set a default start date to now
new_course.metadata['start'] = stringify_time(time.gmtime())
new_course.start = time.gmtime()
initialize_course_tabs(new_course)
@@ -1461,12 +1456,12 @@ def initialize_course_tabs(course):
# This logic is repeated in xmodule/modulestore/tests/factories.py
# so if you change anything here, you need to also change it there.
course.tabs = [{"type": "courseware"},
{"type": "course_info", "name": "Course Info"},
{"type": "course_info", "name": "Course Info"},
{"type": "discussion", "name": "Discussion"},
{"type": "wiki", "name": "Wiki"},
{"type": "progress", "name": "Progress"}]
modulestore('direct').update_metadata(course.location.url(), course.own_metadata)
modulestore('direct').update_metadata(course.location.url(), own_metadata(course))
@ensure_csrf_cookie

View File

@@ -1,13 +1,14 @@
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.inheritance import own_metadata
import json
from json.encoder import JSONEncoder
import time
from contentstore.utils import get_modulestore
from util.converters import jsdate_to_time, time_to_date
from cms.djangoapps.models.settings import course_grading
from cms.djangoapps.contentstore.utils import update_item
from models.settings import course_grading
from contentstore.utils import update_item
import re
import logging
@@ -43,25 +44,25 @@ class CourseDetails(object):
temploc = course_location._replace(category='about', name='syllabus')
try:
course.syllabus = get_modulestore(temploc).get_item(temploc).definition['data']
course.syllabus = get_modulestore(temploc).get_item(temploc).data
except ItemNotFoundError:
pass
temploc = temploc._replace(name='overview')
try:
course.overview = get_modulestore(temploc).get_item(temploc).definition['data']
course.overview = get_modulestore(temploc).get_item(temploc).data
except ItemNotFoundError:
pass
temploc = temploc._replace(name='effort')
try:
course.effort = get_modulestore(temploc).get_item(temploc).definition['data']
course.effort = get_modulestore(temploc).get_item(temploc).data
except ItemNotFoundError:
pass
temploc = temploc._replace(name='video')
try:
raw_video = get_modulestore(temploc).get_item(temploc).definition['data']
raw_video = get_modulestore(temploc).get_item(temploc).data
course.intro_video = CourseDetails.parse_video_tag(raw_video)
except ItemNotFoundError:
pass
@@ -116,7 +117,7 @@ class CourseDetails(object):
descriptor.enrollment_end = converted
if dirty:
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
get_modulestore(course_location).update_metadata(course_location, own_metadata(descriptor))
# NOTE: below auto writes to the db w/o verifying that any of the fields actually changed
# to make faster, could compare against db or could have client send over a list of which fields changed.
@@ -133,7 +134,6 @@ class CourseDetails(object):
recomposed_video_tag = CourseDetails.recompose_video_tag(jsondict['intro_video'])
update_item(temploc, recomposed_video_tag)
# Could just generate and return a course obj w/o doing any db reads, but I put the reads in as a means to confirm
# it persisted correctly
return CourseDetails.fetch(course_location)

View File

@@ -2,6 +2,7 @@ from xmodule.modulestore import Location
from contentstore.utils import get_modulestore
import re
from util import converters
from datetime import timedelta
class CourseGradingModel(object):
@@ -91,7 +92,7 @@ class CourseGradingModel(object):
descriptor.raw_grader = graders_parsed
descriptor.grade_cutoffs = jsondict['grade_cutoffs']
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data)
CourseGradingModel.update_grace_period_from_json(course_location, jsondict['grace_period'])
return CourseGradingModel.fetch(course_location)
@@ -119,7 +120,7 @@ class CourseGradingModel(object):
else:
descriptor.raw_grader.append(grader)
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data)
return CourseGradingModel.jsonize_grader(index, descriptor.raw_grader[index])
@@ -134,7 +135,7 @@ class CourseGradingModel(object):
descriptor = get_modulestore(course_location).get_item(course_location)
descriptor.grade_cutoffs = cutoffs
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data)
return cutoffs
@@ -156,11 +157,11 @@ class CourseGradingModel(object):
graceperiodjson = graceperiodjson['grace_period']
# lms requires these to be in a fixed order
grace_rep = "{0[hours]:d} hours {0[minutes]:d} minutes {0[seconds]:d} seconds".format(graceperiodjson)
grace_timedelta = timedelta(**graceperiodjson)
descriptor = get_modulestore(course_location).get_item(course_location)
descriptor.metadata['graceperiod'] = grace_rep
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
descriptor.lms.graceperiod = grace_timedelta
get_modulestore(course_location).update_metadata(course_location, descriptor._model_data._kvs._metadata)
@staticmethod
def delete_grader(course_location, index):
@@ -176,7 +177,7 @@ class CourseGradingModel(object):
del descriptor.raw_grader[index]
# force propagation to definition
descriptor.raw_grader = descriptor.raw_grader
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data)
# NOTE cannot delete cutoffs. May be useful to reset
@staticmethod
@@ -189,7 +190,7 @@ class CourseGradingModel(object):
descriptor = get_modulestore(course_location).get_item(course_location)
descriptor.grade_cutoffs = descriptor.defaut_grading_policy['GRADE_CUTOFFS']
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data)
return descriptor.grade_cutoffs
@@ -202,8 +203,8 @@ class CourseGradingModel(object):
course_location = Location(course_location)
descriptor = get_modulestore(course_location).get_item(course_location)
if 'graceperiod' in descriptor.metadata: del descriptor.metadata['graceperiod']
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
del descriptor.lms.graceperiod
get_modulestore(course_location).update_metadata(course_location, descriptor._model_data._kvs._metadata)
@staticmethod
def get_section_grader_type(location):
@@ -212,7 +213,7 @@ class CourseGradingModel(object):
descriptor = get_modulestore(location).get_item(location)
return {
"graderType": descriptor.metadata.get('format', u"Not Graded"),
"graderType": descriptor.lms.format if descriptor.lms.format is not None else 'Not Graded',
"location": location,
"id": 99 # just an arbitrary value to
}
@@ -224,23 +225,41 @@ class CourseGradingModel(object):
descriptor = get_modulestore(location).get_item(location)
if 'graderType' in jsondict and jsondict['graderType'] != u"Not Graded":
descriptor.metadata['format'] = jsondict.get('graderType')
descriptor.metadata['graded'] = True
descriptor.lms.format = jsondict.get('graderType')
descriptor.lms.graded = True
else:
if 'format' in descriptor.metadata: del descriptor.metadata['format']
if 'graded' in descriptor.metadata: del descriptor.metadata['graded']
del descriptor.lms.format
del descriptor.lms.graded
get_modulestore(location).update_metadata(location, descriptor.metadata)
get_modulestore(location).update_metadata(location, descriptor._model_data._kvs._metadata)
@staticmethod
def convert_set_grace_period(descriptor):
# 5 hours 59 minutes 59 seconds => { hours: 5, minutes : 59, seconds : 59}
rawgrace = descriptor.metadata.get('graceperiod', None)
# 5 hours 59 minutes 59 seconds => converted to iso format
rawgrace = descriptor.lms.graceperiod
if rawgrace:
parsedgrace = {str(key): int(val) for (val, key) in re.findall('\s*(\d+)\s*(\w+)', rawgrace)}
return parsedgrace
else: return None
hours_from_days = rawgrace.days*24
seconds = rawgrace.seconds
hours_from_seconds = int(seconds / 3600)
hours = hours_from_days + hours_from_seconds
seconds -= hours_from_seconds * 3600
minutes = int(seconds / 60)
seconds -= minutes * 60
graceperiod = {'hours': 0, 'minutes': 0, 'seconds': 0}
if hours > 0:
graceperiod['hours'] = hours
if minutes > 0:
graceperiod['minutes'] = minutes
if seconds > 0:
graceperiod['seconds'] = seconds
return graceperiod
else:
return None
@staticmethod
def parse_grader(json_grader):

View File

@@ -1,6 +1,8 @@
from xmodule.modulestore import Location
from contentstore.utils import get_modulestore
from xmodule.x_module import XModuleDescriptor
from xmodule.modulestore.inheritance import own_metadata
from xblock.core import Scope
class CourseMetadata(object):
@@ -8,8 +10,7 @@ class CourseMetadata(object):
For CRUD operations on metadata fields which do not have specific editors on the other pages including any user generated ones.
The objects have no predefined attrs but instead are obj encodings of the editable metadata.
'''
# __new_advanced_key__ is used by client not server; so, could argue against it being here
FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start', 'end', 'enrollment_start', 'enrollment_end', 'tabs', 'graceperiod', '__new_advanced_key__']
FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start', 'end', 'enrollment_start', 'enrollment_end', 'tabs', 'graceperiod']
@classmethod
def fetch(cls, course_location):
@@ -23,17 +24,20 @@ class CourseMetadata(object):
descriptor = get_modulestore(course_location).get_item(course_location)
for k, v in descriptor.metadata.iteritems():
if k not in cls.FILTERED_LIST:
course[k] = v
for field in descriptor.fields + descriptor.lms.fields:
if field.scope != Scope.settings:
continue
if field.name not in cls.FILTERED_LIST:
course[field.name] = field.read_from(descriptor)
return course
@classmethod
def update_from_json(cls, course_location, jsondict):
"""
Decode the json into CourseMetadata and save any changed attrs to the db.
Ensures none of the fields are in the blacklist.
"""
descriptor = get_modulestore(course_location).get_item(course_location)
@@ -42,12 +46,18 @@ class CourseMetadata(object):
for k, v in jsondict.iteritems():
# should it be an error if one of the filtered list items is in the payload?
if k not in cls.FILTERED_LIST and (k not in descriptor.metadata or descriptor.metadata[k] != v):
if k in cls.FILTERED_LIST:
continue
if hasattr(descriptor, k) and getattr(descriptor, k) != v:
dirty = True
descriptor.metadata[k] = v
setattr(descriptor, k, v)
elif hasattr(descriptor.lms, k) and getattr(descriptor.lms, k) != k:
dirty = True
setattr(descriptor.lms, k, v)
if dirty:
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
get_modulestore(course_location).update_metadata(course_location, own_metadata(descriptor))
# Could just generate and return a course obj w/o doing any db reads, but I put the reads in as a means to confirm
# it persisted correctly
@@ -61,10 +71,11 @@ class CourseMetadata(object):
descriptor = get_modulestore(course_location).get_item(course_location)
for key in payload['deleteKeys']:
if key in descriptor.metadata:
del descriptor.metadata[key]
if hasattr(descriptor, key):
delattr(descriptor, key)
elif hasattr(descriptor.lms, key):
delattr(descriptor.lms, key)
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
get_modulestore(course_location).update_metadata(course_location, own_metadata(descriptor))
return cls.fetch(course_location)

View File

@@ -96,6 +96,13 @@ CACHES = {
'KEY_PREFIX': 'general',
'VERSION': 4,
'KEY_FUNCTION': 'util.memcache.safe_key',
},
'mongo_metadata_inheritance': {
'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
'LOCATION': '/var/tmp/mongo_metadata_inheritance',
'TIMEOUT': 300,
'KEY_FUNCTION': 'util.memcache.safe_key',
}
}

View File

@@ -98,6 +98,13 @@ CACHES = {
'KEY_PREFIX': 'general',
'VERSION': 4,
'KEY_FUNCTION': 'util.memcache.safe_key',
},
'mongo_metadata_inheritance': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': '/var/tmp/mongo_metadata_inheritance',
'TIMEOUT': 300,
'KEY_FUNCTION': 'util.memcache.safe_key',
}
}

View File

@@ -1,5 +1,13 @@
from dogapi import dog_http_api, dog_stats_api
from django.conf import settings
from xmodule.modulestore.django import modulestore
from django.core.cache import get_cache, InvalidCacheBackendError
cache = get_cache('mongo_metadata_inheritance')
for store_name in settings.MODULESTORE:
store = modulestore(store_name)
store.metadata_inheritance_cache = cache
if hasattr(settings, 'DATADOG_API'):
dog_http_api.api_key = settings.DATADOG_API

View File

@@ -1,16 +1,11 @@
<li class="field-group course-advanced-policy-list-item">
<div class="field text key" id="<%= (_.isEmpty(key) ? '__new_advanced_key__' : key) %>">
<div class="field is-not-editable text key" id="<%= key %>">
<label for="<%= keyUniqueId %>">Policy Key:</label>
<input type="text" class="short policy-key" id="<%= keyUniqueId %>" value="<%= key %>" />
<span class="tip tip-stacked">Keys are case sensitive and cannot contain spaces or start with a number</span>
<input readonly title="This field is disabled: policy keys cannot be edited." type="text" class="short policy-key" id="<%= keyUniqueId %>" value="<%= key %>" />
</div>
<div class="field text value">
<label for="<%= valueUniqueId %>">Policy Value:</label>
<textarea class="json text" id="<%= valueUniqueId %>"><%= value %></textarea>
</div>
<div class="actions">
<a href="#" class="button delete-button standard remove-item remove-grading-data"><span class="delete-icon"></span>Delete</a>
</div>
</li>

View File

@@ -1,27 +1,15 @@
if (!CMS.Models['Settings']) CMS.Models.Settings = {};
CMS.Models.Settings.Advanced = Backbone.Model.extend({
// the key for a newly added policy-- before the user has entered a key value
new_key : "__new_advanced_key__",
defaults: {
// the properties are whatever the user types in (in addition to whatever comes originally from the server)
},
// which keys to send as the deleted keys on next save
deleteKeys : [],
blacklistKeys : [], // an array which the controller should populate directly for now [static not instance based]
validate: function (attrs) {
var errors = {};
for (var key in attrs) {
if (key === this.new_key || _.isEmpty(key)) {
errors[key] = "A key must be entered.";
}
else if (_.contains(this.blacklistKeys, key)) {
errors[key] = key + " is a reserved keyword or can be edited on another screen";
}
}
if (!_.isEmpty(errors)) return errors;
// Keys can no longer be edited. We are currently not validating values.
},
save : function (attrs, options) {

View File

@@ -5,7 +5,7 @@
if (typeof window.templateLoader == 'function') return;
var templateLoader = {
templateVersion: "0.0.15",
templateVersion: "0.0.16",
templates: {},
loadRemoteTemplate: function(templateName, filename, callback) {
if (!this.templates[templateName]) {

View File

@@ -6,14 +6,6 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
// Model class is CMS.Models.Settings.Advanced
events : {
'click .delete-button' : "deleteEntry",
'click .new-button' : "addEntry",
// update model on changes
'change .policy-key' : "updateKey",
// keypress to catch alpha keys and backspace/delete on some browsers
'keypress .policy-key' : "showSaveCancelButtons",
// keyup to catch backspace/delete reliably
'keyup .policy-key' : "showSaveCancelButtons",
'focus :input' : "focusInput",
'blur :input' : "blurInput"
// TODO enable/disable save based on validation (currently enabled whenever there are changes)
@@ -95,16 +87,11 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
mirror.setValue(stringValue);
} catch(quotedE) {
// TODO: validation error
console.log("Error with JSON, even after converting to String.");
console.log(quotedE);
// console.log("Error with JSON, even after converting to String.");
// console.log(quotedE);
JSONValue = undefined;
}
}
else {
// TODO: validation error
console.log("Error with JSON, but will not convert to String.");
console.log(e);
}
}
if (JSONValue !== undefined) {
self.clearValidationErrors();
@@ -113,7 +100,6 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
}
});
},
showMessage: function (type) {
$(".wrapper-alert").removeClass("is-shown");
if (type) {
@@ -128,56 +114,19 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
else {
// This is the case of the page first rendering, or when Cancel is pressed.
this.hideSaveCancelButtons();
this.toggleNewButton(true);
}
},
showSaveCancelButtons: function(event) {
if (!this.buttonsVisible) {
if (event && (event.type === 'keypress' || event.type === 'keyup')) {
// check whether it's really an altering event: note, String.fromCharCode(keyCode) will
// give positive values for control/command/option-letter combos; so, don't use it
if (!((event.charCode && String.fromCharCode(event.charCode) !== "") ||
// 8 = backspace, 46 = delete
event.keyCode === 8 || event.keyCode === 46)) return;
}
this.$el.find(".message-status").removeClass("is-shown");
$('.wrapper-notification').removeClass('is-hiding').addClass('is-shown');
this.buttonsVisible = true;
}
},
hideSaveCancelButtons: function() {
$('.wrapper-notification').removeClass('is-shown').addClass('is-hiding');
this.buttonsVisible = false;
},
toggleNewButton: function (enable) {
var newButton = this.$el.find(".new-button");
if (enable) {
newButton.removeClass('disabled');
}
else {
newButton.addClass('disabled');
}
},
deleteEntry : function(event) {
event.preventDefault();
// find out which entry
var li$ = $(event.currentTarget).closest('li');
// Not data b/c the validation view uses it for a selector
var key = $('.key', li$).attr('id');
delete this.selectorToField[this.fieldToSelectorMap[key]];
delete this.fieldToSelectorMap[key];
if (key !== this.model.new_key) {
this.model.deleteKeys.push(key);
this.model.unset(key);
}
li$.remove();
this.showSaveCancelButtons();
},
saveView : function(event) {
// TODO one last verification scan:
// call validateKey on each to ensure proper format
@@ -201,102 +150,15 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
error : CMS.ServerError
});
},
addEntry : function() {
var listEle$ = this.$el.find('.course-advanced-policy-list');
var newEle = this.renderTemplate("", "");
listEle$.append(newEle);
// need to re-find b/c replaceWith seems to copy rather than use the specific ele instance
var policyValueDivs = this.$el.find('#' + this.model.new_key).closest('li').find('.json');
// only 1 but hey, let's take advantage of the context mechanism
_.each(policyValueDivs, this.attachJSONEditor, this);
this.toggleNewButton(false);
},
updateKey : function(event) {
var parentElement = $(event.currentTarget).closest('.key');
// old key: either the key as in the model or new_key.
// That is, it doesn't change as the val changes until val is accepted.
var oldKey = parentElement.attr('id');
// TODO: validation of keys with spaces. For now at least trim strings to remove initial and
// trailing whitespace
var newKey = $.trim($(event.currentTarget).val());
if (oldKey !== newKey) {
// TODO: is it OK to erase other validation messages?
this.clearValidationErrors();
if (!this.validateKey(oldKey, newKey)) return;
if (this.model.has(newKey)) {
var error = {};
error[oldKey] = 'You have already defined "' + newKey + '" in the manual policy definitions.';
error[newKey] = "You tried to enter a duplicate of this key.";
this.model.trigger("invalid", this.model, error);
return false;
}
// explicitly call validate to determine whether to proceed (relying on triggered error means putting continuation in the success
// method which is uglier I think?)
var newEntryModel = {};
// set the new key's value to the old one's
newEntryModel[newKey] = (oldKey === this.model.new_key ? '' : this.model.get(oldKey));
var validation = this.model.validate(newEntryModel);
if (validation) {
if (_.has(validation, newKey)) {
// swap to the key which the map knows about
validation[oldKey] = validation[newKey];
}
this.model.trigger("invalid", this.model, validation);
// abandon update
return;
}
// Now safe to actually do the update
this.model.set(newEntryModel);
// update maps
var selector = this.fieldToSelectorMap[oldKey];
this.selectorToField[selector] = newKey;
this.fieldToSelectorMap[newKey] = selector;
delete this.fieldToSelectorMap[oldKey];
if (oldKey !== this.model.new_key) {
// mark the old key for deletion and delete from field maps
this.model.deleteKeys.push(oldKey);
this.model.unset(oldKey) ;
}
else {
// id for the new entry will now be the key value. Enable new entry button.
this.toggleNewButton(true);
}
// check for newkey being the name of one which was previously deleted in this session
var wasDeleting = this.model.deleteKeys.indexOf(newKey);
if (wasDeleting >= 0) {
this.model.deleteKeys.splice(wasDeleting, 1);
}
// Update the ID to the new value.
parentElement.attr('id', newKey);
}
},
validateKey : function(oldKey, newKey) {
// model validation can't handle malformed keys nor notice if 2 fields have same key; so, need to add that chk here
// TODO ensure there's no spaces or illegal chars (note some checking for spaces currently done in model's
// validate method.
return true;
},
renderTemplate: function (key, value) {
var newKeyId = _.uniqueId('policy_key_'),
newEle = this.template({ key : key, value : JSON.stringify(value, null, 4),
keyUniqueId: newKeyId, valueUniqueId: _.uniqueId('policy_value_')});
this.fieldToSelectorMap[(_.isEmpty(key) ? this.model.new_key : key)] = newKeyId;
this.selectorToField[newKeyId] = (_.isEmpty(key) ? this.model.new_key : key);
this.fieldToSelectorMap[key] = newKeyId;
this.selectorToField[newKeyId] = key;
return newEle;
},
focusInput : function(event) {
$(event.target).prev().addClass("is-focused");
},

View File

@@ -472,6 +472,21 @@ textarea.text {
@include linear-gradient($paleYellow, tint($paleYellow, 90%));
outline: 0;
}
&[disabled] {
border-color: $gray-l4;
color: $gray-l2;
}
&[readonly] {
border-color: $gray-l4;
color: $gray-l1;
&:focus {
@include linear-gradient($lightGrey, tint($lightGrey, 90%));
outline: 0;
}
}
}
// forms - specific

View File

@@ -239,13 +239,9 @@ body.course.settings {
// not editable fields
.field.is-not-editable {
label, .label {
color: $gray-l3;
}
input {
opacity: 0.25;
& label.is-focused {
color: $gray-d2;
}
}

View File

@@ -6,12 +6,12 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>
<%block name="title"></%block> |
% if context_course:
<% ctx_loc = context_course.location %>
${context_course.display_name} |
% endif
edX Studio
<%block name="title"></%block> |
% if context_course:
<% ctx_loc = context_course.location %>
${context_course.display_name_with_default} |
% endif
edX Studio
</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
@@ -66,4 +66,4 @@
<%block name="jsextra"></%block>
</body>
</html>
</html>

View File

@@ -1,14 +0,0 @@
<%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>

View File

@@ -22,7 +22,7 @@
<article class="subsection-body window" data-id="${subsection.location}">
<div class="subsection-name-input">
<label>Display Name:</label>
<input type="text" value="${subsection.display_name | h}" class="subsection-display-name-input" data-metadata-name="display_name"/>
<input type="text" value="${subsection.display_name_with_default | h}" class="subsection-display-name-input" data-metadata-name="display_name"/>
</div>
<div class="sortable-unit-list">
<label>Units:</label>
@@ -40,7 +40,7 @@
<a href="#" class="save-button">Save</a>
<a href="#" class="cancel-button">Cancel</a>
<a href="#" class="delete-icon remove-policy-data"></a>
</li>
</li>
</div>
<div class="sidebar">
@@ -51,28 +51,28 @@
<label>Release date:<!-- <span class="description">Determines when this subsection and the units within it will be released publicly.</span>--></label>
<div class="datepair" data-language="javascript">
<%
start_date = datetime.fromtimestamp(mktime(subsection.start)) if subsection.start is not None else None
parent_start_date = datetime.fromtimestamp(mktime(parent_item.start)) if parent_item.start is not None else None
start_date = datetime.fromtimestamp(mktime(subsection.lms.start)) if subsection.lms.start is not None else None
parent_start_date = datetime.fromtimestamp(mktime(parent_item.lms.start)) if parent_item.lms.start is not None else None
%>
<input type="text" id="start_date" name="start_date" value="${start_date.strftime('%m/%d/%Y') if start_date is not None else ''}" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
<input type="text" id="start_time" name="start_time" value="${start_date.strftime('%H:%M') if start_date is not None else ''}" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
<input type="text" id="start_time" name="start_time" value="${start_date.strftime('%H:%M') if start_date is not None else ''}" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
</div>
% if subsection.start != parent_item.start and subsection.start:
% if subsection.lms.start != parent_item.lms.start and subsection.lms.start:
% if parent_start_date is None:
<p class="notice">The date above differs from the release date of ${parent_item.display_name}, which is unset.
<p class="notice">The date above differs from the release date of ${parent_item.display_name_with_default}, which is unset.
% else:
<p class="notice">The date above differs from the release date of ${parent_item.display_name} ${parent_start_date.strftime('%m/%d/%Y')} at ${parent_start_date.strftime('%H:%M')}.
<p class="notice">The date above differs from the release date of ${parent_item.display_name_with_default} ${parent_start_date.strftime('%m/%d/%Y')} at ${parent_start_date.strftime('%H:%M')}.
% endif
<a href="#" class="sync-date no-spinner">Sync to ${parent_item.display_name}.</a></p>
<a href="#" class="sync-date no-spinner">Sync to ${parent_item.display_name_with_default}.</a></p>
% endif
</div>
<div class="row gradable">
<label>Graded as:</label>
<div class="gradable-status" data-initial-status="${subsection.metadata.get('format', 'Not Graded')}">
<div class="gradable-status" data-initial-status="${subsection.lms.format if subsection.lms.format is not None else 'Not Graded'}">
</div>
<div class="due-date-input row">
<label>Due date:</label>
<a href="#" class="set-date">Set a due date</a>
@@ -80,9 +80,9 @@
<p class="date-description">
<%
# due date uses it own formatting for stringifying the date. As with capa_module.py, there's a utility module available for us to use
due_date = dateutil.parser.parse(subsection.metadata.get('due')) if 'due' in subsection.metadata else None
due_date = dateutil.parser.parse(subsection.lms.due) if subsection.lms.due else None
%>
<input type="text" id="due_date" name="due_date" value="${due_date.strftime('%m/%d/%Y') if due_date is not None else ''}" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
<input type="text" id="due_date" name="due_date" value="${due_date.strftime('%m/%d/%Y') if due_date is not None else ''}" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
<input type="text" id="due_time" name="due_time" value="${due_date.strftime('%H:%M') if due_date is not None else ''}" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
<a href="#" class="remove-date">Remove due date</a>
</p>
@@ -110,7 +110,7 @@
<script type="text/javascript" src="${static.url('js/views/grader-select-view.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/settings/course_grading_policy.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/overview.js')}"></script>
<script type="text/javascript">
$(document).ready(function() {
@@ -128,7 +128,7 @@
window.graderTypes.course_location = new CMS.Models.Location('${parent_location}');
window.graderTypes.reset(${course_graders|n});
}
$(".gradable-status").each(function(index, ele) {
var gradeView = new CMS.Views.OverviewAssignmentGrader({
el : ele,

View File

@@ -21,14 +21,14 @@
<div class="description">
<p><strong>Importing a new course will delete all content currently associated with your course
and replace it with the contents of the uploaded file.</strong></p>
<p>File uploads must be gzipped tar files (.tar.gz or .tgz) containing, at a minimum, a <code>course.xml</code> file.</p>
<p>File uploads must be gzipped tar files (.tar.gz) containing, at a minimum, a <code>course.xml</code> file.</p>
<p>Please note that if your course has any problems with auto-generated <code>url_name</code> nodes,
re-importing your course could cause the loss of student data associated with those problems.</p>
</div>
<form action="${reverse('import_course', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}" method="post" enctype="multipart/form-data" class="import-form">
<h2>Course to import:</h2>
<p class="error-block"></p>
<a href="#" class="choose-file-button">Choose File</a>
<a href="#" class="choose-file-button">Choose File</a>
<p class="file-name-block"><span class="file-name"></span><a href="#" class="choose-file-button-inline">change</a></p>
<input type="file" name="course-data" class="file-input">
<input type="submit" value="Replace my course with the one above" class="submit-button">
@@ -45,13 +45,13 @@
<%block name="jsextra">
<script>
(function() {
var bar = $('.progress-bar');
var fill = $('.progress-fill');
var percent = $('.percent');
var status = $('#status');
var submitBtn = $('.submit-button');
$('form').ajaxForm({
beforeSend: function() {
status.empty();
@@ -76,7 +76,7 @@ $('form').ajaxForm({
submitBtn.show();
bar.hide();
}
});
})();
});
})();
</script>
</%block>

View File

@@ -8,7 +8,7 @@
<div>${module_type}</div>
<div>
% for template in module_templates:
<a class="save" data-template-id="${template.location.url()}">${template.display_name}</a>
<a class="save" data-template-id="${template.location.url()}">${template.display_name_with_default}</a>
% endfor
</div>
</div>

View File

@@ -32,7 +32,7 @@
window.graderTypes.course_location = new CMS.Models.Location('${parent_location}');
window.graderTypes.reset(${course_graders|n});
}
$(".gradable-status").each(function(index, ele) {
var gradeView = new CMS.Views.OverviewAssignmentGrader({
el : ele,
@@ -40,7 +40,7 @@
});
});
});
</script>
</%block>
@@ -146,7 +146,7 @@
<div class="main-wrapper">
<div class="inner-wrapper">
<article class="courseware-overview" data-course-id="${context_course.location.url()}">
<article class="courseware-overview" data-course-id="${context_course.location.url()}">
% for section in sections:
<section class="courseware-section branch" data-id="${section.location}">
<header>
@@ -154,16 +154,16 @@
<div class="item-details" data-id="${section.location}">
<h3 class="section-name">
<span data-tooltip="Edit this section's name" class="section-name-span">${section.display_name}</span>
<span data-tooltip="Edit this section's name" class="section-name-span">${section.display_name_with_default}</span>
<form class="section-name-edit" style="display:none">
<input type="text" value="${section.display_name | h}" class="edit-section-name" autocomplete="off"/>
<input type="text" value="${section.display_name_with_default | h}" class="edit-section-name" autocomplete="off"/>
<input type="submit" class="save-button edit-section-name-save" value="Save" />
<input type="button" class="cancel-button edit-section-name-cancel" value="Cancel" />
</form>
</h3>
<div class="section-published-date">
<%
start_date = datetime.fromtimestamp(mktime(section.start)) if section.start is not None else None
start_date = datetime.fromtimestamp(mktime(section.lms.start)) if section.lms.start is not None else None
start_date_str = start_date.strftime('%m/%d/%Y') if start_date is not None else ''
start_time_str = start_date.strftime('%H:%M') if start_date is not None else ''
%>
@@ -174,9 +174,9 @@
<span class="published-status"><strong>Will Release:</strong> ${start_date_str} at ${start_time_str}</span>
<a href="#" class="edit-button" data-date="${start_date_str}" data-time="${start_time_str}" data-id="${section.location}">Edit</a>
%endif
</div>
</div>
</div>
<div class="item-actions">
<a href="#" data-tooltip="Delete this section" class="delete-button delete-section-button"><span class="delete-icon"></span></a>
<a href="#" data-tooltip="Drag to reorder" class="drag-handle"></a>
@@ -196,15 +196,15 @@
<a href="#" data-tooltip="Expand/collapse this subsection" class="expand-collapse-icon expand"></a>
<a href="${reverse('edit_subsection', args=[subsection.location])}">
<span class="folder-icon"></span>
<span class="subsection-name"><span class="subsection-name-value">${subsection.display_name}</span></span>
<span class="subsection-name"><span class="subsection-name-value">${subsection.display_name_with_default}</span></span>
</a>
</div>
<div class="gradable-status" data-initial-status="${subsection.metadata.get('format', 'Not Graded')}">
<div class="gradable-status" data-initial-status="${subsection.lms.format if section.lms.format is not None else 'Not Graded'}">
</div>
<div class="item-actions">
<a href="#" data-tooltip="Delete this subsection" class="delete-button delete-subsection-button"><span class="delete-icon"></span></a>
<a href="#" data-tooltip="Delete this subsection" class="delete-button delete-subsection-button"><span class="delete-icon"></span></a>
<a href="#" data-tooltip="Drag to reorder" class="drag-handle"></a>
</div>
</div>

View File

@@ -70,17 +70,17 @@ from contentstore import utils
<ol class="list-input">
<li class="field text is-not-editable" id="field-course-organization">
<label for="course-organization">Organization</label>
<input type="text" class="long" id="course-organization" value="[Course Organization]" disabled="disabled" />
<input title="This field is disabled: this information cannot be changed." type="text" class="long" id="course-organization" value="[Course Organization]" readonly />
</li>
<li class="field text is-not-editable" id="field-course-number">
<label for="course-number">Course Number</label>
<input type="text" class="short" id="course-number" value="[Course No.]" disabled="disabled">
<input title="This field is disabled: this information cannot be changed." type="text" class="short" id="course-number" value="[Course No.]" readonly>
</li>
<li class="field text is-not-editable" id="field-course-name">
<label for="course-name">Course Name</label>
<input type="text" class="long" id="course-name" value="[Course Name]" disabled="disabled" />
<input title="This field is disabled: this information cannot be changed." type="text" class="long" id="course-name" value="[Course Name]" readonly />
</li>
</ol>
<span class="tip tip-stacked">These are used in <a rel="external" href="${utils.get_lms_link_for_about_page(course_location)}" />your course URL</a>, and cannot be changed</span>

View File

@@ -21,7 +21,6 @@ $(document).ready(function () {
// proactively populate advanced b/c it has the filtered list and doesn't really follow the model pattern
var advancedModel = new CMS.Models.Settings.Advanced(${advanced_dict | n}, {parse: true});
advancedModel.blacklistKeys = ${advanced_blacklist | n};
advancedModel.url = "${reverse('course_advanced_settings_updates', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}";
var editor = new CMS.Views.Settings.Advanced({
@@ -61,18 +60,11 @@ editor.render();
<span class="tip">Manually Edit Course Policy Values (JSON Key / Value pairs)</span>
</header>
<p class="instructions"><strong>Warning</strong>: Add only manual policy data that you are familiar
with.</p>
<p class="instructions"><strong>Warning</strong>: Do not modify these policies unless you are familiar with their purpose.</p>
<ul class="list-input course-advanced-policy-list enum">
</ul>
<div class="actions">
<a href="#" class="button new-button new-advanced-policy-item add-policy-data">
<span class="plus-icon white"></span>New Manual Policy
</a>
</div>
</section>
</form>
</article>
@@ -80,9 +72,9 @@ editor.render();
<aside class="content-supplementary" role="complimentary">
<div class="bit">
<h3 class="title-3">How will these settings be used?</h3>
<p>Manual policies are JSON-based key and value pairs that allow you add additional settings which edX Studio will use when generating your course.</p>
<p>Manual policies are JSON-based key and value pairs that give you control over specific course settings that edX Studio will use when displaying and running your course.</p>
<p>Any policies you define here will override any other information you've defined elsewhere in Studio. With this in mind, please be very careful and do not add policies that you are unfamiliar with (both their purpose and their syntax).</p>
<p>Any policies you modify here will override any other information you've defined elsewhere in Studio. With this in mind, please be very careful and do not edit policies that you are unfamiliar with (both their purpose and their syntax).</p>
</div>
<div class="bit">

View File

@@ -43,12 +43,12 @@
</div>
<div class="main-column">
<article class="unit-body window">
<p class="unit-name-input"><label>Display Name:</label><input type="text" value="${unit.display_name | h}" class="unit-display-name-input" /></p>
<p class="unit-name-input"><label>Display Name:</label><input type="text" value="${unit.display_name_with_default | h}" class="unit-display-name-input" /></p>
<ol class="components">
% for id in components:
<li class="component" data-id="${id}"/>
% endfor
<li class="new-component-item adding">
<li class="new-component-item adding">
<div class="new-component">
<h5>Add New Component</h5>
<ul class="new-component-type">
@@ -85,7 +85,7 @@
<span class="name"> ${name}</span>
</a>
</li>
% else:
<li class="editor-md">
<a href="#" data-location="${location}">
@@ -94,7 +94,7 @@
</li>
% endif
% endif
%endfor
</ul>
</div>
@@ -102,20 +102,20 @@
<div class="tab" id="tab2">
<ul class="new-component-template">
% for name, location, has_markdown, is_empty in templates:
% if not has_markdown:
% if not has_markdown:
% if is_empty:
<li class="editor-manual empty">
<a href="#" data-location="${location}">
<span class="name">${name}</span>
</a>
</li>
% else:
<li class="editor-manual">
<a href="#" data-location="${location}">
<span class="name"> ${name}</span>
</a>
</li>
% endif
% endif
@@ -146,13 +146,13 @@
<div class="row published-alert">
<p class="edit-draft-message">This unit has been published. To make changes, you must <a href="#" class="create-draft">edit a draft</a>.</p>
<p class="publish-draft-message">This is a draft of the published unit. To update the live version, you must <a href="#" class="publish-draft">replace it with this draft</a>.</p>
</div>
</div>
<div class="row status">
<p>This unit is scheduled to be released to <strong>students</strong>
<p>This unit is scheduled to be released to <strong>students</strong>
% if release_date is not None:
on <strong>${release_date}</strong>
% endif
with the subsection <a href="${reverse('edit_subsection', kwargs={'location': subsection.location})}">"${subsection.display_name}"</a></p>
% endif
with the subsection <a href="${reverse('edit_subsection', kwargs={'location': subsection.location})}">"${subsection.display_name_with_default}"</a></p>
</div>
<div class="row unit-actions">
<a href="#" class="delete-draft delete-button">Delete Draft</a>
@@ -167,18 +167,18 @@
<div><input type="text" class="url" value="/courseware/${section.url_name}/${subsection.url_name}" disabled /></div>
<ol>
<li>
<a href="#" class="section-item">${section.display_name}</a>
<a href="#" class="section-item">${section.display_name_with_default}</a>
<ol>
<li>
<a href="${reverse('edit_subsection', args=[subsection.location])}" class="section-item">
<span class="folder-icon"></span>
<span class="subsection-name"><span class="subsection-name-value">${subsection.display_name}</span></span>
<span class="subsection-name"><span class="subsection-name-value">${subsection.display_name_with_default}</span></span>
</a>
${units.enum_units(subsection, actions=False, selected=unit.location)}
</li>
</ol>
</li>
</ol>
</ol>
</div>
</div>
</div>

View File

@@ -5,24 +5,24 @@
<div class="wrapper wrapper-left ">
<h1 class="branding"><a href="/">edX Studio</a></h1>
% if context_course:
<% ctx_loc = context_course.location %>
<div class="info-course">
<h2 class="sr">Current Course:</h2>
<a href="${reverse('course_index', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">
<span class="course-org">${ctx_loc.org}</span><span class="course-number">${ctx_loc.course}</span>
<span class="course-title" title="${context_course.display_name}">${context_course.display_name}</span>
<span class="course-title" title="${context_course.display_name_with_default}">${context_course.display_name_with_default}</span>
</a>
</div>
<nav class="nav-course primary nav-dropdown" role="navigation">
<h2 class="sr">${context_course.display_name}'s Navigation:</h2>
<h2 class="sr">${context_course.display_name_with_default}'s Navigation:</h2>
<ol>
<li class="nav-item nav-course-courseware">
<h3 class="title"><span class="label-prefix">Course </span>Content <i class="ss-icon ss-symbolicons-block icon-expand">&#x25BE;</i></h3>
<div class="wrapper wrapper-nav-sub">
<div class="nav-sub">
<ul>
@@ -32,12 +32,12 @@
<li class="nav-item nav-course-courseware-uploads"><a href="${reverse('asset_index', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">Files &amp; Uploads</a></li>
</ul>
</div>
</div>
</div>
</li>
<li class="nav-item nav-course-settings">
<h3 class="title"><span class="label-prefix">Course </span>Settings <i class="ss-icon ss-symbolicons-block icon-expand">&#x25BE;</i></h3>
<div class="wrapper wrapper-nav-sub">
<div class="nav-sub">
<ul>
@@ -47,12 +47,12 @@
<li class="nav-item nav-course-settings-advanced"><a href="${reverse('course_advanced_settings', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">Advanced Settings</a></li>
</ul>
</div>
</div>
</div>
</li>
<li class="nav-item nav-course-tools">
<h3 class="title">Tools <i class="ss-icon ss-symbolicons-block icon-expand">&#x25BE;</i></h3>
<div class="wrapper wrapper-nav-sub">
<div class="nav-sub">
<ul>
@@ -66,16 +66,16 @@
</nav>
% endif
</div>
<div class="wrapper wrapper-right">
% if user.is_authenticated():
% if user.is_authenticated():
<nav class="nav-account nav-is-signedin nav-dropdown">
<h2 class="sr">Currently logged in as:</h2>
<ol>
<li class="nav-item nav-account-username">
<a href="#" class="title">
<span class="account-username">
<i class="ss-icon ss-symbolicons-standard icon-user">&#x1F464;</i>
<i class="ss-icon ss-symbolicons-standard icon-user">&#x1F464;</i>
${ user.username }
</span>
<i class="ss-icon ss-symbolicons-block icon-expand">&#x25BE;</i>
@@ -111,7 +111,7 @@
</li>
</ol>
</nav>
% endif
% endif
</div>
</header>
</div>
</div>

View File

@@ -1,18 +1,17 @@
% if metadata:
<%
import hashlib
hlskey = hashlib.md5(module.location.url()).hexdigest()
%>
<section class="metadata_edit">
<ul>
% for keyname in editable_metadata_fields:
% for field_name, field_value in editable_metadata_fields.items():
<li>
% if keyname=='source_code':
<a href="#hls-modal-${hlskey}" style="color:yellow;" id="hls-trig-${hlskey}" >Edit High Level Source</a>
% else:
<label>${keyname}:</label>
<input type='text' data-metadata-name='${keyname}' value='${metadata[keyname]}' size='60' />
% endif
% if field_name == 'source_code':
<a href="#hls-modal-${hlskey}" style="color:yellow;" id="hls-trig-${hlskey}" >Edit High Level Source</a>
% else:
<label>${field_name}:</label>
<input type='text' data-metadata-name='${field_name}' value='${field_value}' size='60' />
% endif
</li>
% endfor
</ul>
@@ -22,4 +21,3 @@
% endif
</section>
% endif

View File

@@ -1,101 +0,0 @@
<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>

View File

@@ -40,7 +40,7 @@
<a href="#" class="module-edit"
data-id="${child.location.url()}"
data-type="${child.js_module_name}"
data-preview-type="${child.module_class.js_module_name}">${child.display_name}</a>
data-preview-type="${child.module_class.js_module_name}">${child.display_name_with_default}</a>
<a href="#" class="draggable">handle</a>
</li>
%endfor

View File

@@ -22,7 +22,7 @@ This def will enumerate through a passed in subsection and list all of the units
<div class="section-item ${selected_class}">
<a href="${reverse('edit_unit', args=[unit.location])}" class="${unit_state}-item">
<span class="${unit.category}-icon"></span>
<span class="unit-name">${unit.display_name}</span>
<span class="unit-name">${unit.display_name_with_default}</span>
</a>
% if actions:
<div class="item-actions">
@@ -39,7 +39,7 @@ This def will enumerate through a passed in subsection and list all of the units
</a>
</li>
</ol>
</%def>
</%def>

30
cms/xmodule_namespace.py Normal file
View File

@@ -0,0 +1,30 @@
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)