Merge pull request #15556 from edx/andya/pre-start-date-access
Enable conditional pre-start-date access to courses
This commit is contained in:
@@ -28,7 +28,8 @@ from courseware.access_utils import (
|
||||
adjust_start_date,
|
||||
check_start_date,
|
||||
debug,
|
||||
in_preview_mode
|
||||
in_preview_mode,
|
||||
check_course_open_for_learner,
|
||||
)
|
||||
from courseware.masquerade import get_masquerade_role, is_masquerading_as_student
|
||||
from lms.djangoapps.ccx.custom_exception import CCXLocatorValidationException
|
||||
@@ -173,31 +174,6 @@ def has_staff_access_to_preview_mode(user, course_key):
|
||||
return has_admin_access_to_course or is_masquerading_as_student(user, course_key)
|
||||
|
||||
|
||||
def _can_access_descriptor_with_start_date(user, descriptor, course_key): # pylint: disable=invalid-name
|
||||
"""
|
||||
Checks if a user has access to a descriptor based on its start date.
|
||||
|
||||
If there is no start date specified, grant access.
|
||||
Else, check if we're past the start date.
|
||||
|
||||
Note:
|
||||
We do NOT check whether the user is staff or if the descriptor
|
||||
is detached... it is assumed both of these are checked by the caller.
|
||||
|
||||
Arguments:
|
||||
user (User): the user whose descriptor access we are checking.
|
||||
descriptor (AType): the descriptor for which we are checking access,
|
||||
where AType is CourseDescriptor, CourseOverview, or any other class
|
||||
that represents a descriptor and has the attributes .location, .id,
|
||||
.start, and .days_early_for_beta.
|
||||
|
||||
Returns:
|
||||
AccessResponse: The result of this access check. Possible results are
|
||||
ACCESS_GRANTED or a StartDateError.
|
||||
"""
|
||||
return check_start_date(user, descriptor.days_early_for_beta, descriptor.start, course_key)
|
||||
|
||||
|
||||
def _can_view_courseware_with_prerequisites(user, course): # pylint: disable=invalid-name
|
||||
"""
|
||||
Checks if a user has access to a course based on its prerequisites.
|
||||
@@ -333,7 +309,7 @@ def _has_access_course(user, action, courselike):
|
||||
"""
|
||||
response = (
|
||||
_visible_to_nonstaff_users(courselike) and
|
||||
_can_access_descriptor_with_start_date(user, courselike, courselike.id)
|
||||
check_course_open_for_learner(user, courselike)
|
||||
)
|
||||
|
||||
return (
|
||||
@@ -519,7 +495,7 @@ def _has_access_descriptor(user, action, descriptor, course_key=None):
|
||||
_can_access_descriptor_with_milestones(user, descriptor, course_key) and
|
||||
(
|
||||
_has_detached_class_tag(descriptor) or
|
||||
_can_access_descriptor_with_start_date(user, descriptor, course_key)
|
||||
check_start_date(user, descriptor.days_early_for_beta, descriptor.start, course_key)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ from django.utils.timezone import UTC
|
||||
|
||||
from courseware.access_response import AccessResponse, StartDateError
|
||||
from courseware.masquerade import is_masquerading_as_student
|
||||
from openedx.features.course_experience import COURSE_PRE_START_ACCESS_FLAG
|
||||
from student.roles import CourseBetaTesterRole
|
||||
from xmodule.util.django import get_current_request_hostname
|
||||
|
||||
@@ -83,10 +84,13 @@ def in_preview_mode():
|
||||
return bool(preview_lms_base and hostname and hostname.split(':')[0] == preview_lms_base.split(':')[0])
|
||||
|
||||
|
||||
def is_course_open_for_learner(user, course):
|
||||
def check_course_open_for_learner(user, course):
|
||||
"""
|
||||
Check if the course is open for learners based on the start date.
|
||||
|
||||
Returns:
|
||||
AccessResponse: Either ACCESS_GRANTED or StartDateError.
|
||||
"""
|
||||
now = datetime.now(UTC())
|
||||
effective_start = adjust_start_date(user, course.days_early_for_beta, course.start, course.id)
|
||||
return not(not in_preview_mode() and now < effective_start)
|
||||
if COURSE_PRE_START_ACCESS_FLAG.is_enabled(course.id):
|
||||
return ACCESS_GRANTED
|
||||
return check_start_date(user, course.days_early_for_beta, course.start, course.id)
|
||||
|
||||
@@ -19,7 +19,6 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
|
||||
import courseware.access as access
|
||||
import courseware.access_response as access_response
|
||||
from ccx.tests.factories import CcxFactory
|
||||
from courseware.masquerade import CourseMasquerade
|
||||
from courseware.tests.factories import (
|
||||
BetaTesterFactory,
|
||||
@@ -31,6 +30,7 @@ from courseware.tests.factories import (
|
||||
from courseware.tests.helpers import LoginEnrollmentTestCase, masquerade_as_group_member
|
||||
from lms.djangoapps.ccx.models import CustomCourseForEdX
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
|
||||
from student.models import CourseEnrollment
|
||||
from student.roles import CourseCcxCoachRole, CourseStaffRole
|
||||
from student.tests.factories import (
|
||||
@@ -45,7 +45,6 @@ from xmodule.course_module import (
|
||||
CATALOG_VISIBILITY_CATALOG_AND_ABOUT,
|
||||
CATALOG_VISIBILITY_NONE
|
||||
)
|
||||
from xmodule.error_module import ErrorDescriptor
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.django_utils import (
|
||||
@@ -54,10 +53,9 @@ from xmodule.modulestore.tests.django_utils import (
|
||||
SharedModuleStoreTestCase
|
||||
)
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
from xmodule.modulestore.xml import CourseLocationManager
|
||||
from xmodule.partitions.partitions import MINIMUM_STATIC_PARTITION_ID, Group, UserPartition
|
||||
from xmodule.tests import get_test_system
|
||||
|
||||
QUERY_COUNT_TABLE_BLACKLIST = WAFFLE_TABLES
|
||||
|
||||
# pylint: disable=protected-access
|
||||
|
||||
@@ -847,5 +845,5 @@ class CourseOverviewAccessTestCase(ModuleStoreTestCase):
|
||||
num_queries = 0
|
||||
|
||||
course_overview = CourseOverview.get_from_id(course.id)
|
||||
with self.assertNumQueries(num_queries):
|
||||
with self.assertNumQueries(num_queries, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
|
||||
bool(access.has_access(user, action, course_overview, course_key=course.id))
|
||||
|
||||
@@ -11,6 +11,7 @@ from django.test.utils import override_settings
|
||||
from lms.djangoapps.ccx.tests.factories import CcxFactory
|
||||
from nose.plugins.attrib import attr
|
||||
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
|
||||
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
|
||||
from pyquery import PyQuery as pq
|
||||
from student.models import CourseEnrollment
|
||||
from student.tests.factories import AdminFactory
|
||||
@@ -27,6 +28,8 @@ from xmodule.modulestore.xml_importer import import_course_from_xml
|
||||
|
||||
from .helpers import LoginEnrollmentTestCase
|
||||
|
||||
QUERY_COUNT_TABLE_BLACKLIST = WAFFLE_TABLES
|
||||
|
||||
|
||||
@attr(shard=1)
|
||||
class CourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTestCase):
|
||||
@@ -378,14 +381,14 @@ class SelfPacedCourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTest
|
||||
and Mongo queries.
|
||||
"""
|
||||
url = reverse('info', args=[unicode(course.id)])
|
||||
with self.assertNumQueries(sql_queries):
|
||||
with self.assertNumQueries(sql_queries, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
|
||||
with check_mongo_calls(mongo_queries):
|
||||
with mock.patch("openedx.core.djangoapps.theming.helpers.get_current_site", return_value=None):
|
||||
resp = self.client.get(url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
def test_num_queries_instructor_paced(self):
|
||||
self.fetch_course_info_with_queries(self.instructor_paced_course, 26, 3)
|
||||
self.fetch_course_info_with_queries(self.instructor_paced_course, 22, 3)
|
||||
|
||||
def test_num_queries_self_paced(self):
|
||||
self.fetch_course_info_with_queries(self.self_paced_course, 26, 3)
|
||||
self.fetch_course_info_with_queries(self.self_paced_course, 22, 3)
|
||||
|
||||
@@ -20,7 +20,7 @@ from certificates.tests.factories import CertificateInvalidationFactory, Generat
|
||||
from commerce.models import CommerceConfiguration
|
||||
from course_modes.models import CourseMode
|
||||
from course_modes.tests.factories import CourseModeFactory
|
||||
from courseware.access_utils import is_course_open_for_learner
|
||||
from courseware.access_utils import check_course_open_for_learner
|
||||
from courseware.model_data import FieldDataCache, set_score
|
||||
from courseware.module_render import get_module
|
||||
from courseware.tests.factories import GlobalStaffFactory, StudentModuleFactory
|
||||
@@ -2567,9 +2567,10 @@ class AccessUtilsTestCase(ModuleStoreTestCase):
|
||||
(-1, True)
|
||||
)
|
||||
@ddt.unpack
|
||||
@patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
|
||||
def test_is_course_open_for_learner(self, start_date_modifier, expected_value):
|
||||
staff_user = AdminFactory()
|
||||
start_date = datetime.now(UTC) + timedelta(days=start_date_modifier)
|
||||
course = CourseFactory.create(start=start_date)
|
||||
|
||||
self.assertEqual(is_course_open_for_learner(staff_user, course), expected_value)
|
||||
self.assertEqual(bool(check_course_open_for_learner(staff_user, course)), expected_value)
|
||||
|
||||
@@ -39,7 +39,7 @@ from xmodule.modulestore.django import modulestore
|
||||
from xmodule.x_module import STUDENT_VIEW
|
||||
|
||||
from ..access import has_access
|
||||
from ..access_utils import in_preview_mode, is_course_open_for_learner
|
||||
from ..access_utils import in_preview_mode, check_course_open_for_learner
|
||||
from ..courses import get_course_with_access, get_current_child, get_studio_url
|
||||
from ..entrance_exams import (
|
||||
course_has_entrance_exam,
|
||||
@@ -372,7 +372,7 @@ class CoursewareIndex(View):
|
||||
self._add_entrance_exam_to_context(courseware_context)
|
||||
|
||||
# staff masquerading data
|
||||
if not is_course_open_for_learner(self.effective_user, self.course):
|
||||
if not check_course_open_for_learner(self.effective_user, self.course):
|
||||
# Disable student view button if user is staff and
|
||||
# course is not yet visible to students.
|
||||
courseware_context['disable_student_access'] = True
|
||||
|
||||
@@ -18,7 +18,7 @@ from commerce.utils import EcommerceService
|
||||
from course_modes.models import CourseMode
|
||||
from courseware.access import has_access, has_ccx_coach_role
|
||||
from courseware.access_response import StartDateError
|
||||
from courseware.access_utils import in_preview_mode, is_course_open_for_learner
|
||||
from courseware.access_utils import in_preview_mode, check_course_open_for_learner
|
||||
from courseware.courses import (
|
||||
can_self_enroll_in_course,
|
||||
get_course,
|
||||
@@ -350,8 +350,7 @@ def course_info(request, course_id):
|
||||
if SelfPacedConfiguration.current().enable_course_home_improvements:
|
||||
context['resume_course_url'] = get_last_accessed_courseware(course, request, user)
|
||||
|
||||
# LEARNER-981/LEARNER-837: Hide masquerade as necessary in Course Home (DONE)
|
||||
if not is_course_open_for_learner(user, course):
|
||||
if not check_course_open_for_learner(user, course):
|
||||
# Disable student view button if user is staff and
|
||||
# course is not yet visible to students.
|
||||
context['disable_student_access'] = True
|
||||
@@ -486,7 +485,7 @@ class CourseTabView(EdxFragmentView):
|
||||
else:
|
||||
masquerade = None
|
||||
|
||||
if course and not is_course_open_for_learner(request.user, course):
|
||||
if course and not check_course_open_for_learner(request.user, course):
|
||||
# Disable student view button if user is staff and
|
||||
# course is not yet visible to students.
|
||||
supports_preview_menu = False
|
||||
|
||||
@@ -4,19 +4,15 @@ from datetime import datetime
|
||||
|
||||
import ddt
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import HttpResponse, Http404
|
||||
from django.http import Http404
|
||||
from django.test.client import Client, RequestFactory
|
||||
from django.test.utils import override_settings
|
||||
from django.utils import translation
|
||||
from mock import ANY, Mock, call, patch
|
||||
from nose.tools import assert_true
|
||||
from rest_framework.test import APIRequestFactory
|
||||
|
||||
from common.test.utils import MockSignalHandlerMixin, disable_signal
|
||||
from course_modes.models import CourseMode
|
||||
from course_modes.tests.factories import CourseModeFactory
|
||||
from discussion_api import api
|
||||
from discussion_api.tests.utils import CommentsServiceMockMixin, make_minimal_cs_thread
|
||||
from django_comment_client.constants import TYPE_ENTRY, TYPE_SUBCATEGORY
|
||||
from django_comment_client.permissions import get_team
|
||||
from django_comment_client.tests.group_id import (
|
||||
@@ -24,7 +20,6 @@ from django_comment_client.tests.group_id import (
|
||||
GroupIdAssertionMixin,
|
||||
NonCohortedTopicGroupIdTestMixin
|
||||
)
|
||||
from django_comment_client.base.views import create_thread
|
||||
from django_comment_client.tests.unicode import UnicodeTestMixin
|
||||
from django_comment_client.tests.utils import (
|
||||
CohortedTestCase,
|
||||
@@ -37,7 +32,6 @@ from django_comment_common.models import (
|
||||
CourseDiscussionSettings,
|
||||
ForumsConfig,
|
||||
FORUM_ROLE_STUDENT,
|
||||
Role
|
||||
)
|
||||
from django_comment_common.utils import ThreadContext, seed_permissions_roles
|
||||
from lms.djangoapps.courseware.exceptions import CourseAccessRedirect
|
||||
@@ -50,9 +44,10 @@ from openedx.core.djangoapps.course_groups.models import CourseUserGroup
|
||||
from openedx.core.djangoapps.course_groups.tests.helpers import config_course_cohorts
|
||||
from openedx.core.djangoapps.course_groups.tests.test_views import CohortViewsTestCase
|
||||
from openedx.core.djangoapps.util.testing import ContentGroupTestCase
|
||||
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
|
||||
from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseTestConsentRequired
|
||||
from student.roles import CourseStaffRole, UserBasedRole
|
||||
from student.tests.factories import CourseAccessRoleFactory, CourseEnrollmentFactory, UserFactory
|
||||
from student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
from util.testing import EventTestMixin, UrlResetMixin
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import modulestore
|
||||
@@ -65,6 +60,8 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, chec
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
QUERY_COUNT_TABLE_BLACKLIST = WAFFLE_TABLES
|
||||
|
||||
# pylint: disable=missing-docstring
|
||||
|
||||
|
||||
@@ -467,7 +464,7 @@ class SingleThreadQueryCountTestCase(ForumsEnableMixin, ModuleStoreTestCase):
|
||||
[num_cached_mongo_calls, num_cached_sql_queries],
|
||||
]
|
||||
for expected_mongo_calls, expected_sql_queries in cached_calls:
|
||||
with self.assertNumQueries(expected_sql_queries):
|
||||
with self.assertNumQueries(expected_sql_queries, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
|
||||
with check_mongo_calls(expected_mongo_calls):
|
||||
call_single_thread()
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ from lms.djangoapps.teams.tests.factories import CourseTeamFactory, CourseTeamMe
|
||||
from lms.lib.comment_client import Thread
|
||||
from openedx.core.djangoapps.course_groups.cohorts import set_course_cohorted
|
||||
from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory
|
||||
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
|
||||
from student.roles import CourseStaffRole, UserBasedRole
|
||||
from student.tests.factories import CourseAccessRoleFactory, CourseEnrollmentFactory, UserFactory
|
||||
from util.testing import UrlResetMixin
|
||||
@@ -60,6 +61,8 @@ log = logging.getLogger(__name__)
|
||||
|
||||
CS_PREFIX = "http://localhost:4567/api/v1"
|
||||
|
||||
QUERY_COUNT_TABLE_BLACKLIST = WAFFLE_TABLES
|
||||
|
||||
# pylint: disable=missing-docstring
|
||||
|
||||
|
||||
@@ -395,7 +398,7 @@ class ViewsQueryCountTestCase(
|
||||
with modulestore().default_store(default_store):
|
||||
self.set_up_course(module_count=module_count)
|
||||
self.clear_caches()
|
||||
with self.assertNumQueries(sql_queries):
|
||||
with self.assertNumQueries(sql_queries, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
|
||||
with check_mongo_calls(mongo_calls):
|
||||
func(self, *args, **kwargs)
|
||||
return inner
|
||||
|
||||
@@ -247,6 +247,13 @@ class WaffleFlag(object):
|
||||
self.flag_name = flag_name
|
||||
self.flag_undefined_default = flag_undefined_default
|
||||
|
||||
@property
|
||||
def namespaced_flag_name(self):
|
||||
"""
|
||||
Returns the fully namespaced flag name.
|
||||
"""
|
||||
return self.waffle_namespace._namespaced_name(self.flag_name)
|
||||
|
||||
def is_enabled(self):
|
||||
"""
|
||||
Returns whether or not the flag is enabled.
|
||||
|
||||
@@ -18,6 +18,9 @@ UNIFIED_COURSE_TAB_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'unified_cours
|
||||
# Waffle flag to enable the sock on the footer of the home and courseware pages
|
||||
DISPLAY_COURSE_SOCK_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'display_course_sock')
|
||||
|
||||
# Waffle flag to let learners access a course before its start date
|
||||
COURSE_PRE_START_ACCESS_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'pre_start_access')
|
||||
|
||||
# Waffle flag to enable a review page link from the unified home page
|
||||
SHOW_REVIEWS_TOOL_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'show_reviews_tool')
|
||||
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
"""
|
||||
Tests for the course home page.
|
||||
"""
|
||||
import datetime
|
||||
import ddt
|
||||
import mock
|
||||
import pytz
|
||||
from waffle.testutils import override_flag
|
||||
|
||||
from courseware.tests.factories import StaffFactory
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
@@ -18,6 +22,7 @@ from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.tests.django_utils import CourseUserType, SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls
|
||||
|
||||
from ... import COURSE_PRE_START_ACCESS_FLAG
|
||||
from .helpers import add_course_mode
|
||||
from .test_course_updates import create_course_update
|
||||
|
||||
@@ -142,6 +147,26 @@ class TestCourseHomePage(CourseHomePageTestCase):
|
||||
url = course_home_url(self.course)
|
||||
self.client.get(url)
|
||||
|
||||
@mock.patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
|
||||
def test_start_date_handling(self):
|
||||
"""
|
||||
Verify that the course home page handles start dates correctly.
|
||||
"""
|
||||
now = datetime.datetime.now(pytz.UTC)
|
||||
tomorrow = now + datetime.timedelta(days=1)
|
||||
self.course.start = tomorrow
|
||||
|
||||
# The course home page should 404 for a course starting in the future
|
||||
url = course_home_url(self.course)
|
||||
response = self.client.get(url)
|
||||
self.assertRedirects(response, '/dashboard?notlive=Jan+01%2C+2030')
|
||||
|
||||
# With the Waffle flag enabled, the course should be visible
|
||||
with override_flag(COURSE_PRE_START_ACCESS_FLAG.namespaced_flag_name, True):
|
||||
url = course_home_url(self.course)
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestCourseHomePageAccess(CourseHomePageTestCase):
|
||||
|
||||
@@ -71,9 +71,9 @@ def get_course_outline_block_tree(request, course_id):
|
||||
block_types_filter=['course', 'chapter', 'sequential']
|
||||
)
|
||||
|
||||
course_outline_root_block = all_blocks['blocks'][all_blocks['root']]
|
||||
populate_children(course_outline_root_block, all_blocks['blocks'])
|
||||
set_last_accessed_default(course_outline_root_block)
|
||||
mark_last_accessed(request.user, course_key, course_outline_root_block)
|
||||
|
||||
course_outline_root_block = all_blocks['blocks'].get(all_blocks['root'], None)
|
||||
if course_outline_root_block:
|
||||
populate_children(course_outline_root_block, all_blocks['blocks'])
|
||||
set_last_accessed_default(course_outline_root_block)
|
||||
mark_last_accessed(request.user, course_key, course_outline_root_block)
|
||||
return course_outline_root_block
|
||||
|
||||
@@ -83,14 +83,14 @@ class CourseHomeFragmentView(EdxFragmentView):
|
||||
return block
|
||||
|
||||
course_outline_root_block = get_course_outline_block_tree(request, course_id)
|
||||
last_accessed_block = get_last_accessed_block(course_outline_root_block)
|
||||
last_accessed_block = get_last_accessed_block(course_outline_root_block) if course_outline_root_block else None
|
||||
has_visited_course = bool(last_accessed_block)
|
||||
if last_accessed_block:
|
||||
resume_course_url = last_accessed_block['lms_web_url']
|
||||
else:
|
||||
resume_course_url = course_outline_root_block['lms_web_url']
|
||||
resume_course_url = course_outline_root_block['lms_web_url'] if course_outline_root_block else None
|
||||
|
||||
return (has_visited_course, resume_course_url)
|
||||
return has_visited_course, resume_course_url
|
||||
|
||||
def _get_course_handouts(self, request, course):
|
||||
"""
|
||||
@@ -141,9 +141,6 @@ class CourseHomeFragmentView(EdxFragmentView):
|
||||
# Get the course tools enabled for this user and course
|
||||
course_tools = CourseToolsPluginManager.get_enabled_course_tools(request, course_key)
|
||||
|
||||
# Get the course tools enabled for this user and course
|
||||
course_tools = CourseToolsPluginManager.get_enabled_course_tools(request, course_key)
|
||||
|
||||
# Render the course home fragment
|
||||
context = {
|
||||
'request': request,
|
||||
|
||||
@@ -25,6 +25,8 @@ class CourseOutlineFragmentView(EdxFragmentView):
|
||||
course_overview = get_course_overview_with_access(request.user, 'load', course_key, check_if_enrolled=True)
|
||||
|
||||
course_block_tree = get_course_outline_block_tree(request, course_id)
|
||||
if not course_block_tree:
|
||||
return None
|
||||
|
||||
context = {
|
||||
'csrf': csrf(request)['csrf_token'],
|
||||
|
||||
Reference in New Issue
Block a user