dgp-schedule_attributes 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.rspec +1 -0
- data/.travis.yml +28 -0
- data/Appraisals +21 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +63 -0
- data/README.markdown +78 -0
- data/Rakefile +7 -0
- data/gemfiles/rails_3.2.gemfile +8 -0
- data/gemfiles/rails_3.2.gemfile.lock +112 -0
- data/gemfiles/rails_4.0.gemfile +7 -0
- data/gemfiles/rails_4.0.gemfile.lock +76 -0
- data/gemfiles/rails_4.1.gemfile +7 -0
- data/gemfiles/rails_4.1.gemfile.lock +75 -0
- data/gemfiles/rails_4.2.gemfile +7 -0
- data/gemfiles/rails_4.2.gemfile.lock +75 -0
- data/gemfiles/rails_edge.gemfile +8 -0
- data/gemfiles/rails_edge.gemfile.lock +83 -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 +146 -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 +159 -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 +36 -0
- data/lib/schedule_attributes/version.rb +3 -0
- data/schedule_attributes.gemspec +32 -0
- data/spec/active_record_integration_spec.rb +57 -0
- data/spec/schedule_attributes/configuration_spec.rb +29 -0
- data/spec/schedule_attributes/input_spec.rb +182 -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 +226 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/support/parser_macros.rb +31 -0
- data/spec/support/scheduled_active_record_model.rb +39 -0
- data/spec/support/scheduled_model.rb +16 -0
- metadata +234 -0
@@ -0,0 +1,159 @@
|
|
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(end_time_date, @params[:end_time])
|
123
|
+
end
|
124
|
+
|
125
|
+
# if end_time < start_time, the schedule occurs over night
|
126
|
+
def end_time_date
|
127
|
+
return date_input unless @params[:start_time].present?
|
128
|
+
return nil if @params[:all_day]
|
129
|
+
return nil unless @params[:end_time].present?
|
130
|
+
parse_date_time(date_input, @params[:end_time]) <= start_time ? (Time.parse(date_input) + 1.day).strftime('%Y-%m-%d') : date_input
|
131
|
+
end
|
132
|
+
|
133
|
+
def start_date
|
134
|
+
parse_date_time(@params[:start_date]) if @params[:start_date]
|
135
|
+
end
|
136
|
+
|
137
|
+
def end_date
|
138
|
+
parse_date_time(@params[:end_date], @params[:start_time]) if @params[:end_date]
|
139
|
+
end
|
140
|
+
|
141
|
+
def ends?
|
142
|
+
@params[:end_date].present? && @params[:ends] != "never"
|
143
|
+
end
|
144
|
+
|
145
|
+
def dates
|
146
|
+
dates = (@params[:dates] || [@params[:date]]).compact
|
147
|
+
time = start_time.strftime('%H:%M') if @params[:start_time]
|
148
|
+
dates.map { |d| parse_date_time(d, time) }
|
149
|
+
end
|
150
|
+
|
151
|
+
private
|
152
|
+
|
153
|
+
def parse_date_time(date, time=nil)
|
154
|
+
date_time_parts = [date, time].compact
|
155
|
+
return if date_time_parts.empty?
|
156
|
+
TimeHelpers.parse_in_zone(date_time_parts.join(' '))
|
157
|
+
end
|
158
|
+
end
|
159
|
+
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
|