Files
nairobi/app/models/poll.rb
Javi Martín a4461a1a56 Expire the stats cache once per day
When we first started caching the stats, generating them was a process
that took several minutes, so we never expired the cache.

However, there have been cases where we run into issues where the stats
shown on the screen were outdated. That's why we introduced a task to
manually expire the cache.

But now, generating the stats only takes a few seconds, so we can
automatically expire them every day, remove all the logic needed to
manually expire them, and get rid of most of the issues related to the
cache being outdated.

We're expiring them every day because it's the same day we were doing in
public stats (which we removed in commit 631b48f58), only we're using
`expires_at:` to set the expiration time, in order to simplify the code.

Note that, in the test, we're using `travel_to(time)` so the test passes
even when it starts an instant before midnight. We aren't using
`:with_frozen_time` because, in similar cases (although not in this
case, but I'm not sure whether that's intentional), `travel_to` shows
this error:

> Calling `travel_to` with a block, when we have previously already made
> a call to `travel_to`, can lead to confusing time stubbing.
2024-05-17 20:11:16 +02:00

258 lines
6.6 KiB
Ruby

class Poll < ApplicationRecord
require_dependency "poll/answer"
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
validate :date_range
validate :start_date_is_not_past_date, on: :create
validate :start_date_change, on: :update
validate :end_date_is_not_past_date, 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 and ends_at >= :time", time: 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)
.where.not(id: poll.id)
.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 date_range
if starts_at.blank? || ends_at.blank? || starts_at > ends_at
errors.add(:starts_at, I18n.t("errors.messages.invalid_date_range"))
end
end
def start_date_is_not_past_date
if starts_at.present? && starts_at < Time.current
errors.add(:starts_at, I18n.t("errors.messages.past_date"))
end
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_is_not_past_date
if will_save_change_to_ends_at? && ends_at < Time.current
errors.add(:ends_at, I18n.t("errors.messages.past_date"))
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