schedule_attributes 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|