From 9906bd2b6bd93ac27993411812357c42375acc92 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Mon, 21 May 2012 14:54:03 -0400 Subject: [PATCH 01/57] Moved code examples into snippets --- djangoapps/courseware/capa/responsetypes.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/djangoapps/courseware/capa/responsetypes.py b/djangoapps/courseware/capa/responsetypes.py index 380ab85c56..3159b70d5c 100644 --- a/djangoapps/courseware/capa/responsetypes.py +++ b/djangoapps/courseware/capa/responsetypes.py @@ -66,10 +66,8 @@ class GenericResponse(object): #----------------------------------------------------------------------------- class MultipleChoiceResponse(GenericResponse): - ''' - Example: - - + # TODO: handle direction and randomize + snippets = [{'snippet': ''' `a+b`
a+b^2
@@ -77,10 +75,7 @@ class MultipleChoiceResponse(GenericResponse): a+b+d
- - TODO: handle direction and randomize - - ''' + '''}] def __init__(self, xml, context, system=None): self.xml = xml self.correct_choices = xml.xpath('//*[@id=$id]//choice[@correct="true"]', @@ -218,8 +213,9 @@ class NumericalResponse(GenericResponse): class CustomResponse(GenericResponse): ''' Custom response. The python code to be run should be in .... Example: + ''' - + snippets = [{'snippet': '''
Suppose that \(I(t)\) rises from \(0\) to \(I_S\) at a time \(t_0 \neq 0\) @@ -237,9 +233,10 @@ class CustomResponse(GenericResponse): if not(r=="IS*u(t-t0)"): correct[0] ='incorrect' -
+
'''}] - Alternatively, the check function can be defined in Example: + + '''Footnote: the check function can also be defined in Example: tags so they don't break mid-string''' - if 'init_js' not in m: m['init_js']="" - if 'type' not in m: m['init_js']="" - content=json.dumps(m['content']) - content=content.replace('', '<"+"/script>') + content=json.dumps(m['content']) + content=content.replace('', '<"+"/script>') - return {'content':content, - "destroy_js":m['destroy_js'], - 'init_js':m['init_js'], + return {'content':content, 'type': m['type']} ## Returns a set of all types of all sub-children child_classes = [set([i.tag for i in e.iter()]) for e in self.xmltree] - self.titles = json.dumps(["\n".join([i.get("name").strip() for i in e.iter() if i.get("name") != None]) \ - for e in self.xmltree]) + titles = ["\n".join([i.get("name").strip() for i in e.iter() if i.get("name") != None]) \ + for e in self.xmltree] self.contents = [j(self.render_function(e)) \ for e in self.xmltree] - print self.titles + for i in range(len(self.contents)): + self.contents[i]['title'] = titles[i] for (content, element_class) in zip(self.contents, child_classes): new_class = 'other' for c in class_priority: - if c in element_class: + if c in element_class: new_class = c content['type'] = new_class - - js="" params={'items':self.contents, 'id':self.item_id, 'position': self.position, - 'titles':self.titles, + 'titles':titles, 'tag':self.xmltree.tag} # TODO/BUG: Destroy JavaScript should only be called for the active view @@ -94,16 +81,11 @@ class Module(XModule): destroy_js="".join([e['destroy_js'] for e in self.contents if 'destroy_js' in e]) if self.xmltree.tag in ['sequential', 'videosequence']: - self.init_js=js+render_to_string('seq_module.js',params) - self.destroy_js=destroy_js self.content=render_to_string('seq_module.html',params) if self.xmltree.tag == 'tab': params['id'] = 'tab' - self.init_js=js+render_to_string('tab_module.js',params) - self.destroy_js=destroy_js self.content=render_to_string('tab_module.html',params) self.rendered = True - def __init__(self, system, xml, item_id, state=None): XModule.__init__(self, system, xml, item_id, state) diff --git a/templates/coffee/src/modules/sequence.coffee b/templates/coffee/src/modules/sequence.coffee new file mode 100644 index 0000000000..013206093c --- /dev/null +++ b/templates/coffee/src/modules/sequence.coffee @@ -0,0 +1,70 @@ +class window.Sequence + constructor: (@id, @elements, @tag, position) -> + @buildNavigation() + @bind() + @render position + + bind: -> + $('#sequence-list a').click @goto + + buildNavigation: -> + $.each @elements, (index, item) -> + link = $('').attr class: "seq_#{item.type}_inactive", 'data-element': index + 1 + title = $('

').html(item.title) + list_item = $('

  • ').append(link.append(title)) + $('#sequence-list').append list_item + + toggleArrows: -> + $('.sequence-nav-buttons a').unbind('click') + + if @position == 1 + $('.sequence-nav-buttons .prev a').addClass('disabled') + else + $('.sequence-nav-buttons .prev a').removeClass('disabled').click(@previous) + + if @position == @elements.length + $('.sequence-nav-buttons .next a').addClass('disabled') + else + $('.sequence-nav-buttons .next a').removeClass('disabled').click(@next) + + render: (new_position) -> + if @position != undefined + @mark_visited @position + $.post "/modx/#{@tag}/#{@id}/goto_position", position: new_position + + if @position != new_position + @mark_active new_position + $('#seq_content').html eval(@elements[new_position - 1].content) + + MathJax.Hub.Queue(["Typeset",MathJax.Hub]) + @position = new_position + @toggleArrows() + + goto: (e) => + e.preventDefault() + new_position = $(e.srcElement).data('element') + log_event("seq_goto", old: @position, new: new_position, id: @id) + @render new_position + + next: (e) => + e.preventDefault() + new_position = @position + 1 + log_event("seq_next", old: @position, new: new_position, id: @id) + @render new_position + + previous: (e) => + e.preventDefault() + new_position = @position - 1 + log_event("seq_prev", old: @position, new: new_position, id: @id) + @render new_position + + link_for: (position) -> + $("#sequence-list a[data-element=#{position}]") + + mark_visited: (position) -> + @link_for(position).attr class: "seq_#{@elements[position - 1].type}_visited" + + mark_active: (position) -> + @link_for(position).attr class: "seq_#{@elements[position - 1].type}_active" + + diff --git a/templates/seq_module.html b/templates/seq_module.html index 94aa1661bc..ab903457dc 100644 --- a/templates/seq_module.html +++ b/templates/seq_module.html @@ -1,22 +1,28 @@ - -
    +
    - + + +<%block name="js_extra"> + + diff --git a/templates/seq_module.js b/templates/seq_module.js deleted file mode 100644 index 691cbb5544..0000000000 --- a/templates/seq_module.js +++ /dev/null @@ -1,118 +0,0 @@ -// IMPORTANT TODO: Namespace - -var ${ id }contents=["", - %for t in items: - ${t['content']} , - %endfor - "" - ]; - -var ${ id }types=["", - %for t in items: - "${t['type']}" , - %endfor - "" - ]; - -var ${ id }init_functions=["", - %for t in items: - function(){ ${t['init_js']} }, - %endfor - ""]; - -var ${ id }titles=${titles}; - -var ${ id }destroy_functions=["", - %for t in items: - function(){ ${t['destroy_js']} }, - %endfor - ""]; - -var ${ id }loc = -1; -function disablePrev() { - var i=${ id }loc-1; - log_event("seq_prev", {'old':${id}loc, 'new':i,'id':'${id}'}); - if (i < 1 ) { - $('.${ id }prev a').addClass('disabled'); - } else { - $('.${ id }prev a').removeClass('disabled'); - }; - } - - function disableNext() { - var i=${ id }loc+1; - log_event("seq_next", {'old':${id}loc, 'new':i,'id':'${id}'}); - - if(i > ${ len(items) } ) { - $('.${ id }next a').addClass('disabled'); - } else { - $('.${ id }next a').removeClass('disabled'); - }; -} - -function ${ id }goto(i) { - log_event("seq_goto", {'old':${id}loc, 'new':i,'id':'${id}'}); - - postJSON('${ MITX_ROOT_URL }/modx/${tag}/${ id }/goto_position', - {'position' : i }); - - if (${ id }loc!=-1) - ${ id }destroy_functions[ ${ id }loc ](); - $('#seq_content').html(${ id }contents[i]); - ${ id }init_functions[i]() - //$('#tt_'+${ id }loc).attr("style", "background-color:gray"); - $('#tt_'+${ id }loc).removeClass(); - $('#tt_'+${ id }loc).addClass("seq_"+${ id }types[${ id }loc]+"_visited"); - ${ id }loc=i; - //$('#tt_'+i).attr("style", "background-color:red"); - $('#tt_'+i).removeClass(); - $('#tt_'+i).addClass("seq_"+${ id }types[${ id }loc]+"_active"); - - MathJax.Hub.Queue(["Typeset",MathJax.Hub]); - - disableNext(); - disablePrev(); -} - -function ${ id }setup_click(i) { - $('#tt_'+i).click(function(eo) { ${ id }goto(i);}); - $('#tt_'+i).addClass("seq_"+${ id }types[i]+"_inactive"); - $('#tt_'+i).append("

    " + ${ id }titles[i-1] + "

    "); - -} - -function ${ id }next() { - var i=${ id }loc+1; - log_event("seq_next", {'old':${id}loc, 'new':i,'id':'${id}'}); - if(i > ${ len(items) } ) { - i = ${ len(items) }; - } else { - ${ id }goto(i); - }; -} - - -function ${ id }prev() { - var i=${ id }loc-1; - log_event("seq_prev", {'old':${id}loc, 'new':i,'id':'${id}'}); - if (i < 1 ) { - i = 1; - } else { - ${ id }goto(i); - }; -} - - - -$(function() { - var i; - for(i=1; i<${ len(items)+1 }; i++) { - ${ id }setup_click(i); - } - - - $('.${ id }next a').click(function(eo) { ${ id }next(); return false;}); - $('.${ id }prev a').click(function(eo) { ${ id }prev(); return false;}); - ${ id }goto( ${ position } ); - -}); From 8624927fc4d2c3519d42f68f913456e25eca8747 Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Tue, 1 May 2012 17:54:11 -0400 Subject: [PATCH 16/57] Rewritten video module --- djangoapps/courseware/modules/video_module.py | 23 +-- static/coffee/src/courseware.coffee | 2 + static/coffee/src/main.coffee | 4 + static/js/jquery.scrollTo-1.4.2-min.js | 11 ++ static/sass/courseware/_video.scss | 114 +++++++------- templates/coffee/src/modules/video.coffee | 41 +++++ .../src/modules/video/video_caption.coffee | 116 ++++++++++++++ .../src/modules/video/video_player.coffee | 145 ++++++++++++++++++ .../video/video_progress_slider.coffee | 54 +++++++ .../modules/video/video_speed_control.coffee | 38 +++++ templates/coffee/src/time.coffee | 21 +++ templates/courseware.html | 2 +- templates/main.html | 5 + templates/video.html | 142 ++++------------- 14 files changed, 530 insertions(+), 188 deletions(-) create mode 100644 static/js/jquery.scrollTo-1.4.2-min.js create mode 100644 templates/coffee/src/modules/video.coffee create mode 100644 templates/coffee/src/modules/video/video_caption.coffee create mode 100644 templates/coffee/src/modules/video/video_player.coffee create mode 100644 templates/coffee/src/modules/video/video_progress_slider.coffee create mode 100644 templates/coffee/src/modules/video/video_speed_control.coffee create mode 100644 templates/coffee/src/time.coffee diff --git a/djangoapps/courseware/modules/video_module.py b/djangoapps/courseware/modules/video_module.py index c678838f2b..1cf2c71435 100644 --- a/djangoapps/courseware/modules/video_module.py +++ b/djangoapps/courseware/modules/video_module.py @@ -30,32 +30,19 @@ class Module(XModule): def get_xml_tags(c): '''Tags in the courseware file guaranteed to correspond to the module''' return ["video"] - + def video_list(self): l = self.youtube.split(',') l = [i.split(":") for i in l] return json.dumps(dict(l)) - + def get_html(self): return render_to_string('video.html',{'streams':self.video_list(), 'id':self.item_id, - 'position':self.position, - 'name':self.name, + 'position':self.position, + 'name':self.name, 'annotations':self.annotations}) - def get_init_js(self): - '''JavaScript code to be run when problem is shown. Be aware - that this may happen several times on the same page - (e.g. student switching tabs). Common functions should be put - in the main course .js files for now. ''' - log.debug(u"INIT POSITION {0}".format(self.position)) - return render_to_string('video_init.js',{'streams':self.video_list(), - 'id':self.item_id, - 'position':self.position})+self.annotations_init - - def get_destroy_js(self): - return "videoDestroy(\"{0}\");".format(self.item_id)+self.annotations_destroy - def __init__(self, system, xml, item_id, state=None): XModule.__init__(self, system, xml, item_id, state) xmltree=etree.fromstring(xml) @@ -69,5 +56,3 @@ class Module(XModule): self.annotations=[(e.get("name"),self.render_function(e)) \ for e in xmltree] - self.annotations_init="".join([e[1]['init_js'] for e in self.annotations if 'init_js' in e[1]]) - self.annotations_destroy="".join([e[1]['destroy_js'] for e in self.annotations if 'destroy_js' in e[1]]) diff --git a/static/coffee/src/courseware.coffee b/static/coffee/src/courseware.coffee index a4c93ec0b4..73b7938b46 100644 --- a/static/coffee/src/courseware.coffee +++ b/static/coffee/src/courseware.coffee @@ -13,6 +13,8 @@ class window.Courseware autoHeight: false $('#open_close_accordion a').click navigation.toggle + $('#accordion').show() + log: (event, ui) -> log_event 'accordion', newheader: ui.newHeader.text() diff --git a/static/coffee/src/main.coffee b/static/coffee/src/main.coffee index 8a9a892f94..39f84228a6 100644 --- a/static/coffee/src/main.coffee +++ b/static/coffee/src/main.coffee @@ -2,7 +2,11 @@ $ -> $.ajaxSetup headers : { 'X-CSRFToken': $.cookie 'csrftoken' } + window.onTouchBasedDevice = -> + navigator.userAgent.match /iPhone|iPod|iPad/i + Calculator.bind() Courseware.bind() FeedbackForm.bind() $("a[rel*=leanModal]").leanModal() + diff --git a/static/js/jquery.scrollTo-1.4.2-min.js b/static/js/jquery.scrollTo-1.4.2-min.js new file mode 100644 index 0000000000..73a334184e --- /dev/null +++ b/static/js/jquery.scrollTo-1.4.2-min.js @@ -0,0 +1,11 @@ +/** + * jQuery.ScrollTo - Easy element scrolling using jQuery. + * Copyright (c) 2007-2009 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com + * Dual licensed under MIT and GPL. + * Date: 5/25/2009 + * @author Ariel Flesler + * @version 1.4.2 + * + * http://flesler.blogspot.com/2007/10/jqueryscrollto.html + */ +;(function(d){var k=d.scrollTo=function(a,i,e){d(window).scrollTo(a,i,e)};k.defaults={axis:'xy',duration:parseFloat(d.fn.jquery)>=1.3?0:1};k.window=function(a){return d(window)._scrollable()};d.fn._scrollable=function(){return this.map(function(){var a=this,i=!a.nodeName||d.inArray(a.nodeName.toLowerCase(),['iframe','#document','html','body'])!=-1;if(!i)return a;var e=(a.contentWindow||a).document||a.ownerDocument||a;return d.browser.safari||e.compatMode=='BackCompat'?e.body:e.documentElement})};d.fn.scrollTo=function(n,j,b){if(typeof j=='object'){b=j;j=0}if(typeof b=='function')b={onAfter:b};if(n=='max')n=9e9;b=d.extend({},k.defaults,b);j=j||b.speed||b.duration;b.queue=b.queue&&b.axis.length>1;if(b.queue)j/=2;b.offset=p(b.offset);b.over=p(b.over);return this._scrollable().each(function(){var q=this,r=d(q),f=n,s,g={},u=r.is('html,body');switch(typeof f){case'number':case'string':if(/^([+-]=)?\d+(\.\d+)?(px|%)?$/.test(f)){f=p(f);break}f=d(f,this);case'object':if(f.is||f.style)s=(f=d(f)).offset()}d.each(b.axis.split(''),function(a,i){var e=i=='x'?'Left':'Top',h=e.toLowerCase(),c='scroll'+e,l=q[c],m=k.max(q,i);if(s){g[c]=s[h]+(u?0:l-r.offset()[h]);if(b.margin){g[c]-=parseInt(f.css('margin'+e))||0;g[c]-=parseInt(f.css('border'+e+'Width'))||0}g[c]+=b.offset[h]||0;if(b.over[h])g[c]+=f[i=='x'?'width':'height']()*b.over[h]}else{var o=f[h];g[c]=o.slice&&o.slice(-1)=='%'?parseFloat(o)/100*m:o}if(/^\d+$/.test(g[c]))g[c]=g[c]<=0?0:Math.min(g[c],m);if(!a&&b.queue){if(l!=g[c])t(b.onAfterFirst);delete g[c]}});t(b.onAfter);function t(a){r.animate(g,j,b.easing,a&&function(){a.call(this,n,b)})}}).end()};k.max=function(a,i){var e=i=='x'?'Width':'Height',h='scroll'+e;if(!d(a).is('html,body'))return a[h]-d(a)[e.toLowerCase()]();var c='client'+e,l=a.ownerDocument.documentElement,m=a.ownerDocument.body;return Math.max(l[h],m[h])-Math.min(l[c],m[c])};function p(a){return typeof a=='object'?a:{top:a,left:a}}})(jQuery); \ No newline at end of file diff --git a/static/sass/courseware/_video.scss b/static/sass/courseware/_video.scss index 54c1b9f600..76da33d652 100644 --- a/static/sass/courseware/_video.scss +++ b/static/sass/courseware/_video.scss @@ -14,38 +14,29 @@ section.course-content { } } - div.video-subtitles { + div.video { + @include clearfix(); background: #f3f3f3; border-bottom: 1px solid #e1e1e1; border-top: 1px solid #e1e1e1; - @include clearfix(); display: block; margin: 0 (-(lh())); padding: 6px lh(); - div.video-wrapper { + article.video-wrapper { float: left; margin-right: flex-gutter(9); width: flex-grid(6, 9); - div.video-player { + section.video-player { height: 0; overflow: hidden; padding-bottom: 56.25%; padding-top: 30px; position: relative; - object { - height: 100%; - left: 0; - position: absolute; - top: 0; - width: 100%; - } - - iframe#html5_player { + object, iframe { border: none; - display: none; height: 100%; left: 0; position: absolute; @@ -68,7 +59,7 @@ section.course-content { } } - div#slider { + div.slider { @extend .clearfix; background: #c2c2c2; border: none; @@ -175,7 +166,8 @@ section.course-content { } } - div#vidtime { + div.vidtime { + padding-left: lh(.75); font-weight: bold; line-height: 46px; //height of play pause buttons padding-left: lh(.75); @@ -190,6 +182,7 @@ section.course-content { div.speeds { float: left; + position: relative; a { background: url('../images/closed-arrow.png') 10px center no-repeat; @@ -211,7 +204,7 @@ section.course-content { &.open { background: url('../images/open-arrow.png') 10px center no-repeat; - ol#video_speeds { + ol.video_speeds { display: block; opacity: 1; } @@ -234,47 +227,52 @@ section.course-content { padding: 0 lh(.5) 0 0; } - // fix for now - ol#video_speeds { + &:hover, &:active, &:focus { + opacity: 1; background-color: #444; - border: 1px solid #000; - @include box-shadow(inset 1px 0 0 #555, 0 3px 0 #444); - display: none; - left: -1px; - opacity: 0; - position: absolute; - top:0; - @include transition(); - width: 100%; - z-index: 10; + } + } - li { - border-bottom: 1px solid #000; - @include box-shadow( 0 1px 0 #555); + // 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: 125px; + 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; - cursor: pointer; - padding: 0 lh(.5); - - &.active { - font-weight: bold; - } - - &:last-child { - border-bottom: 0; - @include box-shadow(none); - margin-top: 0; - } + display: block; + padding: lh(.5); &:hover { background-color: #666; color: #aaa; } } - } - &:hover { - background-color: #444; - opacity: 1; + &.active { + font-weight: bold; + } + + &:last-child { + @include box-shadow(none); + border-bottom: 0; + margin-top: 0; + } } } } @@ -334,7 +332,7 @@ section.course-content { opacity: 1; } - div#slider { + div.slider { height: 14px; margin-top: -7px; @@ -352,7 +350,7 @@ section.course-content { ol.subtitles { float: left; max-height: 460px; - overflow: hidden; + overflow: auto; padding-top: 10px; width: flex-grid(3, 9); @@ -360,7 +358,7 @@ section.course-content { border: 0; color: #666; cursor: pointer; - margin-bottom: 0px; + margin-bottom: 8px; padding: 0; @include transition(all, .5s, ease-in); @@ -373,11 +371,7 @@ section.course-content { color: $mit-red; } - div { - margin-bottom: 8px; - } - - div:empty { + &:empty { margin-bottom: 0px; } } @@ -386,7 +380,7 @@ section.course-content { &.closed { @extend .trans; - div.video-wrapper { + article.video-wrapper { width: flex-grid(9,9); } @@ -441,11 +435,11 @@ section.course-content { } div.tc-wrapper { - div.video-wrapper { + article.video-wrapper { width: 100%; } - object#myytplayer, iframe { + object, iframe { bottom: 0; height: 100%; left: 0; @@ -487,7 +481,7 @@ section.course-content { } } -div.course-wrapper.closed section.course-content div.video-subtitles { +div.course-wrapper.closed section.course-content div.video { ol.subtitles { max-height: 577px; } diff --git a/templates/coffee/src/modules/video.coffee b/templates/coffee/src/modules/video.coffee new file mode 100644 index 0000000000..01aad7c334 --- /dev/null +++ b/templates/coffee/src/modules/video.coffee @@ -0,0 +1,41 @@ +class window.Video + constructor: (@id, videos) -> + @element = $("#video_#{@id}") + @parseVideos videos + @fetchMetadata() + @parseSpeed() + $("#video_#{@id}").data('video', this) + window.onYouTubePlayerAPIReady = => + $('.course-content .video').each -> + $(this).data('video').embed() + + youtubeId: (speed)-> + @videos[speed || @speed] + + parseVideos: (videos) -> + @videos = {} + $.each videos, (speed, url) => + speed = parseFloat(speed).toFixed(2).replace /\.00$/, '.0' + @videos[speed] = url + + 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(this) + + fetchMetadata: (url) -> + @metadata = {} + $.each @videos, (speed, url) => + $.get "http://gdata.youtube.com/feeds/api/videos/#{url}?v=2&alt=jsonc", ((data) => @metadata[data.data.id] = data.data) , 'jsonp' + + getDuration: -> + @metadata[@youtubeId()].duration diff --git a/templates/coffee/src/modules/video/video_caption.coffee b/templates/coffee/src/modules/video/video_caption.coffee new file mode 100644 index 0000000000..8742c2031a --- /dev/null +++ b/templates/coffee/src/modules/video/video_caption.coffee @@ -0,0 +1,116 @@ +class VideoCaption + constructor: (@player, @youtubeId) -> + @index = [] + @fetchCaption() + @bind() + + $: (selector) -> + @player.$(selector) + + bind: -> + $(window).bind('resize', @onWindowResize) + $(@player).bind('resize', @onWindowResize) + $(@player).bind('updatePlayTime', @onUpdatePlayTime) + @$('.hide-subtitles').click @toggle + + fetchCaption: -> + $.getJSON @captionURL(), (captions) => + @captions = captions.text + @start = captions.start + for index in [0...captions.start.length] + for time in [captions.start[index]..captions.end[index]] + @index[time] ||= [] + @index[time].push(index) + @render() + + captionURL: -> + "/static/subs/#{@youtubeId}.srt.sjson" + + render: -> + container = $('
      ') + container.css maxHeight: @$('.video-wrapper').height() - 5 + + $.each @captions, (index, text) => + container.append $('
    1. ').html(text).attr + 'data-index': index + 'data-start': @start[index] + + @$('.subtitles').replaceWith(container) + @$('.subtitles').mouseenter(@onMouseEnter).mouseleave(@onMouseLeave) + .mousemove(@onMovement).bind('mousewheel', @onMovement) + .bind('DOMMouseScroll', @onMovement) + @$('.subtitles li[data-index]').click @seekPlayer + + # prepend and append an empty
    2. for cosmatic reason + @$('.subtitles').prepend($('
    3. ').height(@topSpacingHeight())) + .append($('
    4. ').height(@bottomSpacingHeight())) + + onUpdatePlayTime: (event, time) => + # This 250ms offset is required to match the video speed + time = Math.round(Time.convert(time, @player.currentSpeed(), '1.0') * 1000 + 250) + newIndex = @index[time] + + if newIndex != undefined && @currentIndex != newIndex + if @currentIndex + for index in @currentIndex + @$(".subtitles li[data-index='#{index}']").removeClass('current') + + for index in newIndex + @$(".subtitles li[data-index='#{newIndex}']").addClass('current') + + @currentIndex = newIndex + @scrollCaption() + + onWindowResize: => + @$('.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: (event) => + @onMouseEnter() + + onMouseLeave: => + clearTimeout @frozen if @frozen + @frozen = null + @scrollCaption() if @player.isPlaying() + + 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', @player.currentSpeed()) / 1000) + $(@player).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 @player.element.hasClass('closed') + @$('.hide-subtitles').attr('title', 'Turn off captions') + @player.element.removeClass('closed') + else + @$('.hide-subtitles').attr('title', 'Turn on captions') + @player.element.addClass('closed') + @scrollCaption() + + captionHeight: -> + if @player.element.hasClass('fullscreen') + $(window).height() - @$('.video-controls').height() + else + @$('.video-wrapper').height() + diff --git a/templates/coffee/src/modules/video/video_player.coffee b/templates/coffee/src/modules/video/video_player.coffee new file mode 100644 index 0000000000..b6ae94aabb --- /dev/null +++ b/templates/coffee/src/modules/video/video_player.coffee @@ -0,0 +1,145 @@ +class VideoPlayer + constructor: (@video) -> + @currentTime = 0 + @element = $("#video_#{@video.id}") + @buildPlayer() + @bind() + + $: (selector) -> + $(selector, @element) + + bind: -> + $(@).bind('seek', @onSeek) + $(@).bind('updatePlayTime', @onUpdatePlayTime) + $(@).bind('speedChange', @onSpeedChange) + $(document).keyup @bindExitFullScreen + + @$('.video_control').click @togglePlayback + @$('.add-fullscreen').click @toggleFullScreen + @addToolTip unless onTouchBasedDevice() + + bindExitFullScreen: (event) => + if @element.hasClass('fullscreen') && event.keyCode == 27 + @toggleFullScreen(event) + + buildPlayer: -> + new VideoCaption(this, @video.youtubeId('1.0')) + new VideoSpeedControl(this, @video.speeds) + new VideoProgressSlider(this) + @player = new YT.Player @video.id, + playerVars: + controls: 0 + wmode: 'transparent' + rel: 0 + showinfo: 0 + enablejsapi: 1 + videoId: @video.youtubeId() + events: + onReady: @onReady + onStateChange: @onStateChange + + addToolTip: -> + @$('.add-fullscreen, .hide-subtitles').qtip + position: + my: 'top right' + at: 'top center' + + onReady: => + @setProgress(0, @duration()) + $(@).trigger('ready') + unless true || onTouchBasedDevice() + $('.course-content .video:first').data('video').player.play() + + onStateChange: (event) => + switch event.data + when YT.PlayerState.PLAYING + if window.player && window.player != @player + window.player.pauseVideo() + window.player = @player + @onPlay() + when YT.PlayerState.PAUSED + if window.player == @player + window.player = null + @onPause() + when YT.PlayerState.ENDED + if window.player == @player + window.player = null + @onPause() + + onPlay: -> + @$('.video_control').removeClass('play').addClass('pause').html('Pause') + unless @player.interval + @player.interval = setInterval(@update, 200) + + onPause: -> + @$('.video_control').removeClass('pause').addClass('play').html('Play') + clearInterval(@player.interval) + @player.interval = null + + onSeek: (event, time) -> + @player.seekTo(time, true) + if @isPlaying() + clearInterval(@player.interval) + @player.interval = setInterval(@update, 200) + else + @currentTime = time + $(@).trigger('updatePlayTime', time) + + onSpeedChange: (event, newSpeed) => + @currentTime = Time.convert(@currentTime, parseFloat(@currentSpeed()), newSpeed) + @video.setSpeed(parseFloat(newSpeed).toFixed(2).replace /\.00$/, '.0') + + if @isPlaying() + @player.loadVideoById(@video.youtubeId(), @currentTime) + else + @player.cueVideoById(@video.youtubeId(), @currentTime) + @setProgress(@currentTime, @duration()) + $(@).trigger('updatePlayTime', @currentTime) + + update: => + if @currentTime = @player.getCurrentTime() + $(@).trigger('updatePlayTime', @currentTime) + + onUpdatePlayTime: (event, time) => + @setProgress(@currentTime) if time + + setProgress: (time) => + progress = Time.format(time) + ' / ' + Time.format(@duration()) + if @progress != progress + @$(".vidtime").html(progress) + @progress = progress + + togglePlayback: (event) => + event.preventDefault() + if $(event.target).hasClass('play') + @play() + else + @pause() + + toggleFullScreen: (event) => + event.preventDefault() + if @element.hasClass('fullscreen') + @$('.exit').remove() + @$('.add-fullscreen').attr('title', 'Fill browser') + @element.removeClass('fullscreen') + else + @element.append('Exit').addClass('fullscreen') + @$('.add-fullscreen').attr('title', 'Exit fill browser') + @$('.exit').click @toggleFullScreen + $(@).trigger('resize') + + # Delegates + play: -> + @player.playVideo() if @player.playVideo + + isPlaying: -> + @player.getPlayerState() == YT.PlayerState.PLAYING + + pause: -> + @player.pauseVideo() + + duration: -> + @video.getDuration() + + currentSpeed: -> + @video.speed diff --git a/templates/coffee/src/modules/video/video_progress_slider.coffee b/templates/coffee/src/modules/video/video_progress_slider.coffee new file mode 100644 index 0000000000..4eaba392ba --- /dev/null +++ b/templates/coffee/src/modules/video/video_progress_slider.coffee @@ -0,0 +1,54 @@ +class VideoProgressSlider + constructor: (@player) -> + @buildSlider() + @buildHandle() + $(@player).bind('updatePlayTime', @onUpdatePlayTime) + $(@player).bind('ready', @onReady) + + $: (selector) -> + @player.$(selector) + + buildSlider: -> + @slider = @$('.slider').slider + range: 'min' + change: @onChange + slide: @onSlide + stop: @onStop + + buildHandle: -> + @handle = @$('.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 + + onReady: => + @slider.slider('option', 'max', @player.duration()) + + onUpdatePlayTime: (event, currentTime) => + if !@frozen + @slider.slider('option', 'max', @player.duration()) + @slider.slider('value', currentTime) + + onSlide: (event, ui) => + @frozen = true + @updateTooltip(ui.value) + $(@player).trigger('seek', ui.value) + + onChange: (event, ui) => + @updateTooltip(ui.value) + + onStop: (event, ui) => + @frozen = true + $(@player).trigger('seek', ui.value) + setTimeout (=> @frozen = false), 200 + + updateTooltip: (value)-> + @handle.qtip('option', 'content.text', "#{Time.format(value)}") diff --git a/templates/coffee/src/modules/video/video_speed_control.coffee b/templates/coffee/src/modules/video/video_speed_control.coffee new file mode 100644 index 0000000000..1214053fd9 --- /dev/null +++ b/templates/coffee/src/modules/video/video_speed_control.coffee @@ -0,0 +1,38 @@ +class VideoSpeedControl + constructor: (@player, @speeds) -> + @build() + @bind() + + $: (selector) -> + @player.$(selector) + + bind: -> + $(@player).bind('speedChange', @onSpeedChange) + @$('.video_speeds a').click @changeVideoSpeed + if onTouchBasedDevice() + @$('.speeds').click -> $(this).toggleClass('open') + else + @$('.speeds').mouseover -> $(this).addClass('open') + .mouseout -> $(this).removeClass('open') + .click (event) -> + event.preventDefault() + $(this).removeClass('open') + + build: -> + $.each @speeds, (index, speed) => + link = $('').attr(href: "#").html("#{speed}x") + @$('.video_speeds').prepend($('
    5. ').attr('data-speed', speed).html(link)) + @setSpeed(@player.currentSpeed()) + + changeVideoSpeed: (event) => + event.preventDefault() + unless $(event.target).parent().hasClass('active') + $(@player).trigger 'speedChange', $(event.target).parent().data('speed') + + onSpeedChange: (event, speed) => + @setSpeed(parseFloat(speed).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/templates/coffee/src/time.coffee b/templates/coffee/src/time.coffee new file mode 100644 index 0000000000..3e39820866 --- /dev/null +++ b/templates/coffee/src/time.coffee @@ -0,0 +1,21 @@ +class @Time + @format: (time) -> + seconds = Math.floor time + minutes = Math.floor seconds / 60 + hours = Math.floor minutes / 60 + seconds = seconds % 60 + minutes = minutes % 60 + + if hours + "#{hours}:#{@pad(minutes)}:#{@pad(seconds % 60)}" + else + "#{minutes}:#{@pad(seconds % 60)}" + + @pad: (number) -> + if number < 10 + "0#{number}" + else + number + + @convert: (time, oldSpeed, newSpeed) -> + (time * oldSpeed / newSpeed).toFixed(3) diff --git a/templates/courseware.html b/templates/courseware.html index 404871e49c..ca74d843a8 100644 --- a/templates/courseware.html +++ b/templates/courseware.html @@ -26,7 +26,7 @@ close -
      +
    6. '+key+'x
    7. '); - $("p.active").text(key + 'x'); - } else { - $("#video_speeds").append('
    8. '+key+'x
    9. '); - } - - $("#"+id).click(function(){ - change_video_speed(key, stream); - $(this).siblings().removeClass("active"); - $(this).addClass("active"); - var active = $(this).text(); - $("p.active").text(active); - }); - -} - -var l=[] -for (var key in streams) { - l.push(key); -} - -function sort_by_value(a,b) { - var x=parseFloat(a); - var y=parseFloat(b); - var r=((x < y) ? -1 : ((x > y) ? 1 : 0)); - return r; -} - -l.sort(sort_by_value); - -$(document).ready(function() { - video_speed = $.cookie("video_speed"); - - //ugly hack to account for different formats in vid speed in the XML (.75 vs 0.75, 1.5 vs 1.50); - if (( !video_speed ) || ( !streams[video_speed] && !streams[video_speed + "0"]) && !streams[video_speed.slice(0,-1)] && !streams[video_speed.slice(1)] && !streams["0" + video_speed]) { - video_speed = "1.0"; - } - - if (streams[video_speed + "0"]){ - video_speed = video_speed + "0"; - } else if (streams[video_speed.slice(0, -1)]){ - video_speed = video_speed.slice(0, -1); - } else if (streams[video_speed.slice(1)]) { - video_speed = video_speed.slice(1); - } else if (streams["0" + video_speed]) { - video_speed = "0" + video_speed; - } - - loadNewVideo(streams["1.0"], streams[video_speed], ${ position }); - - for(var i=0; i Date: Wed, 9 May 2012 18:20:38 -0400 Subject: [PATCH 20/57] Preserve window.postJSON for now --- static/coffee/src/main.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/static/coffee/src/main.coffee b/static/coffee/src/main.coffee index 5c658f3415..85c98ff929 100644 --- a/static/coffee/src/main.coffee +++ b/static/coffee/src/main.coffee @@ -20,3 +20,6 @@ $ -> schematic_value $("#schematic_#{circuit_id}").attr("value") $.post "/save_circuit/#{circuit_id}", schematic: schematic_value, (data) -> alert('Saved') if data.results == 'success' + + window.postJSON = (url, data, callback) -> + $.post url, data, callback From 823843cd2fdd3d771ee0d3645caeacb2f284576d Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Wed, 9 May 2012 18:22:33 -0400 Subject: [PATCH 21/57] Make sure video is working in sequence module --- static/coffee/src/courseware.coffee | 9 +++++++-- templates/coffee/src/modules/sequence.coffee | 5 +++-- templates/coffee/src/modules/video.coffee | 15 ++++++++++----- .../coffee/src/modules/video/video_player.coffee | 2 ++ 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/static/coffee/src/courseware.coffee b/static/coffee/src/courseware.coffee index d7469cec04..762df721c4 100644 --- a/static/coffee/src/courseware.coffee +++ b/static/coffee/src/courseware.coffee @@ -4,12 +4,17 @@ class window.Courseware new Calculator new FeedbackForm Logger.bind() - @renderModules() + @bind() + @render() @start: -> new Courseware - renderModules: -> + bind: -> + if $('#seq_content').length + $('#seq_content').change @render + + render: -> $('.course-content .video').each -> id = $(this).attr('id').replace(/video_/, '') new Video id, $(this).data('streams') diff --git a/templates/coffee/src/modules/sequence.coffee b/templates/coffee/src/modules/sequence.coffee index 96450199e8..e33cad5416 100644 --- a/templates/coffee/src/modules/sequence.coffee +++ b/templates/coffee/src/modules/sequence.coffee @@ -6,6 +6,7 @@ class window.Sequence bind: -> $('#sequence-list a').click @goto + $('#seq_content').change @toggleArrows buildNavigation: -> $.each @elements, (index, item) -> @@ -14,7 +15,7 @@ class window.Sequence list_item = $('
    10. ').append(link.append(title)) $('#sequence-list').append list_item - toggleArrows: -> + toggleArrows: => $('.sequence-nav-buttons a').unbind('click') if @position == 1 @@ -38,7 +39,7 @@ class window.Sequence MathJax.Hub.Queue(["Typeset",MathJax.Hub]) @position = new_position - @toggleArrows() + $('#seq_content').change() goto: (event) => event.preventDefault() diff --git a/templates/coffee/src/modules/video.coffee b/templates/coffee/src/modules/video.coffee index 8f35ae0cb1..9d059cf0b5 100644 --- a/templates/coffee/src/modules/video.coffee +++ b/templates/coffee/src/modules/video.coffee @@ -1,13 +1,16 @@ class Video constructor: (@id, videos) -> + window.player = null @element = $("#video_#{@id}") @parseVideos videos - @fetchMetadata() - @parseSpeed() $("#video_#{@id}").data('video', this) - window.onYouTubePlayerAPIReady = => - $('.course-content .video').each -> - $(this).data('video').embed() + + if YT.Player + @embed() + else + window.onYouTubePlayerAPIReady = => + $('.course-content .video').each -> + $(this).data('video').embed() youtubeId: (speed)-> @videos[speed || @speed] @@ -18,6 +21,8 @@ class Video video = video.split(/:/) speed = parseFloat(video[0]).toFixed(2).replace /\.00$/, '.0' @videos[speed] = video[1] + @fetchMetadata() + @parseSpeed() parseSpeed: -> @setSpeed($.cookie('video_speed')) diff --git a/templates/coffee/src/modules/video/video_player.coffee b/templates/coffee/src/modules/video/video_player.coffee index b51dbcd95e..2871f6ff0c 100644 --- a/templates/coffee/src/modules/video/video_player.coffee +++ b/templates/coffee/src/modules/video/video_player.coffee @@ -63,12 +63,14 @@ class VideoPlayer $(@).trigger('ended') onPlay: => + Logger.log 'play_video', id: @currentTime, code: @player.getVideoEmbedCode() window.player.pauseVideo() if window.player && window.player != @player window.player = @player unless @player.interval @player.interval = setInterval(@update, 200) onPause: => + Logger.log 'pause_video', id: @currentTime, code: @player.getVideoEmbedCode() window.player = null if window.player == @player clearInterval(@player.interval) @player.interval = null From afde79da334ccdd24510549c911b69745b13a5ad Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Wed, 9 May 2012 18:25:16 -0400 Subject: [PATCH 22/57] Fix the broken caption select --- templates/coffee/src/modules/video/video_caption.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/coffee/src/modules/video/video_caption.coffee b/templates/coffee/src/modules/video/video_caption.coffee index fd6ade363f..9115274222 100644 --- a/templates/coffee/src/modules/video/video_caption.coffee +++ b/templates/coffee/src/modules/video/video_caption.coffee @@ -15,7 +15,6 @@ class VideoCaption @$('.subtitles').mouseenter(@onMouseEnter).mouseleave(@onMouseLeave) .mousemove(@onMovement).bind('mousewheel', @onMovement) .bind('DOMMouseScroll', @onMovement) - @$('.subtitles li[data-index]').click @seekPlayer captionURL: -> "/static/subs/#{@youtubeId}.srt.sjson" @@ -39,6 +38,7 @@ class VideoCaption 'data-start': @start[index] @$('.subtitles').html(container.html()) + @$('.subtitles li[data-index]').click @seekPlayer # prepend and append an empty
    11. for cosmatic reason @$('.subtitles').prepend($('
    12. ').height(@topSpacingHeight())) From 8e7bf4c1825cc3c4e56791917fc7b463ab7c35e7 Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Thu, 10 May 2012 16:07:39 -0400 Subject: [PATCH 23/57] Fix broken test case --- static/coffee/files.json | 3 +- static/coffee/spec/courseware_spec.coffee | 111 ++++++---------- static/coffee/spec/courseware_spec.js | 120 ++++++------------ static/coffee/spec/feedback_form_spec.coffee | 4 +- static/coffee/spec/feedback_form_spec.js | 4 +- static/coffee/src/calculator.coffee | 2 +- static/coffee/src/courseware.coffee | 4 +- static/coffee/src/feedback_form.coffee | 2 +- templates/coffee/spec/navigation_spec.coffee | 74 +++++++++++ templates/coffee/spec/navigation_spec.js | 94 ++++++++++++++ .../coffee/src/courseware_navigation.coffee | 19 --- templates/coffee/src/modules/video.coffee | 2 +- templates/coffee/src/navigation.coffee | 2 +- 13 files changed, 256 insertions(+), 185 deletions(-) create mode 100644 templates/coffee/spec/navigation_spec.coffee create mode 100644 templates/coffee/spec/navigation_spec.js delete mode 100644 templates/coffee/src/courseware_navigation.coffee diff --git a/static/coffee/files.json b/static/coffee/files.json index bfae4dfe87..67bf671f6a 100644 --- a/static/coffee/files.json +++ b/static/coffee/files.json @@ -2,7 +2,8 @@ "js_files": [ "/static/js/jquery-1.6.2.min.js", "/static/js/jquery-ui-1.8.16.custom.min.js", - "/static/js/jquery.leanModal.js" + "/static/js/jquery.leanModal.js", + "/static/js/jquery.cookie.js" ], "static_files": [ "js/application.js" diff --git a/static/coffee/spec/courseware_spec.coffee b/static/coffee/spec/courseware_spec.coffee index 5933e3e686..8aa5e544a0 100644 --- a/static/coffee/spec/courseware_spec.coffee +++ b/static/coffee/spec/courseware_spec.coffee @@ -1,77 +1,46 @@ describe 'Courseware', -> + describe 'start', -> + it 'create the navigation', -> + spyOn(window, 'Navigation') + Courseware.start() + expect(window.Navigation).toHaveBeenCalled() + + it 'create the calculator', -> + spyOn(window, 'Calculator') + Courseware.start() + expect(window.Calculator).toHaveBeenCalled() + + it 'creates the FeedbackForm', -> + spyOn(window, 'FeedbackForm') + Courseware.start() + expect(window.FeedbackForm).toHaveBeenCalled() + + it 'binds the Logger', -> + spyOn(Logger, 'bind') + Courseware.start() + expect(Logger.bind).toHaveBeenCalled() + describe 'bind', -> - it 'bind the navigation', -> - spyOn Courseware.Navigation, 'bind' - Courseware.bind() - expect(Courseware.Navigation.bind).toHaveBeenCalled() - - describe 'Navigation', -> beforeEach -> - loadFixtures 'accordion.html' - @navigation = new Courseware.Navigation + @courseware = new Courseware + setFixtures """
      """ - describe 'bind', -> - describe 'when the #accordion exists', -> - describe 'when there is an active section', -> - it 'activate the accordion with correct active section', -> - spyOn $.fn, 'accordion' - $('#accordion').append('
      ') - Courseware.Navigation.bind() - expect($('#accordion').accordion).toHaveBeenCalledWith - active: 1 - header: 'h3' - autoHeight: false + it 'binds the sequential content change event', -> + @courseware.bind() + expect($('#seq_content')).toHandleWith 'change', @courseware.render - describe 'when there is no active section', -> - it 'activate the accordian with section 1 as active', -> - spyOn $.fn, 'accordion' - $('#accordion').append('
      ') - Courseware.Navigation.bind() - expect($('#accordion').accordion).toHaveBeenCalledWith - active: 1 - header: 'h3' - autoHeight: false + describe 'render', -> + beforeEach -> + @courseware = new Courseware + setFixtures """ +
      +
      +
      +
      + """ - it 'binds the accordionchange event', -> - Courseware.Navigation.bind() - expect($('#accordion')).toHandleWith 'accordionchange', @navigation.log - - it 'bind the navigation toggle', -> - Courseware.Navigation.bind() - expect($('#open_close_accordion a')).toHandleWith 'click', @navigation.toggle - - describe 'when the #accordion does not exists', -> - beforeEach -> - $('#accordion').remove() - - it 'does not activate the accordion', -> - spyOn $.fn, 'accordion' - Courseware.Navigation.bind() - expect($('#accordion').accordion).wasNotCalled() - - describe 'toggle', -> - it 'toggle closed class on the wrapper', -> - $('.course-wrapper').removeClass('closed') - - @navigation.toggle() - expect($('.course-wrapper')).toHaveClass('closed') - - @navigation.toggle() - expect($('.course-wrapper')).not.toHaveClass('closed') - - describe 'log', -> - beforeEach -> - window.log_event = -> - spyOn window, 'log_event' - - it 'submit event log', -> - @navigation.log {}, { - newHeader: - text: -> "new" - oldHeader: - text: -> "old" - } - - expect(window.log_event).toHaveBeenCalledWith 'accordion', - newheader: 'new' - oldheader: 'old' + it 'detect the video element and convert them', -> + spyOn(window, 'Video') + @courseware.render() + expect(window.Video).toHaveBeenCalledWith('1', '1.0:abc1234') + expect(window.Video).toHaveBeenCalledWith('2', '1.0:def5678') diff --git a/static/coffee/spec/courseware_spec.js b/static/coffee/spec/courseware_spec.js index d2d6e5583b..ce374dc421 100644 --- a/static/coffee/spec/courseware_spec.js +++ b/static/coffee/spec/courseware_spec.js @@ -1,96 +1,48 @@ (function() { describe('Courseware', function() { - describe('bind', function() { - return it('bind the navigation', function() { - spyOn(Courseware.Navigation, 'bind'); - Courseware.bind(); - return expect(Courseware.Navigation.bind).toHaveBeenCalled(); + describe('start', function() { + it('create the navigation', function() { + spyOn(window, 'Navigation'); + Courseware.start(); + return expect(window.Navigation).toHaveBeenCalled(); + }); + it('create the calculator', function() { + spyOn(window, 'Calculator'); + Courseware.start(); + return expect(window.Calculator).toHaveBeenCalled(); + }); + it('creates the FeedbackForm', function() { + spyOn(window, 'FeedbackForm'); + Courseware.start(); + return expect(window.FeedbackForm).toHaveBeenCalled(); + }); + return it('binds the Logger', function() { + spyOn(Logger, 'bind'); + Courseware.start(); + return expect(Logger.bind).toHaveBeenCalled(); }); }); - return describe('Navigation', function() { + describe('bind', function() { beforeEach(function() { - loadFixtures('accordion.html'); - return this.navigation = new Courseware.Navigation; + this.courseware = new Courseware; + return setFixtures("
      "); }); - describe('bind', function() { - describe('when the #accordion exists', function() { - describe('when there is an active section', function() { - return it('activate the accordion with correct active section', function() { - spyOn($.fn, 'accordion'); - $('#accordion').append('
      '); - Courseware.Navigation.bind(); - return expect($('#accordion').accordion).toHaveBeenCalledWith({ - active: 1, - header: 'h3', - autoHeight: false - }); - }); - }); - describe('when there is no active section', function() { - return it('activate the accordian with section 1 as active', function() { - spyOn($.fn, 'accordion'); - $('#accordion').append('
      '); - Courseware.Navigation.bind(); - return expect($('#accordion').accordion).toHaveBeenCalledWith({ - active: 1, - header: 'h3', - autoHeight: false - }); - }); - }); - it('binds the accordionchange event', function() { - Courseware.Navigation.bind(); - return expect($('#accordion')).toHandleWith('accordionchange', this.navigation.log); - }); - return it('bind the navigation toggle', function() { - Courseware.Navigation.bind(); - return expect($('#open_close_accordion a')).toHandleWith('click', this.navigation.toggle); - }); - }); - return describe('when the #accordion does not exists', function() { - beforeEach(function() { - return $('#accordion').remove(); - }); - return it('does not activate the accordion', function() { - spyOn($.fn, 'accordion'); - Courseware.Navigation.bind(); - return expect($('#accordion').accordion).wasNotCalled(); - }); - }); + return it('binds the sequential content change event', function() { + this.courseware.bind(); + return expect($('#seq_content')).toHandleWith('change', this.courseware.render); }); - describe('toggle', function() { - return it('toggle closed class on the wrapper', function() { - $('.course-wrapper').removeClass('closed'); - this.navigation.toggle(); - expect($('.course-wrapper')).toHaveClass('closed'); - this.navigation.toggle(); - return expect($('.course-wrapper')).not.toHaveClass('closed'); - }); + }); + return describe('render', function() { + beforeEach(function() { + this.courseware = new Courseware; + return setFixtures("
      \n
      \n
      \n
      "); }); - return describe('log', function() { - beforeEach(function() { - window.log_event = function() {}; - return spyOn(window, 'log_event'); - }); - return it('submit event log', function() { - this.navigation.log({}, { - newHeader: { - text: function() { - return "new"; - } - }, - oldHeader: { - text: function() { - return "old"; - } - } - }); - return expect(window.log_event).toHaveBeenCalledWith('accordion', { - newheader: 'new', - oldheader: 'old' - }); - }); + return it('detect the video element and convert them', function() { + spyOn(window, 'Video'); + this.courseware.render(); + expect(window.Video).toHaveBeenCalledWith('1', '1.0:abc1234'); + return expect(window.Video).toHaveBeenCalledWith('2', '1.0:def5678'); }); }); }); diff --git a/static/coffee/spec/feedback_form_spec.coffee b/static/coffee/spec/feedback_form_spec.coffee index 191645b3d3..41ba2ec04d 100644 --- a/static/coffee/spec/feedback_form_spec.coffee +++ b/static/coffee/spec/feedback_form_spec.coffee @@ -2,9 +2,9 @@ describe 'FeedbackForm', -> beforeEach -> loadFixtures 'feedback_form.html' - describe 'bind', -> + describe 'constructor', -> beforeEach -> - FeedbackForm.bind() + new FeedbackForm spyOn($, 'post').andCallFake (url, data, callback, format) -> callback() diff --git a/static/coffee/spec/feedback_form_spec.js b/static/coffee/spec/feedback_form_spec.js index bccb53604f..9824915d03 100644 --- a/static/coffee/spec/feedback_form_spec.js +++ b/static/coffee/spec/feedback_form_spec.js @@ -4,9 +4,9 @@ beforeEach(function() { return loadFixtures('feedback_form.html'); }); - return describe('bind', function() { + return describe('constructor', function() { beforeEach(function() { - FeedbackForm.bind(); + new FeedbackForm; return spyOn($, 'post').andCallFake(function(url, data, callback, format) { return callback(); }); diff --git a/static/coffee/src/calculator.coffee b/static/coffee/src/calculator.coffee index 1486775ad4..bca88503fd 100644 --- a/static/coffee/src/calculator.coffee +++ b/static/coffee/src/calculator.coffee @@ -1,4 +1,4 @@ -class Calculator +class @Calculator constructor: -> $('.calc').click @toggle $('form#calculator').submit(@calculate).submit (e) -> diff --git a/static/coffee/src/courseware.coffee b/static/coffee/src/courseware.coffee index 762df721c4..fa746956f0 100644 --- a/static/coffee/src/courseware.coffee +++ b/static/coffee/src/courseware.coffee @@ -1,6 +1,6 @@ -class window.Courseware +class @Courseware constructor: -> - new CoursewareNavigation + new Navigation new Calculator new FeedbackForm Logger.bind() diff --git a/static/coffee/src/feedback_form.coffee b/static/coffee/src/feedback_form.coffee index 8d34748dbf..ffb8b37521 100644 --- a/static/coffee/src/feedback_form.coffee +++ b/static/coffee/src/feedback_form.coffee @@ -1,4 +1,4 @@ -class FeedbackForm +class @FeedbackForm constructor: -> $('#feedback_button').click -> data = diff --git a/templates/coffee/spec/navigation_spec.coffee b/templates/coffee/spec/navigation_spec.coffee new file mode 100644 index 0000000000..cb98c2b64c --- /dev/null +++ b/templates/coffee/spec/navigation_spec.coffee @@ -0,0 +1,74 @@ +describe 'Navigation', -> + beforeEach -> + loadFixtures 'accordion.html' + @navigation = new Navigation + + describe 'constructor', -> + describe 'when the #accordion exists', -> + describe 'when there is an active section', -> + beforeEach -> + spyOn $.fn, 'accordion' + $('#accordion').append('
      ') + new Navigation + + it 'activate the accordion with correct active section', -> + expect($('#accordion').accordion).toHaveBeenCalledWith + active: 1 + header: 'h3' + autoHeight: false + + describe 'when there is no active section', -> + beforeEach -> + spyOn $.fn, 'accordion' + $('#accordion').append('
      ') + new Navigation + + it 'activate the accordian with section 1 as active', -> + expect($('#accordion').accordion).toHaveBeenCalledWith + active: 1 + header: 'h3' + autoHeight: false + + it 'binds the accordionchange event', -> + Navigation.bind() + expect($('#accordion')).toHandleWith 'accordionchange', @navigation.log + + it 'bind the navigation toggle', -> + Navigation.bind() + expect($('#open_close_accordion a')).toHandleWith 'click', @navigation.toggle + + describe 'when the #accordion does not exists', -> + beforeEach -> + $('#accordion').remove() + + it 'does not activate the accordion', -> + spyOn $.fn, 'accordion' + Navigation.bind() + expect($('#accordion').accordion).wasNotCalled() + + describe 'toggle', -> + it 'toggle closed class on the wrapper', -> + $('.course-wrapper').removeClass('closed') + + @navigation.toggle() + expect($('.course-wrapper')).toHaveClass('closed') + + @navigation.toggle() + expect($('.course-wrapper')).not.toHaveClass('closed') + + describe 'log', -> + beforeEach -> + window.log_event = -> + spyOn window, 'log_event' + + it 'submit event log', -> + @navigation.log {}, { + newHeader: + text: -> "new" + oldHeader: + text: -> "old" + } + + expect(window.log_event).toHaveBeenCalledWith 'accordion', + newheader: 'new' + oldheader: 'old' diff --git a/templates/coffee/spec/navigation_spec.js b/templates/coffee/spec/navigation_spec.js new file mode 100644 index 0000000000..3ba6508eec --- /dev/null +++ b/templates/coffee/spec/navigation_spec.js @@ -0,0 +1,94 @@ +// Generated by CoffeeScript 1.3.2-pre +(function() { + + describe('Navigation', function() { + beforeEach(function() { + loadFixtures('accordion.html'); + return this.navigation = new Navigation; + }); + describe('constructor', function() { + describe('when the #accordion exists', function() { + describe('when there is an active section', function() { + beforeEach(function() { + spyOn($.fn, 'accordion'); + $('#accordion').append('
      '); + return new Navigation; + }); + return it('activate the accordion with correct active section', function() { + return expect($('#accordion').accordion).toHaveBeenCalledWith({ + active: 1, + header: 'h3', + autoHeight: false + }); + }); + }); + describe('when there is no active section', function() { + beforeEach(function() { + spyOn($.fn, 'accordion'); + $('#accordion').append('
      '); + return new Navigation; + }); + return it('activate the accordian with section 1 as active', function() { + return expect($('#accordion').accordion).toHaveBeenCalledWith({ + active: 1, + header: 'h3', + autoHeight: false + }); + }); + }); + it('binds the accordionchange event', function() { + Navigation.bind(); + return expect($('#accordion')).toHandleWith('accordionchange', this.navigation.log); + }); + return it('bind the navigation toggle', function() { + Navigation.bind(); + return expect($('#open_close_accordion a')).toHandleWith('click', this.navigation.toggle); + }); + }); + return describe('when the #accordion does not exists', function() { + beforeEach(function() { + return $('#accordion').remove(); + }); + return it('does not activate the accordion', function() { + spyOn($.fn, 'accordion'); + Navigation.bind(); + return expect($('#accordion').accordion).wasNotCalled(); + }); + }); + }); + describe('toggle', function() { + return it('toggle closed class on the wrapper', function() { + $('.course-wrapper').removeClass('closed'); + this.navigation.toggle(); + expect($('.course-wrapper')).toHaveClass('closed'); + this.navigation.toggle(); + return expect($('.course-wrapper')).not.toHaveClass('closed'); + }); + }); + return describe('log', function() { + beforeEach(function() { + window.log_event = function() {}; + return spyOn(window, 'log_event'); + }); + return it('submit event log', function() { + this.navigation.log({}, { + newHeader: { + text: function() { + return "new"; + } + }, + oldHeader: { + text: function() { + return "old"; + } + } + }); + return expect(window.log_event).toHaveBeenCalledWith('accordion', { + newheader: 'new', + oldheader: 'old' + }); + }); + }); + }); + +}).call(this); diff --git a/templates/coffee/src/courseware_navigation.coffee b/templates/coffee/src/courseware_navigation.coffee deleted file mode 100644 index aef204ea41..0000000000 --- a/templates/coffee/src/courseware_navigation.coffee +++ /dev/null @@ -1,19 +0,0 @@ -class CoursewareNavigation - constructor: -> - if $('#accordion').length - active = $('#accordion ul:has(li.active)').index('#accordion ul') - $('#accordion').bind('accordionchange', @log).accordion - active: if active >= 0 then active else 1 - header: 'h3' - autoHeight: false - $('#open_close_accordion a').click @toggle - - $('#accordion').show() - - log: (event, ui) -> - log_event 'accordion', - newheader: ui.newHeader.text() - oldheader: ui.oldHeader.text() - - toggle: -> - $('.course-wrapper').toggleClass('closed') diff --git a/templates/coffee/src/modules/video.coffee b/templates/coffee/src/modules/video.coffee index 9d059cf0b5..9fd0605c97 100644 --- a/templates/coffee/src/modules/video.coffee +++ b/templates/coffee/src/modules/video.coffee @@ -1,4 +1,4 @@ -class Video +class @Video constructor: (@id, videos) -> window.player = null @element = $("#video_#{@id}") diff --git a/templates/coffee/src/navigation.coffee b/templates/coffee/src/navigation.coffee index 0f33d0fa10..7ba0a94fad 100644 --- a/templates/coffee/src/navigation.coffee +++ b/templates/coffee/src/navigation.coffee @@ -1,4 +1,4 @@ -class Courseware::Navigation +class @Navigation constructor: -> if $('#accordion').length active = $('#accordion ul:has(li.active)').index('#accordion ul') From 1abe15d4d9b5d84b40884f7b18f53269fd32de7e Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Thu, 10 May 2012 19:40:55 -0400 Subject: [PATCH 24/57] Rewrite Problem module --- djangoapps/courseware/modules/capa_module.py | 10 +- static/coffee/spec/courseware_spec.coffee | 2 +- static/coffee/spec/courseware_spec.js | 2 +- static/coffee/src/courseware.coffee | 5 +- static/sass/courseware/_courseware.scss | 7 ++ templates/coffee/src/modules/problem.coffee | 57 ++++++++++++ templates/coffee/src/modules/sequence.coffee | 2 +- templates/problem.html | 33 ++++--- templates/problem.js | 96 -------------------- templates/problem_ajax.html | 2 +- 10 files changed, 90 insertions(+), 126 deletions(-) create mode 100644 templates/coffee/src/modules/problem.coffee delete mode 100644 templates/problem.js diff --git a/djangoapps/courseware/modules/capa_module.py b/djangoapps/courseware/modules/capa_module.py index 7d42cfb250..b54b028c11 100644 --- a/djangoapps/courseware/modules/capa_module.py +++ b/djangoapps/courseware/modules/capa_module.py @@ -61,12 +61,6 @@ class Module(XModule): 'ajax_url':self.ajax_url, }) - def get_init_js(self): - return render_to_string('problem.js', - {'id':self.item_id, - 'ajax_url':self.ajax_url, - }) - def get_problem_html(self, encapsulate=True): html = self.lcp.get_html() content={'name':self.name, @@ -130,8 +124,8 @@ class Module(XModule): html=render_to_string('problem.html', context) if encapsulate: - html = '
      '.format(id=self.item_id)+html+"
      " - + html = '
      '.format(id=self.item_id)+html+"
      " + return html def __init__(self, system, xml, item_id, state=None): diff --git a/static/coffee/spec/courseware_spec.coffee b/static/coffee/spec/courseware_spec.coffee index 8aa5e544a0..3baaf73acd 100644 --- a/static/coffee/spec/courseware_spec.coffee +++ b/static/coffee/spec/courseware_spec.coffee @@ -27,7 +27,7 @@ describe 'Courseware', -> it 'binds the sequential content change event', -> @courseware.bind() - expect($('#seq_content')).toHandleWith 'change', @courseware.render + expect($('#seq_content')).toHandleWith 'contentChanged', @courseware.render describe 'render', -> beforeEach -> diff --git a/static/coffee/spec/courseware_spec.js b/static/coffee/spec/courseware_spec.js index ce374dc421..346fd46e20 100644 --- a/static/coffee/spec/courseware_spec.js +++ b/static/coffee/spec/courseware_spec.js @@ -30,7 +30,7 @@ }); return it('binds the sequential content change event', function() { this.courseware.bind(); - return expect($('#seq_content')).toHandleWith('change', this.courseware.render); + return expect($('#seq_content')).toHandleWith('contentChanged', this.courseware.render); }); }); return describe('render', function() { diff --git a/static/coffee/src/courseware.coffee b/static/coffee/src/courseware.coffee index fa746956f0..c27db29006 100644 --- a/static/coffee/src/courseware.coffee +++ b/static/coffee/src/courseware.coffee @@ -12,9 +12,12 @@ class @Courseware bind: -> if $('#seq_content').length - $('#seq_content').change @render + $('#seq_content').bind 'contentChanged', @render render: -> $('.course-content .video').each -> id = $(this).attr('id').replace(/video_/, '') new Video id, $(this).data('streams') + $('.course-content .problems-wrapper').each -> + id = $(this).attr('id').replace(/problem_/, '') + new Problem id, $(this).data('url') diff --git a/static/sass/courseware/_courseware.scss b/static/sass/courseware/_courseware.scss index 520440c234..bd707e1b80 100644 --- a/static/sass/courseware/_courseware.scss +++ b/static/sass/courseware/_courseware.scss @@ -54,6 +54,13 @@ div.course-wrapper { display: table-cell; vertical-align: top; + &.problem-header { + section.staff { + margin-top: 30px; + font-size: 80%; + } + } + @media screen and (max-width:1120px) { display: block; width: auto; diff --git a/templates/coffee/src/modules/problem.coffee b/templates/coffee/src/modules/problem.coffee new file mode 100644 index 0000000000..add045266c --- /dev/null +++ b/templates/coffee/src/modules/problem.coffee @@ -0,0 +1,57 @@ +class @Problem + constructor: (@id, url) -> + @element = $("#problem_#{id}") + @content_url = "#{url}problem_get?id=#{@id}" + @render() + + $: (selector) -> + $(selector, @element) + + bind: => + MathJax.Hub.Queue ["Typeset",MathJax.Hub] + window.update_schematics() + @$('section.action input:button').click @refreshAnswers + @$('section.action input.check').click @check + @$('section.action input.reset').click @reset + @$('section.action input.show').click @show + @$('section.action input.save').click @save + + render: (content) -> + if content + @element.html(content) + @bind() + else + @element.load @content_url, @bind + + check: => + Logger.log 'problem_check', @answers + $.post "/modx/problem/#{@id}/problem_check", @answers, (response) => + switch response.success + when 'incorrect', 'correct' + @render(response.contents) + else + alert(response.success) + + reset: => + Logger.log 'problem_reset', @answers + $.post "/modx/problem/#{@id}/problem_reset", id: @id, (content) => + @render(content) + + show: => + Logger.log 'problem_show', problem: @id + $.post "/modx/problem/#{@id}/problem_show", (response) => + $.each response, (key, value) => + @$("#answer_#{key}").text(value) + + save: => + Logger.log 'problem_save', @answers + $.post "/modx/problem/#{@id}/problem_save", @answers, (response) => + if response.success + alert 'Saved' + + refreshAnswers: => + @answers = {} + @$('input.schematic').each (index, element) -> + element.schematic.update_value() + $.each @$("[id^=input_#{@id}_]"), (index, input) => + @answers[$(input).attr('id')] = $(input).val() diff --git a/templates/coffee/src/modules/sequence.coffee b/templates/coffee/src/modules/sequence.coffee index e33cad5416..9c5fd7b7c0 100644 --- a/templates/coffee/src/modules/sequence.coffee +++ b/templates/coffee/src/modules/sequence.coffee @@ -39,7 +39,7 @@ class window.Sequence MathJax.Hub.Queue(["Typeset",MathJax.Hub]) @position = new_position - $('#seq_content').change() + $('#seq_content').trigger 'contentChanged' goto: (event) => event.preventDefault() diff --git a/templates/problem.html b/templates/problem.html index af712f67ac..3f79be3b3a 100644 --- a/templates/problem.html +++ b/templates/problem.html @@ -1,17 +1,15 @@ <%namespace name='static' file='static_content.html'/> -

      ${ problem['name'] } -% if problem['weight']: -: ${ problem['weight'] } points -% endif -% if settings.QUICKEDIT: - -
      -
      -
      -
      -Quick -Edit Problem
      -% endif +

      + ${ problem['name'] } + % if problem['weight']: + : ${ problem['weight'] } points + % endif + + % if settings.QUICKEDIT: +
      + Quick Edit Problem +
      + % endif

      @@ -21,16 +19,16 @@ Edit Problem % if check_button: - + % endif % if reset_button: - + % endif % if save_button: - + % endif % if answer_available: - + % endif % if explain : Explanation @@ -42,3 +40,4 @@ Edit Problem % endif
      + diff --git a/templates/problem.js b/templates/problem.js deleted file mode 100644 index 80924a79a6..0000000000 --- a/templates/problem.js +++ /dev/null @@ -1,96 +0,0 @@ -function ${ id }_content_updated() { - MathJax.Hub.Queue(["Typeset",MathJax.Hub]); - update_schematics(); - - $('#check_${ id }').unbind('click').click(function() { - $("input.schematic").each(function(index,element){ element.schematic.update_value(); }); - $(".CodeMirror").each(function(index,element){ if (element.CodeMirror.save) element.CodeMirror.save(); }); - var submit_data={}; - $.each($("[id^=input_${ id }_]"), function(index,value){ - if (value.type==="checkbox"){ - if (value.checked) { - if (typeof submit_data[value.name] == 'undefined'){ - submit_data[value.name]=[]; - } - submit_data[value.name].push(value.value); - } - } - if (value.type==="radio"){ - if (value.checked) { - submit_data[value.name]= value.value; - } - } - else{ - submit_data[value.id]=value.value; - } - }); - postJSON('${ MITX_ROOT_URL }/modx/problem/${ id }/problem_check', - submit_data, - function(json) { - switch(json.success) { - case 'incorrect': // Worked, but answer not - case 'correct': - $('#main_${ id }').html(json.contents); - ${ id }_content_updated(); - break; - default: - alert(json.success); - }} - ); - log_event('problem_check', submit_data); - }); - - $('#reset_${ id }').unbind('click').click(function() { - var submit_data={}; - $.each($("[id^=input_${ id }_]"), function(index,value){ - submit_data[value.id]=value.value; - }); - - postJSON('${ MITX_ROOT_URL }/modx/problem/${ id }/problem_reset', {'id':'${ id }'}, function(html_as_json) { - $('#main_${ id }').html(html_as_json); - ${ id }_content_updated(); - }); - log_event('problem_reset', submit_data); - }); - - // show answer button - // TODO: the button should turn into "hide answer" afterwards - $('#show_${ id }').unbind('click').click(function() { - postJSON('${ MITX_ROOT_URL }/modx/problem/${ id }/problem_show', {}, function(data) { - for (var key in data) { - if ($.isArray(data[key])){ - for (var ans_index in data[key]){ - var choice_id = 'input_'+key+'_'+data[key][ans_index]; - $("label[for="+choice_id+"]").attr("correct_answer", "true"); - } - } - $("#answer_"+key).text(data[key]); - } - }); - - log_event('problem_show', {'problem':'${ id }'}); - }); - - $('#save_${ id }').unbind('click').click(function() { - $("input.schematic").each(function(index,element){ element.schematic.update_value(); }); - var submit_data={}; - $.each($("[id^=input_${ id }_]"), function(index,value) { - submit_data[value.id]=value.value; - }); - postJSON('${ MITX_ROOT_URL }/modx/problem/${ id }/problem_save', - submit_data, - function(data) { - if(data.success) { - alert('Saved'); - }}); - log_event('problem_save', submit_data); - }); -} - -function ${ id }_load() { - $('#main_${ id }').load('${ ajax_url }problem_get?id=${ id }', ${ id }_content_updated); -} - -$(function() { - ${ id }_load(); -}); diff --git a/templates/problem_ajax.html b/templates/problem_ajax.html index 7296b362ad..78b85df3c1 100644 --- a/templates/problem_ajax.html +++ b/templates/problem_ajax.html @@ -1 +1 @@ -
      +
      From 59b453488ac528b1573bbb53788d06eda3a277bc Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Thu, 10 May 2012 21:04:53 -0400 Subject: [PATCH 25/57] Rewrite Tab module, refactor Sequence module --- djangoapps/courseware/modules/seq_module.py | 8 ++-- static/coffee/spec/courseware_spec.coffee | 10 +++-- static/coffee/spec/courseware_spec.js | 6 +-- static/coffee/src/courseware.coffee | 4 +- templates/coffee/src/modules/sequence.coffee | 28 +++++++----- .../coffee/src/modules/tab_module.coffee | 23 ++++++++++ templates/tab_module.html | 22 +++++----- templates/tab_module.js | 44 ------------------- 8 files changed, 64 insertions(+), 81 deletions(-) create mode 100644 templates/coffee/src/modules/tab_module.coffee delete mode 100644 templates/tab_module.js diff --git a/djangoapps/courseware/modules/seq_module.py b/djangoapps/courseware/modules/seq_module.py index 4239bc492f..a9c6138cc9 100644 --- a/djangoapps/courseware/modules/seq_module.py +++ b/djangoapps/courseware/modules/seq_module.py @@ -52,13 +52,12 @@ class Module(XModule): child_classes = [set([i.tag for i in e.iter()]) for e in self.xmltree] titles = ["\n".join([i.get("name").strip() for i in e.iter() if i.get("name") != None]) \ - for e in self.xmltree] + for e in self.xmltree] - self.contents = [j(self.render_function(e)) \ - for e in self.xmltree] + self.contents = [j(self.render_function(e)) for e in self.xmltree] for i in range(len(self.contents)): - self.contents[i]['title'] = titles[i] + self.contents[i]['title'] = titles[i] for (content, element_class) in zip(self.contents, child_classes): new_class = 'other' @@ -83,7 +82,6 @@ class Module(XModule): if self.xmltree.tag in ['sequential', 'videosequence']: self.content=render_to_string('seq_module.html',params) if self.xmltree.tag == 'tab': - params['id'] = 'tab' self.content=render_to_string('tab_module.html',params) self.rendered = True diff --git a/static/coffee/spec/courseware_spec.coffee b/static/coffee/spec/courseware_spec.coffee index 3baaf73acd..91b9187668 100644 --- a/static/coffee/spec/courseware_spec.coffee +++ b/static/coffee/spec/courseware_spec.coffee @@ -23,11 +23,15 @@ describe 'Courseware', -> describe 'bind', -> beforeEach -> @courseware = new Courseware - setFixtures """
      """ + setFixtures """ +
      +
      +
      + """ - it 'binds the sequential content change event', -> + it 'binds the content change event', -> @courseware.bind() - expect($('#seq_content')).toHandleWith 'contentChanged', @courseware.render + expect($('.course-content .sequence')).toHandleWith 'contentChanged', @courseware.render describe 'render', -> beforeEach -> diff --git a/static/coffee/spec/courseware_spec.js b/static/coffee/spec/courseware_spec.js index 346fd46e20..adb04adc49 100644 --- a/static/coffee/spec/courseware_spec.js +++ b/static/coffee/spec/courseware_spec.js @@ -26,11 +26,11 @@ describe('bind', function() { beforeEach(function() { this.courseware = new Courseware; - return setFixtures("
      "); + return setFixtures("
      \n
      \n
      "); }); - return it('binds the sequential content change event', function() { + return it('binds the content change event', function() { this.courseware.bind(); - return expect($('#seq_content')).toHandleWith('contentChanged', this.courseware.render); + return expect($('.course-content .sequence')).toHandleWith('contentChanged', this.courseware.render); }); }); return describe('render', function() { diff --git a/static/coffee/src/courseware.coffee b/static/coffee/src/courseware.coffee index c27db29006..e2f015baef 100644 --- a/static/coffee/src/courseware.coffee +++ b/static/coffee/src/courseware.coffee @@ -11,8 +11,8 @@ class @Courseware new Courseware bind: -> - if $('#seq_content').length - $('#seq_content').bind 'contentChanged', @render + $('.course-content .sequence, .course-content .tab') + .bind 'contentChanged', @render render: -> $('.course-content .video').each -> diff --git a/templates/coffee/src/modules/sequence.coffee b/templates/coffee/src/modules/sequence.coffee index 9c5fd7b7c0..42e9cde71f 100644 --- a/templates/coffee/src/modules/sequence.coffee +++ b/templates/coffee/src/modules/sequence.coffee @@ -1,32 +1,36 @@ -class window.Sequence +class @Sequence constructor: (@id, @elements, @tag, position) -> + @element = $("#sequence_#{@id}") @buildNavigation() @bind() @render position + $: (selector) -> + $(selector, @element) + bind: -> - $('#sequence-list a').click @goto - $('#seq_content').change @toggleArrows + @element.bind 'contentChanged', @toggleArrows + @$('#sequence-list a').click @goto buildNavigation: -> $.each @elements, (index, item) -> link = $('').attr class: "seq_#{item.type}_inactive", 'data-element': index + 1 title = $('

      ').html(item.title) list_item = $('

    13. ').append(link.append(title)) - $('#sequence-list').append list_item + @$('#sequence-list').append list_item toggleArrows: => - $('.sequence-nav-buttons a').unbind('click') + @$('.sequence-nav-buttons a').unbind('click') if @position == 1 - $('.sequence-nav-buttons .prev a').addClass('disabled') + @$('.sequence-nav-buttons .prev a').addClass('disabled') else - $('.sequence-nav-buttons .prev a').removeClass('disabled').click(@previous) + @$('.sequence-nav-buttons .prev a').removeClass('disabled').click(@previous) if @position == @elements.length - $('.sequence-nav-buttons .next a').addClass('disabled') + @$('.sequence-nav-buttons .next a').addClass('disabled') else - $('.sequence-nav-buttons .next a').removeClass('disabled').click(@next) + @$('.sequence-nav-buttons .next a').removeClass('disabled').click(@next) render: (new_position) -> if @position != undefined @@ -35,11 +39,11 @@ class window.Sequence if @position != new_position @mark_active new_position - $('#seq_content').html eval(@elements[new_position - 1].content) + @$('#seq_content').html eval(@elements[new_position - 1].content) MathJax.Hub.Queue(["Typeset",MathJax.Hub]) @position = new_position - $('#seq_content').trigger 'contentChanged' + @element.trigger 'contentChanged' goto: (event) => event.preventDefault() @@ -60,7 +64,7 @@ class window.Sequence @render new_position link_for: (position) -> - $("#sequence-list a[data-element=#{position}]") + @$("#sequence-list a[data-element=#{position}]") mark_visited: (position) -> @link_for(position).attr class: "seq_#{@elements[position - 1].type}_visited" diff --git a/templates/coffee/src/modules/tab_module.coffee b/templates/coffee/src/modules/tab_module.coffee new file mode 100644 index 0000000000..69d833b86e --- /dev/null +++ b/templates/coffee/src/modules/tab_module.coffee @@ -0,0 +1,23 @@ +class @Tab + constructor: (@id, @items) -> + @element = $("#tab_#{id}") + @render() + + $: (selector) -> + $(selector, @element) + + render: -> + $.each @items, (index, item) => + tab = $('').attr(href: "##{@tabId(index)}").html(item.title) + @$('.navigation').append($('
    14. ').append(tab)) + @element.append($('
      ').attr(id: @tabId(index))) + @element.tabs + show: @onShow + + onShow: (element, ui) => + @$('section.ui-tabs-hide').html('') + @$("##{@tabId(ui.index)}").html(eval(@items[ui.index]['content'])) + @element.trigger 'contentChanged' + + tabId: (index) -> + "tab-#{@id}-#{index}" diff --git a/templates/tab_module.html b/templates/tab_module.html index b2f2e3280e..21f69fc359 100644 --- a/templates/tab_module.html +++ b/templates/tab_module.html @@ -1,13 +1,11 @@ -
      - - -% for t in items: -
      -
      -% endfor - +
      +
      + +<%block name="js_extra"> + + diff --git a/templates/tab_module.js b/templates/tab_module.js deleted file mode 100644 index e8e5adc7e7..0000000000 --- a/templates/tab_module.js +++ /dev/null @@ -1,44 +0,0 @@ -// IMPORTANT TODO: Namespace - -var ${ id }contents=["", - %for t in items: - ${t[1]['content']} , - %endfor - "" - ]; - -var ${ id }init_functions=["", - %for t in items: - function(){ ${t[1]['init_js']} }, - %endfor - ""]; - -var ${ id }destroy_functions=["", - %for t in items: - function(){ ${t[1]['destroy_js']} }, - %endfor - ""]; - -var ${ id }loc = -1; - -function ${ id }goto(i) { - if (${ id }loc!=-1) - ${ id }destroy_functions[ ${ id }loc ](); - $('#tabs-'+(i-1)).html(${ id }contents[i]); - ${ id }init_functions[i]() - $('#tt_'+${ id }loc).attr("style", "background-color:grey"); - ${ id }loc=i; - $('#tt_'+i).attr("style", "background-color:red"); - MathJax.Hub.Queue(["Typeset",MathJax.Hub]); -} - -$("#tabs").tabs({select:function(event, ui){ - //global=ui; - return true; - }, - show:function(event,ui){ - //global=ui; - ${ id }goto(ui.index+1); - return true; - }, - }); From d393a503e604a288c0eee1f98e42b721e329ac76 Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Thu, 10 May 2012 21:19:24 -0400 Subject: [PATCH 26/57] Remove unused JavaScript on the page bottom --- templates/coffee/src/modules/sequence.coffee | 6 +++++- templates/courseware.html | 6 ++---- templates/main.html | 2 -- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/templates/coffee/src/modules/sequence.coffee b/templates/coffee/src/modules/sequence.coffee index 42e9cde71f..9c1549ff78 100644 --- a/templates/coffee/src/modules/sequence.coffee +++ b/templates/coffee/src/modules/sequence.coffee @@ -11,9 +11,10 @@ class @Sequence bind: -> @element.bind 'contentChanged', @toggleArrows @$('#sequence-list a').click @goto + @$('.sequence-nav li a').hover @navHover buildNavigation: -> - $.each @elements, (index, item) -> + $.each @elements, (index, item) => link = $('').attr class: "seq_#{item.type}_inactive", 'data-element': index + 1 title = $('

      ').html(item.title) list_item = $('

    15. ').append(link.append(title)) @@ -45,6 +46,9 @@ class @Sequence @position = new_position @element.trigger 'contentChanged' + navHover: (event) => + $(event.target).siblings().toggleClass("shown") + goto: (event) => event.preventDefault() new_position = $(event.target).data('element') diff --git a/templates/courseware.html b/templates/courseware.html index ca74d843a8..2b53eaa3a0 100644 --- a/templates/courseware.html +++ b/templates/courseware.html @@ -7,12 +7,10 @@ <%block name="js_extra"> -##Is there a reason this isn't in header_extra? Is it important that the javascript is at the bottom of the generated document? diff --git a/templates/main.html b/templates/main.html index 0a3424a053..00abe2902d 100644 --- a/templates/main.html +++ b/templates/main.html @@ -113,7 +113,6 @@ - @@ -121,7 +120,6 @@ document.write('\x3Cscript type="text/javascript" src="' + document.location.protocol + '//www.youtube.com/player_api">\x3C/script>'); - <%block name="js_extra"/> From 80ba9bd0ffcfb0f903614561faf6316a17f8cb2f Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Thu, 10 May 2012 22:25:14 -0400 Subject: [PATCH 27/57] Remove unused `init_js` and `destroy_js` --- .../management/commands/check_course.py | 2 +- djangoapps/courseware/module_render.py | 11 ++--------- .../courseware/modules/vertical_module.py | 9 --------- djangoapps/courseware/modules/x_module.py | 19 ------------------- 4 files changed, 3 insertions(+), 38 deletions(-) diff --git a/djangoapps/courseware/management/commands/check_course.py b/djangoapps/courseware/management/commands/check_course.py index 2f069ee5f3..17a97268cf 100644 --- a/djangoapps/courseware/management/commands/check_course.py +++ b/djangoapps/courseware/management/commands/check_course.py @@ -33,7 +33,7 @@ class Command(BaseCommand): ajax_url='', state=None, track_function = lambda x,y,z:None, - render_function = lambda x: {'content':'','destroy_js':'','init_js':'','type':'video'}) + render_function = lambda x: {'content':'','type':'video'}) except: print "==============> Error in ", etree.tostring(module) check = False diff --git a/djangoapps/courseware/module_render.py b/djangoapps/courseware/module_render.py index 278d19fd2c..2463e31d9b 100644 --- a/djangoapps/courseware/module_render.py +++ b/djangoapps/courseware/module_render.py @@ -130,8 +130,6 @@ def render_x_module(user, request, xml_module, module_object_preload): # Grab content content = instance.get_html() - init_js = instance.get_init_js() - destory_js = instance.get_destroy_js() # special extra information about each problem, only for users who are staff if user.is_staff: @@ -140,13 +138,8 @@ def render_x_module(user, request, xml_module, module_object_preload): content=content+render_to_string("staff_problem_info.html", {'xml':etree.tostring(xml_module), 'module_id' : module_id, 'render_histogram' : render_histogram}) - if render_histogram: - init_js = init_js+render_to_string("staff_problem_histogram.js", {'histogram' : histogram, - 'module_id' : module_id}) - - content = {'content':content, - "destroy_js":destory_js, - 'init_js':init_js, + + content = {'content':content, 'type':module_type} return content diff --git a/djangoapps/courseware/modules/vertical_module.py b/djangoapps/courseware/modules/vertical_module.py index e57a58e33e..a834614e26 100644 --- a/djangoapps/courseware/modules/vertical_module.py +++ b/djangoapps/courseware/modules/vertical_module.py @@ -18,17 +18,8 @@ class Module(XModule): def get_html(self): return render_to_string('vert_module.html',{'items':self.contents}) - def get_init_js(self): - return self.init_js_text - - def get_destroy_js(self): - return self.destroy_js_text - - def __init__(self, system, xml, item_id, state=None): XModule.__init__(self, system, xml, item_id, state) xmltree=etree.fromstring(xml) self.contents=[(e.get("name"),self.render_function(e)) \ for e in xmltree] - self.init_js_text="".join([e[1]['init_js'] for e in self.contents if 'init_js' in e[1]]) - self.destroy_js_text="".join([e[1]['destroy_js'] for e in self.contents if 'destroy_js' in e[1]]) diff --git a/djangoapps/courseware/modules/x_module.py b/djangoapps/courseware/modules/x_module.py index b475fd0280..464e2f0a90 100644 --- a/djangoapps/courseware/modules/x_module.py +++ b/djangoapps/courseware/modules/x_module.py @@ -78,25 +78,6 @@ class XModule(object): ''' return "Unimplemented" - # TODO: - # def get_header_js(self): - # ''' Filename of common js that needs to be included in the header - # ''' - # raise NotImplementedError - - def get_init_js(self): - ''' JavaScript code to be run when problem is shown. Be aware - that this may happen several times on the same page - (e.g. student switching tabs). Common functions should be put - in the main course .js files for now. ''' - return "" - - def get_destroy_js(self): - ''' JavaScript called to destroy the problem (e.g. when a user switches to a different tab). - We make an attempt, but not a promise, to call this when the user closes the web page. - ''' - return "" - def handle_ajax(self, dispatch, get): ''' dispatch is last part of the URL. get is a dictionary-like object ''' From 2bc68c0f25876ee3bd0450a21485b17ea22e15f9 Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Mon, 14 May 2012 16:32:27 -0400 Subject: [PATCH 28/57] Add path prefix to all AJAX requests This will handle the case where the site was deployed under a path prefix. --- static/coffee/spec/calculator_spec.coffee | 2 +- static/coffee/spec/feedback_form_spec.coffee | 2 +- static/coffee/src/calculator.coffee | 2 +- static/coffee/src/courseware.coffee | 1 + static/coffee/src/feedback_form.coffee | 2 +- static/coffee/src/main.coffee | 10 ++++++++-- templates/coffee/src/logger.coffee | 4 ++-- templates/coffee/src/modules/problem.coffee | 8 ++++---- templates/coffee/src/modules/sequence.coffee | 2 +- .../coffee/src/modules/video/video_caption.coffee | 2 +- templates/main.html | 1 + templates/profile.html | 2 +- 12 files changed, 23 insertions(+), 15 deletions(-) diff --git a/static/coffee/spec/calculator_spec.coffee b/static/coffee/spec/calculator_spec.coffee index 5c3fde5e2d..a9ef69ed26 100644 --- a/static/coffee/spec/calculator_spec.coffee +++ b/static/coffee/spec/calculator_spec.coffee @@ -60,7 +60,7 @@ describe 'Calculator', -> @calculator.calculate() it 'send data to /calculate', -> - expect($.getJSON).toHaveBeenCalledWith '/calculate', + expect($.getWithPrefix).toHaveBeenCalledWith '/calculate', equation: '1+2' , jasmine.any(Function) diff --git a/static/coffee/spec/feedback_form_spec.coffee b/static/coffee/spec/feedback_form_spec.coffee index 41ba2ec04d..64fa5cba24 100644 --- a/static/coffee/spec/feedback_form_spec.coffee +++ b/static/coffee/spec/feedback_form_spec.coffee @@ -16,7 +16,7 @@ describe 'FeedbackForm', -> $('#feedback_message').val 'This site is really good.' $('#feedback_button').click() - expect($.post).toHaveBeenCalledWith '/send_feedback', { + expect($.postWithPrefix).toHaveBeenCalledWith '/send_feedback', { subject: 'Awesome!' message: 'This site is really good.' url: window.location.href diff --git a/static/coffee/src/calculator.coffee b/static/coffee/src/calculator.coffee index bca88503fd..2f9b8a1aa8 100644 --- a/static/coffee/src/calculator.coffee +++ b/static/coffee/src/calculator.coffee @@ -20,5 +20,5 @@ class @Calculator $('.help').toggleClass 'shown' calculate: -> - $.getJSON '/calculate', { equation: $('#calculator_input').val() }, (data) -> + $.getWithPrefix '/calculate', { equation: $('#calculator_input').val() }, (data) -> $('#calculator_output').val(data.result) diff --git a/static/coffee/src/courseware.coffee b/static/coffee/src/courseware.coffee index e2f015baef..b495e14bc2 100644 --- a/static/coffee/src/courseware.coffee +++ b/static/coffee/src/courseware.coffee @@ -1,5 +1,6 @@ class @Courseware constructor: -> + Courseware.prefix = $("meta[name='path_prefix']").attr('content') new Navigation new Calculator new FeedbackForm diff --git a/static/coffee/src/feedback_form.coffee b/static/coffee/src/feedback_form.coffee index ffb8b37521..3394db448f 100644 --- a/static/coffee/src/feedback_form.coffee +++ b/static/coffee/src/feedback_form.coffee @@ -5,6 +5,6 @@ class @FeedbackForm subject: $('#feedback_subject').val() message: $('#feedback_message').val() url: window.location.href - $.post '/send_feedback', data, -> + $.postWithPrefix '/send_feedback', data, -> $('#feedback_div').html 'Feedback submitted. Thank you' ,'json' diff --git a/static/coffee/src/main.coffee b/static/coffee/src/main.coffee index 85c98ff929..733322b9bb 100644 --- a/static/coffee/src/main.coffee +++ b/static/coffee/src/main.coffee @@ -1,3 +1,9 @@ +jQuery.postWithPrefix = (url, data, callback, type) -> + $.post("#{Courseware.prefix}#{url}", data, callback, type) + +jQuery.getWithPrefix = (url, data, callback, type) -> + $.get("#{Courseware.prefix}#{url}", data, callback, type) + $ -> $.ajaxSetup headers : { 'X-CSRFToken': $.cookie 'csrftoken' } @@ -18,8 +24,8 @@ $ -> element.schematic.update_value() schematic_value $("#schematic_#{circuit_id}").attr("value") - $.post "/save_circuit/#{circuit_id}", schematic: schematic_value, (data) -> + $.postWithPrefix "/save_circuit/#{circuit_id}", schematic: schematic_value, (data) -> alert('Saved') if data.results == 'success' window.postJSON = (url, data, callback) -> - $.post url, data, callback + $.postWithPrefix url, data, callback diff --git a/templates/coffee/src/logger.coffee b/templates/coffee/src/logger.coffee index 729cb54982..97d6deaacc 100644 --- a/templates/coffee/src/logger.coffee +++ b/templates/coffee/src/logger.coffee @@ -1,6 +1,6 @@ class @Logger @log: (event_type, data) -> - $.getJSON '/event', + $.getWithPrefix '/event', event_type: event_type event: JSON.stringify(data) page: window.location.href @@ -8,7 +8,7 @@ class @Logger @bind: -> window.onunload = -> $.ajax - url: '/event' + url: "#{Courseware.prefix}/event" data: event_type: 'page_close' event: '' diff --git a/templates/coffee/src/modules/problem.coffee b/templates/coffee/src/modules/problem.coffee index add045266c..b2d4d0f5ed 100644 --- a/templates/coffee/src/modules/problem.coffee +++ b/templates/coffee/src/modules/problem.coffee @@ -25,7 +25,7 @@ class @Problem check: => Logger.log 'problem_check', @answers - $.post "/modx/problem/#{@id}/problem_check", @answers, (response) => + $.postWithPrefix "/modx/problem/#{@id}/problem_check", @answers, (response) => switch response.success when 'incorrect', 'correct' @render(response.contents) @@ -34,18 +34,18 @@ class @Problem reset: => Logger.log 'problem_reset', @answers - $.post "/modx/problem/#{@id}/problem_reset", id: @id, (content) => + $.postWithPrefix "/modx/problem/#{@id}/problem_reset", id: @id, (content) => @render(content) show: => Logger.log 'problem_show', problem: @id - $.post "/modx/problem/#{@id}/problem_show", (response) => + $.postWithPrefix "/modx/problem/#{@id}/problem_show", (response) => $.each response, (key, value) => @$("#answer_#{key}").text(value) save: => Logger.log 'problem_save', @answers - $.post "/modx/problem/#{@id}/problem_save", @answers, (response) => + $.postWithPrefix "/modx/problem/#{@id}/problem_save", @answers, (response) => if response.success alert 'Saved' diff --git a/templates/coffee/src/modules/sequence.coffee b/templates/coffee/src/modules/sequence.coffee index 9c1549ff78..4fb6225bbc 100644 --- a/templates/coffee/src/modules/sequence.coffee +++ b/templates/coffee/src/modules/sequence.coffee @@ -36,7 +36,7 @@ class @Sequence render: (new_position) -> if @position != undefined @mark_visited @position - $.post "/modx/#{@tag}/#{@id}/goto_position", position: new_position + $.postWithPrefix "/modx/#{@tag}/#{@id}/goto_position", position: new_position if @position != new_position @mark_active new_position diff --git a/templates/coffee/src/modules/video/video_caption.coffee b/templates/coffee/src/modules/video/video_caption.coffee index 9115274222..97122e1704 100644 --- a/templates/coffee/src/modules/video/video_caption.coffee +++ b/templates/coffee/src/modules/video/video_caption.coffee @@ -45,7 +45,7 @@ class VideoCaption .append($('
    16. ').height(@bottomSpacingHeight())) fetchCaption: -> - $.getJSON @captionURL(), (captions) => + $.getWithPrefix @captionURL(), (captions) => @captions = captions.text @start = captions.start for index in [0...captions.start.length] diff --git a/templates/main.html b/templates/main.html index 00abe2902d..33ca0b85eb 100644 --- a/templates/main.html +++ b/templates/main.html @@ -28,6 +28,7 @@ It can't be run through static.url because MathJax uses crazy url introspection to do lazy loading of MathJax extension libraries --> + diff --git a/templates/profile.html b/templates/profile.html index 98d5b38c7b..e732616d5a 100644 --- a/templates/profile.html +++ b/templates/profile.html @@ -75,7 +75,7 @@ $(function() { }); $('#pwd_reset_button').click(function() { - $.post('/password_reset/',{ "csrfmiddlewaretoken" : "${ csrf }", + $.postWithPrefix('/password_reset/',{ "csrfmiddlewaretoken" : "${ csrf }", "email" : $('#id_email').val()}, function(data){ $("#password_reset_complete_link").click(); log_event("profile", {"type":"password_send"}); From 5eac6ffa1df39eb938f2989e57a519a9a819b800 Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Mon, 14 May 2012 16:36:48 -0400 Subject: [PATCH 29/57] Refactor Problem from changes upstream --- templates/coffee/src/modules/problem.coffee | 24 +++++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/templates/coffee/src/modules/problem.coffee b/templates/coffee/src/modules/problem.coffee index b2d4d0f5ed..2bba68224e 100644 --- a/templates/coffee/src/modules/problem.coffee +++ b/templates/coffee/src/modules/problem.coffee @@ -38,10 +38,20 @@ class @Problem @render(content) show: => - Logger.log 'problem_show', problem: @id - $.postWithPrefix "/modx/problem/#{@id}/problem_show", (response) => - $.each response, (key, value) => - @$("#answer_#{key}").text(value) + if !@element.hasClass 'showed' + Logger.log 'problem_show', problem: @id + $.postWithPrefix "/modx/problem/#{@id}/problem_show", (response) => + $.each response, (key, value) => + if $.isArray(data[key]) + $.each data[key], (index, answer_index) => + @$("#label[for='input_#{key}_#{data[key][answer_index]}']").attr + correct_answer: 'true' + @$("#answer_#{key}").text(value) + @element.addClass 'showed' + else + @$('[id^=answer_]').text('') + @$('[correct_answer]').attr(correct_answer: null) + save: => Logger.log 'problem_save', @answers @@ -50,8 +60,8 @@ class @Problem alert 'Saved' refreshAnswers: => - @answers = {} @$('input.schematic').each (index, element) -> element.schematic.update_value() - $.each @$("[id^=input_#{@id}_]"), (index, input) => - @answers[$(input).attr('id')] = $(input).val() + @$(".CodeMirror").each (index, element) -> + element.CodeMirror.save() if element.CodeMirror.save + @answers = @$("[id^=input_#{@id}_]").serialize() From fc4d4c8295c9bc0313b7a4089627d41179aa174c Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Mon, 14 May 2012 16:58:10 -0400 Subject: [PATCH 30/57] Fix section convention and code highlight --- static/js/CodeMirror/python.js | 341 +++++++++++++++++++++++++++++++++ templates/jstextline.html | 20 +- templates/textbox.html | 34 ++-- templates/textinput.html | 14 +- 4 files changed, 377 insertions(+), 32 deletions(-) create mode 100644 static/js/CodeMirror/python.js diff --git a/static/js/CodeMirror/python.js b/static/js/CodeMirror/python.js new file mode 100644 index 0000000000..cb59e5cbc4 --- /dev/null +++ b/static/js/CodeMirror/python.js @@ -0,0 +1,341 @@ +CodeMirror.defineMode("python", function(conf, parserConf) { + var ERRORCLASS = 'error'; + + function wordRegexp(words) { + return new RegExp("^((" + words.join(")|(") + "))\\b"); + } + + var singleOperators = new RegExp("^[\\+\\-\\*/%&|\\^~<>!]"); + var singleDelimiters = new RegExp('^[\\(\\)\\[\\]\\{\\}@,:`=;\\.]'); + var doubleOperators = new RegExp("^((==)|(!=)|(<=)|(>=)|(<>)|(<<)|(>>)|(//)|(\\*\\*))"); + var doubleDelimiters = new RegExp("^((\\+=)|(\\-=)|(\\*=)|(%=)|(/=)|(&=)|(\\|=)|(\\^=))"); + var tripleDelimiters = new RegExp("^((//=)|(>>=)|(<<=)|(\\*\\*=))"); + var identifiers = new RegExp("^[_A-Za-z][_A-Za-z0-9]*"); + + var wordOperators = wordRegexp(['and', 'or', 'not', 'is', 'in']); + var commonkeywords = ['as', 'assert', 'break', 'class', 'continue', + 'def', 'del', 'elif', 'else', 'except', 'finally', + 'for', 'from', 'global', 'if', 'import', + 'lambda', 'pass', 'raise', 'return', + 'try', 'while', 'with', 'yield']; + var commonBuiltins = ['abs', 'all', 'any', 'bin', 'bool', 'bytearray', 'callable', 'chr', + 'classmethod', 'compile', 'complex', 'delattr', 'dict', 'dir', 'divmod', + 'enumerate', 'eval', 'filter', 'float', 'format', 'frozenset', + 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', + 'input', 'int', 'isinstance', 'issubclass', 'iter', 'len', + 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', + 'object', 'oct', 'open', 'ord', 'pow', 'property', 'range', + 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', + 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', + 'type', 'vars', 'zip', '__import__', 'NotImplemented', + 'Ellipsis', '__debug__']; + var py2 = {'builtins': ['apply', 'basestring', 'buffer', 'cmp', 'coerce', 'execfile', + 'file', 'intern', 'long', 'raw_input', 'reduce', 'reload', + 'unichr', 'unicode', 'xrange', 'False', 'True', 'None'], + 'keywords': ['exec', 'print']}; + var py3 = {'builtins': ['ascii', 'bytes', 'exec', 'print'], + 'keywords': ['nonlocal', 'False', 'True', 'None']}; + + if (!!parserConf.version && parseInt(parserConf.version, 10) === 3) { + commonkeywords = commonkeywords.concat(py3.keywords); + commonBuiltins = commonBuiltins.concat(py3.builtins); + var stringPrefixes = new RegExp("^(([rb]|(br))?('{3}|\"{3}|['\"]))", "i"); + } else { + commonkeywords = commonkeywords.concat(py2.keywords); + commonBuiltins = commonBuiltins.concat(py2.builtins); + var stringPrefixes = new RegExp("^(([rub]|(ur)|(br))?('{3}|\"{3}|['\"]))", "i"); + } + var keywords = wordRegexp(commonkeywords); + var builtins = wordRegexp(commonBuiltins); + + var indentInfo = null; + + // tokenizers + function tokenBase(stream, state) { + // Handle scope changes + if (stream.sol()) { + var scopeOffset = state.scopes[0].offset; + if (stream.eatSpace()) { + var lineOffset = stream.indentation(); + if (lineOffset > scopeOffset) { + indentInfo = 'indent'; + } else if (lineOffset < scopeOffset) { + indentInfo = 'dedent'; + } + return null; + } else { + if (scopeOffset > 0) { + dedent(stream, state); + } + } + } + if (stream.eatSpace()) { + return null; + } + + var ch = stream.peek(); + + // Handle Comments + if (ch === '#') { + stream.skipToEnd(); + return 'comment'; + } + + // Handle Number Literals + if (stream.match(/^[0-9\.]/, false)) { + var floatLiteral = false; + // Floats + if (stream.match(/^\d*\.\d+(e[\+\-]?\d+)?/i)) { floatLiteral = true; } + if (stream.match(/^\d+\.\d*/)) { floatLiteral = true; } + if (stream.match(/^\.\d+/)) { floatLiteral = true; } + if (floatLiteral) { + // Float literals may be "imaginary" + stream.eat(/J/i); + return 'number'; + } + // Integers + var intLiteral = false; + // Hex + if (stream.match(/^0x[0-9a-f]+/i)) { intLiteral = true; } + // Binary + if (stream.match(/^0b[01]+/i)) { intLiteral = true; } + // Octal + if (stream.match(/^0o[0-7]+/i)) { intLiteral = true; } + // Decimal + if (stream.match(/^[1-9]\d*(e[\+\-]?\d+)?/)) { + // Decimal literals may be "imaginary" + stream.eat(/J/i); + // TODO - Can you have imaginary longs? + intLiteral = true; + } + // Zero by itself with no other piece of number. + if (stream.match(/^0(?![\dx])/i)) { intLiteral = true; } + if (intLiteral) { + // Integer literals may be "long" + stream.eat(/L/i); + return 'number'; + } + } + + // Handle Strings + if (stream.match(stringPrefixes)) { + state.tokenize = tokenStringFactory(stream.current()); + return state.tokenize(stream, state); + } + + // Handle operators and Delimiters + if (stream.match(tripleDelimiters) || stream.match(doubleDelimiters)) { + return null; + } + if (stream.match(doubleOperators) + || stream.match(singleOperators) + || stream.match(wordOperators)) { + return 'operator'; + } + if (stream.match(singleDelimiters)) { + return null; + } + + if (stream.match(keywords)) { + return 'keyword'; + } + + if (stream.match(builtins)) { + return 'builtin'; + } + + if (stream.match(identifiers)) { + return 'variable'; + } + + // Handle non-detected items + stream.next(); + return ERRORCLASS; + } + + function tokenStringFactory(delimiter) { + while ('rub'.indexOf(delimiter.charAt(0).toLowerCase()) >= 0) { + delimiter = delimiter.substr(1); + } + var singleline = delimiter.length == 1; + var OUTCLASS = 'string'; + + return function tokenString(stream, state) { + while (!stream.eol()) { + stream.eatWhile(/[^'"\\]/); + if (stream.eat('\\')) { + stream.next(); + if (singleline && stream.eol()) { + return OUTCLASS; + } + } else if (stream.match(delimiter)) { + state.tokenize = tokenBase; + return OUTCLASS; + } else { + stream.eat(/['"]/); + } + } + if (singleline) { + if (parserConf.singleLineStringErrors) { + return ERRORCLASS; + } else { + state.tokenize = tokenBase; + } + } + return OUTCLASS; + }; + } + + function indent(stream, state, type) { + type = type || 'py'; + var indentUnit = 0; + if (type === 'py') { + if (state.scopes[0].type !== 'py') { + state.scopes[0].offset = stream.indentation(); + return; + } + for (var i = 0; i < state.scopes.length; ++i) { + if (state.scopes[i].type === 'py') { + indentUnit = state.scopes[i].offset + conf.indentUnit; + break; + } + } + } else { + indentUnit = stream.column() + stream.current().length; + } + state.scopes.unshift({ + offset: indentUnit, + type: type + }); + } + + function dedent(stream, state, type) { + type = type || 'py'; + if (state.scopes.length == 1) return; + if (state.scopes[0].type === 'py') { + var _indent = stream.indentation(); + var _indent_index = -1; + for (var i = 0; i < state.scopes.length; ++i) { + if (_indent === state.scopes[i].offset) { + _indent_index = i; + break; + } + } + if (_indent_index === -1) { + return true; + } + while (state.scopes[0].offset !== _indent) { + state.scopes.shift(); + } + return false + } else { + if (type === 'py') { + state.scopes[0].offset = stream.indentation(); + return false; + } else { + if (state.scopes[0].type != type) { + return true; + } + state.scopes.shift(); + return false; + } + } + } + + function tokenLexer(stream, state) { + indentInfo = null; + var style = state.tokenize(stream, state); + var current = stream.current(); + + // Handle '.' connected identifiers + if (current === '.') { + style = state.tokenize(stream, state); + current = stream.current(); + if (style === 'variable' || style === 'builtin') { + return 'variable'; + } else { + return ERRORCLASS; + } + } + + // Handle decorators + if (current === '@') { + style = state.tokenize(stream, state); + current = stream.current(); + if (style === 'variable' + || current === '@staticmethod' + || current === '@classmethod') { + return 'meta'; + } else { + return ERRORCLASS; + } + } + + // Handle scope changes. + if (current === 'pass' || current === 'return') { + state.dedent += 1; + } + if (current === 'lambda') state.lambda = true; + if ((current === ':' && !state.lambda && state.scopes[0].type == 'py') + || indentInfo === 'indent') { + indent(stream, state); + } + var delimiter_index = '[({'.indexOf(current); + if (delimiter_index !== -1) { + indent(stream, state, '])}'.slice(delimiter_index, delimiter_index+1)); + } + if (indentInfo === 'dedent') { + if (dedent(stream, state)) { + return ERRORCLASS; + } + } + delimiter_index = '])}'.indexOf(current); + if (delimiter_index !== -1) { + if (dedent(stream, state, current)) { + return ERRORCLASS; + } + } + if (state.dedent > 0 && stream.eol() && state.scopes[0].type == 'py') { + if (state.scopes.length > 1) state.scopes.shift(); + state.dedent -= 1; + } + + return style; + } + + var external = { + startState: function(basecolumn) { + return { + tokenize: tokenBase, + scopes: [{offset:basecolumn || 0, type:'py'}], + lastToken: null, + lambda: false, + dedent: 0 + }; + }, + + token: function(stream, state) { + var style = tokenLexer(stream, state); + + state.lastToken = {style:style, content: stream.current()}; + + if (stream.eol() && stream.lambda) { + state.lambda = false; + } + + return style; + }, + + indent: function(state, textAfter) { + if (state.tokenize != tokenBase) { + return 0; + } + + return state.scopes[0].offset; + } + + }; + return external; +}); + +CodeMirror.defineMIME("text/x-python", "python"); diff --git a/templates/jstextline.html b/templates/jstextline.html index a062252392..045f329ad4 100644 --- a/templates/jstextline.html +++ b/templates/jstextline.html @@ -1,12 +1,12 @@ -
      - +
      + % if dojs == 'math': `{::}` @@ -20,7 +20,7 @@ % if state == 'unsubmitted': - % elif state == 'correct': + % elif state == 'correct': % elif state == 'incorrect': diff --git a/templates/textbox.html b/templates/textbox.html index cbbab7babc..5f09dd8f11 100644 --- a/templates/textbox.html +++ b/templates/textbox.html @@ -1,11 +1,11 @@ -
      +
      % if state == 'unsubmitted': - % elif state == 'correct': + % elif state == 'correct': % elif state == 'incorrect': @@ -18,17 +18,21 @@ ${msg|n}
      -
      - - +
      + + + + + +
      diff --git a/templates/textinput.html b/templates/textinput.html index 7c330777f3..b30c515dc3 100644 --- a/templates/textinput.html +++ b/templates/textinput.html @@ -1,15 +1,15 @@ -
      - +
      + % if state == 'unsubmitted': - % elif state == 'correct': + % elif state == 'correct': % elif state == 'incorrect': From 4298c920b8506085ed9bfce85e27f9d7f61173dc Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Tue, 15 May 2012 11:25:05 -0400 Subject: [PATCH 31/57] Make show answer button toggleable --- templates/coffee/src/modules/problem.coffee | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/templates/coffee/src/modules/problem.coffee b/templates/coffee/src/modules/problem.coffee index 2bba68224e..21b63ff0d7 100644 --- a/templates/coffee/src/modules/problem.coffee +++ b/templates/coffee/src/modules/problem.coffee @@ -42,16 +42,18 @@ class @Problem Logger.log 'problem_show', problem: @id $.postWithPrefix "/modx/problem/#{@id}/problem_show", (response) => $.each response, (key, value) => - if $.isArray(data[key]) - $.each data[key], (index, answer_index) => - @$("#label[for='input_#{key}_#{data[key][answer_index]}']").attr + if $.isArray(value) + $.each value, (index, answer_index) => + @$("#label[for='input_#{key}_#{value[answer_index]}']").attr correct_answer: 'true' @$("#answer_#{key}").text(value) + @$('.show').val 'Hide Answer' @element.addClass 'showed' else @$('[id^=answer_]').text('') @$('[correct_answer]').attr(correct_answer: null) - + @element.removeClass 'showed' + @$('.show').val 'Show Answer' save: => Logger.log 'problem_save', @answers From 8a87a4ba8bc19c67634e9d25fde4808fa0bd21e0 Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Tue, 15 May 2012 13:57:51 -0400 Subject: [PATCH 32/57] Make staff problem histogram work again --- djangoapps/courseware/module_render.py | 2 ++ static/coffee/spec/calculator_spec.js | 2 +- static/coffee/spec/feedback_form_spec.js | 2 +- static/coffee/src/courseware.coffee | 4 +++ static/sass/courseware/_courseware.scss | 4 +++ templates/coffee/spec/navigation_spec.js | 1 - templates/coffee/src/histogram.coffee | 35 +++++++++++++++++++++ templates/staff_problem_histogram.js | 40 ------------------------ templates/staff_problem_info.html | 2 +- 9 files changed, 48 insertions(+), 44 deletions(-) create mode 100644 templates/coffee/src/histogram.coffee delete mode 100644 templates/staff_problem_histogram.js diff --git a/djangoapps/courseware/module_render.py b/djangoapps/courseware/module_render.py index 2463e31d9b..510d26033c 100644 --- a/djangoapps/courseware/module_render.py +++ b/djangoapps/courseware/module_render.py @@ -1,3 +1,4 @@ +import json import logging from lxml import etree @@ -137,6 +138,7 @@ def render_x_module(user, request, xml_module, module_object_preload): render_histogram = len(histogram) > 0 content=content+render_to_string("staff_problem_info.html", {'xml':etree.tostring(xml_module), 'module_id' : module_id, + 'histogram': json.dumps(histogram), 'render_histogram' : render_histogram}) content = {'content':content, diff --git a/static/coffee/spec/calculator_spec.js b/static/coffee/spec/calculator_spec.js index 9ea57f3aef..66033964de 100644 --- a/static/coffee/spec/calculator_spec.js +++ b/static/coffee/spec/calculator_spec.js @@ -66,7 +66,7 @@ return this.calculator.calculate(); }); it('send data to /calculate', function() { - return expect($.getJSON).toHaveBeenCalledWith('/calculate', { + return expect($.getWithPrefix).toHaveBeenCalledWith('/calculate', { equation: '1+2' }, jasmine.any(Function)); }); diff --git a/static/coffee/spec/feedback_form_spec.js b/static/coffee/spec/feedback_form_spec.js index 9824915d03..4a02b4d541 100644 --- a/static/coffee/spec/feedback_form_spec.js +++ b/static/coffee/spec/feedback_form_spec.js @@ -18,7 +18,7 @@ $('#feedback_subject').val('Awesome!'); $('#feedback_message').val('This site is really good.'); $('#feedback_button').click(); - return expect($.post).toHaveBeenCalledWith('/send_feedback', { + return expect($.postWithPrefix).toHaveBeenCalledWith('/send_feedback', { subject: 'Awesome!', message: 'This site is really good.', url: window.location.href diff --git a/static/coffee/src/courseware.coffee b/static/coffee/src/courseware.coffee index b495e14bc2..697aff8af4 100644 --- a/static/coffee/src/courseware.coffee +++ b/static/coffee/src/courseware.coffee @@ -22,3 +22,7 @@ class @Courseware $('.course-content .problems-wrapper').each -> id = $(this).attr('id').replace(/problem_/, '') new Problem id, $(this).data('url') + $('.course-content .histogram').each -> + id = $(this).attr('id').replace(/histogram_/, '') + new Histogram id, $(this).data('histogram') + diff --git a/static/sass/courseware/_courseware.scss b/static/sass/courseware/_courseware.scss index bd707e1b80..44d54492ea 100644 --- a/static/sass/courseware/_courseware.scss +++ b/static/sass/courseware/_courseware.scss @@ -202,6 +202,10 @@ div.course-wrapper { padding-bottom: 0; } + .histogram { + width: 200px; + height: 150px; + } ul { list-style: disc outside none; diff --git a/templates/coffee/spec/navigation_spec.js b/templates/coffee/spec/navigation_spec.js index 3ba6508eec..cb51aacbf8 100644 --- a/templates/coffee/spec/navigation_spec.js +++ b/templates/coffee/spec/navigation_spec.js @@ -1,4 +1,3 @@ -// Generated by CoffeeScript 1.3.2-pre (function() { describe('Navigation', function() { diff --git a/templates/coffee/src/histogram.coffee b/templates/coffee/src/histogram.coffee new file mode 100644 index 0000000000..0ef2d64764 --- /dev/null +++ b/templates/coffee/src/histogram.coffee @@ -0,0 +1,35 @@ +class @Histogram + constructor: (@id, @rawData) -> + @xTicks = [] + @yTicks = [] + @data = [] + @calculate() + @render() + + calculate: -> + for [score, count] in @rawData + log_count = Math.log(count + 1) + @data.push [score, log_count] + @xTicks.push [score, score.toString()] + @yTicks.push [log_count, count.toString()] + + render: -> + $.plot $("#histogram_#{@id}"), [ + data: @data + bars: + show: true + align: 'center' + lineWidth: 0 + fill: 1.0 + color: "#b72121" + ], + xaxis: + min: -1 + max: Math.max $.map(@xTicks, (data) -> data[0] + 1) + ticks: @xTicks + tickLength: 0 + yaxis: + min: 0.0 + max: Math.max $.map(@yTicks, (data) -> data[0] * 1.1) + ticks: @yTicks + labelWidth: 50 diff --git a/templates/staff_problem_histogram.js b/templates/staff_problem_histogram.js deleted file mode 100644 index 77ba59d490..0000000000 --- a/templates/staff_problem_histogram.js +++ /dev/null @@ -1,40 +0,0 @@ -<%! - import json - import math -%> - - -var rawData = ${json.dumps(histogram)}; - -var maxx = 1; -var maxy = 1.5; -var xticks = Array(); -var yticks = Array(); -var data = Array(); -for (var i = 0; i < rawData.length; i++) { - var score = rawData[i][0]; - var count = rawData[i][1]; - var log_count = Math.log(count + 1); - - data.push( [score, log_count] ); - - xticks.push( [score, score.toString()] ); - yticks.push( [log_count, count.toString()] ); - - maxx = Math.max( score + 1, maxx ); - maxy = Math.max(log_count*1.1, maxy ); -} - -$.plot($("#histogram_${module_id}"), [{ - data: data, - bars: { show: true, - align: 'center', - lineWidth: 0, - fill: 1.0 }, - color: "#b72121", - }], - { - xaxis: {min: -1, max: maxx, ticks: xticks, tickLength: 0}, - yaxis: {min: 0.0, max: maxy, ticks: yticks, labelWidth: 50}, - } -); diff --git a/templates/staff_problem_info.html b/templates/staff_problem_info.html index 20370a9c81..24450c797a 100644 --- a/templates/staff_problem_info.html +++ b/templates/staff_problem_info.html @@ -2,5 +2,5 @@ ${xml | h}
    17. %if render_histogram: -
      +
      %endif From b1daa59cea9fec4e26957a7609464123883008d0 Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Tue, 15 May 2012 15:59:59 -0400 Subject: [PATCH 33/57] Refactor video caption to use binary search --- .../src/modules/video/video_caption.coffee | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/templates/coffee/src/modules/video/video_caption.coffee b/templates/coffee/src/modules/video/video_caption.coffee index 97122e1704..2e807fb93c 100644 --- a/templates/coffee/src/modules/video/video_caption.coffee +++ b/templates/coffee/src/modules/video/video_caption.coffee @@ -1,6 +1,5 @@ class VideoCaption constructor: (@player, @youtubeId) -> - @index = [] @render() @bind() @@ -29,6 +28,12 @@ class VideoCaption @$('.subtitles').css maxHeight: @$('.video-wrapper').height() - 5 @fetchCaption() + fetchCaption: -> + $.getWithPrefix @captionURL(), (captions) => + @captions = captions.text + @start = captions.start + @renderCaption() + renderCaption: -> container = $('
        ') @@ -44,28 +49,28 @@ class VideoCaption @$('.subtitles').prepend($('
      1. ').height(@topSpacingHeight())) .append($('
      2. ').height(@bottomSpacingHeight())) - fetchCaption: -> - $.getWithPrefix @captionURL(), (captions) => - @captions = captions.text - @start = captions.start - for index in [0...captions.start.length] - for time in [captions.start[index]..captions.end[index]] - @index[time] ||= [] - @index[time].push(index) - @renderCaption() + search: (time) -> + 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 onUpdatePlayTime: (event, time) => # This 250ms offset is required to match the video speed time = Math.round(Time.convert(time, @player.currentSpeed(), '1.0') * 1000 + 250) - newIndex = @index[time] + newIndex = @search time if newIndex != undefined && @currentIndex != newIndex if @currentIndex - for index in @currentIndex - @$(".subtitles li[data-index='#{index}']").removeClass('current') - - for index in newIndex - @$(".subtitles li[data-index='#{newIndex}']").addClass('current') + @$(".subtitles li.current").removeClass('current') + @$(".subtitles li[data-index='#{newIndex}']").addClass('current') @currentIndex = newIndex @scrollCaption() From 60094a1772c8934658f536a08e6f3a5d596aa1bb Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Thu, 17 May 2012 12:33:39 -0400 Subject: [PATCH 34/57] Add test coverage for JavaScript --- static/coffee/files.json | 2 +- static/coffee/spec/calculator_spec.coffee | 3 +- static/coffee/spec/calculator_spec.js | 3 +- static/coffee/spec/courseware_spec.coffee | 18 +- static/coffee/spec/courseware_spec.js | 17 +- static/coffee/spec/feedback_form_spec.coffee | 2 +- static/coffee/spec/feedback_form_spec.js | 2 +- static/coffee/spec/helper.coffee | 75 +++ static/coffee/spec/helper.js | 86 ++++ static/coffee/src/courseware.coffee | 3 +- templates/coffee/fixtures/items.json | 15 + templates/coffee/fixtures/problem.html | 1 + .../coffee/fixtures/problem_content.html | 16 + templates/coffee/fixtures/sequence.html | 20 + templates/coffee/fixtures/tab.html | 3 + templates/coffee/fixtures/video.html | 12 + templates/coffee/spec/histogram_spec.coffee | 46 ++ templates/coffee/spec/histogram_spec.js | 62 +++ templates/coffee/spec/logger_spec.coffee | 35 ++ templates/coffee/spec/logger_spec.js | 46 ++ .../coffee/spec/modules/problem_spec.coffee | 250 ++++++++++ templates/coffee/spec/modules/problem_spec.js | 301 ++++++++++++ .../coffee/spec/modules/sequence_spec.coffee | 156 +++++++ .../coffee/spec/modules/sequence_spec.js | 199 ++++++++ templates/coffee/spec/modules/tab_spec.coffee | 39 ++ templates/coffee/spec/modules/tab_spec.js | 55 +++ .../modules/video/video_caption_spec.coffee | 294 ++++++++++++ .../spec/modules/video/video_caption_spec.js | 317 +++++++++++++ .../modules/video/video_control_spec.coffee | 73 +++ .../spec/modules/video/video_control_spec.js | 72 +++ .../modules/video/video_player_spec.coffee | 381 +++++++++++++++ .../spec/modules/video/video_player_spec.js | 441 ++++++++++++++++++ .../video/video_progress_slider_spec.coffee | 122 +++++ .../video/video_progress_slider_spec.js | 154 ++++++ .../video/video_speed_control_spec.coffee | 95 ++++ .../modules/video/video_speed_control_spec.js | 103 ++++ .../coffee/spec/modules/video_spec.coffee | 121 +++++ templates/coffee/spec/modules/video_spec.js | 148 ++++++ templates/coffee/spec/time_spec.coffee | 18 + templates/coffee/spec/time_spec.js | 28 ++ templates/coffee/src/histogram.coffee | 4 +- templates/coffee/src/logger.coffee | 1 - templates/coffee/src/modules/problem.coffee | 13 +- templates/coffee/src/modules/sequence.coffee | 20 +- .../modules/{tab_module.coffee => tab.coffee} | 0 templates/coffee/src/modules/video.coffee | 4 +- .../src/modules/video/video_caption.coffee | 11 +- .../src/modules/video/video_control.coffee | 8 +- .../src/modules/video/video_player.coffee | 24 +- .../video/video_progress_slider.coffee | 2 +- .../modules/video/video_speed_control.coffee | 6 +- templates/coffee/src/time.coffee | 12 +- templates/jasmine/base.html | 68 +++ 53 files changed, 3934 insertions(+), 73 deletions(-) create mode 100644 templates/coffee/fixtures/items.json create mode 100644 templates/coffee/fixtures/problem.html create mode 100644 templates/coffee/fixtures/problem_content.html create mode 100644 templates/coffee/fixtures/sequence.html create mode 100644 templates/coffee/fixtures/tab.html create mode 100644 templates/coffee/fixtures/video.html create mode 100644 templates/coffee/spec/histogram_spec.coffee create mode 100644 templates/coffee/spec/histogram_spec.js create mode 100644 templates/coffee/spec/logger_spec.coffee create mode 100644 templates/coffee/spec/logger_spec.js create mode 100644 templates/coffee/spec/modules/problem_spec.coffee create mode 100644 templates/coffee/spec/modules/problem_spec.js create mode 100644 templates/coffee/spec/modules/sequence_spec.coffee create mode 100644 templates/coffee/spec/modules/sequence_spec.js create mode 100644 templates/coffee/spec/modules/tab_spec.coffee create mode 100644 templates/coffee/spec/modules/tab_spec.js create mode 100644 templates/coffee/spec/modules/video/video_caption_spec.coffee create mode 100644 templates/coffee/spec/modules/video/video_caption_spec.js create mode 100644 templates/coffee/spec/modules/video/video_control_spec.coffee create mode 100644 templates/coffee/spec/modules/video/video_control_spec.js create mode 100644 templates/coffee/spec/modules/video/video_player_spec.coffee create mode 100644 templates/coffee/spec/modules/video/video_player_spec.js create mode 100644 templates/coffee/spec/modules/video/video_progress_slider_spec.coffee create mode 100644 templates/coffee/spec/modules/video/video_progress_slider_spec.js create mode 100644 templates/coffee/spec/modules/video/video_speed_control_spec.coffee create mode 100644 templates/coffee/spec/modules/video/video_speed_control_spec.js create mode 100644 templates/coffee/spec/modules/video_spec.coffee create mode 100644 templates/coffee/spec/modules/video_spec.js create mode 100644 templates/coffee/spec/time_spec.coffee create mode 100644 templates/coffee/spec/time_spec.js rename templates/coffee/src/modules/{tab_module.coffee => tab.coffee} (100%) create mode 100644 templates/jasmine/base.html diff --git a/static/coffee/files.json b/static/coffee/files.json index 67bf671f6a..1e5e010d73 100644 --- a/static/coffee/files.json +++ b/static/coffee/files.json @@ -3,7 +3,7 @@ "/static/js/jquery-1.6.2.min.js", "/static/js/jquery-ui-1.8.16.custom.min.js", "/static/js/jquery.leanModal.js", - "/static/js/jquery.cookie.js" + "/static/js/flot/jquery.flot.js" ], "static_files": [ "js/application.js" diff --git a/static/coffee/spec/calculator_spec.coffee b/static/coffee/spec/calculator_spec.coffee index a9ef69ed26..58d7c70790 100644 --- a/static/coffee/spec/calculator_spec.coffee +++ b/static/coffee/spec/calculator_spec.coffee @@ -24,6 +24,7 @@ describe 'Calculator', -> expect($('form#calculator')).toHandleWith 'submit', @calculator.calculate it 'prevent default behavior on form submit', -> + jasmine.stubRequests() $('form#calculator').submit (e) -> expect(e.isDefaultPrevented()).toBeTruthy() e.preventDefault() @@ -55,7 +56,7 @@ describe 'Calculator', -> describe 'calculate', -> beforeEach -> $('#calculator_input').val '1+2' - spyOn($, 'getJSON').andCallFake (url, data, callback) -> + spyOn($, 'getWithPrefix').andCallFake (url, data, callback) -> callback({ result: 3 }) @calculator.calculate() diff --git a/static/coffee/spec/calculator_spec.js b/static/coffee/spec/calculator_spec.js index 66033964de..4bc54700a5 100644 --- a/static/coffee/spec/calculator_spec.js +++ b/static/coffee/spec/calculator_spec.js @@ -26,6 +26,7 @@ return expect($('form#calculator')).toHandleWith('submit', this.calculator.calculate); }); return it('prevent default behavior on form submit', function() { + jasmine.stubRequests(); $('form#calculator').submit(function(e) { expect(e.isDefaultPrevented()).toBeTruthy(); return e.preventDefault(); @@ -58,7 +59,7 @@ return describe('calculate', function() { beforeEach(function() { $('#calculator_input').val('1+2'); - spyOn($, 'getJSON').andCallFake(function(url, data, callback) { + spyOn($, 'getWithPrefix').andCallFake(function(url, data, callback) { return callback({ result: 3 }); diff --git a/static/coffee/spec/courseware_spec.coffee b/static/coffee/spec/courseware_spec.coffee index 91b9187668..9d938c14e1 100644 --- a/static/coffee/spec/courseware_spec.coffee +++ b/static/coffee/spec/courseware_spec.coffee @@ -35,16 +35,28 @@ describe 'Courseware', -> describe 'render', -> beforeEach -> + jasmine.stubRequests() @courseware = new Courseware + spyOn(window, 'Histogram') + spyOn(window, 'Problem') + spyOn(window, 'Video') setFixtures """
        +
        +
        +
        """ - - it 'detect the video element and convert them', -> - spyOn(window, 'Video') @courseware.render() + + it 'detect the video elements and convert them', -> expect(window.Video).toHaveBeenCalledWith('1', '1.0:abc1234') expect(window.Video).toHaveBeenCalledWith('2', '1.0:def5678') + + it 'detect the problem element and convert it', -> + expect(window.Problem).toHaveBeenCalledWith('3', '/example/url/') + + it 'detect the histrogram element and convert it', -> + expect(window.Histogram).toHaveBeenCalledWith('3', [[0, 1]]) diff --git a/static/coffee/spec/courseware_spec.js b/static/coffee/spec/courseware_spec.js index adb04adc49..757943118c 100644 --- a/static/coffee/spec/courseware_spec.js +++ b/static/coffee/spec/courseware_spec.js @@ -35,15 +35,24 @@ }); return describe('render', function() { beforeEach(function() { + jasmine.stubRequests(); this.courseware = new Courseware; - return setFixtures("
        \n
        \n
        \n
        "); - }); - return it('detect the video element and convert them', function() { + spyOn(window, 'Histogram'); + spyOn(window, 'Problem'); spyOn(window, 'Video'); - this.courseware.render(); + setFixtures("
        \n
        \n
        \n
        \n
        \n
        \n
        "); + return this.courseware.render(); + }); + it('detect the video elements and convert them', function() { expect(window.Video).toHaveBeenCalledWith('1', '1.0:abc1234'); return expect(window.Video).toHaveBeenCalledWith('2', '1.0:def5678'); }); + it('detect the problem element and convert it', function() { + return expect(window.Problem).toHaveBeenCalledWith('3', '/example/url/'); + }); + return it('detect the histrogram element and convert it', function() { + return expect(window.Histogram).toHaveBeenCalledWith('3', [[0, 1]]); + }); }); }); diff --git a/static/coffee/spec/feedback_form_spec.coffee b/static/coffee/spec/feedback_form_spec.coffee index 64fa5cba24..ce4195faab 100644 --- a/static/coffee/spec/feedback_form_spec.coffee +++ b/static/coffee/spec/feedback_form_spec.coffee @@ -5,7 +5,7 @@ describe 'FeedbackForm', -> describe 'constructor', -> beforeEach -> new FeedbackForm - spyOn($, 'post').andCallFake (url, data, callback, format) -> + spyOn($, 'postWithPrefix').andCallFake (url, data, callback, format) -> callback() it 'binds to the #feedback_button', -> diff --git a/static/coffee/spec/feedback_form_spec.js b/static/coffee/spec/feedback_form_spec.js index 4a02b4d541..eb5546baa1 100644 --- a/static/coffee/spec/feedback_form_spec.js +++ b/static/coffee/spec/feedback_form_spec.js @@ -7,7 +7,7 @@ return describe('constructor', function() { beforeEach(function() { new FeedbackForm; - return spyOn($, 'post').andCallFake(function(url, data, callback, format) { + return spyOn($, 'postWithPrefix').andCallFake(function(url, data, callback, format) { return callback(); }); }); diff --git a/static/coffee/spec/helper.coffee b/static/coffee/spec/helper.coffee index 1f27e257c2..c81cb83405 100644 --- a/static/coffee/spec/helper.coffee +++ b/static/coffee/spec/helper.coffee @@ -1 +1,76 @@ jasmine.getFixtures().fixturesPath = "/_jasmine/fixtures/" + +jasmine.stubbedMetadata = + abc123: + id: 'abc123' + duration: 100 + def456: + id: 'def456' + duration: 200 + bogus: + duration: 300 + +jasmine.stubbedCaption = + start: [0, 10000, 20000, 30000, 40000, 50000, 60000, 70000, 80000, 90000, + 100000, 110000, 120000] + text: ['Caption at 0', 'Caption at 10000', 'Caption at 20000', + 'Caption at 30000', 'Caption at 40000', 'Caption at 50000', 'Caption at 60000', + 'Caption at 70000', 'Caption at 80000', 'Caption at 90000', 'Caption at 100000', + 'Caption at 110000', 'Caption at 120000'] + +jasmine.stubRequests = -> + spyOn($, 'ajax').andCallFake (settings) -> + if match = settings.url.match /youtube\.com\/.+\/videos\/(.+)\?v=2&alt=jsonc/ + settings.success data: jasmine.stubbedMetadata[match[1]] + else if match = settings.url.match /static\/subs\/(.+)\.srt\.sjson/ + settings.success jasmine.stubbedCaption + else if settings.url == '/calculate' || + settings.url == '/6002x/modx/sequential/1/goto_position' || + settings.url.match(/event$/) || + settings.url.match(/6002x\/modx\/problem\/.+\/problem_(check|reset|show|save)$/) + # do nothing + else + throw "External request attempted for #{settings.url}, which is not defined." + +jasmine.stubYoutubePlayer = -> + YT.Player = -> jasmine.createSpyObj 'YT.Player', ['cueVideoById', 'getVideoEmbedCode', + 'getCurrentTime', 'getPlayerState', 'loadVideoById', 'playVideo', 'pauseVideo', 'seekTo'] + +jasmine.stubVideoPlayer = (context, enableParts) -> + enableParts = [enableParts] unless $.isArray(enableParts) + + suite = context.suite + currentPartName = suite.description while suite = suite.parentSuite + enableParts.push currentPartName + + for part in ['VideoCaption', 'VideoSpeedControl', 'VideoProgressSlider'] + unless $.inArray(part, enableParts) >= 0 + spyOn window, part + + loadFixtures 'video.html' + jasmine.stubRequests() + YT.Player = undefined + context.video = new Video 'example', '.75:abc123,1.0:def456' + jasmine.stubYoutubePlayer() + return new VideoPlayer context.video + +spyOn(window, 'onunload') + +# Stub Youtube API +window.YT = + PlayerState: + UNSTARTED: -1 + ENDED: 0 + PLAYING: 1 + PAUSED: 2 + BUFFERING: 3 + CUED: 5 + +# Stub jQuery.cookie +$.cookie = jasmine.createSpy('jQuery.cookie').andReturn '1.0' + +# Stub jQuery.qtip +$.fn.qtip = jasmine.createSpy 'jQuery.qtip' + +# Stub jQuery.scrollTo +$.fn.scrollTo = jasmine.createSpy 'jQuery.scrollTo' diff --git a/static/coffee/spec/helper.js b/static/coffee/spec/helper.js index 3add5f2bf8..0abaa18090 100644 --- a/static/coffee/spec/helper.js +++ b/static/coffee/spec/helper.js @@ -2,4 +2,90 @@ jasmine.getFixtures().fixturesPath = "/_jasmine/fixtures/"; + jasmine.stubbedMetadata = { + abc123: { + id: 'abc123', + duration: 100 + }, + def456: { + id: 'def456', + duration: 200 + }, + bogus: { + duration: 300 + } + }; + + jasmine.stubbedCaption = { + start: [0, 10000, 20000, 30000, 40000, 50000, 60000, 70000, 80000, 90000, 100000, 110000, 120000], + text: ['Caption at 0', 'Caption at 10000', 'Caption at 20000', 'Caption at 30000', 'Caption at 40000', 'Caption at 50000', 'Caption at 60000', 'Caption at 70000', 'Caption at 80000', 'Caption at 90000', 'Caption at 100000', 'Caption at 110000', 'Caption at 120000'] + }; + + jasmine.stubRequests = function() { + return spyOn($, 'ajax').andCallFake(function(settings) { + var match; + if (match = settings.url.match(/youtube\.com\/.+\/videos\/(.+)\?v=2&alt=jsonc/)) { + return settings.success({ + data: jasmine.stubbedMetadata[match[1]] + }); + } else if (match = settings.url.match(/static\/subs\/(.+)\.srt\.sjson/)) { + return settings.success(jasmine.stubbedCaption); + } else if (settings.url === '/calculate' || settings.url === '/6002x/modx/sequential/1/goto_position' || settings.url.match(/event$/) || settings.url.match(/6002x\/modx\/problem\/.+\/problem_(check|reset|show|save)$/)) { + + } else { + throw "External request attempted for " + settings.url + ", which is not defined."; + } + }); + }; + + jasmine.stubYoutubePlayer = function() { + return YT.Player = function() { + return jasmine.createSpyObj('YT.Player', ['cueVideoById', 'getVideoEmbedCode', 'getCurrentTime', 'getPlayerState', 'loadVideoById', 'playVideo', 'pauseVideo', 'seekTo']); + }; + }; + + jasmine.stubVideoPlayer = function(context, enableParts) { + var currentPartName, part, suite, _i, _len, _ref; + if (!$.isArray(enableParts)) { + enableParts = [enableParts]; + } + suite = context.suite; + while (suite = suite.parentSuite) { + currentPartName = suite.description; + } + enableParts.push(currentPartName); + _ref = ['VideoCaption', 'VideoSpeedControl', 'VideoProgressSlider']; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + part = _ref[_i]; + if (!($.inArray(part, enableParts) >= 0)) { + spyOn(window, part); + } + } + loadFixtures('video.html'); + jasmine.stubRequests(); + YT.Player = void 0; + context.video = new Video('example', '.75:abc123,1.0:def456'); + jasmine.stubYoutubePlayer(); + return new VideoPlayer(context.video); + }; + + spyOn(window, 'onunload'); + + window.YT = { + PlayerState: { + UNSTARTED: -1, + ENDED: 0, + PLAYING: 1, + PAUSED: 2, + BUFFERING: 3, + CUED: 5 + } + }; + + $.cookie = jasmine.createSpy('jQuery.cookie').andReturn('1.0'); + + $.fn.qtip = jasmine.createSpy('jQuery.qtip'); + + $.fn.scrollTo = jasmine.createSpy('jQuery.scrollTo'); + }).call(this); diff --git a/static/coffee/src/courseware.coffee b/static/coffee/src/courseware.coffee index 697aff8af4..e110dfe0ac 100644 --- a/static/coffee/src/courseware.coffee +++ b/static/coffee/src/courseware.coffee @@ -1,4 +1,6 @@ class @Courseware + @prefix: '' + constructor: -> Courseware.prefix = $("meta[name='path_prefix']").attr('content') new Navigation @@ -25,4 +27,3 @@ class @Courseware $('.course-content .histogram').each -> id = $(this).attr('id').replace(/histogram_/, '') new Histogram id, $(this).data('histogram') - diff --git a/templates/coffee/fixtures/items.json b/templates/coffee/fixtures/items.json new file mode 100644 index 0000000000..df37531f3f --- /dev/null +++ b/templates/coffee/fixtures/items.json @@ -0,0 +1,15 @@ +[ + { + "content": "\"Video 1\"", + "type": "video", + "title": "Video 1" + }, { + "content": "\"Video 2\"", + "type": "video", + "title": "Video 2" + }, { + "content": "\"Sample Problem\"", + "type": "problem", + "title": "Sample Problem" + } +] diff --git a/templates/coffee/fixtures/problem.html b/templates/coffee/fixtures/problem.html new file mode 100644 index 0000000000..f77ece7845 --- /dev/null +++ b/templates/coffee/fixtures/problem.html @@ -0,0 +1 @@ +
        diff --git a/templates/coffee/fixtures/problem_content.html b/templates/coffee/fixtures/problem_content.html new file mode 100644 index 0000000000..d2e89fed2b --- /dev/null +++ b/templates/coffee/fixtures/problem_content.html @@ -0,0 +1,16 @@ +

        Problem Header

        + +
        +

        Problem Content

        + +
        + + + + + + + Explanation +
        +
        +
        diff --git a/templates/coffee/fixtures/sequence.html b/templates/coffee/fixtures/sequence.html new file mode 100644 index 0000000000..53e9531dd2 --- /dev/null +++ b/templates/coffee/fixtures/sequence.html @@ -0,0 +1,20 @@ +
        + + +
        + + +
        diff --git a/templates/coffee/fixtures/tab.html b/templates/coffee/fixtures/tab.html new file mode 100644 index 0000000000..7d28fa2ad7 --- /dev/null +++ b/templates/coffee/fixtures/tab.html @@ -0,0 +1,3 @@ +
        + +
        diff --git a/templates/coffee/fixtures/video.html b/templates/coffee/fixtures/video.html new file mode 100644 index 0000000000..15404a89d1 --- /dev/null +++ b/templates/coffee/fixtures/video.html @@ -0,0 +1,12 @@ +
        +
        +
        +
        +
        +
        +
        +
        +
        +
        +
        +
        diff --git a/templates/coffee/spec/histogram_spec.coffee b/templates/coffee/spec/histogram_spec.coffee new file mode 100644 index 0000000000..4fd7ef98c3 --- /dev/null +++ b/templates/coffee/spec/histogram_spec.coffee @@ -0,0 +1,46 @@ +describe 'Histogram', -> + beforeEach -> + spyOn $, 'plot' + + describe 'constructor', -> + it 'instantiate the data arrays', -> + histogram = new Histogram 1, [] + expect(histogram.xTicks).toEqual [] + expect(histogram.yTicks).toEqual [] + expect(histogram.data).toEqual [] + + describe 'calculate', -> + beforeEach -> + @histogram = new Histogram(1, [[1, 1], [2, 2], [3, 3]]) + + it 'store the correct value for data', -> + expect(@histogram.data).toEqual [[1, Math.log(2)], [2, Math.log(3)], [3, Math.log(4)]] + + it 'store the correct value for x ticks', -> + expect(@histogram.xTicks).toEqual [[1, '1'], [2, '2'], [3, '3']] + + it 'store the correct value for y ticks', -> + expect(@histogram.yTicks).toEqual + + describe 'render', -> + it 'call flot with correct option', -> + new Histogram(1, [[1, 1], [2, 2], [3, 3]]) + expect($.plot).toHaveBeenCalledWith $("#histogram_1"), [ + data: [[1, Math.log(2)], [2, Math.log(3)], [3, Math.log(4)]] + bars: + show: true + align: 'center' + lineWidth: 0 + fill: 1.0 + color: "#b72121" + ], + xaxis: + min: -1 + max: 4 + ticks: [[1, '1'], [2, '2'], [3, '3']] + tickLength: 0 + yaxis: + min: 0.0 + max: Math.log(4) * 1.1 + ticks: [[Math.log(2), '1'], [Math.log(3), '2'], [Math.log(4), '3']] + labelWidth: 50 diff --git a/templates/coffee/spec/histogram_spec.js b/templates/coffee/spec/histogram_spec.js new file mode 100644 index 0000000000..edfeec15d5 --- /dev/null +++ b/templates/coffee/spec/histogram_spec.js @@ -0,0 +1,62 @@ +(function() { + + describe('Histogram', function() { + beforeEach(function() { + return spyOn($, 'plot'); + }); + describe('constructor', function() { + return it('instantiate the data arrays', function() { + var histogram; + histogram = new Histogram(1, []); + expect(histogram.xTicks).toEqual([]); + expect(histogram.yTicks).toEqual([]); + return expect(histogram.data).toEqual([]); + }); + }); + describe('calculate', function() { + beforeEach(function() { + return this.histogram = new Histogram(1, [[1, 1], [2, 2], [3, 3]]); + }); + it('store the correct value for data', function() { + return expect(this.histogram.data).toEqual([[1, Math.log(2)], [2, Math.log(3)], [3, Math.log(4)]]); + }); + it('store the correct value for x ticks', function() { + return expect(this.histogram.xTicks).toEqual([[1, '1'], [2, '2'], [3, '3']]); + }); + return it('store the correct value for y ticks', function() { + return expect(this.histogram.yTicks).toEqual; + }); + }); + return describe('render', function() { + return it('call flot with correct option', function() { + new Histogram(1, [[1, 1], [2, 2], [3, 3]]); + return expect($.plot).toHaveBeenCalledWith($("#histogram_1"), [ + { + data: [[1, Math.log(2)], [2, Math.log(3)], [3, Math.log(4)]], + bars: { + show: true, + align: 'center', + lineWidth: 0, + fill: 1.0 + }, + color: "#b72121" + } + ], { + xaxis: { + min: -1, + max: 4, + ticks: [[1, '1'], [2, '2'], [3, '3']], + tickLength: 0 + }, + yaxis: { + min: 0.0, + max: Math.log(4) * 1.1, + ticks: [[Math.log(2), '1'], [Math.log(3), '2'], [Math.log(4), '3']], + labelWidth: 50 + } + }); + }); + }); + }); + +}).call(this); diff --git a/templates/coffee/spec/logger_spec.coffee b/templates/coffee/spec/logger_spec.coffee new file mode 100644 index 0000000000..bfad742de3 --- /dev/null +++ b/templates/coffee/spec/logger_spec.coffee @@ -0,0 +1,35 @@ +describe 'Logger', -> + it 'expose window.log_event', -> + jasmine.stubRequests() + expect(window.log_event).toBe Logger.log + + describe 'log', -> + it 'send a request to log event', -> + spyOn $, 'getWithPrefix' + Logger.log 'example', 'data' + expect($.getWithPrefix).toHaveBeenCalledWith '/event', + event_type: 'example' + event: '"data"' + page: window.location.href + + describe 'bind', -> + beforeEach -> + Logger.bind() + Courseware.prefix = '/6002x' + + afterEach -> + window.onunload = null + + it 'bind the onunload event', -> + expect(window.onunload).toEqual jasmine.any(Function) + + it 'send a request to log event', -> + spyOn($, 'ajax') + $(window).trigger('onunload') + expect($.ajax).toHaveBeenCalledWith + url: "#{Courseware.prefix}/event", + data: + event_type: 'page_close' + event: '' + page: window.location.href + async: false diff --git a/templates/coffee/spec/logger_spec.js b/templates/coffee/spec/logger_spec.js new file mode 100644 index 0000000000..86b184a873 --- /dev/null +++ b/templates/coffee/spec/logger_spec.js @@ -0,0 +1,46 @@ +(function() { + + describe('Logger', function() { + it('expose window.log_event', function() { + jasmine.stubRequests(); + return expect(window.log_event).toBe(Logger.log); + }); + describe('log', function() { + return it('send a request to log event', function() { + spyOn($, 'getWithPrefix'); + Logger.log('example', 'data'); + return expect($.getWithPrefix).toHaveBeenCalledWith('/event', { + event_type: 'example', + event: '"data"', + page: window.location.href + }); + }); + }); + return describe('bind', function() { + beforeEach(function() { + Logger.bind(); + return Courseware.prefix = '/6002x'; + }); + afterEach(function() { + return window.onunload = null; + }); + it('bind the onunload event', function() { + return expect(window.onunload).toEqual(jasmine.any(Function)); + }); + return it('send a request to log event', function() { + spyOn($, 'ajax'); + $(window).trigger('onunload'); + return expect($.ajax).toHaveBeenCalledWith({ + url: "" + Courseware.prefix + "/event", + data: { + event_type: 'page_close', + event: '', + page: window.location.href + }, + async: false + }); + }); + }); + }); + +}).call(this); diff --git a/templates/coffee/spec/modules/problem_spec.coffee b/templates/coffee/spec/modules/problem_spec.coffee new file mode 100644 index 0000000000..6bca63cfe1 --- /dev/null +++ b/templates/coffee/spec/modules/problem_spec.coffee @@ -0,0 +1,250 @@ +describe 'Problem', -> + beforeEach -> + # Stub MathJax + window.MathJax = { Hub: { Queue: -> } } + window.update_schematics = -> + + loadFixtures 'problem.html' + spyOn Logger, 'log' + spyOn($.fn, 'load').andCallFake (url, callback) -> + $(@).html readFixtures('problem_content.html') + callback() + + describe 'constructor', -> + beforeEach -> + @problem = new Problem 1, '/problem/url/' + + it 'set the element', -> + expect(@problem.element).toBe '#problem_1' + + it 'set the content url', -> + expect(@problem.content_url).toEqual '/problem/url/problem_get?id=1' + + it 'render the content', -> + expect($.fn.load).toHaveBeenCalledWith @problem.content_url, @problem.bind + + describe 'bind', -> + beforeEach -> + spyOn MathJax.Hub, 'Queue' + spyOn window, 'update_schematics' + @problem = new Problem 1, '/problem/url/' + + it 'set mathjax typeset', -> + expect(MathJax.Hub.Queue).toHaveBeenCalled() + + it 'update schematics', -> + expect(window.update_schematics).toHaveBeenCalled() + + it 'bind answer refresh on button click', -> + expect($('section.action input:button')).toHandleWith 'click', @problem.refreshAnswers + + it 'bind the check button', -> + expect($('section.action input.check')).toHandleWith 'click', @problem.check + + it 'bind the reset button', -> + expect($('section.action input.reset')).toHandleWith 'click', @problem.reset + + it 'bind the show button', -> + expect($('section.action input.show')).toHandleWith 'click', @problem.show + + it 'bind the save button', -> + expect($('section.action input.save')).toHandleWith 'click', @problem.save + + describe 'render', -> + beforeEach -> + @problem = new Problem 1, '/problem/url/' + @bind = @problem.bind + spyOn @problem, 'bind' + + describe 'with content given', -> + beforeEach -> + @problem.render 'Hello World' + + it 'render the content', -> + expect(@problem.element.html()).toEqual 'Hello World' + + it 're-bind the content', -> + expect(@problem.bind).toHaveBeenCalled() + + describe 'with no content given', -> + it 'load the content via ajax', -> + expect($.fn.load).toHaveBeenCalledWith @problem.content_url, @bind + + describe 'check', -> + beforeEach -> + jasmine.stubRequests() + @problem = new Problem 1, '/problem/url/' + @problem.answers = 'foo=1&bar=2' + + it 'log the problem_check event', -> + @problem.check() + expect(Logger.log).toHaveBeenCalledWith 'problem_check', 'foo=1&bar=2' + + it 'submit the answer for check', -> + spyOn $, 'postWithPrefix' + @problem.check() + expect($.postWithPrefix).toHaveBeenCalledWith '/modx/problem/1/problem_check', 'foo=1&bar=2', jasmine.any(Function) + + describe 'when the response is correct', -> + it 'call render with returned content', -> + spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) -> callback(success: 'correct', contents: 'Correct!') + @problem.check() + expect(@problem.element.html()).toEqual 'Correct!' + + describe 'when the response is incorrect', -> + it 'call render with returned content', -> + spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) -> callback(success: 'incorrect', contents: 'Correct!') + @problem.check() + expect(@problem.element.html()).toEqual 'Correct!' + + describe 'when the response is undetermined', -> + it 'alert the response', -> + spyOn window, 'alert' + spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) -> callback(success: 'Number Only!') + @problem.check() + expect(window.alert).toHaveBeenCalledWith 'Number Only!' + + describe 'reset', -> + beforeEach -> + jasmine.stubRequests() + @problem = new Problem 1, '/problem/url/' + + it 'log the problem_reset event', -> + @problem.answers = 'foo=1&bar=2' + @problem.reset() + expect(Logger.log).toHaveBeenCalledWith 'problem_reset', 'foo=1&bar=2' + + it 'POST to the problem reset page', -> + spyOn $, 'postWithPrefix' + @problem.reset() + expect($.postWithPrefix).toHaveBeenCalledWith '/modx/problem/1/problem_reset', { id: 1 }, jasmine.any(Function) + + it 'render the returned content', -> + spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) -> callback("Reset!") + @problem.reset() + expect(@problem.element.html()).toEqual 'Reset!' + + describe 'show', -> + beforeEach -> + jasmine.stubRequests() + @problem = new Problem 1, '/problem/url/' + @problem.element.prepend '
        ' + + describe 'when the answer has not yet shown', -> + beforeEach -> + @problem.element.removeClass 'showed' + + it 'log the problem_show event', -> + @problem.show() + expect(Logger.log).toHaveBeenCalledWith 'problem_show', problem: 1 + + it 'fetch the answers', -> + spyOn $, 'postWithPrefix' + @problem.show() + expect($.postWithPrefix).toHaveBeenCalledWith '/modx/problem/1/problem_show', jasmine.any(Function) + + it 'show the answers', -> + spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback('1_1': 'One', '1_2': 'Two') + @problem.show() + expect($('#answer_1_1')).toHaveHtml 'One' + expect($('#answer_1_2')).toHaveHtml 'Two' + + it 'toggle the show answer button', -> + spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback({}) + @problem.show() + expect($('.show')).toHaveValue 'Hide Answer' + + it 'add the showed class to element', -> + spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback({}) + @problem.show() + expect(@problem.element).toHaveClass 'showed' + + describe 'multiple choice question', -> + beforeEach -> + @problem.element.prepend ''' + + + + + ''' + + it 'set the correct_answer attribute on the choice', -> + spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback('1_1': [2, 3]) + @problem.show() + expect($('label[for="input_1_1_1"]')).not.toHaveAttr 'correct_answer', 'true' + expect($('label[for="input_1_1_2"]')).toHaveAttr 'correct_answer', 'true' + expect($('label[for="input_1_1_3"]')).toHaveAttr 'correct_answer', 'true' + expect($('label[for="input_1_2_1"]')).not.toHaveAttr 'correct_answer', 'true' + + describe 'when the answers are alreay shown', -> + beforeEach -> + @problem.element.addClass 'showed' + @problem.element.prepend ''' + + ''' + $('#answer_1_1').html('One') + $('#answer_1_2').html('Two') + + it 'hide the answers', -> + @problem.show() + expect($('#answer_1_1')).toHaveHtml '' + expect($('#answer_1_2')).toHaveHtml '' + expect($('label[for="input_1_1_1"]')).not.toHaveAttr 'correct_answer' + + it 'toggle the show answer button', -> + @problem.show() + expect($('.show')).toHaveValue 'Show Answer' + + it 'remove the showed class from element', -> + @problem.show() + expect(@problem.element).not.toHaveClass 'showed' + + describe 'save', -> + beforeEach -> + jasmine.stubRequests() + @problem = new Problem 1, '/problem/url/' + @problem.answers = 'foo=1&bar=2' + + it 'log the problem_save event', -> + @problem.save() + expect(Logger.log).toHaveBeenCalledWith 'problem_save', 'foo=1&bar=2' + + it 'POST to save problem', -> + spyOn $, 'postWithPrefix' + @problem.save() + expect($.postWithPrefix).toHaveBeenCalledWith '/modx/problem/1/problem_save', 'foo=1&bar=2', jasmine.any(Function) + + it 'alert to the user', -> + spyOn window, 'alert' + spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) -> callback(success: 'OK') + @problem.save() + expect(window.alert).toHaveBeenCalledWith 'Saved' + + describe 'refreshAnswers', -> + beforeEach -> + @problem = new Problem 1, '/problem/url/' + @problem.element.html ''' +