acts_as_bookable 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +2 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +43 -0
  6. data/Appraisals +21 -0
  7. data/Gemfile +21 -0
  8. data/Gemfile.lock +168 -0
  9. data/Guardfile +5 -0
  10. data/MIT-LICENSE +20 -0
  11. data/README.md +473 -0
  12. data/Rakefile +19 -0
  13. data/acts_as_bookable.gemspec +41 -0
  14. data/app/assets/images/acts_as_bookable/.keep +0 -0
  15. data/app/assets/javascripts/acts_as_bookable/application.js +13 -0
  16. data/app/assets/stylesheets/acts_as_bookable/application.css +15 -0
  17. data/app/controllers/acts_as_bookable/application_controller.rb +4 -0
  18. data/app/helpers/acts_as_bookable/application_helper.rb +4 -0
  19. data/app/views/layouts/acts_as_bookable/application.html.erb +14 -0
  20. data/bin/rails +12 -0
  21. data/config/locales/en.yml +12 -0
  22. data/config/routes.rb +2 -0
  23. data/db/migrate/20160217085200_create_acts_as_bookable_bookings.rb +14 -0
  24. data/gemfiles/activerecord_3.2.gemfile +16 -0
  25. data/gemfiles/activerecord_4.0.gemfile +16 -0
  26. data/gemfiles/activerecord_4.1.gemfile +16 -0
  27. data/gemfiles/activerecord_4.2.gemfile +17 -0
  28. data/gemfiles/activerecord_5.0.gemfile +17 -0
  29. data/lib/acts_as_bookable/bookable/core.rb +285 -0
  30. data/lib/acts_as_bookable/bookable.rb +56 -0
  31. data/lib/acts_as_bookable/booker.rb +70 -0
  32. data/lib/acts_as_bookable/booking.rb +54 -0
  33. data/lib/acts_as_bookable/db_utils.rb +39 -0
  34. data/lib/acts_as_bookable/engine.rb +5 -0
  35. data/lib/acts_as_bookable/t.rb +11 -0
  36. data/lib/acts_as_bookable/time_utils.rb +135 -0
  37. data/lib/acts_as_bookable/version.rb +3 -0
  38. data/lib/acts_as_bookable.rb +42 -0
  39. data/lib/tasks/acts_as_bookable_tasks.rake +4 -0
  40. data/spec/acts_as_bookable/acts_as_bookable_spec.rb +52 -0
  41. data/spec/acts_as_bookable/acts_as_booker_spec.rb +57 -0
  42. data/spec/acts_as_bookable/bookable/core_spec.rb +593 -0
  43. data/spec/acts_as_bookable/bookable_spec.rb +124 -0
  44. data/spec/acts_as_bookable/booker_spec.rb +140 -0
  45. data/spec/acts_as_bookable/booking_spec.rb +241 -0
  46. data/spec/acts_as_bookable/schedule_spec.rb +137 -0
  47. data/spec/acts_as_bookable/time_utils_spec.rb +525 -0
  48. data/spec/factories/bookable.rb +7 -0
  49. data/spec/factories/booker.rb +5 -0
  50. data/spec/factories/room.rb +11 -0
  51. data/spec/internal/app/models/Bookable.rb +3 -0
  52. data/spec/internal/app/models/Booker.rb +3 -0
  53. data/spec/internal/app/models/Event.rb +3 -0
  54. data/spec/internal/app/models/Generic.rb +2 -0
  55. data/spec/internal/app/models/NotBooker.rb +2 -0
  56. data/spec/internal/app/models/Room.rb +3 -0
  57. data/spec/internal/app/models/Show.rb +3 -0
  58. data/spec/internal/app/models/Unbookable.rb +2 -0
  59. data/spec/internal/config/database.yml.sample +17 -0
  60. data/spec/internal/db/schema.rb +51 -0
  61. data/spec/spec_helper.rb +26 -0
  62. data/spec/support/0-helpers.rb +32 -0
  63. data/spec/support/1-database.rb +42 -0
  64. data/spec/support/2-database_cleaner.rb +21 -0
  65. data/spec/support/3-factory_girl.rb +12 -0
  66. metadata +296 -0
@@ -0,0 +1,135 @@
1
+ module ActsAsBookable
2
+ ##
3
+ # Provide helper functions to manage operations and queries related to times
4
+ # and schedules
5
+ #
6
+ module TimeUtils
7
+ class << self
8
+ ##
9
+ # Check if time is included in a time interval. The ending time is excluded
10
+ #
11
+ # @param time The time to check
12
+ # @param interval_start The beginning time of the interval to match against
13
+ # @param interval_end The ending time of the interval to match against
14
+ #
15
+ def time_in_interval? (time, interval_start, interval_end)
16
+ time >= interval_start && time < interval_end
17
+ end
18
+
19
+ ##
20
+ # Check if there is an occurrence of a schedule that contains a time interval
21
+ #
22
+ # @param schedule The schedule
23
+ # @param interval_start The beginning Time of the interval
24
+ # @param interval_end The ending Time of the interval
25
+ # @return true if the interval falls within an occurrence of the schedule, otherwise false
26
+ #
27
+ def interval_in_schedule?(schedule, interval_start, interval_end)
28
+ # Check if interval_start and interval_end falls within any occurrence
29
+ return false if(!time_in_schedule?(schedule,interval_start) || !time_in_schedule?(schedule,interval_end))
30
+
31
+ # Check if both interval_start and interval_end falls within the SAME occurrence
32
+ between = schedule.occurrences_between(interval_start, interval_end, true)
33
+ contains = false
34
+ between.each do |oc|
35
+ oc_end = oc + schedule.duration
36
+ contains = true if (time_in_interval?(interval_start,oc,oc_end) && time_in_interval?(interval_end,oc,oc_end))
37
+ break if contains
38
+ end
39
+
40
+ contains
41
+ end
42
+
43
+ ##
44
+ # Check if there is an occurrence of a schedule that contains a time
45
+ # @param schedule The schedule
46
+ # @param time The time
47
+ # @return true if the time falls within an occurrence of the schedule, otherwise false
48
+ #
49
+ def time_in_schedule?(schedule, time)
50
+ return schedule.occurring_at? time
51
+ end
52
+
53
+ ##
54
+ # Returns an array of sub-intervals given another array of intervals, which are the overlapping insersections of each-others.
55
+ #
56
+ # @param intervals an array of intervals
57
+ # @return an array of subintervals, sorted by time_start
58
+ #
59
+ # An interval is defined as a hash with at least the following fields: `time_from` and `time_end`. An interval may contain more
60
+ # fields. In that case, it's suggested to give a block with the instructions to correctly merge two intervals when needed.
61
+ #
62
+ # e.g: given these 7 intervals
63
+ # |------| |---| |----------|
64
+ # |---| |--|
65
+ # |------| |--| |-------------|
66
+ # the output is an array containing these 8 intervals:
67
+ # |--| |--| |---| |--| |---| |------|
68
+ # |---| |------|
69
+ # the number of subintervals may increase or decrease because some intervals may be split, while
70
+ # some others may be merged.
71
+ #
72
+ # If a block is given, it's called before merging two intervals. The block should provide instructions to merge intervals, and should return the merged fields in a hash
73
+ def subintervals(intervals, &block)
74
+ raise ArgumentError.new('intervals must be an array') unless intervals.is_a? Array
75
+
76
+ steps = [] # Steps will be extracted from intervals
77
+ subintervals = [] # The output
78
+ last_time = nil
79
+ last_attrs = nil
80
+ started_count = 0 # The number of intervals opened inside the cycle
81
+
82
+ # Extract start times and end times from intervals, and create steps
83
+ intervals.each do |el|
84
+ begin
85
+ ts = el[:time_start].to_time
86
+ te = el[:time_end].to_time
87
+ rescue NoMethodError
88
+ raise ArgumentError.new('intervals must define :time_start and :time_end as Time or Date')
89
+ end
90
+ attrs = el.clone
91
+ attrs.delete(:time_start)
92
+ attrs.delete(:time_end)
93
+ steps << { opening: 1, time: el[:time_start], attrs: attrs } # Start step
94
+ steps << { opening: -1, time: el[:time_end], attrs: attrs.clone } # End step
95
+ end
96
+
97
+ # Sort steps by time (and opening if time is the same)
98
+ steps.sort! do |a,b|
99
+ diff = a[:time] <=> b[:time]
100
+ diff = a[:opening] <=> b[:opening] if (diff == 0)
101
+ diff
102
+ end
103
+
104
+ # Iterate over steps
105
+ steps.each do |step|
106
+ if (started_count == 0)
107
+ last_time = step[:time]
108
+ last_attrs = step[:attrs]
109
+ else
110
+ if(step[:time] > last_time)
111
+ subintervals << ({
112
+ time_start: last_time,
113
+ time_end: step[:time]
114
+ }.merge(last_attrs))
115
+
116
+ last_time = step[:time]
117
+ end
118
+
119
+ if block_given?
120
+ last_attrs = block.call(last_attrs.clone, step[:attrs],(step[:opening] == 1 ? :open : :close))
121
+ else
122
+ last_attrs = step[:attrs]
123
+ end
124
+ end
125
+
126
+ # Update started_count
127
+ started_count += step[:opening]
128
+ end
129
+
130
+ subintervals
131
+ end
132
+
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,3 @@
1
+ module ActsAsBookable
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,42 @@
1
+ require 'active_record'
2
+ require 'active_record/version'
3
+ require 'active_support/core_ext/module'
4
+ require_relative 'acts_as_bookable/engine' if defined?(Rails)
5
+ require 'ice_cube'
6
+ IceCube.compatibility = 12 # Drop compatibility for :start_date, avoiding a bunch of warnings caused by serialization
7
+
8
+ module ActsAsBookable
9
+ extend ActiveSupport::Autoload
10
+
11
+ autoload :Bookable
12
+ autoload :Booker
13
+ autoload :Booking
14
+ autoload :T
15
+ autoload :VERSION
16
+ autoload :TimeUtils
17
+ autoload :DBUtils
18
+
19
+ autoload_under 'bookable' do
20
+ autoload :Core
21
+ end
22
+
23
+ class InitializationError < StandardError
24
+ def initialize model, message
25
+ super "Error initializing acts_as_bookable on #{model.to_s} - " + message
26
+ end
27
+ end
28
+
29
+ class OptionsInvalid < StandardError
30
+ def initialize model, message
31
+ super "Error validating options for #{model.to_s} - " + message
32
+ end
33
+ end
34
+
35
+ class AvailabilityError < StandardError
36
+ end
37
+ end
38
+
39
+ ActiveSupport.on_load(:active_record) do
40
+ extend ActsAsBookable::Bookable
41
+ include ActsAsBookable::Booker
42
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :acts_as_bookable do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,52 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'acts_as_bookable' do
4
+ it "should provide a class method 'bookable?' that is false for unbookable models" do
5
+ expect(Unbookable).not_to be_bookable
6
+ end
7
+
8
+ describe 'Bookable Method Generation' do
9
+ before :each do
10
+ Unbookable.acts_as_bookable
11
+ @bookable = Unbookable.new()
12
+ end
13
+
14
+ it "should respond 'true' to bookable?" do
15
+ expect(@bookable.class).to be_bookable
16
+ end
17
+ end
18
+
19
+ describe 'class configured as Bookable' do
20
+ before(:each) do
21
+ @bookable = Bookable.new
22
+ end
23
+
24
+ it 'should add #bookable? query method to the class-side' do
25
+ expect(Bookable).to respond_to(:bookable?)
26
+ end
27
+
28
+ it 'should return true from the class-side #bookable?' do
29
+ expect(Bookable.bookable?).to be_truthy
30
+ end
31
+
32
+ it 'should return false from the base #bookable?' do
33
+ expect(ActiveRecord::Base.bookable?).to be_falsy
34
+ end
35
+
36
+ # it 'should add #tag method on the instance-side' do
37
+ # expect(@bookable).to respond_to(:tag)
38
+ # end
39
+
40
+ # it 'should generate an association for #owned_taggings and #owned_tags' do
41
+ # expect(@bookable).to respond_to(:owned_taggings, :owned_tags)
42
+ # end
43
+ end
44
+
45
+ describe 'Reloading' do
46
+ it 'should save a model instantiated by Model.find' do
47
+ bookable = create(:bookable)
48
+ found_bookable = Bookable.find(bookable.id)
49
+ expect(found_bookable.save).to eq true
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,57 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'acts_as_booker' do
4
+ it "should provide a class method 'booker?' that is false for not booker models" do
5
+ expect(NotBooker).not_to be_booker
6
+ end
7
+
8
+ describe 'Booker Method Generation' do
9
+ before :each do
10
+ NotBooker.acts_as_booker
11
+ @booker = NotBooker.new()
12
+ end
13
+
14
+ it "should respond 'true' to booker?" do
15
+ expect(@booker.class).to be_booker
16
+ end
17
+ end
18
+
19
+ describe 'class configured as Booker' do
20
+ before(:each) do
21
+ @booker = Booker.new
22
+ end
23
+
24
+ it 'should add #booker? query method to the class-side' do
25
+ expect(Booker).to respond_to(:booker?)
26
+ end
27
+
28
+ it 'should return true from the class-side #booker?' do
29
+ expect(Booker.booker?).to be_truthy
30
+ end
31
+
32
+ it 'should return false from the base #booker?' do
33
+ expect(ActiveRecord::Base.booker?).to be_falsy
34
+ end
35
+
36
+ it 'should add #booker? query method to the instance-side' do
37
+ expect(@booker).to respond_to(:booker?)
38
+ end
39
+
40
+ it 'should add #booker? query method to the instance-side' do
41
+ expect(@booker.booker?).to be_truthy
42
+ end
43
+
44
+
45
+ # it 'should generate an association for #owned_taggings and #owned_tags' do
46
+ # expect(@booker).to respond_to(:owned_taggings, :owned_tags)
47
+ # end
48
+ end
49
+
50
+ describe 'Reloading' do
51
+ it 'should save a model instantiated by Model.find' do
52
+ booker = Booker.create!(name: 'Booker')
53
+ found_booker = Booker.find(booker.id)
54
+ expect(found_booker.save).to eq true
55
+ end
56
+ end
57
+ end