app/models/poll.rb
class Poll < ApplicationRecord
include Imageable
acts_as_paranoid column: :hidden_at
include ActsAsParanoidAliases
include Notifiable
include Searchable
include Sluggable
include Reportable
include SDG::Relatable
translates :name, touch: true
translates :summary, touch: true
translates :description, touch: true
include Globalizable
RECOUNT_DURATION = 1.week
has_many :booth_assignments, class_name: "Poll::BoothAssignment"
has_many :booths, through: :booth_assignments
has_many :partial_results, through: :booth_assignments
has_many :recounts, through: :booth_assignments
has_many :voters
has_many :officer_assignments, through: :booth_assignments
has_many :officers, through: :officer_assignments
has_many :questions, inverse_of: :poll, dependent: :destroy
has_many :comments, as: :commentable, inverse_of: :commentable
has_many :ballot_sheets
has_many :geozones_polls
has_many :geozones, through: :geozones_polls
belongs_to :author, -> { with_hidden }, class_name: "User", inverse_of: :polls
belongs_to :related, polymorphic: true
belongs_to :budget
validates_translation :name, presence: true
validates :starts_at, presence: true
validates :ends_at, presence: true
validates :starts_at,
comparison: {
less_than_or_equal_to: :ends_at,
message: ->(*) { I18n.t("errors.messages.invalid_date_range") }
},
allow_blank: true,
if: -> { ends_at }
validates :starts_at,
comparison: {
greater_than_or_equal_to: ->(*) { Time.current },
message: ->(*) { I18n.t("errors.messages.past_date") }
},
allow_blank: true,
on: :create
validates :ends_at,
comparison: {
greater_than_or_equal_to: ->(*) { Time.current },
message: ->(*) { I18n.t("errors.messages.past_date") }
},
on: :update,
if: -> { will_save_change_to_ends_at? }
validate :start_date_change, on: :update
validate :end_date_change, on: :update
validate :only_one_active, unless: :public?
accepts_nested_attributes_for :questions, reject_if: :all_blank, allow_destroy: true
scope :for, ->(element) { where(related: element) }
scope :current, -> { where(starts_at: ..Time.current, ends_at: Time.current..) }
scope :expired, -> { where(ends_at: ...Time.current) }
scope :recounting, -> { where(ends_at: (RECOUNT_DURATION.ago)...Time.current) }
scope :published, -> { where(published: true) }
scope :by_geozone_id, ->(geozone_id) { where(geozones: { id: geozone_id }.joins(:geozones)) }
scope :public_for_api, -> { all }
scope :not_budget, -> { where(budget_id: nil) }
scope :created_by_admin, -> { where(related_type: nil) }
def self.sort_for_list(user = nil)
all.sort do |poll, another_poll|
compare_polls(poll, another_poll, user)
end
end
def self.compare_polls(poll, another_poll, user)
weight_comparison = poll.weight(user) <=> another_poll.weight(user)
return weight_comparison unless weight_comparison.zero?
time_comparison = compare_times(poll, another_poll)
return time_comparison unless time_comparison.zero?
poll.name <=> another_poll.name
end
def self.compare_times(poll, another_poll)
if poll.expired? && another_poll.expired?
another_poll.ends_at <=> poll.ends_at
else
poll.starts_at <=> another_poll.starts_at
end
end
def self.overlaping_with(poll)
where("? < ends_at and ? >= starts_at",
poll.starts_at.beginning_of_day,
poll.ends_at.end_of_day)
.excluding(poll)
.where(related: poll.related)
end
def title
name
end
def started?(timestamp = Time.current)
starts_at.present? && starts_at < timestamp
end
def current?(timestamp = Time.current)
starts_at <= timestamp && timestamp <= ends_at
end
def expired?(timestamp = Time.current)
ends_at < timestamp
end
def recounts_confirmed?
ends_at < 1.month.ago
end
def self.current_or_recounting
current.or(recounting)
end
def answerable_by?(user)
user.present? &&
user.level_two_or_three_verified? &&
current? &&
(!geozone_restricted || geozone_ids.include?(user.geozone_id))
end
def self.answerable_by(user)
return none if user.nil? || user.unverified?
current.left_joins(:geozones)
.where("geozone_restricted = ? OR geozones.id = ?", false, user.geozone_id)
end
def self.votable_by(user)
answerable_by(user).not_voted_by(user)
end
def votable_by?(user)
return false if user_has_an_online_ballot?(user)
answerable_by?(user) && not_voted_by?(user)
end
def user_has_an_online_ballot?(user)
budget.present? && budget.ballots.find_by(user: user)&.lines.present?
end
def self.not_voted_by(user)
where.not(id: poll_ids_voted_by(user))
end
def self.poll_ids_voted_by(user)
return -1 if Poll::Voter.where(user: user).empty?
Poll::Voter.where(user: user).pluck(:poll_id)
end
def not_voted_by?(user)
Poll::Voter.where(poll: self, user: user).empty?
end
def voted_by?(user)
Poll::Voter.where(poll: self, user: user).exists?
end
def voted_in_booth?(user)
Poll::Voter.where(poll: self, user: user, origin: "booth").exists?
end
def voted_in_web?(user)
Poll::Voter.where(poll: self, user: user, origin: "web").exists?
end
def start_date_change
if will_save_change_to_starts_at?
if starts_at_in_database < Time.current
errors.add(:starts_at, I18n.t("errors.messages.cannot_change_date.poll_started"))
elsif starts_at < Time.current
errors.add(:starts_at, I18n.t("errors.messages.past_date"))
end
end
end
def end_date_change
if will_save_change_to_ends_at? && ends_at_in_database < Time.current
errors.add(:ends_at, I18n.t("errors.messages.cannot_change_date.poll_ended"))
end
end
def geozone_restricted_to=(geozones)
self.geozone_restricted = true
self.geozones = geozones
end
def generate_slug?
slug.nil?
end
def only_one_active
return if starts_at.blank?
return if ends_at.blank?
return if Poll.overlaping_with(self).none?
errors.add(:starts_at, I18n.t("activerecord.errors.messages.another_poll_active"))
end
def public?
related.nil?
end
def answer_count
Poll::Answer.where(question: questions).count
end
def budget_poll?
budget.present?
end
def searchable_translations_definitions
{
name => "A",
summary => "C",
description => "D"
}
end
def searchable_values
searchable_globalized_values
end
def self.search(terms)
pg_search(terms)
end
def weight(user)
if geozone_restricted?
if answerable_by?(user)
50
else
100
end
else
0
end
end
end