Commits

Luke Plant committed 31952b4

Implemented combine_presentation and related fixes

Comments (0)

Files changed (6)

semanticeditor/media/semanticeditor/javascript/wymeditor/plugins/semantic/wymeditor.semantic.js

 function PresentationControls(wym) {
     this.name = wym._element.get(0).name;
     this.wym = wym;
-    // available_styles: an array of dictionaries corresponding
-    // to PresentationInfo objects
+    // available_styles: an array of dictionaries corresponding to PresentationInfo objects
     this.available_styles = new Array();
     // stored_headings: an array of 2 elements arrays, [heading level, heading name]
     this.stored_headings = new Array();
     // presentation_info: a dictionary of { heading name : [PresentationInfo] }
     this.presentation_info = {};
+
     this.setup_controls(jQuery(wym._box).find(".wym_area_bottom"));
 }
 
-/*
- * TODO
- * combine_presentation
- *  - called before teardown
- * update_optsbox
- *  - remove any existing event handlers
- *  - sets up control state from stored data
- *  - then add event handlers
- * optsbox event handlers
- *  - these need to store style data depending on what
- *    the user just changed
- * // wrappers for storing data:
- * add_style(heading, pres)
- * remove_style(heading, pres)
- * update_stored_headings
- *  - called when new info about headings arrives
- * current_heading
- */
-
 function escapeHtml(html) {
     return html.replace(/&/g,"&")
 	.replace(/\"/g,""")
     var headingsbox_id = id_prefix + 'headings';
     var optsbox_id = id_prefix + 'optsbox';
     var self = this;
+
+    // Create elements
     container.after(
 	"<div class=\"prescontrol\">" +
 	"<table width=\"100%\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td width=\"50%\"><div class=\"prescontrolheadings\">Headings:<br/><select size=\"7\" id=\"" + headingsbox_id + "\"></select></div></td>" +
     this.headingscontrol = jQuery('#' + headingsbox_id);
     this.errorbox = jQuery('#' + id_prefix + "errorbox");
     this.optsbox.css('height', this.headingscontrol.get(0).clientHeight.toString() + "px");
+
+    // Initial set up
+    this.retrieve_styles();
+    this.refresh_headings();
+    this.separate_presentation();
+    this.update_optsbox();
+
+    // Event handlers
+    this.headingscontrol.click(function(event) {
+	self.update_optsbox();
+    });
     jQuery("#" + id_prefix + "refresh").click(function(event) {
 						  self.refresh_headings();
 						  self.headingscontrol.get(0).focus();
 						  return false;
 					      });
-    this.retrieve_styles();
-    this.refresh_headings();
-    this.separate_presentation();
-    this.update_optsbox();
-    // Event handlers
-    this.headingscontrol.click(function(event) {
-	self.update_optsbox();
-    });
+
+    // Insert rewriting of HTML before the WYMeditor updates the textarea.
+    jQuery(this.wym._options.updateSelector)
+	.bind(this.wym._options.updateEvent, function(event) {
+		  self.form_submit(event);
+	      });
+
+    // Other event handlers added in update_optsbox
 };
 
-/*
- * Splits the HTML into 'content HTML' and 'presentation'
- */
+// Splits the HTML into 'content HTML' and 'presentation'
 PresentationControls.prototype.separate_presentation = function() {
     var self = this;
-    jQuery.post("/semantic/separate_presentation/", { html: self.wym.html() } ,
+    jQuery.post("/semantic/separate_presentation/", { html: self.wym.xhtml() } ,
 		function(data) {
 		    self.with_good_data(data,
 			function(value) {
 		}, "json");
 };
 
+PresentationControls.prototype.form_submit = function(event) {
+    // Since we are in the middle of submitting the page, an asynchronous
+    // request will be too late! So we block instead.
+    var res = jQuery.ajax({
+		  type: "POST",
+		  data: {
+		      html: this.wym.xhtml(),
+		      presentation: JSON.stringify(this.presentation_info)
+		  },
+		  url: "/semantic/combine_presentation/",
+		  dataType: "json",
+		  async: false
+    }).responseText;
+    var data = JSON.parse(res);
+    if (data.result == 'ok') {
+	// Replace existing HTML with combined.
+	this.wym.html(data.value.html);
+	// In case the normal WYMeditor update got called *before* this
+	// event handler, we do another update.
+	this.wym.update();
+    } else {
+	event.preventDefault();
+	this.show_error(data.message);
+	var pos = this.wym._box.offset();
+	window.scrollTo(pos.left, pos.top);
+	alert(data.message);
+    }
+};
+
 PresentationControls.prototype.build_optsbox = function() {
     this.optsbox.empty();
     var self = this;
 PresentationControls.prototype.remove_style = function(heading, presinfo) {
     var styles = this.presentation_info[heading];
     styles = jQuery.grep(styles, function(item, i) {
-			     return item != presinfo;
+			     return !(item.prestype == presinfo.prestype
+				      && item.name == presinfo.name);
 			 });
     this.presentation_info[heading] = styles;
 };
 
 PresentationControls.prototype.refresh_headings = function() {
     var self = this;
-    var html = this.wym.html();
+    var html = this.wym.xhtml();
     jQuery.post("/semantic/extract_headings/", { 'html':html },
 	function(data) {
 	    self.with_good_data(data, function(value) {

semanticeditor/tests.py

         self.assertEqual(set([p1]), set([p2]))
 
 class TestFormat(TestCase):
+    def test_empty(self):
+        self.assertEqual('', format_html('', {}));
+
     def test_no_headings(self):
         html = "<p>Test</p>"
         self.assertEqual(html, format_html(html, {}))

semanticeditor/urls.py

 from django.conf.urls.defaults import patterns, url
-from semanticeditor.views import extract_headings_view, retrieve_styles, separate_presentation
+from semanticeditor.views import extract_headings_view, retrieve_styles, separate_presentation, combine_presentation
 
 urlpatterns = patterns('',
     url(r'extract_headings/', extract_headings_view),
     url(r'retrieve_styles/', retrieve_styles),
     url(r'separate_presentation/', separate_presentation),
+    url(r'combine_presentation/', combine_presentation),
 )

semanticeditor/utils/presentation.py

     return _html_extract(root)
 
 def _html_extract(root):
+    if len(root) == 0 and root.text is None and root.tail is None:
+        return ''
     return ET.tostring(root).replace('<html>','').replace('</html>','')
 
 def _strip_presentation(tree):
                     # This is the first child, therefore the beginning
                     # of the row.
                     pres[name].add(NEWROW)
-                    pres[name].remove(NEWCOL) # not technically necessary
+                    pres[name].discard(NEWCOL) # not technically necessary
 
     _strip_presentation(root)
     out_html = _html_extract(root)

semanticeditor/views.py

 from django.utils import simplejson
 from django.core.mail import mail_admins
 from django.utils.translation import ugettext as _
-from semanticeditor.utils import extract_headings, extract_presentation, AllUserErrors, NEWROW, NEWCOL, PresentationClass
+from semanticeditor.utils import extract_headings, extract_presentation, format_html, AllUserErrors, NEWROW, NEWCOL, PresentationInfo, PresentationClass
 from semanticeditor.models import CssClass
 import sys
+try:
+    from functools import wraps
+except ImportError:
+    from django.utils.functional import wraps  # Python 2.3, 2.4 fallback.
+
 
 def json_view(func):
     """
      'message': an_error_message
     }
     """
-    def wrap(request, *a, **kw):
+    def wrapper(request, *a, **kw):
         response = None
         try:
             response = func(request, *a, **kw)
             if hasattr(e, 'message'):
                 msg = e.message
             else:
-                msg = _('Internal error')+': '+str(e)
+                msg = _('Internal error')+': '+ str(e)
             response = error(msg)
 
         json = simplejson.dumps(response)
         return HttpResponse(json, mimetype='application/json')
-    return wrap
+
+    return wraps(func)(wrapper)
 
 def error(msg):
     """
 
 # Anything that depends on values the user may have entered and might
 # contain 'errors' should use this, normally passing in AllUserErrors
-# as 'exceptions'.
+# as 'exceptions'.  'server' errors are handled by @json_view
 def graceful_errors(exceptions, callback):
     """
     Retrieve a value from a callback, handling the exceptions that
     """
     return pi.__dict__
 
+def dict_to_PI(d):
+    return PresentationInfo(prestype=d['prestype'], name=d['name'])
+
 @json_view
 def retrieve_styles(request):
     retval = [NEWROW, NEWCOL]
     if data is None:
         return failure("No HTML sent for parsing")
 
-    def _ret():
+    def _handled():
         pres, html = extract_presentation(data)
-        # Rewrite pres so that we can serialise it
+        # Rewrite pres so that we can serialise it to JSON
         pres2 = {}
         for k, v in pres.items():
-            pres2[k] = list(v)
+            pres2[k] = [PI_to_dict(p) for p in v]
         return dict(presentation=pres2,
                     html=html)
 
-    return graceful_errors(AllUserErrors, _ret)
+    return graceful_errors(AllUserErrors, _handled)
+
+@json_view
+def combine_presentation(request):
+    """
+    Combines submitted 'html' and 'presentation' data,
+    returning a dictionary containg { html: <combined html> }
+    """
+    html = request.POST.get('html', '')
+    presentation = request.POST.get('presentation', '{}')
+    presentation = simplejson.loads(presentation)
+    # Convert dictionaries into PresentationInfo classes
+    for k, v in presentation.items():
+        # v is list of PI dicts
+        for i, item in enumerate(v):
+            v[i] = dict_to_PI(item)
+
+    return graceful_errors(AllUserErrors, lambda: dict(html=format_html(html, presentation)))

semanticeditor/widgets.py

     class Media:
         js = [join(settings.SE_MEDIA_URL, path) for path in
               ('javascript/wymeditor/plugins/semantic/wymeditor.semantic.js',
+               'javascript/json2.js',
                )]
 
     def render_extra(self, name, value, attrs=None):