Merge remote-tracking branch 'origin/master' into feature/bridger/new_wiki
This commit is contained in:
301
lms/djangoapps/courseware/access.py
Normal file
301
lms/djangoapps/courseware/access.py
Normal file
@@ -0,0 +1,301 @@
|
||||
"""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"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.error_module import ErrorDescriptor
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.timeparse import parse_time
|
||||
from xmodule.x_module import XModule, XModuleDescriptor
|
||||
|
||||
|
||||
DEBUG_ACCESS = False
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def debug(*args, **kwargs):
|
||||
# to avoid overly verbose output, this is off by default
|
||||
if DEBUG_ACCESS:
|
||||
log.debug(*args, **kwargs)
|
||||
|
||||
def has_access(user, obj, action):
|
||||
"""
|
||||
Check whether a user has the access to do action on obj. Handles any magic
|
||||
switching based on various settings.
|
||||
|
||||
Things this module understands:
|
||||
- start dates for modules
|
||||
- DISABLE_START_DATES
|
||||
- different access for staff, course staff, and students.
|
||||
|
||||
user: a Django user object. May be anonymous.
|
||||
|
||||
obj: The object to check access for. For now, a module or descriptor.
|
||||
|
||||
action: A string specifying the action that the client is trying to perform.
|
||||
|
||||
actions depend on the obj type, but include e.g. 'enroll' for courses. See the
|
||||
type-specific functions below for the known actions for that type.
|
||||
|
||||
Returns a bool. It is up to the caller to actually deny access in a way
|
||||
that makes sense in context.
|
||||
"""
|
||||
# delegate the work to type-specific functions.
|
||||
# (start with more specific types, then get more general)
|
||||
if isinstance(obj, CourseDescriptor):
|
||||
return _has_access_course_desc(user, obj, action)
|
||||
|
||||
if isinstance(obj, ErrorDescriptor):
|
||||
return _has_access_error_desc(user, obj, action)
|
||||
|
||||
# NOTE: any descriptor access checkers need to go above this
|
||||
if isinstance(obj, XModuleDescriptor):
|
||||
return _has_access_descriptor(user, obj, action)
|
||||
|
||||
if isinstance(obj, XModule):
|
||||
return _has_access_xmodule(user, obj, action)
|
||||
|
||||
if isinstance(obj, Location):
|
||||
return _has_access_location(user, obj, action)
|
||||
|
||||
# Passing an unknown object here is a coding error, so rather than
|
||||
# returning a default, complain.
|
||||
raise TypeError("Unknown object type in has_access(). Object type: '{}'"
|
||||
.format(type(obj)))
|
||||
|
||||
# ================ Implementation helpers ================================
|
||||
|
||||
def _has_access_course_desc(user, course, action):
|
||||
"""
|
||||
Check if user has access to a course descriptor.
|
||||
|
||||
Valid actions:
|
||||
|
||||
'load' -- load the courseware, see inside the course
|
||||
'enroll' -- enroll. Checks for enrollment window,
|
||||
ACCESS_REQUIRE_STAFF_FOR_COURSE,
|
||||
'see_exists' -- can see that the course exists.
|
||||
'staff' -- staff access to course.
|
||||
"""
|
||||
def can_load():
|
||||
"Can this user load this course?"
|
||||
# delegate to generic descriptor check
|
||||
return _has_access_descriptor(user, course, action)
|
||||
|
||||
def can_enroll():
|
||||
"""
|
||||
If the course has an enrollment period, check whether we are in it.
|
||||
(staff can always enroll)
|
||||
"""
|
||||
|
||||
now = time.gmtime()
|
||||
start = course.enrollment_start
|
||||
end = course.enrollment_end
|
||||
|
||||
if (start is None or now > start) and (end is None or now < end):
|
||||
# in enrollment period, so any user is allowed to enroll.
|
||||
debug("Allow: in enrollment period")
|
||||
return True
|
||||
|
||||
# otherwise, need staff access
|
||||
return _has_staff_access_to_descriptor(user, course)
|
||||
|
||||
def see_exists():
|
||||
"""
|
||||
Can see if can enroll, but also if can load it: if user enrolled in a course and now
|
||||
it's past the enrollment period, they should still see it.
|
||||
|
||||
TODO (vshnayder): This means that courses with limited enrollment periods will not appear
|
||||
to non-staff visitors after the enrollment period is over. If this is not what we want, will
|
||||
need to change this logic.
|
||||
"""
|
||||
# VS[compat] -- this setting should go away once all courses have
|
||||
# properly configured enrollment_start times (if course should be
|
||||
# staff-only, set enrollment_start far in the future.)
|
||||
if settings.MITX_FEATURES.get('ACCESS_REQUIRE_STAFF_FOR_COURSE'):
|
||||
# if this feature is on, only allow courses that have ispublic set to be
|
||||
# seen by non-staff
|
||||
if course.metadata.get('ispublic'):
|
||||
debug("Allow: ACCESS_REQUIRE_STAFF_FOR_COURSE and ispublic")
|
||||
return True
|
||||
return _has_staff_access_to_descriptor(user, course)
|
||||
|
||||
return can_enroll() or can_load()
|
||||
|
||||
checkers = {
|
||||
'load': can_load,
|
||||
'enroll': can_enroll,
|
||||
'see_exists': see_exists,
|
||||
'staff': lambda: _has_staff_access_to_descriptor(user, course)
|
||||
}
|
||||
|
||||
return _dispatch(checkers, action, user, course)
|
||||
|
||||
|
||||
def _has_access_error_desc(user, descriptor, action):
|
||||
"""
|
||||
Only staff should see error descriptors.
|
||||
|
||||
Valid actions:
|
||||
'load' -- load this descriptor, showing it to the user.
|
||||
'staff' -- staff access to descriptor.
|
||||
"""
|
||||
def check_for_staff():
|
||||
return _has_staff_access_to_descriptor(user, descriptor)
|
||||
|
||||
checkers = {
|
||||
'load': check_for_staff,
|
||||
'staff': check_for_staff
|
||||
}
|
||||
|
||||
return _dispatch(checkers, action, user, descriptor)
|
||||
|
||||
|
||||
def _has_access_descriptor(user, descriptor, action):
|
||||
"""
|
||||
Check if user has access to this descriptor.
|
||||
|
||||
Valid actions:
|
||||
'load' -- load this descriptor, showing it to the user.
|
||||
'staff' -- staff access to descriptor.
|
||||
|
||||
NOTE: This is the fallback logic for descriptors that don't have custom policy
|
||||
(e.g. courses). If you call this method directly instead of going through
|
||||
has_access(), it will not do the right thing.
|
||||
"""
|
||||
def can_load():
|
||||
# If start dates are off, can always load
|
||||
if settings.MITX_FEATURES['DISABLE_START_DATES']:
|
||||
debug("Allow: DISABLE_START_DATES")
|
||||
return True
|
||||
|
||||
# Check start date
|
||||
if descriptor.start is not None:
|
||||
now = time.gmtime()
|
||||
if now > descriptor.start:
|
||||
# after start date, everyone can see it
|
||||
debug("Allow: now > start date")
|
||||
return True
|
||||
# otherwise, need staff access
|
||||
return _has_staff_access_to_descriptor(user, descriptor)
|
||||
|
||||
# No start date, so can always load.
|
||||
debug("Allow: no start date")
|
||||
return True
|
||||
|
||||
checkers = {
|
||||
'load': can_load,
|
||||
'staff': lambda: _has_staff_access_to_descriptor(user, descriptor)
|
||||
}
|
||||
|
||||
return _dispatch(checkers, action, user, descriptor)
|
||||
|
||||
|
||||
|
||||
|
||||
def _has_access_xmodule(user, xmodule, action):
|
||||
"""
|
||||
Check if user has access to this xmodule.
|
||||
|
||||
Valid actions:
|
||||
- same as the valid actions for xmodule.descriptor
|
||||
"""
|
||||
# Delegate to the descriptor
|
||||
return has_access(user, xmodule.descriptor, action)
|
||||
|
||||
|
||||
def _has_access_location(user, location, action):
|
||||
"""
|
||||
Check if user has access to this location.
|
||||
|
||||
Valid actions:
|
||||
'staff' : True if the user has staff access to this location
|
||||
|
||||
NOTE: if you add other actions, make sure that
|
||||
|
||||
has_access(user, location, action) == has_access(user, get_item(location), action)
|
||||
|
||||
And in general, prefer checking access on loaded items, rather than locations.
|
||||
"""
|
||||
checkers = {
|
||||
'staff': lambda: _has_staff_access_to_location(user, location)
|
||||
}
|
||||
|
||||
return _dispatch(checkers, action, user, location)
|
||||
|
||||
|
||||
##### Internal helper methods below
|
||||
|
||||
def _dispatch(table, action, user, obj):
|
||||
"""
|
||||
Helper: call table[action], raising a nice pretty error if there is no such key.
|
||||
|
||||
user and object passed in only for error messages and debugging
|
||||
"""
|
||||
if action in table:
|
||||
result = table[action]()
|
||||
debug("%s user %s, object %s, action %s",
|
||||
'ALLOWED' if result else 'DENIED',
|
||||
user,
|
||||
obj.location.url() if isinstance(obj, XModuleDescriptor) else str(obj)[:60],
|
||||
action)
|
||||
return result
|
||||
|
||||
raise ValueError("Unknown action for object type '{}': '{}'".format(
|
||||
type(obj), action))
|
||||
|
||||
def _course_staff_group_name(location):
|
||||
"""
|
||||
Get the name of the staff group for a location. Right now, that's staff_COURSE.
|
||||
|
||||
location: something that can passed to Location.
|
||||
"""
|
||||
return 'staff_%s' % Location(location).course
|
||||
|
||||
def _has_staff_access_to_location(user, location):
|
||||
'''
|
||||
Returns True if the given user has staff access to a location. For now this
|
||||
is equivalent to having staff access to the course location.course.
|
||||
|
||||
This means that user is in the staff_* group, or is an overall admin.
|
||||
|
||||
TODO (vshnayder): this needs to be changed to allow per-course_id permissions, not per-course
|
||||
(e.g. staff in 2012 is different from 2013, but maybe some people always have access)
|
||||
|
||||
course is a string: the course field of the location being accessed.
|
||||
'''
|
||||
if user is None or (not user.is_authenticated()):
|
||||
debug("Deny: no user or anon user")
|
||||
return False
|
||||
if user.is_staff:
|
||||
debug("Allow: user.is_staff")
|
||||
return True
|
||||
|
||||
# If not global staff, is the user in the Auth group for this class?
|
||||
user_groups = [x[1] for x in user.groups.values_list()]
|
||||
staff_group = _course_staff_group_name(location)
|
||||
if staff_group in user_groups:
|
||||
debug("Allow: user in group %s", staff_group)
|
||||
return True
|
||||
debug("Deny: user not in group %s", staff_group)
|
||||
return False
|
||||
|
||||
def _has_staff_access_to_course_id(user, course_id):
|
||||
"""Helper method that takes a course_id instead of a course name"""
|
||||
loc = CourseDescriptor.id_to_location(course_id)
|
||||
return _has_staff_access_to_location(user, loc)
|
||||
|
||||
|
||||
def _has_staff_access_to_descriptor(user, descriptor):
|
||||
"""Helper method that checks whether the user has staff access to
|
||||
the course of the location.
|
||||
|
||||
location: something that can be passed to Location
|
||||
"""
|
||||
return _has_staff_access_to_location(user, descriptor.location)
|
||||
|
||||
@@ -12,49 +12,51 @@ from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from static_replace import replace_urls, try_staticfiles_lookup
|
||||
from courseware.access import has_access
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def check_course(user, course_id, course_must_be_open=True, course_required=True):
|
||||
def get_course_by_id(course_id):
|
||||
"""
|
||||
Given a django user and a course_id, this returns the course
|
||||
object. By default, if the course is not found or the course is
|
||||
not open yet, this method will raise a 404.
|
||||
Given a course id, return the corresponding course descriptor.
|
||||
|
||||
If course_must_be_open is False, the course will be returned
|
||||
without a 404 even if it is not open.
|
||||
|
||||
If course_required is False, a course_id of None is acceptable. The
|
||||
course returned will be None. Even if the course is not required,
|
||||
if a course_id is given that does not exist a 404 will be raised.
|
||||
|
||||
This behavior is modified by MITX_FEATURES['DARK_LAUNCH']:
|
||||
if dark launch is enabled, course_must_be_open is ignored for
|
||||
users that have staff access.
|
||||
If course_id is not valid, raises a 404.
|
||||
"""
|
||||
course = None
|
||||
if course_required or course_id:
|
||||
try:
|
||||
course_loc = CourseDescriptor.id_to_location(course_id)
|
||||
course = modulestore().get_item(course_loc)
|
||||
try:
|
||||
course_loc = CourseDescriptor.id_to_location(course_id)
|
||||
return modulestore().get_item(course_loc)
|
||||
except (KeyError, ItemNotFoundError):
|
||||
raise Http404("Course not found.")
|
||||
|
||||
except (KeyError, ItemNotFoundError):
|
||||
raise Http404("Course not found.")
|
||||
|
||||
started = course.has_started() or settings.MITX_FEATURES['DISABLE_START_DATES']
|
||||
|
||||
must_be_open = course_must_be_open
|
||||
if (settings.MITX_FEATURES['DARK_LAUNCH'] and
|
||||
has_staff_access_to_course(user, course)):
|
||||
must_be_open = False
|
||||
|
||||
if must_be_open and not started:
|
||||
raise Http404("This course has not yet started.")
|
||||
def get_course_with_access(user, course_id, action):
|
||||
"""
|
||||
Given a course_id, look up the corresponding course descriptor,
|
||||
check that the user has the access to perform the specified action
|
||||
on the course, and return the descriptor.
|
||||
|
||||
Raises a 404 if the course_id is invalid, or the user doesn't have access.
|
||||
"""
|
||||
course = get_course_by_id(course_id)
|
||||
if not has_access(user, course, action):
|
||||
# Deliberately return a non-specific error message to avoid
|
||||
# leaking info about access control settings
|
||||
raise Http404("Course not found.")
|
||||
return course
|
||||
|
||||
|
||||
def get_opt_course_with_access(user, course_id, action):
|
||||
"""
|
||||
Same as get_course_with_access, except that if course_id is None,
|
||||
return None without performing any access checks.
|
||||
"""
|
||||
if course_id is None:
|
||||
return None
|
||||
return get_course_with_access(user, course_id, action)
|
||||
|
||||
|
||||
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"""
|
||||
@@ -140,66 +142,10 @@ def get_course_info_section(course, section_key):
|
||||
|
||||
raise KeyError("Invalid about key " + str(section_key))
|
||||
|
||||
def course_staff_group_name(course):
|
||||
'''
|
||||
course should be either a CourseDescriptor instance, or a string (the
|
||||
.course entry of a Location)
|
||||
'''
|
||||
if isinstance(course, str) or isinstance(course, unicode):
|
||||
coursename = course
|
||||
else:
|
||||
# should be a CourseDescriptor, so grab its location.course:
|
||||
coursename = course.location.course
|
||||
return 'staff_%s' % coursename
|
||||
|
||||
def has_staff_access_to_course(user, course):
|
||||
'''
|
||||
Returns True if the given user has staff access to the course.
|
||||
This means that user is in the staff_* group, or is an overall admin.
|
||||
TODO (vshnayder): this needs to be changed to allow per-course_id permissions, not per-course
|
||||
(e.g. staff in 2012 is different from 2013, but maybe some people always have access)
|
||||
|
||||
course is the course field of the location being accessed.
|
||||
'''
|
||||
if user is None or (not user.is_authenticated()) or course is None:
|
||||
return False
|
||||
if user.is_staff:
|
||||
return True
|
||||
|
||||
# note this is the Auth group, not UserTestGroup
|
||||
user_groups = [x[1] for x in user.groups.values_list()]
|
||||
staff_group = course_staff_group_name(course)
|
||||
if staff_group in user_groups:
|
||||
return True
|
||||
return False
|
||||
|
||||
def has_staff_access_to_course_id(user, course_id):
|
||||
"""Helper method that takes a course_id instead of a course name"""
|
||||
loc = CourseDescriptor.id_to_location(course_id)
|
||||
return has_staff_access_to_course(user, loc.course)
|
||||
|
||||
|
||||
def has_staff_access_to_location(user, location):
|
||||
"""Helper method that checks whether the user has staff access to
|
||||
the course of the location.
|
||||
|
||||
location: something that can be passed to Location
|
||||
"""
|
||||
return has_staff_access_to_course(user, Location(location).course)
|
||||
|
||||
def has_access_to_course(user, course):
|
||||
'''course is the .course element of a location'''
|
||||
if course.metadata.get('ispublic'):
|
||||
return True
|
||||
return has_staff_access_to_course(user,course)
|
||||
|
||||
def get_courses_by_university(user):
|
||||
'''
|
||||
Returns dict of lists of courses available, keyed by course.org (ie university).
|
||||
Courses are sorted by course.number.
|
||||
|
||||
if ACCESS_REQUIRE_STAFF_FOR_COURSE then list only includes those accessible
|
||||
to user.
|
||||
'''
|
||||
# TODO: Clean up how 'error' is done.
|
||||
# filter out any courses that errored.
|
||||
@@ -208,9 +154,7 @@ def get_courses_by_university(user):
|
||||
courses = sorted(courses, key=lambda course: course.number)
|
||||
universities = defaultdict(list)
|
||||
for course in courses:
|
||||
if settings.MITX_FEATURES.get('ACCESS_REQUIRE_STAFF_FOR_COURSE'):
|
||||
if not has_access_to_course(user,course):
|
||||
continue
|
||||
universities[course.org].append(course)
|
||||
if has_access(user, course, 'see_exists'):
|
||||
universities[course.org].append(course)
|
||||
return universities
|
||||
|
||||
|
||||
@@ -63,7 +63,12 @@ def grade(student, request, course, student_module_cache=None):
|
||||
scores = []
|
||||
# TODO: We need the request to pass into here. If we could forgo that, our arguments
|
||||
# would be simpler
|
||||
section_module = get_module(student, request, section_descriptor.location, student_module_cache)
|
||||
section_module = get_module(student, request,
|
||||
section_descriptor.location, student_module_cache)
|
||||
if section_module is None:
|
||||
# student doesn't have access to this module, or something else
|
||||
# went wrong.
|
||||
continue
|
||||
|
||||
# TODO: We may be able to speed this up by only getting a list of children IDs from section_module
|
||||
# Then, we may not need to instatiate any problems if they are already in the database
|
||||
|
||||
@@ -67,7 +67,7 @@ class StudentModuleCache(object):
|
||||
"""
|
||||
A cache of StudentModules for a specific student
|
||||
"""
|
||||
def __init__(self, user, descriptors):
|
||||
def __init__(self, user, descriptors, select_for_update=False):
|
||||
'''
|
||||
Find any StudentModule objects that are needed by any descriptor
|
||||
in descriptors. Avoids making multiple queries to the database.
|
||||
@@ -77,6 +77,7 @@ class StudentModuleCache(object):
|
||||
Arguments
|
||||
user: The user for which to fetch maching StudentModules
|
||||
descriptors: An array of XModuleDescriptors.
|
||||
select_for_update: Flag indicating whether the row should be locked until end of transaction
|
||||
'''
|
||||
if user.is_authenticated():
|
||||
module_ids = self._get_module_state_keys(descriptors)
|
||||
@@ -86,23 +87,30 @@ class StudentModuleCache(object):
|
||||
self.cache = []
|
||||
chunk_size = 500
|
||||
for id_chunk in [module_ids[i:i + chunk_size] for i in xrange(0, len(module_ids), chunk_size)]:
|
||||
self.cache.extend(StudentModule.objects.filter(
|
||||
student=user,
|
||||
module_state_key__in=id_chunk)
|
||||
)
|
||||
if select_for_update:
|
||||
self.cache.extend(StudentModule.objects.select_for_update().filter(
|
||||
student=user,
|
||||
module_state_key__in=id_chunk)
|
||||
)
|
||||
else:
|
||||
self.cache.extend(StudentModule.objects.filter(
|
||||
student=user,
|
||||
module_state_key__in=id_chunk)
|
||||
)
|
||||
|
||||
else:
|
||||
self.cache = []
|
||||
|
||||
|
||||
@classmethod
|
||||
def cache_for_descriptor_descendents(cls, user, descriptor, depth=None, descriptor_filter=lambda descriptor: True):
|
||||
def cache_for_descriptor_descendents(cls, user, descriptor, depth=None, descriptor_filter=lambda descriptor: True, select_for_update=False):
|
||||
"""
|
||||
descriptor: An XModuleDescriptor
|
||||
depth is the number of levels of descendent modules to load StudentModules for, in addition to
|
||||
the supplied descriptor. If depth is None, load all descendent StudentModules
|
||||
descriptor_filter is a function that accepts a descriptor and return wether the StudentModule
|
||||
should be cached
|
||||
select_for_update: Flag indicating whether the row should be locked until end of transaction
|
||||
"""
|
||||
|
||||
def get_child_descriptors(descriptor, depth, descriptor_filter):
|
||||
@@ -122,7 +130,7 @@ class StudentModuleCache(object):
|
||||
|
||||
descriptors = get_child_descriptors(descriptor, depth, descriptor_filter)
|
||||
|
||||
return StudentModuleCache(user, descriptors)
|
||||
return StudentModuleCache(user, descriptors, select_for_update)
|
||||
|
||||
def _get_module_state_keys(self, descriptors):
|
||||
'''
|
||||
|
||||
@@ -2,27 +2,40 @@ import json
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import Http404
|
||||
from django.http import HttpResponse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from capa.xqueue_interface import qinterface
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
from capa.xqueue_interface import XQueueInterface
|
||||
from courseware.access import has_access
|
||||
from mitxmako.shortcuts import render_to_string
|
||||
from models import StudentModule, StudentModuleCache
|
||||
from static_replace import replace_urls
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.x_module import ModuleSystem
|
||||
from xmodule_modifiers import replace_static_urls, add_histogram, wrap_xmodule
|
||||
|
||||
from courseware.courses import (has_staff_access_to_course,
|
||||
has_staff_access_to_location)
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
|
||||
if settings.XQUEUE_INTERFACE['basic_auth'] is not None:
|
||||
requests_auth = HTTPBasicAuth(*settings.XQUEUE_INTERFACE['basic_auth'])
|
||||
else:
|
||||
requests_auth = None
|
||||
|
||||
xqueue_interface = XQueueInterface(
|
||||
settings.XQUEUE_INTERFACE['url'],
|
||||
settings.XQUEUE_INTERFACE['django_auth'],
|
||||
requests_auth,
|
||||
)
|
||||
|
||||
|
||||
def make_track_function(request):
|
||||
'''
|
||||
Make a tracking function that logs what happened.
|
||||
@@ -52,6 +65,9 @@ def toc_for_course(user, request, course, active_chapter, active_section):
|
||||
Everything else comes from the xml, or defaults to "".
|
||||
|
||||
chapters with name 'hidden' are skipped.
|
||||
|
||||
NOTE: assumes that if we got this far, user has access to course. Returns
|
||||
None if this is not the case.
|
||||
'''
|
||||
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(user, course, depth=2)
|
||||
@@ -129,6 +145,10 @@ def get_module(user, request, location, student_module_cache, position=None):
|
||||
'''
|
||||
descriptor = modulestore().get_item(location)
|
||||
|
||||
# Short circuit--if the user shouldn't have access, bail without doing any work
|
||||
if not has_access(user, descriptor, 'load'):
|
||||
return None
|
||||
|
||||
#TODO Only check the cache if this module can possibly have state
|
||||
instance_module = None
|
||||
shared_module = None
|
||||
@@ -142,22 +162,16 @@ def get_module(user, request, location, student_module_cache, position=None):
|
||||
shared_module = student_module_cache.lookup(descriptor.category,
|
||||
shared_state_key)
|
||||
|
||||
|
||||
|
||||
instance_state = instance_module.state if instance_module is not None else None
|
||||
shared_state = shared_module.state if shared_module is not None else None
|
||||
|
||||
# TODO (vshnayder): fix hardcoded urls (use reverse)
|
||||
# Setup system context for module instance
|
||||
|
||||
ajax_url = reverse('modx_dispatch',
|
||||
ajax_url = reverse('modx_dispatch',
|
||||
kwargs=dict(course_id=descriptor.location.course_id,
|
||||
id=descriptor.location.url(),
|
||||
dispatch=''),
|
||||
)
|
||||
|
||||
# ajax_url = settings.MITX_ROOT_URL + '/modx/' + descriptor.location.url() + '/'
|
||||
|
||||
# Fully qualified callback URL for external queueing system
|
||||
xqueue_callback_url = request.build_absolute_uri('/')[:-1] # Trailing slash provided by reverse
|
||||
xqueue_callback_url += reverse('xqueue_callback',
|
||||
@@ -172,11 +186,14 @@ def get_module(user, request, location, student_module_cache, position=None):
|
||||
# TODO: Queuename should be derived from 'course_settings.json' of each course
|
||||
xqueue_default_queuename = descriptor.location.org + '-' + descriptor.location.course
|
||||
|
||||
xqueue = { 'interface': qinterface,
|
||||
'callback_url': xqueue_callback_url,
|
||||
'default_queuename': xqueue_default_queuename.replace(' ','_') }
|
||||
xqueue = {'interface': xqueue_interface,
|
||||
'callback_url': xqueue_callback_url,
|
||||
'default_queuename': xqueue_default_queuename.replace(' ', '_')}
|
||||
|
||||
def _get_module(location):
|
||||
"""
|
||||
Delegate to get_module. It does an access check, so may return None
|
||||
"""
|
||||
return get_module(user, request, location,
|
||||
student_module_cache, position)
|
||||
|
||||
@@ -195,12 +212,11 @@ def get_module(user, request, location, student_module_cache, position=None):
|
||||
# a module is coming through get_html and is therefore covered
|
||||
# by the replace_static_urls code below
|
||||
replace_urls=replace_urls,
|
||||
is_staff=has_staff_access_to_location(user, location),
|
||||
node_path=settings.NODE_PATH
|
||||
)
|
||||
# pass position specified in URL to module through ModuleSystem
|
||||
system.set('position', position)
|
||||
system.set('DEBUG',settings.DEBUG)
|
||||
system.set('DEBUG', settings.DEBUG)
|
||||
|
||||
module = descriptor.xmodule_constructor(system)(instance_state, shared_state)
|
||||
|
||||
@@ -210,7 +226,7 @@ def get_module(user, request, location, student_module_cache, position=None):
|
||||
)
|
||||
|
||||
if settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF'):
|
||||
if has_staff_access_to_course(user, module.location.course):
|
||||
if has_access(user, module, 'staff'):
|
||||
module.get_html = add_histogram(module.get_html, module, user)
|
||||
|
||||
return module
|
||||
@@ -291,12 +307,17 @@ def xqueue_callback(request, course_id, userid, id, dispatch):
|
||||
# Retrieve target StudentModule
|
||||
user = User.objects.get(id=userid)
|
||||
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(user, modulestore().get_item(id))
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
|
||||
user, modulestore().get_item(id), depth=0, select_for_update=True)
|
||||
instance = get_module(user, request, id, student_module_cache)
|
||||
if instance is None:
|
||||
log.debug("No module {} for user {}--access denied?".format(id, user))
|
||||
raise Http404
|
||||
|
||||
instance_module = get_instance_module(user, instance, student_module_cache)
|
||||
|
||||
if instance_module is None:
|
||||
log.debug("Couldn't find module '%s' for user '%s'", id, user)
|
||||
log.debug("Couldn't find instance of module '%s' for user '%s'", id, user)
|
||||
raise Http404
|
||||
|
||||
oldgrade = instance_module.grade
|
||||
@@ -336,14 +357,25 @@ def modx_dispatch(request, dispatch=None, id=None, course_id=None):
|
||||
- id -- the module id. Used to look up the XModule instance
|
||||
'''
|
||||
# ''' (fix emacs broken parsing)
|
||||
# Check for submitted files
|
||||
|
||||
# Check for submitted files and basic file size checks
|
||||
p = request.POST.copy()
|
||||
if request.FILES:
|
||||
for inputfile_id in request.FILES.keys():
|
||||
p[inputfile_id] = request.FILES[inputfile_id]
|
||||
inputfile = request.FILES[inputfile_id]
|
||||
if inputfile.size > settings.STUDENT_FILEUPLOAD_MAX_SIZE: # Bytes
|
||||
file_too_big_msg = 'Submission aborted! Your file "%s" is too large (max size: %d MB)' %\
|
||||
(inputfile.name, settings.STUDENT_FILEUPLOAD_MAX_SIZE/(1000**2))
|
||||
return HttpResponse(json.dumps({'success': file_too_big_msg}))
|
||||
p[inputfile_id] = inputfile
|
||||
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(request.user, modulestore().get_item(id))
|
||||
instance = get_module(request.user, request, id, student_module_cache)
|
||||
if instance is None:
|
||||
# Either permissions just changed, or someone is trying to be clever
|
||||
# and load something they shouldn't have access to.
|
||||
log.debug("No module {} for user {}--access denied?".format(id, user))
|
||||
raise Http404
|
||||
|
||||
instance_module = get_instance_module(request.user, instance, student_module_cache)
|
||||
shared_module = get_shared_instance_module(request.user, instance, student_module_cache)
|
||||
|
||||
@@ -18,12 +18,14 @@ from override_settings import override_settings
|
||||
|
||||
import xmodule.modulestore.django
|
||||
|
||||
# Need access to internal func to put users in the right group
|
||||
from courseware.access import _course_staff_group_name
|
||||
|
||||
from student.models import Registration
|
||||
from courseware.courses import course_staff_group_name
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
|
||||
from xmodule.timeparse import stringify_time
|
||||
|
||||
def parse_json(response):
|
||||
"""Parse response, which is assumed to be json"""
|
||||
@@ -310,7 +312,7 @@ class TestViewAuth(PageLoader):
|
||||
self.check_for_get_code(404, url)
|
||||
|
||||
# Make the instructor staff in the toy course
|
||||
group_name = course_staff_group_name(self.toy)
|
||||
group_name = _course_staff_group_name(self.toy.location)
|
||||
g = Group.objects.create(name=group_name)
|
||||
g.user_set.add(user(self.instructor))
|
||||
|
||||
@@ -340,25 +342,22 @@ class TestViewAuth(PageLoader):
|
||||
|
||||
def run_wrapped(self, test):
|
||||
"""
|
||||
test.py turns off start dates. Enable them and DARK_LAUNCH.
|
||||
test.py turns off start dates. Enable them.
|
||||
Because settings is global, be careful not to mess it up for other tests
|
||||
(Can't use override_settings because we're only changing part of the
|
||||
MITX_FEATURES dict)
|
||||
"""
|
||||
oldDSD = settings.MITX_FEATURES['DISABLE_START_DATES']
|
||||
oldDL = settings.MITX_FEATURES['DARK_LAUNCH']
|
||||
|
||||
try:
|
||||
settings.MITX_FEATURES['DISABLE_START_DATES'] = False
|
||||
settings.MITX_FEATURES['DARK_LAUNCH'] = True
|
||||
test()
|
||||
finally:
|
||||
settings.MITX_FEATURES['DISABLE_START_DATES'] = oldDSD
|
||||
settings.MITX_FEATURES['DARK_LAUNCH'] = oldDL
|
||||
|
||||
|
||||
def test_dark_launch(self):
|
||||
"""Make sure that when dark launch is on, students can't access course
|
||||
"""Make sure that before course start, students can't access course
|
||||
pages, but instructors can"""
|
||||
self.run_wrapped(self._do_test_dark_launch)
|
||||
|
||||
@@ -372,13 +371,12 @@ class TestViewAuth(PageLoader):
|
||||
|
||||
# Make courses start in the future
|
||||
tomorrow = time.time() + 24*3600
|
||||
self.toy.start = self.toy.metadata['start'] = time.gmtime(tomorrow)
|
||||
self.full.start = self.full.metadata['start'] = time.gmtime(tomorrow)
|
||||
self.toy.metadata['start'] = stringify_time(time.gmtime(tomorrow))
|
||||
self.full.metadata['start'] = stringify_time(time.gmtime(tomorrow))
|
||||
|
||||
self.assertFalse(self.toy.has_started())
|
||||
self.assertFalse(self.full.has_started())
|
||||
self.assertFalse(settings.MITX_FEATURES['DISABLE_START_DATES'])
|
||||
self.assertTrue(settings.MITX_FEATURES['DARK_LAUNCH'])
|
||||
|
||||
def reverse_urls(names, course):
|
||||
"""Reverse a list of course urls"""
|
||||
@@ -444,7 +442,7 @@ class TestViewAuth(PageLoader):
|
||||
|
||||
print '=== Testing course instructor access....'
|
||||
# Make the instructor staff in the toy course
|
||||
group_name = course_staff_group_name(self.toy)
|
||||
group_name = _course_staff_group_name(self.toy.location)
|
||||
g = Group.objects.create(name=group_name)
|
||||
g.user_set.add(user(self.instructor))
|
||||
|
||||
@@ -494,7 +492,7 @@ class TestViewAuth(PageLoader):
|
||||
|
||||
print '=== Testing course instructor access....'
|
||||
# Make the instructor staff in the toy course
|
||||
group_name = course_staff_group_name(self.toy)
|
||||
group_name = _course_staff_group_name(self.toy.location)
|
||||
g = Group.objects.create(name=group_name)
|
||||
g.user_set.add(user(self.instructor))
|
||||
|
||||
|
||||
@@ -15,21 +15,19 @@ from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
from django.views.decorators.cache import cache_control
|
||||
|
||||
from module_render import toc_for_course, get_module, get_section
|
||||
from models import StudentModuleCache
|
||||
from student.models import UserProfile
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.search import path_to_location
|
||||
from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
|
||||
from util.cache import cache, cache_if_anonymous
|
||||
from student.models import UserTestGroup, CourseEnrollment
|
||||
from courseware import grades
|
||||
from courseware.courses import (check_course, get_courses_by_university,
|
||||
has_staff_access_to_course_id)
|
||||
|
||||
from courseware.access import has_access
|
||||
from courseware.courses import (get_course_with_access, get_courses_by_university)
|
||||
from models import StudentModuleCache
|
||||
from module_render import toc_for_course, get_module, get_section
|
||||
from student.models import UserProfile
|
||||
from student.models import UserTestGroup, CourseEnrollment
|
||||
from util.cache import cache, cache_if_anonymous
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem
|
||||
from xmodule.modulestore.search import path_to_location
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
@@ -93,7 +91,8 @@ def render_accordion(request, course, chapter, section):
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def index(request, course_id, chapter=None, section=None,
|
||||
position=None):
|
||||
''' Displays courseware accordion, and any associated content.
|
||||
"""
|
||||
Displays courseware accordion, and any associated content.
|
||||
If course, chapter, and section aren't all specified, just returns
|
||||
the accordion. If they are specified, returns an error if they don't
|
||||
point to a valid module.
|
||||
@@ -109,8 +108,8 @@ def index(request, course_id, chapter=None, section=None,
|
||||
Returns:
|
||||
|
||||
- HTTPresponse
|
||||
'''
|
||||
course = check_course(request.user, course_id)
|
||||
"""
|
||||
course = get_course_with_access(request.user, course_id, 'load')
|
||||
registered = registered_for_course(course, request.user)
|
||||
if not registered:
|
||||
# TODO (vshnayder): do course instructors need to be registered to see course?
|
||||
@@ -137,6 +136,10 @@ def index(request, course_id, chapter=None, section=None,
|
||||
module = get_module(request.user, request,
|
||||
section_descriptor.location,
|
||||
student_module_cache)
|
||||
if module is None:
|
||||
# User is probably being clever and trying to access something
|
||||
# they don't have access to.
|
||||
raise Http404
|
||||
context['content'] = module.get_html()
|
||||
else:
|
||||
log.warning("Couldn't find a section descriptor for course_id '{0}',"
|
||||
@@ -204,7 +207,7 @@ def course_info(request, course_id):
|
||||
|
||||
Assumes the course_id is in a valid format.
|
||||
"""
|
||||
course = check_course(request.user, course_id)
|
||||
course = get_course_with_access(request.user, course_id, 'load')
|
||||
|
||||
return render_to_response('info.html', {'course': course})
|
||||
|
||||
@@ -221,7 +224,7 @@ def registered_for_course(course, user):
|
||||
@ensure_csrf_cookie
|
||||
@cache_if_anonymous
|
||||
def course_about(request, course_id):
|
||||
course = check_course(request.user, course_id, course_must_be_open=False)
|
||||
course = get_course_with_access(request.user, course_id, 'see_exists')
|
||||
registered = registered_for_course(course, request.user)
|
||||
return render_to_response('portal/course_about.html', {'course': course, 'registered': registered})
|
||||
|
||||
@@ -253,14 +256,14 @@ def profile(request, course_id, student_id=None):
|
||||
|
||||
Course staff are allowed to see the profiles of students in their class.
|
||||
"""
|
||||
course = check_course(request.user, course_id)
|
||||
course = get_course_with_access(request.user, course_id, 'load')
|
||||
|
||||
if student_id is None or student_id == request.user.id:
|
||||
# always allowed to see your own profile
|
||||
student = request.user
|
||||
else:
|
||||
# Requesting access to a different student's profile
|
||||
if not has_staff_access_to_course_id(request.user, course_id):
|
||||
if not has_access(request.user, course, 'staff'):
|
||||
raise Http404
|
||||
student = User.objects.get(id=int(student_id))
|
||||
|
||||
@@ -297,10 +300,7 @@ def gradebook(request, course_id):
|
||||
- only displayed to course staff
|
||||
- shows students who are enrolled.
|
||||
"""
|
||||
if not has_staff_access_to_course_id(request.user, course_id):
|
||||
raise Http404
|
||||
|
||||
course = check_course(request.user, course_id)
|
||||
course = get_course_with_access(request.user, course_id, 'staff')
|
||||
|
||||
enrolled_students = User.objects.filter(courseenrollment__course_id=course_id).order_by('username')
|
||||
|
||||
@@ -322,10 +322,7 @@ def gradebook(request, course_id):
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def grade_summary(request, course_id):
|
||||
"""Display the grade summary for a course."""
|
||||
if not has_staff_access_to_course_id(request.user, course_id):
|
||||
raise Http404
|
||||
|
||||
course = check_course(request.user, course_id)
|
||||
course = get_course_with_access(request.user, course_id, 'staff')
|
||||
|
||||
# For now, just a static page
|
||||
context = {'course': course }
|
||||
@@ -335,10 +332,7 @@ def grade_summary(request, course_id):
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def instructor_dashboard(request, course_id):
|
||||
"""Display the instructor dashboard for a course."""
|
||||
if not has_staff_access_to_course_id(request.user, course_id):
|
||||
raise Http404
|
||||
|
||||
course = check_course(request.user, course_id)
|
||||
course = get_course_with_access(request.user, course_id, 'staff')
|
||||
|
||||
# For now, just a static page
|
||||
context = {'course': course }
|
||||
|
||||
@@ -9,7 +9,7 @@ from django.utils import simplejson
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from mitxmako.shortcuts import render_to_response
|
||||
|
||||
from courseware.courses import check_course
|
||||
from courseware.courses import get_opt_course_with_access
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
@@ -51,7 +51,7 @@ def update_template_dictionary(dictionary, request=None, course=None, article=No
|
||||
|
||||
|
||||
def view(request, article_path, course_id=None):
|
||||
course = check_course(request.user, course_id, course_required=False)
|
||||
course = get_opt_course_with_access(request.user, course_id, 'load')
|
||||
|
||||
(article, err) = get_article(request, article_path, course)
|
||||
if err:
|
||||
@@ -67,7 +67,7 @@ def view(request, article_path, course_id=None):
|
||||
|
||||
|
||||
def view_revision(request, revision_number, article_path, course_id=None):
|
||||
course = check_course(request.user, course_id, course_required=False)
|
||||
course = get_opt_course_with_access(request.user, course_id, 'load')
|
||||
|
||||
(article, err) = get_article(request, article_path, course)
|
||||
if err:
|
||||
@@ -91,7 +91,7 @@ def view_revision(request, revision_number, article_path, course_id=None):
|
||||
|
||||
|
||||
def root_redirect(request, course_id=None):
|
||||
course = check_course(request.user, course_id, course_required=False)
|
||||
course = get_opt_course_with_access(request.user, course_id, 'load')
|
||||
|
||||
#TODO: Add a default namespace to settings.
|
||||
namespace = course.wiki_namespace if course else "edX"
|
||||
@@ -109,7 +109,7 @@ def root_redirect(request, course_id=None):
|
||||
|
||||
|
||||
def create(request, article_path, course_id=None):
|
||||
course = check_course(request.user, course_id, course_required=False)
|
||||
course = get_opt_course_with_access(request.user, course_id, 'load')
|
||||
|
||||
article_path_components = article_path.split('/')
|
||||
|
||||
@@ -170,7 +170,7 @@ def create(request, article_path, course_id=None):
|
||||
|
||||
|
||||
def edit(request, article_path, course_id=None):
|
||||
course = check_course(request.user, course_id, course_required=False)
|
||||
course = get_opt_course_with_access(request.user, course_id, 'load')
|
||||
|
||||
(article, err) = get_article(request, article_path, course)
|
||||
if err:
|
||||
@@ -218,7 +218,7 @@ def edit(request, article_path, course_id=None):
|
||||
|
||||
|
||||
def history(request, article_path, page=1, course_id=None):
|
||||
course = check_course(request.user, course_id, course_required=False)
|
||||
course = get_opt_course_with_access(request.user, course_id, 'load')
|
||||
|
||||
(article, err) = get_article(request, article_path, course)
|
||||
if err:
|
||||
@@ -300,7 +300,7 @@ def history(request, article_path, page=1, course_id=None):
|
||||
|
||||
|
||||
def revision_feed(request, page=1, namespace=None, course_id=None):
|
||||
course = check_course(request.user, course_id, course_required=False)
|
||||
course = get_opt_course_with_access(request.user, course_id, 'load')
|
||||
|
||||
page_size = 10
|
||||
|
||||
@@ -333,7 +333,7 @@ def revision_feed(request, page=1, namespace=None, course_id=None):
|
||||
|
||||
|
||||
def search_articles(request, namespace=None, course_id=None):
|
||||
course = check_course(request.user, course_id, course_required=False)
|
||||
course = get_opt_course_with_access(request.user, course_id, 'load')
|
||||
|
||||
# blampe: We should check for the presence of other popular django search
|
||||
# apps and use those if possible. Only fall back on this as a last resort.
|
||||
@@ -382,7 +382,7 @@ def search_articles(request, namespace=None, course_id=None):
|
||||
|
||||
|
||||
def search_add_related(request, course_id, slug, namespace):
|
||||
course = check_course(request.user, course_id, course_required=False)
|
||||
course = get_opt_course_with_access(request.user, course_id, 'load')
|
||||
|
||||
(article, err) = get_article(request, slug, namespace if namespace else course_id)
|
||||
if err:
|
||||
@@ -415,7 +415,7 @@ def search_add_related(request, course_id, slug, namespace):
|
||||
|
||||
|
||||
def add_related(request, course_id, slug, namespace):
|
||||
course = check_course(request.user, course_id, course_required=False)
|
||||
course = get_opt_course_with_access(request.user, course_id, 'load')
|
||||
|
||||
(article, err) = get_article(request, slug, namespace if namespace else course_id)
|
||||
if err:
|
||||
@@ -439,7 +439,7 @@ def add_related(request, course_id, slug, namespace):
|
||||
|
||||
|
||||
def remove_related(request, course_id, namespace, slug, related_id):
|
||||
course = check_course(request.user, course_id, course_required=False)
|
||||
course = get_opt_course_with_access(request.user, course_id, 'load')
|
||||
|
||||
(article, err) = get_article(request, slug, namespace if namespace else course_id)
|
||||
|
||||
@@ -462,7 +462,7 @@ def remove_related(request, course_id, namespace, slug, related_id):
|
||||
|
||||
|
||||
def random_article(request, course_id=None):
|
||||
course = check_course(request.user, course_id, course_required=False)
|
||||
course = get_opt_course_with_access(request.user, course_id, 'load')
|
||||
|
||||
from random import randint
|
||||
num_arts = Article.objects.count()
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from mitxmako.shortcuts import render_to_response
|
||||
|
||||
from courseware.courses import check_course
|
||||
from courseware.courses import get_course_with_access
|
||||
from lxml import etree
|
||||
|
||||
@login_required
|
||||
def index(request, course_id, page=0):
|
||||
course = check_course(request.user, course_id)
|
||||
course = get_course_with_access(request.user, course_id, 'load')
|
||||
raw_table_of_contents = open('lms/templates/book_toc.xml', 'r') # TODO: This will need to come from S3
|
||||
table_of_contents = etree.parse(raw_table_of_contents).getroot()
|
||||
return render_to_response('staticbook.html', {'page': int(page), 'course': course, 'table_of_contents': table_of_contents})
|
||||
return render_to_response('staticbook.html',
|
||||
{'page': int(page), 'course': course,
|
||||
'table_of_contents': table_of_contents})
|
||||
|
||||
|
||||
def index_shifted(request, course_id, page):
|
||||
|
||||
Reference in New Issue
Block a user