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..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 @@ -130,8 +131,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: @@ -139,14 +138,10 @@ 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}) - 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/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/djangoapps/courseware/modules/seq_module.py b/djangoapps/courseware/modules/seq_module.py index af41d0e8da..063c37e5b3 100644 --- a/djangoapps/courseware/modules/seq_module.py +++ b/djangoapps/courseware/modules/seq_module.py @@ -28,14 +28,6 @@ class Module(XModule): self.render() return self.content - def get_init_js(self): - self.render() - return self.init_js - - def get_destroy_js(self): - self.render() - return self.destroy_js - def handle_ajax(self, dispatch, get): if dispatch=='goto_position': self.position = int(get['position']) @@ -45,45 +37,39 @@ class Module(XModule): def render(self): if self.rendered: return - def j(m): + def j(m): ''' jsonify contents so it can be embedded in a js array We also need to split 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] + self.contents = [j(self.render_function(e)) for e in self.xmltree] - print self.titles + for contents, title in zip(self.contents, titles): + contents['title'] = title 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 +80,10 @@ 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/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/video_module.py b/djangoapps/courseware/modules/video_module.py index c678838f2b..80a1ce2f80 100644 --- a/djangoapps/courseware/modules/video_module.py +++ b/djangoapps/courseware/modules/video_module.py @@ -30,32 +30,17 @@ 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)) - + return self.youtube + 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 +54,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/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 ''' diff --git a/envs/common.py b/envs/common.py index af678f9a14..d6a9d0c889 100644 --- a/envs/common.py +++ b/envs/common.py @@ -20,6 +20,7 @@ Longer TODO: """ import sys import tempfile +import glob2 import djcelery from path import path @@ -285,13 +286,12 @@ PIPELINE_CSS = { PIPELINE_JS = { 'application': { - 'source_filenames': [ - 'coffee/src/calculator.coffee', - 'coffee/src/courseware.coffee', - 'coffee/src/feedback_form.coffee', - 'coffee/src/main.coffee' - ], + 'source_filenames': [path.replace('static/', '') for path in glob2.glob('static/coffee/src/**/*.coffee')], 'output_filename': 'js/application.js' + }, + 'spec': { + 'source_filenames': [path.replace('static/', '') for path in glob2.glob('static/coffee/spec/**/*.coffee')], + 'output_filename': 'js/spec.js' } } diff --git a/requirements.txt b/requirements.txt index c05b9eb265..7acec6ea27 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,3 +21,4 @@ django-jasmine beautifulsoup requests newrelic +glob2 diff --git a/static/coffee/.gitignore b/static/coffee/.gitignore new file mode 100644 index 0000000000..a6c7c2852d --- /dev/null +++ b/static/coffee/.gitignore @@ -0,0 +1 @@ +*.js diff --git a/static/coffee/files.json b/static/coffee/files.json index bfae4dfe87..1e5e010d73 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/flot/jquery.flot.js" ], "static_files": [ "js/application.js" diff --git a/static/coffee/fixtures/items.json b/static/coffee/fixtures/items.json new file mode 100644 index 0000000000..df37531f3f --- /dev/null +++ b/static/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/static/coffee/fixtures/problem.html b/static/coffee/fixtures/problem.html new file mode 100644 index 0000000000..f77ece7845 --- /dev/null +++ b/static/coffee/fixtures/problem.html @@ -0,0 +1 @@ +
diff --git a/static/coffee/fixtures/problem_content.html b/static/coffee/fixtures/problem_content.html new file mode 100644 index 0000000000..d2e89fed2b --- /dev/null +++ b/static/coffee/fixtures/problem_content.html @@ -0,0 +1,16 @@ +

Problem Header

+ +
+

Problem Content

+ +
+ + + + + + + Explanation +
+
+
diff --git a/static/coffee/fixtures/sequence.html b/static/coffee/fixtures/sequence.html new file mode 100644 index 0000000000..53e9531dd2 --- /dev/null +++ b/static/coffee/fixtures/sequence.html @@ -0,0 +1,20 @@ +
+ + +
+ + +
diff --git a/static/coffee/fixtures/tab.html b/static/coffee/fixtures/tab.html new file mode 100644 index 0000000000..7d28fa2ad7 --- /dev/null +++ b/static/coffee/fixtures/tab.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/static/coffee/fixtures/video.html b/static/coffee/fixtures/video.html new file mode 100644 index 0000000000..15404a89d1 --- /dev/null +++ b/static/coffee/fixtures/video.html @@ -0,0 +1,12 @@ +
+
+
+
+
+
+
+
+
+
+
+
diff --git a/static/coffee/spec/calculator_spec.coffee b/static/coffee/spec/calculator_spec.coffee index 5c3fde5e2d..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,12 +56,12 @@ 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() 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/calculator_spec.js b/static/coffee/spec/calculator_spec.js deleted file mode 100644 index 9ea57f3aef..0000000000 --- a/static/coffee/spec/calculator_spec.js +++ /dev/null @@ -1,79 +0,0 @@ -(function() { - - describe('Calculator', function() { - beforeEach(function() { - loadFixtures('calculator.html'); - return this.calculator = new Calculator; - }); - describe('bind', function() { - beforeEach(function() { - return Calculator.bind(); - }); - it('bind the calculator button', function() { - return expect($('.calc')).toHandleWith('click', this.calculator.toggle); - }); - it('bind the help button', function() { - expect($('div.help-wrapper a')).toHandleWith('mouseenter', this.calculator.helpToggle); - return expect($('div.help-wrapper a')).toHandleWith('mouseleave', this.calculator.helpToggle); - }); - it('prevent default behavior on help button', function() { - $('div.help-wrapper a').click(function(e) { - return expect(e.isDefaultPrevented()).toBeTruthy(); - }); - return $('div.help-wrapper a').click(); - }); - it('bind the calculator submit', function() { - return expect($('form#calculator')).toHandleWith('submit', this.calculator.calculate); - }); - return it('prevent default behavior on form submit', function() { - $('form#calculator').submit(function(e) { - expect(e.isDefaultPrevented()).toBeTruthy(); - return e.preventDefault(); - }); - return $('form#calculator').submit(); - }); - }); - describe('toggle', function() { - it('toggle the calculator and focus the input', function() { - spyOn($.fn, 'focus'); - this.calculator.toggle(); - expect($('li.calc-main')).toHaveClass('open'); - return expect($('#calculator_wrapper #calculator_input').focus).toHaveBeenCalled(); - }); - return it('toggle the close button on the calculator button', function() { - this.calculator.toggle(); - expect($('.calc')).toHaveClass('closed'); - this.calculator.toggle(); - return expect($('.calc')).not.toHaveClass('closed'); - }); - }); - describe('helpToggle', function() { - return it('toggle the help overlay', function() { - this.calculator.helpToggle(); - expect($('.help')).toHaveClass('shown'); - this.calculator.helpToggle(); - return expect($('.help')).not.toHaveClass('shown'); - }); - }); - return describe('calculate', function() { - beforeEach(function() { - $('#calculator_input').val('1+2'); - spyOn($, 'getJSON').andCallFake(function(url, data, callback) { - return callback({ - result: 3 - }); - }); - return this.calculator.calculate(); - }); - it('send data to /calculate', function() { - return expect($.getJSON).toHaveBeenCalledWith('/calculate', { - equation: '1+2' - }, jasmine.any(Function)); - }); - return it('update the calculator output', function() { - return expect($('#calculator_output').val()).toEqual('3'); - }); - }); - }); - -}).call(this); diff --git a/static/coffee/spec/courseware_spec.coffee b/static/coffee/spec/courseware_spec.coffee index 5933e3e686..9d938c14e1 100644 --- a/static/coffee/spec/courseware_spec.coffee +++ b/static/coffee/spec/courseware_spec.coffee @@ -1,77 +1,62 @@ 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 content change event', -> + @courseware.bind() + expect($('.course-content .sequence')).toHandleWith 'contentChanged', @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 -> + jasmine.stubRequests() + @courseware = new Courseware + spyOn(window, 'Histogram') + spyOn(window, 'Problem') + spyOn(window, 'Video') + setFixtures """ +
+
+
+
+
+
+
+ """ + @courseware.render() - it 'binds the accordionchange event', -> - Courseware.Navigation.bind() - expect($('#accordion')).toHandleWith 'accordionchange', @navigation.log + 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 'bind the navigation toggle', -> - Courseware.Navigation.bind() - expect($('#open_close_accordion a')).toHandleWith 'click', @navigation.toggle + it 'detect the problem element and convert it', -> + expect(window.Problem).toHaveBeenCalledWith('3', '/example/url/') - 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 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 deleted file mode 100644 index d2d6e5583b..0000000000 --- a/static/coffee/spec/courseware_spec.js +++ /dev/null @@ -1,98 +0,0 @@ -(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(); - }); - }); - return describe('Navigation', function() { - beforeEach(function() { - loadFixtures('accordion.html'); - return this.navigation = new Courseware.Navigation; - }); - 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(); - }); - }); - }); - 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/static/coffee/spec/feedback_form_spec.coffee b/static/coffee/spec/feedback_form_spec.coffee index 191645b3d3..ce4195faab 100644 --- a/static/coffee/spec/feedback_form_spec.coffee +++ b/static/coffee/spec/feedback_form_spec.coffee @@ -2,10 +2,10 @@ describe 'FeedbackForm', -> beforeEach -> loadFixtures 'feedback_form.html' - describe 'bind', -> + describe 'constructor', -> beforeEach -> - FeedbackForm.bind() - spyOn($, 'post').andCallFake (url, data, callback, format) -> + new FeedbackForm + spyOn($, 'postWithPrefix').andCallFake (url, data, callback, format) -> callback() it 'binds to the #feedback_button', -> @@ -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/spec/feedback_form_spec.js b/static/coffee/spec/feedback_form_spec.js deleted file mode 100644 index bccb53604f..0000000000 --- a/static/coffee/spec/feedback_form_spec.js +++ /dev/null @@ -1,34 +0,0 @@ -(function() { - - describe('FeedbackForm', function() { - beforeEach(function() { - return loadFixtures('feedback_form.html'); - }); - return describe('bind', function() { - beforeEach(function() { - FeedbackForm.bind(); - return spyOn($, 'post').andCallFake(function(url, data, callback, format) { - return callback(); - }); - }); - it('binds to the #feedback_button', function() { - return expect($('#feedback_button')).toHandle('click'); - }); - it('post data to /send_feedback on click', function() { - $('#feedback_subject').val('Awesome!'); - $('#feedback_message').val('This site is really good.'); - $('#feedback_button').click(); - return expect($.post).toHaveBeenCalledWith('/send_feedback', { - subject: 'Awesome!', - message: 'This site is really good.', - url: window.location.href - }, jasmine.any(Function), 'json'); - }); - return it('replace the form with a thank you message', function() { - $('#feedback_button').click(); - return expect($('#feedback_div').html()).toEqual('Feedback submitted. Thank you'); - }); - }); - }); - -}).call(this); diff --git a/static/coffee/spec/helper.coffee b/static/coffee/spec/helper.coffee index 1f27e257c2..34e03e9d32 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/sequence/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 deleted file mode 100644 index 3add5f2bf8..0000000000 --- a/static/coffee/spec/helper.js +++ /dev/null @@ -1,5 +0,0 @@ -(function() { - - jasmine.getFixtures().fixturesPath = "/_jasmine/fixtures/"; - -}).call(this); diff --git a/static/coffee/spec/histogram_spec.coffee b/static/coffee/spec/histogram_spec.coffee new file mode 100644 index 0000000000..4fd7ef98c3 --- /dev/null +++ b/static/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/static/coffee/spec/logger_spec.coffee b/static/coffee/spec/logger_spec.coffee new file mode 100644 index 0000000000..bfad742de3 --- /dev/null +++ b/static/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/static/coffee/spec/modules/problem_spec.coffee b/static/coffee/spec/modules/problem_spec.coffee new file mode 100644 index 0000000000..6bca63cfe1 --- /dev/null +++ b/static/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 ''' + % 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': diff --git a/templates/video.html b/templates/video.html index 0d1f6c5641..54d6f8fa37 100644 --- a/templates/video.html +++ b/templates/video.html @@ -2,127 +2,18 @@

${name}

% endif -
-
- -
-
-
-
- - -
- -
-
- -
-
    -
  • Pause
  • - -
  • -
    0:00 / 0:00
    -
  • -
- - - +
+
+
+
+
-
+ +
+
- -
    - -
  1. -
  2. -
  3. -
  4. -
  5. -
  6. -
  7. -
  8. -
  9. -
  10. -
  11. -
  12. -
  13. -
  14. -
  15. -
-
- -<%block name="js_extra"> - - - -
    % for t in annotations:
  1. diff --git a/templates/video_init.js b/templates/video_init.js deleted file mode 100644 index bcbaecd249..0000000000 --- a/templates/video_init.js +++ /dev/null @@ -1,156 +0,0 @@ -var streams=${ streams } -var params = { allowScriptAccess: "always", bgcolor: "#cccccc", wmode: "transparent", allowFullScreen: "true" }; -var atts = { id: "myytplayer" }; - -// If the user doesn't have flash, use the HTML5 Video instead. YouTube's -// iFrame API which supports HTML5 is still developmental so it is not default -if (swfobject.hasFlashPlayerVersion("10.1")){ - swfobject.embedSWF(document.location.protocol + "//www.youtube.com/apiplayer?enablejsapi=1&playerapiid=ytplayer?wmode=transparent", - "ytapiplayer", "640", "385", "8", null, null, params, atts); -} else { - - //end of this URL may need &origin=http://..... once pushed to production to prevent XSS - $("#html5_player").attr("src", document.location.protocol + "//www.youtube.com/embed/" + streams["1.0"] + "?enablejsapi=1&controls=0"); - $("#html5_player").show(); - - var tag = document.createElement('script'); - tag.src = document.location.protocol + "//www.youtube.com/player_api"; - var firstScriptTag = document.getElementsByTagName('script')[0]; - firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); - // Make sure the callback is called once API ready, YT seems to be buggy - loadHTML5Video(); -} - -var captions=0; - -/* Cache a reference to our slider element */ -var slider = $('#slider') - -.slider({ - range: "min", - slide: function(event,ui) { - var slider_time = format_time(ui.value) - - seek_slide('slide',event.originalEvent,ui.value); - handle.qtip('option', 'content.text', '' + slider_time); - }, - stop:function(event,ui){seek_slide('stop',event.originalEvent,ui.value);} -}), - -/* Grab and cache the newly created slider handle */ -handle = $('.ui-slider-handle', slider); - -/* - * Selector needs changing here to match your elements. - * - * Notice the second argument to the $() constructor, which tells - * jQuery to use that as the top-level element to seareh down from. - */ - handle.qtip({ - content: '' + slider.slider('option', 'value'), // Use the current value of the slider - position: { - my: 'bottom center', - at: 'top center', - container: handle // Stick it inside the handle element so it keeps the position synched up - }, - hide: { - delay: 700 // Give it a longer delay so it doesn't hide frequently as we move the handle - }, - style: { - classes: 'ui-tooltip-slider', - widget: true // Make it Themeroller compatible - } - }); - -function good() { - window['console'].log(ytplayer.getCurrentTime()); -} - -ajax_video=good; - -// load the same video speed your last video was at in a sequence -// if the last speed played on video doesn't exist on another video just use 1.0 as default - -function add_speed(key, stream) { - var id = 'speed_' + stream; - - if (key == video_speed) { - $("#video_speeds").append('
  2. '+key+'x
  3. '); - $("p.active").text(key + 'x'); - } else { - $("#video_speeds").append('
  4. '+key+'x
  5. '); - } - - $("#"+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