diff --git a/cms/djangoapps/models/settings/course_metadata.py b/cms/djangoapps/models/settings/course_metadata.py index 2f22ba400f8c01916ae186d7538dc7e8fc56c474..7d37c193a5fd77ff469f5709092e7dab3b0178bd 100644 --- a/cms/djangoapps/models/settings/course_metadata.py +++ b/cms/djangoapps/models/settings/course_metadata.py @@ -78,6 +78,9 @@ class CourseMetadata(object): if not settings.FEATURES.get('ENABLE_TEAMS'): filtered_list.append('teams_configuration') + if not settings.FEATURES.get('ENABLE_VIDEO_BUMPER'): + filtered_list.append('video_bumper') + return filtered_list @classmethod diff --git a/cms/envs/bok_choy.py b/cms/envs/bok_choy.py index df8d0b73b16a7be929fcbb3c6f804e0a5d12475b..dcd120abf18acf4455d2e8c92a2fee489f8bbca9 100644 --- a/cms/envs/bok_choy.py +++ b/cms/envs/bok_choy.py @@ -74,6 +74,9 @@ FEATURES['ENABLE_TEAMS'] = True # Enable custom content licensing FEATURES['LICENSING'] = True +FEATURES['ENABLE_MOBILE_REST_API'] = True # Enable video bumper in Studio +FEATURES['ENABLE_VIDEO_BUMPER'] = True # Enable video bumper in Studio settings + ########################### Entrance Exams ################################# FEATURES['ENTRANCE_EXAMS'] = True diff --git a/cms/envs/common.py b/cms/envs/common.py index 39a3008c307508d060c4a05b8ad95e345c4f88e9..f8d9e27354ce5985a9f64c36bdfa1dbe439cfbad 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -163,6 +163,13 @@ FEATURES = { # Teams feature 'ENABLE_TEAMS': False, + + # Show video bumper in Studio + 'ENABLE_VIDEO_BUMPER': False, + + # How many seconds to show the bumper again, default is 7 days: + 'SHOW_BUMPER_PERIODICITY': 7 * 24 * 3600, + } ENABLE_JASMINE = False @@ -645,6 +652,8 @@ YOUTUBE = { 'v': 'set_youtube_id_of_11_symbols_here', }, }, + + 'IMAGE_API': 'http://img.youtube.com/vi/{youtube_id}/0.jpg', # /maxresdefault.jpg for 1920*1080 } ############################# VIDEO UPLOAD PIPELINE ############################# diff --git a/common/lib/xmodule/xmodule/css/video/accessible_menu.scss b/common/lib/xmodule/xmodule/css/video/accessible_menu.scss index 3c283df770677eaf5dbb5f13960864a13a5e762f..05739ef2ade412cd806118d0a34b49dd1636a55c 100644 --- a/common/lib/xmodule/xmodule/css/video/accessible_menu.scss +++ b/common/lib/xmodule/xmodule/css/video/accessible_menu.scss @@ -22,7 +22,7 @@ $a11y--blue-s1: saturate($blue,15%); } .a11y-menu-list { - @extend %ui-depth1; + @extend %ui-depth3; top: 100%; margin: 0; padding: 0; diff --git a/common/lib/xmodule/xmodule/css/video/display.scss b/common/lib/xmodule/xmodule/css/video/display.scss index 8bbfdd1b2588959c94b70b656d766ddc4f3100bf..eb7ae8414968085a80d939dc9c6da002025f486f 100644 --- a/common/lib/xmodule/xmodule/css/video/display.scss +++ b/common/lib/xmodule/xmodule/css/video/display.scss @@ -27,6 +27,23 @@ div.video { } } + // CASE: video pre-roll state + &.is-pre-roll { + .slider { + visibility: hidden; + } + + .video-player { + position: relative; + &:before { + display: block; + content: ""; + width: 100%; + padding-top: 55%; + } + } + } + div.tc-wrapper { @include clearfix(); position: relative; @@ -169,6 +186,7 @@ div.video { } object, iframe, video { + display: block; border: none; width: 100%; } @@ -282,7 +300,7 @@ div.video { } } - ul.vcr { + .vcr { float: left; list-style: none; margin: 0 lh() 0 0; @@ -293,49 +311,52 @@ div.video { font-size: em(14); } - li { + .video_control { + @extend %video-button; float: left; - margin-bottom: 0; + background-image: url('../images/vcr.png'); + background-position: 15px 15px ; + background-repeat: no-repeat; + border-left: none; + padding: 0 lh(.75); + width: 14px; - a { - @extend %video-button; - background-image: url('../images/vcr.png'); - background-position: 15px 15px ; - background-repeat: no-repeat; - border-left: none; - box-shadow: 1px 0 0 #555; - padding: 0 lh(.75); - width: 14px; - - &:focus { - @extend %ui-depth4; - position: relative; - outline: $white dotted thin; - outline-offset: -2px; - } + &:focus { + @extend %ui-depth4; + position: relative; + outline: $white dotted thin; + outline-offset: -2px; + } - &:empty { - height: 46px; - background-position: 15px 15px; - } + &:empty { + height: 46px; + background-position: 15px 15px; + } - &.play { - background-position: 17px -114px; - } + &.play { + background-position: 17px -114px; + } - &.pause { - background-position: 16px -50px; - } + &.pause { + background-position: 16px -50px; } - div.vidtime { - font-weight: bold; - line-height: 46px; //height of play pause buttons - -webkit-font-smoothing: antialiased; - padding-left: lh(.75); - @media (max-width: 1120px) { - padding-left: lh(0.5); - } + &.skip { + background-image: none; + text-indent: 0; + width: initial; + white-space: nowrap; + } + } + + div.vidtime { + @extend %t-strong; + float: left; + line-height: 46px; //height of play pause buttons + -webkit-font-smoothing: antialiased; + padding-left: lh(.75); + @media (max-width: 1120px) { + padding-left: lh(0.5); } } } @@ -504,11 +525,14 @@ div.video { background-image: url('../images/volume.png'); background-position: 10px center; background-repeat: no-repeat; - border-left: none; width: 30px; height: 46px; } + &:not(:first-child) > a { + border-left: none; + } + .volume-slider-container { @include transition(none); @extend %ui-depth1; @@ -686,8 +710,7 @@ div.video { } ol.subtitles { - width: 0; - height: 0; + @extend .is-hidden; } ol.subtitles.html5 { @@ -792,13 +815,38 @@ div.video { &.is-touch { div.tc-wrapper { article.video-wrapper { - object, iframe, video{ + object, iframe, video { width: 100%; height: 100%; } } } } + + .video-pre-roll { + @extend %ui-depth3; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-position: 50% 50%; + background-repeat: no-repeat; + background-size: 100%; + background-color: $black; + + &.is-html5 { + background-size: 15%; + } + + .btn-play { + text-indent: -999px; + overflow: hidden; + border: none; + box-shadow: none; + line-height: 0; + } + } } diff --git a/common/lib/xmodule/xmodule/js/fixtures/poster.jpg b/common/lib/xmodule/xmodule/js/fixtures/poster.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7f987f944e2dc670f2687b216cdead3579effc8d Binary files /dev/null and b/common/lib/xmodule/xmodule/js/fixtures/poster.jpg differ diff --git a/common/lib/xmodule/xmodule/js/fixtures/video.html b/common/lib/xmodule/xmodule/js/fixtures/video.html index e1fe11ae4736ccd83c9fb890fb553213568a85a5..dabb3801b97fb7e4c8e0862a18744a3f91b99033 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/video.html +++ b/common/lib/xmodule/xmodule/js/fixtures/video.html @@ -4,22 +4,7 @@ <div id="video_id" class="video closed" - data-streams="0.5:7tqY6eQzVhE,1.0:cogebirgzzM,1.5:abcdefghijkl" - data-show-captions="true" - data-save-state-url="/save_user_state" - data-speed="1.5" - data-start="" - data-end="" - data-saved-video-position="0" - data-transcript-language="en" - data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通è¯"}' - data-transcript-translation-url="/transcript/translation" - data-transcript-available-translations-url="/transcript/available_translations" - data-autoplay="False" - data-yt-test-timeout="1500" - data-yt-api-url="www.youtube.com/iframe_api" - data-yt-test-url="gdata.youtube.com/feeds/api/videos/" - data-autohide-html5="True" + data-metadata='{"autohideHtml5": "true", "autoplay": "false", "captionDataDir": "", "endTime": "", "generalSpeed": "1.0", "saveStateUrl": "/save_user_state", "savedVideoPosition": "0", "showCaptions": "true", "sources": "[]", "speed": "1.5", "startTime": "", "streams": "0.5:7tqY6eQzVhE,1.0:cogebirgzzM,1.5:abcdefghijkl", "sub": "Z5KLxerq05Y", "transcriptAvailableTranslationsUrl": "/transcript/available_translations", "transcriptLanguage": "en", "transcriptLanguages": {"en": "English", "de": "Deutsch", "zh": "普通è¯"}, "transcriptTranslationUrl": "/transcript/translation/__lang__", "ytApiUrl": "www.youtube.com/iframe_api", "ytImageUrl": "", "ytTestTimeout": "1500", "ytTestUrl": "gdata.youtube.com/feeds/api/videos/"}' > <div class="focus_grabber first"></div> @@ -35,35 +20,11 @@ <section class="video-controls is-hidden"> <div class="slider"></div> <div> - <ul class="vcr"> - <li><a class="video_control" href="#" title="Play" role="button" aria-disabled="false"></a></li> - <li><div class="vidtime">0:00 / 0:00</div></li> - </ul> - <div class="secondary-controls"> - <div class="speeds"> - <a class="speed-button" href="#" title="Speeds" role="button" aria-disabled="false"> - <span class="label">Speed</span> - <span class="value"></span> - </a> - <ol class="video-speeds"></ol> - </div> - <div class="volume"> - <a href="#" title="Volume" role="button" aria-disabled="false"></a> - <div class="volume-slider-container"> - <div class="volume-slider"></div> - </div> - </div> - <a href="#" class="add-fullscreen" title="Fill browser" role="button" aria-disabled="false">Fill Browser</a> - <a href="#" class="quality-control is-hidden" title="HD off" role="button" aria-disabled="false">HD off</a> - <div class="lang menu-container"> - <a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a> - </div> - </div> + <div class="vcr"><div class="vidtime">0:00 / 0:00</div></div> + <div class="secondary-controls"></div> </div> </section> </article> - - <ol class="subtitles"><li></li></ol> </div> <div class="focus_grabber last"></div> diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_all.html b/common/lib/xmodule/xmodule/js/fixtures/video_all.html index 1b0727d3d797937927a55b7b313a2da2d6e002c5..617d9583576084cba5e7385c14a3a1b493a788ef 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/video_all.html +++ b/common/lib/xmodule/xmodule/js/fixtures/video_all.html @@ -4,23 +4,7 @@ <div id="video_id" class="video closed" - data-show-captions="true" - data-save-state-url="/save_user_state" - data-speed="1.5" - data-start="" - data-end="" - data-saved-video-position="0" - data-transcript-language="en" - data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通è¯"}' - data-transcript-translation-url="/transcript/translation" - data-transcript-available-translations-url="/transcript/available_translations" - data-sub="Z5KLxerq05Y" - data-sources='["xmodule/include/fixtures/test.mp4","xmodule/include/fixtures/test.webm","xmodule/include/fixtures/test.ogv"]' - data-autoplay="False" - data-yt-test-timeout="1500" - data-yt-api-url="www.youtube.com/iframe_api" - data-yt-test-url="gdata.youtube.com/feeds/api/videos/" - data-autohide-html5="True" + data-metadata='{"autohideHtml5": "true", "autoplay": "false", "captionDataDir": "", "endTime": "", "generalSpeed": "1.0", "saveStateUrl": "/save_user_state", "savedVideoPosition": "0", "showCaptions": "true", "sources": ["xmodule/include/fixtures/test.mp4","xmodule/include/fixtures/test.webm","xmodule/include/fixtures/test.ogv"], "speed": "1.5", "startTime": "", "streams": "", "sub": "Z5KLxerq05Y", "transcriptAvailableTranslationsUrl": "/transcript/available_translations", "transcriptLanguage": "en", "transcriptLanguages": {"en": "English", "de": "Deutsch", "zh": "普通è¯"}, "transcriptTranslationUrl": "/transcript/translation/__lang__", "ytApiUrl": "www.youtube.com/iframe_api", "ytImageUrl": "", "ytTestTimeout": "1500", "ytTestUrl": "gdata.youtube.com/feeds/api/videos/"}' > <div class="focus_grabber first"></div> @@ -36,35 +20,11 @@ <section class="video-controls is-hidden"> <div class="slider"></div> <div> - <ul class="vcr"> - <li><a class="video_control" href="#" title="Play" role="button" aria-disabled="false"></a></li> - <li><div class="vidtime">0:00 / 0:00</div></li> - </ul> - <div class="secondary-controls"> - <div class="speeds"> - <a class="speed-button" href="#" title="Speeds" role="button" aria-disabled="false"> - <span class="label">Speed</span> - <span class="value"></span> - </a> - <ol class="video-speeds"></ol> - </div> - <div class="volume"> - <a href="#" title="Volume" role="button" aria-disabled="false"></a> - <div class="volume-slider-container"> - <div class="volume-slider"></div> - </div> - </div> - <a href="#" class="add-fullscreen" title="Fill browser" role="button" aria-disabled="false">Fill Browser</a> - <a href="#" class="quality-control is-hidden" title="HD off" role="button" aria-disabled="false">HD off</a> - <div class="lang menu-container"> - <a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a> - </div> - </div> + <div class="vcr"><div class="vidtime">0:00 / 0:00</div></div> + <div class="secondary-controls"></div> </div> </section> </article> - - <ol class="subtitles"><li></li></ol> </div> <div class="focus_grabber last"></div> diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_html5.html b/common/lib/xmodule/xmodule/js/fixtures/video_html5.html index 678803a90ddeee2a4c72b3624438aa4c1e2d8d93..47be4f04fcb9461ac235f8b6eaed668d0cef4a9f 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/video_html5.html +++ b/common/lib/xmodule/xmodule/js/fixtures/video_html5.html @@ -4,23 +4,7 @@ <div id="video_id" class="video closed" - data-show-captions="true" - data-save-state-url="/save_user_state" - data-speed="1.5" - data-start="" - data-end="" - data-saved-video-position="0" - data-transcript-language="en" - data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通è¯"}' - data-transcript-translation-url="/transcript/translation" - data-transcript-available-translations-url="/transcript/available_translations" - data-sub="Z5KLxerq05Y" - data-sources='["xmodule/include/fixtures/test.mp4","xmodule/include/fixtures/test.webm","xmodule/include/fixtures/test.ogv"]' - data-autoplay="False" - data-yt-test-timeout="1500" - data-yt-api-url="www.youtube.com/iframe_api" - data-yt-test-url="gdata.youtube.com/feeds/api/videos/" - data-autohide-html5="True" + data-metadata='{"autohideHtml5": "true", "autoplay": "false", "captionDataDir": "", "endTime": "", "generalSpeed": "1.0", "saveStateUrl": "/save_user_state", "savedVideoPosition": "0", "showCaptions": "true", "sources": ["xmodule/include/fixtures/test.mp4","xmodule/include/fixtures/test.webm","xmodule/include/fixtures/test.ogv"], "speed": "1.5", "startTime": "", "streams": "", "sub": "Z5KLxerq05Y", "transcriptAvailableTranslationsUrl": "/transcript/available_translations", "transcriptLanguage": "en", "transcriptLanguages": {"en": "English", "de": "Deutsch", "zh": "普通è¯"}, "transcriptTranslationUrl": "/transcript/translation/__lang__", "ytApiUrl": "www.youtube.com/iframe_api", "ytImageUrl": "", "ytTestTimeout": "1500", "ytTestUrl": "gdata.youtube.com/feeds/api/videos/", "source": "", "html5_sources": ["http://youtu.be/3_yD_cEKoCk.mp4"]}' > <div class="focus_grabber first"></div> @@ -33,8 +17,6 @@ </section> <section class="video-controls is-hidden"></section> </article> - - <ol class="subtitles"><li></li></ol> </div> <div class="focus_grabber last"></div> diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_no_captions.html b/common/lib/xmodule/xmodule/js/fixtures/video_no_captions.html index a34df976bfb695724d7f997ad4f471d3102b6e58..77017d403dd16084d8c8e11544b73c046d6b28a1 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/video_no_captions.html +++ b/common/lib/xmodule/xmodule/js/fixtures/video_no_captions.html @@ -4,22 +4,7 @@ <div id="video_id" class="video closed" - data-streams="0.5:7tqY6eQzVhE,1.0:cogebirgzzM,1.5:abcdefghijkl" - data-show-captions="false" - data-save-state-url="/save_user_state" - data-speed="1.5" - data-start="" - data-end="" - data-saved-video-position="0" - data-transcript-language="en" - data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通è¯"}' - data-transcript-translation-url="/transcript/translation" - data-transcript-available-translations-url="/transcript/available_translations" - data-autoplay="False" - data-yt-test-timeout="1500" - data-yt-api-url="www.youtube.com/iframe_api" - data-yt-test-url="gdata.youtube.com/feeds/api/videos/" - data-autohide-html5="True" + data-metadata='{"streams":"0.5:7tqY6eQzVhE,1.0:cogebirgzzM,1.5:abcdefghijkl", "showCaptions": false, "saveStateUrl": "/save_user_state", "savedVideoPosition": "0", "speed": "1.5", "startTime": "", "end": "", "transcriptAvailableTranslationsUrl": "/transcript/available_translations", "transcriptLanguage": "en", "transcriptLanguages": {"en": "English", "de": "Deutsch", "zh": "普通è¯"}, "transcriptTranslationUrl": "/transcript/translation/__lang__", "ytApiUrl": "www.youtube.com/iframe_api", "ytTestTimeout": "1500", "ytTestUrl": "gdata.youtube.com/feeds/api/videos/"}' > <div class="focus_grabber first"></div> diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_with_bumper.html b/common/lib/xmodule/xmodule/js/fixtures/video_with_bumper.html new file mode 100644 index 0000000000000000000000000000000000000000..22bd2062689fdf203f788cfab7f81dd4cf5d8eaf --- /dev/null +++ b/common/lib/xmodule/xmodule/js/fixtures/video_with_bumper.html @@ -0,0 +1,36 @@ +<div class="course-content"> + <div id="video_example"> + <div id="example"> + <div + id="video_id" + class="video closed" + data-metadata='{"autohideHtml5": "true", "autoplay": "false", "captionDataDir": "", "endTime": "", "generalSpeed": "1.0", "saveStateUrl": "/save_user_state", "savedVideoPosition": "0", "showCaptions": "true", "sources": ["xmodule/include/fixtures/test.mp4","xmodule/include/fixtures/test.webm","xmodule/include/fixtures/test.ogv"], "speed": "1.5", "startTime": "", "streams": "", "sub": "Z5KLxerq05Y", "transcriptAvailableTranslationsUrl": "/transcript/available_translations", "transcriptLanguage": "en", "transcriptLanguages": {"en": "English", "de": "Deutsch", "zh": "普通è¯"}, "transcriptTranslationUrl": "/transcript/translation/__lang__", "ytApiUrl": "www.youtube.com/iframe_api", "ytImageUrl": "", "ytTestTimeout": "1500", "ytTestUrl": "gdata.youtube.com/feeds/api/videos/"}' + data-bumper-metadata='{"transcriptLanguage": "en", "showCaptions": "true", "transcriptLanguages": {"en": "English", "de": "Deutsch", "zh": "普通è¯"}, "sources": ["xmodule/include/fixtures/test.mp4","xmodule/include/fixtures/test.webm","xmodule/include/fixtures/test.ogv"], "transcriptTranslationUrl": "/transcript/translation/__lang__/?is_bumper=1", "transcriptAvailableTranslationsUrl": "/transcript/available_translations/?is_bumper=1", "streams": "", "saveStateUrl": "/save_user_state"}' + data-poster='{"url": "xmodule/include/fixtures/poster.jpg", "type": "youtube"}' + > + <div class="focus_grabber first"></div> + + <div class="tc-wrapper"> + <article class="video-wrapper"> + <span tabindex="0" class="spinner" aria-hidden="false" aria-label="${_('Loading video player')}"></span> + <span tabindex="-1" class="btn-play is-hidden" aria-hidden="true" aria-label="${_('Play video')}"></span> + <div class="video-player-pre"></div> + <section class="video-player"> + <iframe id="id"></iframe> + </section> + <div class="video-player-post"></div> + <section class="video-controls is-hidden"> + <div class="slider"></div> + <div> + <div class="vcr"><div class="vidtime">0:00 / 0:00</div></div> + <div class="secondary-controls"></div> + </div> + </section> + </article> + </div> + + <div class="focus_grabber last"></div> + </div> + </div> + </div> +</div> diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_yt_multiple.html b/common/lib/xmodule/xmodule/js/fixtures/video_yt_multiple.html index 8086c2b2691af07ace19552217f92ddb54088dbe..8842b1e5926f6c5e45dd0e97cbb2d632a5fd1b01 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/video_yt_multiple.html +++ b/common/lib/xmodule/xmodule/js/fixtures/video_yt_multiple.html @@ -4,22 +4,7 @@ <div id="video_id1" class="video closed" - data-streams="0.5:7tqY6eQzVhE,1.0:cogebirgzzM,1.5:abcdefghijkl" - data-show-captions="true" - data-save-state-url="/save_user_state" - data-speed="1.5" - data-start="" - data-end="" - data-saved-video-position="0" - data-transcript-language="en" - data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通è¯"}' - data-transcript-translation-url="/transcript/translation" - data-transcript-available-translations-url="/transcript/available_translations" - data-autoplay="False" - data-yt-test-timeout="1500" - data-yt-api-url="www.youtube.com/iframe_api" - data-yt-test-url="gdata.youtube.com/feeds/api/videos/" - data-autohide-html5="True" + data-metadata='{"autohideHtml5": "true", "autoplay": "false", "captionDataDir": "", "endTime": "", "generalSpeed": "1.0", "saveStateUrl": "/save_user_state", "savedVideoPosition": "0", "showCaptions": "true", "sources": ["xmodule/include/fixtures/test.mp4","xmodule/include/fixtures/test.webm","xmodule/include/fixtures/test.ogv"], "speed": "1.5", "startTime": "", "streams": "0.5:7tqY6eQzVhE,1.0:cogebirgzzM,1.5:abcdefghijkl", "sub": "", "transcriptAvailableTranslationsUrl": "/transcript/available_translations", "transcriptLanguage": "en", "transcriptLanguages": {"en": "English", "de": "Deutsch", "zh": "普通è¯"}, "transcriptTranslationUrl": "/transcript/translation/__lang__", "ytApiUrl": "www.youtube.com/iframe_api", "ytImageUrl": "", "ytTestTimeout": "1500", "ytTestUrl": "gdata.youtube.com/feeds/api/videos/"}' > <div class="focus_grabber first"></div> @@ -35,35 +20,11 @@ <section class="video-controls is-hidden"> <div class="slider"></div> <div> - <ul class="vcr"> - <li><a class="video_control" href="#" title="Play" role="button" aria-disabled="false"></a></li> - <li><div class="vidtime">0:00 / 0:00</div></li> - </ul> - <div class="secondary-controls"> - <div class="speeds"> - <a class="speed-button" href="#" title="Speeds" role="button" aria-disabled="false"> - <span class="label">Speed</span> - <span class="value"></span> - </a> - <ol class="video-speeds"></ol> - </div> - <div class="volume"> - <a href="#" title="Volume" role="button" aria-disabled="false"></a> - <div class="volume-slider-container"> - <div class="volume-slider"></div> - </div> - </div> - <a href="#" class="add-fullscreen" title="Fill browser" role="button" aria-disabled="false">Fill Browser</a> - <a href="#" class="quality-control is-hidden" title="HD off" role="button" aria-disabled="false">HD off</a> - <div class="lang menu-container"> - <a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a> - </div> - </div> + <div class="vcr"><div class="vidtime">0:00 / 0:00</div></div> + <div class="secondary-controls"></div> </div> </section> </article> - - <ol class="subtitles"><li></li></ol> </div> <div class="focus_grabber last"></div> @@ -77,20 +38,7 @@ <div id="video_id2" class="video" - data-streams="0.75:7tqY6eQzVhE,1.0:cogebirgzzM" - data-show-captions="true" - data-speed="1.0" - data-start="" - data-end="" - data-transcript-language="en" - data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通è¯"}' - data-transcript-translation-url="/transcript/translation" - data-transcript-available-translations-url="/transcript/available_translations" - data-autoplay="False" - data-yt-test-timeout="1500" - data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/" - - data-autohide-html5="True" + data-metadata='{"autohideHtml5": "true", "autoplay": "false", "captionDataDir": "", "endTime": "", "generalSpeed": "1.0", "saveStateUrl": "/save_user_state", "savedVideoPosition": "0", "showCaptions": "true", "sources": ["xmodule/include/fixtures/test.mp4","xmodule/include/fixtures/test.webm","xmodule/include/fixtures/test.ogv"], "speed": "1.0", "startTime": "", "streams": "0.75:7tqY6eQzVhE,1.0:cogebirgzzM", "sub": "", "transcriptAvailableTranslationsUrl": "/transcript/available_translations", "transcriptLanguage": "en", "transcriptLanguages": {"en": "English", "de": "Deutsch", "zh": "普通è¯"}, "transcriptTranslationUrl": "/transcript/translation/__lang__", "ytApiUrl": "www.youtube.com/iframe_api", "ytImageUrl": "", "ytTestTimeout": "1500", "ytTestUrl": "gdata.youtube.com/feeds/api/videos/"}' > <div class="tc-wrapper"> <article class="video-wrapper"> @@ -102,30 +50,8 @@ <section class="video-controls"> <div class="slider"></div> <div> - <ul class="vcr"> - <li><a class="video_control" href="#" title="Play"></a></li> - <li><div class="vidtime">0:00 / 0:00</div></li> - </ul> - <div class="secondary-controls"> - <div class="speeds"> - <a class="speed-button" href="#" title="Speeds" role="button" aria-disabled="false"> - <span class="label">Speed</span> - <span class="value"></span> - </a> - <ol class="video-speeds"></ol> - </div> - <div class="volume"> - <a href="#"></a> - <div class="volume-slider-container"> - <div class="volume-slider"></div> - </div> - </div> - <a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a> - <a href="#" class="quality-control is-hidden" title="HD">HD</a> - <div class="lang menu-container"> - <a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a> - </div> - </div> + <div class="vcr"><div class="vidtime">0:00 / 0:00</div></div> + <div class="secondary-controls"></div> </div> </section> </article> @@ -142,20 +68,7 @@ <div id="video_id3" class="video" - data-streams="0.75:7tqY6eQzVhE,1.0:cogebirgzzM" - data-show-captions="true" - data-speed="1.0" - data-start="" - data-end="" - data-transcript-language="en" - data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通è¯"}' - data-transcript-translation-url="/transcript/translation" - data-transcript-available-translations-url="/transcript/available_translations" - data-autoplay="False" - data-yt-test-timeout="1500" - data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/" - - data-autohide-html5="True" + data-metadata='{"autohideHtml5": "true", "autoplay": "false", "captionDataDir": "", "endTime": "", "generalSpeed": "1.0", "saveStateUrl": "/save_user_state", "savedVideoPosition": "0", "showCaptions": "true", "sources": ["xmodule/include/fixtures/test.mp4","xmodule/include/fixtures/test.webm","xmodule/include/fixtures/test.ogv"], "speed": "1.0", "startTime": "", "streams": "0.75:7tqY6eQzVhE,1.0:cogebirgzzM", "sub": "", "transcriptAvailableTranslationsUrl": "/transcript/available_translations", "transcriptLanguage": "en", "transcriptLanguages": {"en": "English", "de": "Deutsch", "zh": "普通è¯"}, "transcriptTranslationUrl": "/transcript/translation/__lang__", "ytApiUrl": "www.youtube.com/iframe_api", "ytImageUrl": "", "ytTestTimeout": "1500", "ytTestUrl": "gdata.youtube.com/feeds/api/videos/"}' > <div class="tc-wrapper"> <article class="video-wrapper"> diff --git a/common/lib/xmodule/xmodule/js/spec/helper.js b/common/lib/xmodule/xmodule/js/spec/helper.js index f188c9c639eac6660f8a687173d0cc92de0d48b7..97d422d5d8917cf8aae6e3e8240d19a5f1ab3523 100644 --- a/common/lib/xmodule/xmodule/js/spec/helper.js +++ b/common/lib/xmodule/xmodule/js/spec/helper.js @@ -206,6 +206,9 @@ }, toBeInArray: function (array) { return $.inArray(this.actual, array) > -1; + }, + toBeFocused: function () { + return $(this.actual)[0] === $(this.actual)[0].ownerDocument.activeElement; } }); @@ -239,12 +242,11 @@ loadFixtures('video_all.html'); } - // If `params` is an object, assign it's properties as data attributes + // If `params` is an object, assign its properties as data attributes // to the main video DIV element. if (_.isObject(params)) { - $('#example') - .find('#video_id') - .data(params); + var metadata = _.extend($('#video_id').data('metadata'), params); + $('#video_id').data('metadata', metadata); } jasmine.stubRequests(); diff --git a/common/lib/xmodule/xmodule/js/spec/video/general_spec.js b/common/lib/xmodule/xmodule/js/spec/video/general_spec.js index 72fc269fd8ad755169c80e7057c740866227ec14..7cefab5e4999dcf7aa058a5c15da93053e77d60e 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/general_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/general_spec.js @@ -1,6 +1,6 @@ (function (undefined) { describe('Video', function () { - var oldOTBD; + var oldOTBD, state; beforeEach(function () { jasmine.stubRequests(); @@ -17,11 +17,12 @@ beforeEach(function () { loadFixtures('video.html'); $.cookie.andReturn('0.50'); + this.state = jasmine.initializePlayerYouTube('video_html5.html'); }); describe('by default', function () { - beforeEach(function () { - this.state = new window.Video('#example'); + afterEach(function () { + this.state.videoPlayer.destroy(); }); it('check videoType', function () { @@ -54,19 +55,16 @@ var state; beforeEach(function () { - loadFixtures('video_html5.html'); $.cookie.andReturn('0.75'); + state = jasmine.initializePlayer('video_html5.html'); }); - describe('by default', function () { - beforeEach(function () { - state = new window.Video('#example'); - }); - - afterEach(function () { - state = undefined; - }); + afterEach(function () { + state.videoPlayer.destroy(); + state = undefined; + }); + describe('by default', function () { it('check videoType', function () { expect(state.videoType).toEqual('html5'); }); @@ -95,14 +93,6 @@ // the stand alone HTML5 player object is already loaded, so no // further testing in that case is required. describe('HTML5 API is available', function () { - beforeEach(function () { - state = new Video('#example'); - }); - - afterEach(function () { - state = null; - }); - it('create the Video Player', function () { expect(state.videoPlayer.player).not.toBeUndefined(); }); @@ -113,8 +103,11 @@ describe('YouTube API is not loaded', function () { beforeEach(function () { window.YT = undefined; + state = jasmine.initializePlayerYouTube(); + }) - state = jasmine.initializePlayerYouTube('video.html'); + afterEach(function () { + state.videoPlayer.destroy(); }); it('callback, to be called after YouTube API loads, exists and is called', function () { @@ -159,9 +152,8 @@ } ]; - beforeEach(function () { - loadFixtures('video.html'); - + afterEach(function () { + state.videoPlayer.destroy(); }); $.each(miniTestSuite, function (index, test) { @@ -172,13 +164,10 @@ function itFabrique(itDescription, data, expectData) { it(itDescription, function () { - $('#example').find('.video') - .data({ - 'start': data.start, - 'end': data.end - }); - - state = new Video('#example'); + state = jasmine.initializePlayer('video.html', { + 'start': data.start, + 'end': data.end + }); expect(state.config.startTime).toBe(expectData.start); expect(state.config.endTime).toBe(expectData.end); @@ -238,26 +227,5 @@ expect(numAjaxCalls).toBe(1); }); }); - - describe('log', function () { - beforeEach(function () { - loadFixtures('video_html5.html'); - state = new Video('#example'); - spyOn(Logger, 'log'); - state.videoPlayer.log('someEvent', { - currentTime: 25, - speed: '1.0' - }); - }); - - it('call the logger with valid extra parameters', function () { - expect(Logger.log).toHaveBeenCalledWith('someEvent', { - id: 'id', - code: 'html5', - currentTime: 25, - speed: '1.0' - }); - }); - }); }); }).call(this); diff --git a/common/lib/xmodule/xmodule/js/spec/video/html5_video_spec.js b/common/lib/xmodule/xmodule/js/spec/video/html5_video_spec.js index baefb4dea1cc46dd74e001176f33c4864df58a3b..b22cdd375efde43d2e59eaa5fb64e0a03cf90cd4 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/html5_video_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/html5_video_spec.js @@ -10,8 +10,8 @@ afterEach(function () { state.storage.clear(); + state.videoPlayer.destroy(); $.fn.scrollTo.reset(); - $('.subtitles').remove(); $('source').remove(); window.onTouchBasedDevice = oldOTBD; }); diff --git a/common/lib/xmodule/xmodule/js/spec/video/initialize_spec.js b/common/lib/xmodule/xmodule/js/spec/video/initialize_spec.js index e08ba56a7516cfaa844f7105e952fa6c1a3512e0..f3194b8bce3a13223e08be0409ea929e60e425bd 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/initialize_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/initialize_spec.js @@ -12,158 +12,6 @@ function (Initialize) { state = {}; }); - describe('saveState function', function () { - var videoPlayerCurrentTime, newCurrentTime, speed; - - // We make sure that `currentTime` is a float. We need to test - // that Math.round() is called. - videoPlayerCurrentTime = 3.1242; - - // We have two times, because one is stored in - // `videoPlayer.currentTime`, and the other is passed directly to - // `saveState` in `data` object. In each case, there is different - // code that handles these times. They have to be different for - // test completeness sake. Also, make sure it is float, as is the - // time above. - newCurrentTime = 5.4; - - speed = '0.75'; - - beforeEach(function () { - state = { - videoPlayer: { - currentTime: videoPlayerCurrentTime - }, - storage: { - setItem: jasmine.createSpy() - }, - config: { - saveStateUrl: 'http://example.com/save_user_state' - } - }; - - spyOn($, 'ajax'); - spyOn(Time, 'formatFull').andCallThrough(); - }); - - it('data is not an object, async is true', function () { - itSpec({ - asyncVal: true, - speedVal: undefined, - positionVal: videoPlayerCurrentTime, - data: undefined, - ajaxData: { - saved_video_position: Time.formatFull(Math.round(videoPlayerCurrentTime)) - } - }); - }); - - it('data contains speed, async is false', function () { - itSpec({ - asyncVal: false, - speedVal: speed, - positionVal: undefined, - data: { - speed: speed - }, - ajaxData: { - speed: speed - } - }); - }); - - it('data contains float position, async is true', function () { - itSpec({ - asyncVal: true, - speedVal: undefined, - positionVal: newCurrentTime, - data: { - saved_video_position: newCurrentTime - }, - ajaxData: { - saved_video_position: Time.formatFull(Math.round(newCurrentTime)) - } - }); - }); - - it('data contains speed and rounded position, async is false', function () { - itSpec({ - asyncVal: false, - speedVal: speed, - positionVal: Math.round(newCurrentTime), - data: { - speed: speed, - saved_video_position: Math.round(newCurrentTime) - }, - ajaxData: { - speed: speed, - saved_video_position: Time.formatFull(Math.round(newCurrentTime)) - } - }); - }); - - it('data contains empty object, async is true', function () { - itSpec({ - asyncVal: true, - speedVal: undefined, - positionVal: undefined, - data: {}, - ajaxData: {} - }); - }); - - it('data contains position 0, async is true', function () { - itSpec({ - asyncVal: true, - speedVal: undefined, - positionVal: 0, - data: { - saved_video_position: 0 - }, - ajaxData: { - saved_video_position: Time.formatFull(Math.round(0)) - } - }); - }); - - return; - - function itSpec(value) { - var asyncVal = value.asyncVal, - speedVal = value.speedVal, - positionVal = value.positionVal, - data = value.data, - ajaxData = value.ajaxData; - - Initialize.prototype.saveState.call(state, asyncVal, data); - - if (speedVal) { - expect(state.storage.setItem).toHaveBeenCalledWith( - 'speed', - speedVal, - true - ); - } - if (positionVal) { - expect(state.storage.setItem).toHaveBeenCalledWith( - 'savedVideoPosition', - positionVal, - true - ); - expect(Time.formatFull).toHaveBeenCalledWith( - positionVal - ); - } - expect($.ajax).toHaveBeenCalledWith({ - url: state.config.saveStateUrl, - type: 'POST', - async: asyncVal, - dataType: 'json', - data: ajaxData - }); - } - }); - describe('getCurrentLanguage', function () { var msg; @@ -356,20 +204,12 @@ function (Initialize) { describe('when new speed is available', function () { beforeEach(function () { - Initialize.prototype.setSpeed.call(state, '0.75', true); + Initialize.prototype.setSpeed.call(state, '0.75'); }); it('set new speed', function () { expect(state.speed).toEqual('0.75'); }); - - it('save setting for new speed', function () { - expect(state.storage.setItem.calls[0].args) - .toEqual(['speed', '0.75', true]); - - expect(state.storage.setItem.calls[1].args) - .toEqual(['general_speed', '0.75']); - }); }); describe('when new speed is not available', function () { @@ -390,7 +230,7 @@ function (Initialize) { }; $.each(map, function(key, expected) { - Initialize.prototype.setSpeed.call(state, key, true); + Initialize.prototype.setSpeed.call(state, key); expect(state.speed).toBe(expected); }); }); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_accessible_menu_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_accessible_menu_spec.js index feb332122cf7cf386e24acdef84d6998f80bc361..a790f8ecaee8bc29cad84c02ead5d5d4453849d5 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_accessible_menu_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_accessible_menu_spec.js @@ -5,6 +5,7 @@ afterEach(function () { $('source').remove(); state.storage.clear(); + state.videoPlayer.destroy(); }); describe('constructor', function () { @@ -56,24 +57,6 @@ }); */ }); - - it('add ARIA attributes to button, menu, and menu items links', - function () { - expect(button).toHaveAttrs({ - 'role': 'button', - 'title': '.srt', - 'aria-disabled': 'false' - }); - - expect(menuList).toHaveAttr('role', 'menu'); - - menuItemsLinks.each(function(){ - expect($(this)).toHaveAttrs({ - 'role': 'menuitem', - 'aria-disabled': 'false' - }); - }); - }); }); describe('when running', function () { diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_bumper_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_bumper_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..0a355e918d2c288ba9dcb921c7eb4fa290c412ce --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/video/video_bumper_spec.js @@ -0,0 +1,109 @@ +(function (WAIT_TIMEOUT) { + 'use strict'; + describe('VideoBumper', function () { + var state, oldOTBD, waitForPlaying; + + waitForPlaying = function (state) { + waitsFor(function () { + return state.el.hasClass('is-playing'); + }, 'Player is not playing.', WAIT_TIMEOUT); + }; + + beforeEach(function () { + oldOTBD = window.onTouchBasedDevice; + window.onTouchBasedDevice = jasmine + .createSpy('onTouchBasedDevice').andReturn(null); + state = jasmine.initializePlayer('video_with_bumper.html'); + $('.poster .btn-play').click(); + jasmine.Clock.useMock(); + }); + + afterEach(function () { + $('source').remove(); + state.storage.clear(); + if (state.bumperState && state.bumperState.videoPlayer) { + state.bumperState.videoPlayer.destroy(); + } + if (state.videoPlayer) { + state.videoPlayer.destroy(); + } + window.onTouchBasedDevice = oldOTBD; + }); + + it('can render the bumper video', function () { + expect($('.is-bumper')).toExist(); + }); + + it('can show the main video on error', function () { + state.el.trigger('error'); + jasmine.Clock.tick(20); + expect($('.is-bumper')).not.toExist(); + waitForPlaying(state); + }); + + it('can show the main video once bumper ends', function () { + state.el.trigger('ended'); + jasmine.Clock.tick(20); + expect($('.is-bumper')).not.toExist(); + waitForPlaying(state); + }); + + it('can show the main video on skip', function () { + state.bumperState.videoBumper.skip(); + jasmine.Clock.tick(20); + expect($('.is-bumper')).not.toExist(); + waitForPlaying(state); + }); + + it('can stop the bumper video playing if it is too long', function () { + state.el.trigger('timeupdate', [state.bumperState.videoBumper.maxBumperDuration + 1]); + jasmine.Clock.tick(20); + expect($('.is-bumper')).not.toExist(); + waitForPlaying(state); + }); + + it('can save appropriate states correctly on ended', function () { + var saveState = jasmine.createSpy('saveState'); + state.bumperState.videoSaveStatePlugin.saveState = saveState; + state.el.trigger('ended'); + jasmine.Clock.tick(20); + expect(saveState).toHaveBeenCalledWith(true, { + bumper_last_view_date: true}); + }); + + it('can save appropriate states correctly on skip', function () { + var saveState = jasmine.createSpy('saveState'); + state.bumperState.videoSaveStatePlugin.saveState = saveState; + state.bumperState.videoBumper.skip(); + expect(state.storage.getItem('isBumperShown')).toBeTruthy(); + jasmine.Clock.tick(20); + expect(saveState).toHaveBeenCalledWith(true, { + bumper_last_view_date: true}); + }); + + it('can save appropriate states correctly on error', function () { + var saveState = jasmine.createSpy('saveState'); + state.bumperState.videoSaveStatePlugin.saveState = saveState; + state.el.trigger('error'); + expect(state.storage.getItem('isBumperShown')).toBeTruthy(); + jasmine.Clock.tick(20); + expect(saveState).toHaveBeenCalledWith(true, { + bumper_last_view_date: true}); + }); + + it('can save appropriate states correctly on skip and do not show again', function () { + var saveState = jasmine.createSpy('saveState'); + state.bumperState.videoSaveStatePlugin.saveState = saveState; + state.bumperState.videoBumper.skipAndDoNotShowAgain(); + expect(state.storage.getItem('isBumperShown')).toBeTruthy(); + jasmine.Clock.tick(20); + expect(saveState).toHaveBeenCalledWith(true, { + bumper_last_view_date: true, bumper_do_not_show_again: true}); + }); + + it('can destroy itself', function () { + state.bumperState.videoBumper.destroy(); + expect(state.videoBumper).toBeUndefined(); + }); + }); +}).call(this, window.WAIT_TIMEOUT); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js index 39778d7ba6ca5512847d5bebe9490783effff260..f269bde541a2fba11382604ebf5dc068d0a085f0 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js @@ -11,14 +11,13 @@ }); afterEach(function () { - $('.subtitles').remove(); - // `source` tags should be removed to avoid memory leak bug that we // had before. Removing of `source` tag, not `video` tag, stops // loading video source and clears the memory. $('source').remove(); $.fn.scrollTo.reset(); state.storage.clear(); + state.videoPlayer.destroy(); window.onTouchBasedDevice = oldOTBD; }); @@ -121,11 +120,6 @@ }); }); - it('bind the hide caption button', function () { - state = jasmine.initializePlayer(); - expect($('.hide-subtitles')).toHandle('click'); - }); - it('bind the mouse movement', function () { state = jasmine.initializePlayer(); expect($('.subtitles')).toHandle('mouseover'); @@ -143,6 +137,27 @@ }); + it('can destroy itself', function () { + spyOn($, 'ajaxWithPrefix'); + state = jasmine.initializePlayer(); + var plugin = state.videoCaption; + + spyOn($.fn, 'off').andCallThrough(); + state.videoCaption.destroy(); + + expect(state.videoCaption).toBeUndefined(); + expect($.fn.off).toHaveBeenCalledWith({ + 'caption:fetch': plugin.fetchCaption, + 'caption:resize': plugin.onResize, + 'caption:update': plugin.onCaptionUpdate, + 'ended': plugin.pause, + 'fullscreen': plugin.onResize, + 'pause': plugin.pause, + 'play': plugin.play, + 'destroy': plugin.destroy + }); + }); + describe('renderLanguageMenu', function () { describe('is rendered', function () { it('if languages more than 1', function () { @@ -593,7 +608,7 @@ it(msg, function () { spyOn(Caption, 'fetchAvailableTranslations'); $.ajax.andCallFake(function (settings) { - settings.error([]); + _.result(settings, 'error'); }); state.config.transcriptLanguages = {}; @@ -612,7 +627,7 @@ xit(msg, function () { $.ajax .andCallFake(function (settings) { - settings.error([]); + _.result(settings, 'error'); }); state.config.transcriptLanguages = { @@ -690,7 +705,7 @@ msg = 'on error: captions are hidden if there are no transcript'; it(msg, function () { $.ajax.andCallFake(function (settings) { - settings.error(); + _.result(settings, 'error'); }); Caption.fetchAvailableTranslations(); @@ -907,8 +922,8 @@ $('.subtitles').css('maxHeight'), 10 ); videoWrapperHeight = $('.video-wrapper').height(); - progressSliderHeight = videoControl.sliderEl.height(); - controlHeight = videoControl.el.height(); + progressSliderHeight = state.el.find('.slider').height(); + controlHeight = state.el.find('.video-controls').height(); shouldBeHeight = videoWrapperHeight - 0.5 * progressSliderHeight - controlHeight; @@ -1043,7 +1058,6 @@ describe('toggle', function () { beforeEach(function () { state = jasmine.initializePlayer(); - spyOn(state.videoPlayer, 'log'); $('.subtitles li[data-index=1]').addClass('current'); }); @@ -1053,15 +1067,6 @@ state.videoCaption.toggle(jQuery.Event('click')); }); - it('log the hide_transcript event', function () { - expect(state.videoPlayer.log).toHaveBeenCalledWith( - 'hide_transcript', - { - currentTime: state.videoPlayer.currentTime - } - ); - }); - it('hide the caption', function () { expect(state.el).toHaveClass('closed'); }); @@ -1079,15 +1084,6 @@ jasmine.Clock.useMock(); }); - it('log the show_transcript event', function () { - expect(state.videoPlayer.log).toHaveBeenCalledWith( - 'show_transcript', - { - currentTime: state.videoPlayer.currentTime - } - ); - }); - it('show the caption', function () { expect(state.el).not.toHaveClass('closed'); }); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_context_menu_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_context_menu_spec.js index 269a75053c45325c8defd51fbdc17f043c131a6d..295b151a4fd0fda46cc4d09e00152bce3378ef94 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_context_menu_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_context_menu_spec.js @@ -68,6 +68,7 @@ $('source').remove(); _.result(state.storage, 'clear'); _.result($('video').data('contextmenu'), 'destroy'); + _.result(state.videoPlayer, 'destroy'); }); describe('constructor', function () { @@ -219,12 +220,13 @@ it('mouse left/right-clicking behaves as expected on play/pause menu item', function () { var menuItem = menuItems.first(); + spyOn(state.videoPlayer, 'isPlaying'); spyOn(state.videoPlayer, 'play').andCallFake(function () { - state.videoControl.isPlaying = true; + state.videoPlayer.isPlaying.andReturn(true); state.el.trigger('play'); }); spyOn(state.videoPlayer, 'pause').andCallFake(function () { - state.videoControl.isPlaying = false; + state.videoPlayer.isPlaying.andReturn(false); state.el.trigger('pause'); }); // Left-click on play diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_control_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_control_spec.js index 98569620c2298d7b4ba2e6f1cb47a3fd5c188621..85794dc2d5ecc7910cbda09d987a5620442dd889 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_control_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_control_spec.js @@ -13,12 +13,13 @@ afterEach(function () { $('source').remove(); state.storage.clear(); - window.Video.previousState = null; + state.videoPlayer.destroy(); window.onTouchBasedDevice = oldOTBD; }); describe('constructor', function () { beforeEach(function () { + window.VideoState = {}; state = jasmine.initializePlayer(); }); @@ -28,83 +29,13 @@ '.slider', 'ul.vcr', 'a.play', - '.vidtime', - '.add-fullscreen' + '.vidtime' ].join(',') ); expect($('.video-controls').find('.vidtime')) .toHaveText('0:00 / 0:00'); }); - - it('add ARIA attributes to time control', function () { - var timeControl = $('div.slider > a'); - - expect(timeControl).toHaveAttrs({ - 'role': 'slider', - 'title': 'Video position', - 'aria-disabled': 'false' - }); - - expect(timeControl).toHaveAttr('aria-valuetext'); - }); - - it('add ARIA attributes to play control', function () { - var playControl = $('ul.vcr a'); - - expect(playControl).toHaveAttrs({ - 'role': 'button', - 'title': 'Play', - 'aria-disabled': 'false' - }); - }); - - it('add ARIA attributes to fullscreen control', function () { - var fullScreenControl = $('a.add-fullscreen'); - - expect(fullScreenControl).toHaveAttrs({ - 'role': 'button', - 'title': 'Fill browser', - 'aria-disabled': 'false' - }); - }); - - it('bind the playback button', function () { - expect($('.video_control')).toHandleWith( - 'click', - state.videoControl.togglePlayback - ); - }); - - describe('when on a non-touch based device', function () { - beforeEach(function () { - state = jasmine.initializePlayer(); - }); - - it('add the play class to video control', function () { - expect($('.video_control')).toHaveClass('play'); - expect($('.video_control')).toHaveAttr( - 'title', 'Play' - ); - }); - }); - - describe('when on a touch based device', function () { - beforeEach(function () { - window.onTouchBasedDevice.andReturn(['iPad']); - state = jasmine.initializePlayer(); - }); - - it( - 'does not add the play class to video control', - function () - { - expect($('.video_control')).toHaveClass('play'); - expect($('.video_control')).toHaveAttr( - 'title', 'Play' - ); - }); - }); }); describe('constructor with start-time', function () { @@ -115,6 +46,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ start: 10, savedVideoPosition: 0 @@ -147,6 +79,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ start: 10, savedVideoPosition: 15 @@ -181,6 +114,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ start: 10, savedVideoPosition: -15 @@ -215,6 +149,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ start: 10, savedVideoPosition: 'a' @@ -249,6 +184,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ start: 10, savedVideoPosition: 10000 @@ -278,13 +214,14 @@ describe('constructor with end-time', function () { it( - 'saved position is 0, timer slider and VCR set to 0:00 ' + + 'saved position is 0, timer slider and VCR set to 0:00 ' + 'and ending at specified end-time', function () { var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ end: 20, savedVideoPosition: 0 @@ -319,6 +256,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ end: 20, savedVideoPosition: 15 @@ -353,6 +291,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ end: 20, savedVideoPosition: -15 @@ -387,6 +326,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ end: 20, savedVideoPosition: 'a' @@ -422,6 +362,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ end: 20, savedVideoPosition: 10000 @@ -457,6 +398,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ start: 10, end: 20, @@ -492,6 +434,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ start: 10, end: 20, @@ -527,6 +470,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ start: 10, end: 20, @@ -562,6 +506,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ start: 10, end: 20, @@ -597,6 +542,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ start: 10, end: 20, @@ -625,217 +571,8 @@ }); }); - it('Controls height is actual on switch to fullscreen', function () { - spyOn($.fn, 'height').andCallFake(function (val) { - return _.isUndefined(val) ? 100: this; - }); - - state = jasmine.initializePlayer(); - $(state.el).trigger('fullscreen'); - - expect(state.videoControl.height).toBe(150); - }); - - describe('play', function () { - beforeEach(function () { - state = jasmine.initializePlayer(); - state.videoControl.play(); - }); - - it('switch playback button to play state', function () { - expect($('.video_control')).not.toHaveClass('play'); - expect($('.video_control')).toHaveClass('pause'); - expect($('.video_control')).toHaveAttr('title', 'Pause'); - }); - }); - - describe('pause', function () { - beforeEach(function () { - state = jasmine.initializePlayer(); - state.videoControl.pause(); - }); - - it('switch playback button to pause state', function () { - expect($('.video_control')).not.toHaveClass('pause'); - expect($('.video_control')).toHaveClass('play'); - expect($('.video_control')).toHaveAttr('title', 'Play'); - }); - }); - - describe('togglePlayback', function () { - beforeEach(function () { - state = jasmine.initializePlayer(); - }); - - describe( - 'when the control does not have play or pause class', - function () - { - beforeEach(function () { - $('.video_control').removeClass('play') - .removeClass('pause'); - }); - - describe('when the video is playing', function () { - beforeEach(function () { - $('.video_control').addClass('play'); - spyOnEvent(state.videoControl, 'pause'); - state.videoControl.togglePlayback( - $.Event('click') - ); - }); - - it('does not trigger the pause event', function () { - expect('pause').not - .toHaveBeenTriggeredOn(state.videoControl); - }); - }); - - describe('when the video is paused', function () { - beforeEach(function () { - $('.video_control').addClass('pause'); - spyOnEvent(state.videoControl, 'play'); - state.videoControl.togglePlayback( - $.Event('click') - ); - }); - - it('does not trigger the play event', function () { - expect('play').not - .toHaveBeenTriggeredOn(state.videoControl); - }); - }); - }); - }); - - describe('Play placeholder', function () { - var cases = [ - { - name: 'PC', - isShown: false, - isTouch: null - }, { - name: 'iPad', - isShown: true, - isTouch: ['iPad'] - }, { - name: 'Android', - isShown: true, - isTouch: ['Android'] - }, { - name: 'iPhone', - isShown: false, - isTouch: ['iPhone'] - } - ]; - - beforeEach(function () { - jasmine.stubRequests(); - - spyOn(window.YT, 'Player').andCallThrough(); - }); - - it ('works correctly on calling proper methods', function () { - var btnPlay; - - state = jasmine.initializePlayer(); - btnPlay = state.el.find('.btn-play'); - - state.videoControl.showPlayPlaceholder(); - - expect(btnPlay).not.toHaveClass('is-hidden'); - expect(btnPlay).toHaveAttrs({ - 'aria-hidden': 'false', - 'tabindex': 0 - }); - - state.videoControl.hidePlayPlaceholder(); - - expect(btnPlay).toHaveClass('is-hidden'); - expect(btnPlay).toHaveAttrs({ - 'aria-hidden': 'true', - 'tabindex': -1 - }); - }); - - $.each(cases, function (index, data) { - var message = [ - (data.isShown) ? 'is' : 'is not', - ' shown on', - data.name - ].join(''); - - it(message, function () { - var btnPlay; - - window.onTouchBasedDevice.andReturn(data.isTouch); - state = jasmine.initializePlayer(); - btnPlay = state.el.find('.btn-play'); - - if (data.isShown) { - expect(btnPlay).not.toHaveClass('is-hidden'); - } else { - expect(btnPlay).toHaveClass('is-hidden'); - } - }); - }); - - $.each(['iPad', 'Android'], function (index, device) { - it( - 'is shown on paused video on ' + device + - ' in HTML5 player', - function () - { - var btnPlay; - - window.onTouchBasedDevice.andReturn([device]); - state = jasmine.initializePlayer(); - btnPlay = state.el.find('.btn-play'); - - state.videoControl.play(); - state.videoControl.pause(); - - expect(btnPlay).not.toHaveClass('is-hidden'); - }); - - it( - 'is hidden on playing video on ' + device + - ' in HTML5 player', - function () - { - var btnPlay; - - window.onTouchBasedDevice.andReturn([device]); - state = jasmine.initializePlayer(); - btnPlay = state.el.find('.btn-play'); - - state.videoControl.play(); - - expect(btnPlay).toHaveClass('is-hidden'); - }); - - it( - 'is hidden on paused video on ' + device + - ' in YouTube player', - function () - { - var btnPlay; - - window.onTouchBasedDevice.andReturn([device]); - state = jasmine.initializePlayerYouTube(); - btnPlay = state.el.find('.btn-play'); - - state.videoControl.play(); - state.videoControl.pause(); - - expect(btnPlay).toHaveClass('is-hidden'); - }); - }); - }); - it('show', function () { var controls; - state = jasmine.initializePlayer(); controls = state.el.find('.video-controls'); controls.addClass('is-hidden'); @@ -843,5 +580,23 @@ state.videoControl.show(); expect(controls).not.toHaveClass('is-hidden'); }); + + it('can destroy itself', function () { + state = jasmine.initializePlayer(); + state.videoControl.destroy(); + expect(state.videoControl).toBeUndefined(); + }); + + it('can focus the first control', function () { + var btnPlay; + state = jasmine.initializePlayer({focusFirstControl: true}); + btnPlay = state.el.find('.video-controls .play'); + waitsFor(function () { + return state.el.hasClass('is-initialized'); + }, 'Player is not initialized', WAIT_TIMEOUT); + runs(function () { + expect(btnPlay).toBeFocused(); + }); + }); }); }).call(this, window.WAIT_TIMEOUT); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_events_bumper_plugin_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_events_bumper_plugin_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..e41b40e782e2664871e45642b4b8e27e5453187b --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/video/video_events_bumper_plugin_spec.js @@ -0,0 +1,157 @@ +(function (undefined) { + 'use strict'; + describe('VideoPlayer Events Bumper plugin', function () { + var state, oldOTBD; + + beforeEach(function () { + oldOTBD = window.onTouchBasedDevice; + window.onTouchBasedDevice = jasmine + .createSpy('onTouchBasedDevice') + .andReturn(null); + + jasmine.stubRequests(); + state = jasmine.initializePlayer('video_with_bumper.html'); + spyOn(Logger, 'log'); + $('.poster .btn-play').click(); + spyOn(state.bumperState.videoEventsBumperPlugin, 'getCurrentTime').andReturn(10); + spyOn(state.bumperState.videoEventsBumperPlugin, 'getDuration').andReturn(20); + }); + + afterEach(function () { + $('source').remove(); + window.onTouchBasedDevice = oldOTBD; + state.storage.clear(); + if (state.bumperState && state.bumperState.videoPlayer) { + state.bumperState.videoPlayer.destroy(); + } + if (state.videoPlayer) { + state.videoPlayer.destroy(); + } + }); + + it('can emit "edx.video.bumper.loaded" event', function () { + state.el.trigger('ready'); + expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.loaded', { + host_component_id: 'id', + bumper_id: 'xmodule/include/fixtures/test.mp4', + code: 'html5', + duration: 20 + }); + }); + + it('can emit "edx.video.bumper.played" event', function () { + state.el.trigger('play'); + expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.played', { + host_component_id: 'id', + bumper_id: 'xmodule/include/fixtures/test.mp4', + code: 'html5', + currentTime: 10, + duration: 20 + }); + }); + + it('can emit "edx.video.bumper.stopped" event', function () { + state.el.trigger('ended'); + expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.stopped', { + host_component_id: 'id', + bumper_id: 'xmodule/include/fixtures/test.mp4', + code: 'html5', + currentTime: 10, + duration: 20 + }); + + Logger.log.reset(); + state.el.trigger('stop'); + expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.stopped', { + host_component_id: 'id', + bumper_id: 'xmodule/include/fixtures/test.mp4', + code: 'html5', + currentTime: 10, + duration: 20 + }); + }); + + it('can emit "edx.video.bumper.skipped" event', function () { + state.el.trigger('skip', [false]); + expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.skipped', { + host_component_id: 'id', + bumper_id: 'xmodule/include/fixtures/test.mp4', + code: 'html5', + currentTime: 10, + duration: 20 + }); + }); + + it('can emit "edx.video.bumper.dismissed" event', function () { + state.el.trigger('skip', [true]); + expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.dismissed', { + host_component_id: 'id', + bumper_id: 'xmodule/include/fixtures/test.mp4', + code: 'html5', + currentTime: 10, + duration: 20 + }); + }); + + it('can emit "edx.video.bumper.transcript.menu.shown" event', function () { + state.el.trigger('language_menu:show'); + expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.transcript.menu.shown', { + host_component_id: 'id', + bumper_id: 'xmodule/include/fixtures/test.mp4', + code: 'html5', + duration: 20 + }); + }); + + it('can emit "edx.video.bumper.transcript.menu.hidden" event', function () { + state.el.trigger('language_menu:hide'); + expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.transcript.menu.hidden', { + host_component_id: 'id', + bumper_id: 'xmodule/include/fixtures/test.mp4', + code: 'html5', + duration: 20 + }); + }); + + it('can emit "edx.video.bumper.transcript.shown" event', function () { + state.el.trigger('captions:show'); + expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.transcript.shown', { + host_component_id: 'id', + bumper_id: 'xmodule/include/fixtures/test.mp4', + code: 'html5', + currentTime: 10, + duration: 20 + }); + }); + + it('can emit "edx.video.bumper.transcript.hidden" event', function () { + state.el.trigger('captions:hide'); + expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.transcript.hidden', { + host_component_id: 'id', + bumper_id: 'xmodule/include/fixtures/test.mp4', + code: 'html5', + currentTime: 10, + duration: 20 + }); + }); + + it('can destroy itself', function () { + var plugin = state.bumperState.videoEventsBumperPlugin; + spyOn($.fn, 'off').andCallThrough(); + plugin.destroy(); + expect(state.bumperState.videoEventsBumperPlugin).toBeUndefined(); + expect($.fn.off).toHaveBeenCalledWith({ + 'ready': plugin.onReady, + 'play': plugin.onPlay, + 'ended stop': plugin.onEnded, + 'skip': plugin.onSkip, + 'language_menu:show': plugin.onShowLanguageMenu, + 'language_menu:hide': plugin.onHideLanguageMenu, + 'captions:show': plugin.onShowCaptions, + 'captions:hide': plugin.onHideCaptions, + 'destroy': plugin.destroy + }); + }); + }); + +}).call(this); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_events_plugin_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_events_plugin_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..78352e575f1fae952d21470a9e9ac1aef9e39f05 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/video/video_events_plugin_spec.js @@ -0,0 +1,166 @@ +(function (undefined) { + 'use strict'; + describe('VideoPlayer Events plugin', function () { + var state, oldOTBD; + + beforeEach(function () { + oldOTBD = window.onTouchBasedDevice; + window.onTouchBasedDevice = jasmine + .createSpy('onTouchBasedDevice') + .andReturn(null); + + jasmine.stubRequests(); + state = jasmine.initializePlayer(); + spyOn(Logger, 'log'); + spyOn(state.videoEventsPlugin, 'getCurrentTime').andReturn(10); + }); + + afterEach(function () { + $('source').remove(); + window.onTouchBasedDevice = oldOTBD; + state.storage.clear(); + if (state.videoPlayer) { + state.videoPlayer.destroy(); + } + }); + + it('can emit "load_video" event', function () { + state.el.trigger('ready'); + expect(Logger.log).toHaveBeenCalledWith('load_video', { + id: 'id', + code: 'html5' + }); + }); + + it('can emit "play_video" event', function () { + state.el.trigger('play'); + expect(Logger.log).toHaveBeenCalledWith('play_video', { + id: 'id', + code: 'html5', + currentTime: 10 + }); + }); + + it('can emit "pause_video" event', function () { + state.el.trigger('pause'); + expect(Logger.log).toHaveBeenCalledWith('pause_video', { + id: 'id', + code: 'html5', + currentTime: 10 + }); + }); + + it('can emit "speed_change_video" event', function () { + state.el.trigger('speedchange', ['2.0', '1.0']); + expect(Logger.log).toHaveBeenCalledWith('speed_change_video', { + id: 'id', + code: 'html5', + current_time: 10, + old_speed: '1.0', + new_speed: '2.0' + }); + }); + + it('can emit "seek_video" event', function () { + state.el.trigger('seek', [1, 0, 'any']); + expect(Logger.log).toHaveBeenCalledWith('seek_video', { + id: 'id', + code: 'html5', + old_time: 0, + new_time: 1, + type: 'any' + }); + }); + + it('can emit "stop_video" event', function () { + state.el.trigger('ended'); + expect(Logger.log).toHaveBeenCalledWith('stop_video', { + id: 'id', + code: 'html5', + currentTime: 10 + }); + + Logger.log.reset(); + state.el.trigger('stop'); + expect(Logger.log).toHaveBeenCalledWith('stop_video', { + id: 'id', + code: 'html5', + currentTime: 10 + }); + }); + + it('can emit "skip_video" event', function () { + state.el.trigger('skip', [false]); + expect(Logger.log).toHaveBeenCalledWith('skip_video', { + id: 'id', + code: 'html5', + currentTime: 10 + }); + }); + + it('can emit "do_not_show_again_video" event', function () { + state.el.trigger('skip', [true]); + expect(Logger.log).toHaveBeenCalledWith('do_not_show_again_video', { + id: 'id', + code: 'html5', + currentTime: 10 + }); + }); + + it('can emit "video_show_cc_menu" event', function () { + state.el.trigger('language_menu:show'); + expect(Logger.log).toHaveBeenCalledWith('video_show_cc_menu', { + id: 'id', + code: 'html5' + }); + }); + + it('can emit "video_hide_cc_menu" event', function () { + state.el.trigger('language_menu:hide'); + expect(Logger.log).toHaveBeenCalledWith('video_hide_cc_menu', { + id: 'id', + code: 'html5' + }); + }); + + it('can emit "show_transcript" event', function () { + state.el.trigger('captions:show'); + expect(Logger.log).toHaveBeenCalledWith('show_transcript', { + id: 'id', + code: 'html5', + current_time: 10 + }); + }); + + it('can emit "hide_transcript" event', function () { + state.el.trigger('captions:hide'); + expect(Logger.log).toHaveBeenCalledWith('hide_transcript', { + id: 'id', + code: 'html5', + current_time: 10 + }); + }); + + it('can destroy itself', function () { + var plugin = state.videoEventsPlugin; + spyOn($.fn, 'off').andCallThrough(); + state.videoEventsPlugin.destroy(); + expect(state.videoEventsPlugin).toBeUndefined(); + expect($.fn.off).toHaveBeenCalledWith({ + 'ready': plugin.onReady, + 'play': plugin.onPlay, + 'pause': plugin.onPause, + 'ended stop': plugin.onEnded, + 'seek': plugin.onSeek, + 'skip': plugin.onSkip, + 'speedchange': plugin.onSpeedChange, + 'language_menu:show': plugin.onShowLanguageMenu, + 'language_menu:hide': plugin.onHideLanguageMenu, + 'captions:show': plugin.onShowCaptions, + 'captions:hide': plugin.onHideCaptions, + 'destroy': plugin.destroy + }); + }); + }); + +}).call(this); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_focus_grabber_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_focus_grabber_spec.js index ab3c12df6f5aa87f38f45833ef323f8f1bd31717..5f69d2c7c782fb5f4b0673d8a58066b64317252f 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_focus_grabber_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_focus_grabber_spec.js @@ -26,6 +26,7 @@ afterEach(function () { // Turn jQuery animations back on. jQuery.fx.off = true; + state.videoPlayer.destroy(); }); it( diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_full_screen_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_full_screen_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..215b891f41ff56d9a823ced896975181e93841be --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/video/video_full_screen_spec.js @@ -0,0 +1,102 @@ +(function () { + 'use strict'; + describe('VideoFullScreen', function () { + var state, oldOTBD; + + beforeEach(function () { + oldOTBD = window.onTouchBasedDevice; + window.onTouchBasedDevice = jasmine + .createSpy('onTouchBasedDevice').andReturn(null); + }); + + afterEach(function () { + $('source').remove(); + state.storage.clear(); + state.videoPlayer.destroy(); + window.onTouchBasedDevice = oldOTBD; + }); + + describe('constructor', function () { + beforeEach(function () { + state = jasmine.initializePlayer(); + }); + + it('renders the fullscreen control', function () { + expect($('.add-fullscreen')).toExist(); + expect(state.videoFullScreen.fullScreenState).toBe(false); + }); + + it('correctly adds ARIA attributes to fullscreen control', function () { + var fullScreenControl = $('.add-fullscreen'); + + expect(fullScreenControl).toHaveAttrs({ + 'role': 'button', + 'title': 'Fill browser', + 'aria-disabled': 'false' + }); + }); + + it('correctly triggers the event handler to toggle fullscreen mode', function () { + spyOn(state.videoFullScreen, 'exit'); + spyOn(state.videoFullScreen, 'enter'); + + state.videoFullScreen.fullScreenState = false; + state.videoFullScreen.toggle(); + expect(state.videoFullScreen.enter).toHaveBeenCalled(); + + state.videoFullScreen.fullScreenState = true; + state.videoFullScreen.toggle(); + expect(state.videoFullScreen.exit).toHaveBeenCalled(); + }); + + it('correctly updates ARIA on state change', function () { + var fullScreenControl = $('.add-fullscreen'); + fullScreenControl.click(); + expect(fullScreenControl).toHaveAttrs({ + 'role': 'button', + 'title': 'Exit full browser', + 'aria-disabled': 'false' + }); + fullScreenControl.click(); + expect(fullScreenControl).toHaveAttrs({ + 'role': 'button', + 'title': 'Fill browser', + 'aria-disabled': 'false' + }); + }); + + it('correctly can out of fullscreen by pressing esc', function () { + spyOn(state.videoCommands, 'execute'); + var esc = $.Event('keyup'); + esc.keyCode = 27; + state.isFullScreen = true; + $(document).trigger(esc); + expect(state.videoCommands.execute).toHaveBeenCalledWith('toggleFullScreen'); + }); + + it('can update video dimensions on state change', function () { + state.el.trigger('fullscreen', [true]); + expect(state.resizer.setMode).toHaveBeenCalledWith('both'); + state.el.trigger('fullscreen', [false]); + expect(state.resizer.setMode).toHaveBeenCalledWith('width'); + }); + + it('can destroy itself', function () { + state.videoFullScreen.destroy(); + expect($('.add-fullscreen')).not.toExist(); + expect(state.videoFullScreen).toBeUndefined(); + }); + }); + + it('Controls height is actual on switch to fullscreen', function () { + spyOn($.fn, 'height').andCallFake(function (val) { + return _.isUndefined(val) ? 100: this; + }); + + state = jasmine.initializePlayer(); + $(state.el).trigger('fullscreen'); + + expect(state.videoFullScreen.height).toBe(150); + }); + }); +}).call(this); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_play_pause_control_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_play_pause_control_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..877dc9861e249e3ba3d262578574e8d49bd72061 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/video/video_play_pause_control_spec.js @@ -0,0 +1,68 @@ +(function () { + 'use strict'; + describe('VideoPlayPauseControl', function () { + var state, oldOTBD; + + beforeEach(function () { + oldOTBD = window.onTouchBasedDevice; + window.onTouchBasedDevice = jasmine + .createSpy('onTouchBasedDevice').andReturn(null); + state = jasmine.initializePlayer(); + spyOn(state.videoCommands, 'execute'); + spyOn(state.videoSaveStatePlugin, 'saveState'); + }); + + afterEach(function () { + $('source').remove(); + state.storage.clear(); + state.videoPlayer.destroy(); + window.onTouchBasedDevice = oldOTBD; + }); + + it('can render the control', function () { + expect($('.video_control.play')).toExist(); + }); + + it('add ARIA attributes to play control', function () { + expect($('.video_control.play')).toHaveAttrs({ + 'role': 'button', + 'title': 'Play', + 'aria-disabled': 'false' + }); + }); + + it('can update ARIA state on play', function () { + state.el.trigger('play'); + expect($('.video_control.pause')).toHaveAttrs({ + 'role': 'button', + 'title': 'Pause', + 'aria-disabled': 'false' + }); + }); + + it('can update ARIA state on video ends', function () { + state.el.trigger('play'); + state.el.trigger('ended'); + expect($('.video_control.play')).toHaveAttrs({ + 'role': 'button', + 'title': 'Play', + 'aria-disabled': 'false' + }); + }); + + it('can update state on pause', function () { + state.el.trigger('pause'); + expect(state.videoSaveStatePlugin.saveState).toHaveBeenCalledWith(true); + }); + + it('can start video playing on click', function () { + $('.video_control.play').click(); + expect(state.videoCommands.execute).toHaveBeenCalledWith('togglePlayback'); + }); + + it('can destroy itself', function () { + state.videoPlayPauseControl.destroy(); + expect(state.videoPlayPauseControl).toBeUndefined(); + }); + }); +}).call(this); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_play_placeholder_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_play_placeholder_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..d99c12e24ccfb3d824555713f317ad6583d504b1 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/video/video_play_placeholder_spec.js @@ -0,0 +1,151 @@ +(function () { + 'use strict'; + describe('VideoPlayPlaceholder', function () { + var state, oldOTBD; + + beforeEach(function () { + oldOTBD = window.onTouchBasedDevice; + window.onTouchBasedDevice = jasmine + .createSpy('onTouchBasedDevice').andReturn(['iPad']); + + state = jasmine.initializePlayer(); + spyOn(state.videoCommands, 'execute'); + }); + + afterEach(function () { + $('source').remove(); + state.storage.clear(); + state.videoPlayer.destroy(); + window.onTouchBasedDevice = oldOTBD; + }); + + var cases = [ + { + name: 'PC', + isShown: false, + isTouch: null + }, { + name: 'iPad', + isShown: true, + isTouch: ['iPad'] + }, { + name: 'Android', + isShown: true, + isTouch: ['Android'] + }, { + name: 'iPhone', + isShown: false, + isTouch: ['iPhone'] + } + ]; + + beforeEach(function () { + jasmine.stubRequests(); + spyOn(window.YT, 'Player').andCallThrough(); + }); + + it ('works correctly on calling proper methods', function () { + var btnPlay; + + state = jasmine.initializePlayer(); + btnPlay = state.el.find('.btn-play'); + + state.videoPlayPlaceholder.show(); + + expect(btnPlay).not.toHaveClass('is-hidden'); + expect(btnPlay).toHaveAttrs({ + 'aria-hidden': 'false', + 'tabindex': 0 + }); + + state.videoPlayPlaceholder.hide(); + + expect(btnPlay).toHaveClass('is-hidden'); + expect(btnPlay).toHaveAttrs({ + 'aria-hidden': 'true', + 'tabindex': -1 + }); + }); + + $.each(cases, function (index, data) { + var message = [ + (data.isShown) ? 'is' : 'is not', + ' shown on', + data.name + ].join(''); + + it(message, function () { + var btnPlay; + + window.onTouchBasedDevice.andReturn(data.isTouch); + state = jasmine.initializePlayer(); + btnPlay = state.el.find('.btn-play'); + + if (data.isShown) { + expect(btnPlay).not.toHaveClass('is-hidden'); + } else { + expect(btnPlay).toHaveClass('is-hidden'); + } + }); + }); + + $.each(['iPad', 'Android'], function (index, device) { + it( + 'is shown on paused video on ' + device + + ' in HTML5 player', + function () + { + var btnPlay; + + window.onTouchBasedDevice.andReturn([device]); + state = jasmine.initializePlayer(); + btnPlay = state.el.find('.btn-play'); + + state.el.trigger('play'); + state.el.trigger('pause'); + expect(btnPlay).not.toHaveClass('is-hidden'); + }); + + it( + 'is hidden on playing video on ' + device + + ' in HTML5 player', + function () + { + var btnPlay; + + window.onTouchBasedDevice.andReturn([device]); + state = jasmine.initializePlayer(); + btnPlay = state.el.find('.btn-play'); + + state.el.trigger('play'); + expect(btnPlay).toHaveClass('is-hidden'); + }); + + it( + 'is hidden on paused video on ' + device + + ' in YouTube player', + function () + { + var btnPlay; + + window.onTouchBasedDevice.andReturn([device]); + state = jasmine.initializePlayerYouTube(); + btnPlay = state.el.find('.btn-play'); + + state.el.trigger('play'); + state.el.trigger('pause'); + expect(btnPlay).toHaveClass('is-hidden'); + }); + }); + + it('starts play the video on click', function () { + $('.btn-play').click(); + expect(state.videoCommands.execute).toHaveBeenCalledWith('play'); + }); + + it('can destroy itself', function () { + state.videoPlayPlaceholder.destroy(); + expect(state.videoPlayPlaceholder).toBeUndefined(); + }); + }); +}).call(this); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_play_skip_control_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_play_skip_control_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..9ccea6a0ab9b6d55cd2b431b79357197e6ead9a4 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/video/video_play_skip_control_spec.js @@ -0,0 +1,64 @@ +(function () { + 'use strict'; + describe('VideoPlaySkipControl', function () { + var state, oldOTBD; + + beforeEach(function () { + oldOTBD = window.onTouchBasedDevice; + window.onTouchBasedDevice = jasmine + .createSpy('onTouchBasedDevice').andReturn(null); + state = jasmine.initializePlayer('video_with_bumper.html'); + $('.poster .btn-play').click(); + spyOn(state.bumperState.videoCommands, 'execute'); + }); + + afterEach(function () { + $('source').remove(); + state.storage.clear(); + if (state.bumperState && state.bumperState.videoPlayer) { + state.bumperState.videoPlayer.destroy(); + } + window.onTouchBasedDevice = oldOTBD; + }); + + it('can render the control', function () { + expect($('.video_control.play')).toExist(); + }); + + it('add ARIA attributes to play control', function () { + expect($('.video_control.play')).toHaveAttrs({ + 'role': 'button', + 'title': 'Play', + 'aria-disabled': 'false' + }); + }); + + it('can update state on play', function () { + state.el.trigger('play'); + expect($('.video_control.play')).not.toExist(); + expect($('.video_control.skip')).toExist(); + }); + + it('can start video playing on click', function () { + $('.video_control.play').click(); + expect(state.bumperState.videoCommands.execute).toHaveBeenCalledWith('play'); + }); + + it('can skip the video on click', function () { + state.el.trigger('play'); + spyOn(state.bumperState.videoPlayer, 'isPlaying').andReturn(true); + $('.video_control.skip').first().click(); + expect(state.bumperState.videoCommands.execute).toHaveBeenCalledWith('skip'); + }); + + it('can destroy itself', function () { + var plugin = state.bumperState.videoPlaySkipControl, + el = plugin.el; + spyOn($.fn, 'off').andCallThrough(); + plugin.destroy(); + expect(state.bumperState.videoPlaySkipControl).toBeUndefined(); + expect(el).not.toExist(); + expect($.fn.off).toHaveBeenCalledWith('destroy', plugin.destroy); + }); + }); +}).call(this); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js index da910d7bb0fba2e84524a36d5540d725ebe7db1c..fc74ef9229336e39e04a1de22ccb73b40670c702 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js @@ -1,5 +1,4 @@ (function (requirejs, require, define, undefined) { - 'use strict'; require( @@ -21,6 +20,9 @@ function (VideoPlayer) { if (state.storage) { state.storage.clear(); } + if (state.videoPlayer) { + _.result(state.videoPlayer, 'destroy'); + } }); describe('constructor', function () { @@ -47,7 +49,7 @@ function (VideoPlayer) { expect(state.videoCaption).toBeDefined(); expect(state.speed).toEqual('1.50'); expect(state.config.transcriptTranslationUrl) - .toEqual('/transcript/translation'); + .toEqual('/transcript/translation/__lang__'); }); it('create video speed control', function () { @@ -71,18 +73,15 @@ function (VideoPlayer) { var events; jasmine.stubRequests(); - spyOn(window.YT, 'Player').andCallThrough(); - state = jasmine.initializePlayerYouTube(); - state.videoEl = $('video, iframe'); events = { onReady: state.videoPlayer.onReady, onStateChange: state.videoPlayer.onStateChange, - onPlaybackQualityChange: state.videoPlayer - .onPlaybackQualityChange + onPlaybackQualityChange: state.videoPlayer.onPlaybackQualityChange, + onError: state.videoPlayer.onError }; expect(YT.Player).toHaveBeenCalledWith('id', { @@ -156,7 +155,7 @@ function (VideoPlayer) { }); it('controls are in paused state', function () { - expect(state.videoControl.isPlaying).toBe(false); + expect(state.videoPlayer.isPlaying()).toBe(false); }); }); }); @@ -166,16 +165,10 @@ function (VideoPlayer) { state = jasmine.initializePlayer(); state.videoEl = $('video, iframe'); - - spyOn(state.videoPlayer, 'log').andCallThrough(); spyOn(state.videoPlayer, 'play').andCallThrough(); state.videoPlayer.onReady(); }); - it('log the load_video event', function () { - expect(state.videoPlayer.log).toHaveBeenCalledWith('load_video'); - }); - it('autoplay the first video', function () { expect(state.videoPlayer.play).not.toHaveBeenCalled(); }); @@ -197,9 +190,7 @@ function (VideoPlayer) { var playbackRates = state.videoPlayer.player.getAvailablePlaybackRates(); state.currentPlayerMode = 'flash'; - state.videoPlayer.onReady(); - expect(playbackRates.length).toBe(4); expect(state.currentPlayerMode).toBe('html5'); }); @@ -209,10 +200,7 @@ function (VideoPlayer) { describe('when the video is unstarted', function () { beforeEach(function () { state = jasmine.initializePlayer(); - state.videoEl = $('video, iframe'); - - spyOn(state.videoControl, 'pause').andCallThrough(); spyOn($.fn, 'trigger').andCallThrough(); state.videoPlayer.onStateChange({ @@ -221,7 +209,7 @@ function (VideoPlayer) { }); it('pause the video control', function () { - expect(state.videoControl.pause).toHaveBeenCalled(); + expect($('.video_control')).toHaveClass('play'); }); it('pause the video caption', function () { @@ -244,9 +232,7 @@ function (VideoPlayer) { state.videoEl = $('video, iframe'); - spyOn(state.videoPlayer, 'log').andCallThrough(); spyOn(window, 'setInterval').andReturn(100); - spyOn(state.videoControl, 'play'); spyOn($.fn, 'trigger').andCallThrough(); state.videoPlayer.onStateChange({ @@ -254,23 +240,6 @@ function (VideoPlayer) { }); }); - it('speed_change_video event is not logged when speed not change', function () { - expect(state.videoPlayer.log).not.toHaveBeenCalledWith( - 'speed_change_video', - { - current_time: state.videoPlayer.currentTime, - old_speed: state.speed, - new_speed: state.speed - } - ); - }); - - it('log the play_video event', function () { - expect(state.videoPlayer.log).toHaveBeenCalledWith( - 'play_video', { currentTime: 0 } - ); - }); - it('set update interval', function () { expect(window.setInterval).toHaveBeenCalledWith( state.videoPlayer.update, 200 @@ -279,7 +248,7 @@ function (VideoPlayer) { }); it('play the video control', function () { - expect(state.videoControl.play).toHaveBeenCalled(); + expect($('.video_control')).toHaveClass('pause'); }); it('play the video caption', function () { @@ -295,10 +264,7 @@ function (VideoPlayer) { state.videoEl = $('video, iframe'); - spyOn(state.videoPlayer, 'log').andCallThrough(); - spyOn(state.videoControl, 'pause').andCallThrough(); spyOn($.fn, 'trigger').andCallThrough(); - state.videoPlayer.onStateChange({ data: YT.PlayerState.PLAYING }); @@ -310,18 +276,12 @@ function (VideoPlayer) { }); }); - it('log the pause_video event', function () { - expect(state.videoPlayer.log).toHaveBeenCalledWith( - 'pause_video', { currentTime: 0 } - ); - }); - it('clear update interval', function () { expect(state.videoPlayer.updateInterval).toBeUndefined(); }); it('pause the video control', function () { - expect(state.videoControl.pause).toHaveBeenCalled(); + expect($('.video_control')).toHaveClass('play'); }); it('pause the video caption', function () { @@ -334,32 +294,19 @@ function (VideoPlayer) { state = jasmine.initializePlayer(); state.videoEl = $('video, iframe'); - - spyOn(state.videoPlayer, 'log').andCallThrough(); - spyOn(state.videoControl, 'pause').andCallThrough(); spyOn($.fn, 'trigger').andCallThrough(); - state.videoPlayer.onStateChange({ data: YT.PlayerState.ENDED }); }); it('pause the video control', function () { - expect(state.videoControl.pause).toHaveBeenCalled(); + expect($('.video_control')).toHaveClass('play'); }); it('pause the video caption', function () { expect($.fn.trigger).toHaveBeenCalledWith('ended', {}); }); - - it('log stop_video event', function () { - expect(state.videoPlayer.log).toHaveBeenCalledWith( - 'stop_video', - { - currentTime: state.videoPlayer.currentTime - } - ); - }); }); }); @@ -397,25 +344,6 @@ function (VideoPlayer) { }); }); - it('slider event causes log update', function () { - runs(function () { - spyOn(state.videoPlayer, 'log'); - state.videoProgressSlider.onSlide( - jQuery.Event('slide'), { value: 2 } - ); - // Video player uses _.debounce (with a wait time in 300 ms) for seeking. - // That's why we have to do this tick(300). - jasmine.Clock.tick(300); - expect(state.videoPlayer.currentTime).toBe(2); - - expect(state.videoPlayer.log).toHaveBeenCalledWith('seek_video', { - old_time: jasmine.any(Number), - new_time: 2, - type: 'onSlideSeek' - }); - }); - }); - it('seek the player', function () { runs(function () { spyOn(state.videoPlayer.player, 'seekTo').andCallThrough(); @@ -469,24 +397,6 @@ function (VideoPlayer) { .andCallThrough(); }); - it('slider event causes log update', function () { - spyOn(state.videoPlayer, 'log'); - state.videoProgressSlider.onSlide( - jQuery.Event('slide'), { value: 2 } - ); - // Video player uses _.debounce (with a wait time in 300 ms) for seeking. - // That's why we have to do this tick(300). - jasmine.Clock.tick(300); - expect(state.videoPlayer.currentTime).toBe(2); - expect(state.videoPlayer.log).toHaveBeenCalledWith( - 'seek_video', { - old_time: 0, - new_time: 2, - type: 'onSlideSeek' - } - ); - }); - it('video has a correct speed', function () { state.speed = '2.0'; state.videoPlayer.onPlay(); @@ -785,7 +695,7 @@ function (VideoPlayer) { state = jasmine.initializePlayer(); state.videoEl = $('video, iframe'); spyOn($.fn, 'trigger').andCallThrough(); - state.videoControl.toggleFullScreen(jQuery.Event('click')); + $('.add-fullscreen').click(); }); it('replace the full screen button tooltip', function () { @@ -810,11 +720,10 @@ function (VideoPlayer) { state.videoEl = $('video, iframe'); spyOn($.fn, 'trigger').andCallThrough(); state.el.addClass('video-fullscreen'); - state.videoControl.fullScreenState = true; - state.videoControl.isFullScreen = true; - state.videoControl.fullScreenEl.attr('title', 'Exit-fullscreen'); - - state.videoControl.toggleFullScreen(jQuery.Event('click')); + state.videoFullScreen.fullScreenState = true; + state.videoFullScreen.isFullScreen = true; + state.videoFullScreen.fullScreenEl.attr('title', 'Exit-fullscreen'); + $('.add-fullscreen').click(); }); it('replace the full screen button tooltip', function () { @@ -835,83 +744,6 @@ function (VideoPlayer) { }); }); - describe('play', function () { - beforeEach(function () { - state = jasmine.initializePlayer(); - - state.videoEl = $('video, iframe'); - - spyOn(state.videoPlayer.player, 'playVideo').andCallThrough(); - }); - - describe('when the player is not ready', function () { - beforeEach(function () { - state.videoPlayer.player.playVideo = void 0; - state.videoPlayer.play(); - }); - - it('does nothing', function () { - expect(state.videoPlayer.player.playVideo).toBeUndefined(); - }); - }); - - describe('when the player is ready', function () { - beforeEach(function () { - state.videoPlayer.player.playVideo.andReturn(true); - state.videoPlayer.play(); - }); - - it('delegate to the player', function () { - expect(state.videoPlayer.player.playVideo).toHaveBeenCalled(); - }); - }); - }); - - describe('isPlaying', function () { - beforeEach(function () { - state = jasmine.initializePlayer(); - - state.videoEl = $('video, iframe'); - - spyOn(state.videoPlayer.player, 'getPlayerState').andCallThrough(); - }); - - describe('when the video is playing', function () { - beforeEach(function () { - state.videoPlayer.player.getPlayerState.andReturn(YT.PlayerState.PLAYING); - }); - - it('return true', function () { - expect(state.videoPlayer.isPlaying()).toBeTruthy(); - }); - }); - - describe('when the video is not playing', function () { - beforeEach(function () { - state.videoPlayer.player.getPlayerState.andReturn(YT.PlayerState.PAUSED); - }); - - it('return false', function () { - expect(state.videoPlayer.isPlaying()).toBeFalsy(); - }); - }); - }); - - describe('pause', function () { - beforeEach(function () { - state = jasmine.initializePlayer(); - - state.videoEl = $('video, iframe'); - - spyOn(state.videoPlayer.player, 'pauseVideo').andCallThrough(); - state.videoPlayer.pause(); - }); - - it('delegate to the player', function () { - expect(state.videoPlayer.player.pauseVideo).toHaveBeenCalled(); - }); - }); - describe('duration', function () { beforeEach(function () { state = jasmine.initializePlayer(); @@ -1016,9 +848,7 @@ function (VideoPlayer) { runs(function () { state = jasmine.initializePlayer(); - state.videoEl = $('video, iframe'); - controls = state.el.find('.video-controls'); }); @@ -1053,7 +883,6 @@ function (VideoPlayer) { saveState: jasmine.createSpy(), videoPlayer: { currentTime: 60, - log: jasmine.createSpy(), updatePlayTime: jasmine.createSpy(), setPlaybackRate: jasmine.createSpy(), player: jasmine.createSpyObj('player', ['setPlaybackRate']) @@ -1063,18 +892,6 @@ function (VideoPlayer) { }); describe('always', function () { - it('check if speed_change_video is logged', function () { - VideoPlayer.prototype.onSpeedChange.call(state, '0.75', false); - expect(state.videoPlayer.log).toHaveBeenCalledWith( - 'speed_change_video', - { - current_time: state.videoPlayer.currentTime, - old_speed: '1.50', - new_speed: '0.75' - } - ); - }); - it('convert the current time to the new speed', function () { state.isFlashMode.andReturn(true); VideoPlayer.prototype.onSpeedChange.call(state, '0.75', false); @@ -1083,10 +900,7 @@ function (VideoPlayer) { it('set video speed to the new speed', function () { VideoPlayer.prototype.onSpeedChange.call(state, '0.75', false); - expect(state.setSpeed).toHaveBeenCalledWith('0.75', true); - expect(state.saveState).toHaveBeenCalledWith(true, { - speed: '0.75' - }); + expect(state.setSpeed).toHaveBeenCalledWith('0.75'); expect(state.videoPlayer.setPlaybackRate) .toHaveBeenCalledWith('0.75'); }); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_poster_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_poster_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..18a6f6874c8bfd80fb473ce855ead409381dcc45 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/video/video_poster_spec.js @@ -0,0 +1,42 @@ +(function (WAIT_TIMEOUT) { + 'use strict'; + describe('VideoPoster', function () { + var state, oldOTBD; + + beforeEach(function () { + oldOTBD = window.onTouchBasedDevice; + window.onTouchBasedDevice = jasmine + .createSpy('onTouchBasedDevice').andReturn(null); + state = jasmine.initializePlayer('video_with_bumper.html'); + }); + + afterEach(function () { + $('source').remove(); + state.storage.clear(); + if (state.bumperState && state.bumperState.videoPlayer) { + state.bumperState.videoPlayer.destroy(); + } + if (state.videoPlayer) { + state.videoPlayer.destroy(); + } + window.onTouchBasedDevice = oldOTBD; + }); + + it('can render the poster', function () { + expect($('.poster')).toExist(); + expect($('.btn-play')).toExist(); + }); + + it('can start playing the video on click', function () { + $('.btn-play').click(); + waitsFor(function () { + return state.el.hasClass('is-playing'); + }, 'Player is not playing.', WAIT_TIMEOUT); + }); + + it('destroy itself on "play" event', function () { + $('.btn-play').click(); + expect($('.poster')).not.toExist(); + }); + }); +}).call(this, window.WAIT_TIMEOUT); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_progress_slider_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_progress_slider_spec.js index d7a56857190a1921b43658b65d592c689ef71b3b..491e98fae72940e6f3c9cb44a7505855ebf3b198 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_progress_slider_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_progress_slider_spec.js @@ -12,6 +12,7 @@ $('source').remove(); window.onTouchBasedDevice = oldOTBD; state.storage.clear(); + state.videoPlayer.destroy(); }); describe('constructor', function () { @@ -38,6 +39,18 @@ expect(state.videoProgressSlider.handle) .toBe('.slider .ui-slider-handle'); }); + + it('add ARIA attributes to time control', function () { + var timeControl = $('div.slider > a'); + + expect(timeControl).toHaveAttrs({ + 'role': 'slider', + 'title': 'Video position', + 'aria-disabled': 'false' + }); + + expect(timeControl).toHaveAttr('aria-valuetext'); + }); }); describe('on a touch-based device', function () { @@ -304,6 +317,13 @@ }); }); + it('can destroy itself', function () { + state = jasmine.initializePlayer(); + state.videoProgressSlider.destroy(); + expect(state.videoProgressSlider).toBeUndefined(); + expect($('.slider')).toBeEmpty(); + }); + }); }).call(this); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_quality_control_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_quality_control_spec.js index 1ade3cb9cedd00e6ca4a520ddca0a5112d855f3a..0bf3722a4c4cbeb3df367a845851e1ab871652ae 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_quality_control_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_quality_control_spec.js @@ -1,12 +1,13 @@ (function (undefined) { describe('VideoQualityControl', function () { - var state, qualityControl, qualityControlEl, videoPlayer, player; + var state, qualityControl, videoPlayer, player; afterEach(function () { $('source').remove(); if (state.storage) { state.storage.clear(); } + state.videoPlayer.destroy(); }); describe('constructor, YouTube mode', function () { @@ -105,6 +106,11 @@ expect(qualityControl.el).toHaveClass('active'); }); + it('can destroy itself', function () { + state.videoQualityControl.destroy(); + expect(state.videoQualityControl).toBeUndefined(); + expect($('.quality-control')).not.toExist(); + }); }); describe('constructor, HTML5 mode', function () { diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_save_state_plugin_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_save_state_plugin_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..7c101cdb3242b35095d83ed1ab9f170f0b66d5bc --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/video/video_save_state_plugin_spec.js @@ -0,0 +1,230 @@ +(function (undefined) { + 'use strict'; + describe('VideoPlayer Save State plugin', function () { + var state, oldOTBD; + + beforeEach(function () { + oldOTBD = window.onTouchBasedDevice; + window.onTouchBasedDevice = jasmine + .createSpy('onTouchBasedDevice') + .andReturn(null); + + jasmine.stubRequests(); + state = jasmine.initializePlayer(); + spyOn(state.storage, 'setItem'); + }); + + afterEach(function () { + $('source').remove(); + window.onTouchBasedDevice = oldOTBD; + state.storage.clear(); + if (state.videoPlayer) { + state.videoPlayer.destroy(); + } + }); + + describe('saveState function', function () { + var videoPlayerCurrentTime, newCurrentTime, speed; + + // We make sure that `currentTime` is a float. We need to test + // that Math.round() is called. + videoPlayerCurrentTime = 3.1242; + + // We have two times, because one is stored in + // `videoPlayer.currentTime`, and the other is passed directly to + // `saveState` in `data` object. In each case, there is different + // code that handles these times. They have to be different for + // test completeness sake. Also, make sure it is float, as is the + // time above. + newCurrentTime = 5.4; + speed = '0.75'; + + beforeEach(function () { + state.videoPlayer.currentTime = videoPlayerCurrentTime; + spyOn(Time, 'formatFull').andCallThrough(); + }); + + it('data is not an object, async is true', function () { + itSpec({ + asyncVal: true, + speedVal: undefined, + positionVal: videoPlayerCurrentTime, + data: undefined, + ajaxData: { + saved_video_position: Time.formatFull(Math.round(videoPlayerCurrentTime)) + } + }); + }); + + it('data contains speed, async is false', function () { + itSpec({ + asyncVal: false, + speedVal: speed, + positionVal: undefined, + data: { + speed: speed + }, + ajaxData: { + speed: speed + } + }); + }); + + it('data contains float position, async is true', function () { + itSpec({ + asyncVal: true, + speedVal: undefined, + positionVal: newCurrentTime, + data: { + saved_video_position: newCurrentTime + }, + ajaxData: { + saved_video_position: Time.formatFull(Math.round(newCurrentTime)) + } + }); + }); + + it('data contains speed and rounded position, async is false', function () { + itSpec({ + asyncVal: false, + speedVal: speed, + positionVal: Math.round(newCurrentTime), + data: { + speed: speed, + saved_video_position: Math.round(newCurrentTime) + }, + ajaxData: { + speed: speed, + saved_video_position: Time.formatFull(Math.round(newCurrentTime)) + } + }); + }); + + it('data contains empty object, async is true', function () { + itSpec({ + asyncVal: true, + speedVal: undefined, + positionVal: undefined, + data: {}, + ajaxData: {} + }); + }); + + it('data contains position 0, async is true', function () { + itSpec({ + asyncVal: true, + speedVal: undefined, + positionVal: 0, + data: { + saved_video_position: 0 + }, + ajaxData: { + saved_video_position: Time.formatFull(Math.round(0)) + } + }); + }); + + function itSpec(value) { + var asyncVal = value.asyncVal, + speedVal = value.speedVal, + positionVal = value.positionVal, + data = value.data, + ajaxData = value.ajaxData; + + state.videoSaveStatePlugin.saveState(asyncVal, data); + + if (speedVal) { + expect(state.storage.setItem).toHaveBeenCalledWith( + 'speed', + speedVal, + true + ); + } + if (positionVal) { + expect(state.storage.setItem).toHaveBeenCalledWith( + 'savedVideoPosition', + positionVal, + true + ); + expect(Time.formatFull).toHaveBeenCalledWith( + positionVal + ); + } + expect($.ajax).toHaveBeenCalledWith({ + url: state.config.saveStateUrl, + type: 'POST', + async: asyncVal, + dataType: 'json', + data: ajaxData + }); + } + }); + + it('can save state on speed change', function () { + state.el.trigger('speedchange', ['2.0']); + expect($.ajax).toHaveBeenCalledWith({ + url: state.config.saveStateUrl, + type: 'POST', + async: true, + dataType: 'json', + data: {speed: '2.0'} + }); + }); + + it('can save state on page unload', function () { + $.ajax.reset(); + state.videoSaveStatePlugin.onUnload(); + expect($.ajax).toHaveBeenCalledWith({ + url: state.config.saveStateUrl, + type: 'POST', + async: false, + dataType: 'json', + data: {saved_video_position: '00:00:00'} + }); + }); + + it('can save state on pause', function () { + state.el.trigger('pause'); + expect($.ajax).toHaveBeenCalledWith({ + url: state.config.saveStateUrl, + type: 'POST', + async: true, + dataType: 'json', + data: {saved_video_position: '00:00:00'} + }); + }); + + it('can save state on language change', function () { + state.el.trigger('language_menu:change', ['ua']); + expect(state.storage.setItem).toHaveBeenCalledWith('language', 'ua'); + }); + + it('can save information about youtube availability', function () { + state.el.trigger('youtube_availability', [true]); + expect($.ajax).toHaveBeenCalledWith({ + url: state.config.saveStateUrl, + type: 'POST', + async: true, + dataType: 'json', + data: {youtube_is_available: true} + }); + }); + + it('can destroy itself', function () { + var plugin = state.videoSaveStatePlugin; + spyOn($.fn, 'off').andCallThrough(); + state.videoSaveStatePlugin.destroy(); + expect(state.videoSaveStatePlugin).toBeUndefined(); + expect($.fn.off).toHaveBeenCalledWith({ + 'speedchange': plugin.onSpeedChange, + 'play': plugin.bindUnloadHandler, + 'pause destroy': plugin.saveStateHandler, + 'language_menu:change': plugin.onLanguageChange, + 'youtube_availability': plugin.onYoutubeAvailability + }); + expect($.fn.off).toHaveBeenCalledWith('destroy', plugin.destroy); + expect($.fn.off).toHaveBeenCalledWith('unload', plugin.onUnload); + }); + }); + +}).call(this); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_skip_control_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_skip_control_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..da3a87845b0c94aeab7c68d4bf398dd85cd1b215 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/video/video_skip_control_spec.js @@ -0,0 +1,55 @@ +(function () { + 'use strict'; + describe('VideoSkipControl', function () { + var state, oldOTBD; + + beforeEach(function () { + oldOTBD = window.onTouchBasedDevice; + window.onTouchBasedDevice = jasmine + .createSpy('onTouchBasedDevice').andReturn(null); + state = jasmine.initializePlayer('video_with_bumper.html'); + $('.poster .btn-play').click(); + spyOn(state.bumperState.videoCommands, 'execute').andCallThrough(); + }); + + afterEach(function () { + $('source').remove(); + state.storage.clear(); + if (state.bumperState && state.bumperState.videoPlayer) { + state.bumperState.videoPlayer.destroy(); + } + if (state.videoPlayer) { + state.videoPlayer.destroy(); + } + window.onTouchBasedDevice = oldOTBD; + }); + + it('can render the control when video starts playing', function () { + expect($('.skip-control')).not.toExist(); + state.el.trigger('play'); + expect($('.skip-control')).toExist(); + }); + + it('add ARIA attributes to play control', function () { + state.el.trigger('play'); + expect($('.skip-control')).toHaveAttrs({ + 'role': 'button', + 'title': 'Do not show again', + 'aria-disabled': 'false' + }); + }); + + it('can skip the video on click', function () { + spyOn(state.bumperState.videoBumper, 'skipAndDoNotShowAgain'); + state.el.trigger('play'); + $('.skip-control').click(); + expect(state.bumperState.videoCommands.execute).toHaveBeenCalledWith('skip', true); + expect(state.bumperState.videoBumper.skipAndDoNotShowAgain).toHaveBeenCalled(); + }); + + it('can destroy itself', function () { + state.bumperState.videoPlaySkipControl.destroy(); + expect(state.bumperState.videoPlaySkipControl).toBeUndefined(); + }); + }); +}).call(this); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js index 45db124f0a0efc55d2204be0738c821337342a22..d5b14e6b2d79718958fa32b2ed0452a695d82716 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js @@ -12,6 +12,7 @@ $('source').remove(); window.onTouchBasedDevice = oldOTBD; state.storage.clear(); + state.videoPlayer.destroy(); }); describe('constructor', function () { @@ -247,5 +248,13 @@ expect($('.speeds .value')).toHaveHtml('0.75x'); }); }); + + it('can destroy itself', function () { + state = jasmine.initializePlayer(); + state.videoSpeedControl.destroy(); + expect(state.videoSpeedControl).toBeUndefined(); + expect($('.video-speeds')).not.toExist(); + expect($('.speed-button')).not.toExist(); + }); }); }).call(this); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_volume_control_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_volume_control_spec.js index 7ff313f956f5c2e5d5d0e55c063fc4b53558a65b..e1edb571d3f36c2ca8672221acc675fad29020b5 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_volume_control_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_volume_control_spec.js @@ -13,6 +13,7 @@ describe('VideoVolumeControl', function () { $('source').remove(); window.onTouchBasedDevice = oldOTBD; state.storage.clear(); + state.videoPlayer.destroy(); }); it('Volume level has correct value even if cookie is broken', function () { @@ -35,8 +36,7 @@ describe('VideoVolumeControl', function () { }); it('render the volume control', function () { - expect(state.videoControl.secondaryControlsEl.html()) - .toContain('<div class="volume">\n'); + expect($('.volume')).toExist(); }); it('create the slider', function () { @@ -292,7 +292,7 @@ describe('VideoVolumeControl', function () { shiftKey: true }); }); - }) + }); describe('keyDownButtonHandler', function () { beforeEach(function () { @@ -308,6 +308,6 @@ describe('VideoVolumeControl', function () { })); expect(volumeControl.getMuteStatus()).toEqual(isMuted); }); - }) + }); }); }).call(this); diff --git a/common/lib/xmodule/xmodule/js/src/video/00_resizer.js b/common/lib/xmodule/xmodule/js/src/video/00_resizer.js index cbf7df47eecc2308f718383160305b8f27ae8f0b..f0c1debcd0af0b5bb71b85c34298af22f6e9b343 100644 --- a/common/lib/xmodule/xmodule/js/src/video/00_resizer.js +++ b/common/lib/xmodule/xmodule/js/src/video/00_resizer.js @@ -177,9 +177,8 @@ function () { } }; - var cleanDelta = function () { - delta['height'] = 0; - delta['width'] = 0; + var resetDelta = function () { + delta['height'] = delta['width'] = 0; return module; }; @@ -200,12 +199,23 @@ function () { return module; }; + var destroy = function () { + var data = getData(); + data.element.css({ + 'height': '', 'width': '', 'top': '', 'left': '' + }); + removeCallbacks(); + resetDelta(); + mode = null; + }; + initialize.apply(module, arguments); return $.extend(true, module, { align: align, alignByWidthOnly: alignByWidthOnly, alignByHeightOnly: alignByHeightOnly, + destroy: destroy, setParams: initialize, setMode: setMode, setElement: setElement, @@ -218,7 +228,7 @@ function () { delta: { add: addDelta, substract: substractDelta, - reset: cleanDelta + reset: resetDelta } }); }; diff --git a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js index 05654966f5b296839e2660bb35b5b6d96e30772d..ad7edc8b566d7d1aff67994252fa06c67fe25d7d 100644 --- a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js +++ b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js @@ -14,8 +14,8 @@ define( 'video/01_initialize.js', -['video/03_video_player.js', 'video/00_video_storage.js', 'video/00_i18n.js'], -function (VideoPlayer, VideoStorage, i18n) { +['video/03_video_player.js', 'video/00_i18n.js'], +function (VideoPlayer, i18n) { /** * @function * @@ -71,7 +71,6 @@ function (VideoPlayer, VideoStorage, i18n) { isYoutubeType: isYoutubeType, parseSpeed: parseSpeed, parseYoutubeStreams: parseYoutubeStreams, - saveState: saveState, setPlayerMode: setPlayerMode, setSpeed: setSpeed, speedToString: speedToString, @@ -145,9 +144,7 @@ function (VideoPlayer, VideoStorage, i18n) { _youtubeApiDeferred.resolve(); } - window.YT.ready(function () { - onYTApiReady(); - }); + window.YT.ready(onYTApiReady); } else { // There is only one global variable window.onYouTubeIframeAPIReady which // is supposed to be a function that will be called by the YouTube API @@ -191,9 +188,7 @@ function (VideoPlayer, VideoStorage, i18n) { // Attach a callback to our Deferred object to be called once the // YouTube API loads. window.onYouTubeIframeAPIReady.done(function () { - window.YT.ready(function () { - onYTApiReady(); - }); + window.YT.ready(onYTApiReady); }); } } else { @@ -212,20 +207,15 @@ function (VideoPlayer, VideoStorage, i18n) { // callback, which will set `state.youtubeApiAvailable` to `true`. // If something goes wrong at this stage, `state.youtubeApiAvailable` is // `false`. - _reportToServer(state, state.youtubeApiAvailable); + if (!state.youtubeIsAvailable) { + console.log('[Video info]: YouTube API is not available.'); + } + state.el.trigger('youtube_availability', [state.youtubeIsAvailable]); }, state.config.ytTestTimeout); $.getScript(document.location.protocol + '//' + state.config.ytApiUrl); } - function _reportToServer(state, youtubeIsAvailable) { - if (!youtubeIsAvailable) { - console.log('[Video info]: YouTube API is not available.'); - } - - state.saveState(true, { youtube_is_available: youtubeIsAvailable }); - } - // function _configureCaptions(state) // Configure displaying of captions. // @@ -296,8 +286,7 @@ function (VideoPlayer, VideoStorage, i18n) { state.videoType = 'html5'; - if (!state.config.sub || !state.config.sub.length) { - state.config.sub = ''; + if (!_.keys(state.config.transcriptLanguages).length) { state.config.showCaptions = false; } state.setSpeed(state.speed); @@ -328,8 +317,9 @@ function (VideoPlayer, VideoStorage, i18n) { function _initializeModules(state, i18n) { var dfd = $.Deferred(), modulesList = $.map(state.modules, function(module) { - if ($.isFunction(module)) { - return module(state, i18n); + var options = state.options[module.moduleName] || {}; + if (_.isFunction(module)) { + return module(state, i18n, options); } else if ($.isPlainObject(module)) { return module; } @@ -388,7 +378,6 @@ function (VideoPlayer, VideoStorage, i18n) { }, 'startTime': function (value) { value = parseInt(value, 10); - if (!isFinite(value) || value < 0) { return 0; } @@ -407,6 +396,13 @@ function (VideoPlayer, VideoStorage, i18n) { }, config = {}; + data = _.extend({ + startTime: 0, + endTime: null, + sub: '', + streams: '' + }, data); + $.each(data, function(option, value) { // Extract option that is in `extractKeys`. if ($.inArray(option, extractKeys) !== -1) { @@ -420,7 +416,7 @@ function (VideoPlayer, VideoStorage, i18n) { // Pre-process data. if (conversions[option]) { - if ($.isFunction(conversions[option])) { + if (_.isFunction(conversions[option])) { value = conversions[option].call(this, value); } else { throw new TypeError(option + ' is not a function.'); @@ -463,12 +459,11 @@ function (VideoPlayer, VideoStorage, i18n) { function initialize(element) { var self = this, - el = $(element).find('.video'), + el = this.el, + id = this.id, container = el.find('.video-wrapper'), - id = el.attr('id').replace(/video_/, ''), __dfd__ = $.Deferred(), - isTouch = onTouchBasedDevice() || '', - storage = VideoStorage('VideoState', id); + isTouch = onTouchBasedDevice() || ''; if (isTouch) { el.addClass('is-touch'); @@ -476,23 +471,18 @@ function (VideoPlayer, VideoStorage, i18n) { $.extend(this, { __dfd__: __dfd__, - el: el, container: container, - id: id, isFullScreen: false, - isTouch: isTouch, - storage: storage + isTouch: isTouch }); - console.log( - '[Video info]: Initializing video with id "' + id + '".' - ); + console.log('[Video info]: Initializing video with id "%s".', id); // We store all settings passed to us by the server in one place. These // are "read only", so don't modify them. All variable content lives in // 'state' object. // jQuery .data() return object with keys in lower camelCase format. - this.config = $.extend({}, _getConfiguration(el.data(), storage), { + this.config = $.extend({}, _getConfiguration(this.metadata, this.storage), { element: element, fadeOutTimeout: 1400, captionsFreezeTime: 10000, @@ -602,26 +592,18 @@ function (VideoPlayer, VideoStorage, i18n) { // true: Parsing of YouTube video IDs went OK, and we can proceed // onwards to play YouTube videos. function parseYoutubeStreams(youtubeStreams) { - var _this; - - if ( - typeof youtubeStreams === 'undefined' || - youtubeStreams.length === 0 - ) { + if (_.isUndefined(youtubeStreams) || !youtubeStreams.length) { return false; } - _this = this; this.videos = {}; - $.each(youtubeStreams.split(/,/), function (index, video) { + _.each(youtubeStreams.split(/,/), function (video) { var speed; - video = video.split(/:/); - speed = _this.speedToString(video[0]); - - _this.videos[speed] = video[1]; - }); + speed = this.speedToString(video[0]); + this.videos[speed] = video[1]; + }, this); return _.isString(this.videos['1.0']); } @@ -633,23 +615,21 @@ function (VideoPlayer, VideoStorage, i18n) { // example the length of the video can be determined from the meta // data. function fetchMetadata() { - var _this = this, + var self = this, metadataXHRs = []; this.metadata = {}; - $.each(this.videos, function (speed, url) { - var xhr = _this.getVideoMetadata(url, function (data) { + metadataXHRs = _.map(this.videos, function (url, speed) { + return self.getVideoMetadata(url, function (data) { if (data.data) { - _this.metadata[data.data.id] = data.data; + self.metadata[data.data.id] = data.data; } }); - - metadataXHRs.push(xhr); }); $.when.apply(this, metadataXHRs).done(function () { - _this.el.trigger('metadata_received'); + self.el.trigger('metadata_received'); // Not only do we trigger the "metadata_received" event, we also // set a flag to notify that metadata has been received. This @@ -657,7 +637,7 @@ function (VideoPlayer, VideoStorage, i18n) { // to know that metadata has been received. This is important in // cases when some code will subscribe to the "metadata_received" // event after it has been triggered. - _this.youtubeMetadataReceived = true; + self.youtubeMetadataReceived = true; }); } @@ -666,23 +646,21 @@ function (VideoPlayer, VideoStorage, i18n) { // // Create a separate array of available speeds. function parseSpeed() { - this.speeds = ($.map(this.videos, function (url, speed) { - return speed; - })).sort(); + this.speeds = _.keys(this.videos).sort(); } - function setSpeed(newSpeed, updateStorage) { + function setSpeed(newSpeed) { // Possible speeds for each player type. // HTML5 = [0.75, 1, 1.25, 1.5] // Youtube Flash = [0.75, 1, 1.25, 1.5] // Youtube HTML5 = [0.25, 0.5, 1, 1.5, 2] var map = { - '0.25': '0.75', // Youtube HTML5 -> HTML5 or Youtube Flash - '0.50': '0.75', // Youtube HTML5 -> HTML5 or Youtube Flash - '0.75': '0.50', // HTML5 or Youtube Flash -> Youtube HTML5 - '1.25': '1.50', // HTML5 or Youtube Flash -> Youtube HTML5 - '2.0': '1.50' // Youtube HTML5 -> HTML5 or Youtube Flash - }; + '0.25': '0.75', // Youtube HTML5 -> HTML5 or Youtube Flash + '0.50': '0.75', // Youtube HTML5 -> HTML5 or Youtube Flash + '0.75': '0.50', // HTML5 or Youtube Flash -> Youtube HTML5 + '1.25': '1.50', // HTML5 or Youtube Flash -> Youtube HTML5 + '2.0': '1.50' // Youtube HTML5 -> HTML5 or Youtube Flash + }; if (_.contains(this.speeds, newSpeed)) { this.speed = newSpeed; @@ -690,57 +668,21 @@ function (VideoPlayer, VideoStorage, i18n) { newSpeed = map[newSpeed]; this.speed = _.contains(this.speeds, newSpeed) ? newSpeed : '1.0'; } - - if (updateStorage) { - this.storage.setItem('speed', this.speed, true); - this.storage.setItem('general_speed', this.speed); - } } function getVideoMetadata(url, callback) { - var successHandler, xhr; - - if (typeof url !== 'string') { + if (!(_.isString(url))) { url = this.videos['1.0'] || ''; } - successHandler = ($.isFunction(callback)) ? callback : null; - xhr = $.ajax({ + + return $.ajax({ url: [ document.location.protocol, '//', this.config.ytTestUrl, url, '?v=2&alt=jsonc' ].join(''), dataType: 'jsonp', timeout: this.config.ytTestTimeout, - success: successHandler - }); - - return xhr; - } - - function saveState(async, data) { - - if (!($.isPlainObject(data))) { - data = { - saved_video_position: this.videoPlayer.currentTime - }; - } - - if (data.speed) { - this.storage.setItem('speed', data.speed, true); - } - - if (data.hasOwnProperty('saved_video_position')) { - this.storage.setItem('savedVideoPosition', data.saved_video_position, true); - - data.saved_video_position = Time.formatFull(data.saved_video_position); - } - - $.ajax({ - url: this.config.saveStateUrl, - type: 'POST', - async: async ? true : false, - dataType: 'json', - data: data, + success: _.isFunction(callback) ? callback : null }); } diff --git a/common/lib/xmodule/xmodule/js/src/video/02_html5_video.js b/common/lib/xmodule/xmodule/js/src/video/02_html5_video.js index d055b85d62c4523de5d115493f422629f5eeff7b..dc3fd7974b8dc199d22043b1fb3b9244bd41f1b4 100644 --- a/common/lib/xmodule/xmodule/js/src/video/02_html5_video.js +++ b/common/lib/xmodule/xmodule/js/src/video/02_html5_video.js @@ -110,6 +110,54 @@ function () { }); }; + Player.prototype.onError = function (event) { + if ($.isFunction(this.config.events.onError)) { + this.config.events.onError(); + } + }; + + Player.prototype.destroy = function () { + this.video.removeEventListener('loadedmetadata', this.onLoadedMetadata, false); + this.video.removeEventListener('play', this.onPlay, false); + this.video.removeEventListener('playing', this.onPlaying, false); + this.video.removeEventListener('pause', this.onPause, false); + this.video.removeEventListener('ended', this.onEnded, false); + this.el + .find('.video-player div').removeClass('hidden') + .end() + .find('.video-player h3').addClass('hidden') + .end().removeClass('is-initialized') + .find('.spinner').attr({'aria-hidden': 'false'}); + this.videoEl.remove(); + }; + + Player.prototype.onLoadedMetadata = function () { + this.playerState = HTML5Video.PlayerState.PAUSED; + if ($.isFunction(this.config.events.onReady)) { + this.config.events.onReady(null); + } + }; + + Player.prototype.onPlay = function () { + this.playerState = HTML5Video.PlayerState.BUFFERING; + this.callStateChangeCallback(); + }; + + Player.prototype.onPlaying = function () { + this.playerState = HTML5Video.PlayerState.PLAYING; + this.callStateChangeCallback(); + }; + + Player.prototype.onPause = function () { + this.playerState = HTML5Video.PlayerState.PAUSED; + this.callStateChangeCallback(); + }; + + Player.prototype.onEnded = function () { + this.playerState = HTML5Video.PlayerState.ENDED; + this.callStateChangeCallback(); + }; + return Player; /* @@ -152,6 +200,7 @@ function () { var isTouch = onTouchBasedDevice() || '', sourceList, _this, errorMessage, lastSource; + _.bindAll(this, 'onLoadedMetadata', 'onPlay', 'onPlaying', 'onPause', 'onEnded'); this.logs = []; // Initially we assume that el is a DOM element. If jQuery selector // fails to select something, we assume that el is an ID of a DOM @@ -226,6 +275,8 @@ function () { lastSource = this.videoEl.find('source').last(); lastSource.on('error', this.showErrorMessage.bind(this)); + lastSource.on('error', this.onError.bind(this)); + this.videoEl.on('error', this.onError.bind(this)); if (/iP(hone|od)/i.test(isTouch[0])) { this.videoEl.prop('controls', true); @@ -280,35 +331,11 @@ function () { // When the <video> tag has been processed by the browser, and it // is ready for playback, notify other parts of the VideoPlayer, // and initially pause the video. - this.video.addEventListener('loadedmetadata', function () { - _this.playerState = HTML5Video.PlayerState.PAUSED; - if ($.isFunction(_this.config.events.onReady)) { - _this.config.events.onReady(null); - } - }, false); - - // Register the 'play' event. - this.video.addEventListener('play', function () { - _this.playerState = HTML5Video.PlayerState.BUFFERING; - _this.callStateChangeCallback(); - }, false); - - this.video.addEventListener('playing', function () { - _this.playerState = HTML5Video.PlayerState.PLAYING; - _this.callStateChangeCallback(); - }, false); - - // Register the 'pause' event. - this.video.addEventListener('pause', function () { - _this.playerState = HTML5Video.PlayerState.PAUSED; - _this.callStateChangeCallback(); - }, false); - - // Register the 'ended' event. - this.video.addEventListener('ended', function () { - _this.playerState = HTML5Video.PlayerState.ENDED; - _this.callStateChangeCallback(); - }, false); + this.video.addEventListener('loadedmetadata', this.onLoadedMetadata, false); + this.video.addEventListener('play', this.onPlay, false); + this.video.addEventListener('playing', this.onPlaying, false); + this.video.addEventListener('pause', this.onPause, false); + this.video.addEventListener('ended', this.onEnded, false); // Place the <video> element on the page. this.videoEl.appendTo(this.el.find('.video-player div')); diff --git a/common/lib/xmodule/xmodule/js/src/video/035_video_accessible_menu.js b/common/lib/xmodule/xmodule/js/src/video/035_video_accessible_menu.js index 53e54f817cd9bce499821953a4d685ef25c9de0b..e1c2ce142a9ad1fa97b00627d7d973bad7194a50 100644 --- a/common/lib/xmodule/xmodule/js/src/video/035_video_accessible_menu.js +++ b/common/lib/xmodule/xmodule/js/src/video/035_video_accessible_menu.js @@ -1,308 +1,241 @@ -(function (requirejs, require, define) { - +(function(define) { +'use strict'; // VideoAccessibleMenu module. define( -'video/035_video_accessible_menu.js', -[], -function () { - - // VideoAccessibleMenu() function - what this module "exports". - return function (state) { - var dfd = $.Deferred(); - - if (state.el.find('li.video-tracks') === 0) { - dfd.resolve(); - return dfd.promise(); +'video/035_video_accessible_menu.js', [], +function() { + /** + * Video Download Transcript control module. + * @exports video/035_video_accessible_menu.js + * @constructor + * @param {jquery Element} element + * @param {Object} options + */ + var VideoAccessibleMenu = function(element, options) { + if (!(this instanceof VideoAccessibleMenu)) { + return new VideoAccessibleMenu(element, options); } - state.videoAccessibleMenu = { - value: state.storage.getItem('transcript_download_format') - }; - - _initialize(state); - dfd.resolve(); - return dfd.promise(); - }; - - // *************************************************************** - // Private functions start here. - // *************************************************************** + _.bindAll(this, 'openMenu', 'openMenuHandler', 'closeMenu', 'closeMenuHandler', 'toggleMenuHandler', + 'clickHandler', 'keyDownHandler', 'render', 'menuItemsLinksFocused', 'changeFileType', 'setValue' + ); - function _initialize(state) { - _makeFunctionsPublic(state); - _renderElements(state); - _addAriaAttributes(state); - _bindHandlers(state); - } + this.container = element; + this.options = options || {}; - // function _makeFunctionsPublic(state) - // - // Functions which will be accessible via 'state' object. When called, - // these functions will get the 'state' object as a context. - function _makeFunctionsPublic(state) { - var methodsDict = { - changeFileType: changeFileType, - setValue: setValue - }; - - state.bindTo(methodsDict, state.videoAccessibleMenu, state); - } - - // function _renderElements(state) - // - // Create any necessary DOM elements, attach them, and set their - // initial configuration. Also make the created DOM elements available - // via the 'state' object. Much easier to work this way - you don't - // have to do repeated jQuery element selects. - function _renderElements(state) { + if (this.container.find('.video-tracks')) { + this.initialize(); + } + }; - // For the time being, we assume that the menu structure is present in - // the template HTML. In the future accessible menu plugin, everything - // inside <div class='menu-container'></div> will be generated in this - // file. - var container = state.el.find('li.video-tracks>div.a11y-menu-container'), - button = container.children('a.a11y-menu-button'), - menuList = container.children('ol.a11y-menu-list'), - menuItems = menuList.children('li.a11y-menu-item'), - menuItemsLinks = menuItems.children('a.a11y-menu-item-link'), + VideoAccessibleMenu.prototype = { + /** Initializes the module. */ + initialize: function() { + this.value = this.options.storage.getItem('transcript_download_format'); + this.el = this.container.find('.video-tracks .a11y-menu-container'); + this.render(); + this.bindHandlers(); + }, + + /** + * Creates any necessary DOM elements, attach them, and set their, + * initial configuration. + */ + render: function() { + var value, msg; + // For the time being, we assume that the menu structure is present in + // the template HTML. In the future accessible menu plugin, everything + // inside <div class='menu-container'></div> will be generated in this + // file. + this.button = this.el.children('.a11y-menu-button'); + this.menuList = this.el.children('.a11y-menu-list'); + this.menuItems = this.menuList.children('.a11y-menu-item'); + this.menuItemsLinks = this.menuItems.children('.a11y-menu-item-link'); value = (function (val, activeElement) { return val || activeElement.find('a').data('value') || 'srt'; - }(state.videoAccessibleMenu.value, menuItems.filter('.active'))), + }(this.value, this.menuItems.filter('.active'))); msg = '.' + value; - $.extend(state.videoAccessibleMenu, { - container: container, - button: button, - menuList: menuList, - menuItems: menuItems, - menuItemsLinks: menuItemsLinks - }); - - if (value) { - state.videoAccessibleMenu.setValue(value); - button.text(gettext(msg)); - } - } - - function _addAriaAttributes(state) { - var menu = state.videoAccessibleMenu; - - menu.button.attr({ - 'role': 'button', - 'aria-disabled': 'false' - }); - - menu.menuList.attr('role', 'menu'); - - menu.menuItemsLinks.each(function(){ - $(this).attr({ - 'role': 'menuitem', - 'aria-disabled': 'false' + if (value) { + this.setValue(value); + this.button.text(gettext(msg)); + } + }, + + /** Bind any necessary function callbacks to DOM events. */ + bindHandlers: function() { + // Attach various events handlers to menu container. + this.el.on({ + 'mouseenter': this.openMenuHandler, + 'mouseleave': this.closeMenuHandler, + 'click': this.toggleMenuHandler, + 'keydown': this.keyDownHandler }); - }); - } - - // Get previous element in array or cyles back to the last if it is the - // first. - function _previousMenuItemLink(links, index) { - return $(links.eq(index < 1 ? links.length - 1 : index - 1)); - } - - // Get next element in array or cyles back to the first if it is the last. - function _nextMenuItemLink(links, index) { - return $(links.eq(index >= links.length - 1 ? 0 : index + 1)); - } - - function _menuItemsLinksFocused(menu) { - return menu.menuItemsLinks.is(':focus'); - } - - function _openMenu(menu, without_handler) { - // When menu items have focus, the menu stays open on - // mouseleave. A _closeMenuHandler is added to the window - // element to have clicks close the menu when they happen - // outside of it. We namespace the click event to easily remove it (and - // only it) in _closeMenu. - menu.container.addClass('open'); - menu.button.text('...'); - if (!without_handler) { - $(window).on('click.currentMenu', _closeMenuHandler.bind(menu)); - } - - // @TODO: onOpen callback - } - - function _closeMenu(menu, without_handler) { - // Remove the previously added clickHandler from window element. - var msg = '.' + menu.value; - - menu.container.removeClass('open'); - menu.button.text(gettext(msg)); - if (!without_handler) { - $(window).off('click.currentMenu'); - } - - // @TODO: onClose callback - } - - function _openMenuHandler(event) { - _openMenu(this, true); - return false; - } - - function _closeMenuHandler(event) { - // Only close the menu if no menu item link has focus or `click` event. - if (!_menuItemsLinksFocused(this) || event.type == 'click') { - _closeMenu(this, true); - } - - return false; - } - - function _toggleMenuHandler(event) { - if (this.container.hasClass('open')) { - _closeMenu(this, true); - } else { - _openMenu(this, true); - } - - return false; - } - - // Various event handlers. They all return false to stop propagation and - // prevent default behavior. - function _clickHandler(event) { - var target = $(event.currentTarget); - - this.changeFileType.call(this, event); - _closeMenu(this, true); + // Attach click and keydown event handlers to individual menu items. + this.menuItems + .on('click', 'a.a11y-menu-item-link', this.clickHandler) + .on('keydown', 'a.a11y-menu-item-link', this.keyDownHandler); + }, + + // Get previous element in array or cyles back to the last if it is the + // first. + previousMenuItemLink: function(links, index) { + return index < 1 ? links.last() : links.eq(index - 1); + }, + + // Get next element in array or cyles back to the first if it is the last. + nextMenuItemLink: function(links, index) { + return index >= links.length - 1 ? links.first() : links.eq(index + 1); + }, + + menuItemsLinksFocused: function() { + return this.menuItemsLinks.is(':focus'); + }, + + openMenu: function(withoutHandler) { + // When menu items have focus, the menu stays open on + // mouseleave. A closeMenuHandler is added to the window + // element to have clicks close the menu when they happen + // outside of it. We namespace the click event to easily remove it (and + // only it) in closeMenu. + this.el.addClass('open'); + this.button.text('...'); + if (!withoutHandler) { + $(window).on('click.currentMenu', this.closeMenuHandler); + } + // @TODO: onOpen callback + }, - return false; - } + closeMenu: function(withoutHandler) { + // Remove the previously added clickHandler from window element. + var msg = '.' + this.value; - function _keyDownHandler(event) { - var KEY = $.ui.keyCode, - keyCode = event.keyCode, - target = $(event.currentTarget), - index; + this.el.removeClass('open'); + this.button.text(gettext(msg)); + if (!withoutHandler) { + $(window).off('click.currentMenu'); + } + // @TODO: onClose callback + }, - if (target.is('a.a11y-menu-item-link')) { + openMenuHandler: function() { + this.openMenu(true); + return false; + }, - index = target.parent().index(); + closeMenuHandler: function(event) { + // Only close the menu if no menu item link has focus or `click` event. + if (!this.menuItemsLinksFocused() || event.type === 'click') { + this.closeMenu(true); + } + return false; + }, - switch (keyCode) { - // Scroll up menu, wrapping at the top. Keep menu open. - case KEY.UP: - _previousMenuItemLink(this.menuItemsLinks, index).focus(); - break; - // Scroll down menu, wrapping at the bottom. Keep menu - // open. - case KEY.DOWN: - _nextMenuItemLink(this.menuItemsLinks, index).focus(); - break; - // Close menu. - case KEY.TAB: - _closeMenu(this); - // TODO - // What has to happen here? In speed menu, tabbing backward - // will give focus to Play/Pause button and tabbing - // forward to Volume button. - break; - // Close menu, give focus to button and change - // file type. - case KEY.ENTER: - case KEY.SPACE: - this.button.focus(); - this.changeFileType.call(this, event); - _closeMenu(this); - break; - // Close menu and give focus to speed control. - case KEY.ESCAPE: - _closeMenu(this); - this.button.focus(); - break; + toggleMenuHandler: function() { + if (this.el.hasClass('open')) { + this.closeMenu(true); + } else { + this.openMenu(true); } return false; - } - else { - switch(keyCode) { - // Open menu and focus on last element of list above it. - case KEY.ENTER: - case KEY.SPACE: - case KEY.UP: - _openMenu(this); - this.menuItemsLinks.last().focus(); - break; - // Close menu. - case KEY.ESCAPE: - _closeMenu(this); - break; + }, + + // Various event handlers. They all return false to stop propagation and + // prevent default behavior. + clickHandler: function(event) { + this.changeFileType.call(this, event); + this.closeMenu(true); + return false; + }, + + keyDownHandler: function(event) { + var KEY = $.ui.keyCode, + keyCode = event.keyCode, + target = $(event.currentTarget), + index; + + if (target.is('a.a11y-menu-item-link')) { + index = target.parent().index(); + switch (keyCode) { + // Scroll up menu, wrapping at the top. Keep menu open. + case KEY.UP: + this.previousMenuItemLink(this.menuItemsLinks, index).focus(); + break; + // Scroll down menu, wrapping at the bottom. Keep menu + // open. + case KEY.DOWN: + this.nextMenuItemLink(this.menuItemsLinks, index).focus(); + break; + // Close menu. + case KEY.TAB: + this.closeMenu(); + // TODO + // What has to happen here? In speed menu, tabbing backward + // will give focus to Play/Pause button and tabbing + // forward to Volume button. + break; + // Close menu, give focus to button and change + // file type. + case KEY.ENTER: + case KEY.SPACE: + this.button.focus(); + this.changeFileType.call(this, event); + this.closeMenu(); + break; + // Close menu and give focus to speed control. + case KEY.ESCAPE: + this.closeMenu(); + this.button.focus(); + break; + } + return false; + } + else { + switch(keyCode) { + // Open menu and focus on last element of list above it. + case KEY.ENTER: + case KEY.SPACE: + case KEY.UP: + this.openMenu(); + this.menuItemsLinks.last().focus(); + break; + // Close menu. + case KEY.ESCAPE: + this.closeMenu(); + break; + } + // We do not stop propagation and default behavior on a TAB + // keypress. + return event.keyCode === KEY.TAB; } - // We do not stop propagation and default behavior on a TAB - // keypress. - return event.keyCode === KEY.TAB; +     }, + + setValue: function(value) { + this.value = value; + this.menuItems + .removeClass('active') + .find("a[data-value='" + value + "']") + .parent() + .addClass('active'); + }, + + changeFileType: function(event) { + var fileType = $(event.currentTarget).data('value'), + data = {'transcript_download_format': fileType}; + + this.setValue(fileType); + this.options.storage.setItem('transcript_download_format', fileType); + + $.ajax({ + url: this.options.saveStateUrl, + type: 'POST', + dataType: 'json', + data: data + }); } -    } - - /** - * @desc Bind any necessary function callbacks to DOM events (click, - * mousemove, etc.). - * - * @type {function} - * @access private - * - * @param {object} state The object containg the state of the video player. - * All other modules, their parameters, public variables, etc. are - * available via this object. - * - * @this {object} The global window object. - * - * @returns {undefined} - */ - function _bindHandlers(state) { - var menu = state.videoAccessibleMenu; - - // Attach various events handlers to menu container. - menu.container.on({ - 'mouseenter': _openMenuHandler.bind(menu), - 'mouseleave': _closeMenuHandler.bind(menu), - 'click': _toggleMenuHandler.bind(menu), - 'keydown': _keyDownHandler.bind(menu) - }); - - // Attach click and keydown event handlers to individual menu items. - menu.menuItems - .on('click', 'a.a11y-menu-item-link', _clickHandler.bind(menu)) - .on('keydown', 'a.a11y-menu-item-link', _keyDownHandler.bind(menu)); - } - - function setValue(value) { - var menu = this.videoAccessibleMenu; - - menu.value = value; - menu.menuItems - .removeClass('active') - .find("a[data-value='" + value + "']") - .parent() - .addClass('active'); - } - - // *************************************************************** - // Public functions start here. - // These are available via the 'state' object. Their context ('this' - // keyword) is the 'state' object. The magic private function that makes - // them available and sets up their context is makeFunctionsPublic(). - // *************************************************************** - - function changeFileType(event) { - var fileType = $(event.currentTarget).data('value'); - - this.videoAccessibleMenu.setValue(fileType); - this.saveState(true, {'transcript_download_format': fileType}); - this.storage.setItem('transcript_download_format', fileType); - } + }; + return VideoAccessibleMenu; }); - -}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); +}(RequireJS.define)); diff --git a/common/lib/xmodule/xmodule/js/src/video/03_video_player.js b/common/lib/xmodule/xmodule/js/src/video/03_video_player.js index a4c268cfa7cac835aab69f58a7da212b69f04bc5..c2307941e91e5f83e39c158ef963e391c671e58d 100644 --- a/common/lib/xmodule/xmodule/js/src/video/03_video_player.js +++ b/common/lib/xmodule/xmodule/js/src/video/03_video_player.js @@ -15,6 +15,7 @@ function (HTML5Video, Resizer) { return dfd.promise(); }, methodsDict = { + destroy: destroy, duration: duration, handlePlaybackQualityChange: handlePlaybackQualityChange, @@ -28,13 +29,14 @@ function (HTML5Video, Resizer) { isEnded: isEnded, isPlaying: isPlaying, isUnstarted: isUnstarted, - log: log, onCaptionSeek: onSeek, onEnded: onEnded, + onError: onError, onPause: onPause, onPlay: onPlay, runTimer: runTimer, stopTimer: stopTimer, + onLoadMetadataHtml5: onLoadMetadataHtml5, onPlaybackQualityChange: onPlaybackQualityChange, onReady: onReady, onSlideSeek: onSeek, @@ -49,8 +51,7 @@ function (HTML5Video, Resizer) { update: update, figureOutStartEndTime: figureOutStartEndTime, figureOutStartingTime: figureOutStartingTime, - updatePlayTime: updatePlayTime, - logStopVideo:logStopVideo + updatePlayTime: updatePlayTime }; VideoPlayer.prototype = methodsDict; @@ -80,6 +81,17 @@ function (HTML5Video, Resizer) { state.videoPlayer.onCaptionSeek = debouncedF; } + // Updates players state, once metadata is loaded for html5 player. + function onLoadMetadataHtml5() { + var player = this.videoPlayer.player.videoEl, + videoWidth = player[0].videoWidth || player.width(), + videoHeight = player[0].videoHeight || player.height(); + + _resize(this, videoWidth, videoHeight); + _updateVcrAndRegion(this); + } + + // function _initialize(state) // // Create any necessary DOM elements, attach them, and set their @@ -94,8 +106,6 @@ function (HTML5Video, Resizer) { // metadata is loaded, which normally happens just after the video // starts playing. Just after that configurations can be applied. state.videoPlayer.ready = _.once(function () { - $(window).on('unload', state.saveState); - if (!state.isFlashMode() && state.speed != '1.0') { // Work around a bug in the Youtube API that causes videos to @@ -150,20 +160,13 @@ function (HTML5Video, Resizer) { videoSources: state.config.sources, events: { onReady: state.videoPlayer.onReady, - onStateChange: state.videoPlayer.onStateChange + onStateChange: state.videoPlayer.onStateChange, + onError: state.videoPlayer.onError } }); player = state.videoEl = state.videoPlayer.player.videoEl; - - player[0].addEventListener('loadedmetadata', function () { - var videoWidth = player[0].videoWidth || player.width(), - videoHeight = player[0].videoHeight || player.height(); - - _resize(state, videoWidth, videoHeight); - - _updateVcrAndRegion(state); - }, false); + player[0].addEventListener('loadedmetadata', state.videoPlayer.onLoadMetadataHtml5, false); } else { youTubeId = state.youtubeId(); @@ -174,8 +177,8 @@ function (HTML5Video, Resizer) { events: { onReady: state.videoPlayer.onReady, onStateChange: state.videoPlayer.onStateChange, - onPlaybackQualityChange: state.videoPlayer - .onPlaybackQualityChange + onPlaybackQualityChange: state.videoPlayer.onPlaybackQualityChange, + onError: state.videoPlayer.onError } }); @@ -261,8 +264,8 @@ function (HTML5Video, Resizer) { }); } - $(window).on('resize', _.debounce(function () { - state.trigger('videoControl.updateControlsHeight', null); + $(window).on('resize.video', _.debounce(function () { + state.trigger('videoFullScreen.updateControlsHeight', null); state.el.trigger('caption:resize'); state.resizer.align(); }, 100)); @@ -292,8 +295,8 @@ function (HTML5Video, Resizer) { events: { onReady: state.videoPlayer.onReady, onStateChange: state.videoPlayer.onStateChange, - onPlaybackQualityChange: state.videoPlayer - .onPlaybackQualityChange + onPlaybackQualityChange: state.videoPlayer.onPlaybackQualityChange, + onError: state.videoPlayer.onError } }); @@ -309,6 +312,28 @@ function (HTML5Video, Resizer) { // them available and sets up their context is makeFunctionsPublic(). // *************************************************************** + function destroy() { + var player = this.videoPlayer.player; + this.el.removeClass([ + 'is-unstarted', 'is-playing', 'is-paused', 'is-buffered', + 'is-ended', 'is-cued' + ].join(' ')); + $(window).off('.video'); + this.el.trigger('destroy'); + this.el.off(); + this.videoPlayer.stopTimer(); + if (this.resizer && this.resizer.destroy) { + this.resizer.destroy(); + } + if (player && player.video) { + player.video.removeEventListener('loadedmetadata', this.videoPlayer.onLoadMetadataHtml5, false); + } + if (player && _.isFunction(player.destroy)) { + player.destroy(); + } + delete this.videoPlayer; + } + function pause() { if (this.videoPlayer.player.pauseVideo) { this.videoPlayer.player.pauseVideo(); @@ -349,9 +374,10 @@ function (HTML5Video, Resizer) { this.trigger('videoProgressSlider.notifyThroughHandleEnd', { end: true }); - // Emit `stop_video` event - this.videoPlayer.logStopVideo(); + + this.el.trigger('stop'); } + this.el.trigger('timeupdate', [this.videoPlayer.currentTime]); } } @@ -436,19 +462,8 @@ function (HTML5Video, Resizer) { } newSpeed = parseFloat(newSpeed).toFixed(2).replace(/\.00$/, '.0'); - - this.videoPlayer.log( - 'speed_change_video', - { - current_time: time, - old_speed: this.speed, - new_speed: newSpeed - } - ); - - this.setSpeed(newSpeed, true); + this.setSpeed(newSpeed); this.videoPlayer.setPlaybackRate(newSpeed); - this.saveState(true, { speed: newSpeed }); } // Every 200 ms, if the video is playing, we call the function update, via @@ -459,20 +474,12 @@ function (HTML5Video, Resizer) { var time = params.time, type = params.type, oldTime = this.videoPlayer.currentTime; - // After the user seeks, the video will start playing from // the sought point, and stop playing at the end. this.videoPlayer.goToStartTime = false; this.videoPlayer.seekTo(time); - this.videoPlayer.log( - 'seek_video', - { - old_time: oldTime, - new_time: time, - type: type - } - ); + this.el.trigger('seek', [time, oldTime, type]); } function seekTo(time) { @@ -509,7 +516,6 @@ function (HTML5Video, Resizer) { } this.videoPlayer.updatePlayTime(time, true); - this.el.trigger('seek', arguments); // the timer is stopped above; restart it. if (this.videoPlayer.isPlaying()) { @@ -534,9 +540,8 @@ function (HTML5Video, Resizer) { function onEnded() { var time = this.videoPlayer.duration(); - this.videoPlayer.logStopVideo(); - this.trigger('videoControl.pause', null); + this.trigger('videoProgressSlider.notifyThroughHandleEnd', { end: true }); @@ -544,40 +549,20 @@ function (HTML5Video, Resizer) { if (this.videoPlayer.skipOnEndedStartEndReset) { this.videoPlayer.skipOnEndedStartEndReset = undefined; } - // Sometimes `onEnded` events fires when `currentTime` not equal // `duration`. In this case, slider doesn't reach the end point of // timeline. this.videoPlayer.updatePlayTime(time); - this.el.trigger('ended', arguments); } function onPause() { - this.videoPlayer.log( - 'pause_video', - { - currentTime: this.videoPlayer.currentTime - } - ); - this.videoPlayer.stopTimer(); - - this.trigger('videoControl.pause', null); - this.saveState(true); this.el.trigger('pause', arguments); } function onPlay() { - this.videoPlayer.log( - 'play_video', - { - currentTime: this.videoPlayer.currentTime - } - ); - this.videoPlayer.runTimer(); - this.trigger('videoControl.play', null); this.trigger('videoProgressSlider.notifyThroughHandleEnd', { end: false }); @@ -591,22 +576,12 @@ function (HTML5Video, Resizer) { this.videoPlayer.player.setPlaybackQuality(value); } - function logStopVideo(){ - this.videoPlayer.log( - 'stop_video', - { - currentTime: this.videoPlayer.currentTime - } - ); - } - function onPlaybackQualityChange() { var quality; quality = this.videoPlayer.player.getPlaybackQuality(); this.trigger('videoQualityControl.onQualityChange', quality); - this.el.trigger('qualitychange', arguments); } @@ -625,8 +600,6 @@ function (HTML5Video, Resizer) { _this.videoPlayer.onVolumeChange(volume); }); - this.videoPlayer.log('load_video'); - availablePlaybackRates = this.videoPlayer.player .getAvailablePlaybackRates(); @@ -717,6 +690,10 @@ function (HTML5Video, Resizer) { } this.el.trigger('ready', arguments); + + if (this.config.autoplay) { + this.videoPlayer.play(); + } } function onStateChange(event) { @@ -755,6 +732,10 @@ function (HTML5Video, Resizer) { } } + function onError (code) { + this.el.trigger('error', [code]); + } + function figureOutStartEndTime(duration) { var videoPlayer = this.videoPlayer; @@ -937,30 +918,6 @@ function (HTML5Video, Resizer) { return Math.floor(dur); } - function log(eventName, data) { - var logInfo; - - // Default parameters that always get logged. - logInfo = { - id: this.id - }; - - // If extra parameters were passed to the log. - if (data) { - $.each(data, function (paramName, value) { - logInfo[paramName] = value; - }); - } - - if (this.isYoutubeType()) { - logInfo.code = this.youtubeId(); - } else { - logInfo.code = 'html5'; - } - - Logger.log(eventName, logInfo); - } - function onVolumeChange(volume) { this.videoPlayer.player.setVolume(volume); } diff --git a/common/lib/xmodule/xmodule/js/src/video/04_video_control.js b/common/lib/xmodule/xmodule/js/src/video/04_video_control.js index 561c8c02d65c598f573ce1a1b16fa824b2afef68..ce67eb18ad1fbb91c43e928798ec5b8d645964fc 100644 --- a/common/lib/xmodule/xmodule/js/src/video/04_video_control.js +++ b/common/lib/xmodule/xmodule/js/src/video/04_video_control.js @@ -1,5 +1,4 @@ (function (requirejs, require, define) { - // VideoControl module. define( 'video/04_video_control.js', @@ -30,24 +29,29 @@ function () { // get the 'state' object as a context. function _makeFunctionsPublic(state) { var methodsDict = { - exitFullScreenHandler: exitFullScreenHandler, + destroy: destroy, hideControls: hideControls, - hidePlayPlaceholder: hidePlayPlaceholder, - pause: pause, - play: play, show: show, showControls: showControls, - showPlayPlaceholder: showPlayPlaceholder, - toggleFullScreen: toggleFullScreen, - toggleFullScreenHandler: toggleFullScreenHandler, - togglePlayback: togglePlayback, - updateControlsHeight: updateControlsHeight, + focusFirst: focusFirst, updateVcrVidTime: updateVcrVidTime }; state.bindTo(methodsDict, state.videoControl, state); } + function destroy() { + this.el.off({ + 'mousemove': this.videoControl.showControls, + 'keydown': this.videoControl.showControls, + 'destroy': this.videoControl.destroy, + 'initialize': this.videoControl.focusFirst + }); + + this.el.off('controls:show'); + delete this.videoControl; + } + // function _renderElements(state) // // Create any necessary DOM elements, attach them, and set their initial configuration. Also @@ -55,21 +59,7 @@ function () { // way - you don't have to do repeated jQuery element selects. function _renderElements(state) { state.videoControl.el = state.el.find('.video-controls'); - // state.videoControl.el.append(el); - - state.videoControl.sliderEl = state.videoControl.el.find('.slider'); - state.videoControl.playPauseEl = state.videoControl.el.find('.video_control'); - state.videoControl.playPlaceholder = state.el.find('.btn-play'); - state.videoControl.secondaryControlsEl = state.videoControl.el.find('.secondary-controls'); - state.videoControl.fullScreenEl = state.videoControl.el.find('.add-fullscreen'); - state.videoControl.vidTimeEl = state.videoControl.el.find('.vidtime'); - - state.videoControl.fullScreenState = false; - state.videoControl.pause(); - - if (state.isTouch && state.videoType === 'html5') { - state.videoControl.showPlayPlaceholder(); - } + state.videoControl.vidTimeEl = state.videoControl.el.find('.vidtime'); if ((state.videoType === 'html5') && (state.config.autohideHtml5)) { state.videoControl.fadeOutTimeout = state.config.fadeOutTimeout; @@ -77,62 +67,23 @@ function () { state.videoControl.el.addClass('html5'); state.controlHideTimeout = setTimeout(state.videoControl.hideControls, state.videoControl.fadeOutTimeout); } - - // ARIA - // Let screen readers know that this anchor, representing the slider - // handle, behaves as a slider named 'video slider'. - state.videoControl.sliderEl.find('.ui-slider-handle').attr({ - 'role': 'slider', - 'title': gettext('Video slider') - }); - - state.videoControl.updateControlsHeight(); } // function _bindHandlers(state) // // Bind any necessary function callbacks to DOM events (click, mousemove, etc.). function _bindHandlers(state) { - state.videoControl.playPauseEl.on('click', state.videoControl.togglePlayback); - state.videoControl.fullScreenEl.on('click', state.videoControl.toggleFullScreenHandler); - state.el.on('fullscreen', function (event, isFullScreen) { - var height = state.videoControl.updateControlsHeight(); - - if (isFullScreen) { - state.resizer - .delta - .substract(height, 'height') - .setMode('both'); - - } else { - state.resizer - .delta - .reset() - .setMode('width'); - } - }); - - $(document).on('keyup', state.videoControl.exitFullScreenHandler); - if ((state.videoType === 'html5') && (state.config.autohideHtml5)) { - state.el.on('mousemove', state.videoControl.showControls); - state.el.on('keydown', state.videoControl.showControls); + state.el.on({ + 'mousemove': state.videoControl.showControls, + 'keydown': state.videoControl.showControls + }); } - // The state.previousFocus is used in video_speed_control to track - // the element that had the focus before it. - state.videoControl.playPauseEl.on('blur', function () { - state.previousFocus = 'playPause'; - }); - if (/iPad|Android/i.test(state.isTouch[0])) { - state.videoControl.playPlaceholder - .on('click', function () { - state.trigger('videoPlayer.play', null); - }); + if (state.config.focusFirstControl) { + state.el.on('initialize', state.videoControl.focusFirst); } - } - function _getControlsHeight(control) { - return control.el.height() + 0.5 * control.sliderEl.height(); + state.el.on('destroy', state.videoControl.destroy); } // *************************************************************** @@ -141,10 +92,8 @@ function () { // The magic private function that makes them available and sets up their context is makeFunctionsPublic(). // *************************************************************** - function updateControlsHeight () { - this.videoControl.height = _getControlsHeight(this.videoControl); - - return this.videoControl.height; + function focusFirst() { + this.videoControl.el.find('.vcr a, .vcr button').first().focus(); } function show() { @@ -171,13 +120,12 @@ function () { } this.controlHideTimeout = setTimeout(this.videoControl.hideControls, this.videoControl.fadeOutTimeout); - this.controlShowLock = false; } } function hideControls() { - var _this; + var _this = this; this.controlHideTimeout = null; @@ -186,12 +134,8 @@ function () { } this.controlState = 'hiding'; - - _this = this; - this.videoControl.el.fadeOut(this.videoControl.fadeOutTimeout, function () { _this.controlState = 'invisible'; - // If the focus was on the video control or the volume control, // then we must make sure to close these dialogs. Otherwise, after // next autofocus, these dialogs will be open, but the focus will @@ -203,98 +147,6 @@ function () { }); } - function showPlayPlaceholder(event) { - this.videoControl.playPlaceholder - .removeClass('is-hidden') - .attr({ - 'aria-hidden': 'false', - 'tabindex': 0 - }); - } - - function hidePlayPlaceholder(event) { - this.videoControl.playPlaceholder - .addClass('is-hidden') - .attr({ - 'aria-hidden': 'true', - 'tabindex': -1 - }); - } - - function play() { - this.videoControl.isPlaying = true; - this.videoControl.playPauseEl - .removeClass('play') - .addClass('pause') - .attr('title', gettext('Pause')); - - if (/iPad|Android/i.test(this.isTouch[0]) && this.videoType === 'html5') { - this.videoControl.hidePlayPlaceholder(); - } - } - - function pause() { - this.videoControl.isPlaying = false; - this.videoControl.playPauseEl - .removeClass('pause') - .addClass('play') - .attr('title', gettext('Play')); - - if (/iPad|Android/i.test(this.isTouch[0]) && this.videoType === 'html5') { - this.videoControl.showPlayPlaceholder(); - } - } - - function togglePlayback(event) { - event.preventDefault(); - this.videoCommands.execute('togglePlayback'); - } - - /** - * Event handler to toggle fullscreen mode. - * @param {jquery Event} event - */ - function toggleFullScreenHandler(event) { - event.preventDefault(); - this.videoCommands.execute('toggleFullScreen'); - } - - /** Toggle fullscreen mode. */ - function toggleFullScreen() { - var fullScreenClassNameEl = this.el.add(document.documentElement), - win = $(window), text; - - if (this.videoControl.fullScreenState) { - this.videoControl.fullScreenState = this.isFullScreen = false; - fullScreenClassNameEl.removeClass('video-fullscreen'); - text = gettext('Fill browser'); - win.scrollTop(this.scrollPos); - } else { - this.scrollPos = win.scrollTop(); - win.scrollTop(0); - this.videoControl.fullScreenState = this.isFullScreen = true; - fullScreenClassNameEl.addClass('video-fullscreen'); - text = gettext('Exit full browser'); - } - - this.videoControl.fullScreenEl - .attr('title', text) - .text(text); - - this.el.trigger('fullscreen', [this.isFullScreen]); - } - - /** - * Event handler to exit from fullscreen mode. - * @param {jquery Event} event - */ - function exitFullScreenHandler(event) { - if ((this.isFullScreen) && (event.keyCode === 27)) { - event.preventDefault(); - this.videoCommands.execute('toggleFullScreen'); - } - } - function updateVcrVidTime(params) { var endTime = (this.config.endTime !== null) ? this.config.endTime : params.duration; // in case endTime is accidentally specified as being greater than the video diff --git a/common/lib/xmodule/xmodule/js/src/video/04_video_full_screen.js b/common/lib/xmodule/xmodule/js/src/video/04_video_full_screen.js new file mode 100644 index 0000000000000000000000000000000000000000..e5618520579b2314cb92423a591980f50e7ae373 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/video/04_video_full_screen.js @@ -0,0 +1,175 @@ +(function (define) { +'use strict'; +define('video/04_video_full_screen.js', [], function () { + var template = [ + '<a href="#" class="add-fullscreen" title="', + gettext('Fill browser'), '" role="button" aria-disabled="false">', + gettext('Fill browser'), + '</a>' + ].join(''); + + // VideoControl() function - what this module "exports". + return function (state) { + var dfd = $.Deferred(); + + state.videoFullScreen = {}; + + _makeFunctionsPublic(state); + _renderElements(state); + _bindHandlers(state); + + dfd.resolve(); + return dfd.promise(); + }; + + // *************************************************************** + // Private functions start here. + // *************************************************************** + + // function _makeFunctionsPublic(state) + // + // Functions which will be accessible via 'state' object. When called, these functions will + // get the 'state' object as a context. + function _makeFunctionsPublic(state) { + var methodsDict = { + destroy: destroy, + enter: enter, + exitHandler: exitHandler, + exit: exit, + onFullscreenChange: onFullscreenChange, + toggle: toggle, + toggleHandler: toggleHandler, + updateControlsHeight: updateControlsHeight + }; + + state.bindTo(methodsDict, state.videoFullScreen, state); + } + + function destroy() { + $(document).off('keyup', this.videoFullScreen.exitHandler); + this.videoFullScreen.fullScreenEl.remove(); + this.el.off({ + 'fullscreen': this.videoFullScreen.onFullscreenChange, + 'destroy': this.videoFullScreen.destroy + }); + if (this.isFullScreen) { + this.videoFullScreen.exit(); + } + delete this.videoFullScreen; + } + + // function _renderElements(state) + // + // Create any necessary DOM elements, attach them, and set their initial configuration. Also + // make the created DOM elements available via the 'state' object. Much easier to work this + // way - you don't have to do repeated jQuery element selects. + function _renderElements(state) { + state.videoFullScreen.fullScreenEl = $(template); + state.videoFullScreen.sliderEl = state.el.find('.slider'); + state.videoFullScreen.fullScreenState = false; + state.el.find('.secondary-controls').append(state.videoFullScreen.fullScreenEl); + state.videoFullScreen.updateControlsHeight(); + } + + // function _bindHandlers(state) + // + // Bind any necessary function callbacks to DOM events (click, mousemove, etc.). + function _bindHandlers(state) { + state.videoFullScreen.fullScreenEl.on('click', state.videoFullScreen.toggleHandler); + state.el.on({ + 'fullscreen': state.videoFullScreen.onFullscreenChange, + 'destroy': state.videoFullScreen.destroy + }); + $(document).on('keyup', state.videoFullScreen.exitHandler); + } + + function _getControlsHeight(controls, slider) { + return controls.height() + 0.5 * slider.height(); + } + + // *************************************************************** + // Public functions start here. + // These are available via the 'state' object. Their context ('this' keyword) is the 'state' object. + // The magic private function that makes them available and sets up their context is makeFunctionsPublic(). + // *************************************************************** + + function onFullscreenChange (event, isFullScreen) { + var height = this.videoFullScreen.updateControlsHeight(); + + if (isFullScreen) { + this.resizer + .delta + .substract(height, 'height') + .setMode('both'); + + } else { + this.resizer + .delta + .reset() + .setMode('width'); + } + } + + function updateControlsHeight() { + var controls = this.el.find('.video-controls'), + slider = this.videoFullScreen.sliderEl; + this.videoFullScreen.height = _getControlsHeight(controls, slider); + return this.videoFullScreen.height; + } + + /** + * Event handler to toggle fullscreen mode. + * @param {jquery Event} event + */ + function toggleHandler(event) { + event.preventDefault(); + this.videoCommands.execute('toggleFullScreen'); + } + + function exit() { + var fullScreenClassNameEl = this.el.add(document.documentElement); + + this.videoFullScreen.fullScreenState = this.isFullScreen = false; + fullScreenClassNameEl.removeClass('video-fullscreen'); + $(window).scrollTop(this.scrollPos); + this.videoFullScreen.fullScreenEl + .attr('title', gettext('Fill browser')) + .text(gettext('Fill browser')); + this.el.trigger('fullscreen', [this.isFullScreen]); + } + + function enter() { + var fullScreenClassNameEl = this.el.add(document.documentElement); + + this.scrollPos = $(window).scrollTop(); + $(window).scrollTop(0); + this.videoFullScreen.fullScreenState = this.isFullScreen = true; + fullScreenClassNameEl.addClass('video-fullscreen'); + this.videoFullScreen.fullScreenEl + .attr('title', gettext('Exit full browser')) + .text(gettext('Exit full browser')); + this.el.trigger('fullscreen', [this.isFullScreen]); + } + + /** Toggle fullscreen mode. */ + function toggle() { + if (this.videoFullScreen.fullScreenState) { + this.videoFullScreen.exit(); + } else { + this.videoFullScreen.enter(); + } + } + + /** + * Event handler to exit from fullscreen mode. + * @param {jquery Event} event + */ + function exitHandler(event) { + if ((this.isFullScreen) && (event.keyCode === 27)) { + event.preventDefault(); + this.videoCommands.execute('toggleFullScreen'); + } + } +}); + +}(RequireJS.define)); diff --git a/common/lib/xmodule/xmodule/js/src/video/05_video_quality_control.js b/common/lib/xmodule/xmodule/js/src/video/05_video_quality_control.js index 03eb4ccad01e94f003580db05d0a788bbc1a8bad..11965fe31e71417261f2b822060e2ccf97fae89d 100644 --- a/common/lib/xmodule/xmodule/js/src/video/05_video_quality_control.js +++ b/common/lib/xmodule/xmodule/js/src/video/05_video_quality_control.js @@ -5,6 +5,12 @@ define( 'video/05_video_quality_control.js', [], function () { + var template = [ + '<a href="#" class="quality-control is-hidden" title="', + gettext('HD off'), '" role="button" aria-disabled="false">', + gettext('HD off'), + '</a>' + ].join(''); // VideoQualityControl() function - what this module "exports". return function (state) { @@ -12,7 +18,6 @@ function () { // Changing quality for now only works for YouTube videos. if (state.videoType !== 'youtube') { - state.el.find('a.quality-control').remove(); return; } @@ -36,6 +41,7 @@ function () { // get the 'state' object as a context. function _makeFunctionsPublic(state) { var methodsDict = { + destroy: destroy, fetchAvailableQualities: fetchAvailableQualities, onQualityChange: onQualityChange, showQualityControl: showQualityControl, @@ -45,16 +51,25 @@ function () { state.bindTo(methodsDict, state.videoQualityControl, state); } + function destroy() { + this.videoQualityControl.el.off({ + 'click': this.videoQualityControl.toggleQuality, + 'destroy': this.videoQualityControl.destroy + }); + this.el.off('.quality'); + this.videoQualityControl.el.remove(); + delete this.videoQualityControl; + } + // function _renderElements(state) // // Create any necessary DOM elements, attach them, and set their initial configuration. Also // make the created DOM elements available via the 'state' object. Much easier to work this // way - you don't have to do repeated jQuery element selects. function _renderElements(state) { - state.videoQualityControl.el = state.el.find('a.quality-control'); - - state.videoQualityControl.el.show(); + var element = state.videoQualityControl.el = $(template); state.videoQualityControl.quality = 'large'; + state.el.find('.secondary-controls').append(element); } // function _bindHandlers(state) @@ -64,9 +79,11 @@ function () { state.videoQualityControl.el.on('click', state.videoQualityControl.toggleQuality ); - state.el.on('play', _.once( + state.el.on('play.quality', _.once( state.videoQualityControl.fetchAvailableQualities )); + + state.el.on('destroy.quality', state.videoQualityControl.destroy); } // *************************************************************** @@ -141,7 +158,7 @@ function () { event.preventDefault(); newQuality = isHD ? 'large' : 'highres'; - + this.trigger('videoPlayer.handlePlaybackQualityChange', newQuality); } diff --git a/common/lib/xmodule/xmodule/js/src/video/06_video_progress_slider.js b/common/lib/xmodule/xmodule/js/src/video/06_video_progress_slider.js index 61da5b7ce094c0c70a18adf37784064555fe1885..d5c5439e47a2df23f86da6170254a5c5b8e16c84 100644 --- a/common/lib/xmodule/xmodule/js/src/video/06_video_progress_slider.js +++ b/common/lib/xmodule/xmodule/js/src/video/06_video_progress_slider.js @@ -12,15 +12,17 @@ define( 'video/06_video_progress_slider.js', [], function () { + var template = [ + '<div class="slider" title="', gettext('Video position'), '"></div>' + ].join(''); + // VideoProgressSlider() function - what this module "exports". return function (state) { var dfd = $.Deferred(); state.videoProgressSlider = {}; - _makeFunctionsPublic(state); _renderElements(state); - // No callbacks to DOM events (click, mousemove, etc.). dfd.resolve(); return dfd.promise(); @@ -36,6 +38,7 @@ function () { // these functions will get the 'state' object as a context. function _makeFunctionsPublic(state) { var methodsDict = { + destroy: destroy, buildSlider: buildSlider, getRangeParams: getRangeParams, onSlide: onSlide, @@ -49,6 +52,12 @@ function () { state.bindTo(methodsDict, state.videoProgressSlider, state); } + function destroy() { + this.videoProgressSlider.el.removeAttr('tabindex').slider('destroy'); + this.el.off('destroy', this.videoProgressSlider.destroy); + delete this.videoProgressSlider; + } + // function _renderElements(state) // // Create any necessary DOM elements, attach them, and set their @@ -56,8 +65,9 @@ function () { // via the 'state' object. Much easier to work this way - you don't // have to do repeated jQuery element selects. function _renderElements(state) { - state.videoProgressSlider.el = state.videoControl.sliderEl; + state.videoProgressSlider.el = $(template); + state.el.find('.video-controls').prepend(state.videoProgressSlider.el); state.videoProgressSlider.buildSlider(); _buildHandle(state); } @@ -81,6 +91,8 @@ function () { 'aria-valuemin': '0', 'aria-valuenow': state.videoPlayer.currentTime }); + + state.el.on('destroy', state.videoProgressSlider.destroy); } // *************************************************************** @@ -109,7 +121,7 @@ function () { // whole slider). Remember that endTime === null means the end-time // is set to the end of video by default. function updateStartEndTimeRegion(params) { - var left, width, start, end, duration, rangeParams; + var start, end, duration, rangeParams; // We must have a duration in order to determine the area of range. // It also must be non-zero. diff --git a/common/lib/xmodule/xmodule/js/src/video/07_video_volume_control.js b/common/lib/xmodule/xmodule/js/src/video/07_video_volume_control.js index 646fe8e732a73bd12b4d72cb81ff452106e83374..7177ee9215a42d61ee4e8b3bcab457af0069e900 100644 --- a/common/lib/xmodule/xmodule/js/src/video/07_video_volume_control.js +++ b/common/lib/xmodule/xmodule/js/src/video/07_video_volume_control.js @@ -17,6 +17,10 @@ function() { return new VolumeControl(state, i18n); } + _.bindAll(this, 'keyDownHandler', 'updateVolumeSilently', + 'onVolumeChangeHandler', 'openMenu', 'closeMenu', + 'toggleMuteHandler', 'keyDownButtonHandler', 'destroy' + ); this.state = state; this.state.videoVolumeControl = this; this.i18n = i18n; @@ -33,17 +37,55 @@ function() { /** Step to increase/decrease volume level via keyboard. */ step: 20, + template: [ + '<div class="volume">', + '<a href="#" role="button" aria-disabled="false" title="', + gettext('Volume'), '" aria-label="', + gettext('Click on this button to mute or unmute this video or press UP or DOWN buttons to increase or decrease volume level.'), + '"></a>', + '<div role="presentation" class="volume-slider-container">', + '<div class="volume-slider"></div>', + '</div>', + '</div>' + ].join(''), + + destroy: function () { + this.volumeSlider.slider('destroy'); + this.state.el.find('iframe').removeAttr('tabindex'); + this.a11y.destroy(); + this.cookie = this.a11y = null; + this.closeMenu(); + + this.state.el + .off('play.volume') + .off({ + 'keydown': this.keyDownHandler, + 'volumechange': this.onVolumeChangeHandler + }); + this.el.off({ + 'mouseenter': this.openMenu, + 'mouseleave': this.closeMenu + }); + this.button.off({ + 'mousedown': this.toggleMuteHandler, + 'keydown': this.keyDownButtonHandler, + 'focus': this.openMenu, + 'blur': this.closeMenu + }); + this.el.remove(); + delete this.state.videoVolumeControl; + }, + /** Initializes the module. */ initialize: function() { var volume; - this.el = this.state.el.find('.volume'); - if (this.state.isTouch) { // iOS doesn't support volume change - this.el.remove(); return false; } + + this.el = $(this.template); // Youtube iframe react on key buttons and has his own handlers. // So, we disallow focusing on iframe. this.state.el.find('iframe').attr('tabindex', -1); @@ -80,26 +122,28 @@ function() { // Therefore, we do not need redundant focusing on slider in TAB // order. container.find('a').attr('tabindex', -1); + this.state.el.find('.secondary-controls').append(this.el); }, /** Bind any necessary function callbacks to DOM events. */ bindHandlers: function() { this.state.el.on({ - 'keydown': this.keyDownHandler.bind(this), - 'play': _.once(this.updateVolumeSilently.bind(this)), - 'volumechange': this.onVolumeChangeHandler.bind(this) + 'keydown': this.keyDownHandler, + 'play.volume': _.once(this.updateVolumeSilently), + 'volumechange': this.onVolumeChangeHandler }); this.el.on({ - 'mouseenter': this.openMenu.bind(this), - 'mouseleave': this.closeMenu.bind(this) + 'mouseenter': this.openMenu, + 'mouseleave': this.closeMenu }); this.button.on({ 'click': false, - 'mousedown': this.toggleMuteHandler.bind(this), - 'keydown': this.keyDownButtonHandler.bind(this), - 'focus': this.openMenu.bind(this), - 'blur': this.closeMenu.bind(this) + 'mousedown': this.toggleMuteHandler, + 'keydown': this.keyDownButtonHandler, + 'focus': this.openMenu, + 'blur': this.closeMenu }); + this.state.el.on('destroy', this.destroy); }, /** @@ -343,6 +387,10 @@ function() { }; Accessibility.prototype = { + destroy: function () { + this.liveRegion.remove(); + }, + /** Initializes the module. */ initialize: function() { this.liveRegion = $('<div />', { diff --git a/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js b/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js index 8f4b95d36df60af62395d5cc7912fd1766c76bfc..c813cb48ab6e6666439d1d0ac59b87e718319de3 100644 --- a/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js +++ b/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js @@ -16,6 +16,10 @@ function (Iterator) { return new SpeedControl(state); } + _.bindAll(this, 'onSetSpeed', 'onRenderSpeed', 'clickLinkHandler', + 'keyDownLinkHandler', 'mouseEnterHandler', 'mouseLeaveHandler', + 'clickMenuHandler', 'keyDownMenuHandler', 'destroy' + ); this.state = state; this.state.videoSpeedControl = this; this.initialize(); @@ -24,24 +28,51 @@ function (Iterator) { }; SpeedControl.prototype = { + template: [ + '<div class="speeds menu-container">', + '<a class="speed-button" href="#" title="', + gettext('Speeds'), '" role="button" aria-disabled="false">', + '<span class="label">', gettext('Speed'), '</span>', + '<span class="value"></span>', + '</a>', + '<ol class="video-speeds menu" role="menu"></ol>', + '</div>' + ].join(''), + + destroy: function () { + this.el.off({ + 'mouseenter': this.mouseEnterHandler, + 'mouseleave': this.mouseLeaveHandler, + 'click': this.clickMenuHandler, + 'keydown': this.keyDownMenuHandler + }); + + this.state.el.off({ + 'speed:set': this.onSetSpeed, + 'speed:render': this.onRenderSpeed + }); + this.closeMenu(true); + this.speedsContainer.remove(); + this.el.remove(); + delete this.state.videoSpeedControl; + }, + /** Initializes the module. */ initialize: function () { var state = this.state; - this.el = state.el.find('.speeds'); - this.speedsContainer = this.el.find('.video-speeds'); - this.speedButton = this.el.find('.speed-button'); - if (!this.isPlaybackRatesSupported(state)) { - this.el.remove(); console.log( '[Video info]: playbackRate is not supported.' ); return false; } - + this.el = $(this.template); + this.speedsContainer = this.el.find('.video-speeds'); + this.speedButton = this.el.find('.speed-button'); this.render(state.speeds, state.speed); + this.setSpeed(state.speed, true, true); this.bindHandlers(); return true; @@ -51,13 +82,11 @@ function (Iterator) { * Creates any necessary DOM elements, attach them, and set their, * initial configuration. * @param {array} speeds List of speeds available for the player. - * @param {string|number} currentSpeed Current speed for the player. */ - render: function (speeds, currentSpeed) { - var self = this, - speedsContainer = this.speedsContainer, + render: function (speeds) { + var speedsContainer = this.speedsContainer, reversedSpeeds = speeds.concat().reverse(), - speedsList = $.map(reversedSpeeds, function (speed, index) { + speedsList = $.map(reversedSpeeds, function (speed) { return [ '<li data-speed="', speed, '" role="presentation">', '<a class="speed-link" href="#" role="menuitem" tabindex="-1">', @@ -69,7 +98,7 @@ function (Iterator) { speedsContainer.html(speedsList.join('')); this.speedLinks = new Iterator(speedsContainer.find('.speed-link')); - this.setSpeed(currentSpeed, true, true); + this.state.el.find('.secondary-controls').prepend(this.el); }, /** @@ -77,31 +106,34 @@ function (Iterator) { * mousemove, etc.). */ bindHandlers: function () { - var self = this; - // Attach various events handlers to the speed menu button. this.el.on({ - 'mouseenter': this.mouseEnterHandler.bind(this), - 'mouseleave': this.mouseLeaveHandler.bind(this), - 'click': this.clickMenuHandler.bind(this), - 'keydown': this.keyDownMenuHandler.bind(this) + 'mouseenter': this.mouseEnterHandler, + 'mouseleave': this.mouseLeaveHandler, + 'click': this.clickMenuHandler, + 'keydown': this.keyDownMenuHandler }); // Attach click and keydown event handlers to the individual speed // entries. this.speedsContainer.on({ - click: this.clickLinkHandler.bind(this), - keydown: this.keyDownLinkHandler.bind(this) + click: this.clickLinkHandler, + keydown: this.keyDownLinkHandler }, 'a.speed-link'); this.state.el.on({ - 'speed:set': function (event, speed) { - self.setSpeed(speed, true); - }, - 'speed:render': function (event, speeds, currentSpeed) { - self.render(speeds, currentSpeed); - } + 'speed:set': this.onSetSpeed, + 'speed:render': this.onRenderSpeed }); + this.state.el.on('destroy', this.destroy); + }, + + onSetSpeed: function (event, speed) { + this.setSpeed(speed, true); + }, + + onRenderSpeed: function (event, speeds, currentSpeed) { + this.render(speeds, currentSpeed); }, /** @@ -133,7 +165,7 @@ function (Iterator) { // element to have clicks close the menu when they happen // outside of it. if (bindEvent) { - $(window).on('click.speedMenu', this.clickMenuHandler.bind(this)); + $(window).on('click.speedMenu', this.clickMenuHandler); } this.el.addClass('is-opened'); @@ -175,7 +207,7 @@ function (Iterator) { this.currentSpeed = speed; if (!silent) { - this.el.trigger('speedchange', [speed]); + this.el.trigger('speedchange', [speed, this.state.speed]); } } }, diff --git a/common/lib/xmodule/xmodule/js/src/video/095_video_context_menu.js b/common/lib/xmodule/xmodule/js/src/video/095_video_context_menu.js index 33fbfa752bd77f625c76fcad8858e7edd93ba023..951f2a639e966a89e6f388fff166ec0f8b66e5d0 100644 --- a/common/lib/xmodule/xmodule/js/src/video/095_video_context_menu.js +++ b/common/lib/xmodule/xmodule/js/src/video/095_video_context_menu.js @@ -656,6 +656,12 @@ function (Component) { if (!state.isYoutubeType()) { state.el.find('video').contextmenu(state.el, options); + state.el.on('destroy', function () { + var contextmenu = $(this).find('video').data('contextmenu'); + if (contextmenu) { + contextmenu.destroy(); + } + }); } return $.Deferred().resolve().promise(); diff --git a/common/lib/xmodule/xmodule/js/src/video/09_bumper.js b/common/lib/xmodule/xmodule/js/src/video/09_bumper.js new file mode 100644 index 0000000000000000000000000000000000000000..7c3ee16929187c39c1f2f1bf4fe318fcbbd9c7ae --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/video/09_bumper.js @@ -0,0 +1,109 @@ +(function (define) { +'use strict'; +define('video/09_bumper.js',[], function () { + /** + * VideoBumper module. + * @exports video/09_bumper.js + * @constructor + * @param {Object} player The player factory. + * @param {Object} state The object containing the state of the video + * @return {jquery Promise} + */ + var VideoBumper = function (player, state) { + if (!(this instanceof VideoBumper)) { + return new VideoBumper(player, state); + } + + _.bindAll( + this, 'showMainVideoHandler', 'destroy', 'skipByDuration', 'destroyAndResolve' + ); + this.dfd = $.Deferred(); + this.element = state.el; + this.element.addClass('is-bumper'); + this.player = player; + this.state = state; + this.doNotShowAgain = false; + this.state.videoBumper = this; + this.bindHandlers(); + this.initialize(); + this.maxBumperDuration = 35; // seconds + }; + + VideoBumper.prototype = { + initialize: function () { + this.player(); + }, + + getPromise: function () { + return this.dfd.promise(); + }, + + showMainVideoHandler: function () { + this.state.storage.setItem('isBumperShown', true); + setTimeout(function () { + this.saveState(); + this.showMainVideo(); + }.bind(this), 20); + }, + + destroyAndResolve: function () { + this.destroy(); + this.dfd.resolve(); + }, + + showMainVideo: function () { + if (this.state.videoPlayer) { + this.destroyAndResolve(); + } else { + this.state.el.on('initialize', this.destroyAndResolve); + } + }, + + skip: function () { + this.element.trigger('skip', [this.doNotShowAgain]); + this.showMainVideoHandler(); + }, + + skipAndDoNotShowAgain: function () { + this.doNotShowAgain = true; + this.skip(); + }, + + skipByDuration: function (event, time) { + if (time > this.maxBumperDuration) { + this.element.trigger('ended'); + } + }, + + bindHandlers: function () { + var events = ['ended', 'error'].join(' '); + this.element.on(events, this.showMainVideoHandler); + this.element.on('timeupdate', this.skipByDuration); + }, + + saveState: function () { + var info = {bumper_last_view_date: true}; + if (this.doNotShowAgain) { + _.extend(info, {bumper_do_not_show_again: true}); + } + this.state.videoSaveStatePlugin.saveState(true, info); + }, + + destroy: function () { + var events = ['ended', 'error'].join(' '); + this.element.off(events, this.showMainVideoHandler); + this.element.off({ + 'timeupdate': this.skipByDuration, + 'initialize': this.destroyAndResolve + }); + this.element.removeClass('is-bumper'); + if (_.isFunction(this.state.videoPlayer.destroy)) { + this.state.videoPlayer.destroy(); + } + delete this.state.videoBumper; + } + }; + + return VideoBumper; +}); +}(RequireJS.define)); diff --git a/common/lib/xmodule/xmodule/js/src/video/09_events_bumper_plugin.js b/common/lib/xmodule/xmodule/js/src/video/09_events_bumper_plugin.js new file mode 100644 index 0000000000000000000000000000000000000000..4e18332eb238f02873dc00a7ad794f455ca84345 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/video/09_events_bumper_plugin.js @@ -0,0 +1,112 @@ +(function(define) { +'use strict'; +define('video/09_events_bumper_plugin.js', [], function() { + /** + * Events module. + * @exports video/09_events_bumper_plugin.js + * @constructor + * @param {Object} state The object containing the state of the video + * @param {Object} i18n The object containing strings with translations. + * @param {Object} options + * @return {jquery Promise} + */ + var EventsBumperPlugin = function(state, i18n, options) { + if (!(this instanceof EventsBumperPlugin)) { + return new EventsBumperPlugin(state, i18n, options); + } + + _.bindAll(this, 'onReady', 'onPlay', 'onEnded', 'onShowLanguageMenu', 'onHideLanguageMenu', 'onSkip', + 'onShowCaptions', 'onHideCaptions', 'destroy'); + this.state = state; + this.options = _.extend({}, options); + this.state.videoEventsBumperPlugin = this; + this.i18n = i18n; + this.initialize(); + + return $.Deferred().resolve().promise(); + }; + + EventsBumperPlugin.moduleName = 'EventsBumperPlugin'; + EventsBumperPlugin.prototype = { + destroy: function () { + this.state.el.off(this.events); + delete this.state.videoEventsBumperPlugin; + }, + + initialize: function() { + this.events = { + 'ready': this.onReady, + 'play': this.onPlay, + 'ended stop': this.onEnded, + 'skip': this.onSkip, + 'language_menu:show': this.onShowLanguageMenu, + 'language_menu:hide': this.onHideLanguageMenu, + 'captions:show': this.onShowCaptions, + 'captions:hide': this.onHideCaptions, + 'destroy': this.destroy + }; + this.bindHandlers(); + }, + + bindHandlers: function() { + this.state.el.on(this.events); + }, + + onReady: function () { + this.log('edx.video.bumper.loaded'); + }, + + onPlay: function () { + this.log('edx.video.bumper.played', {currentTime: this.getCurrentTime()}); + }, + + onEnded: function () { + this.log('edx.video.bumper.stopped', {currentTime: this.getCurrentTime()}); + }, + + onSkip: function (event, doNotShowAgain) { + var info = {currentTime: this.getCurrentTime()}, + eventName = 'edx.video.bumper.' + (doNotShowAgain ? 'dismissed': 'skipped'); + this.log(eventName, info); + }, + + onShowLanguageMenu: function () { + this.log('edx.video.bumper.transcript.menu.shown'); + }, + + onHideLanguageMenu: function () { + this.log('edx.video.bumper.transcript.menu.hidden'); + }, + + onShowCaptions: function () { + this.log('edx.video.bumper.transcript.shown', {currentTime: this.getCurrentTime()}); + }, + + onHideCaptions: function () { + this.log('edx.video.bumper.transcript.hidden', {currentTime: this.getCurrentTime()}); + }, + + getCurrentTime: function () { + var player = this.state.videoPlayer; + return player ? player.currentTime : 0; + }, + + getDuration: function () { + var player = this.state.videoPlayer; + return player ? player.duration() : 0; + }, + + log: function (eventName, data) { + var logInfo = _.extend({ + host_component_id: this.state.id, + bumper_id: this.state.config.sources[0] || '', + duration: this.getDuration(), + code: 'html5' + }, data, this.options.data); + Logger.log(eventName, logInfo); + } + }; + + return EventsBumperPlugin; +}); +}(RequireJS.define)); diff --git a/common/lib/xmodule/xmodule/js/src/video/09_events_plugin.js b/common/lib/xmodule/xmodule/js/src/video/09_events_plugin.js new file mode 100644 index 0000000000000000000000000000000000000000..bf25c7f92d902e6cb7c61cb458facd37679d6e33 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/video/09_events_plugin.js @@ -0,0 +1,129 @@ +(function(define) { +'use strict'; +define('video/09_events_plugin.js', [], function() { + /** + * Events module. + * @exports video/09_events_plugin.js + * @constructor + * @param {Object} state The object containing the state of the video + * @param {Object} i18n The object containing strings with translations. + * @param {Object} options + * @return {jquery Promise} + */ + var EventsPlugin = function(state, i18n, options) { + if (!(this instanceof EventsPlugin)) { + return new EventsPlugin(state, i18n, options); + } + + _.bindAll(this, 'onReady', 'onPlay', 'onPause', 'onEnded', 'onSeek', + 'onSpeedChange', 'onShowLanguageMenu', 'onHideLanguageMenu', 'onSkip', + 'onShowCaptions', 'onHideCaptions', 'destroy'); + this.state = state; + this.options = _.extend({}, options); + this.state.videoEventsPlugin = this; + this.i18n = i18n; + this.initialize(); + + return $.Deferred().resolve().promise(); + }; + + EventsPlugin.moduleName = 'EventsPlugin'; + EventsPlugin.prototype = { + destroy: function () { + this.state.el.off(this.events); + delete this.state.videoEventsPlugin; + }, + + initialize: function() { + this.events = { + 'ready': this.onReady, + 'play': this.onPlay, + 'pause': this.onPause, + 'ended stop': this.onEnded, + 'seek': this.onSeek, + 'skip': this.onSkip, + 'speedchange': this.onSpeedChange, + 'language_menu:show': this.onShowLanguageMenu, + 'language_menu:hide': this.onHideLanguageMenu, + 'captions:show': this.onShowCaptions, + 'captions:hide': this.onHideCaptions, + 'destroy': this.destroy + }; + this.bindHandlers(); + }, + + bindHandlers: function() { + this.state.el.on(this.events); + }, + + onReady: function () { + this.log('load_video'); + }, + + onPlay: function () { + this.log('play_video', {currentTime: this.getCurrentTime()}); + }, + + onPause: function () { + this.log('pause_video', {currentTime: this.getCurrentTime()}); + }, + + onEnded: function () { + this.log('stop_video', {currentTime: this.getCurrentTime()}); + }, + + onSkip: function (event, doNotShowAgain) { + var info = {currentTime: this.getCurrentTime()}, + eventName = doNotShowAgain ? 'do_not_show_again_video': 'skip_video'; + this.log(eventName, info); + }, + + onSeek: function (event, time, oldTime, type) { + this.log('seek_video', { + old_time: oldTime, + new_time: time, + type: type + }); + }, + + onSpeedChange: function (event, newSpeed, oldSpeed) { + this.log('speed_change_video', { + current_time: this.getCurrentTime(), + old_speed: oldSpeed, + new_speed: newSpeed + }); + }, + + onShowLanguageMenu: function () { + this.log('video_show_cc_menu'); + }, + + onHideLanguageMenu: function () { + this.log('video_hide_cc_menu'); + }, + + onShowCaptions: function () { + this.log('show_transcript', {current_time: this.getCurrentTime()}); + }, + + onHideCaptions: function () { + this.log('hide_transcript', {current_time: this.getCurrentTime()}); + }, + + getCurrentTime: function () { + var player = this.state.videoPlayer; + return player ? player.currentTime : 0; + }, + + log: function (eventName, data) { + var logInfo = _.extend({ + id: this.state.id, + code: this.state.isYoutubeType() ? this.state.youtubeId() : 'html5' + }, data, this.options.data); + Logger.log(eventName, logInfo); + } + }; + + return EventsPlugin; +}); +}(RequireJS.define)); diff --git a/common/lib/xmodule/xmodule/js/src/video/09_play_pause_control.js b/common/lib/xmodule/xmodule/js/src/video/09_play_pause_control.js new file mode 100644 index 0000000000000000000000000000000000000000..201c50c4728ebecbde97ff65ad47ae829ed92bf6 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/video/09_play_pause_control.js @@ -0,0 +1,87 @@ +(function(define) { +'use strict'; +define('video/09_play_pause_control.js', [], function() { + /** + * Play/pause control module. + * @exports video/09_play_pause_control.js + * @constructor + * @param {Object} state The object containing the state of the video + * @param {Object} i18n The object containing strings with translations. + * @return {jquery Promise} + */ + var PlayPauseControl = function(state, i18n) { + if (!(this instanceof PlayPauseControl)) { + return new PlayPauseControl(state, i18n); + } + + _.bindAll(this, 'play', 'pause', 'onClick', 'destroy'); + this.state = state; + this.state.videoPlayPauseControl = this; + this.i18n = i18n; + this.initialize(); + + return $.Deferred().resolve().promise(); + }; + + PlayPauseControl.prototype = { + template: [ + '<a class="video_control play" href="#" title="', + gettext('Play'), '" role="button" aria-disabled="false">', + gettext('Play'), + '</a>' + ].join(''), + + destroy: function () { + this.el.remove(); + this.state.el.off('destroy', this.destroy); + delete this.state.videoPlayPauseControl; + }, + + /** Initializes the module. */ + initialize: function() { + this.el = $(this.template); + this.render(); + this.bindHandlers(); + }, + + /** + * Creates any necessary DOM elements, attach them, and set their, + * initial configuration. + */ + render: function() { + this.state.el.find('.vcr').prepend(this.el); + }, + + /** Bind any necessary function callbacks to DOM events. */ + bindHandlers: function() { + this.el.on({ + 'click': this.onClick + }); + this.state.el.on({ + 'play': this.play, + 'pause ended': this.pause, + 'destroy': this.destroy + }); + }, + + onClick: function (event) { + event.preventDefault(); + this.state.videoCommands.execute('togglePlayback'); + }, + + play: function () { + this.el + .attr('title', this.i18n['Pause']).text(this.i18n['Pause']) + .removeClass('play').addClass('pause'); + }, + + pause: function () { + this.el + .attr('title', this.i18n['Play']).text(this.i18n['Play']) + .removeClass('pause').addClass('play'); + } + }; + + return PlayPauseControl; +}); +}(RequireJS.define)); diff --git a/common/lib/xmodule/xmodule/js/src/video/09_play_placeholder.js b/common/lib/xmodule/xmodule/js/src/video/09_play_placeholder.js new file mode 100644 index 0000000000000000000000000000000000000000..bcd20dabbfed8a6a6c0718cea4f1271754ab656e --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/video/09_play_placeholder.js @@ -0,0 +1,87 @@ +(function(define) { +'use strict'; +define('video/09_play_placeholder.js', [], function() { + /** + * Play placeholder control module. + * @exports video/09_play_placeholder.js + * @constructor + * @param {Object} state The object containing the state of the video + * @param {Object} i18n The object containing strings with translations. + * @return {jquery Promise} + */ + var PlayPlaceholder = function(state, i18n) { + if (!(this instanceof PlayPlaceholder)) { + return new PlayPlaceholder(state, i18n); + } + + _.bindAll(this, 'onClick', 'hide', 'show', 'destroy'); + this.state = state; + this.state.videoPlayPlaceholder = this; + this.i18n = i18n; + this.initialize(); + + return $.Deferred().resolve().promise(); + }; + + PlayPlaceholder.prototype = { + destroy: function () { + this.el.off('click', this.onClick); + this.state.el.on({ + 'destroy': this.destroy, + 'play': this.hide, + 'ended pause': this.show + }); + this.hide(); + delete this.state.videoPlayPlaceholder; + }, + + /** + * Indicates whether the placeholder should be shown. We display it + * for html5 videos on iPad and Android devices. + * @return {Boolean} + */ + shouldBeShown: function () { + return /iPad|Android/i.test(this.state.isTouch[0]) && !this.state.isYoutubeType(); + }, + + /** Initializes the module. */ + initialize: function() { + if (!this.shouldBeShown()) { + return false; + } + + this.el = this.state.el.find('.btn-play'); + this.bindHandlers(); + this.show(); + }, + + /** Bind any necessary function callbacks to DOM events. */ + bindHandlers: function() { + this.el.on('click', this.onClick); + this.state.el.on({ + 'destroy': this.destroy, + 'play': this.hide, + 'ended pause': this.show + }); + }, + + onClick: function () { + this.state.videoCommands.execute('play'); + }, + + hide: function () { + this.el + .addClass('is-hidden') + .attr({'aria-hidden': 'true', 'tabindex': -1}); + }, + + show: function () { + this.el + .removeClass('is-hidden') + .attr({'aria-hidden': 'false', 'tabindex': 0}); + } + }; + + return PlayPlaceholder; +}); +}(RequireJS.define)); diff --git a/common/lib/xmodule/xmodule/js/src/video/09_play_skip_control.js b/common/lib/xmodule/xmodule/js/src/video/09_play_skip_control.js new file mode 100644 index 0000000000000000000000000000000000000000..c042230c7c063a994ac383c8f534a51133135b80 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/video/09_play_skip_control.js @@ -0,0 +1,84 @@ +(function(define) { +'use strict'; +define('video/09_play_skip_control.js', [], function() { + /** + * Play/skip control module. + * @exports video/09_play_skip_control.js + * @constructor + * @param {Object} state The object containing the state of the video + * @param {Object} i18n The object containing strings with translations. + * @return {jquery Promise} + */ + var PlaySkipControl = function(state, i18n) { + if (!(this instanceof PlaySkipControl)) { + return new PlaySkipControl(state, i18n); + } + + _.bindAll(this, 'play', 'onClick', 'destroy'); + this.state = state; + this.state.videoPlaySkipControl = this; + this.i18n = i18n; + this.initialize(); + + return $.Deferred().resolve().promise(); + }; + + PlaySkipControl.prototype = { + template: [ + '<a class="video_control play play-skip-control" href="#" title="', + gettext('Play'), '" role="button" aria-disabled="false">', + gettext('Play'), + '</a>' + ].join(''), + + destroy: function () { + this.el.remove(); + this.state.el.off('destroy', this.destroy); + delete this.state.videoPlaySkipControl; + }, + + /** Initializes the module. */ + initialize: function() { + this.el = $(this.template); + this.render(); + this.bindHandlers(); + }, + + /** + * Creates any necessary DOM elements, attach them, and set their, + * initial configuration. + */ + render: function() { + this.state.el.find('.vcr').prepend(this.el); + }, + + /** Bind any necessary function callbacks to DOM events. */ + bindHandlers: function() { + this.el.on('click', this.onClick); + this.state.el.on({ + 'play': this.play, + 'destroy': this.destroy + }); + }, + + onClick: function (event) { + event.preventDefault(); + if (this.state.videoPlayer.isPlaying()) { + this.state.videoCommands.execute('skip'); + } else { + this.state.videoCommands.execute('play'); + } + }, + + play: function () { + this.el + .attr('title', gettext('Skip')).text(gettext('Skip')) + .removeClass('play').addClass('skip'); + // Disable possibility to pause the video. + this.state.el.find('video').off('click'); + } + }; + + return PlaySkipControl; +}); +}(RequireJS.define)); diff --git a/common/lib/xmodule/xmodule/js/src/video/09_poster.js b/common/lib/xmodule/xmodule/js/src/video/09_poster.js new file mode 100644 index 0000000000000000000000000000000000000000..b243200a300898207228cf9f102d244fcd3a42bb --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/video/09_poster.js @@ -0,0 +1,66 @@ +(function (define) { +'use strict'; +define('video/09_poster.js', [], function () { + /** + * Poster module. + * @exports video/09_poster.js + * @constructor + * @param {jquery Element} element + * @param {Object} options + */ + var VideoPoster = function (element, options) { + if (!(this instanceof VideoPoster)) { + return new VideoPoster(element, options); + } + + _.bindAll(this, 'onClick', 'destroy'); + this.element = element; + this.container = element.find('.video-player'); + this.options = options || {}; + this.initialize(); + }; + + VideoPoster.moduleName = 'Poster'; + VideoPoster.prototype = { + template: _.template([ + '<div class="video-pre-roll is-<%= type %> poster" ', + 'style="background-image: url(<%= url %>)">', + '<button class="btn-play">', gettext('Play video'), '</button>', + '</div>' + ].join('')), + + initialize: function () { + this.el = $(this.template({ + url: this.options.poster.url, + type: this.options.poster.type + })); + this.element.addClass('is-pre-roll'); + this.render(); + this.bindHandlers(); + }, + + bindHandlers: function () { + this.el.on('click', this.onClick); + this.element.on('destroy', this.destroy); + }, + + render: function () { + this.container.append(this.el); + }, + + onClick: function () { + if (_.isFunction(this.options.onClick)) { + this.options.onClick(); + } + this.destroy(); + }, + + destroy: function () { + this.element.off('destroy', this.destroy).removeClass('is-pre-roll'); + this.el.remove(); + } + }; + + return VideoPoster; +}); +}(RequireJS.define)); diff --git a/common/lib/xmodule/xmodule/js/src/video/09_save_state_plugin.js b/common/lib/xmodule/xmodule/js/src/video/09_save_state_plugin.js new file mode 100644 index 0000000000000000000000000000000000000000..11d2772803ccf0c7f2cd0c2d60449c700eaf234d --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/video/09_save_state_plugin.js @@ -0,0 +1,118 @@ +(function(define) { +'use strict'; +define('video/09_save_state_plugin.js', [], function() { + /** + * Save state module. + * @exports video/09_save_state_plugin.js + * @constructor + * @param {Object} state The object containing the state of the video + * @param {Object} i18n The object containing strings with translations. + * @param {Object} options + * @return {jquery Promise} + */ + var SaveStatePlugin = function(state, i18n, options) { + if (!(this instanceof SaveStatePlugin)) { + return new SaveStatePlugin(state, i18n, options); + } + + _.bindAll(this, 'onSpeedChange', 'saveStateHandler', 'bindUnloadHandler', 'onUnload', 'onYoutubeAvailability', + 'onLanguageChange', 'destroy'); + this.state = state; + this.options = _.extend({events: []}, options); + this.state.videoSaveStatePlugin = this; + this.i18n = i18n; + this.initialize(); + + return $.Deferred().resolve().promise(); + }; + + + SaveStatePlugin.moduleName = 'SaveStatePlugin'; + SaveStatePlugin.prototype = { + destroy: function () { + this.state.el.off(this.events).off('destroy', this.destroy); + $(window).off('unload', this.onUnload); + delete this.state.videoSaveStatePlugin; + }, + + initialize: function() { + this.events = { + 'speedchange': this.onSpeedChange, + 'play': this.bindUnloadHandler, + 'pause destroy': this.saveStateHandler, + 'language_menu:change': this.onLanguageChange, + 'youtube_availability': this.onYoutubeAvailability + }; + this.bindHandlers(); + }, + + bindHandlers: function() { + if (this.options.events.length) { + _.each(this.options.events, function (eventName) { + var callback; + if (_.has(this.events, eventName)) { + callback = this.events[eventName]; + this.state.el.on(eventName, callback); + } + }, this); + } else { + this.state.el.on(this.events); + } + this.state.el.on('destroy', this.destroy); + }, + + bindUnloadHandler: _.once(function () { + $(window).on('unload.video', this.onUnload); + }), + + onSpeedChange: function (event, newSpeed) { + this.saveState(true, {speed: newSpeed}); + this.state.storage.setItem('speed', newSpeed, true); + this.state.storage.setItem('general_speed', newSpeed); + }, + + saveStateHandler: function () { + this.saveState(true); + }, + + onUnload: function () { + this.saveState(); + }, + + onLanguageChange: function (event, langCode) { + this.state.storage.setItem('language', langCode); + }, + + onYoutubeAvailability: function (event, youtubeIsAvailable) { + this.saveState(true, {youtube_is_available: youtubeIsAvailable}); + }, + + saveState: function (async, data) { + if (!($.isPlainObject(data))) { + data = { + saved_video_position: this.state.videoPlayer.currentTime + }; + } + + if (data.speed) { + this.state.storage.setItem('speed', data.speed, true); + } + + if (_.has(data, 'saved_video_position')) { + this.state.storage.setItem('savedVideoPosition', data.saved_video_position, true); + data.saved_video_position = Time.formatFull(data.saved_video_position); + } + + $.ajax({ + url: this.state.config.saveStateUrl, + type: 'POST', + async: async ? true : false, + dataType: 'json', + data: data + }); + } + }; + + return SaveStatePlugin; +}); +}(RequireJS.define)); diff --git a/common/lib/xmodule/xmodule/js/src/video/09_skip_control.js b/common/lib/xmodule/xmodule/js/src/video/09_skip_control.js new file mode 100644 index 0000000000000000000000000000000000000000..c508c3ebb19c597cb3a26cfb0f1a74b76c278301 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/video/09_skip_control.js @@ -0,0 +1,74 @@ +(function(define) { +'use strict'; +// VideoSkipControl module. +define( +'video/09_skip_control.js', [], +function() { + /** + * Video skip control module. + * @exports video/09_skip_control.js + * @constructor + * @param {Object} state The object containing the state of the video + * @param {Object} i18n The object containing strings with translations. + * @return {jquery Promise} + */ + var SkipControl = function(state, i18n) { + if (!(this instanceof SkipControl)) { + return new SkipControl(state, i18n); + } + + _.bindAll(this, 'onClick', 'render', 'destroy'); + this.state = state; + this.state.videoSkipControl = this; + this.i18n = i18n; + this.initialize(); + + return $.Deferred().resolve().promise(); + }; + + SkipControl.prototype = { + template: [ + '<a class="video_control skip skip-control" href="#" title="', + gettext('Do not show again'), '" role="button" aria-disabled="false">', + gettext('Do not show again'), + '</a>' + ].join(''), + + destroy: function () { + this.el.remove(); + this.state.el.off('.skip'); + delete this.state.videoSkipControl; + }, + + /** Initializes the module. */ + initialize: function() { + this.el = $(this.template); + this.bindHandlers(); + }, + + /** + * Creates any necessary DOM elements, attach them, and set their, + * initial configuration. + */ + render: function() { + this.state.el.find('.vcr a').after(this.el); + }, + + /** Bind any necessary function callbacks to DOM events. */ + bindHandlers: function() { + this.el.on('click', this.onClick); + this.state.el.on({ + 'play.skip': _.once(this.render), + 'destroy.skip': this.destroy + }); + }, + + onClick: function (event) { + event.preventDefault(); + this.state.videoCommands.execute('skip', true); + } + }; + + return SkipControl; +}); +}(RequireJS.define)); diff --git a/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js b/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js index 27608e99a546b7f2e1692789154d5e7967d3c7e1..56d22655a66ffc0479c776e966e412cdf02b90e9 100644 --- a/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js +++ b/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js @@ -1,5 +1,4 @@ (function (define) { - // VideoCaption module. define( 'video/09_video_caption.js', @@ -24,6 +23,10 @@ function (Sjson, AsyncProcess) { return new VideoCaption(state); } + _.bindAll(this, 'toggle', 'onMouseEnter', 'onMouseLeave', 'onMovement', + 'onContainerMouseEnter', 'onContainerMouseLeave', 'fetchCaption', + 'onResize', 'pause', 'play', 'onCaptionUpdate', 'onCaptionHandler', 'destroy' + ); this.state = state; this.state.videoCaption = this; this.renderElements(); @@ -32,29 +35,61 @@ function (Sjson, AsyncProcess) { }; VideoCaption.prototype = { + langTemplate: [ + '<div class="lang menu-container">', + '<a href="#" class="hide-subtitles" title="', + gettext('Turn off captions'), '" role="button" aria-disabled="false">', + gettext('Turn off captions'), + '</a>', + '</div>' + ].join(''), + + template: [ + '<ol id="transcript-captions" class="subtitles" tabindex="0" role="group" aria-label="', + gettext('Activating an item in this group will spool the video to the corresponding time point. To skip transcript, go to previous item.'), + '">', + '<li></li>', + '</ol>' + ].join(''), + + destroy: function () { + this.state.el + .off({ + 'caption:fetch': this.fetchCaption, + 'caption:resize': this.onResize, + 'caption:update': this.onCaptionUpdate, + 'ended': this.pause, + 'fullscreen': this.onResize, + 'pause': this.pause, + 'play': this.play, + 'destroy': this.destroy + }) + .removeClass('is-captions-rendered'); + if (this.fetchXHR && this.fetchXHR.abort) { + this.fetchXHR.abort(); + } + if (this.availableTranslationsXHR && this.availableTranslationsXHR.abort) { + this.availableTranslationsXHR.abort(); + } + this.subtitlesEl.remove(); + this.container.remove(); + delete this.state.videoCaption; + }, /** * @desc Initiate rendering of elements, and set their initial configuration. * */ renderElements: function () { - var state = this.state, - languages = this.state.config.transcriptLanguages; + var languages = this.state.config.transcriptLanguages; this.loaded = false; - this.subtitlesEl = state.el.find('ol.subtitles'); - this.container = state.el.find('.lang'); - this.hideSubtitlesEl = state.el.find('a.hide-subtitles'); + this.subtitlesEl = $(this.template); + this.container = $(this.langTemplate); + this.hideSubtitlesEl = this.container.find('a.hide-subtitles'); if (_.keys(languages).length) { this.renderLanguageMenu(languages); - - if (!this.fetchCaption()) { - this.hideCaptions(true); - this.hideSubtitlesEl.hide(); - } - } else { - this.hideCaptions(true, false); - this.hideSubtitlesEl.hide(); + this.fetchCaption(); } }, @@ -64,65 +99,40 @@ function (Sjson, AsyncProcess) { * */ bindHandlers: function () { - var self = this, - state = this.state, + var state = this.state, events = [ 'mouseover', 'mouseout', 'mousedown', 'click', 'focus', 'blur', 'keydown' ].join(' '); - // Change context to VideoCaption of event handlers using `bind`. - this.hideSubtitlesEl.on('click', this.toggle.bind(this)); + this.hideSubtitlesEl.on('click', this.toggle); this.subtitlesEl .on({ - mouseenter: this.onMouseEnter.bind(this), - mouseleave: this.onMouseLeave.bind(this), - mousemove: this.onMovement.bind(this), - mousewheel: this.onMovement.bind(this), - DOMMouseScroll: this.onMovement.bind(this) + mouseenter: this.onMouseEnter, + mouseleave: this.onMouseLeave, + mousemove: this.onMovement, + mousewheel: this.onMovement, + DOMMouseScroll: this.onMovement }) - .on(events, 'li[data-index]', function (event) { - switch (event.type) { - case 'mouseover': - case 'mouseout': - self.captionMouseOverOut(event); - break; - case 'mousedown': - self.captionMouseDown(event); - break; - case 'click': - self.captionClick(event); - break; - case 'focusin': - self.captionFocus(event); - break; - case 'focusout': - self.captionBlur(event); - break; - case 'keydown': - self.captionKeyDown(event); - break; - } - }); + .on(events, 'li[data-index]', this.onCaptionHandler); if (this.showLanguageMenu) { this.container.on({ - mouseenter: this.onContainerMouseEnter.bind(this), - mouseleave: this.onContainerMouseLeave.bind(this) + mouseenter: this.onContainerMouseEnter, + mouseleave: this.onContainerMouseLeave }); } state.el .on({ - 'caption:fetch': this.fetchCaption.bind(this), - 'caption:resize': this.onResize.bind(this), - 'caption:update': function (event, time) { - self.updatePlayTime(time); - }, - 'ended': this.pause.bind(this), - 'fullscreen': this.onResize.bind(this), - 'pause': this.pause.bind(this), - 'play': this.play.bind(this) + 'caption:fetch': this.fetchCaption, + 'caption:resize': this.onResize, + 'caption:update': this.onCaptionUpdate, + 'ended': this.pause, + 'fullscreen': this.onResize, + 'pause': this.pause, + 'play': this.play, + 'destroy': this.destroy }); if ((state.videoType === 'html5') && (state.config.autohideHtml5)) { @@ -130,6 +140,33 @@ function (Sjson, AsyncProcess) { } }, + onCaptionUpdate: function (event, time) { + this.updatePlayTime(time); + }, + + onCaptionHandler: function (event) { + switch (event.type) { + case 'mouseover': + case 'mouseout': + this.captionMouseOverOut(event); + break; + case 'mousedown': + this.captionMouseDown(event); + break; + case 'click': + this.captionClick(event); + break; + case 'focusin': + this.captionFocus(event); + break; + case 'focusout': + this.captionBlur(event); + break; + case 'keydown': + this.captionKeyDown(event); + break; + } + }, /** * @desc Opens language menu. @@ -138,8 +175,8 @@ function (Sjson, AsyncProcess) { */ onContainerMouseEnter: function (event) { event.preventDefault(); - this.state.videoPlayer.log('video_show_cc_menu', {}); $(event.currentTarget).addClass('is-opened'); + this.state.el.trigger('language_menu:show'); }, /** @@ -149,8 +186,8 @@ function (Sjson, AsyncProcess) { */ onContainerMouseLeave: function (event) { event.preventDefault(); - this.state.videoPlayer.log('video_hide_cc_menu', {}); $(event.currentTarget).removeClass('is-opened'); + this.state.el.trigger('language_menu:hide'); }, /** @@ -247,12 +284,11 @@ function (Sjson, AsyncProcess) { var self = this, state = this.state, language = state.getCurrentLanguage(), + url = state.config.transcriptTranslationUrl.replace('__lang__', language), data, youtubeId; if (this.loaded) { this.hideCaptions(false); - } else { - this.hideCaptions(state.hide_captions, false); } if (this.fetchXHR && this.fetchXHR.abort) { @@ -266,16 +302,14 @@ function (Sjson, AsyncProcess) { return false; } - data = { - videoId: youtubeId - }; + data = {videoId: youtubeId}; } state.el.removeClass('is-captions-rendered'); // Fetch the captions file. If no file was specified, or if an error // occurred, then we hide the captions panel, and the "CC" button this.fetchXHR = $.ajaxWithPrefix({ - url: state.config.transcriptTranslationUrl + '/' + language, + url: url, notifyOnError: false, data: data, success: function (sjson) { @@ -300,7 +334,9 @@ function (Sjson, AsyncProcess) { } else { self.renderCaption(start, captions); } - + self.hideCaptions(state.hide_captions, false); + self.state.el.find('.video-wrapper').after(self.subtitlesEl); + self.state.el.find('.secondary-controls').append(self.container); self.bindHandlers(); } @@ -336,7 +372,7 @@ function (Sjson, AsyncProcess) { var self = this, state = this.state; - return $.ajaxWithPrefix({ + this.availableTranslationsXHR = $.ajaxWithPrefix({ url: state.config.transcriptAvailableTranslationsUrl, notifyOnError: false, success: function (response) { @@ -359,6 +395,8 @@ function (Sjson, AsyncProcess) { self.hideSubtitlesEl.hide(); } }); + + return this.availableTranslationsXHR; }, /** @@ -417,11 +455,11 @@ function (Sjson, AsyncProcess) { if (state.lang !== langCode) { state.lang = langCode; - state.storage.setItem('language', langCode); el .addClass('is-active') .siblings('li') .removeClass('is-active'); + state.el.trigger('language_menu:change', [langCode]); self.fetchCaption(); } }); @@ -658,7 +696,7 @@ function (Sjson, AsyncProcess) { * */ play: function () { - var startAndCaptions, start, end; + var captions, startAndCaptions, start; if (this.loaded) { if (!this.rendered) { startAndCaptions = this.getBoundedCaptions(); @@ -689,10 +727,7 @@ function (Sjson, AsyncProcess) { */ updatePlayTime: function (time) { var state = this.state, - startTime, - endTime, - params, - newIndex; + params, newIndex; if (this.loaded) { if (state.isFlashMode()) { @@ -797,9 +832,9 @@ function (Sjson, AsyncProcess) { event.preventDefault(); if (this.state.el.hasClass('closed')) { - this.hideCaptions(false); + this.hideCaptions(false, true, true); } else { - this.hideCaptions(true); + this.hideCaptions(true, true, true); } }, @@ -811,38 +846,35 @@ function (Sjson, AsyncProcess) { * @param {boolean} update_cookie Flag to update or not the cookie. * */ - hideCaptions: function (hide_captions, update_cookie) { + hideCaptions: function (hide_captions, update_cookie, trigger_event) { var hideSubtitlesEl = this.hideSubtitlesEl, - state = this.state, - type, text; + state = this.state, text; if (typeof update_cookie === 'undefined') { update_cookie = true; } if (hide_captions) { - type = 'hide_transcript'; state.captionsHidden = true; state.el.addClass('closed'); text = gettext('Turn on captions'); + if (trigger_event) { + this.state.el.trigger('captions:hide'); + } } else { - type = 'show_transcript'; state.captionsHidden = false; state.el.removeClass('closed'); this.scrollCaption(); text = gettext('Turn off captions'); + if (trigger_event) { + this.state.el.trigger('captions:show'); + } } hideSubtitlesEl .attr('title', text) .text(gettext(text)); - if (state.videoPlayer) { - state.videoPlayer.log(type, { - currentTime: state.videoPlayer.currentTime - }); - } - if (state.resizer) { if (state.isFullScreen) { state.resizer.setMode('both'); @@ -868,9 +900,8 @@ function (Sjson, AsyncProcess) { */ captionHeight: function () { var state = this.state; - if (state.isFullScreen) { - return state.container.height() - state.videoControl.height; + return state.container.height() - state.videoFullScreen.height; } else { return state.container.height(); } @@ -889,8 +920,8 @@ function (Sjson, AsyncProcess) { ) { // In case of html5 autoshowing subtitles, we adjust height of // subs, by height of scrollbar. - height = state.videoControl.el.height() + - 0.5 * state.videoControl.sliderEl.height(); + height = state.el.find('.video-controls').height() + + 0.5 * state.el.find('.slider').height(); // Height of videoControl does not contain height of slider. // css is set to absolute, to avoid yanking when slider // autochanges its height. diff --git a/common/lib/xmodule/xmodule/js/src/video/10_commands.js b/common/lib/xmodule/xmodule/js/src/video/10_commands.js index ad78066864d34e5924b11c27069cb5cf19d54ee4..5ed94c929645fd45eb93e7d9e9056d7031acba5e 100644 --- a/common/lib/xmodule/xmodule/js/src/video/10_commands.js +++ b/common/lib/xmodule/xmodule/js/src/video/10_commands.js @@ -1,11 +1,8 @@ (function(define) { 'use strict'; -// VideoCommands module. define('video/10_commands.js', [], function() { var VideoCommands, Command, playCommand, pauseCommand, togglePlaybackCommand, - muteCommand, unmuteCommand, toggleMuteCommand, toggleFullScreenCommand, - setSpeedCommand; - + toggleMuteCommand, toggleFullScreenCommand, setSpeedCommand, skipCommand; /** * Video commands module. * @exports video/10_commands.js @@ -19,6 +16,7 @@ define('video/10_commands.js', [], function() { return new VideoCommands(state, i18n); } + _.bindAll(this, 'destroy'); this.state = state; this.state.videoCommands = this; this.i18n = i18n; @@ -29,9 +27,15 @@ define('video/10_commands.js', [], function() { }; VideoCommands.prototype = { + destroy: function () { + this.state.el.off('destroy', this.destroy); + delete this.state.videoCommands; + }, + /** Initializes the module. */ initialize: function() { this.commands = this.getCommands(); + this.state.el.on('destroy', this.destroy); }, execute: function (command) { @@ -48,7 +52,8 @@ define('video/10_commands.js', [], function() { var commands = {}, commandsList = [ playCommand, pauseCommand, togglePlaybackCommand, - toggleMuteCommand, toggleFullScreenCommand, setSpeedCommand + toggleMuteCommand, toggleFullScreenCommand, setSpeedCommand, + skipCommand ]; _.each(commandsList, function(command) { @@ -73,7 +78,7 @@ define('video/10_commands.js', [], function() { }); togglePlaybackCommand = new Command('togglePlayback', function (state) { - if (state.videoControl.isPlaying) { + if (state.videoPlayer.isPlaying()) { pauseCommand.execute(state); } else { playCommand.execute(state); @@ -85,13 +90,21 @@ define('video/10_commands.js', [], function() { }); toggleFullScreenCommand = new Command('toggleFullScreen', function (state) { - state.videoControl.toggleFullScreen(); + state.videoFullScreen.toggle(); }); setSpeedCommand = new Command('speed', function (state, speed) { state.videoSpeedControl.setSpeed(state.speedToString(speed)); }); + skipCommand = new Command('skip', function (state, doNotShowAgain) { + if (doNotShowAgain) { + state.videoBumper.skipAndDoNotShowAgain(); + } else { + state.videoBumper.skip(); + } + }); + return VideoCommands; }); }(RequireJS.define)); diff --git a/common/lib/xmodule/xmodule/js/src/video/10_main.js b/common/lib/xmodule/xmodule/js/src/video/10_main.js index 1ac539111726776ab91fd82806e5d1a9db44ad03..be3bbc8ae87f5952deec404442ba72f4f03f6571 100644 --- a/common/lib/xmodule/xmodule/js/src/video/10_main.js +++ b/common/lib/xmodule/xmodule/js/src/video/10_main.js @@ -1,6 +1,5 @@ (function (require, $) { 'use strict'; - // In the case when the Video constructor will be called before RequireJS finishes loading all of the Video // dependencies, we will have a mock function that will collect all the elements that must be initialized as // Video elements. @@ -35,74 +34,122 @@ // Main module. require( [ + 'video/00_video_storage.js', 'video/01_initialize.js', 'video/025_focus_grabber.js', 'video/035_video_accessible_menu.js', 'video/04_video_control.js', + 'video/04_video_full_screen.js', 'video/05_video_quality_control.js', 'video/06_video_progress_slider.js', 'video/07_video_volume_control.js', 'video/08_video_speed_control.js', 'video/09_video_caption.js', + 'video/09_play_placeholder.js', + 'video/09_play_pause_control.js', + 'video/09_play_skip_control.js', + 'video/09_skip_control.js', + 'video/09_bumper.js', + 'video/09_save_state_plugin.js', + 'video/09_events_plugin.js', + 'video/09_events_bumper_plugin.js', + 'video/09_poster.js', 'video/10_commands.js', 'video/095_video_context_menu.js' ], function ( - initialize, - FocusGrabber, - VideoAccessibleMenu, - VideoControl, - VideoQualityControl, - VideoProgressSlider, - VideoVolumeControl, - VideoSpeedControl, - VideoCaption, - VideoCommands, + VideoStorage, initialize, FocusGrabber, VideoAccessibleMenu, VideoControl, VideoFullScreen, + VideoQualityControl, VideoProgressSlider, VideoVolumeControl, VideoSpeedControl, VideoCaption, + VideoPlayPlaceholder, VideoPlayPauseControl, VideoPlaySkipControl, VideoSkipControl, VideoBumper, + VideoSaveStatePlugin, VideoEventsPlugin, VideoEventsBumperPlugin, VideoPoster, VideoCommands, VideoContextMenu ) { var youtubeXhr = null, oldVideo = window.Video; window.Video = function (element) { - var previousState = window.Video.previousState, - state; - - // Check for existance of previous state, uninitialize it if necessary, and create a new state. Store - // new state for future invocation of this module consturctor function. - if (previousState && previousState.videoPlayer) { - previousState.saveState(true); - $(window).off('unload', previousState.saveState); + var el = $(element).find('.video'), + id = el.attr('id').replace(/video_/, ''), + storage = VideoStorage('VideoState', id), + bumperMetadata = el.data('bumper-metadata'), + mainVideoModules = [FocusGrabber, VideoControl, VideoPlayPlaceholder, + VideoPlayPauseControl, VideoProgressSlider, VideoSpeedControl, VideoVolumeControl, + VideoQualityControl, VideoFullScreen, VideoCaption, VideoCommands, VideoContextMenu, + VideoSaveStatePlugin, VideoEventsPlugin], + bumperVideoModules = [VideoControl, VideoPlaySkipControl, VideoSkipControl, + VideoVolumeControl, VideoCaption, VideoCommands, VideoSaveStatePlugin, VideoEventsBumperPlugin], + state = { + el: el, + id: id, + metadata: el.data('metadata'), + storage: storage, + options: {}, + youtubeXhr: youtubeXhr, + modules: mainVideoModules + }; + + var getBumperState = function (metadata) { + var bumperState = $.extend(true, { + el: el, + id: id, + storage: storage, + options: {}, + youtubeXhr: youtubeXhr + }, {metadata: metadata}); + + bumperState.modules = bumperVideoModules; + bumperState.options = { + SaveStatePlugin: {events: ['language_menu:change']} + }; + return bumperState; + }; + + var player = function (state) { + return function () { + _.extend(state.metadata, {autoplay: true, focusFirstControl: true}); + initialize(state, element); + }; + }; + + new VideoAccessibleMenu(el, { + storage: storage, + saveStateUrl: state.metadata.saveStateUrl + }); + + if (bumperMetadata) { + new VideoPoster(el, { + poster: el.data('poster'), + onClick: _.once(function () { + var mainVideoPlayer = player(state), bumper, bumperState; + if (storage.getItem('isBumperShown')) { + mainVideoPlayer(); + } else { + bumperState = getBumperState(bumperMetadata); + bumper = new VideoBumper(player(bumperState), bumperState); + state.bumperState = bumperState; + bumper.getPromise().done(function () { + delete state.bumperState; + mainVideoPlayer(); + }); + } + }) + }); + } else { + initialize(state, element); } - state = {}; - // Because this constructor can be called multiple times on a single page (when the user switches - // verticals, the page doesn't reload, but the content changes), we must will check each time if there - // is a previous copy of 'state' object. If there is, we will make sure that copy exists cleanly. We - // have to do this because when verticals switch, the code does not handle any Xmodule JS code that is - // running - it simply removes DOM elements from the page. Any functions that were running during this, - // and that will run afterwards (expecting the DOM elements to be present) must be stopped by hand. - window.Video.previousState = state; - - state.modules = [ - FocusGrabber, - VideoAccessibleMenu, - VideoControl, - VideoQualityControl, - VideoProgressSlider, - VideoVolumeControl, - VideoSpeedControl, - VideoCaption, - VideoCommands, - VideoContextMenu - ]; - - state.youtubeXhr = youtubeXhr; - initialize(state, element); if (!youtubeXhr) { youtubeXhr = state.youtubeXhr; } - $(element).find('.video').data('video-player-state', state); + el.data('video-player-state', state); + var onSequenceChange = function onSequenceChange () { + if (state && state.videoPlayer) { + state.videoPlayer.destroy(); + } + $('.sequence').off('sequence:change', onSequenceChange); + }; + $('.sequence').on('sequence:change', onSequenceChange); // Because the 'state' object is only available inside this closure, we will also make it available to // the caller by returning it. This is necessary so that we can test Video with Jasmine. diff --git a/common/lib/xmodule/xmodule/modulestore/inheritance.py b/common/lib/xmodule/xmodule/modulestore/inheritance.py index d6293647c7185fc8744d9310b9f2576610816547..d30a216df5420acbd23a699b0c2ae6ed2cc77248 100644 --- a/common/lib/xmodule/xmodule/modulestore/inheritance.py +++ b/common/lib/xmodule/xmodule/modulestore/inheritance.py @@ -149,6 +149,18 @@ class InheritanceMixin(XBlockMixin): default=True, scope=Scope.settings ) + video_bumper = Dict( + display_name=_("Video Pre-Roll"), + help=_( + """Identify a video, 5-10 seconds in length, to play before course videos. Enter the video ID from""" + """ the Video Uploads page and one or more transcript files in the following format:""" + """ {"video_id": "ID", "transcripts": {"language": "/static/filename.srt"}}.""" + """ For example, an entry for a video with two transcripts looks like this:""" + """ {"video_id": "77cef264-d6f5-4cf2-ad9d-0178ab8c77be",""" + """ "transcripts": {"en": "/static/DemoX-D01_1.srt", "uk": "/static/DemoX-D01_1_uk.srt"}}""" + ), + scope=Scope.settings + ) reset_key = "DEFAULT_SHOW_RESET_BUTTON" default_reset_button = getattr(settings, reset_key) if hasattr(settings, reset_key) else False diff --git a/common/lib/xmodule/xmodule/video_module/__init__.py b/common/lib/xmodule/xmodule/video_module/__init__.py index 9aa96ecee95be7b9292295a28dc724719169c826..ad2040c1983e21914c3e47ff4c81cf6599dbc7ca 100644 --- a/common/lib/xmodule/xmodule/video_module/__init__.py +++ b/common/lib/xmodule/xmodule/video_module/__init__.py @@ -8,3 +8,4 @@ Container for video module and it's utils. from .transcripts_utils import * from .video_utils import * from .video_module import * +from .bumper_utils import * diff --git a/common/lib/xmodule/xmodule/video_module/bumper_utils.py b/common/lib/xmodule/xmodule/video_module/bumper_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..0bb914d280241053329813e30e89ac5cab179f37 --- /dev/null +++ b/common/lib/xmodule/xmodule/video_module/bumper_utils.py @@ -0,0 +1,142 @@ +""" +Utils for video bumper +""" +import copy +import json +import pytz +import logging +from collections import OrderedDict + +from datetime import datetime, timedelta +from django.conf import settings + +from .video_utils import set_query_parameter + +try: + import edxval.api as edxval_api +except ImportError: + edxval_api = None + +log = logging.getLogger(__name__) + + +def get_bumper_settings(video): + """ + Get bumper settings from video instance. + """ + bumper_settings = copy.deepcopy(getattr(video, 'video_bumper', {})) + + # clean up /static/ prefix from bumper transcripts + for lang, transcript_url in bumper_settings.get('transcripts', {}).items(): + bumper_settings['transcripts'][lang] = transcript_url.replace("/static/", "") + + return bumper_settings + + +def is_bumper_enabled(video): + """ + Check if bumper enabled. + + - Feature flag ENABLE_VIDEO_BUMPER should be set to True + - Do not show again button should not be clicked by user. + - Current time minus periodicity must be greater that last time viewed + - edxval_api should be presented + + Returns: + bool. + """ + bumper_last_view_date = getattr(video, 'bumper_last_view_date', None) + utc_now = datetime.utcnow().replace(tzinfo=pytz.utc) + periodicity = settings.FEATURES.get('SHOW_BUMPER_PERIODICITY', 0) + has_viewed = any([ + getattr(video, 'bumper_do_not_show_again'), + (bumper_last_view_date and bumper_last_view_date + timedelta(seconds=periodicity) > utc_now) + ]) + is_studio = getattr(video.system, "is_author_mode", False) + return bool( + not is_studio and + settings.FEATURES.get('ENABLE_VIDEO_BUMPER') and + get_bumper_settings(video) and + edxval_api and + not has_viewed + ) + + +def bumperize(video): + """ + Populate video with bumper settings, if they are presented. + """ + video.bumper = { + 'enabled': False, + 'edx_video_id': "", + 'transcripts': {}, + 'metadata': None, + } + + if not is_bumper_enabled(video): + return + + bumper_settings = get_bumper_settings(video) + + try: + video.bumper['edx_video_id'] = bumper_settings['video_id'] + video.bumper['transcripts'] = bumper_settings['transcripts'] + except (TypeError, KeyError): + log.warning( + "Could not retrieve video bumper information from course settings" + ) + return + + sources = get_bumper_sources(video) + if not sources: + return + + video.bumper.update({ + 'metadata': bumper_metadata(video, sources), + 'enabled': True, # Video poster needs this. + }) + + +def get_bumper_sources(video): + """ + Get bumper sources from edxval. + + Returns list of sources. + """ + try: + val_profiles = ["desktop_webm", "desktop_mp4"] + val_video_urls = edxval_api.get_urls_for_profiles(video.bumper['edx_video_id'], val_profiles) + bumper_sources = filter(None, [val_video_urls[p] for p in val_profiles]) + except edxval_api.ValInternalError: + # if no bumper sources, nothing will be showed + log.warning( + "Could not retrieve information from VAL for Bumper edx Video ID: %s.", video.bumper['edx_video_id'] + ) + return [] + + return bumper_sources + + +def bumper_metadata(video, sources): + """ + Generate bumper metadata. + """ + transcripts = video.get_transcripts_info(is_bumper=True) + unused_track_url, bumper_transcript_language, bumper_languages = video.get_transcripts_for_student(transcripts) + + metadata = OrderedDict({ + 'saveStateUrl': video.system.ajax_url + '/save_user_state', + 'showCaptions': json.dumps(video.show_captions), + 'sources': sources, + 'streams': '', + 'transcriptLanguage': bumper_transcript_language, + 'transcriptLanguages': bumper_languages, + 'transcriptTranslationUrl': set_query_parameter( + video.runtime.handler_url(video, 'transcript', 'translation/__lang__').rstrip('/?'), 'is_bumper', 1 + ), + 'transcriptAvailableTranslationsUrl': set_query_parameter( + video.runtime.handler_url(video, 'transcript', 'available_translations').rstrip('/?'), 'is_bumper', 1 + ), + }) + + return metadata diff --git a/common/lib/xmodule/xmodule/video_module/transcripts_utils.py b/common/lib/xmodule/xmodule/video_module/transcripts_utils.py index 8dca188d9add6c37d8ad77fb1c2d9e4d18fd66e8..fd82ed3452802105363c683d73603362f4b490af 100644 --- a/common/lib/xmodule/xmodule/video_module/transcripts_utils.py +++ b/common/lib/xmodule/xmodule/video_module/transcripts_utils.py @@ -15,6 +15,8 @@ from xmodule.exceptions import NotFoundError from xmodule.contentstore.content import StaticContent from xmodule.contentstore.django import contentstore +from .bumper_utils import get_bumper_settings + log = logging.getLogger(__name__) @@ -408,20 +410,23 @@ def generate_sjson_for_all_speeds(item, user_filename, result_subs_dict, lang): ) -def get_or_create_sjson(item): +def get_or_create_sjson(item, transcripts): """ Get sjson if already exists, otherwise generate it. Generate sjson with subs_id name, from user uploaded srt. Subs_id is extracted from srt filename, which was set by user. + Args: + transcipts (dict): dictionary of (language: file) pairs. + Raises: TranscriptException: when srt subtitles do not exist, and exceptions from generate_subs_from_source. `item` is module object. """ - user_filename = item.transcripts[item.transcript_language] + user_filename = transcripts[item.transcript_language] user_subs_id = os.path.splitext(user_filename)[0] source_subs_id, result_subs_dict = user_subs_id, {1.0: user_subs_id} try: @@ -517,7 +522,7 @@ class VideoTranscriptsMixin(object): This is necessary for both VideoModule and VideoDescriptor. """ - def available_translations(self, verify_assets=True): + def available_translations(self, transcripts, verify_assets=True): """Return a list of language codes for which we have transcripts. Args: @@ -528,39 +533,51 @@ class VideoTranscriptsMixin(object): when trying to make a listing of videos and their languages. Defaults to True. + + transcripts (dict): A dict with all transcripts and a sub. + + Defaults to False """ translations = [] + sub, other_lang = transcripts["sub"], transcripts["transcripts"] # If we're not verifying the assets, we just trust our field values if not verify_assets: - translations = list(self.transcripts) - if not translations or self.sub: + translations = list(other_lang) + if not translations or sub: translations += ['en'] return set(translations) # If we've gotten this far, we're going to verify that the transcripts # being referenced are actually in the contentstore. - if self.sub: # check if sjson exists for 'en'. + if sub: # check if sjson exists for 'en'. try: - Transcript.asset(self.location, self.sub, 'en') + Transcript.asset(self.location, sub, 'en') except NotFoundError: - pass + try: + Transcript.asset(self.location, None, None, sub) + except NotFoundError: + pass + else: + translations = ['en'] else: translations = ['en'] - for lang in self.transcripts: + for lang in other_lang: try: - Transcript.asset(self.location, None, None, self.transcripts[lang]) + Transcript.asset(self.location, None, None, other_lang[lang]) except NotFoundError: continue translations.append(lang) return translations - def get_transcript(self, transcript_format='srt', lang=None): + def get_transcript(self, transcripts, transcript_format='srt', lang=None): """ Returns transcript, filename and MIME type. + transcripts (dict): A dict with all transcripts and a sub. + Raises: - NotFoundError if cannot find transcript file in storage. - ValueError if transcript file is empty or incorrect JSON. @@ -572,11 +589,12 @@ class VideoTranscriptsMixin(object): If language is not 'en', give back transcript in proper language and format. """ if not lang: - lang = self.transcript_language + lang = self.get_default_transcript_language(transcripts) + sub, other_lang = transcripts["sub"], transcripts["transcripts"] if lang == 'en': - if self.sub: # HTML5 case and (Youtube case for new style videos) - transcript_name = self.sub + if sub: # HTML5 case and (Youtube case for new style videos) + transcript_name = sub elif self.youtube_id_1_0: # old courses transcript_name = self.youtube_id_1_0 else: @@ -587,8 +605,8 @@ class VideoTranscriptsMixin(object): filename = u'{}.{}'.format(transcript_name, transcript_format) content = Transcript.convert(data, 'sjson', transcript_format) else: - data = Transcript.asset(self.location, None, None, self.transcripts[lang]).data - filename = u'{}.{}'.format(os.path.splitext(self.transcripts[lang])[0], transcript_format) + data = Transcript.asset(self.location, None, None, other_lang[lang]).data + filename = u'{}.{}'.format(os.path.splitext(other_lang[lang])[0], transcript_format) content = Transcript.convert(data, 'srt', transcript_format) if not content: @@ -597,16 +615,36 @@ class VideoTranscriptsMixin(object): return content, filename, Transcript.mime_types[transcript_format] - def get_default_transcript_language(self): + def get_default_transcript_language(self, transcripts): """ Returns the default transcript language for this video module. + + Args: + transcripts (dict): A dict with all transcripts and a sub. """ - if self.transcript_language in self.transcripts: + sub, other_lang = transcripts["sub"], transcripts["transcripts"] + if self.transcript_language in other_lang: transcript_language = self.transcript_language - elif self.sub: + elif sub: transcript_language = u'en' - elif len(self.transcripts) > 0: - transcript_language = sorted(self.transcripts)[0] + elif len(other_lang) > 0: + transcript_language = sorted(other_lang)[0] else: transcript_language = u'en' return transcript_language + + def get_transcripts_info(self, is_bumper=False): + """ + Returns a transcript dictionary for the video. + """ + if is_bumper: + transcripts = copy.deepcopy(get_bumper_settings(self).get('transcripts', {})) + return { + "sub": transcripts.pop("en", ""), + "transcripts": transcripts, + } + else: + return { + "sub": self.sub, + "transcripts": self.transcripts, + } diff --git a/common/lib/xmodule/xmodule/video_module/video_handlers.py b/common/lib/xmodule/xmodule/video_module/video_handlers.py index 20535884446df3d8affa7a1c74eae1ebff0db713..4ed6e16e18eb1d8cf8ee8618e87dfe331bf27c8b 100644 --- a/common/lib/xmodule/xmodule/video_module/video_handlers.py +++ b/common/lib/xmodule/xmodule/video_module/video_handlers.py @@ -7,6 +7,7 @@ StudioViewHandlers are handlers for video descriptor instance. import json import logging +from datetime import datetime from webob import Response from xblock.core import XBlock @@ -44,7 +45,8 @@ class VideoStudentViewHandlers(object): """ accepted_keys = [ 'speed', 'saved_video_position', 'transcript_language', - 'transcript_download_format', 'youtube_is_available' + 'transcript_download_format', 'youtube_is_available', + 'bumper_last_view_date', 'bumper_do_not_show_again' ] conversions = { @@ -61,6 +63,9 @@ class VideoStudentViewHandlers(object): else: value = data[key] + if key == 'bumper_last_view_date': + value = datetime.utcnow() + setattr(self, key, value) if key == 'speed': @@ -73,16 +78,17 @@ class VideoStudentViewHandlers(object): raise NotFoundError('Unexpected dispatch type') - def translation(self, youtube_id): + def translation(self, youtube_id, transcripts): """ This is called to get transcript file for specific language. youtube_id: str: must be one of youtube_ids or None if HTML video + transcripts (dict): A dict with all transcripts and a sub. Logic flow: If youtube_id doesn't exist, we have a video in HTML5 mode. Otherwise, - video video in Youtube or Flash modes. + video in Youtube or Flash modes. if youtube: If english -> give back youtube_id subtitles: @@ -106,6 +112,7 @@ class VideoStudentViewHandlers(object): NotFoundError if for 'en' subtitles no asset is uploaded. NotFoundError if youtube_id does not exist / invalid youtube_id """ + sub, other_lang = transcripts["sub"], transcripts["transcripts"] if youtube_id: # Youtube case: if self.transcript_language == 'en': @@ -122,7 +129,7 @@ class VideoStudentViewHandlers(object): log.info("Can't find content in storage for %s transcript: generating.", youtube_id) generate_sjson_for_all_speeds( self, - self.transcripts[self.transcript_language], + other_lang[self.transcript_language], {speed: youtube_id for youtube_id, speed in youtube_ids.iteritems()}, self.transcript_language ) @@ -132,11 +139,18 @@ class VideoStudentViewHandlers(object): else: # HTML5 case if self.transcript_language == 'en': - return Transcript.asset(self.location, self.sub).data - else: - return get_or_create_sjson(self) + if '.srt' not in sub: # not bumper case + return Transcript.asset(self.location, sub).data + try: + return get_or_create_sjson(self, {'en': sub}) + except TranscriptException: + pass # to raise NotFoundError and try to get data in get_static_transcript + elif other_lang: + return get_or_create_sjson(self, other_lang) + + raise NotFoundError - def get_static_transcript(self, request): + def get_static_transcript(self, request, transcripts): """ Courses that are imported with the --nostatic flag do not show transcripts/captions properly even if those captions are stored inside @@ -144,6 +158,8 @@ class VideoStudentViewHandlers(object): the static asset path of the course if the transcript can't be found inside the contentstore and the course has the static_asset_path field set. + + transcripts (dict): A dict with all transcripts and a sub. """ response = Response(status=404) # Only do redirect for English @@ -154,7 +170,7 @@ class VideoStudentViewHandlers(object): if video_id: transcript_name = video_id else: - transcript_name = self.sub + transcript_name = transcripts["sub"] if transcript_name: # Get the asset path for course @@ -181,7 +197,9 @@ class VideoStudentViewHandlers(object): """ Entry point for transcript handlers for student_view. - Request GET may contain `videoId` for `translation` dispatch. + Request GET contains: + (optional) `videoId` for `translation` dispatch. + `is_bumper=1` flag for bumper case. Dispatches, (HTTP GET): /translation/[language_id] @@ -197,15 +215,16 @@ class VideoStudentViewHandlers(object): Returns list of languages, for which transcript files exist. For 'en' check if SJSON exists. For non-`en` check if SRT file exists. """ + is_bumper = request.GET.get('is_bumper', False) + transcripts = self.get_transcripts_info(is_bumper) if dispatch.startswith('translation'): - language = dispatch.replace('translation', '').strip('/') if not language: log.info("Invalid /translation request: no language.") return Response(status=400) - if language not in ['en'] + self.transcripts.keys(): + if language not in ['en'] + transcripts["transcripts"].keys(): log.info("Video: transcript facilities are not available for given language.") return Response(status=404) @@ -213,12 +232,12 @@ class VideoStudentViewHandlers(object): self.transcript_language = language try: - transcript = self.translation(request.GET.get('videoId', None)) + transcript = self.translation(request.GET.get('videoId', None), transcripts) except (TypeError, NotFoundError) as ex: log.info(ex.message) # Try to return static URL redirection as last resort # if no translation is required - return self.get_static_transcript(request) + return self.get_static_transcript(request, transcripts) except ( TranscriptException, UnicodeDecodeError, @@ -232,7 +251,9 @@ class VideoStudentViewHandlers(object): elif dispatch == 'download': try: - transcript_content, transcript_filename, transcript_mime_type = self.get_transcript(self.transcript_download_format) + transcript_content, transcript_filename, transcript_mime_type = self.get_transcript( + transcripts, transcript_format=self.transcript_download_format + ) except (NotFoundError, ValueError, KeyError, UnicodeDecodeError): log.debug("Video@download exception") return Response(status=404) @@ -246,8 +267,9 @@ class VideoStudentViewHandlers(object): ) response.content_type = transcript_mime_type - elif dispatch == 'available_translations': - available_translations = self.available_translations() + elif dispatch.startswith('available_translations'): + + available_translations = self.available_translations(transcripts) if available_translations: response = Response(json.dumps(available_translations)) response.content_type = 'application/json' diff --git a/common/lib/xmodule/xmodule/video_module/video_module.py b/common/lib/xmodule/xmodule/video_module/video_module.py index 8c80f79d14e87cde6a0c93ee5d32921eb23cdba2..d424a9352ce4bf8b61705345de34e3337e2a696a 100644 --- a/common/lib/xmodule/xmodule/video_module/video_module.py +++ b/common/lib/xmodule/xmodule/video_module/video_module.py @@ -1,3 +1,4 @@ + # -*- coding: utf-8 -*- # pylint: disable=abstract-method """Video is ungraded Xmodule for support video content. @@ -37,7 +38,8 @@ from xmodule.xml_module import is_pointer_tag, name_to_pathname, deserialize_fie from xmodule.exceptions import NotFoundError from .transcripts_utils import VideoTranscriptsMixin -from .video_utils import create_youtube_string, get_video_from_cdn +from .video_utils import create_youtube_string, get_video_from_cdn, get_poster +from .bumper_utils import bumperize from .video_xfields import VideoFields from .video_handlers import VideoStudentViewHandlers, VideoStudioViewHandlers @@ -117,11 +119,21 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers, resource_string(module, 'js/src/video/03_video_player.js'), resource_string(module, 'js/src/video/035_video_accessible_menu.js'), resource_string(module, 'js/src/video/04_video_control.js'), + resource_string(module, 'js/src/video/04_video_full_screen.js'), resource_string(module, 'js/src/video/05_video_quality_control.js'), resource_string(module, 'js/src/video/06_video_progress_slider.js'), resource_string(module, 'js/src/video/07_video_volume_control.js'), resource_string(module, 'js/src/video/08_video_speed_control.js'), resource_string(module, 'js/src/video/09_video_caption.js'), + resource_string(module, 'js/src/video/09_play_placeholder.js'), + resource_string(module, 'js/src/video/09_play_pause_control.js'), + resource_string(module, 'js/src/video/09_play_skip_control.js'), + resource_string(module, 'js/src/video/09_skip_control.js'), + resource_string(module, 'js/src/video/09_bumper.js'), + resource_string(module, 'js/src/video/09_save_state_plugin.js'), + resource_string(module, 'js/src/video/09_events_plugin.js'), + resource_string(module, 'js/src/video/09_events_bumper_plugin.js'), + resource_string(module, 'js/src/video/09_poster.js'), resource_string(module, 'js/src/video/095_video_context_menu.js'), resource_string(module, 'js/src/video/10_commands.js'), resource_string(module, 'js/src/video/10_main.js') @@ -133,9 +145,13 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers, ]} js_module_name = "Video" - def get_transcripts_for_student(self): + def get_transcripts_for_student(self, transcripts): """Return transcript information necessary for rendering the XModule student view. This is more or less a direct extraction from `get_html`. + + Args: + transcripts (dict): A dict with all transcripts and a sub. + Returns: Tuple of (track_url, transcript_language, sorted_languages) track_url -> subtitle download url @@ -143,31 +159,27 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers, sorted_languages -> dictionary of available transcript languages """ track_url = None + sub, other_lang = transcripts["sub"], transcripts["transcripts"] if self.download_track: if self.track: track_url = self.track - elif self.sub or self.transcripts: + elif sub or other_lang: track_url = self.runtime.handler_url(self, 'transcript', 'download').rstrip('/?') - if not self.transcripts: - transcript_language = u'en' - languages = {'en': 'English'} - else: - transcript_language = self.get_default_transcript_language() - - native_languages = {lang: label for lang, label in settings.LANGUAGES if len(lang) == 2} - languages = { - lang: native_languages.get(lang, display) - for lang, display in settings.ALL_LANGUAGES - if lang in self.transcripts - } + transcript_language = self.get_default_transcript_language(transcripts) - if self.sub: - languages['en'] = 'English' + native_languages = {lang: label for lang, label in settings.LANGUAGES if len(lang) == 2} + languages = { + lang: native_languages.get(lang, display) + for lang, display in settings.ALL_LANGUAGES + if lang in other_lang + } + if not other_lang or (other_lang and sub): + languages['en'] = 'English' # OrderedDict for easy testing of rendered context in tests sorted_languages = sorted(languages.items(), key=itemgetter(1)) - if 'table' in self.transcripts: + if 'table' in other_lang: sorted_languages.insert(0, ('table', 'Table of Contents')) sorted_languages = OrderedDict(sorted_languages) @@ -233,7 +245,7 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers, elif self.html5_sources: download_video_link = self.html5_sources[0] - track_url, transcript_language, sorted_languages = self.get_transcripts_for_student() + track_url, transcript_language, sorted_languages = self.get_transcripts_for_student(self.get_transcripts_info()) # CDN_VIDEO_URLS is only to be used here and will be deleted # TODO(ali@edx.org): Delete this after the CDN experiment has completed. @@ -250,42 +262,73 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers, cdn_eval = False cdn_exp_group = None - return self.system.render_template('video.html', { - 'ajax_url': self.system.ajax_url + '/save_user_state', + self.youtube_streams = youtube_streams or create_youtube_string(self) # pylint: disable=W0201 + metadata = { + 'saveStateUrl': self.system.ajax_url + '/save_user_state', 'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', False), + 'streams': self.youtube_streams, + 'sub': self.sub, + 'sources': sources, + + # This won't work when we move to data that + # isn't on the filesystem + 'captionDataDir': getattr(self, 'data_dir', None), + + 'showCaptions': json.dumps(self.show_captions), + 'generalSpeed': self.global_speed, + 'speed': self.speed, + 'savedVideoPosition': self.saved_video_position.total_seconds(), + 'start': self.start_time.total_seconds(), + 'end': self.end_time.total_seconds(), + 'transcriptLanguage': transcript_language, + 'transcriptLanguages': sorted_languages, + + # TODO: Later on the value 1500 should be taken from some global + # configuration setting field. + 'ytTestTimeout': 1500, + + 'ytApiUrl': settings.YOUTUBE['API'], + 'ytTestUrl': settings.YOUTUBE['TEST_URL'], + 'transcriptTranslationUrl': self.runtime.handler_url( + self, 'transcript', 'translation/__lang__' + ).rstrip('/?'), + 'transcriptAvailableTranslationsUrl': self.runtime.handler_url( + self, 'transcript', 'available_translations' + ).rstrip('/?'), + + ## For now, the option "data-autohide-html5" is hard coded. This option + ## either enables or disables autohiding of controls and captions on mouse + ## inactivity. If set to true, controls and captions will autohide for + ## HTML5 sources (non-YouTube) after a period of mouse inactivity over the + ## whole video. When the mouse moves (or a key is pressed while any part of + ## the video player is focused), the captions and controls will be shown + ## once again. + ## + ## There is no option in the "Advanced Editor" to set this option. However, + ## this option will have an effect if changed to "True". The code on + ## front-end exists. + 'autohideHtml5': False + } + + bumperize(self) + + context = { + 'bumper_metadata': json.dumps(self.bumper['metadata']), # pylint: disable=E1101 + 'metadata': json.dumps(OrderedDict(metadata)), + 'poster': json.dumps(get_poster(self)), 'branding_info': branding_info, 'cdn_eval': cdn_eval, 'cdn_exp_group': cdn_exp_group, - # This won't work when we move to data that - # isn't on the filesystem - 'data_dir': getattr(self, 'data_dir', None), + 'id': self.location.html_id(), 'display_name': self.display_name_with_default, - 'end': self.end_time.total_seconds(), 'handout': self.handout, - 'id': self.location.html_id(), - 'show_captions': json.dumps(self.show_captions), 'download_video_link': download_video_link, - 'sources': json.dumps(sources), - 'speed': json.dumps(self.speed), - 'general_speed': self.global_speed, - 'saved_video_position': self.saved_video_position.total_seconds(), - 'start': self.start_time.total_seconds(), - 'sub': self.sub, 'track': track_url, - 'youtube_streams': youtube_streams or create_youtube_string(self), - # TODO: Later on the value 1500 should be taken from some global - # configuration setting field. - 'yt_test_timeout': 1500, - 'yt_api_url': settings.YOUTUBE['API'], - 'yt_test_url': settings.YOUTUBE['TEST_URL'], 'transcript_download_format': transcript_download_format, 'transcript_download_formats_list': self.descriptor.fields['transcript_download_format'].values, - 'transcript_language': transcript_language, - 'transcript_languages': json.dumps(sorted_languages), - 'transcript_translation_url': self.runtime.handler_url(self, 'transcript', 'translation').rstrip('/?'), - 'transcript_available_translations_url': self.runtime.handler_url(self, 'transcript', 'available_translations').rstrip('/?'), 'license': getattr(self, "license", None), - }) + } + return self.system.render_template('video.html', context) @XBlock.wants("settings") @@ -670,7 +713,10 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler def _update_transcript_for_index(language=None): """ Find video transcript - if not found, don't update index """ try: - transcript = self.get_transcript(transcript_format='txt', lang=language)[0].replace("\n", " ") + transcripts = self.get_transcripts_info() + transcript = self.get_transcript( + transcripts, transcript_format='txt', lang=language + )[0].replace("\n", " ") transcript_index_name = "transcript_{}".format(language if language else self.transcript_language) video_body.update({transcript_index_name: transcript}) except NotFoundError: diff --git a/common/lib/xmodule/xmodule/video_module/video_utils.py b/common/lib/xmodule/xmodule/video_module/video_utils.py index 428e50e05e9fda8edf6059853e5c6ec60aeb9065..dc556dea690bfe0e033e6a0efb9196e37bcfef07 100644 --- a/common/lib/xmodule/xmodule/video_module/video_utils.py +++ b/common/lib/xmodule/xmodule/video_module/video_utils.py @@ -3,9 +3,14 @@ Module contains utils specific for video_module but not for transcripts. """ import json +from collections import OrderedDict import logging import urllib import requests +from urllib import urlencode +from urlparse import parse_qs, urlsplit, urlunsplit + +from django.conf import settings from requests.exceptions import RequestException @@ -71,3 +76,40 @@ def get_video_from_cdn(cdn_base_url, original_video_url): return cdn_content['sources'][0] else: return None + + +def get_poster(video): + """ + Generate poster metadata. + + youtube_streams is string that contains '1.00:youtube_id' + + Poster metadata is dict of youtube url for image thumbnail and edx logo + """ + if not video.bumper.get("enabled"): + return + + poster = OrderedDict({"url": "", "type": ""}) + + if video.youtube_streams: + youtube_id = video.youtube_streams.split('1.00:')[1].split(',')[0] + poster["url"] = settings.YOUTUBE['IMAGE_API'].format(youtube_id=youtube_id) + poster["type"] = "youtube" + else: + poster["url"] = "https://www.edx.org/sites/default/files/theme/edx-logo-header.png" + poster["type"] = "html5" + + return poster + + +def set_query_parameter(url, param_name, param_value): + """ + Given a URL, set or replace a query parameter and return the + modified URL. + """ + scheme, netloc, path, query_string, fragment = urlsplit(url) + query_params = parse_qs(query_string) + query_params[param_name] = [param_value] + new_query_string = urlencode(query_params, doseq=True) + + return urlunsplit((scheme, netloc, path, new_query_string, fragment)) diff --git a/common/lib/xmodule/xmodule/video_module/video_xfields.py b/common/lib/xmodule/xmodule/video_module/video_xfields.py index 5bf2195b8836a1ed1193e14a0bc13b86819de31c..83375f23ab29c3e80ef9956cd8f03b9ba161f9b2 100644 --- a/common/lib/xmodule/xmodule/video_module/video_xfields.py +++ b/common/lib/xmodule/xmodule/video_module/video_xfields.py @@ -3,7 +3,7 @@ XFields for video module. """ import datetime -from xblock.fields import Scope, String, Float, Boolean, List, Dict +from xblock.fields import Scope, String, Float, Boolean, List, Dict, DateTime from xmodule.fields import RelativeTime from xmodule.mixin import LicenseMixin @@ -142,7 +142,7 @@ class VideoFields(LicenseMixin): ) speed = Float( help=_("The last speed that the user specified for the video."), - scope=Scope.user_state, + scope=Scope.user_state ) global_speed = Float( help=_("The default speed for the video."), @@ -174,3 +174,12 @@ class VideoFields(LicenseMixin): scope=Scope.settings, default="", ) + bumper_last_view_date = DateTime( + display_name=_("Date of the last view of the bumper"), + scope=Scope.preferences, + ) + bumper_do_not_show_again = Boolean( + display_name=_("Do not show bumper again"), + scope=Scope.preferences, + default=False, + ) diff --git a/common/test/acceptance/pages/lms/video/video.py b/common/test/acceptance/pages/lms/video/video.py index 86150798ee60988f40266def182f18e634beb15c..c730023466491378b8a4ac1c0a74a5536a83a803 100644 --- a/common/test/acceptance/pages/lms/video/video.py +++ b/common/test/acceptance/pages/lms/video/video.py @@ -3,6 +3,7 @@ Video player in the courseware. """ import time +import json import requests from selenium.webdriver.common.action_chains import ActionChains from bok_choy.page_object import PageObject @@ -21,10 +22,12 @@ VIDEO_BUTTONS = { 'download_transcript': '.video-tracks > a', 'speed': '.speeds', 'quality': '.quality-control', + 'do_not_show_again': '.skip-control', + 'skip_bumper': '.play-skip-control', } CSS_CLASS_NAMES = { - 'closed_captions': '.closed .subtitles', + 'closed_captions': '.video.closed', 'captions_rendered': '.video.is-captions-rendered', 'captions': '.subtitles', 'captions_text': '.subtitles > li', @@ -37,7 +40,8 @@ CSS_CLASS_NAMES = { 'video_time': 'div.vidtime', 'video_display_name': '.vert h2', 'captions_lang_list': '.langs-list li', - 'video_speed': '.speeds .value' + 'video_speed': '.speeds .value', + 'poster': '.poster', } VIDEO_MODES = { @@ -79,7 +83,7 @@ class VideoPage(PageObject): self.wait_for_element_presence(video_selector, 'Video is initialized') @wait_for_js - def wait_for_video_player_render(self): + def wait_for_video_player_render(self, autoplay=False): """ Wait until Video Player Rendered Completely. @@ -88,7 +92,12 @@ class VideoPage(PageObject): self.wait_for_element_presence(CSS_CLASS_NAMES['video_init'], 'Video Player Initialized') self.wait_for_element_presence(CSS_CLASS_NAMES['video_time'], 'Video Player Initialized') - video_player_buttons = ['volume', 'play', 'fullscreen', 'speed'] + video_player_buttons = ['volume', 'fullscreen', 'speed'] + if autoplay: + video_player_buttons.append('pause') + else: + video_player_buttons.append('play') + for button in video_player_buttons: self.wait_for_element_visibility(VIDEO_BUTTONS[button], '{} button is visible'.format(button.title())) @@ -106,6 +115,34 @@ class VideoPage(PageObject): self.wait_for_ajax() + @wait_for_js + def wait_for_video_bumper_render(self): + """ + Wait until Poster, Video Pre-Roll and main Video Player are Rendered Completely. + """ + self.wait_for_video_class() + self.wait_for_element_presence(CSS_CLASS_NAMES['video_init'], 'Video Player Initialized') + self.wait_for_element_presence(CSS_CLASS_NAMES['video_time'], 'Video Player Initialized') + + video_player_buttons = ['do_not_show_again', 'skip_bumper', 'volume'] + for button in video_player_buttons: + self.wait_for_element_visibility(VIDEO_BUTTONS[button], '{} button is visible'.format(button.title())) + + @property + def is_poster_shown(self): + """ + Check whether a poster is show. + """ + selector = self.get_element_selector(CSS_CLASS_NAMES['poster']) + return self.q(css=selector).visible + + def click_on_poster(self): + """ + Click on the video poster. + """ + selector = self.get_element_selector(CSS_CLASS_NAMES['poster']) + self.q(css=selector).click() + def get_video_vertical_selector(self, video_display_name=None): """ Get selector for a video vertical with display name specified by `video_display_name`. @@ -184,19 +221,14 @@ class VideoPage(PageObject): @property def is_autoplay_enabled(self): """ - Extract `data-autoplay` attribute to check video autoplay is enabled or disabled. + Extract autoplay value of `data-metadata` attribute to check video autoplay is enabled or disabled. Returns: bool: Tells if autoplay enabled/disabled. - """ selector = self.get_element_selector(CSS_CLASS_NAMES['video_container']) - auto_play = self.q(css=selector).attrs('data-autoplay')[0] - - if auto_play.lower() == 'false': - return False - - return True + auto_play = json.loads(self.q(css=selector).attrs('data-metadata')[0])['autoplay'] + return auto_play @property def is_error_message_shown(self): @@ -268,6 +300,7 @@ class VideoPage(PageObject): bool: True means captions are visible, False means captions are not visible """ + self.wait_for_ajax() caption_state_selector = self.get_element_selector(CSS_CLASS_NAMES['closed_captions']) return not self.q(css=caption_state_selector).present @@ -515,6 +548,7 @@ class VideoPage(PageObject): language_selector = VIDEO_MENUS["language"] + ' li[data-lang-code="{code}"]'.format(code=code) language_selector = self.get_element_selector(language_selector) + self.wait_for_element_visibility(language_selector, 'language menu is visible') self.q(css=language_selector).first.click() diff --git a/common/test/acceptance/pages/studio/settings_advanced.py b/common/test/acceptance/pages/studio/settings_advanced.py index 0dfad8cf44570ebd0f02439623b4a4b7705c1a9f..dfca78bd55faa6768b27305321121f6ad7c46d86 100644 --- a/common/test/acceptance/pages/studio/settings_advanced.py +++ b/common/test/acceptance/pages/studio/settings_advanced.py @@ -200,4 +200,5 @@ class AdvancedSettingsPage(CoursePage): 'social_sharing_url', 'teams_configuration', 'minimum_grade_credit', + 'video_bumper', ] diff --git a/common/test/acceptance/tests/video/test_video_events.py b/common/test/acceptance/tests/video/test_video_events.py index dabe17bda2a9c03d5398f4d99219f045517b4143..f851a02bcaea3219d016eaab21caac7d5b29efc8 100644 --- a/common/test/acceptance/tests/video/test_video_events.py +++ b/common/test/acceptance/tests/video/test_video_events.py @@ -2,15 +2,62 @@ import datetime import json +import ddt from ..helpers import EventsTestMixin from .test_video_module import VideoBaseTest +from ...pages.lms.video.video import _parse_time_str from openedx.core.lib.tests.assertions.events import assert_event_matches, assert_events_equal from opaque_keys.edx.keys import UsageKey, CourseKey -class VideoEventsTest(EventsTestMixin, VideoBaseTest): +class VideoEventsTestMixin(EventsTestMixin, VideoBaseTest): + """ + Useful helper methods to test video player event emission. + """ + def assert_payload_contains_ids(self, video_event): + """ + Video events should all contain "id" and "code" attributes in their payload. + + This function asserts that those fields are present and have correct values. + """ + video_descriptors = self.course_fixture.get_nested_xblocks(category='video') + video_desc = video_descriptors[0] + video_locator = UsageKey.from_string(video_desc.locator) + + expected_event = { + 'event': { + 'id': video_locator.html_id(), + 'code': '3_yD_cEKoCk' + } + } + self.assert_events_match([expected_event], [video_event]) + + def assert_valid_control_event_at_time(self, video_event, time_in_seconds): + """ + Video control events should contain valid ID fields and a valid "currentTime" field. + + This function asserts that those fields are present and have correct values. + """ + current_time = json.loads(video_event['event'])['currentTime'] + self.assertAlmostEqual(current_time, time_in_seconds, delta=1) + + def assert_field_type(self, event_dict, field, field_type): + """Assert that a particular `field` in the `event_dict` has a particular type""" + self.assertIn(field, event_dict, '{0} not found in the root of the event'.format(field)) + self.assertTrue( + isinstance(event_dict[field], field_type), + 'Expected "{key}" to be a "{field_type}", but it has the value "{value}" of type "{t}"'.format( + key=field, + value=event_dict[field], + t=type(event_dict[field]), + field_type=field_type, + ) + ) + + +class VideoEventsTest(VideoEventsTestMixin): """ Test video player event emission """ def test_video_control_events(self): @@ -47,33 +94,197 @@ class VideoEventsTest(EventsTestMixin, VideoBaseTest): assert_event_matches({'event_type': 'pause_video'}, video_event) self.assert_valid_control_event_at_time(video_event, self.video.seconds) - def assert_payload_contains_ids(self, video_event): + def test_strict_event_format(self): """ - Video events should all contain "id" and "code" attributes in their payload. + This test makes a very strong assertion about the fields present in events. The goal of it is to ensure that new + fields are not added to all events mistakenly. It should be the only existing test that is updated when new top + level fields are added to all events. + """ + + captured_events = [] + with self.capture_events(lambda e: e['event_type'] == 'load_video', captured_events=captured_events): + self.navigate_to_video() + + load_video_event = captured_events[0] + + # Validate the event payload + self.assert_payload_contains_ids(load_video_event) + + # We cannot predict the value of these fields so we make weaker assertions about them + dynamic_string_fields = ( + 'accept_language', + 'agent', + 'host', + 'ip', + 'event', + 'session' + ) + for field in dynamic_string_fields: + self.assert_field_type(load_video_event, field, basestring) + self.assertIn(field, load_video_event, '{0} not found in the root of the event'.format(field)) + del load_video_event[field] + + # A weak assertion for the timestamp as well + self.assert_field_type(load_video_event, 'time', datetime.datetime) + del load_video_event['time'] + + # Note that all unpredictable fields have been deleted from the event at this point + + course_key = CourseKey.from_string(self.course_id) + static_fields_pattern = { + 'context': { + 'course_id': unicode(course_key), + 'org_id': course_key.org, + 'path': '/event', + 'user_id': self.user_info['user_id'] + }, + 'event_source': 'browser', + 'event_type': 'load_video', + 'username': self.user_info['username'], + 'page': self.browser.current_url, + 'referer': self.browser.current_url, + 'name': 'load_video', + } + assert_events_equal(static_fields_pattern, load_video_event) + + +@ddt.ddt +class VideoBumperEventsTest(VideoEventsTestMixin): + """ Test bumper video event emission """ + + # helper methods + def watch_video_and_skip(self): + """ + Wait 5 seconds and press "skip" button. + """ + self.video.wait_for_position('0:05') + self.video.click_player_button('skip_bumper') + + def watch_video_and_dismiss(self): + """ + Wait 5 seconds and press "do not show again" button. + """ + self.video.wait_for_position('0:05') + self.video.click_player_button('do_not_show_again') + + def wait_for_state(self, state='finished'): + """ + Wait until video will be in given state. + + Finished state means that video is played to the end. + """ + self.video.wait_for_state(state) + + def add_bumper(self): + """ + Add video bumper to the course. + """ + additional_data = { + u'video_bumper': { + u'value': { + "transcripts": {}, + "video_id": "edx_video_id" + } + } + } + self.course_fixture.add_advanced_settings(additional_data) + + @ddt.data( + ('edx.video.bumper.skipped', watch_video_and_skip), + ('edx.video.bumper.dismissed', watch_video_and_dismiss), + ('edx.video.bumper.stopped', wait_for_state) + ) + @ddt.unpack + def test_video_control_events(self, event_type, action): + """ + Scenario: Video component with pre-roll emits events correctly + Given the course has a Video component in "Youtube" mode with pre-roll enabled + And I click on the video poster + And the pre-roll video start playing + And I watch (5 seconds/5 seconds/to the end of) it + And I click (skip/do not show again) video button + + Then a "edx.video.bumper.loaded" event is emitted + And a "edx.video.bumper.played" event is emitted + And a "edx.video.bumper.skipped/dismissed/stopped" event is emitted + And a "load_video" event is emitted + And a "play_video" event is emitted + """ + + def is_video_event(event): + """Filter out anything other than the video events of interest""" + return event['event_type'] in ( + 'edx.video.bumper.loaded', + 'edx.video.bumper.played', + 'edx.video.bumper.skipped', + 'edx.video.bumper.dismissed', + 'edx.video.bumper.stopped', + 'load_video', + 'play_video', + 'pause_video' + ) and self.video.state != 'buffering' + + captured_events = [] + self.add_bumper() + with self.capture_events(is_video_event, number_of_matches=5, captured_events=captured_events): + self.navigate_to_video_no_render() + self.video.click_on_poster() + self.video.wait_for_video_bumper_render() + sources, duration = self.video.sources[0], self.video.duration + action(self) + + # Filter subsequent events that appear due to bufferisation: edx.video.bumper.played + # As bumper does not emit pause event, we filter subsequent edx.video.bumper.played events from + # the list, except first. + filtered_events = [] + for video_event in captured_events: + is_played_event = video_event['event_type'] == 'edx.video.bumper.played' + appears_again = filtered_events and video_event['event_type'] == filtered_events[-1]['event_type'] + if is_played_event and appears_again: + continue + filtered_events.append(video_event) + + for idx, video_event in enumerate(filtered_events): + if idx < 3: + self.assert_bumper_payload_contains_ids(video_event, sources, duration) + else: + self.assert_payload_contains_ids(video_event) + + if idx == 0: + assert_event_matches({'event_type': 'edx.video.bumper.loaded'}, video_event) + elif idx == 1: + assert_event_matches({'event_type': 'edx.video.bumper.played'}, video_event) + self.assert_valid_control_event_at_time(video_event, 0) + elif idx == 2: + assert_event_matches({'event_type': event_type}, video_event) + elif idx == 3: + assert_event_matches({'event_type': 'load_video'}, video_event) + elif idx == 4: + assert_event_matches({'event_type': 'play_video'}, video_event) + self.assert_valid_control_event_at_time(video_event, 0) + + def assert_bumper_payload_contains_ids(self, video_event, sources, duration): + """ + Bumper video events should all contain "host_component_id", "bumper_id", + "duration", "code" attributes in their payload. This function asserts that those fields are present and have correct values. """ + self.add_bumper() video_descriptors = self.course_fixture.get_nested_xblocks(category='video') video_desc = video_descriptors[0] video_locator = UsageKey.from_string(video_desc.locator) expected_event = { 'event': { - 'id': video_locator.html_id(), - 'code': '3_yD_cEKoCk' + 'host_component_id': video_locator.html_id(), + 'bumper_id': sources, + 'duration': _parse_time_str(duration), + 'code': 'html5' } } self.assert_events_match([expected_event], [video_event]) - def assert_valid_control_event_at_time(self, video_event, time_in_seconds): - """ - Video control events should contain valid ID fields and a valid "currentTime" field. - - This function asserts that those fields are present and have correct values. - """ - current_time = json.loads(video_event['event'])['currentTime'] - self.assertAlmostEqual(current_time, time_in_seconds, delta=1) - def test_strict_event_format(self): """ This test makes a very strong assertion about the fields present in events. The goal of it is to ensure that new @@ -82,13 +293,17 @@ class VideoEventsTest(EventsTestMixin, VideoBaseTest): """ captured_events = [] - with self.capture_events(lambda e: e['event_type'] == 'load_video', captured_events=captured_events): - self.navigate_to_video() + self.add_bumper() + filter_event = lambda e: e['event_type'] == 'edx.video.bumper.loaded' + with self.capture_events(filter_event, captured_events=captured_events): + self.navigate_to_video_no_render() + self.video.click_on_poster() load_video_event = captured_events[0] # Validate the event payload - self.assert_payload_contains_ids(load_video_event) + sources, duration = self.video.sources[0], self.video.duration + self.assert_bumper_payload_contains_ids(load_video_event, sources, duration) # We cannot predict the value of these fields so we make weaker assertions about them dynamic_string_fields = ( @@ -119,23 +334,10 @@ class VideoEventsTest(EventsTestMixin, VideoBaseTest): 'user_id': self.user_info['user_id'] }, 'event_source': 'browser', - 'event_type': 'load_video', + 'event_type': 'edx.video.bumper.loaded', 'username': self.user_info['username'], 'page': self.browser.current_url, 'referer': self.browser.current_url, - 'name': 'load_video', + 'name': 'edx.video.bumper.loaded', } assert_events_equal(static_fields_pattern, load_video_event) - - def assert_field_type(self, event_dict, field, field_type): - """Assert that a particular `field` in the `event_dict` has a particular type""" - self.assertIn(field, event_dict, '{0} not found in the root of the event'.format(field)) - self.assertTrue( - isinstance(event_dict[field], field_type), - 'Expected "{key}" to be a "{field_type}", but it has the value "{value}" of type "{t}"'.format( - key=field, - value=event_dict[field], - t=type(event_dict[field]), - field_type=field_type, - ) - ) diff --git a/common/test/acceptance/tests/video/test_video_module.py b/common/test/acceptance/tests/video/test_video_module.py index 727ff7e8ba4fc67bd2f0c9bebc18558eb41a4b4e..48ee3b5dd859d5995716f59d7d46f4c50a875ba8 100644 --- a/common/test/acceptance/tests/video/test_video_module.py +++ b/common/test/acceptance/tests/video/test_video_module.py @@ -397,6 +397,7 @@ class YouTubeVideoTest(VideoBaseTest): 'time_to_response': 2.0, 'youtube_api_blocked': True, }) + self.metadata = self.metadata_for_mode('youtube_html5') self.navigate_to_video() @@ -711,6 +712,84 @@ class YouTubeVideoTest(VideoBaseTest): self.assertEqual(self.video.caption_languages, {'zh_HANS': 'Simplified Chinese', 'zh_HANT': 'Traditional Chinese'}) + def test_video_bumper_render(self): + """ + Scenario: Multiple videos with bumper in sequentials all load and work, switching between sequentials + Given it has videos "A,B" in "Youtube" and "HTML5" modes in position "1" of sequential + And video "C" in "Youtube" mode in position "2" of sequential + When I open sequential position "1" + Then I see video "B" has a poster + When I click on it + Then I see video bumper is playing + When I skip the bumper + Then I see the main video + When I click on video "A" + Then the main video starts playing + When I open sequential position "2" + And click on the poster + Then the main video starts playing + Then I see that the main video starts playing once I go back to position "2" of sequential + When I reload the page + Then I see that the main video starts playing when I click on the poster + """ + additional_data = { + u'video_bumper': { + u'value': { + "transcripts": {}, + "video_id": "edx_video_id" + } + } + } + + self.verticals = [ + [{'display_name': 'A'}, {'display_name': 'B', 'metadata': self.metadata_for_mode('html5')}], + [{'display_name': 'C'}] + ] + + tab1_video_names = ['A', 'B'] + tab2_video_names = ['C'] + + def execute_video_steps(video_names): + """ + Execute video steps + """ + for video_name in video_names: + self.video.use_video(video_name) + self.assertTrue(self.video.is_poster_shown) + self.video.click_on_poster() + self.video.wait_for_video_player_render(autoplay=True) + self.assertIn(self.video.state, ['playing', 'buffering', 'finished']) + + self.course_fixture.add_advanced_settings(additional_data) + self.navigate_to_video_no_render() + + self.video.use_video('B') + self.assertTrue(self.video.is_poster_shown) + self.video.click_on_poster() + self.video.wait_for_video_bumper_render() + self.assertIn(self.video.state, ['playing', 'buffering', 'finished']) + self.video.click_player_button('skip_bumper') + + # no autoplay here, maybe video is too small, so pause is not switched + self.video.wait_for_video_player_render() + self.assertIn(self.video.state, ['playing', 'buffering', 'finished']) + + self.video.use_video('A') + execute_video_steps(['A']) + + # go to second sequential position + self.course_nav.go_to_sequential_position(2) + + execute_video_steps(tab2_video_names) + + # go back to first sequential position + # we are again playing tab 1 videos to ensure that switching didn't broke some video functionality. + self.course_nav.go_to_sequential_position(1) + execute_video_steps(tab1_video_names) + + self.video.browser.refresh() + execute_video_steps(tab1_video_names) + class YouTubeHtml5VideoTest(VideoBaseTest): """ Test YouTube HTML5 Video Player """ diff --git a/common/test/db_cache/bok_choy_data.json b/common/test/db_cache/bok_choy_data.json index f8500b6768a054ed9c946d579038c4aeb687bfe1..9f41bdda53b2d7fb944b2c1e650a63f5297771f8 100644 --- a/common/test/db_cache/bok_choy_data.json +++ b/common/test/db_cache/bok_choy_data.json @@ -1 +1 @@ -[{"pk": 74, "model": "contenttypes.contenttype", "fields": {"model": "accesstoken", "name": "access token", "app_label": "oauth2"}}, {"pk": 151, "model": "contenttypes.contenttype", "fields": {"model": "aiclassifier", "name": "ai classifier", "app_label": "assessment"}}, {"pk": 150, "model": "contenttypes.contenttype", "fields": {"model": "aiclassifierset", "name": "ai classifier set", "app_label": "assessment"}}, {"pk": 153, "model": "contenttypes.contenttype", "fields": {"model": "aigradingworkflow", "name": "ai grading workflow", "app_label": "assessment"}}, {"pk": 152, "model": "contenttypes.contenttype", "fields": {"model": "aitrainingworkflow", "name": "ai training workflow", "app_label": "assessment"}}, {"pk": 34, "model": "contenttypes.contenttype", "fields": {"model": "anonymoususerid", "name": "anonymous user id", "app_label": "student"}}, {"pk": 77, "model": "contenttypes.contenttype", "fields": {"model": "article", "name": "article", "app_label": "wiki"}}, {"pk": 78, "model": "contenttypes.contenttype", "fields": {"model": "articleforobject", "name": "Article for object", "app_label": "wiki"}}, {"pk": 81, "model": "contenttypes.contenttype", "fields": {"model": "articleplugin", "name": "article plugin", "app_label": "wiki"}}, {"pk": 79, "model": "contenttypes.contenttype", "fields": {"model": "articlerevision", "name": "article revision", "app_label": "wiki"}}, {"pk": 86, "model": "contenttypes.contenttype", "fields": {"model": "articlesubscription", "name": "article subscription", "app_label": "wiki"}}, {"pk": 141, "model": "contenttypes.contenttype", "fields": {"model": "assessment", "name": "assessment", "app_label": "assessment"}}, {"pk": 144, "model": "contenttypes.contenttype", "fields": {"model": "assessmentfeedback", "name": "assessment feedback", "app_label": "assessment"}}, {"pk": 143, "model": "contenttypes.contenttype", "fields": {"model": "assessmentfeedbackoption", "name": "assessment feedback option", "app_label": "assessment"}}, {"pk": 142, "model": "contenttypes.contenttype", "fields": {"model": "assessmentpart", "name": "assessment part", "app_label": "assessment"}}, {"pk": 154, "model": "contenttypes.contenttype", "fields": {"model": "assessmentworkflow", "name": "assessment workflow", "app_label": "workflow"}}, {"pk": 156, "model": "contenttypes.contenttype", "fields": {"model": "assessmentworkflowcancellation", "name": "assessment workflow cancellation", "app_label": "workflow"}}, {"pk": 155, "model": "contenttypes.contenttype", "fields": {"model": "assessmentworkflowstep", "name": "assessment workflow step", "app_label": "workflow"}}, {"pk": 19, "model": "contenttypes.contenttype", "fields": {"model": "association", "name": "association", "app_label": "django_openid_auth"}}, {"pk": 25, "model": "contenttypes.contenttype", "fields": {"model": "association", "name": "association", "app_label": "default"}}, {"pk": 70, "model": "contenttypes.contenttype", "fields": {"model": "brandinginfoconfig", "name": "branding info config", "app_label": "branding"}}, {"pk": 57, "model": "contenttypes.contenttype", "fields": {"model": "certificategenerationconfiguration", "name": "certificate generation configuration", "app_label": "certificates"}}, {"pk": 56, "model": "contenttypes.contenttype", "fields": {"model": "certificategenerationcoursesetting", "name": "certificate generation course setting", "app_label": "certificates"}}, {"pk": 58, "model": "contenttypes.contenttype", "fields": {"model": "certificatehtmlviewconfiguration", "name": "certificate html view configuration", "app_label": "certificates"}}, {"pk": 113, "model": "contenttypes.contenttype", "fields": {"model": "certificateitem", "name": "certificate item", "app_label": "shoppingcart"}}, {"pk": 52, "model": "contenttypes.contenttype", "fields": {"model": "certificatewhitelist", "name": "certificate whitelist", "app_label": "certificates"}}, {"pk": 72, "model": "contenttypes.contenttype", "fields": {"model": "client", "name": "client", "app_label": "oauth2"}}, {"pk": 26, "model": "contenttypes.contenttype", "fields": {"model": "code", "name": "code", "app_label": "default"}}, {"pk": 4, "model": "contenttypes.contenttype", "fields": {"model": "contenttype", "name": "content type", "app_label": "contenttypes"}}, {"pk": 22, "model": "contenttypes.contenttype", "fields": {"model": "corsmodel", "name": "cors model", "app_label": "corsheaders"}}, {"pk": 124, "model": "contenttypes.contenttype", "fields": {"model": "country", "name": "country", "app_label": "embargo"}}, {"pk": 125, "model": "contenttypes.contenttype", "fields": {"model": "countryaccessrule", "name": "country access rule", "app_label": "embargo"}}, {"pk": 107, "model": "contenttypes.contenttype", "fields": {"model": "coupon", "name": "coupon", "app_label": "shoppingcart"}}, {"pk": 108, "model": "contenttypes.contenttype", "fields": {"model": "couponredemption", "name": "coupon redemption", "app_label": "shoppingcart"}}, {"pk": 46, "model": "contenttypes.contenttype", "fields": {"model": "courseaccessrole", "name": "course access role", "app_label": "student"}}, {"pk": 126, "model": "contenttypes.contenttype", "fields": {"model": "courseaccessrulehistory", "name": "course access rule history", "app_label": "embargo"}}, {"pk": 69, "model": "contenttypes.contenttype", "fields": {"model": "courseauthorization", "name": "course authorization", "app_label": "bulk_email"}}, {"pk": 65, "model": "contenttypes.contenttype", "fields": {"model": "coursecohort", "name": "course cohort", "app_label": "course_groups"}}, {"pk": 64, "model": "contenttypes.contenttype", "fields": {"model": "coursecohortssettings", "name": "course cohorts settings", "app_label": "course_groups"}}, {"pk": 165, "model": "contenttypes.contenttype", "fields": {"model": "coursecontentmilestone", "name": "course content milestone", "app_label": "milestones"}}, {"pk": 168, "model": "contenttypes.contenttype", "fields": {"model": "coursecreator", "name": "course creator", "app_label": "course_creators"}}, {"pk": 66, "model": "contenttypes.contenttype", "fields": {"model": "courseemail", "name": "course email", "app_label": "bulk_email"}}, {"pk": 68, "model": "contenttypes.contenttype", "fields": {"model": "courseemailtemplate", "name": "course email template", "app_label": "bulk_email"}}, {"pk": 44, "model": "contenttypes.contenttype", "fields": {"model": "courseenrollment", "name": "course enrollment", "app_label": "student"}}, {"pk": 45, "model": "contenttypes.contenttype", "fields": {"model": "courseenrollmentallowed", "name": "course enrollment allowed", "app_label": "student"}}, {"pk": 164, "model": "contenttypes.contenttype", "fields": {"model": "coursemilestone", "name": "course milestone", "app_label": "milestones"}}, {"pk": 116, "model": "contenttypes.contenttype", "fields": {"model": "coursemode", "name": "course mode", "app_label": "course_modes"}}, {"pk": 117, "model": "contenttypes.contenttype", "fields": {"model": "coursemodesarchive", "name": "course modes archive", "app_label": "course_modes"}}, {"pk": 110, "model": "contenttypes.contenttype", "fields": {"model": "courseregcodeitem", "name": "course reg code item", "app_label": "shoppingcart"}}, {"pk": 111, "model": "contenttypes.contenttype", "fields": {"model": "courseregcodeitemannotation", "name": "course reg code item annotation", "app_label": "shoppingcart"}}, {"pk": 105, "model": "contenttypes.contenttype", "fields": {"model": "courseregistrationcode", "name": "course registration code", "app_label": "shoppingcart"}}, {"pk": 103, "model": "contenttypes.contenttype", "fields": {"model": "courseregistrationcodeinvoiceitem", "name": "course registration code invoice item", "app_label": "shoppingcart"}}, {"pk": 128, "model": "contenttypes.contenttype", "fields": {"model": "coursererunstate", "name": "course rerun state", "app_label": "course_action_state"}}, {"pk": 60, "model": "contenttypes.contenttype", "fields": {"model": "coursesoftware", "name": "course software", "app_label": "licenses"}}, {"pk": 133, "model": "contenttypes.contenttype", "fields": {"model": "coursestructure", "name": "course structure", "app_label": "course_structures"}}, {"pk": 62, "model": "contenttypes.contenttype", "fields": {"model": "courseusergroup", "name": "course user group", "app_label": "course_groups"}}, {"pk": 63, "model": "contenttypes.contenttype", "fields": {"model": "courseusergrouppartitiongroup", "name": "course user group partition group", "app_label": "course_groups"}}, {"pk": 159, "model": "contenttypes.contenttype", "fields": {"model": "coursevideo", "name": "course video", "app_label": "edxval"}}, {"pk": 139, "model": "contenttypes.contenttype", "fields": {"model": "criterion", "name": "criterion", "app_label": "assessment"}}, {"pk": 140, "model": "contenttypes.contenttype", "fields": {"model": "criterionoption", "name": "criterion option", "app_label": "assessment"}}, {"pk": 10, "model": "contenttypes.contenttype", "fields": {"model": "crontabschedule", "name": "crontab", "app_label": "djcelery"}}, {"pk": 119, "model": "contenttypes.contenttype", "fields": {"model": "darklangconfig", "name": "dark lang config", "app_label": "dark_lang"}}, {"pk": 47, "model": "contenttypes.contenttype", "fields": {"model": "dashboardconfiguration", "name": "dashboard configuration", "app_label": "student"}}, {"pk": 115, "model": "contenttypes.contenttype", "fields": {"model": "donation", "name": "donation", "app_label": "shoppingcart"}}, {"pk": 114, "model": "contenttypes.contenttype", "fields": {"model": "donationconfiguration", "name": "donation configuration", "app_label": "shoppingcart"}}, {"pk": 121, "model": "contenttypes.contenttype", "fields": {"model": "embargoedcourse", "name": "embargoed course", "app_label": "embargo"}}, {"pk": 122, "model": "contenttypes.contenttype", "fields": {"model": "embargoedstate", "name": "embargoed state", "app_label": "embargo"}}, {"pk": 160, "model": "contenttypes.contenttype", "fields": {"model": "encodedvideo", "name": "encoded video", "app_label": "edxval"}}, {"pk": 49, "model": "contenttypes.contenttype", "fields": {"model": "entranceexamconfiguration", "name": "entrance exam configuration", "app_label": "student"}}, {"pk": 55, "model": "contenttypes.contenttype", "fields": {"model": "examplecertificate", "name": "example certificate", "app_label": "certificates"}}, {"pk": 54, "model": "contenttypes.contenttype", "fields": {"model": "examplecertificateset", "name": "example certificate set", "app_label": "certificates"}}, {"pk": 71, "model": "contenttypes.contenttype", "fields": {"model": "externalauthmap", "name": "external auth map", "app_label": "external_auth"}}, {"pk": 53, "model": "contenttypes.contenttype", "fields": {"model": "generatedcertificate", "name": "generated certificate", "app_label": "certificates"}}, {"pk": 73, "model": "contenttypes.contenttype", "fields": {"model": "grant", "name": "grant", "app_label": "oauth2"}}, {"pk": 2, "model": "contenttypes.contenttype", "fields": {"model": "group", "name": "group", "app_label": "auth"}}, {"pk": 59, "model": "contenttypes.contenttype", "fields": {"model": "instructortask", "name": "instructor task", "app_label": "instructor_task"}}, {"pk": 9, "model": "contenttypes.contenttype", "fields": {"model": "intervalschedule", "name": "interval", "app_label": "djcelery"}}, {"pk": 100, "model": "contenttypes.contenttype", "fields": {"model": "invoice", "name": "invoice", "app_label": "shoppingcart"}}, {"pk": 104, "model": "contenttypes.contenttype", "fields": {"model": "invoicehistory", "name": "invoice history", "app_label": "shoppingcart"}}, {"pk": 102, "model": "contenttypes.contenttype", "fields": {"model": "invoiceitem", "name": "invoice item", "app_label": "shoppingcart"}}, {"pk": 101, "model": "contenttypes.contenttype", "fields": {"model": "invoicetransaction", "name": "invoice transaction", "app_label": "shoppingcart"}}, {"pk": 127, "model": "contenttypes.contenttype", "fields": {"model": "ipfilter", "name": "ip filter", "app_label": "embargo"}}, {"pk": 48, "model": "contenttypes.contenttype", "fields": {"model": "linkedinaddtoprofileconfiguration", "name": "linked in add to profile configuration", "app_label": "student"}}, {"pk": 21, "model": "contenttypes.contenttype", "fields": {"model": "logentry", "name": "log entry", "app_label": "admin"}}, {"pk": 43, "model": "contenttypes.contenttype", "fields": {"model": "loginfailures", "name": "login failures", "app_label": "student"}}, {"pk": 120, "model": "contenttypes.contenttype", "fields": {"model": "midcoursereverificationwindow", "name": "midcourse reverification window", "app_label": "reverification"}}, {"pk": 15, "model": "contenttypes.contenttype", "fields": {"model": "migrationhistory", "name": "migration history", "app_label": "south"}}, {"pk": 162, "model": "contenttypes.contenttype", "fields": {"model": "milestone", "name": "milestone", "app_label": "milestones"}}, {"pk": 163, "model": "contenttypes.contenttype", "fields": {"model": "milestonerelationshiptype", "name": "milestone relationship type", "app_label": "milestones"}}, {"pk": 129, "model": "contenttypes.contenttype", "fields": {"model": "mobileapiconfig", "name": "mobile api config", "app_label": "mobile_api"}}, {"pk": 18, "model": "contenttypes.contenttype", "fields": {"model": "nonce", "name": "nonce", "app_label": "django_openid_auth"}}, {"pk": 24, "model": "contenttypes.contenttype", "fields": {"model": "nonce", "name": "nonce", "app_label": "default"}}, {"pk": 93, "model": "contenttypes.contenttype", "fields": {"model": "note", "name": "note", "app_label": "notes"}}, {"pk": 90, "model": "contenttypes.contenttype", "fields": {"model": "notification", "name": "notification", "app_label": "django_notify"}}, {"pk": 32, "model": "contenttypes.contenttype", "fields": {"model": "offlinecomputedgrade", "name": "offline computed grade", "app_label": "courseware"}}, {"pk": 33, "model": "contenttypes.contenttype", "fields": {"model": "offlinecomputedgradelog", "name": "offline computed grade log", "app_label": "courseware"}}, {"pk": 67, "model": "contenttypes.contenttype", "fields": {"model": "optout", "name": "optout", "app_label": "bulk_email"}}, {"pk": 98, "model": "contenttypes.contenttype", "fields": {"model": "order", "name": "order", "app_label": "shoppingcart"}}, {"pk": 99, "model": "contenttypes.contenttype", "fields": {"model": "orderitem", "name": "order item", "app_label": "shoppingcart"}}, {"pk": 109, "model": "contenttypes.contenttype", "fields": {"model": "paidcourseregistration", "name": "paid course registration", "app_label": "shoppingcart"}}, {"pk": 112, "model": "contenttypes.contenttype", "fields": {"model": "paidcourseregistrationannotation", "name": "paid course registration annotation", "app_label": "shoppingcart"}}, {"pk": 42, "model": "contenttypes.contenttype", "fields": {"model": "passwordhistory", "name": "password history", "app_label": "student"}}, {"pk": 145, "model": "contenttypes.contenttype", "fields": {"model": "peerworkflow", "name": "peer workflow", "app_label": "assessment"}}, {"pk": 146, "model": "contenttypes.contenttype", "fields": {"model": "peerworkflowitem", "name": "peer workflow item", "app_label": "assessment"}}, {"pk": 41, "model": "contenttypes.contenttype", "fields": {"model": "pendingemailchange", "name": "pending email change", "app_label": "student"}}, {"pk": 40, "model": "contenttypes.contenttype", "fields": {"model": "pendingnamechange", "name": "pending name change", "app_label": "student"}}, {"pk": 12, "model": "contenttypes.contenttype", "fields": {"model": "periodictask", "name": "periodic task", "app_label": "djcelery"}}, {"pk": 11, "model": "contenttypes.contenttype", "fields": {"model": "periodictasks", "name": "periodic tasks", "app_label": "djcelery"}}, {"pk": 1, "model": "contenttypes.contenttype", "fields": {"model": "permission", "name": "permission", "app_label": "auth"}}, {"pk": 157, "model": "contenttypes.contenttype", "fields": {"model": "profile", "name": "profile", "app_label": "edxval"}}, {"pk": 17, "model": "contenttypes.contenttype", "fields": {"model": "psychometricdata", "name": "psychometric data", "app_label": "psychometrics"}}, {"pk": 92, "model": "contenttypes.contenttype", "fields": {"model": "puzzlecomplete", "name": "puzzle complete", "app_label": "foldit"}}, {"pk": 51, "model": "contenttypes.contenttype", "fields": {"model": "ratelimitconfiguration", "name": "rate limit configuration", "app_label": "util"}}, {"pk": 75, "model": "contenttypes.contenttype", "fields": {"model": "refreshtoken", "name": "refresh token", "app_label": "oauth2"}}, {"pk": 39, "model": "contenttypes.contenttype", "fields": {"model": "registration", "name": "registration", "app_label": "student"}}, {"pk": 106, "model": "contenttypes.contenttype", "fields": {"model": "registrationcoderedemption", "name": "registration code redemption", "app_label": "shoppingcart"}}, {"pk": 123, "model": "contenttypes.contenttype", "fields": {"model": "restrictedcourse", "name": "restricted course", "app_label": "embargo"}}, {"pk": 82, "model": "contenttypes.contenttype", "fields": {"model": "reusableplugin", "name": "reusable plugin", "app_label": "wiki"}}, {"pk": 84, "model": "contenttypes.contenttype", "fields": {"model": "revisionplugin", "name": "revision plugin", "app_label": "wiki"}}, {"pk": 85, "model": "contenttypes.contenttype", "fields": {"model": "revisionpluginrevision", "name": "revision plugin revision", "app_label": "wiki"}}, {"pk": 138, "model": "contenttypes.contenttype", "fields": {"model": "rubric", "name": "rubric", "app_label": "assessment"}}, {"pk": 8, "model": "contenttypes.contenttype", "fields": {"model": "tasksetmeta", "name": "saved group result", "app_label": "djcelery"}}, {"pk": 91, "model": "contenttypes.contenttype", "fields": {"model": "score", "name": "score", "app_label": "foldit"}}, {"pk": 136, "model": "contenttypes.contenttype", "fields": {"model": "score", "name": "score", "app_label": "submissions"}}, {"pk": 137, "model": "contenttypes.contenttype", "fields": {"model": "scoresummary", "name": "score summary", "app_label": "submissions"}}, {"pk": 16, "model": "contenttypes.contenttype", "fields": {"model": "servercircuit", "name": "server circuit", "app_label": "circuit"}}, {"pk": 5, "model": "contenttypes.contenttype", "fields": {"model": "session", "name": "session", "app_label": "sessions"}}, {"pk": 88, "model": "contenttypes.contenttype", "fields": {"model": "settings", "name": "settings", "app_label": "django_notify"}}, {"pk": 83, "model": "contenttypes.contenttype", "fields": {"model": "simpleplugin", "name": "simple plugin", "app_label": "wiki"}}, {"pk": 6, "model": "contenttypes.contenttype", "fields": {"model": "site", "name": "site", "app_label": "sites"}}, {"pk": 118, "model": "contenttypes.contenttype", "fields": {"model": "softwaresecurephotoverification", "name": "software secure photo verification", "app_label": "verify_student"}}, {"pk": 94, "model": "contenttypes.contenttype", "fields": {"model": "splashconfig", "name": "splash config", "app_label": "splash"}}, {"pk": 134, "model": "contenttypes.contenttype", "fields": {"model": "studentitem", "name": "student item", "app_label": "submissions"}}, {"pk": 27, "model": "contenttypes.contenttype", "fields": {"model": "studentmodule", "name": "student module", "app_label": "courseware"}}, {"pk": 28, "model": "contenttypes.contenttype", "fields": {"model": "studentmodulehistory", "name": "student module history", "app_label": "courseware"}}, {"pk": 148, "model": "contenttypes.contenttype", "fields": {"model": "studenttrainingworkflow", "name": "student training workflow", "app_label": "assessment"}}, {"pk": 149, "model": "contenttypes.contenttype", "fields": {"model": "studenttrainingworkflowitem", "name": "student training workflow item", "app_label": "assessment"}}, {"pk": 169, "model": "contenttypes.contenttype", "fields": {"model": "studioconfig", "name": "studio config", "app_label": "xblock_config"}}, {"pk": 135, "model": "contenttypes.contenttype", "fields": {"model": "submission", "name": "submission", "app_label": "submissions"}}, {"pk": 89, "model": "contenttypes.contenttype", "fields": {"model": "subscription", "name": "subscription", "app_label": "django_notify"}}, {"pk": 161, "model": "contenttypes.contenttype", "fields": {"model": "subtitle", "name": "subtitle", "app_label": "edxval"}}, {"pk": 131, "model": "contenttypes.contenttype", "fields": {"model": "surveyanswer", "name": "survey answer", "app_label": "survey"}}, {"pk": 130, "model": "contenttypes.contenttype", "fields": {"model": "surveyform", "name": "survey form", "app_label": "survey"}}, {"pk": 14, "model": "contenttypes.contenttype", "fields": {"model": "taskstate", "name": "task", "app_label": "djcelery"}}, {"pk": 7, "model": "contenttypes.contenttype", "fields": {"model": "taskmeta", "name": "task state", "app_label": "djcelery"}}, {"pk": 50, "model": "contenttypes.contenttype", "fields": {"model": "trackinglog", "name": "tracking log", "app_label": "track"}}, {"pk": 147, "model": "contenttypes.contenttype", "fields": {"model": "trainingexample", "name": "training example", "app_label": "assessment"}}, {"pk": 76, "model": "contenttypes.contenttype", "fields": {"model": "trustedclient", "name": "trusted client", "app_label": "oauth2_provider"}}, {"pk": 87, "model": "contenttypes.contenttype", "fields": {"model": "notificationtype", "name": "type", "app_label": "django_notify"}}, {"pk": 80, "model": "contenttypes.contenttype", "fields": {"model": "urlpath", "name": "URL path", "app_label": "wiki"}}, {"pk": 3, "model": "contenttypes.contenttype", "fields": {"model": "user", "name": "user", "app_label": "auth"}}, {"pk": 96, "model": "contenttypes.contenttype", "fields": {"model": "usercoursetag", "name": "user course tag", "app_label": "user_api"}}, {"pk": 61, "model": "contenttypes.contenttype", "fields": {"model": "userlicense", "name": "user license", "app_label": "licenses"}}, {"pk": 166, "model": "contenttypes.contenttype", "fields": {"model": "usermilestone", "name": "user milestone", "app_label": "milestones"}}, {"pk": 20, "model": "contenttypes.contenttype", "fields": {"model": "useropenid", "name": "user open id", "app_label": "django_openid_auth"}}, {"pk": 97, "model": "contenttypes.contenttype", "fields": {"model": "userorgtag", "name": "user org tag", "app_label": "user_api"}}, {"pk": 95, "model": "contenttypes.contenttype", "fields": {"model": "userpreference", "name": "user preference", "app_label": "user_api"}}, {"pk": 36, "model": "contenttypes.contenttype", "fields": {"model": "userprofile", "name": "user profile", "app_label": "student"}}, {"pk": 37, "model": "contenttypes.contenttype", "fields": {"model": "usersignupsource", "name": "user signup source", "app_label": "student"}}, {"pk": 23, "model": "contenttypes.contenttype", "fields": {"model": "usersocialauth", "name": "user social auth", "app_label": "default"}}, {"pk": 35, "model": "contenttypes.contenttype", "fields": {"model": "userstanding", "name": "user standing", "app_label": "student"}}, {"pk": 38, "model": "contenttypes.contenttype", "fields": {"model": "usertestgroup", "name": "user test group", "app_label": "student"}}, {"pk": 158, "model": "contenttypes.contenttype", "fields": {"model": "video", "name": "video", "app_label": "edxval"}}, {"pk": 167, "model": "contenttypes.contenttype", "fields": {"model": "videouploadconfig", "name": "video upload config", "app_label": "contentstore"}}, {"pk": 13, "model": "contenttypes.contenttype", "fields": {"model": "workerstate", "name": "worker", "app_label": "djcelery"}}, {"pk": 132, "model": "contenttypes.contenttype", "fields": {"model": "xblockasidesconfig", "name": "x block asides config", "app_label": "lms_xblock"}}, {"pk": 31, "model": "contenttypes.contenttype", "fields": {"model": "xmodulestudentinfofield", "name": "x module student info field", "app_label": "courseware"}}, {"pk": 30, "model": "contenttypes.contenttype", "fields": {"model": "xmodulestudentprefsfield", "name": "x module student prefs field", "app_label": "courseware"}}, {"pk": 29, "model": "contenttypes.contenttype", "fields": {"model": "xmoduleuserstatesummaryfield", "name": "x module user state summary field", "app_label": "courseware"}}, {"pk": 1, "model": "sites.site", "fields": {"domain": "example.com", "name": "example.com"}}, {"pk": 1, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:39Z", "app_name": "courseware", "migration": "0001_initial"}}, {"pk": 2, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:39Z", "app_name": "courseware", "migration": "0002_add_indexes"}}, {"pk": 3, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:39Z", "app_name": "courseware", "migration": "0003_done_grade_cache"}}, {"pk": 4, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:39Z", "app_name": "courseware", "migration": "0004_add_field_studentmodule_course_id"}}, {"pk": 5, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:39Z", "app_name": "courseware", "migration": "0005_auto__add_offlinecomputedgrade__add_unique_offlinecomputedgrade_user_c"}}, {"pk": 6, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:39Z", "app_name": "courseware", "migration": "0006_create_student_module_history"}}, {"pk": 7, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:39Z", "app_name": "courseware", "migration": "0007_allow_null_version_in_history"}}, {"pk": 8, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:40Z", "app_name": "courseware", "migration": "0008_add_xmodule_storage"}}, {"pk": 9, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:40Z", "app_name": "courseware", "migration": "0009_add_field_default"}}, {"pk": 10, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:40Z", "app_name": "courseware", "migration": "0010_rename_xblock_field_content_to_user_state_summary"}}, {"pk": 11, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:40Z", "app_name": "student", "migration": "0001_initial"}}, {"pk": 12, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:40Z", "app_name": "student", "migration": "0002_text_to_varchar_and_indexes"}}, {"pk": 13, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:40Z", "app_name": "student", "migration": "0003_auto__add_usertestgroup"}}, {"pk": 14, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0004_add_email_index"}}, {"pk": 15, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0005_name_change"}}, {"pk": 16, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0006_expand_meta_field"}}, {"pk": 17, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0007_convert_to_utf8"}}, {"pk": 18, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0008__auto__add_courseregistration"}}, {"pk": 19, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0009_auto__del_courseregistration__add_courseenrollment"}}, {"pk": 20, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0010_auto__chg_field_courseenrollment_course_id"}}, {"pk": 21, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0011_auto__chg_field_courseenrollment_user__del_unique_courseenrollment_use"}}, {"pk": 22, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0012_auto__add_field_userprofile_gender__add_field_userprofile_date_of_birt"}}, {"pk": 23, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0013_auto__chg_field_userprofile_meta"}}, {"pk": 24, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0014_auto__del_courseenrollment"}}, {"pk": 25, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0015_auto__add_courseenrollment__add_unique_courseenrollment_user_course_id"}}, {"pk": 26, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0016_auto__add_field_courseenrollment_date__chg_field_userprofile_country"}}, {"pk": 27, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0017_rename_date_to_created"}}, {"pk": 28, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0018_auto"}}, {"pk": 29, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0019_create_approved_demographic_fields_fall_2012"}}, {"pk": 30, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0020_add_test_center_user"}}, {"pk": 31, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0021_remove_askbot"}}, {"pk": 32, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0022_auto__add_courseenrollmentallowed__add_unique_courseenrollmentallowed_"}}, {"pk": 33, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0023_add_test_center_registration"}}, {"pk": 34, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0024_add_allow_certificate"}}, {"pk": 35, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0025_auto__add_field_courseenrollmentallowed_auto_enroll"}}, {"pk": 36, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0026_auto__remove_index_student_testcenterregistration_accommodation_request"}}, {"pk": 37, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0027_add_active_flag_and_mode_to_courseware_enrollment"}}, {"pk": 38, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0028_auto__add_userstanding"}}, {"pk": 39, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0029_add_lookup_table_between_user_and_anonymous_student_id"}}, {"pk": 40, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0029_remove_pearson"}}, {"pk": 41, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0030_auto__chg_field_anonymoususerid_anonymous_user_id"}}, {"pk": 42, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0031_drop_student_anonymoususerid_temp_archive"}}, {"pk": 43, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0032_add_field_UserProfile_country_add_field_UserProfile_city"}}, {"pk": 44, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:43Z", "app_name": "student", "migration": "0032_auto__add_loginfailures"}}, {"pk": 45, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:43Z", "app_name": "student", "migration": "0033_auto__add_passwordhistory"}}, {"pk": 46, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:43Z", "app_name": "student", "migration": "0034_auto__add_courseaccessrole"}}, {"pk": 47, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:44Z", "app_name": "student", "migration": "0035_access_roles"}}, {"pk": 48, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:44Z", "app_name": "student", "migration": "0036_access_roles_orgless"}}, {"pk": 49, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:44Z", "app_name": "student", "migration": "0037_auto__add_courseregistrationcode"}}, {"pk": 50, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:44Z", "app_name": "student", "migration": "0038_auto__add_usersignupsource"}}, {"pk": 51, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:44Z", "app_name": "student", "migration": "0039_auto__del_courseregistrationcode"}}, {"pk": 52, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:44Z", "app_name": "student", "migration": "0040_auto__del_field_usersignupsource_user_id__add_field_usersignupsource_u"}}, {"pk": 53, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:44Z", "app_name": "student", "migration": "0041_add_dashboard_config"}}, {"pk": 54, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:44Z", "app_name": "student", "migration": "0042_grant_sales_admin_roles"}}, {"pk": 55, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:44Z", "app_name": "student", "migration": "0043_auto__add_linkedinaddtoprofileconfiguration"}}, {"pk": 56, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:44Z", "app_name": "student", "migration": "0044_linkedin_add_company_identifier"}}, {"pk": 57, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:45Z", "app_name": "student", "migration": "0045_add_trk_partner_to_linkedin_config"}}, {"pk": 58, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:45Z", "app_name": "student", "migration": "0046_auto__add_entranceexamconfiguration__add_unique_entranceexamconfigurat"}}, {"pk": 59, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:45Z", "app_name": "track", "migration": "0001_initial"}}, {"pk": 60, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:45Z", "app_name": "track", "migration": "0002_auto__add_field_trackinglog_host__chg_field_trackinglog_event_type__ch"}}, {"pk": 61, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:45Z", "app_name": "util", "migration": "0001_initial"}}, {"pk": 62, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:45Z", "app_name": "util", "migration": "0002_default_rate_limit_config"}}, {"pk": 63, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0001_added_generatedcertificates"}}, {"pk": 64, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0002_auto__add_field_generatedcertificate_download_url"}}, {"pk": 65, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0003_auto__add_field_generatedcertificate_enabled"}}, {"pk": 66, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0004_auto__add_field_generatedcertificate_graded_certificate_id__add_field_"}}, {"pk": 67, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0005_auto__add_field_generatedcertificate_name"}}, {"pk": 68, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0006_auto__chg_field_generatedcertificate_certificate_id"}}, {"pk": 69, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0007_auto__add_revokedcertificate"}}, {"pk": 70, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0008_auto__del_revokedcertificate__del_field_generatedcertificate_name__add"}}, {"pk": 71, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0009_auto__del_field_generatedcertificate_graded_download_url__del_field_ge"}}, {"pk": 72, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0010_auto__del_field_generatedcertificate_enabled__add_field_generatedcerti"}}, {"pk": 73, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0011_auto__del_field_generatedcertificate_certificate_id__add_field_generat"}}, {"pk": 74, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0012_auto__add_field_generatedcertificate_name__add_field_generatedcertific"}}, {"pk": 75, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0013_auto__add_field_generatedcertificate_error_reason"}}, {"pk": 76, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0014_adding_whitelist"}}, {"pk": 77, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:47Z", "app_name": "certificates", "migration": "0015_adding_mode_for_verified_certs"}}, {"pk": 78, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:47Z", "app_name": "certificates", "migration": "0016_change_course_key_fields"}}, {"pk": 79, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:47Z", "app_name": "certificates", "migration": "0017_auto__add_certificategenerationconfiguration"}}, {"pk": 80, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:47Z", "app_name": "certificates", "migration": "0018_add_example_cert_models"}}, {"pk": 81, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:47Z", "app_name": "certificates", "migration": "0019_auto__add_certificatehtmlviewconfiguration"}}, {"pk": 82, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:47Z", "app_name": "certificates", "migration": "0020_certificatehtmlviewconfiguration_data"}}, {"pk": 83, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:47Z", "app_name": "instructor_task", "migration": "0001_initial"}}, {"pk": 84, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:47Z", "app_name": "instructor_task", "migration": "0002_add_subtask_field"}}, {"pk": 85, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:48Z", "app_name": "licenses", "migration": "0001_initial"}}, {"pk": 86, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:48Z", "app_name": "course_groups", "migration": "0001_initial"}}, {"pk": 87, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:48Z", "app_name": "course_groups", "migration": "0002_add_model_CourseUserGroupPartitionGroup"}}, {"pk": 88, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:48Z", "app_name": "course_groups", "migration": "0003_auto__add_coursecohort__add_coursecohortssettings"}}, {"pk": 89, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:48Z", "app_name": "course_groups", "migration": "0004_auto__del_field_coursecohortssettings_cohorted_discussions__add_field_"}}, {"pk": 90, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:49Z", "app_name": "bulk_email", "migration": "0001_initial"}}, {"pk": 91, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:49Z", "app_name": "bulk_email", "migration": "0002_change_field_names"}}, {"pk": 92, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:49Z", "app_name": "bulk_email", "migration": "0003_add_optout_user"}}, {"pk": 93, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:49Z", "app_name": "bulk_email", "migration": "0004_migrate_optout_user"}}, {"pk": 94, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:49Z", "app_name": "bulk_email", "migration": "0005_remove_optout_email"}}, {"pk": 95, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:49Z", "app_name": "bulk_email", "migration": "0006_add_course_email_template"}}, {"pk": 96, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:49Z", "app_name": "bulk_email", "migration": "0007_load_course_email_template"}}, {"pk": 97, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:49Z", "app_name": "bulk_email", "migration": "0008_add_course_authorizations"}}, {"pk": 98, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:49Z", "app_name": "bulk_email", "migration": "0009_force_unique_course_ids"}}, {"pk": 99, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:49Z", "app_name": "bulk_email", "migration": "0010_auto__chg_field_optout_course_id__add_field_courseemail_template_name_"}}, {"pk": 100, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:50Z", "app_name": "branding", "migration": "0001_initial"}}, {"pk": 101, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:50Z", "app_name": "external_auth", "migration": "0001_initial"}}, {"pk": 102, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:50Z", "app_name": "oauth2", "migration": "0001_initial"}}, {"pk": 103, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:50Z", "app_name": "oauth2", "migration": "0002_auto__chg_field_client_user"}}, {"pk": 104, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:51Z", "app_name": "oauth2", "migration": "0003_auto__add_field_client_name"}}, {"pk": 105, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:51Z", "app_name": "oauth2", "migration": "0004_auto__add_index_accesstoken_token"}}, {"pk": 106, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:51Z", "app_name": "oauth2_provider", "migration": "0001_initial"}}, {"pk": 107, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:52Z", "app_name": "wiki", "migration": "0001_initial"}}, {"pk": 108, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:52Z", "app_name": "wiki", "migration": "0002_auto__add_field_articleplugin_created"}}, {"pk": 109, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:52Z", "app_name": "wiki", "migration": "0003_auto__add_field_urlpath_article"}}, {"pk": 110, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:52Z", "app_name": "wiki", "migration": "0004_populate_urlpath__article"}}, {"pk": 111, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:52Z", "app_name": "wiki", "migration": "0005_auto__chg_field_urlpath_article"}}, {"pk": 112, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:52Z", "app_name": "wiki", "migration": "0006_auto__add_attachmentrevision__add_image__add_attachment"}}, {"pk": 113, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:52Z", "app_name": "wiki", "migration": "0007_auto__add_articlesubscription"}}, {"pk": 114, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:52Z", "app_name": "wiki", "migration": "0008_auto__add_simpleplugin__add_revisionpluginrevision__add_imagerevision_"}}, {"pk": 115, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:52Z", "app_name": "wiki", "migration": "0009_auto__add_field_imagerevision_width__add_field_imagerevision_height"}}, {"pk": 116, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:53Z", "app_name": "wiki", "migration": "0010_auto__chg_field_imagerevision_image"}}, {"pk": 117, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:53Z", "app_name": "wiki", "migration": "0011_auto__chg_field_imagerevision_width__chg_field_imagerevision_height"}}, {"pk": 118, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:53Z", "app_name": "django_notify", "migration": "0001_initial"}}, {"pk": 119, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:53Z", "app_name": "notifications", "migration": "0001_initial"}}, {"pk": 120, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:54Z", "app_name": "foldit", "migration": "0001_initial"}}, {"pk": 121, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:54Z", "app_name": "django_comment_client", "migration": "0001_initial"}}, {"pk": 122, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:54Z", "app_name": "django_comment_common", "migration": "0001_initial"}}, {"pk": 123, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:55Z", "app_name": "notes", "migration": "0001_initial"}}, {"pk": 124, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:55Z", "app_name": "splash", "migration": "0001_initial"}}, {"pk": 125, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:55Z", "app_name": "splash", "migration": "0002_auto__add_field_splashconfig_unaffected_url_paths"}}, {"pk": 126, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:56Z", "app_name": "user_api", "migration": "0001_initial"}}, {"pk": 127, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:56Z", "app_name": "user_api", "migration": "0002_auto__add_usercoursetags__add_unique_usercoursetags_user_course_id_key"}}, {"pk": 128, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:56Z", "app_name": "user_api", "migration": "0003_rename_usercoursetags"}}, {"pk": 129, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:56Z", "app_name": "user_api", "migration": "0004_auto__add_userorgtag__add_unique_userorgtag_user_org_key__chg_field_us"}}, {"pk": 130, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:56Z", "app_name": "shoppingcart", "migration": "0001_initial"}}, {"pk": 131, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:56Z", "app_name": "shoppingcart", "migration": "0002_auto__add_field_paidcourseregistration_mode"}}, {"pk": 132, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:56Z", "app_name": "shoppingcart", "migration": "0003_auto__del_field_orderitem_line_cost"}}, {"pk": 133, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:56Z", "app_name": "shoppingcart", "migration": "0004_auto__add_field_orderitem_fulfilled_time"}}, {"pk": 134, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:57Z", "app_name": "shoppingcart", "migration": "0005_auto__add_paidcourseregistrationannotation__add_field_orderitem_report"}}, {"pk": 135, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:57Z", "app_name": "shoppingcart", "migration": "0006_auto__add_field_order_refunded_time__add_field_orderitem_refund_reques"}}, {"pk": 136, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:57Z", "app_name": "shoppingcart", "migration": "0007_auto__add_field_orderitem_service_fee"}}, {"pk": 137, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:57Z", "app_name": "shoppingcart", "migration": "0008_auto__add_coupons__add_couponredemption__chg_field_certificateitem_cou"}}, {"pk": 138, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:57Z", "app_name": "shoppingcart", "migration": "0009_auto__del_coupons__add_courseregistrationcode__add_coupon__chg_field_c"}}, {"pk": 139, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:57Z", "app_name": "shoppingcart", "migration": "0010_auto__add_registrationcoderedemption__del_field_courseregistrationcode"}}, {"pk": 140, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:57Z", "app_name": "shoppingcart", "migration": "0011_auto__add_invoice__add_field_courseregistrationcode_invoice"}}, {"pk": 141, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:58Z", "app_name": "shoppingcart", "migration": "0012_auto__del_field_courseregistrationcode_transaction_group_name__del_fie"}}, {"pk": 142, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:58Z", "app_name": "shoppingcart", "migration": "0013_auto__add_field_invoice_is_valid"}}, {"pk": 143, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:58Z", "app_name": "shoppingcart", "migration": "0014_auto__del_field_invoice_tax_id__add_field_invoice_address_line_1__add_"}}, {"pk": 144, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:58Z", "app_name": "shoppingcart", "migration": "0015_auto__del_field_invoice_purchase_order_number__del_field_invoice_compa"}}, {"pk": 145, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:58Z", "app_name": "shoppingcart", "migration": "0016_auto__del_field_invoice_company_email__del_field_invoice_company_refer"}}, {"pk": 146, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:58Z", "app_name": "shoppingcart", "migration": "0017_auto__add_field_courseregistrationcode_order__chg_field_registrationco"}}, {"pk": 147, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:58Z", "app_name": "shoppingcart", "migration": "0018_auto__add_donation"}}, {"pk": 148, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:58Z", "app_name": "shoppingcart", "migration": "0019_auto__add_donationconfiguration"}}, {"pk": 149, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:58Z", "app_name": "shoppingcart", "migration": "0020_auto__add_courseregcodeitem__add_courseregcodeitemannotation__add_fiel"}}, {"pk": 150, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:58Z", "app_name": "shoppingcart", "migration": "0021_auto__add_field_orderitem_created__add_field_orderitem_modified"}}, {"pk": 151, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:59Z", "app_name": "shoppingcart", "migration": "0022_auto__add_field_registrationcoderedemption_course_enrollment__add_fiel"}}, {"pk": 152, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:59Z", "app_name": "shoppingcart", "migration": "0023_auto__add_field_coupon_expiration_date"}}, {"pk": 153, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:59Z", "app_name": "shoppingcart", "migration": "0024_auto__add_field_courseregistrationcode_mode_slug"}}, {"pk": 154, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:59Z", "app_name": "shoppingcart", "migration": "0025_update_invoice_models"}}, {"pk": 155, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:59Z", "app_name": "shoppingcart", "migration": "0026_migrate_invoices"}}, {"pk": 156, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:59Z", "app_name": "shoppingcart", "migration": "0027_add_invoice_history"}}, {"pk": 157, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:00Z", "app_name": "course_modes", "migration": "0001_initial"}}, {"pk": 158, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:00Z", "app_name": "course_modes", "migration": "0002_auto__add_field_coursemode_currency"}}, {"pk": 159, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:00Z", "app_name": "course_modes", "migration": "0003_auto__add_unique_coursemode_course_id_currency_mode_slug"}}, {"pk": 160, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:00Z", "app_name": "course_modes", "migration": "0004_auto__add_field_coursemode_expiration_date"}}, {"pk": 161, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:00Z", "app_name": "course_modes", "migration": "0005_auto__add_field_coursemode_expiration_datetime"}}, {"pk": 162, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:00Z", "app_name": "course_modes", "migration": "0006_expiration_date_to_datetime"}}, {"pk": 163, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:00Z", "app_name": "course_modes", "migration": "0007_add_description"}}, {"pk": 164, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:00Z", "app_name": "course_modes", "migration": "0007_auto__add_coursemodesarchive__chg_field_coursemode_course_id"}}, {"pk": 165, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:00Z", "app_name": "course_modes", "migration": "0008_auto__del_field_coursemodesarchive_description__add_field_coursemode_s"}}, {"pk": 166, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:00Z", "app_name": "verify_student", "migration": "0001_initial"}}, {"pk": 167, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:00Z", "app_name": "verify_student", "migration": "0002_auto__add_field_softwaresecurephotoverification_window"}}, {"pk": 168, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:00Z", "app_name": "verify_student", "migration": "0003_auto__add_field_softwaresecurephotoverification_display"}}, {"pk": 169, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:01Z", "app_name": "dark_lang", "migration": "0001_initial"}}, {"pk": 170, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:01Z", "app_name": "dark_lang", "migration": "0002_enable_on_install"}}, {"pk": 171, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:01Z", "app_name": "reverification", "migration": "0001_initial"}}, {"pk": 172, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:01Z", "app_name": "embargo", "migration": "0001_initial"}}, {"pk": 173, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:02Z", "app_name": "embargo", "migration": "0002_add_country_access_models"}}, {"pk": 174, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:02Z", "app_name": "embargo", "migration": "0003_add_countries"}}, {"pk": 175, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:02Z", "app_name": "embargo", "migration": "0004_migrate_embargo_config"}}, {"pk": 176, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:02Z", "app_name": "embargo", "migration": "0005_add_courseaccessrulehistory"}}, {"pk": 177, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:03Z", "app_name": "course_action_state", "migration": "0001_initial"}}, {"pk": 178, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:03Z", "app_name": "course_action_state", "migration": "0002_add_rerun_display_name"}}, {"pk": 179, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:03Z", "app_name": "mobile_api", "migration": "0001_initial"}}, {"pk": 180, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:04Z", "app_name": "survey", "migration": "0001_initial"}}, {"pk": 181, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:04Z", "app_name": "lms_xblock", "migration": "0001_initial"}}, {"pk": 182, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:04Z", "app_name": "course_structures", "migration": "0001_initial"}}, {"pk": 183, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:05Z", "app_name": "submissions", "migration": "0001_initial"}}, {"pk": 184, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:05Z", "app_name": "submissions", "migration": "0002_auto__add_scoresummary"}}, {"pk": 185, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:05Z", "app_name": "submissions", "migration": "0003_auto__del_field_submission_answer__add_field_submission_raw_answer"}}, {"pk": 186, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:05Z", "app_name": "submissions", "migration": "0004_auto__add_field_score_reset"}}, {"pk": 187, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:06Z", "app_name": "assessment", "migration": "0001_initial"}}, {"pk": 188, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:06Z", "app_name": "assessment", "migration": "0002_auto__add_assessmentfeedbackoption__del_field_assessmentfeedback_feedb"}}, {"pk": 189, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:06Z", "app_name": "assessment", "migration": "0003_add_index_pw_course_item_student"}}, {"pk": 190, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:06Z", "app_name": "assessment", "migration": "0004_auto__add_field_peerworkflow_graded_count"}}, {"pk": 191, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:06Z", "app_name": "assessment", "migration": "0005_auto__del_field_peerworkflow_graded_count__add_field_peerworkflow_grad"}}, {"pk": 192, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:06Z", "app_name": "assessment", "migration": "0006_auto__add_field_assessmentpart_feedback"}}, {"pk": 193, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:06Z", "app_name": "assessment", "migration": "0007_auto__chg_field_assessmentpart_feedback"}}, {"pk": 194, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:06Z", "app_name": "assessment", "migration": "0008_student_training"}}, {"pk": 195, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:06Z", "app_name": "assessment", "migration": "0009_auto__add_unique_studenttrainingworkflowitem_order_num_workflow"}}, {"pk": 196, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:06Z", "app_name": "assessment", "migration": "0010_auto__add_unique_studenttrainingworkflow_submission_uuid"}}, {"pk": 197, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:07Z", "app_name": "assessment", "migration": "0011_ai_training"}}, {"pk": 198, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:07Z", "app_name": "assessment", "migration": "0012_move_algorithm_id_to_classifier_set"}}, {"pk": 199, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:07Z", "app_name": "assessment", "migration": "0013_auto__add_field_aigradingworkflow_essay_text"}}, {"pk": 200, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:07Z", "app_name": "assessment", "migration": "0014_auto__add_field_aitrainingworkflow_item_id__add_field_aitrainingworkfl"}}, {"pk": 201, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:07Z", "app_name": "assessment", "migration": "0015_auto__add_unique_aitrainingworkflow_uuid__add_unique_aigradingworkflow"}}, {"pk": 202, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:07Z", "app_name": "assessment", "migration": "0016_auto__add_field_aiclassifierset_course_id__add_field_aiclassifierset_i"}}, {"pk": 203, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:07Z", "app_name": "assessment", "migration": "0016_auto__add_field_rubric_structure_hash"}}, {"pk": 204, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:07Z", "app_name": "assessment", "migration": "0017_rubric_structure_hash"}}, {"pk": 205, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:07Z", "app_name": "assessment", "migration": "0018_auto__add_field_assessmentpart_criterion"}}, {"pk": 206, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:07Z", "app_name": "assessment", "migration": "0019_assessmentpart_criterion_field"}}, {"pk": 207, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:07Z", "app_name": "assessment", "migration": "0020_assessmentpart_criterion_not_null"}}, {"pk": 208, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:07Z", "app_name": "assessment", "migration": "0021_assessmentpart_option_nullable"}}, {"pk": 209, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:08Z", "app_name": "assessment", "migration": "0022__add_label_fields"}}, {"pk": 210, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:08Z", "app_name": "assessment", "migration": "0023_assign_criteria_and_option_labels"}}, {"pk": 211, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:08Z", "app_name": "assessment", "migration": "0024_auto__chg_field_assessmentpart_criterion"}}, {"pk": 212, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:08Z", "app_name": "assessment", "migration": "0025_auto__add_field_peerworkflow_cancelled_at"}}, {"pk": 213, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:08Z", "app_name": "workflow", "migration": "0001_initial"}}, {"pk": 214, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:08Z", "app_name": "workflow", "migration": "0002_auto__add_field_assessmentworkflow_course_id__add_field_assessmentwork"}}, {"pk": 215, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:08Z", "app_name": "workflow", "migration": "0003_auto__add_assessmentworkflowstep"}}, {"pk": 216, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:08Z", "app_name": "workflow", "migration": "0004_auto__add_assessmentworkflowcancellation"}}, {"pk": 217, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:09Z", "app_name": "edxval", "migration": "0001_initial"}}, {"pk": 218, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:09Z", "app_name": "edxval", "migration": "0002_default_profiles"}}, {"pk": 219, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:09Z", "app_name": "edxval", "migration": "0003_status_and_created_fields"}}, {"pk": 220, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:09Z", "app_name": "edxval", "migration": "0004_remove_profile_fields"}}, {"pk": 221, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:10Z", "app_name": "milestones", "migration": "0001_initial"}}, {"pk": 222, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:10Z", "app_name": "milestones", "migration": "0002_seed_relationship_types"}}, {"pk": 223, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:10Z", "app_name": "django_extensions", "migration": "0001_empty"}}, {"pk": 224, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:15Z", "app_name": "contentstore", "migration": "0001_initial"}}, {"pk": 225, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:15Z", "app_name": "contentstore", "migration": "0002_auto__del_field_videouploadconfig_status_whitelist"}}, {"pk": 226, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:15Z", "app_name": "course_creators", "migration": "0001_initial"}}, {"pk": 227, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:16Z", "app_name": "xblock_config", "migration": "0001_initial"}}, {"pk": 5, "model": "embargo.country", "fields": {"country": "AD"}}, {"pk": 233, "model": "embargo.country", "fields": {"country": "AE"}}, {"pk": 1, "model": "embargo.country", "fields": {"country": "AF"}}, {"pk": 9, "model": "embargo.country", "fields": {"country": "AG"}}, {"pk": 7, "model": "embargo.country", "fields": {"country": "AI"}}, {"pk": 2, "model": "embargo.country", "fields": {"country": "AL"}}, {"pk": 11, "model": "embargo.country", "fields": {"country": "AM"}}, {"pk": 6, "model": "embargo.country", "fields": {"country": "AO"}}, {"pk": 8, "model": "embargo.country", "fields": {"country": "AQ"}}, {"pk": 10, "model": "embargo.country", "fields": {"country": "AR"}}, {"pk": 4, "model": "embargo.country", "fields": {"country": "AS"}}, {"pk": 14, "model": "embargo.country", "fields": {"country": "AT"}}, {"pk": 13, "model": "embargo.country", "fields": {"country": "AU"}}, {"pk": 12, "model": "embargo.country", "fields": {"country": "AW"}}, {"pk": 249, "model": "embargo.country", "fields": {"country": "AX"}}, {"pk": 15, "model": "embargo.country", "fields": {"country": "AZ"}}, {"pk": 28, "model": "embargo.country", "fields": {"country": "BA"}}, {"pk": 19, "model": "embargo.country", "fields": {"country": "BB"}}, {"pk": 18, "model": "embargo.country", "fields": {"country": "BD"}}, {"pk": 21, "model": "embargo.country", "fields": {"country": "BE"}}, {"pk": 35, "model": "embargo.country", "fields": {"country": "BF"}}, {"pk": 34, "model": "embargo.country", "fields": {"country": "BG"}}, {"pk": 17, "model": "embargo.country", "fields": {"country": "BH"}}, {"pk": 36, "model": "embargo.country", "fields": {"country": "BI"}}, {"pk": 23, "model": "embargo.country", "fields": {"country": "BJ"}}, {"pk": 184, "model": "embargo.country", "fields": {"country": "BL"}}, {"pk": 24, "model": "embargo.country", "fields": {"country": "BM"}}, {"pk": 33, "model": "embargo.country", "fields": {"country": "BN"}}, {"pk": 26, "model": "embargo.country", "fields": {"country": "BO"}}, {"pk": 27, "model": "embargo.country", "fields": {"country": "BQ"}}, {"pk": 31, "model": "embargo.country", "fields": {"country": "BR"}}, {"pk": 16, "model": "embargo.country", "fields": {"country": "BS"}}, {"pk": 25, "model": "embargo.country", "fields": {"country": "BT"}}, {"pk": 30, "model": "embargo.country", "fields": {"country": "BV"}}, {"pk": 29, "model": "embargo.country", "fields": {"country": "BW"}}, {"pk": 20, "model": "embargo.country", "fields": {"country": "BY"}}, {"pk": 22, "model": "embargo.country", "fields": {"country": "BZ"}}, {"pk": 39, "model": "embargo.country", "fields": {"country": "CA"}}, {"pk": 47, "model": "embargo.country", "fields": {"country": "CC"}}, {"pk": 51, "model": "embargo.country", "fields": {"country": "CD"}}, {"pk": 42, "model": "embargo.country", "fields": {"country": "CF"}}, {"pk": 50, "model": "embargo.country", "fields": {"country": "CG"}}, {"pk": 215, "model": "embargo.country", "fields": {"country": "CH"}}, {"pk": 59, "model": "embargo.country", "fields": {"country": "CI"}}, {"pk": 52, "model": "embargo.country", "fields": {"country": "CK"}}, {"pk": 44, "model": "embargo.country", "fields": {"country": "CL"}}, {"pk": 38, "model": "embargo.country", "fields": {"country": "CM"}}, {"pk": 45, "model": "embargo.country", "fields": {"country": "CN"}}, {"pk": 48, "model": "embargo.country", "fields": {"country": "CO"}}, {"pk": 53, "model": "embargo.country", "fields": {"country": "CR"}}, {"pk": 55, "model": "embargo.country", "fields": {"country": "CU"}}, {"pk": 40, "model": "embargo.country", "fields": {"country": "CV"}}, {"pk": 56, "model": "embargo.country", "fields": {"country": "CW"}}, {"pk": 46, "model": "embargo.country", "fields": {"country": "CX"}}, {"pk": 57, "model": "embargo.country", "fields": {"country": "CY"}}, {"pk": 58, "model": "embargo.country", "fields": {"country": "CZ"}}, {"pk": 82, "model": "embargo.country", "fields": {"country": "DE"}}, {"pk": 61, "model": "embargo.country", "fields": {"country": "DJ"}}, {"pk": 60, "model": "embargo.country", "fields": {"country": "DK"}}, {"pk": 62, "model": "embargo.country", "fields": {"country": "DM"}}, {"pk": 63, "model": "embargo.country", "fields": {"country": "DO"}}, {"pk": 3, "model": "embargo.country", "fields": {"country": "DZ"}}, {"pk": 64, "model": "embargo.country", "fields": {"country": "EC"}}, {"pk": 69, "model": "embargo.country", "fields": {"country": "EE"}}, {"pk": 65, "model": "embargo.country", "fields": {"country": "EG"}}, {"pk": 245, "model": "embargo.country", "fields": {"country": "EH"}}, {"pk": 68, "model": "embargo.country", "fields": {"country": "ER"}}, {"pk": 208, "model": "embargo.country", "fields": {"country": "ES"}}, {"pk": 70, "model": "embargo.country", "fields": {"country": "ET"}}, {"pk": 74, "model": "embargo.country", "fields": {"country": "FI"}}, {"pk": 73, "model": "embargo.country", "fields": {"country": "FJ"}}, {"pk": 71, "model": "embargo.country", "fields": {"country": "FK"}}, {"pk": 144, "model": "embargo.country", "fields": {"country": "FM"}}, {"pk": 72, "model": "embargo.country", "fields": {"country": "FO"}}, {"pk": 75, "model": "embargo.country", "fields": {"country": "FR"}}, {"pk": 79, "model": "embargo.country", "fields": {"country": "GA"}}, {"pk": 234, "model": "embargo.country", "fields": {"country": "GB"}}, {"pk": 87, "model": "embargo.country", "fields": {"country": "GD"}}, {"pk": 81, "model": "embargo.country", "fields": {"country": "GE"}}, {"pk": 76, "model": "embargo.country", "fields": {"country": "GF"}}, {"pk": 91, "model": "embargo.country", "fields": {"country": "GG"}}, {"pk": 83, "model": "embargo.country", "fields": {"country": "GH"}}, {"pk": 84, "model": "embargo.country", "fields": {"country": "GI"}}, {"pk": 86, "model": "embargo.country", "fields": {"country": "GL"}}, {"pk": 80, "model": "embargo.country", "fields": {"country": "GM"}}, {"pk": 92, "model": "embargo.country", "fields": {"country": "GN"}}, {"pk": 88, "model": "embargo.country", "fields": {"country": "GP"}}, {"pk": 67, "model": "embargo.country", "fields": {"country": "GQ"}}, {"pk": 85, "model": "embargo.country", "fields": {"country": "GR"}}, {"pk": 206, "model": "embargo.country", "fields": {"country": "GS"}}, {"pk": 90, "model": "embargo.country", "fields": {"country": "GT"}}, {"pk": 89, "model": "embargo.country", "fields": {"country": "GU"}}, {"pk": 93, "model": "embargo.country", "fields": {"country": "GW"}}, {"pk": 94, "model": "embargo.country", "fields": {"country": "GY"}}, {"pk": 99, "model": "embargo.country", "fields": {"country": "HK"}}, {"pk": 96, "model": "embargo.country", "fields": {"country": "HM"}}, {"pk": 98, "model": "embargo.country", "fields": {"country": "HN"}}, {"pk": 54, "model": "embargo.country", "fields": {"country": "HR"}}, {"pk": 95, "model": "embargo.country", "fields": {"country": "HT"}}, {"pk": 100, "model": "embargo.country", "fields": {"country": "HU"}}, {"pk": 103, "model": "embargo.country", "fields": {"country": "ID"}}, {"pk": 106, "model": "embargo.country", "fields": {"country": "IE"}}, {"pk": 108, "model": "embargo.country", "fields": {"country": "IL"}}, {"pk": 107, "model": "embargo.country", "fields": {"country": "IM"}}, {"pk": 102, "model": "embargo.country", "fields": {"country": "IN"}}, {"pk": 32, "model": "embargo.country", "fields": {"country": "IO"}}, {"pk": 105, "model": "embargo.country", "fields": {"country": "IQ"}}, {"pk": 104, "model": "embargo.country", "fields": {"country": "IR"}}, {"pk": 101, "model": "embargo.country", "fields": {"country": "IS"}}, {"pk": 109, "model": "embargo.country", "fields": {"country": "IT"}}, {"pk": 112, "model": "embargo.country", "fields": {"country": "JE"}}, {"pk": 110, "model": "embargo.country", "fields": {"country": "JM"}}, {"pk": 113, "model": "embargo.country", "fields": {"country": "JO"}}, {"pk": 111, "model": "embargo.country", "fields": {"country": "JP"}}, {"pk": 115, "model": "embargo.country", "fields": {"country": "KE"}}, {"pk": 120, "model": "embargo.country", "fields": {"country": "KG"}}, {"pk": 37, "model": "embargo.country", "fields": {"country": "KH"}}, {"pk": 116, "model": "embargo.country", "fields": {"country": "KI"}}, {"pk": 49, "model": "embargo.country", "fields": {"country": "KM"}}, {"pk": 186, "model": "embargo.country", "fields": {"country": "KN"}}, {"pk": 117, "model": "embargo.country", "fields": {"country": "KP"}}, {"pk": 118, "model": "embargo.country", "fields": {"country": "KR"}}, {"pk": 119, "model": "embargo.country", "fields": {"country": "KW"}}, {"pk": 41, "model": "embargo.country", "fields": {"country": "KY"}}, {"pk": 114, "model": "embargo.country", "fields": {"country": "KZ"}}, {"pk": 121, "model": "embargo.country", "fields": {"country": "LA"}}, {"pk": 123, "model": "embargo.country", "fields": {"country": "LB"}}, {"pk": 187, "model": "embargo.country", "fields": {"country": "LC"}}, {"pk": 127, "model": "embargo.country", "fields": {"country": "LI"}}, {"pk": 209, "model": "embargo.country", "fields": {"country": "LK"}}, {"pk": 125, "model": "embargo.country", "fields": {"country": "LR"}}, {"pk": 124, "model": "embargo.country", "fields": {"country": "LS"}}, {"pk": 128, "model": "embargo.country", "fields": {"country": "LT"}}, {"pk": 129, "model": "embargo.country", "fields": {"country": "LU"}}, {"pk": 122, "model": "embargo.country", "fields": {"country": "LV"}}, {"pk": 126, "model": "embargo.country", "fields": {"country": "LY"}}, {"pk": 150, "model": "embargo.country", "fields": {"country": "MA"}}, {"pk": 146, "model": "embargo.country", "fields": {"country": "MC"}}, {"pk": 145, "model": "embargo.country", "fields": {"country": "MD"}}, {"pk": 148, "model": "embargo.country", "fields": {"country": "ME"}}, {"pk": 188, "model": "embargo.country", "fields": {"country": "MF"}}, {"pk": 132, "model": "embargo.country", "fields": {"country": "MG"}}, {"pk": 138, "model": "embargo.country", "fields": {"country": "MH"}}, {"pk": 131, "model": "embargo.country", "fields": {"country": "MK"}}, {"pk": 136, "model": "embargo.country", "fields": {"country": "ML"}}, {"pk": 152, "model": "embargo.country", "fields": {"country": "MM"}}, {"pk": 147, "model": "embargo.country", "fields": {"country": "MN"}}, {"pk": 130, "model": "embargo.country", "fields": {"country": "MO"}}, {"pk": 164, "model": "embargo.country", "fields": {"country": "MP"}}, {"pk": 139, "model": "embargo.country", "fields": {"country": "MQ"}}, {"pk": 140, "model": "embargo.country", "fields": {"country": "MR"}}, {"pk": 149, "model": "embargo.country", "fields": {"country": "MS"}}, {"pk": 137, "model": "embargo.country", "fields": {"country": "MT"}}, {"pk": 141, "model": "embargo.country", "fields": {"country": "MU"}}, {"pk": 135, "model": "embargo.country", "fields": {"country": "MV"}}, {"pk": 133, "model": "embargo.country", "fields": {"country": "MW"}}, {"pk": 143, "model": "embargo.country", "fields": {"country": "MX"}}, {"pk": 134, "model": "embargo.country", "fields": {"country": "MY"}}, {"pk": 151, "model": "embargo.country", "fields": {"country": "MZ"}}, {"pk": 153, "model": "embargo.country", "fields": {"country": "NA"}}, {"pk": 157, "model": "embargo.country", "fields": {"country": "NC"}}, {"pk": 160, "model": "embargo.country", "fields": {"country": "NE"}}, {"pk": 163, "model": "embargo.country", "fields": {"country": "NF"}}, {"pk": 161, "model": "embargo.country", "fields": {"country": "NG"}}, {"pk": 159, "model": "embargo.country", "fields": {"country": "NI"}}, {"pk": 156, "model": "embargo.country", "fields": {"country": "NL"}}, {"pk": 165, "model": "embargo.country", "fields": {"country": "NO"}}, {"pk": 155, "model": "embargo.country", "fields": {"country": "NP"}}, {"pk": 154, "model": "embargo.country", "fields": {"country": "NR"}}, {"pk": 162, "model": "embargo.country", "fields": {"country": "NU"}}, {"pk": 158, "model": "embargo.country", "fields": {"country": "NZ"}}, {"pk": 166, "model": "embargo.country", "fields": {"country": "OM"}}, {"pk": 170, "model": "embargo.country", "fields": {"country": "PA"}}, {"pk": 173, "model": "embargo.country", "fields": {"country": "PE"}}, {"pk": 77, "model": "embargo.country", "fields": {"country": "PF"}}, {"pk": 171, "model": "embargo.country", "fields": {"country": "PG"}}, {"pk": 174, "model": "embargo.country", "fields": {"country": "PH"}}, {"pk": 167, "model": "embargo.country", "fields": {"country": "PK"}}, {"pk": 176, "model": "embargo.country", "fields": {"country": "PL"}}, {"pk": 189, "model": "embargo.country", "fields": {"country": "PM"}}, {"pk": 175, "model": "embargo.country", "fields": {"country": "PN"}}, {"pk": 178, "model": "embargo.country", "fields": {"country": "PR"}}, {"pk": 169, "model": "embargo.country", "fields": {"country": "PS"}}, {"pk": 177, "model": "embargo.country", "fields": {"country": "PT"}}, {"pk": 168, "model": "embargo.country", "fields": {"country": "PW"}}, {"pk": 172, "model": "embargo.country", "fields": {"country": "PY"}}, {"pk": 179, "model": "embargo.country", "fields": {"country": "QA"}}, {"pk": 183, "model": "embargo.country", "fields": {"country": "RE"}}, {"pk": 180, "model": "embargo.country", "fields": {"country": "RO"}}, {"pk": 196, "model": "embargo.country", "fields": {"country": "RS"}}, {"pk": 181, "model": "embargo.country", "fields": {"country": "RU"}}, {"pk": 182, "model": "embargo.country", "fields": {"country": "RW"}}, {"pk": 194, "model": "embargo.country", "fields": {"country": "SA"}}, {"pk": 203, "model": "embargo.country", "fields": {"country": "SB"}}, {"pk": 197, "model": "embargo.country", "fields": {"country": "SC"}}, {"pk": 210, "model": "embargo.country", "fields": {"country": "SD"}}, {"pk": 214, "model": "embargo.country", "fields": {"country": "SE"}}, {"pk": 199, "model": "embargo.country", "fields": {"country": "SG"}}, {"pk": 185, "model": "embargo.country", "fields": {"country": "SH"}}, {"pk": 202, "model": "embargo.country", "fields": {"country": "SI"}}, {"pk": 212, "model": "embargo.country", "fields": {"country": "SJ"}}, {"pk": 201, "model": "embargo.country", "fields": {"country": "SK"}}, {"pk": 198, "model": "embargo.country", "fields": {"country": "SL"}}, {"pk": 192, "model": "embargo.country", "fields": {"country": "SM"}}, {"pk": 195, "model": "embargo.country", "fields": {"country": "SN"}}, {"pk": 204, "model": "embargo.country", "fields": {"country": "SO"}}, {"pk": 211, "model": "embargo.country", "fields": {"country": "SR"}}, {"pk": 207, "model": "embargo.country", "fields": {"country": "SS"}}, {"pk": 193, "model": "embargo.country", "fields": {"country": "ST"}}, {"pk": 66, "model": "embargo.country", "fields": {"country": "SV"}}, {"pk": 200, "model": "embargo.country", "fields": {"country": "SX"}}, {"pk": 216, "model": "embargo.country", "fields": {"country": "SY"}}, {"pk": 213, "model": "embargo.country", "fields": {"country": "SZ"}}, {"pk": 229, "model": "embargo.country", "fields": {"country": "TC"}}, {"pk": 43, "model": "embargo.country", "fields": {"country": "TD"}}, {"pk": 78, "model": "embargo.country", "fields": {"country": "TF"}}, {"pk": 222, "model": "embargo.country", "fields": {"country": "TG"}}, {"pk": 220, "model": "embargo.country", "fields": {"country": "TH"}}, {"pk": 218, "model": "embargo.country", "fields": {"country": "TJ"}}, {"pk": 223, "model": "embargo.country", "fields": {"country": "TK"}}, {"pk": 221, "model": "embargo.country", "fields": {"country": "TL"}}, {"pk": 228, "model": "embargo.country", "fields": {"country": "TM"}}, {"pk": 226, "model": "embargo.country", "fields": {"country": "TN"}}, {"pk": 224, "model": "embargo.country", "fields": {"country": "TO"}}, {"pk": 227, "model": "embargo.country", "fields": {"country": "TR"}}, {"pk": 225, "model": "embargo.country", "fields": {"country": "TT"}}, {"pk": 230, "model": "embargo.country", "fields": {"country": "TV"}}, {"pk": 217, "model": "embargo.country", "fields": {"country": "TW"}}, {"pk": 219, "model": "embargo.country", "fields": {"country": "TZ"}}, {"pk": 232, "model": "embargo.country", "fields": {"country": "UA"}}, {"pk": 231, "model": "embargo.country", "fields": {"country": "UG"}}, {"pk": 236, "model": "embargo.country", "fields": {"country": "UM"}}, {"pk": 235, "model": "embargo.country", "fields": {"country": "US"}}, {"pk": 237, "model": "embargo.country", "fields": {"country": "UY"}}, {"pk": 238, "model": "embargo.country", "fields": {"country": "UZ"}}, {"pk": 97, "model": "embargo.country", "fields": {"country": "VA"}}, {"pk": 190, "model": "embargo.country", "fields": {"country": "VC"}}, {"pk": 240, "model": "embargo.country", "fields": {"country": "VE"}}, {"pk": 242, "model": "embargo.country", "fields": {"country": "VG"}}, {"pk": 243, "model": "embargo.country", "fields": {"country": "VI"}}, {"pk": 241, "model": "embargo.country", "fields": {"country": "VN"}}, {"pk": 239, "model": "embargo.country", "fields": {"country": "VU"}}, {"pk": 244, "model": "embargo.country", "fields": {"country": "WF"}}, {"pk": 191, "model": "embargo.country", "fields": {"country": "WS"}}, {"pk": 246, "model": "embargo.country", "fields": {"country": "YE"}}, {"pk": 142, "model": "embargo.country", "fields": {"country": "YT"}}, {"pk": 205, "model": "embargo.country", "fields": {"country": "ZA"}}, {"pk": 247, "model": "embargo.country", "fields": {"country": "ZM"}}, {"pk": 248, "model": "embargo.country", "fields": {"country": "ZW"}}, {"pk": 1, "model": "edxval.profile", "fields": {"profile_name": "desktop_mp4"}}, {"pk": 2, "model": "edxval.profile", "fields": {"profile_name": "desktop_webm"}}, {"pk": 3, "model": "edxval.profile", "fields": {"profile_name": "mobile_high"}}, {"pk": 4, "model": "edxval.profile", "fields": {"profile_name": "mobile_low"}}, {"pk": 5, "model": "edxval.profile", "fields": {"profile_name": "youtube"}}, {"pk": 1, "model": "milestones.milestonerelationshiptype", "fields": {"active": true, "description": "Autogenerated milestone relationship type \"fulfills\"", "modified": "2015-03-31T06:26:10Z", "name": "fulfills", "created": "2015-03-31T06:26:10Z"}}, {"pk": 2, "model": "milestones.milestonerelationshiptype", "fields": {"active": true, "description": "Autogenerated milestone relationship type \"requires\"", "modified": "2015-03-31T06:26:10Z", "name": "requires", "created": "2015-03-31T06:26:10Z"}}, {"pk": 61, "model": "auth.permission", "fields": {"codename": "add_logentry", "name": "Can add log entry", "content_type": 21}}, {"pk": 62, "model": "auth.permission", "fields": {"codename": "change_logentry", "name": "Can change log entry", "content_type": 21}}, {"pk": 63, "model": "auth.permission", "fields": {"codename": "delete_logentry", "name": "Can delete log entry", "content_type": 21}}, {"pk": 454, "model": "auth.permission", "fields": {"codename": "add_aiclassifier", "name": "Can add ai classifier", "content_type": 151}}, {"pk": 455, "model": "auth.permission", "fields": {"codename": "change_aiclassifier", "name": "Can change ai classifier", "content_type": 151}}, {"pk": 456, "model": "auth.permission", "fields": {"codename": "delete_aiclassifier", "name": "Can delete ai classifier", "content_type": 151}}, {"pk": 451, "model": "auth.permission", "fields": {"codename": "add_aiclassifierset", "name": "Can add ai classifier set", "content_type": 150}}, {"pk": 452, "model": "auth.permission", "fields": {"codename": "change_aiclassifierset", "name": "Can change ai classifier set", "content_type": 150}}, {"pk": 453, "model": "auth.permission", "fields": {"codename": "delete_aiclassifierset", "name": "Can delete ai classifier set", "content_type": 150}}, {"pk": 460, "model": "auth.permission", "fields": {"codename": "add_aigradingworkflow", "name": "Can add ai grading workflow", "content_type": 153}}, {"pk": 461, "model": "auth.permission", "fields": {"codename": "change_aigradingworkflow", "name": "Can change ai grading workflow", "content_type": 153}}, {"pk": 462, "model": "auth.permission", "fields": {"codename": "delete_aigradingworkflow", "name": "Can delete ai grading workflow", "content_type": 153}}, {"pk": 457, "model": "auth.permission", "fields": {"codename": "add_aitrainingworkflow", "name": "Can add ai training workflow", "content_type": 152}}, {"pk": 458, "model": "auth.permission", "fields": {"codename": "change_aitrainingworkflow", "name": "Can change ai training workflow", "content_type": 152}}, {"pk": 459, "model": "auth.permission", "fields": {"codename": "delete_aitrainingworkflow", "name": "Can delete ai training workflow", "content_type": 152}}, {"pk": 424, "model": "auth.permission", "fields": {"codename": "add_assessment", "name": "Can add assessment", "content_type": 141}}, {"pk": 425, "model": "auth.permission", "fields": {"codename": "change_assessment", "name": "Can change assessment", "content_type": 141}}, {"pk": 426, "model": "auth.permission", "fields": {"codename": "delete_assessment", "name": "Can delete assessment", "content_type": 141}}, {"pk": 433, "model": "auth.permission", "fields": {"codename": "add_assessmentfeedback", "name": "Can add assessment feedback", "content_type": 144}}, {"pk": 434, "model": "auth.permission", "fields": {"codename": "change_assessmentfeedback", "name": "Can change assessment feedback", "content_type": 144}}, {"pk": 435, "model": "auth.permission", "fields": {"codename": "delete_assessmentfeedback", "name": "Can delete assessment feedback", "content_type": 144}}, {"pk": 430, "model": "auth.permission", "fields": {"codename": "add_assessmentfeedbackoption", "name": "Can add assessment feedback option", "content_type": 143}}, {"pk": 431, "model": "auth.permission", "fields": {"codename": "change_assessmentfeedbackoption", "name": "Can change assessment feedback option", "content_type": 143}}, {"pk": 432, "model": "auth.permission", "fields": {"codename": "delete_assessmentfeedbackoption", "name": "Can delete assessment feedback option", "content_type": 143}}, {"pk": 427, "model": "auth.permission", "fields": {"codename": "add_assessmentpart", "name": "Can add assessment part", "content_type": 142}}, {"pk": 428, "model": "auth.permission", "fields": {"codename": "change_assessmentpart", "name": "Can change assessment part", "content_type": 142}}, {"pk": 429, "model": "auth.permission", "fields": {"codename": "delete_assessmentpart", "name": "Can delete assessment part", "content_type": 142}}, {"pk": 418, "model": "auth.permission", "fields": {"codename": "add_criterion", "name": "Can add criterion", "content_type": 139}}, {"pk": 419, "model": "auth.permission", "fields": {"codename": "change_criterion", "name": "Can change criterion", "content_type": 139}}, {"pk": 420, "model": "auth.permission", "fields": {"codename": "delete_criterion", "name": "Can delete criterion", "content_type": 139}}, {"pk": 421, "model": "auth.permission", "fields": {"codename": "add_criterionoption", "name": "Can add criterion option", "content_type": 140}}, {"pk": 422, "model": "auth.permission", "fields": {"codename": "change_criterionoption", "name": "Can change criterion option", "content_type": 140}}, {"pk": 423, "model": "auth.permission", "fields": {"codename": "delete_criterionoption", "name": "Can delete criterion option", "content_type": 140}}, {"pk": 436, "model": "auth.permission", "fields": {"codename": "add_peerworkflow", "name": "Can add peer workflow", "content_type": 145}}, {"pk": 437, "model": "auth.permission", "fields": {"codename": "change_peerworkflow", "name": "Can change peer workflow", "content_type": 145}}, {"pk": 438, "model": "auth.permission", "fields": {"codename": "delete_peerworkflow", "name": "Can delete peer workflow", "content_type": 145}}, {"pk": 439, "model": "auth.permission", "fields": {"codename": "add_peerworkflowitem", "name": "Can add peer workflow item", "content_type": 146}}, {"pk": 440, "model": "auth.permission", "fields": {"codename": "change_peerworkflowitem", "name": "Can change peer workflow item", "content_type": 146}}, {"pk": 441, "model": "auth.permission", "fields": {"codename": "delete_peerworkflowitem", "name": "Can delete peer workflow item", "content_type": 146}}, {"pk": 415, "model": "auth.permission", "fields": {"codename": "add_rubric", "name": "Can add rubric", "content_type": 138}}, {"pk": 416, "model": "auth.permission", "fields": {"codename": "change_rubric", "name": "Can change rubric", "content_type": 138}}, {"pk": 417, "model": "auth.permission", "fields": {"codename": "delete_rubric", "name": "Can delete rubric", "content_type": 138}}, {"pk": 445, "model": "auth.permission", "fields": {"codename": "add_studenttrainingworkflow", "name": "Can add student training workflow", "content_type": 148}}, {"pk": 446, "model": "auth.permission", "fields": {"codename": "change_studenttrainingworkflow", "name": "Can change student training workflow", "content_type": 148}}, {"pk": 447, "model": "auth.permission", "fields": {"codename": "delete_studenttrainingworkflow", "name": "Can delete student training workflow", "content_type": 148}}, {"pk": 448, "model": "auth.permission", "fields": {"codename": "add_studenttrainingworkflowitem", "name": "Can add student training workflow item", "content_type": 149}}, {"pk": 449, "model": "auth.permission", "fields": {"codename": "change_studenttrainingworkflowitem", "name": "Can change student training workflow item", "content_type": 149}}, {"pk": 450, "model": "auth.permission", "fields": {"codename": "delete_studenttrainingworkflowitem", "name": "Can delete student training workflow item", "content_type": 149}}, {"pk": 442, "model": "auth.permission", "fields": {"codename": "add_trainingexample", "name": "Can add training example", "content_type": 147}}, {"pk": 443, "model": "auth.permission", "fields": {"codename": "change_trainingexample", "name": "Can change training example", "content_type": 147}}, {"pk": 444, "model": "auth.permission", "fields": {"codename": "delete_trainingexample", "name": "Can delete training example", "content_type": 147}}, {"pk": 4, "model": "auth.permission", "fields": {"codename": "add_group", "name": "Can add group", "content_type": 2}}, {"pk": 5, "model": "auth.permission", "fields": {"codename": "change_group", "name": "Can change group", "content_type": 2}}, {"pk": 6, "model": "auth.permission", "fields": {"codename": "delete_group", "name": "Can delete group", "content_type": 2}}, {"pk": 1, "model": "auth.permission", "fields": {"codename": "add_permission", "name": "Can add permission", "content_type": 1}}, {"pk": 2, "model": "auth.permission", "fields": {"codename": "change_permission", "name": "Can change permission", "content_type": 1}}, {"pk": 3, "model": "auth.permission", "fields": {"codename": "delete_permission", "name": "Can delete permission", "content_type": 1}}, {"pk": 7, "model": "auth.permission", "fields": {"codename": "add_user", "name": "Can add user", "content_type": 3}}, {"pk": 8, "model": "auth.permission", "fields": {"codename": "change_user", "name": "Can change user", "content_type": 3}}, {"pk": 9, "model": "auth.permission", "fields": {"codename": "delete_user", "name": "Can delete user", "content_type": 3}}, {"pk": 208, "model": "auth.permission", "fields": {"codename": "add_brandinginfoconfig", "name": "Can add branding info config", "content_type": 70}}, {"pk": 209, "model": "auth.permission", "fields": {"codename": "change_brandinginfoconfig", "name": "Can change branding info config", "content_type": 70}}, {"pk": 210, "model": "auth.permission", "fields": {"codename": "delete_brandinginfoconfig", "name": "Can delete branding info config", "content_type": 70}}, {"pk": 205, "model": "auth.permission", "fields": {"codename": "add_courseauthorization", "name": "Can add course authorization", "content_type": 69}}, {"pk": 206, "model": "auth.permission", "fields": {"codename": "change_courseauthorization", "name": "Can change course authorization", "content_type": 69}}, {"pk": 207, "model": "auth.permission", "fields": {"codename": "delete_courseauthorization", "name": "Can delete course authorization", "content_type": 69}}, {"pk": 196, "model": "auth.permission", "fields": {"codename": "add_courseemail", "name": "Can add course email", "content_type": 66}}, {"pk": 197, "model": "auth.permission", "fields": {"codename": "change_courseemail", "name": "Can change course email", "content_type": 66}}, {"pk": 198, "model": "auth.permission", "fields": {"codename": "delete_courseemail", "name": "Can delete course email", "content_type": 66}}, {"pk": 202, "model": "auth.permission", "fields": {"codename": "add_courseemailtemplate", "name": "Can add course email template", "content_type": 68}}, {"pk": 203, "model": "auth.permission", "fields": {"codename": "change_courseemailtemplate", "name": "Can change course email template", "content_type": 68}}, {"pk": 204, "model": "auth.permission", "fields": {"codename": "delete_courseemailtemplate", "name": "Can delete course email template", "content_type": 68}}, {"pk": 199, "model": "auth.permission", "fields": {"codename": "add_optout", "name": "Can add optout", "content_type": 67}}, {"pk": 200, "model": "auth.permission", "fields": {"codename": "change_optout", "name": "Can change optout", "content_type": 67}}, {"pk": 201, "model": "auth.permission", "fields": {"codename": "delete_optout", "name": "Can delete optout", "content_type": 67}}, {"pk": 169, "model": "auth.permission", "fields": {"codename": "add_certificategenerationconfiguration", "name": "Can add certificate generation configuration", "content_type": 57}}, {"pk": 170, "model": "auth.permission", "fields": {"codename": "change_certificategenerationconfiguration", "name": "Can change certificate generation configuration", "content_type": 57}}, {"pk": 171, "model": "auth.permission", "fields": {"codename": "delete_certificategenerationconfiguration", "name": "Can delete certificate generation configuration", "content_type": 57}}, {"pk": 166, "model": "auth.permission", "fields": {"codename": "add_certificategenerationcoursesetting", "name": "Can add certificate generation course setting", "content_type": 56}}, {"pk": 167, "model": "auth.permission", "fields": {"codename": "change_certificategenerationcoursesetting", "name": "Can change certificate generation course setting", "content_type": 56}}, {"pk": 168, "model": "auth.permission", "fields": {"codename": "delete_certificategenerationcoursesetting", "name": "Can delete certificate generation course setting", "content_type": 56}}, {"pk": 172, "model": "auth.permission", "fields": {"codename": "add_certificatehtmlviewconfiguration", "name": "Can add certificate html view configuration", "content_type": 58}}, {"pk": 173, "model": "auth.permission", "fields": {"codename": "change_certificatehtmlviewconfiguration", "name": "Can change certificate html view configuration", "content_type": 58}}, {"pk": 174, "model": "auth.permission", "fields": {"codename": "delete_certificatehtmlviewconfiguration", "name": "Can delete certificate html view configuration", "content_type": 58}}, {"pk": 154, "model": "auth.permission", "fields": {"codename": "add_certificatewhitelist", "name": "Can add certificate whitelist", "content_type": 52}}, {"pk": 155, "model": "auth.permission", "fields": {"codename": "change_certificatewhitelist", "name": "Can change certificate whitelist", "content_type": 52}}, {"pk": 156, "model": "auth.permission", "fields": {"codename": "delete_certificatewhitelist", "name": "Can delete certificate whitelist", "content_type": 52}}, {"pk": 163, "model": "auth.permission", "fields": {"codename": "add_examplecertificate", "name": "Can add example certificate", "content_type": 55}}, {"pk": 164, "model": "auth.permission", "fields": {"codename": "change_examplecertificate", "name": "Can change example certificate", "content_type": 55}}, {"pk": 165, "model": "auth.permission", "fields": {"codename": "delete_examplecertificate", "name": "Can delete example certificate", "content_type": 55}}, {"pk": 160, "model": "auth.permission", "fields": {"codename": "add_examplecertificateset", "name": "Can add example certificate set", "content_type": 54}}, {"pk": 161, "model": "auth.permission", "fields": {"codename": "change_examplecertificateset", "name": "Can change example certificate set", "content_type": 54}}, {"pk": 162, "model": "auth.permission", "fields": {"codename": "delete_examplecertificateset", "name": "Can delete example certificate set", "content_type": 54}}, {"pk": 157, "model": "auth.permission", "fields": {"codename": "add_generatedcertificate", "name": "Can add generated certificate", "content_type": 53}}, {"pk": 158, "model": "auth.permission", "fields": {"codename": "change_generatedcertificate", "name": "Can change generated certificate", "content_type": 53}}, {"pk": 159, "model": "auth.permission", "fields": {"codename": "delete_generatedcertificate", "name": "Can delete generated certificate", "content_type": 53}}, {"pk": 46, "model": "auth.permission", "fields": {"codename": "add_servercircuit", "name": "Can add server circuit", "content_type": 16}}, {"pk": 47, "model": "auth.permission", "fields": {"codename": "change_servercircuit", "name": "Can change server circuit", "content_type": 16}}, {"pk": 48, "model": "auth.permission", "fields": {"codename": "delete_servercircuit", "name": "Can delete server circuit", "content_type": 16}}, {"pk": 502, "model": "auth.permission", "fields": {"codename": "add_videouploadconfig", "name": "Can add video upload config", "content_type": 167}}, {"pk": 503, "model": "auth.permission", "fields": {"codename": "change_videouploadconfig", "name": "Can change video upload config", "content_type": 167}}, {"pk": 504, "model": "auth.permission", "fields": {"codename": "delete_videouploadconfig", "name": "Can delete video upload config", "content_type": 167}}, {"pk": 10, "model": "auth.permission", "fields": {"codename": "add_contenttype", "name": "Can add content type", "content_type": 4}}, {"pk": 11, "model": "auth.permission", "fields": {"codename": "change_contenttype", "name": "Can change content type", "content_type": 4}}, {"pk": 12, "model": "auth.permission", "fields": {"codename": "delete_contenttype", "name": "Can delete content type", "content_type": 4}}, {"pk": 64, "model": "auth.permission", "fields": {"codename": "add_corsmodel", "name": "Can add cors model", "content_type": 22}}, {"pk": 65, "model": "auth.permission", "fields": {"codename": "change_corsmodel", "name": "Can change cors model", "content_type": 22}}, {"pk": 66, "model": "auth.permission", "fields": {"codename": "delete_corsmodel", "name": "Can delete cors model", "content_type": 22}}, {"pk": 94, "model": "auth.permission", "fields": {"codename": "add_offlinecomputedgrade", "name": "Can add offline computed grade", "content_type": 32}}, {"pk": 95, "model": "auth.permission", "fields": {"codename": "change_offlinecomputedgrade", "name": "Can change offline computed grade", "content_type": 32}}, {"pk": 96, "model": "auth.permission", "fields": {"codename": "delete_offlinecomputedgrade", "name": "Can delete offline computed grade", "content_type": 32}}, {"pk": 97, "model": "auth.permission", "fields": {"codename": "add_offlinecomputedgradelog", "name": "Can add offline computed grade log", "content_type": 33}}, {"pk": 98, "model": "auth.permission", "fields": {"codename": "change_offlinecomputedgradelog", "name": "Can change offline computed grade log", "content_type": 33}}, {"pk": 99, "model": "auth.permission", "fields": {"codename": "delete_offlinecomputedgradelog", "name": "Can delete offline computed grade log", "content_type": 33}}, {"pk": 79, "model": "auth.permission", "fields": {"codename": "add_studentmodule", "name": "Can add student module", "content_type": 27}}, {"pk": 80, "model": "auth.permission", "fields": {"codename": "change_studentmodule", "name": "Can change student module", "content_type": 27}}, {"pk": 81, "model": "auth.permission", "fields": {"codename": "delete_studentmodule", "name": "Can delete student module", "content_type": 27}}, {"pk": 82, "model": "auth.permission", "fields": {"codename": "add_studentmodulehistory", "name": "Can add student module history", "content_type": 28}}, {"pk": 83, "model": "auth.permission", "fields": {"codename": "change_studentmodulehistory", "name": "Can change student module history", "content_type": 28}}, {"pk": 84, "model": "auth.permission", "fields": {"codename": "delete_studentmodulehistory", "name": "Can delete student module history", "content_type": 28}}, {"pk": 91, "model": "auth.permission", "fields": {"codename": "add_xmodulestudentinfofield", "name": "Can add x module student info field", "content_type": 31}}, {"pk": 92, "model": "auth.permission", "fields": {"codename": "change_xmodulestudentinfofield", "name": "Can change x module student info field", "content_type": 31}}, {"pk": 93, "model": "auth.permission", "fields": {"codename": "delete_xmodulestudentinfofield", "name": "Can delete x module student info field", "content_type": 31}}, {"pk": 88, "model": "auth.permission", "fields": {"codename": "add_xmodulestudentprefsfield", "name": "Can add x module student prefs field", "content_type": 30}}, {"pk": 89, "model": "auth.permission", "fields": {"codename": "change_xmodulestudentprefsfield", "name": "Can change x module student prefs field", "content_type": 30}}, {"pk": 90, "model": "auth.permission", "fields": {"codename": "delete_xmodulestudentprefsfield", "name": "Can delete x module student prefs field", "content_type": 30}}, {"pk": 85, "model": "auth.permission", "fields": {"codename": "add_xmoduleuserstatesummaryfield", "name": "Can add x module user state summary field", "content_type": 29}}, {"pk": 86, "model": "auth.permission", "fields": {"codename": "change_xmoduleuserstatesummaryfield", "name": "Can change x module user state summary field", "content_type": 29}}, {"pk": 87, "model": "auth.permission", "fields": {"codename": "delete_xmoduleuserstatesummaryfield", "name": "Can delete x module user state summary field", "content_type": 29}}, {"pk": 385, "model": "auth.permission", "fields": {"codename": "add_coursererunstate", "name": "Can add course rerun state", "content_type": 128}}, {"pk": 386, "model": "auth.permission", "fields": {"codename": "change_coursererunstate", "name": "Can change course rerun state", "content_type": 128}}, {"pk": 387, "model": "auth.permission", "fields": {"codename": "delete_coursererunstate", "name": "Can delete course rerun state", "content_type": 128}}, {"pk": 505, "model": "auth.permission", "fields": {"codename": "add_coursecreator", "name": "Can add course creator", "content_type": 168}}, {"pk": 506, "model": "auth.permission", "fields": {"codename": "change_coursecreator", "name": "Can change course creator", "content_type": 168}}, {"pk": 507, "model": "auth.permission", "fields": {"codename": "delete_coursecreator", "name": "Can delete course creator", "content_type": 168}}, {"pk": 193, "model": "auth.permission", "fields": {"codename": "add_coursecohort", "name": "Can add course cohort", "content_type": 65}}, {"pk": 194, "model": "auth.permission", "fields": {"codename": "change_coursecohort", "name": "Can change course cohort", "content_type": 65}}, {"pk": 195, "model": "auth.permission", "fields": {"codename": "delete_coursecohort", "name": "Can delete course cohort", "content_type": 65}}, {"pk": 190, "model": "auth.permission", "fields": {"codename": "add_coursecohortssettings", "name": "Can add course cohorts settings", "content_type": 64}}, {"pk": 191, "model": "auth.permission", "fields": {"codename": "change_coursecohortssettings", "name": "Can change course cohorts settings", "content_type": 64}}, {"pk": 192, "model": "auth.permission", "fields": {"codename": "delete_coursecohortssettings", "name": "Can delete course cohorts settings", "content_type": 64}}, {"pk": 184, "model": "auth.permission", "fields": {"codename": "add_courseusergroup", "name": "Can add course user group", "content_type": 62}}, {"pk": 185, "model": "auth.permission", "fields": {"codename": "change_courseusergroup", "name": "Can change course user group", "content_type": 62}}, {"pk": 186, "model": "auth.permission", "fields": {"codename": "delete_courseusergroup", "name": "Can delete course user group", "content_type": 62}}, {"pk": 187, "model": "auth.permission", "fields": {"codename": "add_courseusergrouppartitiongroup", "name": "Can add course user group partition group", "content_type": 63}}, {"pk": 188, "model": "auth.permission", "fields": {"codename": "change_courseusergrouppartitiongroup", "name": "Can change course user group partition group", "content_type": 63}}, {"pk": 189, "model": "auth.permission", "fields": {"codename": "delete_courseusergrouppartitiongroup", "name": "Can delete course user group partition group", "content_type": 63}}, {"pk": 349, "model": "auth.permission", "fields": {"codename": "add_coursemode", "name": "Can add course mode", "content_type": 116}}, {"pk": 350, "model": "auth.permission", "fields": {"codename": "change_coursemode", "name": "Can change course mode", "content_type": 116}}, {"pk": 351, "model": "auth.permission", "fields": {"codename": "delete_coursemode", "name": "Can delete course mode", "content_type": 116}}, {"pk": 352, "model": "auth.permission", "fields": {"codename": "add_coursemodesarchive", "name": "Can add course modes archive", "content_type": 117}}, {"pk": 353, "model": "auth.permission", "fields": {"codename": "change_coursemodesarchive", "name": "Can change course modes archive", "content_type": 117}}, {"pk": 354, "model": "auth.permission", "fields": {"codename": "delete_coursemodesarchive", "name": "Can delete course modes archive", "content_type": 117}}, {"pk": 400, "model": "auth.permission", "fields": {"codename": "add_coursestructure", "name": "Can add course structure", "content_type": 133}}, {"pk": 401, "model": "auth.permission", "fields": {"codename": "change_coursestructure", "name": "Can change course structure", "content_type": 133}}, {"pk": 402, "model": "auth.permission", "fields": {"codename": "delete_coursestructure", "name": "Can delete course structure", "content_type": 133}}, {"pk": 358, "model": "auth.permission", "fields": {"codename": "add_darklangconfig", "name": "Can add dark lang config", "content_type": 119}}, {"pk": 359, "model": "auth.permission", "fields": {"codename": "change_darklangconfig", "name": "Can change dark lang config", "content_type": 119}}, {"pk": 360, "model": "auth.permission", "fields": {"codename": "delete_darklangconfig", "name": "Can delete dark lang config", "content_type": 119}}, {"pk": 73, "model": "auth.permission", "fields": {"codename": "add_association", "name": "Can add association", "content_type": 25}}, {"pk": 74, "model": "auth.permission", "fields": {"codename": "change_association", "name": "Can change association", "content_type": 25}}, {"pk": 75, "model": "auth.permission", "fields": {"codename": "delete_association", "name": "Can delete association", "content_type": 25}}, {"pk": 76, "model": "auth.permission", "fields": {"codename": "add_code", "name": "Can add code", "content_type": 26}}, {"pk": 77, "model": "auth.permission", "fields": {"codename": "change_code", "name": "Can change code", "content_type": 26}}, {"pk": 78, "model": "auth.permission", "fields": {"codename": "delete_code", "name": "Can delete code", "content_type": 26}}, {"pk": 70, "model": "auth.permission", "fields": {"codename": "add_nonce", "name": "Can add nonce", "content_type": 24}}, {"pk": 71, "model": "auth.permission", "fields": {"codename": "change_nonce", "name": "Can change nonce", "content_type": 24}}, {"pk": 72, "model": "auth.permission", "fields": {"codename": "delete_nonce", "name": "Can delete nonce", "content_type": 24}}, {"pk": 67, "model": "auth.permission", "fields": {"codename": "add_usersocialauth", "name": "Can add user social auth", "content_type": 23}}, {"pk": 68, "model": "auth.permission", "fields": {"codename": "change_usersocialauth", "name": "Can change user social auth", "content_type": 23}}, {"pk": 69, "model": "auth.permission", "fields": {"codename": "delete_usersocialauth", "name": "Can delete user social auth", "content_type": 23}}, {"pk": 271, "model": "auth.permission", "fields": {"codename": "add_notification", "name": "Can add notification", "content_type": 90}}, {"pk": 272, "model": "auth.permission", "fields": {"codename": "change_notification", "name": "Can change notification", "content_type": 90}}, {"pk": 273, "model": "auth.permission", "fields": {"codename": "delete_notification", "name": "Can delete notification", "content_type": 90}}, {"pk": 262, "model": "auth.permission", "fields": {"codename": "add_notificationtype", "name": "Can add type", "content_type": 87}}, {"pk": 263, "model": "auth.permission", "fields": {"codename": "change_notificationtype", "name": "Can change type", "content_type": 87}}, {"pk": 264, "model": "auth.permission", "fields": {"codename": "delete_notificationtype", "name": "Can delete type", "content_type": 87}}, {"pk": 265, "model": "auth.permission", "fields": {"codename": "add_settings", "name": "Can add settings", "content_type": 88}}, {"pk": 266, "model": "auth.permission", "fields": {"codename": "change_settings", "name": "Can change settings", "content_type": 88}}, {"pk": 267, "model": "auth.permission", "fields": {"codename": "delete_settings", "name": "Can delete settings", "content_type": 88}}, {"pk": 268, "model": "auth.permission", "fields": {"codename": "add_subscription", "name": "Can add subscription", "content_type": 89}}, {"pk": 269, "model": "auth.permission", "fields": {"codename": "change_subscription", "name": "Can change subscription", "content_type": 89}}, {"pk": 270, "model": "auth.permission", "fields": {"codename": "delete_subscription", "name": "Can delete subscription", "content_type": 89}}, {"pk": 55, "model": "auth.permission", "fields": {"codename": "add_association", "name": "Can add association", "content_type": 19}}, {"pk": 56, "model": "auth.permission", "fields": {"codename": "change_association", "name": "Can change association", "content_type": 19}}, {"pk": 57, "model": "auth.permission", "fields": {"codename": "delete_association", "name": "Can delete association", "content_type": 19}}, {"pk": 52, "model": "auth.permission", "fields": {"codename": "add_nonce", "name": "Can add nonce", "content_type": 18}}, {"pk": 53, "model": "auth.permission", "fields": {"codename": "change_nonce", "name": "Can change nonce", "content_type": 18}}, {"pk": 54, "model": "auth.permission", "fields": {"codename": "delete_nonce", "name": "Can delete nonce", "content_type": 18}}, {"pk": 58, "model": "auth.permission", "fields": {"codename": "add_useropenid", "name": "Can add user open id", "content_type": 20}}, {"pk": 59, "model": "auth.permission", "fields": {"codename": "change_useropenid", "name": "Can change user open id", "content_type": 20}}, {"pk": 60, "model": "auth.permission", "fields": {"codename": "delete_useropenid", "name": "Can delete user open id", "content_type": 20}}, {"pk": 28, "model": "auth.permission", "fields": {"codename": "add_crontabschedule", "name": "Can add crontab", "content_type": 10}}, {"pk": 29, "model": "auth.permission", "fields": {"codename": "change_crontabschedule", "name": "Can change crontab", "content_type": 10}}, {"pk": 30, "model": "auth.permission", "fields": {"codename": "delete_crontabschedule", "name": "Can delete crontab", "content_type": 10}}, {"pk": 25, "model": "auth.permission", "fields": {"codename": "add_intervalschedule", "name": "Can add interval", "content_type": 9}}, {"pk": 26, "model": "auth.permission", "fields": {"codename": "change_intervalschedule", "name": "Can change interval", "content_type": 9}}, {"pk": 27, "model": "auth.permission", "fields": {"codename": "delete_intervalschedule", "name": "Can delete interval", "content_type": 9}}, {"pk": 34, "model": "auth.permission", "fields": {"codename": "add_periodictask", "name": "Can add periodic task", "content_type": 12}}, {"pk": 35, "model": "auth.permission", "fields": {"codename": "change_periodictask", "name": "Can change periodic task", "content_type": 12}}, {"pk": 36, "model": "auth.permission", "fields": {"codename": "delete_periodictask", "name": "Can delete periodic task", "content_type": 12}}, {"pk": 31, "model": "auth.permission", "fields": {"codename": "add_periodictasks", "name": "Can add periodic tasks", "content_type": 11}}, {"pk": 32, "model": "auth.permission", "fields": {"codename": "change_periodictasks", "name": "Can change periodic tasks", "content_type": 11}}, {"pk": 33, "model": "auth.permission", "fields": {"codename": "delete_periodictasks", "name": "Can delete periodic tasks", "content_type": 11}}, {"pk": 19, "model": "auth.permission", "fields": {"codename": "add_taskmeta", "name": "Can add task state", "content_type": 7}}, {"pk": 20, "model": "auth.permission", "fields": {"codename": "change_taskmeta", "name": "Can change task state", "content_type": 7}}, {"pk": 21, "model": "auth.permission", "fields": {"codename": "delete_taskmeta", "name": "Can delete task state", "content_type": 7}}, {"pk": 22, "model": "auth.permission", "fields": {"codename": "add_tasksetmeta", "name": "Can add saved group result", "content_type": 8}}, {"pk": 23, "model": "auth.permission", "fields": {"codename": "change_tasksetmeta", "name": "Can change saved group result", "content_type": 8}}, {"pk": 24, "model": "auth.permission", "fields": {"codename": "delete_tasksetmeta", "name": "Can delete saved group result", "content_type": 8}}, {"pk": 40, "model": "auth.permission", "fields": {"codename": "add_taskstate", "name": "Can add task", "content_type": 14}}, {"pk": 41, "model": "auth.permission", "fields": {"codename": "change_taskstate", "name": "Can change task", "content_type": 14}}, {"pk": 42, "model": "auth.permission", "fields": {"codename": "delete_taskstate", "name": "Can delete task", "content_type": 14}}, {"pk": 37, "model": "auth.permission", "fields": {"codename": "add_workerstate", "name": "Can add worker", "content_type": 13}}, {"pk": 38, "model": "auth.permission", "fields": {"codename": "change_workerstate", "name": "Can change worker", "content_type": 13}}, {"pk": 39, "model": "auth.permission", "fields": {"codename": "delete_workerstate", "name": "Can delete worker", "content_type": 13}}, {"pk": 478, "model": "auth.permission", "fields": {"codename": "add_coursevideo", "name": "Can add course video", "content_type": 159}}, {"pk": 479, "model": "auth.permission", "fields": {"codename": "change_coursevideo", "name": "Can change course video", "content_type": 159}}, {"pk": 480, "model": "auth.permission", "fields": {"codename": "delete_coursevideo", "name": "Can delete course video", "content_type": 159}}, {"pk": 481, "model": "auth.permission", "fields": {"codename": "add_encodedvideo", "name": "Can add encoded video", "content_type": 160}}, {"pk": 482, "model": "auth.permission", "fields": {"codename": "change_encodedvideo", "name": "Can change encoded video", "content_type": 160}}, {"pk": 483, "model": "auth.permission", "fields": {"codename": "delete_encodedvideo", "name": "Can delete encoded video", "content_type": 160}}, {"pk": 472, "model": "auth.permission", "fields": {"codename": "add_profile", "name": "Can add profile", "content_type": 157}}, {"pk": 473, "model": "auth.permission", "fields": {"codename": "change_profile", "name": "Can change profile", "content_type": 157}}, {"pk": 474, "model": "auth.permission", "fields": {"codename": "delete_profile", "name": "Can delete profile", "content_type": 157}}, {"pk": 484, "model": "auth.permission", "fields": {"codename": "add_subtitle", "name": "Can add subtitle", "content_type": 161}}, {"pk": 485, "model": "auth.permission", "fields": {"codename": "change_subtitle", "name": "Can change subtitle", "content_type": 161}}, {"pk": 486, "model": "auth.permission", "fields": {"codename": "delete_subtitle", "name": "Can delete subtitle", "content_type": 161}}, {"pk": 475, "model": "auth.permission", "fields": {"codename": "add_video", "name": "Can add video", "content_type": 158}}, {"pk": 476, "model": "auth.permission", "fields": {"codename": "change_video", "name": "Can change video", "content_type": 158}}, {"pk": 477, "model": "auth.permission", "fields": {"codename": "delete_video", "name": "Can delete video", "content_type": 158}}, {"pk": 373, "model": "auth.permission", "fields": {"codename": "add_country", "name": "Can add country", "content_type": 124}}, {"pk": 374, "model": "auth.permission", "fields": {"codename": "change_country", "name": "Can change country", "content_type": 124}}, {"pk": 375, "model": "auth.permission", "fields": {"codename": "delete_country", "name": "Can delete country", "content_type": 124}}, {"pk": 376, "model": "auth.permission", "fields": {"codename": "add_countryaccessrule", "name": "Can add country access rule", "content_type": 125}}, {"pk": 377, "model": "auth.permission", "fields": {"codename": "change_countryaccessrule", "name": "Can change country access rule", "content_type": 125}}, {"pk": 378, "model": "auth.permission", "fields": {"codename": "delete_countryaccessrule", "name": "Can delete country access rule", "content_type": 125}}, {"pk": 379, "model": "auth.permission", "fields": {"codename": "add_courseaccessrulehistory", "name": "Can add course access rule history", "content_type": 126}}, {"pk": 380, "model": "auth.permission", "fields": {"codename": "change_courseaccessrulehistory", "name": "Can change course access rule history", "content_type": 126}}, {"pk": 381, "model": "auth.permission", "fields": {"codename": "delete_courseaccessrulehistory", "name": "Can delete course access rule history", "content_type": 126}}, {"pk": 364, "model": "auth.permission", "fields": {"codename": "add_embargoedcourse", "name": "Can add embargoed course", "content_type": 121}}, {"pk": 365, "model": "auth.permission", "fields": {"codename": "change_embargoedcourse", "name": "Can change embargoed course", "content_type": 121}}, {"pk": 366, "model": "auth.permission", "fields": {"codename": "delete_embargoedcourse", "name": "Can delete embargoed course", "content_type": 121}}, {"pk": 367, "model": "auth.permission", "fields": {"codename": "add_embargoedstate", "name": "Can add embargoed state", "content_type": 122}}, {"pk": 368, "model": "auth.permission", "fields": {"codename": "change_embargoedstate", "name": "Can change embargoed state", "content_type": 122}}, {"pk": 369, "model": "auth.permission", "fields": {"codename": "delete_embargoedstate", "name": "Can delete embargoed state", "content_type": 122}}, {"pk": 382, "model": "auth.permission", "fields": {"codename": "add_ipfilter", "name": "Can add ip filter", "content_type": 127}}, {"pk": 383, "model": "auth.permission", "fields": {"codename": "change_ipfilter", "name": "Can change ip filter", "content_type": 127}}, {"pk": 384, "model": "auth.permission", "fields": {"codename": "delete_ipfilter", "name": "Can delete ip filter", "content_type": 127}}, {"pk": 370, "model": "auth.permission", "fields": {"codename": "add_restrictedcourse", "name": "Can add restricted course", "content_type": 123}}, {"pk": 371, "model": "auth.permission", "fields": {"codename": "change_restrictedcourse", "name": "Can change restricted course", "content_type": 123}}, {"pk": 372, "model": "auth.permission", "fields": {"codename": "delete_restrictedcourse", "name": "Can delete restricted course", "content_type": 123}}, {"pk": 211, "model": "auth.permission", "fields": {"codename": "add_externalauthmap", "name": "Can add external auth map", "content_type": 71}}, {"pk": 212, "model": "auth.permission", "fields": {"codename": "change_externalauthmap", "name": "Can change external auth map", "content_type": 71}}, {"pk": 213, "model": "auth.permission", "fields": {"codename": "delete_externalauthmap", "name": "Can delete external auth map", "content_type": 71}}, {"pk": 277, "model": "auth.permission", "fields": {"codename": "add_puzzlecomplete", "name": "Can add puzzle complete", "content_type": 92}}, {"pk": 278, "model": "auth.permission", "fields": {"codename": "change_puzzlecomplete", "name": "Can change puzzle complete", "content_type": 92}}, {"pk": 279, "model": "auth.permission", "fields": {"codename": "delete_puzzlecomplete", "name": "Can delete puzzle complete", "content_type": 92}}, {"pk": 274, "model": "auth.permission", "fields": {"codename": "add_score", "name": "Can add score", "content_type": 91}}, {"pk": 275, "model": "auth.permission", "fields": {"codename": "change_score", "name": "Can change score", "content_type": 91}}, {"pk": 276, "model": "auth.permission", "fields": {"codename": "delete_score", "name": "Can delete score", "content_type": 91}}, {"pk": 175, "model": "auth.permission", "fields": {"codename": "add_instructortask", "name": "Can add instructor task", "content_type": 59}}, {"pk": 176, "model": "auth.permission", "fields": {"codename": "change_instructortask", "name": "Can change instructor task", "content_type": 59}}, {"pk": 177, "model": "auth.permission", "fields": {"codename": "delete_instructortask", "name": "Can delete instructor task", "content_type": 59}}, {"pk": 178, "model": "auth.permission", "fields": {"codename": "add_coursesoftware", "name": "Can add course software", "content_type": 60}}, {"pk": 179, "model": "auth.permission", "fields": {"codename": "change_coursesoftware", "name": "Can change course software", "content_type": 60}}, {"pk": 180, "model": "auth.permission", "fields": {"codename": "delete_coursesoftware", "name": "Can delete course software", "content_type": 60}}, {"pk": 181, "model": "auth.permission", "fields": {"codename": "add_userlicense", "name": "Can add user license", "content_type": 61}}, {"pk": 182, "model": "auth.permission", "fields": {"codename": "change_userlicense", "name": "Can change user license", "content_type": 61}}, {"pk": 183, "model": "auth.permission", "fields": {"codename": "delete_userlicense", "name": "Can delete user license", "content_type": 61}}, {"pk": 397, "model": "auth.permission", "fields": {"codename": "add_xblockasidesconfig", "name": "Can add x block asides config", "content_type": 132}}, {"pk": 398, "model": "auth.permission", "fields": {"codename": "change_xblockasidesconfig", "name": "Can change x block asides config", "content_type": 132}}, {"pk": 399, "model": "auth.permission", "fields": {"codename": "delete_xblockasidesconfig", "name": "Can delete x block asides config", "content_type": 132}}, {"pk": 496, "model": "auth.permission", "fields": {"codename": "add_coursecontentmilestone", "name": "Can add course content milestone", "content_type": 165}}, {"pk": 497, "model": "auth.permission", "fields": {"codename": "change_coursecontentmilestone", "name": "Can change course content milestone", "content_type": 165}}, {"pk": 498, "model": "auth.permission", "fields": {"codename": "delete_coursecontentmilestone", "name": "Can delete course content milestone", "content_type": 165}}, {"pk": 493, "model": "auth.permission", "fields": {"codename": "add_coursemilestone", "name": "Can add course milestone", "content_type": 164}}, {"pk": 494, "model": "auth.permission", "fields": {"codename": "change_coursemilestone", "name": "Can change course milestone", "content_type": 164}}, {"pk": 495, "model": "auth.permission", "fields": {"codename": "delete_coursemilestone", "name": "Can delete course milestone", "content_type": 164}}, {"pk": 487, "model": "auth.permission", "fields": {"codename": "add_milestone", "name": "Can add milestone", "content_type": 162}}, {"pk": 488, "model": "auth.permission", "fields": {"codename": "change_milestone", "name": "Can change milestone", "content_type": 162}}, {"pk": 489, "model": "auth.permission", "fields": {"codename": "delete_milestone", "name": "Can delete milestone", "content_type": 162}}, {"pk": 490, "model": "auth.permission", "fields": {"codename": "add_milestonerelationshiptype", "name": "Can add milestone relationship type", "content_type": 163}}, {"pk": 491, "model": "auth.permission", "fields": {"codename": "change_milestonerelationshiptype", "name": "Can change milestone relationship type", "content_type": 163}}, {"pk": 492, "model": "auth.permission", "fields": {"codename": "delete_milestonerelationshiptype", "name": "Can delete milestone relationship type", "content_type": 163}}, {"pk": 499, "model": "auth.permission", "fields": {"codename": "add_usermilestone", "name": "Can add user milestone", "content_type": 166}}, {"pk": 500, "model": "auth.permission", "fields": {"codename": "change_usermilestone", "name": "Can change user milestone", "content_type": 166}}, {"pk": 501, "model": "auth.permission", "fields": {"codename": "delete_usermilestone", "name": "Can delete user milestone", "content_type": 166}}, {"pk": 388, "model": "auth.permission", "fields": {"codename": "add_mobileapiconfig", "name": "Can add mobile api config", "content_type": 129}}, {"pk": 389, "model": "auth.permission", "fields": {"codename": "change_mobileapiconfig", "name": "Can change mobile api config", "content_type": 129}}, {"pk": 390, "model": "auth.permission", "fields": {"codename": "delete_mobileapiconfig", "name": "Can delete mobile api config", "content_type": 129}}, {"pk": 280, "model": "auth.permission", "fields": {"codename": "add_note", "name": "Can add note", "content_type": 93}}, {"pk": 281, "model": "auth.permission", "fields": {"codename": "change_note", "name": "Can change note", "content_type": 93}}, {"pk": 282, "model": "auth.permission", "fields": {"codename": "delete_note", "name": "Can delete note", "content_type": 93}}, {"pk": 220, "model": "auth.permission", "fields": {"codename": "add_accesstoken", "name": "Can add access token", "content_type": 74}}, {"pk": 221, "model": "auth.permission", "fields": {"codename": "change_accesstoken", "name": "Can change access token", "content_type": 74}}, {"pk": 222, "model": "auth.permission", "fields": {"codename": "delete_accesstoken", "name": "Can delete access token", "content_type": 74}}, {"pk": 214, "model": "auth.permission", "fields": {"codename": "add_client", "name": "Can add client", "content_type": 72}}, {"pk": 215, "model": "auth.permission", "fields": {"codename": "change_client", "name": "Can change client", "content_type": 72}}, {"pk": 216, "model": "auth.permission", "fields": {"codename": "delete_client", "name": "Can delete client", "content_type": 72}}, {"pk": 217, "model": "auth.permission", "fields": {"codename": "add_grant", "name": "Can add grant", "content_type": 73}}, {"pk": 218, "model": "auth.permission", "fields": {"codename": "change_grant", "name": "Can change grant", "content_type": 73}}, {"pk": 219, "model": "auth.permission", "fields": {"codename": "delete_grant", "name": "Can delete grant", "content_type": 73}}, {"pk": 223, "model": "auth.permission", "fields": {"codename": "add_refreshtoken", "name": "Can add refresh token", "content_type": 75}}, {"pk": 224, "model": "auth.permission", "fields": {"codename": "change_refreshtoken", "name": "Can change refresh token", "content_type": 75}}, {"pk": 225, "model": "auth.permission", "fields": {"codename": "delete_refreshtoken", "name": "Can delete refresh token", "content_type": 75}}, {"pk": 226, "model": "auth.permission", "fields": {"codename": "add_trustedclient", "name": "Can add trusted client", "content_type": 76}}, {"pk": 227, "model": "auth.permission", "fields": {"codename": "change_trustedclient", "name": "Can change trusted client", "content_type": 76}}, {"pk": 228, "model": "auth.permission", "fields": {"codename": "delete_trustedclient", "name": "Can delete trusted client", "content_type": 76}}, {"pk": 49, "model": "auth.permission", "fields": {"codename": "add_psychometricdata", "name": "Can add psychometric data", "content_type": 17}}, {"pk": 50, "model": "auth.permission", "fields": {"codename": "change_psychometricdata", "name": "Can change psychometric data", "content_type": 17}}, {"pk": 51, "model": "auth.permission", "fields": {"codename": "delete_psychometricdata", "name": "Can delete psychometric data", "content_type": 17}}, {"pk": 361, "model": "auth.permission", "fields": {"codename": "add_midcoursereverificationwindow", "name": "Can add midcourse reverification window", "content_type": 120}}, {"pk": 362, "model": "auth.permission", "fields": {"codename": "change_midcoursereverificationwindow", "name": "Can change midcourse reverification window", "content_type": 120}}, {"pk": 363, "model": "auth.permission", "fields": {"codename": "delete_midcoursereverificationwindow", "name": "Can delete midcourse reverification window", "content_type": 120}}, {"pk": 13, "model": "auth.permission", "fields": {"codename": "add_session", "name": "Can add session", "content_type": 5}}, {"pk": 14, "model": "auth.permission", "fields": {"codename": "change_session", "name": "Can change session", "content_type": 5}}, {"pk": 15, "model": "auth.permission", "fields": {"codename": "delete_session", "name": "Can delete session", "content_type": 5}}, {"pk": 340, "model": "auth.permission", "fields": {"codename": "add_certificateitem", "name": "Can add certificate item", "content_type": 113}}, {"pk": 341, "model": "auth.permission", "fields": {"codename": "change_certificateitem", "name": "Can change certificate item", "content_type": 113}}, {"pk": 342, "model": "auth.permission", "fields": {"codename": "delete_certificateitem", "name": "Can delete certificate item", "content_type": 113}}, {"pk": 322, "model": "auth.permission", "fields": {"codename": "add_coupon", "name": "Can add coupon", "content_type": 107}}, {"pk": 323, "model": "auth.permission", "fields": {"codename": "change_coupon", "name": "Can change coupon", "content_type": 107}}, {"pk": 324, "model": "auth.permission", "fields": {"codename": "delete_coupon", "name": "Can delete coupon", "content_type": 107}}, {"pk": 325, "model": "auth.permission", "fields": {"codename": "add_couponredemption", "name": "Can add coupon redemption", "content_type": 108}}, {"pk": 326, "model": "auth.permission", "fields": {"codename": "change_couponredemption", "name": "Can change coupon redemption", "content_type": 108}}, {"pk": 327, "model": "auth.permission", "fields": {"codename": "delete_couponredemption", "name": "Can delete coupon redemption", "content_type": 108}}, {"pk": 331, "model": "auth.permission", "fields": {"codename": "add_courseregcodeitem", "name": "Can add course reg code item", "content_type": 110}}, {"pk": 332, "model": "auth.permission", "fields": {"codename": "change_courseregcodeitem", "name": "Can change course reg code item", "content_type": 110}}, {"pk": 333, "model": "auth.permission", "fields": {"codename": "delete_courseregcodeitem", "name": "Can delete course reg code item", "content_type": 110}}, {"pk": 334, "model": "auth.permission", "fields": {"codename": "add_courseregcodeitemannotation", "name": "Can add course reg code item annotation", "content_type": 111}}, {"pk": 335, "model": "auth.permission", "fields": {"codename": "change_courseregcodeitemannotation", "name": "Can change course reg code item annotation", "content_type": 111}}, {"pk": 336, "model": "auth.permission", "fields": {"codename": "delete_courseregcodeitemannotation", "name": "Can delete course reg code item annotation", "content_type": 111}}, {"pk": 316, "model": "auth.permission", "fields": {"codename": "add_courseregistrationcode", "name": "Can add course registration code", "content_type": 105}}, {"pk": 317, "model": "auth.permission", "fields": {"codename": "change_courseregistrationcode", "name": "Can change course registration code", "content_type": 105}}, {"pk": 318, "model": "auth.permission", "fields": {"codename": "delete_courseregistrationcode", "name": "Can delete course registration code", "content_type": 105}}, {"pk": 310, "model": "auth.permission", "fields": {"codename": "add_courseregistrationcodeinvoiceitem", "name": "Can add course registration code invoice item", "content_type": 103}}, {"pk": 311, "model": "auth.permission", "fields": {"codename": "change_courseregistrationcodeinvoiceitem", "name": "Can change course registration code invoice item", "content_type": 103}}, {"pk": 312, "model": "auth.permission", "fields": {"codename": "delete_courseregistrationcodeinvoiceitem", "name": "Can delete course registration code invoice item", "content_type": 103}}, {"pk": 346, "model": "auth.permission", "fields": {"codename": "add_donation", "name": "Can add donation", "content_type": 115}}, {"pk": 347, "model": "auth.permission", "fields": {"codename": "change_donation", "name": "Can change donation", "content_type": 115}}, {"pk": 348, "model": "auth.permission", "fields": {"codename": "delete_donation", "name": "Can delete donation", "content_type": 115}}, {"pk": 343, "model": "auth.permission", "fields": {"codename": "add_donationconfiguration", "name": "Can add donation configuration", "content_type": 114}}, {"pk": 344, "model": "auth.permission", "fields": {"codename": "change_donationconfiguration", "name": "Can change donation configuration", "content_type": 114}}, {"pk": 345, "model": "auth.permission", "fields": {"codename": "delete_donationconfiguration", "name": "Can delete donation configuration", "content_type": 114}}, {"pk": 301, "model": "auth.permission", "fields": {"codename": "add_invoice", "name": "Can add invoice", "content_type": 100}}, {"pk": 302, "model": "auth.permission", "fields": {"codename": "change_invoice", "name": "Can change invoice", "content_type": 100}}, {"pk": 303, "model": "auth.permission", "fields": {"codename": "delete_invoice", "name": "Can delete invoice", "content_type": 100}}, {"pk": 313, "model": "auth.permission", "fields": {"codename": "add_invoicehistory", "name": "Can add invoice history", "content_type": 104}}, {"pk": 314, "model": "auth.permission", "fields": {"codename": "change_invoicehistory", "name": "Can change invoice history", "content_type": 104}}, {"pk": 315, "model": "auth.permission", "fields": {"codename": "delete_invoicehistory", "name": "Can delete invoice history", "content_type": 104}}, {"pk": 307, "model": "auth.permission", "fields": {"codename": "add_invoiceitem", "name": "Can add invoice item", "content_type": 102}}, {"pk": 308, "model": "auth.permission", "fields": {"codename": "change_invoiceitem", "name": "Can change invoice item", "content_type": 102}}, {"pk": 309, "model": "auth.permission", "fields": {"codename": "delete_invoiceitem", "name": "Can delete invoice item", "content_type": 102}}, {"pk": 304, "model": "auth.permission", "fields": {"codename": "add_invoicetransaction", "name": "Can add invoice transaction", "content_type": 101}}, {"pk": 305, "model": "auth.permission", "fields": {"codename": "change_invoicetransaction", "name": "Can change invoice transaction", "content_type": 101}}, {"pk": 306, "model": "auth.permission", "fields": {"codename": "delete_invoicetransaction", "name": "Can delete invoice transaction", "content_type": 101}}, {"pk": 295, "model": "auth.permission", "fields": {"codename": "add_order", "name": "Can add order", "content_type": 98}}, {"pk": 296, "model": "auth.permission", "fields": {"codename": "change_order", "name": "Can change order", "content_type": 98}}, {"pk": 297, "model": "auth.permission", "fields": {"codename": "delete_order", "name": "Can delete order", "content_type": 98}}, {"pk": 298, "model": "auth.permission", "fields": {"codename": "add_orderitem", "name": "Can add order item", "content_type": 99}}, {"pk": 299, "model": "auth.permission", "fields": {"codename": "change_orderitem", "name": "Can change order item", "content_type": 99}}, {"pk": 300, "model": "auth.permission", "fields": {"codename": "delete_orderitem", "name": "Can delete order item", "content_type": 99}}, {"pk": 328, "model": "auth.permission", "fields": {"codename": "add_paidcourseregistration", "name": "Can add paid course registration", "content_type": 109}}, {"pk": 329, "model": "auth.permission", "fields": {"codename": "change_paidcourseregistration", "name": "Can change paid course registration", "content_type": 109}}, {"pk": 330, "model": "auth.permission", "fields": {"codename": "delete_paidcourseregistration", "name": "Can delete paid course registration", "content_type": 109}}, {"pk": 337, "model": "auth.permission", "fields": {"codename": "add_paidcourseregistrationannotation", "name": "Can add paid course registration annotation", "content_type": 112}}, {"pk": 338, "model": "auth.permission", "fields": {"codename": "change_paidcourseregistrationannotation", "name": "Can change paid course registration annotation", "content_type": 112}}, {"pk": 339, "model": "auth.permission", "fields": {"codename": "delete_paidcourseregistrationannotation", "name": "Can delete paid course registration annotation", "content_type": 112}}, {"pk": 319, "model": "auth.permission", "fields": {"codename": "add_registrationcoderedemption", "name": "Can add registration code redemption", "content_type": 106}}, {"pk": 320, "model": "auth.permission", "fields": {"codename": "change_registrationcoderedemption", "name": "Can change registration code redemption", "content_type": 106}}, {"pk": 321, "model": "auth.permission", "fields": {"codename": "delete_registrationcoderedemption", "name": "Can delete registration code redemption", "content_type": 106}}, {"pk": 16, "model": "auth.permission", "fields": {"codename": "add_site", "name": "Can add site", "content_type": 6}}, {"pk": 17, "model": "auth.permission", "fields": {"codename": "change_site", "name": "Can change site", "content_type": 6}}, {"pk": 18, "model": "auth.permission", "fields": {"codename": "delete_site", "name": "Can delete site", "content_type": 6}}, {"pk": 43, "model": "auth.permission", "fields": {"codename": "add_migrationhistory", "name": "Can add migration history", "content_type": 15}}, {"pk": 44, "model": "auth.permission", "fields": {"codename": "change_migrationhistory", "name": "Can change migration history", "content_type": 15}}, {"pk": 45, "model": "auth.permission", "fields": {"codename": "delete_migrationhistory", "name": "Can delete migration history", "content_type": 15}}, {"pk": 283, "model": "auth.permission", "fields": {"codename": "add_splashconfig", "name": "Can add splash config", "content_type": 94}}, {"pk": 284, "model": "auth.permission", "fields": {"codename": "change_splashconfig", "name": "Can change splash config", "content_type": 94}}, {"pk": 285, "model": "auth.permission", "fields": {"codename": "delete_splashconfig", "name": "Can delete splash config", "content_type": 94}}, {"pk": 100, "model": "auth.permission", "fields": {"codename": "add_anonymoususerid", "name": "Can add anonymous user id", "content_type": 34}}, {"pk": 101, "model": "auth.permission", "fields": {"codename": "change_anonymoususerid", "name": "Can change anonymous user id", "content_type": 34}}, {"pk": 102, "model": "auth.permission", "fields": {"codename": "delete_anonymoususerid", "name": "Can delete anonymous user id", "content_type": 34}}, {"pk": 136, "model": "auth.permission", "fields": {"codename": "add_courseaccessrole", "name": "Can add course access role", "content_type": 46}}, {"pk": 137, "model": "auth.permission", "fields": {"codename": "change_courseaccessrole", "name": "Can change course access role", "content_type": 46}}, {"pk": 138, "model": "auth.permission", "fields": {"codename": "delete_courseaccessrole", "name": "Can delete course access role", "content_type": 46}}, {"pk": 130, "model": "auth.permission", "fields": {"codename": "add_courseenrollment", "name": "Can add course enrollment", "content_type": 44}}, {"pk": 131, "model": "auth.permission", "fields": {"codename": "change_courseenrollment", "name": "Can change course enrollment", "content_type": 44}}, {"pk": 132, "model": "auth.permission", "fields": {"codename": "delete_courseenrollment", "name": "Can delete course enrollment", "content_type": 44}}, {"pk": 133, "model": "auth.permission", "fields": {"codename": "add_courseenrollmentallowed", "name": "Can add course enrollment allowed", "content_type": 45}}, {"pk": 134, "model": "auth.permission", "fields": {"codename": "change_courseenrollmentallowed", "name": "Can change course enrollment allowed", "content_type": 45}}, {"pk": 135, "model": "auth.permission", "fields": {"codename": "delete_courseenrollmentallowed", "name": "Can delete course enrollment allowed", "content_type": 45}}, {"pk": 139, "model": "auth.permission", "fields": {"codename": "add_dashboardconfiguration", "name": "Can add dashboard configuration", "content_type": 47}}, {"pk": 140, "model": "auth.permission", "fields": {"codename": "change_dashboardconfiguration", "name": "Can change dashboard configuration", "content_type": 47}}, {"pk": 141, "model": "auth.permission", "fields": {"codename": "delete_dashboardconfiguration", "name": "Can delete dashboard configuration", "content_type": 47}}, {"pk": 145, "model": "auth.permission", "fields": {"codename": "add_entranceexamconfiguration", "name": "Can add entrance exam configuration", "content_type": 49}}, {"pk": 146, "model": "auth.permission", "fields": {"codename": "change_entranceexamconfiguration", "name": "Can change entrance exam configuration", "content_type": 49}}, {"pk": 147, "model": "auth.permission", "fields": {"codename": "delete_entranceexamconfiguration", "name": "Can delete entrance exam configuration", "content_type": 49}}, {"pk": 142, "model": "auth.permission", "fields": {"codename": "add_linkedinaddtoprofileconfiguration", "name": "Can add linked in add to profile configuration", "content_type": 48}}, {"pk": 143, "model": "auth.permission", "fields": {"codename": "change_linkedinaddtoprofileconfiguration", "name": "Can change linked in add to profile configuration", "content_type": 48}}, {"pk": 144, "model": "auth.permission", "fields": {"codename": "delete_linkedinaddtoprofileconfiguration", "name": "Can delete linked in add to profile configuration", "content_type": 48}}, {"pk": 127, "model": "auth.permission", "fields": {"codename": "add_loginfailures", "name": "Can add login failures", "content_type": 43}}, {"pk": 128, "model": "auth.permission", "fields": {"codename": "change_loginfailures", "name": "Can change login failures", "content_type": 43}}, {"pk": 129, "model": "auth.permission", "fields": {"codename": "delete_loginfailures", "name": "Can delete login failures", "content_type": 43}}, {"pk": 124, "model": "auth.permission", "fields": {"codename": "add_passwordhistory", "name": "Can add password history", "content_type": 42}}, {"pk": 125, "model": "auth.permission", "fields": {"codename": "change_passwordhistory", "name": "Can change password history", "content_type": 42}}, {"pk": 126, "model": "auth.permission", "fields": {"codename": "delete_passwordhistory", "name": "Can delete password history", "content_type": 42}}, {"pk": 121, "model": "auth.permission", "fields": {"codename": "add_pendingemailchange", "name": "Can add pending email change", "content_type": 41}}, {"pk": 122, "model": "auth.permission", "fields": {"codename": "change_pendingemailchange", "name": "Can change pending email change", "content_type": 41}}, {"pk": 123, "model": "auth.permission", "fields": {"codename": "delete_pendingemailchange", "name": "Can delete pending email change", "content_type": 41}}, {"pk": 118, "model": "auth.permission", "fields": {"codename": "add_pendingnamechange", "name": "Can add pending name change", "content_type": 40}}, {"pk": 119, "model": "auth.permission", "fields": {"codename": "change_pendingnamechange", "name": "Can change pending name change", "content_type": 40}}, {"pk": 120, "model": "auth.permission", "fields": {"codename": "delete_pendingnamechange", "name": "Can delete pending name change", "content_type": 40}}, {"pk": 115, "model": "auth.permission", "fields": {"codename": "add_registration", "name": "Can add registration", "content_type": 39}}, {"pk": 116, "model": "auth.permission", "fields": {"codename": "change_registration", "name": "Can change registration", "content_type": 39}}, {"pk": 117, "model": "auth.permission", "fields": {"codename": "delete_registration", "name": "Can delete registration", "content_type": 39}}, {"pk": 106, "model": "auth.permission", "fields": {"codename": "add_userprofile", "name": "Can add user profile", "content_type": 36}}, {"pk": 107, "model": "auth.permission", "fields": {"codename": "change_userprofile", "name": "Can change user profile", "content_type": 36}}, {"pk": 108, "model": "auth.permission", "fields": {"codename": "delete_userprofile", "name": "Can delete user profile", "content_type": 36}}, {"pk": 109, "model": "auth.permission", "fields": {"codename": "add_usersignupsource", "name": "Can add user signup source", "content_type": 37}}, {"pk": 110, "model": "auth.permission", "fields": {"codename": "change_usersignupsource", "name": "Can change user signup source", "content_type": 37}}, {"pk": 111, "model": "auth.permission", "fields": {"codename": "delete_usersignupsource", "name": "Can delete user signup source", "content_type": 37}}, {"pk": 103, "model": "auth.permission", "fields": {"codename": "add_userstanding", "name": "Can add user standing", "content_type": 35}}, {"pk": 104, "model": "auth.permission", "fields": {"codename": "change_userstanding", "name": "Can change user standing", "content_type": 35}}, {"pk": 105, "model": "auth.permission", "fields": {"codename": "delete_userstanding", "name": "Can delete user standing", "content_type": 35}}, {"pk": 112, "model": "auth.permission", "fields": {"codename": "add_usertestgroup", "name": "Can add user test group", "content_type": 38}}, {"pk": 113, "model": "auth.permission", "fields": {"codename": "change_usertestgroup", "name": "Can change user test group", "content_type": 38}}, {"pk": 114, "model": "auth.permission", "fields": {"codename": "delete_usertestgroup", "name": "Can delete user test group", "content_type": 38}}, {"pk": 409, "model": "auth.permission", "fields": {"codename": "add_score", "name": "Can add score", "content_type": 136}}, {"pk": 410, "model": "auth.permission", "fields": {"codename": "change_score", "name": "Can change score", "content_type": 136}}, {"pk": 411, "model": "auth.permission", "fields": {"codename": "delete_score", "name": "Can delete score", "content_type": 136}}, {"pk": 412, "model": "auth.permission", "fields": {"codename": "add_scoresummary", "name": "Can add score summary", "content_type": 137}}, {"pk": 413, "model": "auth.permission", "fields": {"codename": "change_scoresummary", "name": "Can change score summary", "content_type": 137}}, {"pk": 414, "model": "auth.permission", "fields": {"codename": "delete_scoresummary", "name": "Can delete score summary", "content_type": 137}}, {"pk": 403, "model": "auth.permission", "fields": {"codename": "add_studentitem", "name": "Can add student item", "content_type": 134}}, {"pk": 404, "model": "auth.permission", "fields": {"codename": "change_studentitem", "name": "Can change student item", "content_type": 134}}, {"pk": 405, "model": "auth.permission", "fields": {"codename": "delete_studentitem", "name": "Can delete student item", "content_type": 134}}, {"pk": 406, "model": "auth.permission", "fields": {"codename": "add_submission", "name": "Can add submission", "content_type": 135}}, {"pk": 407, "model": "auth.permission", "fields": {"codename": "change_submission", "name": "Can change submission", "content_type": 135}}, {"pk": 408, "model": "auth.permission", "fields": {"codename": "delete_submission", "name": "Can delete submission", "content_type": 135}}, {"pk": 394, "model": "auth.permission", "fields": {"codename": "add_surveyanswer", "name": "Can add survey answer", "content_type": 131}}, {"pk": 395, "model": "auth.permission", "fields": {"codename": "change_surveyanswer", "name": "Can change survey answer", "content_type": 131}}, {"pk": 396, "model": "auth.permission", "fields": {"codename": "delete_surveyanswer", "name": "Can delete survey answer", "content_type": 131}}, {"pk": 391, "model": "auth.permission", "fields": {"codename": "add_surveyform", "name": "Can add survey form", "content_type": 130}}, {"pk": 392, "model": "auth.permission", "fields": {"codename": "change_surveyform", "name": "Can change survey form", "content_type": 130}}, {"pk": 393, "model": "auth.permission", "fields": {"codename": "delete_surveyform", "name": "Can delete survey form", "content_type": 130}}, {"pk": 148, "model": "auth.permission", "fields": {"codename": "add_trackinglog", "name": "Can add tracking log", "content_type": 50}}, {"pk": 149, "model": "auth.permission", "fields": {"codename": "change_trackinglog", "name": "Can change tracking log", "content_type": 50}}, {"pk": 150, "model": "auth.permission", "fields": {"codename": "delete_trackinglog", "name": "Can delete tracking log", "content_type": 50}}, {"pk": 289, "model": "auth.permission", "fields": {"codename": "add_usercoursetag", "name": "Can add user course tag", "content_type": 96}}, {"pk": 290, "model": "auth.permission", "fields": {"codename": "change_usercoursetag", "name": "Can change user course tag", "content_type": 96}}, {"pk": 291, "model": "auth.permission", "fields": {"codename": "delete_usercoursetag", "name": "Can delete user course tag", "content_type": 96}}, {"pk": 292, "model": "auth.permission", "fields": {"codename": "add_userorgtag", "name": "Can add user org tag", "content_type": 97}}, {"pk": 293, "model": "auth.permission", "fields": {"codename": "change_userorgtag", "name": "Can change user org tag", "content_type": 97}}, {"pk": 294, "model": "auth.permission", "fields": {"codename": "delete_userorgtag", "name": "Can delete user org tag", "content_type": 97}}, {"pk": 286, "model": "auth.permission", "fields": {"codename": "add_userpreference", "name": "Can add user preference", "content_type": 95}}, {"pk": 287, "model": "auth.permission", "fields": {"codename": "change_userpreference", "name": "Can change user preference", "content_type": 95}}, {"pk": 288, "model": "auth.permission", "fields": {"codename": "delete_userpreference", "name": "Can delete user preference", "content_type": 95}}, {"pk": 151, "model": "auth.permission", "fields": {"codename": "add_ratelimitconfiguration", "name": "Can add rate limit configuration", "content_type": 51}}, {"pk": 152, "model": "auth.permission", "fields": {"codename": "change_ratelimitconfiguration", "name": "Can change rate limit configuration", "content_type": 51}}, {"pk": 153, "model": "auth.permission", "fields": {"codename": "delete_ratelimitconfiguration", "name": "Can delete rate limit configuration", "content_type": 51}}, {"pk": 355, "model": "auth.permission", "fields": {"codename": "add_softwaresecurephotoverification", "name": "Can add software secure photo verification", "content_type": 118}}, {"pk": 356, "model": "auth.permission", "fields": {"codename": "change_softwaresecurephotoverification", "name": "Can change software secure photo verification", "content_type": 118}}, {"pk": 357, "model": "auth.permission", "fields": {"codename": "delete_softwaresecurephotoverification", "name": "Can delete software secure photo verification", "content_type": 118}}, {"pk": 229, "model": "auth.permission", "fields": {"codename": "add_article", "name": "Can add article", "content_type": 77}}, {"pk": 233, "model": "auth.permission", "fields": {"codename": "assign", "name": "Can change ownership of any article", "content_type": 77}}, {"pk": 230, "model": "auth.permission", "fields": {"codename": "change_article", "name": "Can change article", "content_type": 77}}, {"pk": 231, "model": "auth.permission", "fields": {"codename": "delete_article", "name": "Can delete article", "content_type": 77}}, {"pk": 234, "model": "auth.permission", "fields": {"codename": "grant", "name": "Can assign permissions to other users", "content_type": 77}}, {"pk": 232, "model": "auth.permission", "fields": {"codename": "moderate", "name": "Can edit all articles and lock/unlock/restore", "content_type": 77}}, {"pk": 235, "model": "auth.permission", "fields": {"codename": "add_articleforobject", "name": "Can add Article for object", "content_type": 78}}, {"pk": 236, "model": "auth.permission", "fields": {"codename": "change_articleforobject", "name": "Can change Article for object", "content_type": 78}}, {"pk": 237, "model": "auth.permission", "fields": {"codename": "delete_articleforobject", "name": "Can delete Article for object", "content_type": 78}}, {"pk": 244, "model": "auth.permission", "fields": {"codename": "add_articleplugin", "name": "Can add article plugin", "content_type": 81}}, {"pk": 245, "model": "auth.permission", "fields": {"codename": "change_articleplugin", "name": "Can change article plugin", "content_type": 81}}, {"pk": 246, "model": "auth.permission", "fields": {"codename": "delete_articleplugin", "name": "Can delete article plugin", "content_type": 81}}, {"pk": 238, "model": "auth.permission", "fields": {"codename": "add_articlerevision", "name": "Can add article revision", "content_type": 79}}, {"pk": 239, "model": "auth.permission", "fields": {"codename": "change_articlerevision", "name": "Can change article revision", "content_type": 79}}, {"pk": 240, "model": "auth.permission", "fields": {"codename": "delete_articlerevision", "name": "Can delete article revision", "content_type": 79}}, {"pk": 259, "model": "auth.permission", "fields": {"codename": "add_articlesubscription", "name": "Can add article subscription", "content_type": 86}}, {"pk": 260, "model": "auth.permission", "fields": {"codename": "change_articlesubscription", "name": "Can change article subscription", "content_type": 86}}, {"pk": 261, "model": "auth.permission", "fields": {"codename": "delete_articlesubscription", "name": "Can delete article subscription", "content_type": 86}}, {"pk": 247, "model": "auth.permission", "fields": {"codename": "add_reusableplugin", "name": "Can add reusable plugin", "content_type": 82}}, {"pk": 248, "model": "auth.permission", "fields": {"codename": "change_reusableplugin", "name": "Can change reusable plugin", "content_type": 82}}, {"pk": 249, "model": "auth.permission", "fields": {"codename": "delete_reusableplugin", "name": "Can delete reusable plugin", "content_type": 82}}, {"pk": 253, "model": "auth.permission", "fields": {"codename": "add_revisionplugin", "name": "Can add revision plugin", "content_type": 84}}, {"pk": 254, "model": "auth.permission", "fields": {"codename": "change_revisionplugin", "name": "Can change revision plugin", "content_type": 84}}, {"pk": 255, "model": "auth.permission", "fields": {"codename": "delete_revisionplugin", "name": "Can delete revision plugin", "content_type": 84}}, {"pk": 256, "model": "auth.permission", "fields": {"codename": "add_revisionpluginrevision", "name": "Can add revision plugin revision", "content_type": 85}}, {"pk": 257, "model": "auth.permission", "fields": {"codename": "change_revisionpluginrevision", "name": "Can change revision plugin revision", "content_type": 85}}, {"pk": 258, "model": "auth.permission", "fields": {"codename": "delete_revisionpluginrevision", "name": "Can delete revision plugin revision", "content_type": 85}}, {"pk": 250, "model": "auth.permission", "fields": {"codename": "add_simpleplugin", "name": "Can add simple plugin", "content_type": 83}}, {"pk": 251, "model": "auth.permission", "fields": {"codename": "change_simpleplugin", "name": "Can change simple plugin", "content_type": 83}}, {"pk": 252, "model": "auth.permission", "fields": {"codename": "delete_simpleplugin", "name": "Can delete simple plugin", "content_type": 83}}, {"pk": 241, "model": "auth.permission", "fields": {"codename": "add_urlpath", "name": "Can add URL path", "content_type": 80}}, {"pk": 242, "model": "auth.permission", "fields": {"codename": "change_urlpath", "name": "Can change URL path", "content_type": 80}}, {"pk": 243, "model": "auth.permission", "fields": {"codename": "delete_urlpath", "name": "Can delete URL path", "content_type": 80}}, {"pk": 463, "model": "auth.permission", "fields": {"codename": "add_assessmentworkflow", "name": "Can add assessment workflow", "content_type": 154}}, {"pk": 464, "model": "auth.permission", "fields": {"codename": "change_assessmentworkflow", "name": "Can change assessment workflow", "content_type": 154}}, {"pk": 465, "model": "auth.permission", "fields": {"codename": "delete_assessmentworkflow", "name": "Can delete assessment workflow", "content_type": 154}}, {"pk": 469, "model": "auth.permission", "fields": {"codename": "add_assessmentworkflowcancellation", "name": "Can add assessment workflow cancellation", "content_type": 156}}, {"pk": 470, "model": "auth.permission", "fields": {"codename": "change_assessmentworkflowcancellation", "name": "Can change assessment workflow cancellation", "content_type": 156}}, {"pk": 471, "model": "auth.permission", "fields": {"codename": "delete_assessmentworkflowcancellation", "name": "Can delete assessment workflow cancellation", "content_type": 156}}, {"pk": 466, "model": "auth.permission", "fields": {"codename": "add_assessmentworkflowstep", "name": "Can add assessment workflow step", "content_type": 155}}, {"pk": 467, "model": "auth.permission", "fields": {"codename": "change_assessmentworkflowstep", "name": "Can change assessment workflow step", "content_type": 155}}, {"pk": 468, "model": "auth.permission", "fields": {"codename": "delete_assessmentworkflowstep", "name": "Can delete assessment workflow step", "content_type": 155}}, {"pk": 508, "model": "auth.permission", "fields": {"codename": "add_studioconfig", "name": "Can add studio config", "content_type": 169}}, {"pk": 509, "model": "auth.permission", "fields": {"codename": "change_studioconfig", "name": "Can change studio config", "content_type": 169}}, {"pk": 510, "model": "auth.permission", "fields": {"codename": "delete_studioconfig", "name": "Can delete studio config", "content_type": 169}}, {"pk": 1, "model": "util.ratelimitconfiguration", "fields": {"change_date": "2015-03-31T06:25:45Z", "changed_by": null, "enabled": true}}, {"pk": 1, "model": "certificates.certificatehtmlviewconfiguration", "fields": {"change_date": "2015-03-31T06:25:47Z", "changed_by": null, "configuration": "{\n {\n \"default\": {\n \"accomplishment_class_append\": \"accomplishment-certificate\",\n \"platform_name\": \"edX\",\n \"company_privacy_url\": \"http://www.edx.org/edx-privacy-policy\",\n \"company_tos_url\": \"http://www.edx.org/edx-terms-service\",\n \"company_verified_certificate_url\": \"http://www.edx.org/verified-certificate\",\n \"document_stylesheet_url_application\": \"/static/certificates/sass/main-ltr.css\",\n \"logo_src\": \"/static/certificates/images/logo-edx.svg\",\n \"logo_url\": \"http://www.edx.org\"\n },\n \"honor\": {\n \"certificate_type\": \"Honor Code\",\n \"document_body_class_append\": \"is-honorcode\"\n },\n \"verified\": {\n \"certificate_type\": \"Verified\",\n \"document_body_class_append\": \"is-idverified\"\n },\n \"xseries\": {\n \"certificate_type\": \"XSeries\",\n \"document_body_class_append\": \"is-xseries\"\n }\n}\n }", "enabled": false}}, {"pk": 1, "model": "dark_lang.darklangconfig", "fields": {"change_date": "2015-03-31T06:26:01Z", "changed_by": null, "enabled": true, "released_languages": ""}}, {"pk": 1, "model": "mobile_api.mobileapiconfig", "fields": {"change_date": "2015-03-31T06:26:03Z", "video_profiles": "mobile_low,mobile_high,youtube", "changed_by": null, "enabled": false}}] \ No newline at end of file +[{"pk": 74, "model": "contenttypes.contenttype", "fields": {"model": "accesstoken", "name": "access token", "app_label": "oauth2"}}, {"pk": 151, "model": "contenttypes.contenttype", "fields": {"model": "aiclassifier", "name": "ai classifier", "app_label": "assessment"}}, {"pk": 150, "model": "contenttypes.contenttype", "fields": {"model": "aiclassifierset", "name": "ai classifier set", "app_label": "assessment"}}, {"pk": 153, "model": "contenttypes.contenttype", "fields": {"model": "aigradingworkflow", "name": "ai grading workflow", "app_label": "assessment"}}, {"pk": 152, "model": "contenttypes.contenttype", "fields": {"model": "aitrainingworkflow", "name": "ai training workflow", "app_label": "assessment"}}, {"pk": 34, "model": "contenttypes.contenttype", "fields": {"model": "anonymoususerid", "name": "anonymous user id", "app_label": "student"}}, {"pk": 77, "model": "contenttypes.contenttype", "fields": {"model": "article", "name": "article", "app_label": "wiki"}}, {"pk": 78, "model": "contenttypes.contenttype", "fields": {"model": "articleforobject", "name": "Article for object", "app_label": "wiki"}}, {"pk": 81, "model": "contenttypes.contenttype", "fields": {"model": "articleplugin", "name": "article plugin", "app_label": "wiki"}}, {"pk": 79, "model": "contenttypes.contenttype", "fields": {"model": "articlerevision", "name": "article revision", "app_label": "wiki"}}, {"pk": 86, "model": "contenttypes.contenttype", "fields": {"model": "articlesubscription", "name": "article subscription", "app_label": "wiki"}}, {"pk": 141, "model": "contenttypes.contenttype", "fields": {"model": "assessment", "name": "assessment", "app_label": "assessment"}}, {"pk": 144, "model": "contenttypes.contenttype", "fields": {"model": "assessmentfeedback", "name": "assessment feedback", "app_label": "assessment"}}, {"pk": 143, "model": "contenttypes.contenttype", "fields": {"model": "assessmentfeedbackoption", "name": "assessment feedback option", "app_label": "assessment"}}, {"pk": 142, "model": "contenttypes.contenttype", "fields": {"model": "assessmentpart", "name": "assessment part", "app_label": "assessment"}}, {"pk": 154, "model": "contenttypes.contenttype", "fields": {"model": "assessmentworkflow", "name": "assessment workflow", "app_label": "workflow"}}, {"pk": 156, "model": "contenttypes.contenttype", "fields": {"model": "assessmentworkflowcancellation", "name": "assessment workflow cancellation", "app_label": "workflow"}}, {"pk": 155, "model": "contenttypes.contenttype", "fields": {"model": "assessmentworkflowstep", "name": "assessment workflow step", "app_label": "workflow"}}, {"pk": 19, "model": "contenttypes.contenttype", "fields": {"model": "association", "name": "association", "app_label": "django_openid_auth"}}, {"pk": 25, "model": "contenttypes.contenttype", "fields": {"model": "association", "name": "association", "app_label": "default"}}, {"pk": 70, "model": "contenttypes.contenttype", "fields": {"model": "brandinginfoconfig", "name": "branding info config", "app_label": "branding"}}, {"pk": 57, "model": "contenttypes.contenttype", "fields": {"model": "certificategenerationconfiguration", "name": "certificate generation configuration", "app_label": "certificates"}}, {"pk": 56, "model": "contenttypes.contenttype", "fields": {"model": "certificategenerationcoursesetting", "name": "certificate generation course setting", "app_label": "certificates"}}, {"pk": 58, "model": "contenttypes.contenttype", "fields": {"model": "certificatehtmlviewconfiguration", "name": "certificate html view configuration", "app_label": "certificates"}}, {"pk": 113, "model": "contenttypes.contenttype", "fields": {"model": "certificateitem", "name": "certificate item", "app_label": "shoppingcart"}}, {"pk": 52, "model": "contenttypes.contenttype", "fields": {"model": "certificatewhitelist", "name": "certificate whitelist", "app_label": "certificates"}}, {"pk": 72, "model": "contenttypes.contenttype", "fields": {"model": "client", "name": "client", "app_label": "oauth2"}}, {"pk": 26, "model": "contenttypes.contenttype", "fields": {"model": "code", "name": "code", "app_label": "default"}}, {"pk": 4, "model": "contenttypes.contenttype", "fields": {"model": "contenttype", "name": "content type", "app_label": "contenttypes"}}, {"pk": 22, "model": "contenttypes.contenttype", "fields": {"model": "corsmodel", "name": "cors model", "app_label": "corsheaders"}}, {"pk": 124, "model": "contenttypes.contenttype", "fields": {"model": "country", "name": "country", "app_label": "embargo"}}, {"pk": 125, "model": "contenttypes.contenttype", "fields": {"model": "countryaccessrule", "name": "country access rule", "app_label": "embargo"}}, {"pk": 107, "model": "contenttypes.contenttype", "fields": {"model": "coupon", "name": "coupon", "app_label": "shoppingcart"}}, {"pk": 108, "model": "contenttypes.contenttype", "fields": {"model": "couponredemption", "name": "coupon redemption", "app_label": "shoppingcart"}}, {"pk": 46, "model": "contenttypes.contenttype", "fields": {"model": "courseaccessrole", "name": "course access role", "app_label": "student"}}, {"pk": 126, "model": "contenttypes.contenttype", "fields": {"model": "courseaccessrulehistory", "name": "course access rule history", "app_label": "embargo"}}, {"pk": 69, "model": "contenttypes.contenttype", "fields": {"model": "courseauthorization", "name": "course authorization", "app_label": "bulk_email"}}, {"pk": 65, "model": "contenttypes.contenttype", "fields": {"model": "coursecohort", "name": "course cohort", "app_label": "course_groups"}}, {"pk": 64, "model": "contenttypes.contenttype", "fields": {"model": "coursecohortssettings", "name": "course cohorts settings", "app_label": "course_groups"}}, {"pk": 165, "model": "contenttypes.contenttype", "fields": {"model": "coursecontentmilestone", "name": "course content milestone", "app_label": "milestones"}}, {"pk": 168, "model": "contenttypes.contenttype", "fields": {"model": "coursecreator", "name": "course creator", "app_label": "course_creators"}}, {"pk": 66, "model": "contenttypes.contenttype", "fields": {"model": "courseemail", "name": "course email", "app_label": "bulk_email"}}, {"pk": 68, "model": "contenttypes.contenttype", "fields": {"model": "courseemailtemplate", "name": "course email template", "app_label": "bulk_email"}}, {"pk": 44, "model": "contenttypes.contenttype", "fields": {"model": "courseenrollment", "name": "course enrollment", "app_label": "student"}}, {"pk": 45, "model": "contenttypes.contenttype", "fields": {"model": "courseenrollmentallowed", "name": "course enrollment allowed", "app_label": "student"}}, {"pk": 164, "model": "contenttypes.contenttype", "fields": {"model": "coursemilestone", "name": "course milestone", "app_label": "milestones"}}, {"pk": 116, "model": "contenttypes.contenttype", "fields": {"model": "coursemode", "name": "course mode", "app_label": "course_modes"}}, {"pk": 117, "model": "contenttypes.contenttype", "fields": {"model": "coursemodesarchive", "name": "course modes archive", "app_label": "course_modes"}}, {"pk": 110, "model": "contenttypes.contenttype", "fields": {"model": "courseregcodeitem", "name": "course reg code item", "app_label": "shoppingcart"}}, {"pk": 111, "model": "contenttypes.contenttype", "fields": {"model": "courseregcodeitemannotation", "name": "course reg code item annotation", "app_label": "shoppingcart"}}, {"pk": 105, "model": "contenttypes.contenttype", "fields": {"model": "courseregistrationcode", "name": "course registration code", "app_label": "shoppingcart"}}, {"pk": 103, "model": "contenttypes.contenttype", "fields": {"model": "courseregistrationcodeinvoiceitem", "name": "course registration code invoice item", "app_label": "shoppingcart"}}, {"pk": 128, "model": "contenttypes.contenttype", "fields": {"model": "coursererunstate", "name": "course rerun state", "app_label": "course_action_state"}}, {"pk": 60, "model": "contenttypes.contenttype", "fields": {"model": "coursesoftware", "name": "course software", "app_label": "licenses"}}, {"pk": 133, "model": "contenttypes.contenttype", "fields": {"model": "coursestructure", "name": "course structure", "app_label": "course_structures"}}, {"pk": 62, "model": "contenttypes.contenttype", "fields": {"model": "courseusergroup", "name": "course user group", "app_label": "course_groups"}}, {"pk": 63, "model": "contenttypes.contenttype", "fields": {"model": "courseusergrouppartitiongroup", "name": "course user group partition group", "app_label": "course_groups"}}, {"pk": 159, "model": "contenttypes.contenttype", "fields": {"model": "coursevideo", "name": "course video", "app_label": "edxval"}}, {"pk": 139, "model": "contenttypes.contenttype", "fields": {"model": "criterion", "name": "criterion", "app_label": "assessment"}}, {"pk": 140, "model": "contenttypes.contenttype", "fields": {"model": "criterionoption", "name": "criterion option", "app_label": "assessment"}}, {"pk": 10, "model": "contenttypes.contenttype", "fields": {"model": "crontabschedule", "name": "crontab", "app_label": "djcelery"}}, {"pk": 119, "model": "contenttypes.contenttype", "fields": {"model": "darklangconfig", "name": "dark lang config", "app_label": "dark_lang"}}, {"pk": 47, "model": "contenttypes.contenttype", "fields": {"model": "dashboardconfiguration", "name": "dashboard configuration", "app_label": "student"}}, {"pk": 115, "model": "contenttypes.contenttype", "fields": {"model": "donation", "name": "donation", "app_label": "shoppingcart"}}, {"pk": 114, "model": "contenttypes.contenttype", "fields": {"model": "donationconfiguration", "name": "donation configuration", "app_label": "shoppingcart"}}, {"pk": 121, "model": "contenttypes.contenttype", "fields": {"model": "embargoedcourse", "name": "embargoed course", "app_label": "embargo"}}, {"pk": 122, "model": "contenttypes.contenttype", "fields": {"model": "embargoedstate", "name": "embargoed state", "app_label": "embargo"}}, {"pk": 160, "model": "contenttypes.contenttype", "fields": {"model": "encodedvideo", "name": "encoded video", "app_label": "edxval"}}, {"pk": 49, "model": "contenttypes.contenttype", "fields": {"model": "entranceexamconfiguration", "name": "entrance exam configuration", "app_label": "student"}}, {"pk": 55, "model": "contenttypes.contenttype", "fields": {"model": "examplecertificate", "name": "example certificate", "app_label": "certificates"}}, {"pk": 54, "model": "contenttypes.contenttype", "fields": {"model": "examplecertificateset", "name": "example certificate set", "app_label": "certificates"}}, {"pk": 71, "model": "contenttypes.contenttype", "fields": {"model": "externalauthmap", "name": "external auth map", "app_label": "external_auth"}}, {"pk": 53, "model": "contenttypes.contenttype", "fields": {"model": "generatedcertificate", "name": "generated certificate", "app_label": "certificates"}}, {"pk": 73, "model": "contenttypes.contenttype", "fields": {"model": "grant", "name": "grant", "app_label": "oauth2"}}, {"pk": 2, "model": "contenttypes.contenttype", "fields": {"model": "group", "name": "group", "app_label": "auth"}}, {"pk": 59, "model": "contenttypes.contenttype", "fields": {"model": "instructortask", "name": "instructor task", "app_label": "instructor_task"}}, {"pk": 9, "model": "contenttypes.contenttype", "fields": {"model": "intervalschedule", "name": "interval", "app_label": "djcelery"}}, {"pk": 100, "model": "contenttypes.contenttype", "fields": {"model": "invoice", "name": "invoice", "app_label": "shoppingcart"}}, {"pk": 104, "model": "contenttypes.contenttype", "fields": {"model": "invoicehistory", "name": "invoice history", "app_label": "shoppingcart"}}, {"pk": 102, "model": "contenttypes.contenttype", "fields": {"model": "invoiceitem", "name": "invoice item", "app_label": "shoppingcart"}}, {"pk": 101, "model": "contenttypes.contenttype", "fields": {"model": "invoicetransaction", "name": "invoice transaction", "app_label": "shoppingcart"}}, {"pk": 127, "model": "contenttypes.contenttype", "fields": {"model": "ipfilter", "name": "ip filter", "app_label": "embargo"}}, {"pk": 48, "model": "contenttypes.contenttype", "fields": {"model": "linkedinaddtoprofileconfiguration", "name": "linked in add to profile configuration", "app_label": "student"}}, {"pk": 21, "model": "contenttypes.contenttype", "fields": {"model": "logentry", "name": "log entry", "app_label": "admin"}}, {"pk": 43, "model": "contenttypes.contenttype", "fields": {"model": "loginfailures", "name": "login failures", "app_label": "student"}}, {"pk": 120, "model": "contenttypes.contenttype", "fields": {"model": "midcoursereverificationwindow", "name": "midcourse reverification window", "app_label": "reverification"}}, {"pk": 15, "model": "contenttypes.contenttype", "fields": {"model": "migrationhistory", "name": "migration history", "app_label": "south"}}, {"pk": 162, "model": "contenttypes.contenttype", "fields": {"model": "milestone", "name": "milestone", "app_label": "milestones"}}, {"pk": 163, "model": "contenttypes.contenttype", "fields": {"model": "milestonerelationshiptype", "name": "milestone relationship type", "app_label": "milestones"}}, {"pk": 129, "model": "contenttypes.contenttype", "fields": {"model": "mobileapiconfig", "name": "mobile api config", "app_label": "mobile_api"}}, {"pk": 18, "model": "contenttypes.contenttype", "fields": {"model": "nonce", "name": "nonce", "app_label": "django_openid_auth"}}, {"pk": 24, "model": "contenttypes.contenttype", "fields": {"model": "nonce", "name": "nonce", "app_label": "default"}}, {"pk": 93, "model": "contenttypes.contenttype", "fields": {"model": "note", "name": "note", "app_label": "notes"}}, {"pk": 90, "model": "contenttypes.contenttype", "fields": {"model": "notification", "name": "notification", "app_label": "django_notify"}}, {"pk": 32, "model": "contenttypes.contenttype", "fields": {"model": "offlinecomputedgrade", "name": "offline computed grade", "app_label": "courseware"}}, {"pk": 33, "model": "contenttypes.contenttype", "fields": {"model": "offlinecomputedgradelog", "name": "offline computed grade log", "app_label": "courseware"}}, {"pk": 67, "model": "contenttypes.contenttype", "fields": {"model": "optout", "name": "optout", "app_label": "bulk_email"}}, {"pk": 98, "model": "contenttypes.contenttype", "fields": {"model": "order", "name": "order", "app_label": "shoppingcart"}}, {"pk": 99, "model": "contenttypes.contenttype", "fields": {"model": "orderitem", "name": "order item", "app_label": "shoppingcart"}}, {"pk": 109, "model": "contenttypes.contenttype", "fields": {"model": "paidcourseregistration", "name": "paid course registration", "app_label": "shoppingcart"}}, {"pk": 112, "model": "contenttypes.contenttype", "fields": {"model": "paidcourseregistrationannotation", "name": "paid course registration annotation", "app_label": "shoppingcart"}}, {"pk": 42, "model": "contenttypes.contenttype", "fields": {"model": "passwordhistory", "name": "password history", "app_label": "student"}}, {"pk": 145, "model": "contenttypes.contenttype", "fields": {"model": "peerworkflow", "name": "peer workflow", "app_label": "assessment"}}, {"pk": 146, "model": "contenttypes.contenttype", "fields": {"model": "peerworkflowitem", "name": "peer workflow item", "app_label": "assessment"}}, {"pk": 41, "model": "contenttypes.contenttype", "fields": {"model": "pendingemailchange", "name": "pending email change", "app_label": "student"}}, {"pk": 40, "model": "contenttypes.contenttype", "fields": {"model": "pendingnamechange", "name": "pending name change", "app_label": "student"}}, {"pk": 12, "model": "contenttypes.contenttype", "fields": {"model": "periodictask", "name": "periodic task", "app_label": "djcelery"}}, {"pk": 11, "model": "contenttypes.contenttype", "fields": {"model": "periodictasks", "name": "periodic tasks", "app_label": "djcelery"}}, {"pk": 1, "model": "contenttypes.contenttype", "fields": {"model": "permission", "name": "permission", "app_label": "auth"}}, {"pk": 157, "model": "contenttypes.contenttype", "fields": {"model": "profile", "name": "profile", "app_label": "edxval"}}, {"pk": 17, "model": "contenttypes.contenttype", "fields": {"model": "psychometricdata", "name": "psychometric data", "app_label": "psychometrics"}}, {"pk": 92, "model": "contenttypes.contenttype", "fields": {"model": "puzzlecomplete", "name": "puzzle complete", "app_label": "foldit"}}, {"pk": 51, "model": "contenttypes.contenttype", "fields": {"model": "ratelimitconfiguration", "name": "rate limit configuration", "app_label": "util"}}, {"pk": 75, "model": "contenttypes.contenttype", "fields": {"model": "refreshtoken", "name": "refresh token", "app_label": "oauth2"}}, {"pk": 39, "model": "contenttypes.contenttype", "fields": {"model": "registration", "name": "registration", "app_label": "student"}}, {"pk": 106, "model": "contenttypes.contenttype", "fields": {"model": "registrationcoderedemption", "name": "registration code redemption", "app_label": "shoppingcart"}}, {"pk": 123, "model": "contenttypes.contenttype", "fields": {"model": "restrictedcourse", "name": "restricted course", "app_label": "embargo"}}, {"pk": 82, "model": "contenttypes.contenttype", "fields": {"model": "reusableplugin", "name": "reusable plugin", "app_label": "wiki"}}, {"pk": 84, "model": "contenttypes.contenttype", "fields": {"model": "revisionplugin", "name": "revision plugin", "app_label": "wiki"}}, {"pk": 85, "model": "contenttypes.contenttype", "fields": {"model": "revisionpluginrevision", "name": "revision plugin revision", "app_label": "wiki"}}, {"pk": 138, "model": "contenttypes.contenttype", "fields": {"model": "rubric", "name": "rubric", "app_label": "assessment"}}, {"pk": 8, "model": "contenttypes.contenttype", "fields": {"model": "tasksetmeta", "name": "saved group result", "app_label": "djcelery"}}, {"pk": 91, "model": "contenttypes.contenttype", "fields": {"model": "score", "name": "score", "app_label": "foldit"}}, {"pk": 136, "model": "contenttypes.contenttype", "fields": {"model": "score", "name": "score", "app_label": "submissions"}}, {"pk": 137, "model": "contenttypes.contenttype", "fields": {"model": "scoresummary", "name": "score summary", "app_label": "submissions"}}, {"pk": 16, "model": "contenttypes.contenttype", "fields": {"model": "servercircuit", "name": "server circuit", "app_label": "circuit"}}, {"pk": 5, "model": "contenttypes.contenttype", "fields": {"model": "session", "name": "session", "app_label": "sessions"}}, {"pk": 88, "model": "contenttypes.contenttype", "fields": {"model": "settings", "name": "settings", "app_label": "django_notify"}}, {"pk": 83, "model": "contenttypes.contenttype", "fields": {"model": "simpleplugin", "name": "simple plugin", "app_label": "wiki"}}, {"pk": 6, "model": "contenttypes.contenttype", "fields": {"model": "site", "name": "site", "app_label": "sites"}}, {"pk": 118, "model": "contenttypes.contenttype", "fields": {"model": "softwaresecurephotoverification", "name": "software secure photo verification", "app_label": "verify_student"}}, {"pk": 94, "model": "contenttypes.contenttype", "fields": {"model": "splashconfig", "name": "splash config", "app_label": "splash"}}, {"pk": 134, "model": "contenttypes.contenttype", "fields": {"model": "studentitem", "name": "student item", "app_label": "submissions"}}, {"pk": 27, "model": "contenttypes.contenttype", "fields": {"model": "studentmodule", "name": "student module", "app_label": "courseware"}}, {"pk": 28, "model": "contenttypes.contenttype", "fields": {"model": "studentmodulehistory", "name": "student module history", "app_label": "courseware"}}, {"pk": 148, "model": "contenttypes.contenttype", "fields": {"model": "studenttrainingworkflow", "name": "student training workflow", "app_label": "assessment"}}, {"pk": 149, "model": "contenttypes.contenttype", "fields": {"model": "studenttrainingworkflowitem", "name": "student training workflow item", "app_label": "assessment"}}, {"pk": 169, "model": "contenttypes.contenttype", "fields": {"model": "studioconfig", "name": "studio config", "app_label": "xblock_config"}}, {"pk": 135, "model": "contenttypes.contenttype", "fields": {"model": "submission", "name": "submission", "app_label": "submissions"}}, {"pk": 89, "model": "contenttypes.contenttype", "fields": {"model": "subscription", "name": "subscription", "app_label": "django_notify"}}, {"pk": 161, "model": "contenttypes.contenttype", "fields": {"model": "subtitle", "name": "subtitle", "app_label": "edxval"}}, {"pk": 131, "model": "contenttypes.contenttype", "fields": {"model": "surveyanswer", "name": "survey answer", "app_label": "survey"}}, {"pk": 130, "model": "contenttypes.contenttype", "fields": {"model": "surveyform", "name": "survey form", "app_label": "survey"}}, {"pk": 14, "model": "contenttypes.contenttype", "fields": {"model": "taskstate", "name": "task", "app_label": "djcelery"}}, {"pk": 7, "model": "contenttypes.contenttype", "fields": {"model": "taskmeta", "name": "task state", "app_label": "djcelery"}}, {"pk": 50, "model": "contenttypes.contenttype", "fields": {"model": "trackinglog", "name": "tracking log", "app_label": "track"}}, {"pk": 147, "model": "contenttypes.contenttype", "fields": {"model": "trainingexample", "name": "training example", "app_label": "assessment"}}, {"pk": 76, "model": "contenttypes.contenttype", "fields": {"model": "trustedclient", "name": "trusted client", "app_label": "oauth2_provider"}}, {"pk": 87, "model": "contenttypes.contenttype", "fields": {"model": "notificationtype", "name": "type", "app_label": "django_notify"}}, {"pk": 80, "model": "contenttypes.contenttype", "fields": {"model": "urlpath", "name": "URL path", "app_label": "wiki"}}, {"pk": 3, "model": "contenttypes.contenttype", "fields": {"model": "user", "name": "user", "app_label": "auth"}}, {"pk": 96, "model": "contenttypes.contenttype", "fields": {"model": "usercoursetag", "name": "user course tag", "app_label": "user_api"}}, {"pk": 61, "model": "contenttypes.contenttype", "fields": {"model": "userlicense", "name": "user license", "app_label": "licenses"}}, {"pk": 166, "model": "contenttypes.contenttype", "fields": {"model": "usermilestone", "name": "user milestone", "app_label": "milestones"}}, {"pk": 20, "model": "contenttypes.contenttype", "fields": {"model": "useropenid", "name": "user open id", "app_label": "django_openid_auth"}}, {"pk": 97, "model": "contenttypes.contenttype", "fields": {"model": "userorgtag", "name": "user org tag", "app_label": "user_api"}}, {"pk": 95, "model": "contenttypes.contenttype", "fields": {"model": "userpreference", "name": "user preference", "app_label": "user_api"}}, {"pk": 36, "model": "contenttypes.contenttype", "fields": {"model": "userprofile", "name": "user profile", "app_label": "student"}}, {"pk": 37, "model": "contenttypes.contenttype", "fields": {"model": "usersignupsource", "name": "user signup source", "app_label": "student"}}, {"pk": 23, "model": "contenttypes.contenttype", "fields": {"model": "usersocialauth", "name": "user social auth", "app_label": "default"}}, {"pk": 35, "model": "contenttypes.contenttype", "fields": {"model": "userstanding", "name": "user standing", "app_label": "student"}}, {"pk": 38, "model": "contenttypes.contenttype", "fields": {"model": "usertestgroup", "name": "user test group", "app_label": "student"}}, {"pk": 158, "model": "contenttypes.contenttype", "fields": {"model": "video", "name": "video", "app_label": "edxval"}}, {"pk": 167, "model": "contenttypes.contenttype", "fields": {"model": "videouploadconfig", "name": "video upload config", "app_label": "contentstore"}}, {"pk": 13, "model": "contenttypes.contenttype", "fields": {"model": "workerstate", "name": "worker", "app_label": "djcelery"}}, {"pk": 132, "model": "contenttypes.contenttype", "fields": {"model": "xblockasidesconfig", "name": "x block asides config", "app_label": "lms_xblock"}}, {"pk": 31, "model": "contenttypes.contenttype", "fields": {"model": "xmodulestudentinfofield", "name": "x module student info field", "app_label": "courseware"}}, {"pk": 30, "model": "contenttypes.contenttype", "fields": {"model": "xmodulestudentprefsfield", "name": "x module student prefs field", "app_label": "courseware"}}, {"pk": 29, "model": "contenttypes.contenttype", "fields": {"model": "xmoduleuserstatesummaryfield", "name": "x module user state summary field", "app_label": "courseware"}}, {"pk": 1, "model": "sites.site", "fields": {"domain": "example.com", "name": "example.com"}}, {"pk": 1, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:39Z", "app_name": "courseware", "migration": "0001_initial"}}, {"pk": 2, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:39Z", "app_name": "courseware", "migration": "0002_add_indexes"}}, {"pk": 3, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:39Z", "app_name": "courseware", "migration": "0003_done_grade_cache"}}, {"pk": 4, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:39Z", "app_name": "courseware", "migration": "0004_add_field_studentmodule_course_id"}}, {"pk": 5, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:39Z", "app_name": "courseware", "migration": "0005_auto__add_offlinecomputedgrade__add_unique_offlinecomputedgrade_user_c"}}, {"pk": 6, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:39Z", "app_name": "courseware", "migration": "0006_create_student_module_history"}}, {"pk": 7, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:39Z", "app_name": "courseware", "migration": "0007_allow_null_version_in_history"}}, {"pk": 8, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:40Z", "app_name": "courseware", "migration": "0008_add_xmodule_storage"}}, {"pk": 9, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:40Z", "app_name": "courseware", "migration": "0009_add_field_default"}}, {"pk": 10, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:40Z", "app_name": "courseware", "migration": "0010_rename_xblock_field_content_to_user_state_summary"}}, {"pk": 11, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:40Z", "app_name": "student", "migration": "0001_initial"}}, {"pk": 12, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:40Z", "app_name": "student", "migration": "0002_text_to_varchar_and_indexes"}}, {"pk": 13, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:40Z", "app_name": "student", "migration": "0003_auto__add_usertestgroup"}}, {"pk": 14, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0004_add_email_index"}}, {"pk": 15, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0005_name_change"}}, {"pk": 16, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0006_expand_meta_field"}}, {"pk": 17, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0007_convert_to_utf8"}}, {"pk": 18, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0008__auto__add_courseregistration"}}, {"pk": 19, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0009_auto__del_courseregistration__add_courseenrollment"}}, {"pk": 20, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0010_auto__chg_field_courseenrollment_course_id"}}, {"pk": 21, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0011_auto__chg_field_courseenrollment_user__del_unique_courseenrollment_use"}}, {"pk": 22, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0012_auto__add_field_userprofile_gender__add_field_userprofile_date_of_birt"}}, {"pk": 23, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0013_auto__chg_field_userprofile_meta"}}, {"pk": 24, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0014_auto__del_courseenrollment"}}, {"pk": 25, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0015_auto__add_courseenrollment__add_unique_courseenrollment_user_course_id"}}, {"pk": 26, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0016_auto__add_field_courseenrollment_date__chg_field_userprofile_country"}}, {"pk": 27, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0017_rename_date_to_created"}}, {"pk": 28, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0018_auto"}}, {"pk": 29, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0019_create_approved_demographic_fields_fall_2012"}}, {"pk": 30, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0020_add_test_center_user"}}, {"pk": 31, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0021_remove_askbot"}}, {"pk": 32, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0022_auto__add_courseenrollmentallowed__add_unique_courseenrollmentallowed_"}}, {"pk": 33, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0023_add_test_center_registration"}}, {"pk": 34, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0024_add_allow_certificate"}}, {"pk": 35, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0025_auto__add_field_courseenrollmentallowed_auto_enroll"}}, {"pk": 36, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0026_auto__remove_index_student_testcenterregistration_accommodation_request"}}, {"pk": 37, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0027_add_active_flag_and_mode_to_courseware_enrollment"}}, {"pk": 38, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0028_auto__add_userstanding"}}, {"pk": 39, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0029_add_lookup_table_between_user_and_anonymous_student_id"}}, {"pk": 40, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0029_remove_pearson"}}, {"pk": 41, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0030_auto__chg_field_anonymoususerid_anonymous_user_id"}}, {"pk": 42, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0031_drop_student_anonymoususerid_temp_archive"}}, {"pk": 43, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0032_add_field_UserProfile_country_add_field_UserProfile_city"}}, {"pk": 44, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:43Z", "app_name": "student", "migration": "0032_auto__add_loginfailures"}}, {"pk": 45, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:43Z", "app_name": "student", "migration": "0033_auto__add_passwordhistory"}}, {"pk": 46, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:43Z", "app_name": "student", "migration": "0034_auto__add_courseaccessrole"}}, {"pk": 47, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:44Z", "app_name": "student", "migration": "0035_access_roles"}}, {"pk": 48, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:44Z", "app_name": "student", "migration": "0036_access_roles_orgless"}}, {"pk": 49, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:44Z", "app_name": "student", "migration": "0037_auto__add_courseregistrationcode"}}, {"pk": 50, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:44Z", "app_name": "student", "migration": "0038_auto__add_usersignupsource"}}, {"pk": 51, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:44Z", "app_name": "student", "migration": "0039_auto__del_courseregistrationcode"}}, {"pk": 52, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:44Z", "app_name": "student", "migration": "0040_auto__del_field_usersignupsource_user_id__add_field_usersignupsource_u"}}, {"pk": 53, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:44Z", "app_name": "student", "migration": "0041_add_dashboard_config"}}, {"pk": 54, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:44Z", "app_name": "student", "migration": "0042_grant_sales_admin_roles"}}, {"pk": 55, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:44Z", "app_name": "student", "migration": "0043_auto__add_linkedinaddtoprofileconfiguration"}}, {"pk": 56, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:44Z", "app_name": "student", "migration": "0044_linkedin_add_company_identifier"}}, {"pk": 57, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:45Z", "app_name": "student", "migration": "0045_add_trk_partner_to_linkedin_config"}}, {"pk": 58, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:45Z", "app_name": "student", "migration": "0046_auto__add_entranceexamconfiguration__add_unique_entranceexamconfigurat"}}, {"pk": 59, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:45Z", "app_name": "track", "migration": "0001_initial"}}, {"pk": 60, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:45Z", "app_name": "track", "migration": "0002_auto__add_field_trackinglog_host__chg_field_trackinglog_event_type__ch"}}, {"pk": 61, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:45Z", "app_name": "util", "migration": "0001_initial"}}, {"pk": 62, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:45Z", "app_name": "util", "migration": "0002_default_rate_limit_config"}}, {"pk": 63, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0001_added_generatedcertificates"}}, {"pk": 64, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0002_auto__add_field_generatedcertificate_download_url"}}, {"pk": 65, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0003_auto__add_field_generatedcertificate_enabled"}}, {"pk": 66, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0004_auto__add_field_generatedcertificate_graded_certificate_id__add_field_"}}, {"pk": 67, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0005_auto__add_field_generatedcertificate_name"}}, {"pk": 68, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0006_auto__chg_field_generatedcertificate_certificate_id"}}, {"pk": 69, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0007_auto__add_revokedcertificate"}}, {"pk": 70, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0008_auto__del_revokedcertificate__del_field_generatedcertificate_name__add"}}, {"pk": 71, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0009_auto__del_field_generatedcertificate_graded_download_url__del_field_ge"}}, {"pk": 72, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0010_auto__del_field_generatedcertificate_enabled__add_field_generatedcerti"}}, {"pk": 73, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0011_auto__del_field_generatedcertificate_certificate_id__add_field_generat"}}, {"pk": 74, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0012_auto__add_field_generatedcertificate_name__add_field_generatedcertific"}}, {"pk": 75, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0013_auto__add_field_generatedcertificate_error_reason"}}, {"pk": 76, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0014_adding_whitelist"}}, {"pk": 77, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:47Z", "app_name": "certificates", "migration": "0015_adding_mode_for_verified_certs"}}, {"pk": 78, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:47Z", "app_name": "certificates", "migration": "0016_change_course_key_fields"}}, {"pk": 79, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:47Z", "app_name": "certificates", "migration": "0017_auto__add_certificategenerationconfiguration"}}, {"pk": 80, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:47Z", "app_name": "certificates", "migration": "0018_add_example_cert_models"}}, {"pk": 81, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:47Z", "app_name": "certificates", "migration": "0019_auto__add_certificatehtmlviewconfiguration"}}, {"pk": 82, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:47Z", "app_name": "certificates", "migration": "0020_certificatehtmlviewconfiguration_data"}}, {"pk": 83, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:47Z", "app_name": "instructor_task", "migration": "0001_initial"}}, {"pk": 84, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:47Z", "app_name": "instructor_task", "migration": "0002_add_subtask_field"}}, {"pk": 85, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:48Z", "app_name": "licenses", "migration": "0001_initial"}}, {"pk": 86, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:48Z", "app_name": "course_groups", "migration": "0001_initial"}}, {"pk": 87, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:48Z", "app_name": "course_groups", "migration": "0002_add_model_CourseUserGroupPartitionGroup"}}, {"pk": 88, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:48Z", "app_name": "course_groups", "migration": "0003_auto__add_coursecohort__add_coursecohortssettings"}}, {"pk": 89, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:48Z", "app_name": "course_groups", "migration": "0004_auto__del_field_coursecohortssettings_cohorted_discussions__add_field_"}}, {"pk": 90, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:49Z", "app_name": "bulk_email", "migration": "0001_initial"}}, {"pk": 91, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:49Z", "app_name": "bulk_email", "migration": "0002_change_field_names"}}, {"pk": 92, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:49Z", "app_name": "bulk_email", "migration": "0003_add_optout_user"}}, {"pk": 93, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:49Z", "app_name": "bulk_email", "migration": "0004_migrate_optout_user"}}, {"pk": 94, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:49Z", "app_name": "bulk_email", "migration": "0005_remove_optout_email"}}, {"pk": 95, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:49Z", "app_name": "bulk_email", "migration": "0006_add_course_email_template"}}, {"pk": 96, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:49Z", "app_name": "bulk_email", "migration": "0007_load_course_email_template"}}, {"pk": 97, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:49Z", "app_name": "bulk_email", "migration": "0008_add_course_authorizations"}}, {"pk": 98, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:49Z", "app_name": "bulk_email", "migration": "0009_force_unique_course_ids"}}, {"pk": 99, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:49Z", "app_name": "bulk_email", "migration": "0010_auto__chg_field_optout_course_id__add_field_courseemail_template_name_"}}, {"pk": 100, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:50Z", "app_name": "branding", "migration": "0001_initial"}}, {"pk": 101, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:50Z", "app_name": "external_auth", "migration": "0001_initial"}}, {"pk": 102, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:50Z", "app_name": "oauth2", "migration": "0001_initial"}}, {"pk": 103, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:50Z", "app_name": "oauth2", "migration": "0002_auto__chg_field_client_user"}}, {"pk": 104, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:51Z", "app_name": "oauth2", "migration": "0003_auto__add_field_client_name"}}, {"pk": 105, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:51Z", "app_name": "oauth2", "migration": "0004_auto__add_index_accesstoken_token"}}, {"pk": 106, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:51Z", "app_name": "oauth2_provider", "migration": "0001_initial"}}, {"pk": 107, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:52Z", "app_name": "wiki", "migration": "0001_initial"}}, {"pk": 108, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:52Z", "app_name": "wiki", "migration": "0002_auto__add_field_articleplugin_created"}}, {"pk": 109, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:52Z", "app_name": "wiki", "migration": "0003_auto__add_field_urlpath_article"}}, {"pk": 110, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:52Z", "app_name": "wiki", "migration": "0004_populate_urlpath__article"}}, {"pk": 111, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:52Z", "app_name": "wiki", "migration": "0005_auto__chg_field_urlpath_article"}}, {"pk": 112, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:52Z", "app_name": "wiki", "migration": "0006_auto__add_attachmentrevision__add_image__add_attachment"}}, {"pk": 113, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:52Z", "app_name": "wiki", "migration": "0007_auto__add_articlesubscription"}}, {"pk": 114, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:52Z", "app_name": "wiki", "migration": "0008_auto__add_simpleplugin__add_revisionpluginrevision__add_imagerevision_"}}, {"pk": 115, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:52Z", "app_name": "wiki", "migration": "0009_auto__add_field_imagerevision_width__add_field_imagerevision_height"}}, {"pk": 116, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:53Z", "app_name": "wiki", "migration": "0010_auto__chg_field_imagerevision_image"}}, {"pk": 117, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:53Z", "app_name": "wiki", "migration": "0011_auto__chg_field_imagerevision_width__chg_field_imagerevision_height"}}, {"pk": 118, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:53Z", "app_name": "django_notify", "migration": "0001_initial"}}, {"pk": 119, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:53Z", "app_name": "notifications", "migration": "0001_initial"}}, {"pk": 120, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:54Z", "app_name": "foldit", "migration": "0001_initial"}}, {"pk": 121, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:54Z", "app_name": "django_comment_client", "migration": "0001_initial"}}, {"pk": 122, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:54Z", "app_name": "django_comment_common", "migration": "0001_initial"}}, {"pk": 123, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:55Z", "app_name": "notes", "migration": "0001_initial"}}, {"pk": 124, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:55Z", "app_name": "splash", "migration": "0001_initial"}}, {"pk": 125, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:55Z", "app_name": "splash", "migration": "0002_auto__add_field_splashconfig_unaffected_url_paths"}}, {"pk": 126, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:56Z", "app_name": "user_api", "migration": "0001_initial"}}, {"pk": 127, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:56Z", "app_name": "user_api", "migration": "0002_auto__add_usercoursetags__add_unique_usercoursetags_user_course_id_key"}}, {"pk": 128, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:56Z", "app_name": "user_api", "migration": "0003_rename_usercoursetags"}}, {"pk": 129, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:56Z", "app_name": "user_api", "migration": "0004_auto__add_userorgtag__add_unique_userorgtag_user_org_key__chg_field_us"}}, {"pk": 130, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:56Z", "app_name": "shoppingcart", "migration": "0001_initial"}}, {"pk": 131, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:56Z", "app_name": "shoppingcart", "migration": "0002_auto__add_field_paidcourseregistration_mode"}}, {"pk": 132, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:56Z", "app_name": "shoppingcart", "migration": "0003_auto__del_field_orderitem_line_cost"}}, {"pk": 133, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:56Z", "app_name": "shoppingcart", "migration": "0004_auto__add_field_orderitem_fulfilled_time"}}, {"pk": 134, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:57Z", "app_name": "shoppingcart", "migration": "0005_auto__add_paidcourseregistrationannotation__add_field_orderitem_report"}}, {"pk": 135, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:57Z", "app_name": "shoppingcart", "migration": "0006_auto__add_field_order_refunded_time__add_field_orderitem_refund_reques"}}, {"pk": 136, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:57Z", "app_name": "shoppingcart", "migration": "0007_auto__add_field_orderitem_service_fee"}}, {"pk": 137, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:57Z", "app_name": "shoppingcart", "migration": "0008_auto__add_coupons__add_couponredemption__chg_field_certificateitem_cou"}}, {"pk": 138, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:57Z", "app_name": "shoppingcart", "migration": "0009_auto__del_coupons__add_courseregistrationcode__add_coupon__chg_field_c"}}, {"pk": 139, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:57Z", "app_name": "shoppingcart", "migration": "0010_auto__add_registrationcoderedemption__del_field_courseregistrationcode"}}, {"pk": 140, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:57Z", "app_name": "shoppingcart", "migration": "0011_auto__add_invoice__add_field_courseregistrationcode_invoice"}}, {"pk": 141, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:58Z", "app_name": "shoppingcart", "migration": "0012_auto__del_field_courseregistrationcode_transaction_group_name__del_fie"}}, {"pk": 142, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:58Z", "app_name": "shoppingcart", "migration": "0013_auto__add_field_invoice_is_valid"}}, {"pk": 143, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:58Z", "app_name": "shoppingcart", "migration": "0014_auto__del_field_invoice_tax_id__add_field_invoice_address_line_1__add_"}}, {"pk": 144, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:58Z", "app_name": "shoppingcart", "migration": "0015_auto__del_field_invoice_purchase_order_number__del_field_invoice_compa"}}, {"pk": 145, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:58Z", "app_name": "shoppingcart", "migration": "0016_auto__del_field_invoice_company_email__del_field_invoice_company_refer"}}, {"pk": 146, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:58Z", "app_name": "shoppingcart", "migration": "0017_auto__add_field_courseregistrationcode_order__chg_field_registrationco"}}, {"pk": 147, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:58Z", "app_name": "shoppingcart", "migration": "0018_auto__add_donation"}}, {"pk": 148, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:58Z", "app_name": "shoppingcart", "migration": "0019_auto__add_donationconfiguration"}}, {"pk": 149, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:58Z", "app_name": "shoppingcart", "migration": "0020_auto__add_courseregcodeitem__add_courseregcodeitemannotation__add_fiel"}}, {"pk": 150, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:58Z", "app_name": "shoppingcart", "migration": "0021_auto__add_field_orderitem_created__add_field_orderitem_modified"}}, {"pk": 151, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:59Z", "app_name": "shoppingcart", "migration": "0022_auto__add_field_registrationcoderedemption_course_enrollment__add_fiel"}}, {"pk": 152, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:59Z", "app_name": "shoppingcart", "migration": "0023_auto__add_field_coupon_expiration_date"}}, {"pk": 153, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:59Z", "app_name": "shoppingcart", "migration": "0024_auto__add_field_courseregistrationcode_mode_slug"}}, {"pk": 154, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:59Z", "app_name": "shoppingcart", "migration": "0025_update_invoice_models"}}, {"pk": 155, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:59Z", "app_name": "shoppingcart", "migration": "0026_migrate_invoices"}}, {"pk": 156, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:59Z", "app_name": "shoppingcart", "migration": "0027_add_invoice_history"}}, {"pk": 157, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:00Z", "app_name": "course_modes", "migration": "0001_initial"}}, {"pk": 158, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:00Z", "app_name": "course_modes", "migration": "0002_auto__add_field_coursemode_currency"}}, {"pk": 159, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:00Z", "app_name": "course_modes", "migration": "0003_auto__add_unique_coursemode_course_id_currency_mode_slug"}}, {"pk": 160, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:00Z", "app_name": "course_modes", "migration": "0004_auto__add_field_coursemode_expiration_date"}}, {"pk": 161, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:00Z", "app_name": "course_modes", "migration": "0005_auto__add_field_coursemode_expiration_datetime"}}, {"pk": 162, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:00Z", "app_name": "course_modes", "migration": "0006_expiration_date_to_datetime"}}, {"pk": 163, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:00Z", "app_name": "course_modes", "migration": "0007_add_description"}}, {"pk": 164, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:00Z", "app_name": "course_modes", "migration": "0007_auto__add_coursemodesarchive__chg_field_coursemode_course_id"}}, {"pk": 165, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:00Z", "app_name": "course_modes", "migration": "0008_auto__del_field_coursemodesarchive_description__add_field_coursemode_s"}}, {"pk": 166, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:00Z", "app_name": "verify_student", "migration": "0001_initial"}}, {"pk": 167, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:00Z", "app_name": "verify_student", "migration": "0002_auto__add_field_softwaresecurephotoverification_window"}}, {"pk": 168, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:00Z", "app_name": "verify_student", "migration": "0003_auto__add_field_softwaresecurephotoverification_display"}}, {"pk": 169, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:01Z", "app_name": "dark_lang", "migration": "0001_initial"}}, {"pk": 170, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:01Z", "app_name": "dark_lang", "migration": "0002_enable_on_install"}}, {"pk": 171, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:01Z", "app_name": "reverification", "migration": "0001_initial"}}, {"pk": 172, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:01Z", "app_name": "embargo", "migration": "0001_initial"}}, {"pk": 173, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:02Z", "app_name": "embargo", "migration": "0002_add_country_access_models"}}, {"pk": 174, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:02Z", "app_name": "embargo", "migration": "0003_add_countries"}}, {"pk": 175, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:02Z", "app_name": "embargo", "migration": "0004_migrate_embargo_config"}}, {"pk": 176, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:02Z", "app_name": "embargo", "migration": "0005_add_courseaccessrulehistory"}}, {"pk": 177, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:03Z", "app_name": "course_action_state", "migration": "0001_initial"}}, {"pk": 178, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:03Z", "app_name": "course_action_state", "migration": "0002_add_rerun_display_name"}}, {"pk": 179, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:03Z", "app_name": "mobile_api", "migration": "0001_initial"}}, {"pk": 180, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:04Z", "app_name": "survey", "migration": "0001_initial"}}, {"pk": 181, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:04Z", "app_name": "lms_xblock", "migration": "0001_initial"}}, {"pk": 182, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:04Z", "app_name": "course_structures", "migration": "0001_initial"}}, {"pk": 183, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:05Z", "app_name": "submissions", "migration": "0001_initial"}}, {"pk": 184, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:05Z", "app_name": "submissions", "migration": "0002_auto__add_scoresummary"}}, {"pk": 185, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:05Z", "app_name": "submissions", "migration": "0003_auto__del_field_submission_answer__add_field_submission_raw_answer"}}, {"pk": 186, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:05Z", "app_name": "submissions", "migration": "0004_auto__add_field_score_reset"}}, {"pk": 187, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:06Z", "app_name": "assessment", "migration": "0001_initial"}}, {"pk": 188, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:06Z", "app_name": "assessment", "migration": "0002_auto__add_assessmentfeedbackoption__del_field_assessmentfeedback_feedb"}}, {"pk": 189, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:06Z", "app_name": "assessment", "migration": "0003_add_index_pw_course_item_student"}}, {"pk": 190, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:06Z", "app_name": "assessment", "migration": "0004_auto__add_field_peerworkflow_graded_count"}}, {"pk": 191, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:06Z", "app_name": "assessment", "migration": "0005_auto__del_field_peerworkflow_graded_count__add_field_peerworkflow_grad"}}, {"pk": 192, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:06Z", "app_name": "assessment", "migration": "0006_auto__add_field_assessmentpart_feedback"}}, {"pk": 193, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:06Z", "app_name": "assessment", "migration": "0007_auto__chg_field_assessmentpart_feedback"}}, {"pk": 194, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:06Z", "app_name": "assessment", "migration": "0008_student_training"}}, {"pk": 195, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:06Z", "app_name": "assessment", "migration": "0009_auto__add_unique_studenttrainingworkflowitem_order_num_workflow"}}, {"pk": 196, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:06Z", "app_name": "assessment", "migration": "0010_auto__add_unique_studenttrainingworkflow_submission_uuid"}}, {"pk": 197, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:07Z", "app_name": "assessment", "migration": "0011_ai_training"}}, {"pk": 198, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:07Z", "app_name": "assessment", "migration": "0012_move_algorithm_id_to_classifier_set"}}, {"pk": 199, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:07Z", "app_name": "assessment", "migration": "0013_auto__add_field_aigradingworkflow_essay_text"}}, {"pk": 200, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:07Z", "app_name": "assessment", "migration": "0014_auto__add_field_aitrainingworkflow_item_id__add_field_aitrainingworkfl"}}, {"pk": 201, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:07Z", "app_name": "assessment", "migration": "0015_auto__add_unique_aitrainingworkflow_uuid__add_unique_aigradingworkflow"}}, {"pk": 202, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:07Z", "app_name": "assessment", "migration": "0016_auto__add_field_aiclassifierset_course_id__add_field_aiclassifierset_i"}}, {"pk": 203, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:07Z", "app_name": "assessment", "migration": "0016_auto__add_field_rubric_structure_hash"}}, {"pk": 204, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:07Z", "app_name": "assessment", "migration": "0017_rubric_structure_hash"}}, {"pk": 205, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:07Z", "app_name": "assessment", "migration": "0018_auto__add_field_assessmentpart_criterion"}}, {"pk": 206, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:07Z", "app_name": "assessment", "migration": "0019_assessmentpart_criterion_field"}}, {"pk": 207, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:07Z", "app_name": "assessment", "migration": "0020_assessmentpart_criterion_not_null"}}, {"pk": 208, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:07Z", "app_name": "assessment", "migration": "0021_assessmentpart_option_nullable"}}, {"pk": 209, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:08Z", "app_name": "assessment", "migration": "0022__add_label_fields"}}, {"pk": 210, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:08Z", "app_name": "assessment", "migration": "0023_assign_criteria_and_option_labels"}}, {"pk": 211, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:08Z", "app_name": "assessment", "migration": "0024_auto__chg_field_assessmentpart_criterion"}}, {"pk": 212, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:08Z", "app_name": "assessment", "migration": "0025_auto__add_field_peerworkflow_cancelled_at"}}, {"pk": 213, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:08Z", "app_name": "workflow", "migration": "0001_initial"}}, {"pk": 214, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:08Z", "app_name": "workflow", "migration": "0002_auto__add_field_assessmentworkflow_course_id__add_field_assessmentwork"}}, {"pk": 215, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:08Z", "app_name": "workflow", "migration": "0003_auto__add_assessmentworkflowstep"}}, {"pk": 216, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:08Z", "app_name": "workflow", "migration": "0004_auto__add_assessmentworkflowcancellation"}}, {"pk": 217, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:09Z", "app_name": "edxval", "migration": "0001_initial"}}, {"pk": 218, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:09Z", "app_name": "edxval", "migration": "0002_default_profiles"}}, {"pk": 219, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:09Z", "app_name": "edxval", "migration": "0003_status_and_created_fields"}}, {"pk": 220, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:09Z", "app_name": "edxval", "migration": "0004_remove_profile_fields"}}, {"pk": 221, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:10Z", "app_name": "milestones", "migration": "0001_initial"}}, {"pk": 222, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:10Z", "app_name": "milestones", "migration": "0002_seed_relationship_types"}}, {"pk": 223, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:10Z", "app_name": "django_extensions", "migration": "0001_empty"}}, {"pk": 224, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:15Z", "app_name": "contentstore", "migration": "0001_initial"}}, {"pk": 225, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:15Z", "app_name": "contentstore", "migration": "0002_auto__del_field_videouploadconfig_status_whitelist"}}, {"pk": 226, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:15Z", "app_name": "course_creators", "migration": "0001_initial"}}, {"pk": 227, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:16Z", "app_name": "xblock_config", "migration": "0001_initial"}}, {"pk": 5, "model": "embargo.country", "fields": {"country": "AD"}}, {"pk": 233, "model": "embargo.country", "fields": {"country": "AE"}}, {"pk": 1, "model": "embargo.country", "fields": {"country": "AF"}}, {"pk": 9, "model": "embargo.country", "fields": {"country": "AG"}}, {"pk": 7, "model": "embargo.country", "fields": {"country": "AI"}}, {"pk": 2, "model": "embargo.country", "fields": {"country": "AL"}}, {"pk": 11, "model": "embargo.country", "fields": {"country": "AM"}}, {"pk": 6, "model": "embargo.country", "fields": {"country": "AO"}}, {"pk": 8, "model": "embargo.country", "fields": {"country": "AQ"}}, {"pk": 10, "model": "embargo.country", "fields": {"country": "AR"}}, {"pk": 4, "model": "embargo.country", "fields": {"country": "AS"}}, {"pk": 14, "model": "embargo.country", "fields": {"country": "AT"}}, {"pk": 13, "model": "embargo.country", "fields": {"country": "AU"}}, {"pk": 12, "model": "embargo.country", "fields": {"country": "AW"}}, {"pk": 249, "model": "embargo.country", "fields": {"country": "AX"}}, {"pk": 15, "model": "embargo.country", "fields": {"country": "AZ"}}, {"pk": 28, "model": "embargo.country", "fields": {"country": "BA"}}, {"pk": 19, "model": "embargo.country", "fields": {"country": "BB"}}, {"pk": 18, "model": "embargo.country", "fields": {"country": "BD"}}, {"pk": 21, "model": "embargo.country", "fields": {"country": "BE"}}, {"pk": 35, "model": "embargo.country", "fields": {"country": "BF"}}, {"pk": 34, "model": "embargo.country", "fields": {"country": "BG"}}, {"pk": 17, "model": "embargo.country", "fields": {"country": "BH"}}, {"pk": 36, "model": "embargo.country", "fields": {"country": "BI"}}, {"pk": 23, "model": "embargo.country", "fields": {"country": "BJ"}}, {"pk": 184, "model": "embargo.country", "fields": {"country": "BL"}}, {"pk": 24, "model": "embargo.country", "fields": {"country": "BM"}}, {"pk": 33, "model": "embargo.country", "fields": {"country": "BN"}}, {"pk": 26, "model": "embargo.country", "fields": {"country": "BO"}}, {"pk": 27, "model": "embargo.country", "fields": {"country": "BQ"}}, {"pk": 31, "model": "embargo.country", "fields": {"country": "BR"}}, {"pk": 16, "model": "embargo.country", "fields": {"country": "BS"}}, {"pk": 25, "model": "embargo.country", "fields": {"country": "BT"}}, {"pk": 30, "model": "embargo.country", "fields": {"country": "BV"}}, {"pk": 29, "model": "embargo.country", "fields": {"country": "BW"}}, {"pk": 20, "model": "embargo.country", "fields": {"country": "BY"}}, {"pk": 22, "model": "embargo.country", "fields": {"country": "BZ"}}, {"pk": 39, "model": "embargo.country", "fields": {"country": "CA"}}, {"pk": 47, "model": "embargo.country", "fields": {"country": "CC"}}, {"pk": 51, "model": "embargo.country", "fields": {"country": "CD"}}, {"pk": 42, "model": "embargo.country", "fields": {"country": "CF"}}, {"pk": 50, "model": "embargo.country", "fields": {"country": "CG"}}, {"pk": 215, "model": "embargo.country", "fields": {"country": "CH"}}, {"pk": 59, "model": "embargo.country", "fields": {"country": "CI"}}, {"pk": 52, "model": "embargo.country", "fields": {"country": "CK"}}, {"pk": 44, "model": "embargo.country", "fields": {"country": "CL"}}, {"pk": 38, "model": "embargo.country", "fields": {"country": "CM"}}, {"pk": 45, "model": "embargo.country", "fields": {"country": "CN"}}, {"pk": 48, "model": "embargo.country", "fields": {"country": "CO"}}, {"pk": 53, "model": "embargo.country", "fields": {"country": "CR"}}, {"pk": 55, "model": "embargo.country", "fields": {"country": "CU"}}, {"pk": 40, "model": "embargo.country", "fields": {"country": "CV"}}, {"pk": 56, "model": "embargo.country", "fields": {"country": "CW"}}, {"pk": 46, "model": "embargo.country", "fields": {"country": "CX"}}, {"pk": 57, "model": "embargo.country", "fields": {"country": "CY"}}, {"pk": 58, "model": "embargo.country", "fields": {"country": "CZ"}}, {"pk": 82, "model": "embargo.country", "fields": {"country": "DE"}}, {"pk": 61, "model": "embargo.country", "fields": {"country": "DJ"}}, {"pk": 60, "model": "embargo.country", "fields": {"country": "DK"}}, {"pk": 62, "model": "embargo.country", "fields": {"country": "DM"}}, {"pk": 63, "model": "embargo.country", "fields": {"country": "DO"}}, {"pk": 3, "model": "embargo.country", "fields": {"country": "DZ"}}, {"pk": 64, "model": "embargo.country", "fields": {"country": "EC"}}, {"pk": 69, "model": "embargo.country", "fields": {"country": "EE"}}, {"pk": 65, "model": "embargo.country", "fields": {"country": "EG"}}, {"pk": 245, "model": "embargo.country", "fields": {"country": "EH"}}, {"pk": 68, "model": "embargo.country", "fields": {"country": "ER"}}, {"pk": 208, "model": "embargo.country", "fields": {"country": "ES"}}, {"pk": 70, "model": "embargo.country", "fields": {"country": "ET"}}, {"pk": 74, "model": "embargo.country", "fields": {"country": "FI"}}, {"pk": 73, "model": "embargo.country", "fields": {"country": "FJ"}}, {"pk": 71, "model": "embargo.country", "fields": {"country": "FK"}}, {"pk": 144, "model": "embargo.country", "fields": {"country": "FM"}}, {"pk": 72, "model": "embargo.country", "fields": {"country": "FO"}}, {"pk": 75, "model": "embargo.country", "fields": {"country": "FR"}}, {"pk": 79, "model": "embargo.country", "fields": {"country": "GA"}}, {"pk": 234, "model": "embargo.country", "fields": {"country": "GB"}}, {"pk": 87, "model": "embargo.country", "fields": {"country": "GD"}}, {"pk": 81, "model": "embargo.country", "fields": {"country": "GE"}}, {"pk": 76, "model": "embargo.country", "fields": {"country": "GF"}}, {"pk": 91, "model": "embargo.country", "fields": {"country": "GG"}}, {"pk": 83, "model": "embargo.country", "fields": {"country": "GH"}}, {"pk": 84, "model": "embargo.country", "fields": {"country": "GI"}}, {"pk": 86, "model": "embargo.country", "fields": {"country": "GL"}}, {"pk": 80, "model": "embargo.country", "fields": {"country": "GM"}}, {"pk": 92, "model": "embargo.country", "fields": {"country": "GN"}}, {"pk": 88, "model": "embargo.country", "fields": {"country": "GP"}}, {"pk": 67, "model": "embargo.country", "fields": {"country": "GQ"}}, {"pk": 85, "model": "embargo.country", "fields": {"country": "GR"}}, {"pk": 206, "model": "embargo.country", "fields": {"country": "GS"}}, {"pk": 90, "model": "embargo.country", "fields": {"country": "GT"}}, {"pk": 89, "model": "embargo.country", "fields": {"country": "GU"}}, {"pk": 93, "model": "embargo.country", "fields": {"country": "GW"}}, {"pk": 94, "model": "embargo.country", "fields": {"country": "GY"}}, {"pk": 99, "model": "embargo.country", "fields": {"country": "HK"}}, {"pk": 96, "model": "embargo.country", "fields": {"country": "HM"}}, {"pk": 98, "model": "embargo.country", "fields": {"country": "HN"}}, {"pk": 54, "model": "embargo.country", "fields": {"country": "HR"}}, {"pk": 95, "model": "embargo.country", "fields": {"country": "HT"}}, {"pk": 100, "model": "embargo.country", "fields": {"country": "HU"}}, {"pk": 103, "model": "embargo.country", "fields": {"country": "ID"}}, {"pk": 106, "model": "embargo.country", "fields": {"country": "IE"}}, {"pk": 108, "model": "embargo.country", "fields": {"country": "IL"}}, {"pk": 107, "model": "embargo.country", "fields": {"country": "IM"}}, {"pk": 102, "model": "embargo.country", "fields": {"country": "IN"}}, {"pk": 32, "model": "embargo.country", "fields": {"country": "IO"}}, {"pk": 105, "model": "embargo.country", "fields": {"country": "IQ"}}, {"pk": 104, "model": "embargo.country", "fields": {"country": "IR"}}, {"pk": 101, "model": "embargo.country", "fields": {"country": "IS"}}, {"pk": 109, "model": "embargo.country", "fields": {"country": "IT"}}, {"pk": 112, "model": "embargo.country", "fields": {"country": "JE"}}, {"pk": 110, "model": "embargo.country", "fields": {"country": "JM"}}, {"pk": 113, "model": "embargo.country", "fields": {"country": "JO"}}, {"pk": 111, "model": "embargo.country", "fields": {"country": "JP"}}, {"pk": 115, "model": "embargo.country", "fields": {"country": "KE"}}, {"pk": 120, "model": "embargo.country", "fields": {"country": "KG"}}, {"pk": 37, "model": "embargo.country", "fields": {"country": "KH"}}, {"pk": 116, "model": "embargo.country", "fields": {"country": "KI"}}, {"pk": 49, "model": "embargo.country", "fields": {"country": "KM"}}, {"pk": 186, "model": "embargo.country", "fields": {"country": "KN"}}, {"pk": 117, "model": "embargo.country", "fields": {"country": "KP"}}, {"pk": 118, "model": "embargo.country", "fields": {"country": "KR"}}, {"pk": 119, "model": "embargo.country", "fields": {"country": "KW"}}, {"pk": 41, "model": "embargo.country", "fields": {"country": "KY"}}, {"pk": 114, "model": "embargo.country", "fields": {"country": "KZ"}}, {"pk": 121, "model": "embargo.country", "fields": {"country": "LA"}}, {"pk": 123, "model": "embargo.country", "fields": {"country": "LB"}}, {"pk": 187, "model": "embargo.country", "fields": {"country": "LC"}}, {"pk": 127, "model": "embargo.country", "fields": {"country": "LI"}}, {"pk": 209, "model": "embargo.country", "fields": {"country": "LK"}}, {"pk": 125, "model": "embargo.country", "fields": {"country": "LR"}}, {"pk": 124, "model": "embargo.country", "fields": {"country": "LS"}}, {"pk": 128, "model": "embargo.country", "fields": {"country": "LT"}}, {"pk": 129, "model": "embargo.country", "fields": {"country": "LU"}}, {"pk": 122, "model": "embargo.country", "fields": {"country": "LV"}}, {"pk": 126, "model": "embargo.country", "fields": {"country": "LY"}}, {"pk": 150, "model": "embargo.country", "fields": {"country": "MA"}}, {"pk": 146, "model": "embargo.country", "fields": {"country": "MC"}}, {"pk": 145, "model": "embargo.country", "fields": {"country": "MD"}}, {"pk": 148, "model": "embargo.country", "fields": {"country": "ME"}}, {"pk": 188, "model": "embargo.country", "fields": {"country": "MF"}}, {"pk": 132, "model": "embargo.country", "fields": {"country": "MG"}}, {"pk": 138, "model": "embargo.country", "fields": {"country": "MH"}}, {"pk": 131, "model": "embargo.country", "fields": {"country": "MK"}}, {"pk": 136, "model": "embargo.country", "fields": {"country": "ML"}}, {"pk": 152, "model": "embargo.country", "fields": {"country": "MM"}}, {"pk": 147, "model": "embargo.country", "fields": {"country": "MN"}}, {"pk": 130, "model": "embargo.country", "fields": {"country": "MO"}}, {"pk": 164, "model": "embargo.country", "fields": {"country": "MP"}}, {"pk": 139, "model": "embargo.country", "fields": {"country": "MQ"}}, {"pk": 140, "model": "embargo.country", "fields": {"country": "MR"}}, {"pk": 149, "model": "embargo.country", "fields": {"country": "MS"}}, {"pk": 137, "model": "embargo.country", "fields": {"country": "MT"}}, {"pk": 141, "model": "embargo.country", "fields": {"country": "MU"}}, {"pk": 135, "model": "embargo.country", "fields": {"country": "MV"}}, {"pk": 133, "model": "embargo.country", "fields": {"country": "MW"}}, {"pk": 143, "model": "embargo.country", "fields": {"country": "MX"}}, {"pk": 134, "model": "embargo.country", "fields": {"country": "MY"}}, {"pk": 151, "model": "embargo.country", "fields": {"country": "MZ"}}, {"pk": 153, "model": "embargo.country", "fields": {"country": "NA"}}, {"pk": 157, "model": "embargo.country", "fields": {"country": "NC"}}, {"pk": 160, "model": "embargo.country", "fields": {"country": "NE"}}, {"pk": 163, "model": "embargo.country", "fields": {"country": "NF"}}, {"pk": 161, "model": "embargo.country", "fields": {"country": "NG"}}, {"pk": 159, "model": "embargo.country", "fields": {"country": "NI"}}, {"pk": 156, "model": "embargo.country", "fields": {"country": "NL"}}, {"pk": 165, "model": "embargo.country", "fields": {"country": "NO"}}, {"pk": 155, "model": "embargo.country", "fields": {"country": "NP"}}, {"pk": 154, "model": "embargo.country", "fields": {"country": "NR"}}, {"pk": 162, "model": "embargo.country", "fields": {"country": "NU"}}, {"pk": 158, "model": "embargo.country", "fields": {"country": "NZ"}}, {"pk": 166, "model": "embargo.country", "fields": {"country": "OM"}}, {"pk": 170, "model": "embargo.country", "fields": {"country": "PA"}}, {"pk": 173, "model": "embargo.country", "fields": {"country": "PE"}}, {"pk": 77, "model": "embargo.country", "fields": {"country": "PF"}}, {"pk": 171, "model": "embargo.country", "fields": {"country": "PG"}}, {"pk": 174, "model": "embargo.country", "fields": {"country": "PH"}}, {"pk": 167, "model": "embargo.country", "fields": {"country": "PK"}}, {"pk": 176, "model": "embargo.country", "fields": {"country": "PL"}}, {"pk": 189, "model": "embargo.country", "fields": {"country": "PM"}}, {"pk": 175, "model": "embargo.country", "fields": {"country": "PN"}}, {"pk": 178, "model": "embargo.country", "fields": {"country": "PR"}}, {"pk": 169, "model": "embargo.country", "fields": {"country": "PS"}}, {"pk": 177, "model": "embargo.country", "fields": {"country": "PT"}}, {"pk": 168, "model": "embargo.country", "fields": {"country": "PW"}}, {"pk": 172, "model": "embargo.country", "fields": {"country": "PY"}}, {"pk": 179, "model": "embargo.country", "fields": {"country": "QA"}}, {"pk": 183, "model": "embargo.country", "fields": {"country": "RE"}}, {"pk": 180, "model": "embargo.country", "fields": {"country": "RO"}}, {"pk": 196, "model": "embargo.country", "fields": {"country": "RS"}}, {"pk": 181, "model": "embargo.country", "fields": {"country": "RU"}}, {"pk": 182, "model": "embargo.country", "fields": {"country": "RW"}}, {"pk": 194, "model": "embargo.country", "fields": {"country": "SA"}}, {"pk": 203, "model": "embargo.country", "fields": {"country": "SB"}}, {"pk": 197, "model": "embargo.country", "fields": {"country": "SC"}}, {"pk": 210, "model": "embargo.country", "fields": {"country": "SD"}}, {"pk": 214, "model": "embargo.country", "fields": {"country": "SE"}}, {"pk": 199, "model": "embargo.country", "fields": {"country": "SG"}}, {"pk": 185, "model": "embargo.country", "fields": {"country": "SH"}}, {"pk": 202, "model": "embargo.country", "fields": {"country": "SI"}}, {"pk": 212, "model": "embargo.country", "fields": {"country": "SJ"}}, {"pk": 201, "model": "embargo.country", "fields": {"country": "SK"}}, {"pk": 198, "model": "embargo.country", "fields": {"country": "SL"}}, {"pk": 192, "model": "embargo.country", "fields": {"country": "SM"}}, {"pk": 195, "model": "embargo.country", "fields": {"country": "SN"}}, {"pk": 204, "model": "embargo.country", "fields": {"country": "SO"}}, {"pk": 211, "model": "embargo.country", "fields": {"country": "SR"}}, {"pk": 207, "model": "embargo.country", "fields": {"country": "SS"}}, {"pk": 193, "model": "embargo.country", "fields": {"country": "ST"}}, {"pk": 66, "model": "embargo.country", "fields": {"country": "SV"}}, {"pk": 200, "model": "embargo.country", "fields": {"country": "SX"}}, {"pk": 216, "model": "embargo.country", "fields": {"country": "SY"}}, {"pk": 213, "model": "embargo.country", "fields": {"country": "SZ"}}, {"pk": 229, "model": "embargo.country", "fields": {"country": "TC"}}, {"pk": 43, "model": "embargo.country", "fields": {"country": "TD"}}, {"pk": 78, "model": "embargo.country", "fields": {"country": "TF"}}, {"pk": 222, "model": "embargo.country", "fields": {"country": "TG"}}, {"pk": 220, "model": "embargo.country", "fields": {"country": "TH"}}, {"pk": 218, "model": "embargo.country", "fields": {"country": "TJ"}}, {"pk": 223, "model": "embargo.country", "fields": {"country": "TK"}}, {"pk": 221, "model": "embargo.country", "fields": {"country": "TL"}}, {"pk": 228, "model": "embargo.country", "fields": {"country": "TM"}}, {"pk": 226, "model": "embargo.country", "fields": {"country": "TN"}}, {"pk": 224, "model": "embargo.country", "fields": {"country": "TO"}}, {"pk": 227, "model": "embargo.country", "fields": {"country": "TR"}}, {"pk": 225, "model": "embargo.country", "fields": {"country": "TT"}}, {"pk": 230, "model": "embargo.country", "fields": {"country": "TV"}}, {"pk": 217, "model": "embargo.country", "fields": {"country": "TW"}}, {"pk": 219, "model": "embargo.country", "fields": {"country": "TZ"}}, {"pk": 232, "model": "embargo.country", "fields": {"country": "UA"}}, {"pk": 231, "model": "embargo.country", "fields": {"country": "UG"}}, {"pk": 236, "model": "embargo.country", "fields": {"country": "UM"}}, {"pk": 235, "model": "embargo.country", "fields": {"country": "US"}}, {"pk": 237, "model": "embargo.country", "fields": {"country": "UY"}}, {"pk": 238, "model": "embargo.country", "fields": {"country": "UZ"}}, {"pk": 97, "model": "embargo.country", "fields": {"country": "VA"}}, {"pk": 190, "model": "embargo.country", "fields": {"country": "VC"}}, {"pk": 240, "model": "embargo.country", "fields": {"country": "VE"}}, {"pk": 242, "model": "embargo.country", "fields": {"country": "VG"}}, {"pk": 243, "model": "embargo.country", "fields": {"country": "VI"}}, {"pk": 241, "model": "embargo.country", "fields": {"country": "VN"}}, {"pk": 239, "model": "embargo.country", "fields": {"country": "VU"}}, {"pk": 244, "model": "embargo.country", "fields": {"country": "WF"}}, {"pk": 191, "model": "embargo.country", "fields": {"country": "WS"}}, {"pk": 246, "model": "embargo.country", "fields": {"country": "YE"}}, {"pk": 142, "model": "embargo.country", "fields": {"country": "YT"}}, {"pk": 205, "model": "embargo.country", "fields": {"country": "ZA"}}, {"pk": 247, "model": "embargo.country", "fields": {"country": "ZM"}}, {"pk": 248, "model": "embargo.country", "fields": {"country": "ZW"}}, {"pk": 1, "model": "edxval.profile", "fields": {"profile_name": "desktop_mp4"}}, {"pk": 2, "model": "edxval.profile", "fields": {"profile_name": "desktop_webm"}}, {"pk": 3, "model": "edxval.profile", "fields": {"profile_name": "mobile_high"}}, {"pk": 4, "model": "edxval.profile", "fields": {"profile_name": "mobile_low"}}, {"pk": 5, "model": "edxval.profile", "fields": {"profile_name": "youtube"}}, {"pk": 1, "model": "milestones.milestonerelationshiptype", "fields": {"active": true, "description": "Autogenerated milestone relationship type \"fulfills\"", "modified": "2015-03-31T06:26:10Z", "name": "fulfills", "created": "2015-03-31T06:26:10Z"}}, {"pk": 2, "model": "milestones.milestonerelationshiptype", "fields": {"active": true, "description": "Autogenerated milestone relationship type \"requires\"", "modified": "2015-03-31T06:26:10Z", "name": "requires", "created": "2015-03-31T06:26:10Z"}}, {"pk": 61, "model": "auth.permission", "fields": {"codename": "add_logentry", "name": "Can add log entry", "content_type": 21}}, {"pk": 62, "model": "auth.permission", "fields": {"codename": "change_logentry", "name": "Can change log entry", "content_type": 21}}, {"pk": 63, "model": "auth.permission", "fields": {"codename": "delete_logentry", "name": "Can delete log entry", "content_type": 21}}, {"pk": 454, "model": "auth.permission", "fields": {"codename": "add_aiclassifier", "name": "Can add ai classifier", "content_type": 151}}, {"pk": 455, "model": "auth.permission", "fields": {"codename": "change_aiclassifier", "name": "Can change ai classifier", "content_type": 151}}, {"pk": 456, "model": "auth.permission", "fields": {"codename": "delete_aiclassifier", "name": "Can delete ai classifier", "content_type": 151}}, {"pk": 451, "model": "auth.permission", "fields": {"codename": "add_aiclassifierset", "name": "Can add ai classifier set", "content_type": 150}}, {"pk": 452, "model": "auth.permission", "fields": {"codename": "change_aiclassifierset", "name": "Can change ai classifier set", "content_type": 150}}, {"pk": 453, "model": "auth.permission", "fields": {"codename": "delete_aiclassifierset", "name": "Can delete ai classifier set", "content_type": 150}}, {"pk": 460, "model": "auth.permission", "fields": {"codename": "add_aigradingworkflow", "name": "Can add ai grading workflow", "content_type": 153}}, {"pk": 461, "model": "auth.permission", "fields": {"codename": "change_aigradingworkflow", "name": "Can change ai grading workflow", "content_type": 153}}, {"pk": 462, "model": "auth.permission", "fields": {"codename": "delete_aigradingworkflow", "name": "Can delete ai grading workflow", "content_type": 153}}, {"pk": 457, "model": "auth.permission", "fields": {"codename": "add_aitrainingworkflow", "name": "Can add ai training workflow", "content_type": 152}}, {"pk": 458, "model": "auth.permission", "fields": {"codename": "change_aitrainingworkflow", "name": "Can change ai training workflow", "content_type": 152}}, {"pk": 459, "model": "auth.permission", "fields": {"codename": "delete_aitrainingworkflow", "name": "Can delete ai training workflow", "content_type": 152}}, {"pk": 424, "model": "auth.permission", "fields": {"codename": "add_assessment", "name": "Can add assessment", "content_type": 141}}, {"pk": 425, "model": "auth.permission", "fields": {"codename": "change_assessment", "name": "Can change assessment", "content_type": 141}}, {"pk": 426, "model": "auth.permission", "fields": {"codename": "delete_assessment", "name": "Can delete assessment", "content_type": 141}}, {"pk": 433, "model": "auth.permission", "fields": {"codename": "add_assessmentfeedback", "name": "Can add assessment feedback", "content_type": 144}}, {"pk": 434, "model": "auth.permission", "fields": {"codename": "change_assessmentfeedback", "name": "Can change assessment feedback", "content_type": 144}}, {"pk": 435, "model": "auth.permission", "fields": {"codename": "delete_assessmentfeedback", "name": "Can delete assessment feedback", "content_type": 144}}, {"pk": 430, "model": "auth.permission", "fields": {"codename": "add_assessmentfeedbackoption", "name": "Can add assessment feedback option", "content_type": 143}}, {"pk": 431, "model": "auth.permission", "fields": {"codename": "change_assessmentfeedbackoption", "name": "Can change assessment feedback option", "content_type": 143}}, {"pk": 432, "model": "auth.permission", "fields": {"codename": "delete_assessmentfeedbackoption", "name": "Can delete assessment feedback option", "content_type": 143}}, {"pk": 427, "model": "auth.permission", "fields": {"codename": "add_assessmentpart", "name": "Can add assessment part", "content_type": 142}}, {"pk": 428, "model": "auth.permission", "fields": {"codename": "change_assessmentpart", "name": "Can change assessment part", "content_type": 142}}, {"pk": 429, "model": "auth.permission", "fields": {"codename": "delete_assessmentpart", "name": "Can delete assessment part", "content_type": 142}}, {"pk": 418, "model": "auth.permission", "fields": {"codename": "add_criterion", "name": "Can add criterion", "content_type": 139}}, {"pk": 419, "model": "auth.permission", "fields": {"codename": "change_criterion", "name": "Can change criterion", "content_type": 139}}, {"pk": 420, "model": "auth.permission", "fields": {"codename": "delete_criterion", "name": "Can delete criterion", "content_type": 139}}, {"pk": 421, "model": "auth.permission", "fields": {"codename": "add_criterionoption", "name": "Can add criterion option", "content_type": 140}}, {"pk": 422, "model": "auth.permission", "fields": {"codename": "change_criterionoption", "name": "Can change criterion option", "content_type": 140}}, {"pk": 423, "model": "auth.permission", "fields": {"codename": "delete_criterionoption", "name": "Can delete criterion option", "content_type": 140}}, {"pk": 436, "model": "auth.permission", "fields": {"codename": "add_peerworkflow", "name": "Can add peer workflow", "content_type": 145}}, {"pk": 437, "model": "auth.permission", "fields": {"codename": "change_peerworkflow", "name": "Can change peer workflow", "content_type": 145}}, {"pk": 438, "model": "auth.permission", "fields": {"codename": "delete_peerworkflow", "name": "Can delete peer workflow", "content_type": 145}}, {"pk": 439, "model": "auth.permission", "fields": {"codename": "add_peerworkflowitem", "name": "Can add peer workflow item", "content_type": 146}}, {"pk": 440, "model": "auth.permission", "fields": {"codename": "change_peerworkflowitem", "name": "Can change peer workflow item", "content_type": 146}}, {"pk": 441, "model": "auth.permission", "fields": {"codename": "delete_peerworkflowitem", "name": "Can delete peer workflow item", "content_type": 146}}, {"pk": 415, "model": "auth.permission", "fields": {"codename": "add_rubric", "name": "Can add rubric", "content_type": 138}}, {"pk": 416, "model": "auth.permission", "fields": {"codename": "change_rubric", "name": "Can change rubric", "content_type": 138}}, {"pk": 417, "model": "auth.permission", "fields": {"codename": "delete_rubric", "name": "Can delete rubric", "content_type": 138}}, {"pk": 445, "model": "auth.permission", "fields": {"codename": "add_studenttrainingworkflow", "name": "Can add student training workflow", "content_type": 148}}, {"pk": 446, "model": "auth.permission", "fields": {"codename": "change_studenttrainingworkflow", "name": "Can change student training workflow", "content_type": 148}}, {"pk": 447, "model": "auth.permission", "fields": {"codename": "delete_studenttrainingworkflow", "name": "Can delete student training workflow", "content_type": 148}}, {"pk": 448, "model": "auth.permission", "fields": {"codename": "add_studenttrainingworkflowitem", "name": "Can add student training workflow item", "content_type": 149}}, {"pk": 449, "model": "auth.permission", "fields": {"codename": "change_studenttrainingworkflowitem", "name": "Can change student training workflow item", "content_type": 149}}, {"pk": 450, "model": "auth.permission", "fields": {"codename": "delete_studenttrainingworkflowitem", "name": "Can delete student training workflow item", "content_type": 149}}, {"pk": 442, "model": "auth.permission", "fields": {"codename": "add_trainingexample", "name": "Can add training example", "content_type": 147}}, {"pk": 443, "model": "auth.permission", "fields": {"codename": "change_trainingexample", "name": "Can change training example", "content_type": 147}}, {"pk": 444, "model": "auth.permission", "fields": {"codename": "delete_trainingexample", "name": "Can delete training example", "content_type": 147}}, {"pk": 4, "model": "auth.permission", "fields": {"codename": "add_group", "name": "Can add group", "content_type": 2}}, {"pk": 5, "model": "auth.permission", "fields": {"codename": "change_group", "name": "Can change group", "content_type": 2}}, {"pk": 6, "model": "auth.permission", "fields": {"codename": "delete_group", "name": "Can delete group", "content_type": 2}}, {"pk": 1, "model": "auth.permission", "fields": {"codename": "add_permission", "name": "Can add permission", "content_type": 1}}, {"pk": 2, "model": "auth.permission", "fields": {"codename": "change_permission", "name": "Can change permission", "content_type": 1}}, {"pk": 3, "model": "auth.permission", "fields": {"codename": "delete_permission", "name": "Can delete permission", "content_type": 1}}, {"pk": 7, "model": "auth.permission", "fields": {"codename": "add_user", "name": "Can add user", "content_type": 3}}, {"pk": 8, "model": "auth.permission", "fields": {"codename": "change_user", "name": "Can change user", "content_type": 3}}, {"pk": 9, "model": "auth.permission", "fields": {"codename": "delete_user", "name": "Can delete user", "content_type": 3}}, {"pk": 208, "model": "auth.permission", "fields": {"codename": "add_brandinginfoconfig", "name": "Can add branding info config", "content_type": 70}}, {"pk": 209, "model": "auth.permission", "fields": {"codename": "change_brandinginfoconfig", "name": "Can change branding info config", "content_type": 70}}, {"pk": 210, "model": "auth.permission", "fields": {"codename": "delete_brandinginfoconfig", "name": "Can delete branding info config", "content_type": 70}}, {"pk": 205, "model": "auth.permission", "fields": {"codename": "add_courseauthorization", "name": "Can add course authorization", "content_type": 69}}, {"pk": 206, "model": "auth.permission", "fields": {"codename": "change_courseauthorization", "name": "Can change course authorization", "content_type": 69}}, {"pk": 207, "model": "auth.permission", "fields": {"codename": "delete_courseauthorization", "name": "Can delete course authorization", "content_type": 69}}, {"pk": 196, "model": "auth.permission", "fields": {"codename": "add_courseemail", "name": "Can add course email", "content_type": 66}}, {"pk": 197, "model": "auth.permission", "fields": {"codename": "change_courseemail", "name": "Can change course email", "content_type": 66}}, {"pk": 198, "model": "auth.permission", "fields": {"codename": "delete_courseemail", "name": "Can delete course email", "content_type": 66}}, {"pk": 202, "model": "auth.permission", "fields": {"codename": "add_courseemailtemplate", "name": "Can add course email template", "content_type": 68}}, {"pk": 203, "model": "auth.permission", "fields": {"codename": "change_courseemailtemplate", "name": "Can change course email template", "content_type": 68}}, {"pk": 204, "model": "auth.permission", "fields": {"codename": "delete_courseemailtemplate", "name": "Can delete course email template", "content_type": 68}}, {"pk": 199, "model": "auth.permission", "fields": {"codename": "add_optout", "name": "Can add optout", "content_type": 67}}, {"pk": 200, "model": "auth.permission", "fields": {"codename": "change_optout", "name": "Can change optout", "content_type": 67}}, {"pk": 201, "model": "auth.permission", "fields": {"codename": "delete_optout", "name": "Can delete optout", "content_type": 67}}, {"pk": 169, "model": "auth.permission", "fields": {"codename": "add_certificategenerationconfiguration", "name": "Can add certificate generation configuration", "content_type": 57}}, {"pk": 170, "model": "auth.permission", "fields": {"codename": "change_certificategenerationconfiguration", "name": "Can change certificate generation configuration", "content_type": 57}}, {"pk": 171, "model": "auth.permission", "fields": {"codename": "delete_certificategenerationconfiguration", "name": "Can delete certificate generation configuration", "content_type": 57}}, {"pk": 166, "model": "auth.permission", "fields": {"codename": "add_certificategenerationcoursesetting", "name": "Can add certificate generation course setting", "content_type": 56}}, {"pk": 167, "model": "auth.permission", "fields": {"codename": "change_certificategenerationcoursesetting", "name": "Can change certificate generation course setting", "content_type": 56}}, {"pk": 168, "model": "auth.permission", "fields": {"codename": "delete_certificategenerationcoursesetting", "name": "Can delete certificate generation course setting", "content_type": 56}}, {"pk": 172, "model": "auth.permission", "fields": {"codename": "add_certificatehtmlviewconfiguration", "name": "Can add certificate html view configuration", "content_type": 58}}, {"pk": 173, "model": "auth.permission", "fields": {"codename": "change_certificatehtmlviewconfiguration", "name": "Can change certificate html view configuration", "content_type": 58}}, {"pk": 174, "model": "auth.permission", "fields": {"codename": "delete_certificatehtmlviewconfiguration", "name": "Can delete certificate html view configuration", "content_type": 58}}, {"pk": 154, "model": "auth.permission", "fields": {"codename": "add_certificatewhitelist", "name": "Can add certificate whitelist", "content_type": 52}}, {"pk": 155, "model": "auth.permission", "fields": {"codename": "change_certificatewhitelist", "name": "Can change certificate whitelist", "content_type": 52}}, {"pk": 156, "model": "auth.permission", "fields": {"codename": "delete_certificatewhitelist", "name": "Can delete certificate whitelist", "content_type": 52}}, {"pk": 163, "model": "auth.permission", "fields": {"codename": "add_examplecertificate", "name": "Can add example certificate", "content_type": 55}}, {"pk": 164, "model": "auth.permission", "fields": {"codename": "change_examplecertificate", "name": "Can change example certificate", "content_type": 55}}, {"pk": 165, "model": "auth.permission", "fields": {"codename": "delete_examplecertificate", "name": "Can delete example certificate", "content_type": 55}}, {"pk": 160, "model": "auth.permission", "fields": {"codename": "add_examplecertificateset", "name": "Can add example certificate set", "content_type": 54}}, {"pk": 161, "model": "auth.permission", "fields": {"codename": "change_examplecertificateset", "name": "Can change example certificate set", "content_type": 54}}, {"pk": 162, "model": "auth.permission", "fields": {"codename": "delete_examplecertificateset", "name": "Can delete example certificate set", "content_type": 54}}, {"pk": 157, "model": "auth.permission", "fields": {"codename": "add_generatedcertificate", "name": "Can add generated certificate", "content_type": 53}}, {"pk": 158, "model": "auth.permission", "fields": {"codename": "change_generatedcertificate", "name": "Can change generated certificate", "content_type": 53}}, {"pk": 159, "model": "auth.permission", "fields": {"codename": "delete_generatedcertificate", "name": "Can delete generated certificate", "content_type": 53}}, {"pk": 46, "model": "auth.permission", "fields": {"codename": "add_servercircuit", "name": "Can add server circuit", "content_type": 16}}, {"pk": 47, "model": "auth.permission", "fields": {"codename": "change_servercircuit", "name": "Can change server circuit", "content_type": 16}}, {"pk": 48, "model": "auth.permission", "fields": {"codename": "delete_servercircuit", "name": "Can delete server circuit", "content_type": 16}}, {"pk": 502, "model": "auth.permission", "fields": {"codename": "add_videouploadconfig", "name": "Can add video upload config", "content_type": 167}}, {"pk": 503, "model": "auth.permission", "fields": {"codename": "change_videouploadconfig", "name": "Can change video upload config", "content_type": 167}}, {"pk": 504, "model": "auth.permission", "fields": {"codename": "delete_videouploadconfig", "name": "Can delete video upload config", "content_type": 167}}, {"pk": 10, "model": "auth.permission", "fields": {"codename": "add_contenttype", "name": "Can add content type", "content_type": 4}}, {"pk": 11, "model": "auth.permission", "fields": {"codename": "change_contenttype", "name": "Can change content type", "content_type": 4}}, {"pk": 12, "model": "auth.permission", "fields": {"codename": "delete_contenttype", "name": "Can delete content type", "content_type": 4}}, {"pk": 64, "model": "auth.permission", "fields": {"codename": "add_corsmodel", "name": "Can add cors model", "content_type": 22}}, {"pk": 65, "model": "auth.permission", "fields": {"codename": "change_corsmodel", "name": "Can change cors model", "content_type": 22}}, {"pk": 66, "model": "auth.permission", "fields": {"codename": "delete_corsmodel", "name": "Can delete cors model", "content_type": 22}}, {"pk": 94, "model": "auth.permission", "fields": {"codename": "add_offlinecomputedgrade", "name": "Can add offline computed grade", "content_type": 32}}, {"pk": 95, "model": "auth.permission", "fields": {"codename": "change_offlinecomputedgrade", "name": "Can change offline computed grade", "content_type": 32}}, {"pk": 96, "model": "auth.permission", "fields": {"codename": "delete_offlinecomputedgrade", "name": "Can delete offline computed grade", "content_type": 32}}, {"pk": 97, "model": "auth.permission", "fields": {"codename": "add_offlinecomputedgradelog", "name": "Can add offline computed grade log", "content_type": 33}}, {"pk": 98, "model": "auth.permission", "fields": {"codename": "change_offlinecomputedgradelog", "name": "Can change offline computed grade log", "content_type": 33}}, {"pk": 99, "model": "auth.permission", "fields": {"codename": "delete_offlinecomputedgradelog", "name": "Can delete offline computed grade log", "content_type": 33}}, {"pk": 79, "model": "auth.permission", "fields": {"codename": "add_studentmodule", "name": "Can add student module", "content_type": 27}}, {"pk": 80, "model": "auth.permission", "fields": {"codename": "change_studentmodule", "name": "Can change student module", "content_type": 27}}, {"pk": 81, "model": "auth.permission", "fields": {"codename": "delete_studentmodule", "name": "Can delete student module", "content_type": 27}}, {"pk": 82, "model": "auth.permission", "fields": {"codename": "add_studentmodulehistory", "name": "Can add student module history", "content_type": 28}}, {"pk": 83, "model": "auth.permission", "fields": {"codename": "change_studentmodulehistory", "name": "Can change student module history", "content_type": 28}}, {"pk": 84, "model": "auth.permission", "fields": {"codename": "delete_studentmodulehistory", "name": "Can delete student module history", "content_type": 28}}, {"pk": 91, "model": "auth.permission", "fields": {"codename": "add_xmodulestudentinfofield", "name": "Can add x module student info field", "content_type": 31}}, {"pk": 92, "model": "auth.permission", "fields": {"codename": "change_xmodulestudentinfofield", "name": "Can change x module student info field", "content_type": 31}}, {"pk": 93, "model": "auth.permission", "fields": {"codename": "delete_xmodulestudentinfofield", "name": "Can delete x module student info field", "content_type": 31}}, {"pk": 88, "model": "auth.permission", "fields": {"codename": "add_xmodulestudentprefsfield", "name": "Can add x module student prefs field", "content_type": 30}}, {"pk": 89, "model": "auth.permission", "fields": {"codename": "change_xmodulestudentprefsfield", "name": "Can change x module student prefs field", "content_type": 30}}, {"pk": 90, "model": "auth.permission", "fields": {"codename": "delete_xmodulestudentprefsfield", "name": "Can delete x module student prefs field", "content_type": 30}}, {"pk": 85, "model": "auth.permission", "fields": {"codename": "add_xmoduleuserstatesummaryfield", "name": "Can add x module user state summary field", "content_type": 29}}, {"pk": 86, "model": "auth.permission", "fields": {"codename": "change_xmoduleuserstatesummaryfield", "name": "Can change x module user state summary field", "content_type": 29}}, {"pk": 87, "model": "auth.permission", "fields": {"codename": "delete_xmoduleuserstatesummaryfield", "name": "Can delete x module user state summary field", "content_type": 29}}, {"pk": 385, "model": "auth.permission", "fields": {"codename": "add_coursererunstate", "name": "Can add course rerun state", "content_type": 128}}, {"pk": 386, "model": "auth.permission", "fields": {"codename": "change_coursererunstate", "name": "Can change course rerun state", "content_type": 128}}, {"pk": 387, "model": "auth.permission", "fields": {"codename": "delete_coursererunstate", "name": "Can delete course rerun state", "content_type": 128}}, {"pk": 505, "model": "auth.permission", "fields": {"codename": "add_coursecreator", "name": "Can add course creator", "content_type": 168}}, {"pk": 506, "model": "auth.permission", "fields": {"codename": "change_coursecreator", "name": "Can change course creator", "content_type": 168}}, {"pk": 507, "model": "auth.permission", "fields": {"codename": "delete_coursecreator", "name": "Can delete course creator", "content_type": 168}}, {"pk": 193, "model": "auth.permission", "fields": {"codename": "add_coursecohort", "name": "Can add course cohort", "content_type": 65}}, {"pk": 194, "model": "auth.permission", "fields": {"codename": "change_coursecohort", "name": "Can change course cohort", "content_type": 65}}, {"pk": 195, "model": "auth.permission", "fields": {"codename": "delete_coursecohort", "name": "Can delete course cohort", "content_type": 65}}, {"pk": 190, "model": "auth.permission", "fields": {"codename": "add_coursecohortssettings", "name": "Can add course cohorts settings", "content_type": 64}}, {"pk": 191, "model": "auth.permission", "fields": {"codename": "change_coursecohortssettings", "name": "Can change course cohorts settings", "content_type": 64}}, {"pk": 192, "model": "auth.permission", "fields": {"codename": "delete_coursecohortssettings", "name": "Can delete course cohorts settings", "content_type": 64}}, {"pk": 184, "model": "auth.permission", "fields": {"codename": "add_courseusergroup", "name": "Can add course user group", "content_type": 62}}, {"pk": 185, "model": "auth.permission", "fields": {"codename": "change_courseusergroup", "name": "Can change course user group", "content_type": 62}}, {"pk": 186, "model": "auth.permission", "fields": {"codename": "delete_courseusergroup", "name": "Can delete course user group", "content_type": 62}}, {"pk": 187, "model": "auth.permission", "fields": {"codename": "add_courseusergrouppartitiongroup", "name": "Can add course user group partition group", "content_type": 63}}, {"pk": 188, "model": "auth.permission", "fields": {"codename": "change_courseusergrouppartitiongroup", "name": "Can change course user group partition group", "content_type": 63}}, {"pk": 189, "model": "auth.permission", "fields": {"codename": "delete_courseusergrouppartitiongroup", "name": "Can delete course user group partition group", "content_type": 63}}, {"pk": 349, "model": "auth.permission", "fields": {"codename": "add_coursemode", "name": "Can add course mode", "content_type": 116}}, {"pk": 350, "model": "auth.permission", "fields": {"codename": "change_coursemode", "name": "Can change course mode", "content_type": 116}}, {"pk": 351, "model": "auth.permission", "fields": {"codename": "delete_coursemode", "name": "Can delete course mode", "content_type": 116}}, {"pk": 352, "model": "auth.permission", "fields": {"codename": "add_coursemodesarchive", "name": "Can add course modes archive", "content_type": 117}}, {"pk": 353, "model": "auth.permission", "fields": {"codename": "change_coursemodesarchive", "name": "Can change course modes archive", "content_type": 117}}, {"pk": 354, "model": "auth.permission", "fields": {"codename": "delete_coursemodesarchive", "name": "Can delete course modes archive", "content_type": 117}}, {"pk": 400, "model": "auth.permission", "fields": {"codename": "add_coursestructure", "name": "Can add course structure", "content_type": 133}}, {"pk": 401, "model": "auth.permission", "fields": {"codename": "change_coursestructure", "name": "Can change course structure", "content_type": 133}}, {"pk": 402, "model": "auth.permission", "fields": {"codename": "delete_coursestructure", "name": "Can delete course structure", "content_type": 133}}, {"pk": 358, "model": "auth.permission", "fields": {"codename": "add_darklangconfig", "name": "Can add dark lang config", "content_type": 119}}, {"pk": 359, "model": "auth.permission", "fields": {"codename": "change_darklangconfig", "name": "Can change dark lang config", "content_type": 119}}, {"pk": 360, "model": "auth.permission", "fields": {"codename": "delete_darklangconfig", "name": "Can delete dark lang config", "content_type": 119}}, {"pk": 73, "model": "auth.permission", "fields": {"codename": "add_association", "name": "Can add association", "content_type": 25}}, {"pk": 74, "model": "auth.permission", "fields": {"codename": "change_association", "name": "Can change association", "content_type": 25}}, {"pk": 75, "model": "auth.permission", "fields": {"codename": "delete_association", "name": "Can delete association", "content_type": 25}}, {"pk": 76, "model": "auth.permission", "fields": {"codename": "add_code", "name": "Can add code", "content_type": 26}}, {"pk": 77, "model": "auth.permission", "fields": {"codename": "change_code", "name": "Can change code", "content_type": 26}}, {"pk": 78, "model": "auth.permission", "fields": {"codename": "delete_code", "name": "Can delete code", "content_type": 26}}, {"pk": 70, "model": "auth.permission", "fields": {"codename": "add_nonce", "name": "Can add nonce", "content_type": 24}}, {"pk": 71, "model": "auth.permission", "fields": {"codename": "change_nonce", "name": "Can change nonce", "content_type": 24}}, {"pk": 72, "model": "auth.permission", "fields": {"codename": "delete_nonce", "name": "Can delete nonce", "content_type": 24}}, {"pk": 67, "model": "auth.permission", "fields": {"codename": "add_usersocialauth", "name": "Can add user social auth", "content_type": 23}}, {"pk": 68, "model": "auth.permission", "fields": {"codename": "change_usersocialauth", "name": "Can change user social auth", "content_type": 23}}, {"pk": 69, "model": "auth.permission", "fields": {"codename": "delete_usersocialauth", "name": "Can delete user social auth", "content_type": 23}}, {"pk": 271, "model": "auth.permission", "fields": {"codename": "add_notification", "name": "Can add notification", "content_type": 90}}, {"pk": 272, "model": "auth.permission", "fields": {"codename": "change_notification", "name": "Can change notification", "content_type": 90}}, {"pk": 273, "model": "auth.permission", "fields": {"codename": "delete_notification", "name": "Can delete notification", "content_type": 90}}, {"pk": 262, "model": "auth.permission", "fields": {"codename": "add_notificationtype", "name": "Can add type", "content_type": 87}}, {"pk": 263, "model": "auth.permission", "fields": {"codename": "change_notificationtype", "name": "Can change type", "content_type": 87}}, {"pk": 264, "model": "auth.permission", "fields": {"codename": "delete_notificationtype", "name": "Can delete type", "content_type": 87}}, {"pk": 265, "model": "auth.permission", "fields": {"codename": "add_settings", "name": "Can add settings", "content_type": 88}}, {"pk": 266, "model": "auth.permission", "fields": {"codename": "change_settings", "name": "Can change settings", "content_type": 88}}, {"pk": 267, "model": "auth.permission", "fields": {"codename": "delete_settings", "name": "Can delete settings", "content_type": 88}}, {"pk": 268, "model": "auth.permission", "fields": {"codename": "add_subscription", "name": "Can add subscription", "content_type": 89}}, {"pk": 269, "model": "auth.permission", "fields": {"codename": "change_subscription", "name": "Can change subscription", "content_type": 89}}, {"pk": 270, "model": "auth.permission", "fields": {"codename": "delete_subscription", "name": "Can delete subscription", "content_type": 89}}, {"pk": 55, "model": "auth.permission", "fields": {"codename": "add_association", "name": "Can add association", "content_type": 19}}, {"pk": 56, "model": "auth.permission", "fields": {"codename": "change_association", "name": "Can change association", "content_type": 19}}, {"pk": 57, "model": "auth.permission", "fields": {"codename": "delete_association", "name": "Can delete association", "content_type": 19}}, {"pk": 52, "model": "auth.permission", "fields": {"codename": "add_nonce", "name": "Can add nonce", "content_type": 18}}, {"pk": 53, "model": "auth.permission", "fields": {"codename": "change_nonce", "name": "Can change nonce", "content_type": 18}}, {"pk": 54, "model": "auth.permission", "fields": {"codename": "delete_nonce", "name": "Can delete nonce", "content_type": 18}}, {"pk": 58, "model": "auth.permission", "fields": {"codename": "add_useropenid", "name": "Can add user open id", "content_type": 20}}, {"pk": 59, "model": "auth.permission", "fields": {"codename": "change_useropenid", "name": "Can change user open id", "content_type": 20}}, {"pk": 60, "model": "auth.permission", "fields": {"codename": "delete_useropenid", "name": "Can delete user open id", "content_type": 20}}, {"pk": 28, "model": "auth.permission", "fields": {"codename": "add_crontabschedule", "name": "Can add crontab", "content_type": 10}}, {"pk": 29, "model": "auth.permission", "fields": {"codename": "change_crontabschedule", "name": "Can change crontab", "content_type": 10}}, {"pk": 30, "model": "auth.permission", "fields": {"codename": "delete_crontabschedule", "name": "Can delete crontab", "content_type": 10}}, {"pk": 25, "model": "auth.permission", "fields": {"codename": "add_intervalschedule", "name": "Can add interval", "content_type": 9}}, {"pk": 26, "model": "auth.permission", "fields": {"codename": "change_intervalschedule", "name": "Can change interval", "content_type": 9}}, {"pk": 27, "model": "auth.permission", "fields": {"codename": "delete_intervalschedule", "name": "Can delete interval", "content_type": 9}}, {"pk": 34, "model": "auth.permission", "fields": {"codename": "add_periodictask", "name": "Can add periodic task", "content_type": 12}}, {"pk": 35, "model": "auth.permission", "fields": {"codename": "change_periodictask", "name": "Can change periodic task", "content_type": 12}}, {"pk": 36, "model": "auth.permission", "fields": {"codename": "delete_periodictask", "name": "Can delete periodic task", "content_type": 12}}, {"pk": 31, "model": "auth.permission", "fields": {"codename": "add_periodictasks", "name": "Can add periodic tasks", "content_type": 11}}, {"pk": 32, "model": "auth.permission", "fields": {"codename": "change_periodictasks", "name": "Can change periodic tasks", "content_type": 11}}, {"pk": 33, "model": "auth.permission", "fields": {"codename": "delete_periodictasks", "name": "Can delete periodic tasks", "content_type": 11}}, {"pk": 19, "model": "auth.permission", "fields": {"codename": "add_taskmeta", "name": "Can add task state", "content_type": 7}}, {"pk": 20, "model": "auth.permission", "fields": {"codename": "change_taskmeta", "name": "Can change task state", "content_type": 7}}, {"pk": 21, "model": "auth.permission", "fields": {"codename": "delete_taskmeta", "name": "Can delete task state", "content_type": 7}}, {"pk": 22, "model": "auth.permission", "fields": {"codename": "add_tasksetmeta", "name": "Can add saved group result", "content_type": 8}}, {"pk": 23, "model": "auth.permission", "fields": {"codename": "change_tasksetmeta", "name": "Can change saved group result", "content_type": 8}}, {"pk": 24, "model": "auth.permission", "fields": {"codename": "delete_tasksetmeta", "name": "Can delete saved group result", "content_type": 8}}, {"pk": 40, "model": "auth.permission", "fields": {"codename": "add_taskstate", "name": "Can add task", "content_type": 14}}, {"pk": 41, "model": "auth.permission", "fields": {"codename": "change_taskstate", "name": "Can change task", "content_type": 14}}, {"pk": 42, "model": "auth.permission", "fields": {"codename": "delete_taskstate", "name": "Can delete task", "content_type": 14}}, {"pk": 37, "model": "auth.permission", "fields": {"codename": "add_workerstate", "name": "Can add worker", "content_type": 13}}, {"pk": 38, "model": "auth.permission", "fields": {"codename": "change_workerstate", "name": "Can change worker", "content_type": 13}}, {"pk": 39, "model": "auth.permission", "fields": {"codename": "delete_workerstate", "name": "Can delete worker", "content_type": 13}}, {"pk": 478, "model": "auth.permission", "fields": {"codename": "add_coursevideo", "name": "Can add course video", "content_type": 159}}, {"pk": 479, "model": "auth.permission", "fields": {"codename": "change_coursevideo", "name": "Can change course video", "content_type": 159}}, {"pk": 480, "model": "auth.permission", "fields": {"codename": "delete_coursevideo", "name": "Can delete course video", "content_type": 159}}, {"pk": 481, "model": "auth.permission", "fields": {"codename": "add_encodedvideo", "name": "Can add encoded video", "content_type": 160}}, {"pk": 482, "model": "auth.permission", "fields": {"codename": "change_encodedvideo", "name": "Can change encoded video", "content_type": 160}}, {"pk": 483, "model": "auth.permission", "fields": {"codename": "delete_encodedvideo", "name": "Can delete encoded video", "content_type": 160}}, {"pk": 472, "model": "auth.permission", "fields": {"codename": "add_profile", "name": "Can add profile", "content_type": 157}}, {"pk": 473, "model": "auth.permission", "fields": {"codename": "change_profile", "name": "Can change profile", "content_type": 157}}, {"pk": 474, "model": "auth.permission", "fields": {"codename": "delete_profile", "name": "Can delete profile", "content_type": 157}}, {"pk": 484, "model": "auth.permission", "fields": {"codename": "add_subtitle", "name": "Can add subtitle", "content_type": 161}}, {"pk": 485, "model": "auth.permission", "fields": {"codename": "change_subtitle", "name": "Can change subtitle", "content_type": 161}}, {"pk": 486, "model": "auth.permission", "fields": {"codename": "delete_subtitle", "name": "Can delete subtitle", "content_type": 161}}, {"pk": 475, "model": "auth.permission", "fields": {"codename": "add_video", "name": "Can add video", "content_type": 158}}, {"pk": 476, "model": "auth.permission", "fields": {"codename": "change_video", "name": "Can change video", "content_type": 158}}, {"pk": 477, "model": "auth.permission", "fields": {"codename": "delete_video", "name": "Can delete video", "content_type": 158}}, {"pk": 373, "model": "auth.permission", "fields": {"codename": "add_country", "name": "Can add country", "content_type": 124}}, {"pk": 374, "model": "auth.permission", "fields": {"codename": "change_country", "name": "Can change country", "content_type": 124}}, {"pk": 375, "model": "auth.permission", "fields": {"codename": "delete_country", "name": "Can delete country", "content_type": 124}}, {"pk": 376, "model": "auth.permission", "fields": {"codename": "add_countryaccessrule", "name": "Can add country access rule", "content_type": 125}}, {"pk": 377, "model": "auth.permission", "fields": {"codename": "change_countryaccessrule", "name": "Can change country access rule", "content_type": 125}}, {"pk": 378, "model": "auth.permission", "fields": {"codename": "delete_countryaccessrule", "name": "Can delete country access rule", "content_type": 125}}, {"pk": 379, "model": "auth.permission", "fields": {"codename": "add_courseaccessrulehistory", "name": "Can add course access rule history", "content_type": 126}}, {"pk": 380, "model": "auth.permission", "fields": {"codename": "change_courseaccessrulehistory", "name": "Can change course access rule history", "content_type": 126}}, {"pk": 381, "model": "auth.permission", "fields": {"codename": "delete_courseaccessrulehistory", "name": "Can delete course access rule history", "content_type": 126}}, {"pk": 364, "model": "auth.permission", "fields": {"codename": "add_embargoedcourse", "name": "Can add embargoed course", "content_type": 121}}, {"pk": 365, "model": "auth.permission", "fields": {"codename": "change_embargoedcourse", "name": "Can change embargoed course", "content_type": 121}}, {"pk": 366, "model": "auth.permission", "fields": {"codename": "delete_embargoedcourse", "name": "Can delete embargoed course", "content_type": 121}}, {"pk": 367, "model": "auth.permission", "fields": {"codename": "add_embargoedstate", "name": "Can add embargoed state", "content_type": 122}}, {"pk": 368, "model": "auth.permission", "fields": {"codename": "change_embargoedstate", "name": "Can change embargoed state", "content_type": 122}}, {"pk": 369, "model": "auth.permission", "fields": {"codename": "delete_embargoedstate", "name": "Can delete embargoed state", "content_type": 122}}, {"pk": 382, "model": "auth.permission", "fields": {"codename": "add_ipfilter", "name": "Can add ip filter", "content_type": 127}}, {"pk": 383, "model": "auth.permission", "fields": {"codename": "change_ipfilter", "name": "Can change ip filter", "content_type": 127}}, {"pk": 384, "model": "auth.permission", "fields": {"codename": "delete_ipfilter", "name": "Can delete ip filter", "content_type": 127}}, {"pk": 370, "model": "auth.permission", "fields": {"codename": "add_restrictedcourse", "name": "Can add restricted course", "content_type": 123}}, {"pk": 371, "model": "auth.permission", "fields": {"codename": "change_restrictedcourse", "name": "Can change restricted course", "content_type": 123}}, {"pk": 372, "model": "auth.permission", "fields": {"codename": "delete_restrictedcourse", "name": "Can delete restricted course", "content_type": 123}}, {"pk": 211, "model": "auth.permission", "fields": {"codename": "add_externalauthmap", "name": "Can add external auth map", "content_type": 71}}, {"pk": 212, "model": "auth.permission", "fields": {"codename": "change_externalauthmap", "name": "Can change external auth map", "content_type": 71}}, {"pk": 213, "model": "auth.permission", "fields": {"codename": "delete_externalauthmap", "name": "Can delete external auth map", "content_type": 71}}, {"pk": 277, "model": "auth.permission", "fields": {"codename": "add_puzzlecomplete", "name": "Can add puzzle complete", "content_type": 92}}, {"pk": 278, "model": "auth.permission", "fields": {"codename": "change_puzzlecomplete", "name": "Can change puzzle complete", "content_type": 92}}, {"pk": 279, "model": "auth.permission", "fields": {"codename": "delete_puzzlecomplete", "name": "Can delete puzzle complete", "content_type": 92}}, {"pk": 274, "model": "auth.permission", "fields": {"codename": "add_score", "name": "Can add score", "content_type": 91}}, {"pk": 275, "model": "auth.permission", "fields": {"codename": "change_score", "name": "Can change score", "content_type": 91}}, {"pk": 276, "model": "auth.permission", "fields": {"codename": "delete_score", "name": "Can delete score", "content_type": 91}}, {"pk": 175, "model": "auth.permission", "fields": {"codename": "add_instructortask", "name": "Can add instructor task", "content_type": 59}}, {"pk": 176, "model": "auth.permission", "fields": {"codename": "change_instructortask", "name": "Can change instructor task", "content_type": 59}}, {"pk": 177, "model": "auth.permission", "fields": {"codename": "delete_instructortask", "name": "Can delete instructor task", "content_type": 59}}, {"pk": 178, "model": "auth.permission", "fields": {"codename": "add_coursesoftware", "name": "Can add course software", "content_type": 60}}, {"pk": 179, "model": "auth.permission", "fields": {"codename": "change_coursesoftware", "name": "Can change course software", "content_type": 60}}, {"pk": 180, "model": "auth.permission", "fields": {"codename": "delete_coursesoftware", "name": "Can delete course software", "content_type": 60}}, {"pk": 181, "model": "auth.permission", "fields": {"codename": "add_userlicense", "name": "Can add user license", "content_type": 61}}, {"pk": 182, "model": "auth.permission", "fields": {"codename": "change_userlicense", "name": "Can change user license", "content_type": 61}}, {"pk": 183, "model": "auth.permission", "fields": {"codename": "delete_userlicense", "name": "Can delete user license", "content_type": 61}}, {"pk": 397, "model": "auth.permission", "fields": {"codename": "add_xblockasidesconfig", "name": "Can add x block asides config", "content_type": 132}}, {"pk": 398, "model": "auth.permission", "fields": {"codename": "change_xblockasidesconfig", "name": "Can change x block asides config", "content_type": 132}}, {"pk": 399, "model": "auth.permission", "fields": {"codename": "delete_xblockasidesconfig", "name": "Can delete x block asides config", "content_type": 132}}, {"pk": 496, "model": "auth.permission", "fields": {"codename": "add_coursecontentmilestone", "name": "Can add course content milestone", "content_type": 165}}, {"pk": 497, "model": "auth.permission", "fields": {"codename": "change_coursecontentmilestone", "name": "Can change course content milestone", "content_type": 165}}, {"pk": 498, "model": "auth.permission", "fields": {"codename": "delete_coursecontentmilestone", "name": "Can delete course content milestone", "content_type": 165}}, {"pk": 493, "model": "auth.permission", "fields": {"codename": "add_coursemilestone", "name": "Can add course milestone", "content_type": 164}}, {"pk": 494, "model": "auth.permission", "fields": {"codename": "change_coursemilestone", "name": "Can change course milestone", "content_type": 164}}, {"pk": 495, "model": "auth.permission", "fields": {"codename": "delete_coursemilestone", "name": "Can delete course milestone", "content_type": 164}}, {"pk": 487, "model": "auth.permission", "fields": {"codename": "add_milestone", "name": "Can add milestone", "content_type": 162}}, {"pk": 488, "model": "auth.permission", "fields": {"codename": "change_milestone", "name": "Can change milestone", "content_type": 162}}, {"pk": 489, "model": "auth.permission", "fields": {"codename": "delete_milestone", "name": "Can delete milestone", "content_type": 162}}, {"pk": 490, "model": "auth.permission", "fields": {"codename": "add_milestonerelationshiptype", "name": "Can add milestone relationship type", "content_type": 163}}, {"pk": 491, "model": "auth.permission", "fields": {"codename": "change_milestonerelationshiptype", "name": "Can change milestone relationship type", "content_type": 163}}, {"pk": 492, "model": "auth.permission", "fields": {"codename": "delete_milestonerelationshiptype", "name": "Can delete milestone relationship type", "content_type": 163}}, {"pk": 499, "model": "auth.permission", "fields": {"codename": "add_usermilestone", "name": "Can add user milestone", "content_type": 166}}, {"pk": 500, "model": "auth.permission", "fields": {"codename": "change_usermilestone", "name": "Can change user milestone", "content_type": 166}}, {"pk": 501, "model": "auth.permission", "fields": {"codename": "delete_usermilestone", "name": "Can delete user milestone", "content_type": 166}}, {"pk": 388, "model": "auth.permission", "fields": {"codename": "add_mobileapiconfig", "name": "Can add mobile api config", "content_type": 129}}, {"pk": 389, "model": "auth.permission", "fields": {"codename": "change_mobileapiconfig", "name": "Can change mobile api config", "content_type": 129}}, {"pk": 390, "model": "auth.permission", "fields": {"codename": "delete_mobileapiconfig", "name": "Can delete mobile api config", "content_type": 129}}, {"pk": 280, "model": "auth.permission", "fields": {"codename": "add_note", "name": "Can add note", "content_type": 93}}, {"pk": 281, "model": "auth.permission", "fields": {"codename": "change_note", "name": "Can change note", "content_type": 93}}, {"pk": 282, "model": "auth.permission", "fields": {"codename": "delete_note", "name": "Can delete note", "content_type": 93}}, {"pk": 220, "model": "auth.permission", "fields": {"codename": "add_accesstoken", "name": "Can add access token", "content_type": 74}}, {"pk": 221, "model": "auth.permission", "fields": {"codename": "change_accesstoken", "name": "Can change access token", "content_type": 74}}, {"pk": 222, "model": "auth.permission", "fields": {"codename": "delete_accesstoken", "name": "Can delete access token", "content_type": 74}}, {"pk": 214, "model": "auth.permission", "fields": {"codename": "add_client", "name": "Can add client", "content_type": 72}}, {"pk": 215, "model": "auth.permission", "fields": {"codename": "change_client", "name": "Can change client", "content_type": 72}}, {"pk": 216, "model": "auth.permission", "fields": {"codename": "delete_client", "name": "Can delete client", "content_type": 72}}, {"pk": 217, "model": "auth.permission", "fields": {"codename": "add_grant", "name": "Can add grant", "content_type": 73}}, {"pk": 218, "model": "auth.permission", "fields": {"codename": "change_grant", "name": "Can change grant", "content_type": 73}}, {"pk": 219, "model": "auth.permission", "fields": {"codename": "delete_grant", "name": "Can delete grant", "content_type": 73}}, {"pk": 223, "model": "auth.permission", "fields": {"codename": "add_refreshtoken", "name": "Can add refresh token", "content_type": 75}}, {"pk": 224, "model": "auth.permission", "fields": {"codename": "change_refreshtoken", "name": "Can change refresh token", "content_type": 75}}, {"pk": 225, "model": "auth.permission", "fields": {"codename": "delete_refreshtoken", "name": "Can delete refresh token", "content_type": 75}}, {"pk": 226, "model": "auth.permission", "fields": {"codename": "add_trustedclient", "name": "Can add trusted client", "content_type": 76}}, {"pk": 227, "model": "auth.permission", "fields": {"codename": "change_trustedclient", "name": "Can change trusted client", "content_type": 76}}, {"pk": 228, "model": "auth.permission", "fields": {"codename": "delete_trustedclient", "name": "Can delete trusted client", "content_type": 76}}, {"pk": 49, "model": "auth.permission", "fields": {"codename": "add_psychometricdata", "name": "Can add psychometric data", "content_type": 17}}, {"pk": 50, "model": "auth.permission", "fields": {"codename": "change_psychometricdata", "name": "Can change psychometric data", "content_type": 17}}, {"pk": 51, "model": "auth.permission", "fields": {"codename": "delete_psychometricdata", "name": "Can delete psychometric data", "content_type": 17}}, {"pk": 361, "model": "auth.permission", "fields": {"codename": "add_midcoursereverificationwindow", "name": "Can add midcourse reverification window", "content_type": 120}}, {"pk": 362, "model": "auth.permission", "fields": {"codename": "change_midcoursereverificationwindow", "name": "Can change midcourse reverification window", "content_type": 120}}, {"pk": 363, "model": "auth.permission", "fields": {"codename": "delete_midcoursereverificationwindow", "name": "Can delete midcourse reverification window", "content_type": 120}}, {"pk": 13, "model": "auth.permission", "fields": {"codename": "add_session", "name": "Can add session", "content_type": 5}}, {"pk": 14, "model": "auth.permission", "fields": {"codename": "change_session", "name": "Can change session", "content_type": 5}}, {"pk": 15, "model": "auth.permission", "fields": {"codename": "delete_session", "name": "Can delete session", "content_type": 5}}, {"pk": 340, "model": "auth.permission", "fields": {"codename": "add_certificateitem", "name": "Can add certificate item", "content_type": 113}}, {"pk": 341, "model": "auth.permission", "fields": {"codename": "change_certificateitem", "name": "Can change certificate item", "content_type": 113}}, {"pk": 342, "model": "auth.permission", "fields": {"codename": "delete_certificateitem", "name": "Can delete certificate item", "content_type": 113}}, {"pk": 322, "model": "auth.permission", "fields": {"codename": "add_coupon", "name": "Can add coupon", "content_type": 107}}, {"pk": 323, "model": "auth.permission", "fields": {"codename": "change_coupon", "name": "Can change coupon", "content_type": 107}}, {"pk": 324, "model": "auth.permission", "fields": {"codename": "delete_coupon", "name": "Can delete coupon", "content_type": 107}}, {"pk": 325, "model": "auth.permission", "fields": {"codename": "add_couponredemption", "name": "Can add coupon redemption", "content_type": 108}}, {"pk": 326, "model": "auth.permission", "fields": {"codename": "change_couponredemption", "name": "Can change coupon redemption", "content_type": 108}}, {"pk": 327, "model": "auth.permission", "fields": {"codename": "delete_couponredemption", "name": "Can delete coupon redemption", "content_type": 108}}, {"pk": 331, "model": "auth.permission", "fields": {"codename": "add_courseregcodeitem", "name": "Can add course reg code item", "content_type": 110}}, {"pk": 332, "model": "auth.permission", "fields": {"codename": "change_courseregcodeitem", "name": "Can change course reg code item", "content_type": 110}}, {"pk": 333, "model": "auth.permission", "fields": {"codename": "delete_courseregcodeitem", "name": "Can delete course reg code item", "content_type": 110}}, {"pk": 334, "model": "auth.permission", "fields": {"codename": "add_courseregcodeitemannotation", "name": "Can add course reg code item annotation", "content_type": 111}}, {"pk": 335, "model": "auth.permission", "fields": {"codename": "change_courseregcodeitemannotation", "name": "Can change course reg code item annotation", "content_type": 111}}, {"pk": 336, "model": "auth.permission", "fields": {"codename": "delete_courseregcodeitemannotation", "name": "Can delete course reg code item annotation", "content_type": 111}}, {"pk": 316, "model": "auth.permission", "fields": {"codename": "add_courseregistrationcode", "name": "Can add course registration code", "content_type": 105}}, {"pk": 317, "model": "auth.permission", "fields": {"codename": "change_courseregistrationcode", "name": "Can change course registration code", "content_type": 105}}, {"pk": 318, "model": "auth.permission", "fields": {"codename": "delete_courseregistrationcode", "name": "Can delete course registration code", "content_type": 105}}, {"pk": 310, "model": "auth.permission", "fields": {"codename": "add_courseregistrationcodeinvoiceitem", "name": "Can add course registration code invoice item", "content_type": 103}}, {"pk": 311, "model": "auth.permission", "fields": {"codename": "change_courseregistrationcodeinvoiceitem", "name": "Can change course registration code invoice item", "content_type": 103}}, {"pk": 312, "model": "auth.permission", "fields": {"codename": "delete_courseregistrationcodeinvoiceitem", "name": "Can delete course registration code invoice item", "content_type": 103}}, {"pk": 346, "model": "auth.permission", "fields": {"codename": "add_donation", "name": "Can add donation", "content_type": 115}}, {"pk": 347, "model": "auth.permission", "fields": {"codename": "change_donation", "name": "Can change donation", "content_type": 115}}, {"pk": 348, "model": "auth.permission", "fields": {"codename": "delete_donation", "name": "Can delete donation", "content_type": 115}}, {"pk": 343, "model": "auth.permission", "fields": {"codename": "add_donationconfiguration", "name": "Can add donation configuration", "content_type": 114}}, {"pk": 344, "model": "auth.permission", "fields": {"codename": "change_donationconfiguration", "name": "Can change donation configuration", "content_type": 114}}, {"pk": 345, "model": "auth.permission", "fields": {"codename": "delete_donationconfiguration", "name": "Can delete donation configuration", "content_type": 114}}, {"pk": 301, "model": "auth.permission", "fields": {"codename": "add_invoice", "name": "Can add invoice", "content_type": 100}}, {"pk": 302, "model": "auth.permission", "fields": {"codename": "change_invoice", "name": "Can change invoice", "content_type": 100}}, {"pk": 303, "model": "auth.permission", "fields": {"codename": "delete_invoice", "name": "Can delete invoice", "content_type": 100}}, {"pk": 313, "model": "auth.permission", "fields": {"codename": "add_invoicehistory", "name": "Can add invoice history", "content_type": 104}}, {"pk": 314, "model": "auth.permission", "fields": {"codename": "change_invoicehistory", "name": "Can change invoice history", "content_type": 104}}, {"pk": 315, "model": "auth.permission", "fields": {"codename": "delete_invoicehistory", "name": "Can delete invoice history", "content_type": 104}}, {"pk": 307, "model": "auth.permission", "fields": {"codename": "add_invoiceitem", "name": "Can add invoice item", "content_type": 102}}, {"pk": 308, "model": "auth.permission", "fields": {"codename": "change_invoiceitem", "name": "Can change invoice item", "content_type": 102}}, {"pk": 309, "model": "auth.permission", "fields": {"codename": "delete_invoiceitem", "name": "Can delete invoice item", "content_type": 102}}, {"pk": 304, "model": "auth.permission", "fields": {"codename": "add_invoicetransaction", "name": "Can add invoice transaction", "content_type": 101}}, {"pk": 305, "model": "auth.permission", "fields": {"codename": "change_invoicetransaction", "name": "Can change invoice transaction", "content_type": 101}}, {"pk": 306, "model": "auth.permission", "fields": {"codename": "delete_invoicetransaction", "name": "Can delete invoice transaction", "content_type": 101}}, {"pk": 295, "model": "auth.permission", "fields": {"codename": "add_order", "name": "Can add order", "content_type": 98}}, {"pk": 296, "model": "auth.permission", "fields": {"codename": "change_order", "name": "Can change order", "content_type": 98}}, {"pk": 297, "model": "auth.permission", "fields": {"codename": "delete_order", "name": "Can delete order", "content_type": 98}}, {"pk": 298, "model": "auth.permission", "fields": {"codename": "add_orderitem", "name": "Can add order item", "content_type": 99}}, {"pk": 299, "model": "auth.permission", "fields": {"codename": "change_orderitem", "name": "Can change order item", "content_type": 99}}, {"pk": 300, "model": "auth.permission", "fields": {"codename": "delete_orderitem", "name": "Can delete order item", "content_type": 99}}, {"pk": 328, "model": "auth.permission", "fields": {"codename": "add_paidcourseregistration", "name": "Can add paid course registration", "content_type": 109}}, {"pk": 329, "model": "auth.permission", "fields": {"codename": "change_paidcourseregistration", "name": "Can change paid course registration", "content_type": 109}}, {"pk": 330, "model": "auth.permission", "fields": {"codename": "delete_paidcourseregistration", "name": "Can delete paid course registration", "content_type": 109}}, {"pk": 337, "model": "auth.permission", "fields": {"codename": "add_paidcourseregistrationannotation", "name": "Can add paid course registration annotation", "content_type": 112}}, {"pk": 338, "model": "auth.permission", "fields": {"codename": "change_paidcourseregistrationannotation", "name": "Can change paid course registration annotation", "content_type": 112}}, {"pk": 339, "model": "auth.permission", "fields": {"codename": "delete_paidcourseregistrationannotation", "name": "Can delete paid course registration annotation", "content_type": 112}}, {"pk": 319, "model": "auth.permission", "fields": {"codename": "add_registrationcoderedemption", "name": "Can add registration code redemption", "content_type": 106}}, {"pk": 320, "model": "auth.permission", "fields": {"codename": "change_registrationcoderedemption", "name": "Can change registration code redemption", "content_type": 106}}, {"pk": 321, "model": "auth.permission", "fields": {"codename": "delete_registrationcoderedemption", "name": "Can delete registration code redemption", "content_type": 106}}, {"pk": 16, "model": "auth.permission", "fields": {"codename": "add_site", "name": "Can add site", "content_type": 6}}, {"pk": 17, "model": "auth.permission", "fields": {"codename": "change_site", "name": "Can change site", "content_type": 6}}, {"pk": 18, "model": "auth.permission", "fields": {"codename": "delete_site", "name": "Can delete site", "content_type": 6}}, {"pk": 43, "model": "auth.permission", "fields": {"codename": "add_migrationhistory", "name": "Can add migration history", "content_type": 15}}, {"pk": 44, "model": "auth.permission", "fields": {"codename": "change_migrationhistory", "name": "Can change migration history", "content_type": 15}}, {"pk": 45, "model": "auth.permission", "fields": {"codename": "delete_migrationhistory", "name": "Can delete migration history", "content_type": 15}}, {"pk": 283, "model": "auth.permission", "fields": {"codename": "add_splashconfig", "name": "Can add splash config", "content_type": 94}}, {"pk": 284, "model": "auth.permission", "fields": {"codename": "change_splashconfig", "name": "Can change splash config", "content_type": 94}}, {"pk": 285, "model": "auth.permission", "fields": {"codename": "delete_splashconfig", "name": "Can delete splash config", "content_type": 94}}, {"pk": 100, "model": "auth.permission", "fields": {"codename": "add_anonymoususerid", "name": "Can add anonymous user id", "content_type": 34}}, {"pk": 101, "model": "auth.permission", "fields": {"codename": "change_anonymoususerid", "name": "Can change anonymous user id", "content_type": 34}}, {"pk": 102, "model": "auth.permission", "fields": {"codename": "delete_anonymoususerid", "name": "Can delete anonymous user id", "content_type": 34}}, {"pk": 136, "model": "auth.permission", "fields": {"codename": "add_courseaccessrole", "name": "Can add course access role", "content_type": 46}}, {"pk": 137, "model": "auth.permission", "fields": {"codename": "change_courseaccessrole", "name": "Can change course access role", "content_type": 46}}, {"pk": 138, "model": "auth.permission", "fields": {"codename": "delete_courseaccessrole", "name": "Can delete course access role", "content_type": 46}}, {"pk": 130, "model": "auth.permission", "fields": {"codename": "add_courseenrollment", "name": "Can add course enrollment", "content_type": 44}}, {"pk": 131, "model": "auth.permission", "fields": {"codename": "change_courseenrollment", "name": "Can change course enrollment", "content_type": 44}}, {"pk": 132, "model": "auth.permission", "fields": {"codename": "delete_courseenrollment", "name": "Can delete course enrollment", "content_type": 44}}, {"pk": 133, "model": "auth.permission", "fields": {"codename": "add_courseenrollmentallowed", "name": "Can add course enrollment allowed", "content_type": 45}}, {"pk": 134, "model": "auth.permission", "fields": {"codename": "change_courseenrollmentallowed", "name": "Can change course enrollment allowed", "content_type": 45}}, {"pk": 135, "model": "auth.permission", "fields": {"codename": "delete_courseenrollmentallowed", "name": "Can delete course enrollment allowed", "content_type": 45}}, {"pk": 139, "model": "auth.permission", "fields": {"codename": "add_dashboardconfiguration", "name": "Can add dashboard configuration", "content_type": 47}}, {"pk": 140, "model": "auth.permission", "fields": {"codename": "change_dashboardconfiguration", "name": "Can change dashboard configuration", "content_type": 47}}, {"pk": 141, "model": "auth.permission", "fields": {"codename": "delete_dashboardconfiguration", "name": "Can delete dashboard configuration", "content_type": 47}}, {"pk": 145, "model": "auth.permission", "fields": {"codename": "add_entranceexamconfiguration", "name": "Can add entrance exam configuration", "content_type": 49}}, {"pk": 146, "model": "auth.permission", "fields": {"codename": "change_entranceexamconfiguration", "name": "Can change entrance exam configuration", "content_type": 49}}, {"pk": 147, "model": "auth.permission", "fields": {"codename": "delete_entranceexamconfiguration", "name": "Can delete entrance exam configuration", "content_type": 49}}, {"pk": 142, "model": "auth.permission", "fields": {"codename": "add_linkedinaddtoprofileconfiguration", "name": "Can add linked in add to profile configuration", "content_type": 48}}, {"pk": 143, "model": "auth.permission", "fields": {"codename": "change_linkedinaddtoprofileconfiguration", "name": "Can change linked in add to profile configuration", "content_type": 48}}, {"pk": 144, "model": "auth.permission", "fields": {"codename": "delete_linkedinaddtoprofileconfiguration", "name": "Can delete linked in add to profile configuration", "content_type": 48}}, {"pk": 127, "model": "auth.permission", "fields": {"codename": "add_loginfailures", "name": "Can add login failures", "content_type": 43}}, {"pk": 128, "model": "auth.permission", "fields": {"codename": "change_loginfailures", "name": "Can change login failures", "content_type": 43}}, {"pk": 129, "model": "auth.permission", "fields": {"codename": "delete_loginfailures", "name": "Can delete login failures", "content_type": 43}}, {"pk": 124, "model": "auth.permission", "fields": {"codename": "add_passwordhistory", "name": "Can add password history", "content_type": 42}}, {"pk": 125, "model": "auth.permission", "fields": {"codename": "change_passwordhistory", "name": "Can change password history", "content_type": 42}}, {"pk": 126, "model": "auth.permission", "fields": {"codename": "delete_passwordhistory", "name": "Can delete password history", "content_type": 42}}, {"pk": 121, "model": "auth.permission", "fields": {"codename": "add_pendingemailchange", "name": "Can add pending email change", "content_type": 41}}, {"pk": 122, "model": "auth.permission", "fields": {"codename": "change_pendingemailchange", "name": "Can change pending email change", "content_type": 41}}, {"pk": 123, "model": "auth.permission", "fields": {"codename": "delete_pendingemailchange", "name": "Can delete pending email change", "content_type": 41}}, {"pk": 118, "model": "auth.permission", "fields": {"codename": "add_pendingnamechange", "name": "Can add pending name change", "content_type": 40}}, {"pk": 119, "model": "auth.permission", "fields": {"codename": "change_pendingnamechange", "name": "Can change pending name change", "content_type": 40}}, {"pk": 120, "model": "auth.permission", "fields": {"codename": "delete_pendingnamechange", "name": "Can delete pending name change", "content_type": 40}}, {"pk": 115, "model": "auth.permission", "fields": {"codename": "add_registration", "name": "Can add registration", "content_type": 39}}, {"pk": 116, "model": "auth.permission", "fields": {"codename": "change_registration", "name": "Can change registration", "content_type": 39}}, {"pk": 117, "model": "auth.permission", "fields": {"codename": "delete_registration", "name": "Can delete registration", "content_type": 39}}, {"pk": 106, "model": "auth.permission", "fields": {"codename": "add_userprofile", "name": "Can add user profile", "content_type": 36}}, {"pk": 107, "model": "auth.permission", "fields": {"codename": "change_userprofile", "name": "Can change user profile", "content_type": 36}}, {"pk": 108, "model": "auth.permission", "fields": {"codename": "delete_userprofile", "name": "Can delete user profile", "content_type": 36}}, {"pk": 109, "model": "auth.permission", "fields": {"codename": "add_usersignupsource", "name": "Can add user signup source", "content_type": 37}}, {"pk": 110, "model": "auth.permission", "fields": {"codename": "change_usersignupsource", "name": "Can change user signup source", "content_type": 37}}, {"pk": 111, "model": "auth.permission", "fields": {"codename": "delete_usersignupsource", "name": "Can delete user signup source", "content_type": 37}}, {"pk": 103, "model": "auth.permission", "fields": {"codename": "add_userstanding", "name": "Can add user standing", "content_type": 35}}, {"pk": 104, "model": "auth.permission", "fields": {"codename": "change_userstanding", "name": "Can change user standing", "content_type": 35}}, {"pk": 105, "model": "auth.permission", "fields": {"codename": "delete_userstanding", "name": "Can delete user standing", "content_type": 35}}, {"pk": 112, "model": "auth.permission", "fields": {"codename": "add_usertestgroup", "name": "Can add user test group", "content_type": 38}}, {"pk": 113, "model": "auth.permission", "fields": {"codename": "change_usertestgroup", "name": "Can change user test group", "content_type": 38}}, {"pk": 114, "model": "auth.permission", "fields": {"codename": "delete_usertestgroup", "name": "Can delete user test group", "content_type": 38}}, {"pk": 409, "model": "auth.permission", "fields": {"codename": "add_score", "name": "Can add score", "content_type": 136}}, {"pk": 410, "model": "auth.permission", "fields": {"codename": "change_score", "name": "Can change score", "content_type": 136}}, {"pk": 411, "model": "auth.permission", "fields": {"codename": "delete_score", "name": "Can delete score", "content_type": 136}}, {"pk": 412, "model": "auth.permission", "fields": {"codename": "add_scoresummary", "name": "Can add score summary", "content_type": 137}}, {"pk": 413, "model": "auth.permission", "fields": {"codename": "change_scoresummary", "name": "Can change score summary", "content_type": 137}}, {"pk": 414, "model": "auth.permission", "fields": {"codename": "delete_scoresummary", "name": "Can delete score summary", "content_type": 137}}, {"pk": 403, "model": "auth.permission", "fields": {"codename": "add_studentitem", "name": "Can add student item", "content_type": 134}}, {"pk": 404, "model": "auth.permission", "fields": {"codename": "change_studentitem", "name": "Can change student item", "content_type": 134}}, {"pk": 405, "model": "auth.permission", "fields": {"codename": "delete_studentitem", "name": "Can delete student item", "content_type": 134}}, {"pk": 406, "model": "auth.permission", "fields": {"codename": "add_submission", "name": "Can add submission", "content_type": 135}}, {"pk": 407, "model": "auth.permission", "fields": {"codename": "change_submission", "name": "Can change submission", "content_type": 135}}, {"pk": 408, "model": "auth.permission", "fields": {"codename": "delete_submission", "name": "Can delete submission", "content_type": 135}}, {"pk": 394, "model": "auth.permission", "fields": {"codename": "add_surveyanswer", "name": "Can add survey answer", "content_type": 131}}, {"pk": 395, "model": "auth.permission", "fields": {"codename": "change_surveyanswer", "name": "Can change survey answer", "content_type": 131}}, {"pk": 396, "model": "auth.permission", "fields": {"codename": "delete_surveyanswer", "name": "Can delete survey answer", "content_type": 131}}, {"pk": 391, "model": "auth.permission", "fields": {"codename": "add_surveyform", "name": "Can add survey form", "content_type": 130}}, {"pk": 392, "model": "auth.permission", "fields": {"codename": "change_surveyform", "name": "Can change survey form", "content_type": 130}}, {"pk": 393, "model": "auth.permission", "fields": {"codename": "delete_surveyform", "name": "Can delete survey form", "content_type": 130}}, {"pk": 148, "model": "auth.permission", "fields": {"codename": "add_trackinglog", "name": "Can add tracking log", "content_type": 50}}, {"pk": 149, "model": "auth.permission", "fields": {"codename": "change_trackinglog", "name": "Can change tracking log", "content_type": 50}}, {"pk": 150, "model": "auth.permission", "fields": {"codename": "delete_trackinglog", "name": "Can delete tracking log", "content_type": 50}}, {"pk": 289, "model": "auth.permission", "fields": {"codename": "add_usercoursetag", "name": "Can add user course tag", "content_type": 96}}, {"pk": 290, "model": "auth.permission", "fields": {"codename": "change_usercoursetag", "name": "Can change user course tag", "content_type": 96}}, {"pk": 291, "model": "auth.permission", "fields": {"codename": "delete_usercoursetag", "name": "Can delete user course tag", "content_type": 96}}, {"pk": 292, "model": "auth.permission", "fields": {"codename": "add_userorgtag", "name": "Can add user org tag", "content_type": 97}}, {"pk": 293, "model": "auth.permission", "fields": {"codename": "change_userorgtag", "name": "Can change user org tag", "content_type": 97}}, {"pk": 294, "model": "auth.permission", "fields": {"codename": "delete_userorgtag", "name": "Can delete user org tag", "content_type": 97}}, {"pk": 286, "model": "auth.permission", "fields": {"codename": "add_userpreference", "name": "Can add user preference", "content_type": 95}}, {"pk": 287, "model": "auth.permission", "fields": {"codename": "change_userpreference", "name": "Can change user preference", "content_type": 95}}, {"pk": 288, "model": "auth.permission", "fields": {"codename": "delete_userpreference", "name": "Can delete user preference", "content_type": 95}}, {"pk": 151, "model": "auth.permission", "fields": {"codename": "add_ratelimitconfiguration", "name": "Can add rate limit configuration", "content_type": 51}}, {"pk": 152, "model": "auth.permission", "fields": {"codename": "change_ratelimitconfiguration", "name": "Can change rate limit configuration", "content_type": 51}}, {"pk": 153, "model": "auth.permission", "fields": {"codename": "delete_ratelimitconfiguration", "name": "Can delete rate limit configuration", "content_type": 51}}, {"pk": 355, "model": "auth.permission", "fields": {"codename": "add_softwaresecurephotoverification", "name": "Can add software secure photo verification", "content_type": 118}}, {"pk": 356, "model": "auth.permission", "fields": {"codename": "change_softwaresecurephotoverification", "name": "Can change software secure photo verification", "content_type": 118}}, {"pk": 357, "model": "auth.permission", "fields": {"codename": "delete_softwaresecurephotoverification", "name": "Can delete software secure photo verification", "content_type": 118}}, {"pk": 229, "model": "auth.permission", "fields": {"codename": "add_article", "name": "Can add article", "content_type": 77}}, {"pk": 233, "model": "auth.permission", "fields": {"codename": "assign", "name": "Can change ownership of any article", "content_type": 77}}, {"pk": 230, "model": "auth.permission", "fields": {"codename": "change_article", "name": "Can change article", "content_type": 77}}, {"pk": 231, "model": "auth.permission", "fields": {"codename": "delete_article", "name": "Can delete article", "content_type": 77}}, {"pk": 234, "model": "auth.permission", "fields": {"codename": "grant", "name": "Can assign permissions to other users", "content_type": 77}}, {"pk": 232, "model": "auth.permission", "fields": {"codename": "moderate", "name": "Can edit all articles and lock/unlock/restore", "content_type": 77}}, {"pk": 235, "model": "auth.permission", "fields": {"codename": "add_articleforobject", "name": "Can add Article for object", "content_type": 78}}, {"pk": 236, "model": "auth.permission", "fields": {"codename": "change_articleforobject", "name": "Can change Article for object", "content_type": 78}}, {"pk": 237, "model": "auth.permission", "fields": {"codename": "delete_articleforobject", "name": "Can delete Article for object", "content_type": 78}}, {"pk": 244, "model": "auth.permission", "fields": {"codename": "add_articleplugin", "name": "Can add article plugin", "content_type": 81}}, {"pk": 245, "model": "auth.permission", "fields": {"codename": "change_articleplugin", "name": "Can change article plugin", "content_type": 81}}, {"pk": 246, "model": "auth.permission", "fields": {"codename": "delete_articleplugin", "name": "Can delete article plugin", "content_type": 81}}, {"pk": 238, "model": "auth.permission", "fields": {"codename": "add_articlerevision", "name": "Can add article revision", "content_type": 79}}, {"pk": 239, "model": "auth.permission", "fields": {"codename": "change_articlerevision", "name": "Can change article revision", "content_type": 79}}, {"pk": 240, "model": "auth.permission", "fields": {"codename": "delete_articlerevision", "name": "Can delete article revision", "content_type": 79}}, {"pk": 259, "model": "auth.permission", "fields": {"codename": "add_articlesubscription", "name": "Can add article subscription", "content_type": 86}}, {"pk": 260, "model": "auth.permission", "fields": {"codename": "change_articlesubscription", "name": "Can change article subscription", "content_type": 86}}, {"pk": 261, "model": "auth.permission", "fields": {"codename": "delete_articlesubscription", "name": "Can delete article subscription", "content_type": 86}}, {"pk": 247, "model": "auth.permission", "fields": {"codename": "add_reusableplugin", "name": "Can add reusable plugin", "content_type": 82}}, {"pk": 248, "model": "auth.permission", "fields": {"codename": "change_reusableplugin", "name": "Can change reusable plugin", "content_type": 82}}, {"pk": 249, "model": "auth.permission", "fields": {"codename": "delete_reusableplugin", "name": "Can delete reusable plugin", "content_type": 82}}, {"pk": 253, "model": "auth.permission", "fields": {"codename": "add_revisionplugin", "name": "Can add revision plugin", "content_type": 84}}, {"pk": 254, "model": "auth.permission", "fields": {"codename": "change_revisionplugin", "name": "Can change revision plugin", "content_type": 84}}, {"pk": 255, "model": "auth.permission", "fields": {"codename": "delete_revisionplugin", "name": "Can delete revision plugin", "content_type": 84}}, {"pk": 256, "model": "auth.permission", "fields": {"codename": "add_revisionpluginrevision", "name": "Can add revision plugin revision", "content_type": 85}}, {"pk": 257, "model": "auth.permission", "fields": {"codename": "change_revisionpluginrevision", "name": "Can change revision plugin revision", "content_type": 85}}, {"pk": 258, "model": "auth.permission", "fields": {"codename": "delete_revisionpluginrevision", "name": "Can delete revision plugin revision", "content_type": 85}}, {"pk": 250, "model": "auth.permission", "fields": {"codename": "add_simpleplugin", "name": "Can add simple plugin", "content_type": 83}}, {"pk": 251, "model": "auth.permission", "fields": {"codename": "change_simpleplugin", "name": "Can change simple plugin", "content_type": 83}}, {"pk": 252, "model": "auth.permission", "fields": {"codename": "delete_simpleplugin", "name": "Can delete simple plugin", "content_type": 83}}, {"pk": 241, "model": "auth.permission", "fields": {"codename": "add_urlpath", "name": "Can add URL path", "content_type": 80}}, {"pk": 242, "model": "auth.permission", "fields": {"codename": "change_urlpath", "name": "Can change URL path", "content_type": 80}}, {"pk": 243, "model": "auth.permission", "fields": {"codename": "delete_urlpath", "name": "Can delete URL path", "content_type": 80}}, {"pk": 463, "model": "auth.permission", "fields": {"codename": "add_assessmentworkflow", "name": "Can add assessment workflow", "content_type": 154}}, {"pk": 464, "model": "auth.permission", "fields": {"codename": "change_assessmentworkflow", "name": "Can change assessment workflow", "content_type": 154}}, {"pk": 465, "model": "auth.permission", "fields": {"codename": "delete_assessmentworkflow", "name": "Can delete assessment workflow", "content_type": 154}}, {"pk": 469, "model": "auth.permission", "fields": {"codename": "add_assessmentworkflowcancellation", "name": "Can add assessment workflow cancellation", "content_type": 156}}, {"pk": 470, "model": "auth.permission", "fields": {"codename": "change_assessmentworkflowcancellation", "name": "Can change assessment workflow cancellation", "content_type": 156}}, {"pk": 471, "model": "auth.permission", "fields": {"codename": "delete_assessmentworkflowcancellation", "name": "Can delete assessment workflow cancellation", "content_type": 156}}, {"pk": 466, "model": "auth.permission", "fields": {"codename": "add_assessmentworkflowstep", "name": "Can add assessment workflow step", "content_type": 155}}, {"pk": 467, "model": "auth.permission", "fields": {"codename": "change_assessmentworkflowstep", "name": "Can change assessment workflow step", "content_type": 155}}, {"pk": 468, "model": "auth.permission", "fields": {"codename": "delete_assessmentworkflowstep", "name": "Can delete assessment workflow step", "content_type": 155}}, {"pk": 508, "model": "auth.permission", "fields": {"codename": "add_studioconfig", "name": "Can add studio config", "content_type": 169}}, {"pk": 509, "model": "auth.permission", "fields": {"codename": "change_studioconfig", "name": "Can change studio config", "content_type": 169}}, {"pk": 510, "model": "auth.permission", "fields": {"codename": "delete_studioconfig", "name": "Can delete studio config", "content_type": 169}}, {"pk": 1, "model": "util.ratelimitconfiguration", "fields": {"change_date": "2015-03-31T06:25:45Z", "changed_by": null, "enabled": true}}, {"pk": 1, "model": "certificates.certificatehtmlviewconfiguration", "fields": {"change_date": "2015-03-31T06:25:47Z", "changed_by": null, "configuration": "{\n {\n \"default\": {\n \"accomplishment_class_append\": \"accomplishment-certificate\",\n \"platform_name\": \"edX\",\n \"company_privacy_url\": \"http://www.edx.org/edx-privacy-policy\",\n \"company_tos_url\": \"http://www.edx.org/edx-terms-service\",\n \"company_verified_certificate_url\": \"http://www.edx.org/verified-certificate\",\n \"document_stylesheet_url_application\": \"/static/certificates/sass/main-ltr.css\",\n \"logo_src\": \"/static/certificates/images/logo-edx.svg\",\n \"logo_url\": \"http://www.edx.org\"\n },\n \"honor\": {\n \"certificate_type\": \"Honor Code\",\n \"document_body_class_append\": \"is-honorcode\"\n },\n \"verified\": {\n \"certificate_type\": \"Verified\",\n \"document_body_class_append\": \"is-idverified\"\n },\n \"xseries\": {\n \"certificate_type\": \"XSeries\",\n \"document_body_class_append\": \"is-xseries\"\n }\n}\n }", "enabled": false}}, {"pk": 1, "model": "dark_lang.darklangconfig", "fields": {"change_date": "2015-03-31T06:26:01Z", "changed_by": null, "enabled": true, "released_languages": ""}}, {"pk": 1, "model": "mobile_api.mobileapiconfig", "fields": {"change_date": "2015-03-31T06:26:03Z", "video_profiles": "mobile_low,mobile_high,youtube", "changed_by": null, "enabled": false}},{"pk": 1, "model": "edxval.profile", "fields": {"profile_name": "desktop_mp4"}}, {"pk": 1, "model": "edxval.video", "fields": {"duration": 10.0, "status": "status", "edx_video_id": "edx_video_id", "client_video_id": "", "created": "2015-04-25T18:04:41Z"}}, {"pk": 1, "model": "edxval.encodedvideo", "fields": {"profile": 1, "created": "2015-04-25T18:04:41Z", "url": "http://www.w3schools.com/html/mov_bbb.webm", "modified": "2015-04-25T18:04:41Z", "video": 1, "file_size": 1000, "bitrate": 1000}}] diff --git a/lms/djangoapps/courseware/tests/test_video_handlers.py b/lms/djangoapps/courseware/tests/test_video_handlers.py index d785136597b3133f213f2ca5731b24a408a4eaeb..86be625395df533697b536e3ed7e7c65dcbb88f5 100644 --- a/lms/djangoapps/courseware/tests/test_video_handlers.py +++ b/lms/djangoapps/courseware/tests/test_video_handlers.py @@ -1,15 +1,17 @@ # -*- coding: utf-8 -*- """Video xmodule tests in mongo.""" -from mock import patch -from nose.plugins.attrib import attr import os +import freezegun import tempfile import textwrap import json -from datetime import timedelta +import ddt + +from nose.plugins.attrib import attr +from datetime import timedelta, datetime from webob import Request -from mock import MagicMock, Mock +from mock import MagicMock, Mock, patch from xmodule.contentstore.content import StaticContent from xmodule.contentstore.django import contentstore @@ -26,6 +28,9 @@ from xmodule.video_module.transcripts_utils import ( TranscriptsGenerationException, ) + +TRANSCRIPT = {"start": [10], "end": [100], "text": ["Hi, welcome to Edx."]} +BUMPER_TRANSCRIPT = {"start": [1], "end": [10], "text": ["A bumper"]} SRT_content = textwrap.dedent(""" 0 00:00:00,12 --> 00:00:00,100 @@ -104,6 +109,20 @@ def _upload_file(subs_file, location, filename): del_cached_content(content.location) +def attach_sub(item, filename): + """ + Attach `en` transcript. + """ + item.sub = filename + + +def attach_bumper_transcript(item, filename, lang="en"): + """ + Attach bumper transcript. + """ + item.video_bumper["transcripts"][lang] = filename + + @attr('shard_1') class TestVideo(BaseTestXmodule): """Integration tests: web client + mongo.""" @@ -129,6 +148,8 @@ class TestVideo(BaseTestXmodule): {'speed': 2.0}, {'saved_video_position': "00:00:10"}, {'transcript_language': 'uk'}, + {'bumper_do_not_show_again': True}, + {'bumper_last_view_date': True}, {'demoo�': 'sample'} ] for sample in data: @@ -151,6 +172,15 @@ class TestVideo(BaseTestXmodule): self.item_descriptor.handle_ajax('save_user_state', {'transcript_language': "uk"}) self.assertEqual(self.item_descriptor.transcript_language, 'uk') + self.assertEqual(self.item_descriptor.bumper_do_not_show_again, False) + self.item_descriptor.handle_ajax('save_user_state', {'bumper_do_not_show_again': True}) + self.assertEqual(self.item_descriptor.bumper_do_not_show_again, True) + + with freezegun.freeze_time(datetime.now()): + self.assertEqual(self.item_descriptor.bumper_last_view_date, None) + self.item_descriptor.handle_ajax('save_user_state', {'bumper_last_view_date': True}) + self.assertEqual(self.item_descriptor.bumper_last_view_date, datetime.utcnow()) + response = self.item_descriptor.handle_ajax('save_user_state', {u'demoo�': "sample"}) self.assertEqual(json.loads(response)['success'], True) @@ -166,7 +196,7 @@ class TestTranscriptAvailableTranslationsDispatch(TestVideo): Tests for `available_translations` dispatch. """ - non_en_file = _create_srt_file() + srt_file = _create_srt_file() DATA = """ <video show_captions="true" display_name="A Name" @@ -175,7 +205,7 @@ class TestTranscriptAvailableTranslationsDispatch(TestVideo): <source src="example.webm"/> <transcript language="uk" src="{}"/> </video> - """.format(os.path.split(non_en_file.name)[1]) + """.format(os.path.split(srt_file.name)[1]) MODEL_DATA = { 'data': DATA @@ -197,7 +227,7 @@ class TestTranscriptAvailableTranslationsDispatch(TestVideo): self.assertEqual(json.loads(response.body), ['en']) def test_available_translation_non_en(self): - _upload_file(self.non_en_file, self.item_descriptor.location, os.path.split(self.non_en_file.name)[1]) + _upload_file(self.srt_file, self.item_descriptor.location, os.path.split(self.srt_file.name)[1]) request = Request.blank('/available_translations') response = self.item.transcript(request=request, dispatch='available_translations') @@ -210,7 +240,7 @@ class TestTranscriptAvailableTranslationsDispatch(TestVideo): _upload_sjson_file(good_sjson, self.item_descriptor.location) # Upload non-english transcript. - _upload_file(self.non_en_file, self.item_descriptor.location, os.path.split(self.non_en_file.name)[1]) + _upload_file(self.srt_file, self.item_descriptor.location, os.path.split(self.srt_file.name)[1]) self.item.sub = _get_subs_id(good_sjson.name) @@ -220,6 +250,63 @@ class TestTranscriptAvailableTranslationsDispatch(TestVideo): @attr('shard_1') +@ddt.ddt +class TestTranscriptAvailableTranslationsBumperDispatch(TestVideo): + """ + Test video handler that provide available translations info. + + Tests for `available_translations_bumper` dispatch. + """ + srt_file = _create_srt_file() + DATA = """ + <video show_captions="true" + display_name="A Name" + > + <source src="example.mp4"/> + <source src="example.webm"/> + <transcript language="uk" src="{}"/> + </video> + """.format(os.path.split(srt_file.name)[1]) + + MODEL_DATA = { + 'data': DATA + } + + def setUp(self): + super(TestTranscriptAvailableTranslationsBumperDispatch, self).setUp() + self.item_descriptor.render(STUDENT_VIEW) + self.item = self.item_descriptor.xmodule_runtime.xmodule_instance + self.dispatch = "available_translations/?is_bumper=1" + self.item.video_bumper = {"transcripts": {"en": ""}} + + @ddt.data("en", "uk") + def test_available_translation_en_and_non_en(self, lang): + filename = os.path.split(self.srt_file.name)[1] + _upload_file(self.srt_file, self.item_descriptor.location, filename) + self.item.video_bumper["transcripts"][lang] = filename + + request = Request.blank('/' + self.dispatch) + response = self.item.transcript(request=request, dispatch=self.dispatch) + self.assertEqual(json.loads(response.body), [lang]) + + def test_multiple_available_translations(self): + en_translation = _create_srt_file() + en_translation_filename = os.path.split(en_translation.name)[1] + uk_translation_filename = os.path.split(self.srt_file.name)[1] + # Upload english transcript. + _upload_file(en_translation, self.item_descriptor.location, en_translation_filename) + + # Upload non-english transcript. + _upload_file(self.srt_file, self.item_descriptor.location, uk_translation_filename) + + self.item.video_bumper["transcripts"]["en"] = en_translation_filename + self.item.video_bumper["transcripts"]["uk"] = uk_translation_filename + + request = Request.blank('/' + self.dispatch) + response = self.item.transcript(request=request, dispatch=self.dispatch) + self.assertEqual(json.loads(response.body), ['en', 'uk']) + + class TestTranscriptDownloadDispatch(TestVideo): """ Test video handler that provide translation transcripts. @@ -272,8 +359,9 @@ class TestTranscriptDownloadDispatch(TestVideo): request = Request.blank('/download') response = self.item.transcript(request=request, dispatch='download') self.assertEqual(response.status, '404 Not Found') + transcripts = self.item.get_transcripts_info() with self.assertRaises(NotFoundError): - self.item.get_transcript() + self.item.get_transcript(transcripts) @patch('xmodule.video_module.VideoModule.get_transcript', return_value=('Subs!', u"å¡ž.srt", 'application/x-subrip; charset=utf-8')) def test_download_non_en_non_ascii_filename(self, __): @@ -285,14 +373,15 @@ class TestTranscriptDownloadDispatch(TestVideo): @attr('shard_1') +@ddt.ddt class TestTranscriptTranslationGetDispatch(TestVideo): """ Test video handler that provide translation transcripts. - Tests for `translation` dispatch. + Tests for `translation` and `translation_bumper` dispatches. """ - non_en_file = _create_srt_file() + srt_file = _create_srt_file() DATA = """ <video show_captions="true" display_name="A Name" @@ -301,7 +390,7 @@ class TestTranscriptTranslationGetDispatch(TestVideo): <source src="example.webm"/> <transcript language="uk" src="{}"/> </video> - """.format(os.path.split(non_en_file.name)[1]) + """.format(os.path.split(srt_file.name)[1]) MODEL_DATA = { 'data': DATA @@ -311,37 +400,41 @@ class TestTranscriptTranslationGetDispatch(TestVideo): super(TestTranscriptTranslationGetDispatch, self).setUp() self.item_descriptor.render(STUDENT_VIEW) self.item = self.item_descriptor.xmodule_runtime.xmodule_instance + self.item.video_bumper = {"transcripts": {"en": ""}} - def test_translation_fails(self): + @ddt.data( # No language - request = Request.blank('/translation') - response = self.item.transcript(request=request, dispatch='translation') - self.assertEqual(response.status, '400 Bad Request') - + ('/translation', 'translation', '400 Bad Request'), # No videoId - HTML5 video with language that is not in available languages - request = Request.blank('/translation/ru') - response = self.item.transcript(request=request, dispatch='translation/ru') - self.assertEqual(response.status, '404 Not Found') - + ('/translation/ru', 'translation/ru', '404 Not Found'), # Language is not in available languages - request = Request.blank('/translation/ru?videoId=12345') - response = self.item.transcript(request=request, dispatch='translation/ru') - self.assertEqual(response.status, '404 Not Found') - + ('/translation/ru?videoId=12345', 'translation/ru', '404 Not Found'), # Youtube_id is invalid or does not exist - request = Request.blank('/translation/uk?videoId=9855256955511225') - response = self.item.transcript(request=request, dispatch='translation/uk') - self.assertEqual(response.status, '404 Not Found') - - def test_translaton_en_youtube_success(self): + ('/translation/uk?videoId=9855256955511225', 'translation/uk', '404 Not Found'), + ('/translation?is_bumper=1', 'translation', '400 Bad Request'), + ('/translation/ru?is_bumper=1', 'translation/ru', '404 Not Found'), + ('/translation/ru?videoId=12345&is_bumper=1', 'translation/ru', '404 Not Found'), + ('/translation/uk?videoId=9855256955511225&is_bumper=1', 'translation/uk', '404 Not Found'), + ) + @ddt.unpack + def test_translation_fails(self, url, dispatch, status_code): + request = Request.blank(url) + response = self.item.transcript(request=request, dispatch=dispatch) + self.assertEqual(response.status, status_code) + + @ddt.data( + ('translation/en?videoId={}', 'translation/en', attach_sub), + ('translation/en?videoId={}&is_bumper=1', 'translation/en', attach_bumper_transcript)) + @ddt.unpack + def test_translaton_en_youtube_success(self, url, dispatch, attach): subs = {"start": [10], "end": [100], "text": ["Hi, welcome to Edx."]} good_sjson = _create_file(json.dumps(subs)) _upload_sjson_file(good_sjson, self.item_descriptor.location) subs_id = _get_subs_id(good_sjson.name) - self.item.sub = subs_id - request = Request.blank('/translation/en?videoId={}'.format(subs_id)) - response = self.item.transcript(request=request, dispatch='translation/en') + attach(self.item, subs_id) + request = Request.blank(url.format(subs_id)) + response = self.item.transcript(request=request, dispatch=dispatch) self.assertDictEqual(json.loads(response.body), subs) def test_translation_non_en_youtube_success(self): @@ -352,9 +445,9 @@ class TestTranscriptTranslationGetDispatch(TestVideo): u'\u041f\u0440\u0438\u0432\u0456\u0442, edX \u0432\u0456\u0442\u0430\u0454 \u0432\u0430\u0441.' ] } - self.non_en_file.seek(0) - _upload_file(self.non_en_file, self.item_descriptor.location, os.path.split(self.non_en_file.name)[1]) - subs_id = _get_subs_id(self.non_en_file.name) + self.srt_file.seek(0) + _upload_file(self.srt_file, self.item_descriptor.location, os.path.split(self.srt_file.name)[1]) + subs_id = _get_subs_id(self.srt_file.name) # youtube 1_0 request, will generate for all speeds for existing ids self.item.youtube_id_1_0 = subs_id @@ -387,16 +480,19 @@ class TestTranscriptTranslationGetDispatch(TestVideo): } self.assertDictEqual(json.loads(response.body), calculated_1_5) - def test_translaton_en_html5_success(self): - subs = {"start": [10], "end": [100], "text": ["Hi, welcome to Edx."]} - good_sjson = _create_file(json.dumps(subs)) + @ddt.data( + ('translation/en', 'translation/en', attach_sub), + ('translation/en?is_bumper=1', 'translation/en', attach_bumper_transcript)) + @ddt.unpack + def test_translaton_en_html5_success(self, url, dispatch, attach): + good_sjson = _create_file(json.dumps(TRANSCRIPT)) _upload_sjson_file(good_sjson, self.item_descriptor.location) subs_id = _get_subs_id(good_sjson.name) - self.item.sub = subs_id - request = Request.blank('/translation/en') - response = self.item.transcript(request=request, dispatch='translation/en') - self.assertDictEqual(json.loads(response.body), subs) + attach(self.item, subs_id) + request = Request.blank(url) + response = self.item.transcript(request=request, dispatch=dispatch) + self.assertDictEqual(json.loads(response.body), TRANSCRIPT) def test_translaton_non_en_html5_success(self): subs = { @@ -406,8 +502,8 @@ class TestTranscriptTranslationGetDispatch(TestVideo): u'\u041f\u0440\u0438\u0432\u0456\u0442, edX \u0432\u0456\u0442\u0430\u0454 \u0432\u0430\u0441.' ] } - self.non_en_file.seek(0) - _upload_file(self.non_en_file, self.item_descriptor.location, os.path.split(self.non_en_file.name)[1]) + self.srt_file.seek(0) + _upload_file(self.srt_file, self.item_descriptor.location, os.path.split(self.srt_file.name)[1]) # manually clean youtube_id_1_0, as it has default value self.item.youtube_id_1_0 = "" @@ -453,7 +549,22 @@ class TestTranscriptTranslationGetDispatch(TestVideo): response = self.item.transcript(request=request, dispatch='translation/uk') self.assertEqual(response.status, '404 Not Found') - def test_translation_static_transcript(self): + @ddt.data( + # Test youtube style en + ('/translation/en?videoId=12345', 'translation/en', '307 Temporary Redirect', '12345'), + # Test html5 style en + ('/translation/en', 'translation/en', '307 Temporary Redirect', 'OEoXaMPEzfM', attach_sub), + # Test different language to ensure we are just ignoring it since we can't + # translate with static fallback + ('/translation/uk', 'translation/uk', '404 Not Found'), + ( + '/translation/en?is_bumper=1', 'translation/en', '307 Temporary Redirect', 'OEoXaMPEzfM', + attach_bumper_transcript + ), + ('/translation/uk?is_bumper=1', 'translation/uk', '404 Not Found'), + ) + @ddt.unpack + def test_translation_static_transcript(self, url, dispatch, status_code, sub=None, attach=None): """ Set course static_asset_path and ensure we get redirected to that path if it isn't found in the contentstore @@ -464,30 +575,16 @@ class TestTranscriptTranslationGetDispatch(TestVideo): with store.branch_setting(ModuleStoreEnum.Branch.draft_preferred, self.course.id): store.update_item(self.course, self.user.id) - # Test youtube style en - request = Request.blank('/translation/en?videoId=12345') - response = self.item.transcript(request=request, dispatch='translation/en') - self.assertEqual(response.status, '307 Temporary Redirect') - self.assertIn( - ('Location', '/static/dummy/static/subs_12345.srt.sjson'), - response.headerlist - ) - - # Test HTML5 video style - self.item.sub = 'OEoXaMPEzfM' - request = Request.blank('/translation/en') - response = self.item.transcript(request=request, dispatch='translation/en') - self.assertEqual(response.status, '307 Temporary Redirect') - self.assertIn( - ('Location', '/static/dummy/static/subs_OEoXaMPEzfM.srt.sjson'), - response.headerlist - ) - - # Test different language to ensure we are just ignoring it since we can't - # translate with static fallback - request = Request.blank('/translation/uk') - response = self.item.transcript(request=request, dispatch='translation/uk') - self.assertEqual(response.status, '404 Not Found') + if attach: + attach(self.item, sub) + request = Request.blank(url) + response = self.item.transcript(request=request, dispatch=dispatch) + self.assertEqual(response.status, status_code) + if sub: + self.assertIn( + ('Location', '/static/dummy/static/subs_{}.srt.sjson'.format(sub)), + response.headerlist + ) @attr('shard_1') @@ -497,7 +594,7 @@ class TestStudioTranscriptTranslationGetDispatch(TestVideo): Tests for `translation` dispatch GET HTTP method. """ - non_en_file = _create_srt_file() + srt_file = _create_srt_file() DATA = """ <video show_captions="true" display_name="A Name" @@ -507,7 +604,7 @@ class TestStudioTranscriptTranslationGetDispatch(TestVideo): <transcript language="uk" src="{}"/> <transcript language="zh" src="{}"/> </video> - """.format(os.path.split(non_en_file.name)[1], u"å¡ž.srt".encode('utf8')) + """.format(os.path.split(srt_file.name)[1], u"å¡ž.srt".encode('utf8')) MODEL_DATA = {'data': DATA} @@ -523,12 +620,12 @@ class TestStudioTranscriptTranslationGetDispatch(TestVideo): self.assertEqual(response.status, '400 Bad Request') # Correct case: - filename = os.path.split(self.non_en_file.name)[1] - _upload_file(self.non_en_file, self.item_descriptor.location, filename) - self.non_en_file.seek(0) + filename = os.path.split(self.srt_file.name)[1] + _upload_file(self.srt_file, self.item_descriptor.location, filename) + self.srt_file.seek(0) request = Request.blank(u'translation/uk?filename={}'.format(filename)) response = self.item_descriptor.studio_transcript(request=request, dispatch='translation/uk') - self.assertEqual(response.body, self.non_en_file.read()) + self.assertEqual(response.body, self.srt_file.read()) self.assertEqual(response.headers['Content-Type'], 'application/x-subrip; charset=utf-8') self.assertEqual( response.headers['Content-Disposition'], @@ -537,12 +634,12 @@ class TestStudioTranscriptTranslationGetDispatch(TestVideo): self.assertEqual(response.headers['Content-Language'], 'uk') # Non ascii file name download: - self.non_en_file.seek(0) - _upload_file(self.non_en_file, self.item_descriptor.location, u'å¡ž.srt') - self.non_en_file.seek(0) + self.srt_file.seek(0) + _upload_file(self.srt_file, self.item_descriptor.location, u'å¡ž.srt') + self.srt_file.seek(0) request = Request.blank('translation/zh?filename={}'.format(u'å¡ž.srt'.encode('utf8'))) response = self.item_descriptor.studio_transcript(request=request, dispatch='translation/zh') - self.assertEqual(response.body, self.non_en_file.read()) + self.assertEqual(response.body, self.srt_file.read()) self.assertEqual(response.headers['Content-Type'], 'application/x-subrip; charset=utf-8') self.assertEqual(response.headers['Content-Disposition'], 'attachment; filename="å¡ž.srt"') self.assertEqual(response.headers['Content-Language'], 'zh') @@ -614,7 +711,7 @@ class TestGetTranscript(TestVideo): """ Make sure that `get_transcript` method works correctly """ - non_en_file = _create_srt_file() + srt_file = _create_srt_file() DATA = """ <video show_captions="true" display_name="A Name" @@ -624,7 +721,7 @@ class TestGetTranscript(TestVideo): <transcript language="uk" src="{}"/> <transcript language="zh" src="{}"/> </video> - """.format(os.path.split(non_en_file.name)[1], u"å¡ž.srt".encode('utf8')) + """.format(os.path.split(srt_file.name)[1], u"å¡ž.srt".encode('utf8')) MODEL_DATA = { 'data': DATA @@ -660,7 +757,8 @@ class TestGetTranscript(TestVideo): _upload_sjson_file(good_sjson, self.item.location) self.item.sub = _get_subs_id(good_sjson.name) - text, filename, mime_type = self.item.get_transcript() + transcripts = self.item.get_transcripts_info() + text, filename, mime_type = self.item.get_transcript(transcripts) expected_text = textwrap.dedent("""\ 0 @@ -697,7 +795,8 @@ class TestGetTranscript(TestVideo): _upload_sjson_file(good_sjson, self.item.location) self.item.sub = _get_subs_id(good_sjson.name) - text, filename, mime_type = self.item.get_transcript("txt") + transcripts = self.item.get_transcripts_info() + text, filename, mime_type = self.item.get_transcript(transcripts, transcript_format="txt") expected_text = textwrap.dedent("""\ Hi, welcome to Edx. Let's start with what is on your screen right now.""") @@ -708,14 +807,15 @@ class TestGetTranscript(TestVideo): def test_en_with_empty_sub(self): + transcripts = {"transcripts": {}, "sub": ""} # no self.sub, self.youttube_1_0 exist, but no file in assets with self.assertRaises(NotFoundError): - self.item.get_transcript() + self.item.get_transcript(transcripts) - # no self.sub and no self.youtube_1_0 + # no self.sub and no self.youtube_1_0, no non-en transcritps self.item.youtube_id_1_0 = None with self.assertRaises(ValueError): - self.item.get_transcript() + self.item.get_transcript(transcripts) # no self.sub but youtube_1_0 exists with file in assets good_sjson = _create_file(content=textwrap.dedent("""\ @@ -737,7 +837,7 @@ class TestGetTranscript(TestVideo): _upload_sjson_file(good_sjson, self.item.location) self.item.youtube_id_1_0 = _get_subs_id(good_sjson.name) - text, filename, mime_type = self.item.get_transcript() + text, filename, mime_type = self.item.get_transcript(transcripts) expected_text = textwrap.dedent("""\ 0 00:00:00,270 --> 00:00:02,720 @@ -755,10 +855,11 @@ class TestGetTranscript(TestVideo): def test_non_en_with_non_ascii_filename(self): self.item.transcript_language = 'zh' - self.non_en_file.seek(0) - _upload_file(self.non_en_file, self.item_descriptor.location, u"å¡ž.srt") + self.srt_file.seek(0) + _upload_file(self.srt_file, self.item_descriptor.location, u"å¡ž.srt") - text, filename, mime_type = self.item.get_transcript() + transcripts = self.item.get_transcripts_info() + text, filename, mime_type = self.item.get_transcript(transcripts) expected_text = textwrap.dedent(""" 0 00:00:00,12 --> 00:00:00,100 @@ -774,8 +875,9 @@ class TestGetTranscript(TestVideo): _upload_sjson_file(good_sjson, self.item.location) self.item.sub = _get_subs_id(good_sjson.name) + transcripts = self.item.get_transcripts_info() with self.assertRaises(ValueError): - self.item.get_transcript() + self.item.get_transcript(transcripts) def test_key_error(self): good_sjson = _create_file(content=""" @@ -794,5 +896,6 @@ class TestGetTranscript(TestVideo): _upload_sjson_file(good_sjson, self.item.location) self.item.sub = _get_subs_id(good_sjson.name) + transcripts = self.item.get_transcripts_info() with self.assertRaises(KeyError): - self.item.get_transcript() + self.item.get_transcript(transcripts) diff --git a/lms/djangoapps/courseware/tests/test_video_mongo.py b/lms/djangoapps/courseware/tests/test_video_mongo.py index c9b02696f383e26545f15c7d49da3324e3a32c9c..36c1eacb2ee0e1af6710c1b8ba7bf0edb1b72133 100644 --- a/lms/djangoapps/courseware/tests/test_video_mongo.py +++ b/lms/djangoapps/courseware/tests/test_video_mongo.py @@ -9,8 +9,9 @@ from nose.plugins.attrib import attr from django.conf import settings from django.test import TestCase +from django.test.utils import override_settings -from xmodule.video_module import create_youtube_string, VideoDescriptor +from xmodule.video_module import VideoDescriptor, bumper_utils, video_utils from xmodule.x_module import STUDENT_VIEW from xmodule.tests.test_video import VideoDescriptorTestBase from xmodule.tests.test_import import DummySystem @@ -31,43 +32,51 @@ class TestVideoYouTube(TestVideo): def test_video_constructor(self): """Make sure that all parameters extracted correctly from xml""" context = self.item_descriptor.render(STUDENT_VIEW).content - sources = json.dumps([u'example.mp4', u'example.webm']) + sources = [u'example.mp4', u'example.webm'] expected_context = { - 'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state', - 'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', False), 'branding_info': None, 'license': None, + 'bumper_metadata': 'null', 'cdn_eval': False, 'cdn_exp_group': None, - 'data_dir': getattr(self, 'data_dir', None), 'display_name': u'A Name', - 'end': 3610.0, - 'id': self.item_descriptor.location.html_id(), - 'show_captions': 'true', - 'handout': None, 'download_video_link': u'example.mp4', - 'sources': sources, - 'speed': 'null', - 'general_speed': 1.0, - 'start': 3603.0, - 'saved_video_position': 0.0, - 'sub': u'a_sub_file.srt.sjson', + 'handout': None, + 'id': self.item_descriptor.location.html_id(), + 'metadata': json.dumps(OrderedDict({ + "saveStateUrl": self.item_descriptor.xmodule_runtime.ajax_url + "/save_user_state", + "autoplay": False, + "streams": "0.75:jNCf2gIqpeE,1.00:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg", + "sub": "a_sub_file.srt.sjson", + "sources": sources, + "captionDataDir": None, + "showCaptions": "true", + "generalSpeed": 1.0, + "speed": None, + "savedVideoPosition": 0.0, + "start": 3603.0, + "end": 3610.0, + "transcriptLanguage": "en", + "transcriptLanguages": OrderedDict({"en": "English", "uk": u"УкраїнÑька"}), + "ytTestTimeout": 1500, + "ytApiUrl": "www.youtube.com/iframe_api", + "ytTestUrl": "gdata.youtube.com/feeds/api/videos/", + "transcriptTranslationUrl": self.item_descriptor.xmodule_runtime.handler_url( + self.item_descriptor, 'transcript', 'translation/__lang__' + ).rstrip('/?'), + "transcriptAvailableTranslationsUrl": self.item_descriptor.xmodule_runtime.handler_url( + self.item_descriptor, 'transcript', 'available_translations' + ).rstrip('/?'), + "autohideHtml5": False, + })), 'track': None, - 'youtube_streams': create_youtube_string(self.item_descriptor), - 'yt_test_timeout': 1500, - 'yt_api_url': 'www.youtube.com/iframe_api', - 'yt_test_url': 'gdata.youtube.com/feeds/api/videos/', 'transcript_download_format': 'srt', - 'transcript_download_formats_list': [{'display_name': 'SubRip (.srt) file', 'value': 'srt'}, {'display_name': 'Text (.txt) file', 'value': 'txt'}], - 'transcript_language': u'en', - 'transcript_languages': json.dumps(OrderedDict({"en": "English", "uk": u"УкраїнÑька"})), - 'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url( - self.item_descriptor, 'transcript', 'translation' - ).rstrip('/?'), - 'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url( - self.item_descriptor, 'transcript', 'available_translations' - ).rstrip('/?'), + 'transcript_download_formats_list': [ + {'display_name': 'SubRip (.srt) file', 'value': 'srt'}, + {'display_name': 'Text (.txt) file', 'value': 'txt'} + ], + 'poster': 'null', } self.assertEqual( @@ -100,43 +109,51 @@ class TestVideoNonYouTube(TestVideo): the template generates an empty string for the YouTube streams. """ context = self.item_descriptor.render(STUDENT_VIEW).content - sources = json.dumps([u'example.mp4', u'example.webm']) + sources = [u'example.mp4', u'example.webm'] expected_context = { - 'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state', 'branding_info': None, 'license': None, + 'bumper_metadata': 'null', 'cdn_eval': False, 'cdn_exp_group': None, - 'data_dir': getattr(self, 'data_dir', None), - 'show_captions': 'true', - 'handout': None, 'display_name': u'A Name', 'download_video_link': u'example.mp4', - 'end': 3610.0, + 'handout': None, 'id': self.item_descriptor.location.html_id(), - 'sources': sources, - 'speed': 'null', - 'general_speed': 1.0, - 'start': 3603.0, - 'saved_video_position': 0.0, - 'sub': u'a_sub_file.srt.sjson', + 'metadata': json.dumps(OrderedDict({ + "saveStateUrl": self.item_descriptor.xmodule_runtime.ajax_url + "/save_user_state", + "autoplay": False, + "streams": "1.00:3_yD_cEKoCk", + "sub": "a_sub_file.srt.sjson", + "sources": sources, + "captionDataDir": None, + "showCaptions": "true", + "generalSpeed": 1.0, + "speed": None, + "savedVideoPosition": 0.0, + "start": 3603.0, + "end": 3610.0, + "transcriptLanguage": "en", + "transcriptLanguages": OrderedDict({"en": "English"}), + "ytTestTimeout": 1500, + "ytApiUrl": "www.youtube.com/iframe_api", + "ytTestUrl": "gdata.youtube.com/feeds/api/videos/", + "transcriptTranslationUrl": self.item_descriptor.xmodule_runtime.handler_url( + self.item_descriptor, 'transcript', 'translation/__lang__' + ).rstrip('/?'), + "transcriptAvailableTranslationsUrl": self.item_descriptor.xmodule_runtime.handler_url( + self.item_descriptor, 'transcript', 'available_translations' + ).rstrip('/?'), + "autohideHtml5": False, + })), 'track': None, - 'youtube_streams': '1.00:3_yD_cEKoCk', - 'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', True), - 'yt_test_timeout': 1500, - 'yt_api_url': 'www.youtube.com/iframe_api', - 'yt_test_url': 'gdata.youtube.com/feeds/api/videos/', 'transcript_download_format': 'srt', - 'transcript_download_formats_list': [{'display_name': 'SubRip (.srt) file', 'value': 'srt'}, {'display_name': 'Text (.txt) file', 'value': 'txt'}], - 'transcript_language': u'en', - 'transcript_languages': '{"en": "English"}', - 'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url( - self.item_descriptor, 'transcript', 'translation' - ).rstrip('/?'), - 'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url( - self.item_descriptor, 'transcript', 'available_translations' - ).rstrip('/?') + 'transcript_download_formats_list': [ + {'display_name': 'SubRip (.srt) file', 'value': 'srt'}, + {'display_name': 'Text (.txt) file', 'value': 'txt'} + ], + 'poster': 'null', } self.assertEqual( @@ -157,6 +174,32 @@ class TestGetHtmlMethod(BaseTestXmodule): def setUp(self): super(TestGetHtmlMethod, self).setUp() self.setup_course() + self.default_metadata_dict = OrderedDict({ + "saveStateUrl": "", + "autoplay": settings.FEATURES.get('AUTOPLAY_VIDEOS', True), + "streams": "1.00:3_yD_cEKoCk", + "sub": "a_sub_file.srt.sjson", + "sources": '[]', + "captionDataDir": None, + "showCaptions": "true", + "generalSpeed": 1.0, + "speed": None, + "savedVideoPosition": 0.0, + "start": 3603.0, + "end": 3610.0, + "transcriptLanguage": "en", + "transcriptLanguages": OrderedDict({"en": "English"}), + "ytTestTimeout": 1500, + "ytApiUrl": "www.youtube.com/iframe_api", + "ytTestUrl": "gdata.youtube.com/feeds/api/videos/", + "transcriptTranslationUrl": self.item_descriptor.xmodule_runtime.handler_url( + self.item_descriptor, 'transcript', 'translation/__lang__' + ).rstrip('/?'), + "transcriptAvailableTranslationsUrl": self.item_descriptor.xmodule_runtime.handler_url( + self.item_descriptor, 'transcript', 'available_translations' + ).rstrip('/?'), + "autohideHtml5": False, + }) def test_get_html_track(self): SOURCE_XML = """ @@ -209,36 +252,31 @@ class TestGetHtmlMethod(BaseTestXmodule): 'transcripts': '<transcript language="uk" src="ukrainian.srt" />', }, ] - sources = json.dumps([u'example.mp4', u'example.webm']) + sources = [u'example.mp4', u'example.webm'] expected_context = { 'branding_info': None, 'license': None, + 'bumper_metadata': 'null', 'cdn_eval': False, 'cdn_exp_group': None, - 'data_dir': getattr(self, 'data_dir', None), - 'show_captions': 'true', - 'handout': None, 'display_name': u'A Name', 'download_video_link': u'example.mp4', - 'end': 3610.0, - 'id': None, - 'sources': sources, - 'start': 3603.0, - 'saved_video_position': 0.0, - 'sub': u'a_sub_file.srt.sjson', - 'speed': 'null', - 'general_speed': 1.0, - 'track': u'http://www.example.com/track', - 'youtube_streams': '1.00:3_yD_cEKoCk', - 'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', True), - 'yt_test_timeout': 1500, - 'yt_api_url': 'www.youtube.com/iframe_api', - 'yt_test_url': 'gdata.youtube.com/feeds/api/videos/', - 'transcript_download_formats_list': [{'display_name': 'SubRip (.srt) file', 'value': 'srt'}, {'display_name': 'Text (.txt) file', 'value': 'txt'}], + 'handout': None, + 'id': self.item_descriptor.location.html_id(), + 'metadata': '', + 'track': None, + 'transcript_download_format': 'srt', + 'transcript_download_formats_list': [ + {'display_name': 'SubRip (.srt) file', 'value': 'srt'}, + {'display_name': 'Text (.txt) file', 'value': 'txt'} + ], + 'poster': 'null', } for data in cases: + metadata = self.default_metadata_dict + metadata['sources'] = sources DATA = SOURCE_XML.format( download_track=data['download_track'], track=data['track'], @@ -252,22 +290,29 @@ class TestGetHtmlMethod(BaseTestXmodule): ).rstrip('/?') context = self.item_descriptor.render(STUDENT_VIEW).content - - expected_context.update({ - 'transcript_download_format': None if self.item_descriptor.track and self.item_descriptor.download_track else 'srt', - 'transcript_languages': '{"en": "English"}' if not data['transcripts'] else json.dumps({"uk": u'УкраїнÑька'}), - 'transcript_language': u'en' if not data['transcripts'] or data.get('sub') else u'uk', - 'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url( - self.item_descriptor, 'transcript', 'translation' + metadata.update({ + 'transcriptLanguages': {"en": "English"} if not data['transcripts'] else {"uk": u'УкраїнÑька'}, + 'transcriptLanguage': u'en' if not data['transcripts'] or data.get('sub') else u'uk', + 'transcriptTranslationUrl': self.item_descriptor.xmodule_runtime.handler_url( + self.item_descriptor, 'transcript', 'translation/__lang__' ).rstrip('/?'), - 'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url( + 'transcriptAvailableTranslationsUrl': self.item_descriptor.xmodule_runtime.handler_url( self.item_descriptor, 'transcript', 'available_translations' ).rstrip('/?'), - 'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state', - 'track': track_url if data['expected_track_url'] == u'a_sub_file.srt.sjson' else data['expected_track_url'], + 'saveStateUrl': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state', 'sub': data['sub'], + }) + expected_context.update({ + 'transcript_download_format': ( + None if self.item_descriptor.track and self.item_descriptor.download_track else 'srt' + ), + 'track': ( + track_url if data['expected_track_url'] == u'a_sub_file.srt.sjson' else data['expected_track_url'] + ), 'id': self.item_descriptor.location.html_id(), + 'metadata': json.dumps(metadata) }) + self.assertEqual( context, self.item_descriptor.xmodule_runtime.render_template('video.html', expected_context), @@ -295,7 +340,7 @@ class TestGetHtmlMethod(BaseTestXmodule): """, 'result': { 'download_video_link': u'example_source.mp4', - 'sources': json.dumps([u'example.mp4', u'example.webm']), + 'sources': [u'example.mp4', u'example.webm'], }, }, { @@ -307,7 +352,7 @@ class TestGetHtmlMethod(BaseTestXmodule): """, 'result': { 'download_video_link': u'example.mp4', - 'sources': json.dumps([u'example.mp4', u'example.webm']), + 'sources': [u'example.mp4', u'example.webm'], }, }, { @@ -326,7 +371,7 @@ class TestGetHtmlMethod(BaseTestXmodule): <source src="example.webm"/> """, 'result': { - 'sources': json.dumps([u'example.mp4', u'example.webm']), + 'sources': [u'example.mp4', u'example.webm'], }, }, ] @@ -334,31 +379,21 @@ class TestGetHtmlMethod(BaseTestXmodule): initial_context = { 'branding_info': None, 'license': None, + 'bumper_metadata': 'null', 'cdn_eval': False, 'cdn_exp_group': None, - 'data_dir': getattr(self, 'data_dir', None), - 'show_captions': 'true', - 'handout': None, 'display_name': u'A Name', - 'download_video_link': None, - 'end': 3610.0, - 'id': None, - 'sources': '[]', - 'speed': 'null', - 'general_speed': 1.0, - 'start': 3603.0, - 'saved_video_position': 0.0, - 'sub': u'a_sub_file.srt.sjson', + 'download_video_link': u'example.mp4', + 'handout': None, + 'id': self.item_descriptor.location.html_id(), + 'metadata': self.default_metadata_dict, 'track': None, - 'youtube_streams': '1.00:3_yD_cEKoCk', - 'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', True), - 'yt_test_timeout': 1500, - 'yt_api_url': 'www.youtube.com/iframe_api', - 'yt_test_url': 'gdata.youtube.com/feeds/api/videos/', 'transcript_download_format': 'srt', - 'transcript_download_formats_list': [{'display_name': 'SubRip (.srt) file', 'value': 'srt'}, {'display_name': 'Text (.txt) file', 'value': 'txt'}], - 'transcript_language': u'en', - 'transcript_languages': '{"en": "English"}', + 'transcript_download_formats_list': [ + {'display_name': 'SubRip (.srt) file', 'value': 'srt'}, + {'display_name': 'Text (.txt) file', 'value': 'txt'} + ], + 'poster': 'null', } for data in cases: @@ -371,17 +406,21 @@ class TestGetHtmlMethod(BaseTestXmodule): context = self.item_descriptor.render(STUDENT_VIEW).content expected_context = dict(initial_context) - expected_context.update({ - 'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url( - self.item_descriptor, 'transcript', 'translation' + expected_context['metadata'].update({ + 'transcriptTranslationUrl': self.item_descriptor.xmodule_runtime.handler_url( + self.item_descriptor, 'transcript', 'translation/__lang__' ).rstrip('/?'), - 'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url( + 'transcriptAvailableTranslationsUrl': self.item_descriptor.xmodule_runtime.handler_url( self.item_descriptor, 'transcript', 'available_translations' ).rstrip('/?'), - 'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state', + 'saveStateUrl': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state', + 'sources': data['result'].get('sources', []), + }) + expected_context.update({ 'id': self.item_descriptor.location.html_id(), + 'download_video_link': data['result'].get('download_video_link'), + 'metadata': json.dumps(expected_context['metadata']) }) - expected_context.update(data['result']) self.assertEqual( context, @@ -413,7 +452,7 @@ class TestGetHtmlMethod(BaseTestXmodule): 'edx_video_id': "meow", 'result': { 'download_video_link': u'example_source.mp4', - 'sources': json.dumps([u'example.mp4', u'example.webm']), + 'sources': [u'example.mp4', u'example.webm'], } } DATA = SOURCE_XML.format( @@ -469,39 +508,32 @@ class TestGetHtmlMethod(BaseTestXmodule): 'result': { 'download_video_link': None, # make sure the desktop_mp4 url is included as part of the alternative sources. - 'sources': json.dumps([u'example.mp4', u'example.webm', u'http://www.meowmix.com']), + 'sources': [u'example.mp4', u'example.webm', u'http://www.meowmix.com'], } } # Video found for edx_video_id + metadata = self.default_metadata_dict + metadata['autoplay'] = False + metadata['sources'] = "" initial_context = { 'branding_info': None, 'license': None, + 'bumper_metadata': 'null', 'cdn_eval': False, 'cdn_exp_group': None, - 'data_dir': getattr(self, 'data_dir', None), - 'show_captions': 'true', - 'handout': None, 'display_name': u'A Name', - 'download_video_link': None, - 'end': 3610.0, - 'id': None, - 'sources': '[]', - 'speed': 'null', - 'general_speed': 1.0, - 'start': 3603.0, - 'saved_video_position': 0.0, - 'sub': u'a_sub_file.srt.sjson', + 'download_video_link': u'example.mp4', + 'handout': None, + 'id': self.item_descriptor.location.html_id(), 'track': None, - 'youtube_streams': '1.00:3_yD_cEKoCk', - 'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', True), - 'yt_test_timeout': 1500, - 'yt_api_url': 'www.youtube.com/iframe_api', - 'yt_test_url': 'gdata.youtube.com/feeds/api/videos/', 'transcript_download_format': 'srt', - 'transcript_download_formats_list': [{'display_name': 'SubRip (.srt) file', 'value': 'srt'}, {'display_name': 'Text (.txt) file', 'value': 'txt'}], - 'transcript_language': u'en', - 'transcript_languages': '{"en": "English"}', + 'transcript_download_formats_list': [ + {'display_name': 'SubRip (.srt) file', 'value': 'srt'}, + {'display_name': 'Text (.txt) file', 'value': 'txt'} + ], + 'poster': 'null', + 'metadata': metadata } DATA = SOURCE_XML.format( @@ -514,17 +546,21 @@ class TestGetHtmlMethod(BaseTestXmodule): context = self.item_descriptor.render(STUDENT_VIEW).content expected_context = dict(initial_context) - expected_context.update({ - 'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url( - self.item_descriptor, 'transcript', 'translation' + expected_context['metadata'].update({ + 'transcriptTranslationUrl': self.item_descriptor.xmodule_runtime.handler_url( + self.item_descriptor, 'transcript', 'translation/__lang__' ).rstrip('/?'), - 'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url( + 'transcriptAvailableTranslationsUrl': self.item_descriptor.xmodule_runtime.handler_url( self.item_descriptor, 'transcript', 'available_translations' ).rstrip('/?'), - 'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state', + 'saveStateUrl': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state', + 'sources': data['result']['sources'], + }) + expected_context.update({ 'id': self.item_descriptor.location.html_id(), + 'download_video_link': data['result']['download_video_link'], + 'metadata': json.dumps(expected_context['metadata']) }) - expected_context.update(data['result']) self.assertEqual( context, @@ -579,42 +615,32 @@ class TestGetHtmlMethod(BaseTestXmodule): 'result': { 'download_video_link': u'http://fake-video.edx.org/thundercats.mp4', # make sure the urls for the various encodings are included as part of the alternative sources. - 'sources': json.dumps( - [u'example.mp4', u'example.webm'] + - [video['url'] for video in encoded_videos] - ), + 'sources': [u'example.mp4', u'example.webm'] + + [video['url'] for video in encoded_videos], } } # Video found for edx_video_id + metadata = self.default_metadata_dict + metadata['sources'] = "" initial_context = { 'branding_info': None, 'license': None, + 'bumper_metadata': 'null', 'cdn_eval': False, 'cdn_exp_group': None, - 'data_dir': getattr(self, 'data_dir', None), - 'show_captions': 'true', - 'handout': None, 'display_name': u'A Name', - 'download_video_link': None, - 'end': 3610.0, - 'id': None, - 'sources': '[]', - 'speed': 'null', - 'general_speed': 1.0, - 'start': 3603.0, - 'saved_video_position': 0.0, - 'sub': u'a_sub_file.srt.sjson', + 'download_video_link': u'example.mp4', + 'handout': None, + 'id': self.item_descriptor.location.html_id(), 'track': None, - 'youtube_streams': '1.00:3_yD_cEKoCk', - 'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', True), - 'yt_test_timeout': 1500, - 'yt_api_url': 'www.youtube.com/iframe_api', - 'yt_test_url': 'gdata.youtube.com/feeds/api/videos/', 'transcript_download_format': 'srt', - 'transcript_download_formats_list': [{'display_name': 'SubRip (.srt) file', 'value': 'srt'}, {'display_name': 'Text (.txt) file', 'value': 'txt'}], - 'transcript_language': u'en', - 'transcript_languages': '{"en": "English"}', + 'transcript_download_formats_list': [ + {'display_name': 'SubRip (.srt) file', 'value': 'srt'}, + {'display_name': 'Text (.txt) file', 'value': 'txt'} + ], + 'poster': 'null', + 'metadata': metadata, } DATA = SOURCE_XML.format( @@ -627,17 +653,21 @@ class TestGetHtmlMethod(BaseTestXmodule): context = self.item_descriptor.render(STUDENT_VIEW).content expected_context = dict(initial_context) - expected_context.update({ - 'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url( - self.item_descriptor, 'transcript', 'translation' + expected_context['metadata'].update({ + 'transcriptTranslationUrl': self.item_descriptor.xmodule_runtime.handler_url( + self.item_descriptor, 'transcript', 'translation/__lang__' ).rstrip('/?'), - 'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url( + 'transcriptAvailableTranslationsUrl': self.item_descriptor.xmodule_runtime.handler_url( self.item_descriptor, 'transcript', 'available_translations' ).rstrip('/?'), - 'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state', + 'saveStateUrl': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state', + 'sources': data['result']['sources'], + }) + expected_context.update({ 'id': self.item_descriptor.location.html_id(), + 'download_video_link': data['result']['download_video_link'], + 'metadata': json.dumps(expected_context['metadata']) }) - expected_context.update(data['result']) self.assertEqual( context, @@ -690,12 +720,10 @@ class TestGetHtmlMethod(BaseTestXmodule): """, 'result': { 'download_video_link': u'example_source.mp4', - 'sources': json.dumps( - [ - u'http://cdn_example.com/example.mp4', - u'http://cdn_example.com/example.webm' - ] - ), + 'sources': [ + u'http://cdn_example.com/example.mp4', + u'http://cdn_example.com/example.webm' + ], }, } @@ -712,31 +740,21 @@ class TestGetHtmlMethod(BaseTestXmodule): 'url': 'http://www.xuetangx.com' }, 'license': None, + 'bumper_metadata': 'null', 'cdn_eval': False, 'cdn_exp_group': None, - 'data_dir': getattr(self, 'data_dir', None), - 'show_captions': 'true', - 'handout': None, 'display_name': u'A Name', 'download_video_link': None, - 'end': 3610.0, + 'handout': None, 'id': None, - 'sources': '[]', - 'speed': 'null', - 'general_speed': 1.0, - 'start': 3603.0, - 'saved_video_position': 0.0, - 'sub': u'a_sub_file.srt.sjson', + 'metadata': self.default_metadata_dict, 'track': None, - 'youtube_streams': '1.00:3_yD_cEKoCk', - 'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', True), - 'yt_test_timeout': 1500, - 'yt_api_url': 'www.youtube.com/iframe_api', - 'yt_test_url': 'gdata.youtube.com/feeds/api/videos/', 'transcript_download_format': 'srt', - 'transcript_download_formats_list': [{'display_name': 'SubRip (.srt) file', 'value': 'srt'}, {'display_name': 'Text (.txt) file', 'value': 'txt'}], - 'transcript_language': u'en', - 'transcript_languages': '{"en": "English"}', + 'transcript_download_formats_list': [ + {'display_name': 'SubRip (.srt) file', 'value': 'srt'}, + {'display_name': 'Text (.txt) file', 'value': 'txt'} + ], + 'poster': 'null', } for data in cases: @@ -748,21 +766,23 @@ class TestGetHtmlMethod(BaseTestXmodule): ) self.initialize_module(data=DATA) self.item_descriptor.xmodule_runtime.user_location = 'CN' - context = self.item_descriptor.render('student_view').content - expected_context = dict(initial_context) - expected_context.update({ - 'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url( - self.item_descriptor, 'transcript', 'translation' + expected_context['metadata'].update({ + 'transcriptTranslationUrl': self.item_descriptor.xmodule_runtime.handler_url( + self.item_descriptor, 'transcript', 'translation/__lang__' ).rstrip('/?'), - 'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url( + 'transcriptAvailableTranslationsUrl': self.item_descriptor.xmodule_runtime.handler_url( self.item_descriptor, 'transcript', 'available_translations' ).rstrip('/?'), - 'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state', + 'saveStateUrl': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state', + 'sources': data['result'].get('sources', []), + }) + expected_context.update({ 'id': self.item_descriptor.location.html_id(), + 'download_video_link': data['result'].get('download_video_link'), + 'metadata': json.dumps(expected_context['metadata']) }) - expected_context.update(data['result']) self.assertEqual( context, @@ -948,3 +968,125 @@ class VideoDescriptorTest(TestCase, VideoDescriptorTestBase): VideoDescriptor.from_xml(xml_data, module_system, id_generator=Mock()) with self.assertRaises(ValVideoNotFoundError): get_video_info("test_edx_video_id") + + +class TestVideoWithBumper(TestVideo): + """ + Tests rendered content in presence of video bumper. + """ + CATEGORY = "video" + METADATA = {} + FEATURES = settings.FEATURES + + @patch('xmodule.video_module.bumper_utils.get_bumper_settings') + def test_is_bumper_enabled(self, get_bumper_settings): + """ + Check that bumper is (not)shown if ENABLE_VIDEO_BUMPER is (False)True + + Assume that bumper settings are correct. + """ + self.FEATURES.update({ + "SHOW_BUMPER_PERIODICITY": 1, + "ENABLE_VIDEO_BUMPER": True, + }) + + get_bumper_settings.return_value = { + "video_id": "edx_video_id", + "transcripts": {}, + } + with override_settings(FEATURES=self.FEATURES): + self.assertTrue(bumper_utils.is_bumper_enabled(self.item_descriptor)) + + self.FEATURES.update({"ENABLE_VIDEO_BUMPER": False}) + + with override_settings(FEATURES=self.FEATURES): + self.assertFalse(bumper_utils.is_bumper_enabled(self.item_descriptor)) + + @patch('xmodule.video_module.bumper_utils.is_bumper_enabled') + @patch('xmodule.video_module.bumper_utils.get_bumper_settings') + @patch('edxval.api.get_urls_for_profiles') + def test_bumper_metadata(self, get_url_for_profiles, get_bumper_settings, is_bumper_enabled): + """ + Test content with rendered bumper metadata. + """ + get_url_for_profiles.return_value = { + "desktop_mp4": "http://test_bumper.mp4", + "desktop_webm": "", + } + + get_bumper_settings.return_value = { + "video_id": "edx_video_id", + "transcripts": {}, + } + + is_bumper_enabled.return_value = True + + content = self.item_descriptor.render(STUDENT_VIEW).content + sources = [u'example.mp4', u'example.webm'] + expected_context = { + 'branding_info': None, + 'license': None, + 'bumper_metadata': json.dumps(OrderedDict({ + 'saveStateUrl': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state', + "showCaptions": "true", + "sources": ["http://test_bumper.mp4"], + 'streams': '', + "transcriptLanguage": "en", + "transcriptLanguages": {"en": "English"}, + "transcriptTranslationUrl": video_utils.set_query_parameter( + self.item_descriptor.xmodule_runtime.handler_url( + self.item_descriptor, 'transcript', 'translation/__lang__' + ).rstrip('/?'), 'is_bumper', 1 + ), + "transcriptAvailableTranslationsUrl": video_utils.set_query_parameter( + self.item_descriptor.xmodule_runtime.handler_url( + self.item_descriptor, 'transcript', 'available_translations' + ).rstrip('/?'), 'is_bumper', 1 + ), + })), + 'cdn_eval': False, + 'cdn_exp_group': None, + 'display_name': u'A Name', + 'download_video_link': u'example.mp4', + 'handout': None, + 'id': self.item_descriptor.location.html_id(), + 'metadata': json.dumps(OrderedDict({ + "saveStateUrl": self.item_descriptor.xmodule_runtime.ajax_url + "/save_user_state", + "autoplay": False, + "streams": "0.75:jNCf2gIqpeE,1.00:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg", + "sub": "a_sub_file.srt.sjson", + "sources": sources, + "captionDataDir": None, + "showCaptions": "true", + "generalSpeed": 1.0, + "speed": None, + "savedVideoPosition": 0.0, + "start": 3603.0, + "end": 3610.0, + "transcriptLanguage": "en", + "transcriptLanguages": OrderedDict({"en": "English", "uk": u"УкраїнÑька"}), + "ytTestTimeout": 1500, + "ytApiUrl": "www.youtube.com/iframe_api", + "ytTestUrl": "gdata.youtube.com/feeds/api/videos/", + "transcriptTranslationUrl": self.item_descriptor.xmodule_runtime.handler_url( + self.item_descriptor, 'transcript', 'translation/__lang__' + ).rstrip('/?'), + "transcriptAvailableTranslationsUrl": self.item_descriptor.xmodule_runtime.handler_url( + self.item_descriptor, 'transcript', 'available_translations' + ).rstrip('/?'), + "autohideHtml5": False, + })), + 'track': None, + 'transcript_download_format': 'srt', + 'transcript_download_formats_list': [ + {'display_name': 'SubRip (.srt) file', 'value': 'srt'}, + {'display_name': 'Text (.txt) file', 'value': 'txt'} + ], + 'poster': json.dumps(OrderedDict({ + "url": "http://img.youtube.com/vi/ZwkTiUPN0mg/0.jpg", + "type": "youtube" + })) + } + + expected_content = self.item_descriptor.xmodule_runtime.render_template('video.html', expected_context) + self.assertEqual(content, expected_content) diff --git a/lms/djangoapps/mobile_api/video_outlines/serializers.py b/lms/djangoapps/mobile_api/video_outlines/serializers.py index 73f7089df695b7ea37a2bb4554dd03cfa1141b42..167abf3e23dca2becf0d6da5119ee050a101674f 100644 --- a/lms/djangoapps/mobile_api/video_outlines/serializers.py +++ b/lms/djangoapps/mobile_api/video_outlines/serializers.py @@ -206,7 +206,8 @@ def video_summary(video_profiles, course_id, video_descriptor, request, local_ca size = default_encoded_video.get('file_size', 0) # Transcripts... - transcript_langs = video_descriptor.available_translations(verify_assets=False) + transcripts_info = video_descriptor.get_transcripts_info() + transcript_langs = video_descriptor.available_translations(transcripts_info, verify_assets=False) transcripts = { lang: reverse( @@ -227,7 +228,7 @@ def video_summary(video_profiles, course_id, video_descriptor, request, local_ca "duration": duration, "size": size, "transcripts": transcripts, - "language": video_descriptor.get_default_transcript_language(), + "language": video_descriptor.get_default_transcript_language(transcripts_info), "encoded_videos": video_data.get('profiles') } ret.update(always_available_data) diff --git a/lms/djangoapps/mobile_api/video_outlines/views.py b/lms/djangoapps/mobile_api/video_outlines/views.py index ae1d7361c6a363e973c5e39848fb60624b990cb9..f0a25a77af84095a385147ccf974e0b226eb1281 100644 --- a/lms/djangoapps/mobile_api/video_outlines/views.py +++ b/lms/djangoapps/mobile_api/video_outlines/views.py @@ -119,7 +119,8 @@ class VideoTranscripts(generics.RetrieveAPIView): ) try: video_descriptor = modulestore().get_item(usage_key) - content, filename, mimetype = video_descriptor.get_transcript(lang=lang) + transcripts = video_descriptor.get_transcripts_info() + content, filename, mimetype = video_descriptor.get_transcript(transcripts, lang=lang) except (NotFoundError, ValueError, KeyError): raise Http404(u"Transcript not found for {}, lang: {}".format(block_id, lang)) diff --git a/lms/envs/bok_choy.py b/lms/envs/bok_choy.py index 3a1c1a450ede47c905f4074704433a736e63ecd6..ce93aac8da5f9dcaa91def1de145a550df1bb9f2 100644 --- a/lms/envs/bok_choy.py +++ b/lms/envs/bok_choy.py @@ -123,6 +123,11 @@ FEATURES['ENABLE_MAX_FAILED_LOGIN_ATTEMPTS'] = False FEATURES['SQUELCH_PII_IN_LOGS'] = False FEATURES['PREVENT_CONCURRENT_LOGINS'] = False FEATURES['ADVANCED_SECURITY'] = False + +FEATURES['ENABLE_MOBILE_REST_API'] = True # Show video bumper in LMS +FEATURES['ENABLE_VIDEO_BUMPER'] = True # Show video bumper in LMS +FEATURES['SHOW_BUMPER_PERIODICITY'] = 1 + PASSWORD_MIN_LENGTH = None PASSWORD_COMPLEXITY = {} diff --git a/lms/envs/common.py b/lms/envs/common.py index 3e0d69b7d0c8a7db9da4f46fc7faf348a6f2b25f..74dac034260ef0f585b18945406e86b0a965871f 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -392,6 +392,13 @@ FEATURES = { # Teams feature 'ENABLE_TEAMS': False, + + # Show video bumper in LMS + 'ENABLE_VIDEO_BUMPER': False, + + # How many seconds to show the bumper again, default is 7 days: + 'SHOW_BUMPER_PERIODICITY': 7 * 24 * 3600, + } # Ignore static asset files on import which match this pattern @@ -1665,6 +1672,8 @@ YOUTUBE = { 'v': 'set_youtube_id_of_11_symbols_here', }, }, + + 'IMAGE_API': 'http://img.youtube.com/vi/{youtube_id}/0.jpg', # /maxresdefault.jpg for 1920*1080 } ################################### APPS ###################################### diff --git a/lms/templates/video.html b/lms/templates/video.html index 9936b97b7d4be0a418f28399ee81ca3fe13872f4..48c069a8739a4a099de8b259b0a4597271ce4f35 100644 --- a/lms/templates/video.html +++ b/lms/templates/video.html @@ -7,47 +7,9 @@ <div id="video_${id}" class="video closed" - - data-streams="${youtube_streams}" - - % if sub: - data-sub="${sub}" - % endif - % if autoplay: - data-autoplay="${autoplay}" - % endif - - data-sources='${sources}' - data-save-state-url="${ajax_url}" - data-caption-data-dir="${data_dir}" - data-show-captions="${show_captions}" - data-general-speed="${general_speed}" - data-speed="${speed}" - data-saved-video-position="${saved_video_position}" - data-start="${start}" - data-end="${end}" - data-transcript-language="${transcript_language}" - data-transcript-languages='${transcript_languages}' - data-autoplay="${autoplay}" - data-yt-test-timeout="${yt_test_timeout}" - data-yt-api-url="${yt_api_url}" - data-yt-test-url="${yt_test_url}" - data-transcript-translation-url="${transcript_translation_url}" - data-transcript-available-translations-url="${transcript_available_translations_url}" - - ## For now, the option "data-autohide-html5" is hard coded. This option - ## either enables or disables autohiding of controls and captions on mouse - ## inactivity. If set to true, controls and captions will autohide for - ## HTML5 sources (non-YouTube) after a period of mouse inactivity over the - ## whole video. When the mouse moves (or a key is pressed while any part of - ## the video player is focused), the captions and controls will be shown - ## once again. - ## - ## There is no option in the "Advanced Editor" to set this option. However, - ## this option will have an effect if changed to "True". The code on - ## front-end exists. - data-autohide-html5="False" - + data-metadata='${metadata}' + data-bumper-metadata='${bumper_metadata}' + data-poster='${poster}' tabindex="-1" > <div class="focus_grabber first"></div> @@ -65,41 +27,13 @@ </section> <div class="video-player-post"></div> <section class="video-controls is-hidden"> - <div class="slider" title="${_('Video position')}"></div> <div> - <ul class="vcr"> - <li><a class="video_control" href="#" title="${_('Play')}" role="button" aria-disabled="false"></a></li> - <li><div class="vidtime">0:00 / 0:00</div></li> - </ul> - <div class="secondary-controls"> - <div class="speeds menu-container"> - <a class="speed-button" href="#" title="${_('Speeds')}" role="button" aria-disabled="false"> - <span class="label">${_('Speed')}</span> - <span class="value"></span> - </a> - <ol class="video-speeds menu" role="menu"></ol> - </div> - <div class="volume"> - <a href="#" role="button" aria-disabled="false" title="${_('Volume')}" aria-label="${_('Click on this button to mute or unmute this video or press UP or DOWN buttons to increase or decrease volume level.')}"></a> - <div role="presentation" class="volume-slider-container"> - <div class="volume-slider"></div> - </div> - </div> - <a href="#" class="add-fullscreen" title="${_('Fill browser')}" role="button" aria-disabled="false">${_('Fill browser')}</a> - <a href="#" class="quality-control is-hidden" title="${_('HD off')}" role="button" aria-disabled="false">${_('HD off')}</a> - - <div class="lang menu-container"> - <a href="#" class="hide-subtitles" title="${_('Turn off captions')}" role="button" aria-disabled="false">${_('Turn off captions')}</a> - </div> - </div> + <div class="vcr"><div class="vidtime">0:00 / 0:00</div></div> + <div class="secondary-controls"></div> </div> </section> <a class="nav-skip sr" id="before-transcript_${id}" href="#after-transcript_${id}">${_('Skip to end of transcript.')}</a> </article> - - <ol id="transcript-captions" class="subtitles" tabindex="0" role="group" aria-label="${_('Activating an item in this group will spool the video to the corresponding time point. To skip transcript, go to previous item.')}"> - <li></li> - </ol> </div> <a class="nav-skip sr" id="after-transcript_${id}" href="#before-transcript_${id}">${_('Go back to start of transcript.')}</a> @@ -116,8 +50,8 @@ % if transcript_download_format: <a href="${track}">${_('Download transcript')}</a> <div class="a11y-menu-container"> - <a class="a11y-menu-button" href="#" title="${'.' + transcript_download_format}">${'.' + transcript_download_format}</a> - <ol class="a11y-menu-list"> + <a class="a11y-menu-button" href="#" title="${'.' + transcript_download_format}" role="button" aria-disabled="false">${'.' + transcript_download_format}</a> + <ol class="a11y-menu-list" role="menu"> % for item in transcript_download_formats_list: % if item['value'] == transcript_download_format: <li class="a11y-menu-item active"> @@ -126,7 +60,7 @@ % endif ## This is necessary so we don't scrape 'display_name' as a string. <% dname = item['display_name'] %> - <a class="a11y-menu-item-link" href="#${item['value']}" title="${_(dname)}" data-value="${item['value']}"> + <a class="a11y-menu-item-link" href="#${item['value']}" title="${_(dname)}" data-value="${item['value']}" role="menuitem" aria-disabled="false"> ${_(dname)} </a> </li>