studio - alerts: resolving local master merge conflcits
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
@@ -7,7 +7,7 @@ from xmodule.modulestore.django import modulestore
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from prompt import query_yes_no
|
||||
from .prompt import query_yes_no
|
||||
|
||||
from auth.authz import _delete_course_group
|
||||
|
||||
|
||||
@@ -15,10 +15,10 @@ def get_module_info(store, location, parent_location=None, rewrite_static_links=
|
||||
template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty'])
|
||||
module = store.clone_item(template_location, location)
|
||||
|
||||
data = module.definition['data']
|
||||
data = module.data
|
||||
if rewrite_static_links:
|
||||
data = replace_static_urls(
|
||||
module.definition['data'],
|
||||
module.data,
|
||||
None,
|
||||
course_namespace=Location([
|
||||
module.location.tag,
|
||||
@@ -32,7 +32,8 @@ def get_module_info(store, location, parent_location=None, rewrite_static_links=
|
||||
return {
|
||||
'id': module.location.url(),
|
||||
'data': data,
|
||||
'metadata': module.metadata
|
||||
# TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
|
||||
'metadata': module._model_data._kvs._metadata
|
||||
}
|
||||
|
||||
|
||||
@@ -70,23 +71,23 @@ def set_module_info(store, location, post_data):
|
||||
# 'apply' the submitted metadata, so we don't end up deleting system metadata
|
||||
if post_data.get('metadata') is not None:
|
||||
posted_metadata = post_data['metadata']
|
||||
|
||||
|
||||
# update existing metadata with submitted metadata (which can be partial)
|
||||
# IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
|
||||
for metadata_key in posted_metadata.keys():
|
||||
|
||||
for metadata_key, value in posted_metadata.items():
|
||||
|
||||
# let's strip out any metadata fields from the postback which have been identified as system metadata
|
||||
# and therefore should not be user-editable, so we should accept them back from the client
|
||||
if metadata_key in module.system_metadata_fields:
|
||||
del posted_metadata[metadata_key]
|
||||
elif posted_metadata[metadata_key] is None:
|
||||
# remove both from passed in collection as well as the collection read in from the modulestore
|
||||
if metadata_key in module.metadata:
|
||||
del module.metadata[metadata_key]
|
||||
if metadata_key in module._model_data:
|
||||
del module._model_data[metadata_key]
|
||||
del posted_metadata[metadata_key]
|
||||
|
||||
# overlay the new metadata over the modulestore sourced collection to support partial updates
|
||||
module.metadata.update(posted_metadata)
|
||||
|
||||
else:
|
||||
module._model_data[metadata_key] = value
|
||||
|
||||
# commit to datastore
|
||||
store.update_metadata(location, module.metadata)
|
||||
# TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
|
||||
store.update_metadata(location, module._model_data._kvs._metadata)
|
||||
|
||||
@@ -6,15 +6,16 @@ from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from path import path
|
||||
from tempdir import mkdtemp_clean
|
||||
from datetime import timedelta
|
||||
import json
|
||||
from fs.osfs import OSFS
|
||||
import copy
|
||||
from json import loads
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from cms.djangoapps.contentstore.utils import get_modulestore
|
||||
from contentstore.utils import get_modulestore
|
||||
|
||||
from utils import ModuleStoreTestCase, parse_json
|
||||
from .utils import ModuleStoreTestCase, parse_json
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
@@ -25,6 +26,7 @@ from xmodule.contentstore.django import contentstore
|
||||
from xmodule.templates import update_templates
|
||||
from xmodule.modulestore.xml_exporter import export_to_xml
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
from xmodule.modulestore.inheritance import own_metadata
|
||||
|
||||
from xmodule.capa_module import CapaDescriptor
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
@@ -109,10 +111,10 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter','Week_1', None]))
|
||||
|
||||
# make sure the parent no longer points to the child object which was deleted
|
||||
self.assertTrue(sequential.location.url() in chapter.definition['children'])
|
||||
self.assertTrue(sequential.location.url() in chapter.children)
|
||||
|
||||
self.client.post(reverse('delete_item'),
|
||||
json.dumps({'id': sequential.location.url(), 'delete_children':'true'}),
|
||||
self.client.post(reverse('delete_item'),
|
||||
json.dumps({'id': sequential.location.url(), 'delete_children': 'true', 'delete_all_versions': 'true'}),
|
||||
"application/json")
|
||||
|
||||
found = False
|
||||
@@ -127,9 +129,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter','Week_1', None]))
|
||||
|
||||
# make sure the parent no longer points to the child object which was deleted
|
||||
self.assertFalse(sequential.location.url() in chapter.definition['children'])
|
||||
self.assertFalse(sequential.location.url() in chapter.children)
|
||||
|
||||
|
||||
|
||||
|
||||
def test_about_overrides(self):
|
||||
'''
|
||||
@@ -139,11 +141,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
module_store = modulestore('direct')
|
||||
effort = module_store.get_item(Location(['i4x', 'edX', 'full', 'about', 'effort', None]))
|
||||
self.assertEqual(effort.definition['data'], '6 hours')
|
||||
self.assertEqual(effort.data, '6 hours')
|
||||
|
||||
# this one should be in a non-override folder
|
||||
effort = module_store.get_item(Location(['i4x', 'edX', 'full', 'about', 'end_date', None]))
|
||||
self.assertEqual(effort.definition['data'], 'TBD')
|
||||
self.assertEqual(effort.data, 'TBD')
|
||||
|
||||
def test_remove_hide_progress_tab(self):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
@@ -153,7 +155,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
|
||||
source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
|
||||
course = module_store.get_item(source_location)
|
||||
self.assertNotIn('hide_progress_tab', course.metadata)
|
||||
self.assertFalse(course.hide_progress_tab)
|
||||
|
||||
def test_clone_course(self):
|
||||
|
||||
@@ -246,7 +248,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
# compare what's on disk compared to what we have in our course
|
||||
with fs.open('grading_policy.json', 'r') as grading_policy:
|
||||
on_disk = loads(grading_policy.read())
|
||||
self.assertEqual(on_disk, course.definition['data']['grading_policy'])
|
||||
self.assertEqual(on_disk, course.grading_policy)
|
||||
|
||||
#check for policy.json
|
||||
self.assertTrue(fs.exists('policy.json'))
|
||||
@@ -255,7 +257,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
with fs.open('policy.json', 'r') as course_policy:
|
||||
on_disk = loads(course_policy.read())
|
||||
self.assertIn('course/6.002_Spring_2012', on_disk)
|
||||
self.assertEqual(on_disk['course/6.002_Spring_2012'], course.metadata)
|
||||
self.assertEqual(on_disk['course/6.002_Spring_2012'], own_metadata(course))
|
||||
|
||||
# remove old course
|
||||
delete_course(module_store, content_store, location)
|
||||
@@ -302,10 +304,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
|
||||
course = module_store.get_item(location)
|
||||
|
||||
metadata = own_metadata(course)
|
||||
# add a bool piece of unknown metadata so we can verify we don't throw an exception
|
||||
course.metadata['new_metadata'] = True
|
||||
metadata['new_metadata'] = True
|
||||
|
||||
module_store.update_metadata(location, course.metadata)
|
||||
module_store.update_metadata(location, metadata)
|
||||
|
||||
print 'Exporting to tempdir = {0}'.format(root_dir)
|
||||
|
||||
@@ -473,21 +476,20 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
self.assertIsInstance(problem, CapaDescriptor, "New problem is not a CapaDescriptor")
|
||||
context = problem.get_context()
|
||||
self.assertIn('markdown', context, "markdown is missing from context")
|
||||
self.assertIn('markdown', problem.metadata, "markdown is missing from metadata")
|
||||
self.assertNotIn('markdown', problem.editable_metadata_fields, "Markdown slipped into the editable metadata fields")
|
||||
|
||||
def test_import_metadata_with_attempts_empty_string(self):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['simple'])
|
||||
module_store = modulestore('direct')
|
||||
did_load_item = False
|
||||
try:
|
||||
try:
|
||||
module_store.get_item(Location(['i4x', 'edX', 'simple', 'problem', 'ps01-simple', None]))
|
||||
did_load_item = True
|
||||
except ItemNotFoundError:
|
||||
pass
|
||||
|
||||
# make sure we found the item (e.g. it didn't error while loading)
|
||||
self.assertTrue(did_load_item)
|
||||
self.assertTrue(did_load_item)
|
||||
|
||||
def test_metadata_inheritance(self):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
@@ -499,8 +501,7 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
|
||||
# let's assert on the metadata_inheritance on an existing vertical
|
||||
for vertical in verticals:
|
||||
self.assertIn('xqa_key', vertical.metadata)
|
||||
self.assertEqual(course.metadata['xqa_key'], vertical.metadata['xqa_key'])
|
||||
self.assertEqual(course.lms.xqa_key, vertical.lms.xqa_key)
|
||||
|
||||
self.assertGreater(len(verticals), 0)
|
||||
|
||||
@@ -510,36 +511,33 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
# crate a new module and add it as a child to a vertical
|
||||
module_store.clone_item(source_template_location, new_component_location)
|
||||
parent = verticals[0]
|
||||
module_store.update_children(parent.location, parent.definition.get('children', []) + [new_component_location.url()])
|
||||
module_store.update_children(parent.location, parent.children + [new_component_location.url()])
|
||||
|
||||
# flush the cache
|
||||
module_store.get_cached_metadata_inheritance_tree(new_component_location, -1)
|
||||
new_module = module_store.get_item(new_component_location)
|
||||
|
||||
# check for grace period definition which should be defined at the course level
|
||||
self.assertIn('graceperiod', new_module.metadata)
|
||||
self.assertEqual(parent.lms.graceperiod, new_module.lms.graceperiod)
|
||||
|
||||
self.assertEqual(parent.metadata['graceperiod'], new_module.metadata['graceperiod'])
|
||||
|
||||
self.assertEqual(course.metadata['xqa_key'], new_module.metadata['xqa_key'])
|
||||
self.assertEqual(course.lms.xqa_key, new_module.lms.xqa_key)
|
||||
|
||||
#
|
||||
# now let's define an override at the leaf node level
|
||||
#
|
||||
new_module.metadata['graceperiod'] = '1 day'
|
||||
module_store.update_metadata(new_module.location, new_module.metadata)
|
||||
new_module.lms.graceperiod = timedelta(1)
|
||||
module_store.update_metadata(new_module.location, own_metadata(new_module))
|
||||
|
||||
# flush the cache and refetch
|
||||
module_store.get_cached_metadata_inheritance_tree(new_component_location, -1)
|
||||
new_module = module_store.get_item(new_component_location)
|
||||
|
||||
self.assertIn('graceperiod', new_module.metadata)
|
||||
self.assertEqual('1 day', new_module.metadata['graceperiod'])
|
||||
self.assertEqual(timedelta(1), new_module.lms.graceperiod)
|
||||
|
||||
|
||||
class TemplateTestCase(ModuleStoreTestCase):
|
||||
|
||||
def test_template_cleanup(self):
|
||||
def test_template_cleanup(self):
|
||||
module_store = modulestore('direct')
|
||||
|
||||
# insert a bogus template in the store
|
||||
@@ -562,4 +560,3 @@ class TemplateTestCase(ModuleStoreTestCase):
|
||||
asserted = True
|
||||
|
||||
self.assertTrue(asserted)
|
||||
|
||||
|
||||
@@ -10,16 +10,16 @@ from django.core.urlresolvers import reverse
|
||||
from django.utils.timezone import UTC
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
from cms.djangoapps.models.settings.course_details import (CourseDetails,
|
||||
from models.settings.course_details import (CourseDetails,
|
||||
CourseSettingsEncoder)
|
||||
from cms.djangoapps.models.settings.course_grading import CourseGradingModel
|
||||
from cms.djangoapps.contentstore.utils import get_modulestore
|
||||
from models.settings.course_grading import CourseGradingModel
|
||||
from contentstore.utils import get_modulestore
|
||||
|
||||
from django.test import TestCase
|
||||
from utils import ModuleStoreTestCase
|
||||
from .utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
from cms.djangoapps.models.settings.course_metadata import CourseMetadata
|
||||
from models.settings.course_metadata import CourseMetadata
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
@@ -246,8 +246,9 @@ class CourseGradingTest(CourseTestCase):
|
||||
altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
|
||||
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "cutoff add D")
|
||||
|
||||
test_grader.grace_period = {'hours' : 4, 'minutes' : 5, 'seconds': 0}
|
||||
test_grader.grace_period = {'hours': 4, 'minutes': 5, 'seconds': 0}
|
||||
altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
|
||||
print test_grader.grace_period, altered_grader.grace_period
|
||||
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "4 hour grace period")
|
||||
|
||||
def test_update_grader_from_json(self):
|
||||
@@ -286,31 +287,31 @@ class CourseMetadataEditingTest(CourseTestCase):
|
||||
|
||||
def test_update_from_json(self):
|
||||
test_model = CourseMetadata.update_from_json(self.course_location,
|
||||
{ "a" : 1,
|
||||
"b_a_c_h" : { "c" : "test" },
|
||||
"test_text" : "a text string"})
|
||||
{ "advertised_start" : "start A",
|
||||
"testcenter_info" : { "c" : "test" },
|
||||
"days_early_for_beta" : 2})
|
||||
self.update_check(test_model)
|
||||
# try fresh fetch to ensure persistence
|
||||
test_model = CourseMetadata.fetch(self.course_location)
|
||||
self.update_check(test_model)
|
||||
# now change some of the existing metadata
|
||||
test_model = CourseMetadata.update_from_json(self.course_location,
|
||||
{ "a" : 2,
|
||||
{ "advertised_start" : "start B",
|
||||
"display_name" : "jolly roger"})
|
||||
self.assertIn('display_name', test_model, 'Missing editable metadata field')
|
||||
self.assertEqual(test_model['display_name'], 'jolly roger', "not expected value")
|
||||
self.assertIn('a', test_model, 'Missing revised a metadata field')
|
||||
self.assertEqual(test_model['a'], 2, "a not expected value")
|
||||
self.assertIn('advertised_start', test_model, 'Missing revised advertised_start metadata field')
|
||||
self.assertEqual(test_model['advertised_start'], 'start B', "advertised_start not expected value")
|
||||
|
||||
def update_check(self, test_model):
|
||||
self.assertIn('display_name', test_model, 'Missing editable metadata field')
|
||||
self.assertEqual(test_model['display_name'], 'Robot Super Course', "not expected value")
|
||||
self.assertIn('a', test_model, 'Missing new a metadata field')
|
||||
self.assertEqual(test_model['a'], 1, "a not expected value")
|
||||
self.assertIn('b_a_c_h', test_model, 'Missing b_a_c_h metadata field')
|
||||
self.assertDictEqual(test_model['b_a_c_h'], { "c" : "test" }, "b_a_c_h not expected value")
|
||||
self.assertIn('test_text', test_model, 'Missing test_text metadata field')
|
||||
self.assertEqual(test_model['test_text'], "a text string", "test_text not expected value")
|
||||
self.assertIn('advertised_start', test_model, 'Missing new advertised_start metadata field')
|
||||
self.assertEqual(test_model['advertised_start'], 'start A', "advertised_start not expected value")
|
||||
self.assertIn('testcenter_info', test_model, 'Missing testcenter_info metadata field')
|
||||
self.assertDictEqual(test_model['testcenter_info'], { "c" : "test" }, "testcenter_info not expected value")
|
||||
self.assertIn('days_early_for_beta', test_model, 'Missing days_early_for_beta metadata field')
|
||||
self.assertEqual(test_model['days_early_for_beta'], 2, "days_early_for_beta not expected value")
|
||||
|
||||
|
||||
def test_delete_key(self):
|
||||
@@ -321,5 +322,5 @@ class CourseMetadataEditingTest(CourseTestCase):
|
||||
self.assertEqual(test_model['display_name'], 'Testing', "not expected value")
|
||||
self.assertIn('rerandomize', test_model, 'Missing rerandomize metadata field')
|
||||
# check for deletion effectiveness
|
||||
self.assertNotIn('showanswer', test_model, 'showanswer field still in')
|
||||
self.assertNotIn('xqa_key', test_model, 'xqa_key field still in')
|
||||
self.assertEqual('closed', test_model['showanswer'], 'showanswer field still in')
|
||||
self.assertEqual(None, test_model['xqa_key'], 'xqa_key field still in')
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from cms.djangoapps.contentstore import utils
|
||||
from contentstore import utils
|
||||
import mock
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import json
|
||||
from fs.osfs import OSFS
|
||||
import copy
|
||||
|
||||
from cms.djangoapps.contentstore.utils import get_modulestore
|
||||
from contentstore.utils import get_modulestore
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.store_utilities import clone_course
|
||||
@@ -24,7 +24,7 @@ from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.seq_module import SequenceDescriptor
|
||||
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
from utils import ModuleStoreTestCase, parse_json, user, registration
|
||||
from .utils import ModuleStoreTestCase, parse_json, user, registration
|
||||
|
||||
|
||||
class ContentStoreTestCase(ModuleStoreTestCase):
|
||||
|
||||
@@ -39,10 +39,10 @@ def get_course_location_for_item(location):
|
||||
# make sure we found exactly one match on this above course search
|
||||
found_cnt = len(courses)
|
||||
if found_cnt == 0:
|
||||
raise BaseException('Could not find course at {0}'.format(course_search_location))
|
||||
raise Exception('Could not find course at {0}'.format(course_search_location))
|
||||
|
||||
if found_cnt > 1:
|
||||
raise BaseException('Found more than one course at {0}. There should only be one!!! Dump = {1}'.format(course_search_location, courses))
|
||||
raise Exception('Found more than one course at {0}. There should only be one!!! Dump = {1}'.format(course_search_location, courses))
|
||||
|
||||
location = courses[0].location
|
||||
|
||||
@@ -136,7 +136,7 @@ def compute_unit_state(unit):
|
||||
'private' content is editabled and not visible in the LMS
|
||||
"""
|
||||
|
||||
if unit.metadata.get('is_draft', False):
|
||||
if unit.cms.is_draft:
|
||||
try:
|
||||
modulestore('direct').get_item(unit.location)
|
||||
return UnitState.draft
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -96,6 +96,13 @@ CACHES = {
|
||||
'KEY_PREFIX': 'general',
|
||||
'VERSION': 4,
|
||||
'KEY_FUNCTION': 'util.memcache.safe_key',
|
||||
},
|
||||
|
||||
'mongo_metadata_inheritance': {
|
||||
'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
|
||||
'LOCATION': '/var/tmp/mongo_metadata_inheritance',
|
||||
'TIMEOUT': 300,
|
||||
'KEY_FUNCTION': 'util.memcache.safe_key',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -98,6 +98,13 @@ CACHES = {
|
||||
'KEY_PREFIX': 'general',
|
||||
'VERSION': 4,
|
||||
'KEY_FUNCTION': 'util.memcache.safe_key',
|
||||
},
|
||||
|
||||
'mongo_metadata_inheritance': {
|
||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||
'LOCATION': '/var/tmp/mongo_metadata_inheritance',
|
||||
'TIMEOUT': 300,
|
||||
'KEY_FUNCTION': 'util.memcache.safe_key',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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) {
|
||||
|
||||
@@ -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]) {
|
||||
|
||||
@@ -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");
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -22,7 +22,7 @@
|
||||
<article class="subsection-body window" data-id="${subsection.location}">
|
||||
<div class="subsection-name-input">
|
||||
<label>Display Name:</label>
|
||||
<input type="text" value="${subsection.display_name | h}" class="subsection-display-name-input" data-metadata-name="display_name"/>
|
||||
<input type="text" value="${subsection.display_name_with_default | h}" class="subsection-display-name-input" data-metadata-name="display_name"/>
|
||||
</div>
|
||||
<div class="sortable-unit-list">
|
||||
<label>Units:</label>
|
||||
@@ -40,7 +40,7 @@
|
||||
<a href="#" class="save-button">Save</a>
|
||||
<a href="#" class="cancel-button">Cancel</a>
|
||||
<a href="#" class="delete-icon remove-policy-data"></a>
|
||||
</li>
|
||||
</li>
|
||||
</div>
|
||||
|
||||
<div class="sidebar">
|
||||
@@ -51,28 +51,28 @@
|
||||
<label>Release date:<!-- <span class="description">Determines when this subsection and the units within it will be released publicly.</span>--></label>
|
||||
<div class="datepair" data-language="javascript">
|
||||
<%
|
||||
start_date = datetime.fromtimestamp(mktime(subsection.start)) if subsection.start is not None else None
|
||||
parent_start_date = datetime.fromtimestamp(mktime(parent_item.start)) if parent_item.start is not None else None
|
||||
start_date = datetime.fromtimestamp(mktime(subsection.lms.start)) if subsection.lms.start is not None else None
|
||||
parent_start_date = datetime.fromtimestamp(mktime(parent_item.lms.start)) if parent_item.lms.start is not None else None
|
||||
%>
|
||||
<input type="text" id="start_date" name="start_date" value="${start_date.strftime('%m/%d/%Y') if start_date is not None else ''}" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
|
||||
<input type="text" id="start_time" name="start_time" value="${start_date.strftime('%H:%M') if start_date is not None else ''}" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
|
||||
<input type="text" id="start_time" name="start_time" value="${start_date.strftime('%H:%M') if start_date is not None else ''}" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
|
||||
</div>
|
||||
% if subsection.start != parent_item.start and subsection.start:
|
||||
% if subsection.lms.start != parent_item.lms.start and subsection.lms.start:
|
||||
% if parent_start_date is None:
|
||||
<p class="notice">The date above differs from the release date of ${parent_item.display_name}, which is unset.
|
||||
<p class="notice">The date above differs from the release date of ${parent_item.display_name_with_default}, which is unset.
|
||||
% else:
|
||||
<p class="notice">The date above differs from the release date of ${parent_item.display_name} – ${parent_start_date.strftime('%m/%d/%Y')} at ${parent_start_date.strftime('%H:%M')}.
|
||||
<p class="notice">The date above differs from the release date of ${parent_item.display_name_with_default} – ${parent_start_date.strftime('%m/%d/%Y')} at ${parent_start_date.strftime('%H:%M')}.
|
||||
% endif
|
||||
<a href="#" class="sync-date no-spinner">Sync to ${parent_item.display_name}.</a></p>
|
||||
<a href="#" class="sync-date no-spinner">Sync to ${parent_item.display_name_with_default}.</a></p>
|
||||
% endif
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row gradable">
|
||||
<label>Graded as:</label>
|
||||
|
||||
<div class="gradable-status" data-initial-status="${subsection.metadata.get('format', 'Not Graded')}">
|
||||
|
||||
<div class="gradable-status" data-initial-status="${subsection.lms.format if subsection.lms.format is not None else 'Not Graded'}">
|
||||
</div>
|
||||
|
||||
|
||||
<div class="due-date-input row">
|
||||
<label>Due date:</label>
|
||||
<a href="#" class="set-date">Set a due date</a>
|
||||
@@ -80,9 +80,9 @@
|
||||
<p class="date-description">
|
||||
<%
|
||||
# due date uses it own formatting for stringifying the date. As with capa_module.py, there's a utility module available for us to use
|
||||
due_date = dateutil.parser.parse(subsection.metadata.get('due')) if 'due' in subsection.metadata else None
|
||||
due_date = dateutil.parser.parse(subsection.lms.due) if subsection.lms.due else None
|
||||
%>
|
||||
<input type="text" id="due_date" name="due_date" value="${due_date.strftime('%m/%d/%Y') if due_date is not None else ''}" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
|
||||
<input type="text" id="due_date" name="due_date" value="${due_date.strftime('%m/%d/%Y') if due_date is not None else ''}" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
|
||||
<input type="text" id="due_time" name="due_time" value="${due_date.strftime('%H:%M') if due_date is not None else ''}" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
|
||||
<a href="#" class="remove-date">Remove due date</a>
|
||||
</p>
|
||||
@@ -110,7 +110,7 @@
|
||||
<script type="text/javascript" src="${static.url('js/views/grader-select-view.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/models/settings/course_grading_policy.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/overview.js')}"></script>
|
||||
|
||||
|
||||
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
@@ -128,7 +128,7 @@
|
||||
window.graderTypes.course_location = new CMS.Models.Location('${parent_location}');
|
||||
window.graderTypes.reset(${course_graders|n});
|
||||
}
|
||||
|
||||
|
||||
$(".gradable-status").each(function(index, ele) {
|
||||
var gradeView = new CMS.Views.OverviewAssignmentGrader({
|
||||
el : ele,
|
||||
|
||||
@@ -21,14 +21,14 @@
|
||||
<div class="description">
|
||||
<p><strong>Importing a new course will delete all content currently associated with your course
|
||||
and replace it with the contents of the uploaded file.</strong></p>
|
||||
<p>File uploads must be gzipped tar files (.tar.gz or .tgz) containing, at a minimum, a <code>course.xml</code> file.</p>
|
||||
<p>File uploads must be gzipped tar files (.tar.gz) containing, at a minimum, a <code>course.xml</code> file.</p>
|
||||
<p>Please note that if your course has any problems with auto-generated <code>url_name</code> nodes,
|
||||
re-importing your course could cause the loss of student data associated with those problems.</p>
|
||||
</div>
|
||||
<form action="${reverse('import_course', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}" method="post" enctype="multipart/form-data" class="import-form">
|
||||
<h2>Course to import:</h2>
|
||||
<p class="error-block"></p>
|
||||
<a href="#" class="choose-file-button">Choose File</a>
|
||||
<a href="#" class="choose-file-button">Choose File</a>
|
||||
<p class="file-name-block"><span class="file-name"></span><a href="#" class="choose-file-button-inline">change</a></p>
|
||||
<input type="file" name="course-data" class="file-input">
|
||||
<input type="submit" value="Replace my course with the one above" class="submit-button">
|
||||
@@ -45,13 +45,13 @@
|
||||
<%block name="jsextra">
|
||||
<script>
|
||||
(function() {
|
||||
|
||||
|
||||
var bar = $('.progress-bar');
|
||||
var fill = $('.progress-fill');
|
||||
var percent = $('.percent');
|
||||
var status = $('#status');
|
||||
var submitBtn = $('.submit-button');
|
||||
|
||||
|
||||
$('form').ajaxForm({
|
||||
beforeSend: function() {
|
||||
status.empty();
|
||||
@@ -76,7 +76,7 @@ $('form').ajaxForm({
|
||||
submitBtn.show();
|
||||
bar.hide();
|
||||
}
|
||||
});
|
||||
})();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</%block>
|
||||
@@ -8,7 +8,7 @@
|
||||
<div>${module_type}</div>
|
||||
<div>
|
||||
% for template in module_templates:
|
||||
<a class="save" data-template-id="${template.location.url()}">${template.display_name}</a>
|
||||
<a class="save" data-template-id="${template.location.url()}">${template.display_name_with_default}</a>
|
||||
% endfor
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
window.graderTypes.course_location = new CMS.Models.Location('${parent_location}');
|
||||
window.graderTypes.reset(${course_graders|n});
|
||||
}
|
||||
|
||||
|
||||
$(".gradable-status").each(function(index, ele) {
|
||||
var gradeView = new CMS.Views.OverviewAssignmentGrader({
|
||||
el : ele,
|
||||
@@ -40,7 +40,7 @@
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
@@ -146,7 +146,7 @@
|
||||
|
||||
<div class="main-wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<article class="courseware-overview" data-course-id="${context_course.location.url()}">
|
||||
<article class="courseware-overview" data-course-id="${context_course.location.url()}">
|
||||
% for section in sections:
|
||||
<section class="courseware-section branch" data-id="${section.location}">
|
||||
<header>
|
||||
@@ -154,16 +154,16 @@
|
||||
|
||||
<div class="item-details" data-id="${section.location}">
|
||||
<h3 class="section-name">
|
||||
<span data-tooltip="Edit this section's name" class="section-name-span">${section.display_name}</span>
|
||||
<span data-tooltip="Edit this section's name" class="section-name-span">${section.display_name_with_default}</span>
|
||||
<form class="section-name-edit" style="display:none">
|
||||
<input type="text" value="${section.display_name | h}" class="edit-section-name" autocomplete="off"/>
|
||||
<input type="text" value="${section.display_name_with_default | h}" class="edit-section-name" autocomplete="off"/>
|
||||
<input type="submit" class="save-button edit-section-name-save" value="Save" />
|
||||
<input type="button" class="cancel-button edit-section-name-cancel" value="Cancel" />
|
||||
</form>
|
||||
</h3>
|
||||
<div class="section-published-date">
|
||||
<%
|
||||
start_date = datetime.fromtimestamp(mktime(section.start)) if section.start is not None else None
|
||||
start_date = datetime.fromtimestamp(mktime(section.lms.start)) if section.lms.start is not None else None
|
||||
start_date_str = start_date.strftime('%m/%d/%Y') if start_date is not None else ''
|
||||
start_time_str = start_date.strftime('%H:%M') if start_date is not None else ''
|
||||
%>
|
||||
@@ -174,9 +174,9 @@
|
||||
<span class="published-status"><strong>Will Release:</strong> ${start_date_str} at ${start_time_str}</span>
|
||||
<a href="#" class="edit-button" data-date="${start_date_str}" data-time="${start_time_str}" data-id="${section.location}">Edit</a>
|
||||
%endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="item-actions">
|
||||
<a href="#" data-tooltip="Delete this section" class="delete-button delete-section-button"><span class="delete-icon"></span></a>
|
||||
<a href="#" data-tooltip="Drag to reorder" class="drag-handle"></a>
|
||||
@@ -196,15 +196,15 @@
|
||||
<a href="#" data-tooltip="Expand/collapse this subsection" class="expand-collapse-icon expand"></a>
|
||||
<a href="${reverse('edit_subsection', args=[subsection.location])}">
|
||||
<span class="folder-icon"></span>
|
||||
<span class="subsection-name"><span class="subsection-name-value">${subsection.display_name}</span></span>
|
||||
<span class="subsection-name"><span class="subsection-name-value">${subsection.display_name_with_default}</span></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="gradable-status" data-initial-status="${subsection.metadata.get('format', 'Not Graded')}">
|
||||
|
||||
<div class="gradable-status" data-initial-status="${subsection.lms.format if section.lms.format is not None else 'Not Graded'}">
|
||||
</div>
|
||||
|
||||
|
||||
<div class="item-actions">
|
||||
<a href="#" data-tooltip="Delete this subsection" class="delete-button delete-subsection-button"><span class="delete-icon"></span></a>
|
||||
<a href="#" data-tooltip="Delete this subsection" class="delete-button delete-subsection-button"><span class="delete-icon"></span></a>
|
||||
<a href="#" data-tooltip="Drag to reorder" class="drag-handle"></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -70,17 +70,17 @@ from contentstore import utils
|
||||
<ol class="list-input">
|
||||
<li class="field text is-not-editable" id="field-course-organization">
|
||||
<label for="course-organization">Organization</label>
|
||||
<input type="text" class="long" id="course-organization" value="[Course Organization]" disabled="disabled" />
|
||||
<input title="This field is disabled: this information cannot be changed." type="text" class="long" id="course-organization" value="[Course Organization]" readonly />
|
||||
</li>
|
||||
|
||||
<li class="field text is-not-editable" id="field-course-number">
|
||||
<label for="course-number">Course Number</label>
|
||||
<input type="text" class="short" id="course-number" value="[Course No.]" disabled="disabled">
|
||||
<input title="This field is disabled: this information cannot be changed." type="text" class="short" id="course-number" value="[Course No.]" readonly>
|
||||
</li>
|
||||
|
||||
<li class="field text is-not-editable" id="field-course-name">
|
||||
<label for="course-name">Course Name</label>
|
||||
<input type="text" class="long" id="course-name" value="[Course Name]" disabled="disabled" />
|
||||
<input title="This field is disabled: this information cannot be changed." type="text" class="long" id="course-name" value="[Course Name]" readonly />
|
||||
</li>
|
||||
</ol>
|
||||
<span class="tip tip-stacked">These are used in <a rel="external" href="${utils.get_lms_link_for_about_page(course_location)}" />your course URL</a>, and cannot be changed</span>
|
||||
|
||||
@@ -21,7 +21,6 @@ $(document).ready(function () {
|
||||
|
||||
// proactively populate advanced b/c it has the filtered list and doesn't really follow the model pattern
|
||||
var advancedModel = new CMS.Models.Settings.Advanced(${advanced_dict | n}, {parse: true});
|
||||
advancedModel.blacklistKeys = ${advanced_blacklist | n};
|
||||
advancedModel.url = "${reverse('course_advanced_settings_updates', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}";
|
||||
|
||||
var editor = new CMS.Views.Settings.Advanced({
|
||||
@@ -61,18 +60,11 @@ editor.render();
|
||||
<span class="tip">Manually Edit Course Policy Values (JSON Key / Value pairs)</span>
|
||||
</header>
|
||||
|
||||
<p class="instructions"><strong>Warning</strong>: Add only manual policy data that you are familiar
|
||||
with.</p>
|
||||
<p class="instructions"><strong>Warning</strong>: Do not modify these policies unless you are familiar with their purpose.</p>
|
||||
|
||||
<ul class="list-input course-advanced-policy-list enum">
|
||||
|
||||
</ul>
|
||||
|
||||
<div class="actions">
|
||||
<a href="#" class="button new-button new-advanced-policy-item add-policy-data">
|
||||
<span class="plus-icon white"></span>New Manual Policy
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
</article>
|
||||
@@ -80,9 +72,9 @@ editor.render();
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<div class="bit">
|
||||
<h3 class="title-3">How will these settings be used?</h3>
|
||||
<p>Manual policies are JSON-based key and value pairs that allow you add additional settings which edX Studio will use when generating your course.</p>
|
||||
<p>Manual policies are JSON-based key and value pairs that give you control over specific course settings that edX Studio will use when displaying and running your course.</p>
|
||||
|
||||
<p>Any policies you define here will override any other information you've defined elsewhere in Studio. With this in mind, please be very careful and do not add policies that you are unfamiliar with (both their purpose and their syntax).</p>
|
||||
<p>Any policies you modify here will override any other information you've defined elsewhere in Studio. With this in mind, please be very careful and do not edit policies that you are unfamiliar with (both their purpose and their syntax).</p>
|
||||
</div>
|
||||
|
||||
<div class="bit">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">▾</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 & 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">▾</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">▾</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">👤</i>
|
||||
<i class="ss-icon ss-symbolicons-standard icon-user">👤</i>
|
||||
${ user.username }
|
||||
</span>
|
||||
<i class="ss-icon ss-symbolicons-block icon-expand">▾</i>
|
||||
@@ -111,7 +111,7 @@
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
% endif
|
||||
% endif
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<a href="#" class="module-edit"
|
||||
data-id="${child.location.url()}"
|
||||
data-type="${child.js_module_name}"
|
||||
data-preview-type="${child.module_class.js_module_name}">${child.display_name}</a>
|
||||
data-preview-type="${child.module_class.js_module_name}">${child.display_name_with_default}</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
%endfor
|
||||
|
||||
@@ -22,7 +22,7 @@ This def will enumerate through a passed in subsection and list all of the units
|
||||
<div class="section-item ${selected_class}">
|
||||
<a href="${reverse('edit_unit', args=[unit.location])}" class="${unit_state}-item">
|
||||
<span class="${unit.category}-icon"></span>
|
||||
<span class="unit-name">${unit.display_name}</span>
|
||||
<span class="unit-name">${unit.display_name_with_default}</span>
|
||||
</a>
|
||||
% if actions:
|
||||
<div class="item-actions">
|
||||
@@ -39,7 +39,7 @@ This def will enumerate through a passed in subsection and list all of the units
|
||||
</a>
|
||||
</li>
|
||||
</ol>
|
||||
</%def>
|
||||
</%def>
|
||||
|
||||
|
||||
|
||||
|
||||
30
cms/xmodule_namespace.py
Normal file
30
cms/xmodule_namespace.py
Normal 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)
|
||||
Reference in New Issue
Block a user