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.
Files changed (66) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +2 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +43 -0
  6. data/Appraisals +21 -0
  7. data/Gemfile +21 -0
  8. data/Gemfile.lock +168 -0
  9. data/Guardfile +5 -0
  10. data/MIT-LICENSE +20 -0
  11. data/README.md +473 -0
  12. data/Rakefile +19 -0
  13. data/acts_as_bookable.gemspec +41 -0
  14. data/app/assets/images/acts_as_bookable/.keep +0 -0
  15. data/app/assets/javascripts/acts_as_bookable/application.js +13 -0
  16. data/app/assets/stylesheets/acts_as_bookable/application.css +15 -0
  17. data/app/controllers/acts_as_bookable/application_controller.rb +4 -0
  18. data/app/helpers/acts_as_bookable/application_helper.rb +4 -0
  19. data/app/views/layouts/acts_as_bookable/application.html.erb +14 -0
  20. data/bin/rails +12 -0
  21. data/config/locales/en.yml +12 -0
  22. data/config/routes.rb +2 -0
  23. data/db/migrate/20160217085200_create_acts_as_bookable_bookings.rb +14 -0
  24. data/gemfiles/activerecord_3.2.gemfile +16 -0
  25. data/gemfiles/activerecord_4.0.gemfile +16 -0
  26. data/gemfiles/activerecord_4.1.gemfile +16 -0
  27. data/gemfiles/activerecord_4.2.gemfile +17 -0
  28. data/gemfiles/activerecord_5.0.gemfile +17 -0
  29. data/lib/acts_as_bookable/bookable/core.rb +285 -0
  30. data/lib/acts_as_bookable/bookable.rb +56 -0
  31. data/lib/acts_as_bookable/booker.rb +70 -0
  32. data/lib/acts_as_bookable/booking.rb +54 -0
  33. data/lib/acts_as_bookable/db_utils.rb +39 -0
  34. data/lib/acts_as_bookable/engine.rb +5 -0
  35. data/lib/acts_as_bookable/t.rb +11 -0
  36. data/lib/acts_as_bookable/time_utils.rb +135 -0
  37. data/lib/acts_as_bookable/version.rb +3 -0
  38. data/lib/acts_as_bookable.rb +42 -0
  39. data/lib/tasks/acts_as_bookable_tasks.rake +4 -0
  40. data/spec/acts_as_bookable/acts_as_bookable_spec.rb +52 -0
  41. data/spec/acts_as_bookable/acts_as_booker_spec.rb +57 -0
  42. data/spec/acts_as_bookable/bookable/core_spec.rb +593 -0
  43. data/spec/acts_as_bookable/bookable_spec.rb +124 -0
  44. data/spec/acts_as_bookable/booker_spec.rb +140 -0
  45. data/spec/acts_as_bookable/booking_spec.rb +241 -0
  46. data/spec/acts_as_bookable/schedule_spec.rb +137 -0
  47. data/spec/acts_as_bookable/time_utils_spec.rb +525 -0
  48. data/spec/factories/bookable.rb +7 -0
  49. data/spec/factories/booker.rb +5 -0
  50. data/spec/factories/room.rb +11 -0
  51. data/spec/internal/app/models/Bookable.rb +3 -0
  52. data/spec/internal/app/models/Booker.rb +3 -0
  53. data/spec/internal/app/models/Event.rb +3 -0
  54. data/spec/internal/app/models/Generic.rb +2 -0
  55. data/spec/internal/app/models/NotBooker.rb +2 -0
  56. data/spec/internal/app/models/Room.rb +3 -0
  57. data/spec/internal/app/models/Show.rb +3 -0
  58. data/spec/internal/app/models/Unbookable.rb +2 -0
  59. data/spec/internal/config/database.yml.sample +17 -0
  60. data/spec/internal/db/schema.rb +51 -0
  61. data/spec/spec_helper.rb +26 -0
  62. data/spec/support/0-helpers.rb +32 -0
  63. data/spec/support/1-database.rb +42 -0
  64. data/spec/support/2-database_cleaner.rb +21 -0
  65. data/spec/support/3-factory_girl.rb +12 -0
  66. 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
@@ -0,0 +1,5 @@
1
+ require 'rails/engine'
2
+ module ActsAsBookable
3
+ class Engine < ::Rails::Engine
4
+ end
5
+ end
@@ -0,0 +1,11 @@
1
+ module ActsAsBookable
2
+ module T
3
+ def self.t(message, opts={})
4
+ I18n.t('.acts_as_bookable.' + message, opts)
5
+ end
6
+
7
+ def self.er(message, opts={})
8
+ self.t('errors.messages.' + message, opts)
9
+ end
10
+ end
11
+ end