From a770e34bec6b339776f64f46ad09faf8659d48e6 Mon Sep 17 00:00:00 2001 From: Julian Arni Date: Wed, 6 Feb 2013 20:36:44 -0500 Subject: [PATCH 01/75] Adding multiple-choice loncapa integration. --- common/lib/capa/capa/responsetypes.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 78c986a963..1ecba36d50 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -632,8 +632,10 @@ class MultipleChoiceResponse(LoncapaResponse): # define correct choices (after calling secondary setup) xml = self.xml - cxml = xml.xpath('//*[@id=$id]//choice[@correct="true"]', id=xml.get('id')) - self.correct_choices = [contextualize_text(choice.get('name'), self.context) for choice in cxml] + cxml = xml.xpath('//*[@id=$id]//choice', id=xml.get('id')) + self.correct_choices = [contextualize_text(choice.get('name'), + self.context) for choice in cxml if + contextualize_text(choice.get('correct'), self.context) == "true"] def mc_setup_response(self): ''' From 8a75e1ad19894101863bfbcdb3814955488b9d98 Mon Sep 17 00:00:00 2001 From: Julian Arni Date: Thu, 7 Feb 2013 14:50:18 -0500 Subject: [PATCH 02/75] Added comment --- common/lib/capa/capa/responsetypes.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 1ecba36d50..0ecbfdc55d 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -633,9 +633,11 @@ class MultipleChoiceResponse(LoncapaResponse): # define correct choices (after calling secondary setup) xml = self.xml cxml = xml.xpath('//*[@id=$id]//choice', id=xml.get('id')) - self.correct_choices = [contextualize_text(choice.get('name'), - self.context) for choice in cxml if - contextualize_text(choice.get('correct'), self.context) == "true"] + # contextualize correct attribute and then select ones for which + # correct = "true" + self.correct_choices = [contextualize_text(choice.get('name'), self.context) + for choice in cxml + if contextualize_text(choice.get('correct'), self.context) == "true"] def mc_setup_response(self): ''' From f39a98f9baee2aceedfc275fa316b3362fecbb0e Mon Sep 17 00:00:00 2001 From: John Hess Date: Thu, 7 Feb 2013 16:14:42 -0500 Subject: [PATCH 03/75] Revert "Protex now loads correctly in every scenario" --- actually breaks grading, though it did fix loading This reverts commit 2627a2f12ce8ba09d0c72c186bd959afe18297d7. --- common/static/js/capa/design-protein-2d.js | 34 +- ...0950A947CFA1ACD8CC83306B9515908.cache.html | 750 +++++++++++++++++ ...096DE2845FBF7D7D836936320AD0AC4.cache.html | 746 +++++++++++++++++ ...56E8E1EA8930F0EA56B8973ACF7A92B.cache.html | 735 +++++++++++++++++ ...9CC89519B0E1FCB47B935AC9FE13D7B.cache.html | 743 ----------------- ...CE0BC9042E03326D1C86C810404FE4A.cache.html | 742 +++++++++++++++++ ...E05B1CD5BFCAF7D53C7C64D84318178.cache.html | 750 ----------------- ...9DCE0145624582CBBEEA35B857D07CD.cache.html | 759 +++++++++++++++++ ...D83FC6A58D4713A62FE989ECFDECF12.cache.html | 742 +++++++++++++++++ ...824A958AB642DC2213DFFDAC640BEAA.cache.html | 760 ------------------ ...9267DE8FB02F8B995B4A58C66C76E29.cache.html | 758 ----------------- ...275492F7098103BCB05F4F86ABF6218.cache.html | 750 ----------------- ...3301B0E65F38C7FCF2EF3764B3BB0B6.cache.html | 754 ----------------- .../static/js/capa/protex/protex.nocache.js | 4 +- 14 files changed, 4481 insertions(+), 4546 deletions(-) create mode 100644 common/static/js/capa/protex/00950A947CFA1ACD8CC83306B9515908.cache.html create mode 100644 common/static/js/capa/protex/0096DE2845FBF7D7D836936320AD0AC4.cache.html create mode 100644 common/static/js/capa/protex/356E8E1EA8930F0EA56B8973ACF7A92B.cache.html delete mode 100644 common/static/js/capa/protex/39CC89519B0E1FCB47B935AC9FE13D7B.cache.html create mode 100644 common/static/js/capa/protex/5CE0BC9042E03326D1C86C810404FE4A.cache.html delete mode 100644 common/static/js/capa/protex/6E05B1CD5BFCAF7D53C7C64D84318178.cache.html create mode 100644 common/static/js/capa/protex/89DCE0145624582CBBEEA35B857D07CD.cache.html create mode 100644 common/static/js/capa/protex/8D83FC6A58D4713A62FE989ECFDECF12.cache.html delete mode 100644 common/static/js/capa/protex/C824A958AB642DC2213DFFDAC640BEAA.cache.html delete mode 100644 common/static/js/capa/protex/D9267DE8FB02F8B995B4A58C66C76E29.cache.html delete mode 100644 common/static/js/capa/protex/F275492F7098103BCB05F4F86ABF6218.cache.html delete mode 100644 common/static/js/capa/protex/F3301B0E65F38C7FCF2EF3764B3BB0B6.cache.html diff --git a/common/static/js/capa/design-protein-2d.js b/common/static/js/capa/design-protein-2d.js index c0db798e2c..18945a401a 100644 --- a/common/static/js/capa/design-protein-2d.js +++ b/common/static/js/capa/design-protein-2d.js @@ -7,39 +7,15 @@ if (typeof(protex) !== "undefined" && protex) { protex.onInjectionDone("protex"); } - /*if (typeof(protex) !== "undefined") { - //initializeProtex(); - }*/ + if (typeof(protex) !== "undefined") { + initializeProtex(); + } else { setTimeout(function() { waitForProtex(); }, timeout); } } - //NOTE: - // Protex uses three global functions: - // protexSetTargetShape (exported from GWT) - // exported protexCheckAnswer (exported from GWT) - // It calls protexIsReady with a deferred command when it has finished - // initialization and has drawn itself - - protexIsReady = function() { - //Load target shape - var target_shape = $('#target_shape').val(); - protexSetTargetShape(target_shape); - - //Get answer from protex and store it into the hidden input field - //when Check button is clicked - var problem = $('#protex_container').parents('.problem'); - var check_button = problem.find('input.check'); - var input_field = problem.find('input[type=hidden]'); - check_button.on('click', function() { - var protex_answer = protexCheckAnswer(); - var value = {protex_answer: protex_answer}; - input_field.val(JSON.stringify(value)); - }); - }; - - /*function initializeProtex() { + function initializeProtex() { //Check to see if the two exported GWT functions protexSetTargetShape // and protexCheckAnswer have been appended to global scope -- this //happens at the end of onModuleLoad() in GWT @@ -69,5 +45,5 @@ else { setTimeout(function() {initializeProtex(); }, timeout); } - }*/ + } }).call(this); diff --git a/common/static/js/capa/protex/00950A947CFA1ACD8CC83306B9515908.cache.html b/common/static/js/capa/protex/00950A947CFA1ACD8CC83306B9515908.cache.html new file mode 100644 index 0000000000..c236939b62 --- /dev/null +++ b/common/static/js/capa/protex/00950A947CFA1ACD8CC83306B9515908.cache.html @@ -0,0 +1,750 @@ + + + + \ No newline at end of file diff --git a/common/static/js/capa/protex/0096DE2845FBF7D7D836936320AD0AC4.cache.html b/common/static/js/capa/protex/0096DE2845FBF7D7D836936320AD0AC4.cache.html new file mode 100644 index 0000000000..4bf742011f --- /dev/null +++ b/common/static/js/capa/protex/0096DE2845FBF7D7D836936320AD0AC4.cache.html @@ -0,0 +1,746 @@ + + + + \ No newline at end of file diff --git a/common/static/js/capa/protex/356E8E1EA8930F0EA56B8973ACF7A92B.cache.html b/common/static/js/capa/protex/356E8E1EA8930F0EA56B8973ACF7A92B.cache.html new file mode 100644 index 0000000000..5b7a580be5 --- /dev/null +++ b/common/static/js/capa/protex/356E8E1EA8930F0EA56B8973ACF7A92B.cache.html @@ -0,0 +1,735 @@ + + + + \ No newline at end of file diff --git a/common/static/js/capa/protex/39CC89519B0E1FCB47B935AC9FE13D7B.cache.html b/common/static/js/capa/protex/39CC89519B0E1FCB47B935AC9FE13D7B.cache.html deleted file mode 100644 index e5db692aac..0000000000 --- a/common/static/js/capa/protex/39CC89519B0E1FCB47B935AC9FE13D7B.cache.html +++ /dev/null @@ -1,743 +0,0 @@ - - - - \ No newline at end of file diff --git a/common/static/js/capa/protex/5CE0BC9042E03326D1C86C810404FE4A.cache.html b/common/static/js/capa/protex/5CE0BC9042E03326D1C86C810404FE4A.cache.html new file mode 100644 index 0000000000..b5b610878c --- /dev/null +++ b/common/static/js/capa/protex/5CE0BC9042E03326D1C86C810404FE4A.cache.html @@ -0,0 +1,742 @@ + + + + \ No newline at end of file diff --git a/common/static/js/capa/protex/6E05B1CD5BFCAF7D53C7C64D84318178.cache.html b/common/static/js/capa/protex/6E05B1CD5BFCAF7D53C7C64D84318178.cache.html deleted file mode 100644 index ce6b444862..0000000000 --- a/common/static/js/capa/protex/6E05B1CD5BFCAF7D53C7C64D84318178.cache.html +++ /dev/null @@ -1,750 +0,0 @@ - - - - \ No newline at end of file diff --git a/common/static/js/capa/protex/89DCE0145624582CBBEEA35B857D07CD.cache.html b/common/static/js/capa/protex/89DCE0145624582CBBEEA35B857D07CD.cache.html new file mode 100644 index 0000000000..501abe3613 --- /dev/null +++ b/common/static/js/capa/protex/89DCE0145624582CBBEEA35B857D07CD.cache.html @@ -0,0 +1,759 @@ + + + + \ No newline at end of file diff --git a/common/static/js/capa/protex/8D83FC6A58D4713A62FE989ECFDECF12.cache.html b/common/static/js/capa/protex/8D83FC6A58D4713A62FE989ECFDECF12.cache.html new file mode 100644 index 0000000000..010508a13d --- /dev/null +++ b/common/static/js/capa/protex/8D83FC6A58D4713A62FE989ECFDECF12.cache.html @@ -0,0 +1,742 @@ + + + + \ No newline at end of file diff --git a/common/static/js/capa/protex/C824A958AB642DC2213DFFDAC640BEAA.cache.html b/common/static/js/capa/protex/C824A958AB642DC2213DFFDAC640BEAA.cache.html deleted file mode 100644 index f84a8b9822..0000000000 --- a/common/static/js/capa/protex/C824A958AB642DC2213DFFDAC640BEAA.cache.html +++ /dev/null @@ -1,760 +0,0 @@ - - - - \ No newline at end of file diff --git a/common/static/js/capa/protex/D9267DE8FB02F8B995B4A58C66C76E29.cache.html b/common/static/js/capa/protex/D9267DE8FB02F8B995B4A58C66C76E29.cache.html deleted file mode 100644 index b6fc4f6a08..0000000000 --- a/common/static/js/capa/protex/D9267DE8FB02F8B995B4A58C66C76E29.cache.html +++ /dev/null @@ -1,758 +0,0 @@ - - - - \ No newline at end of file diff --git a/common/static/js/capa/protex/F275492F7098103BCB05F4F86ABF6218.cache.html b/common/static/js/capa/protex/F275492F7098103BCB05F4F86ABF6218.cache.html deleted file mode 100644 index 311c82b3a0..0000000000 --- a/common/static/js/capa/protex/F275492F7098103BCB05F4F86ABF6218.cache.html +++ /dev/null @@ -1,750 +0,0 @@ - - - - \ No newline at end of file diff --git a/common/static/js/capa/protex/F3301B0E65F38C7FCF2EF3764B3BB0B6.cache.html b/common/static/js/capa/protex/F3301B0E65F38C7FCF2EF3764B3BB0B6.cache.html deleted file mode 100644 index f39f0d68d2..0000000000 --- a/common/static/js/capa/protex/F3301B0E65F38C7FCF2EF3764B3BB0B6.cache.html +++ /dev/null @@ -1,754 +0,0 @@ - - - - \ No newline at end of file diff --git a/common/static/js/capa/protex/protex.nocache.js b/common/static/js/capa/protex/protex.nocache.js index 77f770bf9c..034032fe6b 100644 --- a/common/static/js/capa/protex/protex.nocache.js +++ b/common/static/js/capa/protex/protex.nocache.js @@ -1,4 +1,4 @@ -function protex(){var P='',xb='" for "gwt:onLoadErrorFn"',vb='" for "gwt:onPropertyErrorFn"',ib='"><\/script>',Z='#',Xb='.cache.html',_='/',lb='//',Qb='39CC89519B0E1FCB47B935AC9FE13D7B',Rb='6E05B1CD5BFCAF7D53C7C64D84318178',Wb=':',pb='::',dc=' - - - \ No newline at end of file diff --git a/common/static/js/capa/protex/0096DE2845FBF7D7D836936320AD0AC4.cache.html b/common/static/js/capa/protex/0096DE2845FBF7D7D836936320AD0AC4.cache.html deleted file mode 100644 index 4bf742011f..0000000000 --- a/common/static/js/capa/protex/0096DE2845FBF7D7D836936320AD0AC4.cache.html +++ /dev/null @@ -1,746 +0,0 @@ - - - - \ No newline at end of file diff --git a/common/static/js/capa/protex/356E8E1EA8930F0EA56B8973ACF7A92B.cache.html b/common/static/js/capa/protex/356E8E1EA8930F0EA56B8973ACF7A92B.cache.html deleted file mode 100644 index 5b7a580be5..0000000000 --- a/common/static/js/capa/protex/356E8E1EA8930F0EA56B8973ACF7A92B.cache.html +++ /dev/null @@ -1,735 +0,0 @@ - - - - \ No newline at end of file diff --git a/common/static/js/capa/protex/39CC89519B0E1FCB47B935AC9FE13D7B.cache.html b/common/static/js/capa/protex/39CC89519B0E1FCB47B935AC9FE13D7B.cache.html new file mode 100644 index 0000000000..e5db692aac --- /dev/null +++ b/common/static/js/capa/protex/39CC89519B0E1FCB47B935AC9FE13D7B.cache.html @@ -0,0 +1,743 @@ + + + + \ No newline at end of file diff --git a/common/static/js/capa/protex/5CE0BC9042E03326D1C86C810404FE4A.cache.html b/common/static/js/capa/protex/5CE0BC9042E03326D1C86C810404FE4A.cache.html deleted file mode 100644 index b5b610878c..0000000000 --- a/common/static/js/capa/protex/5CE0BC9042E03326D1C86C810404FE4A.cache.html +++ /dev/null @@ -1,742 +0,0 @@ - - - - \ No newline at end of file diff --git a/common/static/js/capa/protex/6E05B1CD5BFCAF7D53C7C64D84318178.cache.html b/common/static/js/capa/protex/6E05B1CD5BFCAF7D53C7C64D84318178.cache.html new file mode 100644 index 0000000000..ce6b444862 --- /dev/null +++ b/common/static/js/capa/protex/6E05B1CD5BFCAF7D53C7C64D84318178.cache.html @@ -0,0 +1,750 @@ + + + + \ No newline at end of file diff --git a/common/static/js/capa/protex/89DCE0145624582CBBEEA35B857D07CD.cache.html b/common/static/js/capa/protex/89DCE0145624582CBBEEA35B857D07CD.cache.html deleted file mode 100644 index 501abe3613..0000000000 --- a/common/static/js/capa/protex/89DCE0145624582CBBEEA35B857D07CD.cache.html +++ /dev/null @@ -1,759 +0,0 @@ - - - - \ No newline at end of file diff --git a/common/static/js/capa/protex/8D83FC6A58D4713A62FE989ECFDECF12.cache.html b/common/static/js/capa/protex/8D83FC6A58D4713A62FE989ECFDECF12.cache.html deleted file mode 100644 index 010508a13d..0000000000 --- a/common/static/js/capa/protex/8D83FC6A58D4713A62FE989ECFDECF12.cache.html +++ /dev/null @@ -1,742 +0,0 @@ - - - - \ No newline at end of file diff --git a/common/static/js/capa/protex/C824A958AB642DC2213DFFDAC640BEAA.cache.html b/common/static/js/capa/protex/C824A958AB642DC2213DFFDAC640BEAA.cache.html new file mode 100644 index 0000000000..f84a8b9822 --- /dev/null +++ b/common/static/js/capa/protex/C824A958AB642DC2213DFFDAC640BEAA.cache.html @@ -0,0 +1,760 @@ + + + + \ No newline at end of file diff --git a/common/static/js/capa/protex/D9267DE8FB02F8B995B4A58C66C76E29.cache.html b/common/static/js/capa/protex/D9267DE8FB02F8B995B4A58C66C76E29.cache.html new file mode 100644 index 0000000000..b6fc4f6a08 --- /dev/null +++ b/common/static/js/capa/protex/D9267DE8FB02F8B995B4A58C66C76E29.cache.html @@ -0,0 +1,758 @@ + + + + \ No newline at end of file diff --git a/common/static/js/capa/protex/F275492F7098103BCB05F4F86ABF6218.cache.html b/common/static/js/capa/protex/F275492F7098103BCB05F4F86ABF6218.cache.html new file mode 100644 index 0000000000..311c82b3a0 --- /dev/null +++ b/common/static/js/capa/protex/F275492F7098103BCB05F4F86ABF6218.cache.html @@ -0,0 +1,750 @@ + + + + \ No newline at end of file diff --git a/common/static/js/capa/protex/F3301B0E65F38C7FCF2EF3764B3BB0B6.cache.html b/common/static/js/capa/protex/F3301B0E65F38C7FCF2EF3764B3BB0B6.cache.html new file mode 100644 index 0000000000..f39f0d68d2 --- /dev/null +++ b/common/static/js/capa/protex/F3301B0E65F38C7FCF2EF3764B3BB0B6.cache.html @@ -0,0 +1,754 @@ + + + + \ No newline at end of file diff --git a/common/static/js/capa/protex/protex.nocache.js b/common/static/js/capa/protex/protex.nocache.js index 034032fe6b..77f770bf9c 100644 --- a/common/static/js/capa/protex/protex.nocache.js +++ b/common/static/js/capa/protex/protex.nocache.js @@ -1,4 +1,4 @@ -function protex(){var P='',xb='" for "gwt:onLoadErrorFn"',vb='" for "gwt:onPropertyErrorFn"',ib='"><\/script>',Z='#',Xb='.cache.html',_='/',lb='//',Qb='00950A947CFA1ACD8CC83306B9515908',Rb='0096DE2845FBF7D7D836936320AD0AC4',Sb='356E8E1EA8930F0EA56B8973ACF7A92B',Tb='5CE0BC9042E03326D1C86C810404FE4A',Ub='89DCE0145624582CBBEEA35B857D07CD',Vb='8D83FC6A58D4713A62FE989ECFDECF12',Wb=':',pb='::',dc=' - - - -% if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']: - <%static:js group='application'/> -% endif - -% if not settings.MITX_FEATURES['USE_DJANGO_PIPELINE']: - % for jsfn in [ '/static/%s' % x.replace('.coffee','.js') for x in settings.PIPELINE_JS['application']['source_filenames'] ]: - - % endfor -% endif - -## codemirror - - - -## alternate codemirror -## -## -## - -## image input: for clicking on images (see imageinput.html) - - - -<%include file="mathjax_include.html" /> - - - - - - -
- -## ----------------------------------------------------------------------------- -## information - -##
-##

Rendition of your problem code

-##
- -## ----------------------------------------------------------------------------- -## rendered problem display - - - - - - - -
-
- ${phtml} -
-
- - - - - -## - - - -## image input: for clicking on images (see imageinput.html) - - - - - <%block name="js_extra"/> - - - diff --git a/lms/templates/gitupdate.html b/lms/templates/gitupdate.html deleted file mode 100644 index a0cedabeae..0000000000 --- a/lms/templates/gitupdate.html +++ /dev/null @@ -1,32 +0,0 @@ - - -edX gitupdate - - - -
-

edX gitupdate

-
- -

Coursename: ${coursename}

- -% if msg: - - ${msg} - -% else: -

-Do you REALLY want to overwrite all the course.xml + problems + html -files with version from the main git repository? -

- -
- -## - -
-% endif - -

Return to site

- - diff --git a/lms/templates/quickedit.html b/lms/templates/quickedit.html deleted file mode 100644 index bc8e74eb65..0000000000 --- a/lms/templates/quickedit.html +++ /dev/null @@ -1,180 +0,0 @@ -<%namespace name='static' file='static_content.html'/> - - -## ----------------------------------------------------------------------------- -## Template for courseware.views.quickedit -## -## Used for quick-edit link present when viewing capa-format assesment problems. -## ----------------------------------------------------------------------------- - - -## -## - -% if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']: - <%static:css group='application'/> -% endif - -% if not settings.MITX_FEATURES['USE_DJANGO_PIPELINE']: -## -% endif - - - - - -% if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']: - <%static:js group='application'/> -% endif - -% if not settings.MITX_FEATURES['USE_DJANGO_PIPELINE']: - % for jsfn in [ '/static/%s' % x.replace('.coffee','.js') for x in settings.PIPELINE_JS['application']['source_filenames'] ]: - - % endfor -% endif - -## codemirror - - - -## alternate codemirror -## -## -## - -## image input: for clicking on images (see imageinput.html) - - -## - - - -<%block name="headextra"/> - - - <%include file="mathjax_include.html" /> - - - - - - - -## ----------------------------------------------------------------------------- -## information and i4x PSL code - -
-

QuickEdit

-
-
    -
  • File = ${filename}
  • -
  • ID = ${id}
  • -
- -
- -
- - - -
- -${msg|n} - -## ----------------------------------------------------------------------------- -## rendered problem display - - - -
- - - - - - - -
-
-
- ${phtml} -
-
-
- - - - - -## - - - - - - - - <%block name="js_extra"/> - - - diff --git a/lms/urls.py b/lms/urls.py index b25c4d259e..a203d468e7 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -320,10 +320,6 @@ if settings.COURSEWARE_ENABLED: 'courseware.views.static_tab', name="static_tab"), ) -if settings.QUICKEDIT: - urlpatterns += (url(r'^quickedit/(?P[^/]*)$', 'dogfood.views.quickedit'),) - urlpatterns += (url(r'^dogfood/(?P[^/]*)$', 'dogfood.views.df_capa_problem'),) - if settings.ENABLE_JASMINE: urlpatterns += (url(r'^_jasmine/', include('django_jasmine.urls')),) From 259bd8817f5c62da5700cb9fb1451f9eea58ace0 Mon Sep 17 00:00:00 2001 From: John Hess Date: Wed, 13 Feb 2013 15:10:32 -0500 Subject: [PATCH 39/75] Moved onclick to 'fold' button instead of to courseware's button --- common/static/js/capa/design-protein-2d.js | 23 ++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/common/static/js/capa/design-protein-2d.js b/common/static/js/capa/design-protein-2d.js index fb97b93956..e068fa8dcf 100644 --- a/common/static/js/capa/design-protein-2d.js +++ b/common/static/js/capa/design-protein-2d.js @@ -22,6 +22,15 @@ // It calls protexIsReady with a deferred command when it has finished // initialization and has drawn itself + function updateProtexField() { + var problem = $('#protex_container').parents('.problem'); + var input_field = problem.find('input[type=hidden]'); + var protex_answer = protexCheckAnswer(); + var value = {protex_answer: protex_answer}; + //console.log(JSON.stringify(value)); + input_field.val(JSON.stringify(value)); + } + protexIsReady = function() { //Load target shape var target_shape = $('#target_shape').val(); @@ -29,16 +38,18 @@ //Get answer from protex and store it into the hidden input field //when Check button is clicked - var problem = $('#protex_container').parents('.problem'); - var check_button = problem.find('input.check'); - var input_field = problem.find('input[type=hidden]'); - check_button.on('click', function() { + var fold_button = $("#fold-button"); + fold_button.on('click', function(){ + var problem = $('#protex_container').parents('.problem'); + var input_field = problem.find('input[type=hidden]'); var protex_answer = protexCheckAnswer(); var value = {protex_answer: protex_answer}; + //console.log(JSON.stringify(value)); input_field.val(JSON.stringify(value)); - console.log(JSON.stringify(value)); - }); + }); + updateProtexField(); }; + /*function initializeProtex() { //Check to see if the two exported GWT functions protexSetTargetShape From e413d16bffb235f418d43a426fcbf9567824c4a8 Mon Sep 17 00:00:00 2001 From: ichuang Date: Wed, 13 Feb 2013 21:25:31 -0500 Subject: [PATCH 40/75] allow "rows" attrib of coderesponse textbox to set box height --- common/lib/capa/capa/templates/codeinput.html | 1 + 1 file changed, 1 insertion(+) diff --git a/common/lib/capa/capa/templates/codeinput.html b/common/lib/capa/capa/templates/codeinput.html index 5c2ff2aca5..eb8cad0d70 100644 --- a/common/lib/capa/capa/templates/codeinput.html +++ b/common/lib/capa/capa/templates/codeinput.html @@ -50,6 +50,7 @@ }, smartIndent: false }); + $("#textbox_${id}").find('.CodeMirror-scroll').height(${int(13.5*eval(rows))}); }); From 7c7e09500ac0d24fefd7fe74aa5190c66dd38deb Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Wed, 30 Jan 2013 11:47:17 +0200 Subject: [PATCH 41/75] Renamed videox to videoalpha, as per Piotr naming scheme request. --- common/lib/xmodule/setup.py | 1 + .../xmodule/css/videoalpha/display.scss | 559 ++++++++++++++++++ .../xmodule/js/src/videoalpha/display.coffee | 63 ++ .../js/src/videoalpha/display/_subview.coffee | 14 + .../videoalpha/display/video_caption.coffee | 152 +++++ .../videoalpha/display/video_control.coffee | 35 ++ .../videoalpha/display/video_player.coffee | 180 ++++++ .../display/video_progress_slider.coffee | 49 ++ .../display/video_quality_control.coffee | 26 + .../display/video_speed_control.coffee | 43 ++ .../display/video_volume_control.coffee | 40 ++ .../xmodule/templates/videoalpha/default.yaml | 7 + .../lib/xmodule/xmodule/videoalpha_module.py | 150 +++++ lms/templates/videoalpha.html | 31 + 14 files changed, 1350 insertions(+) create mode 100644 common/lib/xmodule/xmodule/css/videoalpha/display.scss create mode 100644 common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee create mode 100644 common/lib/xmodule/xmodule/js/src/videoalpha/display/_subview.coffee create mode 100644 common/lib/xmodule/xmodule/js/src/videoalpha/display/video_caption.coffee create mode 100644 common/lib/xmodule/xmodule/js/src/videoalpha/display/video_control.coffee create mode 100644 common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee create mode 100644 common/lib/xmodule/xmodule/js/src/videoalpha/display/video_progress_slider.coffee create mode 100644 common/lib/xmodule/xmodule/js/src/videoalpha/display/video_quality_control.coffee create mode 100644 common/lib/xmodule/xmodule/js/src/videoalpha/display/video_speed_control.coffee create mode 100644 common/lib/xmodule/xmodule/js/src/videoalpha/display/video_volume_control.coffee create mode 100644 common/lib/xmodule/xmodule/templates/videoalpha/default.yaml create mode 100644 common/lib/xmodule/xmodule/videoalpha_module.py create mode 100644 lms/templates/videoalpha.html diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py index 0a9c05f3ec..6b7114c439 100644 --- a/common/lib/xmodule/setup.py +++ b/common/lib/xmodule/setup.py @@ -37,6 +37,7 @@ setup( "timelimit = xmodule.timelimit_module:TimeLimitDescriptor", "vertical = xmodule.vertical_module:VerticalDescriptor", "video = xmodule.video_module:VideoDescriptor", + "videoalpha = xmodule.videoalpha_module:VideoAlphaDescriptor", "videodev = xmodule.backcompat_module:TranslateCustomTagDescriptor", "videosequence = xmodule.seq_module:SequenceDescriptor", "discussion = xmodule.discussion_module:DiscussionDescriptor", diff --git a/common/lib/xmodule/xmodule/css/videoalpha/display.scss b/common/lib/xmodule/xmodule/css/videoalpha/display.scss new file mode 100644 index 0000000000..bf575e74a3 --- /dev/null +++ b/common/lib/xmodule/xmodule/css/videoalpha/display.scss @@ -0,0 +1,559 @@ +& { + margin-bottom: 30px; +} + +div.video { + @include clearfix(); + background: #f3f3f3; + display: block; + margin: 0 -12px; + padding: 12px; + border-radius: 5px; + + article.video-wrapper { + float: left; + margin-right: flex-gutter(9); + width: flex-grid(6, 9); + + section.video-player { + height: 0; + overflow: hidden; + padding-bottom: 56.25%; + position: relative; + + object, iframe { + border: none; + height: 100%; + left: 0; + position: absolute; + top: 0; + width: 100%; + } + } + + section.video-controls { + @include clearfix(); + background: #333; + border: 1px solid #000; + border-top: 0; + color: #ccc; + position: relative; + + &:hover { + ul, div { + opacity: 1; + } + } + + div.slider { + @include clearfix(); + background: #c2c2c2; + border: 1px solid #000; + @include border-radius(0); + border-top: 1px solid #000; + @include box-shadow(inset 0 1px 0 #eee, 0 1px 0 #555); + height: 7px; + margin-left: -1px; + margin-right: -1px; + @include transition(height 2.0s ease-in-out); + + div.ui-widget-header { + background: #777; + @include box-shadow(inset 0 1px 0 #999); + } + + a.ui-slider-handle { + background: $pink url(../images/slider-handle.png) center center no-repeat; + @include background-size(50%); + border: 1px solid darken($pink, 20%); + @include border-radius(15px); + @include box-shadow(inset 0 1px 0 lighten($pink, 10%)); + cursor: pointer; + height: 15px; + margin-left: -7px; + top: -4px; + @include transition(height 2.0s ease-in-out, width 2.0s ease-in-out); + width: 15px; + + &:focus, &:hover { + background-color: lighten($pink, 10%); + outline: none; + } + } + } + + ul.vcr { + @extend .dullify; + float: left; + list-style: none; + margin: 0 lh() 0 0; + padding: 0; + + li { + float: left; + margin-bottom: 0; + + a { + border-bottom: none; + border-right: 1px solid #000; + @include box-shadow(1px 0 0 #555); + cursor: pointer; + display: block; + line-height: 46px; + padding: 0 lh(.75); + text-indent: -9999px; + @include transition(background-color, opacity); + width: 14px; + background: url('../images/vcr.png') 15px 15px no-repeat; + outline: 0; + + &:focus { + outline: 0; + } + + &:empty { + height: 46px; + background: url('../images/vcr.png') 15px 15px no-repeat; + } + + &.play { + background-position: 17px -114px; + + &:hover { + background-color: #444; + } + } + + &.pause { + background-position: 16px -50px; + + &:hover { + background-color: #444; + } + } + } + + div.vidtime { + padding-left: lh(.75); + font-weight: bold; + line-height: 46px; //height of play pause buttons + padding-left: lh(.75); + -webkit-font-smoothing: antialiased; + } + } + } + + div.secondary-controls { + @extend .dullify; + float: right; + + div.speeds { + float: left; + position: relative; + + &.open { + &>a { + background: url('../images/open-arrow.png') 10px center no-repeat; + } + + ol.video_speeds { + display: block; + opacity: 1; + padding: 0; + margin: 0; + list-style: none; + } + } + + &>a { + background: url('../images/closed-arrow.png') 10px center no-repeat; + border-left: 1px solid #000; + border-right: 1px solid #000; + @include box-shadow(1px 0 0 #555, inset 1px 0 0 #555); + @include clearfix(); + color: #fff; + cursor: pointer; + display: block; + line-height: 46px; //height of play pause buttons + margin-right: 0; + padding-left: 15px; + position: relative; + @include transition(); + -webkit-font-smoothing: antialiased; + width: 116px; + outline: 0; + + &:focus { + outline: 0; + } + + h3 { + color: #999; + float: left; + font-size: em(14); + font-weight: normal; + letter-spacing: 1px; + padding: 0 lh(.25) 0 lh(.5); + line-height: 46px; + text-transform: uppercase; + } + + p.active { + float: left; + font-weight: bold; + margin-bottom: 0; + padding: 0 lh(.5) 0 0; + line-height: 46px; + color: #fff; + } + + &:hover, &:active, &:focus { + opacity: 1; + background-color: #444; + } + } + + // fix for now + ol.video_speeds { + @include box-shadow(inset 1px 0 0 #555, 0 3px 0 #444); + @include transition(); + background-color: #444; + border: 1px solid #000; + bottom: 46px; + display: none; + opacity: 0; + position: absolute; + width: 133px; + z-index: 10; + + li { + @include box-shadow( 0 1px 0 #555); + border-bottom: 1px solid #000; + color: #fff; + cursor: pointer; + + a { + border: 0; + color: #fff; + display: block; + padding: lh(.5); + + &:hover { + background-color: #666; + color: #aaa; + } + } + + &.active { + font-weight: bold; + } + + &:last-child { + @include box-shadow(none); + border-bottom: 0; + margin-top: 0; + } + } + } + } + + div.volume { + float: left; + position: relative; + + &.open { + .volume-slider-container { + display: block; + opacity: 1; + } + } + + &.muted { + &>a { + background: url('../images/mute.png') 10px center no-repeat; + } + } + + > a { + background: url('../images/volume.png') 10px center no-repeat; + border-right: 1px solid #000; + @include box-shadow(1px 0 0 #555, inset 1px 0 0 #555); + @include clearfix(); + color: #fff; + cursor: pointer; + display: block; + height: 46px; + margin-right: 0; + padding-left: 15px; + position: relative; + @include transition(); + -webkit-font-smoothing: antialiased; + width: 30px; + + &:hover, &:active, &:focus { + background-color: #444; + } + } + + .volume-slider-container { + @include box-shadow(inset 1px 0 0 #555, 0 3px 0 #444); + @include transition(); + background-color: #444; + border: 1px solid #000; + bottom: 46px; + display: none; + opacity: 0; + position: absolute; + width: 45px; + height: 125px; + margin-left: -1px; + z-index: 10; + + .volume-slider { + height: 100px; + border: 0; + width: 5px; + margin: 14px auto; + background: #666; + border: 1px solid #000; + @include box-shadow(0 1px 0 #333); + + a.ui-slider-handle { + background: $pink url(../images/slider-handle.png) center center no-repeat; + @include background-size(50%); + border: 1px solid darken($pink, 20%); + @include border-radius(15px); + @include box-shadow(inset 0 1px 0 lighten($pink, 10%)); + cursor: pointer; + height: 15px; + left: -6px; + @include transition(height 2.0s ease-in-out, width 2.0s ease-in-out); + width: 15px; + } + + .ui-slider-range { + background: #ddd; + } + } + } + } + + a.add-fullscreen { + background: url(../images/fullscreen.png) center no-repeat; + border-right: 1px solid #000; + @include box-shadow(1px 0 0 #555, inset 1px 0 0 #555); + color: #797979; + display: block; + float: left; + line-height: 46px; //height of play pause buttons + margin-left: 0; + padding: 0 lh(.5); + text-indent: -9999px; + @include transition(); + width: 30px; + + &:hover { + background-color: #444; + color: #fff; + text-decoration: none; + } + } + + a.quality_control { + background: url(../images/hd.png) center no-repeat; + border-right: 1px solid #000; + @include box-shadow(1px 0 0 #555, inset 1px 0 0 #555); + color: #797979; + display: block; + float: left; + line-height: 46px; //height of play pause buttons + margin-left: 0; + padding: 0 lh(.5); + text-indent: -9999px; + @include transition(); + width: 30px; + + &:hover { + background-color: #444; + color: #fff; + text-decoration: none; + } + + &.active { + background-color: #F44; + color: #0ff; + text-decoration: none; + } + } + + + a.hide-subtitles { + background: url('../images/cc.png') center no-repeat; + color: #797979; + display: block; + float: left; + font-weight: 800; + line-height: 46px; //height of play pause buttons + margin-left: 0; + opacity: 1; + padding: 0 lh(.5); + position: relative; + text-indent: -9999px; + @include transition(); + -webkit-font-smoothing: antialiased; + width: 30px; + + &:hover { + background-color: #444; + color: #fff; + text-decoration: none; + } + + &.off { + opacity: .7; + } + } + } + } + + &:hover section.video-controls { + ul, div { + opacity: 1; + } + + div.slider { + height: 14px; + margin-top: -7px; + + a.ui-slider-handle { + @include border-radius(20px); + height: 20px; + margin-left: -10px; + top: -4px; + width: 20px; + } + } + } + } + + ol.subtitles { + padding-left: 0; + float: left; + max-height: 460px; + overflow: auto; + width: flex-grid(3, 9); + margin: 0; + font-size: 14px; + list-style: none; + + li { + border: 0; + color: #666; + cursor: pointer; + margin-bottom: 8px; + padding: 0; + line-height: lh(); + + &.current { + color: #333; + font-weight: 700; + } + + &:hover { + color: $blue; + } + + &:empty { + margin-bottom: 0px; + } + } + } + + &.closed { + @extend .trans; + + article.video-wrapper { + width: flex-grid(9,9); + } + + ol.subtitles { + width: 0; + height: 0; + } + } + + &.fullscreen { + background: rgba(#000, .95); + border: 0; + bottom: 0; + height: 100%; + left: 0; + margin: 0; + overflow: hidden; + padding: 0; + position: fixed; + top: 0; + width: 100%; + z-index: 999; + vertical-align: middle; + + &.closed { + ol.subtitles { + right: -(flex-grid(4)); + width: auto; + } + } + + div.tc-wrapper { + @include clearfix; + display: table; + width: 100%; + height: 100%; + + article.video-wrapper { + width: 100%; + display: table-cell; + vertical-align: middle; + float: none; + } + + object, iframe { + bottom: 0; + height: 100%; + left: 0; + overflow: hidden; + position: fixed; + top: 0; + } + + section.video-controls { + bottom: 0; + left: 0; + position: absolute; + width: 100%; + z-index: 9999; + } + } + + ol.subtitles { + background: rgba(#000, .8); + bottom: 0; + height: 100%; + max-height: 100%; + max-width: flex-grid(3); + padding: lh(); + position: fixed; + right: 0; + top: 0; + @include transition(); + + li { + color: #aaa; + + &.current { + color: #fff; + } + } + } + } +} diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee new file mode 100644 index 0000000000..1876330340 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee @@ -0,0 +1,63 @@ +class @Video + constructor: (element) -> + @el = $(element).find('.video') + @id = @el.attr('id').replace(/video_/, '') + @start = @el.data('start') + @end = @el.data('end') + @caption_data_dir = @el.data('caption-data-dir') + @caption_asset_path = @el.data('caption-asset-path') + @show_captions = @el.data('show-captions') == "true" + window.player = null + @el = $("#video_#{@id}") + @parseVideos @el.data('streams') + @fetchMetadata() + @parseSpeed() + $("#video_#{@id}").data('video', this).addClass('video-load-complete') + + @hide_captions = $.cookie('hide_captions') == 'true' + + if YT.Player + @embed() + else + window.onYouTubePlayerAPIReady = => + @el.each -> + $(this).data('video').embed() + + youtubeId: (speed)-> + @videos[speed || @speed] + + parseVideos: (videos) -> + @videos = {} + $.each videos.split(/,/), (index, video) => + video = video.split(/:/) + speed = parseFloat(video[0]).toFixed(2).replace /\.00$/, '.0' + @videos[speed] = video[1] + + parseSpeed: -> + @setSpeed($.cookie('video_speed')) + @speeds = ($.map @videos, (url, speed) -> speed).sort() + + setSpeed: (newSpeed) -> + if @videos[newSpeed] != undefined + @speed = newSpeed + $.cookie('video_speed', "#{newSpeed}", expires: 3650, path: '/') + else + @speed = '1.0' + + embed: -> + @player = new VideoPlayer video: this + + fetchMetadata: (url) -> + @metadata = {} + $.each @videos, (speed, url) => + $.get "https://gdata.youtube.com/feeds/api/videos/#{url}?v=2&alt=jsonc", ((data) => @metadata[data.data.id] = data.data) , 'jsonp' + + getDuration: -> + @metadata[@youtubeId()].duration + + log: (eventName) -> + Logger.log eventName, + id: @id + code: @youtubeId() + currentTime: @player.currentTime + speed: @speed diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/_subview.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/_subview.coffee new file mode 100644 index 0000000000..2e14289843 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/_subview.coffee @@ -0,0 +1,14 @@ +class @Subview + constructor: (options) -> + $.each options, (key, value) => + @[key] = value + @initialize() + @render() + @bind() + + $: (selector) -> + $(selector, @el) + + initialize: -> + render: -> + bind: -> diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_caption.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_caption.coffee new file mode 100644 index 0000000000..e840cd2a77 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_caption.coffee @@ -0,0 +1,152 @@ +class @VideoCaption extends Subview + initialize: -> + @loaded = false + + bind: -> + $(window).bind('resize', @resize) + @$('.hide-subtitles').click @toggle + @$('.subtitles').mouseenter(@onMouseEnter).mouseleave(@onMouseLeave) + .mousemove(@onMovement).bind('mousewheel', @onMovement) + .bind('DOMMouseScroll', @onMovement) + + captionURL: -> + "#{@captionAssetPath}#{@youtubeId}.srt.sjson" + + render: -> + # TODO: make it so you can have a video with no captions. + #@$('.video-wrapper').after """ + #
  1. Attempting to load captions...
+ # """ + @$('.video-wrapper').after """ +
    + """ + @$('.video-controls .secondary-controls').append """ + Captions + """#" + @$('.subtitles').css maxHeight: @$('.video-wrapper').height() - 5 + @fetchCaption() + + fetchCaption: -> + $.getWithPrefix @captionURL(), (captions) => + @captions = captions.text + @start = captions.start + + @loaded = true + + if onTouchBasedDevice() + $('.subtitles li').html "Caption will be displayed when you start playing the video." + else + @renderCaption() + + renderCaption: -> + container = $('
      ') + + $.each @captions, (index, text) => + container.append $('
    1. ').html(text).attr + 'data-index': index + 'data-start': @start[index] + + @$('.subtitles').html(container.html()) + @$('.subtitles li[data-index]').click @seekPlayer + + # prepend and append an empty
    2. for cosmetic reason + @$('.subtitles').prepend($('
    3. ').height(@topSpacingHeight())) + .append($('
    4. ').height(@bottomSpacingHeight())) + + @rendered = true + + search: (time) -> + if @loaded + min = 0 + max = @start.length - 1 + + while min < max + index = Math.ceil((max + min) / 2) + if time < @start[index] + max = index - 1 + if time >= @start[index] + min = index + return min + + play: -> + if @loaded + @renderCaption() unless @rendered + @playing = true + + pause: -> + if @loaded + @playing = false + + updatePlayTime: (time) -> + if @loaded + # This 250ms offset is required to match the video speed + time = Math.round(Time.convert(time, @currentSpeed, '1.0') * 1000 + 250) + newIndex = @search time + + if newIndex != undefined && @currentIndex != newIndex + if @currentIndex + @$(".subtitles li.current").removeClass('current') + @$(".subtitles li[data-index='#{newIndex}']").addClass('current') + + @currentIndex = newIndex + @scrollCaption() + + resize: => + @$('.subtitles').css maxHeight: @captionHeight() + @$('.subtitles .spacing:first').height(@topSpacingHeight()) + @$('.subtitles .spacing:last').height(@bottomSpacingHeight()) + @scrollCaption() + + onMouseEnter: => + clearTimeout @frozen if @frozen + @frozen = setTimeout @onMouseLeave, 10000 + + onMovement: => + @onMouseEnter() + + onMouseLeave: => + clearTimeout @frozen if @frozen + @frozen = null + @scrollCaption() if @playing + + scrollCaption: -> + if !@frozen && @$('.subtitles .current:first').length + @$('.subtitles').scrollTo @$('.subtitles .current:first'), + offset: - @calculateOffset(@$('.subtitles .current:first')) + + seekPlayer: (event) => + event.preventDefault() + time = Math.round(Time.convert($(event.target).data('start'), '1.0', @currentSpeed) / 1000) + $(@).trigger('seek', time) + + calculateOffset: (element) -> + @captionHeight() / 2 - element.height() / 2 + + topSpacingHeight: -> + @calculateOffset(@$('.subtitles li:not(.spacing):first')) + + bottomSpacingHeight: -> + @calculateOffset(@$('.subtitles li:not(.spacing):last')) + + toggle: (event) => + event.preventDefault() + if @el.hasClass('closed') # Captions are "closed" e.g. turned off + @hideCaptions(false) + else # Captions are on + @hideCaptions(true) + + hideCaptions: (hide_captions) => + if hide_captions + @$('.hide-subtitles').attr('title', 'Turn on captions') + @el.addClass('closed') + else + @$('.hide-subtitles').attr('title', 'Turn off captions') + @el.removeClass('closed') + @scrollCaption() + $.cookie('hide_captions', hide_captions, expires: 3650, path: '/') + + captionHeight: -> + if @el.hasClass('fullscreen') + $(window).height() - @$('.video-controls').height() + else + @$('.video-wrapper').height() diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_control.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_control.coffee new file mode 100644 index 0000000000..856549c3e2 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_control.coffee @@ -0,0 +1,35 @@ +class @VideoControl extends Subview + bind: -> + @$('.video_control').click @togglePlayback + + render: -> + @el.append """ +
      +
      +
        +
      • +
      • +
        0:00 / 0:00
        +
      • +
      + +
      + """#" + + unless onTouchBasedDevice() + @$('.video_control').addClass('play').html('Play') + + play: -> + @$('.video_control').removeClass('play').addClass('pause').html('Pause') + + pause: -> + @$('.video_control').removeClass('pause').addClass('play').html('Play') + + togglePlayback: (event) => + event.preventDefault() + if @$('.video_control').hasClass('play') + $(@).trigger('play') + else if @$('.video_control').hasClass('pause') + $(@).trigger('pause') diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee new file mode 100644 index 0000000000..22308a5568 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee @@ -0,0 +1,180 @@ +class @VideoPlayer extends Subview + initialize: -> + # Define a missing constant of Youtube API + YT.PlayerState.UNSTARTED = -1 + + @currentTime = 0 + @el = $("#video_#{@video.id}") + + bind: -> + $(@control).bind('play', @play) + .bind('pause', @pause) + $(@qualityControl).bind('changeQuality', @handlePlaybackQualityChange) + $(@caption).bind('seek', @onSeek) + $(@speedControl).bind('speedChange', @onSpeedChange) + $(@progressSlider).bind('seek', @onSeek) + if @volumeControl + $(@volumeControl).bind('volumeChange', @onVolumeChange) + $(document).keyup @bindExitFullScreen + + @$('.add-fullscreen').click @toggleFullScreen + @addToolTip() unless onTouchBasedDevice() + + bindExitFullScreen: (event) => + if @el.hasClass('fullscreen') && event.keyCode == 27 + @toggleFullScreen(event) + + render: -> + @control = new VideoControl el: @$('.video-controls') + @qualityControl = new VideoQualityControl el: @$('.secondary-controls') + @caption = new VideoCaption + el: @el + youtubeId: @video.youtubeId('1.0') + currentSpeed: @currentSpeed() + captionAssetPath: @video.caption_asset_path + unless onTouchBasedDevice() + @volumeControl = new VideoVolumeControl el: @$('.secondary-controls') + @speedControl = new VideoSpeedControl el: @$('.secondary-controls'), speeds: @video.speeds, currentSpeed: @currentSpeed() + @progressSlider = new VideoProgressSlider el: @$('.slider') + @playerVars = + controls: 0 + wmode: 'transparent' + rel: 0 + showinfo: 0 + enablejsapi: 1 + modestbranding: 1 + if @video.start + @playerVars.start = @video.start + @playerVars.wmode = 'window' + if @video.end + # work in AS3, not HMLT5. but iframe use AS3 + @playerVars.end = @video.end + + @player = new YT.Player @video.id, + playerVars: @playerVars + videoId: @video.youtubeId() + events: + onReady: @onReady + onStateChange: @onStateChange + onPlaybackQualityChange: @onPlaybackQualityChange + @caption.hideCaptions(@['video'].hide_captions) + + addToolTip: -> + @$('.add-fullscreen, .hide-subtitles').qtip + position: + my: 'top right' + at: 'top center' + + onReady: (event) => + unless onTouchBasedDevice() + $('.video-load-complete:first').data('video').player.play() + + onStateChange: (event) => + switch event.data + when YT.PlayerState.UNSTARTED + @onUnstarted() + when YT.PlayerState.PLAYING + @onPlay() + when YT.PlayerState.PAUSED + @onPause() + when YT.PlayerState.ENDED + @onEnded() + + onPlaybackQualityChange: (event, value) => + quality = @player.getPlaybackQuality() + @qualityControl.onQualityChange(quality) + + handlePlaybackQualityChange: (event, value) => + @player.setPlaybackQuality(value) + + onUnstarted: => + @control.pause() + @caption.pause() + + onPlay: => + @video.log 'play_video' + window.player.pauseVideo() if window.player && window.player != @player + window.player = @player + unless @player.interval + @player.interval = setInterval(@update, 200) + @caption.play() + @control.play() + @progressSlider.play() + + onPause: => + @video.log 'pause_video' + window.player = null if window.player == @player + clearInterval(@player.interval) + @player.interval = null + @caption.pause() + @control.pause() + + onEnded: => + @control.pause() + @caption.pause() + + onSeek: (event, time) => + @player.seekTo(time, true) + if @isPlaying() + clearInterval(@player.interval) + @player.interval = setInterval(@update, 200) + else + @currentTime = time + @updatePlayTime time + + onSpeedChange: (event, newSpeed) => + @currentTime = Time.convert(@currentTime, parseFloat(@currentSpeed()), newSpeed) + newSpeed = parseFloat(newSpeed).toFixed(2).replace /\.00$/, '.0' + @video.setSpeed(newSpeed) + @caption.currentSpeed = newSpeed + + if @isPlaying() + @player.loadVideoById(@video.youtubeId(), @currentTime) + else + @player.cueVideoById(@video.youtubeId(), @currentTime) + @updatePlayTime @currentTime + + onVolumeChange: (event, volume) => + @player.setVolume volume + + update: => + if @currentTime = @player.getCurrentTime() + @updatePlayTime @currentTime + + updatePlayTime: (time) -> + progress = Time.format(time) + ' / ' + Time.format(@duration()) + @$(".vidtime").html(progress) + @caption.updatePlayTime(time) + @progressSlider.updatePlayTime(time, @duration()) + + toggleFullScreen: (event) => + event.preventDefault() + if @el.hasClass('fullscreen') + @$('.add-fullscreen').attr('title', 'Fill browser') + @el.removeClass('fullscreen') + else + @el.addClass('fullscreen') + @$('.add-fullscreen').attr('title', 'Exit fill browser') + @caption.resize() + + # Delegates + play: => + @player.playVideo() if @player.playVideo + + isPlaying: -> + @player.getPlayerState() == YT.PlayerState.PLAYING + + pause: => + @player.pauseVideo() if @player.pauseVideo + + duration: -> + @video.getDuration() + + currentSpeed: -> + @video.speed + + volume: (value) -> + if value? + @player.setVolume value + else + @player.getVolume() diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_progress_slider.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_progress_slider.coffee new file mode 100644 index 0000000000..874756cb71 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_progress_slider.coffee @@ -0,0 +1,49 @@ +class @VideoProgressSlider extends Subview + initialize: -> + @buildSlider() unless onTouchBasedDevice() + + buildSlider: -> + @slider = @el.slider + range: 'min' + change: @onChange + slide: @onSlide + stop: @onStop + @buildHandle() + + buildHandle: -> + @handle = @$('.slider .ui-slider-handle') + @handle.qtip + content: "#{Time.format(@slider.slider('value'))}" + position: + my: 'bottom center' + at: 'top center' + container: @handle + hide: + delay: 700 + style: + classes: 'ui-tooltip-slider' + widget: true + + play: => + @buildSlider() unless @slider + + updatePlayTime: (currentTime, duration) -> + if @slider && !@frozen + @slider.slider('option', 'max', duration) + @slider.slider('value', currentTime) + + onSlide: (event, ui) => + @frozen = true + @updateTooltip(ui.value) + $(@).trigger('seek', ui.value) + + onChange: (event, ui) => + @updateTooltip(ui.value) + + onStop: (event, ui) => + @frozen = true + $(@).trigger('seek', ui.value) + setTimeout (=> @frozen = false), 200 + + updateTooltip: (value)-> + @handle.qtip('option', 'content.text', "#{Time.format(value)}") diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_quality_control.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_quality_control.coffee new file mode 100644 index 0000000000..f8f6167075 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_quality_control.coffee @@ -0,0 +1,26 @@ +class @VideoQualityControl extends Subview + initialize: -> + @quality = null; + + bind: -> + @$('.quality_control').click @toggleQuality + + render: -> + @el.append """ + HD + """#" + + onQualityChange: (value) -> + @quality = value + if @quality in ['hd720', 'hd1080', 'highres'] + @el.addClass('active') + else + @el.removeClass('active') + + toggleQuality: (event) => + event.preventDefault() + if @quality in ['hd720', 'hd1080', 'highres'] + newQuality = 'large' + else + newQuality = 'hd720' + $(@).trigger('changeQuality', newQuality) \ No newline at end of file diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_speed_control.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_speed_control.coffee new file mode 100644 index 0000000000..1d0d8b7d44 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_speed_control.coffee @@ -0,0 +1,43 @@ +class @VideoSpeedControl extends Subview + bind: -> + @$('.video_speeds a').click @changeVideoSpeed + if onTouchBasedDevice() + @$('.speeds').click (event) -> + event.preventDefault() + $(this).toggleClass('open') + else + @$('.speeds').mouseenter -> + $(this).addClass('open') + @$('.speeds').mouseleave -> + $(this).removeClass('open') + @$('.speeds').click (event) -> + event.preventDefault() + $(this).removeClass('open') + + render: -> + @el.prepend """ + + """ + + $.each @speeds, (index, speed) => + link = $('').attr(href: "#").html("#{speed}x") + @$('.video_speeds').prepend($('
    5. ').attr('data-speed', speed).html(link)) + @setSpeed(@currentSpeed) + + changeVideoSpeed: (event) => + event.preventDefault() + unless $(event.target).parent().hasClass('active') + @currentSpeed = $(event.target).parent().data('speed') + $(@).trigger 'speedChange', $(event.target).parent().data('speed') + @setSpeed(parseFloat(@currentSpeed).toFixed(2).replace /\.00$/, '.0') + + setSpeed: (speed) -> + @$('.video_speeds li').removeClass('active') + @$(".video_speeds li[data-speed='#{speed}']").addClass('active') + @$('.speeds p.active').html("#{speed}x") diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_volume_control.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_volume_control.coffee new file mode 100644 index 0000000000..096b50042d --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_volume_control.coffee @@ -0,0 +1,40 @@ +class @VideoVolumeControl extends Subview + initialize: -> + @currentVolume = 100 + + bind: -> + @$('.volume').mouseenter -> + $(this).addClass('open') + @$('.volume').mouseleave -> + $(this).removeClass('open') + @$('.volume>a').click(@toggleMute) + + render: -> + @el.prepend """ +
      + +
      +
      +
      +
      + """#" + @slider = @$('.volume-slider').slider + orientation: "vertical" + range: "min" + min: 0 + max: 100 + value: 100 + change: @onChange + slide: @onChange + + onChange: (event, ui) => + @currentVolume = ui.value + $(@).trigger 'volumeChange', @currentVolume + @$('.volume').toggleClass 'muted', @currentVolume == 0 + + toggleMute: => + if @currentVolume > 0 + @previousVolume = @currentVolume + @slider.slider 'option', 'value', 0 + else + @slider.slider 'option', 'value', @previousVolume diff --git a/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml b/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml new file mode 100644 index 0000000000..69ed22cc1e --- /dev/null +++ b/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml @@ -0,0 +1,7 @@ +--- +metadata: + display_name: default + data_dir: a_made_up_name +data: | + +children: [] diff --git a/common/lib/xmodule/xmodule/videoalpha_module.py b/common/lib/xmodule/xmodule/videoalpha_module.py new file mode 100644 index 0000000000..e41f9783e4 --- /dev/null +++ b/common/lib/xmodule/xmodule/videoalpha_module.py @@ -0,0 +1,150 @@ +import json +import logging + +from lxml import etree +from pkg_resources import resource_string, resource_listdir + +from xmodule.x_module import XModule +from xmodule.raw_module import RawDescriptor +from xmodule.modulestore.mongo import MongoModuleStore +from xmodule.modulestore.django import modulestore +from xmodule.contentstore.content import StaticContent + +import datetime +import time + +import datetime +import time + +log = logging.getLogger(__name__) + + +class VideoModule(XModule): + video_time = 0 + icon_class = 'video' + + js = {'coffee': + [resource_string(__name__, 'js/src/time.coffee'), + resource_string(__name__, 'js/src/videoalpha/display.coffee')] + + [resource_string(__name__, 'js/src/videoalpha/display/' + filename) + for filename + in sorted(resource_listdir(__name__, 'js/src/videoalpha/display')) + if filename.endswith('.coffee')]} + css = {'scss': [resource_string(__name__, 'css/videoalpha/display.scss')]} + js_module_name = "Video" + + def __init__(self, system, location, definition, descriptor, + instance_state=None, shared_state=None, **kwargs): + XModule.__init__(self, system, location, definition, descriptor, + instance_state, shared_state, **kwargs) + xmltree = etree.fromstring(self.definition['data']) + self.youtube = xmltree.get('youtube') + self.position = 0 + self.show_captions = xmltree.get('show_captions', 'true') + self.source = self._get_source(xmltree) + self.track = self._get_track(xmltree) + self.start_time, self.end_time = self._get_timeframe(xmltree) + + if instance_state is not None: + state = json.loads(instance_state) + if 'position' in state: + self.position = int(float(state['position'])) + + def _get_source(self, xmltree): + # find the first valid source + return self._get_first_external(xmltree, 'source') + + def _get_track(self, xmltree): + # find the first valid track + return self._get_first_external(xmltree, 'track') + + def _get_first_external(self, xmltree, tag): + """ + Will return the first valid element + of the given tag. + 'valid' means has a non-empty 'src' attribute + """ + result = None + for element in xmltree.findall(tag): + src = element.get('src') + if src: + result = src + break + return result + + def _get_timeframe(self, xmltree): + """ Converts 'from' and 'to' parameters in video tag to seconds. + If there are no parameters, returns empty string. """ + + def parse_time(s): + """Converts s in '12:34:45' format to seconds. If s is + None, returns empty string""" + if s is None: + return '' + else: + x = time.strptime(s, '%H:%M:%S') + return datetime.timedelta(hours=x.tm_hour, + minutes=x.tm_min, + seconds=x.tm_sec).total_seconds() + + return parse_time(xmltree.get('from')), parse_time(xmltree.get('to')) + + def handle_ajax(self, dispatch, get): + ''' + Handle ajax calls to this video. + TODO (vshnayder): This is not being called right now, so the position + is not being saved. + ''' + log.debug(u"GET {0}".format(get)) + log.debug(u"DISPATCH {0}".format(dispatch)) + if dispatch == 'goto_position': + self.position = int(float(get['position'])) + log.info(u"NEW POSITION {0}".format(self.position)) + return json.dumps({'success': True}) + raise Http404() + + def get_progress(self): + ''' TODO (vshnayder): Get and save duration of youtube video, then return + fraction watched. + (Be careful to notice when video link changes and update) + + For now, we have no way of knowing if the video has even been watched, so + just return None. + ''' + return None + + def get_instance_state(self): + #log.debug(u"STATE POSITION {0}".format(self.position)) + return json.dumps({'position': self.position}) + + def videoalpha_list(self): + return self.youtube + + def get_html(self): + if isinstance(modulestore(), MongoModuleStore) : + caption_asset_path = StaticContent.get_base_url_path_for_course_assets(self.location) + '/subs_' + else: + # VS[compat] + # cdodge: filesystem static content support. + caption_asset_path = "/static/{0}/subs/".format(self.metadata['data_dir']) + + return self.system.render_template('videoalpha.html', { + 'streams': self.videoalpha_list(), + 'id': self.location.html_id(), + 'position': self.position, + 'source': self.source, + 'track': self.track, + 'display_name': self.display_name, + # TODO (cpennington): This won't work when we move to data that isn't on the filesystem + 'data_dir': self.metadata['data_dir'], + 'caption_asset_path': caption_asset_path, + 'show_captions': self.show_captions, + 'start': self.start_time, + 'end': self.end_time + }) + + +class VideoAlphaDescriptor(RawDescriptor): + module_class = VideoModule + stores_state = True + template_dir_name = "videoalpha" diff --git a/lms/templates/videoalpha.html b/lms/templates/videoalpha.html new file mode 100644 index 0000000000..6cee9ed39b --- /dev/null +++ b/lms/templates/videoalpha.html @@ -0,0 +1,31 @@ +% if display_name is not UNDEFINED and display_name is not None: +

      ${display_name}

      +% endif + + +%if settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']: +
      +%else: +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +%endif + +% if source: +
      +

      Download video here.

      +
      +% endif + +% if track: +
      +

      Download subtitles here.

      +
      +% endif From ca83c3953a18cdd7a3d91fbe2138633d595649c2 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Wed, 30 Jan 2013 18:06:27 +0200 Subject: [PATCH 42/75] Adding HTML5Video class and modifying coffee sources to use it when video sources are provided instead of YouTube IDs. --- common/lib/xmodule/xmodule/js/src/.gitignore | 3 +- .../xmodule/js/src/videoalpha/display.coffee | 80 +++++-- .../js/src/videoalpha/display/html5_video.js | 196 ++++++++++++++++++ .../videoalpha/display/video_player.coffee | 32 ++- .../lib/xmodule/xmodule/videoalpha_module.py | 10 +- lms/templates/videoalpha.html | 15 +- 6 files changed, 301 insertions(+), 35 deletions(-) create mode 100644 common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js diff --git a/common/lib/xmodule/xmodule/js/src/.gitignore b/common/lib/xmodule/xmodule/js/src/.gitignore index 03534687ca..bbd93c90e3 100644 --- a/common/lib/xmodule/xmodule/js/src/.gitignore +++ b/common/lib/xmodule/xmodule/js/src/.gitignore @@ -1,2 +1 @@ -*.js - +# Please do not ignore *.js files. Some xmodules are written in JS. diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee index 1876330340..b97b02e68c 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee @@ -1,4 +1,4 @@ -class @Video +class @VideoAlpha constructor: (element) -> @el = $(element).find('.video') @id = @el.attr('id').replace(/video_/, '') @@ -9,40 +9,77 @@ class @Video @show_captions = @el.data('show-captions') == "true" window.player = null @el = $("#video_#{@id}") - @parseVideos @el.data('streams') - @fetchMetadata() - @parseSpeed() + + if @parseVideos(@el.data("streams")) is true + @videoType = "youtube" + @fetchMetadata() + @parseSpeed() + else + @videoType = "html5" + @parseVideoSources @el.data("mp4-source"), @el.data("webm-source"), @el.data("ogg-source") + @speeds = ["0.75", "1.0", "1.25", "1.5"] + @setSpeed($.cookie('video_speed')) + $("#video_#{@id}").data('video', this).addClass('video-load-complete') @hide_captions = $.cookie('hide_captions') == 'true' - if YT.Player + if ((@videoType is "youtube") and (YT.Player)) or ((@videoType is "html5") and (HTML5Video.Player)) + console.log 'one' @embed() else - window.onYouTubePlayerAPIReady = => - @el.each -> - $(this).data('video').embed() + console.log 'two' + if @videoType is "youtube" + console.log 'three' + window.onYouTubePlayerAPIReady = -> + _this.embed() + else if @videoType is "html5" + console.log 'four' + console.log @videoType + console.log HTML5Video.Player + window.onHTML5PlayerAPIReady = -> + _this.embed() youtubeId: (speed)-> @videos[speed || @speed] - parseVideos: (videos) -> + VideoAlpha::parseVideos = (videos) -> + return false if (typeof videos isnt "string") or (videos.length is 0) + + console.log 'We got this far' + console.log videos + @videos = {} - $.each videos.split(/,/), (index, video) => + _this = this + $.each videos.split(/,/), (index, video) -> + speed = undefined video = video.split(/:/) - speed = parseFloat(video[0]).toFixed(2).replace /\.00$/, '.0' - @videos[speed] = video[1] + speed = parseFloat(video[0]).toFixed(2).replace(/\.00$/, ".0") + _this.videos[speed] = video[1] + true + + VideoAlpha::parseVideoSources = (mp4Source, webmSource, oggSource) -> + @html5Sources = + mp4: null + webm: null + ogg: null + + @html5Sources.mp4 = mp4Source if (typeof mp4Source is "string") and (mp4Source.length > 0) + @html5Sources.webm = webmSource if (typeof webmSource is "string") and (webmSource.length > 0) + @html5Sources.ogg = oggSource if (typeof oggSource is "string") and (oggSource.length > 0) parseSpeed: -> - @setSpeed($.cookie('video_speed')) @speeds = ($.map @videos, (url, speed) -> speed).sort() + @setSpeed($.cookie('video_speed')) - setSpeed: (newSpeed) -> - if @videos[newSpeed] != undefined + VideoAlpha::setSpeed = (newSpeed) -> + if @speeds.indexOf(newSpeed) isnt -1 @speed = newSpeed - $.cookie('video_speed', "#{newSpeed}", expires: 3650, path: '/') + $.cookie "video_speed", "" + newSpeed, + expires: 3650 + path: "/" else - @speed = '1.0' + @speed = "1.0" embed: -> @player = new VideoPlayer video: this @@ -55,9 +92,14 @@ class @Video getDuration: -> @metadata[@youtubeId()].duration - log: (eventName) -> - Logger.log eventName, + VideoAlpha::log = (eventName) -> + logInfo = id: @id code: @youtubeId() currentTime: @player.currentTime speed: @speed + + if @videoType is "youtube" + logInfo.code = @youtubeId() + else logInfo.code = "html5" if @videoType is "html5" + Logger.log eventName, logInfo diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js b/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js new file mode 100644 index 0000000000..3db8bb97ed --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js @@ -0,0 +1,196 @@ +console.log('We are in "html5_video.js" script.'); + +this.HTML5Video = (function () { + var HTML5Video = {}; + + HTML5Video.Player = (function () { + + /* + * Constructor function for HTML5 Video player. + * + * @el - A DOM element where the HTML5 player will be inserted (as returned by jQuery(selector) function), + * or a selector string which will be used to select an element. This is a required parameter. + * + * @config - An object whose properties will be used as configuration options for the HTML5 video + * player. This is an optional parameter. In the case if this parameter is missing, or some of the config + * object's properties are missing, defaults will be used. The available options (and their defaults) are as + * follows: + * + * config = { + * 'width': 640, + * + * 'height': 390, + * + * 'videoSources': null, // An object of with properties being video sources. The property name is the + * // video format of the source. Supported video formats are: 'mp4', 'webm', and + * // 'ogg'. By default videoSources property is null. This means that the + * // player will initialize, and not play anything. If you do not provide a + * // 'videoSource' option, you can later call loadVideoBySource() method to load + * // a video and start playing it. + * + * 'playerVars': { // Object's properties identify player parameters. + * + * 'controls': 1, // Possible values: 0, or 1. Value of 1 will enable the default browser video + * // controls. + * + * 'start': null, // Possible values: positive integer. Position from which to start playing the + * // video. Measured in seconds. If value is null, or 'start' property is not + * // specified, the video will start playing from the beginning. + * + * 'end': null // Possible values: positive integer. Position when to stop playing the + * // video. Measured in seconds. If value is null, or 'end' property is not + * // specified, the video will end playing at the end. + * + * }, + * + * 'events': { // Object's properties identify the events that the API fires, and the + * // functions (event listeners) that the API will call when those events occur. + * // If value is null, or property is not specified, then no callback will be + * // called for that event. + * + * 'onReady': null, + * 'onStateChange': null, + * 'onPlaybackQualityChange': null + * } + * } + */ + function Player(el, config) { + console.log('We are inside HTML5Video.Player constructor.'); + + if (typeof el === 'string') { + this.el = $(el); + } else if ($.isPlainObject(el) === true) { + this.el = el; + } else { + // Error. el parameter is required. + + // TODO: Make sure that nothing breaks if one of the methods available via this object's prototype + // is called after we return. + + return; + } + + console.log('We got a proper DOM element.'); + + if ($.isPlainObject(config) === true) { + this.config = config; + } else { + this.config = { + 'width': 640, + 'height': 390, + 'videoSource': '', + 'playerVars': { + 'controls': 1, + 'start': null, + 'end': null + }, + 'events': { + 'onReady': null, + 'onStateChange': null, + 'onPlaybackQualityChange': null + } + }; + } + + console.log('The config is:'); + console.log(this.config); + } + + /* + * This function returns the quality of the video. Possible return values are (type String) + * + * highres + * hd1080 + * hd720 + * large + * medium + * small + * + * It returns undefined if there is no current video. + * + * If there is a current video, but it is impossible to determine it's quality, the function will return + * 'medium'. + */ + Player.prototype.getPlayBackQuality = function () { + if (this.config.videoSource === '') { + return undefined; + } + + // TODO: Figure out if we can get the quality of a video from a source (when it is loaded by the browser). + + return 'medium'; + }; + + /* + * The original YouTube API function player.setPlayBackQuality changed (if it was possible) the quality of the + * played video. In our case, this function will not do anything because we can't change the quality of HTML5 + * video since we only get one source of video with one quality. + */ + Player.prototype.setPlayBackQuality = function (value) { + + }; + + Player.prototype.pauseVideo = function () { + + }; + + Player.prototype.sekkTo = function () { + + }; + + // YouTube API has player.loadVideoById, but since we are working with a video source, we will rename this + // function accordingly. + Player.prototype.loadVideoBySource = function () { + + }; + + // YouTube API has player.cueVideoById, but since we are working with a video source, we will rename this + // function accordingly. + Player.prototype.cueVideoBySource = function () { + + }; + + Player.prototype.setVolume = function () { + + }; + + Player.prototype.getCurrentTime = function () { + + }; + + Player.prototype.playVideo = function () { + + }; + + Player.prototype.getPlayerState = function () { + + }; + + Player.prototype.pauseVideo = function () { + + }; + + Player.prototype.setVolume = function () { + + }; + + Player.prototype.getVolume = function () { + + }; + + return Player; + }()); + + HTML5Video.PlayerState = { + 'UNSTARTED': -1, + 'ENDED': 0, + 'PLAYING': 1, + 'PAUSED': 2, + 'BUFFERING': 3, + 'CUED': 5 + }; + + return HTML5Video; +}()); + +console.log(HTML5Video); diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee index 22308a5568..3bee570bc8 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee @@ -1,7 +1,8 @@ class @VideoPlayer extends Subview initialize: -> - # Define a missing constant of Youtube API - YT.PlayerState.UNSTARTED = -1 + if @video.videoType is 'youtube' + # Define a missing constant of Youtube API + YT.PlayerState.UNSTARTED = -1 @currentTime = 0 @el = $("#video_#{@video.id}") @@ -25,6 +26,7 @@ class @VideoPlayer extends Subview @toggleFullScreen(event) render: -> + console.log '1.1' @control = new VideoControl el: @$('.video-controls') @qualityControl = new VideoQualityControl el: @$('.secondary-controls') @caption = new VideoCaption @@ -34,6 +36,7 @@ class @VideoPlayer extends Subview captionAssetPath: @video.caption_asset_path unless onTouchBasedDevice() @volumeControl = new VideoVolumeControl el: @$('.secondary-controls') + console.log '1.2' @speedControl = new VideoSpeedControl el: @$('.secondary-controls'), speeds: @video.speeds, currentSpeed: @currentSpeed() @progressSlider = new VideoProgressSlider el: @$('.slider') @playerVars = @@ -43,20 +46,31 @@ class @VideoPlayer extends Subview showinfo: 0 enablejsapi: 1 modestbranding: 1 + console.log '1.3' if @video.start @playerVars.start = @video.start @playerVars.wmode = 'window' if @video.end # work in AS3, not HMLT5. but iframe use AS3 @playerVars.end = @video.end + console.log '1.4' - @player = new YT.Player @video.id, - playerVars: @playerVars - videoId: @video.youtubeId() - events: - onReady: @onReady - onStateChange: @onStateChange - onPlaybackQualityChange: @onPlaybackQualityChange + if @video.videoType is 'html5' + @player = new HTML5Video.Player @video.id, + playerVars: @playerVars, + videoSources: @video.html5Sources, + events: + onReady: @onReady + onStateChange: @onStateChange + onPlaybackQualityChange: @onPlaybackQualityChange + else if @video.videoType is 'youtube' + @player = new YT.Player @video.id, + playerVars: @playerVars + videoId: @video.youtubeId() + events: + onReady: @onReady + onStateChange: @onStateChange + onPlaybackQualityChange: @onPlaybackQualityChange @caption.hideCaptions(@['video'].hide_captions) addToolTip: -> diff --git a/common/lib/xmodule/xmodule/videoalpha_module.py b/common/lib/xmodule/xmodule/videoalpha_module.py index e41f9783e4..912505d0a6 100644 --- a/common/lib/xmodule/xmodule/videoalpha_module.py +++ b/common/lib/xmodule/xmodule/videoalpha_module.py @@ -19,11 +19,13 @@ import time log = logging.getLogger(__name__) -class VideoModule(XModule): +class VideoAlphaModule(XModule): video_time = 0 icon_class = 'video' - js = {'coffee': + js = { + 'js': [resource_string(__name__, 'js/src/videoalpha/display/html5_video.js')], + 'coffee': [resource_string(__name__, 'js/src/time.coffee'), resource_string(__name__, 'js/src/videoalpha/display.coffee')] + [resource_string(__name__, 'js/src/videoalpha/display/' + filename) @@ -31,7 +33,7 @@ class VideoModule(XModule): in sorted(resource_listdir(__name__, 'js/src/videoalpha/display')) if filename.endswith('.coffee')]} css = {'scss': [resource_string(__name__, 'css/videoalpha/display.scss')]} - js_module_name = "Video" + js_module_name = "VideoAlpha" def __init__(self, system, location, definition, descriptor, instance_state=None, shared_state=None, **kwargs): @@ -145,6 +147,6 @@ class VideoModule(XModule): class VideoAlphaDescriptor(RawDescriptor): - module_class = VideoModule + module_class = VideoAlphaModule stores_state = True template_dir_name = "videoalpha" diff --git a/lms/templates/videoalpha.html b/lms/templates/videoalpha.html index 6cee9ed39b..58704ed6b1 100644 --- a/lms/templates/videoalpha.html +++ b/lms/templates/videoalpha.html @@ -2,11 +2,24 @@

      ${display_name}

      % endif + %if settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']:
      %else: -
      +
      From ed00d20708721c62978622f58169b1654f22fb1b Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Thu, 31 Jan 2013 10:35:07 +0200 Subject: [PATCH 43/75] Updated all coffee scripts to define and use Alpha version of classes. Added missing functions for HTML5Video.Player prototype. Now bare bones works without errors. YouTube version also works without errors. --- .../xmodule/js/src/videoalpha/display.coffee | 23 +++------- .../js/src/videoalpha/display/_subview.coffee | 2 +- .../js/src/videoalpha/display/html5_video.js | 43 +++++++++++-------- .../videoalpha/display/video_caption.coffee | 2 +- .../videoalpha/display/video_control.coffee | 2 +- .../videoalpha/display/video_player.coffee | 24 +++++------ .../display/video_progress_slider.coffee | 2 +- .../display/video_quality_control.coffee | 2 +- .../display/video_speed_control.coffee | 2 +- .../display/video_volume_control.coffee | 2 +- lms/templates/videoalpha.html | 2 +- 11 files changed, 49 insertions(+), 57 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee index b97b02e68c..735e916573 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee @@ -9,7 +9,6 @@ class @VideoAlpha @show_captions = @el.data('show-captions') == "true" window.player = null @el = $("#video_#{@id}") - if @parseVideos(@el.data("streams")) is true @videoType = "youtube" @fetchMetadata() @@ -18,25 +17,21 @@ class @VideoAlpha @videoType = "html5" @parseVideoSources @el.data("mp4-source"), @el.data("webm-source"), @el.data("ogg-source") @speeds = ["0.75", "1.0", "1.25", "1.5"] + @videos = + "0.75": "" + "1.0": "" + "1.25": "" + "1.5": "" @setSpeed($.cookie('video_speed')) - $("#video_#{@id}").data('video', this).addClass('video-load-complete') - @hide_captions = $.cookie('hide_captions') == 'true' - if ((@videoType is "youtube") and (YT.Player)) or ((@videoType is "html5") and (HTML5Video.Player)) - console.log 'one' @embed() else - console.log 'two' if @videoType is "youtube" - console.log 'three' window.onYouTubePlayerAPIReady = -> _this.embed() else if @videoType is "html5" - console.log 'four' - console.log @videoType - console.log HTML5Video.Player window.onHTML5PlayerAPIReady = -> _this.embed() @@ -45,10 +40,6 @@ class @VideoAlpha VideoAlpha::parseVideos = (videos) -> return false if (typeof videos isnt "string") or (videos.length is 0) - - console.log 'We got this far' - console.log videos - @videos = {} _this = this $.each videos.split(/,/), (index, video) -> @@ -63,7 +54,6 @@ class @VideoAlpha mp4: null webm: null ogg: null - @html5Sources.mp4 = mp4Source if (typeof mp4Source is "string") and (mp4Source.length > 0) @html5Sources.webm = webmSource if (typeof webmSource is "string") and (webmSource.length > 0) @html5Sources.ogg = oggSource if (typeof oggSource is "string") and (oggSource.length > 0) @@ -82,7 +72,7 @@ class @VideoAlpha @speed = "1.0" embed: -> - @player = new VideoPlayer video: this + @player = new VideoPlayerAlpha video: this fetchMetadata: (url) -> @metadata = {} @@ -98,7 +88,6 @@ class @VideoAlpha code: @youtubeId() currentTime: @player.currentTime speed: @speed - if @videoType is "youtube" logInfo.code = @youtubeId() else logInfo.code = "html5" if @videoType is "html5" diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/_subview.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/_subview.coffee index 2e14289843..6b86296dfa 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/_subview.coffee +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/_subview.coffee @@ -1,4 +1,4 @@ -class @Subview +class @SubviewAlpha constructor: (options) -> $.each options, (key, value) => @[key] = value diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js b/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js index 3db8bb97ed..c61d725e5c 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js @@ -1,5 +1,3 @@ -console.log('We are in "html5_video.js" script.'); - this.HTML5Video = (function () { var HTML5Video = {}; @@ -55,8 +53,6 @@ this.HTML5Video = (function () { * } */ function Player(el, config) { - console.log('We are inside HTML5Video.Player constructor.'); - if (typeof el === 'string') { this.el = $(el); } else if ($.isPlainObject(el) === true) { @@ -70,8 +66,6 @@ this.HTML5Video = (function () { return; } - console.log('We got a proper DOM element.'); - if ($.isPlainObject(config) === true) { this.config = config; } else { @@ -91,9 +85,6 @@ this.HTML5Video = (function () { } }; } - - console.log('The config is:'); - console.log(this.config); } /* @@ -122,11 +113,11 @@ this.HTML5Video = (function () { }; /* - * The original YouTube API function player.setPlayBackQuality changed (if it was possible) the quality of the + * The original YouTube API function player.setPlaybackQuality changed (if it was possible) the quality of the * played video. In our case, this function will not do anything because we can't change the quality of HTML5 * video since we only get one source of video with one quality. */ - Player.prototype.setPlayBackQuality = function (value) { + Player.prototype.setPlaybackQuality = function (value) { }; @@ -134,20 +125,30 @@ this.HTML5Video = (function () { }; - Player.prototype.sekkTo = function () { + Player.prototype.seekTo = function () { }; // YouTube API has player.loadVideoById, but since we are working with a video source, we will rename this - // function accordingly. - Player.prototype.loadVideoBySource = function () { + // function accordingly. However, not to cause conflicts, there will also be a loadVideoById function which + // will call this function. + Player.prototype.loadVideoBySource = function (source) { }; - // YouTube API has player.cueVideoById, but since we are working with a video source, we will rename this - // function accordingly. - Player.prototype.cueVideoBySource = function () { + Player.prototype.loadVideoById = function (id) { + this.loadVideoBySource(id); + } + // YouTube API has player.cueVideoById, but since we are working with a video source, we will rename this + // function accordingly. However, not to cause conflicts, there will also be a cueVideoById function which + // will call this function. + Player.prototype.cueVideoBySource = function (source) { + + }; + + Player.prototype.cueVideoById = function (id) { + this.cueVideoBySource(id); }; Player.prototype.setVolume = function () { @@ -178,6 +179,12 @@ this.HTML5Video = (function () { }; + Player.prototype.getDuration = function () { + // TODO: Return valid video duration. + + return 0; + }; + return Player; }()); @@ -192,5 +199,3 @@ this.HTML5Video = (function () { return HTML5Video; }()); - -console.log(HTML5Video); diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_caption.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_caption.coffee index e840cd2a77..9ecdaca474 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_caption.coffee +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_caption.coffee @@ -1,4 +1,4 @@ -class @VideoCaption extends Subview +class @VideoCaptionAlpha extends SubviewAlpha initialize: -> @loaded = false diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_control.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_control.coffee index 856549c3e2..311b69dbbe 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_control.coffee +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_control.coffee @@ -1,4 +1,4 @@ -class @VideoControl extends Subview +class @VideoControlAlpha extends SubviewAlpha bind: -> @$('.video_control').click @togglePlayback diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee index 3bee570bc8..f295096388 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee @@ -1,4 +1,4 @@ -class @VideoPlayer extends Subview +class @VideoPlayerAlpha extends SubviewAlpha initialize: -> if @video.videoType is 'youtube' # Define a missing constant of Youtube API @@ -26,19 +26,17 @@ class @VideoPlayer extends Subview @toggleFullScreen(event) render: -> - console.log '1.1' - @control = new VideoControl el: @$('.video-controls') - @qualityControl = new VideoQualityControl el: @$('.secondary-controls') - @caption = new VideoCaption + @control = new VideoControlAlpha el: @$('.video-controls') + @qualityControl = new VideoQualityControlAlpha el: @$('.secondary-controls') + @caption = new VideoCaptionAlpha el: @el youtubeId: @video.youtubeId('1.0') currentSpeed: @currentSpeed() captionAssetPath: @video.caption_asset_path unless onTouchBasedDevice() - @volumeControl = new VideoVolumeControl el: @$('.secondary-controls') - console.log '1.2' - @speedControl = new VideoSpeedControl el: @$('.secondary-controls'), speeds: @video.speeds, currentSpeed: @currentSpeed() - @progressSlider = new VideoProgressSlider el: @$('.slider') + @volumeControl = new VideoVolumeControlAlpha el: @$('.secondary-controls') + @speedControl = new VideoSpeedControlAlpha el: @$('.secondary-controls'), speeds: @video.speeds, currentSpeed: @currentSpeed() + @progressSlider = new VideoProgressSliderAlpha el: @$('.slider') @playerVars = controls: 0 wmode: 'transparent' @@ -46,15 +44,12 @@ class @VideoPlayer extends Subview showinfo: 0 enablejsapi: 1 modestbranding: 1 - console.log '1.3' if @video.start @playerVars.start = @video.start @playerVars.wmode = 'window' if @video.end # work in AS3, not HMLT5. but iframe use AS3 @playerVars.end = @video.end - console.log '1.4' - if @video.videoType is 'html5' @player = new HTML5Video.Player @video.id, playerVars: @playerVars, @@ -182,7 +177,10 @@ class @VideoPlayer extends Subview @player.pauseVideo() if @player.pauseVideo duration: -> - @video.getDuration() + if @video.videoType is "youtube" + return @video.getDuration() + else return @player.getDuration() if @video.videoType is "html5" + 0 currentSpeed: -> @video.speed diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_progress_slider.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_progress_slider.coffee index 874756cb71..19eff226fb 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_progress_slider.coffee +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_progress_slider.coffee @@ -1,4 +1,4 @@ -class @VideoProgressSlider extends Subview +class @VideoProgressSliderAlpha extends SubviewAlpha initialize: -> @buildSlider() unless onTouchBasedDevice() diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_quality_control.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_quality_control.coffee index f8f6167075..c67969e34e 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_quality_control.coffee +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_quality_control.coffee @@ -1,4 +1,4 @@ -class @VideoQualityControl extends Subview +class @VideoQualityControlAlpha extends SubviewAlpha initialize: -> @quality = null; diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_speed_control.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_speed_control.coffee index 1d0d8b7d44..7b75baddb8 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_speed_control.coffee +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_speed_control.coffee @@ -1,4 +1,4 @@ -class @VideoSpeedControl extends Subview +class @VideoSpeedControlAlpha extends SubviewAlpha bind: -> @$('.video_speeds a').click @changeVideoSpeed if onTouchBasedDevice() diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_volume_control.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_volume_control.coffee index 096b50042d..8c17012ca2 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_volume_control.coffee +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_volume_control.coffee @@ -1,4 +1,4 @@ -class @VideoVolumeControl extends Subview +class @VideoVolumeControlAlpha extends SubviewAlpha initialize: -> @currentVolume = 100 diff --git a/lms/templates/videoalpha.html b/lms/templates/videoalpha.html index 58704ed6b1..7761eb980d 100644 --- a/lms/templates/videoalpha.html +++ b/lms/templates/videoalpha.html @@ -2,7 +2,7 @@

      ${display_name}

      % endif - + %if settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']:
      From 786c11484ea3a3795dc2657c71246734228aebda Mon Sep 17 00:00:00 2001 From: Vasyl Nakvasiuk Date: Thu, 31 Jan 2013 11:23:13 +0200 Subject: [PATCH 44/75] tag now support multi-source --- common/lib/xmodule/xmodule/video_module.py | 5 ++--- .../lib/xmodule/xmodule/videoalpha_module.py | 20 ++++++++++++------- lms/templates/videoalpha.html | 6 +++--- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/common/lib/xmodule/xmodule/video_module.py b/common/lib/xmodule/xmodule/video_module.py index 359b97df7c..27388f7630 100644 --- a/common/lib/xmodule/xmodule/video_module.py +++ b/common/lib/xmodule/xmodule/video_module.py @@ -4,6 +4,8 @@ import logging from lxml import etree from pkg_resources import resource_string, resource_listdir +from django.http import Http404 + from xmodule.x_module import XModule from xmodule.raw_module import RawDescriptor from xmodule.modulestore.xml import XMLModuleStore @@ -13,9 +15,6 @@ from xmodule.contentstore.content import StaticContent import datetime import time -import datetime -import time - log = logging.getLogger(__name__) diff --git a/common/lib/xmodule/xmodule/videoalpha_module.py b/common/lib/xmodule/xmodule/videoalpha_module.py index 912505d0a6..06dd993f7a 100644 --- a/common/lib/xmodule/xmodule/videoalpha_module.py +++ b/common/lib/xmodule/xmodule/videoalpha_module.py @@ -4,6 +4,8 @@ import logging from lxml import etree from pkg_resources import resource_string, resource_listdir +from django.http import Http404 + from xmodule.x_module import XModule from xmodule.raw_module import RawDescriptor from xmodule.modulestore.mongo import MongoModuleStore @@ -13,9 +15,6 @@ from xmodule.contentstore.content import StaticContent import datetime import time -import datetime -import time - log = logging.getLogger(__name__) @@ -44,6 +43,9 @@ class VideoAlphaModule(XModule): self.position = 0 self.show_captions = xmltree.get('show_captions', 'true') self.source = self._get_source(xmltree) + self.mp4_source = self._get_source(xmltree, ['mp4']) + self.wemb_source = self._get_source(xmltree, ['wemb']) + self.ogv_source = self._get_source(xmltree, ['ogv']) self.track = self._get_track(xmltree) self.start_time, self.end_time = self._get_timeframe(xmltree) @@ -52,15 +54,16 @@ class VideoAlphaModule(XModule): if 'position' in state: self.position = int(float(state['position'])) - def _get_source(self, xmltree): + def _get_source(self, xmltree, extension=['mp4', 'ogv', 'avi', 'webm']): # find the first valid source - return self._get_first_external(xmltree, 'source') + condition = lambda src: any([src.endswith(ext) for ext in extension]) + return self._get_first_external(xmltree, 'source', condition) def _get_track(self, xmltree): # find the first valid track return self._get_first_external(xmltree, 'track') - def _get_first_external(self, xmltree, tag): + def _get_first_external(self, xmltree, tag, condition=bool): """ Will return the first valid element of the given tag. @@ -69,7 +72,7 @@ class VideoAlphaModule(XModule): result = None for element in xmltree.findall(tag): src = element.get('src') - if src: + if condition(src): result = src break return result @@ -134,6 +137,9 @@ class VideoAlphaModule(XModule): 'streams': self.videoalpha_list(), 'id': self.location.html_id(), 'position': self.position, + 'mp4_source': self.mp4_source, + 'wemb_source': self.wemb_source, + 'ogv_source': self.ogv_source, 'source': self.source, 'track': self.track, 'display_name': self.display_name, diff --git a/lms/templates/videoalpha.html b/lms/templates/videoalpha.html index 7761eb980d..932d247c61 100644 --- a/lms/templates/videoalpha.html +++ b/lms/templates/videoalpha.html @@ -11,9 +11,9 @@ id="video_${id}" class="video" data-streams="" - data-mp4-source="http://clips.vorwaerts-gmbh.de/VfE_html5.mp4" - data-webm-source="http://clips.vorwaerts-gmbh.de/VfE_html5.webm" - data-ogg-source="http://clips.vorwaerts-gmbh.de/VfE_html5.ogv" + ${'data-mp4-source="{}"'.format(mp4_source) if mp4_source else ''} + ${'data-wemb-source="{}"'.format(wemb_source) if wemb_source else ''} + ${'data-ogg-source="{}"'.format(ogv_source) if ogv_source else ''} data-caption-data-dir="${data_dir}" data-show-captions="${show_captions}" data-start="${start}" From 067f2e97f524d09f815f97c8660e4dacb7422ff6 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Thu, 31 Jan 2013 12:48:03 +0200 Subject: [PATCH 45/75] Enabled play/pause buttons for HTML5 video player. --- .../js/src/videoalpha/display/html5_video.js | 127 ++++++++++++++---- .../videoalpha/display/video_player.coffee | 21 ++- lms/templates/videoalpha.html | 2 - 3 files changed, 115 insertions(+), 35 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js b/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js index c61d725e5c..05a2e97bf4 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js @@ -19,7 +19,7 @@ this.HTML5Video = (function () { * * 'height': 390, * - * 'videoSources': null, // An object of with properties being video sources. The property name is the + * 'videoSources': {}, // An object of with properties being video sources. The property name is the * // video format of the source. Supported video formats are: 'mp4', 'webm', and * // 'ogg'. By default videoSources property is null. This means that the * // player will initialize, and not play anything. If you do not provide a @@ -53,12 +53,14 @@ this.HTML5Video = (function () { * } */ function Player(el, config) { + var sourceStr, _this; + if (typeof el === 'string') { this.el = $(el); - } else if ($.isPlainObject(el) === true) { + } else if (el instanceof jQuery) { this.el = el; } else { - // Error. el parameter is required. + // Error. Parameter el does not have a recognized type. // TODO: Make sure that nothing breaks if one of the methods available via this object's prototype // is called after we return. @@ -69,22 +71,99 @@ this.HTML5Video = (function () { if ($.isPlainObject(config) === true) { this.config = config; } else { - this.config = { - 'width': 640, - 'height': 390, - 'videoSource': '', - 'playerVars': { - 'controls': 1, - 'start': null, - 'end': null - }, - 'events': { - 'onReady': null, - 'onStateChange': null, - 'onPlaybackQualityChange': null - } - }; + // Error. Parameter config does not have a recognized type. + + // TODO: Make sure that nothing breaks if one of the methods available via this object's prototype + // is called after we return. + + return; } + + sourceStr = { + 'mp4': ' ', + 'webm': ' ', + 'ogg': ' ' + }; + + _this = this; + $.each(sourceStr, function (videoType, videoSource) { + if ( + (_this.config.videoSources.hasOwnProperty(videoType) === true) && + (typeof _this.config.videoSources[videoType] === 'string') && + (_this.config.videoSources[videoType].length > 0) + ) { + sourceStr[videoType] = + ' '; + } + }); + + this.playerState = HTML5Video.PlayerState.UNSTARTED; + + this.videoEl = $( + '' + ); + + this.video = this.videoEl[0]; + + this.video.addEventListener('canplay', function () { + console.log('We got a "canplay" event.'); + + _this.playerState = HTML5Video.PlayerState.PAUSED; + + if ($.isFunction(_this.config.events.onReady) === true) { + console.log('Callback function "onReady" is defined.'); + + _this.config.events.onReady({}); + } + }, false); + this.video.addEventListener('play', function () { + console.log('We got a "play" event.'); + + _this.playerState = HTML5Video.PlayerState.PLAYING; + + if ($.isFunction(_this.config.events.onStateChange) === true) { + console.log('Callback function "onStateChange" is defined.'); + + _this.config.events.onStateChange({ + 'data': _this.playerState + }); + } + }, false); + this.video.addEventListener('pause', function () { + console.log('We got a "pause" event.'); + + _this.playerState = HTML5Video.PlayerState.PAUSED; + + if ($.isFunction(_this.config.events.onStateChange) === true) { + console.log('Callback function "onStateChange" is defined.'); + + _this.config.events.onStateChange({ + 'data': _this.playerState + }); + } + }, false); + this.video.addEventListener('ended', function () { + console.log('We got a "ended" event.'); + + _this.playerState = HTML5Video.PlayerState.ENDED; + + if ($.isFunction(_this.config.events.onStateChange) === true) { + console.log('Callback function "onStateChange" is defined.'); + + _this.config.events.onStateChange({ + 'data': _this.playerState + }); + } + }, false); + + this.videoEl.appendTo(this.el.find('.video-player div')); } /* @@ -122,7 +201,9 @@ this.HTML5Video = (function () { }; Player.prototype.pauseVideo = function () { + console.log('Player.prototype.pauseVideo'); + this.video.pause(); }; Player.prototype.seekTo = function () { @@ -160,21 +241,15 @@ this.HTML5Video = (function () { }; Player.prototype.playVideo = function () { + console.log('Player.prototype.playVideo'); + this.video.play(); }; Player.prototype.getPlayerState = function () { }; - Player.prototype.pauseVideo = function () { - - }; - - Player.prototype.setVolume = function () { - - }; - Player.prototype.getVolume = function () { }; diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee index f295096388..f7f90971df 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee @@ -1,8 +1,11 @@ class @VideoPlayerAlpha extends SubviewAlpha initialize: -> if @video.videoType is 'youtube' + @PlayerState = YT.PlayerState # Define a missing constant of Youtube API - YT.PlayerState.UNSTARTED = -1 + @PlayerState.UNSTARTED = -1 + else if @video.videoType is 'html5' + @PlayerState = HTML5Video.PlayerState @currentTime = 0 @el = $("#video_#{@video.id}") @@ -51,7 +54,7 @@ class @VideoPlayerAlpha extends SubviewAlpha # work in AS3, not HMLT5. but iframe use AS3 @playerVars.end = @video.end if @video.videoType is 'html5' - @player = new HTML5Video.Player @video.id, + @player = new HTML5Video.Player @video.el, playerVars: @playerVars, videoSources: @video.html5Sources, events: @@ -80,13 +83,13 @@ class @VideoPlayerAlpha extends SubviewAlpha onStateChange: (event) => switch event.data - when YT.PlayerState.UNSTARTED + when @PlayerState.UNSTARTED @onUnstarted() - when YT.PlayerState.PLAYING + when @PlayerState.PLAYING @onPlay() - when YT.PlayerState.PAUSED + when @PlayerState.PAUSED @onPause() - when YT.PlayerState.ENDED + when @PlayerState.ENDED @onEnded() onPlaybackQualityChange: (event, value) => @@ -168,12 +171,16 @@ class @VideoPlayerAlpha extends SubviewAlpha # Delegates play: => + console.log 'Play clicked' + console.log @player.playVideo @player.playVideo() if @player.playVideo isPlaying: -> - @player.getPlayerState() == YT.PlayerState.PLAYING + @player.getPlayerState() == @PlayerState.PLAYING pause: => + console.log 'Pause clicked' + console.log @player.pauseVideo @player.pauseVideo() if @player.pauseVideo duration: -> diff --git a/lms/templates/videoalpha.html b/lms/templates/videoalpha.html index 932d247c61..9d146a222c 100644 --- a/lms/templates/videoalpha.html +++ b/lms/templates/videoalpha.html @@ -2,8 +2,6 @@

      ${display_name}

      % endif - - %if settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']:
      %else: From 80fa9d0116ad84654549b49d26027bd338cbfdfa Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Thu, 31 Jan 2013 13:24:17 +0200 Subject: [PATCH 46/75] Added support for time line in HTML5. --- .../js/src/videoalpha/display/html5_video.js | 25 ++----------------- .../videoalpha/display/video_player.coffee | 7 ++---- 2 files changed, 4 insertions(+), 28 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js b/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js index 05a2e97bf4..555d12187d 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js @@ -113,50 +113,34 @@ this.HTML5Video = (function () { this.video = this.videoEl[0]; this.video.addEventListener('canplay', function () { - console.log('We got a "canplay" event.'); - _this.playerState = HTML5Video.PlayerState.PAUSED; if ($.isFunction(_this.config.events.onReady) === true) { - console.log('Callback function "onReady" is defined.'); - _this.config.events.onReady({}); } }, false); this.video.addEventListener('play', function () { - console.log('We got a "play" event.'); - _this.playerState = HTML5Video.PlayerState.PLAYING; if ($.isFunction(_this.config.events.onStateChange) === true) { - console.log('Callback function "onStateChange" is defined.'); - _this.config.events.onStateChange({ 'data': _this.playerState }); } }, false); this.video.addEventListener('pause', function () { - console.log('We got a "pause" event.'); - _this.playerState = HTML5Video.PlayerState.PAUSED; if ($.isFunction(_this.config.events.onStateChange) === true) { - console.log('Callback function "onStateChange" is defined.'); - _this.config.events.onStateChange({ 'data': _this.playerState }); } }, false); this.video.addEventListener('ended', function () { - console.log('We got a "ended" event.'); - _this.playerState = HTML5Video.PlayerState.ENDED; if ($.isFunction(_this.config.events.onStateChange) === true) { - console.log('Callback function "onStateChange" is defined.'); - _this.config.events.onStateChange({ 'data': _this.playerState }); @@ -201,7 +185,6 @@ this.HTML5Video = (function () { }; Player.prototype.pauseVideo = function () { - console.log('Player.prototype.pauseVideo'); this.video.pause(); }; @@ -237,12 +220,10 @@ this.HTML5Video = (function () { }; Player.prototype.getCurrentTime = function () { - + return this.video.currentTime; }; Player.prototype.playVideo = function () { - console.log('Player.prototype.playVideo'); - this.video.play(); }; @@ -255,9 +236,7 @@ this.HTML5Video = (function () { }; Player.prototype.getDuration = function () { - // TODO: Return valid video duration. - - return 0; + return this.video.duration; }; return Player; diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee index f7f90971df..20b16ae01c 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee @@ -171,22 +171,19 @@ class @VideoPlayerAlpha extends SubviewAlpha # Delegates play: => - console.log 'Play clicked' - console.log @player.playVideo @player.playVideo() if @player.playVideo isPlaying: -> @player.getPlayerState() == @PlayerState.PLAYING pause: => - console.log 'Pause clicked' - console.log @player.pauseVideo @player.pauseVideo() if @player.pauseVideo duration: -> if @video.videoType is "youtube" return @video.getDuration() - else return @player.getDuration() if @video.videoType is "html5" + else if @video.videoType is "html5" + return @player.getDuration() 0 currentSpeed: -> From 4f08d96cfd1c33207742772a64ae0820a45bd745 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Thu, 31 Jan 2013 14:34:36 +0200 Subject: [PATCH 47/75] Added ability to change playback rate, added pause/play on video click, minor improvements. --- .../xmodule/js/src/videoalpha/display.coffee | 1 + .../js/src/videoalpha/display/html5_video.js | 47 ++++++++++++++++--- .../videoalpha/display/video_player.coffee | 12 +++-- 3 files changed, 49 insertions(+), 11 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee index 735e916573..86f8e896c0 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee @@ -25,6 +25,7 @@ class @VideoAlpha @setSpeed($.cookie('video_speed')) $("#video_#{@id}").data('video', this).addClass('video-load-complete') @hide_captions = $.cookie('hide_captions') == 'true' + _this = this if ((@videoType is "youtube") and (YT.Player)) or ((@videoType is "html5") and (HTML5Video.Player)) @embed() else diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js b/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js index 555d12187d..12a5734f13 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js @@ -112,6 +112,28 @@ this.HTML5Video = (function () { this.video = this.videoEl[0]; + this.videoEl.on('click', function (event) { + if (_this.playerState === HTML5Video.PlayerState.PAUSED) { + _this.video.play(); + _this.playerState = HTML5Video.PlayerState.PLAYING; + + if ($.isFunction(_this.config.events.onStateChange) === true) { + _this.config.events.onStateChange({ + 'data': _this.playerState + }); + } + } else if (_this.playerState === HTML5Video.PlayerState.PLAYING) { + _this.video.pause(); + _this.playerState = HTML5Video.PlayerState.PAUSED; + + if ($.isFunction(_this.config.events.onStateChange) === true) { + _this.config.events.onStateChange({ + 'data': _this.playerState + }); + } + } + }); + this.video.addEventListener('canplay', function () { _this.playerState = HTML5Video.PlayerState.PAUSED; @@ -185,12 +207,13 @@ this.HTML5Video = (function () { }; Player.prototype.pauseVideo = function () { - this.video.pause(); }; - Player.prototype.seekTo = function () { - + Player.prototype.seekTo = function (value) { + if ((typeof value === 'number') && (value <= this.video.duration) && (value >= 0)) { + this.video.currentTime = value; + } }; // YouTube API has player.loadVideoById, but since we are working with a video source, we will rename this @@ -215,8 +238,10 @@ this.HTML5Video = (function () { this.cueVideoBySource(id); }; - Player.prototype.setVolume = function () { - + Player.prototype.setVolume = function (value) { + if ((typeof value === 'number') && (value <= 100) && (value >= 0)) { + this.video.volume = value * 0.01; + } }; Player.prototype.getCurrentTime = function () { @@ -232,13 +257,23 @@ this.HTML5Video = (function () { }; Player.prototype.getVolume = function () { - + return this.video.volume; }; Player.prototype.getDuration = function () { return this.video.duration; }; + Player.prototype.setSpeed = function (value) { + var newSpeed; + + newSpeed = parseFloat(value); + + if (isFinite(newSpeed) === true) { + this.video.playbackRate = value; + } + } + return Player; }()); diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee index 20b16ae01c..0094a2f47f 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee @@ -139,11 +139,13 @@ class @VideoPlayerAlpha extends SubviewAlpha newSpeed = parseFloat(newSpeed).toFixed(2).replace /\.00$/, '.0' @video.setSpeed(newSpeed) @caption.currentSpeed = newSpeed - - if @isPlaying() - @player.loadVideoById(@video.youtubeId(), @currentTime) - else - @player.cueVideoById(@video.youtubeId(), @currentTime) + if @video.videoType is 'html5' + @player.setSpeed(newSpeed) + else if @video.videoType is 'youtube' + if @isPlaying() + @player.loadVideoById(@video.youtubeId(), @currentTime) + else + @player.cueVideoById(@video.youtubeId(), @currentTime) @updatePlayTime @currentTime onVolumeChange: (event, volume) => From 5cce5688363ee3728b45f4c042243c44916824c4 Mon Sep 17 00:00:00 2001 From: Vasyl Nakvasiuk Date: Thu, 31 Jan 2013 14:52:58 +0200 Subject: [PATCH 48/75] add docstrings in videoalpha_module.py --- common/lib/xmodule/xmodule/videoalpha_module.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/common/lib/xmodule/xmodule/videoalpha_module.py b/common/lib/xmodule/xmodule/videoalpha_module.py index 06dd993f7a..47183a7fc7 100644 --- a/common/lib/xmodule/xmodule/videoalpha_module.py +++ b/common/lib/xmodule/xmodule/videoalpha_module.py @@ -54,9 +54,10 @@ class VideoAlphaModule(XModule): if 'position' in state: self.position = int(float(state['position'])) - def _get_source(self, xmltree, extension=['mp4', 'ogv', 'avi', 'webm']): - # find the first valid source - condition = lambda src: any([src.endswith(ext) for ext in extension]) + def _get_source(self, xmltree, extensions=['mp4', 'ogv', 'avi', 'webm']): + """Find the first valid source, which ends with one of + `extensions`.""" + condition = lambda src: any([src.endswith(ext) for ext in extensions]) return self._get_first_external(xmltree, 'source', condition) def _get_track(self, xmltree): @@ -64,10 +65,8 @@ class VideoAlphaModule(XModule): return self._get_first_external(xmltree, 'track') def _get_first_external(self, xmltree, tag, condition=bool): - """ - Will return the first valid element - of the given tag. - 'valid' means has a non-empty 'src' attribute + """Will return the first 'valid' element of the given tag. + 'valid' means that `condition('src' attribute) == True` """ result = None for element in xmltree.findall(tag): From 3ac9d54ff7c4fcc0e601b8d03afe78c7bf003375 Mon Sep 17 00:00:00 2001 From: Vasyl Nakvasiuk Date: Thu, 31 Jan 2013 15:43:39 +0200 Subject: [PATCH 49/75] fix wemb -> webm --- common/lib/xmodule/xmodule/videoalpha_module.py | 4 ++-- lms/templates/videoalpha.html | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/videoalpha_module.py b/common/lib/xmodule/xmodule/videoalpha_module.py index 47183a7fc7..7ec63f6015 100644 --- a/common/lib/xmodule/xmodule/videoalpha_module.py +++ b/common/lib/xmodule/xmodule/videoalpha_module.py @@ -44,7 +44,7 @@ class VideoAlphaModule(XModule): self.show_captions = xmltree.get('show_captions', 'true') self.source = self._get_source(xmltree) self.mp4_source = self._get_source(xmltree, ['mp4']) - self.wemb_source = self._get_source(xmltree, ['wemb']) + self.webm_source = self._get_source(xmltree, ['webm']) self.ogv_source = self._get_source(xmltree, ['ogv']) self.track = self._get_track(xmltree) self.start_time, self.end_time = self._get_timeframe(xmltree) @@ -137,7 +137,7 @@ class VideoAlphaModule(XModule): 'id': self.location.html_id(), 'position': self.position, 'mp4_source': self.mp4_source, - 'wemb_source': self.wemb_source, + 'webm_source': self.webm_source, 'ogv_source': self.ogv_source, 'source': self.source, 'track': self.track, diff --git a/lms/templates/videoalpha.html b/lms/templates/videoalpha.html index 9d146a222c..18c8135823 100644 --- a/lms/templates/videoalpha.html +++ b/lms/templates/videoalpha.html @@ -10,7 +10,7 @@ class="video" data-streams="" ${'data-mp4-source="{}"'.format(mp4_source) if mp4_source else ''} - ${'data-wemb-source="{}"'.format(wemb_source) if wemb_source else ''} + ${'data-webm-source="{}"'.format(webm_source) if webm_source else ''} ${'data-ogg-source="{}"'.format(ogv_source) if ogv_source else ''} data-caption-data-dir="${data_dir}" data-show-captions="${show_captions}" From d72036a6dc2adcab45373bf84d71bbacefbef2dc Mon Sep 17 00:00:00 2001 From: Vasyl Nakvasiuk Date: Thu, 31 Jan 2013 19:04:55 +0200 Subject: [PATCH 50/75] add support `sub` attribute for
      %endif -% if source: +% if sources.get('main'):
      -

      Download video here.

      +

      Download video here.

      % endif From 0b73f0a59dd3ea52880202a9e9fe36078f9fd817 Mon Sep 17 00:00:00 2001 From: Vasyl Nakvasiuk Date: Wed, 13 Feb 2013 13:11:59 +0200 Subject: [PATCH 60/75] remove unnecessary code --- common/lib/xmodule/xmodule/videoalpha_module.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/common/lib/xmodule/xmodule/videoalpha_module.py b/common/lib/xmodule/xmodule/videoalpha_module.py index f47a433fa7..b12dd359c3 100644 --- a/common/lib/xmodule/xmodule/videoalpha_module.py +++ b/common/lib/xmodule/xmodule/videoalpha_module.py @@ -111,16 +111,6 @@ class VideoAlphaModule(XModule): return json.dumps({'success': True}) raise Http404() - def get_progress(self): - ''' TODO (vshnayder): Get and save duration of youtube video, then return - fraction watched. - (Be careful to notice when video link changes and update) - - For now, we have no way of knowing if the video has even been watched, so - just return None. - ''' - return None - def get_instance_state(self): #log.debug(u"STATE POSITION {0}".format(self.position)) return json.dumps({'position': self.position}) @@ -139,7 +129,6 @@ class VideoAlphaModule(XModule): return self.system.render_template('videoalpha.html', { 'streams': self.videoalpha_list(), 'id': self.location.html_id(), - 'position': self.position, 'sub': self.sub, 'sources': self.sources, 'track': self.track, From 8cf8dcd10bcbdb45a019eccd072bc59c3675cb22 Mon Sep 17 00:00:00 2001 From: Vasyl Nakvasiuk Date: Wed, 13 Feb 2013 13:28:09 +0200 Subject: [PATCH 61/75] rm `videoalpha_list` method --- common/lib/xmodule/xmodule/videoalpha_module.py | 7 ++----- lms/templates/videoalpha.html | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/common/lib/xmodule/xmodule/videoalpha_module.py b/common/lib/xmodule/xmodule/videoalpha_module.py index b12dd359c3..2861fe57f8 100644 --- a/common/lib/xmodule/xmodule/videoalpha_module.py +++ b/common/lib/xmodule/xmodule/videoalpha_module.py @@ -39,7 +39,7 @@ class VideoAlphaModule(XModule): XModule.__init__(self, system, location, definition, descriptor, instance_state, shared_state, **kwargs) xmltree = etree.fromstring(self.definition['data']) - self.youtube = xmltree.get('youtube') + self.youtube_streams = xmltree.get('youtube') self.sub = xmltree.get('sub') self.position = 0 self.show_captions = xmltree.get('show_captions', 'true') @@ -115,9 +115,6 @@ class VideoAlphaModule(XModule): #log.debug(u"STATE POSITION {0}".format(self.position)) return json.dumps({'position': self.position}) - def videoalpha_list(self): - return self.youtube - def get_html(self): if isinstance(modulestore(), MongoModuleStore): caption_asset_path = StaticContent.get_base_url_path_for_course_assets(self.location) + '/subs_' @@ -127,7 +124,7 @@ class VideoAlphaModule(XModule): caption_asset_path = "/static/{0}/subs/".format(self.metadata['data_dir']) return self.system.render_template('videoalpha.html', { - 'streams': self.videoalpha_list(), + 'youtube_streams': self.youtube_streams, 'id': self.location.html_id(), 'sub': self.sub, 'sources': self.sources, diff --git a/lms/templates/videoalpha.html b/lms/templates/videoalpha.html index baed857a56..2ddcdd57e1 100644 --- a/lms/templates/videoalpha.html +++ b/lms/templates/videoalpha.html @@ -8,7 +8,7 @@
      Date: Wed, 13 Feb 2013 13:35:10 +0200 Subject: [PATCH 62/75] add xml exmaple for videoalpja module in docstring --- common/lib/xmodule/xmodule/videoalpha_module.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/common/lib/xmodule/xmodule/videoalpha_module.py b/common/lib/xmodule/xmodule/videoalpha_module.py index 2861fe57f8..067932a5de 100644 --- a/common/lib/xmodule/xmodule/videoalpha_module.py +++ b/common/lib/xmodule/xmodule/videoalpha_module.py @@ -19,6 +19,18 @@ log = logging.getLogger(__name__) class VideoAlphaModule(XModule): + """ + XML source example: + + + + + + + """ video_time = 0 icon_class = 'video' From b69b88a718cd7b61fc9513414c65cf11135380c7 Mon Sep 17 00:00:00 2001 From: Vasyl Nakvasiuk Date: Wed, 13 Feb 2013 13:35:34 +0200 Subject: [PATCH 63/75] some small fix --- common/lib/xmodule/xmodule/videoalpha_module.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/common/lib/xmodule/xmodule/videoalpha_module.py b/common/lib/xmodule/xmodule/videoalpha_module.py index 067932a5de..6910728d8c 100644 --- a/common/lib/xmodule/xmodule/videoalpha_module.py +++ b/common/lib/xmodule/xmodule/videoalpha_module.py @@ -22,14 +22,14 @@ class VideoAlphaModule(XModule): """ XML source example: - - - - - + + + + + """ video_time = 0 icon_class = 'video' From 1254e11836b89ae49630ce288a923f15f0bfa427 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Wed, 13 Feb 2013 14:09:35 +0200 Subject: [PATCH 64/75] Fixes and additions. Addressing comments by Carlos for pull request 1409. --- .../xmodule/js/src/videoalpha/display.coffee | 32 ++++++------ .../js/src/videoalpha/display/html5_video.js | 4 ++ .../videoalpha/display/video_player.coffee | 52 ++++++++++++++++--- 3 files changed, 64 insertions(+), 24 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee index 8079383f6f..a27362b094 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee @@ -8,13 +8,13 @@ class @VideoAlpha @caption_asset_path = @el.data('caption-asset-path') @show_captions = @el.data('show-captions').toString() == "true" @el = $("#video_#{@id}") - if @parseVideos(@el.data("streams")) is true + if @parseYoutubeId(@el.data("streams")) is true @videoType = "youtube" @fetchMetadata() @parseSpeed() else @videoType = "html5" - @parseVideoSources @el.data('mp4-source'), @el.data('webm-source'), @el.data('ogg-source') + @parseHtml5Sources @el.data('mp4-source'), @el.data('webm-source'), @el.data('ogg-source') @speeds = ['0.75', '1.0', '1.25', '1.50'] sub = @el.data('sub') if (typeof sub isnt "string") or (sub.length is 0) @@ -33,32 +33,30 @@ class @VideoAlpha @hide_captions = true $.cookie('hide_captions', @hide_captions, expires: 3650, path: '/') @el.addClass 'closed' - _this = this if ((@videoType is "youtube") and (YT.Player)) or ((@videoType is "html5") and (HTML5Video.Player)) @embed() else if @videoType is "youtube" - window.onYouTubePlayerAPIReady = -> - _this.embed() + window.onYouTubePlayerAPIReady = => + @embed() else if @videoType is "html5" - window.onHTML5PlayerAPIReady = -> - _this.embed() + window.onHTML5PlayerAPIReady = => + @embed() youtubeId: (speed)-> @videos[speed || @speed] - parseVideos: (videos)-> + parseYoutubeId: (videos)-> return false if (typeof videos isnt "string") or (videos.length is 0) @videos = {} - _this = this - $.each videos.split(/,/), (index, video) -> + $.each videos.split(/,/), (index, video) => speed = undefined video = video.split(/:/) speed = parseFloat(video[0]).toFixed(2).replace(/\.00$/, ".0") - _this.videos[speed] = video[1] + @videos[speed] = video[1] true - parseVideoSources: (mp4Source, webmSource, oggSource)-> + parseHtml5Sources: (mp4Source, webmSource, oggSource)-> @html5Sources = mp4: null webm: null @@ -71,12 +69,14 @@ class @VideoAlpha @speeds = ($.map @videos, (url, speed) -> speed).sort() @setSpeed $.cookie('video_speed') - setSpeed: (newSpeed)-> + setSpeed: (newSpeed, updateCookie)-> if @speeds.indexOf(newSpeed) isnt -1 @speed = newSpeed - $.cookie "video_speed", "" + newSpeed, - expires: 3650 - path: "/" + + if updateCookie isnt false + $.cookie "video_speed", "" + newSpeed, + expires: 3650 + path: "/" else @speed = "1.0" diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js b/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js index fb34733323..acdc03932c 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js @@ -48,6 +48,10 @@ this.HTML5Video = (function () { }; Player.prototype.getDuration = function () { + if (isFinite(this.video.duration) === false) { + return 0; + } + return this.video.duration; }; diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee index 566e4d785a..2def749d23 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee @@ -1,8 +1,13 @@ class @VideoPlayerAlpha extends SubviewAlpha initialize: -> + # If we switch verticals while the video is playing, then HTML content is + # removed, but JS code is still executing (setInterval() method), and there will + # arise conflicts (no HTML content, but code tries to access it). Therefore + # we must pause the player (stop setInterval() method). if (window.OldVideoPlayerAlpha) and (window.OldVideoPlayerAlpha.onPause) window.OldVideoPlayerAlpha.onPause() window.OldVideoPlayerAlpha = this + if @video.videoType is 'youtube' @PlayerState = YT.PlayerState # Define a missing constant of Youtube API @@ -99,16 +104,37 @@ class @VideoPlayerAlpha extends SubviewAlpha _this = this switch event.data when @PlayerState.UNSTARTED + # Before the video starts playing, let us see if we are in YouTube player, + # and if YouTube is in HTML5 mode. If both cases are true, then we can make + # it so that speed switching happens natively. + if @video.videoType is "youtube" + # Because YouTube API does not have a direct method to determine the mode we + # are in (Flash or HTML5), we rely on an indirect method. Currently, when in + # Flash mode, YouTube player reports that there is only one (1.0) speed + # available. When in HTML5 mode, it reports multiple speeds available. We + # will use this fact. + # + # NOTE: It is my strong belief that in the future YouTube Flash player will + # not get speed changes. This is a dying technology. So we can safely use + # this indirect method to determine player mode. availableSpeeds = @player.getAvailablePlaybackRates() prev_player_type = $.cookie('prev_player_type') if availableSpeeds.length > 1 + # If the user last accessed the page and watched a movie via YouTube + # player, and it was using Flash mode, then we must reset the current + # YouTube speed to 1.0 (by loading appropriate video that is encoded at + # 1.0 speed). if prev_player_type == 'youtube' $.cookie('prev_player_type', 'html5', expires: 3650, path: '/') - @onSpeedChange null, '1.0' + @onSpeedChange null, '1.0', false else if prev_player_type != 'html5' $.cookie('prev_player_type', 'html5', expires: 3650, path: '/') + # Now we must update all the speeds to the ones available via the YouTube + # HTML5 API. The default speeds are not exactly the same as reported by + # YouTube, so we will remove the default speeds, and populate all the + # necessary data with correct available speeds. baseSpeedSubs = @video.videos["1.0"] $.each @video.videos, (index, value) -> delete _this.video.videos[index] @@ -116,15 +142,26 @@ class @VideoPlayerAlpha extends SubviewAlpha $.each availableSpeeds, (index, value) -> _this.video.videos[value.toFixed(2).replace(/\.00$/, ".0")] = baseSpeedSubs _this.video.speeds.push value.toFixed(2).replace(/\.00$/, ".0") + + # We must update the Speed Control to reflect the new avialble speeds. @speedControl.reRender @video.speeds, @video.speed + + # Now we set the videoType to 'HTML5'. This works because my HTML5Video + # class is fully compatible with YouTube HTML5 API. @video.videoType = 'html5' @video.setSpeed $.cookie('video_speed') + + # Change the speed to the required one. @player.setPlaybackRate @video.speed else + # We are in YouTube player, and in Flash mode. Check previos mode. if prev_player_type != 'youtube' $.cookie('prev_player_type', 'youtube', expires: 3650, path: '/') + # We need to set the proper speed when previous mode was not 'youtube'. + @onSpeedChange null, $.cookie('video_speed') + @onUnstarted() when @PlayerState.PLAYING @onPlay() @@ -176,11 +213,11 @@ class @VideoPlayerAlpha extends SubviewAlpha @currentTime = time @updatePlayTime time - onSpeedChange: (event, newSpeed) => + onSpeedChange: (event, newSpeed, updateCookie) => if @video.videoType is 'youtube' @currentTime = Time.convert(@currentTime, parseFloat(@currentSpeed()), newSpeed) newSpeed = parseFloat(newSpeed).toFixed(2).replace /\.00$/, '.0' - @video.setSpeed newSpeed + @video.setSpeed newSpeed, updateCookie if @video.videoType is 'youtube' if @video.show_captions is true @caption.currentSpeed = newSpeed @@ -230,11 +267,10 @@ class @VideoPlayerAlpha extends SubviewAlpha @player.pauseVideo() if @player.pauseVideo duration: -> - if @video.videoType is "youtube" - return @video.getDuration() - else if @video.videoType is "html5" - return @player.getDuration() - 0 + duration = @player.getDuration() + if isFinite(duration) is false + duration = @video.getDuration() + duration currentSpeed: -> @video.speed From 92dc8859a3fd619396ce4cc520e0e2ebefe58d95 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Wed, 13 Feb 2013 16:47:06 +0200 Subject: [PATCH 65/75] YouTube HTML5 mode is used by default. Fix typo in video alpha template. --- .../xmodule/js/src/videoalpha/display/video_player.coffee | 1 + lms/templates/videoalpha.html | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee index 2def749d23..1b761594de 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee @@ -59,6 +59,7 @@ class @VideoPlayerAlpha extends SubviewAlpha showinfo: 0 enablejsapi: 1 modestbranding: 1 + html5: 1 if @video.start @playerVars.start = @video.start @playerVars.wmode = 'window' diff --git a/lms/templates/videoalpha.html b/lms/templates/videoalpha.html index 2ddcdd57e1..2028d3c320 100644 --- a/lms/templates/videoalpha.html +++ b/lms/templates/videoalpha.html @@ -32,7 +32,7 @@ % if sources.get('main'):
      -

      Download video here.

      +

      Download video here.

      % endif From d55b818bb2bb518402250b252cab06c0726824b3 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Thu, 14 Feb 2013 11:25:36 +0200 Subject: [PATCH 66/75] Updated YouTube JS API file to the new one. --- lms/templates/courseware/courseware.html | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lms/templates/courseware/courseware.html b/lms/templates/courseware/courseware.html index fcbc83d815..33dc9562a7 100644 --- a/lms/templates/courseware/courseware.html +++ b/lms/templates/courseware/courseware.html @@ -32,7 +32,7 @@ % if timer_expiration_duration: - % endif From 93000af79d09345bfec4ae2c5dae249f0dd27a76 Mon Sep 17 00:00:00 2001 From: Vasyl Nakvasiuk Date: Thu, 14 Feb 2013 11:30:21 +0200 Subject: [PATCH 67/75] fix problem with mutable default value in `VideoAlphaModule._get_source` --- .../lib/xmodule/xmodule/videoalpha_module.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/common/lib/xmodule/xmodule/videoalpha_module.py b/common/lib/xmodule/xmodule/videoalpha_module.py index 6910728d8c..c892e7b1b8 100644 --- a/common/lib/xmodule/xmodule/videoalpha_module.py +++ b/common/lib/xmodule/xmodule/videoalpha_module.py @@ -69,10 +69,10 @@ class VideoAlphaModule(XModule): if 'position' in state: self.position = int(float(state['position'])) - def _get_source(self, xmltree, extensions=['mp4', 'ogv', 'avi', 'webm']): - """Find the first valid source, which ends with one of - `extensions`.""" - condition = lambda src: any([src.endswith(ext) for ext in extensions]) + def _get_source(self, xmltree, ext=None): + """Find the first valid source, which ends with one of `ext`.""" + ext = ['mp4', 'ogv', 'avi', 'webm'] if ext is None else ext + condition = lambda src: any([src.endswith(ext) for ext in ext]) return self._get_first_external(xmltree, 'source', condition) def _get_track(self, xmltree): @@ -110,11 +110,10 @@ class VideoAlphaModule(XModule): return parse_time(xmltree.get('from')), parse_time(xmltree.get('to')) def handle_ajax(self, dispatch, get): - ''' - Handle ajax calls to this video. - TODO (vshnayder): This is not being called right now, so the position - is not being saved. - ''' + """Handle ajax calls to this video. + TODO (vshnayder): This is not being called right now, so the + position is not being saved. + """ log.debug(u"GET {0}".format(get)) log.debug(u"DISPATCH {0}".format(dispatch)) if dispatch == 'goto_position': @@ -124,7 +123,6 @@ class VideoAlphaModule(XModule): raise Http404() def get_instance_state(self): - #log.debug(u"STATE POSITION {0}".format(self.position)) return json.dumps({'position': self.position}) def get_html(self): From d614c80cc298aada2c956f654a17e89942f6f7da Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 14 Feb 2013 10:39:16 -0500 Subject: [PATCH 68/75] Firefox doesn't draw the buttons right with -90deg, but will draw it right if no direction is specified. --- lms/static/sass/shared/_header.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/static/sass/shared/_header.scss b/lms/static/sass/shared/_header.scss index 49c9ac250b..688ffbf57e 100644 --- a/lms/static/sass/shared/_header.scss +++ b/lms/static/sass/shared/_header.scss @@ -101,7 +101,7 @@ header.global { margin-right: 5px; > a { - @include background-image(linear-gradient(-90deg, #fff 0%, rgb(250,250,250) 50%, rgb(237,237,237) 50%, rgb(220,220,220) 100%)); + @include background-image(linear-gradient(#fff 0%, rgb(250,250,250) 50%, rgb(237,237,237) 50%, rgb(220,220,220) 100%)); border: 1px solid transparent; border-color: rgb(200,200,200); @include border-radius(3px); From 21dab27ed6b09bcdf3a6d00950c1a14c14771413 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Tue, 5 Feb 2013 11:09:30 -0500 Subject: [PATCH 69/75] fix logger name in module_render --- lms/djangoapps/courseware/module_render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 7ed32c8597..208ce26082 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -31,7 +31,7 @@ from xmodule_modifiers import replace_course_urls, replace_static_urls, add_hist from xmodule.modulestore.exceptions import ItemNotFoundError from statsd import statsd -log = logging.getLogger("mitx.courseware") +log = logging.getLogger(__name__) if settings.XQUEUE_INTERFACE.get('basic_auth') is not None: From fffbb5594405c0bbf8733a0c4395ac5d88c057e2 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Thu, 14 Feb 2013 09:09:11 -0500 Subject: [PATCH 70/75] Make hidden_module not cause 500 errors by adding a get_html() --- common/lib/xmodule/xmodule/hidden_module.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/hidden_module.py b/common/lib/xmodule/xmodule/hidden_module.py index d4f2a0fa33..e7639e63c8 100644 --- a/common/lib/xmodule/xmodule/hidden_module.py +++ b/common/lib/xmodule/xmodule/hidden_module.py @@ -3,7 +3,11 @@ from xmodule.raw_module import RawDescriptor class HiddenModule(XModule): - pass + def get_html(self): + if self.system.user_is_staff: + return "ERROR: This module is unknown--students will not see it at all" + else: + return "" class HiddenDescriptor(RawDescriptor): From 5e44846596e732c1091a3f2f4e4971ac47d9a851 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Tue, 5 Feb 2013 11:09:21 -0500 Subject: [PATCH 71/75] Foldit integration. - ops view for the desktop app to talk to - xmodule that talks to the foldit model and displays the student's state - grading tweak to make grade updates from an external service work: - Add an always_recalculate_grades property to XModuleDescriptor. --- common/djangoapps/foldit/__init__.py | 0 common/djangoapps/foldit/models.py | 95 +++++++ common/djangoapps/foldit/tests.py | 263 ++++++++++++++++++ common/djangoapps/foldit/views.py | 129 +++++++++ common/lib/xmodule/setup.py | 5 +- common/lib/xmodule/xmodule/foldit_module.py | 129 +++++++++ common/lib/xmodule/xmodule/x_module.py | 11 + lms/djangoapps/courseware/grades.py | 8 + lms/envs/common.py | 3 + .../sass/course/courseware/_courseware.scss | 14 + lms/templates/foldit.html | 28 ++ lms/urls.py | 8 +- 12 files changed, 690 insertions(+), 3 deletions(-) create mode 100644 common/djangoapps/foldit/__init__.py create mode 100644 common/djangoapps/foldit/models.py create mode 100644 common/djangoapps/foldit/tests.py create mode 100644 common/djangoapps/foldit/views.py create mode 100644 common/lib/xmodule/xmodule/foldit_module.py create mode 100644 lms/templates/foldit.html diff --git a/common/djangoapps/foldit/__init__.py b/common/djangoapps/foldit/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/djangoapps/foldit/models.py b/common/djangoapps/foldit/models.py new file mode 100644 index 0000000000..ea4f099216 --- /dev/null +++ b/common/djangoapps/foldit/models.py @@ -0,0 +1,95 @@ +import logging + +from django.conf import settings +from django.contrib.auth.models import User +from django.db import models + +from student.models import unique_id_for_user + + +log = logging.getLogger(__name__) + +class Score(models.Model): + """ + This model stores the scores of different users on FoldIt problems. + """ + user = models.ForeignKey(User, db_index=True, + related_name='foldit_scores') + + # The XModule that wants to access this doesn't have access to the real + # userid. Save the anonymized version so we can look up by that. + unique_user_id = models.CharField(max_length=50, db_index=True) + puzzle_id = models.IntegerField() + best_score = models.FloatField(db_index=True) + current_score = models.FloatField(db_index=True) + score_version = models.IntegerField() + created = models.DateTimeField(auto_now_add=True) + + +class PuzzleComplete(models.Model): + """ + This keeps track of the sets of puzzles completed by each user. + + e.g. PuzzleID 1234, set 1, subset 3. (Sets and subsets correspond to levels + in the intro puzzles) + """ + class Meta: + # there should only be one puzzle complete entry for any particular + # puzzle for any user + unique_together = ('user', 'puzzle_id', 'puzzle_set', 'puzzle_subset') + ordering = ['puzzle_id'] + + user = models.ForeignKey(User, db_index=True, + related_name='foldit_puzzles_complete') + + # The XModule that wants to access this doesn't have access to the real + # userid. Save the anonymized version so we can look up by that. + unique_user_id = models.CharField(max_length=50, db_index=True) + puzzle_id = models.IntegerField() + puzzle_set = models.IntegerField(db_index=True) + puzzle_subset = models.IntegerField(db_index=True) + created = models.DateTimeField(auto_now_add=True) + + def __unicode__(self): + return "PuzzleComplete({0}, id={1}, set={2}, subset={3}, created={4})".format( + self.user.username, self.puzzle_id, + self.puzzle_set, self.puzzle_subset, + self.created) + + + @staticmethod + def completed_puzzles(anonymous_user_id): + """ + Return a list of puzzles that this user has completed, as an array of + dicts: + + [ {'set': int, + 'subset': int, + 'created': datetime} ] + """ + complete = PuzzleComplete.objects.filter(unique_user_id=anonymous_user_id) + return [{'set': c.puzzle_set, + 'subset': c.puzzle_subset, + 'created': c.created} for c in complete] + + + @staticmethod + def is_level_complete(anonymous_user_id, level, sub_level, due=None): + """ + Return True if this user completed level--sub_level by due. + + Users see levels as e.g. 4-5. + + Args: + level: int + sub_level: int + due (optional): If specified, a datetime. Ignored if None. + """ + complete = PuzzleComplete.objects.filter(unique_user_id=anonymous_user_id, + puzzle_set=level, + puzzle_subset=sub_level) + if due is not None: + complete = complete.filter(created__lte=due) + + return complete.exists() + diff --git a/common/djangoapps/foldit/tests.py b/common/djangoapps/foldit/tests.py new file mode 100644 index 0000000000..4d387b44e8 --- /dev/null +++ b/common/djangoapps/foldit/tests.py @@ -0,0 +1,263 @@ +import json +import logging +from functools import partial + +from django.contrib.auth.models import User +from django.test import TestCase +from django.test.client import RequestFactory +from django.conf import settings +from django.core.urlresolvers import reverse + +from foldit.views import foldit_ops, verify_code +from foldit.models import PuzzleComplete +from student.models import UserProfile, unique_id_for_user + +from datetime import datetime, timedelta + +log = logging.getLogger(__name__) + + +class FolditTestCase(TestCase): + + def setUp(self): + self.factory = RequestFactory() + self.url = reverse('foldit_ops') + + pwd = 'abc' + self.user = User.objects.create_user('testuser', 'test@test.com', pwd) + self.unique_user_id = unique_id_for_user(self.user) + now = datetime.now() + self.tomorrow = now + timedelta(days=1) + self.yesterday = now - timedelta(days=1) + + UserProfile.objects.create(user=self.user) + + def make_request(self, post_data): + request = self.factory.post(self.url, post_data) + request.user = self.user + return request + + def test_SetPlayerPuzzleScores(self): + + scores = [ {"PuzzleID": 994391, + "ScoreType": "score", + "BestScore": 0.078034, + "CurrentScore":0.080035, + "ScoreVersion":23}] + scores_str = json.dumps(scores) + + verify = {"Verify": verify_code(self.user.email, scores_str), + "VerifyMethod":"FoldItVerify"} + data = {'SetPlayerPuzzleScoresVerify': json.dumps(verify), + 'SetPlayerPuzzleScores': scores_str} + + request = self.make_request(data) + + response = foldit_ops(request) + self.assertEqual(response.status_code, 200) + + self.assertEqual(response.content, json.dumps( + [{"OperationID": "SetPlayerPuzzleScores", + "Value": [{ + "PuzzleID": 994391, + "Status": "Success"}]}])) + + + def test_SetPlayerPuzzleScores_many(self): + + scores = [ {"PuzzleID": 994391, + "ScoreType": "score", + "BestScore": 0.078034, + "CurrentScore":0.080035, + "ScoreVersion":23}, + + {"PuzzleID": 994392, + "ScoreType": "score", + "BestScore": 0.078000, + "CurrentScore":0.080011, + "ScoreVersion":23}] + + scores_str = json.dumps(scores) + + verify = {"Verify": verify_code(self.user.email, scores_str), + "VerifyMethod":"FoldItVerify"} + data = {'SetPlayerPuzzleScoresVerify': json.dumps(verify), + 'SetPlayerPuzzleScores': scores_str} + + request = self.make_request(data) + + response = foldit_ops(request) + self.assertEqual(response.status_code, 200) + + self.assertEqual(response.content, json.dumps( + [{"OperationID": "SetPlayerPuzzleScores", + "Value": [{ + "PuzzleID": 994391, + "Status": "Success"}, + + {"PuzzleID": 994392, + "Status": "Success"}]}])) + + + + def test_SetPlayerPuzzleScores_error(self): + + scores = [ {"PuzzleID": 994391, + "ScoreType": "score", + "BestScore": 0.078034, + "CurrentScore":0.080035, + "ScoreVersion":23}] + validation_str = json.dumps(scores) + + verify = {"Verify": verify_code(self.user.email, validation_str), + "VerifyMethod":"FoldItVerify"} + + # change the real string -- should get an error + scores[0]['ScoreVersion'] = 22 + scores_str = json.dumps(scores) + + data = {'SetPlayerPuzzleScoresVerify': json.dumps(verify), + 'SetPlayerPuzzleScores': scores_str} + + request = self.make_request(data) + + response = foldit_ops(request) + self.assertEqual(response.status_code, 200) + + response_data = json.loads(response.content) + + self.assertEqual(response.content, + json.dumps([{ + "OperationID": "SetPlayerPuzzleScores", + "Success": "false", + "ErrorString": "Verification failed", + "ErrorCode": "VerifyFailed"}])) + + + def make_puzzles_complete_request(self, puzzles): + """ + Make a puzzles complete request, given an array of + puzzles. E.g. + + [ {"PuzzleID": 13, "Set": 1, "SubSet": 2}, + {"PuzzleID": 53524, "Set": 1, "SubSet": 1} ] + """ + puzzles_str = json.dumps(puzzles) + + verify = {"Verify": verify_code(self.user.email, puzzles_str), + "VerifyMethod":"FoldItVerify"} + + data = {'SetPuzzlesCompleteVerify': json.dumps(verify), + 'SetPuzzlesComplete': puzzles_str} + + request = self.make_request(data) + + response = foldit_ops(request) + self.assertEqual(response.status_code, 200) + return response + + @staticmethod + def set_puzzle_complete_response(values): + return json.dumps([{"OperationID":"SetPuzzlesComplete", + "Value": values}]) + + + def test_SetPlayerPuzzlesComplete(self): + + puzzles = [ {"PuzzleID": 13, "Set": 1, "SubSet": 2}, + {"PuzzleID": 53524, "Set": 1, "SubSet": 1} ] + + response = self.make_puzzles_complete_request(puzzles) + + self.assertEqual(response.content, + self.set_puzzle_complete_response([13, 53524])) + + + + def test_SetPlayerPuzzlesComplete_multiple(self): + """Check that state is stored properly""" + + puzzles = [ {"PuzzleID": 13, "Set": 1, "SubSet": 2}, + {"PuzzleID": 53524, "Set": 1, "SubSet": 1} ] + + response = self.make_puzzles_complete_request(puzzles) + + self.assertEqual(response.content, + self.set_puzzle_complete_response([13, 53524])) + + puzzles = [ {"PuzzleID": 14, "Set": 1, "SubSet": 3}, + {"PuzzleID": 15, "Set": 1, "SubSet": 1} ] + + response = self.make_puzzles_complete_request(puzzles) + + self.assertEqual(response.content, + self.set_puzzle_complete_response([13, 14, 15, 53524])) + + + + def test_SetPlayerPuzzlesComplete_level_complete(self): + """Check that the level complete function works""" + + puzzles = [ {"PuzzleID": 13, "Set": 1, "SubSet": 2}, + {"PuzzleID": 53524, "Set": 1, "SubSet": 1} ] + + response = self.make_puzzles_complete_request(puzzles) + + self.assertEqual(response.content, + self.set_puzzle_complete_response([13, 53524])) + + puzzles = [ {"PuzzleID": 14, "Set": 1, "SubSet": 3}, + {"PuzzleID": 15, "Set": 1, "SubSet": 1} ] + + response = self.make_puzzles_complete_request(puzzles) + + self.assertEqual(response.content, + self.set_puzzle_complete_response([13, 14, 15, 53524])) + + is_complete = partial( + PuzzleComplete.is_level_complete, self.unique_user_id) + + self.assertTrue(is_complete(1, 1)) + self.assertTrue(is_complete(1, 3)) + self.assertTrue(is_complete(1, 2)) + self.assertFalse(is_complete(4, 5)) + + puzzles = [ {"PuzzleID": 74, "Set": 4, "SubSet": 5} ] + + response = self.make_puzzles_complete_request(puzzles) + + self.assertTrue(is_complete(4, 5)) + + # Now check due dates + + self.assertTrue(is_complete(1, 1, due=self.tomorrow)) + self.assertFalse(is_complete(1, 1, due=self.yesterday)) + + + + def test_SetPlayerPuzzlesComplete_error(self): + + puzzles = [ {"PuzzleID": 13, "Set": 1, "SubSet": 2}, + {"PuzzleID": 53524, "Set": 1, "SubSet": 1} ] + + puzzles_str = json.dumps(puzzles) + + verify = {"Verify": verify_code(self.user.email, puzzles_str + "x"), + "VerifyMethod":"FoldItVerify"} + + data = {'SetPuzzlesCompleteVerify': json.dumps(verify), + 'SetPuzzlesComplete': puzzles_str} + + request = self.make_request(data) + + response = foldit_ops(request) + self.assertEqual(response.status_code, 200) + + response_data = json.loads(response.content) + + self.assertEqual(response.content, + json.dumps([{ + "OperationID": "SetPuzzlesComplete", + "Success": "false", + "ErrorString": "Verification failed", + "ErrorCode": "VerifyFailed"}])) diff --git a/common/djangoapps/foldit/views.py b/common/djangoapps/foldit/views.py new file mode 100644 index 0000000000..62e60ee0de --- /dev/null +++ b/common/djangoapps/foldit/views.py @@ -0,0 +1,129 @@ +import hashlib +import json +import logging + +from django.contrib.auth.decorators import login_required +from django.http import HttpResponse +from django.views.decorators.http import require_POST + +from foldit.models import Score, PuzzleComplete +from student.models import unique_id_for_user + +log = logging.getLogger(__name__) + + +@login_required +@require_POST +def foldit_ops(request): + log.debug(request.POST) + + responses = [] + if "SetPlayerPuzzleScores" in request.POST: + puzzle_scores_json = request.POST.get("SetPlayerPuzzleScores") + pz_verify_json = request.POST.get("SetPlayerPuzzleScoresVerify") + + puzzle_score_verify = json.loads(pz_verify_json) + if not verifies_ok(request.user.email, + puzzle_scores_json, puzzle_score_verify): + responses.append({"OperationID": "SetPlayerPuzzleScores", + "Success": "false", + "ErrorString": "Verification failed", + "ErrorCode": "VerifyFailed"}) + log.info("Verification of SetPlayerPuzzleScores failed:" + + "user %s, scores json %r, verify %r", + request.user, puzzle_scores_json, pz_verify_json) + else: + puzzle_scores = json.loads(puzzle_scores_json) + responses.append(save_scores(request.user, puzzle_scores)) + + if "SetPuzzlesComplete" in request.POST: + puzzles_complete_json = request.POST.get("SetPuzzlesComplete") + pc_verify_json = request.POST.get("SetPuzzlesCompleteVerify") + + puzzles_complete_verify = json.loads(pc_verify_json) + + if not verifies_ok(request.user.email, + puzzles_complete_json, puzzles_complete_verify): + responses.append({"OperationID": "SetPuzzlesComplete", + "Success": "false", + "ErrorString": "Verification failed", + "ErrorCode": "VerifyFailed"}) + log.info("Verification of SetPuzzlesComplete failed:" + + " user %s, puzzles json %r, verify %r", + request.user, puzzles_complete_json, pc_verify_json) + else: + puzzles_complete = json.loads(puzzles_complete_json) + responses.append(save_complete(request.user, puzzles_complete)) + + return HttpResponse(json.dumps(responses)) + + +def verify_code(email, val): + """ + Given the email and passed in value (str), return the expected + verification code. + """ + # TODO: is this the right string? + verification_string = email.lower() + '|' + val + return hashlib.md5(verification_string).hexdigest() + + +def verifies_ok(email, val, verification): + """ + Check that the hash_str matches the expected hash of val. + + Returns True if verification ok, False otherwise + """ + if verification.get("VerifyMethod") != "FoldItVerify": + log.debug("VerificationMethod in %r isn't FoldItVerify", verification) + return False + hash_str = verification.get("Verify") + + return verify_code(email, val) == hash_str + + +def save_scores(user, puzzle_scores): + score_responses = [] + for score in puzzle_scores: + log.debug("score: %s", score) + # expected keys ScoreType, PuzzleID (int), + # BestScore (energy), CurrentScore (Energy), ScoreVersion (int) + + puzzle_id = score['PuzzleID'] + + # TODO: save the score + + # SetPlayerPuzzleScoreResponse object + score_responses.append({'PuzzleID': puzzle_id, + 'Status': 'Success'}) + + return {"OperationID": "SetPlayerPuzzleScores", "Value": score_responses} + + +def save_complete(user, puzzles_complete): + """ + Returned list of PuzzleIDs should be in sorted order (I don't think client + cares, but tests do) + """ + for complete in puzzles_complete: + log.debug("Puzzle complete: %s", complete) + puzzle_id = complete['PuzzleID'] + puzzle_set = complete['Set'] + puzzle_subset = complete['SubSet'] + + # create if not there + PuzzleComplete.objects.get_or_create( + user=user, + unique_user_id=unique_id_for_user(user), + puzzle_id=puzzle_id, + puzzle_set=puzzle_set, + puzzle_subset=puzzle_subset) + + # List of all puzzle ids of intro-level puzzles completed ever, including on this + # request + # TODO: this is just in this request... + + complete_responses = list(pc.puzzle_id + for pc in PuzzleComplete.objects.filter(user=user)) + + return {"OperationID": "SetPuzzlesComplete", "Value": complete_responses} diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py index a1b059b889..061e93fd21 100644 --- a/common/lib/xmodule/setup.py +++ b/common/lib/xmodule/setup.py @@ -41,7 +41,8 @@ setup( "static_tab = xmodule.html_module:StaticTabDescriptor", "custom_tag_template = xmodule.raw_module:RawDescriptor", "about = xmodule.html_module:AboutDescriptor", - "graphical_slider_tool = xmodule.gst_module:GraphicalSliderToolDescriptor" - ] + "graphical_slider_tool = xmodule.gst_module:GraphicalSliderToolDescriptor", + "foldit = xmodule.foldit_module:FolditDescriptor", + ] } ) diff --git a/common/lib/xmodule/xmodule/foldit_module.py b/common/lib/xmodule/xmodule/foldit_module.py new file mode 100644 index 0000000000..2be8fd5990 --- /dev/null +++ b/common/lib/xmodule/xmodule/foldit_module.py @@ -0,0 +1,129 @@ +import logging +from lxml import etree +from dateutil import parser + +from pkg_resources import resource_string + +from xmodule.editing_module import EditingDescriptor +from xmodule.x_module import XModule +from xmodule.xml_module import XmlDescriptor + +log = logging.getLogger(__name__) + +class FolditModule(XModule): + def __init__(self, system, location, definition, descriptor, + instance_state=None, shared_state=None, **kwargs): + XModule.__init__(self, system, location, definition, descriptor, + instance_state, shared_state, **kwargs) + # ooh look--I'm lazy, so hardcoding the 7.00x required level. + # If we need it generalized, can pull from the xml later + self.required_level = 4 + self.required_sublevel = 5 + + def parse_due_date(): + """ + Pull out the date, or None + """ + s = self.metadata.get("due") + if s: + return parser.parse(s) + else: + return None + + self.due_str = self.metadata.get("due", "None") + self.due = parse_due_date() + + def is_complete(self): + """ + Did the user get to the required level before the due date? + """ + # We normally don't want django dependencies in xmodule. foldit is + # special. Import this late to avoid errors with things not yet being + # initialized. + from foldit.models import PuzzleComplete + + complete = PuzzleComplete.is_level_complete( + self.system.anonymous_student_id, + self.required_level, + self.required_sublevel, + self.due) + return complete + + def completed_puzzles(self): + """ + Return a list of puzzles that this user has completed, as an array of + dicts: + + [ {'set': int, + 'subset': int, + 'created': datetime} ] + + The list is sorted by set, then subset + """ + from foldit.models import PuzzleComplete + + return sorted( + PuzzleComplete.completed_puzzles(self.system.anonymous_student_id), + key=lambda d: (d['set'], d['subset'])) + + + def get_html(self): + """ + Render the html for the module. + """ + goal_level = '{0}-{1}'.format( + self.required_level, + self.required_sublevel) + + context = { + 'due': self.due_str, + 'success': self.is_complete(), + 'goal_level': goal_level, + 'completed': self.completed_puzzles(), + } + + return self.system.render_template('foldit.html', context) + + + def get_score(self): + """ + 0 / 1 based on whether student has gotten far enough. + """ + score = 1 if self.is_complete() else 0 + return {'score': score, + 'total': self.max_score()} + + def max_score(self): + return 1 + + +class FolditDescriptor(XmlDescriptor, EditingDescriptor): + """ + Module for adding open ended response questions to courses + """ + mako_template = "widgets/html-edit.html" + module_class = FolditModule + filename_extension = "xml" + + stores_state = True + has_score = True + template_dir_name = "foldit" + + js = {'coffee': [resource_string(__name__, 'js/src/html/edit.coffee')]} + js_module_name = "HTMLEditingDescriptor" + + @property + def always_recalculate_grades(self): + """ + The grade changes without any student interaction with the edx website, + so always need to actually check. + """ + return True + + + @classmethod + def definition_from_xml(cls, xml_object, system): + """ + For now, don't need anything from the xml + """ + return {} diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index 0e4e8e0f00..874a603c66 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -645,6 +645,17 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates): return False + @property + def always_recalculate_grades(self): + """ + Return whether this descriptor always requires recalculation of grades, + for example if the score can change via an extrnal service, not just + when the student interacts with the module on the page. A specific + example is FoldIt, which posts grade-changing updates through a separate + API. + """ + return False + # ================================= JSON PARSING =========================== @staticmethod def load_from_json(json_data, system, default_class=None): diff --git a/lms/djangoapps/courseware/grades.py b/lms/djangoapps/courseware/grades.py index f532e6c530..72876ff23d 100644 --- a/lms/djangoapps/courseware/grades.py +++ b/lms/djangoapps/courseware/grades.py @@ -339,6 +339,14 @@ def get_score(course_id, user, problem_descriptor, module_creator, student_modul Can return None if user doesn't have access, or if something else went wrong. cache: A StudentModuleCache """ + if problem_descriptor.always_recalculate_grades: + problem = module_creator(problem_descriptor) + d = problem.get_score() + if d is not None: + return (d['score'], d['total']) + else: + return (None, None) + if not (problem_descriptor.stores_state and problem_descriptor.has_score): # These are not problems, and do not have a score return (None, None) diff --git a/lms/envs/common.py b/lms/envs/common.py index 16472795e0..a22c426314 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -590,6 +590,9 @@ INSTALLED_APPS = ( 'wiki.plugins.notifications', 'course_wiki.plugins.markdownedx', + # foldit integration + 'foldit', + # For testing 'django.contrib.admin', # only used in DEBUG mode diff --git a/lms/static/sass/course/courseware/_courseware.scss b/lms/static/sass/course/courseware/_courseware.scss index 038903b756..ea987d8b2f 100644 --- a/lms/static/sass/course/courseware/_courseware.scss +++ b/lms/static/sass/course/courseware/_courseware.scss @@ -248,3 +248,17 @@ section.self-assessment { font-weight: bold; } } + +section.foldit { + table { + margin-top: 10px; + } + th { + text-align: center; + } + td { + padding-left: 5px; + padding-right: 5px; + + } +} \ No newline at end of file diff --git a/lms/templates/foldit.html b/lms/templates/foldit.html new file mode 100644 index 0000000000..2c16ebbfeb --- /dev/null +++ b/lms/templates/foldit.html @@ -0,0 +1,28 @@ +
      +

      Due: ${due} + +

      +Status: +% if success: +You have successfully gotten to level ${goal_level}. +% else: +You have not yet gotten to level ${goal_level}. +% endif +

      + +

      Completed puzzles

      + + + + + + + % for puzzle in completed: + + + + + % endfor +
      LevelSubmitted
      ${'{0}-{1}'.format(puzzle['set'], puzzle['subset'])}${puzzle['created'].strftime('%Y-%m-%d %H:%M')}
      + +
      \ No newline at end of file diff --git a/lms/urls.py b/lms/urls.py index 2d4267ec71..f4e098c9f6 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -288,7 +288,7 @@ if settings.COURSEWARE_ENABLED: # Open Ended problem list url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/open_ended_problems$', 'open_ended_grading.views.student_problem_list', name='open_ended_problems'), - + # Cohorts management url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/cohorts$', 'course_groups.views.list_cohorts', name="cohorts"), @@ -369,6 +369,12 @@ if settings.MITX_FEATURES.get('ENABLE_SQL_TRACKING_LOGS'): url(r'^event_logs/(?P.+)$', 'track.views.view_tracking_log'), ) +# FoldIt views +urlpatterns += ( + # The path is hardcoded into their app... + url(r'^comm/foldit_ops', 'foldit.views.foldit_ops', name="foldit_ops"), +) + urlpatterns = patterns(*urlpatterns) if settings.DEBUG: From d7f9bdda66b5a2c781514c0ae80216f26dc6439a Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Thu, 14 Feb 2013 13:23:58 -0500 Subject: [PATCH 72/75] Move foldit app into the lms. No need for it in the cms, and it was breaking tests there.. --- {common => lms}/djangoapps/foldit/__init__.py | 0 {common => lms}/djangoapps/foldit/models.py | 0 {common => lms}/djangoapps/foldit/tests.py | 0 {common => lms}/djangoapps/foldit/views.py | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename {common => lms}/djangoapps/foldit/__init__.py (100%) rename {common => lms}/djangoapps/foldit/models.py (100%) rename {common => lms}/djangoapps/foldit/tests.py (100%) rename {common => lms}/djangoapps/foldit/views.py (100%) diff --git a/common/djangoapps/foldit/__init__.py b/lms/djangoapps/foldit/__init__.py similarity index 100% rename from common/djangoapps/foldit/__init__.py rename to lms/djangoapps/foldit/__init__.py diff --git a/common/djangoapps/foldit/models.py b/lms/djangoapps/foldit/models.py similarity index 100% rename from common/djangoapps/foldit/models.py rename to lms/djangoapps/foldit/models.py diff --git a/common/djangoapps/foldit/tests.py b/lms/djangoapps/foldit/tests.py similarity index 100% rename from common/djangoapps/foldit/tests.py rename to lms/djangoapps/foldit/tests.py diff --git a/common/djangoapps/foldit/views.py b/lms/djangoapps/foldit/views.py similarity index 100% rename from common/djangoapps/foldit/views.py rename to lms/djangoapps/foldit/views.py From 1108137dc3dc003970083ab130a43d0a4bd61314 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Thu, 14 Feb 2013 13:24:21 -0500 Subject: [PATCH 73/75] Address Cale's comment--make property a simple class var --- common/lib/xmodule/xmodule/foldit_module.py | 11 +++-------- common/lib/xmodule/xmodule/x_module.py | 21 ++++++++++----------- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/common/lib/xmodule/xmodule/foldit_module.py b/common/lib/xmodule/xmodule/foldit_module.py index 2be8fd5990..ea16fee7f1 100644 --- a/common/lib/xmodule/xmodule/foldit_module.py +++ b/common/lib/xmodule/xmodule/foldit_module.py @@ -112,14 +112,9 @@ class FolditDescriptor(XmlDescriptor, EditingDescriptor): js = {'coffee': [resource_string(__name__, 'js/src/html/edit.coffee')]} js_module_name = "HTMLEditingDescriptor" - @property - def always_recalculate_grades(self): - """ - The grade changes without any student interaction with the edx website, - so always need to actually check. - """ - return True - + # The grade changes without any student interaction with the edx website, + # so always need to actually check. + always_recalculate_grades = True @classmethod def definition_from_xml(cls, xml_object, system): diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index 9e52eacaed..07b9d3653e 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -515,6 +515,16 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates): self._child_instances = None self._inherited_metadata = set() + + # Class level variable + always_recalculate_grades = False + """ + Return whether this descriptor always requires recalculation of grades, for + example if the score can change via an extrnal service, not just when the + student interacts with the module on the page. A specific example is + FoldIt, which posts grade-changing updates through a separate API. + """ + @property def display_name(self): ''' @@ -645,17 +655,6 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates): return False - @property - def always_recalculate_grades(self): - """ - Return whether this descriptor always requires recalculation of grades, - for example if the score can change via an extrnal service, not just - when the student interacts with the module on the page. A specific - example is FoldIt, which posts grade-changing updates through a separate - API. - """ - return False - # ================================= JSON PARSING =========================== @staticmethod def load_from_json(json_data, system, default_class=None): From ea068d4aa89849cc3d4db75369c25ba43188955c Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Thu, 14 Feb 2013 14:19:38 -0500 Subject: [PATCH 74/75] add exporting of policy.json --- .../contentstore/tests/test_contentstore.py | 13 +++++++++++-- .../lib/xmodule/xmodule/modulestore/xml_exporter.py | 11 +++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index dcd1f408cd..9c8c13c86a 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -212,12 +212,21 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): fs = OSFS(root_dir / 'test_export/policies/6.002_Spring_2012') self.assertTrue(fs.exists('grading_policy.json')) + course = ms.get_item(location) # compare what's on disk compared to what we have in our course with fs.open('grading_policy.json','r') as grading_policy: - on_disk = loads(grading_policy.read()) - course = ms.get_item(location) + on_disk = loads(grading_policy.read()) self.assertEqual(on_disk, course.definition['data']['grading_policy']) + #check for policy.json + self.assertTrue(fs.exists('policy.json')) + + # compare what's on disk to what we have in the course module + with fs.open('policy.json','r') as course_policy: + on_disk = loads(course_policy.read()) + self.assertIn('course/6.002_Spring_2012', on_disk) + self.assertEqual(on_disk['course/6.002_Spring_2012'], course.metadata) + # remove old course delete_course(ms, cs, location) diff --git a/common/lib/xmodule/xmodule/modulestore/xml_exporter.py b/common/lib/xmodule/xmodule/modulestore/xml_exporter.py index 509a2c7db9..55844116c6 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_exporter.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_exporter.py @@ -31,8 +31,15 @@ def export_to_xml(modulestore, contentstore, course_location, root_dir, course_d # export the grading policy policies_dir = export_fs.makeopendir('policies') course_run_policy_dir = policies_dir.makeopendir(course.location.name) - with course_run_policy_dir.open('grading_policy.json', 'w') as grading_policy: - grading_policy.write(dumps(course.definition['data']['grading_policy'])) + if 'grading_policy' in course.definition['data']: + with course_run_policy_dir.open('grading_policy.json', 'w') as grading_policy: + grading_policy.write(dumps(course.definition['data']['grading_policy'])) + + # export all of the course metadata in policy.json + with course_run_policy_dir.open('policy.json', 'w') as course_policy: + policy = {} + policy = {'course/' + course.location.name: course.metadata} + course_policy.write(dumps(policy)) def export_extra_content(export_fs, modulestore, course_location, category_type, dirname, file_suffix=''): From 723012b2bbdfb312c5061b6c361a5855bf183f14 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Thu, 14 Feb 2013 14:50:18 -0500 Subject: [PATCH 75/75] Better logging, and make view csrf exempt... --- lms/djangoapps/foldit/views.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/lms/djangoapps/foldit/views.py b/lms/djangoapps/foldit/views.py index 62e60ee0de..8bd3684c04 100644 --- a/lms/djangoapps/foldit/views.py +++ b/lms/djangoapps/foldit/views.py @@ -5,6 +5,7 @@ import logging from django.contrib.auth.decorators import login_required from django.http import HttpResponse from django.views.decorators.http import require_POST +from django.views.decorators.csrf import csrf_exempt from foldit.models import Score, PuzzleComplete from student.models import unique_id_for_user @@ -13,14 +14,18 @@ log = logging.getLogger(__name__) @login_required +@csrf_exempt @require_POST def foldit_ops(request): - log.debug(request.POST) - + """ + Endpoint view for foldit operations. + """ responses = [] if "SetPlayerPuzzleScores" in request.POST: puzzle_scores_json = request.POST.get("SetPlayerPuzzleScores") pz_verify_json = request.POST.get("SetPlayerPuzzleScoresVerify") + log.debug("SetPlayerPuzzleScores message: puzzle scores: %r", + puzzle_scores_json) puzzle_score_verify = json.loads(pz_verify_json) if not verifies_ok(request.user.email, @@ -29,7 +34,7 @@ def foldit_ops(request): "Success": "false", "ErrorString": "Verification failed", "ErrorCode": "VerifyFailed"}) - log.info("Verification of SetPlayerPuzzleScores failed:" + + log.warning("Verification of SetPlayerPuzzleScores failed:" + "user %s, scores json %r, verify %r", request.user, puzzle_scores_json, pz_verify_json) else: @@ -40,6 +45,9 @@ def foldit_ops(request): puzzles_complete_json = request.POST.get("SetPuzzlesComplete") pc_verify_json = request.POST.get("SetPuzzlesCompleteVerify") + log.debug("SetPuzzlesComplete message: %r", + puzzles_complete_json) + puzzles_complete_verify = json.loads(pc_verify_json) if not verifies_ok(request.user.email, @@ -48,7 +56,7 @@ def foldit_ops(request): "Success": "false", "ErrorString": "Verification failed", "ErrorCode": "VerifyFailed"}) - log.info("Verification of SetPuzzlesComplete failed:" + + log.warning("Verification of SetPuzzlesComplete failed:" + " user %s, puzzles json %r, verify %r", request.user, puzzles_complete_json, pc_verify_json) else: