reportable 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,4 @@
1
+ v1.0.0
2
+ ------
3
+
4
+ * Initial release of the new Reportable gem (formerly known as the ReportsAsSparkline plugin)
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008-2010 Marco Otte-Witte, Martin Kavalar
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.
@@ -0,0 +1,88 @@
1
+ Reportable
2
+ ==========
3
+
4
+ Reportable allows for the easy creation of reports based on `ActiveRecord` models.
5
+
6
+
7
+ Usage
8
+ -----
9
+
10
+ Usage is pretty easy. To declare a report on a model, simply define that the model provides a report:
11
+
12
+ class User < ActiveRecord::Base
13
+
14
+ reportable :registrations, :aggregation => :count
15
+
16
+ end
17
+
18
+ The `reportable` method takes a bunch more options which are described in the [API docs](http://rdoc.info/projects/saulabs/reportable). For example you could generate a report on
19
+ the number of updated users records per second or the number of registrations of users that have a last name that starts with `'A'` per month:
20
+
21
+ class User < ActiveRecord::Base
22
+
23
+ reportable :last_name_starting_with_a_registrations, :aggregation => :count, :grouping => :month, :conditions => ["last_name LIKE 'A%'"]
24
+
25
+ reportable :updated_per_second, :aggregation => :count, :grouping => :second, :date_column => :updated_at
26
+
27
+ end
28
+
29
+ For every declared report a method is generated on the model that returns the date:
30
+
31
+ User.registrations_report
32
+
33
+ User.last_name_starting_with_a_registrations_report
34
+
35
+ User.updated_per_second_report
36
+
37
+
38
+ Working with the data
39
+ ---------------------
40
+
41
+ The data is returned as an `Array` of `Array`s of `DateTime`s and `Float`s, e.g.:
42
+
43
+ [
44
+ [DateTime.now, 1.0],
45
+ [DateTime.now - 1.day, 2.0],
46
+ [DateTime.now - 2.days, 3.0]
47
+ ]
48
+
49
+ Reportable provides a helper method to generate a sparkline image from this data that you can use in your views:
50
+
51
+ <%= sparkline_tag(User.registrations_report) %>
52
+
53
+
54
+ Installation
55
+ ------------
56
+
57
+ To install Reportable, simply run
58
+
59
+ [sudo] gem install reportable
60
+
61
+ and add it to your application's dependencies in your `environment.rb`:
62
+
63
+ config.gem 'reportable', :lib => 'saulabs/reportable'
64
+
65
+ When you installed the gem, you have to generate the migration that creates Reportable's cache table:
66
+
67
+ ./script/generate reportable_migration create_reportable_cache
68
+
69
+ and migrate:
70
+
71
+ rake db:migrate
72
+
73
+
74
+ Plans
75
+ -----
76
+
77
+ * add support for Oracle and MSSQL
78
+ * add support for DataMapper
79
+ * add more options to generate graphs from the data
80
+ * add the option to generate textual reports on the command line
81
+
82
+
83
+ Authors
84
+ -------
85
+
86
+ © 2008-2010 Marco Otte-Witte (<http://simplabs.com>), Martin Kavalar (<http://www.sauspiel.de>)
87
+
88
+ Released under the MIT license
@@ -0,0 +1,29 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'bundler'
4
+
5
+ Bundler.setup
6
+ Bundler.require
7
+
8
+ require 'spec/rake/spectask'
9
+ require 'simplabs/excellent/rake'
10
+
11
+ desc 'Default: run specs.'
12
+ task :default => :spec
13
+
14
+ desc 'Run the specs'
15
+ Spec::Rake::SpecTask.new(:spec) do |t|
16
+ t.rcov_opts << '--exclude "gems/*,spec/*,init.rb"'
17
+ t.rcov = true
18
+ t.rcov_dir = 'doc/coverage'
19
+ t.spec_files = FileList['spec/**/*_spec.rb']
20
+ end
21
+
22
+ YARD::Rake::YardocTask.new(:doc) do |t|
23
+ t.files = ['lib/**/*.rb', '-', 'HISTORY.md']
24
+ t.options = ['--no-private', '--title', 'Reportable Documentation']
25
+ end
26
+
27
+ Simplabs::Excellent::Rake::ExcellentTask.new(:excellent) do |t|
28
+ t.paths = %w(lib)
29
+ end
@@ -0,0 +1,9 @@
1
+ class ReportableMigrationGenerator < Rails::Generator::NamedBase
2
+
3
+ def manifest
4
+ record do |m|
5
+ m.migration_template 'migration.erb', 'db/migrate'
6
+ end
7
+ end
8
+
9
+ end
@@ -0,0 +1,40 @@
1
+ class <%= class_name %> < ActiveRecord::Migration
2
+
3
+ def self.up
4
+ create_table :reportable_cache, :force => true do |t|
5
+ t.string :model_name, :null => false
6
+ t.string :report_name, :null => false
7
+ t.string :grouping, :null => false
8
+ t.string :aggregation, :null => false
9
+ t.string :condition, :null => false
10
+ t.float :value, :null => false, :default => 0
11
+ t.datetime :reporting_period, :null => false
12
+
13
+ t.timestamps
14
+ end
15
+
16
+ add_index :reportable_cache, [
17
+ :model_name,
18
+ :report_name,
19
+ :grouping,
20
+ :aggregation,
21
+ :condition
22
+ ], :name => :name_model_grouping_agregation
23
+ add_index :reportable_cache, [
24
+ :model_name,
25
+ :report_name,
26
+ :grouping,
27
+ :aggregation,
28
+ :condition,
29
+ :reporting_period
30
+ ], :unique => true, :name => :name_model_grouping_aggregation_period
31
+ end
32
+
33
+ def self.down
34
+ remove_index :reportable_cache, :name => :name_model_grouping_agregation
35
+ remove_index :reportable_cache, :name => :name_model_grouping_aggregation_period
36
+
37
+ drop_table :reportable_cache
38
+ end
39
+
40
+ end
@@ -0,0 +1,63 @@
1
+ module Saulabs
2
+
3
+ module Reportable
4
+
5
+ # Extends the {Saulabs::Reportable::ClassMethods#reportable} method into +base+.
6
+ #
7
+ def self.included(base)
8
+ base.extend ClassMethods
9
+ end
10
+
11
+ module ClassMethods
12
+
13
+ # Generates a report on a model. That report can then be executed via the new method +<name>_report+ (see documentation of {Saulabs::Reportable::Report#run}).
14
+ #
15
+ # @param [String] name
16
+ # the name of the report, also defines the name of the generated report method (+<name>_report+)
17
+ # @param [Hash] options
18
+ # the options to generate the reports with
19
+ #
20
+ # @option options [Symbol] :date_column (created_at)
21
+ # the name of the date column over that the records are aggregated
22
+ # @option options [String, Symbol] :value_column (:id)
23
+ # the name of the column that holds the values to aggregate when using a calculation aggregation like +:sum+
24
+ # @option options [Symbol] :aggregation (:count)
25
+ # the aggregation to use (one of +:count+, +:sum+, +:minimum+, +:maximum+ or +:average+); when using anything other than +:count+, +:value_column+ must also be specified
26
+ # @option options [Symbol] :grouping (:day)
27
+ # the period records are grouped in (+:hour+, +:day+, +:week+, +:month+); <b>Beware that <tt>reportable</tt> treats weeks as starting on monday!</b>
28
+ # @option options [Fixnum] :limit (100)
29
+ # the number of reporting periods to get (see +:grouping+)
30
+ # @option options [Hash] :conditions ({})
31
+ # conditions like in +ActiveRecord::Base#find+; only records that match these conditions are reported;
32
+ # @option options [Boolean] :live_data (false)
33
+ # specifies whether data for the current reporting period is to be read; <b>if +:live_data+ is +true+, you will experience a performance hit since the request cannot be satisfied from the cache alone</b>
34
+ # @option options [DateTime, Boolean] :end_date (false)
35
+ # when specified, the report will only include data for the +:limit+ reporting periods until this date.
36
+ #
37
+ # @example Declaring reports on a model
38
+ #
39
+ # class User < ActiveRecord::Base
40
+ # reportable :registrations, :aggregation => :count
41
+ # reportable :activations, :aggregation => :count, :date_column => :activated_at
42
+ # reportable :total_users, :cumulate => true
43
+ # reportable :rake, :aggregation => :sum, :value_column => :profile_visits
44
+ # end
45
+ def reportable(name, options = {})
46
+ (class << self; self; end).instance_eval do
47
+ define_method "#{name.to_s}_report".to_sym do |*args|
48
+ if options.delete(:cumulate)
49
+ report = Saulabs::Reportable::CumulatedReport.new(self, name, options)
50
+ else
51
+ report = Saulabs::Reportable::Report.new(self, name, options)
52
+ end
53
+ raise ArgumentError.new unless args.length == 0 || (args.length == 1 && args[0].is_a?(Hash))
54
+ report.run(args.length == 0 ? {} : args[0])
55
+ end
56
+ end
57
+ end
58
+
59
+ end
60
+
61
+ end
62
+
63
+ end
@@ -0,0 +1,42 @@
1
+ module Saulabs
2
+
3
+ module Reportable
4
+
5
+ # A special report class that cumulates all data (see {Saulabs::Reportable::Report})
6
+ #
7
+ # @example Cumulated reports as opposed to regular reports
8
+ #
9
+ # [[<DateTime today>, 1], [<DateTime yesterday>, 2], etc.]
10
+ # [[<DateTime today>, 3], [<DateTime yesterday>, 2], etc.]
11
+ #
12
+ class CumulatedReport < Report
13
+
14
+ # Runs the report (see {Saulabs::Reportable::Report#run})
15
+ #
16
+ def run(options = {})
17
+ cumulate(super, options_for_run(options))
18
+ end
19
+
20
+ private
21
+
22
+ def cumulate(data, options)
23
+ first_reporting_period = ReportingPeriod.first(options[:grouping], options[:limit], options[:end_date])
24
+ acc = initial_cumulative_value(first_reporting_period.date_time, options)
25
+ result = []
26
+ data.each do |element|
27
+ acc += element[1].to_f
28
+ result << [element[0], acc]
29
+ end
30
+ result
31
+ end
32
+
33
+ def initial_cumulative_value(date, options)
34
+ conditions = setup_conditions(nil, date, options[:conditions])
35
+ @klass.send(@aggregation, @value_column, :conditions => conditions)
36
+ end
37
+
38
+ end
39
+
40
+ end
41
+
42
+ end
@@ -0,0 +1,140 @@
1
+ module Saulabs
2
+
3
+ module Reportable
4
+
5
+ # The grouping specifies which records are grouped into one {Saulabs::Reportable::ReportingPeriod}.
6
+ #
7
+ class Grouping
8
+
9
+ # Initializes a new grouping.
10
+ #
11
+ # @param [Symbol] identifier
12
+ # the identifier of the grouping (one of +:hour+, +:day+, +:week+ or +:month+)
13
+ #
14
+ def initialize(identifier)
15
+ raise ArgumentError.new("Invalid grouping #{identifier}") unless [:hour, :day, :week, :month].include?(identifier)
16
+ @identifier = identifier
17
+ end
18
+
19
+ # Gets the identifier of the grouping.
20
+ #
21
+ # @return [Symbol]
22
+ # the identifier of the grouping.
23
+ #
24
+ def identifier
25
+ @identifier
26
+ end
27
+
28
+ # Gets an array of date parts from a DB string.
29
+ #
30
+ # @param [String] db_string
31
+ # the DB string to get the date parts from
32
+ #
33
+ # @return [Array<Fixnum>]
34
+ # array of numbers that represent the values of the date
35
+ #
36
+ def date_parts_from_db_string(db_string)
37
+ case ActiveRecord::Base.connection.adapter_name
38
+ when /mysql/i
39
+ from_mysql_db_string(db_string)
40
+ when /sqlite/i
41
+ from_sqlite_db_string(db_string)
42
+ when /postgres/i
43
+ from_postgresql_db_string(db_string)
44
+ end
45
+ end
46
+
47
+ # Converts the grouping into a DB specific string that can be used to group records.
48
+ #
49
+ # @param [String] date_column
50
+ # the name of the DB column that holds the date
51
+ #
52
+ def to_sql(date_column)
53
+ case ActiveRecord::Base.connection.adapter_name
54
+ when /mysql/i
55
+ mysql_format(date_column)
56
+ when /sqlite/i
57
+ sqlite_format(date_column)
58
+ when /postgres/i
59
+ postgresql_format(date_column)
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def from_mysql_db_string(db_string)
66
+ if @identifier == :week
67
+ parts = [db_string[0..3], db_string[4..5]].map(&:to_i)
68
+ else
69
+ db_string.split('/').map(&:to_i)
70
+ end
71
+ end
72
+
73
+ def from_sqlite_db_string(db_string)
74
+ if @identifier == :week
75
+ parts = db_string.split('-').map(&:to_i)
76
+ date = Date.new(parts[0], parts[1], parts[2])
77
+ return [date.cwyear, date.cweek]
78
+ end
79
+ db_string.split('/').map(&:to_i)
80
+ end
81
+
82
+ def from_postgresql_db_string(db_string)
83
+ case @identifier
84
+ when :hour
85
+ return (db_string[0..9].split('-') + [db_string[11..12]]).map(&:to_i)
86
+ when :day
87
+ return db_string[0..9].split('-').map(&:to_i)
88
+ when :week
89
+ parts = db_string[0..9].split('-').map(&:to_i)
90
+ date = Date.new(parts[0], parts[1], parts[2])
91
+ return [date.cwyear, date.cweek]
92
+ when :month
93
+ return db_string[0..6].split('-')[0..1].map(&:to_i)
94
+ end
95
+ end
96
+
97
+ def mysql_format(date_column)
98
+ case @identifier
99
+ when :hour
100
+ "DATE_FORMAT(#{date_column}, '%Y/%m/%d/%H')"
101
+ when :day
102
+ "DATE_FORMAT(#{date_column}, '%Y/%m/%d')"
103
+ when :week
104
+ "YEARWEEK(#{date_column}, 3)"
105
+ when :month
106
+ "DATE_FORMAT(#{date_column}, '%Y/%m')"
107
+ end
108
+ end
109
+
110
+ def sqlite_format(date_column)
111
+ case @identifier
112
+ when :hour
113
+ "strftime('%Y/%m/%d/%H', #{date_column})"
114
+ when :day
115
+ "strftime('%Y/%m/%d', #{date_column})"
116
+ when :week
117
+ "date(#{date_column}, 'weekday 0')"
118
+ when :month
119
+ "strftime('%Y/%m', #{date_column})"
120
+ end
121
+ end
122
+
123
+ def postgresql_format(date_column)
124
+ case @identifier
125
+ when :hour
126
+ "date_trunc('hour', #{date_column})"
127
+ when :day
128
+ "date_trunc('day', #{date_column})"
129
+ when :week
130
+ "date_trunc('week', #{date_column})"
131
+ when :month
132
+ "date_trunc('month', #{date_column})"
133
+ end
134
+ end
135
+
136
+ end
137
+
138
+ end
139
+
140
+ end
@@ -0,0 +1,165 @@
1
+ module Saulabs
2
+
3
+ module Reportable
4
+
5
+ # The Report class that does all the data retrieval and calculations.
6
+ #
7
+ class Report
8
+
9
+ # the model the report works on (This is the class you invoke {Saulabs::Reportable::ClassMethods#reportable} on)
10
+ #
11
+ attr_reader :klass
12
+
13
+ # the name of the report (as in {Saulabs::Reportable::ClassMethods#reportable})
14
+ #
15
+ attr_reader :name
16
+
17
+ # the name of the date column over that the records are aggregated
18
+ #
19
+ attr_reader :date_column
20
+
21
+ # the name of the column that holds the values to aggregate when using a calculation aggregation like +:sum+
22
+ #
23
+ attr_reader :value_column
24
+
25
+ # the aggregation to use (one of +:count+, +:sum+, +:minimum+, +:maximum+ or +:average+); when using anything other than +:count+, +:value_column+ must also be specified
26
+ #
27
+ attr_reader :aggregation
28
+
29
+ # options for the report
30
+ #
31
+ attr_reader :options
32
+
33
+ # Initializes a new {Saulabs::Reportable::Report}
34
+ #
35
+ # @param [Class] klass
36
+ # the model the report works on (This is the class you invoke {Saulabs::Reportable::ClassMethods#reportable} on)
37
+ # @param [String] name
38
+ # the name of the report (as in {Saulabs::Reportable::ClassMethods#reportable})
39
+ # @param [Hash] options
40
+ # options for the report creation
41
+ #
42
+ # @option options [Symbol] :date_column (created_at)
43
+ # the name of the date column over that the records are aggregated
44
+ # @option options [String, Symbol] :value_column (:id)
45
+ # the name of the column that holds the values to aggregate when using a calculation aggregation like +:sum+
46
+ # @option options [Symbol] :aggregation (:count)
47
+ # the aggregation to use (one of +:count+, +:sum+, +:minimum+, +:maximum+ or +:average+); when using anything other than +:count+, +:value_column+ must also be specified
48
+ # @option options [Symbol] :grouping (:day)
49
+ # the period records are grouped in (+:hour+, +:day+, +:week+, +:month+); <b>Beware that <tt>reportable</tt> treats weeks as starting on monday!</b>
50
+ # @option options [Fixnum] :limit (100)
51
+ # the number of reporting periods to get (see +:grouping+)
52
+ # @option options [Hash] :conditions ({})
53
+ # conditions like in +ActiveRecord::Base#find+; only records that match these conditions are reported;
54
+ # @option options [Boolean] :live_data (false)
55
+ # specifies whether data for the current reporting period is to be read; <b>if +:live_data+ is +true+, you will experience a performance hit since the request cannot be satisfied from the cache alone</b>
56
+ # @option options [DateTime, Boolean] :end_date (false)
57
+ # when specified, the report will only include data for the +:limit+ reporting periods until this date.
58
+ #
59
+ def initialize(klass, name, options = {})
60
+ ensure_valid_options(options)
61
+ @klass = klass
62
+ @name = name
63
+ @date_column = (options[:date_column] || 'created_at').to_s
64
+ @aggregation = options[:aggregation] || :count
65
+ @value_column = (options[:value_column] || (@aggregation == :count ? 'id' : name)).to_s
66
+ @options = {
67
+ :limit => options[:limit] || 100,
68
+ :conditions => options[:conditions] || [],
69
+ :grouping => Grouping.new(options[:grouping] || :day),
70
+ :live_data => options[:live_data] || false,
71
+ :end_date => options[:end_date] || false
72
+ }
73
+ @options.merge!(options)
74
+ @options.freeze
75
+ end
76
+
77
+ # Runs the report and returns an array of array of DateTimes and Floats
78
+ #
79
+ # @param [Hash] options
80
+ # options to run the report with
81
+ #
82
+ # @option options [Symbol] :grouping (:day)
83
+ # the period records are grouped in (+:hour+, +:day+, +:week+, +:month+); <b>Beware that <tt>reportable</tt> treats weeks as starting on monday!</b>
84
+ # @option options [Fixnum] :limit (100)
85
+ # the number of reporting periods to get (see +:grouping+)
86
+ # @option options [Hash] :conditions ({})
87
+ # conditions like in +ActiveRecord::Base#find+; only records that match these conditions are reported;
88
+ # @option options [Boolean] :live_data (false)
89
+ # specifies whether data for the current reporting period is to be read; <b>if +:live_data+ is +true+, you will experience a performance hit since the request cannot be satisfied from the cache alone</b>
90
+ # @option options [DateTime, Boolean] :end_date (false)
91
+ # when specified, the report will only include data for the +:limit+ reporting periods until this date.
92
+ #
93
+ # @return [Array<Array<DateTime, Float>>]
94
+ # the result of the report as pairs of {DateTime}s and {Float}s
95
+ #
96
+ def run(options = {})
97
+ options = options_for_run(options)
98
+ ReportCache.process(self, options) do |begin_at, end_at|
99
+ read_data(begin_at, end_at, options)
100
+ end
101
+ end
102
+
103
+ private
104
+
105
+ def options_for_run(options = {})
106
+ options = options.dup
107
+ ensure_valid_options(options, :run)
108
+ options.reverse_merge!(@options)
109
+ options[:grouping] = Grouping.new(options[:grouping]) unless options[:grouping].is_a?(Grouping)
110
+ return options
111
+ end
112
+
113
+ def read_data(begin_at, end_at, options)
114
+ conditions = setup_conditions(begin_at, end_at, options[:conditions])
115
+ @klass.send(@aggregation,
116
+ @value_column,
117
+ :conditions => conditions,
118
+ :group => options[:grouping].to_sql(@date_column),
119
+ :order => "#{options[:grouping].to_sql(@date_column)} ASC",
120
+ :limit => options[:limit]
121
+ )
122
+ end
123
+
124
+ def setup_conditions(begin_at, end_at, custom_conditions = [])
125
+ conditions = [@klass.send(:sanitize_sql_for_conditions, custom_conditions) || '']
126
+ conditions[0] += "#{(conditions[0].blank? ? '' : ' AND ')}#{ActiveRecord::Base.connection.quote_table_name(@klass.table_name)}.#{ActiveRecord::Base.connection.quote_column_name(@date_column.to_s)} "
127
+ conditions[0] += if begin_at && end_at
128
+ 'BETWEEN ? AND ?'
129
+ elsif begin_at
130
+ '>= ?'
131
+ elsif end_at
132
+ '<= ?'
133
+ else
134
+ raise ArgumentError.new('You must pass either begin_at, end_at or both to setup_conditions.')
135
+ end
136
+ conditions << begin_at if begin_at
137
+ conditions << end_at if end_at
138
+ conditions
139
+ end
140
+
141
+ def ensure_valid_options(options, context = :initialize)
142
+ case context
143
+ when :initialize
144
+ options.each_key do |k|
145
+ raise ArgumentError.new("Invalid option #{k}!") unless [:limit, :aggregation, :grouping, :date_column, :value_column, :conditions, :live_data, :end_date].include?(k)
146
+ end
147
+ raise ArgumentError.new("Invalid aggregation #{options[:aggregation]}!") if options[:aggregation] && ![:count, :sum, :maximum, :minimum, :average].include?(options[:aggregation])
148
+ raise ArgumentError.new('The name of the column holding the value to sum has to be specified for aggregation :sum!') if [:sum, :maximum, :minimum, :average].include?(options[:aggregation]) && !options.key?(:value_column)
149
+ when :run
150
+ options.each_key do |k|
151
+ raise ArgumentError.new("Invalid option #{k}!") unless [:limit, :conditions, :grouping, :live_data, :end_date].include?(k)
152
+ end
153
+ end
154
+ raise ArgumentError.new('Options :live_data and :end_date may not both be specified!') if options[:live_data] && options[:end_date]
155
+ raise ArgumentError.new("Invalid grouping #{options[:grouping]}!") if options[:grouping] && ![:hour, :day, :week, :month].include?(options[:grouping])
156
+ raise ArgumentError.new("Invalid conditions: #{options[:conditions].inspect}!") if options[:conditions] && !options[:conditions].is_a?(Array) && !options[:conditions].is_a?(Hash)
157
+ raise ArgumentError.new("Invalid end date: #{options[:end_date].inspect}; must be a DateTime!") if options[:end_date] && !options[:end_date].is_a?(DateTime) && !options[:end_date].is_a?(Time)
158
+ raise ArgumentError.new('End date may not be in the future!') if options[:end_date] && options[:end_date] > DateTime.now
159
+ end
160
+
161
+ end
162
+
163
+ end
164
+
165
+ end