Skip to content

Commit 82598a3

Browse files
authored
Merge branch 'develop' into art/1738/activerecord-caching
2 parents 2840baa + bdc4d55 commit 82598a3

5 files changed

Lines changed: 388 additions & 139 deletions

File tree

app/helpers/search_helper.rb

Lines changed: 32 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
module SearchHelper
2+
include SearchQualifierHelper
3+
24
##
35
# Search & sort a default posts list based on parameters in the current request.
46
#
@@ -129,86 +131,46 @@ def parse_search(raw_search)
129131
{ qualifiers: qualifiers, search: search }
130132
end
131133

132-
# rubocop:disable Metrics/CyclomaticComplexity
133-
134134
##
135135
# Parses a full qualifier string into an array of qualifier objects.
136136
# @param qualifiers [String] A qualifier string as returned by {#parse_search}.
137137
# @return [Array<Hash{Symbol => Object}>]
138138
def parse_qualifier_strings(qualifiers)
139-
valid_value = {
140-
date: /^[<>=]{0,2}[\d.]+(?:s|m|h|d|w|mo|y)?$/,
141-
status: /any|open|closed/,
142-
numeric: /^[<>=]{0,2}[\d.]+$/
143-
}
144-
145-
qualifiers.map do |qualifier| # rubocop:disable Metrics/BlockLength
139+
qualifiers.map do |qualifier|
146140
splat = qualifier.split ':'
147141
parameter = splat[0]
148142
value = splat[1]
149143

150-
case parameter
151-
when 'score'
152-
next unless value.match?(valid_value[:numeric])
153-
154-
operator, val = numeric_value_sql value
155-
{ param: :score, operator: operator.presence || '=', value: val.to_f }
156-
when 'created'
157-
next unless value.match?(valid_value[:date])
158-
159-
operator, val, timeframe = date_value_sql value
160-
{ param: :created, operator: operator.presence || '=', timeframe: timeframe, value: val.to_i }
161-
when 'user'
162-
operator, val = if value.match?(valid_value[:numeric])
163-
numeric_value_sql value
164-
elsif value == 'me'
165-
['=', current_user&.id&.to_i]
166-
else
167-
next
168-
end
169-
170-
{ param: :user, operator: operator.presence || '=', user_id: val }
171-
when 'upvotes'
172-
next unless value.match?(valid_value[:numeric])
173-
174-
operator, val = numeric_value_sql value
175-
{ param: :upvotes, operator: operator.presence || '=', value: val.to_i }
176-
when 'downvotes'
177-
next unless value.match?(valid_value[:numeric])
178-
179-
operator, val = numeric_value_sql value
180-
{ param: :downvotes, operator: operator.presence || '=', value: val.to_i }
181-
when 'votes'
182-
next unless value.match?(valid_value[:numeric])
183-
184-
operator, val = numeric_value_sql value
185-
{ param: :net_votes, operator: operator.presence || '=', value: val.to_i }
186-
when 'tag'
187-
{ param: :include_tag, tag_id: Tag.where(name: value).select(:id) }
188-
when '-tag'
189-
{ param: :exclude_tag, tag_id: Tag.where(name: value).select(:id) }
190-
when 'category'
191-
next unless value.match?(valid_value[:numeric])
192-
193-
operator, val = numeric_value_sql value
194-
{ param: :category, operator: operator.presence || '=', category_id: val.to_i }
195-
when 'post_type'
196-
next unless value.match?(valid_value[:numeric])
197-
198-
operator, val = numeric_value_sql value
199-
{ param: :post_type, operator: operator.presence || '=', post_type_id: val.to_i }
200-
when 'answers'
201-
next unless value.match?(valid_value[:numeric])
202-
203-
operator, val = numeric_value_sql value
204-
{ param: :answers, operator: operator.presence || '=', value: val.to_i }
205-
when 'status'
206-
next unless value.match?(valid_value[:status])
207-
208-
{ param: :status, value: value }
209-
end
144+
parsed = case parameter
145+
when 'score'
146+
parse_score_qualifier(value)
147+
when 'created'
148+
parse_created_qualifier(value)
149+
when 'user'
150+
parse_user_qualifier(value)
151+
when 'upvotes'
152+
parse_upvotes_qualifier(value)
153+
when 'downvotes'
154+
parse_downvotes_qualifier(value)
155+
when 'votes'
156+
parse_votes_qualifier(value)
157+
when 'tag'
158+
parse_include_tag_qualifier(value)
159+
when '-tag'
160+
parse_exclude_tag_qualifier(value)
161+
when 'category'
162+
parse_category_qualifier(value)
163+
when 'post_type'
164+
parse_post_type_qualifier(value)
165+
when 'answers'
166+
parse_answers_qualifier(value)
167+
when 'status'
168+
parse_status_qualifier(value)
169+
end
170+
171+
parsed
210172
end.compact
211-
# Consider partitioning and telling the user which filters were invalid
173+
# Consider telling the user which filters were invalid
212174
end
213175

214176
##
@@ -266,49 +228,4 @@ def qualifiers_to_sql(qualifiers, query, user)
266228

267229
query
268230
end
269-
# rubocop:enable Metrics/CyclomaticComplexity
270-
271-
##
272-
# Parses a qualifier value string, including operator, as a numeric value.
273-
# @param value [String] The value part of the qualifier, i.e. +">=10"+
274-
# @return [Array(String, String)] A 2-tuple containing operator and value.
275-
# @api private
276-
def numeric_value_sql(value)
277-
operator = ''
278-
while ['<', '>', '='].include? value[0]
279-
operator += value[0]
280-
value = value[1..-1]
281-
end
282-
283-
# whatever's left after stripping operator is the number
284-
# validated by regex in qualifiers_to_sql
285-
[operator, value]
286-
end
287-
288-
##
289-
# Parses a qualifier value string, including operator, as a date value.
290-
# @param value [String] The value part of the qualifier, i.e. +">=10d"+
291-
# @return [Array(String, String, String)] A 3-tuple containing operator, value, and timeframe.
292-
# @api private
293-
def date_value_sql(value)
294-
operator = ''
295-
296-
while ['<', '>', '='].include? value[0]
297-
operator += value[0]
298-
value = value[1..-1]
299-
end
300-
301-
# working with dates: <1y ('less than one year ago') is SQL: > 1y ago
302-
operator = { '<' => '>', '>' => '<', '<=' => '>=', '>=' => '<=' }[operator] || ''
303-
304-
val = ''
305-
while value[0] =~ /[[:digit:]]/
306-
val += value[0]
307-
value = value[1..-1]
308-
end
309-
310-
timeframe = { s: 'SECOND', m: 'MINUTE', h: 'HOUR', d: 'DAY', w: 'WEEK', mo: 'MONTH', y: 'YEAR' }[value.to_sym]
311-
312-
[operator, val, timeframe || 'MONTH']
313-
end
314231
end
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
module SearchQualifierHelper
2+
def matches_date?(value)
3+
value.match?(/^[<>=]{0,2}[\d.]+(?:s|m|h|d|w|mo|y)?$/)
4+
end
5+
6+
def matches_id?(value)
7+
value.match?(/^[<>=]{0,2}\d+$/)
8+
end
9+
10+
def matches_int?(value)
11+
value.match?(/^[<>=]{0,2}-?\d+$/)
12+
end
13+
14+
def matches_numeric?(value)
15+
value.match?(/^[<>=]{0,2}[\d.]+$/)
16+
end
17+
18+
def matches_non_negative_int?(value)
19+
value.match?(/^[<>=]{0,2}\d+$/)
20+
end
21+
22+
def matches_status?(value)
23+
value.match?(/any|open|closed/)
24+
end
25+
26+
def parse_answers_qualifier(value)
27+
return unless matches_non_negative_int?(value)
28+
29+
operator, val = numeric_value_sql(value)
30+
31+
{ param: :answers, operator: operator.presence || '=', value: val.to_i }
32+
end
33+
34+
def parse_category_qualifier(value)
35+
return unless matches_id?(value)
36+
37+
operator, val = numeric_value_sql(value)
38+
39+
{ param: :category, operator: operator.presence || '=', category_id: val.to_i }
40+
end
41+
42+
def parse_created_qualifier(value)
43+
return unless matches_date?(value)
44+
45+
operator, val, timeframe = date_value_sql(value)
46+
47+
{ param: :created, operator: operator.presence || '=', timeframe: timeframe, value: val.to_i }
48+
end
49+
50+
def parse_downvotes_qualifier(value)
51+
return unless matches_non_negative_int?(value)
52+
53+
operator, val = numeric_value_sql(value)
54+
55+
{ param: :downvotes, operator: operator.presence || '=', value: val.to_i }
56+
end
57+
58+
def parse_exclude_tag_qualifier(value)
59+
{ param: :exclude_tag, tag_id: Tag.where(name: value).select(:id) }
60+
end
61+
62+
def parse_post_type_qualifier(value)
63+
return unless matches_id?(value)
64+
65+
operator, val = numeric_value_sql(value)
66+
67+
{ param: :post_type, operator: operator.presence || '=', post_type_id: val.to_i }
68+
end
69+
70+
def parse_score_qualifier(value)
71+
return unless matches_numeric?(value)
72+
73+
operator, val = numeric_value_sql(value)
74+
75+
{ param: :score, operator: operator.presence || '=', value: val.to_f }
76+
end
77+
78+
def parse_status_qualifier(value)
79+
return unless matches_status?(value)
80+
81+
{ param: :status, value: value }
82+
end
83+
84+
def parse_include_tag_qualifier(value)
85+
{ param: :include_tag, tag_id: Tag.where(name: value).select(:id) }
86+
end
87+
88+
def parse_upvotes_qualifier(value)
89+
return unless matches_non_negative_int?(value)
90+
91+
operator, val = numeric_value_sql(value)
92+
93+
{ param: :upvotes, operator: operator.presence || '=', value: val.to_i }
94+
end
95+
96+
def parse_user_qualifier(value)
97+
return unless matches_int?(value) || value == 'me'
98+
99+
operator, val = if value == 'me'
100+
['=', current_user&.id]
101+
else
102+
numeric_value_sql(value)
103+
end
104+
105+
{ param: :user, operator: operator.presence || '=', user_id: val.to_i }
106+
end
107+
108+
def parse_votes_qualifier(value)
109+
return unless matches_int?(value)
110+
111+
operator, val = numeric_value_sql(value)
112+
113+
{ param: :net_votes, operator: operator.presence || '=', value: val.to_i }
114+
end
115+
116+
# Parses a qualifier value string, including operator, as a numeric value.
117+
# @param value [String] The value part of the qualifier, i.e. +">=10"+
118+
# @return [Array(String, String)] A 2-tuple containing operator and value.
119+
# @api private
120+
def numeric_value_sql(value)
121+
operator = ''
122+
while ['<', '>', '='].include? value[0]
123+
operator += value[0]
124+
value = value[1..-1]
125+
end
126+
127+
# whatever's left after stripping operator is the number
128+
# validated by regex in qualifiers_to_sql
129+
[operator, value]
130+
end
131+
132+
# Parses a qualifier value string, including operator, as a date value.
133+
# @param value [String] The value part of the qualifier, i.e. +">=10d"+
134+
# @return [Array(String, String, String)] A 3-tuple containing operator, value, and timeframe.
135+
# @api private
136+
def date_value_sql(value)
137+
operator = ''
138+
139+
while ['<', '>', '='].include? value[0]
140+
operator += value[0]
141+
value = value[1..-1]
142+
end
143+
144+
# working with dates: <1y ('less than one year ago') is SQL: > 1y ago
145+
operator = { '<' => '>', '>' => '<', '<=' => '>=', '>=' => '<=' }[operator] || ''
146+
147+
val = ''
148+
while value[0] =~ /[[:digit:]]/
149+
val += value[0]
150+
value = value[1..-1]
151+
end
152+
153+
timeframe = { s: 'SECOND', m: 'MINUTE', h: 'HOUR', d: 'DAY', w: 'WEEK', mo: 'MONTH', y: 'YEAR' }[value.to_sym]
154+
155+
[operator, val, timeframe || 'MONTH']
156+
end
157+
end

test/controllers/comments/create_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ class CommentsControllerTest < ActionController::TestCase
9797
end
9898
end
9999

100-
test 'should correclty create comments in threads' do
100+
test 'should correctly create comments in threads' do
101101
sign_in users(:editor)
102102
before_author_notifs = users(:standard_user).notifications.count
103103
before_follow_notifs = users(:deleter).notifications.count

test/helpers/search_helper_test.rb

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,29 +13,6 @@ class SearchHelperTest < ActionView::TestCase
1313
end
1414
end
1515

16-
test 'numeric_value_sql should return correct operator and value' do
17-
expected = {
18-
'12345' => ['', '12345'],
19-
'<12345' => ['<', '12345'],
20-
'>=12345' => ['>=', '12345']
21-
}
22-
expected.each do |input, expect|
23-
assert_equal expect, numeric_value_sql(input)
24-
end
25-
end
26-
27-
test 'date_value_sql should return correct operator, value, and timeframe' do
28-
expected = {
29-
'1' => ['', '1', 'MONTH'],
30-
'1y' => ['', '1', 'YEAR'],
31-
'<1y' => ['>', '1', 'YEAR'],
32-
'>=2w' => ['<=', '2', 'WEEK']
33-
}
34-
expected.each do |input, expect|
35-
assert_equal expect, date_value_sql(input)
36-
end
37-
end
38-
3916
test 'qualifiers_to_sql should correctly narrow by :category qualifier' do
4017
main = categories(:main)
4118
admin_only = categories(:admin_only)

0 commit comments

Comments
 (0)