define('home/models/UserProfile',[
'jquery',
'underscore',
'backbone',
'home/utils/backboneUtils',
'home/collections/PaginatedUserCollection',
'home/collections/FeedItemCollection',
'home/collections/FollowingForumCollection',
'home/collections/FollowingChannelCollection',
'home/collections/FollowingUserCollection',
'home/collections/MostActiveForumCollection',
'home/collections/UserManagedChannelCollection',
'core/UniqueModel',
'home/models/User',
'home/models/Feed',
'home/models/UserModerationDetails',
'core/api',
], function (
$,
_,
Backbone,
backboneUtils,
PaginatedUserCollection,
FeedItemCollection,
FollowingForumCollection,
FollowingChannelCollection,
FollowingUserCollection,
MostActiveForumCollection,
UserManagedChannelCollection,
UniqueModel,
User,
Feed,
UserModerationDetails,
api
) {
'use strict';
var UserProfile = Backbone.Model.extend({
idAttribute: 'username',
initialize: function (_attributes, options) {
this.initializeRelated(
'user',
_.defaults({
user: { username: this.get('username') },
}, options),
User
);
this.user.listenTo(this, 'change:username', this.updateUsername);
this.listenTo(this.user, 'change:isFollowing', this.updateFollowers);
var userFetchString = 'username:' + this.user.get('username');
var fetchOptions = {
user: userFetchString,
order: 'desc',
};
this.forumsFollowing = new Feed({
fetchOptions: fetchOptions,
}, {
collectionClass: FollowingForumCollection,
});
// channelsFollowing is a list of channels that are followed directly because
// their primary forums cannot be followed (as opposed to followedChannels below,
// which contains channels followed via any means).
// ex: Disqus Picks is followed by too many people, if the forum were followed
// it would result in activities being copied to too many users' activity feeds.
this.channelsFollowing = new Feed({
fetchOptions: fetchOptions,
}, {
collectionClass: FollowingChannelCollection,
});
this.listenToOnce(this.channelsFollowing, 'items:sync', function () {
this.user.set({
numChannelsFollowing: this.channelsFollowing.items.length,
});
});
this.syncFollowingCommunities();
this.following = new Feed({
fetchOptions: fetchOptions,
}, {
collectionClass: FollowingUserCollection,
});
this.followers = new Feed({
fetchOptions: fetchOptions,
}, {
collectionClass: PaginatedUserCollection,
collectionUrl: api.getURL('users/listFollowers'),
});
this.mostActiveForums = new Feed({
fetchOptions: {
user: userFetchString,
limit: 5,
},
}, {
collectionClass: MostActiveForumCollection,
});
// A feed of channels the user manages: either as a mod or creator
this.managedChannels = new Feed(undefined, {
collectionClass: UserManagedChannelCollection,
collectionOptions: {
user: this.user,
},
});
// A feed of channels the user follows, either directly or via a primary forum.
// Excludes managed channels, as those are fetched in another feed.
this.followedChannels = new Feed({
fetchOptions: {
user: userFetchString,
// Endpoint expects 1 for true, 0 for false
includeFollowedPrimaryForums: 1,
excludeModerated: 1,
excludeOwned: 1,
},
}, {
collectionClass: FollowingChannelCollection,
collectionOptions: {
url: api.getURL('users/listFollowingChannels'),
},
});
// For the endpoints that expect a target argument of the forum
// <object_type>:<object_id>
// ex: {target: 'user:username:joesmith'}
var target = 'user:' + userFetchString;
this.posts = new Feed(undefined, {
collectionClass: FeedItemCollection,
collectionOptions: {
type: 'profile',
index: 'comments',
target: target,
},
});
this.favorites = new Feed(undefined, {
collectionClass: FeedItemCollection,
collectionOptions: {
type: 'profile',
index: 'favorites',
target: target,
},
});
this.threads = new Feed(undefined, {
collectionClass: FeedItemCollection,
collectionOptions: {
type: 'profile',
index: 'threads',
target: target,
},
});
this.profileFeed = new Feed(undefined, {
collectionClass: FeedItemCollection,
collectionOptions: {
type: 'profile',
target: target,
},
});
this.moderationDetails = new UserModerationDetails({
username: this.user.get('username'),
});
this.listenTo(this.user, 'change:isOnGlobalBlacklist', this.handleChangeGlobalBlacklist);
this.listenTo(this.user, 'blacklist:add', this.removeUserDiscussions);
this.listenTo(this.user, 'change:username', this.updateUsernameInChildren);
},
// On the frontend we want to treat followed channels and followed forums the
// same. Copy all the followed channels' primary forums into the followed
// forums list so they will be shown where we show followed communities.
syncFollowingCommunities: function () {
var forums = this.forumsFollowing.items;
var channels = this.channelsFollowing.items;
this.listenTo(channels, {
reset: function () {
forums.add(_.pluck(channels.models, 'primaryForum'), { at: 0 });
},
add: function (channel) {
forums.add(channel.primaryForum, { at: 0 });
},
remove: function (channel) {
forums.remove(channel.primaryForum);
},
});
// When forumsFollowing is fetched it'll reset and overwrite all the forums
// manually added from followingChannels. Re-add them.
this.listenTo(forums, 'reset', function () {
forums.add(_.pluck(channels.models, 'primaryForum'), { at: 0 });
});
},
handleChangeGlobalBlacklist: function () {
if (!this.user.get('isOnGlobalBlacklist'))
return;
this.removeUserDiscussions();
},
// Pages through all the user's discussions and deletes them. If a `forum` is
// passed, it'll only delete discussions from that forum.
removeUserDiscussions: function (forum) {
// If we've already fetched this user's discussions, go ahead and start
// loading the next page with `fetchItems()`. If we haven't fetched threads
// yet, call `ensureFetched()` because this handles the logic of not
// duplicating a fetch that's in progress.
var fetchPromise = this.threads.get('fetched') ? this.threads.fetchItems() : this.threads.ensureFetched();
fetchPromise.then(_.bind(function () {
this.threads.items.each(function (threadActivity) {
var thread = threadActivity.thread;
if (forum && thread.forum.id !== forum)
return;
thread.removeThread();
});
// Repeat as long as there's more pages
if (this.threads.get('hasNext'))
this.removeUserDiscussions(forum);
}, this));
},
isPrivate: function () {
return !this.user.isSessionUser() && this.user.get('isPrivate');
},
isBlocked: function () {
return !this.user.isSessionUser() && this.user.get('isBlocked');
},
ensureFetched: function () {
// We require Session inline because of a circular dependency
// between the UserProfile and Session models
var Session = require('home/models/Session');
var session = Session.get();
session.loadPromise().always(_.bind(function () {
var isGlobalAdmin = session.user.isGlobalAdmin();
// Wait until we can tell if the user is private
// before fetching anything else
backboneUtils.getPromiseFor(this.user, 'isPrivate').then(_.bind(function () {
if (this.isPrivate() && !isGlobalAdmin)
return;
this.favorites.ensureFetched();
this.posts.ensureFetched();
this.threads.ensureFetched();
this.mostActiveForums.ensureFetched();
// Don't fetch followedChannels or managedChannels. Those are only
// shown for the session user and are fetched directly when needed.
// numChannelsFollowing is not returned by the user details API endpoint
// like both numFollowing (users) and numForumsFollowing are,
// so we need to fetch channels to get the count
this.channelsFollowing.ensureFetched();
// Only load moderation details for Disqus admins
var self = this;
if (isGlobalAdmin) {
self.moderationDetails.ensureFetched();
$.when(self.moderationDetails.fetchPromise).then(function () {
self.trigger('moderationDetailsReady');
});
}
}, this));
}, this));
this.fetchUser();
},
ensureFetchedAllChannels: function () {
this.managedChannels.ensureFetched();
this.followedChannels.ensureFetched();
},
toJSON: function () {
return _.defaults({
user: this.user.toJSON(),
following: this.following.toJSON(),
followers: this.followers.toJSON(),
mostActiveForums: this.mostActiveForums.toJSON(),
posts: this.posts.toJSON(),
threads: this.threads.toJSON(),
profileFeed: this.profileFeed.toJSON(),
moderationDetails: this.moderationDetails.toJSON(),
}, Backbone.Model.prototype.toJSON.call(this));
},
/**
* Makes the necessary fetch calls in order to render the profile
* view for this user.
*/
fetchUser: function () {
// If this user has an id, it should also have a username. Users
// get ids along with all the rest of the user details, whereas
// it's possible to have just a username with no other details.
if (!this.user.get('username'))
throw new Error('UserProfile requires a username or id in order to be fetched');
if (this.user.shouldFetch())
this.user.fetch({ data: { user: 'username:' + this.user.get('username') } });
},
fetchFollowing: function () {
this.following.fetchItems({ reset: true });
},
fetchForumsFollowing: function () {
this.forumsFollowing.fetchItems({ reset: true });
},
fetchChannelsFollowing: function () {
this.channelsFollowing.fetchItems({ reset: true });
},
/**
* Updates the list of this user's followers when the
* session user follows/unfollows this user.
*/
updateFollowers: function () {
var Session = require('home/models/Session');
var session = Session.get();
// Don't add an anonymous user to the followers list.
//
// (If an anon user tried to follow someone, they'll be prompted
// to login. That happens elsewhere and may happen after this
// handler)
if (session.isAnonymous())
return;
var items = this.followers.get('items');
if (this.user.get('isFollowing'))
items.add(session.user);
else
items.remove(session.user);
},
/**
* Updates an object's username.
* Meant to be used as an event listener
*
* @param {Object} _model - the Backbone model that was updated
* @param {string} username - the new username
* @returns {void}
*/
updateUsername: function (_model, username) {
this.set('username', username);
},
/**
* Updates the username for children.
* Meant to be used as an event listener, listening to changes to this.user.username.
*
* Doesn't need to update `this.managedChannels` because it has a reference to `this.user`.
*
* @param {Object} _model - the Backbone model that was updated
* @param {string} username - the new username
* @returns {void}
*/
updateUsernameInChildren: function (_model, username) {
// Some places use `username:<username>` and others use `user:username:<username>`.
// The ones that use `username:<username>` are initialized with `fetchOptions`.
// The ones that use `user:username:<username>` are initialized with `collectionOptions`.
var targetUsername = 'username:' + username;
var usernameObj = { user: targetUsername };
var objsUsingFetchOptions = [this.forumsFollowing, this.channelsFollowing, this.following,
this.followers, this.mostActiveForums, this.followedChannels];
_.each(objsUsingFetchOptions, function (feed) {
feed.set('fetchOptions', _.extend(feed.get('fetchOptions'), usernameObj));
});
targetUsername = 'user:' + targetUsername;
var uniqueTogether = { target: targetUsername };
var objsUsingUniqueTogether = [this.posts, this.favorites, this.threads, this.profileFeed];
_.each(objsUsingUniqueTogether, function (feed) {
feed.items.updateUniqueTogether(uniqueTogether);
});
this.moderationDetails.set('username', username); // Just has a reference to username
},
});
UniqueModel.addType('UserProfile', UserProfile);
return UserProfile;
});
// https://c.disquscdn.com/next/82c6de3/home/js/models/UserProfile.js