Commit da9a4710 by kimth

Merge branch 'master' into kimth/xqueue_callback_blocking

parents 9805b416 be1b0638
......@@ -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,
)
......
import functools
import json
import logging
import random
......@@ -156,7 +157,7 @@ def edXauth_signup(request, eamap=None):
log.debug('ExtAuth: doing signup for %s' % eamap.external_email)
return student_views.main_index(extra_context=context)
return student_views.main_index(request, extra_context=context)
#-----------------------------------------------------------------------------
# MIT SSL
......@@ -206,7 +207,7 @@ def edXauth_ssl_login(request):
pass
if not cert:
# no certificate information - go onward to main index
return student_views.main_index()
return student_views.main_index(request)
(user, email, fullname) = ssl_dn_extract_info(cert)
......@@ -216,4 +217,4 @@ def edXauth_ssl_login(request):
credentials=cert,
email=email,
fullname=fullname,
retfun = student_views.main_index)
retfun = functools.partial(student_views.main_index, request))
......@@ -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')
......@@ -68,9 +68,9 @@ def index(request):
from external_auth.views import edXauth_ssl_login
return edXauth_ssl_login(request)
return main_index(user=request.user)
return main_index(request, user=request.user)
def main_index(extra_context = {}, user=None):
def main_index(request, extra_context={}, user=None):
'''
Render the edX main page.
......@@ -93,7 +93,8 @@ def main_index(extra_context = {}, user=None):
entry.summary = soup.getText()
# The course selection work is done in courseware.courses.
universities = get_courses_by_university(None)
universities = get_courses_by_university(None,
domain=request.META.get('HTTP_HOST'))
context = {'universities': universities, 'entries': entries}
context.update(extra_context)
return render_to_response('index.html', context)
......@@ -166,22 +167,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 +194,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)}
......
......@@ -34,6 +34,17 @@ def wrap_xmodule(get_html, module, template):
return _get_html
def replace_course_urls(get_html, course_id, module):
"""
Updates the supplied module with a new get_html function that wraps
the old get_html function and substitutes urls of the form /course/...
with urls that are /courses/<course_id>/...
"""
@wraps(get_html)
def _get_html():
return replace_urls(get_html(), staticfiles_prefix='/courses/'+course_id, replace_prefix='/course/')
return _get_html
def replace_static_urls(get_html, prefix, module):
"""
Updates the supplied module with a new get_html function that wraps
......
......@@ -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]]
child_locations = self.definition['data']['group_content'][self.group]
children = [self.system.get_module(loc) for loc in child_locations]
return [c for c in children if c is not None]
# TODO (cpennington): Use Groups should be a first class object, rather than being
......
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):
"""
......
import json
import logging
import os
import re
......@@ -149,7 +150,7 @@ class XMLModuleStore(ModuleStoreBase):
for course_dir in course_dirs:
self.try_load_course(course_dir)
def try_load_course(self,course_dir):
def try_load_course(self, course_dir):
'''
Load a course, keeping track of errors as we go along.
'''
......@@ -170,7 +171,27 @@ class XMLModuleStore(ModuleStoreBase):
'''
String representation - for debugging
'''
return '<XMLModuleStore>data_dir=%s, %d courses, %d modules' % (self.data_dir,len(self.courses),len(self.modules))
return '<XMLModuleStore>data_dir=%s, %d courses, %d modules' % (
self.data_dir, len(self.courses), len(self.modules))
def load_policy(self, policy_path, tracker):
"""
Attempt to read a course policy from policy_path. If the file
exists, but is invalid, log an error and return {}.
If the policy loads correctly, returns the deserialized version.
"""
if not os.path.exists(policy_path):
return {}
try:
with open(policy_path) as f:
return json.load(f)
except (IOError, ValueError) as err:
msg = "Error loading course policy from {}".format(policy_path)
tracker(msg)
log.warning(msg + " " + str(err))
return {}
def load_course(self, course_dir, tracker):
"""
......@@ -214,6 +235,11 @@ class XMLModuleStore(ModuleStoreBase):
system = ImportSystem(self, org, course, course_dir, tracker)
course_descriptor = system.process_xml(etree.tostring(course_data))
policy_path = self.data_dir / course_dir / 'policy.json'
policy = self.load_policy(policy_path, tracker)
XModuleDescriptor.apply_policy(course_descriptor, policy)
# NOTE: The descriptors end up loading somewhat bottom up, which
# breaks metadata inheritance via get_children(). Instead
# (actually, in addition to, for now), we do a final inheritance pass
......
......@@ -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):
......
"""
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', [])]
child_locations = self.definition.get('children', [])
children = [self.system.get_module(loc) for loc in child_locations]
# get_module returns None if the current user doesn't have access
# to the location.
self._loaded_children = [c for c in children if c is not None]
return self._loaded_children
......@@ -295,6 +298,14 @@ class XModule(HTMLSnippet):
return ""
def policy_key(location):
"""
Get the key for a location in a policy file. (Since the policy file is
specific to a course, it doesn't need the full location url).
"""
return '{cat}/{name}'.format(cat=location.category, name=location.name)
class XModuleDescriptor(Plugin, HTMLSnippet):
"""
An XModuleDescriptor is a specification for an element of a course. This
......@@ -397,6 +408,15 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
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):
"""
Return the metadata that is not inherited, but was defined on this module.
......@@ -404,6 +424,24 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
return dict((k,v) for k,v in self.metadata.items()
if k not in self._inherited_metadata)
@staticmethod
def apply_policy(node, policy):
"""
Given a descriptor, traverse all its descendants and update its metadata
with the policy.
Notes:
- this does not propagate inherited metadata. The caller should
call compute_inherited_metadata after applying the policy.
- metadata specified in the policy overrides metadata in the xml
"""
k = policy_key(node.location)
if k in policy:
node.metadata.update(policy[k])
for c in node.get_children():
XModuleDescriptor.apply_policy(c, policy)
@staticmethod
def compute_inherited_metadata(node):
"""Given a descriptor, traverse all of its descendants and do metadata
......@@ -596,6 +634,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 +731,6 @@ class ModuleSystem(object):
filestore=None,
debug=False,
xqueue=None,
is_staff=False,
node_path=""):
'''
Create a closure around the system environment.
......@@ -688,7 +743,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 +761,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 +771,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):
......
......@@ -166,7 +166,7 @@ class XmlDescriptor(XModuleDescriptor):
Subclasses should not need to override this except in special
cases (e.g. html module)'''
# VS[compat] -- the filename tag should go away once everything is
# VS[compat] -- the filename attr should go away once everything is
# converted. (note: make sure html files still work once this goes away)
filename = xml_object.get('filename')
if filename is None:
......
......@@ -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
......@@ -2,8 +2,8 @@ from collections import defaultdict
from fs.errors import ResourceNotFoundError
from functools import wraps
import logging
from path import path
from path import path
from django.conf import settings
from django.http import Http404
......@@ -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.
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.
Given a course id, return the corresponding course descriptor.
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)
except (KeyError, ItemNotFoundError):
raise Http404("Course not found.")
try:
course_loc = CourseDescriptor.id_to_location(course_id)
return modulestore().get_item(course_loc)
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,77 +142,32 @@ 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):
def get_courses_by_university(user, domain=None):
'''
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.
courses = [c for c in modulestore().get_courses()
if isinstance(c, CourseDescriptor)]
courses = sorted(courses, key=lambda course: course.number)
if domain and settings.MITX_FEATURES.get('SUBDOMAIN_COURSE_LISTINGS'):
subdomain = domain.split(".")[0]
if subdomain not in settings.COURSE_LISTINGS:
subdomain = 'default'
visible_courses = frozenset(settings.COURSE_LISTINGS[subdomain])
else:
visible_courses = frozenset(c.id for c in courses)
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
if not has_access(user, course, 'see_exists'):
continue
if course.id not in visible_courses:
continue
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
......
"""
A script to walk a course xml tree, generate a dictionary of all the metadata,
and print it out as a json dict.
"""
import os
import sys
import json
from collections import OrderedDict
from path import path
from django.core.management.base import BaseCommand
from xmodule.modulestore.xml import XMLModuleStore
from xmodule.x_module import policy_key
def import_course(course_dir, verbose=True):
course_dir = path(course_dir)
data_dir = course_dir.dirname()
course_dirs = [course_dir.basename()]
# No default class--want to complain if it doesn't find plugins for any
# module.
modulestore = XMLModuleStore(data_dir,
default_class=None,
eager=True,
course_dirs=course_dirs)
def str_of_err(tpl):
(msg, exc_str) = tpl
return '{msg}\n{exc}'.format(msg=msg, exc=exc_str)
courses = modulestore.get_courses()
n = len(courses)
if n != 1:
sys.stderr.write('ERROR: Expect exactly 1 course. Loaded {n}: {lst}\n'.format(
n=n, lst=courses))
return None
course = courses[0]
errors = modulestore.get_item_errors(course.location)
if len(errors) != 0:
sys.stderr.write('ERRORs during import: {}\n'.format('\n'.join(map(str_of_err, errors))))
return course
def node_metadata(node):
# make a copy
to_export = ('format', 'display_name',
'graceperiod', 'showanswer', 'rerandomize',
'start', 'due', 'graded', 'hide_from_toc',
'ispublic', 'xqa_key')
orig = node.own_metadata
d = {k: orig[k] for k in to_export if k in orig}
return d
def get_metadata(course):
d = OrderedDict({})
queue = [course]
while len(queue) > 0:
node = queue.pop()
d[policy_key(node.location)] = node_metadata(node)
# want to print first children first, so put them at the end
# (we're popping from the end)
queue.extend(reversed(node.get_children()))
return d
def print_metadata(course_dir, output):
course = import_course(course_dir)
if course:
meta = get_metadata(course)
result = json.dumps(meta, indent=4)
if output:
with file(output, 'w') as f:
f.write(result)
else:
print result
class Command(BaseCommand):
help = """Imports specified course.xml and prints its
metadata as a json dict.
Usage: metadata_to_json PATH-TO-COURSE-DIR OUTPUT-PATH
if OUTPUT-PATH isn't given, print to stdout.
"""
def handle(self, *args, **options):
n = len(args)
if n < 1 or n > 2:
print Command.help
return
output_path = args[1] if n > 1 else None
print_metadata(args[0], output_path)
......@@ -2,24 +2,24 @@ 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 requests.auth import HTTPBasicAuth
from capa.xqueue_interface import XQueueInterface
from django.contrib.auth.models import User
from xmodule.modulestore.django import modulestore
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)
from requests.auth import HTTPBasicAuth
from xmodule_modifiers import replace_course_urls, replace_static_urls, add_histogram, wrap_xmodule
log = logging.getLogger("mitx.courseware")
......@@ -48,7 +48,7 @@ def make_track_function(request):
return f
def toc_for_course(user, request, course, active_chapter, active_section):
def toc_for_course(user, request, course, active_chapter, active_section, course_id=None):
'''
Create a table of contents from the module store
......@@ -65,10 +65,13 @@ 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)
course = get_module(user, request, course.location, student_module_cache)
course = get_module(user, request, course.location, student_module_cache, course_id=course_id)
chapters = list()
for chapter in course.get_display_items():
......@@ -124,7 +127,7 @@ def get_section(course_module, chapter, section):
return section_module
def get_module(user, request, location, student_module_cache, position=None):
def get_module(user, request, location, student_module_cache, position=None, course_id=None):
''' Get an instance of the xmodule class identified by location,
setting the state based on an existing StudentModule, or creating one if none
exists.
......@@ -141,6 +144,18 @@ def get_module(user, request, location, student_module_cache, position=None):
'''
descriptor = modulestore().get_item(location)
# NOTE:
# A 'course_id' is understood to be the triplet (org, course, run), for example
# (MITx, 6.002x, 2012_Spring).
# At the moment generic XModule does not contain enough information to replicate
# the triplet (it is missing 'run'), so we must pass down course_id
if course_id is None:
course_id = descriptor.location.course_id # Will NOT produce (org, course, run) for non-CourseModule's
# 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
......@@ -155,15 +170,12 @@ 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
# Setup system context for module instance
ajax_url = reverse('modx_dispatch',
kwargs=dict(course_id=descriptor.location.course_id,
ajax_url = reverse('modx_dispatch',
kwargs=dict(course_id=course_id,
id=descriptor.location.url(),
dispatch=''),
)
......@@ -171,7 +183,7 @@ def get_module(user, request, location, student_module_cache, position=None):
# 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',
kwargs=dict(course_id=descriptor.location.course_id,
kwargs=dict(course_id=course_id,
userid=str(user.id),
id=descriptor.location.url(),
dispatch='score_update'),
......@@ -187,8 +199,11 @@ def get_module(user, request, location, student_module_cache, position=None):
'default_queuename': xqueue_default_queuename.replace(' ', '_')}
def _get_module(location):
"""
Delegate to get_module. It does an access check, so may return None
"""
return get_module(user, request, location,
student_module_cache, position)
student_module_cache, position, course_id=course_id)
# TODO (cpennington): When modules are shared between courses, the static
# prefix is going to have to be specific to the module, not the directory
......@@ -205,12 +220,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)
......@@ -219,8 +233,12 @@ def get_module(user, request, location, student_module_cache, position=None):
module.metadata['data_dir'], module
)
# Allow URLs of the form '/course/' refer to the root of multicourse directory
# hierarchy of this course
module.get_html = replace_course_urls(module.get_html, course_id, module)
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
......@@ -304,10 +322,14 @@ def xqueue_callback(request, course_id, userid, id, dispatch):
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
......@@ -360,7 +382,12 @@ def modx_dispatch(request, dispatch=None, id=None, course_id=None):
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)
instance = get_module(request.user, request, id, student_module_cache, course_id=course_id)
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))
......
......@@ -9,7 +9,8 @@ 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 courseware.access import has_access
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.django import modulestore
......@@ -49,9 +50,13 @@ def update_template_dictionary(dictionary, request=None, course=None, article=No
if request:
dictionary.update(csrf(request))
if request and course:
dictionary['staff_access'] = has_access(request.user, course, 'staff')
else:
dictionary['staff_access'] = False
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 +72,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 +96,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 +114,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 +175,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 +223,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 +305,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 +338,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 +387,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 +420,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 +444,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 +467,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()
......
from django.contrib.auth.decorators import login_required
from mitxmako.shortcuts import render_to_response
from courseware.courses import check_course
from courseware.access import has_access
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)
raw_table_of_contents = open('lms/templates/book_toc.xml', 'r') # TODO: This will need to come from S3
course = get_course_with_access(request.user, course_id, 'load')
staff_access = has_access(request.user, course, 'staff')
# TODO: This will need to come from S3
raw_table_of_contents = open('lms/templates/book_toc.xml', 'r')
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,
'staff_access': staff_access})
def index_shifted(request, course_id, page):
......
......@@ -48,7 +48,11 @@ 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
# When True, will only publicly list courses by the subdomain. Expects you
# to define COURSE_LISTINGS, a dictionary mapping subdomains to lists of
# course_ids (see dev_int.py for an example)
'SUBDOMAIN_COURSE_LISTINGS' : False,
'ENABLE_TEXTBOOK' : True,
'ENABLE_DISCUSSION' : True,
......@@ -62,6 +66,7 @@ MITX_FEATURES = {
'ACCESS_REQUIRE_STAFF_FOR_COURSE': False,
'AUTH_USE_OPENID': False,
'AUTH_USE_MIT_CERTIFICATES' : False,
}
# Used for A/B testing
......@@ -95,12 +100,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
......
......@@ -54,7 +54,7 @@ CACHES = {
}
XQUEUE_INTERFACE = {
"url": "http://xqueue.sandbox.edx.org",
"url": "http://sandbox-xqueue.edx.org",
"django_auth": {
"username": "lms",
"password": "***REMOVED***"
......
"""
This enables use of course listings by subdomain. To see it in action, point the
following domains to 127.0.0.1 in your /etc/hosts file:
berkeley.dev
harvard.dev
mit.dev
Note that OS X has a bug where using *.local domains is excruciatingly slow, so
use *.dev domains instead for local testing.
"""
from .dev import *
MITX_FEATURES['SUBDOMAIN_COURSE_LISTINGS'] = True
COURSE_LISTINGS = {
'default' : ['BerkeleyX/CS169.1x/2012_Fall',
'BerkeleyX/CS188.1x/2012_Fall',
'HarvardX/CS50x/2012',
'HarvardX/PH207x/2012_Fall',
'MITx/3.091x/2012_Fall',
'MITx/6.002x/2012_Fall',
'MITx/6.00x/2012_Fall'],
'berkeley': ['BerkeleyX/CS169.1x/2012_Fall',
'BerkeleyX/CS188.1x/2012_Fall'],
'harvard' : ['HarvardX/CS50x/2012'],
'mit' : ['MITx/3.091x/2012_Fall',
'MITx/6.00x/2012_Fall']
}
......@@ -51,7 +51,7 @@ GITHUB_REPO_ROOT = ENV_ROOT / "data"
XQUEUE_INTERFACE = {
"url": "http://xqueue.sandbox.edx.org",
"url": "http://sandbox-xqueue.edx.org",
"django_auth": {
"username": "lms",
"password": "***REMOVED***"
......
......@@ -7,7 +7,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">
......@@ -28,7 +28,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 staff_access:
<li class="instructor"><a href="${reverse('instructor_dashboard', args=[course.id])}" class="${url_class('instructor')}">Instructor</a></li>
% endif
......
<%!
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])
......
......@@ -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>
<%!
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])
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment