schedule_attributes 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/.rspec +1 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +57 -0
- data/README.markdown +69 -0
- data/Rakefile +7 -0
- data/lib/schedule_attributes.rb +18 -0
- data/lib/schedule_attributes/active_record.rb +50 -0
- data/lib/schedule_attributes/configuration.rb +22 -0
- data/lib/schedule_attributes/core.rb +144 -0
- data/lib/schedule_attributes/extensions/ice_cube.rb +11 -0
- data/lib/schedule_attributes/form_builder.rb +42 -0
- data/lib/schedule_attributes/input.rb +151 -0
- data/lib/schedule_attributes/model.rb +24 -0
- data/lib/schedule_attributes/railtie.rb +13 -0
- data/lib/schedule_attributes/rule_parser.rb +25 -0
- data/lib/schedule_attributes/rule_parser/base.rb +96 -0
- data/lib/schedule_attributes/rule_parser/day.rb +13 -0
- data/lib/schedule_attributes/rule_parser/month.rb +40 -0
- data/lib/schedule_attributes/rule_parser/week.rb +18 -0
- data/lib/schedule_attributes/rule_parser/year.rb +21 -0
- data/lib/schedule_attributes/serializer.rb +30 -0
- data/lib/schedule_attributes/time_helpers.rb +31 -0
- data/lib/schedule_attributes/version.rb +3 -0
- data/schedule_attributes.gemspec +28 -0
- data/spec/active_record_integration_spec.rb +45 -0
- data/spec/schedule_attributes/configuration_spec.rb +25 -0
- data/spec/schedule_attributes/input_spec.rb +174 -0
- data/spec/schedule_attributes/rule_parser/day_spec.rb +113 -0
- data/spec/schedule_attributes/rule_parser/month_spec.rb +47 -0
- data/spec/schedule_attributes/rule_parser/week_spec.rb +36 -0
- data/spec/schedule_attributes/rule_parser/year_spec.rb +53 -0
- data/spec/schedule_attributes/rule_parser_spec.rb +18 -0
- data/spec/schedule_attributes/time_helpers_spec.rb +39 -0
- data/spec/schedule_attributes_spec.rb +219 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/support/parser_macros.rb +27 -0
- data/spec/support/scheduled_active_record_model.rb +42 -0
- data/spec/support/scheduled_model.rb +16 -0
- metadata +218 -0
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
schedule_attributes (0.3.0)
|
5
|
+
activesupport
|
6
|
+
ice_cube (>= 0.10.0)
|
7
|
+
tzinfo
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: http://rubygems.org/
|
11
|
+
specs:
|
12
|
+
activemodel (3.2.13)
|
13
|
+
activesupport (= 3.2.13)
|
14
|
+
builder (~> 3.0.0)
|
15
|
+
activerecord (3.2.13)
|
16
|
+
activemodel (= 3.2.13)
|
17
|
+
activesupport (= 3.2.13)
|
18
|
+
arel (~> 3.0.2)
|
19
|
+
tzinfo (~> 0.3.29)
|
20
|
+
activesupport (3.2.13)
|
21
|
+
i18n (= 0.6.1)
|
22
|
+
multi_json (~> 1.0)
|
23
|
+
arel (3.0.2)
|
24
|
+
builder (3.0.4)
|
25
|
+
coderay (1.0.9)
|
26
|
+
diff-lcs (1.2.4)
|
27
|
+
i18n (0.6.1)
|
28
|
+
ice_cube (0.10.0)
|
29
|
+
method_source (0.8.1)
|
30
|
+
multi_json (1.7.3)
|
31
|
+
pry (0.9.12.1)
|
32
|
+
coderay (~> 1.0.5)
|
33
|
+
method_source (~> 0.8)
|
34
|
+
slop (~> 3.4)
|
35
|
+
rake (10.0.4)
|
36
|
+
rspec (2.13.0)
|
37
|
+
rspec-core (~> 2.13.0)
|
38
|
+
rspec-expectations (~> 2.13.0)
|
39
|
+
rspec-mocks (~> 2.13.0)
|
40
|
+
rspec-core (2.13.1)
|
41
|
+
rspec-expectations (2.13.0)
|
42
|
+
diff-lcs (>= 1.1.3, < 2.0)
|
43
|
+
rspec-mocks (2.13.1)
|
44
|
+
slop (3.4.4)
|
45
|
+
sqlite3 (1.3.7)
|
46
|
+
tzinfo (0.3.37)
|
47
|
+
|
48
|
+
PLATFORMS
|
49
|
+
ruby
|
50
|
+
|
51
|
+
DEPENDENCIES
|
52
|
+
activerecord
|
53
|
+
pry
|
54
|
+
rake
|
55
|
+
rspec
|
56
|
+
schedule_attributes!
|
57
|
+
sqlite3
|
data/README.markdown
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
# Schedule Attributes
|
2
|
+
|
3
|
+
Schedule Attributes allows models (ORM agnostic) to accept recurring schedule form parameters and translate them into an [IceCube](https://github.com/seejohnrun/ice_cube/) `Schedule` object. Schedule Attributes adds `#schedule_attributes` and `#schedule_attributes=` methods that let your model automatically populate Rails forms and receive HTML form parameters. Additionally, it provides access to the `IceCube::Schedule` object itself.
|
4
|
+
|
5
|
+
**Note**: This is a fork of [theunraveler/Schedule-Attributes](https://github.com/theunraveler/Schedule-Attributes), adding support for some extra rules.
|
6
|
+
|
7
|
+
## Usage
|
8
|
+
|
9
|
+
To use, include the `ScheduleAttributes` module in your model class.
|
10
|
+
|
11
|
+
class SomeModel
|
12
|
+
include ScheduleAttributes
|
13
|
+
end
|
14
|
+
|
15
|
+
Your model must respond to `:schedule_yaml` and `:schedule_yaml=`, because ScheduleAttributes will serialize and deserialize the schedule in YAML using this column. If you are using ActiveRecord, make a string column called `schedule_yaml`. If you're using Mongoid, make a string field like so: `field :schedule_yaml`.
|
16
|
+
|
17
|
+
You model will gain the following methods:
|
18
|
+
|
19
|
+
## `schedule_attributes=`
|
20
|
+
|
21
|
+
Accepts form a parameter hash that represent a schedule, and creates an `IceCube::Schedule` object, and serializes it into your `schedule_yaml` field.
|
22
|
+
|
23
|
+
E.x.
|
24
|
+
|
25
|
+
@event.schedule_attributes = params[:event][:schedule_attributes]
|
26
|
+
|
27
|
+
### Parameters Accepted
|
28
|
+
|
29
|
+
Because they are coming from a form, all parameters are expected to be in string format.
|
30
|
+
|
31
|
+
##### `:repeat`
|
32
|
+
|
33
|
+
Can be `0` or `1`. The parameter should respond to `:to_i`. `0` indicates that the event does not repeat. Anything else indicates that the event repeats.
|
34
|
+
|
35
|
+
#### Parameters for non-repeating events
|
36
|
+
|
37
|
+
The following parameters will only be used if the `repeat` parameter is `0`.
|
38
|
+
|
39
|
+
##### `:date`
|
40
|
+
|
41
|
+
The date that this (non-repeating) event is scheduled for. Should be parseable by `Time.parse`. This parameter is only used if `:repeat` is `0`.
|
42
|
+
|
43
|
+
#### Parameters for repeating events
|
44
|
+
|
45
|
+
The following parameters will only be used if the `repeat` parameter is `1`.
|
46
|
+
|
47
|
+
##### `:start_date`
|
48
|
+
|
49
|
+
The date at which the event starts repeating. Must be parseable by `Time.parse`.
|
50
|
+
|
51
|
+
#### `:interval_unit`
|
52
|
+
|
53
|
+
The interval unit by which the event repeats. Can be `"day"` or `"week"`. For example, if the even repeats every day, this would be `"day"`. If it repeats weekly, this would be `"week"`.
|
54
|
+
|
55
|
+
#### `:interval`
|
56
|
+
|
57
|
+
The interval by which the event repeats. Should be an integer (in string format). For example, if the event repeats every other day, this parameter should be `2`, and `:interval_unit` should be `"day"`. If it repeats every other week, it should be `2` and interval unit should be `"week"`.
|
58
|
+
|
59
|
+
#### `:sunday`, `:monday`, `:tuesday`, `:wednesday`, `:thursday`, `:friday`, `:saturday`
|
60
|
+
|
61
|
+
Indicates with a `0` or `1` whether the event repeats on this day. For example, if the event repeats every other week on Tues and Thursday, then the parameters would include these: `:interval => "2", :interval_unit => "week", :tuesday => "1", :thursday => "1"`. These parameters are only used if `:repeat` is `1` and `:interval_unit` is `"week"`.
|
62
|
+
|
63
|
+
## `schedule_attributes`
|
64
|
+
|
65
|
+
Provides an OStruct with methods that correspond to the attributes accepted by `#schedule_attributes=`. ActiveRecord can use this object to populate a form using [`FormHelper#fields_for`](http://apidock.com/rails/ActionView/Helpers/FormHelper/fields_for).
|
66
|
+
|
67
|
+
##`schedule`
|
68
|
+
|
69
|
+
Returns a the `IceCube::Schedule` object that is serialized to the model's `schedule_yaml` field. If there is none, it returns a schedule with `start_date` of the current date, recurring daily, ending never.
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'schedule_attributes/model'
|
2
|
+
require 'schedule_attributes/railtie' if defined? Rails
|
3
|
+
require 'schedule_attributes/form_builder' if defined? Formtastic
|
4
|
+
|
5
|
+
if defined? Arel
|
6
|
+
module Arel::Visitors
|
7
|
+
class ToSql
|
8
|
+
|
9
|
+
# Allow schedule occurrences to be used directly as Time objects in
|
10
|
+
# ActiveRecord queries
|
11
|
+
#
|
12
|
+
def visit_IceCube_Occurrence(value)
|
13
|
+
quoted(value.to_time)
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'schedule_attributes/core'
|
2
|
+
require 'schedule_attributes/serializer'
|
3
|
+
|
4
|
+
module ScheduleAttributes::ActiveRecord
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
include ScheduleAttributes::Core
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
attr_accessor :schedule_field
|
10
|
+
attr_accessor :default_schedule
|
11
|
+
|
12
|
+
def default_schedule
|
13
|
+
@default_schedule || ScheduleAttributes.default_schedule
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module Sugar
|
18
|
+
def has_schedule_attributes(options={:column_name => :schedule})
|
19
|
+
options[:column_name] ||= ScheduleAttributes::DEFAULT_ATTRIBUTE_KEY
|
20
|
+
@schedule_field = options[:column_name]
|
21
|
+
@default_schedule = options[:default_schedule] if options.has_key?(:default_schedule)
|
22
|
+
serialize @schedule_field, ScheduleAttributes::Serializer
|
23
|
+
include ScheduleAttributes::ActiveRecord
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def initialize(*args)
|
30
|
+
super
|
31
|
+
self[self.class.schedule_field] ||= default_schedule
|
32
|
+
end
|
33
|
+
|
34
|
+
def read_schedule_field
|
35
|
+
self[self.class.schedule_field] or default_schedule
|
36
|
+
end
|
37
|
+
|
38
|
+
def write_schedule_field(value)
|
39
|
+
self[self.class.schedule_field] = value
|
40
|
+
end
|
41
|
+
|
42
|
+
def default_schedule
|
43
|
+
self.class.default_schedule
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Injects the has_schedule_attributes method when loading without Rails
|
48
|
+
if defined? ActiveRecord::Base
|
49
|
+
ActiveRecord::Base.send :extend, ScheduleAttributes::ActiveRecord::Sugar
|
50
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module ScheduleAttributes
|
2
|
+
|
3
|
+
class Configuration
|
4
|
+
|
5
|
+
attr_accessor :time_format
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@time_format = '%H:%M'
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class << self
|
13
|
+
def configure
|
14
|
+
@configuration ||= Configuration.new
|
15
|
+
if block_given?
|
16
|
+
yield @configuration
|
17
|
+
end
|
18
|
+
return @configuration
|
19
|
+
end
|
20
|
+
alias_method :configuration, :configure
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
require 'active_support/time'
|
2
|
+
require 'active_support/concern'
|
3
|
+
require 'active_support/time_with_zone'
|
4
|
+
require 'ice_cube'
|
5
|
+
require 'ostruct'
|
6
|
+
require 'schedule_attributes/configuration'
|
7
|
+
require 'schedule_attributes/extensions/ice_cube'
|
8
|
+
require 'schedule_attributes/input'
|
9
|
+
require 'schedule_attributes/rule_parser'
|
10
|
+
|
11
|
+
module ScheduleAttributes
|
12
|
+
DEFAULT_ATTRIBUTE_KEY = :schedule
|
13
|
+
DAY_NAMES = Date::DAYNAMES.map(&:downcase).map(&:to_sym)
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def default_schedule
|
17
|
+
IceCube::Schedule.new(TimeHelpers.today).tap do |s|
|
18
|
+
s.add_recurrence_rule(IceCube::Rule.daily)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def parse_rule(options)
|
23
|
+
RuleParser[options[:interval_unit]].parse(options)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
module Core
|
28
|
+
extend ActiveSupport::Concern
|
29
|
+
|
30
|
+
def schedule_attributes=(options)
|
31
|
+
input = ScheduleAttributes::Input.new(options)
|
32
|
+
new_schedule = IceCube::Schedule.new(input.start_time || TimeHelpers.today)
|
33
|
+
|
34
|
+
if input.repeat?
|
35
|
+
parser = ScheduleAttributes::RuleParser[input.interval_unit || 'day'].new(input)
|
36
|
+
new_schedule.add_recurrence_rule(parser.rule)
|
37
|
+
parser.exceptions.each do |exrule|
|
38
|
+
new_schedule.add_exception_rule(exrule)
|
39
|
+
end
|
40
|
+
else
|
41
|
+
input.dates.each do |d|
|
42
|
+
new_schedule.add_recurrence_time(d)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
new_schedule.duration = input.duration if input.duration
|
47
|
+
|
48
|
+
write_schedule_field(new_schedule)
|
49
|
+
end
|
50
|
+
|
51
|
+
# TODO: use a proper form input model, not OpenStruct
|
52
|
+
#
|
53
|
+
def schedule_attributes
|
54
|
+
atts = {}
|
55
|
+
time_format = ScheduleAttributes.configuration.time_format
|
56
|
+
schedule = read_schedule_field || ScheduleAttributes.default_schedule
|
57
|
+
|
58
|
+
if schedule.start_time.seconds_since_midnight == 0 && schedule.end_time.nil?
|
59
|
+
atts[:all_day] = true
|
60
|
+
else
|
61
|
+
atts[:start_time] = schedule.start_time.strftime(time_format)
|
62
|
+
atts[:end_time] = (schedule.end_time).strftime(time_format) if schedule.end_time
|
63
|
+
end
|
64
|
+
|
65
|
+
if rule = schedule.rrules.first
|
66
|
+
atts[:repeat] = 1
|
67
|
+
atts[:start_date] = schedule.start_time.to_date
|
68
|
+
atts[:date] = Date.current # default for populating the other part of the form
|
69
|
+
|
70
|
+
rule_hash = rule.to_hash
|
71
|
+
atts[:interval] = rule_hash[:interval]
|
72
|
+
|
73
|
+
case rule
|
74
|
+
when IceCube::DailyRule
|
75
|
+
atts[:interval_unit] = 'day'
|
76
|
+
when IceCube::WeeklyRule
|
77
|
+
atts[:interval_unit] = 'week'
|
78
|
+
|
79
|
+
if rule_hash[:validations][:day]
|
80
|
+
rule_hash[:validations][:day].each do |day_idx|
|
81
|
+
atts[ ScheduleAttributes::DAY_NAMES[day_idx] ] = 1
|
82
|
+
end
|
83
|
+
end
|
84
|
+
when IceCube::MonthlyRule
|
85
|
+
atts[:interval_unit] = 'month'
|
86
|
+
|
87
|
+
day_of_week = rule_hash[:validations][:day_of_week]
|
88
|
+
day_of_month = rule_hash[:validations][:day_of_month]
|
89
|
+
|
90
|
+
if day_of_week
|
91
|
+
day_of_week = day_of_week.first.flatten
|
92
|
+
atts[:ordinal_week] = day_of_week.first
|
93
|
+
atts[:ordinal_unit] = 'week'
|
94
|
+
elsif day_of_month
|
95
|
+
atts[:ordinal_day] = day_of_month.first
|
96
|
+
atts[:ordinal_unit] = 'day'
|
97
|
+
end
|
98
|
+
when IceCube::YearlyRule
|
99
|
+
atts[:interval_unit] = 'year'
|
100
|
+
end
|
101
|
+
|
102
|
+
if rule.until_time
|
103
|
+
atts[:end_date] = rule.until_time.to_date
|
104
|
+
atts[:ends] = 'eventually'
|
105
|
+
else
|
106
|
+
atts[:ends] = 'never'
|
107
|
+
end
|
108
|
+
|
109
|
+
if months = rule.validations_for(:month_of_year).map(&:month)
|
110
|
+
atts[:yearly_start_month] = months.first
|
111
|
+
atts[:yearly_end_month] = months.last
|
112
|
+
|
113
|
+
# get leading & trailing days from exception rules
|
114
|
+
schedule.exrules.each do |x|
|
115
|
+
x.validations_for(:month_of_year).map(&:month).each do |m|
|
116
|
+
days = x.validations_for(:day_of_month).map(&:day)
|
117
|
+
|
118
|
+
if m == atts[:yearly_start_month]
|
119
|
+
atts[:yearly_start_month_day] = days.last + 1 if days.first == 1
|
120
|
+
end
|
121
|
+
|
122
|
+
if m == atts[:yearly_end_month]
|
123
|
+
if days.last == 31
|
124
|
+
atts[:yearly_end_month_day] = days.first - 1
|
125
|
+
atts[:yearly_start_month_day] ||= 1
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
else
|
133
|
+
atts[:repeat] = 0
|
134
|
+
atts[:interval] = 1
|
135
|
+
atts[:date] = schedule.rtimes.first.to_date
|
136
|
+
atts[:dates] = schedule.rtimes.map(&:to_date)
|
137
|
+
atts[:start_date] = DateHelpers.today # default for populating the other part of the form
|
138
|
+
end
|
139
|
+
|
140
|
+
OpenStruct.new(atts.delete_if { |k,v| v.blank? })
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
class ScheduleAttributes::FormBuilder < Formtastic::FormBuilder
|
2
|
+
|
3
|
+
def weekdays
|
4
|
+
Hash[ I18n.t('date.day_names').zip(ScheduleAttributes::DAY_NAMES) ]
|
5
|
+
end
|
6
|
+
|
7
|
+
def ordinal_month_days
|
8
|
+
(1..31).map { |d| [d.ordinalize, d] }
|
9
|
+
end
|
10
|
+
|
11
|
+
def ordinal_month_weeks
|
12
|
+
Hash["first", 1, "second", 2, "third", 3, "fourth", 4, "last", -1]
|
13
|
+
end
|
14
|
+
|
15
|
+
def one_time_fields(options={}, &block)
|
16
|
+
hidden = object.repeat.to_i == 1
|
17
|
+
@template.content_tag :div, hiding_field_options("schedule_one_time_fields", hidden, options), &block
|
18
|
+
end
|
19
|
+
|
20
|
+
def repeat_fields(options={}, &block)
|
21
|
+
hidden = object.repeat.to_i != 1
|
22
|
+
@template.content_tag :div, hiding_field_options("schedule_repeat_fields", hidden, options), &block
|
23
|
+
end
|
24
|
+
|
25
|
+
def ordinal_fields(options={}, &block)
|
26
|
+
hidden = object.interval_unit == 'month' && object.by_day_of == 'week'
|
27
|
+
@template.content_tag :div, hiding_field_options("schedule_ordinal_fields", hidden, options), &block
|
28
|
+
end
|
29
|
+
|
30
|
+
def weekday_fields(options={}, &block)
|
31
|
+
hidden = false
|
32
|
+
@template.content_tag :div, hiding_field_options("schedule_weekday_fields", hidden, options), &block
|
33
|
+
end
|
34
|
+
|
35
|
+
def hiding_field_options(class_name, hidden=false, options={})
|
36
|
+
hidden_style = "display: none" if hidden
|
37
|
+
options.tap do |o|
|
38
|
+
o.merge!(style: [o[:style], hidden_style].compact.join('; '))
|
39
|
+
o.merge!(class: [o[:class], class_name].compact.join(' '))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|