schedulable 0.0.7 → 0.0.8

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.
@@ -9,10 +9,10 @@ class CreateSchedules < ActiveRecord::Migration
9
9
  t.string :rule
10
10
  t.string :interval
11
11
 
12
- t.text :days
12
+ t.text :day
13
13
  t.text :day_of_week
14
14
 
15
- t.date :until
15
+ t.datetime :until
16
16
  t.integer :count
17
17
 
18
18
  t.timestamps
@@ -1,13 +1,2 @@
1
- class Schedule < ActiveRecord::Base
2
-
3
- include Schedulable::ScheduleSupport
4
-
5
- serialize :days
6
- serialize :day_of_week, Hash
7
-
8
- belongs_to :schedulable, polymorphic: true
9
-
10
- after_initialize :init_schedule
11
- after_save :init_schedule
12
-
1
+ class Schedule < Schedulable::Model::Schedule
13
2
  end
@@ -0,0 +1,29 @@
1
+ en:
2
+ model:
3
+ schedule: 'Schedule'
4
+
5
+ activerecord:
6
+ attributes:
7
+ schedule:
8
+ name: Name
9
+ rule: Regel
10
+ time: Time
11
+ date: Date
12
+ rule: Rule
13
+ day: Weekdays
14
+ day_of_week: Weekdays of nth week
15
+ until: Repeat until
16
+ count: Repetition count
17
+
18
+ schedulable:
19
+ monthly_week_names:
20
+ 1st: '1st'
21
+ 2nd: '2nd'
22
+ 3rd: '3rd'
23
+ 4th: '4th'
24
+ last: 'Last'
25
+ rules:
26
+ singular: Singular
27
+ monthly: Monthly
28
+ weekly: Weekly
29
+ daily: Daily
data/lib/schedulable.rb CHANGED
@@ -1,16 +1,18 @@
1
- require 'schedulable/railtie.rb' if defined? ::Rails::Railtie
2
- require 'schedulable/acts_as_schedulable.rb'
3
- require 'schedulable/schedule_support.rb'
1
+ require 'schedulable/railtie' if defined? ::Rails::Railtie
2
+ require 'schedulable/acts_as_schedulable'
3
+ require 'schedulable/schedule'
4
+ require 'schedulable/schedule_support'
5
+ require 'schedulable/form_helper'
4
6
  require 'i18n'
5
7
 
6
8
  module Schedulable
7
9
 
8
10
  class Config
9
- attr_accessor :max_build_count, :max_build_period
11
+ attr_accessor :max_build_count, :max_build_period, :form_helper, :update_mode
10
12
  end
11
13
 
12
14
  def self.config
13
- @@config ||= Config.new
15
+ @@config||= Config.new
14
16
  end
15
17
 
16
18
  def self.configure
@@ -9,9 +9,9 @@ module Schedulable
9
9
 
10
10
  module ClassMethods
11
11
 
12
- def acts_as_schedulable(options = {})
12
+ def acts_as_schedulable(name, options = {})
13
13
 
14
- name = options[:name] || :schedule
14
+ name||= :schedule
15
15
  attribute = :date
16
16
 
17
17
  has_one name, as: :schedulable, dependent: :destroy
@@ -37,8 +37,6 @@ module Schedulable
37
37
  # table_name
38
38
  occurrences_table_name = occurrences_association.to_s.tableize
39
39
 
40
- puts "SCHEDULABLE OCCURRENCES TABLE NAME: " + occurrences_table_name.to_s
41
-
42
40
  # remaining
43
41
  remaining_occurrences_options = options[:occurrences].clone
44
42
  remaining_occurrences_association = ("remaining_" << occurrences_association.to_s).to_sym
@@ -58,7 +56,6 @@ module Schedulable
58
56
  # build occurrences for all events
59
57
  # TODO: only invalid events
60
58
  schedulables = self.all
61
- puts "build occurrences for #{schedulables.length} #{self.name.tableize}"
62
59
  schedulables.each do |schedulable|
63
60
  schedulable.send("build_#{occurrences_association}")
64
61
  end
@@ -67,93 +64,103 @@ module Schedulable
67
64
 
68
65
  define_method "build_#{occurrences_association}" do
69
66
 
67
+ puts 'build occurrences...'
68
+
70
69
  # build occurrences for events
71
70
 
72
71
  schedule = self.send(name)
73
72
 
74
73
  now = Time.now
75
- occurrence_attribute = :date
74
+
75
+ # TODO: Make configurable
76
+ occurrence_attribute = :date
76
77
 
77
78
  schedulable = schedule.schedulable
78
- terminating = schedule.until.present? || schedule.count.present? && schedule.count > 0
79
-
80
- max_build_period = Schedulable.config.max_build_period || 1.year
81
- max_date = now + max_build_period
79
+ terminating = schedule.rule != 'singular' && (schedule.until.present? || schedule.count.present? && schedule.count > 1)
80
+
81
+ max_period = Schedulable.config.max_build_period || 1.year
82
+ max_date = now + max_period
83
+
82
84
  max_date = terminating ? [max_date, schedule.last.to_time].min : max_date
83
85
 
84
- max_build_count = Schedulable.config.max_build_count || 0
85
- max_build_count = terminating ? [max_build_count, schedule.remaining_occurrences.count].min : max_build_count
86
+ max_count = Schedulable.config.max_build_count || 100
87
+ max_count = terminating && schedule.remaining_occurrences.any? ? [max_count, schedule.remaining_occurrences.count].min : max_count
86
88
 
87
- # get occurrences
88
- if max_build_count > 0
89
- # get next occurrences for max_build_count
90
- occurrences = schedule.next_occurrences(max_build_count)
91
- end
92
-
93
- if !occurrences || occurrences.last && occurrences.last.to_time > max_date
94
- # get next occurrences for max_date
89
+ if schedule.rule != 'singular'
90
+
91
+ # Get schedule occurrences
95
92
  all_occurrences = schedule.occurrences(max_date)
96
93
  occurrences = []
97
- # filter future dates
98
- all_occurrences.each do |occurrence_date|
99
- if occurrence_date.to_time > now
100
- occurrences << occurrence_date
94
+ # Filter valid dates
95
+ all_occurrences.each_with_index do |occurrence_date, index|
96
+ if occurrence_date.present? && occurrence_date.to_time > now
97
+ if occurrence_date.to_time < max_date && index < max_count
98
+ occurrences << occurrence_date
99
+ else
100
+ max_date = [max_date, occurrence_date].min
101
+ end
101
102
  end
102
103
  end
104
+
105
+ else
106
+ singular_date_time = schedule.date.to_datetime + schedule.time.seconds_since_midnight.seconds
107
+ occurrences = [singular_date_time]
103
108
  end
104
109
 
110
+ # Build occurrences
111
+ update_mode = Schedulable.config.update_mode || :datetime
105
112
 
106
- puts 'build occurrences'
113
+ # Get existing remaining records
114
+ occurrences_records = schedulable.send("remaining_#{occurrences_association}")
107
115
 
108
116
  # build occurrences
109
- assocs = schedulable.class.reflect_on_all_associations(:has_many)
110
- assocs.each do |assoc|
111
- puts assoc.name
117
+ existing_record = nil
118
+ occurrences.each_with_index do |occurrence, index|
119
+
120
+ # Pull an existing record
121
+
122
+ if update_mode == :index
123
+ existing_records = [occurrences_records[index]]
124
+ elsif update_mode == :datetime
125
+ existing_records = occurrences_records.select { |record|
126
+ record.date.to_datetime == occurrence.to_datetime
127
+ }
128
+ else
129
+ existing_records = []
130
+ end
131
+
132
+ if existing_records.any?
133
+ # Overwrite existing records
134
+ existing_records.each do |existing_record|
135
+ if !occurrences_records.update(existing_record.id, date: occurrence.to_datetime)
136
+ puts 'an error occurred while saving an existing occurrence record'
137
+ end
138
+ end
139
+ else
140
+ # Create new record
141
+ if !occurrences_records.create(date: occurrence.to_datetime)
142
+ puts 'an error occurred while creating an occurrence record'
143
+ end
144
+ end
112
145
  end
113
146
 
114
- occurrences_records = schedulable.send(occurrences_association)
115
147
 
116
- # clean up unused remaining occurrences
148
+ # Clean up unused remaining occurrences
149
+ occurrences_records = schedulable.send("remaining_#{occurrences_association}")
117
150
  record_count = 0
118
151
  occurrences_records.each do |occurrence_record|
119
152
  if occurrence_record.date > now
120
- # destroy occurrence if it's not used anymore
121
- if !schedule.occurs_on?(occurrence_record.date) || occurrence_record.date > max_date || record_count > max_build_count
122
- if occurrences_records.destroy(occurrence_record)
123
- puts 'an error occurred while destroying an unused occurrence record'
124
- end
153
+ # Destroy occurrence if date or count lies beyond range
154
+ if schedule.rule != 'singular' && (!schedule.occurs_on?(occurrence_record.date.to_date) || !schedule.occurring_at?(occurrence_record.date.to_time) || occurrence_record.date > max_date) || schedule.rule == 'singular' && record_count > 0
155
+ occurrences_records.destroy(occurrence_record)
125
156
  end
126
157
  record_count = record_count + 1
127
158
  end
128
159
  end
129
160
 
130
- # build occurrences
131
- occurrences.each do |occurrence|
132
-
133
- # filter existing occurrence records
134
- existing = occurrences_records.select { |record|
135
- record.date.to_date == occurrence.to_date
136
- }
137
- if existing.length > 0
138
- # a record for this date already exists, adjust time
139
- existing.each { |record|
140
- #record.date = occurrence.to_datetime
141
- if !occurrences_records.update(record, date: occurrence.to_datetime)
142
- puts 'an error occurred while saving an existing occurrence record'
143
- end
144
- }
145
- else
146
- # create new record
147
- if !occurrences_records.create(date: occurrence.to_datetime)
148
- puts 'an error occurred while creating an occurrence record'
149
- end
150
- end
151
- end
152
-
153
161
  end
154
162
 
155
163
  end
156
-
157
164
  end
158
165
 
159
166
  end
@@ -0,0 +1,228 @@
1
+ module Schedulable
2
+ module FormHelper
3
+
4
+ STYLES = {
5
+ default: {
6
+ field_html: {class: 'field'},
7
+ input_wrapper: {tag: 'div'}
8
+ },
9
+ bootstrap: {
10
+ field_html: {class: 'field form-group'},
11
+ num_field_html: {class: 'form-control'},
12
+ date_select_html: {class: 'form-control'},
13
+ date_select_wrapper: {tag: 'div', class: 'form-inline'},
14
+ collection_select_html: {class: 'form-control'},
15
+ collection_check_boxes_item_wrapper: {tag: 'span', class: 'checkbox'}
16
+ }
17
+ }
18
+
19
+ def self.included(base)
20
+ ActionView::Helpers::FormBuilder.instance_eval do
21
+ include FormBuilderMethods
22
+ end
23
+ end
24
+
25
+ module FormBuilderMethods
26
+
27
+ def schedule_select(attribute, input_options = {})
28
+
29
+ template = @template
30
+
31
+ weekdays = Date::DAYNAMES.map(&:downcase)
32
+ weekdays = weekdays.slice(1..7) << weekdays.slice(0)
33
+
34
+ day_names = I18n.t('date.day_names', default: "")
35
+ day_names = day_names.blank? ? weekdays.map { |day| day.capitalize } : day_names.slice(1..7) << day_names.slice(0)
36
+ day_labels = Hash[weekdays.zip(day_names)]
37
+
38
+ # Pass in default month names when missing in translations
39
+ month_names = I18n.t('date.month_names', default: "")
40
+ month_names = month_names.blank? ? Date::MONTHNAMES : month_names
41
+
42
+ # Pass in default order when missing in translations
43
+ date_order = I18n.t('date.order', default: [:year, :month, :day])
44
+ date_order = date_order.map { |order|
45
+ order.to_sym
46
+ }
47
+
48
+ date_options = {
49
+ order: date_order,
50
+ use_month_names: month_names
51
+ }
52
+
53
+ # form helper style options
54
+
55
+ config_options = Schedulable.config.form_helper.present? ? Schedulable.config.form_helper : {style: :default}
56
+
57
+ input_options = config_options.merge(input_options)
58
+
59
+ # Setup style options
60
+ if input_options[:style].is_a?(Symbol) || input_options[:style].is_a?(String)
61
+ style_options = STYLES.has_key?(input_options[:style]) ? STYLES[input_options[:style]] : STYLES[:default]
62
+ elsif input_options[:style].is_a?(Hash)
63
+ style_options = input_options[:style]
64
+ else
65
+ style_options = STYLES[:default]
66
+ end
67
+
68
+ # Merge with general options
69
+ style_options = style_options.merge(input_options)
70
+
71
+ style_options[:field_html]||= {}
72
+
73
+ style_options[:label_html]||= {}
74
+ style_options[:label_wrapper]||= {}
75
+
76
+ style_options[:input_html]||= {}
77
+ style_options[:input_wrapper]||= {}
78
+
79
+ style_options[:number_field_html]||= {}
80
+ style_options[:number_field_wrapper]||= {}
81
+
82
+ style_options[:date_select_html]||= {}
83
+ style_options[:date_select_wrapper]||= {}
84
+
85
+ style_options[:collection_select_html]||= {}
86
+ style_options[:collection_select_wrapper]||= {}
87
+
88
+ style_options[:collection_check_boxes_item_html]||= {}
89
+ style_options[:collection_check_boxes_item_wrapper]||= {}
90
+
91
+ # Merge with input options
92
+ style_options[:number_field_html] = style_options[:input_html].merge(style_options[:number_field_html])
93
+ style_options[:number_field_wrapper] = style_options[:input_wrapper].merge(style_options[:number_field_wrapper])
94
+
95
+ style_options[:date_select_html] = style_options[:input_html].merge(style_options[:date_select_html])
96
+ style_options[:date_select_wrapper] = style_options[:input_wrapper].merge(style_options[:date_select_wrapper])
97
+
98
+ style_options[:collection_select_html] = style_options[:input_html].merge(style_options[:collection_select_html])
99
+ style_options[:collection_select_wrapper] = style_options[:input_wrapper].merge(style_options[:collection_select_wrapper])
100
+
101
+ style_options[:collection_check_boxes_item_html] = style_options[:input_html].merge(style_options[:collection_check_boxes_item_html])
102
+ style_options[:collection_check_boxes_item_wrapper] = style_options[:input_wrapper].merge(style_options[:collection_check_boxes_item_wrapper])
103
+
104
+
105
+ # Javascript element id
106
+ field_id = @object_name.to_s.gsub(/\]\[|[^-a-zA-Z0-9:.]/,"_").sub(/_$/,"") + "_" + attribute.to_s
107
+
108
+ @template.content_tag("div", {id: field_id}) do
109
+
110
+ self.fields_for(attribute, @object.send(attribute.to_s) || @object.send("build_" + attribute.to_s)) do |f|
111
+
112
+ @template.content_tag("div", style_options[:field_html]) do
113
+ select_output = f.collection_select(:rule, ['singular', 'daily', 'weekly', 'monthly'], lambda { |v| return v}, lambda { |v| I18n.t("schedulable.rules.#{v}", default: v.capitalize) }, {include_blank: false}, style_options[:collection_select_html])
114
+ content_wrap(@template, select_output, style_options[:collection_select_wrapper])
115
+ end <<
116
+
117
+ @template.content_tag("div", style_options[:field_html].merge({data: {group: 'singular'}})) do
118
+ content_wrap(@template, f.label(:date, style_options[:label_html]), style_options[:label_wrapper]) <<
119
+ content_wrap(@template, f.date_select(:date, date_options, style_options[:date_select_html]), style_options[:date_select_wrapper])
120
+ end <<
121
+
122
+ @template.content_tag("div", style_options[:field_html].merge({data: {group: 'weekly'}})) do
123
+ content_wrap(@template, f.label(:day), style_options[:label_wrapper]) <<
124
+ f.collection_check_boxes(:day, weekdays, lambda { |v| return v}, lambda { |v| ("&nbsp;" + day_labels[v]).html_safe}) do |cb|
125
+ check_box_output = cb.check_box(style_options[:collection_check_boxes_item_html])
126
+ text = cb.text
127
+ nested_output = cb.label({}) do |l|
128
+ check_box_output + text
129
+ end
130
+ content_wrap(@template, nested_output, style_options[:collection_check_boxes_item_wrapper])
131
+ end
132
+ end <<
133
+
134
+ @template.content_tag("div", style_options[:field_html].merge({data: {group: 'monthly'}})) do
135
+ f.fields_for :day_of_week, OpenStruct.new(f.object.day_of_week || {}) do |db|
136
+ content_wrap(@template, f.label(:day_of_week), style_options[:label_wrapper]) <<
137
+ @template.content_tag("div", nil, style: 'min-width: 280px; display: table') do
138
+ @template.content_tag("div", nil, style: 'display: table-row') do
139
+ @template.content_tag("span", nil, style: 'display: table-cell;') <<
140
+ ['1st', '2nd', '3rd', '4th', 'last'].reduce(''.html_safe) { | content, item |
141
+ content << @template.content_tag("span", I18n.t("schedulable.monthly_week_names.#{item}", default: item.to_s), style: 'display: table-cell; text-align: center')
142
+ }
143
+ end <<
144
+ weekdays.reduce(''.html_safe) do | content, weekday |
145
+ content << @template.content_tag("div", nil, style: 'display: table-row') do
146
+ @template.content_tag("span", day_labels[weekday] || weekday, style: 'display: table-cell') <<
147
+ db.collection_check_boxes(weekday.to_sym, [1, 2, 3, 4, -1], lambda { |i| i} , lambda { |i| "&nbsp;".html_safe}, checked: db.object.send(weekday)) do |cb|
148
+ @template.content_tag("span", style: 'display: table-cell; text-align: center') { cb.check_box() }
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end <<
155
+
156
+ @template.content_tag("div", style_options[:field_html].merge({data: {group: 'singular,daily,weekly,monthly'}})) do
157
+ content_wrap(@template, f.label(:time, style_options[:label_html]), style_options[:label_wrapper]) <<
158
+ content_wrap(@template, f.time_select(:time, date_options, style_options[:date_select_html]), style_options[:date_select_wrapper])
159
+ end <<
160
+
161
+ (if input_options[:interval]
162
+ @template.content_tag("div", style_options[:field_html].merge({data: {group: 'daily,weekly,monthly'}})) do
163
+ content_wrap(@template, f.label(:interval, style_options[:label_html]), style_options[:label_wrapper]) <<
164
+ content_wrap(@template, f.number_field(:interval, style_options[:number_field_html]), style_options[:number_field_wrapper])
165
+ end
166
+ else
167
+ f.hidden_field(:interval, value: 1)
168
+ end) <<
169
+
170
+ (if input_options[:until]
171
+ @template.content_tag("div", style_options[:field_html].merge({data: {group: 'daily,weekly,monthly'}})) do
172
+ content_wrap(@template, f.label(:until, style_options[:label_html]), style_options[:label_wrapper]) <<
173
+ content_wrap(@template, f.datetime_select(:until, date_options, style_options[:date_select_html]), style_options[:date_select_wrapper])
174
+ end
175
+ else
176
+ f.hidden_field(:until, value: nil)
177
+ end) <<
178
+
179
+ if input_options[:count]
180
+ @template.content_tag("div", style_options[:field_html].merge({data: {group: 'daily,weekly,monthly'}})) do
181
+ content_wrap(@template, f.label(:count, style_options[:label_html]), style_options[:label_wrapper]) <<
182
+ content_wrap(@template, f.number_field(:count, style_options[:number_field_html]), style_options[:number_field_wrapper])
183
+ end
184
+ else
185
+ f.hidden_field(:count, value: 0)
186
+ end
187
+
188
+ end
189
+
190
+ end <<
191
+
192
+ template.javascript_tag(
193
+ "(function() {" <<
194
+ " var container = document.querySelectorAll('##{field_id}'); container = container[container.length - 1]; " <<
195
+ " var select = container.querySelector(\"select[name*='rule']\"); " <<
196
+ " function update() {" <<
197
+ " var value = this.value;" <<
198
+ " [].slice.call(container.querySelectorAll(\"*[data-group]\")).forEach(function(elem) { " <<
199
+ " var groups = elem.getAttribute('data-group').split(',');" <<
200
+ " if (groups.indexOf(value) >= 0) {" <<
201
+ " elem.style.display = ''" <<
202
+ " } else {" <<
203
+ " elem.style.display = 'none'" <<
204
+ " }" <<
205
+ " });" <<
206
+ " }" <<
207
+ " if (jQuery) { jQuery(select).on('change', update); } else { select.addEventListener('change', update); }" <<
208
+ " update.call(select);" <<
209
+ "})()"
210
+ )
211
+
212
+ end
213
+
214
+
215
+ private
216
+ def content_wrap(template, content, options = nil)
217
+ if options.present? && options.has_key?(:tag)
218
+ template.content_tag(options[:tag], content, options.except(:tag))
219
+ else
220
+ content
221
+ end
222
+ end
223
+
224
+ end
225
+
226
+
227
+ end
228
+ end