Commit e2659a97 by Bill Filler

changes to dynamically update the sequence page via javascript, changes to gating api

parent d231ffaf
......@@ -58,6 +58,9 @@ $seq-nav-height: 50px;
// ====================
.recal-grade {
font-weight: bold;
}
.sequence-nav {
@extend .topbar;
......
......@@ -38,6 +38,12 @@
this.displayTabTooltip = function(event) {
return Sequence.prototype.displayTabTooltip.apply(self, [event]);
};
this.loadSeqContents = function(event) {
return Sequence.prototype.loadSeqContents.apply(self, [event]);
}
this.recalcGrade = function(event) {
return Sequence.prototype.recalcGrade.apply(self, [event]);
}
this.arrowKeys = {
LEFT: 37,
UP: 38,
......@@ -57,6 +63,12 @@
this.ajaxUrl = this.el.data('ajax-url');
this.nextUrl = this.el.data('next-url');
this.prevUrl = this.el.data('prev-url');
this.gateContent = this.el.data('gate-content');
this.prereqUrl = this.el.data('prereq-url');
this.prereqSectionName = this.el.data('prereq-section-name');
this.unitName = this.el.data('unit-name');
this.scoreReached = this.el.data('score-reached');
this.calculateScore = this.el.data('calculate-score');
this.keydownHandler($(element).find('#sequence-list .tab'));
this.base_page_title = ($('title').data('base-title') || '').trim();
this.bind();
......@@ -74,6 +86,52 @@
this.el.on('bookmark:remove', this.removeBookmarkIconFromActiveNavItem);
this.$('#sequence-list .nav-item').on('focus mouseenter', this.displayTabTooltip);
this.$('#sequence-list .nav-item').on('blur mouseleave', this.hideTabTooltip);
this.$('.recalc-grade').click(this.recalcGrade);
};
Sequence.prototype.loadSeqContents = function(event) {
var modxFullUrl, currentText, self;
modxFullUrl = '' + this.ajaxUrl + '/load_seq_contents';
self = this;
$.postWithPrefix(modxFullUrl, {},
function(response) {
console.log('load_seq_contents response = ');
console.log(response);
self.position = -1;
$('.sequence-list-wrapper').contents(response.seq_list_html);
$('#main-content').html(response.seq_contents_html);
self.contents = self.$('.seq_contents');
self.content_container = self.$('#seq_content');
self.sr_container = self.$('.sr-is-focusable');
self.render(1);
}
);
};
Sequence.prototype.recalcGrade = function(event) {
var modxFullUrl, currentText, self;
modxFullUrl = '' + this.ajaxUrl + '/recalc_grade';
self = this;
$.postWithPrefix(modxFullUrl, {
"gate_conent": this.gateContent,
"prereq_url": this.prereqUrl,
"prereq_section_name": this.prereqSectionName,
"unit_name": this.unitName,
"score_reached": this.scoreReached,
"calculate_score": this.calculateScore
}, function(response) {
console.log('Got response = ');
console.log(response);
self.gateContent = response.gate_content;
self.scoreReached = response.score_reached;
self.calculateScore = response.calculate_score;
$('#main-content').html(response.html);
if (self.scoreReached) {
setTimeout(self.loadSeqContents(event), 3000);
}
}
);
};
Sequence.prototype.previousNav = function(focused, index) {
......@@ -240,7 +298,9 @@
// Added for aborting video bufferization, see ../video/10_main.js
this.el.trigger('sequence:change');
this.mark_active(newPosition);
console.log("this.contents=" + this.contents);
currentTab = this.contents.eq(newPosition - 1);
console.log("current tab text=" + currentTab.text());
bookmarked = this.el.find('.active .bookmark-icon').hasClass('bookmarked');
// update the data-attributes with latest contents only for updated problems.
......@@ -263,6 +323,7 @@
.data('attempts-used', latestResponse.attempts_used);
});
}
console.log("calling XBlock.initialize");
XBlock.initializeBlocks(this.content_container, this.requestToken);
// For embedded circuit simulator exercises in 6.002x
......
......@@ -173,6 +173,7 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
'scss': [resource_string(__name__, 'css/sequence/display.scss')],
}
js_module_name = "Sequence"
gating_milestone = None
def __init__(self, *args, **kwargs):
super(SequenceModule, self).__init__(*args, **kwargs)
......@@ -204,6 +205,39 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
else:
self.position = 1
return json.dumps({'success': True})
elif dispatch == 'recalc_grade':
# force a grade recalculation
unlock = self._is_prereq_met(True)
unlock = True
params = {
'gate_content': not unlock,
'prereq_url': data['prereq_url'],
'prereq_section_name': data['prereq_section_name'],
'unit_name': data['unit_name'],
'score_reached': unlock,
'calculate_score': False,
}
html = self.system.render_template("_gated_content.html", params)
return json.dumps({'score_reached': unlock, 'gate_content': not unlock, 'calculate_score': False, 'html': html})
elif dispatch == 'load_seq_contents':
# load the sequence items
display_items = display_items = self.get_display_items();
# TODO - figure out how to get the real context here or at least add more of the values
context = {
'user_authenticated': True,
'requested_child': 'first'
}
items = self._render_student_view_for_items(context, display_items, Fragment())
params = {
'items': items,
'gate_content': False,
'disable_navigation': not self.is_user_authenticated(context)
}
seq_list_html = self.system.render_template("_sequence_list.html", params)
seq_contents_html = self.system.render_template("_sequence_contents.html", params)
return json.dumps({'seq_list_html': seq_list_html, 'seq_contents_html': seq_contents_html})
raise NotFoundError('Unexpected dispatch type')
......@@ -292,17 +326,17 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
self._update_position(context, len(display_items))
gate_content = False
milestone = self._get_gating_milestone()
if milestone:
gating_milestone = self._find_gating_milestone()
if gating_milestone:
if self.runtime.user_is_staff:
banner_text = _('This subsection is unlocked for learners when they meet the prerequisite requirements.')
elif not self._is_prereq_met(milestone):
elif not self._is_prereq_met(False):
gate_content = True
milestone_meta_info = self._get_milestone_meta_info(milestone)
milestone_meta_info = self._get_milestone_meta_info(gating_milestone)
fragment = Fragment()
params = {
'items': self._render_student_view_for_items(context, display_items, fragment) if not gate_content else [],
'items': self._render_student_view_for_items(context, display_items, fragment) if not gate_content else {},
'element_id': self.location.html_id(),
'item_id': self.location.to_deprecated_string(),
'position': self.position,
......@@ -313,18 +347,20 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
'banner_text': banner_text,
'disable_navigation': not self.is_user_authenticated(context),
'gate_content': gate_content,
'required_grade': milestone['requirements']['min_score'] if gate_content else None,
'prereq_url': milestone_meta_info['url'] if gate_content else None,
'prereq_section_name': milestone_meta_info['display_name'] if gate_content else None
'prereq_section_name': milestone_meta_info['display_name'] if gate_content else None,
'unit_name': "My Unit",
'score_reached': False,
'calculate_score': True
}
fragment.add_content(self.system.render_template("seq_module.html", params))
fragment.add_content(self.system.render_template("seq_module.html", params))
self._capture_full_seq_item_metrics(display_items)
self._capture_current_unit_metrics(display_items)
return fragment
def _get_gating_milestone(self):
def _find_gating_milestone(self):
"""
Checks whether a gating milestone exists for this Section
"""
......@@ -337,14 +373,14 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
return False
def _is_prereq_met(self, milestone):
def _is_prereq_met(self, force_on_unmet):
"""
Evaluate if the user has completed the prerequiste
"""
gating_service = self.runtime.service(self, 'gating')
if gating_service:
# if it's complete then a user milestone record will exist
return gating_service.is_prereq_met(self.runtime.user_id, milestone)
return gating_service.is_prereq_met(self.course_id, self.location, self.runtime.user_id, force_on_unmet)
return False
......
<%page args="gate_content, prereq_url, prereq_section_name, unit_name, score_reached, calculate_score" expression_filter="h"/>
<%!
from django.utils.translation import ugettext as _
from openedx.core.djangolib.markup import HTML, Text
%>
<div class="hidden-content proctored-exam completed">
<h3>
${unit_name}
</h3>
<hr>
<h3>
% if calculate_score:
<span class="recalc-grade">
${Text(_(
"Calculating your score for {section_name}..."
)).format(
section_name=prereq_section_name
)}
</span>
% elif score_reached:
${_("Score achieved!")}
% else:
${_("Content Locked")}
% endif
</h3>
<p>
% if calculate_score:
${_("Processing...")}
% elif score_reached:
${_("Congratulations you have unlocked this section!")}
% else:
${Text(_(
"You must complete the section '{section_name}' with the required score before you are able to view this content."
)).format(section_name=prereq_section_name)}
<p>
<button><a href="${prereq_url}">${_("Go to Prerequiste Section")}</a></button>
</p>
<p>
<button><span class="recalc-grade">Recalculate Grade</span></button>
</p>
% endif
</p>
</div>
<%page args="items" expression_filter="h"/>
<div class="sr-is-focusable" tabindex="-1"></div>
% for idx, item in enumerate(items):
<div id="seq_contents_${idx}"
aria-labelledby="tab_${idx}"
aria-hidden="true"
class="seq_contents tex2jax_ignore asciimath2jax_ignore">
${item['content']}
</div>
% endfor
<div id="seq_content" role="tabpanel"></div>
\ No newline at end of file
<%page args="items, gate_content, disable_navigation" expression_filter="h"/>
<%!
from django.utils.translation import ugettext as _
from openedx.core.djangolib.markup import HTML, Text
%>
<nav class="sequence-list-wrapper" aria-label="${_('Sequence')}">
<ol id="sequence-list" role="tablist">
% if gate_content:
<li>Locked</li>
% else:
% for idx, item in enumerate(items):
<li>
<button class="seq_${item['type']} inactive nav-item tab"
role="tab"
tabindex="-1"
aria-selected="false"
aria-expanded="false"
aria-controls="seq_content"
data-index="${idx}"
data-id="${item['id']}"
data-element="${idx+1}"
data-page-title="${item['page_title']}"
data-path="${item['path']}"
id="tab_${idx}"
${"disabled=disabled" if disable_navigation else ""}>
<span class="icon fa seq_${item['type']}" aria-hidden="true"></span>
<span class="fa fa-fw fa-bookmark bookmark-icon ${"is-hidden" if not item['bookmarked'] else "bookmarked"}" aria-hidden="true"></span>
<div class="sequence-tooltip sr"><span class="sr">${item['type']}&nbsp;</span>${item['page_title']}<span class="sr bookmark-icon-sr">&nbsp;${_("Bookmarked") if item['bookmarked'] else ""}</span></div>
</button>
</li>
% endfor
% endif
</ol>
</nav>
......@@ -4,24 +4,13 @@ from django.utils.translation import ugettext as _
from openedx.core.djangolib.markup import HTML, Text
%>
<div id="sequence_${element_id}" class="sequence" data-id="${item_id}" data-position="${position}" data-ajax-url="${ajax_url}" data-next-url="${next_url}" data-prev-url="${prev_url}">
% if banner_text or gate_content:
<div id="sequence_${element_id}" class="sequence" data-id="${item_id}" data-position="${position}" data-ajax-url="${ajax_url}" data-next-url="${next_url}" data-prev-url="${prev_url}" data-gate-content="${gate_content}" data-prereq-url="${prereq_url}" data-prereq-section-name="${prereq_section_name}" data-unit-name="${unit_name}" data-score-reached="${score_reached}" data-calculate-score="${calculate_score}">
% if banner_text:
<div class="pattern-library-shim alert alert-information subsection-header" tabindex="-1">
<span class="pattern-library-shim icon alert-icon fa fa-bullhorn" aria-hidden="true"></span>
<div class="pattern-library-shim alert-message">
<p class="pattern-library-shim alert-copy">
% if gate_content:
${Text(_(
"This content is blocked until you reach a grade of {req_grade} in {link_start}{section_name}{link_end}."
)).format(
req_grade=HTML("<b>{}%</b>").format(required_grade),
link_start=HTML("<a href='{}'>").format(prereq_url),
section_name=prereq_section_name,
link_end=HTML("</a>"),
)}
% else:
${banner_text}
% endif
</p>
</div>
</div>
......@@ -35,44 +24,18 @@ from openedx.core.djangolib.markup import HTML, Text
<span class="sequence-nav-button-label">${_('Next')}</span>
<span class="icon fa fa-chevron-next" aria-hidden="true"></span>
</button>
<nav class="sequence-list-wrapper" aria-label="${_('Sequence')}">
<ol id="sequence-list" role="tablist">
% for idx, item in enumerate(items):
<li role="presentation">
<button class="seq_${item['type']} inactive nav-item tab"
role="tab"
tabindex="-1"
aria-selected="false"
aria-expanded="false"
aria-controls="seq_content"
data-index="${idx}"
data-id="${item['id']}"
data-element="${idx+1}"
data-page-title="${item['page_title']}"
data-path="${item['path']}"
id="tab_${idx}"
${"disabled=disabled" if disable_navigation else ""}>
<span class="icon fa seq_${item['type']}" aria-hidden="true"></span>
<span class="fa fa-fw fa-bookmark bookmark-icon ${"is-hidden" if not item['bookmarked'] else "bookmarked"}" aria-hidden="true"></span>
<div class="sequence-tooltip sr"><span class="sr">${item['type']}&nbsp;</span>${item['page_title']}<span class="sr bookmark-icon-sr">&nbsp;${_("Bookmarked") if item['bookmarked'] else ""}</span></div>
</button>
</li>
% endfor
</ol>
</nav>
</div>
<div class="sr-is-focusable" tabindex="-1"></div>
<%include file="_sequence_list.html" args="items=items, gate_content=gate_content, disable_navigation=disable_navigation"/>
% for idx, item in enumerate(items):
<div id="seq_contents_${idx}"
aria-labelledby="tab_${idx}"
aria-hidden="true"
class="seq_contents tex2jax_ignore asciimath2jax_ignore">
${item['content']}
</div>
% endfor
<div id="seq_content" role="tabpanel"></div>
<div id="main-content">
% if gate_content:
<%include file="_gated_content.html" args="gate_content=gate_content, prereq_url=prereq_url, prereq_section_name=prereq_section_name, unit_name=unit_name, score_reached=score_reached, calculate_score=calculate_score"/>
% else:
<%include file="_sequence_contents.html" args="items=items"/>
% endif
</div>
<nav class="sequence-bottom" aria-label="${_('Section')}">
<button class="sequence-nav-button button-previous">
......
......@@ -3,12 +3,15 @@ API for the gating djangoapp
"""
import logging
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
from lms.djangoapps.courseware.access import _has_access_to_course
from lms.djangoapps.course_blocks.api import get_course_blocks
from lms.djangoapps.grades.subsection_grade_factory import SubsectionGradeFactory
from milestones import api as milestones_api
from opaque_keys.edx.keys import UsageKey
from lms.djangoapps.courseware.access import _has_access_to_course
from opaque_keys.edx.locator import BlockUsageLocator
from openedx.core.lib.gating.exceptions import GatingValidationError
from xmodule.modulestore.django import modulestore
......@@ -301,15 +304,42 @@ def get_gated_content(course, user):
)
]
def is_prereq_met(user_id, milestone):
def _get_block_id(milestone):
"""
Returns true if the prequiste has been met for a given milestone
Get the block id for the given milestone
"""
prereq_content_key = milestone['namespace'].replace(GATING_NAMESPACE_QUALIFIER, '')
block_id = UsageKey.from_string(prereq_content_key).block_id
return block_id
Arguments:
user_id: The id of the user
milestone (Milestone) : The milestone to check
def _get_minimum_required_percentage(milestone):
"""
Returns the minimum percentage requirement for the given milestone.
"""
# Default minimum score to 100
min_score = 100
requirements = milestone.get('requirements')
if requirements:
try:
min_score = int(requirements.get('min_score'))
except (ValueError, TypeError):
log.warning(
u'Gating: Failed to find minimum score for gating milestone %s, defaulting to 100',
)
return min_score
def _get_subsection_percentage(subsection_grade):
"""
return milestones_api.user_has_milestone({'id': user_id}, milestone)
Returns the percentage value of the given subsection_grade.
"""
return _calculate_ratio(subsection_grade.graded_total.earned, subsection_grade.graded_total.possible) * 100.0
def _calculate_ratio(earned, possible):
"""
Returns the percentage of the given earned and possible values.
"""
return float(earned) / float(possible) if possible else 0.0
def get_gating_milestone_meta_info(course_id, milestone):
"""
......@@ -329,3 +359,46 @@ def get_gating_milestone_meta_info(course_id, milestone):
if blocks:
gating_milestone_display_name = blocks[0].display_name
return {'url': gating_milestone_url, 'display_name': gating_milestone_display_name}
def is_prereq_met(course_id, content_id, user_id, recalc_on_unmet=False):
"""
Returns true if the prequiste has been met for a given milestone
Arguments:
course_id (CourseLocator): CourseLocator object for the course
content_id (BlockUsageLocator): BlockUsageLocator for the content
user_id: The id of the user
recalc_on_unmet: Recalculate the grade if prereq has not yet been met
"""
# first check source of truth.. the database
#prereq_met = milestones_api.user_has_milestone({'id': user_id}, milestone)
unfulfilled_milestones = milestones_api.get_course_content_milestones(course_id, content_id, 'requires', {'id': user_id})
prereq_met = not unfulfilled_milestones
if prereq_met or not recalc_on_unmet:
return prereq_met
student = User.objects.get(id=user_id)
store = modulestore()
with store.bulk_operations(course_id):
course_structure = get_course_blocks(student, store.make_course_usage_key(course_id))
course = store.get_course(course_id, depth=0)
subsection_grade_factory = SubsectionGradeFactory(student, course, course_structure)
subsection_usage_key = BlockUsageLocator(course_id, 'sequential', _get_block_id(unfulfilled_milestones[0]))
if subsection_usage_key in course_structure:
subsection_grade = subsection_grade_factory.update(
course_structure[subsection_usage_key]
)
min_percentage = _get_minimum_required_percentage(unfulfilled_milestones[0])
subsection_percentage = _get_subsection_percentage(subsection_grade)
if subsection_percentage >= min_percentage:
prereq_met = True
#milestones_helpers.add_user_milestone({'id': user.id}, prereq_milestone)
else:
prereq_met = False
#milestones_helpers.remove_user_milestone({'id': user.id}, prereq_milestone)
return prereq_met
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