portera 0.1.3
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/CHANGELOG.markdown +19 -0
- data/Gemfile +3 -0
- data/README.rdoc +104 -0
- data/Rakefile +0 -0
- data/lib/portera/event.rb +145 -0
- data/lib/portera/participant.rb +157 -0
- data/lib/portera/presenters/simple.rb +88 -0
- data/lib/portera/presenters.rb +15 -0
- data/lib/portera/version.rb +8 -0
- data/lib/portera/views/simple.erb +9 -0
- data/lib/portera.rb +13 -0
- data/test/acceptance.rb +65 -0
- data/test/test_helper.rb +5 -0
- metadata +113 -0
data/CHANGELOG.markdown
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
## 0.1.3 / 2012-02-21
|
4
|
+
|
5
|
+
- Draft simple presenter
|
6
|
+
|
7
|
+
## 0.1.2 / 2012-02-21
|
8
|
+
|
9
|
+
- Cache availability results in `Participant#available_for?`
|
10
|
+
|
11
|
+
## 0.1.1 / 2012-02-20
|
12
|
+
|
13
|
+
- Re-release under Portera top-level namespace
|
14
|
+
|
15
|
+
## 0.1.0 / 2012-02-20
|
16
|
+
|
17
|
+
- Initial release
|
18
|
+
- Note coalescing timeslot ranges does not quite work
|
19
|
+
- Note `Schedule.view` has not been implemented yet
|
data/Gemfile
ADDED
data/README.rdoc
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
= portera
|
2
|
+
|
3
|
+
== DESCRIPTION:
|
4
|
+
|
5
|
+
An application for determining the best time to conduct meetings.
|
6
|
+
|
7
|
+
Adapted from Gordon Thiesfeld's {rmu-scheduler}[https://github.com/vertiginous/rmu-scheduler]
|
8
|
+
|
9
|
+
|
10
|
+
== SYNOPSIS:
|
11
|
+
|
12
|
+
==== Basic usage
|
13
|
+
|
14
|
+
schedule = Portera::Event.new do
|
15
|
+
|
16
|
+
week_of Date.civil(2010,11,21)
|
17
|
+
duration 90
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
schedule.participants <<
|
22
|
+
Portera::Participant.new('Malcolm Reynolds').available do
|
23
|
+
on(:monday, :from => "15:00", :to => "18:00" )
|
24
|
+
on(:wednesday, :from => "20:00", :to => "23:00" )
|
25
|
+
on(:friday, :from => "16:00", :to => "19:00" )
|
26
|
+
end
|
27
|
+
|
28
|
+
schedule.participants <<
|
29
|
+
Portera::Participant.new('Hoban Washburne').available do
|
30
|
+
on(:monday, :from => "15:00", :to => "18:00" )
|
31
|
+
on(:wednesday, :from => "20:00", :to => "23:00" )
|
32
|
+
on(:thursday, :from => "15:00", :to => "18:30" )
|
33
|
+
end
|
34
|
+
|
35
|
+
schedule.participants <<
|
36
|
+
Portera::Participant.new('Jayne Cobb').available do
|
37
|
+
weekdays(:from => "15:00", :to => "18:00" )
|
38
|
+
end
|
39
|
+
|
40
|
+
schedule.participants <<
|
41
|
+
Portera::Participant.new('Zoe Washburne').available do
|
42
|
+
on([:monday, :tuesday, :thursday], :from => "15:00", :to => "18:00" )
|
43
|
+
end
|
44
|
+
|
45
|
+
schedule.participants <<
|
46
|
+
Portera::Participant.new('Inara Serra').available do
|
47
|
+
on([:monday, :tuesday, :thursday])
|
48
|
+
end
|
49
|
+
|
50
|
+
puts Portera.view(:simple, schedule)
|
51
|
+
|
52
|
+
==== Output
|
53
|
+
|
54
|
+
Monday 15:00-18:00 +00:00
|
55
|
+
Hoban Washburne
|
56
|
+
Inara Serra
|
57
|
+
Jayne Cobb
|
58
|
+
Malcolm Reynolds
|
59
|
+
Zoe Washburne
|
60
|
+
Thursday 15:00-18:00 +00:00
|
61
|
+
Hoban Washburne
|
62
|
+
Inara Serra
|
63
|
+
Jayne Cobb
|
64
|
+
Zoe Washburne
|
65
|
+
Tuesday 15:00-18:00 +00:00
|
66
|
+
Inara Serra
|
67
|
+
Jayne Cobb
|
68
|
+
Zoe Washburne
|
69
|
+
|
70
|
+
|
71
|
+
== REQUIREMENTS:
|
72
|
+
|
73
|
+
* {tempr}[https://github.com/ericgj/tempr]
|
74
|
+
* {tilt}[https://github.com/rtomayko/tilt]
|
75
|
+
* {erubis}[http://www.kuwata-lab.com/erubis/]
|
76
|
+
|
77
|
+
== INSTALL:
|
78
|
+
|
79
|
+
* git clone
|
80
|
+
|
81
|
+
== LICENSE:
|
82
|
+
|
83
|
+
(The MIT License)
|
84
|
+
|
85
|
+
Copyright (c) 2012 Eric Gjertsen
|
86
|
+
|
87
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
88
|
+
a copy of this software and associated documentation files (the
|
89
|
+
'Software'), to deal in the Software without restriction, including
|
90
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
91
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
92
|
+
permit persons to whom the Software is furnished to do so, subject to
|
93
|
+
the following conditions:
|
94
|
+
|
95
|
+
The above copyright notice and this permission notice shall be
|
96
|
+
included in all copies or substantial portions of the Software.
|
97
|
+
|
98
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
99
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
100
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
101
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
102
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
103
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
104
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
File without changes
|
@@ -0,0 +1,145 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Portera
|
4
|
+
|
5
|
+
# Event model - see README for usage
|
6
|
+
class Event
|
7
|
+
attr_accessor :name, :duration, :range
|
8
|
+
def participants; @participants ||= []; end
|
9
|
+
|
10
|
+
# Define events using a block passed to the constructor, e.g.
|
11
|
+
#
|
12
|
+
# Event.new do
|
13
|
+
# duration 60
|
14
|
+
# week_of Date.civil(2012,2,13)
|
15
|
+
# end
|
16
|
+
def initialize(name=nil,&proc)
|
17
|
+
self.name = name
|
18
|
+
Builder.new(self).instance_eval(&proc)
|
19
|
+
end
|
20
|
+
|
21
|
+
# iterate over each timeslot in range (week),
|
22
|
+
# gathering participant availability for each slot
|
23
|
+
# parameters:
|
24
|
+
# [+interval+]: duration of timeslots in minutes (default 15)
|
25
|
+
# [+all+]: return all timeslots even if no participants available (default false)
|
26
|
+
def availability(params={})
|
27
|
+
interval = params.fetch(:interval,15)
|
28
|
+
keep_all = params.fetch(:all,false)
|
29
|
+
iterate(interval).each_with_object(TimeslotEnum.new) do |slot, accum|
|
30
|
+
avails = []
|
31
|
+
self.participants.each do |person|
|
32
|
+
if person.available_for?(self, slot)
|
33
|
+
avails << person
|
34
|
+
end
|
35
|
+
end
|
36
|
+
if !avails.empty? || keep_all
|
37
|
+
accum << Timeslot.new(slot, ::Set.new(avails.sort_by(&:name)))
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def coalesced(params={})
|
43
|
+
availability(params).coalesced
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def iterate(i)
|
49
|
+
self.range.dup.extend(Tempr::DateTimeRange).each_minute(i,0,self.duration)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Internal builder class for events
|
53
|
+
class Builder
|
54
|
+
|
55
|
+
def initialize(event)
|
56
|
+
@event = event
|
57
|
+
end
|
58
|
+
|
59
|
+
# duration in minutes of the proposed event
|
60
|
+
def duration(minutes)
|
61
|
+
@event.duration = minutes
|
62
|
+
end
|
63
|
+
|
64
|
+
# range of possible dates for the proposed event
|
65
|
+
# for a weekly event, use `week_of` instead of explicit date range
|
66
|
+
def range(dates)
|
67
|
+
@event.range = dates
|
68
|
+
end
|
69
|
+
|
70
|
+
# first date of week for the proposed event
|
71
|
+
def week_of(date)
|
72
|
+
range date...(date+7)
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
class Timeslot < Struct.new(:range, :participants)
|
80
|
+
|
81
|
+
# temporary, TODO use views/presenters instead
|
82
|
+
def to_s
|
83
|
+
["#{self.range}",
|
84
|
+
self.participants.empty? ? nil : " #{self.participants.to_a.join("\n ")}"
|
85
|
+
].compact.join("\n")
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
class TimeslotEnum
|
91
|
+
include Enumerable
|
92
|
+
|
93
|
+
def <<(timeslot)
|
94
|
+
timeslots << timeslot
|
95
|
+
self
|
96
|
+
end
|
97
|
+
|
98
|
+
def each(&b)
|
99
|
+
timeslots.each(&b)
|
100
|
+
end
|
101
|
+
|
102
|
+
# join intersecting timeslots with identical participants
|
103
|
+
# note that this returns a new TimeslotEnum, so that you can call
|
104
|
+
# [+enum.best+] all timeslots sorted by highest participation
|
105
|
+
# [+enum.coalesced.best+] joined timeslots sorted by highest participation
|
106
|
+
#
|
107
|
+
# Note assumes timeslots are appended in ascending order by range.begin
|
108
|
+
# TODO could use some refactoring
|
109
|
+
def coalesced
|
110
|
+
accum = self.class.new
|
111
|
+
inject(nil) do |last_slot, this_slot|
|
112
|
+
if last_slot
|
113
|
+
rng = this_slot.range
|
114
|
+
last_rng = last_slot.range
|
115
|
+
if (rng.intersects?(last_rng) || rng.succeeds?(last_rng)) &&
|
116
|
+
(this_slot.participants == last_slot.participants)
|
117
|
+
Timeslot.new(last_rng.begin...rng.end,
|
118
|
+
this_slot.participants)
|
119
|
+
else
|
120
|
+
accum << last_slot
|
121
|
+
this_slot
|
122
|
+
end
|
123
|
+
else
|
124
|
+
this_slot
|
125
|
+
end
|
126
|
+
end
|
127
|
+
accum
|
128
|
+
end
|
129
|
+
|
130
|
+
# sort timeslot availability by
|
131
|
+
# 1. most participants available
|
132
|
+
# 2. timeslot date/time
|
133
|
+
def best
|
134
|
+
sort do |a, b|
|
135
|
+
comp = b.participants.count <=> a.participants.count
|
136
|
+
comp.zero? ? (a.range.begin <=> b.range.begin) : comp
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
private
|
141
|
+
def timeslots; @timeslots ||= []; end
|
142
|
+
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
|
2
|
+
module Portera
|
3
|
+
|
4
|
+
# Participant model - for use with event(s). See README for usage.
|
5
|
+
class Participant < Struct.new(:name, :email)
|
6
|
+
|
7
|
+
# List of availability rules
|
8
|
+
def availables; @availables ||= []; end
|
9
|
+
|
10
|
+
# Define availability rule passing a block, e.g.
|
11
|
+
#
|
12
|
+
# participant.available('+09:00') do
|
13
|
+
# weekdays :from => '11:00am', :to => '3:00pm'
|
14
|
+
# on [0,6] :from => '5:00pm', :to => '6:30pm'
|
15
|
+
# end
|
16
|
+
def available(utc_offset=nil, &sched_proc)
|
17
|
+
Builder.new(self.availables,utc_offset).instance_eval(&sched_proc)
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
# For given event range,
|
22
|
+
# evaluate if the given timeslot fits within any of the participant's available times
|
23
|
+
#
|
24
|
+
# Note that range-availability arrays are cached here per event
|
25
|
+
# Possibly caching should be moved down into Tempr, e.g.
|
26
|
+
# avail.for_range(event.range).all #=> caches within Tempr::SubRangeIterator
|
27
|
+
def available_for?(event, slot)
|
28
|
+
availables.any? do |avail|
|
29
|
+
cache[event.range] ||= avail.for_range(event.range).to_a
|
30
|
+
cache[event.range].any? {|free| free.subsume?(slot)}
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Display participant as valid email `name <address>`
|
35
|
+
def to_s
|
36
|
+
[self.name ? "#{self.name}" : nil,
|
37
|
+
self.email ? "<#{self.email}>" : nil
|
38
|
+
].compact.join(" ")
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def cache; @cache ||= {}; end
|
44
|
+
|
45
|
+
# Internal builder for participant availability
|
46
|
+
class Builder
|
47
|
+
|
48
|
+
Days = { :sunday => 0,
|
49
|
+
:monday => 1,
|
50
|
+
:tuesday => 2,
|
51
|
+
:wednesday => 3,
|
52
|
+
:thursday => 4,
|
53
|
+
:friday => 5,
|
54
|
+
:saturday => 6,
|
55
|
+
:sun => 0,
|
56
|
+
:mon => 1,
|
57
|
+
:tue => 2,
|
58
|
+
:wed => 3,
|
59
|
+
:thu => 4,
|
60
|
+
:fri => 5,
|
61
|
+
:sat => 6,
|
62
|
+
:Sunday => 0,
|
63
|
+
:Monday => 1,
|
64
|
+
:Tuesday => 2,
|
65
|
+
:Wednesday => 3,
|
66
|
+
:Thursday => 4,
|
67
|
+
:Friday => 5,
|
68
|
+
:Saturday => 6,
|
69
|
+
:Sun => 0,
|
70
|
+
:Mon => 1,
|
71
|
+
:Tue => 2,
|
72
|
+
:Wed => 3,
|
73
|
+
:Thu => 4,
|
74
|
+
:Fri => 5,
|
75
|
+
:Sat => 6
|
76
|
+
}
|
77
|
+
|
78
|
+
def initialize(collect,utc_offset=nil)
|
79
|
+
@collect = collect
|
80
|
+
@utc_offset = utc_offset || 0
|
81
|
+
end
|
82
|
+
|
83
|
+
# Example:
|
84
|
+
# on [2,3,5], :from => '9:00am', :to => '9:30am', :utc_offset => '-05:00'
|
85
|
+
#
|
86
|
+
# Note that if not specified, timezone is
|
87
|
+
# 1. the offset passed into the constructor, or
|
88
|
+
# 2. UTC, otherwise -- _not the process-local timezone_!
|
89
|
+
def on(days, time={})
|
90
|
+
@collect << Availability.new( Array(days).map {|d| to_weekday(d)},
|
91
|
+
time[:from],
|
92
|
+
time[:to],
|
93
|
+
time[:utc_offset] || @utc_offset
|
94
|
+
)
|
95
|
+
end
|
96
|
+
|
97
|
+
# sugar for `on( [1,2,3,4,5], time)`
|
98
|
+
def weekdays(time={})
|
99
|
+
on( [1,2,3,4,5], time)
|
100
|
+
end
|
101
|
+
|
102
|
+
# sugar for `on( [], time)`
|
103
|
+
def any_day(time={})
|
104
|
+
on( [], time)
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def to_weekday(day)
|
110
|
+
Days[day] || day
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
|
115
|
+
# This wrapper stores the parameters for participant availability rules
|
116
|
+
#
|
117
|
+
# [+days+] array of wday numbers, or empty array for all days
|
118
|
+
# [+from+] start time (parseable time string)
|
119
|
+
# [+to+] end time (parseable time string)
|
120
|
+
# [+utc_offset+] offset string or seconds from utc, if not specified assumes process-local offset
|
121
|
+
#
|
122
|
+
# An iterator is returned when you call `for_range`
|
123
|
+
class Availability < Struct.new(:days, :from, :to, :utc_offset)
|
124
|
+
|
125
|
+
def initialize(*args)
|
126
|
+
super
|
127
|
+
self.days ||= []
|
128
|
+
end
|
129
|
+
|
130
|
+
def for_range(range)
|
131
|
+
apply_time_range(for_day_range(range))
|
132
|
+
end
|
133
|
+
|
134
|
+
def for_day_range(range)
|
135
|
+
r = range.extend(Tempr::DateTimeRange)
|
136
|
+
if self.days.empty?
|
137
|
+
r.each_days_of_week
|
138
|
+
else
|
139
|
+
r.each_days_of_week(*self.days)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
private
|
144
|
+
|
145
|
+
def apply_time_range(expr)
|
146
|
+
if self.from && self.to
|
147
|
+
expr.between_times(self.from, self.to, self.utc_offset)
|
148
|
+
else
|
149
|
+
expr
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'erubis'
|
2
|
+
require 'tilt'
|
3
|
+
|
4
|
+
module Portera
|
5
|
+
|
6
|
+
#TODO: delegation to event; extract base class
|
7
|
+
module Presenters
|
8
|
+
|
9
|
+
class Simple
|
10
|
+
include Enumerable
|
11
|
+
|
12
|
+
attr_accessor :template, :view_path
|
13
|
+
def template; @template ||= 'simple'; end
|
14
|
+
def view_path
|
15
|
+
@view_path ||= File.expand_path('../views', File.dirname(__FILE__))
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(e)
|
19
|
+
self.event = e
|
20
|
+
end
|
21
|
+
|
22
|
+
def name
|
23
|
+
event.name
|
24
|
+
end
|
25
|
+
|
26
|
+
def description
|
27
|
+
present_event(event)
|
28
|
+
end
|
29
|
+
|
30
|
+
def best
|
31
|
+
event.coalesced.best
|
32
|
+
end
|
33
|
+
|
34
|
+
def each
|
35
|
+
best.each do |timeslot|
|
36
|
+
yield present_time_range(timeslot.range),
|
37
|
+
timeslot.participants.map {|p| present_participant(p)}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# shortcut for typical case
|
42
|
+
def minimum_participants(min=2)
|
43
|
+
select { |timeslot, participants|
|
44
|
+
participants.count >= min
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
def render
|
49
|
+
Tilt.new(Dir.glob(File.join(view_path, template + '.*')).first)
|
50
|
+
.render(self)
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
attr_accessor :event
|
55
|
+
|
56
|
+
def present_event(e)
|
57
|
+
"best times #{present_date_range(e.range)} (#{e.duration} mins)"
|
58
|
+
end
|
59
|
+
|
60
|
+
def present_date_range(r)
|
61
|
+
if r.end - r.begin == 7
|
62
|
+
"week of #{present_date(r.begin)}"
|
63
|
+
else
|
64
|
+
"between #{present_date(r.begin)} and #{present_date(r.end-1)}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def present_time_range(r)
|
69
|
+
"#{present_time(r.begin)} - #{present_time(r.end)}"
|
70
|
+
end
|
71
|
+
|
72
|
+
def present_date(d)
|
73
|
+
d.strftime("%a %-d %b")
|
74
|
+
end
|
75
|
+
|
76
|
+
def present_time(t)
|
77
|
+
t.getutc.strftime("%l:%M%P %Z")
|
78
|
+
end
|
79
|
+
|
80
|
+
def present_participant(p)
|
81
|
+
p.to_s
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
data/lib/portera.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'tempr'
|
2
|
+
|
3
|
+
%w[ portera/version
|
4
|
+
portera/event
|
5
|
+
portera/participant
|
6
|
+
portera/presenters
|
7
|
+
].each do |f|
|
8
|
+
require File.expand_path(f, File.dirname(__FILE__))
|
9
|
+
end
|
10
|
+
|
11
|
+
Dir[File.expand_path('portera/presenters/*', File.dirname(__FILE__))].each do |f|
|
12
|
+
require f
|
13
|
+
end
|
data/test/acceptance.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
require File.expand_path('test_helper',File.dirname(__FILE__))
|
2
|
+
|
3
|
+
describe 'simple schedule' do
|
4
|
+
|
5
|
+
let(:schedule) do
|
6
|
+
schedule = Portera::Event.new("Meeting on Frobartz project") do
|
7
|
+
week_of Date.civil(2010,11,21)
|
8
|
+
duration 90
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
let(:malcolm) do
|
13
|
+
Portera::Participant.new('Malcolm Reynolds').available do
|
14
|
+
on(:Mon, :from => "15:00", :to => "18:00" )
|
15
|
+
on(:Wed, :from => "20:00", :to => "23:00" )
|
16
|
+
on(:Fri, :from => "16:00", :to => "19:00" )
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
let(:hoban) do
|
21
|
+
Portera::Participant.new('Hoban Washburne').available do
|
22
|
+
on(:monday, :from => "15:00", :to => "18:00" )
|
23
|
+
on(:wednesday, :from => "20:00", :to => "23:00" )
|
24
|
+
on(:thursday, :from => "15:00", :to => "18:30" )
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
let(:jayne) do
|
29
|
+
Portera::Participant.new('Jayne Cobb').available do
|
30
|
+
weekdays(:from => "15:00", :to => "18:00" )
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
let(:zoe) do
|
35
|
+
Portera::Participant.new('Zoe Washburne').available do
|
36
|
+
on([1, 2, 4], :from => "15:00", :to => "18:00" )
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
let(:inara) do
|
41
|
+
Portera::Participant.new('Inara Serra').available do
|
42
|
+
on([:Monday, :tue, :Thu])
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
before do
|
47
|
+
schedule.participants << malcolm << hoban << jayne << zoe << inara
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'should return best timeslots' do
|
51
|
+
puts
|
52
|
+
puts schedule.availability.best
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'should return best with coalesced timeslots' do
|
56
|
+
puts
|
57
|
+
puts schedule.coalesced.best
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'should display simple report' do
|
61
|
+
puts
|
62
|
+
puts Portera.view(:simple, schedule)
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: portera
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.3
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Eric Gjertsen
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-02-24 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: tempr
|
16
|
+
requirement: &10133680 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 0.1.4
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *10133680
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: erubis
|
27
|
+
requirement: &10133300 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *10133300
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: tilt
|
38
|
+
requirement: &10132840 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *10132840
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: minitest
|
49
|
+
requirement: &10132420 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *10132420
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: rake
|
60
|
+
requirement: &10132000 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
type: :development
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *10132000
|
69
|
+
description: ''
|
70
|
+
email:
|
71
|
+
- ericgj72@gmail.com
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- CHANGELOG.markdown
|
77
|
+
- Gemfile
|
78
|
+
- README.rdoc
|
79
|
+
- Rakefile
|
80
|
+
- lib/portera.rb
|
81
|
+
- lib/portera/event.rb
|
82
|
+
- lib/portera/participant.rb
|
83
|
+
- lib/portera/presenters.rb
|
84
|
+
- lib/portera/presenters/simple.rb
|
85
|
+
- lib/portera/version.rb
|
86
|
+
- lib/portera/views/simple.erb
|
87
|
+
- test/acceptance.rb
|
88
|
+
- test/test_helper.rb
|
89
|
+
homepage: http://github.com/ericgj/portera
|
90
|
+
licenses: []
|
91
|
+
post_install_message:
|
92
|
+
rdoc_options: []
|
93
|
+
require_paths:
|
94
|
+
- lib
|
95
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
96
|
+
none: false
|
97
|
+
requirements:
|
98
|
+
- - ! '>='
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: 1.9.2
|
101
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
102
|
+
none: false
|
103
|
+
requirements:
|
104
|
+
- - ! '>='
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: 1.3.6
|
107
|
+
requirements: []
|
108
|
+
rubyforge_project: portera
|
109
|
+
rubygems_version: 1.8.10
|
110
|
+
signing_key:
|
111
|
+
specification_version: 3
|
112
|
+
summary: Group event coordination
|
113
|
+
test_files: []
|