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
198 lines
5.0 KiB
Ruby
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
|