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,
|
||||
debug=True,
|
||||
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_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 ###################################
|
||||
# List of finder classes that know how to find static files in
|
||||
# various locations.
|
||||
|
||||
@@ -38,8 +38,8 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
|
||||
from datetime import date
|
||||
from collections import namedtuple
|
||||
from courseware.courses import (course_staff_group_name, has_staff_access_to_course,
|
||||
get_courses_by_university)
|
||||
from courseware.courses import get_courses_by_university
|
||||
from courseware.access import has_access
|
||||
|
||||
log = logging.getLogger("mitx.student")
|
||||
Article = namedtuple('Article', 'title url author image deck publication publish_date')
|
||||
@@ -166,22 +166,6 @@ def change_enrollment_view(request):
|
||||
"""Delegate to change_enrollment to actually do the work."""
|
||||
return HttpResponse(json.dumps(change_enrollment(request)))
|
||||
|
||||
def enrollment_allowed(user, course):
|
||||
"""If the course has an enrollment period, check whether we are in it.
|
||||
Also respects the DARK_LAUNCH setting"""
|
||||
now = time.gmtime()
|
||||
start = course.enrollment_start
|
||||
end = course.enrollment_end
|
||||
|
||||
if (start is None or now > start) and (end is None or now < end):
|
||||
# in enrollment period.
|
||||
return True
|
||||
|
||||
if settings.MITX_FEATURES['DARK_LAUNCH']:
|
||||
if has_staff_access_to_course(user, course):
|
||||
# if dark launch, staff can enroll outside enrollment window
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def change_enrollment(request):
|
||||
@@ -209,18 +193,7 @@ def change_enrollment(request):
|
||||
.format(user.username, enrollment.course_id))
|
||||
return {'success': False, 'error': 'The course requested does not exist.'}
|
||||
|
||||
if settings.MITX_FEATURES.get('ACCESS_REQUIRE_STAFF_FOR_COURSE'):
|
||||
# require that user be in the staff_* group (or be an
|
||||
# overall admin) to be able to enroll eg staff_6.002x or
|
||||
# staff_6.00x
|
||||
if not has_staff_access_to_course(user, course):
|
||||
staff_group = course_staff_group_name(course)
|
||||
log.debug('user %s denied enrollment to %s ; not in %s' % (
|
||||
user, course.location.url(), staff_group))
|
||||
return {'success': False,
|
||||
'error' : '%s membership required to access course.' % staff_group}
|
||||
|
||||
if not enrollment_allowed(user, course):
|
||||
if not has_access(user, course, 'enroll'):
|
||||
return {'success': False,
|
||||
'error': 'enrollment in {} not allowed at this time'
|
||||
.format(course.display_name)}
|
||||
|
||||
@@ -7,13 +7,10 @@ import logging
|
||||
import requests
|
||||
import time
|
||||
|
||||
# TODO: Collection of parameters to be hooked into rest of edX system
|
||||
XQUEUE_LMS_AUTH = { 'username': 'LMS',
|
||||
'password': 'PaloAltoCA' }
|
||||
XQUEUE_URL = 'http://xqueue.edx.org'
|
||||
|
||||
log = logging.getLogger('mitx.' + __name__)
|
||||
|
||||
|
||||
def make_hashkey(seed=None):
|
||||
'''
|
||||
Generate a string key by hashing
|
||||
@@ -58,15 +55,15 @@ def parse_xreply(xreply):
|
||||
return (return_code, content)
|
||||
|
||||
|
||||
class XqueueInterface:
|
||||
class XQueueInterface(object):
|
||||
'''
|
||||
Interface to the external grading system
|
||||
'''
|
||||
|
||||
def __init__(self, url=XQUEUE_URL, auth=XQUEUE_LMS_AUTH):
|
||||
def __init__(self, url, django_auth, requests_auth=None):
|
||||
self.url = url
|
||||
self.auth = auth
|
||||
self.session = requests.session()
|
||||
self.auth = django_auth
|
||||
self.session = requests.session(auth=requests_auth)
|
||||
|
||||
def send_to_queue(self, header, body, file_to_upload=None):
|
||||
'''
|
||||
@@ -117,5 +114,3 @@ class XqueueInterface:
|
||||
return (1, 'unexpected HTTP status code [%d]' % r.status_code)
|
||||
|
||||
return parse_xreply(r.text)
|
||||
|
||||
qinterface = XqueueInterface()
|
||||
|
||||
@@ -49,9 +49,9 @@ class ABTestModule(XModule):
|
||||
return json.dumps({'group': self.group})
|
||||
|
||||
def displayable_items(self):
|
||||
return [self.system.get_module(child)
|
||||
for child
|
||||
in self.definition['data']['group_content'][self.group]]
|
||||
return filter(None, [self.system.get_module(child)
|
||||
for child
|
||||
in self.definition['data']['group_content'][self.group]])
|
||||
|
||||
|
||||
# TODO (cpennington): Use Groups should be a first class object, rather than being
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
from fs.errors import ResourceNotFoundError
|
||||
import time
|
||||
import dateutil.parser
|
||||
import logging
|
||||
|
||||
from xmodule.util.decorators import lazyproperty
|
||||
from xmodule.graders import load_grading_policy
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.seq_module import SequenceDescriptor, SequenceModule
|
||||
from xmodule.timeparse import parse_time, stringify_time
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -18,38 +18,15 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
super(CourseDescriptor, self).__init__(system, definition, **kwargs)
|
||||
|
||||
msg = None
|
||||
try:
|
||||
self.start = time.strptime(self.metadata["start"], "%Y-%m-%dT%H:%M")
|
||||
except KeyError:
|
||||
msg = "Course loaded without a start date. id = %s" % self.id
|
||||
except ValueError as e:
|
||||
msg = "Course loaded with a bad start date. %s '%s'" % (self.id, e)
|
||||
|
||||
# Don't call the tracker from the exception handler.
|
||||
if msg is not None:
|
||||
self.start = time.gmtime(0) # The epoch
|
||||
if self.start is None:
|
||||
msg = "Course loaded without a valid start date. id = %s" % self.id
|
||||
# hack it -- start in 1970
|
||||
self.metadata['start'] = stringify_time(time.gmtime(0))
|
||||
log.critical(msg)
|
||||
system.error_tracker(msg)
|
||||
|
||||
def try_parse_time(key):
|
||||
"""
|
||||
Parse an optional metadata key: if present, must be valid.
|
||||
Return None if not present.
|
||||
"""
|
||||
if key in self.metadata:
|
||||
try:
|
||||
return time.strptime(self.metadata[key], "%Y-%m-%dT%H:%M")
|
||||
except ValueError as e:
|
||||
msg = "Course %s loaded with a bad metadata key %s '%s'" % (
|
||||
self.id, self.metadata[key], e)
|
||||
log.warning(msg)
|
||||
return None
|
||||
|
||||
self.enrollment_start = try_parse_time("enrollment_start")
|
||||
self.enrollment_end = try_parse_time("enrollment_end")
|
||||
|
||||
|
||||
|
||||
self.enrollment_start = self._try_parse_time("enrollment_start")
|
||||
self.enrollment_end = self._try_parse_time("enrollment_end")
|
||||
|
||||
def has_started(self):
|
||||
return time.gmtime() > self.start
|
||||
@@ -154,6 +131,7 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
"""Return the course_id for this course"""
|
||||
return self.location_to_id(self.location)
|
||||
|
||||
@property
|
||||
|
||||
@@ -24,16 +24,8 @@ class ErrorModule(XModule):
|
||||
return self.system.render_template('module-error.html', {
|
||||
'data' : self.definition['data']['contents'],
|
||||
'error' : self.definition['data']['error_msg'],
|
||||
'is_staff' : self.system.is_staff,
|
||||
})
|
||||
|
||||
def displayable_items(self):
|
||||
"""Hide errors in the profile and table of contents for non-staff
|
||||
users.
|
||||
"""
|
||||
if self.system.is_staff:
|
||||
return [self]
|
||||
return []
|
||||
|
||||
class ErrorDescriptor(EditingDescriptor):
|
||||
"""
|
||||
|
||||
@@ -32,22 +32,37 @@ class @Problem
|
||||
|
||||
queueing: =>
|
||||
@queued_items = @$(".xqueue")
|
||||
if @queued_items.length > 0
|
||||
@num_queued_items = @queued_items.length
|
||||
if @num_queued_items > 0
|
||||
if window.queuePollerID # Only one poller 'thread' per Problem
|
||||
window.clearTimeout(window.queuePollerID)
|
||||
window.queuePollerID = window.setTimeout(@poll, 100)
|
||||
queuelen = @get_queuelen()
|
||||
window.queuePollerID = window.setTimeout(@poll, queuelen*10)
|
||||
|
||||
# Retrieves the minimum queue length of all queued items
|
||||
get_queuelen: =>
|
||||
minlen = Infinity
|
||||
@queued_items.each (index, qitem) ->
|
||||
len = parseInt($.text(qitem))
|
||||
if len < minlen
|
||||
minlen = len
|
||||
return minlen
|
||||
|
||||
poll: =>
|
||||
$.postWithPrefix "#{@url}/problem_get", (response) =>
|
||||
@queued_items = $(response.html).find(".xqueue")
|
||||
if @queued_items.length == 0
|
||||
# If queueing status changed, then render
|
||||
@new_queued_items = $(response.html).find(".xqueue")
|
||||
if @new_queued_items.length isnt @num_queued_items
|
||||
@el.html(response.html)
|
||||
@executeProblemScripts () =>
|
||||
@setupInputTypes()
|
||||
@bind()
|
||||
|
||||
@num_queued_items = @new_queued_items.length
|
||||
if @num_queued_items == 0
|
||||
delete window.queuePollerID
|
||||
else
|
||||
# TODO: Dynamically adjust timeout interval based on @queued_items.value
|
||||
# TODO: Some logic to dynamically adjust polling rate based on queuelen
|
||||
window.queuePollerID = window.setTimeout(@poll, 1000)
|
||||
|
||||
render: (content) ->
|
||||
@@ -141,9 +156,16 @@ class @Problem
|
||||
|
||||
fd = new FormData()
|
||||
|
||||
# Sanity check of file size
|
||||
file_too_large = false
|
||||
max_filesize = 4*1000*1000 # 4 MB
|
||||
|
||||
@inputs.each (index, element) ->
|
||||
if element.type is 'file'
|
||||
if element.files[0] instanceof File
|
||||
if element.files[0].size > max_filesize
|
||||
file_too_large = true
|
||||
alert 'Submission aborted! Your file "' + element.files[0].name + '" is too large (max size: ' + max_filesize/(1000*1000) + ' MB)'
|
||||
fd.append(element.id, element.files[0])
|
||||
else
|
||||
fd.append(element.id, '')
|
||||
@@ -163,7 +185,8 @@ class @Problem
|
||||
else
|
||||
alert(response.success)
|
||||
|
||||
$.ajaxWithPrefix("#{@url}/problem_check", settings)
|
||||
if not file_too_large
|
||||
$.ajaxWithPrefix("#{@url}/problem_check", settings)
|
||||
|
||||
check: =>
|
||||
Logger.log 'problem_check', @answers
|
||||
|
||||
@@ -35,7 +35,6 @@ i4xs = ModuleSystem(
|
||||
filestore=fs.osfs.OSFS(os.path.dirname(os.path.realpath(__file__))+"/test_files"),
|
||||
debug=True,
|
||||
xqueue={'interface':None, 'callback_url':'/', 'default_queuename': 'testqueue'},
|
||||
is_staff=False,
|
||||
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules")
|
||||
)
|
||||
|
||||
@@ -336,7 +335,7 @@ class CodeResponseTest(unittest.TestCase):
|
||||
self.assertFalse(test_lcp.correct_map.is_queued(answer_ids[j])) # Should be dequeued, message delivered
|
||||
else:
|
||||
self.assertTrue(test_lcp.correct_map.is_queued(answer_ids[j])) # Should be queued, message undelivered
|
||||
|
||||
|
||||
def test_convert_files_to_filenames(self):
|
||||
problem_file = os.path.dirname(__file__) + "/test_files/coderesponse.xml"
|
||||
fp = open(problem_file)
|
||||
@@ -347,7 +346,7 @@ class CodeResponseTest(unittest.TestCase):
|
||||
self.assertEquals(answers_converted['1_2_1'], 'String-based answer')
|
||||
self.assertEquals(answers_converted['1_3_1'], ['answer1', 'answer2', 'answer3'])
|
||||
self.assertEquals(answers_converted['1_4_1'], fp.name)
|
||||
|
||||
|
||||
|
||||
class ChoiceResponseTest(unittest.TestCase):
|
||||
|
||||
|
||||
19
common/lib/xmodule/xmodule/timeparse.py
Normal file
@@ -0,0 +1,19 @@
|
||||
"""
|
||||
Helper functions for handling time in the format we like.
|
||||
"""
|
||||
import time
|
||||
|
||||
TIME_FORMAT = "%Y-%m-%dT%H:%M"
|
||||
|
||||
def parse_time(time_str):
|
||||
"""
|
||||
Takes a time string in TIME_FORMAT, returns
|
||||
it as a time_struct. Raises ValueError if the string is not in the right format.
|
||||
"""
|
||||
return time.strptime(time_str, TIME_FORMAT)
|
||||
|
||||
def stringify_time(time_struct):
|
||||
"""
|
||||
Convert a time struct to a string
|
||||
"""
|
||||
return time.strftime(TIME_FORMAT, time_struct)
|
||||
@@ -8,8 +8,9 @@ from lxml import etree
|
||||
from lxml.etree import XMLSyntaxError
|
||||
from pprint import pprint
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.errortracker import exc_info_to_str
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.timeparse import parse_time
|
||||
|
||||
log = logging.getLogger('mitx.' + __name__)
|
||||
|
||||
@@ -218,9 +219,11 @@ class XModule(HTMLSnippet):
|
||||
Return module instances for all the children of this module.
|
||||
'''
|
||||
if self._loaded_children is None:
|
||||
self._loaded_children = [
|
||||
self.system.get_module(child)
|
||||
for child in self.definition.get('children', [])]
|
||||
# get_module returns None if the current user doesn't have access
|
||||
# to the location.
|
||||
self._loaded_children = filter(None,
|
||||
[self.system.get_module(child)
|
||||
for child in self.definition.get('children', [])])
|
||||
|
||||
return self._loaded_children
|
||||
|
||||
@@ -396,6 +399,15 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
|
||||
return self.metadata.get('display_name',
|
||||
self.url_name.replace('_', ' '))
|
||||
|
||||
@property
|
||||
def start(self):
|
||||
"""
|
||||
If self.metadata contains start, return it. Else return None.
|
||||
"""
|
||||
if 'start' not in self.metadata:
|
||||
return None
|
||||
return self._try_parse_time('start')
|
||||
|
||||
@property
|
||||
def own_metadata(self):
|
||||
"""
|
||||
@@ -596,6 +608,24 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
|
||||
metadata=self.metadata
|
||||
))
|
||||
|
||||
# ================================ Internal helpers =======================
|
||||
|
||||
def _try_parse_time(self, key):
|
||||
"""
|
||||
Parse an optional metadata key containing a time: if present, complain
|
||||
if it doesn't parse.
|
||||
Return None if not present or invalid.
|
||||
"""
|
||||
if key in self.metadata:
|
||||
try:
|
||||
return parse_time(self.metadata[key])
|
||||
except ValueError as e:
|
||||
msg = "Descriptor {} loaded with a bad metadata key '{}': '{}'".format(
|
||||
self.location.url(), self.metadata[key], e)
|
||||
log.warning(msg)
|
||||
return None
|
||||
|
||||
|
||||
|
||||
class DescriptorSystem(object):
|
||||
def __init__(self, load_item, resources_fs, error_tracker, **kwargs):
|
||||
@@ -675,7 +705,6 @@ class ModuleSystem(object):
|
||||
filestore=None,
|
||||
debug=False,
|
||||
xqueue=None,
|
||||
is_staff=False,
|
||||
node_path=""):
|
||||
'''
|
||||
Create a closure around the system environment.
|
||||
@@ -688,7 +717,8 @@ class ModuleSystem(object):
|
||||
files. Update or remove.
|
||||
|
||||
get_module - function that takes (location) and returns a corresponding
|
||||
module instance object.
|
||||
module instance object. If the current user does not have
|
||||
access to that location, returns None.
|
||||
|
||||
render_template - a function that takes (template_file, context), and
|
||||
returns rendered html.
|
||||
@@ -705,9 +735,6 @@ class ModuleSystem(object):
|
||||
replace_urls - TEMPORARY - A function like static_replace.replace_urls
|
||||
that capa_module can use to fix up the static urls in
|
||||
ajax results.
|
||||
|
||||
is_staff - Is the user making the request a staff user?
|
||||
TODO (vshnayder): this will need to change once we have real user roles.
|
||||
'''
|
||||
self.ajax_url = ajax_url
|
||||
self.xqueue = xqueue
|
||||
@@ -718,7 +745,6 @@ class ModuleSystem(object):
|
||||
self.DEBUG = self.debug = debug
|
||||
self.seed = user.id if user is not None else 0
|
||||
self.replace_urls = replace_urls
|
||||
self.is_staff = is_staff
|
||||
self.node_path = node_path
|
||||
|
||||
def get(self, attr):
|
||||
|
||||
@@ -65,3 +65,4 @@ To run a single nose test:
|
||||
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">
|
||||
{% spaceless %}
|
||||
<head>
|
||||
<title>{% block title %}{% endblock %} - MITX 6.002</title>
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
{% include "meta/html_head_meta.html" %}
|
||||
<link rel="shortcut icon" href="{{ settings.SITE_FAVICON|media }}" />
|
||||
{% include "meta/html_head_stylesheets.html" %}
|
||||
|
||||
@@ -33,8 +33,14 @@
|
||||
<!-- Quick fix -- we should reference askbot jquery properly -->
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="{{ settings.STATIC_URL }}/js/askbot_jquery.min.js"
|
||||
src="{{'/js/jquery-1.4.3.js'|media}}"
|
||||
></script>
|
||||
{#
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="{{ settings.STATIC_ROOT }}/js/askbot_jquery.min.js"
|
||||
></script>
|
||||
#}
|
||||
<!-- History.js -->
|
||||
<script type='text/javascript' src="{{"/js/jquery.history.js"|media }}"></script>
|
||||
<script type='text/javascript' src="{{"/js/utils.js"|media }}"></script>
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
<header class="global" aria-label="Global Navigation">
|
||||
<nav>
|
||||
<h1 class="logo"><a href="${reverse('root')}"></a></h1>
|
||||
<h1 class="logo"><a href="{% url root %}"></a></h1>
|
||||
<ol class="left">
|
||||
<li class="primary">
|
||||
<a href="${reverse('courses')}">Find Courses</a>
|
||||
<a href="{% url courses %}">Find Courses</a>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<ol class="user">
|
||||
<li class="primary">
|
||||
<a href="${reverse('dashboard')}" class="user-link">
|
||||
<a href="{% url dashboard %}" class="user-link">
|
||||
<span class="avatar"></span>
|
||||
${user.username}
|
||||
{{user.username}}
|
||||
</a>
|
||||
</li>
|
||||
<li class="primary">
|
||||
<a href="#" class="dropdown">▾</a>
|
||||
<ul class="dropdown-menu">
|
||||
## <li><a href="#">Account Settings</a></li>
|
||||
<li><a href="${reverse('help_edx')}">Help</a></li>
|
||||
<li><a href="${reverse('logout')}">Log Out</a></li>
|
||||
{# <li><a href="#">Account Settings</a></li> #}
|
||||
<li><a href="{% url help_edx %}">Help</a></li>
|
||||
<li><a href="{% url logout %}">Log Out</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
@@ -3,19 +3,19 @@
|
||||
<nav>
|
||||
<section class="top">
|
||||
<section class="primary">
|
||||
<a href="${reverse('root')}" class="logo"></a>
|
||||
<a href="${reverse('courses')}">Find Courses</a>
|
||||
<a href="${reverse('about_edx')}">About</a>
|
||||
<a href="{% url root %}" class="logo"></a>
|
||||
<a href="{% url courses %}">Find Courses</a>
|
||||
<a href="{% url about_edx %}">About</a>
|
||||
<a href="http://edxonline.tumblr.com/">Blog</a>
|
||||
<a href="${reverse('jobs')}">Jobs</a>
|
||||
<a href="${reverse('contact')}">Contact</a>
|
||||
<a href="{% url jobs %}">Jobs</a>
|
||||
<a href="{% url contact %}">Contact</a>
|
||||
</section>
|
||||
|
||||
<section class="social">
|
||||
<a href="http://youtube.com/user/edxonline"><img src="${static.url('images/social/youtube-sharing.png')}" /></a>
|
||||
<a href="https://plus.google.com/108235383044095082735"><img src="${static.url('images/social/google-plus-sharing.png')}" /></a>
|
||||
<a href="http://www.facebook.com/EdxOnline"><img src="${static.url('images/social/facebook-sharing.png')}" /></a>
|
||||
<a href="https://twitter.com/edXOnline"><img src="${static.url('images/social/twitter-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="{{('images/social/google-plus-sharing.png'|media)}}" /></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="{{'images/social/twitter-sharing.png'|media}}" /></a>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
@@ -25,10 +25,10 @@
|
||||
</section>
|
||||
|
||||
<section class="secondary">
|
||||
<a href="${reverse('tos')}">Terms of Service</a>
|
||||
<a href="${reverse('privacy_edx')}">Privacy Policy</a>
|
||||
<a href="${reverse('honor')}">Honor Code</a>
|
||||
<a href="${reverse('help_edx')}">Help</a>
|
||||
<a href="{% url tos %}">Terms of Service</a>
|
||||
<a href="{% url privacy_edx %}">Privacy Policy</a>
|
||||
<a href="{% url honor %}">Honor Code</a>
|
||||
<a href="{% url help_edx %}">Help</a>
|
||||
</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.exceptions import ItemNotFoundError
|
||||
from static_replace import replace_urls, try_staticfiles_lookup
|
||||
from courseware.access import has_access
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def check_course(user, course_id, course_must_be_open=True, course_required=True):
|
||||
def get_course_by_id(course_id):
|
||||
"""
|
||||
Given a django user and a course_id, this returns the course
|
||||
object. By default, if the course is not found or the course is
|
||||
not open yet, this method will raise a 404.
|
||||
Given a course id, return the corresponding course descriptor.
|
||||
|
||||
If course_must_be_open is False, the course will be returned
|
||||
without a 404 even if it is not open.
|
||||
|
||||
If course_required is False, a course_id of None is acceptable. The
|
||||
course returned will be None. Even if the course is not required,
|
||||
if a course_id is given that does not exist a 404 will be raised.
|
||||
|
||||
This behavior is modified by MITX_FEATURES['DARK_LAUNCH']:
|
||||
if dark launch is enabled, course_must_be_open is ignored for
|
||||
users that have staff access.
|
||||
If course_id is not valid, raises a 404.
|
||||
"""
|
||||
course = None
|
||||
if course_required or course_id:
|
||||
try:
|
||||
course_loc = CourseDescriptor.id_to_location(course_id)
|
||||
course = modulestore().get_item(course_loc)
|
||||
try:
|
||||
course_loc = CourseDescriptor.id_to_location(course_id)
|
||||
return modulestore().get_item(course_loc)
|
||||
except (KeyError, ItemNotFoundError):
|
||||
raise Http404("Course not found.")
|
||||
|
||||
except (KeyError, ItemNotFoundError):
|
||||
raise Http404("Course not found.")
|
||||
|
||||
started = course.has_started() or settings.MITX_FEATURES['DISABLE_START_DATES']
|
||||
|
||||
must_be_open = course_must_be_open
|
||||
if (settings.MITX_FEATURES['DARK_LAUNCH'] and
|
||||
has_staff_access_to_course(user, course)):
|
||||
must_be_open = False
|
||||
|
||||
if must_be_open and not started:
|
||||
raise Http404("This course has not yet started.")
|
||||
def get_course_with_access(user, course_id, action):
|
||||
"""
|
||||
Given a course_id, look up the corresponding course descriptor,
|
||||
check that the user has the access to perform the specified action
|
||||
on the course, and return the descriptor.
|
||||
|
||||
Raises a 404 if the course_id is invalid, or the user doesn't have access.
|
||||
"""
|
||||
course = get_course_by_id(course_id)
|
||||
if not has_access(user, course, action):
|
||||
# Deliberately return a non-specific error message to avoid
|
||||
# leaking info about access control settings
|
||||
raise Http404("Course not found.")
|
||||
return course
|
||||
|
||||
|
||||
def get_opt_course_with_access(user, course_id, action):
|
||||
"""
|
||||
Same as get_course_with_access, except that if course_id is None,
|
||||
return None without performing any access checks.
|
||||
"""
|
||||
if course_id is None:
|
||||
return None
|
||||
return get_course_with_access(user, course_id, action)
|
||||
|
||||
|
||||
def course_image_url(course):
|
||||
"""Try to look up the image url for the course. If it's not found,
|
||||
log an error and return the dead link"""
|
||||
@@ -140,66 +142,10 @@ def get_course_info_section(course, section_key):
|
||||
|
||||
raise KeyError("Invalid about key " + str(section_key))
|
||||
|
||||
def course_staff_group_name(course):
|
||||
'''
|
||||
course should be either a CourseDescriptor instance, or a string (the
|
||||
.course entry of a Location)
|
||||
'''
|
||||
if isinstance(course, str) or isinstance(course, unicode):
|
||||
coursename = course
|
||||
else:
|
||||
# should be a CourseDescriptor, so grab its location.course:
|
||||
coursename = course.location.course
|
||||
return 'staff_%s' % coursename
|
||||
|
||||
def has_staff_access_to_course(user, course):
|
||||
'''
|
||||
Returns True if the given user has staff access to the course.
|
||||
This means that user is in the staff_* group, or is an overall admin.
|
||||
TODO (vshnayder): this needs to be changed to allow per-course_id permissions, not per-course
|
||||
(e.g. staff in 2012 is different from 2013, but maybe some people always have access)
|
||||
|
||||
course is the course field of the location being accessed.
|
||||
'''
|
||||
if user is None or (not user.is_authenticated()) or course is None:
|
||||
return False
|
||||
if user.is_staff:
|
||||
return True
|
||||
|
||||
# note this is the Auth group, not UserTestGroup
|
||||
user_groups = [x[1] for x in user.groups.values_list()]
|
||||
staff_group = course_staff_group_name(course)
|
||||
if staff_group in user_groups:
|
||||
return True
|
||||
return False
|
||||
|
||||
def has_staff_access_to_course_id(user, course_id):
|
||||
"""Helper method that takes a course_id instead of a course name"""
|
||||
loc = CourseDescriptor.id_to_location(course_id)
|
||||
return has_staff_access_to_course(user, loc.course)
|
||||
|
||||
|
||||
def has_staff_access_to_location(user, location):
|
||||
"""Helper method that checks whether the user has staff access to
|
||||
the course of the location.
|
||||
|
||||
location: something that can be passed to Location
|
||||
"""
|
||||
return has_staff_access_to_course(user, Location(location).course)
|
||||
|
||||
def has_access_to_course(user, course):
|
||||
'''course is the .course element of a location'''
|
||||
if course.metadata.get('ispublic'):
|
||||
return True
|
||||
return has_staff_access_to_course(user,course)
|
||||
|
||||
def get_courses_by_university(user):
|
||||
'''
|
||||
Returns dict of lists of courses available, keyed by course.org (ie university).
|
||||
Courses are sorted by course.number.
|
||||
|
||||
if ACCESS_REQUIRE_STAFF_FOR_COURSE then list only includes those accessible
|
||||
to user.
|
||||
'''
|
||||
# TODO: Clean up how 'error' is done.
|
||||
# filter out any courses that errored.
|
||||
@@ -208,9 +154,7 @@ def get_courses_by_university(user):
|
||||
courses = sorted(courses, key=lambda course: course.number)
|
||||
universities = defaultdict(list)
|
||||
for course in courses:
|
||||
if settings.MITX_FEATURES.get('ACCESS_REQUIRE_STAFF_FOR_COURSE'):
|
||||
if not has_access_to_course(user,course):
|
||||
continue
|
||||
universities[course.org].append(course)
|
||||
if has_access(user, course, 'see_exists'):
|
||||
universities[course.org].append(course)
|
||||
return universities
|
||||
|
||||
|
||||
@@ -63,7 +63,12 @@ def grade(student, request, course, student_module_cache=None):
|
||||
scores = []
|
||||
# TODO: We need the request to pass into here. If we could forgo that, our arguments
|
||||
# would be simpler
|
||||
section_module = get_module(student, request, section_descriptor.location, student_module_cache)
|
||||
section_module = get_module(student, request,
|
||||
section_descriptor.location, student_module_cache)
|
||||
if section_module is None:
|
||||
# student doesn't have access to this module, or something else
|
||||
# went wrong.
|
||||
continue
|
||||
|
||||
# TODO: We may be able to speed this up by only getting a list of children IDs from section_module
|
||||
# Then, we may not need to instatiate any problems if they are already in the database
|
||||
|
||||
@@ -67,7 +67,7 @@ class StudentModuleCache(object):
|
||||
"""
|
||||
A cache of StudentModules for a specific student
|
||||
"""
|
||||
def __init__(self, user, descriptors):
|
||||
def __init__(self, user, descriptors, select_for_update=False):
|
||||
'''
|
||||
Find any StudentModule objects that are needed by any descriptor
|
||||
in descriptors. Avoids making multiple queries to the database.
|
||||
@@ -77,6 +77,7 @@ class StudentModuleCache(object):
|
||||
Arguments
|
||||
user: The user for which to fetch maching StudentModules
|
||||
descriptors: An array of XModuleDescriptors.
|
||||
select_for_update: Flag indicating whether the row should be locked until end of transaction
|
||||
'''
|
||||
if user.is_authenticated():
|
||||
module_ids = self._get_module_state_keys(descriptors)
|
||||
@@ -86,23 +87,30 @@ class StudentModuleCache(object):
|
||||
self.cache = []
|
||||
chunk_size = 500
|
||||
for id_chunk in [module_ids[i:i + chunk_size] for i in xrange(0, len(module_ids), chunk_size)]:
|
||||
self.cache.extend(StudentModule.objects.filter(
|
||||
student=user,
|
||||
module_state_key__in=id_chunk)
|
||||
)
|
||||
if select_for_update:
|
||||
self.cache.extend(StudentModule.objects.select_for_update().filter(
|
||||
student=user,
|
||||
module_state_key__in=id_chunk)
|
||||
)
|
||||
else:
|
||||
self.cache.extend(StudentModule.objects.filter(
|
||||
student=user,
|
||||
module_state_key__in=id_chunk)
|
||||
)
|
||||
|
||||
else:
|
||||
self.cache = []
|
||||
|
||||
|
||||
@classmethod
|
||||
def cache_for_descriptor_descendents(cls, user, descriptor, depth=None, descriptor_filter=lambda descriptor: True):
|
||||
def cache_for_descriptor_descendents(cls, user, descriptor, depth=None, descriptor_filter=lambda descriptor: True, select_for_update=False):
|
||||
"""
|
||||
descriptor: An XModuleDescriptor
|
||||
depth is the number of levels of descendent modules to load StudentModules for, in addition to
|
||||
the supplied descriptor. If depth is None, load all descendent StudentModules
|
||||
descriptor_filter is a function that accepts a descriptor and return wether the StudentModule
|
||||
should be cached
|
||||
select_for_update: Flag indicating whether the row should be locked until end of transaction
|
||||
"""
|
||||
|
||||
def get_child_descriptors(descriptor, depth, descriptor_filter):
|
||||
@@ -122,7 +130,7 @@ class StudentModuleCache(object):
|
||||
|
||||
descriptors = get_child_descriptors(descriptor, depth, descriptor_filter)
|
||||
|
||||
return StudentModuleCache(user, descriptors)
|
||||
return StudentModuleCache(user, descriptors, select_for_update)
|
||||
|
||||
def _get_module_state_keys(self, descriptors):
|
||||
'''
|
||||
|
||||
@@ -2,27 +2,40 @@ import json
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import Http404
|
||||
from django.http import HttpResponse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from capa.xqueue_interface import qinterface
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
from capa.xqueue_interface import XQueueInterface
|
||||
from courseware.access import has_access
|
||||
from mitxmako.shortcuts import render_to_string
|
||||
from models import StudentModule, StudentModuleCache
|
||||
from static_replace import replace_urls
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.x_module import ModuleSystem
|
||||
from xmodule_modifiers import replace_static_urls, add_histogram, wrap_xmodule
|
||||
|
||||
from courseware.courses import (has_staff_access_to_course,
|
||||
has_staff_access_to_location)
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
|
||||
if settings.XQUEUE_INTERFACE['basic_auth'] is not None:
|
||||
requests_auth = HTTPBasicAuth(*settings.XQUEUE_INTERFACE['basic_auth'])
|
||||
else:
|
||||
requests_auth = None
|
||||
|
||||
xqueue_interface = XQueueInterface(
|
||||
settings.XQUEUE_INTERFACE['url'],
|
||||
settings.XQUEUE_INTERFACE['django_auth'],
|
||||
requests_auth,
|
||||
)
|
||||
|
||||
|
||||
def make_track_function(request):
|
||||
'''
|
||||
Make a tracking function that logs what happened.
|
||||
@@ -52,6 +65,9 @@ def toc_for_course(user, request, course, active_chapter, active_section):
|
||||
Everything else comes from the xml, or defaults to "".
|
||||
|
||||
chapters with name 'hidden' are skipped.
|
||||
|
||||
NOTE: assumes that if we got this far, user has access to course. Returns
|
||||
None if this is not the case.
|
||||
'''
|
||||
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(user, course, depth=2)
|
||||
@@ -129,6 +145,10 @@ def get_module(user, request, location, student_module_cache, position=None):
|
||||
'''
|
||||
descriptor = modulestore().get_item(location)
|
||||
|
||||
# Short circuit--if the user shouldn't have access, bail without doing any work
|
||||
if not has_access(user, descriptor, 'load'):
|
||||
return None
|
||||
|
||||
#TODO Only check the cache if this module can possibly have state
|
||||
instance_module = None
|
||||
shared_module = None
|
||||
@@ -142,22 +162,16 @@ def get_module(user, request, location, student_module_cache, position=None):
|
||||
shared_module = student_module_cache.lookup(descriptor.category,
|
||||
shared_state_key)
|
||||
|
||||
|
||||
|
||||
instance_state = instance_module.state if instance_module is not None else None
|
||||
shared_state = shared_module.state if shared_module is not None else None
|
||||
|
||||
# TODO (vshnayder): fix hardcoded urls (use reverse)
|
||||
# Setup system context for module instance
|
||||
|
||||
ajax_url = reverse('modx_dispatch',
|
||||
ajax_url = reverse('modx_dispatch',
|
||||
kwargs=dict(course_id=descriptor.location.course_id,
|
||||
id=descriptor.location.url(),
|
||||
dispatch=''),
|
||||
)
|
||||
|
||||
# ajax_url = settings.MITX_ROOT_URL + '/modx/' + descriptor.location.url() + '/'
|
||||
|
||||
# Fully qualified callback URL for external queueing system
|
||||
xqueue_callback_url = request.build_absolute_uri('/')[:-1] # Trailing slash provided by reverse
|
||||
xqueue_callback_url += reverse('xqueue_callback',
|
||||
@@ -172,11 +186,14 @@ def get_module(user, request, location, student_module_cache, position=None):
|
||||
# TODO: Queuename should be derived from 'course_settings.json' of each course
|
||||
xqueue_default_queuename = descriptor.location.org + '-' + descriptor.location.course
|
||||
|
||||
xqueue = { 'interface': qinterface,
|
||||
'callback_url': xqueue_callback_url,
|
||||
'default_queuename': xqueue_default_queuename.replace(' ','_') }
|
||||
xqueue = {'interface': xqueue_interface,
|
||||
'callback_url': xqueue_callback_url,
|
||||
'default_queuename': xqueue_default_queuename.replace(' ', '_')}
|
||||
|
||||
def _get_module(location):
|
||||
"""
|
||||
Delegate to get_module. It does an access check, so may return None
|
||||
"""
|
||||
return get_module(user, request, location,
|
||||
student_module_cache, position)
|
||||
|
||||
@@ -195,12 +212,11 @@ def get_module(user, request, location, student_module_cache, position=None):
|
||||
# a module is coming through get_html and is therefore covered
|
||||
# by the replace_static_urls code below
|
||||
replace_urls=replace_urls,
|
||||
is_staff=has_staff_access_to_location(user, location),
|
||||
node_path=settings.NODE_PATH
|
||||
)
|
||||
# pass position specified in URL to module through ModuleSystem
|
||||
system.set('position', position)
|
||||
system.set('DEBUG',settings.DEBUG)
|
||||
system.set('DEBUG', settings.DEBUG)
|
||||
|
||||
module = descriptor.xmodule_constructor(system)(instance_state, shared_state)
|
||||
|
||||
@@ -210,7 +226,7 @@ def get_module(user, request, location, student_module_cache, position=None):
|
||||
)
|
||||
|
||||
if settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF'):
|
||||
if has_staff_access_to_course(user, module.location.course):
|
||||
if has_access(user, module, 'staff'):
|
||||
module.get_html = add_histogram(module.get_html, module, user)
|
||||
|
||||
return module
|
||||
@@ -291,12 +307,17 @@ def xqueue_callback(request, course_id, userid, id, dispatch):
|
||||
# Retrieve target StudentModule
|
||||
user = User.objects.get(id=userid)
|
||||
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(user, modulestore().get_item(id))
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
|
||||
user, modulestore().get_item(id), depth=0, select_for_update=True)
|
||||
instance = get_module(user, request, id, student_module_cache)
|
||||
if instance is None:
|
||||
log.debug("No module {} for user {}--access denied?".format(id, user))
|
||||
raise Http404
|
||||
|
||||
instance_module = get_instance_module(user, instance, student_module_cache)
|
||||
|
||||
if instance_module is None:
|
||||
log.debug("Couldn't find module '%s' for user '%s'", id, user)
|
||||
log.debug("Couldn't find instance of module '%s' for user '%s'", id, user)
|
||||
raise Http404
|
||||
|
||||
oldgrade = instance_module.grade
|
||||
@@ -336,14 +357,25 @@ def modx_dispatch(request, dispatch=None, id=None, course_id=None):
|
||||
- id -- the module id. Used to look up the XModule instance
|
||||
'''
|
||||
# ''' (fix emacs broken parsing)
|
||||
# Check for submitted files
|
||||
|
||||
# Check for submitted files and basic file size checks
|
||||
p = request.POST.copy()
|
||||
if request.FILES:
|
||||
for inputfile_id in request.FILES.keys():
|
||||
p[inputfile_id] = request.FILES[inputfile_id]
|
||||
inputfile = request.FILES[inputfile_id]
|
||||
if inputfile.size > settings.STUDENT_FILEUPLOAD_MAX_SIZE: # Bytes
|
||||
file_too_big_msg = 'Submission aborted! Your file "%s" is too large (max size: %d MB)' %\
|
||||
(inputfile.name, settings.STUDENT_FILEUPLOAD_MAX_SIZE/(1000**2))
|
||||
return HttpResponse(json.dumps({'success': file_too_big_msg}))
|
||||
p[inputfile_id] = inputfile
|
||||
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(request.user, modulestore().get_item(id))
|
||||
instance = get_module(request.user, request, id, student_module_cache)
|
||||
if instance is None:
|
||||
# Either permissions just changed, or someone is trying to be clever
|
||||
# and load something they shouldn't have access to.
|
||||
log.debug("No module {} for user {}--access denied?".format(id, user))
|
||||
raise Http404
|
||||
|
||||
instance_module = get_instance_module(request.user, instance, student_module_cache)
|
||||
shared_module = get_shared_instance_module(request.user, instance, student_module_cache)
|
||||
|
||||
@@ -18,12 +18,14 @@ from override_settings import override_settings
|
||||
|
||||
import xmodule.modulestore.django
|
||||
|
||||
# Need access to internal func to put users in the right group
|
||||
from courseware.access import _course_staff_group_name
|
||||
|
||||
from student.models import Registration
|
||||
from courseware.courses import course_staff_group_name
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
|
||||
from xmodule.timeparse import stringify_time
|
||||
|
||||
def parse_json(response):
|
||||
"""Parse response, which is assumed to be json"""
|
||||
@@ -310,7 +312,7 @@ class TestViewAuth(PageLoader):
|
||||
self.check_for_get_code(404, url)
|
||||
|
||||
# Make the instructor staff in the toy course
|
||||
group_name = course_staff_group_name(self.toy)
|
||||
group_name = _course_staff_group_name(self.toy.location)
|
||||
g = Group.objects.create(name=group_name)
|
||||
g.user_set.add(user(self.instructor))
|
||||
|
||||
@@ -340,25 +342,22 @@ class TestViewAuth(PageLoader):
|
||||
|
||||
def run_wrapped(self, test):
|
||||
"""
|
||||
test.py turns off start dates. Enable them and DARK_LAUNCH.
|
||||
test.py turns off start dates. Enable them.
|
||||
Because settings is global, be careful not to mess it up for other tests
|
||||
(Can't use override_settings because we're only changing part of the
|
||||
MITX_FEATURES dict)
|
||||
"""
|
||||
oldDSD = settings.MITX_FEATURES['DISABLE_START_DATES']
|
||||
oldDL = settings.MITX_FEATURES['DARK_LAUNCH']
|
||||
|
||||
try:
|
||||
settings.MITX_FEATURES['DISABLE_START_DATES'] = False
|
||||
settings.MITX_FEATURES['DARK_LAUNCH'] = True
|
||||
test()
|
||||
finally:
|
||||
settings.MITX_FEATURES['DISABLE_START_DATES'] = oldDSD
|
||||
settings.MITX_FEATURES['DARK_LAUNCH'] = oldDL
|
||||
|
||||
|
||||
def test_dark_launch(self):
|
||||
"""Make sure that when dark launch is on, students can't access course
|
||||
"""Make sure that before course start, students can't access course
|
||||
pages, but instructors can"""
|
||||
self.run_wrapped(self._do_test_dark_launch)
|
||||
|
||||
@@ -372,13 +371,12 @@ class TestViewAuth(PageLoader):
|
||||
|
||||
# Make courses start in the future
|
||||
tomorrow = time.time() + 24*3600
|
||||
self.toy.start = self.toy.metadata['start'] = time.gmtime(tomorrow)
|
||||
self.full.start = self.full.metadata['start'] = time.gmtime(tomorrow)
|
||||
self.toy.metadata['start'] = stringify_time(time.gmtime(tomorrow))
|
||||
self.full.metadata['start'] = stringify_time(time.gmtime(tomorrow))
|
||||
|
||||
self.assertFalse(self.toy.has_started())
|
||||
self.assertFalse(self.full.has_started())
|
||||
self.assertFalse(settings.MITX_FEATURES['DISABLE_START_DATES'])
|
||||
self.assertTrue(settings.MITX_FEATURES['DARK_LAUNCH'])
|
||||
|
||||
def reverse_urls(names, course):
|
||||
"""Reverse a list of course urls"""
|
||||
@@ -444,7 +442,7 @@ class TestViewAuth(PageLoader):
|
||||
|
||||
print '=== Testing course instructor access....'
|
||||
# Make the instructor staff in the toy course
|
||||
group_name = course_staff_group_name(self.toy)
|
||||
group_name = _course_staff_group_name(self.toy.location)
|
||||
g = Group.objects.create(name=group_name)
|
||||
g.user_set.add(user(self.instructor))
|
||||
|
||||
@@ -494,7 +492,7 @@ class TestViewAuth(PageLoader):
|
||||
|
||||
print '=== Testing course instructor access....'
|
||||
# Make the instructor staff in the toy course
|
||||
group_name = course_staff_group_name(self.toy)
|
||||
group_name = _course_staff_group_name(self.toy.location)
|
||||
g = Group.objects.create(name=group_name)
|
||||
g.user_set.add(user(self.instructor))
|
||||
|
||||
|
||||
@@ -15,21 +15,19 @@ from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
from django.views.decorators.cache import cache_control
|
||||
|
||||
from module_render import toc_for_course, get_module, get_section
|
||||
from models import StudentModuleCache
|
||||
from student.models import UserProfile
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.search import path_to_location
|
||||
from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
|
||||
from util.cache import cache, cache_if_anonymous
|
||||
from student.models import UserTestGroup, CourseEnrollment
|
||||
from courseware import grades
|
||||
from courseware.courses import (check_course, get_courses_by_university,
|
||||
has_staff_access_to_course_id)
|
||||
|
||||
from courseware.access import has_access
|
||||
from courseware.courses import (get_course_with_access, get_courses_by_university)
|
||||
from models import StudentModuleCache
|
||||
from module_render import toc_for_course, get_module, get_section
|
||||
from student.models import UserProfile
|
||||
from student.models import UserTestGroup, CourseEnrollment
|
||||
from util.cache import cache, cache_if_anonymous
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem
|
||||
from xmodule.modulestore.search import path_to_location
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
@@ -93,7 +91,8 @@ def render_accordion(request, course, chapter, section):
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def index(request, course_id, chapter=None, section=None,
|
||||
position=None):
|
||||
''' Displays courseware accordion, and any associated content.
|
||||
"""
|
||||
Displays courseware accordion, and any associated content.
|
||||
If course, chapter, and section aren't all specified, just returns
|
||||
the accordion. If they are specified, returns an error if they don't
|
||||
point to a valid module.
|
||||
@@ -109,8 +108,8 @@ def index(request, course_id, chapter=None, section=None,
|
||||
Returns:
|
||||
|
||||
- HTTPresponse
|
||||
'''
|
||||
course = check_course(request.user, course_id)
|
||||
"""
|
||||
course = get_course_with_access(request.user, course_id, 'load')
|
||||
registered = registered_for_course(course, request.user)
|
||||
if not registered:
|
||||
# TODO (vshnayder): do course instructors need to be registered to see course?
|
||||
@@ -137,6 +136,10 @@ def index(request, course_id, chapter=None, section=None,
|
||||
module = get_module(request.user, request,
|
||||
section_descriptor.location,
|
||||
student_module_cache)
|
||||
if module is None:
|
||||
# User is probably being clever and trying to access something
|
||||
# they don't have access to.
|
||||
raise Http404
|
||||
context['content'] = module.get_html()
|
||||
else:
|
||||
log.warning("Couldn't find a section descriptor for course_id '{0}',"
|
||||
@@ -204,7 +207,7 @@ def course_info(request, course_id):
|
||||
|
||||
Assumes the course_id is in a valid format.
|
||||
"""
|
||||
course = check_course(request.user, course_id)
|
||||
course = get_course_with_access(request.user, course_id, 'load')
|
||||
|
||||
return render_to_response('info.html', {'course': course})
|
||||
|
||||
@@ -221,7 +224,7 @@ def registered_for_course(course, user):
|
||||
@ensure_csrf_cookie
|
||||
@cache_if_anonymous
|
||||
def course_about(request, course_id):
|
||||
course = check_course(request.user, course_id, course_must_be_open=False)
|
||||
course = get_course_with_access(request.user, course_id, 'see_exists')
|
||||
registered = registered_for_course(course, request.user)
|
||||
return render_to_response('portal/course_about.html', {'course': course, 'registered': registered})
|
||||
|
||||
@@ -253,14 +256,14 @@ def profile(request, course_id, student_id=None):
|
||||
|
||||
Course staff are allowed to see the profiles of students in their class.
|
||||
"""
|
||||
course = check_course(request.user, course_id)
|
||||
course = get_course_with_access(request.user, course_id, 'load')
|
||||
|
||||
if student_id is None or student_id == request.user.id:
|
||||
# always allowed to see your own profile
|
||||
student = request.user
|
||||
else:
|
||||
# Requesting access to a different student's profile
|
||||
if not has_staff_access_to_course_id(request.user, course_id):
|
||||
if not has_access(request.user, course, 'staff'):
|
||||
raise Http404
|
||||
student = User.objects.get(id=int(student_id))
|
||||
|
||||
@@ -297,10 +300,7 @@ def gradebook(request, course_id):
|
||||
- only displayed to course staff
|
||||
- shows students who are enrolled.
|
||||
"""
|
||||
if not has_staff_access_to_course_id(request.user, course_id):
|
||||
raise Http404
|
||||
|
||||
course = check_course(request.user, course_id)
|
||||
course = get_course_with_access(request.user, course_id, 'staff')
|
||||
|
||||
enrolled_students = User.objects.filter(courseenrollment__course_id=course_id).order_by('username')
|
||||
|
||||
@@ -322,10 +322,7 @@ def gradebook(request, course_id):
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def grade_summary(request, course_id):
|
||||
"""Display the grade summary for a course."""
|
||||
if not has_staff_access_to_course_id(request.user, course_id):
|
||||
raise Http404
|
||||
|
||||
course = check_course(request.user, course_id)
|
||||
course = get_course_with_access(request.user, course_id, 'staff')
|
||||
|
||||
# For now, just a static page
|
||||
context = {'course': course }
|
||||
@@ -335,10 +332,7 @@ def grade_summary(request, course_id):
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def instructor_dashboard(request, course_id):
|
||||
"""Display the instructor dashboard for a course."""
|
||||
if not has_staff_access_to_course_id(request.user, course_id):
|
||||
raise Http404
|
||||
|
||||
course = check_course(request.user, course_id)
|
||||
course = get_course_with_access(request.user, course_id, 'staff')
|
||||
|
||||
# For now, just a static page
|
||||
context = {'course': course }
|
||||
|
||||
@@ -9,7 +9,7 @@ from django.utils import simplejson
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from mitxmako.shortcuts import render_to_response
|
||||
|
||||
from courseware.courses import check_course
|
||||
from courseware.courses import get_opt_course_with_access
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
@@ -51,7 +51,7 @@ def update_template_dictionary(dictionary, request=None, course=None, article=No
|
||||
|
||||
|
||||
def view(request, article_path, course_id=None):
|
||||
course = check_course(request.user, course_id, course_required=False)
|
||||
course = get_opt_course_with_access(request.user, course_id, 'load')
|
||||
|
||||
(article, err) = get_article(request, article_path, course)
|
||||
if err:
|
||||
@@ -67,7 +67,7 @@ def view(request, article_path, course_id=None):
|
||||
|
||||
|
||||
def view_revision(request, revision_number, article_path, course_id=None):
|
||||
course = check_course(request.user, course_id, course_required=False)
|
||||
course = get_opt_course_with_access(request.user, course_id, 'load')
|
||||
|
||||
(article, err) = get_article(request, article_path, course)
|
||||
if err:
|
||||
@@ -91,7 +91,7 @@ def view_revision(request, revision_number, article_path, course_id=None):
|
||||
|
||||
|
||||
def root_redirect(request, course_id=None):
|
||||
course = check_course(request.user, course_id, course_required=False)
|
||||
course = get_opt_course_with_access(request.user, course_id, 'load')
|
||||
|
||||
#TODO: Add a default namespace to settings.
|
||||
namespace = course.wiki_namespace if course else "edX"
|
||||
@@ -109,7 +109,7 @@ def root_redirect(request, course_id=None):
|
||||
|
||||
|
||||
def create(request, article_path, course_id=None):
|
||||
course = check_course(request.user, course_id, course_required=False)
|
||||
course = get_opt_course_with_access(request.user, course_id, 'load')
|
||||
|
||||
article_path_components = article_path.split('/')
|
||||
|
||||
@@ -170,7 +170,7 @@ def create(request, article_path, course_id=None):
|
||||
|
||||
|
||||
def edit(request, article_path, course_id=None):
|
||||
course = check_course(request.user, course_id, course_required=False)
|
||||
course = get_opt_course_with_access(request.user, course_id, 'load')
|
||||
|
||||
(article, err) = get_article(request, article_path, course)
|
||||
if err:
|
||||
@@ -218,7 +218,7 @@ def edit(request, article_path, course_id=None):
|
||||
|
||||
|
||||
def history(request, article_path, page=1, course_id=None):
|
||||
course = check_course(request.user, course_id, course_required=False)
|
||||
course = get_opt_course_with_access(request.user, course_id, 'load')
|
||||
|
||||
(article, err) = get_article(request, article_path, course)
|
||||
if err:
|
||||
@@ -300,7 +300,7 @@ def history(request, article_path, page=1, course_id=None):
|
||||
|
||||
|
||||
def revision_feed(request, page=1, namespace=None, course_id=None):
|
||||
course = check_course(request.user, course_id, course_required=False)
|
||||
course = get_opt_course_with_access(request.user, course_id, 'load')
|
||||
|
||||
page_size = 10
|
||||
|
||||
@@ -333,7 +333,7 @@ def revision_feed(request, page=1, namespace=None, course_id=None):
|
||||
|
||||
|
||||
def search_articles(request, namespace=None, course_id=None):
|
||||
course = check_course(request.user, course_id, course_required=False)
|
||||
course = get_opt_course_with_access(request.user, course_id, 'load')
|
||||
|
||||
# blampe: We should check for the presence of other popular django search
|
||||
# apps and use those if possible. Only fall back on this as a last resort.
|
||||
@@ -382,7 +382,7 @@ def search_articles(request, namespace=None, course_id=None):
|
||||
|
||||
|
||||
def search_add_related(request, course_id, slug, namespace):
|
||||
course = check_course(request.user, course_id, course_required=False)
|
||||
course = get_opt_course_with_access(request.user, course_id, 'load')
|
||||
|
||||
(article, err) = get_article(request, slug, namespace if namespace else course_id)
|
||||
if err:
|
||||
@@ -415,7 +415,7 @@ def search_add_related(request, course_id, slug, namespace):
|
||||
|
||||
|
||||
def add_related(request, course_id, slug, namespace):
|
||||
course = check_course(request.user, course_id, course_required=False)
|
||||
course = get_opt_course_with_access(request.user, course_id, 'load')
|
||||
|
||||
(article, err) = get_article(request, slug, namespace if namespace else course_id)
|
||||
if err:
|
||||
@@ -439,7 +439,7 @@ def add_related(request, course_id, slug, namespace):
|
||||
|
||||
|
||||
def remove_related(request, course_id, namespace, slug, related_id):
|
||||
course = check_course(request.user, course_id, course_required=False)
|
||||
course = get_opt_course_with_access(request.user, course_id, 'load')
|
||||
|
||||
(article, err) = get_article(request, slug, namespace if namespace else course_id)
|
||||
|
||||
@@ -462,7 +462,7 @@ def remove_related(request, course_id, namespace, slug, related_id):
|
||||
|
||||
|
||||
def random_article(request, course_id=None):
|
||||
course = check_course(request.user, course_id, course_required=False)
|
||||
course = get_opt_course_with_access(request.user, course_id, 'load')
|
||||
|
||||
from random import randint
|
||||
num_arts = Article.objects.count()
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from mitxmako.shortcuts import render_to_response
|
||||
|
||||
from courseware.courses import check_course
|
||||
from courseware.courses import get_course_with_access
|
||||
from lxml import etree
|
||||
|
||||
@login_required
|
||||
def index(request, course_id, page=0):
|
||||
course = check_course(request.user, course_id)
|
||||
course = get_course_with_access(request.user, course_id, 'load')
|
||||
raw_table_of_contents = open('lms/templates/book_toc.xml', 'r') # TODO: This will need to come from S3
|
||||
table_of_contents = etree.parse(raw_table_of_contents).getroot()
|
||||
return render_to_response('staticbook.html', {'page': int(page), 'course': course, 'table_of_contents': table_of_contents})
|
||||
return render_to_response('staticbook.html',
|
||||
{'page': int(page), 'course': course,
|
||||
'table_of_contents': table_of_contents})
|
||||
|
||||
|
||||
def index_shifted(request, course_id, page):
|
||||
|
||||
@@ -54,3 +54,5 @@ AWS_ACCESS_KEY_ID = AUTH_TOKENS["AWS_ACCESS_KEY_ID"]
|
||||
AWS_SECRET_ACCESS_KEY = AUTH_TOKENS["AWS_SECRET_ACCESS_KEY"]
|
||||
|
||||
DATABASES = AUTH_TOKENS['DATABASES']
|
||||
|
||||
XQUEUE_INTERFACE = AUTH_TOKENS['XQUEUE_INTERFACE']
|
||||
|
||||
@@ -48,7 +48,6 @@ MITX_FEATURES = {
|
||||
## DO NOT SET TO True IN THIS FILE
|
||||
## 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
|
||||
'DARK_LAUNCH': False, # When True, courses will be active for staff only
|
||||
|
||||
'ENABLE_TEXTBOOK' : True,
|
||||
'ENABLE_DISCUSSION' : True,
|
||||
@@ -95,12 +94,12 @@ system_node_path = os.environ.get("NODE_PATH", None)
|
||||
if system_node_path is None:
|
||||
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",
|
||||
system_node_path
|
||||
]
|
||||
NODE_PATH = ':'.join(node_paths)
|
||||
|
||||
|
||||
################################## MITXWEB #####################################
|
||||
# This is where we stick our compiled template files. Most of the app uses Mako
|
||||
# templates
|
||||
@@ -135,6 +134,7 @@ TEMPLATE_CONTEXT_PROCESSORS = (
|
||||
'course_wiki.course_nav.context_processor',
|
||||
)
|
||||
|
||||
STUDENT_FILEUPLOAD_MAX_SIZE = 4*1000*1000 # 4 MB
|
||||
|
||||
# FIXME:
|
||||
# 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 = [
|
||||
COMMON_ROOT / "static",
|
||||
PROJECT_ROOT / "static",
|
||||
ASKBOT_ROOT / "askbot" / "skins",
|
||||
|
||||
PROJECT_ROOT / "askbot" / "skins",
|
||||
]
|
||||
if os.path.isdir(DATA_DIR):
|
||||
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
|
||||
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 *
|
||||
|
||||
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 *
|
||||
|
||||
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"
|
||||
|
||||
|
||||
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
|
||||
# into common.py so that we don't have to repeat this sort of thing
|
||||
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';
|
||||
|
||||
// Course base / layout styles
|
||||
@import 'course/layout/courseware_subnav';
|
||||
@import 'course/layout/courseware_header';
|
||||
@import 'course/base/base';
|
||||
@import 'course/base/extends';
|
||||
@import 'module/module-styles.scss';
|
||||
|
||||
@@ -1,11 +1,203 @@
|
||||
$cell-border-color: #e1e1e1;
|
||||
$table-border-color: #c8c8c8;
|
||||
|
||||
div.gradebook-wrapper {
|
||||
@extend .table-wrapper;
|
||||
|
||||
section.gradebook-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 {
|
||||
@extend .top-header;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -173,29 +173,3 @@ h1.top-header {
|
||||
@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 ""
|
||||
%>
|
||||
<%! 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">
|
||||
<div class="inner-wrapper">
|
||||
@@ -33,7 +33,7 @@ def url_class(url):
|
||||
% if user.is_authenticated():
|
||||
<li class="profile"><a href="${reverse('profile', args=[course.id])}" class="${url_class('profile')}">Profile</a></li>
|
||||
% 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>
|
||||
% 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.access import has_access
|
||||
%>
|
||||
<%inherit file="main.html" />
|
||||
|
||||
@@ -17,7 +18,7 @@
|
||||
$("#unenroll_course_number").text( $(event.target).data("course-number") );
|
||||
|
||||
});
|
||||
|
||||
|
||||
$(document).delegate('#unenroll_form', 'ajax:success', function(data, json, xhr) {
|
||||
if(json.success) {
|
||||
location.href="${reverse('dashboard')}";
|
||||
@@ -33,7 +34,7 @@
|
||||
</%block>
|
||||
|
||||
<section class="container dashboard">
|
||||
|
||||
|
||||
%if message:
|
||||
<section class="dashboard-banner">
|
||||
${message}
|
||||
@@ -66,7 +67,7 @@
|
||||
|
||||
<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])
|
||||
else:
|
||||
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.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/jquery.gradebook.js')}"></script>
|
||||
</%block>
|
||||
|
||||
<%block name="headextra">
|
||||
@@ -19,6 +20,12 @@
|
||||
.grade_None {color:LightGray;}
|
||||
</style>
|
||||
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
var gradebook = new Gradebook($('.gradebook-content'));
|
||||
});
|
||||
</script>
|
||||
|
||||
</%block>
|
||||
|
||||
<%include file="course_navigation.html" args="active_page=''" />
|
||||
@@ -28,50 +35,75 @@
|
||||
<section class="gradebook-content">
|
||||
<h1>Gradebook</h1>
|
||||
|
||||
%if len(students) > 0:
|
||||
<table>
|
||||
<%
|
||||
templateSummary = students[0]['grade_summary']
|
||||
%>
|
||||
|
||||
|
||||
<tr> <!-- Header Row -->
|
||||
<th>Student</th>
|
||||
%for section in templateSummary['section_breakdown']:
|
||||
<th>${section['label']}</th>
|
||||
<table class="student-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<form class="student-search">
|
||||
<input type="search" class="student-search-field" placeholder="Search students" />
|
||||
</form>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<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
|
||||
<th>Total</th>
|
||||
</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
|
||||
</tbody>
|
||||
</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
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
@@ -2,12 +2,10 @@
|
||||
<h1>There has been an error on the <em>MITx</em> servers</h1>
|
||||
<p>We're sorry, this module is temporarily unavailable. Our staff is working to fix it as soon as possible. Please email us at <a href="mailto:technical@mitx.mit.edu">technical@mitx.mit.edu</a> to report any problems or downtime.</p>
|
||||
|
||||
% if is_staff:
|
||||
<h1>Staff-only details below:</h1>
|
||||
<h1>Details below:</h1>
|
||||
|
||||
<p>Error: ${error | h}</p>
|
||||
|
||||
<p>Raw data: ${data | h}</p>
|
||||
% endif
|
||||
|
||||
</section>
|
||||
|
||||
@@ -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.access import has_access
|
||||
%>
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
|
||||
@@ -15,7 +16,7 @@
|
||||
$(".register").click(function() {
|
||||
$("#class_enroll_form").submit();
|
||||
});
|
||||
|
||||
|
||||
$(document).delegate('#class_enroll_form', 'ajax:success', function(data, json, xhr) {
|
||||
if(json.success) {
|
||||
location.href="${reverse('dashboard')}";
|
||||
@@ -64,7 +65,7 @@
|
||||
%if registered:
|
||||
<%
|
||||
## 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])
|
||||
else:
|
||||
course_target = reverse('about_course', args=[course.id])
|
||||
|
||||