Added subsection gating feature
This commit is contained in:
@@ -5,10 +5,12 @@ from pytz import UTC
|
||||
|
||||
from django.dispatch import receiver
|
||||
|
||||
from xmodule.modulestore.django import SignalHandler
|
||||
from xmodule.modulestore.django import modulestore, SignalHandler
|
||||
from contentstore.courseware_index import CoursewareSearchIndexer, LibrarySearchIndexer
|
||||
from contentstore.proctoring import register_special_exams
|
||||
from openedx.core.djangoapps.credit.signals import on_course_publish
|
||||
from openedx.core.lib.gating import api as gating_api
|
||||
from util.module_utils import yield_dynamic_descriptor_descendants
|
||||
|
||||
|
||||
@receiver(SignalHandler.course_published)
|
||||
@@ -48,3 +50,30 @@ def listen_for_library_update(sender, library_key, **kwargs): # pylint: disable
|
||||
from .tasks import update_library_index
|
||||
|
||||
update_library_index.delay(unicode(library_key), datetime.now(UTC).isoformat())
|
||||
|
||||
|
||||
@receiver(SignalHandler.item_deleted)
|
||||
def handle_item_deleted(**kwargs):
|
||||
"""
|
||||
Receives the item_deleted signal sent by Studio when an XBlock is removed from
|
||||
the course structure and removes any gating milestone data associated with it or
|
||||
its descendants.
|
||||
|
||||
Arguments:
|
||||
kwargs (dict): Contains the content usage key of the item deleted
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
usage_key = kwargs.get('usage_key')
|
||||
if usage_key:
|
||||
# Strip branch info
|
||||
usage_key = usage_key.for_branch(None)
|
||||
course_key = usage_key.course_key
|
||||
deleted_module = modulestore().get_item(usage_key)
|
||||
for module in yield_dynamic_descriptor_descendants(deleted_module, kwargs.get('user_id')):
|
||||
# Remove prerequisite milestone data
|
||||
gating_api.remove_prerequisite(module.location)
|
||||
# Remove any 'requires' course content milestone relationships
|
||||
gating_api.set_required_content(course_key, module.location, None, None)
|
||||
|
||||
@@ -26,7 +26,7 @@ from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.tabs import InvalidTabsException
|
||||
from util.milestones_helpers import seed_milestone_relationship_types
|
||||
from milestones.tests.utils import MilestonesTestCaseMixin
|
||||
|
||||
from .utils import CourseTestCase
|
||||
|
||||
@@ -69,7 +69,7 @@ class CourseSettingsEncoderTest(CourseTestCase):
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class CourseDetailsViewTest(CourseTestCase):
|
||||
class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin):
|
||||
"""
|
||||
Tests for modifying content on the first course settings page (course dates, overview, etc.).
|
||||
"""
|
||||
@@ -156,14 +156,12 @@ class CourseDetailsViewTest(CourseTestCase):
|
||||
|
||||
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True, 'MILESTONES_APP': True})
|
||||
def test_pre_requisite_course_list_present(self):
|
||||
seed_milestone_relationship_types()
|
||||
settings_details_url = get_url(self.course.id)
|
||||
response = self.client.get_html(settings_details_url)
|
||||
self.assertContains(response, "Prerequisite Course")
|
||||
|
||||
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True, 'MILESTONES_APP': True})
|
||||
def test_pre_requisite_course_update_and_fetch(self):
|
||||
seed_milestone_relationship_types()
|
||||
url = get_url(self.course.id)
|
||||
resp = self.client.get_json(url)
|
||||
course_detail_json = json.loads(resp.content)
|
||||
@@ -191,7 +189,6 @@ class CourseDetailsViewTest(CourseTestCase):
|
||||
|
||||
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True, 'MILESTONES_APP': True})
|
||||
def test_invalid_pre_requisite_course(self):
|
||||
seed_milestone_relationship_types()
|
||||
url = get_url(self.course.id)
|
||||
resp = self.client.get_json(url)
|
||||
course_detail_json = json.loads(resp.content)
|
||||
@@ -254,7 +251,6 @@ class CourseDetailsViewTest(CourseTestCase):
|
||||
|
||||
@unittest.skipUnless(settings.FEATURES.get('ENTRANCE_EXAMS', False), True)
|
||||
def test_entrance_exam_created_updated_and_deleted_successfully(self):
|
||||
seed_milestone_relationship_types()
|
||||
settings_details_url = get_url(self.course.id)
|
||||
data = {
|
||||
'entrance_exam_enabled': 'true',
|
||||
@@ -305,7 +301,6 @@ class CourseDetailsViewTest(CourseTestCase):
|
||||
test that creating an entrance exam should store the default value, if key missing in json request
|
||||
or entrance_exam_minimum_score_pct is an empty string
|
||||
"""
|
||||
seed_milestone_relationship_types()
|
||||
settings_details_url = get_url(self.course.id)
|
||||
test_data_1 = {
|
||||
'entrance_exam_enabled': 'true',
|
||||
|
||||
57
cms/djangoapps/contentstore/tests/test_gating.py
Normal file
57
cms/djangoapps/contentstore/tests/test_gating.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""
|
||||
Unit tests for the gating feature in Studio
|
||||
"""
|
||||
from contentstore.signals import handle_item_deleted
|
||||
from milestones.tests.utils import MilestonesTestCaseMixin
|
||||
from mock import patch
|
||||
from openedx.core.lib.gating import api as gating_api
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import ItemFactory, CourseFactory
|
||||
|
||||
|
||||
class TestHandleItemDeleted(ModuleStoreTestCase, MilestonesTestCaseMixin):
|
||||
"""
|
||||
Test case for handle_score_changed django signal handler
|
||||
"""
|
||||
def setUp(self):
|
||||
"""
|
||||
Initial data setup
|
||||
"""
|
||||
super(TestHandleItemDeleted, self).setUp()
|
||||
|
||||
self.course = CourseFactory.create()
|
||||
self.course.enable_subsection_gating = True
|
||||
self.course.save()
|
||||
self.chapter = ItemFactory.create(
|
||||
parent=self.course,
|
||||
category="chapter",
|
||||
display_name="Chapter"
|
||||
)
|
||||
self.open_seq = ItemFactory.create(
|
||||
parent=self.chapter,
|
||||
category='sequential',
|
||||
display_name="Open Sequential"
|
||||
)
|
||||
self.gated_seq = ItemFactory.create(
|
||||
parent=self.chapter,
|
||||
category='sequential',
|
||||
display_name="Gated Sequential"
|
||||
)
|
||||
gating_api.add_prerequisite(self.course.id, self.open_seq.location)
|
||||
gating_api.set_required_content(self.course.id, self.gated_seq.location, self.open_seq.location, 100)
|
||||
|
||||
@patch('contentstore.signals.gating_api.set_required_content')
|
||||
@patch('contentstore.signals.gating_api.remove_prerequisite')
|
||||
def test_chapter_deleted(self, mock_remove_prereq, mock_set_required):
|
||||
""" Test gating milestone data is cleanup up when course content item is deleted """
|
||||
handle_item_deleted(usage_key=self.chapter.location, user_id=0)
|
||||
mock_remove_prereq.assert_called_with(self.open_seq.location)
|
||||
mock_set_required.assert_called_with(self.open_seq.location.course_key, self.open_seq.location, None, None)
|
||||
|
||||
@patch('contentstore.signals.gating_api.set_required_content')
|
||||
@patch('contentstore.signals.gating_api.remove_prerequisite')
|
||||
def test_sequential_deleted(self, mock_remove_prereq, mock_set_required):
|
||||
""" Test gating milestone data is cleanup up when course content item is deleted """
|
||||
handle_item_deleted(usage_key=self.open_seq.location, user_id=0)
|
||||
mock_remove_prereq.assert_called_with(self.open_seq.location)
|
||||
mock_set_required.assert_called_with(self.open_seq.location.course_key, self.open_seq.location, None, None)
|
||||
@@ -3,44 +3,26 @@ from __future__ import absolute_import
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
from uuid import uuid4
|
||||
from datetime import datetime
|
||||
from pytz import UTC
|
||||
import json
|
||||
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime
|
||||
from functools import partial
|
||||
from static_replace import replace_static_urls
|
||||
from openedx.core.lib.xblock_utils import wrap_xblock, request_token
|
||||
from uuid import uuid4
|
||||
|
||||
import dogstats_wrapper as dog_stats_api
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import HttpResponseBadRequest, HttpResponse, Http404
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.decorators.http import require_http_methods
|
||||
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from opaque_keys.edx.locator import LibraryUsageLocator
|
||||
from pytz import UTC
|
||||
from xblock.fields import Scope
|
||||
from xblock.fragment import Fragment
|
||||
|
||||
import xmodule
|
||||
from xmodule.tabs import CourseTabList
|
||||
from xmodule.modulestore import ModuleStoreEnum, EdxJSONEncoder
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
|
||||
from xmodule.modulestore.inheritance import own_metadata
|
||||
from xmodule.modulestore.draft_and_published import DIRECT_ONLY_CATEGORIES
|
||||
from xmodule.x_module import PREVIEW_VIEWS, STUDIO_VIEW, STUDENT_VIEW, DEPRECATION_VSCOMPAT_EVENT
|
||||
|
||||
from xmodule.course_module import DEFAULT_START_DATE
|
||||
from django.contrib.auth.models import User
|
||||
from util.date_utils import get_default_time_display
|
||||
|
||||
from util.json_request import expect_json, JsonResponse
|
||||
from util.milestones_helpers import is_entrance_exams_enabled
|
||||
|
||||
from student.auth import has_studio_write_access, has_studio_read_access
|
||||
from cms.lib.xblock.authoring_mixin import VISIBILITY_VIEW
|
||||
from contentstore.utils import (
|
||||
find_release_date_source, find_staff_lock_source, is_currently_visible_to_students,
|
||||
ancestor_has_staff_lock, has_children_visible_to_specific_content_groups,
|
||||
@@ -49,11 +31,23 @@ from contentstore.utils import (
|
||||
from contentstore.views.helpers import is_unit, xblock_studio_url, xblock_primary_child_category, \
|
||||
xblock_type_display_name, get_parent_xblock, create_xblock, usage_key_with_run
|
||||
from contentstore.views.preview import get_preview_fragment
|
||||
from openedx.core.lib.gating import api as gating_api
|
||||
from edxmako.shortcuts import render_to_string
|
||||
from models.settings.course_grading import CourseGradingModel
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from opaque_keys.edx.locator import LibraryUsageLocator
|
||||
from cms.lib.xblock.authoring_mixin import VISIBILITY_VIEW
|
||||
from openedx.core.lib.xblock_utils import wrap_xblock, request_token
|
||||
from static_replace import replace_static_urls
|
||||
from student.auth import has_studio_write_access, has_studio_read_access
|
||||
from util.date_utils import get_default_time_display
|
||||
from util.json_request import expect_json, JsonResponse
|
||||
from util.milestones_helpers import is_entrance_exams_enabled
|
||||
from xmodule.course_module import DEFAULT_START_DATE
|
||||
from xmodule.modulestore import ModuleStoreEnum, EdxJSONEncoder
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.draft_and_published import DIRECT_ONLY_CATEGORIES
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
|
||||
from xmodule.modulestore.inheritance import own_metadata
|
||||
from xmodule.tabs import CourseTabList
|
||||
from xmodule.x_module import PREVIEW_VIEWS, STUDIO_VIEW, STUDENT_VIEW, DEPRECATION_VSCOMPAT_EVENT
|
||||
|
||||
__all__ = [
|
||||
'orphan_handler', 'xblock_handler', 'xblock_view_handler', 'xblock_outline_handler', 'xblock_container_handler'
|
||||
@@ -110,6 +104,10 @@ def xblock_handler(request, usage_key_string):
|
||||
to None! Absent ones will be left alone.
|
||||
:nullout: which metadata fields to set to None
|
||||
:graderType: change how this unit is graded
|
||||
:isPrereq: Set this xblock as a prerequisite which can be used to limit access to other xblocks
|
||||
:prereqUsageKey: Use the xblock identified by this usage key to limit access to this xblock
|
||||
:prereqMinScore: The minimum score that needs to be achieved on the prerequisite xblock
|
||||
identifed by prereqUsageKey
|
||||
:publish: can be:
|
||||
'make_public': publish the content
|
||||
'republish': publish this item *only* if it was previously published
|
||||
@@ -163,6 +161,9 @@ def xblock_handler(request, usage_key_string):
|
||||
metadata=request.json.get('metadata'),
|
||||
nullout=request.json.get('nullout'),
|
||||
grader_type=request.json.get('graderType'),
|
||||
is_prereq=request.json.get('isPrereq'),
|
||||
prereq_usage_key=request.json.get('prereqUsageKey'),
|
||||
prereq_min_score=request.json.get('prereqMinScore'),
|
||||
publish=request.json.get('publish'),
|
||||
)
|
||||
elif request.method in ('PUT', 'POST'):
|
||||
@@ -379,7 +380,7 @@ def _update_with_callback(xblock, user, old_metadata=None, old_content=None):
|
||||
|
||||
|
||||
def _save_xblock(user, xblock, data=None, children_strings=None, metadata=None, nullout=None,
|
||||
grader_type=None, publish=None):
|
||||
grader_type=None, is_prereq=None, prereq_usage_key=None, prereq_min_score=None, publish=None):
|
||||
"""
|
||||
Saves xblock w/ its fields. Has special processing for grader_type, publish, and nullout and Nones in metadata.
|
||||
nullout means to truly set the field to None whereas nones in metadata mean to unset them (so they revert
|
||||
@@ -479,8 +480,8 @@ def _save_xblock(user, xblock, data=None, children_strings=None, metadata=None,
|
||||
xblock = _update_with_callback(xblock, user, old_metadata, old_content)
|
||||
|
||||
# for static tabs, their containing course also records their display name
|
||||
course = store.get_course(xblock.location.course_key)
|
||||
if xblock.location.category == 'static_tab':
|
||||
course = store.get_course(xblock.location.course_key)
|
||||
# find the course's reference to this tab and update the name.
|
||||
static_tab = CourseTabList.get_tab_by_slug(course.tabs, xblock.location.name)
|
||||
# only update if changed
|
||||
@@ -497,9 +498,23 @@ def _save_xblock(user, xblock, data=None, children_strings=None, metadata=None,
|
||||
if grader_type is not None:
|
||||
result.update(CourseGradingModel.update_section_grader_type(xblock, grader_type, user))
|
||||
|
||||
# If publish is set to 'republish' and this item is not in direct only categories and has previously been published,
|
||||
# then this item should be republished. This is used by staff locking to ensure that changing the draft
|
||||
# value of the staff lock will also update the published version, but only at the unit level.
|
||||
# Save gating info
|
||||
if xblock.category == 'sequential' and course.enable_subsection_gating:
|
||||
if is_prereq is not None:
|
||||
if is_prereq:
|
||||
gating_api.add_prerequisite(xblock.location.course_key, xblock.location)
|
||||
else:
|
||||
gating_api.remove_prerequisite(xblock.location)
|
||||
result['is_prereq'] = is_prereq
|
||||
|
||||
if prereq_usage_key is not None:
|
||||
gating_api.set_required_content(
|
||||
xblock.location.course_key, xblock.location, prereq_usage_key, prereq_min_score
|
||||
)
|
||||
|
||||
# If publish is set to 'republish' and this item is not in direct only categories and has previously been
|
||||
# published, then this item should be republished. This is used by staff locking to ensure that changing the
|
||||
# draft value of the staff lock will also update the published version, but only at the unit level.
|
||||
if publish == 'republish' and xblock.category not in DIRECT_ONLY_CATEGORIES:
|
||||
if modulestore().has_published_version(xblock):
|
||||
publish = 'make_public'
|
||||
@@ -751,6 +766,35 @@ def _get_module_info(xblock, rewrite_static_links=True, include_ancestor_info=Fa
|
||||
return xblock_info
|
||||
|
||||
|
||||
def _get_gating_info(course, xblock):
|
||||
"""
|
||||
Returns a dict containing gating information for the given xblock which
|
||||
can be added to xblock info responses.
|
||||
|
||||
Arguments:
|
||||
course (CourseDescriptor): The course
|
||||
xblock (XBlock): The xblock
|
||||
|
||||
Returns:
|
||||
dict: Gating information
|
||||
"""
|
||||
info = {}
|
||||
if xblock.category == 'sequential' and course.enable_subsection_gating:
|
||||
info["is_prereq"] = gating_api.is_prerequisite(course.id, xblock.location)
|
||||
info["prereqs"] = [
|
||||
p for p in course.gating_prerequisites if unicode(xblock.location) not in p['namespace']
|
||||
]
|
||||
prereq, prereq_min_score = gating_api.get_required_content(
|
||||
course.id,
|
||||
xblock.location
|
||||
)
|
||||
info["prereq"] = prereq
|
||||
info["prereq_min_score"] = prereq_min_score
|
||||
if prereq:
|
||||
info["visibility_state"] = VisibilityState.gated
|
||||
return info
|
||||
|
||||
|
||||
def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=False, include_child_info=False,
|
||||
course_outline=False, include_children_predicate=NEVER, parent_xblock=None, graders=None,
|
||||
user=None, course=None):
|
||||
@@ -873,9 +917,14 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
|
||||
"default_time_limit_minutes": xblock.default_time_limit_minutes
|
||||
})
|
||||
|
||||
# Entrance exam subsection should be hidden. in_entrance_exam is inherited metadata, all children will have it.
|
||||
if xblock.category == 'sequential' and getattr(xblock, "in_entrance_exam", False):
|
||||
xblock_info["is_header_visible"] = False
|
||||
# Update with gating info
|
||||
xblock_info.update(_get_gating_info(course, xblock))
|
||||
|
||||
if xblock.category == 'sequential':
|
||||
# Entrance exam subsection should be hidden. in_entrance_exam is
|
||||
# inherited metadata, all children will have it.
|
||||
if getattr(xblock, "in_entrance_exam", False):
|
||||
xblock_info["is_header_visible"] = False
|
||||
|
||||
if data is not None:
|
||||
xblock_info["data"] = data
|
||||
@@ -953,12 +1002,15 @@ class VisibilityState(object):
|
||||
|
||||
staff_only - all of the block's content is to be shown to staff only
|
||||
Note: staff only items do not affect their parent's state.
|
||||
|
||||
gated - all of the block's content is to be shown to students only after the configured prerequisite is met
|
||||
"""
|
||||
live = 'live'
|
||||
ready = 'ready'
|
||||
unscheduled = 'unscheduled'
|
||||
needs_attention = 'needs_attention'
|
||||
staff_only = 'staff_only'
|
||||
gated = 'gated'
|
||||
|
||||
|
||||
def _compute_visibility_state(xblock, child_info, is_unit_with_changes):
|
||||
|
||||
@@ -20,10 +20,11 @@ from student.tests.factories import UserFactory
|
||||
from util import milestones_helpers
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from contentstore.views.helpers import create_xblock
|
||||
from milestones.tests.utils import MilestonesTestCaseMixin
|
||||
|
||||
|
||||
@patch.dict(settings.FEATURES, {'ENTRANCE_EXAMS': True})
|
||||
class EntranceExamHandlerTests(CourseTestCase):
|
||||
class EntranceExamHandlerTests(CourseTestCase, MilestonesTestCaseMixin):
|
||||
"""
|
||||
Base test class for create, save, and delete
|
||||
"""
|
||||
@@ -36,7 +37,6 @@ class EntranceExamHandlerTests(CourseTestCase):
|
||||
self.usage_key = self.course.location
|
||||
self.course_url = '/course/{}'.format(unicode(self.course.id))
|
||||
self.exam_url = '/course/{}/entrance_exam/'.format(unicode(self.course.id))
|
||||
milestones_helpers.seed_milestone_relationship_types()
|
||||
self.milestone_relationship_types = milestones_helpers.get_milestone_relationship_types()
|
||||
|
||||
def test_entrance_exam_milestone_addition(self):
|
||||
|
||||
140
cms/djangoapps/contentstore/views/tests/test_gating.py
Normal file
140
cms/djangoapps/contentstore/views/tests/test_gating.py
Normal file
@@ -0,0 +1,140 @@
|
||||
"""
|
||||
Unit tests for the gating feature in Studio
|
||||
"""
|
||||
import json
|
||||
|
||||
from mock import patch
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE
|
||||
from xmodule.modulestore.tests.factories import ItemFactory
|
||||
from contentstore.tests.utils import CourseTestCase
|
||||
from contentstore.utils import reverse_usage_url
|
||||
from contentstore.views.item import VisibilityState
|
||||
from openedx.core.lib.gating.api import GATING_NAMESPACE_QUALIFIER
|
||||
|
||||
|
||||
class TestSubsectionGating(CourseTestCase):
|
||||
"""
|
||||
Tests for the subsection gating feature
|
||||
"""
|
||||
MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Initial data setup
|
||||
"""
|
||||
super(TestSubsectionGating, self).setUp()
|
||||
|
||||
# Enable subsection gating for the test course
|
||||
self.course.enable_subsection_gating = True
|
||||
self.save_course()
|
||||
|
||||
# create a chapter
|
||||
self.chapter = ItemFactory.create(
|
||||
parent_location=self.course.location,
|
||||
category='chapter',
|
||||
display_name='untitled chapter'
|
||||
)
|
||||
|
||||
# create 2 sequentials
|
||||
self.seq1 = ItemFactory.create(
|
||||
parent_location=self.chapter.location,
|
||||
category='sequential',
|
||||
display_name='untitled sequential 1'
|
||||
)
|
||||
self.seq1_url = reverse_usage_url('xblock_handler', self.seq1.location)
|
||||
|
||||
self.seq2 = ItemFactory.create(
|
||||
parent_location=self.chapter.location,
|
||||
category='sequential',
|
||||
display_name='untitled sequential 2'
|
||||
)
|
||||
self.seq2_url = reverse_usage_url('xblock_handler', self.seq2.location)
|
||||
|
||||
@patch('contentstore.views.item.gating_api.add_prerequisite')
|
||||
def test_add_prerequisite(self, mock_add_prereq):
|
||||
"""
|
||||
Test adding a subsection as a prerequisite
|
||||
"""
|
||||
|
||||
self.client.ajax_post(
|
||||
self.seq1_url,
|
||||
data={'isPrereq': True}
|
||||
)
|
||||
mock_add_prereq.assert_called_with(self.course.id, self.seq1.location)
|
||||
|
||||
@patch('contentstore.views.item.gating_api.remove_prerequisite')
|
||||
def test_remove_prerequisite(self, mock_remove_prereq):
|
||||
"""
|
||||
Test removing a subsection as a prerequisite
|
||||
"""
|
||||
|
||||
self.client.ajax_post(
|
||||
self.seq1_url,
|
||||
data={'isPrereq': False}
|
||||
)
|
||||
mock_remove_prereq.assert_called_with(self.seq1.location)
|
||||
|
||||
@patch('contentstore.views.item.gating_api.set_required_content')
|
||||
def test_add_gate(self, mock_set_required_content):
|
||||
"""
|
||||
Test adding a gated subsection
|
||||
"""
|
||||
|
||||
self.client.ajax_post(
|
||||
self.seq2_url,
|
||||
data={'prereqUsageKey': unicode(self.seq1.location), 'prereqMinScore': '100'}
|
||||
)
|
||||
mock_set_required_content.assert_called_with(
|
||||
self.course.id,
|
||||
self.seq2.location,
|
||||
unicode(self.seq1.location),
|
||||
'100'
|
||||
)
|
||||
|
||||
@patch('contentstore.views.item.gating_api.set_required_content')
|
||||
def test_remove_gate(self, mock_set_required_content):
|
||||
"""
|
||||
Test removing a gated subsection
|
||||
"""
|
||||
|
||||
self.client.ajax_post(
|
||||
self.seq2_url,
|
||||
data={'prereqUsageKey': '', 'prereqMinScore': ''}
|
||||
)
|
||||
mock_set_required_content.assert_called_with(
|
||||
self.course.id,
|
||||
self.seq2.location,
|
||||
'',
|
||||
''
|
||||
)
|
||||
|
||||
@patch('xmodule.course_module.gating_api.get_prerequisites')
|
||||
@patch('contentstore.views.item.gating_api.get_required_content')
|
||||
@patch('contentstore.views.item.gating_api.is_prerequisite')
|
||||
def test_get_prerequisite(self, mock_is_prereq, mock_get_required_content, mock_get_prereqs):
|
||||
mock_is_prereq.return_value = True
|
||||
mock_get_required_content.return_value = unicode(self.seq1.location), 100
|
||||
mock_get_prereqs.return_value = [
|
||||
{'namespace': '{}{}'.format(unicode(self.seq1.location), GATING_NAMESPACE_QUALIFIER)},
|
||||
{'namespace': '{}{}'.format(unicode(self.seq2.location), GATING_NAMESPACE_QUALIFIER)}
|
||||
]
|
||||
resp = json.loads(self.client.get_json(self.seq2_url).content)
|
||||
mock_is_prereq.assert_called_with(self.course.id, self.seq2.location)
|
||||
mock_get_required_content.assert_called_with(self.course.id, self.seq2.location)
|
||||
mock_get_prereqs.assert_called_with(self.course.id)
|
||||
self.assertTrue(resp['is_prereq'])
|
||||
self.assertEqual(resp['prereq'], unicode(self.seq1.location))
|
||||
self.assertEqual(resp['prereq_min_score'], 100)
|
||||
self.assertEqual(resp['visibility_state'], VisibilityState.gated)
|
||||
|
||||
@patch('contentstore.signals.gating_api.set_required_content')
|
||||
@patch('contentstore.signals.gating_api.remove_prerequisite')
|
||||
def test_delete_item_signal_handler_called(self, mock_remove_prereq, mock_set_required):
|
||||
seq3 = ItemFactory.create(
|
||||
parent_location=self.chapter.location,
|
||||
category='sequential',
|
||||
display_name='untitled sequential 3'
|
||||
)
|
||||
self.client.delete(reverse_usage_url('xblock_handler', seq3.location))
|
||||
mock_remove_prereq.assert_called_with(seq3.location)
|
||||
mock_set_required.assert_called_with(seq3.location.course_key, seq3.location, None, None)
|
||||
@@ -26,10 +26,10 @@ from contentstore.tests.utils import CourseTestCase
|
||||
from openedx.core.lib.extract_tar import safetar_extractall
|
||||
from student import auth
|
||||
from student.roles import CourseInstructorRole, CourseStaffRole
|
||||
from util.milestones_helpers import seed_milestone_relationship_types
|
||||
from models.settings.course_metadata import CourseMetadata
|
||||
from util import milestones_helpers
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from milestones.tests.utils import MilestonesTestCaseMixin
|
||||
|
||||
TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE)
|
||||
TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'] = 'test_xcontent_%s' % uuid4().hex
|
||||
@@ -39,7 +39,7 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE)
|
||||
class ImportEntranceExamTestCase(CourseTestCase):
|
||||
class ImportEntranceExamTestCase(CourseTestCase, MilestonesTestCaseMixin):
|
||||
"""
|
||||
Unit tests for importing a course with entrance exam
|
||||
"""
|
||||
@@ -51,7 +51,6 @@ class ImportEntranceExamTestCase(CourseTestCase):
|
||||
|
||||
# Create tar test file -----------------------------------------------
|
||||
# OK course with entrance exam section:
|
||||
seed_milestone_relationship_types()
|
||||
entrance_exam_dir = tempfile.mkdtemp(dir=self.content_dir)
|
||||
# test course being deeper down than top of tar file
|
||||
embedded_exam_dir = os.path.join(entrance_exam_dir, "grandparent", "parent")
|
||||
|
||||
@@ -842,6 +842,9 @@ INSTALLED_APPS = (
|
||||
|
||||
# Credentials support
|
||||
'openedx.core.djangoapps.credentials',
|
||||
|
||||
# edx-milestones service
|
||||
'milestones',
|
||||
)
|
||||
|
||||
|
||||
@@ -948,9 +951,6 @@ OPTIONAL_APPS = (
|
||||
# edxval
|
||||
'edxval',
|
||||
|
||||
# milestones
|
||||
'milestones',
|
||||
|
||||
# Organizations App (http://github.com/edx/edx-organizations)
|
||||
'organizations',
|
||||
)
|
||||
|
||||
@@ -67,6 +67,10 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "common/js/components/u
|
||||
edited_by: 'MockUser',
|
||||
course_graders: ["Lab", "Howework"],
|
||||
has_explicit_staff_lock: false,
|
||||
is_prereq: false,
|
||||
prereqs: [],
|
||||
prereq: '',
|
||||
prereq_min_score: '',
|
||||
child_info: {
|
||||
category: 'vertical',
|
||||
display_name: 'Unit',
|
||||
@@ -216,7 +220,8 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "common/js/components/u
|
||||
'course-outline', 'xblock-string-field-editor', 'modal-button',
|
||||
'basic-modal', 'course-outline-modal', 'release-date-editor',
|
||||
'due-date-editor', 'grading-editor', 'publish-editor',
|
||||
'staff-lock-editor', 'settings-tab-section', 'timed-examination-preference-editor'
|
||||
'staff-lock-editor', 'settings-modal-tabs', 'timed-examination-preference-editor',
|
||||
'access-editor'
|
||||
]);
|
||||
appendSetFixtures(mockOutlinePage);
|
||||
mockCourseJSON = createMockCourseJSON({}, [
|
||||
@@ -580,8 +585,9 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "common/js/components/u
|
||||
|
||||
describe("Subsection", function() {
|
||||
var getDisplayNameWrapper, setEditModalValues, mockServerValuesJson,
|
||||
selectDisableSpecialExams, selectGeneralSettings, selectAdvancedSettings,
|
||||
selectTimedExam, selectProctoredExam, selectPracticeExam;
|
||||
selectDisableSpecialExams, selectBasicSettings, selectAdvancedSettings,
|
||||
selectAccessSettings, selectTimedExam, selectProctoredExam, selectPracticeExam,
|
||||
selectPrerequisite, selectLastPrerequisiteSubsection;
|
||||
|
||||
getDisplayNameWrapper = function() {
|
||||
return getItemHeaders('subsection').find('.wrapper-xblock-field');
|
||||
@@ -598,12 +604,16 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "common/js/components/u
|
||||
this.$("#id_not_timed").prop('checked', true).trigger('change');
|
||||
};
|
||||
|
||||
selectGeneralSettings = function() {
|
||||
this.$(".modal-section .general-settings-button").click();
|
||||
selectBasicSettings = function() {
|
||||
this.$(".modal-section .settings-tab-button[data-tab='basic']").click();
|
||||
};
|
||||
|
||||
selectAdvancedSettings = function() {
|
||||
this.$(".modal-section .advanced-settings-button").click();
|
||||
this.$(".modal-section .settings-tab-button[data-tab='advanced']").click();
|
||||
};
|
||||
|
||||
selectAccessSettings = function() {
|
||||
this.$(".modal-section .settings-tab-button[data-tab='access']").click();
|
||||
};
|
||||
|
||||
selectTimedExam = function(time_limit) {
|
||||
@@ -624,6 +634,15 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "common/js/components/u
|
||||
this.$("#id_time_limit").trigger('focusout');
|
||||
};
|
||||
|
||||
selectPrerequisite = function() {
|
||||
this.$("#is_prereq").prop('checked', true).trigger('change');
|
||||
};
|
||||
|
||||
selectLastPrerequisiteSubsection = function(minScore) {
|
||||
this.$("#prereq option:last").prop('selected', true).trigger('change');
|
||||
this.$("#prereq_min_score").val(minScore).trigger('keyup');
|
||||
};
|
||||
|
||||
// Contains hard-coded dates because dates are presented in different formats.
|
||||
mockServerValuesJson = createMockSectionJSON({
|
||||
release_date: 'Jan 01, 2970 at 05:00 UTC'
|
||||
@@ -637,6 +656,7 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "common/js/components/u
|
||||
due: "2014-07-10T00:00:00Z",
|
||||
has_explicit_staff_lock: true,
|
||||
staff_only_message: true,
|
||||
is_prereq: false,
|
||||
"is_time_limited": true,
|
||||
"is_practice_exam": false,
|
||||
"is_proctored_exam": true,
|
||||
@@ -710,21 +730,63 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "common/js/components/u
|
||||
collapseItemsAndVerifyState('subsection');
|
||||
expandItemsAndVerifyState('subsection');
|
||||
});
|
||||
|
||||
it('can show general settings', function() {
|
||||
|
||||
it('can show basic settings', function() {
|
||||
createCourseOutlinePage(this, mockCourseJSON, false);
|
||||
outlinePage.$('.outline-subsection .configure-button').click();
|
||||
selectGeneralSettings();
|
||||
expect($('.modal-section .general-settings-button')).toHaveClass('active');
|
||||
expect($('.modal-section .advanced-settings-button')).not.toHaveClass('active');
|
||||
selectBasicSettings();
|
||||
expect($('.modal-section .settings-tab-button[data-tab="basic"]')).toHaveClass('active');
|
||||
expect($('.modal-section .settings-tab-button[data-tab="advanced"]')).not.toHaveClass('active');
|
||||
expect($('.modal-section .settings-tab-button[data-tab="access"]')).not.toHaveClass('active');
|
||||
});
|
||||
|
||||
it('can show advanced settings', function() {
|
||||
createCourseOutlinePage(this, mockCourseJSON, false);
|
||||
outlinePage.$('.outline-subsection .configure-button').click();
|
||||
selectAdvancedSettings();
|
||||
expect($('.modal-section .general-settings-button')).not.toHaveClass('active');
|
||||
expect($('.modal-section .advanced-settings-button')).toHaveClass('active');
|
||||
expect($('.modal-section .settings-tab-button[data-tab="basic"]')).not.toHaveClass('active');
|
||||
expect($('.modal-section .settings-tab-button[data-tab="advanced"]')).toHaveClass('active');
|
||||
expect($('.modal-section .settings-tab-button[data-tab="access"]')).not.toHaveClass('active');
|
||||
});
|
||||
|
||||
it('can show access settings', function() {
|
||||
createCourseOutlinePage(this, mockCourseJSON, false);
|
||||
outlinePage.$('.outline-subsection .configure-button').click();
|
||||
selectAccessSettings();
|
||||
expect($('.modal-section .settings-tab-button[data-tab="basic"]')).not.toHaveClass('active');
|
||||
expect($('.modal-section .settings-tab-button[data-tab="advanced"]')).not.toHaveClass('active');
|
||||
expect($('.modal-section .settings-tab-button[data-tab="access"]')).toHaveClass('active');
|
||||
});
|
||||
|
||||
it('does not show settings tab headers if there is only one tab to show', function() {
|
||||
var mockSubsectionJSON = createMockSubsectionJSON({}, []);
|
||||
delete mockSubsectionJSON.is_prereq;
|
||||
delete mockSubsectionJSON.prereqs;
|
||||
delete mockSubsectionJSON.prereq;
|
||||
delete mockSubsectionJSON.prereq_min_score;
|
||||
var mockCourseJSON = createMockCourseJSON({
|
||||
enable_proctored_exams: false,
|
||||
enable_timed_exams: false
|
||||
}, [
|
||||
createMockSectionJSON({}, [mockSubsectionJSON])
|
||||
]);
|
||||
createCourseOutlinePage(this, mockCourseJSON, false);
|
||||
outlinePage.$('.outline-subsection .configure-button').click();
|
||||
expect($(".settings-tabs-header").length).toBe(0);
|
||||
});
|
||||
|
||||
it('can show correct editors for self_paced course', function() {
|
||||
var mockCourseJSON = createMockCourseJSON({}, [
|
||||
createMockSectionJSON({}, [
|
||||
createMockSubsectionJSON({}, [])
|
||||
])
|
||||
]);
|
||||
createCourseOutlinePage(this, mockCourseJSON, false);
|
||||
/* global course */
|
||||
course.set('self_paced', true);
|
||||
outlinePage.$('.outline-subsection .configure-button').click();
|
||||
expect($(".edit-settings-release").length).toBe(0);
|
||||
expect($(".grading-due-date").length).toBe(0);
|
||||
});
|
||||
|
||||
it('can select valid time', function() {
|
||||
@@ -759,6 +821,7 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "common/js/components/u
|
||||
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/mock-subsection', {
|
||||
"graderType":"Lab",
|
||||
"publish": "republish",
|
||||
"isPrereq": false,
|
||||
"metadata":{
|
||||
"visible_to_staff_only": true,
|
||||
"start":"2014-07-09T00:00:00.000Z",
|
||||
@@ -988,6 +1051,176 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "common/js/components/u
|
||||
expect($("#id_time_limit").val()).toBe("02:30");
|
||||
});
|
||||
|
||||
it('can select prerequisite', function() {
|
||||
createCourseOutlinePage(this, mockCourseJSON, false);
|
||||
outlinePage.$('.outline-subsection .configure-button').click();
|
||||
selectPrerequisite();
|
||||
expect($('#is_prereq').is(':checked')).toBe(true);
|
||||
$('.wrapper-modal-window .action-save').click();
|
||||
});
|
||||
|
||||
it('can be deleted when it is a prerequisite', function() {
|
||||
var promptSpy = EditHelpers.createPromptSpy();
|
||||
var mockCourseWithPrequisiteJSON = createMockCourseJSON({}, [
|
||||
createMockSectionJSON({}, [
|
||||
createMockSubsectionJSON({
|
||||
is_prereq: true,
|
||||
}, []),
|
||||
])
|
||||
]);
|
||||
createCourseOutlinePage(this, mockCourseWithPrequisiteJSON, false);
|
||||
getItemHeaders('subsection').find('.delete-button').click();
|
||||
EditHelpers.confirmPrompt(promptSpy);
|
||||
AjaxHelpers.expectJsonRequest(requests, 'DELETE', '/xblock/mock-subsection');
|
||||
AjaxHelpers.respondWithJson(requests, {});
|
||||
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section');
|
||||
});
|
||||
|
||||
it('can show a saved prerequisite correctly', function() {
|
||||
var mockCourseWithPrequisiteJSON = createMockCourseJSON({}, [
|
||||
createMockSectionJSON({}, [
|
||||
createMockSubsectionJSON({
|
||||
is_prereq: true,
|
||||
}, []),
|
||||
])
|
||||
]);
|
||||
createCourseOutlinePage(this, mockCourseWithPrequisiteJSON, false);
|
||||
outlinePage.$('.outline-subsection .configure-button').click();
|
||||
expect($('#is_prereq').is(':checked')).toBe(true);
|
||||
});
|
||||
|
||||
it('does not display prerequisite subsections if none are available', function() {
|
||||
createCourseOutlinePage(this, mockCourseJSON, false);
|
||||
outlinePage.$('.outline-subsection .configure-button').click();
|
||||
expect($('.gating-prereq').length).toBe(0);
|
||||
});
|
||||
|
||||
it('can display available prerequisite subsections', function() {
|
||||
var mockCourseWithPreqsJSON = createMockCourseJSON({}, [
|
||||
createMockSectionJSON({}, [
|
||||
createMockSubsectionJSON({
|
||||
prereqs: [{block_usage_key: 'usage_key', block_display_name: 'Prereq Subsection 1'}]
|
||||
}, []),
|
||||
])
|
||||
]);
|
||||
createCourseOutlinePage(this, mockCourseWithPreqsJSON, false);
|
||||
outlinePage.$('.outline-subsection .configure-button').click();
|
||||
expect($('.gating-prereq').length).toBe(1);
|
||||
});
|
||||
|
||||
it('can select prerequisite subsection', function() {
|
||||
var mockCourseWithPreqsJSON = createMockCourseJSON({}, [
|
||||
createMockSectionJSON({}, [
|
||||
createMockSubsectionJSON({
|
||||
prereqs: [{block_usage_key: 'usage_key', block_display_name: 'Prereq Subsection 1'}]
|
||||
}, []),
|
||||
])
|
||||
]);
|
||||
createCourseOutlinePage(this, mockCourseWithPreqsJSON, false);
|
||||
outlinePage.$('.outline-subsection .configure-button').click();
|
||||
selectLastPrerequisiteSubsection('80');
|
||||
expect($('#prereq_min_score_input').css('display')).not.toBe('none');
|
||||
expect($('#prereq option:selected').val()).toBe('usage_key');
|
||||
expect($('#prereq_min_score').val()).toBe('80');
|
||||
$('.wrapper-modal-window .action-save').click();
|
||||
});
|
||||
|
||||
it('can display gating correctly', function() {
|
||||
var mockCourseWithPreqsJSON = createMockCourseJSON({}, [
|
||||
createMockSectionJSON({}, [
|
||||
createMockSubsectionJSON({
|
||||
visibility_state: 'gated',
|
||||
prereqs: [{block_usage_key: 'usage_key', block_display_name: 'Prereq Subsection 1'}],
|
||||
prereq: 'usage_key',
|
||||
prereq_min_score: '80'
|
||||
}, []),
|
||||
])
|
||||
]);
|
||||
createCourseOutlinePage(this, mockCourseWithPreqsJSON, false);
|
||||
expect($(".outline-subsection .status-message-copy")).toContainText(
|
||||
"Prerequisite: Prereq Subsection 1"
|
||||
);
|
||||
});
|
||||
|
||||
it('can show a saved prerequisite subsection correctly', function() {
|
||||
var mockCourseWithPreqsJSON = createMockCourseJSON({}, [
|
||||
createMockSectionJSON({}, [
|
||||
createMockSubsectionJSON({
|
||||
prereqs: [{block_usage_key: 'usage_key', block_display_name: 'Prereq Subsection 1'}],
|
||||
prereq: 'usage_key',
|
||||
prereq_min_score: '80'
|
||||
}, []),
|
||||
])
|
||||
]);
|
||||
createCourseOutlinePage(this, mockCourseWithPreqsJSON, false);
|
||||
outlinePage.$('.outline-subsection .configure-button').click();
|
||||
expect($('.gating-prereq').length).toBe(1);
|
||||
expect($('#prereq option:selected').val()).toBe('usage_key');
|
||||
expect($('#prereq_min_score_input').css('display')).not.toBe('none');
|
||||
expect($('#prereq_min_score').val()).toBe('80');
|
||||
});
|
||||
|
||||
it('can display validation error on non-integer minimum score', function() {
|
||||
var mockCourseWithPreqsJSON = createMockCourseJSON({}, [
|
||||
createMockSectionJSON({}, [
|
||||
createMockSubsectionJSON({
|
||||
prereqs: [{block_usage_key: 'usage_key', block_display_name: 'Prereq Subsection 1'}]
|
||||
}, []),
|
||||
])
|
||||
]);
|
||||
createCourseOutlinePage(this, mockCourseWithPreqsJSON, false);
|
||||
outlinePage.$('.outline-subsection .configure-button').click();
|
||||
selectLastPrerequisiteSubsection('abc');
|
||||
expect($('#prereq_min_score_error').css('display')).not.toBe('none');
|
||||
expect($(".wrapper-modal-window .action-save").prop('disabled')).toBe(true);
|
||||
expect($(".wrapper-modal-window .action-save").hasClass('is-disabled')).toBe(true);
|
||||
selectLastPrerequisiteSubsection('5.5');
|
||||
expect($('#prereq_min_score_error').css('display')).not.toBe('none');
|
||||
expect($(".wrapper-modal-window .action-save").prop('disabled')).toBe(true);
|
||||
expect($(".wrapper-modal-window .action-save").hasClass('is-disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('can display validation error on out of bounds minimum score', function() {
|
||||
var mockCourseWithPreqsJSON = createMockCourseJSON({}, [
|
||||
createMockSectionJSON({}, [
|
||||
createMockSubsectionJSON({
|
||||
prereqs: [{block_usage_key: 'usage_key', block_display_name: 'Prereq Subsection 1'}]
|
||||
}, []),
|
||||
])
|
||||
]);
|
||||
createCourseOutlinePage(this, mockCourseWithPreqsJSON, false);
|
||||
outlinePage.$('.outline-subsection .configure-button').click();
|
||||
selectLastPrerequisiteSubsection('-5');
|
||||
expect($('#prereq_min_score_error').css('display')).not.toBe('none');
|
||||
expect($(".wrapper-modal-window .action-save").prop('disabled')).toBe(true);
|
||||
expect($(".wrapper-modal-window .action-save").hasClass('is-disabled')).toBe(true);
|
||||
selectLastPrerequisiteSubsection('105');
|
||||
expect($('#prereq_min_score_error').css('display')).not.toBe('none');
|
||||
expect($(".wrapper-modal-window .action-save").prop('disabled')).toBe(true);
|
||||
expect($(".wrapper-modal-window .action-save").hasClass('is-disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('does not display validation error on valid minimum score', function() {
|
||||
var mockCourseWithPreqsJSON = createMockCourseJSON({}, [
|
||||
createMockSectionJSON({}, [
|
||||
createMockSubsectionJSON({
|
||||
prereqs: [{block_usage_key: 'usage_key', block_display_name: 'Prereq Subsection 1'}]
|
||||
}, []),
|
||||
])
|
||||
]);
|
||||
createCourseOutlinePage(this, mockCourseWithPreqsJSON, false);
|
||||
outlinePage.$('.outline-subsection .configure-button').click();
|
||||
selectAccessSettings();
|
||||
selectLastPrerequisiteSubsection('');
|
||||
expect($('#prereq_min_score_error').css('display')).toBe('none');
|
||||
selectLastPrerequisiteSubsection('80');
|
||||
expect($('#prereq_min_score_error').css('display')).toBe('none');
|
||||
selectLastPrerequisiteSubsection('0');
|
||||
expect($('#prereq_min_score_error').css('display')).toBe('none');
|
||||
selectLastPrerequisiteSubsection('100');
|
||||
expect($('#prereq_min_score_error').css('display')).toBe('none');
|
||||
});
|
||||
|
||||
it('release date, due date, grading type, and staff lock can be cleared.', function() {
|
||||
createCourseOutlinePage(this, mockCourseJSON, false);
|
||||
outlinePage.$('.outline-item .outline-subsection .configure-button').click();
|
||||
|
||||
@@ -147,6 +147,14 @@ define(["jquery", "underscore", "gettext", "js/views/baseview"],
|
||||
return this.getActionBar().find('.action-' + type);
|
||||
},
|
||||
|
||||
enableActionButton: function(type) {
|
||||
this.getActionBar().find('.action-' + type).prop('disabled', false).removeClass('is-disabled');
|
||||
},
|
||||
|
||||
disableActionButton: function(type) {
|
||||
this.getActionBar().find('.action-' + type).prop('disabled', true).addClass('is-disabled');
|
||||
},
|
||||
|
||||
resize: function() {
|
||||
var top, left, modalWindow, modalWidth, modalHeight,
|
||||
availableWidth, availableHeight, maxWidth, maxHeight;
|
||||
|
||||
@@ -14,7 +14,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
|
||||
'use strict';
|
||||
var CourseOutlineXBlockModal, SettingsXBlockModal, PublishXBlockModal, AbstractEditor, BaseDateEditor,
|
||||
ReleaseDateEditor, DueDateEditor, GradingEditor, PublishEditor, StaffLockEditor,
|
||||
VerificationAccessEditor, TimedExaminationPreferenceEditor;
|
||||
VerificationAccessEditor, TimedExaminationPreferenceEditor, AccessEditor;
|
||||
|
||||
CourseOutlineXBlockModal = BaseModal.extend({
|
||||
events : {
|
||||
@@ -104,6 +104,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
|
||||
});
|
||||
|
||||
SettingsXBlockModal = CourseOutlineXBlockModal.extend({
|
||||
|
||||
getTitle: function () {
|
||||
return interpolate(
|
||||
gettext('%(display_name)s Settings'),
|
||||
@@ -112,37 +113,42 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
|
||||
},
|
||||
|
||||
getIntroductionMessage: function () {
|
||||
return interpolate(
|
||||
gettext('Change the settings for %(display_name)s'),
|
||||
{ display_name: this.model.get('display_name') }, true
|
||||
);
|
||||
var message = '';
|
||||
var tabs = this.options.tabs;
|
||||
if (!tabs || tabs.length < 2) {
|
||||
message = interpolate(
|
||||
gettext('Change the settings for %(display_name)s'),
|
||||
{ display_name: this.model.get('display_name') }, true);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
initializeEditors: function () {
|
||||
var special_exams_editors = this.options.special_exam_editors;
|
||||
if (typeof special_exams_editors !== 'undefined' && special_exams_editors.length > 0) {
|
||||
var tabs_html = this.loadTemplate('settings-tab-section');
|
||||
this.$('.modal-section').html(tabs_html);
|
||||
this.options.editors = _.map(this.options.editors, function (Editor) {
|
||||
return new Editor({
|
||||
parentElement: this.$('.modal-section .general-settings'),
|
||||
model: this.model,
|
||||
xblockType: this.options.xblockType,
|
||||
enable_proctored_exams: this.options.enable_proctored_exams,
|
||||
enable_timed_exams: this.options.enable_timed_exams
|
||||
});
|
||||
}, this);
|
||||
|
||||
this.options.special_exam_editors = _.map(special_exams_editors, function (Editor) {
|
||||
return new Editor({
|
||||
parentElement: this.$('.modal-section .advanced-settings'),
|
||||
model: this.model,
|
||||
xblockType: this.options.xblockType,
|
||||
enable_proctored_exams: this.options.enable_proctored_exams,
|
||||
enable_timed_exams: this.options.enable_timed_exams
|
||||
});
|
||||
}, this);
|
||||
this.hideAdvancedSettings();
|
||||
var tabs = this.options.tabs;
|
||||
if (tabs && tabs.length > 0) {
|
||||
if (tabs.length > 1) {
|
||||
var tabsTemplate = this.loadTemplate('settings-modal-tabs');
|
||||
this.$('.modal-section').html(tabsTemplate({tabs: tabs}));
|
||||
_.each(this.options.tabs, function(tab) {
|
||||
this.options.editors.push.apply(
|
||||
this.options.editors,
|
||||
_.map(tab.editors, function (Editor) {
|
||||
return new Editor({
|
||||
parent: this,
|
||||
parentElement: this.$('.modal-section .' + tab.name),
|
||||
model: this.model,
|
||||
xblockType: this.options.xblockType,
|
||||
enable_proctored_exams: this.options.enable_proctored_exams,
|
||||
enable_timed_exams: this.options.enable_timed_exams
|
||||
});
|
||||
}, this)
|
||||
);
|
||||
}, this);
|
||||
this.showTab(tabs[0].name);
|
||||
} else {
|
||||
this.options.editors = tabs[0].editors;
|
||||
CourseOutlineXBlockModal.prototype.initializeEditors.call(this);
|
||||
}
|
||||
} else {
|
||||
CourseOutlineXBlockModal.prototype.initializeEditors.call(this);
|
||||
}
|
||||
@@ -150,8 +156,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
|
||||
|
||||
events: {
|
||||
'click .action-save': 'save',
|
||||
'click .general-settings-button': 'showGeneralSettings',
|
||||
'click .advanced-settings-button': 'showAdvancedSettings'
|
||||
'click .settings-tab-button': 'handleShowTab',
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -159,35 +164,22 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
|
||||
* @return {Object}
|
||||
*/
|
||||
getRequestData: function () {
|
||||
var combined_editors = this.options.editors.concat(this.options.special_exam_editors);
|
||||
var requestData = _.map(combined_editors, function (editor) {
|
||||
var requestData = _.map(this.options.editors, function (editor) {
|
||||
return editor.getRequestData();
|
||||
});
|
||||
return $.extend.apply(this, [true, {}].concat(requestData));
|
||||
},
|
||||
|
||||
hideAdvancedSettings: function() {
|
||||
this.$('.modal-section .general-settings-button').addClass('active');
|
||||
this.$('.modal-section .advanced-settings-button').removeClass('active');
|
||||
this.$('.modal-section .general-settings').show();
|
||||
this.$('.modal-section .advanced-settings').hide();
|
||||
|
||||
},
|
||||
|
||||
hideGeneralSettings: function() {
|
||||
this.$('.modal-section .general-settings-button').removeClass('active');
|
||||
this.$('.modal-section .advanced-settings-button').addClass('active');
|
||||
this.$('.modal-section .general-settings').hide();
|
||||
this.$('.modal-section .advanced-settings').show();
|
||||
},
|
||||
showGeneralSettings: function (event) {
|
||||
handleShowTab: function (event) {
|
||||
event.preventDefault();
|
||||
this.hideAdvancedSettings();
|
||||
this.showTab($(event.target).data('tab'));
|
||||
},
|
||||
|
||||
showAdvancedSettings: function (event) {
|
||||
event.preventDefault();
|
||||
this.hideGeneralSettings();
|
||||
showTab: function (tab) {
|
||||
this.$('.modal-section .settings-tab-button').removeClass('active');
|
||||
this.$('.modal-section .settings-tab-button[data-tab="' + tab + '"]').addClass('active');
|
||||
this.$('.modal-section .settings-tab').hide();
|
||||
this.$('.modal-section .' + tab).show();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -230,6 +222,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
|
||||
templateName: null,
|
||||
initialize: function() {
|
||||
this.template = this.loadTemplate(this.templateName);
|
||||
this.parent = this.options.parent;
|
||||
this.parentElement = this.options.parentElement;
|
||||
this.render();
|
||||
},
|
||||
@@ -488,6 +481,58 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
|
||||
};
|
||||
}
|
||||
});
|
||||
AccessEditor = AbstractEditor.extend({
|
||||
templateName: 'access-editor',
|
||||
className: 'edit-settings-access',
|
||||
events : {
|
||||
'change #prereq': 'handlePrereqSelect',
|
||||
'keyup #prereq_min_score': 'validateMinScore'
|
||||
},
|
||||
afterRender: function () {
|
||||
AbstractEditor.prototype.afterRender.call(this);
|
||||
var prereq = this.model.get('prereq') || '';
|
||||
var prereq_min_score = this.model.get('prereq_min_score') || '';
|
||||
this.$('#is_prereq').prop('checked', this.model.get('is_prereq'));
|
||||
this.$('#prereq option[value="' + prereq + '"]').prop('selected', true);
|
||||
this.$('#prereq_min_score').val(prereq_min_score);
|
||||
this.$('#prereq_min_score_input').toggle(prereq.length > 0);
|
||||
},
|
||||
handlePrereqSelect: function () {
|
||||
var showPrereqInput = this.$('#prereq option:selected').val().length > 0;
|
||||
this.$('#prereq_min_score_input').toggle(showPrereqInput);
|
||||
},
|
||||
validateMinScore: function () {
|
||||
var minScore = this.$('#prereq_min_score').val().trim();
|
||||
var minScoreInt = parseInt(minScore);
|
||||
// minScore needs to be an integer between 0 and 100
|
||||
if (
|
||||
minScore &&
|
||||
(
|
||||
typeof(minScoreInt) === 'undefined' ||
|
||||
String(minScoreInt) !== minScore ||
|
||||
minScoreInt < 0 ||
|
||||
minScoreInt > 100
|
||||
)
|
||||
) {
|
||||
this.$('#prereq_min_score_error').show();
|
||||
BaseModal.prototype.disableActionButton.call(this.parent, 'save');
|
||||
} else {
|
||||
this.$('#prereq_min_score_error').hide();
|
||||
BaseModal.prototype.enableActionButton.call(this.parent, 'save');
|
||||
}
|
||||
},
|
||||
getRequestData: function () {
|
||||
var minScore = this.$('#prereq_min_score').val();
|
||||
if (minScore) {
|
||||
minScore = minScore.trim();
|
||||
}
|
||||
return {
|
||||
isPrereq: this.$('#is_prereq').is(':checked'),
|
||||
prereqUsageKey: this.$('#prereq option:selected').val(),
|
||||
prereqMinScore: minScore
|
||||
};
|
||||
}
|
||||
});
|
||||
GradingEditor = AbstractEditor.extend({
|
||||
templateName: 'grading-editor',
|
||||
className: 'edit-settings-grading',
|
||||
@@ -687,20 +732,33 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
|
||||
|
||||
getEditModal: function (xblockInfo, options) {
|
||||
var editors = [];
|
||||
var special_exam_editors = [];
|
||||
var tabs = [];
|
||||
|
||||
if (xblockInfo.isChapter()) {
|
||||
editors = [ReleaseDateEditor, StaffLockEditor];
|
||||
} else if (xblockInfo.isSequential()) {
|
||||
editors = [ReleaseDateEditor, GradingEditor, DueDateEditor];
|
||||
tabs.push({
|
||||
name: 'basic',
|
||||
displayName: gettext('Basic'),
|
||||
editors: [ReleaseDateEditor, GradingEditor, DueDateEditor, StaffLockEditor]
|
||||
});
|
||||
|
||||
var enable_special_exams = (options.enable_proctored_exams || options.enable_timed_exams);
|
||||
if (enable_special_exams) {
|
||||
special_exam_editors.push(TimedExaminationPreferenceEditor);
|
||||
if (options.enable_proctored_exams || options.enable_timed_exams) {
|
||||
tabs.push({
|
||||
name: 'advanced',
|
||||
displayName: gettext('Advanced'),
|
||||
editors: [TimedExaminationPreferenceEditor]
|
||||
});
|
||||
}
|
||||
|
||||
editors.push(StaffLockEditor);
|
||||
|
||||
if (typeof(xblockInfo.get('is_prereq')) !== 'undefined') {
|
||||
tabs.push({
|
||||
name: 'access',
|
||||
// Translators: This label refers to access to course content.
|
||||
displayName: gettext('Access'),
|
||||
editors: [AccessEditor]
|
||||
});
|
||||
}
|
||||
} else if (xblockInfo.isVertical()) {
|
||||
editors = [StaffLockEditor];
|
||||
|
||||
@@ -711,10 +769,13 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
|
||||
/* globals course */
|
||||
if (course.get('self_paced')) {
|
||||
editors = _.without(editors, ReleaseDateEditor, DueDateEditor);
|
||||
_.each(tabs, function (tab) {
|
||||
tab.editors = _.without(editors, ReleaseDateEditor, DueDateEditor);
|
||||
});
|
||||
}
|
||||
return new SettingsXBlockModal($.extend({
|
||||
tabs: tabs,
|
||||
editors: editors,
|
||||
special_exam_editors: special_exam_editors,
|
||||
model: xblockInfo
|
||||
}, options));
|
||||
},
|
||||
|
||||
@@ -31,7 +31,8 @@ define(["jquery", "underscore", "gettext", "common/js/components/utils/view_util
|
||||
ready: 'ready',
|
||||
unscheduled: 'unscheduled',
|
||||
needsAttention: 'needs_attention',
|
||||
staffOnly: 'staff_only'
|
||||
staffOnly: 'staff_only',
|
||||
gated: 'gated'
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -73,15 +74,7 @@ define(["jquery", "underscore", "gettext", "common/js/components/utils/view_util
|
||||
deleteXBlock = function(xblockInfo, xblockType) {
|
||||
var deletion = $.Deferred(),
|
||||
url = ModuleUtils.getUpdateUrl(xblockInfo.id),
|
||||
xblockType = xblockType || gettext('component');
|
||||
ViewUtils.confirmThenRunOperation(
|
||||
interpolate(gettext('Delete this %(xblock_type)s?'), { xblock_type: xblockType }, true),
|
||||
interpolate(
|
||||
gettext('Deleting this %(xblock_type)s is permanent and cannot be undone.'),
|
||||
{ xblock_type: xblockType }, true
|
||||
),
|
||||
interpolate(gettext('Yes, delete this %(xblock_type)s'), { xblock_type: xblockType }, true),
|
||||
function() {
|
||||
operation = function() {
|
||||
ViewUtils.runOperationShowingMessage(gettext('Deleting'),
|
||||
function() {
|
||||
return $.ajax({
|
||||
@@ -90,8 +83,47 @@ define(["jquery", "underscore", "gettext", "common/js/components/utils/view_util
|
||||
}).success(function() {
|
||||
deletion.resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
},
|
||||
messageBody = interpolate(
|
||||
gettext('Deleting this %(xblock_type)s is permanent and cannot be undone.'),
|
||||
{ xblock_type: xblockType },
|
||||
true
|
||||
);
|
||||
xblockType = xblockType || 'component';
|
||||
if (xblockInfo.get('is_prereq')) {
|
||||
messageBody += ' ' + gettext('Any content that has listed this content as a prerequisite will also have access limitations removed.'); // jshint ignore:line
|
||||
ViewUtils.confirmThenRunOperation(
|
||||
interpolate(
|
||||
gettext('Delete this %(xblock_type)s (and prerequisite)?'),
|
||||
{ xblock_type: xblockType },
|
||||
true
|
||||
),
|
||||
messageBody,
|
||||
interpolate(
|
||||
gettext('Yes, delete this %(xblock_type)s'),
|
||||
{ xblock_type: xblockType },
|
||||
true
|
||||
),
|
||||
operation
|
||||
);
|
||||
} else {
|
||||
ViewUtils.confirmThenRunOperation(
|
||||
interpolate(
|
||||
gettext('Delete this %(xblock_type)s?'),
|
||||
{ xblock_type: xblockType },
|
||||
true
|
||||
),
|
||||
messageBody,
|
||||
interpolate(
|
||||
gettext('Yes, delete this %(xblock_type)s'),
|
||||
{ xblock_type: xblockType },
|
||||
true
|
||||
),
|
||||
operation
|
||||
);
|
||||
}
|
||||
return deletion.promise();
|
||||
};
|
||||
|
||||
@@ -141,6 +173,9 @@ define(["jquery", "underscore", "gettext", "common/js/components/utils/view_util
|
||||
if (visibilityState === VisibilityState.staffOnly) {
|
||||
return 'is-staff-only';
|
||||
}
|
||||
if (visibilityState === VisibilityState.gated) {
|
||||
return 'is-gated';
|
||||
}
|
||||
if (visibilityState === VisibilityState.live) {
|
||||
return 'is-live';
|
||||
}
|
||||
|
||||
@@ -188,6 +188,7 @@ $color-ready: $green;
|
||||
$color-warning: $orange-l2;
|
||||
$color-error: $red-l2;
|
||||
$color-staff-only: $black;
|
||||
$color-gated: $black;
|
||||
$color-visibility-set: $black;
|
||||
|
||||
$color-heading-base: $gray-d2;
|
||||
|
||||
@@ -99,16 +99,15 @@
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.settings-tab {
|
||||
.settings-tabs-header {
|
||||
margin-bottom: $baseline;
|
||||
border-bottom: 1px solid $gray-l3;
|
||||
|
||||
li.settings-section {
|
||||
li.settings-tab-buttons {
|
||||
display: inline-block;
|
||||
margin-right: $baseline;
|
||||
|
||||
.general-settings-button,
|
||||
.advanced-settings-button {
|
||||
.settings-tab-button {
|
||||
@extend %t-copy-sub1;
|
||||
@extend %t-regular;
|
||||
background-image: none;
|
||||
@@ -561,7 +560,7 @@
|
||||
.course-outline-modal {
|
||||
.exam-time-list-fields,
|
||||
.exam-review-rules-list-fields {
|
||||
margin: 0 0 ($baseline/2) ($baseline/2);
|
||||
margin: 0 0 ($baseline/2) 0;
|
||||
}
|
||||
.list-fields {
|
||||
.field-message {
|
||||
@@ -742,5 +741,12 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// UI: Access settings section
|
||||
.edit-settings-access {
|
||||
.gating-prereq {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,6 +339,20 @@ $outline-indent-width: $baseline;
|
||||
}
|
||||
}
|
||||
|
||||
// CASE: has gated content
|
||||
&.is-gated {
|
||||
|
||||
// needed to make sure direct children only
|
||||
> .section-status,
|
||||
> .subsection-status,
|
||||
> .unit-status {
|
||||
|
||||
.status-message .icon {
|
||||
color: $color-gated;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CASE: has unpublished content
|
||||
&.has-warnings {
|
||||
|
||||
@@ -421,6 +435,11 @@ $outline-indent-width: $baseline;
|
||||
border-left-color: $color-staff-only;
|
||||
}
|
||||
|
||||
// CASE: has gated content
|
||||
&.is-gated {
|
||||
border-left-color: $color-gated;
|
||||
}
|
||||
|
||||
// CASE: has unpublished content
|
||||
&.has-warnings {
|
||||
border-left-color: $color-warning;
|
||||
@@ -505,6 +524,11 @@ $outline-indent-width: $baseline;
|
||||
border-left-color: $color-staff-only;
|
||||
}
|
||||
|
||||
// CASE: is presented for gated
|
||||
&.is-gated {
|
||||
border-left-color: $color-gated;
|
||||
}
|
||||
|
||||
// CASE: has unpublished content
|
||||
&.has-warnings {
|
||||
border-left-color: $color-warning;
|
||||
@@ -697,4 +721,3 @@ $outline-indent-width: $baseline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -149,6 +149,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
// CASE: content is gated
|
||||
&.is-gated {
|
||||
@extend %bar-module-black;
|
||||
}
|
||||
|
||||
.bar-mod-content {
|
||||
border: 0;
|
||||
padding: ($baseline/2) ($baseline*0.75) ($baseline/4) ($baseline*0.75);
|
||||
|
||||
@@ -21,7 +21,7 @@ from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
|
||||
|
||||
<%block name="header_extras">
|
||||
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
|
||||
% for template_name in ['course-outline', 'xblock-string-field-editor', 'basic-modal', 'modal-button', 'course-outline-modal', 'due-date-editor', 'release-date-editor', 'grading-editor', 'publish-editor', 'staff-lock-editor', 'verification-access-editor', 'timed-examination-preference-editor', 'settings-tab-section']:
|
||||
% for template_name in ['course-outline', 'xblock-string-field-editor', 'basic-modal', 'modal-button', 'course-outline-modal', 'due-date-editor', 'release-date-editor', 'grading-editor', 'publish-editor', 'staff-lock-editor', 'verification-access-editor', 'timed-examination-preference-editor', 'access-editor', 'settings-modal-tabs']:
|
||||
<script type="text/template" id="${template_name}-tpl">
|
||||
<%static:include path="js/${template_name}.underscore" />
|
||||
</script>
|
||||
|
||||
45
cms/templates/js/access-editor.underscore
Normal file
45
cms/templates/js/access-editor.underscore
Normal file
@@ -0,0 +1,45 @@
|
||||
<form>
|
||||
<% if (xblockInfo.get('prereqs').length > 0) { %>
|
||||
<h3 class="modal-section-title"><%- gettext('Limit Access') %></h3>
|
||||
<div class="modal-section-content gating-prereq">
|
||||
<ul class="list-fields list-input">
|
||||
<p class="field-message">
|
||||
<%- gettext('Select a prerequisite subsection and enter a minimum score percentage to limit access to this subsection.') %>
|
||||
</p>
|
||||
<li class="field field-select">
|
||||
<label class="label">
|
||||
<%- gettext('Prerequisite:') %>
|
||||
<select id="prereq" class="input">
|
||||
<option value=""><%- gettext('No prerequisite') %></option>
|
||||
<% _.each(xblockInfo.get('prereqs'), function(prereq){ %>
|
||||
<option value="<%- prereq.block_usage_key %>"><%- prereq.block_display_name %></option>
|
||||
<% }); %>
|
||||
</select>
|
||||
</label>
|
||||
</li>
|
||||
<li id="prereq_min_score_input" class="field field-input input-cosmetic">
|
||||
<label class="label">
|
||||
<%- gettext('Minimum Score:') %>
|
||||
<input type="text" id="prereq_min_score" name="prereq_min_score" class="input input-text" size="3" />
|
||||
</label>
|
||||
</li>
|
||||
<div id="prereq_min_score_error" class="message-status error">
|
||||
<%- gettext('Minimum score must be an integer between 0 and 100.') %>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
<% } %>
|
||||
<h3 class="modal-section-title"><%- gettext('Use this as a Prerequisite') %></h3>
|
||||
<div class="modal-section-content gating-is-prereq">
|
||||
<ul class="list-fields list-input">
|
||||
<li class="field field-checkbox checkbox-cosmetic">
|
||||
<input type="checkbox" id="is_prereq" name="is_prereq" class="input input-checkbox" />
|
||||
<label for="is_prereq" class="label">
|
||||
<i class="icon fa fa-check-square-o input-checkbox-checked"></i>
|
||||
<i class="icon fa fa-square-o input-checkbox-unchecked"></i>
|
||||
<%- gettext('Yes, set this as a prerequisite which can be used to limit access to other content') %>
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,14 +1,6 @@
|
||||
<%
|
||||
var enable_proctored_exams = enable_proctored_exams;
|
||||
var enable_timed_exams = enable_timed_exams;
|
||||
%>
|
||||
|
||||
<div class="xblock-editor" data-locator="<%= xblockInfo.get('id') %>" data-course-key="<%= xblockInfo.get('courseKey') %>">
|
||||
<% if (!( enable_proctored_exams || enable_timed_exams )) { %>
|
||||
<div class="message modal-introduction">
|
||||
<p><%- introductionMessage %></p>
|
||||
</div>
|
||||
<% } %>
|
||||
<div class="message modal-introduction">
|
||||
<p><%- introductionMessage %></p>
|
||||
</div>
|
||||
<div class="modal-section"></div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,9 +2,25 @@
|
||||
var releasedToStudents = xblockInfo.get('released_to_students');
|
||||
var visibilityState = xblockInfo.get('visibility_state');
|
||||
var published = xblockInfo.get('published');
|
||||
var prereq = xblockInfo.get('prereq');
|
||||
|
||||
var statusMessage = null;
|
||||
var statusType = null;
|
||||
if (prereq) {
|
||||
var prereqDisplayName = '';
|
||||
_.each(xblockInfo.get('prereqs'), function (p) {
|
||||
if (p.block_usage_key == prereq) {
|
||||
prereqDisplayName = p.block_display_name;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
statusType = 'gated';
|
||||
statusMessage = interpolate(
|
||||
gettext('Prerequisite: %(prereq_display_name)s'),
|
||||
{prereq_display_name: prereqDisplayName},
|
||||
true
|
||||
);
|
||||
}
|
||||
if (staffOnlyMessage) {
|
||||
statusType = 'staff-only';
|
||||
statusMessage = gettext('Contains staff only content');
|
||||
@@ -28,6 +44,8 @@ if (statusType === 'warning') {
|
||||
statusIconClass = 'fa-warning';
|
||||
} else if (statusType === 'staff-only') {
|
||||
statusIconClass = 'fa-lock';
|
||||
} else if (statusType === 'gated') {
|
||||
statusIconClass = 'fa-lock';
|
||||
}
|
||||
|
||||
var gradingType = gettext('Ungraded');
|
||||
|
||||
10
cms/templates/js/settings-modal-tabs.underscore
Normal file
10
cms/templates/js/settings-modal-tabs.underscore
Normal file
@@ -0,0 +1,10 @@
|
||||
<ul class="settings-tabs-header">
|
||||
<% _.each(tabs, function(tab) { %>
|
||||
<li class="settings-tab-buttons">
|
||||
<button class="settings-tab-button" data-tab="<%- tab.name %>" href="#"><%- tab.displayName %></button>
|
||||
</li>
|
||||
<% }); %>
|
||||
</ul>
|
||||
<% _.each(tabs, function(tab) { %>
|
||||
<div class='settings-tab <%- tab.name %>'></div>
|
||||
<% }); %>
|
||||
@@ -1,10 +0,0 @@
|
||||
<ul class="settings-tab">
|
||||
<li class="settings-section">
|
||||
<button class="general-settings-button" href="#"><%- gettext('General Settings') %></button>
|
||||
</li>
|
||||
<li class="settings-section">
|
||||
<button class="advanced-settings-button" href="#"><%- gettext('Advanced') %></button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class='general-settings'></div>
|
||||
<div class='advanced-settings'></div>
|
||||
@@ -1,4 +1,5 @@
|
||||
<form>
|
||||
<h3 class="modal-section-title"><%= gettext('Set as a Special Exam') %></h3>
|
||||
<div class="modal-section-content has-actions">
|
||||
<div class='exam-time-list-fields'>
|
||||
<ul class="list-fields list-input">
|
||||
|
||||
Reference in New Issue
Block a user