Merge remote-tracking branch 'origin/master' into feature/bridger/new_wiki

This commit is contained in:
Bridger Maxwell
2012-08-15 11:15:21 -04:00
61 changed files with 1084 additions and 397 deletions

View File

@@ -38,8 +38,8 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
from datetime import date
from collections import namedtuple
from courseware.courses import (course_staff_group_name, has_staff_access_to_course,
get_courses_by_university)
from courseware.courses import get_courses_by_university
from courseware.access import has_access
log = logging.getLogger("mitx.student")
Article = namedtuple('Article', 'title url author image deck publication publish_date')
@@ -166,22 +166,6 @@ def change_enrollment_view(request):
"""Delegate to change_enrollment to actually do the work."""
return HttpResponse(json.dumps(change_enrollment(request)))
def enrollment_allowed(user, course):
"""If the course has an enrollment period, check whether we are in it.
Also respects the DARK_LAUNCH setting"""
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.
return True
if settings.MITX_FEATURES['DARK_LAUNCH']:
if has_staff_access_to_course(user, course):
# if dark launch, staff can enroll outside enrollment window
return True
return False
def change_enrollment(request):
@@ -209,18 +193,7 @@ def change_enrollment(request):
.format(user.username, enrollment.course_id))
return {'success': False, 'error': 'The course requested does not exist.'}
if settings.MITX_FEATURES.get('ACCESS_REQUIRE_STAFF_FOR_COURSE'):
# require that user be in the staff_* group (or be an
# overall admin) to be able to enroll eg staff_6.002x or
# staff_6.00x
if not has_staff_access_to_course(user, course):
staff_group = course_staff_group_name(course)
log.debug('user %s denied enrollment to %s ; not in %s' % (
user, course.location.url(), staff_group))
return {'success': False,
'error' : '%s membership required to access course.' % staff_group}
if not enrollment_allowed(user, course):
if not has_access(user, course, 'enroll'):
return {'success': False,
'error': 'enrollment in {} not allowed at this time'
.format(course.display_name)}

View File

@@ -7,13 +7,10 @@ import logging
import requests
import time
# TODO: Collection of parameters to be hooked into rest of edX system
XQUEUE_LMS_AUTH = { 'username': 'LMS',
'password': 'PaloAltoCA' }
XQUEUE_URL = 'http://xqueue.edx.org'
log = logging.getLogger('mitx.' + __name__)
def make_hashkey(seed=None):
'''
Generate a string key by hashing
@@ -58,15 +55,15 @@ def parse_xreply(xreply):
return (return_code, content)
class XqueueInterface:
class XQueueInterface(object):
'''
Interface to the external grading system
'''
def __init__(self, url=XQUEUE_URL, auth=XQUEUE_LMS_AUTH):
def __init__(self, url, django_auth, requests_auth=None):
self.url = url
self.auth = auth
self.session = requests.session()
self.auth = django_auth
self.session = requests.session(auth=requests_auth)
def send_to_queue(self, header, body, file_to_upload=None):
'''
@@ -117,5 +114,3 @@ class XqueueInterface:
return (1, 'unexpected HTTP status code [%d]' % r.status_code)
return parse_xreply(r.text)
qinterface = XqueueInterface()

View File

@@ -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

View File

@@ -1,12 +1,12 @@
from fs.errors import ResourceNotFoundError
import time
import dateutil.parser
import logging
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, stringify_time
log = logging.getLogger(__name__)
@@ -18,38 +18,15 @@ class CourseDescriptor(SequenceDescriptor):
super(CourseDescriptor, self).__init__(system, definition, **kwargs)
msg = None
try:
self.start = time.strptime(self.metadata["start"], "%Y-%m-%dT%H:%M")
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)
def try_parse_time(key):
"""
Parse an optional metadata key: if present, must be valid.
Return None if not present.
"""
if key in self.metadata:
try:
return time.strptime(self.metadata[key], "%Y-%m-%dT%H:%M")
except ValueError as e:
msg = "Course %s loaded with a bad metadata key %s '%s'" % (
self.id, self.metadata[key], e)
log.warning(msg)
return None
self.enrollment_start = try_parse_time("enrollment_start")
self.enrollment_end = try_parse_time("enrollment_end")
self.enrollment_start = self._try_parse_time("enrollment_start")
self.enrollment_end = self._try_parse_time("enrollment_end")
def has_started(self):
return time.gmtime() > self.start
@@ -154,6 +131,7 @@ class CourseDescriptor(SequenceDescriptor):
@property
def id(self):
"""Return the course_id for this course"""
return self.location_to_id(self.location)
@property

View File

@@ -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):
"""

View File

@@ -32,22 +32,37 @@ class @Problem
queueing: =>
@queued_items = @$(".xqueue")
if @queued_items.length > 0
@num_queued_items = @queued_items.length
if @num_queued_items > 0
if window.queuePollerID # Only one poller 'thread' per Problem
window.clearTimeout(window.queuePollerID)
window.queuePollerID = window.setTimeout(@poll, 100)
queuelen = @get_queuelen()
window.queuePollerID = window.setTimeout(@poll, queuelen*10)
# Retrieves the minimum queue length of all queued items
get_queuelen: =>
minlen = Infinity
@queued_items.each (index, qitem) ->
len = parseInt($.text(qitem))
if len < minlen
minlen = len
return minlen
poll: =>
$.postWithPrefix "#{@url}/problem_get", (response) =>
@queued_items = $(response.html).find(".xqueue")
if @queued_items.length == 0
# If queueing status changed, then render
@new_queued_items = $(response.html).find(".xqueue")
if @new_queued_items.length isnt @num_queued_items
@el.html(response.html)
@executeProblemScripts () =>
@setupInputTypes()
@bind()
@num_queued_items = @new_queued_items.length
if @num_queued_items == 0
delete window.queuePollerID
else
# TODO: Dynamically adjust timeout interval based on @queued_items.value
# TODO: Some logic to dynamically adjust polling rate based on queuelen
window.queuePollerID = window.setTimeout(@poll, 1000)
render: (content) ->
@@ -141,9 +156,16 @@ class @Problem
fd = new FormData()
# Sanity check of file size
file_too_large = false
max_filesize = 4*1000*1000 # 4 MB
@inputs.each (index, element) ->
if element.type is 'file'
if element.files[0] instanceof File
if element.files[0].size > max_filesize
file_too_large = true
alert 'Submission aborted! Your file "' + element.files[0].name + '" is too large (max size: ' + max_filesize/(1000*1000) + ' MB)'
fd.append(element.id, element.files[0])
else
fd.append(element.id, '')
@@ -163,7 +185,8 @@ class @Problem
else
alert(response.success)
$.ajaxWithPrefix("#{@url}/problem_check", settings)
if not file_too_large
$.ajaxWithPrefix("#{@url}/problem_check", settings)
check: =>
Logger.log 'problem_check', @answers

View File

@@ -35,7 +35,6 @@ i4xs = ModuleSystem(
filestore=fs.osfs.OSFS(os.path.dirname(os.path.realpath(__file__))+"/test_files"),
debug=True,
xqueue={'interface':None, 'callback_url':'/', 'default_queuename': 'testqueue'},
is_staff=False,
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules")
)
@@ -336,7 +335,7 @@ class CodeResponseTest(unittest.TestCase):
self.assertFalse(test_lcp.correct_map.is_queued(answer_ids[j])) # Should be dequeued, message delivered
else:
self.assertTrue(test_lcp.correct_map.is_queued(answer_ids[j])) # Should be queued, message undelivered
def test_convert_files_to_filenames(self):
problem_file = os.path.dirname(__file__) + "/test_files/coderesponse.xml"
fp = open(problem_file)
@@ -347,7 +346,7 @@ class CodeResponseTest(unittest.TestCase):
self.assertEquals(answers_converted['1_2_1'], 'String-based answer')
self.assertEquals(answers_converted['1_3_1'], ['answer1', 'answer2', 'answer3'])
self.assertEquals(answers_converted['1_4_1'], fp.name)
class ChoiceResponseTest(unittest.TestCase):

View File

@@ -0,0 +1,19 @@
"""
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 TIME_FORMAT, returns
it as a time_struct. Raises ValueError if the string is not in the right format.
"""
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)

View File

@@ -8,8 +8,9 @@ from lxml import etree
from lxml.etree import XMLSyntaxError
from pprint import pprint
from xmodule.modulestore import Location
from xmodule.errortracker import exc_info_to_str
from xmodule.modulestore import Location
from xmodule.timeparse import parse_time
log = logging.getLogger('mitx.' + __name__)
@@ -218,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
@@ -396,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):
"""
@@ -596,6 +608,24 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
metadata=self.metadata
))
# ================================ Internal helpers =======================
def _try_parse_time(self, key):
"""
Parse an optional metadata key containing a time: if present, complain
if it doesn't parse.
Return None if not present or invalid.
"""
if key in self.metadata:
try:
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)
log.warning(msg)
return None
class DescriptorSystem(object):
def __init__(self, load_item, resources_fs, error_tracker, **kwargs):
@@ -675,7 +705,6 @@ class ModuleSystem(object):
filestore=None,
debug=False,
xqueue=None,
is_staff=False,
node_path=""):
'''
Create a closure around the system environment.
@@ -688,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.
@@ -705,9 +735,6 @@ class ModuleSystem(object):
replace_urls - TEMPORARY - A function like static_replace.replace_urls
that capa_module can use to fix up the static urls in
ajax results.
is_staff - Is the user making the request a staff user?
TODO (vshnayder): this will need to change once we have real user roles.
'''
self.ajax_url = ajax_url
self.xqueue = xqueue
@@ -718,7 +745,6 @@ class ModuleSystem(object):
self.DEBUG = self.debug = debug
self.seed = user.id if user is not None else 0
self.replace_urls = replace_urls
self.is_staff = is_staff
self.node_path = node_path
def get(self, attr):