PERF: Refactor public polls users.

This commit is contained in:
Guo Xiang Tan 2016-08-05 16:39:14 +08:00
parent 6827239444
commit d80f340a36
12 changed files with 213 additions and 101 deletions

View file

@ -1,15 +0,0 @@
import computed from 'ember-addons/ember-computed-decorators';
import PollVoters from 'discourse/plugins/poll/components/poll-voters';
export default PollVoters.extend({
@computed("poll.voters", "pollsVoters")
canLoadMore(voters, pollsVoters) {
return pollsVoters.length < voters;
},
@computed("poll.options", "offset")
voterIds(options) {
const ids = [].concat(...(options.map(option => option.voter_ids)));
return this._getIds(ids);
}
});

View file

@ -1,7 +1,24 @@
import round from "discourse/lib/round";
import computed from 'ember-addons/ember-computed-decorators';
import { ajax } from 'discourse/lib/ajax';
export default Em.Component.extend({
didInsertElement() {
this._super();
this._fetchUsers();
},
_fetchUsers() {
if (!this.get('isPublic')) return;
this.send("fetchUsers", this.get('voterIds').slice(0, 20));
},
@computed('poll.options', 'poll.options.[]')
voterIds(options) {
const voterIds = _.uniq([].concat(...(options.map(option => option.get('voter_ids')))));
return voterIds;
},
@computed("poll.options.@each.{html,votes}")
totalScore() {
return _.reduce(this.get("poll.options"), function(total, o) {
@ -22,4 +39,32 @@ export default Em.Component.extend({
return I18n.t("poll.average_rating", { average: this.get("average") });
},
actions: {
fetchUsers(voterIds, defer) {
const pollVoters = this.get('poll.pollVoters') || [];
const ids = _.difference(voterIds, pollVoters.map(pollVoter => pollVoter.id));
if (ids.length > 0) {
ajax("/polls/voters.json", {
type: "put",
data: { options: { default: ids } }
}).then(result => {
const voters = result.voters;
const poll = this.get('poll');
poll.set('pollVoters', _.uniq(
pollVoters.concat(voters['default']),
user => user.id
));
if (defer) defer.resolve();
}).catch((error) => {
Ember.Logger.error(error);
bootbox.alert(I18n.t('poll.error_while_fetching_voters'));
});
} else {
if (defer) defer.resolve();
}
}
}
});

View file

@ -1,14 +0,0 @@
import computed from 'ember-addons/ember-computed-decorators';
import PollVoters from 'discourse/plugins/poll/components/poll-voters';
export default PollVoters.extend({
@computed("option.votes", "pollsVoters")
canLoadMore(voters, pollsVoters) {
return pollsVoters.length < voters;
},
@computed("option.voter_ids", "offset")
voterIds(ids) {
return this._getIds(ids);
}
});

View file

@ -1,10 +1,38 @@
import evenRound from "discourse/plugins/poll/lib/even-round";
import computed from "ember-addons/ember-computed-decorators";
import { default as computed, observes } from "ember-addons/ember-computed-decorators";
import { ajax } from 'discourse/lib/ajax';
export default Em.Component.extend({
tagName: "ul",
classNames: ["results"],
didInsertElement() {
this._super();
this._fetchUsers();
},
_fetchUsers() {
if (!this.get('isPublic')) return;
this.send("fetchUsers", this.get('optionsVoterIds'));
},
@observes('isPublic', 'poll.options.[]')
updateNewVoters(isPublic) {
if (!isPublic) return;
this._fetchUsers();
},
@computed('options', 'poll.options.[]')
optionsVoterIds(options) {
const ids = {};
options.forEach(option => {
ids[option.get('id')] = option.get('voter_ids').slice(0, 20);
});
return ids;
},
@computed("poll.voters", "poll.type", "poll.options.[]")
options(voters, type) {
const options = this.get("poll.options").slice(0).sort((a, b) => {
@ -31,10 +59,56 @@ export default Em.Component.extend({
option.setProperties({
percentage,
style,
title: I18n.t("poll.option_title", { count: option.get("votes") })
title: I18n.t("poll.option_title", { count: option.get("votes") }),
});
});
return options;
},
actions: {
fetchUsers(optionsVoterIds, defer) {
const ids = {};
let updated = false;
this.get('options').forEach(option => {
const optionId = option.get('id');
const newIds = optionsVoterIds[optionId];
const oldIds = (option.get('voters') || []).map(user => user.id);
const diffIds = _.difference(newIds, oldIds);
if (diffIds.length > 0) {
ids[optionId] = diffIds;
updated = true;
}
});
if (updated) {
ajax("/polls/voters.json", {
type: "put",
data: { options: ids }
}).then(result => {
const voters = result.voters;
this.get('options').forEach(option => {
const optionVoters = voters[option.get('id')];
if (!optionVoters) return;
option.set('voters', _.uniq(
(option.get('voters') || []).concat(optionVoters),
user => user.id
));
if (defer) defer.resolve();
});
}).catch((error) => {
Ember.Logger.error(error);
bootbox.alert(I18n.t('poll.error_while_fetching_voters'));
});
} else {
if (defer) defer.resolve();
}
}
}
});

View file

@ -0,0 +1,29 @@
import computed from 'ember-addons/ember-computed-decorators';
export default Ember.Component.extend({
layoutName: "components/poll-voters",
classNames: ["poll-voters"],
offset: 1,
loading: false,
voters: Ember.computed.alias('poll.pollVoters'),
@computed('poll.voters', 'poll.pollVoters')
canLoadMore(voters, pollVoters) {
return (!pollVoters) ? false : pollVoters.length < voters;
},
actions: {
loadMore() {
this.set('loading', true);
const defer = Em.RSVP.defer();
defer.promise.then(() => {
this.set('loading', false);
this.incrementProperty('offset');
});
const offset = this.get('offset');
this.sendAction('fetch', this.get('voterIds').slice(20 * offset, 20 * (offset + 1)), defer);
}
}
});

View file

@ -0,0 +1,33 @@
import computed from 'ember-addons/ember-computed-decorators';
export default Ember.Component.extend({
layoutName: "components/poll-voters",
classNames: ["poll-voters"],
offset: 1,
loading: false,
voters: Ember.computed.alias('option.voters'),
@computed('option.votes', 'option.voters')
canLoadMore(votes, voters) {
return (!voters) ? false : voters.length < votes;
},
actions: {
loadMore() {
this.set('loading', true);
const defer = Em.RSVP.defer();
defer.promise.then(() => {
this.set('loading', false);
this.incrementProperty('offset');
});
const offset = this.get('offset');
const ids = this.get('option.voter_ids');
const optionVoterIds = {};
optionVoterIds[this.get('option.id')] = ids.slice(20 * offset, 20 * (offset + 1));
this.sendAction('fetch', optionVoterIds, defer);
}
}
});

View file

@ -1,52 +0,0 @@
import { ajax } from 'discourse/lib/ajax';
export default Ember.Component.extend({
layoutName: "components/poll-voters",
tagName: 'ul',
classNames: ["poll-voters-list"],
isExpanded: false,
numOfVotersToShow: 0,
offset: 0,
loading: false,
pollsVoters: null,
init() {
this._super();
this.set("pollsVoters", []);
},
_fetchUsers() {
this.set("loading", true);
ajax("/polls/voters.json", {
type: "get",
data: { user_ids: this.get("voterIds") }
}).then(result => {
if (this.isDestroyed) return;
this.set("pollsVoters", this.get("pollsVoters").concat(result.users));
this.incrementProperty("offset");
this.set("loading", false);
}).catch((error) => {
Ember.logger.log(error);
bootbox.alert(I18n.t('poll.error_while_fetching_voters'));
});
},
_getIds(ids) {
const numOfVotersToShow = this.get("numOfVotersToShow");
const offset = this.get("offset");
return ids.slice(numOfVotersToShow * offset, numOfVotersToShow * (offset + 1));
},
didInsertElement() {
this._super();
this.set("numOfVotersToShow", Math.round(this.$().width() / 25) * 2);
if (this.get("voterIds").length > 0) this._fetchUsers();
},
actions: {
loadMore() {
this._fetchUsers();
}
}
});

View file

@ -3,5 +3,5 @@
</div>
{{#if isPublic}}
{{poll-results-number-voters poll=poll}}
{{poll-voters-number voterIds=voterIds poll=poll fetch='fetchUsers'}}
{{/if}}

View file

@ -11,7 +11,7 @@
</div>
{{#if isPublic}}
{{poll-results-standard-voters option=option}}
{{poll-voters-standard option=option fetch='fetchUsers'}}
{{/if}}
</li>
{{/each}}

View file

@ -1,17 +1,17 @@
<div class="poll-voters">
{{#each pollsVoters as |user|}}
<li>
<a data-user-card={{unbound user.username}}>
<ul class="poll-voters-list">
{{#each voters as |user|}}
<li class="poll-voters-list-item">
<a data-user-card={{unbound user.username}}>
{{avatar user imageSize="tiny" ignoreTitle="true"}}
</a>
</a>
</li>
{{/each}}
</ul>
{{#if canLoadMore}}
<div class="poll-voters-toggle-expand">
{{#if canLoadMore}}
{{#conditional-loading-spinner condition=loading size="small"}}
<a {{action "loadMore"}}>{{fa-icon "chevron-down"}}</a>
{{/conditional-loading-spinner}}
{{/if}}
{{#conditional-loading-spinner condition=loading size="small"}}
<a {{action "loadMore"}}>{{fa-icon "chevron-down"}}</a>
{{/conditional-loading-spinner}}
</div>
</div>
{{/if}}

View file

@ -83,6 +83,14 @@ div.poll {
.poll-voters-toggle-expand {
width: 100%;
text-align: center;
.inline-spinner {
display: block;
}
a {
display: block;
}
}
.results {

View file

@ -211,20 +211,24 @@ after_initialize do
end
def voters
user_ids = params.require(:user_ids)
options = params.require(:options)
user_ids = options.values.flatten.uniq
users = Hash[User.where(id: user_ids).map { |user| [user.id.to_s, UserNameSerializer.new(user).serializable_hash] }]
users = User.where(id: user_ids).map do |user|
UserNameSerializer.new(user).serializable_hash
voters = {}
options.each do |key, value|
voters[key] = value.map { |user_id| users[user_id] }
end
render json: { users: users }
render json: { voters: voters }
end
end
DiscoursePoll::Engine.routes.draw do
put "/vote" => "polls#vote"
put "/toggle_status" => "polls#toggle_status"
get "/voters" => 'polls#voters'
put "/voters" => 'polls#voters'
end
Discourse::Application.routes.append do