forest_liana 3.3.0 → 4.0.0.pre.beta.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 +4 -4
- data/app/controllers/forest_liana/application_controller.rb +9 -2
- data/app/services/forest_liana/filters_parser.rb +266 -0
- data/app/services/forest_liana/line_stat_getter.rb +2 -11
- data/app/services/forest_liana/login_handler.rb +7 -19
- data/app/services/forest_liana/operator_date_interval_parser.rb +105 -109
- data/app/services/forest_liana/pie_stat_getter.rb +4 -13
- data/app/services/forest_liana/search_query_builder.rb +4 -123
- data/app/services/forest_liana/value_stat_getter.rb +10 -39
- data/lib/forest_liana.rb +0 -10
- data/lib/forest_liana/bootstraper.rb +2 -1
- data/lib/forest_liana/version.rb +1 -1
- data/spec/dummy/app/models/user.rb +2 -0
- data/spec/dummy/db/migrate/20190716130830_add_age_to_tree.rb +5 -0
- data/spec/dummy/db/migrate/20190716135241_add_type_to_user.rb +5 -0
- data/spec/dummy/db/schema.rb +3 -1
- data/spec/requests/resources_spec.rb +15 -3
- data/spec/services/forest_liana/filters_parser_spec.rb +476 -0
- data/test/forest_liana_test.rb +0 -18
- data/test/services/forest_liana/resources_getter_test.rb +52 -27
- data/test/services/forest_liana/value_stat_getter_test.rb +13 -13
- metadata +11 -5
- data/app/services/forest_liana/operator_value_parser.rb +0 -155
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dafadb3ed7cf1c34d0010729e459c13ba38e22de
|
|
4
|
+
data.tar.gz: 6dff3ee72a4aa231b68508e3d87b0931fb16d718
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 48338ecfc6319ca9ab824fa47771167e9203e3bbe567b3d38ff907b0ae067dc01f20e5830ed7079bdeb67115d54ede8570ce53125a738a016874b90bbc76979f
|
|
7
|
+
data.tar.gz: 052c0e82c436681276c2982480002adcc107b7e0b0018f1bd7056af1e6f6d5f3e281e9f9022d6b432b798224d08595bf9da81da04ab5c1fb52a87c2071c56d08
|
|
@@ -25,10 +25,11 @@ module ForestLiana
|
|
|
25
25
|
# NOTICE: The Forest user email is returned to track changes made using
|
|
26
26
|
# Forest with Papertrail.
|
|
27
27
|
define_method :user_for_paper_trail do
|
|
28
|
-
|
|
28
|
+
@jwt_decoded_token['email']
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
+
# NOTICE: Helper method for Smart Routes logic based on current user info.
|
|
32
33
|
def forest_user
|
|
33
34
|
@jwt_decoded_token
|
|
34
35
|
end
|
|
@@ -69,7 +70,13 @@ module ForestLiana
|
|
|
69
70
|
|
|
70
71
|
@jwt_decoded_token = JWT.decode(token, ForestLiana.auth_secret, true,
|
|
71
72
|
{ algorithm: 'HS256' }).try(:first)
|
|
72
|
-
|
|
73
|
+
|
|
74
|
+
# NOTICE: Automatically logs out the users that use tokens having an old data format.
|
|
75
|
+
if @jwt_decoded_token['data']
|
|
76
|
+
raise ForestLiana::Errors::HTTP401Error.new("Your token format is invalid, please login again.")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
@rendering_id = @jwt_decoded_token['rendering_id']
|
|
73
80
|
else
|
|
74
81
|
head :unauthorized
|
|
75
82
|
end
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
module ForestLiana
|
|
2
|
+
class FiltersParser
|
|
3
|
+
AGGREGATOR_OPERATOR = %w(and or)
|
|
4
|
+
|
|
5
|
+
def initialize(filters, resource, timezone)
|
|
6
|
+
begin
|
|
7
|
+
@filters = JSON.parse(filters)
|
|
8
|
+
rescue JSON::ParserError
|
|
9
|
+
raise ForestLiana::Errors::HTTP422Error.new('Invalid filters JSON format')
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
@resource = resource
|
|
13
|
+
@operator_date_parser = OperatorDateIntervalParser.new(timezone)
|
|
14
|
+
@joins = []
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def apply_filters
|
|
18
|
+
return @resource unless @filters
|
|
19
|
+
|
|
20
|
+
where = parse_aggregation(@filters)
|
|
21
|
+
return @resource unless where
|
|
22
|
+
|
|
23
|
+
@joins.each do |join|
|
|
24
|
+
current_resource = @resource.reflect_on_association(join.name).klass
|
|
25
|
+
current_resource.include(ArelHelpers::Aliases)
|
|
26
|
+
current_resource.aliased_as(join.name) do |aliased_resource|
|
|
27
|
+
@resource = @resource.joins(ArelHelpers.join_association(@resource, join.name, Arel::Nodes::OuterJoin, aliases: [aliased_resource]))
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
@resource.where(where)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def parse_aggregation(node)
|
|
35
|
+
ensure_valid_aggregation(node)
|
|
36
|
+
|
|
37
|
+
return parse_condition(node) unless node['aggregator']
|
|
38
|
+
|
|
39
|
+
conditions = []
|
|
40
|
+
node['conditions'].each do |condition|
|
|
41
|
+
conditions.push(parse_aggregation(condition))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
operator = parse_aggregation_operator(node['aggregator'])
|
|
45
|
+
|
|
46
|
+
conditions.empty? ? nil : "(#{conditions.join(" #{operator} ")})"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def parse_condition(condition)
|
|
50
|
+
ensure_valid_condition(condition)
|
|
51
|
+
|
|
52
|
+
operator = condition['operator']
|
|
53
|
+
value = condition['value']
|
|
54
|
+
field = condition['field']
|
|
55
|
+
|
|
56
|
+
if @operator_date_parser.is_date_operator?(operator)
|
|
57
|
+
condition = @operator_date_parser.get_date_filter(operator, value)
|
|
58
|
+
return "#{parse_field_name(field)} #{condition}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
if is_belongs_to(field)
|
|
62
|
+
association = field.partition(':').first.to_sym
|
|
63
|
+
association_field = field.partition(':').last
|
|
64
|
+
|
|
65
|
+
unless @resource.reflect_on_association(association)
|
|
66
|
+
raise ForestLiana::Errors::HTTP422Error.new("Association '#{association}' not found")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
current_resource = @resource.reflect_on_association(association).klass
|
|
70
|
+
else
|
|
71
|
+
association_field = field
|
|
72
|
+
current_resource = @resource
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# NOTICE: Set the integer value instead of a string if "enum" type
|
|
76
|
+
# NOTICE: Rails 3 do not have a defined_enums method
|
|
77
|
+
if current_resource.respond_to?(:defined_enums) && current_resource.defined_enums.has_key?(association_field)
|
|
78
|
+
value = current_resource.defined_enums[association_field][value]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
parsed_field = parse_field_name(field)
|
|
82
|
+
parsed_operator = parse_operator(operator)
|
|
83
|
+
parsed_value = parse_value(operator, value)
|
|
84
|
+
|
|
85
|
+
if Rails::VERSION::MAJOR >= 5
|
|
86
|
+
ActiveRecord::Base.sanitize_sql(["#{parsed_field} #{parsed_operator} ?", parsed_value])
|
|
87
|
+
else
|
|
88
|
+
"#{parsed_field} #{parsed_operator} #{ActiveRecord::Base.sanitize(parsed_value)}"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def parse_aggregation_operator(aggregator_operator)
|
|
93
|
+
unless AGGREGATOR_OPERATOR.include?(aggregator_operator)
|
|
94
|
+
raise_unknown_operator_error(aggregator_operator)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
aggregator_operator.upcase
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def parse_operator(operator)
|
|
101
|
+
case operator
|
|
102
|
+
when 'not'
|
|
103
|
+
'NOT'
|
|
104
|
+
when 'greater_than', 'after'
|
|
105
|
+
'>'
|
|
106
|
+
when 'less_than', 'before'
|
|
107
|
+
'<'
|
|
108
|
+
when 'contains', 'starts_with', 'ends_with'
|
|
109
|
+
'LIKE'
|
|
110
|
+
when 'not_contains'
|
|
111
|
+
'NOT LIKE'
|
|
112
|
+
when 'not_equal'
|
|
113
|
+
'!='
|
|
114
|
+
when 'equal'
|
|
115
|
+
'='
|
|
116
|
+
when 'blank'
|
|
117
|
+
'IS'
|
|
118
|
+
when 'present'
|
|
119
|
+
'IS NOT'
|
|
120
|
+
else
|
|
121
|
+
raise_unknown_operator_error(operator)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def parse_value(operator, value)
|
|
126
|
+
case operator
|
|
127
|
+
when 'not', 'greater_than', 'less_than', 'not_equal', 'equal', 'before', 'after'
|
|
128
|
+
value
|
|
129
|
+
when 'contains', 'not_contains'
|
|
130
|
+
"%#{value}%"
|
|
131
|
+
when 'starts_with'
|
|
132
|
+
"#{value}%"
|
|
133
|
+
when 'ends_with'
|
|
134
|
+
"%#{value}"
|
|
135
|
+
when 'present', 'blank'
|
|
136
|
+
else
|
|
137
|
+
raise_unknown_operator_error(operator)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def parse_field_name(field)
|
|
142
|
+
if is_belongs_to(field)
|
|
143
|
+
current_resource = @resource.reflect_on_association(field.split(':').first.to_sym)&.klass
|
|
144
|
+
raise ForestLiana::Errors::HTTP422Error.new("Field '#{field}' not found") unless current_resource
|
|
145
|
+
|
|
146
|
+
association = get_association_name_for_condition(field)
|
|
147
|
+
quoted_table_name = ActiveRecord::Base.connection.quote_column_name(association)
|
|
148
|
+
quoted_field_name = ActiveRecord::Base.connection.quote_column_name(field.split(':')[1])
|
|
149
|
+
else
|
|
150
|
+
quoted_table_name = @resource.quoted_table_name
|
|
151
|
+
quoted_field_name = ActiveRecord::Base.connection.quote_column_name(field)
|
|
152
|
+
current_resource = @resource
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
column_found = current_resource.columns.find { |column| column.name == field.split(':').last }
|
|
156
|
+
|
|
157
|
+
if column_found.nil?
|
|
158
|
+
raise ForestLiana::Errors::HTTP422Error.new("Field '#{field}' not found")
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
"#{quoted_table_name}.#{quoted_field_name}"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def is_belongs_to(field)
|
|
165
|
+
field.include?(':')
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def get_association_name_for_condition(field)
|
|
169
|
+
field, subfield = field.split(':')
|
|
170
|
+
|
|
171
|
+
association = @resource.reflect_on_association(field.to_sym)
|
|
172
|
+
return nil if association.blank?
|
|
173
|
+
|
|
174
|
+
@joins << association unless @joins.include? association
|
|
175
|
+
|
|
176
|
+
association.name
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# NOTICE: Look for a previous interval condition matching the following:
|
|
180
|
+
# - If the filter is a simple condition at the root the check is done right away.
|
|
181
|
+
# - There can't be a previous interval condition if the aggregator is 'or' (no meaning).
|
|
182
|
+
# - The condition's operator has to be elligible for a previous interval.
|
|
183
|
+
# - There can't be two previous interval condition.
|
|
184
|
+
def get_previous_interval_condition
|
|
185
|
+
current_previous_interval = nil
|
|
186
|
+
# NOTICE: Leaf condition at root
|
|
187
|
+
unless @filters['aggregator']
|
|
188
|
+
return @filters if @operator_date_parser.has_previous_interval?(@filters['operator'])
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
if @filters['aggregator'] === 'and'
|
|
192
|
+
@filters['conditions'].each do |condition|
|
|
193
|
+
# NOTICE: Nested conditions
|
|
194
|
+
return nil if condition['aggregator']
|
|
195
|
+
|
|
196
|
+
if @operator_date_parser.has_previous_interval?(condition['operator'])
|
|
197
|
+
# NOTICE: There can't be two previous_interval.
|
|
198
|
+
return nil if current_previous_interval
|
|
199
|
+
|
|
200
|
+
current_previous_interval = condition
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
current_previous_interval
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def apply_filters_on_previous_interval(previous_condition)
|
|
209
|
+
# Ressource should have already been joined
|
|
210
|
+
where = parse_aggregation_on_previous_interval(@filters, previous_condition)
|
|
211
|
+
|
|
212
|
+
@resource.where(where)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def parse_aggregation_on_previous_interval(node, previous_condition)
|
|
216
|
+
raise_empty_condition_in_filter_error unless node
|
|
217
|
+
|
|
218
|
+
return parse_previous_interval_condition(node) unless node['aggregator']
|
|
219
|
+
|
|
220
|
+
conditions = []
|
|
221
|
+
node['conditions'].each do |condition|
|
|
222
|
+
if condition == previous_condition
|
|
223
|
+
conditions.push(parse_previous_interval_condition(condition))
|
|
224
|
+
else
|
|
225
|
+
conditions.push(parse_aggregation(condition))
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
operator = parse_aggregation_operator(node['aggregator'])
|
|
230
|
+
|
|
231
|
+
conditions.empty? ? nil : "(#{conditions.join(" #{operator} ")})"
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def parse_previous_interval_condition(condition)
|
|
235
|
+
raise_empty_condition_in_filter_error unless condition
|
|
236
|
+
|
|
237
|
+
parsed_condition = @operator_date_parser.get_date_filter_for_previous_interval(
|
|
238
|
+
condition['operator'],
|
|
239
|
+
condition['value']
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
"#{parse_field_name(condition['field'])} #{parsed_condition}"
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def raise_unknown_operator_error(operator)
|
|
246
|
+
raise ForestLiana::Errors::HTTP422Error.new("Unknown provided operator '#{operator}'")
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def raise_empty_condition_in_filter_error
|
|
250
|
+
raise ForestLiana::Errors::HTTP422Error.new('Empty condition in filter')
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def ensure_valid_aggregation(node)
|
|
254
|
+
raise ForestLiana::Errors::HTTP422Error.new('Filters cannot be a raw value') unless node.is_a?(Hash)
|
|
255
|
+
raise_empty_condition_in_filter_error if node.empty?
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def ensure_valid_condition(condition)
|
|
259
|
+
raise_empty_condition_in_filter_error if condition.empty?
|
|
260
|
+
raise ForestLiana::Errors::HTTP422Error.new('Condition cannot be a raw value') unless condition.is_a?(Hash)
|
|
261
|
+
unless condition['field'].is_a?(String) and condition['operator'].is_a?(String)
|
|
262
|
+
raise ForestLiana::Errors::HTTP422Error.new('Invalid condition format')
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
@@ -22,17 +22,8 @@ module ForestLiana
|
|
|
22
22
|
def perform
|
|
23
23
|
value = get_resource().eager_load(@includes)
|
|
24
24
|
|
|
25
|
-
if @params[:
|
|
26
|
-
|
|
27
|
-
filter_operator = " #{@params[:filterType]} ".upcase
|
|
28
|
-
|
|
29
|
-
@params[:filters].try(:each) do |filter|
|
|
30
|
-
operator, filter_value = OperatorValueParser.parse(filter[:value])
|
|
31
|
-
conditions << OperatorValueParser.get_condition(filter[:field],
|
|
32
|
-
operator, filter_value, @resource, @params[:timezone])
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
value = value.where(conditions.join(filter_operator))
|
|
25
|
+
if @params[:filters]
|
|
26
|
+
value = FiltersParser.new(@params[:filters], value, @params[:timezone]).apply_filters
|
|
36
27
|
end
|
|
37
28
|
|
|
38
29
|
value = value.send(time_range, group_by_date_field, {
|
|
@@ -94,25 +94,13 @@ module ForestLiana
|
|
|
94
94
|
|
|
95
95
|
def create_token(user, rendering_id)
|
|
96
96
|
JWT.encode({
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
last_name: user['last_name'],
|
|
105
|
-
teams: user['teams']
|
|
106
|
-
},
|
|
107
|
-
relationships: {
|
|
108
|
-
renderings: {
|
|
109
|
-
data: [{
|
|
110
|
-
type: 'renderings',
|
|
111
|
-
id: rendering_id
|
|
112
|
-
}]
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
97
|
+
id: user['id'],
|
|
98
|
+
email: user['email'],
|
|
99
|
+
first_name: user['first_name'],
|
|
100
|
+
last_name: user['last_name'],
|
|
101
|
+
team: user['teams'][0],
|
|
102
|
+
rendering_id: rendering_id,
|
|
103
|
+
exp: Time.now.to_i + 2.weeks.to_i
|
|
116
104
|
}, ForestLiana.auth_secret, 'HS256')
|
|
117
105
|
end
|
|
118
106
|
end
|
|
@@ -1,67 +1,68 @@
|
|
|
1
1
|
module ForestLiana
|
|
2
2
|
class OperatorDateIntervalParser
|
|
3
|
+
OPERATOR_PAST = 'past'
|
|
4
|
+
OPERATOR_FUTURE = 'future'
|
|
5
|
+
OPERATOR_TODAY = 'today'
|
|
6
|
+
|
|
7
|
+
OPERATOR_YESTERDAY = 'yesterday'
|
|
8
|
+
OPERATOR_PREVIOUS_WEEK = 'previous_week'
|
|
9
|
+
OPERATOR_PREVIOUS_MONTH = 'previous_month'
|
|
10
|
+
OPERATOR_PREVIOUS_QUARTER = 'previous_quarter'
|
|
11
|
+
OPERATOR_PREVIOUS_YEAR = 'previous_year'
|
|
12
|
+
OPERATOR_PREVIOUS_WEEK_TO_DATE = 'previous_week_to_date'
|
|
13
|
+
OPERATOR_PREVIOUS_MONTH_TO_DATE = 'previous_month_to_date'
|
|
14
|
+
OPERATOR_PREVIOUS_QUARTER_TO_DATE = 'previous_quarter_to_date'
|
|
15
|
+
OPERATOR_PREVIOUS_YEAR_TO_DATE = 'previous_year_to_date'
|
|
16
|
+
|
|
17
|
+
OPERATOR_PREVIOUS_X_DAYS = 'previous_x_days'
|
|
18
|
+
OPERATOR_PREVIOUS_X_DAYS_TO_DATE = 'previous_x_days_to_date'
|
|
19
|
+
OPERATOR_BEFORE_X_HOURS_AGO = 'before_x_hours_ago'
|
|
20
|
+
OPERATOR_AFTER_X_HOURS_AGO = 'after_x_hours_ago'
|
|
21
|
+
|
|
3
22
|
PERIODS = {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
period_of_time: 'quarter', to_date: true },
|
|
14
|
-
:$yearToDate => { duration: 1, period: 'year', to_date: true }
|
|
23
|
+
OPERATOR_YESTERDAY => { duration: 1, period: 'day' },
|
|
24
|
+
OPERATOR_PREVIOUS_WEEK => { duration: 1, period: 'week' },
|
|
25
|
+
OPERATOR_PREVIOUS_WEEK_TO_DATE => { duration: 1, period: 'week', to_date: true },
|
|
26
|
+
OPERATOR_PREVIOUS_MONTH => { duration: 1, period: 'month' },
|
|
27
|
+
OPERATOR_PREVIOUS_MONTH_TO_DATE => { duration: 1, period: 'month', to_date: true },
|
|
28
|
+
OPERATOR_PREVIOUS_QUARTER => { duration: 3, period: 'month', period_of_time: 'quarter' },
|
|
29
|
+
OPERATOR_PREVIOUS_QUARTER_TO_DATE => { duration: 3, period: 'month', period_of_time: 'quarter', to_date: true },
|
|
30
|
+
OPERATOR_PREVIOUS_YEAR => { duration: 1, period: 'year' },
|
|
31
|
+
OPERATOR_PREVIOUS_YEAR_TO_DATE => { duration: 1, period: 'year', to_date: true }
|
|
15
32
|
}
|
|
16
33
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
34
|
+
DATE_OPERATORS_HAVING_PREVIOUS_INTERVAL = [
|
|
35
|
+
OPERATOR_TODAY,
|
|
36
|
+
OPERATOR_YESTERDAY,
|
|
37
|
+
OPERATOR_PREVIOUS_WEEK,
|
|
38
|
+
OPERATOR_PREVIOUS_MONTH,
|
|
39
|
+
OPERATOR_PREVIOUS_QUARTER,
|
|
40
|
+
OPERATOR_PREVIOUS_YEAR,
|
|
41
|
+
OPERATOR_PREVIOUS_WEEK_TO_DATE,
|
|
42
|
+
OPERATOR_PREVIOUS_MONTH_TO_DATE,
|
|
43
|
+
OPERATOR_PREVIOUS_QUARTER_TO_DATE,
|
|
44
|
+
OPERATOR_PREVIOUS_YEAR_TO_DATE,
|
|
45
|
+
OPERATOR_PREVIOUS_X_DAYS,
|
|
46
|
+
OPERATOR_PREVIOUS_X_DAYS_TO_DATE
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
DATE_OPERATORS = DATE_OPERATORS_HAVING_PREVIOUS_INTERVAL.concat [
|
|
50
|
+
OPERATOR_FUTURE,
|
|
51
|
+
OPERATOR_PAST,
|
|
52
|
+
OPERATOR_BEFORE_X_HOURS_AGO,
|
|
53
|
+
OPERATOR_AFTER_X_HOURS_AGO
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
def initialize(timezone)
|
|
28
57
|
@timezone_offset = Time.now.in_time_zone(timezone).utc_offset / 3600
|
|
29
58
|
end
|
|
30
59
|
|
|
31
|
-
def
|
|
32
|
-
|
|
33
|
-
return true if PERIODS[@value.to_sym]
|
|
34
|
-
|
|
35
|
-
return true if [PERIODS_PAST, PERIODS_FUTURE, PERIODS_TODAY].include? @value
|
|
36
|
-
|
|
37
|
-
match = PERIODS_PREVIOUS_X_DAYS.match(@value)
|
|
38
|
-
return true if match && match[1]
|
|
39
|
-
|
|
40
|
-
match = PERIODS_X_DAYS_TO_DATE.match(@value)
|
|
41
|
-
return true if match && match[1]
|
|
42
|
-
|
|
43
|
-
match = PERIODS_X_HOURS_BEFORE.match(@value)
|
|
44
|
-
return true if match && match[1]
|
|
45
|
-
|
|
46
|
-
match = PERIODS_X_HOURS_AFTER.match(@value)
|
|
47
|
-
return true if match && match[1]
|
|
48
|
-
|
|
49
|
-
false
|
|
60
|
+
def is_date_operator?(operator)
|
|
61
|
+
DATE_OPERATORS.include? operator
|
|
50
62
|
end
|
|
51
63
|
|
|
52
|
-
def has_previous_interval
|
|
53
|
-
|
|
54
|
-
return true if PERIODS[@value.to_sym]
|
|
55
|
-
|
|
56
|
-
return true if PERIODS_TODAY == @value
|
|
57
|
-
|
|
58
|
-
match = PERIODS_PREVIOUS_X_DAYS.match(@value)
|
|
59
|
-
return true if match && match[1]
|
|
60
|
-
|
|
61
|
-
match = PERIODS_X_DAYS_TO_DATE.match(@value)
|
|
62
|
-
return true if match && match[1]
|
|
63
|
-
|
|
64
|
-
false
|
|
64
|
+
def has_previous_interval?(operator)
|
|
65
|
+
DATE_OPERATORS_HAVING_PREVIOUS_INTERVAL.include? operator
|
|
65
66
|
end
|
|
66
67
|
|
|
67
68
|
def to_client_timezone(date)
|
|
@@ -69,46 +70,39 @@ module ForestLiana
|
|
|
69
70
|
date - @timezone_offset.hours
|
|
70
71
|
end
|
|
71
72
|
|
|
72
|
-
def
|
|
73
|
-
return nil unless
|
|
74
|
-
|
|
75
|
-
return ">= '#{Time.now}'" if @value == PERIODS_FUTURE
|
|
76
|
-
return "<= '#{Time.now}'" if @value == PERIODS_PAST
|
|
73
|
+
def get_date_filter(operator, value)
|
|
74
|
+
return nil unless is_date_operator? operator
|
|
77
75
|
|
|
78
|
-
|
|
76
|
+
case operator
|
|
77
|
+
when OPERATOR_FUTURE
|
|
78
|
+
return ">= '#{Time.now}'"
|
|
79
|
+
when OPERATOR_PAST
|
|
80
|
+
return "<= '#{Time.now}'"
|
|
81
|
+
when OPERATOR_TODAY
|
|
79
82
|
return "BETWEEN '#{to_client_timezone(Time.now.beginning_of_day)}' " +
|
|
80
83
|
"AND '#{to_client_timezone(Time.now.end_of_day)}'"
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
match = PERIODS_PREVIOUS_X_DAYS.match(@value)
|
|
84
|
-
if match && match[1]
|
|
84
|
+
when OPERATOR_PREVIOUS_X_DAYS
|
|
85
|
+
ensure_integer_value(value)
|
|
85
86
|
return "BETWEEN '" +
|
|
86
|
-
"#{to_client_timezone(Integer(
|
|
87
|
+
"#{to_client_timezone(Integer(value).day.ago.beginning_of_day)}'" +
|
|
87
88
|
" AND '#{to_client_timezone(1.day.ago.end_of_day)}'"
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
match = PERIODS_X_DAYS_TO_DATE.match(@value)
|
|
91
|
-
if match && match[1]
|
|
89
|
+
when OPERATOR_PREVIOUS_X_DAYS_TO_DATE
|
|
90
|
+
ensure_integer_value(value)
|
|
92
91
|
return "BETWEEN '" +
|
|
93
|
-
"#{to_client_timezone((Integer(
|
|
92
|
+
"#{to_client_timezone((Integer(value) - 1).day.ago.beginning_of_day)}'" +
|
|
94
93
|
" AND '#{Time.now}'"
|
|
94
|
+
when OPERATOR_BEFORE_X_HOURS_AGO
|
|
95
|
+
ensure_integer_value(value)
|
|
96
|
+
return "< '#{to_client_timezone((Integer(value)).hour.ago)}'"
|
|
97
|
+
when OPERATOR_AFTER_X_HOURS_AGO
|
|
98
|
+
ensure_integer_value(value)
|
|
99
|
+
return "> '#{to_client_timezone((Integer(value)).hour.ago)}'"
|
|
95
100
|
end
|
|
96
101
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
match = PERIODS_X_HOURS_AFTER.match(@value)
|
|
103
|
-
if match && match[1]
|
|
104
|
-
return "> '#{to_client_timezone((Integer(match[1])).hour.ago)}'"
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
duration = PERIODS[@value.to_sym][:duration]
|
|
108
|
-
period = PERIODS[@value.to_sym][:period]
|
|
109
|
-
period_of_time = PERIODS[@value.to_sym][:period_of_time] ||
|
|
110
|
-
PERIODS[@value.to_sym][:period]
|
|
111
|
-
to_date = PERIODS[@value.to_sym][:to_date]
|
|
102
|
+
duration = PERIODS[operator][:duration]
|
|
103
|
+
period = PERIODS[operator][:period]
|
|
104
|
+
period_of_time = PERIODS[operator][:period_of_time] || period
|
|
105
|
+
to_date = PERIODS[operator][:to_date]
|
|
112
106
|
|
|
113
107
|
if to_date
|
|
114
108
|
from = to_client_timezone(Time.now.send("beginning_of_#{period_of_time}"))
|
|
@@ -122,45 +116,47 @@ module ForestLiana
|
|
|
122
116
|
"BETWEEN '#{from}' AND '#{to}'"
|
|
123
117
|
end
|
|
124
118
|
|
|
125
|
-
def
|
|
126
|
-
return nil unless has_previous_interval
|
|
119
|
+
def get_date_filter_for_previous_interval(operator, value)
|
|
120
|
+
return nil unless has_previous_interval? operator
|
|
127
121
|
|
|
128
|
-
|
|
122
|
+
case operator
|
|
123
|
+
when OPERATOR_TODAY
|
|
129
124
|
return "BETWEEN '#{to_client_timezone(1.day.ago.beginning_of_day)}' AND " +
|
|
130
125
|
"'#{to_client_timezone(1.day.ago.end_of_day)}'"
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
match = PERIODS_PREVIOUS_X_DAYS.match(@value)
|
|
134
|
-
if match && match[1]
|
|
126
|
+
when OPERATOR_PREVIOUS_X_DAYS
|
|
127
|
+
ensure_integer_value(value)
|
|
135
128
|
return "BETWEEN '" +
|
|
136
|
-
"#{to_client_timezone((Integer(
|
|
137
|
-
" AND '#{to_client_timezone((Integer(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
match = PERIODS_X_DAYS_TO_DATE.match(@value)
|
|
141
|
-
if match && match[1]
|
|
129
|
+
"#{to_client_timezone((Integer(value) * 2).day.ago.beginning_of_day)}'" +
|
|
130
|
+
" AND '#{to_client_timezone((Integer(value) + 1).day.ago.end_of_day)}'"
|
|
131
|
+
when OPERATOR_PREVIOUS_X_DAYS_TO_DATE
|
|
132
|
+
ensure_integer_value(value)
|
|
142
133
|
return "BETWEEN '" +
|
|
143
|
-
"#{to_client_timezone(((Integer(
|
|
144
|
-
" AND '#{to_client_timezone(Integer(
|
|
134
|
+
"#{to_client_timezone(((Integer(value) * 2) - 1).day.ago.beginning_of_day)}'" +
|
|
135
|
+
" AND '#{to_client_timezone(Integer(value).day.ago)}'"
|
|
145
136
|
end
|
|
146
137
|
|
|
147
|
-
duration = PERIODS[
|
|
148
|
-
period = PERIODS[
|
|
149
|
-
period_of_time = PERIODS[
|
|
150
|
-
|
|
151
|
-
to_date = PERIODS[@value.to_sym][:to_date]
|
|
138
|
+
duration = PERIODS[operator][:duration]
|
|
139
|
+
period = PERIODS[operator][:period]
|
|
140
|
+
period_of_time = PERIODS[operator][:period_of_time] || period
|
|
141
|
+
to_date = PERIODS[operator][:to_date]
|
|
152
142
|
|
|
153
143
|
if to_date
|
|
154
|
-
from = to_client_timezone((duration)
|
|
155
|
-
|
|
144
|
+
from = to_client_timezone((duration)
|
|
145
|
+
.send(period).ago.send("beginning_of_#{period_of_time}"))
|
|
156
146
|
to = to_client_timezone((duration).send(period).ago)
|
|
157
147
|
else
|
|
158
148
|
from = to_client_timezone((duration * 2).send(period).ago
|
|
159
|
-
|
|
149
|
+
.send("beginning_of_#{period_of_time}"))
|
|
160
150
|
to = to_client_timezone((1 + duration).send(period).ago
|
|
161
|
-
|
|
151
|
+
.send("end_of_#{period_of_time}"))
|
|
162
152
|
end
|
|
163
153
|
"BETWEEN '#{from}' AND '#{to}'"
|
|
164
154
|
end
|
|
155
|
+
|
|
156
|
+
def ensure_integer_value(value)
|
|
157
|
+
unless value.is_a?(Integer) || /\A[-+]?\d+\z/.match(value)
|
|
158
|
+
raise ForestLiana::Errors::HTTP422Error.new('\'value\' should be an Integer')
|
|
159
|
+
end
|
|
160
|
+
end
|
|
165
161
|
end
|
|
166
162
|
end
|