portera 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+
3
+ gemspec
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
@@ -0,0 +1,15 @@
1
+ module Portera
2
+
3
+ def self.view(type, *args)
4
+ Presenters.get(type).new(*args).render
5
+ end
6
+
7
+ module Presenters
8
+
9
+ def self.get(type)
10
+ self.const_get(type.to_s.capitalize)
11
+ end
12
+
13
+ end
14
+
15
+ end
@@ -0,0 +1,8 @@
1
+ module Portera
2
+ module Version
3
+ MAJOR = 0
4
+ MINOR = 1
5
+ TINY = 3
6
+ STRING = "#{MAJOR}.#{MINOR}.#{TINY}"
7
+ end
8
+ end
@@ -0,0 +1,9 @@
1
+ <%= name %>
2
+ <%= description %>
3
+ ------------------------------
4
+ <% minimum_participants(2).each do |slot, participants| -%>
5
+ <%= slot %>
6
+ <% participants.each do |p| -%>
7
+ <%= p %>
8
+ <% end -%>
9
+ <% 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
@@ -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
@@ -0,0 +1,5 @@
1
+ require File.expand_path('../lib/portera', File.dirname(__FILE__))
2
+
3
+ gem 'minitest'
4
+ require 'minitest/spec'
5
+ MiniTest::Unit.autorun
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: []