Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,4 @@ David Bodor <david.gabor.bodor@gmail.com>
Sébastien Hinderer <Sebastien.Hinderer@inria.fr>
Kristin Stephens <ksteph@cs.berkeley.edu>
Ben Patterson <bpatterson@edx.org>
Luis Duarte <lduarte1991@gmail.com>
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -754,3 +754,5 @@ LMS: Option to email students when enroll/un-enroll them.

Blades: Added WAI-ARIA markup to the video player controls. These are now fully
accessible by screen readers.

Common: Added advanced_module for annotating images to go with the ones for text and videos.
2 changes: 1 addition & 1 deletion cms/djangoapps/contentstore/tests/test_contentstore.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ def test_advanced_components_in_edit_unit(self):
# response HTML
self.check_components_on_page(
ADVANCED_COMPONENT_TYPES,
['Word cloud', 'Annotation', 'Text Annotation', 'Video Annotation',
['Word cloud', 'Annotation', 'Text Annotation', 'Video Annotation', 'Image Annotation',
'Open Response Assessment', 'Peer Grading Interface', 'openassessment'],
)

Expand Down
1 change: 1 addition & 0 deletions cms/djangoapps/contentstore/views/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
'annotatable',
'textannotation', # module for annotating text (with annotation table)
'videoannotation', # module for annotating video (with annotation table)
'imageannotation', # module for annotating image (with annotation table)
'word_cloud',
'graphical_slider_tool',
'lti',
Expand Down
1 change: 1 addition & 0 deletions common/lib/xmodule/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"annotatable = xmodule.annotatable_module:AnnotatableDescriptor",
"textannotation = xmodule.textannotation_module:TextAnnotationDescriptor",
"videoannotation = xmodule.videoannotation_module:VideoAnnotationDescriptor",
"imageannotation = xmodule.imageannotation_module:ImageAnnotationDescriptor",
"foldit = xmodule.foldit_module:FolditDescriptor",
"word_cloud = xmodule.word_cloud_module:WordCloudDescriptor",
"hidden = xmodule.hidden_module:HiddenDescriptor",
Expand Down
55 changes: 55 additions & 0 deletions common/lib/xmodule/xmodule/annotator_mixin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""
Annotations Tool Mixin
This file contains global variables and functions used in the various Annotation Tools.
"""
from lxml import etree
from urlparse import urlparse
from os.path import splitext, basename
from HTMLParser import HTMLParser


def get_instructions(xmltree):
""" Removes <instructions> from the xmltree and returns them as a string, otherwise None. """
instructions = xmltree.find('instructions')
if instructions is not None:
instructions.tag = 'div'
xmltree.remove(instructions)
return etree.tostring(instructions, encoding='unicode')
return None


def get_extension(srcurl):
"""get the extension of a given url """
if 'youtu' in srcurl:
return 'video/youtube'
else:
disassembled = urlparse(srcurl)
file_ext = splitext(basename(disassembled.path))[1]
return 'video/' + file_ext.replace('.', '')


class MLStripper(HTMLParser):
"helper function for html_to_text below"
def __init__(self):
HTMLParser.__init__(self)
self.reset()
self.fed = []

def handle_data(self, data):
"""takes the data in separate chunks"""
self.fed.append(data)

def handle_entityref(self, name):
"""appends the reference to the body"""
self.fed.append('&%s;' % name)

def get_data(self):
"""joins together the seperate chunks into one cohesive string"""
return ''.join(self.fed)


def html_to_text(html):
"strips the html tags off of the text to return plaintext"
htmlstripper = MLStripper()
htmlstripper.feed(html)
return htmlstripper.get_data()
2 changes: 1 addition & 1 deletion common/lib/xmodule/xmodule/annotator_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def retrieve_token(userid, secret):
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
# 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)
Expand Down
120 changes: 120 additions & 0 deletions common/lib/xmodule/xmodule/imageannotation_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"""
Module for Image annotations using annotator.
"""
from lxml import etree
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_mixin import get_instructions, html_to_text
from xmodule.annotator_token import retrieve_token

import textwrap


class AnnotatableFields(object):
""" Fields for `ImageModule` and `ImageDescriptor`. """
data = String(help="XML data for the annotation", scope=Scope.content, default=textwrap.dedent("""\
<annotatable>
<instructions>
<p>
Add the instructions to the assignment here.
</p>
</instructions>
<p>
Lorem ipsum dolor sit amet, at amet animal petentium nec. Id augue nemore postulant mea. Ex eam dicant noluisse expetenda, alia admodum abhorreant qui et. An ceteros expetenda mea, tale natum ipsum quo no, ut pro paulo alienum noluisse.
</p>
<json>
navigatorSizeRatio: 0.25,
wrapHorizontal: false,
showNavigator: true,
navigatorPosition: "BOTTOM_LEFT",
showNavigationControl: true,
tileSources: [{"profile": "http://library.stanford.edu/iiif/image-api/1.1/compliance.html#level2", "scale_factors": [1, 2, 4, 8, 16, 32, 64], "tile_height": 1024, "height": 3466, "width": 113793, "tile_width": 1024, "qualities": ["native", "bitonal", "grey", "color"], "formats": ["jpg", "png", "gif"], "@context": "http://library.stanford.edu/iiif/image-api/1.1/context.json", "@id": "http://54.187.32.48/loris/suzhou_orig.jp2"}],
</json>
</annotatable>
"""))
display_name = String(
display_name="Display Name",
help="Display name for this module",
scope=Scope.settings,
default='Image Annotation',
)
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,
default='professor:green,teachingAssistant:blue',
)
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 ImageAnnotationModule(AnnotatableFields, XModule):
'''Image Annotation Module'''
js = {
'coffee': [
resource_string(__name__, 'js/src/javascript_loader.coffee'),
resource_string(__name__, 'js/src/html/display.coffee'),
resource_string(__name__, 'js/src/annotatable/display.coffee'),
],
'js': [
resource_string(__name__, 'js/src/collapsible.js'),
]
}
css = {'scss': [resource_string(__name__, 'css/annotatable/display.scss')]}
icon_class = 'imageannotation'

def __init__(self, *args, **kwargs):
super(ImageAnnotationModule, self).__init__(*args, **kwargs)

xmltree = etree.fromstring(self.data)

self.instructions = self._extract_instructions(xmltree)
self.openseadragonjson = html_to_text(etree.tostring(xmltree.find('json'), 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 <instructions> from the xmltree and returns them as a string, otherwise None. """
return get_instructions(xmltree)

def get_html(self):
""" Renders parameters to template. """
context = {
'display_name': self.display_name_with_default,
'instructions_html': self.instructions,
'annotation_storage': self.annotation_storage_url,
'token': retrieve_token(self.user, self.annotation_token_secret),
'tag': self.instructor_tags,
'openseadragonjson': self.openseadragonjson,
}

return self.system.render_template('imageannotation.html', context)


class ImageAnnotationDescriptor(AnnotatableFields, RawDescriptor): # pylint: disable=abstract-method
''' Image annotation descriptor '''
module_class = ImageAnnotationModule
mako_template = "widgets/raw-edit.html"

@property
def non_editable_metadata_fields(self):
non_editable_fields = super(ImageAnnotationDescriptor, self).non_editable_metadata_fields
non_editable_fields.extend([
ImageAnnotationDescriptor.annotation_storage_url,
ImageAnnotationDescriptor.annotation_token_secret,
])
return non_editable_fields
54 changes: 54 additions & 0 deletions common/lib/xmodule/xmodule/tests/test_annotator_mixin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""
This test will run for annotator_mixin.py
"""

import unittest
from lxml import etree

from xmodule.annotator_mixin import get_instructions, get_extension, html_to_text


class HelperFunctionTest(unittest.TestCase):
"""
Tests to ensure that the following helper functions work for the annotation tool
"""
sample_xml = '''
<annotatable>
<instructions><p>Helper Test Instructions.</p></instructions>
</annotatable>
'''
sample_sourceurl = "http://video-js.zencoder.com/oceans-clip.mp4"
sample_youtubeurl = "http://www.youtube.com/watch?v=yxLIu-scR9Y"
sample_html = '<p><b>Testing here</b> and not bolded here</p>'

def test_get_instructions(self):
"""
Function takes in an input of a specific xml string with surrounding instructions
tags and returns a valid html string.
"""
xmltree = etree.fromstring(self.sample_xml)

expected_xml = u"<div><p>Helper Test Instructions.</p></div>"
actual_xml = get_instructions(xmltree)
self.assertIsNotNone(actual_xml)
self.assertEqual(expected_xml.strip(), actual_xml.strip())

xmltree = etree.fromstring('<annotatable>foo</annotatable>')
actual = get_instructions(xmltree)
self.assertIsNone(actual)

def test_get_extension(self):
"""
Tests whether given a url if the video will return a youtube source or extension
"""
expectedyoutube = 'video/youtube'
expectednotyoutube = 'video/mp4'
result1 = get_extension(self.sample_sourceurl)
result2 = get_extension(self.sample_youtubeurl)
self.assertEqual(expectedyoutube, result2)
self.assertEqual(expectednotyoutube, result1)

def test_html_to_text(self):
expectedtext = "Testing here and not bolded here"
result = html_to_text(self.sample_html)
self.assertEqual(expectedtext, result)
7 changes: 5 additions & 2 deletions common/lib/xmodule/xmodule/tests/test_annotator_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@ class TokenRetriever(unittest.TestCase):
"""
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.
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")

# because the middle hashes are dependent on time, conly the header and footer are checked for secret key
self.assertEqual(expected.split('.')[0], response.split('.')[0])
self.assertNotEqual(expected.split('.')[2], response.split('.')[2])
self.assertNotEqual(expected.split('.')[2], response.split('.')[2])
78 changes: 78 additions & 0 deletions common/lib/xmodule/xmodule/tests/test_imageannotation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# -*- coding: utf-8 -*-
"""Test for Image Annotation Xmodule functional logic."""

import unittest
from mock import Mock
from lxml import etree

from xblock.field_data import DictFieldData
from xblock.fields import ScopeIds

from xmodule.imageannotation_module import ImageAnnotationModule

from . import get_test_system


class ImageAnnotationModuleTestCase(unittest.TestCase):
''' Image Annotation Module Test Case '''
sample_xml = '''
<annotatable>
<instructions><p>Image Test Instructions.</p></instructions>
<json>
navigatorSizeRatio: 0.25,
wrapHorizontal: false,
showNavigator: true,
navigatorPosition: "BOTTOM_LEFT",
showNavigationControl: true,
tileSources: [{
Image: {
xmlns: "http://schemas.microsoft.com/deepzoom/2009",
Url: "http://static.seadragon.com/content/misc/milwaukee_files/",
TileSize: "254",
Overlap: "1",
Format: "jpg",
ServerFormat: "Default",
Size: {
Width: "15497",
Height: "5378"
}
}
},],
</json>
</annotatable>
'''

def setUp(self):
"""
Makes sure that the Module is declared and mocked with the sample xml above.
"""
self.mod = ImageAnnotationModule(
Mock(),
get_test_system(),
DictFieldData({'data': self.sample_xml}),
ScopeIds(None, None, None, None)
)

def test_extract_instructions(self):
"""
Tests to make sure that the instructions are correctly pulled from the sample xml above.
It also makes sure that if no instructions exist, that it does in fact return nothing.
"""
xmltree = etree.fromstring(self.sample_xml)

expected_xml = u"<div><p>Image Test Instructions.</p></div>"
actual_xml = self.mod._extract_instructions(xmltree) # pylint: disable=protected-access
self.assertIsNotNone(actual_xml)
self.assertEqual(expected_xml.strip(), actual_xml.strip())

xmltree = etree.fromstring('<annotatable>foo</annotatable>')
actual = self.mod._extract_instructions(xmltree) # pylint: disable=protected-access
self.assertIsNone(actual)

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', 'instructions_html', 'annotation_storage', 'token', 'tag', 'openseadragonjson']:
self.assertIn(key, context)
2 changes: 1 addition & 1 deletion common/lib/xmodule/xmodule/tests/test_videoannotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,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() # pylint: disable=W0212
context = self.mod.get_html()
for key in ['display_name', 'instructions_html', 'sourceUrl', 'typeSource', 'poster', 'annotation_storage']:
self.assertIn(key, context)
Loading