diff --git a/Gemfile.lock b/Gemfile.lock index 77000b108..a8e4487ea 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -527,6 +527,3 @@ DEPENDENCIES unicorn (~> 5.2.0) web-console (= 3.3.0) whenever - -BUNDLED WITH - 1.13.6 diff --git a/app/assets/javascripts/allow_participation.js.coffee b/app/assets/javascripts/allow_participation.js.coffee new file mode 100644 index 000000000..116e374ca --- /dev/null +++ b/app/assets/javascripts/allow_participation.js.coffee @@ -0,0 +1,12 @@ +App.AllowParticipation = + + initialize: -> + $(document).on { + 'mouseenter focus': -> + $(this).find(".js-participation-not-allowed").show(); + $(this).find(".js-participation-allowed").hide(); + mouseleave: -> + $(this).find(".js-participation-not-allowed").hide(); + $(this).find(".js-participation-allowed").show(); + }, ".js-participation" + false diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index e196c94c3..939705c51 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -36,12 +36,14 @@ //= require tags //= require users //= require votes +//= require allow_participation //= require annotatable //= require advanced_search //= require registration_form //= require suggest //= require forms //= require tracks +//= require valuation_budget_investment_form //= require valuation_spending_proposal_form //= require embed_video //= require banners @@ -58,6 +60,7 @@ var initialize_modules = function() { App.Comments.initialize(); App.Users.initialize(); App.Votes.initialize(); + App.AllowParticipation.initialize(); App.Tags.initialize(); App.Dropdown.initialize(); App.LocationChanger.initialize(); @@ -70,6 +73,7 @@ var initialize_modules = function() { App.Suggest.initialize(); App.Forms.initialize(); App.Tracks.initialize(); + App.ValuationBudgetInvestmentForm.initialize(); App.ValuationSpendingProposalForm.initialize(); App.EmbedVideo.initialize(); App.Banners.initialize(); diff --git a/app/assets/javascripts/valuation_budget_investment_form.js.coffee b/app/assets/javascripts/valuation_budget_investment_form.js.coffee new file mode 100644 index 000000000..d79ff600e --- /dev/null +++ b/app/assets/javascripts/valuation_budget_investment_form.js.coffee @@ -0,0 +1,32 @@ +App.ValuationBudgetInvestmentForm = + + showFeasibleFields: -> + $('#valuation_budget_investment_edit_form #unfeasible_fields').hide('down') + $('#valuation_budget_investment_edit_form #feasible_fields').show() + + showNotFeasibleFields: -> + $('#valuation_budget_investment_edit_form #feasible_fields').hide('down') + $('#valuation_budget_investment_edit_form #unfeasible_fields').show() + + showAllFields: -> + $('#valuation_budget_investment_edit_form #feasible_fields').show('down') + $('#valuation_budget_investment_edit_form #unfeasible_fields').show('down') + + showFeasibilityFields: -> + feasibility = $("#valuation_budget_investment_edit_form input[type=radio][name='budget_investment[feasibility]']:checked").val() + if feasibility == 'feasible' + App.ValuationBudgetInvestmentForm.showFeasibleFields() + else if feasibility == 'unfeasible' + App.ValuationBudgetInvestmentForm.showNotFeasibleFields() + + + showFeasibilityFieldsOnChange: -> + $("#valuation_budget_investment_edit_form input[type=radio][name='budget_investment[feasibility]']").change -> + App.ValuationBudgetInvestmentForm.showAllFields() + App.ValuationBudgetInvestmentForm.showFeasibilityFields() + + + initialize: -> + App.ValuationBudgetInvestmentForm.showFeasibilityFields() + App.ValuationBudgetInvestmentForm.showFeasibilityFieldsOnChange() + false \ No newline at end of file diff --git a/app/assets/stylesheets/_settings.scss b/app/assets/stylesheets/_settings.scss index 7704739af..cb96667ee 100644 --- a/app/assets/stylesheets/_settings.scss +++ b/app/assets/stylesheets/_settings.scss @@ -76,7 +76,7 @@ $check: #46DB91; $proposals: #FFA42D; $proposals-dark: #794500; -$budget: #454372; +$budget: #7E328A; $budget-hover: #7571BF; $highlight: #E7F2FC; diff --git a/app/assets/stylesheets/admin.scss b/app/assets/stylesheets/admin.scss index 486b93bc8..8ab9f8e9b 100644 --- a/app/assets/stylesheets/admin.scss +++ b/app/assets/stylesheets/admin.scss @@ -37,12 +37,21 @@ body.admin { input[type="text"], textarea { width: 100%; } + + .input-group input[type="text"] { + border-radius: 0; + margin-bottom: 0 !important; + } } table { th { text-align: left; + + &.with-button { + line-height: $line-height*2; + } } tr { @@ -376,7 +385,7 @@ body.admin { } } -.admin-content .select-geozone { +.admin-content .select-geozone, .admin-content .select-heading { a { display: block; diff --git a/app/assets/stylesheets/icons.scss b/app/assets/stylesheets/icons.scss index 015111db0..9ce619f8f 100644 --- a/app/assets/stylesheets/icons.scss +++ b/app/assets/stylesheets/icons.scss @@ -180,6 +180,9 @@ .icon-arrow-top:before { content: "\57"; } +.icon-help-1:before { + content: "\58"; +} .icon-checkmark-circle:before { content: "\59"; } diff --git a/app/assets/stylesheets/layout.scss b/app/assets/stylesheets/layout.scss index 0ef13f01a..937e5b2a8 100644 --- a/app/assets/stylesheets/layout.scss +++ b/app/assets/stylesheets/layout.scss @@ -600,12 +600,28 @@ h2.sidebar-title { // -------------- .auth-page { + + .wrapper { + margin: 0 auto (-$line-height)*14; + } +} + +.auth-image { background: $brand image-url("auth_bg.jpg"); background-repeat: no-repeat; background-size: cover; - h1:not(.logo) { - @include logo; + @include breakpoint(medium) { + min-height: $line-height*42; + } + + h1 { + margin-top: $line-height; + + img { + height: rem-calc(80); + width: rem-calc(80); + } a { color: white; @@ -621,15 +637,27 @@ h2.sidebar-title { } } -.auth { +.auth-form { + + @include breakpoint(medium) { + padding-top: $line-height*4; + } p, a, .checkbox { font-size: $small-font-size; } +} - .panel { +.auth-divider { + border-bottom: 1px solid $border; + height: $line-height/2; + margin: $line-height 0; + text-align: center; + + span { background: white; - border: 0; + font-weight: bold; + padding: 0 $line-height/2; } } @@ -805,6 +833,11 @@ form { .callout { font-size: $small-font-size; + a { + font-weight: bold; + text-decoration: underline; + } + &.success, &.notice { background-color: $success-bg; border-color: $success-border; @@ -821,12 +854,6 @@ form { background-color: $warning-bg; border-color: $warning-border; color: $color-warning; - - a { - color: $color-warning; - font-weight: bold; - text-decoration: underline; - } } &.alert, &.error { diff --git a/app/assets/stylesheets/participation.scss b/app/assets/stylesheets/participation.scss index 73ad3bec4..e0538d231 100644 --- a/app/assets/stylesheets/participation.scss +++ b/app/assets/stylesheets/participation.scss @@ -5,7 +5,8 @@ // 03. Show participation // 04. List participation // 05. Featured -// 06. Proposals successfull +// 06. Budget +// 07. Proposals successfull // // 01. Votes and supports @@ -238,6 +239,7 @@ .debate-form, .proposal-form, +.budget-investment-form, .spending-proposal-form { .icon-debates, .icon-proposals, .icon-budget { @@ -295,7 +297,8 @@ .debate-show, .proposal-show, .investment-project-show, -.debate-quiz { +.debate-quiz, +.budget-investment-show { p { word-wrap: break-word; @@ -322,14 +325,14 @@ margin-bottom: 0; } - .debate-info, .proposal-info, .investment-project-info { + .debate-info, .proposal-info, .investment-project-info, .budget-investment-show { clear: both; color: $text-medium; font-size: $small-font-size; margin-bottom: $line-height/2; position: relative; - span { + span:not(.label) { line-height: rem-calc(32); // Same as avatar height } @@ -456,11 +459,11 @@ color: $border; } -.investment-project-show p { +.investment-project-show p, .budget-investment-show p { word-break: break-word; } -.proposal-show, .investment-project-show { +.proposal-show, .investment-project-show, .budget-investment-show { .supports { padding: $line-height/2 0 0; @@ -474,21 +477,21 @@ // 04. List participation // ---------------------- -.debates-list, .proposals-list, .investment-projects-list { +.debates-list, .proposals-list, .investment-projects-list, .budget-investments-list { @include breakpoint(medium) { margin-bottom: rem-calc(48); } } -.investment-projects-list { +.investment-projects-list, .budget-investments-list { @include breakpoint(medium) { min-height: $line-height*15; } } -.debate, .proposal, .investment-project, .legislation { +.debate, .proposal, .investment-project, .budget-investment, .legislation { margin: $line-height/4 0; .panel { @@ -506,7 +509,7 @@ padding-bottom: rem-calc(12); } - .label-debate, .label-proposal, .label-investment-project { + .label-debate, .label-proposal, .label-investment-project, .label-budget-investment { background: none; clear: both; display: block; @@ -531,6 +534,10 @@ color: $budget; } + .label-budget-investment { + color: $budget; + } + h3 { font-weight: bold; margin: 0; @@ -540,7 +547,7 @@ } } - .debate-content, .proposal-content, .investment-project-content { + .debate-content, .proposal-content, .investment-project-content, .budget-investment-content { margin: 0; min-height: rem-calc(180); position: relative; @@ -570,7 +577,7 @@ font-size: $small-font-size; } - .debate-info, .proposal-info, .investment-project-info { + .debate-info, .proposal-info, .investment-project-info, .budget-investment-info { color: $text-medium; font-size: $small-font-size; margin: rem-calc(6) 0 0; @@ -585,7 +592,7 @@ } } - .debate-description, .proposal-description, .investment-project-description { + .debate-description, .proposal-description, .investment-project-description, .budget-investment-description { color: $text; font-size: rem-calc(13); height: rem-calc(72); @@ -670,12 +677,14 @@ } } -.investment-project, .investment-project-show { +.investment-project, .investment-project-show, +.budget-investment, .budget-investment-show { .supports { @include supports; - .investment-project-amount { + .investment-project-amount, + .budget-investment-amount { color: $budget; font-size: rem-calc(20); font-weight: bold; @@ -704,6 +713,7 @@ } .remove .icon-check-circle { + color: $budget; display: block; font-size: rem-calc(70); line-height: rem-calc(70); @@ -711,6 +721,11 @@ } } +.investment-project-show .supports, +.budget-investment-show .supports { + border: 0; +} + .proposals-summary { .panel { @@ -719,11 +734,38 @@ } .investment-project .supports .total-supports.no-button, -.investment-project-show .supports .total-supports.no-button { +.investment-project-show .supports .total-supports.no-button, +.budget-investment .supports .total-supports.no-button, +.budget-investment-show .supports .total-supports.no-button { display: block; margin-top: $line-height*1.5; } +.budget-investment-show { + + .label-budget-investment { + background: none; + clear: both; + color: $budget; + display: block; + font-size: rem-calc(12); + font-weight: bold; + line-height: $line-height; + padding-bottom: 0; + padding-left: 0; + padding-top: 0; + text-transform: uppercase; + } + + .icon-budget { + color: $budget; + font-size: $small-font-size; + line-height: $line-height; + margin-left: rem-calc(6); + top: 0; + } +} + // 05. Featured // ------------ @@ -835,7 +877,281 @@ } } -// 06. Proposals successfull +// 06. Budget +// ---------- + +.expanded.budget { + background: $budget; + + h1, h2, p, a.back, .icon-angle-left { + color: white; + } + + .button { + background: white; + color: $budget; + } + + .info { + background: #6A2A72; + + @include breakpoint(medium) { + border-top: rem-calc(6) solid #54225C; + } + } +} + +.jumbo-budget { + background: $budget; + border-bottom: 1px solid $budget; + + &.budget-heading { + min-height: $line-height*10; + } + + h1 { + margin-bottom: 0; + } + + h1, h2, .back, .icon-angle-left, p, a { + color: white; + } + + &.welcome { + background: $budget image-url('spending_proposals_bg.jpg'); + background-position: 50% 50%; + background-repeat: no-repeat; + background-size: cover; + + .spending-proposal-timeline { + padding-top: $line-height; + + ul li { + margin-right: $line-height; + padding-top: $line-height/2; + + .icon-calendar { + display: none; + } + } + } + } + + a { + text-decoration: underline; + + &.button { + background: white; + color: $brand; + margin-bottom: rem-calc(3); + text-decoration: none; + } + } + + .social-share-button a { + color: white; + + &.social-share-button-twitter:hover { + color: #40A2D1; + } + + &.social-share-button-facebook:hover { + color: #354F88; + } + + &.social-share-button-google_plus:hover { + color: #CE3E26; + } + } +} + +.progress-votes { + position: relative; + + .progress { + background: #212033; + clear: both; + } + + .progress-meter { + background: #fdcb10; + border-radius: 0; + -webkit-transition: width 2s; + transition: width 2s; + } + + .spent-amount-progress, + .spent-amount-meter { + background: none !important; + } + + .spent-amount-text { + color: white; + font-size: $base-font-size; + font-weight: normal; + position: absolute; + right: 0; + text-align: right; + top: 16px; + width: 100%; + + &:before { + color: #a5a1ff; + content: "\57"; + font-family: 'icons'; + font-size: $small-font-size; + position: absolute; + right: -6px; + top: -17px; + } + } + + .total-amount { + color: white; + font-size: rem-calc(18); + font-weight: bold; + float: right; + } + + .amount-available { + display: block; + text-align: right; + + span { + font-size: rem-calc(24); + font-weight: bold; + } + } +} + +.big-number { + color: $budget; + font-size: rem-calc(60); + line-height: rem-calc(120); + + @include breakpoint(large) { + font-size: rem-calc(90); + line-height: rem-calc(240); + } +} + +.ballot { + + h2, h3 { + font-weight: normal; + + span { + color: $budget; + font-weight: bold; + } + } + + h3.subtitle { + border-bottom: 3px solid $budget; + + span { + font-size: $base-font-size; + font-weight: normal; + } + } + + .amount-spent { + background: $success-bg; + font-weight: normal; + padding: $line-height/2; + + span { + font-size: rem-calc(24); + font-weight: bold; + } + } +} + +ul.ballot-list { + list-style: none; + margin-left: 0; + + li { + background: #f9f9f9; + line-height: $line-height; + margin-bottom: $line-height/4; + padding: $line-height/2; + position: relative; + + a { + color: $text; + } + + span { + color: #9f9f9f; + display: block; + font-style: italic; + } + + .remove-investment-project { + display: block; + height: 0; + + .icon-x { + color: #9f9f9f; + font-size: rem-calc(24); + line-height: $line-height/2; + position: absolute; + right: 6px; + text-decoration: none; + top: 6px; + + @include breakpoint(medium) { + font-size: $base-font-size; + } + } + } + + &:hover { + background: $budget; + color: white; + + a, span { + color: white; + outline: 0; + text-decoration: none; + } + + .remove-investment-project .icon-x { + color: white; + } + } + } +} + +.select-district a { + display: inline-block; + margin: $line-height/4 0; + padding: $line-height/4; +} + +.select-district .active a { + background: #f9f9f9; + border-radius: rem-calc(3); + color: $budget; + font-weight: bold; + + &:after { + content: "\56"; + font-family: "icons"; + font-size: $small-font-size; + font-weight: normal; + line-height: $line-height; + padding-left: rem-calc(3); + vertical-align: baseline; + + &:hover { + text-decoration: none; + } + } +} + +// 07. Proposals successfull // ------------------------- .dark-heading { diff --git a/app/controllers/admin/budget_groups_controller.rb b/app/controllers/admin/budget_groups_controller.rb new file mode 100644 index 000000000..9c5a54b98 --- /dev/null +++ b/app/controllers/admin/budget_groups_controller.rb @@ -0,0 +1,17 @@ +class Admin::BudgetGroupsController < Admin::BaseController + include FeatureFlags + feature_flag :budgets + + def create + @budget = Budget.find params[:budget_id] + @budget.groups.create(budget_group_params) + @groups = @budget.groups.includes(:headings) + end + + private + + def budget_group_params + params.require(:budget_group).permit(:name) + end + +end \ No newline at end of file diff --git a/app/controllers/admin/budget_headings_controller.rb b/app/controllers/admin/budget_headings_controller.rb new file mode 100644 index 000000000..56903b744 --- /dev/null +++ b/app/controllers/admin/budget_headings_controller.rb @@ -0,0 +1,18 @@ +class Admin::BudgetHeadingsController < Admin::BaseController + include FeatureFlags + feature_flag :budgets + + def create + @budget = Budget.find params[:budget_id] + @budget_group = @budget.groups.find params[:budget_group_id] + @budget_group.headings.create(budget_heading_params) + @headings = @budget_group.headings + end + + private + + def budget_heading_params + params.require(:budget_heading).permit(:name, :price, :geozone_id) + end + +end \ No newline at end of file diff --git a/app/controllers/admin/budget_investments_controller.rb b/app/controllers/admin/budget_investments_controller.rb new file mode 100644 index 000000000..b47fd4e49 --- /dev/null +++ b/app/controllers/admin/budget_investments_controller.rb @@ -0,0 +1,86 @@ +class Admin::BudgetInvestmentsController < Admin::BaseController + include FeatureFlags + feature_flag :budgets + + has_filters(%w{valuation_open without_admin managed valuating valuation_finished + valuation_finished_feasible selected all}, + only: [:index, :toggle_selection]) + + before_action :load_budget + before_action :load_investment, only: [:show, :edit, :update, :toggle_selection] + before_action :load_ballot, only: [:show, :index] + before_action :load_investments, only: [:index, :toggle_selection] + + def index + end + + def show + end + + def edit + load_admins + load_valuators + load_tags + end + + def update + set_valuation_tags + if @investment.update(budget_investment_params) + redirect_to admin_budget_budget_investment_path(@budget, @investment, Budget::Investment.filter_params(params)), + notice: t("flash.actions.update.budget_investment") + else + load_admins + load_valuators + load_tags + render :edit + end + end + + def toggle_selection + @investment.toggle :selected + @investment.save + end + + private + + def load_investments + @investments = Budget::Investment.scoped_filter(params, @current_filter) + .order(cached_votes_up: :desc, created_at: :desc) + .page(params[:page]) + end + + def budget_investment_params + params.require(:budget_investment) + .permit(:title, :description, :external_url, :heading_id, :administrator_id, :valuation_tag_list, valuator_ids: []) + end + + def load_budget + @budget = Budget.includes(:groups).find params[:budget_id] + end + + def load_investment + @investment = Budget::Investment.where(budget_id: @budget.id).find params[:id] + end + + def load_admins + @admins = Administrator.includes(:user).all + end + + def load_valuators + @valuators = Valuator.includes(:user).all.order("description ASC").order("users.email ASC") + end + + def load_tags + @tags = Budget::Investment.tags_on(:valuation).order(:name).uniq + end + + def load_ballot + query = Budget::Ballot.where(user: current_user, budget: @budget) + @ballot = @budget.balloting? ? query.first_or_create : query.first_or_initialize + end + + def set_valuation_tags + @investment.set_tag_list_on(:valuation, budget_investment_params[:valuation_tag_list]) + params[:budget_investment] = params[:budget_investment].except(:valuation_tag_list) + end +end diff --git a/app/controllers/admin/budgets_controller.rb b/app/controllers/admin/budgets_controller.rb new file mode 100644 index 000000000..0b373c0bd --- /dev/null +++ b/app/controllers/admin/budgets_controller.rb @@ -0,0 +1,48 @@ +class Admin::BudgetsController < Admin::BaseController + include FeatureFlags + feature_flag :budgets + + has_filters %w{current finished}, only: :index + + load_and_authorize_resource + + def index + @budgets = Budget.send(@current_filter).order(created_at: :desc).page(params[:page]) + end + + def show + @budget = Budget.includes(groups: :headings).find(params[:id]) + end + + def new + end + + def edit + end + + def update + if @budget.update(budget_params) + redirect_to admin_budget_path(@budget), notice: t('admin.budgets.update.notice') + else + render :edit + end + end + + def create + @budget = Budget.new(budget_params) + if @budget.save + redirect_to admin_budget_path(@budget), notice: t('admin.budgets.create.notice') + else + render :new + end + end + + private + + def budget_params + descriptions = Budget::PHASES.map{|p| "description_#{p}"}.map(&:to_sym) + valid_attributes = [:name, :phase, :currency_symbol] + descriptions + params.require(:budget).permit(*valid_attributes) + end + +end diff --git a/app/controllers/budgets/ballot/lines_controller.rb b/app/controllers/budgets/ballot/lines_controller.rb new file mode 100644 index 000000000..0f8209e5a --- /dev/null +++ b/app/controllers/budgets/ballot/lines_controller.rb @@ -0,0 +1,68 @@ +module Budgets + module Ballot + class LinesController < ApplicationController + before_action :authenticate_user! + #before_action :ensure_final_voting_allowed + before_action :load_budget + before_action :load_ballot + + before_action :load_investments + + load_and_authorize_resource :budget + load_and_authorize_resource :ballot, class: "Budget::Ballot", through: :budget + load_and_authorize_resource :line, through: :ballot, find_by: :investment_id, class: "Budget::Ballot::Line" + + def create + load_investment + load_heading + + unless @ballot.add_investment(@investment) + head :bad_request + end + end + + def destroy + @investment = @line.investment + load_heading + + @line.destroy + load_investments + #@ballot.reset_geozone + end + + private + + def ensure_final_voting_allowed + return head(:forbidden) unless @budget.balloting? + end + + def line_params + params.permit(:investment_id, :budget_id) + end + + def load_budget + @budget = Budget.find(params[:budget_id]) + end + + def load_ballot + @ballot = Budget::Ballot.where(user: current_user, budget: @budget).first_or_create + end + + def load_investment + @investment = Budget::Investment.find(params[:investment_id]) + end + + def load_investments + if params[:investments_ids].present? + @investment_ids = params[:investment_ids] + @investments = Budget::Investment.where(id: params[:investments_ids]) + end + end + + def load_heading + @heading = @investment.heading + end + + end + end +end diff --git a/app/controllers/budgets/ballots_controller.rb b/app/controllers/budgets/ballots_controller.rb new file mode 100644 index 000000000..ce531e145 --- /dev/null +++ b/app/controllers/budgets/ballots_controller.rb @@ -0,0 +1,20 @@ +module Budgets + class BallotsController < ApplicationController + before_action :authenticate_user! + load_and_authorize_resource :budget + before_action :load_ballot + + def show + authorize! :show, @ballot + render template: "budgets/ballot/show" + end + + private + + def load_ballot + query = Budget::Ballot.where(user: current_user, budget: @budget) + @ballot = @budget.balloting? ? query.first_or_create : query.first_or_initialize + end + + end +end diff --git a/app/controllers/budgets/groups_controller.rb b/app/controllers/budgets/groups_controller.rb new file mode 100644 index 000000000..e55974a6a --- /dev/null +++ b/app/controllers/budgets/groups_controller.rb @@ -0,0 +1,10 @@ +module Budgets + class GroupsController < ApplicationController + load_and_authorize_resource :budget + load_and_authorize_resource :group, class: "Budget::Group" + + def show + end + + end +end \ No newline at end of file diff --git a/app/controllers/budgets/investments_controller.rb b/app/controllers/budgets/investments_controller.rb new file mode 100644 index 000000000..235163f2e --- /dev/null +++ b/app/controllers/budgets/investments_controller.rb @@ -0,0 +1,106 @@ +module Budgets + class InvestmentsController < ApplicationController + include FeatureFlags + include CommentableActions + include FlagActions + + before_action :authenticate_user!, except: [:index, :show] + + load_and_authorize_resource :budget + load_and_authorize_resource :investment, through: :budget, class: "Budget::Investment" + + before_action -> { flash.now[:notice] = flash[:notice].html_safe if flash[:html_safe] && flash[:notice] } + before_action :load_ballot, only: [:index, :show] + before_action :load_heading, only: [:index, :show] + before_action :set_random_seed, only: :index + before_action :load_categories, only: [:index, :new, :create] + + feature_flag :budgets + + has_orders %w{most_voted newest oldest}, only: :show + has_orders ->(c) { c.instance_variable_get(:@budget).investments_orders }, only: :index + + invisible_captcha only: [:create, :update], honeypot: :subtitle, scope: :budget_investment + + respond_to :html, :js + + def index + @investments = @investments.apply_filters_and_search(@budget, params).send("sort_by_#{@current_order}").page(params[:page]).per(10).for_render + @investment_ids = @investments.pluck(:id) + load_investment_votes(@investments) + @tag_cloud = tag_cloud + end + + def new + end + + def show + @commentable = @investment + @comment_tree = CommentTree.new(@commentable, params[:page], @current_order) + set_comment_flags(@comment_tree.comments) + load_investment_votes(@investment) + @investment_ids = [@investment.id] + end + + def create + @investment.author = current_user + + if @investment.save + redirect_to budget_investment_path(@budget, @investment), + notice: t('flash.actions.create.budget_investment') + else + render :new + end + end + + def destroy + investment.destroy + redirect_to user_path(current_user, filter: 'budget_investments'), notice: t('flash.actions.destroy.budget_investment') + end + + def vote + @investment.register_selection(current_user) + load_investment_votes(@investment) + end + + private + + def load_investment_votes(investments) + @investment_votes = current_user ? current_user.budget_investment_votes(investments) : {} + end + + def set_random_seed + if params[:order] == 'random' || params[:order].blank? + params[:random_seed] ||= rand(99)/100.0 + Budget::Investment.connection.execute "select setseed(#{params[:random_seed]})" + else + params[:random_seed] = nil + end + end + + def investment_params + params.require(:budget_investment).permit(:title, :description, :external_url, :heading_id, :tag_list, :organization_name, :location, :terms_of_service) + end + + def load_ballot + query = Budget::Ballot.where(user: current_user, budget: @budget) + @ballot = @budget.balloting? ? query.first_or_create : query.first_or_initialize + end + + def load_heading + if params[:heading_id].present? + @heading = @budget.headings.find(params[:heading_id]) + @assigned_heading = @ballot.try(:heading_for_group, @heading.try(:group)) + end + end + + def load_categories + @categories = ActsAsTaggableOn::Tag.where("kind = 'category'").order(:name) + end + + def tag_cloud + TagCloud.new(Budget::Investment, params[:search]) + end + end + +end diff --git a/app/controllers/budgets_controller.rb b/app/controllers/budgets_controller.rb new file mode 100644 index 000000000..9c8a2036b --- /dev/null +++ b/app/controllers/budgets_controller.rb @@ -0,0 +1,16 @@ +class BudgetsController < ApplicationController + include FeatureFlags + feature_flag :budgets + + + load_and_authorize_resource + respond_to :html, :js + + def show + end + + def index + @budgets = @budgets.order(:created_at) + end + +end diff --git a/app/controllers/concerns/has_orders.rb b/app/controllers/concerns/has_orders.rb index 31a98e850..90be317f0 100644 --- a/app/controllers/concerns/has_orders.rb +++ b/app/controllers/concerns/has_orders.rb @@ -3,8 +3,9 @@ module HasOrders class_methods do def has_orders(valid_orders, *args) - before_action(*args) do - @valid_orders = valid_orders + before_action(*args) do |c| + @valid_orders = valid_orders.respond_to?(:call) ? valid_orders.call(c) : valid_orders.dup + @valid_orders.delete('relevance') if params[:search].blank? @current_order = @valid_orders.include?(params[:order]) ? params[:order] : @valid_orders.first end end diff --git a/app/controllers/management/budgets/investments_controller.rb b/app/controllers/management/budgets/investments_controller.rb new file mode 100644 index 000000000..e790312d6 --- /dev/null +++ b/app/controllers/management/budgets/investments_controller.rb @@ -0,0 +1,66 @@ +class Management::Budgets::InvestmentsController < Management::BaseController + + load_resource :budget + load_resource :investment, through: :budget, class: 'Budget::Investment' + + before_action :only_verified_users, except: :print + before_action :load_heading, only: [:index, :show, :print] + + def index + @investments = @investments.apply_filters_and_search(@budget, params).page(params[:page]) + load_investment_votes(@investments) + end + + def new + load_categories + end + + def create + @investment.terms_of_service = "1" + @investment.author = managed_user + + if @investment.save + notice= t('flash.actions.create.notice', resource_name: Budget::Investment.model_name.human, count: 1) + redirect_to management_budget_investment_path(@budget, @investment), notice: notice + else + render :new + end + end + + def show + load_investment_votes(@investment) + end + + def vote + @investment.register_selection(managed_user) + load_investment_votes(@investment) + end + + def print + @investments = @investments.apply_filters_and_search(@budget, params).order(cached_votes_up: :desc).for_render.limit(15) + load_investment_votes(@investments) + end + + private + + def load_investment_votes(investments) + @investment_votes = managed_user ? managed_user.budget_investment_votes(investments) : {} + end + + def investment_params + params.require(:budget_investment).permit(:title, :description, :external_url, :heading_id) + end + + def only_verified_users + check_verified_user t("management.budget_investments.alert.unverified_user") + end + + def load_heading + @heading = @budget.headings.find(params[:heading_id]) if params[:heading_id].present? + end + + def load_categories + @categories = ActsAsTaggableOn::Tag.where("kind = 'category'").order(:name) + end + +end diff --git a/app/controllers/management/budgets_controller.rb b/app/controllers/management/budgets_controller.rb new file mode 100644 index 000000000..c5cfdee76 --- /dev/null +++ b/app/controllers/management/budgets_controller.rb @@ -0,0 +1,26 @@ +class Management::BudgetsController < Management::BaseController + include FeatureFlags + include HasFilters + feature_flag :budgets + + before_action :only_verified_users, except: :print_investments + + def create_investments + @budgets = Budget.accepting.order(created_at: :desc).page(params[:page]) + end + + def support_investments + @budgets = Budget.selecting.order(created_at: :desc).page(params[:page]) + end + + def print_investments + @budgets = Budget.current.order(created_at: :desc).page(params[:page]) + end + + private + + def only_verified_users + check_verified_user t("management.budget_investments.alert.unverified_user") + end + +end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index fbd77f184..9bd98aedd 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,8 +1,7 @@ class UsersController < ApplicationController - has_filters %w{proposals debates comments spending_proposals}, only: :show + has_filters %w{proposals debates budget_investments comments}, only: :show load_and_authorize_resource - helper_method :authorized_for_filter? helper_method :author? helper_method :author_or_admin? @@ -14,9 +13,9 @@ class UsersController < ApplicationController def set_activity_counts @activity_counts = HashWithIndifferentAccess.new( proposals: Proposal.where(author_id: @user.id).count, - debates: Debate.where(author_id: @user.id).count, - comments: Comment.not_as_admin_or_moderator.where(user_id: @user.id).count, - spending_proposals: SpendingProposal.where(author_id: @user.id).count) + debates: (Setting['feature.debates'] ? Debate.where(author_id: @user.id).count : 0), + budget_investments: (Setting['feature.budgets'] ? Budget::Investment.where(author_id: @user.id).count : 0), + comments: only_active_commentables.count) end def load_filtered_activity @@ -24,8 +23,8 @@ class UsersController < ApplicationController case params[:filter] when "proposals" then load_proposals when "debates" then load_debates + when "budget_investments" then load_budget_investments when "comments" then load_comments - when "spending_proposals" then load_spending_proposals if author_or_admin? else load_available_activity end end @@ -34,15 +33,15 @@ class UsersController < ApplicationController if @activity_counts[:proposals] > 0 load_proposals @current_filter = "proposals" - elsif @activity_counts[:debates] > 0 + elsif @activity_counts[:debates] > 0 load_debates @current_filter = "debates" + elsif @activity_counts[:budget_investments] > 0 + load_budget_investments + @current_filter = "budget_investments" elsif @activity_counts[:comments] > 0 load_comments @current_filter = "comments" - elsif @activity_counts[:spending_proposals] > 0 && author_or_admin? - load_spending_proposals - @current_filter = "spending_proposals" end end @@ -55,11 +54,11 @@ class UsersController < ApplicationController end def load_comments - @comments = Comment.not_as_admin_or_moderator.where(user_id: @user.id).includes(:commentable).order(created_at: :desc).page(params[:page]) + @comments = only_active_commentables.includes(:commentable).order(created_at: :desc).page(params[:page]) end - def load_spending_proposals - @spending_proposals = SpendingProposal.where(author_id: @user.id).order(created_at: :desc).page(params[:page]) + def load_budget_investments + @budget_investments = Budget::Investment.where(author_id: @user.id).order(created_at: :desc).page(params[:page]) end def valid_access? @@ -78,7 +77,19 @@ class UsersController < ApplicationController @authorized_current_user ||= current_user && (current_user == @user || current_user.moderator? || current_user.administrator?) end - def authorized_for_filter?(filter) - filter == "spending_proposals" ? author_or_admin? : true + def all_user_comments + Comment.not_as_admin_or_moderator.where(user_id: @user.id) end + + def only_active_commentables + disabled_commentables = [] + disabled_commentables << "Debate" unless Setting['feature.debates'] + disabled_commentables << "Budget::Investment" unless Setting['feature.budgets'] + if disabled_commentables.present? + all_user_comments.where("commentable_type NOT IN (?)", disabled_commentables) + else + all_user_comments + end + end + end diff --git a/app/controllers/valuation/budget_investments_controller.rb b/app/controllers/valuation/budget_investments_controller.rb new file mode 100644 index 000000000..03facadb3 --- /dev/null +++ b/app/controllers/valuation/budget_investments_controller.rb @@ -0,0 +1,79 @@ +class Valuation::BudgetInvestmentsController < Valuation::BaseController + include FeatureFlags + feature_flag :budgets + + before_action :restrict_access_to_assigned_items, only: [:show, :edit, :valuate] + before_action :load_budget + before_action :load_investment, only: [:show, :edit, :valuate] + + has_filters %w{valuating valuation_finished}, only: :index + + load_and_authorize_resource :investment, class: "Budget::Investment" + + def index + @heading_filters = heading_filters + if current_user.valuator? && @budget.present? + @investments = @budget.investments.scoped_filter(params_for_current_valuator, @current_filter).order(cached_votes_up: :desc).page(params[:page]) + else + @investments = Budget::Investment.none.page(params[:page]) + end + end + + def valuate + if valid_price_params? && @investment.update(valuation_params) + redirect_to valuation_budget_budget_investment_path(@budget, @investment), notice: t('valuation.budget_investments.notice.valuate') + else + render action: :edit + end + end + + private + + def load_budget + @budget = Budget.find(params[:budget_id]) + end + + def load_investment + @investment = @budget.investments.find params[:id] + end + + def heading_filters + investments = @budget.investments.by_valuator(current_user.valuator.try(:id)).valuation_open.select(:heading_id).all.to_a + + [ { name: t('valuation.budget_investments.index.headings_filter_all'), + id: nil, + pending_count: investments.size + } + ] + Budget::Heading.where(id: investments.map(&:heading_id).uniq).order(name: :asc).collect do |h| + { name: h.name, + id: h.id, + pending_count: investments.count{|x| x.heading_id == h.id} + } + end + end + + def params_for_current_valuator + Budget::Investment.filter_params(params).merge({valuator_id: current_user.valuator.id, budget_id: @budget.id}) + end + + def valuation_params + params.require(:budget_investment).permit(:price, :price_first_year, :price_explanation, :feasibility, :unfeasibility_explanation, :duration, :valuation_finished, :internal_comments) + end + + def restrict_access_to_assigned_items + raise ActionController::RoutingError.new('Not Found') unless current_user.administrator? || Budget::ValuatorAssignment.exists?(investment_id: params[:id], valuator_id: current_user.valuator.id) + end + + def valid_price_params? + if /\D/.match params[:budget_investment][:price] + @investment.errors.add(:price, I18n.t('budgets.investments.wrong_price_format')) + end + + if /\D/.match params[:budget_investment][:price_first_year] + @investment.errors.add(:price_first_year, I18n.t('budgets.investments.wrong_price_format')) + end + + @investment.errors.empty? + end + +end diff --git a/app/controllers/valuation/budgets_controller.rb b/app/controllers/valuation/budgets_controller.rb new file mode 100644 index 000000000..744b0d4bf --- /dev/null +++ b/app/controllers/valuation/budgets_controller.rb @@ -0,0 +1,17 @@ +class Valuation::BudgetsController < Valuation::BaseController + include FeatureFlags + feature_flag :budgets + + load_and_authorize_resource + + def index + @budgets = @budgets.current.order(created_at: :desc).page(params[:page]) + @investments_with_valuation_open = {} + @budgets.each do |b| + @investments_with_valuation_open[b.id] = b.investments + .by_valuator(current_user.valuator.try(:id)) + .valuation_open + .count + end + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 313375e3f..0ea2bb495 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -42,4 +42,11 @@ module ApplicationHelper return false if authorable.blank? || user.blank? authorable.author_id == user.id end + + def back_link_to(destination_path) + destination = destination_path || :back + link_to destination, class: "back" do + "".html_safe + t("shared.back") + end + end end diff --git a/app/helpers/ballots_helper.rb b/app/helpers/ballots_helper.rb new file mode 100644 index 000000000..ece1be9fa --- /dev/null +++ b/app/helpers/ballots_helper.rb @@ -0,0 +1,7 @@ +module BallotsHelper + + def progress_bar_width(amount_available, amount_spent) + (amount_spent/amount_available.to_f * 100).to_s + "%" + end + +end \ No newline at end of file diff --git a/app/helpers/budget_headings_helper.rb b/app/helpers/budget_headings_helper.rb new file mode 100644 index 000000000..3fa1eac89 --- /dev/null +++ b/app/helpers/budget_headings_helper.rb @@ -0,0 +1,9 @@ +module BudgetHeadingsHelper + + def budget_heading_select_options(budget) + budget.headings.order_by_group_name.map do |heading| + [heading.name_scoped_by_group, heading.id] + end + end + +end diff --git a/app/helpers/budgets_helper.rb b/app/helpers/budgets_helper.rb new file mode 100644 index 000000000..84f04139c --- /dev/null +++ b/app/helpers/budgets_helper.rb @@ -0,0 +1,41 @@ +module BudgetsHelper + + def budget_phases_select_options + Budget::PHASES.map { |ph| [ t("budgets.phase.#{ph}"), ph ] } + end + + def budget_currency_symbol_select_options + Budget::CURRENCY_SYMBOLS.map { |cs| [ cs, cs ] } + end + + def namespaced_budget_investment_path(investment, options={}) + case namespace + when "management::budgets" + management_budget_investment_path(investment.budget, investment, options) + else + budget_investment_path(investment.budget, investment, options) + end + end + + def namespaced_budget_investment_vote_path(investment, options={}) + case namespace + when "management::budgets" + vote_management_budget_investment_path(investment.budget, investment, options) + else + vote_budget_investment_path(investment.budget, investment, options) + end + end + + def display_budget_countdown?(budget) + budget.balloting? + end + + def css_for_ballot_heading(heading) + return '' unless current_ballot.present? + current_ballot.has_lines_in_heading?(heading) ? 'active' : '' + end + + def current_ballot + Budget::Ballot.where(user: current_user, budget: @budget).first + end +end diff --git a/app/helpers/comments_helper.rb b/app/helpers/comments_helper.rb index 13884237b..b9e69ddf9 100644 --- a/app/helpers/comments_helper.rb +++ b/app/helpers/comments_helper.rb @@ -20,6 +20,14 @@ module CommentsHelper end end + def commentable_path(comment) + if comment.commentable_type == "Budget::Investment" + budget_investment_path(comment.commentable.budget_id, comment.commentable) + else + comment.commentable + end + end + def user_level_class(comment) if comment.as_administrator? "is-admin" diff --git a/app/helpers/geozones_helper.rb b/app/helpers/geozones_helper.rb index bfc5f9105..ce03e0579 100644 --- a/app/helpers/geozones_helper.rb +++ b/app/helpers/geozones_helper.rb @@ -8,4 +8,9 @@ module GeozonesHelper Geozone.all.order(name: :asc).collect { |g| [ g.name, g.id ] } end + def geozone_name_from_id(g_id) + @all_geozones ||= Geozone.all.collect{ |g| [ g.id, g.name ] }.to_h + @all_geozones[g_id] || t("geozones.none") + end + end diff --git a/app/helpers/mailer_helper.rb b/app/helpers/mailer_helper.rb index ad6f042af..e541eebb6 100644 --- a/app/helpers/mailer_helper.rb +++ b/app/helpers/mailer_helper.rb @@ -3,6 +3,7 @@ module MailerHelper def commentable_url(commentable) return debate_url(commentable) if commentable.is_a?(Debate) return proposal_url(commentable) if commentable.is_a?(Proposal) + return budget_investment_url(commentable.budget_id, commentable) if commentable.is_a?(Budget::Investment) end end \ No newline at end of file diff --git a/app/helpers/orders_helper.rb b/app/helpers/orders_helper.rb deleted file mode 100644 index 08d5588ca..000000000 --- a/app/helpers/orders_helper.rb +++ /dev/null @@ -1,7 +0,0 @@ -module OrdersHelper - - def valid_orders - @valid_orders.reject { |order| order =='relevance' && params[:search].blank? } - end - -end \ No newline at end of file diff --git a/app/helpers/tags_helper.rb b/app/helpers/tags_helper.rb index da7824622..36ba8d048 100644 --- a/app/helpers/tags_helper.rb +++ b/app/helpers/tags_helper.rb @@ -6,6 +6,8 @@ module TagsHelper debates_path(search: tag_name) when 'proposal' proposals_path(search: tag_name) + when 'budget/investment' + budget_investments_path(@budget, search: tag_name) else '#' end diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index 10da73712..9c13e8ef8 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -30,6 +30,8 @@ module UsersHelper t("users.show.deleted_proposal") when "Debate" t("users.show.deleted_debate") + when "Budget::Investment" + t("users.show.deleted_budget_investment") else t("users.show.deleted") end diff --git a/app/helpers/valuation_helper.rb b/app/helpers/valuation_helper.rb index 0983ea567..ded5fa0ed 100644 --- a/app/helpers/valuation_helper.rb +++ b/app/helpers/valuation_helper.rb @@ -11,14 +11,14 @@ module ValuationHelper def assigned_valuators_info(valuators) case valuators.size when 0 - t("valuation.spending_proposals.index.no_valuators_assigned") + t("valuation.budget_investments.index.no_valuators_assigned") when 1 - "".html_safe + + "".html_safe + valuators.first.name + "".html_safe else "".html_safe + - t('valuation.spending_proposals.index.valuators_assigned', count: valuators.size) + + t('valuation.budget_investments.index.valuators_assigned', count: valuators.size) + "".html_safe end end diff --git a/app/models/abilities/administrator.rb b/app/models/abilities/administrator.rb index 5a660e059..fa09592d5 100644 --- a/app/models/abilities/administrator.rb +++ b/app/models/abilities/administrator.rb @@ -4,7 +4,6 @@ module Abilities def initialize(user) self.merge Abilities::Moderation.new(user) - self.merge Abilities::Valuator.new(user) can :restore, Comment cannot :restore, Comment, hidden_at: nil @@ -33,7 +32,7 @@ module Abilities can :mark_featured, Debate can :unmark_featured, Debate - can :comment_as_administrator, [Debate, Comment, Proposal, Legislation::Question, Legislation::Annotation] + can :comment_as_administrator, [Debate, Comment, Proposal, Budget::Investment, Legislation::Question, Legislation::Annotation] can [:search, :create, :index, :destroy], ::Moderator can [:search, :create, :index, :summary], ::Valuator @@ -41,7 +40,15 @@ module Abilities can :manage, Annotation - can [:read, :update, :destroy, :summary], SpendingProposal + can [:read, :update, :valuate, :destroy, :summary], SpendingProposal + + can [:index, :read, :new, :create, :update, :destroy], Budget + can [:read, :create, :update, :destroy], Budget::Group + can [:read, :create, :update, :destroy], Budget::Heading + can [:hide, :update, :toggle_selection], Budget::Investment + can :valuate, Budget::Investment + can :create, Budget::ValuatorAssignment + can [:search, :edit, :update, :create, :index, :destroy], Banner can [:index, :create, :edit, :update, :destroy], Geozone diff --git a/app/models/abilities/common.rb b/app/models/abilities/common.rb index f82e78b58..e324540d2 100644 --- a/app/models/abilities/common.rb +++ b/app/models/abilities/common.rb @@ -18,8 +18,6 @@ module Abilities end can [:retire_form, :retire], Proposal, author_id: user.id - can :read, SpendingProposal - can :create, Comment can :create, Debate can :create, Proposal @@ -46,6 +44,12 @@ module Abilities can :vote_featured, Proposal can :vote, SpendingProposal can :create, SpendingProposal + + can :create, Budget::Investment, budget: { phase: "accepting" } + can :vote, Budget::Investment, budget: { phase: "selecting" } + can [:show, :create], Budget::Ballot, budget: { phase: "balloting" } + can [:create, :destroy], Budget::Ballot::Line, budget: { phase: "balloting" } + can :create, DirectMessage can :show, DirectMessage, sender_id: user.id end diff --git a/app/models/abilities/everyone.rb b/app/models/abilities/everyone.rb index 179c67350..ae3dc432d 100644 --- a/app/models/abilities/everyone.rb +++ b/app/models/abilities/everyone.rb @@ -6,10 +6,15 @@ module Abilities can [:read, :map], Debate can [:read, :map, :summary], Proposal can :read, Comment + can [:read, :welcome], Budget + can :read, Budget::Investment can :read, SpendingProposal can :read, LegacyLegislation can :read, User can [:search, :read], Annotation + can [:read], Budget + can [:read], Budget::Group + can [:read, :print], Budget::Investment can :new, DirectMessage can [:read, :draft_publication, :allegations, :final_version_publication], Legislation::Process can [:read, :changes, :go_to_version], Legislation::DraftVersion diff --git a/app/models/abilities/moderator.rb b/app/models/abilities/moderator.rb index 23b11b2e5..c808550e2 100644 --- a/app/models/abilities/moderator.rb +++ b/app/models/abilities/moderator.rb @@ -5,7 +5,7 @@ module Abilities def initialize(user) self.merge Abilities::Moderation.new(user) - can :comment_as_moderator, [Debate, Comment, Proposal, Legislation::Question, Legislation::Annotation] + can :comment_as_moderator, [Debate, Comment, Proposal, Budget::Investment, Legislation::Question, Legislation::Annotation] end end end diff --git a/app/models/abilities/valuator.rb b/app/models/abilities/valuator.rb index 15add866a..614869665 100644 --- a/app/models/abilities/valuator.rb +++ b/app/models/abilities/valuator.rb @@ -3,7 +3,10 @@ module Abilities include CanCan::Ability def initialize(user) + valuator = user.valuator can [:read, :update, :valuate], SpendingProposal + can [:read, :update, :valuate], Budget::Investment, id: valuator.investment_ids + cannot [:update, :valuate], Budget::Investment, budget: { phase: 'finished' } end end -end \ No newline at end of file +end diff --git a/app/models/budget.rb b/app/models/budget.rb new file mode 100644 index 000000000..2741ac014 --- /dev/null +++ b/app/models/budget.rb @@ -0,0 +1,118 @@ +class Budget < ActiveRecord::Base + + include Measurable + + PHASES = %w(accepting reviewing selecting valuating balloting reviewing_ballots finished).freeze + CURRENCY_SYMBOLS = %w(€ $ £ ¥).freeze + + validates :name, presence: true + validates :phase, inclusion: { in: PHASES } + validates :currency_symbol, presence: true + + has_many :investments, dependent: :destroy + has_many :ballots, dependent: :destroy + has_many :groups, dependent: :destroy + has_many :headings, through: :groups + + before_validation :sanitize_descriptions + + scope :on_hold, -> { where(phase: %w(reviewing valuating reviewing_ballots")) } + scope :accepting, -> { where(phase: "accepting") } + scope :reviewing, -> { where(phase: "reviewing") } + scope :selecting, -> { where(phase: "selecting") } + scope :valuating, -> { where(phase: "valuating") } + scope :balloting, -> { where(phase: "balloting") } + scope :reviewing_ballots, -> { where(phase: "reviewing_ballots") } + scope :finished, -> { where(phase: "finished") } + + scope :current, -> { where.not(phase: "finished") } + + def description + self.send("description_#{self.phase}").try(:html_safe) + end + + def self.description_max_length + 2000 + end + + def accepting? + phase == "accepting" + end + + def reviewing? + phase == "reviewing" + end + + def selecting? + phase == "selecting" + end + + def valuating? + phase == "valuating" + end + + def balloting? + phase == "balloting" + end + + def reviewing_ballots? + phase == "reviewing_ballots" + end + + def finished? + phase == "finished" + end + + def on_hold? + reviewing? || valuating? || reviewing_ballots? + end + + def current? + !finished? + end + + def heading_price(heading) + heading_ids.include?(heading.id) ? heading.price : -1 + end + + def translated_phase + I18n.t "budgets.phase.#{phase}" + end + + def formatted_amount(amount) + ActionController::Base.helpers.number_to_currency(amount, + precision: 0, + locale: I18n.default_locale, + unit: currency_symbol) + end + + def formatted_heading_price(heading) + formatted_amount(heading_price(heading)) + end + + def formatted_heading_amount_spent(heading) + formatted_amount(amount_spent(heading)) + end + + def investments_orders + case phase + when 'accepting', 'reviewing' + %w{random} + when 'balloting', 'reviewing_ballots' + %w{random price} + else + %w{random confidence_score} + end + end + + private + + def sanitize_descriptions + s = WYSIWYGSanitizer.new + PHASES.each do |phase| + sanitized = s.sanitize(self.send("description_#{phase}")) + self.send("description_#{phase}=", sanitized) + end + end +end + diff --git a/app/models/budget/ballot.rb b/app/models/budget/ballot.rb new file mode 100644 index 000000000..8355232a8 --- /dev/null +++ b/app/models/budget/ballot.rb @@ -0,0 +1,73 @@ +class Budget + class Ballot < ActiveRecord::Base + belongs_to :user + belongs_to :budget + + has_many :lines, dependent: :destroy + has_many :investments, through: :lines + has_many :groups, -> { uniq }, through: :lines + has_many :headings, -> { uniq }, through: :groups + + def add_investment(investment) + lines.create!(investment: investment) + end + + def total_amount_spent + investments.sum(:price).to_i + end + + def amount_spent(heading) + investments.by_heading(heading.id).sum(:price).to_i + end + + def formatted_amount_spent(heading) + budget.formatted_amount(amount_spent(heading)) + end + + def amount_available(heading) + budget.heading_price(heading) - amount_spent(heading) + end + + def formatted_amount_available(heading) + budget.formatted_amount(amount_available(heading)) + end + + def has_lines_in_group?(group) + self.groups.include?(group) + end + + def wrong_budget?(heading) + heading.budget_id != budget_id + end + + def different_heading_assigned?(heading) + other_heading_ids = heading.group.heading_ids - [heading.id] + lines.where(heading_id: other_heading_ids).exists? + end + + def valid_heading?(heading) + !wrong_budget?(heading) && !different_heading_assigned?(heading) + end + + def has_lines_with_no_heading? + investments.no_heading.count > 0 + end + + def has_lines_with_heading? + self.heading_id.present? + end + + def has_lines_in_heading?(heading) + investments.by_heading(heading.id).any? + end + + def has_investment?(investment) + self.investment_ids.include?(investment.id) + end + + def heading_for_group(group) + self.headings.where(group: group).first + end + + end +end diff --git a/app/models/budget/ballot/line.rb b/app/models/budget/ballot/line.rb new file mode 100644 index 000000000..23c6aaef2 --- /dev/null +++ b/app/models/budget/ballot/line.rb @@ -0,0 +1,39 @@ +class Budget + class Ballot + class Line < ActiveRecord::Base + belongs_to :ballot + belongs_to :investment + belongs_to :heading + belongs_to :group + belongs_to :budget + + validates :ballot_id, :investment_id, :heading_id, :group_id, :budget_id, presence: true + + validate :check_selected + validate :check_sufficient_funds + validate :check_valid_heading + + before_validation :set_denormalized_ids + + def check_sufficient_funds + errors.add(:money, "insufficient funds") if ballot.amount_available(investment.heading) < investment.price.to_i + end + + def check_valid_heading + errors.add(:heading, "This heading's budget is invalid, or a heading on the same group was already selected") unless ballot.valid_heading?(self.heading) + end + + def check_selected + errors.add(:investment, "unselected investment") unless investment.selected? + end + + private + + def set_denormalized_ids + self.heading_id ||= self.investment.try(:heading_id) + self.group_id ||= self.investment.try(:group_id) + self.budget_id ||= self.investment.try(:budget_id) + end + end + end +end diff --git a/app/models/budget/group.rb b/app/models/budget/group.rb new file mode 100644 index 000000000..dd7910950 --- /dev/null +++ b/app/models/budget/group.rb @@ -0,0 +1,10 @@ +class Budget + class Group < ActiveRecord::Base + belongs_to :budget + + has_many :headings, dependent: :destroy + + validates :budget_id, presence: true + validates :name, presence: true + end +end \ No newline at end of file diff --git a/app/models/budget/heading.rb b/app/models/budget/heading.rb new file mode 100644 index 000000000..a81308947 --- /dev/null +++ b/app/models/budget/heading.rb @@ -0,0 +1,20 @@ +class Budget + class Heading < ActiveRecord::Base + belongs_to :group + + has_many :investments + + validates :group_id, presence: true + validates :name, presence: true + validates :price, presence: true + + delegate :budget, :budget_id, to: :group, allow_nil: true + + scope :order_by_group_name, -> { includes(:group).order('budget_groups.name', 'budget_headings.name') } + + def name_scoped_by_group + "#{group.name}: #{name}" + end + + end +end diff --git a/app/models/budget/investment.rb b/app/models/budget/investment.rb new file mode 100644 index 000000000..5389825e5 --- /dev/null +++ b/app/models/budget/investment.rb @@ -0,0 +1,232 @@ +class Budget + class Investment < ActiveRecord::Base + + include Measurable + include Sanitizable + include Taggable + include Searchable + + acts_as_votable + acts_as_paranoid column: :hidden_at + include ActsAsParanoidAliases + + belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' + belongs_to :heading + belongs_to :group + belongs_to :budget + belongs_to :administrator + + has_many :valuator_assignments, dependent: :destroy + has_many :valuators, through: :valuator_assignments + has_many :comments, as: :commentable + + validates :title, presence: true + validates :author, presence: true + validates :description, presence: true + validates :heading_id, presence: true + validates_presence_of :unfeasibility_explanation, if: :unfeasibility_explanation_required? + + validates :title, length: { in: 4..Budget::Investment.title_max_length } + validates :description, length: { maximum: Budget::Investment.description_max_length } + validates :terms_of_service, acceptance: { allow_nil: false }, on: :create + + scope :sort_by_confidence_score, -> { reorder(confidence_score: :desc, id: :desc) } + scope :sort_by_price, -> { reorder(price: :desc, confidence_score: :desc, id: :desc) } + scope :sort_by_random, -> { reorder("RANDOM()") } + + scope :valuation_open, -> { where(valuation_finished: false) } + scope :without_admin, -> { valuation_open.where(administrator_id: nil) } + scope :managed, -> { valuation_open.where(valuator_assignments_count: 0).where("administrator_id IS NOT ?", nil) } + scope :valuating, -> { valuation_open.where("valuator_assignments_count > 0 AND valuation_finished = ?", false) } + scope :valuation_finished, -> { where(valuation_finished: true) } + scope :valuation_finished_feasible, -> { where(valuation_finished: true, feasibility: "feasible") } + scope :feasible, -> { where(feasibility: "feasible") } + scope :unfeasible, -> { where(feasibility: "unfeasible") } + scope :not_unfeasible, -> { where.not(feasibility: "unfeasible") } + scope :undecided, -> { where(feasibility: "undecided") } + scope :with_supports, -> { where('cached_votes_up > 0') } + scope :selected, -> { where(selected: true) } + scope :last_week, -> { where("created_at >= ?", 7.days.ago)} + + scope :by_group, -> (group_id) { where(group_id: group_id) } + scope :by_heading, -> (heading_id) { where(heading_id: heading_id) } + scope :by_admin, -> (admin_id) { where(administrator_id: admin_id) } + scope :by_tag, -> (tag_name) { tagged_with(tag_name) } + scope :by_valuator, -> (valuator_id) { where("budget_valuator_assignments.valuator_id = ?", valuator_id).joins(:valuator_assignments) } + + scope :for_render, -> { includes(:heading) } + + before_save :calculate_confidence_score + before_validation :set_responsible_name + before_validation :set_denormalized_ids + + def self.filter_params(params) + params.select{|x,_| %w{heading_id group_id administrator_id tag_name valuator_id}.include? x.to_s } + end + + def self.scoped_filter(params, current_filter) + results = Investment.where(budget_id: params[:budget_id]) + results = results.where(group_id: params[:group_id]) if params[:group_id].present? + results = results.by_heading(params[:heading_id]) if params[:heading_id].present? + results = results.by_admin(params[:administrator_id]) if params[:administrator_id].present? + results = results.by_tag(params[:tag_name]) if params[:tag_name].present? + results = results.by_valuator(params[:valuator_id]) if params[:valuator_id].present? + results = results.send(current_filter) if current_filter.present? + results.includes(:heading, :group, :budget, administrator: :user, valuators: :user) + end + + def self.limit_results(results, budget, max_per_heading, max_for_no_heading) + return results if max_per_heading <= 0 && max_for_no_heading <= 0 + + ids = [] + if max_per_heading > 0 + budget.headings.pluck(:id).each do |hid| + ids += Investment.where(heading_id: hid).order(confidence_score: :desc).limit(max_per_heading).pluck(:id) + end + end + + if max_for_no_heading > 0 + ids += Investment.no_heading.order(confidence_score: :desc).limit(max_for_no_heading).pluck(:id) + end + + conditions = ["investments.id IN (?)"] + values = [ids] + + if max_per_heading == 0 + conditions << "investments.heading_id IS NOT ?" + values << nil + elsif max_for_no_heading == 0 + conditions << "investments.heading_id IS ?" + values << nil + end + + results.where(conditions.join(' OR '), *values) + end + + def searchable_values + { title => 'A', + author.username => 'B', + heading.try(:name) => 'B', + tag_list.join(' ') => 'B', + description => 'C' + } + end + + def self.search(terms) + self.pg_search(terms) + end + + def self.by_heading(heading) + where(heading_id: heading == 'all' ? nil : heading.presence) + end + + def undecided? + feasibility == "undecided" + end + + def feasible? + feasibility == "feasible" + end + + def unfeasible? + feasibility == "unfeasible" + end + + def unfeasibility_explanation_required? + unfeasible? && valuation_finished? + end + + def total_votes + cached_votes_up + physical_votes + end + + def code + "B#{budget.id}I#{id}" + end + + def reason_for_not_being_selectable_by(user) + return permission_problem(user) if permission_problem?(user) + + return :no_selecting_allowed unless budget.selecting? + end + + def reason_for_not_being_ballotable_by(user, ballot) + return permission_problem(user) if permission_problem?(user) + return :not_selected unless selected? + return :no_ballots_allowed unless budget.balloting? + return :different_heading_assigned unless ballot.valid_heading?(heading) + return :not_enough_money if ballot.present? && !enough_money?(ballot) + end + + def permission_problem(user) + return :not_logged_in unless user + return :organization if user.organization? + return :not_verified unless user.can?(:vote, Budget::Investment) + return nil + end + + def permission_problem?(user) + permission_problem(user).present? + end + + def selectable_by?(user) + reason_for_not_being_selectable_by(user).blank? + end + + def ballotable_by?(user) + reason_for_not_being_ballotable_by(user).blank? + end + + def enough_money?(ballot) + available_money = ballot.amount_available(self.heading) + price.to_i <= available_money + end + + def register_selection(user) + vote_by(voter: user, vote: 'yes') if selectable_by?(user) + end + + def calculate_confidence_score + self.confidence_score = ScoreCalculator.confidence_score(total_votes, total_votes) + end + + def set_responsible_name + self.responsible_name = author.try(:document_number) if author.try(:document_number).present? + end + + def should_show_aside? + (budget.selecting? && !unfeasible?) || (budget.balloting? && feasible?) || budget.on_hold? + end + + def should_show_votes? + budget.selecting? || budget.on_hold? + end + + def should_show_ballots? + budget.balloting? + end + + def formatted_price + budget.formatted_amount(price) + end + + def self.apply_filters_and_search(budget, params) + investments = all + if budget.balloting? + investments = investments.selected + else + investments = params[:unfeasible].present? ? investments.unfeasible : investments.not_unfeasible + end + investments = investments.by_heading(params[:heading_id]) if params[:heading_id].present? + investments = investments.search(params[:search]) if params[:search].present? + investments + end + + private + + def set_denormalized_ids + self.group_id ||= self.heading.try(:group_id) + self.budget_id ||= self.heading.try(:group).try(:budget_id) + end + end +end diff --git a/app/models/budget/valuator_assignment.rb b/app/models/budget/valuator_assignment.rb new file mode 100644 index 000000000..18ef73812 --- /dev/null +++ b/app/models/budget/valuator_assignment.rb @@ -0,0 +1,6 @@ +class Budget + class ValuatorAssignment < ActiveRecord::Base + belongs_to :valuator, counter_cache: :budget_investments_count + belongs_to :investment, counter_cache: true + end +end diff --git a/app/models/comment.rb b/app/models/comment.rb index b850d63eb..89008bcae 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -10,7 +10,7 @@ class Comment < ActiveRecord::Base validates :body, presence: true validates :user, presence: true - validates_inclusion_of :commentable_type, in: ["Debate", "Proposal", "Legislation::Question", "Legislation::Annotation"] + validates_inclusion_of :commentable_type, in: ["Debate", "Proposal", "Budget::Investment", "Legislation::Question", "Legislation::Annotation"] validate :validate_body_length diff --git a/app/models/concerns/sanitizable.rb b/app/models/concerns/sanitizable.rb index 261eccbf2..a4520ba2f 100644 --- a/app/models/concerns/sanitizable.rb +++ b/app/models/concerns/sanitizable.rb @@ -6,6 +6,10 @@ module Sanitizable before_validation :sanitize_tag_list end + def description + super.try :html_safe + end + protected def sanitize_description diff --git a/app/models/debate.rb b/app/models/debate.rb index 020259d80..de29c5864 100644 --- a/app/models/debate.rb +++ b/app/models/debate.rb @@ -59,10 +59,6 @@ class Debate < ActiveRecord::Base "#{id}-#{title}".parameterize end - def description - super.try :html_safe - end - def likes cached_votes_up end diff --git a/app/models/proposal.rb b/app/models/proposal.rb index 2f0c76094..32175f9f7 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -94,10 +94,6 @@ class Proposal < ActiveRecord::Base summary end - def description - super.try :html_safe - end - def total_votes cached_votes_up end diff --git a/app/models/tag_cloud.rb b/app/models/tag_cloud.rb index f3ea655f0..107ecbf1a 100644 --- a/app/models/tag_cloud.rb +++ b/app/models/tag_cloud.rb @@ -32,7 +32,7 @@ class TagCloud end def table_name - resource_model.to_s.downcase.pluralize + resource_model.to_s.downcase.pluralize.gsub("::", "/") end end \ No newline at end of file diff --git a/app/models/user.rb b/app/models/user.rb index 4ded8d99e..82324da7e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -19,6 +19,7 @@ class User < ActiveRecord::Base has_many :identities, dependent: :destroy has_many :debates, -> { with_hidden }, foreign_key: :author_id has_many :proposals, -> { with_hidden }, foreign_key: :author_id + has_many :budget_investments, -> { with_hidden }, foreign_key: :author_id, class_name: 'Budget::Investment' has_many :comments, -> { with_hidden } has_many :spending_proposals, foreign_key: :author_id has_many :failed_census_calls @@ -93,6 +94,11 @@ class User < ActiveRecord::Base voted.each_with_object({}) { |v, h| h[v.votable_id] = v.value } end + def budget_investment_votes(budget_investments) + voted = votes.for_budget_investments(budget_investments) + voted.each_with_object({}) { |v, h| h[v.votable_id] = v.value } + end + def comment_flags(comments) comment_flags = flags.for_comments(comments) comment_flags.each_with_object({}){ |f, h| h[f.flaggable_id] = true } @@ -191,6 +197,10 @@ class User < ActiveRecord::Base @@username_max_length ||= self.columns.find { |c| c.name == 'username' }.limit || 60 end + def self.minimum_required_age + (Setting['min_age_to_participate'] || 16).to_i + end + def show_welcome_screen? sign_in_count == 1 && unverified? && !organization && !administrator? end diff --git a/app/models/valuator.rb b/app/models/valuator.rb index 8b82d20b1..5df6ea030 100644 --- a/app/models/valuator.rb +++ b/app/models/valuator.rb @@ -4,6 +4,8 @@ class Valuator < ActiveRecord::Base has_many :valuation_assignments, dependent: :destroy has_many :spending_proposals, through: :valuation_assignments + has_many :valuator_assignments, dependent: :destroy, class_name: 'Budget::ValuatorAssignment' + has_many :investments, through: :valuator_assignments, class_name: 'Budget::Investment' validates :user_id, presence: true, uniqueness: true diff --git a/app/models/verification/management/document.rb b/app/models/verification/management/document.rb index 17ea065bd..4264154d3 100644 --- a/app/models/verification/management/document.rb +++ b/app/models/verification/management/document.rb @@ -23,7 +23,7 @@ class Verification::Management::Document end def valid_age?(response) - if under_sixteen?(response) + if under_age?(response) errors.add(:age, true) return false else @@ -31,8 +31,8 @@ class Verification::Management::Document end end - def under_sixteen?(response) - 16.years.ago.beginning_of_day < response.date_of_birth.beginning_of_day + def under_age?(response) + User.minimum_required_age.years.ago.beginning_of_day < response.date_of_birth.beginning_of_day end def verified? diff --git a/app/models/verification/residence.rb b/app/models/verification/residence.rb index 96a36e5cd..eda562671 100644 --- a/app/models/verification/residence.rb +++ b/app/models/verification/residence.rb @@ -36,7 +36,7 @@ class Verification::Residence def allowed_age return if errors[:date_of_birth].any? - errors.add(:date_of_birth, I18n.t('verification.residence.new.error_not_allowed_age')) unless self.date_of_birth <= 16.years.ago + errors.add(:date_of_birth, I18n.t('verification.residence.new.error_not_allowed_age')) unless self.date_of_birth <= User.minimum_required_age.years.ago end def document_number_uniqueness diff --git a/app/views/admin/_menu.html.erb b/app/views/admin/_menu.html.erb index 8257b3c8a..910ae7140 100644 --- a/app/views/admin/_menu.html.erb +++ b/app/views/admin/_menu.html.erb @@ -35,6 +35,14 @@ <% end %> + <% if feature?(:budgets) %> +
  • > + <%= link_to admin_budgets_path do %> + <%= t("admin.menu.budgets") %> + <% end %> +
  • + <% end %> + <% if feature?(:signature_sheets) %>
  • > <%= link_to admin_signature_sheets_path do %> diff --git a/app/views/admin/budget_groups/create.js.erb b/app/views/admin/budget_groups/create.js.erb new file mode 100644 index 000000000..cb926a7c6 --- /dev/null +++ b/app/views/admin/budget_groups/create.js.erb @@ -0,0 +1,2 @@ +$("#<%= dom_id(@budget) %>_groups").html('<%= j render("admin/budgets/groups", groups: @groups) %>'); +App.Forms.toggleLink(); \ No newline at end of file diff --git a/app/views/admin/budget_headings/create.js.erb b/app/views/admin/budget_headings/create.js.erb new file mode 100644 index 000000000..5d8eefb2d --- /dev/null +++ b/app/views/admin/budget_headings/create.js.erb @@ -0,0 +1,2 @@ +$("#<%= dom_id(@budget_group) %>").html('<%= j render("admin/budgets/group", group: @budget_group, headings: @headings) %>'); +App.Forms.toggleLink(); \ No newline at end of file diff --git a/app/views/admin/budget_investments/_investments.html.erb b/app/views/admin/budget_investments/_investments.html.erb new file mode 100644 index 000000000..c82b0044a --- /dev/null +++ b/app/views/admin/budget_investments/_investments.html.erb @@ -0,0 +1,77 @@ +

    <%= page_entries_info @investments %>

    + + + + + + + + + + + + + + + + <% @investments.each do |investment| %> + + + + + + + + + + + <% end %> +
    <%= t("admin.budget_investments.index.table_id") %><%= t("admin.budget_investments.index.table_title") %><%= t("admin.budget_investments.index.table_admin") %><%= t("admin.budget_investments.index.table_valuator") %><%= t("admin.budget_investments.index.table_geozone") %><%= t("admin.budget_investments.index.table_feasibility") %><%= t("admin.budget_investments.index.table_valuation_finished") %><%= t("admin.budget_investments.index.table_selection") %>
    + <%= investment.id %> + + <%= link_to investment.title, admin_budget_budget_investment_path(budget_id: @budget.id, id: investment.id, params: Budget::Investment.filter_params(params)) %> + + <% if investment.administrator.present? %> + <%= investment.administrator.name %> + <% else %> + <%= t("admin.budget_investments.index.no_admin_assigned") %> + <% end %> + + <% if investment.valuators.size == 0 %> + <%= t("admin.budget_investments.index.no_valuators_assigned") %> + <% else %> + <%= investment.valuators.collect(&:description_or_name).join(', ') %> + <% end %> + + <%= investment.heading.name %> + + <%= t("admin.budget_investments.index.feasibility.#{investment.feasibility}", + price: investment.formatted_price) + %> + + <%= investment.valuation_finished? ? t('shared.yes'): t('shared.no') %> + + <% if investment.selected? %> + <%= link_to toggle_selection_admin_budget_budget_investment_path(@budget, + investment, + filter: params[:filter], + page: params[:page]), + method: :patch, + remote: true, + class: "button small expanded" do %> + <%= t("admin.budget_investments.index.selected") %> + <% end %> + <% elsif investment.feasible? && investment.valuation_finished? %> + <%= link_to toggle_selection_admin_budget_budget_investment_path(@budget, + investment, + filter: params[:filter], + page: params[:page]), + method: :patch, + remote: true, + class: "button small hollow expanded" do %> + <%= t("admin.budget_investments.index.select") %> + <% end %> + <% end %> +
    + +<%= paginate @investments %> diff --git a/app/views/admin/budget_investments/_written_by_author.html.erb b/app/views/admin/budget_investments/_written_by_author.html.erb new file mode 100644 index 000000000..b2c46c6f5 --- /dev/null +++ b/app/views/admin/budget_investments/_written_by_author.html.erb @@ -0,0 +1,36 @@ +
    + <%= t "admin.budget_investments.show.info", budget_name: @budget.name, group_name: @investment.group.name, id: @investment.id %> +
    + +
    +

    <%= @investment.title %>

    + +
    +
    +

    : <%= @investment.group.name %>"> + <%= t("admin.budget_investments.show.heading") %>: + <%= @investment.heading.name %> +

    +
    + +
    +

    + <%= t("admin.budget_investments.show.by") %>: + <%= link_to @investment.author.name, admin_user_path(@investment.author) %> +

    +
    + +
    +

    + <%= t("admin.budget_investments.show.sent") %>: + <%= l @investment.created_at, format: :datetime %> +

    +
    + +
    + +<% if @investment.external_url.present? %> +

    <%= text_with_links @investment.external_url %> 

    +<% end %> + +<%= safe_html_with_links @investment.description %> diff --git a/app/views/admin/budget_investments/edit.html.erb b/app/views/admin/budget_investments/edit.html.erb new file mode 100644 index 000000000..17dd88665 --- /dev/null +++ b/app/views/admin/budget_investments/edit.html.erb @@ -0,0 +1,70 @@ +<%= link_to admin_budget_budget_investment_path(@budget, @investment, Budget::Investment.filter_params(params)), class: 'back' do %> + <%= t("shared.back") %> +<% end %> + +<%= form_for @investment, + url: admin_budget_budget_investment_path(@budget, @investment) do |f| %> + + <% Budget::Investment.filter_params(params).each do |filter_name, filter_value| %> + <%= hidden_field_tag filter_name, filter_value %> + <% end %> + +
    +
    + <%= f.text_field :title, maxlength: Budget::Investment.title_max_length %> +
    + +
    + <%= f.cktext_area :description, maxlength: Budget::Investment.description_max_length, ckeditor: { language: I18n.locale } %> +
    + +
    + <%= f.text_field :external_url %> +
    + +
    + <%= f.select :heading_id, budget_heading_select_options(@budget), include_blank: t("admin.budget_investments.edit.select_heading") %> +
    +
    + +

    <%= t("admin.budget_investments.edit.classification") %>

    + +
    + +
    + <%= f.select(:administrator_id, + @admins.collect{ |a| [a.name_and_email, a.id ] }, + { include_blank: t("admin.budget_investments.edit.undefined") }) %> +
    + + +
    + <%= f.label :tag_list, t("admin.budget_investments.edit.tags") %> +
    + <% @tags.each do |tag| %> + <%= tag.name %> + <% end %> +
    + <%= f.text_field :valuation_tag_list, + value: @investment.tag_list_on(:valuation).sort.join(','), + label: false, + placeholder: t("admin.budget_investments.edit.tags_placeholder"), + class: 'js-tag-list' %> +
    + +
    + <%= f.label :valuator_ids, t("admin.budget_investments.edit.assigned_valuators") %> + + <%= f.collection_check_boxes :valuator_ids, @valuators, :id, :email do |b| %> + <%= b.label(title: valuator_label(b.object)) { b.check_box + truncate(b.object.description_or_email, length: 60) } %> + <% end %> +
    +
    + +

    + <%= f.submit(class: "button", value: t("admin.budget_investments.edit.submit_button")) %> +

    +<% end %> + +
    +<%# render 'valuation/budget_investments/written_by_valuators' %> diff --git a/app/views/admin/budget_investments/index.html.erb b/app/views/admin/budget_investments/index.html.erb new file mode 100644 index 000000000..126a933b7 --- /dev/null +++ b/app/views/admin/budget_investments/index.html.erb @@ -0,0 +1,44 @@ +

    <%= @budget.name %> - <%= t("admin.budget_investments.index.title") %>

    + +
    + <%= form_tag admin_budget_budget_investments_path(budget: @budget), method: :get, enforce_utf8: false do %> +
    + <%= select_tag :administrator_id, + options_for_select(admin_select_options, params[:administrator_id]), + { prompt: t("admin.budget_investments.index.administrator_filter_all"), + label: false, + class: "js-submit-on-change" } %> +
    + +
    + <%= select_tag :valuator_id, + options_for_select(valuator_select_options, params[:valuator_id]), + { prompt: t("admin.budget_investments.index.valuator_filter_all"), + label: false, + class: "js-submit-on-change" } %> +
    + +
    + <%= select_tag :heading_id, + options_for_select(budget_heading_select_options(@budget), params[:heading_id]), + { prompt: t("admin.budget_investments.index.heading_filter_all"), + label: false, + class: "js-submit-on-change" } %> +
    + +
    + <%= select_tag :tag_name, + options_for_select(spending_proposal_tags_select_options, params[:tag_name]), + { prompt: t("admin.budget_investments.index.tags_filter_all"), + label: false, + class: "js-submit-on-change" } %> +
    + <% end %> +
    + +<%= render '/shared/filter_subnav', i18n_namespace: "admin.budget_investments.index" %> + +
    + <%= render '/admin/budget_investments/investments' %> +
    + diff --git a/app/views/admin/budget_investments/show.html.erb b/app/views/admin/budget_investments/show.html.erb new file mode 100644 index 000000000..a58110c94 --- /dev/null +++ b/app/views/admin/budget_investments/show.html.erb @@ -0,0 +1,49 @@ +<%= link_to admin_budget_budget_investments_path(Budget::Investment.filter_params(params)), data: {no_turbolink: true} do %> + <%= t("shared.back") %> +<% end %> + +<%= render 'written_by_author' %> + +<%= link_to t("admin.budget_investments.show.edit"), + edit_admin_budget_budget_investment_path(@budget, @investment, + Budget::Investment.filter_params(params)) %> + +
    + +

    <%= t("admin.budget_investments.show.classification") %>

    + +

    <%= t("admin.budget_investments.show.assigned_admin") %>: + <%= @investment.administrator.try(:name_and_email) || t("admin.budget_investments.show.undefined") %> +

    + +

    + <%= t("admin.budget_investments.show.tags") %>: + + <%= @investment.tags_on(:valuation).pluck(:name).join(', ') %> +

    + +

    + <%= t("admin.budget_investments.show.assigned_valuators") %>: + <% if @investment.valuators.any? %> + <%= @investment.valuators.collect(&:name_and_email).join(', ') %> + <% else %> + <%= t("admin.budget_investments.show.undefined") %> + <% end %> +

    + +

    + <%= link_to t("admin.budget_investments.show.edit_classification"), + edit_admin_budget_budget_investment_path(@budget, @investment, + {anchor: 'classification'}.merge(Budget::Investment.filter_params(params))) %> +

    + +
    + +

    <%= t("admin.budget_investments.show.dossier") %>

    + +<%= render 'valuation/budget_investments/written_by_valuators' %> + +

    + <%= link_to t("admin.budget_investments.show.edit_dossier"), edit_valuation_budget_budget_investment_path(@budget, @investment) %> +

    + diff --git a/app/views/admin/budget_investments/toggle_selection.js.erb b/app/views/admin/budget_investments/toggle_selection.js.erb new file mode 100644 index 000000000..dc3a8d67a --- /dev/null +++ b/app/views/admin/budget_investments/toggle_selection.js.erb @@ -0,0 +1 @@ +$("#investments").html('<%= j render("admin/budget_investments/investments") %>'); diff --git a/app/views/admin/budgets/_form.html.erb b/app/views/admin/budgets/_form.html.erb new file mode 100644 index 000000000..5d323b45c --- /dev/null +++ b/app/views/admin/budgets/_form.html.erb @@ -0,0 +1,20 @@ +<%= form_for [:admin, @budget] do |f| %> + + <%= f.text_field :name, maxlength: Budget.title_max_length %> + + <% Budget::PHASES.each do |phase| %> +
    + <%= f.cktext_area "description_#{phase}", maxlength: Budget.description_max_length, ckeditor: { language: I18n.locale } %> +
    + <% end %> + +
    +
    + <%= f.select :phase, budget_phases_select_options %> +
    +
    + <%= f.select :currency_symbol, budget_currency_symbol_select_options %> +
    +
    + <%= f.submit nil, class: "button success" %> +<% end %> diff --git a/app/views/admin/budgets/_group.html.erb b/app/views/admin/budgets/_group.html.erb new file mode 100644 index 000000000..422834f61 --- /dev/null +++ b/app/views/admin/budgets/_group.html.erb @@ -0,0 +1,67 @@ + + + + + + + <% if headings.blank? %> + + + + + <% else %> + + + + + + + <% end %> + + + + + + + + <% headings.each do |heading| %> + + + + + <% end %> + + +
    + <%= group.name %> + <%= link_to t("admin.budgets.form.add_heading"), "#", class: "button float-right js-toggle-link", data: { "toggle-selector" => "#group-#{group.id}-new-heading-form" } %> +
    +
    + <%= t("admin.budgets.form.no_heading") %> +
    +
    <%= t("admin.budgets.form.table_heading") %><%= t("admin.budgets.form.table_amount") %>
    + <%= heading.name %> + + <%= heading.price %> +
    + diff --git a/app/views/admin/budgets/_groups.html.erb b/app/views/admin/budgets/_groups.html.erb new file mode 100644 index 000000000..537659fec --- /dev/null +++ b/app/views/admin/budgets/_groups.html.erb @@ -0,0 +1,32 @@ +

    <%= t('admin.budgets.show.groups', count: groups.count) %>

    +<% if groups.blank? %> +
    + <%= t("admin.budgets.form.no_groups") %> + <%= link_to t("admin.budgets.form.add_group"), "#", + class: "js-toggle-link", + data: { "toggle-selector" => "#new-group-form" } %> +
    +<% else %> + <%= link_to t("admin.budgets.form.add_group"), "#", class: "button float-right js-toggle-link", data: { "toggle-selector" => "#new-group-form" } %> +<% end %> + +<%= form_for [:admin, @budget, Budget::Group.new], html: {id: "new-group-form", style: "display:none"}, remote: true do |f| %> +
    + + + + <%= f.text_field :name, + label: false, + maxlength: 50, + placeholder: t("admin.budgets.form.group") %> +
    + <%= f.submit t("admin.budgets.form.create_group"), class: "button success" %> +
    +
    +<% end %> + +<% groups.each do |group| %> +
    + <%= render "admin/budgets/group", group: group, headings: group.headings %> +
    +<% end %> diff --git a/app/views/admin/budgets/edit.html.erb b/app/views/admin/budgets/edit.html.erb new file mode 100644 index 000000000..94da65a31 --- /dev/null +++ b/app/views/admin/budgets/edit.html.erb @@ -0,0 +1,9 @@ +<%= back_link_to admin_budgets_path %> + +
    +
    +

    <%= t("admin.budgets.edit.title") %>

    + + <%= render '/admin/budgets/form' %> +
    +
    diff --git a/app/views/admin/budgets/index.html.erb b/app/views/admin/budgets/index.html.erb new file mode 100644 index 000000000..87aaa8c66 --- /dev/null +++ b/app/views/admin/budgets/index.html.erb @@ -0,0 +1,44 @@ +

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

    + +<%= link_to t("admin.budgets.index.new_link"), + new_admin_budget_path, + class: "button float-right margin-right" %> + +<%= render 'shared/filter_subnav', i18n_namespace: "admin.budgets.index" %> + +

    <%= page_entries_info @budgets %>

    + + + + + + + + + + + + + <% @budgets.each do |budget| %> + + + + + + + + <% end %> + +
    <%= t("admin.budgets.index.table_name") %><%= t("admin.budgets.index.table_phase") %><%= t("admin.budgets.index.table_investments") %><%= t("admin.budgets.index.table_edit_groups") %><%= t("admin.budgets.index.table_edit_budget") %>
    + <%= budget.name %> + + <%= t("budgets.phase.#{budget.phase}") %> + + <%= link_to t("admin.budgets.index.budget_investments"), admin_budget_budget_investments_path(budget_id: budget.id) %> + + <%= link_to t("admin.budgets.index.edit_groups"), admin_budget_path(budget) %> + + <%= link_to t("admin.budgets.index.edit_budget"), edit_admin_budget_path(budget) %> +
    + +<%= paginate @budgets %> \ No newline at end of file diff --git a/app/views/admin/budgets/new.html.erb b/app/views/admin/budgets/new.html.erb new file mode 100644 index 000000000..3fa0fac89 --- /dev/null +++ b/app/views/admin/budgets/new.html.erb @@ -0,0 +1,7 @@ +
    +
    +

    <%= t("admin.budgets.new.title") %>

    + + <%= render '/admin/budgets/form' %> +
    +
    diff --git a/app/views/admin/budgets/show.html.erb b/app/views/admin/budgets/show.html.erb new file mode 100644 index 000000000..6c133ab9f --- /dev/null +++ b/app/views/admin/budgets/show.html.erb @@ -0,0 +1,7 @@ +<%= back_link_to admin_budgets_path %> + +

    <%= @budget.name %>

    + +
    + <%= render "groups", groups: @budget.groups %> +
    diff --git a/app/views/admin/shared/_budget_investment_search.html.erb b/app/views/admin/shared/_budget_investment_search.html.erb new file mode 100644 index 000000000..410044c10 --- /dev/null +++ b/app/views/admin/shared/_budget_investment_search.html.erb @@ -0,0 +1,25 @@ +<%= form_for(Budget::Investment.new, url: url, as: :budget_investment, method: :get) do |f| %> +
    +
    + <%= text_field_tag :search, "" %> +
    +
    + <%= select_tag :heading_id, + options_for_select(budget_heading_select_options(@budget), + params[:heading_id]), + include_blank: true + %> +
    +
    +
    +
    + <%= check_box_tag :unfeasible, "1", params[:unfeasible].present? %> +
    +
    + +
    +
    + <%= f.submit t("shared.search"), class: "button" %> +
    +
    +<% end %> diff --git a/app/views/budgets/ballot/_ballot.html.erb b/app/views/budgets/ballot/_ballot.html.erb new file mode 100644 index 000000000..b83005185 --- /dev/null +++ b/app/views/budgets/ballot/_ballot.html.erb @@ -0,0 +1,54 @@ +
    +
    + <%= render 'shared/back_link' %> + +

    <%= t("budgets.ballots.show.title") %>

    + +
    +

    + <%= t("budgets.ballots.show.voted_html", + count: @ballot.investments.count) %> +

    +

    + + <%= t("budgets.ballots.show.voted_info_html") %> + +

    +
    +
    +
    + +
    +
    + <% @ballot.groups.each do |group| %> +
    +

    + <%= group.name %> - <%= group.headings.first.name %> +

    + <% if @ballot.has_lines_in_group?(group) %> +

    + <%= t("budgets.ballots.show.amount_spent") %> + + <%= @ballot.formatted_amount_spent(@ballot.heading_for_group(group)) %> + +

    + <% else %> +

    + <%= t("budgets.ballots.show.zero") %>
    +

    + <% end %> + +
      + <%= render partial: 'budgets/ballot/investment', + collection: @ballot.investments.by_group(group.id) %> +
    + +

    + <%= t("budgets.ballots.show.remaining", + amount: @ballot.formatted_amount_available(@ballot.heading_for_group(group))).html_safe %> +

    +
    + <% end %> +
    +
    diff --git a/app/views/budgets/ballot/_investment.html.erb b/app/views/budgets/ballot/_investment.html.erb new file mode 100644 index 000000000..7f2bafcb8 --- /dev/null +++ b/app/views/budgets/ballot/_investment.html.erb @@ -0,0 +1,14 @@ +
  • + <%= link_to investment.title, budget_investment_path(@budget, investment) %> + <%= investment.formatted_price %> + + <% if @budget.balloting? %> + <%= link_to budget_ballot_line_path(@budget, id: investment.id), + title: t('budgets.ballots.show.remove'), + class: "remove-investment-project", + method: :delete, + remote: true do %> + + <% end %> + <% end %> +
  • diff --git a/app/views/budgets/ballot/_investment_for_sidebar.html.erb b/app/views/budgets/ballot/_investment_for_sidebar.html.erb new file mode 100644 index 000000000..059951253 --- /dev/null +++ b/app/views/budgets/ballot/_investment_for_sidebar.html.erb @@ -0,0 +1,16 @@ +
  • + <%= investment.title %> + <%= investment.formatted_price %> + + <% if @budget.balloting? %> + <%= link_to budget_ballot_line_url(id: investment.id, + investments_ids: investment_ids), + title: t('budgets.ballots.show.remove'), + class: "remove-investment-project", + method: :delete, + remote: true do %> + <%= t('budgets.ballots.show.remove') %> + + <% end %> + <% end %> +
  • diff --git a/app/views/budgets/ballot/_progress_bar.html.erb b/app/views/budgets/ballot/_progress_bar.html.erb new file mode 100644 index 000000000..ec9d28457 --- /dev/null +++ b/app/views/budgets/ballot/_progress_bar.html.erb @@ -0,0 +1,29 @@ + + <%= @budget.formatted_heading_price(@heading) %> + + +
    +
    +
    +
    + +
    + +

    + <%= @ballot.formatted_amount_spent(@heading) %> + + <%= t("budgets.progress_bar.available") %> + <%= @ballot.formatted_amount_available(@heading) %> + +

    +
    +
    diff --git a/app/views/budgets/ballot/lines/_refresh_ballots.js.erb b/app/views/budgets/ballot/lines/_refresh_ballots.js.erb new file mode 100644 index 000000000..f81560d96 --- /dev/null +++ b/app/views/budgets/ballot/lines/_refresh_ballots.js.erb @@ -0,0 +1,8 @@ +<% if @investments.present? %> + <% @investments.each do |investment| %> + $("#<%= dom_id(investment) %>_ballot").html('<%= j render("/budgets/investments/ballot", + investment: investment, + investment_ids: investment_ids, + ballot: ballot) %>'); + <% end %> +<% end %> diff --git a/app/views/budgets/ballot/lines/create.js.erb b/app/views/budgets/ballot/lines/create.js.erb new file mode 100644 index 000000000..b1c9f76be --- /dev/null +++ b/app/views/budgets/ballot/lines/create.js.erb @@ -0,0 +1,11 @@ +$("#progress_bar").html('<%= j render("/budgets/ballot/progress_bar", ballot: @ballot) %>'); +$("#sidebar").html('<%= j render("/budgets/investments/sidebar") %>'); +$("#<%= dom_id(@investment) %>_ballot").html('<%= j render("/budgets/investments/ballot", + investment: @investment, + investment_ids: @investment_ids, + ballot: @ballot) %>'); + +<%= render 'refresh_ballots', + investment: @investment, + investment_ids: @investment_ids, + ballot: @ballot %> diff --git a/app/views/budgets/ballot/lines/destroy.js.erb b/app/views/budgets/ballot/lines/destroy.js.erb new file mode 100644 index 000000000..82c303047 --- /dev/null +++ b/app/views/budgets/ballot/lines/destroy.js.erb @@ -0,0 +1,12 @@ +$("#progress_bar").html('<%= j render("budgets/ballot/progress_bar", ballot: @ballot) %>'); +$("#sidebar").html('<%= j render("budgets/investments/sidebar") %>'); +$("#ballot").html('<%= j render("budgets/ballot/ballot") %>') + +$("#<%= dom_id(@investment) %>_ballot").html('<%= j render("/budgets/investments/ballot", + investment: @investment, + investment_ids: @investment_ids, + ballot: @ballot) %>'); +<%= render 'refresh_ballots', + investment: @investment, + investment_ids: @investment_ids, + ballot: @ballot %> diff --git a/app/views/budgets/ballot/lines/new.js.erb b/app/views/budgets/ballot/lines/new.js.erb new file mode 100644 index 000000000..2e8254866 --- /dev/null +++ b/app/views/budgets/ballot/lines/new.js.erb @@ -0,0 +1,2 @@ +$("#<%= dom_id(@spending_proposal) %>_ballot").html('<%= j render("spending_proposals/ballot", spending_proposal: @spending_proposal) %>'); +$(".no-supports-allowed").show(); \ No newline at end of file diff --git a/app/views/budgets/ballot/show.html.erb b/app/views/budgets/ballot/show.html.erb new file mode 100644 index 000000000..b4aaa2d46 --- /dev/null +++ b/app/views/budgets/ballot/show.html.erb @@ -0,0 +1,3 @@ +
    + <%= render "budgets/ballot/ballot" %> +
    diff --git a/app/views/budgets/groups/show.html.erb b/app/views/budgets/groups/show.html.erb new file mode 100644 index 000000000..8bfbc21a1 --- /dev/null +++ b/app/views/budgets/groups/show.html.erb @@ -0,0 +1,31 @@ +
    +
    +
    + <%= back_link_to budget_path(@budget) %> +

    <%= t("budgets.groups.show.title") %>

    +
    +
    +
    + +
    +
    +
    + <% @group.headings.each_slice(7) do |slice| %> +
    + <% slice.each do |heading| %> + + <%= link_to heading.name, + budget_investments_path(heading_id: heading.id), + data: { no_turbolink: true } %>
    +
    + <% end %> +
    + <% end %> +
    +
    + +
    + <%= image_tag "map.jpg" %> +
    +
    diff --git a/app/views/budgets/index.html.erb b/app/views/budgets/index.html.erb new file mode 100644 index 000000000..73fc3a697 --- /dev/null +++ b/app/views/budgets/index.html.erb @@ -0,0 +1,30 @@ +
    +
    +
    +

    <%= t('budgets.index.title') %>

    +
    +
    +
    + +
    +
    + + + + + + + <% @budgets.each do |budget| %> + + + + + <% end %> + +
    <%= Budget.human_attribute_name(:name) %><%= Budget.human_attribute_name(:phase) %>
    + <%= link_to budget.name, budget %> + + <%= budget.translated_phase %> +
    +
    +
    diff --git a/app/views/budgets/investments/_ballot.html.erb b/app/views/budgets/investments/_ballot.html.erb new file mode 100644 index 000000000..0ad43ab43 --- /dev/null +++ b/app/views/budgets/investments/_ballot.html.erb @@ -0,0 +1,59 @@ +<% reason = investment.reason_for_not_being_ballotable_by(current_user, ballot) %> +
    + <% if ballot.has_investment?(investment) %> + +
    + "> + +

    + <%= investment.formatted_price %> +

    + <% if investment.should_show_ballots? %> + <%= link_to t('budgets.ballots.show.remove'), + budget_ballot_line_path(id: investment.id, + budget_id: investment.budget_id, + investments_ids: investment_ids), + class: "delete small expanded", + method: :delete, + remote: true %> + <% end %> +
    + + <% else %> + +
    +

    + <%= investment.formatted_price %> +

    + <% if investment.should_show_ballots? %> + <%= link_to t("budgets.investments.investment.add"), + budget_ballot_lines_url(investment_id: investment.id, + budget_id: investment.budget_id, + investments_ids: investment_ids), + class: "button button-support small expanded", + title: t('budgets.investments.investment.support_title'), + method: :post, + remote: true %> + <% end %> +
    + + <% end %> + + <% if reason.present? && !ballot.has_investment?(investment) %> + + + + <% end %> +
    diff --git a/app/views/budgets/investments/_categories.html.erb b/app/views/budgets/investments/_categories.html.erb new file mode 100644 index 000000000..ad628b1b1 --- /dev/null +++ b/app/views/budgets/investments/_categories.html.erb @@ -0,0 +1,11 @@ + + +
    + + \ No newline at end of file diff --git a/app/views/budgets/investments/_form.html.erb b/app/views/budgets/investments/_form.html.erb new file mode 100644 index 000000000..12f91f486 --- /dev/null +++ b/app/views/budgets/investments/_form.html.erb @@ -0,0 +1,69 @@ +<%= form_for(@investment, url: form_url, method: :post) do |f| %> + <%= render 'shared/errors', resource: @investment %> + +
    +
    + <%= f.select :heading_id, budget_heading_select_options(@budget), {include_blank: true, } %> +
    + +
    + <%= f.text_field :title, maxlength: SpendingProposal.title_max_length %> +
    + + <%= f.invisible_captcha :subtitle %> + +
    + <%= f.cktext_area :description, maxlength: SpendingProposal.description_max_length, ckeditor: { language: I18n.locale } %> +
    + +
    + <%= f.text_field :external_url %> +
    + +
    + <%= f.text_field :location %> +
    + +
    + <%= f.text_field :organization_name %> +
    + +
    + <%= f.label :tag_list, t("budgets.investments.form.tags_label") %> +

    <%= t("budgets.investments.form.tags_instructions") %>

    + +
    + <%= f.label :category_tag_list, t("budgets.investments.form.tag_category_label") %> + <% @categories.each do |tag| %> + <%= tag.name %> + <% end %> +
    + +
    + <%= f.text_field :tag_list, value: @investment.tag_list.to_s, + label: false, + placeholder: t("budgets.investments.form.tags_placeholder"), + class: 'js-tag-list' %> +
    + + + <% unless current_user.manager? %> + +
    + <%= f.label :terms_of_service do %> + <%= f.check_box :terms_of_service, title: t('form.accept_terms_title'), label: false %> + + <%= t("form.accept_terms", + policy: link_to(t("form.policy"), "/privacy", target: "blank"), + conditions: link_to(t("form.conditions"), "/conditions", target: "blank")).html_safe %> + + <% end %> +
    + + <% end %> + +
    + <%= f.submit(nil, class: "button expanded") %> +
    +
    +<% end %> diff --git a/app/views/budgets/investments/_header.html.erb b/app/views/budgets/investments/_header.html.erb new file mode 100644 index 000000000..b58eea03c --- /dev/null +++ b/app/views/budgets/investments/_header.html.erb @@ -0,0 +1,58 @@ +<% if @heading.present? %> +
    +
    + +
    +
    + <%= back_link_to budget_group_path(@budget, @heading.group) %> + + <% if can? :show, @ballot %> + <%= link_to t("budgets.investments.header.check_ballot"), + budget_ballot_path(@budget), + class: "button float-right" %> + <% end %> +
    +
    + +
    +
    +
    +

    <%= @heading.name %>

    + <% if can? :show, @ballot %> + + <% if @ballot.valid_heading?(@heading) %> +
    + <%= render 'budgets/ballot/progress_bar' %> +
    + <% else %> +
    +

    + <%= t("budgets.investments.header.different_heading_assigned_html", + heading_link: link_to( + @assigned_heading.name, + budget_investments_path(@budget, heading: @assigned_heading)) + ) %> +

    + <% end %> + <% else %> +

    <%= @budget.formatted_heading_price(@heading) %>

    + <% end %> +
    +
    +
    +
    +
    +<% else %> +
    +
    +
    + <%= back_link_to budget_path(@budget) %> + +

    <%= t('budgets.investments.index.title') %>

    +
    +
    +
    +<% end %> diff --git a/app/views/budgets/investments/_investment.html.erb b/app/views/budgets/investments/_investment.html.erb new file mode 100644 index 000000000..c9c247784 --- /dev/null +++ b/app/views/budgets/investments/_investment.html.erb @@ -0,0 +1,75 @@ +
    +
    +
    + +
    +
    + + <% cache [locale_and_user_status(investment), 'index', investment, investment.author] do %> + <%= t("budgets.investments.investment.title") %> + +

    <%= link_to investment.title, namespaced_budget_investment_path(investment) %>

    +

    + + <%= l investment.created_at.to_date %> + + <% if investment.author.hidden? || investment.author.erased? %> +  •  + + <%= t("budgets.investments.show.author_deleted") %> + + <% else %> +  •  + + <%= investment.author.name %> + + <% if investment.author.official? %> +  •  + + <%= investment.author.official_position %> + + <% end %> + <% end %> + +  •  + <%= investment.heading.name %> +

    +
    +

    <%= investment.description %>

    +
    +
    + <%= render "shared/tags", taggable: investment, limit: 5 %> + <% end %> +
    +
    + + <% unless investment.unfeasible? %> + + <% if investment.should_show_votes? %> + +
    + <%= render partial: '/budgets/investments/votes', locals: { + investment: investment, + investment_votes: investment_votes, + vote_url: namespaced_budget_investment_vote_path(investment, value: 'yes') + } %> +
    + + <% elsif investment.should_show_ballots? %> + +
    + <%= render partial: '/budgets/investments/ballot', locals: { + investment: investment, + investment_ids: investment_ids, + ballot: ballot + } %> +
    + + <% end %> + + <% end %> +
    +
    +
    diff --git a/app/views/budgets/investments/_investment_show.html.erb b/app/views/budgets/investments/_investment_show.html.erb new file mode 100644 index 000000000..12f23fe87 --- /dev/null +++ b/app/views/budgets/investments/_investment_show.html.erb @@ -0,0 +1,102 @@ +
    + +
    +
    + <%= back_link_to budget_investments_path(investment.budget) %> + +

    <%= investment.title %>

    + +
    + <%= render '/shared/author_info', resource: investment %> + +  •  + <%= l investment.created_at.to_date %> +  •  + <%= investment.heading.name %> +
    + +
    +

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

    + + <% 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 %> + + <%= render 'shared/tags', taggable: investment %> + + <%= safe_html_with_links investment.description.html_safe %> + + <% if investment.external_url.present? %> + + <% end %> + + <% if investment.unfeasible? && investment.unfeasibility_explanation.present? %> +

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

    +

    <%= investment.unfeasibility_explanation %>

    + <% end %> + + <% if investment.feasible? && investment.price_explanation.present? %> +

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

    +

    <%= investment.price_explanation %>

    + <% end %> +
    + + <% if investment.should_show_aside? %> + + <% else %> +
    +
    + <%= t("budgets.investments.show.title") %> + +
    +
    + <% end %> + +
    +
    diff --git a/app/views/budgets/investments/_sidebar.html.erb b/app/views/budgets/investments/_sidebar.html.erb new file mode 100644 index 000000000..9430120bc --- /dev/null +++ b/app/views/budgets/investments/_sidebar.html.erb @@ -0,0 +1,48 @@ +
    + +<% if can?(:create, Budget::Investment.new(budget: @budget)) %> + <% if current_user && current_user.level_two_or_three_verified? %> + <%= link_to t("budgets.investments.index.sidebar.create"), new_budget_investment_path, class: "button budget expanded" %> + <% else %> +
    + <%= t("budgets.investments.index.sidebar.verified_only", + verify: link_to(t("budgets.investments.index.sidebar.verify_account"), verification_path)).html_safe %> +
    + <% end %> +<% end %> + +<% if @budget.accepting? %> + <%= render "shared/tag_cloud", taggable: 'budget/investment' %> + <%= render 'categories' %> +<% end %> + + +<% if @heading && can?(:show, @ballot) %> + + + + + <% if @ballot.investments.by_heading(@heading.id).count > 0 %> +

    + + <%= t("budgets.investments.index.sidebar.voted_html", + count: @ballot.investments.by_heading(@heading.id).count, + amount_spent: @ballot.formatted_amount_spent(@heading)) %> + +

    + <% else %> +

    <%= t("budgets.investments.index.sidebar.zero") %>

    + <% end %> + + + +

    <%= t("budgets.investments.index.sidebar.voted_info") %>

    +<% end %> diff --git a/app/views/budgets/investments/_votes.html.erb b/app/views/budgets/investments/_votes.html.erb new file mode 100644 index 000000000..6d62b9a02 --- /dev/null +++ b/app/views/budgets/investments/_votes.html.erb @@ -0,0 +1,46 @@ +<% reason = investment.reason_for_not_being_selectable_by(current_user) %> +<% voting_allowed = true unless reason.presence == :not_voting_allowed %> +<% user_voted_for = voted_for?(investment_votes, investment) %> + +
    + + + <%= t("budgets.investments.investment.supports", count: investment.total_votes) %> + + +
    + <% if user_voted_for %> +
    + <%= t("budgets.investments.investment.already_supported") %> +
    + <% elsif investment.should_show_votes? %> + + <%= link_to vote_url, + class: "button button-support small expanded", + title: t('budgets.investments.investment.support_title'), + method: "post", + remote: true, + "aria-hidden" => css_for_aria_hidden(reason) do %> + <%= t("budgets.investments.investment.give_support") %> + <% end %> + <% end %> +
    + + <% if reason.present? && !user_voted_for %> + + <% end %> + + <% if user_voted_for && setting['twitter_handle'] %> + + <% end %> +
    diff --git a/app/views/budgets/investments/index.html.erb b/app/views/budgets/investments/index.html.erb new file mode 100644 index 000000000..b3bcf03fe --- /dev/null +++ b/app/views/budgets/investments/index.html.erb @@ -0,0 +1,59 @@ +<% provide :title do %><%= t('budgets.investments.index.title') %><% end %> +<% content_for :header_addon do %> + <%= render "shared/search_form", + search_path: budget_investments_path(budget_id: @budget.id, page: 1), + i18n_namespace: "budgets.investments.index.search_form" %> +<% end %> + +
    + + <% unless params[:search].present? %> + <%= render '/budgets/investments/header' %> + <% end %> + +
    +
    + +
    + + <% if params[:unfeasible].present? %> +

    <%= t("budgets.investments.index.unfeasible") %>

    +

    + <%= t("budgets.investments.index.unfeasible_text", + definitions: link_to(t("budgets.investments.index.unfeasible_text_definitions"), "https://decide.madrid.es/participatory_budget_info#20")).html_safe %> +

    + <% end %> + + <%= content_tag(:h2, t("budgets.investments.index.by_heading", heading: @heading.name)) if @heading.present? %> + <% if params[:search].present? %> +

    + <%= page_entries_info @investments %> + <%= t("budgets.investments.index.search_results", count: @investments.size, search_term: params[:search]) %> +

    + <% end %> +
    + + <%= render('shared/order_links', i18n_namespace: "budgets.investments.index") unless params[:unfeasible].present? %> + + <% @investments.each do |investment| %> + <%= render partial: '/budgets/investments/investment', locals: { + investment: investment, + investment_ids: @investment_ids, + investment_votes: @investment_votes, + ballot: @ballot + } %> + <% end %> + + <%= paginate @investments %> +
    + +
    + +
    + +
    +
    diff --git a/app/views/budgets/investments/new.html.erb b/app/views/budgets/investments/new.html.erb new file mode 100644 index 000000000..457fbd550 --- /dev/null +++ b/app/views/budgets/investments/new.html.erb @@ -0,0 +1,7 @@ +
    +
    +

    <%= t("management.budget_investments.create") %>

    + + <%= render '/budgets/investments/form', form_url: budget_investments_path(@budget) %> +
    +
    diff --git a/app/views/budgets/investments/show.html.erb b/app/views/budgets/investments/show.html.erb new file mode 100644 index 000000000..be33605b0 --- /dev/null +++ b/app/views/budgets/investments/show.html.erb @@ -0,0 +1,10 @@ +<% provide :title do %><%= @investment.title %><% end %> + +<%= render partial: '/budgets/investments/investment_show', locals: { + investment: @investment, + investment_ids: @investment_ids, + investment_votes: @investment_votes, + ballot: @ballot +} %> + +<%= render partial: '/comments/comment_tree', locals: { comment_tree: @comment_tree, comment_flags: @comment_flags } %> diff --git a/app/views/budgets/investments/vote.js.erb b/app/views/budgets/investments/vote.js.erb new file mode 100644 index 000000000..56248ba68 --- /dev/null +++ b/app/views/budgets/investments/vote.js.erb @@ -0,0 +1,4 @@ +$("#<%= dom_id(@investment) %>_votes").html('<%= j render("/budgets/investments/votes", + investment: @investment, + investment_votes: @investment_votes, + vote_url: namespaced_budget_investment_vote_path(@investment, value: 'yes')) %>'); diff --git a/app/views/budgets/show.html.erb b/app/views/budgets/show.html.erb new file mode 100644 index 000000000..499b7eb58 --- /dev/null +++ b/app/views/budgets/show.html.erb @@ -0,0 +1,63 @@ +
    +
    +
    + <%= back_link_to budgets_path %> + +

    <%= @budget.name %>

    + + <%= @budget.description %> +
    +
    +

    + <%= t('budgets.show.phase') %> +
    + <%= t("budgets.phase.#{@budget.phase}") %> +

    + + <% if can?(:create, Budget::Investment.new(budget: @budget))%> + <% if current_user %> + <% if current_user.level_two_or_three_verified? %> + <%= link_to t("budgets.investments.index.sidebar.create"), new_budget_investment_path(@budget), class: "button margin-top expanded" %> + <% else %> +
    + <%= t("budgets.investments.index.sidebar.verified_only", + verify: link_to(t("budgets.investments.index.sidebar.verify_account"), verification_path)).html_safe %> +
    + <% end %> + <% else %> +
    + <%= t("budgets.investments.index.sidebar.not_logged_in", + sign_in: link_to(t("budgets.investments.index.sidebar.sign_in"), new_user_session_path), + sign_up: link_to(t("budgets.investments.index.sidebar.sign_up"), new_user_registration_path)).html_safe %> +
    + <% end %> + <% end %> +
    +
    +
    + +
    +
    + + + + + + <% @budget.groups.each do |group| %> + + + + <% end %> + +
    <%= t('budgets.show.group') %>
    + <% if group.headings.count == 1 %> + <%= link_to group.name, + budget_investments_path(@budget, heading_id: group.headings.first.id), + data: { no_turbolink: true } %> + <% else %> + <%= link_to group.name, budget_group_path(@budget, group) %> + <% end %> +
    +
    +
    +
    diff --git a/app/views/comments/_comment.html.erb b/app/views/comments/_comment.html.erb index 46039f65d..4960baba8 100644 --- a/app/views/comments/_comment.html.erb +++ b/app/views/comments/_comment.html.erb @@ -1,4 +1,5 @@ -<% cache [locale_and_user_status(comment), comment, commentable_cache_key(comment.commentable), comment.author, (@comment_flags[comment.id] if @comment_flags)] do %> +<% comment_flags ||= @comment_flags %> +<% cache [locale_and_user_status(comment), comment, commentable_cache_key(comment.commentable), comment.author, (comment_flags[comment.id] if comment_flags)] do %>