diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 44258bc7a..0d5b962c1 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -33,6 +33,7 @@ //= require moderator_comment //= require moderator_debates //= require moderator_proposals +//= require moderator_budget_investments //= require moderator_proposal_notifications //= require prevent_double_submission //= require gettext diff --git a/app/assets/javascripts/moderator_budget_investments.js.coffee b/app/assets/javascripts/moderator_budget_investments.js.coffee new file mode 100644 index 000000000..612a058a4 --- /dev/null +++ b/app/assets/javascripts/moderator_budget_investments.js.coffee @@ -0,0 +1,8 @@ +App.ModeratorBudgetInvestments = + + add_class_faded: (id) -> + $("##{id}").addClass("faded") + $("#comments").addClass("faded") + + hide_moderator_actions: (id) -> + $("##{id} .js-moderator-investment-actions:first").hide() diff --git a/app/controllers/admin/hidden_budget_investments_controller.rb b/app/controllers/admin/hidden_budget_investments_controller.rb new file mode 100644 index 000000000..439f3c722 --- /dev/null +++ b/app/controllers/admin/hidden_budget_investments_controller.rb @@ -0,0 +1,34 @@ +class Admin::HiddenBudgetInvestmentsController < Admin::BaseController + include FeatureFlags + + has_filters %w{all with_confirmed_hide without_confirmed_hide}, only: :index + + feature_flag :budgets + + before_action :load_investment, only: [:confirm_hide, :restore] + + def index + @investments = Budget::Investment.only_hidden.send(@current_filter) + .order(hidden_at: :desc) + .page(params[:page]) + end + + def confirm_hide + @investment.confirm_hide + redirect_to request.query_parameters.merge(action: :index) + end + + def restore + @investment.restore + @investment.ignore_flag + Activity.log(current_user, :restore, @investment) + redirect_to request.query_parameters.merge(action: :index) + end + + private + + def load_investment + @investment = Budget::Investment.with_hidden.find(params[:id]) + end + +end diff --git a/app/controllers/concerns/flag_actions.rb b/app/controllers/concerns/flag_actions.rb index 3f5d0b2c7..da637407a 100644 --- a/app/controllers/concerns/flag_actions.rb +++ b/app/controllers/concerns/flag_actions.rb @@ -3,18 +3,32 @@ module FlagActions def flag Flag.flag(current_user, flaggable) - respond_with flaggable, template: "#{controller_name}/_refresh_flag_actions" + + if controller_name == 'investments' + respond_with flaggable, template: "budgets/#{controller_name}/_refresh_flag_actions" + else + respond_with flaggable, template: "#{controller_name}/_refresh_flag_actions" + end end def unflag Flag.unflag(current_user, flaggable) - respond_with flaggable, template: "#{controller_name}/_refresh_flag_actions" + + if controller_name == 'investments' + respond_with flaggable, template: "budgets/#{controller_name}/_refresh_flag_actions" + else + respond_with flaggable, template: "#{controller_name}/_refresh_flag_actions" + end end private def flaggable - instance_variable_get("@#{resource_model.to_s.downcase}") + if resource_model.to_s == 'Budget::Investment' + instance_variable_get("@investment") + else + instance_variable_get("@#{resource_model.to_s.downcase}") + end end -end \ No newline at end of file +end diff --git a/app/controllers/concerns/polymorphic.rb b/app/controllers/concerns/polymorphic.rb index 8fd4ab312..0e25b4231 100644 --- a/app/controllers/concerns/polymorphic.rb +++ b/app/controllers/concerns/polymorphic.rb @@ -3,7 +3,11 @@ module Polymorphic private def resource - @resource ||= instance_variable_get("@#{resource_name}") + if resource_model.to_s == 'Budget::Investment' + @resource ||= instance_variable_get("@investment") + else + @resource ||= instance_variable_get("@#{resource_name}") + end end def resource_name diff --git a/app/controllers/moderation/budgets/investments_controller.rb b/app/controllers/moderation/budgets/investments_controller.rb new file mode 100644 index 000000000..7f33058e8 --- /dev/null +++ b/app/controllers/moderation/budgets/investments_controller.rb @@ -0,0 +1,24 @@ +class Moderation::Budgets::InvestmentsController < Moderation::BaseController + include FeatureFlags + include ModerateActions + + has_filters %w{pending_flag_review all with_ignored_flag}, only: :index + has_orders %w{flags created_at}, only: :index + + feature_flag :budgets + + before_action :load_resources, only: [:index, :moderate] + + load_and_authorize_resource class: 'Budget::Investment' + + private + + def resource_name + 'budget_investment' + end + + def resource_model + Budget::Investment + end + +end diff --git a/app/controllers/stats_controller.rb b/app/controllers/stats_controller.rb index 7130e661d..7bb872257 100644 --- a/app/controllers/stats_controller.rb +++ b/app/controllers/stats_controller.rb @@ -14,6 +14,7 @@ class StatsController < ApplicationController @debate_votes = daily_cache('debate_votes') { Vote.where(votable_type: 'Debate').count } @proposal_votes = daily_cache('proposal_votes') { Vote.where(votable_type: 'Proposal').count } @comment_votes = daily_cache('comment_votes') { Vote.where(votable_type: 'Comment').count } + @investment_votes = daily_cache('budget_investment_votes') { Vote.where(votable_type: 'Budget::Investment').count } @votes = daily_cache('votes') { Vote.count } @verified_users = daily_cache('verified_users') { User.with_hidden.level_two_or_three_verified.count } diff --git a/app/helpers/admin_helper.rb b/app/helpers/admin_helper.rb index 2c1a32c70..8a32e5630 100644 --- a/app/helpers/admin_helper.rb +++ b/app/helpers/admin_helper.rb @@ -1,19 +1,31 @@ module AdminHelper def side_menu - render "/#{namespace}/menu" + if namespace == 'moderation/budgets' + render "/moderation/menu" + else + render "/#{namespace}/menu" + end end def namespaced_root_path - "/#{namespace}" + if namespace == 'moderation/budgets' + "/moderation" + else + "/#{namespace}" + end end def namespaced_header_title - t("#{namespace}.header.title") + if namespace == 'moderation/budgets' + t("moderation.header.title") + else + t("#{namespace}.header.title") + end end def menu_moderated_content? - ["proposals", "debates", "comments", "hidden_users", "activity"].include?(controller_name) && controller.class.parent != Admin::Legislation + ["proposals", "debates", "comments", "hidden_users", "activity", "hidden_budget_investments"].include?(controller_name) && controller.class.parent != Admin::Legislation end def menu_budget? diff --git a/app/models/abilities/administrator.rb b/app/models/abilities/administrator.rb index e8bb005d4..40e951326 100644 --- a/app/models/abilities/administrator.rb +++ b/app/models/abilities/administrator.rb @@ -17,6 +17,9 @@ module Abilities can :restore, Legislation::Proposal cannot :restore, Legislation::Proposal, hidden_at: nil + can :restore, Budget::Investment + cannot :restore, Budget::Investment, hidden_at: nil + can :restore, User cannot :restore, User, hidden_at: nil @@ -32,6 +35,9 @@ module Abilities can :confirm_hide, Legislation::Proposal cannot :confirm_hide, Legislation::Proposal, hidden_at: nil + can :confirm_hide, Budget::Investment + cannot :confirm_hide, Budget::Investment, hidden_at: nil + can :confirm_hide, User cannot :confirm_hide, User, hidden_at: nil diff --git a/app/models/abilities/common.rb b/app/models/abilities/common.rb index d8d716ec2..7c84089b4 100644 --- a/app/models/abilities/common.rb +++ b/app/models/abilities/common.rb @@ -46,6 +46,9 @@ module Abilities can [:flag, :unflag], Legislation::Proposal cannot [:flag, :unflag], Legislation::Proposal, author_id: user.id + can [:flag, :unflag], Budget::Investment + cannot [:flag, :unflag], Budget::Investment, author_id: user.id + can [:create, :destroy], Follow can [:destroy], Document, documentable: { author_id: user.id } diff --git a/app/models/abilities/moderation.rb b/app/models/abilities/moderation.rb index 801e752ed..86f44f25e 100644 --- a/app/models/abilities/moderation.rb +++ b/app/models/abilities/moderation.rb @@ -63,6 +63,15 @@ module Abilities cannot :moderate, ProposalNotification, author_id: user.id can :index, ProposalNotification + + can :hide, Budget::Investment, hidden_at: nil + cannot :hide, Budget::Investment, author_id: user.id + + can :ignore_flag, Budget::Investment, ignored_flag_at: nil, hidden_at: nil + cannot :ignore_flag, Budget::Investment, author_id: user.id + + can :moderate, Budget::Investment + cannot :moderate, Budget::Investment, author_id: user.id end end end diff --git a/app/models/budget/investment.rb b/app/models/budget/investment.rb index d69a0d64a..e531612a8 100644 --- a/app/models/budget/investment.rb +++ b/app/models/budget/investment.rb @@ -23,6 +23,7 @@ class Budget include Relationable include Notifiable include Filterable + include Flaggable belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' belongs_to :heading @@ -81,6 +82,8 @@ class Budget scope :winners, -> { selected.compatible.where(winner: true) } scope :unselected, -> { not_unfeasible.where(selected: false) } scope :last_week, -> { where("created_at >= ?", 7.days.ago)} + scope :sort_by_flags, -> { order(flags_count: :desc, updated_at: :desc) } + scope :sort_by_created_at, -> { reorder(created_at: :desc) } scope :by_budget, ->(budget) { where(budget: budget) } scope :by_group, ->(group_id) { where(group_id: group_id) } diff --git a/app/models/user.rb b/app/models/user.rb index 67fe5d104..4b999207d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -182,6 +182,7 @@ class User < ActiveRecord::Base debates_ids = Debate.where(author_id: id).pluck(:id) comments_ids = Comment.where(user_id: id).pluck(:id) proposal_ids = Proposal.where(author_id: id).pluck(:id) + investment_ids = Budget::Investment.where(author_id: id).pluck(:id) proposal_notification_ids = ProposalNotification.where(author_id: id).pluck(:id) hide @@ -189,6 +190,7 @@ class User < ActiveRecord::Base Debate.hide_all debates_ids Comment.hide_all comments_ids Proposal.hide_all proposal_ids + Budget::Investment.hide_all investment_ids ProposalNotification.hide_all proposal_notification_ids end diff --git a/app/views/admin/_menu.html.erb b/app/views/admin/_menu.html.erb index 78ce6b351..8385b8742 100644 --- a/app/views/admin/_menu.html.erb +++ b/app/views/admin/_menu.html.erb @@ -151,6 +151,12 @@ <% end %> + <% if feature?(:budgets) %> +
  • > + <%= link_to t("admin.menu.hidden_budget_investments"), admin_hidden_budget_investments_path %> +
  • + <% end %> +
  • > <%= link_to t("admin.menu.hidden_comments"), admin_comments_path %>
  • diff --git a/app/views/admin/hidden_budget_investments/index.html.erb b/app/views/admin/hidden_budget_investments/index.html.erb new file mode 100644 index 000000000..68f5f6ce6 --- /dev/null +++ b/app/views/admin/hidden_budget_investments/index.html.erb @@ -0,0 +1,49 @@ +

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

    +

    <%= t("admin.shared.moderated_content") %>

    + +<%= render 'shared/filter_subnav', i18n_namespace: "admin.hidden_budget_investments.index" %> + +<% if @investments.any? %> +

    <%= page_entries_info @investments %>

    + + + + + + + + + <% @investments.each do |investment| %> + + + + + + <% end %> + +
    <%= t("admin.shared.title") %><%= t("admin.shared.description") %><%= t("admin.shared.actions") %>
    + <%= investment.title %> + +
    + <%= investment.description %> +
    +
    + <%= link_to t("admin.actions.restore"), + restore_admin_hidden_budget_investment_path(investment, request.query_parameters), + method: :put, + data: { confirm: t("admin.actions.confirm") }, + class: "button hollow warning" %> + <% unless investment.confirmed_hide? %> + <%= link_to t("admin.actions.confirm_hide"), + confirm_hide_admin_hidden_budget_investment_path(investment, request.query_parameters), + method: :put, + class: "button" %> + <% end %> +
    + + <%= paginate @investments %> +<% else %> +
    + <%= t("admin.hidden_budget_investments.index.no_hidden_budget_investments") %> +
    +<% end %> diff --git a/app/views/budgets/investments/_actions.html.erb b/app/views/budgets/investments/_actions.html.erb new file mode 100644 index 000000000..a464d7495 --- /dev/null +++ b/app/views/budgets/investments/_actions.html.erb @@ -0,0 +1,10 @@ +<% if can? :hide, investment %> + <%= link_to t("admin.actions.hide").capitalize, hide_moderation_budget_investment_path(investment), + method: :put, remote: true, data: { confirm: t("admin.actions.confirm") } %> +<% end %> + +<% if can? :hide, investment.author %> +  |  + <%= link_to t("admin.actions.hide_author").capitalize, hide_moderation_user_path(investment.author_id), + method: :put, data: { confirm: t("admin.actions.confirm") } %> +<% end %> diff --git a/app/views/budgets/investments/_flag_actions.html.erb b/app/views/budgets/investments/_flag_actions.html.erb new file mode 100644 index 000000000..8627dfe94 --- /dev/null +++ b/app/views/budgets/investments/_flag_actions.html.erb @@ -0,0 +1,25 @@ + + <% if show_flag_action? investment %> + + + + + <%= link_to t('shared.flag'), flag_budget_investment_path(investment.budget, investment.id), + method: :put, + remote: true, + id: "flag-investment-#{ investment.id }" %> + + <% end %> + + <% if show_unflag_action? investment %> + + + + + <%= link_to t('shared.unflag'), unflag_budget_investment_path(investment.budget, investment.id), + method: :put, + remote: true, + id: "unflag-investment-#{ investment.id }" %> + + <% end %> + diff --git a/app/views/budgets/investments/_investment_show.html.erb b/app/views/budgets/investments/_investment_show.html.erb index acca21a80..2f3528653 100644 --- a/app/views/budgets/investments/_investment_show.html.erb +++ b/app/views/budgets/investments/_investment_show.html.erb @@ -7,180 +7,195 @@ og_image_url: (investment.image.present? ? investment.image_url(:thumb) : nil) %> <% end %> -
    +<% cache [locale_and_user_status(investment), + investment, + investment.author, + Flag.flagged?(current_user, investment), + @investment_votes] do %> +
    -
    -
    - <%= back_link_to budget_investments_path(investment.budget, heading_id: investment.heading) %> +
    +
    + <%= back_link_to budget_investments_path(investment.budget, heading_id: investment.heading) %> -

    <%= investment.title %>

    -
    - <%= render '/shared/author_info', resource: investment %> +

    <%= investment.title %>

    +
    + <%= render '/shared/author_info', resource: investment %> -  •  - <%= l investment.created_at.to_date %> -  •  - <%= investment.heading.name %> +  •  + <%= l investment.created_at.to_date %> +  •  + <%= investment.heading.name %> +  •  + + <%= render 'budgets/investments/flag_actions', investment: @investment %> + +
    + +
    + + <%= render_image(investment.image, :large, true) if investment.image.present? %> + +

    + <%= t("budgets.investments.show.code_html", code: investment.id) %> +

    + + <%= safe_html_with_links investment.description.html_safe %> + + <% if feature?(:map) && map_location_available?(@investment.map_location) %> +
    + <%= render_map(@investment.map_location, "budget_investment", false, nil) %> +
    + <% end %> + + <% if investment.location.present? %> +

    + <%= t("budgets.investments.show.location_html", location: investment.location) %> +

    + <% end %> + + <% if investment.organization_name.present? %> +

    + <%= t("budgets.investments.show.organization_name_html", name: investment.organization_name) %> +

    + <% end %> + + <% if feature?(:allow_attached_documents) %> + <%= render 'documents/documents', + documents: investment.documents, + max_documents_allowed: Budget::Investment.max_documents_allowed %> + <% end %> + + <%= render 'shared/tags', taggable: investment %> + + <% if investment.external_url.present? %> + + <% end %> + + <% if investment.should_show_unfeasibility_explanation? %> +

    <%= t('budgets.investments.show.unfeasibility_explanation') %>

    +

    <%= investment.unfeasibility_explanation %>

    + <% end %> + + <% if investment.should_show_price_explanation? %> +

    <%= t('budgets.investments.show.price_explanation') %>

    +

    <%= investment.price_explanation %>

    + <% end %> + + <%= render 'relationable/related_content', relationable: @investment %> + +
    + <%= render "budgets/investments/actions", investment: @investment %> +
    -
    +
    - - -
    -
    + + +
    +<% end %> diff --git a/app/views/budgets/investments/_refresh_flag_actions.js.erb b/app/views/budgets/investments/_refresh_flag_actions.js.erb new file mode 100644 index 000000000..07cf8d30a --- /dev/null +++ b/app/views/budgets/investments/_refresh_flag_actions.js.erb @@ -0,0 +1 @@ +App.Flaggable.update("<%= dom_id(@investment) %>", "<%= j render("budgets/investments/flag_actions", investment: @investment) %>") diff --git a/app/views/moderation/_menu.html.erb b/app/views/moderation/_menu.html.erb index 8d7abb85d..0a34adb8b 100644 --- a/app/views/moderation/_menu.html.erb +++ b/app/views/moderation/_menu.html.erb @@ -29,6 +29,15 @@ <% end %> + <% if feature?(:budgets) %> +
  • > + <%= link_to moderation_budget_investments_path do %> + + <%= t("moderation.menu.flagged_investments") %> + <% end %> +
  • + <% end %> +
  • > <%= link_to moderation_comments_path do %> diff --git a/app/views/moderation/budgets/investments/hide.js.erb b/app/views/moderation/budgets/investments/hide.js.erb new file mode 100644 index 000000000..9365b3837 --- /dev/null +++ b/app/views/moderation/budgets/investments/hide.js.erb @@ -0,0 +1,3 @@ +var investment_id = '<%= dom_id(@investment) %>' +App.ModeratorBudgetInvestments.add_class_faded(investment_id) +App.ModeratorBudgetInvestments.hide_moderator_actions(investment_id) diff --git a/app/views/moderation/budgets/investments/index.html.erb b/app/views/moderation/budgets/investments/index.html.erb new file mode 100644 index 000000000..d9c636d63 --- /dev/null +++ b/app/views/moderation/budgets/investments/index.html.erb @@ -0,0 +1,73 @@ +

    <%= t("moderation.budget_investments.index.title") %>

    + +<%= render "shared/filter_subnav", i18n_namespace: "moderation.budget_investments.index" %> + +

    <%= page_entries_info @budget_investments %>

    +
    + <%= t("moderation.budget_investments.index.order") %> + <%= render "shared/order_selector", i18n_namespace: "moderation.budget_investments.index" %> +
    + +<%= form_tag moderate_moderation_budget_investments_path(request.query_parameters), method: :put do %> +

    + <%= t("shared.check") %>: + <%= link_to t("shared.check_all"), "#", data: { check_all: "budget_investment_ids[]" } %> + | + <%= link_to t("shared.check_none"), "#", data: { check_none: "budget_investment_ids[]" } %> +

    + + + + + + + + + + + <% @budget_investments.each do |investment| %> + + + + + <% end %> + +
    <%= t("moderation.budget_investments.index.headers.budget_investment") %><%= t("moderation.budget_investments.index.headers.moderate") %>
    + <%= link_to investment.title, admin_budget_budget_investment_path( + budget_id: investment.budget_id, + id: investment.id + ), target: "_blank" %> +
    + <%= l investment.updated_at.to_date %> +  •  + <%= investment.flags_count %> +  •  + <%= investment.author.username %> +
    +
    + <%= investment.description %> +
    +
    + <%= check_box_tag "budget_investment_ids[]", investment.id, nil, id: "#{dom_id(investment)}_check" %> +
    + + <%= submit_tag t("moderation.budget_investments.index.block_authors"), + name: "block_authors", + class: "button hollow alert", + data: { confirm: t("moderation.budget_investments.index.confirm") } %> + +
    + <%= submit_tag t("moderation.budget_investments.index.hide_budget_investments"), + name: "hide_budget_investments", + class: "button hollow alert", + data: { confirm: t("moderation.budget_investments.index.confirm") } %> + + <%= submit_tag t("moderation.budget_investments.index.ignore_flags"), + name: "ignore_flags", + class: "button hollow", + data: { confirm: t("moderation.budget_investments.index.confirm") } %> +
    + + <%= paginate @budget_investments %> + +<% end %> diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index a79e521bf..a0fb7110e 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -135,6 +135,7 @@ ignore_unused: - 'admin.spending_proposals.index.filter*' - 'admin.organizations.index.filter*' - 'admin.hidden_users.index.filter*' + - 'admin.hidden_budget_investments.index.filter*' - 'admin.activity.show.filter*' - 'admin.legislation.processes.index.filter*' - 'admin.legislation.processes.*.submit_button' @@ -152,6 +153,8 @@ ignore_unused: - 'moderation.proposals.index.order*' - 'moderation.debates.index.filter*' - 'moderation.debates.index.order*' + - 'moderation.budget_investments.index.filter*' + - 'moderation.budget_investments.index.order*' - 'moderation.proposal_notifications.index.filter*' - 'moderation.proposal_notifications.index.order*' - 'valuation.spending_proposals.index.filter*' diff --git a/config/locales/en/admin.yml b/config/locales/en/admin.yml index 259ea7c44..f2154da67 100644 --- a/config/locales/en/admin.yml +++ b/config/locales/en/admin.yml @@ -345,6 +345,15 @@ en: hidden_at: 'Hidden at:' registered_at: 'Registered at:' title: Activity of user (%{user}) + hidden_budget_investments: + index: + filter: Filter + filters: + all: All + with_confirmed_hide: Confirmed + without_confirmed_hide: Pending + title: Hidden budgets investments + no_hidden_budget_investments: There are no hidden budget investments legislation: processes: create: @@ -514,6 +523,7 @@ en: hidden_comments: Hidden comments hidden_debates: Hidden debates hidden_proposals: Hidden proposals + hidden_budget_investments: Hidden budget investments hidden_proposal_notifications: Hidden proposal notifications hidden_users: Hidden users administrators: Administrators diff --git a/config/locales/en/moderation.yml b/config/locales/en/moderation.yml index 5ed2f69bb..623ec0f92 100644 --- a/config/locales/en/moderation.yml +++ b/config/locales/en/moderation.yml @@ -46,6 +46,7 @@ en: menu: flagged_comments: Comments flagged_debates: Debates + flagged_investments: Budget investments proposals: Proposals proposal_notifications: Proposals notifications users: Block users @@ -68,6 +69,25 @@ en: created_at: Most recent flags: Most flagged title: Proposals + budget_investments: + index: + block_authors: Block authors + confirm: Are you sure? + filter: Filter + filters: + all: All + pending_flag_review: Pending + with_ignored_flag: Marked as viewed + headers: + moderate: Moderate + budget_investment: Budget investment + hide_budget_investments: Hide budget investments + ignore_flags: Mark as viewed + order: Order by + orders: + created_at: Most recent + flags: Most flagged + title: Budget investments proposal_notifications: index: block_authors: Block authors diff --git a/config/locales/es/admin.yml b/config/locales/es/admin.yml index 00cd907a3..0365f9b57 100644 --- a/config/locales/es/admin.yml +++ b/config/locales/es/admin.yml @@ -346,6 +346,15 @@ es: hidden_at: 'Bloqueado:' registered_at: 'Fecha de alta:' title: Actividad del usuario (%{user}) + hidden_budget_investments: + index: + filter: Filtro + filters: + all: Todos + with_confirmed_hide: Confirmados + without_confirmed_hide: Pendientes + title: Proyectos de gasto ocultos + no_hidden_budget_investments: No hay proyectos de gasto ocultos legislation: processes: create: @@ -515,6 +524,7 @@ es: hidden_comments: Comentarios ocultos hidden_debates: Debates ocultos hidden_proposals: Propuestas ocultas + hidden_budget_investments: Proyectos de gasto ocultos hidden_proposal_notifications: Notificationes de propuesta ocultas hidden_users: Usuarios bloqueados administrators: Administradores diff --git a/config/locales/es/moderation.yml b/config/locales/es/moderation.yml index c51013aee..a1caac77c 100644 --- a/config/locales/es/moderation.yml +++ b/config/locales/es/moderation.yml @@ -46,6 +46,7 @@ es: menu: flagged_comments: Comentarios flagged_debates: Debates + flagged_investments: Proyectos de gasto proposals: Propuestas proposal_notifications: Notificaciones de propuestas users: Bloquear usuarios @@ -68,6 +69,25 @@ es: created_at: Más recientes flags: Más denunciadas title: Propuestas + budget_investments: + index: + block_authors: Bloquear autores + confirm: '¿Estás seguro?' + filter: Filtro + filters: + all: Todos + pending_flag_review: Pendientes de revisión + with_ignored_flag: Marcadas como revisadas + headers: + moderate: Moderar + budget_investment: Proyecto de gasto + hide_budget_investments: Ocultar proyectos de gasto + ignore_flags: Marcar como revisadas + order: Ordenar por + orders: + created_at: Más recientes + flags: Más denunciadas + title: Proyectos de gasto proposal_notifications: index: block_authors: Bloquear autores diff --git a/config/routes/admin.rb b/config/routes/admin.rb index 31138e48c..ee6c0f050 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -15,6 +15,13 @@ namespace :admin do end end + resources :hidden_budget_investments, only: :index do + member do + put :restore + put :confirm_hide + end + end + resources :debates, only: :index do member do put :restore @@ -78,6 +85,7 @@ namespace :admin do end resources :tags, only: [:index, :create, :update, :destroy] + resources :officials, only: [:index, :edit, :update, :destroy] do get :search, on: :collection end @@ -93,6 +101,7 @@ namespace :admin do get :search, on: :collection get :summary, on: :collection end + resources :valuator_groups resources :managers, only: [:index, :create, :destroy] do diff --git a/config/routes/budget.rb b/config/routes/budget.rb index aef2c1413..b3422744f 100644 --- a/config/routes/budget.rb +++ b/config/routes/budget.rb @@ -1,7 +1,12 @@ resources :budgets, only: [:show, :index] do resources :groups, controller: "budgets/groups", only: [:show] resources :investments, controller: "budgets/investments", only: [:index, :new, :create, :show, :destroy] do - member { post :vote } + member do + post :vote + put :flag + put :unflag + end + collection { get :suggest } end diff --git a/config/routes/moderation.rb b/config/routes/moderation.rb index 90dc9be27..e6b0020cd 100644 --- a/config/routes/moderation.rb +++ b/config/routes/moderation.rb @@ -27,4 +27,9 @@ namespace :moderation do put :hide, on: :member put :moderate, on: :collection end + + resources :budget_investments, only: :index, controller: 'budgets/investments' do + put :hide, on: :member + put :moderate, on: :collection + end end diff --git a/db/migrate/20180613143922_add_moderation_attrs_to_investments.rb b/db/migrate/20180613143922_add_moderation_attrs_to_investments.rb new file mode 100644 index 000000000..ebb4fdf1a --- /dev/null +++ b/db/migrate/20180613143922_add_moderation_attrs_to_investments.rb @@ -0,0 +1,9 @@ +class AddModerationAttrsToInvestments < ActiveRecord::Migration + def change + change_table :budget_investments do |t| + t.datetime :confirmed_hide_at + t.datetime :ignored_flag_at + t.integer :flags_count, default: 0 + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 142990d93..b7ed9d5c3 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -207,6 +207,9 @@ ActiveRecord::Schema.define(version: 20180711224810) do t.integer "community_id" t.boolean "visible_to_valuators", default: false t.integer "valuator_group_assignments_count", default: 0 + t.datetime "confirmed_hide_at" + t.datetime "ignored_flag_at" + t.integer "flags_count", default: 0 end add_index "budget_investments", ["administrator_id"], name: "index_budget_investments_on_administrator_id", using: :btree diff --git a/spec/factories.rb b/spec/factories.rb index beb64aa2d..eadd26a9d 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -362,6 +362,24 @@ FactoryBot.define do feasibility "feasible" valuation_finished true end + + trait :hidden do + hidden_at Time.current + end + + trait :with_ignored_flag do + ignored_flag_at Time.current + end + + trait :flagged do + after :create do |investment| + Flag.flag(create(:user), investment) + end + end + + trait :with_confirmed_hide do + confirmed_hide_at Time.current + end end factory :budget_phase, class: 'Budget::Phase' do diff --git a/spec/features/admin/hidden_budget_investments_spec.rb b/spec/features/admin/hidden_budget_investments_spec.rb new file mode 100644 index 000000000..2b001830b --- /dev/null +++ b/spec/features/admin/hidden_budget_investments_spec.rb @@ -0,0 +1,108 @@ +require 'rails_helper' + +feature 'Admin hidden budget investments' do + + let(:budget) { create(:budget) } + let(:group) { create(:budget_group, name: 'Music', budget: budget) } + let(:heading) { create(:budget_heading, name: 'Black metal', price: 666666, group: group) } + + background do + admin = create(:administrator) + login_as(admin.user) + end + + scenario 'Disabled with a feature flag' do + Setting['feature.budgets'] = nil + + expect{ visit admin_hidden_budget_investments_path }.to raise_exception(FeatureFlags::FeatureDisabled) + + Setting['feature.budgets'] = true + end + + scenario 'List shows all relevant info' do + investment = create(:budget_investment, :hidden, heading: heading) + + visit admin_hidden_budget_investments_path + + expect(page).to have_content(investment.title) + expect(page).to have_content(investment.description) + end + + scenario 'Restore' do + investment = create(:budget_investment, :hidden, heading: heading) + + visit admin_hidden_budget_investments_path + + click_link 'Restore' + + expect(page).not_to have_content(investment.title) + + investment.reload + + expect(investment).to be_ignored_flag + end + + scenario 'Confirm hide' do + investment = create(:budget_investment, :hidden, heading: heading) + visit admin_hidden_budget_investments_path + + click_link('Pending') + expect(page).to have_content(investment.title) + + click_link 'Confirm moderation' + + expect(page).not_to have_content(investment.title) + + click_link('Confirmed') + expect(page).to have_content(investment.title) + + expect(investment.reload).to be_confirmed_hide + end + + scenario "Current filter is properly highlighted" do + visit admin_hidden_budget_investments_path + expect(page).not_to have_link('All') + expect(page).to have_link('Pending') + expect(page).to have_link('Confirmed') + + visit admin_hidden_budget_investments_path(filter: 'without_confirmed_hide') + expect(page).to have_link('All') + expect(page).to have_link('Confirmed') + expect(page).not_to have_link('Pending') + + visit admin_hidden_budget_investments_path(filter: 'with_confirmed_hide') + expect(page).to have_link('All') + expect(page).to have_link('Pending') + expect(page).not_to have_link('Confirmed') + end + + scenario 'Filtering investments' do + create(:budget_investment, :hidden, heading: heading, title: 'Unconfirmed investment') + create(:budget_investment, :hidden, :with_confirmed_hide, heading: heading, title: 'Confirmed investment') + + visit admin_hidden_budget_investments_path(filter: 'without_confirmed_hide') + expect(page).to have_content('Unconfirmed investment') + expect(page).not_to have_content('Confirmed investment') + + visit admin_hidden_budget_investments_path(filter: 'all') + expect(page).to have_content('Unconfirmed investment') + expect(page).to have_content('Confirmed investment') + + visit admin_hidden_budget_investments_path(filter: 'with_confirmed_hide') + expect(page).not_to have_content('Unconfirmed investment') + expect(page).to have_content('Confirmed investment') + end + + scenario "Action links remember the pagination setting and the filter" do + per_page = Kaminari.config.default_per_page + (per_page + 2).times { create(:budget_investment, :hidden, :with_confirmed_hide, heading: heading) } + + visit admin_hidden_budget_investments_path(filter: 'with_confirmed_hide', page: 2) + + click_on('Restore', match: :first, exact: true) + + expect(current_url).to include('filter=with_confirmed_hide') + expect(current_url).to include('page=2') + end + +end diff --git a/spec/features/budgets/investments_spec.rb b/spec/features/budgets/investments_spec.rb index ce97af1fc..6580d9806 100644 --- a/spec/features/budgets/investments_spec.rb +++ b/spec/features/budgets/investments_spec.rb @@ -1617,4 +1617,69 @@ feature 'Budget Investments' do end end + + scenario 'Flagging an investment as innapropriate', :js do + user = create(:user) + investment = create(:budget_investment, heading: heading) + + login_as(user) + + visit budget_investment_path(budget, investment) + + within "#budget_investment_#{investment.id}" do + find("#flag-expand-investment-#{investment.id}").click + find("#flag-investment-#{investment.id}").click + + expect(page).to have_css("#unflag-expand-investment-#{investment.id}") + end + + expect(Flag.flagged?(user, investment)).to be + end + + scenario 'Unflagging an investment', :js do + user = create(:user) + investment = create(:budget_investment, heading: heading) + Flag.flag(user, investment) + + login_as(user) + + visit budget_investment_path(budget, investment) + + within "#budget_investment_#{investment.id}" do + find("#unflag-expand-investment-#{investment.id}").click + find("#unflag-investment-#{investment.id}").click + + expect(page).to have_css("#flag-expand-investment-#{investment.id}") + end + + expect(Flag.flagged?(user, investment)).not_to be + end + + scenario 'Flagging an investment updates the DOM properly', :js do + user = create(:user) + investment = create(:budget_investment, heading: heading) + + login_as(user) + + visit budget_investment_path(budget, investment) + + within "#budget_investment_#{investment.id}" do + find("#flag-expand-investment-#{investment.id}").click + find("#flag-investment-#{investment.id}").click + + expect(page).to have_css("#unflag-expand-investment-#{investment.id}") + end + + expect(Flag.flagged?(user, investment)).to be + + within "#budget_investment_#{investment.id}" do + find("#unflag-expand-investment-#{investment.id}").click + find("#unflag-investment-#{investment.id}").click + + expect(page).to have_css("#flag-expand-investment-#{investment.id}") + end + + expect(Flag.flagged?(user, investment)).not_to be + end + end diff --git a/spec/features/moderation/budget_investments_spec.rb b/spec/features/moderation/budget_investments_spec.rb new file mode 100644 index 000000000..7cb4f5020 --- /dev/null +++ b/spec/features/moderation/budget_investments_spec.rb @@ -0,0 +1,246 @@ +require 'rails_helper' + +feature 'Moderate budget investments' do + + let(:budget) { create(:budget) } + let(:group) { create(:budget_group, name: 'Culture', budget: budget) } + let(:heading) { create(:budget_heading, name: 'More libraries', price: 666666, group: group) } + + background do + @mod = create(:moderator) + @investment = create(:budget_investment, heading: heading, author: create(:user)) + end + + scenario 'Disabled with a feature flag' do + Setting['feature.budgets'] = nil + login_as(@mod.user) + + expect{ visit moderation_budget_investments_path }.to raise_exception(FeatureFlags::FeatureDisabled) + + Setting['feature.budgets'] = true + end + + scenario 'Hiding an investment', :js do + login_as(@mod.user) + visit budget_investment_path(budget, @investment) + + accept_confirm { click_link 'Hide' } + + expect(page).to have_css('.faded', count: 2) + + visit budget_investments_path(budget.id, heading_id: heading.id) + + expect(page).not_to have_content(@investment.title) + end + + scenario "Hiding an investment's author", :js do + login_as(@mod.user) + visit budget_investment_path(budget, @investment) + + accept_confirm { click_link 'Hide author' } + + expect(page).to have_current_path(debates_path) + + visit budget_investments_path(budget.id, heading_id: heading.id) + + expect(page).not_to have_content(@investment.title) + end + + scenario 'Can not hide own investment' do + @investment.update(author: @mod.user) + login_as(@mod.user) + + visit budget_investment_path(budget, @investment) + + within "#budget_investment_#{@investment.id}" do + expect(page).not_to have_link('Hide') + expect(page).not_to have_link('Hide author') + end + end + + feature '/moderation/ screen' do + + background do + login_as(@mod.user) + end + + feature 'moderate in bulk' do + feature 'When an investment has been selected for moderation' do + + background do + visit moderation_budget_investments_path + + within('.menu.simple') do + click_link 'All' + end + + within("#investment_#{@investment.id}") do + check "budget_investment_#{@investment.id}_check" + end + + expect(page).not_to have_css("investment#{@investment.id}") + end + + scenario 'Hide the investment' do + click_button 'Hide budget investments' + expect(page).not_to have_css("investment_#{@investment.id}") + + @investment.reload + + expect(@investment.author).not_to be_hidden + end + + scenario 'Block the author' do + click_button 'Block authors' + expect(page).not_to have_css("investment_#{@investment.id}") + + @investment.reload + + expect(@investment.author).to be_hidden + end + + scenario 'Ignore the investment' do + click_button 'Mark as viewed' + expect(page).not_to have_css("investment_#{@investment.id}") + + @investment.reload + + expect(@investment).to be_ignored_flag + expect(@investment).not_to be_hidden + expect(@investment.author).not_to be_hidden + end + end + + scenario 'select all/none', :js do + create_list(:budget_investment, 2, heading: heading, author: create(:user)) + + visit moderation_budget_investments_path + + within('.js-check') { click_on 'All' } + + expect(all('input[type=checkbox]')).to all(be_checked) + + within('.js-check') { click_on 'None' } + + all('input[type=checkbox]').each do |checkbox| + expect(checkbox).not_to be_checked + end + end + + scenario 'remembering page, filter and order' do + create_list(:budget_investment, 52, heading: heading, author: create(:user)) + + visit moderation_budget_investments_path(filter: 'all', page: '2', order: 'created_at') + + click_button 'Mark as viewed' + + expect(page).to have_selector('.js-order-selector[data-order="created_at"]') + + expect(current_url).to include('filter=all') + expect(current_url).to include('page=2') + expect(current_url).to include('order=created_at') + end + end + + scenario 'Current filter is properly highlighted' do + visit moderation_budget_investments_path + + expect(page).not_to have_link('Pending') + expect(page).to have_link('All') + expect(page).to have_link('Marked as viewed') + + visit moderation_budget_investments_path(filter: 'all') + + within('.menu.simple') do + expect(page).not_to have_link('All') + expect(page).to have_link('Pending') + expect(page).to have_link('Marked as viewed') + end + + visit moderation_budget_investments_path(filter: 'pending_flag_review') + + within('.menu.simple') do + expect(page).to have_link('All') + expect(page).not_to have_link('Pending') + expect(page).to have_link('Marked as viewed') + end + + visit moderation_budget_investments_path(filter: 'with_ignored_flag') + + within('.menu.simple') do + expect(page).to have_link('All') + expect(page).to have_link('Pending') + expect(page).not_to have_link('Marked as viewed') + end + end + + scenario 'Filtering investments' do + create(:budget_investment, heading: heading, title: 'Books investment') + create(:budget_investment, :flagged, heading: heading, title: 'Non-selected investment') + create(:budget_investment, :hidden, heading: heading, title: 'Hidden investment') + create(:budget_investment, :flagged, :with_ignored_flag, heading: heading, title: 'Ignored investment') + + visit moderation_budget_investments_path(filter: 'all') + + expect(page).to have_content('Books investment') + expect(page).to have_content('Non-selected investment') + expect(page).not_to have_content('Hidden investment') + expect(page).to have_content('Ignored investment') + + visit moderation_budget_investments_path(filter: 'pending_flag_review') + + expect(page).not_to have_content('Books investment') + expect(page).to have_content('Non-selected investment') + expect(page).not_to have_content('Hidden investment') + expect(page).not_to have_content('Ignored investment') + + visit moderation_budget_investments_path(filter: 'with_ignored_flag') + + expect(page).not_to have_content('Books investment') + expect(page).not_to have_content('Non-selected investment') + expect(page).not_to have_content('Hidden investment') + expect(page).to have_content('Ignored investment') + end + + scenario 'sorting investments' do + flagged_investment = create(:budget_investment, + heading: heading, + title: 'Flagged investment', + created_at: Time.current - 1.day, + flags_count: 5 + ) + + flagged_new_investment = create(:budget_investment, + heading: heading, + title: 'Flagged new investment', + created_at: Time.current - 12.hours, + flags_count: 3 + ) + + latest_investment = create(:budget_investment, + heading: heading, + title: 'Latest investment', + created_at: Time.current + ) + + visit moderation_budget_investments_path(order: 'created_at') + + expect(flagged_new_investment.title).to appear_before(flagged_investment.title) + + visit moderation_budget_investments_path(order: 'flags') + + expect(flagged_investment.title).to appear_before(flagged_new_investment.title) + + visit moderation_budget_investments_path(filter: 'all', order: 'created_at') + + expect(latest_investment.title).to appear_before(flagged_new_investment.title) + expect(flagged_new_investment.title).to appear_before(flagged_investment.title) + + visit moderation_budget_investments_path(filter: 'all', order: 'flags') + + expect(flagged_investment.title).to appear_before(flagged_new_investment.title) + expect(flagged_new_investment.title).to appear_before(latest_investment.title) + end + end + +end