From 48bb447fbde50916c14bc3179e368c191b5d0f33 Mon Sep 17 00:00:00 2001
From: Arthur Barrett <abarrett@Arthurs-MacBook-Pro.local>
Date: Thu, 31 Jan 2013 18:42:11 -0500
Subject: [PATCH] Adding basic annotatable module and related files.

---
 common/lib/xmodule/setup.py                   |   3 +-
 .../lib/xmodule/xmodule/annotatable_module.py | 128 ++++++++++++++++++
 .../xmodule/css/annotatable/display.scss      |  63 +++++++++
 .../xmodule/js/src/annotatable/display.coffee |   9 ++
 lms/templates/annotatable.html                |  23 ++++
 5 files changed, 225 insertions(+), 1 deletion(-)
 create mode 100644 common/lib/xmodule/xmodule/annotatable_module.py
 create mode 100644 common/lib/xmodule/xmodule/css/annotatable/display.scss
 create mode 100644 common/lib/xmodule/xmodule/js/src/annotatable/display.coffee
 create mode 100644 lms/templates/annotatable.html

diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py
index 29227c31884..a2d9b3e4df3 100644
--- a/common/lib/xmodule/setup.py
+++ b/common/lib/xmodule/setup.py
@@ -40,7 +40,8 @@ setup(
             "static_tab = xmodule.html_module:StaticTabDescriptor",
             "custom_tag_template = xmodule.raw_module:RawDescriptor",
             "about = xmodule.html_module:AboutDescriptor",
-            "graphical_slider_tool = xmodule.gst_module:GraphicalSliderToolDescriptor"
+            "graphical_slider_tool = xmodule.gst_module:GraphicalSliderToolDescriptor",
+            "annotatable = xmodule.annotatable_module:AnnotatableDescriptor"
         ]
     }
 )
diff --git a/common/lib/xmodule/xmodule/annotatable_module.py b/common/lib/xmodule/xmodule/annotatable_module.py
new file mode 100644
index 00000000000..bf76d7fc8c4
--- /dev/null
+++ b/common/lib/xmodule/xmodule/annotatable_module.py
@@ -0,0 +1,128 @@
+import json
+import logging
+import re
+
+from lxml import etree
+from pkg_resources import resource_string, resource_listdir
+
+from xmodule.x_module import XModule
+from xmodule.raw_module import RawDescriptor
+from xmodule.modulestore.mongo import MongoModuleStore
+from xmodule.modulestore.django import modulestore
+from xmodule.contentstore.content import StaticContent
+
+import datetime
+import time
+
+log = logging.getLogger(__name__)
+
+class AnnotatableModule(XModule):
+    # Note: js and css in common/lib/xmodule/xmodule
+    js = {'coffee': [resource_string(__name__, 'js/src/javascript_loader.coffee'),
+                     resource_string(__name__, 'js/src/collapsible.coffee'),
+                     resource_string(__name__, 'js/src/html/display.coffee'),
+                     resource_string(__name__, 'js/src/annotatable/display.coffee')],
+          'js': []
+         }
+    js_module_name = "Annotatable"
+    css = {'scss': [resource_string(__name__, 'css/annotatable/display.scss')]}
+
+    def _is_span(self, element):
+        """ Returns true if the element is a valid annotation span, false otherwise. """
+        return element.tag == 'span' and element.get('class') == 'annotatable'
+
+    def _is_span_container(self, element):
+        """ Returns true if the element is a valid span contanier, false otherwise. """
+        return element.tag == 'p' # Assume content is in paragraph form (for now...)
+
+    def _iterspans(self, xmltree, callbacks):
+        """ Iterates over span elements and invokes each callback on the span. """
+
+        index = 0
+        for element in xmltree.iter('span'):
+            if self._is_span(element):
+                for callback in callbacks:
+                    callback(element, index, xmltree)
+                index += 1
+    
+    def _get_span_container(self, span):
+        """ Returns the first container element of the span.
+            The intent is to add the discussion widgets at the 
+            end of the container, not interspersed with the text. """
+
+        container = None
+        for parent in span.iterancestors():
+            if self._is_span_container(parent):
+                container = parent
+                break 
+       
+        if container is None:
+            return parent
+        return container
+    
+    def _attach_discussion(self, span, index, xmltree):
+        """ Attaches a discussion thread to the annotation span. """
+
+        tpl = u'<div class="annotatable-discussion" data-discussion-id="{0}">'
+        tpl += '<div class="annotatable-icon"> </div>'
+        tpl += '<span class="annotatable-discussion-label">Guided Discussion: </span>'
+        tpl += '<span class="annotatable-discussion-thread">{1}</span>'
+        tpl += '<a class="annotatable-show-discussion" href="javascript:void(0);">Show Discussion</a>'
+        tpl += '</div>'
+
+        span_id = 'span-{0}'.format(index) # How should we anchor spans? 
+        span.set('data-span-id', span_id)
+
+        discussion_id = 'discussion-{0}'.format(index) # How do we get a real discussion ID?
+        discussion_title = 'Thread Title {0}'.format(index) # How do we get the discussion Title?
+        discussion_html = tpl.format(discussion_id, discussion_title)
+        discussion = etree.fromstring(discussion_html)
+
+        span_container = self._get_span_container(span)
+        span_container.append(discussion)
+
+        self.discussion_for[span_id] = discussion_id
+    
+    def _add_icon(self, span, index, xmltree):
+        """ Adds an icon to the annotation span. """
+
+        span_icon = etree.Element('span', { 'class': 'annotatable-icon'} )
+        span_icon.text = '';
+        span_icon.tail = span.text
+        span.text = ''
+        span.insert(0, span_icon)
+    
+    def _render(self):
+        """ Renders annotatable content by transforming spans and adding discussions. """
+
+        xmltree = etree.fromstring(self.content)
+        self._iterspans(xmltree, [ self._add_icon, self._attach_discussion ])
+        return etree.tostring(xmltree)
+
+    def get_html(self):
+        """ Renders parameters to template. """
+        
+        context = {
+            'display_name': self.display_name,
+            'element_id': self.element_id,
+            'html_content': self._render(),
+            'json_discussion_for': json.dumps(self.discussion_for)        
+        }
+
+        # template dir: lms/templates
+        return self.system.render_template('annotatable.html', context)
+
+    def __init__(self, system, location, definition, descriptor,
+                 instance_state=None, shared_state=None, **kwargs):
+        XModule.__init__(self, system, location, definition, descriptor,
+                         instance_state, shared_state, **kwargs)
+        
+        self.element_id = self.location.html_id();
+        self.content = self.definition['data']
+        self.discussion_for = {} # Maps spans to discussions by id (for JS)
+
+
+class AnnotatableDescriptor(RawDescriptor):
+    module_class = AnnotatableModule
+    stores_state = True
+    template_dir_name = "annotatable"
diff --git a/common/lib/xmodule/xmodule/css/annotatable/display.scss b/common/lib/xmodule/xmodule/css/annotatable/display.scss
new file mode 100644
index 00000000000..9b6404ceb82
--- /dev/null
+++ b/common/lib/xmodule/xmodule/css/annotatable/display.scss
@@ -0,0 +1,63 @@
+.annotatable-header {
+  border: 1px solid $border-color;
+  border-radius: 3px;
+  margin-bottom: 1em;
+  padding: 2px 4px;
+  position: relative;
+
+  .annotatable-title {
+    font-size: em(18);
+    text-transform: uppercase;
+  }
+  .annotatable-description {
+    font-size: $body-font-size;
+  }
+}
+
+
+span.annotatable {
+  color: $blue;
+  .annotatable-icon {
+    margin: auto 2px auto 4px;
+  }
+}
+
+.annotatable-icon {
+  display: inline-block;
+  vertical-align: middle;
+  width: 16px;
+  height: 17px;
+  background: url(../images/link-icon.png) no-repeat;  
+}
+
+.help-icon {
+  display: block;
+  position: absolute;
+  right: 0;
+  top: 33%;
+  width: 16px;
+  height: 17px;
+  margin: 0 7px 0 0;
+  background: url(../images/info-icon.png) no-repeat;
+}
+
+.annotatable-discussion {
+  display: block;
+  border: 1px solid $border-color;
+  border-radius: 3px;
+  margin: 1em 0;
+  position: relative;
+  padding: 4px;
+
+  .annotatable-discussion-label {
+    font-weight: bold;
+  }
+  .annotatable-icon {
+    margin: auto 4px auto 0px;
+  }
+  .annotatable-show-discussion {
+    position: absolute;
+    right: 8px;
+    margin-top: 4px;
+  }
+}
diff --git a/common/lib/xmodule/xmodule/js/src/annotatable/display.coffee b/common/lib/xmodule/xmodule/js/src/annotatable/display.coffee
new file mode 100644
index 00000000000..1db6ac2f6b2
--- /dev/null
+++ b/common/lib/xmodule/xmodule/js/src/annotatable/display.coffee
@@ -0,0 +1,9 @@
+class @Annotatable
+    constructor: (el) ->
+        console.log "loaded Annotatable"
+        $(el).find(".annotatable").on "click", (e) ->
+            data = $(".annotatable-wrapper", el).data("spans")
+            span_id = e.target.getAttribute("data-span-id")
+            msg = "annotatable span clicked. discuss span [" + span_id + "] in discussion [" + data[span_id] + "]"
+            console.log data
+            window.alert msg
diff --git a/lms/templates/annotatable.html b/lms/templates/annotatable.html
new file mode 100644
index 00000000000..3df0a599219
--- /dev/null
+++ b/lms/templates/annotatable.html
@@ -0,0 +1,23 @@
+<div class="annotatable-wrapper" id="${element_id}">
+
+<div class="annotatable-header">
+	<div class="help-icon"></div>
+	% if display_name is not UNDEFINED and display_name is not None:
+	  <div class="annotatable-title">${display_name} </div>
+	% endif
+	<div class="annotatable-description">Annotated Reading + Guided Discussion</div>
+</div>
+
+<div class="annotatable-content">
+	${html_content}
+</div>
+	
+<script>
+$(function() {
+	// TODO pass spans to module directly
+	var el = $('#${element_id}.annotatable-wrapper');
+	el.data('spans', ${json_discussion_for});
+});
+</script>
+
+</div>
\ No newline at end of file
-- 
GitLab