diff --git a/app/controllers/proposals_dashboard_controller.rb b/app/controllers/proposals_dashboard_controller.rb index 775b4ff04..0e235664c 100644 --- a/app/controllers/proposals_dashboard_controller.rb +++ b/app/controllers/proposals_dashboard_controller.rb @@ -4,7 +4,7 @@ class ProposalsDashboardController < ApplicationController before_action :authenticate_user! - helper_method :proposal + helper_method :proposal, :proposed_actions respond_to :html layout 'proposals_dashboard' @@ -19,9 +19,23 @@ class ProposalsDashboardController < ApplicationController redirect_to proposal_dashboard_index_path(proposal), notice: t('proposals.notice.published') end + def execute + authorize! :dashboard, proposal + ProposalExecutedDashboardAction.create(proposal: proposal, proposal_dashboard_action: proposal_dashboard_action, executed_at: Time.now) + redirect_to proposal_dashboard_index_path(proposal.to_param) + end + private + def proposal_dashboard_action + @proposal_dashboard_action ||= ProposalDashboardAction.find(params[:id]) + end + def proposal @proposal ||= Proposal.find(params[:proposal_id]) end + + def proposed_actions + @proposed_actions ||= ProposalDashboardAction.proposed_actions.active_for(proposal) + end end diff --git a/app/models/proposal.rb b/app/models/proposal.rb index 0f0407ebb..488ffced4 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -31,6 +31,8 @@ class Proposal < ActiveRecord::Base belongs_to :geozone has_many :comments, as: :commentable, dependent: :destroy has_many :proposal_notifications, dependent: :destroy + has_many :proposal_executed_dashboard_actions, dependent: :destroy + has_many :proposal_dashboard_actions, through: :proposal_executed_dashboard_actions validates :title, presence: true validates :question, presence: true diff --git a/app/models/proposal_dashboard_action.rb b/app/models/proposal_dashboard_action.rb index 6dd581485..e438d9f76 100644 --- a/app/models/proposal_dashboard_action.rb +++ b/app/models/proposal_dashboard_action.rb @@ -4,6 +4,9 @@ class ProposalDashboardAction < ActiveRecord::Base acts_as_paranoid column: :hidden_at include ActsAsParanoidAliases + has_many :proposal_executed_dashboard_actions, dependent: :restrict_with_error + has_many :proposals, through: :proposal_executed_dashboard_actions + enum action_type: %i[proposed_action resource] validates :title, @@ -41,6 +44,17 @@ class ProposalDashboardAction < ActiveRecord::Base scope :active, -> { where(active: true) } scope :inactive, -> { where(active: false) } + scope :resources, -> { where(action_type: 'resource') } + scope :proposed_actions, -> { where(action_type: 'proposed_action') } + scope :active_for, ->(proposal) do + published_at = proposal.published_at || Date.today + + active + .where('required_supports <= ?', proposal.votes_for.size) + .where('day_offset <= ?', (Date.today - published_at).to_i) + end + + default_scope { order(order: :asc, title: :asc) } def request_to_administrators? request_to_administrators || false diff --git a/app/models/proposal_executed_dashboard_action.rb b/app/models/proposal_executed_dashboard_action.rb new file mode 100644 index 000000000..6d205ac90 --- /dev/null +++ b/app/models/proposal_executed_dashboard_action.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class ProposalExecutedDashboardAction < ActiveRecord::Base + belongs_to :proposal + belongs_to :proposal_dashboard_action + + validates :proposal, presence: true, uniqueness: { scope: :proposal_dashboard_action } + validates :proposal_dashboard_action, presence: true + validates :executed_at, presence: true +end diff --git a/app/views/proposals_dashboard/_proposed_action.html.erb b/app/views/proposals_dashboard/_proposed_action.html.erb new file mode 100644 index 000000000..237ad83bf --- /dev/null +++ b/app/views/proposals_dashboard/_proposed_action.html.erb @@ -0,0 +1,25 @@ + + + <% if action.link.blank? %> + + <%= action.title %> + + <% else %> + <%= link_to action.link, target: '_blank' do %> + + <%= action.title %> + + <% end %> + <% end %> + + + <% if action.proposals.where(id: proposal.id).any? %> + <%=l action.proposal_executed_dashboard_actions.find_by(proposal: proposal).executed_at, format: :short %> + <% else %> + <%= link_to t('.execute'), + execute_proposal_dashboard_path(proposal.to_param, action.to_param), + method: 'post', + data: { confirm: t('admin.actions.confirm') } %> + <% end %> + + diff --git a/app/views/proposals_dashboard/index.html.erb b/app/views/proposals_dashboard/index.html.erb index 932a90d0a..77439e6e1 100644 --- a/app/views/proposals_dashboard/index.html.erb +++ b/app/views/proposals_dashboard/index.html.erb @@ -41,13 +41,12 @@ <%= link_to t('.publish'), publish_proposal_dashboard_index_path(proposal), method: :patch, class: 'menu-entry' if can?(:publish, proposal) %> <% end %> - + <%= link_to t('.actions'), proposal_dashboard_index_path(proposal.to_param), class: 'menu-entry' %> + + + +
+ + + <% proposed_actions.each do |action| %> + <%= render partial: 'proposed_action', locals: { action: action } %> + <% end %> +
<%= t('.actions') %>
+
diff --git a/config/locales/en/general.yml b/config/locales/en/general.yml index 033bc0f05..216ad41cc 100644 --- a/config/locales/en/general.yml +++ b/config/locales/en/general.yml @@ -498,6 +498,10 @@ en: edit_proposal_link: Edit send_notification: Send notification retire: Retire + progress: Progress + actions: Actions + proposed_action: + execute: Execute polls: all: "All" no_dates: "no date assigned" diff --git a/config/locales/es/general.yml b/config/locales/es/general.yml index 6529ee239..29f2bc68d 100644 --- a/config/locales/es/general.yml +++ b/config/locales/es/general.yml @@ -498,6 +498,10 @@ es: edit_proposal_link: Editar propuesta send_notification: Enviar notificación retire: Retirar + progress: Progreso + actions: Acciones + proposed_action: + execute: Ejecutar polls: all: "Todas" no_dates: "sin fecha asignada" diff --git a/config/routes/proposal.rb b/config/routes/proposal.rb index e00a180ec..9666d77f7 100644 --- a/config/routes/proposal.rb +++ b/config/routes/proposal.rb @@ -3,6 +3,10 @@ resources :proposals do collection do patch :publish end + + member do + post :execute + end end member do diff --git a/db/migrate/20180614122630_create_proposal_executed_dashboard_actions.rb b/db/migrate/20180614122630_create_proposal_executed_dashboard_actions.rb new file mode 100644 index 000000000..1af6170cc --- /dev/null +++ b/db/migrate/20180614122630_create_proposal_executed_dashboard_actions.rb @@ -0,0 +1,12 @@ +class CreateProposalExecutedDashboardActions < ActiveRecord::Migration + def change + create_table :proposal_executed_dashboard_actions do |t| + t.references :proposal, index: true, foreign_key: true + t.references :proposal_dashboard_action, index: { name: 'index_proposal_action' }, foreign_key: true + t.datetime :executed_at + t.text :comments + + t.timestamps null: false + end + end +end diff --git a/db/schema.rb b/db/schema.rb index e4decd4e8..031c55128 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -891,6 +891,18 @@ ActiveRecord::Schema.define(version: 20180711224810) do t.integer "action_type", default: 0, null: false end + create_table "proposal_executed_dashboard_actions", force: :cascade do |t| + t.integer "proposal_id" + t.integer "proposal_dashboard_action_id" + t.datetime "executed_at" + t.text "comments" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "proposal_executed_dashboard_actions", ["proposal_dashboard_action_id"], name: "index_proposal_action", using: :btree + add_index "proposal_executed_dashboard_actions", ["proposal_id"], name: "index_proposal_executed_dashboard_actions_on_proposal_id", using: :btree + create_table "proposal_notifications", force: :cascade do |t| t.string "title" t.text "body" @@ -1324,6 +1336,8 @@ ActiveRecord::Schema.define(version: 20180711224810) do add_foreign_key "poll_recounts", "poll_booth_assignments", column: "booth_assignment_id" add_foreign_key "poll_recounts", "poll_officer_assignments", column: "officer_assignment_id" add_foreign_key "poll_voters", "polls" + add_foreign_key "proposal_executed_dashboard_actions", "proposal_dashboard_actions" + add_foreign_key "proposal_executed_dashboard_actions", "proposals" add_foreign_key "proposals", "communities" add_foreign_key "related_content_scores", "related_contents" add_foreign_key "related_content_scores", "users" diff --git a/spec/factories.rb b/spec/factories.rb index 3707da650..2012f8008 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -1071,4 +1071,14 @@ LOREM_IPSUM action_type 'resource' end end + + factory :proposal_executed_dashboard_action, class: 'ProposalExecutedDashboardAction' do + proposal + proposal_dashboard_action + executed_at { Time.now } + + trait :with_comments do + comments { Faker::Lorem.sentence(10) } + end + end end diff --git a/spec/models/abilities/common_spec.rb b/spec/models/abilities/common_spec.rb index e6cb210bb..ed12c797d 100644 --- a/spec/models/abilities/common_spec.rb +++ b/spec/models/abilities/common_spec.rb @@ -160,6 +160,19 @@ describe Abilities::Common do it { should_not be_able_to(:destroy, proposal_image) } it { should_not be_able_to(:destroy, proposal_document) } end + + describe 'proposals dashboard' do + it { is_expected.to be_able_to(:dashboard, own_proposal) } + it { is_expected.not_to be_able_to(:dashboard, proposal) } + end + + describe 'publishing proposals' do + let(:draft_own_proposal) { create(:proposal, :draft, author: user) } + + it { is_expected.to be_able_to(:publish, draft_own_proposal) } + it { is_expected.not_to be_able_to(:publish, own_proposal) } + it { is_expected.not_to be_able_to(:publish, proposal) } + end describe "when level 2 verified" do let(:own_spending_proposal) { create(:spending_proposal, author: user) } diff --git a/spec/models/proposal_executed_dashboard_action_spec.rb b/spec/models/proposal_executed_dashboard_action_spec.rb new file mode 100644 index 000000000..bc76d6800 --- /dev/null +++ b/spec/models/proposal_executed_dashboard_action_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true +require 'rails_helper' + +describe ProposalExecutedDashboardAction do + subject do + build :proposal_executed_dashboard_action, + proposal: proposal, + proposal_dashboard_action: proposal_dashboard_action, + executed_at: executed_at + end + + let(:proposal) { create :proposal } + let(:proposal_dashboard_action) { create :proposal_dashboard_action } + let(:executed_at) { Time.now } + + it { is_expected.to be_valid } + + context 'when proposal is nil' do + let(:proposal) { nil } + + it { is_expected.not_to be_valid } + end + + context 'when proposal_dashboard_action is nil' do + let(:proposal_dashboard_action) { nil } + + it { is_expected.not_to be_valid } + end + + context 'when executed_at is nil' do + let(:executed_at) { nil } + + it { is_expected.not_to be_valid } + end + + context 'when it has been already executed' do + let!(:executed) { create(:proposal_executed_dashboard_action, proposal: proposal, proposal_dashboard_action: proposal_dashboard_action) } + + it { is_expected.not_to be_valid } + end +end