diff --git a/cms/envs/common.py b/cms/envs/common.py
index f7195d2c5429..76e0f4d50f92 100644
--- a/cms/envs/common.py
+++ b/cms/envs/common.py
@@ -318,7 +318,7 @@
'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/pipeline_mako/__init__.py b/common/djangoapps/pipeline_mako/__init__.py
index ed343588dacc..97c17b6bf39a 100644
--- a/common/djangoapps/pipeline_mako/__init__.py
+++ b/common/djangoapps/pipeline_mako/__init__.py
@@ -6,7 +6,7 @@
from static_replace import try_staticfiles_lookup
-def compressed_css(package_name):
+def compressed_css(package_name, raw=False):
package = settings.PIPELINE_CSS.get(package_name, {})
if package:
package = {package_name: package}
@@ -15,17 +15,19 @@ def compressed_css(package_name):
package = packager.package_for('css', package_name)
if settings.PIPELINE:
- return render_css(package, package.output_filename)
+ return render_css(package, package.output_filename, raw=raw)
else:
paths = packager.compile(package.paths)
- return render_individual_css(package, paths)
+ return render_individual_css(package, paths, raw=raw)
-def render_css(package, path):
+def render_css(package, path, raw=False):
template_name = package.template_name or "mako/css.html"
context = package.extra_context
url = try_staticfiles_lookup(path)
+ if raw:
+ url += "?raw"
context.update({
'type': guess_type(path, 'text/css'),
'url': url,
@@ -33,8 +35,8 @@ def render_css(package, path):
return render_to_string(template_name, context)
-def render_individual_css(package, paths):
- tags = [render_css(package, path) for path in paths]
+def render_individual_css(package, paths, raw=False):
+ tags = [render_css(package, path, raw) for path in paths]
return '\n'.join(tags)
diff --git a/common/djangoapps/pipeline_mako/templates/static_content.html b/common/djangoapps/pipeline_mako/templates/static_content.html
index 6efcba7ced3c..a4ca0eba9f25 100644
--- a/common/djangoapps/pipeline_mako/templates/static_content.html
+++ b/common/djangoapps/pipeline_mako/templates/static_content.html
@@ -3,19 +3,19 @@
from pipeline_mako import compressed_css, compressed_js
%>
-<%def name='url(file)'><%
+<%def name='url(file, raw=False)'><%
try:
url = staticfiles_storage.url(file)
except:
url = file
-%>${url}%def>
+%>${url}${"?raw" if raw else ""}%def>
-<%def name='css(group)'>
+<%def name='css(group, raw=False)'>
% if settings.FEATURES['USE_DJANGO_PIPELINE']:
- ${compressed_css(group)}
+ ${compressed_css(group, raw=raw)}
% else:
% for filename in settings.PIPELINE_CSS[group]['source_filenames']:
-
+
% endfor
%endif
%def>
diff --git a/common/djangoapps/student/firebase_token_generator.py b/common/djangoapps/student/firebase_token_generator.py
deleted file mode 100644
index f84a85277e47..000000000000
--- a/common/djangoapps/student/firebase_token_generator.py
+++ /dev/null
@@ -1,99 +0,0 @@
-'''
- 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
deleted file mode 100644
index 1eb09c9173c0..000000000000
--- a/common/djangoapps/student/tests/test_token_generator.py
+++ /dev/null
@@ -1,43 +0,0 @@
-"""
-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 c28a54afe8de..199a794bc4f6 100644
--- a/common/djangoapps/student/tests/tests.py
+++ b/common/djangoapps/student/tests/tests.py
@@ -26,7 +26,7 @@
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, token)
+ change_enrollment, complete_course_mode_info)
from student.tests.factories import UserFactory, CourseModeFactory
import shoppingcart
@@ -498,26 +498,3 @@ def test_roundtrip_for_logged_user(self):
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 5cecaff5df2f..b199196679f9 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -44,7 +44,6 @@
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
@@ -1852,26 +1851,3 @@ 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
new file mode 100644
index 000000000000..6fa569597851
--- /dev/null
+++ b/common/lib/xmodule/xmodule/annotator_token.py
@@ -0,0 +1,32 @@
+"""
+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
new file mode 100644
index 000000000000..ae06808bba59
--- /dev/null
+++ b/common/lib/xmodule/xmodule/tests/test_annotator_token.py
@@ -0,0 +1,20 @@
+"""
+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 397e3990ef60..907eb787802d 100644
--- a/common/lib/xmodule/xmodule/tests/test_textannotation.py
+++ b/common/lib/xmodule/xmodule/tests/test_textannotation.py
@@ -38,17 +38,6 @@ def setUp(self):
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.
@@ -70,5 +59,5 @@ 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']:
+ for key in ['display_name', 'tag', 'source', 'instructions_html', 'content_html', 'annotation_storage', 'token']:
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 cb63d055038d..4a081803aa86 100644
--- a/common/lib/xmodule/xmodule/tests/test_videoannotation.py
+++ b/common/lib/xmodule/xmodule/tests/test_videoannotation.py
@@ -34,100 +34,6 @@ def setUp(self):
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
@@ -160,6 +66,6 @@ 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']:
+ context = self.mod.get_html() # pylint: disable=W0212
+ for key in ['display_name', 'instructions_html', 'sourceUrl', 'typeSource', 'poster', '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 1d732d870925..4a673eb33e54 100644
--- a/common/lib/xmodule/xmodule/textannotation_module.py
+++ b/common/lib/xmodule/xmodule/textannotation_module.py
@@ -6,6 +6,7 @@
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
@@ -30,7 +31,7 @@ class AnnotatableFields(object):
scope=Scope.settings,
default='Text Annotation',
)
- tags = String(
+ instructor_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,
@@ -43,6 +44,7 @@ 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):
@@ -59,15 +61,9 @@ def __init__(self, *args, **kwargs):
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')
+ 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
def _extract_instructions(self, xmltree):
""" Removes from the xmltree and returns them as a string, otherwise None. """
@@ -82,13 +78,13 @@ def get_html(self):
""" Renders parameters to template. """
context = {
'display_name': self.display_name_with_default,
- 'tag': self.tags,
+ 'tag': self.instructor_tags,
'source': self.source,
'instructions_html': self.instructions,
- 'content_html': self._render_content(),
- 'annotation_storage': self.annotation_storage_url
+ 'content_html': self.content,
+ 'annotation_storage': self.annotation_storage_url,
+ 'token': retrieve_token(self.user_email, self.annotation_token_secret),
}
-
return self.system.render_template('textannotation.html', context)
@@ -101,6 +97,7 @@ 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_storage_url,
+ TextAnnotationDescriptor.annotation_token_secret,
])
return non_editable_fields
diff --git a/common/lib/xmodule/xmodule/videoannotation_module.py b/common/lib/xmodule/xmodule/videoannotation_module.py
index 5f31509d013e..68e5b40413ed 100644
--- a/common/lib/xmodule/xmodule/videoannotation_module.py
+++ b/common/lib/xmodule/xmodule/videoannotation_module.py
@@ -7,6 +7,7 @@
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 +32,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'''
@@ -55,73 +56,9 @@ def __init__(self, *args, **kwargs):
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')
+ 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
def _extract_instructions(self, xmltree):
""" Removes from the xmltree and returns them as a string, otherwise None. """
@@ -154,9 +91,9 @@ def get_html(self):
'sourceUrl': self.sourceurl,
'typeSource': extension,
'poster': self.poster_url,
- 'alert': self,
- 'content_html': self._render_content(),
- 'annotation_storage': self.annotation_storage_url
+ 'content_html': self.content,
+ 'annotation_storage': self.annotation_storage_url,
+ 'token': retrieve_token(self.user_email, self.annotation_token_secret),
}
return self.system.render_template('videoannotation.html', context)
@@ -171,6 +108,7 @@ 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_storage_url,
+ VideoAnnotationDescriptor.annotation_token_secret,
])
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
new file mode 100644
index 000000000000..defc25fc953e
--- /dev/null
+++ b/common/static/js/vendor/ova/annotator-full-firebase-auth.js
@@ -0,0 +1,22 @@
+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/common/static/js/vendor/ova/richText-annotator.js b/common/static/js/vendor/ova/richText-annotator.js
index 828d7ef8ef89..af3410faab01 100644
--- a/common/static/js/vendor/ova/richText-annotator.js
+++ b/common/static/js/vendor/ova/richText-annotator.js
@@ -31,11 +31,21 @@ Annotator.Plugin.RichText = (function(_super) {
RichText.prototype.options = {
tinymce:{
selector: "li.annotator-item textarea",
- plugins: "media image insertdatetime link code",
+ skin: 'studio-tmce4',
+ formats: {
+ code: {
+ inline: 'code'
+ }
+ },
+ codemirror: {
+ path: "static/js/vendor"
+ },
+ plugins: "image link codemirror",
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 ",
+ toolbar: "insertfile undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image rubric | code ",
+ resize: "both",
}
};
diff --git a/lms/djangoapps/notes/views.py b/lms/djangoapps/notes/views.py
index b6670a7e0923..1e14fcaa2540 100644
--- a/lms/djangoapps/notes/views.py
+++ b/lms/djangoapps/notes/views.py
@@ -4,6 +4,7 @@
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
@@ -22,7 +23,8 @@ def notes(request, course_id):
'course': course,
'notes': notes,
'student': student,
- 'storage': storage
+ 'storage': storage,
+ 'token': retrieve_token(student.email, course.annotation_token_secret),
}
return render_to_response('notes.html', context)
diff --git a/lms/envs/common.py b/lms/envs/common.py
index 9340aecc541e..fe721c8abac6 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -828,11 +828,11 @@
'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',
'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',
diff --git a/lms/templates/notes.html b/lms/templates/notes.html
index d8967255814f..e44a78b08ec0 100644
--- a/lms/templates/notes.html
+++ b/lms/templates/notes.html
@@ -68,10 +68,8 @@
${_('My Notes')}
//Grab uri of the course
var parts = window.location.href.split("/"),
- uri = '',
- courseid;
+ uri = '';
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 = {
@@ -130,7 +128,7 @@
${_('My Notes')}
},
},
auth: {
- tokenUrl: location.protocol+'//'+location.host+"/token?course_id="+courseid
+ token: "${token}"
},
store: {
// The endpoint of the store on your server.
diff --git a/lms/templates/textannotation.html b/lms/templates/textannotation.html
index 35326810511c..dc6ba4bcfb1b 100644
--- a/lms/templates/textannotation.html
+++ b/lms/templates/textannotation.html
@@ -1,78 +1,82 @@
<%! from django.utils.translation import ugettext as _ %>
+<%namespace name='static' file='/static_content.html'/>
+${static.css(group='style-vendor-tinymce-content', raw=True)}
+${static.css(group='style-vendor-tinymce-skin', raw=True)}
+
+
-
- % 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:
-