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 +324 -0
- data/lib/xteam_schedule/core.rb +31 -0
- data/lib/xteam_schedule/facilitation/composer.rb +166 -0
- data/lib/xteam_schedule/facilitation/db.rb +83 -0
- data/lib/xteam_schedule/facilitation/io.rb +11 -0
- data/lib/xteam_schedule/facilitation/parser.rb +168 -0
- data/lib/xteam_schedule/models/assignment.rb +51 -0
- data/lib/xteam_schedule/models/assignment_group.rb +7 -0
- data/lib/xteam_schedule/models/interface.rb +11 -0
- data/lib/xteam_schedule/models/resource.rb +22 -0
- data/lib/xteam_schedule/models/resource_group.rb +7 -0
- data/lib/xteam_schedule/models/schedule.rb +25 -0
- data/lib/xteam_schedule/models/weekly_working_schedule.rb +24 -0
- data/lib/xteam_schedule/models/working_day.rb +28 -0
- data/lib/xteam_schedule/models/working_time.rb +9 -0
- data/lib/xteam_schedule.rb +22 -0
- metadata +163 -0
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,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,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,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
|
+
|