Commit 65bcc50e by Nimisha Asthagiri

Merge pull request #8240 from edx/mobile/render_xblock

MA-722 Render xBlock API
parents 8d1651cc d240785b
"""
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))
"""
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,31 +92,25 @@ 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.")
return course
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)
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)
return course
def course_image_url(course):
......
......@@ -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)
"""
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):
"""
Configure mocks for all the dependencies of the render method
"""
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):
def get_response(self):
"""
Verify that get_course_with_access is called with the right parameters
Overridable method to get the response from the endpoint that is being tested.
"""
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))
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment