xteam_schedule 0.0.1

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