Merge pull request #4269 from consul/sdg_management

Add SDG content management section
This commit is contained in:
Javi Martín
2020-12-21 18:52:25 +01:00
committed by GitHub
48 changed files with 949 additions and 59 deletions

View File

@@ -36,7 +36,8 @@
} }
&.budgets-link, &.budgets-link,
&.investments-link { &.investments-link,
&.budget-investments-link {
@include icon(chart-pie, solid); @include icon(chart-pie, solid);
} }
@@ -56,7 +57,8 @@
@include icon(envelope, regular); @include icon(envelope, regular);
} }
&.legislation-link { &.legislation-link,
&.legislation-processes-link {
@include icon(file-alt, solid); @include icon(file-alt, solid);
} }

View File

@@ -1,6 +1,25 @@
.admin [role=search] { .admin [role=search] {
display: flex; 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"] { [type="submit"] {
@include button($background: $link); @include button($background: $link);
border-radius: 0; border-radius: 0;
@@ -10,8 +29,4 @@
@include button-disabled; @include button-disabled;
} }
} }
@include breakpoint(medium) {
width: 50%;
}
} }

View File

@@ -1,4 +1,5 @@
<%= form_tag(url, options) do |f| %> <%= form_tag(url, options) do |f| %>
<%= text_field_tag :search, search_terms.to_s, placeholder: label, "aria-label": label %> <%= text_field_tag :search, search_terms.to_s, placeholder: label, "aria-label": label %>
<%= content %>
<%= submit_tag t("admin.shared.search.search") %> <%= submit_tag t("admin.shared.search.search") %>
<% end %> <% end %>

View File

@@ -1,9 +1 @@
<%= link_list( <%= link_list(*links, class: "sdg-content-menu") %>
[
t("sdg_management.menu.sdg_content"),
sdg_management_goals_path,
sdg?,
class: "goals-link"
],
class: "sdg-content-menu"
) %>

View File

@@ -3,7 +3,50 @@ class SDGManagement::MenuComponent < ApplicationComponent
private private
def links
[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? def sdg?
%w[goals targets local_targets].include?(controller_name) %w[goals targets local_targets].include?(controller_name)
end 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 end

View File

@@ -0,0 +1,7 @@
<%= header %>
<%= form_for record, url: update_path do |f| %>
<%= f.text_field :sdg_target_list %>
<%= f.submit %>
<% end %>

View File

@@ -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

View File

@@ -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 %>
<table>
<thead>
<tr>
<th><%= model_class.human_attribute_name(:title) %></th>
<th><%= SDG::Goal.model_name.human(count: 2).upcase_first %></th>
<th><%= SDG::Target.model_name.human(count: 2).upcase_first %></th>
<th><%= t("admin.actions.actions") %></th>
</tr>
</thead>
<tbody>
<% @records.each do |record| %>
<tr>
<td><%= record.title %></td>
<td><%= record.sdg_goal_list %></td>
<td><%= record.sdg_target_list %></td>
<td>
<%= render Admin::TableActionsComponent.new(
record,
actions: [:edit],
edit_text: t("sdg_management.actions.edit"),
edit_path: edit_path_for(record)
) %>
</td>
</tr>
<% end %>
</tbody>
</table>
<%= paginate(@records) %>

View File

@@ -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

View File

@@ -13,7 +13,7 @@
<% targets.group_by(&:goal).map do |goal, targets| %> <% targets.group_by(&:goal).map do |goal, targets| %>
<tr class="goal-header"> <tr class="goal-header">
<th id="<%= header_id(goal) %>" colspan="2" scope="colgroup"> <th id="<%= header_id(goal) %>" colspan="2" scope="colgroup">
<%= goal.code %>. <%= goal.title %> <%= goal.code_and_title %>
</th> </th>
</tr> </tr>

View File

@@ -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

View File

@@ -12,7 +12,40 @@ module SDG::Relatable
end end
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 def related_sdgs
sdg_relations.map(&:related_sdg) sdg_relations.map(&:related_sdg)
end 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 end

View File

@@ -5,6 +5,7 @@ class Legislation::Process < ApplicationRecord
include Imageable include Imageable
include Documentable include Documentable
include SDG::Relatable include SDG::Relatable
include Searchable
acts_as_paranoid column: :hidden_at acts_as_paranoid column: :hidden_at
acts_as_taggable_on :customs acts_as_taggable_on :customs
@@ -123,6 +124,22 @@ class Legislation::Process < ApplicationRecord
end end
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 private
def valid_date_ranges def valid_date_ranges

View File

@@ -5,6 +5,7 @@ class Poll < ApplicationRecord
acts_as_paranoid column: :hidden_at acts_as_paranoid column: :hidden_at
include ActsAsParanoidAliases include ActsAsParanoidAliases
include Notifiable include Notifiable
include Searchable
include Sluggable include Sluggable
include StatsVersionable include StatsVersionable
include Reportable include Reportable
@@ -175,4 +176,20 @@ class Poll < ApplicationRecord
def budget_poll? def budget_poll?
budget.present? budget.present?
end 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 end

View File

@@ -16,4 +16,8 @@ class SDG::Goal < ApplicationRecord
def self.[](code) def self.[](code)
find_by!(code: code) find_by!(code: code)
end end
def code_and_title
"#{code}. #{title}"
end
end end

View File

@@ -5,7 +5,7 @@
<h2><%= t("admin.debates.index.title") %></h2> <h2><%= t("admin.debates.index.title") %></h2>
<% if @debates.any? %> <% 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")) %>
<h3 class="inline-block"><%= page_entries_info @debates %></h3> <h3 class="inline-block"><%= page_entries_info @debates %></h3>

View File

@@ -9,7 +9,7 @@
class: "button float-right hollow" %> class: "button float-right hollow" %>
<%= render Admin::SearchComponent.new( <%= render Admin::SearchComponent.new(
label: t("admin.local_census_records.index.search.placeholder"), label: t("admin.shared.search.label.local_census_records"),
remote: true remote: true
) %> ) %>

View File

@@ -2,7 +2,7 @@
<%= render Admin::SearchComponent.new( <%= render Admin::SearchComponent.new(
url: search_admin_organizations_path, url: search_admin_organizations_path,
label: t("admin.organizations.index.search_placeholder") label: t("admin.shared.search.label.organizations")
) %> ) %>
<%= render "shared/filter_subnav", i18n_namespace: "admin.organizations.index" %> <%= render "shared/filter_subnav", i18n_namespace: "admin.organizations.index" %>

View File

@@ -1,6 +1,6 @@
<h2><%= t("admin.organizations.search.title") %></h2> <h2><%= t("admin.organizations.search.title") %></h2>
<%= render Admin::SearchComponent.new(label: t("admin.organizations.index.search_placeholder")) %> <%= render Admin::SearchComponent.new(label: t("admin.shared.search.label.organizations")) %>
<div id="search-results"> <div id="search-results">
<% if @organizations.any? %> <% if @organizations.any? %>

View File

@@ -3,7 +3,7 @@
<div class="input-group"> <div class="input-group">
<%= text_field_tag :search, <%= text_field_tag :search,
@search, @search,
placeholder: t("admin.shared.booths_search.placeholder"), id: "search-booths" %> placeholder: t("admin.shared.search.label.booths"), id: "search-booths" %>
<div class="input-group-button"> <div class="input-group-button">
<%= submit_tag t("admin.shared.search.search"), class: "button" %> <%= submit_tag t("admin.shared.search.search"), class: "button" %>
</div> </div>

View File

@@ -4,7 +4,7 @@
<%= link_to t("admin.booths.index.add_booth"), new_admin_booth_path, class: "button float-right" %> <%= link_to t("admin.booths.index.add_booth"), new_admin_booth_path, class: "button float-right" %>
<% end %> <% 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? %> <% if @booths.empty? %>
<div class="callout primary"> <div class="callout primary">

View File

@@ -3,7 +3,7 @@
<div class="input-group"> <div class="input-group">
<%= text_field_tag :search, <%= text_field_tag :search,
@search, @search,
placeholder: t("admin.shared.poll_officers_search.placeholder"), id: "search-officers" %> placeholder: t("admin.shared.search.label.poll_officers"), id: "search-officers" %>
<div class="input-group-button"> <div class="input-group-button">
<%= submit_tag t("admin.shared.search.search"), class: "button" %> <%= submit_tag t("admin.shared.search.search"), class: "button" %>
</div> </div>

View File

@@ -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")) %>

View File

@@ -5,7 +5,7 @@
<h2><%= t("admin.proposals.index.title") %></h2> <h2><%= t("admin.proposals.index.title") %></h2>
<% if @proposals.any? %> <% 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")) %>
<h3><%= page_entries_info @proposals %></h3> <h3><%= page_entries_info @proposals %></h3>

View File

@@ -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")) %>

View File

@@ -1,7 +1,7 @@
<main> <main>
<h2><%= t("management.proposals.index.title") %></h2> <h2><%= t("management.proposals.index.title") %></h2>
<%= render Admin::SearchComponent.new(label: t("admin.shared.proposal_search.placeholder")) %> <%= render Admin::SearchComponent.new(label: t("admin.shared.search.label.proposals")) %>
<div class="management-list"> <div class="management-list">
<div class="proposals-list"> <div class="proposals-list">

View File

@@ -0,0 +1 @@
<%= render SDGManagement::Relations::EditComponent.new(@record) %>

View File

@@ -0,0 +1 @@
<%= render SDGManagement::Relations::IndexComponent.new(@records) %>

View File

@@ -2,6 +2,7 @@ en:
attributes: attributes:
geozone_id: "Scope of operation" geozone_id: "Scope of operation"
results_enabled: "Show results" results_enabled: "Show results"
sdg_target_list: "Targets"
stats_enabled: "Show stats" stats_enabled: "Show stats"
advanced_stats_enabled: "Show advanced stats" advanced_stats_enabled: "Show advanced stats"
name: Name name: Name
@@ -287,6 +288,7 @@ en:
responsible_name: "Person responsible for the group" responsible_name: "Person responsible for the group"
poll: poll:
name: "Name" name: "Name"
title: "Name"
starts_at: "Start Date" starts_at: "Start Date"
ends_at: "Closing Date" ends_at: "Closing Date"
geozone_restricted: "Restricted by geozone" geozone_restricted: "Restricted by geozone"

View File

@@ -1169,7 +1169,6 @@ en:
no_organizations: There are no organizations. no_organizations: There are no organizations.
reject: Reject reject: Reject
rejected: Rejected rejected: Rejected
search_placeholder: Name, email or phone number
title: Organisations title: Organisations
verified: Verified verified: Verified
verify: Verify verify: Verify
@@ -1254,19 +1253,26 @@ en:
true_value: "Yes" true_value: "Yes"
false_value: "No" false_value: "No"
search: 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"
local_census_records: "Search by document number"
organizations: "Name, email or phone number"
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" 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" search_results: "Search results"
no_search_results: "No results found." no_search_results: "No results found."
actions: Actions actions: Actions
@@ -1572,8 +1578,6 @@ en:
document_number: Document number document_number: Document number
date_of_birth: Date of birth date_of_birth: Date of birth
postal_code: Postal code postal_code: Postal code
search:
placeholder: Search by document number
import: Import CSV import: Import CSV
new: new:
creating: Creating new local census record creating: Creating new local census record

View File

@@ -1,8 +1,15 @@
en: en:
sdg_management: sdg_management:
actions:
edit: "Manage goals and targets"
header: header:
title: "SDG content" title: "SDG content"
menu: menu:
budget_investments: "Participatory budgets"
debates: "Debates"
legislation_processes: "Collaborative legislation"
polls: "Polls"
proposals: "Proposals"
sdg_content: "Goals and Targets" sdg_content: "Goals and Targets"
local_targets: local_targets:
create: create:

View File

@@ -2,6 +2,7 @@ es:
attributes: attributes:
geozone_id: "Ámbito de actuación" geozone_id: "Ámbito de actuación"
results_enabled: "Mostrar resultados" results_enabled: "Mostrar resultados"
sdg_target_list: "Metas"
stats_enabled: "Mostrar estadísticas" stats_enabled: "Mostrar estadísticas"
advanced_stats_enabled: "Mostrar estadísticas avanzadas" advanced_stats_enabled: "Mostrar estadísticas avanzadas"
name: Nombre name: Nombre
@@ -287,6 +288,7 @@ es:
responsible_name: "Persona responsable del colectivo" responsible_name: "Persona responsable del colectivo"
poll: poll:
name: "Nombre" name: "Nombre"
title: "Nombre"
starts_at: "Fecha de apertura" starts_at: "Fecha de apertura"
ends_at: "Fecha de cierre" ends_at: "Fecha de cierre"
geozone_restricted: "Restringida por zonas" geozone_restricted: "Restringida por zonas"

View File

@@ -1168,7 +1168,6 @@ es:
no_organizations: No hay organizaciones. no_organizations: No hay organizaciones.
reject: Rechazar reject: Rechazar
rejected: Rechazada rejected: Rechazada
search_placeholder: Nombre, email o teléfono
title: Organizaciones title: Organizaciones
verified: Verificada verified: Verificada
verify: Verificar verify: Verificar
@@ -1253,19 +1252,26 @@ es:
true_value: "Sí" true_value: "Sí"
false_value: "No" false_value: "No"
search: 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"
local_census_records: "Búsqueda por número de documento"
organizations: "Nombre, email o teléfono"
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" 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" search_results: "Resultados de la búsqueda"
no_search_results: "No se han encontrado resultados." no_search_results: "No se han encontrado resultados."
actions: Acciones actions: Acciones
@@ -1571,8 +1577,6 @@ es:
document_number: Número de documento document_number: Número de documento
date_of_birth: Fecha de nacimiento date_of_birth: Fecha de nacimiento
postal_code: Código postal postal_code: Código postal
search:
placeholder: Búsqueda por número de documento
import: Importar CSV import: Importar CSV
new: new:
creating: Creando nuevo registro de censo local creating: Creando nuevo registro de censo local

View File

@@ -1,8 +1,15 @@
es: es:
sdg_management: sdg_management:
actions:
edit: "Asignar objetivos y metas"
header: header:
title: "Contenido ODS" title: "Contenido ODS"
menu: menu:
budget_investments: "Presupuestos participativos"
debates: "Debates"
legislation_processes: "Legislación colaborativa"
polls: "Votaciones"
proposals: "Propuestas"
sdg_content: "Objetivos y Metas" sdg_content: "Objetivos y Metas"
local_targets: local_targets:
create: create:

View File

@@ -4,4 +4,16 @@ namespace :sdg_management do
resources :goals, only: [:index] resources :goals, only: [:index]
resources :targets, only: [:index] resources :targets, only: [:index]
resources :local_targets, except: [:show] 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 end

View File

@@ -0,0 +1,5 @@
class AddTsvToPolls < ActiveRecord::Migration[5.2]
def change
add_column :polls, :tsv, :tsvector
end
end

View File

@@ -0,0 +1,5 @@
class AddTsvToLegislationProcesses < ActiveRecord::Migration[5.2]
def change
add_column :legislation_processes, :tsv, :tsvector
end
end

View File

@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "pg_trgm" enable_extension "pg_trgm"
@@ -747,6 +747,7 @@ ActiveRecord::Schema.define(version: 2020_11_24_145559) do
t.boolean "homepage_enabled", default: false t.boolean "homepage_enabled", default: false
t.text "background_color" t.text "background_color"
t.text "font_color" t.text "font_color"
t.tsvector "tsv"
t.index ["allegations_end_date"], name: "index_legislation_processes_on_allegations_end_date" 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 ["allegations_start_date"], name: "index_legislation_processes_on_allegations_start_date"
t.index ["debate_end_date"], name: "index_legislation_processes_on_debate_end_date" t.index ["debate_end_date"], name: "index_legislation_processes_on_debate_end_date"
@@ -1166,6 +1167,7 @@ ActiveRecord::Schema.define(version: 2020_11_24_145559) do
t.integer "budget_id" t.integer "budget_id"
t.string "related_type" t.string "related_type"
t.integer "related_id" t.integer "related_id"
t.tsvector "tsv"
t.index ["budget_id"], name: "index_polls_on_budget_id", unique: true 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 ["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" t.index ["starts_at", "ends_at"], name: "index_polls_on_starts_at_and_ends_at"

View File

@@ -6,6 +6,7 @@ namespace :consul do
desc "Runs tasks needed to upgrade from 1.2.0 to 1.3.0" desc "Runs tasks needed to upgrade from 1.2.0 to 1.3.0"
task "execute_release_1.3.0_tasks": [ task "execute_release_1.3.0_tasks": [
"db:load_sdg" "db:load_sdg",
"db:calculate_tsv"
] ]
end end

View File

@@ -10,4 +10,10 @@ namespace :db do
ApplicationLogger.new.info "Adding Sustainable Development Goals content" ApplicationLogger.new.info "Adding Sustainable Development Goals content"
load(Rails.root.join("db", "sdg.rb")) load(Rails.root.join("db", "sdg.rb"))
end 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 end

View File

@@ -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

View File

@@ -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

View File

@@ -32,3 +32,33 @@ describe "rake db:load_sdg" do
expect(SDG::Target.last.id).to eq target_id expect(SDG::Target.last.id).to eq target_id
end end
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

View File

@@ -229,4 +229,35 @@ describe Legislation::Process do
end end
end 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 end

View File

@@ -433,4 +433,29 @@ describe Poll do
expect(poll.recounts_confirmed?).to be true expect(poll.recounts_confirmed?).to be true
end end
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 end

View File

@@ -28,6 +28,14 @@ describe SDG::Relatable do
end end
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 describe "#sdg_targets" do
it "can assign targets to a model" do it "can assign targets to a model" do
relatable.sdg_targets = [target, another_target] relatable.sdg_targets = [target, another_target]
@@ -46,6 +54,14 @@ describe SDG::Relatable do
end end
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 describe "#sdg_local_targets" do
it "can assign local targets to a model" do it "can assign local targets to a model" do
relatable.sdg_local_targets = [local_target, another_local_target] relatable.sdg_local_targets = [local_target, another_local_target]
@@ -74,4 +90,72 @@ describe SDG::Relatable do
expect(relatable.reload.related_sdgs).to match_array related_sdgs expect(relatable.reload.related_sdgs).to match_array related_sdgs
end end
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 end

View File

@@ -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

View File

@@ -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