Commit e4e4b32c by lduarte1991

Image Annotation Tool: Add linkback functionality to image thumbnails

Changes requested in PR
parent 0318c955
...@@ -147,3 +147,4 @@ David Bodor <david.gabor.bodor@gmail.com> ...@@ -147,3 +147,4 @@ David Bodor <david.gabor.bodor@gmail.com>
Sébastien Hinderer <Sebastien.Hinderer@inria.fr> Sébastien Hinderer <Sebastien.Hinderer@inria.fr>
Kristin Stephens <ksteph@cs.berkeley.edu> Kristin Stephens <ksteph@cs.berkeley.edu>
Ben Patterson <bpatterson@edx.org> Ben Patterson <bpatterson@edx.org>
Luis Duarte <lduarte1991@gmail.com>
...@@ -754,3 +754,5 @@ LMS: Option to email students when enroll/un-enroll them. ...@@ -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 Blades: Added WAI-ARIA markup to the video player controls. These are now fully
accessible by screen readers. accessible by screen readers.
Common: Added advanced_module for annotating images to go with the ones for text and videos.
...@@ -50,6 +50,6 @@ class MLStripper(HTMLParser): ...@@ -50,6 +50,6 @@ class MLStripper(HTMLParser):
def html_to_text(html): def html_to_text(html):
"strips the html tags off of the text to return plaintext" "strips the html tags off of the text to return plaintext"
htmlStripper = MLStripper() htmlstripper = MLStripper()
htmlStripper.feed(html) htmlstripper.feed(html)
return htmlStripper.get_data() return htmlstripper.get_data()
...@@ -23,8 +23,8 @@ def retrieve_token(userid, secret): ...@@ -23,8 +23,8 @@ def retrieve_token(userid, secret):
dtnow = datetime.datetime.now() dtnow = datetime.datetime.now()
dtutcnow = datetime.datetime.utcnow() dtutcnow = datetime.datetime.utcnow()
delta = dtnow - dtutcnow delta = dtnow - dtutcnow
newhour, newmin = divmod((delta.days * 24 * 60 * 60 + delta.seconds + 30) // 60, 60) # pylint: disable=E1103 newhour, newmin = divmod((delta.days * 24 * 60 * 60 + delta.seconds + 30) // 60, 60)
newtime = "%s%+02d:%02d" % (dtnow.isoformat(), newhour, newmin) # pylint: disable=E1103 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 # federated system in the annotation backend server
custom_data = {"issuedAt": newtime, "consumerKey": secret, "userId": userid, "ttl": 86400} custom_data = {"issuedAt": newtime, "consumerKey": secret, "userId": userid, "ttl": 86400}
......
# pylint: disable=W0223
""" """
Module for Image annotations using annotator. Module for Image annotations using annotator.
""" """
...@@ -48,8 +47,18 @@ class AnnotatableFields(object): ...@@ -48,8 +47,18 @@ class AnnotatableFields(object):
scope=Scope.settings, scope=Scope.settings,
default='professor:green,teachingAssistant:blue', 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_storage_url = String(
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") 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): class ImageAnnotationModule(AnnotatableFields, XModule):
...@@ -96,7 +105,7 @@ class ImageAnnotationModule(AnnotatableFields, XModule): ...@@ -96,7 +105,7 @@ class ImageAnnotationModule(AnnotatableFields, XModule):
return self.system.render_template('imageannotation.html', context) return self.system.render_template('imageannotation.html', context)
class ImageAnnotationDescriptor(AnnotatableFields, RawDescriptor): class ImageAnnotationDescriptor(AnnotatableFields, RawDescriptor): # pylint: disable=abstract-method
''' Image annotation descriptor ''' ''' Image annotation descriptor '''
module_class = ImageAnnotationModule module_class = ImageAnnotationModule
mako_template = "widgets/raw-edit.html" mako_template = "widgets/raw-edit.html"
......
...@@ -23,17 +23,18 @@ class HelperFunctionTest(unittest.TestCase): ...@@ -23,17 +23,18 @@ class HelperFunctionTest(unittest.TestCase):
def test_get_instructions(self): 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. 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) xmltree = etree.fromstring(self.sample_xml)
expected_xml = u"<div><p>Helper Test Instructions.</p></div>" expected_xml = u"<div><p>Helper Test Instructions.</p></div>"
actual_xml = get_instructions(xmltree) # pylint: disable=W0212 actual_xml = get_instructions(xmltree)
self.assertIsNotNone(actual_xml) self.assertIsNotNone(actual_xml)
self.assertEqual(expected_xml.strip(), actual_xml.strip()) self.assertEqual(expected_xml.strip(), actual_xml.strip())
xmltree = etree.fromstring('<annotatable>foo</annotatable>') xmltree = etree.fromstring('<annotatable>foo</annotatable>')
actual = get_instructions(xmltree) # pylint: disable=W0212 actual = get_instructions(xmltree)
self.assertIsNone(actual) self.assertIsNone(actual)
def test_get_extension(self): def test_get_extension(self):
...@@ -42,8 +43,8 @@ class HelperFunctionTest(unittest.TestCase): ...@@ -42,8 +43,8 @@ class HelperFunctionTest(unittest.TestCase):
""" """
expectedyoutube = 'video/youtube' expectedyoutube = 'video/youtube'
expectednotyoutube = 'video/mp4' expectednotyoutube = 'video/mp4'
result1 = get_extension(self.sample_sourceurl) # pylint: disable=W0212 result1 = get_extension(self.sample_sourceurl)
result2 = get_extension(self.sample_youtubeurl) # pylint: disable=W0212 result2 = get_extension(self.sample_youtubeurl)
self.assertEqual(expectedyoutube, result2) self.assertEqual(expectedyoutube, result2)
self.assertEqual(expectednotyoutube, result1) self.assertEqual(expectednotyoutube, result1)
......
...@@ -12,9 +12,12 @@ class TokenRetriever(unittest.TestCase): ...@@ -12,9 +12,12 @@ class TokenRetriever(unittest.TestCase):
""" """
def test_token(self): 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" expected = "eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJpc3N1ZWRBdCI6ICIyMDE0LTAyLTI3VDE3OjAwOjQyLjQwNjQ0MSswOjAwIiwgImNvbnN1bWVyS2V5IjogImZha2Vfc2VjcmV0IiwgInVzZXJJZCI6ICJ1c2VybmFtZSIsICJ0dGwiOiA4NjQwMH0.Dx1PoF-7mqBOOSGDMZ9R_s3oaaLRPnn6CJgGGF2A5CQ"
response = retrieve_token("username", "fake_secret") 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.assertEqual(expected.split('.')[0], response.split('.')[0])
self.assertNotEqual(expected.split('.')[2], response.split('.')[2]) self.assertNotEqual(expected.split('.')[2], response.split('.')[2])
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"Test for Image Annotation Xmodule functional logic." """Test for Image Annotation Xmodule functional logic."""
import unittest import unittest
from mock import Mock from mock import Mock
...@@ -61,12 +61,12 @@ class ImageAnnotationModuleTestCase(unittest.TestCase): ...@@ -61,12 +61,12 @@ class ImageAnnotationModuleTestCase(unittest.TestCase):
xmltree = etree.fromstring(self.sample_xml) xmltree = etree.fromstring(self.sample_xml)
expected_xml = u"<div><p>Image Test Instructions.</p></div>" expected_xml = u"<div><p>Image Test Instructions.</p></div>"
actual_xml = self.mod._extract_instructions(xmltree) # pylint: disable=W0212 actual_xml = self.mod._extract_instructions(xmltree) # pylint: disable=protected-access
self.assertIsNotNone(actual_xml) self.assertIsNotNone(actual_xml)
self.assertEqual(expected_xml.strip(), actual_xml.strip()) self.assertEqual(expected_xml.strip(), actual_xml.strip())
xmltree = etree.fromstring('<annotatable>foo</annotatable>') xmltree = etree.fromstring('<annotatable>foo</annotatable>')
actual = self.mod._extract_instructions(xmltree) # pylint: disable=W0212 actual = self.mod._extract_instructions(xmltree) # pylint: disable=protected-access
self.assertIsNone(actual) self.assertIsNone(actual)
def test_get_html(self): def test_get_html(self):
......
...@@ -66,6 +66,6 @@ class VideoAnnotationModuleTestCase(unittest.TestCase): ...@@ -66,6 +66,6 @@ class VideoAnnotationModuleTestCase(unittest.TestCase):
""" """
Tests to make sure variables passed in truly exist within the html once it is all rendered. 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']: for key in ['display_name', 'instructions_html', 'sourceUrl', 'typeSource', 'poster', 'annotation_storage']:
self.assertIn(key, context) self.assertIn(key, context)
...@@ -68,9 +68,9 @@ class VideoAnnotationModule(AnnotatableFields, XModule): ...@@ -68,9 +68,9 @@ class VideoAnnotationModule(AnnotatableFields, XModule):
""" Removes <instructions> from the xmltree and returns them as a string, otherwise None. """ """ Removes <instructions> from the xmltree and returns them as a string, otherwise None. """
return get_instructions(xmltree) return get_instructions(xmltree)
def _get_extension(self, srcurl): def _get_extension(self, src_url):
''' get the extension of a given url ''' ''' get the extension of a given url '''
return get_extension(srcurl) return get_extension(src_url)
def get_html(self): def get_html(self):
""" Renders parameters to template. """ """ Renders parameters to template. """
......
...@@ -16,10 +16,6 @@ ...@@ -16,10 +16,6 @@
font-style: italic; font-style: italic;
} }
.annotator-wrapper .mce-container {
z-index: 3000000000!important; /*To fix full-screen problems*/
}
.mce-container-body { .mce-container-body {
min-width: 400px; min-width: 400px;
} }
...@@ -29,6 +25,10 @@ ...@@ -29,6 +25,10 @@
min-width: 400px; min-width: 400px;
} }
.mce-floatpanel {
z-index: 700000000!important;
}
div.mce-tinymce.mce-container.mce-panel { div.mce-tinymce.mce-container.mce-panel {
min-width:400px; min-width:400px;
} }
......
...@@ -225,7 +225,14 @@ ...@@ -225,7 +225,14 @@
margin-top: 15px; margin-top: 15px;
} }
#mainCatch .playMediaButton { #mainCatch .zoomToImageBounds {
width: 20em;
text-align:center;
margin-top: 15px;
margin-bottom: 5px;
}
#mainCatch .playMediaButton, #mainCatch .zoomToImageBounds {
border-top: 2px solid #d3bd89; border-top: 2px solid #d3bd89;
border-bottom: 2px solid #b3ad69; border-bottom: 2px solid #b3ad69;
border-right: 2px solid #b3ad69; border-right: 2px solid #b3ad69;
...@@ -242,7 +249,7 @@ ...@@ -242,7 +249,7 @@
border-radius: 24px; border-radius: 24px;
color: #000000; color: #000000;
} }
#mainCatch .playMediaButton:hover { #mainCatch .playMediaButton:hover, #mainCatch .zoomToImageBounds:hover {
border-top: 2px solid #d3bd89; border-top: 2px solid #d3bd89;
border-bottom: 2px solid #b3ad69; border-bottom: 2px solid #b3ad69;
border-right: 2px solid #b3ad69; border-right: 2px solid #b3ad69;
...@@ -250,7 +257,7 @@ ...@@ -250,7 +257,7 @@
background: #f5c105; background: #f5c105;
color: #080708; color: #080708;
} }
#mainCatch .playMediaButton:active { #mainCatch .playMediaButton:active, #mainCatch .zoomToImageBounds:active {
border-top: 2px solid #d3bd89; border-top: 2px solid #d3bd89;
border-bottom: 2px solid #b3ad69; border-bottom: 2px solid #b3ad69;
border-right: 2px solid #b3ad69; border-right: 2px solid #b3ad69;
......
...@@ -222,7 +222,11 @@ annotationDetail: ...@@ -222,7 +222,11 @@ annotationDetail:
'</div>'+ '</div>'+
'{{/if}}'+ '{{/if}}'+
'{{#if mediatypeforgrid.image}}'+ '{{#if mediatypeforgrid.image}}'+
'<img src="http://www.paraemigrantes.com/wp-content/themes/daily/images/default-thumb.gif">'+ '<div class="zoomToImageBounds">'+
'<img src="{{{ thumbnailLink }}}">'+
'<span class="idAnnotation" style="display:none">{{{ id }}}</span>'+
'<span class="uri" style="display:none">{{{uri}}}</span>'+
'</div>'+
'{{/if}}'+ '{{/if}}'+
'<div class="body">'+ '<div class="body">'+
'{{{ text }}}'+ '{{{ text }}}'+
...@@ -254,10 +258,6 @@ annotationDetail: ...@@ -254,10 +258,6 @@ annotationDetail:
'{{/if}}'+ '{{/if}}'+
'<div class="controlPanel">'+ '<div class="controlPanel">'+
//'<img class="privacy_button" src="'+root+'privacy_icon.png" width="36" height="36" alt="Privacy Settings" title="Privacy Settings">'+
// '<img class="groups_button" src="'+root+'groups_icon.png" width="36" height="36" alt="Groups Access" title="Groups Access">'+
// '<img class="reply_button" src="'+root+'groups_icon.png" width="36" height="36" alt="Reply" title="Reply" idAnnotation="{{{ id }}}">'+
//'<img class="share_button" src="'+root+'share_icon.png" width="36" height="36" alt="Share Annotation" title="Share Annotation"/>'+
'</div>'+ '</div>'+
'</div>', '</div>',
}; };
...@@ -412,18 +412,19 @@ CatchAnnotation.prototype = { ...@@ -412,18 +412,19 @@ CatchAnnotation.prototype = {
el.off(); el.off();
//Bind functions //Bind functions
var openAnnotationItem = this.__bind(this._openAnnotationItem,this), var openAnnotationItem = this.__bind(this._openAnnotationItem, this),
closeAnnotationItem = this.__bind(this._closeAnnotationItem,this), closeAnnotationItem = this.__bind(this._closeAnnotationItem, this),
onGeolocationClick = this.__bind(this._onGeolocationClick,this), onGeolocationClick = this.__bind(this._onGeolocationClick, this),
onPlaySelectionClick = this.__bind(this._onPlaySelectionClick,this), onPlaySelectionClick = this.__bind(this._onPlaySelectionClick, this),
onShareControlsClick = this.__bind(this._onShareControlsClick,this), onShareControlsClick = this.__bind(this._onShareControlsClick, this),
onSelectionButtonClick = this.__bind(this._onSelectionButtonClick,this), onSelectionButtonClick = this.__bind(this._onSelectionButtonClick, this),
onPublicPrivateButtonClick = this.__bind(this._onPublicPrivateButtonClick,this), onPublicPrivateButtonClick = this.__bind(this._onPublicPrivateButtonClick, this),
onQuoteMediaButton = this.__bind(this._onQuoteMediaButton,this), onQuoteMediaButton = this.__bind(this._onQuoteMediaButton, this),
onControlRepliesClick = this.__bind(this._onControlRepliesClick,this), onControlRepliesClick = this.__bind(this._onControlRepliesClick, this),
onMoreButtonClick = this.__bind(this._onMoreButtonClick,this), onMoreButtonClick = this.__bind(this._onMoreButtonClick, this),
onSearchButtonClick = this.__bind(this._onSearchButtonClick, this), onSearchButtonClick = this.__bind(this._onSearchButtonClick, this),
onDeleteReplyButtonClick = this.__bind(this._onDeleteReplyButtonClick,this); onDeleteReplyButtonClick = this.__bind(this._onDeleteReplyButtonClick, this),
onZoomToImageBoundsButtonClick = this.__bind(this._onZoomToImageBoundsButtonClick, this);
//Open Button //Open Button
el.on("click", ".annotationItem .annotationRow", openAnnotationItem); el.on("click", ".annotationItem .annotationRow", openAnnotationItem);
...@@ -444,6 +445,12 @@ CatchAnnotation.prototype = { ...@@ -444,6 +445,12 @@ CatchAnnotation.prototype = {
el.on("click",".annotationItem .annotationDetail .quote", onQuoteMediaButton); el.on("click",".annotationItem .annotationDetail .quote", onQuoteMediaButton);
} }
//IMAGE
if (this.options.media=='image') {
//PlaySelection button
el.on("click",".annotationItem .annotationDetail .zoomToImageBounds", onZoomToImageBoundsButtonClick);
}
//controlReplies //controlReplies
el.on("click",".annotationItem .controlReplies", onControlRepliesClick); el.on("click",".annotationItem .controlReplies", onControlRepliesClick);
...@@ -651,6 +658,10 @@ CatchAnnotation.prototype = { ...@@ -651,6 +658,10 @@ CatchAnnotation.prototype = {
item.mediatypeforgrid = {}; item.mediatypeforgrid = {};
item.mediatypeforgrid[item.media] = true; item.mediatypeforgrid[item.media] = true;
if (item.mediatypeforgrid.image) {
item.thumbnailLink = item.target.thumb;
};
//Flags //Flags
if(!this.options.flags && typeof item.tags != 'undefined' && item.tags.length > 0){ if(!this.options.flags && typeof item.tags != 'undefined' && item.tags.length > 0){
for(var len=item.tags.length, index = len-1; index >= 0; --index){ for(var len=item.tags.length, index = len-1; index >= 0; --index){
...@@ -751,6 +762,26 @@ CatchAnnotation.prototype = { ...@@ -751,6 +762,26 @@ CatchAnnotation.prototype = {
} }
} }
}, },
_onZoomToImageBoundsButtonClick: function(evt){
var zoomToBounds = $(evt.target).hasClass('zoomToImageBounds')?$(evt.target):$(evt.target).parents('.zoomToImageBounds:first'),
osdaId = zoomToBounds.find('.idAnnotation').html(),
uri = zoomToBounds.find('.uri').html();
var allannotations = this.annotator.plugins['Store'].annotations,
osda = this.annotator.osda;
for(var item in allannotations){
var an = allannotations[item];
if (typeof an.id!='undefined' && an.id == osdaId){//this is the annotation
var bounds = new OpenSeadragon.Rect(an.bounds.x, an.bounds.y, an.bounds.width, an.bounds.height);
osda.viewer.viewport.fitBounds(bounds, false);
console.log(an.target.container);
$('html,body').animate({scrollTop: $("#"+an.target.container).offset().top},
'slow');
}
}
},
_onQuoteMediaButton: function(evt){ _onQuoteMediaButton: function(evt){
var quote = $(evt.target).hasClass('quote')?$(evt.target):$(evt.target).parents('.quote:first'), var quote = $(evt.target).hasClass('quote')?$(evt.target):$(evt.target).parents('.quote:first'),
id = quote.find('.idAnnotation').html(), id = quote.find('.idAnnotation').html(),
......
...@@ -78,7 +78,6 @@ Annotator.Plugin.RichText = (function(_super) { ...@@ -78,7 +78,6 @@ Annotator.Plugin.RichText = (function(_super) {
annotator.subscribe("annotationEditorShown", function(){ annotator.subscribe("annotationEditorShown", function(){
$(annotator.editor.element).find('.mce-tinymce')[0].style.display='block'; $(annotator.editor.element).find('.mce-tinymce')[0].style.display='block';
$(annotator.editor.element).find('.mce-container').css('z-index',3000000000);
annotator.editor.checkOrientation(); annotator.editor.checkOrientation();
}); });
annotator.subscribe("annotationEditorHidden", function(){ annotator.subscribe("annotationEditorHidden", function(){
...@@ -91,9 +90,6 @@ Annotator.Plugin.RichText = (function(_super) { ...@@ -91,9 +90,6 @@ Annotator.Plugin.RichText = (function(_super) {
//set the modification in the textarea of annotator //set the modification in the textarea of annotator
$(editor.element).find('textarea')[0].value = tinymce.activeEditor.getContent(); $(editor.element).find('textarea')[0].value = tinymce.activeEditor.getContent();
}); });
ed.on('Init', function(ed){
$('.mce-container').css('z-index','3090000000000000000');
});
//New button to add Rubrics of the url https://gteavirtual.org/rubric //New button to add Rubrics of the url https://gteavirtual.org/rubric
ed.addButton('rubric', { ed.addButton('rubric', {
icon: 'rubric', icon: 'rubric',
......
...@@ -35,6 +35,7 @@ ...@@ -35,6 +35,7 @@
<script type="text/javascript" src="${static.url('js/vendor/tinymce/js/tinymce/jquery.tinymce.min.js', raw=True)}" /> <script type="text/javascript" src="${static.url('js/vendor/tinymce/js/tinymce/jquery.tinymce.min.js', raw=True)}" />
</div> </div>
<div id="catchDIV"> <div id="catchDIV">
## Translators: Notes below refer to annotations. They wil later be put under a "Notes" section.
<div class="annotationListContainer">${_('You do not have any notes.')}</div> <div class="annotationListContainer">${_('You do not have any notes.')}</div>
</div> </div>
</div> </div>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment