Commit 48f867bd by David Ormsbee

Merge branch 'master' into ormsbee/verifyuser3

Conflicts:
	lms/envs/common.py
parents 79a9c867 b53d5554
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
from lettuce import world, step from lettuce import world, step
from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.keys import Keys
from nose.tools import assert_true # pylint: disable=E0611
@step(u'I go to the static pages page') @step(u'I go to the static pages page')
...@@ -21,18 +22,21 @@ def add_page(_step): ...@@ -21,18 +22,21 @@ def add_page(_step):
@step(u'I should( not)? see a "([^"]*)" static page$') @step(u'I should( not)? see a "([^"]*)" static page$')
def see_page(_step, doesnt, page): def see_page(_step, doesnt, page):
index = get_index(page)
if doesnt: should_exist = not doesnt
assert index == -1
else: # Need to retry here because the element
assert index != -1 # will sometimes exist before the HTML content is loaded
exists_func = lambda(driver): page_exists(page) == should_exist
world.wait_for(exists_func)
assert_true(exists_func(None))
@step(u'I "([^"]*)" the "([^"]*)" page$') @step(u'I "([^"]*)" the "([^"]*)" page$')
def click_edit_delete(_step, edit_delete, page): def click_edit_delete(_step, edit_delete, page):
button_css = 'a.%s-button' % edit_delete button_css = 'a.%s-button' % edit_delete
index = get_index(page) index = get_index(page)
assert index != -1 assert index is not None
world.css_click(button_css, index=index) world.css_click(button_css, index=index)
...@@ -54,4 +58,7 @@ def get_index(name): ...@@ -54,4 +58,7 @@ def get_index(name):
for i in range(len(all_pages)): for i in range(len(all_pages)):
if world.css_html(page_name_css, index=i) == '\n {name}\n'.format(name=name): if world.css_html(page_name_css, index=i) == '\n {name}\n'.format(name=name):
return i return i
return -1 return None
def page_exists(page):
return get_index(page) is not None
...@@ -10,6 +10,16 @@ Feature: Upload Files ...@@ -10,6 +10,16 @@ Feature: Upload Files
Then I should see the file "test" was uploaded Then I should see the file "test" was uploaded
And The url for the file "test" is valid And The url for the file "test" is valid
@skip_safari
Scenario: Users can upload multiple files
Given I have opened a new course in studio
And I go to the files and uploads page
When I upload the files "test","test2"
Then I should see the file "test" was uploaded
And I should see the file "test2" was uploaded
And The url for the file "test2" is valid
And The url for the file "test" is valid
# Uploading isn't working on safari with sauce labs # Uploading isn't working on safari with sauce labs
@skip_safari @skip_safari
Scenario: Users can update files Scenario: Users can update files
......
...@@ -26,7 +26,24 @@ def upload_file(_step, file_name): ...@@ -26,7 +26,24 @@ def upload_file(_step, file_name):
#uploading the file itself #uploading the file itself
path = os.path.join(TEST_ROOT, 'uploads/', file_name) path = os.path.join(TEST_ROOT, 'uploads/', file_name)
world.browser.execute_script("$('input.file-input').css('display', 'block')") world.browser.execute_script("$('input.file-input').css('display', 'block')")
world.browser.attach_file('file', os.path.abspath(path)) world.browser.attach_file('files[]', os.path.abspath(path))
close_css = 'a.close-button'
world.css_click(close_css)
@step(u'I upload the files (".*")$')
def upload_file(_step, files_string):
# Turn files_string to a list of file names
files = files_string.split(",")
files = map(lambda x: string.strip(x, ' "\''), files)
upload_css = 'a.upload-button'
world.css_click(upload_css)
#uploading the files
for f in files:
path = os.path.join(TEST_ROOT, 'uploads/', f)
world.browser.execute_script("$('input.file-input').css('display', 'block')")
world.browser.attach_file('files[]', os.path.abspath(path))
close_css = 'a.close-button' close_css = 'a.close-button'
world.css_click(close_css) world.css_click(close_css)
......
...@@ -17,13 +17,13 @@ Feature: Video Component Editor ...@@ -17,13 +17,13 @@ Feature: Video Component Editor
# Sauce Labs cannot delete cookies # Sauce Labs cannot delete cookies
@skip_sauce @skip_sauce
Scenario: Captions are hidden when "show captions" is false Scenario: Captions are hidden when "show captions" is false
Given I have created a Video component Given I have created a Video component with subtitles
And I have set "show captions" to False And I have set "show captions" to False
Then when I view the video it does not show the captions Then when I view the video it does not show the captions
# Sauce Labs cannot delete cookies # Sauce Labs cannot delete cookies
@skip_sauce @skip_sauce
Scenario: Captions are shown when "show captions" is true Scenario: Captions are shown when "show captions" is true
Given I have created a Video component Given I have created a Video component with subtitles
And I have set "show captions" to True And I have set "show captions" to True
Then when I view the video it does show the captions Then when I view the video it does show the captions
...@@ -7,20 +7,21 @@ from terrain.steps import reload_the_page ...@@ -7,20 +7,21 @@ from terrain.steps import reload_the_page
@step('I have set "show captions" to (.*)$') @step('I have set "show captions" to (.*)$')
def set_show_captions(step, setting): def set_show_captions(step, setting):
# Prevent cookies from overriding course settings
world.browser.cookies.delete('hide_captions')
world.css_click('a.edit-button') world.css_click('a.edit-button')
world.wait_for(lambda _driver: world.css_visible('a.save-button')) world.wait_for(lambda _driver: world.css_visible('a.save-button'))
world.browser.select('Show Captions', setting) world.browser.select('Show Captions', setting)
world.css_click('a.save-button') world.css_click('a.save-button')
@step('when I view the (video.*) it (.*) show the captions$') @step('when I view the video it (.*) show the captions$')
def shows_captions(_step, video_type, show_captions): def shows_captions(_step, show_captions):
# Prevent cookies from overriding course settings
world.browser.cookies.delete('hide_captions')
if show_captions == 'does not': if show_captions == 'does not':
assert world.css_has_class('.%s' % video_type, 'closed') assert world.is_css_present('div.video.closed')
else: else:
assert world.is_css_not_present('.%s.closed' % video_type) assert world.is_css_not_present('div.video.closed')
@step('I see the correct video settings and default values$') @step('I see the correct video settings and default values$')
......
...@@ -13,20 +13,20 @@ Feature: Video Component ...@@ -13,20 +13,20 @@ Feature: Video Component
# Sauce Labs cannot delete cookies # Sauce Labs cannot delete cookies
@skip_sauce @skip_sauce
Scenario: Captions are hidden correctly Scenario: Captions are hidden correctly
Given I have created a Video component Given I have created a Video component with subtitles
And I have hidden captions And I have hidden captions
Then when I view the video it does not show the captions Then when I view the video it does not show the captions
# Sauce Labs cannot delete cookies # Sauce Labs cannot delete cookies
@skip_sauce @skip_sauce
Scenario: Captions are shown correctly Scenario: Captions are shown correctly
Given I have created a Video component Given I have created a Video component with subtitles
Then when I view the video it does show the captions Then when I view the video it does show the captions
# Sauce Labs cannot delete cookies # Sauce Labs cannot delete cookies
@skip_sauce @skip_sauce
Scenario: Captions are toggled correctly Scenario: Captions are toggled correctly
Given I have created a Video component Given I have created a Video component with subtitles
And I have toggled captions And I have toggled captions
Then when I view the video it does show the captions Then when I view the video it does show the captions
......
...@@ -8,7 +8,6 @@ from contentstore.utils import get_modulestore ...@@ -8,7 +8,6 @@ from contentstore.utils import get_modulestore
############### ACTIONS #################### ############### ACTIONS ####################
@step('I have created a Video component$') @step('I have created a Video component$')
def i_created_a_video_component(step): def i_created_a_video_component(step):
world.create_component_instance( world.create_component_instance(
...@@ -19,6 +18,26 @@ def i_created_a_video_component(step): ...@@ -19,6 +18,26 @@ def i_created_a_video_component(step):
) )
@step('I have created a Video component with subtitles$')
def i_created_a_video_component(step):
step.given('I have created a Video component')
# Store the current URL so we can return here
video_url = world.browser.url
# Upload subtitles for the video using the upload interface
step.given('I have uploaded subtitles')
# Return to the video
world.visit(video_url)
@step('I have uploaded subtitles')
def i_have_uploaded_subtitles(step):
step.given('I go to the files and uploads page')
step.given('I upload the file "subs_OEoXaMPEzfM.srt.sjson"')
@step('when I view the (.*) it does not have autoplay enabled$') @step('when I view the (.*) it does not have autoplay enabled$')
def does_not_autoplay(_step, video_type): def does_not_autoplay(_step, video_type):
assert world.css_find('.%s' % video_type)[0]['data-autoplay'] == 'False' assert world.css_find('.%s' % video_type)[0]['data-autoplay'] == 'False'
......
...@@ -60,4 +60,3 @@ class Command(BaseCommand): ...@@ -60,4 +60,3 @@ class Command(BaseCommand):
for item in queried_discussion_items: for item in queried_discussion_items:
if item.location.url() not in discussion_items: if item.location.url() not in discussion_items:
print 'Found dangling discussion module = {0}'.format(item.location.url()) print 'Found dangling discussion module = {0}'.format(item.location.url())
...@@ -64,11 +64,11 @@ def set_module_info(store, location, post_data): ...@@ -64,11 +64,11 @@ def set_module_info(store, location, post_data):
if posted_metadata[metadata_key] is None: if posted_metadata[metadata_key] is None:
# remove both from passed in collection as well as the collection read in from the modulestore # remove both from passed in collection as well as the collection read in from the modulestore
if metadata_key in module._model_data: if module._field_data.has(module, metadata_key):
del module._model_data[metadata_key] module._field_data.delete(module, metadata_key)
del posted_metadata[metadata_key] del posted_metadata[metadata_key]
else: else:
module._model_data[metadata_key] = value module._field_data.set(module, metadata_key, value)
# commit to datastore # commit to datastore
# TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata # TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
......
...@@ -118,16 +118,20 @@ class CourseDetailsTestCase(CourseTestCase): ...@@ -118,16 +118,20 @@ class CourseDetailsTestCase(CourseTestCase):
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': True}): with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': True}):
response = self.client.get(settings_details_url) response = self.client.get(settings_details_url)
self.assertContains(response, "Course Summary Page") self.assertNotContains(response, "Course Summary Page")
self.assertNotContains(response, "Send a note to students via email")
self.assertContains(response, "course summary page will not be viewable") self.assertContains(response, "course summary page will not be viewable")
self.assertContains(response, "Course Start Date") self.assertContains(response, "Course Start Date")
self.assertContains(response, "Course End Date") self.assertContains(response, "Course End Date")
self.assertNotContains(response, "Enrollment Start Date") self.assertContains(response, "Enrollment Start Date")
self.assertNotContains(response, "Enrollment End Date") self.assertContains(response, "Enrollment End Date")
self.assertContains(response, "not the dates shown on your course summary page") self.assertContains(response, "not the dates shown on your course summary page")
self.assertNotContains(response, "Introducing Your Course") self.assertContains(response, "Introducing Your Course")
self.assertContains(response, "Course Image")
self.assertNotContains(response,"Course Overview")
self.assertNotContains(response,"Course Introduction Video")
self.assertNotContains(response, "Requirements") self.assertNotContains(response, "Requirements")
def test_regular_site_fetch(self): def test_regular_site_fetch(self):
...@@ -143,6 +147,7 @@ class CourseDetailsTestCase(CourseTestCase): ...@@ -143,6 +147,7 @@ class CourseDetailsTestCase(CourseTestCase):
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': False}): with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': False}):
response = self.client.get(settings_details_url) response = self.client.get(settings_details_url)
self.assertContains(response, "Course Summary Page") self.assertContains(response, "Course Summary Page")
self.assertContains(response, "Send a note to students via email")
self.assertNotContains(response, "course summary page will not be viewable") self.assertNotContains(response, "course summary page will not be viewable")
self.assertContains(response, "Course Start Date") self.assertContains(response, "Course Start Date")
...@@ -152,6 +157,9 @@ class CourseDetailsTestCase(CourseTestCase): ...@@ -152,6 +157,9 @@ class CourseDetailsTestCase(CourseTestCase):
self.assertNotContains(response, "not the dates shown on your course summary page") self.assertNotContains(response, "not the dates shown on your course summary page")
self.assertContains(response, "Introducing Your Course") self.assertContains(response, "Introducing Your Course")
self.assertContains(response, "Course Image")
self.assertContains(response,"Course Overview")
self.assertContains(response,"Course Introduction Video")
self.assertContains(response, "Requirements") self.assertContains(response, "Requirements")
...@@ -341,8 +349,8 @@ class CourseGradingTest(CourseTestCase): ...@@ -341,8 +349,8 @@ class CourseGradingTest(CourseTestCase):
section_grader_type = CourseGradingModel.get_section_grader_type(self.course.location) section_grader_type = CourseGradingModel.get_section_grader_type(self.course.location)
self.assertEqual('Not Graded', section_grader_type['graderType']) self.assertEqual('Not Graded', section_grader_type['graderType'])
self.assertEqual(None, descriptor.lms.format) self.assertEqual(None, descriptor.format)
self.assertEqual(False, descriptor.lms.graded) self.assertEqual(False, descriptor.graded)
# Change the default grader type to Homework, which should also mark the section as graded # Change the default grader type to Homework, which should also mark the section as graded
CourseGradingModel.update_section_grader_type(self.course.location, {'graderType': 'Homework'}) CourseGradingModel.update_section_grader_type(self.course.location, {'graderType': 'Homework'})
...@@ -350,8 +358,8 @@ class CourseGradingTest(CourseTestCase): ...@@ -350,8 +358,8 @@ class CourseGradingTest(CourseTestCase):
section_grader_type = CourseGradingModel.get_section_grader_type(self.course.location) section_grader_type = CourseGradingModel.get_section_grader_type(self.course.location)
self.assertEqual('Homework', section_grader_type['graderType']) self.assertEqual('Homework', section_grader_type['graderType'])
self.assertEqual('Homework', descriptor.lms.format) self.assertEqual('Homework', descriptor.format)
self.assertEqual(True, descriptor.lms.graded) self.assertEqual(True, descriptor.graded)
# Change the grader type back to Not Graded, which should also unmark the section as graded # Change the grader type back to Not Graded, which should also unmark the section as graded
CourseGradingModel.update_section_grader_type(self.course.location, {'graderType': 'Not Graded'}) CourseGradingModel.update_section_grader_type(self.course.location, {'graderType': 'Not Graded'})
...@@ -359,8 +367,8 @@ class CourseGradingTest(CourseTestCase): ...@@ -359,8 +367,8 @@ class CourseGradingTest(CourseTestCase):
section_grader_type = CourseGradingModel.get_section_grader_type(self.course.location) section_grader_type = CourseGradingModel.get_section_grader_type(self.course.location)
self.assertEqual('Not Graded', section_grader_type['graderType']) self.assertEqual('Not Graded', section_grader_type['graderType'])
self.assertEqual(None, descriptor.lms.format) self.assertEqual(None, descriptor.format)
self.assertEqual(False, descriptor.lms.graded) self.assertEqual(False, descriptor.graded)
class CourseMetadataEditingTest(CourseTestCase): class CourseMetadataEditingTest(CourseTestCase):
......
...@@ -218,20 +218,16 @@ class TemplateTests(unittest.TestCase): ...@@ -218,20 +218,16 @@ class TemplateTests(unittest.TestCase):
) )
usage_id = json_data.get('_id', None) usage_id = json_data.get('_id', None)
if not '_inherited_settings' in json_data and parent_xblock is not None: if not '_inherited_settings' in json_data and parent_xblock is not None:
json_data['_inherited_settings'] = parent_xblock.xblock_kvs.get_inherited_settings().copy() json_data['_inherited_settings'] = parent_xblock.xblock_kvs.inherited_settings.copy()
json_fields = json_data.get('fields', {}) json_fields = json_data.get('fields', {})
for field in inheritance.INHERITABLE_METADATA: for field_name in inheritance.InheritanceMixin.fields:
if field in json_fields: if field_name in json_fields:
json_data['_inherited_settings'][field] = json_fields[field] json_data['_inherited_settings'][field_name] = json_fields[field_name]
new_block = system.xblock_from_json(class_, usage_id, json_data) new_block = system.xblock_from_json(class_, usage_id, json_data)
if parent_xblock is not None: if parent_xblock is not None:
children = parent_xblock.children parent_xblock.children.append(new_block.scope_ids.usage_id)
children.append(new_block) # decache pending children field settings
# trigger setter method by using top level field access
parent_xblock.children = children
# decache pending children field settings (Note, truly persisting at this point would break b/c
# persistence assumes children is a list of ids not actual xblocks)
parent_xblock.save() parent_xblock.save()
return new_block return new_block
...@@ -97,9 +97,9 @@ class ContentStoreImportNoStaticTest(ModuleStoreTestCase): ...@@ -97,9 +97,9 @@ class ContentStoreImportNoStaticTest(ModuleStoreTestCase):
self.assertIsNotNone(content) self.assertIsNotNone(content)
# make sure course.lms.static_asset_path is correct # make sure course.static_asset_path is correct
print "static_asset_path = {0}".format(course.lms.static_asset_path) print "static_asset_path = {0}".format(course.static_asset_path)
self.assertEqual(course.lms.static_asset_path, 'test_import_course') self.assertEqual(course.static_asset_path, 'test_import_course')
def test_asset_import_nostatic(self): def test_asset_import_nostatic(self):
''' '''
......
from contentstore.tests.test_course_settings import CourseTestCase from contentstore.tests.utils import CourseTestCase
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from xmodule.capa_module import CapaDescriptor from xmodule.capa_module import CapaDescriptor
...@@ -69,7 +69,7 @@ class TestCreateItem(CourseTestCase): ...@@ -69,7 +69,7 @@ class TestCreateItem(CourseTestCase):
# get the new item and check its category and display_name # get the new item and check its category and display_name
chap_location = self.response_id(resp) chap_location = self.response_id(resp)
new_obj = modulestore().get_item(chap_location) new_obj = modulestore().get_item(chap_location)
self.assertEqual(new_obj.category, 'chapter') self.assertEqual(new_obj.scope_ids.block_type, 'chapter')
self.assertEqual(new_obj.display_name, display_name) self.assertEqual(new_obj.display_name, display_name)
self.assertEqual(new_obj.location.org, self.course.location.org) self.assertEqual(new_obj.location.org, self.course.location.org)
self.assertEqual(new_obj.location.course, self.course.location.course) self.assertEqual(new_obj.location.course, self.course.location.course)
...@@ -226,7 +226,7 @@ class TestEditItem(CourseTestCase): ...@@ -226,7 +226,7 @@ class TestEditItem(CourseTestCase):
Test setting due & start dates on sequential Test setting due & start dates on sequential
""" """
sequential = modulestore().get_item(self.seq_location) sequential = modulestore().get_item(self.seq_location)
self.assertIsNone(sequential.lms.due) self.assertIsNone(sequential.due)
self.client.post( self.client.post(
reverse('save_item'), reverse('save_item'),
json.dumps({ json.dumps({
...@@ -236,7 +236,7 @@ class TestEditItem(CourseTestCase): ...@@ -236,7 +236,7 @@ class TestEditItem(CourseTestCase):
content_type="application/json" content_type="application/json"
) )
sequential = modulestore().get_item(self.seq_location) sequential = modulestore().get_item(self.seq_location)
self.assertEqual(sequential.lms.due, datetime.datetime(2010, 11, 22, 4, 0, tzinfo=UTC)) self.assertEqual(sequential.due, datetime.datetime(2010, 11, 22, 4, 0, tzinfo=UTC))
self.client.post( self.client.post(
reverse('save_item'), reverse('save_item'),
json.dumps({ json.dumps({
...@@ -246,5 +246,5 @@ class TestEditItem(CourseTestCase): ...@@ -246,5 +246,5 @@ class TestEditItem(CourseTestCase):
content_type="application/json" content_type="application/json"
) )
sequential = modulestore().get_item(self.seq_location) sequential = modulestore().get_item(self.seq_location)
self.assertEqual(sequential.lms.due, datetime.datetime(2010, 11, 22, 4, 0, tzinfo=UTC)) self.assertEqual(sequential.due, datetime.datetime(2010, 11, 22, 4, 0, tzinfo=UTC))
self.assertEqual(sequential.lms.start, datetime.datetime(2010, 9, 12, 14, 0, tzinfo=UTC)) self.assertEqual(sequential.start, datetime.datetime(2010, 9, 12, 14, 0, tzinfo=UTC))
...@@ -149,14 +149,14 @@ def upload_asset(request, org, course, coursename): ...@@ -149,14 +149,14 @@ def upload_asset(request, org, course, coursename):
logging.error('Could not find course' + location) logging.error('Could not find course' + location)
return HttpResponseBadRequest() return HttpResponseBadRequest()
if 'file' not in request.FILES: if 'files[]' not in request.FILES:
return HttpResponseBadRequest() return HttpResponseBadRequest()
# compute a 'filename' which is similar to the location formatting, we're # compute a 'filename' which is similar to the location formatting, we're
# using the 'filename' nomenclature since we're using a FileSystem paradigm # using the 'filename' nomenclature since we're using a FileSystem paradigm
# here. We're just imposing the Location string formatting expectations to # here. We're just imposing the Location string formatting expectations to
# keep things a bit more consistent # keep things a bit more consistent
upload_file = request.FILES['file'] upload_file = request.FILES['files[]']
filename = upload_file.name filename = upload_file.name
mime_type = upload_file.content_type mime_type = upload_file.content_type
......
...@@ -2,27 +2,27 @@ import json ...@@ -2,27 +2,27 @@ import json
import logging import logging
from collections import defaultdict from collections import defaultdict
from django.http import ( HttpResponse, HttpResponseBadRequest, from django.http import (HttpResponse, HttpResponseBadRequest,
HttpResponseForbidden ) HttpResponseForbidden)
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django_future.csrf import ensure_csrf_cookie from django_future.csrf import ensure_csrf_cookie
from django.conf import settings from django.conf import settings
from xmodule.modulestore.exceptions import ( ItemNotFoundError, from xmodule.modulestore.exceptions import (ItemNotFoundError,
InvalidLocationError ) InvalidLocationError)
from mitxmako.shortcuts import render_to_response from mitxmako.shortcuts import render_to_response
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.util.date_utils import get_default_time_display from xmodule.util.date_utils import get_default_time_display
from xblock.core import Scope from xblock.fields import Scope
from util.json_request import expect_json, JsonResponse from util.json_request import expect_json, JsonResponse
from contentstore.module_info_model import get_module_info, set_module_info from contentstore.module_info_model import get_module_info, set_module_info
from contentstore.utils import ( get_modulestore, get_lms_link_for_item, from contentstore.utils import (get_modulestore, get_lms_link_for_item,
compute_unit_state, UnitState, get_course_for_item ) compute_unit_state, UnitState, get_course_for_item)
from models.settings.course_grading import CourseGradingModel from models.settings.course_grading import CourseGradingModel
...@@ -30,6 +30,7 @@ from .requests import _xmodule_recurse ...@@ -30,6 +30,7 @@ from .requests import _xmodule_recurse
from .access import has_access from .access import has_access
from xmodule.x_module import XModuleDescriptor from xmodule.x_module import XModuleDescriptor
from xblock.plugin import PluginMissingError from xblock.plugin import PluginMissingError
from xblock.runtime import Mixologist
__all__ = ['OPEN_ENDED_COMPONENT_TYPES', __all__ = ['OPEN_ENDED_COMPONENT_TYPES',
'ADVANCED_COMPONENT_POLICY_KEY', 'ADVANCED_COMPONENT_POLICY_KEY',
...@@ -91,7 +92,7 @@ def edit_subsection(request, location): ...@@ -91,7 +92,7 @@ def edit_subsection(request, location):
# we're for now assuming a single parent # we're for now assuming a single parent
if len(parent_locs) != 1: if len(parent_locs) != 1:
logging.error( logging.error(
'Multiple (or none) parents have been found for %', 'Multiple (or none) parents have been found for %s',
location location
) )
...@@ -99,12 +100,14 @@ def edit_subsection(request, location): ...@@ -99,12 +100,14 @@ def edit_subsection(request, location):
parent = modulestore().get_item(parent_locs[0]) parent = modulestore().get_item(parent_locs[0])
# remove all metadata from the generic dictionary that is presented in a # remove all metadata from the generic dictionary that is presented in a
# more normalized UI # more normalized UI. We only want to display the XBlocks fields, not
# the fields from any mixins that have been added
fields = getattr(item, 'unmixed_class', item.__class__).fields
policy_metadata = dict( policy_metadata = dict(
(field.name, field.read_from(item)) (field.name, field.read_from(item))
for field for field
in item.fields in fields.values()
if field.name not in ['display_name', 'start', 'due', 'format'] if field.name not in ['display_name', 'start', 'due', 'format']
and field.scope == Scope.settings and field.scope == Scope.settings
) )
...@@ -135,6 +138,15 @@ def edit_subsection(request, location): ...@@ -135,6 +138,15 @@ def edit_subsection(request, location):
) )
def load_mixed_class(category):
"""
Load an XBlock by category name, and apply all defined mixins
"""
component_class = XModuleDescriptor.load_class(category)
mixologist = Mixologist(settings.XBLOCK_MIXINS)
return mixologist.mix(component_class)
@login_required @login_required
def edit_unit(request, location): def edit_unit(request, location):
""" """
...@@ -163,22 +175,29 @@ def edit_unit(request, location): ...@@ -163,22 +175,29 @@ def edit_unit(request, location):
component_templates = defaultdict(list) component_templates = defaultdict(list)
for category in COMPONENT_TYPES: for category in COMPONENT_TYPES:
component_class = XModuleDescriptor.load_class(category) component_class = load_mixed_class(category)
# add the default template # add the default template
# TODO: Once mixins are defined per-application, rather than per-runtime,
# this should use a cms mixed-in class. (cpennington)
if hasattr(component_class, 'display_name'):
display_name = component_class.display_name.default or 'Blank'
else:
display_name = 'Blank'
component_templates[category].append(( component_templates[category].append((
component_class.display_name.default or 'Blank', display_name,
category, category,
False, # No defaults have markdown (hardcoded current default) False, # No defaults have markdown (hardcoded current default)
None # no boilerplate for overrides None # no boilerplate for overrides
)) ))
# add boilerplates # add boilerplates
for template in component_class.templates(): if hasattr(component_class, 'templates'):
component_templates[category].append(( for template in component_class.templates():
template['metadata'].get('display_name'), component_templates[category].append((
category, template['metadata'].get('display_name'),
template['metadata'].get('markdown') is not None, category,
template.get('template_id') template['metadata'].get('markdown') is not None,
)) template.get('template_id')
))
# Check if there are any advanced modules specified in the course policy. # 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 # These modules should be specified as a list of strings, where the strings
...@@ -194,7 +213,7 @@ def edit_unit(request, location): ...@@ -194,7 +213,7 @@ def edit_unit(request, location):
# class? i.e., can an advanced have more than one entry in the # class? i.e., can an advanced have more than one entry in the
# menu? one for default and others for prefilled boilerplates? # menu? one for default and others for prefilled boilerplates?
try: try:
component_class = XModuleDescriptor.load_class(category) component_class = load_mixed_class(category)
component_templates['advanced'].append(( component_templates['advanced'].append((
component_class.display_name.default or category, component_class.display_name.default or category,
...@@ -272,13 +291,17 @@ def edit_unit(request, location): ...@@ -272,13 +291,17 @@ def edit_unit(request, location):
'draft_preview_link': preview_lms_link, 'draft_preview_link': preview_lms_link,
'published_preview_link': lms_link, 'published_preview_link': lms_link,
'subsection': containing_subsection, 'subsection': containing_subsection,
'release_date': get_default_time_display(containing_subsection.lms.start) 'release_date': (
if containing_subsection.lms.start is not None else None, get_default_time_display(containing_subsection.start)
if containing_subsection.start is not None else None
),
'section': containing_section, 'section': containing_section,
'new_unit_category': 'vertical', 'new_unit_category': 'vertical',
'unit_state': unit_state, 'unit_state': unit_state,
'published_date': get_default_time_display(item.cms.published_date) 'published_date': (
if item.cms.published_date is not None else None get_default_time_display(item.published_date)
if item.published_date is not None else None
),
}) })
......
...@@ -124,29 +124,33 @@ def create_new_course(request): ...@@ -124,29 +124,33 @@ def create_new_course(request):
pass pass
if existing_course is not None: if existing_course is not None:
return JsonResponse({ return JsonResponse({
'ErrMsg': _('There is already a course defined with the same ' 'ErrMsg': _('There is already a course defined with the same '
'organization, course number, and course run. Please ' 'organization, course number, and course run. Please '
'change either organization or course number to be ' 'change either organization or course number to be '
'unique.'), 'unique.'),
'OrgErrMsg': _('Please change either the organization or ' 'OrgErrMsg': _('Please change either the organization or '
'course number so that it is unique.'), 'course number so that it is unique.'),
'CourseErrMsg': _('Please change either the organization or ' 'CourseErrMsg': _('Please change either the organization or '
'course number so that it is unique.'), 'course number so that it is unique.'),
}) })
course_search_location = ['i4x', dest_location.org, dest_location.course, course_search_location = [
'course', None 'i4x',
dest_location.org,
dest_location.course,
'course',
None
] ]
courses = modulestore().get_items(course_search_location) courses = modulestore().get_items(course_search_location)
if len(courses) > 0: if len(courses) > 0:
return JsonResponse({ return JsonResponse({
'ErrMsg': _('There is already a course defined with the same ' 'ErrMsg': _('There is already a course defined with the same '
'organization and course number. Please ' 'organization and course number. Please '
'change at least one field to be unique.'), 'change at least one field to be unique.'),
'OrgErrMsg': _('Please change either the organization or ' 'OrgErrMsg': _('Please change either the organization or '
'course number so that it is unique.'), 'course number so that it is unique.'),
'CourseErrMsg': _('Please change either the organization or ' 'CourseErrMsg': _('Please change either the organization or '
'course number so that it is unique.'), 'course number so that it is unique.'),
}) })
# instantiate the CourseDescriptor and then persist it # instantiate the CourseDescriptor and then persist it
...@@ -156,15 +160,15 @@ def create_new_course(request): ...@@ -156,15 +160,15 @@ def create_new_course(request):
else: else:
metadata = {'display_name': display_name} metadata = {'display_name': display_name}
modulestore('direct').create_and_save_xmodule( modulestore('direct').create_and_save_xmodule(
dest_location, dest_location,
metadata=metadata metadata=metadata
) )
new_course = modulestore('direct').get_item(dest_location) new_course = modulestore('direct').get_item(dest_location)
# clone a default 'about' overview module as well # clone a default 'about' overview module as well
dest_about_location = dest_location.replace( dest_about_location = dest_location.replace(
category='about', category='about',
name='overview' name='overview'
) )
overview_template = AboutDescriptor.get_template('overview.yaml') overview_template = AboutDescriptor.get_template('overview.yaml')
modulestore('direct').create_and_save_xmodule( modulestore('direct').create_and_save_xmodule(
...@@ -203,13 +207,16 @@ def course_info(request, org, course, name, provided_id=None): ...@@ -203,13 +207,16 @@ def course_info(request, org, course, name, provided_id=None):
# get current updates # get current updates
location = Location(['i4x', org, course, 'course_info', "updates"]) location = Location(['i4x', org, course, 'course_info', "updates"])
return render_to_response('course_info.html', { return render_to_response(
'context_course': course_module, 'course_info.html',
'url_base': "/" + org + "/" + course + "/", {
'course_updates': json.dumps(get_course_updates(location)), 'context_course': course_module,
'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url(), 'url_base': "/" + org + "/" + course + "/",
'base_asset_url': StaticContent.get_base_url_path_for_course_assets(location) + '/'}) 'course_updates': json.dumps(get_course_updates(location)),
'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url(),
'base_asset_url': StaticContent.get_base_url_path_for_course_assets(location) + '/'
}
)
@expect_json @expect_json
@require_http_methods(("GET", "POST", "PUT", "DELETE")) @require_http_methods(("GET", "POST", "PUT", "DELETE"))
...@@ -245,7 +252,7 @@ def course_info_updates(request, org, course, provided_id=None): ...@@ -245,7 +252,7 @@ def course_info_updates(request, org, course, provided_id=None):
content_type="text/plain" content_type="text/plain"
) )
# can be either and sometimes django is rewriting one to the other: # can be either and sometimes django is rewriting one to the other:
elif request.method in ('POST', 'PUT'): elif request.method in ('POST', 'PUT'):
try: try:
return JsonResponse(update_course_updates(location, request.POST, provided_id)) return JsonResponse(update_course_updates(location, request.POST, provided_id))
except: except:
...@@ -380,7 +387,7 @@ def course_grader_updates(request, org, course, name, grader_index=None): ...@@ -380,7 +387,7 @@ def course_grader_updates(request, org, course, name, grader_index=None):
if request.method == 'GET': if request.method == 'GET':
# Cannot just do a get w/o knowing the course name :-( # Cannot just do a get w/o knowing the course name :-(
return JsonResponse(CourseGradingModel.fetch_grader( return JsonResponse(CourseGradingModel.fetch_grader(
Location(location), grader_index Location(location), grader_index
)) ))
elif request.method == "DELETE": elif request.method == "DELETE":
# ??? Should this return anything? Perhaps success fail? # ??? Should this return anything? Perhaps success fail?
...@@ -388,8 +395,8 @@ def course_grader_updates(request, org, course, name, grader_index=None): ...@@ -388,8 +395,8 @@ def course_grader_updates(request, org, course, name, grader_index=None):
return JsonResponse() return JsonResponse()
else: # post or put, doesn't matter. else: # post or put, doesn't matter.
return JsonResponse(CourseGradingModel.update_grader_from_json( return JsonResponse(CourseGradingModel.update_grader_from_json(
Location(location), Location(location),
request.POST request.POST
)) ))
...@@ -411,8 +418,8 @@ def course_advanced_updates(request, org, course, name): ...@@ -411,8 +418,8 @@ def course_advanced_updates(request, org, course, name):
return JsonResponse(CourseMetadata.fetch(location)) return JsonResponse(CourseMetadata.fetch(location))
elif request.method == 'DELETE': elif request.method == 'DELETE':
return JsonResponse(CourseMetadata.delete_key( return JsonResponse(CourseMetadata.delete_key(
location, location,
json.loads(request.body) json.loads(request.body)
)) ))
else: else:
# NOTE: request.POST is messed up because expect_json # NOTE: request.POST is messed up because expect_json
...@@ -479,9 +486,9 @@ def course_advanced_updates(request, org, course, name): ...@@ -479,9 +486,9 @@ def course_advanced_updates(request, org, course, name):
filter_tabs = False filter_tabs = False
try: try:
return JsonResponse(CourseMetadata.update_from_json( return JsonResponse(CourseMetadata.update_from_json(
location, location,
request_body, request_body,
filter_tabs=filter_tabs filter_tabs=filter_tabs
)) ))
except (TypeError, ValueError) as err: except (TypeError, ValueError) as err:
return HttpResponseBadRequest( return HttpResponseBadRequest(
...@@ -585,8 +592,8 @@ def textbook_index(request, org, course, name): ...@@ -585,8 +592,8 @@ def textbook_index(request, org, course, name):
# MongoKeyValueStore before we update the mongo datastore. # MongoKeyValueStore before we update the mongo datastore.
course_module.save() course_module.save()
store.update_metadata( store.update_metadata(
course_module.location, course_module.location,
own_metadata(course_module) own_metadata(course_module)
) )
return JsonResponse(course_module.pdf_textbooks) return JsonResponse(course_module.pdf_textbooks)
else: else:
......
...@@ -58,13 +58,13 @@ def save_item(request): ...@@ -58,13 +58,13 @@ def save_item(request):
# 'apply' the submitted metadata, so we don't end up deleting system metadata # 'apply' the submitted metadata, so we don't end up deleting system metadata
existing_item = modulestore().get_item(item_location) existing_item = modulestore().get_item(item_location)
for metadata_key in request.POST.get('nullout', []): for metadata_key in request.POST.get('nullout', []):
_get_xblock_field(existing_item, metadata_key).write_to(existing_item, None) setattr(existing_item, metadata_key, None)
# update existing metadata with submitted metadata (which can be partial) # update existing metadata with submitted metadata (which can be partial)
# IMPORTANT NOTE: if the client passed 'null' (None) for a piece of metadata that means 'remove it'. If # IMPORTANT NOTE: if the client passed 'null' (None) for a piece of metadata that means 'remove it'. If
# the intent is to make it None, use the nullout field # the intent is to make it None, use the nullout field
for metadata_key, value in request.POST.get('metadata', {}).items(): for metadata_key, value in request.POST.get('metadata', {}).items():
field = _get_xblock_field(existing_item, metadata_key) field = existing_item.fields[metadata_key]
if value is None: if value is None:
field.delete_from(existing_item) field.delete_from(existing_item)
...@@ -80,16 +80,6 @@ def save_item(request): ...@@ -80,16 +80,6 @@ def save_item(request):
return JsonResponse() return JsonResponse()
def _get_xblock_field(xblock, field_name):
"""
A temporary function to get the xblock field either from the xblock or one of its namespaces by name.
:param xblock:
:param field_name:
"""
for field in xblock.iterfields():
if field.name == field_name:
return field
@login_required @login_required
@expect_json @expect_json
def create_item(request): def create_item(request):
......
...@@ -2,6 +2,7 @@ import logging ...@@ -2,6 +2,7 @@ import logging
import sys import sys
from functools import partial from functools import partial
from django.conf import settings
from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
...@@ -11,12 +12,12 @@ from xmodule_modifiers import replace_static_urls, wrap_xmodule, save_module # ...@@ -11,12 +12,12 @@ from xmodule_modifiers import replace_static_urls, wrap_xmodule, save_module #
from xmodule.error_module import ErrorDescriptor from xmodule.error_module import ErrorDescriptor
from xmodule.errortracker import exc_info_to_str from xmodule.errortracker import exc_info_to_str
from xmodule.exceptions import NotFoundError, ProcessingError from xmodule.exceptions import NotFoundError, ProcessingError
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.mongo import MongoUsage
from xmodule.x_module import ModuleSystem from xmodule.x_module import ModuleSystem
from xblock.runtime import DbModel from xblock.runtime import DbModel
from lms.xblock.field_data import lms_field_data
from util.sandboxing import can_execute_unsafe_code from util.sandboxing import can_execute_unsafe_code
import static_replace import static_replace
...@@ -97,14 +98,10 @@ def preview_module_system(request, preview_id, descriptor): ...@@ -97,14 +98,10 @@ def preview_module_system(request, preview_id, descriptor):
descriptor: An XModuleDescriptor descriptor: An XModuleDescriptor
""" """
def preview_model_data(descriptor): def preview_field_data(descriptor):
"Helper method to create a DbModel from a descriptor" "Helper method to create a DbModel from a descriptor"
return DbModel( student_data = DbModel(SessionKeyValueStore(request))
SessionKeyValueStore(request, descriptor._model_data), return lms_field_data(descriptor._field_data, student_data)
descriptor.module_class,
preview_id,
MongoUsage(preview_id, descriptor.location.url()),
)
course_id = get_course_for_item(descriptor.location).location.course_id course_id = get_course_for_item(descriptor.location).location.course_id
...@@ -118,8 +115,9 @@ def preview_module_system(request, preview_id, descriptor): ...@@ -118,8 +115,9 @@ def preview_module_system(request, preview_id, descriptor):
debug=True, debug=True,
replace_urls=partial(static_replace.replace_static_urls, data_directory=None, course_id=course_id), replace_urls=partial(static_replace.replace_static_urls, data_directory=None, course_id=course_id),
user=request.user, user=request.user,
xblock_model_data=preview_model_data, xblock_field_data=preview_field_data,
can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)), can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)),
mixins=settings.XBLOCK_MIXINS,
) )
......
from xblock.runtime import KeyValueStore, InvalidScopeError """
An :class:`~xblock.runtime.KeyValueStore` that stores data in the django session
"""
from xblock.runtime import KeyValueStore
class SessionKeyValueStore(KeyValueStore): class SessionKeyValueStore(KeyValueStore):
def __init__(self, request, descriptor_model_data): def __init__(self, request):
self._descriptor_model_data = descriptor_model_data
self._session = request.session self._session = request.session
def get(self, key): def get(self, key):
try: return self._session[tuple(key)]
return self._descriptor_model_data[key.field_name]
except (KeyError, InvalidScopeError):
return self._session[tuple(key)]
def set(self, key, value): def set(self, key, value):
try: self._session[tuple(key)] = value
self._descriptor_model_data[key.field_name] = value
except (KeyError, InvalidScopeError):
self._session[tuple(key)] = value
def delete(self, key): def delete(self, key):
try: del self._session[tuple(key)]
del self._descriptor_model_data[key.field_name]
except (KeyError, InvalidScopeError):
del self._session[tuple(key)]
def has(self, key): def has(self, key):
return key.field_name in self._descriptor_model_data or tuple(key) in self._session return tuple(key) in self._session
...@@ -125,7 +125,7 @@ class CourseGradingModel(object): ...@@ -125,7 +125,7 @@ class CourseGradingModel(object):
# Save the data that we've just changed to the underlying # Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore. # MongoKeyValueStore before we update the mongo datastore.
descriptor.save() descriptor.save()
get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data) get_modulestore(course_location).update_item(course_location, descriptor._field_data._kvs._data)
return CourseGradingModel.jsonize_grader(index, descriptor.raw_grader[index]) return CourseGradingModel.jsonize_grader(index, descriptor.raw_grader[index])
...@@ -144,7 +144,7 @@ class CourseGradingModel(object): ...@@ -144,7 +144,7 @@ class CourseGradingModel(object):
# Save the data that we've just changed to the underlying # Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore. # MongoKeyValueStore before we update the mongo datastore.
descriptor.save() descriptor.save()
get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data) get_modulestore(course_location).update_item(course_location, descriptor._field_data._kvs._data)
return cutoffs return cutoffs
...@@ -168,12 +168,12 @@ class CourseGradingModel(object): ...@@ -168,12 +168,12 @@ class CourseGradingModel(object):
grace_timedelta = timedelta(**graceperiodjson) grace_timedelta = timedelta(**graceperiodjson)
descriptor = get_modulestore(course_location).get_item(course_location) descriptor = get_modulestore(course_location).get_item(course_location)
descriptor.lms.graceperiod = grace_timedelta descriptor.graceperiod = grace_timedelta
# Save the data that we've just changed to the underlying # Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore. # MongoKeyValueStore before we update the mongo datastore.
descriptor.save() descriptor.save()
get_modulestore(course_location).update_metadata(course_location, descriptor._model_data._kvs._metadata) get_modulestore(course_location).update_metadata(course_location, descriptor._field_data._kvs._metadata)
@staticmethod @staticmethod
def delete_grader(course_location, index): def delete_grader(course_location, index):
...@@ -193,7 +193,7 @@ class CourseGradingModel(object): ...@@ -193,7 +193,7 @@ class CourseGradingModel(object):
# Save the data that we've just changed to the underlying # Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore. # MongoKeyValueStore before we update the mongo datastore.
descriptor.save() descriptor.save()
get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data) get_modulestore(course_location).update_item(course_location, descriptor._field_data._kvs._data)
@staticmethod @staticmethod
def delete_grace_period(course_location): def delete_grace_period(course_location):
...@@ -204,12 +204,12 @@ class CourseGradingModel(object): ...@@ -204,12 +204,12 @@ class CourseGradingModel(object):
course_location = Location(course_location) course_location = Location(course_location)
descriptor = get_modulestore(course_location).get_item(course_location) descriptor = get_modulestore(course_location).get_item(course_location)
del descriptor.lms.graceperiod del descriptor.graceperiod
# Save the data that we've just changed to the underlying # Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore. # MongoKeyValueStore before we update the mongo datastore.
descriptor.save() descriptor.save()
get_modulestore(course_location).update_metadata(course_location, descriptor._model_data._kvs._metadata) get_modulestore(course_location).update_metadata(course_location, descriptor._field_data._kvs._metadata)
@staticmethod @staticmethod
def get_section_grader_type(location): def get_section_grader_type(location):
...@@ -217,7 +217,7 @@ class CourseGradingModel(object): ...@@ -217,7 +217,7 @@ class CourseGradingModel(object):
location = Location(location) location = Location(location)
descriptor = get_modulestore(location).get_item(location) descriptor = get_modulestore(location).get_item(location)
return {"graderType": descriptor.lms.format if descriptor.lms.format is not None else 'Not Graded', return {"graderType": descriptor.format if descriptor.format is not None else 'Not Graded',
"location": location, "location": location,
"id": 99 # just an arbitrary value to "id": 99 # just an arbitrary value to
} }
...@@ -229,21 +229,21 @@ class CourseGradingModel(object): ...@@ -229,21 +229,21 @@ class CourseGradingModel(object):
descriptor = get_modulestore(location).get_item(location) descriptor = get_modulestore(location).get_item(location)
if 'graderType' in jsondict and jsondict['graderType'] != u"Not Graded": if 'graderType' in jsondict and jsondict['graderType'] != u"Not Graded":
descriptor.lms.format = jsondict.get('graderType') descriptor.format = jsondict.get('graderType')
descriptor.lms.graded = True descriptor.graded = True
else: else:
del descriptor.lms.format del descriptor.format
del descriptor.lms.graded del descriptor.graded
# Save the data that we've just changed to the underlying # Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore. # MongoKeyValueStore before we update the mongo datastore.
descriptor.save() descriptor.save()
get_modulestore(location).update_metadata(location, descriptor._model_data._kvs._metadata) get_modulestore(location).update_metadata(location, descriptor._field_data._kvs._metadata)
@staticmethod @staticmethod
def convert_set_grace_period(descriptor): def convert_set_grace_period(descriptor):
# 5 hours 59 minutes 59 seconds => converted to iso format # 5 hours 59 minutes 59 seconds => converted to iso format
rawgrace = descriptor.lms.graceperiod rawgrace = descriptor.graceperiod
if rawgrace: if rawgrace:
hours_from_days = rawgrace.days * 24 hours_from_days = rawgrace.days * 24
seconds = rawgrace.seconds seconds = rawgrace.seconds
......
from xmodule.modulestore import Location from xmodule.modulestore import Location
from contentstore.utils import get_modulestore from contentstore.utils import get_modulestore
from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.inheritance import own_metadata
from xblock.core import Scope from xblock.fields import Scope
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
import copy from cms.xmodule_namespace import CmsBlockMixin
class CourseMetadata(object): class CourseMetadata(object):
...@@ -35,12 +35,17 @@ class CourseMetadata(object): ...@@ -35,12 +35,17 @@ class CourseMetadata(object):
descriptor = get_modulestore(course_location).get_item(course_location) descriptor = get_modulestore(course_location).get_item(course_location)
for field in descriptor.fields + descriptor.lms.fields: for field in descriptor.fields.values():
if field.name in CmsBlockMixin.fields:
continue
if field.scope != Scope.settings: if field.scope != Scope.settings:
continue continue
if field.name not in cls.FILTERED_LIST: if field.name in cls.FILTERED_LIST:
course[field.name] = field.read_json(descriptor) continue
course[field.name] = field.read_json(descriptor)
return course return course
...@@ -55,9 +60,9 @@ class CourseMetadata(object): ...@@ -55,9 +60,9 @@ class CourseMetadata(object):
dirty = False dirty = False
#Copy the filtered list to avoid permanently changing the class attribute # Copy the filtered list to avoid permanently changing the class attribute.
filtered_list = copy.copy(cls.FILTERED_LIST) filtered_list = list(cls.FILTERED_LIST)
#Don't filter on the tab attribute if filter_tabs is False # Don't filter on the tab attribute if filter_tabs is False.
if not filter_tabs: if not filter_tabs:
filtered_list.remove("tabs") filtered_list.remove("tabs")
...@@ -68,12 +73,8 @@ class CourseMetadata(object): ...@@ -68,12 +73,8 @@ class CourseMetadata(object):
if hasattr(descriptor, key) and getattr(descriptor, key) != val: if hasattr(descriptor, key) and getattr(descriptor, key) != val:
dirty = True dirty = True
value = getattr(CourseDescriptor, key).from_json(val) value = descriptor.fields[key].from_json(val)
setattr(descriptor, key, value) setattr(descriptor, key, value)
elif hasattr(descriptor.lms, key) and getattr(descriptor.lms, key) != key:
dirty = True
value = getattr(CourseDescriptor.lms, key).from_json(val)
setattr(descriptor.lms, key, value)
if dirty: if dirty:
# Save the data that we've just changed to the underlying # Save the data that we've just changed to the underlying
...@@ -97,8 +98,6 @@ class CourseMetadata(object): ...@@ -97,8 +98,6 @@ class CourseMetadata(object):
for key in payload['deleteKeys']: for key in payload['deleteKeys']:
if hasattr(descriptor, key): if hasattr(descriptor, key):
delattr(descriptor, key) delattr(descriptor, key)
elif hasattr(descriptor.lms, key):
delattr(descriptor.lms, key)
# Save the data that we've just changed to the underlying # Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore. # MongoKeyValueStore before we update the mongo datastore.
......
...@@ -68,8 +68,8 @@ CONTENTSTORE = { ...@@ -68,8 +68,8 @@ CONTENTSTORE = {
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.sqlite3',
'NAME': TEST_ROOT / "db" / "test_mitx_%s.db" % seed(), 'NAME': TEST_ROOT / "db" / "test_edx.db",
'TEST_NAME': TEST_ROOT / "db" / "test_mitx_%s.db" % seed(), 'TEST_NAME': TEST_ROOT / "db" / "test_edx.db"
} }
} }
......
"""
This config file extends the test environment configuration
so that we can run the lettuce acceptance tests.
This is used in the django-admin call as acceptance.py
contains random seeding, causing django-admin to create a random collection
"""
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .test import *
# You need to start the server in debug mode,
# otherwise the browser will not render the pages correctly
DEBUG = True
# Disable warnings for acceptance tests, to make the logs readable
import logging
logging.disable(logging.ERROR)
import os
import random
MODULESTORE_OPTIONS = {
'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost',
'db': 'acceptance_xmodule',
'collection': 'acceptance_modulestore',
'fs_root': TEST_ROOT / "data",
'render_template': 'mitxmako.shortcuts.render_to_string',
}
MODULESTORE = {
'default': {
'ENGINE': 'xmodule.modulestore.draft.DraftModuleStore',
'OPTIONS': MODULESTORE_OPTIONS
},
'direct': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': MODULESTORE_OPTIONS
},
'draft': {
'ENGINE': 'xmodule.modulestore.draft.DraftModuleStore',
'OPTIONS': MODULESTORE_OPTIONS
}
}
CONTENTSTORE = {
'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore',
'OPTIONS': {
'host': 'localhost',
'db': 'acceptance_xcontent',
},
# allow for additional options that can be keyed on a name, e.g. 'trashcan'
'ADDITIONAL_OPTIONS': {
'trashcan': {
'bucket': 'trash_fs'
}
}
}
# Set this up so that rake lms[acceptance] and running the
# harvest command both use the same (test) database
# which they can flush without messing up your dev db
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': TEST_ROOT / "db" / "test_mitx.db",
'TEST_NAME': TEST_ROOT / "db" / "test_mitx.db",
}
}
# Include the lettuce app for acceptance testing, including the 'harvest' django-admin command
INSTALLED_APPS += ('lettuce.django',)
LETTUCE_APPS = ('contentstore',)
LETTUCE_SERVER_PORT = random.randint(1024, 65535)
LETTUCE_BROWSER = 'chrome'
...@@ -28,6 +28,10 @@ import lms.envs.common ...@@ -28,6 +28,10 @@ import lms.envs.common
from lms.envs.common import USE_TZ, TECH_SUPPORT_EMAIL, PLATFORM_NAME, BUGS_EMAIL from lms.envs.common import USE_TZ, TECH_SUPPORT_EMAIL, PLATFORM_NAME, BUGS_EMAIL
from path import path from path import path
from lms.xblock.mixin import LmsBlockMixin
from cms.xmodule_namespace import CmsBlockMixin
from xmodule.modulestore.inheritance import InheritanceMixin
############################ FEATURE CONFIGURATION ############################# ############################ FEATURE CONFIGURATION #############################
MITX_FEATURES = { MITX_FEATURES = {
...@@ -55,7 +59,7 @@ MITX_FEATURES = { ...@@ -55,7 +59,7 @@ MITX_FEATURES = {
# If set to True, new Studio users won't be able to author courses unless # If set to True, new Studio users won't be able to author courses unless
# edX has explicitly added them to the course creator group. # edX has explicitly added them to the course creator group.
'ENABLE_CREATOR_GROUP': False 'ENABLE_CREATOR_GROUP': False,
} }
ENABLE_JASMINE = False ENABLE_JASMINE = False
...@@ -160,6 +164,13 @@ MIDDLEWARE_CLASSES = ( ...@@ -160,6 +164,13 @@ MIDDLEWARE_CLASSES = (
'ratelimitbackend.middleware.RateLimitMiddleware', 'ratelimitbackend.middleware.RateLimitMiddleware',
) )
############# XBlock Configuration ##########
# This should be moved into an XBlock Runtime/Application object
# once the responsibility of XBlock creation is moved out of modulestore - cpennington
XBLOCK_MIXINS = (LmsBlockMixin, CmsBlockMixin, InheritanceMixin)
############################ SIGNAL HANDLERS ################################ ############################ SIGNAL HANDLERS ################################
# This is imported to register the exception signal handling that logs exceptions # This is imported to register the exception signal handling that logs exceptions
import monitoring.exceptions # noqa import monitoring.exceptions # noqa
......
""" """
Module with code executed during Studio startup Module with code executed during Studio startup
""" """
import logging
from django.conf import settings from django.conf import settings
# Force settings to run so that the python path is modified # Force settings to run so that the python path is modified
...@@ -8,6 +9,8 @@ settings.INSTALLED_APPS # pylint: disable=W0104 ...@@ -8,6 +9,8 @@ settings.INSTALLED_APPS # pylint: disable=W0104
from django_startup import autostartup from django_startup import autostartup
log = logging.getLogger(__name__)
# TODO: Remove this code once Studio/CMS runs via wsgi in all environments # TODO: Remove this code once Studio/CMS runs via wsgi in all environments
INITIALIZED = False INITIALIZED = False
...@@ -22,4 +25,3 @@ def run(): ...@@ -22,4 +25,3 @@ def run():
INITIALIZED = True INITIALIZED = True
autostartup() autostartup()
<li class="field-group course-advanced-policy-list-item">
<div class="field is-not-editable text key" id="<%= key %>">
<label for="<%= keyUniqueId %>">Policy Key:</label>
<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>
</li>
\ No newline at end of file
<!-- In order to enable better debugging of templates, put them in
the script tag section.
TODO add lazy load fn to load templates as needed (called
from backbone view initialize to set this.template of the view)
-->
<%block name="jsextra">
<script type="text/javascript" charset="utf-8">
// How do I load an html file server side so I can
// Precompiling your templates can be a big help when debugging errors you can't reproduce. This is because precompiled templates can provide line numbers and a stack trace, something that is not possible when compiling templates on the client. The source property is available on the compiled template function for easy precompilation.
// <script>CMS.course_info_update = <%= _.template(jstText).source %>;</script>
</script>
</%block>
\ No newline at end of file
...@@ -26,7 +26,6 @@ describe "Test Metadata Editor", -> ...@@ -26,7 +26,6 @@ describe "Test Metadata Editor", ->
explicitly_set: true, explicitly_set: true,
field_name: "display_name", field_name: "display_name",
help: "Specifies the name for this component.", help: "Specifies the name for this component.",
inheritable: false,
options: [], options: [],
type: CMS.Models.Metadata.GENERIC_TYPE, type: CMS.Models.Metadata.GENERIC_TYPE,
value: "Word cloud" value: "Word cloud"
...@@ -38,7 +37,6 @@ describe "Test Metadata Editor", -> ...@@ -38,7 +37,6 @@ describe "Test Metadata Editor", ->
explicitly_set: false, explicitly_set: false,
field_name: "show_answer", field_name: "show_answer",
help: "When should you show the answer", help: "When should you show the answer",
inheritable: true,
options: [ options: [
{"display_name": "Always", "value": "always"}, {"display_name": "Always", "value": "always"},
{"display_name": "Answered", "value": "answered"}, {"display_name": "Answered", "value": "answered"},
...@@ -54,7 +52,6 @@ describe "Test Metadata Editor", -> ...@@ -54,7 +52,6 @@ describe "Test Metadata Editor", ->
explicitly_set: false, explicitly_set: false,
field_name: "num_inputs", field_name: "num_inputs",
help: "Number of text boxes for student to input words/sentences.", help: "Number of text boxes for student to input words/sentences.",
inheritable: false,
options: {min: 1}, options: {min: 1},
type: CMS.Models.Metadata.INTEGER_TYPE, type: CMS.Models.Metadata.INTEGER_TYPE,
value: 5 value: 5
...@@ -66,7 +63,6 @@ describe "Test Metadata Editor", -> ...@@ -66,7 +63,6 @@ describe "Test Metadata Editor", ->
explicitly_set: true, explicitly_set: true,
field_name: "weight", field_name: "weight",
help: "Weight for this problem", help: "Weight for this problem",
inheritable: true,
options: {min: 1.3, max:100.2, step:0.1}, options: {min: 1.3, max:100.2, step:0.1},
type: CMS.Models.Metadata.FLOAT_TYPE, type: CMS.Models.Metadata.FLOAT_TYPE,
value: 10.2 value: 10.2
...@@ -78,7 +74,6 @@ describe "Test Metadata Editor", -> ...@@ -78,7 +74,6 @@ describe "Test Metadata Editor", ->
explicitly_set: false, explicitly_set: false,
field_name: "list", field_name: "list",
help: "A list of things.", help: "A list of things.",
inheritable: false,
options: [], options: [],
type: CMS.Models.Metadata.LIST_TYPE, type: CMS.Models.Metadata.LIST_TYPE,
value: ["the first display value", "the second"] value: ["the first display value", "the second"]
...@@ -99,7 +94,6 @@ describe "Test Metadata Editor", -> ...@@ -99,7 +94,6 @@ describe "Test Metadata Editor", ->
explicitly_set: true, explicitly_set: true,
field_name: "unknown_type", field_name: "unknown_type",
help: "Mystery property.", help: "Mystery property.",
inheritable: false,
options: [ options: [
{"display_name": "Always", "value": "always"}, {"display_name": "Always", "value": "always"},
{"display_name": "Answered", "value": "answered"}, {"display_name": "Answered", "value": "answered"},
...@@ -145,7 +139,6 @@ describe "Test Metadata Editor", -> ...@@ -145,7 +139,6 @@ describe "Test Metadata Editor", ->
explicitly_set: false, explicitly_set: false,
field_name: "display_name", field_name: "display_name",
help: "", help: "",
inheritable: false,
options: [], options: [],
type: CMS.Models.Metadata.GENERIC_TYPE, type: CMS.Models.Metadata.GENERIC_TYPE,
value: null value: null
......
// <!-- from https://github.com/Gazler/Underscore-Template-Loader/blob/master/index.html -->
// TODO Figure out how to initialize w/ static views from server (don't call .load but instead inject in django as strings)
// so this only loads the lazily loaded ones.
(function () {
if (typeof window.templateLoader == 'function') return;
var templateLoader = {
templateVersion: "0.0.15",
templates: {},
// Control whether template caching in local memory occurs. Caching screws up development but may
// be a good optimization in production (it works fairly well).
cacheTemplates: false,
loadRemoteTemplate: function (templateName, filename, callback) {
if (!this.templates[templateName]) {
var self = this;
jQuery.ajax({url: filename,
success: function (data) {
self.addTemplate(templateName, data);
self.saveLocalTemplates();
callback(data);
},
error: function (xhdr, textStatus, errorThrown) {
console.log(textStatus);
},
dataType: "html"
})
}
else {
callback(this.templates[templateName]);
}
},
addTemplate: function (templateName, data) {
// is there a reason this doesn't go ahead and compile the template? _.template(data)
// I suppose localstorage use would still req raw string rather than compiled version, but that sd work
// if it maintains a separate cache of uncompiled ones
this.templates[templateName] = data;
},
localStorageAvailable: function () {
try {
return this.cacheTemplates && 'localStorage' in window && window['localStorage'] !== null;
} catch (e) {
return false;
}
},
saveLocalTemplates: function () {
if (this.localStorageAvailable()) {
localStorage.setItem("templates", JSON.stringify(this.templates));
localStorage.setItem("templateVersion", this.templateVersion);
}
},
loadLocalTemplates: function () {
if (this.localStorageAvailable()) {
var templateVersion = localStorage.getItem("templateVersion");
if (templateVersion && templateVersion == this.templateVersion) {
var templates = localStorage.getItem("templates");
if (templates) {
templates = JSON.parse(templates);
for (var x in templates) {
if (!this.templates[x]) {
this.addTemplate(x, templates[x]);
}
}
}
}
else {
localStorage.removeItem("templates");
localStorage.removeItem("templateVersion");
}
}
}
};
templateLoader.loadLocalTemplates();
window.templateLoader = templateLoader;
})();
...@@ -52,7 +52,29 @@ function removeAsset(e){ ...@@ -52,7 +52,29 @@ function removeAsset(e){
function showUploadModal(e) { function showUploadModal(e) {
e.preventDefault(); e.preventDefault();
resetUploadModal();
$modal = $('.upload-modal').show(); $modal = $('.upload-modal').show();
$('.upload-modal .file-chooser').fileupload({
dataType: 'json',
type: 'POST',
maxChunkSize: 100 * 1000 * 1000, // 100 MB
autoUpload: true,
progressall: function(e, data) {
var percentComplete = parseInt((100 * data.loaded) / data.total, 10);
showUploadFeedback(e, percentComplete);
},
maxFileSize: 100 * 1000 * 1000, // 100 MB
maxNumberofFiles: 100,
add: function(e, data) {
data.process().done(function () {
data.submit();
});
},
done: function(e, data) {
displayFinishedUpload(data.result);
}
});
$('.file-input').bind('change', startUpload); $('.file-input').bind('change', startUpload);
$modalCover.show(); $modalCover.show();
} }
...@@ -69,11 +91,6 @@ function startUpload(e) { ...@@ -69,11 +91,6 @@ function startUpload(e) {
$('.upload-modal h1').html(gettext('Uploading…')); $('.upload-modal h1').html(gettext('Uploading…'));
$('.upload-modal .file-name').html(files[0].name); $('.upload-modal .file-name').html(files[0].name);
$('.upload-modal .file-chooser').ajaxSubmit({
beforeSend: resetUploadBar,
uploadProgress: showUploadFeedback,
complete: displayFinishedUpload
});
$('.upload-modal .choose-file-button').hide(); $('.upload-modal .choose-file-button').hide();
$('.upload-modal .progress-bar').removeClass('loaded').show(); $('.upload-modal .progress-bar').removeClass('loaded').show();
} }
...@@ -84,18 +101,28 @@ function resetUploadBar() { ...@@ -84,18 +101,28 @@ function resetUploadBar() {
$('.upload-modal .progress-fill').html(percentVal); $('.upload-modal .progress-fill').html(percentVal);
} }
function showUploadFeedback(event, position, total, percentComplete) { function resetUploadModal() {
// Reset modal so it no longer displays information about previously
// completed uploads.
resetUploadBar();
$('.upload-modal .file-name').html('');
$('.upload-modal h1').html(gettext('Upload New File'));
$('.upload-modal .choose-file-button').html(gettext('Choose File'));
$('.upload-modal .embeddable-xml-input').val('');
$('.upload-modal .embeddable').hide();
}
function showUploadFeedback(event, percentComplete) {
var percentVal = percentComplete + '%'; var percentVal = percentComplete + '%';
$('.upload-modal .progress-fill').width(percentVal); $('.upload-modal .progress-fill').width(percentVal);
$('.upload-modal .progress-fill').html(percentVal); $('.upload-modal .progress-fill').html(percentVal);
} }
function displayFinishedUpload(xhr) { function displayFinishedUpload(resp) {
if (xhr.status == 200) { if (resp.status == 200) {
markAsLoaded(); markAsLoaded();
} }
var resp = JSON.parse(xhr.responseText);
$('.upload-modal .embeddable-xml-input').val(resp.portable_url); $('.upload-modal .embeddable-xml-input').val(resp.portable_url);
$('.upload-modal .embeddable').show(); $('.upload-modal .embeddable').show();
$('.upload-modal .file-name').hide(); $('.upload-modal .file-name').hide();
......
...@@ -10,27 +10,12 @@ CMS.Views.Checklists = Backbone.View.extend({ ...@@ -10,27 +10,12 @@ CMS.Views.Checklists = Backbone.View.extend({
}, },
initialize : function() { initialize : function() {
var self = this; this.template = _.template($("#checklist-tpl").text());
this.listenTo(this.collection, 'reset', this.render);
this.collection.fetch({ this.render();
complete: function () {
window.templateLoader.loadRemoteTemplate("checklist",
"/static/client_templates/checklist.html",
function (raw_template) {
self.template = _.template(raw_template);
self.render();
}
);
},
reset: true
}
);
}, },
render: function() { render: function() {
// catch potential outside call before template loaded
if (!this.template) return this;
this.$el.empty(); this.$el.empty();
var self = this; var self = this;
......
...@@ -184,8 +184,16 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({ ...@@ -184,8 +184,16 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
if (forcedTarget) { if (forcedTarget) {
thisTarget = forcedTarget; thisTarget = forcedTarget;
thisTarget.id = $(thisTarget).attr('id'); thisTarget.id = $(thisTarget).attr('id');
} else { } else if (e !== null) {
thisTarget = e.currentTarget; thisTarget = e.currentTarget;
} else
{
// e and forcedTarget can be null so don't deference it
// This is because in cases where we have a marketing site
// we don't display the codeMirrors for editing the marketing
// materials, except we do need to show the 'set course image'
// workflow. So in this case e = forcedTarget = null.
return;
} }
if (!this.codeMirrors[thisTarget.id]) { if (!this.codeMirrors[thisTarget.id]) {
......
...@@ -20,6 +20,7 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({ ...@@ -20,6 +20,7 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
initialize : function() { initialize : function() {
// load template for grading view // load template for grading view
var self = this; var self = this;
this.template = _.template($("#course_grade_policy-tpl").text());
this.gradeCutoffTemplate = _.template('<li class="grade-specific-bar" style="width:<%= width %>%"><span class="letter-grade" contenteditable="true">' + this.gradeCutoffTemplate = _.template('<li class="grade-specific-bar" style="width:<%= width %>%"><span class="letter-grade" contenteditable="true">' +
'<%= descriptor %>' + '<%= descriptor %>' +
'</span><span class="range"></span>' + '</span><span class="range"></span>' +
...@@ -27,27 +28,15 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({ ...@@ -27,27 +28,15 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
'</li>'); '</li>');
this.setupCutoffs(); this.setupCutoffs();
// instantiates an editor template for each update in the collection
// Because this calls render, put it after everything which render may depend upon to prevent race condition.
window.templateLoader.loadRemoteTemplate("course_grade_policy",
"/static/client_templates/course_grade_policy.html",
function (raw_template) {
self.template = _.template(raw_template);
self.render();
}
);
this.listenTo(this.model, 'invalid', this.handleValidationError); this.listenTo(this.model, 'invalid', this.handleValidationError);
this.listenTo(this.model, 'change', this.showNotificationBar); this.listenTo(this.model, 'change', this.showNotificationBar);
this.model.get('graders').on('reset', this.render, this); this.model.get('graders').on('reset', this.render, this);
this.model.get('graders').on('add', this.render, this); this.model.get('graders').on('add', this.render, this);
this.selectorToField = _.invert(this.fieldToSelectorMap); this.selectorToField = _.invert(this.fieldToSelectorMap);
this.render();
}, },
render: function() { render: function() {
// prevent bootstrap race condition by event dispatch
if (!this.template) return;
this.clearValidationErrors(); this.clearValidationErrors();
this.renderGracePeriod(); this.renderGracePeriod();
......
...@@ -8,6 +8,8 @@ ...@@ -8,6 +8,8 @@
<%block name="jsextra"> <%block name="jsextra">
<script src="${static.url('js/vendor/mustache.js')}"></script> <script src="${static.url('js/vendor/mustache.js')}"></script>
<script src="${static.url('js/vendor/jQuery-File-Upload/js/jquery.iframe-transport.js')}"> </script>
<script src="${static.url('js/vendor/jQuery-File-Upload/js/jquery.fileupload.js')}"> </script>
</%block> </%block>
<%block name="content"> <%block name="content">
...@@ -40,12 +42,12 @@ ...@@ -40,12 +42,12 @@
<div class="wrapper-mast wrapper"> <div class="wrapper-mast wrapper">
<header class="mast has-actions has-subtitle"> <header class="mast has-actions has-subtitle">
<h1 class="page-header"> <h1 class="page-header">
<small class="subtitle">Content</small> <small class="subtitle">${_("Content")}</small>
<span class="sr">&gt; </span>Files &amp; Uploads <span class="sr">&gt; </span>${_("Files &amp; Uploads")}
</h1> </h1>
<nav class="nav-actions"> <nav class="nav-actions">
<h3 class="sr">Page Actions</h3> <h3 class="sr">${_("Page Actions")}</h3>
<ul> <ul>
<li class="nav-item"> <li class="nav-item">
<a href="#" class="button upload-button new-button"><i class="icon-plus"></i> ${_("Upload New File")}</a> <a href="#" class="button upload-button new-button"><i class="icon-plus"></i> ${_("Upload New File")}</a>
...@@ -114,26 +116,26 @@ ...@@ -114,26 +116,26 @@
</div> </div>
<div class="upload-modal modal"> <div class="upload-modal modal">
<a href="#" class="close-button"><span class="close-icon"></span></a> <a href="#" class="close-button"><span class="close-icon"></span></a>
<div class="modal-body"> <div class="modal-body">
<h1>Upload New File</h1> <h1>${_("Upload New File")}</h1>
<p class="file-name"></a> <p class="file-name"></a>
<div class="progress-bar"> <div class="progress-bar">
<div class="progress-fill"></div> <div class="progress-fill"></div>
</div> </div>
<div class="embeddable"> <div class="embeddable">
<label>URL:</label> <label>URL:</label>
<input type="text" class="embeddable-xml-input" value='' readonly> <input type="text" class="embeddable-xml-input" value='' readonly>
</div> </div>
<form class="file-chooser" action="${upload_asset_callback_url}" <form class="file-chooser" action="${upload_asset_callback_url}"
method="post" enctype="multipart/form-data"> method="post" enctype="multipart/form-data">
<a href="#" class="choose-file-button">Choose File</a> <a href="#" class="choose-file-button">${_("Choose File")}</a>
<input type="file" class="file-input" name="file"> <input type="file" class="file-input" name="files[]" multiple>
</form> </form>
</div> </div>
</div> </div>
<div class="modal-cover"></div> <div class="modal-cover"></div>
</%block> </%block>
......
...@@ -5,9 +5,17 @@ ...@@ -5,9 +5,17 @@
<%block name="bodyclass">is-signedin course uxdesign checklists</%block> <%block name="bodyclass">is-signedin course uxdesign checklists</%block>
<%namespace name='static' file='static_content.html'/> <%namespace name='static' file='static_content.html'/>
<%block name="header_extras">
% for template_name in ["checklist"]:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="js/${template_name}.underscore" />
</script>
% endfor
</%block>
<%block name="jsextra"> <%block name="jsextra">
<script type="text/javascript" src="${static.url('js/template_loader.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/checklists_view.js')}"></script> <script type="text/javascript" src="${static.url('js/views/checklists_view.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/checklists.js')}"></script> <script type="text/javascript" src="${static.url('js/models/checklists.js')}"></script>
...@@ -20,7 +28,7 @@ ...@@ -20,7 +28,7 @@
el: $('.course-checklists'), el: $('.course-checklists'),
collection: checklistCollection collection: checklistCollection
}); });
checklistCollection.fetch({reset: true});
}); });
</script> </script>
......
...@@ -37,21 +37,21 @@ ...@@ -37,21 +37,21 @@
<div class="field field-start-date"> <div class="field field-start-date">
<label for="start_date">${_("Release Day")}</label> <label for="start_date">${_("Release Day")}</label>
<input type="text" id="start_date" name="start_date" <input type="text" id="start_date" name="start_date"
value="${subsection.lms.start.strftime('%m/%d/%Y') if subsection.lms.start else ''}" value="${subsection.start.strftime('%m/%d/%Y') if subsection.start else ''}"
placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/> placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
</div> </div>
<div class="field field-start-time"> <div class="field field-start-time">
<label for="start_time">${_("Release Time")} (<abbr title="${_("Coordinated Universal Time")}">${_("UTC")}</abbr>)</label> <label for="start_time">${_("Release Time")} (<abbr title="${_("Coordinated Universal Time")}">${_("UTC")}</abbr>)</label>
<input type="text" id="start_time" name="start_time" <input type="text" id="start_time" name="start_time"
value="${subsection.lms.start.strftime('%H:%M') if subsection.lms.start else ''}" value="${subsection.start.strftime('%H:%M') if subsection.start else ''}"
placeholder="HH:MM" class="time" size='10' autocomplete="off"/> placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
</div> </div>
</div> </div>
% if subsection.lms.start and not almost_same_datetime(subsection.lms.start, parent_item.lms.start): % if subsection.start and not almost_same_datetime(subsection.start, parent_item.start):
% if parent_item.lms.start is None: % if parent_item.start is None:
<p class="notice">${_("The date above differs from the release date of {name}, which is unset.").format(name=parent_item.display_name_with_default)} <p class="notice">${_("The date above differs from the release date of {name}, which is unset.").format(name=parent_item.display_name_with_default)}
% else: % else:
<p class="notice">${_("The date above differs from the release date of {name} - {start_time}").format(name=parent_item.display_name_with_default, start_time=get_default_time_display(parent_item.lms.start))}. <p class="notice">${_("The date above differs from the release date of {name} - {start_time}").format(name=parent_item.display_name_with_default, start_time=get_default_time_display(parent_item.start))}.
% endif % endif
<a href="#" class="sync-date no-spinner">${_("Sync to {name}.").format(name=parent_item.display_name_with_default)}</a></p> <a href="#" class="sync-date no-spinner">${_("Sync to {name}.").format(name=parent_item.display_name_with_default)}</a></p>
% endif % endif
...@@ -60,7 +60,7 @@ ...@@ -60,7 +60,7 @@
<div class="row gradable"> <div class="row gradable">
<label>${_("Graded as:")}</label> <label>${_("Graded as:")}</label>
<div class="gradable-status" data-initial-status="${subsection.lms.format if subsection.lms.format is not None else _('Not Graded')}"> <div class="gradable-status" data-initial-status="${subsection.format if subsection.format is not None else _('Not Graded')}">
</div> </div>
<div class="due-date-input row"> <div class="due-date-input row">
...@@ -69,13 +69,13 @@ ...@@ -69,13 +69,13 @@
<div class="field field-start-date"> <div class="field field-start-date">
<label for="due_date">${_("Due Day")}</label> <label for="due_date">${_("Due Day")}</label>
<input type="text" id="due_date" name="due_date" <input type="text" id="due_date" name="due_date"
value="${subsection.lms.due.strftime('%m/%d/%Y') if subsection.lms.due else ''}" value="${subsection.due.strftime('%m/%d/%Y') if subsection.due else ''}"
placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/> placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
</div> </div>
<div class="field field-start-time"> <div class="field field-start-time">
<label for="due_time">${_("Due Time")} (<abbr title="${_('Coordinated Universal Time')}">UTC</abbr>)</label> <label for="due_time">${_("Due Time")} (<abbr title="${_('Coordinated Universal Time')}">UTC</abbr>)</label>
<input type="text" id="due_time" name="due_time" <input type="text" id="due_time" name="due_time"
value="${subsection.lms.due.strftime('%H:%M') if subsection.lms.due else ''}" value="${subsection.due.strftime('%H:%M') if subsection.due else ''}"
placeholder="HH:MM" class="time" size='10' autocomplete="off"/> placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
</div> </div>
<a href="#" class="remove-date">${_("Remove due date")}</a> <a href="#" class="remove-date">${_("Remove due date")}</a>
......
...@@ -72,7 +72,7 @@ $('#fileupload').fileupload({ ...@@ -72,7 +72,7 @@ $('#fileupload').fileupload({
add: function(e, data) { add: function(e, data) {
submitBtn.unbind('click'); submitBtn.unbind('click');
var file = data.files[0]; var file = data.files[0];
if (file.type == "application/x-gzip") { if (file.name.match(/tar\.gz$/)) {
submitBtn.click(function(e){ submitBtn.click(function(e){
e.preventDefault(); e.preventDefault();
submitBtn.hide(); submitBtn.hide();
......
...@@ -157,19 +157,19 @@ ...@@ -157,19 +157,19 @@
<h3 class="section-name" data-name="${section.display_name_with_default | h}"></h3> <h3 class="section-name" data-name="${section.display_name_with_default | h}"></h3>
<div class="section-published-date"> <div class="section-published-date">
<% <%
if section.lms.start is not None: if section.start is not None:
start_date_str = section.lms.start.strftime('%m/%d/%Y') start_date_str = section.start.strftime('%m/%d/%Y')
start_time_str = section.lms.start.strftime('%H:%M') start_time_str = section.start.strftime('%H:%M')
else: else:
start_date_str = '' start_date_str = ''
start_time_str = '' start_time_str = ''
%> %>
%if section.lms.start is None: %if section.start is None:
<span class="published-status">${_("This section has not been released.")}</span> <span class="published-status">${_("This section has not been released.")}</span>
<a href="#" class="schedule-button" data-date="" data-time="" data-id="${section.location}">${_("Schedule")}</a> <a href="#" class="schedule-button" data-date="" data-time="" data-id="${section.location}">${_("Schedule")}</a>
%else: %else:
<span class="published-status"><strong>${_("Will Release:")}</strong> <span class="published-status"><strong>${_("Will Release:")}</strong>
${date_utils.get_default_time_display(section.lms.start)}</span> ${date_utils.get_default_time_display(section.start)}</span>
<a href="#" class="edit-button" data-date="${start_date_str}" <a href="#" class="edit-button" data-date="${start_date_str}"
data-time="${start_time_str}" data-id="${section.location}">${_("Edit")}</a> data-time="${start_time_str}" data-id="${section.location}">${_("Edit")}</a>
%endif %endif
...@@ -199,7 +199,7 @@ ...@@ -199,7 +199,7 @@
</a> </a>
</div> </div>
<div class="gradable-status" data-initial-status="${subsection.lms.format if subsection.lms.format is not None else _('Not Graded')}"> <div class="gradable-status" data-initial-status="${subsection.format if subsection.format is not None else _('Not Graded')}">
</div> </div>
<div class="item-actions"> <div class="item-actions">
......
...@@ -16,7 +16,6 @@ from contentstore import utils ...@@ -16,7 +16,6 @@ from contentstore import utils
<script src="${static.url('js/vendor/timepicker/datepair.js')}"></script> <script src="${static.url('js/vendor/timepicker/datepair.js')}"></script>
<script src="${static.url('js/vendor/date.js')}"></script> <script src="${static.url('js/vendor/date.js')}"></script>
<script type="text/javascript" src="${static.url('js/template_loader.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/course_relative.js')}"></script> <script type="text/javascript" src="${static.url('js/models/course_relative.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/validating_view.js')}"></script> <script type="text/javascript" src="${static.url('js/views/validating_view.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/settings/main_settings_view.js')}"></script> <script type="text/javascript" src="${static.url('js/views/settings/main_settings_view.js')}"></script>
...@@ -91,6 +90,7 @@ from contentstore import utils ...@@ -91,6 +90,7 @@ from contentstore import utils
</li> </li>
</ol> </ol>
% if about_page_editable:
<div class="note note-promotion note-promotion-courseURL has-actions"> <div class="note note-promotion note-promotion-courseURL has-actions">
<h3 class="title">${_("Course Summary Page")} <span class="tip">${_("(for student enrollment and access)")}</span></h3> <h3 class="title">${_("Course Summary Page")} <span class="tip">${_("(for student enrollment and access)")}</span></h3>
<div class="copy"> <div class="copy">
...@@ -103,6 +103,7 @@ from contentstore import utils ...@@ -103,6 +103,7 @@ from contentstore import utils
</li> </li>
</ul> </ul>
</div> </div>
% endif
% if not about_page_editable: % if not about_page_editable:
<div class="notice notice-incontext notice-workflow"> <div class="notice notice-incontext notice-workflow">
...@@ -152,7 +153,6 @@ from contentstore import utils ...@@ -152,7 +153,6 @@ from contentstore import utils
</li> </li>
</ol> </ol>
% if about_page_editable:
<ol class="list-input"> <ol class="list-input">
<li class="field-group field-group-enrollment-start" id="enrollment-start"> <li class="field-group field-group-enrollment-start" id="enrollment-start">
<div class="field date" id="field-enrollment-start-date"> <div class="field date" id="field-enrollment-start-date">
...@@ -182,7 +182,6 @@ from contentstore import utils ...@@ -182,7 +182,6 @@ from contentstore import utils
</div> </div>
</li> </li>
</ol> </ol>
% endif
% if not about_page_editable: % if not about_page_editable:
<div class="notice notice-incontext notice-workflow"> <div class="notice notice-incontext notice-workflow">
...@@ -194,14 +193,13 @@ from contentstore import utils ...@@ -194,14 +193,13 @@ from contentstore import utils
% endif % endif
</section> </section>
<hr class="divide" /> <hr class="divide" />
% if about_page_editable:
<section class="group-settings marketing"> <section class="group-settings marketing">
<header> <header>
<h2 class="title-2">${_("Introducing Your Course")}</h2> <h2 class="title-2">${_("Introducing Your Course")}</h2>
<span class="tip">${_("Information for prospective students")}</span> <span class="tip">${_("Information for prospective students")}</span>
</header> </header>
<ol class="list-input"> <ol class="list-input">
% if about_page_editable:
<li class="field text" id="field-course-overview"> <li class="field text" id="field-course-overview">
<label for="course-overview">${_("Course Overview")}</label> <label for="course-overview">${_("Course Overview")}</label>
<textarea class="tinymce text-editor" id="course-overview"></textarea> <textarea class="tinymce text-editor" id="course-overview"></textarea>
...@@ -213,6 +211,7 @@ from contentstore import utils ...@@ -213,6 +211,7 @@ from contentstore import utils
%>${text}</%def> %>${text}</%def>
<span class="tip tip-stacked">${overview_text()}</span> <span class="tip tip-stacked">${overview_text()}</span>
</li> </li>
% endif
<li class="field image" id="field-course-image"> <li class="field image" id="field-course-image">
<label>${_("Course Image")}</label> <label>${_("Course Image")}</label>
...@@ -242,6 +241,7 @@ from contentstore import utils ...@@ -242,6 +241,7 @@ from contentstore import utils
</div> </div>
</li> </li>
% if about_page_editable:
<li class="field video" id="field-course-introduction-video"> <li class="field video" id="field-course-introduction-video">
<label for="course-overview">${_("Course Introduction Video")}</label> <label for="course-overview">${_("Course Introduction Video")}</label>
<div class="input input-existing"> <div class="input input-existing">
...@@ -258,9 +258,11 @@ from contentstore import utils ...@@ -258,9 +258,11 @@ from contentstore import utils
<span class="tip tip-stacked">${_("Enter your YouTube video's ID (along with any restriction parameters)")}</span> <span class="tip tip-stacked">${_("Enter your YouTube video's ID (along with any restriction parameters)")}</span>
</div> </div>
</li> </li>
% endif
</ol> </ol>
</section> </section>
% if about_page_editable:
<hr class="divide" /> <hr class="divide" />
<section class="group-settings requirements"> <section class="group-settings requirements">
...@@ -277,7 +279,7 @@ from contentstore import utils ...@@ -277,7 +279,7 @@ from contentstore import utils
</li> </li>
</ol> </ol>
</section> </section>
% endif % endif
</form> </form>
</article> </article>
<aside class="content-supplementary" role="complimentary"> <aside class="content-supplementary" role="complimentary">
......
...@@ -13,7 +13,6 @@ ...@@ -13,7 +13,6 @@
</script> </script>
% endfor % endfor
<script type="text/javascript" src="${static.url('js/template_loader.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/validating_view.js')}"></script> <script type="text/javascript" src="${static.url('js/views/validating_view.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/settings/advanced.js')}"></script> <script type="text/javascript" src="${static.url('js/models/settings/advanced.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/settings/advanced_view.js')}"></script> <script type="text/javascript" src="${static.url('js/views/settings/advanced_view.js')}"></script>
......
...@@ -12,7 +12,6 @@ from contentstore import utils ...@@ -12,7 +12,6 @@ from contentstore import utils
<%block name="jsextra"> <%block name="jsextra">
<script type="text/javascript" src="${static.url('js/template_loader.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/course_relative.js')}"></script> <script type="text/javascript" src="${static.url('js/models/course_relative.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/validating_view.js')}"></script> <script type="text/javascript" src="${static.url('js/views/validating_view.js')}"></script>
......
<%! from django.utils.translation import ugettext as _ %>
<%inherit file="base.html" /> <%inherit file="base.html" />
<%block name="title">${_("Grading Settings")}</%block> <%block name="title">${_("Grading Settings")}</%block>
<%block name="bodyclass">is-signedin course grading settings</%block> <%block name="bodyclass">is-signedin course grading settings</%block>
<%namespace name='static' file='static_content.html'/> <%namespace name='static' file='static_content.html'/>
<%! <%!
from contentstore import utils from contentstore import utils
from django.utils.translation import ugettext as _
%> %>
<%block name="header_extras">
% for template_name in ["course_grade_policy"]:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="js/${template_name}.underscore" />
</script>
% endfor
</%block>
<%block name="jsextra"> <%block name="jsextra">
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" /> <link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
<script src="${static.url('js/vendor/timepicker/jquery.timepicker.js')}"></script> <script src="${static.url('js/vendor/timepicker/jquery.timepicker.js')}"></script>
<script type="text/javascript" src="${static.url('js/template_loader.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/course_relative.js')}"></script> <script type="text/javascript" src="${static.url('js/models/course_relative.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/validating_view.js')}"></script> <script type="text/javascript" src="${static.url('js/views/validating_view.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/settings/course_grading_policy.js')}"></script> <script type="text/javascript" src="${static.url('js/models/settings/course_grading_policy.js')}"></script>
......
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
<li> <li>
<ol id="sortable"> <ol id="sortable">
% for child in module.get_children(): % for child in module.get_children():
<li class="${module.category}"> <li class="${module.scope_ids.block_type}">
<a href="#" class="module-edit" <a href="#" class="module-edit"
data-id="${child.location.url()}" data-id="${child.location.url()}"
data-type="${child.js_module_name}" data-type="${child.js_module_name}"
......
...@@ -21,7 +21,7 @@ This def will enumerate through a passed in subsection and list all of the units ...@@ -21,7 +21,7 @@ This def will enumerate through a passed in subsection and list all of the units
%> %>
<div class="section-item ${selected_class}"> <div class="section-item ${selected_class}">
<a href="${reverse('edit_unit', args=[unit.location])}" class="${unit_state}-item"> <a href="${reverse('edit_unit', args=[unit.location])}" class="${unit_state}-item">
<span class="${unit.category}-icon"></span> <span class="${unit.scope_ids.block_type}-icon"></span>
<span class="unit-name">${unit.display_name_with_default}</span> <span class="unit-name">${unit.display_name_with_default}</span>
</a> </a>
% if actions: % if actions:
......
...@@ -4,12 +4,12 @@ Namespace defining common fields used by Studio for all blocks ...@@ -4,12 +4,12 @@ Namespace defining common fields used by Studio for all blocks
import datetime import datetime
from xblock.core import Namespace, Scope, ModelType, String from xblock.fields import Scope, Field, Integer, XBlockMixin
class DateTuple(ModelType): class DateTuple(Field):
""" """
ModelType that stores datetime objects as time tuples Field that stores datetime objects as time tuples
""" """
def from_json(self, value): def from_json(self, value):
return datetime.datetime(*value[0:6]) return datetime.datetime(*value[0:6])
...@@ -21,9 +21,9 @@ class DateTuple(ModelType): ...@@ -21,9 +21,9 @@ class DateTuple(ModelType):
return list(value.timetuple()) return list(value.timetuple())
class CmsNamespace(Namespace): class CmsBlockMixin(XBlockMixin):
""" """
Namespace with fields common to all blocks in Studio Mixin with fields common to all blocks in Studio
""" """
published_date = DateTuple(help="Date when the module was published", 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) published_by = Integer(help="Id of the user who published this module", scope=Scope.settings)
...@@ -17,7 +17,10 @@ from django.core.urlresolvers import reverse ...@@ -17,7 +17,10 @@ from django.core.urlresolvers import reverse
from django.core.validators import validate_email from django.core.validators import validate_email
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from student.models import TestCenterUser, TestCenterRegistration if settings.MITX_FEATURES.get('AUTH_USE_CAS'):
from django_cas.views import login as django_cas_login
from student.models import UserProfile, TestCenterUser, TestCenterRegistration
from django.http import HttpResponse, HttpResponseRedirect, HttpRequest, HttpResponseForbidden from django.http import HttpResponse, HttpResponseRedirect, HttpRequest, HttpResponseForbidden
from django.utils.http import urlquote from django.utils.http import urlquote
...@@ -44,7 +47,7 @@ from ratelimitbackend.exceptions import RateLimitException ...@@ -44,7 +47,7 @@ from ratelimitbackend.exceptions import RateLimitException
import student.views as student_views import student.views as student_views
# Required for Pearson # Required for Pearson
from courseware.views import get_module_for_descriptor, jump_to from courseware.views import get_module_for_descriptor, jump_to
from courseware.model_data import ModelDataCache from courseware.model_data import FieldDataCache
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
from xmodule.modulestore import Location from xmodule.modulestore import Location
...@@ -382,6 +385,32 @@ def ssl_login(request): ...@@ -382,6 +385,32 @@ def ssl_login(request):
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# CAS (Central Authentication Service)
# -----------------------------------------------------------------------------
def cas_login(request, next_page=None, required=False):
"""
Uses django_cas for authentication.
CAS is a common authentcation method pioneered by Yale.
See http://en.wikipedia.org/wiki/Central_Authentication_Service
Does normal CAS login then generates user_profile if nonexistent,
and if login was successful. We assume that user details are
maintained by the central service, and thus an empty user profile
is appropriate.
"""
ret = django_cas_login(request, next_page, required)
if request.user.is_authenticated():
user = request.user
if not UserProfile.objects.filter(user=user):
user_profile = UserProfile(name=user.username, user=user)
user_profile.save()
return ret
# -----------------------------------------------------------------------------
# Shibboleth (Stanford and others. Uses *Apache* environment variables) # Shibboleth (Stanford and others. Uses *Apache* environment variables)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def shib_login(request): def shib_login(request):
...@@ -915,7 +944,7 @@ def test_center_login(request): ...@@ -915,7 +944,7 @@ def test_center_login(request):
log.error("cand {} on exam {} for course {}: descriptor not found for location {}".format(client_candidate_id, exam_series_code, course_id, location)) log.error("cand {} on exam {} for course {}: descriptor not found for location {}".format(client_candidate_id, exam_series_code, course_id, location))
return HttpResponseRedirect(makeErrorURL(error_url, "missingClientProgram")) return HttpResponseRedirect(makeErrorURL(error_url, "missingClientProgram"))
timelimit_module_cache = ModelDataCache.cache_for_descriptor_descendents(course_id, testcenteruser.user, timelimit_module_cache = FieldDataCache.cache_for_descriptor_descendents(course_id, testcenteruser.user,
timelimit_descriptor, depth=None) timelimit_descriptor, depth=None)
timelimit_module = get_module_for_descriptor(request.user, request, timelimit_descriptor, timelimit_module = get_module_for_descriptor(request.user, request, timelimit_descriptor,
timelimit_module_cache, course_id, position=None) timelimit_module_cache, course_id, position=None)
......
...@@ -415,6 +415,8 @@ def change_enrollment(request): ...@@ -415,6 +415,8 @@ def change_enrollment(request):
@ensure_csrf_cookie @ensure_csrf_cookie
def accounts_login(request, error=""): def accounts_login(request, error=""):
if settings.MITX_FEATURES.get('AUTH_USE_CAS'):
return redirect(reverse('cas-login'))
return render_to_response('login.html', {'error': error}) return render_to_response('login.html', {'error': error})
# Need different levels of logging # Need different levels of logging
...@@ -511,7 +513,11 @@ def logout_user(request): ...@@ -511,7 +513,11 @@ def logout_user(request):
# We do not log here, because we have a handler registered # We do not log here, because we have a handler registered
# to perform logging on successful logouts. # to perform logging on successful logouts.
logout(request) logout(request)
response = redirect('/') if settings.MITX_FEATURES.get('AUTH_USE_CAS'):
target = reverse('cas-logout')
else:
target = '/'
response = redirect(target)
response.delete_cookie(settings.EDXMKTG_COOKIE_NAME, response.delete_cookie(settings.EDXMKTG_COOKIE_NAME,
path='/', path='/',
domain=settings.SESSION_COOKIE_DOMAIN) domain=settings.SESSION_COOKIE_DOMAIN)
......
...@@ -29,11 +29,13 @@ from xmodule.contentstore.django import _CONTENTSTORE ...@@ -29,11 +29,13 @@ from xmodule.contentstore.django import _CONTENTSTORE
# to use staticfiles. # to use staticfiles.
try: try:
import staticfiles import staticfiles
import staticfiles.handlers
except ImportError: except ImportError:
pass pass
else: else:
import sys import sys
sys.modules['django.contrib.staticfiles'] = staticfiles sys.modules['django.contrib.staticfiles'] = staticfiles
sys.modules['django.contrib.staticfiles.handlers'] = staticfiles.handlers
LOGGER = getLogger(__name__) LOGGER = getLogger(__name__)
LOGGER.info("Loading the lettuce acceptance testing terrain file...") LOGGER.info("Loading the lettuce acceptance testing terrain file...")
......
...@@ -32,7 +32,7 @@ class TestXmoduleModfiers(ModuleStoreTestCase): ...@@ -32,7 +32,7 @@ class TestXmoduleModfiers(ModuleStoreTestCase):
late_problem = ItemFactory.create( late_problem = ItemFactory.create(
parent_location=section.location, display_name='problem hist 2', parent_location=section.location, display_name='problem hist 2',
category='problem') category='problem')
late_problem.lms.start = datetime.datetime.now(UTC) + datetime.timedelta(days=32) late_problem.start = datetime.datetime.now(UTC) + datetime.timedelta(days=32)
late_problem.has_score = False late_problem.has_score = False
......
...@@ -29,12 +29,16 @@ def wrap_xmodule(get_html, module, template, context=None): ...@@ -29,12 +29,16 @@ def wrap_xmodule(get_html, module, template, context=None):
if context is None: if context is None:
context = {} context = {}
# If XBlock generated this class, then use the first baseclass
# as the name (since that's the original, unmixed class)
class_name = getattr(module, 'unmixed_class', module.__class__).__name__
@wraps(get_html) @wraps(get_html)
def _get_html(): def _get_html():
context.update({ context.update({
'content': get_html(), 'content': get_html(),
'display_name': module.display_name, 'display_name': module.display_name,
'class_': module.__class__.__name__, 'class_': class_name,
'module_name': module.js_module_name 'module_name': module.js_module_name
}) })
...@@ -157,7 +161,7 @@ def add_histogram(get_html, module, user): ...@@ -157,7 +161,7 @@ def add_histogram(get_html, module, user):
# doesn't like symlinks) # doesn't like symlinks)
filepath = filename filepath = filename
data_dir = osfs.root_path.rsplit('/')[-1] data_dir = osfs.root_path.rsplit('/')[-1]
giturl = module.lms.giturl or 'https://github.com/MITx' giturl = module.giturl or 'https://github.com/MITx'
edit_link = "%s/%s/tree/master/%s" % (giturl, data_dir, filepath) edit_link = "%s/%s/tree/master/%s" % (giturl, data_dir, filepath)
else: else:
edit_link = False edit_link = False
...@@ -165,22 +169,21 @@ def add_histogram(get_html, module, user): ...@@ -165,22 +169,21 @@ def add_histogram(get_html, module, user):
giturl = "" giturl = ""
data_dir = "" data_dir = ""
source_file = module.lms.source_file # source used to generate the problem XML, eg latex or word source_file = module.source_file # source used to generate the problem XML, eg latex or word
# useful to indicate to staff if problem has been released or not # useful to indicate to staff if problem has been released or not
# TODO (ichuang): use _has_access_descriptor.can_load in lms.courseware.access, instead of now>mstart comparison here # TODO (ichuang): use _has_access_descriptor.can_load in lms.courseware.access, instead of now>mstart comparison here
now = datetime.datetime.now(UTC()) now = datetime.datetime.now(UTC())
is_released = "unknown" is_released = "unknown"
mstart = module.descriptor.lms.start mstart = module.descriptor.start
if mstart is not None: if mstart is not None:
is_released = "<font color='red'>Yes!</font>" if (now > mstart) else "<font color='green'>Not yet</font>" is_released = "<font color='red'>Yes!</font>" if (now > mstart) else "<font color='green'>Not yet</font>"
staff_context = {'fields': [(field.name, getattr(module, field.name)) for field in module.fields], staff_context = {'fields': [(name, field.read_from(module)) for name, field in module.fields.items()],
'lms_fields': [(field.name, getattr(module.lms, field.name)) for field in module.lms.fields], 'xml_attributes': getattr(module.descriptor, 'xml_attributes', {}),
'xml_attributes' : getattr(module.descriptor, 'xml_attributes', {}),
'location': module.location, 'location': module.location,
'xqa_key': module.lms.xqa_key, 'xqa_key': module.xqa_key,
'source_file': source_file, 'source_file': source_file,
'source_url': '%s/%s/tree/master/%s' % (giturl, data_dir, source_file), 'source_url': '%s/%s/tree/master/%s' % (giturl, data_dir, source_file),
'category': str(module.__class__.__name__), 'category': str(module.__class__.__name__),
......
...@@ -208,10 +208,10 @@ class InputTypeBase(object): ...@@ -208,10 +208,10 @@ class InputTypeBase(object):
# end up in a partially-initialized state. # end up in a partially-initialized state.
loaded = {} loaded = {}
to_render = set() to_render = set()
for a in self.get_attributes(): for attribute in self.get_attributes():
loaded[a.name] = a.parse_from_xml(self.xml) loaded[attribute.name] = attribute.parse_from_xml(self.xml)
if a.render: if attribute.render:
to_render.add(a.name) to_render.add(attribute.name)
self.loaded_attributes = loaded self.loaded_attributes = loaded
self.to_render = to_render self.to_render = to_render
...@@ -325,7 +325,7 @@ class OptionInput(InputTypeBase): ...@@ -325,7 +325,7 @@ class OptionInput(InputTypeBase):
Convert options to a convenient format. Convert options to a convenient format.
""" """
return [Attribute('options', transform=cls.parse_options), return [Attribute('options', transform=cls.parse_options),
Attribute('inline', '')] Attribute('inline', False)]
registry.register(OptionInput) registry.register(OptionInput)
...@@ -493,17 +493,17 @@ class JSInput(InputTypeBase): ...@@ -493,17 +493,17 @@ class JSInput(InputTypeBase):
""" """
Register the attributes. Register the attributes.
""" """
return [Attribute('params', None), # extra iframe params return [
Attribute('html_file', None), Attribute('params', None), # extra iframe params
Attribute('gradefn', "gradefn"), Attribute('html_file', None),
Attribute('get_statefn', None), # Function to call in iframe Attribute('gradefn', "gradefn"),
# to get current state. Attribute('get_statefn', None), # Function to call in iframe
Attribute('set_statefn', None), # Function to call iframe to # to get current state.
# set state Attribute('set_statefn', None), # Function to call iframe to
Attribute('width', "400"), # iframe width # set state
Attribute('height', "300")] # iframe height Attribute('width', "400"), # iframe width
Attribute('height', "300") # iframe height
]
def _extra_context(self): def _extra_context(self):
context = { context = {
...@@ -514,7 +514,6 @@ class JSInput(InputTypeBase): ...@@ -514,7 +514,6 @@ class JSInput(InputTypeBase):
return context return context
registry.register(JSInput) registry.register(JSInput)
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
...@@ -1048,8 +1047,8 @@ class ChemicalEquationInput(InputTypeBase): ...@@ -1048,8 +1047,8 @@ class ChemicalEquationInput(InputTypeBase):
try: try:
result['preview'] = chemcalc.render_to_html(formula) result['preview'] = chemcalc.render_to_html(formula)
except pyparsing.ParseException as p: except pyparsing.ParseException as err:
result['error'] = u"Couldn't parse formula: {0}".format(p.msg) result['error'] = u"Couldn't parse formula: {0}".format(err.msg)
except Exception: except Exception:
# this is unexpected, so log # this is unexpected, so log
log.warning( log.warning(
...@@ -1189,15 +1188,19 @@ class DragAndDropInput(InputTypeBase): ...@@ -1189,15 +1188,19 @@ class DragAndDropInput(InputTypeBase):
'can_reuse': smth}. 'can_reuse': smth}.
""" """
tag_attrs = dict() tag_attrs = dict()
tag_attrs['draggable'] = {'id': Attribute._sentinel, tag_attrs['draggable'] = {
'label': "", 'icon': "", 'id': Attribute._sentinel,
'can_reuse': ""} 'label': "", 'icon': "",
'can_reuse': ""
tag_attrs['target'] = {'id': Attribute._sentinel, }
'x': Attribute._sentinel,
'y': Attribute._sentinel, tag_attrs['target'] = {
'w': Attribute._sentinel, 'id': Attribute._sentinel,
'h': Attribute._sentinel} 'x': Attribute._sentinel,
'y': Attribute._sentinel,
'w': Attribute._sentinel,
'h': Attribute._sentinel
}
dic = dict() dic = dict()
......
<form class="inputtype option-input"> <% doinline = "inline" if inline else "" %>
<form class="inputtype option-input ${doinline}">
<select name="input_${id}" id="input_${id}" aria-describedby="answer_${id}"> <select name="input_${id}" id="input_${id}" aria-describedby="answer_${id}">
<option value="option_${id}_dummy_default"> </option> <option value="option_${id}_dummy_default"> </option>
% for option_id, option_description in options: % for option_id, option_description in options:
......
...@@ -55,7 +55,7 @@ class OptionInputTest(unittest.TestCase): ...@@ -55,7 +55,7 @@ class OptionInputTest(unittest.TestCase):
'options': [('Up', 'Up'), ('Down', 'Down')], 'options': [('Up', 'Up'), ('Down', 'Down')],
'status': 'answered', 'status': 'answered',
'msg': '', 'msg': '',
'inline': '', 'inline': False,
'id': 'sky_input'} 'id': 'sky_input'}
self.assertEqual(context, expected) self.assertEqual(context, expected)
......
...@@ -6,7 +6,7 @@ from xmodule.x_module import XModule ...@@ -6,7 +6,7 @@ from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
from xmodule.xml_module import XmlDescriptor from xmodule.xml_module import XmlDescriptor
from xmodule.exceptions import InvalidDefinitionError from xmodule.exceptions import InvalidDefinitionError
from xblock.core import String, Scope, Dict from xblock.fields import String, Scope, Dict
DEFAULT = "_DEFAULT_GROUP" DEFAULT = "_DEFAULT_GROUP"
......
...@@ -5,7 +5,7 @@ from pkg_resources import resource_string ...@@ -5,7 +5,7 @@ from pkg_resources import resource_string
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
from xblock.core import Scope, String from xblock.fields import Scope, String
import textwrap import textwrap
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
......
...@@ -18,7 +18,7 @@ from .progress import Progress ...@@ -18,7 +18,7 @@ from .progress import Progress
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
from xmodule.exceptions import NotFoundError, ProcessingError from xmodule.exceptions import NotFoundError, ProcessingError
from xblock.core import Scope, String, Boolean, Dict, Integer, Float from xblock.fields import Scope, String, Boolean, Dict, Integer, Float
from .fields import Timedelta, Date from .fields import Timedelta, Date
from django.utils.timezone import UTC from django.utils.timezone import UTC
......
...@@ -5,7 +5,7 @@ from pkg_resources import resource_string ...@@ -5,7 +5,7 @@ from pkg_resources import resource_string
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
from .x_module import XModule from .x_module import XModule
from xblock.core import Integer, Scope, String, List, Float, Boolean from xblock.fields import Integer, Scope, String, List, Float, Boolean
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor
from collections import namedtuple from collections import namedtuple
from .fields import Date, Timedelta from .fields import Date, Timedelta
......
...@@ -10,7 +10,7 @@ from pkg_resources import resource_string ...@@ -10,7 +10,7 @@ from pkg_resources import resource_string
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.seq_module import SequenceDescriptor from xmodule.seq_module import SequenceDescriptor
from xblock.core import Scope, List from xblock.fields import Scope, List
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
......
...@@ -13,7 +13,7 @@ from xmodule.util.decorators import lazyproperty ...@@ -13,7 +13,7 @@ from xmodule.util.decorators import lazyproperty
from xmodule.graders import grader_from_conf from xmodule.graders import grader_from_conf
import json import json
from xblock.core import Scope, List, String, Dict, Boolean from xblock.fields import Scope, List, String, Dict, Boolean
from .fields import Date from .fields import Date
from xmodule.modulestore.locator import CourseLocator from xmodule.modulestore.locator import CourseLocator
from django.utils.timezone import UTC from django.utils.timezone import UTC
...@@ -118,6 +118,13 @@ class Textbook(object): ...@@ -118,6 +118,13 @@ class Textbook(object):
return table_of_contents return table_of_contents
def __eq__(self, other):
return (self.title == other.title and
self.book_url == other.book_url)
def __ne__(self, other):
return not self == other
class TextbookList(List): class TextbookList(List):
def from_json(self, values): def from_json(self, values):
...@@ -737,7 +744,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): ...@@ -737,7 +744,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
all_descriptors - This contains a list of all xmodules that can all_descriptors - This contains a list of all xmodules that can
effect grading a student. This is used to efficiently fetch effect grading a student. This is used to efficiently fetch
all the xmodule state for a ModelDataCache without walking all the xmodule state for a FieldDataCache without walking
the descriptor tree again. the descriptor tree again.
...@@ -754,14 +761,14 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): ...@@ -754,14 +761,14 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
for c in self.get_children(): for c in self.get_children():
for s in c.get_children(): for s in c.get_children():
if s.lms.graded: if s.graded:
xmoduledescriptors = list(yield_descriptor_descendents(s)) xmoduledescriptors = list(yield_descriptor_descendents(s))
xmoduledescriptors.append(s) xmoduledescriptors.append(s)
# The xmoduledescriptors included here are only the ones that have scores. # The xmoduledescriptors included here are only the ones that have scores.
section_description = {'section_descriptor': s, 'xmoduledescriptors': filter(lambda child: child.has_score, xmoduledescriptors)} section_description = {'section_descriptor': s, 'xmoduledescriptors': filter(lambda child: child.has_score, xmoduledescriptors)}
section_format = s.lms.format if s.lms.format is not None else '' section_format = s.format if s.format is not None else ''
graded_sections[section_format] = graded_sections.get(section_format, []) + [section_description] graded_sections[section_format] = graded_sections.get(section_format, []) + [section_description]
all_descriptors.extend(xmoduledescriptors) all_descriptors.extend(xmoduledescriptors)
......
...@@ -15,7 +15,7 @@ from lxml import etree ...@@ -15,7 +15,7 @@ from lxml import etree
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
from xblock.core import Scope, String, Integer, Boolean, Dict, List from xblock.fields import Scope, String, Integer, Boolean, Dict, List
from capa.responsetypes import FormulaResponse from capa.responsetypes import FormulaResponse
......
...@@ -964,3 +964,11 @@ section.peer-grading-container{ ...@@ -964,3 +964,11 @@ section.peer-grading-container{
} }
} }
} }
div.staff-info{
background-color: #eee;
border-radius: 10px;
border-bottom: 1px solid lightgray;
padding: 10px;
margin: 10px 0px 10px 0px;
}
...@@ -40,6 +40,12 @@ div.video { ...@@ -40,6 +40,12 @@ div.video {
padding-bottom: 56.25%; padding-bottom: 56.25%;
position: relative; position: relative;
div {
&.hidden {
display: none;
}
}
object, iframe { object, iframe {
border: none; border: none;
height: 100%; height: 100%;
...@@ -48,6 +54,15 @@ div.video { ...@@ -48,6 +54,15 @@ div.video {
top: 0; top: 0;
width: 100%; width: 100%;
} }
h3 {
text-align: center;
color: white;
&.hidden {
display: none;
}
}
} }
section.video-controls { section.video-controls {
...@@ -516,6 +531,12 @@ div.video { ...@@ -516,6 +531,12 @@ div.video {
height: 0px; height: 0px;
} }
article.video-wrapper section.video-player {
h3 {
color: black;
}
}
ol.subtitles { ol.subtitles {
width: 0; width: 0;
height: 0; height: 0;
...@@ -563,6 +584,12 @@ div.video { ...@@ -563,6 +584,12 @@ div.video {
position: static; position: static;
} }
article.video-wrapper section.video-player {
h3 {
color: white;
}
}
div.tc-wrapper { div.tc-wrapper {
@include clearfix; @include clearfix;
display: table; display: table;
......
...@@ -3,7 +3,7 @@ from pkg_resources import resource_string ...@@ -3,7 +3,7 @@ from pkg_resources import resource_string
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
from xmodule.editing_module import MetadataOnlyEditingDescriptor from xmodule.editing_module import MetadataOnlyEditingDescriptor
from xblock.core import String, Scope from xblock.fields import String, Scope
from uuid import uuid4 from uuid import uuid4
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
from pkg_resources import resource_string from pkg_resources import resource_string
from xmodule.mako_module import MakoModuleDescriptor from xmodule.mako_module import MakoModuleDescriptor
from xblock.core import Scope, String from xblock.fields import Scope, String
import logging import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
......
...@@ -12,7 +12,8 @@ from lxml import etree ...@@ -12,7 +12,8 @@ from lxml import etree
from xmodule.x_module import XModule, XModuleDescriptor from xmodule.x_module import XModule, XModuleDescriptor
from xmodule.errortracker import exc_info_to_str from xmodule.errortracker import exc_info_to_str
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xblock.core import String, Scope from xblock.fields import String, Scope, ScopeIds
from xblock.field_data import DictFieldData
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -95,16 +96,19 @@ class ErrorDescriptor(ErrorFields, XModuleDescriptor): ...@@ -95,16 +96,19 @@ class ErrorDescriptor(ErrorFields, XModuleDescriptor):
) )
# real metadata stays in the content, but add a display name # real metadata stays in the content, but add a display name
model_data = { field_data = DictFieldData({
'error_msg': str(error_msg), 'error_msg': str(error_msg),
'contents': contents, 'contents': contents,
'display_name': 'Error: ' + location.url(), 'display_name': 'Error: ' + location.url(),
'location': location, 'location': location,
'category': 'error' 'category': 'error'
} })
return cls( return system.construct_xblock_from_class(
system, cls,
model_data, field_data,
# The error module doesn't use scoped data, and thus doesn't need
# real scope keys
ScopeIds('error', None, location, location)
) )
def get_context(self): def get_context(self):
......
...@@ -2,7 +2,7 @@ import time ...@@ -2,7 +2,7 @@ import time
import logging import logging
import re import re
from xblock.core import ModelType from xblock.fields import Field
import datetime import datetime
import dateutil.parser import dateutil.parser
...@@ -11,7 +11,7 @@ from pytz import UTC ...@@ -11,7 +11,7 @@ from pytz import UTC
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class Date(ModelType): class Date(Field):
''' '''
Date fields know how to parse and produce json (iso) compatible formats. Converts to tz aware datetimes. Date fields know how to parse and produce json (iso) compatible formats. Converts to tz aware datetimes.
''' '''
...@@ -20,6 +20,8 @@ class Date(ModelType): ...@@ -20,6 +20,8 @@ class Date(ModelType):
PREVENT_DEFAULT_DAY_MON_SEED1 = datetime.datetime(CURRENT_YEAR, 1, 1, tzinfo=UTC) PREVENT_DEFAULT_DAY_MON_SEED1 = datetime.datetime(CURRENT_YEAR, 1, 1, tzinfo=UTC)
PREVENT_DEFAULT_DAY_MON_SEED2 = datetime.datetime(CURRENT_YEAR, 2, 2, tzinfo=UTC) PREVENT_DEFAULT_DAY_MON_SEED2 = datetime.datetime(CURRENT_YEAR, 2, 2, tzinfo=UTC)
MUTABLE = False
def _parse_date_wo_default_month_day(self, field): def _parse_date_wo_default_month_day(self, field):
""" """
Parse the field as an iso string but prevent dateutils from defaulting the day or month while Parse the field as an iso string but prevent dateutils from defaulting the day or month while
...@@ -76,12 +78,12 @@ class Date(ModelType): ...@@ -76,12 +78,12 @@ class Date(ModelType):
else: else:
return value.isoformat() return value.isoformat()
else: else:
raise TypeError("Cannot convert {} to json".format(value)) raise TypeError("Cannot convert {!r} to json".format(value))
TIMEDELTA_REGEX = re.compile(r'^((?P<days>\d+?) day(?:s?))?(\s)?((?P<hours>\d+?) hour(?:s?))?(\s)?((?P<minutes>\d+?) minute(?:s)?)?(\s)?((?P<seconds>\d+?) second(?:s)?)?$') TIMEDELTA_REGEX = re.compile(r'^((?P<days>\d+?) day(?:s?))?(\s)?((?P<hours>\d+?) hour(?:s?))?(\s)?((?P<minutes>\d+?) minute(?:s)?)?(\s)?((?P<seconds>\d+?) second(?:s)?)?$')
class Timedelta(ModelType): class Timedelta(Field):
# Timedeltas are immutable, see http://docs.python.org/2/library/datetime.html#available-types # Timedeltas are immutable, see http://docs.python.org/2/library/datetime.html#available-types
MUTABLE = False MUTABLE = False
......
...@@ -6,7 +6,7 @@ from pkg_resources import resource_string ...@@ -6,7 +6,7 @@ from pkg_resources import resource_string
from xmodule.editing_module import EditingDescriptor from xmodule.editing_module import EditingDescriptor
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.xml_module import XmlDescriptor from xmodule.xml_module import XmlDescriptor
from xblock.core import Scope, Integer, String from xblock.fields import Scope, Integer, String
from .fields import Date from .fields import Date
......
...@@ -14,7 +14,7 @@ from xmodule.xml_module import XmlDescriptor ...@@ -14,7 +14,7 @@ from xmodule.xml_module import XmlDescriptor
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.stringify import stringify_children from xmodule.stringify import stringify_children
from pkg_resources import resource_string from pkg_resources import resource_string
from xblock.core import String, Scope from xblock.fields import String, Scope
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
......
...@@ -7,7 +7,7 @@ from lxml import etree ...@@ -7,7 +7,7 @@ from lxml import etree
from path import path from path import path
from pkg_resources import resource_string from pkg_resources import resource_string
from xblock.core import Scope, String from xblock.fields import Scope, String
from xmodule.editing_module import EditingDescriptor from xmodule.editing_module import EditingDescriptor
from xmodule.html_checker import check_html from xmodule.html_checker import check_html
from xmodule.stringify import stringify_children from xmodule.stringify import stringify_children
......
<div class="base_wrapper"> <div class="component-editor">
<section class="editor-with-tabs"> <div class="base_wrapper">
<div class="wrapper-comp-editor" id="editor-tab-id" data-html_id='test_id'> <section class="editor-with-tabs">
<div class="edit-header"> <div class="wrapper-comp-editor" id="editor-tab-id" data-html_id='test_id'>
<ul class="editor-tabs"> <div class="edit-header">
<li class="inner_tab_wrap"><a href="#tab-0" class="tab">Tab 0 Editor</a></li> <ul class="editor-tabs">
<li class="inner_tab_wrap"><a href="#tab-1" class="tab">Tab 1 Transcripts</a></li> <li class="inner_tab_wrap"><a href="#tab-0" class="tab">Tab 0 Editor</a></li>
<li class="inner_tab_wrap" id="settings"><a href="#tab-2" class="tab">Tab 2 Settings</a></li> <li class="inner_tab_wrap"><a href="#tab-1" class="tab">Tab 1 Transcripts</a></li>
</ul> <li class="inner_tab_wrap" id="settings"><a href="#tab-2" class="tab">Tab 2 Settings</a></li>
</div> </ul>
<div class="tabs-wrapper"> </div>
<div class="component-tab" id="tab-0"> <div class="tabs-wrapper">
<textarea name="" class="edit-box">XML Editor Text</textarea> <div class="component-tab" id="tab-0">
<textarea name="" class="edit-box">XML Editor Text</textarea>
</div>
<div class="component-tab" id="tab-1">
Transcripts
</div>
<div class="component-tab" id="tab-2">
Subtitles
</div>
</div> </div>
<div class="component-tab" id="tab-1"> <div class="wrapper-comp-settings">
Transcripts <ul>
<li id="editor-mode"><a>Editor</a></li>
<li id="settings-mode"><a>Settings</a></li>
</ul>
</div> </div>
<div class="component-tab" id="tab-2">
Subtitles
</div>
</div>
<div class="wrapper-comp-settings">
<ul>
<li id="editor-mode"><a>Editor</a></li>
<li id="settings-mode"><a>Settings</a></li>
</ul>
</div> </div>
</div> </section>
</section>
<div class="component-edit-header" style="display: block"/> <div class="component-edit-header" style="display: block"/>
</div>
</div> </div>
...@@ -10,6 +10,8 @@ ...@@ -10,6 +10,8 @@
data-end="" data-end=""
data-caption-asset-path="/static/subs/" data-caption-asset-path="/static/subs/"
data-autoplay="False" data-autoplay="False"
data-yt-test-timeout="1500"
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
> >
<div class="tc-wrapper"> <div class="tc-wrapper">
<article class="video-wrapper"> <article class="video-wrapper">
......
...@@ -13,6 +13,8 @@ ...@@ -13,6 +13,8 @@
data-webm-source="xmodule/include/fixtures/test.webm" data-webm-source="xmodule/include/fixtures/test.webm"
data-ogg-source="xmodule/include/fixtures/test.ogv" data-ogg-source="xmodule/include/fixtures/test.ogv"
data-autoplay="False" data-autoplay="False"
data-yt-test-timeout="1500"
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
> >
<div class="tc-wrapper"> <div class="tc-wrapper">
<article class="video-wrapper"> <article class="video-wrapper">
......
...@@ -13,6 +13,8 @@ ...@@ -13,6 +13,8 @@
data-webm-source="xmodule/include/fixtures/test.webm" data-webm-source="xmodule/include/fixtures/test.webm"
data-ogg-source="xmodule/include/fixtures/test.ogv" data-ogg-source="xmodule/include/fixtures/test.ogv"
data-autoplay="False" data-autoplay="False"
data-yt-test-timeout="1500"
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
> >
<div class="tc-wrapper"> <div class="tc-wrapper">
<article class="video-wrapper"> <article class="video-wrapper">
......
...@@ -10,6 +10,8 @@ ...@@ -10,6 +10,8 @@
data-end="" data-end=""
data-caption-asset-path="/static/subs/" data-caption-asset-path="/static/subs/"
data-autoplay="False" data-autoplay="False"
data-yt-test-timeout="1500"
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
> >
<div class="tc-wrapper"> <div class="tc-wrapper">
<article class="video-wrapper"> <article class="video-wrapper">
......
<div class="course-content">
<div id="video_example1">
<div id="example1">
<div
id="video_id1"
class="video"
data-streams="0.75:7tqY6eQzVhE,1.0:cogebirgzzM"
data-show-captions="true"
data-start=""
data-end=""
data-caption-asset-path="/static/subs/"
data-autoplay="False"
data-yt-test-timeout="1500"
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
>
<div class="tc-wrapper">
<article class="video-wrapper">
<div class="video-player-pre"></div>
<section class="video-player">
<div id="id1"></div>
</section>
<div class="video-player-post"></div>
<section class="video-controls">
<div class="slider"></div>
<div>
<ul class="vcr">
<li><a class="video_control" href="#" title="Play"></a></li>
<li><div class="vidtime">0:00 / 0:00</div></li>
</ul>
<div class="secondary-controls">
<div class="speeds">
<a href="#">
<h3>Speed</h3>
<p class="active"></p>
</a>
<ol class="video_speeds"></ol>
</div>
<div class="volume">
<a href="#"></a>
<div class="volume-slider-container">
<div class="volume-slider"></div>
</div>
</div>
<a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a>
<a href="#" class="quality_control" title="HD">HD</a>
<a href="#" class="hide-subtitles" title="Turn off captions">Captions</a>
</div>
</div>
</section>
</article>
<ol class="subtitles"><li></li></ol>
</div>
</div>
</div>
</div>
</div>
<div class="course-content">
<div id="video_example2">
<div id="example2">
<div
id="video_id2"
class="video"
data-streams="0.75:7tqY6eQzVhE,1.0:cogebirgzzM"
data-show-captions="true"
data-start=""
data-end=""
data-caption-asset-path="/static/subs/"
data-autoplay="False"
data-yt-test-timeout="1500"
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
>
<div class="tc-wrapper">
<article class="video-wrapper">
<div class="video-player-pre"></div>
<section class="video-player">
<div id="id2"></div>
</section>
<div class="video-player-post"></div>
<section class="video-controls">
<div class="slider"></div>
<div>
<ul class="vcr">
<li><a class="video_control" href="#" title="Play"></a></li>
<li><div class="vidtime">0:00 / 0:00</div></li>
</ul>
<div class="secondary-controls">
<div class="speeds">
<a href="#">
<h3>Speed</h3>
<p class="active"></p>
</a>
<ol class="video_speeds"></ol>
</div>
<div class="volume">
<a href="#"></a>
<div class="volume-slider-container">
<div class="volume-slider"></div>
</div>
</div>
<a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a>
<a href="#" class="quality_control" title="HD">HD</a>
<a href="#" class="hide-subtitles" title="Turn off captions">Captions</a>
</div>
</div>
</section>
</article>
<ol class="subtitles"><li></li></ol>
</div>
</div>
</div>
</div>
</div>
<div class="course-content">
<div id="video_example3">
<div id="example3">
<div
id="video_id3"
class="video"
data-streams="0.75:7tqY6eQzVhE,1.0:cogebirgzzM"
data-show-captions="true"
data-start=""
data-end=""
data-caption-asset-path="/static/subs/"
data-autoplay="False"
data-yt-test-timeout="1500"
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
>
<div class="tc-wrapper">
<article class="video-wrapper">
<div class="video-player-pre"></div>
<section class="video-player">
<div id="id3"></div>
</section>
<div class="video-player-post"></div>
<section class="video-controls">
<div class="slider"></div>
<div>
<ul class="vcr">
<li><a class="video_control" href="#" title="Play"></a></li>
<li><div class="vidtime">0:00 / 0:00</div></li>
</ul>
<div class="secondary-controls">
<div class="speeds">
<a href="#">
<h3>Speed</h3>
<p class="active"></p>
</a>
<ol class="video_speeds"></ol>
</div>
<div class="volume">
<a href="#"></a>
<div class="volume-slider-container">
<div class="volume-slider"></div>
</div>
</div>
<a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a>
<a href="#" class="quality_control" title="HD">HD</a>
<a href="#" class="hide-subtitles" title="Turn off captions">Captions</a>
</div>
</div>
</section>
</article>
<ol class="subtitles"><li></li></ol>
</div>
</div>
</div>
</div>
</div>
...@@ -90,12 +90,24 @@ jasmine.stubbedHtml5Speeds = ['0.75', '1.0', '1.25', '1.50'] ...@@ -90,12 +90,24 @@ jasmine.stubbedHtml5Speeds = ['0.75', '1.0', '1.25', '1.50']
jasmine.stubRequests = -> jasmine.stubRequests = ->
spyOn($, 'ajax').andCallFake (settings) -> spyOn($, 'ajax').andCallFake (settings) ->
if match = settings.url.match /youtube\.com\/.+\/videos\/(.+)\?v=2&alt=jsonc/ if match = settings.url.match /youtube\.com\/.+\/videos\/(.+)\?v=2&alt=jsonc/
if settings.success status = match[1].split('_')
if status and status[0] is 'status'
{
always: (callback) ->
callback.call(window, {}, status[1])
error: (callback) ->
callback.call(window, {}, status[1])
done: (callback) ->
callback.call(window, {}, status[1])
}
else if settings.success
# match[1] - it's video ID # match[1] - it's video ID
settings.success data: jasmine.stubbedMetadata[match[1]] settings.success data: jasmine.stubbedMetadata[match[1]]
else { else {
always: (callback) -> always: (callback) ->
callback.call(window, {}, 'success'); callback.call(window, {}, 'success')
done: (callback) ->
callback.call(window, {}, 'success')
} }
else if match = settings.url.match /static(\/.*)?\/subs\/(.+)\.srt\.sjson/ else if match = settings.url.match /static(\/.*)?\/subs\/(.+)\.srt\.sjson/
settings.success jasmine.stubbedCaption settings.success jasmine.stubbedCaption
......
...@@ -65,7 +65,7 @@ describe "TabsEditingDescriptor", -> ...@@ -65,7 +65,7 @@ describe "TabsEditingDescriptor", ->
describe "editor/settings header", -> describe "editor/settings header", ->
it "is hidden", -> it "is hidden", ->
expect(@descriptor.element.find(".component-edit-header").css('display')).toEqual('none') expect(@descriptor.element.closest(".component-editor").find(".component-edit-header")).toBeHidden()
describe "TabsEditingDescriptor special save cases", -> describe "TabsEditingDescriptor special save cases", ->
beforeEach -> beforeEach ->
......
...@@ -55,46 +55,6 @@ ...@@ -55,46 +55,6 @@
expect(this.state.speed).toEqual('0.75'); expect(this.state.speed).toEqual('0.75');
}); });
}); });
describe('Check Youtube link existence', function () {
var statusList = {
error: 'html5',
timeout: 'html5',
abort: 'html5',
parsererror: 'html5',
success: 'youtube',
notmodified: 'youtube'
};
function stubDeffered(data, status) {
return {
always: function(callback) {
callback.call(window, data, status);
}
}
}
function checkPlayer(videoType, data, status) {
this.state = new window.Video('#example');
spyOn(this.state , 'getVideoMetadata')
.andReturn(stubDeffered(data, status));
this.state.initialize('#example');
expect(this.state.videoType).toEqual(videoType);
}
it('if video id is incorrect', function () {
checkPlayer('html5', { error: {} }, 'success');
});
$.each(statusList, function(status, mode){
it('Status:' + status + ', mode:' + mode, function () {
checkPlayer(mode, {}, status);
});
});
});
}); });
describe('HTML5', function () { describe('HTML5', function () {
...@@ -154,10 +114,22 @@ ...@@ -154,10 +114,22 @@
it('parse Html5 sources', function () { it('parse Html5 sources', function () {
var html5Sources = { var html5Sources = {
mp4: 'xmodule/include/fixtures/test.mp4', mp4: null,
webm: 'xmodule/include/fixtures/test.webm', webm: null,
ogg: 'xmodule/include/fixtures/test.ogv' ogg: null
}; }, v = document.createElement('video');
if (!!(v.canPlayType && v.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/no/, ''))) {
html5Sources['webm'] = 'xmodule/include/fixtures/test.webm';
}
if (!!(v.canPlayType && v.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"').replace(/no/, ''))) {
html5Sources['mp4'] = 'xmodule/include/fixtures/test.mp4';
}
if (!!(v.canPlayType && v.canPlayType('video/ogg; codecs="theora"').replace(/no/, ''))) {
html5Sources['ogg'] = 'xmodule/include/fixtures/test.ogv';
}
expect(state.html5Sources).toEqual(html5Sources); expect(state.html5Sources).toEqual(html5Sources);
}); });
...@@ -214,6 +186,46 @@ ...@@ -214,6 +186,46 @@
}); });
}); });
describe('multiple YT on page', function () {
var state1, state2, state3;
beforeEach(function () {
loadFixtures('video_yt_multiple.html');
spyOn($, 'ajaxWithPrefix');
$.ajax.calls.length = 0;
$.ajaxWithPrefix.calls.length = 0;
// Because several other tests have run, the variable
// that stores the value of the first ajax request must be
// cleared so that we test a pristine state of the video
// module.
Video.clearYoutubeXhr();
state1 = new Video('#example1');
state2 = new Video('#example2');
state3 = new Video('#example3');
});
it('check for YT availability is performed only once', function () {
var numAjaxCalls = 0;
// Total ajax calls made.
numAjaxCalls = $.ajax.calls.length;
// Subtract ajax calls to get captions.
numAjaxCalls -= $.ajaxWithPrefix.calls.length;
// Subtract ajax calls to get metadata for each video.
numAjaxCalls -= 3;
// This should leave just one call. It was made to check
// for YT availability.
expect(numAjaxCalls).toBe(1);
});
});
describe('setSpeed', function () { describe('setSpeed', function () {
describe('YT', function () { describe('YT', function () {
beforeEach(function () { beforeEach(function () {
......
...@@ -82,6 +82,38 @@ ...@@ -82,6 +82,38 @@
$('.speeds').mouseenter().click(); $('.speeds').mouseenter().click();
expect($('.speeds')).not.toHaveClass('open'); expect($('.speeds')).not.toHaveClass('open');
}); });
// Tabbing depends on the following order:
// 1. Play anchor
// 2. Speed anchor
// 3. A number of speed entry anchors
// 4. Volume anchor
// If an other focusable element is inserted or if the order is changed, things will
// malfunction as a flag, state.previousFocus, is set in the 1,3,4 elements and is
// used to determine the behavior of foucus() and blur() for the speed anchor.
it('checks for a certain order in focusable elements in video controls', function() {
var playIndex, speedIndex, firstSpeedEntry, lastSpeedEntry, volumeIndex, foundFirst = false;
$('.video-controls').find('a, :focusable').each(function(index) {
if ($(this).hasClass('video_control')) {
playIndex = index;
}
else if ($(this).parent().hasClass('speeds')) {
speedIndex = index;
}
else if ($(this).hasClass('speed_link')) {
if (!foundFirst) {
firstSpeedEntry = index;
foundFirst = true;
}
lastSpeedEntry = index;
}
else if ($(this).parent().hasClass('volume')) {
volumeIndex = index;
}
});
expect(playIndex+1).toEqual(speedIndex);
expect(speedIndex+1).toEqual(firstSpeedEntry);
expect(lastSpeedEntry+1).toEqual(volumeIndex);
});
}); });
}); });
......
...@@ -9,7 +9,7 @@ class @TabsEditingDescriptor ...@@ -9,7 +9,7 @@ class @TabsEditingDescriptor
### ###
# hide editor/settings bar # hide editor/settings bar
$('.component-edit-header').hide() @element.closest('.component-editor').find('.component-edit-header').hide()
@$tabs = $(".tab", @element) @$tabs = $(".tab", @element)
@$content = $(".component-tab", @element) @$content = $(".component-tab", @element)
......
...@@ -77,6 +77,11 @@ function () { ...@@ -77,6 +77,11 @@ function () {
state.el.on('mousemove', state.videoControl.showControls); state.el.on('mousemove', state.videoControl.showControls);
state.el.on('keydown', state.videoControl.showControls); state.el.on('keydown', state.videoControl.showControls);
} }
// The state.previousFocus is used in video_speed_control to track
// the element that had the focus before it.
state.videoControl.playPauseEl.on('blur', function () {
state.previousFocus = 'playPause';
});
} }
// *************************************************************** // ***************************************************************
......
...@@ -125,6 +125,9 @@ function () { ...@@ -125,6 +125,9 @@ function () {
// We store the fact that previous element that lost focus was // We store the fact that previous element that lost focus was
// the volume clontrol. // the volume clontrol.
state.volumeBlur = true; state.volumeBlur = true;
// The following field is used in video_speed_control to track
// the element that had the focus before it.
state.previousFocus = 'volume';
}); });
} }
......
...@@ -20,7 +20,8 @@ function ( ...@@ -20,7 +20,8 @@ function (
VideoSpeedControl, VideoSpeedControl,
VideoCaption VideoCaption
) { ) {
var previousState; var previousState,
youtubeXhr = null;
// Because this constructor can be called multiple times on a single page (when // Because this constructor can be called multiple times on a single page (when
// the user switches verticals, the page doesn't reload, but the content changes), we must // the user switches verticals, the page doesn't reload, but the content changes), we must
...@@ -53,7 +54,11 @@ function ( ...@@ -53,7 +54,11 @@ function (
state = {}; state = {};
previousState = state; previousState = state;
state.youtubeXhr = youtubeXhr;
Initialize(state, element); Initialize(state, element);
if (!youtubeXhr) {
youtubeXhr = state.youtubeXhr;
}
VideoControl(state); VideoControl(state);
VideoQualityControl(state); VideoQualityControl(state);
...@@ -67,6 +72,10 @@ function ( ...@@ -67,6 +72,10 @@ function (
// Video with Jasmine. // Video with Jasmine.
return state; return state;
}; };
window.Video.clearYoutubeXhr = function () {
youtubeXhr = null;
};
}); });
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); }(RequireJS.requirejs, RequireJS.require, RequireJS.define));
...@@ -2,10 +2,8 @@ from .x_module import XModuleDescriptor, DescriptorSystem ...@@ -2,10 +2,8 @@ from .x_module import XModuleDescriptor, DescriptorSystem
class MakoDescriptorSystem(DescriptorSystem): class MakoDescriptorSystem(DescriptorSystem):
def __init__(self, load_item, resources_fs, error_tracker, def __init__(self, render_template, **kwargs):
render_template, **kwargs): super(MakoDescriptorSystem, self).__init__(**kwargs)
super(MakoDescriptorSystem, self).__init__(
load_item, resources_fs, error_tracker, **kwargs)
self.render_template = render_template self.render_template = render_template
......
...@@ -398,7 +398,7 @@ class ModuleStoreBase(ModuleStore): ...@@ -398,7 +398,7 @@ class ModuleStoreBase(ModuleStore):
''' '''
Implement interface functionality that can be shared. Implement interface functionality that can be shared.
''' '''
def __init__(self, metadata_inheritance_cache_subsystem=None, request_cache=None, modulestore_update_signal=None): def __init__(self, metadata_inheritance_cache_subsystem=None, request_cache=None, modulestore_update_signal=None, xblock_mixins=()):
''' '''
Set up the error-tracking logic. Set up the error-tracking logic.
''' '''
...@@ -406,6 +406,7 @@ class ModuleStoreBase(ModuleStore): ...@@ -406,6 +406,7 @@ class ModuleStoreBase(ModuleStore):
self.metadata_inheritance_cache_subsystem = metadata_inheritance_cache_subsystem self.metadata_inheritance_cache_subsystem = metadata_inheritance_cache_subsystem
self.modulestore_update_signal = modulestore_update_signal self.modulestore_update_signal = modulestore_update_signal
self.request_cache = request_cache self.request_cache = request_cache
self.xblock_mixins = xblock_mixins
def _get_errorlog(self, location): def _get_errorlog(self, location):
""" """
......
...@@ -62,6 +62,7 @@ def create_modulestore_instance(engine, options): ...@@ -62,6 +62,7 @@ def create_modulestore_instance(engine, options):
metadata_inheritance_cache_subsystem=metadata_inheritance_cache, metadata_inheritance_cache_subsystem=metadata_inheritance_cache,
request_cache=request_cache, request_cache=request_cache,
modulestore_update_signal=Signal(providing_args=['modulestore', 'course_id', 'location']), modulestore_update_signal=Signal(providing_args=['modulestore', 'course_id', 'location']),
xblock_mixins=getattr(settings, 'XBLOCK_MIXINS', ()),
**_options **_options
) )
......
from xblock.core import Scope from datetime import datetime
from pytz import UTC
# A list of metadata that this module can inherit from its parent module
INHERITABLE_METADATA = ( from xblock.fields import Scope, Boolean, String, Float, XBlockMixin
'graded', 'start', 'due', 'graceperiod', 'showanswer', 'rerandomize', from xmodule.fields import Date, Timedelta
# TODO (ichuang): used for Fall 2012 xqa server access from xblock.runtime import KeyValueStore
'xqa_key',
# How many days early to show a course element to beta testers (float)
# intended to be set per-course, but can be overridden in for specific class InheritanceMixin(XBlockMixin):
# elements. Can be a float. """Field definitions for inheritable fields"""
'days_early_for_beta',
'giturl', # for git edit link graded = Boolean(
'static_asset_path', # for static assets placed outside xcontent contentstore help="Whether this module contributes to the final course grade",
) default=False,
scope=Scope.settings
)
start = Date(
help="Start time when this module is visible",
default=datetime.fromtimestamp(0, UTC),
scope=Scope.settings
)
due = Date(help="Date that this problem is due by", scope=Scope.settings)
giturl = String(help="url root for course data git repository", scope=Scope.settings)
xqa_key = String(help="DO NOT USE", scope=Scope.settings)
graceperiod = Timedelta(
help="Amount of time after the due date that submissions will be accepted",
scope=Scope.settings
)
showanswer = String(
help="When to show the problem answer to the student",
scope=Scope.settings,
default="finished"
)
rerandomize = String(
help="When to rerandomize the problem",
default="never",
scope=Scope.settings
)
days_early_for_beta = Float(
help="Number of days early to show content to beta users",
default=None,
scope=Scope.settings
)
static_asset_path = String(help="Path to use for static assets - overrides Studio c4x://", scope=Scope.settings, default='')
def compute_inherited_metadata(descriptor): def compute_inherited_metadata(descriptor):
...@@ -21,59 +52,69 @@ def compute_inherited_metadata(descriptor): ...@@ -21,59 +52,69 @@ def compute_inherited_metadata(descriptor):
NOTE: This means that there is no such thing as lazy loading at the NOTE: This means that there is no such thing as lazy loading at the
moment--this accesses all the children.""" moment--this accesses all the children."""
for child in descriptor.get_children(): if descriptor.has_children:
inherit_metadata(child, descriptor._model_data) parent_metadata = descriptor.xblock_kvs.inherited_settings.copy()
compute_inherited_metadata(child) # add any of descriptor's explicitly set fields to the inheriting list
for field in InheritanceMixin.fields.values():
# pylint: disable = W0212
if descriptor._field_data.has(descriptor, field.name):
# inherited_settings values are json repr
parent_metadata[field.name] = field.read_json(descriptor)
for child in descriptor.get_children():
inherit_metadata(child, parent_metadata)
compute_inherited_metadata(child)
def inherit_metadata(descriptor, model_data):
def inherit_metadata(descriptor, inherited_data):
""" """
Updates this module with metadata inherited from a containing module. Updates this module with metadata inherited from a containing module.
Only metadata specified in self.inheritable_metadata will Only metadata specified in self.inheritable_metadata will
be inherited be inherited
"""
# The inherited values that are actually being used.
if not hasattr(descriptor, '_inherited_metadata'):
setattr(descriptor, '_inherited_metadata', {})
# All inheritable metadata values (for which a value exists in model_data). `inherited_data`: A dictionary mapping field names to the values that
if not hasattr(descriptor, '_inheritable_metadata'): they should inherit
setattr(descriptor, '_inheritable_metadata', {}) """
try:
# Set all inheritable metadata from kwargs that are descriptor.xblock_kvs.inherited_settings = inherited_data
# in self.inheritable_metadata and aren't already set in metadata except AttributeError: # the kvs doesn't have inherited_settings probably b/c it's an error module
for attr in INHERITABLE_METADATA: pass
if attr in model_data:
descriptor._inheritable_metadata[attr] = model_data[attr]
if attr not in descriptor._model_data:
descriptor._inherited_metadata[attr] = model_data[attr]
descriptor._model_data[attr] = model_data[attr]
def own_metadata(module): def own_metadata(module):
# IN SPLIT MONGO this is just ['metadata'] as it keeps ['_inherited_metadata'] separate!
# FIXME move into kvs? will that work for xml mongo?
""" """
Return a dictionary that contains only non-inherited field keys, Return a dictionary that contains only non-inherited field keys,
mapped to their values mapped to their serialized values
""" """
inherited_metadata = getattr(module, '_inherited_metadata', {}) return module.get_explicitly_set_fields_by_scope(Scope.settings)
metadata = {}
for field in module.fields + module.lms.fields: class InheritanceKeyValueStore(KeyValueStore):
# Only save metadata that wasn't inherited """
if field.scope != Scope.settings: Common superclass for kvs's which know about inheritance of settings. Offers simple
continue dict-based storage of fields and lookup of inherited values.
if field.name in inherited_metadata and module._model_data.get(field.name) == inherited_metadata.get(field.name): Note: inherited_settings is a dict of key to json values (internal xblock field repr)
continue """
def __init__(self, initial_values=None, inherited_settings=None):
if field.name not in module._model_data: super(InheritanceKeyValueStore, self).__init__()
continue self.inherited_settings = inherited_settings or {}
self._fields = initial_values or {}
try:
metadata[field.name] = module._model_data[field.name] def get(self, key):
except KeyError: return self._fields[key.field_name]
# Ignore any missing keys in _model_data
pass def set(self, key, value):
# xml backed courses are read-only, but they do have some computed fields
return metadata self._fields[key.field_name] = value
def delete(self, key):
del self._fields[key.field_name]
def has(self, key):
return key.field_name in self._fields
def default(self, key):
"""
Check to see if the default should be from inheritance rather than from the field's global default
"""
return self.inherited_settings[key.field_name]
...@@ -6,7 +6,6 @@ from __future__ import absolute_import ...@@ -6,7 +6,6 @@ from __future__ import absolute_import
import logging import logging
import inspect import inspect
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
from urllib import quote
from bson.objectid import ObjectId from bson.objectid import ObjectId
from bson.errors import InvalidId from bson.errors import InvalidId
...@@ -19,6 +18,15 @@ from .parsers import BRANCH_PREFIX, BLOCK_PREFIX, URL_VERSION_PREFIX ...@@ -19,6 +18,15 @@ from .parsers import BRANCH_PREFIX, BLOCK_PREFIX, URL_VERSION_PREFIX
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class LocalId(object):
"""
Class for local ids for non-persisted xblocks
Should be hashable and distinguishable, but nothing else
"""
pass
class Locator(object): class Locator(object):
""" """
A locator is like a URL, it refers to a course resource. A locator is like a URL, it refers to a course resource.
...@@ -386,9 +394,12 @@ class BlockUsageLocator(CourseLocator): ...@@ -386,9 +394,12 @@ class BlockUsageLocator(CourseLocator):
self.set_property('usage_id', new) self.set_property('usage_id', new)
def init_block_ref(self, block_ref): def init_block_ref(self, block_ref):
parse = parse_block_ref(block_ref) if isinstance(block_ref, LocalId):
assert parse, 'Could not parse "%s" as a block_ref' % block_ref self.set_usage_id(block_ref)
self.set_usage_id(parse['block']) else:
parse = parse_block_ref(block_ref)
assert parse, 'Could not parse "%s" as a block_ref' % block_ref
self.set_usage_id(parse['block'])
def init_block_ref_from_url(self, url): def init_block_ref_from_url(self, url):
if isinstance(url, Locator): if isinstance(url, Locator):
...@@ -409,12 +420,8 @@ class BlockUsageLocator(CourseLocator): ...@@ -409,12 +420,8 @@ class BlockUsageLocator(CourseLocator):
""" """
Return a string representing this location. Return a string representing this location.
""" """
rep = CourseLocator.__unicode__(self) rep = super(BlockUsageLocator, self).__unicode__()
if self.usage_id is None: return rep + BLOCK_PREFIX + unicode(self.usage_id)
# usage_id has not been initialized
return rep + BLOCK_PREFIX + 'NONE'
else:
return rep + BLOCK_PREFIX + self.usage_id
class DescriptionLocator(Locator): class DescriptionLocator(Locator):
......
...@@ -29,9 +29,9 @@ class MixedModuleStore(ModuleStoreBase): ...@@ -29,9 +29,9 @@ class MixedModuleStore(ModuleStoreBase):
if 'default' not in stores: if 'default' not in stores:
raise Exception('Missing a default modulestore in the MixedModuleStore __init__ method.') raise Exception('Missing a default modulestore in the MixedModuleStore __init__ method.')
for key in stores: for key, store in stores.items():
self.modulestores[key] = create_modulestore_instance(stores[key]['ENGINE'], self.modulestores[key] = create_modulestore_instance(store['ENGINE'],
stores[key]['OPTIONS']) store['OPTIONS'])
def _get_modulestore_for_courseid(self, course_id): def _get_modulestore_for_courseid(self, course_id):
""" """
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
Provide names as exported by older mongo.py module Provide names as exported by older mongo.py module
""" """
from xmodule.modulestore.mongo.base import MongoModuleStore, MongoKeyValueStore, MongoUsage from xmodule.modulestore.mongo.base import MongoModuleStore, MongoKeyValueStore
# Backwards compatibility for prod systems that refererence # Backwards compatibility for prod systems that refererence
# xmodule.modulestore.mongo.DraftMongoModuleStore # xmodule.modulestore.mongo.DraftMongoModuleStore
......
...@@ -42,7 +42,7 @@ def wrap_draft(item): ...@@ -42,7 +42,7 @@ def wrap_draft(item):
non-draft location in either case non-draft location in either case
""" """
setattr(item, 'is_draft', item.location.revision == DRAFT) setattr(item, 'is_draft', item.location.revision == DRAFT)
item.location = item.location.replace(revision=None) item.scope_ids = item.scope_ids._replace(usage_id=item.location.replace(revision=None))
return item return item
...@@ -235,10 +235,10 @@ class DraftModuleStore(MongoModuleStore): ...@@ -235,10 +235,10 @@ class DraftModuleStore(MongoModuleStore):
""" """
draft = self.get_item(location) draft = self.get_item(location)
draft.cms.published_date = datetime.now(UTC) draft.published_date = datetime.now(UTC)
draft.cms.published_by = published_by_id draft.published_by = published_by_id
super(DraftModuleStore, self).update_item(location, draft._model_data._kvs._data) super(DraftModuleStore, self).update_item(location, draft._field_data._kvs._data)
super(DraftModuleStore, self).update_children(location, draft._model_data._kvs._children) super(DraftModuleStore, self).update_children(location, draft._field_data._kvs._children)
super(DraftModuleStore, self).update_metadata(location, own_metadata(draft)) super(DraftModuleStore, self).update_metadata(location, own_metadata(draft))
self.delete_item(location) self.delete_item(location)
......
...@@ -2,15 +2,17 @@ import sys ...@@ -2,15 +2,17 @@ import sys
import logging import logging
from xmodule.mako_module import MakoDescriptorSystem from xmodule.mako_module import MakoDescriptorSystem
from xmodule.x_module import XModuleDescriptor from xmodule.x_module import XModuleDescriptor
from xmodule.modulestore.locator import BlockUsageLocator from xmodule.modulestore.locator import BlockUsageLocator, LocalId
from xmodule.error_module import ErrorDescriptor from xmodule.error_module import ErrorDescriptor
from xmodule.errortracker import exc_info_to_str from xmodule.errortracker import exc_info_to_str
from xblock.runtime import DbModel from xblock.runtime import DbModel
from ..exceptions import ItemNotFoundError from ..exceptions import ItemNotFoundError
from .split_mongo_kvs import SplitMongoKVS, SplitMongoKVSid from .split_mongo_kvs import SplitMongoKVS
from xblock.fields import ScopeIds
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class CachingDescriptorSystem(MakoDescriptorSystem): class CachingDescriptorSystem(MakoDescriptorSystem):
""" """
A system that has a cache of a course version's json that it will use to load modules A system that has a cache of a course version's json that it will use to load modules
...@@ -18,8 +20,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem): ...@@ -18,8 +20,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
Computes the settings (nee 'metadata') inheritance upon creation. Computes the settings (nee 'metadata') inheritance upon creation.
""" """
def __init__(self, modulestore, course_entry, module_data, lazy, def __init__(self, modulestore, course_entry, default_class, module_data, lazy, **kwargs):
default_class, error_tracker, render_template):
""" """
Computes the settings inheritance and sets up the cache. Computes the settings inheritance and sets up the cache.
...@@ -28,34 +29,31 @@ class CachingDescriptorSystem(MakoDescriptorSystem): ...@@ -28,34 +29,31 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
module_data: a dict mapping Location -> json that was cached from the module_data: a dict mapping Location -> json that was cached from the
underlying modulestore underlying modulestore
default_class: The default_class to use when loading an
XModuleDescriptor from the module_data
resources_fs: a filesystem, as per MakoDescriptorSystem
error_tracker: a function that logs errors for later display to users
render_template: a function for rendering templates, as per
MakoDescriptorSystem
""" """
# TODO find all references to resources_fs and make handle None # TODO find all references to resources_fs and make handle None
super(CachingDescriptorSystem, self).__init__( super(CachingDescriptorSystem, self).__init__(load_item=self._load_item, **kwargs)
self._load_item, None, error_tracker, render_template)
self.modulestore = modulestore self.modulestore = modulestore
self.course_entry = course_entry self.course_entry = course_entry
self.lazy = lazy self.lazy = lazy
self.module_data = module_data self.module_data = module_data
self.default_class = default_class
# TODO see if self.course_id is needed: is already in course_entry but could be > 1 value # TODO see if self.course_id is needed: is already in course_entry but could be > 1 value
# Compute inheritance # Compute inheritance
modulestore.inherit_settings( modulestore.inherit_settings(
course_entry.get('blocks', {}), course_entry.get('blocks', {}),
course_entry.get('blocks', {}).get(course_entry.get('root')) course_entry.get('blocks', {}).get(course_entry.get('root'))
) )
self.default_class = default_class
self.local_modules = {}
def _load_item(self, usage_id, course_entry_override=None): def _load_item(self, usage_id, course_entry_override=None):
# TODO ensure all callers of system.load_item pass just the id # TODO ensure all callers of system.load_item pass just the id
if isinstance(usage_id, BlockUsageLocator) and isinstance(usage_id.usage_id, LocalId):
try:
return self.local_modules[usage_id]
except KeyError:
raise ItemNotFoundError
json_data = self.module_data.get(usage_id) json_data = self.module_data.get(usage_id)
if json_data is None: if json_data is None:
# deeper than initial descendant fetch or doesn't exist # deeper than initial descendant fetch or doesn't exist
...@@ -75,6 +73,11 @@ class CachingDescriptorSystem(MakoDescriptorSystem): ...@@ -75,6 +73,11 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
course_entry_override = self.course_entry course_entry_override = self.course_entry
# most likely a lazy loader or the id directly # most likely a lazy loader or the id directly
definition = json_data.get('definition', {}) definition = json_data.get('definition', {})
definition_id = self.modulestore.definition_locator(definition)
# If no usage id is provided, generate an in-memory id
if usage_id is None:
usage_id = LocalId()
block_locator = BlockUsageLocator( block_locator = BlockUsageLocator(
version_guid=course_entry_override['_id'], version_guid=course_entry_override['_id'],
...@@ -87,25 +90,24 @@ class CachingDescriptorSystem(MakoDescriptorSystem): ...@@ -87,25 +90,24 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
definition, definition,
json_data.get('fields', {}), json_data.get('fields', {}),
json_data.get('_inherited_settings'), json_data.get('_inherited_settings'),
block_locator, )
json_data.get('category')) field_data = DbModel(kvs)
model_data = DbModel(kvs, class_, None,
SplitMongoKVSid(
# DbModel req's that these support .url()
block_locator,
self.modulestore.definition_locator(definition)))
try: try:
module = class_(self, model_data) module = self.construct_xblock_from_class(
class_,
field_data,
ScopeIds(None, json_data.get('category'), definition_id, block_locator)
)
except Exception: except Exception:
log.warning("Failed to load descriptor", exc_info=True) log.warning("Failed to load descriptor", exc_info=True)
if usage_id is None:
usage_id = "MISSING"
return ErrorDescriptor.from_json( return ErrorDescriptor.from_json(
json_data, json_data,
self, self,
BlockUsageLocator(version_guid=course_entry_override['_id'], BlockUsageLocator(
usage_id=usage_id), version_guid=course_entry_override['_id'],
usage_id=usage_id
),
error_msg=exc_info_to_str(sys.exc_info()) error_msg=exc_info_to_str(sys.exc_info())
) )
...@@ -117,4 +119,9 @@ class CachingDescriptorSystem(MakoDescriptorSystem): ...@@ -117,4 +119,9 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
module.definition_locator = self.modulestore.definition_locator(definition) module.definition_locator = self.modulestore.definition_locator(definition)
# decache any pending field settings # decache any pending field settings
module.save() module.save()
# If this is an in-memory block, store it in this system
if isinstance(block_locator.usage_id, LocalId):
self.local_modules[block_locator] = module
return module return module
...@@ -8,14 +8,15 @@ from path import path ...@@ -8,14 +8,15 @@ from path import path
from xmodule.errortracker import null_error_tracker from xmodule.errortracker import null_error_tracker
from xmodule.x_module import XModuleDescriptor from xmodule.x_module import XModuleDescriptor
from xmodule.modulestore.locator import BlockUsageLocator, DescriptionLocator, CourseLocator, VersionTree from xmodule.modulestore.locator import BlockUsageLocator, DescriptionLocator, CourseLocator, VersionTree, LocalId
from xmodule.modulestore.exceptions import InsufficientSpecificationError, VersionConflictError from xmodule.modulestore.exceptions import InsufficientSpecificationError, VersionConflictError
from xmodule.modulestore import inheritance, ModuleStoreBase from xmodule.modulestore import inheritance, ModuleStoreBase
from ..exceptions import ItemNotFoundError from ..exceptions import ItemNotFoundError
from .definition_lazy_loader import DefinitionLazyLoader from .definition_lazy_loader import DefinitionLazyLoader
from .caching_descriptor_system import CachingDescriptorSystem from .caching_descriptor_system import CachingDescriptorSystem
from xblock.core import Scope from xblock.fields import Scope
from xblock.runtime import Mixologist
from pytz import UTC from pytz import UTC
import collections import collections
...@@ -41,6 +42,8 @@ log = logging.getLogger(__name__) ...@@ -41,6 +42,8 @@ log = logging.getLogger(__name__)
#============================================================================== #==============================================================================
class SplitMongoModuleStore(ModuleStoreBase): class SplitMongoModuleStore(ModuleStoreBase):
""" """
A Mongodb backed ModuleStore supporting versions, inheritance, A Mongodb backed ModuleStore supporting versions, inheritance,
...@@ -53,7 +56,7 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -53,7 +56,7 @@ class SplitMongoModuleStore(ModuleStoreBase):
mongo_options=None, mongo_options=None,
**kwargs): **kwargs):
ModuleStoreBase.__init__(self) super(SplitMongoModuleStore, self).__init__(**kwargs)
if mongo_options is None: if mongo_options is None:
mongo_options = {} mongo_options = {}
...@@ -93,6 +96,11 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -93,6 +96,11 @@ class SplitMongoModuleStore(ModuleStoreBase):
self.error_tracker = error_tracker self.error_tracker = error_tracker
self.render_template = render_template self.render_template = render_template
# TODO: Don't have a runtime just to generate the appropriate mixin classes (cpennington)
# This is only used by _partition_fields_by_scope, which is only needed because
# the split mongo store is used for item creation as well as item persistence
self.mixologist = Mixologist(self.xblock_mixins)
def cache_items(self, system, base_usage_ids, depth=0, lazy=True): def cache_items(self, system, base_usage_ids, depth=0, lazy=True):
''' '''
Handles caching of items once inheritance and any other one time Handles caching of items once inheritance and any other one time
...@@ -144,13 +152,15 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -144,13 +152,15 @@ class SplitMongoModuleStore(ModuleStoreBase):
system = self._get_cache(course_entry['_id']) system = self._get_cache(course_entry['_id'])
if system is None: if system is None:
system = CachingDescriptorSystem( system = CachingDescriptorSystem(
self, modulestore=self,
course_entry, course_entry=course_entry,
{}, module_data={},
lazy, lazy=lazy,
self.default_class, default_class=self.default_class,
self.error_tracker, error_tracker=self.error_tracker,
self.render_template render_template=self.render_template,
resources_fs=None,
mixins=self.xblock_mixins
) )
self._add_cache(course_entry['_id'], system) self._add_cache(course_entry['_id'], system)
self.cache_items(system, usage_ids, depth, lazy) self.cache_items(system, usage_ids, depth, lazy)
...@@ -943,12 +953,12 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -943,12 +953,12 @@ class SplitMongoModuleStore(ModuleStoreBase):
xblock.definition_locator, is_updated = self.update_definition_from_data( xblock.definition_locator, is_updated = self.update_definition_from_data(
xblock.definition_locator, new_def_data, user_id) xblock.definition_locator, new_def_data, user_id)
if xblock.location.usage_id is None: if isinstance(xblock.scope_ids.usage_id.usage_id, LocalId):
# generate an id # generate an id
is_new = True is_new = True
is_updated = True is_updated = True
usage_id = self._generate_usage_id(structure_blocks, xblock.category) usage_id = self._generate_usage_id(structure_blocks, xblock.category)
xblock.location.usage_id = usage_id xblock.scope_ids.usage_id.usage_id = usage_id
else: else:
is_new = False is_new = False
usage_id = xblock.location.usage_id usage_id = xblock.location.usage_id
...@@ -960,9 +970,10 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -960,9 +970,10 @@ class SplitMongoModuleStore(ModuleStoreBase):
updated_blocks = [] updated_blocks = []
if xblock.has_children: if xblock.has_children:
for child in xblock.children: for child in xblock.children:
if isinstance(child, XModuleDescriptor): if isinstance(child.usage_id, LocalId):
updated_blocks += self._persist_subdag(child, user_id, structure_blocks) child_block = xblock.system.get_block(child)
children.append(child.location.usage_id) updated_blocks += self._persist_subdag(child_block, user_id, structure_blocks)
children.append(child_block.location.usage_id)
else: else:
children.append(child) children.append(child)
...@@ -1118,11 +1129,11 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -1118,11 +1129,11 @@ class SplitMongoModuleStore(ModuleStoreBase):
""" """
return {} return {}
def inherit_settings(self, block_map, block, inheriting_settings=None): def inherit_settings(self, block_map, block_json, inheriting_settings=None):
""" """
Updates block with any inheritable setting set by an ancestor and recurses to children. Updates block_json with any inheritable setting set by an ancestor and recurses to children.
""" """
if block is None: if block_json is None:
return return
if inheriting_settings is None: if inheriting_settings is None:
...@@ -1132,14 +1143,14 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -1132,14 +1143,14 @@ class SplitMongoModuleStore(ModuleStoreBase):
# NOTE: this should show the values which all fields would have if inherited: i.e., # NOTE: this should show the values which all fields would have if inherited: i.e.,
# not set to the locally defined value but to value set by nearest ancestor who sets it # not set to the locally defined value but to value set by nearest ancestor who sets it
# ALSO NOTE: no xblock should ever define a _inherited_settings field as it will collide w/ this logic. # ALSO NOTE: no xblock should ever define a _inherited_settings field as it will collide w/ this logic.
block.setdefault('_inherited_settings', {}).update(inheriting_settings) block_json.setdefault('_inherited_settings', {}).update(inheriting_settings)
# update the inheriting w/ what should pass to children # update the inheriting w/ what should pass to children
inheriting_settings = block['_inherited_settings'].copy() inheriting_settings = block_json['_inherited_settings'].copy()
block_fields = block['fields'] block_fields = block_json['fields']
for field in inheritance.INHERITABLE_METADATA: for field_name in inheritance.InheritanceMixin.fields:
if field in block_fields: if field_name in block_fields:
inheriting_settings[field] = block_fields[field] inheriting_settings[field_name] = block_fields[field_name]
for child in block_fields.get('children', []): for child in block_fields.get('children', []):
self.inherit_settings(block_map, block_map[child], inheriting_settings) self.inherit_settings(block_map, block_map[child], inheriting_settings)
...@@ -1308,7 +1319,7 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -1308,7 +1319,7 @@ class SplitMongoModuleStore(ModuleStoreBase):
""" """
if fields is None: if fields is None:
return {} return {}
cls = XModuleDescriptor.load_class(category) cls = self.mixologist.mix(XModuleDescriptor.load_class(category))
result = collections.defaultdict(dict) result = collections.defaultdict(dict)
for field_name, value in fields.iteritems(): for field_name, value in fields.iteritems():
field = getattr(cls, field_name) field = getattr(cls, field_name)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment