acts_as_bookable 0.1.0
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.
- 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
|