schedulable 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ MTg5Y2E4OWJkYzkwYTliN2UxZDE0NjRjMTNlMjViYWQ4MTA5NjIwNA==
5
+ data.tar.gz: !binary |-
6
+ YWNjZmE2NWE2ZjNjNmM4NjFhMmJiNDIyZTYxODcwNzQ4Yzc4NWFiMQ==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ NTY0Y2UyZmQ2N2IzYjk3NjU2NWM3YzQ1NjRkNmExNGJlMDVkYTFlZDY0OWYw
10
+ NzNmMzcxYWYwZjUyZTkwYmI1ZTVmMDVlMzgyMTIzZGU4ZmVhODFiZTM3NmRk
11
+ YjEyM2YwNTIxNGIyZjQ0OWM3ODA3NzhkMjM1MWJlOTk1YTZiYWE=
12
+ data.tar.gz: !binary |-
13
+ ZWVjNzcxM2I5ZjA5NjgzMzNkYmJjZjZmYWQzZDJjN2M5YWVjMTgyMzAwYmM0
14
+ MWY1MWQ0MmY5M2JhMzM2NDVlNzljZjEwNmFhNzhjNmIzMGExMThhMTRlMjEz
15
+ ZWI5ZWJkMzBiYjlmNDRmMTM1NjdiNmZhMzUxMTJmMzYxODQ5MzQ=
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2014 YOURNAME
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,175 @@
1
+ schedulable
2
+ ===========
3
+
4
+ Handling recurring events in rails.
5
+
6
+
7
+ The schedulable plugin depends on the ice_cube scheduling-library:
8
+ ```
9
+ gem 'ice_cube'
10
+ ```
11
+
12
+ Install schedule migration and model
13
+ ```
14
+ rails g schedulable:install
15
+ ```
16
+
17
+ ### Basic Usage
18
+
19
+ Create your event model
20
+ ```
21
+ rails g scaffold Event name:string
22
+ ```
23
+
24
+ Configure your model to be schedulable:
25
+ ```
26
+ # app/models/event.rb
27
+ class Event < ActiveRecord::Base
28
+ acts_as_schedulable
29
+ end
30
+ ```
31
+ This will add an association named 'schedule' that holds the schedule information.
32
+
33
+ Now you're ready to setup form fields for the schedule association using the fields_for-form_helper.
34
+
35
+ ### Attributes
36
+ The schedule object respects the following attributes:
37
+ <table>
38
+ <tr>
39
+ <th>Name</th><th>Type</th><th>Description</th>
40
+ </tr>
41
+ <tr>
42
+ <td>rule</td><td>String</td><td>One of 'singular', 'daily', 'weekly', 'monthly'</td>
43
+ </tr>
44
+ <tr>
45
+ <td>date</td><td>Date</td><td>The date-attribute is used for singular events and also as startdate of the schedule</td>
46
+ </tr>
47
+ <tr>
48
+ <td>time</td><td>Time</td><td>The time-attribute is used for singular events and also as starttime of the schedule</td>
49
+ </tr>
50
+ <tr>
51
+ <td>days</td><td>Array</td><td>An array of weekday-names, i.e. ['monday', 'wednesday']</td>
52
+ </tr>
53
+ <tr>
54
+ <td>day_of_week</td><td>Hash</td><td>A hash of weekday-names, containing arrays with indices, i.e. {:monday => [1, -1]} ('every first and last monday in month')</td>
55
+ </tr>
56
+ <tr>
57
+ <td>interval</td><td>Integer</td><td>Specifies the interval of the recurring rule, i.e. every two weeks</td>
58
+ </tr>
59
+ <tr>
60
+ <td>until</td><td>Date</td><td>Specifies the enddate of the schedule. Required for terminating events.</td>
61
+ </tr>
62
+ <tr>
63
+ <td>count</td><td>Integer</td><td>Specifies the total number of occurrences. Required for terminating events.</td>
64
+ </tr>
65
+ </table>
66
+
67
+ #### SimpleForm
68
+ A custom input for simple_form is provided with the plugin
69
+ ```
70
+ rails g schedulable:simple_form
71
+ ```
72
+
73
+ ```
74
+ -# app/views/events/_form.html.haml
75
+ .form-inputs
76
+ = f.input :name
77
+ = f.input :schedule, as: :schedule
78
+ ```
79
+
80
+ #### Strong parameters
81
+ ```
82
+ # app/controllers/event_controller.rb
83
+ def event_params
84
+ params.require(:event).permit(:name, schedule_attributes: Schedulable::ScheduleSupport.param_names)
85
+ end
86
+ ```
87
+
88
+ ### IceCube
89
+ The schedulable plugin uses ice_cube for calculating occurrences.
90
+ You can access ice_cube-methods via the schedule association:
91
+ ```
92
+ # prints all occurrences of the event until one year from now
93
+ puts @event.schedule.occurrences(Time.now + 1.year)
94
+ # export to ical
95
+ puts @event.schedule.to_ical
96
+ ```
97
+ See https://github.com/seejohnrun/ice_cube for more information.
98
+
99
+ ### Event occurrences
100
+ We need to have the occurrences persisted because we want to query the database for all occurrences of all instances of an event model or need to add additional attributes and functionality, such as allowing users to attend to a specific occurrence of an event.
101
+ The schedulable gem handles this for you.
102
+ Your occurrence model must include an attribute of type 'datetime' with name 'date' as well as a reference to your event model to setup up the association properly:
103
+
104
+ ```
105
+ rails g model EventOccurrence event_id:integer date:datetime
106
+ ```
107
+
108
+ ```
109
+ # app/models/event_occurrence.rb
110
+ class EventOccurrence < ActiveRecord::Base
111
+ belongs_to :event
112
+ end
113
+ ```
114
+
115
+ Then you can simply declare your occurrences with the acts_as_schedule-method like this:
116
+ ```
117
+ # app/models/event.rb
118
+ class Event < ActiveRecord::Base
119
+ acts_as_schedulable occurrences: :event_occurrences
120
+ end
121
+ ```
122
+ This will add a has_many-association with the name 'event_occurences' to your event-model.
123
+ Instances of remaining occurrences are built when the schedule is saved.
124
+ If the schedule has changed, the occurrences will be rebuilt if dates have also changed. Otherwise the time of the occurrence record will be adjusted to the new time.
125
+ As in real life, previous occurrences will always stay untouched.
126
+
127
+ #### Terminating and non-terminating events
128
+ An event is terminating if an until- or count-attribute has been specified.
129
+ Since non-terminating events have infinite occurrences, we cannot build all occurrences at once ;-)
130
+ So we need to limit the number of occurrences in the database.
131
+ By default this will be one year from now.
132
+ This can be configured via the 'build_max_count' and 'build_max_period'-options.
133
+ See notes on configuration.
134
+
135
+ #### Automate build of occurrences
136
+ Since we cannot build all occurrences at once, we will need a task that adds occurrences as time goes by.
137
+ Schedulable comes with a rake-task that performs an update on all scheduled occurrences.
138
+ ```
139
+ rake schedulable:build_occurrences
140
+ ```
141
+ You may add the task to crontab.
142
+ With the 'whenever' gem this can be easily achieved.
143
+ ```
144
+ gem 'whenever', :require => false
145
+ ```
146
+ Create the 'whenever'-configuration file:
147
+ ```
148
+ wheneverize .
149
+ ```
150
+ Open up the file 'config/schedule.rb' and add the job:
151
+ ```
152
+ set :environment, "development"
153
+ set :output, {:error => "log/cron_error_log.log", :standard => "log/cron_log.log"}
154
+
155
+ every 1.day do
156
+ rake "schedulable:build_occurrences"
157
+ end
158
+ ```
159
+ Write to the crontab:
160
+ ```
161
+ whenever -w
162
+ ```
163
+
164
+ ### Configuration
165
+ Generate the configuration file
166
+ ```
167
+ rails g schedulable:config
168
+ ```
169
+ Open 'config/initializers/schedulable.rb' and edit options as you need:
170
+ ```
171
+ Schedulable.configure do |config|
172
+ config.max_build_count = 0
173
+ config.max_build_period = 1.year
174
+ end
175
+ ```
data/Rakefile ADDED
@@ -0,0 +1,32 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'Schedulable'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.rdoc')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+
18
+
19
+
20
+ Bundler::GemHelper.install_tasks
21
+
22
+ require 'rake/testtask'
23
+
24
+ Rake::TestTask.new(:test) do |t|
25
+ t.libs << 'lib'
26
+ t.libs << 'test'
27
+ t.pattern = 'test/**/*_test.rb'
28
+ t.verbose = false
29
+ end
30
+
31
+
32
+ task default: :test
@@ -0,0 +1,32 @@
1
+ de:
2
+ model:
3
+ schedule: 'Zeitplan'
4
+
5
+ activerecord:
6
+ attributes:
7
+ schedule:
8
+ name: Name
9
+ rule: Regel
10
+ time: Zeit
11
+ date: Datum
12
+ days: Wochentage
13
+ day_of_week: "Wochentage"
14
+ interval: Intervall
15
+ until: Wiederholen bis
16
+ count: Anzahl Wiederholungen
17
+
18
+ schedulable:
19
+ monthly_week_names:
20
+ 1st: '1.'
21
+ 2nd: '2.'
22
+ 3rd: '3.'
23
+ 4th: '4.'
24
+ last: 'Letzte'
25
+ rules:
26
+ singular: Einmalig
27
+ monthly: Monatlich
28
+ weekly: Wöchentlich
29
+ daily: Täglich
30
+
31
+
32
+
@@ -0,0 +1,32 @@
1
+ en:
2
+ model:
3
+ schedule: 'Schedule'
4
+
5
+ activerecord:
6
+ attributes:
7
+ schedule:
8
+ name: Name
9
+ rule: Regel
10
+ time: Time
11
+ date: Date
12
+ rule: Rule
13
+ days: Weekdays
14
+ day_of_week: Weekdays
15
+ until: Repeat until
16
+ count: Repetition count
17
+
18
+ schedulable:
19
+ monthly_week_names:
20
+ 1st: '1st'
21
+ 2nd: '2nd'
22
+ 3rd: '3rd'
23
+ 4th: '4th'
24
+ last: 'Last'
25
+ rules:
26
+ singular: Singular
27
+ monthly: Monthly
28
+ weekly: Weekly
29
+ daily: Daily
30
+
31
+
32
+
@@ -0,0 +1,15 @@
1
+ module Schedulable
2
+ module Generators
3
+ class ConfigGenerator < ::Rails::Generators::Base
4
+
5
+ include Rails::Generators::Migration
6
+ source_root File.expand_path('../templates', __FILE__)
7
+
8
+ def create_config
9
+ puts 'install schedulable config'
10
+ template 'config/schedulable.rb', "config/initializers/schedulable.rb"
11
+ end
12
+
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,27 @@
1
+ module Schedulable
2
+ module Generators
3
+ class InstallGenerator < ::Rails::Generators::Base
4
+
5
+ # TODO: skip-migration
6
+
7
+ include Rails::Generators::Migration
8
+ source_root File.expand_path('../templates', __FILE__)
9
+
10
+ def self.next_migration_number(path)
11
+ unless @prev_migration_nr
12
+ @prev_migration_nr = Time.now.utc.strftime("%Y%m%d%H%M%S").to_i
13
+ else
14
+ @prev_migration_nr += 1
15
+ end
16
+ @prev_migration_nr.to_s
17
+ end
18
+
19
+ def create_migrations
20
+ puts 'install schedulable'
21
+ migration_template 'migrations/create_schedules.rb', "db/migrate/create_schedules.rb"
22
+ template 'models/schedule.rb', "app/models/schedule.rb"
23
+ end
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,16 @@
1
+ module Schedulable
2
+ module Generators
3
+ class LocaleGenerator < ::Rails::Generators::Base
4
+
5
+ argument :locale, :type => :string, :default => "en"
6
+
7
+ source_root File.expand_path('../../../../../config/locales', __FILE__)
8
+
9
+ def create_locale
10
+ puts 'install locale'
11
+ template "schedulable.#{locale}.yml", "config/locales/schedulable.#{locale}.yml"
12
+ end
13
+
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,27 @@
1
+ require 'active_support/core_ext/module/introspection'
2
+ require 'rails/generators/base'
3
+ require 'rails/generators/generated_attribute'
4
+
5
+ module Schedulable
6
+ module Generators
7
+ class OccurrenceGenerator < ::Rails::Generators::NamedBase
8
+
9
+ argument :attributes, type: :array, default: [], banner: "field[:type][:index] field[:type][:index]"
10
+
11
+ include Rails::Generators::Migration
12
+ source_root File.expand_path('../templates', __FILE__)
13
+
14
+ def self.next_migration_number(path)
15
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
16
+ end
17
+
18
+ def create_migrations
19
+ puts 'create schedulable occurrence model'
20
+ migration_template 'migrations/create_occurrences.erb', "db/migrate/create_#{name.tableize}.rb", {name: self.name, attributes: self.attributes}
21
+ template 'models/occurrence.erb', "app/models/#{name.tableize.singularize}.rb", {name: self.name, attributes: self.attributes}
22
+ end
23
+
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,15 @@
1
+ module Schedulable
2
+ module Generators
3
+ class SimpleFormGenerator < ::Rails::Generators::Base
4
+
5
+ include Rails::Generators::Migration
6
+ source_root File.expand_path('../templates', __FILE__)
7
+
8
+ def create_config
9
+ puts 'install simple_form custom input'
10
+ template 'inputs/schedule_input.rb', "app/inputs/schedule_input.rb"
11
+ end
12
+
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,4 @@
1
+ Schedulable.configure do |config|
2
+ config.max_build_count = 0
3
+ config.max_build_period = 1.year
4
+ end
@@ -0,0 +1,102 @@
1
+ class ScheduleInput < SimpleForm::Inputs::Base
2
+
3
+
4
+ def input
5
+
6
+ input_html_options[:type] ||= input_type if html5?
7
+
8
+ # options
9
+ input_options[:interval] = !input_options[:interval].nil? ? input_options[:interval] : true
10
+ input_options[:until] = !input_options[:until].nil? ? input_options[:until] : true
11
+ input_options[:count] = !input_options[:count].nil? ? input_options[:count] : true
12
+
13
+
14
+ @builder.simple_fields_for(:schedule, @builder.object.schedule || @builder.object.build_schedule) do |b|
15
+
16
+ b.template.content_tag("div", {id: b.object_name.to_s.gsub(/\]\[|[^-a-zA-Z0-9:.]/,"_").sub(/_$/,"")}) do
17
+
18
+ b.input(:rule, collection: ['singular', 'daily', 'weekly', 'monthly'], label_method: lambda { |i| i.capitalize }, label: false) <<
19
+
20
+ template.content_tag("div", {data: {group: 'singular'}}) do
21
+ b.input :date
22
+ end <<
23
+
24
+ template.content_tag("div", {data: {group: 'weekly'}}) do
25
+ b.input :days, collection: Time::DAYS_INTO_WEEK.invert.values, label_method: lambda { |v| v.capitalize }, as: :check_boxes
26
+ end <<
27
+
28
+ template.content_tag("div", {data: {group: 'monthly'}}) do
29
+ b.simple_fields_for :day_of_week, OpenStruct.new(b.object.day_of_week || {}) do |db|
30
+
31
+ template.content_tag("div", class: 'form-group') do
32
+
33
+ db.label(:day_of_week, required: false) <<
34
+
35
+ template.content_tag("table", style: 'min-width: 280px') do
36
+ template.content_tag("tr") do
37
+ template.content_tag("td") <<
38
+ ['1.', '2.', '3.', '4.', 'Last'].reduce(''.html_safe) { | x, item |
39
+ x << template.content_tag("td") do
40
+ db.label(item, required: false)
41
+ end
42
+ }
43
+ end <<
44
+ Time::DAYS_INTO_WEEK.invert.values.reduce(''.html_safe) { | x, weekday |
45
+ x << template.content_tag("tr") do
46
+ template.content_tag("td") do
47
+ db.label t(weekday.capitalize), required: false
48
+ end <<
49
+ db.collection_check_boxes(weekday.to_sym, [1, 2, 3, 4, -1], lambda { |i| i} , lambda { |i| "&nbsp;".html_safe}, item_wrapper_tag: :td, checked: db.object.send(weekday))
50
+ end
51
+ }
52
+ end
53
+ end
54
+ end
55
+ end <<
56
+
57
+ template.content_tag("div", {data: {group: 'singular,daily,weekly,monthly'}}) do
58
+ b.input :time
59
+ end <<
60
+
61
+ (template.content_tag("div", {data: {group: 'daily,weekly,monthly'}}) do
62
+ b.input :interval
63
+ end if input_options[:interval]) <<
64
+
65
+ (template.content_tag("div", {data: {group: 'daily,weekly,monthly'}}) do
66
+ b.input :until
67
+ end if input_options[:until]) <<
68
+
69
+ (template.content_tag("div", {data: {group: 'daily,weekly,monthly'}}) do
70
+ b.input :count
71
+ end if input_options[:count])
72
+
73
+
74
+
75
+ end <<
76
+
77
+ template.javascript_tag(
78
+ "(function() {" <<
79
+ " var container = $(\"*[id='#{b.object_name.to_s.gsub(/\]\[|[^-a-zA-Z0-9:.]/,"_").sub(/_$/,"")}']\");" <<
80
+ " var select = container.find(\"select[name*='rule']\");" <<
81
+ " function update() {" <<
82
+ " var value = this.value;" <<
83
+ " container.find(\"*[data-group]\").each(function() {" <<
84
+ " var groups = $(this).data('group').split(',');" <<
85
+ " if ($.inArray(value, groups) >= 0) {" <<
86
+ " $(this).css('display', '');" <<
87
+ " } else {" <<
88
+ " $(this).css('display', 'none');" <<
89
+ " }" <<
90
+ " });" <<
91
+ " }" <<
92
+ " select.on('change', update);" <<
93
+ " update.call(select[0]);" <<
94
+ "})()"
95
+ )
96
+
97
+
98
+ end
99
+
100
+
101
+ end
102
+ end
@@ -0,0 +1,18 @@
1
+ class Create<%= name.pluralize %> < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :<%= name.tableize %> do |t|
4
+ <% attributes.each do |attribute| %>
5
+ t.<%= attribute.type %> :<%= attribute.name %>
6
+ <% end %>
7
+ t.references :schedulable, polymorphic: true
8
+ t.datetime :date
9
+
10
+ t.timestamps
11
+
12
+ end
13
+ end
14
+
15
+ def self.down
16
+ drop_table :<%= name.tableize %>
17
+ end
18
+ end
@@ -0,0 +1,25 @@
1
+ class CreateSchedules < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :schedules do |t|
4
+ t.references :schedulable, polymorphic: true
5
+
6
+ t.date :date
7
+ t.time :time
8
+
9
+ t.string :rule
10
+ t.string :interval
11
+
12
+ t.text :days
13
+ t.text :day_of_week
14
+
15
+ t.date :until
16
+ t.integer :count
17
+
18
+ t.timestamps
19
+ end
20
+ end
21
+
22
+ def self.down
23
+ drop_table :schedules
24
+ end
25
+ end
@@ -0,0 +1,6 @@
1
+ class <%= name %> < ActiveRecord::Base
2
+ belongs_to :schedulable, polymorphic: true
3
+ default_scope :order => 'date ASC'
4
+ scope :remaining, lambda{where(["date >= ?",Time.now])}
5
+ scope :previous, lambda{where(["date < ?",Time.now])}
6
+ end
@@ -0,0 +1,13 @@
1
+ class Schedule < ActiveRecord::Base
2
+
3
+ include Schedulable::ScheduleSupport
4
+
5
+ serialize :days
6
+ serialize :day_of_week, Hash
7
+
8
+ belongs_to :schedulable, polymorphic: true
9
+
10
+ after_initialize :init_schedule
11
+ after_save :init_schedule
12
+
13
+ end
@@ -0,0 +1,20 @@
1
+ require 'schedulable/railtie.rb' if defined? ::Rails::Railtie
2
+ require 'schedulable/acts_as_schedulable.rb'
3
+ require 'schedulable/schedule_support.rb'
4
+ require 'i18n'
5
+
6
+ module Schedulable
7
+
8
+ class Config
9
+ attr_accessor :max_build_count, :max_build_period
10
+ end
11
+
12
+ def self.config
13
+ @@config ||= Config.new
14
+ end
15
+
16
+ def self.configure
17
+ yield self.config
18
+ end
19
+
20
+ end
@@ -0,0 +1,175 @@
1
+ module Schedulable
2
+
3
+ module ActsAsSchedulable
4
+
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ end
9
+
10
+ module ClassMethods
11
+
12
+ def acts_as_schedulable(options = {})
13
+
14
+ name = options[:name] || :schedule
15
+ attribute = :date
16
+
17
+ has_one name, as: :schedulable, dependent: :destroy
18
+ accepts_nested_attributes_for name
19
+
20
+ if options[:occurrences]
21
+
22
+ # setup association
23
+ if options[:occurrences].is_a?(String) || options[:occurrences].is_a?(Symbol)
24
+ occurrences_association = options[:occurrences].to_sym
25
+ options[:occurrences] = {}
26
+ else
27
+ occurrences_association = options[:occurrences][:name]
28
+ options[:occurrences].delete(:name)
29
+ end
30
+ options[:occurrences][:class_name] = occurrences_association.to_s.classify
31
+ options[:occurrences][:as]||= :schedulable
32
+ options[:occurrences][:dependent]||:destroy
33
+ options[:occurrences][:autosave]||= true
34
+
35
+ has_many occurrences_association, options[:occurrences]
36
+
37
+ # remaining
38
+ remaining_occurrences_options = options[:occurrences].clone
39
+ remaining_occurrences_association = ("remaining_" << occurrences_association.to_s).to_sym
40
+ has_many remaining_occurrences_association, -> { where "date >= ?", Time.now}, remaining_occurrences_options
41
+
42
+ # previous
43
+ previous_occurrences_options = options[:occurrences].clone
44
+ previous_occurrences_association = ("previous_" << occurrences_association.to_s).to_sym
45
+ has_many previous_occurrences_association, -> { where "date < ?", Time.now}, previous_occurrences_options
46
+
47
+ ActsAsSchedulable.add_occurrences_association(self, occurrences_association)
48
+
49
+ after_save "build_#{occurrences_association}"
50
+
51
+ self.class.instance_eval do
52
+ define_method("build_#{occurrences_association}") do
53
+ # build occurrences for all events
54
+ # TODO: only invalid events
55
+ schedulables = self.all
56
+ puts "build occurrences for #{schedulables.length} #{self.name.tableize}"
57
+ schedulables.each do |schedulable|
58
+ schedulable.send("build_#{occurrences_association}")
59
+ end
60
+ end
61
+ end
62
+
63
+ define_method "build_#{occurrences_association}" do
64
+
65
+ # build occurrences for events
66
+
67
+ schedule = self.send(name)
68
+
69
+ now = Time.now
70
+ occurrence_attribute = :date
71
+
72
+ schedulable = schedule.schedulable
73
+ terminating = schedule.until.present? || schedule.count.present? && schedule.count > 0
74
+
75
+ max_build_period = Schedulable.config.max_build_period || 1.year
76
+ max_date = now + max_build_period
77
+ max_date = terminating ? [max_date, schedule.last.to_time].min : max_date
78
+
79
+ max_build_count = Schedulable.config.max_build_count || 0
80
+ max_build_count = terminating ? [max_build_count, schedule.remaining_occurrences.count].min : max_build_count
81
+
82
+ # get occurrences
83
+ if max_build_count > 0
84
+ # get next occurrences for max_build_count
85
+ occurrences = schedule.next_occurrences(max_build_count)
86
+ end
87
+
88
+ if !occurrences || occurrences.last && occurrences.last.to_time > max_date
89
+ # get next occurrences for max_date
90
+ all_occurrences = schedule.occurrences(max_date)
91
+ occurrences = []
92
+ # filter future dates
93
+ all_occurrences.each do |occurrence_date|
94
+ if occurrence_date.to_time > now
95
+ occurrences << occurrence_date
96
+ end
97
+ end
98
+ end
99
+
100
+
101
+ puts 'build occurrences'
102
+
103
+ # build occurrences
104
+ assocs = schedulable.class.reflect_on_all_associations(:has_many)
105
+ assocs.each do |assoc|
106
+ puts assoc.name
107
+ end
108
+
109
+ occurrences_records = schedulable.send(occurrences_association)
110
+
111
+ # clean up unused remaining occurrences
112
+ record_count = 0
113
+ occurrences_records.each do |occurrence_record|
114
+ if occurrence_record.date > now
115
+ # destroy occurrence if it's not used anymore
116
+ if !schedule.occurs_on?(occurrence_record.date) || occurrence_record.date > max_date || record_count > max_build_count
117
+ if occurrences_records.destroy(occurrence_record)
118
+ puts 'an error occurred while destroying an unused occurrence record'
119
+ end
120
+ end
121
+ record_count = record_count + 1
122
+ end
123
+ end
124
+
125
+ # build occurrences
126
+ occurrences.each do |occurrence|
127
+
128
+ # filter existing occurrence records
129
+ existing = occurrences_records.select { |record|
130
+ record.date.to_date == occurrence.to_date
131
+ }
132
+ if existing.length > 0
133
+ # a record for this date already exists, adjust time
134
+ existing.each { |record|
135
+ #record.date = occurrence.to_datetime
136
+ if !occurrences_records.update(record, date: occurrence.to_datetime)
137
+ puts 'an error occurred while saving an existing occurrence record'
138
+ end
139
+ }
140
+ else
141
+ # create new record
142
+ if !occurrences_records.create(date: occurrence.to_datetime)
143
+ puts 'an error occurred while creating an occurrence record'
144
+ end
145
+ end
146
+ end
147
+
148
+ end
149
+
150
+ end
151
+
152
+ end
153
+
154
+ end
155
+
156
+ def self.occurrences_associations_for(clazz)
157
+ @@schedulable_occurrences||= []
158
+ @@schedulable_occurrences.select { |item|
159
+ item[:class] == clazz
160
+ }.map { |item|
161
+ item[:name]
162
+ }
163
+ end
164
+
165
+ private
166
+
167
+ def self.add_occurrences_association(clazz, name)
168
+ @@schedulable_occurrences||= []
169
+ @@schedulable_occurrences << {class: clazz, name: name}
170
+ end
171
+
172
+
173
+ end
174
+ end
175
+ ActiveRecord::Base.send :include, Schedulable::ActsAsSchedulable
@@ -0,0 +1,26 @@
1
+ require 'rails'
2
+ module Schedulable
3
+ class Railtie < ::Rails::Railtie
4
+
5
+ railtie_name :schedulable
6
+
7
+ # application configuration initializer
8
+ config.schedulable = ActiveSupport::OrderedOptions.new # enable namespaced configuration in Rails environments
9
+
10
+ initializer "schedulable.configure" do |app|
11
+ Schedulable.configure do |config|
12
+
13
+ # copy parameters from application configuration
14
+ config.max_build_count = app.config.schedulable[:max_build_count]
15
+ config.max_build_period = app.config.schedulable[:max_build_period]
16
+ end
17
+ end
18
+
19
+
20
+ # rake tasks
21
+ rake_tasks do
22
+ load "tasks/schedulable_tasks.rake"
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,69 @@
1
+ module Schedulable
2
+
3
+ module ScheduleSupport
4
+
5
+ def to_icecube
6
+ return @schedule
7
+ end
8
+
9
+ def to_s
10
+ return @schedule.to_s
11
+ end
12
+
13
+ def method_missing(meth, *args, &block)
14
+ if @schedule
15
+ @schedule.send(meth, *args, &block)
16
+ end
17
+ end
18
+
19
+ def self.param_names
20
+ [:id, :date, :time, :rule, :until, :count, :interval, days: [], day_of_week: [monday: [], tuesday: [], wednesday: [], thursday: [], friday: [], saturday: [], sunday: []]]
21
+ end
22
+
23
+ private
24
+
25
+ def init_schedule()
26
+
27
+ self.rule||= "singular"
28
+ self.interval||= 1
29
+
30
+ date = self.date ? self.date.to_time : Time.now
31
+ if self.time
32
+ date = date.change({hour: self.time.hour, min: self.time.min})
33
+ end
34
+
35
+ @schedule = IceCube::Schedule.new(date)
36
+
37
+ if self.rule && self.rule != 'singular'
38
+
39
+ self.interval = self.interval.present? ? self.interval.to_i : 1
40
+
41
+ rule = IceCube::Rule.send("#{self.rule}", self.interval)
42
+
43
+ if self.until
44
+ rule.until(self.until)
45
+ end
46
+
47
+ if self.count && self.count.to_i > 0
48
+ rule.count(self.count.to_i)
49
+ end
50
+
51
+ if self.days
52
+ days = self.days.reject(&:empty?)
53
+ if self.rule == 'weekly'
54
+ days.each do |day|
55
+ rule.day(day.to_sym)
56
+ end
57
+ elsif self.rule == 'monthly'
58
+ days = {}
59
+ day_of_week.each do |weekday, value|
60
+ days[weekday.to_sym] = value.reject(&:empty?).map { |x| x.to_i }
61
+ end
62
+ rule.day_of_week(days)
63
+ end
64
+ end
65
+ @schedule.add_recurrence_rule(rule)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,3 @@
1
+ module Schedulable
2
+ VERSION = "0.0.2"
3
+ end
@@ -0,0 +1,13 @@
1
+ require 'rake'
2
+ desc 'builds occurrences for schedulable models'
3
+ namespace :schedulable do
4
+ task :build_occurrences => :environment do
5
+ Schedule.all(group: :schedulable_type).each do |schedule|
6
+ clazz = schedule.schedulable.class
7
+ occurrences_associations = Schedulable::ActsAsSchedulable.occurrences_associations_for(clazz)
8
+ occurrences_associations.each do |association|
9
+ clazz.send("build_" + association.to_s)
10
+ end
11
+ end
12
+ end
13
+ end
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: schedulable
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Rafael Nowrotek
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-07-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: 4.0.3
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: 4.0.3
27
+ - !ruby/object:Gem::Dependency
28
+ name: ice_cube
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ! '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: Handling recurring events in rails.
42
+ email:
43
+ - mail@benignware.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - config/locales/schedulable.de.yml
49
+ - config/locales/schedulable.en.yml
50
+ - lib/generators/schedulable/config_generator.rb
51
+ - lib/generators/schedulable/install_generator.rb
52
+ - lib/generators/schedulable/locale_generator.rb
53
+ - lib/generators/schedulable/occurrence_generator.rb
54
+ - lib/generators/schedulable/simple_form_generator.rb
55
+ - lib/generators/schedulable/templates/config/schedulable.rb
56
+ - lib/generators/schedulable/templates/inputs/schedule_input.rb
57
+ - lib/generators/schedulable/templates/migrations/create_occurrences.erb
58
+ - lib/generators/schedulable/templates/migrations/create_schedules.rb
59
+ - lib/generators/schedulable/templates/models/occurrence.erb
60
+ - lib/generators/schedulable/templates/models/schedule.rb
61
+ - lib/schedulable/acts_as_schedulable.rb
62
+ - lib/schedulable/railtie.rb
63
+ - lib/schedulable/schedule_support.rb
64
+ - lib/schedulable/version.rb
65
+ - lib/schedulable.rb
66
+ - lib/tasks/schedulable_tasks.rake
67
+ - MIT-LICENSE
68
+ - Rakefile
69
+ - README.md
70
+ homepage: http://github.com/benignware
71
+ licenses: []
72
+ metadata: {}
73
+ post_install_message:
74
+ rdoc_options: []
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ! '>='
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ! '>='
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ requirements: []
88
+ rubyforge_project:
89
+ rubygems_version: 2.1.11
90
+ signing_key:
91
+ specification_version: 4
92
+ summary: Handling recurring events in rails.
93
+ test_files: []