From 95b813473151d42609ec556b52ad3f5c8610d0d6 Mon Sep 17 00:00:00 2001 From: ichuang Date: Thu, 17 Jan 2013 03:40:24 +0000 Subject: [PATCH 1/8] initial implementation of conditional_module; doesn't load dynamically yet --- common/lib/xmodule/setup.py | 1 + common/lib/xmodule/xmodule/capa_module.py | 5 ++ .../lib/xmodule/xmodule/conditional_module.py | 88 +++++++++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 common/lib/xmodule/xmodule/conditional_module.py diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py index 29227c3188..a1b059b889 100644 --- a/common/lib/xmodule/setup.py +++ b/common/lib/xmodule/setup.py @@ -20,6 +20,7 @@ setup( "book = xmodule.backcompat_module:TranslateCustomTagDescriptor", "chapter = xmodule.seq_module:SequenceDescriptor", "combinedopenended = xmodule.combined_open_ended_module:CombinedOpenEndedDescriptor", + "conditional = xmodule.conditional_module:ConditionalDescriptor", "course = xmodule.course_module:CourseDescriptor", "customtag = xmodule.template_module:CustomTagDescriptor", "discuss = xmodule.backcompat_module:TranslateCustomTagDescriptor", diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index f33da6e3a4..01e67d9f8b 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -398,6 +398,11 @@ class CapaModule(XModule): return False + def is_completed(self): + # used by conditional module + # return self.answer_available() + return self.lcp.done + def answer_available(self): ''' Is the user allowed to see an answer? ''' diff --git a/common/lib/xmodule/xmodule/conditional_module.py b/common/lib/xmodule/xmodule/conditional_module.py new file mode 100644 index 0000000000..c022c3742a --- /dev/null +++ b/common/lib/xmodule/xmodule/conditional_module.py @@ -0,0 +1,88 @@ +import logging + +from xmodule.x_module import XModule +from xmodule.modulestore import Location +from xmodule.seq_module import SequenceDescriptor + +log = logging.getLogger('mitx.' + __name__) + +class ConditionalModule(XModule): + ''' + Blocks child module from showing unless certain conditions are met. + + Example: + + + + + ''' + + def __init__(self, system, location, definition, descriptor, instance_state=None, shared_state=None, **kwargs): + """ + In addition to the normal XModule init, provide: + + self.required_module_list = list of (tag, url_name) tuples of modules required by this one. + self.condition = string describing condition required + + """ + XModule.__init__(self, system, location, definition, descriptor, instance_state, shared_state, **kwargs) + self.contents = None + self.required_modules_list = [tuple(x.split('/',1)) for x in self.metadata.get('required','').split('&')] + self.condition = self.metadata.get('condition','') + log.debug('conditional module required=%s' % self.required_modules_list) + + def _get_required_modules(self): + self.required_modules = [] + for (tag, name) in self.required_modules_list: + loc = self.location.dict() + loc['category'] = tag + loc['name'] = name + module = self.system.get_module(loc) + self.required_modules.append(module) + log.debug('required_modules=%s' % (self.required_modules)) + + def is_condition_satisfied(self): + self._get_required_modules() + + if self.condition=='require_completed': + # all required modules must be completed, as determined by + # the modules .is_completed() method + for module in self.required_modules: + if not hasattr(module, 'is_completed'): + raise Exception('Error in conditional module: required module %s has no .is_completed() method' % module) + if not module.is_completed(): + log.debug('condition module: %s not completed' % module) + return False + else: + log.debug('condition module: %s IS completed' % module) + return True + else: + raise Exception('Error in conditional module: unknown condition "%s"' % self.condition) + + return True + + def get_html(self): + if not self.is_condition_satisfied(): + context = {'module': self, + } + return self.system.render_template('conditional_module.html', context) + + if self.contents is None: + self.contents = [child.get_html() for child in self.get_display_items()] + + # for now, just deal with one child + html = self.contents[0] + + log.debug('rendered conditional module %s' % str(self.location)) + + return html + +class ConditionalDescriptor(SequenceDescriptor): + module_class = ConditionalModule + + filename_extension = "xml" + + stores_state = True + has_score = False + \ No newline at end of file From 8db96abb183f01b821d1ef1f88f2ed83cc51199c Mon Sep 17 00:00:00 2001 From: ichuang Date: Thu, 17 Jan 2013 03:50:06 +0000 Subject: [PATCH 2/8] conditional_module.html --- lms/templates/conditional_module.html | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 lms/templates/conditional_module.html diff --git a/lms/templates/conditional_module.html b/lms/templates/conditional_module.html new file mode 100644 index 0000000000..6405e33068 --- /dev/null +++ b/lms/templates/conditional_module.html @@ -0,0 +1,9 @@ +<% + from django.core.urlresolvers import reverse + reqm = module.required_modules[0] + course_id = module.system.course_id +%> + +

Sorry, you must complete ${reqm.display_name} +before this is visible.

From 19051e40d23798d610151c40469bbefddd44c933 Mon Sep 17 00:00:00 2001 From: ichuang Date: Fri, 18 Jan 2013 02:20:23 +0000 Subject: [PATCH 3/8] use ajax for doing conditional_module --- .../lib/xmodule/xmodule/conditional_module.py | 65 +++++++++++++++---- common/lib/xmodule/xmodule/x_module.py | 5 ++ lms/djangoapps/courseware/models.py | 5 +- 3 files changed, 63 insertions(+), 12 deletions(-) diff --git a/common/lib/xmodule/xmodule/conditional_module.py b/common/lib/xmodule/xmodule/conditional_module.py index c022c3742a..c7f4ee51eb 100644 --- a/common/lib/xmodule/xmodule/conditional_module.py +++ b/common/lib/xmodule/xmodule/conditional_module.py @@ -1,9 +1,12 @@ +import json import logging from xmodule.x_module import XModule from xmodule.modulestore import Location from xmodule.seq_module import SequenceDescriptor +from pkg_resources import resource_string + log = logging.getLogger('mitx.' + __name__) class ConditionalModule(XModule): @@ -18,26 +21,31 @@ class ConditionalModule(XModule): ''' + js = {'coffee': [resource_string(__name__, 'js/src/capa/display.coffee'), + resource_string(__name__, 'js/src/collapsible.coffee'), + resource_string(__name__, 'js/src/javascript_loader.coffee'), + ]} + + js_module_name = "Problem" + css = {'scss': [resource_string(__name__, 'css/capa/display.scss')]} + + def __init__(self, system, location, definition, descriptor, instance_state=None, shared_state=None, **kwargs): """ In addition to the normal XModule init, provide: - self.required_module_list = list of (tag, url_name) tuples of modules required by this one. self.condition = string describing condition required """ XModule.__init__(self, system, location, definition, descriptor, instance_state, shared_state, **kwargs) self.contents = None - self.required_modules_list = [tuple(x.split('/',1)) for x in self.metadata.get('required','').split('&')] + #self.required_modules_list = [tuple(x.split('/',1)) for x in self.metadata.get('required','').split('&')] self.condition = self.metadata.get('condition','') - log.debug('conditional module required=%s' % self.required_modules_list) + #log.debug('conditional module required=%s' % self.required_modules_list) def _get_required_modules(self): self.required_modules = [] - for (tag, name) in self.required_modules_list: - loc = self.location.dict() - loc['category'] = tag - loc['name'] = name + for loc in self.descriptor.required_module_locations: module = self.system.get_module(loc) self.required_modules.append(module) log.debug('required_modules=%s' % (self.required_modules)) @@ -49,6 +57,8 @@ class ConditionalModule(XModule): # all required modules must be completed, as determined by # the modules .is_completed() method for module in self.required_modules: + log.debug('in is_condition_satisfied; student_answers=%s' % module.lcp.student_answers) + log.debug('in is_condition_satisfied; instance_state=%s' % module.instance_state) if not hasattr(module, 'is_completed'): raise Exception('Error in conditional module: required module %s has no .is_completed() method' % module) if not module.is_completed(): @@ -63,10 +73,25 @@ class ConditionalModule(XModule): return True def get_html(self): + self.is_condition_satisfied() + return self.system.render_template('problem_ajax.html', { + 'element_id': self.location.html_id(), + 'id': self.id, + 'ajax_url': self.system.ajax_url, + }) + + def handle_ajax(self, dispatch, post): + ''' + This is called by courseware.module_render, to handle an AJAX call. + ''' + log.debug('conditional_module handle_ajax: dispatch=%s' % dispatch) + if not self.is_condition_satisfied(): - context = {'module': self, - } - return self.system.render_template('conditional_module.html', context) + context = {'module': self} + html = self.system.render_template('conditional_module.html', context) + # html = render_to_string('conditional_module.html', context) + return json.dumps({'html': html}) + #return self.system.render_template('conditional_module.html', context) if self.contents is None: self.contents = [child.get_html() for child in self.get_display_items()] @@ -76,7 +101,8 @@ class ConditionalModule(XModule): log.debug('rendered conditional module %s' % str(self.location)) - return html + return json.dumps({'html': html}) + #return html class ConditionalDescriptor(SequenceDescriptor): module_class = ConditionalModule @@ -85,4 +111,21 @@ class ConditionalDescriptor(SequenceDescriptor): stores_state = True has_score = False + + def __init__(self, *args, **kwargs): + super(ConditionalDescriptor, self).__init__(*args, **kwargs) + + required_module_list = [tuple(x.split('/',1)) for x in self.metadata.get('required','').split('&')] + self.required_module_locations = [] + for (tag, name) in required_module_list: + loc = self.location.dict() + loc['category'] = tag + loc['name'] = name + self.required_module_locations.append(Location(loc)) + log.debug('ConditionalDescriptor required_module_locations=%s' % self.required_module_locations) + + def get_required_module_descriptors(self): + """Returns a list of XModuleDescritpor instances upon which this module depends, but are + not children of this module""" + return [self.system.load_item(loc) for loc in self.required_module_locations] \ No newline at end of file diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index 5387a9b083..0e4e8e0f00 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -585,6 +585,11 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates): self._inherited_metadata.add(attr) self.metadata[attr] = metadata[attr] + def get_required_module_descriptors(self): + """Returns a list of XModuleDescritpor instances upon which this module depends, but are + not children of this module""" + return [] + def get_children(self): """Returns a list of XModuleDescriptor instances for the children of this module""" diff --git a/lms/djangoapps/courseware/models.py b/lms/djangoapps/courseware/models.py index 21ef8b3d66..bd01318f63 100644 --- a/lms/djangoapps/courseware/models.py +++ b/lms/djangoapps/courseware/models.py @@ -113,6 +113,9 @@ class StudentModuleCache(object): descriptor_filter=lambda descriptor: True, select_for_update=False): """ + obtain and return cache for descriptor descendents (ie children) AND modules required by the descriptor, + but which are not children of the module + course_id: the course in the context of which we want StudentModules. user: the django user for whom to load modules. descriptor: An XModuleDescriptor @@ -132,7 +135,7 @@ class StudentModuleCache(object): if depth is None or depth > 0: new_depth = depth - 1 if depth is not None else depth - for child in descriptor.get_children(): + for child in descriptor.get_children() + descriptor.get_required_module_descriptors(): descriptors.extend(get_child_descriptors(child, new_depth, descriptor_filter)) return descriptors From 363ca7031e38c234bea97921c8176780df8ff26e Mon Sep 17 00:00:00 2001 From: ichuang Date: Fri, 18 Jan 2013 03:43:31 +0000 Subject: [PATCH 4/8] add coffeescript/js for conditional module, does XModule.loadModules --- .../lib/xmodule/xmodule/conditional_module.py | 6 ++--- .../xmodule/js/src/conditional/display.coffee | 26 +++++++++++++++++++ lms/templates/conditional_ajax.html | 1 + lms/templates/conditional_module.html | 4 +-- 4 files changed, 32 insertions(+), 5 deletions(-) create mode 100644 common/lib/xmodule/xmodule/js/src/conditional/display.coffee create mode 100644 lms/templates/conditional_ajax.html diff --git a/common/lib/xmodule/xmodule/conditional_module.py b/common/lib/xmodule/xmodule/conditional_module.py index c7f4ee51eb..70dc9ae7bb 100644 --- a/common/lib/xmodule/xmodule/conditional_module.py +++ b/common/lib/xmodule/xmodule/conditional_module.py @@ -21,12 +21,12 @@ class ConditionalModule(XModule): ''' - js = {'coffee': [resource_string(__name__, 'js/src/capa/display.coffee'), + js = {'coffee': [resource_string(__name__, 'js/src/conditional/display.coffee'), resource_string(__name__, 'js/src/collapsible.coffee'), resource_string(__name__, 'js/src/javascript_loader.coffee'), ]} - js_module_name = "Problem" + js_module_name = "Conditional" css = {'scss': [resource_string(__name__, 'css/capa/display.scss')]} @@ -74,7 +74,7 @@ class ConditionalModule(XModule): def get_html(self): self.is_condition_satisfied() - return self.system.render_template('problem_ajax.html', { + return self.system.render_template('conditional_ajax.html', { 'element_id': self.location.html_id(), 'id': self.id, 'ajax_url': self.system.ajax_url, diff --git a/common/lib/xmodule/xmodule/js/src/conditional/display.coffee b/common/lib/xmodule/xmodule/js/src/conditional/display.coffee new file mode 100644 index 0000000000..5e8dc41dd8 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/conditional/display.coffee @@ -0,0 +1,26 @@ +class @Conditional + + constructor: (element) -> + @el = $(element).find('.conditional-wrapper') + @id = @el.data('problem-id') + @element_id = @el.attr('id') + @url = @el.data('url') + @render() + + $: (selector) -> + $(selector, @el) + + updateProgress: (response) => + if response.progress_changed + @el.attr progress: response.progress_status + @el.trigger('progressChanged') + + render: (content) -> + if content + @el.html(content) + XModule.loadModules('display', @el) + else + $.postWithPrefix "#{@url}/conditional_get", (response) => + @el.html(response.html) + XModule.loadModules('display', @el) + diff --git a/lms/templates/conditional_ajax.html b/lms/templates/conditional_ajax.html new file mode 100644 index 0000000000..0a5887be04 --- /dev/null +++ b/lms/templates/conditional_ajax.html @@ -0,0 +1 @@ +
diff --git a/lms/templates/conditional_module.html b/lms/templates/conditional_module.html index 6405e33068..e9a42b95ce 100644 --- a/lms/templates/conditional_module.html +++ b/lms/templates/conditional_module.html @@ -4,6 +4,6 @@ course_id = module.system.course_id %> -

Sorry, you must complete ${reqm.display_name} -before this is visible.

+must be completed before this will become visible.

From 1d29e1f2f4d5589785c12a2ed59b1f525d8e0dcc Mon Sep 17 00:00:00 2001 From: ichuang Date: Fri, 18 Jan 2013 04:15:45 +0000 Subject: [PATCH 5/8] comment out some debug in conditional_module; leave undeleted for future tests --- common/lib/xmodule/xmodule/conditional_module.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/common/lib/xmodule/xmodule/conditional_module.py b/common/lib/xmodule/xmodule/conditional_module.py index 70dc9ae7bb..35b38d21fe 100644 --- a/common/lib/xmodule/xmodule/conditional_module.py +++ b/common/lib/xmodule/xmodule/conditional_module.py @@ -48,7 +48,7 @@ class ConditionalModule(XModule): for loc in self.descriptor.required_module_locations: module = self.system.get_module(loc) self.required_modules.append(module) - log.debug('required_modules=%s' % (self.required_modules)) + #log.debug('required_modules=%s' % (self.required_modules)) def is_condition_satisfied(self): self._get_required_modules() @@ -57,15 +57,15 @@ class ConditionalModule(XModule): # all required modules must be completed, as determined by # the modules .is_completed() method for module in self.required_modules: - log.debug('in is_condition_satisfied; student_answers=%s' % module.lcp.student_answers) - log.debug('in is_condition_satisfied; instance_state=%s' % module.instance_state) + #log.debug('in is_condition_satisfied; student_answers=%s' % module.lcp.student_answers) + #log.debug('in is_condition_satisfied; instance_state=%s' % module.instance_state) if not hasattr(module, 'is_completed'): raise Exception('Error in conditional module: required module %s has no .is_completed() method' % module) if not module.is_completed(): - log.debug('condition module: %s not completed' % module) + log.debug('conditional module: %s not completed' % module) return False else: - log.debug('condition module: %s IS completed' % module) + log.debug('conditional module: %s IS completed' % module) return True else: raise Exception('Error in conditional module: unknown condition "%s"' % self.condition) @@ -84,7 +84,7 @@ class ConditionalModule(XModule): ''' This is called by courseware.module_render, to handle an AJAX call. ''' - log.debug('conditional_module handle_ajax: dispatch=%s' % dispatch) + #log.debug('conditional_module handle_ajax: dispatch=%s' % dispatch) if not self.is_condition_satisfied(): context = {'module': self} @@ -99,7 +99,7 @@ class ConditionalModule(XModule): # for now, just deal with one child html = self.contents[0] - log.debug('rendered conditional module %s' % str(self.location)) + #log.debug('rendered conditional module %s' % str(self.location)) return json.dumps({'html': html}) #return html From 95ef12169af7c24a016fd24c163b8868a5c34c8f Mon Sep 17 00:00:00 2001 From: ichuang Date: Sun, 3 Feb 2013 18:52:44 -0500 Subject: [PATCH 6/8] extend conditional module to add require_attempted --- common/lib/xmodule/xmodule/capa_module.py | 4 ++++ .../lib/xmodule/xmodule/conditional_module.py | 18 +++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 01e67d9f8b..0cec95287d 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -403,6 +403,10 @@ class CapaModule(XModule): # return self.answer_available() return self.lcp.done + def is_attempted(self): + # used by conditional module + return self.attempts > 0 + def answer_available(self): ''' Is the user allowed to see an answer? ''' diff --git a/common/lib/xmodule/xmodule/conditional_module.py b/common/lib/xmodule/xmodule/conditional_module.py index 35b38d21fe..22384133a9 100644 --- a/common/lib/xmodule/xmodule/conditional_module.py +++ b/common/lib/xmodule/xmodule/conditional_module.py @@ -19,6 +19,10 @@ class ConditionalModule(XModule):