Make start dates work for all modules
* error modules now hidden via access control * get_module() returns None if user doesn't have access
This commit is contained in:
@@ -49,9 +49,9 @@ class ABTestModule(XModule):
|
||||
return json.dumps({'group': self.group})
|
||||
|
||||
def displayable_items(self):
|
||||
return [self.system.get_module(child)
|
||||
for child
|
||||
in self.definition['data']['group_content'][self.group]]
|
||||
return filter(None, [self.system.get_module(child)
|
||||
for child
|
||||
in self.definition['data']['group_content'][self.group]])
|
||||
|
||||
|
||||
# TODO (cpennington): Use Groups should be a first class object, rather than being
|
||||
|
||||
@@ -6,7 +6,7 @@ from xmodule.util.decorators import lazyproperty
|
||||
from xmodule.graders import load_grading_policy
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.seq_module import SequenceDescriptor, SequenceModule
|
||||
from xmodule.timeparse import parse_time
|
||||
from xmodule.timeparse import parse_time, stringify_time
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -18,16 +18,10 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
super(CourseDescriptor, self).__init__(system, definition, **kwargs)
|
||||
|
||||
msg = None
|
||||
try:
|
||||
self.start = parse_time(self.metadata["start"])
|
||||
except KeyError:
|
||||
msg = "Course loaded without a start date. id = %s" % self.id
|
||||
except ValueError as e:
|
||||
msg = "Course loaded with a bad start date. %s '%s'" % (self.id, e)
|
||||
|
||||
# Don't call the tracker from the exception handler.
|
||||
if msg is not None:
|
||||
self.start = time.gmtime(0) # The epoch
|
||||
if self.start is None:
|
||||
msg = "Course loaded without a valid start date. id = %s" % self.id
|
||||
# hack it -- start in 1970
|
||||
self.metadata['start'] = stringify_time(time.gmtime(0))
|
||||
log.critical(msg)
|
||||
system.error_tracker(msg)
|
||||
|
||||
|
||||
@@ -24,16 +24,8 @@ class ErrorModule(XModule):
|
||||
return self.system.render_template('module-error.html', {
|
||||
'data' : self.definition['data']['contents'],
|
||||
'error' : self.definition['data']['error_msg'],
|
||||
'is_staff' : self.system.is_staff,
|
||||
})
|
||||
|
||||
def displayable_items(self):
|
||||
"""Hide errors in the profile and table of contents for non-staff
|
||||
users.
|
||||
"""
|
||||
if self.system.is_staff:
|
||||
return [self]
|
||||
return []
|
||||
|
||||
class ErrorDescriptor(EditingDescriptor):
|
||||
"""
|
||||
|
||||
@@ -3,9 +3,17 @@ Helper functions for handling time in the format we like.
|
||||
"""
|
||||
import time
|
||||
|
||||
TIME_FORMAT = "%Y-%m-%dT%H:%M"
|
||||
|
||||
def parse_time(time_str):
|
||||
"""
|
||||
Takes a time string in our format ("%Y-%m-%dT%H:%M"), and returns
|
||||
Takes a time string in TIME_FORMAT, returns
|
||||
it as a time_struct. Raises ValueError if the string is not in the right format.
|
||||
"""
|
||||
return time.strptime(time_str, "%Y-%m-%dT%H:%M")
|
||||
return time.strptime(time_str, TIME_FORMAT)
|
||||
|
||||
def stringify_time(time_struct):
|
||||
"""
|
||||
Convert a time struct to a string
|
||||
"""
|
||||
return time.strftime(TIME_FORMAT, time_struct)
|
||||
|
||||
@@ -219,9 +219,11 @@ class XModule(HTMLSnippet):
|
||||
Return module instances for all the children of this module.
|
||||
'''
|
||||
if self._loaded_children is None:
|
||||
self._loaded_children = [
|
||||
self.system.get_module(child)
|
||||
for child in self.definition.get('children', [])]
|
||||
# get_module returns None if the current user doesn't have access
|
||||
# to the location.
|
||||
self._loaded_children = filter(None,
|
||||
[self.system.get_module(child)
|
||||
for child in self.definition.get('children', [])])
|
||||
|
||||
return self._loaded_children
|
||||
|
||||
@@ -385,9 +387,6 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
|
||||
self.category = self.location.category
|
||||
self.shared_state_key = kwargs.get('shared_state_key')
|
||||
|
||||
# look for a start time, setting to None if not present
|
||||
self.start = self._try_parse_time('start')
|
||||
|
||||
self._child_instances = None
|
||||
self._inherited_metadata = set()
|
||||
|
||||
@@ -400,6 +399,15 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
|
||||
return self.metadata.get('display_name',
|
||||
self.url_name.replace('_', ' '))
|
||||
|
||||
@property
|
||||
def start(self):
|
||||
"""
|
||||
If self.metadata contains start, return it. Else return None.
|
||||
"""
|
||||
if 'start' not in self.metadata:
|
||||
return None
|
||||
return self._try_parse_time('start')
|
||||
|
||||
@property
|
||||
def own_metadata(self):
|
||||
"""
|
||||
@@ -609,7 +617,7 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
|
||||
"""
|
||||
if key in self.metadata:
|
||||
try:
|
||||
parse_time(self.metadata[key])
|
||||
return parse_time(self.metadata[key])
|
||||
except ValueError as e:
|
||||
msg = "Descriptor {} loaded with a bad metadata key '{}': '{}'".format(
|
||||
self.location.url(), self.metadata[key], e)
|
||||
@@ -709,7 +717,8 @@ class ModuleSystem(object):
|
||||
files. Update or remove.
|
||||
|
||||
get_module - function that takes (location) and returns a corresponding
|
||||
module instance object.
|
||||
module instance object. If the current user does not have
|
||||
access to that location, returns None.
|
||||
|
||||
render_template - a function that takes (template_file, context), and
|
||||
returns rendered html.
|
||||
|
||||
@@ -8,6 +8,7 @@ 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
|
||||
@@ -49,6 +50,10 @@ def has_access(user, obj, action):
|
||||
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)
|
||||
|
||||
@@ -94,6 +99,7 @@ def _has_access_course_desc(user, course, action):
|
||||
|
||||
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
|
||||
@@ -115,6 +121,7 @@ def _has_access_course_desc(user, course, action):
|
||||
# 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)
|
||||
|
||||
@@ -130,6 +137,25 @@ def _has_access_course_desc(user, course, action):
|
||||
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.
|
||||
@@ -145,6 +171,7 @@ def _has_access_descriptor(user, descriptor, action):
|
||||
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
|
||||
@@ -152,11 +179,13 @@ def _has_access_descriptor(user, descriptor, action):
|
||||
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 = {
|
||||
@@ -212,7 +241,9 @@ def _dispatch(table, action, user, obj):
|
||||
result = table[action]()
|
||||
debug("%s user %s, object %s, action %s",
|
||||
'ALLOWED' if result else 'DENIED',
|
||||
user, obj, action)
|
||||
user,
|
||||
obj.location.url() if isinstance(obj, XModuleDescriptor) else str(obj)[:60],
|
||||
action)
|
||||
return result
|
||||
|
||||
raise ValueError("Unknown action for object type '{}': '{}'".format(
|
||||
@@ -239,15 +270,19 @@ def _has_staff_access_to_location(user, location):
|
||||
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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -65,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)
|
||||
@@ -144,6 +147,11 @@ def get_module(user, request, location, student_module_cache, position=None):
|
||||
location = Location(location)
|
||||
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
|
||||
@@ -192,6 +200,9 @@ def get_module(user, request, location, student_module_cache, position=None):
|
||||
'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)
|
||||
|
||||
@@ -308,10 +319,14 @@ def xqueue_callback(request, course_id, userid, id, dispatch):
|
||||
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(user, modulestore().get_item(id))
|
||||
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
|
||||
@@ -365,6 +380,11 @@ def modx_dispatch(request, dispatch=None, id=None, course_id=None):
|
||||
|
||||
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)
|
||||
|
||||
@@ -25,7 +25,7 @@ from student.models import Registration
|
||||
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"""
|
||||
@@ -371,8 +371,8 @@ 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())
|
||||
|
||||
@@ -136,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}',"
|
||||
|
||||
@@ -2,12 +2,10 @@
|
||||
<h1>There has been an error on the <em>MITx</em> servers</h1>
|
||||
<p>We're sorry, this module is temporarily unavailable. Our staff is working to fix it as soon as possible. Please email us at <a href="mailto:technical@mitx.mit.edu">technical@mitx.mit.edu</a> to report any problems or downtime.</p>
|
||||
|
||||
% if is_staff:
|
||||
<h1>Staff-only details below:</h1>
|
||||
<h1>Details below:</h1>
|
||||
|
||||
<p>Error: ${error | h}</p>
|
||||
|
||||
<p>Raw data: ${data | h}</p>
|
||||
% endif
|
||||
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user