xteam_schedule 0.0.4 → 0.1.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.
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