Merge pull request #1990 from edx/feature/abarrett/lms-notes-app
LMS Notes Djangoapp
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
""" Tests for utils. """
|
||||
from contentstore import utils
|
||||
import mock
|
||||
import collections
|
||||
import copy
|
||||
from django.test import TestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
@@ -70,3 +72,79 @@ class UrlReverseTestCase(ModuleStoreTestCase):
|
||||
'https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about',
|
||||
utils.get_url_reverse('https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about', course)
|
||||
)
|
||||
|
||||
|
||||
class ExtraPanelTabTestCase(TestCase):
|
||||
""" Tests adding and removing extra course tabs. """
|
||||
|
||||
def get_tab_type_dicts(self, tab_types):
|
||||
""" Returns an array of tab dictionaries. """
|
||||
if tab_types:
|
||||
return [{'tab_type': tab_type} for tab_type in tab_types.split(',')]
|
||||
else:
|
||||
return []
|
||||
|
||||
def get_course_with_tabs(self, tabs=[]):
|
||||
""" Returns a mock course object with a tabs attribute. """
|
||||
course = collections.namedtuple('MockCourse', ['tabs'])
|
||||
if isinstance(tabs, basestring):
|
||||
course.tabs = self.get_tab_type_dicts(tabs)
|
||||
else:
|
||||
course.tabs = tabs
|
||||
return course
|
||||
|
||||
def test_add_extra_panel_tab(self):
|
||||
""" Tests if a tab can be added to a course tab list. """
|
||||
for tab_type in utils.EXTRA_TAB_PANELS.keys():
|
||||
tab = utils.EXTRA_TAB_PANELS.get(tab_type)
|
||||
|
||||
# test adding with changed = True
|
||||
for tab_setup in ['', 'x', 'x,y,z']:
|
||||
course = self.get_course_with_tabs(tab_setup)
|
||||
expected_tabs = copy.copy(course.tabs)
|
||||
expected_tabs.append(tab)
|
||||
changed, actual_tabs = utils.add_extra_panel_tab(tab_type, course)
|
||||
self.assertTrue(changed)
|
||||
self.assertEqual(actual_tabs, expected_tabs)
|
||||
|
||||
# test adding with changed = False
|
||||
tab_test_setup = [
|
||||
[tab],
|
||||
[tab, self.get_tab_type_dicts('x,y,z')],
|
||||
[self.get_tab_type_dicts('x,y'), tab, self.get_tab_type_dicts('z')],
|
||||
[self.get_tab_type_dicts('x,y,z'), tab]]
|
||||
|
||||
for tab_setup in tab_test_setup:
|
||||
course = self.get_course_with_tabs(tab_setup)
|
||||
expected_tabs = copy.copy(course.tabs)
|
||||
changed, actual_tabs = utils.add_extra_panel_tab(tab_type, course)
|
||||
self.assertFalse(changed)
|
||||
self.assertEqual(actual_tabs, expected_tabs)
|
||||
|
||||
def test_remove_extra_panel_tab(self):
|
||||
""" Tests if a tab can be removed from a course tab list. """
|
||||
for tab_type in utils.EXTRA_TAB_PANELS.keys():
|
||||
tab = utils.EXTRA_TAB_PANELS.get(tab_type)
|
||||
|
||||
# test removing with changed = True
|
||||
tab_test_setup = [
|
||||
[tab],
|
||||
[tab, self.get_tab_type_dicts('x,y,z')],
|
||||
[self.get_tab_type_dicts('x,y'), tab, self.get_tab_type_dicts('z')],
|
||||
[self.get_tab_type_dicts('x,y,z'), tab]]
|
||||
|
||||
for tab_setup in tab_test_setup:
|
||||
course = self.get_course_with_tabs(tab_setup)
|
||||
expected_tabs = [t for t in course.tabs if t != utils.EXTRA_TAB_PANELS.get(tab_type)]
|
||||
changed, actual_tabs = utils.remove_extra_panel_tab(tab_type, course)
|
||||
self.assertTrue(changed)
|
||||
self.assertEqual(actual_tabs, expected_tabs)
|
||||
|
||||
# test removing with changed = False
|
||||
for tab_setup in ['', 'x', 'x,y,z']:
|
||||
course = self.get_course_with_tabs(tab_setup)
|
||||
expected_tabs = copy.copy(course.tabs)
|
||||
changed, actual_tabs = utils.remove_extra_panel_tab(tab_type, course)
|
||||
self.assertFalse(changed)
|
||||
self.assertEqual(actual_tabs, expected_tabs)
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_ta
|
||||
|
||||
#In order to instantiate an open ended tab automatically, need to have this data
|
||||
OPEN_ENDED_PANEL = {"name": "Open Ended Panel", "type": "open_ended"}
|
||||
NOTES_PANEL = {"name": "My Notes", "type": "notes"}
|
||||
EXTRA_TAB_PANELS = dict([(p['type'], p) for p in [OPEN_ENDED_PANEL, NOTES_PANEL]])
|
||||
|
||||
|
||||
def get_modulestore(location):
|
||||
@@ -192,9 +194,10 @@ class CoursePageNames:
|
||||
Checklists = "checklists"
|
||||
|
||||
|
||||
def add_open_ended_panel_tab(course):
|
||||
def add_extra_panel_tab(tab_type, course):
|
||||
"""
|
||||
Used to add the open ended panel tab to a course if it does not exist.
|
||||
Used to add the panel tab to a course if it does not exist.
|
||||
@param tab_type: A string representing the tab type.
|
||||
@param course: A course object from the modulestore.
|
||||
@return: Boolean indicating whether or not a tab was added and a list of tabs for the course.
|
||||
"""
|
||||
@@ -202,16 +205,19 @@ def add_open_ended_panel_tab(course):
|
||||
course_tabs = copy.copy(course.tabs)
|
||||
changed = False
|
||||
#Check to see if open ended panel is defined in the course
|
||||
if OPEN_ENDED_PANEL not in course_tabs:
|
||||
|
||||
tab_panel = EXTRA_TAB_PANELS.get(tab_type)
|
||||
if tab_panel not in course_tabs:
|
||||
#Add panel to the tabs if it is not defined
|
||||
course_tabs.append(OPEN_ENDED_PANEL)
|
||||
course_tabs.append(tab_panel)
|
||||
changed = True
|
||||
return changed, course_tabs
|
||||
|
||||
|
||||
def remove_open_ended_panel_tab(course):
|
||||
def remove_extra_panel_tab(tab_type, course):
|
||||
"""
|
||||
Used to remove the open ended panel tab from a course if it exists.
|
||||
Used to remove the panel tab from a course if it exists.
|
||||
@param tab_type: A string representing the tab type.
|
||||
@param course: A course object from the modulestore.
|
||||
@return: Boolean indicating whether or not a tab was added and a list of tabs for the course.
|
||||
"""
|
||||
@@ -219,8 +225,10 @@ def remove_open_ended_panel_tab(course):
|
||||
course_tabs = copy.copy(course.tabs)
|
||||
changed = False
|
||||
#Check to see if open ended panel is defined in the course
|
||||
if OPEN_ENDED_PANEL in course_tabs:
|
||||
|
||||
tab_panel = EXTRA_TAB_PANELS.get(tab_type)
|
||||
if tab_panel in course_tabs:
|
||||
#Add panel to the tabs if it is not defined
|
||||
course_tabs = [ct for ct in course_tabs if ct != OPEN_ENDED_PANEL]
|
||||
course_tabs = [ct for ct in course_tabs if ct != tab_panel]
|
||||
changed = True
|
||||
return changed, course_tabs
|
||||
|
||||
@@ -41,7 +41,8 @@ log = logging.getLogger(__name__)
|
||||
COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video']
|
||||
|
||||
OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"]
|
||||
ADVANCED_COMPONENT_TYPES = ['annotatable', 'word_cloud'] + OPEN_ENDED_COMPONENT_TYPES
|
||||
NOTE_COMPONENT_TYPES = ['notes']
|
||||
ADVANCED_COMPONENT_TYPES = ['annotatable' + 'word_cloud'] + OPEN_ENDED_COMPONENT_TYPES + NOTE_COMPONENT_TYPES
|
||||
ADVANCED_COMPONENT_CATEGORY = 'advanced'
|
||||
ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules'
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ from xmodule.modulestore import Location
|
||||
from contentstore.course_info_model \
|
||||
import get_course_updates, update_course_updates, delete_course_update
|
||||
from contentstore.utils \
|
||||
import get_lms_link_for_item, add_open_ended_panel_tab, \
|
||||
remove_open_ended_panel_tab
|
||||
import get_lms_link_for_item, add_extra_panel_tab, \
|
||||
remove_extra_panel_tab
|
||||
from models.settings.course_details \
|
||||
import CourseDetails, CourseSettingsEncoder
|
||||
from models.settings.course_grading import CourseGradingModel
|
||||
@@ -32,7 +32,8 @@ from util.json_request import expect_json
|
||||
from .access import has_access, get_location_and_verify_access
|
||||
from .requests import get_request_method
|
||||
from .tabs import initialize_course_tabs
|
||||
from .component import OPEN_ENDED_COMPONENT_TYPES, ADVANCED_COMPONENT_POLICY_KEY
|
||||
from .component import OPEN_ENDED_COMPONENT_TYPES, \
|
||||
NOTE_COMPONENT_TYPES, ADVANCED_COMPONENT_POLICY_KEY
|
||||
|
||||
__all__ = ['course_index', 'create_new_course', 'course_info',
|
||||
'course_info_updates', 'get_course_settings',
|
||||
@@ -352,38 +353,52 @@ def course_advanced_updates(request, org, course, name):
|
||||
request_body = json.loads(request.body)
|
||||
# Whether or not to filter the tabs key out of the settings metadata
|
||||
filter_tabs = True
|
||||
# Check to see if the user instantiated any advanced components.
|
||||
# This is a hack to add the open ended panel tab
|
||||
# to a course automatically if the user has indicated that they want
|
||||
# to edit the combinedopenended or peergrading
|
||||
# module, and to remove it if they have removed the open ended elements.
|
||||
|
||||
#Check to see if the user instantiated any advanced components. This is a hack
|
||||
#that does the following :
|
||||
# 1) adds/removes the open ended panel tab to a course automatically if the user
|
||||
# has indicated that they want to edit the combinedopendended or peergrading module
|
||||
# 2) adds/removes the notes panel tab to a course automatically if the user has
|
||||
# indicated that they want the notes module enabled in their course
|
||||
# TODO refactor the above into distinct advanced policy settings
|
||||
if ADVANCED_COMPONENT_POLICY_KEY in request_body:
|
||||
# Check to see if the user instantiated any open ended components
|
||||
found_oe_type = False
|
||||
# Get the course so that we can scrape current tabs
|
||||
#Get the course so that we can scrape current tabs
|
||||
course_module = modulestore().get_item(location)
|
||||
for oe_type in OPEN_ENDED_COMPONENT_TYPES:
|
||||
if oe_type in request_body[ADVANCED_COMPONENT_POLICY_KEY]:
|
||||
# Add an open ended tab to the course if needed
|
||||
changed, new_tabs = add_open_ended_panel_tab(course_module)
|
||||
# If a tab has been added to the course, then send the
|
||||
# metadata along to CourseMetadata.update_from_json
|
||||
|
||||
#Maps tab types to components
|
||||
tab_component_map = {
|
||||
'open_ended': OPEN_ENDED_COMPONENT_TYPES,
|
||||
'notes': NOTE_COMPONENT_TYPES,
|
||||
}
|
||||
|
||||
#Check to see if the user instantiated any notes or open ended components
|
||||
for tab_type in tab_component_map.keys():
|
||||
component_types = tab_component_map.get(tab_type)
|
||||
found_ac_type = False
|
||||
for ac_type in component_types:
|
||||
if ac_type in request_body[ADVANCED_COMPONENT_POLICY_KEY]:
|
||||
#Add tab to the course if needed
|
||||
changed, new_tabs = add_extra_panel_tab(tab_type, course_module)
|
||||
#If a tab has been added to the course, then send the metadata along to CourseMetadata.update_from_json
|
||||
if changed:
|
||||
course_module.tabs = new_tabs
|
||||
request_body.update({'tabs': new_tabs})
|
||||
#Indicate that tabs should not be filtered out of the metadata
|
||||
filter_tabs = False
|
||||
#Set this flag to avoid the tab removal code below.
|
||||
found_ac_type = True
|
||||
break
|
||||
#If we did not find a module type in the advanced settings,
|
||||
# we may need to remove the tab from the course.
|
||||
if not found_ac_type:
|
||||
#Remove tab from the course if needed
|
||||
changed, new_tabs = remove_extra_panel_tab(tab_type, course_module)
|
||||
if changed:
|
||||
course_module.tabs = new_tabs
|
||||
request_body.update({'tabs': new_tabs})
|
||||
# Indicate that tabs should not be filtered out of the metadata
|
||||
#Indicate that tabs should *not* be filtered out of the metadata
|
||||
filter_tabs = False
|
||||
# Set this flag to avoid the open ended tab removal code below.
|
||||
found_oe_type = True
|
||||
break
|
||||
# If we did not find an open ended module type in the advanced settings,
|
||||
# we may need to remove the open ended tab from the course.
|
||||
if not found_oe_type:
|
||||
# Remove open ended tab to the course if needed
|
||||
changed, new_tabs = remove_open_ended_panel_tab(course_module)
|
||||
if changed:
|
||||
request_body.update({'tabs': new_tabs})
|
||||
# Indicate that tabs should not be filtered out of the metadata
|
||||
filter_tabs = False
|
||||
|
||||
response_json = json.dumps(CourseMetadata.update_from_json(location,
|
||||
request_body,
|
||||
filter_tabs=filter_tabs))
|
||||
|
||||
1827
common/static/js/vendor/annotator.js
vendored
Normal file
1827
common/static/js/vendor/annotator.js
vendored
Normal file
@@ -0,0 +1,1827 @@
|
||||
/*
|
||||
** Annotator 1.2.6-dev-dc18206
|
||||
** https://github.com/okfn/annotator/
|
||||
**
|
||||
** Copyright 2012 Aron Carroll, Rufus Pollock, and Nick Stenning.
|
||||
** Dual licensed under the MIT and GPLv3 licenses.
|
||||
** https://github.com/okfn/annotator/blob/master/LICENSE
|
||||
**
|
||||
** Built at: 2013-05-16 18:01:57Z
|
||||
*/
|
||||
|
||||
|
||||
(function() {
|
||||
var $, Annotator, Delegator, LinkParser, Range, findChild, fn, functions, g, getNodeName, getNodePosition, gettext, simpleXPathJQuery, simpleXPathPure, util, _Annotator, _gettext, _i, _j, _len, _len1, _ref, _ref1, _t,
|
||||
__slice = [].slice,
|
||||
__hasProp = {}.hasOwnProperty,
|
||||
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
|
||||
__bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
|
||||
|
||||
simpleXPathJQuery = function(relativeRoot) {
|
||||
var jq;
|
||||
|
||||
jq = this.map(function() {
|
||||
var elem, idx, path, tagName;
|
||||
|
||||
path = '';
|
||||
elem = this;
|
||||
while (elem && elem.nodeType === 1 && elem !== relativeRoot) {
|
||||
tagName = elem.tagName.replace(":", "\\:");
|
||||
idx = $(elem.parentNode).children(tagName).index(elem) + 1;
|
||||
idx = "[" + idx + "]";
|
||||
path = "/" + elem.tagName.toLowerCase() + idx + path;
|
||||
elem = elem.parentNode;
|
||||
}
|
||||
return path;
|
||||
});
|
||||
return jq.get();
|
||||
};
|
||||
|
||||
simpleXPathPure = function(relativeRoot) {
|
||||
var getPathSegment, getPathTo, jq, rootNode;
|
||||
|
||||
getPathSegment = function(node) {
|
||||
var name, pos;
|
||||
|
||||
name = getNodeName(node);
|
||||
pos = getNodePosition(node);
|
||||
return "" + name + "[" + pos + "]";
|
||||
};
|
||||
rootNode = relativeRoot;
|
||||
getPathTo = function(node) {
|
||||
var xpath;
|
||||
|
||||
xpath = '';
|
||||
while (node !== rootNode) {
|
||||
if (node == null) {
|
||||
throw new Error("Called getPathTo on a node which was not a descendant of @rootNode. " + rootNode);
|
||||
}
|
||||
xpath = (getPathSegment(node)) + '/' + xpath;
|
||||
node = node.parentNode;
|
||||
}
|
||||
xpath = '/' + xpath;
|
||||
xpath = xpath.replace(/\/$/, '');
|
||||
return xpath;
|
||||
};
|
||||
jq = this.map(function() {
|
||||
var path;
|
||||
|
||||
path = getPathTo(this);
|
||||
return path;
|
||||
});
|
||||
return jq.get();
|
||||
};
|
||||
|
||||
findChild = function(node, type, index) {
|
||||
var child, children, found, name, _i, _len;
|
||||
|
||||
if (!node.hasChildNodes()) {
|
||||
throw new Error("XPath error: node has no children!");
|
||||
}
|
||||
children = node.childNodes;
|
||||
found = 0;
|
||||
for (_i = 0, _len = children.length; _i < _len; _i++) {
|
||||
child = children[_i];
|
||||
name = getNodeName(child);
|
||||
if (name === type) {
|
||||
found += 1;
|
||||
if (found === index) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error("XPath error: wanted child not found.");
|
||||
};
|
||||
|
||||
getNodeName = function(node) {
|
||||
var nodeName;
|
||||
|
||||
nodeName = node.nodeName.toLowerCase();
|
||||
switch (nodeName) {
|
||||
case "#text":
|
||||
return "text()";
|
||||
case "#comment":
|
||||
return "comment()";
|
||||
case "#cdata-section":
|
||||
return "cdata-section()";
|
||||
default:
|
||||
return nodeName;
|
||||
}
|
||||
};
|
||||
|
||||
getNodePosition = function(node) {
|
||||
var pos, tmp;
|
||||
|
||||
pos = 0;
|
||||
tmp = node;
|
||||
while (tmp) {
|
||||
if (tmp.nodeName === node.nodeName) {
|
||||
pos++;
|
||||
}
|
||||
tmp = tmp.previousSibling;
|
||||
}
|
||||
return pos;
|
||||
};
|
||||
|
||||
gettext = null;
|
||||
|
||||
if (typeof Gettext !== "undefined" && Gettext !== null) {
|
||||
_gettext = new Gettext({
|
||||
domain: "annotator"
|
||||
});
|
||||
gettext = function(msgid) {
|
||||
return _gettext.gettext(msgid);
|
||||
};
|
||||
} else {
|
||||
gettext = function(msgid) {
|
||||
return msgid;
|
||||
};
|
||||
}
|
||||
|
||||
_t = function(msgid) {
|
||||
return gettext(msgid);
|
||||
};
|
||||
|
||||
if (!(typeof jQuery !== "undefined" && jQuery !== null ? (_ref = jQuery.fn) != null ? _ref.jquery : void 0 : void 0)) {
|
||||
console.error(_t("Annotator requires jQuery: have you included lib/vendor/jquery.js?"));
|
||||
}
|
||||
|
||||
if (!(JSON && JSON.parse && JSON.stringify)) {
|
||||
console.error(_t("Annotator requires a JSON implementation: have you included lib/vendor/json2.js?"));
|
||||
}
|
||||
|
||||
$ = jQuery.sub();
|
||||
|
||||
$.flatten = function(array) {
|
||||
var flatten;
|
||||
|
||||
flatten = function(ary) {
|
||||
var el, flat, _i, _len;
|
||||
|
||||
flat = [];
|
||||
for (_i = 0, _len = ary.length; _i < _len; _i++) {
|
||||
el = ary[_i];
|
||||
flat = flat.concat(el && $.isArray(el) ? flatten(el) : el);
|
||||
}
|
||||
return flat;
|
||||
};
|
||||
return flatten(array);
|
||||
};
|
||||
|
||||
$.plugin = function(name, object) {
|
||||
return jQuery.fn[name] = function(options) {
|
||||
var args;
|
||||
|
||||
args = Array.prototype.slice.call(arguments, 1);
|
||||
return this.each(function() {
|
||||
var instance;
|
||||
|
||||
instance = $.data(this, name);
|
||||
if (instance) {
|
||||
return options && instance[options].apply(instance, args);
|
||||
} else {
|
||||
instance = new object(this, options);
|
||||
return $.data(this, name, instance);
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
$.fn.textNodes = function() {
|
||||
var getTextNodes;
|
||||
|
||||
getTextNodes = function(node) {
|
||||
var nodes;
|
||||
|
||||
if (node && node.nodeType !== 3) {
|
||||
nodes = [];
|
||||
if (node.nodeType !== 8) {
|
||||
node = node.lastChild;
|
||||
while (node) {
|
||||
nodes.push(getTextNodes(node));
|
||||
node = node.previousSibling;
|
||||
}
|
||||
}
|
||||
return nodes.reverse();
|
||||
} else {
|
||||
return node;
|
||||
}
|
||||
};
|
||||
return this.map(function() {
|
||||
return $.flatten(getTextNodes(this));
|
||||
});
|
||||
};
|
||||
|
||||
$.fn.xpath = function(relativeRoot) {
|
||||
var exception, result;
|
||||
|
||||
try {
|
||||
result = simpleXPathJQuery.call(this, relativeRoot);
|
||||
} catch (_error) {
|
||||
exception = _error;
|
||||
console.log("jQuery-based XPath construction failed! Falling back to manual.");
|
||||
result = simpleXPathPure.call(this, relativeRoot);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
$.xpath = function(xp, root) {
|
||||
var idx, name, node, step, steps, _i, _len, _ref1;
|
||||
|
||||
steps = xp.substring(1).split("/");
|
||||
node = root;
|
||||
for (_i = 0, _len = steps.length; _i < _len; _i++) {
|
||||
step = steps[_i];
|
||||
_ref1 = step.split("["), name = _ref1[0], idx = _ref1[1];
|
||||
idx = idx != null ? parseInt((idx != null ? idx.split("]") : void 0)[0]) : 1;
|
||||
node = findChild(node, name.toLowerCase(), idx);
|
||||
}
|
||||
return node;
|
||||
};
|
||||
|
||||
$.escape = function(html) {
|
||||
return html.replace(/&(?!\w+;)/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
};
|
||||
|
||||
$.fn.escape = function(html) {
|
||||
if (arguments.length) {
|
||||
return this.html($.escape(html));
|
||||
}
|
||||
return this.html();
|
||||
};
|
||||
|
||||
$.fn.reverse = []._reverse || [].reverse;
|
||||
|
||||
functions = ["log", "debug", "info", "warn", "exception", "assert", "dir", "dirxml", "trace", "group", "groupEnd", "groupCollapsed", "time", "timeEnd", "profile", "profileEnd", "count", "clear", "table", "error", "notifyFirebug", "firebug", "userObjects"];
|
||||
|
||||
if (typeof console !== "undefined" && console !== null) {
|
||||
if (console.group == null) {
|
||||
console.group = function(name) {
|
||||
return console.log("GROUP: ", name);
|
||||
};
|
||||
}
|
||||
if (console.groupCollapsed == null) {
|
||||
console.groupCollapsed = console.group;
|
||||
}
|
||||
for (_i = 0, _len = functions.length; _i < _len; _i++) {
|
||||
fn = functions[_i];
|
||||
if (console[fn] == null) {
|
||||
console[fn] = function() {
|
||||
return console.log(_t("Not implemented:") + (" console." + name));
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.console = {};
|
||||
for (_j = 0, _len1 = functions.length; _j < _len1; _j++) {
|
||||
fn = functions[_j];
|
||||
this.console[fn] = function() {};
|
||||
}
|
||||
this.console['error'] = function() {
|
||||
var args;
|
||||
|
||||
args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
|
||||
return alert("ERROR: " + (args.join(', ')));
|
||||
};
|
||||
this.console['warn'] = function() {
|
||||
var args;
|
||||
|
||||
args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
|
||||
return alert("WARNING: " + (args.join(', ')));
|
||||
};
|
||||
}
|
||||
|
||||
Delegator = (function() {
|
||||
Delegator.prototype.events = {};
|
||||
|
||||
Delegator.prototype.options = {};
|
||||
|
||||
Delegator.prototype.element = null;
|
||||
|
||||
function Delegator(element, options) {
|
||||
this.options = $.extend(true, {}, this.options, options);
|
||||
this.element = $(element);
|
||||
this.on = this.subscribe;
|
||||
this.addEvents();
|
||||
}
|
||||
|
||||
Delegator.prototype.addEvents = function() {
|
||||
var event, functionName, sel, selector, _k, _ref1, _ref2, _results;
|
||||
|
||||
_ref1 = this.events;
|
||||
_results = [];
|
||||
for (sel in _ref1) {
|
||||
functionName = _ref1[sel];
|
||||
_ref2 = sel.split(' '), selector = 2 <= _ref2.length ? __slice.call(_ref2, 0, _k = _ref2.length - 1) : (_k = 0, []), event = _ref2[_k++];
|
||||
_results.push(this.addEvent(selector.join(' '), event, functionName));
|
||||
}
|
||||
return _results;
|
||||
};
|
||||
|
||||
Delegator.prototype.addEvent = function(bindTo, event, functionName) {
|
||||
var closure, isBlankSelector,
|
||||
_this = this;
|
||||
|
||||
closure = function() {
|
||||
return _this[functionName].apply(_this, arguments);
|
||||
};
|
||||
isBlankSelector = typeof bindTo === 'string' && bindTo.replace(/\s+/g, '') === '';
|
||||
if (isBlankSelector) {
|
||||
bindTo = this.element;
|
||||
}
|
||||
if (typeof bindTo === 'string') {
|
||||
this.element.delegate(bindTo, event, closure);
|
||||
} else {
|
||||
if (this.isCustomEvent(event)) {
|
||||
this.subscribe(event, closure);
|
||||
} else {
|
||||
$(bindTo).bind(event, closure);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
Delegator.prototype.isCustomEvent = function(event) {
|
||||
event = event.split('.')[0];
|
||||
return $.inArray(event, Delegator.natives) === -1;
|
||||
};
|
||||
|
||||
Delegator.prototype.publish = function() {
|
||||
this.element.triggerHandler.apply(this.element, arguments);
|
||||
return this;
|
||||
};
|
||||
|
||||
Delegator.prototype.subscribe = function(event, callback) {
|
||||
var closure;
|
||||
|
||||
closure = function() {
|
||||
return callback.apply(this, [].slice.call(arguments, 1));
|
||||
};
|
||||
closure.guid = callback.guid = ($.guid += 1);
|
||||
this.element.bind(event, closure);
|
||||
return this;
|
||||
};
|
||||
|
||||
Delegator.prototype.unsubscribe = function() {
|
||||
this.element.unbind.apply(this.element, arguments);
|
||||
return this;
|
||||
};
|
||||
|
||||
return Delegator;
|
||||
|
||||
})();
|
||||
|
||||
Delegator.natives = (function() {
|
||||
var key, specials, val;
|
||||
|
||||
specials = (function() {
|
||||
var _ref1, _results;
|
||||
|
||||
_ref1 = jQuery.event.special;
|
||||
_results = [];
|
||||
for (key in _ref1) {
|
||||
if (!__hasProp.call(_ref1, key)) continue;
|
||||
val = _ref1[key];
|
||||
_results.push(key);
|
||||
}
|
||||
return _results;
|
||||
})();
|
||||
return "blur focus focusin focusout load resize scroll unload click dblclick\nmousedown mouseup mousemove mouseover mouseout mouseenter mouseleave\nchange select submit keydown keypress keyup error".split(/[^a-z]+/).concat(specials);
|
||||
})();
|
||||
|
||||
Range = {};
|
||||
|
||||
Range.sniff = function(r) {
|
||||
if (r.commonAncestorContainer != null) {
|
||||
return new Range.BrowserRange(r);
|
||||
} else if (typeof r.start === "string") {
|
||||
return new Range.SerializedRange(r);
|
||||
} else if (r.start && typeof r.start === "object") {
|
||||
return new Range.NormalizedRange(r);
|
||||
} else {
|
||||
console.error(_t("Could not sniff range type"));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
Range.nodeFromXPath = function(xpath, root) {
|
||||
var customResolver, evaluateXPath, namespace, node, segment;
|
||||
|
||||
if (root == null) {
|
||||
root = document;
|
||||
}
|
||||
evaluateXPath = function(xp, nsResolver) {
|
||||
var exception;
|
||||
|
||||
if (nsResolver == null) {
|
||||
nsResolver = null;
|
||||
}
|
||||
try {
|
||||
return document.evaluate('.' + xp, root, nsResolver, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
||||
} catch (_error) {
|
||||
exception = _error;
|
||||
console.log("XPath evaluation failed.");
|
||||
console.log("Trying fallback...");
|
||||
return $.xpath(xp, root);
|
||||
}
|
||||
};
|
||||
if (!$.isXMLDoc(document.documentElement)) {
|
||||
return evaluateXPath(xpath);
|
||||
} else {
|
||||
customResolver = document.createNSResolver(document.ownerDocument === null ? document.documentElement : document.ownerDocument.documentElement);
|
||||
node = evaluateXPath(xpath, customResolver);
|
||||
if (!node) {
|
||||
xpath = ((function() {
|
||||
var _k, _len2, _ref1, _results;
|
||||
|
||||
_ref1 = xpath.split('/');
|
||||
_results = [];
|
||||
for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
|
||||
segment = _ref1[_k];
|
||||
if (segment && segment.indexOf(':') === -1) {
|
||||
_results.push(segment.replace(/^([a-z]+)/, 'xhtml:$1'));
|
||||
} else {
|
||||
_results.push(segment);
|
||||
}
|
||||
}
|
||||
return _results;
|
||||
})()).join('/');
|
||||
namespace = document.lookupNamespaceURI(null);
|
||||
customResolver = function(ns) {
|
||||
if (ns === 'xhtml') {
|
||||
return namespace;
|
||||
} else {
|
||||
return document.documentElement.getAttribute('xmlns:' + ns);
|
||||
}
|
||||
};
|
||||
node = evaluateXPath(xpath, customResolver);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
};
|
||||
|
||||
Range.RangeError = (function(_super) {
|
||||
__extends(RangeError, _super);
|
||||
|
||||
function RangeError(type, message, parent) {
|
||||
this.type = type;
|
||||
this.message = message;
|
||||
this.parent = parent != null ? parent : null;
|
||||
RangeError.__super__.constructor.call(this, this.message);
|
||||
}
|
||||
|
||||
return RangeError;
|
||||
|
||||
})(Error);
|
||||
|
||||
Range.BrowserRange = (function() {
|
||||
function BrowserRange(obj) {
|
||||
this.commonAncestorContainer = obj.commonAncestorContainer;
|
||||
this.startContainer = obj.startContainer;
|
||||
this.startOffset = obj.startOffset;
|
||||
this.endContainer = obj.endContainer;
|
||||
this.endOffset = obj.endOffset;
|
||||
}
|
||||
|
||||
BrowserRange.prototype.normalize = function(root) {
|
||||
var it, node, nr, offset, p, r, _k, _len2, _ref1;
|
||||
|
||||
if (this.tainted) {
|
||||
console.error(_t("You may only call normalize() once on a BrowserRange!"));
|
||||
return false;
|
||||
} else {
|
||||
this.tainted = true;
|
||||
}
|
||||
r = {};
|
||||
nr = {};
|
||||
_ref1 = ['start', 'end'];
|
||||
for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
|
||||
p = _ref1[_k];
|
||||
node = this[p + 'Container'];
|
||||
offset = this[p + 'Offset'];
|
||||
if (node.nodeType === 1) {
|
||||
it = node.childNodes[offset];
|
||||
node = it || node.childNodes[offset - 1];
|
||||
if (node.nodeType === 1 && !node.firstChild) {
|
||||
it = null;
|
||||
node = node.previousSibling;
|
||||
}
|
||||
while (node.nodeType !== 3) {
|
||||
node = node.firstChild;
|
||||
}
|
||||
offset = it ? 0 : node.nodeValue.length;
|
||||
}
|
||||
r[p] = node;
|
||||
r[p + 'Offset'] = offset;
|
||||
}
|
||||
nr.start = r.startOffset > 0 ? r.start.splitText(r.startOffset) : r.start;
|
||||
if (r.start === r.end) {
|
||||
if ((r.endOffset - r.startOffset) < nr.start.nodeValue.length) {
|
||||
nr.start.splitText(r.endOffset - r.startOffset);
|
||||
}
|
||||
nr.end = nr.start;
|
||||
} else {
|
||||
if (r.endOffset < r.end.nodeValue.length) {
|
||||
r.end.splitText(r.endOffset);
|
||||
}
|
||||
nr.end = r.end;
|
||||
}
|
||||
nr.commonAncestor = this.commonAncestorContainer;
|
||||
while (nr.commonAncestor.nodeType !== 1) {
|
||||
nr.commonAncestor = nr.commonAncestor.parentNode;
|
||||
}
|
||||
return new Range.NormalizedRange(nr);
|
||||
};
|
||||
|
||||
BrowserRange.prototype.serialize = function(root, ignoreSelector) {
|
||||
return this.normalize(root).serialize(root, ignoreSelector);
|
||||
};
|
||||
|
||||
return BrowserRange;
|
||||
|
||||
})();
|
||||
|
||||
Range.NormalizedRange = (function() {
|
||||
function NormalizedRange(obj) {
|
||||
this.commonAncestor = obj.commonAncestor;
|
||||
this.start = obj.start;
|
||||
this.end = obj.end;
|
||||
}
|
||||
|
||||
NormalizedRange.prototype.normalize = function(root) {
|
||||
return this;
|
||||
};
|
||||
|
||||
NormalizedRange.prototype.limit = function(bounds) {
|
||||
var nodes, parent, startParents, _k, _len2, _ref1;
|
||||
|
||||
nodes = $.grep(this.textNodes(), function(node) {
|
||||
return node.parentNode === bounds || $.contains(bounds, node.parentNode);
|
||||
});
|
||||
if (!nodes.length) {
|
||||
return null;
|
||||
}
|
||||
this.start = nodes[0];
|
||||
this.end = nodes[nodes.length - 1];
|
||||
startParents = $(this.start).parents();
|
||||
_ref1 = $(this.end).parents();
|
||||
for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
|
||||
parent = _ref1[_k];
|
||||
if (startParents.index(parent) !== -1) {
|
||||
this.commonAncestor = parent;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
NormalizedRange.prototype.serialize = function(root, ignoreSelector) {
|
||||
var end, serialization, start;
|
||||
|
||||
serialization = function(node, isEnd) {
|
||||
var n, nodes, offset, origParent, textNodes, xpath, _k, _len2;
|
||||
|
||||
if (ignoreSelector) {
|
||||
origParent = $(node).parents(":not(" + ignoreSelector + ")").eq(0);
|
||||
} else {
|
||||
origParent = $(node).parent();
|
||||
}
|
||||
xpath = origParent.xpath(root)[0];
|
||||
textNodes = origParent.textNodes();
|
||||
nodes = textNodes.slice(0, textNodes.index(node));
|
||||
offset = 0;
|
||||
for (_k = 0, _len2 = nodes.length; _k < _len2; _k++) {
|
||||
n = nodes[_k];
|
||||
offset += n.nodeValue.length;
|
||||
}
|
||||
if (isEnd) {
|
||||
return [xpath, offset + node.nodeValue.length];
|
||||
} else {
|
||||
return [xpath, offset];
|
||||
}
|
||||
};
|
||||
start = serialization(this.start);
|
||||
end = serialization(this.end, true);
|
||||
return new Range.SerializedRange({
|
||||
start: start[0],
|
||||
end: end[0],
|
||||
startOffset: start[1],
|
||||
endOffset: end[1]
|
||||
});
|
||||
};
|
||||
|
||||
NormalizedRange.prototype.text = function() {
|
||||
var node;
|
||||
|
||||
return ((function() {
|
||||
var _k, _len2, _ref1, _results;
|
||||
|
||||
_ref1 = this.textNodes();
|
||||
_results = [];
|
||||
for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
|
||||
node = _ref1[_k];
|
||||
_results.push(node.nodeValue);
|
||||
}
|
||||
return _results;
|
||||
}).call(this)).join('');
|
||||
};
|
||||
|
||||
NormalizedRange.prototype.textNodes = function() {
|
||||
var end, start, textNodes, _ref1;
|
||||
|
||||
textNodes = $(this.commonAncestor).textNodes();
|
||||
_ref1 = [textNodes.index(this.start), textNodes.index(this.end)], start = _ref1[0], end = _ref1[1];
|
||||
return $.makeArray(textNodes.slice(start, +end + 1 || 9e9));
|
||||
};
|
||||
|
||||
NormalizedRange.prototype.toRange = function() {
|
||||
var range;
|
||||
|
||||
range = document.createRange();
|
||||
range.setStartBefore(this.start);
|
||||
range.setEndAfter(this.end);
|
||||
return range;
|
||||
};
|
||||
|
||||
return NormalizedRange;
|
||||
|
||||
})();
|
||||
|
||||
Range.SerializedRange = (function() {
|
||||
function SerializedRange(obj) {
|
||||
this.start = obj.start;
|
||||
this.startOffset = obj.startOffset;
|
||||
this.end = obj.end;
|
||||
this.endOffset = obj.endOffset;
|
||||
}
|
||||
|
||||
SerializedRange.prototype.normalize = function(root) {
|
||||
var contains, e, length, node, p, range, tn, _k, _l, _len2, _len3, _ref1, _ref2;
|
||||
|
||||
range = {};
|
||||
_ref1 = ['start', 'end'];
|
||||
for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
|
||||
p = _ref1[_k];
|
||||
try {
|
||||
node = Range.nodeFromXPath(this[p], root);
|
||||
} catch (_error) {
|
||||
e = _error;
|
||||
throw new Range.RangeError(p, ("Error while finding " + p + " node: " + this[p] + ": ") + e, e);
|
||||
}
|
||||
if (!node) {
|
||||
throw new Range.RangeError(p, "Couldn't find " + p + " node: " + this[p]);
|
||||
}
|
||||
length = 0;
|
||||
_ref2 = $(node).textNodes();
|
||||
for (_l = 0, _len3 = _ref2.length; _l < _len3; _l++) {
|
||||
tn = _ref2[_l];
|
||||
if (length + tn.nodeValue.length >= this[p + 'Offset']) {
|
||||
range[p + 'Container'] = tn;
|
||||
range[p + 'Offset'] = this[p + 'Offset'] - length;
|
||||
break;
|
||||
} else {
|
||||
length += tn.nodeValue.length;
|
||||
}
|
||||
}
|
||||
if (range[p + 'Offset'] == null) {
|
||||
throw new Range.RangeError("" + p + "offset", "Couldn't find offset " + this[p + 'Offset'] + " in element " + this[p]);
|
||||
}
|
||||
}
|
||||
contains = document.compareDocumentPosition == null ? function(a, b) {
|
||||
return a.contains(b);
|
||||
} : function(a, b) {
|
||||
return a.compareDocumentPosition(b) & 16;
|
||||
};
|
||||
$(range.startContainer).parents().each(function() {
|
||||
if (contains(this, range.endContainer)) {
|
||||
range.commonAncestorContainer = this;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return new Range.BrowserRange(range).normalize(root);
|
||||
};
|
||||
|
||||
SerializedRange.prototype.serialize = function(root, ignoreSelector) {
|
||||
return this.normalize(root).serialize(root, ignoreSelector);
|
||||
};
|
||||
|
||||
SerializedRange.prototype.toObject = function() {
|
||||
return {
|
||||
start: this.start,
|
||||
startOffset: this.startOffset,
|
||||
end: this.end,
|
||||
endOffset: this.endOffset
|
||||
};
|
||||
};
|
||||
|
||||
return SerializedRange;
|
||||
|
||||
})();
|
||||
|
||||
util = {
|
||||
uuid: (function() {
|
||||
var counter;
|
||||
|
||||
counter = 0;
|
||||
return function() {
|
||||
return counter++;
|
||||
};
|
||||
})(),
|
||||
getGlobal: function() {
|
||||
return (function() {
|
||||
return this;
|
||||
})();
|
||||
},
|
||||
maxZIndex: function($elements) {
|
||||
var all, el;
|
||||
|
||||
all = (function() {
|
||||
var _k, _len2, _results;
|
||||
|
||||
_results = [];
|
||||
for (_k = 0, _len2 = $elements.length; _k < _len2; _k++) {
|
||||
el = $elements[_k];
|
||||
if ($(el).css('position') === 'static') {
|
||||
_results.push(-1);
|
||||
} else {
|
||||
_results.push(parseInt($(el).css('z-index'), 10) || -1);
|
||||
}
|
||||
}
|
||||
return _results;
|
||||
})();
|
||||
return Math.max.apply(Math, all);
|
||||
},
|
||||
mousePosition: function(e, offsetEl) {
|
||||
var offset;
|
||||
|
||||
offset = $(offsetEl).position();
|
||||
return {
|
||||
top: e.pageY - offset.top,
|
||||
left: e.pageX - offset.left
|
||||
};
|
||||
},
|
||||
preventEventDefault: function(event) {
|
||||
return event != null ? typeof event.preventDefault === "function" ? event.preventDefault() : void 0 : void 0;
|
||||
}
|
||||
};
|
||||
|
||||
_Annotator = this.Annotator;
|
||||
|
||||
Annotator = (function(_super) {
|
||||
__extends(Annotator, _super);
|
||||
|
||||
Annotator.prototype.events = {
|
||||
".annotator-adder button click": "onAdderClick",
|
||||
".annotator-adder button mousedown": "onAdderMousedown",
|
||||
".annotator-hl mouseover": "onHighlightMouseover",
|
||||
".annotator-hl mouseout": "startViewerHideTimer"
|
||||
};
|
||||
|
||||
Annotator.prototype.html = {
|
||||
adder: '<div class="annotator-adder"><button>' + _t('Annotate') + '</button></div>',
|
||||
wrapper: '<div class="annotator-wrapper"></div>'
|
||||
};
|
||||
|
||||
Annotator.prototype.options = {
|
||||
readOnly: false
|
||||
};
|
||||
|
||||
Annotator.prototype.plugins = {};
|
||||
|
||||
Annotator.prototype.editor = null;
|
||||
|
||||
Annotator.prototype.viewer = null;
|
||||
|
||||
Annotator.prototype.selectedRanges = null;
|
||||
|
||||
Annotator.prototype.mouseIsDown = false;
|
||||
|
||||
Annotator.prototype.ignoreMouseup = false;
|
||||
|
||||
Annotator.prototype.viewerHideTimer = null;
|
||||
|
||||
function Annotator(element, options) {
|
||||
this.onDeleteAnnotation = __bind(this.onDeleteAnnotation, this);
|
||||
this.onEditAnnotation = __bind(this.onEditAnnotation, this);
|
||||
this.onAdderClick = __bind(this.onAdderClick, this);
|
||||
this.onAdderMousedown = __bind(this.onAdderMousedown, this);
|
||||
this.onHighlightMouseover = __bind(this.onHighlightMouseover, this);
|
||||
this.checkForEndSelection = __bind(this.checkForEndSelection, this);
|
||||
this.checkForStartSelection = __bind(this.checkForStartSelection, this);
|
||||
this.clearViewerHideTimer = __bind(this.clearViewerHideTimer, this);
|
||||
this.startViewerHideTimer = __bind(this.startViewerHideTimer, this);
|
||||
this.showViewer = __bind(this.showViewer, this);
|
||||
this.onEditorSubmit = __bind(this.onEditorSubmit, this);
|
||||
this.onEditorHide = __bind(this.onEditorHide, this);
|
||||
this.showEditor = __bind(this.showEditor, this); Annotator.__super__.constructor.apply(this, arguments);
|
||||
this.plugins = {};
|
||||
if (!Annotator.supported()) {
|
||||
return this;
|
||||
}
|
||||
if (!this.options.readOnly) {
|
||||
this._setupDocumentEvents();
|
||||
}
|
||||
this._setupWrapper()._setupViewer()._setupEditor();
|
||||
this._setupDynamicStyle();
|
||||
this.adder = $(this.html.adder).appendTo(this.wrapper).hide();
|
||||
}
|
||||
|
||||
Annotator.prototype._setupWrapper = function() {
|
||||
this.wrapper = $(this.html.wrapper);
|
||||
this.element.find('script').remove();
|
||||
this.element.wrapInner(this.wrapper);
|
||||
this.wrapper = this.element.find('.annotator-wrapper');
|
||||
return this;
|
||||
};
|
||||
|
||||
Annotator.prototype._setupViewer = function() {
|
||||
var _this = this;
|
||||
|
||||
this.viewer = new Annotator.Viewer({
|
||||
readOnly: this.options.readOnly
|
||||
});
|
||||
this.viewer.hide().on("edit", this.onEditAnnotation).on("delete", this.onDeleteAnnotation).addField({
|
||||
load: function(field, annotation) {
|
||||
if (annotation.text) {
|
||||
$(field).escape(annotation.text);
|
||||
} else {
|
||||
$(field).html("<i>" + (_t('No Comment')) + "</i>");
|
||||
}
|
||||
return _this.publish('annotationViewerTextField', [field, annotation]);
|
||||
}
|
||||
}).element.appendTo(this.wrapper).bind({
|
||||
"mouseover": this.clearViewerHideTimer,
|
||||
"mouseout": this.startViewerHideTimer
|
||||
});
|
||||
return this;
|
||||
};
|
||||
|
||||
Annotator.prototype._setupEditor = function() {
|
||||
this.editor = new Annotator.Editor();
|
||||
this.editor.hide().on('hide', this.onEditorHide).on('save', this.onEditorSubmit).addField({
|
||||
type: 'textarea',
|
||||
label: _t('Comments') + '\u2026',
|
||||
load: function(field, annotation) {
|
||||
return $(field).find('textarea').val(annotation.text || '');
|
||||
},
|
||||
submit: function(field, annotation) {
|
||||
return annotation.text = $(field).find('textarea').val();
|
||||
}
|
||||
});
|
||||
this.editor.element.appendTo(this.wrapper);
|
||||
return this;
|
||||
};
|
||||
|
||||
Annotator.prototype._setupDocumentEvents = function() {
|
||||
$(document).bind({
|
||||
"mouseup": this.checkForEndSelection,
|
||||
"mousedown": this.checkForStartSelection
|
||||
});
|
||||
return this;
|
||||
};
|
||||
|
||||
Annotator.prototype._setupDynamicStyle = function() {
|
||||
var max, sel, style, x;
|
||||
|
||||
style = $('#annotator-dynamic-style');
|
||||
if (!style.length) {
|
||||
style = $('<style id="annotator-dynamic-style"></style>').appendTo(document.head);
|
||||
}
|
||||
sel = '*' + ((function() {
|
||||
var _k, _len2, _ref1, _results;
|
||||
|
||||
_ref1 = ['adder', 'outer', 'notice', 'filter'];
|
||||
_results = [];
|
||||
for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
|
||||
x = _ref1[_k];
|
||||
_results.push(":not(.annotator-" + x + ")");
|
||||
}
|
||||
return _results;
|
||||
})()).join('');
|
||||
max = util.maxZIndex($(document.body).find(sel));
|
||||
max = Math.max(max, 1000);
|
||||
style.text([".annotator-adder, .annotator-outer, .annotator-notice {", " z-index: " + (max + 20) + ";", "}", ".annotator-filter {", " z-index: " + (max + 10) + ";", "}"].join("\n"));
|
||||
return this;
|
||||
};
|
||||
|
||||
Annotator.prototype.getSelectedRanges = function() {
|
||||
var browserRange, i, normedRange, r, ranges, rangesToIgnore, selection, _k, _len2;
|
||||
|
||||
selection = util.getGlobal().getSelection();
|
||||
ranges = [];
|
||||
rangesToIgnore = [];
|
||||
if (!selection.isCollapsed) {
|
||||
ranges = (function() {
|
||||
var _k, _ref1, _results;
|
||||
|
||||
_results = [];
|
||||
for (i = _k = 0, _ref1 = selection.rangeCount; 0 <= _ref1 ? _k < _ref1 : _k > _ref1; i = 0 <= _ref1 ? ++_k : --_k) {
|
||||
r = selection.getRangeAt(i);
|
||||
browserRange = new Range.BrowserRange(r);
|
||||
normedRange = browserRange.normalize().limit(this.wrapper[0]);
|
||||
if (normedRange === null) {
|
||||
rangesToIgnore.push(r);
|
||||
}
|
||||
_results.push(normedRange);
|
||||
}
|
||||
return _results;
|
||||
}).call(this);
|
||||
selection.removeAllRanges();
|
||||
}
|
||||
for (_k = 0, _len2 = rangesToIgnore.length; _k < _len2; _k++) {
|
||||
r = rangesToIgnore[_k];
|
||||
selection.addRange(r);
|
||||
}
|
||||
return $.grep(ranges, function(range) {
|
||||
if (range) {
|
||||
selection.addRange(range.toRange());
|
||||
}
|
||||
return range;
|
||||
});
|
||||
};
|
||||
|
||||
Annotator.prototype.createAnnotation = function() {
|
||||
var annotation;
|
||||
|
||||
annotation = {};
|
||||
this.publish('beforeAnnotationCreated', [annotation]);
|
||||
return annotation;
|
||||
};
|
||||
|
||||
Annotator.prototype.setupAnnotation = function(annotation) {
|
||||
var e, normed, normedRanges, r, root, _k, _l, _len2, _len3, _ref1;
|
||||
|
||||
root = this.wrapper[0];
|
||||
annotation.ranges || (annotation.ranges = this.selectedRanges);
|
||||
normedRanges = [];
|
||||
_ref1 = annotation.ranges;
|
||||
for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
|
||||
r = _ref1[_k];
|
||||
try {
|
||||
normedRanges.push(Range.sniff(r).normalize(root));
|
||||
} catch (_error) {
|
||||
e = _error;
|
||||
if (e instanceof Range.RangeError) {
|
||||
this.publish('rangeNormalizeFail', [annotation, r, e]);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
annotation.quote = [];
|
||||
annotation.ranges = [];
|
||||
annotation.highlights = [];
|
||||
for (_l = 0, _len3 = normedRanges.length; _l < _len3; _l++) {
|
||||
normed = normedRanges[_l];
|
||||
annotation.quote.push($.trim(normed.text()));
|
||||
annotation.ranges.push(normed.serialize(this.wrapper[0], '.annotator-hl'));
|
||||
$.merge(annotation.highlights, this.highlightRange(normed));
|
||||
}
|
||||
annotation.quote = annotation.quote.join(' / ');
|
||||
$(annotation.highlights).data('annotation', annotation);
|
||||
return annotation;
|
||||
};
|
||||
|
||||
Annotator.prototype.updateAnnotation = function(annotation) {
|
||||
this.publish('beforeAnnotationUpdated', [annotation]);
|
||||
this.publish('annotationUpdated', [annotation]);
|
||||
return annotation;
|
||||
};
|
||||
|
||||
Annotator.prototype.deleteAnnotation = function(annotation) {
|
||||
var child, h, _k, _len2, _ref1;
|
||||
|
||||
if (annotation.highlights != null) {
|
||||
_ref1 = annotation.highlights;
|
||||
for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
|
||||
h = _ref1[_k];
|
||||
if (!(h.parentNode != null)) {
|
||||
continue;
|
||||
}
|
||||
child = h.childNodes[0];
|
||||
$(h).replaceWith(h.childNodes);
|
||||
}
|
||||
}
|
||||
this.publish('annotationDeleted', [annotation]);
|
||||
return annotation;
|
||||
};
|
||||
|
||||
Annotator.prototype.loadAnnotations = function(annotations) {
|
||||
var clone, loader,
|
||||
_this = this;
|
||||
|
||||
if (annotations == null) {
|
||||
annotations = [];
|
||||
}
|
||||
loader = function(annList) {
|
||||
var n, now, _k, _len2;
|
||||
|
||||
if (annList == null) {
|
||||
annList = [];
|
||||
}
|
||||
now = annList.splice(0, 10);
|
||||
for (_k = 0, _len2 = now.length; _k < _len2; _k++) {
|
||||
n = now[_k];
|
||||
_this.setupAnnotation(n);
|
||||
}
|
||||
if (annList.length > 0) {
|
||||
return setTimeout((function() {
|
||||
return loader(annList);
|
||||
}), 10);
|
||||
} else {
|
||||
return _this.publish('annotationsLoaded', [clone]);
|
||||
}
|
||||
};
|
||||
clone = annotations.slice();
|
||||
if (annotations.length) {
|
||||
loader(annotations);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
Annotator.prototype.dumpAnnotations = function() {
|
||||
if (this.plugins['Store']) {
|
||||
return this.plugins['Store'].dumpAnnotations();
|
||||
} else {
|
||||
console.warn(_t("Can't dump annotations without Store plugin."));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
Annotator.prototype.highlightRange = function(normedRange, cssClass) {
|
||||
var hl, node, white, _k, _len2, _ref1, _results;
|
||||
|
||||
if (cssClass == null) {
|
||||
cssClass = 'annotator-hl';
|
||||
}
|
||||
white = /^\s*$/;
|
||||
hl = $("<span class='" + cssClass + "'></span>");
|
||||
_ref1 = normedRange.textNodes();
|
||||
_results = [];
|
||||
for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
|
||||
node = _ref1[_k];
|
||||
if (!white.test(node.nodeValue)) {
|
||||
_results.push($(node).wrapAll(hl).parent().show()[0]);
|
||||
}
|
||||
}
|
||||
return _results;
|
||||
};
|
||||
|
||||
Annotator.prototype.highlightRanges = function(normedRanges, cssClass) {
|
||||
var highlights, r, _k, _len2;
|
||||
|
||||
if (cssClass == null) {
|
||||
cssClass = 'annotator-hl';
|
||||
}
|
||||
highlights = [];
|
||||
for (_k = 0, _len2 = normedRanges.length; _k < _len2; _k++) {
|
||||
r = normedRanges[_k];
|
||||
$.merge(highlights, this.highlightRange(r, cssClass));
|
||||
}
|
||||
return highlights;
|
||||
};
|
||||
|
||||
Annotator.prototype.addPlugin = function(name, options) {
|
||||
var klass, _base;
|
||||
|
||||
if (this.plugins[name]) {
|
||||
console.error(_t("You cannot have more than one instance of any plugin."));
|
||||
} else {
|
||||
klass = Annotator.Plugin[name];
|
||||
if (typeof klass === 'function') {
|
||||
this.plugins[name] = new klass(this.element[0], options);
|
||||
this.plugins[name].annotator = this;
|
||||
if (typeof (_base = this.plugins[name]).pluginInit === "function") {
|
||||
_base.pluginInit();
|
||||
}
|
||||
} else {
|
||||
console.error(_t("Could not load ") + name + _t(" plugin. Have you included the appropriate <script> tag?"));
|
||||
}
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
Annotator.prototype.showEditor = function(annotation, location) {
|
||||
this.editor.element.css(location);
|
||||
this.editor.load(annotation);
|
||||
this.publish('annotationEditorShown', [this.editor, annotation]);
|
||||
return this;
|
||||
};
|
||||
|
||||
Annotator.prototype.onEditorHide = function() {
|
||||
this.publish('annotationEditorHidden', [this.editor]);
|
||||
return this.ignoreMouseup = false;
|
||||
};
|
||||
|
||||
Annotator.prototype.onEditorSubmit = function(annotation) {
|
||||
return this.publish('annotationEditorSubmit', [this.editor, annotation]);
|
||||
};
|
||||
|
||||
Annotator.prototype.showViewer = function(annotations, location) {
|
||||
this.viewer.element.css(location);
|
||||
this.viewer.load(annotations);
|
||||
return this.publish('annotationViewerShown', [this.viewer, annotations]);
|
||||
};
|
||||
|
||||
Annotator.prototype.startViewerHideTimer = function() {
|
||||
if (!this.viewerHideTimer) {
|
||||
return this.viewerHideTimer = setTimeout(this.viewer.hide, 250);
|
||||
}
|
||||
};
|
||||
|
||||
Annotator.prototype.clearViewerHideTimer = function() {
|
||||
clearTimeout(this.viewerHideTimer);
|
||||
return this.viewerHideTimer = false;
|
||||
};
|
||||
|
||||
Annotator.prototype.checkForStartSelection = function(event) {
|
||||
if (!(event && this.isAnnotator(event.target))) {
|
||||
this.startViewerHideTimer();
|
||||
return this.mouseIsDown = true;
|
||||
}
|
||||
};
|
||||
|
||||
Annotator.prototype.checkForEndSelection = function(event) {
|
||||
var container, range, _k, _len2, _ref1;
|
||||
|
||||
this.mouseIsDown = false;
|
||||
if (this.ignoreMouseup) {
|
||||
return;
|
||||
}
|
||||
this.selectedRanges = this.getSelectedRanges();
|
||||
_ref1 = this.selectedRanges;
|
||||
for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
|
||||
range = _ref1[_k];
|
||||
container = range.commonAncestor;
|
||||
if ($(container).hasClass('annotator-hl')) {
|
||||
container = $(container).parents('[class^=annotator-hl]')[0];
|
||||
}
|
||||
if (this.isAnnotator(container)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (event && this.selectedRanges.length) {
|
||||
return this.adder.css(util.mousePosition(event, this.wrapper[0])).show();
|
||||
} else {
|
||||
return this.adder.hide();
|
||||
}
|
||||
};
|
||||
|
||||
Annotator.prototype.isAnnotator = function(element) {
|
||||
return !!$(element).parents().andSelf().filter('[class^=annotator-]').not(this.wrapper).length;
|
||||
};
|
||||
|
||||
Annotator.prototype.onHighlightMouseover = function(event) {
|
||||
var annotations;
|
||||
|
||||
this.clearViewerHideTimer();
|
||||
if (this.mouseIsDown || this.viewer.isShown()) {
|
||||
return false;
|
||||
}
|
||||
annotations = $(event.target).parents('.annotator-hl').andSelf().map(function() {
|
||||
return $(this).data("annotation");
|
||||
});
|
||||
return this.showViewer($.makeArray(annotations), util.mousePosition(event, this.wrapper[0]));
|
||||
};
|
||||
|
||||
Annotator.prototype.onAdderMousedown = function(event) {
|
||||
if (event != null) {
|
||||
event.preventDefault();
|
||||
}
|
||||
return this.ignoreMouseup = true;
|
||||
};
|
||||
|
||||
Annotator.prototype.onAdderClick = function(event) {
|
||||
var annotation, cancel, cleanup, position, save,
|
||||
_this = this;
|
||||
|
||||
if (event != null) {
|
||||
event.preventDefault();
|
||||
}
|
||||
position = this.adder.position();
|
||||
this.adder.hide();
|
||||
annotation = this.setupAnnotation(this.createAnnotation());
|
||||
$(annotation.highlights).addClass('annotator-hl-temporary');
|
||||
save = function() {
|
||||
cleanup();
|
||||
$(annotation.highlights).removeClass('annotator-hl-temporary');
|
||||
return _this.publish('annotationCreated', [annotation]);
|
||||
};
|
||||
cancel = function() {
|
||||
cleanup();
|
||||
return _this.deleteAnnotation(annotation);
|
||||
};
|
||||
cleanup = function() {
|
||||
_this.unsubscribe('annotationEditorHidden', cancel);
|
||||
return _this.unsubscribe('annotationEditorSubmit', save);
|
||||
};
|
||||
this.subscribe('annotationEditorHidden', cancel);
|
||||
this.subscribe('annotationEditorSubmit', save);
|
||||
return this.showEditor(annotation, position);
|
||||
};
|
||||
|
||||
Annotator.prototype.onEditAnnotation = function(annotation) {
|
||||
var cleanup, offset, update,
|
||||
_this = this;
|
||||
|
||||
offset = this.viewer.element.position();
|
||||
update = function() {
|
||||
cleanup();
|
||||
return _this.updateAnnotation(annotation);
|
||||
};
|
||||
cleanup = function() {
|
||||
_this.unsubscribe('annotationEditorHidden', cleanup);
|
||||
return _this.unsubscribe('annotationEditorSubmit', update);
|
||||
};
|
||||
this.subscribe('annotationEditorHidden', cleanup);
|
||||
this.subscribe('annotationEditorSubmit', update);
|
||||
this.viewer.hide();
|
||||
return this.showEditor(annotation, offset);
|
||||
};
|
||||
|
||||
Annotator.prototype.onDeleteAnnotation = function(annotation) {
|
||||
this.viewer.hide();
|
||||
return this.deleteAnnotation(annotation);
|
||||
};
|
||||
|
||||
return Annotator;
|
||||
|
||||
})(Delegator);
|
||||
|
||||
Annotator.Plugin = (function(_super) {
|
||||
__extends(Plugin, _super);
|
||||
|
||||
function Plugin(element, options) {
|
||||
Plugin.__super__.constructor.apply(this, arguments);
|
||||
}
|
||||
|
||||
Plugin.prototype.pluginInit = function() {};
|
||||
|
||||
return Plugin;
|
||||
|
||||
})(Delegator);
|
||||
|
||||
g = util.getGlobal();
|
||||
|
||||
if (((_ref1 = g.document) != null ? _ref1.evaluate : void 0) == null) {
|
||||
$.getScript('http://assets.annotateit.org/vendor/xpath.min.js');
|
||||
}
|
||||
|
||||
if (g.getSelection == null) {
|
||||
$.getScript('http://assets.annotateit.org/vendor/ierange.min.js');
|
||||
}
|
||||
|
||||
if (g.JSON == null) {
|
||||
$.getScript('http://assets.annotateit.org/vendor/json2.min.js');
|
||||
}
|
||||
|
||||
Annotator.$ = $;
|
||||
|
||||
Annotator.Delegator = Delegator;
|
||||
|
||||
Annotator.Range = Range;
|
||||
|
||||
Annotator._t = _t;
|
||||
|
||||
Annotator.supported = function() {
|
||||
return (function() {
|
||||
return !!this.getSelection;
|
||||
})();
|
||||
};
|
||||
|
||||
Annotator.noConflict = function() {
|
||||
util.getGlobal().Annotator = _Annotator;
|
||||
return this;
|
||||
};
|
||||
|
||||
$.plugin('annotator', Annotator);
|
||||
|
||||
this.Annotator = Annotator;
|
||||
|
||||
Annotator.Widget = (function(_super) {
|
||||
__extends(Widget, _super);
|
||||
|
||||
Widget.prototype.classes = {
|
||||
hide: 'annotator-hide',
|
||||
invert: {
|
||||
x: 'annotator-invert-x',
|
||||
y: 'annotator-invert-y'
|
||||
}
|
||||
};
|
||||
|
||||
function Widget(element, options) {
|
||||
Widget.__super__.constructor.apply(this, arguments);
|
||||
this.classes = $.extend({}, Annotator.Widget.prototype.classes, this.classes);
|
||||
}
|
||||
|
||||
Widget.prototype.checkOrientation = function() {
|
||||
var current, offset, viewport, widget, window;
|
||||
|
||||
this.resetOrientation();
|
||||
window = $(util.getGlobal());
|
||||
widget = this.element.children(":first");
|
||||
offset = widget.offset();
|
||||
viewport = {
|
||||
top: window.scrollTop(),
|
||||
right: window.width() + window.scrollLeft()
|
||||
};
|
||||
current = {
|
||||
top: offset.top,
|
||||
right: offset.left + widget.width()
|
||||
};
|
||||
if ((current.top - viewport.top) < 0) {
|
||||
this.invertY();
|
||||
}
|
||||
if ((current.right - viewport.right) > 0) {
|
||||
this.invertX();
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
Widget.prototype.resetOrientation = function() {
|
||||
this.element.removeClass(this.classes.invert.x).removeClass(this.classes.invert.y);
|
||||
return this;
|
||||
};
|
||||
|
||||
Widget.prototype.invertX = function() {
|
||||
this.element.addClass(this.classes.invert.x);
|
||||
return this;
|
||||
};
|
||||
|
||||
Widget.prototype.invertY = function() {
|
||||
this.element.addClass(this.classes.invert.y);
|
||||
return this;
|
||||
};
|
||||
|
||||
Widget.prototype.isInvertedY = function() {
|
||||
return this.element.hasClass(this.classes.invert.y);
|
||||
};
|
||||
|
||||
Widget.prototype.isInvertedX = function() {
|
||||
return this.element.hasClass(this.classes.invert.x);
|
||||
};
|
||||
|
||||
return Widget;
|
||||
|
||||
})(Delegator);
|
||||
|
||||
Annotator.Editor = (function(_super) {
|
||||
__extends(Editor, _super);
|
||||
|
||||
Editor.prototype.events = {
|
||||
"form submit": "submit",
|
||||
".annotator-save click": "submit",
|
||||
".annotator-cancel click": "hide",
|
||||
".annotator-cancel mouseover": "onCancelButtonMouseover",
|
||||
"textarea keydown": "processKeypress"
|
||||
};
|
||||
|
||||
Editor.prototype.classes = {
|
||||
hide: 'annotator-hide',
|
||||
focus: 'annotator-focus'
|
||||
};
|
||||
|
||||
Editor.prototype.html = "<div class=\"annotator-outer annotator-editor\">\n <form class=\"annotator-widget\">\n <ul class=\"annotator-listing\"></ul>\n <div class=\"annotator-controls\">\n <a href=\"#cancel\" class=\"annotator-cancel\">" + _t('Cancel') + "</a>\n<a href=\"#save\" class=\"annotator-save annotator-focus\">" + _t('Save') + "</a>\n </div>\n </form>\n</div>";
|
||||
|
||||
Editor.prototype.options = {};
|
||||
|
||||
function Editor(options) {
|
||||
this.onCancelButtonMouseover = __bind(this.onCancelButtonMouseover, this);
|
||||
this.processKeypress = __bind(this.processKeypress, this);
|
||||
this.submit = __bind(this.submit, this);
|
||||
this.load = __bind(this.load, this);
|
||||
this.hide = __bind(this.hide, this);
|
||||
this.show = __bind(this.show, this); Editor.__super__.constructor.call(this, $(this.html)[0], options);
|
||||
this.fields = [];
|
||||
this.annotation = {};
|
||||
}
|
||||
|
||||
Editor.prototype.show = function(event) {
|
||||
util.preventEventDefault(event);
|
||||
this.element.removeClass(this.classes.hide);
|
||||
this.element.find('.annotator-save').addClass(this.classes.focus);
|
||||
this.checkOrientation();
|
||||
this.element.find(":input:first").focus();
|
||||
this.setupDraggables();
|
||||
return this.publish('show');
|
||||
};
|
||||
|
||||
Editor.prototype.hide = function(event) {
|
||||
util.preventEventDefault(event);
|
||||
this.element.addClass(this.classes.hide);
|
||||
return this.publish('hide');
|
||||
};
|
||||
|
||||
Editor.prototype.load = function(annotation) {
|
||||
var field, _k, _len2, _ref2;
|
||||
|
||||
this.annotation = annotation;
|
||||
this.publish('load', [this.annotation]);
|
||||
_ref2 = this.fields;
|
||||
for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) {
|
||||
field = _ref2[_k];
|
||||
field.load(field.element, this.annotation);
|
||||
}
|
||||
return this.show();
|
||||
};
|
||||
|
||||
Editor.prototype.submit = function(event) {
|
||||
var field, _k, _len2, _ref2;
|
||||
|
||||
util.preventEventDefault(event);
|
||||
_ref2 = this.fields;
|
||||
for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) {
|
||||
field = _ref2[_k];
|
||||
field.submit(field.element, this.annotation);
|
||||
}
|
||||
this.publish('save', [this.annotation]);
|
||||
return this.hide();
|
||||
};
|
||||
|
||||
Editor.prototype.addField = function(options) {
|
||||
var element, field, input;
|
||||
|
||||
field = $.extend({
|
||||
id: 'annotator-field-' + util.uuid(),
|
||||
type: 'input',
|
||||
label: '',
|
||||
load: function() {},
|
||||
submit: function() {}
|
||||
}, options);
|
||||
input = null;
|
||||
element = $('<li class="annotator-item" />');
|
||||
field.element = element[0];
|
||||
switch (field.type) {
|
||||
case 'textarea':
|
||||
input = $('<textarea />');
|
||||
break;
|
||||
case 'input':
|
||||
case 'checkbox':
|
||||
input = $('<input />');
|
||||
}
|
||||
element.append(input);
|
||||
input.attr({
|
||||
id: field.id,
|
||||
placeholder: field.label
|
||||
});
|
||||
if (field.type === 'checkbox') {
|
||||
input[0].type = 'checkbox';
|
||||
element.addClass('annotator-checkbox');
|
||||
element.append($('<label />', {
|
||||
"for": field.id,
|
||||
html: field.label
|
||||
}));
|
||||
}
|
||||
this.element.find('ul:first').append(element);
|
||||
this.fields.push(field);
|
||||
return field.element;
|
||||
};
|
||||
|
||||
Editor.prototype.checkOrientation = function() {
|
||||
var controls, list;
|
||||
|
||||
Editor.__super__.checkOrientation.apply(this, arguments);
|
||||
list = this.element.find('ul');
|
||||
controls = this.element.find('.annotator-controls');
|
||||
if (this.element.hasClass(this.classes.invert.y)) {
|
||||
controls.insertBefore(list);
|
||||
} else if (controls.is(':first-child')) {
|
||||
controls.insertAfter(list);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
Editor.prototype.processKeypress = function(event) {
|
||||
if (event.keyCode === 27) {
|
||||
return this.hide();
|
||||
} else if (event.keyCode === 13 && !event.shiftKey) {
|
||||
return this.submit();
|
||||
}
|
||||
};
|
||||
|
||||
Editor.prototype.onCancelButtonMouseover = function() {
|
||||
return this.element.find('.' + this.classes.focus).removeClass(this.classes.focus);
|
||||
};
|
||||
|
||||
Editor.prototype.setupDraggables = function() {
|
||||
var classes, controls, cornerItem, editor, mousedown, onMousedown, onMousemove, onMouseup, resize, textarea, throttle,
|
||||
_this = this;
|
||||
|
||||
this.element.find('.annotator-resize').remove();
|
||||
if (this.element.hasClass(this.classes.invert.y)) {
|
||||
cornerItem = this.element.find('.annotator-item:last');
|
||||
} else {
|
||||
cornerItem = this.element.find('.annotator-item:first');
|
||||
}
|
||||
if (cornerItem) {
|
||||
$('<span class="annotator-resize"></span>').appendTo(cornerItem);
|
||||
}
|
||||
mousedown = null;
|
||||
classes = this.classes;
|
||||
editor = this.element;
|
||||
textarea = null;
|
||||
resize = editor.find('.annotator-resize');
|
||||
controls = editor.find('.annotator-controls');
|
||||
throttle = false;
|
||||
onMousedown = function(event) {
|
||||
if (event.target === this) {
|
||||
mousedown = {
|
||||
element: this,
|
||||
top: event.pageY,
|
||||
left: event.pageX
|
||||
};
|
||||
textarea = editor.find('textarea:first');
|
||||
$(window).bind({
|
||||
'mouseup.annotator-editor-resize': onMouseup,
|
||||
'mousemove.annotator-editor-resize': onMousemove
|
||||
});
|
||||
return event.preventDefault();
|
||||
}
|
||||
};
|
||||
onMouseup = function() {
|
||||
mousedown = null;
|
||||
return $(window).unbind('.annotator-editor-resize');
|
||||
};
|
||||
onMousemove = function(event) {
|
||||
var diff, directionX, directionY, height, width;
|
||||
|
||||
if (mousedown && throttle === false) {
|
||||
diff = {
|
||||
top: event.pageY - mousedown.top,
|
||||
left: event.pageX - mousedown.left
|
||||
};
|
||||
if (mousedown.element === resize[0]) {
|
||||
height = textarea.outerHeight();
|
||||
width = textarea.outerWidth();
|
||||
directionX = editor.hasClass(classes.invert.x) ? -1 : 1;
|
||||
directionY = editor.hasClass(classes.invert.y) ? 1 : -1;
|
||||
textarea.height(height + (diff.top * directionY));
|
||||
textarea.width(width + (diff.left * directionX));
|
||||
if (textarea.outerHeight() !== height) {
|
||||
mousedown.top = event.pageY;
|
||||
}
|
||||
if (textarea.outerWidth() !== width) {
|
||||
mousedown.left = event.pageX;
|
||||
}
|
||||
} else if (mousedown.element === controls[0]) {
|
||||
editor.css({
|
||||
top: parseInt(editor.css('top'), 10) + diff.top,
|
||||
left: parseInt(editor.css('left'), 10) + diff.left
|
||||
});
|
||||
mousedown.top = event.pageY;
|
||||
mousedown.left = event.pageX;
|
||||
}
|
||||
throttle = true;
|
||||
return setTimeout(function() {
|
||||
return throttle = false;
|
||||
}, 1000 / 60);
|
||||
}
|
||||
};
|
||||
resize.bind('mousedown', onMousedown);
|
||||
return controls.bind('mousedown', onMousedown);
|
||||
};
|
||||
|
||||
return Editor;
|
||||
|
||||
})(Annotator.Widget);
|
||||
|
||||
Annotator.Viewer = (function(_super) {
|
||||
__extends(Viewer, _super);
|
||||
|
||||
Viewer.prototype.events = {
|
||||
".annotator-edit click": "onEditClick",
|
||||
".annotator-delete click": "onDeleteClick"
|
||||
};
|
||||
|
||||
Viewer.prototype.classes = {
|
||||
hide: 'annotator-hide',
|
||||
showControls: 'annotator-visible'
|
||||
};
|
||||
|
||||
Viewer.prototype.html = {
|
||||
element: "<div class=\"annotator-outer annotator-viewer\">\n <ul class=\"annotator-widget annotator-listing\"></ul>\n</div>",
|
||||
item: "<li class=\"annotator-annotation annotator-item\">\n <span class=\"annotator-controls\">\n <a href=\"#\" title=\"View as webpage\" class=\"annotator-link\">View as webpage</a>\n <button title=\"Edit\" class=\"annotator-edit\">Edit</button>\n <button title=\"Delete\" class=\"annotator-delete\">Delete</button>\n </span>\n</li>"
|
||||
};
|
||||
|
||||
Viewer.prototype.options = {
|
||||
readOnly: false
|
||||
};
|
||||
|
||||
function Viewer(options) {
|
||||
this.onDeleteClick = __bind(this.onDeleteClick, this);
|
||||
this.onEditClick = __bind(this.onEditClick, this);
|
||||
this.load = __bind(this.load, this);
|
||||
this.hide = __bind(this.hide, this);
|
||||
this.show = __bind(this.show, this); Viewer.__super__.constructor.call(this, $(this.html.element)[0], options);
|
||||
this.item = $(this.html.item)[0];
|
||||
this.fields = [];
|
||||
this.annotations = [];
|
||||
}
|
||||
|
||||
Viewer.prototype.show = function(event) {
|
||||
var controls,
|
||||
_this = this;
|
||||
|
||||
util.preventEventDefault(event);
|
||||
controls = this.element.find('.annotator-controls').addClass(this.classes.showControls);
|
||||
setTimeout((function() {
|
||||
return controls.removeClass(_this.classes.showControls);
|
||||
}), 500);
|
||||
this.element.removeClass(this.classes.hide);
|
||||
return this.checkOrientation().publish('show');
|
||||
};
|
||||
|
||||
Viewer.prototype.isShown = function() {
|
||||
return !this.element.hasClass(this.classes.hide);
|
||||
};
|
||||
|
||||
Viewer.prototype.hide = function(event) {
|
||||
util.preventEventDefault(event);
|
||||
this.element.addClass(this.classes.hide);
|
||||
return this.publish('hide');
|
||||
};
|
||||
|
||||
Viewer.prototype.load = function(annotations) {
|
||||
var annotation, controller, controls, del, edit, element, field, item, link, links, list, _k, _l, _len2, _len3, _ref2, _ref3;
|
||||
|
||||
this.annotations = annotations || [];
|
||||
list = this.element.find('ul:first').empty();
|
||||
_ref2 = this.annotations;
|
||||
for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) {
|
||||
annotation = _ref2[_k];
|
||||
item = $(this.item).clone().appendTo(list).data('annotation', annotation);
|
||||
controls = item.find('.annotator-controls');
|
||||
link = controls.find('.annotator-link');
|
||||
edit = controls.find('.annotator-edit');
|
||||
del = controls.find('.annotator-delete');
|
||||
links = new LinkParser(annotation.links || []).get('alternate', {
|
||||
'type': 'text/html'
|
||||
});
|
||||
if (links.length === 0 || (links[0].href == null)) {
|
||||
link.remove();
|
||||
} else {
|
||||
link.attr('href', links[0].href);
|
||||
}
|
||||
if (this.options.readOnly) {
|
||||
edit.remove();
|
||||
del.remove();
|
||||
} else {
|
||||
controller = {
|
||||
showEdit: function() {
|
||||
return edit.removeAttr('disabled');
|
||||
},
|
||||
hideEdit: function() {
|
||||
return edit.attr('disabled', 'disabled');
|
||||
},
|
||||
showDelete: function() {
|
||||
return del.removeAttr('disabled');
|
||||
},
|
||||
hideDelete: function() {
|
||||
return del.attr('disabled', 'disabled');
|
||||
}
|
||||
};
|
||||
}
|
||||
_ref3 = this.fields;
|
||||
for (_l = 0, _len3 = _ref3.length; _l < _len3; _l++) {
|
||||
field = _ref3[_l];
|
||||
element = $(field.element).clone().appendTo(item)[0];
|
||||
field.load(element, annotation, controller);
|
||||
}
|
||||
}
|
||||
this.publish('load', [this.annotations]);
|
||||
return this.show();
|
||||
};
|
||||
|
||||
Viewer.prototype.addField = function(options) {
|
||||
var field;
|
||||
|
||||
field = $.extend({
|
||||
load: function() {}
|
||||
}, options);
|
||||
field.element = $('<div />')[0];
|
||||
this.fields.push(field);
|
||||
field.element;
|
||||
return this;
|
||||
};
|
||||
|
||||
Viewer.prototype.onEditClick = function(event) {
|
||||
return this.onButtonClick(event, 'edit');
|
||||
};
|
||||
|
||||
Viewer.prototype.onDeleteClick = function(event) {
|
||||
return this.onButtonClick(event, 'delete');
|
||||
};
|
||||
|
||||
Viewer.prototype.onButtonClick = function(event, type) {
|
||||
var item;
|
||||
|
||||
item = $(event.target).parents('.annotator-annotation');
|
||||
return this.publish(type, [item.data('annotation')]);
|
||||
};
|
||||
|
||||
return Viewer;
|
||||
|
||||
})(Annotator.Widget);
|
||||
|
||||
LinkParser = (function() {
|
||||
function LinkParser(data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
LinkParser.prototype.get = function(rel, cond) {
|
||||
var d, k, keys, match, v, _k, _len2, _ref2, _results;
|
||||
|
||||
if (cond == null) {
|
||||
cond = {};
|
||||
}
|
||||
cond = $.extend({}, cond, {
|
||||
rel: rel
|
||||
});
|
||||
keys = (function() {
|
||||
var _results;
|
||||
|
||||
_results = [];
|
||||
for (k in cond) {
|
||||
if (!__hasProp.call(cond, k)) continue;
|
||||
v = cond[k];
|
||||
_results.push(k);
|
||||
}
|
||||
return _results;
|
||||
})();
|
||||
_ref2 = this.data;
|
||||
_results = [];
|
||||
for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) {
|
||||
d = _ref2[_k];
|
||||
match = keys.reduce((function(m, k) {
|
||||
return m && (d[k] === cond[k]);
|
||||
}), true);
|
||||
if (match) {
|
||||
_results.push(d);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return _results;
|
||||
};
|
||||
|
||||
return LinkParser;
|
||||
|
||||
})();
|
||||
|
||||
Annotator = Annotator || {};
|
||||
|
||||
Annotator.Notification = (function(_super) {
|
||||
__extends(Notification, _super);
|
||||
|
||||
Notification.prototype.events = {
|
||||
"click": "hide"
|
||||
};
|
||||
|
||||
Notification.prototype.options = {
|
||||
html: "<div class='annotator-notice'></div>",
|
||||
classes: {
|
||||
show: "annotator-notice-show",
|
||||
info: "annotator-notice-info",
|
||||
success: "annotator-notice-success",
|
||||
error: "annotator-notice-error"
|
||||
}
|
||||
};
|
||||
|
||||
function Notification(options) {
|
||||
this.hide = __bind(this.hide, this);
|
||||
this.show = __bind(this.show, this); Notification.__super__.constructor.call(this, $(this.options.html).appendTo(document.body)[0], options);
|
||||
}
|
||||
|
||||
Notification.prototype.show = function(message, status) {
|
||||
if (status == null) {
|
||||
status = Annotator.Notification.INFO;
|
||||
}
|
||||
$(this.element).addClass(this.options.classes.show).addClass(this.options.classes[status]).escape(message || "");
|
||||
setTimeout(this.hide, 5000);
|
||||
return this;
|
||||
};
|
||||
|
||||
Notification.prototype.hide = function() {
|
||||
$(this.element).removeClass(this.options.classes.show);
|
||||
return this;
|
||||
};
|
||||
|
||||
return Notification;
|
||||
|
||||
})(Delegator);
|
||||
|
||||
Annotator.Notification.INFO = 'show';
|
||||
|
||||
Annotator.Notification.SUCCESS = 'success';
|
||||
|
||||
Annotator.Notification.ERROR = 'error';
|
||||
|
||||
$(function() {
|
||||
var notification;
|
||||
|
||||
notification = new Annotator.Notification;
|
||||
Annotator.showNotification = notification.show;
|
||||
return Annotator.hideNotification = notification.hide;
|
||||
});
|
||||
|
||||
}).call(this);
|
||||
2
common/static/js/vendor/annotator.min.js
vendored
Normal file
2
common/static/js/vendor/annotator.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
273
common/static/js/vendor/annotator.store.js
vendored
Normal file
273
common/static/js/vendor/annotator.store.js
vendored
Normal file
@@ -0,0 +1,273 @@
|
||||
/*
|
||||
** Annotator 1.2.6-dev-dc18206
|
||||
** https://github.com/okfn/annotator/
|
||||
**
|
||||
** Copyright 2012 Aron Carroll, Rufus Pollock, and Nick Stenning.
|
||||
** Dual licensed under the MIT and GPLv3 licenses.
|
||||
** https://github.com/okfn/annotator/blob/master/LICENSE
|
||||
**
|
||||
** Built at: 2013-05-16 18:02:02Z
|
||||
*/
|
||||
|
||||
|
||||
(function() {
|
||||
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
|
||||
__hasProp = {}.hasOwnProperty,
|
||||
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
|
||||
__indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
|
||||
|
||||
Annotator.Plugin.Store = (function(_super) {
|
||||
__extends(Store, _super);
|
||||
|
||||
Store.prototype.events = {
|
||||
'annotationCreated': 'annotationCreated',
|
||||
'annotationDeleted': 'annotationDeleted',
|
||||
'annotationUpdated': 'annotationUpdated'
|
||||
};
|
||||
|
||||
Store.prototype.options = {
|
||||
annotationData: {},
|
||||
emulateHTTP: false,
|
||||
loadFromSearch: false,
|
||||
prefix: '/store',
|
||||
urls: {
|
||||
create: '/annotations',
|
||||
read: '/annotations/:id',
|
||||
update: '/annotations/:id',
|
||||
destroy: '/annotations/:id',
|
||||
search: '/search'
|
||||
}
|
||||
};
|
||||
|
||||
function Store(element, options) {
|
||||
this._onError = __bind(this._onError, this);
|
||||
this._onLoadAnnotationsFromSearch = __bind(this._onLoadAnnotationsFromSearch, this);
|
||||
this._onLoadAnnotations = __bind(this._onLoadAnnotations, this);
|
||||
this._getAnnotations = __bind(this._getAnnotations, this); Store.__super__.constructor.apply(this, arguments);
|
||||
this.annotations = [];
|
||||
}
|
||||
|
||||
Store.prototype.pluginInit = function() {
|
||||
if (!Annotator.supported()) {
|
||||
return;
|
||||
}
|
||||
if (this.annotator.plugins.Auth) {
|
||||
return this.annotator.plugins.Auth.withToken(this._getAnnotations);
|
||||
} else {
|
||||
return this._getAnnotations();
|
||||
}
|
||||
};
|
||||
|
||||
Store.prototype._getAnnotations = function() {
|
||||
if (this.options.loadFromSearch) {
|
||||
return this.loadAnnotationsFromSearch(this.options.loadFromSearch);
|
||||
} else {
|
||||
return this.loadAnnotations();
|
||||
}
|
||||
};
|
||||
|
||||
Store.prototype.annotationCreated = function(annotation) {
|
||||
var _this = this;
|
||||
|
||||
if (__indexOf.call(this.annotations, annotation) < 0) {
|
||||
this.registerAnnotation(annotation);
|
||||
return this._apiRequest('create', annotation, function(data) {
|
||||
if (data.id == null) {
|
||||
console.warn(Annotator._t("Warning: No ID returned from server for annotation "), annotation);
|
||||
}
|
||||
return _this.updateAnnotation(annotation, data);
|
||||
});
|
||||
} else {
|
||||
return this.updateAnnotation(annotation, {});
|
||||
}
|
||||
};
|
||||
|
||||
Store.prototype.annotationUpdated = function(annotation) {
|
||||
var _this = this;
|
||||
|
||||
if (__indexOf.call(this.annotations, annotation) >= 0) {
|
||||
return this._apiRequest('update', annotation, (function(data) {
|
||||
return _this.updateAnnotation(annotation, data);
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
Store.prototype.annotationDeleted = function(annotation) {
|
||||
var _this = this;
|
||||
|
||||
if (__indexOf.call(this.annotations, annotation) >= 0) {
|
||||
return this._apiRequest('destroy', annotation, (function() {
|
||||
return _this.unregisterAnnotation(annotation);
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
Store.prototype.registerAnnotation = function(annotation) {
|
||||
return this.annotations.push(annotation);
|
||||
};
|
||||
|
||||
Store.prototype.unregisterAnnotation = function(annotation) {
|
||||
return this.annotations.splice(this.annotations.indexOf(annotation), 1);
|
||||
};
|
||||
|
||||
Store.prototype.updateAnnotation = function(annotation, data) {
|
||||
if (__indexOf.call(this.annotations, annotation) < 0) {
|
||||
console.error(Annotator._t("Trying to update unregistered annotation!"));
|
||||
} else {
|
||||
$.extend(annotation, data);
|
||||
}
|
||||
return $(annotation.highlights).data('annotation', annotation);
|
||||
};
|
||||
|
||||
Store.prototype.loadAnnotations = function() {
|
||||
return this._apiRequest('read', null, this._onLoadAnnotations);
|
||||
};
|
||||
|
||||
Store.prototype._onLoadAnnotations = function(data) {
|
||||
if (data == null) {
|
||||
data = [];
|
||||
}
|
||||
this.annotations = this.annotations.concat(data);
|
||||
return this.annotator.loadAnnotations(data.slice());
|
||||
};
|
||||
|
||||
Store.prototype.loadAnnotationsFromSearch = function(searchOptions) {
|
||||
return this._apiRequest('search', searchOptions, this._onLoadAnnotationsFromSearch);
|
||||
};
|
||||
|
||||
Store.prototype._onLoadAnnotationsFromSearch = function(data) {
|
||||
if (data == null) {
|
||||
data = {};
|
||||
}
|
||||
return this._onLoadAnnotations(data.rows || []);
|
||||
};
|
||||
|
||||
Store.prototype.dumpAnnotations = function() {
|
||||
var ann, _i, _len, _ref, _results;
|
||||
|
||||
_ref = this.annotations;
|
||||
_results = [];
|
||||
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
||||
ann = _ref[_i];
|
||||
_results.push(JSON.parse(this._dataFor(ann)));
|
||||
}
|
||||
return _results;
|
||||
};
|
||||
|
||||
Store.prototype._apiRequest = function(action, obj, onSuccess) {
|
||||
var id, options, request, url;
|
||||
|
||||
id = obj && obj.id;
|
||||
url = this._urlFor(action, id);
|
||||
options = this._apiRequestOptions(action, obj, onSuccess);
|
||||
request = $.ajax(url, options);
|
||||
request._id = id;
|
||||
request._action = action;
|
||||
return request;
|
||||
};
|
||||
|
||||
Store.prototype._apiRequestOptions = function(action, obj, onSuccess) {
|
||||
var data, method, opts;
|
||||
|
||||
method = this._methodFor(action);
|
||||
opts = {
|
||||
type: method,
|
||||
headers: this.element.data('annotator:headers'),
|
||||
dataType: "json",
|
||||
success: onSuccess || function() {},
|
||||
error: this._onError
|
||||
};
|
||||
if (this.options.emulateHTTP && (method === 'PUT' || method === 'DELETE')) {
|
||||
opts.headers = $.extend(opts.headers, {
|
||||
'X-HTTP-Method-Override': method
|
||||
});
|
||||
opts.type = 'POST';
|
||||
}
|
||||
if (action === "search") {
|
||||
opts = $.extend(opts, {
|
||||
data: obj
|
||||
});
|
||||
return opts;
|
||||
}
|
||||
data = obj && this._dataFor(obj);
|
||||
if (this.options.emulateJSON) {
|
||||
opts.data = {
|
||||
json: data
|
||||
};
|
||||
if (this.options.emulateHTTP) {
|
||||
opts.data._method = method;
|
||||
}
|
||||
return opts;
|
||||
}
|
||||
opts = $.extend(opts, {
|
||||
data: data,
|
||||
contentType: "application/json; charset=utf-8"
|
||||
});
|
||||
return opts;
|
||||
};
|
||||
|
||||
Store.prototype._urlFor = function(action, id) {
|
||||
var url;
|
||||
|
||||
url = this.options.prefix != null ? this.options.prefix : '';
|
||||
url += this.options.urls[action];
|
||||
url = url.replace(/\/:id/, id != null ? '/' + id : '');
|
||||
url = url.replace(/:id/, id != null ? id : '');
|
||||
return url;
|
||||
};
|
||||
|
||||
Store.prototype._methodFor = function(action) {
|
||||
var table;
|
||||
|
||||
table = {
|
||||
'create': 'POST',
|
||||
'read': 'GET',
|
||||
'update': 'PUT',
|
||||
'destroy': 'DELETE',
|
||||
'search': 'GET'
|
||||
};
|
||||
return table[action];
|
||||
};
|
||||
|
||||
Store.prototype._dataFor = function(annotation) {
|
||||
var data, highlights;
|
||||
|
||||
highlights = annotation.highlights;
|
||||
delete annotation.highlights;
|
||||
$.extend(annotation, this.options.annotationData);
|
||||
data = JSON.stringify(annotation);
|
||||
if (highlights) {
|
||||
annotation.highlights = highlights;
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
Store.prototype._onError = function(xhr) {
|
||||
var action, message;
|
||||
|
||||
action = xhr._action;
|
||||
message = Annotator._t("Sorry we could not ") + action + Annotator._t(" this annotation");
|
||||
if (xhr._action === 'search') {
|
||||
message = Annotator._t("Sorry we could not search the store for annotations");
|
||||
} else if (xhr._action === 'read' && !xhr._id) {
|
||||
message = Annotator._t("Sorry we could not ") + action + Annotator._t(" the annotations from the store");
|
||||
}
|
||||
switch (xhr.status) {
|
||||
case 401:
|
||||
message = Annotator._t("Sorry you are not allowed to ") + action + Annotator._t(" this annotation");
|
||||
break;
|
||||
case 404:
|
||||
message = Annotator._t("Sorry we could not connect to the annotations store");
|
||||
break;
|
||||
case 500:
|
||||
message = Annotator._t("Sorry something went wrong with the annotation store");
|
||||
}
|
||||
Annotator.showNotification(message, Annotator.Notification.ERROR);
|
||||
return console.error(Annotator._t("API request failed:") + (" '" + xhr.status + "'"));
|
||||
};
|
||||
|
||||
return Store;
|
||||
|
||||
})(Annotator.Plugin);
|
||||
|
||||
}).call(this);
|
||||
1
common/static/js/vendor/annotator.store.min.js
vendored
Normal file
1
common/static/js/vendor/annotator.store.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
136
common/static/js/vendor/annotator.tags.js
vendored
Normal file
136
common/static/js/vendor/annotator.tags.js
vendored
Normal file
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
** Annotator 1.2.6-dev-dc18206
|
||||
** https://github.com/okfn/annotator/
|
||||
**
|
||||
** Copyright 2012 Aron Carroll, Rufus Pollock, and Nick Stenning.
|
||||
** Dual licensed under the MIT and GPLv3 licenses.
|
||||
** https://github.com/okfn/annotator/blob/master/LICENSE
|
||||
**
|
||||
** Built at: 2013-05-16 18:02:02Z
|
||||
*/
|
||||
|
||||
|
||||
(function() {
|
||||
var _ref,
|
||||
__bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
|
||||
__hasProp = {}.hasOwnProperty,
|
||||
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
|
||||
|
||||
Annotator.Plugin.Tags = (function(_super) {
|
||||
__extends(Tags, _super);
|
||||
|
||||
function Tags() {
|
||||
this.setAnnotationTags = __bind(this.setAnnotationTags, this);
|
||||
this.updateField = __bind(this.updateField, this); _ref = Tags.__super__.constructor.apply(this, arguments);
|
||||
return _ref;
|
||||
}
|
||||
|
||||
Tags.prototype.options = {
|
||||
parseTags: function(string) {
|
||||
var tags;
|
||||
|
||||
string = $.trim(string);
|
||||
tags = [];
|
||||
if (string) {
|
||||
tags = string.split(/\s+/);
|
||||
}
|
||||
return tags;
|
||||
},
|
||||
stringifyTags: function(array) {
|
||||
return array.join(" ");
|
||||
}
|
||||
};
|
||||
|
||||
Tags.prototype.field = null;
|
||||
|
||||
Tags.prototype.input = null;
|
||||
|
||||
Tags.prototype.pluginInit = function() {
|
||||
if (!Annotator.supported()) {
|
||||
return;
|
||||
}
|
||||
this.field = this.annotator.editor.addField({
|
||||
label: Annotator._t('Add some tags here') + '\u2026',
|
||||
load: this.updateField,
|
||||
submit: this.setAnnotationTags
|
||||
});
|
||||
this.annotator.viewer.addField({
|
||||
load: this.updateViewer
|
||||
});
|
||||
if (this.annotator.plugins.Filter) {
|
||||
this.annotator.plugins.Filter.addFilter({
|
||||
label: Annotator._t('Tag'),
|
||||
property: 'tags',
|
||||
isFiltered: Annotator.Plugin.Tags.filterCallback
|
||||
});
|
||||
}
|
||||
return this.input = $(this.field).find(':input');
|
||||
};
|
||||
|
||||
Tags.prototype.parseTags = function(string) {
|
||||
return this.options.parseTags(string);
|
||||
};
|
||||
|
||||
Tags.prototype.stringifyTags = function(array) {
|
||||
return this.options.stringifyTags(array);
|
||||
};
|
||||
|
||||
Tags.prototype.updateField = function(field, annotation) {
|
||||
var value;
|
||||
|
||||
value = '';
|
||||
if (annotation.tags) {
|
||||
value = this.stringifyTags(annotation.tags);
|
||||
}
|
||||
return this.input.val(value);
|
||||
};
|
||||
|
||||
Tags.prototype.setAnnotationTags = function(field, annotation) {
|
||||
return annotation.tags = this.parseTags(this.input.val());
|
||||
};
|
||||
|
||||
Tags.prototype.updateViewer = function(field, annotation) {
|
||||
field = $(field);
|
||||
if (annotation.tags && $.isArray(annotation.tags) && annotation.tags.length) {
|
||||
return field.addClass('annotator-tags').html(function() {
|
||||
var string;
|
||||
|
||||
return string = $.map(annotation.tags, function(tag) {
|
||||
return '<span class="annotator-tag">' + Annotator.$.escape(tag) + '</span>';
|
||||
}).join(' ');
|
||||
});
|
||||
} else {
|
||||
return field.remove();
|
||||
}
|
||||
};
|
||||
|
||||
return Tags;
|
||||
|
||||
})(Annotator.Plugin);
|
||||
|
||||
Annotator.Plugin.Tags.filterCallback = function(input, tags) {
|
||||
var keyword, keywords, matches, tag, _i, _j, _len, _len1;
|
||||
|
||||
if (tags == null) {
|
||||
tags = [];
|
||||
}
|
||||
matches = 0;
|
||||
keywords = [];
|
||||
if (input) {
|
||||
keywords = input.split(/\s+/g);
|
||||
for (_i = 0, _len = keywords.length; _i < _len; _i++) {
|
||||
keyword = keywords[_i];
|
||||
if (tags.length) {
|
||||
for (_j = 0, _len1 = tags.length; _j < _len1; _j++) {
|
||||
tag = tags[_j];
|
||||
if (tag.indexOf(keyword) !== -1) {
|
||||
matches += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return matches === keywords.length;
|
||||
};
|
||||
|
||||
}).call(this);
|
||||
1
common/static/js/vendor/annotator.tags.min.js
vendored
Normal file
1
common/static/js/vendor/annotator.tags.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
(function(){var _ref,__bind=function(fn,me){return function(){return fn.apply(me,arguments)}},__hasProp={}.hasOwnProperty,__extends=function(child,parent){for(var key in parent){if(__hasProp.call(parent,key))child[key]=parent[key]}function ctor(){this.constructor=child}ctor.prototype=parent.prototype;child.prototype=new ctor;child.__super__=parent.prototype;return child};Annotator.Plugin.Tags=function(_super){__extends(Tags,_super);function Tags(){this.setAnnotationTags=__bind(this.setAnnotationTags,this);this.updateField=__bind(this.updateField,this);_ref=Tags.__super__.constructor.apply(this,arguments);return _ref}Tags.prototype.options={parseTags:function(string){var tags;string=$.trim(string);tags=[];if(string){tags=string.split(/\s+/)}return tags},stringifyTags:function(array){return array.join(" ")}};Tags.prototype.field=null;Tags.prototype.input=null;Tags.prototype.pluginInit=function(){if(!Annotator.supported()){return}this.field=this.annotator.editor.addField({label:Annotator._t("Add some tags here")+"…",load:this.updateField,submit:this.setAnnotationTags});this.annotator.viewer.addField({load:this.updateViewer});if(this.annotator.plugins.Filter){this.annotator.plugins.Filter.addFilter({label:Annotator._t("Tag"),property:"tags",isFiltered:Annotator.Plugin.Tags.filterCallback})}return this.input=$(this.field).find(":input")};Tags.prototype.parseTags=function(string){return this.options.parseTags(string)};Tags.prototype.stringifyTags=function(array){return this.options.stringifyTags(array)};Tags.prototype.updateField=function(field,annotation){var value;value="";if(annotation.tags){value=this.stringifyTags(annotation.tags)}return this.input.val(value)};Tags.prototype.setAnnotationTags=function(field,annotation){return annotation.tags=this.parseTags(this.input.val())};Tags.prototype.updateViewer=function(field,annotation){field=$(field);if(annotation.tags&&$.isArray(annotation.tags)&&annotation.tags.length){return field.addClass("annotator-tags").html(function(){var string;return string=$.map(annotation.tags,function(tag){return'<span class="annotator-tag">'+Annotator.$.escape(tag)+"</span>"}).join(" ")})}else{return field.remove()}};return Tags}(Annotator.Plugin);Annotator.Plugin.Tags.filterCallback=function(input,tags){var keyword,keywords,matches,tag,_i,_j,_len,_len1;if(tags==null){tags=[]}matches=0;keywords=[];if(input){keywords=input.split(/\s+/g);for(_i=0,_len=keywords.length;_i<_len;_i++){keyword=keywords[_i];if(tags.length){for(_j=0,_len1=tags.length;_j<_len1;_j++){tag=tags[_j];if(tag.indexOf(keyword)!==-1){matches+=1}}}}}return matches===keywords.length}}).call(this);
|
||||
@@ -185,6 +185,11 @@ def _combined_open_ended_grading(tab, user, course, active_page):
|
||||
return tab
|
||||
return []
|
||||
|
||||
def _notes_tab(tab, user, course, active_page):
|
||||
if user.is_authenticated() and settings.MITX_FEATURES.get('ENABLE_STUDENT_NOTES'):
|
||||
link = reverse('notes', args=[course.id])
|
||||
return [CourseTab(tab['name'], link, active_page == 'notes')]
|
||||
return []
|
||||
|
||||
#### Validators
|
||||
|
||||
@@ -227,6 +232,7 @@ VALID_TAB_TYPES = {
|
||||
'peer_grading': TabImpl(null_validator, _peer_grading),
|
||||
'staff_grading': TabImpl(null_validator, _staff_grading),
|
||||
'open_ended': TabImpl(null_validator, _combined_open_ended_grading),
|
||||
'notes': TabImpl(null_validator, _notes_tab)
|
||||
}
|
||||
|
||||
|
||||
|
||||
57
lms/djangoapps/notes/README.md
Normal file
57
lms/djangoapps/notes/README.md
Normal file
@@ -0,0 +1,57 @@
|
||||
Notes Django App
|
||||
================
|
||||
|
||||
This is a django application that stores and displays notes that students make while reading static HTML book(s) in their courseware. Note taking functionality in the static HTML book(s) is handled by a wrapper script around [annotator.js](http://okfnlabs.org/annotator/), which interfaces with the API provided by this application to store and retrieve notes.
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
To use this application, course staff must opt-in by doing the following:
|
||||
|
||||
* Login to [Studio](http://studio.edx.org/).
|
||||
* Go to *Course Settings* -> *Advanced Settings*
|
||||
* Find the ```advanced_modules``` policy key and in the policy value field, add ```"notes"``` to the list.
|
||||
* Save the course settings.
|
||||
|
||||
The result of following these steps is that you should see a new tab appear in the courseware named *My Notes*. This will display a journal of notes that the student has created in the static HTML book(s). Second, when you highlight text in the static HTML book(s), a dialog will appear. You can enter some notes and tags and save it. The note will appear highlighted in the text and will also be saved to the journal.
|
||||
|
||||
To disable the *My Notes* tab and notes in the static HTML book(s), simply reverse the above steps (i.e. remove ```"notes"``` from the ```advanced_modules``` policy setting).
|
||||
|
||||
### Caveats and Limitations
|
||||
|
||||
* Notes are private to each student.
|
||||
* Sharing and replying to notes is not supported.
|
||||
* The student *My Notes* interface is very limited.
|
||||
* There is no instructor interface to view student notes.
|
||||
|
||||
Developer Overview
|
||||
------------------
|
||||
|
||||
### Quickstart
|
||||
|
||||
```
|
||||
$ rake django-admin[syncdb]
|
||||
$ rake django-admin[migrate]
|
||||
```
|
||||
|
||||
Then follow the steps above to enable the *My Notes* tab or manually add a tab to the policy tab configuration with ```{"type": "notes", "name": "My Notes"}```.
|
||||
|
||||
### App Directory Structure:
|
||||
|
||||
lms/djangoapps/notes:
|
||||
|
||||
* api.py - API used by annotator.js on the frontend
|
||||
* models.py - Contains note model for storing notes
|
||||
* tests.py - Unit tests
|
||||
* views.py - View to display the journal of notes (i.e. *My Notes* tab)
|
||||
* urls.py - Maps the API and View routes.
|
||||
* utils.py - Contains method for checking if the course has this app enabled. Intended to be public to other modules.
|
||||
|
||||
Also requires:
|
||||
|
||||
* lms/static/coffee/src/notes.coffee -- wrapper around annotator.js
|
||||
* lms/templates/notes.html -- used by views.py to display the notes
|
||||
|
||||
Interacts with:
|
||||
|
||||
* lms/djangoapps/staticbook - the html static book checks to see if notes is enabled and has some logic to enable/disable accordingly
|
||||
0
lms/djangoapps/notes/__init__.py
Normal file
0
lms/djangoapps/notes/__init__.py
Normal file
251
lms/djangoapps/notes/api.py
Normal file
251
lms/djangoapps/notes/api.py
Normal file
@@ -0,0 +1,251 @@
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import HttpResponse, Http404
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from notes.models import Note
|
||||
from notes.utils import notes_enabled_for_course
|
||||
from courseware.courses import get_course_with_access
|
||||
|
||||
import json
|
||||
import logging
|
||||
import collections
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
API_SETTINGS = {
|
||||
'META': {'name': 'Notes API', 'version': 1},
|
||||
|
||||
# Maps resources to HTTP methods and actions
|
||||
'RESOURCE_MAP': {
|
||||
'root': {'GET': 'root'},
|
||||
'notes': {'GET': 'index', 'POST': 'create'},
|
||||
'note': {'GET': 'read', 'PUT': 'update', 'DELETE': 'delete'},
|
||||
'search': {'GET': 'search'},
|
||||
},
|
||||
|
||||
# Cap the number of notes that can be returned in one request
|
||||
'MAX_NOTE_LIMIT': 1000,
|
||||
}
|
||||
|
||||
# Wrapper class for HTTP response and data. All API actions are expected to return this.
|
||||
ApiResponse = collections.namedtuple('ApiResponse', ['http_response', 'data'])
|
||||
|
||||
#----------------------------------------------------------------------#
|
||||
# API requests are routed through api_request() using the resource map.
|
||||
|
||||
|
||||
def api_enabled(request, course_id):
|
||||
'''
|
||||
Returns True if the api is enabled for the course, otherwise False.
|
||||
'''
|
||||
course = _get_course(request, course_id)
|
||||
return notes_enabled_for_course(course)
|
||||
|
||||
|
||||
@login_required
|
||||
def api_request(request, course_id, **kwargs):
|
||||
'''
|
||||
Routes API requests to the appropriate action method and returns JSON.
|
||||
Raises a 404 if the requested resource does not exist or notes are
|
||||
disabled for the course.
|
||||
'''
|
||||
|
||||
# Verify that the api should be accessible to this course
|
||||
if not api_enabled(request, course_id):
|
||||
log.debug('Notes are disabled for course: {0}'.format(course_id))
|
||||
raise Http404
|
||||
|
||||
# Locate the requested resource
|
||||
resource_map = API_SETTINGS.get('RESOURCE_MAP', {})
|
||||
resource_name = kwargs.pop('resource')
|
||||
resource_method = request.method
|
||||
resource = resource_map.get(resource_name)
|
||||
|
||||
if resource is None:
|
||||
log.debug('Resource "{0}" does not exist'.format(resource_name))
|
||||
raise Http404
|
||||
|
||||
if resource_method not in resource.keys():
|
||||
log.debug('Resource "{0}" does not support method "{1}"'.format(resource_name, resource_method))
|
||||
raise Http404
|
||||
|
||||
# Execute the action associated with the resource
|
||||
func = resource.get(resource_method)
|
||||
module = globals()
|
||||
if func not in module:
|
||||
log.debug('Function "{0}" does not exist for request {1} {2}'.format(func, resource_method, resource_name))
|
||||
raise Http404
|
||||
|
||||
log.debug('API request: {0} {1}'.format(resource_method, resource_name))
|
||||
|
||||
api_response = module[func](request, course_id, **kwargs)
|
||||
http_response = api_format(api_response)
|
||||
|
||||
return http_response
|
||||
|
||||
|
||||
def api_format(api_response):
|
||||
'''
|
||||
Takes an ApiResponse and returns an HttpResponse.
|
||||
'''
|
||||
http_response = api_response.http_response
|
||||
content_type = 'application/json'
|
||||
content = ''
|
||||
|
||||
# not doing a strict boolean check on data becuase it could be an empty list
|
||||
if api_response.data is not None and api_response.data != '':
|
||||
content = json.dumps(api_response.data)
|
||||
|
||||
http_response['Content-type'] = content_type
|
||||
http_response.content = content
|
||||
|
||||
log.debug('API response type: {0} content: {1}'.format(content_type, content))
|
||||
|
||||
return http_response
|
||||
|
||||
|
||||
def _get_course(request, course_id):
|
||||
'''
|
||||
Helper function to load and return a user's course.
|
||||
'''
|
||||
return get_course_with_access(request.user, course_id, 'load')
|
||||
|
||||
#----------------------------------------------------------------------#
|
||||
# API actions exposed via the resource map.
|
||||
|
||||
|
||||
def index(request, course_id):
|
||||
'''
|
||||
Returns a list of annotation objects.
|
||||
'''
|
||||
MAX_LIMIT = API_SETTINGS.get('MAX_NOTE_LIMIT')
|
||||
|
||||
notes = Note.objects.order_by('id').filter(course_id=course_id,
|
||||
user=request.user)[:MAX_LIMIT]
|
||||
|
||||
return ApiResponse(http_response=HttpResponse(), data=[note.as_dict() for note in notes])
|
||||
|
||||
|
||||
def create(request, course_id):
|
||||
'''
|
||||
Receives an annotation object to create and returns a 303 with the read location.
|
||||
'''
|
||||
note = Note(course_id=course_id, user=request.user)
|
||||
|
||||
try:
|
||||
note.clean(request.body)
|
||||
except ValidationError as e:
|
||||
log.debug(e)
|
||||
return ApiResponse(http_response=HttpResponse('', status=400), data=None)
|
||||
|
||||
note.save()
|
||||
response = HttpResponse('', status=303)
|
||||
response['Location'] = note.get_absolute_url()
|
||||
|
||||
return ApiResponse(http_response=response, data=None)
|
||||
|
||||
|
||||
def read(request, course_id, note_id):
|
||||
'''
|
||||
Returns a single annotation object.
|
||||
'''
|
||||
try:
|
||||
note = Note.objects.get(id=note_id)
|
||||
except Note.DoesNotExist:
|
||||
return ApiResponse(http_response=HttpResponse('', status=404), data=None)
|
||||
|
||||
if note.user.id != request.user.id:
|
||||
return ApiResponse(http_response=HttpResponse('', status=403), data=None)
|
||||
|
||||
return ApiResponse(http_response=HttpResponse(), data=note.as_dict())
|
||||
|
||||
|
||||
def update(request, course_id, note_id):
|
||||
'''
|
||||
Updates an annotation object and returns a 303 with the read location.
|
||||
'''
|
||||
try:
|
||||
note = Note.objects.get(id=note_id)
|
||||
except Note.DoesNotExist:
|
||||
return ApiResponse(http_response=HttpResponse('', status=404), data=None)
|
||||
|
||||
if note.user.id != request.user.id:
|
||||
return ApiResponse(http_response=HttpResponse('', status=403), data=None)
|
||||
|
||||
try:
|
||||
note.clean(request.body)
|
||||
except ValidationError as e:
|
||||
log.debug(e)
|
||||
return ApiResponse(http_response=HttpResponse('', status=400), data=None)
|
||||
|
||||
note.save()
|
||||
|
||||
response = HttpResponse('', status=303)
|
||||
response['Location'] = note.get_absolute_url()
|
||||
|
||||
return ApiResponse(http_response=response, data=None)
|
||||
|
||||
|
||||
def delete(request, course_id, note_id):
|
||||
'''
|
||||
Deletes the annotation object and returns a 204 with no content.
|
||||
'''
|
||||
try:
|
||||
note = Note.objects.get(id=note_id)
|
||||
except Note.DoesNotExist:
|
||||
return ApiResponse(http_response=HttpResponse('', status=404), data=None)
|
||||
|
||||
if note.user.id != request.user.id:
|
||||
return ApiResponse(http_response=HttpResponse('', status=403), data=None)
|
||||
|
||||
note.delete()
|
||||
|
||||
return ApiResponse(http_response=HttpResponse('', status=204), data=None)
|
||||
|
||||
|
||||
def search(request, course_id):
|
||||
'''
|
||||
Returns a subset of annotation objects based on a search query.
|
||||
'''
|
||||
MAX_LIMIT = API_SETTINGS.get('MAX_NOTE_LIMIT')
|
||||
|
||||
# search parameters
|
||||
offset = request.GET.get('offset', '')
|
||||
limit = request.GET.get('limit', '')
|
||||
uri = request.GET.get('uri', '')
|
||||
|
||||
# validate search parameters
|
||||
if offset.isdigit():
|
||||
offset = int(offset)
|
||||
else:
|
||||
offset = 0
|
||||
|
||||
if limit.isdigit():
|
||||
limit = int(limit)
|
||||
if limit == 0 or limit > MAX_LIMIT:
|
||||
limit = MAX_LIMIT
|
||||
else:
|
||||
limit = MAX_LIMIT
|
||||
|
||||
# set filters
|
||||
filters = {'course_id': course_id, 'user': request.user}
|
||||
if uri != '':
|
||||
filters['uri'] = uri
|
||||
|
||||
# retrieve notes
|
||||
notes = Note.objects.order_by('id').filter(**filters)
|
||||
total = notes.count()
|
||||
rows = notes[offset:offset + limit]
|
||||
result = {
|
||||
'total': total,
|
||||
'rows': [note.as_dict() for note in rows]
|
||||
}
|
||||
|
||||
return ApiResponse(http_response=HttpResponse(), data=result)
|
||||
|
||||
|
||||
def root(request, course_id):
|
||||
'''
|
||||
Returns version information about the API.
|
||||
'''
|
||||
return ApiResponse(http_response=HttpResponse(), data=API_SETTINGS.get('META'))
|
||||
90
lms/djangoapps/notes/migrations/0001_initial.py
Normal file
90
lms/djangoapps/notes/migrations/0001_initial.py
Normal file
@@ -0,0 +1,90 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Adding model 'Note'
|
||||
db.create_table('notes_note', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
|
||||
('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
|
||||
('uri', self.gf('django.db.models.fields.CharField')(max_length=1024, db_index=True)),
|
||||
('text', self.gf('django.db.models.fields.TextField')(default='')),
|
||||
('quote', self.gf('django.db.models.fields.TextField')(default='')),
|
||||
('range_start', self.gf('django.db.models.fields.CharField')(max_length=2048)),
|
||||
('range_start_offset', self.gf('django.db.models.fields.IntegerField')()),
|
||||
('range_end', self.gf('django.db.models.fields.CharField')(max_length=2048)),
|
||||
('range_end_offset', self.gf('django.db.models.fields.IntegerField')()),
|
||||
('tags', self.gf('django.db.models.fields.TextField')(default='')),
|
||||
('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, null=True, db_index=True, blank=True)),
|
||||
('updated', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, db_index=True, blank=True)),
|
||||
))
|
||||
db.send_create_signal('notes', ['Note'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting model 'Note'
|
||||
db.delete_table('notes_note')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'notes.note': {
|
||||
'Meta': {'object_name': 'Note'},
|
||||
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'quote': ('django.db.models.fields.TextField', [], {'default': "''"}),
|
||||
'range_end': ('django.db.models.fields.CharField', [], {'max_length': '2048'}),
|
||||
'range_end_offset': ('django.db.models.fields.IntegerField', [], {}),
|
||||
'range_start': ('django.db.models.fields.CharField', [], {'max_length': '2048'}),
|
||||
'range_start_offset': ('django.db.models.fields.IntegerField', [], {}),
|
||||
'tags': ('django.db.models.fields.TextField', [], {'default': "''"}),
|
||||
'text': ('django.db.models.fields.TextField', [], {'default': "''"}),
|
||||
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'uri': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'db_index': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['notes']
|
||||
0
lms/djangoapps/notes/migrations/__init__.py
Normal file
0
lms/djangoapps/notes/migrations/__init__.py
Normal file
81
lms/djangoapps/notes/models.py
Normal file
81
lms/djangoapps/notes/models.py
Normal file
@@ -0,0 +1,81 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.html import strip_tags
|
||||
import json
|
||||
|
||||
|
||||
class Note(models.Model):
|
||||
user = models.ForeignKey(User, db_index=True)
|
||||
course_id = models.CharField(max_length=255, db_index=True)
|
||||
uri = models.CharField(max_length=1024, db_index=True)
|
||||
text = models.TextField(default="")
|
||||
quote = models.TextField(default="")
|
||||
range_start = models.CharField(max_length=2048) # xpath string
|
||||
range_start_offset = models.IntegerField()
|
||||
range_end = models.CharField(max_length=2048) # xpath string
|
||||
range_end_offset = models.IntegerField()
|
||||
tags = models.TextField(default="") # comma-separated string
|
||||
created = models.DateTimeField(auto_now_add=True, null=True, db_index=True)
|
||||
updated = models.DateTimeField(auto_now=True, db_index=True)
|
||||
|
||||
def clean(self, json_body):
|
||||
'''
|
||||
Cleans the note object or raises a ValidationError.
|
||||
'''
|
||||
if json_body is None:
|
||||
raise ValidationError('Note must have a body.')
|
||||
|
||||
body = json.loads(json_body)
|
||||
if not type(body) is dict:
|
||||
raise ValidationError('Note body must be a dictionary.')
|
||||
|
||||
# NOTE: all three of these fields should be considered user input
|
||||
# and may be output back to the user, so we need to sanitize them.
|
||||
# These fields should only contain _plain text_.
|
||||
self.uri = strip_tags(body.get('uri', ''))
|
||||
self.text = strip_tags(body.get('text', ''))
|
||||
self.quote = strip_tags(body.get('quote', ''))
|
||||
|
||||
ranges = body.get('ranges')
|
||||
if ranges is None or len(ranges) != 1:
|
||||
raise ValidationError('Note must contain exactly one range.')
|
||||
|
||||
self.range_start = ranges[0]['start']
|
||||
self.range_start_offset = ranges[0]['startOffset']
|
||||
self.range_end = ranges[0]['end']
|
||||
self.range_end_offset = ranges[0]['endOffset']
|
||||
|
||||
self.tags = ""
|
||||
tags = [strip_tags(tag) for tag in body.get('tags', [])]
|
||||
if len(tags) > 0:
|
||||
self.tags = ",".join(tags)
|
||||
|
||||
def get_absolute_url(self):
|
||||
'''
|
||||
Returns the aboslute url for the note object.
|
||||
'''
|
||||
kwargs = {'course_id': self.course_id, 'note_id': str(self.pk)}
|
||||
return reverse('notes_api_note', kwargs=kwargs)
|
||||
|
||||
def as_dict(self):
|
||||
'''
|
||||
Returns the note object as a dictionary.
|
||||
'''
|
||||
return {
|
||||
'id': self.pk,
|
||||
'user_id': self.user.pk,
|
||||
'uri': self.uri,
|
||||
'text': self.text,
|
||||
'quote': self.quote,
|
||||
'ranges': [{
|
||||
'start': self.range_start,
|
||||
'startOffset': self.range_start_offset,
|
||||
'end': self.range_end,
|
||||
'endOffset': self.range_end_offset
|
||||
}],
|
||||
'tags': self.tags.split(","),
|
||||
'created': str(self.created),
|
||||
'updated': str(self.updated)
|
||||
}
|
||||
398
lms/djangoapps/notes/tests.py
Normal file
398
lms/djangoapps/notes/tests.py
Normal file
@@ -0,0 +1,398 @@
|
||||
"""
|
||||
Unit tests for the notes app.
|
||||
"""
|
||||
|
||||
from django.test import TestCase
|
||||
from django.test.client import Client
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
import collections
|
||||
import unittest
|
||||
import json
|
||||
import logging
|
||||
|
||||
from . import utils, api, models
|
||||
|
||||
|
||||
class UtilsTest(TestCase):
|
||||
def setUp(self):
|
||||
'''
|
||||
Setup a dummy course-like object with a tabs field that can be
|
||||
accessed via attribute lookup.
|
||||
'''
|
||||
self.course = collections.namedtuple('DummyCourse', ['tabs'])
|
||||
self.course.tabs = []
|
||||
|
||||
def test_notes_not_enabled(self):
|
||||
'''
|
||||
Tests that notes are disabled when the course tab configuration does NOT
|
||||
contain a tab with type "notes."
|
||||
'''
|
||||
self.assertFalse(utils.notes_enabled_for_course(self.course))
|
||||
|
||||
def test_notes_enabled(self):
|
||||
'''
|
||||
Tests that notes are enabled when the course tab configuration contains
|
||||
a tab with type "notes."
|
||||
'''
|
||||
self.course.tabs = [{'type': 'foo'},
|
||||
{'name': 'My Notes', 'type': 'notes'},
|
||||
{'type': 'bar'}]
|
||||
|
||||
self.assertTrue(utils.notes_enabled_for_course(self.course))
|
||||
|
||||
|
||||
class ApiTest(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
|
||||
# Mocks
|
||||
api.api_enabled = self.mock_api_enabled(True)
|
||||
|
||||
# Create two accounts
|
||||
self.password = 'abc'
|
||||
self.student = User.objects.create_user('student', 'student@test.com', self.password)
|
||||
self.student2 = User.objects.create_user('student2', 'student2@test.com', self.password)
|
||||
self.instructor = User.objects.create_user('instructor', 'instructor@test.com', self.password)
|
||||
self.course_id = 'HarvardX/CB22x/The_Ancient_Greek_Hero'
|
||||
self.note = {
|
||||
'user': self.student,
|
||||
'course_id': self.course_id,
|
||||
'uri': '/',
|
||||
'text': 'foo',
|
||||
'quote': 'bar',
|
||||
'range_start': 0,
|
||||
'range_start_offset': 0,
|
||||
'range_end': 100,
|
||||
'range_end_offset': 0,
|
||||
'tags': 'a,b,c'
|
||||
}
|
||||
|
||||
# Make sure no note with this ID ever exists for testing purposes
|
||||
self.NOTE_ID_DOES_NOT_EXIST = 99999
|
||||
|
||||
def mock_api_enabled(self, is_enabled):
|
||||
return (lambda request, course_id: is_enabled)
|
||||
|
||||
def login(self, as_student=None):
|
||||
username = None
|
||||
password = self.password
|
||||
|
||||
if as_student is None:
|
||||
username = self.student.username
|
||||
else:
|
||||
username = as_student.username
|
||||
|
||||
self.client.login(username=username, password=password)
|
||||
|
||||
def url(self, name, args={}):
|
||||
args.update({'course_id': self.course_id})
|
||||
return reverse(name, kwargs=args)
|
||||
|
||||
def create_notes(self, num_notes, create=True):
|
||||
notes = []
|
||||
for n in range(num_notes):
|
||||
note = models.Note(**self.note)
|
||||
if create:
|
||||
note.save()
|
||||
notes.append(note)
|
||||
return notes
|
||||
|
||||
def test_root(self):
|
||||
self.login()
|
||||
|
||||
resp = self.client.get(self.url('notes_api_root'))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertNotEqual(resp.content, '')
|
||||
|
||||
content = json.loads(resp.content)
|
||||
|
||||
self.assertEqual(set(('name', 'version')), set(content.keys()))
|
||||
self.assertIsInstance(content['version'], int)
|
||||
self.assertEqual(content['name'], 'Notes API')
|
||||
|
||||
def test_index_empty(self):
|
||||
self.login()
|
||||
|
||||
resp = self.client.get(self.url('notes_api_notes'))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertNotEqual(resp.content, '')
|
||||
|
||||
content = json.loads(resp.content)
|
||||
self.assertEqual(len(content), 0)
|
||||
|
||||
def test_index_with_notes(self):
|
||||
num_notes = 3
|
||||
self.login()
|
||||
self.create_notes(num_notes)
|
||||
|
||||
resp = self.client.get(self.url('notes_api_notes'))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertNotEqual(resp.content, '')
|
||||
|
||||
content = json.loads(resp.content)
|
||||
self.assertIsInstance(content, list)
|
||||
self.assertEqual(len(content), num_notes)
|
||||
|
||||
def test_index_max_notes(self):
|
||||
self.login()
|
||||
|
||||
MAX_LIMIT = api.API_SETTINGS.get('MAX_NOTE_LIMIT')
|
||||
num_notes = MAX_LIMIT + 1
|
||||
self.create_notes(num_notes)
|
||||
|
||||
resp = self.client.get(self.url('notes_api_notes'))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertNotEqual(resp.content, '')
|
||||
|
||||
content = json.loads(resp.content)
|
||||
self.assertIsInstance(content, list)
|
||||
self.assertEqual(len(content), MAX_LIMIT)
|
||||
|
||||
def test_create_note(self):
|
||||
self.login()
|
||||
|
||||
notes = self.create_notes(1)
|
||||
self.assertEqual(len(notes), 1)
|
||||
|
||||
note_dict = notes[0].as_dict()
|
||||
excluded_fields = ['id', 'user_id', 'created', 'updated']
|
||||
note = dict([(k, v) for k, v in note_dict.items() if k not in excluded_fields])
|
||||
|
||||
resp = self.client.post(self.url('notes_api_notes'),
|
||||
json.dumps(note),
|
||||
content_type='application/json',
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
|
||||
self.assertEqual(resp.status_code, 303)
|
||||
self.assertEqual(len(resp.content), 0)
|
||||
|
||||
def test_create_empty_notes(self):
|
||||
self.login()
|
||||
|
||||
for empty_test in [None, [], '']:
|
||||
resp = self.client.post(self.url('notes_api_notes'),
|
||||
json.dumps(empty_test),
|
||||
content_type='application/json',
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
def test_create_note_missing_ranges(self):
|
||||
self.login()
|
||||
|
||||
notes = self.create_notes(1)
|
||||
self.assertEqual(len(notes), 1)
|
||||
note_dict = notes[0].as_dict()
|
||||
|
||||
excluded_fields = ['id', 'user_id', 'created', 'updated'] + ['ranges']
|
||||
note = dict([(k, v) for k, v in note_dict.items() if k not in excluded_fields])
|
||||
|
||||
resp = self.client.post(self.url('notes_api_notes'),
|
||||
json.dumps(note),
|
||||
content_type='application/json',
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
def test_read_note(self):
|
||||
self.login()
|
||||
|
||||
notes = self.create_notes(3)
|
||||
self.assertEqual(len(notes), 3)
|
||||
|
||||
for note in notes:
|
||||
resp = self.client.get(self.url('notes_api_note', {'note_id': note.pk}))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertNotEqual(resp.content, '')
|
||||
|
||||
content = json.loads(resp.content)
|
||||
self.assertEqual(content['id'], note.pk)
|
||||
self.assertEqual(content['user_id'], note.user_id)
|
||||
|
||||
def test_note_doesnt_exist_to_read(self):
|
||||
self.login()
|
||||
resp = self.client.get(self.url('notes_api_note', {
|
||||
'note_id': self.NOTE_ID_DOES_NOT_EXIST
|
||||
}))
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
self.assertEqual(resp.content, '')
|
||||
|
||||
def test_student_doesnt_have_permission_to_read_note(self):
|
||||
notes = self.create_notes(1)
|
||||
self.assertEqual(len(notes), 1)
|
||||
note = notes[0]
|
||||
|
||||
# set the student id to a different student (not the one that created the notes)
|
||||
self.login(as_student=self.student2)
|
||||
resp = self.client.get(self.url('notes_api_note', {'note_id': note.pk}))
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
self.assertEqual(resp.content, '')
|
||||
|
||||
def test_delete_note(self):
|
||||
self.login()
|
||||
|
||||
notes = self.create_notes(1)
|
||||
self.assertEqual(len(notes), 1)
|
||||
note = notes[0]
|
||||
|
||||
resp = self.client.delete(self.url('notes_api_note', {
|
||||
'note_id': note.pk
|
||||
}))
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
self.assertEqual(resp.content, '')
|
||||
|
||||
with self.assertRaises(models.Note.DoesNotExist):
|
||||
models.Note.objects.get(pk=note.pk)
|
||||
|
||||
def test_note_does_not_exist_to_delete(self):
|
||||
self.login()
|
||||
|
||||
resp = self.client.delete(self.url('notes_api_note', {
|
||||
'note_id': self.NOTE_ID_DOES_NOT_EXIST
|
||||
}))
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
self.assertEqual(resp.content, '')
|
||||
|
||||
def test_student_doesnt_have_permission_to_delete_note(self):
|
||||
notes = self.create_notes(1)
|
||||
self.assertEqual(len(notes), 1)
|
||||
note = notes[0]
|
||||
|
||||
self.login(as_student=self.student2)
|
||||
resp = self.client.delete(self.url('notes_api_note', {
|
||||
'note_id': note.pk
|
||||
}))
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
self.assertEqual(resp.content, '')
|
||||
|
||||
try:
|
||||
models.Note.objects.get(pk=note.pk)
|
||||
except models.Note.DoesNotExist:
|
||||
self.fail('note should exist and not be deleted because the student does not have permission to do so')
|
||||
|
||||
def test_update_note(self):
|
||||
notes = self.create_notes(1)
|
||||
note = notes[0]
|
||||
|
||||
updated_dict = note.as_dict()
|
||||
updated_dict.update({
|
||||
'text': 'itchy and scratchy',
|
||||
'tags': ['simpsons', 'cartoons', 'animation']
|
||||
})
|
||||
|
||||
self.login()
|
||||
resp = self.client.put(self.url('notes_api_note', {'note_id': note.pk}),
|
||||
json.dumps(updated_dict),
|
||||
content_type='application/json',
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
self.assertEqual(resp.status_code, 303)
|
||||
self.assertEqual(resp.content, '')
|
||||
|
||||
actual = models.Note.objects.get(pk=note.pk)
|
||||
actual_dict = actual.as_dict()
|
||||
for field in ['text', 'tags']:
|
||||
self.assertEqual(actual_dict[field], updated_dict[field])
|
||||
|
||||
def test_search_note_params(self):
|
||||
self.login()
|
||||
|
||||
total = 3
|
||||
notes = self.create_notes(total)
|
||||
invalid_uri = ''.join([note.uri for note in notes])
|
||||
|
||||
tests = [{'limit': 0, 'offset': 0, 'expected_rows': total},
|
||||
{'limit': 0, 'offset': 2, 'expected_rows': total - 2},
|
||||
{'limit': 0, 'offset': total, 'expected_rows': 0},
|
||||
{'limit': 1, 'offset': 0, 'expected_rows': 1},
|
||||
{'limit': 2, 'offset': 0, 'expected_rows': 2},
|
||||
{'limit': total, 'offset': 2, 'expected_rows': 1},
|
||||
{'limit': total, 'offset': total, 'expected_rows': 0},
|
||||
{'limit': total + 1, 'offset': total + 1, 'expected_rows': 0},
|
||||
{'limit': total + 1, 'offset': 0, 'expected_rows': total},
|
||||
{'limit': 0, 'offset': 0, 'uri': invalid_uri, 'expected_rows': 0, 'expected_total': 0}]
|
||||
|
||||
for test in tests:
|
||||
params = dict([(k, str(test[k]))
|
||||
for k in ('limit', 'offset', 'uri')
|
||||
if k in test])
|
||||
resp = self.client.get(self.url('notes_api_search'),
|
||||
params,
|
||||
content_type='application/json',
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertNotEqual(resp.content, '')
|
||||
|
||||
content = json.loads(resp.content)
|
||||
|
||||
for expected_key in ('total', 'rows'):
|
||||
self.assertTrue(expected_key in content)
|
||||
|
||||
if 'expected_total' in test:
|
||||
self.assertEqual(content['total'], test['expected_total'])
|
||||
else:
|
||||
self.assertEqual(content['total'], total)
|
||||
|
||||
self.assertEqual(len(content['rows']), test['expected_rows'])
|
||||
|
||||
for row in content['rows']:
|
||||
self.assertTrue('id' in row)
|
||||
|
||||
|
||||
class NoteTest(TestCase):
|
||||
def setUp(self):
|
||||
self.password = 'abc'
|
||||
self.student = User.objects.create_user('student', 'student@test.com', self.password)
|
||||
self.course_id = 'HarvardX/CB22x/The_Ancient_Greek_Hero'
|
||||
self.note = {
|
||||
'user': self.student,
|
||||
'course_id': self.course_id,
|
||||
'uri': '/',
|
||||
'text': 'foo',
|
||||
'quote': 'bar',
|
||||
'range_start': 0,
|
||||
'range_start_offset': 0,
|
||||
'range_end': 100,
|
||||
'range_end_offset': 0,
|
||||
'tags': 'a,b,c'
|
||||
}
|
||||
|
||||
def test_clean_valid_note(self):
|
||||
reference_note = models.Note(**self.note)
|
||||
body = reference_note.as_dict()
|
||||
|
||||
note = models.Note(course_id=self.course_id, user=self.student)
|
||||
try:
|
||||
note.clean(json.dumps(body))
|
||||
self.assertEqual(note.uri, body['uri'])
|
||||
self.assertEqual(note.text, body['text'])
|
||||
self.assertEqual(note.quote, body['quote'])
|
||||
self.assertEqual(note.range_start, body['ranges'][0]['start'])
|
||||
self.assertEqual(note.range_start_offset, body['ranges'][0]['startOffset'])
|
||||
self.assertEqual(note.range_end, body['ranges'][0]['end'])
|
||||
self.assertEqual(note.range_end_offset, body['ranges'][0]['endOffset'])
|
||||
self.assertEqual(note.tags, ','.join(body['tags']))
|
||||
except ValidationError:
|
||||
self.fail('a valid note should not raise an exception')
|
||||
|
||||
def test_clean_invalid_note(self):
|
||||
note = models.Note(course_id=self.course_id, user=self.student)
|
||||
for empty_type in (None, '', 0, []):
|
||||
with self.assertRaises(ValidationError):
|
||||
note.clean(None)
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
note.clean(json.dumps({
|
||||
'text': 'foo',
|
||||
'quote': 'bar',
|
||||
'ranges': [{} for i in range(10)] # too many ranges
|
||||
}))
|
||||
|
||||
def test_as_dict(self):
|
||||
note = models.Note(course_id=self.course_id, user=self.student)
|
||||
d = note.as_dict()
|
||||
self.assertNotIsInstance(d, basestring)
|
||||
self.assertEqual(d['user_id'], self.student.id)
|
||||
self.assertTrue('course_id' not in d)
|
||||
10
lms/djangoapps/notes/urls.py
Normal file
10
lms/djangoapps/notes/urls.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django.conf.urls import patterns, url
|
||||
|
||||
|
||||
id_regex = r"(?P<note_id>[0-9A-Fa-f]+)"
|
||||
urlpatterns = patterns('notes.api',
|
||||
url(r'^api$', 'api_request', {'resource': 'root'}, name='notes_api_root'),
|
||||
url(r'^api/annotations$', 'api_request', {'resource': 'notes'}, name='notes_api_notes'),
|
||||
url(r'^api/annotations/' + id_regex + r'$', 'api_request', {'resource': 'note'}, name='notes_api_note'),
|
||||
url(r'^api/search', 'api_request', {'resource': 'search'}, name='notes_api_search')
|
||||
)
|
||||
17
lms/djangoapps/notes/utils.py
Normal file
17
lms/djangoapps/notes/utils.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def notes_enabled_for_course(course):
|
||||
|
||||
'''
|
||||
Returns True if the notes app is enabled for the course, False otherwise.
|
||||
|
||||
In order for the app to be enabled it must be:
|
||||
1) enabled globally via MITX_FEATURES.
|
||||
2) present in the course tab configuration.
|
||||
'''
|
||||
|
||||
tab_found = next((True for t in course.tabs if t['type'] == 'notes'), False)
|
||||
feature_enabled = settings.MITX_FEATURES.get('ENABLE_STUDENT_NOTES')
|
||||
|
||||
return feature_enabled and tab_found
|
||||
24
lms/djangoapps/notes/views.py
Normal file
24
lms/djangoapps/notes/views.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import Http404
|
||||
from mitxmako.shortcuts import render_to_response
|
||||
from courseware.courses import get_course_with_access
|
||||
from notes.models import Note
|
||||
from notes.utils import notes_enabled_for_course
|
||||
import json
|
||||
|
||||
|
||||
@login_required
|
||||
def notes(request, course_id):
|
||||
''' Displays the student's notes. '''
|
||||
|
||||
course = get_course_with_access(request.user, course_id, 'load')
|
||||
if not notes_enabled_for_course(course):
|
||||
raise Http404
|
||||
|
||||
notes = Note.objects.filter(course_id=course_id, user=request.user).order_by('-created', 'uri')
|
||||
context = {
|
||||
'course': course,
|
||||
'notes': notes
|
||||
}
|
||||
|
||||
return render_to_response('notes.html', context)
|
||||
@@ -1,9 +1,11 @@
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import Http404
|
||||
from django.core.urlresolvers import reverse
|
||||
from mitxmako.shortcuts import render_to_response
|
||||
|
||||
from courseware.access import has_access
|
||||
from courseware.courses import get_course_with_access
|
||||
from notes.utils import notes_enabled_for_course
|
||||
from static_replace import replace_static_urls
|
||||
|
||||
|
||||
@@ -23,7 +25,8 @@ def index(request, course_id, book_index, page=None):
|
||||
|
||||
return render_to_response('staticbook.html',
|
||||
{'book_index': book_index, 'page': int(page),
|
||||
'course': course, 'book_url': textbook.book_url,
|
||||
'course': course,
|
||||
'book_url': textbook.book_url,
|
||||
'table_of_contents': table_of_contents,
|
||||
'start_page': textbook.start_page,
|
||||
'end_page': textbook.end_page,
|
||||
@@ -100,6 +103,7 @@ def html_index(request, course_id, book_index, chapter=None):
|
||||
"""
|
||||
course = get_course_with_access(request.user, course_id, 'load')
|
||||
staff_access = has_access(request.user, course, 'staff')
|
||||
notes_enabled = notes_enabled_for_course(course)
|
||||
|
||||
book_index = int(book_index)
|
||||
if book_index < 0 or book_index >= len(course.html_textbooks):
|
||||
@@ -128,4 +132,5 @@ def html_index(request, course_id, book_index, chapter=None):
|
||||
'course': course,
|
||||
'textbook': textbook,
|
||||
'chapter': chapter,
|
||||
'staff_access': staff_access})
|
||||
'staff_access': staff_access,
|
||||
'notes_enabled': notes_enabled})
|
||||
|
||||
@@ -92,6 +92,9 @@ MITX_FEATURES = {
|
||||
# Staff Debug tool.
|
||||
'ENABLE_STUDENT_HISTORY_VIEW': True,
|
||||
|
||||
# Enables the student notes API and UI.
|
||||
'ENABLE_STUDENT_NOTES': True,
|
||||
|
||||
# Provide a UI to allow users to submit feedback from the LMS
|
||||
'ENABLE_FEEDBACK_SUBMISSION': False,
|
||||
}
|
||||
@@ -422,11 +425,15 @@ main_vendor_js = [
|
||||
'js/vendor/jquery.qtip.min.js',
|
||||
'js/vendor/swfobject/swfobject.js',
|
||||
'js/vendor/jquery.ba-bbq.min.js',
|
||||
'js/vendor/annotator.min.js',
|
||||
'js/vendor/annotator.store.min.js',
|
||||
'js/vendor/annotator.tags.min.js'
|
||||
]
|
||||
|
||||
discussion_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/discussion/**/*.js'))
|
||||
staff_grading_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/staff_grading/**/*.js'))
|
||||
open_ended_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/open_ended/**/*.js'))
|
||||
notes_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/notes/**/*.coffee'))
|
||||
|
||||
PIPELINE_CSS = {
|
||||
'application': {
|
||||
@@ -439,6 +446,7 @@ PIPELINE_CSS = {
|
||||
'css/vendor/jquery.treeview.css',
|
||||
'css/vendor/ui-lightness/jquery-ui-1.8.22.custom.css',
|
||||
'css/vendor/jquery.qtip.min.css',
|
||||
'css/vendor/annotator.min.css',
|
||||
'sass/course.css',
|
||||
'xmodule/modules.css',
|
||||
],
|
||||
@@ -460,7 +468,7 @@ PIPELINE_JS = {
|
||||
'source_filenames': sorted(
|
||||
set(rooted_glob(COMMON_ROOT / 'static', 'coffee/src/**/*.js') +
|
||||
rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/**/*.js')) -
|
||||
set(courseware_js + discussion_js + staff_grading_js + open_ended_js)
|
||||
set(courseware_js + discussion_js + staff_grading_js + open_ended_js + notes_js)
|
||||
) + [
|
||||
'js/form.ext.js',
|
||||
'js/my_courses_dropdown.js',
|
||||
@@ -501,7 +509,12 @@ PIPELINE_JS = {
|
||||
'source_filenames': open_ended_js,
|
||||
'output_filename': 'js/open_ended.js',
|
||||
'test_order': 6,
|
||||
}
|
||||
},
|
||||
'notes': {
|
||||
'source_filenames': notes_js,
|
||||
'output_filename': 'js/notes.js',
|
||||
'test_order': 7
|
||||
},
|
||||
}
|
||||
|
||||
PIPELINE_DISABLE_WRAPPER = True
|
||||
@@ -591,5 +604,8 @@ INSTALLED_APPS = (
|
||||
|
||||
# Discussion forums
|
||||
'django_comment_client',
|
||||
|
||||
# Student notes
|
||||
'notes',
|
||||
)
|
||||
|
||||
|
||||
73
lms/static/coffee/src/notes.coffee
Normal file
73
lms/static/coffee/src/notes.coffee
Normal file
@@ -0,0 +1,73 @@
|
||||
class StudentNotes
|
||||
_debug: false
|
||||
|
||||
targets: [] # holds elements with annotator() instances
|
||||
|
||||
# Adds a listener for "notes" events that may bubble up from descendants.
|
||||
constructor: ($, el) ->
|
||||
console.log 'student notes init', arguments, this if @_debug
|
||||
|
||||
if not $(el).data('notes-instance')
|
||||
events = 'notes:init': @onInitNotes
|
||||
$(el).delegate('*', events)
|
||||
$(el).data('notes-instance', @)
|
||||
|
||||
# Initializes annotations on a container element in response to an init event.
|
||||
onInitNotes: (event, uri=null) =>
|
||||
event.stopPropagation()
|
||||
|
||||
storeConfig = @getStoreConfig uri
|
||||
found = @targets.some (target) -> target is event.target
|
||||
|
||||
if found
|
||||
annotator = $(event.target).data('annotator')
|
||||
if annotator
|
||||
store = annotator.plugins['Store']
|
||||
$.extend(store.options, storeConfig)
|
||||
if uri
|
||||
store.loadAnnotationsFromSearch(storeConfig['loadFromSearch'])
|
||||
else
|
||||
console.log 'URI is required to load annotations'
|
||||
else
|
||||
console.log 'No annotator() instance found for target: ', event.target
|
||||
else
|
||||
$(event.target).annotator()
|
||||
.annotator('addPlugin', 'Tags')
|
||||
.annotator('addPlugin', 'Store', storeConfig)
|
||||
@targets.push(event.target)
|
||||
|
||||
# Returns a JSON config object that can be passed to the annotator Store plugin
|
||||
getStoreConfig: (uri) ->
|
||||
prefix = @getPrefix()
|
||||
if uri is null
|
||||
uri = @getURIPath()
|
||||
|
||||
storeConfig =
|
||||
prefix: prefix
|
||||
loadFromSearch:
|
||||
uri: uri
|
||||
limit: 0
|
||||
annotationData:
|
||||
uri: uri
|
||||
storeConfig
|
||||
|
||||
# Returns the API endpoint for the annotation store
|
||||
getPrefix: () ->
|
||||
re = /^(\/courses\/[^/]+\/[^/]+\/[^/]+)/
|
||||
match = re.exec(@getURIPath())
|
||||
prefix = (if match then match[1] else '')
|
||||
return "#{prefix}/notes/api"
|
||||
|
||||
# Returns the URI path of the current page for filtering annotations
|
||||
getURIPath: () ->
|
||||
window.location.href.toString().split(window.location.host)[1]
|
||||
|
||||
|
||||
# Enable notes by default on the document root.
|
||||
# To initialize annotations on a container element in the document:
|
||||
#
|
||||
# $('#myElement').trigger('notes:init');
|
||||
#
|
||||
# Comment this line to disable notes.
|
||||
|
||||
$(document).ready ($) -> new StudentNotes $, @
|
||||
899
lms/static/css/vendor/annotator.css
vendored
Normal file
899
lms/static/css/vendor/annotator.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
lms/static/css/vendor/annotator.min.css
vendored
Normal file
1
lms/static/css/vendor/annotator.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
81
lms/templates/notes.html
Normal file
81
lms/templates/notes.html
Normal file
@@ -0,0 +1,81 @@
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
<%inherit file="main.html" />
|
||||
<%!
|
||||
from django.core.urlresolvers import reverse
|
||||
%>
|
||||
|
||||
<%block name="headextra">
|
||||
<%static:css group='course'/>
|
||||
<%static:js group='courseware'/>
|
||||
<style type="text/css">
|
||||
blockquote {
|
||||
background:#f9f9f9;
|
||||
border-left:10px solid #ccc;
|
||||
margin:1.5em 10px;
|
||||
padding:.5em 10px;
|
||||
}
|
||||
blockquote:before {
|
||||
color:#ccc;
|
||||
content:'“';
|
||||
font-size:4em;
|
||||
line-height:.1em;
|
||||
margin-right:.25em;
|
||||
vertical-align:-.4em;
|
||||
}
|
||||
blockquote p {
|
||||
display:inline;
|
||||
}
|
||||
.notes-wrapper {
|
||||
padding: 32px 40px;
|
||||
}
|
||||
.note {
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding: 0 0 1em 0;
|
||||
}
|
||||
.note .text {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.note ul.meta {
|
||||
margin: .5em 0;
|
||||
}
|
||||
.note ul.meta li {
|
||||
font-size: .9em;
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
</style>
|
||||
|
||||
</%block>
|
||||
|
||||
<%block name="js_extra">
|
||||
<script type="text/javascript">
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
<%include file="/courseware/course_navigation.html" args="active_page='notes'" />
|
||||
|
||||
<section class="container">
|
||||
<div class="notes-wrapper">
|
||||
<h1>My Notes</h1>
|
||||
% for note in notes:
|
||||
<div class="note">
|
||||
<blockquote>${note.quote|h}</blockquote>
|
||||
<div class="text">${note.text.replace("\n", "<br />") | n,h}</div>
|
||||
<ul class="meta">
|
||||
% if note.tags:
|
||||
<li class="tags">Tags: ${note.tags|h}</li>
|
||||
% endif
|
||||
<li class="user">Author: ${note.user.username}</li>
|
||||
<li class="time">Created: ${note.created.strftime('%m/%d/%Y %H:%m')}</li>
|
||||
<li class="uri">Source: <a href="${note.uri}">${note.uri|h}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
% endfor
|
||||
% if notes is UNDEFINED or len(notes) == 0:
|
||||
<p>You do not have any notes.</p>
|
||||
% endif
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -26,22 +26,41 @@
|
||||
// chapters, and it should be in-bounds.
|
||||
chapterToLoad = options.chapterNum;
|
||||
}
|
||||
var anchorToLoad = null;
|
||||
if (options.chapters) {
|
||||
anchorToLoad = options.anchor_id;
|
||||
}
|
||||
|
||||
loadUrl = function htmlViewLoadUrl(url) {
|
||||
var onComplete = function() {};
|
||||
if(options.notesEnabled) {
|
||||
onComplete = function(url) {
|
||||
return function() {
|
||||
$('#viewerContainer').trigger('notes:init', [url]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
loadUrl = function htmlViewLoadUrl(url, anchorId) {
|
||||
// clear out previous load, if any:
|
||||
parentElement = document.getElementById('bookpage');
|
||||
while (parentElement.hasChildNodes())
|
||||
parentElement.removeChild(parentElement.lastChild);
|
||||
// load new URL in:
|
||||
$('#bookpage').load(url);
|
||||
};
|
||||
$('#bookpage').load(url, null, onComplete(url));
|
||||
|
||||
loadChapterUrl = function htmlViewLoadChapterUrl(chapterNum) {
|
||||
// if there is an anchor set, then go to that location:
|
||||
if (anchorId != null) {
|
||||
// TODO: add implementation....
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
loadChapterUrl = function htmlViewLoadChapterUrl(chapterNum, anchorId) {
|
||||
if (chapterNum < 1 || chapterNum > chapterUrls.length) {
|
||||
return;
|
||||
}
|
||||
var chapterUrl = chapterUrls[chapterNum-1];
|
||||
loadUrl(chapterUrl);
|
||||
loadUrl(chapterUrl, anchorId);
|
||||
};
|
||||
|
||||
// define navigation links for chapters:
|
||||
@@ -54,15 +73,15 @@
|
||||
};
|
||||
for (var index = 1; index <= chapterUrls.length; index += 1) {
|
||||
$("#htmlchapter-" + index).click(loadChapterUrlHelper(index));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// finally, load the appropriate url/page
|
||||
if (urlToLoad != null) {
|
||||
loadUrl(urlToLoad);
|
||||
loadUrl(urlToLoad, anchorToLoad);
|
||||
} else {
|
||||
loadChapterUrl(chapterToLoad);
|
||||
}
|
||||
loadChapterUrl(chapterToLoad, anchorToLoad);
|
||||
}
|
||||
|
||||
}
|
||||
})(jQuery);
|
||||
@@ -82,6 +101,14 @@
|
||||
%if chapter is not None:
|
||||
options.chapterNum = ${chapter};
|
||||
%endif
|
||||
%if anchor_id is not UNDEFINED and anchor_id is not None:
|
||||
options.anchor_id = ${anchor_id};
|
||||
%endif
|
||||
|
||||
options.notesEnabled = false;
|
||||
%if notes_enabled is not UNDEFINED and notes_enabled:
|
||||
options.notesEnabled = true;
|
||||
%endif
|
||||
|
||||
$('#outerContainer').myHTMLViewer(options);
|
||||
});
|
||||
|
||||
@@ -283,6 +283,10 @@ if settings.COURSEWARE_ENABLED:
|
||||
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/peer_grading$',
|
||||
'open_ended_grading.views.peer_grading', name='peer_grading'),
|
||||
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/notes$', 'notes.views.notes', name='notes'),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/notes/', include('notes.urls')),
|
||||
|
||||
)
|
||||
|
||||
# allow course staff to change to student view of courseware
|
||||
|
||||
Reference in New Issue
Block a user