Commit 15fa639e by Brittany Cheng

merge conflict

parents 69475c63 e59ea854
...@@ -11,33 +11,33 @@ class PostReceiveTestCase(TestCase): ...@@ -11,33 +11,33 @@ class PostReceiveTestCase(TestCase):
def setUp(self): def setUp(self):
self.client = Client() self.client = Client()
@patch('github_sync.views.sync_with_github') @patch('github_sync.views.import_from_github')
def test_non_branch(self, sync_with_github): def test_non_branch(self, import_from_github):
self.client.post('/github_service_hook', {'payload': json.dumps({ self.client.post('/github_service_hook', {'payload': json.dumps({
'ref': 'refs/tags/foo'}) 'ref': 'refs/tags/foo'})
}) })
self.assertFalse(sync_with_github.called) self.assertFalse(import_from_github.called)
@patch('github_sync.views.sync_with_github') @patch('github_sync.views.import_from_github')
def test_non_watched_repo(self, sync_with_github): def test_non_watched_repo(self, import_from_github):
self.client.post('/github_service_hook', {'payload': json.dumps({ self.client.post('/github_service_hook', {'payload': json.dumps({
'ref': 'refs/heads/branch', 'ref': 'refs/heads/branch',
'repository': {'name': 'bad_repo'}}) 'repository': {'name': 'bad_repo'}})
}) })
self.assertFalse(sync_with_github.called) self.assertFalse(import_from_github.called)
@patch('github_sync.views.sync_with_github') @patch('github_sync.views.import_from_github')
def test_non_tracked_branch(self, sync_with_github): def test_non_tracked_branch(self, import_from_github):
self.client.post('/github_service_hook', {'payload': json.dumps({ self.client.post('/github_service_hook', {'payload': json.dumps({
'ref': 'refs/heads/non_branch', 'ref': 'refs/heads/non_branch',
'repository': {'name': 'repo'}}) 'repository': {'name': 'repo'}})
}) })
self.assertFalse(sync_with_github.called) self.assertFalse(import_from_github.called)
@patch('github_sync.views.sync_with_github') @patch('github_sync.views.import_from_github')
def test_tracked_branch(self, sync_with_github): def test_tracked_branch(self, import_from_github):
self.client.post('/github_service_hook', {'payload': json.dumps({ self.client.post('/github_service_hook', {'payload': json.dumps({
'ref': 'refs/heads/branch', 'ref': 'refs/heads/branch',
'repository': {'name': 'repo'}}) 'repository': {'name': 'repo'}})
}) })
sync_with_github.assert_called_with(load_repo_settings('repo')) import_from_github.assert_called_with(load_repo_settings('repo'))
...@@ -5,7 +5,7 @@ from django.http import HttpResponse ...@@ -5,7 +5,7 @@ from django.http import HttpResponse
from django.conf import settings from django.conf import settings
from django_future.csrf import csrf_exempt from django_future.csrf import csrf_exempt
from . import sync_with_github, load_repo_settings from . import import_from_github, load_repo_settings
log = logging.getLogger() log = logging.getLogger()
...@@ -46,6 +46,6 @@ def github_post_receive(request): ...@@ -46,6 +46,6 @@ def github_post_receive(request):
log.info('Ignoring changes to non-tracked branch %s in repo %s' % (branch_name, repo_name)) log.info('Ignoring changes to non-tracked branch %s in repo %s' % (branch_name, repo_name))
return HttpResponse('Ignoring non-tracked branch') return HttpResponse('Ignoring non-tracked branch')
sync_with_github(repo) import_from_github(repo)
return HttpResponse('Push received') return HttpResponse('Push received')
...@@ -89,8 +89,8 @@ def add_histogram(get_html, module): ...@@ -89,8 +89,8 @@ def add_histogram(get_html, module):
else: else:
edit_link = False edit_link = False
staff_context = {'definition': dict(module.definition), staff_context = {'definition': json.dumps(module.definition, indent=4),
'metadata': dict(module.metadata), 'metadata': json.dumps(module.metadata, indent=4),
'element_id': module.location.html_id(), 'element_id': module.location.html_id(),
'edit_link': edit_link, 'edit_link': edit_link,
'histogram': json.dumps(histogram), 'histogram': json.dumps(histogram),
......
"""
A handy util to print a django-debug-screen-like stack trace with
values of local variables.
"""
import sys, traceback
from django.utils.encoding import smart_unicode
def supertrace(max_len=160):
"""
Print the usual traceback information, followed by a listing of all the
local variables in each frame. Should be called from an exception handler.
if max_len is not None, will print up to max_len chars for each local variable.
(cite: modified from somewhere on stackoverflow)
"""
tb = sys.exc_info()[2]
while True:
if not tb.tb_next:
break
tb = tb.tb_next
stack = []
frame = tb.tb_frame
while frame:
stack.append(f)
frame = frame.f_back
stack.reverse()
# First print the regular traceback
traceback.print_exc()
print "Locals by frame, innermost last"
for frame in stack:
print
print "Frame %s in %s at line %s" % (frame.f_code.co_name,
frame.f_code.co_filename,
frame.f_lineno)
for key, value in frame.f_locals.items():
print ("\t%20s = " % smart_unicode(key, errors='ignore')),
# We have to be careful not to cause a new error in our error
# printer! Calling str() on an unknown object could cause an
# error.
try:
s = smart_unicode(value, errors='ignore')
if max_len is not None:
s = s[:max_len]
print s
except:
print "<ERROR WHILE PRINTING VALUE>"
from collections import MutableMapping
class LazyLoadingDict(MutableMapping):
"""
A dictionary object that lazily loads its contents from a provided
function on reads (of members that haven't already been set).
"""
def __init__(self, loader):
'''
On the first read from this dictionary, it will call loader() to
populate its contents. loader() must return something dict-like. Any
elements set before the first read will be preserved.
'''
self._contents = {}
self._loaded = False
self._loader = loader
self._deleted = set()
def __getitem__(self, name):
if not (self._loaded or name in self._contents or name in self._deleted):
self.load()
return self._contents[name]
def __setitem__(self, name, value):
self._contents[name] = value
self._deleted.discard(name)
def __delitem__(self, name):
del self._contents[name]
self._deleted.add(name)
def __contains__(self, name):
self.load()
return name in self._contents
def __len__(self):
self.load()
return len(self._contents)
def __iter__(self):
self.load()
return iter(self._contents)
def __repr__(self):
self.load()
return repr(self._contents)
def load(self):
if self._loaded:
return
loaded_contents = self._loader()
loaded_contents.update(self._contents)
self._contents = loaded_contents
self._loaded = True
'''
Progress class for modules. Represents where a student is in a module.
Useful things to know:
- Use Progress.to_js_status_str() to convert a progress into a simple
status string to pass to js.
- Use Progress.to_js_detail_str() to convert a progress into a more detailed
string to pass to js.
In particular, these functions have a canonical handing of None.
For most subclassing needs, you should only need to reimplement
frac() and __str__().
'''
from collections import namedtuple
import numbers
class Progress(object):
'''Represents a progress of a/b (a out of b done)
a and b must be numeric, but not necessarily integer, with
0 <= a <= b and b > 0.
Progress can only represent Progress for modules where that makes sense. Other
modules (e.g. html) should return None from get_progress().
TODO: add tag for module type? Would allow for smarter merging.
'''
def __init__(self, a, b):
'''Construct a Progress object. a and b must be numbers, and must have
0 <= a <= b and b > 0
'''
# Want to do all checking at construction time, so explicitly check types
if not (isinstance(a, numbers.Number) and
isinstance(b, numbers.Number)):
raise TypeError('a and b must be numbers. Passed {0}/{1}'.format(a, b))
if not (0 <= a <= b and b > 0):
raise ValueError(
'fraction a/b = {0}/{1} must have 0 <= a <= b and b > 0'.format(a, b))
self._a = a
self._b = b
def frac(self):
''' Return tuple (a,b) representing progress of a/b'''
return (self._a, self._b)
def percent(self):
''' Returns a percentage progress as a float between 0 and 100.
subclassing note: implemented in terms of frac(), assumes sanity
checking is done at construction time.
'''
(a, b) = self.frac()
return 100.0 * a / b
def started(self):
''' Returns True if fractional progress is greater than 0.
subclassing note: implemented in terms of frac(), assumes sanity
checking is done at construction time.
'''
return self.frac()[0] > 0
def inprogress(self):
''' Returns True if fractional progress is strictly between 0 and 1.
subclassing note: implemented in terms of frac(), assumes sanity
checking is done at construction time.
'''
(a, b) = self.frac()
return a > 0 and a < b
def done(self):
''' Return True if this represents done.
subclassing note: implemented in terms of frac(), assumes sanity
checking is done at construction time.
'''
(a, b) = self.frac()
return a == b
def ternary_str(self):
''' Return a string version of this progress: either
"none", "in_progress", or "done".
subclassing note: implemented in terms of frac()
'''
(a, b) = self.frac()
if a == 0:
return "none"
if a < b:
return "in_progress"
return "done"
def __eq__(self, other):
''' Two Progress objects are equal if they have identical values.
Implemented in terms of frac()'''
if not isinstance(other, Progress):
return False
(a, b) = self.frac()
(a2, b2) = other.frac()
return a == a2 and b == b2
def __ne__(self, other):
''' The opposite of equal'''
return not self.__eq__(other)
def __str__(self):
''' Return a string representation of this string.
subclassing note: implemented in terms of frac().
'''
(a, b) = self.frac()
return "{0}/{1}".format(a, b)
@staticmethod
def add_counts(a, b):
'''Add two progress indicators, assuming that each represents items done:
(a / b) + (c / d) = (a + c) / (b + d).
If either is None, returns the other.
'''
if a is None:
return b
if b is None:
return a
# get numerators + denominators
(n, d) = a.frac()
(n2, d2) = b.frac()
return Progress(n + n2, d + d2)
@staticmethod
def to_js_status_str(progress):
'''
Return the "status string" version of the passed Progress
object that should be passed to js. Use this function when
sending Progress objects to js to limit dependencies.
'''
if progress is None:
return "NA"
return progress.ternary_str()
@staticmethod
def to_js_detail_str(progress):
'''
Return the "detail string" version of the passed Progress
object that should be passed to js. Use this function when
passing Progress objects to js to limit dependencies.
'''
if progress is None:
return "NA"
return str(progress)
...@@ -20,5 +20,10 @@ class EditingDescriptor(MakoModuleDescriptor): ...@@ -20,5 +20,10 @@ class EditingDescriptor(MakoModuleDescriptor):
def get_context(self): def get_context(self):
return { return {
'module': self, 'module': self,
'data': self.definition['data'], 'data': self.definition.get('data', ''),
# TODO (vshnayder): allow children and metadata to be edited.
#'children' : self.definition.get('children, ''),
# TODO: show both own metadata and inherited?
#'metadata' : self.own_metadata,
} }
import sys
import logging
from pkg_resources import resource_string from pkg_resources import resource_string
from lxml import etree from lxml import etree
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.mako_module import MakoModuleDescriptor from xmodule.mako_module import MakoModuleDescriptor
from xmodule.xml_module import XmlDescriptor from xmodule.xml_module import XmlDescriptor
from xmodule.editing_module import EditingDescriptor from xmodule.editing_module import EditingDescriptor
from xmodule.errortracker import exc_info_to_str
import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -14,14 +17,11 @@ class ErrorModule(XModule): ...@@ -14,14 +17,11 @@ class ErrorModule(XModule):
'''Show an error. '''Show an error.
TODO (vshnayder): proper style, divs, etc. TODO (vshnayder): proper style, divs, etc.
''' '''
if not self.system.is_staff:
return self.system.render_template('module-error.html', {})
# staff get to see all the details # staff get to see all the details
return self.system.render_template('module-error-staff.html', { return self.system.render_template('module-error.html', {
'data' : self.definition['data'], 'data' : self.definition['data']['contents'],
# TODO (vshnayder): need to get non-syntax errors in here somehow 'error' : self.definition['data']['error_msg'],
'error' : self.definition.get('error', 'Error not available') 'is_staff' : self.system.is_staff,
}) })
class ErrorDescriptor(EditingDescriptor): class ErrorDescriptor(EditingDescriptor):
...@@ -31,29 +31,36 @@ class ErrorDescriptor(EditingDescriptor): ...@@ -31,29 +31,36 @@ class ErrorDescriptor(EditingDescriptor):
module_class = ErrorModule module_class = ErrorModule
@classmethod @classmethod
def from_xml(cls, xml_data, system, org=None, course=None, err=None): def from_xml(cls, xml_data, system, org=None, course=None,
error_msg='Error not available'):
'''Create an instance of this descriptor from the supplied data. '''Create an instance of this descriptor from the supplied data.
Does not try to parse the data--just stores it. Does not try to parse the data--just stores it.
Takes an extra, optional, parameter--the error that caused an Takes an extra, optional, parameter--the error that caused an
issue. issue. (should be a string, or convert usefully into one).
''' '''
# Use a nested inner dictionary because 'data' is hardcoded
definition = {} inner = {}
if err is not None: definition = {'data': inner}
definition['error'] = err inner['error_msg'] = str(error_msg)
try: try:
# If this is already an error tag, don't want to re-wrap it. # If this is already an error tag, don't want to re-wrap it.
xml_obj = etree.fromstring(xml_data) xml_obj = etree.fromstring(xml_data)
if xml_obj.tag == 'error': if xml_obj.tag == 'error':
xml_data = xml_obj.text xml_data = xml_obj.text
except etree.XMLSyntaxError as err: error_node = xml_obj.find('error_msg')
if error_node is not None:
inner['error_msg'] = error_node.text
else:
inner['error_msg'] = 'Error not available'
except etree.XMLSyntaxError:
# Save the error to display later--overrides other problems # Save the error to display later--overrides other problems
definition['error'] = err inner['error_msg'] = exc_info_to_str(sys.exc_info())
definition['data'] = xml_data inner['contents'] = xml_data
# TODO (vshnayder): Do we need a unique slug here? Just pick a random # TODO (vshnayder): Do we need a unique slug here? Just pick a random
# 64-bit num? # 64-bit num?
location = ['i4x', org, course, 'error', 'slug'] location = ['i4x', org, course, 'error', 'slug']
...@@ -71,10 +78,12 @@ class ErrorDescriptor(EditingDescriptor): ...@@ -71,10 +78,12 @@ class ErrorDescriptor(EditingDescriptor):
files, etc. That would just get re-wrapped on import. files, etc. That would just get re-wrapped on import.
''' '''
try: try:
xml = etree.fromstring(self.definition['data']) xml = etree.fromstring(self.definition['data']['contents'])
return etree.tostring(xml) return etree.tostring(xml)
except etree.XMLSyntaxError: except etree.XMLSyntaxError:
# still not valid. # still not valid.
root = etree.Element('error') root = etree.Element('error')
root.text = self.definition['data'] root.text = self.definition['data']['contents']
err_node = etree.SubElement(root, 'error_msg')
err_node.text = self.definition['data']['error_msg']
return etree.tostring(root) return etree.tostring(root)
...@@ -8,6 +8,12 @@ log = logging.getLogger(__name__) ...@@ -8,6 +8,12 @@ log = logging.getLogger(__name__)
ErrorLog = namedtuple('ErrorLog', 'tracker errors') ErrorLog = namedtuple('ErrorLog', 'tracker errors')
def exc_info_to_str(exc_info):
"""Given some exception info, convert it into a string using
the traceback.format_exception() function.
"""
return ''.join(traceback.format_exception(*exc_info))
def in_exception_handler(): def in_exception_handler():
'''Is there an active exception?''' '''Is there an active exception?'''
return sys.exc_info() != (None, None, None) return sys.exc_info() != (None, None, None)
...@@ -27,7 +33,7 @@ def make_error_tracker(): ...@@ -27,7 +33,7 @@ def make_error_tracker():
'''Log errors''' '''Log errors'''
exc_str = '' exc_str = ''
if in_exception_handler(): if in_exception_handler():
exc_str = ''.join(traceback.format_exception(*sys.exc_info())) exc_str = exc_info_to_str(sys.exc_info())
errors.append((msg, exc_str)) errors.append((msg, exc_str))
......
...@@ -5,11 +5,11 @@ import os ...@@ -5,11 +5,11 @@ import os
import sys import sys
from lxml import etree from lxml import etree
from xmodule.x_module import XModule from .x_module import XModule
from xmodule.xml_module import XmlDescriptor from .xml_module import XmlDescriptor
from xmodule.editing_module import EditingDescriptor from .editing_module import EditingDescriptor
from stringify import stringify_children from .stringify import stringify_children
from html_checker import check_html from .html_checker import check_html
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
......
...@@ -3,10 +3,13 @@ This module provides an abstraction for working with XModuleDescriptors ...@@ -3,10 +3,13 @@ This module provides an abstraction for working with XModuleDescriptors
that are stored in a database an accessible using their Location as an identifier that are stored in a database an accessible using their Location as an identifier
""" """
import logging
import re import re
from collections import namedtuple from collections import namedtuple
from .exceptions import InvalidLocationError, InsufficientSpecificationError from .exceptions import InvalidLocationError, InsufficientSpecificationError
import logging from xmodule.errortracker import ErrorLog, make_error_tracker
log = logging.getLogger('mitx.' + 'modulestore') log = logging.getLogger('mitx.' + 'modulestore')
...@@ -290,3 +293,38 @@ class ModuleStore(object): ...@@ -290,3 +293,38 @@ class ModuleStore(object):
''' '''
raise NotImplementedError raise NotImplementedError
class ModuleStoreBase(ModuleStore):
'''
Implement interface functionality that can be shared.
'''
def __init__(self):
'''
Set up the error-tracking logic.
'''
self._location_errors = {} # location -> ErrorLog
def _get_errorlog(self, location):
"""
If we already have an errorlog for this location, return it. Otherwise,
create one.
"""
location = Location(location)
if location not in self._location_errors:
self._location_errors[location] = make_error_tracker()
return self._location_errors[location]
def get_item_errors(self, location):
"""
Return list of errors for this location, if any. Raise the same
errors as get_item if location isn't present.
NOTE: For now, the only items that track errors are CourseDescriptors in
the xml datastore. This will return an empty list for all other items
and datastores.
"""
# check that item is present and raise the promised exceptions if needed
self.get_item(location)
errorlog = self._get_errorlog(location)
return errorlog.errors
...@@ -11,7 +11,7 @@ from xmodule.x_module import XModuleDescriptor ...@@ -11,7 +11,7 @@ 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 mitxmako.shortcuts import render_to_string
from . import ModuleStore, Location from . import ModuleStoreBase, Location
from .exceptions import (ItemNotFoundError, from .exceptions import (ItemNotFoundError,
NoPathToItem, DuplicateItemError) NoPathToItem, DuplicateItemError)
...@@ -38,7 +38,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem): ...@@ -38,7 +38,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
resources_fs: a filesystem, as per MakoDescriptorSystem resources_fs: a filesystem, as per MakoDescriptorSystem
error_tracker: error_tracker: a function that logs errors for later display to users
render_template: a function for rendering templates, as per render_template: a function for rendering templates, as per
MakoDescriptorSystem MakoDescriptorSystem
...@@ -73,7 +73,7 @@ def location_to_query(location): ...@@ -73,7 +73,7 @@ def location_to_query(location):
return query return query
class MongoModuleStore(ModuleStore): class MongoModuleStore(ModuleStoreBase):
""" """
A Mongodb backed ModuleStore A Mongodb backed ModuleStore
""" """
...@@ -81,6 +81,9 @@ class MongoModuleStore(ModuleStore): ...@@ -81,6 +81,9 @@ class MongoModuleStore(ModuleStore):
# 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, port=27017, default_class=None,
error_tracker=null_error_tracker): error_tracker=null_error_tracker):
ModuleStoreBase.__init__(self)
self.collection = pymongo.connection.Connection( self.collection = pymongo.connection.Connection(
host=host, host=host,
port=port port=port
......
...@@ -12,7 +12,7 @@ from xmodule.course_module import CourseDescriptor ...@@ -12,7 +12,7 @@ from xmodule.course_module import CourseDescriptor
from xmodule.mako_module import MakoDescriptorSystem from xmodule.mako_module import MakoDescriptorSystem
from cStringIO import StringIO from cStringIO import StringIO
from . import ModuleStore, Location from . import ModuleStoreBase, Location
from .exceptions import ItemNotFoundError from .exceptions import ItemNotFoundError
etree.set_default_parser( etree.set_default_parser(
...@@ -98,7 +98,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): ...@@ -98,7 +98,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
error_tracker, process_xml, **kwargs) error_tracker, process_xml, **kwargs)
class XMLModuleStore(ModuleStore): class XMLModuleStore(ModuleStoreBase):
""" """
An XML backed ModuleStore An XML backed ModuleStore
""" """
...@@ -118,13 +118,12 @@ class XMLModuleStore(ModuleStore): ...@@ -118,13 +118,12 @@ class XMLModuleStore(ModuleStore):
course_dirs: If specified, the list of course_dirs to load. Otherwise, course_dirs: If specified, the list of course_dirs to load. Otherwise,
load all course dirs load all course dirs
""" """
ModuleStoreBase.__init__(self)
self.eager = eager self.eager = eager
self.data_dir = path(data_dir) self.data_dir = path(data_dir)
self.modules = {} # location -> XModuleDescriptor self.modules = {} # location -> XModuleDescriptor
self.courses = {} # course_dir -> XModuleDescriptor for the course self.courses = {} # course_dir -> XModuleDescriptor for the course
self.location_errors = {} # location -> ErrorLog
if default_class is None: if default_class is None:
self.default_class = None self.default_class = None
...@@ -148,12 +147,14 @@ class XMLModuleStore(ModuleStore): ...@@ -148,12 +147,14 @@ class XMLModuleStore(ModuleStore):
for course_dir in course_dirs: for course_dir in course_dirs:
try: try:
# make a tracker, then stick in the right place once the course loads # Special-case code here, since we don't have a location for the
# and we know its location # course before it loads.
# So, make a tracker to track load-time errors, then put in the right
# place after the course loads and we have its location
errorlog = make_error_tracker() errorlog = make_error_tracker()
course_descriptor = self.load_course(course_dir, errorlog.tracker) course_descriptor = self.load_course(course_dir, errorlog.tracker)
self.courses[course_dir] = course_descriptor self.courses[course_dir] = course_descriptor
self.location_errors[course_descriptor.location] = errorlog self._location_errors[course_descriptor.location] = errorlog
except: except:
msg = "Failed to load course '%s'" % course_dir msg = "Failed to load course '%s'" % course_dir
log.exception(msg) log.exception(msg)
...@@ -221,23 +222,6 @@ class XMLModuleStore(ModuleStore): ...@@ -221,23 +222,6 @@ class XMLModuleStore(ModuleStore):
raise ItemNotFoundError(location) raise ItemNotFoundError(location)
def get_item_errors(self, location):
"""
Return list of errors for this location, if any. Raise the same
errors as get_item if location isn't present.
NOTE: This only actually works for courses in the xml datastore--
will return an empty list for all other modules.
"""
location = Location(location)
# check that item is present
self.get_item(location)
# now look up errors
if location in self.location_errors:
return self.location_errors[location].errors
return []
def get_courses(self, depth=0): def get_courses(self, depth=0):
""" """
Returns a list of course descriptors. If there were errors on loading, Returns a list of course descriptors. If there were errors on loading,
...@@ -245,9 +229,11 @@ class XMLModuleStore(ModuleStore): ...@@ -245,9 +229,11 @@ class XMLModuleStore(ModuleStore):
""" """
return self.courses.values() return self.courses.values()
def create_item(self, location): def create_item(self, location):
raise NotImplementedError("XMLModuleStores are read-only") raise NotImplementedError("XMLModuleStores are read-only")
def update_item(self, location, data): def update_item(self, location, data):
""" """
Set the data in the item specified by the location to Set the data in the item specified by the location to
...@@ -258,6 +244,7 @@ class XMLModuleStore(ModuleStore): ...@@ -258,6 +244,7 @@ class XMLModuleStore(ModuleStore):
""" """
raise NotImplementedError("XMLModuleStores are read-only") raise NotImplementedError("XMLModuleStores are read-only")
def update_children(self, location, children): def update_children(self, location, children):
""" """
Set the children for the item specified by the location to Set the children for the item specified by the location to
...@@ -268,6 +255,7 @@ class XMLModuleStore(ModuleStore): ...@@ -268,6 +255,7 @@ class XMLModuleStore(ModuleStore):
""" """
raise NotImplementedError("XMLModuleStores are read-only") raise NotImplementedError("XMLModuleStores are read-only")
def update_metadata(self, location, metadata): def update_metadata(self, location, metadata):
""" """
Set the metadata for the item specified by the location to Set the metadata for the item specified by the location to
......
...@@ -73,6 +73,7 @@ class ImportTestCase(unittest.TestCase): ...@@ -73,6 +73,7 @@ class ImportTestCase(unittest.TestCase):
def test_reimport(self): def test_reimport(self):
'''Make sure an already-exported error xml tag loads properly''' '''Make sure an already-exported error xml tag loads properly'''
self.maxDiff = None
bad_xml = '''<sequential display_name="oops"><video url="hi"></sequential>''' bad_xml = '''<sequential display_name="oops"><video url="hi"></sequential>'''
system = self.get_system() system = self.get_system()
descriptor = XModuleDescriptor.load_from_xml(bad_xml, system, 'org', 'course', descriptor = XModuleDescriptor.load_from_xml(bad_xml, system, 'org', 'course',
......
from nose.tools import assert_equals from nose.tools import assert_equals
from lxml import etree from lxml import etree
from stringify import stringify_children from xmodule.stringify import stringify_children
def test_stringify(): def test_stringify():
html = '''<html a="b" foo="bar">Hi <div x="foo">there <span>Bruce</span><b>!</b></div></html>''' text = 'Hi <div x="foo">there <span>Bruce</span><b>!</b></div>'
html = '''<html a="b" foo="bar">{0}</html>'''.format(text)
xml = etree.fromstring(html) xml = etree.fromstring(html)
out = stringify_children(xml) out = stringify_children(xml)
assert_equals(out, '''Hi <div x="foo">there <span>Bruce</span><b>!</b></div>''') assert_equals(out, text)
from lxml import etree
from lxml.etree import XMLSyntaxError
import pkg_resources
import logging import logging
import pkg_resources
import sys
from fs.errors import ResourceNotFoundError from fs.errors import ResourceNotFoundError
from functools import partial from functools import partial
from lxml import etree
from lxml.etree import XMLSyntaxError
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.errortracker import exc_info_to_str
log = logging.getLogger('mitx.' + __name__) log = logging.getLogger('mitx.' + __name__)
...@@ -471,7 +473,9 @@ class XModuleDescriptor(Plugin, HTMLSnippet): ...@@ -471,7 +473,9 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
msg = "Error loading from xml." msg = "Error loading from xml."
log.exception(msg) log.exception(msg)
system.error_tracker(msg) system.error_tracker(msg)
descriptor = ErrorDescriptor.from_xml(xml_data, system, org, course, err) err_msg = msg + "\n" + exc_info_to_str(sys.exc_info())
descriptor = ErrorDescriptor.from_xml(xml_data, system, org, course,
err_msg)
return descriptor return descriptor
......
...@@ -257,6 +257,7 @@ def index(request, course_id, chapter=None, section=None, ...@@ -257,6 +257,7 @@ def index(request, course_id, chapter=None, section=None,
return result return result
@ensure_csrf_cookie @ensure_csrf_cookie
def jump_to(request, location): def jump_to(request, location):
''' '''
...@@ -283,7 +284,7 @@ def jump_to(request, location): ...@@ -283,7 +284,7 @@ def jump_to(request, location):
except NoPathToItem: except NoPathToItem:
raise Http404("This location is not in any class: {0}".format(location)) raise Http404("This location is not in any class: {0}".format(location))
# Rely on index to do all error handling
return index(request, course_id, chapter, section, position) return index(request, course_id, chapter, section, position)
@ensure_csrf_cookie @ensure_csrf_cookie
......
...@@ -15,6 +15,7 @@ from django.core.files.storage import get_storage_class ...@@ -15,6 +15,7 @@ from django.core.files.storage import get_storage_class
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.conf import settings from django.conf import settings
from mitxmako.shortcuts import render_to_response, render_to_string
from django_comment_client.utils import JsonResponse, JsonError, extract from django_comment_client.utils import JsonResponse, JsonError, extract
def thread_author_only(fn): def thread_author_only(fn):
...@@ -58,6 +59,16 @@ def create_thread(request, course_id, commentable_id): ...@@ -58,6 +59,16 @@ def create_thread(request, course_id, commentable_id):
if request.POST.get('autowatch', 'false').lower() == 'true': if request.POST.get('autowatch', 'false').lower() == 'true':
attributes['auto_subscribe'] = True attributes['auto_subscribe'] = True
response = comment_client.create_thread(commentable_id, attributes) response = comment_client.create_thread(commentable_id, attributes)
if request.is_ajax():
context = {
'course_id': course_id,
'thread': response,
}
html = render_to_string('discussion/ajax_thread_only.html', context)
return JsonResponse({
'html': html,
})
else:
return JsonResponse(response) return JsonResponse(response)
@thread_author_only @thread_author_only
...@@ -68,9 +79,7 @@ def update_thread(request, course_id, thread_id): ...@@ -68,9 +79,7 @@ def update_thread(request, course_id, thread_id):
response = comment_client.update_thread(thread_id, attributes) response = comment_client.update_thread(thread_id, attributes)
return JsonResponse(response) return JsonResponse(response)
@login_required def _create_comment(request, course_id, _response_from_attributes):
@require_POST
def create_comment(request, course_id, thread_id):
attributes = extract(request.POST, ['body']) attributes = extract(request.POST, ['body'])
attributes['user_id'] = request.user.id attributes['user_id'] = request.user.id
attributes['course_id'] = course_id attributes['course_id'] = course_id
...@@ -78,9 +87,25 @@ def create_comment(request, course_id, thread_id): ...@@ -78,9 +87,25 @@ def create_comment(request, course_id, thread_id):
attributes['anonymous'] = True attributes['anonymous'] = True
if request.POST.get('autowatch', 'false').lower() == 'true': if request.POST.get('autowatch', 'false').lower() == 'true':
attributes['auto_subscribe'] = True attributes['auto_subscribe'] = True
response = comment_client.create_comment(thread_id, attributes) response = _response_from_attributes(attributes)
if request.is_ajax():
context = {
'comment': response,
}
html = render_to_string('discussion/ajax_comment_only.html', context)
return JsonResponse({
'html': html,
})
else:
return JsonResponse(response) return JsonResponse(response)
@login_required
@require_POST
def create_comment(request, course_id, thread_id):
def _response_from_attributes(attributes):
return comment_client.create_comment(thread_id, attributes)
return _create_comment(request, course_id, _response_from_attributes)
@thread_author_only @thread_author_only
@login_required @login_required
@require_POST @require_POST
...@@ -107,15 +132,9 @@ def endorse_comment(request, course_id, comment_id): ...@@ -107,15 +132,9 @@ def endorse_comment(request, course_id, comment_id):
@login_required @login_required
@require_POST @require_POST
def create_sub_comment(request, course_id, comment_id): def create_sub_comment(request, course_id, comment_id):
attributes = extract(request.POST, ['body']) def _response_from_attributes(attributes):
attributes['user_id'] = request.user.id return comment_client.create_sub_comment(comment_id, attributes)
attributes['course_id'] = course_id return _create_comment(request, course_id, _response_from_attributes)
if request.POST.get('anonymous', 'false').lower() == 'true':
attributes['anonymous'] = True
if request.POST.get('autowatch', 'false').lower() == 'true':
attributes['auto_subscribe'] = True
response = comment_client.create_sub_comment(comment_id, attributes)
return JsonResponse(response)
@comment_author_only @comment_author_only
@login_required @login_required
......
...@@ -11,9 +11,7 @@ from courseware.courses import check_course ...@@ -11,9 +11,7 @@ from courseware.courses import check_course
from dateutil.tz import tzlocal from dateutil.tz import tzlocal
from datehelper import time_ago_in_words from datehelper import time_ago_in_words
from django_comment_client.utils import get_categorized_discussion_info, \ import django_comment_client.utils as utils
extract, strip_none, \
JsonResponse
from urllib import urlencode from urllib import urlencode
import json import json
...@@ -24,13 +22,9 @@ import dateutil ...@@ -24,13 +22,9 @@ import dateutil
THREADS_PER_PAGE = 20 THREADS_PER_PAGE = 20
PAGES_NEARBY_DELTA = 2 PAGES_NEARBY_DELTA = 2
class HtmlResponse(HttpResponse):
def __init__(self, html=''):
super(HtmlResponse, self).__init__(html, content_type='text/plain')
def render_accordion(request, course, discussion_id): def render_accordion(request, course, discussion_id):
discussion_info = get_categorized_discussion_info(request, course) discussion_info = utils.get_categorized_discussion_info(request, course)
context = { context = {
'course': course, 'course': course,
...@@ -63,7 +57,7 @@ def render_discussion(request, course_id, threads, discussion_id=None, \ ...@@ -63,7 +57,7 @@ def render_discussion(request, course_id, threads, discussion_id=None, \
'pages_nearby_delta': PAGES_NEARBY_DELTA, 'pages_nearby_delta': PAGES_NEARBY_DELTA,
'discussion_type': discussion_type, 'discussion_type': discussion_type,
'base_url': base_url, 'base_url': base_url,
'query_params': strip_none(extract(query_params, ['page', 'sort_key', 'sort_order', 'tags', 'text'])), 'query_params': utils.strip_none(utils.extract(query_params, ['page', 'sort_key', 'sort_order', 'tags', 'text'])),
} }
context = dict(context.items() + query_params.items()) context = dict(context.items() + query_params.items())
return render_to_string(template, context) return render_to_string(template, context)
...@@ -86,9 +80,9 @@ def get_threads(request, course_id, discussion_id): ...@@ -86,9 +80,9 @@ def get_threads(request, course_id, discussion_id):
if query_params['text'] or query_params['tags']: #TODO do tags search without sunspot if query_params['text'] or query_params['tags']: #TODO do tags search without sunspot
query_params['commentable_id'] = discussion_id query_params['commentable_id'] = discussion_id
threads, page, num_pages = comment_client.search_threads(course_id, recursive=False, query_params=strip_none(query_params)) threads, page, num_pages = comment_client.search_threads(course_id, recursive=False, query_params=utils.strip_none(query_params))
else: else:
threads, page, num_pages = comment_client.get_threads(discussion_id, recursive=False, query_params=strip_none(query_params)) threads, page, num_pages = comment_client.get_threads(discussion_id, recursive=False, query_params=utils.strip_none(query_params))
query_params['page'] = page query_params['page'] = page
query_params['num_pages'] = num_pages query_params['num_pages'] = num_pages
...@@ -162,7 +156,7 @@ def single_thread(request, course_id, discussion_id, thread_id): ...@@ -162,7 +156,7 @@ def single_thread(request, course_id, discussion_id, thread_id):
context = {'thread': thread} context = {'thread': thread}
html = render_to_string('discussion/_ajax_single_thread.html', context) html = render_to_string('discussion/_ajax_single_thread.html', context)
return JsonResponse({ return utils.JsonResponse({
'html': html, 'html': html,
'annotated_content_info': annotated_content_info, 'annotated_content_info': annotated_content_info,
}) })
......
...@@ -116,3 +116,7 @@ class JsonError(HttpResponse): ...@@ -116,3 +116,7 @@ class JsonError(HttpResponse):
ensure_ascii=False) ensure_ascii=False)
super(JsonError, self).__init__(content, super(JsonError, self).__init__(content,
mimetype='application/json; charset=utf8') mimetype='application/json; charset=utf8')
class HtmlResponse(HttpResponse):
def __init__(self, html=''):
super(HtmlResponse, self).__init__(html, content_type='text/plain')
...@@ -174,3 +174,4 @@ $ -> ...@@ -174,3 +174,4 @@ $ ->
text: text text: text
previewSetter: previewSet previewSetter: previewSet
editor.run() editor.run()
editor
...@@ -18,12 +18,13 @@ Discussion = @Discussion ...@@ -18,12 +18,13 @@ Discussion = @Discussion
if $replyView.length if $replyView.length
$replyView.show() $replyView.show()
else else
thread_id = $discussionContent.parents(".thread").attr("_id")
view = { view = {
id: id id: id
showWatchCheckbox: $discussionContent.parents(".thread").attr("_id") not in $$user_info.subscribed_thread_ids showWatchCheckbox: not Discussion.isSubscribed(thread_id, "thread")
} }
$discussionContent.append Mustache.render Discussion.replyTemplate, view $discussionContent.append Mustache.render Discussion.replyTemplate, view
Markdown.makeWmdEditor $local(".reply-body"), "-reply-body-#{id}", Discussion.urlFor('upload') Discussion.makeWmdEditor $content, $local, "reply-body"
$local(".discussion-submit-post").click -> handleSubmitReply(this) $local(".discussion-submit-post").click -> handleSubmitReply(this)
$local(".discussion-cancel-post").click -> handleCancelReply(this) $local(".discussion-cancel-post").click -> handleCancelReply(this)
$local(".discussion-link").hide() $local(".discussion-link").hide()
...@@ -33,8 +34,8 @@ Discussion = @Discussion ...@@ -33,8 +34,8 @@ Discussion = @Discussion
$replyView = $local(".discussion-reply-new") $replyView = $local(".discussion-reply-new")
if $replyView.length if $replyView.length
$replyView.hide() $replyView.hide()
reply = Discussion.generateDiscussionLink("discussion-reply", "Reply", handleReply) #reply = Discussion.generateDiscussionLink("discussion-reply", "Reply", handleReply)
$(elem).replaceWith(reply) #$(elem).replaceWith(reply)
$discussionContent.attr("status", "normal") $discussionContent.attr("status", "normal")
handleSubmitReply = (elem) -> handleSubmitReply = (elem) ->
...@@ -45,26 +46,30 @@ Discussion = @Discussion ...@@ -45,26 +46,30 @@ Discussion = @Discussion
else else
return return
body = $local("#wmd-input-reply-body-#{id}").val() body = Discussion.getWmdContent $content, $local, "reply-body"
anonymous = false || $local(".discussion-post-anonymously").is(":checked") anonymous = false || $local(".discussion-post-anonymously").is(":checked")
autowatch = false || $local(".discussion-auto-watch").is(":checked") autowatch = false || $local(".discussion-auto-watch").is(":checked")
Discussion.safeAjax Discussion.safeAjax
$elem: $(elem)
url: url url: url
type: "POST" type: "POST"
dataType: 'json'
data: data:
body: body body: body
anonymous: anonymous anonymous: anonymous
autowatch: autowatch autowatch: autowatch
success: (response, textStatus) -> success: Discussion.formErrorHandler($local(".discussion-errors"), (response, textStatus) ->
if response.errors? and response.errors.length > 0 console.log response
errorsField = $local(".discussion-errors").empty() $comment = $(response.html)
for error in response.errors $content.children(".comments").prepend($comment)
errorsField.append($("<li>").addClass("new-post-form-error").html(error)) Discussion.setWmdContent $content, $local, "reply-body", ""
else Discussion.initializeContent($comment)
Discussion.handleAnchorAndReload(response) Discussion.bindContentEvents($comment)
dataType: 'json' $local(".discussion-reply-new").hide()
$discussionContent.attr("status", "normal")
)
handleVote = (elem, value) -> handleVote = (elem, value) ->
contentType = if $content.hasClass("thread") then "thread" else "comment" contentType = if $content.hasClass("thread") then "thread" else "comment"
...@@ -91,7 +96,7 @@ Discussion = @Discussion ...@@ -91,7 +96,7 @@ Discussion = @Discussion
tags: $local(".thread-raw-tags").html() tags: $local(".thread-raw-tags").html()
} }
$discussionContent.append Mustache.render Discussion.editThreadTemplate, view $discussionContent.append Mustache.render Discussion.editThreadTemplate, view
Markdown.makeWmdEditor $local(".thread-body-edit"), "-thread-body-edit-#{id}", Discussion.urlFor('update_thread', id) Discussion.makeWmdEditor $content, $local, "thread-body-edit"
$local(".thread-tags-edit").tagsInput $local(".thread-tags-edit").tagsInput
autocomplete_url: Discussion.urlFor('tags_autocomplete') autocomplete_url: Discussion.urlFor('tags_autocomplete')
autocomplete: autocomplete:
...@@ -107,16 +112,16 @@ Discussion = @Discussion ...@@ -107,16 +112,16 @@ Discussion = @Discussion
handleSubmitEditThread = (elem) -> handleSubmitEditThread = (elem) ->
url = Discussion.urlFor('update_thread', id) url = Discussion.urlFor('update_thread', id)
title = $local(".thread-title-edit").val() title = $local(".thread-title-edit").val()
body = $local("#wmd-input-thread-body-edit-#{id}").val() body = Discussion.getWmdContent $content, $local, "thread-body-edit"
tags = $local(".thread-tags-edit").val() tags = $local(".thread-tags-edit").val()
$.post url, {title: title, body: body, tags: tags}, (response, textStatus) -> $.ajax
if response.errors url: url
errorsField = $local(".discussion-update-errors").empty() type: "POST"
for error in response.errors dataType: 'json'
errorsField.append($("<li>").addClass("new-post-form-error").html(error)) data: {title: title, body: body, tags: tags},
else success: Discussion.formErrorHandler($local(".discussion-update-errors"), (response, textStatus) ->
Discussion.handleAnchorAndReload(response) Discussion.handleAnchorAndReload(response)
, 'json' )
handleEditComment = (elem) -> handleEditComment = (elem) ->
$local(".discussion-content-wrapper").hide() $local(".discussion-content-wrapper").hide()
...@@ -124,26 +129,23 @@ Discussion = @Discussion ...@@ -124,26 +129,23 @@ Discussion = @Discussion
if $editView.length if $editView.length
$editView.show() $editView.show()
else else
view = { view = { id: id, body: $local(".comment-raw-body").html() }
id: id
body: $local(".comment-raw-body").html()
}
$discussionContent.append Mustache.render Discussion.editCommentTemplate, view $discussionContent.append Mustache.render Discussion.editCommentTemplate, view
Markdown.makeWmdEditor $local(".comment-body-edit"), "-comment-body-edit-#{id}", Discussion.urlFor('update_comment', id) Discussion.makeWmdEditor $content, $local, "comment-body-edit"
$local(".discussion-submit-update").unbind("click").click -> handleSubmitEditComment(this) $local(".discussion-submit-update").unbind("click").click -> handleSubmitEditComment(this)
$local(".discussion-cancel-update").unbind("click").click -> handleCancelEdit(this) $local(".discussion-cancel-update").unbind("click").click -> handleCancelEdit(this)
handleSubmitEditComment= (elem) -> handleSubmitEditComment= (elem) ->
url = Discussion.urlFor('update_comment', id) url = Discussion.urlFor('update_comment', id)
body = $local("#wmd-input-comment-body-edit-#{id}").val() body = Discussion.getWmdContent $content, $local, "comment-body-edit"
$.post url, {body: body}, (response, textStatus) -> $.ajax
if response.errors url: url
errorsField = $local(".discussion-update-errors").empty() type: "POST"
for error in response.errors dataType: "json"
errorsField.append($("<li>").addClass("new-post-form-error").html(error)) data: {body: body}
else success: Discussion.formErrorHandler($local(".discussion-update-errors"), (response, textStatus) ->
Discussion.handleAnchorAndReload(response) Discussion.handleAnchorAndReload(response)
, 'json' )
handleEndorse = (elem) -> handleEndorse = (elem) ->
url = Discussion.urlFor('endorse_comment', id) url = Discussion.urlFor('endorse_comment', id)
...@@ -166,6 +168,9 @@ Discussion = @Discussion ...@@ -166,6 +168,9 @@ Discussion = @Discussion
$threadTitle = $local(".thread-title") $threadTitle = $local(".thread-title")
$showComments = $local(".discussion-show-comments") $showComments = $local(".discussion-show-comments")
if not $showComments.length or not $threadTitle.length
return
rebindHideEvents = -> rebindHideEvents = ->
$threadTitle.unbind('click').click handleHideSingleThread $threadTitle.unbind('click').click handleHideSingleThread
$showComments.unbind('click').click handleHideSingleThread $showComments.unbind('click').click handleHideSingleThread
...@@ -182,6 +187,7 @@ Discussion = @Discussion ...@@ -182,6 +187,7 @@ Discussion = @Discussion
$elem: $.merge($threadTitle, $showComments) $elem: $.merge($threadTitle, $showComments)
url: url url: url
type: "GET" type: "GET"
dataType: 'json'
success: (response, textStatus) -> success: (response, textStatus) ->
if not $$annotated_content_info? if not $$annotated_content_info?
window.$$annotated_content_info = {} window.$$annotated_content_info = {}
...@@ -191,33 +197,35 @@ Discussion = @Discussion ...@@ -191,33 +197,35 @@ Discussion = @Discussion
Discussion.initializeContent(comment) Discussion.initializeContent(comment)
Discussion.bindContentEvents(comment) Discussion.bindContentEvents(comment)
rebindHideEvents() rebindHideEvents()
dataType: 'json'
Discussion.bindLocalEvents $local,
$local(".thread-title").click handleShowSingleThread "click .thread-title": ->
handleShowSingleThread(this)
$local(".discussion-show-comments").click handleShowSingleThread "click .discussion-show-comments": ->
handleShowSingleThread(this)
$local(".discussion-reply-thread").click -> "click .discussion-reply-thread": ->
handleShowSingleThread($local(".thread-title")) handleShowSingleThread($local(".thread-title"))
handleReply(this) handleReply(this)
$local(".discussion-reply-comment").click -> "click .discussion-reply-comment": ->
handleReply(this) handleReply(this)
$local(".discussion-cancel-reply").click -> "click .discussion-cancel-reply": ->
handleCancelReply(this) handleCancelReply(this)
$local(".discussion-vote-up").click -> "click .discussion-vote-up": ->
handleVote(this, "up") handleVote(this, "up")
$local(".discussion-vote-down").click -> "click .discussion-vote-down": ->
handleVote(this, "down") handleVote(this, "down")
$local(".discussion-endorse").click -> "click .discussion-endorse": ->
handleEndorse(this) handleEndorse(this)
$local(".discussion-edit").click -> "click .discussion-edit": ->
if $content.hasClass("thread") if $content.hasClass("thread")
handleEditThread(this) handleEditThread(this)
else else
......
...@@ -91,17 +91,28 @@ initializeFollowThread = (index, thread) -> ...@@ -91,17 +91,28 @@ initializeFollowThread = (index, thread) ->
handleSubmitNewPost = (elem) -> handleSubmitNewPost = (elem) ->
title = $local(".new-post-title").val() title = $local(".new-post-title").val()
body = $local("#wmd-input-new-post-body-#{id}").val() body = Discussion.getWmdContent $discussion, $local, "new-post-body"
tags = $local(".new-post-tags").val() tags = $local(".new-post-tags").val()
url = Discussion.urlFor('create_thread', $local(".new-post-form").attr("_id")) url = Discussion.urlFor('create_thread', $local(".new-post-form").attr("_id"))
$.post url, {title: title, body: body, tags: tags}, (response, textStatus) -> Discussion.safeAjax
if response.errors $elem: $(elem)
errorsField = $local(".discussion-errors").empty() url: url
for error in response.errors type: "POST"
errorsField.append($("<li>").addClass("new-post-form-error").html(error)) dataType: 'json'
else data:
Discussion.handleAnchorAndReload(response) title: title
, 'json' body: body
tags: tags
success: Discussion.formErrorHandler($local(".new-post-form-error"), (response, textStatus) ->
console.log response
$thread = $(response.html)
$discussion.children(".threads").prepend($thread)
Discussion.setWmdContent $discussion, $local, "new-post-body", ""
Discussion.initializeContent($thread)
Discussion.bindContentEvents($thread)
$(".new-post-form").hide()
$local(".discussion-new-post").show()
)
handleCancelNewPost = (elem) -> handleCancelNewPost = (elem) ->
$local(".new-post-form").hide() $local(".new-post-form").hide()
...@@ -115,9 +126,9 @@ initializeFollowThread = (index, thread) -> ...@@ -115,9 +126,9 @@ initializeFollowThread = (index, thread) ->
else else
view = { discussion_id: id } view = { discussion_id: id }
$discussionNonContent.append Mustache.render Discussion.newPostTemplate, view $discussionNonContent.append Mustache.render Discussion.newPostTemplate, view
newPostBody = $(discussion).find(".new-post-body") newPostBody = $discussion.find(".new-post-body")
if newPostBody.length if newPostBody.length
Markdown.makeWmdEditor newPostBody, "-new-post-body-#{$(discussion).attr('_id')}", Discussion.urlFor('upload') Discussion.makeWmdEditor $discussion, $local, "new-post-body"
$local(".new-post-tags").tagsInput Discussion.tagsInputOptions() $local(".new-post-tags").tagsInput Discussion.tagsInputOptions()
...@@ -128,8 +139,6 @@ initializeFollowThread = (index, thread) -> ...@@ -128,8 +139,6 @@ initializeFollowThread = (index, thread) ->
$(elem).hide() $(elem).hide()
handleUpdateDiscussionContent = ($elem, $discussion, url) ->
handleAjaxSearch = (elem) -> handleAjaxSearch = (elem) ->
handle handle
$elem = $(elem) $elem = $(elem)
......
...@@ -3,7 +3,9 @@ if not @Discussion? ...@@ -3,7 +3,9 @@ if not @Discussion?
Discussion = @Discussion Discussion = @Discussion
@Discussion = $.extend @Discussion, @Discussion = $.extend @Discussion,
newPostTemplate: """ newPostTemplate: """
<form class="new-post-form" _id="{{discussion_id}}"> <form class="new-post-form" _id="{{discussion_id}}">
<ul class="discussion-errors"></ul> <ul class="discussion-errors"></ul>
......
...@@ -3,6 +3,8 @@ if not @Discussion? ...@@ -3,6 +3,8 @@ if not @Discussion?
Discussion = @Discussion Discussion = @Discussion
wmdEditors = {}
@Discussion = $.extend @Discussion, @Discussion = $.extend @Discussion,
generateLocal: (elem) -> generateLocal: (elem) ->
...@@ -65,3 +67,45 @@ Discussion = @Discussion ...@@ -65,3 +67,45 @@ Discussion = @Discussion
height: "30px" height: "30px"
width: "100%" width: "100%"
removeWithBackspace: true removeWithBackspace: true
isSubscribed: (id, type) ->
if type == "thread"
id in $$user_info.subscribed_thread_ids
else if type == "commentable" or type == "discussion"
id in $$user_info.subscribed_commentable_ids
else
id in $$user_info.subscribed_user_ids
formErrorHandler: (errorsField, success) ->
(response, textStatus, xhr) ->
if response.errors? and response.errors.length > 0
errorsField.empty()
for error in response.errors
errorsField.append($("<li>").addClass("new-post-form-error").html(error))
else
success(response, textStatus, xhr)
makeWmdEditor: ($content, $local, cls_identifier) ->
elem = $local(".#{cls_identifier}")
id = $content.attr("_id")
appended_id = "-#{cls_identifier}-#{id}"
imageUploadUrl = Discussion.urlFor('upload')
editor = Markdown.makeWmdEditor elem, appended_id, imageUploadUrl
wmdEditors["#{cls_identifier}-#{id}"] = editor
console.log wmdEditors
editor
getWmdEditor: ($content, $local, cls_identifier) ->
id = $content.attr("_id")
wmdEditors["#{cls_identifier}-#{id}"]
getWmdContent: ($content, $local, cls_identifier) ->
id = $content.attr("_id")
$local("#wmd-input-#{cls_identifier}-#{id}").val()
setWmdContent: ($content, $local, cls_identifier, text) ->
id = $content.attr("_id")
$local("#wmd-input-#{cls_identifier}-#{id}").val(text)
console.log wmdEditors
console.log "#{cls_identifier}-#{id}"
wmdEditors["#{cls_identifier}-#{id}"].refreshPreview()
...@@ -15,9 +15,11 @@ ...@@ -15,9 +15,11 @@
<div class="discussion-new-post control-button" href="javascript:void(0)">New Post</div> <div class="discussion-new-post control-button" href="javascript:void(0)">New Post</div>
</div> </div>
<%include file="_sort.html" /> <%include file="_sort.html" />
<div class="threads">
% for thread in threads: % for thread in threads:
${renderer.render_thread(course_id, thread, edit_thread=False, show_comments=False)} ${renderer.render_thread(course_id, thread, edit_thread=False, show_comments=False)}
% endfor % endfor
</div>
<%include file="_paginator.html" /> <%include file="_paginator.html" />
</section> </section>
......
...@@ -8,9 +8,11 @@ ...@@ -8,9 +8,11 @@
<div class="discussion-new-post control-button" href="javascript:void(0)">New Post</div> <div class="discussion-new-post control-button" href="javascript:void(0)">New Post</div>
<%include file="_sort.html" /> <%include file="_sort.html" />
</div> </div>
<div class="threads">
% for thread in threads: % for thread in threads:
${renderer.render_thread(course_id, thread, edit_thread=False, show_comments=False)} ${renderer.render_thread(course_id, thread, edit_thread=False, show_comments=False)}
% endfor % endfor
</div>
<%include file="_paginator.html" /> <%include file="_paginator.html" />
</section> </section>
......
...@@ -7,14 +7,12 @@ ...@@ -7,14 +7,12 @@
<div class="thread" _id="${thread['id']}"> <div class="thread" _id="${thread['id']}">
${render_content(thread, "thread", edit_thread=edit_thread, show_comments=show_comments)} ${render_content(thread, "thread", edit_thread=edit_thread, show_comments=show_comments)}
% if show_comments: % if show_comments:
${render_comments(thread['children'])} ${render_comments(thread.get('children', []))}
% endif % endif
</div> </div>
</%def> </%def>
<%def name="render_comments(comments)"> <%def name="render_comment(comment)">
<div class="comments">
% for comment in comments:
% if comment['endorsed']: % if comment['endorsed']:
<div class="comment endorsed" _id="${comment['id']}"> <div class="comment endorsed" _id="${comment['id']}">
% else: % else:
...@@ -22,9 +20,15 @@ ...@@ -22,9 +20,15 @@
% endif % endif
${render_content(comment, "comment")} ${render_content(comment, "comment")}
<div class="comments"> <div class="comments">
${render_comments(comment['children'])} ${render_comments(comment.get('children', []))}
</div> </div>
</div> </div>
</%def>
<%def name="render_comments(comments)">
<div class="comments">
% for comment in comments:
${render_comment(comment)}
% endfor % endfor
</div> </div>
</%def> </%def>
......
<%namespace name="renderer" file="_thread.html"/>
${renderer.render_comment(comment)}
<%namespace name="renderer" file="_thread.html"/>
${renderer.render_thread(course_id, thread, edit_thread=True, show_comments=False)}
<section class="outside-app">
<h1>There has been an error on the <em>MITx</em> servers</h1>
<p>We're sorry, this module is temporarily unavailable. Our staff is working to fix it as soon as possible. Please email us at <a href="mailto:technical@mitx.mit.edu">technical@mitx.mit.edu</a> to report any problems or downtime.</p>
<h1>Staff-only details below:</h1>
<p>Error: ${error}</p>
<p>Raw data: ${data}</p>
</section>
<section class="outside-app"> <section class="outside-app">
<h1>There has been an error on the <em>MITx</em> servers</h1> <h1>There has been an error on the <em>MITx</em> servers</h1>
<p>We're sorry, this module is temporarily unavailable. Our staff is working to fix it as soon as possible. Please email us at <a href="mailto:technical@mitx.mit.edu">technical@mitx.mit.edu</a> to report any problems or downtime.</p> <p>We're sorry, this module is temporarily unavailable. Our staff is working to fix it as soon as possible. Please email us at <a href="mailto:technical@mitx.mit.edu">technical@mitx.mit.edu</a> to report any problems or downtime.</p>
% if is_staff:
<h1>Staff-only details below:</h1>
<p>Error: ${error | h}</p>
<p>Raw data: ${data | h}</p>
% endif
</section> </section>
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