aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLouise Crow <louise.crow@gmail.com>2014-12-15 17:16:11 +0000
committerLouise Crow <louise.crow@gmail.com>2014-12-15 18:23:05 +0000
commit8e69a9372fb29bc8107ee8c542f9408efa54b76e (patch)
tree763662478955bb44584b6abd9a060c6aa7863f53
parentceae995a5356a651f5e466afc2cd30ff70fe1555 (diff)
Allow import of holidays from feed or built-in suggestions
-rw-r--r--Gemfile3
-rw-r--r--Gemfile.lock6
-rw-r--r--app/assets/javascripts/admin/holidays.js23
-rw-r--r--app/assets/stylesheets/admin.scss5
-rw-r--r--app/controllers/admin_holiday_imports_controller.rb28
-rw-r--r--app/controllers/admin_holidays_controller.rb2
-rw-r--r--app/models/holiday_import.rb93
-rw-r--r--app/views/admin_holiday_imports/new.html.erb81
-rw-r--r--app/views/admin_holidays/_edit_form.html.erb2
-rw-r--r--app/views/admin_holidays/_form.html.erb11
-rw-r--r--spec/controllers/admin_holiday_imports_controller_spec.rb73
-rw-r--r--spec/controllers/admin_holidays_controller_spec.rb3
-rw-r--r--spec/fixtures/files/ical-holidays.ics22
-rw-r--r--spec/models/holiday_import_spec.rb155
14 files changed, 497 insertions, 10 deletions
diff --git a/Gemfile b/Gemfile
index 495024bac..a6315ea04 100644
--- a/Gemfile
+++ b/Gemfile
@@ -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/holidays.js b/app/assets/javascripts/admin/holidays.js
index 81f269c80..55eae9e2a 100644
--- a/app/assets/javascripts/admin/holidays.js
+++ b/app/assets/javascripts/admin/holidays.js
@@ -14,14 +14,33 @@ $(function() {
$('.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/stylesheets/admin.scss b/app/assets/stylesheets/admin.scss
index 69fce108c..469c3ed91 100644
--- a/app/assets/stylesheets/admin.scss
+++ b/app/assets/stylesheets/admin.scss
@@ -143,5 +143,10 @@ body.admin {
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
index 3c4b37530..9177ebd44 100644
--- a/app/controllers/admin_holidays_controller.rb
+++ b/app/controllers/admin_holidays_controller.rb
@@ -7,7 +7,7 @@ class AdminHolidaysController < AdminController
def new
@holiday = Holiday.new
if request.xhr?
- render :partial => 'new_form'
+ render :partial => 'new_form', :locals => { :holiday => @holiday }
else
render :action => 'new'
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_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
index 1a4047890..b750dbf4c 100644
--- a/app/views/admin_holidays/_edit_form.html.erb
+++ b/app/views/admin_holidays/_edit_form.html.erb
@@ -1,6 +1,6 @@
<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 } %>
+ <%= render :partial => 'form', :locals => { :f => f, :holiday => @holiday, :context => :edit } %>
<% end %>
<div class="holiday-destroy ">
diff --git a/app/views/admin_holidays/_form.html.erb b/app/views/admin_holidays/_form.html.erb
index f61c5fc61..35370e5fc 100644
--- a/app/views/admin_holidays/_form.html.erb
+++ b/app/views/admin_holidays/_form.html.erb
@@ -11,11 +11,12 @@
<div class="holiday-day">
<%= f.date_select :day, { :use_month_numbers => true }, { :class => "day_select" } %>
</div>
-
<div class="holiday-buttons">
- <%= link_to("Cancel", admin_holidays_path, :class => 'btn') %>
- <%= f.submit "Save", :class => 'btn btn-warning' %>
+ <% 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/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
index 1f76a7fcb..21cb51d29 100644
--- a/spec/controllers/admin_holidays_controller_spec.rb
+++ b/spec/controllers/admin_holidays_controller_spec.rb
@@ -51,7 +51,8 @@ describe AdminHolidaysController do
end
it 'creates a new holiday' do
- assigns[:holiday].should be_an_instance_of(Holiday)
+ get :new
+ assigns[:holiday].should be_instance_of(Holiday)
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