discourse/plugins/poll/lib/polls_updater.rb

141 lines
4.6 KiB
Ruby

# frozen_string_literal: true
module DiscoursePoll
class PollsUpdater
POLL_ATTRIBUTES = %w[close_at max min results status step type visibility title groups]
def self.update(post, polls)
::Poll.transaction do
has_changed = false
edit_window = SiteSetting.poll_edit_window_mins
old_poll_names = ::Poll.where(post: post).pluck(:name)
new_poll_names = polls.keys
deleted_poll_names = old_poll_names - new_poll_names
created_poll_names = new_poll_names - old_poll_names
# delete polls
if deleted_poll_names.present?
::Poll.where(post: post, name: deleted_poll_names).destroy_all
end
# create polls
if created_poll_names.present?
has_changed = true
polls.slice(*created_poll_names).values.each { |poll| Poll.create!(post.id, poll) }
end
# update polls
::Poll
.includes(:poll_votes, :poll_options)
.where(post: post)
.find_each do |old_poll|
new_poll = polls[old_poll.name]
new_poll_options = new_poll["options"]
attributes = new_poll.slice(*POLL_ATTRIBUTES)
attributes["visibility"] = new_poll["public"] == "true" ? "everyone" : "secret"
attributes["close_at"] = begin
Time.zone.parse(new_poll["close"])
rescue StandardError
nil
end
attributes["status"] = old_poll["status"]
attributes["groups"] = new_poll["groups"]
poll = ::Poll.new(attributes)
if is_different?(old_poll, poll, new_poll_options)
# only prevent changes when there's at least 1 vote
if old_poll.poll_votes.size > 0
# can't change after edit window (when enabled)
if edit_window > 0 && old_poll.created_at < edit_window.minutes.ago
error =
(
if poll.name == DiscoursePoll::DEFAULT_POLL_NAME
I18n.t(
"poll.edit_window_expired.cannot_edit_default_poll_with_votes",
minutes: edit_window,
)
else
I18n.t(
"poll.edit_window_expired.cannot_edit_named_poll_with_votes",
minutes: edit_window,
name: poll.name,
)
end
)
post.errors.add(:base, error)
# rubocop:disable Lint/NonLocalExitFromIterator
return
# rubocop:enable Lint/NonLocalExitFromIterator
end
end
# update poll
POLL_ATTRIBUTES.each do |attr|
old_poll.public_send("#{attr}=", poll.public_send(attr))
end
old_poll.save!
# keep track of anonymous votes
anonymous_votes =
old_poll.poll_options.map { |pv| [pv.digest, pv.anonymous_votes] }.to_h
# destroy existing options & votes
::PollOption.where(poll: old_poll).destroy_all
# create new options
new_poll_options.each do |option|
::PollOption.create!(
poll: old_poll,
digest: option["id"],
html: option["html"].strip,
anonymous_votes: anonymous_votes[option["id"]],
)
end
has_changed = true
end
end
if ::Poll.exists?(post: post)
post.custom_fields[HAS_POLLS] = true
else
post.custom_fields.delete(HAS_POLLS)
end
post.save_custom_fields(true)
if has_changed
polls = ::Poll.includes(poll_options: :poll_votes).where(post: post)
polls =
ActiveModel::ArraySerializer.new(
polls,
each_serializer: PollSerializer,
root: false,
scope: Guardian.new(nil),
).as_json
post.publish_message!("/polls/#{post.topic_id}", post_id: post.id, polls: polls)
end
end
end
private
def self.is_different?(old_poll, new_poll, new_options)
# an attribute was changed?
POLL_ATTRIBUTES.each do |attr|
return true if old_poll.public_send(attr) != new_poll.public_send(attr)
end
sorted_old_options = old_poll.poll_options.map { |o| o.digest }.sort
sorted_new_options = new_options.map { |o| o["id"] }.sort
sorted_old_options != sorted_new_options
end
end
end