From 5ae1ca96856fa18453619e898b5505262be3e13a Mon Sep 17 00:00:00 2001 From: lduarte1991 Date: Thu, 24 Apr 2014 16:14:05 -0400 Subject: [PATCH 1/6] Reconnecting Token Generator for Annotation Tool --- 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 | 27 +++++ .../xmodule/tests/test_annotator_token.py | 20 ++++ .../xmodule/tests/test_textannotation.py | 13 +-- .../xmodule/tests/test_videoannotation.py | 98 +----------------- .../xmodule/xmodule/textannotation_module.py | 34 ++++--- .../xmodule/xmodule/videoannotation_module.py | 80 ++------------- .../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 | 14 ++- lms/templates/videoannotation.html | 11 +-- lms/urls.py | 1 - requirements/edx/base.txt | 1 + 19 files changed, 118 insertions(+), 407 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 9e199e3372..00e606e42f 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -319,7 +319,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 deleted file mode 100644 index f84a85277e..0000000000 --- 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 1eb09c9173..0000000000 --- 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 48d8bb642e..93f4fbd7f4 100644 --- a/common/djangoapps/student/tests/tests.py +++ b/common/djangoapps/student/tests/tests.py @@ -25,7 +25,7 @@ from mock import Mock, patch, sentinel 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 @@ -458,26 +458,3 @@ 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 d2e8b32d6b..390680f2cb 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -45,7 +45,6 @@ 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 @@ -1788,26 +1787,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 0000000000..9d44962495 --- /dev/null +++ b/common/lib/xmodule/xmodule/annotator_token.py @@ -0,0 +1,27 @@ +""" +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. + ''' + 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) + custom_data = {"issuedAt": newtime, "consumerKey": secret, "userId": userid, "ttl": 86400} + newtoken = create_token(secret, custom_data) + return newtoken \ No newline at end of file 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 0000000000..ae06808bba --- /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 397e3990ef..907eb78780 100644 --- a/common/lib/xmodule/xmodule/tests/test_textannotation.py +++ b/common/lib/xmodule/xmodule/tests/test_textannotation.py @@ -38,17 +38,6 @@ 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. @@ -70,5 +59,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']: + 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 cb63d05503..4a081803aa 100644 --- a/common/lib/xmodule/xmodule/tests/test_videoannotation.py +++ b/common/lib/xmodule/xmodule/tests/test_videoannotation.py @@ -34,100 +34,6 @@ 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 @@ -160,6 +66,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() - 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 1d732d8709..6cc0f9512f 100644 --- a/common/lib/xmodule/xmodule/textannotation_module.py +++ b/common/lib/xmodule/xmodule/textannotation_module.py @@ -6,6 +6,7 @@ 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 @@ -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,13 @@ 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") + diacritics = String( + display_name="Diacritic Marks", + help= "Add diacritic marks to be added to a text using the comma-separated form, i.e. markname;urltomark;baseline,markname2;urltomark2;baseline2", + scope=Scope.settings, + default='', + ) class TextAnnotationModule(AnnotatableFields, XModule): @@ -59,15 +67,9 @@ class TextAnnotationModule(AnnotatableFields, XModule): 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 = "" + if self.runtime.get_real_user is not None: + self.user = 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 +84,14 @@ class TextAnnotationModule(AnnotatableFields, XModule): """ 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, self.annotation_token_secret), + 'diacritic_marks': self.diacritics, } - return self.system.render_template('textannotation.html', context) @@ -101,6 +104,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 5f31509d01..2a7a0e75e5 100644 --- a/common/lib/xmodule/xmodule/videoannotation_module.py +++ b/common/lib/xmodule/xmodule/videoannotation_module.py @@ -7,6 +7,7 @@ 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 +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 @@ class VideoAnnotationModule(AnnotatableFields, XModule): 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 = "" + if self.runtime.get_real_user is not None: + self.user = 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 @@ class VideoAnnotationModule(AnnotatableFields, XModule): 'sourceUrl': self.sourceurl, 'typeSource': extension, 'poster': self.poster_url, - 'alert': self, 'content_html': self._render_content(), - 'annotation_storage': self.annotation_storage_url + 'annotation_storage': self.annotation_storage_url, + 'token': retrieve_token(self.user, 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 0000000000..d915f05bf0 --- /dev/null +++ b/common/static/js/vendor/ova/annotator-full-firebase-auth.js @@ -0,0 +1,22 @@ +Annotator.Plugin.Auth.prototype.haveValidToken = function() { + var allFields; + allFields = this._unsafeToken && this._unsafeToken.d.issuedAt && this._unsafeToken.d.ttl && this._unsafeToken.d.consumerKey; + if (allFields && this.timeToExpiry() > 0) { + return true; + } else { + return false; + } +}; + +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 b6670a7e09..1e14fcaa25 100644 --- a/lms/djangoapps/notes/views.py +++ b/lms/djangoapps/notes/views.py @@ -4,6 +4,7 @@ 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 @@ -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 af6d1153d5..1613495284 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -798,6 +798,7 @@ 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 d896725581..e44a78b08e 100644 --- a/lms/templates/notes.html +++ b/lms/templates/notes.html @@ -68,10 +68,8 @@ //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 @@ }, }, 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 3532681051..adb39c1189 100644 --- a/lms/templates/textannotation.html +++ b/lms/templates/textannotation.html @@ -46,14 +46,11 @@ //Grab uri of the course var parts = window.location.href.split("/"), - uri = '', - courseid; + uri = ''; for (var index = 0; index <= 9; index += 1) uri += parts[index]+"/"; //Get the unit url - courseid = parts[4] + "/" + parts[5] + "/" + parts[6]; //Change uri in cms var lms_location = $('.sidebar .preview-button').attr('href'); if (typeof lms_location!='undefined'){ - courseid = parts[4].split(".").join("/"); uri = window.location.protocol; for (var index = 0; index <= 9; index += 1) uri += lms_location.split("/")[index]+"/"; //Get the unit url } @@ -115,7 +112,7 @@ }, }, auth: { - tokenUrl: location.protocol+'//'+location.host+"/token?course_id="+courseid + token: "${token}" }, store: { // The endpoint of the store on your server. @@ -145,6 +142,9 @@ }, highlightTags:{ tag: "${tag}", + }, + diacriticMarks:{ + diacritics: "${diacritic_marks}" } }, optionsVideoJS: {techOrder: ["html5","flash","youtube"]}, @@ -161,7 +161,6 @@ } }, }; - var imgURLRoot = "${settings.STATIC_URL}" + "js/vendor/ova/catch/img/"; tinyMCE.baseURL = "${settings.STATIC_URL}" + "js/vendor/ova"; @@ -174,7 +173,6 @@ //Load the plugin Video/Text Annotation var ova = new OpenVideoAnnotation.Annotator($('#textHolder'),options); - //Catch var annotator = ova.annotator, catchOptions = { @@ -183,7 +181,7 @@ imageUrlRoot:imgURLRoot, showMediaSelector: false, showPublicPrivate: true, - userId:'${user.email}', + userId:'${user.email}', pagination:pagination,//Number of Annotations per load in the pagination, flags:is_staff }, diff --git a/lms/templates/videoannotation.html b/lms/templates/videoannotation.html index 407ec43193..64c5db4e5d 100644 --- a/lms/templates/videoannotation.html +++ b/lms/templates/videoannotation.html @@ -49,14 +49,11 @@ //Grab uri of the course var parts = window.location.href.split("/"), - uri = '', - courseid; + uri = ''; for (var index = 0; index <= 9; index += 1) uri += parts[index]+"/"; //Get the unit url - courseid = parts[4] + "/" + parts[5] + "/" + parts[6]; //Change uri in cms var lms_location = $('.sidebar .preview-button').attr('href'); if (typeof lms_location!='undefined'){ - courseid = parts[4].split(".").join("/"); uri = window.location.protocol; for (var index = 0; index <= 9; index += 1) uri += lms_location.split("/")[index]+"/"; //Get the unit url } @@ -119,7 +116,7 @@ }, }, auth: { - tokenUrl: location.protocol+'//'+location.host+"/token?course_id="+courseid + token: "${token}" }, store: { // The endpoint of the store on your server. @@ -175,8 +172,6 @@ var ova = new OpenVideoAnnotation.Annotator($('#videoHolder'),options); ova.annotator.addPlugin('Tags'); - - //Catch var annotator = ova.annotator, @@ -186,7 +181,7 @@ imageUrlRoot:imgURLRoot, showMediaSelector: false, showPublicPrivate: true, - userId:'${user.email}', + userId:'${user.email}', pagination:pagination,//Number of Annotations per load in the pagination, flags:is_staff }, diff --git a/lms/urls.py b/lms/urls.py index 28c0ca7071..a6a45342f8 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -14,7 +14,6 @@ urlpatterns = ('', # nopep8 url(r'^update_certificate$', 'certificates.views.update_certificate'), url(r'^$', 'branding.views.index', name="root"), # Main marketing page, or redirect to courseware url(r'^dashboard$', 'student.views.dashboard', name="dashboard"), - url(r'^token$', 'student.views.token', name="token"), url(r'^login$', 'student.views.signin_user', name="signin_user"), url(r'^register$', 'student.views.register_user', name="register_user"), diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 012b0cefa3..7913992f2b 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -35,6 +35,7 @@ django-method-override==0.1.0 djangorestframework==2.3.5 django==1.4.8 feedparser==5.1.3 +firebase-token-generator==1.3.2 fs==0.4.0 GitPython==0.3.2.RC1 glob2==0.3 From 7dee9768504b4fc55a4ddc924daf6d62822466d3 Mon Sep 17 00:00:00 2001 From: lduarte1991 Date: Fri, 25 Apr 2014 18:53:29 -0400 Subject: [PATCH 2/6] Removed diacritic mention and explained token --- common/lib/xmodule/xmodule/annotator_token.py | 7 ++++++- common/lib/xmodule/xmodule/textannotation_module.py | 6 ------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/common/lib/xmodule/xmodule/annotator_token.py b/common/lib/xmodule/xmodule/annotator_token.py index 9d44962495..631db723c6 100644 --- a/common/lib/xmodule/xmodule/annotator_token.py +++ b/common/lib/xmodule/xmodule/annotator_token.py @@ -17,11 +17,16 @@ def retrieve_token(userid, secret): the token was issued. This will be stored with the user along with the id for identification purposes in the backend. ''' + + #retrieve difference between current time and Greenwich time to assure timezones are normalized dtnow = datetime.datetime.now() dtutcnow = datetime.datetime.utcnow() delta = dtnow - dtutcnow + #use the new normalized time to retreive the date the token was issued 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, 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 \ No newline at end of file + return newtoken diff --git a/common/lib/xmodule/xmodule/textannotation_module.py b/common/lib/xmodule/xmodule/textannotation_module.py index 6cc0f9512f..e3ef5aa4f9 100644 --- a/common/lib/xmodule/xmodule/textannotation_module.py +++ b/common/lib/xmodule/xmodule/textannotation_module.py @@ -45,12 +45,6 @@ class AnnotatableFields(object): ) 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") - diacritics = String( - display_name="Diacritic Marks", - help= "Add diacritic marks to be added to a text using the comma-separated form, i.e. markname;urltomark;baseline,markname2;urltomark2;baseline2", - scope=Scope.settings, - default='', - ) class TextAnnotationModule(AnnotatableFields, XModule): From 2fd071fc39af840c2da9d1fbd3f067cb0785dbf0 Mon Sep 17 00:00:00 2001 From: lduarte1991 Date: Mon, 28 Apr 2014 13:59:22 -0400 Subject: [PATCH 3/6] Fixed Unit Test Issues --- common/lib/xmodule/xmodule/textannotation_module.py | 1 - common/lib/xmodule/xmodule/videoannotation_module.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/textannotation_module.py b/common/lib/xmodule/xmodule/textannotation_module.py index e3ef5aa4f9..9a561b9411 100644 --- a/common/lib/xmodule/xmodule/textannotation_module.py +++ b/common/lib/xmodule/xmodule/textannotation_module.py @@ -84,7 +84,6 @@ class TextAnnotationModule(AnnotatableFields, XModule): 'content_html': self.content, 'annotation_storage': self.annotation_storage_url, 'token':retrieve_token(self.user, self.annotation_token_secret), - 'diacritic_marks': self.diacritics, } return self.system.render_template('textannotation.html', context) diff --git a/common/lib/xmodule/xmodule/videoannotation_module.py b/common/lib/xmodule/xmodule/videoannotation_module.py index 2a7a0e75e5..c5d47b5c32 100644 --- a/common/lib/xmodule/xmodule/videoannotation_module.py +++ b/common/lib/xmodule/xmodule/videoannotation_module.py @@ -91,7 +91,7 @@ class VideoAnnotationModule(AnnotatableFields, XModule): 'sourceUrl': self.sourceurl, 'typeSource': extension, 'poster': self.poster_url, - 'content_html': self._render_content(), + 'content_html': self.content, 'annotation_storage': self.annotation_storage_url, 'token': retrieve_token(self.user, self.annotation_token_secret), } From 24abd2bb86f2db623837a286a2a8a90807ecd8eb Mon Sep 17 00:00:00 2001 From: lduarte1991 Date: Tue, 29 Apr 2014 13:23:53 -0400 Subject: [PATCH 4/6] Fix comment formatting and Timezone explanation --- common/lib/xmodule/xmodule/annotator_token.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/annotator_token.py b/common/lib/xmodule/xmodule/annotator_token.py index 631db723c6..6fa5695978 100644 --- a/common/lib/xmodule/xmodule/annotator_token.py +++ b/common/lib/xmodule/xmodule/annotator_token.py @@ -18,15 +18,15 @@ def retrieve_token(userid, secret): the id for identification purposes in the backend. ''' - #retrieve difference between current time and Greenwich time to assure timezones are normalized + # 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 - #use the new normalized time to retreive the date the token was issued 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, the consumer key and the user's email to maintain a - #federated system in the annotation backend server + # 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 From 26eb16ebc2744a92dc6d770898ffab325b647189 Mon Sep 17 00:00:00 2001 From: lduarte1991 Date: Wed, 30 Apr 2014 12:24:07 -0400 Subject: [PATCH 5/6] Added unit signifier - narrower annotation target --- lms/templates/textannotation.html | 2 ++ lms/templates/videoannotation.html | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lms/templates/textannotation.html b/lms/templates/textannotation.html index adb39c1189..01d94bf054 100644 --- a/lms/templates/textannotation.html +++ b/lms/templates/textannotation.html @@ -54,6 +54,8 @@ uri = window.location.protocol; for (var index = 0; index <= 9; index += 1) uri += lms_location.split("/")[index]+"/"; //Get the unit url } + var unit_id = $('#sequence-list').find('.active').attr("data-element"); + uri += unit_id; var pagination = 100, is_staff = !('${user.is_staff}'=='False'), options = { diff --git a/lms/templates/videoannotation.html b/lms/templates/videoannotation.html index 64c5db4e5d..82a820a79a 100644 --- a/lms/templates/videoannotation.html +++ b/lms/templates/videoannotation.html @@ -57,7 +57,8 @@ uri = window.location.protocol; for (var index = 0; index <= 9; index += 1) uri += lms_location.split("/")[index]+"/"; //Get the unit url } - + var unit_id = $('#sequence-list').find('.active').attr("data-element"); + uri += unit_id; var pagination = 100, is_staff = !('${user.is_staff}'=='False'), options = { From edeebe7bead73e184b5b769b0eb02bb8fab3ee96 Mon Sep 17 00:00:00 2001 From: lduarte1991 Date: Thu, 8 May 2014 11:37:49 -0400 Subject: [PATCH 6/6] Fixes from @singingwolfboy in PR #3466 --- .../xmodule/xmodule/textannotation_module.py | 6 +- .../xmodule/xmodule/videoannotation_module.py | 6 +- .../ova/annotator-full-firebase-auth.js | 14 +-- lms/templates/textannotation.html | 114 +++++++++--------- 4 files changed, 70 insertions(+), 70 deletions(-) diff --git a/common/lib/xmodule/xmodule/textannotation_module.py b/common/lib/xmodule/xmodule/textannotation_module.py index 9a561b9411..4a673eb33e 100644 --- a/common/lib/xmodule/xmodule/textannotation_module.py +++ b/common/lib/xmodule/xmodule/textannotation_module.py @@ -61,9 +61,9 @@ class TextAnnotationModule(AnnotatableFields, XModule): self.instructions = self._extract_instructions(xmltree) self.content = etree.tostring(xmltree, encoding='unicode') - self.user = "" + self.user_email = "" if self.runtime.get_real_user is not None: - self.user = self.runtime.get_real_user(self.runtime.anonymous_student_id).email + 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. """ @@ -83,7 +83,7 @@ class TextAnnotationModule(AnnotatableFields, XModule): 'instructions_html': self.instructions, 'content_html': self.content, 'annotation_storage': self.annotation_storage_url, - 'token':retrieve_token(self.user, self.annotation_token_secret), + 'token': retrieve_token(self.user_email, self.annotation_token_secret), } return self.system.render_template('textannotation.html', context) diff --git a/common/lib/xmodule/xmodule/videoannotation_module.py b/common/lib/xmodule/xmodule/videoannotation_module.py index c5d47b5c32..68e5b40413 100644 --- a/common/lib/xmodule/xmodule/videoannotation_module.py +++ b/common/lib/xmodule/xmodule/videoannotation_module.py @@ -56,9 +56,9 @@ class VideoAnnotationModule(AnnotatableFields, XModule): self.instructions = self._extract_instructions(xmltree) self.content = etree.tostring(xmltree, encoding='unicode') - self.user = "" + self.user_email = "" if self.runtime.get_real_user is not None: - self.user = self.runtime.get_real_user(self.runtime.anonymous_student_id).email + 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. """ @@ -93,7 +93,7 @@ class VideoAnnotationModule(AnnotatableFields, XModule): 'poster': self.poster_url, 'content_html': self.content, 'annotation_storage': self.annotation_storage_url, - 'token': retrieve_token(self.user, self.annotation_token_secret), + 'token': retrieve_token(self.user_email, self.annotation_token_secret), } return self.system.render_template('videoannotation.html', context) diff --git a/common/static/js/vendor/ova/annotator-full-firebase-auth.js b/common/static/js/vendor/ova/annotator-full-firebase-auth.js index d915f05bf0..defc25fc95 100644 --- a/common/static/js/vendor/ova/annotator-full-firebase-auth.js +++ b/common/static/js/vendor/ova/annotator-full-firebase-auth.js @@ -1,11 +1,11 @@ Annotator.Plugin.Auth.prototype.haveValidToken = function() { - var allFields; - allFields = this._unsafeToken && this._unsafeToken.d.issuedAt && this._unsafeToken.d.ttl && this._unsafeToken.d.consumerKey; - if (allFields && this.timeToExpiry() > 0) { - return true; - } else { - return false; - } + return ( + this._unsafeToken && + this._unsafeToken.d.issuedAt && + this._unsafeToken.d.ttl && + this._unsafeToken.d.consumerKey && + this.timeToExpiry() > 0 + ); }; Annotator.Plugin.Auth.prototype.timeToExpiry = function() { diff --git a/lms/templates/textannotation.html b/lms/templates/textannotation.html index 01d94bf054..f69cb7b68c 100644 --- a/lms/templates/textannotation.html +++ b/lms/templates/textannotation.html @@ -1,63 +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.')}
+
+
+