REFACTOR: use tables instead of custom fields for polls (#6359)

REFACTOR: use tables instead of custom fields for polls (#6359)

Co-authored-by: Guo Xiang Tan tgx_world@hotmail.com

From 4459665deeb84fb43621be90d64378ebc5fbd365 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9gis=20Hanol?= <regis@hanol.fr>
Date: Mon, 19 Nov 2018 14:50:00 +0100
Subject: [PATCH] REFACTOR: use tables instead of custom fields for polls
 (#6359)

Co-authored-by: Guo Xiang Tan <tgx_world@hotmail.com>

diff --git a/db/migrate/20180828065005_change_bounce_score_to_float.rb b/db/migrate/20180828065005_change_bounce_score_to_float.rb
index b2cd5a4..3164f7c 100644
--- a/db/migrate/20180828065005_change_bounce_score_to_float.rb
+++ b/db/migrate/20180828065005_change_bounce_score_to_float.rb
@@ -1,5 +1,9 @@
 class ChangeBounceScoreToFloat < ActiveRecord::Migration[5.2]
-  def change
+  def up
     change_column :user_stats, :bounce_score, :float
   end
+
+  def down
+    change_column :user_stats, :bounce_score, :integer
+  end
 end
diff --git a/plugins/poll/app/models/poll.rb b/plugins/poll/app/models/poll.rb
new file mode 100644
index 0000000..192e599
--- /dev/null
+++ b/plugins/poll/app/models/poll.rb
@@ -0,0 +1,76 @@
+class Poll < ActiveRecord::Base
+  # because we want to use the 'type' column and don't want to use STI
+  self.inheritance_column = nil
+
+  belongs_to :post
+
+  has_many :poll_options, dependent: :destroy
+  has_many :poll_votes
+
+  enum type: {
+    regular: 0,
+    multiple: 1,
+    number: 2,
+  }
+
+  enum status: {
+    open: 0,
+    closed: 1,
+  }
+
+  enum results: {
+    always: 0,
+    on_vote: 1,
+    on_close: 2,
+  }
+
+  enum visibility: {
+    secret: 0,
+    everyone: 1,
+  }
+
+  validates :min, numericality: { allow_nil: true, only_integer: true, greater_than: 0 }
+  validates :max, numericality: { allow_nil: true, only_integer: true, greater_than: 0 }
+  validates :step, numericality: { allow_nil: true, only_integer: true, greater_than: 0 }
+
+  def is_closed?
+    closed? || (close_at && close_at <= Time.zone.now)
+  end
+
+  def can_see_results?(user)
+    always? || is_closed? || (on_vote? && has_voted?(user))
+  end
+
+  def has_voted?(user)
+    user&.id && poll_votes.any? { |v| v.user_id == user.id }
+  end
+
+  def can_see_voters?(user)
+    everyone? && can_see_results?(user)
+  end
+end
+
+# == Schema Information
+#
+# Table name: polls
+#
+#  id               :bigint(8)        not null, primary key
+#  post_id          :bigint(8)
+#  name             :string           default("poll"), not null
+#  close_at         :datetime
+#  type             :integer          default("regular"), not null
+#  status           :integer          default("open"), not null
+#  results          :integer          default("always"), not null
+#  visibility       :integer          default("secret"), not null
+#  min              :integer
+#  max              :integer
+#  step             :integer
+#  anonymous_voters :integer
+#  created_at       :datetime         not null
+#  updated_at       :datetime         not null
+#
+# Indexes
+#
+#  index_polls_on_post_id           (post_id)
+#  index_polls_on_post_id_and_name  (post_id,name) UNIQUE
+#
diff --git a/plugins/poll/app/models/poll_option.rb b/plugins/poll/app/models/poll_option.rb
new file mode 100644
index 0000000..f245d61
--- /dev/null
+++ b/plugins/poll/app/models/poll_option.rb
@@ -0,0 +1,24 @@
+class PollOption < ActiveRecord::Base
+  belongs_to :poll
+  has_many :poll_votes, dependent: :delete_all
+
+  default_scope { order(created_at: :asc) }
+end
+
+# == Schema Information
+#
+# Table name: poll_options
+#
+#  id              :bigint(8)        not null, primary key
+#  poll_id         :bigint(8)
+#  digest          :string           not null
+#  html            :text             not null
+#  anonymous_votes :integer
+#  created_at      :datetime         not null
+#  updated_at      :datetime         not null
+#
+# Indexes
+#
+#  index_poll_options_on_poll_id             (poll_id)
+#  index_poll_options_on_poll_id_and_digest  (poll_id,digest) UNIQUE
+#
diff --git a/plugins/poll/app/models/poll_vote.rb b/plugins/poll/app/models/poll_vote.rb
new file mode 100644
index 0000000..6f5e7c2
--- /dev/null
+++ b/plugins/poll/app/models/poll_vote.rb
@@ -0,0 +1,23 @@
+class PollVote < ActiveRecord::Base
+  belongs_to :poll
+  belongs_to :poll_option
+  belongs_to :user
+end
+
+# == Schema Information
+#
+# Table name: poll_votes
+#
+#  poll_id        :bigint(8)
+#  poll_option_id :bigint(8)
+#  user_id        :bigint(8)
+#  created_at     :datetime         not null
+#  updated_at     :datetime         not null
+#
+# Indexes
+#
+#  index_poll_votes_on_poll_id                                 (poll_id)
+#  index_poll_votes_on_poll_id_and_poll_option_id_and_user_id  (poll_id,poll_option_id,user_id) UNIQUE
+#  index_poll_votes_on_poll_option_id                          (poll_option_id)
+#  index_poll_votes_on_user_id                                 (user_id)
+#
diff --git a/plugins/poll/app/serializers/poll_option_serializer.rb b/plugins/poll/app/serializers/poll_option_serializer.rb
new file mode 100644
index 0000000..b23ac7a
--- /dev/null
+++ b/plugins/poll/app/serializers/poll_option_serializer.rb
@@ -0,0 +1,14 @@
+class PollOptionSerializer < ApplicationSerializer
+
+  attributes :id, :html, :votes
+
+  def id
+    object.digest
+  end
+
+  def votes
+    # `size` instead of `count` to prevent N+1
+    object.poll_votes.size + object.anonymous_votes.to_i
+  end
+
+end
diff --git a/plugins/poll/app/serializers/poll_serializer.rb b/plugins/poll/app/serializers/poll_serializer.rb
new file mode 100644
index 0000000..fb8cd26
--- /dev/null
+++ b/plugins/poll/app/serializers/poll_serializer.rb
@@ -0,0 +1,50 @@
+class PollSerializer < ApplicationSerializer
+  attributes :name,
+             :type,
+             :status,
+             :public,
+             :results,
+             :min,
+             :max,
+             :step,
+             :options,
+             :voters,
+             :close
+
+  def public
+    true
+  end
+
+  def include_public?
+    object.everyone?
+  end
+
+  def include_min?
+    object.min.present? && (object.number? || object.multiple?)
+  end
+
+  def include_max?
+    object.max.present? && (object.number? || object.multiple?)
+  end
+
+  def include_step?
+    object.step.present? && object.number?
+  end
+
+  def options
+    object.poll_options.map { |o| PollOptionSerializer.new(o, root: false).as_json }
+  end
+
+  def voters
+    object.poll_votes.map { |v| v.user_id }.uniq.count + object.anonymous_voters.to_i
+  end
+
+  def close
+    object.close_at
+  end
+
+  def include_close?
+    object.close_at.present?
+  end
+
+end
diff --git a/plugins/poll/assets/javascripts/controllers/poll-ui-builder.js.es6 b/plugins/poll/assets/javascripts/controllers/poll-ui-builder.js.es6
index eab85ca..fc599f3 100644
--- a/plugins/poll/assets/javascripts/controllers/poll-ui-builder.js.es6
+++ b/plugins/poll/assets/javascripts/controllers/poll-ui-builder.js.es6
@@ -9,6 +9,10 @@ export default Ember.Controller.extend({
   numberPollType: "number",
   multiplePollType: "multiple",
 
+  alwaysPollResult: "always",
+  votePollResult: "on_vote",
+  closedPollResult: "on_close",
+
   init() {
     this._super();
     this._setupPoll();
@@ -32,6 +36,24 @@ export default Ember.Controller.extend({
     ];
   },
 
+  @computed("alwaysPollResult", "votePollResult", "closedPollResult")
+  pollResults(alwaysPollResult, votePollResult, closedPollResult) {
+    return [
+      {
+        name: I18n.t("poll.ui_builder.poll_result.always"),
+        value: alwaysPollResult
+      },
+      {
+        name: I18n.t("poll.ui_builder.poll_result.vote"),
+        value: votePollResult
+      },
+      {
+        name: I18n.t("poll.ui_builder.poll_result.closed"),
+        value: closedPollResult
+      }
+    ];
+  },
+
   @computed("pollType", "regularPollType")
   isRegular(pollType, regularPollType) {
     return pollType === regularPollType;
@@ -128,6 +150,7 @@ export default Ember.Controller.extend({
     "isNumber",
     "showMinMax",
     "pollType",
+    "pollResult",
     "publicPoll",

GitHub

2 Likes

This commit has been mentioned on Discourse Meta. There might be relevant details there:

This commit has been mentioned on Discourse Meta. There might be relevant details there:

This commit has been mentioned on Discourse Meta. There might be relevant details there:

This commit has been mentioned on Discourse Meta. There might be relevant details there:

This commit has been mentioned on Discourse Meta. There might be relevant details there:

:confetti_ball: :heart_eyes::smiling_face_with_three_hearts::sun_with_face::sunny:

This has been quite an adventure, so glad it is merged in!

5 Likes

This commit has been mentioned on Discourse Meta. There might be relevant details there:

This commit has been mentioned on Discourse Meta. There might be relevant details there: