Add polls (#10111)
* Add polls Fix #1629 * Add tests * Fixes * Change API for creating polls * Use name instead of content for votes * Remove poll validation for remote polls * Add polls to public pages * When updating the poll, update options just in case they were changed * Fix public pages showing both poll and other media
This commit is contained in:
		
					parent
					
						
							
								99dc212ae5
							
						
					
				
			
			
				commit
				
					
						230a012f00
					
				
			
		
					 47 changed files with 1038 additions and 19 deletions
				
			
		|  | @ -26,6 +26,7 @@ module AccountAssociations | |||
| 
 | ||||
|     # Media | ||||
|     has_many :media_attachments, dependent: :destroy | ||||
|     has_many :polls, dependent: :destroy | ||||
| 
 | ||||
|     # PuSH subscriptions | ||||
|     has_many :subscriptions, dependent: :destroy | ||||
|  |  | |||
							
								
								
									
										90
									
								
								app/models/poll.rb
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								app/models/poll.rb
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,90 @@ | |||
| # frozen_string_literal: true | ||||
| # == Schema Information | ||||
| # | ||||
| # Table name: polls | ||||
| # | ||||
| #  id              :bigint(8)        not null, primary key | ||||
| #  account_id      :bigint(8) | ||||
| #  status_id       :bigint(8) | ||||
| #  expires_at      :datetime | ||||
| #  options         :string           default([]), not null, is an Array | ||||
| #  cached_tallies  :bigint(8)        default([]), not null, is an Array | ||||
| #  multiple        :boolean          default(FALSE), not null | ||||
| #  hide_totals     :boolean          default(FALSE), not null | ||||
| #  votes_count     :bigint(8)        default(0), not null | ||||
| #  last_fetched_at :datetime | ||||
| #  created_at      :datetime         not null | ||||
| #  updated_at      :datetime         not null | ||||
| # | ||||
| 
 | ||||
| class Poll < ApplicationRecord | ||||
|   include Expireable | ||||
| 
 | ||||
|   belongs_to :account | ||||
|   belongs_to :status | ||||
| 
 | ||||
|   has_many :votes, class_name: 'PollVote', inverse_of: :poll, dependent: :destroy | ||||
| 
 | ||||
|   validates :options, presence: true | ||||
|   validates :expires_at, presence: true, if: :local? | ||||
|   validates_with PollValidator, if: :local? | ||||
| 
 | ||||
|   scope :attached, -> { where.not(status_id: nil) } | ||||
|   scope :unattached, -> { where(status_id: nil) } | ||||
| 
 | ||||
|   before_validation :prepare_votes_count | ||||
|   after_initialize :prepare_cached_tallies | ||||
|   after_commit :reset_parent_cache, on: :update | ||||
| 
 | ||||
|   def loaded_options | ||||
|     options.map.with_index { |title, key| Option.new(self, key.to_s, title, cached_tallies[key]) } | ||||
|   end | ||||
| 
 | ||||
|   def unloaded_options | ||||
|     options.map.with_index { |title, key| Option.new(self, key.to_s, title, nil) } | ||||
|   end | ||||
| 
 | ||||
|   def possibly_stale? | ||||
|     remote? && last_fetched_before_expiration? && time_passed_since_last_fetch? | ||||
|   end | ||||
| 
 | ||||
|   delegate :local?, to: :account | ||||
| 
 | ||||
|   def remote? | ||||
|     !local? | ||||
|   end | ||||
| 
 | ||||
|   class Option < ActiveModelSerializers::Model | ||||
|     attributes :id, :title, :votes_count, :poll | ||||
| 
 | ||||
|     def initialize(poll, id, title, votes_count) | ||||
|       @poll        = poll | ||||
|       @id          = id | ||||
|       @title       = title | ||||
|       @votes_count = votes_count | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def prepare_cached_tallies | ||||
|     self.cached_tallies = options.map { 0 } if cached_tallies.empty? | ||||
|   end | ||||
| 
 | ||||
|   def prepare_votes_count | ||||
|     self.votes_count = cached_tallies.sum unless cached_tallies.empty? | ||||
|   end | ||||
| 
 | ||||
|   def reset_parent_cache | ||||
|     return if status_id.nil? | ||||
|     Rails.cache.delete("statuses/#{status_id}") | ||||
|   end | ||||
| 
 | ||||
|   def last_fetched_before_expiration? | ||||
|     last_fetched_at.nil? || expires_at.nil? || last_fetched_at < expires_at | ||||
|   end | ||||
| 
 | ||||
|   def time_passed_since_last_fetch? | ||||
|     last_fetched_at.nil? || last_fetched_at < 1.minute.ago | ||||
|   end | ||||
| end | ||||
							
								
								
									
										29
									
								
								app/models/poll_vote.rb
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								app/models/poll_vote.rb
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,29 @@ | |||
| # frozen_string_literal: true | ||||
| # == Schema Information | ||||
| # | ||||
| # Table name: poll_votes | ||||
| # | ||||
| #  id         :bigint(8)        not null, primary key | ||||
| #  account_id :bigint(8) | ||||
| #  poll_id    :bigint(8) | ||||
| #  choice     :integer          default(0), not null | ||||
| #  created_at :datetime         not null | ||||
| #  updated_at :datetime         not null | ||||
| # | ||||
| 
 | ||||
| class PollVote < ApplicationRecord | ||||
|   belongs_to :account | ||||
|   belongs_to :poll, inverse_of: :votes | ||||
| 
 | ||||
|   validates :choice, presence: true | ||||
|   validates_with VoteValidator | ||||
| 
 | ||||
|   after_create_commit :increment_counter_cache | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def increment_counter_cache | ||||
|     poll.cached_tallies[choice] = (poll.cached_tallies[choice] || 0) + 1 | ||||
|     poll.save | ||||
|   end | ||||
| end | ||||
|  | @ -21,6 +21,7 @@ | |||
| #  account_id             :bigint(8)        not null | ||||
| #  application_id         :bigint(8) | ||||
| #  in_reply_to_account_id :bigint(8) | ||||
| #  poll_id                :bigint(8) | ||||
| # | ||||
| 
 | ||||
| class Status < ApplicationRecord | ||||
|  | @ -44,6 +45,7 @@ class Status < ApplicationRecord | |||
|   belongs_to :account, inverse_of: :statuses | ||||
|   belongs_to :in_reply_to_account, foreign_key: 'in_reply_to_account_id', class_name: 'Account', optional: true | ||||
|   belongs_to :conversation, optional: true | ||||
|   belongs_to :poll, optional: true | ||||
| 
 | ||||
|   belongs_to :thread, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :replies, optional: true | ||||
|   belongs_to :reblog, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblogs, optional: true | ||||
|  | @ -61,6 +63,7 @@ class Status < ApplicationRecord | |||
|   has_one :notification, as: :activity, dependent: :destroy | ||||
|   has_one :stream_entry, as: :activity, inverse_of: :status | ||||
|   has_one :status_stat, inverse_of: :status | ||||
|   has_one :owned_poll, class_name: 'Poll', inverse_of: :status, dependent: :destroy | ||||
| 
 | ||||
|   validates :uri, uniqueness: true, presence: true, unless: :local? | ||||
|   validates :text, presence: true, unless: -> { with_media? || reblog? } | ||||
|  | @ -101,6 +104,7 @@ class Status < ApplicationRecord | |||
|                    :tags, | ||||
|                    :preview_cards, | ||||
|                    :stream_entry, | ||||
|                    :poll, | ||||
|                    account: :account_stat, | ||||
|                    active_mentions: { account: :account_stat }, | ||||
|                    reblog: [ | ||||
|  | @ -111,6 +115,7 @@ class Status < ApplicationRecord | |||
|                      :media_attachments, | ||||
|                      :conversation, | ||||
|                      :status_stat, | ||||
|                      :poll, | ||||
|                      account: :account_stat, | ||||
|                      active_mentions: { account: :account_stat }, | ||||
|                    ], | ||||
|  | @ -250,6 +255,8 @@ class Status < ApplicationRecord | |||
|   before_validation :set_conversation | ||||
|   before_validation :set_local | ||||
| 
 | ||||
|   before_save :set_poll_id | ||||
| 
 | ||||
|   class << self | ||||
|     def selectable_visibilities | ||||
|       visibilities.keys - %w(direct limited) | ||||
|  | @ -438,6 +445,10 @@ class Status < ApplicationRecord | |||
|     self.reblog = reblog.reblog if reblog? && reblog.reblog? | ||||
|   end | ||||
| 
 | ||||
|   def set_poll_id | ||||
|     self.poll_id = owned_poll.id unless owned_poll.nil? | ||||
|   end | ||||
| 
 | ||||
|   def set_visibility | ||||
|     self.visibility = (account.locked? ? :private : :public) if visibility.nil? | ||||
|     self.visibility = reblog.visibility if reblog? | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue