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,16 @@
|
|
1
|
+
# This file was generated by Appraisal
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
gem "activerecord", :github => "rails/rails", :branch => "4-1-stable"
|
6
|
+
|
7
|
+
group :local_development do
|
8
|
+
gem "guard"
|
9
|
+
gem "guard-rspec"
|
10
|
+
gem "appraisal"
|
11
|
+
gem "rake"
|
12
|
+
gem "byebug", :platform => :mri_21
|
13
|
+
gem "pry-nav"
|
14
|
+
end
|
15
|
+
|
16
|
+
gemspec :path => "../"
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# This file was generated by Appraisal
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
gem "railties", :github => "rails/rails", :branch => "4-2-stable"
|
6
|
+
gem "activerecord", :github => "rails/rails", :branch => "4-2-stable"
|
7
|
+
|
8
|
+
group :local_development do
|
9
|
+
gem "guard"
|
10
|
+
gem "guard-rspec"
|
11
|
+
gem "appraisal"
|
12
|
+
gem "rake"
|
13
|
+
gem "byebug", :platform => :mri_21
|
14
|
+
gem "pry-nav"
|
15
|
+
end
|
16
|
+
|
17
|
+
gemspec :path => "../"
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# This file was generated by Appraisal
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
gem "railties", :github => "rails/rails", :branch => "master"
|
6
|
+
gem "activerecord", :github => "rails/rails", :branch => "master"
|
7
|
+
|
8
|
+
group :local_development do
|
9
|
+
gem "guard"
|
10
|
+
gem "guard-rspec"
|
11
|
+
gem "appraisal"
|
12
|
+
gem "rake"
|
13
|
+
gem "byebug", :platform => :mri_21
|
14
|
+
gem "pry-nav"
|
15
|
+
end
|
16
|
+
|
17
|
+
gemspec :path => "../"
|
@@ -0,0 +1,285 @@
|
|
1
|
+
module ActsAsBookable::Bookable
|
2
|
+
module Core
|
3
|
+
def self.included(base)
|
4
|
+
base.extend ActsAsBookable::Bookable::Core::ClassMethods
|
5
|
+
base.send :include, ActsAsBookable::Bookable::Core::InstanceMethods
|
6
|
+
|
7
|
+
base.initialize_acts_as_bookable_core
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
##
|
12
|
+
# Initialize the core of Bookable
|
13
|
+
#
|
14
|
+
def initialize_acts_as_bookable_core
|
15
|
+
# Manage the options
|
16
|
+
set_options
|
17
|
+
end
|
18
|
+
|
19
|
+
##
|
20
|
+
# Check if options passed for booking this Bookable are valid
|
21
|
+
#
|
22
|
+
# @raise ActsAsBookable::OptionsInvalid if options are not valid
|
23
|
+
#
|
24
|
+
def validate_booking_options!(options)
|
25
|
+
unpermitted_params = []
|
26
|
+
required_params = {}
|
27
|
+
|
28
|
+
#
|
29
|
+
# Set unpermitted parameters and required parameters depending on Bookable options
|
30
|
+
#
|
31
|
+
|
32
|
+
# Switch :time_type
|
33
|
+
case self.booking_opts[:time_type]
|
34
|
+
# when :range, we need :time_start and :time_end
|
35
|
+
when :range
|
36
|
+
required_params[:time_start] = [Time,Date]
|
37
|
+
required_params[:time_end] = [Time,Date]
|
38
|
+
unpermitted_params << :time
|
39
|
+
when :fixed
|
40
|
+
required_params[:time] = [Time,Date]
|
41
|
+
unpermitted_params << :time_start
|
42
|
+
unpermitted_params << :time_end
|
43
|
+
when :none
|
44
|
+
unpermitted_params << :time_start
|
45
|
+
unpermitted_params << :time_end
|
46
|
+
unpermitted_params << :time
|
47
|
+
end
|
48
|
+
|
49
|
+
# Switch :capacity_type
|
50
|
+
case self.booking_opts[:capacity_type]
|
51
|
+
when :closed
|
52
|
+
required_params[:amount] = [Integer]
|
53
|
+
when :open
|
54
|
+
required_params[:amount] = [Integer]
|
55
|
+
when :none
|
56
|
+
unpermitted_params << :amount
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
# Actual validation
|
61
|
+
#
|
62
|
+
unpermitted_params = unpermitted_params
|
63
|
+
.select{ |p| options.has_key?(p) }
|
64
|
+
.map{ |p| "'#{p}'"}
|
65
|
+
wrong_types = required_params
|
66
|
+
.select{ |k,v| options.has_key?(k) && (v.select{|type| options[k].is_a?(type)}.length == 0) }
|
67
|
+
.map{ |k,v| "'#{k}' must be a '#{v.join(' or ')}' but '#{options[k].class.to_s}' found" }
|
68
|
+
required_params = required_params
|
69
|
+
.select{ |k,v| !options.has_key?(k) }
|
70
|
+
.map{ |k,v| "'#{k}'" }
|
71
|
+
|
72
|
+
#
|
73
|
+
# Raise OptionsInvalid if some invalid parameters were found
|
74
|
+
#
|
75
|
+
if unpermitted_params.length + required_params.length + wrong_types.length > 0
|
76
|
+
message = ""
|
77
|
+
message << " unpermitted parameters: #{unpermitted_params.join(',')}." if (unpermitted_params.length > 0)
|
78
|
+
message << " missing parameters: #{required_params.join(',')}." if (required_params.length > 0)
|
79
|
+
message << " parameters type mismatch: #{wrong_types.join(',')}" if (wrong_types.length > 0)
|
80
|
+
raise ActsAsBookable::OptionsInvalid.new(self, message)
|
81
|
+
end
|
82
|
+
|
83
|
+
#
|
84
|
+
# Convert options (Date to Time)
|
85
|
+
#
|
86
|
+
options[:time_start] = options[:time_start].to_time if options[:time_start].present?
|
87
|
+
options[:time_end] = options[:time_end].to_time if options[:time_end].present?
|
88
|
+
options[:time] = options[:time].to_time if options[:time].present?
|
89
|
+
|
90
|
+
# Return true if everything's ok
|
91
|
+
true
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
##
|
96
|
+
# Set the options
|
97
|
+
#
|
98
|
+
def set_options
|
99
|
+
# The default preset is 'room'
|
100
|
+
self.booking_opts[:preset]
|
101
|
+
|
102
|
+
defaults = nil
|
103
|
+
|
104
|
+
# Validates options
|
105
|
+
permitted_options = {
|
106
|
+
time_type: [:range, :fixed, :none],
|
107
|
+
capacity_type: [:open, :closed, :none],
|
108
|
+
preset: [:room,:event,:show],
|
109
|
+
bookable_across_occurrences: [true, false]
|
110
|
+
}
|
111
|
+
self.booking_opts.each_pair do |key, val|
|
112
|
+
if !permitted_options.has_key? key
|
113
|
+
raise ActsAsBookable::InitializationError.new(self, "#{key} is not a valid option")
|
114
|
+
elsif !permitted_options[key].include? val
|
115
|
+
raise ActsAsBookable::InitializationError.new(self, "#{val} is not a valid value for #{key}. Allowed values are: #{permitted_options[key]}")
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
case self.booking_opts[:preset]
|
120
|
+
# Room preset
|
121
|
+
when :room
|
122
|
+
defaults = {
|
123
|
+
time_type: :range, # time_start is check-in, time_end is check-out
|
124
|
+
capacity_type: :closed, # capacity is closed: after the first booking the room is not bookable anymore, even though the capacity has not been reached
|
125
|
+
bookable_across_occurrences: true # a room is bookable across recurrences: if a recurrence is daily, a booker must be able to book from a date to another date, even though time_start and time_end falls in different occurrences of the schedule
|
126
|
+
}
|
127
|
+
# Event preset (e.g. a birthday party)
|
128
|
+
when :event
|
129
|
+
defaults = {
|
130
|
+
time_type: :none, # time is ininfluent for booking an event.
|
131
|
+
capacity_type: :open, # capacity is open: after a booking the event is still bookable until capacity is reached.
|
132
|
+
bookable_across_occurrences: false # an event is not bookable across recurrences
|
133
|
+
}
|
134
|
+
# Show preset (e.g. a movie)
|
135
|
+
when :show
|
136
|
+
defaults = {
|
137
|
+
time_type: :fixed, # time is fixed: a user chooses the time of the show (the show may have a number of occurrences)
|
138
|
+
capacity_type: :open, # capacity is open: after a booking the show is still bookable until capacity is reached
|
139
|
+
bookable_across_occurrences: false # a show is not bookable across recurrences
|
140
|
+
}
|
141
|
+
else
|
142
|
+
defaults = {
|
143
|
+
time_type: :none,
|
144
|
+
capacity_type: :none,
|
145
|
+
bookable_across_occurrences: false
|
146
|
+
}
|
147
|
+
end
|
148
|
+
|
149
|
+
# Merge options with defaults
|
150
|
+
self.booking_opts.reverse_merge!(defaults)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
module InstanceMethods
|
155
|
+
##
|
156
|
+
# Check availability of current bookable, raising an error if the bookable is not available
|
157
|
+
#
|
158
|
+
# @param opts The booking options
|
159
|
+
# @return true if the bookable is available for given options
|
160
|
+
# @raise ActsAsBookable::AvailabilityError if the bookable is not available for given options
|
161
|
+
#
|
162
|
+
# Example:
|
163
|
+
# @room.check_availability!(from: Date.today, to: Date.tomorrow, amount: 2)
|
164
|
+
def check_availability!(opts)
|
165
|
+
# Capacity check (done first because it doesn't require additional queries)
|
166
|
+
if self.booking_opts[:capacity_type] != :none
|
167
|
+
# Amount > capacity
|
168
|
+
if opts[:amount] > self.capacity
|
169
|
+
raise ActsAsBookable::AvailabilityError.new ActsAsBookable::T.er('.availability.amount_gt_capacity', model: self.class.to_s)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
##
|
174
|
+
# Time check
|
175
|
+
#
|
176
|
+
if self.booking_opts[:time_type] == :range
|
177
|
+
time_check_ok = true
|
178
|
+
# If it's bookable across recurrences, just check start time and end time
|
179
|
+
if self.booking_opts[:bookable_across_occurrences]
|
180
|
+
# Check start time
|
181
|
+
if !(ActsAsBookable::TimeUtils.time_in_schedule?(self.schedule, opts[:time_start]))
|
182
|
+
time_check_ok = false
|
183
|
+
end
|
184
|
+
# Check end time
|
185
|
+
if !(ActsAsBookable::TimeUtils.time_in_schedule?(self.schedule, opts[:time_end]))
|
186
|
+
time_check_ok = false
|
187
|
+
end
|
188
|
+
# If it's not bookable across recurrences, check if the whole interval is included in an occurrence
|
189
|
+
else
|
190
|
+
# Check the whole interval
|
191
|
+
if !(ActsAsBookable::TimeUtils.interval_in_schedule?(self.schedule, opts[:time_start], opts[:time_end]))
|
192
|
+
time_check_ok = false
|
193
|
+
end
|
194
|
+
end
|
195
|
+
# If something went wrong
|
196
|
+
unless time_check_ok
|
197
|
+
raise ActsAsBookable::AvailabilityError.new ActsAsBookable::T.er('.availability.unavailable_interval', model: self.class.to_s, time_start: opts[:time_start], time_end: opts[:time_end])
|
198
|
+
end
|
199
|
+
end
|
200
|
+
if self.booking_opts[:time_type] == :fixed
|
201
|
+
if !(ActsAsBookable::TimeUtils.time_in_schedule?(self.schedule, opts[:time]))
|
202
|
+
raise ActsAsBookable::AvailabilityError.new ActsAsBookable::T.er('.availability.unavailable_time', model: self.class.to_s, time: opts[:time])
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
##
|
207
|
+
# Real capacity check (calculated with overlapped bookings)
|
208
|
+
#
|
209
|
+
overlapped = ActsAsBookable::Booking.overlapped(self, opts)
|
210
|
+
# If capacity_type is :closed cannot book if already booked (no matter if amount < capacity)
|
211
|
+
if (self.booking_opts[:capacity_type] == :closed && !overlapped.empty?)
|
212
|
+
raise ActsAsBookable::AvailabilityError.new ActsAsBookable::T.er('.availability.already_booked', model: self.class.to_s)
|
213
|
+
end
|
214
|
+
# if capacity_type is :open, check if amount <= maximum amount of overlapped booking
|
215
|
+
if (self.booking_opts[:capacity_type] == :open && !overlapped.empty?)
|
216
|
+
# if time_type is :range, split in sub-intervals and check the maximum sum of amounts against capacity for each sub-interval
|
217
|
+
if (self.booking_opts[:time_type] == :range)
|
218
|
+
# Map overlapped bookings to a set of intervals with amount
|
219
|
+
intervals = overlapped.map { |e| {time_start: e.time_start, time_end: e.time_end, amount: e.amount} }
|
220
|
+
# Make subintervals from overlapped bookings and check capacity for each of them
|
221
|
+
ActsAsBookable::TimeUtils.subintervals(intervals) do |a,b,op|
|
222
|
+
case op
|
223
|
+
when :open
|
224
|
+
res = {amount: a[:amount] + b[:amount]}
|
225
|
+
when :close
|
226
|
+
res = {amount: a[:amount] - b[:amount]}
|
227
|
+
end
|
228
|
+
raise ActsAsBookable::AvailabilityError.new ActsAsBookable::T.er('.availability.already_booked', model: self.class.to_s) if (res[:amount] > self.capacity)
|
229
|
+
res
|
230
|
+
end
|
231
|
+
# else, just sum the amounts (fixed times are not intervals and they overlap if are the same)
|
232
|
+
else
|
233
|
+
if(overlapped.sum(:amount) + opts[:amount] > self.capacity)
|
234
|
+
raise ActsAsBookable::AvailabilityError.new ActsAsBookable::T.er('.availability.already_booked', model: self.class.to_s)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
true
|
239
|
+
end
|
240
|
+
|
241
|
+
##
|
242
|
+
# Check availability of current bookable
|
243
|
+
#
|
244
|
+
# @param opts The booking options
|
245
|
+
# @return true if the bookable is available for given options, otherwise return false
|
246
|
+
#
|
247
|
+
# Example:
|
248
|
+
# @room.check_availability!(from: Date.today, to: Date.tomorrow, amount: 2)
|
249
|
+
def check_availability(opts)
|
250
|
+
begin
|
251
|
+
check_availability!(opts)
|
252
|
+
rescue ActsAsBookable::AvailabilityError
|
253
|
+
false
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
##
|
258
|
+
# Accept a booking by a booker. This is an alias method,
|
259
|
+
# equivalent to @booker.book!(@bookable, opts)
|
260
|
+
#
|
261
|
+
# @param booker The booker model
|
262
|
+
# @param opts The booking options
|
263
|
+
#
|
264
|
+
# Example:
|
265
|
+
# @room.be_booked!(@user, from: Date.today, to: Date.tomorrow, amount: 2)
|
266
|
+
def be_booked!(booker, opts={})
|
267
|
+
booker.book!(self, opts)
|
268
|
+
end
|
269
|
+
|
270
|
+
##
|
271
|
+
# Check if options passed for booking this Bookable are valid
|
272
|
+
#
|
273
|
+
# @raise ActsAsBookable::OptionsInvalid if options are not valid
|
274
|
+
# @param opts The booking options
|
275
|
+
#
|
276
|
+
def validate_booking_options!(opts)
|
277
|
+
self.validate_booking_options!(opts)
|
278
|
+
end
|
279
|
+
|
280
|
+
def booker?
|
281
|
+
self.class.booker?
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module ActsAsBookable
|
2
|
+
module Bookable
|
3
|
+
|
4
|
+
def bookable?
|
5
|
+
false
|
6
|
+
end
|
7
|
+
|
8
|
+
##
|
9
|
+
# Make a model bookable
|
10
|
+
#
|
11
|
+
# Example:
|
12
|
+
# class Room < ActiveRecord::Base
|
13
|
+
# acts_as_bookable
|
14
|
+
# end
|
15
|
+
def acts_as_bookable(options={})
|
16
|
+
bookable(options)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
# Make a model bookable
|
22
|
+
def bookable(options)
|
23
|
+
|
24
|
+
if bookable?
|
25
|
+
self.booking_opts = options
|
26
|
+
else
|
27
|
+
class_attribute :booking_opts
|
28
|
+
self.booking_opts = options
|
29
|
+
|
30
|
+
class_eval do
|
31
|
+
serialize :schedule, IceCube::Schedule
|
32
|
+
|
33
|
+
has_many :bookings, as: :bookable, dependent: :destroy, class_name: '::ActsAsBookable::Booking'
|
34
|
+
|
35
|
+
validates_presence_of :schedule, if: :schedule_required?
|
36
|
+
validates_presence_of :capacity, if: :capacity_required?
|
37
|
+
validates_numericality_of :capacity, if: :capacity_required?, only_integer: true, greater_than_or_equal_to: 0
|
38
|
+
|
39
|
+
def self.bookable?
|
40
|
+
true
|
41
|
+
end
|
42
|
+
|
43
|
+
def schedule_required?
|
44
|
+
self.booking_opts && self.booking_opts && self.booking_opts[:time_type] != :none
|
45
|
+
end
|
46
|
+
|
47
|
+
def capacity_required?
|
48
|
+
self.booking_opts && self.booking_opts[:capacity_type] != :none
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
include Core
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module ActsAsBookable
|
2
|
+
module Booker
|
3
|
+
def self.included(base)
|
4
|
+
base.extend ClassMethods
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
##
|
9
|
+
# Make a model a booker. This allows an instance of a model to claim ownership
|
10
|
+
# of bookings.
|
11
|
+
#
|
12
|
+
# Example:
|
13
|
+
# class User < ActiveRecord::Base
|
14
|
+
# acts_as_booker
|
15
|
+
# end
|
16
|
+
def acts_as_booker(opts={})
|
17
|
+
class_eval do
|
18
|
+
has_many :bookings, as: :booker, dependent: :destroy, class_name: '::ActsAsBookable::Booking'
|
19
|
+
end
|
20
|
+
|
21
|
+
include ActsAsBookable::Booker::InstanceMethods
|
22
|
+
extend ActsAsBookable::Booker::SingletonMethods
|
23
|
+
end
|
24
|
+
|
25
|
+
def booker?
|
26
|
+
false
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
module InstanceMethods
|
31
|
+
##
|
32
|
+
# Book a bookable model
|
33
|
+
#
|
34
|
+
# @param bookable The resource that will be booked
|
35
|
+
# @return The booking created
|
36
|
+
# @raise ActsAsBookable::OptionsInvalid if opts are not valid for given bookable
|
37
|
+
# @raise ActsAsBookable::AvailabilityError if the bookable is not available for given options
|
38
|
+
# @raise ActiveRecord::RecordInvalid if trying to create an invalid booking
|
39
|
+
#
|
40
|
+
# Example:
|
41
|
+
# @user.book!(@room)
|
42
|
+
def book!(bookable, opts={})
|
43
|
+
# validates options
|
44
|
+
bookable.class.validate_booking_options!(opts) if bookable.class.bookable?
|
45
|
+
|
46
|
+
# check availability
|
47
|
+
bookable.check_availability!(opts) if bookable.class.bookable?
|
48
|
+
|
49
|
+
# create the new booking
|
50
|
+
booking_params = opts.merge({booker: self, bookable: bookable})
|
51
|
+
booking = ActsAsBookable::Booking.create!(booking_params)
|
52
|
+
|
53
|
+
# reload the bookable to make changes available
|
54
|
+
bookable.reload
|
55
|
+
self.reload
|
56
|
+
booking
|
57
|
+
end
|
58
|
+
|
59
|
+
def booker?
|
60
|
+
self.class.booker?
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
module SingletonMethods
|
65
|
+
def booker?
|
66
|
+
true
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module ActsAsBookable
|
2
|
+
##
|
3
|
+
# Booking model. Store in database bookings made by bookers on bookables
|
4
|
+
#
|
5
|
+
class Booking < ::ActiveRecord::Base
|
6
|
+
self.table_name = 'acts_as_bookable_bookings'
|
7
|
+
|
8
|
+
belongs_to :bookable, polymorphic: true
|
9
|
+
belongs_to :booker, polymorphic: true
|
10
|
+
|
11
|
+
validates_presence_of :bookable
|
12
|
+
validates_presence_of :booker
|
13
|
+
validate :bookable_must_be_bookable,
|
14
|
+
:booker_must_be_booker
|
15
|
+
|
16
|
+
##
|
17
|
+
# Retrieves overlapped bookings, given a bookable and some booking options
|
18
|
+
#
|
19
|
+
scope :overlapped, ->(bookable,opts) {
|
20
|
+
query = where(bookable_id: bookable.id)
|
21
|
+
|
22
|
+
# Time options
|
23
|
+
if(opts[:time].present?)
|
24
|
+
query = query.where(time: opts[:time].to_time)
|
25
|
+
end
|
26
|
+
if(opts[:time_start].present?)
|
27
|
+
query = query.where('time_end >= ?', opts[:time_start].to_time)
|
28
|
+
end
|
29
|
+
if(opts[:time_end].present?)
|
30
|
+
query = query.where('time_start < ?', opts[:time_end].to_time)
|
31
|
+
end
|
32
|
+
query
|
33
|
+
}
|
34
|
+
|
35
|
+
private
|
36
|
+
##
|
37
|
+
# Validation method. Check if the bookable resource is actually bookable
|
38
|
+
#
|
39
|
+
def bookable_must_be_bookable
|
40
|
+
if bookable.present? && !bookable.class.bookable?
|
41
|
+
errors.add(:bookable, T.er('booking.bookable_must_be_bookable', model: bookable.class.to_s))
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Validation method. Check if the booker model is actually a booker
|
47
|
+
#
|
48
|
+
def booker_must_be_booker
|
49
|
+
if booker.present? && !booker.class.booker?
|
50
|
+
errors.add(:booker, T.er('booking.booker_must_be_booker', model: booker.class.to_s))
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module ActsAsBookable
|
2
|
+
module DBUtils
|
3
|
+
class << self
|
4
|
+
def connection
|
5
|
+
ActsAsBookable::Booking.connection
|
6
|
+
end
|
7
|
+
|
8
|
+
def using_postgresql?
|
9
|
+
connection && connection.adapter_name == 'PostgreSQL'
|
10
|
+
end
|
11
|
+
|
12
|
+
def using_mysql?
|
13
|
+
#We should probably use regex for mysql to support prehistoric adapters
|
14
|
+
connection && connection.adapter_name == 'Mysql2'
|
15
|
+
end
|
16
|
+
|
17
|
+
def using_sqlite?
|
18
|
+
connection && connection.adapter_name == 'SQLite'
|
19
|
+
end
|
20
|
+
|
21
|
+
def active_record4?
|
22
|
+
::ActiveRecord::VERSION::MAJOR == 4
|
23
|
+
end
|
24
|
+
|
25
|
+
def active_record5?
|
26
|
+
::ActiveRecord::VERSION::MAJOR == 5
|
27
|
+
end
|
28
|
+
|
29
|
+
def like_operator
|
30
|
+
using_postgresql? ? 'ILIKE' : 'LIKE'
|
31
|
+
end
|
32
|
+
|
33
|
+
# escape _ and % characters in strings, since these are wildcards in SQL.
|
34
|
+
def escape_like(str)
|
35
|
+
str.gsub(/[!%_]/) { |x| '!' + x }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|