Skip to content
Permalink
Browse files

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

Co-authored-by: Guo Xiang Tan <tgx_world@hotmail.com>
  • Loading branch information...
ZogStriP and tgxworld committed Nov 19, 2018
1 parent 86dafc1 commit 4459665deeb84fb43621be90d64378ebc5fbd365
Showing with 1,859 additions and 1,520 deletions.
  1. +5 −1 db/migrate/20180828065005_change_bounce_score_to_float.rb
  2. +76 −0 plugins/poll/app/models/poll.rb
  3. +24 −0 plugins/poll/app/models/poll_option.rb
  4. +23 −0 plugins/poll/app/models/poll_vote.rb
  5. +14 −0 plugins/poll/app/serializers/poll_option_serializer.rb
  6. +50 −0 plugins/poll/app/serializers/poll_serializer.rb
  7. +25 −0 plugins/poll/assets/javascripts/controllers/poll-ui-builder.js.es6
  8. +9 −0 plugins/poll/assets/javascripts/discourse/templates/modal/poll-ui-builder.hbs
  9. +5 −8 plugins/poll/assets/javascripts/initializers/extend-for-poll.js.es6
  10. +7 −6 plugins/poll/assets/javascripts/lib/discourse-markdown/poll.js.es6
  11. +192 −199 plugins/poll/assets/javascripts/widgets/discourse-poll.js.es6
  12. +5 −2 plugins/poll/assets/stylesheets/common/poll-ui-builder.scss
  13. +1 −3 plugins/poll/assets/stylesheets/common/poll.scss
  14. +4 −0 plugins/poll/assets/stylesheets/desktop/poll.scss
  15. +12 −1 plugins/poll/config/locales/client.en.yml
  16. +2 −6 plugins/poll/config/locales/server.en.yml
  17. +3 −0 plugins/poll/config/settings.yml
  18. +39 −0 plugins/poll/db/migrate/20180820073549_create_polls_tables.rb
  19. +159 −0 plugins/poll/db/post_migrate/20180820080623_migrate_polls_data.rb
  20. +81 −98 plugins/poll/lib/polls_updater.rb
  21. +3 −14 plugins/poll/lib/polls_validator.rb
  22. +0 −59 plugins/poll/lib/votes_updater.rb
  23. +249 −188 plugins/poll/plugin.rb
  24. +79 −84 plugins/poll/spec/controllers/polls_controller_spec.rb
  25. +26 −77 plugins/poll/spec/controllers/posts_controller_spec.rb
  26. +330 −0 plugins/poll/spec/db/post_migrate/migrate_polls_data_spec.rb
  27. +0 −17 plugins/poll/spec/helpers.rb
  28. +13 −11 plugins/poll/spec/integration/poll_endpoints_spec.rb
  29. +5 −7 plugins/poll/spec/lib/new_post_manager_spec.rb
  30. +116 −352 plugins/poll/spec/lib/polls_updater_spec.rb
  31. +18 −18 plugins/poll/spec/lib/polls_validator_spec.rb
  32. +0 −94 plugins/poll/spec/lib/votes_updater_spec.rb
  33. +271 −269 plugins/poll/test/javascripts/acceptance/polls-test.js.es6
  34. +3 −3 spec/components/migration/safe_migrate_spec.rb
  35. +1 −1 spec/fixtures/db/migrate/drop_table/20990309014014_drop_table.rb
  36. +2 −2 ...migrate/drop_table/{20990309014013_drop_users_table.rb → 20990309014013_drop_email_logs_table.rb}
  37. +7 −0 spec/rails_helper.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
@@ -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
#
@@ -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
#
@@ -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)
#
@@ -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
@@ -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
@@ -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",
"pollOptions",
"pollMin",
@@ -141,6 +164,7 @@ export default Ember.Controller.extend({
isNumber,
showMinMax,
pollType,
pollResult,
publicPoll,
pollOptions,
pollMin,
@@ -167,6 +191,7 @@ export default Ember.Controller.extend({
}

if (pollType) pollHeader += ` type=${pollType}`;
if (pollResult) pollHeader += ` results=${pollResult}`;
if (pollMin && showMinMax) pollHeader += ` min=${pollMin}`;
if (pollMax) pollHeader += ` max=${pollMax}`;
if (isNumber) pollHeader += ` step=${step}`;
@@ -8,6 +8,15 @@
valueAttribute="value"}}
</div>

<div class="input-group poll-select">
<label class="input-group-label">{{i18n 'poll.ui_builder.poll_result.label'}}</label>
{{combo-box content=pollResults
value=pollResult
allowInitialValueMutation=true
valueAttribute="value"}}
</div>


{{#if showMinMax}}
<div class="input-group poll-number">
{{input-tip validation=minMaxValueValidation}}
@@ -39,12 +39,12 @@ function initializePolls(api) {
const polls = this.get("polls");
if (polls) {
this._polls = this._polls || {};
_.map(polls, (v, k) => {
const existing = this._polls[k];
polls.forEach(p => {
const existing = this._polls[p.name];
if (existing) {
this._polls[k].setProperties(v);
this._polls[p.name].setProperties(p);
} else {
this._polls[k] = Em.Object.create(v);
this._polls[p.name] = Em.Object.create(p);
}
});
this.set("pollsObject", this._polls);
@@ -81,14 +81,11 @@ function initializePolls(api) {
const pollName = $poll.data("poll-name");
const poll = polls[pollName];
if (poll) {
const isMultiple = poll.get("type") === "multiple";

const glue = new WidgetGlue("discourse-poll", register, {
id: `${pollName}-${post.id}`,
post,
poll,
vote: votes[pollName] || [],
isMultiple
vote: votes[pollName] || []
});
glue.appendTo(pollElem);
_glued.push(glue);
@@ -3,15 +3,16 @@
const DATA_PREFIX = "data-poll-";
const DEFAULT_POLL_NAME = "poll";
const WHITELISTED_ATTRIBUTES = [
"type",
"name",
"min",
"close",
"max",
"step",
"min",
"name",
"order",
"status",
"public",
"close"
"results",
"status",
"step",
"type",
];

function replaceToken(tokens, target, list) {

8 comments on commit 4459665

@discoursebot

This comment has been minimized.

Copy link

replied Nov 19, 2018

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

https://meta.discourse.org/t/keep-poll-results-hidden/29810/19

@discoursebot

This comment has been minimized.

Copy link

replied Nov 19, 2018

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

https://meta.discourse.org/t/anonymous-votes-in-poll-plugin/42382/9

@discoursebot

This comment has been minimized.

Copy link

replied Nov 19, 2018

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

https://meta.discourse.org/t/polls-should-automatically-switch-to-results-on-page-load-if-user-has-already-voted/50053/10

@discoursebot

This comment has been minimized.

Copy link

replied Nov 19, 2018

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

https://meta.discourse.org/t/polls-dont-appear-in-activity-page/35885/3

@discoursebot

This comment has been minimized.

Copy link

replied Nov 19, 2018

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

https://meta.discourse.org/t/poll-closing-time-is-shown-to-user-in-server-time/96173/5

@SamSaffron

This comment has been minimized.

Copy link
Member

replied Nov 20, 2018

🎊 😍:smiling_face_with_three_hearts:🌞☀️

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

@discoursebot

This comment has been minimized.

Copy link

replied Nov 23, 2018

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

https://meta.discourse.org/t/polls-with-duplicate-choices-not-migrating-properly/102642/1

@discoursebot

This comment has been minimized.

Copy link

replied Sep 11, 2019

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

https://meta.discourse.org/t/zero-downtime-upgrades/120673/2

Please sign in to comment.
You can’t perform that action at this time.