From 8c6a7ebf860db317583b2c339ff8233b51b6f232 Mon Sep 17 00:00:00 2001 From: Manuel <109959820+ManuelMoeri@users.noreply.github.com> Date: Fri, 21 Jun 2024 14:12:22 +0200 Subject: [PATCH] feature/694 CV-search optionally search skills as well (#727) * 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 --- app/controllers/cv_search_controller.rb | 6 ++- app/domain/people_search.rb | 53 +++++++++++-------- app/helpers/cv_search_helper.rb | 6 ++- .../controllers/search_controller.js | 3 ++ app/models/person.rb | 3 +- app/views/cv_search/index.html.haml | 7 ++- app/views/layouts/person.html.haml | 2 +- config/locales/de.yml | 4 +- spec/domain/people_search_spec.rb | 2 +- spec/features/cv_search_spec.rb | 14 ++++- 10 files changed, 67 insertions(+), 33 deletions(-) diff --git a/app/controllers/cv_search_controller.rb b/app/controllers/cv_search_controller.rb index 2c14be9d2..9f571c3da 100644 --- a/app/controllers/cv_search_controller.rb +++ b/app/controllers/cv_search_controller.rb @@ -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 diff --git a/app/domain/people_search.rb b/app/domain/people_search.rb index 3857640f2..e2b062588 100644 --- a/app/domain/people_search.rb +++ b/app/domain/people_search.rb @@ -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 @@ -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 @@ -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('%s#%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) diff --git a/app/helpers/cv_search_helper.rb b/app/helpers/cv_search_helper.rb index dc6139eb9..ff69c808a 100644 --- a/app/helpers/cv_search_helper.rb +++ b/app/helpers/cv_search_helper.rb @@ -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 diff --git a/app/javascript/controllers/search_controller.js b/app/javascript/controllers/search_controller.js index c18d09499..b2ee19439 100644 --- a/app/javascript/controllers/search_controller.js +++ b/app/javascript/controllers/search_controller.js @@ -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; diff --git a/app/models/person.rb b/app/models/person.rb index e5e852424..09a88f47c 100644 --- a/app/models/person.rb +++ b/app/models/person.rb @@ -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: { diff --git a/app/views/cv_search/index.html.haml b/app/views/cv_search/index.html.haml index 9ab93da35..1341b024d 100644 --- a/app/views/cv_search/index.html.haml +++ b/app/views/cv_search/index.html.haml @@ -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"} @@ -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 \ No newline at end of file diff --git a/app/views/layouts/person.html.haml b/app/views/layouts/person.html.haml index bcb93d705..3d0602734 100644 --- a/app/views/layouts/person.html.haml +++ b/app/views/layouts/person.html.haml @@ -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"} diff --git a/config/locales/de.yml b/config/locales/de.yml index 47cd00551..6fac3ab67 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -108,6 +108,7 @@ de: with_enddate: Mit Enddatum cv_search: + search_skills: Skills ebenfalls durchsuchen advanced_trainings: Weiterbildungen educations: Ausbildungen activities: Stationen @@ -117,4 +118,5 @@ de: title: Abschluss competence_notes: Kompetenzen roles: Funktionen - department: Organisationseinheit \ No newline at end of file + department: Organisationseinheit + skills: Skills \ No newline at end of file diff --git a/spec/domain/people_search_spec.rb b/spec/domain/people_search_spec.rb index 650939bd7..f5ef3778f 100644 --- a/spec/domain/people_search_spec.rb +++ b/spec/domain/people_search_spec.rb @@ -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 diff --git a/spec/features/cv_search_spec.rb b/spec/features/cv_search_spec.rb index bdfdb732a..d44ed7908 100644 --- a/spec/features/cv_search_spec.rb +++ b/spec/features/cv_search_spec.rb @@ -36,7 +36,7 @@ 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") @@ -44,7 +44,7 @@ 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 @@ -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