Files
nairobi/app/models/poll.rb
Javi Martín 5a0fde4048 Allow selecting the time when a poll starts/ends
We were already saving it as a time, but we didn't offer an interface to
select the time due to lack of decent browser support for this field
back when this feature was added.

However, nowadays all major browsers support this field type and, at the
time of writing, at least 86.5% of the browsers support it [1]. This
percentage could be much higher, since support in 11.25% of the browsers
is unknown.

Note we still need to support the case where this field isn't supported,
and so we offer a fallback and on the server side we don't assume we're
always getting a time. We're doing a strange hack so we set the field
type to text before changing its value; otherwise old Firefox browsers
crashed.

Also note that, until now, we were storing end dates in the database as
a date with 00:00 as its time, but we were considering the poll to be
open until 23:59 that day. So, in order to keep backwards compatibility,
we're adding a task to update the dates of existing polls so we get the
same behavior we had until now.

This also means budget polls are now created so they end at the
beginning of the day when the balloting phase ends. This is consistent
with the dates we display in the budget phases table.

Finally, there's one test where we're using `beginning_of_minute` when
creating a poll. That's because Chrome provides an interface to enter a
time in a `%H:%M` format when the "seconds" value of the provided time
is zero. However, when the "seconds" value isn't zero, Chrome provides
an interface to enter a time in a `%H:%M:%S` format. Since Capybara
doesn't enter the seconds when using `fill_in` with a time, the test
failed when Capybara tried to enter a time in the `%H:%M` format when
Chrome expected a time in the `%H:%M:%S` format.

To solve this last point, an alternative would be to manually provide
the format when using `fill_in` so it includes the seconds.

[1] https://caniuse.com/mdn-html_elements_input_type_datetime-local
2022-09-14 15:14:23 +02:00

198 lines
5.0 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 StatsVersionable
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 :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|
[poll.weight(user), poll.starts_at, poll.name] <=> [another_poll.weight(user), another_poll.starts_at, another_poll.name]
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 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
unless starts_at.present? && ends_at.present? && starts_at <= ends_at
errors.add(:starts_at, I18n.t("errors.messages.invalid_date_range"))
end
end
def generate_slug?
slug.nil?
end
def only_one_active
return unless starts_at.present?
return unless ends_at.present?
return unless Poll.overlaping_with(self).any?
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