xteam_schedule 0.0.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -4,6 +4,8 @@ xTeam Schedule is a gem that provides full control over schedules for use with [
4
4
 
5
5
  It is capable of reading and writing schedules, whilst providing access to all of its components through the [ActiveRecord](http://api.rubyonrails.org/classes/ActiveRecord/Base.html) interface, which every Ruby on Rails developer will be familiar with. This is absolutely, **the best solution** for managing agile teams.
6
6
 
7
+ You can find a blog post explaining some of the thinking behind its implementation [here](https://www.unboxedconsulting.com/blog/gemnastics-with-activerecord).
8
+
7
9
  <img src="http://www.adnx.com/i/uploads/xTeam1.jpg" width="820" alt="xTeam Schedule" />
8
10
 
9
11
  ### Features:
@@ -303,13 +305,41 @@ wednesday.update_attribute(:break_begin, nil)
303
305
  **Defaults:**
304
306
  The default weekly working schedule is identical to the same as the one set up above (except wednesday lunch). i.e. 9am-5pm Mon-Fri, with lunch from 12pm-1pm.
305
307
 
308
+ ## Holidays
309
+
310
+ Holidays can either belong to a resource, or the entire schedule. Holidays on the schedule are shared by all resources. This is useful for bank holidays. You can leave off the end date for one day holidays.
311
+
312
+ ```ruby
313
+ schedule.holidays.create!(:begin_date => Date.new(2012, 12, 25), :name => 'Christmas Day')
314
+
315
+ resource_group = schedule.resource_groups.create(:name => 'foo')
316
+ resource = resource_group.resources.create!(:name => 'bar')
317
+ resource.holidays.create!(
318
+ :begin_date => Date.new(2012, 07, 01),
319
+ :end_date => Date.new(2012, 07, 10),
320
+ :name => 'Visiting California'
321
+ )
322
+ ```
323
+
324
+ **Required attributes:**
325
+ begin_date
326
+
327
+ **Example queries:**
328
+
329
+ ```ruby
330
+ holiday_names = schedule.resources.map(&:holidays).flatten.map(&:name)
331
+ resources_without_holidays = schedule.resources.select { |r| r.holidays.empty? }
332
+ finished_holidays = schedule.resources.map(&:holidays).flatten.select { |h|
333
+ (h.end_date || h.begin_date) < Date.today
334
+ }
335
+ ```
336
+
306
337
  ## Under Development
307
338
 
308
339
  This gem is far from complete. The following is a list of features that are under development:
309
340
 
310
341
  * Resource images
311
342
  * Sort by
312
- * Holidays
313
343
  * Absences
314
344
  * Remote access
315
345
  * To assign
@@ -1,15 +1,15 @@
1
1
  class XTeamSchedule::Base < ActiveRecord::Base
2
2
  self.abstract_class = true
3
3
  establish_connection(:adapter => 'sqlite3', :database => ':memory:')
4
-
4
+
5
5
  def self.build_schema
6
6
  XTeamSchedule::Schema.define do
7
-
7
+
8
8
  create_table :schedules, :force => true do |table|
9
9
  table.column :begin_date, :date, :default => 10.years.ago.to_date
10
10
  table.column :end_date, :date, :default => 10.years.from_now.to_date
11
11
  end
12
-
12
+
13
13
  create_table :interfaces, :force => true do |table|
14
14
  table.column :schedule_id, :integer
15
15
  table.column :display_assignments_name, :boolean, :default => true
@@ -21,11 +21,11 @@ class XTeamSchedule::Base < ActiveRecord::Base
21
21
  table.column :display_absences, :boolean, :default => true
22
22
  table.column :time_granularity, :integer, :default => XTeamSchedule::Interface::TIME_GRANULARITIES[:month]
23
23
  end
24
-
24
+
25
25
  create_table :weekly_working_schedules, :force => true do |table|
26
26
  table.column :schedule_id, :integer
27
27
  end
28
-
28
+
29
29
  create_table :working_days, :force => true do |table|
30
30
  table.column :weekly_working_schedule_id, :integer
31
31
  table.column :name, :string
@@ -34,13 +34,13 @@ class XTeamSchedule::Base < ActiveRecord::Base
34
34
  table.column :break_begin, :string
35
35
  table.column :break_end, :string
36
36
  end
37
-
37
+
38
38
  create_table :resource_groups, :force => true do |table|
39
39
  table.column :schedule_id, :integer
40
40
  table.column :expanded_in_library, :boolean, :default => true
41
41
  table.column :name, :string
42
42
  end
43
-
43
+
44
44
  create_table :resources, :force => true do |table|
45
45
  table.column :resource_group_id, :integer
46
46
  table.column :displayed_in_planning, :boolean, :default => true
@@ -50,19 +50,19 @@ class XTeamSchedule::Base < ActiveRecord::Base
50
50
  table.column :name, :string
51
51
  table.column :phone, :string
52
52
  end
53
-
53
+
54
54
  create_table :assignment_groups, :force => true do |table|
55
55
  table.column :schedule_id, :integer
56
56
  table.column :expanded_in_library, :boolean, :default => true
57
57
  table.column :name, :string
58
58
  end
59
-
59
+
60
60
  create_table :assignments, :force => true do |table|
61
61
  table.column :assignment_group_id, :integer
62
62
  table.column :name, :string
63
63
  table.column :colour, :string
64
64
  end
65
-
65
+
66
66
  create_table :working_times, :force => true do |table|
67
67
  table.column :resource_id, :integer
68
68
  table.column :assignment_id, :integer
@@ -70,6 +70,14 @@ class XTeamSchedule::Base < ActiveRecord::Base
70
70
  table.column :duration, :integer
71
71
  table.column :notes, :string
72
72
  end
73
+
74
+ create_table :holidays, :force => true do |table|
75
+ table.column :schedule_id, :integer
76
+ table.column :resource_id, :integer
77
+ table.column :begin_date, :date
78
+ table.column :end_date, :date
79
+ table.column :name, :string
80
+ end
73
81
  end
74
82
  end
75
83
  end
@@ -1,17 +1,17 @@
1
1
  class XTeamSchedule::Composer
2
-
2
+
3
3
  attr_accessor :schedule, :hash
4
-
4
+
5
5
  def self.compose(schedule)
6
6
  new(schedule).compose
7
7
  end
8
-
8
+
9
9
  def initialize(schedule)
10
10
  schedule.save!
11
11
  self.schedule = schedule
12
12
  self.hash = {}
13
13
  end
14
-
14
+
15
15
  def compose
16
16
  compose_resource_groups!
17
17
  compose_resources!
@@ -20,12 +20,13 @@ class XTeamSchedule::Composer
20
20
  compose_working_times!
21
21
  compose_interface!
22
22
  compose_weekly_working_schedule!
23
+ compose_holidays!
23
24
  compose_schedule!
24
25
  hash
25
26
  end
26
-
27
+
27
28
  private
28
-
29
+
29
30
  def compose_resource_groups!
30
31
  hash['resource groups'] ||= []
31
32
  resource_groups = schedule.resource_groups
@@ -36,15 +37,16 @@ private
36
37
  }
37
38
  end
38
39
  end
39
-
40
+
40
41
  def compose_resources!
41
42
  hash['resources'] ||= []
42
43
  resources = schedule.resource_groups.map(&:resources).flatten
43
44
  resources.each do |r|
45
+ image = Base64.decode64(r.image) if r.image
44
46
  hash['resources'] << {
45
47
  'displayedInPlanning' => r.displayed_in_planning,
46
48
  'email' => r.email,
47
- 'image' => (StringIO.new(r.image) if r.image),
49
+ 'image' => (StringIO.new(image) if image),
48
50
  'mobile' => r.mobile,
49
51
  'name' => r.name,
50
52
  'phone' => r.phone,
@@ -52,7 +54,7 @@ private
52
54
  }
53
55
  end
54
56
  end
55
-
57
+
56
58
  def compose_assignment_groups!
57
59
  hash['task categories'] ||= []
58
60
  assignment_groups = schedule.assignment_groups
@@ -63,7 +65,7 @@ private
63
65
  }
64
66
  end
65
67
  end
66
-
68
+
67
69
  def compose_assignments!
68
70
  hash['tasks'] ||= []
69
71
  assignments = schedule.assignment_groups.map(&:assignments).flatten
@@ -76,7 +78,7 @@ private
76
78
  }
77
79
  end
78
80
  end
79
-
81
+
80
82
  def compose_working_times!
81
83
  hash['objectsForResources'] ||= {}
82
84
  resources = schedule.resource_groups.map(&:resources).flatten
@@ -95,7 +97,7 @@ private
95
97
  end
96
98
  end
97
99
  end
98
-
100
+
99
101
  def compose_interface!
100
102
  interface = schedule.interface
101
103
  hash['display task names'] = interface.display_assignments_name
@@ -107,15 +109,15 @@ private
107
109
  hash['display absence cells'] = interface.display_absences
108
110
  hash['interface status'] = { 'latest time navigation mode' => interface.time_granularity }
109
111
  end
110
-
112
+
111
113
  def compose_weekly_working_schedule!
112
114
  weekly_working_schedule = schedule.weekly_working_schedule
113
115
  working_days = weekly_working_schedule.working_days
114
-
116
+
115
117
  hash['settings'] ||= {}
116
118
  hash['settings']['days off'] ||= []
117
119
  hash['settings']['working schedule'] ||= {}
118
-
120
+
119
121
  working_days.each do |day|
120
122
  day_name = day.name.downcase
121
123
  hash['settings']['working schedule'][day_name] =
@@ -126,7 +128,7 @@ private
126
128
  else
127
129
  { 'worked' => 'no' }
128
130
  end
129
-
131
+
130
132
  hash['settings']['working schedule']["pause_#{day_name}"] =
131
133
  if day.day_begin.present? and day.break_begin.present?
132
134
  { 'worked' => 'yes',
@@ -138,16 +140,58 @@ private
138
140
  end
139
141
  end
140
142
  end
141
-
143
+
144
+ def compose_holidays!
145
+ compose_schedule_holidays!
146
+ compose_resource_holidays!
147
+ end
148
+
149
+ def compose_schedule_holidays!
150
+ holidays = schedule.holidays
151
+
152
+ hash['settings'] ||= {}
153
+ hash['settings']['days off'] ||= []
154
+
155
+ holidays.each do |h|
156
+ h.end_date ||= h.begin_date
157
+ hash['settings']['days off'] << {
158
+ 'begin date' => compose_date(h.begin_date),
159
+ 'end date' => compose_date(h.end_date),
160
+ 'name' => h.name
161
+ }
162
+ end
163
+ end
164
+
165
+ def compose_resource_holidays!
166
+ resources = schedule.resource_groups.map(&:resources).flatten
167
+ resources.each do |r|
168
+ next unless r.holidays
169
+ index = hash['resources'].find_index { |h| h['name'] == r.name }
170
+ next unless index
171
+
172
+ hash['resources'][index]['settings'] ||= {}
173
+ hash['resources'][index]['settings']['days off'] ||= []
174
+ hash['resources'][index]['settings']['use custom days off'] = 1
175
+ r.holidays.each do |h|
176
+ h.end_date ||= h.begin_date
177
+ hash['resources'][index]['settings']['days off'] << {
178
+ 'begin date' => compose_date(h.begin_date),
179
+ 'end date' => compose_date(h.end_date),
180
+ 'name' => h.name
181
+ }
182
+ end
183
+ end
184
+ end
185
+
142
186
  def compose_schedule!
143
187
  hash['begin date'] = compose_date(schedule.begin_date)
144
188
  hash['end date'] = compose_date(schedule.end_date)
145
189
  end
146
-
190
+
147
191
  def compose_colour(colour_hash)
148
192
  { 'alpha' => 1 }.merge([:red, :green, :blue].inject({}) { |h, c| h[c.to_s] = colour_hash[c]; h })
149
193
  end
150
-
194
+
151
195
  def compose_date(date)
152
196
  return unless date.present?
153
197
  components = []
@@ -156,11 +200,11 @@ private
156
200
  components << date.year
157
201
  components.join('/')
158
202
  end
159
-
203
+
160
204
  def compose_time(time_string)
161
205
  return unless time_string.present?
162
206
  hours, minutes = time_string.split(':').map(&:to_i)
163
-
207
+
164
208
  hours * 60 + minutes
165
209
  end
166
210
  end
@@ -1,16 +1,16 @@
1
1
  class XTeamSchedule::Parser
2
-
2
+
3
3
  attr_accessor :hash, :schedule
4
-
4
+
5
5
  def self.parse(hash)
6
6
  new(hash).parse
7
7
  end
8
-
8
+
9
9
  def initialize(hash)
10
10
  self.hash = hash
11
11
  self.schedule = XTeamSchedule::Schedule.create!
12
12
  end
13
-
13
+
14
14
  def parse
15
15
  parse_resource_groups!
16
16
  parse_resources!
@@ -19,27 +19,29 @@ class XTeamSchedule::Parser
19
19
  parse_working_times!
20
20
  parse_interface!
21
21
  parse_weekly_working_schedule!
22
+ parse_holidays!
22
23
  parse_schedule!
23
24
  schedule
24
25
  end
25
-
26
+
26
27
  private
27
-
28
+
28
29
  def parse_resource_groups!
30
+ return unless hash['resource groups'].present?
29
31
  hash['resource groups'].each do |rg|
30
32
  schedule.resource_groups.create!(
31
33
  :name => rg['name'],
32
34
  :expanded_in_library => rg['expanded in library']
33
35
  )
34
36
  end
35
- rescue
36
37
  end
37
-
38
+
38
39
  def parse_resources!
40
+ return unless hash['resources'].present?
39
41
  hash['resources'].each do |r|
40
42
  resource_group = schedule.resource_groups.find_by_name(r['group'])
41
43
  if resource_group
42
- image = r['image'].class == StringIO ? r['image'].read : ''
44
+ image = r['image'].class == StringIO ? Base64.encode64(r['image'].read) : ''
43
45
  resource_group.resources.create!(
44
46
  :displayed_in_planning => r['displayedInPlanning'],
45
47
  :email => r['email'],
@@ -50,20 +52,20 @@ private
50
52
  )
51
53
  end
52
54
  end
53
- rescue
54
55
  end
55
-
56
+
56
57
  def parse_assignment_groups!
58
+ return unless hash['task categories'].present?
57
59
  hash['task categories'].each do |ag|
58
60
  schedule.assignment_groups.create!(
59
61
  :name => ag['name'],
60
62
  :expanded_in_library => ag['expanded in library']
61
63
  )
62
64
  end
63
- rescue
64
65
  end
65
-
66
+
66
67
  def parse_assignments!
68
+ return unless hash['tasks'].present?
67
69
  hash['tasks'].each do |a|
68
70
  assignment_group = schedule.assignment_groups.find_by_name(a['category'])
69
71
  if assignment_group
@@ -73,13 +75,12 @@ private
73
75
  )
74
76
  end
75
77
  end
76
- rescue
77
78
  end
78
-
79
+
79
80
  def parse_working_times!
81
+ return unless hash['objectsForResources'].present?
80
82
  resources = schedule.resource_groups.map(&:resources).flatten
81
83
  assignments = schedule.assignment_groups.map(&:assignments).flatten
82
- hash['objectsForResources'] ||= {}
83
84
  hash['objectsForResources'].each do |r_name, wt_array|
84
85
  resource = resources.detect { |r| r.name == r_name }
85
86
  next unless resource
@@ -95,10 +96,10 @@ private
95
96
  end
96
97
  end
97
98
  end
98
-
99
+
99
100
  def parse_interface!
100
- interface_status = hash['interface status']
101
- time_granularity = interface_status['latest time navigation mode'] if interface_status.present?
101
+ return unless hash['interface status'].present?
102
+ time_granularity = hash['interface status']['latest time navigation mode']
102
103
  schedule.interface.update_attributes!(
103
104
  :display_assignments_name => hash['display task names'],
104
105
  :display_resources_name => hash['display resource names'],
@@ -110,31 +111,31 @@ private
110
111
  :time_granularity => time_granularity
111
112
  )
112
113
  end
113
-
114
+
114
115
  def parse_weekly_working_schedule!
115
116
  settings = hash['settings']
116
117
  return unless settings.present?
117
118
  working_schedule = settings['working schedule']
118
119
  return unless working_schedule.present?
119
-
120
+
120
121
  weekly_working_schedule = schedule.weekly_working_schedule
121
122
  working_days = weekly_working_schedule.working_days
122
-
123
+
123
124
  working_days.destroy_all
124
125
  XTeamSchedule::WorkingDay::WORKING_DAY_NAMES.each do |name|
125
126
  day = working_schedule[name.downcase]
126
127
  pause = working_schedule["pause_#{name.downcase}"]
127
-
128
+
128
129
  if day.present?
129
130
  day_begin = parse_time(day['begin']) if day['worked'] == 'yes'
130
131
  day_end = parse_time(day['end']) if day_begin
131
132
  end
132
-
133
+
133
134
  if pause.present?
134
135
  break_begin = parse_time(pause['begin']) if pause['worked'] == 'yes'
135
136
  break_end = parse_time(pause['end']) if break_begin
136
137
  end
137
-
138
+
138
139
  working_days << XTeamSchedule::WorkingDay.create!(
139
140
  :name => name,
140
141
  :day_begin => day_begin, :day_end => day_end,
@@ -142,29 +143,74 @@ private
142
143
  )
143
144
  end
144
145
  end
145
-
146
+
147
+ def parse_holidays!
148
+ parse_schedule_holidays!
149
+ parse_resource_holidays!
150
+ end
151
+
152
+ def parse_schedule_holidays!
153
+ settings = hash['settings']
154
+ return unless settings.present?
155
+ holidays = settings['days off']
156
+ return unless holidays.present?
157
+
158
+ holidays.each do |h|
159
+ end_date = h['end date']
160
+ end_date = nil if h['begin date'] == h['end date']
161
+ schedule.holidays.create!(
162
+ :begin_date => parse_date(h['begin date']),
163
+ :end_date => parse_date(end_date),
164
+ :name => h['name']
165
+ )
166
+ end
167
+ end
168
+
169
+ def parse_resource_holidays!
170
+ resources = schedule.resource_groups.map(&:resources).flatten
171
+ hash['resources'] ||= []
172
+ hash['resources'].each do |resource|
173
+ settings = resource['settings']
174
+ next unless settings.present?
175
+ holidays = settings['days off']
176
+ next unless holidays.present?
177
+ resource = resources.detect { |r| r.name == resource['name'] }
178
+ next unless resource.present?
179
+
180
+ holidays.each do |h|
181
+ end_date = h['end date']
182
+ end_date = nil if h['begin date'] == h['end date']
183
+ resource.holidays.create!(
184
+ :begin_date => parse_date(h['begin date']),
185
+ :end_date => parse_date(end_date),
186
+ :name => h['name']
187
+ )
188
+ end
189
+ end
190
+ end
191
+
146
192
  def parse_schedule!
147
193
  schedule.update_attributes!(
148
194
  :begin_date => parse_date(hash['begin date']),
149
195
  :end_date => parse_date(hash['end date'])
150
196
  )
151
197
  end
152
-
198
+
153
199
  def parse_colour(colour_data)
154
200
  [:red, :green, :blue].inject({}) { |h, c| h[c] = colour_data[c.to_s]; h }
155
201
  end
156
-
202
+
157
203
  def parse_date(date_string)
158
204
  return unless date_string.present?
159
205
  month, day, year = date_string.split('/').map(&:to_i)
160
206
  Date.new(year, month, day)
161
207
  end
162
-
208
+
163
209
  def parse_time(seconds)
164
210
  return unless seconds.present?
165
211
  hours = seconds / 60
166
212
  minutes = seconds % 60
167
-
213
+
168
214
  hours = "%02d" % hours
169
215
  minutes = "%02d" % minutes
170
216
  [hours, minutes].join(':')
@@ -0,0 +1,16 @@
1
+ class XTeamSchedule::Holiday < XTeamSchedule::Base
2
+ belongs_to :schedule
3
+ belongs_to :resource
4
+
5
+ validates_presence_of :begin_date
6
+ validate :can_not_belong_to_both_schedule_and_resource
7
+
8
+ private
9
+
10
+ def can_not_belong_to_both_schedule_and_resource
11
+ if [schedule, resource].all?
12
+ errors.add(:base, 'Can not belong to both a schedule and a resource')
13
+ end
14
+ end
15
+
16
+ end
@@ -1,22 +1,23 @@
1
1
  class XTeamSchedule::Resource < XTeamSchedule::Base
2
2
  belongs_to :resource_group
3
3
  has_many :working_times, :dependent => :destroy
4
+ has_many :holidays, :dependent => :destroy
4
5
  delegate :schedule, :to => :resource_group
5
-
6
+
6
7
  validates_presence_of :name
7
8
  validate :uniqueness_of_name_scoped_to_schedule
8
-
9
+
9
10
  private
10
-
11
+
11
12
  def uniqueness_of_name_scoped_to_schedule
12
13
  return unless new_record?
13
14
  resource_group = self.resource_group or return
14
15
  schedule = resource_group.schedule or return
15
16
  resources = schedule.resources or return
16
-
17
+
17
18
  if resources.find_by_name(name).present?
18
19
  errors.add(:name, 'must be unique within the schedule')
19
20
  end
20
21
  end
21
-
22
+
22
23
  end
@@ -4,10 +4,11 @@ class XTeamSchedule::Schedule < XTeamSchedule::Base
4
4
  has_many :assignment_groups, :dependent => :destroy
5
5
  has_many :assignments, :through => :assignment_groups
6
6
  has_many :working_days, :through => :weekly_working_schedule
7
-
7
+ has_many :holidays, :dependent => :destroy
8
+
8
9
  has_one :interface
9
10
  has_one :weekly_working_schedule
10
-
11
+
11
12
  ActiveSupport::Deprecation.silence do
12
13
  after_initialize :set_default_interface
13
14
  after_initialize :set_default_weekly_working_schedule
@@ -16,26 +17,20 @@ class XTeamSchedule::Schedule < XTeamSchedule::Base
16
17
  set_default_weekly_working_schedule
17
18
  end
18
19
  end
19
-
20
+
20
21
  def working_times
21
- ids = XTeamSchedule::WorkingTime.find_by_sql([
22
- "select distinct * from working_times as wt
23
- join resources r on wt.resource_id = r.id
24
- join resource_groups rg on r.resource_group_id = rg.id
25
- where rg.schedule_id = ?", id
26
- ]).map(&:id)
27
-
22
+ ids = resources.map(&:working_times).flatten.map(&:id)
28
23
  XTeamSchedule::WorkingTime.scoped(:conditions => { :id => ids })
29
24
  end
30
-
25
+
31
26
  private
32
-
27
+
33
28
  def set_default_interface
34
29
  self.interface ||= XTeamSchedule::Interface.new
35
30
  end
36
-
31
+
37
32
  def set_default_weekly_working_schedule
38
33
  self.weekly_working_schedule ||= XTeamSchedule::WeeklyWorkingSchedule.new
39
34
  end
40
-
35
+
41
36
  end
@@ -19,5 +19,6 @@ require 'xteam_schedule/models/schedule'
19
19
  require 'xteam_schedule/models/weekly_working_schedule'
20
20
  require 'xteam_schedule/models/working_day'
21
21
  require 'xteam_schedule/models/working_time'
22
+ require 'xteam_schedule/models/holiday'
22
23
 
23
24
  XTeamSchedule::Base.build_schema
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: xteam_schedule
3
3
  version: !ruby/object:Gem::Version
4
- hash: 23
4
+ hash: 27
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
+ - 1
8
9
  - 0
9
- - 4
10
- version: 0.0.4
10
+ version: 0.1.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Christopher Patuzzo
@@ -110,6 +110,7 @@ files:
110
110
  - lib/xteam_schedule/facilitation/schema.rb
111
111
  - lib/xteam_schedule/models/assignment.rb
112
112
  - lib/xteam_schedule/models/assignment_group.rb
113
+ - lib/xteam_schedule/models/holiday.rb
113
114
  - lib/xteam_schedule/models/interface.rb
114
115
  - lib/xteam_schedule/models/resource.rb
115
116
  - lib/xteam_schedule/models/resource_group.rb