schedulable 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/MIT-LICENSE +20 -0
- data/README.md +175 -0
- data/Rakefile +32 -0
- data/config/locales/schedulable.de.yml +32 -0
- data/config/locales/schedulable.en.yml +32 -0
- data/lib/generators/schedulable/config_generator.rb +15 -0
- data/lib/generators/schedulable/install_generator.rb +27 -0
- data/lib/generators/schedulable/locale_generator.rb +16 -0
- data/lib/generators/schedulable/occurrence_generator.rb +27 -0
- data/lib/generators/schedulable/simple_form_generator.rb +15 -0
- data/lib/generators/schedulable/templates/config/schedulable.rb +4 -0
- data/lib/generators/schedulable/templates/inputs/schedule_input.rb +102 -0
- data/lib/generators/schedulable/templates/migrations/create_occurrences.erb +18 -0
- data/lib/generators/schedulable/templates/migrations/create_schedules.rb +25 -0
- data/lib/generators/schedulable/templates/models/occurrence.erb +6 -0
- data/lib/generators/schedulable/templates/models/schedule.rb +13 -0
- data/lib/schedulable.rb +20 -0
- data/lib/schedulable/acts_as_schedulable.rb +175 -0
- data/lib/schedulable/railtie.rb +26 -0
- data/lib/schedulable/schedule_support.rb +69 -0
- data/lib/schedulable/version.rb +3 -0
- data/lib/tasks/schedulable_tasks.rake +13 -0
- metadata +93 -0
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,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| " ".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
|
data/lib/schedulable.rb
ADDED
@@ -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,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: []
|