From e911d579112f721d8911a17a513837589e531fb3 Mon Sep 17 00:00:00 2001 From: lduarte1991 Date: Thu, 15 May 2014 15:23:28 -0400 Subject: [PATCH 1/6] Revert "Revert pull request #3466" This reverts commit 59e3cae4c9c6f2c95a816a8280b470ec6add9486. --- cms/envs/common.py | 2 +- .../student/firebase_token_generator.py | 99 -------------- .../student/tests/test_token_generator.py | 43 ------ common/djangoapps/student/tests/tests.py | 25 +--- common/djangoapps/student/views.py | 24 ---- common/lib/xmodule/xmodule/annotator_token.py | 32 +++++ .../xmodule/tests/test_annotator_token.py | 20 +++ .../xmodule/tests/test_textannotation.py | 13 +- .../xmodule/tests/test_videoannotation.py | 98 +------------- .../xmodule/xmodule/textannotation_module.py | 27 ++-- .../xmodule/xmodule/videoannotation_module.py | 82 ++---------- .../ova/annotator-full-firebase-auth.js | 22 +++ lms/djangoapps/notes/views.py | 4 +- lms/envs/common.py | 1 + lms/templates/notes.html | 6 +- lms/templates/textannotation.html | 126 +++++++++--------- lms/templates/videoannotation.html | 14 +- lms/urls.py | 1 - requirements/edx/base.txt | 1 + 19 files changed, 176 insertions(+), 464 deletions(-) delete mode 100644 common/djangoapps/student/firebase_token_generator.py delete mode 100644 common/djangoapps/student/tests/test_token_generator.py create mode 100644 common/lib/xmodule/xmodule/annotator_token.py create mode 100644 common/lib/xmodule/xmodule/tests/test_annotator_token.py create mode 100644 common/static/js/vendor/ova/annotator-full-firebase-auth.js 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/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/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..0c2b37b2d5b6 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -828,6 +828,7 @@ '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 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..f69cb7b68c9f 100644 --- a/lms/templates/textannotation.html +++ b/lms/templates/textannotation.html @@ -1,64 +1,63 @@ <%! 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: -
-
- ${_('Instructions')} - ${_('Collapse Instructions')} -
-
- ${instructions_html} -
-
- % endif -
-
-
${content_html}
-
${_('Source:')} ${source}
-
-
${_('You do not have any notes.')}
-
-
-
+
+ % 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: +
+
+ ${_('Instructions')} + ${_('Collapse Instructions')} +
+
+ ${instructions_html} +
+
+ % endif +
+
+
${content_html}
+
${_('Source:')} ${source}
+
+
${_('You do not have any notes.')}
+
+
+