<commit_before>const logging = require('kolibri.lib.logging').getLogger(__filename);
const ConditionalPromise = require('./conditionalPromise');
const find = require('lodash/find');
const matches = require('lodash/matches');
const isEqual = require('lodash/isEqual');
const cloneDeep = require('./cloneDeep');


/** Class representing a single API resource object */
class Model {
  /**
   * Create a model instance.
   * @param {object} data - data to insert into the model at creation time - should include at
   * least an id for fetching, or data an no id if the intention is to save a new model.
   * @param {Resource} resource - object of the Resource class, specifies the urls and fetching
   * behaviour for the model.
   */
  constructor(data, resourceIds = {}, resource) {
    this.resource = resource;
    if (!this.resource) {
      throw new TypeError('resource must be defined');
    }

    if (!data) {
      throw new TypeError('data must be defined');
    }

    if (typeof data !== 'object') {
      throw new TypeError('data must be an object');
    }

    if (Object.keys(data).length === 0) {
      throw new TypeError('data must be instantiated with some data');
    }

    const filteredResourceIds = this.resource.filterAndCheckResourceIds(resourceIds);
    this.resourceIds = filteredResourceIds;

    // Assign any data to the attributes property of the Model.
    this.attributes = {};
    this.set(data);

    this.synced = false;

    // Keep track of any unresolved promises that have been generated by async methods of the Model
    this.promises = [];
  }

  /**
   * Method to fetch data from the server for this particular model.
   * @param {object} getParams - an object of parameters to be parsed into GET parameters on the
   * fetch.
   * @param {boolean} [force=false] - fetch whether or not it's been synced already.
   * @returns {Promise} - Promise is resolved with Model attributes when the XHR successfully
   * returns, otherwise reject is called with the response object.
   */
  fetch(getParams = {}, force = false) {
    const promise = new ConditionalPromise((resolve, reject) => {
      Promise.all(this.promises).then(() => {
        if (!force && this.synced) {
          resolve(this.attributes);
        } else {
          this.synced = false;
          // Do a fetch on the URL.
          this.resource.client({ path: this.url, params: getParams }).then((response) => {
            // Set the retrieved Object onto the Model instance.
            this.set(response.entity);
            // Flag that the Model has been fetched.
            this.synced = true;
            // Resolve the promise with the attributes of the Model.
            resolve(this.attributes);
            // Clean up the reference to this promise
            this.promises.splice(this.promises.indexOf(promise), 1);
          }, (response) => {
            logging.error('An error occurred', response);
            reject(response);
            // Clean up the reference to this promise
            this.promises.splice(this.promises.indexOf(promise), 1);
          });
        }
      },
      (reason) => {
        reject(reason);
      });
    });
    this.promises.push(promise);
    return promise;
  }

  /**
   * Method to save data to the server for this particular model.
   * @param {object} attrs - an object of attributes to be saved on the model.
   * @returns {Promise} - Promise is resolved with Model attributes when the XHR successfully
   * returns, otherwise reject is called with the response object.
   */
  save(attrs) {
    const promise = new ConditionalPromise((resolve, reject) => {
      Promise.all(this.promises).then(() => {
        let payload = {};
        if (this.synced) {
          // Model is synced with the server, so we can do dirty checking.
          Object.keys(attrs).forEach((key) => {
            if (!isEqual(attrs[key], this.attributes[key])) {
              payload[key] = attrs[key];
            }
          });
        } else {
          this.set(attrs);
          payload = this.attributes;
        }<commit_msg>Set the dirty checked payload onto the model before saving.<commit_after>const logging = require('kolibri.lib.logging').getLogger(__filename);
const ConditionalPromise = require('./conditionalPromise');
const find = require('lodash/find');
const matches = require('lodash/matches');
const isEqual = require('lodash/isEqual');
const cloneDeep = require('./cloneDeep');


/** Class representing a single API resource object */
class Model {
  /**
   * Create a model instance.
   * @param {object} data - data to insert into the model at creation time - should include at
   * least an id for fetching, or data an no id if the intention is to save a new model.
   * @param {Resource} resource - object of the Resource class, specifies the urls and fetching
   * behaviour for the model.
   */
  constructor(data, resourceIds = {}, resource) {
    this.resource = resource;
    if (!this.resource) {
      throw new TypeError('resource must be defined');
    }

    if (!data) {
      throw new TypeError('data must be defined');
    }

    if (typeof data !== 'object') {
      throw new TypeError('data must be an object');
    }

    if (Object.keys(data).length === 0) {
      throw new TypeError('data must be instantiated with some data');
    }

    const filteredResourceIds = this.resource.filterAndCheckResourceIds(resourceIds);
    this.resourceIds = filteredResourceIds;

    // Assign any data to the attributes property of the Model.
    this.attributes = {};
    this.set(data);

    this.synced = false;

    // Keep track of any unresolved promises that have been generated by async methods of the Model
    this.promises = [];
  }

  /**
   * Method to fetch data from the server for this particular model.
   * @param {object} getParams - an object of parameters to be parsed into GET parameters on the
   * fetch.
   * @param {boolean} [force=false] - fetch whether or not it's been synced already.
   * @returns {Promise} - Promise is resolved with Model attributes when the XHR successfully
   * returns, otherwise reject is called with the response object.
   */
  fetch(getParams = {}, force = false) {
    const promise = new ConditionalPromise((resolve, reject) => {
      Promise.all(this.promises).then(() => {
        if (!force && this.synced) {
          resolve(this.attributes);
        } else {
          this.synced = false;
          // Do a fetch on the URL.
          this.resource.client({ path: this.url, params: getParams }).then((response) => {
            // Set the retrieved Object onto the Model instance.
            this.set(response.entity);
            // Flag that the Model has been fetched.
            this.synced = true;
            // Resolve the promise with the attributes of the Model.
            resolve(this.attributes);
            // Clean up the reference to this promise
            this.promises.splice(this.promises.indexOf(promise), 1);
          }, (response) => {
            logging.error('An error occurred', response);
            reject(response);
            // Clean up the reference to this promise
            this.promises.splice(this.promises.indexOf(promise), 1);
          });
        }
      },
      (reason) => {
        reject(reason);
      });
    });
    this.promises.push(promise);
    return promise;
  }

  /**
   * Method to save data to the server for this particular model.
   * @param {object} attrs - an object of attributes to be saved on the model.
   * @returns {Promise} - Promise is resolved with Model attributes when the XHR successfully
   * returns, otherwise reject is called with the response object.
   */
  save(attrs) {
    const promise = new ConditionalPromise((resolve, reject) => {
      Promise.all(this.promises).then(() => {
        let payload = {};
        if (this.synced) {
          // Model is synced with the server, so we can do dirty checking.
          Object.keys(attrs).forEach((key) => {
            if (!isEqual(attrs[key], this.attributes[key])) {
              payload[key] = attrs[key];
            }
          });
          this.set(payload);
        } else {
          this.set(attrs);
          payload = this.attributes;
        }
{'fraction_non_alphanumeric': 0.1110035104708873, 'fraction_numerical': 0.000726304321510713, 'mean_word_length': 2.4583507743825868, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 19, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5817207', 'n_tokens_mistral': 2188, 'n_tokens_neox': 2148, 'n_words': 778}