Merge remote-tracking branch 'origin/master' into feature/bridger/new_wiki
@@ -215,9 +215,6 @@ def preview_module_system(request, preview_id, descriptor):
|
|||||||
render_template=render_from_lms,
|
render_template=render_from_lms,
|
||||||
debug=True,
|
debug=True,
|
||||||
replace_urls=replace_urls,
|
replace_urls=replace_urls,
|
||||||
# TODO (vshnayder): All CMS users get staff view by default
|
|
||||||
# is that what we want?
|
|
||||||
is_staff=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,16 @@ TEMPLATE_CONTEXT_PROCESSORS = (
|
|||||||
################################# Jasmine ###################################
|
################################# Jasmine ###################################
|
||||||
JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee'
|
JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee'
|
||||||
|
|
||||||
|
|
||||||
|
#################### CAPA External Code Evaluation #############################
|
||||||
|
XQUEUE_INTERFACE = {
|
||||||
|
'url': 'http://localhost:8888',
|
||||||
|
'django_auth': {'username': 'local',
|
||||||
|
'password': 'local'},
|
||||||
|
'basic_auth': None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
################################# Middleware ###################################
|
################################# Middleware ###################################
|
||||||
# List of finder classes that know how to find static files in
|
# List of finder classes that know how to find static files in
|
||||||
# various locations.
|
# various locations.
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
|
|||||||
|
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from courseware.courses import (course_staff_group_name, has_staff_access_to_course,
|
from courseware.courses import get_courses_by_university
|
||||||
get_courses_by_university)
|
from courseware.access import has_access
|
||||||
|
|
||||||
log = logging.getLogger("mitx.student")
|
log = logging.getLogger("mitx.student")
|
||||||
Article = namedtuple('Article', 'title url author image deck publication publish_date')
|
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."""
|
"""Delegate to change_enrollment to actually do the work."""
|
||||||
return HttpResponse(json.dumps(change_enrollment(request)))
|
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):
|
def change_enrollment(request):
|
||||||
@@ -209,18 +193,7 @@ def change_enrollment(request):
|
|||||||
.format(user.username, enrollment.course_id))
|
.format(user.username, enrollment.course_id))
|
||||||
return {'success': False, 'error': 'The course requested does not exist.'}
|
return {'success': False, 'error': 'The course requested does not exist.'}
|
||||||
|
|
||||||
if settings.MITX_FEATURES.get('ACCESS_REQUIRE_STAFF_FOR_COURSE'):
|
if not has_access(user, course, 'enroll'):
|
||||||
# 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):
|
|
||||||
return {'success': False,
|
return {'success': False,
|
||||||
'error': 'enrollment in {} not allowed at this time'
|
'error': 'enrollment in {} not allowed at this time'
|
||||||
.format(course.display_name)}
|
.format(course.display_name)}
|
||||||
|
|||||||
@@ -7,13 +7,10 @@ import logging
|
|||||||
import requests
|
import requests
|
||||||
import time
|
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__)
|
log = logging.getLogger('mitx.' + __name__)
|
||||||
|
|
||||||
|
|
||||||
def make_hashkey(seed=None):
|
def make_hashkey(seed=None):
|
||||||
'''
|
'''
|
||||||
Generate a string key by hashing
|
Generate a string key by hashing
|
||||||
@@ -58,15 +55,15 @@ def parse_xreply(xreply):
|
|||||||
return (return_code, content)
|
return (return_code, content)
|
||||||
|
|
||||||
|
|
||||||
class XqueueInterface:
|
class XQueueInterface(object):
|
||||||
'''
|
'''
|
||||||
Interface to the external grading system
|
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.url = url
|
||||||
self.auth = auth
|
self.auth = django_auth
|
||||||
self.session = requests.session()
|
self.session = requests.session(auth=requests_auth)
|
||||||
|
|
||||||
def send_to_queue(self, header, body, file_to_upload=None):
|
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 (1, 'unexpected HTTP status code [%d]' % r.status_code)
|
||||||
|
|
||||||
return parse_xreply(r.text)
|
return parse_xreply(r.text)
|
||||||
|
|
||||||
qinterface = XqueueInterface()
|
|
||||||
|
|||||||
@@ -49,9 +49,9 @@ class ABTestModule(XModule):
|
|||||||
return json.dumps({'group': self.group})
|
return json.dumps({'group': self.group})
|
||||||
|
|
||||||
def displayable_items(self):
|
def displayable_items(self):
|
||||||
return [self.system.get_module(child)
|
return filter(None, [self.system.get_module(child)
|
||||||
for child
|
for child
|
||||||
in self.definition['data']['group_content'][self.group]]
|
in self.definition['data']['group_content'][self.group]])
|
||||||
|
|
||||||
|
|
||||||
# TODO (cpennington): Use Groups should be a first class object, rather than being
|
# TODO (cpennington): Use Groups should be a first class object, rather than being
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
from fs.errors import ResourceNotFoundError
|
from fs.errors import ResourceNotFoundError
|
||||||
import time
|
import time
|
||||||
import dateutil.parser
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from xmodule.util.decorators import lazyproperty
|
from xmodule.util.decorators import lazyproperty
|
||||||
from xmodule.graders import load_grading_policy
|
from xmodule.graders import load_grading_policy
|
||||||
from xmodule.modulestore import Location
|
from xmodule.modulestore import Location
|
||||||
from xmodule.seq_module import SequenceDescriptor, SequenceModule
|
from xmodule.seq_module import SequenceDescriptor, SequenceModule
|
||||||
|
from xmodule.timeparse import parse_time, stringify_time
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -18,38 +18,15 @@ class CourseDescriptor(SequenceDescriptor):
|
|||||||
super(CourseDescriptor, self).__init__(system, definition, **kwargs)
|
super(CourseDescriptor, self).__init__(system, definition, **kwargs)
|
||||||
|
|
||||||
msg = None
|
msg = None
|
||||||
try:
|
if self.start is None:
|
||||||
self.start = time.strptime(self.metadata["start"], "%Y-%m-%dT%H:%M")
|
msg = "Course loaded without a valid start date. id = %s" % self.id
|
||||||
except KeyError:
|
# hack it -- start in 1970
|
||||||
msg = "Course loaded without a start date. id = %s" % self.id
|
self.metadata['start'] = stringify_time(time.gmtime(0))
|
||||||
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
|
|
||||||
log.critical(msg)
|
log.critical(msg)
|
||||||
system.error_tracker(msg)
|
system.error_tracker(msg)
|
||||||
|
|
||||||
def try_parse_time(key):
|
self.enrollment_start = self._try_parse_time("enrollment_start")
|
||||||
"""
|
self.enrollment_end = self._try_parse_time("enrollment_end")
|
||||||
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")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def has_started(self):
|
def has_started(self):
|
||||||
return time.gmtime() > self.start
|
return time.gmtime() > self.start
|
||||||
@@ -154,6 +131,7 @@ class CourseDescriptor(SequenceDescriptor):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def id(self):
|
def id(self):
|
||||||
|
"""Return the course_id for this course"""
|
||||||
return self.location_to_id(self.location)
|
return self.location_to_id(self.location)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -24,16 +24,8 @@ class ErrorModule(XModule):
|
|||||||
return self.system.render_template('module-error.html', {
|
return self.system.render_template('module-error.html', {
|
||||||
'data' : self.definition['data']['contents'],
|
'data' : self.definition['data']['contents'],
|
||||||
'error' : self.definition['data']['error_msg'],
|
'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):
|
class ErrorDescriptor(EditingDescriptor):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -32,22 +32,37 @@ class @Problem
|
|||||||
|
|
||||||
queueing: =>
|
queueing: =>
|
||||||
@queued_items = @$(".xqueue")
|
@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
|
if window.queuePollerID # Only one poller 'thread' per Problem
|
||||||
window.clearTimeout(window.queuePollerID)
|
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: =>
|
poll: =>
|
||||||
$.postWithPrefix "#{@url}/problem_get", (response) =>
|
$.postWithPrefix "#{@url}/problem_get", (response) =>
|
||||||
@queued_items = $(response.html).find(".xqueue")
|
# If queueing status changed, then render
|
||||||
if @queued_items.length == 0
|
@new_queued_items = $(response.html).find(".xqueue")
|
||||||
|
if @new_queued_items.length isnt @num_queued_items
|
||||||
@el.html(response.html)
|
@el.html(response.html)
|
||||||
@executeProblemScripts () =>
|
@executeProblemScripts () =>
|
||||||
@setupInputTypes()
|
@setupInputTypes()
|
||||||
@bind()
|
@bind()
|
||||||
|
|
||||||
|
@num_queued_items = @new_queued_items.length
|
||||||
|
if @num_queued_items == 0
|
||||||
delete window.queuePollerID
|
delete window.queuePollerID
|
||||||
else
|
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)
|
window.queuePollerID = window.setTimeout(@poll, 1000)
|
||||||
|
|
||||||
render: (content) ->
|
render: (content) ->
|
||||||
@@ -141,9 +156,16 @@ class @Problem
|
|||||||
|
|
||||||
fd = new FormData()
|
fd = new FormData()
|
||||||
|
|
||||||
|
# Sanity check of file size
|
||||||
|
file_too_large = false
|
||||||
|
max_filesize = 4*1000*1000 # 4 MB
|
||||||
|
|
||||||
@inputs.each (index, element) ->
|
@inputs.each (index, element) ->
|
||||||
if element.type is 'file'
|
if element.type is 'file'
|
||||||
if element.files[0] instanceof 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])
|
fd.append(element.id, element.files[0])
|
||||||
else
|
else
|
||||||
fd.append(element.id, '')
|
fd.append(element.id, '')
|
||||||
@@ -163,7 +185,8 @@ class @Problem
|
|||||||
else
|
else
|
||||||
alert(response.success)
|
alert(response.success)
|
||||||
|
|
||||||
$.ajaxWithPrefix("#{@url}/problem_check", settings)
|
if not file_too_large
|
||||||
|
$.ajaxWithPrefix("#{@url}/problem_check", settings)
|
||||||
|
|
||||||
check: =>
|
check: =>
|
||||||
Logger.log 'problem_check', @answers
|
Logger.log 'problem_check', @answers
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ i4xs = ModuleSystem(
|
|||||||
filestore=fs.osfs.OSFS(os.path.dirname(os.path.realpath(__file__))+"/test_files"),
|
filestore=fs.osfs.OSFS(os.path.dirname(os.path.realpath(__file__))+"/test_files"),
|
||||||
debug=True,
|
debug=True,
|
||||||
xqueue={'interface':None, 'callback_url':'/', 'default_queuename': 'testqueue'},
|
xqueue={'interface':None, 'callback_url':'/', 'default_queuename': 'testqueue'},
|
||||||
is_staff=False,
|
|
||||||
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules")
|
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
|
self.assertFalse(test_lcp.correct_map.is_queued(answer_ids[j])) # Should be dequeued, message delivered
|
||||||
else:
|
else:
|
||||||
self.assertTrue(test_lcp.correct_map.is_queued(answer_ids[j])) # Should be queued, message undelivered
|
self.assertTrue(test_lcp.correct_map.is_queued(answer_ids[j])) # Should be queued, message undelivered
|
||||||
|
|
||||||
def test_convert_files_to_filenames(self):
|
def test_convert_files_to_filenames(self):
|
||||||
problem_file = os.path.dirname(__file__) + "/test_files/coderesponse.xml"
|
problem_file = os.path.dirname(__file__) + "/test_files/coderesponse.xml"
|
||||||
fp = open(problem_file)
|
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_2_1'], 'String-based answer')
|
||||||
self.assertEquals(answers_converted['1_3_1'], ['answer1', 'answer2', 'answer3'])
|
self.assertEquals(answers_converted['1_3_1'], ['answer1', 'answer2', 'answer3'])
|
||||||
self.assertEquals(answers_converted['1_4_1'], fp.name)
|
self.assertEquals(answers_converted['1_4_1'], fp.name)
|
||||||
|
|
||||||
|
|
||||||
class ChoiceResponseTest(unittest.TestCase):
|
class ChoiceResponseTest(unittest.TestCase):
|
||||||
|
|
||||||
|
|||||||
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 lxml.etree import XMLSyntaxError
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
|
|
||||||
from xmodule.modulestore import Location
|
|
||||||
from xmodule.errortracker import exc_info_to_str
|
from xmodule.errortracker import exc_info_to_str
|
||||||
|
from xmodule.modulestore import Location
|
||||||
|
from xmodule.timeparse import parse_time
|
||||||
|
|
||||||
log = logging.getLogger('mitx.' + __name__)
|
log = logging.getLogger('mitx.' + __name__)
|
||||||
|
|
||||||
@@ -218,9 +219,11 @@ class XModule(HTMLSnippet):
|
|||||||
Return module instances for all the children of this module.
|
Return module instances for all the children of this module.
|
||||||
'''
|
'''
|
||||||
if self._loaded_children is None:
|
if self._loaded_children is None:
|
||||||
self._loaded_children = [
|
# get_module returns None if the current user doesn't have access
|
||||||
self.system.get_module(child)
|
# to the location.
|
||||||
for child in self.definition.get('children', [])]
|
self._loaded_children = filter(None,
|
||||||
|
[self.system.get_module(child)
|
||||||
|
for child in self.definition.get('children', [])])
|
||||||
|
|
||||||
return self._loaded_children
|
return self._loaded_children
|
||||||
|
|
||||||
@@ -396,6 +399,15 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
|
|||||||
return self.metadata.get('display_name',
|
return self.metadata.get('display_name',
|
||||||
self.url_name.replace('_', ' '))
|
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
|
@property
|
||||||
def own_metadata(self):
|
def own_metadata(self):
|
||||||
"""
|
"""
|
||||||
@@ -596,6 +608,24 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
|
|||||||
metadata=self.metadata
|
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):
|
class DescriptorSystem(object):
|
||||||
def __init__(self, load_item, resources_fs, error_tracker, **kwargs):
|
def __init__(self, load_item, resources_fs, error_tracker, **kwargs):
|
||||||
@@ -675,7 +705,6 @@ class ModuleSystem(object):
|
|||||||
filestore=None,
|
filestore=None,
|
||||||
debug=False,
|
debug=False,
|
||||||
xqueue=None,
|
xqueue=None,
|
||||||
is_staff=False,
|
|
||||||
node_path=""):
|
node_path=""):
|
||||||
'''
|
'''
|
||||||
Create a closure around the system environment.
|
Create a closure around the system environment.
|
||||||
@@ -688,7 +717,8 @@ class ModuleSystem(object):
|
|||||||
files. Update or remove.
|
files. Update or remove.
|
||||||
|
|
||||||
get_module - function that takes (location) and returns a corresponding
|
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
|
render_template - a function that takes (template_file, context), and
|
||||||
returns rendered html.
|
returns rendered html.
|
||||||
@@ -705,9 +735,6 @@ class ModuleSystem(object):
|
|||||||
replace_urls - TEMPORARY - A function like static_replace.replace_urls
|
replace_urls - TEMPORARY - A function like static_replace.replace_urls
|
||||||
that capa_module can use to fix up the static urls in
|
that capa_module can use to fix up the static urls in
|
||||||
ajax results.
|
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.ajax_url = ajax_url
|
||||||
self.xqueue = xqueue
|
self.xqueue = xqueue
|
||||||
@@ -718,7 +745,6 @@ class ModuleSystem(object):
|
|||||||
self.DEBUG = self.debug = debug
|
self.DEBUG = self.debug = debug
|
||||||
self.seed = user.id if user is not None else 0
|
self.seed = user.id if user is not None else 0
|
||||||
self.replace_urls = replace_urls
|
self.replace_urls = replace_urls
|
||||||
self.is_staff = is_staff
|
|
||||||
self.node_path = node_path
|
self.node_path = node_path
|
||||||
|
|
||||||
def get(self, attr):
|
def get(self, attr):
|
||||||
|
|||||||
@@ -65,3 +65,4 @@ To run a single nose test:
|
|||||||
nosetests common/lib/xmodule/xmodule/tests/test_stringify.py:test_stringify
|
nosetests common/lib/xmodule/xmodule/tests/test_stringify.py:test_stringify
|
||||||
|
|
||||||
|
|
||||||
|
Very handy: if you uncomment the `--pdb` argument in `NOSE_ARGS` in `lms/envs/test.py`, it will drop you into pdb on error. This lets you go up and down the stack and see what the values of the variables are. Check out http://docs.python.org/library/pdb.html
|
||||||
|
|||||||
BIN
lms/askbot/skins/mitx/media/images/email-sharing.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
lms/askbot/skins/mitx/media/images/facebook-sharing.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
lms/askbot/skins/mitx/media/images/google-plus-sharing.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
lms/askbot/skins/mitx/media/images/lrg/email-sharing.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
lms/askbot/skins/mitx/media/images/lrg/facebook-sharing.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
lms/askbot/skins/mitx/media/images/lrg/facebook.png
Normal file
|
After Width: | Height: | Size: 205 B |
BIN
lms/askbot/skins/mitx/media/images/lrg/google-plus-sharing.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
lms/askbot/skins/mitx/media/images/lrg/linkedin.png
Normal file
|
After Width: | Height: | Size: 229 B |
BIN
lms/askbot/skins/mitx/media/images/lrg/twitter-sharing.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
lms/askbot/skins/mitx/media/images/lrg/twitter.png
Normal file
|
After Width: | Height: | Size: 235 B |
BIN
lms/askbot/skins/mitx/media/images/lrg/youtube-sharing.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
lms/askbot/skins/mitx/media/images/social/email-sharing.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
lms/askbot/skins/mitx/media/images/social/facebook-sharing.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
BIN
lms/askbot/skins/mitx/media/images/social/twitter-sharing.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
lms/askbot/skins/mitx/media/images/social/youtube-sharing.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
lms/askbot/skins/mitx/media/images/twitter-sharing.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
lms/askbot/skins/mitx/media/images/youtube-sharing.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
@@ -3,7 +3,7 @@
|
|||||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
{% spaceless %}
|
{% spaceless %}
|
||||||
<head>
|
<head>
|
||||||
<title>{% block title %}{% endblock %} - MITX 6.002</title>
|
<title>{% block title %}{% endblock %}</title>
|
||||||
{% include "meta/html_head_meta.html" %}
|
{% include "meta/html_head_meta.html" %}
|
||||||
<link rel="shortcut icon" href="{{ settings.SITE_FAVICON|media }}" />
|
<link rel="shortcut icon" href="{{ settings.SITE_FAVICON|media }}" />
|
||||||
{% include "meta/html_head_stylesheets.html" %}
|
{% include "meta/html_head_stylesheets.html" %}
|
||||||
|
|||||||
@@ -33,8 +33,14 @@
|
|||||||
<!-- Quick fix -- we should reference askbot jquery properly -->
|
<!-- Quick fix -- we should reference askbot jquery properly -->
|
||||||
<script
|
<script
|
||||||
type="text/javascript"
|
type="text/javascript"
|
||||||
src="{{ settings.STATIC_URL }}/js/askbot_jquery.min.js"
|
src="{{'/js/jquery-1.4.3.js'|media}}"
|
||||||
></script>
|
></script>
|
||||||
|
{#
|
||||||
|
<script
|
||||||
|
type="text/javascript"
|
||||||
|
src="{{ settings.STATIC_ROOT }}/js/askbot_jquery.min.js"
|
||||||
|
></script>
|
||||||
|
#}
|
||||||
<!-- History.js -->
|
<!-- History.js -->
|
||||||
<script type='text/javascript' src="{{"/js/jquery.history.js"|media }}"></script>
|
<script type='text/javascript' src="{{"/js/jquery.history.js"|media }}"></script>
|
||||||
<script type='text/javascript' src="{{"/js/utils.js"|media }}"></script>
|
<script type='text/javascript' src="{{"/js/utils.js"|media }}"></script>
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
<header class="global" aria-label="Global Navigation">
|
<header class="global" aria-label="Global Navigation">
|
||||||
<nav>
|
<nav>
|
||||||
<h1 class="logo"><a href="${reverse('root')}"></a></h1>
|
<h1 class="logo"><a href="{% url root %}"></a></h1>
|
||||||
<ol class="left">
|
<ol class="left">
|
||||||
<li class="primary">
|
<li class="primary">
|
||||||
<a href="${reverse('courses')}">Find Courses</a>
|
<a href="{% url courses %}">Find Courses</a>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
<ol class="user">
|
<ol class="user">
|
||||||
<li class="primary">
|
<li class="primary">
|
||||||
<a href="${reverse('dashboard')}" class="user-link">
|
<a href="{% url dashboard %}" class="user-link">
|
||||||
<span class="avatar"></span>
|
<span class="avatar"></span>
|
||||||
${user.username}
|
{{user.username}}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="primary">
|
<li class="primary">
|
||||||
<a href="#" class="dropdown">▾</a>
|
<a href="#" class="dropdown">▾</a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
## <li><a href="#">Account Settings</a></li>
|
{# <li><a href="#">Account Settings</a></li> #}
|
||||||
<li><a href="${reverse('help_edx')}">Help</a></li>
|
<li><a href="{% url help_edx %}">Help</a></li>
|
||||||
<li><a href="${reverse('logout')}">Log Out</a></li>
|
<li><a href="{% url logout %}">Log Out</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|||||||
@@ -3,19 +3,19 @@
|
|||||||
<nav>
|
<nav>
|
||||||
<section class="top">
|
<section class="top">
|
||||||
<section class="primary">
|
<section class="primary">
|
||||||
<a href="${reverse('root')}" class="logo"></a>
|
<a href="{% url root %}" class="logo"></a>
|
||||||
<a href="${reverse('courses')}">Find Courses</a>
|
<a href="{% url courses %}">Find Courses</a>
|
||||||
<a href="${reverse('about_edx')}">About</a>
|
<a href="{% url about_edx %}">About</a>
|
||||||
<a href="http://edxonline.tumblr.com/">Blog</a>
|
<a href="http://edxonline.tumblr.com/">Blog</a>
|
||||||
<a href="${reverse('jobs')}">Jobs</a>
|
<a href="{% url jobs %}">Jobs</a>
|
||||||
<a href="${reverse('contact')}">Contact</a>
|
<a href="{% url contact %}">Contact</a>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="social">
|
<section class="social">
|
||||||
<a href="http://youtube.com/user/edxonline"><img src="${static.url('images/social/youtube-sharing.png')}" /></a>
|
<a href="http://youtube.com/user/edxonline"><img src='{{"images/social/youtube-sharing.png"|media}}' /></a>
|
||||||
<a href="https://plus.google.com/108235383044095082735"><img src="${static.url('images/social/google-plus-sharing.png')}" /></a>
|
<a href="https://plus.google.com/108235383044095082735"><img src="{{('images/social/google-plus-sharing.png'|media)}}" /></a>
|
||||||
<a href="http://www.facebook.com/EdxOnline"><img src="${static.url('images/social/facebook-sharing.png')}" /></a>
|
<a href="http://www.facebook.com/EdxOnline"><img src="{{'images/social/facebook-sharing.png'|media}}" /></a>
|
||||||
<a href="https://twitter.com/edXOnline"><img src="${static.url('images/social/twitter-sharing.png')}" /></a>
|
<a href="https://twitter.com/edXOnline"><img src="{{'images/social/twitter-sharing.png'|media}}" /></a>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -25,10 +25,10 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="secondary">
|
<section class="secondary">
|
||||||
<a href="${reverse('tos')}">Terms of Service</a>
|
<a href="{% url tos %}">Terms of Service</a>
|
||||||
<a href="${reverse('privacy_edx')}">Privacy Policy</a>
|
<a href="{% url privacy_edx %}">Privacy Policy</a>
|
||||||
<a href="${reverse('honor')}">Honor Code</a>
|
<a href="{% url honor %}">Honor Code</a>
|
||||||
<a href="${reverse('help_edx')}">Help</a>
|
<a href="{% url help_edx %}">Help</a>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
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.django import modulestore
|
||||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||||
from static_replace import replace_urls, try_staticfiles_lookup
|
from static_replace import replace_urls, try_staticfiles_lookup
|
||||||
|
from courseware.access import has_access
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
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
|
Given a course id, return the corresponding course descriptor.
|
||||||
object. By default, if the course is not found or the course is
|
|
||||||
not open yet, this method will raise a 404.
|
|
||||||
|
|
||||||
If course_must_be_open is False, the course will be returned
|
If course_id is not valid, raises a 404.
|
||||||
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.
|
|
||||||
"""
|
"""
|
||||||
course = None
|
try:
|
||||||
if course_required or course_id:
|
course_loc = CourseDescriptor.id_to_location(course_id)
|
||||||
try:
|
return modulestore().get_item(course_loc)
|
||||||
course_loc = CourseDescriptor.id_to_location(course_id)
|
except (KeyError, ItemNotFoundError):
|
||||||
course = modulestore().get_item(course_loc)
|
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
|
def get_course_with_access(user, course_id, action):
|
||||||
if (settings.MITX_FEATURES['DARK_LAUNCH'] and
|
"""
|
||||||
has_staff_access_to_course(user, course)):
|
Given a course_id, look up the corresponding course descriptor,
|
||||||
must_be_open = False
|
check that the user has the access to perform the specified action
|
||||||
|
on the course, and return the descriptor.
|
||||||
if must_be_open and not started:
|
|
||||||
raise Http404("This course has not yet started.")
|
|
||||||
|
|
||||||
|
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
|
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):
|
def course_image_url(course):
|
||||||
"""Try to look up the image url for the course. If it's not found,
|
"""Try to look up the image url for the course. If it's not found,
|
||||||
log an error and return the dead link"""
|
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))
|
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):
|
def get_courses_by_university(user):
|
||||||
'''
|
'''
|
||||||
Returns dict of lists of courses available, keyed by course.org (ie university).
|
Returns dict of lists of courses available, keyed by course.org (ie university).
|
||||||
Courses are sorted by course.number.
|
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.
|
# TODO: Clean up how 'error' is done.
|
||||||
# filter out any courses that errored.
|
# filter out any courses that errored.
|
||||||
@@ -208,9 +154,7 @@ def get_courses_by_university(user):
|
|||||||
courses = sorted(courses, key=lambda course: course.number)
|
courses = sorted(courses, key=lambda course: course.number)
|
||||||
universities = defaultdict(list)
|
universities = defaultdict(list)
|
||||||
for course in courses:
|
for course in courses:
|
||||||
if settings.MITX_FEATURES.get('ACCESS_REQUIRE_STAFF_FOR_COURSE'):
|
if has_access(user, course, 'see_exists'):
|
||||||
if not has_access_to_course(user,course):
|
universities[course.org].append(course)
|
||||||
continue
|
|
||||||
universities[course.org].append(course)
|
|
||||||
return universities
|
return universities
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,12 @@ def grade(student, request, course, student_module_cache=None):
|
|||||||
scores = []
|
scores = []
|
||||||
# TODO: We need the request to pass into here. If we could forgo that, our arguments
|
# TODO: We need the request to pass into here. If we could forgo that, our arguments
|
||||||
# would be simpler
|
# 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
|
# 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
|
# 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
|
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
|
Find any StudentModule objects that are needed by any descriptor
|
||||||
in descriptors. Avoids making multiple queries to the database.
|
in descriptors. Avoids making multiple queries to the database.
|
||||||
@@ -77,6 +77,7 @@ class StudentModuleCache(object):
|
|||||||
Arguments
|
Arguments
|
||||||
user: The user for which to fetch maching StudentModules
|
user: The user for which to fetch maching StudentModules
|
||||||
descriptors: An array of XModuleDescriptors.
|
descriptors: An array of XModuleDescriptors.
|
||||||
|
select_for_update: Flag indicating whether the row should be locked until end of transaction
|
||||||
'''
|
'''
|
||||||
if user.is_authenticated():
|
if user.is_authenticated():
|
||||||
module_ids = self._get_module_state_keys(descriptors)
|
module_ids = self._get_module_state_keys(descriptors)
|
||||||
@@ -86,23 +87,30 @@ class StudentModuleCache(object):
|
|||||||
self.cache = []
|
self.cache = []
|
||||||
chunk_size = 500
|
chunk_size = 500
|
||||||
for id_chunk in [module_ids[i:i + chunk_size] for i in xrange(0, len(module_ids), chunk_size)]:
|
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(
|
if select_for_update:
|
||||||
student=user,
|
self.cache.extend(StudentModule.objects.select_for_update().filter(
|
||||||
module_state_key__in=id_chunk)
|
student=user,
|
||||||
)
|
module_state_key__in=id_chunk)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.cache.extend(StudentModule.objects.filter(
|
||||||
|
student=user,
|
||||||
|
module_state_key__in=id_chunk)
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.cache = []
|
self.cache = []
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@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
|
descriptor: An XModuleDescriptor
|
||||||
depth is the number of levels of descendent modules to load StudentModules for, in addition to
|
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
|
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
|
descriptor_filter is a function that accepts a descriptor and return wether the StudentModule
|
||||||
should be cached
|
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):
|
def get_child_descriptors(descriptor, depth, descriptor_filter):
|
||||||
@@ -122,7 +130,7 @@ class StudentModuleCache(object):
|
|||||||
|
|
||||||
descriptors = get_child_descriptors(descriptor, depth, descriptor_filter)
|
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):
|
def _get_module_state_keys(self, descriptors):
|
||||||
'''
|
'''
|
||||||
|
|||||||
@@ -2,27 +2,40 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.models import User
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from requests.auth import HTTPBasicAuth
|
||||||
from xmodule.modulestore.django import modulestore
|
|
||||||
from capa.xqueue_interface import qinterface
|
from capa.xqueue_interface import XQueueInterface
|
||||||
|
from courseware.access import has_access
|
||||||
from mitxmako.shortcuts import render_to_string
|
from mitxmako.shortcuts import render_to_string
|
||||||
from models import StudentModule, StudentModuleCache
|
from models import StudentModule, StudentModuleCache
|
||||||
from static_replace import replace_urls
|
from static_replace import replace_urls
|
||||||
from xmodule.exceptions import NotFoundError
|
from xmodule.exceptions import NotFoundError
|
||||||
|
from xmodule.modulestore import Location
|
||||||
|
from xmodule.modulestore.django import modulestore
|
||||||
from xmodule.x_module import ModuleSystem
|
from xmodule.x_module import ModuleSystem
|
||||||
from xmodule_modifiers import replace_static_urls, add_histogram, wrap_xmodule
|
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")
|
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):
|
def make_track_function(request):
|
||||||
'''
|
'''
|
||||||
Make a tracking function that logs what happened.
|
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 "".
|
Everything else comes from the xml, or defaults to "".
|
||||||
|
|
||||||
chapters with name 'hidden' are skipped.
|
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)
|
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)
|
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
|
#TODO Only check the cache if this module can possibly have state
|
||||||
instance_module = None
|
instance_module = None
|
||||||
shared_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_module = student_module_cache.lookup(descriptor.category,
|
||||||
shared_state_key)
|
shared_state_key)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
instance_state = instance_module.state if instance_module is not None else None
|
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
|
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
|
# Setup system context for module instance
|
||||||
|
ajax_url = reverse('modx_dispatch',
|
||||||
ajax_url = reverse('modx_dispatch',
|
|
||||||
kwargs=dict(course_id=descriptor.location.course_id,
|
kwargs=dict(course_id=descriptor.location.course_id,
|
||||||
id=descriptor.location.url(),
|
id=descriptor.location.url(),
|
||||||
dispatch=''),
|
dispatch=''),
|
||||||
)
|
)
|
||||||
|
|
||||||
# ajax_url = settings.MITX_ROOT_URL + '/modx/' + descriptor.location.url() + '/'
|
|
||||||
|
|
||||||
# Fully qualified callback URL for external queueing system
|
# Fully qualified callback URL for external queueing system
|
||||||
xqueue_callback_url = request.build_absolute_uri('/')[:-1] # Trailing slash provided by reverse
|
xqueue_callback_url = request.build_absolute_uri('/')[:-1] # Trailing slash provided by reverse
|
||||||
xqueue_callback_url += reverse('xqueue_callback',
|
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
|
# TODO: Queuename should be derived from 'course_settings.json' of each course
|
||||||
xqueue_default_queuename = descriptor.location.org + '-' + descriptor.location.course
|
xqueue_default_queuename = descriptor.location.org + '-' + descriptor.location.course
|
||||||
|
|
||||||
xqueue = { 'interface': qinterface,
|
xqueue = {'interface': xqueue_interface,
|
||||||
'callback_url': xqueue_callback_url,
|
'callback_url': xqueue_callback_url,
|
||||||
'default_queuename': xqueue_default_queuename.replace(' ','_') }
|
'default_queuename': xqueue_default_queuename.replace(' ', '_')}
|
||||||
|
|
||||||
def _get_module(location):
|
def _get_module(location):
|
||||||
|
"""
|
||||||
|
Delegate to get_module. It does an access check, so may return None
|
||||||
|
"""
|
||||||
return get_module(user, request, location,
|
return get_module(user, request, location,
|
||||||
student_module_cache, position)
|
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
|
# a module is coming through get_html and is therefore covered
|
||||||
# by the replace_static_urls code below
|
# by the replace_static_urls code below
|
||||||
replace_urls=replace_urls,
|
replace_urls=replace_urls,
|
||||||
is_staff=has_staff_access_to_location(user, location),
|
|
||||||
node_path=settings.NODE_PATH
|
node_path=settings.NODE_PATH
|
||||||
)
|
)
|
||||||
# pass position specified in URL to module through ModuleSystem
|
# pass position specified in URL to module through ModuleSystem
|
||||||
system.set('position', position)
|
system.set('position', position)
|
||||||
system.set('DEBUG',settings.DEBUG)
|
system.set('DEBUG', settings.DEBUG)
|
||||||
|
|
||||||
module = descriptor.xmodule_constructor(system)(instance_state, shared_state)
|
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 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)
|
module.get_html = add_histogram(module.get_html, module, user)
|
||||||
|
|
||||||
return module
|
return module
|
||||||
@@ -291,12 +307,17 @@ def xqueue_callback(request, course_id, userid, id, dispatch):
|
|||||||
# Retrieve target StudentModule
|
# Retrieve target StudentModule
|
||||||
user = User.objects.get(id=userid)
|
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)
|
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)
|
instance_module = get_instance_module(user, instance, student_module_cache)
|
||||||
|
|
||||||
if instance_module is None:
|
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
|
raise Http404
|
||||||
|
|
||||||
oldgrade = instance_module.grade
|
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
|
- id -- the module id. Used to look up the XModule instance
|
||||||
'''
|
'''
|
||||||
# ''' (fix emacs broken parsing)
|
# ''' (fix emacs broken parsing)
|
||||||
# Check for submitted files
|
|
||||||
|
# Check for submitted files and basic file size checks
|
||||||
p = request.POST.copy()
|
p = request.POST.copy()
|
||||||
if request.FILES:
|
if request.FILES:
|
||||||
for inputfile_id in request.FILES.keys():
|
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))
|
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(request.user, modulestore().get_item(id))
|
||||||
instance = get_module(request.user, request, id, student_module_cache)
|
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)
|
instance_module = get_instance_module(request.user, instance, student_module_cache)
|
||||||
shared_module = get_shared_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
|
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 student.models import Registration
|
||||||
from courseware.courses import course_staff_group_name
|
|
||||||
from xmodule.modulestore.django import modulestore
|
from xmodule.modulestore.django import modulestore
|
||||||
from xmodule.modulestore import Location
|
from xmodule.modulestore import Location
|
||||||
from xmodule.modulestore.xml_importer import import_from_xml
|
from xmodule.modulestore.xml_importer import import_from_xml
|
||||||
|
from xmodule.timeparse import stringify_time
|
||||||
|
|
||||||
def parse_json(response):
|
def parse_json(response):
|
||||||
"""Parse response, which is assumed to be json"""
|
"""Parse response, which is assumed to be json"""
|
||||||
@@ -310,7 +312,7 @@ class TestViewAuth(PageLoader):
|
|||||||
self.check_for_get_code(404, url)
|
self.check_for_get_code(404, url)
|
||||||
|
|
||||||
# Make the instructor staff in the toy course
|
# 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 = Group.objects.create(name=group_name)
|
||||||
g.user_set.add(user(self.instructor))
|
g.user_set.add(user(self.instructor))
|
||||||
|
|
||||||
@@ -340,25 +342,22 @@ class TestViewAuth(PageLoader):
|
|||||||
|
|
||||||
def run_wrapped(self, test):
|
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
|
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
|
(Can't use override_settings because we're only changing part of the
|
||||||
MITX_FEATURES dict)
|
MITX_FEATURES dict)
|
||||||
"""
|
"""
|
||||||
oldDSD = settings.MITX_FEATURES['DISABLE_START_DATES']
|
oldDSD = settings.MITX_FEATURES['DISABLE_START_DATES']
|
||||||
oldDL = settings.MITX_FEATURES['DARK_LAUNCH']
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
settings.MITX_FEATURES['DISABLE_START_DATES'] = False
|
settings.MITX_FEATURES['DISABLE_START_DATES'] = False
|
||||||
settings.MITX_FEATURES['DARK_LAUNCH'] = True
|
|
||||||
test()
|
test()
|
||||||
finally:
|
finally:
|
||||||
settings.MITX_FEATURES['DISABLE_START_DATES'] = oldDSD
|
settings.MITX_FEATURES['DISABLE_START_DATES'] = oldDSD
|
||||||
settings.MITX_FEATURES['DARK_LAUNCH'] = oldDL
|
|
||||||
|
|
||||||
|
|
||||||
def test_dark_launch(self):
|
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"""
|
pages, but instructors can"""
|
||||||
self.run_wrapped(self._do_test_dark_launch)
|
self.run_wrapped(self._do_test_dark_launch)
|
||||||
|
|
||||||
@@ -372,13 +371,12 @@ class TestViewAuth(PageLoader):
|
|||||||
|
|
||||||
# Make courses start in the future
|
# Make courses start in the future
|
||||||
tomorrow = time.time() + 24*3600
|
tomorrow = time.time() + 24*3600
|
||||||
self.toy.start = self.toy.metadata['start'] = time.gmtime(tomorrow)
|
self.toy.metadata['start'] = stringify_time(time.gmtime(tomorrow))
|
||||||
self.full.start = self.full.metadata['start'] = time.gmtime(tomorrow)
|
self.full.metadata['start'] = stringify_time(time.gmtime(tomorrow))
|
||||||
|
|
||||||
self.assertFalse(self.toy.has_started())
|
self.assertFalse(self.toy.has_started())
|
||||||
self.assertFalse(self.full.has_started())
|
self.assertFalse(self.full.has_started())
|
||||||
self.assertFalse(settings.MITX_FEATURES['DISABLE_START_DATES'])
|
self.assertFalse(settings.MITX_FEATURES['DISABLE_START_DATES'])
|
||||||
self.assertTrue(settings.MITX_FEATURES['DARK_LAUNCH'])
|
|
||||||
|
|
||||||
def reverse_urls(names, course):
|
def reverse_urls(names, course):
|
||||||
"""Reverse a list of course urls"""
|
"""Reverse a list of course urls"""
|
||||||
@@ -444,7 +442,7 @@ class TestViewAuth(PageLoader):
|
|||||||
|
|
||||||
print '=== Testing course instructor access....'
|
print '=== Testing course instructor access....'
|
||||||
# Make the instructor staff in the toy course
|
# 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 = Group.objects.create(name=group_name)
|
||||||
g.user_set.add(user(self.instructor))
|
g.user_set.add(user(self.instructor))
|
||||||
|
|
||||||
@@ -494,7 +492,7 @@ class TestViewAuth(PageLoader):
|
|||||||
|
|
||||||
print '=== Testing course instructor access....'
|
print '=== Testing course instructor access....'
|
||||||
# Make the instructor staff in the toy course
|
# 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 = Group.objects.create(name=group_name)
|
||||||
g.user_set.add(user(self.instructor))
|
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_future.csrf import ensure_csrf_cookie
|
||||||
from django.views.decorators.cache import cache_control
|
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 import grades
|
||||||
from courseware.courses import (check_course, get_courses_by_university,
|
from courseware.access import has_access
|
||||||
has_staff_access_to_course_id)
|
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")
|
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)
|
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||||
def index(request, course_id, chapter=None, section=None,
|
def index(request, course_id, chapter=None, section=None,
|
||||||
position=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
|
If course, chapter, and section aren't all specified, just returns
|
||||||
the accordion. If they are specified, returns an error if they don't
|
the accordion. If they are specified, returns an error if they don't
|
||||||
point to a valid module.
|
point to a valid module.
|
||||||
@@ -109,8 +108,8 @@ def index(request, course_id, chapter=None, section=None,
|
|||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
- HTTPresponse
|
- 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)
|
registered = registered_for_course(course, request.user)
|
||||||
if not registered:
|
if not registered:
|
||||||
# TODO (vshnayder): do course instructors need to be registered to see course?
|
# 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,
|
module = get_module(request.user, request,
|
||||||
section_descriptor.location,
|
section_descriptor.location,
|
||||||
student_module_cache)
|
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()
|
context['content'] = module.get_html()
|
||||||
else:
|
else:
|
||||||
log.warning("Couldn't find a section descriptor for course_id '{0}',"
|
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.
|
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})
|
return render_to_response('info.html', {'course': course})
|
||||||
|
|
||||||
@@ -221,7 +224,7 @@ def registered_for_course(course, user):
|
|||||||
@ensure_csrf_cookie
|
@ensure_csrf_cookie
|
||||||
@cache_if_anonymous
|
@cache_if_anonymous
|
||||||
def course_about(request, course_id):
|
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)
|
registered = registered_for_course(course, request.user)
|
||||||
return render_to_response('portal/course_about.html', {'course': course, 'registered': registered})
|
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 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:
|
if student_id is None or student_id == request.user.id:
|
||||||
# always allowed to see your own profile
|
# always allowed to see your own profile
|
||||||
student = request.user
|
student = request.user
|
||||||
else:
|
else:
|
||||||
# Requesting access to a different student's profile
|
# 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
|
raise Http404
|
||||||
student = User.objects.get(id=int(student_id))
|
student = User.objects.get(id=int(student_id))
|
||||||
|
|
||||||
@@ -297,10 +300,7 @@ def gradebook(request, course_id):
|
|||||||
- only displayed to course staff
|
- only displayed to course staff
|
||||||
- shows students who are enrolled.
|
- shows students who are enrolled.
|
||||||
"""
|
"""
|
||||||
if not has_staff_access_to_course_id(request.user, course_id):
|
course = get_course_with_access(request.user, course_id, 'staff')
|
||||||
raise Http404
|
|
||||||
|
|
||||||
course = check_course(request.user, course_id)
|
|
||||||
|
|
||||||
enrolled_students = User.objects.filter(courseenrollment__course_id=course_id).order_by('username')
|
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)
|
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||||
def grade_summary(request, course_id):
|
def grade_summary(request, course_id):
|
||||||
"""Display the grade summary for a course."""
|
"""Display the grade summary for a course."""
|
||||||
if not has_staff_access_to_course_id(request.user, course_id):
|
course = get_course_with_access(request.user, course_id, 'staff')
|
||||||
raise Http404
|
|
||||||
|
|
||||||
course = check_course(request.user, course_id)
|
|
||||||
|
|
||||||
# For now, just a static page
|
# For now, just a static page
|
||||||
context = {'course': course }
|
context = {'course': course }
|
||||||
@@ -335,10 +332,7 @@ def grade_summary(request, course_id):
|
|||||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||||
def instructor_dashboard(request, course_id):
|
def instructor_dashboard(request, course_id):
|
||||||
"""Display the instructor dashboard for a course."""
|
"""Display the instructor dashboard for a course."""
|
||||||
if not has_staff_access_to_course_id(request.user, course_id):
|
course = get_course_with_access(request.user, course_id, 'staff')
|
||||||
raise Http404
|
|
||||||
|
|
||||||
course = check_course(request.user, course_id)
|
|
||||||
|
|
||||||
# For now, just a static page
|
# For now, just a static page
|
||||||
context = {'course': course }
|
context = {'course': course }
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from django.utils import simplejson
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from mitxmako.shortcuts import render_to_response
|
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.course_module import CourseDescriptor
|
||||||
from xmodule.modulestore.django import modulestore
|
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):
|
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)
|
(article, err) = get_article(request, article_path, course)
|
||||||
if err:
|
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):
|
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)
|
(article, err) = get_article(request, article_path, course)
|
||||||
if err:
|
if err:
|
||||||
@@ -91,7 +91,7 @@ def view_revision(request, revision_number, article_path, course_id=None):
|
|||||||
|
|
||||||
|
|
||||||
def root_redirect(request, 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.
|
#TODO: Add a default namespace to settings.
|
||||||
namespace = course.wiki_namespace if course else "edX"
|
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):
|
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('/')
|
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):
|
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)
|
(article, err) = get_article(request, article_path, course)
|
||||||
if err:
|
if err:
|
||||||
@@ -218,7 +218,7 @@ def edit(request, article_path, course_id=None):
|
|||||||
|
|
||||||
|
|
||||||
def history(request, article_path, page=1, 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)
|
(article, err) = get_article(request, article_path, course)
|
||||||
if err:
|
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):
|
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
|
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):
|
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
|
# 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.
|
# 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):
|
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)
|
(article, err) = get_article(request, slug, namespace if namespace else course_id)
|
||||||
if err:
|
if err:
|
||||||
@@ -415,7 +415,7 @@ def search_add_related(request, course_id, slug, namespace):
|
|||||||
|
|
||||||
|
|
||||||
def 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)
|
(article, err) = get_article(request, slug, namespace if namespace else course_id)
|
||||||
if err:
|
if err:
|
||||||
@@ -439,7 +439,7 @@ def add_related(request, course_id, slug, namespace):
|
|||||||
|
|
||||||
|
|
||||||
def remove_related(request, course_id, namespace, slug, related_id):
|
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)
|
(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):
|
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
|
from random import randint
|
||||||
num_arts = Article.objects.count()
|
num_arts = Article.objects.count()
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from mitxmako.shortcuts import render_to_response
|
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
|
from lxml import etree
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def index(request, course_id, page=0):
|
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
|
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()
|
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):
|
def index_shifted(request, course_id, page):
|
||||||
|
|||||||
@@ -54,3 +54,5 @@ AWS_ACCESS_KEY_ID = AUTH_TOKENS["AWS_ACCESS_KEY_ID"]
|
|||||||
AWS_SECRET_ACCESS_KEY = AUTH_TOKENS["AWS_SECRET_ACCESS_KEY"]
|
AWS_SECRET_ACCESS_KEY = AUTH_TOKENS["AWS_SECRET_ACCESS_KEY"]
|
||||||
|
|
||||||
DATABASES = AUTH_TOKENS['DATABASES']
|
DATABASES = AUTH_TOKENS['DATABASES']
|
||||||
|
|
||||||
|
XQUEUE_INTERFACE = AUTH_TOKENS['XQUEUE_INTERFACE']
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ MITX_FEATURES = {
|
|||||||
## DO NOT SET TO True IN THIS FILE
|
## DO NOT SET TO True IN THIS FILE
|
||||||
## Doing so will cause all courses to be released on production
|
## Doing so will cause all courses to be released on production
|
||||||
'DISABLE_START_DATES': False, # When True, all courses will be active, regardless of start date
|
'DISABLE_START_DATES': False, # When True, all courses will be active, regardless of start date
|
||||||
'DARK_LAUNCH': False, # When True, courses will be active for staff only
|
|
||||||
|
|
||||||
'ENABLE_TEXTBOOK' : True,
|
'ENABLE_TEXTBOOK' : True,
|
||||||
'ENABLE_DISCUSSION' : True,
|
'ENABLE_DISCUSSION' : True,
|
||||||
@@ -95,12 +94,12 @@ system_node_path = os.environ.get("NODE_PATH", None)
|
|||||||
if system_node_path is None:
|
if system_node_path is None:
|
||||||
system_node_path = "/usr/local/lib/node_modules"
|
system_node_path = "/usr/local/lib/node_modules"
|
||||||
|
|
||||||
node_paths = [COMMON_ROOT / "static/js/vendor",
|
node_paths = [COMMON_ROOT / "static/js/vendor",
|
||||||
COMMON_ROOT / "static/coffee/src",
|
COMMON_ROOT / "static/coffee/src",
|
||||||
system_node_path
|
system_node_path
|
||||||
]
|
]
|
||||||
NODE_PATH = ':'.join(node_paths)
|
NODE_PATH = ':'.join(node_paths)
|
||||||
|
|
||||||
################################## MITXWEB #####################################
|
################################## MITXWEB #####################################
|
||||||
# This is where we stick our compiled template files. Most of the app uses Mako
|
# This is where we stick our compiled template files. Most of the app uses Mako
|
||||||
# templates
|
# templates
|
||||||
@@ -135,6 +134,7 @@ TEMPLATE_CONTEXT_PROCESSORS = (
|
|||||||
'course_wiki.course_nav.context_processor',
|
'course_wiki.course_nav.context_processor',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
STUDENT_FILEUPLOAD_MAX_SIZE = 4*1000*1000 # 4 MB
|
||||||
|
|
||||||
# FIXME:
|
# FIXME:
|
||||||
# We should have separate S3 staged URLs in case we need to make changes to
|
# We should have separate S3 staged URLs in case we need to make changes to
|
||||||
@@ -228,8 +228,7 @@ STATIC_ROOT = ENV_ROOT / "staticfiles"
|
|||||||
STATICFILES_DIRS = [
|
STATICFILES_DIRS = [
|
||||||
COMMON_ROOT / "static",
|
COMMON_ROOT / "static",
|
||||||
PROJECT_ROOT / "static",
|
PROJECT_ROOT / "static",
|
||||||
ASKBOT_ROOT / "askbot" / "skins",
|
PROJECT_ROOT / "askbot" / "skins",
|
||||||
|
|
||||||
]
|
]
|
||||||
if os.path.isdir(DATA_DIR):
|
if os.path.isdir(DATA_DIR):
|
||||||
STATICFILES_DIRS += [
|
STATICFILES_DIRS += [
|
||||||
|
|||||||
@@ -53,6 +53,15 @@ CACHES = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
XQUEUE_INTERFACE = {
|
||||||
|
"url": "http://xqueue.sandbox.edx.org",
|
||||||
|
"django_auth": {
|
||||||
|
"username": "lms",
|
||||||
|
"password": "***REMOVED***"
|
||||||
|
},
|
||||||
|
"basic_auth": ('anant', 'agarwal'),
|
||||||
|
}
|
||||||
|
|
||||||
# Make the keyedcache startup warnings go away
|
# Make the keyedcache startup warnings go away
|
||||||
CACHE_TIMEOUT = 0
|
CACHE_TIMEOUT = 0
|
||||||
|
|
||||||
|
|||||||
@@ -41,3 +41,6 @@ def course_db_for(course_id):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def askbot_url_for(course_id):
|
||||||
|
return "courses/{0}/discussions/".format(course_id)
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
from .courses import *
|
from .courses import *
|
||||||
|
|
||||||
DATABASES = course_db_for('HarvardX/CS50x/2012')
|
DATABASES = course_db_for('HarvardX/CS50x/2012')
|
||||||
|
ASKBOT_URL = askbot_url_for("HarvardX/CS50x/2012")
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
from .courses import *
|
from .courses import *
|
||||||
|
|
||||||
DATABASES = course_db_for('MITx/6.002x/2012_Fall')
|
DATABASES = course_db_for('MITx/6.002x/2012_Fall')
|
||||||
|
ASKBOT_URL = askbot_url_for("MITx/6.002x/2012_Fall")
|
||||||
|
|||||||
@@ -50,6 +50,16 @@ COMMON_TEST_DATA_ROOT = COMMON_ROOT / "test" / "data"
|
|||||||
GITHUB_REPO_ROOT = ENV_ROOT / "data"
|
GITHUB_REPO_ROOT = ENV_ROOT / "data"
|
||||||
|
|
||||||
|
|
||||||
|
XQUEUE_INTERFACE = {
|
||||||
|
"url": "http://xqueue.sandbox.edx.org",
|
||||||
|
"django_auth": {
|
||||||
|
"username": "lms",
|
||||||
|
"password": "***REMOVED***"
|
||||||
|
},
|
||||||
|
"basic_auth": ('anant', 'agarwal'),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# TODO (cpennington): We need to figure out how envs/test.py can inject things
|
# TODO (cpennington): We need to figure out how envs/test.py can inject things
|
||||||
# into common.py so that we don't have to repeat this sort of thing
|
# into common.py so that we don't have to repeat this sort of thing
|
||||||
STATICFILES_DIRS = [
|
STATICFILES_DIRS = [
|
||||||
|
|||||||
BIN
lms/static/images/search-icon.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
76
lms/static/js/jquery.gradebook.js
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var Gradebook = function($element) {
|
||||||
|
var _this = this;
|
||||||
|
var $element = $element;
|
||||||
|
var $grades = $element.find('.grades');
|
||||||
|
var $gradeTable = $element.find('.grade-table');
|
||||||
|
var $leftShadow = $('<div class="left-shadow"></div>');
|
||||||
|
var $rightShadow = $('<div class="right-shadow"></div>');
|
||||||
|
var tableHeight = $gradeTable.height();
|
||||||
|
var maxScroll = $gradeTable.width() - $grades.width();
|
||||||
|
var $body = $('body');
|
||||||
|
var mouseOrigin;
|
||||||
|
var tableOrigin;
|
||||||
|
|
||||||
|
var startDrag = function(e) {
|
||||||
|
mouseOrigin = e.pageX;
|
||||||
|
tableOrigin = $gradeTable.position().left;
|
||||||
|
$body.css('-webkit-user-select', 'none');
|
||||||
|
$body.bind('mousemove', moveDrag);
|
||||||
|
$body.bind('mouseup', stopDrag);
|
||||||
|
};
|
||||||
|
|
||||||
|
var moveDrag = function(e) {
|
||||||
|
var offset = e.pageX - mouseOrigin;
|
||||||
|
var targetLeft = clamp(tableOrigin + offset, -maxScroll, 0);
|
||||||
|
|
||||||
|
updateHorizontalPosition(targetLeft);
|
||||||
|
|
||||||
|
setShadows(targetLeft);
|
||||||
|
};
|
||||||
|
|
||||||
|
var stopDrag = function(e) {
|
||||||
|
$body.css('-webkit-user-select', 'auto');
|
||||||
|
$body.unbind('mousemove', moveDrag);
|
||||||
|
$body.unbind('mouseup', stopDrag);
|
||||||
|
};
|
||||||
|
|
||||||
|
var setShadows = function(left) {
|
||||||
|
var padding = 30;
|
||||||
|
|
||||||
|
var leftPercent = clamp(-left / padding, 0, 1);
|
||||||
|
$leftShadow.css('opacity', leftPercent);
|
||||||
|
|
||||||
|
var rightPercent = clamp((maxScroll + left) / padding, 0, 1);
|
||||||
|
$rightShadow.css('opacity', rightPercent);
|
||||||
|
};
|
||||||
|
|
||||||
|
var clamp = function(val, min, max) {
|
||||||
|
if(val > max) return max;
|
||||||
|
if(val < min) return min;
|
||||||
|
return val;
|
||||||
|
};
|
||||||
|
|
||||||
|
var updateWidths = function(e) {
|
||||||
|
maxScroll = $gradeTable.width() - $grades.width();
|
||||||
|
var targetLeft = clamp($gradeTable.position().left, -maxScroll, 0);
|
||||||
|
updateHorizontalPosition(targetLeft);
|
||||||
|
setShadows(targetLeft);
|
||||||
|
}
|
||||||
|
|
||||||
|
var updateHorizontalPosition = function(left) {
|
||||||
|
$gradeTable.css({
|
||||||
|
'left': left + 'px'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$leftShadow.css('height', tableHeight + 'px');
|
||||||
|
$rightShadow.css('height', tableHeight + 'px');
|
||||||
|
$grades.append($leftShadow).append($rightShadow);
|
||||||
|
setShadows(0);
|
||||||
|
$grades.css('height', tableHeight);
|
||||||
|
$gradeTable.bind('mousedown', startDrag);
|
||||||
|
$(window).bind('resize', updateWidths);
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
@import 'shared/tooltips';
|
@import 'shared/tooltips';
|
||||||
|
|
||||||
// Course base / layout styles
|
// Course base / layout styles
|
||||||
@import 'course/layout/courseware_subnav';
|
@import 'course/layout/courseware_header';
|
||||||
@import 'course/base/base';
|
@import 'course/base/base';
|
||||||
@import 'course/base/extends';
|
@import 'course/base/extends';
|
||||||
@import 'module/module-styles.scss';
|
@import 'module/module-styles.scss';
|
||||||
|
|||||||
@@ -1,11 +1,203 @@
|
|||||||
|
$cell-border-color: #e1e1e1;
|
||||||
|
$table-border-color: #c8c8c8;
|
||||||
|
|
||||||
div.gradebook-wrapper {
|
div.gradebook-wrapper {
|
||||||
@extend .table-wrapper;
|
@extend .table-wrapper;
|
||||||
|
|
||||||
section.gradebook-content {
|
section.gradebook-content {
|
||||||
@extend .content;
|
@extend .content;
|
||||||
|
|
||||||
|
.student-search {
|
||||||
|
padding: 0 20px 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.student-search-field {
|
||||||
|
width: 100%;
|
||||||
|
height: 27px;
|
||||||
|
padding: 0 15px 0 35px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 13px;
|
||||||
|
border: 1px solid $table-border-color;
|
||||||
|
background: url(../images/search-icon.png) no-repeat 9px center #f6f6f6;
|
||||||
|
font-family: $sans-serif;
|
||||||
|
font-size: 11px;
|
||||||
|
@include box-shadow(0 1px 4px rgba(0, 0, 0, .12) inset);
|
||||||
|
outline: none;
|
||||||
|
@include transition(border-color .15s);
|
||||||
|
|
||||||
|
&::-webkit-input-placeholder,
|
||||||
|
&::-moz-input-placeholder {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: #1d9dd9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.student-table {
|
||||||
|
float: left;
|
||||||
|
// width: 264px;
|
||||||
|
width: 24%;
|
||||||
|
border-radius: 3px 0 0 3px;
|
||||||
|
color: #3c3c3c;
|
||||||
|
|
||||||
|
th {
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:first-child td {
|
||||||
|
border-top: 1px solid $table-border-color;
|
||||||
|
border-radius: 5px 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:last-child td {
|
||||||
|
border-bottom: 1px solid $table-border-color;
|
||||||
|
border-radius: 0 0 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
height: 50px;
|
||||||
|
padding-left: 20px;
|
||||||
|
border-bottom: 1px solid $cell-border-color;
|
||||||
|
border-left: 1px solid $table-border-color;
|
||||||
|
background: #f3f3f3;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:nth-child(odd) td {
|
||||||
|
background-color: #fbfbfb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.grades {
|
||||||
|
position: relative;
|
||||||
|
float: left;
|
||||||
|
width: 76%;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.left-shadow,
|
||||||
|
.right-shadow {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
width: 20px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-shadow {
|
||||||
|
left: 0;
|
||||||
|
background: -webkit-linear-gradient(left, rgba(0, 0, 0, .1), rgba(0, 0, 0, 0) 20%), -webkit-linear-gradient(left, rgba(0, 0, 0, .1), rgba(0, 0, 0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-shadow {
|
||||||
|
right: 0;
|
||||||
|
background: -webkit-linear-gradient(right, rgba(0, 0, 0, .1), rgba(0, 0, 0, 0) 20%), -webkit-linear-gradient(right, rgba(0, 0, 0, .1), rgba(0, 0, 0, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.grade-table {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 1000px;
|
||||||
|
cursor: move;
|
||||||
|
-webkit-transition: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
width: 50px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead th {
|
||||||
|
position: relative;
|
||||||
|
height: 50px;
|
||||||
|
background: -webkit-linear-gradient(top, $cell-border-color, #ddd);
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 1px 0 $table-border-color inset, 0 2px 0 rgba(255, 255, 255, .7) inset;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
width: 1px;
|
||||||
|
height: 100%;
|
||||||
|
background: -webkit-linear-gradient(top, rgba(0, 0, 0, 0) 30%, rgba(0, 0, 0, .15));
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-radius: 5px 0 0 0;
|
||||||
|
box-shadow: 1px 1px 0 $table-border-color inset, 1px 2px 0 rgba(255, 255, 255, .7) inset;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
display: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-radius: 0 3px 0 0;
|
||||||
|
box-shadow: -1px 1px 0 $table-border-color inset, -1px 2px 0 rgba(255, 255, 255, .7) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assignment {
|
||||||
|
margin: 9px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type,
|
||||||
|
.number,
|
||||||
|
.max {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.max {
|
||||||
|
height: 12px;
|
||||||
|
background: -webkit-linear-gradient(top, #c6c6c6, #bababa);
|
||||||
|
font-size: 9px;
|
||||||
|
line-height: 12px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tr {
|
||||||
|
border-right: 1px solid $table-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:first-child td {
|
||||||
|
border-top: 1px solid $table-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:last-child td {
|
||||||
|
border-bottom: 1px solid $table-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
height: 50px;
|
||||||
|
border-bottom: 1px solid $cell-border-color;
|
||||||
|
background: #f3f3f3;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 50px;
|
||||||
|
border-left: 1px solid $cell-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:nth-child(odd) td {
|
||||||
|
background-color: #fbfbfb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
@extend .top-header;
|
@extend .top-header;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -173,29 +173,3 @@ h1.top-header {
|
|||||||
@include transition( all, .2s, $ease-in-out-quad);
|
@include transition( all, .2s, $ease-in-out-quad);
|
||||||
}
|
}
|
||||||
|
|
||||||
.global {
|
|
||||||
.find-courses-button {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
display: block;
|
|
||||||
float: left;
|
|
||||||
font-size: 0.9em;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 0;
|
|
||||||
line-height: 40px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
text-shadow: 0 1px 0 #fff;
|
|
||||||
text-transform: none;
|
|
||||||
white-space: nowrap;
|
|
||||||
width: 700px;
|
|
||||||
|
|
||||||
.provider {
|
|
||||||
font: inherit;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #6d6d6d;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
85
lms/static/sass/course/layout/_courseware_header.scss
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
nav.course-material {
|
||||||
|
@include clearfix;
|
||||||
|
@include box-sizing(border-box);
|
||||||
|
background: #f6f6f6;
|
||||||
|
border-bottom: 1px solid rgb(200,200,200);
|
||||||
|
margin: 0px auto 0px;
|
||||||
|
padding: 0px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.inner-wrapper {
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 1200px;
|
||||||
|
width: flex-grid(12);
|
||||||
|
}
|
||||||
|
|
||||||
|
ol.course-tabs {
|
||||||
|
@include border-top-radius(4px);
|
||||||
|
@include clearfix;
|
||||||
|
padding: 10px 0 0 0;
|
||||||
|
|
||||||
|
li {
|
||||||
|
float: left;
|
||||||
|
list-style: none;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: darken($lighter-base-font-color, 20%);
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
padding: 8px 13px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
text-decoration: none;
|
||||||
|
text-shadow: 0 1px rgb(255,255,255);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $base-font-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: rgb(255,255,255);
|
||||||
|
border: 1px solid rgb(200,200,200);
|
||||||
|
border-bottom: 0px;
|
||||||
|
@include border-top-radius(4px);
|
||||||
|
@include box-shadow(0 2px 0 0 rgba(255,255,255, 1));
|
||||||
|
color: $blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-content {
|
||||||
|
margin-top: 30px;
|
||||||
|
|
||||||
|
.courseware {
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.global {
|
||||||
|
.find-courses-button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
display: block;
|
||||||
|
width: 700px;
|
||||||
|
float: left;
|
||||||
|
font-size: 0.9em;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 40px;
|
||||||
|
letter-spacing: 0;
|
||||||
|
text-transform: none;
|
||||||
|
text-shadow: 0 1px 0 #fff;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.provider {
|
||||||
|
font: inherit;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #6d6d6d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ def url_class(url):
|
|||||||
return ""
|
return ""
|
||||||
%>
|
%>
|
||||||
<%! from django.core.urlresolvers import reverse %>
|
<%! from django.core.urlresolvers import reverse %>
|
||||||
<%! from courseware.courses import has_staff_access_to_course_id %>
|
<%! from courseware.access import has_access %>
|
||||||
|
|
||||||
<nav class="${active_page} course-material">
|
<nav class="${active_page} course-material">
|
||||||
<div class="inner-wrapper">
|
<div class="inner-wrapper">
|
||||||
@@ -33,7 +33,7 @@ def url_class(url):
|
|||||||
% if user.is_authenticated():
|
% if user.is_authenticated():
|
||||||
<li class="profile"><a href="${reverse('profile', args=[course.id])}" class="${url_class('profile')}">Profile</a></li>
|
<li class="profile"><a href="${reverse('profile', args=[course.id])}" class="${url_class('profile')}">Profile</a></li>
|
||||||
% endif
|
% endif
|
||||||
% if has_staff_access_to_course_id(user, course.id):
|
% if has_access(user, course, 'staff'):
|
||||||
<li class="instructor"><a href="${reverse('instructor_dashboard', args=[course.id])}" class="${url_class('instructor')}">Instructor</a></li>
|
<li class="instructor"><a href="${reverse('instructor_dashboard', args=[course.id])}" class="${url_class('instructor')}">Instructor</a></li>
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<%!
|
<%!
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from courseware.courses import course_image_url, get_course_about_section
|
from courseware.courses import course_image_url, get_course_about_section
|
||||||
|
from courseware.access import has_access
|
||||||
%>
|
%>
|
||||||
<%inherit file="main.html" />
|
<%inherit file="main.html" />
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@
|
|||||||
$("#unenroll_course_number").text( $(event.target).data("course-number") );
|
$("#unenroll_course_number").text( $(event.target).data("course-number") );
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$(document).delegate('#unenroll_form', 'ajax:success', function(data, json, xhr) {
|
$(document).delegate('#unenroll_form', 'ajax:success', function(data, json, xhr) {
|
||||||
if(json.success) {
|
if(json.success) {
|
||||||
location.href="${reverse('dashboard')}";
|
location.href="${reverse('dashboard')}";
|
||||||
@@ -33,7 +34,7 @@
|
|||||||
</%block>
|
</%block>
|
||||||
|
|
||||||
<section class="container dashboard">
|
<section class="container dashboard">
|
||||||
|
|
||||||
%if message:
|
%if message:
|
||||||
<section class="dashboard-banner">
|
<section class="dashboard-banner">
|
||||||
${message}
|
${message}
|
||||||
@@ -66,7 +67,7 @@
|
|||||||
|
|
||||||
<article class="my-course">
|
<article class="my-course">
|
||||||
<%
|
<%
|
||||||
if course.has_started() or settings.MITX_FEATURES['DISABLE_START_DATES']:
|
if has_access(user, course, 'load'):
|
||||||
course_target = reverse('info', args=[course.id])
|
course_target = reverse('info', args=[course.id])
|
||||||
else:
|
else:
|
||||||
course_target = reverse('about_course', args=[course.id])
|
course_target = reverse('about_course', args=[course.id])
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.js')}"></script>
|
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.js')}"></script>
|
||||||
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.stack.js')}"></script>
|
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.stack.js')}"></script>
|
||||||
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.symbol.js')}"></script>
|
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.symbol.js')}"></script>
|
||||||
|
<script type="text/javascript" src="${static.url('js/jquery.gradebook.js')}"></script>
|
||||||
</%block>
|
</%block>
|
||||||
|
|
||||||
<%block name="headextra">
|
<%block name="headextra">
|
||||||
@@ -19,6 +20,12 @@
|
|||||||
.grade_None {color:LightGray;}
|
.grade_None {color:LightGray;}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(document).ready(function() {
|
||||||
|
var gradebook = new Gradebook($('.gradebook-content'));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
</%block>
|
</%block>
|
||||||
|
|
||||||
<%include file="course_navigation.html" args="active_page=''" />
|
<%include file="course_navigation.html" args="active_page=''" />
|
||||||
@@ -28,50 +35,75 @@
|
|||||||
<section class="gradebook-content">
|
<section class="gradebook-content">
|
||||||
<h1>Gradebook</h1>
|
<h1>Gradebook</h1>
|
||||||
|
|
||||||
%if len(students) > 0:
|
<table class="student-table">
|
||||||
<table>
|
<thead>
|
||||||
<%
|
<tr>
|
||||||
templateSummary = students[0]['grade_summary']
|
<th>
|
||||||
%>
|
<form class="student-search">
|
||||||
|
<input type="search" class="student-search-field" placeholder="Search students" />
|
||||||
|
</form>
|
||||||
<tr> <!-- Header Row -->
|
</th>
|
||||||
<th>Student</th>
|
</tr>
|
||||||
%for section in templateSummary['section_breakdown']:
|
</thead>
|
||||||
<th>${section['label']}</th>
|
<tbody>
|
||||||
|
%for student in students:
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="${reverse('student_profile', kwargs=dict(course_id=course_id, student_id=student['id']))}">${student['username']}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
%endfor
|
%endfor
|
||||||
<th>Total</th>
|
</tbody>
|
||||||
</tr>
|
|
||||||
|
|
||||||
<%def name="percent_data(fraction)">
|
|
||||||
<%
|
|
||||||
letter_grade = 'None'
|
|
||||||
if fraction > 0:
|
|
||||||
letter_grade = 'F'
|
|
||||||
for grade in ['A', 'B', 'C']:
|
|
||||||
if fraction >= course.grade_cutoffs[grade]:
|
|
||||||
letter_grade = grade
|
|
||||||
break
|
|
||||||
|
|
||||||
data_class = "grade_" + letter_grade
|
|
||||||
%>
|
|
||||||
<td class="${data_class}" data-percent="${fraction}">${ "{0:.0f}".format( 100 * fraction ) }</td>
|
|
||||||
</%def>
|
|
||||||
|
|
||||||
%for student in students:
|
|
||||||
<tr>
|
|
||||||
<td><a href="${reverse('student_profile',
|
|
||||||
kwargs=dict(course_id=course_id,
|
|
||||||
student_id=student['id']))}">
|
|
||||||
${student['username']}</a></td>
|
|
||||||
%for section in student['grade_summary']['section_breakdown']:
|
|
||||||
${percent_data( section['percent'] )}
|
|
||||||
%endfor
|
|
||||||
<th>${percent_data( student['grade_summary']['percent'])}</th>
|
|
||||||
</tr>
|
|
||||||
%endfor
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
%if len(students) > 0:
|
||||||
|
<div class="grades">
|
||||||
|
<table class="grade-table">
|
||||||
|
<%
|
||||||
|
templateSummary = students[0]['grade_summary']
|
||||||
|
%>
|
||||||
|
<thead>
|
||||||
|
<tr> <!-- Header Row -->
|
||||||
|
%for section in templateSummary['section_breakdown']:
|
||||||
|
<th>${section['label']}</th>
|
||||||
|
%endfor
|
||||||
|
<th>Total</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<%def name="percent_data(fraction)">
|
||||||
|
<%
|
||||||
|
letter_grade = 'None'
|
||||||
|
if fraction > 0:
|
||||||
|
letter_grade = 'F'
|
||||||
|
for grade in ['A', 'B', 'C']:
|
||||||
|
if fraction >= course.grade_cutoffs[grade]:
|
||||||
|
letter_grade = grade
|
||||||
|
break
|
||||||
|
|
||||||
|
data_class = "grade_" + letter_grade
|
||||||
|
%>
|
||||||
|
<td class="${data_class}" data-percent="${fraction}">${ "{0:.0f}".format( 100 * fraction ) }</td>
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
%for student in students:
|
||||||
|
<tr>
|
||||||
|
%for section in student['grade_summary']['section_breakdown']:
|
||||||
|
${percent_data( section['percent'] )}
|
||||||
|
%endfor
|
||||||
|
<td>${percent_data( student['grade_summary']['percent'])}</td>
|
||||||
|
</tr>
|
||||||
|
%endfor
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
%endif
|
%endif
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,10 @@
|
|||||||
<h1>There has been an error on the <em>MITx</em> servers</h1>
|
<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>
|
<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>Details below:</h1>
|
||||||
<h1>Staff-only details below:</h1>
|
|
||||||
|
|
||||||
<p>Error: ${error | h}</p>
|
<p>Error: ${error | h}</p>
|
||||||
|
|
||||||
<p>Raw data: ${data | h}</p>
|
<p>Raw data: ${data | h}</p>
|
||||||
% endif
|
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<%!
|
<%!
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from courseware.courses import course_image_url, get_course_about_section
|
from courseware.courses import course_image_url, get_course_about_section
|
||||||
|
from courseware.access import has_access
|
||||||
%>
|
%>
|
||||||
<%namespace name='static' file='../static_content.html'/>
|
<%namespace name='static' file='../static_content.html'/>
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@
|
|||||||
$(".register").click(function() {
|
$(".register").click(function() {
|
||||||
$("#class_enroll_form").submit();
|
$("#class_enroll_form").submit();
|
||||||
});
|
});
|
||||||
|
|
||||||
$(document).delegate('#class_enroll_form', 'ajax:success', function(data, json, xhr) {
|
$(document).delegate('#class_enroll_form', 'ajax:success', function(data, json, xhr) {
|
||||||
if(json.success) {
|
if(json.success) {
|
||||||
location.href="${reverse('dashboard')}";
|
location.href="${reverse('dashboard')}";
|
||||||
@@ -64,7 +65,7 @@
|
|||||||
%if registered:
|
%if registered:
|
||||||
<%
|
<%
|
||||||
## TODO: move this logic into a view
|
## TODO: move this logic into a view
|
||||||
if course.has_started() or settings.MITX_FEATURES['DISABLE_START_DATES']:
|
if has_access(user, course, 'load'):
|
||||||
course_target = reverse('info', args=[course.id])
|
course_target = reverse('info', args=[course.id])
|
||||||
else:
|
else:
|
||||||
course_target = reverse('about_course', args=[course.id])
|
course_target = reverse('about_course', args=[course.id])
|
||||||
|
|||||||