dgp-schedule_attributes 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +28 -0
  5. data/Appraisals +21 -0
  6. data/Gemfile +4 -0
  7. data/Gemfile.lock +63 -0
  8. data/README.markdown +78 -0
  9. data/Rakefile +7 -0
  10. data/gemfiles/rails_3.2.gemfile +8 -0
  11. data/gemfiles/rails_3.2.gemfile.lock +112 -0
  12. data/gemfiles/rails_4.0.gemfile +7 -0
  13. data/gemfiles/rails_4.0.gemfile.lock +76 -0
  14. data/gemfiles/rails_4.1.gemfile +7 -0
  15. data/gemfiles/rails_4.1.gemfile.lock +75 -0
  16. data/gemfiles/rails_4.2.gemfile +7 -0
  17. data/gemfiles/rails_4.2.gemfile.lock +75 -0
  18. data/gemfiles/rails_edge.gemfile +8 -0
  19. data/gemfiles/rails_edge.gemfile.lock +83 -0
  20. data/lib/schedule_attributes.rb +18 -0
  21. data/lib/schedule_attributes/active_record.rb +50 -0
  22. data/lib/schedule_attributes/configuration.rb +22 -0
  23. data/lib/schedule_attributes/core.rb +146 -0
  24. data/lib/schedule_attributes/extensions/ice_cube.rb +11 -0
  25. data/lib/schedule_attributes/form_builder.rb +42 -0
  26. data/lib/schedule_attributes/input.rb +159 -0
  27. data/lib/schedule_attributes/model.rb +24 -0
  28. data/lib/schedule_attributes/railtie.rb +13 -0
  29. data/lib/schedule_attributes/rule_parser.rb +25 -0
  30. data/lib/schedule_attributes/rule_parser/base.rb +96 -0
  31. data/lib/schedule_attributes/rule_parser/day.rb +13 -0
  32. data/lib/schedule_attributes/rule_parser/month.rb +40 -0
  33. data/lib/schedule_attributes/rule_parser/week.rb +18 -0
  34. data/lib/schedule_attributes/rule_parser/year.rb +21 -0
  35. data/lib/schedule_attributes/serializer.rb +30 -0
  36. data/lib/schedule_attributes/time_helpers.rb +36 -0
  37. data/lib/schedule_attributes/version.rb +3 -0
  38. data/schedule_attributes.gemspec +32 -0
  39. data/spec/active_record_integration_spec.rb +57 -0
  40. data/spec/schedule_attributes/configuration_spec.rb +29 -0
  41. data/spec/schedule_attributes/input_spec.rb +182 -0
  42. data/spec/schedule_attributes/rule_parser/day_spec.rb +113 -0
  43. data/spec/schedule_attributes/rule_parser/month_spec.rb +47 -0
  44. data/spec/schedule_attributes/rule_parser/week_spec.rb +36 -0
  45. data/spec/schedule_attributes/rule_parser/year_spec.rb +53 -0
  46. data/spec/schedule_attributes/rule_parser_spec.rb +18 -0
  47. data/spec/schedule_attributes/time_helpers_spec.rb +39 -0
  48. data/spec/schedule_attributes_spec.rb +226 -0
  49. data/spec/spec_helper.rb +11 -0
  50. data/spec/support/parser_macros.rb +31 -0
  51. data/spec/support/scheduled_active_record_model.rb +39 -0
  52. data/spec/support/scheduled_model.rb +16 -0
  53. 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,13 @@
1
+ module ScheduleAttributes::RuleParser
2
+ class Day < Base
3
+
4
+ private
5
+
6
+ # @return [IceCube::Rule]
7
+ #
8
+ def parse_options
9
+ @rule = rule_factory.daily(input.interval)
10
+ end
11
+
12
+ end
13
+ 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