Commit 7e2652b5 by daniel cebrian Committed by lduarte1991

annotation tools

First set of fixes from the pull request

This does not include some of the testing files. The textannotation and
videoannotation test files are not ready. waiting for an answer on the
issue.

Deleted token line in api.py and added test for token generator

Added notes_spec.coffee

remove spec file

fixed minor error with the test

fixes some quality errors

fixed unit test

fixed unit test

added advanced module

Added notes_spec.coffee

remove spec file

Quality and  Testing Coverage

1. in test_textannotation.py I already check for line 75 as it states
in the diff in line 43, same with test_videoanntotation
2. Like you said, exceptions cannot be checked for
firebase_token_generator.py. The version of python that is active on
the edx server is 2.7 or higher, but the code is there for correctness.
Error checking works the same way.
3. I added a test for student/views/.py within tests and deleted the
unused secret assignment.
4. test_token_generator.py is now its own file

Added Secret Token data input

fixed token generator

Annotation Tools in Place

The purpose of this pull request is to install two major modules: (1) a
module to annotate text and (2) a module to annotate video. In either
case an instructor can declare them in advanced settings under
advanced_modules and input content (HTML in text, mp4 or YouTube videos
for video). Students will be able to highlight portions and add their
comments as well as reply to each other. There needs to be a storage
server set up per course as well as a secret token to talk with said
storage.

Changes:
1. Added test to check for the creation of a token in tests.py (along
with the rest of the tests for student/view.py)
2. Removed items in cms pertaining to annotation as this will only be
possible in the lms
3. Added more comments to firebase_token_generator.py, the test files,
students/views.py
4. Added some internationalization stuff to textannotation.html and
videoannotation.html. I need some help with doing it in javascript, but
the html is covered.

incorporated lib for traslate

fixed quality errors

fixed my notes with catch token

Text and Video Annotation Modules - First Iteration

The following code-change is the first iteration of the modules for
text and video annotation.

Installing Modules:
1. Under “Advanced Settings”, add “textannotation” and
“videoannotation” to the list of advanced_modules.
2. Add link to an external storage for annotations under
“annotation_storage_url”
3. Add the secret token for talking with said storage under
“annotation_token_secret”

Using Modules
1. When creating  new unit, you can find Text and Video annotation
modules under “Advanced” component
2. Make sure you have either Text or Video in one unit, but not both.
3. Annotations are only allowed on Live/Public version and not Studio.

Added missing templates and fixed more of the quality errors

Fixed annotator not existing issue in cmd and tried to find the get_html() from the annotation module class to the descriptor

Added a space after # in comments

Fixed issue with an empty Module and token links

Added licenses and fixed vis naming scheme and location.
parent 48a5c760
......@@ -146,6 +146,8 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# response HTML
self.check_components_on_page(ADVANCED_COMPONENT_TYPES, ['Word cloud',
'Annotation',
'Text Annotation',
'Video Annotation',
'Open Response Assessment',
'Peer Grading Interface'])
......
......@@ -52,6 +52,8 @@ else:
ADVANCED_COMPONENT_TYPES = [
'annotatable',
'textannotation', # module for annotating text (with annotation table)
'videoannotation', # module for annotating video (with annotation table)
'word_cloud',
'graphical_slider_tool',
'lti',
......
'''
Firebase - library to generate a token
License: https://github.com/firebase/firebase-token-generator-python/blob/master/LICENSE
Tweaked and Edited by @danielcebrianr and @lduarte1991
This library will take either objects or strings and use python's built-in encoding
system as specified by RFC 3548. Thanks to the firebase team for their open-source
library. This was made specifically for speaking with the annotation_storage_url and
can be used and expanded, but not modified by anyone else needing such a process.
'''
from base64 import urlsafe_b64encode
import hashlib
import hmac
import sys
try:
import json
except ImportError:
import simplejson as json
__all__ = ['create_token']
TOKEN_SEP = '.'
def create_token(secret, data):
'''
Simply takes in the secret key and the data and
passes it to the local function _encode_token
'''
return _encode_token(secret, data)
if sys.version_info < (2, 7):
def _encode(bytes_data):
'''
Takes a json object, string, or binary and
uses python's urlsafe_b64encode to encode data
and make it safe pass along in a url.
To make sure it does not conflict with variables
we make sure equal signs are removed.
More info: docs.python.org/2/library/base64.html
'''
encoded = urlsafe_b64encode(bytes(bytes_data))
return encoded.decode('utf-8').replace('=', '')
else:
def _encode(bytes_info):
'''
Same as above function but for Python 2.7 or later
'''
encoded = urlsafe_b64encode(bytes_info)
return encoded.decode('utf-8').replace('=', '')
def _encode_json(obj):
'''
Before a python dict object can be properly encoded,
it must be transformed into a jason object and then
transformed into bytes to be encoded using the function
defined above.
'''
return _encode(bytearray(json.dumps(obj), 'utf-8'))
def _sign(secret, to_sign):
'''
This function creates a sign that goes at the end of the
message that is specific to the secret and not the actual
content of the encoded body.
More info on hashing: http://docs.python.org/2/library/hmac.html
The function creates a hashed values of the secret and to_sign
and returns the digested values based the secure hash
algorithm, 256
'''
def portable_bytes(string):
'''
Simply transforms a string into a bytes object,
which is a series of immutable integers 0<=x<=256.
Always try to encode as utf-8, unless it is not
compliant.
'''
try:
return bytes(string, 'utf-8')
except TypeError:
return bytes(string)
return _encode(hmac.new(portable_bytes(secret), portable_bytes(to_sign), hashlib.sha256).digest()) # pylint: disable=E1101
def _encode_token(secret, claims):
'''
This is the main function that takes the secret token and
the data to be transmitted. There is a header created for decoding
purposes. Token_SEP means that a period/full stop separates the
header, data object/message, and signatures.
'''
encoded_header = _encode_json({'typ': 'JWT', 'alg': 'HS256'})
encoded_claims = _encode_json(claims)
secure_bits = '%s%s%s' % (encoded_header, TOKEN_SEP, encoded_claims)
sig = _sign(secret, secure_bits)
return '%s%s%s' % (secure_bits, TOKEN_SEP, sig)
"""
This test will run for firebase_token_generator.py.
"""
from django.test import TestCase
from student.firebase_token_generator import _encode, _encode_json, _encode_token, create_token
class TokenGenerator(TestCase):
"""
Tests for the file firebase_token_generator.py
"""
def test_encode(self):
"""
This tests makes sure that no matter what version of python
you have, the _encode function still returns the appropriate result
for a string.
"""
expected = "dGVzdDE"
result = _encode("test1")
self.assertEqual(expected, result)
def test_encode_json(self):
"""
Same as above, but this one focuses on a python dict type
transformed into a json object and then encoded.
"""
expected = "eyJ0d28iOiAidGVzdDIiLCAib25lIjogInRlc3QxIn0"
result = _encode_json({'one': 'test1', 'two': 'test2'})
self.assertEqual(expected, result)
def test_create_token(self):
"""
Unlike its counterpart in student/views.py, this function
just checks for the encoding of a token. The other function
will test depending on time and user.
"""
expected = "eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJ1c2VySWQiOiAidXNlcm5hbWUiLCAidHRsIjogODY0MDB9.-p1sr7uwCapidTQ0qB7DdU2dbF-hViKpPNN_5vD10t8"
result1 = _encode_token('4c7f4d1c-8ac4-4e9f-84c8-b271c57fcac4', {"userId": "username", "ttl": 86400})
result2 = create_token('4c7f4d1c-8ac4-4e9f-84c8-b271c57fcac4', {"userId": "username", "ttl": 86400})
self.assertEqual(expected, result1)
self.assertEqual(expected, result2)
......@@ -20,6 +20,7 @@ from django.contrib.auth.hashers import UNUSABLE_PASSWORD
from django.contrib.auth.tokens import default_token_generator
from django.utils.http import int_to_base36
from django.core.urlresolvers import reverse
from django.http import HttpResponse
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
......@@ -30,7 +31,7 @@ from textwrap import dedent
from student.models import anonymous_id_for_user, user_by_anonymous_id, CourseEnrollment, unique_id_for_user
from student.views import (process_survey_link, _cert_info, password_reset, password_reset_confirm_wrapper,
change_enrollment, complete_course_mode_info)
change_enrollment, complete_course_mode_info, token, course_from_id)
from student.tests.factories import UserFactory, CourseModeFactory
from student.tests.test_email import mock_render_to_string
......@@ -556,3 +557,26 @@ class AnonymousLookupTable(TestCase):
anonymous_id = anonymous_id_for_user(self.user, self.course.id)
real_user = user_by_anonymous_id(anonymous_id)
self.assertEqual(self.user, real_user)
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
class Token(ModuleStoreTestCase):
"""
Test for the token generator. This creates a random course and passes it through the token file which generates the
token that will be passed in to the annotation_storage_url.
"""
request_factory = RequestFactory()
COURSE_SLUG = "100"
COURSE_NAME = "test_course"
COURSE_ORG = "edx"
def setUp(self):
self.course = CourseFactory.create(org=self.COURSE_ORG, display_name=self.COURSE_NAME, number=self.COURSE_SLUG)
self.user = User.objects.create(username="username", email="username")
self.req = self.request_factory.post('/token?course_id=edx/100/test_course', {'user': self.user})
self.req.user = self.user
def test_token(self):
expected = HttpResponse("eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJpc3N1ZWRBdCI6ICIyMDE0LTAxLTIzVDE5OjM1OjE3LjUyMjEwNC01OjAwIiwgImNvbnN1bWVyS2V5IjogInh4eHh4eHh4LXh4eHgteHh4eC14eHh4LXh4eHh4eHh4eHh4eCIsICJ1c2VySWQiOiAidXNlcm5hbWUiLCAidHRsIjogODY0MDB9.OjWz9mzqJnYuzX-f3uCBllqJUa8PVWJjcDy_McfxLvc", mimetype="text/plain")
response = token(self.req)
self.assertEqual(expected.content.split('.')[0], response.content.split('.')[0])
......@@ -1443,3 +1443,28 @@ def change_email_settings(request):
track.views.server_track(request, "change-email-settings", {"receive_emails": "no", "course": course_id}, page='dashboard')
return HttpResponse(json.dumps({'success': True}))
from student.firebase_token_generator import create_token
@login_required
def token(request):
'''
Return a token for the backend of annotations.
It uses the course id to retrieve a variable that contains the secret
token found in inheritance.py. It also contains information of when
the token was issued. This will be stored with the user along with
the id for identification purposes in the backend.
'''
course_id = request.GET.get("course_id")
course = course_from_id(course_id)
dtnow = datetime.datetime.now()
dtutcnow = datetime.datetime.utcnow()
delta = dtnow - dtutcnow
newhour, newmin = divmod((delta.days * 24 * 60 * 60 + delta.seconds + 30) // 60, 60)
newtime = "%s%+02d:%02d" % (dtnow.isoformat(), newhour, newmin)
secret = course.annotation_token_secret
custom_data = {"issuedAt": newtime, "consumerKey": secret, "userId": request.user.email, "ttl": 86400}
newtoken = create_token(secret, custom_data)
response = HttpResponse(newtoken, mimetype="text/plain")
return response
......@@ -33,6 +33,8 @@ XMODULES = [
"wrapper = xmodule.wrapper_module:WrapperDescriptor",
"graphical_slider_tool = xmodule.gst_module:GraphicalSliderToolDescriptor",
"annotatable = xmodule.annotatable_module:AnnotatableDescriptor",
"textannotation = xmodule.textannotation_module:TextAnnotationDescriptor",
"videoannotation = xmodule.videoannotation_module:VideoAnnotationDescriptor",
"foldit = xmodule.foldit_module:FolditDescriptor",
"word_cloud = xmodule.word_cloud_module:WordCloudDescriptor",
"hidden = xmodule.hidden_module:HiddenDescriptor",
......
......@@ -41,6 +41,8 @@ class InheritanceMixin(XBlockMixin):
scope=Scope.settings,
)
xqa_key = String(help="DO NOT USE", scope=Scope.settings)
annotation_storage_url = String(help="Location of Annotation backend", scope=Scope.settings, default="http://your_annotation_storage.com", display_name="Url for Annotation Storage")
annotation_token_secret = String(help="Secret string for annotation storage", scope=Scope.settings, default="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", display_name="Secret Token String for Annotation")
graceperiod = Timedelta(
help="Amount of time after the due date that submissions will be accepted",
scope=Scope.settings,
......
# -*- coding: utf-8 -*-
"Test for Annotation Xmodule functional logic."
import unittest
from mock import Mock
from lxml import etree
from xblock.field_data import DictFieldData
from xblock.fields import ScopeIds
from xmodule.textannotation_module import TextAnnotationModule
from . import get_test_system
class TextAnnotationModuleTestCase(unittest.TestCase):
''' text Annotation Module Test Case '''
sample_xml = '''
<annotatable>
<instructions><p>Test Instructions.</p></instructions>
<p>
One Fish. Two Fish.
Red Fish. Blue Fish.
Oh the places you'll go!
</p>
</annotatable>
'''
def setUp(self):
"""
Makes sure that the Module is declared and mocked with the sample xml above.
"""
self.mod = TextAnnotationModule(
Mock(),
get_test_system(),
DictFieldData({'data': self.sample_xml}),
ScopeIds(None, None, None, None)
)
def test_render_content(self):
"""
Tests to make sure the sample xml is rendered and that it forms a valid xmltree
that does not contain a display_name.
"""
content = self.mod._render_content() # pylint: disable=W0212
self.assertIsNotNone(content)
element = etree.fromstring(content)
self.assertIsNotNone(element)
self.assertFalse('display_name' in element.attrib, "Display Name should have been deleted from Content")
def test_extract_instructions(self):
"""
Tests to make sure that the instructions are correctly pulled from the sample xml above.
It also makes sure that if no instructions exist, that it does in fact return nothing.
"""
xmltree = etree.fromstring(self.sample_xml)
expected_xml = u"<div><p>Test Instructions.</p></div>"
actual_xml = self.mod._extract_instructions(xmltree) # pylint: disable=W0212
self.assertIsNotNone(actual_xml)
self.assertEqual(expected_xml.strip(), actual_xml.strip())
xmltree = etree.fromstring('<annotatable>foo</annotatable>')
actual = self.mod._extract_instructions(xmltree) # pylint: disable=W0212
self.assertIsNone(actual)
def test_get_html(self):
"""
Tests the function that passes in all the information in the context that will be used in templates/textannotation.html
"""
context = self.mod.get_html()
for key in ['display_name', 'tag', 'source', 'instructions_html', 'content_html', 'annotation_storage']:
self.assertIn(key, context)
# -*- coding: utf-8 -*-
"Test for Annotation Xmodule functional logic."
import unittest
from mock import Mock
from lxml import etree
from xblock.field_data import DictFieldData
from xblock.fields import ScopeIds
from xmodule.videoannotation_module import VideoAnnotationModule
from . import get_test_system
class VideoAnnotationModuleTestCase(unittest.TestCase):
''' Video Annotation Module Test Case '''
sample_xml = '''
<annotatable>
<instructions><p>Video Test Instructions.</p></instructions>
</annotatable>
'''
sample_sourceurl = "http://video-js.zencoder.com/oceans-clip.mp4"
sample_youtubeurl = "http://www.youtube.com/watch?v=yxLIu-scR9Y"
def setUp(self):
"""
Makes sure that the Video Annotation Module is created.
"""
self.mod = VideoAnnotationModule(
Mock(),
get_test_system(),
DictFieldData({'data': self.sample_xml, 'sourceUrl': self.sample_sourceurl}),
ScopeIds(None, None, None, None)
)
def test_annotation_class_attr_default(self):
"""
Makes sure that it can detect annotation values in text-form if user
decides to add text to the area below video, video functionality is completely
found in javascript.
"""
xml = '<annotation title="x" body="y" problem="0">test</annotation>'
element = etree.fromstring(xml)
expected_attr = {'class': {'value': 'annotatable-span highlight'}}
actual_attr = self.mod._get_annotation_class_attr(element) # pylint: disable=W0212
self.assertIsInstance(actual_attr, dict)
self.assertDictEqual(expected_attr, actual_attr)
def test_annotation_class_attr_with_valid_highlight(self):
"""
Same as above but more specific to an area that is highlightable in the appropriate
color designated.
"""
xml = '<annotation title="x" body="y" problem="0" highlight="{highlight}">test</annotation>'
for color in self.mod.highlight_colors:
element = etree.fromstring(xml.format(highlight=color))
value = 'annotatable-span highlight highlight-{highlight}'.format(highlight=color)
expected_attr = {'class': {
'value': value,
'_delete': 'highlight'}
}
actual_attr = self.mod._get_annotation_class_attr(element) # pylint: disable=W0212
self.assertIsInstance(actual_attr, dict)
self.assertDictEqual(expected_attr, actual_attr)
def test_annotation_class_attr_with_invalid_highlight(self):
"""
Same as above, but checked with invalid colors.
"""
xml = '<annotation title="x" body="y" problem="0" highlight="{highlight}">test</annotation>'
for invalid_color in ['rainbow', 'blink', 'invisible', '', None]:
element = etree.fromstring(xml.format(highlight=invalid_color))
expected_attr = {'class': {
'value': 'annotatable-span highlight',
'_delete': 'highlight'}
}
actual_attr = self.mod._get_annotation_class_attr(element) # pylint: disable=W0212
self.assertIsInstance(actual_attr, dict)
self.assertDictEqual(expected_attr, actual_attr)
def test_annotation_data_attr(self):
"""
Test that each highlight contains the data information from the annotation itself.
"""
element = etree.fromstring('<annotation title="bar" body="foo" problem="0">test</annotation>')
expected_attr = {
'data-comment-body': {'value': 'foo', '_delete': 'body'},
'data-comment-title': {'value': 'bar', '_delete': 'title'},
'data-problem-id': {'value': '0', '_delete': 'problem'}
}
actual_attr = self.mod._get_annotation_data_attr(element) # pylint: disable=W0212
self.assertIsInstance(actual_attr, dict)
self.assertDictEqual(expected_attr, actual_attr)
def test_render_annotation(self):
"""
Tests to make sure that the spans designating annotations acutally visually render as annotations.
"""
expected_html = '<span class="annotatable-span highlight highlight-yellow" data-comment-title="x" data-comment-body="y" data-problem-id="0">z</span>'
expected_el = etree.fromstring(expected_html)
actual_el = etree.fromstring('<annotation title="x" body="y" problem="0" highlight="yellow">z</annotation>')
self.mod._render_annotation(actual_el) # pylint: disable=W0212
self.assertEqual(expected_el.tag, actual_el.tag)
self.assertEqual(expected_el.text, actual_el.text)
self.assertDictEqual(dict(expected_el.attrib), dict(actual_el.attrib))
def test_render_content(self):
"""
Like above, but using the entire text, it makes sure that display_name is removed and that there is only one
div encompassing the annotatable area.
"""
content = self.mod._render_content() # pylint: disable=W0212
element = etree.fromstring(content)
self.assertIsNotNone(element)
self.assertEqual('div', element.tag, 'root tag is a div')
self.assertFalse('display_name' in element.attrib, "Display Name should have been deleted from Content")
def test_extract_instructions(self):
"""
This test ensures that if an instruction exists it is pulled and
formatted from the <instructions> tags. Otherwise, it should return nothing.
"""
xmltree = etree.fromstring(self.sample_xml)
expected_xml = u"<div><p>Video Test Instructions.</p></div>"
actual_xml = self.mod._extract_instructions(xmltree) # pylint: disable=W0212
self.assertIsNotNone(actual_xml)
self.assertEqual(expected_xml.strip(), actual_xml.strip())
xmltree = etree.fromstring('<annotatable>foo</annotatable>')
actual = self.mod._extract_instructions(xmltree) # pylint: disable=W0212
self.assertIsNone(actual)
def test_get_extension(self):
"""
Tests the function that returns the appropriate extension depending on whether it is
a video from youtube, or one uploaded to the EdX server.
"""
expectedyoutube = 'video/youtube'
expectednotyoutube = 'video/mp4'
result1 = self.mod._get_extension(self.sample_sourceurl) # pylint: disable=W0212
result2 = self.mod._get_extension(self.sample_youtubeurl) # pylint: disable=W0212
self.assertEqual(expectedyoutube, result2)
self.assertEqual(expectednotyoutube, result1)
def test_get_html(self):
"""
Tests to make sure variables passed in truly exist within the html once it is all rendered.
"""
context = self.mod.get_html()
for key in ['display_name', 'content_html', 'instructions_html', 'sourceUrl', 'typeSource', 'poster', 'alert', 'annotation_storage']:
self.assertIn(key, context)
''' Text annotation module '''
from lxml import etree
from pkg_resources import resource_string
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from xblock.core import Scope, String
import textwrap
class AnnotatableFields(object):
"""Fields for `TextModule` and `TextDescriptor`."""
data = String(help="XML data for the annotation", scope=Scope.content, default=textwrap.dedent("""\
<annotatable>
<instructions>
<p>
Add the instructions to the assignment here.
</p>
</instructions>
<p>
Lorem ipsum dolor sit amet, at amet animal petentium nec. Id augue nemore postulant mea. Ex eam dicant noluisse expetenda, alia admodum abhorreant qui et. An ceteros expetenda mea, tale natum ipsum quo no, ut pro paulo alienum noluisse.
</p>
</annotatable>
"""))
display_name = String(
display_name="Display Name",
help="Display name for this module",
scope=Scope.settings,
default='Text Annotation',
)
tags = String(
display_name="Tags for Assignments",
help="Add tags that automatically highlight in a certain color using the comma-separated form, i.e. imagery:red,parallelism:blue",
scope=Scope.settings,
default='imagery:red,parallelism:blue',
)
source = String(
display_name="Source/Citation",
help="Optional for citing source of any material used. Automatic citation can be done using <a href=\"http://easybib.com\">EasyBib</a>",
scope=Scope.settings,
default='None',
)
annotation_storage_url = String(help="Location of Annotation backend", scope=Scope.settings, default="http://your_annotation_storage.com", display_name="Url for Annotation Storage")
class TextAnnotationModule(AnnotatableFields, XModule):
''' Text Annotation Module '''
js = {'coffee': [],
'js': []}
css = {'scss': [resource_string(__name__, 'css/annotatable/display.scss')]}
icon_class = 'textannotation'
def __init__(self, *args, **kwargs):
super(TextAnnotationModule, self).__init__(*args, **kwargs)
xmltree = etree.fromstring(self.data)
self.instructions = self._extract_instructions(xmltree)
self.content = etree.tostring(xmltree, encoding='unicode')
self.highlight_colors = ['yellow', 'orange', 'purple', 'blue', 'green']
def _render_content(self):
""" Renders annotatable content with annotation spans and returns HTML. """
xmltree = etree.fromstring(self.content)
if 'display_name' in xmltree.attrib:
del xmltree.attrib['display_name']
return etree.tostring(xmltree, encoding='unicode')
def _extract_instructions(self, xmltree):
""" Removes <instructions> from the xmltree and returns them as a string, otherwise None. """
instructions = xmltree.find('instructions')
if instructions is not None:
instructions.tag = 'div'
xmltree.remove(instructions)
return etree.tostring(instructions, encoding='unicode')
return None
def get_html(self):
""" Renders parameters to template. """
context = {
'display_name': self.display_name_with_default,
'tag': self.tags,
'source': self.source,
'instructions_html': self.instructions,
'content_html': self._render_content(),
'annotation_storage': self.annotation_storage_url
}
return self.system.render_template('textannotation.html', context)
class TextAnnotationDescriptor(AnnotatableFields, RawDescriptor):
''' Text Annotation Descriptor '''
module_class = TextAnnotationModule
mako_template = "widgets/raw-edit.html"
@property
def non_editable_metadata_fields(self):
non_editable_fields = super(TextAnnotationDescriptor, self).non_editable_metadata_fields
non_editable_fields.extend([
TextAnnotationDescriptor.annotation_storage_url
])
return non_editable_fields
"""
Module for Video annotations using annotator.
"""
from lxml import etree
from pkg_resources import resource_string
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from xblock.core import Scope, String
import textwrap
class AnnotatableFields(object):
""" Fields for `VideoModule` and `VideoDescriptor`. """
data = String(help="XML data for the annotation", scope=Scope.content, default=textwrap.dedent("""\
<annotatable>
<instructions>
<p>
Add the instructions to the assignment here.
</p>
</instructions>
</annotatable>
"""))
display_name = String(
display_name="Display Name",
help="Display name for this module",
scope=Scope.settings,
default='Video Annotation',
)
sourceurl = String(help="The external source URL for the video.", display_name="Source URL", scope=Scope.settings, default="http://video-js.zencoder.com/oceans-clip.mp4")
poster_url = String(help="Poster Image URL", display_name="Poster URL", scope=Scope.settings, default="")
annotation_storage_url = String(help="Location of Annotation backend", scope=Scope.settings, default="http://your_annotation_storage.com", display_name="Url for Annotation Storage")
class VideoAnnotationModule(AnnotatableFields, XModule):
'''Video Annotation Module'''
js = {'coffee': [resource_string(__name__, 'js/src/javascript_loader.coffee'),
resource_string(__name__, 'js/src/collapsible.coffee'),
resource_string(__name__, 'js/src/html/display.coffee'),
resource_string(__name__, 'js/src/annotatable/display.coffee')
],
'js': []}
css = {'scss': [resource_string(__name__, 'css/annotatable/display.scss')]}
icon_class = 'videoannotation'
def __init__(self, *args, **kwargs):
super(VideoAnnotationModule, self).__init__(*args, **kwargs)
xmltree = etree.fromstring(self.data)
self.instructions = self._extract_instructions(xmltree)
self.content = etree.tostring(xmltree, encoding='unicode')
self.highlight_colors = ['yellow', 'orange', 'purple', 'blue', 'green']
def _get_annotation_class_attr(self, element):
""" Returns a dict with the CSS class attribute to set on the annotation
and an XML key to delete from the element.
"""
attr = {}
cls = ['annotatable-span', 'highlight']
highlight_key = 'highlight'
color = element.get(highlight_key)
if color is not None:
if color in self.highlight_colors:
cls.append('highlight-' + color)
attr['_delete'] = highlight_key
attr['value'] = ' '.join(cls)
return {'class': attr}
def _get_annotation_data_attr(self, element):
""" Returns a dict in which the keys are the HTML data attributes
to set on the annotation element. Each data attribute has a
corresponding 'value' and (optional) '_delete' key to specify
an XML attribute to delete.
"""
data_attrs = {}
attrs_map = {
'body': 'data-comment-body',
'title': 'data-comment-title',
'problem': 'data-problem-id'
}
for xml_key in attrs_map.keys():
if xml_key in element.attrib:
value = element.get(xml_key, '')
html_key = attrs_map[xml_key]
data_attrs[html_key] = {'value': value, '_delete': xml_key}
return data_attrs
def _render_annotation(self, element):
""" Renders an annotation element for HTML output. """
attr = {}
attr.update(self._get_annotation_class_attr(element))
attr.update(self._get_annotation_data_attr(element))
element.tag = 'span'
for key in attr.keys():
element.set(key, attr[key]['value'])
if '_delete' in attr[key] and attr[key]['_delete'] is not None:
delete_key = attr[key]['_delete']
del element.attrib[delete_key]
def _render_content(self):
""" Renders annotatable content with annotation spans and returns HTML. """
xmltree = etree.fromstring(self.content)
xmltree.tag = 'div'
if 'display_name' in xmltree.attrib:
del xmltree.attrib['display_name']
for element in xmltree.findall('.//annotation'):
self._render_annotation(element)
return etree.tostring(xmltree, encoding='unicode')
def _extract_instructions(self, xmltree):
""" Removes <instructions> from the xmltree and returns them as a string, otherwise None. """
instructions = xmltree.find('instructions')
if instructions is not None:
instructions.tag = 'div'
xmltree.remove(instructions)
return etree.tostring(instructions, encoding='unicode')
return None
def _get_extension(self, srcurl):
''' get the extension of a given url '''
if 'youtu' in srcurl:
return 'video/youtube'
else:
spliturl = srcurl.split(".")
extensionplus1 = spliturl[len(spliturl) - 1]
spliturl = extensionplus1.split("?")
extensionplus2 = spliturl[0]
spliturl = extensionplus2.split("#")
return 'video/' + spliturl[0]
def get_html(self):
""" Renders parameters to template. """
extension = self._get_extension(self.sourceurl)
context = {
'display_name': self.display_name_with_default,
'instructions_html': self.instructions,
'sourceUrl': self.sourceurl,
'typeSource': extension,
'poster': self.poster_url,
'alert': self,
'content_html': self._render_content(),
'annotation_storage': self.annotation_storage_url
}
return self.system.render_template('videoannotation.html', context)
class VideoAnnotationDescriptor(AnnotatableFields, RawDescriptor):
''' Video annotation descriptor '''
module_class = VideoAnnotationModule
mako_template = "widgets/raw-edit.html"
@property
def non_editable_metadata_fields(self):
non_editable_fields = super(VideoAnnotationDescriptor, self).non_editable_metadata_fields
non_editable_fields.extend([
VideoAnnotationDescriptor.annotation_storage_url
])
return non_editable_fields
/*This is written to fix some design problems with edX*/
.annotator-wrapper .annotator-adder button {
opacity:0;
}
.annotator-editor a, .annotator-filter .annotator-filter-property label{
line-height: 24px !important;
color: #363636 !important;
font-size: 12px!important;
font-weight: bold !important;
text-shadow: none !important;
}
.annotator-outer ul {
list-style: none !important;
padding-left: 0em !important;
}
.annotator-outer li {
margin-bottom: 0em!important;
}
.vjs-rangeslider-holder span.vjs-time-text{
line-height: 1!important;
float: left;
}
span .annotator-hl{
font:inherit;
}
.vjs-has-started .vjs-loading-spinner {
display: none!important;
}
/*Catch*/
#mainCatch *{
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
/*PublicPrivate Notes in My notes */
.notes-wrapper .PublicPrivate.separator,
.notes-wrapper .PublicPrivate.myNotes{
position:relative;
float:left;
}
.notes-wrapper .PublicPrivate.active *{
color:black;
}
/* My notes buttons */
.notes-wrapper .buttonCatch{
-moz-box-shadow:inset 0px 1px 0px 0px #ffffff;
-webkit-box-shadow:inset 0px 1px 0px 0px #ffffff;
box-shadow:inset 0px 1px 0px 0px #ffffff;
background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #ffffff), color-stop(1, #a6a3a3));
background:-moz-linear-gradient(top, #ffffff 5%, #a6a3a3 100%);
background:-webkit-linear-gradient(top, #ffffff 5%, #a6a3a3 100%);
background:-o-linear-gradient(top, #ffffff 5%, #a6a3a3 100%);
background:-ms-linear-gradient(top, #ffffff 5%, #a6a3a3 100%);
background:linear-gradient(to bottom, #ffffff 5%, #a6a3a3 100%);
filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#a6a3a3',GradientType=0);
background-color:#ffffff;
-moz-border-radius:6px;
-webkit-border-radius:6px;
border-radius:6px;
border:1px solid #c2c2c2;
display:inline-block;
color:#302f2f;
font-family:arial;
font-size:15px;
font-weight:bold;
padding:6px 24px;
text-decoration:none;
text-shadow:0px 1px 0px #ffffff;
margin: 0px 5px 10px 5px;
cursor:pointer;
}
.notes-wrapper .buttonCatch.active{
color:red;
}
.notes-wrapper .buttonCatch:hover {
background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #a6a3a3), color-stop(1, #ffffff));
background:-moz-linear-gradient(top, #a6a3a3 5%, #ffffff 100%);
background:-webkit-linear-gradient(top, #a6a3a3 5%, #ffffff 100%);
background:-o-linear-gradient(top, #a6a3a3 5%, #ffffff 100%);
background:-ms-linear-gradient(top, #a6a3a3 5%, #ffffff 100%);
background:linear-gradient(to bottom, #a6a3a3 5%, #ffffff 100%);
filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#a6a3a3', endColorstr='#ffffff',GradientType=0);
background-color:#a6a3a3;
}
.notes-wrapper .buttonCatch:active {
position:relative;
top:1px;
}
.annotatable-content #sourceCitation {
color:#CCC;
font-style:italic;
font-size:12px;
}
.flag-icon{
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAsCAMAAAAgsQpJAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAytpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDE0IDc5LjE1MTQ4MSwgMjAxMy8wMy8xMy0xMjowOToxNSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NDQ1NzExMTM2MEUwMTFFM0I2RjhDOENEOEZGRUY1MzQiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NDQ1NzExMTI2MEUwMTFFM0I2RjhDOENEOEZGRUY1MzQiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIChNYWNpbnRvc2gpIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5kaWQ6YWNmNmE5ZGQtZDlkOC00YTlhLThmMjAtY2EzNGM5ODcxZDJhIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOmFjZjZhOWRkLWQ5ZDgtNGE5YS04ZjIwLWNhMzRjOTg3MWQyYSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pnen528AAAAJUExURc3Nzf///////3aZ7dEAAAADdFJOU///ANfKDUEAAACzSURBVHjarNRLFoJADETRZ+1/0cpAYuevx8w4XOjQdApZ8SpVxQ+QJYRW4hzV+3GO4rk3hFNCWMG765a7NEhbBtlBxtK86peQNbws/y+teoz/ofiWFbSTtHLTHt0TErddqfMHNxxGWzUbj8SlUIdL5trBc9Br6AKh7PHh5pXc1XM9uQiT9lJYOQc//mGXj51LgrTIPpbOBWmTuizaC/moOZr79k44uDtIJ3cmriao2ekpwADT/geMPTQb1AAAAABJRU5ErkJggg==");
background-repeat: no-repeat;
background-size:13px 13px;
top: 2px;
left: 5px;
float: left;
border: none;
width: 13px;
height: 13px;
cursor: pointer;
position: absolute;
}
.flag-icon-used, .flag-icon:hover{
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAsCAMAAAAgsQpJAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAytpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDE0IDc5LjE1MTQ4MSwgMjAxMy8wMy8xMy0xMjowOToxNSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NDQ1NzExMEY2MEUwMTFFM0I2RjhDOENEOEZGRUY1MzQiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NDQ1NzExMEU2MEUwMTFFM0I2RjhDOENEOEZGRUY1MzQiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIChNYWNpbnRvc2gpIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5kaWQ6YWNmNmE5ZGQtZDlkOC00YTlhLThmMjAtY2EzNGM5ODcxZDJhIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOmFjZjZhOWRkLWQ5ZDgtNGE5YS04ZjIwLWNhMzRjOTg3MWQyYSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PpBey0IAAAAVUExURQAAAAEBAQ4ODrGxscHBwfj4+P///6napTIAAAAHdFJOU////////wAaSwNGAAABW0lEQVR42oyVWQLDIAhEBwZy/yNX3EpSo+WrSV+GVcT1NVf1682Qfquq/QNSAP4BEhC8+56gAUFOSVKVC9BUg4Sm7zQnh5GwVMWhwngQTR4w9KSRUkWIZuWlZZBNr5NKxbTpHcPP/Cscfp8gHKAT2URE8iO6c7jiYK0QUNlj0vuKu6c1auZFEUdOQ/UcY3FZuqm4eOKiO0Xtug4x1qaG4l6ydsZYwS1ZueLde6+TguXs2CafY3p8TI/SyyjN0rL5szy4LEPD1lZNaRjRO7M4HtbaGkCpcp+zFRiSZS4tPpkjvgTjYATHOY2vIOTBLcFyMki32yFcgZQQ8ju3AFnLZxIl3YFtU9nPGsIv56H6s9fwKHVwkbTv9mMPrNC07SJtgcXcHxZp4wDbL1KNBOKo2XaRek/jdd8jcy/h3fcjH0OwBqsSd5dCAy247S3TwYhQd9dWjnHVjWwfAQYAR+4aJLhjvXgAAAAASUVORK5CYII=");
background-repeat: no-repeat;
background-size:13px 13px;
top: 2px;
left: 5px;
float: left;
border: none;
width: 13px;
height: 13px;
cursor: pointer;
position: absolute;
}
/*Range Slider Bar Time*/
.vjs-default-skin .vjs-timebar-RS{
color: red;
top: -1em;
height: 100%;
position: relative;
background: rgba(100,100,100,.5);/*Quitar*/
}
/*Selection Range Slider Bar Selected*/
.vjs-default-skin .vjs-rangeslider-holder{height: 100%;}
.vjs-default-skin .vjs-selectionbar-RS{
height: 100%;
float: left;
width: 100%;
left: 0em;
right: 0em;
position:absolute;
background-color: #FFE800;
background: #FFE800;
background: -moz-linear-gradient(top, #FFE800, #A69700);
background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#FFE800), to(#A69700));
background: -webkit-linear-gradient(top, #FFE800, #A69700);
background: -o-linear-gradient(top, #FFE800, #A69700);
background: -ms-linear-gradient(top, #FFE800, #A69700);
background: linear-gradient(top, #FFE800, #A69700);
opacity: 0.8;
}
.vjs-default-skin div.vjs-rangeslider-holder.locked > div.vjs-selectionbar-RS {
background-color: #FF6565;
background: #FF6565;
background: -moz-linear-gradient(top, #FF6565, #300000);
background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#FF6565), to(#300000));
background: -webkit-linear-gradient(top, #FF6565, #300000);
background: -o-linear-gradient(top, #FF6565, #300000);
background: -ms-linear-gradient(top, #FF6565, #300000);
background: linear-gradient(top, #FF6565, #300000);
}
/*Arrow and Handle*/
.vjs-default-skin div.vjs-rangeslider-handle {
position: absolute;
margin-top: 0;
cursor: pointer !important;
background-color: transparent;
}
.vjs-default-skin .vjs-selectionbar-left-RS{height: 100%;left: 0;z-index:10}
.vjs-default-skin .vjs-selectionbar-right-RS{height: 100%;left: 100%;z-index:20}
.vjs-default-skin div.vjs-selectionbar-left-RS,
.vjs-default-skin div.vjs-selectionbar-right-RS {
top: 0em;
position: absolute;
width:0em;
}
.vjs-default-skin div.vjs-selectionbar-arrow-RS {
width: 0;
height: 0;
border-left: 1em solid transparent;
border-right: 1em solid transparent;
border-top: 1em solid #FFF273;
margin-left: -1em;
opacity: 0.8;
position: absolute;
top: -1em;
}
.vjs-default-skin div.vjs-rangeslider-handle.active > div.vjs-selectionbar-arrow-RS {
border-top-color: #5F5FB3;
}
.vjs-default-skin div.vjs-rangeslider-holder.locked .vjs-rangeslider-handle > div.vjs-selectionbar-arrow-RS {
border-top-color: #FF6565;
}
.vjs-default-skin div.vjs-selectionbar-line-RS {
width: 1px;
height: 1em;
background-color: #FFF273;
position:absolute;
top: 0em;
}
.vjs-default-skin div.vjs-rangeslider-handle.active > div.vjs-selectionbar-line-RS {
background-color: #5F5FB3;
}
.vjs-default-skin div.vjs-rangeslider-holder.locked .vjs-rangeslider-handle > div.vjs-selectionbar-line-RS {
background-color: #FF6565;
}
/* Time Panel */
.vjs-default-skin .vjs-timepanel-RS{
width: 100%;
height: 1em;
font-weight: bold;
font-size: 15px;
top: -2em;
position: absolute;
visibility:visible;
opacity:1;
transition-delay:0s;
}
.vjs-default-skin .vjs-timepanel-RS.disable{
visibility:hidden;
opacity:0;
-webkit-transition: visibility 1s linear 1s,opacity 1s linear;
-moz-transition: visibility 1s linear 1s,opacity 1s linear;
-o-transition: visibility 1s linear 1s,opacity 1s linear;
transition:visibility 1s linear 1s,opacity 1s linear;
}
.vjs-default-skin .vjs-timepanel-left-RS,
.vjs-default-skin .vjs-timepanel-right-RS{
font-weight: normal;
font-size: 1em;
color: #666666;
border: 1px solid #666666;
background-color: white;
border-radius: 5px;
position: absolute;
height:116%;
padding-right: 0.3em;
padding-left: 0.3em;
}
.vjs-default-skin .vjs-timepanel-left-RS{
left:0.5%
}
.vjs-default-skin .vjs-timepanel-right-RS{
left:92%
}
/* Control Time Panel */
.vjs-default-skin .vjs-controltimepanel-RS{
width: 18em;
font-size: 1em;
line-height: 3em;
}
.vjs-default-skin .vjs-controltimepanel-RS input{
width: 1.5em;
background: rgba(102, 168, 204, 0.16);
border: 1px solid transparent;
color: black;
font-size: 1em;
margin-left: 2px;
text-align: center;
color: white;
}
.vjs-default-skin .vjs-controltimepanel-left-RS{
width: 50%;
float: left;
}
.vjs-default-skin .vjs-controltimepanel-right-RS{
float:right;
width: 48%;
}
.vjs-default-skin .vjs-controltimepanel-RS input{
margin: 0;
padding: 0;
display: table-cell;
}
/* ---------------- Video-js plugin ---------------- */
.vjs-default-skin *, .vjs-default-skin *:before, .vjs-default-skin *:after {
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
margin: 0;
padding: 0;
}
.annotator-viewer div:first-of-type.richText-annotation *,
.annotator-viewer div:first-of-type.richText-annotation{
font-style: normal;
font-weight: inherit;
text-align: inherit;
}
.annotator-viewer div:first-of-type.richText-annotation{
padding-top: 22px;
}
/* Fix in the tinymce */
.annotator-viewer div:first-of-type.richText-annotation strong{
font-weight: bold;
}
.annotator-viewer div:first-of-type.richText-annotation em{
font-style: italic;
}
.mce-container {
z-index:3000000000!important; /*To fix full-screen problems*/
}
/* Some change in the design of Annotator */
.annotator-editor .annotator-widget{
min-width: 400px;
}
/*Rubric icon*/
.mce-ico.mce-i-rubric{
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAYAAAA71pVKAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QkBBB07nraNoQAAAhZJREFUKM+NkstrE1EUxr+5c08ykztpJtVoazHBF8FgQQzonyBKEZS6FrQKLl0EXBRT0ZULJSs3oii4TyHgu90IlTaL6qouWlv7Ck1N0BSnmZk714WbPHz07M4534+Pw3eAHdTY8A9+Nd9/bshU1DpnO4HXjh2ZY2J9/OSTxHTrnP8PvJYf+BDQ6qEDaQBB43jrTusUFy4oPjsYWYzF+VS91nxLYfdhKgONaQT3W/KMxr1XY5e+qj86f8zsKYYsZ6AvjWFzA8ORHkAnwN8So7evzL/8pzMAXL/Hq8mMv1up371T7Z+/c3n9cKeuDS6Xy6dN07zLuZ56Onk2Ed2/ANJsnE/PQMpgyffle+kYzwazB1+3waVS6X48Hr9BRPB9H57nYXplFKeSt8D1Hriug9XKF0x+Lmw+ys8m2m42DOOn4zhQSsGyLOi6jqONm9isbmFVFlDbaGKx8QaB1rvdlbNhGLAsC0IIGIYBIQSy2ROQ0oOp7wOPraHXEugRvDtnzjmi0SiICEIIEBGklAB9B6cmbG0AUnrY5m73h+m6DsYYTNMEYwxEBMY0hGNVhHkcZigBO9qHlDHS7cwYg23bAIBQKAQigud7IH0XwtxDoHwEIQ9SLKx0wa7rPiaivYyxESklXNeFBg0mjyNQTQSuATMSm6ipuYt//eVcLhdeXl5+UKlUlur1upqamVAv3j3/VCyOD3VqfwF6uLp3q+vMcgAAAABJRU5ErkJggg==');
background-repeat: no-repeat;
}
/* Editor */
li.token-input-token {
overflow: hidden;
height: auto !important;
margin: 3px;
padding: 1px 3px;
background-color: #eff2f7;
color: #000;
cursor: default;
border: 1px solid #ccd5e4;
font-size: 11px;
border-radius: 5px;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
display: inline;
white-space: nowrap;
}
li.token-input-token p {
display: inline;
padding: 0;
margin: 0;
}
li.token-input-token span {
color: #a6b3cf;
margin-left: 5px;
font-weight: bold;
cursor: pointer;
}
li.token-input-selected-token {
background-color: #5670a6;
border: 1px solid #3b5998;
color: #fff;
}
li.token-input-input-token {
margin: 0;
padding: 0;
list-style-type: none;
}
div.token-input-dropdown {
position: absolute;
width: 120px;
background-color: #fff;
overflow: hidden;
border-left: 1px solid #ccc;
border-right: 1px solid #ccc;
border-bottom: 1px solid #ccc;
cursor: default;
font-size: 11px;
font-family: Verdana;
z-index: 1000000000;
}
div.token-input-dropdown p {
margin: 0;
padding: 5px;
font-weight: bold;
color: #777;
}
div.token-input-dropdown ul {
margin: 0;
padding: 0;
}
div.token-input-dropdown ul li {
background-color: #fff;
padding: 3px;
margin: 0;
list-style-type: none;
}
div.token-input-dropdown ul li.token-input-dropdown-item {
background-color: #fff;
}
div.token-input-dropdown ul li.token-input-dropdown-item2 {
background-color: #fff;
}
div.token-input-dropdown ul li em {
font-weight: bold;
font-style: normal;
}
div.token-input-dropdown ul li.token-input-selected-dropdown-item {
background-color: #3b5998;
color: #fff;
}
.token-input-list{
padding:0px;
}
\ No newline at end of file
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>
This is a custom SVG font generated by IcoMoon.
<iconset grid="16"></iconset>
</metadata>
<defs>
<font id="VideoJS" horiz-adv-x="512" >
<font-face units-per-em="512" ascent="480" descent="-32" />
<missing-glyph horiz-adv-x="512" />
<glyph class="hidden" unicode="&#xf000;" d="M0,480L 512 -32L0 -32 z" horiz-adv-x="0" />
<glyph unicode="&#xe002;" d="M 64,416L 224,416L 224,32L 64,32zM 288,416L 448,416L 448,32L 288,32z" />
<glyph unicode="&#xe003;" d="M 200.666,440.666 C 213.5,453.5 224,449.15 224,431 L 224,17 C 224-1.15 213.5-5.499 200.666,7.335 L 80,128 L 0,128 L 0,320 L 80,320 L 200.666,440.666 Z" />
<glyph unicode="&#xe004;" d="M 274.51,109.49c-6.143,0-12.284,2.343-16.971,7.029c-9.373,9.373-9.373,24.568,0,33.941
c 40.55,40.55, 40.55,106.529,0,147.078c-9.373,9.373-9.373,24.569,0,33.941c 9.373,9.372, 24.568,9.372, 33.941,0
c 59.265-59.265, 59.265-155.696,0-214.961C 286.794,111.833, 280.652,109.49, 274.51,109.49zM 200.666,440.666 C 213.5,453.5 224,449.15 224,431 L 224,17 C 224-1.15 213.5-5.499 200.666,7.335 L 80,128 L 0,128 L 0,320 L 80,320 L 200.666,440.666 Z" />
<glyph unicode="&#xe005;" d="M 359.765,64.235c-6.143,0-12.284,2.343-16.971,7.029c-9.372,9.372-9.372,24.568,0,33.941
c 65.503,65.503, 65.503,172.085,0,237.588c-9.372,9.373-9.372,24.569,0,33.941c 9.372,9.371, 24.569,9.372, 33.941,0
C 417.532,335.938, 440,281.696, 440,224c0-57.695-22.468-111.938-63.265-152.735C 372.049,66.578, 365.907,64.235, 359.765,64.235zM 274.51,109.49c-6.143,0-12.284,2.343-16.971,7.029c-9.373,9.373-9.373,24.568,0,33.941
c 40.55,40.55, 40.55,106.529,0,147.078c-9.373,9.373-9.373,24.569,0,33.941c 9.373,9.372, 24.568,9.372, 33.941,0
c 59.265-59.265, 59.265-155.696,0-214.961C 286.794,111.833, 280.652,109.49, 274.51,109.49zM 200.666,440.666 C 213.5,453.5 224,449.15 224,431 L 224,17 C 224-1.15 213.5-5.499 200.666,7.335 L 80,128 L 0,128 L 0,320 L 80,320 L 200.666,440.666 Z" />
<glyph unicode="&#xe006;" d="M 445.020,18.98c-6.143,0-12.284,2.343-16.971,7.029c-9.372,9.373-9.372,24.568,0,33.941
C 471.868,103.771, 496.001,162.030, 496.001,224c0,61.969-24.133,120.229-67.952,164.049c-9.372,9.373-9.372,24.569,0,33.941
c 9.372,9.372, 24.569,9.372, 33.941,0c 52.885-52.886, 82.011-123.2, 82.011-197.99c0-74.791-29.126-145.104-82.011-197.99
C 457.304,21.323, 451.162,18.98, 445.020,18.98zM 359.765,64.235c-6.143,0-12.284,2.343-16.971,7.029c-9.372,9.372-9.372,24.568,0,33.941
c 65.503,65.503, 65.503,172.085,0,237.588c-9.372,9.373-9.372,24.569,0,33.941c 9.372,9.371, 24.569,9.372, 33.941,0
C 417.532,335.938, 440,281.696, 440,224c0-57.695-22.468-111.938-63.265-152.735C 372.049,66.578, 365.907,64.235, 359.765,64.235zM 274.51,109.49c-6.143,0-12.284,2.343-16.971,7.029c-9.373,9.373-9.373,24.568,0,33.941
c 40.55,40.55, 40.55,106.529,0,147.078c-9.373,9.373-9.373,24.569,0,33.941c 9.373,9.372, 24.568,9.372, 33.941,0
c 59.265-59.265, 59.265-155.696,0-214.961C 286.794,111.833, 280.652,109.49, 274.51,109.49zM 200.666,440.666 C 213.5,453.5 224,449.15 224,431 L 224,17 C 224-1.15 213.5-5.499 200.666,7.335 L 80,128 L 0,128 L 0,320 L 80,320 L 200.666,440.666 Z" horiz-adv-x="544" />
<glyph unicode="&#xe007;" d="M 256,480L 96,224L 256-32L 416,224 z" />
<glyph unicode="&#xe008;" d="M 0,480 L 687.158,480 L 687.158-35.207 L 0-35.207 L 0,480 z M 622.731,224.638 C 621.878,314.664 618.46,353.922 597.131,381.656 C 593.291,387.629 586.038,391.042 580.065,395.304 C 559.158,410.669 460.593,416.211 346.247,416.211 C 231.896,416.211 128.642,410.669 108.162,395.304 C 101.762,391.042 94.504,387.629 90.242,381.656 C 69.331,353.922 66.349,314.664 65.069,224.638 C 66.349,134.607 69.331,95.353 90.242,67.62 C 94.504,61.22 101.762,58.233 108.162,53.967 C 128.642,38.18 231.896,33.060 346.247,32.207 C 460.593,33.060 559.158,38.18 580.065,53.967 C 586.038,58.233 593.291,61.22 597.131,67.62 C 618.46,95.353 621.878,134.607 622.731,224.638 z M 331.179,247.952 C 325.389,318.401 287.924,359.905 220.901,359.905 C 159.672,359.905 111.54,304.689 111.54,215.965 C 111.54,126.859 155.405,71.267 227.907,71.267 C 285.79,71.267 326.306,113.916 332.701,184.742 L 263.55,184.742 C 260.81,158.468 249.843,138.285 226.69,138.285 C 190.136,138.285 183.435,174.462 183.435,212.92 C 183.435,265.854 198.665,292.886 223.951,292.886 C 246.492,292.886 260.81,276.511 262.939,247.952 L 331.179,247.952 z M 570.013,247.952 C 564.228,318.401 526.758,359.905 459.74,359.905 C 398.507,359.905 350.379,304.689 350.379,215.965 C 350.379,126.859 394.244,71.267 466.746,71.267 C 524.625,71.267 565.14,113.916 571.536,184.742 L 502.384,184.742 C 499.649,158.468 488.682,138.285 465.529,138.285 C 428.971,138.285 422.27,174.462 422.27,212.92 C 422.27,265.854 437.504,292.886 462.785,292.886 C 485.327,292.886 499.649,276.511 501.778,247.952 L 570.013,247.952 z " horiz-adv-x="687.158" />
<glyph unicode="&#xe009;" d="M 64,416L 448,416L 448,32L 64,32z" />
<glyph unicode="&#xe00a;" d="M 192,416A64,64 12780 1 1 320,416A64,64 12780 1 1 192,416zM 327.765,359.765A64,64 12780 1 1 455.765,359.765A64,64 12780 1 1 327.765,359.765zM 416,224A32,32 12780 1 1 480,224A32,32 12780 1 1 416,224zM 359.765,88.235A32,32 12780 1 1 423.765,88.23500000000001A32,32 12780 1 1 359.765,88.23500000000001zM 224.001,32A32,32 12780 1 1 288.001,32A32,32 12780 1 1 224.001,32zM 88.236,88.235A32,32 12780 1 1 152.236,88.23500000000001A32,32 12780 1 1 88.236,88.23500000000001zM 72.236,359.765A48,48 12780 1 1 168.236,359.765A48,48 12780 1 1 72.236,359.765zM 28,224A36,36 12780 1 1 100,224A36,36 12780 1 1 28,224z" />
<glyph unicode="&#xe00b;" d="M 224,192 L 224-16 L 144,64 L 48-32 L 0,16 L 96,112 L 16,192 ZM 512,432 L 416,336 L 496,256 L 288,256 L 288,464 L 368,384 L 464,480 Z" />
<glyph unicode="&#xe00c;" d="M 256,448 C 397.385,448 512,354.875 512,240 C 512,125.124 397.385,32 256,32 C 242.422,32 229.095,32.867 216.088,34.522 C 161.099-20.467 95.463-30.328 32-31.776 L 32-18.318 C 66.268-1.529 96,29.052 96,64 C 96,68.877 95.621,73.665 94.918,78.348 C 37.020,116.48 0,174.725 0,240 C 0,354.875 114.615,448 256,448 Z" />
<glyph unicode="&#xe00d;" d="M 256,480C 114.615,480,0,365.385,0,224s 114.615-256, 256-256s 256,114.615, 256,256S 397.385,480, 256,480z M 256,352
c 70.692,0, 128-57.308, 128-128s-57.308-128-128-128s-128,57.308-128,128S 185.308,352, 256,352z M 408.735,71.265
C 367.938,30.468, 313.695,8, 256,8c-57.696,0-111.938,22.468-152.735,63.265C 62.468,112.062, 40,166.304, 40,224
c0,57.695, 22.468,111.938, 63.265,152.735l 33.941-33.941c0,0,0,0,0,0c-65.503-65.503-65.503-172.085,0-237.588
C 168.937,73.475, 211.125,56, 256,56c 44.874,0, 87.062,17.475, 118.794,49.206c 65.503,65.503, 65.503,172.084,0,237.588l 33.941,33.941
C 449.532,335.938, 472,281.695, 472,224C 472,166.304, 449.532,112.062, 408.735,71.265z" />
<glyph unicode="&#xe01e;" d="M 512,224c-0.639,33.431-7.892,66.758-21.288,97.231c-13.352,30.5-32.731,58.129-56.521,80.96
c-23.776,22.848-51.972,40.91-82.492,52.826C 321.197,466.979, 288.401,472.693, 256,472c-32.405-0.641-64.666-7.687-94.167-20.678
c-29.524-12.948-56.271-31.735-78.367-54.788c-22.112-23.041-39.58-50.354-51.093-79.899C 20.816,287.104, 15.309,255.375, 16,224
c 0.643-31.38, 7.482-62.574, 20.067-91.103c 12.544-28.55, 30.738-54.414, 53.055-75.774c 22.305-21.377, 48.736-38.252, 77.307-49.36
C 194.988-3.389, 225.652-8.688, 256-8c 30.354,0.645, 60.481,7.277, 88.038,19.457c 27.575,12.141, 52.558,29.74, 73.183,51.322
c 20.641,21.57, 36.922,47.118, 47.627,74.715c 6.517,16.729, 10.94,34.2, 13.271,51.899c 0.623-0.036, 1.249-0.060, 1.881-0.060
c 17.673,0, 32,14.326, 32,32c0,0.898-0.047,1.786-0.119,2.666L 512,223.999 z M 461.153,139.026c-11.736-26.601-28.742-50.7-49.589-70.59
c-20.835-19.905-45.5-35.593-72.122-45.895C 312.828,12.202, 284.297,7.315, 256,8c-28.302,0.649-56.298,6.868-81.91,18.237
c-25.625,11.333-48.842,27.745-67.997,47.856c-19.169,20.099-34.264,43.882-44.161,69.529C 51.997,169.264, 47.318,196.729, 48,224
c 0.651,27.276, 6.664,54.206, 17.627,78.845c 10.929,24.65, 26.749,46.985, 46.123,65.405c 19.365,18.434, 42.265,32.935, 66.937,42.428
C 203.356,420.208, 229.755,424.681, 256,424c 26.25-0.653, 52.114-6.459, 75.781-17.017c 23.676-10.525, 45.128-25.751, 62.812-44.391
c 17.698-18.629, 31.605-40.647, 40.695-64.344C 444.412,274.552, 448.679,249.219, 448,224l 0.119,0 c-0.072-0.88-0.119-1.768-0.119-2.666
c0-16.506, 12.496-30.087, 28.543-31.812C 473.431,172.111, 468.278,155.113, 461.153,139.026z" />
<glyph unicode="&#xe01f;" d="M 256,480 C 116.626,480 3.271,368.619 0.076,230.013 C 3.036,350.945 94.992,448 208,448 C 322.875,448 416,347.712 416,224 C 416,197.49 437.49,176 464,176 C 490.51,176 512,197.49 512,224 C 512,365.385 397.385,480 256,480 ZM 256-32 C 395.374-32 508.729,79.381 511.924,217.987 C 508.964,97.055 417.008,0 304,0 C 189.125,0 96,100.288 96,224 C 96,250.51 74.51,272 48,272 C 21.49,272 0,250.51 0,224 C 0,82.615 114.615-32 256-32 Z" />
<glyph unicode="&#xe00e;" d="M 432,128c-22.58,0-42.96-9.369-57.506-24.415L 158.992,211.336C 159.649,215.462, 160,219.689, 160,224
s-0.351,8.538-1.008,12.663l 215.502,107.751C 389.040,329.369, 409.42,320, 432,320c 44.183,0, 80,35.817, 80,80S 476.183,480, 432,480
s-80-35.817-80-80c0-4.311, 0.352-8.538, 1.008-12.663L 137.506,279.585C 122.96,294.63, 102.58,304, 80,304c-44.183,0-80-35.818-80-80
c0-44.184, 35.817-80, 80-80c 22.58,0, 42.96,9.369, 57.506,24.414l 215.502-107.751C 352.352,56.538, 352,52.311, 352,48
c0-44.184, 35.817-80, 80-80s 80,35.816, 80,80C 512,92.182, 476.183,128, 432,128z" />
<glyph unicode="&#xe001;" d="M 96,416L 416,224L 96,32 z" />
<glyph unicode="&#xe000;" d="M 512,480 L 512,272 L 432,352 L 336,256 L 288,304 L 384,400 L 304,480 ZM 224,144 L 128,48 L 208-32 L 0-32 L 0,176 L 80,96 L 176,192 Z" />
<glyph unicode="&#x20;" horiz-adv-x="256" />
</font></defs></svg>
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
// Generated by CoffeeScript 1.6.3
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.Flagging = (function(_super) {
__extends(Flagging, _super);
//If you do not have options, delete next line and the parameters in the declaration function
Flagging.prototype.options = null;
//declaration function, remember to set up submit and/or update as necessary, if you don't have
//options, delete the options line below.
function Flagging(element,options) {
this.updateViewer = __bind(this.updateViewer, this);
this.flagAnnotation = __bind(this.flagAnnotation, this);
this.unflagAnnotation = __bind(this.unflagAnnotation, this);
this.options = options;
_ref = Flagging.__super__.constructor.apply(this, arguments);
return _ref;
}
//example variables to be used to receive input in the annotator view
Flagging.prototype.field = null;
Flagging.prototype.input = null;
Flagging.prototype.hasPressed = false;
//this function will initialize the plug in. Create your fields here in the editor and viewer.
Flagging.prototype.pluginInit = function() {
console.log("Flagging-pluginInit");
//Check that annotator is working
if (!Annotator.supported()) {
return;
}
//-- Viewer
var newview = this.annotator.viewer.addField({
load: this.updateViewer,
});
return this.input = $(this.field).find(':input');
};
//The following allows you to edit the annotation popup when the viewer has already
//hit submit and is just viewing the annotation.
Flagging.prototype.updateViewer = function(field, annotation) {
$(field).remove();//remove the empty div created by annotator
var self = this;
this.hasPressed = false;
//perform routine to check if user has pressed the button before
var tags = typeof annotation.tags != 'undefined'?annotation.tags:[];
var user = this.annotator.plugins.Permissions.user.id;
tags.forEach(function(t){
if (t.indexOf("flagged")>=0) {
var usertest = t.replace('flagged-','');
if (usertest == user)
self.hasPressed = true;
}
});
var fieldControl = $(this.annotator.viewer.element.find('.annotator-controls')).parent();
if (this.hasPressed) {
fieldControl.prepend('<button title="You have already reported this annotation." class="flag-icon-used">');
var flagEl = fieldControl.find('.flag-icon-used'),
self = this;
flagEl.click(function(){self.unflagAnnotation(annotation,user,flagEl,field)});
} else{
fieldControl.prepend('<button title="Report annotation as inappropriate or offensive." class="flag-icon">');
var flagEl = fieldControl.find('.flag-icon'),
self = this;
flagEl.click(function(){self.flagAnnotation(annotation,user,flagEl,field)});
}
}
Flagging.prototype.flagAnnotation = function(annotation, userId, flagElement, field) {
flagElement.attr("class","flag-icon-used");
flagElement.attr("title","You have already reported this annotation.");
if (typeof annotation.tags == 'undefined') {
annotation.tags = ['flagged-'+userId];
} else{
annotation.tags.push("flagged-"+userId);
}
this.annotator.plugins['Store'].annotationUpdated(annotation);
this.annotator.publish("flaggedAnnotation",[field,annotation]);
}
Flagging.prototype.unflagAnnotation = function(annotation, userId, flagElement, field) {
flagElement.attr("class", "flag-icon");
flagElement.attr("title","Report annotation as inappropriate or offensive.");
annotation.tags.splice(annotation.tags.indexOf('flagged-'+userId));
this.annotator.plugins['Store'].annotationUpdated(annotation);
this.annotator.publish("flaggedAnnotation",[field,annotation]);
}
return Flagging;
})(Annotator.Plugin);
\ No newline at end of file
/**
* jQuery Watch Plugin
*
* @author Darcy Clarke
* @version 2.0
*
* Copyright (c) 2012 Darcy Clarke
* Dual licensed under the MIT and GPL licenses.
*
* ADDS:
*
* - $.watch()
*
* USES:
*
* - DOMAttrModified event
*
* FALLBACKS:
*
* - propertychange event
* - setTimeout() with delay
*
* EXAMPLE:
*
* $('div').watch('width height', function(){
* console.log(this.style.width, this.style.height);
* });
*
* $('div').animate({width:'100px',height:'200px'}, 500);
*
*/
(function($){
$.extend($.fn, {
/**
* Watch Method
*
* @param {String} the name of the properties to watch
* @param {Object} options to overide defaults (only 'throttle' right now)
* @param {Function} callback function to be executed when attributes change
*
* @return {jQuery Object} returns the jQuery object for chainability
*/
watch : function(props, options, callback){
// Dummmy element
var element = document.createElement('div');
/**
* Checks Support for Event
*
* @param {String} the name of the event
* @param {Element Object} the element to test support against
*
* @return {Boolean} returns result of test (true/false)
*/
var isEventSupported = function(eventName, el) {
eventName = 'on' + eventName;
var supported = (eventName in el);
if(!supported){
el.setAttribute(eventName, 'return;');
supported = typeof el[eventName] == 'function';
}
return supported;
};
// Type check options
if(typeof(options) == 'function'){
callback = options;
options = {};
}
// Type check callback
if(typeof(callback) != 'function')
callback = function(){};
// Map options over defaults
options = $.extend({}, { throttle : 10 }, options);
/**
* Checks if properties have changed
*
* @param {Element Object} the element to watch
*
*/
var check = function(el) {
var data = el.data(),
changed = false,
temp;
// Loop through properties
var length = typeof data!='undefined' && typeof data.props!='undefined'?data.props.length:0;
for(var i=0;i < length; i++){
temp = el.css(data.props[i]);
if(data.vals[i] != temp){
data.vals[i] = temp;
changed = true;
break;
}
}
// Run callback if property has changed
if(changed && data.cb)
data.cb.call(el, data);
};
return this.each(function(){
var el = $(this),
cb = function(){ check.call(this, el) },
data = { props:props.split(','), cb:callback, vals: [] };
$.each(data.props, function(i){ data.vals[i] = el.css(data.props[i]); });
el.data(data);
if(isEventSupported('DOMAttrModified', element)){
el.on('DOMAttrModified', callback);
} else if(isEventSupported('propertychange', element)){
el.on('propertychange', callback);
} else {
setInterval(cb, options.throttle);
}
});
}
});
})(jQuery);
/*
Reply Annotator Plugin v1.0 (https://github.com/danielcebrian/reply-annotator)
Copyright (C) 2014 Daniel Cebri‡n Robles
License: https://github.com/danielcebrian/reply-annotator/blob/master/License.rst
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// Generated by CoffeeScript 1.6.3
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.Reply = (function(_super) {
__extends(Reply, _super);
function Reply() {
this.pluginSubmit = __bind(this.pluginSubmit, this);
this.updateField = __bind(this.updateField, this);
_ref = Reply.__super__.constructor.apply(this, arguments);
return _ref;
}
Reply.prototype.field = null;
Reply.prototype.input = null;
Reply.prototype.pluginInit = function() {
console.log("Reply-pluginInit");
//Check that annotator is working
if (!Annotator.supported()) {
return;
}
//-- Editor
this.field = this.annotator.editor.addField({
type: 'input', //options (textarea,input,select,checkbox)
load: this.updateField,
submit: this.pluginSubmit,
});
var newfield = Annotator.$('<li class="annotator-item reply-item" style="display:none"><span class="parent-annotation">0</span></li>');//reply-item is the parent value
Annotator.$(this.field).replaceWith(newfield);
this.field=newfield[0];
//-- Viewer
var newview = this.annotator.viewer.addField({
load: this.updateViewer,
});
return this.input = $(this.field).find(':input');
};
// New JSON for the database
Reply.prototype.pluginSubmit = function(field, annotation) {
var replyItem = $(this.annotator.editor.element).find(".reply-item span.parent-annotation"),
parent = replyItem.html()!=''?replyItem.html():'0';
console.log(parent);
console.log(replyItem.html());
if (parent!='0') annotation.media = 'comment';
annotation.parent = parent;//set 0, because it is not a reply
console.log(annotation.parent);
return annotation.parent;
};
Reply.prototype.updateViewer = function(field, annotation) {
var self = this,
field = $(field),
ret = field.addClass('reply-viewer-annotator').html(function() {
var string;
return self;
});
this.annotation = annotation;
//Create the actions for the buttons
return ret;
};
Reply.prototype.updateField = function(field, annotation) {
//reset parent value
var replyItem = $(this.annotator.editor.element).find(".reply-item span.parent-annotation");
return replyItem.html('0');
};
return Reply;
})(Annotator.Plugin);
/*
Rich Text Annotator Plugin v1.0 (https://github.com/danielcebrian/richText-annotator)
Copyright (C) 2014 Daniel Cebrin Robles
License: https://github.com/danielcebrian/richText-annotator/blob/master/License.rst
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// Generated by CoffeeScript 1.6.3
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.RichText = (function(_super) {
__extends(RichText, _super);
//Default tinymce configuration
RichText.prototype.options = {
tinymce:{
selector: "li.annotator-item textarea",
plugins: "media image insertdatetime link code",
menubar: false,
toolbar_items_size: 'small',
extended_valid_elements : "iframe[src|frameborder|style|scrolling|class|width|height|name|align|id]",
toolbar: "insertfile undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image media rubric | code ",
}
};
function RichText(element,options) {
_ref = RichText.__super__.constructor.apply(this, arguments);
return _ref;
};
RichText.prototype.pluginInit = function() {
console.log("RichText-pluginInit");
var annotator = this.annotator,
editor = this.annotator.editor;
//Check that annotator is working
if (!Annotator.supported()) {
return;
}
//Editor Setup
annotator.editor.addField({
type: 'input',
load: this.updateEditor,
});
//Viewer setup
annotator.viewer.addField({
load: this.updateViewer,
});
annotator.subscribe("annotationEditorShown", function(){
$(annotator.editor.element).find('.mce-tinymce')[0].style.display='block';
$(annotator.editor.element).find('.mce-container').css('z-index',3000000000);
annotator.editor.checkOrientation();
});
annotator.subscribe("annotationEditorHidden", function(){
$(annotator.editor.element).find('.mce-tinymce')[0].style.display='none';
});
//set listener for tinymce;
this.options.tinymce.setup = function(ed) {
ed.on('change', function(e) {
//set the modification in the textarea of annotator
$(editor.element).find('textarea')[0].value = tinymce.activeEditor.getContent();
});
ed.on('Init', function(ed){
$('.mce-container').css('z-index','3090000000000000000');
});
//New button to add Rubrics of the url https://gteavirtual.org/rubric
ed.addButton('rubric', {
icon: 'rubric',
title : 'Insert a rubric',
onclick: function() {
ed.windowManager.open({
title: 'Insert a public rubric of the webside https://gteavirtual.org/rubric ',
body: [
{type: 'textbox', name: 'url', label: 'Url'}
],
onsubmit: function(e) {
// Insert content when the window form is submitted
var url = e.data.url,
name = 'irb',
irb;
//get the variable 'name' from the given url
name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]");
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
results = regex.exec(url);
//the rubric id
irb = results == null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
if (irb==''){
ed.windowManager.alert('Error: The given webpage didn\'t have a irb variable in the url');
}else{
var iframeRubric = "<iframe src='https://gteavirtual.org/rubric/?mod=portal&scr=viewrb&evt=frame&irb="+irb+"' style='width:800px;height:600px;overflow-y: scroll;background:transparent' frameborder='0' ></iframe>";
ed.setContent(ed.getContent()+iframeRubric);
$(editor.element).find('textarea')[0].value = ed.getContent();
}
}
});
ed.insertContent('Main button');
ed.label = 'My Button';
}
});
};
tinymce.init(this.options.tinymce);
};
RichText.prototype.updateEditor = function(field, annotation) {
var text = typeof annotation.text!='undefined'?annotation.text:'';
tinymce.activeEditor.setContent(text);
$(field).remove(); //this is the auto create field by annotator and it is not necessary
}
RichText.prototype.updateViewer = function(field, annotation) {
var textDiv = $(field.parentNode).find('div:first-of-type')[0];
textDiv.innerHTML =annotation.text;
$(textDiv).addClass('richText-annotation');
$(field).remove(); //this is the auto create field by annotator and it is not necessary
}
return RichText;
})(Annotator.Plugin);
.mce-object{border:1px dotted #3a3a3a;background:#d5d5d5 url(img/object.gif) no-repeat center}.mce-pagebreak{cursor:default;display:block;border:0;width:100%;height:5px;border:1px dashed #666;margin-top:15px}.mce-item-anchor{cursor:default;display:inline-block;-webkit-user-select:all;-webkit-user-modify:read-only;-moz-user-select:all;-moz-user-modify:read-only;user-select:all;user-modify:read-only;width:9px!important;height:9px!important;border:1px dotted #3a3a3a;background:#d5d5d5 url(img/anchor.gif) no-repeat center}.mce-nbsp{background:#AAA}hr{cursor:default}.mce-match-marker{background:green;color:#fff}.mce-spellchecker-word{background:url(img/wline.gif) repeat-x bottom left;cursor:default}.mce-item-table,.mce-item-table td,.mce-item-table th,.mce-item-table caption{border:1px dashed #BBB}td.mce-item-selected,th.mce-item-selected{background-color:#39f!important}.mce-edit-focus{outline:1px dotted #333}
\ No newline at end of file
body{background-color:#fff;font-family:Verdana,Arial,Helvetica,sans-serif;font-size:11px;scrollbar-3dlight-color:#f0f0ee;scrollbar-arrow-color:#676662;scrollbar-base-color:#f0f0ee;scrollbar-darkshadow-color:#ddd;scrollbar-face-color:#e0e0dd;scrollbar-highlight-color:#f0f0ee;scrollbar-shadow-color:#f0f0ee;scrollbar-track-color:#f5f5f5}td,th{font-family:Verdana,Arial,Helvetica,sans-serif;font-size:11px}.mce-object{border:1px dotted #3a3a3a;background:#d5d5d5 url(img/object.gif) no-repeat center}.mce-pagebreak{cursor:default;display:block;border:0;width:100%;height:5px;border:1px dashed #666;margin-top:15px}.mce-item-anchor{cursor:default;display:inline-block;-webkit-user-select:all;-webkit-user-modify:read-only;-moz-user-select:all;-moz-user-modify:read-only;user-select:all;user-modify:read-only;width:9px!important;height:9px!important;border:1px dotted #3a3a3a;background:#d5d5d5 url(img/anchor.gif) no-repeat center}.mce-nbsp{background:#AAA}hr{cursor:default}.mce-match-marker{background:green;color:#fff}.mce-spellchecker-word{background:url(img/wline.gif) repeat-x bottom left;cursor:default}.mce-item-table,.mce-item-table td,.mce-item-table th,.mce-item-table caption{border:1px dashed #BBB}td.mce-item-selected,th.mce-item-selected{background-color:#39f!important}.mce-edit-focus{outline:1px dotted #333}
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -15,9 +15,14 @@ def notes(request, course_id):
raise Http404
notes = Note.objects.filter(course_id=course_id, user=request.user).order_by('-created', 'uri')
student = request.user
storage = course.annotation_storage_url
context = {
'course': course,
'notes': notes
'notes': notes,
'student': student,
'storage': storage
}
return render_to_response('notes.html', context)
......@@ -130,6 +130,7 @@ def html_index(request, course_id, book_index, chapter=None):
for entry in textbook['chapters']:
entry['url'] = remap_static_url(entry['url'], course)
student = request.user
return render_to_response(
'static_htmlbook.html',
{
......@@ -137,6 +138,7 @@ def html_index(request, course_id, book_index, chapter=None):
'course': course,
'textbook': textbook,
'chapter': chapter,
'student': student,
'staff_access': staff_access,
'notes_enabled': notes_enabled,
},
......
......@@ -683,15 +683,26 @@ 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'
'js/vendor/ova/annotator-full.js',
'js/vendor/ova/video.dev.js',
'js/vendor/ova/vjs.youtube.js',
'js/vendor/ova/rangeslider.js',
'js/vendor/ova/share-annotator.js',
'js/vendor/ova/tinymce.min.js',
'js/vendor/ova/richText-annotator.js',
'js/vendor/ova/reply-annotator.js',
'js/vendor/ova/tags-annotator.js',
'js/vendor/ova/flagging-annotator.js',
'js/vendor/ova/jquery-Watch.js',
'js/vendor/ova/ova.js',
'js/vendor/ova/catch/js/catch.js',
'js/vendor/ova/catch/js/handlebars-1.1.2.js'
]
discussion_js = sorted(rooted_glob(COMMON_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 = ['coffee/src/notes.js']
notes_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/notes/**/*.js'))
instructor_dash_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/instructor_dashboard/**/*.js'))
PIPELINE_CSS = {
......@@ -701,6 +712,16 @@ PIPELINE_CSS = {
'css/vendor/jquery.qtip.min.css',
'css/vendor/responsive-carousel/responsive-carousel.css',
'css/vendor/responsive-carousel/responsive-carousel.slide.css',
'css/vendor/ova/edx-annotator.css',
'css/vendor/ova/annotator.css',
'css/vendor/ova/video-js.min.css',
'css/vendor/ova/rangeslider.css',
'css/vendor/ova/share-annotator.css',
'css/vendor/ova/richText-annotator.css',
'css/vendor/ova/tags-annotator.css',
'css/vendor/ova/flagging-annotator.css',
'css/vendor/ova/ova.css',
'js/vendor/ova/catch/css/main.css'
],
'output_filename': 'css/lms-style-vendor.css',
},
......@@ -728,7 +749,6 @@ PIPELINE_CSS = {
'js/vendor/CodeMirror/codemirror.css',
'css/vendor/jquery.treeview.css',
'css/vendor/ui-lightness/jquery-ui-1.8.22.custom.css',
'css/vendor/annotator.min.css',
],
'output_filename': 'css/lms-style-course-vendor.css',
},
......
......@@ -19,22 +19,74 @@ class StudentNotes
storeConfig = @getStoreConfig uri
found = @targets.some (target) -> target is event.target
# Get uri
unless uri.substring(0, 4) is "http"
uri_root = (window.location.href.split(/#|\?/).shift() or "")
uri = uri_root + uri.substring(1)
parts = window.location.href.split("/")
courseid = parts[4] + "/" + parts[5] + "/" + parts[6]
# Get id and name user
idUdiv = $(event.target).parent().find(".idU")[0]
idDUdiv = $(event.target).parent().find(".idDU")[0]
idUdiv = (if typeof idUdiv isnt "undefined" then idUdiv.innerHTML else "")
idDUdiv = (if typeof idDUdiv isnt "undefined" then idDUdiv.innerHTML else "")
options =
optionsAnnotator:
permissions:
user:
id: idUdiv
name: idDUdiv
userString: (user) ->
return user.name if user and user.name
user
userId: (user) ->
return user.id if user and user.id
user
auth:
tokenUrl: location.protocol+'//'+location.host+"/token?course_id="+courseid
store:
prefix: 'http://catch.aws.af.cm/annotator'
annotationData: uri:uri
urls:
create: '/create',
read: '/read/:id',
update: '/update/:id',
destroy: '/delete/:id',
search: '/search'
loadFromSearch:
limit:10000
uri: uri
user:idUdiv
optionsVideoJS: techOrder: ["html5","flash","youtube"],customControlsOnMobile: true
optionsOVA:
posBigNew:'none'
NumAnnotations:20
optionsRichText:
tinymce:
selector: "li.annotator-item textarea"
plugins: "media image insertdatetime link code"
menubar: false
toolbar_items_size: 'small'
extended_valid_elements : "iframe[src|frameborder|style|scrolling|class|width|height|name|align|id]"
toolbar: "insertfile undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image media rubric | code "
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
$(event.target).annotator "destroy" unless Annotator._instances.length is 0
ova = new OpenVideoAnnotation.Annotator($(event.target), options)
else
$(event.target).annotator()
.annotator('addPlugin', 'Tags')
.annotator('addPlugin', 'Store', storeConfig)
@targets.push(event.target)
if event.target.id is "annotator-viewer"
ova = new OpenVideoAnnotation.Annotator($(event.target), options)
else
@targets.push(event.target)
# Returns a JSON config object that can be passed to the annotator Store plugin
getStoreConfig: (uri) ->
......
......@@ -59,24 +59,140 @@
<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: {tags}").format(tags=note.tags) | h}</li>
% endif
<li class="user">${_('Author: {username}').format(username=note.user.username)}</li>
<li class="time">${_('Created: {datetime}').format(datetime=note.created.strftime('%m/%d/%Y %H:%m'))}</li>
<li class="uri">${_('Source: {link}').format(link='<a href="{url}">{url}</a>'.format(url=note.uri))}</li>
</ul>
</div>
% endfor
% if notes is UNDEFINED or len(notes) == 0:
<p>${_('You do not have any notes.')}</p>
% endif
<h1>${_('My Notes')}</h1>
<div id="notesHolder"></div>
<section id="catchDIV">
<div class="annotationListContainer">${_('You do not have any notes.')}</div>
</section>
<script>
//Grab uri of the course
var parts = window.location.href.split("/"),
uri = '',
courseid;
for (var index = 0; index <= 6; index += 1) uri += parts[index]+"/"; //Get the unit url
courseid = parts[4] + "/" + parts[5] + "/" + parts[6];
var pagination = 100,
is_staff = false,
options = {
optionsAnnotator: {
permissions:{
user: {
id:"${student.email}",
name:"${student.username}"
},
userString: function (user) {
if (user && user.name)
return user.name;
return user;
},
userId: function (user) {
if (user && user.id)
return user.id;
return user;
},
permissions: {
'read': [],
'update': ["${student.email}"],
'delete': ["${student.email}"],
'admin': ["${student.email}"]
},
showViewPermissionsCheckbox: true,
showEditPermissionsCheckbox: false,
userAuthorize: function(action, annotation, user) {
var token, tokens, _i, _len;
if (annotation.permissions) {
tokens = annotation.permissions[action] || [];
if (is_staff){
return true;
}
if (tokens.length === 0) {
return true;
}
for (_i = 0, _len = tokens.length; _i < _len; _i++) {
token = tokens[_i];
if (this.userId(user) === token) {
return true;
}
}
return false;
} else if (annotation.user) {
if (user) {
return this.userId(user) === this.userId(annotation.user);
} else {
return false;
}
}
return true;
},
},
auth: {
tokenUrl: location.protocol+'//'+location.host+"/token?course_id="+courseid
},
store: {
// The endpoint of the store on your server.
prefix: "${storage}",
annotationData: {},
urls: {
// These are the default URLs.
create: '/create',
read: '/read/:id',
update: '/update/:id',
destroy: '/delete/:id',
search: '/search'
},
loadFromSearch:{
limit:pagination,
offset:0,
uri:uri
}
},
},
optionsVideoJS: {techOrder: ["html5","flash","youtube"]},
optionsRS: {},
optionsOVA: {posBigNew:'none'},
optionsRichText: {
tinymce:{
selector: "li.annotator-item textarea",
plugins: "media image insertdatetime link code",
menubar: false,
toolbar_items_size: 'small',
extended_valid_elements : "iframe[src|frameborder|style|scrolling|class|width|height|name|align|id]",
toolbar: "insertfile undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image media rubric | code ",
}
},
};
var imgURLRoot = "${settings.STATIC_URL}" + "js/vendor/ova/catch/img/";
//remove old instances
if (Annotator._instances.length !== 0) {
$('#notesHolder').annotator("destroy");
}
delete ova;
//Load the plugin Video/Text Annotation
var ova = new OpenVideoAnnotation.Annotator($('#notesHolder'),options);
//Catch
var annotator = ova.annotator,
catchOptions = {
media:'text',
externalLink:true,
imageUrlRoot:imgURLRoot,
showMediaSelector: true,
showPublicPrivate: true,
pagination:pagination,//Number of Annotations per load in the pagination,
flags:is_staff
},
Catch = new CatchAnnotation($('#catchDIV'),catchOptions);
</script>
</div>
</section>
......@@ -146,7 +146,8 @@
<div id="bookpage" />
</section>
</section>
<span class="idU" style="display:none">${student.id}</span>
<span class="idDU" style="display:none">${student.username}</span>
</div>
</div>
<%! from django.utils.translation import ugettext as _ %>
<div class="annotatable-wrapper">
<div class="annotatable-header">
% if display_name is not UNDEFINED and display_name is not None:
<div class="annotatable-title">${display_name}</div>
% endif
</div>
% if instructions_html is not UNDEFINED and instructions_html is not None:
<div class="annotatable-section shaded">
<div class="annotatable-section-title">
${_('Instructions')}
<a class="annotatable-toggle annotatable-toggle-instructions expanded" href="javascript:void(0)">${_('Collapse Instructions')}</a>
</div>
<div class="annotatable-section-body annotatable-instructions">
${instructions_html}
</div>
</div>
% endif
<div class="annotatable-section">
<div class="annotatable-content">
<div id="textHolder">${content_html}</div>
<div id="sourceCitation">${_('Source:')} ${source}</div>
<div id="catchDIV">
<div class="annotationListContainer">${_('You do not have any notes.')}</div>
</div>
</div>
</div>
</div>
<script>
function onClickHideInstructions(){
//Reset function if there is more than one event handler
$(this).off();
$(this).on('click',onClickHideInstructions);
var hide = $(this).html()=='Collapse Instructions'?true:false,
cls, txt,slideMethod;
txt = (hide ? 'Expand' : 'Collapse') + ' Instructions';
cls = (hide ? ['expanded', 'collapsed'] : ['collapsed', 'expanded']);
slideMethod = (hide ? 'slideUp' : 'slideDown');
$(this).text(txt).removeClass(cls[0]).addClass(cls[1]);
$(this).parents('.annotatable-section:first').find('.annotatable-instructions')[slideMethod]();
}
$('.annotatable-toggle-instructions').on('click', onClickHideInstructions);
//Grab uri of the course
var parts = window.location.href.split("/"),
uri = '',
courseid;
for (var index = 0; index <= 9; index += 1) uri += parts[index]+"/"; //Get the unit url
courseid = parts[4] + "/" + parts[5] + "/" + parts[6];
//Change uri in cms
var lms_location = $('.sidebar .preview-button').attr('href');
if (typeof lms_location!='undefined'){
courseid = parts[4].split(".").join("/");
uri = window.location.protocol;
for (var index = 0; index <= 9; index += 1) uri += lms_location.split("/")[index]+"/"; //Get the unit url
}
var pagination = 100,
is_staff = !('${user.is_staff}'=='False'),
options = {
optionsAnnotator: {
permissions:{
user: {
id:"${user.email}",
name:"${user.username}"
},
userString: function (user) {
if (user && user.name)
return user.name;
return user;
},
userId: function (user) {
if (user && user.id)
return user.id;
return user;
},
permissions: {
'read': [],
'update': ["${user.email}"],
'delete': ["${user.email}"],
'admin': ["${user.email}"]
},
showViewPermissionsCheckbox: true,
showEditPermissionsCheckbox: false,
userAuthorize: function(action, annotation, user) {
var token, tokens, _i, _len;
if (annotation.permissions) {
tokens = annotation.permissions[action] || [];
if (is_staff){
return true;
}
if (tokens.length === 0) {
return true;
}
for (_i = 0, _len = tokens.length; _i < _len; _i++) {
token = tokens[_i];
if (this.userId(user) === token) {
return true;
}
}
return false;
} else if (annotation.user) {
if (user) {
return this.userId(user) === this.userId(annotation.user);
} else {
return false;
}
}
return true;
},
},
auth: {
tokenUrl: location.protocol+'//'+location.host+"/token?course_id="+courseid
},
store: {
// The endpoint of the store on your server.
prefix: "${annotation_storage}",
annotationData: {
uri: uri,
citation: "${source}"
},
urls: {
// These are the default URLs.
create: '/create',
read: '/read/:id',
update: '/update/:id',
destroy: '/delete/:id',
search: '/search'
},
loadFromSearch:{
limit:pagination,
offset:0,
uri:uri,
media:'text',
userid:'${user.email}',
}
},
highlightTags:{
tag: "${tag}",
}
},
optionsVideoJS: {techOrder: ["html5","flash","youtube"]},
optionsRS: {},
optionsOVA: {posBigNew:'none'},
optionsRichText: {
tinymce:{
selector: "li.annotator-item textarea",
plugins: "media image insertdatetime link code",
menubar: false,
toolbar_items_size: 'small',
extended_valid_elements : "iframe[src|frameborder|style|scrolling|class|width|height|name|align|id]",
toolbar: "insertfile undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image media rubric | code ",
}
},
};
var imgURLRoot = window.location.protocol;
imgURLRoot +="//" + uri.replace(imgURLRoot+"//","").split('/')[0] + "/static/js/vendor/ova/catch/img/";
//remove old instances
if (Annotator._instances.length !== 0) {
$('#textHolder').annotator("destroy");
}
delete ova;
//Load the plugin Video/Text Annotation
var ova = new OpenVideoAnnotation.Annotator($('#textHolder'),options);
//Catch
var annotator = ova.annotator,
catchOptions = {
media:'text',
externalLink:false,
imageUrlRoot:imgURLRoot,
showMediaSelector: false,
showPublicPrivate: true,
userId:'${user.email}',
pagination:pagination,//Number of Annotations per load in the pagination,
flags:is_staff
},
Catch = new CatchAnnotation($('#catchDIV'),catchOptions);
</script>
<%! from django.utils.translation import ugettext as _ %>
<div class="annotatable-wrapper">
<div class="annotatable-header">
% if display_name is not UNDEFINED and display_name is not None:
<div class="annotatable-title">${display_name}</div>
% endif
</div>
% if instructions_html is not UNDEFINED and instructions_html is not None:
<div class="annotatable-section shaded">
<div class="annotatable-section-title">
${_('Instructions')}
<a class="annotatable-toggle annotatable-toggle-instructions expanded" href="javascript:void(0)">${_('Collapse Instructions')}</a>
</div>
<div class="annotatable-section-body annotatable-instructions">
${instructions_html}
</div>
</div>
% endif
<div class="annotatable-section">
<div class="annotatable-content">
<div id="videoHolder">
<video id="vid1" class="video-js vjs-default-skin" controls preload="none" width="640" height="264" poster="${poster}">
<source src="${sourceUrl}" type='${typeSource}' />
</video>
</div>
<div id="catchDIV">
<div class="annotationListContainer">${_('You do not have any notes.')}</div>
</div>
</div>
</div>
</div>
<script>
function onClickHideInstructions(){
//Reset function if there is more than one event handler
$(this).off();
$(this).on('click',onClickHideInstructions);
var hide = $(this).html()=='Collapse Instructions'?true:false,
cls, txt,slideMethod;
txt = (hide ? 'Expand' : 'Collapse') + ' Instructions';
cls = (hide ? ['expanded', 'collapsed'] : ['collapsed', 'expanded']);
slideMethod = (hide ? 'slideUp' : 'slideDown');
$(this).text(txt).removeClass(cls[0]).addClass(cls[1]);
$(this).parents('.annotatable-section:first').find('.annotatable-instructions')[slideMethod]();
}
$('.annotatable-toggle-instructions').on('click', onClickHideInstructions);
//Grab uri of the course
var parts = window.location.href.split("/"),
uri = '',
courseid;
for (var index = 0; index <= 9; index += 1) uri += parts[index]+"/"; //Get the unit url
courseid = parts[4] + "/" + parts[5] + "/" + parts[6];
//Change uri in cms
var lms_location = $('.sidebar .preview-button').attr('href');
if (typeof lms_location!='undefined'){
courseid = parts[4].split(".").join("/");
uri = window.location.protocol;
for (var index = 0; index <= 9; index += 1) uri += lms_location.split("/")[index]+"/"; //Get the unit url
}
var pagination = 100,
is_staff = !('${user.is_staff}'=='False'),
options = {
optionsAnnotator: {
permissions:{
user: {
id:"${user.email}",
name:"${user.username}"
},
userString: function (user) {
if (user && user.name)
return user.name;
return user;
},
userId: function (user) {
if (user && user.id)
return user.id;
return user;
},
permissions: {
'read': [],
'update': ["${user.email}"],
'delete': ["${user.email}"],
'admin': ["${user.email}"]
},
showViewPermissionsCheckbox: true,
showEditPermissionsCheckbox: false,
userAuthorize: function(action, annotation, user) {
var token, tokens, _i, _len;
if (annotation.permissions) {
tokens = annotation.permissions[action] || [];
if (is_staff){
return true;
}
if (tokens.length === 0) {
return true;
}
for (_i = 0, _len = tokens.length; _i < _len; _i++) {
token = tokens[_i];
if (this.userId(user) === token) {
return true;
}
}
return false;
} else if (annotation.user) {
if (user) {
return this.userId(user) === this.userId(annotation.user);
} else {
return false;
}
}
return true;
},
},
auth: {
tokenUrl: location.protocol+'//'+location.host+"/token?course_id="+courseid
},
store: {
// The endpoint of the store on your server.
prefix: "${annotation_storage}",
annotationData: {
uri: uri,
},
urls: {
// These are the default URLs.
create: '/create',
read: '/read/:id',
update: '/update/:id',
destroy: '/delete/:id',
search: '/search'
},
loadFromSearch:{
limit:pagination,
offset:0,
uri:uri,
media:'video',
userid:'${user.email}',
}
},
},
optionsVideoJS: {techOrder: ["html5","flash","youtube"]},
optionsRS: {},
optionsOVA: {posBigNew:'none'},
optionsRichText: {
tinymce:{
selector: "li.annotator-item textarea",
plugins: "media image insertdatetime link code",
menubar: false,
toolbar_items_size: 'small',
extended_valid_elements : "iframe[src|frameborder|style|scrolling|class|width|height|name|align|id]",
toolbar: "insertfile undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image media rubric | code ",
}
},
};
var imgURLRoot = window.location.protocol;
imgURLRoot +="//" + uri.replace(imgURLRoot+"//","").split('/')[0] + "/static/js/vendor/ova/catch/img/";
//remove old instances
if (Annotator._instances.length !== 0) {
$('#videoHolder').annotator("destroy");
}
delete ova;
//Load the plugin Video/Text Annotation
var ova = new OpenVideoAnnotation.Annotator($('#videoHolder'),options);
ova.annotator.addPlugin('Tags');
//Catch
var annotator = ova.annotator,
catchOptions = {
media:'video',
externalLink:false,
imageUrlRoot:imgURLRoot,
showMediaSelector: false,
showPublicPrivate: true,
userId:'${user.email}',
pagination:pagination,//Number of Annotations per load in the pagination,
flags:is_staff
},
Catch = new CatchAnnotation($('#catchDIV'),catchOptions);
</script>
......@@ -15,6 +15,7 @@ urlpatterns = ('', # nopep8
url(r'^update_certificate$', 'certificates.views.update_certificate'),
url(r'^$', 'branding.views.index', name="root"), # Main marketing page, or redirect to courseware
url(r'^dashboard$', 'student.views.dashboard', name="dashboard"),
url(r'^token$', 'student.views.token', name="token"),
url(r'^login$', 'student.views.signin_user', name="signin_user"),
url(r'^register$', 'student.views.register_user', name="register_user"),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment