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.
Files changed (40) hide show
  1. data/.gitignore +4 -0
  2. data/.rspec +1 -0
  3. data/Gemfile +6 -0
  4. data/Gemfile.lock +57 -0
  5. data/README.markdown +69 -0
  6. data/Rakefile +7 -0
  7. data/lib/schedule_attributes.rb +18 -0
  8. data/lib/schedule_attributes/active_record.rb +50 -0
  9. data/lib/schedule_attributes/configuration.rb +22 -0
  10. data/lib/schedule_attributes/core.rb +144 -0
  11. data/lib/schedule_attributes/extensions/ice_cube.rb +11 -0
  12. data/lib/schedule_attributes/form_builder.rb +42 -0
  13. data/lib/schedule_attributes/input.rb +151 -0
  14. data/lib/schedule_attributes/model.rb +24 -0
  15. data/lib/schedule_attributes/railtie.rb +13 -0
  16. data/lib/schedule_attributes/rule_parser.rb +25 -0
  17. data/lib/schedule_attributes/rule_parser/base.rb +96 -0
  18. data/lib/schedule_attributes/rule_parser/day.rb +13 -0
  19. data/lib/schedule_attributes/rule_parser/month.rb +40 -0
  20. data/lib/schedule_attributes/rule_parser/week.rb +18 -0
  21. data/lib/schedule_attributes/rule_parser/year.rb +21 -0
  22. data/lib/schedule_attributes/serializer.rb +30 -0
  23. data/lib/schedule_attributes/time_helpers.rb +31 -0
  24. data/lib/schedule_attributes/version.rb +3 -0
  25. data/schedule_attributes.gemspec +28 -0
  26. data/spec/active_record_integration_spec.rb +45 -0
  27. data/spec/schedule_attributes/configuration_spec.rb +25 -0
  28. data/spec/schedule_attributes/input_spec.rb +174 -0
  29. data/spec/schedule_attributes/rule_parser/day_spec.rb +113 -0
  30. data/spec/schedule_attributes/rule_parser/month_spec.rb +47 -0
  31. data/spec/schedule_attributes/rule_parser/week_spec.rb +36 -0
  32. data/spec/schedule_attributes/rule_parser/year_spec.rb +53 -0
  33. data/spec/schedule_attributes/rule_parser_spec.rb +18 -0
  34. data/spec/schedule_attributes/time_helpers_spec.rb +39 -0
  35. data/spec/schedule_attributes_spec.rb +219 -0
  36. data/spec/spec_helper.rb +11 -0
  37. data/spec/support/parser_macros.rb +27 -0
  38. data/spec/support/scheduled_active_record_model.rb +42 -0
  39. data/spec/support/scheduled_model.rb +16 -0
  40. 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,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
@@ -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