Source: src/js/collections/UserGroup.js

define(["jquery", "underscore", "backbone", "models/UserModel"], function (
  $,
  _,
  Backbone,
  UserModel,
) {
  "use strict";

  /**
   * @class UserGroup
   * @classdesc The collection of Users that represent a DataONE group
   * @classcategory Collections
   * @extends Backbone.Collection
   */
  var UserGroup = Backbone.Collection.extend(
    /** @lends UserGroup.prototype */ {
      // Reference to this collection's model.
      model: UserModel,

      //Custom attributes of groups
      groupId: "",
      name: "",
      nameAvailable: null,

      url: function () {
        return (
          MetacatUI.appModel.get("accountsUrl") +
          encodeURIComponent(this.groupId)
        );
      },

      comparator: "lastName", //Sort by last name

      initialize: function (models, options) {
        if (typeof models == "undefined" || !models) var models = [];

        if (typeof options !== "undefined") {
          //Save our options
          $.extend(this, options);
          this.groupId = options.groupId || "";
          this.name = options.name || "";
          this.pending =
            typeof options.pending === "undefined" ? false : options.pending;

          //If raw data is passed, parse it to get a list of users to be added to this group
          if (options.rawData) {
            //Get a list of UserModel attributes to add to this collection
            var toAdd = this.parse(options.rawData);

            //Create a UserModel for each user
            _.each(toAdd, function (modelAttributes) {
              //Don't pass the raw data to the UserModel creation because it is redundant-
              //We already parsed the raw data when we called add() above
              var rawDataSave = modelAttributes.rawData;
              modelAttributes.rawData = null;

              //Create the model then add the raw data back
              var member = new UserModel(modelAttributes);
              member.set("rawData", rawDataSave);

              models.push(member);
            });
          }
        }

        //Add all our models to this collection
        this.add(models);
      },

      /*
       * Gets the group from the server. Options object uses the BackboneJS options API
       */
      getGroup: function (options) {
        if (!this.groupId && this.name) {
          this.groupId = "CN=" + this.name + ",DC=dataone,DC=org";
        }

        this.fetch(options);

        return this;
      },

      /*
       * Fetches the group info from the server. Should not be called directly - use getGroup() instead
       */
      fetch: function (options) {
        options = options || { silent: false, reset: false, remove: false };
        options.dataType = "xml";
        options.error = function (collection, response, options) {
          //If this group is not found, then the name is available
          if (
            response.status == 404 &&
            response.responseText.indexOf("No Such Object") > -1
          ) {
            collection.nameAvailable = true;
            collection.trigger("nameChecked", collection);
          }
        };
        return Backbone.Collection.prototype.fetch.call(this, options);
      },

      /*
       * Backbone.js override - parses the XML reponse from DataONE and creates a JSON representation that will
       * be used to create UserModels
       */
      parse: function (response, options) {
        if (!response) return;

        //This group name is not available/already taken
        this.nameAvailable = false;
        this.trigger("nameChecked", this);

        var group = $(response)
            .find("group subject:contains('" + this.groupId + "')")
            .parent("group"),
          people = $(response).find("person"),
          collection = this,
          toAdd = new Array(),
          existing = this.pluck("username");

        if (!people.length) people = $(group).find("hasMember");

        //Make all existing usernames lowercase for string matching
        if (existing.length) existing = _.invoke(existing, "toLowerCase");

        this.name = $(group).children("groupName").text();

        _.each(people, function (person) {
          //The tag name is "hasMember" if we retrieved info about this group from the group nodes only
          if (person.tagName == "hasMember") {
            var username = $(person).text();

            //If this user is already in the group, skip adding it
            if (_.contains(existing, username.toLowerCase())) return;

            var user = new UserModel({ username: username }),
              userAttr = user.toJSON();

            toAdd.push(userAttr);
          }
          //The tag name is "person" if we retrieved info about this group through the /accounts service, which includes all nodes about all members
          else {
            //If this user is not listed as a member of this group, skip it
            if (
              $(person).children(
                "isMemberOf:contains('" + collection.groupId + "')",
              ).length < 1
            )
              return;

            //Username of this person
            var username = $(person).children("subject").text();

            //If this user is already in the group, skip adding it
            if (_.contains(existing, username.toLowerCase())) return;

            //User attributes - pass the full response for the UserModel to parse
            var userAttr = new UserModel({ username: username }).parseXML(
              response,
            );

            //Add to collection
            toAdd.push(userAttr);
          }
        });

        return toAdd;
      },

      /*
       * An alternative to Backbone sync
       * - will send a POST request to DataONE CNIdentity.createGroup() to create this collection as a new DataONE group
       * or
       * - will send a PUT request to DataONE CNIdentity.updateGroup() to update this existing DataONE group
       *
       *  If this group is marked as pending, then the group is created, otherwise it's updated
       */
      save: function (onSuccess, onError) {
        if (this.pending && this.nameAvailable == false) return false;

        var memberXML = "",
          ownerXML = "",
          collection = this;

        //Create the member and owner XML
        this.forEach(function (member) {
          //Don't list yourself as an owner or member (implied)
          if (MetacatUI.appUserModel == member) return;

          var username = member.get("username")
            ? member.get("username").trim()
            : null;
          if (!username) return;

          memberXML += "<hasMember>" + username + "</hasMember>";

          if (collection.isOwner(member))
            ownerXML += "<rightsHolder>" + username + "</rightsHolder>";
        });

        //Create the group XML
        var groupXML =
          '<?xml version="1.0" encoding="UTF-8"?>' +
          '<d1:group xmlns:d1="http://ns.dataone.org/service/types/v1">' +
          "<subject>" +
          this.groupId +
          "</subject>" +
          "<groupName>" +
          this.name +
          "</groupName>" +
          memberXML +
          ownerXML +
          "</d1:group>";

        var xmlBlob = new Blob([groupXML], { type: "application/xml" });
        var formData = new FormData();
        formData.append("group", xmlBlob, "group");

        // AJAX call to update
        $.ajax({
          type: this.pending ? "POST" : "PUT",
          cache: false,
          contentType: false,
          processData: false,
          xhrFields: {
            withCredentials: true,
          },
          headers: {
            Authorization: "Bearer " + MetacatUI.appUserModel.get("token"),
          },
          url: MetacatUI.appModel.get("groupsUrl"),
          data: formData,
          success: function (data, textStatus, xhr) {
            if (typeof onSuccess != "undefined") onSuccess(data);

            collection.pending = false;
            collection.nameAvailable = null;
            collection.getGroup();
          },
          error: function (xhr, textStatus, error) {
            if (typeof onError != "undefined") onError(xhr);
          },
        });

        return true;
      },

      /*
       * For pending groups only (those in the creation stage)
       * Will check if the given name/id is available
       */
      checkName: function (name) {
        //Only check the name for pending groups
        if (!this.pending) return;

        //Reset the name and ID
        this.name = name || this.name;
        this.groupId = null;
        this.nameAvailable = null;

        //Get group info/check name availablity
        this.getGroup({ add: false });
      },

      /*
       * Retrieves the UserModels that are rightsHolders of this group
       */
      getOwners: function () {
        var groupId = this.groupId;
        return _.filter(this.models, function (user) {
          return _.contains(user.get("isOwnerOf"), groupId);
        });
      },

      /*
       * Shortcut function - will check if a specified User is an owner of this group
       */
      isOwner: function (model) {
        if (typeof model === "undefined") return false;

        if (this.pending && model == MetacatUI.appUserModel) return true;

        var usernames = [];
        _.each(this.getOwners(), function (user) {
          usernames.push(user.get("username"));
        });

        return _.contains(usernames, model.get("username"));
      },
    },
  );

  return UserGroup;
});