Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CL-1097] Search by content from content builder #2542

Merged
merged 2 commits into from
Aug 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions back/app/models/project.rb
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,13 @@ class Project < ApplicationRecord
where(id: project_ids)
}

class << self
def search_ids_by_all_including_patches(term)
result = defined?(super) ? super : []
result + search_by_all(term).pluck(:id)
end
end

def continuous?
process_type == 'continuous'
end
Expand Down
4 changes: 2 additions & 2 deletions back/app/services/admin_publications_filtering_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ def for_homepage_filter(scope)
filtered_projects = ProjectsFilteringService.new.filter(projects, options)

if options[:search].present?
filtered_projects = filtered_projects.search_by_all(options[:search])
project_ids = filtered_projects.search_ids_by_all_including_patches(options[:search])
end

project_publications = scope.where(publication: filtered_projects)
project_publications = scope.where(publication: project_ids || filtered_projects)
other_publications = scope.where.not(publication_type: Project.name)
project_publications.or(other_publications)
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,49 @@
module ContentBuilder
module Patches
module Project
def self.included(base)
base.class_eval do
has_many :content_builder_layouts,
class_name: 'ContentBuilder::Layout',
foreign_key: 'content_buildable_id',
dependent: :destroy
extend ActiveSupport::Concern
# pg_search trick https://github.com/Casecommons/pg_search/issues/252#issuecomment-486606367
# JSONPath spec (JSONB_PATH_QUERY, '$.*...') https://www.postgresql.org/docs/12/functions-json.html#FUNCTIONS-SQLJSON-PATH
# .**. is used insted .*.*. because it's potentially more flexible (what if a new level of nesting is added?)
CRAFTJS_TEXT_QUERY = <<-SQL.squish
(
SELECT
ARRAY_AGG(CONCAT_WS('. ', text, title, url))
FROM
(
SELECT
JSONB_PATH_QUERY("craftjs_jsonmultiloc", '$.**.props.text') AS text,
JSONB_PATH_QUERY("craftjs_jsonmultiloc", '$.**.props.title') AS title,
JSONB_PATH_QUERY("craftjs_jsonmultiloc", '$.**.props.url') AS url
FROM
content_builder_layouts
WHERE
content_builder_layouts.content_buildable_id = "projects"."id" AND
content_builder_layouts.content_buildable_type = 'Project' AND
content_builder_layouts.enabled = true
) AS _required_but_not_used
)
SQL

included do
has_many :content_builder_layouts,
class_name: 'ContentBuilder::Layout',
foreign_key: 'content_buildable_id',
dependent: :destroy

pg_search_scope :search_by_content_layouts, associated_against: {
content_builder_layouts: [Arel.sql(CRAFTJS_TEXT_QUERY)]
}, using: { tsearch: { prefix: true } }
end

class_methods do
def search_ids_by_all_including_patches(term)
# #or gives
# ArgumentError: Relation passed to #or must be structurally compatible. Incompatible values: [:joins]
# from /usr/local/bundle/gems/activerecord-6.1.6.1/lib/active_record/relation/query_methods.rb:725:in `or!'
# So, using arrays of ids.
result = defined?(super) ? super : []
result + search_by_content_layouts(term).pluck(:id)
end
end
end
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

# https://github.com/Casecommons/pg_search/issues/252#issuecomment-840959889
module PgSearchPatch
def initialize(column_name, weight, model)
super

# Re-set this field if it is a SqlLiteral (super sets it with .to_s)
@column_name = column_name if column_name.is_a?(Arel::Nodes::SqlLiteral)
end

def full_name
if @column_name.is_a?(Arel::Nodes::SqlLiteral)
@column_name
else
super
end
end
end

PgSearch::Configuration::Column.prepend PgSearchPatch
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,95 @@
require 'rails_helper'

RSpec.describe ContentBuilder::Patches::Project, type: :model do
subject(:project) { layout.content_buildable }

let(:layout) { create(:layout) }
let! :another_layout do
create(
:layout,
content_buildable: project,
code: 'another_layout',
enabled: false
)
end
context 'when project has two layouts' do
subject(:project) { layout.content_buildable }

let(:layout) { create(:layout) }
let! :another_layout do
create(
:layout,
content_buildable: project,
code: 'another_layout',
enabled: false
)
end

describe '#content_builder_layouts' do
it 'returns the layouts of a project' do
expect(project.content_builder_layouts).to match_array([layout, another_layout])
end
end

describe '#content_builder_layouts' do
it 'returns the layouts of a project' do
expect(project.content_builder_layouts).to match_array([layout, another_layout])
describe '#destroy' do
it 'destroys its layouts' do
expect { project.destroy }.to change { ContentBuilder::Layout.count }.by(-2)
end
end
end

describe '#destroy' do
it 'destroys its layouts' do
expect { project.destroy }.to change { ContentBuilder::Layout.count }.by(-2)
describe '.search_ids_by_all_including_patches' do
def craftjs(props)
{
en: {
ROOT: {
type: 'div',
nodes: %w[nfXxt3641Y Da_X6w7thf 2B2ApMKvLw nUEV-dMfSq],
props: { id: 'e2e-content-builder-frame' },
custom: {},
hidden: false,
isCanvas: true,
displayName: 'div',
linkedNodes: {}
},
'1pzsxkGIsQ': {
type: { resolvedName: 'Container' },
nodes: [],
props: props,
custom: {},
hidden: false,
parent: 'WSe1D-RM_b',
isCanvas: true,
displayName: 'Container',
linkedNodes: {}
}
}
}
end

def create_project(craftjs_props)
create(:project, content_builder_layouts: [build(:layout, craftjs_jsonmultiloc: craftjs(craftjs_props))])
end

it 'finds projects by text, title, and url' do
p1 = create_project(text: 'sometext here')
__ = create_project(text: 'othertext')
p2 = create_project(title: 'sometitle here')
__ = create_project(title: 'othertitle')
p3 = create_project(url: 'someurl')
__ = create_project(url: 'otherurl')

expect(Project.search_ids_by_all_including_patches('sometext')).to match_array([p1.id])
expect(Project.search_ids_by_all_including_patches('sometitle')).to match_array([p2.id])
expect(Project.search_ids_by_all_including_patches('someurl')).to match_array([p3.id])
expect(Project.search_ids_by_all_including_patches('here')).to match_array([p1.id, p2.id])
end

it 'finds projects by both builder content and normal description' do
p1 = create_project(text: 'sometext here')
__ = create_project(text: 'othertext here')
p2 = create(:project, description_multiloc: { en: 'sometext' })
__ = create(:project, description_multiloc: { en: 'othertext' })

expect(Project.search_ids_by_all_including_patches('sometext')).to match_array([p1.id, p2.id])
end

it 'does not find projects by internal craftjs fields' do
p1 = create_project(text: 'sometext here')
expect(Project.search_ids_by_all_including_patches('sometext')).to match_array([p1.id])

expect(Project.search_ids_by_all_including_patches('Container')).to be_empty
expect(Project.search_ids_by_all_including_patches('nodes')).to be_empty
expect(Project.search_ids_by_all_including_patches('ROOT')).to be_empty
end
end
end
15 changes: 15 additions & 0 deletions back/spec/acceptance/admin_publications_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,21 @@
folder.admin_publication.id
)
end

if CitizenLab.ee?
example_request 'Search project by content from content builder' do
project = create(:project, content_builder_layouts: [
build(:layout, craftjs_jsonmultiloc: { en: { someid: { props: { text: 'sometext' } } } })
])
create(:project, content_builder_layouts: [
build(:layout, craftjs_jsonmultiloc: { en: { sometext: { props: { text: 'othertext' } } } })
])
do_request search: 'sometext'

expect(response_data.size).to eq 1
expect(response_ids).to contain_exactly(project.admin_publication.id)
end
end
end
end

Expand Down
1 change: 0 additions & 1 deletion back/spec/acceptance/projects_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
parameter :publication_statuses, 'Return only projects with the specified publication statuses (i.e. given an array of publication statuses); returns all projects by default', required: false
parameter :filter_can_moderate, 'Filter out the projects the user is allowed to moderate. False by default', required: false
parameter :filter_ids, 'Filter out only projects with the given list of IDs', required: false
parameter :search, 'Filter by searching in title_multiloc, description_multiloc and description_preview_multiloc', required: false

parameter :folder, 'Filter by folder (project folder id)', required: false if CitizenLab.ee?

Expand Down