Sam 3829c78526 PERF: shift most user options out of the user table
As it stands we load up user records quite frequently on the topic pages,
this in turn pulls all the columns for the users being selected, just to
discard them after they are loaded

New structure keeps all options in a discrete table, this is better organised
and allows us to easily add more column without worrying about bloating the
user table
2016-02-17 18:08:25 +11:00

486 lines
14 KiB
JavaScript

import { url } from 'discourse/lib/computed';
import RestModel from 'discourse/models/rest';
import UserStream from 'discourse/models/user-stream';
import UserPostsStream from 'discourse/models/user-posts-stream';
import Singleton from 'discourse/mixins/singleton';
import { longDate } from 'discourse/lib/formatter';
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
import Badge from 'discourse/models/badge';
import UserBadge from 'discourse/models/user-badge';
import UserActionStat from 'discourse/models/user-action-stat';
import UserAction from 'discourse/models/user-action';
import Group from 'discourse/models/group';
import Topic from 'discourse/models/topic';
const User = RestModel.extend({
hasPMs: Em.computed.gt("private_messages_stats.all", 0),
hasStartedPMs: Em.computed.gt("private_messages_stats.mine", 0),
hasUnreadPMs: Em.computed.gt("private_messages_stats.unread", 0),
hasPosted: Em.computed.gt("post_count", 0),
hasNotPosted: Em.computed.not("hasPosted"),
canBeDeleted: Em.computed.and("can_be_deleted", "hasNotPosted"),
redirected_to_top: {
reason: null,
},
@computed()
stream() {
return UserStream.create({ user: this });
},
@computed()
postsStream() {
return UserPostsStream.create({ user: this });
},
staff: Em.computed.or('admin', 'moderator'),
destroySession() {
return Discourse.ajax(`/session/${this.get('username')}`, { type: 'DELETE'});
},
@computed("username_lower")
searchContext(username) {
return {
type: 'user',
id: username,
user: this
};
},
@computed("username", "name")
displayName(username, name) {
if (Discourse.SiteSettings.enable_names && !Ember.isEmpty(name)) {
return name;
}
return username;
},
@computed('profile_background')
profileBackground(bgUrl) {
if (Em.isEmpty(bgUrl) || !Discourse.SiteSettings.allow_profile_backgrounds) { return; }
return ('background-image: url(' + Discourse.getURLWithCDN(bgUrl) + ')').htmlSafe();
},
@computed()
path() {
// no need to observe, requires a hard refresh to update
return Discourse.getURL(`/users/${this.get('username_lower')}`);
},
pmPath(topic) {
const userId = this.get('id');
const username = this.get('username_lower');
const details = topic && topic.get('details');
const allowedUsers = details && details.get('allowed_users');
const groups = details && details.get('allowed_groups');
// directly targetted so go to inbox
if (!groups || (allowedUsers && allowedUsers.findBy("id", userId))) {
return Discourse.getURL(`/users/${username}/messages`);
} else {
if (groups && groups[0])
{
return Discourse.getURL(`/users/${username}/messages/group/${groups[0].name}`);
}
}
},
adminPath: url('id', 'username_lower', "/admin/users/%@1/%@2"),
mutedTopicsPath: url('/latest?state=muted'),
@computed("username")
username_lower(username) {
return username.toLowerCase();
},
@computed("trust_level")
trustLevel(trustLevel) {
return Discourse.Site.currentProp('trustLevels').findProperty('id', parseInt(trustLevel, 10));
},
isBasic: Em.computed.equal('trust_level', 0),
isLeader: Em.computed.equal('trust_level', 3),
isElder: Em.computed.equal('trust_level', 4),
canManageTopic: Em.computed.or('staff', 'isElder'),
isSuspended: Em.computed.equal('suspended', true),
@computed("suspended_till")
suspended(suspendedTill) {
return suspendedTill && moment(suspendedTill).isAfter();
},
@computed("suspended_till")
suspendedTillDate(suspendedTill) {
return longDate(suspendedTill);
},
changeUsername(new_username) {
return Discourse.ajax(`/users/${this.get('username_lower')}/preferences/username`, {
type: 'PUT',
data: { new_username }
});
},
changeEmail(email) {
return Discourse.ajax(`/users/${this.get('username_lower')}/preferences/email`, {
type: 'PUT',
data: { email }
});
},
copy() {
return Discourse.User.create(this.getProperties(Ember.keys(this)));
},
save() {
const data = this.getProperties(
'auto_track_topics_after_msecs',
'bio_raw',
'website',
'location',
'name',
'locale',
'new_topic_duration_minutes',
'custom_fields',
'user_fields',
'muted_usernames',
'profile_background',
'card_background'
);
[ 'email_always',
'mailing_list_mode',
'external_links_in_new_tab',
'email_digests',
'email_direct',
'email_private_messages',
'dynamic_favicon',
'enable_quoting',
'disable_jump_reply',
'automatically_unpin_topics',
'digest_after_days'
].forEach(s => {
data[s] = this.get(`user_option.${s}`);
});
['muted','watched','tracked'].forEach(s => {
let cats = this.get(s + 'Categories').map(c => c.get('id'));
// HACK: denote lack of categories
if (cats.length === 0) { cats = [-1]; }
data[s + '_category_ids'] = cats;
});
if (!Discourse.SiteSettings.edit_history_visible_to_public) {
data['edit_history_public'] = this.get('user_option.edit_history_public');
}
// TODO: We can remove this when migrated fully to rest model.
this.set('isSaving', true);
return Discourse.ajax(`/users/${this.get('username_lower')}`, {
data: data,
type: 'PUT'
}).then(result => {
this.set('bio_excerpt', result.user.bio_excerpt);
const userProps = Em.getProperties(this.get('user-option'),'enable_quoting', 'external_links_in_new_tab', 'dynamic_favicon');
Discourse.User.current().setProperties(userProps);
}).finally(() => {
this.set('isSaving', false);
});
},
changePassword() {
return Discourse.ajax("/session/forgot_password", {
dataType: 'json',
data: { login: this.get('username') },
type: 'POST'
});
},
loadUserAction(id) {
const stream = this.get('stream');
return Discourse.ajax(`/user_actions/${id}.json`, { cache: 'false' }).then(result => {
if (result && result.user_action) {
const ua = result.user_action;
if ((this.get('stream.filter') || ua.action_type) !== ua.action_type) return;
if (!this.get('stream.filter') && !this.inAllStream(ua)) return;
ua.title = Discourse.Emoji.unescape(Handlebars.Utils.escapeExpression(ua.title));
const action = UserAction.collapseStream([UserAction.create(ua)]);
stream.set('itemsLoaded', stream.get('itemsLoaded') + 1);
stream.get('content').insertAt(0, action[0]);
}
});
},
inAllStream(ua) {
return ua.action_type === UserAction.TYPES.posts ||
ua.action_type === UserAction.TYPES.topics;
},
@computed("groups.@each")
displayGroups() {
const groups = this.get('groups');
const filtered = groups.filter(group => {
return !group.automatic || group.name === "moderators";
});
return filtered.length === 0 ? null : filtered;
},
// The user's stat count, excluding PMs.
@computed("statsExcludingPms.@each.count")
statsCountNonPM() {
if (Ember.isEmpty(this.get('statsExcludingPms'))) return 0;
let count = 0;
_.each(this.get('statsExcludingPms'), val => {
if (this.inAllStream(val)) {
count += val.count;
}
});
return count;
},
// The user's stats, excluding PMs.
@computed("stats.@each.isPM")
statsExcludingPms() {
if (Ember.isEmpty(this.get('stats'))) return [];
return this.get('stats').rejectProperty('isPM');
},
findDetails(options) {
const user = this;
return PreloadStore.getAndRemove(`user_${user.get('username')}`, () => {
return Discourse.ajax(`/users/${user.get('username')}.json`, { data: options });
}).then(json => {
if (!Em.isEmpty(json.user.stats)) {
json.user.stats = Discourse.User.groupStats(_.map(json.user.stats, s => {
if (s.count) s.count = parseInt(s.count, 10);
return UserActionStat.create(s);
}));
}
if (!Em.isEmpty(json.user.groups)) {
json.user.groups = json.user.groups.map(g => Group.create(g));
}
if (json.user.invited_by) {
json.user.invited_by = Discourse.User.create(json.user.invited_by);
}
if (!Em.isEmpty(json.user.featured_user_badge_ids)) {
const userBadgesMap = {};
UserBadge.createFromJson(json).forEach(userBadge => {
userBadgesMap[ userBadge.get('id') ] = userBadge;
});
json.user.featured_user_badges = json.user.featured_user_badge_ids.map(id => userBadgesMap[id]);
}
if (json.user.card_badge) {
json.user.card_badge = Badge.create(json.user.card_badge);
}
user.setProperties(json.user);
return user;
});
},
findStaffInfo() {
if (!Discourse.User.currentProp("staff")) { return Ember.RSVP.resolve(null); }
return Discourse.ajax(`/users/${this.get("username_lower")}/staff-info.json`).then(info => {
this.setProperties(info);
});
},
pickAvatar(upload_id, type, avatar_template) {
return Discourse.ajax(`/users/${this.get("username_lower")}/preferences/avatar/pick`, {
type: 'PUT',
data: { upload_id, type }
}).then(() => this.setProperties({
avatar_template,
uploaded_avatar_id: upload_id
}));
},
isAllowedToUploadAFile(type) {
return this.get('staff') ||
this.get('trust_level') > 0 ||
Discourse.SiteSettings['newuser_max_' + type + 's'] > 0;
},
createInvite(email, group_names) {
return Discourse.ajax('/invites', {
type: 'POST',
data: { email, group_names }
});
},
generateInviteLink(email, group_names, topic_id) {
return Discourse.ajax('/invites/link', {
type: 'POST',
data: { email, group_names, topic_id }
});
},
@observes("muted_category_ids")
updateMutedCategories() {
this.set("mutedCategories", Discourse.Category.findByIds(this.muted_category_ids));
},
@observes("tracked_category_ids")
updateTrackedCategories() {
this.set("trackedCategories", Discourse.Category.findByIds(this.tracked_category_ids));
},
@observes("watched_category_ids")
updateWatchedCategories() {
this.set("watchedCategories", Discourse.Category.findByIds(this.watched_category_ids));
},
@computed("can_delete_account", "reply_count", "topic_count")
canDeleteAccount(canDeleteAccount, replyCount, topicCount) {
return !Discourse.SiteSettings.enable_sso && canDeleteAccount && ((replyCount || 0) + (topicCount || 0)) <= 1;
},
"delete": function() {
if (this.get('can_delete_account')) {
return Discourse.ajax("/users/" + this.get('username'), {
type: 'DELETE',
data: {context: window.location.pathname}
});
} else {
return Ember.RSVP.reject(I18n.t('user.delete_yourself_not_allowed'));
}
},
dismissBanner(bannerKey) {
this.set("dismissed_banner_key", bannerKey);
Discourse.ajax(`/users/${this.get('username')}`, {
type: 'PUT',
data: { dismissed_banner_key: bannerKey }
});
},
checkEmail() {
return Discourse.ajax(`/users/${this.get("username_lower")}/emails.json`, {
type: "PUT",
data: { context: window.location.pathname }
}).then(result => {
if (result) {
this.setProperties({
email: result.email,
associated_accounts: result.associated_accounts
});
}
});
},
summary() {
return Discourse.ajax(`/users/${this.get("username_lower")}/summary.json`)
.then(json => {
const topicMap = {};
json.topics.forEach(t => {
topicMap[t.id] = Topic.create(t);
});
const badgeMap = {};
Badge.createFromJson(json).forEach(b => {
badgeMap[b.id] = b;
});
const summary = json["user_summary"];
summary.replies.forEach(r => {
r.topic = topicMap[r.topic_id];
r.url = r.topic.urlForPostNumber(r.post_number);
r.createdAt = new Date(r.created_at);
});
summary.topics = summary.topic_ids.map(id => topicMap[id]);
summary.badges = summary.badges.map(ub => {
const badge = badgeMap[ub.badge_id];
badge.count = ub.count;
return badge;
});
return summary;
});
}
});
User.reopenClass(Singleton, {
// Find a `Discourse.User` for a given username.
findByUsername(username, options) {
const user = User.create({username: username});
return user.findDetails(options);
},
// TODO: Use app.register and junk Singleton
createCurrent() {
const userJson = PreloadStore.get('currentUser');
if (userJson) {
const store = Discourse.__container__.lookup('store:main');
return store.createRecord('user', userJson);
}
return null;
},
checkUsername(username, email, for_user_id) {
return Discourse.ajax('/users/check_username', {
data: { username, email, for_user_id }
});
},
groupStats(stats) {
const responses = UserActionStat.create({
count: 0,
action_type: UserAction.TYPES.replies
});
stats.filterProperty('isResponse').forEach(stat => {
responses.set('count', responses.get('count') + stat.get('count'));
});
const result = Em.A();
result.pushObjects(stats.rejectProperty('isResponse'));
let insertAt = 0;
result.forEach((item, index) => {
if (item.action_type === UserAction.TYPES.topics || item.action_type === UserAction.TYPES.posts) {
insertAt = index + 1;
}
});
if (responses.count > 0) {
result.insertAt(insertAt, responses);
}
return result;
},
createAccount(attrs) {
return Discourse.ajax("/users", {
data: {
name: attrs.accountName,
email: attrs.accountEmail,
password: attrs.accountPassword,
username: attrs.accountUsername,
password_confirmation: attrs.accountPasswordConfirm,
challenge: attrs.accountChallenge,
user_fields: attrs.userFields
},
type: 'POST'
});
}
});
export default User;