Merge remote-tracking branch 'origin/master' into feature/bridger/new_wiki
This commit is contained in:
@@ -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)}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
|
||||
19
common/lib/xmodule/xmodule/timeparse.py
Normal file
19
common/lib/xmodule/xmodule/timeparse.py
Normal 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)
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user