diff options
author | Louise Crow <louise.crow@gmail.com> | 2014-12-18 15:24:30 +0000 |
---|---|---|
committer | Louise Crow <louise.crow@gmail.com> | 2014-12-18 15:24:30 +0000 |
commit | 5cfcdaac505e60914ee4398cfe431bd5d21b58ed (patch) | |
tree | 43c2ce66dda3e291cf2dc1aa688a48b2dc5ef45a | |
parent | f0bbeb4abf4bf07e5cfb46668f39bbff72ed7210 (diff) | |
parent | 1d6acb2af8e3f4c4926f47f097466c5e2acdac68 (diff) |
Merge branch 'admin-public-holiday-interface' into rails-3-develop
29 files changed, 1015 insertions, 601 deletions
@@ -11,9 +11,12 @@ gem 'dynamic_form' gem 'exception_notification' gem 'fancybox-rails' gem 'foundation-rails' +gem 'icalendar', '1.4.3' gem 'jquery-rails', '~> 3.0.4' gem 'jquery-ui-rails' gem 'json' +gem 'holidays' +gem 'iso_country_codes' gem 'mahoro' gem 'memcache-client' gem 'net-http-local', :platforms => [:ruby_18, :ruby_19] diff --git a/Gemfile.lock b/Gemfile.lock index ca813eb1a..ffb2286ce 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -125,7 +125,10 @@ GEM tilt highline (1.6.19) hike (1.2.3) + holidays (1.0.8) i18n (0.6.11) + icalendar (1.4.3) + iso_country_codes (0.6.1) journey (1.0.4) jquery-rails (3.0.4) railties (>= 3.0, < 5.0) @@ -310,6 +313,9 @@ DEPENDENCIES gettext gettext_i18n_rails globalize3! + holidays + icalendar (= 1.4.3) + iso_country_codes jquery-rails (~> 3.0.4) jquery-ui-rails json diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js index 4925a65a4..9402f7f6c 100644 --- a/app/assets/javascripts/admin.js +++ b/app/assets/javascripts/admin.js @@ -3,8 +3,10 @@ //= require jquery.ui.tabs //= require jquery.ui.sortable //= require jquery.ui.effect-highlight -//= require admin/bootstrap-collapse -//= require admin/bootstrap-tab +//= require bootstrap-collapse +//= require bootstrap-tab +//= require bootstrap-dropdown //= require admin/admin //= require admin/category-order +//= require admin/holidays //= require jquery_ujs diff --git a/app/assets/javascripts/admin/bootstrap-collapse.js b/app/assets/javascripts/admin/bootstrap-collapse.js deleted file mode 100644 index 9a364468b..000000000 --- a/app/assets/javascripts/admin/bootstrap-collapse.js +++ /dev/null @@ -1,138 +0,0 @@ -/* ============================================================= - * bootstrap-collapse.js v2.0.2 - * http://twitter.github.com/bootstrap/javascript.html#collapse - * ============================================================= - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ============================================================ */ - -!function( $ ){ - - "use strict" - - var Collapse = function ( element, options ) { - this.$element = $(element) - this.options = $.extend({}, $.fn.collapse.defaults, options) - - if (this.options["parent"]) { - this.$parent = $(this.options["parent"]) - } - - this.options.toggle && this.toggle() - } - - Collapse.prototype = { - - constructor: Collapse - - , dimension: function () { - var hasWidth = this.$element.hasClass('width') - return hasWidth ? 'width' : 'height' - } - - , show: function () { - var dimension = this.dimension() - , scroll = $.camelCase(['scroll', dimension].join('-')) - , actives = this.$parent && this.$parent.find('.in') - , hasData - - if (actives && actives.length) { - hasData = actives.data('collapse') - actives.collapse('hide') - hasData || actives.data('collapse', null) - } - - this.$element[dimension](0) - this.transition('addClass', 'show', 'shown') - this.$element[dimension](this.$element[0][scroll]) - - } - - , hide: function () { - var dimension = this.dimension() - this.reset(this.$element[dimension]()) - this.transition('removeClass', 'hide', 'hidden') - this.$element[dimension](0) - } - - , reset: function ( size ) { - var dimension = this.dimension() - - this.$element - .removeClass('collapse') - [dimension](size || 'auto') - [0].offsetWidth - - this.$element[size ? 'addClass' : 'removeClass']('collapse') - - return this - } - - , transition: function ( method, startEvent, completeEvent ) { - var that = this - , complete = function () { - if (startEvent == 'show') that.reset() - that.$element.trigger(completeEvent) - } - - this.$element - .trigger(startEvent) - [method]('in') - - $.support.transition && this.$element.hasClass('collapse') ? - this.$element.one($.support.transition.end, complete) : - complete() - } - - , toggle: function () { - this[this.$element.hasClass('in') ? 'hide' : 'show']() - } - - } - - /* COLLAPSIBLE PLUGIN DEFINITION - * ============================== */ - - $.fn.collapse = function ( option ) { - return this.each(function () { - var $this = $(this) - , data = $this.data('collapse') - , options = typeof option == 'object' && option - if (!data) $this.data('collapse', (data = new Collapse(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - $.fn.collapse.defaults = { - toggle: true - } - - $.fn.collapse.Constructor = Collapse - - - /* COLLAPSIBLE DATA-API - * ==================== */ - - $(function () { - $('body').on('click.collapse.data-api', '[data-toggle=collapse]', function ( e ) { - var $this = $(this), href - , target = $this.attr('data-target') - || e.preventDefault() - || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7 - , option = $(target).data('collapse') ? 'toggle' : $this.data() - $(target).collapse(option) - }) - }) - -}( window.jQuery );
\ No newline at end of file diff --git a/app/assets/javascripts/admin/bootstrap-tab.js b/app/assets/javascripts/admin/bootstrap-tab.js deleted file mode 100644 index 26c9ece75..000000000 --- a/app/assets/javascripts/admin/bootstrap-tab.js +++ /dev/null @@ -1,130 +0,0 @@ -/* ======================================================== - * bootstrap-tab.js v2.0.1 - * http://twitter.github.com/bootstrap/javascript.html#tabs - * ======================================================== - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ======================================================== */ - - -!function( $ ){ - - "use strict" - - /* TAB CLASS DEFINITION - * ==================== */ - - var Tab = function ( element ) { - this.element = $(element) - } - - Tab.prototype = { - - constructor: Tab - - , show: function () { - var $this = this.element - , $ul = $this.closest('ul:not(.dropdown-menu)') - , selector = $this.attr('data-target') - , previous - , $target - - if (!selector) { - selector = $this.attr('href') - selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 - } - - if ( $this.parent('li').hasClass('active') ) return - - previous = $ul.find('.active a').last()[0] - - $this.trigger({ - type: 'show' - , relatedTarget: previous - }) - - $target = $(selector) - - this.activate($this.parent('li'), $ul) - this.activate($target, $target.parent(), function () { - $this.trigger({ - type: 'shown' - , relatedTarget: previous - }) - }) - } - - , activate: function ( element, container, callback) { - var $active = container.find('> .active') - , transition = callback - && $.support.transition - && $active.hasClass('fade') - - function next() { - $active - .removeClass('active') - .find('> .dropdown-menu > .active') - .removeClass('active') - - element.addClass('active') - - if (transition) { - element[0].offsetWidth // reflow for transition - element.addClass('in') - } else { - element.removeClass('fade') - } - - if ( element.parent('.dropdown-menu') ) { - element.closest('li.dropdown').addClass('active') - } - - callback && callback() - } - - transition ? - $active.one($.support.transition.end, next) : - next() - - $active.removeClass('in') - } - } - - - /* TAB PLUGIN DEFINITION - * ===================== */ - - $.fn.tab = function ( option ) { - return this.each(function () { - var $this = $(this) - , data = $this.data('tab') - if (!data) $this.data('tab', (data = new Tab(this))) - if (typeof option == 'string') data[option]() - }) - } - - $.fn.tab.Constructor = Tab - - - /* TAB DATA-API - * ============ */ - - $(function () { - $('body').on('click.tab.data-api', '[data-toggle="tab"], [data-toggle="pill"]', function (e) { - e.preventDefault() - $(this).tab('show') - }) - }) - -}( window.jQuery );
\ No newline at end of file diff --git a/app/assets/javascripts/admin/holidays.js b/app/assets/javascripts/admin/holidays.js new file mode 100644 index 000000000..55eae9e2a --- /dev/null +++ b/app/assets/javascripts/admin/holidays.js @@ -0,0 +1,46 @@ +$(function() { + + // New button loads the 'new' form via AJAX + $('#new-holiday-button').click(function(){ + var new_call = $.ajax({ type: 'GET', url: $(this).attr('href')}); + new_call.done(function(response) { + $('#existing-holidays').before(response); + }); + return false; + + }); + + // Each edit button loads the 'edit' form for that holiday via AJAX + $('.holiday').each(function(index){ + var holiday_row = $(this); + var edit_button = holiday_row.find('.edit-button'); + edit_button.click(function(){ + var edit_call = $.ajax({ type: 'GET', url: holiday_row.data('target') }); + edit_call.done(function(response) { + holiday_row.html(response); + }); + return false; + }); + }); + + // Remove button removes form div for holiday from an import set + $('.remove-holiday').each(function(index){ + $(this).click(function(){ + $(this).parents('.import-holiday-info').remove(); + return false; + }); + }); + + if ($('#holiday_import_source_suggestions').is(':checked')){ + $('#holiday_import_ical_feed_url').attr("disabled", "disabled"); + } + // Enable and disable the feed element when that is selected as the import source + $('#holiday_import_source_feed').click(function(){ + $('#holiday_import_ical_feed_url').removeAttr("disabled"); + }); + + $('#holiday_import_source_suggestions').click(function(){ + $('#holiday_import_ical_feed_url').attr("disabled", "disabled"); + }); + +}); diff --git a/app/assets/javascripts/bootstrap-collapse.js b/app/assets/javascripts/bootstrap-collapse.js deleted file mode 100644 index 9a364468b..000000000 --- a/app/assets/javascripts/bootstrap-collapse.js +++ /dev/null @@ -1,138 +0,0 @@ -/* ============================================================= - * bootstrap-collapse.js v2.0.2 - * http://twitter.github.com/bootstrap/javascript.html#collapse - * ============================================================= - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ============================================================ */ - -!function( $ ){ - - "use strict" - - var Collapse = function ( element, options ) { - this.$element = $(element) - this.options = $.extend({}, $.fn.collapse.defaults, options) - - if (this.options["parent"]) { - this.$parent = $(this.options["parent"]) - } - - this.options.toggle && this.toggle() - } - - Collapse.prototype = { - - constructor: Collapse - - , dimension: function () { - var hasWidth = this.$element.hasClass('width') - return hasWidth ? 'width' : 'height' - } - - , show: function () { - var dimension = this.dimension() - , scroll = $.camelCase(['scroll', dimension].join('-')) - , actives = this.$parent && this.$parent.find('.in') - , hasData - - if (actives && actives.length) { - hasData = actives.data('collapse') - actives.collapse('hide') - hasData || actives.data('collapse', null) - } - - this.$element[dimension](0) - this.transition('addClass', 'show', 'shown') - this.$element[dimension](this.$element[0][scroll]) - - } - - , hide: function () { - var dimension = this.dimension() - this.reset(this.$element[dimension]()) - this.transition('removeClass', 'hide', 'hidden') - this.$element[dimension](0) - } - - , reset: function ( size ) { - var dimension = this.dimension() - - this.$element - .removeClass('collapse') - [dimension](size || 'auto') - [0].offsetWidth - - this.$element[size ? 'addClass' : 'removeClass']('collapse') - - return this - } - - , transition: function ( method, startEvent, completeEvent ) { - var that = this - , complete = function () { - if (startEvent == 'show') that.reset() - that.$element.trigger(completeEvent) - } - - this.$element - .trigger(startEvent) - [method]('in') - - $.support.transition && this.$element.hasClass('collapse') ? - this.$element.one($.support.transition.end, complete) : - complete() - } - - , toggle: function () { - this[this.$element.hasClass('in') ? 'hide' : 'show']() - } - - } - - /* COLLAPSIBLE PLUGIN DEFINITION - * ============================== */ - - $.fn.collapse = function ( option ) { - return this.each(function () { - var $this = $(this) - , data = $this.data('collapse') - , options = typeof option == 'object' && option - if (!data) $this.data('collapse', (data = new Collapse(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - $.fn.collapse.defaults = { - toggle: true - } - - $.fn.collapse.Constructor = Collapse - - - /* COLLAPSIBLE DATA-API - * ==================== */ - - $(function () { - $('body').on('click.collapse.data-api', '[data-toggle=collapse]', function ( e ) { - var $this = $(this), href - , target = $this.attr('data-target') - || e.preventDefault() - || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7 - , option = $(target).data('collapse') ? 'toggle' : $this.data() - $(target).collapse(option) - }) - }) - -}( window.jQuery );
\ No newline at end of file diff --git a/app/assets/javascripts/bootstrap-tab.js b/app/assets/javascripts/bootstrap-tab.js deleted file mode 100644 index 26c9ece75..000000000 --- a/app/assets/javascripts/bootstrap-tab.js +++ /dev/null @@ -1,130 +0,0 @@ -/* ======================================================== - * bootstrap-tab.js v2.0.1 - * http://twitter.github.com/bootstrap/javascript.html#tabs - * ======================================================== - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ======================================================== */ - - -!function( $ ){ - - "use strict" - - /* TAB CLASS DEFINITION - * ==================== */ - - var Tab = function ( element ) { - this.element = $(element) - } - - Tab.prototype = { - - constructor: Tab - - , show: function () { - var $this = this.element - , $ul = $this.closest('ul:not(.dropdown-menu)') - , selector = $this.attr('data-target') - , previous - , $target - - if (!selector) { - selector = $this.attr('href') - selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 - } - - if ( $this.parent('li').hasClass('active') ) return - - previous = $ul.find('.active a').last()[0] - - $this.trigger({ - type: 'show' - , relatedTarget: previous - }) - - $target = $(selector) - - this.activate($this.parent('li'), $ul) - this.activate($target, $target.parent(), function () { - $this.trigger({ - type: 'shown' - , relatedTarget: previous - }) - }) - } - - , activate: function ( element, container, callback) { - var $active = container.find('> .active') - , transition = callback - && $.support.transition - && $active.hasClass('fade') - - function next() { - $active - .removeClass('active') - .find('> .dropdown-menu > .active') - .removeClass('active') - - element.addClass('active') - - if (transition) { - element[0].offsetWidth // reflow for transition - element.addClass('in') - } else { - element.removeClass('fade') - } - - if ( element.parent('.dropdown-menu') ) { - element.closest('li.dropdown').addClass('active') - } - - callback && callback() - } - - transition ? - $active.one($.support.transition.end, next) : - next() - - $active.removeClass('in') - } - } - - - /* TAB PLUGIN DEFINITION - * ===================== */ - - $.fn.tab = function ( option ) { - return this.each(function () { - var $this = $(this) - , data = $this.data('tab') - if (!data) $this.data('tab', (data = new Tab(this))) - if (typeof option == 'string') data[option]() - }) - } - - $.fn.tab.Constructor = Tab - - - /* TAB DATA-API - * ============ */ - - $(function () { - $('body').on('click.tab.data-api', '[data-toggle="tab"], [data-toggle="pill"]', function (e) { - e.preventDefault() - $(this).tab('show') - }) - }) - -}( window.jQuery );
\ No newline at end of file diff --git a/app/assets/stylesheets/admin.scss b/app/assets/stylesheets/admin.scss index 104f10c75..31fe7e95a 100644 --- a/app/assets/stylesheets/admin.scss +++ b/app/assets/stylesheets/admin.scss @@ -80,6 +80,10 @@ body.admin { } } + .fieldWithErrors input{ + border: 1px solid #ff0c11; + } + body.admin blockquote p { font-size: 13px; display: inline; @@ -119,5 +123,34 @@ body.admin { padding: 3px 0; } + /* Holidays */ + .day_select { + width: 75px; + } + + .holiday-description, .holiday-day, .holiday-buttons, .holiday-destroy { + padding: 6px 4px; + } + + .holiday-description, .holiday-day, .holiday-buttons{ + display: inline-block; + } + + .holiday-description { + width: 300px; + } + .holiday-day { + width: 240px; + text-align: center; + } + .holiday-buttons{ + width: 200px; + text-align: right; + } + + #import_start_year, #import_end_year { + width: 75px; + } + } diff --git a/app/controllers/admin_holiday_imports_controller.rb b/app/controllers/admin_holiday_imports_controller.rb new file mode 100644 index 000000000..8596936f0 --- /dev/null +++ b/app/controllers/admin_holiday_imports_controller.rb @@ -0,0 +1,28 @@ +class AdminHolidayImportsController < AdminController + + def new + @holiday_import = HolidayImport.new(holiday_import_params) + @holiday_import.populate if @holiday_import.valid? + end + + def create + @holiday_import = HolidayImport.new(holiday_import_params) + if @holiday_import.save + notice = "Holidays successfully imported" + redirect_to admin_holidays_path, :notice => notice + else + render :new + end + end + + private + + def holiday_import_params(key = :holiday_import) + if params[key] + params[key].slice(:holidays_attributes, :start_year, :end_year, :source, :ical_feed_url) + else + {} + end + end + +end diff --git a/app/controllers/admin_holidays_controller.rb b/app/controllers/admin_holidays_controller.rb new file mode 100644 index 000000000..9177ebd44 --- /dev/null +++ b/app/controllers/admin_holidays_controller.rb @@ -0,0 +1,67 @@ +class AdminHolidaysController < AdminController + + def index + get_all_holidays + end + + def new + @holiday = Holiday.new + if request.xhr? + render :partial => 'new_form', :locals => { :holiday => @holiday } + else + render :action => 'new' + end + end + + def create + @holiday = Holiday.new(holiday_params) + if @holiday.save + notice = "Holiday successfully created." + redirect_to admin_holidays_path, :notice => notice + else + render :new + end + end + + def edit + @holiday = Holiday.find(params[:id]) + if request.xhr? + render :partial => 'edit_form' + else + render :action => 'edit' + end + end + + def update + @holiday = Holiday.find(params[:id]) + if @holiday.update_attributes(holiday_params) + flash[:notice] = 'Holiday successfully updated.' + redirect_to admin_holidays_path + else + render :edit + end + end + + def destroy + @holiday = Holiday.find(params[:id]) + @holiday.destroy + notice = "Holiday successfully destroyed" + redirect_to admin_holidays_path, :notice => notice + end + + private + + def get_all_holidays + @holidays_by_year = Holiday.all.group_by { |holiday| holiday.day.year } + @years = @holidays_by_year.keys.sort.reverse + end + + def holiday_params(key = :holiday) + if params[key] + params[key].slice(:description, 'day(1i)', 'day(2i)', 'day(3i)') + else + {} + end + end + +end diff --git a/app/models/holiday.rb b/app/models/holiday.rb index 4c4941589..34044683a 100644 --- a/app/models/holiday.rb +++ b/app/models/holiday.rb @@ -22,6 +22,8 @@ class Holiday < ActiveRecord::Base + validates_presence_of :day + def self.holidays @@holidays ||= all.collect { |h| h.day }.to_set end diff --git a/app/models/holiday_import.rb b/app/models/holiday_import.rb new file mode 100644 index 000000000..c6019fac0 --- /dev/null +++ b/app/models/holiday_import.rb @@ -0,0 +1,93 @@ +class HolidayImport + + include ActiveModel::Validations + + attr_accessor :holidays, + :ical_feed_url, + :start_year, + :end_year, + :start_date, + :end_date, + :source, + :populated + + validate :all_holidays_valid + validates_inclusion_of :source, :in => %w( suggestions feed ) + validates_presence_of :ical_feed_url, + :if => proc { |holiday_import| holiday_import.source == 'feed' } + + def initialize(opts = {}) + @populated = false + @start_year = opts.fetch(:start_year, Time.now.year).to_i + @end_year = opts.fetch(:end_year, Time.now.year).to_i + @start_date = Date.civil(start_year, 1, 1) + @end_date = Date.civil(end_year, 12, 31) + @source = opts.fetch(:source, 'suggestions') + @ical_feed_url = opts.fetch(:ical_feed_url, nil) + @country_code = AlaveteliConfiguration::iso_country_code.downcase + self.holidays_attributes = opts.fetch(:holidays_attributes, []) + end + + def populate + source == 'suggestions' ? populate_from_suggestions : populate_from_ical_feed + @populated = true + end + + def suggestions_country_name + IsoCountryCodes.find(@country_code).name if @country_code + end + + def period + start_year == end_year ? "#{start_year}" : "#{start_year}-#{end_year}" + end + + def save + holidays.all?(&:save) + end + + def holidays_attributes=(incoming_data) + incoming_data.each{ |offset, incoming| self.holidays << Holiday.new(incoming) } + end + + def holidays + @holidays ||= [] + end + + private + + def all_holidays_valid + errors.add(:base, 'These holidays could not be imported') unless holidays.all?(&:valid?) + end + + def populate_from_ical_feed + begin + cal_file = open(ical_feed_url) + cals = Icalendar.parse(cal_file, strict=false) + cal = cals.first + cal.events.each{ |cal_event| populate_from_ical_event(cal_event) } + rescue Errno::ENOENT, Exception => e + if e.message == 'Invalid line in calendar string!' + errors.add(:ical_feed_url, "Sorry, there's a problem with the format of that feed.") + elsif e.message.starts_with 'No such file or directory' + errors.add(:ical_feed_url, "Sorry we couldn't find that feed.") + else + raise e + end + end + end + + def populate_from_ical_event(cal_event) + if cal_event.dtstart >= start_date and cal_event.dtstart <= end_date + holidays << Holiday.new(:description => cal_event.summary, + :day => cal_event.dtstart) + end + end + + def populate_from_suggestions + holiday_info = Holidays.between(start_date, end_date, @country_code.to_sym, :observed) + holiday_info.each do |holiday_info_hash| + holidays << Holiday.new(:description => holiday_info_hash[:name], + :day => holiday_info_hash[:date]) + end + end +end diff --git a/app/views/admin_general/_admin_navbar.html.erb b/app/views/admin_general/_admin_navbar.html.erb index 14fc06092..15a5e65fa 100644 --- a/app/views/admin_general/_admin_navbar.html.erb +++ b/app/views/admin_general/_admin_navbar.html.erb @@ -9,11 +9,17 @@ <li><%= link_to 'Timeline', admin_timeline_path %></li> <li><%= link_to 'Stats', admin_stats_path %></li> <li><%= link_to 'Debug', admin_debug_path %></li> - <li><%= link_to 'Authorities', admin_body_list_path %></li> - <li><%= link_to 'Categories', admin_categories_path %></li> + <li class="dropdown"> + <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">Authorities<span class="caret"></span></a> + <ul class="dropdown-menu" role="menu"> + <li><%= link_to 'Authorities', admin_body_list_path %></li> + <li><%= link_to 'Categories', admin_categories_path %></li> + </ul> + </li> <li><%= link_to 'Requests', admin_request_list_path %></li> <li><%= link_to 'Users', admin_user_list_path %></li> <li><%= link_to 'Tracks', admin_track_list_path %></li> + <li><%= link_to 'Holidays', admin_holidays_path %></li> <li><%= link_to 'Log out', signout_path %></li> </ul> </div> diff --git a/app/views/admin_holiday_imports/new.html.erb b/app/views/admin_holiday_imports/new.html.erb new file mode 100644 index 000000000..047f321f9 --- /dev/null +++ b/app/views/admin_holiday_imports/new.html.erb @@ -0,0 +1,81 @@ +<% @title = "Create holidays from suggestions or iCal feed" %> +<h1><%= @title %></h1> + +<%= form_for( @holiday_import, :as => 'holiday_import', :url => '', :method => 'get', :html => { :class => 'form-horizontal form-inline' }) do |f| %> + <% if @holiday_import.holidays.empty? %> + <%= error_messages_for 'holiday_import', :header_message => 'There was a problem with these import settings' %> + <% end %> + <legend>Import settings</legend> + <div> + <div class="control-group"> + <label class="control-label">Choose the years to import holidays for</label> + <div class="controls"> + <label for="import_start_year" class="inline">Start year:</label> + <%= f.select :start_year, (Time.now.year)..(Time.now.year + 5) %> + <label for="import_end_year" class="inline">End year:</label> + <%= f.select :end_year, (Time.now.year)..(Time.now.year + 5) %> + </div> + </div> + + <div class="control-group"> + <label class="control-label">Import from built-in suggestions or iCal feed</label> + <div class="controls"> + <label class="radio inline"> + <%= f.radio_button :source, "suggestions" %>Built-in suggestions + </label> + <label class="radio inline"> + <%= f.radio_button :source, "feed" %>iCal feed + </label> + </div> + </div> + + <div class="control-group"> + <label class="control-label">iCal feed URL:</label> + <div class="controls"> + <%= f.text_field 'ical_feed_url' %> + </div> + </div> + + <div class="control-group"> + <input type="submit" value="Show holidays" class="btn btn-primary"> + </div> + + </div> +<% end %> + +<% if @holiday_import.populated %> + <h2>Holidays to import</h2> + + <table class="table table-striped table-condensed"> + <tbody> + <tr> + <td> + <% if @holiday_import.holidays.empty? %> + <% if @holiday_import.source == 'suggestions' %> + Sorry, we don't have any built-in suggestions for holiday days in <%= @holiday_import.suggestions_country_name %>. + <% else %> + Sorry, we couldn't find any holidays in that iCal feed. + <% end %> + <% else %> + <%= form_for( @holiday_import, :as => 'holiday_import', :url => admin_holiday_imports_path, :html => { :class => 'form-inline' } ) do |f| -%> + <%= error_messages_for 'holiday_import' %> + <legend> + <% if @holiday_import.source == 'suggestions' %> + Suggested holidays for <%= @holiday_import.suggestions_country_name %> (<%= @holiday_import.period %>) + <% else %> + Holidays from feed (<%= @holiday_import.period %>) + <% end %> + </legend> + <%= f.fields_for :holidays do |holiday_fields| %> + <div class="import-holiday-info"> + <%= render :partial => 'admin_holidays/form', :locals => {:f => holiday_fields, :context => :import, :holiday => holiday_fields.object } %> + </div> + <% end%> + <%= f.submit "Import", :class => 'btn btn-warning' %> + <% end %> + <% end %> + </td> + </tr> + </tbody> + </table> +<% end %> diff --git a/app/views/admin_holidays/_edit_form.html.erb b/app/views/admin_holidays/_edit_form.html.erb new file mode 100644 index 000000000..b750dbf4c --- /dev/null +++ b/app/views/admin_holidays/_edit_form.html.erb @@ -0,0 +1,14 @@ +<td> + <%= form_for(@holiday, :url => admin_holiday_path(@holiday), :html => { :class => 'form-inline edit-holiday-form'}) do |f| -%> + <%= render :partial => 'form', :locals => { :f => f, :holiday => @holiday, :context => :edit } %> + <% end %> + + <div class="holiday-destroy "> + <%= form_for @holiday, :url => admin_holiday_path(@holiday), :method => 'delete', :html => { :class => "form form-inline delete-holiday-form" } do |f| %> + <%= f.submit "Destroy", + :class => "btn btn-danger", + :confirm => 'Are you sure you want to destroy this public holiday?' %> + <% end %> + </div> + +</td> diff --git a/app/views/admin_holidays/_form.html.erb b/app/views/admin_holidays/_form.html.erb new file mode 100644 index 000000000..35370e5fc --- /dev/null +++ b/app/views/admin_holidays/_form.html.erb @@ -0,0 +1,22 @@ +<%= error_messages_for 'holiday' %> + +<div class="holiday-description"> + <% if holiday.new_record? %> + <%= f.text_field :description, :class => 'input', :placeholder => 'Enter description here' %> + <% else %> + <%= f.text_field :description, :class => 'input' %> + <% end %> +</div> + +<div class="holiday-day"> + <%= f.date_select :day, { :use_month_numbers => true }, { :class => "day_select" } %> +</div> +<div class="holiday-buttons"> + <% if context == :import %> + <%= f.submit "Remove", :class => 'btn remove-holiday' %> + <% else %> + <%= link_to("Cancel", admin_holidays_path, :class => 'btn') %> + <%= f.submit "Save", :class => 'btn btn-warning' %> +<% end %> +</div> + diff --git a/app/views/admin_holidays/_holiday.html.erb b/app/views/admin_holidays/_holiday.html.erb new file mode 100644 index 000000000..78818f411 --- /dev/null +++ b/app/views/admin_holidays/_holiday.html.erb @@ -0,0 +1,7 @@ +<td> + <div class="holiday-description"><%= holiday.description %></div> + <div class="holiday-day"><%= holiday.day %></div> + <div class="holiday-buttons"> + <%= link_to 'Edit', edit_admin_holiday_path(holiday), :class => "btn edit-button" %> + </div> +</td> diff --git a/app/views/admin_holidays/_new_form.html.erb b/app/views/admin_holidays/_new_form.html.erb new file mode 100644 index 000000000..aee73f426 --- /dev/null +++ b/app/views/admin_holidays/_new_form.html.erb @@ -0,0 +1,10 @@ +<table class="table table-striped table-condensed"> + <tbody> + <tr> + <td><%= form_for(@holiday, :url => admin_holidays_path, :html => { :class => 'form-inline new-holiday-form'}) do |f| -%> + <%= render :partial => 'form', :locals => { :f => f, :holiday => @holiday, :context => :new } %> + <% end %> + </td> + </tr> + </tbody> +</table> diff --git a/app/views/admin_holidays/edit.html.erb b/app/views/admin_holidays/edit.html.erb new file mode 100644 index 000000000..8f29c9a44 --- /dev/null +++ b/app/views/admin_holidays/edit.html.erb @@ -0,0 +1,9 @@ +<% @title = 'Edit public holiday' %> +<h1><%= @title %></h1> +<table class="table table-striped table-condensed"> + <tbody> + <tr> + <%= render :partial => 'edit_form' %> + </tr> + </tbody> +</table> diff --git a/app/views/admin_holidays/index.html.erb b/app/views/admin_holidays/index.html.erb new file mode 100644 index 000000000..d4ee8706b --- /dev/null +++ b/app/views/admin_holidays/index.html.erb @@ -0,0 +1,41 @@ +<% @title = 'Public Holidays' %> +<h1><%= @title %></h1> +<p> + + Alaveteli calculates the due dates of requests taking account of the + public holidays shown here. If you have set the + <code>WORKING_OR_CALENDAR_DAYS</code><a + href="http://alaveteli.org/docs/customising/config/#working_or_calendar_days" + target="_blank">(docs)</a> setting for Alaveteli to + <code>working</code>, the date when a response to a request is + officially overdue will be calculated in days that are not weekends + or public holidays. If you have set + <code>WORKING_OR_CALENDAR_DAYS</code> to <code>calendar</code>, the + date will be calculated in calendar days, but if the due date falls + on a public holiday or weekend day, then the due date is considered + to be the next week day that isn't a holiday. + +</p> +<div class="btn-toolbar"> + <div class="btn-group"> + <%= link_to 'New holiday', new_admin_holiday_path, :class => "btn btn-primary", :id => 'new-holiday-button' %> + </div> + <div class="btn-group"> + <%= link_to 'Create holidays from suggestions or iCal feed', new_admin_holiday_import_path, :class => "btn btn-warning" %> + </div> +</div> + +<div id="existing-holidays"> + <% @years.each do |year| %> + <h2><%= year %></h2> + <table class="table table-striped table-condensed"> + <tbody> + <% @holidays_by_year[year].sort_by(&:day).each do |holiday| %> + <%= content_tag_for(:tr, holiday, prefix=nil, 'data-target' => edit_admin_holiday_path(holiday)) do %> + <%= render :partial => 'holiday', :locals => { :holiday => holiday }%> + <% end %> + <% end %> + </tbody> + </table> + <% end %> +</div> diff --git a/app/views/admin_holidays/new.html.erb b/app/views/admin_holidays/new.html.erb new file mode 100644 index 000000000..792c32f52 --- /dev/null +++ b/app/views/admin_holidays/new.html.erb @@ -0,0 +1,4 @@ +<% @title = 'New public holiday' %> +<h1><%= @title %></h1> + +<%= render :partial => 'new_form' %> diff --git a/config/routes.rb b/config/routes.rb index 4b2eb5695..2886c2846 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -200,6 +200,21 @@ Alaveteli::Application.routes.draw do end #### + #### AdminHoliday controller + scope '/admin', :as => 'admin' do + resources :holidays, + :controller => 'admin_holidays' + end + #### + + #### AdminHolidayImports controller + scope '/admin', :as => 'admin' do + resources :holiday_imports, + :controller => 'admin_holiday_imports', + :only => [:new, :create] + end + #### + #### AdminPublicBodyChangeRequest controller match '/admin/change_request/edit/:id' => 'admin_public_body_change_requests#edit', :as => :admin_change_request_edit match '/admin/change_request/update/:id' => 'admin_public_body_change_requests#update', :as => :admin_change_request_update diff --git a/spec/controllers/admin_holiday_imports_controller_spec.rb b/spec/controllers/admin_holiday_imports_controller_spec.rb new file mode 100644 index 000000000..dd23a022f --- /dev/null +++ b/spec/controllers/admin_holiday_imports_controller_spec.rb @@ -0,0 +1,73 @@ +require 'spec_helper' + +describe AdminHolidayImportsController do + + describe :new do + + it 'renders the new template' do + get :new + expect(response).to render_template('new') + end + + it 'creates an import' do + get :new + assigns[:holiday_import].should be_instance_of(HolidayImport) + end + + describe 'if the import is valid' do + + it 'populates the import' do + mock_import = mock(HolidayImport, :valid? => true, + :populate => nil) + HolidayImport.stub!(:new).and_return(mock_import) + mock_import.should_receive(:populate) + get :new + end + + end + + end + + describe :create do + + it 'creates an import' do + post :create + assigns[:holiday_import].should be_instance_of(HolidayImport) + end + + describe 'if the import can be saved' do + + before do + mock_import = mock(HolidayImport, :save => true) + HolidayImport.stub!(:new).and_return(mock_import) + post :create + end + + it 'should show a success notice' do + flash[:notice].should == 'Holidays successfully imported' + end + + it 'should redirect to the index' do + response.should redirect_to(admin_holidays_path) + end + + end + + describe 'if the import cannot be saved' do + + before do + mock_import = mock(HolidayImport, :save => false) + HolidayImport.stub!(:new).and_return(mock_import) + post :create + end + + it 'should render the new template' do + expect(response).to render_template('new') + end + + end + + end + + +end diff --git a/spec/controllers/admin_holidays_controller_spec.rb b/spec/controllers/admin_holidays_controller_spec.rb new file mode 100644 index 000000000..21cb51d29 --- /dev/null +++ b/spec/controllers/admin_holidays_controller_spec.rb @@ -0,0 +1,192 @@ +require 'spec_helper' + +describe AdminHolidaysController do + + describe :index do + + before do + @holiday_one = FactoryGirl.create(:holiday, :day => Date.new(2010, 1, 1)) + @holiday_two = FactoryGirl.create(:holiday, :day => Date.new(2011, 2, 2)) + @holiday_three = FactoryGirl.create(:holiday, :day => Date.new(2011, 3, 3)) + end + + it 'gets a hash of holidays keyed by year' do + get :index + assigns(:holidays_by_year)[2010].should include(@holiday_one) + assigns(:holidays_by_year)[2011].should include(@holiday_two) + assigns(:holidays_by_year)[2011].should include(@holiday_three) + end + + it 'gets a list of years with holidays' do + get :index + assigns(:years).should include(2010) + assigns(:years).should include(2011) + end + + it 'renders the index template' do + get :index + expect(response).to render_template('index') + end + + end + + describe :new do + + + describe 'when not using ajax' do + + it 'renders the new template' do + get :new + expect(response).to render_template('new') + end + + end + + describe 'when using ajax' do + + it 'renders the new form partial' do + xhr :get, :new + expect(response).to render_template('new_form') + end + end + + it 'creates a new holiday' do + get :new + assigns[:holiday].should be_instance_of(Holiday) + end + + end + + describe :create do + + before do + @holiday_params = { :description => "New Year's Day", + 'day(1i)' => '2010', + 'day(2i)' => '1', + 'day(3i)' => '1' } + post :create, :holiday => @holiday_params + end + + it 'creates a new holiday' do + assigns(:holiday).description.should == @holiday_params[:description] + assigns(:holiday).day.should == Date.new(2010, 1, 1) + assigns(:holiday).should be_persisted + end + + it 'shows the admin a success message' do + flash[:notice].should == 'Holiday successfully created.' + end + + it 'redirects to the index' do + response.should redirect_to admin_holidays_path + end + + context 'when there are errors' do + + before do + Holiday.any_instance.stub(:save).and_return(false) + post :create, :holiday => @holiday_params + end + + it 'renders the new template' do + expect(response).to render_template('new') + end + end + + end + + describe :edit do + + before do + @holiday = FactoryGirl.create(:holiday) + end + + describe 'when not using ajax' do + + it 'renders the edit template' do + get :edit, :id => @holiday.id + expect(response).to render_template('edit') + end + + end + + describe 'when using ajax' do + + it 'renders the edit form partial' do + xhr :get, :edit, :id => @holiday.id + expect(response).to render_template('edit_form') + end + + end + + it 'gets the holiday in the id param' do + get :edit, :id => @holiday.id + assigns[:holiday].should == @holiday + end + + end + + describe :update do + + before do + @holiday = FactoryGirl.create(:holiday, :day => Date.new(2010, 1, 1), + :description => "Test Holiday") + put :update, :id => @holiday.id, :holiday => { :description => 'New Test Holiday' } + end + + it 'gets the holiday in the id param' do + assigns[:holiday].should == @holiday + end + + it 'updates the holiday' do + holiday = Holiday.find(@holiday.id).description.should == 'New Test Holiday' + end + + it 'shows the admin a success message' do + flash[:notice].should == 'Holiday successfully updated.' + end + + it 'redirects to the index' do + response.should redirect_to admin_holidays_path + end + + context 'when there are errors' do + + before do + Holiday.any_instance.stub(:update_attributes).and_return(false) + put :update, :id => @holiday.id, :holiday => { :description => 'New Test Holiday' } + end + + it 'renders the edit template' do + expect(response).to render_template('edit') + end + end + + end + + describe :destroy do + + before(:each) do + @holiday = FactoryGirl.create(:holiday) + delete :destroy, :id => @holiday.id + end + + it 'finds the holiday to destroy' do + assigns(:holiday).should == @holiday + end + + it 'destroys the holiday' do + assigns(:holiday).should be_destroyed + end + + it 'tells the admin the holiday has been destroyed' do + msg = "Holiday successfully destroyed" + flash[:notice].should == msg + end + + it 'redirects to the index action' do + expect(response).to redirect_to(admin_holidays_path) + end + end + + end diff --git a/spec/factories/holidays.rb b/spec/factories/holidays.rb new file mode 100644 index 000000000..531130c8a --- /dev/null +++ b/spec/factories/holidays.rb @@ -0,0 +1,8 @@ +FactoryGirl.define do + + factory :holiday do + day Date.new(2010, 1, 1) + description "New Year's Day" + end + +end diff --git a/spec/fixtures/files/ical-holidays.ics b/spec/fixtures/files/ical-holidays.ics new file mode 100644 index 000000000..6ccf31202 --- /dev/null +++ b/spec/fixtures/files/ical-holidays.ics @@ -0,0 +1,22 @@ +BEGIN:VCALENDAR +VERSION:2.0 +METHOD:PUBLISH +PRODID:-//uk.gov/GOVUK calendars//EN +CALSCALE:GREGORIAN +BEGIN:VEVENT +DTEND;VALUE=DATE:20140102 +DTSTART;VALUE=DATE:20140101 +SUMMARY:New Year's Day +UID:ca6af7456b0088abad9a69f9f620f5ac-17@gov.uk +SEQUENCE:0 +DTSTAMP:20140916T090346Z +END:VEVENT +BEGIN:VEVENT +DTEND;VALUE=DATE:20150102 +DTSTART;VALUE=DATE:20150101 +SUMMARY:New Year's Day +UID:ca6af7456b00a69f9f620f5ac-17@gov.uk +SEQUENCE:0 +DTSTAMP:20140916T090346Z +END:VEVENT +END:VCALENDAR diff --git a/spec/models/holiday_import_spec.rb b/spec/models/holiday_import_spec.rb new file mode 100644 index 000000000..d0be6fb98 --- /dev/null +++ b/spec/models/holiday_import_spec.rb @@ -0,0 +1,155 @@ +require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') + +describe HolidayImport do + + it 'validates the presence of a feed if the source is a feed' do + holiday_import = HolidayImport.new(:source => 'feed') + holiday_import.valid?.should be_false + holiday_import.errors[:ical_feed_url].should == ["can't be blank"] + end + + it 'does not validate the presence of a feed if the source is suggestions' do + holiday_import = HolidayImport.new(:source => 'suggestions') + holiday_import.valid?.should be_true + end + + it 'validates that the source is either "feed" or "suggestions"' do + holiday_import = HolidayImport.new(:source => 'something') + holiday_import.valid?.should be_false + holiday_import.errors[:source].should == ["is not included in the list"] + end + + it 'validates that all holidays create from attributes are valid' do + holiday_import = HolidayImport.new(:source => 'suggestions', + :holidays_attributes => {"0" => {:description => '', + "day(1i)"=>"", + "day(2i)"=>"", + "day(3i)"=>""}}) + holiday_import.valid?.should be_false + holiday_import.errors[:base].should == ["These holidays could not be imported"] + end + + it 'validates that all holidays to import are valid' do + holiday_import = HolidayImport.new + holiday_import.holidays = [ Holiday.new ] + holiday_import.valid?.should be_false + holiday_import.errors[:base].should == ['These holidays could not be imported'] + end + + it 'defaults to importing holidays for the current year' do + holiday_import = HolidayImport.new + holiday_import.start_year.should == 2014 + holiday_import.end_year.should == 2014 + end + + it 'allows the start and end year to be set' do + holiday_import = HolidayImport.new(:start_year => 2011, :end_year => 2012) + holiday_import.start_year.should == 2011 + holiday_import.end_year.should == 2012 + end + + it 'sets the start and end dates to the beginning and end of the year' do + holiday_import = HolidayImport.new(:start_year => 2011, :end_year => 2012) + holiday_import.start_date.should == Date.new(2011, 1, 1) + holiday_import.end_date.should == Date.new(2012, 12, 31) + end + + it 'sets a default source of suggestions' do + holiday_import = HolidayImport.new + holiday_import.source.should == 'suggestions' + end + + it 'allows the source to be set' do + holiday_import = HolidayImport.new(:source => 'feed') + holiday_import.source.should == 'feed' + end + + it 'allows an iCal feed URL to be set' do + holiday_import = HolidayImport.new(:ical_feed_url => 'http://www.example.com') + holiday_import.ical_feed_url.should == 'http://www.example.com' + end + + it 'sets a default populated flag to false' do + holiday_import = HolidayImport.new + holiday_import.populated.should == false + end + + it 'returns a readable description of the period for multiple years' do + HolidayImport.new(:start_year => 2011, :end_year => 2012).period.should == '2011-2012' + end + + it 'returns a readable description of the period for a single year' do + HolidayImport.new(:start_year => 2011, :end_year => 2011).period.should == '2011' + end + + it 'returns the country name for which suggestions are generated' do + HolidayImport.new.suggestions_country_name.should == 'Germany' + end + + describe 'when populating a set of holidays to import from suggestions' do + + before do + holidays = [ { :date => Date.new(2014, 1, 1), :name => "New Year's Day", :regions => [:gb] } ] + Holidays.stub!(:between).and_return(holidays) + @holiday_import = HolidayImport.new(:source => 'suggestions') + @holiday_import.populate + end + + it 'should populate holidays from the suggestions' do + @holiday_import.holidays.size.should == 1 + holiday = @holiday_import.holidays.first + holiday.description.should == "New Year's Day" + holiday.day.should == Date.new(2014, 1, 1) + end + + it 'should return a flag that it has been populated' do + @holiday_import.populated.should == true + end + + end + + describe 'when populating a set of holidays to import from a feed' do + + before do + @holiday_import = HolidayImport.new(:source => 'feed', + :ical_feed_url => 'http://www.example.com') + end + + it 'should populate holidays from the feed that are between the dates' do + @holiday_import.stub!(:open).and_return(load_file_fixture('ical-holidays.ics')) + @holiday_import.populate + @holiday_import.holidays.size.should == 1 + holiday = @holiday_import.holidays.first + holiday.description.should == "New Year's Day" + holiday.day.should == Date.new(2014, 1, 1) + end + + it 'should add an error if the calendar cannot be parsed' do + @holiday_import.stub!(:open).and_return('some invalid data') + @holiday_import.populate + expected = ["Sorry, there's a problem with the format of that feed."] + @holiday_import.errors[:ical_feed_url].should == expected + end + + it 'should add an error if the calendar cannot be found' do + @holiday_import.stub!(:open).and_raise Errno::ENOENT.new('No such file or directory') + @holiday_import.populate + expected = ["Sorry we couldn't find that feed."] + @holiday_import.errors[:ical_feed_url].should == expected + end + + end + + describe 'when saving' do + + it 'saves all holidays' do + holiday = Holiday.new + holiday_import = HolidayImport.new + holiday_import.holidays = [ holiday ] + holiday.should_receive(:save) + holiday_import.save + end + + end + +end diff --git a/spec/models/holiday_spec.rb b/spec/models/holiday_spec.rb index 89849abb7..2f8eeabd9 100644 --- a/spec/models/holiday_spec.rb +++ b/spec/models/holiday_spec.rb @@ -9,87 +9,98 @@ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') -describe Holiday, " when calculating due date" do +describe Holiday do - def due_date(ymd) - return Holiday.due_date_from_working_days(Date.strptime(ymd), 20).strftime("%F") - end + describe :new do - context "in working days" do - it "handles no holidays" do - due_date('2008-10-01').should == '2008-10-29' + it 'should require a day' do + holiday = Holiday.new + holiday.valid?.should be_false + holiday.errors[:day].should == ["can't be blank"] end + end - it "handles non leap years" do - due_date('2007-02-01').should == '2007-03-01' - end + describe " when calculating due date" do - it "handles leap years" do - due_date('2008-02-01').should == '2008-02-29' + def due_date(ymd) + return Holiday.due_date_from_working_days(Date.strptime(ymd), 20).strftime("%F") end - it "handles Thursday start" do - due_date('2009-03-12').should == '2009-04-14' - end + context "in working days" do + it "handles no holidays" do + due_date('2008-10-01').should == '2008-10-29' + end - it "handles Friday start" do - due_date('2009-03-13').should == '2009-04-15' - end + it "handles non leap years" do + due_date('2007-02-01').should == '2007-03-01' + end - # Delivery at the weekend ends up the same due day as if it had arrived on - # the Friday before. This is because the next working day (Monday) counts - # as day 1. - # See http://www.whatdotheyknow.com/help/officers#days - it "handles Saturday start" do - due_date('2009-03-14').should == '2009-04-15' - end - it "handles Sunday start" do - due_date('2009-03-15').should == '2009-04-15' - end + it "handles leap years" do + due_date('2008-02-01').should == '2008-02-29' + end - it "handles Monday start" do - due_date('2009-03-16').should == '2009-04-16' - end + it "handles Thursday start" do + due_date('2009-03-12').should == '2009-04-14' + end - it "handles Time objects" do - Holiday.due_date_from_working_days(Time.utc(2009, 03, 16, 12, 0, 0), 20).strftime('%F').should == '2009-04-16' - end - end + it "handles Friday start" do + due_date('2009-03-13').should == '2009-04-15' + end - context "in calendar days" do - it "handles no holidays" do - Holiday.due_date_from_calendar_days(Date.new(2008, 10, 1), 20).should == Date.new(2008, 10, 21) - end + # Delivery at the weekend ends up the same due day as if it had arrived on + # the Friday before. This is because the next working day (Monday) counts + # as day 1. + # See http://www.whatdotheyknow.com/help/officers#days + it "handles Saturday start" do + due_date('2009-03-14').should == '2009-04-15' + end + it "handles Sunday start" do + due_date('2009-03-15').should == '2009-04-15' + end - it "handles the due date falling on a Friday" do - Holiday.due_date_from_calendar_days(Date.new(2008, 10, 4), 20).should == Date.new(2008, 10, 24) - end + it "handles Monday start" do + due_date('2009-03-16').should == '2009-04-16' + end - # If the due date would fall on a Saturday it should in fact fall on the next day that isn't a weekend - # or a holiday - it "handles the due date falling on a Saturday" do - Holiday.due_date_from_calendar_days(Date.new(2008, 10, 5), 20).should == Date.new(2008, 10, 27) + it "handles Time objects" do + Holiday.due_date_from_working_days(Time.utc(2009, 03, 16, 12, 0, 0), 20).strftime('%F').should == '2009-04-16' + end end - it "handles the due date falling on a Sunday" do - Holiday.due_date_from_calendar_days(Date.new(2008, 10, 6), 20).should == Date.new(2008, 10, 27) - end + context "in calendar days" do + it "handles no holidays" do + Holiday.due_date_from_calendar_days(Date.new(2008, 10, 1), 20).should == Date.new(2008, 10, 21) + end - it "handles the due date falling on a Monday" do - Holiday.due_date_from_calendar_days(Date.new(2008, 10, 7), 20).should == Date.new(2008, 10, 27) - end + it "handles the due date falling on a Friday" do + Holiday.due_date_from_calendar_days(Date.new(2008, 10, 4), 20).should == Date.new(2008, 10, 24) + end - it "handles the due date falling on a day before a Holiday" do - Holiday.due_date_from_calendar_days(Date.new(2008, 12, 4), 20).should == Date.new(2008, 12, 24) - end + # If the due date would fall on a Saturday it should in fact fall on the next day that isn't a weekend + # or a holiday + it "handles the due date falling on a Saturday" do + Holiday.due_date_from_calendar_days(Date.new(2008, 10, 5), 20).should == Date.new(2008, 10, 27) + end - it "handles the due date falling on a Holiday" do - Holiday.due_date_from_calendar_days(Date.new(2008, 12, 5), 20).should == Date.new(2008, 12, 29) - end + it "handles the due date falling on a Sunday" do + Holiday.due_date_from_calendar_days(Date.new(2008, 10, 6), 20).should == Date.new(2008, 10, 27) + end + + it "handles the due date falling on a Monday" do + Holiday.due_date_from_calendar_days(Date.new(2008, 10, 7), 20).should == Date.new(2008, 10, 27) + end + + it "handles the due date falling on a day before a Holiday" do + Holiday.due_date_from_calendar_days(Date.new(2008, 12, 4), 20).should == Date.new(2008, 12, 24) + end - it "handles Time objects" do - Holiday.due_date_from_calendar_days(Time.utc(2009, 03, 17, 12, 0, 0), 20).should == Date.new(2009, 4, 6) + it "handles the due date falling on a Holiday" do + Holiday.due_date_from_calendar_days(Date.new(2008, 12, 5), 20).should == Date.new(2008, 12, 29) + end + + it "handles Time objects" do + Holiday.due_date_from_calendar_days(Time.utc(2009, 03, 17, 12, 0, 0), 20).should == Date.new(2009, 4, 6) + end end end end - |