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
@@ -0,0 +1,151 @@
|
|
1
|
+
require 'schedule_attributes/time_helpers'
|
2
|
+
|
3
|
+
module ScheduleAttributes
|
4
|
+
class Input
|
5
|
+
|
6
|
+
module RepeatingDates
|
7
|
+
def repeat?; true end
|
8
|
+
|
9
|
+
# Defaults to 1
|
10
|
+
# @return [Fixnum]
|
11
|
+
#
|
12
|
+
def interval
|
13
|
+
@params.fetch(:interval, 1).to_i
|
14
|
+
end
|
15
|
+
|
16
|
+
def interval_unit
|
17
|
+
@params[:interval_unit] if repeat?
|
18
|
+
end
|
19
|
+
|
20
|
+
def ordinal_unit
|
21
|
+
@params[:ordinal_unit].to_sym if @params[:ordinal_unit]
|
22
|
+
end
|
23
|
+
|
24
|
+
def ordinal_week
|
25
|
+
@params.fetch(:ordinal_week, 1).to_i
|
26
|
+
end
|
27
|
+
|
28
|
+
def ordinal_day
|
29
|
+
@params.fetch(:ordinal_day, 1).to_i
|
30
|
+
end
|
31
|
+
|
32
|
+
def yearly_start_month
|
33
|
+
return unless yearly_start_month?
|
34
|
+
@params[:yearly_start_month].to_i
|
35
|
+
end
|
36
|
+
|
37
|
+
def yearly_end_month
|
38
|
+
return unless yearly_end_month?
|
39
|
+
@params[:yearly_end_month].to_i
|
40
|
+
end
|
41
|
+
|
42
|
+
def yearly_start_month_day
|
43
|
+
return unless yearly_start_month_day?
|
44
|
+
@params[:yearly_start_month_day].to_i
|
45
|
+
end
|
46
|
+
|
47
|
+
def yearly_end_month_day
|
48
|
+
return unless yearly_end_month_day?
|
49
|
+
@params[:yearly_end_month_day].to_i
|
50
|
+
end
|
51
|
+
|
52
|
+
def yearly_start_month?
|
53
|
+
@params[:yearly_start_month].present?
|
54
|
+
end
|
55
|
+
|
56
|
+
def yearly_end_month?
|
57
|
+
@params[:yearly_end_month].present?
|
58
|
+
end
|
59
|
+
|
60
|
+
def yearly_start_month_day?
|
61
|
+
[ @params[:yearly_start_month].present?,
|
62
|
+
@params[:yearly_start_month_day].present?,
|
63
|
+
@params[:yearly_start_month_day].to_i > 1
|
64
|
+
].all?
|
65
|
+
end
|
66
|
+
|
67
|
+
def yearly_end_month_day?
|
68
|
+
@params[:yearly_end_month].present? &&
|
69
|
+
@params[:yearly_end_month_day].present? &&
|
70
|
+
@params[:yearly_end_month_day].to_i < Time.days_in_month(@params[:yearly_end_month].to_i)
|
71
|
+
end
|
72
|
+
|
73
|
+
def weekdays
|
74
|
+
IceCube::TimeUtil::DAYS.keys.select { |day| @params[day].to_i == 1 }
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def date_input
|
80
|
+
@params[:start_date] || @params[:date]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
module SingleDates
|
85
|
+
def repeat?; false end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def date_input
|
90
|
+
@params[:dates] ? @params[:dates].first : @params[:date]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
NEGATIVES = [false, "false", 0, "0", "f", "F", "no", "none"]
|
95
|
+
|
96
|
+
def initialize(params)
|
97
|
+
raise ArgumentError "expecting a Hash" unless params.is_a? Hash
|
98
|
+
@params = params.symbolize_keys.delete_if { |v| v.blank? }
|
99
|
+
date_methods = if NEGATIVES.none? { |v| params[:repeat] == v }
|
100
|
+
RepeatingDates
|
101
|
+
else
|
102
|
+
SingleDates
|
103
|
+
end
|
104
|
+
(class << self; self end).send :include, date_methods
|
105
|
+
end
|
106
|
+
|
107
|
+
attr_reader :params
|
108
|
+
|
109
|
+
def duration
|
110
|
+
return nil unless end_time
|
111
|
+
end_time - start_time
|
112
|
+
end
|
113
|
+
|
114
|
+
def start_time
|
115
|
+
time = @params[:start_time] unless @params[:all_day]
|
116
|
+
parse_date_time(date_input, time)
|
117
|
+
end
|
118
|
+
|
119
|
+
def end_time
|
120
|
+
return nil if @params[:all_day]
|
121
|
+
return nil unless @params[:end_time].present?
|
122
|
+
parse_date_time(date_input, @params[:end_time])
|
123
|
+
end
|
124
|
+
|
125
|
+
def start_date
|
126
|
+
parse_date_time(@params[:start_date]) if @params[:start_date]
|
127
|
+
end
|
128
|
+
|
129
|
+
def end_date
|
130
|
+
parse_date_time(@params[:end_date], @params[:start_time]) if @params[:end_date]
|
131
|
+
end
|
132
|
+
|
133
|
+
def ends?
|
134
|
+
@params[:end_date].present? && @params[:ends] != "never"
|
135
|
+
end
|
136
|
+
|
137
|
+
def dates
|
138
|
+
dates = (@params[:dates] || [@params[:date]]).compact
|
139
|
+
time = start_time.strftime('%H:%M') if @params[:start_time]
|
140
|
+
dates.map { |d| parse_date_time(d, time) }
|
141
|
+
end
|
142
|
+
|
143
|
+
private
|
144
|
+
|
145
|
+
def parse_date_time(date, time=nil)
|
146
|
+
date_time_parts = [date, time].compact
|
147
|
+
return if date_time_parts.empty?
|
148
|
+
TimeHelpers.parse_in_zone(date_time_parts.join(' '))
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'schedule_attributes/core'
|
2
|
+
|
3
|
+
module ScheduleAttributes::Model
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
include ScheduleAttributes::Core
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
attr_accessor :schedule_field
|
9
|
+
end
|
10
|
+
|
11
|
+
included do
|
12
|
+
@schedule_field ||= ScheduleAttributes::DEFAULT_ATTRIBUTE_KEY
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def read_schedule_field
|
18
|
+
send(self.class.schedule_field)
|
19
|
+
end
|
20
|
+
|
21
|
+
def write_schedule_field(value)
|
22
|
+
send("#{self.class.schedule_field}=", value)
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'schedule_attributes/active_record'
|
2
|
+
|
3
|
+
module ScheduleAttributes
|
4
|
+
class Railtie < Rails::Railtie
|
5
|
+
initializer "active_record.initialize_schedule_attributes" do
|
6
|
+
ActiveSupport.on_load(:active_record) do
|
7
|
+
class ActiveRecord::Base
|
8
|
+
extend ScheduleAttributes::ActiveRecord::Sugar
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'schedule_attributes/time_helpers'
|
2
|
+
|
3
|
+
module ScheduleAttributes
|
4
|
+
module RuleParser
|
5
|
+
TimeHelpers = ScheduleAttributes::TimeHelpers
|
6
|
+
|
7
|
+
def self.[](interval)
|
8
|
+
parser_name = interval.to_s.capitalize
|
9
|
+
if parser_name.present? && RuleParser.const_defined?(parser_name)
|
10
|
+
RuleParser.const_get parser_name
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.parse(options)
|
15
|
+
new(options).parse
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
require 'schedule_attributes/rule_parser/base'
|
22
|
+
require 'schedule_attributes/rule_parser/day'
|
23
|
+
require 'schedule_attributes/rule_parser/week'
|
24
|
+
require 'schedule_attributes/rule_parser/month'
|
25
|
+
require 'schedule_attributes/rule_parser/year'
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module ScheduleAttributes::RuleParser
|
2
|
+
class Base
|
3
|
+
# @param [Hash] options
|
4
|
+
#
|
5
|
+
def initialize(input)
|
6
|
+
@input = input
|
7
|
+
@rule ||= rule_factory.daily(input.interval)
|
8
|
+
@exceptions ||= []
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :input
|
12
|
+
|
13
|
+
def rule
|
14
|
+
parse_options
|
15
|
+
set_end_date
|
16
|
+
set_yearly_months
|
17
|
+
|
18
|
+
@rule
|
19
|
+
end
|
20
|
+
|
21
|
+
def exceptions
|
22
|
+
set_yearly_month_exceptions
|
23
|
+
|
24
|
+
@exceptions
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def rule_factory
|
30
|
+
IceCube::Rule
|
31
|
+
end
|
32
|
+
|
33
|
+
def set_end_date
|
34
|
+
@rule.until(input.end_date) if input.ends?
|
35
|
+
end
|
36
|
+
|
37
|
+
def set_yearly_months
|
38
|
+
if (input.yearly_start_month? || input.yearly_end_month?) && (yearly_months.length < 12)
|
39
|
+
@rule.month_of_year(*yearly_months)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def set_yearly_month_exceptions
|
44
|
+
if input.yearly_start_month_day?
|
45
|
+
@exceptions << rule_factory.daily
|
46
|
+
.month_of_year(start_month)
|
47
|
+
.day_of_month(*1...start_day)
|
48
|
+
end
|
49
|
+
|
50
|
+
if input.yearly_end_month_day?
|
51
|
+
@exceptions << rule_factory.daily
|
52
|
+
.month_of_year(end_month)
|
53
|
+
.day_of_month(*(end_day+1)..31)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def yearly_months
|
58
|
+
y1 = Date.current.year
|
59
|
+
y2 = start_month > end_month ? y1+1 : y1
|
60
|
+
range = Date.new(y1,start_month,1)..Date.new(y2,end_month,1)
|
61
|
+
range.group_by(&:month).keys
|
62
|
+
end
|
63
|
+
|
64
|
+
def start_month
|
65
|
+
param_to_month(input.yearly_start_month, 1)
|
66
|
+
end
|
67
|
+
|
68
|
+
def end_month
|
69
|
+
param_to_month(input.yearly_end_month, 12)
|
70
|
+
end
|
71
|
+
|
72
|
+
def start_day
|
73
|
+
param_to_day(input.yearly_start_month_day, 1)
|
74
|
+
end
|
75
|
+
|
76
|
+
def end_day
|
77
|
+
param_to_day(input.yearly_end_month_day, 31)
|
78
|
+
end
|
79
|
+
|
80
|
+
def param_to_month(param, default)
|
81
|
+
param = param.to_i
|
82
|
+
(param % 12).tap do |m|
|
83
|
+
return param if param == 12
|
84
|
+
return default if m == 0
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def param_to_day(param, default)
|
89
|
+
param = param.to_i
|
90
|
+
(param.to_i % 31).tap do |d|
|
91
|
+
return param if param == 31
|
92
|
+
return default if d == 0
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module ScheduleAttributes::RuleParser
|
2
|
+
# Parse an options hash to a monthly rule
|
3
|
+
#
|
4
|
+
# Assume a monthly rule starting on the :start_date if no :ordinal_day or
|
5
|
+
# :ordinal_week is given. If both of these options are present in the options
|
6
|
+
# hash, one can be selected with :ordinal_unit = ("day" or "week").
|
7
|
+
#
|
8
|
+
# Weekdays are applied for :ordinal_week only
|
9
|
+
#
|
10
|
+
class Month < Base
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
# @return [IceCube::Rule]
|
15
|
+
#
|
16
|
+
def parse_options
|
17
|
+
@rule = IceCube::Rule.monthly(input.interval)
|
18
|
+
|
19
|
+
case input.ordinal_unit
|
20
|
+
when :day
|
21
|
+
@rule.day_of_month(input.ordinal_day)
|
22
|
+
when :week
|
23
|
+
# schedule.add_recurrence_rule Rule.monthly.day_of_week(:tuesday => [1, -1])
|
24
|
+
# every month on the first and last tuesdays of the month
|
25
|
+
@rule.day_of_week(weekdays_by_week_of_month)
|
26
|
+
else
|
27
|
+
@rule
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def weekdays_by_week_of_month
|
32
|
+
Hash[selected_weekdays.map { |wd| [wd, Array(input.ordinal_week)] }]
|
33
|
+
end
|
34
|
+
|
35
|
+
def selected_weekdays
|
36
|
+
input.weekdays.any? ? input.weekdays : ScheduleAttributes::DAY_NAMES
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module ScheduleAttributes::RuleParser
|
2
|
+
# Parse an options hash to a weekly rule
|
3
|
+
# Weekdays used only if one or more are given, otherwise assume a weekly
|
4
|
+
# schedule starting the day of the :start_date
|
5
|
+
#
|
6
|
+
class Week < Base
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
# @return [IceCube::Rule]
|
11
|
+
#
|
12
|
+
def parse_options
|
13
|
+
@rule = IceCube::Rule.weekly(input.interval)
|
14
|
+
@rule.day(*input.weekdays) unless input.weekdays.empty?
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module ScheduleAttributes::RuleParser
|
2
|
+
class Year < Base
|
3
|
+
|
4
|
+
private
|
5
|
+
|
6
|
+
# @return [IceCube::Rule]
|
7
|
+
#
|
8
|
+
def parse_options
|
9
|
+
@rule = rule_factory.yearly(input.interval).tap do |rule|
|
10
|
+
if input.start_date
|
11
|
+
rule.month_of_year(input.start_date.month)
|
12
|
+
.day_of_month(input.start_date.day)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def set_yearly_months; nil end
|
18
|
+
|
19
|
+
def set_yearly_month_exceptions; nil end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'ice_cube'
|
2
|
+
|
3
|
+
module ScheduleAttributes
|
4
|
+
class Serializer
|
5
|
+
|
6
|
+
# Only load YAML serializations that are present, not empty strings
|
7
|
+
# An invalid value can raise an error that gets caught by ActiveRecord
|
8
|
+
# and results in the original string getting passed through. This allows
|
9
|
+
# it to be saved back unchanged if necessary.
|
10
|
+
#
|
11
|
+
def self.load(yaml)
|
12
|
+
IceCube::Schedule.from_yaml(yaml) if yaml.present?
|
13
|
+
end
|
14
|
+
|
15
|
+
# This should normally receive a Schedule object.
|
16
|
+
# In some unknown circumstance that I can't reproduce, it would save an
|
17
|
+
# already-dumped serialized YAML Schedule string into a YAML string
|
18
|
+
# wrapper, effectively double-bagging the YAML. I only assume it receives a
|
19
|
+
# String when loading a bad YAML value from the database, which then
|
20
|
+
# propagates and re-re-serializes itself into YAML on every save.
|
21
|
+
#
|
22
|
+
def self.dump(schedule)
|
23
|
+
case schedule
|
24
|
+
when IceCube::Schedule then schedule.to_yaml
|
25
|
+
when String then schedule
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module ScheduleAttributes
|
2
|
+
module TimeHelpers
|
3
|
+
def self.parse_in_zone(str)
|
4
|
+
if Time.respond_to?(:zone) && Time.zone
|
5
|
+
return str.in_time_zone if str.is_a?(Time)
|
6
|
+
str.is_a?(Date) ? str.to_time_in_current_zone : Time.zone.parse(str)
|
7
|
+
else
|
8
|
+
return str if str.is_a?(Time)
|
9
|
+
Time.parse(str)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.today
|
14
|
+
if Time.respond_to?(:zone) && Time.zone
|
15
|
+
Date.current.to_time_in_current_zone
|
16
|
+
else
|
17
|
+
Date.today.to_time
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module DateHelpers
|
23
|
+
def self.today
|
24
|
+
if Time.respond_to?(:zone) && Time.zone
|
25
|
+
Date.current
|
26
|
+
else
|
27
|
+
Date.today
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|