Teach LMS how to render XBlockAsides

[PLAT-217]
This commit is contained in:
Calen Pennington
2014-12-05 13:39:53 -05:00
parent e20fe2b879
commit d919d2ae46
47 changed files with 669 additions and 183 deletions

View File

@@ -12,16 +12,17 @@ from .models import (
XModuleStudentInfoField
)
import logging
from opaque_keys.edx.locations import SlashSeparatedCourseKey, Location
from opaque_keys.edx.keys import CourseKey, UsageKey
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.block_types import BlockTypeKeyV1
from opaque_keys.edx.asides import AsideUsageKeyV1
from django.db import DatabaseError
from django.contrib.auth.models import User
from xblock.runtime import KeyValueStore
from xblock.exceptions import KeyValueMultiSaveError, InvalidScopeError
from xblock.fields import Scope, UserScope
from xmodule.modulestore.django import modulestore
from xblock.core import XBlockAside
log = logging.getLogger(__name__)
@@ -46,7 +47,7 @@ class FieldDataCache(object):
A cache of django model objects needed to supply the data
for a module and its decendants
"""
def __init__(self, descriptors, course_id, user, select_for_update=False):
def __init__(self, descriptors, course_id, user, select_for_update=False, asides=None):
'''
Find any courseware.models objects that are needed by any descriptor
in descriptors. Attempts to minimize the number of queries to the database.
@@ -58,11 +59,17 @@ class FieldDataCache(object):
course_id: The id of the current course
user: The user for which to cache data
select_for_update: True if rows should be locked until end of transaction
asides: The list of aside types to load, or None to prefetch no asides.
'''
self.cache = {}
self.descriptors = descriptors
self.select_for_update = select_for_update
if asides is None:
self.asides = []
else:
self.asides = asides
assert isinstance(course_id, CourseKey)
self.course_id = course_id
self.user = user
@@ -75,7 +82,7 @@ class FieldDataCache(object):
@classmethod
def cache_for_descriptor_descendents(cls, course_id, user, descriptor, depth=None,
descriptor_filter=lambda descriptor: True,
select_for_update=False):
select_for_update=False, asides=None):
"""
course_id: the course in the context of which we want StudentModules.
user: the django user for whom to load modules.
@@ -113,7 +120,7 @@ class FieldDataCache(object):
with modulestore().bulk_operations(descriptor.location.course_key):
descriptors = get_child_descriptors(descriptor, depth, descriptor_filter)
return FieldDataCache(descriptors, course_id, user, select_for_update)
return FieldDataCache(descriptors, course_id, user, select_for_update, asides=asides)
def _query(self, model_class, **kwargs):
"""
@@ -140,6 +147,35 @@ class FieldDataCache(object):
)
return res
@property
def _all_usage_ids(self):
"""
Return a set of all usage_ids for the descriptors that this FieldDataCache is caching
against, and well as all asides for those descriptors.
"""
usage_ids = set()
for descriptor in self.descriptors:
usage_ids.add(descriptor.scope_ids.usage_id)
for aside_type in self.asides:
usage_ids.add(AsideUsageKeyV1(descriptor.scope_ids.usage_id, aside_type))
return usage_ids
@property
def _all_block_types(self):
"""
Return a set of all block_types that are cached by this FieldDataCache.
"""
block_types = set()
for descriptor in self.descriptors:
block_types.add(BlockTypeKeyV1(descriptor.entry_point, descriptor.scope_ids.block_type))
for aside_type in self.asides:
block_types.add(BlockTypeKeyV1(XBlockAside.entry_point, aside_type))
return block_types
def _retrieve_fields(self, scope, fields):
"""
Queries the database for all of the fields in the specified scope
@@ -148,7 +184,7 @@ class FieldDataCache(object):
return self._chunked_query(
StudentModule,
'module_state_key__in',
(descriptor.scope_ids.usage_id for descriptor in self.descriptors),
self._all_usage_ids,
course_id=self.course_id,
student=self.user.pk,
)
@@ -156,14 +192,14 @@ class FieldDataCache(object):
return self._chunked_query(
XModuleUserStateSummaryField,
'usage_id__in',
(descriptor.scope_ids.usage_id for descriptor in self.descriptors),
self._all_usage_ids,
field_name__in=set(field.name for field in fields),
)
elif scope == Scope.preferences:
return self._chunked_query(
XModuleStudentPrefsField,
'module_type__in',
set(descriptor.scope_ids.block_type for descriptor in self.descriptors),
self._all_block_types,
student=self.user.pk,
field_name__in=set(field.name for field in fields),
)
@@ -195,7 +231,7 @@ class FieldDataCache(object):
elif key.scope == Scope.user_state_summary:
return (key.scope, key.block_scope_id, key.field_name)
elif key.scope == Scope.preferences:
return (key.scope, key.block_scope_id, key.field_name)
return (key.scope, BlockTypeKeyV1(key.block_family, key.block_scope_id), key.field_name)
elif key.scope == Scope.user_info:
return (key.scope, key.field_name)
@@ -239,31 +275,28 @@ class FieldDataCache(object):
return field_object
if key.scope == Scope.user_state:
# When we start allowing block_scope_ids to be either Locations or Locators,
# this assertion will fail. Fix the code here when that happens!
assert(isinstance(key.block_scope_id, UsageKey))
field_object, _ = StudentModule.objects.get_or_create(
field_object, __ = StudentModule.objects.get_or_create(
course_id=self.course_id,
student_id=key.user_id,
module_state_key=key.block_scope_id,
defaults={
'state': json.dumps({}),
'module_type': key.block_scope_id.category,
'module_type': key.block_scope_id.block_type,
},
)
elif key.scope == Scope.user_state_summary:
field_object, _ = XModuleUserStateSummaryField.objects.get_or_create(
field_object, __ = XModuleUserStateSummaryField.objects.get_or_create(
field_name=key.field_name,
usage_id=key.block_scope_id
)
elif key.scope == Scope.preferences:
field_object, _ = XModuleStudentPrefsField.objects.get_or_create(
field_object, __ = XModuleStudentPrefsField.objects.get_or_create(
field_name=key.field_name,
module_type=key.block_scope_id,
module_type=BlockTypeKeyV1(key.block_family, key.block_scope_id),
student_id=key.user_id,
)
elif key.scope == Scope.user_info:
field_object, _ = XModuleStudentInfoField.objects.get_or_create(
field_object, __ = XModuleStudentInfoField.objects.get_or_create(
field_name=key.field_name,
student_id=key.user_id,
)

View File

@@ -18,7 +18,7 @@ from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
from xmodule_django.models import CourseKeyField, LocationKeyField
from xmodule_django.models import CourseKeyField, LocationKeyField, BlockTypeKeyField
class StudentModule(models.Model):
@@ -36,10 +36,7 @@ class StudentModule(models.Model):
## These three are the key for the object
module_type = models.CharField(max_length=32, choices=MODULE_TYPES, default='problem', db_index=True)
# Key used to share state. By default, this is the module_id,
# but for abtests and the like, this can be set to a shared value
# for many instances of the module.
# Filename for homeworks, etc.
# Key used to share state. This is the XBlock usage_id
module_state_key = LocationKeyField(max_length=255, db_index=True, db_column='module_id')
student = models.ForeignKey(User, db_index=True)
@@ -150,7 +147,7 @@ class XBlockFieldBase(models.Model):
return u'{}<{!r}'.format(
self.__class__.__name__,
{
key:getattr(self, key)
key: getattr(self, key)
for key in self._meta.get_all_field_names()
if key not in ('created', 'modified')
}
@@ -174,11 +171,11 @@ class XModuleStudentPrefsField(XBlockFieldBase):
Stores data set in the Scope.preferences scope by an xmodule field
"""
class Meta:
class Meta: # pylint: disable=missing-docstring
unique_together = (('student', 'module_type', 'field_name'),)
# The type of the module for these preferences
module_type = models.CharField(max_length=64, db_index=True)
module_type = BlockTypeKeyField(max_length=64, db_index=True)
student = models.ForeignKey(User, db_index=True)

View File

@@ -23,6 +23,7 @@ from courseware.masquerade import setup_masquerade
from courseware.model_data import FieldDataCache, DjangoKeyValueStore
from lms.djangoapps.lms_xblock.field_data import LmsFieldData
from lms.djangoapps.lms_xblock.runtime import LmsModuleSystem, unquote_slashes, quote_slashes
from lms.djangoapps.lms_xblock.models import XBlockAsidesConfig
from edxmako.shortcuts import render_to_string
from eventtracking import tracker
from psychometrics.psychoanalyze import make_psychometrics_data_update_handler
@@ -405,7 +406,8 @@ def get_module_system_for_user(user, field_data_cache,
field_data_cache_real_user = FieldDataCache.cache_for_descriptor_descendents(
course_id,
real_user,
module.descriptor
module.descriptor,
asides=XBlockAsidesConfig.possible_asides(),
)
(inner_system, inner_student_data) = get_module_system_for_user(
@@ -496,6 +498,8 @@ def get_module_system_for_user(user, field_data_cache,
else:
anonymous_student_id = anonymous_id_for_user(user, None)
field_data = LmsFieldData(descriptor._field_data, student_data) # pylint: disable=protected-access
system = LmsModuleSystem(
track_function=track_function,
render_template=render_to_string,
@@ -541,11 +545,13 @@ def get_module_system_for_user(user, field_data_cache,
services={
'i18n': ModuleI18nService(),
'fs': xblock.reference.plugins.FSService(),
'field-data': field_data,
},
get_user_role=lambda: get_user_role(user, course_id),
descriptor_runtime=descriptor.runtime,
rebind_noauth_module_to_user=rebind_noauth_module_to_user,
user_location=user_location,
request_token=request_token,
)
# pass position specified in URL to module through ModuleSystem
@@ -572,7 +578,7 @@ def get_module_system_for_user(user, field_data_cache,
else:
system.error_descriptor_class = NonStaffErrorDescriptor
return system, student_data
return system, field_data
def get_module_for_descriptor_internal(user, descriptor, field_data_cache, course_id, # pylint: disable=invalid-name
@@ -594,7 +600,7 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours
if not has_access(user, 'load', descriptor, course_id):
return None
(system, student_data) = get_module_system_for_user(
(system, field_data) = get_module_system_for_user(
user=user,
field_data_cache=field_data_cache, # These have implicit user bindings, the rest of args are considered not to
descriptor=descriptor,
@@ -609,7 +615,7 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours
request_token=request_token
)
descriptor.bind_for_student(system, LmsFieldData(descriptor._field_data, student_data)) # pylint: disable=protected-access
descriptor.bind_for_student(system, field_data) # pylint: disable=protected-access
descriptor.scope_ids = descriptor.scope_ids._replace(user_id=user.id) # pylint: disable=protected-access
return descriptor

View File

@@ -16,9 +16,10 @@ from courseware.tests.factories import UserStateSummaryFactory
from courseware.tests.factories import StudentPrefsFactory, StudentInfoFactory
from xblock.fields import Scope, BlockScope, ScopeIds
from xblock.exceptions import KeyValueMultiSaveError
from xblock.core import XBlock
from django.test import TestCase
from django.db import DatabaseError
from xblock.exceptions import KeyValueMultiSaveError
def mock_field(scope, name):
@@ -29,7 +30,7 @@ def mock_field(scope, name):
def mock_descriptor(fields=[]):
descriptor = Mock()
descriptor = Mock(entry_point=XBlock.entry_point)
descriptor.scope_ids = ScopeIds('user1', 'mock_problem', location('def_id'), location('usage_id'))
descriptor.module_class.fields.values.return_value = fields
descriptor.fields.values.return_value = fields

View File

@@ -39,6 +39,8 @@ from .module_render import toc_for_course, get_module_for_descriptor, get_module
from courseware.models import StudentModule, StudentModuleHistory
from course_modes.models import CourseMode
from lms.djangoapps.lms_xblock.models import XBlockAsidesConfig
from open_ended_grading import open_ended_notifications
from student.models import UserTestGroup, CourseEnrollment
from student.views import single_course_reverification_info, is_course_blocked
@@ -444,7 +446,8 @@ def _index_bulk_op(request, course_key, chapter, section, position):
# Load all descendants of the section, because we're going to display its
# html, which in general will need all of its children
section_field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
course_key, user, section_descriptor, depth=None)
course_key, user, section_descriptor, depth=None, asides=XBlockAsidesConfig.possible_asides()
)
# Verify that position a string is in fact an int
if position is not None:

View File

@@ -18,6 +18,7 @@ from django.utils.html import escape
from django.http import Http404
from django.conf import settings
from util.json_request import JsonResponse
from mock import patch
from lms.djangoapps.lms_xblock.runtime import quote_slashes
from xmodule_modifiers import wrap_xblock
@@ -323,17 +324,28 @@ def _section_data_download(course, access):
return section_data
def null_get_asides(block): # pylint: disable=unused-argument
"""
get_aside method for monkey-patching into descriptor_global_get_asides
while rendering an HtmlDescriptor for email text editing. This returns
an empty list.
"""
return []
def _section_send_email(course, access):
""" Provide data for the corresponding bulk email section """
course_key = course.id
# This HtmlDescriptor is only being used to generate a nice text editor.
html_module = HtmlDescriptor(
course.system,
DictFieldData({'data': ''}),
ScopeIds(None, None, None, course_key.make_usage_key('html', 'fake'))
)
fragment = course.system.render(html_module, 'studio_view')
# Monkey-patch descriptor_global_get_asides to return no asides for the duration of this render
with patch('xmodule.x_module.descriptor_global_get_asides', null_get_asides):
# This HtmlDescriptor is only being used to generate a nice text editor.
html_module = HtmlDescriptor(
course.system,
DictFieldData({'data': ''}),
ScopeIds(None, None, None, course_key.make_usage_key('html', 'fake'))
)
fragment = course.system.render(html_module, 'studio_view')
fragment = wrap_xblock(
'LmsRuntime', html_module, 'studio_view', fragment, None,
extra_data={"course-id": unicode(course_key)},

View File

@@ -1,3 +1,7 @@
"""
Django admin dashboard configuration for LMS XBlock infrastructure.
"""
from django.contrib import admin
from config_models.admin import ConfigurationModelAdmin
from lms.djangoapps.lms_xblock.models import XBlockAsidesConfig

View File

@@ -16,7 +16,7 @@ class LmsFieldData(SplitFieldData):
def __init__(self, authored_data, student_data):
# Make sure that we don't repeatedly nest LmsFieldData instances
if isinstance(authored_data, LmsFieldData):
authored_data = authored_data._authored_data # pylint: disable=protected-member
authored_data = authored_data._authored_data # pylint: disable=protected-access
else:
authored_data = ReadOnlyFieldData(authored_data)

View File

@@ -1,7 +1,18 @@
"""
Models used by LMS XBlock infrastructure.
Includes:
XBlockAsidesConfig: A ConfigurationModel for managing how XBlockAsides are
rendered in the LMS.
"""
from django.db.models import TextField
from config_models.models import ConfigurationModel
from xblock.core import XBlockAside
class XBlockAsidesConfig(ConfigurationModel):
"""
Configuration for XBlockAsides.
@@ -11,3 +22,10 @@ class XBlockAsidesConfig(ConfigurationModel):
default="about course_info static_tab",
help_text="Space-separated list of XBlocks on which XBlockAsides should never render."
)
@classmethod
def possible_asides(cls):
"""
Return a list of all asides that are enabled across all XBlocks.
"""
return [aside_type for aside_type, __ in XBlockAside.load_classes()]

View File

@@ -7,7 +7,9 @@ import xblock.reference.plugins
from django.core.urlresolvers import reverse
from django.conf import settings
from lms.djangoapps.lms_xblock.models import XBlockAsidesConfig
from openedx.core.djangoapps.user_api.api import course_tag as user_course_tag_api
from xblock.core import XBlockAside
from xmodule.modulestore.django import modulestore
from xmodule.x_module import ModuleSystem
from xmodule.partitions.partitions_service import PartitionService
@@ -87,8 +89,8 @@ class LmsHandlerUrls(object):
view_name = 'xblock_handler_noauth'
url = reverse(view_name, kwargs={
'course_id': self.course_id.to_deprecated_string(),
'usage_id': quote_slashes(block.scope_ids.usage_id.to_deprecated_string().encode('utf-8')),
'course_id': unicode(self.course_id),
'usage_id': quote_slashes(unicode(block.scope_ids.usage_id).encode('utf-8')),
'handler': handler_name,
'suffix': suffix,
})
@@ -198,4 +200,50 @@ class LmsModuleSystem(LmsHandlerUrls, ModuleSystem): # pylint: disable=abstract
track_function=kwargs.get('track_function', None),
)
services['fs'] = xblock.reference.plugins.FSService()
self.request_token = kwargs.pop('request_token', None)
super(LmsModuleSystem, self).__init__(**kwargs)
def wrap_aside(self, block, aside, view, frag, context):
"""
Creates a div which identifies the aside, points to the original block,
and writes out the json_init_args into a script tag.
The default implementation creates a frag to wraps frag w/ a div identifying the xblock. If you have
javascript, you'll need to override this impl
"""
extra_data = {
'block-id': quote_slashes(unicode(block.scope_ids.usage_id)),
'url-selector': 'asideBaseUrl',
'runtime-class': 'LmsRuntime',
}
if self.request_token:
extra_data['request-token'] = self.request_token
return self._wrap_ele(
aside,
view,
frag,
extra_data,
)
def get_asides(self, block):
"""
Return all of the asides which might be decorating this `block`.
Arguments:
block (:class:`.XBlock`): The block to render retrieve asides for.
"""
config = XBlockAsidesConfig.current()
if not config.enabled:
return []
if block.scope_ids.block_type in config.disabled_blocks.split():
return []
return [
self.get_aside_of_type(block, aside_type)
for aside_type, __
in XBlockAside.load_classes()
]

View File

@@ -10,6 +10,7 @@ from unittest import TestCase
from urlparse import urlparse
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from lms.djangoapps.lms_xblock.runtime import quote_slashes, unquote_slashes, LmsModuleSystem
from xblock.fields import ScopeIds
TEST_STRINGS = [
'',
@@ -42,8 +43,7 @@ class TestHandlerUrl(TestCase):
"""Test the LMS handler_url"""
def setUp(self):
self.block = Mock()
self.block.scope_ids.usage_id.to_deprecated_string.return_value.encode.return_value = 'dummy'
self.block = Mock(name='block', scope_ids=ScopeIds(None, None, None, 'dummy'))
self.course_key = SlashSeparatedCourseKey("org", "course", "run")
self.runtime = LmsModuleSystem(
static_url='/static',

View File

@@ -21,7 +21,7 @@ from xblock.fields import ScopeIds
from courseware.tests import factories
from courseware.tests.helpers import LoginEnrollmentTestCase
from lms.lib.xblock.runtime import LmsModuleSystem
from lms.djangoapps.lms_xblock.runtime import LmsModuleSystem
from student.roles import CourseStaffRole
from student.models import unique_id_for_user
from xmodule import peer_grading_module

View File

@@ -1165,7 +1165,7 @@ PIPELINE_JS = {
'application': {
# Application will contain all paths not in courseware_only_js
'source_filenames': sorted(common_js) + sorted(project_js) + [
'source_filenames': ['js/xblock/core.js'] + sorted(common_js) + sorted(project_js) + [
'js/form.ext.js',
'js/my_courses_dropdown.js',
'js/toggle_login_modal.js',
@@ -1514,6 +1514,8 @@ INSTALLED_APPS = (
# Surveys
'survey',
'lms.djangoapps.lms_xblock',
)
######################### MARKETING SITE ###############################

View File

@@ -49,7 +49,7 @@
'tender': '//edxedge.tenderapp.com/tender_widget',
'coffee/src/ajax_prefix': 'xmodule_js/common_static/coffee/src/ajax_prefix',
'xmodule_js/common_static/js/test/add_ajax_prefix': 'xmodule_js/common_static/js/test/add_ajax_prefix',
'xblock/core': 'xmodule_js/common_static/coffee/src/xblock/core',
'xblock/core': 'xmodule_js/common_static/js/xblock/core',
'xblock/runtime.v1': 'xmodule_js/common_static/coffee/src/xblock/runtime.v1',
'xblock/lms.runtime.v1': 'coffee/src/xblock/lms.runtime.v1',
'capa/display': 'xmodule_js/src/capa/display',

View File

@@ -45,6 +45,7 @@ lib_paths:
- xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.iframe-transport.js
- xmodule_js/common_static/js/vendor/url.min.js
- xmodule_js/common_static/coffee/src/jquery.immediateDescendents.js
- xmodule_js/common_static/js/xblock
- xmodule_js/common_static/coffee/src/xblock
- xmodule_js/common_static/js/vendor/sinon-1.7.1.js
- xmodule_js/src/capa/

View File

@@ -42,6 +42,7 @@ lib_paths:
- xmodule_js/common_static/js/vendor/CodeMirror/codemirror.js
- xmodule_js/common_static/js/vendor/URI.min.js
- xmodule_js/common_static/coffee/src/jquery.immediateDescendents.js
- xmodule_js/common_static/js/xblock
- xmodule_js/common_static/coffee/src/xblock
- xmodule_js/src/capa/
- xmodule_js/src/video/