diff --git a/cms/envs/common.py b/cms/envs/common.py
index 76e0f4d50f..f7195d2c54 100644
--- a/cms/envs/common.py
+++ b/cms/envs/common.py
@@ -318,7 +318,7 @@ PIPELINE_CSS = {
'css/vendor/ui-lightness/jquery-ui-1.8.22.custom.css',
'css/vendor/jquery.qtip.min.css',
'js/vendor/markitup/skins/simple/style.css',
- 'js/vendor/markitup/sets/wiki/style.css',
+ 'js/vendor/markitup/sets/wiki/style.css'
],
'output_filename': 'css/cms-style-vendor.css',
},
diff --git a/common/djangoapps/student/firebase_token_generator.py b/common/djangoapps/student/firebase_token_generator.py
new file mode 100644
index 0000000000..f84a85277e
--- /dev/null
+++ b/common/djangoapps/student/firebase_token_generator.py
@@ -0,0 +1,99 @@
+'''
+ 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)
diff --git a/common/djangoapps/student/tests/test_token_generator.py b/common/djangoapps/student/tests/test_token_generator.py
new file mode 100644
index 0000000000..1eb09c9173
--- /dev/null
+++ b/common/djangoapps/student/tests/test_token_generator.py
@@ -0,0 +1,43 @@
+"""
+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)
diff --git a/common/djangoapps/student/tests/tests.py b/common/djangoapps/student/tests/tests.py
index 199a794bc4..c28a54afe8 100644
--- a/common/djangoapps/student/tests/tests.py
+++ b/common/djangoapps/student/tests/tests.py
@@ -26,7 +26,7 @@ from mock import Mock, patch
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,
- change_enrollment, complete_course_mode_info)
+ change_enrollment, complete_course_mode_info, token)
from student.tests.factories import UserFactory, CourseModeFactory
import shoppingcart
@@ -498,3 +498,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])
diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py
index b199196679..5cecaff5df 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -44,6 +44,7 @@ from student.models import (
create_comments_service_user, PasswordHistory
)
from student.forms import PasswordResetFormNoActive
+from student.firebase_token_generator import create_token
from verify_student.models import SoftwareSecurePhotoVerification, MidcourseReverificationWindow
from certificates.models import CertificateStatuses, certificate_status_for_student
@@ -1851,3 +1852,26 @@ def change_email_settings(request):
track.views.server_track(request, "change-email-settings", {"receive_emails": "no", "course": course_id}, page='dashboard')
return JsonResponse({"success": True})
+
+
+@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
diff --git a/common/lib/xmodule/xmodule/annotator_token.py b/common/lib/xmodule/xmodule/annotator_token.py
deleted file mode 100644
index 6fa5695978..0000000000
--- a/common/lib/xmodule/xmodule/annotator_token.py
+++ /dev/null
@@ -1,32 +0,0 @@
-"""
-This file contains a function used to retrieve the token for the annotation backend
-without having to create a view, but just returning a string instead.
-
-It can be called from other files by using the following:
-from xmodule.annotator_token import retrieve_token
-"""
-import datetime
-from firebase_token_generator import create_token
-
-
-def retrieve_token(userid, secret):
- '''
- 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.
- '''
-
- # the following five lines of code allows you to include the default timezone in the iso format
- # for more information: http://stackoverflow.com/questions/3401428/how-to-get-an-isoformat-datetime-string-including-the-default-timezone
- 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)
- # uses the issued time (UTC plus timezone), the consumer key and the user's email to maintain a
- # federated system in the annotation backend server
- custom_data = {"issuedAt": newtime, "consumerKey": secret, "userId": userid, "ttl": 86400}
- newtoken = create_token(secret, custom_data)
- return newtoken
diff --git a/common/lib/xmodule/xmodule/tests/test_annotator_token.py b/common/lib/xmodule/xmodule/tests/test_annotator_token.py
deleted file mode 100644
index ae06808bba..0000000000
--- a/common/lib/xmodule/xmodule/tests/test_annotator_token.py
+++ /dev/null
@@ -1,20 +0,0 @@
-"""
-This test will run for annotator_token.py
-"""
-import unittest
-
-from xmodule.annotator_token import retrieve_token
-
-
-class TokenRetriever(unittest.TestCase):
- """
- Tests to make sure that when passed in a username and secret token, that it will be encoded correctly
- """
- def test_token(self):
- """
- Test for the token generator. Give an a random username and secret token, it should create the properly encoded string of text.
- """
- expected = "eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJpc3N1ZWRBdCI6ICIyMDE0LTAyLTI3VDE3OjAwOjQyLjQwNjQ0MSswOjAwIiwgImNvbnN1bWVyS2V5IjogImZha2Vfc2VjcmV0IiwgInVzZXJJZCI6ICJ1c2VybmFtZSIsICJ0dGwiOiA4NjQwMH0.Dx1PoF-7mqBOOSGDMZ9R_s3oaaLRPnn6CJgGGF2A5CQ"
- response = retrieve_token("username", "fake_secret")
- self.assertEqual(expected.split('.')[0], response.split('.')[0])
- self.assertNotEqual(expected.split('.')[2], response.split('.')[2])
\ No newline at end of file
diff --git a/common/lib/xmodule/xmodule/tests/test_textannotation.py b/common/lib/xmodule/xmodule/tests/test_textannotation.py
index 907eb78780..397e3990ef 100644
--- a/common/lib/xmodule/xmodule/tests/test_textannotation.py
+++ b/common/lib/xmodule/xmodule/tests/test_textannotation.py
@@ -38,6 +38,17 @@ class TextAnnotationModuleTestCase(unittest.TestCase):
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.
@@ -59,5 +70,5 @@ class TextAnnotationModuleTestCase(unittest.TestCase):
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', 'token']:
+ for key in ['display_name', 'tag', 'source', 'instructions_html', 'content_html', 'annotation_storage']:
self.assertIn(key, context)
diff --git a/common/lib/xmodule/xmodule/tests/test_videoannotation.py b/common/lib/xmodule/xmodule/tests/test_videoannotation.py
index 4a081803aa..cb63d05503 100644
--- a/common/lib/xmodule/xmodule/tests/test_videoannotation.py
+++ b/common/lib/xmodule/xmodule/tests/test_videoannotation.py
@@ -34,6 +34,100 @@ class VideoAnnotationModuleTestCase(unittest.TestCase):
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 = 'test'
+ 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 = 'test'
+
+ 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 = 'test'
+
+ 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('test')
+
+ 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 = 'z'
+ expected_el = etree.fromstring(expected_html)
+
+ actual_el = etree.fromstring('z')
+ 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
@@ -66,6 +160,6 @@ class VideoAnnotationModuleTestCase(unittest.TestCase):
"""
Tests to make sure variables passed in truly exist within the html once it is all rendered.
"""
- context = self.mod.get_html() # pylint: disable=W0212
- for key in ['display_name', 'instructions_html', 'sourceUrl', 'typeSource', 'poster', 'annotation_storage']:
+ context = self.mod.get_html()
+ for key in ['display_name', 'content_html', 'instructions_html', 'sourceUrl', 'typeSource', 'poster', 'alert', 'annotation_storage']:
self.assertIn(key, context)
diff --git a/common/lib/xmodule/xmodule/textannotation_module.py b/common/lib/xmodule/xmodule/textannotation_module.py
index 4a673eb33e..1d732d8709 100644
--- a/common/lib/xmodule/xmodule/textannotation_module.py
+++ b/common/lib/xmodule/xmodule/textannotation_module.py
@@ -6,7 +6,6 @@ from pkg_resources import resource_string
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from xblock.core import Scope, String
-from xmodule.annotator_token import retrieve_token
import textwrap
@@ -31,7 +30,7 @@ class AnnotatableFields(object):
scope=Scope.settings,
default='Text Annotation',
)
- instructor_tags = String(
+ 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,
@@ -44,7 +43,6 @@ class AnnotatableFields(object):
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")
- 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")
class TextAnnotationModule(AnnotatableFields, XModule):
@@ -61,9 +59,15 @@ class TextAnnotationModule(AnnotatableFields, XModule):
self.instructions = self._extract_instructions(xmltree)
self.content = etree.tostring(xmltree, encoding='unicode')
- self.user_email = ""
- if self.runtime.get_real_user is not None:
- self.user_email = self.runtime.get_real_user(self.runtime.anonymous_student_id).email
+ 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 from the xmltree and returns them as a string, otherwise None. """
@@ -78,13 +82,13 @@ class TextAnnotationModule(AnnotatableFields, XModule):
""" Renders parameters to template. """
context = {
'display_name': self.display_name_with_default,
- 'tag': self.instructor_tags,
+ 'tag': self.tags,
'source': self.source,
'instructions_html': self.instructions,
- 'content_html': self.content,
- 'annotation_storage': self.annotation_storage_url,
- 'token': retrieve_token(self.user_email, self.annotation_token_secret),
+ 'content_html': self._render_content(),
+ 'annotation_storage': self.annotation_storage_url
}
+
return self.system.render_template('textannotation.html', context)
@@ -97,7 +101,6 @@ class TextAnnotationDescriptor(AnnotatableFields, RawDescriptor):
def non_editable_metadata_fields(self):
non_editable_fields = super(TextAnnotationDescriptor, self).non_editable_metadata_fields
non_editable_fields.extend([
- TextAnnotationDescriptor.annotation_storage_url,
- TextAnnotationDescriptor.annotation_token_secret,
+ TextAnnotationDescriptor.annotation_storage_url
])
return non_editable_fields
diff --git a/common/lib/xmodule/xmodule/videoannotation_module.py b/common/lib/xmodule/xmodule/videoannotation_module.py
index 68e5b40413..5f31509d01 100644
--- a/common/lib/xmodule/xmodule/videoannotation_module.py
+++ b/common/lib/xmodule/xmodule/videoannotation_module.py
@@ -7,7 +7,6 @@ from pkg_resources import resource_string
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from xblock.core import Scope, String
-from xmodule.annotator_token import retrieve_token
import textwrap
@@ -32,7 +31,7 @@ class AnnotatableFields(object):
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")
- 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")
+
class VideoAnnotationModule(AnnotatableFields, XModule):
'''Video Annotation Module'''
@@ -56,9 +55,73 @@ class VideoAnnotationModule(AnnotatableFields, XModule):
self.instructions = self._extract_instructions(xmltree)
self.content = etree.tostring(xmltree, encoding='unicode')
- self.user_email = ""
- if self.runtime.get_real_user is not None:
- self.user_email = self.runtime.get_real_user(self.runtime.anonymous_student_id).email
+ 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 from the xmltree and returns them as a string, otherwise None. """
@@ -91,9 +154,9 @@ class VideoAnnotationModule(AnnotatableFields, XModule):
'sourceUrl': self.sourceurl,
'typeSource': extension,
'poster': self.poster_url,
- 'content_html': self.content,
- 'annotation_storage': self.annotation_storage_url,
- 'token': retrieve_token(self.user_email, self.annotation_token_secret),
+ 'alert': self,
+ 'content_html': self._render_content(),
+ 'annotation_storage': self.annotation_storage_url
}
return self.system.render_template('videoannotation.html', context)
@@ -108,7 +171,6 @@ class VideoAnnotationDescriptor(AnnotatableFields, RawDescriptor):
def non_editable_metadata_fields(self):
non_editable_fields = super(VideoAnnotationDescriptor, self).non_editable_metadata_fields
non_editable_fields.extend([
- VideoAnnotationDescriptor.annotation_storage_url,
- VideoAnnotationDescriptor.annotation_token_secret,
+ VideoAnnotationDescriptor.annotation_storage_url
])
return non_editable_fields
diff --git a/common/static/js/vendor/ova/annotator-full-firebase-auth.js b/common/static/js/vendor/ova/annotator-full-firebase-auth.js
deleted file mode 100644
index defc25fc95..0000000000
--- a/common/static/js/vendor/ova/annotator-full-firebase-auth.js
+++ /dev/null
@@ -1,22 +0,0 @@
-Annotator.Plugin.Auth.prototype.haveValidToken = function() {
- return (
- this._unsafeToken &&
- this._unsafeToken.d.issuedAt &&
- this._unsafeToken.d.ttl &&
- this._unsafeToken.d.consumerKey &&
- this.timeToExpiry() > 0
- );
-};
-
-Annotator.Plugin.Auth.prototype.timeToExpiry = function() {
- var expiry, issue, now, timeToExpiry;
- now = new Date().getTime() / 1000;
- issue = createDateFromISO8601(this._unsafeToken.d.issuedAt).getTime() / 1000;
- expiry = issue + this._unsafeToken.d.ttl;
- timeToExpiry = expiry - now;
- if (timeToExpiry > 0) {
- return timeToExpiry;
- } else {
- return 0;
- }
-};
\ No newline at end of file
diff --git a/lms/djangoapps/notes/views.py b/lms/djangoapps/notes/views.py
index 1e14fcaa25..b6670a7e09 100644
--- a/lms/djangoapps/notes/views.py
+++ b/lms/djangoapps/notes/views.py
@@ -4,7 +4,6 @@ from edxmako.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
-from xmodule.annotator_token import retrieve_token
@login_required
@@ -23,8 +22,7 @@ def notes(request, course_id):
'course': course,
'notes': notes,
'student': student,
- 'storage': storage,
- 'token': retrieve_token(student.email, course.annotation_token_secret),
+ 'storage': storage
}
return render_to_response('notes.html', context)
diff --git a/lms/envs/common.py b/lms/envs/common.py
index 0c2b37b2d5..9340aecc54 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -828,7 +828,6 @@ main_vendor_js = [
'js/vendor/swfobject/swfobject.js',
'js/vendor/jquery.ba-bbq.min.js',
'js/vendor/ova/annotator-full.js',
- 'js/vendor/ova/annotator-full-firebase-auth.js',
'js/vendor/ova/video.dev.js',
'js/vendor/ova/vjs.youtube.js',
'js/vendor/ova/rangeslider.js',
diff --git a/lms/templates/notes.html b/lms/templates/notes.html
index e44a78b08e..d896725581 100644
--- a/lms/templates/notes.html
+++ b/lms/templates/notes.html
@@ -68,8 +68,10 @@
//Grab uri of the course
var parts = window.location.href.split("/"),
- uri = '';
+ 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 = {
@@ -128,7 +130,7 @@
},
},
auth: {
- token: "${token}"
+ tokenUrl: location.protocol+'//'+location.host+"/token?course_id="+courseid
},
store: {
// The endpoint of the store on your server.
diff --git a/lms/templates/textannotation.html b/lms/templates/textannotation.html
index f69cb7b68c..3532681051 100644
--- a/lms/templates/textannotation.html
+++ b/lms/templates/textannotation.html
@@ -1,63 +1,64 @@
<%! from django.utils.translation import ugettext as _ %>
-
- % if display_name is not UNDEFINED and display_name is not None:
-
${display_name}
- % endif
-
- % if instructions_html is not UNDEFINED and instructions_html is not None:
-