schedule_attributes 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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