Commit 028bd23c by Rocky Duan

Merge branch 'master' of github.com:MITx/mitx into profile

Conflicts:
	lms/djangoapps/courseware/module_render.py
	lms/djangoapps/courseware/views.py
	lms/templates/course_navigation.html
parents 0d2f787c 9aeec690
...@@ -215,9 +215,6 @@ def preview_module_system(request, preview_id, descriptor): ...@@ -215,9 +215,6 @@ def preview_module_system(request, preview_id, descriptor):
render_template=render_from_lms, render_template=render_from_lms,
debug=True, debug=True,
replace_urls=replace_urls, replace_urls=replace_urls,
# TODO (vshnayder): All CMS users get staff view by default
# is that what we want?
is_staff=True,
) )
......
...@@ -24,6 +24,7 @@ MODULESTORE = { ...@@ -24,6 +24,7 @@ MODULESTORE = {
'db': 'xmodule', 'db': 'xmodule',
'collection': 'modulestore', 'collection': 'modulestore',
'fs_root': GITHUB_REPO_ROOT, 'fs_root': GITHUB_REPO_ROOT,
'render_template': 'mitxmako.shortcuts.render_to_string',
} }
} }
} }
......
...@@ -47,6 +47,7 @@ MODULESTORE = { ...@@ -47,6 +47,7 @@ MODULESTORE = {
'db': 'test_xmodule', 'db': 'test_xmodule',
'collection': 'modulestore', 'collection': 'modulestore',
'fs_root': GITHUB_REPO_ROOT, 'fs_root': GITHUB_REPO_ROOT,
'render_template': 'mitxmako.shortcuts.render_to_string',
} }
} }
} }
......
import functools
import json import json
import logging import logging
import random import random
...@@ -156,7 +157,7 @@ def edXauth_signup(request, eamap=None): ...@@ -156,7 +157,7 @@ def edXauth_signup(request, eamap=None):
log.debug('ExtAuth: doing signup for %s' % eamap.external_email) 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 # MIT SSL
...@@ -206,7 +207,7 @@ def edXauth_ssl_login(request): ...@@ -206,7 +207,7 @@ def edXauth_ssl_login(request):
pass pass
if not cert: if not cert:
# no certificate information - go onward to main index # 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) (user, email, fullname) = ssl_dn_extract_info(cert)
...@@ -216,4 +217,4 @@ def edXauth_ssl_login(request): ...@@ -216,4 +217,4 @@ def edXauth_ssl_login(request):
credentials=cert, credentials=cert,
email=email, email=email,
fullname=fullname, fullname=fullname,
retfun = student_views.main_index) retfun = functools.partial(student_views.main_index, request))
from django.template.base import TemplateDoesNotExist
from django.template.loader import make_origin, get_template_from_string
from django.template.loaders.filesystem import Loader as FilesystemLoader
from django.template.loaders.app_directories import Loader as AppDirectoriesLoader
from mitxmako.template import Template
class MakoLoader(object):
"""
This is a Django loader object which will load the template as a
Mako template if the first line is "## mako". It is based off BaseLoader
in django.template.loader.
"""
is_usable = False
def __init__(self, base_loader):
# base_loader is an instance of a BaseLoader subclass
self.base_loader = base_loader
def __call__(self, template_name, template_dirs=None):
return self.load_template(template_name, template_dirs)
def load_template(self, template_name, template_dirs=None):
source, display_name = self.load_template_source(template_name, template_dirs)
if source.startswith("## mako\n"):
# This is a mako template
template = Template(text=source, uri=template_name)
return template, None
else:
# This is a regular template
origin = make_origin(display_name, self.load_template_source, template_name, template_dirs)
try:
template = get_template_from_string(source, origin, template_name)
return template, None
except TemplateDoesNotExist:
# If compiling the template we found raises TemplateDoesNotExist, back off to
# returning the source and display name for the template we were asked to load.
# This allows for correct identification (later) of the actual template that does
# not exist.
return source, display_name
def load_template_source(self, template_name, template_dirs=None):
# Just having this makes the template load as an instance, instead of a class.
return self.base_loader.load_template_source(template_name, template_dirs)
def reset(self):
self.base_loader.reset()
class MakoFilesystemLoader(MakoLoader):
is_usable = True
def __init__(self):
MakoLoader.__init__(self, FilesystemLoader())
class MakoAppDirectoriesLoader(MakoLoader):
is_usable = True
def __init__(self):
MakoLoader.__init__(self, AppDirectoriesLoader())
...@@ -12,18 +12,48 @@ ...@@ -12,18 +12,48 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from django.conf import settings
from mako.template import Template as MakoTemplate from mako.template import Template as MakoTemplate
from . import middleware from mitxmako import middleware
django_variables = ['lookup', 'template_dirs', 'output_encoding', django_variables = ['lookup', 'output_encoding',
'module_directory', 'encoding_errors'] 'module_directory', 'encoding_errors']
# TODO: We should make this a Django Template subclass that simply has the MakoTemplate inside of it? (Intead of inheriting from MakoTemplate)
class Template(MakoTemplate): class Template(MakoTemplate):
"""
This bridges the gap between a Mako template and a djano template. It can
be rendered like it is a django template because the arguments are transformed
in a way that MakoTemplate can understand.
"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""Overrides base __init__ to provide django variable overrides""" """Overrides base __init__ to provide django variable overrides"""
if not kwargs.get('no_django', False): if not kwargs.get('no_django', False):
overrides = dict([(k, getattr(middleware, k, None),) for k in django_variables]) overrides = dict([(k, getattr(middleware, k, None),) for k in django_variables])
overrides['lookup'] = overrides['lookup']['main']
kwargs.update(overrides) kwargs.update(overrides)
super(Template, self).__init__(*args, **kwargs) super(Template, self).__init__(*args, **kwargs)
def render(self, context_instance):
"""
This takes a render call with a context (from Django) and translates
it to a render call on the mako template.
"""
# collapse context_instance to a single dictionary for mako
context_dictionary = {}
# In various testing contexts, there might not be a current request context.
if middleware.requestcontext is not None:
for d in middleware.requestcontext:
context_dictionary.update(d)
for d in context_instance:
context_dictionary.update(d)
context_dictionary['settings'] = settings
context_dictionary['MITX_ROOT_URL'] = settings.MITX_ROOT_URL
context_dictionary['django_context'] = context_instance
return super(Template, self).render(**context_dictionary)
from django.template import loader
from django.template.base import Template, Context
from django.template.loader import get_template, select_template
def django_template_include(file_name, mako_context):
"""
This can be used within a mako template to include a django template
in the way that a django-style {% include %} does. Pass it context
which can be the mako context ('context') or a dictionary.
"""
dictionary = dict( mako_context )
return loader.render_to_string(file_name, dictionary=dictionary)
def render_inclusion(func, file_name, takes_context, django_context, *args, **kwargs):
"""
This allows a mako template to call a template tag function (written
for django templates) that is an "inclusion tag". These functions are
decorated with @register.inclusion_tag.
-func: This is the function that is registered as an inclusion tag.
You must import it directly using a python import statement.
-file_name: This is the filename of the template, passed into the
@register.inclusion_tag statement.
-takes_context: This is a parameter of the @register.inclusion_tag.
-django_context: This is an instance of the django context. If this
is a mako template rendered through the regular django rendering calls,
a copy of the django context is available as 'django_context'.
-*args and **kwargs are the arguments to func.
"""
if takes_context:
args = [django_context] + list(args)
_dict = func(*args, **kwargs)
if isinstance(file_name, Template):
t = file_name
elif not isinstance(file_name, basestring) and is_iterable(file_name):
t = select_template(file_name)
else:
t = get_template(file_name)
nodelist = t.nodelist
new_context = Context(_dict)
csrf_token = django_context.get('csrf_token', None)
if csrf_token is not None:
new_context['csrf_token'] = csrf_token
return nodelist.render(new_context)
...@@ -15,7 +15,7 @@ def try_staticfiles_lookup(path): ...@@ -15,7 +15,7 @@ def try_staticfiles_lookup(path):
try: try:
url = staticfiles_storage.url(path) url = staticfiles_storage.url(path)
except Exception as err: except Exception as err:
log.warning("staticfiles_storage couldn't find path {}: {}".format( log.warning("staticfiles_storage couldn't find path {0}: {1}".format(
path, str(err))) path, str(err)))
# Just return the original path; don't kill everything. # Just return the original path; don't kill everything.
url = path url = path
......
...@@ -298,7 +298,8 @@ def replicate_enrollment_save(sender, **kwargs): ...@@ -298,7 +298,8 @@ def replicate_enrollment_save(sender, **kwargs):
enrollment_obj = kwargs['instance'] enrollment_obj = kwargs['instance']
log.debug("Replicating user because of new enrollment") log.debug("Replicating user because of new enrollment")
replicate_user(enrollment_obj.user, enrollment_obj.course_id) for course_db_name in db_names_to_replicate_to(enrollment_obj.user.id):
replicate_user(enrollment_obj.user, course_db_name)
log.debug("Replicating enrollment because of new enrollment") log.debug("Replicating enrollment because of new enrollment")
replicate_model(CourseEnrollment.save, enrollment_obj, enrollment_obj.user_id) replicate_model(CourseEnrollment.save, enrollment_obj, enrollment_obj.user_id)
......
...@@ -38,8 +38,8 @@ from xmodule.modulestore.exceptions import ItemNotFoundError ...@@ -38,8 +38,8 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
from datetime import date from datetime import date
from collections import namedtuple from collections import namedtuple
from courseware.courses import (course_staff_group_name, has_staff_access_to_course, from courseware.courses import get_courses_by_university
get_courses_by_university) from courseware.access import has_access
log = logging.getLogger("mitx.student") log = logging.getLogger("mitx.student")
Article = namedtuple('Article', 'title url author image deck publication publish_date') Article = namedtuple('Article', 'title url author image deck publication publish_date')
...@@ -68,9 +68,9 @@ def index(request): ...@@ -68,9 +68,9 @@ def index(request):
from external_auth.views import edXauth_ssl_login from external_auth.views import edXauth_ssl_login
return edXauth_ssl_login(request) 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. Render the edX main page.
...@@ -93,7 +93,8 @@ def main_index(extra_context = {}, user=None): ...@@ -93,7 +93,8 @@ def main_index(extra_context = {}, user=None):
entry.summary = soup.getText() entry.summary = soup.getText()
# The course selection work is done in courseware.courses. # 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 = {'universities': universities, 'entries': entries}
context.update(extra_context) context.update(extra_context)
return render_to_response('index.html', context) return render_to_response('index.html', context)
...@@ -166,22 +167,6 @@ def change_enrollment_view(request): ...@@ -166,22 +167,6 @@ def change_enrollment_view(request):
"""Delegate to change_enrollment to actually do the work.""" """Delegate to change_enrollment to actually do the work."""
return HttpResponse(json.dumps(change_enrollment(request))) return HttpResponse(json.dumps(change_enrollment(request)))
def enrollment_allowed(user, course):
"""If the course has an enrollment period, check whether we are in it.
Also respects the DARK_LAUNCH setting"""
now = time.gmtime()
start = course.enrollment_start
end = course.enrollment_end
if (start is None or now > start) and (end is None or now < end):
# in enrollment period.
return True
if settings.MITX_FEATURES['DARK_LAUNCH']:
if has_staff_access_to_course(user, course):
# if dark launch, staff can enroll outside enrollment window
return True
return False
def change_enrollment(request): def change_enrollment(request):
...@@ -209,18 +194,7 @@ def change_enrollment(request): ...@@ -209,18 +194,7 @@ def change_enrollment(request):
.format(user.username, enrollment.course_id)) .format(user.username, enrollment.course_id))
return {'success': False, 'error': 'The course requested does not exist.'} return {'success': False, 'error': 'The course requested does not exist.'}
if settings.MITX_FEATURES.get('ACCESS_REQUIRE_STAFF_FOR_COURSE'): if not has_access(user, course, 'enroll'):
# require that user be in the staff_* group (or be an
# overall admin) to be able to enroll eg staff_6.002x or
# staff_6.00x
if not has_staff_access_to_course(user, course):
staff_group = course_staff_group_name(course)
log.debug('user %s denied enrollment to %s ; not in %s' % (
user, course.location.url(), staff_group))
return {'success': False,
'error' : '%s membership required to access course.' % staff_group}
if not enrollment_allowed(user, course):
return {'success': False, return {'success': False,
'error': 'enrollment in {} not allowed at this time' 'error': 'enrollment in {} not allowed at this time'
.format(course.display_name)} .format(course.display_name)}
......
...@@ -34,6 +34,17 @@ def wrap_xmodule(get_html, module, template): ...@@ -34,6 +34,17 @@ def wrap_xmodule(get_html, module, template):
return _get_html 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): def replace_static_urls(get_html, prefix, module):
""" """
Updates the supplied module with a new get_html function that wraps Updates the supplied module with a new get_html function that wraps
......
...@@ -873,6 +873,7 @@ def sympy_check2(): ...@@ -873,6 +873,7 @@ def sympy_check2():
msg = '<font color="red">No answer entered!</font>' if self.xml.get('empty_answer_err') else '' msg = '<font color="red">No answer entered!</font>' if self.xml.get('empty_answer_err') else ''
return CorrectMap(idset[0], 'incorrect', msg=msg) return CorrectMap(idset[0], 'incorrect', msg=msg)
# NOTE: correct = 'unknown' could be dangerous. Inputtypes such as textline are not expecting 'unknown's
correct = ['unknown'] * len(idset) correct = ['unknown'] * len(idset)
messages = [''] * len(idset) messages = [''] * len(idset)
...@@ -898,6 +899,7 @@ def sympy_check2(): ...@@ -898,6 +899,7 @@ def sympy_check2():
if type(self.code) == str: if type(self.code) == str:
try: try:
exec self.code in self.context['global_context'], self.context exec self.code in self.context['global_context'], self.context
correct = self.context['correct']
except Exception as err: except Exception as err:
print "oops in customresponse (code) error %s" % err print "oops in customresponse (code) error %s" % err
print "context = ", self.context print "context = ", self.context
...@@ -1271,7 +1273,7 @@ main() ...@@ -1271,7 +1273,7 @@ main()
def setup_response(self): def setup_response(self):
xml = self.xml xml = self.xml
self.url = xml.get('url') or "http://eecs1.mit.edu:8889/pyloncapa" # FIXME - hardcoded URL self.url = xml.get('url') or "http://qisx.mit.edu:8889/pyloncapa" # FIXME - hardcoded URL
# answer = xml.xpath('//*[@id=$id]//answer',id=xml.get('id'))[0] # FIXME - catch errors # answer = xml.xpath('//*[@id=$id]//answer',id=xml.get('id'))[0] # FIXME - catch errors
answer = xml.find('answer') answer = xml.find('answer')
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
> ${option_description}</option> > ${option_description}</option>
% endfor % endfor
</select> </select>
<span id="answer_${id}"></span> <span id="answer_${id}"></span>
% if state == 'unsubmitted': % if state == 'unsubmitted':
......
...@@ -38,5 +38,7 @@ ...@@ -38,5 +38,7 @@
% if msg: % if msg:
<span class="message">${msg|n}</span> <span class="message">${msg|n}</span>
% endif % endif
% if state in ['unsubmitted', 'correct', 'incorrect', 'incomplete'] or hidden:
</div> </div>
% endif
</section> </section>
from setuptools import setup, find_packages
setup(
name="mitxmako",
version="0.1",
packages=find_packages(exclude=["tests"]),
install_requires=['distribute'],
)
...@@ -10,7 +10,6 @@ setup( ...@@ -10,7 +10,6 @@ setup(
}, },
requires=[ requires=[
'capa', 'capa',
'mitxmako'
], ],
# See http://guide.python-distribute.org/creation.html#entry-points # See http://guide.python-distribute.org/creation.html#entry-points
......
...@@ -49,9 +49,9 @@ class ABTestModule(XModule): ...@@ -49,9 +49,9 @@ class ABTestModule(XModule):
return json.dumps({'group': self.group}) return json.dumps({'group': self.group})
def displayable_items(self): def displayable_items(self):
return [self.system.get_module(child) child_locations = self.definition['data']['group_content'][self.group]
for child children = [self.system.get_module(loc) for loc in child_locations]
in self.definition['data']['group_content'][self.group]] return [c for c in children if c is not None]
# TODO (cpennington): Use Groups should be a first class object, rather than being # TODO (cpennington): Use Groups should be a first class object, rather than being
......
...@@ -21,6 +21,7 @@ def process_includes(fn): ...@@ -21,6 +21,7 @@ def process_includes(fn):
xml_object = etree.fromstring(xml_data) xml_object = etree.fromstring(xml_data)
next_include = xml_object.find('include') next_include = xml_object.find('include')
while next_include is not None: while next_include is not None:
system.error_tracker("WARNING: the <include> tag is deprecated, and will go away.")
file = next_include.get('file') file = next_include.get('file')
parent = next_include.getparent() parent = next_include.getparent()
...@@ -67,6 +68,8 @@ class SemanticSectionDescriptor(XModuleDescriptor): ...@@ -67,6 +68,8 @@ class SemanticSectionDescriptor(XModuleDescriptor):
the child element the child element
""" """
xml_object = etree.fromstring(xml_data) xml_object = etree.fromstring(xml_data)
system.error_tracker("WARNING: the &lt;{0}> tag is deprecated. Please do not use in new content."
.format(xml_object.tag))
if len(xml_object) == 1: if len(xml_object) == 1:
for (key, val) in xml_object.items(): for (key, val) in xml_object.items():
...@@ -74,7 +77,7 @@ class SemanticSectionDescriptor(XModuleDescriptor): ...@@ -74,7 +77,7 @@ class SemanticSectionDescriptor(XModuleDescriptor):
return system.process_xml(etree.tostring(xml_object[0])) return system.process_xml(etree.tostring(xml_object[0]))
else: else:
xml_object.tag = 'sequence' xml_object.tag = 'sequential'
return system.process_xml(etree.tostring(xml_object)) return system.process_xml(etree.tostring(xml_object))
...@@ -83,10 +86,14 @@ class TranslateCustomTagDescriptor(XModuleDescriptor): ...@@ -83,10 +86,14 @@ class TranslateCustomTagDescriptor(XModuleDescriptor):
def from_xml(cls, xml_data, system, org=None, course=None): def from_xml(cls, xml_data, system, org=None, course=None):
""" """
Transforms the xml_data from <$custom_tag attr="" attr=""/> to Transforms the xml_data from <$custom_tag attr="" attr=""/> to
<customtag attr="" attr=""><impl>$custom_tag</impl></customtag> <customtag attr="" attr="" impl="$custom_tag"/>
""" """
xml_object = etree.fromstring(xml_data) xml_object = etree.fromstring(xml_data)
system.error_tracker('WARNING: the <{tag}> tag is deprecated. '
'Instead, use <customtag impl="{tag}" attr1="..." attr2="..."/>. '
.format(tag=xml_object.tag))
tag = xml_object.tag tag = xml_object.tag
xml_object.tag = 'customtag' xml_object.tag = 'customtag'
xml_object.attrib['impl'] = tag xml_object.attrib['impl'] = tag
......
...@@ -108,11 +108,9 @@ class CapaModule(XModule): ...@@ -108,11 +108,9 @@ class CapaModule(XModule):
self.grace_period = None self.grace_period = None
self.close_date = self.display_due_date self.close_date = self.display_due_date
self.max_attempts = only_one(dom2.xpath('/problem/@attempts')) self.max_attempts = self.metadata.get('attempts', None)
if len(self.max_attempts) > 0: if self.max_attempts is not None:
self.max_attempts = int(self.max_attempts) self.max_attempts = int(self.max_attempts)
else:
self.max_attempts = None
self.show_answer = self.metadata.get('showanswer', 'closed') self.show_answer = self.metadata.get('showanswer', 'closed')
...@@ -237,14 +235,21 @@ class CapaModule(XModule): ...@@ -237,14 +235,21 @@ class CapaModule(XModule):
else: else:
raise raise
content = {'name': self.metadata['display_name'], content = {'name': self.display_name,
'html': html, 'html': html,
'weight': self.weight, 'weight': self.weight,
} }
# We using strings as truthy values, because the terminology of the # We using strings as truthy values, because the terminology of the
# check button is context-specific. # check button is context-specific.
check_button = "Grade" if self.max_attempts else "Check"
# Put a "Check" button if unlimited attempts or still some left
if self.max_attempts is None or self.attempts < self.max_attempts-1:
check_button = "Check"
else:
# Will be final check so let user know that
check_button = "Final Check"
reset_button = True reset_button = True
save_button = True save_button = True
...@@ -376,14 +381,17 @@ class CapaModule(XModule): ...@@ -376,14 +381,17 @@ class CapaModule(XModule):
''' '''
For the "show answer" button. For the "show answer" button.
TODO: show answer events should be logged here, not just in the problem.js
Returns the answers: {'answers' : answers} Returns the answers: {'answers' : answers}
''' '''
event_info = dict()
event_info['problem_id'] = self.location.url()
self.system.track_function('show_answer', event_info)
if not self.answer_available(): if not self.answer_available():
raise NotFoundError('Answer is not available') raise NotFoundError('Answer is not available')
else: else:
answers = self.lcp.get_question_answers() answers = self.lcp.get_question_answers()
# answers (eg <solution>) may have embedded images
answers = dict( (k,self.system.replace_urls(answers[k], self.metadata['data_dir'])) for k in answers )
return {'answers': answers} return {'answers': answers}
# Figure out if we should move these to capa_problem? # Figure out if we should move these to capa_problem?
...@@ -464,7 +472,7 @@ class CapaModule(XModule): ...@@ -464,7 +472,7 @@ class CapaModule(XModule):
return {'success': msg} return {'success': msg}
log.exception("Error in capa_module problem checking") log.exception("Error in capa_module problem checking")
raise Exception("error in capa_module") raise Exception("error in capa_module")
self.attempts = self.attempts + 1 self.attempts = self.attempts + 1
self.lcp.done = True self.lcp.done = True
......
from fs.errors import ResourceNotFoundError from fs.errors import ResourceNotFoundError
import time import time
import dateutil.parser
import logging import logging
from lxml import etree
from xmodule.util.decorators import lazyproperty from xmodule.util.decorators import lazyproperty
from xmodule.graders import load_grading_policy from xmodule.graders import load_grading_policy
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.seq_module import SequenceDescriptor, SequenceModule from xmodule.seq_module import SequenceDescriptor, SequenceModule
from xmodule.timeparse import parse_time, stringify_time
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class CourseDescriptor(SequenceDescriptor): class CourseDescriptor(SequenceDescriptor):
module_class = SequenceModule module_class = SequenceModule
class Textbook:
def __init__(self, title, table_of_contents_url):
self.title = title
self.table_of_contents_url = table_of_contents_url
@classmethod
def from_xml_object(cls, xml_object):
return cls(xml_object.get('title'), xml_object.get('table_of_contents_url'))
@property
def table_of_contents(self):
raw_table_of_contents = open(self.table_of_contents_url, 'r') # TODO: This will need to come from S3
table_of_contents = etree.parse(raw_table_of_contents).getroot()
return table_of_contents
def __init__(self, system, definition=None, **kwargs): def __init__(self, system, definition=None, **kwargs):
super(CourseDescriptor, self).__init__(system, definition, **kwargs) super(CourseDescriptor, self).__init__(system, definition, **kwargs)
self.textbooks = self.definition['data']['textbooks']
msg = None msg = None
try: if self.start is None:
self.start = time.strptime(self.metadata["start"], "%Y-%m-%dT%H:%M") msg = "Course loaded without a valid start date. id = %s" % self.id
except KeyError: # hack it -- start in 1970
msg = "Course loaded without a start date. id = %s" % self.id self.metadata['start'] = stringify_time(time.gmtime(0))
except ValueError as e:
msg = "Course loaded with a bad start date. %s '%s'" % (self.id, e)
# Don't call the tracker from the exception handler.
if msg is not None:
self.start = time.gmtime(0) # The epoch
log.critical(msg) log.critical(msg)
system.error_tracker(msg) system.error_tracker(msg)
def try_parse_time(key): self.enrollment_start = self._try_parse_time("enrollment_start")
""" self.enrollment_end = self._try_parse_time("enrollment_end")
Parse an optional metadata key: if present, must be valid.
Return None if not present.
"""
if key in self.metadata:
try:
return time.strptime(self.metadata[key], "%Y-%m-%dT%H:%M")
except ValueError as e:
msg = "Course %s loaded with a bad metadata key %s '%s'" % (
self.id, self.metadata[key], e)
log.warning(msg)
return None
self.enrollment_start = try_parse_time("enrollment_start")
self.enrollment_end = try_parse_time("enrollment_end")
@classmethod
def definition_from_xml(cls, xml_object, system):
textbooks = []
for textbook in xml_object.findall("textbook"):
textbooks.append(cls.Textbook.from_xml_object(textbook))
xml_object.remove(textbook)
definition = super(CourseDescriptor, cls).definition_from_xml(xml_object, system)
definition.setdefault('data', {})['textbooks'] = textbooks
return definition
def has_started(self): def has_started(self):
return time.gmtime() > self.start return time.gmtime() > self.start
...@@ -76,7 +80,6 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -76,7 +80,6 @@ class CourseDescriptor(SequenceDescriptor):
return grading_policy return grading_policy
@lazyproperty @lazyproperty
def grading_context(self): def grading_context(self):
""" """
...@@ -154,6 +157,7 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -154,6 +157,7 @@ class CourseDescriptor(SequenceDescriptor):
@property @property
def id(self): def id(self):
"""Return the course_id for this course"""
return self.location_to_id(self.location) return self.location_to_id(self.location)
@property @property
...@@ -162,14 +166,14 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -162,14 +166,14 @@ class CourseDescriptor(SequenceDescriptor):
@property @property
def title(self): def title(self):
return self.metadata['display_name'] return self.display_name
@property @property
def number(self): def number(self):
return self.location.course return self.location.course
@property @property
def wiki_namespace(self): def wiki_slug(self):
return self.location.course return self.location.course
@property @property
......
...@@ -4,7 +4,7 @@ nav.sequence-nav { ...@@ -4,7 +4,7 @@ nav.sequence-nav {
@extend .topbar; @extend .topbar;
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
@include border-top-right-radius(4px); @include border-top-right-radius(4px);
margin: (-(lh())) (-(lh())) lh() (-(lh())); margin: 0 0 lh() (-(lh()));
position: relative; position: relative;
ol { ol {
...@@ -41,11 +41,11 @@ nav.sequence-nav { ...@@ -41,11 +41,11 @@ nav.sequence-nav {
&:hover { &:hover {
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: center; background-position: center;
background-color: #F6F6F6; background-color: #F3F3F3;
} }
&.visited { &.visited {
background-color: #F6F6F6; background-color: #F3F3F3;
&:hover { &:hover {
background-position: center center; background-position: center center;
...@@ -64,18 +64,6 @@ nav.sequence-nav { ...@@ -64,18 +64,6 @@ nav.sequence-nav {
} }
} }
&.progress-none {
background-color: lighten(red, 50%);
}
&.progress-some {
background-color: yellow;
}
&.progress-done {
background-color: green;
}
//video //video
&.seq_video { &.seq_video {
&.inactive { &.inactive {
...@@ -120,6 +108,18 @@ nav.sequence-nav { ...@@ -120,6 +108,18 @@ nav.sequence-nav {
&.active { &.active {
background-image: url('../images/sequence-nav/list-icon-current.png'); background-image: url('../images/sequence-nav/list-icon-current.png');
} }
&.progress-none {
background-image: url('../images/sequence-nav/list-unstarted.png');
}
&.progress-some, &.progress-in_progress {
background-image: url('../images/sequence-nav/list-unfinished.png');
}
&.progress-done {
background-image: url('../images/sequence-nav/list-finished.png');
}
} }
p { p {
......
...@@ -4,7 +4,7 @@ div.video { ...@@ -4,7 +4,7 @@ div.video {
border-bottom: 1px solid #e1e1e1; border-bottom: 1px solid #e1e1e1;
border-top: 1px solid #e1e1e1; border-top: 1px solid #e1e1e1;
display: block; display: block;
margin: 0 (-(lh())); margin: 0 0 0 (-(lh()));
padding: 6px lh(); padding: 6px lh();
article.video-wrapper { article.video-wrapper {
...@@ -14,7 +14,7 @@ div.video { ...@@ -14,7 +14,7 @@ div.video {
section.video-player { section.video-player {
height: 0; height: 0;
// overflow: hidden; overflow: hidden;
padding-bottom: 56.25%; padding-bottom: 56.25%;
position: relative; position: relative;
...@@ -171,7 +171,7 @@ div.video { ...@@ -171,7 +171,7 @@ div.video {
position: relative; position: relative;
@include transition(); @include transition();
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
width: 110px; width: 116px;
h3 { h3 {
color: #999; color: #999;
...@@ -209,7 +209,7 @@ div.video { ...@@ -209,7 +209,7 @@ div.video {
display: none; display: none;
opacity: 0; opacity: 0;
position: absolute; position: absolute;
width: 125px; width: 133px;
z-index: 10; z-index: 10;
li { li {
...@@ -444,13 +444,13 @@ div.video { ...@@ -444,13 +444,13 @@ div.video {
height: 100%; height: 100%;
left: 0; left: 0;
margin: 0; margin: 0;
max-height: 100%;
overflow: hidden; overflow: hidden;
padding: 0; padding: 0;
position: fixed; position: fixed;
top: 0; top: 0;
width: 100%; width: 100%;
z-index: 999; z-index: 999;
vertical-align: middle;
&.closed { &.closed {
ol.subtitles { ol.subtitles {
...@@ -459,30 +459,17 @@ div.video { ...@@ -459,30 +459,17 @@ div.video {
} }
} }
a.exit {
color: #aaa;
display: none;
font-style: 12px;
left: 20px;
letter-spacing: 1px;
position: absolute;
text-transform: uppercase;
top: 20px;
&::after {
content: "✖";
@include inline-block();
padding-left: 6px;
}
&:hover {
color: $mit-red;
}
}
div.tc-wrapper { div.tc-wrapper {
@include clearfix;
display: table;
width: 100%;
height: 100%;
article.video-wrapper { article.video-wrapper {
width: 100%; width: 100%;
display: table-cell;
vertical-align: middle;
float: none;
} }
object, iframe { object, iframe {
......
...@@ -24,16 +24,8 @@ class ErrorModule(XModule): ...@@ -24,16 +24,8 @@ class ErrorModule(XModule):
return self.system.render_template('module-error.html', { return self.system.render_template('module-error.html', {
'data' : self.definition['data']['contents'], 'data' : self.definition['data']['contents'],
'error' : self.definition['data']['error_msg'], 'error' : self.definition['data']['error_msg'],
'is_staff' : self.system.is_staff,
}) })
def displayable_items(self):
"""Hide errors in the profile and table of contents for non-staff
users.
"""
if self.system.is_staff:
return [self]
return []
class ErrorDescriptor(EditingDescriptor): class ErrorDescriptor(EditingDescriptor):
""" """
......
...@@ -39,6 +39,8 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor): ...@@ -39,6 +39,8 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
def backcompat_paths(cls, path): def backcompat_paths(cls, path):
if path.endswith('.html.xml'): if path.endswith('.html.xml'):
path = path[:-9] + '.html' # backcompat--look for html instead of xml path = path[:-9] + '.html' # backcompat--look for html instead of xml
if path.endswith('.html.html'):
path = path[:-5] # some people like to include .html in filenames..
candidates = [] candidates = []
while os.sep in path: while os.sep in path:
candidates.append(path) candidates.append(path)
...@@ -73,7 +75,9 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor): ...@@ -73,7 +75,9 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
cls.clean_metadata_from_xml(definition_xml) cls.clean_metadata_from_xml(definition_xml)
return {'data': stringify_children(definition_xml)} return {'data': stringify_children(definition_xml)}
else: else:
filepath = cls._format_filepath(xml_object.tag, filename) # html is special. cls.filename_extension is 'xml', but if 'filename' is in the definition,
# that means to load from .html
filepath = "{category}/{name}.html".format(category='html', name=filename)
# VS[compat] # VS[compat]
# TODO (cpennington): If the file doesn't exist at the right path, # TODO (cpennington): If the file doesn't exist at the right path,
......
...@@ -172,11 +172,13 @@ schematic = (function() { ...@@ -172,11 +172,13 @@ schematic = (function() {
this.tools = new Array(); this.tools = new Array();
this.toolbar = []; this.toolbar = [];
/* DISABLE HELP BUTTON (target URL not consistent with multicourse hierarchy) -- SJSU
if (!this.diagram_only) { if (!this.diagram_only) {
this.tools['help'] = this.add_tool(help_icon,'Help: display help page',this.help); this.tools['help'] = this.add_tool(help_icon,'Help: display help page',this.help);
this.enable_tool('help',true); this.enable_tool('help',true);
this.toolbar.push(null); // spacer this.toolbar.push(null); // spacer
} }
END DISABLE HELP BUTTON -- SJSU */
if (this.edits_allowed) { if (this.edits_allowed) {
this.tools['grid'] = this.add_tool(grid_icon,'Grid: toggle grid display',this.toggle_grid); this.tools['grid'] = this.add_tool(grid_icon,'Grid: toggle grid display',this.toggle_grid);
......
...@@ -130,13 +130,11 @@ class @VideoPlayer extends Subview ...@@ -130,13 +130,11 @@ class @VideoPlayer extends Subview
toggleFullScreen: (event) => toggleFullScreen: (event) =>
event.preventDefault() event.preventDefault()
if @el.hasClass('fullscreen') if @el.hasClass('fullscreen')
@$('.exit').remove()
@$('.add-fullscreen').attr('title', 'Fill browser') @$('.add-fullscreen').attr('title', 'Fill browser')
@el.removeClass('fullscreen') @el.removeClass('fullscreen')
else else
@el.append('<a href="#" class="exit">Exit</a>').addClass('fullscreen') @el.addClass('fullscreen')
@$('.add-fullscreen').attr('title', 'Exit fill browser') @$('.add-fullscreen').attr('title', 'Exit fill browser')
@$('.exit').click @toggleFullScreen
@caption.resize() @caption.resize()
# Delegates # Delegates
......
...@@ -12,15 +12,34 @@ from django.conf import settings ...@@ -12,15 +12,34 @@ from django.conf import settings
_MODULESTORES = {} _MODULESTORES = {}
FUNCTION_KEYS = ['render_template']
def load_function(path):
"""
Load a function by name.
path is a string of the form "path.to.module.function"
returns the imported python object `function` from `path.to.module`
"""
module_path, _, name = path.rpartition('.')
return getattr(import_module(module_path), name)
def modulestore(name='default'): def modulestore(name='default'):
global _MODULESTORES global _MODULESTORES
if name not in _MODULESTORES: if name not in _MODULESTORES:
class_path = settings.MODULESTORE[name]['ENGINE'] class_ = load_function(settings.MODULESTORE[name]['ENGINE'])
module_path, _, class_name = class_path.rpartition('.')
class_ = getattr(import_module(module_path), class_name) options = {}
options.update(settings.MODULESTORE[name]['OPTIONS'])
for key in FUNCTION_KEYS:
if key in options:
options[key] = load_function(options[key])
_MODULESTORES[name] = class_( _MODULESTORES[name] = class_(
**settings.MODULESTORE[name]['OPTIONS']) **options
)
return _MODULESTORES[name] return _MODULESTORES[name]
...@@ -9,7 +9,6 @@ from importlib import import_module ...@@ -9,7 +9,6 @@ from importlib import import_module
from xmodule.errortracker import null_error_tracker from xmodule.errortracker import null_error_tracker
from xmodule.x_module import XModuleDescriptor from xmodule.x_module import XModuleDescriptor
from xmodule.mako_module import MakoDescriptorSystem from xmodule.mako_module import MakoDescriptorSystem
from mitxmako.shortcuts import render_to_string
from . import ModuleStoreBase, Location from . import ModuleStoreBase, Location
from .exceptions import (ItemNotFoundError, from .exceptions import (ItemNotFoundError,
...@@ -82,7 +81,8 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -82,7 +81,8 @@ class MongoModuleStore(ModuleStoreBase):
""" """
# TODO (cpennington): Enable non-filesystem filestores # TODO (cpennington): Enable non-filesystem filestores
def __init__(self, host, db, collection, fs_root, port=27017, default_class=None, def __init__(self, host, db, collection, fs_root, render_template,
port=27017, default_class=None,
error_tracker=null_error_tracker): error_tracker=null_error_tracker):
ModuleStoreBase.__init__(self) ModuleStoreBase.__init__(self)
...@@ -108,6 +108,7 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -108,6 +108,7 @@ class MongoModuleStore(ModuleStoreBase):
self.default_class = None self.default_class = None
self.fs_root = path(fs_root) self.fs_root = path(fs_root)
self.error_tracker = error_tracker self.error_tracker = error_tracker
self.render_template = render_template
def _clean_item_data(self, item): def _clean_item_data(self, item):
""" """
...@@ -160,7 +161,7 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -160,7 +161,7 @@ class MongoModuleStore(ModuleStoreBase):
self.default_class, self.default_class,
resource_fs, resource_fs,
self.error_tracker, self.error_tracker,
render_to_string, self.render_template,
) )
return system.load_item(item['location']) return system.load_item(item['location'])
......
...@@ -26,6 +26,7 @@ DB = 'test' ...@@ -26,6 +26,7 @@ DB = 'test'
COLLECTION = 'modulestore' COLLECTION = 'modulestore'
FS_ROOT = DATA_DIR # TODO (vshnayder): will need a real fs_root for testing load_item FS_ROOT = DATA_DIR # TODO (vshnayder): will need a real fs_root for testing load_item
DEFAULT_CLASS = 'xmodule.raw_module.RawDescriptor' DEFAULT_CLASS = 'xmodule.raw_module.RawDescriptor'
RENDER_TEMPLATE = lambda t_n, d, ctx=None, nsp='main': ''
class TestMongoModuleStore(object): class TestMongoModuleStore(object):
...@@ -48,7 +49,7 @@ class TestMongoModuleStore(object): ...@@ -48,7 +49,7 @@ class TestMongoModuleStore(object):
@staticmethod @staticmethod
def initdb(): def initdb():
# connect to the db # connect to the db
store = MongoModuleStore(HOST, DB, COLLECTION, FS_ROOT, default_class=DEFAULT_CLASS) store = MongoModuleStore(HOST, DB, COLLECTION, FS_ROOT, RENDER_TEMPLATE, default_class=DEFAULT_CLASS)
# Explicitly list the courses to load (don't want the big one) # Explicitly list the courses to load (don't want the big one)
courses = ['toy', 'simple'] courses = ['toy', 'simple']
import_from_xml(store, DATA_DIR, courses) import_from_xml(store, DATA_DIR, courses)
...@@ -112,8 +113,8 @@ class TestMongoModuleStore(object): ...@@ -112,8 +113,8 @@ class TestMongoModuleStore(object):
should_work = ( should_work = (
("i4x://edX/toy/video/Welcome", ("i4x://edX/toy/video/Welcome",
("edX/toy/2012_Fall", "Overview", "Welcome", None)), ("edX/toy/2012_Fall", "Overview", "Welcome", None)),
("i4x://edX/toy/html/toylab", ("i4x://edX/toy/chapter/Overview",
("edX/toy/2012_Fall", "Overview", "Toy_Videos", None)), ("edX/toy/2012_Fall", "Overview", None, None)),
) )
for location, expected in should_work: for location, expected in should_work:
assert_equals(path_to_location(self.store, location), expected) assert_equals(path_to_location(self.store, location), expected)
......
import json
import logging import logging
import os import os
import re import re
...@@ -5,6 +6,7 @@ import re ...@@ -5,6 +6,7 @@ import re
from fs.osfs import OSFS from fs.osfs import OSFS
from importlib import import_module from importlib import import_module
from lxml import etree from lxml import etree
from lxml.html import HtmlComment
from path import path from path import path
from xmodule.errortracker import ErrorLog, make_error_tracker from xmodule.errortracker import ErrorLog, make_error_tracker
from xmodule.x_module import XModuleDescriptor, XMLParsingSystem from xmodule.x_module import XModuleDescriptor, XMLParsingSystem
...@@ -15,9 +17,10 @@ from cStringIO import StringIO ...@@ -15,9 +17,10 @@ from cStringIO import StringIO
from . import ModuleStoreBase, Location from . import ModuleStoreBase, Location
from .exceptions import ItemNotFoundError from .exceptions import ItemNotFoundError
etree.set_default_parser( edx_xml_parser = etree.XMLParser(dtd_validation=False, load_dtd=False,
etree.XMLParser(dtd_validation=False, load_dtd=False, remove_comments=True, remove_blank_text=True)
remove_comments=True, remove_blank_text=True))
etree.set_default_parser(edx_xml_parser)
log = logging.getLogger('mitx.' + __name__) log = logging.getLogger('mitx.' + __name__)
...@@ -30,7 +33,8 @@ def clean_out_mako_templating(xml_string): ...@@ -30,7 +33,8 @@ def clean_out_mako_templating(xml_string):
return xml_string return xml_string
class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
def __init__(self, xmlstore, org, course, course_dir, error_tracker, **kwargs): def __init__(self, xmlstore, org, course, course_dir,
policy, error_tracker, **kwargs):
""" """
A class that handles loading from xml. Does some munging to ensure that A class that handles loading from xml. Does some munging to ensure that
all elements have unique slugs. all elements have unique slugs.
...@@ -96,7 +100,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): ...@@ -96,7 +100,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
MakoDescriptorSystem.__init__(self, load_item, resources_fs, MakoDescriptorSystem.__init__(self, load_item, resources_fs,
error_tracker, render_template, **kwargs) error_tracker, render_template, **kwargs)
XMLParsingSystem.__init__(self, load_item, resources_fs, XMLParsingSystem.__init__(self, load_item, resources_fs,
error_tracker, process_xml, **kwargs) error_tracker, process_xml, policy, **kwargs)
class XMLModuleStore(ModuleStoreBase): class XMLModuleStore(ModuleStoreBase):
...@@ -149,7 +153,7 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -149,7 +153,7 @@ class XMLModuleStore(ModuleStoreBase):
for course_dir in course_dirs: for course_dir in course_dirs:
self.try_load_course(course_dir) 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. Load a course, keeping track of errors as we go along.
''' '''
...@@ -170,7 +174,28 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -170,7 +174,28 @@ class XMLModuleStore(ModuleStoreBase):
''' '''
String representation - for debugging 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:
log.debug("Loading policy from {0}".format(policy_path))
with open(policy_path) as f:
return json.load(f)
except (IOError, ValueError) as err:
msg = "Error loading course policy from {0}".format(policy_path)
tracker(msg)
log.warning(msg + " " + str(err))
return {}
def load_course(self, course_dir, tracker): def load_course(self, course_dir, tracker):
""" """
...@@ -188,7 +213,7 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -188,7 +213,7 @@ class XMLModuleStore(ModuleStoreBase):
# been imported into the cms from xml # been imported into the cms from xml
course_file = StringIO(clean_out_mako_templating(course_file.read())) course_file = StringIO(clean_out_mako_templating(course_file.read()))
course_data = etree.parse(course_file).getroot() course_data = etree.parse(course_file,parser=edx_xml_parser).getroot()
org = course_data.get('org') org = course_data.get('org')
...@@ -211,9 +236,17 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -211,9 +236,17 @@ class XMLModuleStore(ModuleStoreBase):
tracker(msg) tracker(msg)
course = course_dir course = course_dir
system = ImportSystem(self, org, course, course_dir, tracker) url_name = course_data.get('url_name')
if url_name:
policy_path = self.data_dir / course_dir / 'policies' / '{0}.json'.format(url_name)
policy = self.load_policy(policy_path, tracker)
else:
policy = {}
system = ImportSystem(self, org, course, course_dir, policy, tracker)
course_descriptor = system.process_xml(etree.tostring(course_data)) course_descriptor = system.process_xml(etree.tostring(course_data))
# NOTE: The descriptors end up loading somewhat bottom up, which # NOTE: The descriptors end up loading somewhat bottom up, which
# breaks metadata inheritance via get_children(). Instead # breaks metadata inheritance via get_children(). Instead
# (actually, in addition to, for now), we do a final inheritance pass # (actually, in addition to, for now), we do a final inheritance pass
......
...@@ -76,7 +76,7 @@ class SequenceModule(XModule): ...@@ -76,7 +76,7 @@ class SequenceModule(XModule):
contents.append({ contents.append({
'content': child.get_html(), 'content': child.get_html(),
'title': "\n".join( 'title': "\n".join(
grand_child.metadata['display_name'].strip() grand_child.display_name.strip()
for grand_child in child.get_children() for grand_child in child.get_children()
if 'display_name' in grand_child.metadata if 'display_name' in grand_child.metadata
), ),
...@@ -107,7 +107,7 @@ class SequenceModule(XModule): ...@@ -107,7 +107,7 @@ class SequenceModule(XModule):
class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor): class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor):
mako_template = 'widgets/sequence-edit.html' mako_template = 'widgets/sequence-edit.html'
module_class = SequenceModule module_class = SequenceModule
stores_state = True # For remembering where in the sequence the student is stores_state = True # For remembering where in the sequence the student is
@classmethod @classmethod
......
...@@ -35,7 +35,6 @@ i4xs = ModuleSystem( ...@@ -35,7 +35,6 @@ i4xs = ModuleSystem(
filestore=fs.osfs.OSFS(os.path.dirname(os.path.realpath(__file__))+"/test_files"), filestore=fs.osfs.OSFS(os.path.dirname(os.path.realpath(__file__))+"/test_files"),
debug=True, debug=True,
xqueue={'interface':None, 'callback_url':'/', 'default_queuename': 'testqueue'}, xqueue={'interface':None, 'callback_url':'/', 'default_queuename': 'testqueue'},
is_staff=False,
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules") node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules")
) )
...@@ -336,7 +335,7 @@ class CodeResponseTest(unittest.TestCase): ...@@ -336,7 +335,7 @@ class CodeResponseTest(unittest.TestCase):
self.assertFalse(test_lcp.correct_map.is_queued(answer_ids[j])) # Should be dequeued, message delivered self.assertFalse(test_lcp.correct_map.is_queued(answer_ids[j])) # Should be dequeued, message delivered
else: else:
self.assertTrue(test_lcp.correct_map.is_queued(answer_ids[j])) # Should be queued, message undelivered self.assertTrue(test_lcp.correct_map.is_queued(answer_ids[j])) # Should be queued, message undelivered
def test_convert_files_to_filenames(self): def test_convert_files_to_filenames(self):
problem_file = os.path.dirname(__file__) + "/test_files/coderesponse.xml" problem_file = os.path.dirname(__file__) + "/test_files/coderesponse.xml"
fp = open(problem_file) fp = open(problem_file)
...@@ -347,7 +346,7 @@ class CodeResponseTest(unittest.TestCase): ...@@ -347,7 +346,7 @@ class CodeResponseTest(unittest.TestCase):
self.assertEquals(answers_converted['1_2_1'], 'String-based answer') self.assertEquals(answers_converted['1_2_1'], 'String-based answer')
self.assertEquals(answers_converted['1_3_1'], ['answer1', 'answer2', 'answer3']) self.assertEquals(answers_converted['1_3_1'], ['answer1', 'answer2', 'answer3'])
self.assertEquals(answers_converted['1_4_1'], fp.name) self.assertEquals(answers_converted['1_4_1'], fp.name)
class ChoiceResponseTest(unittest.TestCase): class ChoiceResponseTest(unittest.TestCase):
......
...@@ -42,9 +42,9 @@ class DummySystem(XMLParsingSystem): ...@@ -42,9 +42,9 @@ class DummySystem(XMLParsingSystem):
descriptor.get_children() descriptor.get_children()
return descriptor return descriptor
policy = {}
XMLParsingSystem.__init__(self, load_item, self.resources_fs, XMLParsingSystem.__init__(self, load_item, self.resources_fs,
self.errorlog.tracker, process_xml) self.errorlog.tracker, process_xml, policy)
def render_template(self, template, context): def render_template(self, template, context):
raise Exception("Shouldn't be called") raise Exception("Shouldn't be called")
...@@ -201,7 +201,7 @@ class ImportTestCase(unittest.TestCase): ...@@ -201,7 +201,7 @@ class ImportTestCase(unittest.TestCase):
def check_for_key(key, node): def check_for_key(key, node):
"recursive check for presence of key" "recursive check for presence of key"
print "Checking {}".format(node.location.url()) print "Checking {0}".format(node.location.url())
self.assertTrue(key in node.metadata) self.assertTrue(key in node.metadata)
for c in node.get_children(): for c in node.get_children():
check_for_key(key, c) check_for_key(key, c)
......
"""
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 ...@@ -8,8 +8,9 @@ from lxml import etree
from lxml.etree import XMLSyntaxError from lxml.etree import XMLSyntaxError
from pprint import pprint from pprint import pprint
from xmodule.modulestore import Location
from xmodule.errortracker import exc_info_to_str from xmodule.errortracker import exc_info_to_str
from xmodule.modulestore import Location
from xmodule.timeparse import parse_time
log = logging.getLogger('mitx.' + __name__) log = logging.getLogger('mitx.' + __name__)
...@@ -218,9 +219,11 @@ class XModule(HTMLSnippet): ...@@ -218,9 +219,11 @@ class XModule(HTMLSnippet):
Return module instances for all the children of this module. Return module instances for all the children of this module.
''' '''
if self._loaded_children is None: if self._loaded_children is None:
self._loaded_children = [ child_locations = self.definition.get('children', [])
self.system.get_module(child) children = [self.system.get_module(loc) for loc in child_locations]
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 = [c for c in children if c is not None]
return self._loaded_children return self._loaded_children
...@@ -295,6 +298,14 @@ class XModule(HTMLSnippet): ...@@ -295,6 +298,14 @@ class XModule(HTMLSnippet):
return "" 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): class XModuleDescriptor(Plugin, HTMLSnippet):
""" """
An XModuleDescriptor is a specification for an element of a course. This An XModuleDescriptor is a specification for an element of a course. This
...@@ -397,6 +408,15 @@ class XModuleDescriptor(Plugin, HTMLSnippet): ...@@ -397,6 +408,15 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
self.url_name.replace('_', ' ')) self.url_name.replace('_', ' '))
@property @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): def own_metadata(self):
""" """
Return the metadata that is not inherited, but was defined on this module. Return the metadata that is not inherited, but was defined on this module.
...@@ -404,6 +424,7 @@ class XModuleDescriptor(Plugin, HTMLSnippet): ...@@ -404,6 +424,7 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
return dict((k,v) for k,v in self.metadata.items() return dict((k,v) for k,v in self.metadata.items()
if k not in self._inherited_metadata) if k not in self._inherited_metadata)
@staticmethod @staticmethod
def compute_inherited_metadata(node): def compute_inherited_metadata(node):
"""Given a descriptor, traverse all of its descendants and do metadata """Given a descriptor, traverse all of its descendants and do metadata
...@@ -596,6 +617,24 @@ class XModuleDescriptor(Plugin, HTMLSnippet): ...@@ -596,6 +617,24 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
metadata=self.metadata metadata=self.metadata
)) ))
# ================================ Internal helpers =======================
def _try_parse_time(self, key):
"""
Parse an optional metadata key containing a time: if present, complain
if it doesn't parse.
Return None if not present or invalid.
"""
if key in self.metadata:
try:
return parse_time(self.metadata[key])
except ValueError as e:
msg = "Descriptor {0} loaded with a bad metadata key '{1}': '{2}'".format(
self.location.url(), self.metadata[key], e)
log.warning(msg)
return None
class DescriptorSystem(object): class DescriptorSystem(object):
def __init__(self, load_item, resources_fs, error_tracker, **kwargs): def __init__(self, load_item, resources_fs, error_tracker, **kwargs):
...@@ -641,16 +680,19 @@ class DescriptorSystem(object): ...@@ -641,16 +680,19 @@ class DescriptorSystem(object):
class XMLParsingSystem(DescriptorSystem): class XMLParsingSystem(DescriptorSystem):
def __init__(self, load_item, resources_fs, error_tracker, process_xml, **kwargs): def __init__(self, load_item, resources_fs, error_tracker, process_xml, policy, **kwargs):
""" """
load_item, resources_fs, error_tracker: see DescriptorSystem load_item, resources_fs, error_tracker: see DescriptorSystem
policy: a policy dictionary for overriding xml metadata
process_xml: Takes an xml string, and returns a XModuleDescriptor process_xml: Takes an xml string, and returns a XModuleDescriptor
created from that xml created from that xml
""" """
DescriptorSystem.__init__(self, load_item, resources_fs, error_tracker, DescriptorSystem.__init__(self, load_item, resources_fs, error_tracker,
**kwargs) **kwargs)
self.process_xml = process_xml self.process_xml = process_xml
self.policy = policy
class ModuleSystem(object): class ModuleSystem(object):
...@@ -675,7 +717,6 @@ class ModuleSystem(object): ...@@ -675,7 +717,6 @@ class ModuleSystem(object):
filestore=None, filestore=None,
debug=False, debug=False,
xqueue=None, xqueue=None,
is_staff=False,
node_path=""): node_path=""):
''' '''
Create a closure around the system environment. Create a closure around the system environment.
...@@ -688,7 +729,8 @@ class ModuleSystem(object): ...@@ -688,7 +729,8 @@ class ModuleSystem(object):
files. Update or remove. files. Update or remove.
get_module - function that takes (location) and returns a corresponding get_module - function that takes (location) and returns a corresponding
module instance object. module instance object. If the current user does not have
access to that location, returns None.
render_template - a function that takes (template_file, context), and render_template - a function that takes (template_file, context), and
returns rendered html. returns rendered html.
...@@ -705,9 +747,6 @@ class ModuleSystem(object): ...@@ -705,9 +747,6 @@ class ModuleSystem(object):
replace_urls - TEMPORARY - A function like static_replace.replace_urls replace_urls - TEMPORARY - A function like static_replace.replace_urls
that capa_module can use to fix up the static urls in that capa_module can use to fix up the static urls in
ajax results. ajax results.
is_staff - Is the user making the request a staff user?
TODO (vshnayder): this will need to change once we have real user roles.
''' '''
self.ajax_url = ajax_url self.ajax_url = ajax_url
self.xqueue = xqueue self.xqueue = xqueue
...@@ -718,7 +757,6 @@ class ModuleSystem(object): ...@@ -718,7 +757,6 @@ class ModuleSystem(object):
self.DEBUG = self.debug = debug self.DEBUG = self.debug = debug
self.seed = user.id if user is not None else 0 self.seed = user.id if user is not None else 0
self.replace_urls = replace_urls self.replace_urls = replace_urls
self.is_staff = is_staff
self.node_path = node_path self.node_path = node_path
def get(self, attr): def get(self, attr):
......
from xmodule.x_module import XModuleDescriptor from xmodule.x_module import (XModuleDescriptor, policy_key)
from xmodule.modulestore import Location from xmodule.modulestore import Location
from lxml import etree from lxml import etree
import json import json
...@@ -166,7 +166,7 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -166,7 +166,7 @@ class XmlDescriptor(XModuleDescriptor):
Subclasses should not need to override this except in special Subclasses should not need to override this except in special
cases (e.g. html module)''' 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) # converted. (note: make sure html files still work once this goes away)
filename = xml_object.get('filename') filename = xml_object.get('filename')
if filename is None: if filename is None:
...@@ -270,6 +270,11 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -270,6 +270,11 @@ class XmlDescriptor(XModuleDescriptor):
log.debug('Error %s in loading metadata %s' % (err,dmdata)) log.debug('Error %s in loading metadata %s' % (err,dmdata))
metadata['definition_metadata_err'] = str(err) metadata['definition_metadata_err'] = str(err)
# Set/override any metadata specified by policy
k = policy_key(location)
if k in system.policy:
metadata.update(system.policy[k])
return cls( return cls(
system, system,
definition, definition,
......
roots/2012_Fall.xml
\ No newline at end of file
<course name="Toy Course" org="edX" course="toy" graceperiod="1 day 5 hours 59 minutes 59 seconds" slug="2012_Fall" start="2015-07-17T12:00">
<chapter name="Overview">
<videosequence format="Lecture Sequence" name="Toy Videos">
<html name="toylab" filename="toylab"/>
<video name="Video Resources" youtube="1.0:1bK-WdDi6Qw"/>
</videosequence>
<video name="Welcome" youtube="1.0:p2Q6BrNhdh8"/>
</chapter>
</course>
<course>
<chapter url_name="Overview">
<videosequence url_name="Toy_Videos">
<html url_name="toylab"/>
<video url_name="Video_Resources" youtube="1.0:1bK-WdDi6Qw"/>
</videosequence>
<video url_name="Welcome" youtube="1.0:p2Q6BrNhdh8"/>
</chapter>
</course>
{
"course/2012_Fall": {
"graceperiod": "2 days 5 hours 59 minutes 59 seconds",
"start": "2015-07-17T12:00",
"display_name": "Toy Course"
},
"chapter/Overview": {
"display_name": "Overview"
},
"videosequence/Toy_Videos": {
"display_name": "Toy Videos",
"format": "Lecture Sequence"
},
"html/toylab": {
"display_name": "Toy lab"
},
"video/Video_Resources": {
"display_name": "Video Resources"
},
"video/Welcome": {
"display_name": "Welcome"
}
}
<course org="edX" course="toy" url_name="2012_Fall"/>
\ No newline at end of file
#!/usr/bin/env python
"""
Victor's xml cleanup script. A big pile of useful hacks. Do not use
without carefully reading the code and deciding that this is what you want.
In particular, the remove-meta option is only intended to be used after pulling out a policy
using the metadata_to_json management command.
"""
import os, fnmatch, re, sys
from lxml import etree
from collections import defaultdict
INVALID_CHARS = re.compile(r"[^\w.-]")
def clean(value):
"""
Return value, made into a form legal for locations
"""
return re.sub('_+', '_', INVALID_CHARS.sub('_', value))
# category -> set of url_names for that category that we've already seen
used_names = defaultdict(set)
def clean_unique(category, name):
cleaned = clean(name)
if cleaned not in used_names[category]:
used_names[category].add(cleaned)
return cleaned
x = 1
while cleaned + str(x) in used_names[category]:
x += 1
# Found one!
cleaned = cleaned + str(x)
used_names[category].add(cleaned)
return cleaned
def cleanup(filepath, remove_meta):
# Keys that are exported to the policy file, and so
# can be removed from the xml afterward
to_remove = ('format', 'display_name',
'graceperiod', 'showanswer', 'rerandomize',
'start', 'due', 'graded', 'hide_from_toc',
'ispublic', 'xqa_key')
try:
print "Cleaning {0}".format(filepath)
with open(filepath) as f:
parser = etree.XMLParser(remove_comments=False)
xml = etree.parse(filepath, parser=parser)
except:
print "Error parsing file {0}".format(filepath)
return
for node in xml.iter(tag=etree.Element):
attrs = node.attrib
if 'url_name' in attrs:
used_names[node.tag].add(attrs['url_name'])
if 'name' in attrs:
# Replace name with an identical display_name, and a unique url_name
name = attrs['name']
attrs['display_name'] = name
attrs['url_name'] = clean_unique(node.tag, name)
del attrs['name']
if 'url_name' in attrs and 'slug' in attrs:
print "WARNING: {0} has both slug and url_name".format(node)
if ('url_name' in attrs and 'filename' in attrs and
len(attrs)==2 and attrs['url_name'] == attrs['filename']):
# This is a pointer tag in disguise. Get rid of the filename.
print 'turning {0}.{1} into a pointer tag'.format(node.tag, attrs['url_name'])
del attrs['filename']
if remove_meta:
for attr in to_remove:
if attr in attrs:
del attrs[attr]
with open(filepath, "w") as f:
f.write(etree.tostring(xml))
def find_replace(directory, filePattern, remove_meta):
for path, dirs, files in os.walk(os.path.abspath(directory)):
for filename in fnmatch.filter(files, filePattern):
filepath = os.path.join(path, filename)
cleanup(filepath, remove_meta)
def main(args):
usage = "xml_cleanup [dir] [remove-meta]"
n = len(args)
if n < 1 or n > 2 or (n == 2 and args[1] != 'remove-meta'):
print usage
return
remove_meta = False
if n == 2:
remove_meta = True
find_replace(args[0], '*.xml', remove_meta)
if __name__ == '__main__':
main(sys.argv[1:])
...@@ -65,3 +65,10 @@ To run a single nose test: ...@@ -65,3 +65,10 @@ To run a single nose test:
nosetests common/lib/xmodule/xmodule/tests/test_stringify.py:test_stringify nosetests common/lib/xmodule/xmodule/tests/test_stringify.py:test_stringify
Very handy: if you uncomment the `--pdb` argument in `NOSE_ARGS` in `lms/envs/test.py`, it will drop you into pdb on error. This lets you go up and down the stack and see what the values of the variables are. Check out http://docs.python.org/library/pdb.html
## Content development
If you change course content, while running the LMS in dev mode, it is unnecessary to restart to refresh the modulestore.
Instead, hit /migrate/modules to see a list of all modules loaded, and click on links (eg /migrate/reload/edx4edx) to reload a course.
This doc is a rough spec of our xml format
Every content element (within a course) should have a unique id. This id is formed as {category}/{url_name}. Categories are the different tag types ('chapter', 'problem', 'html', 'sequential', etc). Url_name is a string containing a-z, A-Z, dot (.) and _. This is what appears in urls that point to this object.
File layout:
- Xml files have content
- "policy", which is also called metadata in various places, should live in a policy file.
- each module (except customtag and course, which are special, see below) should live in a file, located at {category}/{url_name].xml
To include this module in another one (e.g. to put a problem in a vertical), put in a "pointer tag": <{category} url_name="{url_name}"/>. When we read that, we'll load the actual contents.
Customtag is already a pointer, you can just use it in place: <customtag url_name="my_custom_tag" impl="blah" attr1="..."/>
Course tags:
- the top level course pointer tag lives in course.xml
- have 2 extra required attributes: "org" and "course" -- organization name, and course name. Note that the course name is referring to the platonic ideal of this course, not to any particular run of this course. The url_name should be particular run of this course. E.g.
If course.xml contains:
<course org="HarvardX" course="cs50" url_name="2012"/>
we would load the actual course definition from course/2012.xml
To support multiple different runs of the course, you could have a different course.xml, containing
<course org="HarvardX" course="cs50" url_name="2012H"/>
which would load the Harvard-internal version from course/2012H.xml
If there is only one run of the course for now, just have a single course.xml with the right url_name.
If there is more than one run of the course, the different course root pointer files should live in
roots/url_name.xml, and course.xml should be a symbolic link to the one you want to run in your dev instance.
If you want to run both versions, you need to checkout the repo twice, and have course.xml point to different root/{url_name}.xml files.
Policies:
- the policy for a course url_name lives in policies/{url_name}.json
The format is called "json", and is best shown by example (though also feel free to google :)
the file is a dictionary (mapping from keys to values, syntax "{ key : value, key2 : value2, etc}"
Keys are in the form "{category}/{url_name}", which should uniquely id a content element.
Values are dictionaries of the form {"metadata-key" : "metadata-value"}.
metadata can also live in the xml files, but anything defined in the policy file overrides anything in the xml. This is primarily for backwards compatibility, and you should probably not use both. If you do leave some metadata tags in the xml, please be consistent (e.g. if display_names stay in xml, they should all stay in xml).
- note, some xml attributes are not metadata. e.g. in <video youtube="xyz987293487293847"/>, the youtube attribute specifies what video this is, and is logically part of the content, not the policy, so it should stay in video/{url_name}.xml.
Example policy file:
{
"course/2012": {
"graceperiod": "1 day",
"start": "2012-10-15T12:00",
"display_name": "Introduction to Computer Science I",
"xqa_key": "z1y4vdYcy0izkoPeihtPClDxmbY1ogDK"
},
"chapter/Week_0": {
"display_name": "Week 0"
},
"sequential/Pre-Course_Survey": {
"display_name": "Pre-Course Survey",
"format": "Survey"
}
}
NOTE: json is picky about commas. If you have trailing commas before closing braces, it will complain and refuse to parse the file. This is irritating.
Valid tag categories:
abtest
chapter
course
customtag
html
error -- don't put these in by hand :)
problem
problemset
sequential
vertical
video
videosequence
Obsolete tags:
Use customtag instead:
videodev
book
slides
image
discuss
Ex: instead of <book page="12"/>, use <customtag impl="book" page="12"/>
Use something semantic instead, as makes sense: sequential, vertical, videosequence if it's actually a sequence. If the section would only contain a single element, just include that element directly.
section
In general, prefer the most "semantic" name for containers: e.g. use problemset rather than vertical for a problem set. That way, if we decide to display problem sets differently, we don't have to change the xml.
How customtags work:
When we see <customtag impl="special" animal="unicorn" hat="blue"/>, we will:
- look for a file called custom_tags/special in your course dir.
- render it as a mako template, passing parameters {'animal':'unicorn', 'hat':'blue'}, generating html.
METADATA
Metadata that we generally understand:
Only on course tag in courses/url_name.xml
ispublic
xqa_key -- set only on course, inherited to everything else
Everything:
display_name
format (maybe only content containers, e.g. "Lecture sequence", "problem set", "lab", etc. )
start -- modules will not show up to non-course-staff users before the start date (in production)
hide_from_toc -- if this is true, don't show in table of contents for the course. Useful on chapters, and chapter subsections that are linked to from somewhere else.
Used for problems
graceperiod
showanswer
rerandomize
graded
due
These are _inherited_ : if specified on the course, will apply to everything in the course, except for things that explicitly specify them, and their children.
'graded', 'start', 'due', 'graceperiod', 'showanswer', 'rerandomize',
# TODO (ichuang): used for Fall 2012 xqa server access
'xqa_key',
Example sketch:
<course start="tue">
<chap1> -- start tue
<problem> --- start tue
</chap1>
<chap2 start="wed"> -- start wed
<problem2 start="thu"> -- start thu
<problem3> -- start wed
</chap2>
</course>
STATIC LINKS:
if your content links (e.g. in an html file) to "static/blah/ponies.jpg", we will look for this in YOUR_COURSE_DIR/blah/ponies.jpg. Note that this is not looking in a static/ subfolder in your course dir. This may (should?) change at some point.
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
{% spaceless %} {% spaceless %}
<span class="action-link"> <span class="action-link">
<a class="question-delete" id="answer-delete-link-{{answer.id}}"> <a class="question-delete" id="answer-delete-link-{{answer.id}}">
{% if answer.deleted %}{% trans %}undelete{% endtrans %}{% else %}&#10006;{% endif %}</a> {% if answer.deleted %}{% trans %}undelete{% endtrans %}{% else %}delete{% endif %}</a>
</span> </span>
{% endspaceless %} {% endspaceless %}
{% endif %} {% endif %}
......
...@@ -40,6 +40,5 @@ ...@@ -40,6 +40,5 @@
{% endif %} {% endif %}
{% if request.user|can_delete_post(question) %}{{ pipe() }} {% if request.user|can_delete_post(question) %}{{ pipe() }}
<a class="question-delete" id="question-delete-link-{{question.id}}">{% if question.deleted %}{% trans %}undelete{% endtrans %}{% else %}&#10006;{% endif %}</a> <a class="question-delete" id="question-delete-link-{{question.id}}">{% if question.deleted %}{% trans %}undelete{% endtrans %}{% else %}delete{% endif %}</a>
{% endif %} {% endif %}
...@@ -18,11 +18,14 @@ ...@@ -18,11 +18,14 @@
{% include "widgets/system_messages.html" %} {% include "widgets/system_messages.html" %}
{% include "debug_header.html" %} {% include "debug_header.html" %}
{% include "widgets/header.html" %} {# Logo, user tool navigation and meta navitation #} {% include "widgets/header.html" %} {# Logo, user tool navigation and meta navitation #}
{# include "widgets/secondary_header.html" #} {# Scope selector, search input and ask button #}
<section class="container"> <section class="content-wrapper">
{# include "widgets/secondary_header.html" #} {# Scope selector, search input and ask button #}
<section class="container">
{% block body %} {% block body %}
{% endblock %} {% endblock %}
</section>
</section> </section>
{% if settings.FOOTER_MODE == 'default' %} {% if settings.FOOTER_MODE == 'default' %}
......
{% load extra_filters_jinja %} {% load extra_filters_jinja %}
<!--<link href="{{"/style/style.css"|media }}" rel="stylesheet" type="text/css" />-->
{{ 'application' | compressed_css }} {{ 'application' | compressed_css }}
{{ 'course' | compressed_css }} {{ 'course' | compressed_css }}
import re
from urlparse import urlparse
from django.http import Http404
from django.shortcuts import redirect
from wiki.models import reverse as wiki_reverse
from courseware.courses import get_course_with_access
IN_COURSE_WIKI_REGEX = r'/courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/wiki/(?P<wiki_path>.*|)$'
class Middleware(object):
"""
This middleware is to keep the course nav bar above the wiki while
the student clicks around to other wiki pages.
If it intercepts a request for /wiki/.. that has a referrer in the
form /courses/course_id/... it will redirect the user to the page
/courses/course_id/wiki/...
It is also possible that someone followed a link leading to a course
that they don't have access to. In this case, we redirect them to the
same page on the regular wiki.
If we return a redirect, this middleware makes sure that the redirect
keeps the student in the course.
Finally, if the student is in the course viewing a wiki, we change the
reverse() function to resolve wiki urls as a course wiki url by setting
the _transform_url attribute on wiki.models.reverse.
Forgive me Father, for I have hacked.
"""
def __init__(self):
self.redirected = False
def process_request(self, request):
self.redirected = False
wiki_reverse._transform_url = lambda url: url
referer = request.META.get('HTTP_REFERER')
destination = request.path
if request.method == 'GET':
new_destination = self.get_redirected_url(request.user, referer, destination)
if new_destination != destination:
# We mark that we generated this redirection, so we don't modify it again
self.redirected = True
return redirect(new_destination)
course_match = re.match(IN_COURSE_WIKI_REGEX, destination)
if course_match:
course_id = course_match.group('course_id')
prepend_string = '/courses/' + course_match.group('course_id')
wiki_reverse._transform_url = lambda url: prepend_string + url
return None
def process_response(self, request, response):
"""
If this is a redirect response going to /wiki/*, then we might need
to change it to be a redirect going to /courses/*/wiki*.
"""
if not self.redirected and response.status_code == 302: #This is a redirect
referer = request.META.get('HTTP_REFERER')
destination_url = response['LOCATION']
destination = urlparse(destination_url).path
new_destination = self.get_redirected_url(request.user, referer, destination)
if new_destination != destination:
new_url = destination_url.replace(destination, new_destination)
response['LOCATION'] = new_url
return response
def get_redirected_url(self, user, referer, destination):
"""
Returns None if the destination shouldn't be changed.
"""
if not referer:
return destination
referer_path = urlparse(referer).path
path_match = re.match(r'^/wiki/(?P<wiki_path>.*|)$', destination)
if path_match:
# We are going to the wiki. Check if we came from a course
course_match = re.match(r'/courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/.*', referer_path)
if course_match:
course_id = course_match.group('course_id')
# See if we are able to view the course. If we are, redirect to it
try:
course = get_course_with_access(user, course_id, 'load')
return "/courses/" + course.id + "/wiki/" + path_match.group('wiki_path')
except Http404:
# Even though we came from the course, we can't see it. So don't worry about it.
pass
else:
# It is also possible we are going to a course wiki view, but we
# don't have permission to see the course!
course_match = re.match(IN_COURSE_WIKI_REGEX, destination)
if course_match:
course_id = course_match.group('course_id')
# See if we are able to view the course. If we aren't, redirect to regular wiki
try:
course = get_course_with_access(user, course_id, 'load')
# Good, we can see the course. Carry on
return destination
except Http404:
# We can't see the course, so redirect to the regular wiki
return "/wiki/" + course_match.group('wiki_path')
return destination
def context_processor(request):
"""
This is a context processor which looks at the URL while we are
in the wiki. If the url is in the form
/courses/(course_id)/wiki/...
then we add 'course' to the context. This allows the course nav
bar to be shown.
"""
match = re.match(IN_COURSE_WIKI_REGEX, request.path)
if match:
course_id = match.group('course_id')
try:
course = get_course_with_access(request.user, course_id, 'load')
return {'course' : course}
except Http404:
# We couldn't access the course for whatever reason. It is too late to change
# the URL here, so we just leave the course context. The middleware shouldn't
# let this happen
pass
return {}
\ No newline at end of file
from django.core.urlresolvers import reverse
from override_settings import override_settings
import xmodule.modulestore.django
from courseware.tests.tests import PageLoader, TEST_DATA_XML_MODULESTORE
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.xml_importer import import_from_xml
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class WikiRedirectTestCase(PageLoader):
def setUp(self):
xmodule.modulestore.django._MODULESTORES = {}
courses = modulestore().get_courses()
def find_course(name):
"""Assumes the course is present"""
return [c for c in courses if c.location.course==name][0]
self.full = find_course("full")
self.toy = find_course("toy")
# Create two accounts
self.student = 'view@test.com'
self.instructor = 'view2@test.com'
self.password = 'foo'
self.create_account('u1', self.student, self.password)
self.create_account('u2', self.instructor, self.password)
self.activate_user(self.student)
self.activate_user(self.instructor)
def test_wiki_redirect(self):
"""
Test that requesting wiki URLs redirect properly to or out of classes.
An enrolled in student going from /courses/edX/toy/2012_Fall/profile
to /wiki/some/fake/wiki/page/ will redirect to
/courses/edX/toy/2012_Fall/wiki/some/fake/wiki/page/
An unenrolled student going to /courses/edX/toy/2012_Fall/wiki/some/fake/wiki/page/
will be redirected to /wiki/some/fake/wiki/page/
"""
self.login(self.student, self.password)
self.enroll(self.toy)
referer = reverse("profile", kwargs={ 'course_id' : self.toy.id })
destination = reverse("wiki:get", kwargs={'path': 'some/fake/wiki/page/'})
redirected_to = referer.replace("profile", "wiki/some/fake/wiki/page/")
resp = self.client.get( destination, HTTP_REFERER=referer)
self.assertEqual(resp.status_code, 302 )
self.assertEqual(resp['Location'], 'http://testserver' + redirected_to )
# Now we test that the student will be redirected away from that page if the course doesn't exist
# We do this in the same test because we want to make sure the redirected_to is constructed correctly
# This is a location like /courses/*/wiki/* , but with an invalid course ID
bad_course_wiki_page = redirected_to.replace( self.toy.location.course, "bad_course" )
resp = self.client.get( bad_course_wiki_page, HTTP_REFERER=referer)
self.assertEqual(resp.status_code, 302)
self.assertEqual(resp['Location'], 'http://testserver' + destination )
def create_course_page(self, course):
"""
Test that loading the course wiki page creates the wiki page.
The user must be enrolled in the course to see the page.
"""
course_wiki_home = reverse('course_wiki', kwargs={'course_id' : course.id})
referer = reverse("profile", kwargs={ 'course_id' : self.toy.id })
resp = self.client.get(course_wiki_home, follow=True, HTTP_REFERER=referer)
course_wiki_page = referer.replace('profile', 'wiki/' + self.toy.wiki_slug + "/")
ending_location = resp.redirect_chain[-1][0]
ending_status = resp.redirect_chain[-1][1]
self.assertEquals(ending_location, 'http://testserver' + course_wiki_page )
self.assertEquals(resp.status_code, 200)
self.has_course_navigator(resp)
def has_course_navigator(self, resp):
"""
Ensure that the response has the course navigator.
"""
self.assertTrue( "course info" in resp.content.lower() )
self.assertTrue( "courseware" in resp.content.lower() )
def test_course_navigator(self):
""""
Test that going from a course page to a wiki page contains the course navigator.
"""
self.login(self.student, self.password)
self.enroll(self.toy)
self.create_course_page(self.toy)
course_wiki_page = reverse('wiki:get', kwargs={'path' : self.toy.wiki_slug + '/'})
referer = reverse("courseware", kwargs={ 'course_id' : self.toy.id })
resp = self.client.get(course_wiki_page, follow=True, HTTP_REFERER=referer)
self.has_course_navigator(resp)
import logging
import re
from django.conf import settings
from django.contrib.sites.models import Site
from django.core.exceptions import ImproperlyConfigured
from django.shortcuts import redirect
from wiki.core.exceptions import NoRootURL
from wiki.models import URLPath, Article
from courseware.courses import get_course_by_id
log = logging.getLogger(__name__)
def root_create(request):
"""
In the edX wiki, we don't show the root_create view. Instead, we
just create the root automatically if it doesn't exist.
"""
root = get_or_create_root()
return redirect('wiki:get', path=root.path)
def course_wiki_redirect(request, course_id):
"""
This redirects to whatever page on the wiki that the course designates
as it's home page. A course's wiki must be an article on the root (for
example, "/6.002x") to keep things simple.
"""
course = get_course_by_id(course_id)
course_slug = course.wiki_slug
valid_slug = True
if not course_slug:
log.exception("This course is improperly configured. The slug cannot be empty.")
valid_slug = False
if re.match('^[-\w\.]+$', course_slug) == None:
log.exception("This course is improperly configured. The slug can only contain letters, numbers, periods or hyphens.")
valid_slug = False
if not valid_slug:
return redirect("wiki:get", path="")
# The wiki needs a Site object created. We make sure it exists here
try:
site = Site.objects.get_current()
except Site.DoesNotExist:
new_site = Site()
new_site.domain = settings.SITE_NAME
new_site.name = "edX"
new_site.save()
if str(new_site.id) != str(settings.SITE_ID):
raise ImproperlyConfigured("No site object was created and the SITE_ID doesn't match the newly created one. " + str(new_site.id) + "!=" + str(settings.SITE_ID))
try:
urlpath = URLPath.get_by_path(course_slug, select_related=True)
results = list( Article.objects.filter( id = urlpath.article.id ) )
if results:
article = results[0]
else:
article = None
except (NoRootURL, URLPath.DoesNotExist):
# We will create it in the next block
urlpath = None
article = None
if not article:
# create it
root = get_or_create_root()
if urlpath:
# Somehow we got a urlpath without an article. Just delete it and
# recerate it.
urlpath.delete()
urlpath = URLPath.create_article(
root,
course_slug,
title=course.title,
content="This is the wiki for " + course.title + ".",
user_message="Course page automatically created.",
user=None,
ip_address=None,
article_kwargs={'owner': None,
'group': None,
'group_read': True,
'group_write': True,
'other_read': True,
'other_write': True,
})
return redirect("wiki:get", path=urlpath.path)
def get_or_create_root():
"""
Returns the root article, or creates it if it doesn't exist.
"""
try:
root = URLPath.root()
if not root.article:
root.delete()
raise NoRootURL
return root
except NoRootURL:
pass
starting_content = "\n".join((
"Welcome to the edX Wiki",
"===",
"Visit a course wiki to add an article."))
root = URLPath.create_root(title="edX Wiki",
content=starting_content)
article = root.article
article.group = None
article.group_read = True
article.group_write = False
article.other_read = True
article.other_write = False
article.save()
return root
...@@ -2,8 +2,8 @@ from collections import defaultdict ...@@ -2,8 +2,8 @@ from collections import defaultdict
from fs.errors import ResourceNotFoundError from fs.errors import ResourceNotFoundError
from functools import wraps from functools import wraps
import logging import logging
from path import path
from path import path
from django.conf import settings from django.conf import settings
from django.http import Http404 from django.http import Http404
...@@ -12,49 +12,51 @@ from xmodule.modulestore import Location ...@@ -12,49 +12,51 @@ from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from static_replace import replace_urls, try_staticfiles_lookup from static_replace import replace_urls, try_staticfiles_lookup
from courseware.access import has_access
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def check_course(user, course_id, course_must_be_open=True, course_required=True): def get_course_by_id(course_id):
""" """
Given a django user and a course_id, this returns the course Given a course id, return the corresponding course descriptor.
object. By default, if the course is not found or the course is
not open yet, this method will raise a 404.
If course_must_be_open is False, the course will be returned
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 course_id is not valid, raises a 404.
if dark launch is enabled, course_must_be_open is ignored for
users that have staff access.
""" """
course = None try:
if course_required or course_id: course_loc = CourseDescriptor.id_to_location(course_id)
try: return modulestore().get_item(course_loc)
course_loc = CourseDescriptor.id_to_location(course_id) except (KeyError, ItemNotFoundError):
course = modulestore().get_item(course_loc) raise Http404("Course not found.")
except (KeyError, ItemNotFoundError):
raise Http404("Course not found.")
started = course.has_started() or settings.MITX_FEATURES['DISABLE_START_DATES']
must_be_open = course_must_be_open
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: def get_course_with_access(user, course_id, action):
raise Http404("This course has not yet started.") """
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 return course
def get_opt_course_with_access(user, course_id, action):
"""
Same as get_course_with_access, except that if course_id is None,
return None without performing any access checks.
"""
if course_id is None:
return None
return get_course_with_access(user, course_id, action)
def course_image_url(course): def course_image_url(course):
"""Try to look up the image url for the course. If it's not found, """Try to look up the image url for the course. If it's not found,
log an error and return the dead link""" log an error and return the dead link"""
...@@ -140,77 +142,32 @@ def get_course_info_section(course, section_key): ...@@ -140,77 +142,32 @@ def get_course_info_section(course, section_key):
raise KeyError("Invalid about key " + str(section_key)) raise KeyError("Invalid about key " + str(section_key))
def course_staff_group_name(course):
'''
course should be either a CourseDescriptor instance, or a string (the
.course entry of a Location)
'''
if isinstance(course, str) or isinstance(course, unicode):
coursename = course
else:
# should be a CourseDescriptor, so grab its location.course:
coursename = course.location.course
return 'staff_%s' % coursename
def has_staff_access_to_course(user, course):
'''
Returns True if the given user has staff access to the course.
This means that user is in the staff_* group, or is an overall admin.
TODO (vshnayder): this needs to be changed to allow per-course_id permissions, not per-course
(e.g. staff in 2012 is different from 2013, but maybe some people always have access)
course is the course field of the location being accessed. def get_courses_by_university(user, domain=None):
'''
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). Returns dict of lists of courses available, keyed by course.org (ie university).
Courses are sorted by course.number. Courses are sorted by course.number.
if ACCESS_REQUIRE_STAFF_FOR_COURSE then list only includes those accessible
to user.
''' '''
# TODO: Clean up how 'error' is done. # TODO: Clean up how 'error' is done.
# filter out any courses that errored. # filter out any courses that errored.
courses = [c for c in modulestore().get_courses() courses = [c for c in modulestore().get_courses()
if isinstance(c, CourseDescriptor)] if isinstance(c, CourseDescriptor)]
courses = sorted(courses, key=lambda course: course.number) 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) universities = defaultdict(list)
for course in courses: for course in courses:
if settings.MITX_FEATURES.get('ACCESS_REQUIRE_STAFF_FOR_COURSE'): if not has_access(user, course, 'see_exists'):
if not has_access_to_course(user,course): continue
continue if course.id not in visible_courses:
continue
universities[course.org].append(course) universities[course.org].append(course)
return universities return universities
...@@ -63,7 +63,12 @@ def grade(student, request, course, student_module_cache=None): ...@@ -63,7 +63,12 @@ def grade(student, request, course, student_module_cache=None):
scores = [] scores = []
# TODO: We need the request to pass into here. If we could forgo that, our arguments # TODO: We need the request to pass into here. If we could forgo that, our arguments
# would be simpler # would be simpler
section_module = get_module(student, request, section_descriptor.location, student_module_cache) section_module = get_module(student, request,
section_descriptor.location, student_module_cache)
if section_module is None:
# student doesn't have access to this module, or something else
# went wrong.
continue
# TODO: We may be able to speed this up by only getting a list of children IDs from section_module # TODO: We may be able to speed this up by only getting a list of children IDs from section_module
# Then, we may not need to instatiate any problems if they are already in the database # Then, we may not need to instatiate any problems if they are already in the database
......
"""
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: {0}\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)
...@@ -67,7 +67,7 @@ class StudentModuleCache(object): ...@@ -67,7 +67,7 @@ class StudentModuleCache(object):
""" """
A cache of StudentModules for a specific student A cache of StudentModules for a specific student
""" """
def __init__(self, user, descriptors): def __init__(self, user, descriptors, select_for_update=False):
''' '''
Find any StudentModule objects that are needed by any descriptor Find any StudentModule objects that are needed by any descriptor
in descriptors. Avoids making multiple queries to the database. in descriptors. Avoids making multiple queries to the database.
...@@ -77,6 +77,7 @@ class StudentModuleCache(object): ...@@ -77,6 +77,7 @@ class StudentModuleCache(object):
Arguments Arguments
user: The user for which to fetch maching StudentModules user: The user for which to fetch maching StudentModules
descriptors: An array of XModuleDescriptors. descriptors: An array of XModuleDescriptors.
select_for_update: Flag indicating whether the rows should be locked until end of transaction
''' '''
if user.is_authenticated(): if user.is_authenticated():
module_ids = self._get_module_state_keys(descriptors) module_ids = self._get_module_state_keys(descriptors)
...@@ -86,23 +87,30 @@ class StudentModuleCache(object): ...@@ -86,23 +87,30 @@ class StudentModuleCache(object):
self.cache = [] self.cache = []
chunk_size = 500 chunk_size = 500
for id_chunk in [module_ids[i:i + chunk_size] for i in xrange(0, len(module_ids), chunk_size)]: for id_chunk in [module_ids[i:i + chunk_size] for i in xrange(0, len(module_ids), chunk_size)]:
self.cache.extend(StudentModule.objects.filter( if select_for_update:
student=user, self.cache.extend(StudentModule.objects.select_for_update().filter(
module_state_key__in=id_chunk) student=user,
) module_state_key__in=id_chunk)
)
else:
self.cache.extend(StudentModule.objects.filter(
student=user,
module_state_key__in=id_chunk)
)
else: else:
self.cache = [] self.cache = []
@classmethod @classmethod
def cache_for_descriptor_descendents(cls, user, descriptor, depth=None, descriptor_filter=lambda descriptor: True): def cache_for_descriptor_descendents(cls, user, descriptor, depth=None, descriptor_filter=lambda descriptor: True, select_for_update=False):
""" """
descriptor: An XModuleDescriptor descriptor: An XModuleDescriptor
depth is the number of levels of descendent modules to load StudentModules for, in addition to depth is the number of levels of descendent modules to load StudentModules for, in addition to
the supplied descriptor. If depth is None, load all descendent StudentModules the supplied descriptor. If depth is None, load all descendent StudentModules
descriptor_filter is a function that accepts a descriptor and return wether the StudentModule descriptor_filter is a function that accepts a descriptor and return wether the StudentModule
should be cached should be cached
select_for_update: Flag indicating whether the rows should be locked until end of transaction
""" """
def get_child_descriptors(descriptor, depth, descriptor_filter): def get_child_descriptors(descriptor, depth, descriptor_filter):
...@@ -122,7 +130,7 @@ class StudentModuleCache(object): ...@@ -122,7 +130,7 @@ class StudentModuleCache(object):
descriptors = get_child_descriptors(descriptor, depth, descriptor_filter) descriptors = get_child_descriptors(descriptor, depth, descriptor_filter)
return StudentModuleCache(user, descriptors) return StudentModuleCache(user, descriptors, select_for_update)
def _get_module_state_keys(self, descriptors): def _get_module_state_keys(self, descriptors):
''' '''
......
...@@ -2,24 +2,24 @@ import json ...@@ -2,24 +2,24 @@ import json
import logging import logging
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.http import Http404 from django.http import Http404
from django.http import HttpResponse from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from requests.auth import HTTPBasicAuth
from capa.xqueue_interface import XQueueInterface from capa.xqueue_interface import XQueueInterface
from django.contrib.auth.models import User from courseware.access import has_access
from xmodule.modulestore.django import modulestore
from mitxmako.shortcuts import render_to_string from mitxmako.shortcuts import render_to_string
from models import StudentModule, StudentModuleCache from models import StudentModule, StudentModuleCache
from static_replace import replace_urls from static_replace import replace_urls
from xmodule.exceptions import NotFoundError from xmodule.exceptions import NotFoundError
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.x_module import ModuleSystem from xmodule.x_module import ModuleSystem
from xmodule_modifiers import replace_static_urls, add_histogram, wrap_xmodule from xmodule_modifiers import replace_course_urls, 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
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
...@@ -48,7 +48,7 @@ def make_track_function(request): ...@@ -48,7 +48,7 @@ def make_track_function(request):
return f 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 Create a table of contents from the module store
...@@ -65,13 +65,20 @@ def toc_for_course(user, request, course, active_chapter, active_section): ...@@ -65,13 +65,20 @@ def toc_for_course(user, request, course, active_chapter, active_section):
Everything else comes from the xml, or defaults to "". Everything else comes from the xml, or defaults to "".
chapters with name 'hidden' are skipped. chapters with name 'hidden' are skipped.
NOTE: assumes that if we got this far, user has access to course. Returns
None if this is not the case.
''' '''
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(user, course, depth=2) student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(user, course, depth=2)
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() chapters = list()
for chapter in course.get_display_items(): for chapter in course.get_display_items():
hide_from_toc = chapter.metadata.get('hide_from_toc','false').lower() == 'true'
if hide_from_toc:
continue
sections = list() sections = list()
for section in chapter.get_display_items(): for section in chapter.get_display_items():
...@@ -124,7 +131,7 @@ def get_section(course_module, chapter, section): ...@@ -124,7 +131,7 @@ def get_section(course_module, chapter, section):
return section_module 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, ''' Get an instance of the xmodule class identified by location,
setting the state based on an existing StudentModule, or creating one if none setting the state based on an existing StudentModule, or creating one if none
exists. exists.
...@@ -141,6 +148,18 @@ def get_module(user, request, location, student_module_cache, position=None): ...@@ -141,6 +148,18 @@ def get_module(user, request, location, student_module_cache, position=None):
''' '''
descriptor = modulestore().get_item(location) 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 #TODO Only check the cache if this module can possibly have state
instance_module = None instance_module = None
...@@ -156,25 +175,20 @@ def get_module(user, request, location, student_module_cache, position=None): ...@@ -156,25 +175,20 @@ def get_module(user, request, location, student_module_cache, position=None):
shared_state_key) shared_state_key)
instance_state = instance_module.state if instance_module is not None else None
instance_state = instance_module.state if instance_module is not None else '{}'
shared_state = shared_module.state if shared_module is not None else None shared_state = shared_module.state if shared_module is not None else None
# TODO (vshnayder): fix hardcoded urls (use reverse)
# Setup system context for module instance # Setup system context for module instance
ajax_url = reverse('modx_dispatch',
ajax_url = reverse('modx_dispatch', kwargs=dict(course_id=course_id,
kwargs=dict(course_id=descriptor.location.course_id,
id=descriptor.location.url(), id=descriptor.location.url(),
dispatch=''), dispatch=''),
) )
# ajax_url = settings.MITX_ROOT_URL + '/modx/' + descriptor.location.url() + '/'
# Fully qualified callback URL for external queueing system # Fully qualified callback URL for external queueing system
xqueue_callback_url = request.build_absolute_uri('/')[:-1] # Trailing slash provided by reverse xqueue_callback_url = request.build_absolute_uri('/')[:-1] # Trailing slash provided by reverse
xqueue_callback_url += reverse('xqueue_callback', xqueue_callback_url += reverse('xqueue_callback',
kwargs=dict(course_id=descriptor.location.course_id, kwargs=dict(course_id=course_id,
userid=str(user.id), userid=str(user.id),
id=descriptor.location.url(), id=descriptor.location.url(),
dispatch='score_update'), dispatch='score_update'),
...@@ -190,8 +204,11 @@ def get_module(user, request, location, student_module_cache, position=None): ...@@ -190,8 +204,11 @@ def get_module(user, request, location, student_module_cache, position=None):
'default_queuename': xqueue_default_queuename.replace(' ', '_')} 'default_queuename': xqueue_default_queuename.replace(' ', '_')}
def _get_module(location): def _get_module(location):
"""
Delegate to get_module. It does an access check, so may return None
"""
return get_module(user, request, location, return get_module(user, request, location,
student_module_cache, position) student_module_cache, position, course_id=course_id)
# TODO (cpennington): When modules are shared between courses, the static # TODO (cpennington): When modules are shared between courses, the static
# prefix is going to have to be specific to the module, not the directory # prefix is going to have to be specific to the module, not the directory
...@@ -208,12 +225,11 @@ def get_module(user, request, location, student_module_cache, position=None): ...@@ -208,12 +225,11 @@ def get_module(user, request, location, student_module_cache, position=None):
# a module is coming through get_html and is therefore covered # a module is coming through get_html and is therefore covered
# by the replace_static_urls code below # by the replace_static_urls code below
replace_urls=replace_urls, replace_urls=replace_urls,
is_staff=has_staff_access_to_location(user, location),
node_path=settings.NODE_PATH node_path=settings.NODE_PATH
) )
# pass position specified in URL to module through ModuleSystem # pass position specified in URL to module through ModuleSystem
system.set('position', position) system.set('position', position)
system.set('DEBUG',settings.DEBUG) system.set('DEBUG', settings.DEBUG)
module = descriptor.xmodule_constructor(system)(instance_state, shared_state) module = descriptor.xmodule_constructor(system)(instance_state, shared_state)
...@@ -222,8 +238,12 @@ def get_module(user, request, location, student_module_cache, position=None): ...@@ -222,8 +238,12 @@ def get_module(user, request, location, student_module_cache, position=None):
module.metadata['data_dir'], module 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 settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF'):
if has_staff_access_to_course(user, module.location.course): if has_access(user, module, 'staff'):
module.get_html = add_histogram(module.get_html, module, user) module.get_html = add_histogram(module.get_html, module, user)
return module return module
...@@ -304,12 +324,17 @@ def xqueue_callback(request, course_id, userid, id, dispatch): ...@@ -304,12 +324,17 @@ def xqueue_callback(request, course_id, userid, id, dispatch):
# Retrieve target StudentModule # Retrieve target StudentModule
user = User.objects.get(id=userid) user = User.objects.get(id=userid)
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(user, modulestore().get_item(id)) student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
user, modulestore().get_item(id), depth=0, select_for_update=True)
instance = get_module(user, request, id, student_module_cache) instance = get_module(user, request, id, student_module_cache)
if instance is None:
log.debug("No module {0} for user {1}--access denied?".format(id, user))
raise Http404
instance_module = get_instance_module(user, instance, student_module_cache) instance_module = get_instance_module(user, instance, student_module_cache)
if instance_module is None: if instance_module is None:
log.debug("Couldn't find module '%s' for user '%s'", id, user) log.debug("Couldn't find instance of module '%s' for user '%s'", id, user)
raise Http404 raise Http404
oldgrade = instance_module.grade oldgrade = instance_module.grade
...@@ -362,7 +387,12 @@ def modx_dispatch(request, dispatch=None, id=None, course_id=None): ...@@ -362,7 +387,12 @@ def modx_dispatch(request, dispatch=None, id=None, course_id=None):
p[inputfile_id] = inputfile p[inputfile_id] = inputfile
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(request.user, modulestore().get_item(id)) student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(request.user, modulestore().get_item(id))
instance = get_module(request.user, request, id, student_module_cache) instance = get_module(request.user, request, id, student_module_cache, 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 {0} for user {1}--access denied?".format(id, user))
raise Http404
instance_module = get_instance_module(request.user, instance, student_module_cache) instance_module = get_instance_module(request.user, instance, student_module_cache)
shared_module = get_shared_instance_module(request.user, instance, student_module_cache) shared_module = get_shared_instance_module(request.user, instance, student_module_cache)
......
...@@ -18,12 +18,14 @@ from override_settings import override_settings ...@@ -18,12 +18,14 @@ from override_settings import override_settings
import xmodule.modulestore.django import xmodule.modulestore.django
# Need access to internal func to put users in the right group
from courseware.access import _course_staff_group_name
from student.models import Registration from student.models import Registration
from courseware.courses import course_staff_group_name
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.xml_importer import import_from_xml from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.timeparse import stringify_time
def parse_json(response): def parse_json(response):
"""Parse response, which is assumed to be json""" """Parse response, which is assumed to be json"""
...@@ -54,6 +56,7 @@ def mongo_store_config(data_dir): ...@@ -54,6 +56,7 @@ def mongo_store_config(data_dir):
'db': 'xmodule', 'db': 'xmodule',
'collection': 'modulestore', 'collection': 'modulestore',
'fs_root': data_dir, 'fs_root': data_dir,
'render_template': 'mitxmako.shortcuts.render_to_string',
} }
} }
} }
...@@ -174,7 +177,7 @@ class PageLoader(ActivateLoginTestCase): ...@@ -174,7 +177,7 @@ class PageLoader(ActivateLoginTestCase):
def try_enroll(self, course): def try_enroll(self, course):
"""Try to enroll. Return bool success instead of asserting it.""" """Try to enroll. Return bool success instead of asserting it."""
data = self._enroll(course) data = self._enroll(course)
print 'Enrollment in {} result: {}'.format(course.location.url(), data) print 'Enrollment in {0} result: {1}'.format(course.location.url(), data)
return data['success'] return data['success']
def enroll(self, course): def enroll(self, course):
...@@ -185,7 +188,7 @@ class PageLoader(ActivateLoginTestCase): ...@@ -185,7 +188,7 @@ class PageLoader(ActivateLoginTestCase):
def unenroll(self, course): def unenroll(self, course):
"""Unenroll the currently logged-in user, and check that it worked.""" """Unenroll the currently logged-in user, and check that it worked."""
resp = self.client.post('/change_enrollment', { resp = self.client.post('/change_enrollment', {
'enrollment_action': 'enroll', 'enrollment_action': 'unenroll',
'course_id': course.id, 'course_id': course.id,
}) })
data = parse_json(resp) data = parse_json(resp)
...@@ -306,11 +309,11 @@ class TestViewAuth(PageLoader): ...@@ -306,11 +309,11 @@ class TestViewAuth(PageLoader):
# shouldn't be able to get to the instructor pages # shouldn't be able to get to the instructor pages
for url in instructor_urls(self.toy) + instructor_urls(self.full): for url in instructor_urls(self.toy) + instructor_urls(self.full):
print 'checking for 404 on {}'.format(url) print 'checking for 404 on {0}'.format(url)
self.check_for_get_code(404, url) self.check_for_get_code(404, url)
# Make the instructor staff in the toy course # Make the instructor staff in the toy course
group_name = course_staff_group_name(self.toy) group_name = _course_staff_group_name(self.toy.location)
g = Group.objects.create(name=group_name) g = Group.objects.create(name=group_name)
g.user_set.add(user(self.instructor)) g.user_set.add(user(self.instructor))
...@@ -319,11 +322,11 @@ class TestViewAuth(PageLoader): ...@@ -319,11 +322,11 @@ class TestViewAuth(PageLoader):
# Now should be able to get to the toy course, but not the full course # Now should be able to get to the toy course, but not the full course
for url in instructor_urls(self.toy): for url in instructor_urls(self.toy):
print 'checking for 200 on {}'.format(url) print 'checking for 200 on {0}'.format(url)
self.check_for_get_code(200, url) self.check_for_get_code(200, url)
for url in instructor_urls(self.full): for url in instructor_urls(self.full):
print 'checking for 404 on {}'.format(url) print 'checking for 404 on {0}'.format(url)
self.check_for_get_code(404, url) self.check_for_get_code(404, url)
...@@ -334,31 +337,28 @@ class TestViewAuth(PageLoader): ...@@ -334,31 +337,28 @@ class TestViewAuth(PageLoader):
# and now should be able to load both # and now should be able to load both
for url in instructor_urls(self.toy) + instructor_urls(self.full): for url in instructor_urls(self.toy) + instructor_urls(self.full):
print 'checking for 200 on {}'.format(url) print 'checking for 200 on {0}'.format(url)
self.check_for_get_code(200, url) self.check_for_get_code(200, url)
def run_wrapped(self, test): def run_wrapped(self, test):
""" """
test.py turns off start dates. Enable them and DARK_LAUNCH. test.py turns off start dates. Enable them.
Because settings is global, be careful not to mess it up for other tests Because settings is global, be careful not to mess it up for other tests
(Can't use override_settings because we're only changing part of the (Can't use override_settings because we're only changing part of the
MITX_FEATURES dict) MITX_FEATURES dict)
""" """
oldDSD = settings.MITX_FEATURES['DISABLE_START_DATES'] oldDSD = settings.MITX_FEATURES['DISABLE_START_DATES']
oldDL = settings.MITX_FEATURES['DARK_LAUNCH']
try: try:
settings.MITX_FEATURES['DISABLE_START_DATES'] = False settings.MITX_FEATURES['DISABLE_START_DATES'] = False
settings.MITX_FEATURES['DARK_LAUNCH'] = True
test() test()
finally: finally:
settings.MITX_FEATURES['DISABLE_START_DATES'] = oldDSD settings.MITX_FEATURES['DISABLE_START_DATES'] = oldDSD
settings.MITX_FEATURES['DARK_LAUNCH'] = oldDL
def test_dark_launch(self): def test_dark_launch(self):
"""Make sure that when dark launch is on, students can't access course """Make sure that before course start, students can't access course
pages, but instructors can""" pages, but instructors can"""
self.run_wrapped(self._do_test_dark_launch) self.run_wrapped(self._do_test_dark_launch)
...@@ -372,13 +372,12 @@ class TestViewAuth(PageLoader): ...@@ -372,13 +372,12 @@ class TestViewAuth(PageLoader):
# Make courses start in the future # Make courses start in the future
tomorrow = time.time() + 24*3600 tomorrow = time.time() + 24*3600
self.toy.start = self.toy.metadata['start'] = time.gmtime(tomorrow) self.toy.metadata['start'] = stringify_time(time.gmtime(tomorrow))
self.full.start = self.full.metadata['start'] = time.gmtime(tomorrow) self.full.metadata['start'] = stringify_time(time.gmtime(tomorrow))
self.assertFalse(self.toy.has_started()) self.assertFalse(self.toy.has_started())
self.assertFalse(self.full.has_started()) self.assertFalse(self.full.has_started())
self.assertFalse(settings.MITX_FEATURES['DISABLE_START_DATES']) self.assertFalse(settings.MITX_FEATURES['DISABLE_START_DATES'])
self.assertTrue(settings.MITX_FEATURES['DARK_LAUNCH'])
def reverse_urls(names, course): def reverse_urls(names, course):
"""Reverse a list of course urls""" """Reverse a list of course urls"""
...@@ -389,7 +388,11 @@ class TestViewAuth(PageLoader): ...@@ -389,7 +388,11 @@ class TestViewAuth(PageLoader):
list of urls that students should be able to see only list of urls that students should be able to see only
after launch, but staff should see before after launch, but staff should see before
""" """
urls = reverse_urls(['info', 'book', 'courseware', 'profile'], course) urls = reverse_urls(['info', 'courseware', 'profile'], course)
urls.extend([
reverse('book', kwargs={'course_id': course.id, 'book_index': book.title})
for book in course.textbooks
])
return urls return urls
def light_student_urls(course): def light_student_urls(course):
...@@ -414,22 +417,22 @@ class TestViewAuth(PageLoader): ...@@ -414,22 +417,22 @@ class TestViewAuth(PageLoader):
def check_non_staff(course): def check_non_staff(course):
"""Check that access is right for non-staff in course""" """Check that access is right for non-staff in course"""
print '=== Checking non-staff access for {}'.format(course.id) print '=== Checking non-staff access for {0}'.format(course.id)
for url in instructor_urls(course) + dark_student_urls(course): for url in instructor_urls(course) + dark_student_urls(course):
print 'checking for 404 on {}'.format(url) print 'checking for 404 on {0}'.format(url)
self.check_for_get_code(404, url) self.check_for_get_code(404, url)
for url in light_student_urls(course): for url in light_student_urls(course):
print 'checking for 200 on {}'.format(url) print 'checking for 200 on {0}'.format(url)
self.check_for_get_code(200, url) self.check_for_get_code(200, url)
def check_staff(course): def check_staff(course):
"""Check that access is right for staff in course""" """Check that access is right for staff in course"""
print '=== Checking staff access for {}'.format(course.id) print '=== Checking staff access for {0}'.format(course.id)
for url in (instructor_urls(course) + for url in (instructor_urls(course) +
dark_student_urls(course) + dark_student_urls(course) +
light_student_urls(course)): light_student_urls(course)):
print 'checking for 200 on {}'.format(url) print 'checking for 200 on {0}'.format(url)
self.check_for_get_code(200, url) self.check_for_get_code(200, url)
# First, try with an enrolled student # First, try with an enrolled student
...@@ -444,7 +447,7 @@ class TestViewAuth(PageLoader): ...@@ -444,7 +447,7 @@ class TestViewAuth(PageLoader):
print '=== Testing course instructor access....' print '=== Testing course instructor access....'
# Make the instructor staff in the toy course # Make the instructor staff in the toy course
group_name = course_staff_group_name(self.toy) group_name = _course_staff_group_name(self.toy.location)
g = Group.objects.create(name=group_name) g = Group.objects.create(name=group_name)
g.user_set.add(user(self.instructor)) g.user_set.add(user(self.instructor))
...@@ -494,7 +497,7 @@ class TestViewAuth(PageLoader): ...@@ -494,7 +497,7 @@ class TestViewAuth(PageLoader):
print '=== Testing course instructor access....' print '=== Testing course instructor access....'
# Make the instructor staff in the toy course # Make the instructor staff in the toy course
group_name = course_staff_group_name(self.toy) group_name = _course_staff_group_name(self.toy.location)
g = Group.objects.create(name=group_name) g = Group.objects.create(name=group_name)
g.user_set.add(user(self.instructor)) g.user_set.add(user(self.instructor))
......
...@@ -19,7 +19,8 @@ from django.conf import settings ...@@ -19,7 +19,8 @@ from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from mitxmako.shortcuts import render_to_response, render_to_string from mitxmako.shortcuts import render_to_response, render_to_string
from courseware.courses import check_course from courseware.courses import get_course_with_access
from django_comment_client.utils import JsonResponse, JsonError, extract from django_comment_client.utils import JsonResponse, JsonError, extract
...@@ -309,7 +310,7 @@ def update_moderator_status(request, course_id, user_id): ...@@ -309,7 +310,7 @@ def update_moderator_status(request, course_id, user_id):
else: else:
user.roles.remove(role) user.roles.remove(role)
if request.is_ajax(): if request.is_ajax():
course = check_course(request.user, course_id) course = get_course_with_access(request.user, course_id, 'load')
discussion_user = cc.User(id=user_id, course_id=course_id) discussion_user = cc.User(id=user_id, course_id=course_id)
context = { context = {
'course': course, 'course': course,
......
...@@ -7,7 +7,7 @@ from django.core.urlresolvers import reverse ...@@ -7,7 +7,7 @@ from django.core.urlresolvers import reverse
from django.contrib.auth.models import User from django.contrib.auth.models import User
from mitxmako.shortcuts import render_to_response, render_to_string from mitxmako.shortcuts import render_to_response, render_to_string
from courseware.courses import check_course from courseware.courses import get_course_with_access
from dateutil.tz import tzlocal from dateutil.tz import tzlocal
from datehelper import time_ago_in_words from datehelper import time_ago_in_words
...@@ -129,7 +129,7 @@ def render_search_bar(request, course_id, discussion_id=None, text=''): ...@@ -129,7 +129,7 @@ def render_search_bar(request, course_id, discussion_id=None, text=''):
return render_to_string('discussion/_search_bar.html', context) return render_to_string('discussion/_search_bar.html', context)
def forum_form_discussion(request, course_id, discussion_id): def forum_form_discussion(request, course_id, discussion_id):
course = check_course(request.user, course_id) course = get_course_with_access(request.user, course_id, 'load')
threads, query_params = get_threads(request, course_id, discussion_id) threads, query_params = get_threads(request, course_id, discussion_id)
content = render_forum_discussion(request, course_id, threads, discussion_id=discussion_id, \ content = render_forum_discussion(request, course_id, threads, discussion_id=discussion_id, \
query_params=query_params) query_params=query_params)
...@@ -191,7 +191,7 @@ def single_thread(request, course_id, discussion_id, thread_id): ...@@ -191,7 +191,7 @@ def single_thread(request, course_id, discussion_id, thread_id):
}) })
else: else:
course = check_course(request.user, course_id) course = get_course_with_access(request.user, course_id, 'load')
context = { context = {
'discussion_id': discussion_id, 'discussion_id': discussion_id,
...@@ -207,7 +207,7 @@ def single_thread(request, course_id, discussion_id, thread_id): ...@@ -207,7 +207,7 @@ def single_thread(request, course_id, discussion_id, thread_id):
def user_profile(request, course_id, user_id): def user_profile(request, course_id, user_id):
course = check_course(request.user, course_id) course = get_course_with_access(request.user, course_id, 'load')
discussion_user = cc.User(id=user_id, course_id=course_id) discussion_user = cc.User(id=user_id, course_id=course_id)
query_params = { query_params = {
......
...@@ -35,7 +35,7 @@ def manage_modulestores(request,reload_dir=None): ...@@ -35,7 +35,7 @@ def manage_modulestores(request,reload_dir=None):
ip = request.META.get('HTTP_X_REAL_IP','') # nginx reverse proxy ip = request.META.get('HTTP_X_REAL_IP','') # nginx reverse proxy
if not ip: if not ip:
ip = request.META.get('REMOTE_ADDR','None') ip = request.META.get('REMOTE_ADDR','None')
if LOCAL_DEBUG: if LOCAL_DEBUG:
html += '<h3>IP address: %s ' % ip html += '<h3>IP address: %s ' % ip
html += '<h3>User: %s ' % request.user html += '<h3>User: %s ' % request.user
...@@ -48,7 +48,7 @@ def manage_modulestores(request,reload_dir=None): ...@@ -48,7 +48,7 @@ def manage_modulestores(request,reload_dir=None):
html += 'Permission denied' html += 'Permission denied'
html += "</body></html>" html += "</body></html>"
log.debug('request denied, ALLOWED_IPS=%s' % ALLOWED_IPS) log.debug('request denied, ALLOWED_IPS=%s' % ALLOWED_IPS)
return HttpResponse(html) return HttpResponse(html)
#---------------------------------------- #----------------------------------------
# reload course if specified # reload course if specified
...@@ -74,10 +74,10 @@ def manage_modulestores(request,reload_dir=None): ...@@ -74,10 +74,10 @@ def manage_modulestores(request,reload_dir=None):
#---------------------------------------- #----------------------------------------
dumpfields = ['definition','location','metadata'] dumpfields = ['definition','location','metadata']
for cdir, course in def_ms.courses.items(): for cdir, course in def_ms.courses.items():
html += '<hr width="100%"/>' html += '<hr width="100%"/>'
html += '<h2>Course: %s (%s)</h2>' % (course.metadata['display_name'],cdir) html += '<h2>Course: %s (%s)</h2>' % (course.display_name,cdir)
for field in dumpfields: for field in dumpfields:
data = getattr(course,field) data = getattr(course,field)
...@@ -89,7 +89,7 @@ def manage_modulestores(request,reload_dir=None): ...@@ -89,7 +89,7 @@ def manage_modulestores(request,reload_dir=None):
html += '</ul>' html += '</ul>'
else: else:
html += '<ul><li>%s</li></ul>' % escape(data) html += '<ul><li>%s</li></ul>' % escape(data)
#---------------------------------------- #----------------------------------------
...@@ -107,4 +107,4 @@ def manage_modulestores(request,reload_dir=None): ...@@ -107,4 +107,4 @@ def manage_modulestores(request,reload_dir=None):
log.debug('def_ms=%s' % unicode(def_ms)) log.debug('def_ms=%s' % unicode(def_ms))
html += "</body></html>" html += "</body></html>"
return HttpResponse(html) return HttpResponse(html)
...@@ -9,7 +9,8 @@ from django.utils import simplejson ...@@ -9,7 +9,8 @@ from django.utils import simplejson
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from mitxmako.shortcuts import render_to_response from mitxmako.shortcuts import render_to_response
from courseware.courses import check_course from courseware.courses import get_opt_course_with_access
from courseware.access import has_access
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
...@@ -49,9 +50,13 @@ def update_template_dictionary(dictionary, request=None, course=None, article=No ...@@ -49,9 +50,13 @@ def update_template_dictionary(dictionary, request=None, course=None, article=No
if request: if request:
dictionary.update(csrf(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): def view(request, article_path, course_id=None):
course = check_course(request.user, course_id, course_required=False) course = get_opt_course_with_access(request.user, course_id, 'load')
(article, err) = get_article(request, article_path, course) (article, err) = get_article(request, article_path, course)
if err: if err:
...@@ -67,7 +72,7 @@ def view(request, article_path, course_id=None): ...@@ -67,7 +72,7 @@ def view(request, article_path, course_id=None):
def view_revision(request, revision_number, article_path, course_id=None): def view_revision(request, revision_number, article_path, course_id=None):
course = check_course(request.user, course_id, course_required=False) course = get_opt_course_with_access(request.user, course_id, 'load')
(article, err) = get_article(request, article_path, course) (article, err) = get_article(request, article_path, course)
if err: if err:
...@@ -91,7 +96,7 @@ def view_revision(request, revision_number, article_path, course_id=None): ...@@ -91,7 +96,7 @@ def view_revision(request, revision_number, article_path, course_id=None):
def root_redirect(request, course_id=None): def root_redirect(request, course_id=None):
course = check_course(request.user, course_id, course_required=False) course = get_opt_course_with_access(request.user, course_id, 'load')
#TODO: Add a default namespace to settings. #TODO: Add a default namespace to settings.
namespace = course.wiki_namespace if course else "edX" namespace = course.wiki_namespace if course else "edX"
...@@ -109,7 +114,7 @@ def root_redirect(request, course_id=None): ...@@ -109,7 +114,7 @@ def root_redirect(request, course_id=None):
def create(request, article_path, course_id=None): def create(request, article_path, course_id=None):
course = check_course(request.user, course_id, course_required=False) course = get_opt_course_with_access(request.user, course_id, 'load')
article_path_components = article_path.split('/') article_path_components = article_path.split('/')
...@@ -170,7 +175,7 @@ def create(request, article_path, course_id=None): ...@@ -170,7 +175,7 @@ def create(request, article_path, course_id=None):
def edit(request, article_path, course_id=None): def edit(request, article_path, course_id=None):
course = check_course(request.user, course_id, course_required=False) course = get_opt_course_with_access(request.user, course_id, 'load')
(article, err) = get_article(request, article_path, course) (article, err) = get_article(request, article_path, course)
if err: if err:
...@@ -218,7 +223,7 @@ def edit(request, article_path, course_id=None): ...@@ -218,7 +223,7 @@ def edit(request, article_path, course_id=None):
def history(request, article_path, page=1, course_id=None): def history(request, article_path, page=1, course_id=None):
course = check_course(request.user, course_id, course_required=False) course = get_opt_course_with_access(request.user, course_id, 'load')
(article, err) = get_article(request, article_path, course) (article, err) = get_article(request, article_path, course)
if err: if err:
...@@ -300,7 +305,7 @@ def history(request, article_path, page=1, course_id=None): ...@@ -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): def revision_feed(request, page=1, namespace=None, course_id=None):
course = check_course(request.user, course_id, course_required=False) course = get_opt_course_with_access(request.user, course_id, 'load')
page_size = 10 page_size = 10
...@@ -333,7 +338,7 @@ def revision_feed(request, page=1, namespace=None, course_id=None): ...@@ -333,7 +338,7 @@ def revision_feed(request, page=1, namespace=None, course_id=None):
def search_articles(request, namespace=None, course_id=None): def search_articles(request, namespace=None, course_id=None):
course = check_course(request.user, course_id, course_required=False) course = get_opt_course_with_access(request.user, course_id, 'load')
# blampe: We should check for the presence of other popular django search # blampe: We should check for the presence of other popular django search
# apps and use those if possible. Only fall back on this as a last resort. # apps and use those if possible. Only fall back on this as a last resort.
...@@ -382,7 +387,7 @@ def search_articles(request, namespace=None, course_id=None): ...@@ -382,7 +387,7 @@ def search_articles(request, namespace=None, course_id=None):
def search_add_related(request, course_id, slug, namespace): def search_add_related(request, course_id, slug, namespace):
course = check_course(request.user, course_id, course_required=False) course = get_opt_course_with_access(request.user, course_id, 'load')
(article, err) = get_article(request, slug, namespace if namespace else course_id) (article, err) = get_article(request, slug, namespace if namespace else course_id)
if err: if err:
...@@ -415,7 +420,7 @@ def search_add_related(request, course_id, slug, namespace): ...@@ -415,7 +420,7 @@ def search_add_related(request, course_id, slug, namespace):
def add_related(request, course_id, slug, namespace): def add_related(request, course_id, slug, namespace):
course = check_course(request.user, course_id, course_required=False) course = get_opt_course_with_access(request.user, course_id, 'load')
(article, err) = get_article(request, slug, namespace if namespace else course_id) (article, err) = get_article(request, slug, namespace if namespace else course_id)
if err: if err:
...@@ -439,7 +444,7 @@ def add_related(request, course_id, slug, namespace): ...@@ -439,7 +444,7 @@ def add_related(request, course_id, slug, namespace):
def remove_related(request, course_id, namespace, slug, related_id): def remove_related(request, course_id, namespace, slug, related_id):
course = check_course(request.user, course_id, course_required=False) course = get_opt_course_with_access(request.user, course_id, 'load')
(article, err) = get_article(request, slug, namespace if namespace else course_id) (article, err) = get_article(request, slug, namespace if namespace else course_id)
...@@ -462,7 +467,7 @@ def remove_related(request, course_id, namespace, slug, related_id): ...@@ -462,7 +467,7 @@ def remove_related(request, course_id, namespace, slug, related_id):
def random_article(request, course_id=None): def random_article(request, course_id=None):
course = check_course(request.user, course_id, course_required=False) course = get_opt_course_with_access(request.user, course_id, 'load')
from random import randint from random import randint
num_arts = Article.objects.count() num_arts = Article.objects.count()
......
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from mitxmako.shortcuts import render_to_response from mitxmako.shortcuts import render_to_response
from courseware.courses import check_course from courseware.access import has_access
from courseware.courses import get_course_with_access
from lxml import etree from lxml import etree
@login_required @login_required
def index(request, course_id, page=0): def index(request, course_id, book_index, 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 staff_access = has_access(request.user, course, 'staff')
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})
textbook = course.textbooks[int(book_index)]
table_of_contents = textbook.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): def index_shifted(request, course_id, page):
return index(request, course_id=course_id, page=int(page) + 24) return index(request, course_id=course_id, page=int(page) + 24)
...@@ -56,3 +56,7 @@ AWS_SECRET_ACCESS_KEY = AUTH_TOKENS["AWS_SECRET_ACCESS_KEY"] ...@@ -56,3 +56,7 @@ AWS_SECRET_ACCESS_KEY = AUTH_TOKENS["AWS_SECRET_ACCESS_KEY"]
DATABASES = AUTH_TOKENS['DATABASES'] DATABASES = AUTH_TOKENS['DATABASES']
XQUEUE_INTERFACE = AUTH_TOKENS['XQUEUE_INTERFACE'] XQUEUE_INTERFACE = AUTH_TOKENS['XQUEUE_INTERFACE']
if 'COURSE_ID' in ENV_TOKENS:
ASKBOT_URL = "courses/{0}/discussions/".format(ENV_TOKENS['COURSE_ID'])
...@@ -50,7 +50,11 @@ MITX_FEATURES = { ...@@ -50,7 +50,11 @@ MITX_FEATURES = {
## DO NOT SET TO True IN THIS FILE ## DO NOT SET TO True IN THIS FILE
## Doing so will cause all courses to be released on production ## Doing so will cause all courses to be released on production
'DISABLE_START_DATES': False, # When True, all courses will be active, regardless of start date 'DISABLE_START_DATES': False, # When True, all courses will be active, regardless of start date
'DARK_LAUNCH': False, # When True, courses will be active for staff only
# 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_TEXTBOOK' : True,
'ENABLE_DISCUSSION' : True, 'ENABLE_DISCUSSION' : True,
...@@ -65,6 +69,7 @@ MITX_FEATURES = { ...@@ -65,6 +69,7 @@ MITX_FEATURES = {
'ACCESS_REQUIRE_STAFF_FOR_COURSE': False, 'ACCESS_REQUIRE_STAFF_FOR_COURSE': False,
'AUTH_USE_OPENID': False, 'AUTH_USE_OPENID': False,
'AUTH_USE_MIT_CERTIFICATES' : False, 'AUTH_USE_MIT_CERTIFICATES' : False,
} }
# Used for A/B testing # Used for A/B testing
...@@ -98,12 +103,12 @@ system_node_path = os.environ.get("NODE_PATH", None) ...@@ -98,12 +103,12 @@ system_node_path = os.environ.get("NODE_PATH", None)
if system_node_path is None: if system_node_path is None:
system_node_path = "/usr/local/lib/node_modules" system_node_path = "/usr/local/lib/node_modules"
node_paths = [COMMON_ROOT / "static/js/vendor", node_paths = [COMMON_ROOT / "static/js/vendor",
COMMON_ROOT / "static/coffee/src", COMMON_ROOT / "static/coffee/src",
system_node_path system_node_path
] ]
NODE_PATH = ':'.join(node_paths) NODE_PATH = ':'.join(node_paths)
################################## MITXWEB ##################################### ################################## MITXWEB #####################################
# This is where we stick our compiled template files. Most of the app uses Mako # This is where we stick our compiled template files. Most of the app uses Mako
# templates # templates
...@@ -129,6 +134,13 @@ TEMPLATE_CONTEXT_PROCESSORS = ( ...@@ -129,6 +134,13 @@ TEMPLATE_CONTEXT_PROCESSORS = (
'askbot.user_messages.context_processors.user_messages',#must be before auth 'askbot.user_messages.context_processors.user_messages',#must be before auth
'django.contrib.auth.context_processors.auth', #this is required for admin 'django.contrib.auth.context_processors.auth', #this is required for admin
'django.core.context_processors.csrf', #necessary for csrf protection 'django.core.context_processors.csrf', #necessary for csrf protection
# Added for django-wiki
'django.core.context_processors.media',
'django.core.context_processors.tz',
'django.contrib.messages.context_processors.messages',
'sekizai.context_processors.sekizai',
'course_wiki.course_nav.context_processor',
) )
STUDENT_FILEUPLOAD_MAX_SIZE = 4*1000*1000 # 4 MB STUDENT_FILEUPLOAD_MAX_SIZE = 4*1000*1000 # 4 MB
...@@ -286,6 +298,9 @@ djcelery.setup_loader() ...@@ -286,6 +298,9 @@ djcelery.setup_loader()
SIMPLE_WIKI_REQUIRE_LOGIN_EDIT = True SIMPLE_WIKI_REQUIRE_LOGIN_EDIT = True
SIMPLE_WIKI_REQUIRE_LOGIN_VIEW = False SIMPLE_WIKI_REQUIRE_LOGIN_VIEW = False
################################# WIKI ###################################
WIKI_ACCOUNT_HANDLING = False
################################# Jasmine ################################### ################################# Jasmine ###################################
JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee' JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee'
...@@ -299,9 +314,13 @@ STATICFILES_FINDERS = ( ...@@ -299,9 +314,13 @@ STATICFILES_FINDERS = (
# List of callables that know how to import templates from various sources. # List of callables that know how to import templates from various sources.
TEMPLATE_LOADERS = ( TEMPLATE_LOADERS = (
'django.template.loaders.filesystem.Loader', 'mitxmako.makoloader.MakoFilesystemLoader',
'django.template.loaders.app_directories.Loader', 'mitxmako.makoloader.MakoAppDirectoriesLoader',
'askbot.skins.loaders.filesystem_load_template_source',
# 'django.template.loaders.filesystem.Loader',
# 'django.template.loaders.app_directories.Loader',
#'askbot.skins.loaders.filesystem_load_template_source',
# 'django.template.loaders.eggs.Loader', # 'django.template.loaders.eggs.Loader',
) )
...@@ -318,6 +337,8 @@ MIDDLEWARE_CLASSES = ( ...@@ -318,6 +337,8 @@ MIDDLEWARE_CLASSES = (
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'track.middleware.TrackMiddleware', 'track.middleware.TrackMiddleware',
'mitxmako.middleware.MakoMiddleware', 'mitxmako.middleware.MakoMiddleware',
'course_wiki.course_nav.Middleware',
'askbot.middleware.anon_user.ConnectToSessionMessagesMiddleware', 'askbot.middleware.anon_user.ConnectToSessionMessagesMiddleware',
'askbot.middleware.forum_mode.ForumModeMiddleware', 'askbot.middleware.forum_mode.ForumModeMiddleware',
...@@ -536,6 +557,15 @@ INSTALLED_APPS = ( ...@@ -536,6 +557,15 @@ INSTALLED_APPS = (
'track', 'track',
'util', 'util',
'certificates', 'certificates',
#For the wiki
'wiki', # The new django-wiki from benjaoming
'course_wiki', # Our customizations
'django_notify',
'mptt',
'sekizai',
'wiki.plugins.attachments',
'wiki.plugins.notifications',
# For testing # For testing
'django_jasmine', 'django_jasmine',
......
...@@ -54,7 +54,7 @@ CACHES = { ...@@ -54,7 +54,7 @@ CACHES = {
} }
XQUEUE_INTERFACE = { XQUEUE_INTERFACE = {
"url": "http://xqueue.sandbox.edx.org", "url": "https://sandbox-xqueue.edx.org",
"django_auth": { "django_auth": {
"username": "lms", "username": "lms",
"password": "***REMOVED***" "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']
}
...@@ -14,6 +14,7 @@ MODULESTORE = { ...@@ -14,6 +14,7 @@ MODULESTORE = {
'db': 'xmodule', 'db': 'xmodule',
'collection': 'modulestore', 'collection': 'modulestore',
'fs_root': GITHUB_REPO_ROOT, 'fs_root': GITHUB_REPO_ROOT,
'render_template': 'mitxmako.shortcuts.render_to_string',
} }
} }
} }
...@@ -65,5 +65,7 @@ DEBUG_TOOLBAR_PANELS = ( ...@@ -65,5 +65,7 @@ DEBUG_TOOLBAR_PANELS = (
# Django=1.3.1/1.4 where requests to views get duplicated (your method gets # Django=1.3.1/1.4 where requests to views get duplicated (your method gets
# hit twice). So you can uncomment when you need to diagnose performance # hit twice). So you can uncomment when you need to diagnose performance
# problems, but you shouldn't leave it on. # problems, but you shouldn't leave it on.
# 'debug_toolbar.panels.profiling.ProfilingDebugPanel', 'debug_toolbar.panels.profiling.ProfilingDebugPanel',
) )
#PIPELINE = True
...@@ -51,7 +51,7 @@ GITHUB_REPO_ROOT = ENV_ROOT / "data" ...@@ -51,7 +51,7 @@ GITHUB_REPO_ROOT = ENV_ROOT / "data"
XQUEUE_INTERFACE = { XQUEUE_INTERFACE = {
"url": "http://xqueue.sandbox.edx.org", "url": "http://sandbox-xqueue.edx.org",
"django_auth": { "django_auth": {
"username": "lms", "username": "lms",
"password": "***REMOVED***" "password": "***REMOVED***"
......
lms/static/images/askbot/vote-arrow-up.png

200 Bytes | W: | H:

lms/static/images/askbot/vote-arrow-up.png

1.11 KB | W: | H:

lms/static/images/askbot/vote-arrow-up.png
lms/static/images/askbot/vote-arrow-up.png
lms/static/images/askbot/vote-arrow-up.png
lms/static/images/askbot/vote-arrow-up.png
  • 2-up
  • Swipe
  • Onion skin
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