xteam_schedule 0.0.1

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 ADDED
@@ -0,0 +1,324 @@
1
+ ## Introduction
2
+
3
+ xTeam Schedule is a gem that provides full control over schedules for use with [adnX's xTeam](http://www.adnx.com/i/apps/xteam4mac) software.
4
+
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
+
7
+ <img src="http://www.adnx.com/i/uploads/xTeam1.jpg" width="820" alt="xTeam Schedule" />
8
+
9
+ ### Features:
10
+
11
+ * **Read and write schedules** and interact with in-memory models through the ActiveRecord interface
12
+ * **Customise everything**; resources, assignments, groups, colours, interface settings..
13
+ * **Intuitive naming** of models, that correspond to what you see on screen
14
+ * **Full test coverage**, giving confidence to highly dynamic businesses everywhere
15
+
16
+ ### Disclaimer
17
+
18
+ I am in no way associated with adnX. I work for an agile development company that makes use of xTeam. This project is open-source.
19
+
20
+ ## Getting Started
21
+
22
+ It is not required that you have [xTeam](http://www.adnx.com/i/apps/xteam4mac) installed. However, you will not be able to visualise your schedules otherwise.
23
+
24
+ **Install the gem:**
25
+
26
+ ```ruby
27
+ gem install xteam_schedule
28
+ ```
29
+
30
+ **Require it in your project:**
31
+
32
+ ```ruby
33
+ require 'xteam_schedule'
34
+ ```
35
+
36
+ You may need to require 'rubygems' too, if you are running an old version of Ruby.
37
+
38
+ **Create a Schedule**
39
+
40
+ You can create a new schedule, or read one from a file:
41
+
42
+ ```ruby
43
+ schedule = XTeamSchedule.new
44
+ schedule = XTeamSchedule.new('path/to/file.xtps')
45
+ ```
46
+
47
+ ## Schedules
48
+
49
+ Schedules are the top level model through which you access everything. The inspect method is custom-made to give you an overview of the contents of the schedule:
50
+
51
+ ```ruby
52
+ XTeamSchedule.new('path/to/file.xtps')
53
+ => #<XTeamSchedule resoruce_groups(9), resources(42), assignment_groups(14), assignments(118), working_times(79)>
54
+ ```
55
+
56
+ A schedule has many resource groups and assignment groups. It also has many resources and assignments through resource groups and assignment groups respectively. Finally, a schedule has many working times through either resource groups then resources or assignment groups then assignments.
57
+
58
+ ```ruby
59
+ schedule = XTeamSchedule.new('path/to/file.xtps')
60
+
61
+ resource_groups = schedule.resource_groups
62
+ resources = schedule.resources # or schedule.resource_groups.map(&:resources).flatten
63
+ assignment_groups = schedule.assignment_groups
64
+ assignments = schedule.assignments
65
+ working_times = schedule.working_times
66
+ ```
67
+
68
+ There are numerous other models, for example a schedule has one 'interface' which contains various display settings. These are explained in detail below. It is also possible to convert a schedule to/from a hash. After serialisation, this could easily be written to a database.
69
+
70
+ ```ruby
71
+ hash = schedule.hash
72
+ schedule = XTeamSchedule.new(hash)
73
+ ```
74
+
75
+ ## Resource Groups
76
+
77
+ Resource groups contain resources. Typical names for resource groups might be 'Management', 'Sales', or 'Developers'.
78
+
79
+ ```ruby
80
+ schedule.resource_groups.create!(
81
+ :name => 'Management',
82
+ :expanded_in_library => true
83
+ )
84
+ ```
85
+
86
+ **Required attributes:**
87
+ name
88
+
89
+ **Defaults:**
90
+ expanded_in_library => true
91
+
92
+ **Examples queries:**
93
+
94
+ ```ruby
95
+ resource_groups = schedule.resource_groups
96
+
97
+ number_of_groups = resource_groups.count
98
+ junior_developers = resource_groups.find_by_name('Junior Developers').resources
99
+ developer_groups = resource_groups.where('name like "%developer%"')
100
+ ```
101
+
102
+ ## Resources
103
+
104
+ A resource is an employee. A resource can not be in multiple resource groups.
105
+
106
+ ```ruby
107
+ developers = schedule.resource_groups.create!(:name => 'Developers')
108
+ developers.resources.create!(
109
+ :name => 'Christopher Patuzzo',
110
+ :email => 'chris@example.com',
111
+ :mobile => '0123456789',
112
+ :phone => '9876543210',
113
+ :displayed_in_planning => true
114
+ )
115
+ ```
116
+
117
+ **Required attributes:**
118
+ name
119
+
120
+ **Defaults:**
121
+ displayed_in_planning => true
122
+
123
+ **Example queries:**
124
+
125
+ ```ruby
126
+ resources = schedule.resources
127
+
128
+ chris_mobile = resources.find_by_name('Christopher Patuzzo').mobile
129
+ gmail_resource_names = resources.where('email like "%gmail%"').map(&:name)
130
+ resources.each { |r| r.update_attribute(:displayed_in_planning, true) }
131
+ ```
132
+
133
+ ## Assignment Groups
134
+
135
+ Assignment groups are almost identical to resource groups. Typical names might be 'Training' and 'Research'.
136
+
137
+ ```ruby
138
+ schedule.assignment_groups.create!(
139
+ :name => 'Training',
140
+ :expanded_in_library => true
141
+ )
142
+ ```
143
+
144
+ **Required attributes:**
145
+ name
146
+
147
+ **Defaults:**
148
+ expanded_in_library => true
149
+
150
+ **Example queries:**
151
+
152
+ ```ruby
153
+ assignment_groups = schedule.assignment_groups
154
+
155
+ final_group_name = assignment_groups.last.name
156
+ expanded_groups = assignment_groups.where(:expanded_in_library => true)
157
+ visible_assignment_count = expanded_groups.map(&:assignments).map(&:count).inject(:+)
158
+ ```
159
+
160
+ ## Assignments
161
+
162
+ An assignment is a task. An assignment cannot be in multiple assignment groups.
163
+
164
+ ```ruby
165
+ training = schedule.assignment_groups.create!(:name => 'Training')
166
+ training.assignments.create!(
167
+ :name => 'Rails Conference',
168
+ :colour => { :red => 1, :green => 0, :blue => 0 }
169
+ )
170
+ ```
171
+
172
+ **Required attributes:**
173
+ name
174
+
175
+ **Defaults:**
176
+ colour => { :red => 0.5, :green => 0.5, :blue => 0.5 }
177
+
178
+ **Aliases:**
179
+ color => colour
180
+
181
+ **Example queries:**
182
+
183
+ ```ruby
184
+ assignments = schedule.assignments
185
+
186
+ rails_assignments = assignments.where('name like "%rails%"')
187
+ first_assignment_colour = assignments.first.colour
188
+ singleton_assignments = assignments.select { |a| a.assignment_group.assignments.count == 1 }
189
+ ```
190
+
191
+ ## Working Times
192
+
193
+ A working time is a relationship between a resource and an assignment. This is equivalent to scheduling an employee on a specific task for a given duration. Assignments are assigned to resources by creating working times.
194
+
195
+ ```ruby
196
+ developers = schedule.resource_groups.create!(:name => 'Developers')
197
+ chris = developers.resources.create!(:name => 'Christopher Patuzzo')
198
+
199
+ channel_5 = schedule.assignment_groups.create!(:name => 'Channel 5')
200
+ the_gadget_show = channel_5.assignments.create!(:name => 'The Gadget Show')
201
+
202
+ chris.working_times.create!(
203
+ :assignment => the_gadget_show,
204
+ :begin_date => Date.new(2012, 01, 01),
205
+ :duration => 20,
206
+ :notes => 'Based in London'
207
+ )
208
+ ```
209
+
210
+ The creation can also be written from the assignment, or directly from the model:
211
+
212
+ ```ruby
213
+ the_gadget_show.working_times.create!(
214
+ :resource => chris,
215
+ # etc.
216
+ )
217
+
218
+ XTeamSchedule::WorkingTime.create!(
219
+ :resource => chris,
220
+ :assignment => the_gadget_show,
221
+ # etc.
222
+ )
223
+ ```
224
+
225
+ **Required attributes:**
226
+ begin_date, duration
227
+
228
+ **Example queries:**
229
+
230
+ ```ruby
231
+ working_times = schedule.working_times
232
+
233
+ maximum_duration = working_times.map(&:duration).max.to_s + ' days'
234
+ recent_working_times = working_times.where('begin_date > ?', Date.new(2012, 01, 01))
235
+ resources_on_new_projects = recent_working_times.map(&:resource).uniq.map(&:name)
236
+ ```
237
+
238
+ ## Interface
239
+
240
+ A schedule has one interface that is created automatically. The interface is responsible for the display settings of xTeam. Possible values to pass to the granularities constant are: :day, :week, :month and :year
241
+
242
+ ```ruby
243
+ schedule.interface.update_attributes!(
244
+ :display_assignments_name => true,
245
+ :display_resources_name => false,
246
+ :display_working_hours => false,
247
+ :display_resources_pictures => true,
248
+ :display_total_of_working_hours => false,
249
+ :display_assignments_notes => true,
250
+ :display_absences => true,
251
+ :time_granularity => XTeamSchedule::Interface::TIME_GRANULARITIES[:month]
252
+ )
253
+ ```
254
+
255
+ **Defaults:**
256
+ The same as the attributes shown above.
257
+
258
+ **Aliases:**
259
+
260
+ ```ruby
261
+ display_assignment_names => display_assignments_name
262
+ display_resource_names => display_resources_name
263
+ display_resource_pictures => display_resources_pictures
264
+ display_total_working_hours => display_total_of_working_hours
265
+ display_assignment_notes => display_assignments_notes
266
+ ```
267
+
268
+ ## Weekly Working Schedule
269
+
270
+ The weekly working schedule determines the 'opening hours' of the company. Working days can be accessed directly, or through weekly_working_schedule.
271
+
272
+ ```ruby
273
+ # Set all days to start at 9am, and finish at 5pm
274
+ # Set a lunch break from 12pm - 1pm each day
275
+ working_days = schedule.working_days
276
+ working_days.each do |day|
277
+ day.update_attributes!(
278
+ :day_begin => '09:00',
279
+ :day_end => '17:00',
280
+ :break_begin => '12:00',
281
+ :break_end => '13:00'
282
+ )
283
+ end
284
+ ```
285
+
286
+ Non working days are determined by setting the 'day_begin' attribute to nil. This works similarly for days without lunch breaks:
287
+
288
+ ```ruby
289
+ wednesday = working_days.find_by_name('Wednesday')
290
+ saturday = working_days.find_by_name('Saturday')
291
+ sunday = working_days.find_by_name('Sunday')
292
+
293
+ [saturday, sunday].each { |day| day.update_attributes!(:day_begin => nil) }
294
+ wednesday.update_attribute(:break_begin, nil)
295
+ ```
296
+
297
+ **Defaults:**
298
+ The default weekly working schedule is identical to the same as the one set up above. i.e. 9am-5pm Mon-Fri, with lunch from 12pm-1pm.
299
+
300
+ ## Under Development
301
+
302
+ This gem is far from complete. The following is a list of features that are under development:
303
+
304
+ * Resource images
305
+ * Sort by
306
+ * Holidays
307
+ * Absences
308
+ * Remote access
309
+ * To assign
310
+ * Advanced colour controls
311
+ * Schedule splicing between dates
312
+ * Advanced image support
313
+ * Built-in example
314
+ * Documentation
315
+ * Binary operators
316
+ * File system hooks
317
+ * Date/duration helpers
318
+ * Generating reports
319
+
320
+ ## Contribution
321
+
322
+ Please feel free to contribute, either through pull requests or feature requests here on Github.
323
+
324
+ For news and latest updates, follow me on Twitter ([@cpatuzzo](https://twitter.com/#!/cpatuzzo)).
@@ -0,0 +1,31 @@
1
+ class XTeamSchedule
2
+
3
+ def initialize(hash_or_filename = nil)
4
+ if hash_or_filename.present?
5
+ hash = hash_or_filename.class == Hash ? hash_or_filename : IO.read(hash_or_filename)
6
+ @schedule = Parser.parse(hash)
7
+ else
8
+ @schedule = Schedule.create!
9
+ end
10
+ end
11
+
12
+ def write(filename)
13
+ raise 'No filename provided' unless filename.present?
14
+ IO.write(hash, filename)
15
+ end
16
+
17
+ def hash
18
+ Composer.compose(self)
19
+ end
20
+
21
+ def method_missing(*args);
22
+ @schedule.send(*args)
23
+ end
24
+
25
+ def inspect
26
+ stats = [:resource_groups, :resources, :assignment_groups, :assignments, :working_times].map { |s| [s, send(s).count] }
27
+ stats_string = stats.map { |s| "#{s.first}(#{s.second})" }.join(', ')
28
+ "#<XTeamSchedule #{stats_string}>"
29
+ end
30
+
31
+ end
@@ -0,0 +1,166 @@
1
+ class XTeamSchedule::Composer
2
+
3
+ attr_accessor :schedule, :hash
4
+
5
+ def self.compose(schedule)
6
+ new(schedule).compose
7
+ end
8
+
9
+ def initialize(schedule)
10
+ schedule.save!
11
+ self.schedule = schedule
12
+ self.hash = {}
13
+ end
14
+
15
+ def compose
16
+ compose_resource_groups!
17
+ compose_resources!
18
+ compose_assignment_groups!
19
+ compose_assignments!
20
+ compose_working_times!
21
+ compose_interface!
22
+ compose_weekly_working_schedule!
23
+ compose_schedule!
24
+ hash
25
+ end
26
+
27
+ private
28
+
29
+ def compose_resource_groups!
30
+ hash['resource groups'] ||= []
31
+ resource_groups = schedule.resource_groups
32
+ resource_groups.each do |rg|
33
+ hash['resource groups'] << {
34
+ 'name' => rg.name,
35
+ 'expanded in library' => rg.expanded_in_library
36
+ }
37
+ end
38
+ end
39
+
40
+ def compose_resources!
41
+ hash['resources'] ||= []
42
+ resources = schedule.resource_groups.map(&:resources).flatten
43
+ resources.each do |r|
44
+ hash['resources'] << {
45
+ 'displayedInPlanning' => r.displayed_in_planning,
46
+ 'email' => r.email,
47
+ 'image' => (StringIO.new(r.image) if r.image),
48
+ 'mobile' => r.mobile,
49
+ 'name' => r.name,
50
+ 'phone' => r.phone,
51
+ 'group' => r.resource_group.name
52
+ }
53
+ end
54
+ end
55
+
56
+ def compose_assignment_groups!
57
+ hash['task categories'] ||= []
58
+ assignment_groups = schedule.assignment_groups
59
+ assignment_groups.each do |ag|
60
+ hash['task categories'] << {
61
+ 'name' => ag.name,
62
+ 'expanded in library' => ag.expanded_in_library
63
+ }
64
+ end
65
+ end
66
+
67
+ def compose_assignments!
68
+ hash['tasks'] ||= []
69
+ assignments = schedule.assignment_groups.map(&:assignments).flatten
70
+ assignments.each do |a|
71
+ hash['tasks'] << {
72
+ 'name' => a.name,
73
+ 'category' => a.assignment_group.name,
74
+ 'kind' => 0,
75
+ 'color' => compose_colour(a.colour)
76
+ }
77
+ end
78
+ end
79
+
80
+ def compose_working_times!
81
+ hash['objectsForResources'] ||= {}
82
+ resources = schedule.resource_groups.map(&:resources).flatten
83
+ resources.each do |r|
84
+ working_times_with_parents = r.working_times.select { |wt| wt.resource and wt.assignment }
85
+ next unless working_times_with_parents.any?
86
+ hash['objectsForResources'].merge!(r.name => [])
87
+ working_times_with_parents.each do |wt|
88
+ hash['objectsForResources'][r.name] << {
89
+ 'task' => wt.assignment.name,
90
+ 'begin date' => compose_date(wt.begin_date),
91
+ 'duration' => wt.duration,
92
+ 'notes' => wt.notes,
93
+ 'title' => ''
94
+ }
95
+ end
96
+ end
97
+ end
98
+
99
+ def compose_interface!
100
+ interface = schedule.interface
101
+ hash['display task names'] = interface.display_assignments_name
102
+ hash['display resource names'] = interface.display_resources_name
103
+ hash['display worked time'] = interface.display_working_hours
104
+ hash['display resource icons'] = interface.display_resources_pictures
105
+ hash['display resource totals'] = interface.display_total_of_working_hours
106
+ hash['display task notes'] = interface.display_assignments_notes
107
+ hash['display absence cells'] = interface.display_absences
108
+ hash['interface status'] = { 'latest time navigation mode' => interface.time_granularity }
109
+ end
110
+
111
+ def compose_weekly_working_schedule!
112
+ weekly_working_schedule = schedule.weekly_working_schedule
113
+ working_days = weekly_working_schedule.working_days
114
+
115
+ hash['settings'] ||= {}
116
+ hash['settings']['days off'] ||= []
117
+ hash['settings']['working schedule'] ||= {}
118
+
119
+ working_days.each do |day|
120
+ day_name = day.name.downcase
121
+ hash['settings']['working schedule'][day_name] =
122
+ if day.day_begin.present?
123
+ { 'worked' => 'yes',
124
+ 'begin' => compose_time(day.day_begin),
125
+ 'end' => compose_time(day.day_end) }
126
+ else
127
+ { 'worked' => 'no' }
128
+ end
129
+
130
+ hash['settings']['working schedule']["pause_#{day_name}"] =
131
+ if day.day_begin.present? and day.break_begin.present?
132
+ { 'worked' => 'yes',
133
+ 'begin' => compose_time(day.break_begin),
134
+ 'end' => compose_time(day.break_end),
135
+ 'duration' => compose_time(day.break_end) - compose_time(day.break_begin) }
136
+ else
137
+ {}
138
+ end
139
+ end
140
+ end
141
+
142
+ def compose_schedule!
143
+ hash['begin date'] = compose_date(schedule.begin_date)
144
+ hash['end date'] = compose_date(schedule.end_date)
145
+ end
146
+
147
+ def compose_colour(colour_hash)
148
+ { 'alpha' => 1 }.merge([:red, :green, :blue].inject({}) { |h, c| h[c.to_s] = colour_hash[c]; h })
149
+ end
150
+
151
+ def compose_date(date)
152
+ return unless date.present?
153
+ components = []
154
+ components << ("%02d" % date.month)
155
+ components << ("%02d" % date.day)
156
+ components << date.year
157
+ components.join('/')
158
+ end
159
+
160
+ def compose_time(time_string)
161
+ return unless time_string.present?
162
+ hours, minutes = time_string.split(':').map(&:to_i)
163
+
164
+ hours * 60 + minutes
165
+ end
166
+ end
@@ -0,0 +1,83 @@
1
+ class XTeamSchedule::DB
2
+
3
+ def self.connect
4
+ ActiveRecord::Base.establish_connection(
5
+ :adapter => 'sqlite3',
6
+ :database => ':memory:'
7
+ )
8
+ end
9
+
10
+ def self.build_schema
11
+ ActiveRecord::Schema.verbose = false
12
+ ActiveRecord::Schema.define do
13
+
14
+ create_table :schedules do |table|
15
+ table.column :begin_date, :date, :default => 10.years.ago.to_date
16
+ table.column :end_date, :date, :default => 10.years.from_now.to_date
17
+ end
18
+
19
+ create_table :interfaces do |table|
20
+ table.column :schedule_id, :integer
21
+ table.column :display_assignments_name, :boolean, :default => true
22
+ table.column :display_resources_name, :boolean, :default => false
23
+ table.column :display_working_hours, :boolean, :default => false
24
+ table.column :display_resources_pictures, :boolean, :default => true
25
+ table.column :display_total_of_working_hours, :boolean, :default => false
26
+ table.column :display_assignments_notes, :boolean, :default => true
27
+ table.column :display_absences, :boolean, :default => true
28
+ table.column :time_granularity, :integer, :default => XTeamSchedule::Interface::TIME_GRANULARITIES[:month]
29
+ end
30
+
31
+ create_table :weekly_working_schedules do |table|
32
+ table.column :schedule_id, :integer
33
+ end
34
+
35
+ create_table :working_days do |table|
36
+ table.column :weekly_working_schedule_id, :integer
37
+ table.column :name, :string
38
+ table.column :day_begin, :string
39
+ table.column :day_end, :string
40
+ table.column :break_begin, :string
41
+ table.column :break_end, :string
42
+ end
43
+
44
+ create_table :resource_groups do |table|
45
+ table.column :schedule_id, :integer
46
+ table.column :expanded_in_library, :boolean, :default => true
47
+ table.column :name, :string
48
+ end
49
+
50
+ create_table :resources do |table|
51
+ table.column :resource_group_id, :integer
52
+ table.column :displayed_in_planning, :boolean, :default => true
53
+ table.column :email, :string
54
+ table.column :image, :string
55
+ table.column :mobile, :string
56
+ table.column :name, :string
57
+ table.column :phone, :string
58
+ end
59
+
60
+ create_table :assignment_groups do |table|
61
+ table.column :schedule_id, :integer
62
+ table.column :expanded_in_library, :boolean, :default => true
63
+ table.column :name, :string
64
+ end
65
+
66
+ create_table :assignments do |table|
67
+ table.column :assignment_group_id, :integer
68
+ table.column :name, :string
69
+ table.column :colour, :string
70
+ end
71
+
72
+ create_table :working_times do |table|
73
+ table.column :resource_id, :integer
74
+ table.column :assignment_id, :integer
75
+ table.column :begin_date, :date
76
+ table.column :duration, :integer
77
+ table.column :notes, :string
78
+ end
79
+
80
+ end
81
+ end
82
+
83
+ end
@@ -0,0 +1,11 @@
1
+ class XTeamSchedule::IO
2
+
3
+ def self.read(filename)
4
+ Plist.parse_xml(filename)
5
+ end
6
+
7
+ def self.write(hash, filename)
8
+ hash.save_plist(filename)
9
+ end
10
+
11
+ end
@@ -0,0 +1,168 @@
1
+ class XTeamSchedule::Parser
2
+
3
+ attr_accessor :hash, :schedule
4
+
5
+ def self.parse(hash)
6
+ new(hash).parse
7
+ end
8
+
9
+ def initialize(hash)
10
+ self.hash = hash
11
+ self.schedule = XTeamSchedule::Schedule.create!
12
+ end
13
+
14
+ def parse
15
+ parse_resource_groups!
16
+ parse_resources!
17
+ parse_assignment_groups!
18
+ parse_assignments!
19
+ parse_working_times!
20
+ parse_interface!
21
+ parse_weekly_working_schedule!
22
+ parse_schedule!
23
+ schedule
24
+ end
25
+
26
+ private
27
+
28
+ def parse_resource_groups!
29
+ hash['resource groups'].try(:each) do |rg|
30
+ schedule.resource_groups.create!(
31
+ :name => rg['name'],
32
+ :expanded_in_library => rg['expanded in library']
33
+ )
34
+ end
35
+ end
36
+
37
+ def parse_resources!
38
+ hash['resources'].try(:each) do |r|
39
+ resource_group = schedule.resource_groups.find_by_name(r['group'])
40
+ if resource_group
41
+ image = r['image'].class == StringIO ? r['image'].read : ''
42
+ resource_group.resources.create!(
43
+ :displayed_in_planning => r['displayedInPlanning'],
44
+ :email => r['email'],
45
+ :image => image,
46
+ :mobile => r['mobile'],
47
+ :name => r['name'],
48
+ :phone => r['phone']
49
+ )
50
+ end
51
+ end
52
+ end
53
+
54
+ def parse_assignment_groups!
55
+ hash['task categories'].try(:each) do |ag|
56
+ schedule.assignment_groups.create!(
57
+ :name => ag['name'],
58
+ :expanded_in_library => ag['expanded in library']
59
+ )
60
+ end
61
+ end
62
+
63
+ def parse_assignments!
64
+ hash['tasks'].try(:each) do |a|
65
+ assignment_group = schedule.assignment_groups.find_by_name(a['category'])
66
+ if assignment_group
67
+ assignment_group.assignments.create!(
68
+ :name => a['name'],
69
+ :colour => parse_colour(a['color'])
70
+ )
71
+ end
72
+ end
73
+ end
74
+
75
+ def parse_working_times!
76
+ resources = schedule.resource_groups.map(&:resources).flatten
77
+ assignments = schedule.assignment_groups.map(&:assignments).flatten
78
+ hash['objectsForResources'] ||= {}
79
+ hash['objectsForResources'].each do |r_name, wt_array|
80
+ resource = resources.detect { |r| r.name == r_name }
81
+ next unless resource
82
+ wt_array.each do |wt|
83
+ assignment = assignments.detect { |a| a.name == wt['task'] }
84
+ next unless assignment
85
+ resource.working_times.create!(
86
+ :assignment => assignment,
87
+ :begin_date => parse_date(wt['begin date']),
88
+ :duration => wt['duration'],
89
+ :notes => wt['notes']
90
+ )
91
+ end
92
+ end
93
+ end
94
+
95
+ def parse_interface!
96
+ interface_status = hash['interface status']
97
+ time_granularity = interface_status['latest time navigation mode'] if interface_status.present?
98
+ schedule.interface.update_attributes!(
99
+ :display_assignments_name => hash['display task names'],
100
+ :display_resources_name => hash['display resource names'],
101
+ :display_working_hours => hash['display worked time'],
102
+ :display_resources_pictures => hash['display resource icons'],
103
+ :display_total_of_working_hours => hash['display resource totals'],
104
+ :display_assignments_notes => hash['display task notes'],
105
+ :display_absences => hash['display absence cells'],
106
+ :time_granularity => time_granularity
107
+ )
108
+ end
109
+
110
+ def parse_weekly_working_schedule!
111
+ settings = hash['settings']
112
+ return unless settings.present?
113
+ working_schedule = settings['working schedule']
114
+ return unless working_schedule.present?
115
+
116
+ weekly_working_schedule = schedule.weekly_working_schedule
117
+ working_days = weekly_working_schedule.working_days
118
+
119
+ working_days.destroy_all
120
+ XTeamSchedule::WorkingDay::WORKING_DAY_NAMES.each do |name|
121
+ day = working_schedule[name.downcase]
122
+ pause = working_schedule["pause_#{name.downcase}"]
123
+
124
+ if day.present?
125
+ day_begin = parse_time(day['begin']) if day['worked'] == 'yes'
126
+ day_end = parse_time(day['end']) if day_begin
127
+ end
128
+
129
+ if pause.present?
130
+ break_begin = parse_time(pause['begin']) if pause['worked'] == 'yes'
131
+ break_end = parse_time(pause['end']) if break_begin
132
+ end
133
+
134
+ working_days << XTeamSchedule::WorkingDay.create!(
135
+ :name => name,
136
+ :day_begin => day_begin, :day_end => day_end,
137
+ :break_begin => break_begin, :break_end => break_end
138
+ )
139
+ end
140
+ end
141
+
142
+ def parse_schedule!
143
+ schedule.update_attributes!(
144
+ :begin_date => parse_date(hash['begin date']),
145
+ :end_date => parse_date(hash['end date'])
146
+ )
147
+ end
148
+
149
+ def parse_colour(colour_data)
150
+ [:red, :green, :blue].inject({}) { |h, c| h[c] = colour_data[c.to_s]; h }
151
+ end
152
+
153
+ def parse_date(date_string)
154
+ return unless date_string.present?
155
+ month, day, year = date_string.split('/').map(&:to_i)
156
+ Date.new(year, month, day)
157
+ end
158
+
159
+ def parse_time(seconds)
160
+ return unless seconds.present?
161
+ hours = seconds / 60
162
+ minutes = seconds % 60
163
+
164
+ hours = "%02d" % hours
165
+ minutes = "%02d" % minutes
166
+ [hours, minutes].join(':')
167
+ end
168
+ end
@@ -0,0 +1,51 @@
1
+ class XTeamSchedule::Assignment < ActiveRecord::Base
2
+ belongs_to :assignment_group
3
+ has_many :working_times, :dependent => :destroy
4
+ delegate :schedule, :to => :assignment_group
5
+
6
+ validates_presence_of :name
7
+ validate :uniqueness_of_name_scoped_to_schedule
8
+ validates_presence_of :colour
9
+ validate :rgb_colour
10
+
11
+ serialize :colour, Hash
12
+ after_initialize :set_default_colour
13
+ before_save :symbolize_colour!
14
+ after_validation :float_colour_values!
15
+
16
+ alias_attribute :color, :colour
17
+
18
+ private
19
+
20
+ def uniqueness_of_name_scoped_to_schedule
21
+ return unless new_record?
22
+ assignment_group = self.assignment_group or return
23
+ schedule = assignment_group.schedule or return
24
+ assignments = schedule.assignments or return
25
+
26
+ if assignments.find_by_name(name).present?
27
+ errors.add(:name, 'must be unique within the schedule')
28
+ end
29
+ end
30
+
31
+ def set_default_colour
32
+ self.colour = { :red => 0.5, :green => 0.5, :blue => 0.5 } if colour.empty?
33
+ end
34
+
35
+ def symbolize_colour!
36
+ self.colour = colour.inject({}) { |h, (k, v)| h[k.to_sym] = v; h }
37
+ end
38
+
39
+ def float_colour_values!
40
+ self.colour = colour.inject({}) { |h, (k, v)| h[k] = v.to_f; h } if colour.class == Hash
41
+ end
42
+
43
+ def rgb_colour
44
+ is_out_of_range = proc { |c| f = Float(c); f < 0 || f > 1 }
45
+ raise 'invalid' if [:red, :green, :blue].any? { |c| is_out_of_range[colour[c]] }
46
+ raise 'invalid' if colour.count != 3
47
+ rescue
48
+ errors.add(:colour, 'is not a valid rgb hash')
49
+ end
50
+
51
+ end
@@ -0,0 +1,7 @@
1
+ class XTeamSchedule::AssignmentGroup < ActiveRecord::Base
2
+ belongs_to :schedule
3
+ has_many :assignments, :dependent => :destroy
4
+
5
+ validates_presence_of :name
6
+ validates_uniqueness_of :name, :scope => :schedule_id
7
+ end
@@ -0,0 +1,11 @@
1
+ class XTeamSchedule::Interface < ActiveRecord::Base
2
+ belongs_to :schedule
3
+
4
+ alias_attribute :display_assignment_names, :display_assignments_name
5
+ alias_attribute :display_resource_names, :display_resources_name
6
+ alias_attribute :display_resource_pictures, :display_resources_pictures
7
+ alias_attribute :display_total_working_hours, :display_total_of_working_hours
8
+ alias_attribute :display_assignment_notes, :display_assignments_notes
9
+
10
+ TIME_GRANULARITIES = { :day => 4, :week => 2, :month => 1, :year => 0 }
11
+ end
@@ -0,0 +1,22 @@
1
+ class XTeamSchedule::Resource < ActiveRecord::Base
2
+ belongs_to :resource_group
3
+ has_many :working_times, :dependent => :destroy
4
+ delegate :schedule, :to => :resource_group
5
+
6
+ validates_presence_of :name
7
+ validate :uniqueness_of_name_scoped_to_schedule
8
+
9
+ private
10
+
11
+ def uniqueness_of_name_scoped_to_schedule
12
+ return unless new_record?
13
+ resource_group = self.resource_group or return
14
+ schedule = resource_group.schedule or return
15
+ resources = schedule.resources or return
16
+
17
+ if resources.find_by_name(name).present?
18
+ errors.add(:name, 'must be unique within the schedule')
19
+ end
20
+ end
21
+
22
+ end
@@ -0,0 +1,7 @@
1
+ class XTeamSchedule::ResourceGroup < ActiveRecord::Base
2
+ belongs_to :schedule
3
+ has_many :resources, :dependent => :destroy
4
+
5
+ validates_presence_of :name
6
+ validates_uniqueness_of :name, :scope => :schedule_id
7
+ end
@@ -0,0 +1,25 @@
1
+ class XTeamSchedule::Schedule < ActiveRecord::Base
2
+ has_many :resource_groups, :dependent => :destroy
3
+ has_many :resources, :through => :resource_groups
4
+ has_many :assignment_groups, :dependent => :destroy
5
+ has_many :assignments, :through => :assignment_groups
6
+ has_many :working_times, :through => :resources
7
+ has_many :working_days, :through => :weekly_working_schedule
8
+
9
+ has_one :interface
10
+ after_initialize :set_default_interface
11
+
12
+ has_one :weekly_working_schedule
13
+ after_initialize :set_default_weekly_working_schedule
14
+
15
+ private
16
+
17
+ def set_default_interface
18
+ self.interface ||= XTeamSchedule::Interface.new
19
+ end
20
+
21
+ def set_default_weekly_working_schedule
22
+ self.weekly_working_schedule = XTeamSchedule::WeeklyWorkingSchedule.new
23
+ end
24
+
25
+ end
@@ -0,0 +1,24 @@
1
+ class XTeamSchedule::WeeklyWorkingSchedule < ActiveRecord::Base
2
+ belongs_to :schedule
3
+ has_many :working_days, :dependent => :destroy
4
+
5
+ after_initialize :set_default_working_days
6
+
7
+ private
8
+
9
+ def set_default_working_days
10
+ return unless self.working_days.empty?
11
+ day_names = XTeamSchedule::WorkingDay::WORKING_DAY_NAMES
12
+ weekdays = day_names[0..4]
13
+ day_names.each_with_index do |name, i|
14
+ self.working_days << XTeamSchedule::WorkingDay.new(
15
+ :name => name,
16
+ :day_begin => (weekdays.shift.present? ? '09:00' : nil),
17
+ :day_end => '17:00',
18
+ :break_begin => '12:00',
19
+ :break_end => '13:00'
20
+ )
21
+ end
22
+ end
23
+
24
+ end
@@ -0,0 +1,28 @@
1
+ class XTeamSchedule::WorkingDay < ActiveRecord::Base
2
+ belongs_to :weekly_working_schedule
3
+ delegate :schedule, :to => :weekly_working_schedule
4
+
5
+ validate :format_of_times
6
+
7
+ WORKING_DAY_NAMES = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
8
+
9
+ private
10
+
11
+ def format_of_times
12
+ time_format = /\d{2}:\d{2}/
13
+ [:day_begin, :day_end, :break_begin, :break_end].each do |sym|
14
+ time = send(sym)
15
+ return if sym == :day_begin and time.nil?
16
+ return if sym == :break_begin and time.nil?
17
+ unless time =~ time_format
18
+ errors.add(sym, "is not a valid 24-hour time, must be hh:mm format: #{time}")
19
+ next
20
+ end
21
+ hours, minutes = time.split(':').map(&:to_i)
22
+ if hours > 23 or minutes > 59
23
+ errors.add(sym, "is not a valid 24-hour time, must be hh:mm format: #{time}")
24
+ end
25
+ end
26
+ end
27
+
28
+ end
@@ -0,0 +1,9 @@
1
+ class XTeamSchedule::WorkingTime < ActiveRecord::Base
2
+ belongs_to :resource
3
+ belongs_to :assignment
4
+ delegate :resource_group, :to => :resource
5
+ delegate :assignment_group, :to => :assignment
6
+ delegate :schedule, :to => :resource
7
+
8
+ validates_presence_of :begin_date, :duration
9
+ end
@@ -0,0 +1,22 @@
1
+ require 'plist'
2
+ require 'active_record'
3
+ require 'sqlite3'
4
+ require 'xteam_schedule/core'
5
+
6
+ require 'xteam_schedule/models/assignment'
7
+ require 'xteam_schedule/models/assignment_group'
8
+ require 'xteam_schedule/models/interface'
9
+ require 'xteam_schedule/models/resource'
10
+ require 'xteam_schedule/models/resource_group'
11
+ require 'xteam_schedule/models/schedule'
12
+ require 'xteam_schedule/models/weekly_working_schedule'
13
+ require 'xteam_schedule/models/working_day'
14
+ require 'xteam_schedule/models/working_time'
15
+
16
+ require 'xteam_schedule/facilitation/composer'
17
+ require 'xteam_schedule/facilitation/db'
18
+ require 'xteam_schedule/facilitation/io'
19
+ require 'xteam_schedule/facilitation/parser'
20
+
21
+ XTeamSchedule::DB.connect
22
+ XTeamSchedule::DB.build_schema
metadata ADDED
@@ -0,0 +1,163 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: xteam_schedule
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Christopher Patuzzo
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-02-15 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: plist
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ hash: 3
29
+ segments:
30
+ - 0
31
+ version: "0"
32
+ type: :runtime
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: activerecord
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ hash: 3
43
+ segments:
44
+ - 0
45
+ version: "0"
46
+ type: :runtime
47
+ version_requirements: *id002
48
+ - !ruby/object:Gem::Dependency
49
+ name: sqlite3
50
+ prerelease: false
51
+ requirement: &id003 !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ hash: 3
57
+ segments:
58
+ - 0
59
+ version: "0"
60
+ type: :runtime
61
+ version_requirements: *id003
62
+ - !ruby/object:Gem::Dependency
63
+ name: rspec
64
+ prerelease: false
65
+ requirement: &id004 !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ hash: 3
71
+ segments:
72
+ - 0
73
+ version: "0"
74
+ type: :development
75
+ version_requirements: *id004
76
+ - !ruby/object:Gem::Dependency
77
+ name: factory_girl
78
+ prerelease: false
79
+ requirement: &id005 !ruby/object:Gem::Requirement
80
+ none: false
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ hash: 3
85
+ segments:
86
+ - 0
87
+ version: "0"
88
+ type: :development
89
+ version_requirements: *id005
90
+ - !ruby/object:Gem::Dependency
91
+ name: database_cleaner
92
+ prerelease: false
93
+ requirement: &id006 !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ hash: 3
99
+ segments:
100
+ - 0
101
+ version: "0"
102
+ type: :development
103
+ version_requirements: *id006
104
+ description: Full control over schedules for use with adnX's xTeam software
105
+ email: chris.patuzzo@gmail.com
106
+ executables: []
107
+
108
+ extensions: []
109
+
110
+ extra_rdoc_files: []
111
+
112
+ files:
113
+ - README.md
114
+ - lib/xteam_schedule/core.rb
115
+ - lib/xteam_schedule/facilitation/composer.rb
116
+ - lib/xteam_schedule/facilitation/db.rb
117
+ - lib/xteam_schedule/facilitation/io.rb
118
+ - lib/xteam_schedule/facilitation/parser.rb
119
+ - lib/xteam_schedule/models/assignment.rb
120
+ - lib/xteam_schedule/models/assignment_group.rb
121
+ - lib/xteam_schedule/models/interface.rb
122
+ - lib/xteam_schedule/models/resource.rb
123
+ - lib/xteam_schedule/models/resource_group.rb
124
+ - lib/xteam_schedule/models/schedule.rb
125
+ - lib/xteam_schedule/models/weekly_working_schedule.rb
126
+ - lib/xteam_schedule/models/working_day.rb
127
+ - lib/xteam_schedule/models/working_time.rb
128
+ - lib/xteam_schedule.rb
129
+ homepage: https://github.com/cpatuzzo/xteam_schedule
130
+ licenses: []
131
+
132
+ post_install_message:
133
+ rdoc_options: []
134
+
135
+ require_paths:
136
+ - lib
137
+ required_ruby_version: !ruby/object:Gem::Requirement
138
+ none: false
139
+ requirements:
140
+ - - ">="
141
+ - !ruby/object:Gem::Version
142
+ hash: 3
143
+ segments:
144
+ - 0
145
+ version: "0"
146
+ required_rubygems_version: !ruby/object:Gem::Requirement
147
+ none: false
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ hash: 3
152
+ segments:
153
+ - 0
154
+ version: "0"
155
+ requirements: []
156
+
157
+ rubyforge_project:
158
+ rubygems_version: 1.8.15
159
+ signing_key:
160
+ specification_version: 3
161
+ summary: XTeam Schedule
162
+ test_files: []
163
+