Skip to content

Commit

Permalink
feature/694 CV-search optionally search skills as well (#727)
Browse files Browse the repository at this point in the history
* Add the checkbox

* Implement first idea

* Implement dynamically searching skills of people

* Bring back highlighting of search results when clicking on results of cv search

* Show all skills when clicking on a skill result from cv search

* Fix tests

* Write test to test dynamic skill search and add missing translation

* Check checkbox if url is opened in a new tab

* Return results for attributes and associations if they are found in both in cv search

* Change cv search to search over all attributes

* Make rubocop happy

* Make rubocop happy again

* Add translation for checkbox

* Remove expertise topics from skill search and also remove now unused translation

* Make results only return once if they are found in multiple fields of the same sym

* Only return rated skills

* Fix people search domain spec

* Make the search_skills parameter a default parameter

* Fix people search domain specs after making search skills a default param

* Make cv search controller explicitly pass search_skills to people search

---------

Co-authored-by: Jannik Pulfer <[email protected]>
  • Loading branch information
ManuelMoeri and RandomTannenbaum authored Jun 21, 2024
1 parent e1f53d4 commit 8c6a7eb
Show file tree
Hide file tree
Showing 10 changed files with 67 additions and 33 deletions.
6 changes: 5 additions & 1 deletion app/controllers/cv_search_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@ def index
private

def search_results
PeopleSearch.new(query).entries
PeopleSearch.new(query, search_skills: search_skills?).entries
end

def query
params[:q]
end

def search_skills?
params.key?(:search_skills)
end

def should_search
query.nil? || query.length < 3
end
Expand Down
53 changes: 30 additions & 23 deletions app/domain/people_search.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
class PeopleSearch
SEARCHABLE_FIELDS = %w{name title competence_notes description
role technology location}.freeze
attr_reader :search_term, :entries
attr_reader :search_term, :entries, :search_skills

def initialize(search_term)
def initialize(search_term, search_skills: false)
@search_term = search_term
@search_skills = search_skills
@entries = search_result
@entries = @entries.filter { |entry| entry[:found_in] }
end

private
Expand All @@ -17,15 +17,20 @@ def search_result
people = Person.all.search(search_term)
people = pre_load(people)

results = []

people.map do |p|
{ person: { id: p.id, name: p.name }, found_in: found_in(p) }
found_in(p).each do |result|
results.push({ person: { id: p.id, name: p.name }, found_in: result })
end
end
results
end

def found_in(person)
res = in_attributes(person.attributes)
res ||= in_associations(person)
res.try(:camelize, :lower)
res_attributes = in_attributes(person.attributes)
res_associations = in_associations(person)
[res_attributes, res_associations].flatten.compact
end

# Load the attributes of the given people into cache
Expand All @@ -35,51 +40,53 @@ def pre_load(people)
person_keys = people.map(&:id)

Person.includes(:department, :roles, :projects, :activities,
:educations, :advanced_trainings, :expertise_topics)
:educations, :advanced_trainings, (:skills if search_skills))
.find(person_keys)
end

def in_associations(person)
association_symbols.each do |sym|
attribute_name = in_association(person, sym)
if attribute_name
return format('%<association>s#%<attribute_name>s',
association: sym.to_s, attribute_name: attribute_name)
end
end
nil
association_symbols.map do |sym|
attribute_names = in_association(person, sym)
sym.to_s if attribute_names.any?
end.flatten
end

def association_symbols
Person.reflections.keys.excluding('company').map(&:to_sym)
end

# rubocop:disable Metrics/MethodLength
def in_association(person, sym)
target = person.association(sym).target
return if target.nil?

if sym == :skills
target.filter! do |skill|
!person.people_skills.find_by(skill_id: skill.id).unrated?
end
end

if target.is_a?(Array)
attribute_in_array(target)
else
in_attributes(target.attributes)
end
end
# rubocop:enable Metrics/MethodLength

def attribute_in_array(array)
array.each do |t|
attribute = in_attributes(t.attributes)
return attribute unless attribute.nil?
end
nil
array.map do |t|
in_attributes(t.attributes)
end.flatten
end

def in_attributes(attrs)
attribute = searchable_fields(attrs).find do |_key, value|
attribute = searchable_fields(attrs).find_all do |_key, value|
next if value.nil?

value.downcase.include?(search_term.downcase) # PG Search is not case sensitive
end
attribute.try(:first)
attribute.map!(&:first)
end

def searchable_fields(fields)
Expand Down
6 changes: 5 additions & 1 deletion app/helpers/cv_search_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

module CvSearchHelper
def translate_found_in(result)
I18n.t("cv_search.#{result[:found_in].split('#')[0].underscore}")
I18n.t("cv_search.#{result[:found_in].underscore}")
end

def found_in_skills?(result)
result[:found_in].include?('skills')
end
end
3 changes: 3 additions & 0 deletions app/javascript/controllers/search_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ export default class extends Controller {
if(params.has("q")) {
document.getElementById("cv_search_field").value = params.get("q");
}
if(params.has("search_skills")) {
document.getElementById("search_skills_checkbox").checked = true;
}
}

timeout;
Expand Down
3 changes: 2 additions & 1 deletion app/models/person.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ class Person < ApplicationRecord
projects: [:description, :title, :role, :technology],
activities: [:description, :role],
educations: [:location, :title],
advanced_trainings: :description
advanced_trainings: :description,
skills: [:title]
},
using: {
tsearch: {
Expand Down
7 changes: 5 additions & 2 deletions app/views/cv_search/index.html.haml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
%div.mt-2
%form{data: {"turbo-frame": "search-results", "turbo-action": "advance"}, "data-controller": "search"}
%form.d-flex.align-items-center{data: {"turbo-frame": "search-results", "turbo-action": "advance"}, "data-controller": "search"}
%input{class: 'form-control w-75', placeholder: 'CVs durchsuchen...', name: 'q', "data-action": "search#submitWithTimeout", id: "cv_search_field"}
%div.ms-5
%input{type: "checkbox", name: 'search_skills', id: 'search_skills_checkbox', onchange: 'this.form.requestSubmit();'}
= t('cv_search.search_skills')
%div.profile-header.mw-100.border-bottom.mt-2.mb-2
Suchresultate
%turbo-frame{id: "search-results"}
Expand All @@ -12,5 +15,5 @@
= link_to result[:person][:name], person_path(result[:person][:id]), {class: "bg-skills-green w-50 text-decoration-none text-white ps-1 p-2 rounded-1", "data-turbo": "false"}
%div.w-50.d-flex.justify-content-end.align-items-center
%div.me-1 gefunden in:
= link_to translate_found_in(result), person_path(result[:person][:id]), {class: "bg-skills-search-result-blue w-50 text-decoration-none text-white ps-1 p-2 rounded-1 text-center", "data-turbo": "false"}
= link_to translate_found_in(result), found_in_skills?(result) ? person_people_skills_path(result[:person][:id], q: params[:q], rating: 1) : person_path(result[:person][:id], q: params[:q]), {class: "bg-skills-search-result-blue w-50 text-decoration-none text-white ps-1 p-2 rounded-1 text-center", "data-turbo": "false"}
%br
2 changes: 1 addition & 1 deletion app/views/layouts/person.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
%div.profile-header-and-tabs.pt-2
%div
=render partial:"people/search", :locals => {person: @person}
%div{"data-controller": "profile-tab"}
%div{"data-controller": "profile-tab highlight"}
= render "application/tabbar", tabs: person_tabs(@person) do
=link_to image_tag("export.svg")+ "Export", export_cv_person_path(@person), class: "btn text-primary", data: { turbo_frame: "remote_modal" }
%turbo-frame#tab-content.d-flex.gap-3{"data-controller": "scroll"}
Expand Down
4 changes: 3 additions & 1 deletion config/locales/de.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ de:
with_enddate: Mit Enddatum

cv_search:
search_skills: Skills ebenfalls durchsuchen
advanced_trainings: Weiterbildungen
educations: Ausbildungen
activities: Stationen
Expand All @@ -117,4 +118,5 @@ de:
title: Abschluss
competence_notes: Kompetenzen
roles: Funktionen
department: Organisationseinheit
department: Organisationseinheit
skills: Skills
2 changes: 1 addition & 1 deletion spec/domain/people_search_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
search_term = 'duckduck'
people = PeopleSearch.new(search_term).entries
person = people[0]
expect(person[:found_in]).to eq('projects#title')
expect(person[:found_in]).to eq('projects')
end

it 'finds in which person attribute the search term has been found' do
Expand Down
14 changes: 12 additions & 2 deletions spec/features/cv_search_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,15 @@
it 'should open person when clicking result' do
fill_in 'cv_search_field', with: person.projects.first.technology
check_search_results(I18n.t("cv_search.projects"))
click_link(person.name)
first("a", text: person.name).click();
expect(page).to have_current_path(person_path(person))

visit("/cv_search")
education_location = person.educations.first.location
fill_in 'cv_search_field', with: education_location
check_search_results(I18n.t("cv_search.educations"))
click_link(I18n.t("cv_search.educations"))
expect(page).to have_current_path("#{person_path(person)}")
expect(page).to have_current_path("#{person_path(person)}?q=#{education_location.split(" ").join("+")}")
end

it 'should only display results when length of search-text is > 3' do
Expand All @@ -53,6 +53,16 @@
fill_in 'cv_search_field', with: person.name.slice(0, 3)
expect(page).to have_content(person.name)
end

it 'should dynamically search skills' do
skill_title = person.skills.last.title
fill_in 'cv_search_field', with: skill_title
expect(page).to have_content("Keine Resultate")
page.check('search_skills')
expect(page).not_to have_content("Keine Resultate")
check_search_results(I18n.t("cv_search.skills"))
expect(page).to have_link(href: "#{person_people_skills_path(person)}?q=#{skill_title.split(" ").join("+")}&rating=1")
end
end
end

Expand Down

0 comments on commit 8c6a7eb

Please sign in to comment.