Source: js/models/UserModel.js

/*global define */
define(['jquery', 'underscore', 'backbone', 'jws', 'models/Search', "collections/SolrResults"],
	function($, _, Backbone, JWS, SearchModel, SearchResults) {
	'use strict';

	/**
  * @class UserModel
  * @extends Backbone.Model
  * @constructor
  */
	var UserModel = Backbone.Model.extend(
    /** @lends UserModel.prototype */{
		defaults: function(){
			return{
				type: "person", //assume this is a person unless we are told otherwise - other possible type is a "group"
				checked: false, //Is set to true when we have checked the account/subject info of this user
        tokenChecked: false, //Is set to true when the uer auth token has been checked
				basicUser: false, //Set to true to only query for basic info about this user - prevents sending queries for info that will never be displayed in the UI
				lastName: null,
				firstName: null,
				fullName: null,
				email: null,
				logo: null,
				description: null,
				verified: null,
				username: null,
				usernameReadable: null,
				orcid: null,
				searchModel: null,
				searchResults: null,
				loggedIn: false,
				ldapError: false, //Was there an error logging in to LDAP
				registered: false,
				isMemberOf: [],
				isOwnerOf: [],
				identities: [],
				identitiesUsernames: [],
        allIdentitiesAndGroups: [],
				pending: [],
				token: null,
				expires: null,
				timeoutId: null,
				rawData: null,
        portalQuota: -1,
        isAuthorizedCreatePortal: null
			}
		},

		initialize: function(options){
			if(typeof options !== "undefined"){
				if(options.username) this.set("username", options.username);
				if(options.rawData)  this.set(this.parseXML(options.rawData));
			}

			this.on("change:identities", this.pluckIdentityUsernames);

			this.on("change:username change:identities change:type", this.updateSearchModel);
			this.createSearchModel();

			this.on("change:username", this.createReadableUsername());

			//Create a search results model for this person
			var searchResults = new SearchResults([], { rows: 5, start: 0 });
			this.set("searchResults", searchResults);
		},

		createSearchModel: function(){
			//Create a search model that will retrieve data created by this person
			this.set("searchModel", new SearchModel());
			this.updateSearchModel();
		},

		updateSearchModel: function(){
			if(this.get("type") == "node"){
				this.get("searchModel").set("dataSource", [this.get("node").identifier]);
				this.get("searchModel").set("username", []);
			}
			else{
				//Get all the identities for this person
				var ids = [this.get("username")];

				_.each(this.get("identities"), function(equivalentUser){
					ids.push(equivalentUser.get("username"));
				});
				this.get("searchModel").set("username", ids);
			}

			this.trigger("change:searchModel");
		},

		parseXML: function(data){
			var model = this,
				username = this.get("username");

			//Reset the group list so we don't just add it to it with push()
			this.set("isMemberOf", this.defaults().isMemberOf, {silent: true});
			this.set("isOwnerOf", this.defaults().isOwnerOf, {silent: true});
			//Reset the equivalent id list so we don't just add it to it with push()
			this.set("identities", this.defaults().identities, {silent: true});

			//Find this person's node in the XML
			var userNode = null;
			if(!username)
				var username = $(data).children("subject").text();
			if(username){
				var subjects = $(data).find("subject");
				for(var i=0; i<subjects.length; i++){
					if($(subjects[i]).text().toLowerCase() == username.toLowerCase()){
						userNode = $(subjects[i]).parent();
						break;
					}
				}
			}
			if(!userNode)
				userNode = $(data).first();

			//Get the type of user - either a person or group
			var type = $(userNode).prop("tagName");
			if(type) type = type.toLowerCase();

			if(type == "group"){
				var fullName = $(userNode).find("groupName").first().text();
			}
			else if(type){
				//Find the person's info
				var	firstName  = $(userNode).find("givenName").first().text(),
					lastName   = $(userNode).find("familyName").first().text(),
					email      = $(userNode).find("email").first().text(),
					verified   = $(userNode).find("verified").first().text(),
					memberOf   = this.get("isMemberOf"),
					ownerOf	   = this.get("isOwnerOf"),
					identities = this.get("identities");

				//Sometimes names are saved as "NA" when they are not available - translate these to false values
				if(firstName == "NA")
					firstName = null;
				if(lastName == "NA")
					lastName = null;

				//Construct the fullname from the first and last names, but watch out for falsely values
				var fullName = "";
					fullName += firstName? firstName : "";
					fullName += lastName? (" " + lastName) : "";

				if(!fullName)
					fullName = this.getNameFromSubject(username);

				//Don't get this detailed info about basic users
				if(!this.get("basicUser")){
					//Get all the equivalent identities for this user
					var equivalentIds = $(userNode).find("equivalentIdentity");
					if(equivalentIds.length > 0)
						var allPersons = $(data).find("person subject");

					_.each(equivalentIds, function(identity, i){
						//push onto the list
						var username = $(identity).text(),
							equivUserNode;

						//Find the matching person node in the response
						_.each(allPersons, function(person){
							if($(person).text().toLowerCase() == username.toLowerCase()){
								equivUserNode = $(person).parent().first();
								allPersons = _.without(allPersons, person);
							}
						});

						var equivalentUser = new UserModel({ username: username, basicUser: true, rawData: equivUserNode });
						identities.push(equivalentUser);
					});
				}

				//Get each group and save
				_.each($(data).find("group"), function(group, i){
					//Save group ID
					var groupId = $(group).find("subject").first().text(),
						groupName = $(group).find("groupName").text();

					memberOf.push({ groupId: groupId, name: groupName });

					//Check if this person is a rightsholder
					var allRightsHolders = [];
					_.each($(group).children("rightsHolder"), function(rightsHolder){
						allRightsHolders.push($(rightsHolder).text().toLowerCase());
					});
					if(_.contains(allRightsHolders, username.toLowerCase()))
						ownerOf.push(groupId);
				});
			}

      var allSubjects = _.pluck( this.get("isMemberOf"), "groupId" );
      allSubjects.push(this.get("username"));
      allSubjects.push(this.get("identities"));

			return {
				isMemberOf: memberOf,
				isOwnerOf: ownerOf,
				identities: identities,
        allIdentitiesAndGroups: allSubjects,
				verified: verified,
				username: username,
				firstName: firstName,
				lastName: lastName,
				fullName: fullName,
				email: email,
				registered: true,
				type: type,
				rawData: data
			}
		},

		getInfo: function(){
			var model = this;

			//If the accounts service is not on, flag this user as checked/completed
			if(!MetacatUI.appModel.get("accountsUrl")){
				this.set("fullName", this.getNameFromSubject());
				this.set("checked", true);
				return;
			}

			//Only proceed if there is a username
			if(!this.get("username")) return;

			//Get the user info using the DataONE API
			var url = MetacatUI.appModel.get("accountsUrl") + encodeURIComponent(this.get("username"));

			var requestSettings = {
				type: "GET",
				url: url,
				success: function(data, textStatus, xhr) {
					//Parse the XML response to get user info
					var userProperties = model.parseXML(data);
					//Filter out all the falsey values
					_.each(userProperties, function(v, k) {
				      if(!v) {
				        delete userProperties[k];
				      }
				    });
					model.set(userProperties);

					 //Trigger the change events
					model.trigger("change:isMemberOf");
					model.trigger("change:isOwnerOf");
					model.trigger("change:identities");

					model.set("checked", true);
				},
				error: function(xhr, textStatus, errorThrown){
					// Sometimes the node info has not been received before this getInfo() is called.
					// If the node info was received while this getInfo request was pending, and this user was determined
					// to be a node, then we can skip any further action here.
					if(model.get("type") == "node")
						return;

					if((xhr.status == 404) && MetacatUI.nodeModel.get("checked")){
						model.set("fullName", model.getNameFromSubject());
						model.set("checked", true);
					}
					else if((xhr.status == 404) && !MetacatUI.nodeModel.get("checked")){
						model.listenToOnce(MetacatUI.nodeModel, "change:checked", function(){
							if(!model.isNode()){
								model.set("fullName", model.getNameFromSubject());
								model.set("checked", true);
							}
						});
					}
					else{
						//As a backup, search for this user instead
						var requestSettings = {
								type: "GET",
								url: MetacatUI.appModel.get("accountsUrl") + "?query=" + encodeURIComponent(model.get("username")),
								success: function(data, textStatus, xhr) {
									//Parse the XML response to get user info
									model.set(model.parseXML(data));

									 //Trigger the change events
									model.trigger("change:isMemberOf");
									model.trigger("change:isOwnerOf");
									model.trigger("change:identities");

									model.set("checked", true);
								},
								error: function(){
									//Set some blank values and flag as checked
									//model.set("username", "");
									//model.set("fullName", "");
									model.set("notFound", true);
									model.set("checked", true);
								}
							}
						//Send the request
						$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));

					}
				}
			}

			//Send the request
			$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
		},

		//Get the pending identity map requests, if the service is turned on
		getPendingIdentities: function(){
			if(!MetacatUI.appModel.get("pendingMapsUrl")) return false;

			var model = this;

			//Get the pending requests
			var requestSettings = {
				url: MetacatUI.appModel.get("pendingMapsUrl") + encodeURIComponent(this.get("username")),
				success: function(data, textStatus, xhr){
					//Reset the equivalent id list so we don't just add it to it with push()
					model.set("pending", model.defaults().pending);
					var pending = model.get("pending");
					_.each($(data).find("person"), function(person, i) {

						//Don't list yourself as a pending map request
						var personsUsername = $(person).find("subject").text();
						if(personsUsername.toLowerCase() == model.get("username").toLowerCase())
							return;

						//Create a new User Model for this pending identity
						var pendingUser = new UserModel({ rawData: person });

						if(pendingUser.isOrcid())
							pendingUser.getInfo();

						pending.push(pendingUser);
					});
					model.set("pending", pending);
					model.trigger("change:pending"); //Trigger the change event
				},
				error: function(xhr, textStatus){
					if(xhr.responseText.indexOf("error code 34")){
						model.set("pending", model.defaults().pending);
						model.trigger("change:pending"); //Trigger the change event
					}
				}
			}

			$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
		},

		getNameFromSubject: function(username){
			var username  = username || this.get("username"),
				fullName = "";

			if(!username) return;

			if((username.indexOf("uid=") > -1) && (username.indexOf(",") > -1))
				fullName = username.substring(username.indexOf("uid=") + 4, username.indexOf(","));
			else if((username.indexOf("CN=") > -1) && (username.indexOf(",") > -1))
				fullName = username.substring(username.indexOf("CN=") + 3, username.indexOf(","));

			//Cut off the last string after the name when it contains digits - not part of this person's names
			if(fullName.lastIndexOf(" ") > fullName.indexOf(" ")){
				var lastWord = fullName.substring(fullName.lastIndexOf(" "));
				if(lastWord.search(/\d/) > -1)
					fullName = fullName.substring(0, fullName.lastIndexOf(" "));
			}

			//Default to the username
			if(!fullName) fullName = this.get("fullname") || username;

			return fullName;
		},

		isOrcid: function(orcid){
			var username = (typeof orcid === "string")? orcid : this.get("username");

			//Have we already verified this?
			if((typeof orcid == "undefined") && (username == this.get("orcid"))) return true;

			//Checks for ORCIDs using the orcid base URL as a prefix
			if(username.indexOf("orcid.org/") > -1){
				return true;
			}

			//If the ORCID base url is not present, we will check if this is a 19-digit ORCID ID
			//A simple and fast check first
			//ORCiDs are 16 digits and 3 dashes - 19 characters
			if(username.length != 19) return false;

			/* The ORCID checksum algorithm to determine is a character string is an ORCiD
			 * http://support.orcid.org/knowledgebase/articles/116780-structure-of-the-orcid-identifier
			 */
			var total = 0,
				baseDigits = username.replace(/-/g, "").substr(0, 15);

			for(var i=0; i<baseDigits.length; i++){
				var digit = parseInt(baseDigits.charAt(i));
				total = (total + digit) * 2;
			}

			var remainder = total % 11,
				result = (12 - remainder) % 11,
				checkDigit = (result == 10) ? "X" : result.toString(),
				isOrcid = (checkDigit == username.charAt(username.length-1));

			if(isOrcid)
				this.set("orcid", username);

			return isOrcid;
		},

		isNode: function(){
			var node = _.where(MetacatUI.nodeModel.get("members"), { shortIdentifier: this.get("username") });
			return (node && node.length)
		},

		// Will check if this user is a Member Node. If so, it will save the MN info to the model
		saveAsNode: function(){
			if(!this.isNode()) return;

			var node = _.where(MetacatUI.nodeModel.get("members"), { shortIdentifier: this.get("username") })[0];

			this.set({
				type: "node",
				logo: node.logo,
				description: node.description,
				node: node,
				fullName: node.name,
				usernameReadable: this.get("username")
			});
			this.updateSearchModel();
			this.set("checked", true);
		},

		loginLdap: function(formData, success, error){
			if(!formData || !appModel.get("signInUrlLdap")) return false;

			var model = this;

			var requestSettings = {
				type: "POST",
				url: MetacatUI.appModel.get("signInUrlLdap") + window.location.href,
				data: formData,
				success: function(data, textStatus, xhr){
					if(success)
						success(this);

					model.getToken();

				},
				error: function(){
					/*if(error)
						error(this);
					*/
					model.getToken();
				}
			}

			$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
		},

		logout: function(){

			//Construct the sign out url and redirect
			var signOutUrl = MetacatUI.appModel.get('signOutUrl'),
				  target = Backbone.history.location.href;

			// DO NOT include the route otherwise we have an infinite redirect
			// target  = target.split("#")[0];
			target = target.slice(0, -8);

			// make sure to include the target
			signOutUrl += "?target=" + target;

			// do it!
			window.location.replace(signOutUrl);
		},

		// call Metacat or the DataONE CN to validate the session and tell us the user's name
		checkStatus: function(onSuccess, onError) {
			var model = this;

			if (!MetacatUI.appModel.get("tokenUrl")) {
				// look up the URL
				var metacatUrl = MetacatUI.appModel.get('metacatServiceUrl');

				// ajax call to validate the session/get the user info
				var requestSettings = {
					type: "POST",
					url: metacatUrl,
					data: { action: "validatesession" },
					success: function(data, textStatus, xhr) {
						// the Metacat (XML) response should have a fullName element
						var username = $(data).find("name").text();

						// set in the model
						model.set('username', username);

						//Are we logged in?
						if(username){
							model.set("loggedIn", true);
							model.getInfo();
						}
						else{
							model.set("loggedIn", false);
							model.trigger("change:loggedIn");
							model.set("checked", true);
						}

						if(onSuccess) onSuccess(data);

					},
					error: function(data, textStatus, xhr){
						//User is not logged in
						model.reset();

						if(onError) onError();
					}
				}

				$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
			} else {
				// use the token method for checking authentication
				this.getToken();
			}
		},

		getToken: function(customCallback) {
			var tokenUrl = MetacatUI.appModel.get('tokenUrl');
			var model = this;

			if(!tokenUrl) return false;

			//Set up the function that will be called when we retrieve a token
			var callback = (typeof customCallback === "function") ? customCallback : function(data, textStatus, xhr) {

				// the response should have the token
				var payload = model.parseToken(data),
					username = payload ? payload.userId : null,
					fullName = payload ? payload.fullName : model.getNameFromSubject(username) || null,
					token    = payload ? data : null,
					loggedIn = payload ? true : false;

				// set in the model
				model.set('fullName', fullName);
				model.set('username', username);
				model.set("token", token);
				model.set("loggedIn", loggedIn);
        model.set("tokenChecked", true);

				model.getTokenExpiration(payload);

				if(username)
					model.getInfo();
				else
					model.set("checked", true);
			};

			// ajax call to get token
			var requestSettings = {
				type: "GET",
				dataType: "text",
				xhrFields: {
					withCredentials: true
				},
				url: tokenUrl,
				data: {},
				success: callback,
				error: function(xhr, textStatus, errorThrown){
					model.set("checked", true);
				}
			}

			$.ajax(requestSettings);
		},

		getTokenExpiration: function(payload){
			if(!payload && this.get("token")) var payload = this.parseToken(this.get("token"));
			if(!payload) return;

			//The exp claim should be standard - it is in UTC seconds
			var expires = payload.exp? new Date(payload.exp * 1000) : null;

			//Use the issuedAt and ttl as a backup (only used in d1 2.0.0 and 2.0.1)
			if(!expires){
				var issuedAt = payload.issuedAt? new Date(payload.issuedAt) : null,
					lifeSpan = payload.ttl? payload.ttl : null;

				if(issuedAt && lifeSpan && (lifeSpan > 99999))
					issuedAt.setMilliseconds(lifeSpan);
				else if(issuedAt && lifeSpan)
					issuedAt.setSeconds(lifeSpan);

				expires = issuedAt;
			}

			this.set("expires", expires);
		},

		checkToken: function(onSuccess, onError){

			//First check if the token has expired
			if(MetacatUI.appUserModel.get("expires") > new Date()){
				if(onSuccess) onSuccess();

				return;
			}

			var model = this;

			var url = MetacatUI.appModel.get("tokenUrl");
			if(!url) return;

			var requestSettings = {
					type: "GET",
					url: url,
          headers: {
			      "Cache-Control": "no-cache"
				  },
					success: function(data, textStatus, xhr){
						if(data){
							// the response should have the token
							var payload = model.parseToken(data),
								username = payload ? payload.userId : null,
								fullName = payload ? payload.fullName : null,
								token    = payload ? data : null,
								loggedIn = payload ? true : false;

							// set in the model
							model.set('fullName', fullName);
							model.set('username', username);
							model.set("token", token);
							model.set("loggedIn", loggedIn);

							model.getTokenExpiration(payload);

							MetacatUI.appUserModel.set("checked", true);

							if(onSuccess) onSuccess(data, textStatus, xhr);
						}
						else if(onError)
							onError(data, textStatus, xhr);
					},
					error: function(data, textStatus, xhr){
						//If this token in invalid, then reset the user model/log out
						MetacatUI.appUserModel.reset();

						if(onError) onError(data, textStatus, xhr);
					}
			}

			$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
		},

		parseToken: function(token) {
			if(typeof token == "undefined")
				var token = this.get("token");

			var jws = new KJUR.jws.JWS();
			var result = 0;
			try {
				result = jws.parseJWS(token);
			} catch (ex) {
			    result = 0;
			}

			if(!jws.parsedJWS) return "";

			var payload = $.parseJSON(jws.parsedJWS.payloadS);
			return payload;
		},

		update: function(onSuccess, onError){
			var model = this;

			var person =
				'<?xml version="1.0" encoding="UTF-8"?>'
				+ '<d1:person xmlns:d1="http://ns.dataone.org/service/types/v1">'
					+ '<subject>' + this.get("username") + '</subject>'
					+ '<givenName>' + this.get("firstName") + '</givenName>'
					+ '<familyName>' + this.get("lastName") + '</familyName>'
					+ '<email>' + this.get("email") + '</email>'
				+ '</d1:person>';

			var xmlBlob = new Blob([person], {type : 'application/xml'});
			var formData = new FormData();
			formData.append("subject", this.get("username"));
			formData.append("person", xmlBlob, "person");

			var updateUrl = MetacatUI.appModel.get("accountsUrl") + encodeURIComponent(this.get("username"));

			// ajax call to update
			var requestSettings = {
				type: "PUT",
				cache: false,
			    contentType: false,
			    processData: false,
				url: updateUrl,
				data: formData,
				success: function(data, textStatus, xhr) {
					if(typeof onSuccess != "undefined")
						onSuccess(data);

					//model.getInfo();
				},
				error: function(data, textStatus, xhr) {
					if(typeof onError != "undefined")
						onError(data);
				}
			}

			$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
		},

		confirmMapRequest: function(otherUsername, onSuccess, onError){
			if(!otherUsername) return;

			var mapUrl = MetacatUI.appModel.get("pendingMapsUrl") + encodeURIComponent(otherUsername),
				model = this;

			if(!onSuccess)
				var onSuccess = function(){};
			if(!onError)
				var onError = function(){};

			// ajax call to confirm map
			var requestSettings = {
				type: "PUT",
				url: mapUrl,
				success: function(data, textStatus, xhr) {
					if(onSuccess)
						onSuccess(data, textStatus, xhr);

					//Get updated info
					model.getInfo();
				},
				error: function(xhr, textStatus, error) {
					if(onError)
						onError(xhr, textStatus, error);
				}
			}
			$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
		},

		denyMapRequest: function(otherUsername, onSuccess, onError){
			if(!otherUsername) return;

			var mapUrl = MetacatUI.appModel.get("pendingMapsUrl") + encodeURIComponent(otherUsername),
				model = this;

			// ajax call to reject map
			var requestSettings = {
				type: "DELETE",
				url: mapUrl,
				success: function(data, textStatus, xhr) {
					if(typeof onSuccess == "function")
						onSuccess(data, textStatus, xhr);

					model.getInfo();
				},
				error: function(xhr, textStatus, error) {
					if(typeof onError == "function")
						onError(xhr, textStatus, error);
				}
			}
			$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
		},

		addMap: function(otherUsername, onSuccess, onError){
			if(!otherUsername) return;

			var mapUrl = MetacatUI.appModel.get("pendingMapsUrl"),
				model = this;

			if(mapUrl.charAt(mapUrl.length-1) == "/"){
				mapUrl = mapUrl.substring(0, mapUrl.length-1)
			}

			// ajax call to map
			var requestSettings = {
				type: "POST",
				xhrFields: {
					withCredentials: true
				},
				headers: {
			        "Authorization": "Bearer " + this.get("token")
			  },
				url: mapUrl,
				data: {
					subject: otherUsername
				},
				success: function(data, textStatus, xhr) {
					if(typeof onSuccess == "function")
						onSuccess(data, textStatus, xhr);

					model.getInfo();
				},
				error: function(xhr, textStatus, error) {

					//Check if the username might have been spelled or formatted incorrectly
					//ORCIDs, in particular, have different formats that we should account for
					if(xhr.responseText.indexOf("LDAP: error code 32 - No Such Object") > -1 && model.isOrcid(otherUsername)){
						if(otherUsername.length == 19)
							model.addMap("http://orcid.org/" + otherUsername, onSuccess, onError);
						else if(otherUsername.indexOf("https://orcid.org") == 0)
							model.addMap(otherUsername.replace("https", "http"), onSuccess, onError);
						else if(otherUsername.indexOf("orcid.org") == 0)
							model.addMap("http://" + otherUsername, onSuccess, onError);
						else if(otherUsername.indexOf("www.orcid.org") == 0)
								model.addMap(otherUsername.replace("www.", "http://"), onSuccess, onError);
						else if(otherUsername.indexOf("http://www.orcid.org") == 0)
								model.addMap(otherUsername.replace("www.", ""), onSuccess, onError);
						else if(otherUsername.indexOf("https://www.orcid.org") == 0)
								model.addMap(otherUsername.replace("https://www.", "http://"), onSuccess, onError);
						else if(typeof onError == "function")
							onError(xhr, textStatus, error);
					}
					else{
						if(typeof onError == "function")
							onError(xhr, textStatus, error);
				  }
				}
			}

			$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
		},

		removeMap: function(otherUsername, onSuccess, onError){
			if(!otherUsername) return;

			var mapUrl = MetacatUI.appModel.get("accountsMapsUrl") + encodeURIComponent(otherUsername),
				model = this;

			// ajax call to remove mapping
			var requestSettings = {
				type: "DELETE",
				url: mapUrl,
				success: function(data, textStatus, xhr) {
					if(typeof onSuccess == "function")
						onSuccess(data, textStatus, xhr);

					model.getInfo();
				},
				error: function(xhr, textStatus, error) {
					if(typeof onError == "function")
						onError(xhr, textStatus, error);
				}
			}
			$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
		},

		failedLdapLogin: function(){
			this.set("loggedIn", false);
			this.set("checked", true);
			this.set("ldapError", true);
		},

		pluckIdentityUsernames: function(){
			var models = this.get("identities"),
				usernames = [];

			_.each(models, function(m){
				usernames.push(m.get("username").toLowerCase());
			});

			this.set("identitiesUsernames", usernames);
			this.trigger("change:identitiesUsernames");
		},

		createReadableUsername: function(){
			if(!this.get("username")) return;

			var username = this.get("username"),
				readableUsername = username.substring(username.indexOf("=")+1, username.indexOf(",")) || username;

			this.set("usernameReadable", readableUsername);
		},

		createAjaxSettings: function(){
			if(!this.get("token")) return {}

			return { xhrFields: {
					withCredentials: true
					},
				  headers: {
			        "Authorization": "Bearer " + this.get("token")
				  }
				}
		},

    /**
    * Checks if this user has the quota to perform the given action
    * @param {string} action - The action to be performed
    * @param {string} customerGroup - The subject or identifier of the customer/membership group
    * to use this quota against
    */
    checkQuota: function(action, customerGroup){

      //Temporarily reset the quota so a trigger event is changed when the XHR is complete
      this.set("portalQuota", -1, {silent: true});

      //Start of temporary code
      //TODO: Replace this function with real code once the quota service is working
      this.set("portalQuota", 999);
      return;
      //End of temporary code

      var model = this;

      var requestSettings = {
        url: "",
        type: "GET",
        success: function(data, textStatus, xhr) {
          model.set("portalQuota", data.remainingQuota);
        },
        error: function(xhr, textStatus, errorThrown) {
          model.set("portalQuota", 0);
        }
      }

      $.ajax(_.extend(requestSettings, this.createAjaxSettings()));

    },

    /**
    * Checks if the user has authorization to perform the given action.
    */
    isAuthorizedCreatePortal: function(){

      //Reset the isAuthorized attribute silently so a change event is always triggered
      this.set("isAuthorizedCreatePortal", null, {silent: true});

      //If the user isn't logged in, set authorization to false
      if( !this.get("loggedIn") ){
        this.set("isAuthorizedCreatePortal", false);
        return;
      }

      //If creating portals has bbeen disabled app-wide, then set to false
      if( MetacatUI.appModel.get("enableCreatePortals") === false ){
        this.set("isAuthorizedCreatePortal", false);
        return;
      }
      //If creating portals has been limited to only certain subjects, check if this user is one of them
      else if( MetacatUI.appModel.get("limitPortalsToSubjects").length ){
        if( !this.get("allIdentitiesAndGroups").length ){
          this.on("change:allIdentitiesAndGroups", this.isAuthorizedCreatePortal);
          return;
        }

        //Find the subjects that have access to create portals. Could be specific users or groups.
        var subjectsThatHaveAccess = _.intersection(MetacatUI.appModel.get("limitPortalsToSubjects"), this.get("allIdentitiesAndGroups"));
        if( !subjectsThatHaveAccess.length ){
          //If this user is not in the whitelist, set to false
          this.set("isAuthorizedCreatePortal", false);
        }
        else{
          //If this user is in the whitelist, set to true
          this.set("isAuthorizedCreatePortal", true);
        }
        return;
      }
      //If anyone is allowed to create a portal, check if they have the quota to create a portal
      else{
        //Listen to the response from the quota check
        this.once("change:portalQuota", function(){
          //If the quota is at least 1, set to true
          if( this.get("portalQuota") > 0 ){
            this.set("isAuthorizedCreatePortal", true);
          }
          //If the quota is less than or equal to zero, set to false
          else{
            this.set("isAuthorizedCreatePortal", false);
          }
        });
        //Check the quota
        this.checkQuota("createPortal");
        return;
      }

    },

    /**
    * Given a list of user and/or group subjects, this function checks if this user
    * has an equivalent identity in that list, or is a member of a group in the list.
    * A single subject string can be passed instead of an array of subjects.
    * TODO: This needs to support nested group membership.
    * @param {string|string[]} subjects
    * @returns {boolean}
    */
    hasIdentityOverlap: function(subjects){

      try{
        //If only a single subject is given, put it in an array
        if( typeof subjects == "string" ){
          subjects = [subjects];
        }
        //If the subjects are not a string or an array, or if it's an empty array, exit this function.
        else if( !Array.isArray(subjects) || !subjects.length ){
          return false;
        }

        return _.intersection(this.get("allIdentitiesAndGroups"), subjects).length;
      }
      catch(e){
        console.error(e);
        return false;
      }

    },

		reset: function(){
			var defaults = _.omit(this.defaults(), ["searchModel", "searchResults"]);
			this.set(defaults);
		}
	});

	return UserModel;
});