MA-722 Render xBlock API Support
This commit is contained in:
22
common/djangoapps/util/url.py
Normal file
22
common/djangoapps/util/url.py
Normal file
@@ -0,0 +1,22 @@
|
||||
"""
|
||||
Utility functions related to urls.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import set_urlconf
|
||||
from django.utils.importlib import import_module
|
||||
|
||||
|
||||
def reload_django_url_config():
|
||||
"""
|
||||
Reloads Django's URL config.
|
||||
This is useful, for example, when a test enables new URLs
|
||||
with a django setting and the URL config needs to be refreshed.
|
||||
"""
|
||||
urlconf = settings.ROOT_URLCONF
|
||||
if urlconf and urlconf in sys.modules:
|
||||
reload(sys.modules[urlconf])
|
||||
reloaded = import_module(urlconf)
|
||||
reloaded_urls = getattr(reloaded, 'urlpatterns')
|
||||
set_urlconf(tuple(reloaded_urls))
|
||||
@@ -1,7 +1,14 @@
|
||||
"""
|
||||
This file contains (or should), all access control logic for the courseware.
|
||||
Ideally, it will be the only place that needs to know about any special settings
|
||||
like DISABLE_START_DATES
|
||||
like DISABLE_START_DATES.
|
||||
|
||||
Note: The access control logic in this file does NOT check for enrollment in
|
||||
a course. It is expected that higher layers check for enrollment so we
|
||||
don't have to hit the enrollments table on every module load.
|
||||
|
||||
If enrollment is to be checked, use get_course_with_access in courseware.courses.
|
||||
It is a wrapper around has_access that additionally checks for enrollment.
|
||||
"""
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
@@ -27,7 +34,7 @@ from xmodule.util.django import get_current_request_hostname
|
||||
from external_auth.models import ExternalAuthMap
|
||||
from courseware.masquerade import get_masquerade_role, is_masquerading_as_student
|
||||
from student import auth
|
||||
from student.models import CourseEnrollment, CourseEnrollmentAllowed
|
||||
from student.models import CourseEnrollmentAllowed
|
||||
from student.roles import (
|
||||
GlobalStaff, CourseStaffRole, CourseInstructorRole,
|
||||
OrgStaffRole, OrgInstructorRole, CourseBetaTesterRole
|
||||
@@ -140,18 +147,6 @@ def _has_access_course_desc(user, action, course):
|
||||
# delegate to generic descriptor check to check start dates
|
||||
return _has_access_descriptor(user, 'load', course, course.id)
|
||||
|
||||
def can_load_forum():
|
||||
"""
|
||||
Can this user access the forums in this course?
|
||||
"""
|
||||
return (
|
||||
can_load() and
|
||||
(
|
||||
CourseEnrollment.is_enrolled(user, course.id) or
|
||||
_has_staff_access_to_descriptor(user, course, course.id)
|
||||
)
|
||||
)
|
||||
|
||||
def can_load_mobile():
|
||||
"""
|
||||
Can this user access this course from a mobile device?
|
||||
@@ -164,12 +159,8 @@ def _has_access_course_desc(user, action, course):
|
||||
(
|
||||
# either is a staff user or
|
||||
_has_staff_access_to_descriptor(user, course, course.id) or
|
||||
(
|
||||
# check enrollment
|
||||
CourseEnrollment.is_enrolled(user, course.id) and
|
||||
# check for unfulfilled milestones
|
||||
not any_unfulfilled_milestones(course.id, user.id)
|
||||
)
|
||||
# check for unfulfilled milestones
|
||||
not any_unfulfilled_milestones(course.id, user.id)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -294,7 +285,6 @@ def _has_access_course_desc(user, action, course):
|
||||
checkers = {
|
||||
'load': can_load,
|
||||
'view_courseware_with_prerequisites': can_view_courseware_with_prerequisites,
|
||||
'load_forum': can_load_forum,
|
||||
'load_mobile': can_load_mobile,
|
||||
'enroll': can_enroll,
|
||||
'see_exists': see_exists,
|
||||
|
||||
@@ -92,33 +92,27 @@ def get_course_with_access(user, action, course_key, depth=0, check_if_enrolled=
|
||||
Raises a 404 if the course_key is invalid, or the user doesn't have access.
|
||||
|
||||
depth: The number of levels of children for the modulestore to cache. None means infinite depth
|
||||
|
||||
check_if_enrolled: If true, additionally verifies that the user is either enrolled in the course
|
||||
or has staff access.
|
||||
"""
|
||||
assert isinstance(course_key, CourseKey)
|
||||
course = get_course_by_id(course_key, depth=depth)
|
||||
|
||||
if not has_access(user, action, course, course_key):
|
||||
if check_if_enrolled and not CourseEnrollment.is_enrolled(user, course_key):
|
||||
# If user is not enrolled, raise UserNotEnrolled exception that will
|
||||
# be caught by middleware
|
||||
raise UserNotEnrolled(course_key)
|
||||
|
||||
# Deliberately return a non-specific error message to avoid
|
||||
# leaking info about access control settings
|
||||
raise Http404("Course not found.")
|
||||
|
||||
if check_if_enrolled:
|
||||
# Verify that the user is either enrolled in the course or a staff member.
|
||||
# If user is not enrolled, raise UserNotEnrolled exception that will be caught by middleware.
|
||||
if not ((user.id and CourseEnrollment.is_enrolled(user, course_key)) or has_access(user, 'staff', course)):
|
||||
raise UserNotEnrolled(course_key)
|
||||
|
||||
return course
|
||||
|
||||
|
||||
def get_opt_course_with_access(user, action, course_key):
|
||||
"""
|
||||
Same as get_course_with_access, except that if course_key is None,
|
||||
return None without performing any access checks.
|
||||
"""
|
||||
if course_key is None:
|
||||
return None
|
||||
return get_course_with_access(user, action, course_key)
|
||||
|
||||
|
||||
def course_image_url(course):
|
||||
"""Try to look up the image url for the course. If it's not found,
|
||||
log an error and return the dead link"""
|
||||
|
||||
@@ -8,13 +8,11 @@ import logging
|
||||
import mimetypes
|
||||
|
||||
import static_replace
|
||||
import xblock.reference.plugins
|
||||
|
||||
from collections import OrderedDict
|
||||
from functools import partial
|
||||
from requests.auth import HTTPBasicAuth
|
||||
import dogstats_wrapper as dog_stats_api
|
||||
from opaque_keys import InvalidKeyError
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
@@ -37,42 +35,42 @@ from courseware.entrance_exams import (
|
||||
get_entrance_exam_score,
|
||||
user_must_complete_entrance_exam
|
||||
)
|
||||
from edxmako.shortcuts import render_to_string
|
||||
from eventtracking import tracker
|
||||
from lms.djangoapps.lms_xblock.field_data import LmsFieldData
|
||||
from lms.djangoapps.lms_xblock.runtime import LmsModuleSystem, unquote_slashes, quote_slashes
|
||||
from lms.djangoapps.lms_xblock.models import XBlockAsidesConfig
|
||||
from edxmako.shortcuts import render_to_string
|
||||
from eventtracking import tracker
|
||||
from psychometrics.psychoanalyze import make_psychometrics_data_update_handler
|
||||
from student.models import anonymous_id_for_user, user_by_anonymous_id
|
||||
from student.roles import CourseBetaTesterRole
|
||||
from xblock.core import XBlock
|
||||
from xblock.fields import Scope
|
||||
from xblock.runtime import KvsFieldData, KeyValueStore
|
||||
from xblock.exceptions import NoSuchHandlerError, NoSuchViewError
|
||||
from xblock.django.request import django_to_webob_request, webob_to_django_response
|
||||
from xmodule.error_module import ErrorDescriptor, NonStaffErrorDescriptor
|
||||
from xmodule.exceptions import NotFoundError, ProcessingError
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.keys import UsageKey, CourseKey
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.modulestore.django import modulestore, ModuleI18nService
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from openedx.core.lib.xblock_utils import (
|
||||
replace_course_urls,
|
||||
replace_jump_to_id_urls,
|
||||
replace_static_urls,
|
||||
add_staff_markup,
|
||||
wrap_xblock,
|
||||
request_token
|
||||
request_token as xblock_request_token,
|
||||
)
|
||||
from psychometrics.psychoanalyze import make_psychometrics_data_update_handler
|
||||
from student.models import anonymous_id_for_user, user_by_anonymous_id
|
||||
from student.roles import CourseBetaTesterRole
|
||||
from xblock.core import XBlock
|
||||
from xblock.django.request import django_to_webob_request, webob_to_django_response
|
||||
from xblock_django.user_service import DjangoXBlockUserService
|
||||
from xblock.exceptions import NoSuchHandlerError, NoSuchViewError
|
||||
from xblock.reference.plugins import FSService
|
||||
from xblock.runtime import KvsFieldData
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.error_module import ErrorDescriptor, NonStaffErrorDescriptor
|
||||
from xmodule.exceptions import NotFoundError, ProcessingError
|
||||
from xmodule.modulestore.django import modulestore, ModuleI18nService
|
||||
from xmodule.lti_module import LTIModule
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.x_module import XModuleDescriptor
|
||||
from xmodule.mixin import wrap_with_license
|
||||
from xblock_django.user_service import DjangoXBlockUserService
|
||||
from util.json_request import JsonResponse
|
||||
from util.sandboxing import can_execute_unsafe_code, get_python_lib_zip
|
||||
from util import milestones_helpers
|
||||
from util.module_utils import yield_dynamic_descriptor_descendents
|
||||
from verify_student.services import ReverificationService
|
||||
|
||||
from .field_overrides import OverrideFieldData
|
||||
@@ -255,10 +253,12 @@ def get_xqueue_callback_url_prefix(request):
|
||||
|
||||
def get_module_for_descriptor(user, request, descriptor, field_data_cache, course_key,
|
||||
position=None, wrap_xmodule_display=True, grade_bucket_type=None,
|
||||
static_asset_path=''):
|
||||
static_asset_path='', disable_staff_debug_info=False):
|
||||
"""
|
||||
Implements get_module, extracting out the request-specific functionality.
|
||||
|
||||
disable_staff_debug_info : If this is True, exclude staff debug information in the rendering of the module.
|
||||
|
||||
See get_module() docstring for further details.
|
||||
"""
|
||||
track_function = make_track_function(request)
|
||||
@@ -278,15 +278,16 @@ def get_module_for_descriptor(user, request, descriptor, field_data_cache, cours
|
||||
grade_bucket_type=grade_bucket_type,
|
||||
static_asset_path=static_asset_path,
|
||||
user_location=user_location,
|
||||
request_token=request_token(request),
|
||||
request_token=xblock_request_token(request),
|
||||
disable_staff_debug_info=disable_staff_debug_info,
|
||||
)
|
||||
|
||||
|
||||
def get_module_system_for_user(user, field_data_cache,
|
||||
def get_module_system_for_user(user, field_data_cache, # TODO # pylint: disable=too-many-statements
|
||||
# Arguments preceding this comment have user binding, those following don't
|
||||
descriptor, course_id, track_function, xqueue_callback_url_prefix,
|
||||
request_token, position=None, wrap_xmodule_display=True, grade_bucket_type=None,
|
||||
static_asset_path='', user_location=None):
|
||||
static_asset_path='', user_location=None, disable_staff_debug_info=False):
|
||||
"""
|
||||
Helper function that returns a module system and student_data bound to a user and a descriptor.
|
||||
|
||||
@@ -309,7 +310,9 @@ def get_module_system_for_user(user, field_data_cache,
|
||||
student_data = KvsFieldData(DjangoKeyValueStore(field_data_cache))
|
||||
|
||||
def make_xqueue_callback(dispatch='score_update'):
|
||||
# Fully qualified callback URL for external queueing system
|
||||
"""
|
||||
Returns fully qualified callback URL for external queueing system
|
||||
"""
|
||||
relative_xqueue_callback_url = reverse(
|
||||
'xqueue_callback',
|
||||
kwargs=dict(
|
||||
@@ -573,7 +576,7 @@ def get_module_system_for_user(user, field_data_cache,
|
||||
if settings.FEATURES.get('DISPLAY_DEBUG_INFO_TO_STAFF'):
|
||||
if has_access(user, 'staff', descriptor, course_id):
|
||||
has_instructor_access = has_access(user, 'instructor', descriptor, course_id)
|
||||
block_wrappers.append(partial(add_staff_markup, user, has_instructor_access))
|
||||
block_wrappers.append(partial(add_staff_markup, user, has_instructor_access, disable_staff_debug_info))
|
||||
|
||||
# These modules store data using the anonymous_student_id as a key.
|
||||
# To prevent loss of data, we will continue to provide old modules with
|
||||
@@ -637,7 +640,7 @@ def get_module_system_for_user(user, field_data_cache,
|
||||
get_real_user=user_by_anonymous_id,
|
||||
services={
|
||||
'i18n': ModuleI18nService(),
|
||||
'fs': xblock.reference.plugins.FSService(),
|
||||
'fs': FSService(),
|
||||
'field-data': field_data,
|
||||
'user': DjangoXBlockUserService(user, user_is_staff=user_is_staff),
|
||||
"reverification": ReverificationService()
|
||||
@@ -681,7 +684,7 @@ def get_module_system_for_user(user, field_data_cache,
|
||||
def get_module_for_descriptor_internal(user, descriptor, field_data_cache, course_id, # pylint: disable=invalid-name
|
||||
track_function, xqueue_callback_url_prefix, request_token,
|
||||
position=None, wrap_xmodule_display=True, grade_bucket_type=None,
|
||||
static_asset_path='', user_location=None):
|
||||
static_asset_path='', user_location=None, disable_staff_debug_info=False):
|
||||
"""
|
||||
Actually implement get_module, without requiring a request.
|
||||
|
||||
@@ -703,7 +706,8 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours
|
||||
grade_bucket_type=grade_bucket_type,
|
||||
static_asset_path=static_asset_path,
|
||||
user_location=user_location,
|
||||
request_token=request_token
|
||||
request_token=request_token,
|
||||
disable_staff_debug_info=disable_staff_debug_info,
|
||||
)
|
||||
|
||||
descriptor.bind_for_student(
|
||||
@@ -836,7 +840,7 @@ def xblock_resource(request, block_type, uri): # pylint: disable=unused-argumen
|
||||
return HttpResponse(content, mimetype=mimetype)
|
||||
|
||||
|
||||
def get_module_by_usage_id(request, course_id, usage_id):
|
||||
def get_module_by_usage_id(request, course_id, usage_id, disable_staff_debug_info=False):
|
||||
"""
|
||||
Gets a module instance based on its `usage_id` in a course, for a given request/user
|
||||
|
||||
@@ -880,7 +884,14 @@ def get_module_by_usage_id(request, course_id, usage_id):
|
||||
descriptor
|
||||
)
|
||||
setup_masquerade(request, course_id, has_access(user, 'staff', descriptor, course_id))
|
||||
instance = get_module(user, request, usage_key, field_data_cache, grade_bucket_type='ajax')
|
||||
instance = get_module_for_descriptor(
|
||||
user,
|
||||
request,
|
||||
descriptor,
|
||||
field_data_cache,
|
||||
usage_key.course_key,
|
||||
disable_staff_debug_info=disable_staff_debug_info
|
||||
)
|
||||
if instance is None:
|
||||
# Either permissions just changed, or someone is trying to be clever
|
||||
# and load something they shouldn't have access to.
|
||||
|
||||
@@ -29,12 +29,14 @@ from certificates import api as certs_api
|
||||
from certificates.models import CertificateStatuses, CertificateGenerationConfiguration
|
||||
from certificates.tests.factories import GeneratedCertificateFactory
|
||||
from course_modes.models import CourseMode
|
||||
from courseware.testutils import RenderXBlockTestMixin
|
||||
from courseware.tests.factories import StudentModuleFactory
|
||||
from edxmako.middleware import MakoMiddleware
|
||||
from edxmako.tests import mako_middleware_process_request
|
||||
from student.models import CourseEnrollment
|
||||
from student.tests.factories import AdminFactory, UserFactory, CourseEnrollmentFactory
|
||||
from util.tests.test_date_utils import fake_ugettext, fake_pgettext
|
||||
from util.url import reload_django_url_config
|
||||
from util.views import ensure_valid_course_key
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import modulestore
|
||||
@@ -584,6 +586,7 @@ class BaseDueDateTests(ModuleStoreTestCase):
|
||||
|
||||
course = modulestore().get_course(course.id) # pylint: disable=no-member
|
||||
self.assertIsNotNone(course.get_children()[0].get_children()[0].due)
|
||||
CourseEnrollmentFactory(user=self.user, course_id=course.id)
|
||||
return course
|
||||
|
||||
def setUp(self):
|
||||
@@ -752,6 +755,7 @@ class ProgressPageTests(ModuleStoreTestCase):
|
||||
grade_cutoffs={u'çü†øƒƒ': 0.75, 'Pass': 0.5},
|
||||
)
|
||||
self.course = modulestore().get_course(course.id) # pylint: disable=no-member
|
||||
CourseEnrollmentFactory(user=self.user, course_id=self.course.id)
|
||||
|
||||
self.chapter = ItemFactory.create(category='chapter', parent_location=self.course.location) # pylint: disable=no-member
|
||||
self.section = ItemFactory.create(category='sequential', parent_location=self.chapter.location)
|
||||
@@ -1087,3 +1091,22 @@ class TestIndexView(ModuleStoreTestCase):
|
||||
# Trigger the assertions embedded in the ViewCheckerBlocks
|
||||
response = views.index(request, unicode(course.id), chapter=chapter.url_name, section=section.url_name)
|
||||
self.assertEquals(response.content.count("ViewCheckerPassed"), 3)
|
||||
|
||||
|
||||
class TestRenderXBlock(RenderXBlockTestMixin, ModuleStoreTestCase):
|
||||
"""
|
||||
Tests for the courseware.render_xblock endpoint.
|
||||
This class overrides the get_response method, which is used by
|
||||
the tests defined in RenderXBlockTestMixin.
|
||||
"""
|
||||
@patch.dict('django.conf.settings.FEATURES', {'ENABLE_RENDER_XBLOCK_API': True})
|
||||
def setUp(self):
|
||||
reload_django_url_config()
|
||||
super(TestRenderXBlock, self).setUp()
|
||||
|
||||
def get_response(self):
|
||||
"""
|
||||
Overridable method to get the response from the endpoint that is being tested.
|
||||
"""
|
||||
url = reverse('render_xblock', kwargs={"usage_key_string": unicode(self.html_block.location)})
|
||||
return self.client.get(url)
|
||||
|
||||
176
lms/djangoapps/courseware/testutils.py
Normal file
176
lms/djangoapps/courseware/testutils.py
Normal file
@@ -0,0 +1,176 @@
|
||||
"""
|
||||
Common test utilities for courseware functionality
|
||||
"""
|
||||
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from datetime import datetime
|
||||
import ddt
|
||||
from mock import patch
|
||||
|
||||
from lms.djangoapps.courseware.url_helpers import get_redirect_url
|
||||
from student.tests.factories import AdminFactory, UserFactory, CourseEnrollmentFactory
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class RenderXBlockTestMixin(object):
|
||||
"""
|
||||
Mixin for testing the courseware.render_xblock function.
|
||||
It can be used for testing any higher-level endpoint that calls this method.
|
||||
"""
|
||||
__metaclass__ = ABCMeta
|
||||
|
||||
# DOM elements that appear in the LMS Courseware,
|
||||
# but are excluded from the xBlock-only rendering.
|
||||
COURSEWARE_CHROME_HTML_ELEMENTS = [
|
||||
'<header id="open_close_accordion"',
|
||||
'<ol class="course-tabs"',
|
||||
'<footer id="footer-openedx"',
|
||||
'<div class="window-wrap"',
|
||||
'<div class="preview-menu"',
|
||||
]
|
||||
|
||||
# DOM elements that appear in an xBlock,
|
||||
# but are excluded from the xBlock-only rendering.
|
||||
XBLOCK_REMOVED_HTML_ELEMENTS = [
|
||||
'<div class="wrap-instructor-info"',
|
||||
]
|
||||
|
||||
@abstractmethod
|
||||
def get_response(self):
|
||||
"""
|
||||
Abstract method to get the response from the endpoint that is being tested.
|
||||
"""
|
||||
pass # pragma: no cover
|
||||
|
||||
def login(self):
|
||||
"""
|
||||
Logs in the test user.
|
||||
"""
|
||||
self.client.login(username=self.user.username, password='test')
|
||||
|
||||
def setup_course(self, default_store=None):
|
||||
"""
|
||||
Helper method to create the course.
|
||||
"""
|
||||
if not default_store:
|
||||
default_store = self.store.default_modulestore.get_modulestore_type()
|
||||
with self.store.default_store(default_store):
|
||||
self.course = CourseFactory.create() # pylint: disable=attribute-defined-outside-init
|
||||
chapter = ItemFactory.create(parent=self.course, category='chapter')
|
||||
self.html_block = ItemFactory.create( # pylint: disable=attribute-defined-outside-init
|
||||
parent=chapter,
|
||||
category='html',
|
||||
data="<p>Test HTML Content<p>"
|
||||
)
|
||||
|
||||
def setup_user(self, admin=False, enroll=False, login=False):
|
||||
"""
|
||||
Helper method to create the user.
|
||||
"""
|
||||
self.user = AdminFactory() if admin else UserFactory() # pylint: disable=attribute-defined-outside-init
|
||||
|
||||
if enroll:
|
||||
CourseEnrollmentFactory(user=self.user, course_id=self.course.id)
|
||||
|
||||
if login:
|
||||
self.login()
|
||||
|
||||
def verify_response(self, expected_response_code=200):
|
||||
"""
|
||||
Helper method that calls the endpoint, verifies the expected response code, and returns the response.
|
||||
"""
|
||||
response = self.get_response()
|
||||
if expected_response_code == 200:
|
||||
self.assertContains(response, self.html_block.data, status_code=expected_response_code)
|
||||
for chrome_element in [self.COURSEWARE_CHROME_HTML_ELEMENTS + self.XBLOCK_REMOVED_HTML_ELEMENTS]:
|
||||
self.assertNotContains(response, chrome_element)
|
||||
else:
|
||||
self.assertNotContains(response, self.html_block.data, status_code=expected_response_code)
|
||||
return response
|
||||
|
||||
@ddt.data(
|
||||
(ModuleStoreEnum.Type.mongo, 8),
|
||||
(ModuleStoreEnum.Type.split, 5),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_courseware_html(self, default_store, mongo_calls):
|
||||
"""
|
||||
To verify that the removal of courseware chrome elements is working,
|
||||
we include this test here to make sure the chrome elements that should
|
||||
be removed actually exist in the full courseware page.
|
||||
If this test fails, it's probably because the HTML template for courseware
|
||||
has changed and COURSEWARE_CHROME_HTML_ELEMENTS needs to be updated.
|
||||
"""
|
||||
with self.store.default_store(default_store):
|
||||
self.setup_course(default_store)
|
||||
self.setup_user(admin=True, enroll=True, login=True)
|
||||
|
||||
with check_mongo_calls(mongo_calls):
|
||||
url = get_redirect_url(self.course.id, self.html_block.location)
|
||||
response = self.client.get(url)
|
||||
for chrome_element in self.COURSEWARE_CHROME_HTML_ELEMENTS:
|
||||
self.assertContains(response, chrome_element)
|
||||
|
||||
@ddt.data(
|
||||
(ModuleStoreEnum.Type.mongo, 5),
|
||||
(ModuleStoreEnum.Type.split, 5),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_success_enrolled_staff(self, default_store, mongo_calls):
|
||||
with self.store.default_store(default_store):
|
||||
self.setup_course(default_store)
|
||||
self.setup_user(admin=True, enroll=True, login=True)
|
||||
|
||||
# The 5 mongoDB calls include calls for
|
||||
# Old Mongo:
|
||||
# (1) fill_in_run
|
||||
# (2) get_course in get_course_with_access
|
||||
# (3) get_item for HTML block in get_module_by_usage_id
|
||||
# (4) get_parent when loading HTML block
|
||||
# (5) edx_notes descriptor call to get_course
|
||||
# Split:
|
||||
# (1) course_index - bulk_operation call
|
||||
# (2) structure - get_course_with_access
|
||||
# (3) definition - get_course_with_access
|
||||
# (4) definition - HTML block
|
||||
# (5) definition - edx_notes decorator (original_get_html)
|
||||
with check_mongo_calls(mongo_calls):
|
||||
self.verify_response()
|
||||
|
||||
def test_success_unenrolled_staff(self):
|
||||
self.setup_course()
|
||||
self.setup_user(admin=True, enroll=False, login=True)
|
||||
self.verify_response()
|
||||
|
||||
def test_success_enrolled_student(self):
|
||||
self.setup_course()
|
||||
self.setup_user(admin=False, enroll=True, login=True)
|
||||
self.verify_response()
|
||||
|
||||
def test_fail_unauthenticated(self):
|
||||
self.setup_course()
|
||||
self.setup_user(admin=False, enroll=True, login=False)
|
||||
self.verify_response(expected_response_code=302)
|
||||
|
||||
def test_fail_unenrolled_student(self):
|
||||
self.setup_course()
|
||||
self.setup_user(admin=False, enroll=False, login=True)
|
||||
self.verify_response(expected_response_code=302)
|
||||
|
||||
@patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
|
||||
def test_fail_block_unreleased(self):
|
||||
self.setup_course()
|
||||
self.setup_user(admin=False, enroll=True, login=True)
|
||||
self.html_block.start = datetime.max
|
||||
modulestore().update_item(self.html_block, self.user.id) # pylint: disable=no-member
|
||||
self.verify_response(expected_response_code=404)
|
||||
|
||||
def test_fail_block_nonvisible(self):
|
||||
self.setup_course()
|
||||
self.setup_user(admin=False, enroll=True, login=True)
|
||||
self.html_block.visible_to_staff_only = True
|
||||
modulestore().update_item(self.html_block, self.user.id) # pylint: disable=no-member
|
||||
self.verify_response(expected_response_code=404)
|
||||
@@ -19,7 +19,7 @@ from django.core.urlresolvers import reverse
|
||||
from django.contrib.auth.models import User, AnonymousUser
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.utils.timezone import UTC
|
||||
from django.views.decorators.http import require_GET, require_POST
|
||||
from django.views.decorators.http import require_GET, require_POST, require_http_methods
|
||||
from django.http import Http404, HttpResponse, HttpResponseBadRequest
|
||||
from django.shortcuts import redirect
|
||||
from certificates import api as certs_api
|
||||
@@ -39,7 +39,7 @@ from courseware.courses import (
|
||||
)
|
||||
from courseware.masquerade import setup_masquerade
|
||||
from courseware.model_data import FieldDataCache
|
||||
from .module_render import toc_for_course, get_module_for_descriptor, get_module
|
||||
from .module_render import toc_for_course, get_module_for_descriptor, get_module, get_module_by_usage_id
|
||||
from .entrance_exams import (
|
||||
course_has_entrance_exam,
|
||||
get_entrance_exam_content,
|
||||
@@ -1344,7 +1344,8 @@ def generate_user_cert(request, course_id):
|
||||
|
||||
|
||||
def _track_successful_certificate_generation(user_id, course_id): # pylint: disable=invalid-name
|
||||
"""Track an successfully certificate generation event.
|
||||
"""
|
||||
Track a successful certificate generation event.
|
||||
|
||||
Arguments:
|
||||
user_id (str): The ID of the user generting the certificate.
|
||||
@@ -1370,3 +1371,36 @@ def _track_successful_certificate_generation(user_id, course_id): # pylint: dis
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def render_xblock(request, usage_key_string):
|
||||
"""
|
||||
Returns an HttpResponse with HTML content for the xBlock with the given usage_key.
|
||||
The returned HTML is a chromeless rendering of the xBlock (excluding content of the containing courseware).
|
||||
"""
|
||||
usage_key = UsageKey.from_string(usage_key_string)
|
||||
usage_key = usage_key.replace(course_key=modulestore().fill_in_run(usage_key.course_key))
|
||||
course_key = usage_key.course_key
|
||||
|
||||
with modulestore().bulk_operations(course_key):
|
||||
# verify the user has access to the course, including enrollment check
|
||||
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
|
||||
|
||||
# get the block, which verifies whether the user has access to the block.
|
||||
block, _ = get_module_by_usage_id(
|
||||
request, unicode(course_key), unicode(usage_key), disable_staff_debug_info=True
|
||||
)
|
||||
|
||||
context = {
|
||||
'fragment': block.render('student_view', context=request.GET),
|
||||
'course': course,
|
||||
'disable_accordion': True,
|
||||
'allow_iframing': True,
|
||||
'disable_header': True,
|
||||
'disable_window_wrap': True,
|
||||
'disable_preview_menu': True,
|
||||
'staff_access': has_access(request.user, 'staff', course),
|
||||
'xqa_server': settings.FEATURES.get('XQA_SERVER', 'http://your_xqa_server.com'),
|
||||
}
|
||||
return render_to_response('courseware/courseware-chromeless.html', context)
|
||||
|
||||
@@ -37,7 +37,7 @@ def _get_course_or_404(course_key, user):
|
||||
the user cannot access forums for the course, or the discussion tab is
|
||||
disabled for the course.
|
||||
"""
|
||||
course = get_course_with_access(user, 'load_forum', course_key)
|
||||
course = get_course_with_access(user, 'load', course_key, check_if_enrolled=True)
|
||||
if not any([tab.type == 'discussion' for tab in course.tabs]):
|
||||
raise Http404
|
||||
return course
|
||||
|
||||
@@ -720,7 +720,7 @@ def users(request, course_id):
|
||||
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
try:
|
||||
course = get_course_with_access(request.user, 'load_forum', course_key)
|
||||
get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
|
||||
except Http404:
|
||||
# course didn't exist, or requesting user does not have access to it.
|
||||
return JsonError(status=404)
|
||||
|
||||
@@ -197,7 +197,7 @@ def inline_discussion(request, course_key, discussion_id):
|
||||
"""
|
||||
nr_transaction = newrelic.agent.current_transaction()
|
||||
|
||||
course = get_course_with_access(request.user, 'load_forum', course_key)
|
||||
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
|
||||
cc_user = cc.User.from_django_user(request.user)
|
||||
user_info = cc_user.to_dict()
|
||||
|
||||
@@ -232,7 +232,7 @@ def forum_form_discussion(request, course_key):
|
||||
"""
|
||||
nr_transaction = newrelic.agent.current_transaction()
|
||||
|
||||
course = get_course_with_access(request.user, 'load_forum', course_key, check_if_enrolled=True)
|
||||
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
|
||||
course_settings = make_course_settings(course, request.user)
|
||||
|
||||
user = cc.User.from_django_user(request.user)
|
||||
@@ -299,7 +299,7 @@ def single_thread(request, course_key, discussion_id, thread_id):
|
||||
"""
|
||||
nr_transaction = newrelic.agent.current_transaction()
|
||||
|
||||
course = get_course_with_access(request.user, 'load_forum', course_key)
|
||||
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
|
||||
course_settings = make_course_settings(course, request.user)
|
||||
cc_user = cc.User.from_django_user(request.user)
|
||||
user_info = cc_user.to_dict()
|
||||
@@ -402,7 +402,7 @@ def user_profile(request, course_key, user_id):
|
||||
nr_transaction = newrelic.agent.current_transaction()
|
||||
|
||||
#TODO: Allow sorting?
|
||||
course = get_course_with_access(request.user, 'load_forum', course_key)
|
||||
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
|
||||
try:
|
||||
query_params = {
|
||||
'page': request.GET.get('page', 1),
|
||||
@@ -465,7 +465,7 @@ def followed_threads(request, course_key, user_id):
|
||||
|
||||
nr_transaction = newrelic.agent.current_transaction()
|
||||
|
||||
course = get_course_with_access(request.user, 'load_forum', course_key)
|
||||
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
|
||||
try:
|
||||
profiled_user = cc.User(id=user_id, course_id=course_key)
|
||||
|
||||
|
||||
@@ -2,14 +2,17 @@
|
||||
Tests for the LTI provider views
|
||||
"""
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from mock import patch, MagicMock
|
||||
|
||||
from courseware.testutils import RenderXBlockTestMixin
|
||||
from lti_provider import views, models
|
||||
from lti_provider.signature_validator import SignatureValidator
|
||||
from opaque_keys.edx.locator import CourseLocator, BlockUsageLocator
|
||||
from student.tests.factories import UserFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
|
||||
|
||||
LTI_DEFAULT_PARAMS = {
|
||||
@@ -64,14 +67,13 @@ def build_run_request(authenticated=True):
|
||||
return request
|
||||
|
||||
|
||||
class LtiLaunchTest(TestCase):
|
||||
class LtiTestMixin(object):
|
||||
"""
|
||||
Tests for the lti_launch view
|
||||
Mixin for LTI tests
|
||||
"""
|
||||
|
||||
@patch.dict('django.conf.settings.FEATURES', {'ENABLE_LTI_PROVIDER': True})
|
||||
def setUp(self):
|
||||
super(LtiLaunchTest, self).setUp()
|
||||
super(LtiTestMixin, self).setUp()
|
||||
# Always accept the OAuth signature
|
||||
SignatureValidator.verify = MagicMock(return_value=True)
|
||||
self.consumer = models.LtiConsumer(
|
||||
@@ -81,6 +83,11 @@ class LtiLaunchTest(TestCase):
|
||||
)
|
||||
self.consumer.save()
|
||||
|
||||
|
||||
class LtiLaunchTest(LtiTestMixin, TestCase):
|
||||
"""
|
||||
Tests for the lti_launch view
|
||||
"""
|
||||
@patch('lti_provider.views.render_courseware')
|
||||
def test_valid_launch(self, render):
|
||||
"""
|
||||
@@ -88,7 +95,7 @@ class LtiLaunchTest(TestCase):
|
||||
"""
|
||||
request = build_launch_request()
|
||||
views.lti_launch(request, unicode(COURSE_KEY), unicode(USAGE_KEY))
|
||||
render.assert_called_with(request, ALL_PARAMS)
|
||||
render.assert_called_with(request, ALL_PARAMS['usage_key'])
|
||||
|
||||
@patch('lti_provider.views.render_courseware')
|
||||
@patch('lti_provider.views.store_outcome_parameters')
|
||||
@@ -198,28 +205,18 @@ class LtiLaunchTest(TestCase):
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
|
||||
class LtiRunTest(TestCase):
|
||||
class LtiRunTest(LtiTestMixin, TestCase):
|
||||
"""
|
||||
Tests for the lti_run view
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(LtiRunTest, self).setUp()
|
||||
consumer = models.LtiConsumer(
|
||||
consumer_name='consumer',
|
||||
consumer_key=LTI_DEFAULT_PARAMS['oauth_consumer_key'],
|
||||
consumer_secret='secret'
|
||||
)
|
||||
consumer.save()
|
||||
|
||||
@patch('lti_provider.views.render_courseware')
|
||||
def test_valid_launch(self, render):
|
||||
"""
|
||||
Verifies that the view returns OK if called with the correct context
|
||||
"""
|
||||
request = build_run_request()
|
||||
response = views.lti_run(request)
|
||||
render.assert_called_with(request, ALL_PARAMS)
|
||||
views.lti_run(request)
|
||||
render.assert_called_with(request, ALL_PARAMS['usage_key'])
|
||||
|
||||
def test_forbidden_if_session_key_missing(self):
|
||||
"""
|
||||
@@ -269,83 +266,19 @@ class LtiRunTest(TestCase):
|
||||
self.assertEqual(consumer.instance_guid, 'instance_guid')
|
||||
|
||||
|
||||
class RenderCoursewareTest(TestCase):
|
||||
class LtiRunTestRender(LtiTestMixin, RenderXBlockTestMixin, ModuleStoreTestCase):
|
||||
"""
|
||||
Tests for the render_courseware method
|
||||
Tests for the rendering returned by lti_run view.
|
||||
This class overrides the get_response method, which is used by
|
||||
the tests defined in RenderXBlockTestMixin.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
def get_response(self):
|
||||
"""
|
||||
Configure mocks for all the dependencies of the render method
|
||||
Overridable method to get the response from the endpoint that is being tested.
|
||||
"""
|
||||
super(RenderCoursewareTest, self).setUp()
|
||||
self.module_instance = MagicMock()
|
||||
self.module_instance.render.return_value = "Fragment"
|
||||
self.render_mock = self.setup_patch('lti_provider.views.render_to_response', 'Rendered page')
|
||||
self.module_mock = self.setup_patch('lti_provider.views.get_module_by_usage_id', (self.module_instance, None))
|
||||
self.access_mock = self.setup_patch('lti_provider.views.has_access', 'StaffAccess')
|
||||
self.course_mock = self.setup_patch('lti_provider.views.get_course_with_access', 'CourseWithAccess')
|
||||
|
||||
def setup_patch(self, function_name, return_value):
|
||||
"""
|
||||
Patch a method with a given return value, and return the mock
|
||||
"""
|
||||
mock = MagicMock(return_value=return_value)
|
||||
new_patch = patch(function_name, new=mock)
|
||||
new_patch.start()
|
||||
self.addCleanup(new_patch.stop)
|
||||
return mock
|
||||
|
||||
def test_valid_launch(self):
|
||||
"""
|
||||
Verify that the method renders a response when launched correctly
|
||||
"""
|
||||
request = build_run_request()
|
||||
response = views.render_courseware(request, ALL_PARAMS.copy())
|
||||
self.assertEqual(response, 'Rendered page')
|
||||
|
||||
def test_course_with_access(self):
|
||||
"""
|
||||
Verify that get_course_with_access is called with the right parameters
|
||||
"""
|
||||
request = build_run_request()
|
||||
views.render_courseware(request, ALL_PARAMS.copy())
|
||||
self.course_mock.assert_called_with(request.user, 'load', COURSE_KEY)
|
||||
|
||||
def test_has_access(self):
|
||||
"""
|
||||
Verify that has_access is called with the right parameters
|
||||
"""
|
||||
request = build_run_request()
|
||||
views.render_courseware(request, ALL_PARAMS.copy())
|
||||
self.access_mock.assert_called_with(request.user, 'staff', 'CourseWithAccess')
|
||||
|
||||
def test_get_module(self):
|
||||
"""
|
||||
Verify that get_module_by_usage_id is called with the right parameters
|
||||
"""
|
||||
request = build_run_request()
|
||||
views.render_courseware(request, ALL_PARAMS.copy())
|
||||
self.module_mock.assert_called_with(request, unicode(COURSE_KEY), unicode(USAGE_KEY))
|
||||
|
||||
def test_render(self):
|
||||
"""
|
||||
Verify that render is called on the right object with the right parameters
|
||||
"""
|
||||
request = build_run_request()
|
||||
views.render_courseware(request, ALL_PARAMS.copy())
|
||||
self.module_instance.render.assert_called_with('student_view', context={})
|
||||
|
||||
def test_context(self):
|
||||
expected_context = {
|
||||
'fragment': 'Fragment',
|
||||
'course': 'CourseWithAccess',
|
||||
'disable_accordion': True,
|
||||
'allow_iframing': True,
|
||||
'disable_header': True,
|
||||
'staff_access': 'StaffAccess',
|
||||
'xqa_server': 'http://example.com/xqa',
|
||||
}
|
||||
request = build_run_request()
|
||||
views.render_courseware(request, ALL_PARAMS.copy())
|
||||
self.render_mock.assert_called_with('courseware/courseware-chromeless.html', expected_context)
|
||||
lti_launch_url = reverse(
|
||||
'lti_provider_launch',
|
||||
kwargs={'course_id': unicode(self.course.id), 'usage_id': unicode(self.html_block.location)}
|
||||
)
|
||||
SignatureValidator.verify = MagicMock(return_value=True)
|
||||
return self.client.post(lti_launch_url, data=LTI_DEFAULT_PARAMS)
|
||||
|
||||
@@ -8,7 +8,11 @@ from django.conf.urls import patterns, url
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
|
||||
url(r'^courses/{}/(?P<usage_id>[^/]*)$'.format(settings.COURSE_ID_PATTERN),
|
||||
url(
|
||||
r'^courses/{course_id}/{usage_id}$'.format(
|
||||
course_id=settings.COURSE_ID_PATTERN,
|
||||
usage_id=settings.USAGE_ID_PATTERN
|
||||
),
|
||||
'lti_provider.views.lti_launch', name="lti_provider_launch"),
|
||||
url(r'^lti_run$', 'lti_provider.views.lti_run', name="lti_provider_run"),
|
||||
)
|
||||
|
||||
@@ -10,10 +10,6 @@ from django.http import HttpResponseBadRequest, HttpResponseForbidden, Http404
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
import logging
|
||||
|
||||
from courseware.access import has_access
|
||||
from courseware.courses import get_course_with_access
|
||||
from courseware.module_render import get_module_by_usage_id
|
||||
from edxmako.shortcuts import render_to_response
|
||||
from lti_provider.outcomes import store_outcome_parameters
|
||||
from lti_provider.models import LtiConsumer
|
||||
from lti_provider.signature_validator import SignatureValidator
|
||||
@@ -139,7 +135,7 @@ def lti_run(request):
|
||||
)
|
||||
store_outcome_parameters(params, request.user, lti_consumer)
|
||||
|
||||
return render_courseware(request, params)
|
||||
return render_courseware(request, params['usage_key'])
|
||||
|
||||
|
||||
def get_required_parameters(dictionary, additional_params=None):
|
||||
@@ -197,40 +193,19 @@ def restore_params_from_session(request):
|
||||
return session_params
|
||||
|
||||
|
||||
def render_courseware(request, lti_params):
|
||||
def render_courseware(request, usage_key):
|
||||
"""
|
||||
Render the content requested for the LTI launch.
|
||||
TODO: This method depends on the current refactoring work on the
|
||||
courseware/courseware.html template. It's signature may change depending on
|
||||
the requirements for that template once the refactoring is complete.
|
||||
|
||||
:return: an HttpResponse object that contains the template and necessary
|
||||
Return an HttpResponse object that contains the template and necessary
|
||||
context to render the courseware.
|
||||
"""
|
||||
usage_key = lti_params['usage_key']
|
||||
course_key = lti_params['course_key']
|
||||
user = request.user
|
||||
course = get_course_with_access(user, 'load', course_key)
|
||||
staff = has_access(request.user, 'staff', course)
|
||||
instance, _dummy = get_module_by_usage_id(
|
||||
request,
|
||||
unicode(course_key),
|
||||
unicode(usage_key)
|
||||
)
|
||||
|
||||
fragment = instance.render('student_view', context=request.GET)
|
||||
|
||||
context = {
|
||||
'fragment': fragment,
|
||||
'course': course,
|
||||
'disable_accordion': True,
|
||||
'allow_iframing': True,
|
||||
'disable_header': True,
|
||||
'staff_access': staff,
|
||||
'xqa_server': settings.FEATURES.get('XQA_SERVER', 'http://example.com/xqa'),
|
||||
}
|
||||
|
||||
return render_to_response('courseware/courseware-chromeless.html', context)
|
||||
# return an HttpResponse object that contains the template and necessary context to render the courseware.
|
||||
from courseware.views import render_xblock
|
||||
return render_xblock(request, unicode(usage_key))
|
||||
|
||||
|
||||
def parse_course_and_usage_keys(course_id, usage_id):
|
||||
|
||||
@@ -321,6 +321,7 @@ FEATURES = {
|
||||
# ENABLE_OAUTH2_PROVIDER to True
|
||||
'ENABLE_MOBILE_REST_API': False,
|
||||
'ENABLE_MOBILE_SOCIAL_FACEBOOK_FEATURES': False,
|
||||
'ENABLE_RENDER_XBLOCK_API': False,
|
||||
|
||||
# Enable the combined login/registration form
|
||||
'ENABLE_COMBINED_LOGIN_REGISTRATION': False,
|
||||
|
||||
@@ -21,7 +21,7 @@ def url_class(is_active):
|
||||
%>
|
||||
<%
|
||||
cohorted_user_partition = get_cohorted_user_partition(course.id)
|
||||
show_preview_menu = staff_access and active_page in ['courseware', 'info']
|
||||
show_preview_menu = not disable_preview_menu and staff_access and active_page in ['courseware', 'info']
|
||||
is_student_masquerade = masquerade and masquerade.role == 'student'
|
||||
masquerade_group_id = masquerade.group_id if masquerade else None
|
||||
%>
|
||||
|
||||
@@ -129,7 +129,9 @@ from branding import api as branding_api
|
||||
</head>
|
||||
|
||||
<body class="${static.dir_rtl()} <%block name='bodyclass'/> lang_${LANGUAGE_CODE}">
|
||||
% if not disable_window_wrap:
|
||||
<div class="window-wrap" dir="${static.dir_rtl()}">
|
||||
% endif
|
||||
<a class="nav-skip" href="<%block name="nav_skip">#content</%block>">${_("Skip to main content")}</a>
|
||||
|
||||
% if not disable_header:
|
||||
@@ -159,7 +161,9 @@ from branding import api as branding_api
|
||||
</%block>
|
||||
% endif
|
||||
|
||||
% if not disable_window_wrap:
|
||||
</div>
|
||||
% endif
|
||||
|
||||
% if not disable_courseware_js:
|
||||
<%static:js group='application'/>
|
||||
|
||||
@@ -20,7 +20,8 @@ ${block_content}
|
||||
% endif
|
||||
</div>
|
||||
% endif
|
||||
<div aria-hidden="true" class="wrap-instructor-info">
|
||||
% if not disable_staff_debug_info:
|
||||
<div class="wrap-instructor-info" aria-hidden="true">
|
||||
<a class="instructor-info-action" href="#${element_id}_debug" id="${element_id}_trig">${_("Staff Debug Info")}</a>
|
||||
|
||||
% if settings.FEATURES.get('ENABLE_STUDENT_HISTORY_VIEW') and \
|
||||
@@ -28,6 +29,7 @@ ${block_content}
|
||||
<a class="instructor-info-action" href="#${element_id}_history" id="${element_id}_history_trig">${_("Submission history")}</a>
|
||||
% endif
|
||||
</div>
|
||||
% endif
|
||||
|
||||
<section aria-hidden="true" id="${element_id}_xqa-modal" class="modal xqa-modal" style="width:80%; left:20%; height:80%; overflow:auto" >
|
||||
<div class="inner-wrapper">
|
||||
|
||||
@@ -450,6 +450,15 @@ if settings.COURSEWARE_ENABLED:
|
||||
url(r'^courses/{}/teams'.format(settings.COURSE_ID_PATTERN), include('teams.urls'), name="teams_endpoints"),
|
||||
)
|
||||
|
||||
if settings.FEATURES.get('ENABLE_RENDER_XBLOCK_API'):
|
||||
# TODO (MA-789) This endpoint path still needs to be approved by the arch council.
|
||||
# Until then, keep the version at v0.
|
||||
urlpatterns += (
|
||||
url(r'api/xblock/v0/xblock/{usage_key_string}$'.format(usage_key_string=settings.USAGE_KEY_PATTERN),
|
||||
'courseware.views.render_xblock',
|
||||
name='render_xblock'),
|
||||
)
|
||||
|
||||
# allow course staff to change to student view of courseware
|
||||
if settings.FEATURES.get('ENABLE_MASQUERADE'):
|
||||
urlpatterns += (
|
||||
|
||||
@@ -90,7 +90,8 @@ def view_course_access(depth=0, access_action='load', check_for_milestones=False
|
||||
request.user,
|
||||
access_action,
|
||||
course_id,
|
||||
depth=depth
|
||||
depth=depth,
|
||||
check_if_enrolled=True,
|
||||
)
|
||||
except Http404:
|
||||
# any_unfulfilled_milestones called a second time since has_access returns a bool
|
||||
|
||||
@@ -209,7 +209,7 @@ def grade_histogram(module_id):
|
||||
|
||||
|
||||
@contract(user=User, has_instructor_access=bool, block=XBlock, view=basestring, frag=Fragment, context="dict|None")
|
||||
def add_staff_markup(user, has_instructor_access, block, view, frag, context): # pylint: disable=unused-argument
|
||||
def add_staff_markup(user, has_instructor_access, disable_staff_debug_info, block, view, frag, context): # pylint: disable=unused-argument
|
||||
"""
|
||||
Updates the supplied module with a new get_html function that wraps
|
||||
the output of the old get_html function with additional information
|
||||
@@ -305,6 +305,7 @@ def add_staff_markup(user, has_instructor_access, block, view, frag, context):
|
||||
'block_content': frag.content,
|
||||
'is_released': is_released,
|
||||
'has_instructor_access': has_instructor_access,
|
||||
'disable_staff_debug_info': disable_staff_debug_info,
|
||||
}
|
||||
return wrap_fragment(frag, render_to_string("staff_problem_info.html", staff_context))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user