Handling recurring events in rails.
Put the following into your Gemfile and run bundle install
gem 'ice_cube'
gem 'schedulable'
Install schedule migration and model
rails g schedulable:install
Create an event model
rails g scaffold Event name:string
Don't forget to migrate your database at this point in order to reflect the changes: rake db:migrate
Open app/models/event.rb
and add the following to configure your model to be schedulable:
# app/models/event.rb
class Event < ActiveRecord::Base
acts_as_schedulable :schedule
end
This will add an association to the model named 'schedule' which holds the schedule information.
The schedule-object respects the following attributes.
Name | Type | Description |
---|---|---|
rule | String | One of 'singular', 'daily', 'weekly', 'monthly' |
date | Date | The date-attribute is used for singular events and also as startdate of the schedule |
time | Time | The time-attribute is used for singular events and also as starttime of the schedule |
day | Array | Day of week. An array of weekday-names, i.e. ['monday', 'wednesday'] |
day_of_week | Hash | Day of nth week. A hash of weekday-names, containing arrays with indices, i.e. {:monday => [1, -1]} ('every first and last monday in month') |
interval | Integer | Specifies the interval of the recurring rule, i.e. every two weeks |
until | Date | Specifies the enddate of the schedule. Required for terminating events. |
count | Integer | Specifies the total number of occurrences. Required for terminating events. |
Use schedulable's built-in helpers to setup your form.
Schedulable extends FormBuilder with a 'schedule_select'-helper and should therefore seamlessly integrate it with your existing views:
<%# app/views/events/_form.html.erb %>
<%= form_for(@event) do |f| %>
<div class="field">
<%= f.label :name %><br>
<%= f.text_field :name %>
</div>
<div class="field">
<%= f.label :schedule %><br>
<%= f.schedule_select :schedule %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
You can customize the generated markup by providing a hash of html-attributes as style
-option. For wrappers, also provide a tag
-attribute.
- field_html
- input_html
- input_wrapper
- label_html
- label_wrapper
- number_field_html
- number_field_wrapper
- date_select_html
- date_select_wrapper
- collection_select_html
- collection_select_wrapper
- collection_check_boxes_item_html
- collection_check_boxes_item_wrapper
The schedulable-formhelper has built-in-support for Bootstrap. Simply point the style-option of schedule_input to bootstrap
or set it as default in config.
<%= f.schedule_select :schedule, style: :bootstrap %>
Customize datetime-controls by passing FormBuilder-methods as a hash to the input_types
-option.
See below example for integrating date_picker with schedulable:
<%# app/views/events/_form.html.erb %>
<div class="field form-group">
<%= f.label :schedule %><br>
<%= f.schedule_select :schedule, style: :bootstrap, until: true, input_types: {date: :date_picker, time: :time_picker, datetime: :datetime_picker} %>
</div>
Name | Type | Description |
---|---|---|
count | Boolean | Specifies whether to show 'count'-field |
input_types | Hash | Specify a hash containing custom form builder methods. Defaults to `{date: :date_select, time: :time_select, datetime: :datetime_select}`. The interface of the custom form builder method must either match `date_select` or `date_field`-methods |
interval | Boolean | Specifies whether to show 'interval'-field |
style | Hash | Specifies a hash of options to customize markup. By providing a string, you can point to a prefined set of options. Built-in styles are :bootstrap and :default. |
until | Boolean | Specifies whether to show 'until'-field |
Also provided with the plugin is a custom input for simple_form. Make sure, you installed SimpleForm and executed rails generate simple_form:install
.
rails g schedulable:simple_form
<%# app/views/events/_form.html.erb %>
<%= simple_form_for(@event) do |f| %>
<div class="field">
<%= f.label :name %><br>
<%= f.text_field :name %>
</div>
<div class="field">
<%= f.label :schedule %><br>
<%= f.input :schedule, as: :schedule %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
Simple Form has built-in support for Bootstrap as of version 3.0.0. At time of writing it requires some a little extra portion of configuration to make it look as expected:
# config/initializers/simple_form_bootstrap.rb
# Inline date_select-wrapper for Bootstrap
config.wrappers :horizontal_select_date, tag: 'div', class: 'form-group', error_class: 'has-error' do |b|
b.use :html5
b.optional :readonly
b.use :label, class: 'control-label'
b.wrapper tag: 'div', class: 'form-inline' do |ba|
ba.use :input, class: 'form-control'
ba.use :error, wrap_with: { tag: 'span', class: 'help-block' }
ba.use :hint, wrap_with: { tag: 'p', class: 'help-block' }
end
end
# Include date_select-wrapper in mappings
config.wrapper_mappings = {
datetime: :horizontal_select_date,
date: :horizontal_select_date,
time: :horizontal_select_date
}
You can customize datetime-controls by passing simple_form-inputs as a hash to the input_types
-option. See form helper for similar example
For integrating date_picker with schedulable and simple_form, you can also run date_picker's simple_form-generator to create a default date_time
-input:
rails g date_picker:simple_form date_time
Name | Type | Description |
---|---|---|
count | Boolean | Specifies whether to show 'count'-field |
input_types | Hash | Specify a hash containing custom simple form inputs. Defaults to `{date: :date_time, time: :date_time, datetime: :date_time}`. |
interval | Boolean | Specifies whether to show 'interval'-field |
until | Boolean | Specifies whether to show 'until'-field |
Add schedule-attributes to the list of strong parameters in your controller:
# app/controllers/event_controller.rb
def event_params
params.require(:event).permit(:name, schedule_attributes: Schedulable::ScheduleSupport.param_names)
end
Note: If you don't use schedule
as attribute name, you need to rename schedule_attributes
accordingly.
You can access ice_cube-methods directly via the schedule association:
<%# app/views/events/show.html.erb %>
<p>
<strong>Schedule:</strong>
<%# Prints out a human-friendly description of the schedule, such as %>
<%= @event.schedule %>
</p>
# Prints all occurrences of the event until one year from now
puts @event.schedule.occurrences(Time.now + 1.year)
# Export to ical
puts @event.schedule.to_ical
See IceCube for more information.
Schedulable is bundled with translations in english and german which will be automatically initialized with your app. You can customize these messages by running the locale generator and edit the created yml-files:
rails g schedulable:locale de
Appropriate datetime translations should be included. Basic setup for many languages can be found here: https://github.com/svenfuchs/rails-i18n/tree/master/rails/locale.
An internationalization-branch of ice_cube can be found here: https://github.com/joelmeyerhamme/ice_cube:
gem 'ice_cube', git: 'git://github.com/joelmeyerhamme/ice_cube.git', branch: 'international'
Schedulable allows for persisting occurrences and associate them with your event model. The Occurrence Model model must include an attribute of type 'datetime' with name 'date' as well as a polymorphic association to the schedulable event model. Simply use the occurrence generator for setting up the appropriate model:
rails g schedulable:occurrence EventOccurrence
On the other side, pass the association to your schedulable event model by using the occurrences
-option of the acts_as_schedulable
-method:
# app/models/event.rb
class Event < ActiveRecord::Base
acts_as_schedulable :schedule, occurrences: :event_occurrences
end
This will create the corresponding has_many-association and also add remaining_event_occurrences
and previous_event_occurrences
-methods.
Instances of remaining occurrences are persisted when the parent-model is saved. Occurrences records will be reused if their datetime matches the saved schedule. Past occurrences stay untouched.
An event is terminating if an until- or count-attribute has been specified. Since non-terminating events have infinite occurrences, we cannot build all occurrences at once ;-) So we need to limit the number of occurrences in the database. By default this will be one year from now. This can be configured via the 'build_max_count' and 'build_max_period'-options. See notes on configuration.
Since we cannot build an infinite amount of occurrences, we will need a task that adds occurrences as time goes by. Schedulable comes with a rake-task that performs an update on all scheduled occurrences.
rake schedulable:build_occurrences
You may add this task to crontab.
With the 'whenever' gem this can be easily achieved.
gem 'whenever', :require => false
Generate the 'whenever'-configuration file:
wheneverize .
Open up the file 'config/schedule.rb' and add the job:
# config/schedule.rb
set :environment, "development"
set :output, {:error => "log/cron_error_log.log", :standard => "log/cron_log.log"}
every 1.day do
rake "schedulable:build_occurrences"
end
Write to crontab:
whenever -w
Generate the configuration file
rails g schedulable:config
Open 'config/initializers/schedulable.rb' and edit options as needed:
Schedulable.configure do |config|
# Build occurrences
config.max_build_count = 0
config.max_build_period = 1.year
# SimpleForm
config.simple_form = {
input_types: {
date: :date_time,
time: :date_time,
datetime: :date_time
}
}
# Generic Form helper
config.form_helper = {
style: :default
}
end
See the Changelog for recent enhancements, bugfixes and deprecations.