As far as possible I think the code is clearer if we use CRUD actions rather than custom actions. This will make it easier to add the action to remove votes in the next commit. Note that we are adding this line as we need to validate it that a vote can be created on a debate by the current user: ```authorize! :create, Vote.new(voter: current_user, votable: @debate)``` We have done it this way and not with the following code as you might expect, as this way two votes are created instead of one. ```load_and_authorize_resource through: :debate, through_association: :votes_for``` This line tries to load the resource @debate and through the association "votes_for" it tries to create a new vote associated to that debate. Therefore a vote is created when trying to authorise the resource and then another one in the create action, when calling @debate.vote_by (which is called by @debate.register_vote).
414 lines
15 KiB
Ruby
414 lines
15 KiB
Ruby
class Budget
|
|
class Investment < ApplicationRecord
|
|
SORTING_OPTIONS = { id: "id", supports: "cached_votes_up" }.freeze
|
|
|
|
include Measurable
|
|
include Sanitizable
|
|
include Taggable
|
|
include Searchable
|
|
include Reclassification
|
|
include Followable
|
|
include Communitable
|
|
include Imageable
|
|
include Mappable
|
|
include Documentable
|
|
include SDG::Relatable
|
|
|
|
acts_as_taggable_on :valuation_tags
|
|
acts_as_votable
|
|
acts_as_paranoid column: :hidden_at
|
|
include ActsAsParanoidAliases
|
|
include Relationable
|
|
include Notifiable
|
|
include Filterable
|
|
include Flaggable
|
|
include Milestoneable
|
|
include Randomizable
|
|
|
|
translates :title, touch: true
|
|
translates :description, touch: true
|
|
include Globalizable
|
|
|
|
audited on: [:update, :destroy]
|
|
has_associated_audits
|
|
translation_class.class_eval do
|
|
audited associated_with: :globalized_model,
|
|
only: Budget::Investment.translated_attribute_names
|
|
end
|
|
|
|
belongs_to :author, -> { with_hidden }, class_name: "User", inverse_of: :budget_investments
|
|
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 :valuator_group_assignments, dependent: :destroy
|
|
has_many :valuator_groups, through: :valuator_group_assignments
|
|
|
|
has_many :comments, -> { where(valuation: false) }, as: :commentable, inverse_of: :commentable
|
|
has_one :summary_comment, as: :commentable, class_name: "MlSummaryComment", dependent: :destroy
|
|
has_many :valuations, -> { where(valuation: true) },
|
|
as: :commentable,
|
|
inverse_of: :commentable,
|
|
class_name: "Comment"
|
|
|
|
validates_translation :title, presence: true, length: { in: 4..Budget::Investment.title_max_length }
|
|
validates_translation :description, presence: true,
|
|
length: { maximum: Budget::Investment.description_max_length }
|
|
|
|
validates :author, presence: true
|
|
validates :heading_id, presence: true
|
|
validates :unfeasibility_explanation, presence: { if: :unfeasibility_explanation_required? }
|
|
validates :price, presence: { if: :price_required? }
|
|
validates :terms_of_service, acceptance: { allow_nil: false }, on: :create
|
|
|
|
scope :sort_by_confidence_score, -> { reorder(confidence_score: :desc, id: :desc) }
|
|
scope :sort_by_ballots, -> { reorder(ballot_lines_count: :desc, id: :desc) }
|
|
scope :sort_by_price, -> { reorder(price: :desc, confidence_score: :desc, id: :desc) }
|
|
scope :sort_by_id, -> { order("id DESC") }
|
|
scope :sort_by_supports, -> { order("cached_votes_up DESC") }
|
|
|
|
scope :valuation_open, -> { where(valuation_finished: false) }
|
|
scope :with_admin, -> { where.not(administrator_id: nil) }
|
|
scope :without_admin, -> { where(administrator_id: nil) }
|
|
scope :without_valuator_group, -> { where(valuator_group_assignments_count: 0) }
|
|
scope :without_valuator, -> { without_valuator_group.where(valuator_assignments_count: 0) }
|
|
scope :under_valuation, -> { valuation_open.valuating.with_admin }
|
|
scope :managed, -> { valuation_open.where(valuator_assignments_count: 0).with_admin }
|
|
scope :with_valuator_assignments, -> { where("valuator_assignments_count > 0") }
|
|
scope :with_group_assignments, -> { where("valuator_group_assignments_count > 0") }
|
|
scope :with_valuation_assignments, -> { with_valuator_assignments.or(with_group_assignments) }
|
|
scope :valuating, -> { valuation_open.with_valuation_assignments }
|
|
scope :visible_to_valuators, -> { where(visible_to_valuators: true) }
|
|
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, -> { feasible.where(selected: true) }
|
|
scope :compatible, -> { where(incompatible: false) }
|
|
scope :incompatible, -> { where(incompatible: true) }
|
|
scope :winners, -> { selected.compatible.where(winner: true) }
|
|
scope :unselected, -> { not_unfeasible.where(selected: false) }
|
|
scope :last_week, -> { where("created_at >= ?", 7.days.ago) }
|
|
scope :sort_by_flags, -> { order(flags_count: :desc, updated_at: :desc) }
|
|
scope :sort_by_created_at, -> { reorder(created_at: :desc) }
|
|
|
|
scope :by_budget, ->(budget) { where(budget: budget) }
|
|
scope :by_group, ->(group_id) { where(group_id: group_id) }
|
|
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).distinct }
|
|
scope :visible_to_valuator, ->(valuator) do
|
|
visible_to_valuators.where(id: valuator&.assigned_investment_ids)
|
|
end
|
|
|
|
scope :for_render, -> { includes(:heading) }
|
|
|
|
def self.by_valuator(valuator_id)
|
|
where(budget_valuator_assignments: { valuator_id: valuator_id }).joins(:valuator_assignments)
|
|
end
|
|
|
|
def self.by_valuator_group(valuator_group_id)
|
|
joins(:valuator_group_assignments)
|
|
.where(budget_valuator_group_assignments: { valuator_group_id: valuator_group_id })
|
|
end
|
|
|
|
before_validation :set_responsible_name
|
|
before_validation :set_denormalized_ids
|
|
before_save :calculate_confidence_score
|
|
before_create :set_original_heading_id
|
|
after_save :recalculate_heading_winners
|
|
|
|
def comments_count
|
|
comments.count
|
|
end
|
|
|
|
def self.sort_by_title
|
|
all.sort_by(&:title)
|
|
end
|
|
|
|
def self.filter_params(params)
|
|
params.permit(%i[heading_id group_id administrator_id tag_name valuator_id])
|
|
end
|
|
|
|
def self.scoped_filter(params, current_filter)
|
|
budget = Budget.find_by_slug_or_id params[:budget_id]
|
|
results = Investment.by_budget(budget)
|
|
|
|
if params[:min_total_supports].present?
|
|
results = results.where("cached_votes_up + physical_votes >= ?", params[:min_total_supports])
|
|
end
|
|
if params[:max_total_supports].present?
|
|
results = results.where("cached_votes_up + physical_votes <= ?", params[:max_total_supports])
|
|
end
|
|
|
|
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_tag(params[:tag_name]) if params[:tag_name].present?
|
|
results = results.by_tag(params[:milestone_tag_name]) if params[:milestone_tag_name].present?
|
|
results = results.by_valuator(params[:valuator_id]) if params[:valuator_id].present?
|
|
results = results.by_valuator_group(params[:valuator_group_id]) if params[:valuator_group_id].present?
|
|
results = results.by_admin(params[:administrator_id]) if params[:administrator_id].present?
|
|
|
|
results = results.search_by_title_or_id(params[:title_or_id].strip) if params[:title_or_id]
|
|
results = advanced_filters(params, results) if params[:advanced_filters].present?
|
|
results = results.send(current_filter) if current_filter.present?
|
|
|
|
results.includes(:heading, :group, :budget, administrator: :user, valuators: :user)
|
|
end
|
|
|
|
def self.advanced_filters(params, results)
|
|
results = results.without_admin if params[:advanced_filters].include?("without_admin")
|
|
results = results.without_valuator if params[:advanced_filters].include?("without_valuator")
|
|
results = results.under_valuation if params[:advanced_filters].include?("under_valuation")
|
|
results = results.valuation_finished if params[:advanced_filters].include?("valuation_finished")
|
|
results = results.winners if params[:advanced_filters].include?("winners")
|
|
|
|
ids = []
|
|
ids += results.valuation_finished_feasible.ids if params[:advanced_filters].include?("feasible")
|
|
ids += results.where(selected: true).ids if params[:advanced_filters].include?("selected")
|
|
ids += results.undecided.ids if params[:advanced_filters].include?("undecided")
|
|
ids += results.unfeasible.ids if params[:advanced_filters].include?("unfeasible")
|
|
results = results.where(id: ids) if ids.any?
|
|
results
|
|
end
|
|
|
|
def self.order_filter(params)
|
|
sorting_key = params[:sort_by]&.downcase&.to_sym
|
|
allowed_sort_option = SORTING_OPTIONS[sorting_key]
|
|
direction = params[:direction] == "desc" ? "desc" : "asc"
|
|
|
|
if allowed_sort_option.present?
|
|
order("#{allowed_sort_option} #{direction}")
|
|
elsif sorting_key == :title
|
|
direction == "asc" ? sort_by_title : sort_by_title.reverse
|
|
else
|
|
order(cached_votes_up: :desc).order(id: :desc)
|
|
end
|
|
end
|
|
|
|
def self.limit_results(budget, params, results)
|
|
max_per_heading = params[:max_per_heading].to_i
|
|
return results if max_per_heading <= 0
|
|
|
|
ids = []
|
|
budget.headings.ids.each do |hid|
|
|
ids += Investment.where(heading_id: hid).order(confidence_score: :desc).limit(max_per_heading).ids
|
|
end
|
|
|
|
results.where(id: ids)
|
|
end
|
|
|
|
def self.search_by_title_or_id(title_or_id)
|
|
with_joins = with_translations(Globalize.fallbacks(I18n.locale))
|
|
|
|
with_joins.where(id: title_or_id)
|
|
.or(with_joins.where("budget_investment_translations.title ILIKE ?", "%#{title_or_id}%"))
|
|
end
|
|
|
|
def searchable_values
|
|
{
|
|
author.username => "B",
|
|
heading.name => "B",
|
|
tag_list.join(" ") => "B"
|
|
}.merge(searchable_globalized_values)
|
|
end
|
|
|
|
def self.search(terms)
|
|
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 price_required?
|
|
feasible? && valuation_finished? && budget.show_money?
|
|
end
|
|
|
|
def unfeasible_email_pending?
|
|
unfeasible_email_sent_at.blank? && unfeasible? && valuation_finished?
|
|
end
|
|
|
|
def total_votes
|
|
cached_votes_up + physical_votes
|
|
end
|
|
|
|
def code
|
|
"#{created_at.strftime("%Y")}-#{id}" + (administrator.present? ? "-A#{administrator.id}" : "")
|
|
end
|
|
|
|
def send_unfeasible_email
|
|
Mailer.budget_investment_unfeasible(self).deliver_later
|
|
update!(unfeasible_email_sent_at: Time.current)
|
|
end
|
|
|
|
def reason_for_not_being_selectable_by(user)
|
|
return permission_problem(user) if permission_problem?(user)
|
|
return :different_heading_assigned unless valid_heading?(user)
|
|
|
|
: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 :casted_offline if ballot.casted_offline?
|
|
|
|
ballot.reason_for_not_being_ballotable(self)
|
|
end
|
|
|
|
def permission_problem(user)
|
|
return :not_logged_in unless user
|
|
return :organization if user.organization?
|
|
return :not_verified unless user.level_two_or_three_verified?
|
|
|
|
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 valid_heading?(user)
|
|
voted_in?(heading, user) || can_vote_in_another_heading?(user)
|
|
end
|
|
|
|
def can_vote_in_another_heading?(user)
|
|
user.headings_voted_within_group(group).count < group.max_votable_headings
|
|
end
|
|
|
|
def voted_in?(heading, user)
|
|
user.headings_voted_within_group(group).where(id: heading.id).exists?
|
|
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 recalculate_heading_winners
|
|
Budget::Result.new(budget, heading).calculate_winners if saved_change_to_incompatible?
|
|
end
|
|
|
|
def set_responsible_name
|
|
self.responsible_name = author&.document_number if author&.document_number.present?
|
|
end
|
|
|
|
def should_show_aside?
|
|
(budget.selecting? && !unfeasible?) ||
|
|
(budget.balloting? && feasible?) ||
|
|
(budget.valuating? && !unfeasible?)
|
|
end
|
|
|
|
def should_show_votes?
|
|
budget.selecting?
|
|
end
|
|
|
|
def should_show_vote_count?
|
|
budget.valuating?
|
|
end
|
|
|
|
def should_show_ballots?
|
|
budget.balloting? && selected?
|
|
end
|
|
|
|
def should_show_price?
|
|
selected? && price.present? && budget.published_prices? && budget.show_money?
|
|
end
|
|
|
|
def should_show_price_explanation?
|
|
should_show_price? && price_explanation.present?
|
|
end
|
|
|
|
def should_show_unfeasibility_explanation?
|
|
unfeasible? && valuation_finished? && unfeasibility_explanation.present?
|
|
end
|
|
|
|
def formatted_price
|
|
budget.formatted_amount(price)
|
|
end
|
|
|
|
def self.apply_filters_and_search(_budget, params, current_filter = nil)
|
|
investments = all
|
|
investments = investments.send(current_filter) if current_filter.present?
|
|
investments = investments.by_heading(params[:heading_id]) if params[:heading_id].present?
|
|
investments = investments.search(params[:search]) if params[:search].present?
|
|
investments = investments.filter_by(params[:advanced_search])
|
|
investments
|
|
end
|
|
|
|
def assigned_valuators
|
|
valuators.map(&:description_or_name).compact.join(", ").presence
|
|
end
|
|
|
|
def assigned_valuation_groups
|
|
valuator_groups.map(&:name).compact.join(", ").presence
|
|
end
|
|
|
|
def self.with_milestone_status_id(status_id)
|
|
includes(milestones: :translations).select do |investment|
|
|
investment.milestone_status_id == status_id.to_i
|
|
end
|
|
end
|
|
|
|
def milestone_status_id
|
|
milestones.published.with_status.order_by_publication_date.last&.status_id
|
|
end
|
|
|
|
def admin_and_valuator_users_associated
|
|
valuator_users = (valuator_groups.map(&:valuators) + valuators).flatten
|
|
all_users = valuator_users << administrator
|
|
all_users.compact.uniq
|
|
end
|
|
|
|
private
|
|
|
|
def set_denormalized_ids
|
|
self.group_id = heading&.group_id if will_save_change_to_heading_id?
|
|
self.budget_id ||= heading&.group&.budget_id
|
|
end
|
|
|
|
def set_original_heading_id
|
|
self.original_heading_id = heading_id
|
|
end
|
|
|
|
def searchable_translations_definitions
|
|
{ title => "A",
|
|
description => "D" }
|
|
end
|
|
end
|
|
end
|