rails-contact 0.1.11 → 0.1.14
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/CHANGELOG.md +9 -0
- data/app/controllers/rails/contact/contacts_controller.rb +31 -1
- data/app/views/rails/contact/_google_sync_panel.html.erb +27 -19
- data/app/views/rails/contact/index.html.erb +16 -0
- data/lib/generators/rails/contact/templates/create_rails_contact_contacts.rb.tt +3 -0
- data/lib/rails/contact/search/backends/database.rb +41 -0
- data/lib/rails/contact/search/backends/elasticsearch.rb +8 -2
- data/lib/rails/contact/version.rb +1 -1
- data/lib/tasks/rails/contact_tasks.rake +0 -1
- data/spec/controllers/contacts_controller_spec.rb +20 -2
- data/spec/search/database_backend_spec.rb +49 -0
- data/spec/search/elasticsearch_backend_spec.rb +33 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a91f250f66bfd11f808193354a723ffb15cf9d8fefc5a52052bb11c7c7ca54e5
|
|
4
|
+
data.tar.gz: 18871d517ce8e9a9fca74e1e74ff775d0eab9ca7208d84397f390754f1a6c280
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 510ec523c32aedbfb1f867f990f06626e07a99a6170fbad9e96ef70cca883a34a097961cd92a4f335737fa77a103d611158e35ccf1a28069daba6e5ced1c4d49
|
|
7
|
+
data.tar.gz: 8ebc2b2e8a8b28df33bce4f265e4750fa7282ebc37f1208204e1ad8f23b25a178d8b6bb335bc1c3d2f458d6a7d586feba392efa75e60612484c3b0205f9d8d14
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.14
|
|
4
|
+
|
|
5
|
+
- Contacts index filters: **`region` and `csv_import_id` are now multi-select**. `filter_params` permits `region: []` / `csv_import_id: []`, strips the hidden blank a `<select multiple>` submits, and coerces a legacy scalar (`?region=Europe`) to a one-element array. The database backend matches `metadata->>'csv_import_id' IN (...)` across the selected imports (a single id behaves exactly as before); `region` was already array-safe via `where(region_name:)`.
|
|
6
|
+
- Database backend: **search query is now sanitized internally** — LIKE metacharacters (`% _ \`) are escaped and input is capped at 200 chars, so a user typing `%` can no longer widen the match to every row. Host apps no longer need to wrap `search` to be safe.
|
|
7
|
+
|
|
8
|
+
## 0.1.12
|
|
9
|
+
|
|
10
|
+
- `_google_sync_panel`: render the card whenever `google_sync_ui_on_index`; if `google_sync_enabled` is false, show short instructions instead of hiding the section entirely.
|
|
11
|
+
|
|
3
12
|
## 0.1.11
|
|
4
13
|
|
|
5
14
|
- Default **Google sync panel** back on the engine index via `_google_sync_panel` when `google_sync_enabled` and `google_sync_ui_on_index` (default true). Host apps can override the partial or disable with `google_sync_ui_on_index = false`.
|
|
@@ -127,7 +127,37 @@ module Rails
|
|
|
127
127
|
end
|
|
128
128
|
|
|
129
129
|
def filter_params
|
|
130
|
-
params.permit(
|
|
130
|
+
permitted = params.permit(
|
|
131
|
+
:city,
|
|
132
|
+
:sync_eligible,
|
|
133
|
+
:starred,
|
|
134
|
+
:travel_date_start,
|
|
135
|
+
:travel_date_end,
|
|
136
|
+
:contact_created_at_start,
|
|
137
|
+
:contact_created_at_end,
|
|
138
|
+
region: [],
|
|
139
|
+
csv_import_id: []
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
normalize_multi_select!(permitted, :region)
|
|
143
|
+
normalize_multi_select!(permitted, :csv_import_id)
|
|
144
|
+
permitted
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# region and csv_import_id are multi-select filters: a <select multiple>
|
|
148
|
+
# submits param[] (an array) plus a hidden param[]="" that Rails always
|
|
149
|
+
# sends, so the blank must be stripped — otherwise IN ('', 'x') matches
|
|
150
|
+
# every blank-valued row. Legacy bookmarks may still send a scalar
|
|
151
|
+
# (?region=Europe); coerce those to a one-element array so the search
|
|
152
|
+
# backend only ever sees an array (or no key at all).
|
|
153
|
+
def normalize_multi_select!(permitted, key)
|
|
154
|
+
if permitted[key].blank? && params[key].is_a?(String) && params[key].present?
|
|
155
|
+
permitted[key] = [ params[key] ]
|
|
156
|
+
end
|
|
157
|
+
return unless permitted[key].is_a?(Array)
|
|
158
|
+
|
|
159
|
+
permitted[key] = permitted[key].reject(&:blank?)
|
|
160
|
+
permitted.delete(key) if permitted[key].empty?
|
|
131
161
|
end
|
|
132
162
|
|
|
133
163
|
def google_pending_sync_count
|
|
@@ -1,27 +1,35 @@
|
|
|
1
|
-
<% if Rails::Contact.configuration.
|
|
1
|
+
<% if Rails::Contact.configuration.google_sync_ui_on_index %>
|
|
2
2
|
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm space-y-4">
|
|
3
3
|
<h2 class="text-sm font-semibold text-gray-900">Google Contacts</h2>
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
<%=
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
<p class="mt-2 text-xs text-gray-500">
|
|
12
|
-
Pushes the most recently updated contacts (up to your configured limit) to Google—new rows and changes to names, emails, phones, etc.
|
|
13
|
-
</p>
|
|
14
|
-
</div>
|
|
15
|
-
|
|
16
|
-
<% if @google_contacts_pending_sync.to_i.positive? %>
|
|
17
|
-
<div class="border-t border-gray-100 pt-4">
|
|
18
|
-
<%= form_with url: google_sync_unsynced_contacts_path, method: :post, local: true, class: "flex flex-wrap items-center gap-3" do %>
|
|
19
|
-
<%= submit_tag "Sync #{@google_contacts_pending_sync} contact#{'s' if @google_contacts_pending_sync != 1} with no Google link yet (any age)",
|
|
20
|
-
class: "inline-flex rounded-md border border-emerald-700 bg-white px-4 py-2 text-sm font-semibold text-emerald-800 hover:bg-emerald-50",
|
|
21
|
-
data: { confirm: "Queue Google sync for #{@google_contacts_pending_sync} contact(s) that are not linked yet? This runs in the background." } %>
|
|
5
|
+
<% if Rails::Contact.configuration.google_sync_enabled %>
|
|
6
|
+
<div>
|
|
7
|
+
<%= form_with url: google_sync_rolling_window_contacts_path, method: :post, local: true, class: "flex flex-wrap items-center gap-3" do %>
|
|
8
|
+
<%= submit_tag "Re-sync recent contacts with Google",
|
|
9
|
+
class: "inline-flex rounded-md bg-emerald-600 px-4 py-2 text-sm font-semibold text-white hover:bg-emerald-700",
|
|
10
|
+
data: { confirm: "Queue Google sync for the recent contact window? This updates contacts already in Google and creates any that are missing. Runs in the background." } %>
|
|
22
11
|
<% end %>
|
|
23
|
-
<p class="mt-2 text-xs text-gray-500">
|
|
12
|
+
<p class="mt-2 text-xs text-gray-500">
|
|
13
|
+
Pushes the most recently updated contacts (up to your configured limit) to Google—new rows and changes to names, emails, phones, etc.
|
|
14
|
+
</p>
|
|
24
15
|
</div>
|
|
16
|
+
|
|
17
|
+
<% if @google_contacts_pending_sync.to_i.positive? %>
|
|
18
|
+
<div class="border-t border-gray-100 pt-4">
|
|
19
|
+
<%= form_with url: google_sync_unsynced_contacts_path, method: :post, local: true, class: "flex flex-wrap items-center gap-3" do %>
|
|
20
|
+
<%= submit_tag "Sync #{@google_contacts_pending_sync} contact#{'s' if @google_contacts_pending_sync != 1} with no Google link yet (any age)",
|
|
21
|
+
class: "inline-flex rounded-md border border-emerald-700 bg-white px-4 py-2 text-sm font-semibold text-emerald-800 hover:bg-emerald-50",
|
|
22
|
+
data: { confirm: "Queue Google sync for #{@google_contacts_pending_sync} contact(s) that are not linked yet? This runs in the background." } %>
|
|
23
|
+
<% end %>
|
|
24
|
+
<p class="mt-2 text-xs text-gray-500">For contacts outside the recent window or older imports that never received a Google <code class="rounded bg-gray-100 px-1">resourceName</code>.</p>
|
|
25
|
+
</div>
|
|
26
|
+
<% end %>
|
|
27
|
+
<% else %>
|
|
28
|
+
<p class="text-sm text-gray-600">
|
|
29
|
+
Google sync is turned off. Set environment variable
|
|
30
|
+
<code class="rounded bg-gray-100 px-1">RAILS_CONTACT_GOOGLE_SYNC_ENABLED=true</code>
|
|
31
|
+
(and OAuth client + token path in <code class="rounded bg-gray-100 px-1">rails_contact</code> initializer), then restart the server.
|
|
32
|
+
</p>
|
|
25
33
|
<% end %>
|
|
26
34
|
</div>
|
|
27
35
|
<% end %>
|
|
@@ -19,6 +19,22 @@
|
|
|
19
19
|
<%= f.label :region, "Interested Region", class: "mb-1 block text-sm font-medium text-gray-700" %>
|
|
20
20
|
<%= f.text_field :region, value: params[:region], class: "w-full rounded-md border border-gray-300 px-3 py-2 text-sm" %>
|
|
21
21
|
</div>
|
|
22
|
+
<div>
|
|
23
|
+
<%= f.label :travel_date_start, "Travel Start", class: "mb-1 block text-sm font-medium text-gray-700" %>
|
|
24
|
+
<%= f.date_field :travel_date_start, value: params[:travel_date_start], class: "w-full rounded-md border border-gray-300 px-3 py-2 text-sm" %>
|
|
25
|
+
</div>
|
|
26
|
+
<div>
|
|
27
|
+
<%= f.label :travel_date_end, "Travel End", class: "mb-1 block text-sm font-medium text-gray-700" %>
|
|
28
|
+
<%= f.date_field :travel_date_end, value: params[:travel_date_end], class: "w-full rounded-md border border-gray-300 px-3 py-2 text-sm" %>
|
|
29
|
+
</div>
|
|
30
|
+
<div>
|
|
31
|
+
<%= f.label :contact_created_at_start, "Created Start", class: "mb-1 block text-sm font-medium text-gray-700" %>
|
|
32
|
+
<%= f.date_field :contact_created_at_start, value: params[:contact_created_at_start], class: "w-full rounded-md border border-gray-300 px-3 py-2 text-sm" %>
|
|
33
|
+
</div>
|
|
34
|
+
<div>
|
|
35
|
+
<%= f.label :contact_created_at_end, "Created End", class: "mb-1 block text-sm font-medium text-gray-700" %>
|
|
36
|
+
<%= f.date_field :contact_created_at_end, value: params[:contact_created_at_end], class: "w-full rounded-md border border-gray-300 px-3 py-2 text-sm" %>
|
|
37
|
+
</div>
|
|
22
38
|
<div class="flex gap-2">
|
|
23
39
|
<%= f.submit "Filter", class: "inline-flex rounded-md bg-gray-900 px-4 py-2 text-sm font-medium text-white hover:bg-gray-700" %>
|
|
24
40
|
<%= link_to "Reset", contacts_path, class: "inline-flex rounded-md border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50" %>
|
|
@@ -22,5 +22,8 @@ class CreateRailsContactContacts < ActiveRecord::Migration[7.1]
|
|
|
22
22
|
add_index :rails_contact_contacts, :updated_at
|
|
23
23
|
add_index :rails_contact_contacts, :sync_eligible
|
|
24
24
|
add_index :rails_contact_contacts, :google_resource_name, unique: true
|
|
25
|
+
add_index :rails_contact_contacts, "(metadata->>'travel_date')", name: 'idx_contacts_travel_date'
|
|
26
|
+
add_index :rails_contact_contacts, "(metadata->>'contact_created_at')", name: 'idx_contacts_contact_created_at'
|
|
27
|
+
add_index :rails_contact_contacts, "(metadata->>'csv_import_id')", name: 'idx_contacts_csv_import_id'
|
|
25
28
|
end
|
|
26
29
|
end
|
|
@@ -3,7 +3,12 @@ module Rails
|
|
|
3
3
|
module Search
|
|
4
4
|
module Backends
|
|
5
5
|
class Database
|
|
6
|
+
# Cap user input so the LIKE pattern stays bounded; 200 chars covers
|
|
7
|
+
# any realistic name/email/phone substring.
|
|
8
|
+
MAX_QUERY_LENGTH = 200
|
|
9
|
+
|
|
6
10
|
def search(query, filters, page:, per_page:)
|
|
11
|
+
query = sanitize_query(query)
|
|
7
12
|
offset = (page - 1) * per_page
|
|
8
13
|
scope = Contact.includes(:emails, :phones, :labels).recent_first
|
|
9
14
|
scope = apply_filters(scope, filters)
|
|
@@ -33,6 +38,16 @@ module Rails
|
|
|
33
38
|
|
|
34
39
|
private
|
|
35
40
|
|
|
41
|
+
# Escape LIKE metacharacters (% _ \) so a user typing "%" can't widen
|
|
42
|
+
# the match to every row, and cap length to keep the pattern bounded.
|
|
43
|
+
# The backend builds raw "%…%" LIKE patterns, so this guard belongs
|
|
44
|
+
# here — host apps should not have to wrap search() to stay safe.
|
|
45
|
+
def sanitize_query(query)
|
|
46
|
+
return query if query.blank?
|
|
47
|
+
|
|
48
|
+
ActiveRecord::Base.sanitize_sql_like(query.to_s[0, MAX_QUERY_LENGTH])
|
|
49
|
+
end
|
|
50
|
+
|
|
36
51
|
def apply_filters(scope, filters)
|
|
37
52
|
scoped = scope
|
|
38
53
|
scoped = scoped.where(current_city: filters["city"]) if filters["city"].present?
|
|
@@ -41,6 +56,32 @@ module Rails
|
|
|
41
56
|
if filters["sync_eligible"].present?
|
|
42
57
|
scoped = scoped.where(sync_eligible: ActiveModel::Type::Boolean.new.cast(filters["sync_eligible"]))
|
|
43
58
|
end
|
|
59
|
+
|
|
60
|
+
if filters["travel_date_start"].present?
|
|
61
|
+
scoped = scoped.where("metadata->>'travel_date' >= ?", filters["travel_date_start"])
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
if filters["travel_date_end"].present?
|
|
65
|
+
scoped = scoped.where("metadata->>'travel_date' <= ?", filters["travel_date_end"])
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
if filters["contact_created_at_start"].present?
|
|
69
|
+
scoped = scoped.where("metadata->>'contact_created_at' >= ?", filters["contact_created_at_start"])
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
if filters["contact_created_at_end"].present?
|
|
73
|
+
scoped = scoped.where("metadata->>'contact_created_at' <= ?", filters["contact_created_at_end"])
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# csv_import_id is a multi-select filter: it may be a single id or an
|
|
77
|
+
# array of ids. region above is already array-safe via where(region_name:),
|
|
78
|
+
# but this JSON-extraction predicate needs an explicit IN. A single id
|
|
79
|
+
# yields IN ('5'), identical to the old = '5'.
|
|
80
|
+
if filters["csv_import_id"].present?
|
|
81
|
+
ids = Array(filters["csv_import_id"]).map(&:to_s).reject(&:blank?)
|
|
82
|
+
scoped = scoped.where("metadata->>'csv_import_id' IN (?)", ids) if ids.any?
|
|
83
|
+
end
|
|
84
|
+
|
|
44
85
|
scoped
|
|
45
86
|
end
|
|
46
87
|
end
|
|
@@ -120,8 +120,8 @@ module Rails
|
|
|
120
120
|
|
|
121
121
|
def filter_clauses(filters)
|
|
122
122
|
clauses = []
|
|
123
|
-
clauses <<
|
|
124
|
-
clauses <<
|
|
123
|
+
clauses << match_clause(:current_city, filters["city"]) if filters["city"].present?
|
|
124
|
+
clauses << match_clause(:region_name, filters["region"]) if filters["region"].present?
|
|
125
125
|
unless filters["starred"].nil?
|
|
126
126
|
starred_value = ActiveModel::Type::Boolean.new.cast(filters["starred"])
|
|
127
127
|
clauses << { term: { starred: starred_value } }
|
|
@@ -133,6 +133,12 @@ module Rails
|
|
|
133
133
|
clauses
|
|
134
134
|
end
|
|
135
135
|
|
|
136
|
+
# city and region are multi-select filters: an array uses a `terms`
|
|
137
|
+
# clause (match any), a single value keeps the scalar `term` clause.
|
|
138
|
+
def match_clause(field, value)
|
|
139
|
+
value.is_a?(Array) ? { terms: { field => value } } : { term: { field => value } }
|
|
140
|
+
end
|
|
141
|
+
|
|
136
142
|
def document_for(contact)
|
|
137
143
|
{
|
|
138
144
|
id: contact.id,
|
|
@@ -4,10 +4,28 @@ RSpec.describe Rails::Contact::ContactsController do
|
|
|
4
4
|
let(:controller) { described_class.new }
|
|
5
5
|
|
|
6
6
|
describe "private filter params" do
|
|
7
|
-
it "permits city/
|
|
7
|
+
it "permits city/sync_eligible and coerces a scalar region to an array" do
|
|
8
8
|
controller.params = ActionController::Parameters.new(city: "Delhi", region: "Europe", sync_eligible: "true", x: "1")
|
|
9
9
|
permitted = controller.send(:filter_params)
|
|
10
|
-
expect(permitted.to_h).to eq({ "city" => "Delhi", "region" => "Europe", "sync_eligible" => "true" })
|
|
10
|
+
expect(permitted.to_h).to eq({ "city" => "Delhi", "region" => [ "Europe" ], "sync_eligible" => "true" })
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it "permits multi-select region[] and csv_import_id[] arrays" do
|
|
14
|
+
controller.params = ActionController::Parameters.new(region: [ "Europe", "Asia" ], csv_import_id: [ "5", "7" ])
|
|
15
|
+
permitted = controller.send(:filter_params)
|
|
16
|
+
expect(permitted.to_h).to eq({ "region" => [ "Europe", "Asia" ], "csv_import_id" => [ "5", "7" ] })
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "strips the hidden blank a <select multiple> submits" do
|
|
20
|
+
controller.params = ActionController::Parameters.new(csv_import_id: [ "", "7" ])
|
|
21
|
+
permitted = controller.send(:filter_params)
|
|
22
|
+
expect(permitted.to_h).to eq({ "csv_import_id" => [ "7" ] })
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "drops a multi-select key entirely when only blanks are submitted" do
|
|
26
|
+
controller.params = ActionController::Parameters.new(region: [ "" ])
|
|
27
|
+
permitted = controller.send(:filter_params)
|
|
28
|
+
expect(permitted.to_h).to eq({})
|
|
11
29
|
end
|
|
12
30
|
end
|
|
13
31
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
require "rails_helper"
|
|
2
|
+
|
|
3
|
+
RSpec.describe Rails::Contact::Search::Backends::Database do
|
|
4
|
+
# metadata is a text column read back through JSON.parse, so store JSON for
|
|
5
|
+
# the SQL `metadata->>'csv_import_id'` extraction to resolve.
|
|
6
|
+
def make(name, import_id)
|
|
7
|
+
create(:rails_contact_contact, given_name: name, metadata: { "csv_import_id" => import_id }.to_json)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
let!(:alice) { make("Alice", "imp_1") }
|
|
11
|
+
let!(:bob) { make("Bob", "imp_2") }
|
|
12
|
+
let!(:carol) { make("Carol", "imp_3") }
|
|
13
|
+
|
|
14
|
+
def records(filters)
|
|
15
|
+
described_class.new.search("", filters, page: 1, per_page: 25).records
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
describe "csv_import_id filter (multi-select)" do
|
|
19
|
+
it "matches contacts across every selected import (array -> IN)" do
|
|
20
|
+
expect(records("csv_import_id" => [ "imp_1", "imp_2" ])).to match_array([ alice, bob ])
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "matches a single import exactly as before (scalar)" do
|
|
24
|
+
expect(records("csv_import_id" => "imp_1")).to match_array([ alice ])
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "applies no constraint when the selection is blank only" do
|
|
28
|
+
expect(records("csv_import_id" => [ "" ])).to match_array([ alice, bob, carol ])
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe "query sanitization" do
|
|
33
|
+
def search_for(query)
|
|
34
|
+
described_class.new.search(query, {}, page: 1, per_page: 25).records
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "treats % as a literal, not a match-everything wildcard" do
|
|
38
|
+
expect(search_for("%")).to be_empty
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "still matches a real substring" do
|
|
42
|
+
expect(search_for("Ali")).to include(alice)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it "does not blow up on pathologically long input" do
|
|
46
|
+
expect { search_for("a" * 1000) }.not_to raise_error
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require "rails_helper"
|
|
2
|
+
|
|
3
|
+
RSpec.describe Rails::Contact::Search::Backends::Elasticsearch do
|
|
4
|
+
# Captures the query body so we can assert filter-clause shape without a real
|
|
5
|
+
# Elasticsearch server. Returns an empty hit set so #search resolves cleanly.
|
|
6
|
+
let(:captured) { {} }
|
|
7
|
+
let(:client) do
|
|
8
|
+
cap = captured
|
|
9
|
+
Class.new do
|
|
10
|
+
define_method(:initialize) { cap }
|
|
11
|
+
define_method(:search) do |index:, body:|
|
|
12
|
+
cap[:index] = index
|
|
13
|
+
cap[:body] = body
|
|
14
|
+
{ "hits" => { "total" => { "value" => 0 }, "hits" => [] } }
|
|
15
|
+
end
|
|
16
|
+
end.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def filter_clauses(filters)
|
|
20
|
+
described_class.new(client: client).search("", filters, page: 1, per_page: 25)
|
|
21
|
+
captured[:body][:query][:bool][:filter]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "emits a terms clause for a multi-select region array" do
|
|
25
|
+
expect(filter_clauses("region" => [ "Europe", "Asia" ]))
|
|
26
|
+
.to include({ terms: { region_name: [ "Europe", "Asia" ] } })
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "emits a scalar term clause for a single city" do
|
|
30
|
+
expect(filter_clauses("city" => "Delhi"))
|
|
31
|
+
.to include({ term: { current_city: "Delhi" } })
|
|
32
|
+
end
|
|
33
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails-contact
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.14
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kshitiz Sinha
|
|
@@ -193,6 +193,8 @@ files:
|
|
|
193
193
|
- spec/helpers/application_helper_spec.rb
|
|
194
194
|
- spec/models/contact_spec.rb
|
|
195
195
|
- spec/rails_helper.rb
|
|
196
|
+
- spec/search/database_backend_spec.rb
|
|
197
|
+
- spec/search/elasticsearch_backend_spec.rb
|
|
196
198
|
- spec/services/merge_contacts_service_spec.rb
|
|
197
199
|
- spec/spec_helper.rb
|
|
198
200
|
- spec/views/contact_templates_spec.rb
|