mongoid_occurrence_views 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ec270d211cd1ad0bc58b60980d77f4defff948f5c8bd0d63db70f450f1f7d5c7
4
+ data.tar.gz: 2b351998c8815c1d1cd0aa7af2778e44e390f606cd35ae140bf9b246abc20bdb
5
+ SHA512:
6
+ metadata.gz: f915e5edb172f9a21bdb48bd1442fbcc677716212b9314e6463b93b0f6f755481bdf9a690bce663f623968edc0f312b1643d6e9f568e1420519583bb5dd277d2
7
+ data.tar.gz: b818d09e3c18d21b994a7250cf785ad898822d3d134cdc68e57b3e7844e39935b68b9a1ef8f4eeb1e6ff781c7b1031368a0bd551e52110a3979324c8f98ccf90
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ Gemfile.lock
data/.travis.yml ADDED
@@ -0,0 +1,19 @@
1
+ language: ruby
2
+ cache: bundler
3
+ script: 'bundle exec rake'
4
+ rvm:
5
+ - 2.5.1
6
+ services:
7
+ - mongodb
8
+
9
+ notifications:
10
+ email:
11
+ recipients:
12
+ - tomas.celizna@gmail.com
13
+ on_failure: change
14
+ on_success: never
15
+
16
+ matrix:
17
+ include:
18
+ - rvm: 2.5.1
19
+ env: MONGOID_VERSION=7
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # CHANGELOG
2
+
3
+ ## 0.1.0
4
+
5
+ * initial release
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in mongoid_occurrence_views.gemspec
6
+ gemspec
7
+
8
+ case version = ENV['MONGOID_VERSION'] || '~> 7.0'
9
+ when /7/ then gem 'mongoid', '~> 7.0'
10
+ else gem 'mongoid', version
11
+ end
data/Guardfile ADDED
@@ -0,0 +1,5 @@
1
+ guard :minitest do
2
+ watch(%r{^lib/(.+)\.rb$}) { |m| "test/#{m[1]}_test.rb" }
3
+ watch(%r{^test/.+_test\.rb$})
4
+ watch(%r{^test/test_helper\.rb$}) { 'test' }
5
+ end
data/README.md ADDED
@@ -0,0 +1,136 @@
1
+ # Mongoid Occurrence Views
2
+
3
+ [![Build Status](https://travis-ci.org/tomasc/mongoid_occurrence_views.svg)](https://travis-ci.org/tomasc/mongoid_occurrence_views) [![Gem Version](https://badge.fury.io/rb/mongoid_occurrence_views.svg)](http://badge.fury.io/rb/mongoid_occurrence_views) [![Coverage Status](https://img.shields.io/coveralls/tomasc/mongoid_occurrence_views.svg)](https://coveralls.io/r/tomasc/mongoid_occurrence_views)
4
+
5
+ By taking advantage of [Mongodb views](https://docs.mongodb.com/manual/core/views), this gem simplifies querying for events with multiple occurrences or a recurring schedule.
6
+
7
+ ## Requirements
8
+
9
+ * Mongoid 7.1+
10
+ * MongoDB 3.4+
11
+
12
+ ## Installation
13
+
14
+ Add this line to your application's Gemfile:
15
+
16
+ ```ruby
17
+ gem 'mongoid_occurrence_views'
18
+ ```
19
+
20
+ And then execute:
21
+
22
+ $ bundle
23
+
24
+ Or install it yourself as:
25
+
26
+ $ gem install mongoid_occurrence_views
27
+
28
+ ## Usage
29
+
30
+ ### Occurrences
31
+
32
+ Define a Mongoid document class that will hold information about each occurrence.
33
+
34
+ ```ruby
35
+ class Occurrence
36
+ include Mongoid::Document
37
+ include MongoidOccurrenceViews::Event::Occurrence
38
+
39
+ embedded_in_event class_name: 'Event'
40
+ end
41
+ ```
42
+
43
+ This will create the following fields on `Occurrence`:
44
+
45
+ * `dtstart`, (`DateTime`)
46
+ * `dtend` (`DateTime`)
47
+ * `schedule` (`MongoidIceCubeExtension::Schedule`)
48
+
49
+ And the `all_day` (`Boolean`) `attr_accessor`, which is useful for user-facing forms.
50
+
51
+ ### Events
52
+
53
+ ```ruby
54
+ class Event
55
+ include Mongoid::Document
56
+ include MongoidOccurrenceViews::Event
57
+
58
+ embeds_many_occurrences class_name: 'Occurrence'
59
+ end
60
+ ```
61
+
62
+ The `embeds_many_occurences` macro will setup an embedded relation that holds a definition of occurrences. For example:
63
+
64
+ ```ruby
65
+ <Occurrence dtstart: …, dtend: …, schedule: …>
66
+ <Occurrence dtstart: …, dtend: …, schedule: …>
67
+ <Occurrence dtstart: …, dtend: …, schedule: …>
68
+ ```
69
+
70
+ Before each validation callback, each occurrence will expand its definition as follows:
71
+
72
+ * multi-day occurrences are split into single-day occurrences
73
+ * recurring schedules are expanded into single-day occurrences
74
+
75
+ The following scopes are available for querying:
76
+
77
+ * `occurs_between(Time, Time)`
78
+ * `occurs_from(Time)`
79
+ * `occurs_on(Time)`
80
+ * `occurs_until(Time)`
81
+
82
+ And these scopes for ordering:
83
+
84
+ * `order_by_start(:asc / :desc)`
85
+ * `order_by_end(:asc / :desc)`
86
+
87
+ ### Views & queries
88
+
89
+ The gem creates two views (virtual collections):
90
+
91
+ * `expanded_occurrences_view` for working with the expanded events
92
+ * `occurrences_ordering_view` for ordering the unexpanded events
93
+
94
+ #### Expanded Occurrences View
95
+
96
+ This view lists expanded events – meaning events expanded for each daily occurrence.
97
+
98
+ Use the `with_expanded_occurrences_view` method to query the expanded events:
99
+
100
+ ```ruby
101
+ Event.with_expanded_occurrences_view do
102
+ Event.occurs_from(Time.zone.now).…
103
+ end
104
+ ```
105
+
106
+ The `.with_expanded_occurrences_view` method is simply a syntactic sugar on top of Mongoid's `.with(collection: …)` method, which specifies the collection to be the expanded occurrences view (`event__expanded_occurrences_view`).
107
+
108
+ The view adds a `:_dtstart` and `:_dtend` fields to each `Event`.
109
+
110
+ #### Occurrences Ordering View
111
+
112
+ This view lists unexpanded events, circumnavigating Mongodb limitation for sorting by embedded documents. It adds `dtstart` field with a value coming from occurrence chronologically first, and `dtend` field with a value of occurrence chronologically last.
113
+
114
+ Use the `with_occurrences_ordering_view` method to order the unexpanded events.
115
+
116
+ ```ruby
117
+ Event.with_occurrences_ordering_view do
118
+ Event.order_by_start(:asc)…
119
+ end
120
+ ```
121
+
122
+ The `.with_occurrences_ordering_view` method is simply a syntactic sugar on top of Mongoid's `.with(collection: …)` method, which specifies the collection to be the expanded occurrences view (`event__expanded_occurrences_view`).
123
+
124
+ ## Development
125
+
126
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
127
+
128
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
129
+
130
+ ## Contributing
131
+
132
+ Bug reports and pull requests are welcome on GitHub at https://github.com/tomasc/mongoid_occurrence_views.
133
+
134
+ ## License
135
+
136
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << 'test'
6
+ t.libs << 'lib'
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task default: :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "mongoid_occurrence_views"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,25 @@
1
+ require 'mongoid_occurrence_views/version'
2
+
3
+ require 'mongoid'
4
+ require 'mongoid_ice_cube_extension'
5
+
6
+ require 'mongoid_occurrence_views/create_mongodb_view'
7
+ require 'mongoid_occurrence_views/destroy_mongodb_view'
8
+
9
+ require 'mongoid_occurrence_views/event'
10
+ require 'mongoid_occurrence_views/event/occurrence'
11
+
12
+ require 'mongoid_occurrence_views/event/has_views_on_occurrences'
13
+ require 'mongoid_occurrence_views/event/has_occurrence_scopes'
14
+
15
+ require 'mongoid_occurrence_views/event/create_view'
16
+ require 'mongoid_occurrence_views/event/create_expanded_occurrences_view'
17
+ require 'mongoid_occurrence_views/event/create_occurrences_ordering_view'
18
+
19
+ require 'mongoid_occurrence_views/queries/query'
20
+ require 'mongoid_occurrence_views/queries/occurs_between'
21
+ require 'mongoid_occurrence_views/queries/occurs_from'
22
+ require 'mongoid_occurrence_views/queries/occurs_on'
23
+ require 'mongoid_occurrence_views/queries/occurs_until'
24
+ require 'mongoid_occurrence_views/queries/order_by_end'
25
+ require 'mongoid_occurrence_views/queries/order_by_start'
@@ -0,0 +1,25 @@
1
+ module MongoidOccurrenceViews
2
+ class CreateMongodbView
3
+ def initialize(name:, collection:, pipeline:)
4
+ @name = name
5
+ @collection = collection
6
+ @pipeline = pipeline
7
+ end
8
+
9
+ def self.call(*args)
10
+ new(*args).call
11
+ end
12
+
13
+ def call
14
+ Mongoid.clients.each do |client_name, _|
15
+ client = Mongoid.client(client_name)
16
+ next if client.collections.map(&:name).include?(name)
17
+ client.command(create: name, viewOn: collection, pipeline: pipeline)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :name, :collection, :pipeline
24
+ end
25
+ end
@@ -0,0 +1,23 @@
1
+ module MongoidOccurrenceViews
2
+ class DestroyMongodbView
3
+ def initialize(name:)
4
+ @name = name
5
+ end
6
+
7
+ def self.call(*args)
8
+ new(*args).call
9
+ end
10
+
11
+ def call
12
+ Mongoid.clients.each do |client_name, _|
13
+ client = Mongoid.client(client_name)
14
+ next unless client.collections.map(&:name).include?(name)
15
+ client.command(drop: name)
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :name
22
+ end
23
+ end
@@ -0,0 +1,21 @@
1
+ module MongoidOccurrenceViews
2
+ module Event
3
+ def self.included(base)
4
+ base.class_eval do
5
+ include HasViewsOnOccurrences
6
+ include HasOccurrenceScopes
7
+ end
8
+ base.extend ClassMethods
9
+ end
10
+
11
+ module ClassMethods
12
+ def embeds_many_occurrences(options = {})
13
+ embeds_many :occurrences, class_name: options.fetch(:class_name)
14
+ accepts_nested_attributes_for :occurrences, allow_destroy: true, reject_if: :all_blank
15
+
16
+ CreateExpandedOccurrencesView.call(self) unless embedded?
17
+ CreateOccurrencesOrderingView.call(self) unless embedded?
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,34 @@
1
+ module MongoidOccurrenceViews
2
+ module Event
3
+ class CreateExpandedOccurrencesView < CreateView
4
+ def view_name
5
+ klass.expanded_occurrences_view_name
6
+ end
7
+
8
+ def pipeline
9
+ [add_fields,
10
+ unwind_associations_to_parent,
11
+ add_datetime_fields].flatten
12
+ end
13
+
14
+ private
15
+
16
+ def add_fields
17
+ { '$addFields': { "_#{occurrence_relation_chain.first}": "$#{occurrence_relation_chain.first}" } }
18
+ end
19
+
20
+ def unwind_associations_to_parent
21
+ occurrence_relations_chained.map do |chain|
22
+ { '$unwind': "$_#{chain}" }
23
+ end
24
+ end
25
+
26
+ def add_datetime_fields
27
+ { '$addFields': {
28
+ '_dtstart': "$_#{occurrence_relations_chained.last}.ds",
29
+ '_dtend': "$_#{occurrence_relations_chained.last}.de",
30
+ } }
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,31 @@
1
+ module MongoidOccurrenceViews
2
+ module Event
3
+ class CreateOccurrencesOrderingView < CreateView
4
+ def view_name
5
+ klass.occurrences_ordering_view_name
6
+ end
7
+
8
+ def pipeline
9
+ [add_fields].flatten
10
+ end
11
+
12
+ private
13
+
14
+ def add_fields
15
+ { '$addFields': { '_dtstart': order_dtstart_field, '_dtend': order_dtend_field } }
16
+ end
17
+
18
+ def order_dtstart_field
19
+ min = "$#{occurrence_relations_chained.last}.ds"
20
+ occurrence_relations_chained.length.times { min = { '$min': min } }
21
+ min
22
+ end
23
+
24
+ def order_dtend_field
25
+ max = "$#{occurrence_relations_chained.last}.de"
26
+ occurrence_relations_chained.length.times { max = { '$max': max } }
27
+ max
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,40 @@
1
+ module MongoidOccurrenceViews
2
+ module Event
3
+ class CreateView
4
+ delegate(
5
+ :occurrence_relations_chained,
6
+ :occurrence_relation_chain,
7
+
8
+ to: :klass
9
+ )
10
+
11
+ def initialize(klass)
12
+ @klass = klass
13
+ end
14
+
15
+ def self.call(*args)
16
+ new(*args).call
17
+ end
18
+
19
+ def call
20
+ CreateMongodbView.call(
21
+ name: view_name,
22
+ collection: klass.collection.name,
23
+ pipeline: pipeline
24
+ )
25
+ end
26
+
27
+ def pipeline
28
+ raise NotImplementedError
29
+ end
30
+
31
+ def view_name
32
+ raise NotImplementedError
33
+ end
34
+
35
+ private
36
+
37
+ attr_reader :klass
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,32 @@
1
+ module MongoidOccurrenceViews
2
+ module Event
3
+ module HasOccurrenceScopes
4
+ def self.included(base)
5
+ base.extend ClassMethods
6
+ base.class_eval do
7
+ scope :occurs_between, ->(dtstart, dtend, dtstart_field = dtstart_query_field, dtend_field = dtend_query_field) { MongoidOccurrenceViews::Queries::OccursBetween.criteria(criteria, dtstart, dtend, dtstart_field: dtstart_field, dtend_field: dtend_field) }
8
+ scope :occurs_from, ->(date_time, dtstart_field = dtstart_query_field) { MongoidOccurrenceViews::Queries::OccursFrom.criteria(criteria, date_time, dtstart_field: dtstart_field) }
9
+ scope :occurs_on, -> (date_time, dtstart_field = dtstart_query_field, dtend_field = dtend_query_field) { MongoidOccurrenceViews::Queries::OccursOn.criteria(criteria, date_time, dtstart_field: dtstart_field, dtend_field: dtend_field) }
10
+ scope :occurs_until, ->(date_time, dtend_field = dtend_query_field) { MongoidOccurrenceViews::Queries::OccursUntil.criteria(criteria, date_time, dtend_field: dtend_field) }
11
+
12
+ scope :order_by_start, ->(order = :asc, dtstart_field = dtstart_query_field) { MongoidOccurrenceViews::Queries::OrderByStart.criteria(criteria, order, dtstart_field: dtstart_field) }
13
+ scope :order_by_end, ->(order = :asc, dtend_field = dtend_query_field) { MongoidOccurrenceViews::Queries::OrderByEnd.criteria(criteria, order, dtend_field: dtend_field) }
14
+ end
15
+ end
16
+
17
+ module ClassMethods
18
+ def dtstart_query_field
19
+ within_view? ? :_dtstart : :"#{occurrence_relations_chained.last}.ds"
20
+ end
21
+
22
+ def dtend_query_field
23
+ within_view? ? :_dtend : :"#{occurrence_relations_chained.last}.de"
24
+ end
25
+
26
+ def within_view?
27
+ collection.name =~ /#{MongoidOccurrenceViews::Event::HasViewsOnOccurrences::EXPANDED_VIEW_NAME_SUFFIX}|#{MongoidOccurrenceViews::Event::HasViewsOnOccurrences::ORDERING_VIEW_NAME_SUFFIX}/
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,81 @@
1
+ module MongoidOccurrenceViews
2
+ module Event
3
+ module HasViewsOnOccurrences
4
+ ORDERING_VIEW_NAME_SUFFIX = 'occurrences_ordering_view'.freeze
5
+ EXPANDED_VIEW_NAME_SUFFIX = 'expanded_occurrences_view'.freeze
6
+
7
+ def self.included(base)
8
+ base.extend ClassMethods
9
+ end
10
+
11
+ def dtstart
12
+ return unless self.class.within_view?
13
+ DateTime.demongoize(self[:_dtstart])
14
+ end
15
+
16
+ def dtend
17
+ return unless self.class.within_view?
18
+ DateTime.demongoize(self[:_dtend])
19
+ end
20
+
21
+ module ClassMethods
22
+ def occurrences_ordering_view_name
23
+ [collection.name, ORDERING_VIEW_NAME_SUFFIX].join('__').freeze
24
+ end
25
+
26
+ def expanded_occurrences_view_name
27
+ [collection.name, EXPANDED_VIEW_NAME_SUFFIX].join('__').freeze
28
+ end
29
+
30
+ def with_expanded_occurrences_view(&block)
31
+ with_occurrences_view(expanded: true, &block)
32
+ end
33
+
34
+ def with_occurrences_ordering_view(&block)
35
+ with_occurrences_view(expanded: false, &block)
36
+ end
37
+
38
+ def with_occurrences_view(options = {}, &block)
39
+ expanded = options.delete(:expanded) { |_| true }
40
+ options[:collection] ||= expanded ? expanded_occurrences_view_name : occurrences_ordering_view_name
41
+ criteria.with(options, &block)
42
+ end
43
+
44
+ def occurrence_relations_chained
45
+ occurrence_relation_chain.each_with_index.map do |_, i|
46
+ occurrence_relation_chain[0..i].join('.')
47
+ end
48
+ end
49
+
50
+ def occurrence_relation_chain
51
+ return occurrence_relation_names unless event_relations.present?
52
+ [event_relations.first.store_as, occurrence_relation_names].flatten
53
+ end
54
+
55
+ def event_relations
56
+ return unless self.relations.present?
57
+ self.relations.values.select { |rel| is_event_relation?(rel) }
58
+ end
59
+
60
+ def is_event_relation?(rel)
61
+ relation_class_name = rel.options[:class_name]
62
+ return false unless relation_class_name.present?
63
+ return true if event_class_names.include?(relation_class_name)
64
+ return unless relation_class = relation_class_name.safe_constantize
65
+ event_superclasses = event_class_names.map { |cn| cn.safe_constantize.ancestors }.flatten
66
+ event_superclasses.include? relation_class
67
+ end
68
+
69
+ def event_class_names
70
+ ObjectSpace.each_object(Class).select do |cls|
71
+ cls.included_modules.include?(MongoidOccurrenceViews::Event)
72
+ end.map(&:to_s)
73
+ end
74
+
75
+ def occurrence_relation_names
76
+ %w[occurrences daily_occurrences]
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,122 @@
1
+ module MongoidOccurrenceViews
2
+ module Event
3
+ module Occurrence
4
+ SCHEDULE_DURATION = 1.year
5
+
6
+ def self.included(base)
7
+ base.include MongoidOccurrenceViews::Event::HasOccurrenceScopes
8
+ base.extend ClassMethods
9
+ end
10
+
11
+ module ClassMethods
12
+ def embedded_in_event(options = {})
13
+ field :dtstart, type: DateTime
14
+ field :dtend, type: DateTime
15
+
16
+ field :schedule, type: MongoidIceCubeExtension::Schedule
17
+ field :schedule_dtend, type: Time
18
+
19
+ embedded_in :event, class_name: options.fetch(:class_name, nil)
20
+ embeds_many :daily_occurrences, class_name: 'MongoidOccurrenceViews::Event::Occurrence::DailyOccurrence', order: :dtstart.asc
21
+
22
+ validates_presence_of :dtstart
23
+ validates_presence_of :dtend
24
+
25
+ before_validation :nil_schedule, unless: :recurring?
26
+
27
+ after_validation :adjust_dates_for_all_day, if: :changed?
28
+ after_validation :assign_daily_occurrences, if: :changed?
29
+ end
30
+
31
+ def dtstart_query_field
32
+ :"daily_occurrences.ds"
33
+ end
34
+
35
+ def dtend_query_field
36
+ :"daily_occurrences.de"
37
+ end
38
+ end
39
+
40
+ def all_day
41
+ return unless dtstart.present?
42
+ return unless dtend.present?
43
+
44
+ @all_day ||= dtstart == dtstart.beginning_of_day && dtend == dtend.end_of_day
45
+ end
46
+ alias all_day? all_day
47
+
48
+ def all_day=(val)
49
+ @all_day = [true, 'true', 1, '1'].include?(val)
50
+ end
51
+
52
+ def recurring?
53
+ schedule.present?
54
+ end
55
+
56
+ def schedule_dtend
57
+ read_attribute(:schedule_dtend) || (dtstart.try(:to_time) || Time.now) + SCHEDULE_DURATION
58
+ end
59
+
60
+ private
61
+
62
+ def nil_schedule
63
+ self.schedule = nil
64
+ end
65
+
66
+ def adjust_dates_for_all_day
67
+ return unless all_day?
68
+
69
+ self.dtstart = dtstart.beginning_of_day
70
+ self.dtend = dtend.end_of_day
71
+ end
72
+
73
+ def assign_daily_occurrences
74
+ return unless dtstart_changed? || dtend_changed? || schedule_changed? || schedule_dtend_changed?
75
+ self.daily_occurrences = daily_occurrences_from_schedule + daily_occurrences_from_date_range
76
+ end
77
+
78
+ def daily_occurrences_from_schedule
79
+ return [] unless recurring?
80
+
81
+ schedule.occurrences(schedule_dtend).map do |occurrence|
82
+ relations['daily_occurrences'].klass.new(
83
+ dtstart: occurrence.start_time,
84
+ dtend: occurrence.end_time.change(hour: dtend.hour, min: dtend.minute)
85
+ )
86
+ end.compact
87
+ end
88
+
89
+ def daily_occurrences_from_date_range
90
+ return [] if recurring?
91
+ date_range = Range.new(dtstart.to_date, dtend.to_date)
92
+ is_single_day = (date_range.first == date_range.last)
93
+
94
+ date_range.map do |date|
95
+ occurence_dtstart = is_single_day || date == date_range.first ? dtstart : date.beginning_of_day
96
+ occurence_dtend = is_single_day || date == date_range.last ? dtend : date.end_of_day
97
+
98
+ relations['daily_occurrences'].klass.new(
99
+ dtstart: occurence_dtstart,
100
+ dtend: occurence_dtend
101
+ )
102
+ end.compact
103
+ end
104
+
105
+ class DailyOccurrence
106
+ include Mongoid::Document
107
+
108
+ # alias fields to keep document size small
109
+ field :ds, as: :dtstart, type: DateTime
110
+ field :de, as: :dtend, type: DateTime
111
+
112
+ def all_day
113
+ return unless dtstart.present?
114
+ return unless dtend.present?
115
+
116
+ dtstart == dtstart.beginning_of_day && dtend == dtend.end_of_day
117
+ end
118
+ alias all_day? all_day
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,28 @@
1
+ module MongoidOccurrenceViews
2
+ module Queries
3
+ class OccursBetween < Query
4
+ def initialize(base_criteria, dtstart, dtend, options = {})
5
+ @base_criteria = base_criteria
6
+ @dtstart = dtstart
7
+ @dtend = dtend
8
+ @dtstart_field = options.fetch(:dtstart_field)
9
+ @dtend_field = options.fetch(:dtend_field)
10
+ end
11
+
12
+ def criteria
13
+ _dtstart = dtstart.beginning_of_day if dtstart.instance_of?(Date)
14
+ _dtstart = dtstart.utc
15
+
16
+ _dtend = dtend.end_of_day if dtend.instance_of?(Date)
17
+ _dtend = dtend.utc
18
+
19
+ base_criteria.lte(dtstart_field => dtend.to_datetime)
20
+ .gte(dtend_field => dtstart.to_datetime)
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :dtstart, :dtend, :dtstart_field, :dtend_field
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,22 @@
1
+ module MongoidOccurrenceViews
2
+ module Queries
3
+ class OccursFrom < Query
4
+ def initialize(base_criteria, date_time, options = {})
5
+ @base_criteria = base_criteria
6
+ @date_time = date_time
7
+ @dtstart_field = options.fetch(:dtstart_field)
8
+ end
9
+
10
+ def criteria
11
+ _date_time = date_time.end_of_day if date_time.instance_of?(Date)
12
+ _date_time = date_time.utc
13
+
14
+ base_criteria.gte(dtstart_field => _date_time)
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :date_time, :dtstart_field
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,24 @@
1
+ module MongoidOccurrenceViews
2
+ module Queries
3
+ class OccursOn < Query
4
+ def initialize(base_criteria, date_time, options = {})
5
+ @base_criteria = base_criteria
6
+ @date_time = date_time
7
+ @dtstart_field = options.fetch(:dtstart_field)
8
+ @dtend_field = options.fetch(:dtend_field)
9
+ end
10
+
11
+ def criteria
12
+ _date_time = date_time.beginning_of_day if date_time.instance_of?(Date)
13
+ _date_time = date_time.utc
14
+
15
+ base_criteria.lte(dtstart_field => _date_time)
16
+ .gte(dtend_field => _date_time)
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :date_time, :dtstart_field, :dtend_field
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,22 @@
1
+ module MongoidOccurrenceViews
2
+ module Queries
3
+ class OccursUntil < Query
4
+ def initialize(base_criteria, date_time, options = {})
5
+ @base_criteria = base_criteria
6
+ @date_time = date_time
7
+ @dtend_field = options.fetch(:dtend_field)
8
+ end
9
+
10
+ def criteria
11
+ _date_time = date_time.beginning_of_day if date_time.instance_of?(Date)
12
+ _date_time = date_time.utc
13
+
14
+ base_criteria.lte(dtend_field => _date_time)
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :date_time, :dtend_field
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,19 @@
1
+ module MongoidOccurrenceViews
2
+ module Queries
3
+ class OrderByEnd < Query
4
+ def initialize(base_criteria, order, options = {})
5
+ @base_criteria = base_criteria
6
+ @order = order
7
+ @dtend_field = options.fetch(:dtend_field)
8
+ end
9
+
10
+ def criteria
11
+ base_criteria.order_by(dtend_field => order)
12
+ end
13
+
14
+ private
15
+
16
+ attr_reader :order, :dtend_field
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ module MongoidOccurrenceViews
2
+ module Queries
3
+ class OrderByStart < Query
4
+ def initialize(base_criteria, order, options = {})
5
+ @base_criteria = base_criteria
6
+ @order = order
7
+ @dtstart_field = options.fetch(:dtstart_field)
8
+ end
9
+
10
+ def criteria
11
+ base_criteria.order_by(dtstart_field => order)
12
+ end
13
+
14
+ private
15
+
16
+ attr_reader :order, :dtstart_field
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,21 @@
1
+ module MongoidOccurrenceViews
2
+ module Queries
3
+ class Query
4
+ def initialize(base_criteria)
5
+ @base_criteria = base_criteria
6
+ end
7
+
8
+ def self.criteria(*args)
9
+ new(*args).criteria
10
+ end
11
+
12
+ def criteria
13
+ raise NotImplementedError
14
+ end
15
+
16
+ private
17
+
18
+ attr_reader :base_criteria
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,3 @@
1
+ module MongoidOccurrenceViews
2
+ VERSION = '0.1.0'.freeze
3
+ end
@@ -0,0 +1,36 @@
1
+ lib = File.expand_path('lib', __dir__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'mongoid_occurrence_views/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'mongoid_occurrence_views'
7
+ spec.version = MongoidOccurrenceViews::VERSION
8
+ spec.authors = ['Tomas Celizna', 'Asger Behncke Jacobsen']
9
+ spec.email = ['tomas.celizna@gmail.com', 'a@asgerbehnckejacobsen.dk']
10
+
11
+ spec.summary = "Makes one's life easier when working with events that have multiple occurrences, or a recurring schedule."
12
+ spec.homepage = 'https://github.com/tomasc/mongoid_occurrence_views'
13
+ spec.license = 'MIT'
14
+
15
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
16
+ f.match(%r{^(test|spec|features)/})
17
+ end
18
+ spec.bindir = 'exe'
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ['lib']
21
+
22
+ spec.add_dependency 'ice_cube'
23
+ spec.add_dependency 'mongoid', '>= 7.0.2', '< 8'
24
+ spec.add_dependency 'mongoid_ice_cube_extension', '>= 0.1.1'
25
+
26
+ spec.add_development_dependency 'activesupport', '> 3.0'
27
+ spec.add_development_dependency 'bundler', '~> 1.16'
28
+ spec.add_development_dependency 'coveralls'
29
+ spec.add_development_dependency 'database_cleaner'
30
+ spec.add_development_dependency 'factory_bot', '~> 4.0'
31
+ spec.add_development_dependency 'guard'
32
+ spec.add_development_dependency 'guard-minitest'
33
+ spec.add_development_dependency 'minitest', '~> 5.0'
34
+ spec.add_development_dependency 'minitest-implicit-subject'
35
+ spec.add_development_dependency 'rake', '~> 10.0'
36
+ end
metadata ADDED
@@ -0,0 +1,263 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mongoid_occurrence_views
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tomas Celizna
8
+ - Asger Behncke Jacobsen
9
+ autorequire:
10
+ bindir: exe
11
+ cert_chain: []
12
+ date: 2018-10-09 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: ice_cube
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: mongoid
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: 7.0.2
35
+ - - "<"
36
+ - !ruby/object:Gem::Version
37
+ version: '8'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: 7.0.2
45
+ - - "<"
46
+ - !ruby/object:Gem::Version
47
+ version: '8'
48
+ - !ruby/object:Gem::Dependency
49
+ name: mongoid_ice_cube_extension
50
+ requirement: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 0.1.1
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 0.1.1
62
+ - !ruby/object:Gem::Dependency
63
+ name: activesupport
64
+ requirement: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ type: :development
70
+ prerelease: false
71
+ version_requirements: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ - !ruby/object:Gem::Dependency
77
+ name: bundler
78
+ requirement: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.16'
83
+ type: :development
84
+ prerelease: false
85
+ version_requirements: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.16'
90
+ - !ruby/object:Gem::Dependency
91
+ name: coveralls
92
+ requirement: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ type: :development
98
+ prerelease: false
99
+ version_requirements: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ - !ruby/object:Gem::Dependency
105
+ name: database_cleaner
106
+ requirement: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ type: :development
112
+ prerelease: false
113
+ version_requirements: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ - !ruby/object:Gem::Dependency
119
+ name: factory_bot
120
+ requirement: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '4.0'
125
+ type: :development
126
+ prerelease: false
127
+ version_requirements: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '4.0'
132
+ - !ruby/object:Gem::Dependency
133
+ name: guard
134
+ requirement: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ type: :development
140
+ prerelease: false
141
+ version_requirements: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ - !ruby/object:Gem::Dependency
147
+ name: guard-minitest
148
+ requirement: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ type: :development
154
+ prerelease: false
155
+ version_requirements: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ - !ruby/object:Gem::Dependency
161
+ name: minitest
162
+ requirement: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '5.0'
167
+ type: :development
168
+ prerelease: false
169
+ version_requirements: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: '5.0'
174
+ - !ruby/object:Gem::Dependency
175
+ name: minitest-implicit-subject
176
+ requirement: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ type: :development
182
+ prerelease: false
183
+ version_requirements: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ - !ruby/object:Gem::Dependency
189
+ name: rake
190
+ requirement: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - "~>"
193
+ - !ruby/object:Gem::Version
194
+ version: '10.0'
195
+ type: :development
196
+ prerelease: false
197
+ version_requirements: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - "~>"
200
+ - !ruby/object:Gem::Version
201
+ version: '10.0'
202
+ description:
203
+ email:
204
+ - tomas.celizna@gmail.com
205
+ - a@asgerbehnckejacobsen.dk
206
+ executables: []
207
+ extensions: []
208
+ extra_rdoc_files: []
209
+ files:
210
+ - ".gitignore"
211
+ - ".travis.yml"
212
+ - CHANGELOG.md
213
+ - Gemfile
214
+ - Guardfile
215
+ - README.md
216
+ - Rakefile
217
+ - bin/console
218
+ - bin/setup
219
+ - lib/mongoid_occurrence_views.rb
220
+ - lib/mongoid_occurrence_views/create_mongodb_view.rb
221
+ - lib/mongoid_occurrence_views/destroy_mongodb_view.rb
222
+ - lib/mongoid_occurrence_views/event.rb
223
+ - lib/mongoid_occurrence_views/event/create_expanded_occurrences_view.rb
224
+ - lib/mongoid_occurrence_views/event/create_occurrences_ordering_view.rb
225
+ - lib/mongoid_occurrence_views/event/create_view.rb
226
+ - lib/mongoid_occurrence_views/event/has_occurrence_scopes.rb
227
+ - lib/mongoid_occurrence_views/event/has_views_on_occurrences.rb
228
+ - lib/mongoid_occurrence_views/event/occurrence.rb
229
+ - lib/mongoid_occurrence_views/queries/occurs_between.rb
230
+ - lib/mongoid_occurrence_views/queries/occurs_from.rb
231
+ - lib/mongoid_occurrence_views/queries/occurs_on.rb
232
+ - lib/mongoid_occurrence_views/queries/occurs_until.rb
233
+ - lib/mongoid_occurrence_views/queries/order_by_end.rb
234
+ - lib/mongoid_occurrence_views/queries/order_by_start.rb
235
+ - lib/mongoid_occurrence_views/queries/query.rb
236
+ - lib/mongoid_occurrence_views/version.rb
237
+ - mongoid_occurrence_views.gemspec
238
+ homepage: https://github.com/tomasc/mongoid_occurrence_views
239
+ licenses:
240
+ - MIT
241
+ metadata: {}
242
+ post_install_message:
243
+ rdoc_options: []
244
+ require_paths:
245
+ - lib
246
+ required_ruby_version: !ruby/object:Gem::Requirement
247
+ requirements:
248
+ - - ">="
249
+ - !ruby/object:Gem::Version
250
+ version: '0'
251
+ required_rubygems_version: !ruby/object:Gem::Requirement
252
+ requirements:
253
+ - - ">="
254
+ - !ruby/object:Gem::Version
255
+ version: '0'
256
+ requirements: []
257
+ rubyforge_project:
258
+ rubygems_version: 2.7.6
259
+ signing_key:
260
+ specification_version: 4
261
+ summary: Makes one's life easier when working with events that have multiple occurrences,
262
+ or a recurring schedule.
263
+ test_files: []