From 59e3cae4c9c6f2c95a816a8280b470ec6add9486 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Wed, 14 May 2014 13:55:26 -0400 Subject: [PATCH] Revert pull request #3466 --- 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, 464 insertions(+), 176 deletions(-) create mode 100644 common/djangoapps/student/firebase_token_generator.py create mode 100644 common/djangoapps/student/tests/test_token_generator.py delete mode 100644 common/lib/xmodule/xmodule/annotator_token.py delete mode 100644 common/lib/xmodule/xmodule/tests/test_annotator_token.py delete 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 76e0f4d50f..f7195d2c54 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -318,7 +318,7 @@ PIPELINE_CSS = { 'css/vendor/ui-lightness/jquery-ui-1.8.22.custom.css', 'css/vendor/jquery.qtip.min.css', 'js/vendor/markitup/skins/simple/style.css', - 'js/vendor/markitup/sets/wiki/style.css', + 'js/vendor/markitup/sets/wiki/style.css' ], 'output_filename': 'css/cms-style-vendor.css', }, diff --git a/common/djangoapps/student/firebase_token_generator.py b/common/djangoapps/student/firebase_token_generator.py new file mode 100644 index 0000000000..f84a85277e --- /dev/null +++ b/common/djangoapps/student/firebase_token_generator.py @@ -0,0 +1,99 @@ +''' + Firebase - library to generate a token + License: https://github.com/firebase/firebase-token-generator-python/blob/master/LICENSE + Tweaked and Edited by @danielcebrianr and @lduarte1991 + + This library will take either objects or strings and use python's built-in encoding + system as specified by RFC 3548. Thanks to the firebase team for their open-source + library. This was made specifically for speaking with the annotation_storage_url and + can be used and expanded, but not modified by anyone else needing such a process. +''' +from base64 import urlsafe_b64encode +import hashlib +import hmac +import sys +try: + import json +except ImportError: + import simplejson as json + +__all__ = ['create_token'] + +TOKEN_SEP = '.' + + +def create_token(secret, data): + ''' + Simply takes in the secret key and the data and + passes it to the local function _encode_token + ''' + return _encode_token(secret, data) + + +if sys.version_info < (2, 7): + def _encode(bytes_data): + ''' + Takes a json object, string, or binary and + uses python's urlsafe_b64encode to encode data + and make it safe pass along in a url. + To make sure it does not conflict with variables + we make sure equal signs are removed. + More info: docs.python.org/2/library/base64.html + ''' + encoded = urlsafe_b64encode(bytes(bytes_data)) + return encoded.decode('utf-8').replace('=', '') +else: + def _encode(bytes_info): + ''' + Same as above function but for Python 2.7 or later + ''' + encoded = urlsafe_b64encode(bytes_info) + return encoded.decode('utf-8').replace('=', '') + + +def _encode_json(obj): + ''' + Before a python dict object can be properly encoded, + it must be transformed into a jason object and then + transformed into bytes to be encoded using the function + defined above. + ''' + return _encode(bytearray(json.dumps(obj), 'utf-8')) + + +def _sign(secret, to_sign): + ''' + This function creates a sign that goes at the end of the + message that is specific to the secret and not the actual + content of the encoded body. + More info on hashing: http://docs.python.org/2/library/hmac.html + The function creates a hashed values of the secret and to_sign + and returns the digested values based the secure hash + algorithm, 256 + ''' + def portable_bytes(string): + ''' + Simply transforms a string into a bytes object, + which is a series of immutable integers 0<=x<=256. + Always try to encode as utf-8, unless it is not + compliant. + ''' + try: + return bytes(string, 'utf-8') + except TypeError: + return bytes(string) + return _encode(hmac.new(portable_bytes(secret), portable_bytes(to_sign), hashlib.sha256).digest()) # pylint: disable=E1101 + + +def _encode_token(secret, claims): + ''' + This is the main function that takes the secret token and + the data to be transmitted. There is a header created for decoding + purposes. Token_SEP means that a period/full stop separates the + header, data object/message, and signatures. + ''' + encoded_header = _encode_json({'typ': 'JWT', 'alg': 'HS256'}) + encoded_claims = _encode_json(claims) + secure_bits = '%s%s%s' % (encoded_header, TOKEN_SEP, encoded_claims) + sig = _sign(secret, secure_bits) + return '%s%s%s' % (secure_bits, TOKEN_SEP, sig) diff --git a/common/djangoapps/student/tests/test_token_generator.py b/common/djangoapps/student/tests/test_token_generator.py new file mode 100644 index 0000000000..1eb09c9173 --- /dev/null +++ b/common/djangoapps/student/tests/test_token_generator.py @@ -0,0 +1,43 @@ +""" +This test will run for firebase_token_generator.py. +""" + +from django.test import TestCase + +from student.firebase_token_generator import _encode, _encode_json, _encode_token, create_token + + +class TokenGenerator(TestCase): + """ + Tests for the file firebase_token_generator.py + """ + def test_encode(self): + """ + This tests makes sure that no matter what version of python + you have, the _encode function still returns the appropriate result + for a string. + """ + expected = "dGVzdDE" + result = _encode("test1") + self.assertEqual(expected, result) + + def test_encode_json(self): + """ + Same as above, but this one focuses on a python dict type + transformed into a json object and then encoded. + """ + expected = "eyJ0d28iOiAidGVzdDIiLCAib25lIjogInRlc3QxIn0" + result = _encode_json({'one': 'test1', 'two': 'test2'}) + self.assertEqual(expected, result) + + def test_create_token(self): + """ + Unlike its counterpart in student/views.py, this function + just checks for the encoding of a token. The other function + will test depending on time and user. + """ + expected = "eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJ1c2VySWQiOiAidXNlcm5hbWUiLCAidHRsIjogODY0MDB9.-p1sr7uwCapidTQ0qB7DdU2dbF-hViKpPNN_5vD10t8" + result1 = _encode_token('4c7f4d1c-8ac4-4e9f-84c8-b271c57fcac4', {"userId": "username", "ttl": 86400}) + result2 = create_token('4c7f4d1c-8ac4-4e9f-84c8-b271c57fcac4', {"userId": "username", "ttl": 86400}) + self.assertEqual(expected, result1) + self.assertEqual(expected, result2) diff --git a/common/djangoapps/student/tests/tests.py b/common/djangoapps/student/tests/tests.py index 199a794bc4..c28a54afe8 100644 --- a/common/djangoapps/student/tests/tests.py +++ b/common/djangoapps/student/tests/tests.py @@ -26,7 +26,7 @@ from mock import Mock, patch from student.models import anonymous_id_for_user, user_by_anonymous_id, CourseEnrollment, unique_id_for_user from student.views import (process_survey_link, _cert_info, - change_enrollment, complete_course_mode_info) + change_enrollment, complete_course_mode_info, token) from student.tests.factories import UserFactory, CourseModeFactory import shoppingcart @@ -498,3 +498,26 @@ class AnonymousLookupTable(TestCase): anonymous_id = anonymous_id_for_user(self.user, self.course.id) real_user = user_by_anonymous_id(anonymous_id) self.assertEqual(self.user, real_user) + + +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) +class Token(ModuleStoreTestCase): + """ + Test for the token generator. This creates a random course and passes it through the token file which generates the + token that will be passed in to the annotation_storage_url. + """ + request_factory = RequestFactory() + COURSE_SLUG = "100" + COURSE_NAME = "test_course" + COURSE_ORG = "edx" + + def setUp(self): + self.course = CourseFactory.create(org=self.COURSE_ORG, display_name=self.COURSE_NAME, number=self.COURSE_SLUG) + self.user = User.objects.create(username="username", email="username") + self.req = self.request_factory.post('/token?course_id=edx/100/test_course', {'user': self.user}) + self.req.user = self.user + + def test_token(self): + expected = HttpResponse("eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJpc3N1ZWRBdCI6ICIyMDE0LTAxLTIzVDE5OjM1OjE3LjUyMjEwNC01OjAwIiwgImNvbnN1bWVyS2V5IjogInh4eHh4eHh4LXh4eHgteHh4eC14eHh4LXh4eHh4eHh4eHh4eCIsICJ1c2VySWQiOiAidXNlcm5hbWUiLCAidHRsIjogODY0MDB9.OjWz9mzqJnYuzX-f3uCBllqJUa8PVWJjcDy_McfxLvc", mimetype="text/plain") + response = token(self.req) + self.assertEqual(expected.content.split('.')[0], response.content.split('.')[0]) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index b199196679..5cecaff5df 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -44,6 +44,7 @@ from student.models import ( create_comments_service_user, PasswordHistory ) from student.forms import PasswordResetFormNoActive +from student.firebase_token_generator import create_token from verify_student.models import SoftwareSecurePhotoVerification, MidcourseReverificationWindow from certificates.models import CertificateStatuses, certificate_status_for_student @@ -1851,3 +1852,26 @@ def change_email_settings(request): track.views.server_track(request, "change-email-settings", {"receive_emails": "no", "course": course_id}, page='dashboard') return JsonResponse({"success": True}) + + +@login_required +def token(request): + ''' + Return a token for the backend of annotations. + It uses the course id to retrieve a variable that contains the secret + token found in inheritance.py. It also contains information of when + the token was issued. This will be stored with the user along with + the id for identification purposes in the backend. + ''' + course_id = request.GET.get("course_id") + course = course_from_id(course_id) + dtnow = datetime.datetime.now() + dtutcnow = datetime.datetime.utcnow() + delta = dtnow - dtutcnow + newhour, newmin = divmod((delta.days * 24 * 60 * 60 + delta.seconds + 30) // 60, 60) + newtime = "%s%+02d:%02d" % (dtnow.isoformat(), newhour, newmin) + secret = course.annotation_token_secret + custom_data = {"issuedAt": newtime, "consumerKey": secret, "userId": request.user.email, "ttl": 86400} + newtoken = create_token(secret, custom_data) + response = HttpResponse(newtoken, mimetype="text/plain") + return response diff --git a/common/lib/xmodule/xmodule/annotator_token.py b/common/lib/xmodule/xmodule/annotator_token.py deleted file mode 100644 index 6fa5695978..0000000000 --- a/common/lib/xmodule/xmodule/annotator_token.py +++ /dev/null @@ -1,32 +0,0 @@ -""" -This file contains a function used to retrieve the token for the annotation backend -without having to create a view, but just returning a string instead. - -It can be called from other files by using the following: -from xmodule.annotator_token import retrieve_token -""" -import datetime -from firebase_token_generator import create_token - - -def retrieve_token(userid, secret): - ''' - Return a token for the backend of annotations. - It uses the course id to retrieve a variable that contains the secret - token found in inheritance.py. It also contains information of when - the token was issued. This will be stored with the user along with - the id for identification purposes in the backend. - ''' - - # the following five lines of code allows you to include the default timezone in the iso format - # for more information: http://stackoverflow.com/questions/3401428/how-to-get-an-isoformat-datetime-string-including-the-default-timezone - dtnow = datetime.datetime.now() - dtutcnow = datetime.datetime.utcnow() - delta = dtnow - dtutcnow - newhour, newmin = divmod((delta.days * 24 * 60 * 60 + delta.seconds + 30) // 60, 60) - newtime = "%s%+02d:%02d" % (dtnow.isoformat(), newhour, newmin) - # uses the issued time (UTC plus timezone), the consumer key and the user's email to maintain a - # federated system in the annotation backend server - custom_data = {"issuedAt": newtime, "consumerKey": secret, "userId": userid, "ttl": 86400} - newtoken = create_token(secret, custom_data) - return newtoken diff --git a/common/lib/xmodule/xmodule/tests/test_annotator_token.py b/common/lib/xmodule/xmodule/tests/test_annotator_token.py deleted file mode 100644 index ae06808bba..0000000000 --- a/common/lib/xmodule/xmodule/tests/test_annotator_token.py +++ /dev/null @@ -1,20 +0,0 @@ -""" -This test will run for annotator_token.py -""" -import unittest - -from xmodule.annotator_token import retrieve_token - - -class TokenRetriever(unittest.TestCase): - """ - Tests to make sure that when passed in a username and secret token, that it will be encoded correctly - """ - def test_token(self): - """ - Test for the token generator. Give an a random username and secret token, it should create the properly encoded string of text. - """ - expected = "eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJpc3N1ZWRBdCI6ICIyMDE0LTAyLTI3VDE3OjAwOjQyLjQwNjQ0MSswOjAwIiwgImNvbnN1bWVyS2V5IjogImZha2Vfc2VjcmV0IiwgInVzZXJJZCI6ICJ1c2VybmFtZSIsICJ0dGwiOiA4NjQwMH0.Dx1PoF-7mqBOOSGDMZ9R_s3oaaLRPnn6CJgGGF2A5CQ" - response = retrieve_token("username", "fake_secret") - self.assertEqual(expected.split('.')[0], response.split('.')[0]) - self.assertNotEqual(expected.split('.')[2], response.split('.')[2]) \ No newline at end of file diff --git a/common/lib/xmodule/xmodule/tests/test_textannotation.py b/common/lib/xmodule/xmodule/tests/test_textannotation.py index 907eb78780..397e3990ef 100644 --- a/common/lib/xmodule/xmodule/tests/test_textannotation.py +++ b/common/lib/xmodule/xmodule/tests/test_textannotation.py @@ -38,6 +38,17 @@ class TextAnnotationModuleTestCase(unittest.TestCase): ScopeIds(None, None, None, None) ) + def test_render_content(self): + """ + Tests to make sure the sample xml is rendered and that it forms a valid xmltree + that does not contain a display_name. + """ + content = self.mod._render_content() # pylint: disable=W0212 + self.assertIsNotNone(content) + element = etree.fromstring(content) + self.assertIsNotNone(element) + self.assertFalse('display_name' in element.attrib, "Display Name should have been deleted from Content") + def test_extract_instructions(self): """ Tests to make sure that the instructions are correctly pulled from the sample xml above. @@ -59,5 +70,5 @@ class TextAnnotationModuleTestCase(unittest.TestCase): Tests the function that passes in all the information in the context that will be used in templates/textannotation.html """ context = self.mod.get_html() - for key in ['display_name', 'tag', 'source', 'instructions_html', 'content_html', 'annotation_storage', 'token']: + for key in ['display_name', 'tag', 'source', 'instructions_html', 'content_html', 'annotation_storage']: self.assertIn(key, context) diff --git a/common/lib/xmodule/xmodule/tests/test_videoannotation.py b/common/lib/xmodule/xmodule/tests/test_videoannotation.py index 4a081803aa..cb63d05503 100644 --- a/common/lib/xmodule/xmodule/tests/test_videoannotation.py +++ b/common/lib/xmodule/xmodule/tests/test_videoannotation.py @@ -34,6 +34,100 @@ class VideoAnnotationModuleTestCase(unittest.TestCase): ScopeIds(None, None, None, None) ) + def test_annotation_class_attr_default(self): + """ + Makes sure that it can detect annotation values in text-form if user + decides to add text to the area below video, video functionality is completely + found in javascript. + """ + xml = 'test' + element = etree.fromstring(xml) + + expected_attr = {'class': {'value': 'annotatable-span highlight'}} + actual_attr = self.mod._get_annotation_class_attr(element) # pylint: disable=W0212 + + self.assertIsInstance(actual_attr, dict) + self.assertDictEqual(expected_attr, actual_attr) + + def test_annotation_class_attr_with_valid_highlight(self): + """ + Same as above but more specific to an area that is highlightable in the appropriate + color designated. + """ + xml = 'test' + + for color in self.mod.highlight_colors: + element = etree.fromstring(xml.format(highlight=color)) + value = 'annotatable-span highlight highlight-{highlight}'.format(highlight=color) + + expected_attr = {'class': { + 'value': value, + '_delete': 'highlight'} + } + actual_attr = self.mod._get_annotation_class_attr(element) # pylint: disable=W0212 + + self.assertIsInstance(actual_attr, dict) + self.assertDictEqual(expected_attr, actual_attr) + + def test_annotation_class_attr_with_invalid_highlight(self): + """ + Same as above, but checked with invalid colors. + """ + xml = 'test' + + for invalid_color in ['rainbow', 'blink', 'invisible', '', None]: + element = etree.fromstring(xml.format(highlight=invalid_color)) + expected_attr = {'class': { + 'value': 'annotatable-span highlight', + '_delete': 'highlight'} + } + actual_attr = self.mod._get_annotation_class_attr(element) # pylint: disable=W0212 + + self.assertIsInstance(actual_attr, dict) + self.assertDictEqual(expected_attr, actual_attr) + + def test_annotation_data_attr(self): + """ + Test that each highlight contains the data information from the annotation itself. + """ + element = etree.fromstring('test') + + expected_attr = { + 'data-comment-body': {'value': 'foo', '_delete': 'body'}, + 'data-comment-title': {'value': 'bar', '_delete': 'title'}, + 'data-problem-id': {'value': '0', '_delete': 'problem'} + } + + actual_attr = self.mod._get_annotation_data_attr(element) # pylint: disable=W0212 + + self.assertIsInstance(actual_attr, dict) + self.assertDictEqual(expected_attr, actual_attr) + + def test_render_annotation(self): + """ + Tests to make sure that the spans designating annotations acutally visually render as annotations. + """ + expected_html = 'z' + expected_el = etree.fromstring(expected_html) + + actual_el = etree.fromstring('z') + self.mod._render_annotation(actual_el) # pylint: disable=W0212 + + self.assertEqual(expected_el.tag, actual_el.tag) + self.assertEqual(expected_el.text, actual_el.text) + self.assertDictEqual(dict(expected_el.attrib), dict(actual_el.attrib)) + + def test_render_content(self): + """ + Like above, but using the entire text, it makes sure that display_name is removed and that there is only one + div encompassing the annotatable area. + """ + content = self.mod._render_content() # pylint: disable=W0212 + element = etree.fromstring(content) + self.assertIsNotNone(element) + self.assertEqual('div', element.tag, 'root tag is a div') + self.assertFalse('display_name' in element.attrib, "Display Name should have been deleted from Content") + def test_extract_instructions(self): """ This test ensures that if an instruction exists it is pulled and @@ -66,6 +160,6 @@ class VideoAnnotationModuleTestCase(unittest.TestCase): """ Tests to make sure variables passed in truly exist within the html once it is all rendered. """ - context = self.mod.get_html() # pylint: disable=W0212 - for key in ['display_name', 'instructions_html', 'sourceUrl', 'typeSource', 'poster', 'annotation_storage']: + context = self.mod.get_html() + for key in ['display_name', 'content_html', 'instructions_html', 'sourceUrl', 'typeSource', 'poster', 'alert', 'annotation_storage']: self.assertIn(key, context) diff --git a/common/lib/xmodule/xmodule/textannotation_module.py b/common/lib/xmodule/xmodule/textannotation_module.py index 4a673eb33e..1d732d8709 100644 --- a/common/lib/xmodule/xmodule/textannotation_module.py +++ b/common/lib/xmodule/xmodule/textannotation_module.py @@ -6,7 +6,6 @@ from pkg_resources import resource_string from xmodule.x_module import XModule from xmodule.raw_module import RawDescriptor from xblock.core import Scope, String -from xmodule.annotator_token import retrieve_token import textwrap @@ -31,7 +30,7 @@ class AnnotatableFields(object): scope=Scope.settings, default='Text Annotation', ) - instructor_tags = String( + tags = String( display_name="Tags for Assignments", help="Add tags that automatically highlight in a certain color using the comma-separated form, i.e. imagery:red,parallelism:blue", scope=Scope.settings, @@ -44,7 +43,6 @@ class AnnotatableFields(object): default='None', ) annotation_storage_url = String(help="Location of Annotation backend", scope=Scope.settings, default="http://your_annotation_storage.com", display_name="Url for Annotation Storage") - annotation_token_secret = String(help="Secret string for annotation storage", scope=Scope.settings, default="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", display_name="Secret Token String for Annotation") class TextAnnotationModule(AnnotatableFields, XModule): @@ -61,9 +59,15 @@ class TextAnnotationModule(AnnotatableFields, XModule): self.instructions = self._extract_instructions(xmltree) self.content = etree.tostring(xmltree, encoding='unicode') - self.user_email = "" - if self.runtime.get_real_user is not None: - self.user_email = self.runtime.get_real_user(self.runtime.anonymous_student_id).email + self.highlight_colors = ['yellow', 'orange', 'purple', 'blue', 'green'] + + def _render_content(self): + """ Renders annotatable content with annotation spans and returns HTML. """ + xmltree = etree.fromstring(self.content) + if 'display_name' in xmltree.attrib: + del xmltree.attrib['display_name'] + + return etree.tostring(xmltree, encoding='unicode') def _extract_instructions(self, xmltree): """ Removes from the xmltree and returns them as a string, otherwise None. """ @@ -78,13 +82,13 @@ class TextAnnotationModule(AnnotatableFields, XModule): """ Renders parameters to template. """ context = { 'display_name': self.display_name_with_default, - 'tag': self.instructor_tags, + 'tag': self.tags, 'source': self.source, 'instructions_html': self.instructions, - 'content_html': self.content, - 'annotation_storage': self.annotation_storage_url, - 'token': retrieve_token(self.user_email, self.annotation_token_secret), + 'content_html': self._render_content(), + 'annotation_storage': self.annotation_storage_url } + return self.system.render_template('textannotation.html', context) @@ -97,7 +101,6 @@ class TextAnnotationDescriptor(AnnotatableFields, RawDescriptor): def non_editable_metadata_fields(self): non_editable_fields = super(TextAnnotationDescriptor, self).non_editable_metadata_fields non_editable_fields.extend([ - TextAnnotationDescriptor.annotation_storage_url, - TextAnnotationDescriptor.annotation_token_secret, + TextAnnotationDescriptor.annotation_storage_url ]) return non_editable_fields diff --git a/common/lib/xmodule/xmodule/videoannotation_module.py b/common/lib/xmodule/xmodule/videoannotation_module.py index 68e5b40413..5f31509d01 100644 --- a/common/lib/xmodule/xmodule/videoannotation_module.py +++ b/common/lib/xmodule/xmodule/videoannotation_module.py @@ -7,7 +7,6 @@ from pkg_resources import resource_string from xmodule.x_module import XModule from xmodule.raw_module import RawDescriptor from xblock.core import Scope, String -from xmodule.annotator_token import retrieve_token import textwrap @@ -32,7 +31,7 @@ class AnnotatableFields(object): sourceurl = String(help="The external source URL for the video.", display_name="Source URL", scope=Scope.settings, default="http://video-js.zencoder.com/oceans-clip.mp4") poster_url = String(help="Poster Image URL", display_name="Poster URL", scope=Scope.settings, default="") annotation_storage_url = String(help="Location of Annotation backend", scope=Scope.settings, default="http://your_annotation_storage.com", display_name="Url for Annotation Storage") - annotation_token_secret = String(help="Secret string for annotation storage", scope=Scope.settings, default="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", display_name="Secret Token String for Annotation") + class VideoAnnotationModule(AnnotatableFields, XModule): '''Video Annotation Module''' @@ -56,9 +55,73 @@ class VideoAnnotationModule(AnnotatableFields, XModule): self.instructions = self._extract_instructions(xmltree) self.content = etree.tostring(xmltree, encoding='unicode') - self.user_email = "" - if self.runtime.get_real_user is not None: - self.user_email = self.runtime.get_real_user(self.runtime.anonymous_student_id).email + self.highlight_colors = ['yellow', 'orange', 'purple', 'blue', 'green'] + + def _get_annotation_class_attr(self, element): + """ Returns a dict with the CSS class attribute to set on the annotation + and an XML key to delete from the element. + """ + + attr = {} + cls = ['annotatable-span', 'highlight'] + highlight_key = 'highlight' + color = element.get(highlight_key) + + if color is not None: + if color in self.highlight_colors: + cls.append('highlight-' + color) + attr['_delete'] = highlight_key + attr['value'] = ' '.join(cls) + + return {'class': attr} + + def _get_annotation_data_attr(self, element): + """ Returns a dict in which the keys are the HTML data attributes + to set on the annotation element. Each data attribute has a + corresponding 'value' and (optional) '_delete' key to specify + an XML attribute to delete. + """ + + data_attrs = {} + attrs_map = { + 'body': 'data-comment-body', + 'title': 'data-comment-title', + 'problem': 'data-problem-id' + } + + for xml_key in attrs_map.keys(): + if xml_key in element.attrib: + value = element.get(xml_key, '') + html_key = attrs_map[xml_key] + data_attrs[html_key] = {'value': value, '_delete': xml_key} + + return data_attrs + + def _render_annotation(self, element): + """ Renders an annotation element for HTML output. """ + attr = {} + attr.update(self._get_annotation_class_attr(element)) + attr.update(self._get_annotation_data_attr(element)) + + element.tag = 'span' + + for key in attr.keys(): + element.set(key, attr[key]['value']) + if '_delete' in attr[key] and attr[key]['_delete'] is not None: + delete_key = attr[key]['_delete'] + del element.attrib[delete_key] + + def _render_content(self): + """ Renders annotatable content with annotation spans and returns HTML. """ + xmltree = etree.fromstring(self.content) + xmltree.tag = 'div' + if 'display_name' in xmltree.attrib: + del xmltree.attrib['display_name'] + + for element in xmltree.findall('.//annotation'): + self._render_annotation(element) + + return etree.tostring(xmltree, encoding='unicode') def _extract_instructions(self, xmltree): """ Removes from the xmltree and returns them as a string, otherwise None. """ @@ -91,9 +154,9 @@ class VideoAnnotationModule(AnnotatableFields, XModule): 'sourceUrl': self.sourceurl, 'typeSource': extension, 'poster': self.poster_url, - 'content_html': self.content, - 'annotation_storage': self.annotation_storage_url, - 'token': retrieve_token(self.user_email, self.annotation_token_secret), + 'alert': self, + 'content_html': self._render_content(), + 'annotation_storage': self.annotation_storage_url } return self.system.render_template('videoannotation.html', context) @@ -108,7 +171,6 @@ class VideoAnnotationDescriptor(AnnotatableFields, RawDescriptor): def non_editable_metadata_fields(self): non_editable_fields = super(VideoAnnotationDescriptor, self).non_editable_metadata_fields non_editable_fields.extend([ - VideoAnnotationDescriptor.annotation_storage_url, - VideoAnnotationDescriptor.annotation_token_secret, + VideoAnnotationDescriptor.annotation_storage_url ]) return non_editable_fields diff --git a/common/static/js/vendor/ova/annotator-full-firebase-auth.js b/common/static/js/vendor/ova/annotator-full-firebase-auth.js deleted file mode 100644 index defc25fc95..0000000000 --- a/common/static/js/vendor/ova/annotator-full-firebase-auth.js +++ /dev/null @@ -1,22 +0,0 @@ -Annotator.Plugin.Auth.prototype.haveValidToken = function() { - return ( - this._unsafeToken && - this._unsafeToken.d.issuedAt && - this._unsafeToken.d.ttl && - this._unsafeToken.d.consumerKey && - this.timeToExpiry() > 0 - ); -}; - -Annotator.Plugin.Auth.prototype.timeToExpiry = function() { - var expiry, issue, now, timeToExpiry; - now = new Date().getTime() / 1000; - issue = createDateFromISO8601(this._unsafeToken.d.issuedAt).getTime() / 1000; - expiry = issue + this._unsafeToken.d.ttl; - timeToExpiry = expiry - now; - if (timeToExpiry > 0) { - return timeToExpiry; - } else { - return 0; - } -}; \ No newline at end of file diff --git a/lms/djangoapps/notes/views.py b/lms/djangoapps/notes/views.py index 1e14fcaa25..b6670a7e09 100644 --- a/lms/djangoapps/notes/views.py +++ b/lms/djangoapps/notes/views.py @@ -4,7 +4,6 @@ from edxmako.shortcuts import render_to_response from courseware.courses import get_course_with_access from notes.models import Note from notes.utils import notes_enabled_for_course -from xmodule.annotator_token import retrieve_token @login_required @@ -23,8 +22,7 @@ def notes(request, course_id): 'course': course, 'notes': notes, 'student': student, - 'storage': storage, - 'token': retrieve_token(student.email, course.annotation_token_secret), + 'storage': storage } return render_to_response('notes.html', context) diff --git a/lms/envs/common.py b/lms/envs/common.py index 0c2b37b2d5..9340aecc54 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -828,7 +828,6 @@ main_vendor_js = [ 'js/vendor/swfobject/swfobject.js', 'js/vendor/jquery.ba-bbq.min.js', 'js/vendor/ova/annotator-full.js', - 'js/vendor/ova/annotator-full-firebase-auth.js', 'js/vendor/ova/video.dev.js', 'js/vendor/ova/vjs.youtube.js', 'js/vendor/ova/rangeslider.js', diff --git a/lms/templates/notes.html b/lms/templates/notes.html index e44a78b08e..d896725581 100644 --- a/lms/templates/notes.html +++ b/lms/templates/notes.html @@ -68,8 +68,10 @@ //Grab uri of the course var parts = window.location.href.split("/"), - uri = ''; + uri = '', + courseid; for (var index = 0; index <= 6; index += 1) uri += parts[index]+"/"; //Get the unit url + courseid = parts[4] + "/" + parts[5] + "/" + parts[6]; var pagination = 100, is_staff = false, options = { @@ -128,7 +130,7 @@ }, }, auth: { - token: "${token}" + tokenUrl: location.protocol+'//'+location.host+"/token?course_id="+courseid }, store: { // The endpoint of the store on your server. diff --git a/lms/templates/textannotation.html b/lms/templates/textannotation.html index f69cb7b68c..3532681051 100644 --- a/lms/templates/textannotation.html +++ b/lms/templates/textannotation.html @@ -1,63 +1,64 @@ <%! from django.utils.translation import ugettext as _ %>
-
- % if display_name is not UNDEFINED and display_name is not None: -
${display_name}
- % endif -
- % if instructions_html is not UNDEFINED and instructions_html is not None: -
-
- ${_('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.')}
+
+
+