├── .coveralls.yml
├── .gitignore
├── .rspec
├── .ruby-version
├── .travis.yml
├── Appraisals
├── CHANGELOG.md
├── Gemfile
├── Gemfile.lock
├── Guardfile
├── MIT-LICENSE
├── README.md
├── Rakefile
├── acts_as_bookable.gemspec
├── app
├── assets
│ ├── images
│ │ └── acts_as_bookable
│ │ │ └── .keep
│ ├── javascripts
│ │ └── acts_as_bookable
│ │ │ └── application.js
│ └── stylesheets
│ │ └── acts_as_bookable
│ │ └── application.css
├── controllers
│ └── acts_as_bookable
│ │ └── application_controller.rb
├── helpers
│ └── acts_as_bookable
│ │ └── application_helper.rb
└── views
│ └── layouts
│ └── acts_as_bookable
│ └── application.html.erb
├── bin
└── rails
├── config
├── locales
│ └── en.yml
└── routes.rb
├── db
└── migrate
│ └── 20160217085200_create_acts_as_bookable_bookings.rb
├── gemfiles
├── activerecord_3.2.gemfile
├── activerecord_4.0.gemfile
├── activerecord_4.1.gemfile
├── activerecord_4.2.gemfile
└── activerecord_5.0.gemfile
├── lib
├── acts_as_bookable.rb
├── acts_as_bookable
│ ├── bookable.rb
│ ├── bookable
│ │ └── core.rb
│ ├── booker.rb
│ ├── booking.rb
│ ├── db_utils.rb
│ ├── engine.rb
│ ├── t.rb
│ ├── time_utils.rb
│ └── version.rb
└── tasks
│ └── acts_as_bookable_tasks.rake
└── spec
├── acts_as_bookable
├── acts_as_bookable_spec.rb
├── acts_as_booker_spec.rb
├── bookable
│ └── core_spec.rb
├── bookable_spec.rb
├── booker_spec.rb
├── booking_spec.rb
├── schedule_spec.rb
└── time_utils_spec.rb
├── factories
├── bookable.rb
├── booker.rb
└── room.rb
├── internal
├── app
│ └── models
│ │ ├── Bookable.rb
│ │ ├── Booker.rb
│ │ ├── Event.rb
│ │ ├── Generic.rb
│ │ ├── NotBooker.rb
│ │ ├── Room.rb
│ │ ├── Show.rb
│ │ └── Unbookable.rb
├── config
│ └── database.yml.sample
└── db
│ └── schema.rb
├── spec_helper.rb
└── support
├── 0-helpers.rb
├── 1-database.rb
├── 2-database_cleaner.rb
└── 3-factory_girl.rb
/.coveralls.yml:
--------------------------------------------------------------------------------
1 | service_name: travis-ci
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | *.sqlite3
3 | /pkg/*
4 | .bundle
5 | .ruby-version
6 | spec/internal/config/database.yml
7 | tmp*.sw?
8 | *.sw?
9 | tmp
10 | *.gem
11 | *.lock
12 | coverage
13 | docker-compose.yml
14 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 | --backtrace
3 |
--------------------------------------------------------------------------------
/.ruby-version:
--------------------------------------------------------------------------------
1 | 2.3.1
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 |
3 | rvm:
4 | - 2.3.1
5 | - 2.3.0
6 | - 2.2.2
7 | - 2.1
8 | - 2.0.0
9 | - rbx-2
10 |
11 | env:
12 | - DB=sqlite3
13 | - DB=mysql
14 | - DB=postgresql
15 |
16 | gemfile:
17 | - gemfiles/activerecord_3.2.gemfile
18 | - gemfiles/activerecord_4.0.gemfile
19 | - gemfiles/activerecord_4.1.gemfile
20 | - gemfiles/activerecord_4.2.gemfile
21 | - gemfiles/activerecord_5.0.gemfile
22 |
23 | sudo: false
24 |
25 | bundler_args: '--without local_development --jobs 3 --retry 3'
26 |
27 | script: bundle exec rake
28 |
29 | matrix:
30 | fast_finish: true
31 | allow_failures:
32 | - gemfile: gemfiles/activerecord_edge.gemfile
33 | - rvm: rbx-2
34 | exclude:
35 | - rvm: 2.0.0
36 | gemfile: gemfiles/activerecord_4.2.gemfile
37 | - rvm: 2.2.2
38 | gemfile: gemfiles/activerecord_3.2.gemfile
39 | - rvm: rbx-2
40 | gemfile: gemfiles/activerecord_3.2.gemfile
41 | - rvm: 2.1
42 | gemfile: gemfiles/activerecord_5.0.gemfile
43 | - rvm: 2.0.0
44 | gemfile: gemfiles/activerecord_5.0.gemfile
45 | - rvm: rbx-2
46 | gemfile: gemfiles/activerecord_5.0.gemfile
47 |
--------------------------------------------------------------------------------
/Appraisals:
--------------------------------------------------------------------------------
1 | appraise "activerecord-3.2" do
2 | gem "activerecord", github: "rails/rails" , branch: '3-2-stable'
3 | end
4 |
5 | appraise "activerecord-4.0" do
6 | gem "activerecord", github: "rails/rails" , branch: '4-0-stable'
7 | end
8 |
9 | appraise "activerecord-4.1" do
10 | gem "activerecord", github: "rails/rails" , branch: '4-1-stable'
11 | end
12 |
13 | appraise "activerecord-4.2" do
14 | gem "railties", github: "rails/rails" , branch: '4-2-stable'
15 | gem "activerecord", github: "rails/rails" , branch: '4-2-stable'
16 | end
17 |
18 | appraise "activerecord-5.0" do
19 | gem "railties", github: "rails/rails" , branch: '5-0-stable'
20 | gem "activerecord", github: "rails/rails" , branch: '5-0-stable'
21 | end
22 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | ## [v0.1.4](https://github.com/tandusrl/acts_as_bookable/tree/v0.1.4) (2017-01-31)
4 | [Full Changelog](https://github.com/tandusrl/acts_as_bookable/compare/v0.1.3...v0.1.4)
5 |
6 | **Closed issues:**
7 |
8 | - Not compatible with Rails 5/activerecord [\#14](https://github.com/tandusrl/acts_as_bookable/issues/14)
9 |
10 | ## [v0.1.3](https://github.com/tandusrl/acts_as_bookable/tree/v0.1.3) (2016-11-22)
11 | [Full Changelog](https://github.com/tandusrl/acts_as_bookable/compare/v0.1.2...v0.1.3)
12 |
13 | - @bookable.schedule method missing [\#18](https://github.com/tandusrl/acts_as_bookable/issues/18)
14 | - what data does @user.bookings return? [\#17](https://github.com/tandusrl/acts_as_bookable/issues/17)
15 |
16 | ## [v0.1.2](https://github.com/tandusrl/acts_as_bookable/tree/v0.1.2) (2016-02-22)
17 | [Full Changelog](https://github.com/tandusrl/acts_as_bookable/compare/v0.1.1...v0.1.2)
18 |
19 | **Fixed bugs:**
20 |
21 | - Fix dependencies [\#11](https://github.com/tandusrl/acts_as_bookable/issues/11)
22 | - Fix build failing for some DBs [\#10](https://github.com/tandusrl/acts_as_bookable/issues/10)
23 |
24 | ## [v0.1.1](https://github.com/tandusrl/acts_as_bookable/tree/v0.1.1) (2016-02-22)
25 | [Full Changelog](https://github.com/tandusrl/acts_as_bookable/compare/v0.1.0...v0.1.1)
26 |
27 | ## [v0.1.0](https://github.com/tandusrl/acts_as_bookable/tree/v0.1.0) (2016-02-17)
28 | **Implemented enhancements:**
29 |
30 | - Remove location\_type: :range [\#1](https://github.com/tandusrl/acts_as_bookable/issues/1)
31 |
32 | **Closed issues:**
33 |
34 | - Add an option to allow bookings that start in an occurrence and end in another [\#8](https://github.com/tandusrl/acts_as_bookable/issues/8)
35 | - Move ice\_cube gem from Gemfile to gemspec [\#7](https://github.com/tandusrl/acts_as_bookable/issues/7)
36 | - Implement booking with date costraints [\#5](https://github.com/tandusrl/acts_as_bookable/issues/5)
37 | - Merge date and time [\#4](https://github.com/tandusrl/acts_as_bookable/issues/4)
38 | - Create a README [\#3](https://github.com/tandusrl/acts_as_bookable/issues/3)
39 |
40 |
41 |
42 | \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)*
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | # Declare your gem's dependencies in acts_as_bookable.gemspec.
4 | # Bundler will treat runtime dependencies like base dependencies, and
5 | # development dependencies will be added by default to the :development group.
6 | gemspec
7 |
8 | # Declare any dependencies that are still in development here instead of in
9 | # your gemspec. These might include edge Rails or gems from your path or
10 | # Git. Remember to move these dependencies to your gemspec before releasing
11 | # your gem to rubygems.org.
12 |
13 | # To use a debugger
14 | group :local_development do
15 | gem 'guard'
16 | gem 'guard-rspec'
17 | gem 'appraisal'
18 | gem 'byebug' , platform: :mri_21
19 | gem 'pry-nav'
20 | end
21 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: .
3 | specs:
4 | acts_as_bookable (0.1.3)
5 | activerecord (>= 3.2, < 5.1)
6 | ice_cube_chosko (~> 0.1.0)
7 |
8 | GEM
9 | remote: https://rubygems.org/
10 | specs:
11 | actioncable (5.0.1)
12 | actionpack (= 5.0.1)
13 | nio4r (~> 1.2)
14 | websocket-driver (~> 0.6.1)
15 | actionmailer (5.0.1)
16 | actionpack (= 5.0.1)
17 | actionview (= 5.0.1)
18 | activejob (= 5.0.1)
19 | mail (~> 2.5, >= 2.5.4)
20 | rails-dom-testing (~> 2.0)
21 | actionpack (5.0.1)
22 | actionview (= 5.0.1)
23 | activesupport (= 5.0.1)
24 | rack (~> 2.0)
25 | rack-test (~> 0.6.3)
26 | rails-dom-testing (~> 2.0)
27 | rails-html-sanitizer (~> 1.0, >= 1.0.2)
28 | actionview (5.0.1)
29 | activesupport (= 5.0.1)
30 | builder (~> 3.1)
31 | erubis (~> 2.7.0)
32 | rails-dom-testing (~> 2.0)
33 | rails-html-sanitizer (~> 1.0, >= 1.0.2)
34 | activejob (5.0.1)
35 | activesupport (= 5.0.1)
36 | globalid (>= 0.3.6)
37 | activemodel (5.0.1)
38 | activesupport (= 5.0.1)
39 | activerecord (5.0.1)
40 | activemodel (= 5.0.1)
41 | activesupport (= 5.0.1)
42 | arel (~> 7.0)
43 | activesupport (5.0.1)
44 | concurrent-ruby (~> 1.0, >= 1.0.2)
45 | i18n (~> 0.7)
46 | minitest (~> 5.1)
47 | tzinfo (~> 1.1)
48 | appraisal (2.1.0)
49 | bundler
50 | rake
51 | thor (>= 0.14.0)
52 | arel (7.1.4)
53 | awesome_print (1.7.0)
54 | barrier (1.0.2)
55 | builder (3.2.3)
56 | byebug (9.0.6)
57 | coderay (1.1.1)
58 | concurrent-ruby (1.0.4)
59 | coveralls (0.8.19)
60 | json (>= 1.8, < 3)
61 | simplecov (~> 0.12.0)
62 | term-ansicolor (~> 1.3)
63 | thor (~> 0.19.1)
64 | tins (~> 1.6)
65 | database_cleaner (1.5.3)
66 | diff-lcs (1.3)
67 | docile (1.1.5)
68 | erubis (2.7.0)
69 | factory_girl (4.8.0)
70 | activesupport (>= 3.0.0)
71 | factory_girl_rails (4.8.0)
72 | factory_girl (~> 4.8.0)
73 | railties (>= 3.0.0)
74 | ffi (1.9.17)
75 | formatador (0.2.5)
76 | globalid (0.3.7)
77 | activesupport (>= 4.1.0)
78 | guard (2.14.1)
79 | formatador (>= 0.2.4)
80 | listen (>= 2.7, < 4.0)
81 | lumberjack (~> 1.0)
82 | nenv (~> 0.1)
83 | notiffany (~> 0.0)
84 | pry (>= 0.9.12)
85 | shellany (~> 0.0)
86 | thor (>= 0.18.1)
87 | guard-compat (1.2.1)
88 | guard-rspec (4.7.3)
89 | guard (~> 2.1)
90 | guard-compat (~> 1.1)
91 | rspec (>= 2.99.0, < 4.0)
92 | i18n (0.7.0)
93 | ice_cube_chosko (0.1.0)
94 | json (2.0.3)
95 | listen (3.1.5)
96 | rb-fsevent (~> 0.9, >= 0.9.4)
97 | rb-inotify (~> 0.9, >= 0.9.7)
98 | ruby_dep (~> 1.2)
99 | loofah (2.0.3)
100 | nokogiri (>= 1.5.9)
101 | lumberjack (1.0.11)
102 | mail (2.6.4)
103 | mime-types (>= 1.16, < 4)
104 | method_source (0.8.2)
105 | mime-types (3.1)
106 | mime-types-data (~> 3.2015)
107 | mime-types-data (3.2016.0521)
108 | mini_portile2 (2.1.0)
109 | minitest (5.10.1)
110 | mysql2 (0.3.21)
111 | nenv (0.3.0)
112 | nio4r (1.2.1)
113 | nokogiri (1.7.0.1)
114 | mini_portile2 (~> 2.1.0)
115 | notiffany (0.1.1)
116 | nenv (~> 0.1)
117 | shellany (~> 0.0)
118 | pg (0.19.0)
119 | pry (0.10.4)
120 | coderay (~> 1.1.0)
121 | method_source (~> 0.8.1)
122 | slop (~> 3.4)
123 | pry-nav (0.2.4)
124 | pry (>= 0.9.10, < 0.11.0)
125 | rack (2.0.1)
126 | rack-test (0.6.3)
127 | rack (>= 1.0)
128 | rails (5.0.1)
129 | actioncable (= 5.0.1)
130 | actionmailer (= 5.0.1)
131 | actionpack (= 5.0.1)
132 | actionview (= 5.0.1)
133 | activejob (= 5.0.1)
134 | activemodel (= 5.0.1)
135 | activerecord (= 5.0.1)
136 | activesupport (= 5.0.1)
137 | bundler (>= 1.3.0, < 2.0)
138 | railties (= 5.0.1)
139 | sprockets-rails (>= 2.0.0)
140 | rails-dom-testing (2.0.2)
141 | activesupport (>= 4.2.0, < 6.0)
142 | nokogiri (~> 1.6)
143 | rails-html-sanitizer (1.0.3)
144 | loofah (~> 2.0)
145 | railties (5.0.1)
146 | actionpack (= 5.0.1)
147 | activesupport (= 5.0.1)
148 | method_source
149 | rake (>= 0.8.7)
150 | thor (>= 0.18.1, < 2.0)
151 | rake (12.0.0)
152 | rb-fsevent (0.9.8)
153 | rb-inotify (0.9.8)
154 | ffi (>= 0.5.0)
155 | rspec (3.5.0)
156 | rspec-core (~> 3.5.0)
157 | rspec-expectations (~> 3.5.0)
158 | rspec-mocks (~> 3.5.0)
159 | rspec-core (3.5.4)
160 | rspec-support (~> 3.5.0)
161 | rspec-expectations (3.5.0)
162 | diff-lcs (>= 1.2.0, < 2.0)
163 | rspec-support (~> 3.5.0)
164 | rspec-mocks (3.5.0)
165 | diff-lcs (>= 1.2.0, < 2.0)
166 | rspec-support (~> 3.5.0)
167 | rspec-rails (3.5.2)
168 | actionpack (>= 3.0)
169 | activesupport (>= 3.0)
170 | railties (>= 3.0)
171 | rspec-core (~> 3.5.0)
172 | rspec-expectations (~> 3.5.0)
173 | rspec-mocks (~> 3.5.0)
174 | rspec-support (~> 3.5.0)
175 | rspec-support (3.5.0)
176 | ruby_dep (1.5.0)
177 | shellany (0.0.1)
178 | simplecov (0.12.0)
179 | docile (~> 1.1.0)
180 | json (>= 1.8, < 3)
181 | simplecov-html (~> 0.10.0)
182 | simplecov-html (0.10.0)
183 | slop (3.6.0)
184 | sprockets (3.7.1)
185 | concurrent-ruby (~> 1.0)
186 | rack (> 1, < 3)
187 | sprockets-rails (3.2.0)
188 | actionpack (>= 4.0)
189 | activesupport (>= 4.0)
190 | sprockets (>= 3.0.0)
191 | sqlite3 (1.3.13)
192 | term-ansicolor (1.4.0)
193 | tins (~> 1.0)
194 | thor (0.19.4)
195 | thread_safe (0.3.5)
196 | tins (1.13.0)
197 | tzinfo (1.2.2)
198 | thread_safe (~> 0.1)
199 | websocket-driver (0.6.5)
200 | websocket-extensions (>= 0.1.0)
201 | websocket-extensions (0.1.2)
202 |
203 | PLATFORMS
204 | ruby
205 |
206 | DEPENDENCIES
207 | acts_as_bookable!
208 | appraisal
209 | awesome_print (~> 1.6)
210 | barrier (~> 1.0)
211 | byebug
212 | coveralls (~> 0.8)
213 | database_cleaner (~> 1.5)
214 | factory_girl_rails (~> 4.6)
215 | guard
216 | guard-rspec
217 | mysql2 (~> 0.3.7)
218 | pg (~> 0.18)
219 | pry-nav
220 | rails
221 | rake
222 | rspec (~> 3)
223 | rspec-rails (~> 3)
224 | sqlite3 (~> 1.3)
225 |
226 | BUNDLED WITH
227 | 1.14.3
228 |
--------------------------------------------------------------------------------
/Guardfile:
--------------------------------------------------------------------------------
1 | guard :rspec, cmd: "bundle exec rspec" do
2 | watch(%r{^spec/.+_spec\.rb})
3 | watch(%r{^lib/(.+)\.rb}) { |m| "spec/#{m[1]}_spec.rb" }
4 | watch('spec/spec_helper.rb') { "spec" }
5 | end
6 |
--------------------------------------------------------------------------------
/MIT-LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2016 Tandù srl
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ActsAsBookable
2 |
3 | [](http://badge.fury.io/rb/acts_as_bookable)
4 | [](http://travis-ci.org/tandusrl/acts_as_bookable)
5 | [](https://codeclimate.com/github/tandusrl/acts_as_bookable)
6 | [](https://coveralls.io/github/tandusrl/acts_as_bookable?branch=master)
7 | [](http://inch-ci.org/github/tandusrl/acts_as_bookable)
8 |
9 | ActsAsBookable allows resources to be booked by users. It:
10 |
11 | * Is a MVC solution based on Rails engines
12 | * Is designed to cover many use cases (hotels bookings, restaurant reservations, shows...)
13 | * Allows to define bookable availabilities with recurring times and exceptions (based on ice_cube)
14 |
15 | ## Getting started
16 |
17 | ### Installation
18 |
19 | #### Include the gem
20 |
21 | ActsAsBookable works with ActiveRecord 3.2 onwards. You can add it to your Gemfile with:
22 |
23 | ```ruby
24 | gem 'acts_as_bookable'
25 | ```
26 |
27 | run `bundle install` to install it.
28 |
29 | #### Install and run migrations
30 |
31 | ```bash
32 | bundle exec rake acts_as_bookable_engine:install:migrations
33 | bundle exec rake db:migrate
34 | ```
35 |
36 | ### Bookables, Bookers and Bookings
37 |
38 | To set-up a **Bookable** model, use `acts_as_bookable`. A Bookable model is enabled to accept bookings.
39 |
40 | ```ruby
41 | class Room < ActiveRecord::Base
42 | acts_as_bookable
43 | end
44 | ```
45 |
46 | To set-up a **Booker** model, use `acts_as_booker`. Only Bookers can create bookings.
47 |
48 | ```ruby
49 | class User < ActiveRecord::Base
50 | acts_as_booker
51 | end
52 | ```
53 |
54 | From this time on, a User can book a Room with
55 |
56 | ```ruby
57 | @user.book! @room
58 | ```
59 |
60 | Or a Room can accept a booking from a User with
61 |
62 | ```ruby
63 | @room.be_booked! @user
64 | ```
65 |
66 | The functions above perform the same operation: they create and save a new **Booking** that has relations with the **Booker** and the **Bookable**.
67 |
68 | Since only **Bookers** can book **Bookables**, you must configure both the models. You can even have two or more models configured as **Bookable**, as well as two or more models configured as **Booker**.
69 |
70 | You can access bookings both from the Bookable and the Booker
71 |
72 | ```ruby
73 | @room.bookings # return all bookings created on this room
74 | @user.bookings # return all bookings made by this user
75 | ```
76 |
77 | ## Configuring ActsAsBookable options
78 |
79 | There are a number available options to make your models behave differently. They are all configurable in the Bookable model, passing a hash to `acts_as_bookable`
80 |
81 | Available options (with values) are:
82 |
83 | * `:time_type`: Specifies how the Bookable must be booked in terms of time. Allowed values are:
84 | * `:none`
85 | * `:fixed`
86 | * `:range`
87 | * `:capacity_type`: Specifies how the `amount` of a booking (e.g. number of people of a restaurant reservation) affects the future availability of the bookable. Allowed values are:
88 | * `:none`
89 | * `:open`
90 | * `:closed`
91 | * `:bookable_across_occurrences`: Allows or denies the possibility to book across different occurrences of the availability schedule of the bookable (further explanation below)
92 |
93 | > WARNING - Some of the options above need migrations. They are explained in the sections below
94 |
95 | ### No constraints
96 |
97 | The model accepts booking without any constraint. This means every booker can create an infinite number of bookings on it and no capacity or time checks are performed.
98 |
99 | Creating a booking on this model means booking it forever and without care for other existing bookings. In other words, the time and the number of bookings do not affect the availability of this bookable. (e.g. pre-ordering a product that will be released soon)
100 |
101 | **Configuration**
102 |
103 | ```ruby
104 | class Product < ActiveRecord::Base
105 | acts_as_bookable
106 | end
107 | ```
108 |
109 | **Creating a new bookable**
110 |
111 | ```ruby
112 | # Creating a new bookable without constraints does not require any additional attribute
113 | @product = Product.create!(...)
114 | ```
115 |
116 | **Booking**
117 |
118 | ```ruby
119 | # Booking a model without constraints does not require any additional option
120 | @user.book! @product
121 | ```
122 |
123 | ### Time constraints
124 |
125 | The option `time_type` may be used to set a constraint over the booking time.
126 |
127 | #### No time constraints - `time_type: :none`
128 |
129 | The model is bookable without time constraints.
130 |
131 |
132 | ```ruby
133 | class Product < ActiveRecord::Base
134 | # As `time_type: :none` is a default, you can omit it. It's shown here for explanation purposes
135 | acts_as_bookable time_type: :none
136 | end
137 | ```
138 |
139 | #### Fixed time constraint - `time_type: :fixed`
140 |
141 | > WARNING - **migration needed!** - with this option the model must have an attribute `schedule: :text`
142 |
143 | The model accepts bookings that specify a fixed `:time`, and the availability is affected only for that time. (e.g. a show in a movie theater)
144 |
145 | **Configuration**
146 |
147 | ```ruby
148 | class Show < ActiveRecord::Base
149 | acts_as_bookable time_type: :fixed
150 | end
151 | ```
152 |
153 | **Creating a new bookable**
154 |
155 | Each instance of the model must define its availability in terms of time with an [IceCube Schedule](https://github.com/seejohnrun/ice_cube)
156 |
157 | ```ruby
158 | @show = Show.new(...)
159 | @show.schedule = IceCube::Schedule.new
160 | # This show is available every day at 6PM and 10PM
161 | @show.schedule.add_recurrence_rule IceCube::Rule.daily.hour_of_day(18,22)
162 | @show.save!
163 | ```
164 |
165 | **Booking**
166 |
167 | ```ruby
168 | time_ok = Date.today + 18.hours # Today at 6PM
169 | time_wrong = Date.today + 19.hours # Today at 7PM
170 | # Booking a model with `time_type: :fixed` requires a `:time` option
171 | @user1.book! @show, time: time_ok # OK
172 | @user2.book! @show, time: time_wrong # raise ActsAsBookable::AvailabilityError
173 | ```
174 |
175 | #### Time range constraint - `time_type: :range`
176 |
177 | > WARNING - **migration needed!** - with this option the model must have an attribute `schedule: :text`
178 |
179 | The model accepts bookings that specify a `:time_start` and a `:time_end`. After a booking is created, the bookable availability is affected only within that range. (e.g. a meeting room)
180 |
181 | **Configuration**
182 |
183 | ```ruby
184 | class MeetingRoom < ActiveRecord::Base
185 | acts_as_bookable time_type: :range
186 | end
187 | ```
188 |
189 | **Creating a new bookable**
190 |
191 | Each instance of the model must define its availability in terms of time with an [IceCube Schedule](https://github.com/seejohnrun/ice_cube). Although it's not strictly required, it's strongly suggested to create a schedule with a `:duration`, unless you know exactly what you are doing.
192 |
193 | ```ruby
194 | @meeting_room = MeetingRoom.new(...)
195 | # The schedule starts now and each occurrence is 10 hours long
196 | @meeting_room.schedule = IceCube::Schedule.new(Time.now, duration: 10.hours)
197 | # This meeting_room is available on Mondays starting from 8 AM
198 | @meeting_room.schedule.add_recurrence_rule IceCube::Rule.weekly.day(:monday).hour_of_day(8)
199 | @meeting_room.save!
200 | ```
201 |
202 | **Booking**
203 |
204 | ```ruby
205 | # Next Monday from 9AM to 11AM
206 | from_ok = Date.today.next_week + 9.hours
207 | to_ok = from_ok + 2.hours
208 | # Next Tuesday from 9AM to 11AM
209 | from_wrong = Date.today.next_week + 1.day + 9.hours
210 | to_wrong = from_wrong + 2.hours
211 |
212 | # Booking a model with `time_type: :range` requires `:time_start` and `:time_end`
213 | @user1.book! @meeting_room, time_start: from_ok, time_end: to_ok # OK
214 | @user2.book! @meeting_room, time_start: from_wrong, time_end: to_wrong # raise ActsAsBookable::AvailabilityError
215 | ```
216 |
217 | ### Bookability across occurrences
218 |
219 | Combined with `time_type: :range`, the option `bookable_across_occurrences` allows for creating bookings that start in an occurrence of the schedule and end in another occurrence. By default, it's set to `false`.
220 |
221 | Let's use two examples to better explain the difference
222 |
223 | #### Not bookable across occurrences **`bookable_across_occurrences: false`**
224 |
225 | The model accepts only bookings that start and end within the same occurrence (e.g. a meeting room)
226 |
227 | **Configuration**
228 |
229 | ```ruby
230 | class MeetingRoom < ActiveRecord::Base
231 | # bookable_across_occurrences is always combined with time_type: :range
232 | # As `bookable_across_occurrences: false` is a default, you can omit it. It's shown here for explanation purposes
233 | acts_as_bookable time_type: :range, bookable_across_occurrences: false
234 | end
235 | ```
236 |
237 | **Creating a new bookable**
238 |
239 | ```ruby
240 | @meeting_room = MeetingRoom.new(...)
241 | # The schedule starts now and each occurrence is 4 hours long
242 | @meeting_room.schedule = IceCube::Schedule.new(Time.now, duration: 4.hours)
243 | # This meeting_room is available everyday, from 9AM to 13AM and from 2PM to 6PM
244 | @meeting_room.schedule.add_recurrence_rule IceCube::Rule.daily.hour_of_day(9,14)
245 | @meeting_room.save!
246 | ```
247 |
248 | **Booking**
249 |
250 | ```ruby
251 | # Next Monday from 9AM to 11AM
252 | from_ok = Date.today.next_week + 9.hours
253 | to_ok = from_ok + 2.hours
254 |
255 | # Next Monday from 11AM to 6PM
256 | from_wrong = Date.today.next_week + 11.hours
257 | to_wrong = Date.today.next_week + 18.hours
258 |
259 | # OK - time_start and time_end belong to the same occurrence
260 | @user1.book! @meeting_room, time_start: from_ok, time_end: to_ok
261 |
262 | # raise ActsAsBookable::AvailabilityError - both time_start and time_end are inside the schedule, but they belong to different occurrences
263 | @user2.book! @meeting_room, time_start: from_wrong, time_end: to_wrong
264 | ```
265 |
266 | #### Bookable across occurrences **`bookable_across_occurrences: true`**
267 |
268 | The model may accept bookings that start and end in different occurrences (e.g. a hotel room)
269 |
270 | **Configuration**
271 |
272 | ```ruby
273 | class Room < ActiveRecord::Base
274 | # bookable_across_occurrences is always combined with time_type: :range
275 | acts_as_bookable time_type: :range, bookable_across_occurrences: true
276 | end
277 | ```
278 |
279 | **Creating a new bookable**
280 |
281 | ```ruby
282 | @room = Room.new(...)
283 | # The schedule starts today and each occurrence is 1 day long
284 | @room.schedule = IceCube::Schedule.new(Date.today, duration: 1.day)
285 | # This room is available every week, on weekends
286 | @room.schedule.add_recurrence_rule IceCube::Rule.weekly.day(:friday,:saturday,:sunday)
287 | @room.save!
288 | ```
289 |
290 | **Booking**
291 |
292 | ```ruby
293 | # check-in Friday, check-out Sunday
294 | check_in_ok = Date.today.next_week + 4.days
295 | check_out_ok = check_in_ok + 2.days
296 |
297 | # check-in Tuesday, check-out Sunday
298 | check_in_wrong = Date.today.next_week + 4.days
299 | check_out_wrong = check_in_wrong + 3.days
300 |
301 | # OK - time_start and time_end belong to different occurrences
302 | @user1.book! @room, time_start: check_in_ok, time_end: check_out_ok
303 |
304 | # raise ActsAsBookable::AvailabilityError - while time_end belongs to an occurrence, time_begin doesn't belong to any occurrence of the schedule
305 | @user2.book! @room, time_start: check_in_wrong, time_end: check_out_wrong
306 | ```
307 |
308 | ### Capacity constraints
309 |
310 | The option `capacity_type` may be used to set a constraint over the `amount` attribute of the booking
311 |
312 | #### No capacity constraints - `capacity_type: :none`
313 |
314 | The model is bookable without capacity constraints.
315 |
316 | ```ruby
317 | class Product < ActiveRecord::Base
318 | # As `capacity_type: :none` is a default, you can omit it. It's shown here for explanation purposes
319 | acts_as_bookable capacity_type: :none
320 | end
321 | ```
322 |
323 | #### Open capacity - `capacity_type: :open`
324 |
325 | > WARNING - **migration needed!** - with this option the model must have an attribute `capacity: :integer`
326 |
327 | The model is bookable until its `capacity` is reached. (e.g. an event)
328 |
329 | **Configuration**
330 |
331 | ```ruby
332 | class Event < ActiveRecord::Base
333 | acts_as_bookable capacity_type: :open
334 | end
335 | ```
336 |
337 | **Creating a new bookable**
338 |
339 | Each instance of the model must define its capacity.
340 |
341 | ```ruby
342 | @event = Event.new(...)
343 | @event.capacity = 30 # This event accepts 30 people
344 | @event.save!
345 | ```
346 |
347 | **Booking**
348 |
349 | ```ruby
350 | # Booking a model with `capacity_type: :open` requires `:amount`
351 | @user1.book! @event, amount: 5 # booking the event for 5 people, OK
352 | @user2.book! @event, amount: 20 # booking the event for other 20 people, OK
353 | @user3.book! @event, amount: 10 # overbooking! raise ActsAsBookable::AvailabilityError
354 | ```
355 |
356 | #### Closed capacity - `capacity_type: :closed`
357 |
358 | > WARNING - **migration needed!** - with this option the model must have an attribute `capacity: :integer`
359 |
360 | Similar to open capacity, but after the model is booked, it's no more available, no matter if capacity has not been reached. (e.g. a private room)
361 |
362 | **Configuration**
363 |
364 | ```ruby
365 | class PrivateRoom < ActiveRecord::Base
366 | acts_as_bookable capacity_type: :closed
367 | end
368 | ```
369 |
370 | **Creating a new bookable**
371 |
372 | Each instance of the model must define its capacity.
373 |
374 | ```ruby
375 | @private_room = PrivateRoom.new(...)
376 | @private_room.capacity = 30 # This private_room accepts 30 people
377 | @private_room.save!
378 | ```
379 |
380 | **Booking**
381 |
382 | ```ruby
383 | # Booking a model with `capacity_type: :closed` requires `:amount`
384 | @user1.book! @private_room, amount: 35 # overbooking! raise ActsAsBookable::AvailabilityError
385 | @user2.book! @private_room, amount: 5 # booking for 5 people, OK
386 | @user3.book! @private_room, amount: 5 # not available! Although the room can still hosts (potentially) 25 people, it has already been booked. raise ActsAsBookable::AvailabilityError
387 |
388 | ```
389 |
390 | ### Mixing options
391 |
392 | All the options may be mixed together to achieve different goals.
393 |
394 | Adding an option means adding a constraint to the effectiveness of a booking on a bookable.
395 |
396 | ### Presets
397 |
398 | Some combinations of common options are provided as built-in presets. They are activated using just the option `:preset`
399 |
400 | As for now only these presets are provided, others are coming soon:
401 |
402 | * `:room`
403 |
404 | #### Room - `preset: :room`
405 |
406 | > WARNING - **migration needed!** - with this option the model must have an attribute `capacity: :integer` and an attribute `schedule: :text`
407 |
408 | An hotel room has the following costraints:
409 |
410 | 1. It accepts bookings that specify a **range of time** (i.e. check-in and check-out)
411 | 2. It has a **capacity** that cannot be exceeded
412 | 3. After it has been booked, it becomes unavailable for the given range of time, even though its capacity has not been reached.
413 | 4. Its availability is expressed in terms of opening days (a schedule of 1 day long occurrences), but a single booking may cover more than one day (e.g. a weekend)
414 |
415 | **Configuration**:
416 |
417 | ```ruby
418 | class Room < ActiveRecord::Base
419 | acts_as_bookable preset: :room
420 | end
421 | ```
422 |
423 | Which is equivalent to
424 |
425 | ```ruby
426 | class Room < ActiveRecord::Base
427 | acts_as_bookable time_type: :range,
428 | capacity_type: :closed,
429 | bookable_across_occurrences: true
430 | end
431 | ```
432 |
433 |
434 | **Booking:**
435 |
436 | ```ruby
437 | # A @user books a @room for 2 people. Check-in is today and check-out is tomorrow.
438 | @user.book! @room, time_start: Date.today, time_end: Date.tomorrow, amount: 2
439 | ```
440 |
441 | ## FYI
442 |
443 | ### Hey... Why not just an initializer?
444 |
445 | We decided not to provide an initializer to configure bookings because one of the goals of this gem is to allow for different kinds of booking inside the same application.
446 |
447 | This is achieved delegating the responsability of deciding "how a booking should be created" to the Bookable itself. In this way, different configurations of `acts_as_bookable` inside a model bring to different ways of creating and managing bookings for ***that*** *(and only that)* model.
448 |
449 | ### License
450 |
451 | Copyright 2016 Tandù srl
452 |
453 | Permission is hereby granted, free of charge, to any person obtaining
454 | a copy of this software and associated documentation files (the
455 | "Software"), to deal in the Software without restriction, including
456 | without limitation the rights to use, copy, modify, merge, publish,
457 | distribute, sublicense, and/or sell copies of the Software, and to
458 | permit persons to whom the Software is furnished to do so, subject to
459 | the following conditions:
460 |
461 | The above copyright notice and this permission notice shall be
462 | included in all copies or substantial portions of the Software.
463 |
464 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
465 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
466 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
467 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
468 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
469 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
470 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
471 |
472 | ### Acknowledgements
473 |
474 | To speed-up the initialization process of this project, the structure of this repository was strongly influenced by [ActsAsTaggableOn](https://github.com/mbleigh/acts-as-taggable-on) by Michael Bleigh and Intridea Inc.
475 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'bundler/setup'
3 |
4 | desc 'Default: run specs'
5 | task default: :spec
6 |
7 | desc 'Copy sample spec database.yml over if not exists'
8 | task :copy_db_config do
9 | cp 'spec/internal/config/database.yml.sample', 'spec/internal/config/database.yml'
10 | end
11 |
12 | task spec: [:copy_db_config]
13 |
14 | require 'rspec/core/rake_task'
15 | RSpec::Core::RakeTask.new do |t|
16 | t.pattern = 'spec/**/*_spec.rb'
17 | end
18 |
19 | Bundler::GemHelper.install_tasks
20 |
--------------------------------------------------------------------------------
/acts_as_bookable.gemspec:
--------------------------------------------------------------------------------
1 | lib = File.expand_path('../lib', __FILE__)
2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3 |
4 | # Maintain your gem's version:
5 | require "acts_as_bookable/version"
6 |
7 | # Describe your gem and declare its dependencies:
8 | Gem::Specification.new do |gem|
9 | gem.name = "acts_as_bookable"
10 | gem.version = ActsAsBookable::VERSION
11 | gem.authors = ["Chosko"]
12 | gem.email = ["ruben.caliandro@gmail.com"]
13 | gem.homepage = "https://github.com/tandusrl/acts_as_bookable"
14 | gem.summary = "The reservation engine for Rails applications that allows resources to be booked"
15 | gem.description = "ActsAsBookable is a reservation engine for Rails applications that allows resources to be booked. You can define availability rules for bookable models and set costraints to implement different types of booking (hotels, theaters, meeting rooms...)"
16 | gem.licenses = ["MIT"]
17 | gem.platform = Gem::Platform::RUBY
18 |
19 | gem.files = `git ls-files`.split($/)
20 | gem.test_files = gem.files.grep(%r{^spec/})
21 |
22 | gem.require_paths = ['lib']
23 | gem.required_ruby_version = '>= 2.0.0'
24 |
25 | if File.exist?('UPGRADING.md')
26 | gem.post_install_message = File.read('UPGRADING.md')
27 | end
28 |
29 | gem.add_dependency 'ice_cube_chosko', '~> 0.1.0'
30 | gem.add_runtime_dependency 'activerecord', ['>= 3.2', '< 5.1']
31 |
32 | gem.add_development_dependency 'rake'
33 | gem.add_development_dependency 'rails'
34 | gem.add_development_dependency 'sqlite3', '~> 1.3'
35 | gem.add_development_dependency 'mysql2', '~> 0.3.7'
36 | gem.add_development_dependency 'pg', '~> 0.18'
37 | gem.add_development_dependency 'rspec-rails', '~> 3'
38 | gem.add_development_dependency 'rspec', '~> 3'
39 | gem.add_development_dependency 'coveralls', '~> 0.8'
40 | gem.add_development_dependency 'factory_girl_rails', '~> 4.6'
41 | gem.add_development_dependency 'barrier', '~> 1.0'
42 | gem.add_development_dependency 'database_cleaner', '~>1.5'
43 | gem.add_development_dependency 'awesome_print', '~>1.6'
44 | end
45 |
--------------------------------------------------------------------------------
/app/assets/images/acts_as_bookable/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tandusrl/acts_as_bookable/45b78114e1a5dab96e59fc70933277a56f65b53b/app/assets/images/acts_as_bookable/.keep
--------------------------------------------------------------------------------
/app/assets/javascripts/acts_as_bookable/application.js:
--------------------------------------------------------------------------------
1 | // This is a manifest file that'll be compiled into application.js, which will include all the files
2 | // listed below.
3 | //
4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
6 | //
7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8 | // compiled file.
9 | //
10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
11 | // about supported directives.
12 | //
13 | //= require_tree .
14 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/acts_as_bookable/application.css:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll be compiled into application.css, which will include all the files
3 | * listed below.
4 | *
5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7 | *
8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9 | * compiled file so the styles you add here take precedence over styles defined in any styles
10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new
11 | * file per style scope.
12 | *
13 | *= require_tree .
14 | *= require_self
15 | */
16 |
--------------------------------------------------------------------------------
/app/controllers/acts_as_bookable/application_controller.rb:
--------------------------------------------------------------------------------
1 | module ActsAsBookable
2 | class ApplicationController < ActionController::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/app/helpers/acts_as_bookable/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ActsAsBookable
2 | module ApplicationHelper
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/app/views/layouts/acts_as_bookable/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ActsAsBookable
5 | <%= stylesheet_link_tag "acts_as_bookable/application", media: "all" %>
6 | <%= javascript_include_tag "acts_as_bookable/application" %>
7 | <%= csrf_meta_tags %>
8 |
9 |
10 |
11 | <%= yield %>
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # This command will automatically be run when you run "rails" with Rails 4 gems installed from the root of your application.
3 |
4 | ENGINE_ROOT = File.expand_path('../..', __FILE__)
5 | ENGINE_PATH = File.expand_path('../../lib/acts_as_bookable/engine', __FILE__)
6 |
7 | # Set up gems listed in the Gemfile.
8 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
9 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
10 |
11 | require 'rails/all'
12 | require 'rails/engine/commands'
13 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | en:
2 | acts_as_bookable:
3 | errors:
4 | messages:
5 | booking:
6 | bookable_must_be_bookable: "cannot book a %{model} as it\'s not bookable"
7 | booker_must_be_booker: "a %{model} cannot book a resource, because %{model} is not a booker"
8 | availability:
9 | amount_gt_capacity: "amount cannot be greater than %{model} capacity"
10 | already_booked: "the %{model} is fully booked"
11 | unavailable_interval: "the %{model} is not available from %{time_start} to %{time_end}"
12 | unavailable_time: "the %{model} is not available at %{time}"
13 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | ActsAsBookable::Engine.routes.draw do
2 | end
3 |
--------------------------------------------------------------------------------
/db/migrate/20160217085200_create_acts_as_bookable_bookings.rb:
--------------------------------------------------------------------------------
1 | class CreateActsAsBookableBookings < ActiveRecord::Migration
2 | def change
3 | create_table :acts_as_bookable_bookings, force: true do |t|
4 | t.references :bookable, polymorphic: true, index: {name: "index_acts_as_bookable_bookings_bookable"}
5 | t.references :booker, polymorphic: true, index: {name: "index_acts_as_bookable_bookings_booker"}
6 | t.column :amount, :integer
7 | t.column :schedule, :text
8 | t.column :time_start, :datetime
9 | t.column :time_end, :datetime
10 | t.column :time, :datetime
11 | t.datetime :created_at
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/gemfiles/activerecord_3.2.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "activerecord", :github => "rails/rails", :branch => "3-2-stable"
6 |
7 | group :local_development do
8 | gem "guard"
9 | gem "guard-rspec"
10 | gem "appraisal"
11 | gem "byebug", :platform => :mri_21
12 | gem "pry-nav"
13 | end
14 |
15 | gemspec :path => "../"
16 |
--------------------------------------------------------------------------------
/gemfiles/activerecord_4.0.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "activerecord", :github => "rails/rails", :branch => "4-0-stable"
6 |
7 | group :local_development do
8 | gem "guard"
9 | gem "guard-rspec"
10 | gem "appraisal"
11 | gem "byebug", :platform => :mri_21
12 | gem "pry-nav"
13 | end
14 |
15 | gemspec :path => "../"
16 |
--------------------------------------------------------------------------------
/gemfiles/activerecord_4.1.gemfile:
--------------------------------------------------------------------------------
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 "byebug", :platform => :mri_21
12 | gem "pry-nav"
13 | end
14 |
15 | gemspec :path => "../"
16 |
--------------------------------------------------------------------------------
/gemfiles/activerecord_4.2.gemfile:
--------------------------------------------------------------------------------
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 "byebug", :platform => :mri_21
13 | gem "pry-nav"
14 | end
15 |
16 | gemspec :path => "../"
17 |
--------------------------------------------------------------------------------
/gemfiles/activerecord_5.0.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "railties", :github => "rails/rails", :branch => "5-0-stable"
6 | gem "activerecord", :github => "rails/rails", :branch => "5-0-stable"
7 |
8 | group :local_development do
9 | gem "guard"
10 | gem "guard-rspec"
11 | gem "appraisal"
12 | gem "byebug", :platform => :mri_21
13 | gem "pry-nav"
14 | end
15 |
16 | gemspec :path => "../"
17 |
--------------------------------------------------------------------------------
/lib/acts_as_bookable.rb:
--------------------------------------------------------------------------------
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
43 |
--------------------------------------------------------------------------------
/lib/acts_as_bookable/bookable.rb:
--------------------------------------------------------------------------------
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
57 |
--------------------------------------------------------------------------------
/lib/acts_as_bookable/bookable/core.rb:
--------------------------------------------------------------------------------
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 | # validates options
166 | self.validate_booking_options!(opts)
167 |
168 | # Capacity check (done first because it doesn't require additional queries)
169 | if self.booking_opts[:capacity_type] != :none
170 | # Amount > capacity
171 | if opts[:amount] > self.capacity
172 | raise ActsAsBookable::AvailabilityError.new ActsAsBookable::T.er('.availability.amount_gt_capacity', model: self.class.to_s)
173 | end
174 | end
175 |
176 | ##
177 | # Time check
178 | #
179 | if self.booking_opts[:time_type] == :range
180 | time_check_ok = true
181 | # If it's bookable across recurrences, just check start time and end time
182 | if self.booking_opts[:bookable_across_occurrences]
183 | # Check start time
184 | if !(ActsAsBookable::TimeUtils.time_in_schedule?(self.schedule, opts[:time_start]))
185 | time_check_ok = false
186 | end
187 | # Check end time
188 | if !(ActsAsBookable::TimeUtils.time_in_schedule?(self.schedule, opts[:time_end]))
189 | time_check_ok = false
190 | end
191 | # If it's not bookable across recurrences, check if the whole interval is included in an occurrence
192 | else
193 | # Check the whole interval
194 | if !(ActsAsBookable::TimeUtils.interval_in_schedule?(self.schedule, opts[:time_start], opts[:time_end]))
195 | time_check_ok = false
196 | end
197 | end
198 | # If something went wrong
199 | unless time_check_ok
200 | 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])
201 | end
202 | end
203 | if self.booking_opts[:time_type] == :fixed
204 | if !(ActsAsBookable::TimeUtils.time_in_schedule?(self.schedule, opts[:time]))
205 | raise ActsAsBookable::AvailabilityError.new ActsAsBookable::T.er('.availability.unavailable_time', model: self.class.to_s, time: opts[:time])
206 | end
207 | end
208 |
209 | ##
210 | # Real capacity check (calculated with overlapped bookings)
211 | #
212 | overlapped = ActsAsBookable::Booking.overlapped(self, opts)
213 | # If capacity_type is :closed cannot book if already booked (no matter if amount < capacity)
214 | if (self.booking_opts[:capacity_type] == :closed && !overlapped.empty?)
215 | raise ActsAsBookable::AvailabilityError.new ActsAsBookable::T.er('.availability.already_booked', model: self.class.to_s)
216 | end
217 | # if capacity_type is :open, check if amount <= maximum amount of overlapped booking
218 | if (self.booking_opts[:capacity_type] == :open && !overlapped.empty?)
219 | # if time_type is :range, split in sub-intervals and check the maximum sum of amounts against capacity for each sub-interval
220 | if (self.booking_opts[:time_type] == :range)
221 | # Map overlapped bookings to a set of intervals with amount
222 | intervals = overlapped.map { |e| {time_start: e.time_start, time_end: e.time_end, amount: e.amount} }
223 | # Make subintervals from overlapped bookings and check capacity for each of them
224 | ActsAsBookable::TimeUtils.subintervals(intervals) do |a,b,op|
225 | case op
226 | when :open
227 | res = {amount: a[:amount] + b[:amount]}
228 | when :close
229 | res = {amount: a[:amount] - b[:amount]}
230 | end
231 | raise ActsAsBookable::AvailabilityError.new ActsAsBookable::T.er('.availability.already_booked', model: self.class.to_s) if (res[:amount] > self.capacity)
232 | res
233 | end
234 | # else, just sum the amounts (fixed times are not intervals and they overlap if are the same)
235 | else
236 | if(overlapped.sum(:amount) + opts[:amount] > self.capacity)
237 | raise ActsAsBookable::AvailabilityError.new ActsAsBookable::T.er('.availability.already_booked', model: self.class.to_s)
238 | end
239 | end
240 | end
241 | true
242 | end
243 |
244 | ##
245 | # Check availability of current bookable
246 | #
247 | # @param opts The booking options
248 | # @return true if the bookable is available for given options, otherwise return false
249 | #
250 | # Example:
251 | # @room.check_availability!(from: Date.today, to: Date.tomorrow, amount: 2)
252 | def check_availability(opts)
253 | begin
254 | check_availability!(opts)
255 | rescue ActsAsBookable::AvailabilityError
256 | false
257 | end
258 | end
259 |
260 | ##
261 | # Accept a booking by a booker. This is an alias method,
262 | # equivalent to @booker.book!(@bookable, opts)
263 | #
264 | # @param booker The booker model
265 | # @param opts The booking options
266 | #
267 | # Example:
268 | # @room.be_booked!(@user, from: Date.today, to: Date.tomorrow, amount: 2)
269 | def be_booked!(booker, opts={})
270 | booker.book!(self, opts)
271 | end
272 |
273 | ##
274 | # Check if options passed for booking this Bookable are valid
275 | #
276 | # @raise ActsAsBookable::OptionsInvalid if options are not valid
277 | # @param opts The booking options
278 | #
279 | def validate_booking_options!(opts)
280 | self.class.validate_booking_options!(opts)
281 | end
282 |
283 | def booker?
284 | self.class.booker?
285 | end
286 | end
287 | end
288 | end
289 |
--------------------------------------------------------------------------------
/lib/acts_as_bookable/booker.rb:
--------------------------------------------------------------------------------
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 | # check availability
44 | bookable.check_availability!(opts) if bookable.class.bookable?
45 |
46 | # create the new booking
47 | booking_params = opts.merge({booker: self, bookable: bookable})
48 | booking = ActsAsBookable::Booking.create!(booking_params)
49 |
50 | # reload the bookable to make changes available
51 | bookable.reload
52 | self.reload
53 | booking
54 | end
55 |
56 | def booker?
57 | self.class.booker?
58 | end
59 | end
60 |
61 | module SingletonMethods
62 | def booker?
63 | true
64 | end
65 | end
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/lib/acts_as_bookable/booking.rb:
--------------------------------------------------------------------------------
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 = DBUtils.time_comparison(query,'time','=',opts[:time])
25 | end
26 | if(opts[:time_start].present?)
27 | query = DBUtils.time_comparison(query,'time_end', '>=', opts[:time_start])
28 | end
29 | if(opts[:time_end].present?)
30 | query = DBUtils.time_comparison(query,'time_start', '<', opts[:time_end])
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
55 |
--------------------------------------------------------------------------------
/lib/acts_as_bookable/db_utils.rb:
--------------------------------------------------------------------------------
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 | ##
34 | # Compare times according to the DB
35 | #
36 | def time_comparison (query, field, operator, time)
37 | if using_postgresql?
38 | query.where("#{field}::timestamp #{operator} ?::timestamp", time.to_time.utc.to_s)
39 | elsif using_sqlite?
40 | query.where("Datetime(#{field}) #{operator} Datetime('#{time.to_time.utc.iso8601}')")
41 | else
42 | query.where("#{field} #{operator} ?", time.to_time)
43 | end
44 | end
45 |
46 | # escape _ and % characters in strings, since these are wildcards in SQL.
47 | def escape_like(str)
48 | str.gsub(/[!%_]/) { |x| '!' + x }
49 | end
50 | end
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/lib/acts_as_bookable/engine.rb:
--------------------------------------------------------------------------------
1 | require 'rails/engine'
2 | module ActsAsBookable
3 | class Engine < ::Rails::Engine
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/lib/acts_as_bookable/t.rb:
--------------------------------------------------------------------------------
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
12 |
--------------------------------------------------------------------------------
/lib/acts_as_bookable/time_utils.rb:
--------------------------------------------------------------------------------
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
136 |
--------------------------------------------------------------------------------
/lib/acts_as_bookable/version.rb:
--------------------------------------------------------------------------------
1 | module ActsAsBookable
2 | VERSION = "0.1.4"
3 | end
4 |
--------------------------------------------------------------------------------
/lib/tasks/acts_as_bookable_tasks.rake:
--------------------------------------------------------------------------------
1 | # desc "Explaining what the task does"
2 | # task :acts_as_bookable do
3 | # # Task goes here
4 | # end
5 |
--------------------------------------------------------------------------------
/spec/acts_as_bookable/acts_as_bookable_spec.rb:
--------------------------------------------------------------------------------
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
53 |
--------------------------------------------------------------------------------
/spec/acts_as_bookable/acts_as_booker_spec.rb:
--------------------------------------------------------------------------------
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
58 |
--------------------------------------------------------------------------------
/spec/acts_as_bookable/bookable/core_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe 'Bookable model' do
4 | describe 'InstanceMethods' do
5 | it 'should add a method #check_availability! in instance-side' do
6 | @bookable = Bookable.new
7 | expect(@bookable).to respond_to :check_availability!
8 | end
9 |
10 | it 'should add a method #check_availability in instance-side' do
11 | @bookable = Bookable.new
12 | expect(@bookable).to respond_to :check_availability
13 | end
14 |
15 | it 'should add a method #validate_booking_options! in instance-side' do
16 | @bookable = Bookable.new
17 | expect(@bookable).to respond_to :validate_booking_options!
18 | end
19 |
20 | describe '#check_availability! and check_availability' do
21 | after(:each) do
22 | Bookable.booking_opts = {}
23 | Bookable.initialize_acts_as_bookable_core
24 | end
25 |
26 | describe 'whithout any constraint' do
27 | before(:each) do
28 | Bookable.booking_opts = {
29 | time_type: :none,
30 | capacity_type: :none
31 | }
32 | Bookable.initialize_acts_as_bookable_core
33 | @bookable = Bookable.create!(name: 'bookable')
34 | end
35 |
36 | it 'should be always available' do
37 | expect(@bookable.check_availability({})).to be_truthy
38 | expect(@bookable.check_availability!({})).to be_truthy
39 | end
40 | end
41 | end
42 |
43 | describe 'with time_type: :range' do
44 | before(:each) do
45 | Bookable.booking_opts = {
46 | time_type: :range,
47 | capacity_type: :none
48 | }
49 | Bookable.initialize_acts_as_bookable_core
50 | @bookable = Bookable.create!(name: 'bookable', schedule: IceCube::Schedule.new('2016-01-01'.to_date, duration: 1.day))
51 | ## bookable the first and third day of the month
52 | @bookable.schedule.add_recurrence_rule IceCube::Rule.monthly.day_of_month([1,3])
53 | @bookable.save!
54 | end
55 |
56 | it 'should be available in bookable times' do
57 | time = '2016-01-01'.to_date
58 | endtime = time + 10.minutes
59 | expect(@bookable.check_availability!({time_start: time, time_end: endtime})).to be_truthy
60 | expect(@bookable.check_availability({time_start: time, time_end: endtime})).to be_truthy
61 | end
62 |
63 | it 'should not be available in not bookable times' do
64 | time = '2016-01-02'.to_date
65 | endtime = '2016-01-04'.to_date
66 | expect(@bookable.check_availability({time_start: time, time_end: endtime})).to be_falsy
67 | expect{ @bookable.check_availability!({time_start: time, time_end: endtime}) }.to raise_error ActsAsBookable::AvailabilityError
68 | begin
69 | @bookable.check_availability!({time_start: time, time_end: endtime})
70 | rescue ActsAsBookable::AvailabilityError => e
71 | expect(e.message).to include "the Bookable is not available from #{time.to_time} to #{endtime.to_time}"
72 | end
73 | end
74 |
75 | it 'should be bookable within a bookable time' do
76 | time = '2016-01-01'.to_date + 1.minute
77 | endtime = '2016-01-02'.to_date - 1.minute
78 | expect(@bookable.check_availability!({time_start: time, time_end: endtime})).to be_truthy
79 | expect(@bookable.check_availability({time_start: time, time_end: endtime})).to be_truthy
80 | end
81 |
82 | it 'should be bookable within a bookable time' do
83 | time = '2016-01-01'.to_date + 1.minute
84 | endtime = '2016-01-02'.to_date - 1.minute
85 | expect(@bookable.check_availability!({time_start: time, time_end: endtime})).to be_truthy
86 | expect(@bookable.check_availability({time_start: time, time_end: endtime})).to be_truthy
87 | end
88 |
89 | it 'should not be available when time_start is available, time_end is available but the availability is splitted in between' do
90 | time = '2016-01-01'.to_date + 1.minute
91 | endtime = '2016-01-03'.to_date + 1.minute
92 | expect(@bookable.check_availability({time_start: time, time_end: endtime})).to be_falsy
93 | expect{ @bookable.check_availability!({time_start: time, time_end: endtime}) }.to raise_error ActsAsBookable::AvailabilityError
94 | begin
95 | @bookable.check_availability!({time_start: time, time_end: endtime})
96 | rescue ActsAsBookable::AvailabilityError => e
97 | expect(e.message).to include "the Bookable is not available from #{time} to #{endtime}"
98 | end
99 | end
100 | end
101 |
102 | describe 'with time_type: :range and with bookable_across_occurrences: true' do
103 | before(:each) do
104 | Bookable.booking_opts = {
105 | time_type: :range,
106 | capacity_type: :none,
107 | bookable_across_occurrences: true
108 | }
109 | Bookable.initialize_acts_as_bookable_core
110 | @bookable = Bookable.create!(name: 'bookable', schedule: IceCube::Schedule.new('2016-01-01'.to_date, duration: 1.day))
111 | ## bookable the first and third day of the month
112 | @bookable.schedule.add_recurrence_rule IceCube::Rule.monthly.day_of_month([1,3])
113 | @bookable.save!
114 | end
115 |
116 | it 'should be available in bookable times' do
117 | time = '2016-01-01'.to_date
118 | endtime = time + 10.minutes
119 | expect(@bookable.check_availability!({time_start: time, time_end: endtime})).to be_truthy
120 | expect(@bookable.check_availability({time_start: time, time_end: endtime})).to be_truthy
121 | end
122 |
123 | it 'should not be available in not bookable times' do
124 | time = '2016-01-02'.to_date
125 | endtime = '2016-01-04'.to_date
126 | expect(@bookable.check_availability({time_start: time, time_end: endtime})).to be_falsy
127 | expect{ @bookable.check_availability!({time_start: time, time_end: endtime}) }.to raise_error ActsAsBookable::AvailabilityError
128 | begin
129 | @bookable.check_availability!({time_start: time, time_end: endtime})
130 | rescue ActsAsBookable::AvailabilityError => e
131 | expect(e.message).to include "the Bookable is not available from #{time.to_time} to #{endtime.to_time}"
132 | end
133 | end
134 |
135 | it 'should be bookable within a bookable time' do
136 | time = '2016-01-01'.to_date + 1.minute
137 | endtime = '2016-01-02'.to_date - 1.minute
138 | expect(@bookable.check_availability!({time_start: time, time_end: endtime})).to be_truthy
139 | expect(@bookable.check_availability({time_start: time, time_end: endtime})).to be_truthy
140 | end
141 |
142 | it 'should be bookable within a bookable time' do
143 | time = '2016-01-01'.to_date + 1.minute
144 | endtime = '2016-01-02'.to_date - 1.minute
145 | expect(@bookable.check_availability!({time_start: time, time_end: endtime})).to be_truthy
146 | expect(@bookable.check_availability({time_start: time, time_end: endtime})).to be_truthy
147 | end
148 |
149 | it 'should be available when time_start is available, time_end is available and the availability is splitted in between' do
150 | time = '2016-01-01'.to_date + 1.minute
151 | endtime = '2016-01-03'.to_date + 1.minute
152 | expect(@bookable.check_availability({time_start: time, time_end: endtime})).to be_truthy
153 | expect(@bookable.check_availability!({time_start: time, time_end: endtime})).to be_truthy
154 | end
155 | end
156 |
157 | describe 'with time_type: :fixed' do
158 | before(:each) do
159 | Bookable.booking_opts = {
160 | time_type: :fixed,
161 | capacity_type: :none
162 | }
163 | Bookable.initialize_acts_as_bookable_core
164 | @bookable = Bookable.create!(name: 'bookable', schedule: IceCube::Schedule.new('2016-01-01'.to_date))
165 | ## bookable the first and third day of the month, at 9AM
166 | @bookable.schedule.add_recurrence_rule IceCube::Rule.monthly.day_of_month([1,3]).hour_of_day(9)
167 | @bookable.save!
168 | end
169 |
170 | it 'should be available in available times' do
171 | time = '2016-01-01'.to_date + 9.hours
172 | expect(@bookable.check_availability(time: time)).to be_truthy
173 | expect(@bookable.check_availability!(time: time)).to be_truthy
174 | time = '2016-01-03'.to_date + 9.hours
175 | expect(@bookable.check_availability(time: time)).to be_truthy
176 | expect(@bookable.check_availability!(time: time)).to be_truthy
177 | end
178 |
179 | it 'should not be available in not bookable day' do
180 | time = '2016-01-02'.to_date
181 | expect(@bookable.check_availability(time: time)).to be_falsy
182 | expect{ @bookable.check_availability!(time: time) }.to raise_error ActsAsBookable::AvailabilityError
183 | begin
184 | @bookable.check_availability!(time: time)
185 | rescue ActsAsBookable::AvailabilityError => e
186 | expect(e.message).to include "the Bookable is not available at #{time}"
187 | end
188 | end
189 |
190 |
191 | it 'should not be available in bookable day but not bookable time' do
192 | time = '2016-01-02'.to_date + 10.hours
193 | expect(@bookable.check_availability(time: time)).to be_falsy
194 | expect{ @bookable.check_availability!(time: time) }.to raise_error ActsAsBookable::AvailabilityError
195 | begin
196 | @bookable.check_availability!(time: time)
197 | rescue ActsAsBookable::AvailabilityError => e
198 | expect(e.message).to include "the Bookable is not available at #{time}"
199 | end
200 | end
201 |
202 | it 'should not be available very close to a bookable time but not the exact second' do
203 | time = '2016-01-02'.to_date + 9.hours + 1.second
204 | expect(@bookable.check_availability(time: time)).to be_falsy
205 | expect{ @bookable.check_availability!(time: time) }.to raise_error ActsAsBookable::AvailabilityError
206 | begin
207 | @bookable.check_availability!(time: time)
208 | rescue ActsAsBookable::AvailabilityError => e
209 | expect(e.message).to include "the Bookable is not available at #{time}"
210 | end
211 | end
212 | end
213 |
214 | describe 'with capacity_type: :open' do
215 | before(:each) do
216 | Bookable.booking_opts = {
217 | time_type: :none,
218 | capacity_type: :open
219 | }
220 | Bookable.initialize_acts_as_bookable_core
221 | @bookable = Bookable.create!(name: 'bookable', capacity: 4)
222 | end
223 |
224 | it 'should be available if amount <= capacity' do
225 | (1..@bookable.capacity).each do |amount|
226 | expect(@bookable.check_availability(amount: amount)).to be_truthy
227 | expect(@bookable.check_availability!(amount: amount)).to be_truthy
228 | end
229 | end
230 |
231 | it 'should not be available if amount > capacity' do
232 | expect(@bookable.check_availability(amount: @bookable.capacity + 1)).to be_falsy
233 | expect { @bookable.check_availability!(amount: @bookable.capacity + 1) }.to raise_error ActsAsBookable::AvailabilityError
234 | begin
235 | @bookable.check_availability!(amount: @bookable.capacity + 1)
236 | rescue ActsAsBookable::AvailabilityError => e
237 | expect(e.message).to include 'cannot be greater'
238 | end
239 | end
240 |
241 | it 'should be available if already booked but amount <= conditional capacity' do
242 | booker = create(:booker)
243 | @bookable.be_booked!(booker, amount: 2)
244 | (1..(@bookable.capacity - 2)).each do |amount|
245 | expect(@bookable.check_availability(amount: amount)).to be_truthy
246 | expect(@bookable.check_availability!(amount: amount)).to be_truthy
247 | end
248 | end
249 |
250 | it 'should not be available if amount <= capacity but already booked and amount > conditional capacity' do
251 | booker = create(:booker)
252 | @bookable.be_booked!(booker, amount: 2)
253 | amount = @bookable.capacity - 2 + 1
254 | expect(@bookable.check_availability(amount: amount)).to be_falsy
255 | expect { @bookable.check_availability!(amount: amount) }.to raise_error ActsAsBookable::AvailabilityError
256 | begin
257 | @bookable.check_availability!(amount: amount)
258 | rescue ActsAsBookable::AvailabilityError => e
259 | expect(e.message).to include 'is fully booked'
260 | end
261 | end
262 |
263 | it 'should be available if amount <= capacity and already booked and amount > conditional capacity but overlappings are separated in time and space' do
264 | Bookable.booking_opts = {
265 | time_type: :range,
266 | capacity_type: :open,
267 | bookable_across_occurrences: true
268 | }
269 | Bookable.initialize_acts_as_bookable_core
270 | @bookable = Bookable.create!(name: 'bookable', capacity: 4, schedule: IceCube::Schedule.new(Date.today, duration: 1.day))
271 | @bookable.schedule.add_recurrence_rule IceCube::Rule.daily
272 | @bookable.save!
273 | booker = create(:booker)
274 | @bookable.be_booked!(booker, amount: 3, time_start: Date.today, time_end: Date.today + 8.hours)
275 | @bookable.be_booked!(booker, amount: 3, time_start: Date.today + 8.hours, time_end: Date.today + 16.hours)
276 | @bookable.be_booked!(booker, amount: 3, time_start: Date.today + 16.hours, time_end: Date.today + 24.hours)
277 | amount = 1
278 | expect(@bookable.check_availability(amount: amount, time_start: Date.today, time_end: Date.today + 24.hours)).to be_truthy
279 | end
280 |
281 | it 'should not be available if amount <= capacity and already booked and amount > conditional capacity' do
282 | Bookable.booking_opts = {
283 | time_type: :range,
284 | capacity_type: :open,
285 | bookable_across_occurrences: true
286 | }
287 | Bookable.initialize_acts_as_bookable_core
288 | @bookable = Bookable.create!(name: 'bookable', capacity: 4, schedule: IceCube::Schedule.new(Date.today, duration: 1.day))
289 | @bookable.schedule.add_recurrence_rule IceCube::Rule.daily
290 | @bookable.save!
291 | booker = create(:booker)
292 | @bookable.be_booked!(booker, amount: 3, time_start: Date.today, time_end: Date.today + 8.hours)
293 | @bookable.be_booked!(booker, amount: 3, time_start: Date.today + 8.hours, time_end: Date.today + 16.hours)
294 | @bookable.be_booked!(booker, amount: 1, time_start: Date.today + 8.hours, time_end: Date.today + 24.hours)
295 | amount = 2
296 | expect(@bookable.check_availability(amount: amount, time_start: Date.today, time_end: Date.today + 8.hours)).to be_truthy
297 | expect(@bookable.check_availability(amount: amount, time_start: Date.today + 8.hours, time_end: Date.today + 16.hours)).to be_truthy
298 | expect(@bookable.check_availability(amount: amount, time_start: Date.today + 16.hours, time_end: Date.today + 24.hours)).to be_truthy
299 | expect(@bookable.check_availability(amount: amount, time_start: Date.today, time_end: Date.today + 24.hours)).to be_truthy
300 | end
301 | end
302 |
303 | describe 'with capacity_type: :closed' do
304 | before(:each) do
305 | Bookable.booking_opts = {
306 | time_type: :none,
307 | capacity_type: :closed
308 | }
309 | Bookable.initialize_acts_as_bookable_core
310 | @bookable = Bookable.create!(name: 'bookable', capacity: 4)
311 | end
312 |
313 | it 'should be available if amount <= capacity' do
314 | (1..@bookable.capacity).each do |amount|
315 | expect(@bookable.check_availability(amount: amount)).to be_truthy
316 | expect(@bookable.check_availability!(amount: amount)).to be_truthy
317 | end
318 | end
319 |
320 | it 'should not be available if amount > capacity' do
321 | expect(@bookable.check_availability(amount: @bookable.capacity + 1)).to be_falsy
322 | expect { @bookable.check_availability!(amount: @bookable.capacity + 1) }.to raise_error ActsAsBookable::AvailabilityError
323 | begin
324 | @bookable.check_availability!(amount: @bookable.capacity + 1)
325 | rescue ActsAsBookable::AvailabilityError => e
326 | expect(e.message).to include 'cannot be greater'
327 | end
328 | end
329 |
330 | it 'should not be available if already booked (even though amount < capacity - overlapped amounts)' do
331 | booker = create(:booker)
332 | @bookable.be_booked!(booker, amount: 1)
333 | (1..(@bookable.capacity + 1)).each do |amount|
334 | expect(@bookable.check_availability(amount: amount)).to be_falsy
335 | expect { @bookable.check_availability!(amount: amount) }.to raise_error ActsAsBookable::AvailabilityError
336 | begin
337 | @bookable.check_availability!(amount: amount)
338 | rescue ActsAsBookable::AvailabilityError => e
339 | if(amount <= @bookable.capacity)
340 | expect(e.message).to include('is fully booked')
341 | else
342 | expect(e.message).to include('cannot be greater')
343 | end
344 | end
345 | end
346 | end
347 | end
348 | end
349 |
350 | describe 'classMethods' do
351 | before(:each) do
352 | Bookable.booking_opts = {}
353 | Bookable.initialize_acts_as_bookable_core
354 | end
355 | after(:each) do
356 | Bookable.booking_opts = {}
357 | Bookable.initialize_acts_as_bookable_core
358 | end
359 |
360 | describe 'self.initialize_acts_as_bookable_core' do
361 | describe '#set_options' do
362 | it 'preset options for room' do
363 | [:room,:event,:show].each do |p|
364 | Bookable.booking_opts = {preset: p}
365 | Bookable.initialize_acts_as_bookable_core
366 | expect(Bookable.booking_opts[:preset]).to eq p
367 | expect(Bookable.booking_opts[:time_type]).to be(:range).or be(:fixed).or be(:none)
368 | expect(Bookable.booking_opts[:capacity_type]).to be(:open).or be(:closed)
369 | expect(Bookable.booking_opts[:bookable_across_occurrences]).to be(true).or be(false)
370 | end
371 | end
372 |
373 | it 'fails when using an unknown preset' do
374 | Bookable.booking_opts = {preset: 'unknown'}
375 | expect{ Bookable.initialize_acts_as_bookable_core }.to raise_error ActsAsBookable::InitializationError
376 | end
377 |
378 | it 'correctly set undefined options' do
379 | Bookable.booking_opts = {}
380 | Bookable.initialize_acts_as_bookable_core
381 | expect(Bookable.booking_opts[:preset]).not_to be_present
382 | expect(Bookable.booking_opts[:date_type]).not_to be_present
383 | expect(Bookable.booking_opts[:time_type]).to be_present
384 | expect(Bookable.booking_opts[:location_type]).not_to be_present
385 | expect(Bookable.booking_opts[:capacity_type]).to be_present
386 | expect(Bookable.booking_opts[:bookable_across_occurrences]).not_to be_nil
387 | end
388 |
389 | it 'correctly merges options' do
390 | Bookable.booking_opts = {
391 | time_type: :range,
392 | capacity_type: :closed,
393 | bookable_across_occurrences: false
394 | }
395 | Bookable.initialize_acts_as_bookable_core
396 | expect(Bookable.booking_opts[:preset]).not_to be_present
397 | expect(Bookable.booking_opts[:date_type]).not_to be_present
398 | expect(Bookable.booking_opts[:time_type]).to be :range
399 | expect(Bookable.booking_opts[:location_type]).not_to be_present
400 | expect(Bookable.booking_opts[:capacity_type]).to be :closed
401 | expect(Bookable.booking_opts[:bookable_across_occurrences]).to be false
402 | end
403 |
404 | it 'should not allow unknown keys' do
405 | Bookable.booking_opts = {unknown: 'lol'}
406 | expect { Bookable.initialize_acts_as_bookable_core }.to raise_error ActsAsBookable::InitializationError
407 | begin
408 | Bookable.initialize_acts_as_bookable_core
409 | rescue ActsAsBookable::InitializationError => e
410 | expect(e.message).to include 'is not a valid option'
411 | end
412 | end
413 |
414 | it 'should not allow unknown values on :time_type' do
415 | Bookable.booking_opts = {time_type: :unknown}
416 | expect { Bookable.initialize_acts_as_bookable_core }.to raise_error ActsAsBookable::InitializationError
417 | begin
418 | Bookable.initialize_acts_as_bookable_core
419 | rescue ActsAsBookable::InitializationError => e
420 | expect(e.message).to include 'is not a valid value for time_type'
421 | end
422 | end
423 |
424 | it 'should not allow unknown values on :capacity_type' do
425 | Bookable.booking_opts = {capacity_type: :unknown}
426 | expect { Bookable.initialize_acts_as_bookable_core }.to raise_error ActsAsBookable::InitializationError
427 | begin
428 | Bookable.initialize_acts_as_bookable_core
429 | rescue ActsAsBookable::InitializationError => e
430 | expect(e.message).to include 'is not a valid value for capacity_type'
431 | end
432 | end
433 |
434 | it 'should not allow unknown values on bookable_across_occurrences' do
435 | Bookable.booking_opts = {bookable_across_occurrences: :unknown}
436 | expect { Bookable.initialize_acts_as_bookable_core }.to raise_error ActsAsBookable::InitializationError
437 | begin
438 | Bookable.initialize_acts_as_bookable_core
439 | rescue ActsAsBookable::InitializationError => e
440 | expect(e.message).to include 'is not a valid value for bookable_across_occurrences'
441 | end
442 | end
443 | end
444 | end
445 |
446 | describe 'self.validate_booking_options!' do
447 | before(:each) do
448 | Bookable.booking_opts = {
449 | time_type: :none,
450 | capacity_type: :none
451 | }
452 | @opts = {}
453 | end
454 |
455 | describe 'with capacity_type: :none and time_type: :none' do
456 | it 'validates with default options' do
457 | expect(Bookable.validate_booking_options!(@opts)).to be_truthy
458 | end
459 | end
460 |
461 | describe 'with time_type = ' do
462 | describe ':range' do
463 | before(:each) do
464 | Bookable.booking_opts[:time_type] = :range
465 | Bookable.initialize_acts_as_bookable_core
466 | @opts[:time_start] = Time.now + 1.hour
467 | @opts[:time_end] = Time.now + 4.hours
468 | end
469 |
470 | it 'validates with all options fields set' do
471 | expect(Bookable.validate_booking_options!(@opts)).to be_truthy
472 | end
473 |
474 | it 'requires time_start as Time' do
475 | @opts[:time_start] = nil
476 | expect{ Bookable.validate_booking_options!(@opts) }.to raise_error ActsAsBookable::OptionsInvalid
477 | @opts[:time_start] = 'String'
478 | expect{ Bookable.validate_booking_options!(@opts) }.to raise_error ActsAsBookable::OptionsInvalid
479 | end
480 |
481 | it 'requires time_end as Time' do
482 | @opts[:time_end] = nil
483 | expect{ Bookable.validate_booking_options!(@opts) }.to raise_error ActsAsBookable::OptionsInvalid
484 | @opts[:time_end] = 'String'
485 | expect{ Bookable.validate_booking_options!(@opts) }.to raise_error ActsAsBookable::OptionsInvalid
486 | end
487 |
488 | it 'doesn\'t accept a fixed time' do
489 | @opts[:time] = Time.now
490 | expect{ Bookable.validate_booking_options!(@opts) }.to raise_error ActsAsBookable::OptionsInvalid
491 | end
492 | end
493 |
494 | describe ':fixed' do
495 | before(:each) do
496 | Bookable.booking_opts[:time_type] = :fixed
497 | Bookable.initialize_acts_as_bookable_core
498 | @opts[:time] = Time.now + 1.hour
499 | end
500 |
501 | it 'validates with the right fields set' do
502 | expect(Bookable.validate_booking_options!(@opts)).to be_truthy
503 | end
504 |
505 | it 'requires date as Time' do
506 | @opts[:time] = nil
507 | expect{ Bookable.validate_booking_options!(@opts) }.to raise_error ActsAsBookable::OptionsInvalid
508 | @opts[:time] = 'String'
509 | expect{ Bookable.validate_booking_options!(@opts) }.to raise_error ActsAsBookable::OptionsInvalid
510 | end
511 |
512 | it 'doesn\'t accept time_start' do
513 | @opts[:time_start] = Time.now + 13
514 | expect{ Bookable.validate_booking_options!(@opts) }.to raise_error ActsAsBookable::OptionsInvalid
515 | end
516 |
517 | it 'doesn\'t accept time_end' do
518 | @opts[:time_end] = Time.now + 15
519 | expect{ Bookable.validate_booking_options!(@opts) }.to raise_error ActsAsBookable::OptionsInvalid
520 | end
521 | end
522 |
523 | describe ':none' do
524 | before(:each) do
525 | Bookable.initialize_acts_as_bookable_core
526 | end
527 |
528 | it 'doesn\'t accept time' do
529 | @opts[:time] = Time.now + 13
530 | expect{ Bookable.validate_booking_options!(@opts) }.to raise_error ActsAsBookable::OptionsInvalid
531 | end
532 |
533 | it 'doesn\'t accept time_start' do
534 | @opts[:time_start] = Time.now + 13
535 | expect{ Bookable.validate_booking_options!(@opts) }.to raise_error ActsAsBookable::OptionsInvalid
536 | end
537 |
538 | it 'doesn\'t accept time_end' do
539 | @opts[:time_end] = Time.now + 15
540 | expect{ Bookable.validate_booking_options!(@opts) }.to raise_error ActsAsBookable::OptionsInvalid
541 | end
542 | end
543 | end
544 |
545 | describe 'with capacity_type = ' do
546 | describe ':open' do
547 | before(:each) do
548 | Bookable.booking_opts[:capacity_type] = :open
549 | Bookable.initialize_acts_as_bookable_core
550 | @opts[:amount] = 2
551 | end
552 |
553 | it 'validates with all options fields set' do
554 | expect(Bookable.validate_booking_options!(@opts)).to be_truthy
555 | end
556 |
557 | it 'requires :amount as integer' do
558 | @opts[:amount] = nil
559 | expect{ Bookable.validate_booking_options!(@opts) }.to raise_error ActsAsBookable::OptionsInvalid
560 | end
561 | end
562 |
563 | describe ':closed' do
564 | before(:each) do
565 | Bookable.booking_opts[:capacity_type] = :closed
566 | Bookable.initialize_acts_as_bookable_core
567 | @opts[:amount] = 2
568 | end
569 |
570 | it 'validates with all options fields set' do
571 | expect(Bookable.validate_booking_options!(@opts)).to be_truthy
572 | end
573 |
574 | it 'requires :amount as integer' do
575 | @opts[:amount] = nil
576 | expect{ Bookable.validate_booking_options!(@opts) }.to raise_error ActsAsBookable::OptionsInvalid
577 | end
578 | end
579 |
580 | describe ':none' do
581 | before(:each) do
582 | Bookable.initialize_acts_as_bookable_core
583 | end
584 |
585 | it 'doesn\'t accept amount' do
586 | @opts[:amount] = 2.3
587 | expect{ Bookable.validate_booking_options!(@opts) }.to raise_error ActsAsBookable::OptionsInvalid
588 | end
589 | end
590 | end
591 | end
592 | end
593 | end
594 |
--------------------------------------------------------------------------------
/spec/acts_as_bookable/bookable_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe 'Bookable model' do
4 | before(:each) do
5 | @bookable = build(:bookable)
6 | end
7 |
8 | describe 'conditional validations' do
9 | it 'should be valid with all required fields set' do
10 | expect(@bookable).to be_valid
11 | end
12 |
13 | it 'should save a bookable' do
14 | expect(@bookable.save).to be_truthy
15 | end
16 |
17 | describe 'when capacity is required' do
18 | before(:each) do
19 | Bookable.booking_opts[:capacity_type] = :closed
20 | Bookable.initialize_acts_as_bookable_core
21 | end
22 | after(:all) do
23 | Bookable.booking_opts = {}
24 | Bookable.initialize_acts_as_bookable_core
25 | end
26 |
27 | it 'should not validate with capacity < 0 if capacity is required' do
28 | @bookable.capacity = -1
29 | expect(@bookable.valid?).to be_falsy
30 | end
31 |
32 | it 'should not validate without capacity' do
33 | @bookable.capacity = nil
34 | expect(@bookable.valid?).to be_falsy
35 | end
36 | end
37 |
38 | describe 'when capacity is not required' do
39 | before(:each) do
40 | Bookable.booking_opts[:capacity_type] = :none
41 | Bookable.initialize_acts_as_bookable_core
42 | end
43 | after(:all) do
44 | Bookable.booking_opts = {}
45 | Bookable.initialize_acts_as_bookable_core
46 | end
47 |
48 | it 'should validate with capacity < 0' do
49 | @bookable.capacity = -1
50 | expect(@bookable.valid?).to be_truthy
51 | end
52 |
53 | it 'should validate without capacity if it\'s not required' do
54 | @bookable.capacity = nil
55 | expect(@bookable.valid?).to be_truthy
56 | end
57 | end
58 |
59 | describe 'when schedule is required' do
60 | before(:each) do
61 | Bookable.booking_opts[:time_type] = :range
62 | Bookable.initialize_acts_as_bookable_core
63 | end
64 | after(:all) do
65 | Bookable.booking_opts = {}
66 | Bookable.initialize_acts_as_bookable_core
67 | end
68 |
69 | it 'should not validate without schedule' do
70 | @bookable.schedule = nil
71 | expect(@bookable.valid?).to be_falsy
72 | end
73 | end
74 |
75 | describe 'when schedule is not required' do
76 | before(:each) do
77 | Bookable.booking_opts[:time_type] = :none
78 | Bookable.initialize_acts_as_bookable_core
79 | end
80 | after(:all) do
81 | Bookable.booking_opts = {}
82 | Bookable.initialize_acts_as_bookable_core
83 | end
84 |
85 | it 'should validate without schedule if it\'s not required' do
86 | @bookable.schedule = nil
87 | expect(@bookable.valid?).to be_truthy
88 | end
89 | end
90 | end
91 |
92 |
93 | describe 'has_many :bookings' do
94 | before(:each) do
95 | @bookable.save!
96 | booker1 = create(:booker, name: 'Booker 1')
97 | booker2 = create(:booker, name: 'Booker 2')
98 | booking1 = ActsAsBookable::Booking.create!(booker: booker1, bookable: @bookable)
99 | booking2 = ActsAsBookable::Booking.create!(booker: booker1, bookable: @bookable)
100 | @bookable.reload
101 | end
102 |
103 | it 'should have many bookings' do
104 | expect(@bookable.bookings).to be_present
105 | expect(@bookable.bookings.count).to eq 2
106 | end
107 |
108 | it 'dependent: :destroy' do
109 | count = ActsAsBookable::Booking.count
110 | @bookable.destroy
111 | expect(ActsAsBookable::Booking.count).to eq count -2
112 | end
113 | end
114 |
115 | describe '#schedule' do
116 | it 'allows for creation of a bookable with a IceCube schedule' do
117 | schedule = IceCube::Schedule.new
118 | # Every Monday,Tuesday and Friday
119 | schedule.add_recurrence_rule IceCube::Rule.weekly.day(:monday, :tuesday, :friday)
120 | @bookable.schedule = schedule
121 | expect(@bookable.save).to be_truthy
122 | end
123 | end
124 | end
125 |
--------------------------------------------------------------------------------
/spec/acts_as_bookable/booker_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe 'Booker model' do
4 | before(:each) do
5 | @booker = build(:booker)
6 | end
7 |
8 | it 'should be valid with all required fields set' do
9 | expect(@booker).to be_valid
10 | end
11 |
12 | it 'should save a booker' do
13 | expect(@booker.save).to be_truthy
14 | end
15 |
16 | describe 'has_many :bookings' do
17 | before(:each) do
18 | @booker.save!
19 | bookable1 = create(:bookable)
20 | bookable2 = create(:bookable)
21 | booking1 = ActsAsBookable::Booking.create(bookable: bookable1, booker: @booker)
22 | booking2 = ActsAsBookable::Booking.create(bookable: bookable2, booker: @booker)
23 | @booker.reload
24 | end
25 |
26 | it 'should have many bookings' do
27 | expect(@booker.bookings).to be_present
28 | expect(@booker.bookings.count).to eq 2
29 | end
30 |
31 | it 'dependent: :destroy' do
32 | count = ActsAsBookable::Booking.count
33 | @booker.destroy
34 | expect(ActsAsBookable::Booking.count).to eq count -2
35 | end
36 | end
37 |
38 | describe '#book!' do
39 | before(:each) do
40 | @bookable = create(:room)
41 |
42 | end
43 |
44 | it 'should respond to #book!' do
45 | expect(@booker).to respond_to :book!
46 | end
47 |
48 | it 'should create a new booking' do
49 | count = @booker.bookings.count
50 | new_booking = @booker.book!(@bookable, time_start: Date.today, time_end: Date.today + 1.day, amount: 2)
51 | expect(@booker.bookings.count).to eq count+1
52 | expect(new_booking.class.to_s).to eq "ActsAsBookable::Booking"
53 | end
54 |
55 | it 'new booking should have all fields set' do
56 | new_booking = @booker.book!(@bookable, time_start: Date.today, time_end: Date.today + 1.day, amount: 2)
57 | new_booking.reload
58 | expect(new_booking.time_start).to be_present
59 | expect(new_booking.time_end).to be_present
60 | expect(new_booking.amount).to be_present
61 | end
62 |
63 | it 'should raise ActiveRecord::RecordInvalid if new booking is not valid' do
64 | expect{ @booker.book!(Generic.new) }.to raise_error ActiveRecord::RecordInvalid
65 | end
66 |
67 | it 'should not create a new booking if it\'s not valid' do
68 | count = @booker.bookings.count
69 | begin
70 | @booker.book!(Generic.new)
71 | rescue ActiveRecord::RecordInvalid => er
72 | end
73 | expect(@booker.bookings.count).to eq count
74 | end
75 |
76 | it 'should raise ActsAsBookable::AvailabilityError if the bookable is not available' do
77 | @booker.book!(@bookable, time_start: Date.today, time_end: Date.today + 1.day, amount: 2)
78 | expect{ @booker.book!(@bookable, time_start: Date.today, time_end: Date.today + 1.day, amount: 2)}.to raise_error ActsAsBookable::AvailabilityError
79 | end
80 | end
81 |
82 |
83 |
84 | # describe 'Booker Method Generation' do
85 | # before :each do
86 | # Generic.acts_as_booker
87 | # @booker = Generic.new()
88 | # end
89 | #
90 | # it "should responde 'true' to booker?" do
91 | # expect(@booker.class).to be_booker
92 | # end
93 | # end
94 | #
95 | # describe 'class configured as Booker' do
96 | # before(:each) do
97 | # @booker = Booker.new
98 | # end
99 | #
100 | # it 'should add #booker? query method to the class-side' do
101 | # expect(Booker).to respond_to(:booker?)
102 | # end
103 | #
104 | # it 'should return true from the class-side #booker?' do
105 | # expect(Booker.booker?).to be_truthy
106 | # end
107 | #
108 | # it 'should return false from the base #booker?' do
109 | # expect(ActiveRecord::Base.booker?).to be_falsy
110 | # end
111 | #
112 | # it 'should add #booker? query method to the singleton' do
113 | # expect(@booker).to respond_to(:booker?)
114 | # end
115 | #
116 | # it 'should add #booker? query method to the instance-side' do
117 | # expect(@booker).to respond_to(:booker?)
118 | # end
119 | #
120 | # it 'should add #booker? query method to the instance-side' do
121 | # expect(@booker.booker?).to be_truthy
122 | # end
123 | #
124 | # # it 'should add #tag method on the instance-side' do
125 | # # expect(@booker).to respond_to(:tag)
126 | # # end
127 | #
128 | # # it 'should generate an association for #owned_taggings and #owned_tags' do
129 | # # expect(@booker).to respond_to(:owned_taggings, :owned_tags)
130 | # # end
131 | # end
132 | #
133 | # describe 'Reloading' do
134 | # it 'should save a model instantiated by Model.find' do
135 | # booker = Generic.create!(name: 'Booker')
136 | # found_booker = Generic.find(booker.id)
137 | # expect(found_booker.save).to eq true
138 | # end
139 | # end
140 | end
141 |
--------------------------------------------------------------------------------
/spec/acts_as_bookable/booking_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe 'Booking model' do
4 | before(:each) do
5 | @booking = ActsAsBookable::Booking.new(amount: 2)
6 | @booker = create(:booker)
7 | @bookable = create(:bookable)
8 | @booking.booker = @booker
9 | @booking.bookable = @bookable
10 | end
11 |
12 | it 'should be valid with all required fields set' do
13 | expect(@booking).to be_valid
14 | end
15 |
16 | it 'should save a booking' do
17 | expect(@booking.save).to be_truthy
18 | end
19 |
20 | it 'should not be valid without a booker' do
21 | @booking.booker = nil
22 | expect(@booking).not_to be_valid
23 | end
24 |
25 | it 'should not be valid without a bookable' do
26 | @booking.bookable = nil
27 | expect(@booking).not_to be_valid
28 | end
29 |
30 | it 'should not be valid if booking.booker.booker? is false' do
31 | not_booker = Generic.create(name: 'New generic model')
32 | @booking.booker = not_booker
33 | expect(@booking).not_to be_valid
34 | expect(@booking.errors.messages[:booker]).to be_present
35 | expect(@booking.errors.messages[:booker][0]).to include "Generic"
36 | expect(@booking.errors.messages).not_to include "missing translation"
37 | end
38 |
39 | it 'should not be valid if booking.bookable.bookable? is false' do
40 | bookable = Generic.create(name: 'New generic model')
41 | @booking.bookable = bookable
42 | expect(@booking).not_to be_valid
43 | expect(@booking.errors.messages[:bookable]).to be_present
44 | expect(@booking.errors.messages[:bookable][0]).to include "Generic"
45 | expect(@booking.errors.messages).not_to include "missing translation"
46 | end
47 |
48 | it 'should belong to booker' do
49 | expect(@booking.booker.id).to eq @booker.id
50 | end
51 |
52 | it 'should belong to bookable' do
53 | expect(@booking.bookable.id).to eq @bookable.id
54 | end
55 |
56 | describe "overlapped scope" do
57 |
58 | describe "without time" do
59 | it "returns a booking without checking the time" do
60 | time = Date.today.to_time
61 | booking = ActsAsBookable::Booking.create!(time_start: nil, time_end: nil, time: time, bookable: @bookable, booker: @booker)
62 | opts = {}
63 | expect(ActsAsBookable::Booking.overlapped(@bookable,opts).count).to eq 1
64 | expect(ActsAsBookable::Booking.overlapped(@bookable,opts)[0].id).to eq booking.id
65 | end
66 |
67 | it "returns all the bookings without checking the time" do
68 | booking1 = ActsAsBookable::Booking.create!(time_start: nil, time_end: nil, time: Time.now, bookable: @bookable, booker: @booker)
69 | booking2 = ActsAsBookable::Booking.create!(time_start: Time.now, time_end: Time.now + 3.hours, time: nil, bookable: @bookable, booker: @booker)
70 | booking3 = ActsAsBookable::Booking.create!(time_start: Time.now - 10.days, time_end: Time.now - 9.days, time: nil, bookable: @bookable, booker: @booker)
71 | opts = {}
72 | expect(ActsAsBookable::Booking.overlapped(@bookable,opts).count).to eq 3
73 | expect(ActsAsBookable::Booking.overlapped(@bookable,opts).order(:id)[0].id).to eq booking1.id
74 | expect(ActsAsBookable::Booking.overlapped(@bookable,opts).order(:id)[1].id).to eq booking2.id
75 | expect(ActsAsBookable::Booking.overlapped(@bookable,opts).order(:id)[2].id).to eq booking3.id
76 | end
77 | end
78 |
79 | describe "with fixed time" do
80 | it "returns overlapped booking" do
81 | time = Date.today.to_time
82 | booking = ActsAsBookable::Booking.create!(time_start: nil, time_end: nil, time: time, bookable: @bookable, booker: @booker)
83 | opts = {time: time}
84 | expect(ActsAsBookable::Booking.overlapped(@bookable,opts).count).to eq 1
85 | expect(ActsAsBookable::Booking.overlapped(@bookable,opts)[0].id).to eq booking.id
86 | end
87 |
88 | it "returns all the overlapped bookings" do
89 | time = Date.today.to_time
90 | booking1 = ActsAsBookable::Booking.create!(time_start: nil, time_end: nil, time: time, bookable: @bookable, booker: @booker)
91 | booking2 = ActsAsBookable::Booking.create!(time_start: nil, time_end: nil, time: time, bookable: @bookable, booker: @booker)
92 | booking3 = ActsAsBookable::Booking.create!(time_start: nil, time_end: nil, time: time + 1.hour, bookable: @bookable, booker: @booker)
93 | opts = {time: time}
94 | expect(ActsAsBookable::Booking.overlapped(@bookable,opts).count).to eq 2
95 | expect(ActsAsBookable::Booking.overlapped(@bookable,opts).order(:id)[0].id).to eq booking1.id
96 | expect(ActsAsBookable::Booking.overlapped(@bookable,opts).order(:id)[1].id).to eq booking2.id
97 | end
98 |
99 | it "returns no overlapped booking if time is wrong" do
100 | time = Date.today.to_time
101 | booking = ActsAsBookable::Booking.create!(time_start: nil, time_end: nil, time: time, bookable: @bookable, booker: @booker)
102 | opts = {time: (time + 1.hour)}
103 | expect(ActsAsBookable::Booking.overlapped(@bookable,opts).count).to eq 0
104 | end
105 | end
106 |
107 | describe "with time range" do
108 | it "returns overlapped booking" do
109 | time_start = Date.today.to_time
110 | time_end = Date.today.to_time + 10.hours
111 | booking = ActsAsBookable::Booking.create!(time_start: time_start, time_end: time_end, time: nil, bookable: @bookable, booker: @booker)
112 | opts = { time_start: time_start, time_end: time_end }
113 | expect(ActsAsBookable::Booking.overlapped(@bookable,opts).count).to eq 1
114 | expect(ActsAsBookable::Booking.overlapped(@bookable,opts)[0].id).to eq booking.id
115 | end
116 |
117 | it "returns more overlapped bookings" do
118 | time_start = Date.today.to_time
119 | time_end = Date.today.to_time + 10.hours
120 | booking1 = ActsAsBookable::Booking.create!(time_start: time_start, time_end: time_end, time: nil, bookable: @bookable, booker: @booker)
121 | booking2 = ActsAsBookable::Booking.create!(time_start: time_start, time_end: time_end, time: nil, bookable: @bookable, booker: @booker)
122 | booking3 = ActsAsBookable::Booking.create!(time_start: time_start-10.days, time_end: time_end-10.days, time: nil, bookable: @bookable, booker: @booker)
123 | opts = { time_start: time_start, time_end: time_end }
124 | expect(ActsAsBookable::Booking.overlapped(@bookable,opts).count).to eq 2
125 | expect(ActsAsBookable::Booking.overlapped(@bookable,opts).order(:id)[0].id).to eq booking1.id
126 | expect(ActsAsBookable::Booking.overlapped(@bookable,opts).order(:id)[1].id).to eq booking2.id
127 | end
128 |
129 | it "doesn't return any booking if time is wrong" do
130 | time_start = Date.today.to_time
131 | time_end = Date.today.to_time + 10.hours
132 | booking = ActsAsBookable::Booking.create!(time_start: time_start - 10.days, time_end: time_end - 10.days, time: nil, bookable: @bookable, booker: @booker)
133 | opts = { time_start: time_start, time_end: time_end }
134 | expect(ActsAsBookable::Booking.overlapped(@bookable,opts).count).to eq 0
135 | end
136 |
137 | it "returns also a booking overlapping but with time_start outside interval" do
138 | time_start = Date.today.to_time
139 | time_end = Date.tomorrow.to_time
140 | booking = ActsAsBookable::Booking.create!(time_start: time_start - 10.hours, time_end: time_end, bookable: @bookable, booker: @booker)
141 | opts = { time_start: time_start, time_end: time_end }
142 | expect(ActsAsBookable::Booking.overlapped(@bookable,opts).count).to eq 1
143 | expect(ActsAsBookable::Booking.overlapped(@bookable,opts)[0].id).to eq booking.id
144 | end
145 |
146 | it "returns also a booking overlapping but with time_end outside interval" do
147 | time_start = Date.today.to_time
148 | time_end = Date.tomorrow.to_time
149 | booking = ActsAsBookable::Booking.create!(time_start: time_start, time_end: time_end + 10.hours, bookable: @bookable, booker: @booker)
150 | opts = { time_start: time_start, time_end: time_end }
151 | expect(ActsAsBookable::Booking.overlapped(@bookable,opts).count).to eq 1
152 | expect(ActsAsBookable::Booking.overlapped(@bookable,opts)[0].id).to eq booking.id
153 | end
154 |
155 | it "returns more overlapped bookings, some of them not completely overlapping" do
156 | time_start = Date.today.to_time
157 | time_end = Date.today.to_time + 10.hours
158 | booking1 = ActsAsBookable::Booking.create!(time_start: time_start - 5.hours, time_end: time_end - 5.hours, time: nil, bookable: @bookable, booker: @booker)
159 | booking2 = ActsAsBookable::Booking.create!(time_start: time_start + 5.hours, time_end: time_end + 5.hours, time: nil, bookable: @bookable, booker: @booker)
160 | booking3 = ActsAsBookable::Booking.create!(time_start: time_start + 2.hours, time_end: time_end - 2.hours, time: nil, bookable: @bookable, booker: @booker)
161 | booking4 = ActsAsBookable::Booking.create!(time_start: time_start-10.days, time_end: time_end-10.days, time: nil, bookable: @bookable, booker: @booker)
162 | booking4 = ActsAsBookable::Booking.create!(time_start: time_start+10.days, time_end: time_end+10.days, time: nil, bookable: @bookable, booker: @booker)
163 | opts = { time_start: time_start, time_end: time_end }
164 | expect(ActsAsBookable::Booking.overlapped(@bookable,opts).count).to eq 3
165 | expect(ActsAsBookable::Booking.overlapped(@bookable,opts).order(:id)[0].id).to eq booking1.id
166 | expect(ActsAsBookable::Booking.overlapped(@bookable,opts).order(:id)[1].id).to eq booking2.id
167 | expect(ActsAsBookable::Booking.overlapped(@bookable,opts).order(:id)[2].id).to eq booking3.id
168 | end
169 |
170 | describe "should handle limit cases" do
171 | before :each do
172 | @time_start = Date.today.to_time
173 | @time_end = Date.today.to_time + 10.hours
174 |
175 | @time_before_start = @time_start - 1.second
176 | @time_after_start = @time_start + 1.second
177 | @time_before_end = @time_end - 1.second
178 | @time_after_end = @time_end + 1.second
179 |
180 | @opts = { time_start: @time_start, time_end: @time_end }
181 | end
182 |
183 | it "excludes intervals with end before time_start and start after time_end" do
184 | booking1 = ActsAsBookable::Booking.create!(time_start: @time_start - 5.hours, time_end: @time_before_start, time: nil, bookable: @bookable, booker: @booker)
185 | booking2 = ActsAsBookable::Booking.create!(time_start: @time_after_end, time_end: @time_end + 5.hours, time: nil, bookable: @bookable, booker: @booker)
186 | expect(ActsAsBookable::Booking.overlapped(@bookable,@opts).count).to eq 0
187 | end
188 |
189 | it "excludes intervals with start matching exactly with time_end" do
190 | booking1 = ActsAsBookable::Booking.create!(time_start: @time_end, time_end: @time_end + 2.hours, time: nil, bookable: @bookable, booker: @booker)
191 | expect(ActsAsBookable::Booking.overlapped(@bookable,@opts).count).to eq 0
192 | end
193 |
194 | it "includes intervals with end after time_start" do
195 | booking1 = ActsAsBookable::Booking.create!(time_start: @time_start - 5.hours, time_end: @time_after_start, time: nil, bookable: @bookable, booker: @booker)
196 | expect(ActsAsBookable::Booking.overlapped(@bookable,@opts).count).to eq 1
197 | end
198 |
199 | it "includes intervals with end exactly at time_start" do
200 | booking1 = ActsAsBookable::Booking.create!(time_start: @time_start - 5.hours, time_end: @time_start, time: nil, bookable: @bookable, booker: @booker)
201 | expect(ActsAsBookable::Booking.overlapped(@bookable,@opts).count).to eq 1
202 | end
203 | end
204 |
205 | describe "should convert dates to times" do
206 | before :each do
207 | @time_start = Date.today
208 | @time_end = Date.tomorrow
209 |
210 | @time_before_start = @time_start - 1.second
211 | @time_after_start = @time_start + 1.second
212 | @time_before_end = @time_end - 1.second
213 | @time_after_end = @time_end + 1.second
214 |
215 | @opts = { time_start: @time_start, time_end: @time_end }
216 | end
217 |
218 | it "excludes intervals with end before time_start and start after time_end" do
219 | booking1 = ActsAsBookable::Booking.create!(time_start: @time_start - 5.hours, time_end: @time_before_start, time: nil, bookable: @bookable, booker: @booker)
220 | booking2 = ActsAsBookable::Booking.create!(time_start: @time_after_end, time_end: @time_end + 5.hours, time: nil, bookable: @bookable, booker: @booker)
221 | expect(ActsAsBookable::Booking.overlapped(@bookable,@opts).count).to eq 0
222 | end
223 |
224 | it "excludes intervals with start matching exactly with time_end" do
225 | booking1 = ActsAsBookable::Booking.create!(time_start: @time_end, time_end: @time_end + 2.hours, time: nil, bookable: @bookable, booker: @booker)
226 | expect(ActsAsBookable::Booking.overlapped(@bookable,@opts).count).to eq 0
227 | end
228 |
229 | it "includes intervals with end after time_start" do
230 | booking1 = ActsAsBookable::Booking.create!(time_start: @time_start - 5.hours, time_end: @time_after_start, time: nil, bookable: @bookable, booker: @booker)
231 | expect(ActsAsBookable::Booking.overlapped(@bookable,@opts).count).to eq 1
232 | end
233 |
234 | it "includes intervals with end exactly at time_start" do
235 | booking1 = ActsAsBookable::Booking.create!(time_start: @time_start - 5.hours, time_end: @time_start, time: nil, bookable: @bookable, booker: @booker)
236 | expect(ActsAsBookable::Booking.overlapped(@bookable,@opts).count).to eq 1
237 | end
238 | end
239 | end
240 | end
241 | end
242 |
--------------------------------------------------------------------------------
/spec/acts_as_bookable/schedule_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe 'Schedules' do
4 | describe "Room schedules" do
5 | before :each do
6 | @test_from = '2016-02-01'.to_date # it's a monday
7 | @schedule = IceCube::Schedule.new @test_from
8 | @schedule.add_recurrence_rule IceCube::Rule.weekly.day(:monday,:tuesday,:wednesday,:thursday,:friday)
9 | end
10 |
11 | it "Weekly, From monday to friday" do
12 | # Weekly, From monday to friday
13 | expect(@schedule.occurring_at?(@test_from)).to be true
14 | expect(@schedule.occurring_at?(@test_from + 1.day)).to be true
15 | expect(@schedule.occurring_at?(@test_from + 2.day)).to be true
16 | expect(@schedule.occurring_at?(@test_from + 3.day)).to be true
17 | expect(@schedule.occurring_at?(@test_from + 4.day)).to be true
18 | expect(@schedule.occurring_at?(@test_from + 5.day)).to be false
19 | expect(@schedule.occurring_at?(@test_from + 6.day)).to be false
20 | end
21 |
22 | it "doesn't match if not at exact minute" do
23 | expect(@schedule.occurring_at?(@test_from + 1.minute)).to be false
24 | end
25 |
26 | describe "with daily duration" do
27 | before :each do
28 | @test_from = '2016-02-01'.to_date # it's a monday
29 | @schedule = IceCube::Schedule.new @test_from
30 | @schedule.add_recurrence_rule IceCube::Rule.weekly.day(:monday,:tuesday,:wednesday,:thursday,:friday)
31 | @schedule.duration = 1.day
32 | end
33 |
34 | it "match with the exact minute" do
35 | # Weekly, From monday to friday
36 | expect(@schedule.occurring_at?(@test_from)).to be true
37 | end
38 |
39 | it "matches if not at exact minute" do
40 | expect(@schedule.occurring_at?(@test_from + 1.minute)).to be true
41 | end
42 |
43 | it "matches if at end of day" do
44 | expect(@schedule.occurring_at?(@test_from + 1.day - 1.minute)).to be true
45 | expect(@schedule.occurring_at?(@test_from + 1.day - 1.minute)).to be true
46 | expect(@schedule.occurring_at?(@test_from + 4.day - 1.second)).to be true
47 | expect(@schedule.occurring_at?(@test_from + 4.day - 1.second)).to be true
48 | end
49 |
50 | it "doesn't match at the first second of the first day not included" do
51 | expect(@schedule.occurring_at?(@test_from + 5.days)).to be false
52 | expect(@schedule.occurring_at?(@test_from + 5.days + 1.second)).to be false
53 | end
54 | end
55 |
56 | describe "except the first and the last day of the month" do
57 | before :each do
58 | @test_from = '2016-02-01'.to_date # it's a monday
59 | @schedule = IceCube::Schedule.new @test_from
60 | @schedule.add_recurrence_rule IceCube::Rule.weekly.day(:monday,:tuesday,:wednesday,:thursday,:friday)
61 | @schedule.add_exception_rule IceCube::Rule.monthly.day_of_month(1, -1)
62 | end
63 |
64 | it "doesn't match on the first of the month" do
65 | expect(@schedule.occurring_at?(@test_from)).to be false
66 | expect(@schedule.occurring_at?(@test_from.end_of_month)).to be false
67 | expect(@schedule.occurring_at?(@test_from + 1.month)).to be false
68 | expect(@schedule.occurring_at?((@test_from + 1.month).end_of_month)).to be false
69 | expect(@schedule.occurring_at?('2017-01-01'.to_date)).to be false # sunday
70 | expect(@schedule.occurring_at?('2017-01-01'.to_date + 1.second)).to be false # sunday
71 | expect(@schedule.occurring_at?('2017-01-01'.to_date + 1.day)).to be true # monday
72 | end
73 | end
74 | end
75 |
76 | describe "Gym schedules" do
77 | before :each do
78 | # Mon | Tue | Wed | Thu | Fri | Sat |Sun
79 | # 9-10 | 10-11 | 9-10 | 10-11 | | 14-15 |
80 | # 18:10-19:10 | 20-21 | 18:10-19:10 | 20-21 | | |
81 | # Except for the third saturday of the month
82 | @test_from = '2016-02-01'.to_date # it's a monday
83 | @schedule = IceCube::Schedule.new(@test_from, duration: 1.hour)
84 | @schedule.add_recurrence_rule IceCube::Rule.weekly.day(:monday,:wednesday).hour_of_day(9)
85 | @schedule.add_recurrence_rule IceCube::Rule.weekly.day(:monday,:wednesday).hour_of_day(18).minute_of_hour(10)
86 | @schedule.add_recurrence_rule IceCube::Rule.weekly.day(:tuesday,:thursday).hour_of_day(10,20)
87 | @schedule.add_recurrence_rule IceCube::Rule.weekly.day(:saturday).hour_of_day(14)
88 | @schedule.add_exception_rule IceCube::Rule.monthly.day_of_week(saturday: [3]).hour_of_day(14)
89 | @schedule.add_exception_rule IceCube::Rule.monthly.day_of_week(wednesday: [3]).hour_of_day(9)
90 | end
91 |
92 | it "matches at the occurrences" do
93 | (0...12).each do |i|
94 | minute = (i * 5).minutes
95 | expect(@schedule.occurring_at?(@test_from + 9.hours + minute)).to be true # monday
96 | expect(@schedule.occurring_at?(@test_from + 18.hours + 10.minutes + minute)).to be true # monday
97 | expect(@schedule.occurring_at?(@test_from + 5.days + 14.hours + minute)).to be true # tuesday
98 | end
99 | end
100 |
101 | it "doesn't match close to the occurrences" do
102 | expect(@schedule.occurring_at?(@test_from + 9.hours - 1.minute)).to be false # monday
103 | expect(@schedule.occurring_at?(@test_from + 10.hours)).to be false # monday
104 | expect(@schedule.occurring_at?(@test_from + 18.hours + 9.minutes)).to be false # monday
105 | expect(@schedule.occurring_at?(@test_from + 19.hours + 9.minutes)).to be true # monday
106 | expect(@schedule.occurring_at?(@test_from + 19.hours + 10.minutes)).to be false # monday
107 | end
108 |
109 | it "doesn't match the saturday of the third week of the month" do
110 | (0...12).each do |i|
111 | minute = (i * 5).minutes
112 | expect(@schedule.occurring_at?('2016-02-20'.to_date + 14.hours + minute)).to be false # monday
113 | end
114 | end
115 |
116 | describe "borderline matchings" do
117 | before :each do
118 | # Except the third wednesday of the month
119 | @test_from = '2016-02-01'.to_date # it's a monday
120 | @schedule = IceCube::Schedule.new(@test_from)
121 | @schedule.add_recurrence_rule IceCube::Rule.monthly.day_of_week(wednesday: [3])
122 | end
123 |
124 | it "matches the third wednesday of the month, in normal conditions" do
125 | expect(@schedule.occurring_at?('2016-02-17'.to_date)).to be true
126 | end
127 |
128 | it "matches the third wednesday of the month, if the month starts on friday" do
129 | expect(@schedule.occurring_at?('2016-04-20'.to_date)).to be true
130 | end
131 |
132 | it "doesn't match the wednesday of the third week of the month, if the month starts on friday" do
133 | expect(@schedule.occurring_at?('2016-04-13'.to_date)).to be false
134 | end
135 | end
136 | end
137 | end
138 |
--------------------------------------------------------------------------------
/spec/acts_as_bookable/time_utils_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe 'ActsAsBookable::TimeUtils' do
4 |
5 | describe '#time_in_interval?' do
6 | before :each do
7 | @interval_start = Time.now
8 | @interval_end = Time.now + 1.hour
9 | end
10 |
11 | describe 'returns true' do
12 | it 'when time is the interval_start' do
13 | time = @interval_start
14 | expect(ActsAsBookable::TimeUtils.time_in_interval?(time,@interval_start,@interval_end)).to eq true
15 | end
16 |
17 | it 'when time is bewteen interval_start and interval_end' do
18 | time = @interval_start + 5.minutes
19 | expect(ActsAsBookable::TimeUtils.time_in_interval?(time,@interval_start,@interval_end)).to eq true
20 | end
21 |
22 | it 'when time is very close to interval end' do
23 | time = @interval_end - 1.second
24 | expect(ActsAsBookable::TimeUtils.time_in_interval?(time,@interval_start,@interval_end)).to eq true
25 | end
26 |
27 | end
28 |
29 | describe 'returns false' do
30 | it 'when time is before interval_start' do
31 | time = @interval_start - 1.second
32 | expect(ActsAsBookable::TimeUtils.time_in_interval?(time,@interval_start,@interval_end)).to eq false
33 | end
34 |
35 | it 'when time is after interval_end' do
36 | time = @interval_end + 1.second
37 | expect(ActsAsBookable::TimeUtils.time_in_interval?(time,@interval_start,@interval_end)).to eq false
38 | end
39 |
40 | it 'when time is interval_end' do
41 | time = @interval_end
42 | expect(ActsAsBookable::TimeUtils.time_in_interval?(time,@interval_start,@interval_end)).to eq false
43 | end
44 | end
45 | end
46 |
47 | describe '#interval_in_schedule?' do
48 | before :each do
49 | @day0 = '2016-01-05'.to_date.to_time
50 | @schedule = IceCube::Schedule.new(@day0,duration: 1.day)
51 | @schedule.add_recurrence_rule IceCube::Rule.monthly.day_of_month(1,3,5,7)
52 | end
53 |
54 | describe 'returns true' do
55 | it 'when range starts and ends in the middle of an occurrence' do
56 | time_start = @day0 + 1.hour
57 | time_end = @day0 + 3.hours
58 | expect(ActsAsBookable::TimeUtils.interval_in_schedule?(@schedule,time_start,time_end)).to eq true
59 | end
60 |
61 | it 'when range starts and ends in the middle of another occurrence' do
62 | time_start = @day0 + 2.days + 1.hour
63 | time_end = @day0 + 2.days + 3.hours
64 | expect(ActsAsBookable::TimeUtils.interval_in_schedule?(@schedule,time_start,time_end)).to eq true
65 | end
66 |
67 | it 'when range starts at the beginning of an occurrence and ends at the end of the same occurence' do
68 | time_start = @day0
69 | time_end = @day0 + 1.day - 1.second
70 | expect(ActsAsBookable::TimeUtils.interval_in_schedule?(@schedule,time_start,time_end)).to eq true
71 | end
72 | end
73 |
74 | describe 'retuns false' do
75 | it 'when range starts and ends outside any occurrence' do
76 | time_start = '2016-01-15'.to_date
77 | time_end = time_start + 1.day
78 | expect(ActsAsBookable::TimeUtils.interval_in_schedule?(@schedule,time_start,time_end)).to eq false
79 | end
80 |
81 | it 'when range starts and ends outside any occurrence but contains an occurrence' do
82 | time_start = @day0 - 1.hour
83 | time_end = @day0 + 1.day + 1.hour
84 | expect(ActsAsBookable::TimeUtils.interval_in_schedule?(@schedule,time_start,time_end)).to eq false
85 | end
86 |
87 | it 'when range starts within an occurrence but ends outside it' do
88 | time_start = @day0 + 1.hour
89 | time_end = @day0 + 1.day + 1.hour
90 | expect(ActsAsBookable::TimeUtils.interval_in_schedule?(@schedule,time_start,time_end)).to eq false
91 | end
92 |
93 | it 'when range starts outside any occurrence but ends within an occurrence' do
94 | time_start = @day0 - 1.hour
95 | time_end = @day0 + 1.hour
96 | expect(ActsAsBookable::TimeUtils.interval_in_schedule?(@schedule,time_start,time_end)).to eq false
97 | end
98 |
99 | it 'when range starts within an occurrence and ends within a different occurrence' do
100 | time_start = @day0 + 1.hour
101 | time_end = @day0 + 2.days + 1.hour
102 | expect(ActsAsBookable::TimeUtils.interval_in_schedule?(@schedule,time_start,time_end)).to eq false
103 | end
104 |
105 | it 'when range starts within an occurrence and ends just after the end of the same occurrence' do
106 | time_start = @day0 + 1.hour
107 | time_end = @day0 + 1.day
108 | expect(ActsAsBookable::TimeUtils.interval_in_schedule?(@schedule,time_start,time_end)).to eq false
109 | end
110 | end
111 | end
112 |
113 | describe '#time_in_schedule?' do
114 | before :each do
115 | @day0 = '2016-01-05'.to_date
116 | @schedule = IceCube::Schedule.new(@day0,duration: 1.day)
117 | @schedule.add_recurrence_rule IceCube::Rule.monthly.day_of_month(1,3,5,7)
118 | end
119 |
120 | describe 'returns true' do
121 | it 'when time is at the beginning of an occurrence' do
122 | time = @day0
123 | expect(ActsAsBookable::TimeUtils.time_in_schedule?(@schedule,time)).to eq true
124 | end
125 |
126 | it 'when time is in the middle of an occurrence' do
127 | time = @day0 + 5.hours
128 | expect(ActsAsBookable::TimeUtils.time_in_schedule?(@schedule,time)).to eq true
129 | end
130 |
131 | it 'when time is at the end of an occurrence' do
132 | time = @day0 + 1.day - 1.second
133 | expect(ActsAsBookable::TimeUtils.time_in_schedule?(@schedule, time)).to eq true
134 | end
135 | end
136 |
137 | describe 'retuns false' do
138 | it 'when time is outside an occurrence' do
139 | time = '2016-01-15'.to_date
140 | expect(ActsAsBookable::TimeUtils.time_in_schedule?(@schedule, time)).to eq false
141 | end
142 |
143 | it 'when time is close to the end of an occurrence, but outside it' do
144 | time = @day0 + 1.day
145 | expect(ActsAsBookable::TimeUtils.time_in_schedule?(@schedule, time)).to eq false
146 | end
147 |
148 | it 'when time is close to the beginning of an occurrence, but outside it' do
149 | time = @day0 + 2.days - 1.second
150 | end
151 | end
152 | end
153 |
154 | describe '#subintervals' do
155 | before :each do
156 | @time = Time.now
157 | end
158 |
159 | it 'returns ArgumentError if called without an array' do
160 | expect{ ActsAsBookable::TimeUtils.subintervals(1) }.to raise_error ArgumentError
161 | end
162 |
163 | it 'returns ArgumentError if an interval has no time_start or time_end' do
164 | intervals = [
165 | {time_start: @time, time_end: @time + 1.hour},
166 | {time_start: @time}
167 | ]
168 | expect{ ActsAsBookable::TimeUtils.subintervals(1) }.to raise_error ArgumentError
169 | intervals = [
170 | {time_start: @time, time_end: @time + 1.hour},
171 | {time_end: @time}
172 | ]
173 | expect{ ActsAsBookable::TimeUtils.subintervals(1) }.to raise_error ArgumentError
174 | end
175 |
176 | it 'returns ArgumentError if time_start or time_end is not a Time or Date' do
177 | intervals = [
178 | {time_start: @time, time_end: 1}
179 | ]
180 | expect{ ActsAsBookable::TimeUtils.subintervals(1) }.to raise_error ArgumentError
181 | intervals = [
182 | {time_start: 2, time_end: @time + 1.hour}
183 | ]
184 | expect{ ActsAsBookable::TimeUtils.subintervals(1) }.to raise_error ArgumentError
185 | end
186 |
187 | it 'returns empty array if input is an empty array' do
188 | expect(ActsAsBookable::TimeUtils.subintervals([])).to eq []
189 | end
190 |
191 | # |----|
192 | # =>
193 | # |----|
194 | it 'returns a copy of the same interval if input is a single interval' do
195 | intervals = [
196 | {time_start: @time, time_end: @time + 1.hour}
197 | ]
198 | subintervals = ActsAsBookable::TimeUtils.subintervals(intervals)
199 | expect(subintervals.length).to eq 1
200 | expect(subintervals[0][:time_start]).to eq intervals[0][:time_start]
201 | expect(subintervals[0][:time_end]).to eq intervals[0][:time_end]
202 | end
203 |
204 | # |----| |----| |----|
205 | # =>
206 | # |----| |----| |----|
207 | it 'returns a copy of the same intervals if they are all separated' do
208 | intervals = [
209 | {time_start: @time, time_end: @time + 1.hour},
210 | {time_start: @time + 2.hours, time_end: @time + 3.hours},
211 | {time_start: @time + 4.hours, time_end: @time + 5.hours}
212 | ]
213 | subintervals = ActsAsBookable::TimeUtils.subintervals(intervals)
214 | expect(subintervals.length).to eq 3
215 | (0..2).each do |i|
216 | expect(subintervals[i][:time_start]).to eq intervals[i][:time_start]
217 | expect(subintervals[i][:time_end]).to eq intervals[i][:time_end]
218 | end
219 | end
220 |
221 | # |----|
222 | # |----|
223 | # |----|
224 | # =>
225 | # |----|
226 | # |----|
227 | # |----|
228 | it 'returns the sub-intervals sorted' do
229 | time0 = @time
230 | time1 = @time + 1.hour
231 | time2 = @time + 2.hours
232 | time3 = @time + 3.hours
233 | time4 = @time + 4.hours
234 | time5 = @time + 5.hours
235 | time6 = @time + 6.hours
236 |
237 | intervals = [
238 | {time_start: time4, time_end: time5},
239 | {time_start: time2, time_end: time3},
240 | {time_start: time0, time_end: time1}
241 | ]
242 | subintervals = ActsAsBookable::TimeUtils.subintervals(intervals)
243 | expect(subintervals.length).to eq 3
244 | expect(subintervals[0][:time_start]).to eq time0
245 | expect(subintervals[0][:time_end]).to eq time1
246 | expect(subintervals[1][:time_start]).to eq time2
247 | expect(subintervals[1][:time_end]).to eq time3
248 | expect(subintervals[2][:time_start]).to eq time4
249 | expect(subintervals[2][:time_end]).to eq time5
250 | end
251 |
252 | # |----|
253 | # |----|
254 | # |----|
255 | # =>
256 | # |----|
257 | it 'merges intervals if they have same time_start and time_end' do
258 | intervals = [
259 | {time_start: @time, time_end: @time + 1.hour},
260 | {time_start: @time, time_end: @time + 1.hour},
261 | {time_start: @time, time_end: @time + 1.hour}
262 | ]
263 | subintervals = ActsAsBookable::TimeUtils.subintervals(intervals)
264 | expect(subintervals.length).to eq 1
265 | expect(subintervals[0][:time_start]).to eq intervals[0][:time_start]
266 | expect(subintervals[0][:time_end]).to eq intervals[0][:time_end]
267 | end
268 |
269 | # |---|
270 | # |------|
271 | # =>
272 | # |---|
273 | # |--|
274 | it 'returns two intervals if input is 2 intervals with same time_start and different time_end' do
275 | time0 = @time
276 | time1 = @time + 1.hour
277 | time2 = @time + 2.hours
278 | intervals = [
279 | {time_start: time0, time_end: time1},
280 | {time_start: time0, time_end: time2}
281 | ]
282 | subintervals = ActsAsBookable::TimeUtils.subintervals(intervals)
283 | expect(subintervals.length).to eq 2
284 | expect(subintervals[0][:time_start]).to eq time0
285 | expect(subintervals[0][:time_end]).to eq time1
286 | expect(subintervals[1][:time_start]).to eq time1
287 | expect(subintervals[1][:time_end]).to eq time2
288 | end
289 |
290 | # |------|
291 | # |---|
292 | # =>
293 | # |--|
294 | # |---|
295 | it 'returns two intervals if input is 2 intervals with same time_end and different time_start' do
296 | time0 = @time
297 | time1 = @time + 1.hour
298 | time2 = @time + 2.hours
299 | intervals = [
300 | {time_start: time0, time_end: time2},
301 | {time_start: time1, time_end: time2}
302 | ]
303 | subintervals = ActsAsBookable::TimeUtils.subintervals(intervals)
304 | expect(subintervals.length).to eq 2
305 | expect(subintervals[0][:time_start]).to eq time0
306 | expect(subintervals[0][:time_end]).to eq time1
307 | expect(subintervals[1][:time_start]).to eq time1
308 | expect(subintervals[1][:time_end]).to eq time2
309 | end
310 |
311 | # |---------|
312 | # |---|
313 | # =>
314 | # |--|
315 | # |---|
316 | # |--|
317 | it 'returns three intervals if one includes another' do
318 | time0 = @time
319 | time1 = @time + 1.hour
320 | time2 = @time + 2.hours
321 | time3 = @time + 3.hours
322 | intervals = [
323 | {time_start: time0, time_end: time3},
324 | {time_start: time1, time_end: time2}
325 | ]
326 | subintervals = ActsAsBookable::TimeUtils.subintervals(intervals)
327 | expect(subintervals.length).to eq 3
328 | expect(subintervals[0][:time_start]).to eq time0
329 | expect(subintervals[0][:time_end]).to eq time1
330 | expect(subintervals[1][:time_start]).to eq time1
331 | expect(subintervals[1][:time_end]).to eq time2
332 | expect(subintervals[2][:time_start]).to eq time2
333 | expect(subintervals[2][:time_end]).to eq time3
334 | end
335 |
336 | # |---2---|
337 | # |------4------|
338 | # |----3------|
339 | # |----1----|
340 | # |----8----|
341 | # =>
342 | # |-5-|
343 | # |-9-|
344 | # |-7-|
345 | # |--4--|
346 | # |----9----|
347 | it 'correctly merges interval attributes' do
348 | time0 = @time
349 | time1 = @time + 1.hour
350 | time2 = @time + 2.hours
351 | time3 = @time + 3.hours
352 | time4 = @time + 4.hours
353 | time5 = @time + 5.hours
354 | time6 = @time + 6.hours
355 | intervals = [
356 | {time_start: time0, time_end: time2, attr: 2},
357 | {time_start: time1, time_end: time4, attr: 4},
358 | {time_start: time0, time_end: time3, attr: 3},
359 | {time_start: time5, time_end: time6, attr: 1},
360 | {time_start: time5, time_end: time6, attr: 8}
361 | ]
362 | subintervals = ActsAsBookable::TimeUtils.subintervals(intervals) do |a,b,op|
363 | if op == :open
364 | res = {attr: a[:attr] + b[:attr]}
365 | end
366 | if op == :close
367 | res = {attr: a[:attr] - b[:attr]}
368 | end
369 | res
370 | end
371 | expect(subintervals.length).to eq 5
372 | expect(subintervals[0][:time_start]).to eq time0
373 | expect(subintervals[0][:time_end]).to eq time1
374 | expect(subintervals[0][:attr]).to eq 5
375 | expect(subintervals[1][:time_start]).to eq time1
376 | expect(subintervals[1][:time_end]).to eq time2
377 | expect(subintervals[1][:attr]).to eq 9
378 | expect(subintervals[2][:time_start]).to eq time2
379 | expect(subintervals[2][:time_end]).to eq time3
380 | expect(subintervals[2][:attr]).to eq 7
381 | expect(subintervals[3][:time_start]).to eq time3
382 | expect(subintervals[3][:time_end]).to eq time4
383 | expect(subintervals[3][:attr]).to eq 4
384 | expect(subintervals[4][:time_start]).to eq time5
385 | expect(subintervals[4][:time_end]).to eq time6
386 | expect(subintervals[4][:attr]).to eq 9
387 | end
388 |
389 | # |---2---|
390 | # |------4------|
391 | # |----3------|
392 | # |----1----|
393 | # |----8----|
394 | # =>
395 | # |-5-|
396 | # |-9-|
397 | # |-7-|
398 | # |--4--|
399 | # |----9----|
400 | it 'correctly merges interval attributes handling dates and times' do
401 | time0 = Date.today.to_time
402 | time1 = time0 + 1
403 | time2 = time0 + 2
404 | time3 = time0 + 3
405 | time4 = time0 + 4.days + 1.hours
406 | time5 = time0 + 5.days + 1.hours
407 | time6 = time0 + 6.days + 1.hours
408 | intervals = [
409 | {time_start: time0, time_end: time2, attr: 2},
410 | {time_start: time1, time_end: time4, attr: 4},
411 | {time_start: time0, time_end: time3, attr: 3},
412 | {time_start: time5, time_end: time6, attr: 1},
413 | {time_start: time5, time_end: time6, attr: 8}
414 | ]
415 | subintervals = ActsAsBookable::TimeUtils.subintervals(intervals) do |a,b,op|
416 | if op == :open
417 | res = {attr: a[:attr] + b[:attr]}
418 | end
419 | if op == :close
420 | res = {attr: a[:attr] - b[:attr]}
421 | end
422 | res
423 | end
424 | expect(subintervals.length).to eq 5
425 | expect(subintervals[0][:time_start]).to eq time0
426 | expect(subintervals[0][:time_end]).to eq time1
427 | expect(subintervals[0][:attr]).to eq 5
428 | expect(subintervals[1][:time_start]).to eq time1
429 | expect(subintervals[1][:time_end]).to eq time2
430 | expect(subintervals[1][:attr]).to eq 9
431 | expect(subintervals[2][:time_start]).to eq time2
432 | expect(subintervals[2][:time_end]).to eq time3
433 | expect(subintervals[2][:attr]).to eq 7
434 | expect(subintervals[3][:time_start]).to eq time3
435 | expect(subintervals[3][:time_end]).to eq time4
436 | expect(subintervals[3][:attr]).to eq 4
437 | expect(subintervals[4][:time_start]).to eq time5
438 | expect(subintervals[4][:time_end]).to eq time6
439 | expect(subintervals[4][:attr]).to eq 9
440 | end
441 |
442 | # |---2---|
443 | # |---4---|
444 | # |---3---|
445 | # |---5---|
446 | # |---1---|
447 | # |---1---|
448 | # =>
449 | # |---6---|
450 | # |---10---|
451 | it 'merges 3 intervals partially matching' do
452 | time0 = @time
453 | time1 = @time + 1.hour
454 | time2 = @time + 2.hours
455 | intervals = [
456 | {time_start: time0, time_end: time1, attr: 2},
457 | {time_start: time1, time_end: time2, attr: 4},
458 | {time_start: time0, time_end: time1, attr: 3},
459 | {time_start: time1, time_end: time2, attr: 5},
460 | {time_start: time0, time_end: time1, attr: 1},
461 | {time_start: time1, time_end: time2, attr: 1}
462 | ]
463 | subintervals = ActsAsBookable::TimeUtils.subintervals(intervals) do |a,b,op|
464 | if op == :open
465 | res = {attr: a[:attr] + b[:attr]}
466 | end
467 | if op == :close
468 | res = {attr: a[:attr] - b[:attr]}
469 | end
470 | res
471 | end
472 | expect(subintervals.length).to eq 2
473 | expect(subintervals[0][:time_start]).to eq time0
474 | expect(subintervals[0][:time_end]).to eq time1
475 | expect(subintervals[0][:attr]).to eq 6
476 | expect(subintervals[1][:time_start]).to eq time1
477 | expect(subintervals[1][:time_end]).to eq time2
478 | expect(subintervals[1][:attr]).to eq 10
479 | end
480 |
481 | # |---2----|
482 | # |----4---|
483 | # |---3----|
484 | # |----5---|
485 | # |---1----|
486 | # |----1---|
487 | # =>
488 | # |-6-|
489 | # |-16-|
490 | # |-10-|
491 | it 'merges and split 3 intervals partially matching' do
492 | time0 = @time
493 | time1 = @time + 1.hour
494 | time2 = @time + 2.hours
495 | time3 = @time + 3.hours
496 | intervals = [
497 | {time_start: time0, time_end: time2, attr: 2},
498 | {time_start: time1, time_end: time3, attr: 4},
499 | {time_start: time0, time_end: time2, attr: 3},
500 | {time_start: time1, time_end: time3, attr: 5},
501 | {time_start: time0, time_end: time2, attr: 1},
502 | {time_start: time1, time_end: time3, attr: 1}
503 | ]
504 | subintervals = ActsAsBookable::TimeUtils.subintervals(intervals) do |a,b,op|
505 | if op == :open
506 | res = {attr: a[:attr] + b[:attr]}
507 | end
508 | if op == :close
509 | res = {attr: a[:attr] - b[:attr]}
510 | end
511 | res
512 | end
513 | expect(subintervals.length).to eq 3
514 | expect(subintervals[0][:time_start]).to eq time0
515 | expect(subintervals[0][:time_end]).to eq time1
516 | expect(subintervals[0][:attr]).to eq 6
517 | expect(subintervals[1][:time_start]).to eq time1
518 | expect(subintervals[1][:time_end]).to eq time2
519 | expect(subintervals[1][:attr]).to eq 16
520 | expect(subintervals[2][:time_start]).to eq time2
521 | expect(subintervals[2][:time_end]).to eq time3
522 | expect(subintervals[2][:attr]).to eq 10
523 | end
524 | end
525 | end
526 |
--------------------------------------------------------------------------------
/spec/factories/bookable.rb:
--------------------------------------------------------------------------------
1 | FactoryGirl.define do
2 | factory :bookable, class: 'Bookable' do
3 | name 'Bookable name'
4 | capacity 4
5 | schedule IceCube::Schedule.new
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/factories/booker.rb:
--------------------------------------------------------------------------------
1 | FactoryGirl.define do
2 | factory :booker, class: 'Booker' do
3 | name 'Booker name'
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/spec/factories/room.rb:
--------------------------------------------------------------------------------
1 | FactoryGirl.define do
2 | factory :room, class: 'Room' do
3 | name 'Room name'
4 | capacity 4
5 | schedule {
6 | schedule = IceCube::Schedule.new(Date.today, duration: 1.day)
7 | schedule.add_recurrence_rule IceCube::Rule.daily
8 | schedule
9 | }
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/spec/internal/app/models/Bookable.rb:
--------------------------------------------------------------------------------
1 | class Bookable < ActiveRecord::Base
2 | acts_as_bookable
3 | end
4 |
--------------------------------------------------------------------------------
/spec/internal/app/models/Booker.rb:
--------------------------------------------------------------------------------
1 | class Booker < ActiveRecord::Base
2 | acts_as_booker
3 | end
4 |
--------------------------------------------------------------------------------
/spec/internal/app/models/Event.rb:
--------------------------------------------------------------------------------
1 | class Event < ActiveRecord::Base
2 | acts_as_bookable preset: :event
3 | end
4 |
--------------------------------------------------------------------------------
/spec/internal/app/models/Generic.rb:
--------------------------------------------------------------------------------
1 | class Generic < ActiveRecord::Base
2 | end
3 |
--------------------------------------------------------------------------------
/spec/internal/app/models/NotBooker.rb:
--------------------------------------------------------------------------------
1 | class NotBooker < ActiveRecord::Base
2 | end
3 |
--------------------------------------------------------------------------------
/spec/internal/app/models/Room.rb:
--------------------------------------------------------------------------------
1 | class Room < ActiveRecord::Base
2 | acts_as_bookable preset: :room
3 | end
4 |
--------------------------------------------------------------------------------
/spec/internal/app/models/Show.rb:
--------------------------------------------------------------------------------
1 | class Show < ActiveRecord::Base
2 | acts_as_bookable preset: :show
3 | end
4 |
--------------------------------------------------------------------------------
/spec/internal/app/models/Unbookable.rb:
--------------------------------------------------------------------------------
1 | class Unbookable < ActiveRecord::Base
2 | end
3 |
--------------------------------------------------------------------------------
/spec/internal/config/database.yml.sample:
--------------------------------------------------------------------------------
1 | sqlite3:
2 | adapter: sqlite3
3 | database: ':memory:'
4 |
5 | mysql:
6 | adapter: mysql2
7 | host: localhost
8 | username: root
9 | database: acts_as_bookable
10 | charset: utf8
11 |
12 | postgresql:
13 | adapter: postgresql
14 | hostname: localhost
15 | username: postgres
16 | database: acts_as_bookable
17 | encoding: utf8
18 |
--------------------------------------------------------------------------------
/spec/internal/db/schema.rb:
--------------------------------------------------------------------------------
1 | ActiveRecord::Schema.define version: 0 do
2 | create_table :acts_as_bookable_bookings, force: true do |t|
3 | t.references :bookable, polymorphic: true, index: {name: "index_acts_as_bookable_bookings_bookable"}
4 | t.references :booker, polymorphic: true, index: {name: "index_acts_as_bookable_bookings_booker"}
5 | t.column :amount, :integer
6 | t.column :schedule, :text
7 | t.column :time_start, :datetime
8 | t.column :time_end, :datetime
9 | t.column :time, :datetime
10 | t.datetime :created_at
11 | end
12 |
13 | create_table :bookables, force: true do |t|
14 | t.column :name, :string
15 | t.column :schedule, :text
16 | t.column :capacity, :integer
17 | end
18 |
19 | create_table :rooms, force: true do |t|
20 | t.column :name, :string
21 | t.column :schedule, :text
22 | t.column :capacity, :integer
23 | end
24 |
25 | create_table :events, force: true do |t|
26 | t.column :name, :string
27 | t.column :capacity, :integer
28 | end
29 |
30 | create_table :shows, force: true do |t|
31 | t.column :name, :string
32 | t.column :schedule, :text
33 | t.column :capacity, :integer
34 | end
35 |
36 | create_table :unbookables, force: true do |t|
37 | t.column :name, :string
38 | end
39 |
40 | create_table :not_bookers, force: true do |t|
41 | t.column :name, :string
42 | end
43 |
44 | create_table :generics, force: true do |t|
45 | t.column :name, :string
46 | end
47 |
48 | create_table :bookers, force: true do |t|
49 | t.column :name, :string
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require 'coveralls'
2 | Coveralls.wear!
3 |
4 | begin
5 | require 'pry-nav'
6 | rescue LoadError
7 | end
8 | $LOAD_PATH << '.' unless $LOAD_PATH.include?('.')
9 | $LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__))
10 | require 'logger'
11 |
12 | require File.expand_path('../../lib/acts_as_bookable', __FILE__)
13 | I18n.enforce_available_locales = true
14 | require 'rails'
15 | require 'barrier'
16 | require 'database_cleaner'
17 | require 'factory_girl_rails'
18 | require 'awesome_print'
19 |
20 | ENGINE_RAILS_ROOT=File.join(File.dirname(__FILE__), '../')
21 | Dir[File.join(ENGINE_RAILS_ROOT, "spec/factories/**/*.rb")].each {|f| require f }
22 |
23 | Dir['./spec/support/**/*.rb'].sort.each { |f| require f }
24 |
25 | I18n.load_path << File.expand_path("../../config/locales/en.yml", __FILE__)
26 |
27 | RSpec.configure do |config|
28 | config.raise_errors_for_deprecations!
29 | end
30 |
--------------------------------------------------------------------------------
/spec/support/0-helpers.rb:
--------------------------------------------------------------------------------
1 | def using_sqlite?
2 | ActsAsBookable::Utils.connection && ActsAsBookable::Utils.connection.adapter_name == 'SQLite'
3 | end
4 |
5 | def supports_concurrency?
6 | !using_sqlite?
7 | end
8 |
9 | def using_postgresql?
10 | ActsAsBookable::Utils.using_postgresql?
11 | end
12 |
13 | def postgresql_version
14 | if using_postgresql?
15 | ActsAsBookable::Utils.connection.execute('SHOW SERVER_VERSION').first['server_version'].to_f
16 | else
17 | 0.0
18 | end
19 | end
20 |
21 | def postgresql_support_json?
22 | postgresql_version >= 9.2
23 | end
24 |
25 |
26 | def using_mysql?
27 | ActsAsBookable::Utils.using_mysql?
28 | end
29 |
30 | def using_case_insensitive_collation?
31 | using_mysql? && ActsAsBookable::Utils.connection.collation =~ /_ci\Z/
32 | end
33 |
--------------------------------------------------------------------------------
/spec/support/1-database.rb:
--------------------------------------------------------------------------------
1 | # set adapter to use, default is sqlite3
2 | # to use an alternative adapter run => rake spec DB='postgresql'
3 | db_name = ENV['DB'] || 'sqlite3'
4 | database_yml = File.expand_path('../../internal/config/database.yml', __FILE__)
5 |
6 | if File.exist?(database_yml)
7 |
8 | ActiveRecord::Migration.verbose = false
9 | ActiveRecord::Base.default_timezone = :utc
10 | ActiveRecord::Base.configurations = YAML.load_file(database_yml)
11 | ActiveRecord::Base.logger = Logger.new(File.join(File.dirname(__FILE__), '../debug.log'))
12 | ActiveRecord::Base.logger.level = ENV['TRAVIS'] ? ::Logger::ERROR : ::Logger::DEBUG
13 | config = ActiveRecord::Base.configurations[db_name]
14 |
15 | begin
16 | #activerecord 4 uses symbol
17 | #TODO, remove when activerecord 3 support is dropped
18 | if ActsAsBookable::Utils.active_record4? || ActsAsBookable::Utils.active_record5?
19 | ActiveRecord::Base.establish_connection(db_name.to_sym)
20 | else
21 | ActiveRecord::Base.establish_connection(db_name)
22 | end
23 | ActiveRecord::Base.connection
24 | rescue
25 | case db_name
26 | when /mysql/
27 | ActiveRecord::Base.establish_connection(config.merge('database' => nil))
28 | ActiveRecord::Base.connection.create_database(config['database'], {charset: 'utf8', collation: 'utf8_unicode_ci'})
29 | when 'postgresql'
30 | ActiveRecord::Base.establish_connection(config.merge('database' => 'postgres', 'schema_search_path' => 'public'))
31 | ActiveRecord::Base.connection.create_database(config['database'], config.merge('encoding' => 'utf8'))
32 | end
33 |
34 | ActiveRecord::Base.establish_connection(config)
35 | end
36 |
37 | load(File.dirname(__FILE__) + '/../internal/db/schema.rb')
38 | # load(File.dirname(__FILE__) + '/../internal/app/models/models.rb')
39 | Dir['./spec/internal/app/models/*.rb'].sort.each { |f| load f }
40 | else
41 | fail "Please create #{database_yml} first to configure your database. Take a look at: #{database_yml}.sample"
42 | end
43 |
--------------------------------------------------------------------------------
/spec/support/2-database_cleaner.rb:
--------------------------------------------------------------------------------
1 | RSpec.configure do |config|
2 |
3 | config.before(:suite) do
4 | DatabaseCleaner.clean_with(:truncation)
5 | DatabaseCleaner.strategy = :transaction
6 | DatabaseCleaner.clean
7 | end
8 |
9 | config.after(:suite) do
10 | DatabaseCleaner.clean
11 | end
12 |
13 | config.before(:each) do
14 | DatabaseCleaner.start
15 | end
16 |
17 | config.after(:each) do
18 | DatabaseCleaner.clean
19 | end
20 |
21 | end
22 |
--------------------------------------------------------------------------------
/spec/support/3-factory_girl.rb:
--------------------------------------------------------------------------------
1 | RSpec.configure do |config|
2 | config.include FactoryGirl::Syntax::Methods
3 |
4 | config.before(:suite) do
5 | begin
6 | DatabaseCleaner.start
7 | FactoryGirl.lint
8 | ensure
9 | DatabaseCleaner.clean
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------