reservation 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.loco ADDED
@@ -0,0 +1,3 @@
1
+ # -*- mode: ruby -*-
2
+
3
+ Loco::Counter::EXCLUDE << /^spec\/dummy/
@@ -1,4 +1,4 @@
1
- Copyright 2013 YOURNAME
1
+ Copyright 2013 Conan Dalton
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
@@ -0,0 +1,56 @@
1
+ # Reservation
2
+
3
+ A gem for managing reservations. Provides the notion of "Event", which is basically an interval in time, and a
4
+ "Reservation", which is a many-to-many association model between Event and your objects.
5
+
6
+ There is no privileged event "owner", all associations are considered equivalent.
7
+
8
+ This gem uses ActiveRecord to store events and reservations
9
+
10
+ ## Installation
11
+
12
+ Add this line to your application's Gemfile:
13
+
14
+ gem 'reservation'
15
+
16
+ And then execute:
17
+
18
+ $ bundle
19
+
20
+ Or install it yourself as:
21
+
22
+ $ gem install reservation
23
+
24
+ ## Usage
25
+
26
+ Objects associated with an event are called "subjects"; each event has many subjects, via an association
27
+ model called Reservation::Reservation. The simplest way to create a bunch of events is to use Event#create_weekly.
28
+ This will create a set of events, with the given subjects, within the given constraints, repeating weekly.
29
+
30
+ subjects = [ { "role" => "owner", "subject" => matt, "status" => "confirmed" },
31
+ { "role" => "helpr", "subject" => bill, "status" => "tentative" },
32
+ { "role" => "place", "subject" => here, "status" => "tentative" }
33
+ ]
34
+
35
+ pattern = [ { "day" => "wed", "start" => "0930", "finish" => "1030"},
36
+ { "day" => "wed", "start" => "18", "finish" => "20" },
37
+ { "day" => "tue", "start" => "7", "finish" => "830" } ]
38
+
39
+ Reservation::Event.create_weekly "the_title", "2013-09-03", "2013-10-13", subjects, pattern
40
+
41
+ You can use Reservation::Event#build_weekly instead in order to instantiate the object graph without
42
+ persisting it.
43
+
44
+
45
+ ## Contributing
46
+
47
+ 1. Fork it
48
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
49
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
50
+ 4. Push to the branch (`git push origin my-new-feature`)
51
+ 5. Create new Pull Request
52
+
53
+
54
+ ## license
55
+
56
+ MIT License
@@ -1,6 +1,7 @@
1
- module Reservation
2
- end
3
1
 
2
+ require "reservation/schedule"
3
+ require "reservation/scopes"
4
+ require "reservation/event_filter"
4
5
  require "reservation/event"
5
6
  require "reservation/reservation"
6
7
 
@@ -1,4 +1,9 @@
1
+ require "reservation/time_offset"
2
+
1
3
  class Reservation::Event < ActiveRecord::Base
4
+ extend Reservation::TimeOffset
5
+ extend Reservation::EventFilter
6
+
2
7
  has_many :reservations, :class_name => "Reservation::Reservation"
3
8
  attr_accessible :finish, :start, :title
4
9
 
@@ -6,10 +11,61 @@ class Reservation::Event < ActiveRecord::Base
6
11
  scope :upto, lambda { |time| where("reservation_events.start < ?", time) }
7
12
  scope :reserved_for, lambda { |who|
8
13
  klass = who.class.base_class.name
9
- joins("join reservation_reservations rrx on reservation_events.id = rrx.event_id").where("rrx.subject_type = ? and rrx.subject_id = ?", klass, who.id)
14
+ join_name = "rrx_#{Time.now.to_i}#{rand(1000)}"
15
+ joins("join reservation_reservations #{join_name} on reservation_events.id = #{join_name}.event_id").where("#{join_name}.subject_type = ? and #{join_name}.subject_id = ?", klass, who.id)
10
16
  }
11
17
 
12
18
  def overlap? range_start, range_end
13
19
  (range_end > start) && (range_start < finish)
14
20
  end
21
+
22
+ def matches? weekday_spec
23
+ return true if weekday_spec.nil?
24
+ return false if self.start.day != self.finish.day
25
+ spec = weekday_spec[DAY_MAP[self.start.wday]]
26
+ my_start = { :hour => self.start.hour, :min => self.start.min }
27
+ my_finish = { :hour => self.finish.hour, :min => self.finish.min }
28
+ spec.include? [my_start, my_finish]
29
+ end
30
+
31
+ def self.build_weekly title, from, upto, subjects, pattern
32
+ schedule = Reservation::Schedule::Weekly.new pattern
33
+ from = Date.parse from
34
+ max = Date.parse upto
35
+ events = schedule.generate from, max
36
+ events.each { |e|
37
+ e.title = title
38
+ subjects.each { |subject_data|
39
+ role = subject_data["role"]
40
+ status = subject_data["status"]
41
+ subject = subject_data["subject"]
42
+ e.reservations.build :role => role, :reservation_status => status, :subject => subject
43
+ }
44
+ yield e if block_given?
45
+ }
46
+ events
47
+ end
48
+
49
+ def self.create_weekly title, from, upto, subjects, pattern
50
+ build_weekly(title, from, upto, subjects, pattern) { |e| e.save! }
51
+ end
52
+
53
+ def self.remove_subject subject, filter_options
54
+ filter_events(filter_options).each { |event|
55
+ event.reservations.each { |r|
56
+ if r.subject == subject
57
+ r.destroy
58
+ end
59
+ }
60
+ }
61
+ end
62
+
63
+ def self.add_subject subject_data, filter_options
64
+ filter_events(filter_options).each { |event|
65
+ role = subject_data["role"]
66
+ status = subject_data["status"]
67
+ subject = subject_data["subject"]
68
+ event.reservations.create! :role => role, :reservation_status => status, :subject => subject
69
+ }
70
+ end
15
71
  end
@@ -0,0 +1,47 @@
1
+ module Reservation
2
+ module EventFilter
3
+ def parse_time_for_upto txt
4
+ d = Date.parse(txt) rescue nil
5
+ t = Time.parse(txt) rescue nil
6
+ raise "can't read date/time #{txt.inspect}" if d.nil? && t.nil?
7
+ if d && (d.to_time == t)
8
+ return (d + 1.day).to_time
9
+ end
10
+ t
11
+ end
12
+
13
+ def filter_events options
14
+ from = options["from"]
15
+ upto = options["upto"]
16
+ context = options["context"]
17
+ schedule = options["schedule"]
18
+
19
+ events = ::Reservation.events
20
+
21
+ if from
22
+ from = from.is_a?(String) ? Date.parse(from) : from.to_date
23
+ events = events.since(from)
24
+ end
25
+
26
+ if upto
27
+ upto = upto.is_a?(String) ? parse_time_for_upto(upto) : upto.to_time if upto
28
+ events = events.upto(upto) if upto
29
+ end
30
+
31
+ if context
32
+ context = [context] unless context.is_a? Array
33
+ context = context.uniq
34
+ events = context.inject(events) { |ee, ctx|
35
+ ee.reserved_for(ctx)
36
+ }
37
+ end
38
+
39
+ if schedule
40
+ schedule = ::Reservation::Schedule::Weekly.new schedule
41
+ events = schedule.filter events
42
+ end
43
+
44
+ events
45
+ end
46
+ end
47
+ end
@@ -1,5 +1,7 @@
1
- class Reservation::Reservation < ActiveRecord::Base
2
- belongs_to :event, :class_name => "Reservation::Event"
3
- belongs_to :subject, :polymorphic => true
4
- attr_accessible :reservation_status, :role, :subject_id, :subject_type, :event_id, :event, :subject
1
+ module Reservation
2
+ class Reservation < ActiveRecord::Base
3
+ belongs_to :event, :class_name => "Reservation::Event"
4
+ belongs_to :subject, :polymorphic => true
5
+ attr_accessible :reservation_status, :role, :subject_id, :subject_type, :event_id, :event, :subject
6
+ end
5
7
  end
@@ -0,0 +1,191 @@
1
+ module Reservation
2
+ DAY_MAP = { "sun" => 0, "mon" => 1, "tue" => 2, "wed" => 3, "thu" => 4, "fri" => 5, "sat" => 6 }
3
+ MAP_DAY = %w{ sun mon tue wed thu fri sat }
4
+
5
+ module Schedule
6
+ #
7
+ # a utility class to match the hour and minute elements of a Time instance, without considering any other values
8
+ #
9
+ class HourMinute
10
+ attr_accessor :hour, :minute
11
+ def initialize hour, minute
12
+ @hour, @minute = hour, minute
13
+ end
14
+
15
+ def matches_time? time
16
+ time.hour == self.hour && time.min == self.minute
17
+ end
18
+
19
+ #
20
+ # hhmm is a string containg an hour-and-minute value
21
+ #
22
+ # #parse will remove all nondigit characters, pad the result to 4 digits,
23
+ # and interpret the first two as an hour value, the last two as a minute value
24
+ #
25
+ # padding takes place as follows :
26
+ # * one digit becomes 0d00 (assumes "7" means "0700")
27
+ # * two digits become dd00 (assumes "11" means "1100")
28
+ # * three digits become 0ddd (assumes "830" means "0830")
29
+ #
30
+ def self.parse hhmm
31
+ orig = hhmm
32
+ hhmm = hhmm.gsub /[^\d]/, ""
33
+ hhmm = "0#{hhmm}00" if hhmm.length == 1
34
+ hhmm = "#{hhmm}00" if hhmm.length == 2
35
+ hhmm = "0#{hhmm}" if hhmm.length == 3
36
+ raise "Can't parse #{orig.inspect}" unless hhmm.match(/^\d\d\d\d$/)
37
+
38
+ hh = hhmm[0,2].to_i
39
+ mm = hhmm[2,4].to_i
40
+
41
+ new hh, mm
42
+ end
43
+
44
+ def change date
45
+ date.to_time.change :hour => hour, :min => minute
46
+ end
47
+
48
+ def to_s
49
+ "#{hour.to_s.rjust 2, "0"}#{minute.to_s.rjust 2, "0"}"
50
+ end
51
+ end
52
+
53
+ #
54
+ # a utility class to match the start and end times of an Event instance, without considering the event date
55
+ #
56
+ class Interval
57
+ attr_accessor :start, :finish
58
+
59
+ def initialize start, finish
60
+ @start, @finish = start, finish
61
+ end
62
+
63
+ # true if the start time and finish time of the given event matches start and finish times of this Interval
64
+ def matches? event
65
+ start.matches_time?(event.start) && finish.matches_time?(event.finish)
66
+ end
67
+
68
+ # build a new Event with this Interval's start and finish times, on the given date
69
+ def generate date
70
+ Event.new :start => start.change(date), :finish => finish.change(date)
71
+ end
72
+
73
+ def self.from start, finish
74
+ new HourMinute.parse(start), HourMinute.parse(finish)
75
+ end
76
+
77
+ def to_s
78
+ "#{start}-#{finish}"
79
+ end
80
+ end
81
+
82
+ #
83
+ # a utility class to represent a set of intervals on a single day
84
+ #
85
+ class Daily
86
+ attr_accessor :wday, :intervals
87
+
88
+ def initialize wday
89
+ @wday = wday
90
+ @intervals = []
91
+ end
92
+
93
+ def add interval
94
+ intervals << interval
95
+ end
96
+
97
+ def matches? event
98
+ return false if event.start.wday != self.wday
99
+ intervals.each { |interval|
100
+ return true if interval.matches? event
101
+ }
102
+ false
103
+ end
104
+
105
+ def generate date, list
106
+ return list if date.wday != wday
107
+ intervals.inject(list) { |list, interval|
108
+ list << interval.generate(date)
109
+ list
110
+ }
111
+ end
112
+
113
+ def to_s
114
+ "#{MAP_DAY[wday]} => #{intervals}"
115
+ end
116
+ end
117
+
118
+ #
119
+ # Weekly defines a set of intervals that recur weekly
120
+ #
121
+ # This class maintains an array of intervals for each weekday
122
+ #
123
+ # This class will never match multi-day events, as it assumes each event starts and ends within the same day
124
+ #
125
+ class Weekly
126
+
127
+ # wdays is a 7-element array of Daily instances
128
+ #
129
+ # the zeroth element corresponds to Sunday, because Time#wday behaves that way.
130
+ #
131
+ attr_accessor :wdays
132
+
133
+ # pattern is an array of the form
134
+ #
135
+ # [ { "day" => "wed", "start" => "0930", "finish" => "10:30"},
136
+ # { "day" => "wed", "start" => "18", "finish" => "20" },
137
+ # { "day" => "tue", "start" => "7", "finish" => "8h30" } ]
138
+ #
139
+ # see HourMinute#parse for how "start" and "finish" values are read
140
+ #
141
+ def initialize pattern
142
+ self.wdays = (0..6).map { |i| Daily.new(i) }
143
+
144
+ pattern.each { |interval_spec|
145
+ wday = DAY_MAP[interval_spec["day"]]
146
+ raise "unrecognised day #{spec["day"].inspect} in #{pattern.inspect}" if wday.nil?
147
+
148
+ start = HourMinute.parse interval_spec["start"]
149
+ finish = HourMinute.parse interval_spec["finish"]
150
+ wdays[wday].add Interval.new(start, finish)
151
+ }
152
+ end
153
+
154
+ # true if there exists a Daily corresponding to this event's weekday, with
155
+ # an Interval corresponding to this event's start and finish times;
156
+ # false otherwise
157
+ #
158
+ def matches? event
159
+ return false if event.start.day != event.finish.day
160
+ wdays[event.start.wday].matches? event
161
+ end
162
+
163
+ #
164
+ # generate a set of Events according to this Weekly schedule, starting
165
+ # on #from at the earliest, ending on #upto at the latest.
166
+ #
167
+ # Note: #upto is *inclusive*, this will generate events on #upto,
168
+ # if the schedule allows
169
+ #
170
+ def generate from, upto
171
+ events = []
172
+ from.upto(upto).map { |date|
173
+ wdays.each { |day| day.generate date, events }
174
+ }
175
+ events
176
+ end
177
+
178
+ #
179
+ # return a new list including only those events in the given list
180
+ # that match this weekly schedule
181
+ #
182
+ def filter events
183
+ events.select { |e| matches? e }
184
+ end
185
+
186
+ def to_s
187
+ wdays.map(&:to_s).join "\n"
188
+ end
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,11 @@
1
+ module Reservation
2
+ # replace this if you need to globally override the starting scope for events
3
+ def self.events
4
+ ::Reservation::Event.scoped
5
+ end
6
+
7
+ # replace this if you need to globally override the starting scope for reservations
8
+ def self.reservations
9
+ ::Reservation::Reservation.scoped
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ module Reservation
2
+ module TimeOffset
3
+ def parse_time_offset hhmm
4
+ orig = hhmm
5
+ hhmm = hhmm.gsub /[^\d]/, ""
6
+ hhmm = "0#{hhmm}00" if hhmm.length == 1
7
+ hhmm = "#{hhmm}00" if hhmm.length == 2
8
+ hhmm = "0#{hhmm}" if hhmm.length == 3
9
+ raise "Can't parse #{orig.inspect}" unless hhmm.match(/^\d\d\d\d$/)
10
+
11
+ hh = hhmm[0,2].to_i
12
+ mm = hhmm[2,4].to_i
13
+
14
+ { :hour => hh, :min => mm }
15
+ end
16
+ end
17
+ end
@@ -1,3 +1,3 @@
1
1
  module Reservation
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -2,6 +2,7 @@ class CreateContacts < ActiveRecord::Migration
2
2
  def change
3
3
  create_table :contacts do |t|
4
4
  t.string :name
5
+ t.string :type
5
6
 
6
7
  t.timestamps
7
8
  end
@@ -15,6 +15,7 @@ ActiveRecord::Schema.define(:version => 20130711174025) do
15
15
 
16
16
  create_table "contacts", :force => true do |t|
17
17
  t.string "name"
18
+ t.string "type"
18
19
  t.datetime "created_at", :null => false
19
20
  t.datetime "updated_at", :null => false
20
21
  end
@@ -1,6 +1,18 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Reservation::Event do
4
+ before { Time.zone = "Europe/Paris" }
5
+ let(:matt) { Person.create! :name => "matt" }
6
+ let(:here) { Place.create! :name => "here" }
7
+ let(:bill) { Person.create! :name => "bill" }
8
+
9
+ def the_reservations subject=nil
10
+ if subject
11
+ Reservation::Reservation.where(:subject_type => subject.class.base_class.name, :subject_id => subject.id)
12
+ else
13
+ Reservation::Reservation.all
14
+ end.map { |r| "#{r.event.title} #{r.subject.name} #{r.event.start.prettyd} #{r.event.finish.prettyd} #{r.reservation_status}"}.join("\n")
15
+ end
4
16
 
5
17
  it "should be a Reservation::Event" do
6
18
  Reservation::Event.new.should be_an_instance_of Reservation::Event
@@ -188,5 +200,134 @@ describe Reservation::Event do
188
200
  ee[1].reservations.map(&:subject).map(&:name).should == %w{ APPL Cafe }
189
201
  ee[2].reservations.map(&:subject).map(&:name).should == %w{ APPL Ulysses }
190
202
  end
203
+
204
+ it "should find events for Ulysses and AAPL" do
205
+ ee = Reservation::Event.reserved_for(@book).reserved_for(@appl).order(:id)
206
+ ee.should == [@r6]
207
+ end
208
+ end
209
+
210
+ describe :create_weekly do
211
+ it "should build a simple event for a single subject" do
212
+ subjects = [{ "role" => "owner", "subject" => matt, "status" => "confirmed" } ]
213
+ pattern = [ { "day" => "mon", "start" => "0930", "finish" => "1030"} ]
214
+ Reservation::Event.create_weekly "floss teeth", "2013-09-03", "2013-10-13", subjects, pattern
215
+ the_reservations.should == "floss teeth matt Mon,20130909T0930 Mon,20130909T1030 confirmed
216
+ floss teeth matt Mon,20130916T0930 Mon,20130916T1030 confirmed
217
+ floss teeth matt Mon,20130923T0930 Mon,20130923T1030 confirmed
218
+ floss teeth matt Mon,20130930T0930 Mon,20130930T1030 confirmed
219
+ floss teeth matt Mon,20131007T0930 Mon,20131007T1030 confirmed"
220
+ end
221
+
222
+ it "should build multiple events for multiple subjects" do
223
+ subjects = [{ "role" => "owner", "subject" => matt, "status" => "confirmed" }, { "role" => "place", "subject" => here, "status" => "tentative" }]
224
+ pattern = [ { "day" => "wed", "start" => "0930", "finish" => "1030"}, { "day" => "wed", "start" => "18", "finish" => "20"}, { "day" => "tue", "start" => "7", "finish" => "830"} ]
225
+ Reservation::Event.create_weekly "the_title", "2013-09-03", "2013-09-30", subjects, pattern
226
+ the_reservations.should == "the_title matt Tue,20130903T0700 Tue,20130903T0830 confirmed
227
+ the_title here Tue,20130903T0700 Tue,20130903T0830 tentative
228
+ the_title matt Wed,20130904T0930 Wed,20130904T1030 confirmed
229
+ the_title here Wed,20130904T0930 Wed,20130904T1030 tentative
230
+ the_title matt Wed,20130904T1800 Wed,20130904T2000 confirmed
231
+ the_title here Wed,20130904T1800 Wed,20130904T2000 tentative
232
+ the_title matt Tue,20130910T0700 Tue,20130910T0830 confirmed
233
+ the_title here Tue,20130910T0700 Tue,20130910T0830 tentative
234
+ the_title matt Wed,20130911T0930 Wed,20130911T1030 confirmed
235
+ the_title here Wed,20130911T0930 Wed,20130911T1030 tentative
236
+ the_title matt Wed,20130911T1800 Wed,20130911T2000 confirmed
237
+ the_title here Wed,20130911T1800 Wed,20130911T2000 tentative
238
+ the_title matt Tue,20130917T0700 Tue,20130917T0830 confirmed
239
+ the_title here Tue,20130917T0700 Tue,20130917T0830 tentative
240
+ the_title matt Wed,20130918T0930 Wed,20130918T1030 confirmed
241
+ the_title here Wed,20130918T0930 Wed,20130918T1030 tentative
242
+ the_title matt Wed,20130918T1800 Wed,20130918T2000 confirmed
243
+ the_title here Wed,20130918T1800 Wed,20130918T2000 tentative
244
+ the_title matt Tue,20130924T0700 Tue,20130924T0830 confirmed
245
+ the_title here Tue,20130924T0700 Tue,20130924T0830 tentative
246
+ the_title matt Wed,20130925T0930 Wed,20130925T1030 confirmed
247
+ the_title here Wed,20130925T0930 Wed,20130925T1030 tentative
248
+ the_title matt Wed,20130925T1800 Wed,20130925T2000 confirmed
249
+ the_title here Wed,20130925T1800 Wed,20130925T2000 tentative"
250
+ end
251
+ end
252
+
253
+ describe :add_subject do
254
+ it "should add a subject within time constraints" do
255
+ subjects = [{ "role" => "owner", "subject" => matt, "status" => "confirmed" } ]
256
+ pattern = [ { "day" => "mon", "start" => "0930", "finish" => "1030"} ]
257
+ Reservation::Event.create_weekly "my_title", "2013-09-03", "2013-10-13", subjects, pattern
258
+ Reservation::Event.add_subject({ "role" => "student", "status" => "tentative", "subject" => bill}, { "from" => "2013-09-01", "upto" => "2013-09-24", "context" => matt })
259
+ the_reservations.should == "my_title matt Mon,20130909T0930 Mon,20130909T1030 confirmed
260
+ my_title matt Mon,20130916T0930 Mon,20130916T1030 confirmed
261
+ my_title matt Mon,20130923T0930 Mon,20130923T1030 confirmed
262
+ my_title matt Mon,20130930T0930 Mon,20130930T1030 confirmed
263
+ my_title matt Mon,20131007T0930 Mon,20131007T1030 confirmed
264
+ my_title bill Mon,20130909T0930 Mon,20130909T1030 tentative
265
+ my_title bill Mon,20130916T0930 Mon,20130916T1030 tentative
266
+ my_title bill Mon,20130923T0930 Mon,20130923T1030 tentative"
267
+ end
268
+
269
+ it "should add a subject within schedule contraints" do
270
+ subjects = [ { "role" => "owner", "subject" => matt, "status" => "confirmed" },
271
+ { "role" => "place", "subject" => here, "status" => "tentative" } ]
272
+
273
+ pattern = [ { "day" => "wed", "start" => "0930", "finish" => "1030" },
274
+ { "day" => "wed", "start" => "18", "finish" => "20" },
275
+ { "day" => "tue", "start" => "7", "finish" => "830" } ]
276
+
277
+ Reservation::Event.create_weekly "the_title", "2013-09-03", "2013-10-13", subjects, pattern
278
+
279
+ add_schedule = [ { "day" => "wed", "start" => "18", "finish" => "20"} ]
280
+ Reservation::Event.add_subject({ "role" => "student", "status" => "tentative", "subject" => bill}, { "from" => "2013-09-17", "upto" => "2013-10-02", "schedule" => add_schedule })
281
+ the_reservations(bill).should == "the_title bill Wed,20130918T1800 Wed,20130918T2000 tentative
282
+ the_title bill Wed,20130925T1800 Wed,20130925T2000 tentative
283
+ the_title bill Wed,20131002T1800 Wed,20131002T2000 tentative"
284
+ end
285
+ end
286
+
287
+ describe :remove_subject do
288
+ it "should remove a subject within time constraints" do
289
+ subjects = [{ "role" => "owner", "subject" => matt, "status" => "confirmed" }, { "role" => "owner", "subject" => bill, "status" => "confirmed" } ]
290
+ pattern = [ { "day" => "mon", "start" => "0930", "finish" => "1030"} ]
291
+ Reservation::Event.create_weekly "my_title", "2013-09-03", "2013-10-13", subjects, pattern
292
+ Reservation::Event.remove_subject(bill, { "from" => "2013-09-15", "upto" => "2013-09-24", "context" => matt })
293
+ the_reservations.should == "my_title matt Mon,20130909T0930 Mon,20130909T1030 confirmed
294
+ my_title bill Mon,20130909T0930 Mon,20130909T1030 confirmed
295
+ my_title matt Mon,20130916T0930 Mon,20130916T1030 confirmed
296
+ my_title matt Mon,20130923T0930 Mon,20130923T1030 confirmed
297
+ my_title matt Mon,20130930T0930 Mon,20130930T1030 confirmed
298
+ my_title bill Mon,20130930T0930 Mon,20130930T1030 confirmed
299
+ my_title matt Mon,20131007T0930 Mon,20131007T1030 confirmed
300
+ my_title bill Mon,20131007T0930 Mon,20131007T1030 confirmed"
301
+ end
302
+
303
+ it "should remove a subject within schedule contraints" do
304
+ subjects = [ { "role" => "owner", "subject" => matt, "status" => "confirmed" },
305
+ { "role" => "helpr", "subject" => bill, "status" => "tentative" },
306
+ { "role" => "place", "subject" => here, "status" => "tentative" }
307
+ ]
308
+
309
+ pattern = [ { "day" => "wed", "start" => "0930", "finish" => "1030"},
310
+ { "day" => "wed", "start" => "18", "finish" => "20" },
311
+ { "day" => "tue", "start" => "7", "finish" => "830" } ]
312
+
313
+ Reservation::Event.create_weekly "the_title", "2013-09-03", "2013-10-13", subjects, pattern
314
+
315
+ remove_schedule = [ { "day" => "wed", "start" => "18", "finish" => "20"}, { "day" => "wed", "start" => "930", "finish" => "1030"} ]
316
+ Reservation::Event.remove_subject(bill, { "from" => "2013-09-17", "upto" => "2013-10-02", "schedule" => remove_schedule })
317
+
318
+ the_reservations(bill).should == "
319
+ the_title bill Tue,20130903T0700 Tue,20130903T0830 tentative
320
+ the_title bill Wed,20130904T0930 Wed,20130904T1030 tentative
321
+ the_title bill Wed,20130904T1800 Wed,20130904T2000 tentative
322
+ the_title bill Tue,20130910T0700 Tue,20130910T0830 tentative
323
+ the_title bill Wed,20130911T0930 Wed,20130911T1030 tentative
324
+ the_title bill Wed,20130911T1800 Wed,20130911T2000 tentative
325
+ the_title bill Tue,20130917T0700 Tue,20130917T0830 tentative
326
+ the_title bill Tue,20130924T0700 Tue,20130924T0830 tentative
327
+ the_title bill Tue,20131001T0700 Tue,20131001T0830 tentative
328
+ the_title bill Tue,20131008T0700 Tue,20131008T0830 tentative
329
+ the_title bill Wed,20131009T0930 Wed,20131009T1030 tentative
330
+ the_title bill Wed,20131009T1800 Wed,20131009T2000 tentative".strip
331
+ end
191
332
  end
192
333
  end
@@ -0,0 +1,60 @@
1
+ require 'spec_helper'
2
+
3
+ describe Reservation::Schedule::Daily do
4
+ before { Time.zone = "Europe/Paris" }
5
+ let(:event_start) { time("2013-07-12T07:00:00") }
6
+ let(:event_finish) { time("2013-07-12T09:30:00") }
7
+ let(:event) { Reservation::Event.new :start => event_start, :finish => event_finish }
8
+
9
+ def interval start, finish
10
+ start = Reservation::Schedule::HourMinute.parse start
11
+ finish = Reservation::Schedule::HourMinute.parse finish
12
+ Reservation::Schedule::Interval.new(start, finish)
13
+ end
14
+
15
+ it "should match when there exists a matching interval on the same day as the given event" do
16
+ daily = Reservation::Schedule::Daily.new(5)
17
+ daily.add interval("7h", "9:30")
18
+ daily.add interval("18", "2230")
19
+
20
+ daily.matches?(event).should be_true
21
+ end
22
+
23
+ it "should not match when the day is different" do
24
+ daily = Reservation::Schedule::Daily.new(3)
25
+ daily.add interval("7h", "9:30")
26
+ daily.add interval("18", "2230")
27
+
28
+ daily.matches?(event).should be_false
29
+ end
30
+
31
+ it "should not match when there is no corresponding interval" do
32
+ daily = Reservation::Schedule::Daily.new(5)
33
+ daily.add interval("7h", "9:45")
34
+ daily.add interval("18", "2230")
35
+
36
+ daily.matches?(event).should be_false
37
+ end
38
+
39
+ it "should generate events corresponding to its intervals on the given matching date" do
40
+ daily = Reservation::Schedule::Daily.new(5)
41
+ daily.add interval("7h", "9:45")
42
+ daily.add interval("18", "2230")
43
+
44
+ events = []
45
+ daily.generate date("2013-07-12"), events
46
+ events.map { |e| "#{e.start.prettyd} #{e.finish.pretty}"}.join("\n").
47
+ should == "Fri,20130712T0700 20130712T0945
48
+ Fri,20130712T1800 20130712T2230"
49
+ end
50
+
51
+ it "should generate no events when the date does not match its weekday" do
52
+ daily = Reservation::Schedule::Daily.new(2)
53
+ daily.add interval("7h", "9:45")
54
+ daily.add interval("18", "2230")
55
+
56
+ events = []
57
+ daily.generate date("2013-07-12"), events
58
+ events.should == []
59
+ end
60
+ end
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ describe Reservation::Schedule::HourMinute do
4
+ before { Time.zone = "Europe/Paris" }
5
+ let(:rightnow) { time("2013-04-30T18:30:00") }
6
+
7
+ it "should match when hours and minutes match" do
8
+ hm = Reservation::Schedule::HourMinute.new 18, 30
9
+ hm.matches_time?(rightnow).should be_true
10
+ end
11
+
12
+ it "should not match when minutes differ" do
13
+ hm = Reservation::Schedule::HourMinute.new 18, 31
14
+ hm.matches_time?(rightnow).should be_false
15
+ end
16
+
17
+ it "should not match when hours differ" do
18
+ hm = Reservation::Schedule::HourMinute.new 17, 30
19
+ hm.matches_time?(rightnow).should be_false
20
+ end
21
+
22
+ it "should parse '7' as '0700'" do
23
+ hm = Reservation::Schedule::HourMinute.parse "7"
24
+ hm.hour.should == 7
25
+ hm.minute.should == 0
26
+ end
27
+
28
+ it "should parse '11' as '1100'" do
29
+ hm = Reservation::Schedule::HourMinute.parse "11"
30
+ hm.hour.should == 11
31
+ hm.minute.should == 0
32
+ end
33
+
34
+ it "should parse '815' as '0815'" do
35
+ hm = Reservation::Schedule::HourMinute.parse "815"
36
+ hm.hour.should == 8
37
+ hm.minute.should == 15
38
+ end
39
+
40
+ it "should change a date to a time with the given hour and minute" do
41
+ hm = Reservation::Schedule::HourMinute.parse "815"
42
+ time = hm.change date("2013-07-12")
43
+ time.pretty.should == "20130712T0815"
44
+ end
45
+ end
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+
3
+ describe Reservation::Schedule::Interval do
4
+ before { Time.zone = "Europe/Paris" }
5
+
6
+ let(:start) { time("2013-04-30T18:30:00") }
7
+ let(:finish) { time("2013-04-30T22:30:00") }
8
+ let(:event) { Reservation::Event.new :start => start, :finish => finish }
9
+
10
+ it "should match when start and finish match" do
11
+ hm1 = Reservation::Schedule::HourMinute.new 18, 30
12
+ hm2 = Reservation::Schedule::HourMinute.new 22, 30
13
+ interval = Reservation::Schedule::Interval.new hm1, hm2
14
+ interval.matches?(event).should be_true
15
+ end
16
+
17
+ it "should not match when start differs" do
18
+ hm1 = Reservation::Schedule::HourMinute.new 14, 00
19
+ hm2 = Reservation::Schedule::HourMinute.new 22, 30
20
+ interval = Reservation::Schedule::Interval.new hm1, hm2
21
+ interval.matches?(event).should be_false
22
+ end
23
+
24
+ it "should not match when finish differs" do
25
+ hm1 = Reservation::Schedule::HourMinute.new 18, 30
26
+ hm2 = Reservation::Schedule::HourMinute.new 23, 00
27
+ interval = Reservation::Schedule::Interval.new hm1, hm2
28
+ interval.matches?(event).should be_false
29
+ end
30
+
31
+ it "should generate a new Event with its start and finish times on the given date" do
32
+ interval = make_interval "1830", "2300"
33
+ event = interval.generate date('2013-07-12')
34
+ event.start.pretty.should == "20130712T1830"
35
+ event.finish.pretty.should == "20130712T2300"
36
+ end
37
+ end
@@ -0,0 +1,74 @@
1
+ require 'spec_helper'
2
+
3
+ describe Reservation::Schedule::Weekly do
4
+ before { Time.zone = "Europe/Paris" }
5
+ let(:event_start) { time("2013-07-12T07:00:00") } # this is a friday
6
+ let(:event_finish) { time("2013-07-12T09:30:00") }
7
+ let(:event) { Reservation::Event.new :start => event_start, :finish => event_finish }
8
+
9
+ it "should not match in any case if the event spans more than one day" do
10
+ weekly = Reservation::Schedule::Weekly.new [ { "day" => "wed", "start" => "0930", "finish" => "10:30"},
11
+ { "day" => "wed", "start" => "18", "finish" => "20" },
12
+ { "day" => "fri", "start" => "7h", "finish" => "9:30" },
13
+ { "day" => "tue", "start" => "7", "finish" => "8h30" } ]
14
+
15
+ start = time("2013-02-12T07:00:00") # this is a tuesday
16
+ finish = time("2013-03-12T09:30:00")
17
+ my_event = Reservation::Event.new :start => start, :finish => finish
18
+
19
+ weekly.matches?(my_event).should be_false
20
+ end
21
+
22
+ it "should match when there exists a matching interval on the same day as the given event" do
23
+ weekly = Reservation::Schedule::Weekly.new [ { "day" => "wed", "start" => "0930", "finish" => "10:30"},
24
+ { "day" => "wed", "start" => "18", "finish" => "20" },
25
+ { "day" => "fri", "start" => "7h", "finish" => "9:30" },
26
+ { "day" => "tue", "start" => "7", "finish" => "8h30" } ]
27
+
28
+ weekly.matches?(event).should be_true
29
+ end
30
+
31
+ it "should not match when there exists no matching interval on the same day as the given event" do
32
+ weekly = Reservation::Schedule::Weekly.new [ { "day" => "wed", "start" => "0930", "finish" => "10:30"},
33
+ { "day" => "wed", "start" => "18", "finish" => "20" },
34
+ { "day" => "fri", "start" => "7h", "finish" => "8:30" },
35
+ { "day" => "tue", "start" => "7", "finish" => "8h30" } ]
36
+
37
+ weekly.matches?(event).should be_false
38
+ end
39
+
40
+ it "should generate events corresponding to the weekly schedule" do
41
+ weekly = Reservation::Schedule::Weekly.new [ { "day" => "wed", "start" => "0930", "finish" => "10:30"},
42
+ { "day" => "wed", "start" => "18", "finish" => "20" },
43
+ { "day" => "fri", "start" => "10h", "finish" => "11:45" },
44
+ { "day" => "tue", "start" => "7", "finish" => "8h30" } ]
45
+
46
+ events = weekly.generate date("2013-07-08"), date("2013-07-23")
47
+ events.map { |e| "#{e.start.prettyd} #{e.finish.pretty}"}.join("\n").
48
+ should == "Tue,20130709T0700 20130709T0830
49
+ Wed,20130710T0930 20130710T1030
50
+ Wed,20130710T1800 20130710T2000
51
+ Fri,20130712T1000 20130712T1145
52
+ Tue,20130716T0700 20130716T0830
53
+ Wed,20130717T0930 20130717T1030
54
+ Wed,20130717T1800 20130717T2000
55
+ Fri,20130719T1000 20130719T1145
56
+ Tue,20130723T0700 20130723T0830"
57
+ end
58
+
59
+ it "should filter a set of events" do
60
+ weekly = Reservation::Schedule::Weekly.new [ { "day" => "wed", "start" => "0930", "finish" => "10:30"},
61
+ { "day" => "wed", "start" => "18", "finish" => "20" },
62
+ { "day" => "fri", "start" => "10h", "finish" => "11:45" },
63
+ { "day" => "tue", "start" => "7", "finish" => "8h30" } ]
64
+
65
+ events = weekly.generate date("2013-07-08"), date("2013-07-23")
66
+
67
+ filter = Reservation::Schedule::Weekly.new [ { "day" => "wed", "start" => "0930", "finish" => "10:30"} ]
68
+
69
+ events = filter.filter events
70
+ events.map { |e| "#{e.start.prettyd} #{e.finish.pretty}"}.join("\n").
71
+ should == "Wed,20130710T0930 20130710T1030
72
+ Wed,20130717T0930 20130717T1030"
73
+ end
74
+ end
@@ -27,6 +27,10 @@ module Helper
27
27
  def time t
28
28
  Time.parse t
29
29
  end
30
+
31
+ def make_interval start, finish
32
+ Reservation::Schedule::Interval.from start, finish
33
+ end
30
34
  end
31
35
 
32
36
  RSpec::Core::ExampleGroup.send :include, Helper
@@ -39,4 +43,13 @@ ActiveRecord::Base.silence do
39
43
  load(File.dirname(__FILE__) + '/dummy/db/schema.rb')
40
44
  end
41
45
 
46
+ class Time
47
+ def pretty
48
+ self.strftime("%Y%m%dT%H%M")
49
+ end
50
+
51
+ def prettyd
52
+ self.strftime("%a,%Y%m%dT%H%M")
53
+ end
54
+ end
42
55
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: reservation
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-07-11 00:00:00.000000000 Z
12
+ date: 2013-07-12 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -99,16 +99,21 @@ extensions: []
99
99
  extra_rdoc_files: []
100
100
  files:
101
101
  - .gitignore
102
+ - .loco
102
103
  - .rspec
103
104
  - Gemfile
104
105
  - MIT-LICENSE
105
- - README.rdoc
106
+ - README.md
106
107
  - Rakefile
107
108
  - lib/generators/reservation/reservations_generator.rb
108
109
  - lib/generators/reservation/templates/migration.rb
109
110
  - lib/reservation.rb
110
111
  - lib/reservation/event.rb
112
+ - lib/reservation/event_filter.rb
111
113
  - lib/reservation/reservation.rb
114
+ - lib/reservation/schedule.rb
115
+ - lib/reservation/scopes.rb
116
+ - lib/reservation/time_offset.rb
112
117
  - lib/reservation/version.rb
113
118
  - lib/tasks/reservation_tasks.rake
114
119
  - reservation.gemspec
@@ -167,6 +172,10 @@ files:
167
172
  - spec/dummy/test/unit/reservation/time_slot_test.rb
168
173
  - spec/dummy/test/unit/thing_test.rb
169
174
  - spec/models/event_spec.rb
175
+ - spec/models/schedule/daily_spec.rb
176
+ - spec/models/schedule/hour_minute_spec.rb
177
+ - spec/models/schedule/interval_spec.rb
178
+ - spec/models/schedule/weekly_spec.rb
170
179
  - spec/spec_helper.rb
171
180
  homepage: http://github.com/conanite/reservation
172
181
  licenses: []
@@ -248,4 +257,8 @@ test_files:
248
257
  - spec/dummy/test/unit/reservation/time_slot_test.rb
249
258
  - spec/dummy/test/unit/thing_test.rb
250
259
  - spec/models/event_spec.rb
260
+ - spec/models/schedule/daily_spec.rb
261
+ - spec/models/schedule/hour_minute_spec.rb
262
+ - spec/models/schedule/interval_spec.rb
263
+ - spec/models/schedule/weekly_spec.rb
251
264
  - spec/spec_helper.rb
@@ -1,3 +0,0 @@
1
- = Reservation
2
-
3
- This project rocks and uses MIT-LICENSE.