acts_as_bookable 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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