Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
5c8b9cd
annotation tools
danielcebrian Jan 16, 2014
d1a150d
First set of fixes from the pull request
lduarte1991 Jan 17, 2014
667203b
Deleted token line in api.py and added test for token generator
lduarte1991 Jan 17, 2014
a6ab62c
Added notes_spec.coffee
lduarte1991 Jan 17, 2014
1d087e2
remove spec file
danielcebrian Jan 17, 2014
ff0983c
fixed minor error with the test
danielcebrian Jan 17, 2014
35c8000
fixes some quality errors
danielcebrian Jan 17, 2014
63e8260
fixed unit test
danielcebrian Jan 21, 2014
8360060
fixed unit test
danielcebrian Jan 21, 2014
5af91ef
added advanced module
danielcebrian Jan 22, 2014
a9a92c2
Added notes_spec.coffee
lduarte1991 Jan 17, 2014
4e7a420
remove spec file
danielcebrian Jan 17, 2014
6098902
Quality and Testing Coverage
lduarte1991 Jan 22, 2014
c52669b
Added Secret Token data input
lduarte1991 Jan 22, 2014
e39d51d
fixed token generator
danielcebrian Jan 23, 2014
1138dfe
Annotation Tools in Place
lduarte1991 Jan 24, 2014
03a7197
incorporated lib for traslate
danielcebrian Jan 24, 2014
88014a2
fixed quality errors
danielcebrian Jan 24, 2014
60beeb7
fixed my notes with catch token
danielcebrian Jan 24, 2014
ccc686a
Clearing the code
danielcebrian Jan 25, 2014
54321cc
added licenses
danielcebrian Jan 27, 2014
4e9a549
fixed annotation edition
danielcebrian Jan 27, 2014
8ea802e
fix W0223 warning
danielcebrian Jan 27, 2014
a62ef9d
changed token_secret for annotation_token_secret
danielcebrian Jan 27, 2014
9b15ce8
Update edx-annotator.css
danielcebrian Jan 27, 2014
eb5fab8
fixed issue with css for static images in separated hosts
danielcebrian Jan 29, 2014
63c84fe
annotation tools
danielcebrian Jan 16, 2014
d5f88b0
rebase
danielcebrian Feb 4, 2014
750a515
fixed an error in catch for firefox
danielcebrian Feb 4, 2014
f92ae77
Removed console logs in annotator plugin files and removed support fo…
lduarte1991 Feb 4, 2014
dc91aa2
Used url parse to get extension
lduarte1991 Feb 4, 2014
71857a6
Got rid of more console.log from tools going into production
lduarte1991 Feb 4, 2014
df92827
Trying to do feedback_prompt for confirming deletion
lduarte1991 Feb 4, 2014
de0e728
fixed firefox bug
danielcebrian Feb 5, 2014
5835ecf
Stop using JavaScript to set the body CSS class
tusbar Feb 5, 2014
c49f308
fixed redirect between units
danielcebrian Feb 5, 2014
211827b
fixed a warning of annotator undefined in cms
danielcebrian Feb 5, 2014
07bb623
fixed conflict between Annotator and internationalization
danielcebrian Feb 5, 2014
2a88dc6
Not possible to do feedback.js in LMS
lduarte1991 Feb 5, 2014
d408310
Changed tags variable so as not to conflict with XBlock's tag variable
lduarte1991 Feb 5, 2014
9f12006
Added Back element_id for more precise URI reference.
lduarte1991 Feb 5, 2014
aafc2ed
Merge branch 'lduarte-annotation-retarget'
lduarte1991 Feb 5, 2014
f900e8b
add missing people to AUTHORS
Feb 2, 2014
2aab8f8
Pull THEME_NAME from envs tokens if it's there.
feanil Feb 5, 2014
ac4d3e1
Disabled flaky JS transcript test
Feb 6, 2014
3336cc7
Merge pull request #2490 from edx/will/disable-js-transcripts-test
Feb 6, 2014
c82e4ed
Merge pull request #2492 from edx/will/really-fix-tab-nav-test
Feb 6, 2014
e64ab5e
Merge pull request #2417 from edx/ned/add-missing-people-to-authors
nedbat Feb 6, 2014
4636b27
Merge pull request #2494 from edx/feanil/fix_cms_gather_assets
feanil Feb 6, 2014
a41bdb4
Merge pull request #2476 from ionis-education-group/bodyclass-without-js
talbs Feb 6, 2014
b8e1e74
Change the element id back to the unit id tab
lduarte1991 Feb 6, 2014
e21118f
annotation tools
danielcebrian Jan 16, 2014
ebe0749
First set of fixes from the pull request
lduarte1991 Jan 17, 2014
c876e33
Deleted token line in api.py and added test for token generator
lduarte1991 Jan 17, 2014
174604c
Added notes_spec.coffee
lduarte1991 Jan 17, 2014
5773fe3
fixed unit test
danielcebrian Jan 21, 2014
85598fc
remove spec file
danielcebrian Jan 17, 2014
e6591d1
fix W0223 warning
danielcebrian Jan 27, 2014
471b081
Used url parse to get extension
lduarte1991 Feb 4, 2014
a904358
Got rid of more console.log from tools going into production
lduarte1991 Feb 4, 2014
2027bcd
Trying to do feedback_prompt for confirming deletion
lduarte1991 Feb 4, 2014
1e6ee71
fixed firefox bug
danielcebrian Feb 5, 2014
f9cfaa1
fixed redirect between units
danielcebrian Feb 5, 2014
18307e6
fixed a warning of annotator undefined in cms
danielcebrian Feb 5, 2014
2a5e932
fixed conflict between Annotator and internationalization
danielcebrian Feb 5, 2014
64a8e5a
Not possible to do feedback.js in LMS
lduarte1991 Feb 5, 2014
638abf7
Changed tags variable so as not to conflict with XBlock's tag variable
lduarte1991 Feb 5, 2014
b88bc4b
Merge branch 'master' of https://github.com/danielcebrian/edx-platform
lduarte1991 Feb 6, 2014
220f914
Merged Fixes
lduarte1991 Feb 6, 2014
18c7787
Merge branch 'pr/3'
lduarte1991 Feb 6, 2014
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
22 changes: 22 additions & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,25 @@ Matt Drayer <mattdrayer@edx.org>
Cristian Salamea <cristian.salamea@iaen.edu.ec>
Graham Lowe <graham.lowe@gmail.com>
Matt Bachmann <bachmann.matt@gmail.com>
Dave St.Germain <dstgermain@edx.org>
Usman Khalid <symbolist@users.noreply.github.com>
John Kern <kern3020@gmail.com>
John Orr <jorr@google.com>
Mark Hoeber <hoeber@edx.org>
Waheed Ahmed <waheed.ahmed@arbisoft.com>
Javier Orts Peñarrocha <jorts@upv.es>
Stephen Sanchez <steve@edx.org>
Jim Abramson <jsa@edx.org>
Chris Rossi <chris@archimedeanco.com>
Carson Gee <x@carsongee.com>
Oleg Marshev <oleh.marshev@gmail.com>
Sylvia Pearce <spearce@edx.org>
Olga Stroilova <olga@edx.org>
Paul-Olivier Dehaye <pdehaye@gmail.com>
Feanil Patel <feanil@edx.org>
Zubair Afzal <zubair.afzal@arbisoft.com>
Juho Kim <juhokim@edx.org>
Alison Hodges <ahodges@edx.org>
Jane Manning <jmanning@gmail.com>
Toddi Norum <toddi@edx.org>
Xavier Antoviaque <xavier@antoviaque.org>
3 changes: 3 additions & 0 deletions cms/envs/aws.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@

COURSES_WITH_UNSAFE_CODE = ENV_TOKENS.get("COURSES_WITH_UNSAFE_CODE", [])

# Theme overrides
THEME_NAME = ENV_TOKENS.get('THEME_NAME', None)

#Timezone overrides
TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE)

Expand Down
29 changes: 29 additions & 0 deletions cms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,16 @@
'css/vendor/jquery.qtip.min.css',
'js/vendor/markitup/skins/simple/style.css',
'js/vendor/markitup/sets/wiki/style.css',
'css/vendor/ova/edx-annotator.css',
'css/vendor/ova/annotator.css',
'css/vendor/ova/video-js.min.css',
'css/vendor/ova/rangeslider.css',
'css/vendor/ova/share-annotator.css',
'css/vendor/ova/richText-annotator.css',
'css/vendor/ova/tags-annotator.css',
'css/vendor/ova/flagging-annotator.css',
'css/vendor/ova/ova.css',
'js/vendor/ova/catch/css/main.css'
],
'output_filename': 'css/cms-style-vendor.css',
},
Expand Down Expand Up @@ -316,6 +326,25 @@
'output_filename': 'js/cms-modules.js',
'test_order': 1
},
'main_vendor': {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of putting these dependencies here, it would be better to update CMS' RequireJS configuration. See /cms/templates/base.html.

FYI, LMS does not use RequireJS.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These files aren't really used in CMS. You should not be able to create annotations in Studio mode, only do the set up and actually annotate in the LMS. Where would they go if not in this file if we only needed them for the LMS?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is specifically for CMS. It does not impact LMS at all.

LMS has a similar file, and I believe it already has the libraries listed there (in this PR).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I'm sorry, you are correct. We'll remove it from here and the line in cms/urls.py that is associated with it as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you are right. At the beginning, we thought to use annotator in the CMS and finally we decided to put in the LMS. But we are going to remove it.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please look over the rest of the PR and make sure the files are what you want in the final version? When you have addressed all the outstanding issues and feel that the PR is good shape for review, add a comment and I will start reviewing again.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. I am working on this.

'source_filenames': [
'js/vendor/ova/annotator-full.js',
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let @jtauber know about all these 3rd party libraries (and their licenses).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am in contact with him and have given him a list of 3rd party libraries and where the licenses can be found. Thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This library was in edx, but I have updated it.
You can see more information in http://okfnlabs.org/annotator/

'js/vendor/ova/video.dev.js',
'js/vendor/ova/vjs.youtube.js',
'js/vendor/ova/rangeslider.js',
'js/vendor/ova/share-annotator.js',
'js/vendor/ova/tinymce.min.js',
'js/vendor/ova/richText-annotator.js',
'js/vendor/ova/reply-annotator.js',
'js/vendor/ova/tags-annotator.js',
'js/vendor/ova/flagging-annotator.js',
'js/vendor/ova/jquery-Watch.js',
'js/vendor/ova/ova.js',
'js/vendor/ova/catch/js/catch.js',
'js/vendor/ova/catch/js/handlebars-1.1.2.js'
],
'output_filename': 'js/cms-main_vendor.js',
},
}

PIPELINE_COMPILERS = (
Expand Down
3 changes: 2 additions & 1 deletion cms/static/js/spec/transcripts/message_manager_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ function ($, _, Utils, MessageManager, FileUploader, sinon) {
});
});

describe('Render', function () {
// Disabled 2/6/14 after intermittent failure in master
xdescribe('Render', function () {

beforeEach(function () {
spyOn(_,'template').andCallThrough();
Expand Down
3 changes: 3 additions & 0 deletions cms/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@
# ajax view that actually does the work
url(r'^login_post$', 'student.views.login_user', name='login_post'),
url(r'^logout$', 'student.views.logout_user', name='logout'),

#added token
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this comment supposed to mean?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to give information of a new permission to generate a token for the catch (annotation backend)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm not mistaken, with a few changes we've made as of late this comment and the line below will be deleted momentarily.

url(r'^token/', 'student.views.token', name="token"),
)

# restful api
Expand Down
30 changes: 11 additions & 19 deletions common/djangoapps/student/firebase_token_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,25 +30,17 @@ def create_token(secret, data):
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(bytes_info):
'''
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_info)
return encoded.decode('utf-8').replace('=', '')


def _encode_json(obj):
Expand Down
1 change: 1 addition & 0 deletions common/djangoapps/student/tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
change_enrollment, complete_course_mode_info, token, course_from_id)
from student.tests.factories import UserFactory, CourseModeFactory
from student.tests.test_email import mock_render_to_string
from firebase_token_generator import _encode, _encode_json, _sign, _encode_token, create_token

import shoppingcart

Expand Down
34 changes: 7 additions & 27 deletions common/lib/xmodule/xmodule/tests/test_textannotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,66 +9,46 @@
from xblock.fields import ScopeIds

from xmodule.textannotation_module import TextAnnotationModule
from xmodule.modulestore import Location

from . import get_test_system


class TextAnnotationModuleTestCase(unittest.TestCase):
''' text Annotation Module Test Case '''
sample_xml = '''
<annotatable>
<instructions><p>Test Instructions.</p></instructions>
<p>
One Fish. Two Fish.
Red Fish. Blue Fish.

Oh the places you'll go!
</p>
</annotatable>
'''

def setUp(self):
"""
Makes sure that the Module is declared and mocked with the sample xml above.
"""
self.mod = TextAnnotationModule(
Mock(),
get_test_system(),
DictFieldData({'data': self.sample_xml}),
DictFieldData({'data':self.sample_xml}),
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.
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>Test Instructions.</p></div>"
actual_xml = self.mod._extract_instructions(xmltree) # pylint: disable=W0212
actual_xml = self.mod._extract_instructions(xmltree)
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=W0212
actual = self.mod._extract_instructions(xmltree)
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()
context = self.mod.get_html() # pylint: disable=W0212
for key in ['display_name', 'tag', 'source', 'instructions_html', 'content_html', 'annotation_storage']:
self.assertIn(key, context)
141 changes: 18 additions & 123 deletions common/lib/xmodule/xmodule/tests/test_videoannotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,158 +8,53 @@
from xblock.field_data import DictFieldData
from xblock.fields import ScopeIds

from xmodule.videoannotation_module import VideoAnnotationModule
from xmodule.textannotation_module import VideoAnnotationModule
from xmodule.modulestore import Location

from . import get_test_system


class VideoAnnotationModuleTestCase(unittest.TestCase):
''' Video Annotation Module Test Case '''
sample_xml = '''
<annotatable>
<instructions><p>Video 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_sourceURL = "http://video-js.zencoder.com/oceans-clip.mp4"
sample_youtubeURL = "http://www.youtube.com/watch?v=yxLIu-scR9Y"
def setUp(self):
"""
Makes sure that the Video Annotation Module is created.
"""
self.mod = VideoAnnotationModule(
Mock(),
get_test_system(),
DictFieldData({'data': self.sample_xml, 'sourceUrl': self.sample_sourceurl}),
DictFieldData({'data':self.sample_xml, 'sourceUrl':sample_sourceURL}),
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 = '<annotation title="x" body="y" problem="0">test</annotation>'
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 = '<annotation title="x" body="y" problem="0" highlight="{highlight}">test</annotation>'

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 = '<annotation title="x" body="y" problem="0" highlight="{highlight}">test</annotation>'

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('<annotation title="bar" body="foo" problem="0">test</annotation>')

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 = '<span class="annotatable-span highlight highlight-yellow" data-comment-title="x" data-comment-body="y" data-problem-id="0">z</span>'
expected_el = etree.fromstring(expected_html)

actual_el = etree.fromstring('<annotation title="x" body="y" problem="0" highlight="yellow">z</annotation>')
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
formatted from the <instructions> tags. Otherwise, it should return nothing.
"""
xmltree = etree.fromstring(self.sample_xml)

expected_xml = u"<div><p>Video Test Instructions.</p></div>"
actual_xml = self.mod._extract_instructions(xmltree) # pylint: disable=W0212

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=W0212
actual = self.mod._extract_instructions(xmltree)
self.assertIsNone(actual)

def test_get_extension(self):
"""
Tests the function that returns the appropriate extension depending on whether it is
a video from youtube, or one uploaded to the EdX server.
"""
expectedyoutube = 'video/youtube'
expectednotyoutube = 'video/mp4'
result1 = self.mod._get_extension(self.sample_sourceurl) # pylint: disable=W0212
result2 = self.mod._get_extension(self.sample_youtubeurl) # pylint: disable=W0212
self.assertEqual(expectedyoutube, result2)
self.assertEqual(expectednotyoutube, result1)

expectedYoutube = 'video/youtube'
expectedNotYoutube = 'video/mp4'
result1 = self.mod._get_extension(sample_sourceURL)
result2 = self.mod._get_extension(sample_youtubeURL)
self.assertEqual(expectedYoutube, result2)
self.assertEqual(expectedNotYoutube, result1)

def test_get_html(self):
"""
Tests to make sure variables passed in truly exist within the html once it is all rendered.
"""
context = self.mod.get_html()
for key in ['display_name', 'content_html', 'instructions_html', 'sourceUrl', 'typeSource', 'poster', 'alert', 'annotation_storage']:
context = self.mod.get_html() # pylint: disable=W0212
for key in ['display_name', 'instructions_html', 'sourceUrl', 'typeSource', 'poster', 'annotation_storage']:
self.assertIn(key, context)
Loading