diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index ca06942ae4..8ac6aa610e 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -1,3 +1,4 @@ +from util.json_request import expect_json import json from django.http import HttpResponse @@ -43,6 +44,7 @@ def edit_item(request): }) +@expect_json def save_item(request): item_id = request.POST['id'] data = json.loads(request.POST['data']) diff --git a/cms/envs/common.py b/cms/envs/common.py index 400a5138b9..9782ef2fd0 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -24,6 +24,7 @@ import tempfile import os.path import os import errno +import glob2 from path import path ############################ FEATURE CONFIGURATION ############################# @@ -58,6 +59,10 @@ MAKO_TEMPLATES['main'] = [ COMMON_ROOT / 'djangoapps' / 'pipeline_mako' / 'templates' ] +TEMPLATE_DIRS = ( + PROJECT_ROOT / "templates", +) + MITX_ROOT_URL = '' TEMPLATE_CONTEXT_PROCESSORS = ( @@ -68,6 +73,9 @@ TEMPLATE_CONTEXT_PROCESSORS = ( 'django.core.context_processors.csrf', # necessary for csrf protection ) +################################# Jasmine ################################### +JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee' + ################################# Middleware ################################### # List of finder classes that know how to find static files in # various locations. @@ -184,12 +192,16 @@ for xmodule in XModuleDescriptor.load_classes() + [RawDescriptor]: PIPELINE_JS = { 'main': { - 'source_filenames': ['coffee/main.coffee', 'coffee/unit.coffee'], - 'output_filename': 'js/main.js', + 'source_filenames': [pth.replace(PROJECT_ROOT / 'static/', '') for pth in glob2.glob(PROJECT_ROOT / 'static/coffee/src/**/*.coffee')], + 'output_filename': 'js/application.js', }, 'module-js': { 'source_filenames': module_js_sources, 'output_filename': 'js/modules.js', + }, + 'spec': { + 'source_filenames': [pth.replace(PROJECT_ROOT / 'static/', '') for pth in glob2.glob(PROJECT_ROOT / 'static/coffee/spec/**/*.coffee')], + 'output_filename': 'js/spec.js' } } @@ -233,4 +245,7 @@ INSTALLED_APPS = ( # For asset pipelining 'pipeline', 'staticfiles', + + # For testing + 'django_jasmine', ) diff --git a/cms/static/coffee/files.json b/cms/static/coffee/files.json new file mode 100644 index 0000000000..b396bec944 --- /dev/null +++ b/cms/static/coffee/files.json @@ -0,0 +1,8 @@ +{ + "js_files": [ + "/static/js/vendor/jquery.min.js", + "/static/js/vendor/json2.js", + "/static/js/vendor/underscore-min.js", + "/static/js/vendor/backbone-min.js" + ] +} diff --git a/cms/static/coffee/main.coffee b/cms/static/coffee/main.coffee deleted file mode 100644 index 8f7d7d7323..0000000000 --- a/cms/static/coffee/main.coffee +++ /dev/null @@ -1,90 +0,0 @@ -class @CMS - @setHeight = => - windowHeight = $(this).height() - @contentHeight = windowHeight - 29 - - @bind = => - $('a.module-edit').click -> - CMS.edit_item($(this).attr('id')) - return false - $(window).bind('resize', CMS.setHeight) - - @edit_item = (id) => - $.get('/edit_item', {id: id}, (data) => - $('#module-html').empty().append(data) - CMS.bind() - $('body.content .cal').css('height', @contentHeight) - $('body').addClass('content') - $('section.edit-pane').show() - new Unit('unit-wrapper', id) - ) - -$ -> - $.ajaxSetup - headers : { 'X-CSRFToken': $.cookie 'csrftoken' } - $('section.main-content').children().hide() - $('.editable').inlineEdit() - $('.editable-textarea').inlineEdit({control: 'textarea'}) - - heighest = 0 - $('.cal ol > li').each -> - heighest = if $(this).height() > heighest then $(this).height() else heighest - - $('.cal ol > li').css('height',heighest + 'px') - - $('.add-new-section').click -> return false - - $('.new-week .close').click -> - $(this).parents('.new-week').hide() - $('p.add-new-week').show() - return false - - $('.save-update').click -> - $(this).parent().parent().hide() - return false - - # $('html').keypress -> - # $('.wip').css('visibility', 'visible') - - setHeight = -> - windowHeight = $(this).height() - contentHeight = windowHeight - 29 - - $('section.main-content > section').css('min-height', contentHeight) - $('body.content .cal').css('height', contentHeight) - - $('.edit-week').click -> - $('body').addClass('content') - $('body.content .cal').css('height', contentHeight) - $('section.edit-pane').show() - return false - - $('a.week-edit').click -> - $('body').addClass('content') - $('body.content .cal').css('height', contentHeight) - $('section.edit-pane').show() - return false - - $('a.sequence-edit').click -> - $('body').addClass('content') - $('body.content .cal').css('height', contentHeight) - $('section.edit-pane').show() - return false - - $('a.module-edit').click -> - $('body.content .cal').css('height', contentHeight) - - $(document).ready(setHeight) - $(window).bind('resize', setHeight) - - $('.video-new a').click -> - $('section.edit-pane').show() - return false - - $('.problem-new a').click -> - $('section.edit-pane').show() - return false - - CMS.setHeight() - CMS.bind() - diff --git a/cms/static/coffee/spec/helpers.coffee b/cms/static/coffee/spec/helpers.coffee new file mode 100644 index 0000000000..91a411a8fc --- /dev/null +++ b/cms/static/coffee/spec/helpers.coffee @@ -0,0 +1,18 @@ +# Stub jQuery.cookie +@stubCookies = + csrftoken: "stubCSRFToken" + +jQuery.cookie = (key, value) => + if value? + @stubCookies[key] = value + else + @stubCookies[key] + +# Path Jasmine's `it` method to raise an error when the test is not defined. +# This is helpful when writing the specs first before writing the test. +@it = (desc, func) -> + if func? + jasmine.getEnv().it(desc, func) + else + jasmine.getEnv().it desc, -> + throw "test is undefined" diff --git a/cms/static/coffee/spec/main_spec.coffee b/cms/static/coffee/spec/main_spec.coffee new file mode 100644 index 0000000000..c8f6976fed --- /dev/null +++ b/cms/static/coffee/spec/main_spec.coffee @@ -0,0 +1,90 @@ +describe "CMS", -> + beforeEach -> + CMS.unbind() + + it "should iniitalize Models", -> + expect(CMS.Models).toBeDefined() + + it "should initialize Views", -> + expect(CMS.Views).toBeDefined() + + describe "start", -> + beforeEach -> + @element = $("
") + spyOn(CMS.Views, "Course").andReturn(jasmine.createSpyObj("Course", ["render"])) + CMS.start(@element) + + it "create the Course", -> + expect(CMS.Views.Course).toHaveBeenCalledWith(el: @element) + expect(CMS.Views.Course().render).toHaveBeenCalled() + + describe "view stack", -> + beforeEach -> + @currentView = jasmine.createSpy("currentView") + CMS.viewStack = [@currentView] + + describe "replaceView", -> + beforeEach -> + @newView = jasmine.createSpy("newView") + CMS.on("content.show", (@expectedView) =>) + CMS.replaceView(@newView) + + it "replace the views on the viewStack", -> + expect(CMS.viewStack).toEqual([@newView]) + + it "trigger content.show on CMS", -> + expect(@expectedView).toEqual(@newView) + + describe "pushView", -> + beforeEach -> + @newView = jasmine.createSpy("newView") + CMS.on("content.show", (@expectedView) =>) + CMS.pushView(@newView) + + it "push new view onto viewStack", -> + expect(CMS.viewStack).toEqual([@currentView, @newView]) + + it "trigger content.show on CMS", -> + expect(@expectedView).toEqual(@newView) + + describe "popView", -> + it "remove the current view from the viewStack", -> + CMS.popView() + expect(CMS.viewStack).toEqual([]) + + describe "when there's no view on the viewStack", -> + beforeEach -> + CMS.viewStack = [@currentView] + CMS.on("content.hide", => @eventTriggered = true) + CMS.popView() + + it "trigger content.hide on CMS", -> + expect(@eventTriggered).toBeTruthy + + describe "when there's previous view on the viewStack", -> + beforeEach -> + @parentView = jasmine.createSpyObj("parentView", ["delegateEvents"]) + CMS.viewStack = [@parentView, @currentView] + CMS.on("content.show", (@expectedView) =>) + CMS.popView() + + it "trigger content.show with the previous view on CMS", -> + expect(@expectedView).toEqual @parentView + + it "re-bind events on the view", -> + expect(@parentView.delegateEvents).toHaveBeenCalled() + +describe "main helper", -> + beforeEach -> + @previousAjaxSettings = $.extend(true, {}, $.ajaxSettings) + window.stubCookies["csrftoken"] = "stubCSRFToken" + $(document).ready() + + afterEach -> + $.ajaxSettings = @previousAjaxSettings + + it "turn on Backbone emulateHTTP", -> + expect(Backbone.emulateHTTP).toBeTruthy() + + it "setup AJAX CSRF token", -> + expect($.ajaxSettings.headers["X-CSRFToken"]).toEqual("stubCSRFToken") diff --git a/cms/static/coffee/spec/models/module_spec.coffee b/cms/static/coffee/spec/models/module_spec.coffee new file mode 100644 index 0000000000..43ebdf420a --- /dev/null +++ b/cms/static/coffee/spec/models/module_spec.coffee @@ -0,0 +1,65 @@ +describe "CMS.Models.Module", -> + it "set the correct URL", -> + expect(new CMS.Models.Module().url).toEqual("/save_item") + + it "set the correct default", -> + expect(new CMS.Models.Module().defaults).toEqual({data: ""}) + + describe "loadModule", -> + describe "when the module exists", -> + beforeEach -> + @fakeModule = jasmine.createSpy("fakeModuleObject") + window.FakeModule = jasmine.createSpy("FakeModule").andReturn(@fakeModule) + @module = new CMS.Models.Module(type: "FakeModule") + @stubElement = $("
") + @module.loadModule(@stubElement) + + afterEach -> + window.FakeModule = undefined + + it "initialize the module", -> + expect(window.FakeModule).toHaveBeenCalledWith(@stubElement) + expect(@module.module).toEqual(@fakeModule) + + describe "when the module does not exists", -> + beforeEach -> + @previousConsole = window.console + window.console = jasmine.createSpyObj("fakeConsole", ["error"]) + @module = new CMS.Models.Module(type: "HTML") + @module.loadModule($("
")) + + afterEach -> + window.console = @previousConsole + + it "print out error to log", -> + expect(window.console.error).toHaveBeenCalledWith("Unable to load HTML.") + + + describe "editUrl", -> + it "construct the correct URL based on id", -> + expect(new CMS.Models.Module(id: "i4x://mit.edu/module/html_123").editUrl()) + .toEqual("/edit_item?id=i4x%3A%2F%2Fmit.edu%2Fmodule%2Fhtml_123") + + describe "save", -> + beforeEach -> + spyOn(Backbone.Model.prototype, "save") + @module = new CMS.Models.Module() + + describe "when the module exists", -> + beforeEach -> + @module.module = jasmine.createSpyObj("FakeModule", ["save"]) + @module.module.save.andReturn("module data") + @module.save() + + it "set the data and call save on the module", -> + expect(@module.get("data")).toEqual("\"module data\"") + + it "call save on the backbone model", -> + expect(Backbone.Model.prototype.save).toHaveBeenCalled() + + describe "when the module does not exists", -> + beforeEach -> + @module.save() + + it "call save on the backbone model", -> + expect(Backbone.Model.prototype.save).toHaveBeenCalled() diff --git a/cms/static/coffee/spec/views/course_spec.coffee b/cms/static/coffee/spec/views/course_spec.coffee new file mode 100644 index 0000000000..f6a430ac2d --- /dev/null +++ b/cms/static/coffee/spec/views/course_spec.coffee @@ -0,0 +1,85 @@ +describe "CMS.Views.Course", -> + beforeEach -> + setFixtures """ +
+
+
    +
  1. +
  2. +
+
+ """ + CMS.unbind() + + describe "render", -> + beforeEach -> + spyOn(CMS.Views, "Week").andReturn(jasmine.createSpyObj("Week", ["render"])) + new CMS.Views.Course(el: $("#main-section")).render() + + it "create week view for each week",-> + expect(CMS.Views.Week.calls[0].args[0]) + .toEqual({ el: $(".week-one").get(0), height: 101 }) + expect(CMS.Views.Week.calls[1].args[0]) + .toEqual({ el: $(".week-two").get(0), height: 101 }) + + describe "on content.show", -> + beforeEach -> + @view = new CMS.Views.Course(el: $("#main-section")) + @subView = jasmine.createSpyObj("subView", ["render"]) + @subView.render.andReturn(el: "Subview Content") + spyOn(@view, "contentHeight").andReturn(100) + CMS.trigger("content.show", @subView) + + afterEach -> + $("body").removeClass("content") + + it "add content class to body", -> + expect($("body").attr("class")).toEqual("content") + + it "replace content in .main-content", -> + expect($(".main-content")).toHaveHtml("Subview Content") + + it "set height on calendar", -> + expect($(".cal")).toHaveCss(height: "100px") + + it "set minimum height on all sections", -> + expect($("#main-section>section")).toHaveCss(minHeight: "100px") + + describe "on content.hide", -> + beforeEach -> + $("body").addClass("content") + @view = new CMS.Views.Course(el: $("#main-section")) + $(".cal").css(height: 100) + $("#main-section>section").css(minHeight: 100) + CMS.trigger("content.hide") + + afterEach -> + $("body").removeClass("content") + + it "remove content class from body", -> + expect($("body").attr("class")).toEqual("") + + it "remove content from .main-content", -> + expect($(".main-content")).toHaveHtml("") + + it "reset height on calendar", -> + expect($(".cal")).not.toHaveCss(height: "100px") + + it "reset minimum height on all sections", -> + expect($("#main-section>section")).not.toHaveCss(minHeight: "100px") + + describe "maxWeekHeight", -> + it "return maximum height of the week element", -> + @view = new CMS.Views.Course(el: $("#main-section")) + expect(@view.maxWeekHeight()).toEqual(101) + + describe "contentHeight", -> + beforeEach -> + $("body").append($('
').height(100).hide()) + + afterEach -> + $("body>header#test").remove() + + it "return the window height minus the header bar", -> + @view = new CMS.Views.Course(el: $("#main-section")) + expect(@view.contentHeight()).toEqual($(window).height() - 100) diff --git a/cms/static/coffee/spec/views/module_edit_spec.coffee b/cms/static/coffee/spec/views/module_edit_spec.coffee new file mode 100644 index 0000000000..693353ff70 --- /dev/null +++ b/cms/static/coffee/spec/views/module_edit_spec.coffee @@ -0,0 +1,81 @@ +describe "CMS.Views.ModuleEdit", -> + beforeEach -> + @stubModule = jasmine.createSpyObj("Module", ["editUrl", "loadModule"]) + spyOn($.fn, "load") + setFixtures """ +
+ save + cancel +
    +
  1. + submodule +
  2. +
+
+ """ + CMS.unbind() + + describe "defaults", -> + it "set the correct tagName", -> + expect(new CMS.Views.ModuleEdit(model: @stubModule).tagName).toEqual("section") + + it "set the correct className", -> + expect(new CMS.Views.ModuleEdit(model: @stubModule).className).toEqual("edit-pane") + + describe "view creation", -> + beforeEach -> + @stubModule.editUrl.andReturn("/edit_item?id=stub_module") + new CMS.Views.ModuleEdit(el: $("#module-edit"), model: @stubModule) + + it "load the edit from via ajax and pass to the model", -> + expect($.fn.load).toHaveBeenCalledWith("/edit_item?id=stub_module", jasmine.any(Function)) + if $.fn.load.mostRecentCall + $.fn.load.mostRecentCall.args[1]() + expect(@stubModule.loadModule).toHaveBeenCalledWith($("#module-edit").get(0)) + + describe "save", -> + beforeEach -> + @stubJqXHR = jasmine.createSpy("stubJqXHR") + @stubJqXHR.success = jasmine.createSpy("stubJqXHR.success").andReturn(@stubJqXHR) + @stubJqXHR.error= jasmine.createSpy("stubJqXHR.success").andReturn(@stubJqXHR) + @stubModule.save = jasmine.createSpy("stubModule.save").andReturn(@stubJqXHR) + new CMS.Views.ModuleEdit(el: $("#module-edit"), model: @stubModule) + spyOn(window, "alert") + $(".save-update").click() + + it "call save on the model", -> + expect(@stubModule.save).toHaveBeenCalled() + + it "alert user on success", -> + @stubJqXHR.success.mostRecentCall.args[0]() + expect(window.alert).toHaveBeenCalledWith("Your changes have been saved.") + + it "alert user on error", -> + @stubJqXHR.error.mostRecentCall.args[0]() + expect(window.alert).toHaveBeenCalledWith("There was an error saving your changes. Please try again.") + + describe "cancel", -> + beforeEach -> + spyOn(CMS, "popView") + @view = new CMS.Views.ModuleEdit(el: $("#module-edit"), model: @stubModule) + $(".cancel").click() + + it "pop current view from viewStack", -> + expect(CMS.popView).toHaveBeenCalled() + + describe "editSubmodule", -> + beforeEach -> + @view = new CMS.Views.ModuleEdit(el: $("#module-edit"), model: @stubModule) + spyOn(CMS, "pushView") + spyOn(CMS.Views, "ModuleEdit") + .andReturn(@view = jasmine.createSpy("Views.ModuleEdit")) + spyOn(CMS.Models, "Module") + .andReturn(@model = jasmine.createSpy("Models.Module")) + $(".module-edit").click() + + it "push another module editing view into viewStack", -> + expect(CMS.pushView).toHaveBeenCalledWith @view + expect(CMS.Views.ModuleEdit).toHaveBeenCalledWith model: @model + expect(CMS.Models.Module).toHaveBeenCalledWith + id: "i4x://mitx.edu/course/module" + type: "html" diff --git a/cms/static/coffee/spec/views/module_spec.coffee b/cms/static/coffee/spec/views/module_spec.coffee new file mode 100644 index 0000000000..a42c06856c --- /dev/null +++ b/cms/static/coffee/spec/views/module_spec.coffee @@ -0,0 +1,24 @@ +describe "CMS.Views.Module", -> + beforeEach -> + setFixtures """ +
+ edit +
+ """ + + describe "edit", -> + beforeEach -> + @view = new CMS.Views.Module(el: $("#module")) + spyOn(CMS, "replaceView") + spyOn(CMS.Views, "ModuleEdit") + .andReturn(@view = jasmine.createSpy("Views.ModuleEdit")) + spyOn(CMS.Models, "Module") + .andReturn(@model = jasmine.createSpy("Models.Module")) + $(".module-edit").click() + + it "replace the main view with ModuleEdit view", -> + expect(CMS.replaceView).toHaveBeenCalledWith @view + expect(CMS.Views.ModuleEdit).toHaveBeenCalledWith model: @model + expect(CMS.Models.Module).toHaveBeenCalledWith + id: "i4x://mitx.edu/course/module" + type: "html" diff --git a/cms/static/coffee/spec/views/week_edit_spec.coffee b/cms/static/coffee/spec/views/week_edit_spec.coffee new file mode 100644 index 0000000000..754474d77f --- /dev/null +++ b/cms/static/coffee/spec/views/week_edit_spec.coffee @@ -0,0 +1,7 @@ +describe "CMS.Views.WeekEdit", -> + describe "defaults", -> + it "set the correct tagName", -> + expect(new CMS.Views.WeekEdit().tagName).toEqual("section") + + it "set the correct className", -> + expect(new CMS.Views.WeekEdit().className).toEqual("edit-pane") diff --git a/cms/static/coffee/spec/views/week_spec.coffee b/cms/static/coffee/spec/views/week_spec.coffee new file mode 100644 index 0000000000..74b8c22fde --- /dev/null +++ b/cms/static/coffee/spec/views/week_spec.coffee @@ -0,0 +1,67 @@ +describe "CMS.Views.Week", -> + beforeEach -> + setFixtures """ +
+
+ + edit +
    +
  • +
  • +
+
+ """ + CMS.unbind() + + describe "render", -> + beforeEach -> + spyOn(CMS.Views, "Module").andReturn(jasmine.createSpyObj("Module", ["render"])) + $.fn.inlineEdit = jasmine.createSpy("$.fn.inlineEdit") + @view = new CMS.Views.Week(el: $("#week"), height: 100).render() + + it "set the height of the element", -> + expect(@view.el).toHaveCss(height: "100px") + + it "make .editable as inline editor", -> + expect($.fn.inlineEdit.calls[0].object.get(0)) + .toEqual($(".editable").get(0)) + + it "make .editable-test as inline editor", -> + expect($.fn.inlineEdit.calls[1].object.get(0)) + .toEqual($(".editable-textarea").get(0)) + + it "create module subview for each module", -> + expect(CMS.Views.Module.calls[0].args[0]) + .toEqual({ el: $("#module-one").get(0) }) + expect(CMS.Views.Module.calls[1].args[0]) + .toEqual({ el: $("#module-two").get(0) }) + + describe "edit", -> + beforeEach -> + new CMS.Views.Week(el: $("#week"), height: 100).render() + spyOn(CMS, "replaceView") + spyOn(CMS.Views, "WeekEdit") + .andReturn(@view = jasmine.createSpy("Views.WeekEdit")) + $(".week-edit").click() + + it "replace the content with edit week view", -> + expect(CMS.replaceView).toHaveBeenCalledWith @view + expect(CMS.Views.WeekEdit).toHaveBeenCalled() + + describe "on content.show", -> + beforeEach -> + @view = new CMS.Views.Week(el: $("#week"), height: 100).render() + @view.$el.height("") + @view.setHeight() + + it "set the correct height", -> + expect(@view.el).toHaveCss(height: "100px") + + describe "on content.hide", -> + beforeEach -> + @view = new CMS.Views.Week(el: $("#week"), height: 100).render() + @view.$el.height("100px") + @view.resetHeight() + + it "remove height from the element", -> + expect(@view.el).not.toHaveCss(height: "100px") diff --git a/cms/static/coffee/src/main.coffee b/cms/static/coffee/src/main.coffee new file mode 100644 index 0000000000..b88bc7210b --- /dev/null +++ b/cms/static/coffee/src/main.coffee @@ -0,0 +1,35 @@ +@CMS = + Models: {} + Views: {} + + viewStack: [] + + start: (el) -> + new CMS.Views.Course(el: el).render() + + replaceView: (view) -> + @viewStack = [view] + CMS.trigger('content.show', view) + + pushView: (view) -> + @viewStack.push(view) + CMS.trigger('content.show', view) + + popView: -> + @viewStack.pop() + if _.isEmpty(@viewStack) + CMS.trigger('content.hide') + else + view = _.last(@viewStack) + CMS.trigger('content.show', view) + view.delegateEvents() + +_.extend CMS, Backbone.Events + +$ -> + Backbone.emulateHTTP = true + + $.ajaxSetup + headers : { 'X-CSRFToken': $.cookie 'csrftoken' } + + CMS.start($('section.main-container')) diff --git a/cms/static/coffee/src/models/module.coffee b/cms/static/coffee/src/models/module.coffee new file mode 100644 index 0000000000..257eca411e --- /dev/null +++ b/cms/static/coffee/src/models/module.coffee @@ -0,0 +1,17 @@ +class CMS.Models.Module extends Backbone.Model + url: '/save_item' + defaults: + data: '' + + loadModule: (element) -> + try + @module = new window[@get('type')](element) + catch TypeError + console.error "Unable to load #{@get('type')}." if console + + editUrl: -> + "/edit_item?#{$.param(id: @get('id'))}" + + save: (args...) -> + @set(data: JSON.stringify(@module.save())) if @module + super(args...) diff --git a/cms/static/coffee/src/views/course.coffee b/cms/static/coffee/src/views/course.coffee new file mode 100644 index 0000000000..2a5a012c07 --- /dev/null +++ b/cms/static/coffee/src/views/course.coffee @@ -0,0 +1,28 @@ +class CMS.Views.Course extends Backbone.View + initialize: -> + CMS.on('content.show', @showContent) + CMS.on('content.hide', @hideContent) + + render: -> + @$('#weeks > li').each (index, week) => + new CMS.Views.Week(el: week, height: @maxWeekHeight()).render() + return @ + + showContent: (subview) => + $('body').addClass('content') + @$('.main-content').html(subview.render().el) + @$('.cal').css height: @contentHeight() + @$('>section').css minHeight: @contentHeight() + + hideContent: => + $('body').removeClass('content') + @$('.main-content').empty() + @$('.cal').css height: '' + @$('>section').css minHeight: '' + + maxWeekHeight: -> + weekElementBorderSize = 1 + _.max($('#weeks > li').map -> $(this).height()) + weekElementBorderSize + + contentHeight: -> + $(window).height() - $('body>header').outerHeight() diff --git a/cms/static/coffee/src/views/module.coffee b/cms/static/coffee/src/views/module.coffee new file mode 100644 index 0000000000..5407204706 --- /dev/null +++ b/cms/static/coffee/src/views/module.coffee @@ -0,0 +1,7 @@ +class CMS.Views.Module extends Backbone.View + events: + "click .module-edit": "edit" + + edit: (event) => + event.preventDefault() + CMS.replaceView(new CMS.Views.ModuleEdit(model: new CMS.Models.Module(id: @$el.data('id'), type: @$el.data('type')))) diff --git a/cms/static/coffee/src/views/module_edit.coffee b/cms/static/coffee/src/views/module_edit.coffee new file mode 100644 index 0000000000..16968a9126 --- /dev/null +++ b/cms/static/coffee/src/views/module_edit.coffee @@ -0,0 +1,28 @@ +class CMS.Views.ModuleEdit extends Backbone.View + tagName: 'section' + className: 'edit-pane' + + events: + 'click .cancel': 'cancel' + 'click .module-edit': 'editSubmodule' + 'click .save-update': 'save' + + initialize: -> + @$el.load @model.editUrl(), => + @model.loadModule(@el) + + save: (event) -> + event.preventDefault() + @model.save().success(-> + alert("Your changes have been saved.") + ).error(-> + alert("There was an error saving your changes. Please try again.") + ) + + cancel: (event) -> + event.preventDefault() + CMS.popView() + + editSubmodule: (event) -> + event.preventDefault() + CMS.pushView(new CMS.Views.ModuleEdit(model: new CMS.Models.Module(id: $(event.target).data('id'), type: $(event.target).data('type')))) diff --git a/cms/static/coffee/src/views/week.coffee b/cms/static/coffee/src/views/week.coffee new file mode 100644 index 0000000000..8483b9d134 --- /dev/null +++ b/cms/static/coffee/src/views/week.coffee @@ -0,0 +1,25 @@ +class CMS.Views.Week extends Backbone.View + events: + 'click .week-edit': 'edit' + + initialize: -> + CMS.on('content.show', @resetHeight) + CMS.on('content.hide', @setHeight) + + render: -> + @setHeight() + @$('.editable').inlineEdit() + @$('.editable-textarea').inlineEdit(control: 'textarea') + @$('.modules .module').each -> + new CMS.Views.Module(el: this).render() + return @ + + edit: (event) -> + event.preventDefault() + CMS.replaceView(new CMS.Views.WeekEdit()) + + setHeight: => + @$el.height(@options.height) + + resetHeight: => + @$el.height('') diff --git a/cms/static/coffee/src/views/week_edit.coffee b/cms/static/coffee/src/views/week_edit.coffee new file mode 100644 index 0000000000..3082bc9fe2 --- /dev/null +++ b/cms/static/coffee/src/views/week_edit.coffee @@ -0,0 +1,3 @@ +class CMS.Views.WeekEdit extends Backbone.View + tagName: 'section' + className: 'edit-pane' diff --git a/cms/static/js/vendor/backbone-min.js b/cms/static/js/vendor/backbone-min.js new file mode 100644 index 0000000000..c1c0d4fff2 --- /dev/null +++ b/cms/static/js/vendor/backbone-min.js @@ -0,0 +1,38 @@ +// Backbone.js 0.9.2 + +// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc. +// Backbone may be freely distributed under the MIT license. +// For all details and documentation: +// http://backbonejs.org +(function(){var l=this,y=l.Backbone,z=Array.prototype.slice,A=Array.prototype.splice,g;g="undefined"!==typeof exports?exports:l.Backbone={};g.VERSION="0.9.2";var f=l._;!f&&"undefined"!==typeof require&&(f=require("underscore"));var i=l.jQuery||l.Zepto||l.ender;g.setDomLibrary=function(a){i=a};g.noConflict=function(){l.Backbone=y;return this};g.emulateHTTP=!1;g.emulateJSON=!1;var p=/\s+/,k=g.Events={on:function(a,b,c){var d,e,f,g,j;if(!b)return this;a=a.split(p);for(d=this._callbacks||(this._callbacks= +{});e=a.shift();)f=(j=d[e])?j.tail:{},f.next=g={},f.context=c,f.callback=b,d[e]={tail:g,next:j?j.next:f};return this},off:function(a,b,c){var d,e,h,g,j,q;if(e=this._callbacks){if(!a&&!b&&!c)return delete this._callbacks,this;for(a=a?a.split(p):f.keys(e);d=a.shift();)if(h=e[d],delete e[d],h&&(b||c))for(g=h.tail;(h=h.next)!==g;)if(j=h.callback,q=h.context,b&&j!==b||c&&q!==c)this.on(d,j,q);return this}},trigger:function(a){var b,c,d,e,f,g;if(!(d=this._callbacks))return this;f=d.all;a=a.split(p);for(g= +z.call(arguments,1);b=a.shift();){if(c=d[b])for(e=c.tail;(c=c.next)!==e;)c.callback.apply(c.context||this,g);if(c=f){e=c.tail;for(b=[b].concat(g);(c=c.next)!==e;)c.callback.apply(c.context||this,b)}}return this}};k.bind=k.on;k.unbind=k.off;var o=g.Model=function(a,b){var c;a||(a={});b&&b.parse&&(a=this.parse(a));if(c=n(this,"defaults"))a=f.extend({},c,a);b&&b.collection&&(this.collection=b.collection);this.attributes={};this._escapedAttributes={};this.cid=f.uniqueId("c");this.changed={};this._silent= +{};this._pending={};this.set(a,{silent:!0});this.changed={};this._silent={};this._pending={};this._previousAttributes=f.clone(this.attributes);this.initialize.apply(this,arguments)};f.extend(o.prototype,k,{changed:null,_silent:null,_pending:null,idAttribute:"id",initialize:function(){},toJSON:function(){return f.clone(this.attributes)},get:function(a){return this.attributes[a]},escape:function(a){var b;if(b=this._escapedAttributes[a])return b;b=this.get(a);return this._escapedAttributes[a]=f.escape(null== +b?"":""+b)},has:function(a){return null!=this.get(a)},set:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c||(c={});if(!d)return this;d instanceof o&&(d=d.attributes);if(c.unset)for(e in d)d[e]=void 0;if(!this._validate(d,c))return!1;this.idAttribute in d&&(this.id=d[this.idAttribute]);var b=c.changes={},h=this.attributes,g=this._escapedAttributes,j=this._previousAttributes||{};for(e in d){a=d[e];if(!f.isEqual(h[e],a)||c.unset&&f.has(h,e))delete g[e],(c.silent?this._silent: +b)[e]=!0;c.unset?delete h[e]:h[e]=a;!f.isEqual(j[e],a)||f.has(h,e)!=f.has(j,e)?(this.changed[e]=a,c.silent||(this._pending[e]=!0)):(delete this.changed[e],delete this._pending[e])}c.silent||this.change(c);return this},unset:function(a,b){(b||(b={})).unset=!0;return this.set(a,null,b)},clear:function(a){(a||(a={})).unset=!0;return this.set(f.clone(this.attributes),a)},fetch:function(a){var a=a?f.clone(a):{},b=this,c=a.success;a.success=function(d,e,f){if(!b.set(b.parse(d,f),a))return!1;c&&c(b,d)}; +a.error=g.wrapError(a.error,b,a);return(this.sync||g.sync).call(this,"read",this,a)},save:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c=c?f.clone(c):{};if(c.wait){if(!this._validate(d,c))return!1;e=f.clone(this.attributes)}a=f.extend({},c,{silent:!0});if(d&&!this.set(d,c.wait?a:c))return!1;var h=this,i=c.success;c.success=function(a,b,e){b=h.parse(a,e);if(c.wait){delete c.wait;b=f.extend(d||{},b)}if(!h.set(b,c))return false;i?i(h,a):h.trigger("sync",h,a,c)};c.error=g.wrapError(c.error, +h,c);b=this.isNew()?"create":"update";b=(this.sync||g.sync).call(this,b,this,c);c.wait&&this.set(e,a);return b},destroy:function(a){var a=a?f.clone(a):{},b=this,c=a.success,d=function(){b.trigger("destroy",b,b.collection,a)};if(this.isNew())return d(),!1;a.success=function(e){a.wait&&d();c?c(b,e):b.trigger("sync",b,e,a)};a.error=g.wrapError(a.error,b,a);var e=(this.sync||g.sync).call(this,"delete",this,a);a.wait||d();return e},url:function(){var a=n(this,"urlRoot")||n(this.collection,"url")||t(); +return this.isNew()?a:a+("/"==a.charAt(a.length-1)?"":"/")+encodeURIComponent(this.id)},parse:function(a){return a},clone:function(){return new this.constructor(this.attributes)},isNew:function(){return null==this.id},change:function(a){a||(a={});var b=this._changing;this._changing=!0;for(var c in this._silent)this._pending[c]=!0;var d=f.extend({},a.changes,this._silent);this._silent={};for(c in d)this.trigger("change:"+c,this,this.get(c),a);if(b)return this;for(;!f.isEmpty(this._pending);){this._pending= +{};this.trigger("change",this,a);for(c in this.changed)!this._pending[c]&&!this._silent[c]&&delete this.changed[c];this._previousAttributes=f.clone(this.attributes)}this._changing=!1;return this},hasChanged:function(a){return!arguments.length?!f.isEmpty(this.changed):f.has(this.changed,a)},changedAttributes:function(a){if(!a)return this.hasChanged()?f.clone(this.changed):!1;var b,c=!1,d=this._previousAttributes,e;for(e in a)if(!f.isEqual(d[e],b=a[e]))(c||(c={}))[e]=b;return c},previous:function(a){return!arguments.length|| +!this._previousAttributes?null:this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},isValid:function(){return!this.validate(this.attributes)},_validate:function(a,b){if(b.silent||!this.validate)return!0;var a=f.extend({},this.attributes,a),c=this.validate(a,b);if(!c)return!0;b&&b.error?b.error(this,c,b):this.trigger("error",this,c,b);return!1}});var r=g.Collection=function(a,b){b||(b={});b.model&&(this.model=b.model);b.comparator&&(this.comparator=b.comparator); +this._reset();this.initialize.apply(this,arguments);a&&this.reset(a,{silent:!0,parse:b.parse})};f.extend(r.prototype,k,{model:o,initialize:function(){},toJSON:function(a){return this.map(function(b){return b.toJSON(a)})},add:function(a,b){var c,d,e,g,i,j={},k={},l=[];b||(b={});a=f.isArray(a)?a.slice():[a];c=0;for(d=a.length;c=b))this.iframe=i('