Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions common/djangoapps/util/url.py
Original file line number Diff line number Diff line change
@@ -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))
32 changes: 11 additions & 21 deletions lms/djangoapps/courseware/access.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -27,7 +34,7 @@
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
Expand Down Expand Up @@ -140,18 +147,6 @@ def can_load():
# 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?
Expand All @@ -164,12 +159,8 @@ def can_load_mobile():
(
# 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)
)
)

Expand Down Expand Up @@ -294,7 +285,6 @@ def can_view_courseware_with_prerequisites(): # pylint: disable=invalid-name
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,
Expand Down
24 changes: 9 additions & 15 deletions lms/djangoapps/courseware/courses.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to reviewers: get_opt_course_with_access was removed since there were no callers. Must be an old function.

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):
Expand Down
73 changes: 42 additions & 31 deletions lms/djangoapps/courseware/module_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,11 @@
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
Expand All @@ -37,42 +35,42 @@
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
Expand Down Expand Up @@ -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)
Expand All @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This TODO seems weird. Was it supposed to be for the comment underneath?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's supposed to be for the pylint warning. Pylint is complaining that this method is waaay too big, which is completely true. We really should refactor it at some point. So the TODO was added along with disabling the pylint warning.

# 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.

Expand All @@ -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(
Expand Down Expand Up @@ -573,7 +576,7 @@ def rebind_noauth_module_to_user(module, real_user):
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
Expand Down Expand Up @@ -637,7 +640,7 @@ def rebind_noauth_module_to_user(module, real_user):
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()
Expand Down Expand Up @@ -681,7 +684,7 @@ def rebind_noauth_module_to_user(module, real_user):
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.

Expand All @@ -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(
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down
23 changes: 23 additions & 0 deletions lms/djangoapps/courseware/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@
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
Expand Down Expand Up @@ -584,6 +586,7 @@ def set_up_course(self, **course_kwargs):

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):
Expand Down Expand Up @@ -752,6 +755,7 @@ def setUp(self):
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)
Expand Down Expand Up @@ -1087,3 +1091,22 @@ def test_student_state(self, default_store):
# 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)
Loading