# 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