mirror of
https://github.com/discourse/discourse.git
synced 2025-02-27 14:28:42 +08:00

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
486 lines
14 KiB
JavaScript
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;
|