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,4 @@
1
+ pkg/*
2
+ *.gem
3
+ .bundle
4
+ .rvmrc
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem 'rake'
4
+
5
+ # Specify your gem's dependencies in schedule_atts.gemspec
6
+ gemspec
@@ -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
@@ -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.
@@ -0,0 +1,7 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new(:spec)
6
+ task :default => :spec
7
+
@@ -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,11 @@
1
+ class IceCube::Rule
2
+ def ==(other)
3
+ to_hash == other.to_hash
4
+ end
5
+ end
6
+
7
+ class IceCube::Schedule
8
+ def ==(other)
9
+ to_hash == other.to_hash
10
+ end
11
+ 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