about summary refs log tree commit diff
path: root/app/models
diff options
context:
space:
mode:
Diffstat (limited to 'app/models')
-rw-r--r--app/models/account.rb1
-rw-r--r--app/models/concerns/account_associations.rb1
-rw-r--r--app/models/export.rb2
-rw-r--r--app/models/featured_tag.rb5
-rw-r--r--app/models/poll.rb97
-rw-r--r--app/models/poll_vote.rb36
-rw-r--r--app/models/status.rb12
7 files changed, 151 insertions, 3 deletions
diff --git a/app/models/account.rb b/app/models/account.rb
index 0c08c0991..bf2a17a7f 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -245,6 +245,7 @@ class Account < ApplicationRecord
   def fields_attributes=(attributes)
     fields     = []
     old_fields = self[:fields] || []
+    old_fields = [] if old_fields.is_a?(Hash)
 
     if attributes.is_a?(Hash)
       attributes.each_value do |attr|
diff --git a/app/models/concerns/account_associations.rb b/app/models/concerns/account_associations.rb
index 3ab8a0daa..1b22f750c 100644
--- a/app/models/concerns/account_associations.rb
+++ b/app/models/concerns/account_associations.rb
@@ -27,6 +27,7 @@ module AccountAssociations
 
     # Media
     has_many :media_attachments, dependent: :destroy
+    has_many :polls, dependent: :destroy
 
     # PuSH subscriptions
     has_many :subscriptions, dependent: :destroy
diff --git a/app/models/export.rb b/app/models/export.rb
index fc4bb6964..9bf866d35 100644
--- a/app/models/export.rb
+++ b/app/models/export.rb
@@ -23,7 +23,7 @@ class Export
 
   def to_lists_csv
     CSV.generate do |csv|
-      account.owned_lists.select(:title).each do |list|
+      account.owned_lists.select(:title, :id).each do |list|
         list.accounts.select(:username, :domain).each do |account|
           csv << [list.title, acct(account)]
         end
diff --git a/app/models/featured_tag.rb b/app/models/featured_tag.rb
index b5a10ad2d..d06ae26a8 100644
--- a/app/models/featured_tag.rb
+++ b/app/models/featured_tag.rb
@@ -18,11 +18,12 @@ class FeaturedTag < ApplicationRecord
 
   delegate :name, to: :tag, allow_nil: true
 
-  validates :name, presence: true
+  validates_associated :tag, on: :create
+  validates :name, presence: true, on: :create
   validate :validate_featured_tags_limit, on: :create
 
   def name=(str)
-    self.tag = Tag.find_or_initialize_by(name: str.delete('#').mb_chars.downcase.to_s)
+    self.tag = Tag.find_or_initialize_by(name: str.strip.delete('#').mb_chars.downcase.to_s)
   end
 
   def increment(timestamp)
diff --git a/app/models/poll.rb b/app/models/poll.rb
new file mode 100644
index 000000000..ab7236d45
--- /dev/null
+++ b/app/models/poll.rb
@@ -0,0 +1,97 @@
+# 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_options
+  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 prepare_options
+    self.options = options.map(&:strip).reject(&:blank?)
+  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
diff --git a/app/models/poll_vote.rb b/app/models/poll_vote.rb
new file mode 100644
index 000000000..9ad66bbf8
--- /dev/null
+++ b/app/models/poll_vote.rb
@@ -0,0 +1,36 @@
+# 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
+#  uri        :string
+#
+
+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
+
+  delegate :local?, to: :account
+
+  def object_type
+    :vote
+  end
+
+  private
+
+  def increment_counter_cache
+    poll.cached_tallies[choice] = (poll.cached_tallies[choice] || 0) + 1
+    poll.save
+  end
+end
diff --git a/app/models/status.rb b/app/models/status.rb
index 4566c0d20..f576489b4 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -23,6 +23,7 @@
 #  in_reply_to_account_id :bigint(8)
 #  local_only             :boolean
 #  full_status_text       :text             default(""), not null
+#  poll_id                :bigint(8)
 #
 
 class Status < ApplicationRecord
@@ -46,6 +47,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
@@ -64,12 +66,14 @@ 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? }
   validates_with StatusLengthValidator
   validates_with DisallowedHashtagsValidator
   validates :reblog, uniqueness: { scope: :account }, if: :reblog?
+  validates_associated :owned_poll
 
   default_scope { recent }
 
@@ -106,6 +110,7 @@ class Status < ApplicationRecord
                    :tags,
                    :preview_cards,
                    :stream_entry,
+                   :poll,
                    account: :account_stat,
                    active_mentions: { account: :account_stat },
                    reblog: [
@@ -116,6 +121,7 @@ class Status < ApplicationRecord
                      :media_attachments,
                      :conversation,
                      :status_stat,
+                     :poll,
                      account: :account_stat,
                      active_mentions: { account: :account_stat },
                    ],
@@ -257,6 +263,8 @@ class Status < ApplicationRecord
   before_validation :set_conversation
   before_validation :set_local
 
+  after_create :set_poll_id
+
   class << self
     def selectable_visibilities
       visibilities.keys - %w(direct limited)
@@ -458,6 +466,10 @@ class Status < ApplicationRecord
     self.reblog = reblog.reblog if reblog? && reblog.reblog?
   end
 
+  def set_poll_id
+    update_column(: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?