Merge pull request #17338 from edx/sstudent/EDUCATOR-2189
progress indicators on courseware ribbon
This commit is contained in:
@@ -120,7 +120,7 @@ $seq-nav-height: 50px;
|
||||
overflow: visible; // for tooltip - IE11 uses 'hidden' by default if width/height is specified
|
||||
|
||||
.icon {
|
||||
display: block;
|
||||
display: inline-block;
|
||||
line-height: 100%; // This matches the height of the <a> its within (the parent) to get vertical centering.
|
||||
font-size: 110%;
|
||||
color: $seq-nav-icon-color-muted;
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<button role="tab" tabindex="0" aria-selected="true" aria-expanded="true" aria-controls="seq_content" class="seq_problem nav-item tab active" data-index="0" data-id="block-v1:edX+DemoX+Demo_Course+type@vertical+block@fb79dcbad35b466a8c6364f8ffee9050" data-element="1" data-page-title="Unit 101" data-path="Example Week 2: Get Interactive > Homework - Part 1 > Unit 101" id="tab_0">
|
||||
<span class="icon fa seq_problem" aria-hidden="true"></span>
|
||||
<span class="fa fa-fw fa-bookmark bookmark-icon is-hidden" aria-hidden="true"></span>
|
||||
<span class="fa fa-check-circle check-circle is-hidden" style="color:green" aria-hidden="true">
|
||||
<div class="sequence-tooltip sr"><span class="sr">problem</span>Unit 101<span class="sr bookmark-icon-sr"></span></div>
|
||||
</button>
|
||||
</li>
|
||||
@@ -22,6 +23,7 @@
|
||||
<button role="tab" tabindex="-1" aria-selected="true" aria-expanded="true" aria-controls="seq_content" class="seq_problem inactive nav-item tab" data-index="1" data-id="block-v1:edX+DemoX+Demo_Course+type@vertical+block@fb79dcbad35b466a8c6364f8ffee9051" data-element="2" data-page-title="Unit 102" data-path="Example Week 2: Get Interactive > Homework - Part 1 > Unit 102" id="tab_1">
|
||||
<span class="icon fa seq_problem" aria-hidden="true"></span>
|
||||
<span class="fa fa-fw fa-bookmark bookmark-icon is-hidden" aria-hidden="true"></span>
|
||||
<span class="fa fa-check-circle check-circle is-hidden" style="color:green" aria-hidden="true">
|
||||
<div class="sequence-tooltip sr"><span class="sr">problem</span>Unit 102<span class="sr bookmark-icon-sr"></span></div>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
beforeEach(function() {
|
||||
loadFixtures('sequence.html');
|
||||
local.XBlock = window.XBlock = jasmine.createSpyObj('XBlock', ['initializeBlocks']);
|
||||
this.sequence = new Sequence($('.xblock-student_view-sequential'));
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
@@ -29,7 +30,6 @@
|
||||
|
||||
describe('Navbar', function() {
|
||||
it('works with keyboard navigation LEFT and ENTER', function() {
|
||||
this.sequence = new Sequence($('.xblock-student_view-sequential'));
|
||||
this.sequence.$('.nav-item[data-index=0]').focus();
|
||||
keydownHandler(keys.LEFT);
|
||||
keydownHandler(keys.ENTER);
|
||||
@@ -47,7 +47,6 @@
|
||||
});
|
||||
|
||||
it('works with keyboard navigation RIGHT and ENTER', function() {
|
||||
this.sequence = new Sequence($('.xblock-student_view-sequential'));
|
||||
this.sequence.$('.nav-item[data-index=0]').focus();
|
||||
keydownHandler(keys.RIGHT);
|
||||
keydownHandler(keys.ENTER);
|
||||
@@ -63,6 +62,64 @@
|
||||
tabindex: '0'
|
||||
});
|
||||
});
|
||||
|
||||
it('Completion Indicator missing', function() {
|
||||
this.sequence.$('.nav-item[data-index=0]').children('.check-circle').remove();
|
||||
spyOn($, 'postWithPrefix').and.callFake(function(url, data, callback) {
|
||||
callback({
|
||||
complete: true
|
||||
});
|
||||
});
|
||||
this.sequence.update_completion(1);
|
||||
expect($.postWithPrefix).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('Completion', function() {
|
||||
beforeEach(function() {
|
||||
expect(
|
||||
this.sequence.$('.nav-item[data-index=0]').children('.check-circle').first()
|
||||
.hasClass('is-hidden')
|
||||
).toBe(true);
|
||||
expect(
|
||||
this.sequence.$('.nav-item[data-index=1]').children('.check-circle').first()
|
||||
.hasClass('is-hidden')
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
expect($.postWithPrefix).toHaveBeenCalled();
|
||||
expect(
|
||||
this.sequence.$('.nav-item[data-index=1]').children('.check-circle').first()
|
||||
.hasClass('is-hidden')
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('API check returned true', function() {
|
||||
spyOn($, 'postWithPrefix').and.callFake(function(url, data, callback) {
|
||||
callback({
|
||||
complete: true
|
||||
});
|
||||
});
|
||||
this.sequence.update_completion(1);
|
||||
expect(
|
||||
this.sequence.$('.nav-item[data-index=0]').children('.check-circle').first()
|
||||
.hasClass('is-hidden')
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('API check returned false', function() {
|
||||
spyOn($, 'postWithPrefix').and.callFake(function(url, data, callback) {
|
||||
callback({
|
||||
complete: false
|
||||
});
|
||||
});
|
||||
this.sequence.update_completion(1);
|
||||
expect(
|
||||
this.sequence.$('.nav-item[data-index=0]').children('.check-circle').first()
|
||||
.hasClass('is-hidden')
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}).call(this);
|
||||
|
||||
@@ -230,6 +230,7 @@
|
||||
if (this.position !== newPosition) {
|
||||
if (this.position) {
|
||||
this.mark_visited(this.position);
|
||||
this.update_completion(this.position);
|
||||
modxFullUrl = '' + this.ajaxUrl + '/goto_position';
|
||||
$.postWithPrefix(modxFullUrl, {
|
||||
position: newPosition
|
||||
@@ -400,6 +401,22 @@
|
||||
.addClass('visited');
|
||||
};
|
||||
|
||||
Sequence.prototype.update_completion = function(position) {
|
||||
var element = this.link_for(position);
|
||||
var completionUrl = this.ajaxUrl + '/get_completion';
|
||||
var usageKey = element[0].attributes['data-id'].value;
|
||||
var completionIndicators = element.find('.check-circle');
|
||||
if (completionIndicators.length) {
|
||||
$.postWithPrefix(completionUrl, {
|
||||
usage_key: usageKey
|
||||
}, function(data) {
|
||||
if (data.complete === true) {
|
||||
completionIndicators.removeClass('is-hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Sequence.prototype.mark_active = function(position) {
|
||||
// Don't overwrite class attribute to avoid changing Progress class
|
||||
var element = this.link_for(position);
|
||||
|
||||
@@ -9,6 +9,7 @@ import logging
|
||||
from datetime import datetime
|
||||
|
||||
from lxml import etree
|
||||
from opaque_keys.edx.keys import UsageKey
|
||||
from pkg_resources import resource_string
|
||||
from pytz import UTC
|
||||
from six import text_type
|
||||
@@ -161,6 +162,7 @@ class ProctoringFields(object):
|
||||
@XBlock.wants('verification')
|
||||
@XBlock.wants('gating')
|
||||
@XBlock.wants('credit')
|
||||
@XBlock.wants('completion')
|
||||
@XBlock.needs('user')
|
||||
@XBlock.needs('bookmarks')
|
||||
class SequenceModule(SequenceFields, ProctoringFields, XModule):
|
||||
@@ -206,6 +208,20 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
|
||||
self.position = 1
|
||||
return json.dumps({'success': True})
|
||||
|
||||
if dispatch == 'get_completion':
|
||||
completion_service = self.runtime.service(self, 'completion')
|
||||
if not completion_service.visual_progress_enabled():
|
||||
return None
|
||||
|
||||
usage_key = data.get('usage_key', None)
|
||||
item = self.get_child(UsageKey.from_string(usage_key))
|
||||
if not item:
|
||||
return None
|
||||
|
||||
complete = completion_service.vertical_is_complete(item)
|
||||
return json.dumps({
|
||||
'complete': complete
|
||||
})
|
||||
raise NotFoundError('Unexpected dispatch type')
|
||||
|
||||
@classmethod
|
||||
@@ -414,6 +430,7 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
|
||||
"""
|
||||
is_user_authenticated = self.is_user_authenticated(context)
|
||||
bookmarks_service = self.runtime.service(self, 'bookmarks')
|
||||
completion_service = self.runtime.service(self, 'completion')
|
||||
context['username'] = self.runtime.service(self, 'user').get_current_user().opt_attrs.get(
|
||||
'edx-platform.username')
|
||||
display_names = [
|
||||
@@ -455,6 +472,9 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
|
||||
'path': " > ".join(display_names + [item.display_name_with_default]),
|
||||
}
|
||||
|
||||
if is_user_authenticated and completion_service.visual_progress_enabled():
|
||||
iteminfo['complete'] = completion_service.vertical_is_complete(item)
|
||||
|
||||
contents.append(iteminfo)
|
||||
|
||||
return contents
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
Tests for sequence module.
|
||||
"""
|
||||
# pylint: disable=no-member
|
||||
import json
|
||||
|
||||
from datetime import timedelta
|
||||
from django.utils.timezone import now
|
||||
from freezegun import freeze_time
|
||||
@@ -72,6 +74,9 @@ class SequenceBlockTestCase(XModuleXmlImportTest):
|
||||
self._set_up_module_system(block)
|
||||
|
||||
block.xmodule_runtime._services['bookmarks'] = Mock() # pylint: disable=protected-access
|
||||
block.xmodule_runtime._services['completion'] = Mock( # pylint: disable=protected-access
|
||||
return_value=Mock(vertical_is_complete=Mock(return_value=True))
|
||||
)
|
||||
block.xmodule_runtime._services['user'] = StubUserService() # pylint: disable=protected-access
|
||||
block.xmodule_runtime.xmodule_instance = getattr(block, '_xmodule', None) # pylint: disable=protected-access
|
||||
block.parent = parent.location
|
||||
@@ -121,6 +126,7 @@ class SequenceBlockTestCase(XModuleXmlImportTest):
|
||||
self.assertIn("'gated': False", html)
|
||||
self.assertIn("'next_url': 'NextSequential'", html)
|
||||
self.assertIn("'prev_url': 'PrevSequential'", html)
|
||||
self.assertNotIn("fa fa-check-circle check-circle is-hidden", html)
|
||||
|
||||
def test_student_view_first_child(self):
|
||||
html = self._get_rendered_student_view(self.sequence_3_1, requested_child='first')
|
||||
@@ -273,3 +279,34 @@ class SequenceBlockTestCase(XModuleXmlImportTest):
|
||||
|
||||
# assert content shown as normal
|
||||
self._assert_ungated(html, self.sequence_1_2)
|
||||
|
||||
def test_handle_ajax_get_completion_disabled(self):
|
||||
"""
|
||||
Test when completion service is turned off by waffle, the ajax call returns correct
|
||||
None value
|
||||
"""
|
||||
completion_waffle_mock = Mock()
|
||||
completion_waffle_mock.return_value.visual_progress_enabled.return_value = False
|
||||
self.sequence_3_1.xmodule_runtime._services['completion'] = completion_waffle_mock # pylint: disable=protected-access
|
||||
for child in self.sequence_3_1.get_children():
|
||||
usage_key = unicode(child.location)
|
||||
completion_return = self.sequence_3_1.handle_ajax(
|
||||
'get_completion',
|
||||
{'usage_key': usage_key}
|
||||
)
|
||||
self.assertIs(completion_return, None)
|
||||
|
||||
def test_handle_ajax_get_completion_success(self):
|
||||
"""
|
||||
Test that the completion data is returned successfully on
|
||||
targeted vertical through ajax call
|
||||
"""
|
||||
for child in self.sequence_3_1.get_children():
|
||||
usage_key = unicode(child.location)
|
||||
completion_return = json.loads(self.sequence_3_1.handle_ajax(
|
||||
'get_completion',
|
||||
{'usage_key': usage_key}
|
||||
))
|
||||
self.assertIsNot(completion_return, None)
|
||||
self.assertTrue('complete' in completion_return)
|
||||
self.assertEqual(completion_return['complete'], True)
|
||||
|
||||
@@ -6,7 +6,7 @@ from __future__ import absolute_import, division, print_function, unicode_litera
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models, transaction, connection
|
||||
from django.db import models, transaction
|
||||
from django.utils.translation import ugettext as _
|
||||
from model_utils.models import TimeStampedModel
|
||||
from opaque_keys.edx.django.models import CourseKeyField, UsageKeyField
|
||||
@@ -161,12 +161,26 @@ class BlockCompletion(TimeStampedModel, models.Model):
|
||||
id = BigAutoField(primary_key=True) # pylint: disable=invalid-name
|
||||
user = models.ForeignKey(User)
|
||||
course_key = CourseKeyField(max_length=255)
|
||||
|
||||
# note: this usage key may not have the run filled in for
|
||||
# old mongo courses. Use the full_block_key property
|
||||
# instead when you want to use/compare the usage_key.
|
||||
block_key = UsageKeyField(max_length=255)
|
||||
block_type = models.CharField(max_length=64)
|
||||
completion = models.FloatField(validators=[validate_percent])
|
||||
|
||||
objects = BlockCompletionManager()
|
||||
|
||||
@property
|
||||
def full_block_key(self):
|
||||
"""
|
||||
Returns the "correct" usage key value with the run filled in.
|
||||
"""
|
||||
if self.block_key.run is None:
|
||||
return self.block_key.replace(course_key=self.course_key) # pylint: disable=unexpected-keyword-arg, no-value-for-parameter
|
||||
else:
|
||||
return self.block_key
|
||||
|
||||
@classmethod
|
||||
def get_course_completions(cls, user, course_key):
|
||||
"""
|
||||
|
||||
@@ -13,7 +13,9 @@ class CompletionService(object):
|
||||
Exposes
|
||||
|
||||
* self.completion_tracking_enabled() -> bool
|
||||
* self.visual_progress_enabled() -> bool
|
||||
* self.get_completions(candidates)
|
||||
* self.vertical_is_complete(vertical_item)
|
||||
|
||||
Constructor takes a user object and course_key as arguments.
|
||||
"""
|
||||
@@ -31,6 +33,16 @@ class CompletionService(object):
|
||||
"""
|
||||
return waffle.waffle().is_enabled(waffle.ENABLE_COMPLETION_TRACKING)
|
||||
|
||||
def visual_progress_enabled(self):
|
||||
"""
|
||||
Exposes VISUAL_PROGRESS_ENABLED waffle switch to XModule runtime
|
||||
|
||||
Return value:
|
||||
|
||||
bool -> True if VISUAL_PROGRESS flag is enabled.
|
||||
"""
|
||||
return waffle.visual_progress_enabled(self._course_key)
|
||||
|
||||
def get_completions(self, candidates):
|
||||
"""
|
||||
Given an iterable collection of block_keys in the course, returns a
|
||||
@@ -54,8 +66,34 @@ class CompletionService(object):
|
||||
course_key=self._course_key,
|
||||
block_key__in=candidates,
|
||||
)
|
||||
completions = {block.block_key: block.completion for block in completion_queryset}
|
||||
completions = {
|
||||
block.full_block_key: block.completion for block in completion_queryset # pylint: disable=not-an-iterable
|
||||
}
|
||||
for candidate in candidates:
|
||||
if candidate not in completions:
|
||||
completions[candidate] = 0.0
|
||||
return completions
|
||||
|
||||
def vertical_is_complete(self, item):
|
||||
"""
|
||||
Calculates and returns whether a particular vertical is complete.
|
||||
The logic in this method is temporary, and will go away once the
|
||||
completion API is able to store a first-order notion of completeness
|
||||
for parent blocks (right now it just stores completion for leaves-
|
||||
problems, HTML, video, etc.).
|
||||
"""
|
||||
if item.location.block_type != 'vertical':
|
||||
raise ValueError('The passed in xblock is not a vertical type!')
|
||||
|
||||
if not self.completion_tracking_enabled():
|
||||
return None
|
||||
|
||||
# this is temporary local logic and will be removed when the whole course tree is included in completion
|
||||
child_locations = [
|
||||
child.location for child in item.get_children() if child.location.block_type != 'discussion'
|
||||
]
|
||||
completions = self.get_completions(child_locations)
|
||||
for child_location in child_locations:
|
||||
if completions[child_location] < 1.0:
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
Tests of completion xblock runtime services
|
||||
"""
|
||||
import ddt
|
||||
from django.test import TestCase
|
||||
from opaque_keys.edx.keys import CourseKey, UsageKey
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from student.tests.factories import UserFactory
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
|
||||
from ..models import BlockCompletion
|
||||
from ..services import CompletionService
|
||||
@@ -12,20 +13,58 @@ from ..test_utils import CompletionWaffleTestMixin
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class CompletionServiceTestCase(CompletionWaffleTestMixin, TestCase):
|
||||
class CompletionServiceTestCase(CompletionWaffleTestMixin, SharedModuleStoreTestCase):
|
||||
"""
|
||||
Test the data returned by the CompletionService.
|
||||
"""
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(CompletionServiceTestCase, cls).setUpClass()
|
||||
cls.course = CourseFactory.create()
|
||||
with cls.store.bulk_operations(cls.course.id):
|
||||
cls.chapter = ItemFactory.create(
|
||||
parent=cls.course,
|
||||
category="chapter",
|
||||
)
|
||||
cls.sequence = ItemFactory.create(
|
||||
parent=cls.chapter,
|
||||
category='sequential',
|
||||
)
|
||||
cls.vertical = ItemFactory.create(
|
||||
parent=cls.sequence,
|
||||
category='vertical',
|
||||
)
|
||||
cls.problem = ItemFactory.create(
|
||||
parent=cls.vertical,
|
||||
category="problem",
|
||||
)
|
||||
cls.problem2 = ItemFactory.create(
|
||||
parent=cls.vertical,
|
||||
category="problem",
|
||||
)
|
||||
cls.problem3 = ItemFactory.create(
|
||||
parent=cls.vertical,
|
||||
category="problem",
|
||||
)
|
||||
cls.problem4 = ItemFactory.create(
|
||||
parent=cls.vertical,
|
||||
category="problem",
|
||||
)
|
||||
cls.problem5 = ItemFactory.create(
|
||||
parent=cls.vertical,
|
||||
category="problem",
|
||||
)
|
||||
cls.store.update_item(cls.course, UserFactory().id)
|
||||
cls.problems = [cls.problem, cls.problem2, cls.problem3, cls.problem4, cls.problem5]
|
||||
|
||||
def setUp(self):
|
||||
super(CompletionServiceTestCase, self).setUp()
|
||||
self.override_waffle_switch(True)
|
||||
self.user = UserFactory.create()
|
||||
self.other_user = UserFactory.create()
|
||||
self.course_key = CourseKey.from_string("edX/MOOC101/2049_T2")
|
||||
self.course_key = self.course.id
|
||||
self.other_course_key = CourseKey.from_string("course-v1:ReedX+Hum110+1904")
|
||||
self.block_keys = [UsageKey.from_string("i4x://edX/MOOC101/video/{}".format(number)) for number in xrange(5)]
|
||||
|
||||
self.block_keys = [problem.location for problem in self.problems]
|
||||
self.completion_service = CompletionService(self.user, self.course_key)
|
||||
|
||||
# Proper completions for the given runtime
|
||||
@@ -72,3 +111,38 @@ class CompletionServiceTestCase(CompletionWaffleTestMixin, TestCase):
|
||||
def test_enabled_honors_waffle_switch(self, enabled):
|
||||
self.override_waffle_switch(enabled)
|
||||
self.assertEqual(self.completion_service.completion_tracking_enabled(), enabled)
|
||||
|
||||
def test_vertical_completion(self):
|
||||
self.assertEqual(
|
||||
self.completion_service.vertical_is_complete(self.vertical),
|
||||
False,
|
||||
)
|
||||
|
||||
for block_key in self.block_keys:
|
||||
BlockCompletion.objects.submit_completion(
|
||||
user=self.user,
|
||||
course_key=self.course_key,
|
||||
block_key=block_key,
|
||||
completion=1.0
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
self.completion_service.vertical_is_complete(self.vertical),
|
||||
True,
|
||||
)
|
||||
|
||||
def test_vertical_partial_completion(self):
|
||||
block_keys_count = len(self.block_keys)
|
||||
for i in range(block_keys_count - 1):
|
||||
# Mark all the child blocks completed except the last one
|
||||
BlockCompletion.objects.submit_completion(
|
||||
user=self.user,
|
||||
course_key=self.course_key,
|
||||
block_key=self.block_keys[i],
|
||||
completion=1.0
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
self.completion_service.vertical_is_complete(self.vertical),
|
||||
False,
|
||||
)
|
||||
|
||||
@@ -16,9 +16,6 @@ class BlockCompletionTransformer(BlockStructureTransformer):
|
||||
WRITE_VERSION = 1
|
||||
COMPLETION = 'completion'
|
||||
|
||||
def __init__(self):
|
||||
super(BlockCompletionTransformer, self).__init__()
|
||||
|
||||
@classmethod
|
||||
def name(cls):
|
||||
return "blocks_api:completion"
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
<div class="sequence-nav">
|
||||
<button class="sequence-nav-button button-previous">
|
||||
<span class="icon fa fa-chevron-prev" aria-hidden="true"></span>
|
||||
@@ -29,7 +30,7 @@
|
||||
<button class="active nav-item tab" title="${_('Content Locked')}" id="tab_0" role="tab" tabindex="-1" aria-selected="true" aria-expanded="true" aria-controls="content_locked" disabled>
|
||||
<span class="icon fa fa-lock" aria-hidden="true"></span>
|
||||
</button>
|
||||
</li>
|
||||
</li>
|
||||
% else:
|
||||
% for idx, item in enumerate(items):
|
||||
<li role="presentation">
|
||||
@@ -47,6 +48,13 @@
|
||||
id="tab_${idx}"
|
||||
${"disabled=disabled" if disable_navigation else ""}>
|
||||
<span class="icon fa seq_${item['type']}" aria-hidden="true"></span>
|
||||
% if 'complete' in item:
|
||||
<span
|
||||
class="fa fa-check-circle check-circle ${"is-hidden" if not item['complete'] else ""}"
|
||||
style="color:green"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
% endif
|
||||
<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']} </span>${item['page_title']}<span class="sr bookmark-icon-sr"> ${_("Bookmarked") if item['bookmarked'] else ""}</span></div>
|
||||
</button>
|
||||
@@ -56,7 +64,6 @@
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
% if gated_content['gated']:
|
||||
<%include file="_gated_content.html" args="prereq_url=gated_content['prereq_url'], prereq_section_name=gated_content['prereq_section_name'], gated_section_name=gated_content['gated_section_name']"/>
|
||||
% else:
|
||||
|
||||
Reference in New Issue
Block a user