/** A data model representing a user on Discourse @class User @extends Discourse.Model @namespace Discourse @module Discourse **/ Discourse.User = Discourse.Model.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), /** The user's stream @property stream @type {Discourse.UserStream} **/ stream: function() { return Discourse.UserStream.create({ user: this }); }.property(), /** Is this user a member of staff? @property staff @type {Boolean} **/ staff: Em.computed.or('admin', 'moderator'), searchContext: function() { return { type: 'user', id: this.get('username_lower'), user: this }; }.property('username_lower'), /** This user's display name. Returns the name if possible, otherwise returns the username. @property displayName @type {String} **/ displayName: function() { if (Discourse.SiteSettings.enable_names && !this.blank('name')) { return this.get('name'); } return this.get('username'); }.property('username', 'name'), /** This user's website. @property websiteName @type {String} **/ websiteName: function() { var website = this.get('website'); if (Em.isEmpty(website)) { return; } return this.get('website').split("/")[2]; }.property('website'), /** This user's profile background(in CSS). @property websiteName @type {String} **/ profileBackground: function() { var background = this.get('profile_background'); if(Em.isEmpty(background) || !Discourse.SiteSettings.allow_profile_backgrounds) { return; } return 'background-image: url(' + background + ')'; }.property('profile_background'), statusIcon: function() { var name = Handlebars.Utils.escapeExpression(this.get('name')), desc; if(this.get('admin')) { desc = I18n.t('user.admin', {user: name}); return ''; } if(this.get('moderator')){ desc = I18n.t('user.moderator', {user: name}); return ''; } return null; }.property('admin','moderator'), /** Path to this user. @property path @type {String} **/ path: Discourse.computed.url('username_lower', "/users/%@"), /** Path to this user's administration @property adminPath @type {String} **/ adminPath: Discourse.computed.url('username_lower', "/admin/users/%@"), /** This user's username in lowercase. @property username_lower @type {String} **/ username_lower: function() { return this.get('username').toLowerCase(); }.property('username'), /** This user's trust level. @property trustLevel @type {Integer} **/ trustLevel: function() { return Discourse.Site.currentProp('trustLevels').findProperty('id', parseInt(this.get('trust_level'), 10)); }.property('trust_level'), isElder: Em.computed.equal('trust_level', 4), canManageTopic: Em.computed.or('staff', 'isElder'), isSuspended: Em.computed.equal('suspended', true), suspended: function() { return this.get('suspended_till') && moment(this.get('suspended_till')).isAfter(); }.property('suspended_till'), suspendedTillDate: function() { return Discourse.Formatter.longDate(this.get('suspended_till')); }.property('suspended_till'), /** Changes this user's username. @method changeUsername @param {String} newUsername The user's new username @returns Result of ajax call **/ changeUsername: function(newUsername) { return Discourse.ajax("/users/" + this.get('username_lower') + "/preferences/username", { type: 'PUT', data: { new_username: newUsername } }); }, /** Changes this user's email address. @method changeEmail @param {String} email The user's new email address\ @returns Result of ajax call **/ changeEmail: function(email) { return Discourse.ajax("/users/" + this.get('username_lower') + "/preferences/email", { type: 'PUT', data: { email: email } }); }, /** Returns a copy of this user. @method copy @returns {User} **/ copy: function() { return Discourse.User.create(this.getProperties(Ember.keys(this))); }, /** Save's this user's properties over AJAX via a PUT request. @method save @returns {Promise} the result of the operation **/ save: function() { var user = this; var data = this.getProperties('auto_track_topics_after_msecs', 'bio_raw', 'website', 'name', 'locale', 'email_digests', 'email_direct', 'email_always', 'email_private_messages', 'dynamic_favicon', 'digest_after_days', 'new_topic_duration_minutes', 'external_links_in_new_tab', 'mailing_list_mode', 'enable_quoting'); _.each(['muted','watched','tracked'], function(s){ var cats = user.get(s + 'Categories').map(function(c){ return c.get('id')}); // HACK: denote lack of categories if(cats.length === 0) { cats = [-1]; } data[s + '_category_ids'] = cats; }); return Discourse.ajax("/users/" + this.get('username_lower'), { data: data, type: 'PUT' }).then(function(data) { user.set('bio_excerpt',data.user.bio_excerpt); _.each([ 'enable_quoting', 'external_links_in_new_tab', 'dynamic_favicon' ], function(preference) { Discourse.User.current().set(preference, user.get(preference)); }); }); }, /** Changes the password and calls the callback function on AJAX.complete. @method changePassword @returns {Promise} the result of the change password operation **/ changePassword: function() { return Discourse.ajax("/session/forgot_password", { dataType: 'json', data: { login: this.get('username') }, type: 'POST' }); }, /** Loads a single user action by id. @method loadUserAction @param {Integer} id The id of the user action being loaded @returns A stream of the user's actions containing the action of id **/ loadUserAction: function(id) { var user = this; var stream = this.get('stream'); return Discourse.ajax("/user_actions/" + id + ".json", { cache: 'false' }).then(function(result) { if (result) { if ((user.get('streamFilter') || result.action_type) !== result.action_type) return; var action = Discourse.UserAction.collapseStream([Discourse.UserAction.create(result)]); stream.set('itemsLoaded', user.get('itemsLoaded') + 1); stream.insertAt(0, action[0]); } }); }, /** The user's stat count, excluding PMs. @property statsCountNonPM @type {Integer} **/ statsCountNonPM: function() { if (this.blank('statsExcludingPms')) return 0; var count = 0; _.each(this.get('statsExcludingPms'), function(val) { count += val.count; }); return count; }.property('statsExcludingPms.@each.count'), /** The user's stats, excluding PMs. @property statsExcludingPms @type {Array} **/ statsExcludingPms: function() { if (this.blank('stats')) return []; return this.get('stats').rejectProperty('isPM'); }.property('stats.@each.isPM'), findDetails: function() { var user = this; return PreloadStore.getAndRemove("user_" + user.get('username'), function() { return Discourse.ajax("/users/" + user.get('username') + '.json'); }).then(function (json) { if (!Em.isEmpty(json.user.stats)) { json.user.stats = Discourse.User.groupStats(_.map(json.user.stats,function(s) { if (s.count) s.count = parseInt(s.count, 10); return Discourse.UserActionStat.create(s); })); } if (!Em.isEmpty(json.user.custom_groups)) { json.user.custom_groups = json.user.custom_groups.map(function (g) { return Discourse.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)) { var userBadgesMap = {}; Discourse.UserBadge.createFromJson(json).forEach(function(userBadge) { userBadgesMap[ userBadge.get('id') ] = userBadge; }); json.user.featured_user_badges = json.user.featured_user_badge_ids.map(function(id) { return userBadgesMap[id]; }); } user.setProperties(json.user); return user; }); }, /* Change avatar selection @method toggleAvatarSelection @param {Boolean} useUploadedAvatar true if the user is using the uploaded avatar @returns {Promise} the result of the toggle avatar selection */ toggleAvatarSelection: function(useUploadedAvatar) { return Discourse.ajax("/users/" + this.get("username_lower") + "/preferences/avatar/toggle", { type: 'PUT', data: { use_uploaded_avatar: useUploadedAvatar } }); }, /* Clear profile background @method clearProfileBackground @returns {Promise} the result of the clear profile background request */ clearProfileBackground: function() { var user = this; return Discourse.ajax("/users/" + this.get("username_lower") + "/preferences/profile_background/clear", { type: 'PUT', data: { } }).then(function() { user.set('profile_background', null); }); }, /** Determines whether the current user is allowed to upload a file. @method isAllowedToUploadAFile @param {String} type The type of the upload (image, attachment) @returns true if the current user is allowed to upload a file **/ isAllowedToUploadAFile: function(type) { return this.get('staff') || this.get('trust_level') > 0 || Discourse.SiteSettings['newuser_max_' + type + 's'] > 0; }, /** Invite a user to the site @method createInvite @param {String} email The email address of the user to invite to the site @returns {Promise} the result of the server call **/ createInvite: function(email, groupNames) { return Discourse.ajax('/invites', { type: 'POST', data: {email: email, group_names: groupNames} }); }, updateMutedCategories: function() { this.set("mutedCategories", Discourse.Category.findByIds(this.muted_category_ids)); }.observes("muted_category_ids"), updateTrackedCategories: function() { this.set("trackedCategories", Discourse.Category.findByIds(this.tracked_category_ids)); }.observes("tracked_category_ids"), updateWatchedCategories: function() { this.set("watchedCategories", Discourse.Category.findByIds(this.watched_category_ids)); }.observes("watched_category_ids"), canDeleteAccount: function() { return this.get('can_delete_account') && ((this.get('reply_count')||0) + (this.get('topic_count')||0)) <= 1; }.property('can_delete_account', 'reply_count', 'topic_count'), 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')); } } }); Discourse.User.reopenClass(Discourse.Singleton, { /** Find a `Discourse.User` for a given username. @method findByUsername @returns {Promise} a promise that resolves to a `Discourse.User` **/ findByUsername: function(username) { var user = Discourse.User.create({username: username}); return user.findDetails(); }, /** The current singleton will retrieve its attributes from the `PreloadStore` if it exists. Otherwise, no instance is created. @method createCurrent @returns {Discourse.User} the user, if logged in. **/ createCurrent: function() { var userJson = PreloadStore.get('currentUser'); if (userJson) { return Discourse.User.create(userJson); } return null; }, /** Logs out the currently logged in user @method logout @returns {Promise} resolved when the logout finishes **/ logout: function() { var discourseUserClass = this; return Discourse.ajax("/session/" + Discourse.User.currentProp('username'), { type: 'DELETE' }).then(function () { discourseUserClass.currentUser = null; }); }, /** Checks if given username is valid for this email address @method checkUsername @param {String} username A username to check @param {String} email An email address to check @param {Number} forUserId user id - provide when changing username **/ checkUsername: function(username, email, forUserId) { return Discourse.ajax('/users/check_username', { data: { username: username, email: email, for_user_id: forUserId } }); }, /** Groups the user's statistics @method groupStats @param {Array} stats Given stats @returns {Object} **/ groupStats: function(stats) { var responses = Discourse.UserActionStat.create({ count: 0, action_type: Discourse.UserAction.TYPES.replies }); stats.filterProperty('isResponse').forEach(function (stat) { responses.set('count', responses.get('count') + stat.get('count')); }); var result = Em.A(); result.pushObjects(stats.rejectProperty('isResponse')); var insertAt = 0; result.forEach(function(item, index){ if(item.action_type === Discourse.UserAction.TYPES.topics || item.action_type === Discourse.UserAction.TYPES.posts){ insertAt = index + 1; } }); if(responses.count > 0) { result.insertAt(insertAt, responses); } return(result); }, /** Creates a new account over POST @method createAccount @param {String} name This user's name @param {String} email This user's email @param {String} password This user's password @param {String} username This user's username @param {String} passwordConfirm This user's confirmed password @param {String} challenge @returns Result of ajax call **/ createAccount: function(name, email, password, username, passwordConfirm, challenge) { return Discourse.ajax("/users", { data: { name: name, email: email, password: password, username: username, password_confirmation: passwordConfirm, challenge: challenge }, type: 'POST' }); } });