diff --git a/app/assets/stylesheets/admin/menu.scss b/app/assets/stylesheets/admin/menu.scss index f80f074cde71..a414614859a7 100644 --- a/app/assets/stylesheets/admin/menu.scss +++ b/app/assets/stylesheets/admin/menu.scss @@ -36,7 +36,8 @@ } &.budgets-link, - &.investments-link { + &.investments-link, + &.budget-investments-link { @include icon(chart-pie, solid); } @@ -56,7 +57,8 @@ @include icon(envelope, regular); } - &.legislation-link { + &.legislation-link, + &.legislation-processes-link { @include icon(file-alt, solid); } diff --git a/app/assets/stylesheets/admin/search.css b/app/assets/stylesheets/admin/search.css index f4e818abd9a2..0ece75fc50cf 100644 --- a/app/assets/stylesheets/admin/search.css +++ b/app/assets/stylesheets/admin/search.css @@ -1,6 +1,25 @@ .admin [role=search] { display: flex; + &.complex { + @include breakpoint(small only) { + flex-direction: column; + } + + @include breakpoint(medium) { + select { + height: $line-height * 2; + margin: 0 rem-calc(12); + } + } + } + + &:not(.complex) { + @include breakpoint(medium) { + width: 50%; + } + } + [type="submit"] { @include button($background: $link); border-radius: 0; @@ -10,8 +29,4 @@ @include button-disabled; } } - - @include breakpoint(medium) { - width: 50%; - } } diff --git a/app/components/admin/search_component.html.erb b/app/components/admin/search_component.html.erb index 0caf05d0e09e..0286dcb48811 100644 --- a/app/components/admin/search_component.html.erb +++ b/app/components/admin/search_component.html.erb @@ -1,4 +1,5 @@ <%= form_tag(url, options) do |f| %> <%= text_field_tag :search, search_terms.to_s, placeholder: label, "aria-label": label %> + <%= content %> <%= submit_tag t("admin.shared.search.search") %> <% end %> diff --git a/app/components/sdg_management/menu_component.rb b/app/components/sdg_management/menu_component.rb index e626b2736a07..53e27c231a75 100644 --- a/app/components/sdg_management/menu_component.rb +++ b/app/components/sdg_management/menu_component.rb @@ -4,14 +4,49 @@ class SDGManagement::MenuComponent < ApplicationComponent private def links - [goals_link] + [goals_link, *relatable_links] end def goals_link [t("sdg_management.menu.sdg_content"), sdg_management_goals_path, sdg?, class: "goals-link"] end + def relatable_links + SDG::Related::RELATABLE_TYPES.map do |type| + next unless setting["process.#{process_name(type)}"] && setting["sdg.process.#{process_name(type)}"] + + [ + t("sdg_management.menu.#{table_name(type)}"), + relatable_type_path(type), + controller_name == "relations" && params[:relatable_type] == type.tableize, + class: "#{table_name(type).tr("_", "-")}-link" + ] + end + end + def sdg? %w[goals targets local_targets].include?(controller_name) end + + def relatable_type_path(type) + { + controller: "sdg_management/relations", + action: :index, + relatable_type: type.tableize + } + end + + def table_name(type) + type.constantize.table_name + end + + def process_name(type) + process_name = type.split("::").first + + if process_name == "Legislation" + "legislation" + else + process_name.constantize.table_name + end + end end diff --git a/app/components/sdg_management/relations/edit_component.html.erb b/app/components/sdg_management/relations/edit_component.html.erb new file mode 100644 index 000000000000..807a4a2dc6f1 --- /dev/null +++ b/app/components/sdg_management/relations/edit_component.html.erb @@ -0,0 +1,7 @@ +<%= header %> + +<%= form_for record, url: update_path do |f| %> + <%= f.text_field :sdg_target_list %> + + <%= f.submit %> +<% end %> diff --git a/app/components/sdg_management/relations/edit_component.rb b/app/components/sdg_management/relations/edit_component.rb new file mode 100644 index 000000000000..b0dc3b7df6dc --- /dev/null +++ b/app/components/sdg_management/relations/edit_component.rb @@ -0,0 +1,24 @@ +class SDGManagement::Relations::EditComponent < ApplicationComponent + include Header + + attr_reader :record + + def initialize(record) + @record = record + end + + private + + def title + @record.title + end + + def update_path + { + controller: "sdg_management/relations", + action: :update, + relatable_type: record.class.name.tableize, + id: record + } + end +end diff --git a/app/components/sdg_management/relations/index_component.html.erb b/app/components/sdg_management/relations/index_component.html.erb new file mode 100644 index 000000000000..75e6b41705ce --- /dev/null +++ b/app/components/sdg_management/relations/index_component.html.erb @@ -0,0 +1,41 @@ +<%= header %> + +<%= render Admin::SearchComponent.new(label: search_label, class: "complex") do |component| %> + <%= component.select_tag :goal_code, goal_options, + include_blank: goal_blank_option, + "aria-label": goal_label %> + <%= component.select_tag :target_code, target_options, + include_blank: target_blank_option, + "aria-label": target_label %> +<% end %> + + + + + + + + + + + + + <% @records.each do |record| %> + + + + + + + <% end %> + +
<%= model_class.human_attribute_name(:title) %><%= SDG::Goal.model_name.human(count: 2).upcase_first %><%= SDG::Target.model_name.human(count: 2).upcase_first %><%= t("admin.actions.actions") %>
<%= record.title %><%= record.sdg_goal_list %><%= record.sdg_target_list %> + <%= render Admin::TableActionsComponent.new( + record, + actions: [:edit], + edit_text: t("sdg_management.actions.edit"), + edit_path: edit_path_for(record) + ) %> +
+ +<%= paginate(@records) %> diff --git a/app/components/sdg_management/relations/index_component.rb b/app/components/sdg_management/relations/index_component.rb new file mode 100644 index 000000000000..170fa7573cac --- /dev/null +++ b/app/components/sdg_management/relations/index_component.rb @@ -0,0 +1,56 @@ +class SDGManagement::Relations::IndexComponent < ApplicationComponent + include Header + + attr_reader :records + + def initialize(records) + @records = records + end + + private + + def title + t("sdg_management.menu.#{model_class.table_name}") + end + + def model_class + records.model + end + + def edit_path_for(record) + { + controller: "sdg_management/relations", + action: :edit, + relatable_type: record.class.name.tableize, + id: record + } + end + + def search_label + t("admin.shared.search.label.#{model_class.table_name}") + end + + def goal_label + t("admin.shared.search.advanced_filters.sdg_goals.label") + end + + def goal_blank_option + t("admin.shared.search.advanced_filters.sdg_goals.all") + end + + def target_label + t("admin.shared.search.advanced_filters.sdg_targets.label") + end + + def target_blank_option + t("admin.shared.search.advanced_filters.sdg_targets.all") + end + + def goal_options + options_from_collection_for_select(SDG::Goal.all, :code, :code_and_title, params[:goal_code]) + end + + def target_options + options_from_collection_for_select(SDG::Target.all.sort, :code, :code, params[:target_code]) + end +end diff --git a/app/components/sdg_management/targets/index_component.html.erb b/app/components/sdg_management/targets/index_component.html.erb index 161ffd35224c..b7e9315fe775 100644 --- a/app/components/sdg_management/targets/index_component.html.erb +++ b/app/components/sdg_management/targets/index_component.html.erb @@ -13,7 +13,7 @@ <% targets.group_by(&:goal).map do |goal, targets| %> - <%= goal.code %>. <%= goal.title %> + <%= goal.code_and_title %> diff --git a/app/controllers/sdg_management/relations_controller.rb b/app/controllers/sdg_management/relations_controller.rb new file mode 100644 index 000000000000..e16c1f63be94 --- /dev/null +++ b/app/controllers/sdg_management/relations_controller.rb @@ -0,0 +1,42 @@ +class SDGManagement::RelationsController < SDGManagement::BaseController + before_action :check_feature_flags + before_action :load_record, only: [:edit, :update] + + def index + @records = relatable_class + .accessible_by(current_ability) + .by_goal(params[:goal_code]) + .by_target(params[:target_code]) + .order(:id) + .page(params[:page]) + + @records = @records.search(params[:search]) if params[:search].present? + end + + def edit + end + + def update + @record.sdg_target_list = params[@record.class.table_name.singularize][:sdg_target_list] + + redirect_to action: :index + end + + private + + def load_record + @record = relatable_class.find(params[:id]) + end + + def relatable_class + params[:relatable_type].classify.constantize + end + + def check_feature_flags + process_name = params[:relatable_type].split("/").first + process_name = process_name.pluralize unless process_name == "legislation" + + check_feature_flag(process_name) + raise FeatureDisabled, process_name unless Setting["sdg.process.#{process_name}"] + end +end diff --git a/app/models/concerns/sdg/relatable.rb b/app/models/concerns/sdg/relatable.rb index 9da20b0d5368..07d71afdc30e 100644 --- a/app/models/concerns/sdg/relatable.rb +++ b/app/models/concerns/sdg/relatable.rb @@ -12,7 +12,40 @@ module SDG::Relatable end end + class_methods do + def by_goal(code) + by_sdg_related(SDG::Goal, code) + end + + def by_target(code) + by_sdg_related(SDG::Target, code) + end + + def by_sdg_related(sdg_class, code) + return all if code.blank? + + joins(sdg_class.table_name.to_sym).merge(sdg_class.where(code: code)) + end + end + def related_sdgs sdg_relations.map(&:related_sdg) end + + def sdg_goal_list + sdg_goals.order(:code).map(&:code).join(", ") + end + + def sdg_target_list + sdg_targets.sort.map(&:code).join(", ") + end + + def sdg_target_list=(codes) + targets = codes.tr(" ", "").split(",").map { |code| SDG::Target[code] } + + transaction do + self.sdg_targets = targets + self.sdg_goals = targets.map(&:goal).uniq + end + end end diff --git a/app/models/legislation/process.rb b/app/models/legislation/process.rb index f1a2e12c14ae..abf8875a7152 100644 --- a/app/models/legislation/process.rb +++ b/app/models/legislation/process.rb @@ -5,6 +5,7 @@ class Legislation::Process < ApplicationRecord include Imageable include Documentable include SDG::Relatable + include Searchable acts_as_paranoid column: :hidden_at acts_as_taggable_on :customs @@ -123,6 +124,22 @@ def status end end + def searchable_translations_definitions + { + title => "A", + summary => "C", + description => "D" + } + end + + def searchable_values + searchable_globalized_values + end + + def self.search(terms) + pg_search(terms) + end + private def valid_date_ranges diff --git a/app/models/poll.rb b/app/models/poll.rb index 0289a0fc2500..08e11f854a40 100644 --- a/app/models/poll.rb +++ b/app/models/poll.rb @@ -5,6 +5,7 @@ class Poll < ApplicationRecord acts_as_paranoid column: :hidden_at include ActsAsParanoidAliases include Notifiable + include Searchable include Sluggable include StatsVersionable include Reportable @@ -175,4 +176,20 @@ def answer_count def budget_poll? budget.present? end + + def searchable_translations_definitions + { + name => "A", + summary => "C", + description => "D" + } + end + + def searchable_values + searchable_globalized_values + end + + def self.search(terms) + pg_search(terms) + end end diff --git a/app/models/sdg/goal.rb b/app/models/sdg/goal.rb index bd26438c8cff..b90a14dfd306 100644 --- a/app/models/sdg/goal.rb +++ b/app/models/sdg/goal.rb @@ -16,4 +16,8 @@ def description def self.[](code) find_by!(code: code) end + + def code_and_title + "#{code}. #{title}" + end end diff --git a/app/views/admin/debates/index.html.erb b/app/views/admin/debates/index.html.erb index 7a743aea15c9..0d3afd507be4 100644 --- a/app/views/admin/debates/index.html.erb +++ b/app/views/admin/debates/index.html.erb @@ -5,7 +5,7 @@

<%= t("admin.debates.index.title") %>

<% if @debates.any? %> - <%= render Admin::SearchComponent.new(label: t("admin.shared.debate_search.placeholder")) %> + <%= render Admin::SearchComponent.new(label: t("admin.shared.search.label.debates")) %>

<%= page_entries_info @debates %>

diff --git a/app/views/admin/poll/booth_assignments/_search_booths.html.erb b/app/views/admin/poll/booth_assignments/_search_booths.html.erb index adf7ed00d8a5..bbed99f43850 100644 --- a/app/views/admin/poll/booth_assignments/_search_booths.html.erb +++ b/app/views/admin/poll/booth_assignments/_search_booths.html.erb @@ -3,7 +3,7 @@
<%= text_field_tag :search, @search, - placeholder: t("admin.shared.booths_search.placeholder"), id: "search-booths" %> + placeholder: t("admin.shared.search.label.booths"), id: "search-booths" %>
<%= submit_tag t("admin.shared.search.search"), class: "button" %>
diff --git a/app/views/admin/poll/booths/index.html.erb b/app/views/admin/poll/booths/index.html.erb index 4954801a2899..37937d89e685 100644 --- a/app/views/admin/poll/booths/index.html.erb +++ b/app/views/admin/poll/booths/index.html.erb @@ -4,7 +4,7 @@ <%= link_to t("admin.booths.index.add_booth"), new_admin_booth_path, class: "button float-right" %> <% end %> -<%= render Admin::SearchComponent.new(label: t("admin.shared.booths_search.placeholder")) %> +<%= render Admin::SearchComponent.new(label: t("admin.shared.search.label.booths")) %> <% if @booths.empty? %>
diff --git a/app/views/admin/poll/officer_assignments/_search_officers.html.erb b/app/views/admin/poll/officer_assignments/_search_officers.html.erb index b1602dc7e2ab..69f7e2b107ed 100644 --- a/app/views/admin/poll/officer_assignments/_search_officers.html.erb +++ b/app/views/admin/poll/officer_assignments/_search_officers.html.erb @@ -3,7 +3,7 @@
<%= text_field_tag :search, @search, - placeholder: t("admin.shared.poll_officers_search.placeholder"), id: "search-officers" %> + placeholder: t("admin.shared.search.label.poll_officers"), id: "search-officers" %>
<%= submit_tag t("admin.shared.search.search"), class: "button" %>
diff --git a/app/views/admin/poll/questions/_search.html.erb b/app/views/admin/poll/questions/_search.html.erb index 73730dcd62d8..6274fbea1aa9 100644 --- a/app/views/admin/poll/questions/_search.html.erb +++ b/app/views/admin/poll/questions/_search.html.erb @@ -1 +1 @@ -<%= render Admin::SearchComponent.new(label: t("admin.shared.poll_questions_search.placeholder")) %> +<%= render Admin::SearchComponent.new(label: t("admin.shared.search.label.poll_questions")) %> diff --git a/app/views/admin/proposals/index.html.erb b/app/views/admin/proposals/index.html.erb index 075e702789a7..2c62d0c25592 100644 --- a/app/views/admin/proposals/index.html.erb +++ b/app/views/admin/proposals/index.html.erb @@ -5,7 +5,7 @@

<%= t("admin.proposals.index.title") %>

<% if @proposals.any? %> - <%= render Admin::SearchComponent.new(label: t("admin.shared.proposal_search.placeholder")) %> + <%= render Admin::SearchComponent.new(label: t("admin.shared.search.label.proposals")) %>

<%= page_entries_info @proposals %>

diff --git a/app/views/admin/shared/_user_search.html.erb b/app/views/admin/shared/_user_search.html.erb index 2023fd0edec6..5a074eb1e68d 100644 --- a/app/views/admin/shared/_user_search.html.erb +++ b/app/views/admin/shared/_user_search.html.erb @@ -1 +1 @@ -<%= render Admin::SearchComponent.new(url: url, label: t("admin.shared.user_search.placeholder")) %> +<%= render Admin::SearchComponent.new(url: url, label: t("admin.shared.search.label.users")) %> diff --git a/app/views/management/proposals/index.html.erb b/app/views/management/proposals/index.html.erb index a5862961e244..549fc8f4849c 100644 --- a/app/views/management/proposals/index.html.erb +++ b/app/views/management/proposals/index.html.erb @@ -1,7 +1,7 @@

<%= t("management.proposals.index.title") %>

- <%= render Admin::SearchComponent.new(label: t("admin.shared.proposal_search.placeholder")) %> + <%= render Admin::SearchComponent.new(label: t("admin.shared.search.label.proposals")) %>
diff --git a/app/views/sdg_management/relations/edit.html.erb b/app/views/sdg_management/relations/edit.html.erb new file mode 100644 index 000000000000..4092359d7eb7 --- /dev/null +++ b/app/views/sdg_management/relations/edit.html.erb @@ -0,0 +1 @@ +<%= render SDGManagement::Relations::EditComponent.new(@record) %> diff --git a/app/views/sdg_management/relations/index.html.erb b/app/views/sdg_management/relations/index.html.erb new file mode 100644 index 000000000000..55878cd907ff --- /dev/null +++ b/app/views/sdg_management/relations/index.html.erb @@ -0,0 +1 @@ +<%= render SDGManagement::Relations::IndexComponent.new(@records) %> diff --git a/config/locales/en/activerecord.yml b/config/locales/en/activerecord.yml index bc561d09360f..84264fbceedc 100644 --- a/config/locales/en/activerecord.yml +++ b/config/locales/en/activerecord.yml @@ -2,6 +2,7 @@ en: attributes: geozone_id: "Scope of operation" results_enabled: "Show results" + sdg_target_list: "Targets" stats_enabled: "Show stats" advanced_stats_enabled: "Show advanced stats" name: Name @@ -287,6 +288,7 @@ en: responsible_name: "Person responsible for the group" poll: name: "Name" + title: "Name" starts_at: "Start Date" ends_at: "Closing Date" geozone_restricted: "Restricted by geozone" diff --git a/config/locales/en/admin.yml b/config/locales/en/admin.yml index df6d749902af..18d9ceb534a2 100644 --- a/config/locales/en/admin.yml +++ b/config/locales/en/admin.yml @@ -1254,19 +1254,24 @@ en: true_value: "Yes" false_value: "No" search: + advanced_filters: + sdg_goals: + all: "All goals" + label: "By goal" + sdg_targets: + all: "All targets" + label: "By target" + label: + booths: "Search booth by name or location" + budget_investments: "Search investments by title, description or heading" + debates: "Search debates by title or description" + legislation_processes: "Search processes by title or description" + poll_officers: "Search poll officers" + poll_questions: "Search poll questions" + polls: "Search polls by name or description" + proposals: "Search proposals by title, code, description or question" + users: "Search user by name or email" search: "Search" - booths_search: - placeholder: Search booth by name or location - poll_officers_search: - placeholder: Search poll officers - poll_questions_search: - placeholder: Search poll questions - proposal_search: - placeholder: Search proposals by title, code, description or question - debate_search: - placeholder: Search debates by title or description - user_search: - placeholder: Search user by name or email search_results: "Search results" no_search_results: "No results found." actions: Actions diff --git a/config/locales/en/sdg_management.yml b/config/locales/en/sdg_management.yml index 1e075fd76870..bacc84bbb5f5 100644 --- a/config/locales/en/sdg_management.yml +++ b/config/locales/en/sdg_management.yml @@ -1,8 +1,15 @@ en: sdg_management: + actions: + edit: "Manage goals and targets" header: title: "SDG content" menu: + budget_investments: "Participatory budgets" + debates: "Debates" + legislation_processes: "Collaborative legislation" + polls: "Polls" + proposals: "Proposals" sdg_content: "Goals and Targets" local_targets: create: diff --git a/config/locales/es/activerecord.yml b/config/locales/es/activerecord.yml index 1f505a6bca45..d59c643c4070 100644 --- a/config/locales/es/activerecord.yml +++ b/config/locales/es/activerecord.yml @@ -2,6 +2,7 @@ es: attributes: geozone_id: "Ámbito de actuación" results_enabled: "Mostrar resultados" + sdg_target_list: "Metas" stats_enabled: "Mostrar estadísticas" advanced_stats_enabled: "Mostrar estadísticas avanzadas" name: Nombre @@ -287,6 +288,7 @@ es: responsible_name: "Persona responsable del colectivo" poll: name: "Nombre" + title: "Nombre" starts_at: "Fecha de apertura" ends_at: "Fecha de cierre" geozone_restricted: "Restringida por zonas" diff --git a/config/locales/es/admin.yml b/config/locales/es/admin.yml index 81ecc1259d5c..c03515bf223b 100644 --- a/config/locales/es/admin.yml +++ b/config/locales/es/admin.yml @@ -1253,19 +1253,24 @@ es: true_value: "Sí" false_value: "No" search: + advanced_filters: + sdg_goals: + all: "Todos los objetivos" + label: "Por objetivo" + sdg_targets: + all: "Todas las metas" + label: "Por meta" + label: + booths: "Buscar urna por nombre" + budget_investments: "Buscar proyectos por título, descripción o partida" + debates: "Buscar debates por título o descripción" + legislation_processes: "Buscar procesos por título o descripción" + poll_officers: "Buscar presidentes de mesa" + poll_questions: "Buscar preguntas" + polls: "Buscar votaciones por nombre o descripción" + proposals: "Buscar propuestas por título, código, descripción o pregunta" + users: "Buscar usuario por nombre o email" search: "Buscar" - booths_search: - placeholder: Buscar urna por nombre - poll_officers_search: - placeholder: Buscar presidentes de mesa - poll_questions_search: - placeholder: Buscar preguntas - proposal_search: - placeholder: Buscar propuestas por título, código, descripción o pregunta - debate_search: - placeholder: Buscar debates por título o descripción - user_search: - placeholder: Buscar usuario por nombre o email search_results: "Resultados de la búsqueda" no_search_results: "No se han encontrado resultados." actions: Acciones diff --git a/config/locales/es/sdg_management.yml b/config/locales/es/sdg_management.yml index 4ee4d8893c61..23c10bd4a73d 100644 --- a/config/locales/es/sdg_management.yml +++ b/config/locales/es/sdg_management.yml @@ -1,8 +1,15 @@ es: sdg_management: + actions: + edit: "Asignar objetivos y metas" header: title: "Contenido ODS" menu: + budget_investments: "Presupuestos participativos" + debates: "Debates" + legislation_processes: "Legislación colaborativa" + polls: "Votaciones" + proposals: "Propuestas" sdg_content: "Objetivos y Metas" local_targets: create: diff --git a/config/routes/sdg_management.rb b/config/routes/sdg_management.rb index cad7cbe66de9..6f303b45c4cd 100644 --- a/config/routes/sdg_management.rb +++ b/config/routes/sdg_management.rb @@ -4,4 +4,16 @@ resources :goals, only: [:index] resources :targets, only: [:index] resources :local_targets, except: [:show] + + types = SDG::Related::RELATABLE_TYPES.map(&:tableize) + types_constraint = /#{types.join("|")}/ + + get "*relatable_type", to: "relations#index", as: "relations", relatable_type: types_constraint + get "*relatable_type/:id/edit", to: "relations#edit", as: "edit_relation", relatable_type: types_constraint + patch "*relatable_type/:id", to: "relations#update", as: "relation", relatable_type: types_constraint + + types.each do |type| + get type, to: "relations#index", as: type + get "#{type}/:id/edit", to: "relations#edit", as: "edit_#{type.singularize}" + end end diff --git a/db/migrate/20201216132234_add_tsv_to_polls.rb b/db/migrate/20201216132234_add_tsv_to_polls.rb new file mode 100644 index 000000000000..a629bf975046 --- /dev/null +++ b/db/migrate/20201216132234_add_tsv_to_polls.rb @@ -0,0 +1,5 @@ +class AddTsvToPolls < ActiveRecord::Migration[5.2] + def change + add_column :polls, :tsv, :tsvector + end +end diff --git a/db/migrate/20201216132642_add_tsv_to_legislation_processes.rb b/db/migrate/20201216132642_add_tsv_to_legislation_processes.rb new file mode 100644 index 000000000000..fdeb8fdd5a1c --- /dev/null +++ b/db/migrate/20201216132642_add_tsv_to_legislation_processes.rb @@ -0,0 +1,5 @@ +class AddTsvToLegislationProcesses < ActiveRecord::Migration[5.2] + def change + add_column :legislation_processes, :tsv, :tsvector + end +end diff --git a/db/schema.rb b/db/schema.rb index 5f69e9ac43f1..6a67f083fb4a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2020_11_24_145559) do +ActiveRecord::Schema.define(version: 2020_12_16_132642) do # These are extensions that must be enabled in order to support this database enable_extension "pg_trgm" @@ -747,6 +747,7 @@ t.boolean "homepage_enabled", default: false t.text "background_color" t.text "font_color" + t.tsvector "tsv" t.index ["allegations_end_date"], name: "index_legislation_processes_on_allegations_end_date" t.index ["allegations_start_date"], name: "index_legislation_processes_on_allegations_start_date" t.index ["debate_end_date"], name: "index_legislation_processes_on_debate_end_date" @@ -1166,6 +1167,7 @@ t.integer "budget_id" t.string "related_type" t.integer "related_id" + t.tsvector "tsv" t.index ["budget_id"], name: "index_polls_on_budget_id", unique: true t.index ["related_type", "related_id"], name: "index_polls_on_related_type_and_related_id" t.index ["starts_at", "ends_at"], name: "index_polls_on_starts_at_and_ends_at" diff --git a/lib/tasks/consul.rake b/lib/tasks/consul.rake index 911460c68607..4f169c387a50 100644 --- a/lib/tasks/consul.rake +++ b/lib/tasks/consul.rake @@ -6,6 +6,7 @@ namespace :consul do desc "Runs tasks needed to upgrade from 1.2.0 to 1.3.0" task "execute_release_1.3.0_tasks": [ - "db:load_sdg" + "db:load_sdg", + "db:calculate_tsv" ] end diff --git a/lib/tasks/db.rake b/lib/tasks/db.rake index a64643adc518..622f9e9c541d 100644 --- a/lib/tasks/db.rake +++ b/lib/tasks/db.rake @@ -10,4 +10,10 @@ namespace :db do ApplicationLogger.new.info "Adding Sustainable Development Goals content" load(Rails.root.join("db", "sdg.rb")) end + + desc "Calculates the TSV column for all polls and legislation processes" + task calculate_tsv: :environment do + Poll.find_each(&:calculate_tsvector) + Legislation::Process.find_each(&:calculate_tsvector) + end end diff --git a/spec/components/sdg_management/menu_component_spec.rb b/spec/components/sdg_management/menu_component_spec.rb new file mode 100644 index 000000000000..10547e4dd320 --- /dev/null +++ b/spec/components/sdg_management/menu_component_spec.rb @@ -0,0 +1,92 @@ +require "rails_helper" + +describe SDGManagement::MenuComponent, type: :component do + let(:component) { SDGManagement::MenuComponent.new } + + before do + Setting["sdg.process.budgets"] = true + Setting["sdg.process.debates"] = true + Setting["sdg.process.legislation"] = true + Setting["sdg.process.polls"] = true + Setting["sdg.process.proposals"] = true + end + + context "processes enabled" do + it "generates links to all processes" do + render_inline component + + expect(page).to have_link "Goals and Targets" + expect(page).to have_link "Participatory budgets" + expect(page).to have_link "Debates" + expect(page).to have_link "Collaborative legislation" + expect(page).to have_link "Polls" + expect(page).to have_link "Proposals" + end + end + + context "processes disabled" do + before do + Setting["process.budgets"] = false + Setting["process.debates"] = false + Setting["process.legislation"] = false + Setting["process.polls"] = false + Setting["process.proposals"] = false + end + + it "does not generate links to any processes" do + render_inline component + + expect(page).to have_css "a", count: 1 + expect(page).to have_link "Goals and Targets" + end + end + + context "SDG processes disabled" do + before do + Setting["sdg.process.budgets"] = false + Setting["sdg.process.debates"] = false + Setting["sdg.process.legislation"] = false + Setting["sdg.process.polls"] = false + Setting["sdg.process.proposals"] = false + end + + it "does not generate links to any processes" do + render_inline component + + expect(page).to have_css "a", count: 1 + expect(page).to have_link "Goals and Targets" + end + end + + context "one process disabled" do + before { Setting["process.debates"] = false } + + it "generates links to the enabled processes" do + render_inline component + + expect(page).to have_link "Goals and Targets" + expect(page).to have_link "Participatory budgets" + expect(page).to have_link "Collaborative legislation" + expect(page).to have_link "Polls" + expect(page).to have_link "Proposals" + + expect(page).not_to have_link "Debates" + end + end + + context "one SDG process disabled" do + before { Setting["sdg.process.legislation"] = false } + + it "generates links to the enabled processes" do + render_inline component + + expect(page).to have_link "Goals and Targets" + expect(page).to have_link "Debates" + expect(page).to have_link "Participatory budgets" + expect(page).to have_link "Polls" + expect(page).to have_link "Proposals" + + expect(page).not_to have_link "Collaborative legislation" + end + end +end diff --git a/spec/controllers/sdg_management/relations_spec.rb b/spec/controllers/sdg_management/relations_spec.rb new file mode 100644 index 000000000000..fc5e8ecb7d03 --- /dev/null +++ b/spec/controllers/sdg_management/relations_spec.rb @@ -0,0 +1,98 @@ +require "rails_helper" + +describe SDGManagement::RelationsController do + before do + sign_in create(:administrator).user + + Setting["feature.sdg"] = true + Setting["sdg.process.budgets"] = true + Setting["sdg.process.debates"] = true + Setting["sdg.process.legislation"] = true + Setting["sdg.process.polls"] = true + Setting["sdg.process.proposals"] = true + end + + context "processes disabled" do + it "raises feature disabled for budgets" do + Setting["process.budgets"] = false + + expect do + get :index, params: { relatable_type: "budget/investments" } + end.to raise_exception(FeatureFlags::FeatureDisabled) + end + + it "raises feature disabled for debates" do + Setting["process.debates"] = false + + expect do + get :index, params: { relatable_type: "debates" } + end.to raise_exception(FeatureFlags::FeatureDisabled) + end + + it "raises feature disabled for legislation processes" do + Setting["process.legislation"] = false + + expect do + get :index, params: { relatable_type: "legislation/processes" } + end.to raise_exception(FeatureFlags::FeatureDisabled) + end + + it "raises feature disabled for polls" do + Setting["process.polls"] = false + + expect do + get :index, params: { relatable_type: "polls" } + end.to raise_exception(FeatureFlags::FeatureDisabled) + end + + it "raises feature disabled for proposals" do + Setting["process.proposals"] = false + + expect do + get :index, params: { relatable_type: "proposals" } + end.to raise_exception(FeatureFlags::FeatureDisabled) + end + end + + context "SDG processes disabled" do + it "raises feature disabled for budgets" do + Setting["sdg.process.budgets"] = false + + expect do + get :index, params: { relatable_type: "budget/investments" } + end.to raise_exception(FeatureFlags::FeatureDisabled) + end + + it "raises feature disabled for debates" do + Setting["sdg.process.debates"] = false + + expect do + get :index, params: { relatable_type: "debates" } + end.to raise_exception(FeatureFlags::FeatureDisabled) + end + + it "raises feature disabled for legislation processes" do + Setting["sdg.process.legislation"] = false + + expect do + get :index, params: { relatable_type: "legislation/processes" } + end.to raise_exception(FeatureFlags::FeatureDisabled) + end + + it "raises feature disabled for polls" do + Setting["sdg.process.polls"] = false + + expect do + get :index, params: { relatable_type: "polls" } + end.to raise_exception(FeatureFlags::FeatureDisabled) + end + + it "raises feature disabled for proposals" do + Setting["sdg.process.proposals"] = false + + expect do + get :index, params: { relatable_type: "proposals" } + end.to raise_exception(FeatureFlags::FeatureDisabled) + end + end +end diff --git a/spec/lib/tasks/load_sdg_spec.rb b/spec/lib/tasks/db_spec.rb similarity index 56% rename from spec/lib/tasks/load_sdg_spec.rb rename to spec/lib/tasks/db_spec.rb index 6581ba7a5bfe..b556b41b4db7 100644 --- a/spec/lib/tasks/load_sdg_spec.rb +++ b/spec/lib/tasks/db_spec.rb @@ -32,3 +32,33 @@ expect(SDG::Target.last.id).to eq target_id end end + +describe "rake db:calculate_tsv" do + before { Rake::Task["db:calculate_tsv"].reenable } + + let :run_rake_task do + Rake.application.invoke_task("db:calculate_tsv") + end + + it "calculates the tsvector for polls" do + poll = create(:poll) + poll.update_column(:tsv, nil) + + expect(poll.reload.tsv).to be nil + + run_rake_task + + expect(poll.reload.tsv).not_to be nil + end + + it "calculates the tsvector for legislation processes" do + process = create(:legislation_process) + process.update_column(:tsv, nil) + + expect(process.reload.tsv).to be nil + + run_rake_task + + expect(process.reload.tsv).not_to be nil + end +end diff --git a/spec/models/legislation/process_spec.rb b/spec/models/legislation/process_spec.rb index 0ea89c71577d..5302497a07d5 100644 --- a/spec/models/legislation/process_spec.rb +++ b/spec/models/legislation/process_spec.rb @@ -229,4 +229,35 @@ end end end + + describe ".search" do + let!(:traffic) do + create(:legislation_process, + title: "Traffic regulation", + summary: "Lane structure", + description: "From top to bottom") + end + + let!(:animal_farm) do + create(:legislation_process, + title: "Hierarchy structure", + summary: "Pigs at the top", + description: "Napoleon in charge of the traffic") + end + + it "returns only matching polls" do + expect(Legislation::Process.search("lane")).to eq [traffic] + expect(Legislation::Process.search("pigs")).to eq [animal_farm] + expect(Legislation::Process.search("nothing here")).to be_empty + end + + it "gives more weight to name" do + expect(Legislation::Process.search("traffic")).to eq [traffic, animal_farm] + expect(Legislation::Process.search("structure")).to eq [animal_farm, traffic] + end + + it "gives more weight to summary than description" do + expect(Legislation::Process.search("top")).to eq [animal_farm, traffic] + end + end end diff --git a/spec/models/poll/poll_spec.rb b/spec/models/poll/poll_spec.rb index 0a1fc94bdcc3..1223e53d62fe 100644 --- a/spec/models/poll/poll_spec.rb +++ b/spec/models/poll/poll_spec.rb @@ -433,4 +433,29 @@ expect(poll.recounts_confirmed?).to be true end end + + describe ".search" do + let!(:square) do + create(:poll, name: "Square reform", summary: "Next to the park", description: "Give it more space") + end + + let!(:park) do + create(:poll, name: "New park", summary: "Green spaces", description: "Next to the square") + end + + it "returns only matching polls" do + expect(Poll.search("reform")).to eq [square] + expect(Poll.search("green")).to eq [park] + expect(Poll.search("nothing here")).to be_empty + end + + it "gives more weight to name" do + expect(Poll.search("square")).to eq [square, park] + expect(Poll.search("park")).to eq [park, square] + end + + it "gives more weight to summary than description" do + expect(Poll.search("space")).to eq [park, square] + end + end end diff --git a/spec/models/sdg/relatable_spec.rb b/spec/models/sdg/relatable_spec.rb index e6fd1b8d0c00..036b5af96c8b 100644 --- a/spec/models/sdg/relatable_spec.rb +++ b/spec/models/sdg/relatable_spec.rb @@ -28,6 +28,14 @@ end end + describe "#sdg_goal_list" do + it "orders goals by code" do + relatable.sdg_goals = [SDG::Goal[1], SDG::Goal[3], SDG::Goal[2]] + + expect(relatable.sdg_goal_list).to eq "1, 2, 3" + end + end + describe "#sdg_targets" do it "can assign targets to a model" do relatable.sdg_targets = [target, another_target] @@ -46,6 +54,14 @@ end end + describe "#sdg_target_list" do + it "orders targets by code" do + relatable.sdg_targets = [SDG::Target[2.2], SDG::Target[1.2], SDG::Target[2.1]] + + expect(relatable.sdg_target_list).to eq "1.2, 2.1, 2.2" + end + end + describe "#sdg_local_targets" do it "can assign local targets to a model" do relatable.sdg_local_targets = [local_target, another_local_target] @@ -74,4 +90,72 @@ expect(relatable.reload.related_sdgs).to match_array related_sdgs end end + + describe "#sdg_target_list=" do + it "assigns a single target" do + relatable.sdg_target_list = "1.1" + + expect(relatable.reload.sdg_targets).to match_array [SDG::Target["1.1"]] + end + + it "assigns multiple targets" do + relatable.sdg_target_list = "1.1,2.3" + + expect(relatable.reload.sdg_targets).to match_array [SDG::Target["1.1"], SDG::Target["2.3"]] + end + + it "ignores trailing spaces and spaces between commas" do + relatable.sdg_target_list = " 1.1, 2.3 " + + expect(relatable.reload.sdg_targets).to match_array [SDG::Target["1.1"], SDG::Target["2.3"]] + end + + it "assigns goals" do + relatable.sdg_target_list = "1.1,1.2,2.3" + + expect(relatable.reload.sdg_goals).to match_array [SDG::Goal[1], SDG::Goal[2]] + end + end + + describe ".by_goal" do + it "returns everything if no code is provided" do + expect(relatable.class.by_goal("")).to eq [relatable] + expect(relatable.class.by_goal(nil)).to eq [relatable] + end + + it "returns records associated with that goal" do + same_association = create(:proposal, sdg_goals: [goal]) + both_associations = create(:proposal, sdg_goals: [goal, another_goal]) + + expect(relatable.class.by_goal(goal.code)).to match_array [same_association, both_associations] + end + + it "does not return records not associated with that goal" do + create(:proposal) + create(:proposal, sdg_goals: [another_goal]) + + expect(relatable.class.by_goal(goal.code)).to be_empty + end + end + + describe ".by_target" do + it "returns everything if no code is provided" do + expect(relatable.class.by_target("")).to eq [relatable] + expect(relatable.class.by_target(nil)).to eq [relatable] + end + + it "returns records associated with that target" do + same_association = create(:proposal, sdg_targets: [target]) + both_associations = create(:proposal, sdg_targets: [target, another_target]) + + expect(relatable.class.by_target(target.code)).to match_array [same_association, both_associations] + end + + it "does not return records not associated with that target" do + create(:proposal) + create(:proposal, sdg_targets: [another_target]) + + expect(relatable.class.by_target(target.code)).to be_empty + end + end end diff --git a/spec/routing/sdg_management_routes_spec.rb b/spec/routing/sdg_management_routes_spec.rb new file mode 100644 index 000000000000..c04748e47fa5 --- /dev/null +++ b/spec/routing/sdg_management_routes_spec.rb @@ -0,0 +1,32 @@ +require "rails_helper" + +describe "SDG Management routes" do + it "maps routes for relatable classes" do + expect(get("/sdg_management/proposals")).to route_to( + controller: "sdg_management/relations", + action: "index", + relatable_type: "proposals" + ) + end + + it "admits named routes" do + expect(get(sdg_management_polls_path)).to route_to( + controller: "sdg_management/relations", + action: "index", + relatable_type: "polls" + ) + end + + it "routes relatable types containing a slash" do + expect(url_for( + controller: "sdg_management/relations", + action: "index", + relatable_type: "legislation/processes", + only_path: true + )).to eq "/sdg_management/legislation/processes" + end + + it "does not accept non-relatable classes" do + expect(get("/sdg_management/tags")).not_to be_routable + end +end diff --git a/spec/system/sdg_management/relations_spec.rb b/spec/system/sdg_management/relations_spec.rb new file mode 100644 index 000000000000..05b0a9965ce1 --- /dev/null +++ b/spec/system/sdg_management/relations_spec.rb @@ -0,0 +1,145 @@ +require "rails_helper" + +describe "SDG Relations", :js do + before do + login_as(create(:administrator).user) + Setting["feature.sdg"] = true + Setting["sdg.process.budgets"] = true + Setting["sdg.process.debates"] = true + Setting["sdg.process.legislation"] = true + Setting["sdg.process.polls"] = true + Setting["sdg.process.proposals"] = true + end + + scenario "navigation" do + visit sdg_management_root_path + + within("#side_menu") { click_link "Participatory budgets" } + + expect(page).to have_current_path "/sdg_management/budget/investments" + expect(page).to have_css "h2", exact_text: "Participatory budgets" + + within("#side_menu") { click_link "Debates" } + + expect(page).to have_current_path "/sdg_management/debates" + expect(page).to have_css "h2", exact_text: "Debates" + + within("#side_menu") { click_link "Collaborative legislation" } + + expect(page).to have_current_path "/sdg_management/legislation/processes" + expect(page).to have_css "h2", exact_text: "Collaborative legislation" + + within("#side_menu") { click_link "Polls" } + + expect(page).to have_current_path "/sdg_management/polls" + expect(page).to have_css "h2", exact_text: "Polls" + + within("#side_menu") { click_link "Proposals" } + + expect(page).to have_current_path "/sdg_management/proposals" + expect(page).to have_css "h2", exact_text: "Proposals" + end + + describe "Index" do + scenario "list records for the current model" do + create(:debate, title: "I'm a debate") + create(:proposal, title: "And I'm a proposal") + + visit sdg_management_debates_path + + expect(page).to have_text "I'm a debate" + expect(page).not_to have_text "I'm a proposal" + + visit sdg_management_proposals_path + + expect(page).to have_text "I'm a proposal" + expect(page).not_to have_text "I'm a debate" + end + + scenario "list goals and target for all records" do + redistribute = create(:proposal, title: "Resources distribution") + redistribute.sdg_goals = [SDG::Goal[1]] + redistribute.sdg_targets = [SDG::Target["1.1"]] + + treatment = create(:proposal, title: "Treatment system") + treatment.sdg_goals = [SDG::Goal[6]] + treatment.sdg_targets = [SDG::Target["6.1"], SDG::Target["6.2"]] + + visit sdg_management_proposals_path + + within("tr", text: "Resources distribution") do + expect(page).to have_content "1.1" + end + + within("tr", text: "Treatment system") do + expect(page).to have_content "6.1, 6.2" + end + end + + scenario "shows link to edit a record" do + create(:budget_investment, title: "Build a hospital") + + visit sdg_management_budget_investments_path + + within("tr", text: "Build a hospital") do + click_link "Manage goals and targets" + end + + expect(page).to have_css "h2", exact_text: "Build a hospital" + end + + describe "search" do + scenario "search by terms" do + create(:poll, name: "Internet speech freedom") + create(:poll, name: "SDG interest") + + visit sdg_management_polls_path + + fill_in "search", with: "speech" + click_button "Search" + + expect(page).to have_content "Internet speech freedom" + expect(page).not_to have_content "SDG interest" + end + + scenario "goal filter" do + create(:budget_investment, title: "School", sdg_goals: [SDG::Goal[4]]) + create(:budget_investment, title: "Hospital", sdg_goals: [SDG::Goal[3]]) + + visit sdg_management_budget_investments_path + select "4. Quality Education", from: "goal_code" + click_button "Search" + + expect(page).to have_content "School" + expect(page).not_to have_content "Hospital" + end + end + + scenario "target filter" do + create(:budget_investment, title: "School", sdg_targets: [SDG::Target[4.1]]) + create(:budget_investment, title: "Preschool", sdg_targets: [SDG::Target[4.2]]) + + visit sdg_management_budget_investments_path + select "4.1", from: "target_code" + click_button "Search" + + expect(page).to have_content "School" + expect(page).not_to have_content "Preschool" + end + end + + describe "Edit" do + scenario "allows changing the targets" do + process = create(:legislation_process, title: "SDG process") + process.sdg_targets = [SDG::Target["3.3"]] + + visit sdg_management_edit_legislation_process_path(process) + fill_in "Targets", with: "1.2, 2.1" + click_button "Update Process" + + within("tr", text: "SDG process") do + expect(page).to have_css "td", exact_text: "1.2, 2.1" + end + end + end +end