Commit 3ffcb261 by Tom Giannattasio

fix merge conflicts

parents a1ff3457 e89a7b67
...@@ -6,3 +6,4 @@ gfortran ...@@ -6,3 +6,4 @@ gfortran
python python
yuicompressor yuicompressor
node node
graphviz
...@@ -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',
} }
} }
} }
......
...@@ -12,7 +12,6 @@ $bright-blue: #3c8ebf; ...@@ -12,7 +12,6 @@ $bright-blue: #3c8ebf;
$orange: #f96e5b; $orange: #f96e5b;
$yellow: #fff8af; $yellow: #fff8af;
$cream: #F6EFD4; $cream: #F6EFD4;
$mit-red: #933;
$border-color: #ddd; $border-color: #ddd;
@mixin hide-text { @mixin hide-text {
......
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))
import logging
from django.conf import settings
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
import mitxmako.middleware
log = logging.getLogger(__name__)
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
module_directory = getattr(settings, 'MAKO_MODULE_DIR', None)
if module_directory is None:
log.warning("For more caching of mako templates, set the MAKO_MODULE_DIR in settings!")
module_directory = tempfile.mkdtemp()
self.module_directory = module_directory
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, file_path = self.load_template_source(template_name, template_dirs)
if source.startswith("## mako\n"):
# This is a mako template
template = Template(filename=file_path, module_directory=self.module_directory, uri=template_name)
return template, None
else:
# This is a regular template
origin = make_origin(file_path, 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, file_path
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,47 @@ ...@@ -12,18 +12,47 @@
# 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',
'module_directory', 'encoding_errors']
django_variables = ['lookup', 'output_encoding', '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
......
...@@ -279,7 +279,8 @@ def replicate_enrollment_save(sender, **kwargs): ...@@ -279,7 +279,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)
...@@ -101,7 +102,7 @@ def main_index(extra_context = {}, user=None): ...@@ -101,7 +102,7 @@ def main_index(extra_context = {}, user=None):
def course_from_id(course_id): def course_from_id(course_id):
"""Return the CourseDescriptor corresponding to this course_id""" """Return the CourseDescriptor corresponding to this course_id"""
course_loc = CourseDescriptor.id_to_location(course_id) course_loc = CourseDescriptor.id_to_location(course_id)
return modulestore().get_item(course_loc) return modulestore().get_instance(course_id, course_loc)
def press(request): def press(request):
...@@ -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
...@@ -1117,11 +1119,6 @@ class CodeResponse(LoncapaResponse): ...@@ -1117,11 +1119,6 @@ class CodeResponse(LoncapaResponse):
(err, self.answer_id, convert_files_to_filenames(student_answers))) (err, self.answer_id, convert_files_to_filenames(student_answers)))
raise Exception(err) raise Exception(err)
if is_file(submission):
self.context.update({'submission': submission.name})
else:
self.context.update({'submission': submission})
# Prepare xqueue request # Prepare xqueue request
#------------------------------------------------------------ #------------------------------------------------------------
qinterface = self.system.xqueue['interface'] qinterface = self.system.xqueue['interface']
...@@ -1133,14 +1130,19 @@ class CodeResponse(LoncapaResponse): ...@@ -1133,14 +1130,19 @@ class CodeResponse(LoncapaResponse):
queue_name=self.queue_name) queue_name=self.queue_name)
# Generate body # Generate body
if is_list_of_files(submission):
self.context.update({'submission': queuekey}) # For tracking. TODO: May want to record something else here
else:
self.context.update({'submission': submission})
contents = self.payload.copy() contents = self.payload.copy()
# Submit request. When successful, 'msg' is the prior length of the queue # Submit request. When successful, 'msg' is the prior length of the queue
if is_file(submission): if is_list_of_files(submission):
contents.update({'student_response': submission.name}) contents.update({'student_response': ''}) # TODO: Is there any information we want to send here?
(error, msg) = qinterface.send_to_queue(header=xheader, (error, msg) = qinterface.send_to_queue(header=xheader,
body=json.dumps(contents), body=json.dumps(contents),
file_to_upload=submission) files_to_upload=submission)
else: else:
contents.update({'student_response': submission}) contents.update({'student_response': submission})
(error, msg) = qinterface.send_to_queue(header=xheader, (error, msg) = qinterface.send_to_queue(header=xheader,
...@@ -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')
......
<section id="filesubmission_${id}" class="filesubmission"> <section id="filesubmission_${id}" class="filesubmission">
<input type="file" name="input_${id}" id="input_${id}" value="${value}" /><br /> <input type="file" name="input_${id}" id="input_${id}" value="${value}" multiple="multiple" /><br />
% if state == 'unsubmitted': % if state == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span> <span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
% elif state == 'correct': % elif state == 'correct':
......
...@@ -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>
...@@ -39,12 +39,16 @@ def convert_files_to_filenames(answers): ...@@ -39,12 +39,16 @@ def convert_files_to_filenames(answers):
''' '''
new_answers = dict() new_answers = dict()
for answer_id in answers.keys(): for answer_id in answers.keys():
if is_file(answers[answer_id]): answer = answers[answer_id]
new_answers[answer_id] = answers[answer_id].name if is_list_of_files(answer): # Files are stored as a list, even if one file
new_answers[answer_id] = [f.name for f in answer]
else: else:
new_answers[answer_id] = answers[answer_id] new_answers[answer_id] = answers[answer_id]
return new_answers return new_answers
def is_list_of_files(files):
return isinstance(files, list) and all(is_file(f) for f in files)
def is_file(file_to_test): def is_file(file_to_test):
''' '''
Duck typing to check if 'file_to_test' is a File object Duck typing to check if 'file_to_test' is a File object
......
...@@ -65,7 +65,7 @@ class XQueueInterface(object): ...@@ -65,7 +65,7 @@ class XQueueInterface(object):
self.auth = django_auth self.auth = django_auth
self.session = requests.session(auth=requests_auth) self.session = requests.session(auth=requests_auth)
def send_to_queue(self, header, body, file_to_upload=None): def send_to_queue(self, header, body, files_to_upload=None):
''' '''
Submit a request to xqueue. Submit a request to xqueue.
...@@ -74,16 +74,19 @@ class XQueueInterface(object): ...@@ -74,16 +74,19 @@ class XQueueInterface(object):
body: Serialized data for the receipient behind the queueing service. The operation of body: Serialized data for the receipient behind the queueing service. The operation of
xqueue is agnostic to the contents of 'body' xqueue is agnostic to the contents of 'body'
file_to_upload: File object to be uploaded to xqueue along with queue request files_to_upload: List of file objects to be uploaded to xqueue along with queue request
Returns (error_code, msg) where error_code != 0 indicates an error Returns (error_code, msg) where error_code != 0 indicates an error
''' '''
# Attempt to send to queue # Attempt to send to queue
(error, msg) = self._send_to_queue(header, body, file_to_upload) (error, msg) = self._send_to_queue(header, body, files_to_upload)
if error and (msg == 'login_required'): # Log in, then try again if error and (msg == 'login_required'): # Log in, then try again
self._login() self._login()
(error, msg) = self._send_to_queue(header, body, file_to_upload) if files_to_upload is not None:
for f in files_to_upload: # Need to rewind file pointers
f.seek(0)
(error, msg) = self._send_to_queue(header, body, files_to_upload)
return (error, msg) return (error, msg)
...@@ -94,13 +97,15 @@ class XQueueInterface(object): ...@@ -94,13 +97,15 @@ class XQueueInterface(object):
return self._http_post(self.url+'/xqueue/login/', payload) return self._http_post(self.url+'/xqueue/login/', payload)
def _send_to_queue(self, header, body, file_to_upload=None): def _send_to_queue(self, header, body, files_to_upload):
payload = {'xqueue_header': header, payload = {'xqueue_header': header,
'xqueue_body' : body} 'xqueue_body' : body}
files = None files = {}
if file_to_upload is not None: if files_to_upload is not None:
files = { file_to_upload.name: file_to_upload } for f in files_to_upload:
return self._http_post(self.url+'/xqueue/submit/', payload, files) files.update({ f.name: f })
return self._http_post(self.url+'/xqueue/submit/', payload, files=files)
def _http_post(self, url, data, files=None): def _http_post(self, url, data, files=None):
......
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
import requests
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, book_url):
self.title = title
self.book_url = book_url
self.table_of_contents = self._get_toc_from_s3()
@classmethod
def from_xml_object(cls, xml_object):
return cls(xml_object.get('title'), xml_object.get('book_url'))
@property
def table_of_contents(self):
return self.table_of_contents
def _get_toc_from_s3(self):
'''
Accesses the textbook's table of contents (default name "toc.xml") at the URL self.book_url
Returns XML tree representation of the table of contents
'''
toc_url = self.book_url + 'toc.xml'
# Get the table of contents from S3
log.info("Retrieving textbook table of contents from %s" % toc_url)
try:
r = requests.get(toc_url)
except Exception as err:
msg = 'Error %s: Unable to retrieve textbook table of contents at %s' % (err, toc_url)
log.error(msg)
raise Exception(msg)
# TOC is XML. Parse it
try:
table_of_contents = etree.fromstring(r.text)
except Exception as err:
msg = 'Error %s: Unable to parse XML for textbook table of contents at %s' % (err, toc_url)
log.error(msg)
raise Exception(msg)
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 +107,6 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -76,7 +107,6 @@ class CourseDescriptor(SequenceDescriptor):
return grading_policy return grading_policy
@lazyproperty @lazyproperty
def grading_context(self): def grading_context(self):
""" """
...@@ -132,6 +162,10 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -132,6 +162,10 @@ class CourseDescriptor(SequenceDescriptor):
@staticmethod @staticmethod
def make_id(org, course, url_name):
return '/'.join([org, course, url_name])
@staticmethod
def id_to_location(course_id): def id_to_location(course_id):
'''Convert the given course_id (org/course/name) to a location object. '''Convert the given course_id (org/course/name) to a location object.
Throws ValueError if course_id is of the wrong format. Throws ValueError if course_id is of the wrong format.
...@@ -154,6 +188,7 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -154,6 +188,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 +197,14 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -162,14 +197,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
......
h2 { h2 {
margin-top: 0; margin-top: 0;
margin-bottom: 15px; margin-bottom: 15px;
width: flex-grid(2, 9);
padding-right: flex-gutter(9);
border-right: 1px dashed #ddd;
@include box-sizing(border-box);
display: table-cell;
vertical-align: top;
&.problem-header {
section.staff {
margin-top: 30px;
font-size: 80%;
}
}
@media screen and (max-width:1120px) { &.problem-header {
display: block; section.staff {
width: auto; margin-top: 30px;
border-right: 0; font-size: 80%;
} }
}
@media print { @media print {
display: block; display: block;
width: auto; width: auto;
border-right: 0; border-right: 0;
} }
} }
section.problem { section.problem {
display: table-cell; @media print {
width: flex-grid(7, 9); display: block;
padding-left: flex-gutter(9); width: auto;
padding: 0;
@media screen and (max-width:1120px) {
display: block;
width: auto;
padding: 0;
}
@media print {
display: block;
width: auto;
padding: 0;
canvas, img { canvas, img {
page-break-inside: avoid; page-break-inside: avoid;
}
} }
}
div { div {
p.status { p {
text-indent: -9999px; &.answer {
margin: -1px 0 0 10px; margin-top: -2px;
} }
&.status {
text-indent: -9999px;
margin: 8px 0 0 10px;
}
}
&.unanswered { &.unanswered {
p.status { p.status {
@include inline-block(); @include inline-block();
background: url('../images/unanswered-icon.png') center center no-repeat; background: url('../images/unanswered-icon.png') center center no-repeat;
height: 14px; height: 14px;
width: 14px; width: 14px;
}
} }
}
&.processing { &.processing {
p.status { p.status {
@include inline-block(); @include inline-block();
background: url('../images/spinner.gif') center center no-repeat; background: url('../images/spinner.gif') center center no-repeat;
height: 20px; height: 20px;
width: 20px; width: 20px;
text-indent: -9999px; text-indent: -9999px;
}
} }
}
&.correct, &.ui-icon-check { &.correct, &.ui-icon-check {
p.status { p.status {
@include inline-block(); @include inline-block();
background: url('../images/correct-icon.png') center center no-repeat; background: url('../images/correct-icon.png') center center no-repeat;
height: 20px; height: 20px;
width: 25px; width: 25px;
}
input {
border-color: green;
}
} }
input { &.processing {
border-color: green; p.status {
@include inline-block();
background: url('../images/spinner.gif') center center no-repeat;
height: 20px;
width: 20px;
}
input {
border-color: #aaa;
}
} }
}
&.processing { &.incorrect, &.ui-icon-close {
p.status { p.status {
@include inline-block(); @include inline-block();
background: url('../images/spinner.gif') center center no-repeat; background: url('../images/incorrect-icon.png') center center no-repeat;
height: 20px; height: 20px;
width: 20px; width: 20px;
text-indent: -9999px;
}
input {
border-color: red;
}
} }
input { > span {
border-color: #aaa; display: block;
margin-bottom: lh(.5);
} }
}
&.incorrect, &.ui-icon-close { p.answer {
p.status {
@include inline-block(); @include inline-block();
background: url('../images/incorrect-icon.png') center center no-repeat; margin-bottom: 0;
height: 20px; margin-left: 10px;
width: 20px;
text-indent: -9999px;
}
input { &:before {
border-color: red; content: "Answer: ";
font-weight: bold;
display: inline;
}
&:empty {
&:before {
display: none;
}
}
} }
}
> span { div.equation {
display: block; clear: both;
margin-bottom: lh(.5); padding: 6px;
} background: #eee;
p.answer { span {
@include inline-block(); margin-bottom: 0;
margin-bottom: 0; }
margin-left: 10px; }
&:before { span {
content: "Answer: "; &.unanswered, &.ui-icon-bullet {
font-weight: bold; @include inline-block();
display: inline; background: url('../images/unanswered-icon.png') center center no-repeat;
height: 14px;
position: relative;
top: 4px;
width: 14px;
}
} &.processing, &.ui-icon-processing {
&:empty { @include inline-block();
&:before { background: url('../images/spinner.gif') center center no-repeat;
display: none; height: 20px;
position: relative;
top: 6px;
width: 25px;
} }
}
}
div.equation { &.correct, &.ui-icon-check {
clear: both; @include inline-block();
padding: 6px; background: url('../images/correct-icon.png') center center no-repeat;
background: #eee; height: 20px;
position: relative;
top: 6px;
width: 25px;
}
span { &.incorrect, &.ui-icon-close {
margin-bottom: 0; @include inline-block();
background: url('../images/incorrect-icon.png') center center no-repeat;
height: 20px;
width: 20px;
position: relative;
top: 6px;
}
} }
} }
span { ul {
&.unanswered, &.ui-icon-bullet { list-style: disc outside none;
@include inline-block(); margin-bottom: lh();
background: url('../images/unanswered-icon.png') center center no-repeat; margin-left: .75em;
height: 14px; margin-left: .75rem;
position: relative; }
top: 4px;
width: 14px;
}
&.processing, &.ui-icon-processing { ol {
@include inline-block(); list-style: decimal outside none;
background: url('../images/spinner.gif') center center no-repeat; margin-bottom: lh();
height: 20px; margin-left: .75em;
position: relative; margin-left: .75rem;
top: 6px; }
width: 25px;
}
&.correct, &.ui-icon-check { dl {
@include inline-block(); line-height: 1.4em;
background: url('../images/correct-icon.png') center center no-repeat; }
height: 20px;
position: relative;
top: 6px;
width: 25px;
}
&.incorrect, &.ui-icon-close { dl dt {
@include inline-block(); font-weight: bold;
background: url('../images/incorrect-icon.png') center center no-repeat;
height: 20px;
width: 20px;
position: relative;
top: 6px;
}
} }
}
ul { dl dd {
list-style: disc outside none; margin-bottom: 0;
margin-bottom: lh(); }
margin-left: .75em;
margin-left: .75rem;
}
ol { dd {
list-style: decimal outside none; margin-left: .5em;
margin-bottom: lh(); margin-left: .5rem;
margin-left: .75em; }
margin-left: .75rem;
}
dl { li {
line-height: 1.4em; line-height: 1.4em;
} margin-bottom: lh(.5);
dl dt { &:last-child {
font-weight: bold; margin-bottom: 0;
} }
}
dl dd { p {
margin-bottom: 0; margin-bottom: lh();
} }
dd { table {
margin-left: .5em; margin-bottom: lh();
margin-left: .5rem; border-collapse: collapse;
} table-layout: auto;
li { th {
line-height: 1.4em; font-weight: bold;
margin-bottom: lh(.5); text-align: left;
}
&:last-child { caption, th, td {
margin-bottom: 0; padding: .25em .75em .25em 0;
} padding: .25rem .75rem .25rem 0;
} }
p { caption {
margin-bottom: lh(); background: #f1f1f1;
} margin-bottom: .75em;
margin-bottom: .75rem;
padding: .75em 0;
padding: .75rem 0;
}
table { tr, td, th {
margin-bottom: lh(); vertical-align: middle;
width: 100%; }
// border: 1px solid #ddd;
border-collapse: collapse;
th {
// border-bottom: 2px solid #ccc;
font-weight: bold;
text-align: left;
} }
td { hr {
// border: 1px solid #ddd; background: #ddd;
border: none;
clear: both;
color: #ddd;
float: none;
height: 1px;
margin: 0 0 .75rem;
width: 100%;
} }
caption, th, td { .hidden {
padding: .25em .75em .25em 0; display: none;
padding: .25rem .75rem .25rem 0; visibility: hidden;
} }
caption { #{$all-text-inputs} {
background: #f1f1f1; display: inline;
margin-bottom: .75em; width: auto;
margin-bottom: .75rem;
padding: .75em 0;
padding: .75rem 0;
} }
tr, td, th { // this supports a deprecated element and should be removed once the center tag is removed
vertical-align: middle; center {
display: block;
margin: lh() 0;
border: 1px solid #ccc;
padding: lh();
} }
} section.action {
input.save {
hr { @extend .blue-button;
background: #ddd; }
border: none; }
clear: both;
color: #ddd;
float: none;
height: 1px;
margin: 0 0 .75rem;
width: 100%;
}
.hidden {
display: none;
visibility: hidden;
}
#{$all-text-inputs} {
display: inline;
width: auto;
}
// this supports a deprecated element and should be removed once the center tag is removed
center {
display: block;
margin: lh() 0;
border: 1px solid #ccc;
padding: lh();
}
} }
...@@ -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 {
...@@ -305,11 +305,11 @@ div.video { ...@@ -305,11 +305,11 @@ div.video {
@include box-shadow(0 1px 0 #333); @include box-shadow(0 1px 0 #333);
a.ui-slider-handle { a.ui-slider-handle {
background: $mit-red url(../images/slider-handle.png) center center no-repeat; background: $pink url(../images/slider-handle.png) center center no-repeat;
@include background-size(50%); @include background-size(50%);
border: 1px solid darken($mit-red, 20%); border: 1px solid darken($pink, 20%);
@include border-radius(15px); @include border-radius(15px);
@include box-shadow(inset 0 1px 0 lighten($mit-red, 10%)); @include box-shadow(inset 0 1px 0 lighten($pink, 10%));
cursor: pointer; cursor: pointer;
height: 15px; height: 15px;
left: -6px; left: -6px;
...@@ -408,6 +408,7 @@ div.video { ...@@ -408,6 +408,7 @@ div.video {
cursor: pointer; cursor: pointer;
margin-bottom: 8px; margin-bottom: 8px;
padding: 0; padding: 0;
line-height: lh();
&.current { &.current {
color: #333; color: #333;
...@@ -415,7 +416,7 @@ div.video { ...@@ -415,7 +416,7 @@ div.video {
} }
&:hover { &:hover {
color: $mit-red; color: $blue;
} }
&:empty { &:empty {
...@@ -444,13 +445,13 @@ div.video { ...@@ -444,13 +445,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 +460,17 @@ div.video { ...@@ -459,30 +460,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,
......
...@@ -151,26 +151,33 @@ class @Problem ...@@ -151,26 +151,33 @@ class @Problem
return return
if not window.FormData if not window.FormData
alert "Sorry, your browser does not support file uploads. Your submit request could not be fulfilled. If you can, please use Chrome or Safari which have been verified to support file uploads." alert "Submission aborted! Sorry, your browser does not support file uploads. If you can, please use Chrome or Safari which have been verified to support file uploads."
return return
fd = new FormData() fd = new FormData()
# Sanity check of file size # Sanity checks on submission
file_too_large = false
max_filesize = 4*1000*1000 # 4 MB max_filesize = 4*1000*1000 # 4 MB
file_too_large = false
file_not_selected = false
@inputs.each (index, element) -> @inputs.each (index, element) ->
if element.type is 'file' if element.type is 'file'
if element.files[0] instanceof File for file in element.files
if element.files[0].size > max_filesize if file.size > max_filesize
file_too_large = true file_too_large = true
alert 'Submission aborted! Your file "' + element.files[0].name + '" is too large (max size: ' + max_filesize/(1000*1000) + ' MB)' alert 'Submission aborted! Your file "' + file.name '" is too large (max size: ' + max_filesize/(1000*1000) + ' MB)'
fd.append(element.id, element.files[0]) fd.append(element.id, file)
else if element.files.length == 0
fd.append(element.id, '') file_not_selected = true
fd.append(element.id, '') # In case we want to allow submissions with no file
else else
fd.append(element.id, element.value) fd.append(element.id, element.value)
if file_not_selected
alert 'Submission aborted! You did not select any files to submit'
abort_submission = file_too_large or file_not_selected
settings = settings =
type: "POST" type: "POST"
...@@ -184,8 +191,8 @@ class @Problem ...@@ -184,8 +191,8 @@ class @Problem
@updateProgress response @updateProgress response
else else
alert(response.success) alert(response.success)
if not file_too_large if not abort_submission
$.ajaxWithPrefix("#{@url}/problem_check", settings) $.ajaxWithPrefix("#{@url}/problem_check", settings)
check: => check: =>
......
...@@ -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
......
...@@ -223,6 +223,13 @@ class ModuleStore(object): ...@@ -223,6 +223,13 @@ class ModuleStore(object):
""" """
raise NotImplementedError raise NotImplementedError
def get_instance(self, course_id, location):
"""
Get an instance of this location, with policy for course_id applied.
TODO (vshnayder): this may want to live outside the modulestore eventually
"""
raise NotImplementedError
def get_item_errors(self, location): def get_item_errors(self, location):
""" """
Return a list of (msg, exception-or-None) errors that the modulestore Return a list of (msg, exception-or-None) errors that the modulestore
...@@ -331,7 +338,8 @@ class ModuleStoreBase(ModuleStore): ...@@ -331,7 +338,8 @@ class ModuleStoreBase(ModuleStore):
and datastores. and datastores.
""" """
# check that item is present and raise the promised exceptions if needed # check that item is present and raise the promised exceptions if needed
self.get_item(location) # TODO (vshnayder): post-launch, make errors properties of items
#self.get_item(location)
errorlog = self._get_errorlog(location) errorlog = self._get_errorlog(location)
return errorlog.errors return errorlog.errors
...@@ -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'])
...@@ -216,6 +217,13 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -216,6 +217,13 @@ class MongoModuleStore(ModuleStoreBase):
item = self._find_one(location) item = self._find_one(location)
return self._load_items([item], depth)[0] return self._load_items([item], depth)[0]
def get_instance(self, course_id, location):
"""
TODO (vshnayder): implement policy tracking in mongo.
For now, just delegate to get_item and ignore policy.
"""
return self.get_item(location)
def get_items(self, location, depth=0): def get_items(self, location, depth=0):
items = self.collection.find( items = self.collection.find(
location_to_query(location), location_to_query(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
from collections import defaultdict
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 +18,10 @@ from cStringIO import StringIO ...@@ -15,9 +18,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 +34,8 @@ def clean_out_mako_templating(xml_string): ...@@ -30,7 +34,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, course_id, 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.
...@@ -39,6 +44,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): ...@@ -39,6 +44,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
""" """
self.unnamed_modules = 0 self.unnamed_modules = 0
self.used_slugs = set() self.used_slugs = set()
self.org, self.course, self.url_name = course_id.split('/')
def process_xml(xml): def process_xml(xml):
"""Takes an xml string, and returns a XModuleDescriptor created from """Takes an xml string, and returns a XModuleDescriptor created from
...@@ -76,27 +82,30 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): ...@@ -76,27 +82,30 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
xml_data.set('url_name', slug) xml_data.set('url_name', slug)
descriptor = XModuleDescriptor.load_from_xml( descriptor = XModuleDescriptor.load_from_xml(
etree.tostring(xml_data), self, org, etree.tostring(xml_data), self, self.org,
course, xmlstore.default_class) self.course, xmlstore.default_class)
#log.debug('==> importing descriptor location %s' % #log.debug('==> importing descriptor location %s' %
# repr(descriptor.location)) # repr(descriptor.location))
descriptor.metadata['data_dir'] = course_dir descriptor.metadata['data_dir'] = course_dir
xmlstore.modules[descriptor.location] = descriptor xmlstore.modules[course_id][descriptor.location] = descriptor
if xmlstore.eager: if xmlstore.eager:
descriptor.get_children() descriptor.get_children()
return descriptor return descriptor
render_template = lambda: '' render_template = lambda: ''
load_item = xmlstore.get_item # TODO (vshnayder): we are somewhat architecturally confused in the loading code:
# load_item should actually be get_instance, because it expects the course-specific
# policy to be loaded. For now, just add the course_id here...
load_item = lambda location: xmlstore.get_instance(course_id, location)
resources_fs = OSFS(xmlstore.data_dir / course_dir) resources_fs = OSFS(xmlstore.data_dir / course_dir)
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):
...@@ -123,7 +132,7 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -123,7 +132,7 @@ class XMLModuleStore(ModuleStoreBase):
self.eager = eager self.eager = eager
self.data_dir = path(data_dir) self.data_dir = path(data_dir)
self.modules = {} # location -> XModuleDescriptor self.modules = defaultdict(dict) # course_id -> dict(location -> XModuleDescriptor)
self.courses = {} # course_dir -> XModuleDescriptor for the course self.courses = {} # course_dir -> XModuleDescriptor for the course
if default_class is None: if default_class is None:
...@@ -149,7 +158,7 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -149,7 +158,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 +179,28 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -170,7 +179,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 +218,7 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -188,7 +218,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 +241,27 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -211,9 +241,27 @@ 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', course_data.get('slug'))
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 = {}
# VS[compat] : 'name' is deprecated, but support it for now...
if course_data.get('name'):
url_name = Location.clean(course_data.get('name'))
tracker("'name' is deprecated for module xml. Please use "
"display_name and url_name.")
else:
raise ValueError("Can't load a course without a 'url_name' "
"(or 'name') set. Set url_name.")
course_id = CourseDescriptor.make_id(org, course, url_name)
system = ImportSystem(self, course_id, 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
...@@ -224,11 +272,12 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -224,11 +272,12 @@ class XMLModuleStore(ModuleStoreBase):
return course_descriptor return course_descriptor
def get_item(self, location, depth=0): def get_instance(self, course_id, location, depth=0):
""" """
Returns an XModuleDescriptor instance for the item at location. Returns an XModuleDescriptor instance for the item at
If location.revision is None, returns the most item with the most location, with the policy for course_id. (In case two xml
recent revision dirs have different content at the same location, return the
one for this course_id.)
If any segment of the location is None except revision, raises If any segment of the location is None except revision, raises
xmodule.modulestore.exceptions.InsufficientSpecificationError xmodule.modulestore.exceptions.InsufficientSpecificationError
...@@ -240,10 +289,27 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -240,10 +289,27 @@ class XMLModuleStore(ModuleStoreBase):
""" """
location = Location(location) location = Location(location)
try: try:
return self.modules[location] return self.modules[course_id][location]
except KeyError: except KeyError:
raise ItemNotFoundError(location) raise ItemNotFoundError(location)
def get_item(self, location, depth=0):
"""
Returns an XModuleDescriptor instance for the item at location.
If location.revision is None, returns the most item with the most
recent revision
If any segment of the location is None except revision, raises
xmodule.modulestore.exceptions.InsufficientSpecificationError
If no object is found at that location, raises
xmodule.modulestore.exceptions.ItemNotFoundError
location: Something that can be passed to Location
"""
raise NotImplementedError("XMLModuleStores can't guarantee that definitions"
" are unique. Use get_instance.")
def get_courses(self, depth=0): def get_courses(self, depth=0):
""" """
......
...@@ -22,21 +22,22 @@ def import_from_xml(store, data_dir, course_dirs=None, eager=True, ...@@ -22,21 +22,22 @@ def import_from_xml(store, data_dir, course_dirs=None, eager=True,
eager=eager, eager=eager,
course_dirs=course_dirs course_dirs=course_dirs
) )
for module in module_store.modules.itervalues(): for course_id in module_store.modules.keys():
for module in module_store.modules[course_id].itervalues():
# TODO (cpennington): This forces import to overrite the same items.
# This should in the future create new revisions of the items on import # TODO (cpennington): This forces import to overrite the same items.
try: # This should in the future create new revisions of the items on import
store.create_item(module.location) try:
except DuplicateItemError: store.create_item(module.location)
log.exception('Item already exists at %s' % module.location.url()) except DuplicateItemError:
pass log.exception('Item already exists at %s' % module.location.url())
if 'data' in module.definition: pass
store.update_item(module.location, module.definition['data']) if 'data' in module.definition:
if 'children' in module.definition: store.update_item(module.location, module.definition['data'])
store.update_children(module.location, module.definition['children']) if 'children' in module.definition:
# NOTE: It's important to use own_metadata here to avoid writing store.update_children(module.location, module.definition['children'])
# inherited metadata everywhere. # NOTE: It's important to use own_metadata here to avoid writing
store.update_metadata(module.location, dict(module.own_metadata)) # inherited metadata everywhere.
store.update_metadata(module.location, dict(module.own_metadata))
return module_store return module_store
...@@ -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
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
import unittest import unittest
import os import os
import fs import fs
import fs.osfs
import json import json
import json import json
...@@ -35,7 +36,6 @@ i4xs = ModuleSystem( ...@@ -35,7 +36,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,18 +336,18 @@ class CodeResponseTest(unittest.TestCase): ...@@ -336,18 +336,18 @@ 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)
answers_with_file = {'1_2_1': 'String-based answer', answers_with_file = {'1_2_1': 'String-based answer',
'1_3_1': ['answer1', 'answer2', 'answer3'], '1_3_1': ['answer1', 'answer2', 'answer3'],
'1_4_1': fp} '1_4_1': [fp, fp]}
answers_converted = convert_files_to_filenames(answers_with_file) answers_converted = convert_files_to_filenames(answers_with_file)
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, fp.name])
class ChoiceResponseTest(unittest.TestCase): class ChoiceResponseTest(unittest.TestCase):
......
...@@ -84,20 +84,22 @@ class RoundTripTestCase(unittest.TestCase): ...@@ -84,20 +84,22 @@ class RoundTripTestCase(unittest.TestCase):
strip_filenames(exported_course) strip_filenames(exported_course)
self.assertEquals(initial_course, exported_course) self.assertEquals(initial_course, exported_course)
self.assertEquals(initial_course.id, exported_course.id)
course_id = initial_course.id
print "Checking key equality" print "Checking key equality"
self.assertEquals(sorted(initial_import.modules.keys()), self.assertEquals(sorted(initial_import.modules[course_id].keys()),
sorted(second_import.modules.keys())) sorted(second_import.modules[course_id].keys()))
print "Checking module equality" print "Checking module equality"
for location in initial_import.modules.keys(): for location in initial_import.modules[course_id].keys():
print "Checking", location print "Checking", location
if location.category == 'html': if location.category == 'html':
print ("Skipping html modules--they can't import in" print ("Skipping html modules--they can't import in"
" final form without writing files...") " final form without writing files...")
continue continue
self.assertEquals(initial_import.modules[location], self.assertEquals(initial_import.modules[course_id][location],
second_import.modules[location]) second_import.modules[course_id][location])
def setUp(self): def setUp(self):
......
...@@ -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,9 +201,54 @@ class ImportTestCase(unittest.TestCase): ...@@ -201,9 +201,54 @@ 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)
check_for_key('graceperiod', course) check_for_key('graceperiod', course)
def test_policy_loading(self):
"""Make sure that when two courses share content with the same
org and course names, policy applies to the right one."""
def get_course(name):
print "Importing {0}".format(name)
modulestore = XMLModuleStore(DATA_DIR, eager=True, course_dirs=[name])
courses = modulestore.get_courses()
self.assertEquals(len(courses), 1)
return courses[0]
toy = get_course('toy')
two_toys = get_course('two_toys')
self.assertEqual(toy.url_name, "2012_Fall")
self.assertEqual(two_toys.url_name, "TT_2012_Fall")
toy_ch = toy.get_children()[0]
two_toys_ch = two_toys.get_children()[0]
self.assertEqual(toy_ch.display_name, "Overview")
self.assertEqual(two_toys_ch.display_name, "Two Toy Overview")
def test_definition_loading(self):
"""When two courses share the same org and course name and
both have a module with the same url_name, the definitions shouldn't clash.
TODO (vshnayder): once we have a CMS, this shouldn't
happen--locations should uniquely name definitions. But in
our imperfect XML world, it can (and likely will) happen."""
modulestore = XMLModuleStore(DATA_DIR, eager=True, course_dirs=['toy', 'two_toys'])
toy_id = "edX/toy/2012_Fall"
two_toy_id = "edX/toy/TT_2012_Fall"
location = Location(["i4x", "edX", "toy", "video", "Welcome"])
toy_video = modulestore.get_instance(toy_id, location)
two_toy_video = modulestore.get_instance(two_toy_id, location)
self.assertEqual(toy_video.metadata['youtube'], "1.0:p2Q6BrNhdh8")
self.assertEqual(two_toy_video.metadata['youtube'], "1.0:p2Q6BrNhdh9")
"""
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,
......
CodeMirror.defineMode("xml", function(config, parserConfig) { CodeMirror.defineMode("xml", function(config, parserConfig) {
var indentUnit = config.indentUnit; var indentUnit = config.indentUnit;
var Kludges = parserConfig.htmlMode ? { var Kludges = parserConfig.htmlMode ? {
autoSelfClosers: {"br": true, "img": true, "hr": true, "link": true, "input": true, autoSelfClosers: {'area': true, 'base': true, 'br': true, 'col': true, 'command': true,
"meta": true, "col": true, "frame": true, "base": true, "area": true}, 'embed': true, 'frame': true, 'hr': true, 'img': true, 'input': true,
'keygen': true, 'link': true, 'meta': true, 'param': true, 'source': true,
'track': true, 'wbr': true},
implicitlyClosed: {'dd': true, 'li': true, 'optgroup': true, 'option': true, 'p': true,
'rp': true, 'rt': true, 'tbody': true, 'td': true, 'tfoot': true,
'th': true, 'tr': true},
contextGrabbers: {
'dd': {'dd': true, 'dt': true},
'dt': {'dd': true, 'dt': true},
'li': {'li': true},
'option': {'option': true, 'optgroup': true},
'optgroup': {'optgroup': true},
'p': {'address': true, 'article': true, 'aside': true, 'blockquote': true, 'dir': true,
'div': true, 'dl': true, 'fieldset': true, 'footer': true, 'form': true,
'h1': true, 'h2': true, 'h3': true, 'h4': true, 'h5': true, 'h6': true,
'header': true, 'hgroup': true, 'hr': true, 'menu': true, 'nav': true, 'ol': true,
'p': true, 'pre': true, 'section': true, 'table': true, 'ul': true},
'rp': {'rp': true, 'rt': true},
'rt': {'rp': true, 'rt': true},
'tbody': {'tbody': true, 'tfoot': true},
'td': {'td': true, 'th': true},
'tfoot': {'tbody': true},
'th': {'td': true, 'th': true},
'thead': {'tbody': true, 'tfoot': true},
'tr': {'tr': true}
},
doNotIndent: {"pre": true}, doNotIndent: {"pre": true},
allowUnquoted: true, allowUnquoted: true,
allowMissing: false allowMissing: false
} : {autoSelfClosers: {}, doNotIndent: {}, allowUnquoted: false, allowMissing: false}; } : {
autoSelfClosers: {},
implicitlyClosed: {},
contextGrabbers: {},
doNotIndent: {},
allowUnquoted: false,
allowMissing: false
};
var alignCDATA = parserConfig.alignCDATA; var alignCDATA = parserConfig.alignCDATA;
// Return variables for tokenizers // Return variables for tokenizers
...@@ -162,7 +194,12 @@ CodeMirror.defineMode("xml", function(config, parserConfig) { ...@@ -162,7 +194,12 @@ CodeMirror.defineMode("xml", function(config, parserConfig) {
} else if (type == "closeTag") { } else if (type == "closeTag") {
var err = false; var err = false;
if (curState.context) { if (curState.context) {
err = curState.context.tagName != tagName; if (curState.context.tagName != tagName) {
if (Kludges.implicitlyClosed.hasOwnProperty(curState.context.tagName.toLowerCase())) {
popContext();
}
err = !curState.context || curState.context.tagName != tagName;
}
} else { } else {
err = true; err = true;
} }
...@@ -174,9 +211,15 @@ CodeMirror.defineMode("xml", function(config, parserConfig) { ...@@ -174,9 +211,15 @@ CodeMirror.defineMode("xml", function(config, parserConfig) {
function endtag(startOfLine) { function endtag(startOfLine) {
return function(type) { return function(type) {
if (type == "selfcloseTag" || if (type == "selfcloseTag" ||
(type == "endTag" && Kludges.autoSelfClosers.hasOwnProperty(curState.tagName.toLowerCase()))) (type == "endTag" && Kludges.autoSelfClosers.hasOwnProperty(curState.tagName.toLowerCase()))) {
maybePopContext(curState.tagName.toLowerCase());
return cont();
}
if (type == "endTag") {
maybePopContext(curState.tagName.toLowerCase());
pushContext(curState.tagName, startOfLine);
return cont(); return cont();
if (type == "endTag") {pushContext(curState.tagName, startOfLine); return cont();} }
return cont(); return cont();
}; };
} }
...@@ -188,6 +231,20 @@ CodeMirror.defineMode("xml", function(config, parserConfig) { ...@@ -188,6 +231,20 @@ CodeMirror.defineMode("xml", function(config, parserConfig) {
return cont(arguments.callee); return cont(arguments.callee);
} }
} }
function maybePopContext(nextTagName) {
var parentTagName;
while (true) {
if (!curState.context) {
return;
}
parentTagName = curState.context.tagName.toLowerCase();
if (!Kludges.contextGrabbers.hasOwnProperty(parentTagName) ||
!Kludges.contextGrabbers[parentTagName].hasOwnProperty(nextTagName)) {
return;
}
popContext();
}
}
function attributes(type) { function attributes(type) {
if (type == "word") {setStyle = "attribute"; return cont(attribute, attributes);} if (type == "word") {setStyle = "attribute"; return cont(attribute, attributes);}
...@@ -255,7 +312,7 @@ CodeMirror.defineMode("xml", function(config, parserConfig) { ...@@ -255,7 +312,7 @@ CodeMirror.defineMode("xml", function(config, parserConfig) {
if (a.indented != b.indented || a.tokenize != b.tokenize) return false; if (a.indented != b.indented || a.tokenize != b.tokenize) return false;
for (var ca = a.context, cb = b.context; ; ca = ca.prev, cb = cb.prev) { for (var ca = a.context, cb = b.context; ; ca = ca.prev, cb = cb.prev) {
if (!ca || !cb) return ca == cb; if (!ca || !cb) return ca == cb;
if (ca.tagName != cb.tagName) return false; if (ca.tagName != cb.tagName || ca.indent != cb.indent) return false;
} }
}, },
...@@ -263,5 +320,7 @@ CodeMirror.defineMode("xml", function(config, parserConfig) { ...@@ -263,5 +320,7 @@ CodeMirror.defineMode("xml", function(config, parserConfig) {
}; };
}); });
CodeMirror.defineMIME("text/xml", "xml");
CodeMirror.defineMIME("application/xml", "xml"); CodeMirror.defineMIME("application/xml", "xml");
CodeMirror.defineMIME("text/html", {name: "xml", htmlMode: true}); if (!CodeMirror.mimeModes.hasOwnProperty("text/html"))
CodeMirror.defineMIME("text/html", {name: "xml", htmlMode: true});
## ## mako
## File: templates/mathjax_include.html ## File: templates/mathjax_include.html
## ##
## Advanced mathjax using 2.0-latest CDN for Dynamic Math ## Advanced mathjax using 2.0-latest CDN for Dynamic Math
## ##
## This enables ASCIIMathJAX, and is used by js_textbox ## This enables ASCIIMathJAX, and is used by js_textbox
%if mathjax_mode is not Undefined and mathjax_mode == 'wiki':
<script type="text/x-mathjax-config">
MathJax.Hub.Config({
tex2jax: {inlineMath: [ ['$','$'], ["\\(","\\)"]],
displayMath: [ ['$$','$$'], ["\\[","\\]"]]}
});
</script>
%else:
<script type="text/x-mathjax-config"> <script type="text/x-mathjax-config">
MathJax.Hub.Config({ MathJax.Hub.Config({
tex2jax: { tex2jax: {
...@@ -19,6 +28,7 @@ ...@@ -19,6 +28,7 @@
} }
}); });
</script> </script>
%endif
<!-- This must appear after all mathjax-config blocks, so it is after the imports from the other templates. <!-- This must appear after all mathjax-config blocks, so it is after the imports from the other templates.
It can't be run through static.url because MathJax uses crazy url introspection to do lazy loading of It can't be run through static.url because MathJax uses crazy url introspection to do lazy loading of
......
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
A copy of the toy course, with different metadata. Used to test policy loading for course with identical org course fields and shared content.
<chapter>
<videosequence url_name="Toy_Videos"/>
<video url_name="Welcome"/>
</chapter>
<course url_name="TT_2012_Fall" org="edX" course="toy"/>
<course display_name="Toy Course" graceperiod="2 days 5 hours 59 minutes 59 seconds" start="2015-07-17T12:00">
<chapter url_name="Overview"/>
</course>
{
"course/TT_2012_Fall": {
"graceperiod": "2 days 5 hours 59 minutes 59 seconds",
"start": "2015-07-17T12:00",
"display_name": "Two Toys Course"
},
"chapter/Overview": {
"display_name": "Two Toy 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"
}
}
<video youtube="1.0:1bK-WdDi6Qw" display_name="Video Resources"/>
<video youtube="1.0:p2Q6BrNhdh9" display_name="Welcome"/>
<videosequence display_name="Toy Videos" format="Lecture Sequence">
<video url_name="Video_Resources"/>
</videosequence>
#!/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:])
...@@ -105,7 +105,7 @@ NUMPY_VER="1.6.2" ...@@ -105,7 +105,7 @@ NUMPY_VER="1.6.2"
SCIPY_VER="0.10.1" SCIPY_VER="0.10.1"
BREW_FILE="$BASE/mitx/brew-formulas.txt" BREW_FILE="$BASE/mitx/brew-formulas.txt"
LOG="/var/tmp/install-$(date +%Y%m%d-%H%M%S).log" LOG="/var/tmp/install-$(date +%Y%m%d-%H%M%S).log"
APT_PKGS="curl git python-virtualenv build-essential python-dev gfortran liblapack-dev libfreetype6-dev libpng12-dev libxml2-dev libxslt-dev yui-compressor coffeescript" APT_PKGS="curl git python-virtualenv build-essential python-dev gfortran liblapack-dev libfreetype6-dev libpng12-dev libxml2-dev libxslt-dev yui-compressor coffeescript graphviz"
if [[ $EUID -eq 0 ]]; then if [[ $EUID -eq 0 ]]; then
error "This script should not be run using sudo or as the root user" error "This script should not be run using sudo or as the root user"
......
...@@ -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 import forms
from django.forms.util import flatatt
from django.utils.encoding import force_unicode
from django.utils.html import conditional_escape
from django.utils.safestring import mark_safe
from wiki.editors.base import BaseEditor
from wiki.editors.markitup import MarkItUpAdminWidget
class CodeMirrorWidget(forms.Widget):
def __init__(self, attrs=None):
# The 'rows' and 'cols' attributes are required for HTML correctness.
default_attrs = {'class': 'markItUp',
'rows': '10', 'cols': '40',}
if attrs:
default_attrs.update(attrs)
super(CodeMirrorWidget, self).__init__(default_attrs)
def render(self, name, value, attrs=None):
if value is None: value = ''
final_attrs = self.build_attrs(attrs, name=name)
return mark_safe(u'<div><textarea%s>%s</textarea></div>' % (flatatt(final_attrs),
conditional_escape(force_unicode(value))))
class CodeMirror(BaseEditor):
editor_id = 'codemirror'
def get_admin_widget(self, instance=None):
return MarkItUpAdminWidget()
def get_widget(self, instance=None):
return CodeMirrorWidget()
class AdminMedia:
css = {
'all': ("wiki/markitup/skins/simple/style.css",
"wiki/markitup/sets/admin/style.css",)
}
js = ("wiki/markitup/admin.init.js",
"wiki/markitup/jquery.markitup.js",
"wiki/markitup/sets/admin/set.js",
)
class Media:
css = {
'all': ("js/vendor/CodeMirror/codemirror.css",)
}
js = ("js/vendor/CodeMirror/codemirror.js",
"js/vendor/CodeMirror/xml.js",
"js/vendor/CodeMirror/mitx_markdown.js",
"js/wiki/CodeMirror.init.js",
)
# Make sure wiki_plugin.py gets run.
from course_wiki.plugins.markdownedx.wiki_plugin import ExtendMarkdownPlugin
\ No newline at end of file
#!/usr/bin/env python
'''
Image Circuit Extension for Python-Markdown
======================================
Any single line beginning with circuit-schematic: and followed by data (which should be json data, but this
is not enforced at this level) will be displayed as a circuit schematic. This is simply an input element with
the value set to the data. It is left to javascript on the page to render that input as a circuit schematic.
ex:
circuit-schematic:[["r",[128,48,0],{"r":"1","_json_":0},["2","1"]],["view",0,0,2,null,null,null,null,null,null,null],["dc",{"0":0,"1":1,"I(_3)":-1}]]
(This is a schematic with a single one-ohm resistor. Note that this data is not meant to be user-editable.)
'''
import markdown
import re
from django.utils.html import escape
try:
# Markdown 2.1.0 changed from 2.0.3. We try importing the new version first,
# but import the 2.0.3 version if it fails
from markdown.util import etree
except:
from markdown import etree
class CircuitExtension(markdown.Extension):
def __init__(self, configs):
for key, value in configs:
self.setConfig(key, value)
def extendMarkdown(self, md, md_globals):
## Because Markdown treats contigous lines as one block of text, it is hard to match
## a regex that must occupy the whole line (like the circuit regex). This is why we have
## a preprocessor that inspects the lines and replaces the matched lines with text that is
## easier to match
md.preprocessors.add('circuit', CircuitPreprocessor(md), "_begin")
pattern = CircuitLink(r'processed-schematic:(?P<data>.*?)processed-schematic-end')
pattern.md = md
pattern.ext = self
md.inlinePatterns.add('circuit', pattern, "<reference")
class CircuitPreprocessor(markdown.preprocessors.Preprocessor):
preRegex = re.compile(r'^circuit-schematic:(?P<data>.*)$')
def run(self, lines):
print "running circuit preprocessor"
def convertLine(line):
m = self.preRegex.match(line)
if m:
return 'processed-schematic:{0}processed-schematic-end'.format(m.group('data'))
else:
return line
return [convertLine(line) for line in lines]
class CircuitLink(markdown.inlinepatterns.Pattern):
def handleMatch(self, m):
data = m.group('data')
data = escape(data)
return etree.fromstring("<div align='center'><input type='hidden' parts='' value='" + data + "' analyses='' class='schematic ctrls' width='400' height='220'/></div>")
def makeExtension(configs=None):
to_return = CircuitExtension(configs=configs)
print "circuit returning ", to_return
return to_return
#!/usr/bin/env python
'''
Image Embedding Extension for Python-Markdown
======================================
Converts lone links to embedded images, provided the file extension is allowed.
Ex:
http://www.ericfehse.net/media/img/ef/blog/django-pony.jpg
becomes
<img src="http://www.ericfehse.net/media/img/ef/blog/django-pony.jpg">
mypic.jpg becomes <img src="/MEDIA_PATH/mypic.jpg">
Requires Python-Markdown 1.6+
'''
import simplewiki.settings as settings
import markdown
try:
# Markdown 2.1.0 changed from 2.0.3. We try importing the new version first,
# but import the 2.0.3 version if it fails
from markdown.util import etree
except:
from markdown import etree
class ImageExtension(markdown.Extension):
def __init__(self, configs):
for key, value in configs:
self.setConfig(key, value)
def add_inline(self, md, name, klass, re):
pattern = klass(re)
pattern.md = md
pattern.ext = self
md.inlinePatterns.add(name, pattern, "<reference")
def extendMarkdown(self, md, md_globals):
self.add_inline(md, 'image', ImageLink,
r'^(?P<proto>([^:/?#])+://)?(?P<domain>([^/?#]*)/)?(?P<path>[^?#]*\.(?P<ext>[^?#]{3,4}))(?:\?([^#]*))?(?:#(.*))?$')
class ImageLink(markdown.inlinepatterns.Pattern):
def handleMatch(self, m):
img = etree.Element('img')
proto = m.group('proto') or "http://"
domain = m.group('domain')
path = m.group('path')
ext = m.group('ext')
# A fixer upper
if ext.lower() in settings.WIKI_IMAGE_EXTENSIONS:
if domain:
src = proto + domain + path
elif path:
# We need a nice way to source local attachments...
src = "/wiki/media/" + path + ".upload"
else:
src = ''
img.set('src', src)
return img
def makeExtension(configs=None):
return ImageExtension(configs=configs)
if __name__ == "__main__":
import doctest
doctest.testmod()
# Source: https://github.com/mayoff/python-markdown-mathjax
import markdown
try:
# Markdown 2.1.0 changed from 2.0.3. We try importing the new version first,
# but import the 2.0.3 version if it fails
from markdown.util import etree, AtomicString
except:
from markdown import etree, AtomicString
class MathJaxPattern(markdown.inlinepatterns.Pattern):
def __init__(self):
markdown.inlinepatterns.Pattern.__init__(self, r'(?<!\\)(\$\$?)(.+?)\2')
def handleMatch(self, m):
el = etree.Element('span')
el.text = AtomicString(m.group(2) + m.group(3) + m.group(2))
return el
class MathJaxExtension(markdown.Extension):
def extendMarkdown(self, md, md_globals):
# Needs to come before escape matching because \ is pretty important in LaTeX
md.inlinePatterns.add('mathjax', MathJaxPattern(), '<escape')
def makeExtension(configs=None):
return MathJaxExtension(configs)
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