acts_as_bookable 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/.travis.yml +43 -0
- data/Appraisals +21 -0
- data/Gemfile +21 -0
- data/Gemfile.lock +168 -0
- data/Guardfile +5 -0
- data/MIT-LICENSE +20 -0
- data/README.md +473 -0
- data/Rakefile +19 -0
- data/acts_as_bookable.gemspec +41 -0
- data/app/assets/images/acts_as_bookable/.keep +0 -0
- data/app/assets/javascripts/acts_as_bookable/application.js +13 -0
- data/app/assets/stylesheets/acts_as_bookable/application.css +15 -0
- data/app/controllers/acts_as_bookable/application_controller.rb +4 -0
- data/app/helpers/acts_as_bookable/application_helper.rb +4 -0
- data/app/views/layouts/acts_as_bookable/application.html.erb +14 -0
- data/bin/rails +12 -0
- data/config/locales/en.yml +12 -0
- data/config/routes.rb +2 -0
- data/db/migrate/20160217085200_create_acts_as_bookable_bookings.rb +14 -0
- data/gemfiles/activerecord_3.2.gemfile +16 -0
- data/gemfiles/activerecord_4.0.gemfile +16 -0
- data/gemfiles/activerecord_4.1.gemfile +16 -0
- data/gemfiles/activerecord_4.2.gemfile +17 -0
- data/gemfiles/activerecord_5.0.gemfile +17 -0
- data/lib/acts_as_bookable/bookable/core.rb +285 -0
- data/lib/acts_as_bookable/bookable.rb +56 -0
- data/lib/acts_as_bookable/booker.rb +70 -0
- data/lib/acts_as_bookable/booking.rb +54 -0
- data/lib/acts_as_bookable/db_utils.rb +39 -0
- data/lib/acts_as_bookable/engine.rb +5 -0
- data/lib/acts_as_bookable/t.rb +11 -0
- data/lib/acts_as_bookable/time_utils.rb +135 -0
- data/lib/acts_as_bookable/version.rb +3 -0
- data/lib/acts_as_bookable.rb +42 -0
- data/lib/tasks/acts_as_bookable_tasks.rake +4 -0
- data/spec/acts_as_bookable/acts_as_bookable_spec.rb +52 -0
- data/spec/acts_as_bookable/acts_as_booker_spec.rb +57 -0
- data/spec/acts_as_bookable/bookable/core_spec.rb +593 -0
- data/spec/acts_as_bookable/bookable_spec.rb +124 -0
- data/spec/acts_as_bookable/booker_spec.rb +140 -0
- data/spec/acts_as_bookable/booking_spec.rb +241 -0
- data/spec/acts_as_bookable/schedule_spec.rb +137 -0
- data/spec/acts_as_bookable/time_utils_spec.rb +525 -0
- data/spec/factories/bookable.rb +7 -0
- data/spec/factories/booker.rb +5 -0
- data/spec/factories/room.rb +11 -0
- data/spec/internal/app/models/Bookable.rb +3 -0
- data/spec/internal/app/models/Booker.rb +3 -0
- data/spec/internal/app/models/Event.rb +3 -0
- data/spec/internal/app/models/Generic.rb +2 -0
- data/spec/internal/app/models/NotBooker.rb +2 -0
- data/spec/internal/app/models/Room.rb +3 -0
- data/spec/internal/app/models/Show.rb +3 -0
- data/spec/internal/app/models/Unbookable.rb +2 -0
- data/spec/internal/config/database.yml.sample +17 -0
- data/spec/internal/db/schema.rb +51 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/support/0-helpers.rb +32 -0
- data/spec/support/1-database.rb +42 -0
- data/spec/support/2-database_cleaner.rb +21 -0
- data/spec/support/3-factory_girl.rb +12 -0
- metadata +296 -0
@@ -0,0 +1,135 @@
|
|
1
|
+
module ActsAsBookable
|
2
|
+
##
|
3
|
+
# Provide helper functions to manage operations and queries related to times
|
4
|
+
# and schedules
|
5
|
+
#
|
6
|
+
module TimeUtils
|
7
|
+
class << self
|
8
|
+
##
|
9
|
+
# Check if time is included in a time interval. The ending time is excluded
|
10
|
+
#
|
11
|
+
# @param time The time to check
|
12
|
+
# @param interval_start The beginning time of the interval to match against
|
13
|
+
# @param interval_end The ending time of the interval to match against
|
14
|
+
#
|
15
|
+
def time_in_interval? (time, interval_start, interval_end)
|
16
|
+
time >= interval_start && time < interval_end
|
17
|
+
end
|
18
|
+
|
19
|
+
##
|
20
|
+
# Check if there is an occurrence of a schedule that contains a time interval
|
21
|
+
#
|
22
|
+
# @param schedule The schedule
|
23
|
+
# @param interval_start The beginning Time of the interval
|
24
|
+
# @param interval_end The ending Time of the interval
|
25
|
+
# @return true if the interval falls within an occurrence of the schedule, otherwise false
|
26
|
+
#
|
27
|
+
def interval_in_schedule?(schedule, interval_start, interval_end)
|
28
|
+
# Check if interval_start and interval_end falls within any occurrence
|
29
|
+
return false if(!time_in_schedule?(schedule,interval_start) || !time_in_schedule?(schedule,interval_end))
|
30
|
+
|
31
|
+
# Check if both interval_start and interval_end falls within the SAME occurrence
|
32
|
+
between = schedule.occurrences_between(interval_start, interval_end, true)
|
33
|
+
contains = false
|
34
|
+
between.each do |oc|
|
35
|
+
oc_end = oc + schedule.duration
|
36
|
+
contains = true if (time_in_interval?(interval_start,oc,oc_end) && time_in_interval?(interval_end,oc,oc_end))
|
37
|
+
break if contains
|
38
|
+
end
|
39
|
+
|
40
|
+
contains
|
41
|
+
end
|
42
|
+
|
43
|
+
##
|
44
|
+
# Check if there is an occurrence of a schedule that contains a time
|
45
|
+
# @param schedule The schedule
|
46
|
+
# @param time The time
|
47
|
+
# @return true if the time falls within an occurrence of the schedule, otherwise false
|
48
|
+
#
|
49
|
+
def time_in_schedule?(schedule, time)
|
50
|
+
return schedule.occurring_at? time
|
51
|
+
end
|
52
|
+
|
53
|
+
##
|
54
|
+
# Returns an array of sub-intervals given another array of intervals, which are the overlapping insersections of each-others.
|
55
|
+
#
|
56
|
+
# @param intervals an array of intervals
|
57
|
+
# @return an array of subintervals, sorted by time_start
|
58
|
+
#
|
59
|
+
# An interval is defined as a hash with at least the following fields: `time_from` and `time_end`. An interval may contain more
|
60
|
+
# fields. In that case, it's suggested to give a block with the instructions to correctly merge two intervals when needed.
|
61
|
+
#
|
62
|
+
# e.g: given these 7 intervals
|
63
|
+
# |------| |---| |----------|
|
64
|
+
# |---| |--|
|
65
|
+
# |------| |--| |-------------|
|
66
|
+
# the output is an array containing these 8 intervals:
|
67
|
+
# |--| |--| |---| |--| |---| |------|
|
68
|
+
# |---| |------|
|
69
|
+
# the number of subintervals may increase or decrease because some intervals may be split, while
|
70
|
+
# some others may be merged.
|
71
|
+
#
|
72
|
+
# If a block is given, it's called before merging two intervals. The block should provide instructions to merge intervals, and should return the merged fields in a hash
|
73
|
+
def subintervals(intervals, &block)
|
74
|
+
raise ArgumentError.new('intervals must be an array') unless intervals.is_a? Array
|
75
|
+
|
76
|
+
steps = [] # Steps will be extracted from intervals
|
77
|
+
subintervals = [] # The output
|
78
|
+
last_time = nil
|
79
|
+
last_attrs = nil
|
80
|
+
started_count = 0 # The number of intervals opened inside the cycle
|
81
|
+
|
82
|
+
# Extract start times and end times from intervals, and create steps
|
83
|
+
intervals.each do |el|
|
84
|
+
begin
|
85
|
+
ts = el[:time_start].to_time
|
86
|
+
te = el[:time_end].to_time
|
87
|
+
rescue NoMethodError
|
88
|
+
raise ArgumentError.new('intervals must define :time_start and :time_end as Time or Date')
|
89
|
+
end
|
90
|
+
attrs = el.clone
|
91
|
+
attrs.delete(:time_start)
|
92
|
+
attrs.delete(:time_end)
|
93
|
+
steps << { opening: 1, time: el[:time_start], attrs: attrs } # Start step
|
94
|
+
steps << { opening: -1, time: el[:time_end], attrs: attrs.clone } # End step
|
95
|
+
end
|
96
|
+
|
97
|
+
# Sort steps by time (and opening if time is the same)
|
98
|
+
steps.sort! do |a,b|
|
99
|
+
diff = a[:time] <=> b[:time]
|
100
|
+
diff = a[:opening] <=> b[:opening] if (diff == 0)
|
101
|
+
diff
|
102
|
+
end
|
103
|
+
|
104
|
+
# Iterate over steps
|
105
|
+
steps.each do |step|
|
106
|
+
if (started_count == 0)
|
107
|
+
last_time = step[:time]
|
108
|
+
last_attrs = step[:attrs]
|
109
|
+
else
|
110
|
+
if(step[:time] > last_time)
|
111
|
+
subintervals << ({
|
112
|
+
time_start: last_time,
|
113
|
+
time_end: step[:time]
|
114
|
+
}.merge(last_attrs))
|
115
|
+
|
116
|
+
last_time = step[:time]
|
117
|
+
end
|
118
|
+
|
119
|
+
if block_given?
|
120
|
+
last_attrs = block.call(last_attrs.clone, step[:attrs],(step[:opening] == 1 ? :open : :close))
|
121
|
+
else
|
122
|
+
last_attrs = step[:attrs]
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Update started_count
|
127
|
+
started_count += step[:opening]
|
128
|
+
end
|
129
|
+
|
130
|
+
subintervals
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'active_record/version'
|
3
|
+
require 'active_support/core_ext/module'
|
4
|
+
require_relative 'acts_as_bookable/engine' if defined?(Rails)
|
5
|
+
require 'ice_cube'
|
6
|
+
IceCube.compatibility = 12 # Drop compatibility for :start_date, avoiding a bunch of warnings caused by serialization
|
7
|
+
|
8
|
+
module ActsAsBookable
|
9
|
+
extend ActiveSupport::Autoload
|
10
|
+
|
11
|
+
autoload :Bookable
|
12
|
+
autoload :Booker
|
13
|
+
autoload :Booking
|
14
|
+
autoload :T
|
15
|
+
autoload :VERSION
|
16
|
+
autoload :TimeUtils
|
17
|
+
autoload :DBUtils
|
18
|
+
|
19
|
+
autoload_under 'bookable' do
|
20
|
+
autoload :Core
|
21
|
+
end
|
22
|
+
|
23
|
+
class InitializationError < StandardError
|
24
|
+
def initialize model, message
|
25
|
+
super "Error initializing acts_as_bookable on #{model.to_s} - " + message
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class OptionsInvalid < StandardError
|
30
|
+
def initialize model, message
|
31
|
+
super "Error validating options for #{model.to_s} - " + message
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class AvailabilityError < StandardError
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
ActiveSupport.on_load(:active_record) do
|
40
|
+
extend ActsAsBookable::Bookable
|
41
|
+
include ActsAsBookable::Booker
|
42
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'acts_as_bookable' do
|
4
|
+
it "should provide a class method 'bookable?' that is false for unbookable models" do
|
5
|
+
expect(Unbookable).not_to be_bookable
|
6
|
+
end
|
7
|
+
|
8
|
+
describe 'Bookable Method Generation' do
|
9
|
+
before :each do
|
10
|
+
Unbookable.acts_as_bookable
|
11
|
+
@bookable = Unbookable.new()
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should respond 'true' to bookable?" do
|
15
|
+
expect(@bookable.class).to be_bookable
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe 'class configured as Bookable' do
|
20
|
+
before(:each) do
|
21
|
+
@bookable = Bookable.new
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'should add #bookable? query method to the class-side' do
|
25
|
+
expect(Bookable).to respond_to(:bookable?)
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'should return true from the class-side #bookable?' do
|
29
|
+
expect(Bookable.bookable?).to be_truthy
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'should return false from the base #bookable?' do
|
33
|
+
expect(ActiveRecord::Base.bookable?).to be_falsy
|
34
|
+
end
|
35
|
+
|
36
|
+
# it 'should add #tag method on the instance-side' do
|
37
|
+
# expect(@bookable).to respond_to(:tag)
|
38
|
+
# end
|
39
|
+
|
40
|
+
# it 'should generate an association for #owned_taggings and #owned_tags' do
|
41
|
+
# expect(@bookable).to respond_to(:owned_taggings, :owned_tags)
|
42
|
+
# end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe 'Reloading' do
|
46
|
+
it 'should save a model instantiated by Model.find' do
|
47
|
+
bookable = create(:bookable)
|
48
|
+
found_bookable = Bookable.find(bookable.id)
|
49
|
+
expect(found_bookable.save).to eq true
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'acts_as_booker' do
|
4
|
+
it "should provide a class method 'booker?' that is false for not booker models" do
|
5
|
+
expect(NotBooker).not_to be_booker
|
6
|
+
end
|
7
|
+
|
8
|
+
describe 'Booker Method Generation' do
|
9
|
+
before :each do
|
10
|
+
NotBooker.acts_as_booker
|
11
|
+
@booker = NotBooker.new()
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should respond 'true' to booker?" do
|
15
|
+
expect(@booker.class).to be_booker
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe 'class configured as Booker' do
|
20
|
+
before(:each) do
|
21
|
+
@booker = Booker.new
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'should add #booker? query method to the class-side' do
|
25
|
+
expect(Booker).to respond_to(:booker?)
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'should return true from the class-side #booker?' do
|
29
|
+
expect(Booker.booker?).to be_truthy
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'should return false from the base #booker?' do
|
33
|
+
expect(ActiveRecord::Base.booker?).to be_falsy
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'should add #booker? query method to the instance-side' do
|
37
|
+
expect(@booker).to respond_to(:booker?)
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'should add #booker? query method to the instance-side' do
|
41
|
+
expect(@booker.booker?).to be_truthy
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
# it 'should generate an association for #owned_taggings and #owned_tags' do
|
46
|
+
# expect(@booker).to respond_to(:owned_taggings, :owned_tags)
|
47
|
+
# end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe 'Reloading' do
|
51
|
+
it 'should save a model instantiated by Model.find' do
|
52
|
+
booker = Booker.create!(name: 'Booker')
|
53
|
+
found_booker = Booker.find(booker.id)
|
54
|
+
expect(found_booker.save).to eq true
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|