<commit_before>/*
Copyright (C) <year> <copyright holders>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

var
  dummy
, declare = require('simpledeclare')
, Store = require('jsonreststores')

, ObjectId = require('mongowrapper').ObjectId
;

var MongoDriverMixin = declare( null, {

  constructor: function(){

    this.projectionHash = {};

    // Just because I went insane figuring this out, and want to save the hassle to
    // more people in the future
    if( !this.db ){
      throw new Error("The 'db' attribute MUST be specified for MongoDriverMixin" );
    }

    // This.collectionName will default to the store's name if not set
    this.collectionName = this.collectionName ? this.collectionName : this.storeName;
    this.collection = this.db.collection( this.collectionName );

    // Make sure that I have `_id: false` in the projection hash (used in all finds)
    // if `_id` is not explicitely defined in the schema.
    // in "inclusive projections" in mongoDb, _id is added automatically and it needs to be
    // explicitely excluded (it is, in fact, the ONLY field that can be excluded in an inclusive projection)
    for( var k in this.schema.fieldsHash ) this.projectionHash[ k ] = this.schema.fieldsHash[ k ];
    if( typeof( this.schema.fieldsHash._id ) === 'undefined' ) this.projectionHash._id =false ;

  },

  collectionName: null,
  db: null,

  handlePut: false,
  handlePost: false,
  handlePostAppend: false,
  handleGet: false,
  handleGetQuery: false,
  handleDelete: false,

  _mongoFilterFromParams: function( params ){
    var self = this;

    var filter = {};
    self.paramIds.forEach( function( paramId ){
      if( typeof( params[ paramId ]) !== 'undefined' ){
        filter[ paramId ] = params[ paramId ];
      }
    });
    return filter;
  },

  driverAllDbFetch: function( params, body, options, cb ){

    var self = this;

    // Make up the filter, based on the store's IDs (used as filters).
    var filter = self._mongoFilterFromParams( params );

    this.collection.findOne( filter, self.projectionHash, cb );
  },

  driverPostDbInsertNoId: function( params, body, options, generatedId, cb ){

    var self = this;

    var record = {};

    // Make up the `record` variable, based on the passed `body`
    for( var k in body ) record[ k ] = body[ k ];
    self.paramIds.forEach( function( paramId ){
      if( typeof( params[ paramId ] ) !== 'undefined' ){
        record[ paramId ] = params[ paramId ];
      }
    });

    // The last parameter is missing since it
    // wasn't passed: assign an ObjectId to it
    record[ self.idProperty ] = generatedId;

    // Set the record ID to keep Mongo happy and make
    // subsequent search easier.
    if( self.idProperty !== '_id' ) record._id  = ObjectId();

    // Insert the made up record
    self.collection.insert( record, function( err ){
      if( err ) {
        cb( err );
      } else {
        self.collection.findOne( { _id: record._id }, self.projectionHash, cb );
      }
    });

  },

  driverPutDbUpdate: function( params, body, options, doc, fullDoc, cb ){

    var self = this;
    var updateObject = {}, unsetObject = {};

    // Make up the filter, based on the store's IDs (used as filters).
    var filter = self._mongoFilterFromParams( params );

    // Simply copy values over except self.idProperty (which mustn't be
    // overwritten)
    for( var i in body ){
      if( i != self.idProperty ) updateObject[ i ] = body[ i ];
    }

    // Unset any value that is not actually set but IS in the schema,
    // so that partial PUTs will "overwrite" whole objects rather than
    // just overwriting fields that are _actually_ present in `body`
    for( var i in self.schema.structure ){
       if( typeof( body[ i ] ) === 'undefined' ) unsetObject[ i ] = 1;
    }

    self.collection.update( filter, { $set: updateObject, $unset: unsetObject }, function( err, doc ){
      if( err ){
        cb( err, null );
      } else {
        self.collection.findOne( filter, self.projectionHash, cb );
      }

    });
  },

  driverPutDbInsert: function( params, body, options, cb ){

    var self = this;

    var record = {};

    // Make up the `record` variable, based on the passed `body`
    for( var k in body ) record[ k ] = body[ k ];
    self.paramIds.forEach( function( paramId ){
      if( typeof( params[ paramId ] ) !== 'undefined' ){
        record[ paramId ] = params[ paramId ];
      }
    });

    // There is actually a chance that _id is not defined,
    // in case the store is using a different ID to the one
    // in the database.
    if( self.idProperty !== '_id' ) record._id  = ObjectId();

    this.collection.insert( record, function( err ){
      if( err ) {
        cb( err );
      } else {
        self.collection.findOne( {_id: record._id }, self.schema.projectionHash, cb );
      }
    });

  },

  driverPostDbAppend: function( params, body, options, doc, fullDoc, cb ){

    // Is this _ever_ implemented, really? Seriously?
    cb( null, doc );
  },

  driverDeleteDbDelete: function( params, body, options, cb ){

    var self = this;

    // Make up the filter
    var filter = self._mongoFilterFromParams( params );

    // Actually remove the field
    self.collection.remove( filter, function( err, doc ){
      if( err ) {
        cb( err );
      } else {
        cb( null );
      }
    });

  },

  // THE FOLLOWING API FUNCTIONS ARE NOT CALLABLE FROM AN ONLINE API

  driverAPIDbMassDelete: function( params, body, options, cb ){
    var selector = {};

    var self = this;

    // Make up the selector based on the query
    selector = self._queryMakeSelector( options.queryFilterType, options.filters, options.searchPartial, {} );

    // Paranoid check. Important since an empty selector WILL lead
    // to table zapping...
    if( Object.keys( selector ).length == 0 && ! options.allowZapping ){
      cb( new Error("Zapping of table not allowed, mass deletion aborted" ) );
    } else {

      // Actually remove the field
      self.collection.remove( selector, function( err, howMany ){
        if( err ) {
          cb( err );
        } else {
          cb( null, howMany );
        }
      });
    }
  },

   driverAPIDbMassUpdate: function( params, body, options, cb ){
    var selector = {};

    var self = this;

    // Make up the selector based on the query
    selector = self._queryMakeSelector( options.queryFilterType, options.filters, options.searchPartial, {} );

    // Actually remove the field
    self.collection.update( selector, { $set: body } , { multi: true },  function( err, howMany ){
      if( err ) {
        cb( err );
      } else {
        cb( null, howMany );
      }
    });
  },

  // END OF API FUNCTIONS THAT ARE NOT CALLABLE FROM AN ONLINE API

  driverGetDbQuery: function( params, body, options, next ){

    var self = this;

    var cursor;
    var selector = {};

    // Select according to selector
    selector = self._queryMakeSelector( options.queryFilterType, options.filters, options.searchPartial, params );
    cursor = self.collection.find( selector, self.projectionHash );

    if( typeof( options.ranges) == 'object' && typeof( options.ranges) === 'object' && options.ranges !== null ){

      // If `rangeFrom` and `rangeTo` are set, and limit isn't, then `limit`
      // needs to be set
      if( options.ranges.rangeFrom != 0 && options.ranges.rangeTo != 0 && typeof( options.ranges.limit ) === 'undefined' ){
         options.ranges.limit =  options.ranges.rangeTo - options.ranges.rangeFrom + 1;
      }

      // If `limit` makes it go over `rangeTo`, then resize `limit`
      if( options.ranges.rangeFrom + options.ranges.limit > options.ranges.rangeTo ){
        options.ranges.limit =  options.ranges.rangeTo - options.ranges.rangeFrom + 1;
      }

      // Respect hard limit on number of returned records
      if( options.ranges.limit > self.hardLimitOnQueries ){
        options.ranges.limit = self.hardLimitOnQueries;
      }

      // Skipping/limiting according to ranges/limits
      if( options.ranges.rangeFrom != 0 )
        cursor.skip( options.ranges.rangeFrom );
      if( options.ranges.limit != 0 )
        cursor.limit( options.ranges.limit );
    }

    // Sort the results
    cursor.sort( self._queryMakeMongoSortArray( options.sortBy ), function( err ){
      self._sendErrorOnErr( err, next, function(){

        if( ! options.cursor ){

          cursor.count( function( err, total ){
            self._sendErrorOnErr( err, next, function(){

              cursor.toArray( function( err, queryDocs ){
                self._sendErrorOnErr( err, next, function(){

                  // Remove resultsSet if so required
                  if( options.remove ){
                    // Silent failing
                    self.collection.remove( selector, function( err, doc ){ } );
                  }
                  queryDocs.total = total;
                  next( null, queryDocs );
                });
              });
            });
          });

        } else {

          var o = {};

          o.next = function( cb ){
            cursor.nextObject( function( err, obj) {
              if( err ){
                cb( err );
              } else {

                // If options.remove is on, then remove a field straight after fetching it
                if( options.remove && obj !== null ){
                  self.collection.remove( { _id: obj._id }, function( err, howMany ){
                    if( err ){
                      cb( err );
                    } else {
                      cb( null, obj );
                    }
                  });
                } else {
                   cb( null, obj );
                }

              }
            });

          }

          o.rewind = function( cb ){
            cursor.rewind();
            cb( null );
          }
          o.close = function( cb ){
            cursor.close( cb );
          }

          next( null, o );
        }

      }) // err
    }) // cursor.sort

  },

  // Make up the query selector
  _queryMakeSelector: function( queryFilterType, filters, searchPartial, params ){
    var selector;
    var item;
    var self = this;

    // Set a sane, safe default for queryFilterType
    if( queryFilterType !== 'and' && queryFilterType !== 'or' ) queryFilterType = 'and';

    // Set a sane, safe default for searchPartial
    if( typeof( searchPartial ) !== 'object' || searchPartial === null ) searchPartial = {};

    // CASE #1: If there are no filters, then just return the
    // default urlFilter based on params

    if( typeof( filters ) !== 'object' || Object.keys( filters ).length == 0 ){
      return this._mongoFilterFromParams( params );
    }

    // CASE #2: there are filters. So, the query will be based on BOTH the
    // params passed, _and_ the filters

    // Prepare the selector variable for $and and $or arrays
    // $or is only needed if the queryType is 'or'
    selector = {};
    selector[ '$and' ] =  [];
    selector[ '$or' ] =  [];

    // First of all, make up the params filter...
    var paramsFilter = this._mongoFilterFromParams( params );

    // ...and add the criteria to the $and side of the selector
    for( var k in paramsFilter ){
      item = {};
      item[ k ] = paramsFilter[ k ];
      selector[ '$and' ].push( item );
    }

    // Add filters to the selector
    for( var k in filters ){
      item = {};

      // Check if it's a filterType field -- if it is, add the correct filter type
      // if( typeof( self.searchSchema.structure[ k ] ) === 'object' && self.searchSchema.structure[ k ] !== null ){

      if( self.searchSchema.structure[ k ].filterType ){
        var filterType = self.searchSchema.structure[ k ].filterType;

        switch( filterType.type ){

          // "range" type of filter: check direction and reference field
          case 'range':

            // Double check that the referenced field exists
            if( typeof( self.searchSchema.structure[ filterType.field ] ) === 'undefined' ){
              throw( new Error('"field" option in filter type is undefined: ' + filterType.field  ) );
            }

            // Adding the right filter, depending of 'direction' being 'from' or 'to'
            if( filterType.direction == 'from' ){
              item[ filterType.field ] = { $gte: filters[ k ] } ;
            } else {
              item[ filterType.field ] = { $lte: filters[ k ] } ;
            }

          break;
        }

      } else {

        // If searchPartial is requested, then have it as a regular expression
        if( searchPartial[ k ] ){
          item[ k ] = { $regex: new RegExp('^' + filters[ k ] + '.*' ) };
        } else {
          item[ k ] = filters[ k ];
        }
      }

      // Finally adding the item
      selector[ '$' + queryFilterType ].push( item );
    }

    // Clean up selector, as Mongo doesn't like empty arrays for selectors
    if( selector[ '$and' ].length == 0 ) delete selector[ '$and' ];
    if( selector[ '$or' ].length == 0 ) delete selector[ '$or' ];

    return selector;

  },

  _queryMakeMongoSortArray: function( sortBy ){
    return sortBy;

    /*
    var sortArray = [];

    for( var k in sortBy )
      sortArray.push( [ k , sortBy [ k ] ] );

    return sortArray;
    */

  },

});

// "Inherit" the class function "make" so that modules using this don't have to
// use the "parent" module too
// NOTE: No longer needed, as simpledeclare now does this for us
// StoreMongo.make = Store.make;

// Get the default schema for the db
// StoreMongo.Schema = SchemaMongo;

exports = module.exports = MongoDriverMixin;
<commit_msg>Fixed small bug in Mongodriver<commit_after>/*
Copyright (C) <year> <copyright holders>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

var
  dummy
, declare = require('simpledeclare')
, Store = require('jsonreststores')

, ObjectId = require('mongowrapper').ObjectId
;

var MongoDriverMixin = declare( null, {

  constructor: function(){

    this.projectionHash = {};

    // Just because I went insane figuring this out, and want to save the hassle to
    // more people in the future
    if( !this.db ){
      throw new Error("The 'db' attribute MUST be specified for MongoDriverMixin" );
    }

    // This.collectionName will default to the store's name if not set
    this.collectionName = this.collectionName ? this.collectionName : this.storeName;
    this.collection = this.db.collection( this.collectionName );

    // Make sure that I have `_id: false` in the projection hash (used in all finds)
    // if `_id` is not explicitely defined in the schema.
    // in "inclusive projections" in mongoDb, _id is added automatically and it needs to be
    // explicitely excluded (it is, in fact, the ONLY field that can be excluded in an inclusive projection)
    for( var k in this.schema.fieldsHash ) this.projectionHash[ k ] = this.schema.fieldsHash[ k ];
    if( typeof( this.schema.fieldsHash._id ) === 'undefined' ) this.projectionHash._id =false ;

  },

  collectionName: null,
  db: null,

  handlePut: false,
  handlePost: false,
  handlePostAppend: false,
  handleGet: false,
  handleGetQuery: false,
  handleDelete: false,

  _mongoFilterFromParams: function( params ){
    var self = this;

    var filter = {};
    self.paramIds.forEach( function( paramId ){
      if( typeof( params[ paramId ]) !== 'undefined' ){
        filter[ paramId ] = params[ paramId ];
      }
    });
    return filter;
  },

  driverAllDbFetch: function( params, body, options, cb ){

    var self = this;

    // Make up the filter, based on the store's IDs (used as filters).
    var filter = self._mongoFilterFromParams( params );

    this.collection.findOne( filter, self.projectionHash, cb );
  },

  driverPostDbInsertNoId: function( params, body, options, generatedId, cb ){

    var self = this;

    var record = {};

    // Make up the `record` variable, based on the passed `body`
    for( var k in body ) record[ k ] = body[ k ];

    self.paramIds.forEach( function( paramId ){
      if( typeof( params[ paramId ] ) !== 'undefined' ){
        record[ paramId ] = params[ paramId ];
      }
    });

    // The last parameter is missing since it
    // wasn't passed: assign an ObjectId to it
    record[ self.idProperty ] = generatedId;

    // Set the record ID to keep Mongo happy and make
    // subsequent search easier.
    if( self.idProperty !== '_id' ) record._id  = ObjectId();

    // Insert the made up record
    self.collection.insert( record, function( err ){
      if( err ) {
        cb( err );
      } else {
        self.collection.findOne( { _id: record._id }, self.projectionHash, cb );
      }
    });

  },

  driverPutDbUpdate: function( params, body, options, doc, fullDoc, cb ){

    var self = this;
    var updateObject = {}, unsetObject = {};

    // Make up the filter, based on the store's IDs (used as filters).
    var filter = self._mongoFilterFromParams( params );

    // Simply copy values over except self.idProperty (which mustn't be
    // overwritten)
    for( var i in body ){
      if( i != self.idProperty ) updateObject[ i ] = body[ i ];
    }

    // Unset any value that is not actually set but IS in the schema,
    // so that partial PUTs will "overwrite" whole objects rather than
    // just overwriting fields that are _actually_ present in `body`
    for( var i in self.schema.structure ){
       if( typeof( body[ i ] ) === 'undefined' ) unsetObject[ i ] = 1;
    }

    self.collection.update( filter, { $set: updateObject, $unset: unsetObject }, function( err, doc ){
      if( err ){
        cb( err, null );
      } else {
        self.collection.findOne( filter, self.projectionHash, cb );
      }

    });
  },

  driverPutDbInsert: function( params, body, options, cb ){

    var self = this;

    var record = {};

    // Make up the `record` variable, based on the passed `body`
    for( var k in body ) record[ k ] = body[ k ];
    self.paramIds.forEach( function( paramId ){
      if( typeof( params[ paramId ] ) !== 'undefined' ){
        record[ paramId ] = params[ paramId ];
      }
    });

    // There is actually a chance that _id is not defined,
    // in case the store is using a different ID to the one
    // in the database.
    if( self.idProperty !== '_id' ) record._id  = ObjectId();

    this.collection.insert( record, function( err ){
      if( err ) {
        cb( err );
      } else {
        self.collection.findOne( {_id: record._id }, self.projectionHash, cb );
      }
    });

  },

  driverPostDbAppend: function( params, body, options, doc, fullDoc, cb ){

    // Is this _ever_ implemented, really? Seriously?
    cb( null, doc );
  },

  driverDeleteDbDelete: function( params, body, options, cb ){

    var self = this;

    // Make up the filter
    var filter = self._mongoFilterFromParams( params );

    // Actually remove the field
    self.collection.remove( filter, function( err, doc ){
      if( err ) {
        cb( err );
      } else {
        cb( null );
      }
    });

  },

  // THE FOLLOWING API FUNCTIONS ARE NOT CALLABLE FROM AN ONLINE API

  driverAPIDbMassDelete: function( params, body, options, cb ){
    var selector = {};

    var self = this;

    // Make up the selector based on the query
    selector = self._queryMakeSelector( options.queryFilterType, options.filters, options.searchPartial, {} );

    // Paranoid check. Important since an empty selector WILL lead
    // to table zapping...
    if( Object.keys( selector ).length == 0 && ! options.allowZapping ){
      cb( new Error("Zapping of table not allowed, mass deletion aborted" ) );
    } else {

      // Actually remove the field
      self.collection.remove( selector, function( err, howMany ){
        if( err ) {
          cb( err );
        } else {
          cb( null, howMany );
        }
      });
    }
  },

   driverAPIDbMassUpdate: function( params, body, options, cb ){
    var selector = {};

    var self = this;

    // Make up the selector based on the query
    selector = self._queryMakeSelector( options.queryFilterType, options.filters, options.searchPartial, {} );

    // Actually remove the field
    self.collection.update( selector, { $set: body } , { multi: true },  function( err, howMany ){
      if( err ) {
        cb( err );
      } else {
        cb( null, howMany );
      }
    });
  },

  // END OF API FUNCTIONS THAT ARE NOT CALLABLE FROM AN ONLINE API

  driverGetDbQuery: function( params, body, options, next ){

    var self = this;

    var cursor;
    var selector = {};

    // Select according to selector
    selector = self._queryMakeSelector( options.queryFilterType, options.filters, options.searchPartial, params );
    cursor = self.collection.find( selector, self.projectionHash );

    if( typeof( options.ranges) == 'object' && typeof( options.ranges) === 'object' && options.ranges !== null ){

      // If `rangeFrom` and `rangeTo` are set, and limit isn't, then `limit`
      // needs to be set
      if( options.ranges.rangeFrom != 0 && options.ranges.rangeTo != 0 && typeof( options.ranges.limit ) === 'undefined' ){
         options.ranges.limit =  options.ranges.rangeTo - options.ranges.rangeFrom + 1;
      }

      // If `limit` makes it go over `rangeTo`, then resize `limit`
      if( options.ranges.rangeFrom + options.ranges.limit > options.ranges.rangeTo ){
        options.ranges.limit =  options.ranges.rangeTo - options.ranges.rangeFrom + 1;
      }

      // Respect hard limit on number of returned records
      if( options.ranges.limit > self.hardLimitOnQueries ){
        options.ranges.limit = self.hardLimitOnQueries;
      }

      // Skipping/limiting according to ranges/limits
      if( options.ranges.rangeFrom != 0 )
        cursor.skip( options.ranges.rangeFrom );
      if( options.ranges.limit != 0 )
        cursor.limit( options.ranges.limit );
    }

    // Sort the results
    cursor.sort( self._queryMakeMongoSortArray( options.sortBy ), function( err ){
      self._sendErrorOnErr( err, next, function(){

        if( ! options.cursor ){

          cursor.count( function( err, total ){
            self._sendErrorOnErr( err, next, function(){

              cursor.toArray( function( err, queryDocs ){
                self._sendErrorOnErr( err, next, function(){

                  // Remove resultsSet if so required
                  if( options.remove ){
                    // Silent failing
                    self.collection.remove( selector, function( err, doc ){ } );
                  }
                  queryDocs.total = total;
                  next( null, queryDocs );
                });
              });
            });
          });

        } else {

          var o = {};

          o.next = function( cb ){
            cursor.nextObject( function( err, obj) {
              if( err ){
                cb( err );
              } else {

                // If options.remove is on, then remove a field straight after fetching it
                if( options.remove && obj !== null ){
                  self.collection.remove( { _id: obj._id }, function( err, howMany ){
                    if( err ){
                      cb( err );
                    } else {
                      cb( null, obj );
                    }
                  });
                } else {
                   cb( null, obj );
                }

              }
            });

          }

          o.rewind = function( cb ){
            cursor.rewind();
            cb( null );
          }
          o.close = function( cb ){
            cursor.close( cb );
          }

          next( null, o );
        }

      }) // err
    }) // cursor.sort

  },

  // Make up the query selector
  _queryMakeSelector: function( queryFilterType, filters, searchPartial, params ){
    var selector;
    var item;
    var self = this;

    // Set a sane, safe default for queryFilterType
    if( queryFilterType !== 'and' && queryFilterType !== 'or' ) queryFilterType = 'and';

    // Set a sane, safe default for searchPartial
    if( typeof( searchPartial ) !== 'object' || searchPartial === null ) searchPartial = {};

    // CASE #1: If there are no filters, then just return the
    // default urlFilter based on params

    if( typeof( filters ) !== 'object' || Object.keys( filters ).length == 0 ){
      return this._mongoFilterFromParams( params );
    }

    // CASE #2: there are filters. So, the query will be based on BOTH the
    // params passed, _and_ the filters

    // Prepare the selector variable for $and and $or arrays
    // $or is only needed if the queryType is 'or'
    selector = {};
    selector[ '$and' ] =  [];
    selector[ '$or' ] =  [];

    // First of all, make up the params filter...
    var paramsFilter = this._mongoFilterFromParams( params );

    // ...and add the criteria to the $and side of the selector
    for( var k in paramsFilter ){
      item = {};
      item[ k ] = paramsFilter[ k ];
      selector[ '$and' ].push( item );
    }

    // Add filters to the selector
    for( var k in filters ){
      item = {};

      // Check if it's a filterType field -- if it is, add the correct filter type
      // if( typeof( self.searchSchema.structure[ k ] ) === 'object' && self.searchSchema.structure[ k ] !== null ){

      if( self.searchSchema.structure[ k ].filterType ){
        var filterType = self.searchSchema.structure[ k ].filterType;

        switch( filterType.type ){

          // "range" type of filter: check direction and reference field
          case 'range':

            // Double check that the referenced field exists
            if( typeof( self.searchSchema.structure[ filterType.field ] ) === 'undefined' ){
              throw( new Error('"field" option in filter type is undefined: ' + filterType.field  ) );
            }

            // Adding the right filter, depending of 'direction' being 'from' or 'to'
            if( filterType.direction == 'from' ){
              item[ filterType.field ] = { $gte: filters[ k ] } ;
            } else {
              item[ filterType.field ] = { $lte: filters[ k ] } ;
            }

          break;
        }

      } else {

        // If searchPartial is requested, then have it as a regular expression
        if( searchPartial[ k ] ){
          item[ k ] = { $regex: new RegExp('^' + filters[ k ] + '.*' ) };
        } else {
          item[ k ] = filters[ k ];
        }
      }

      // Finally adding the item
      selector[ '$' + queryFilterType ].push( item );
    }

    // Clean up selector, as Mongo doesn't like empty arrays for selectors
    if( selector[ '$and' ].length == 0 ) delete selector[ '$and' ];
    if( selector[ '$or' ].length == 0 ) delete selector[ '$or' ];

    return selector;

  },

  _queryMakeMongoSortArray: function( sortBy ){
    return sortBy;

    /*
    var sortArray = [];

    for( var k in sortBy )
      sortArray.push( [ k , sortBy [ k ] ] );

    return sortArray;
    */

  },

});

// "Inherit" the class function "make" so that modules using this don't have to
// use the "parent" module too
// NOTE: No longer needed, as simpledeclare now does this for us
// StoreMongo.make = Store.make;

// Get the default schema for the db
// StoreMongo.Schema = SchemaMongo;

exports = module.exports = MongoDriverMixin;
<|endoftext|>


<commit_before>/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @format
 * @flow strict-local
 */

'use strict';

const AndroidDropdownPickerNativeComponent = require('AndroidDropdownPickerNativeComponent');
const AndroidDialogPickerNativeComponent = require('AndroidDialogPickerNativeComponent');
const React = require('React');
const StyleSheet = require('StyleSheet');

const processColor = require('processColor');

const REF_PICKER = 'picker';
const MODE_DROPDOWN = 'dropdown';

import type {SyntheticEvent} from 'CoreEventTypes';
import type {TextStyleProp} from 'StyleSheet';

type PickerAndroidChangeEvent = SyntheticEvent<
  $ReadOnly<{|
    position: number,
  |}>,
>;

type PickerAndroidProps = $ReadOnly<{|
  children?: React.Node,
  style?: ?TextStyleProp,
  selectedValue?: ?(number | string),
  enabled?: ?boolean,
  mode?: ?('dialog' | 'dropdown'),
  onValueChange?: ?(itemValue: ?(string | number), itemIndex: number) => mixed,
  prompt?: ?string,
  testID?: string,
|}>;

type Item = $ReadOnly<{|
  label: string,
  value: ?(number | string),
  color?: ?number,
|}>;

type PickerAndroidState = {|
  selectedIndex: number,
  items: $ReadOnlyArray<Item>,
|};

/**
 * Not exposed as a public API - use <Picker> instead.
 */

class PickerAndroid extends React.Component<
  PickerAndroidProps,
  PickerAndroidState,
> {
  static getDerivedStateFromProps(
    props: PickerAndroidProps,
  ): PickerAndroidState {
    let selectedIndex = 0;
    const items = React.Children.map(props.children, (child, index) => {
      if (child.props.value === props.selectedValue) {
        selectedIndex = index;
      }
      const childProps = {
        value: child.props.value,
        label: child.props.label,
      };
      if (child.props.color) {
        /* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was
         * found when making Flow check .android.js files. */
        childProps.color = processColor(child.props.color);
      }
      return childProps;
    });
    return {selectedIndex, items};
  }

  state = PickerAndroid.getDerivedStateFromProps(this.props);

  render() {
    const Picker =
      this.props.mode === MODE_DROPDOWN
        ? AndroidDropdownPickerNativeComponent
        : AndroidDialogPickerNativeComponent;

    const nativeProps = {
      enabled: this.props.enabled,
      items: this.state.items,
      mode: this.props.mode,
      onSelect: this._onChange,
      prompt: this.props.prompt,
      selected: this.state.selectedIndex,
      testID: this.props.testID,
      style: [styles.pickerAndroid, this.props.style],
      /* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was found
       * when making Flow check .android.js files. */
      accessibilityLabel: this.props.accessibilityLabel,
    };

    return <Picker ref={REF_PICKER} {...nativeProps} />;
  }

  _onChange = (event: PickerAndroidChangeEvent) => {
    if (this.props.onValueChange) {
      const position = event.nativeEvent.position;
      if (position >= 0) {
        const children = React.Children.toArray(this.props.children);
        const value = children[position].props.value;
        /* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was
         * found when making Flow check .android.js files. */
        this.props.onValueChange(value, position);
      } else {
        this.props.onValueChange(null, position);
      }
    }

    // The picker is a controlled component. This means we expect the
    // on*Change handlers to be in charge of updating our
    // `selectedValue` prop. That way they can also
    // disallow/undo/mutate the selection of certain values. In other
    // words, the embedder of this component should be the source of
    // truth, not the native component.
    if (
      this.refs[REF_PICKER] &&
      this.state.selectedIndex !== event.nativeEvent.position
    ) {
      this.refs[REF_PICKER].setNativeProps({
        selected: this.state.selectedIndex,
      });
    }
  };
}

const styles = StyleSheet.create({
  pickerAndroid: {
    // The picker will conform to whatever width is given, but we do
    // have to set the component's height explicitly on the
    // surrounding view to ensure it gets rendered.
    // TODO would be better to export a native constant for this,
    // like in iOS the RCTDatePickerManager.m
    height: 50,
  },
});

module.exports = PickerAndroid;
<commit_msg>Fix breaking of PickerAndroid when child is null. (#23748)<commit_after>/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @format
 * @flow strict-local
 */

'use strict';

const AndroidDropdownPickerNativeComponent = require('AndroidDropdownPickerNativeComponent');
const AndroidDialogPickerNativeComponent = require('AndroidDialogPickerNativeComponent');
const React = require('React');
const StyleSheet = require('StyleSheet');

const processColor = require('processColor');

const REF_PICKER = 'picker';
const MODE_DROPDOWN = 'dropdown';

import type {SyntheticEvent} from 'CoreEventTypes';
import type {TextStyleProp} from 'StyleSheet';

type PickerAndroidChangeEvent = SyntheticEvent<
  $ReadOnly<{|
    position: number,
  |}>,
>;

type PickerAndroidProps = $ReadOnly<{|
  children?: React.Node,
  style?: ?TextStyleProp,
  selectedValue?: ?(number | string),
  enabled?: ?boolean,
  mode?: ?('dialog' | 'dropdown'),
  onValueChange?: ?(itemValue: ?(string | number), itemIndex: number) => mixed,
  prompt?: ?string,
  testID?: string,
|}>;

type Item = $ReadOnly<{|
  label: string,
  value: ?(number | string),
  color?: ?number,
|}>;

type PickerAndroidState = {|
  selectedIndex: number,
  items: $ReadOnlyArray<Item>,
|};

/**
 * Not exposed as a public API - use <Picker> instead.
 */

class PickerAndroid extends React.Component<
  PickerAndroidProps,
  PickerAndroidState,
> {
  static getDerivedStateFromProps(
    props: PickerAndroidProps,
  ): PickerAndroidState {
    let selectedIndex = 0;
    const items = React.Children.map(props.children, (child, index) => {
      if (child === null) {
        return;
      }
      if (child.props.value === props.selectedValue) {
        selectedIndex = index;
      }
      const childProps = {
        value: child.props.value,
        label: child.props.label,
      };
      if (child.props.color) {
        /* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was
         * found when making Flow check .android.js files. */
        childProps.color = processColor(child.props.color);
      }
      return childProps;
    });
    return {selectedIndex, items};
  }

  state = PickerAndroid.getDerivedStateFromProps(this.props);

  render() {
    const Picker =
      this.props.mode === MODE_DROPDOWN
        ? AndroidDropdownPickerNativeComponent
        : AndroidDialogPickerNativeComponent;

    const nativeProps = {
      enabled: this.props.enabled,
      items: this.state.items,
      mode: this.props.mode,
      onSelect: this._onChange,
      prompt: this.props.prompt,
      selected: this.state.selectedIndex,
      testID: this.props.testID,
      style: [styles.pickerAndroid, this.props.style],
      /* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was found
       * when making Flow check .android.js files. */
      accessibilityLabel: this.props.accessibilityLabel,
    };

    return <Picker ref={REF_PICKER} {...nativeProps} />;
  }

  _onChange = (event: PickerAndroidChangeEvent) => {
    if (this.props.onValueChange) {
      const position = event.nativeEvent.position;
      if (position >= 0) {
        const children = React.Children.toArray(this.props.children);
        const value = children[position].props.value;
        /* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was
         * found when making Flow check .android.js files. */
        this.props.onValueChange(value, position);
      } else {
        this.props.onValueChange(null, position);
      }
    }

    // The picker is a controlled component. This means we expect the
    // on*Change handlers to be in charge of updating our
    // `selectedValue` prop. That way they can also
    // disallow/undo/mutate the selection of certain values. In other
    // words, the embedder of this component should be the source of
    // truth, not the native component.
    if (
      this.refs[REF_PICKER] &&
      this.state.selectedIndex !== event.nativeEvent.position
    ) {
      this.refs[REF_PICKER].setNativeProps({
        selected: this.state.selectedIndex,
      });
    }
  };
}

const styles = StyleSheet.create({
  pickerAndroid: {
    // The picker will conform to whatever width is given, but we do
    // have to set the component's height explicitly on the
    // surrounding view to ensure it gets rendered.
    // TODO would be better to export a native constant for this,
    // like in iOS the RCTDatePickerManager.m
    height: 50,
  },
});

module.exports = PickerAndroid;
<|endoftext|>


<commit_before><commit_msg>Added a few more solutions<commit_after><|endoftext|>


<commit_before>/**
 * Copyright 2004-present Facebook. All Rights Reserved.
 *
 * @providesModule TouchableHighlight
 */
'use strict';

var NativeMethodsMixin = require('NativeMethodsMixin');
var React = require('React');
var ReactIOSViewAttributes = require('ReactIOSViewAttributes');
var StyleSheet = require('StyleSheet');
var TimerMixin = require('TimerMixin');
var Touchable = require('Touchable');
var TouchableWithoutFeedback = require('TouchableWithoutFeedback');
var View = require('View');

var cloneWithProps = require('cloneWithProps');
var ensureComponentIsNative = require('ensureComponentIsNative');
var keyOf = require('keyOf');
var merge = require('merge');
var onlyChild = require('onlyChild');

var DEFAULT_PROPS = {
  activeOpacity: 0.8,
  underlayColor: 'black',
};

/**
 * A wrapper for making views respond properly to touches.
 * On press down, the opacity of the wrapped view is decreased, which allows
 * the underlay color to show through, darkening or tinting the view.  The
 * underlay comes from adding a view to the view hierarchy, which can sometimes
 * cause unwanted visual artifacts if not used correctly, for example if the
 * backgroundColor of the wrapped view isn't explicitly set to an opaque color.
 *
 * Example:
 *
 * ```
 * renderButton: function() {
 *   return (
 *     <TouchableHighlight onPress={this._onPressButton}>
 *       <Image
 *         style={styles.button}
 *         source={ix('myButton')}
 *       />
 *     </View>
 *   );
 * },
 * ```
 */

var TouchableHighlight = React.createClass({
  propTypes: {
    ...TouchableWithoutFeedback.propTypes,
    /**
     * Determines what the opacity of the wrapped view should be when touch is
     * active.
     */
    activeOpacity: React.PropTypes.number,
    /**
     * The color of the underlay that will show through when the touch is
     * active.
     */
    underlayColor: React.PropTypes.string,
    style: View.propTypes.style,
  },

  mixins: [NativeMethodsMixin, TimerMixin, Touchable.Mixin],

  getDefaultProps: () => DEFAULT_PROPS,

  // Performance optimization to avoid constantly re-generating these objects.
  computeSyntheticState: function(props) {
    return {
      activeProps: {
        style: {
          opacity: props.activeOpacity,
        }
      },
      activeUnderlayProps: {
        style: {
          backgroundColor: props.underlayColor,
        }
      },
      underlayStyle: [
        INACTIVE_UNDERLAY_PROPS.style,
        props.style,
      ]
    };
  },

  getInitialState: function() {
    return merge(
      this.touchableGetInitialState(), this.computeSyntheticState(this.props)
    );
  },

  componentDidMount: function() {
    ensureComponentIsNative(this.refs[CHILD_REF]);
  },

  componentDidUpdate: function() {
    ensureComponentIsNative(this.refs[CHILD_REF]);
  },

  componentWillReceiveProps: function(nextProps) {
    if (nextProps.activeOpacity !== this.props.activeOpacity ||
        nextProps.underlayColor !== this.props.underlayColor ||
        nextProps.style !== this.props.style) {
      this.setState(this.computeSyntheticState(nextProps));
    }
  },

  viewConfig: {
    uiViewClassName: 'RCTView',
    validAttributes: ReactIOSViewAttributes.RCTView
  },

  /**
   * `Touchable.Mixin` self callbacks. The mixin will invoke these if they are
   * defined on your component.
   */
  touchableHandleActivePressIn: function() {
    this.clearTimeout(this._hideTimeout);
    this._hideTimeout = null;
    this._showUnderlay();
    this.props.onPressIn && this.props.onPressIn();
  },

  touchableHandleActivePressOut: function() {
    if (!this._hideTimeout) {
      this._hideUnderlay();
    }
    this.props.onPressOut && this.props.onPressOut();
  },

  touchableHandlePress: function() {
    this.clearTimeout(this._hideTimeout);
    this._showUnderlay();
    this._hideTimeout = this.setTimeout(this._hideUnderlay, 100);
    this.props.onPress && this.props.onPress();
  },

  touchableHandleLongPress: function() {
    this.props.onLongPress && this.props.onLongPress();
  },

  touchableGetPressRectOffset: function() {
    return PRESS_RECT_OFFSET;   // Always make sure to predeclare a constant!
  },

  _showUnderlay: function() {
    this.refs[UNDERLAY_REF].setNativeProps(this.state.activeUnderlayProps);
    this.refs[CHILD_REF].setNativeProps(this.state.activeProps);
  },

  _hideUnderlay: function() {
    this.clearTimeout(this._hideTimeout);
    this._hideTimeout = null;
    if (this.refs[UNDERLAY_REF]) {
      this.refs[CHILD_REF].setNativeProps(INACTIVE_CHILD_PROPS);
      this.refs[UNDERLAY_REF].setNativeProps(INACTIVE_UNDERLAY_PROPS);
    }
  },

  render: function() {
    return (
      <View
        ref={UNDERLAY_REF}
        style={this.state.underlayStyle}
        onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder}
        onResponderTerminationRequest={this.touchableHandleResponderTerminationRequest}
        onResponderGrant={this.touchableHandleResponderGrant}
        onResponderMove={this.touchableHandleResponderMove}
        onResponderRelease={this.touchableHandleResponderRelease}
        onResponderTerminate={this.touchableHandleResponderTerminate}>
        {cloneWithProps(
          onlyChild(this.props.children),
          {
            ref: CHILD_REF,
            accessible: true,
            testID: this.props.testID,
          }
        )}
      </View>
    );
  }
});

var PRESS_RECT_OFFSET = {top: 20, left: 20, right: 20, bottom: 30};
var CHILD_REF = keyOf({childRef: null});
var UNDERLAY_REF = keyOf({underlayRef: null});
var INACTIVE_CHILD_PROPS = {
  style: StyleSheet.create({x: {opacity: 1.0}}).x,
};
var INACTIVE_UNDERLAY_PROPS = {
  style: StyleSheet.create({x: {backgroundColor: 'transparent'}}).x,
};

module.exports = TouchableHighlight;
<commit_msg>[TouchableHighlight] Preserve underlay style when restoring inactive props<commit_after>/**
 * Copyright 2004-present Facebook. All Rights Reserved.
 *
 * @providesModule TouchableHighlight
 */
'use strict';

var NativeMethodsMixin = require('NativeMethodsMixin');
var React = require('React');
var ReactIOSViewAttributes = require('ReactIOSViewAttributes');
var StyleSheet = require('StyleSheet');
var TimerMixin = require('TimerMixin');
var Touchable = require('Touchable');
var TouchableWithoutFeedback = require('TouchableWithoutFeedback');
var View = require('View');

var cloneWithProps = require('cloneWithProps');
var ensureComponentIsNative = require('ensureComponentIsNative');
var keyOf = require('keyOf');
var merge = require('merge');
var onlyChild = require('onlyChild');

var DEFAULT_PROPS = {
  activeOpacity: 0.8,
  underlayColor: 'black',
};

/**
 * A wrapper for making views respond properly to touches.
 * On press down, the opacity of the wrapped view is decreased, which allows
 * the underlay color to show through, darkening or tinting the view.  The
 * underlay comes from adding a view to the view hierarchy, which can sometimes
 * cause unwanted visual artifacts if not used correctly, for example if the
 * backgroundColor of the wrapped view isn't explicitly set to an opaque color.
 *
 * Example:
 *
 * ```
 * renderButton: function() {
 *   return (
 *     <TouchableHighlight onPress={this._onPressButton}>
 *       <Image
 *         style={styles.button}
 *         source={ix('myButton')}
 *       />
 *     </View>
 *   );
 * },
 * ```
 */

var TouchableHighlight = React.createClass({
  propTypes: {
    ...TouchableWithoutFeedback.propTypes,
    /**
     * Determines what the opacity of the wrapped view should be when touch is
     * active.
     */
    activeOpacity: React.PropTypes.number,
    /**
     * The color of the underlay that will show through when the touch is
     * active.
     */
    underlayColor: React.PropTypes.string,
    style: View.propTypes.style,
  },

  mixins: [NativeMethodsMixin, TimerMixin, Touchable.Mixin],

  getDefaultProps: () => DEFAULT_PROPS,

  // Performance optimization to avoid constantly re-generating these objects.
  computeSyntheticState: function(props) {
    return {
      activeProps: {
        style: {
          opacity: props.activeOpacity,
        }
      },
      activeUnderlayProps: {
        style: {
          backgroundColor: props.underlayColor,
        }
      },
      underlayStyle: [
        INACTIVE_UNDERLAY_PROPS.style,
        props.style,
      ]
    };
  },

  getInitialState: function() {
    return merge(
      this.touchableGetInitialState(), this.computeSyntheticState(this.props)
    );
  },

  componentDidMount: function() {
    ensureComponentIsNative(this.refs[CHILD_REF]);
  },

  componentDidUpdate: function() {
    ensureComponentIsNative(this.refs[CHILD_REF]);
  },

  componentWillReceiveProps: function(nextProps) {
    if (nextProps.activeOpacity !== this.props.activeOpacity ||
        nextProps.underlayColor !== this.props.underlayColor ||
        nextProps.style !== this.props.style) {
      this.setState(this.computeSyntheticState(nextProps));
    }
  },

  viewConfig: {
    uiViewClassName: 'RCTView',
    validAttributes: ReactIOSViewAttributes.RCTView
  },

  /**
   * `Touchable.Mixin` self callbacks. The mixin will invoke these if they are
   * defined on your component.
   */
  touchableHandleActivePressIn: function() {
    this.clearTimeout(this._hideTimeout);
    this._hideTimeout = null;
    this._showUnderlay();
    this.props.onPressIn && this.props.onPressIn();
  },

  touchableHandleActivePressOut: function() {
    if (!this._hideTimeout) {
      this._hideUnderlay();
    }
    this.props.onPressOut && this.props.onPressOut();
  },

  touchableHandlePress: function() {
    this.clearTimeout(this._hideTimeout);
    this._showUnderlay();
    this._hideTimeout = this.setTimeout(this._hideUnderlay, 100);
    this.props.onPress && this.props.onPress();
  },

  touchableHandleLongPress: function() {
    this.props.onLongPress && this.props.onLongPress();
  },

  touchableGetPressRectOffset: function() {
    return PRESS_RECT_OFFSET;   // Always make sure to predeclare a constant!
  },

  _showUnderlay: function() {
    this.refs[UNDERLAY_REF].setNativeProps(this.state.activeUnderlayProps);
    this.refs[CHILD_REF].setNativeProps(this.state.activeProps);
  },

  _hideUnderlay: function() {
    this.clearTimeout(this._hideTimeout);
    this._hideTimeout = null;
    if (this.refs[UNDERLAY_REF]) {
      this.refs[CHILD_REF].setNativeProps(INACTIVE_CHILD_PROPS);
      this.refs[UNDERLAY_REF].setNativeProps({
        ...INACTIVE_UNDERLAY_PROPS,
        style: this.state.underlayStyle,
      });
    }
  },

  render: function() {
    return (
      <View
        ref={UNDERLAY_REF}
        style={this.state.underlayStyle}
        onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder}
        onResponderTerminationRequest={this.touchableHandleResponderTerminationRequest}
        onResponderGrant={this.touchableHandleResponderGrant}
        onResponderMove={this.touchableHandleResponderMove}
        onResponderRelease={this.touchableHandleResponderRelease}
        onResponderTerminate={this.touchableHandleResponderTerminate}>
        {cloneWithProps(
          onlyChild(this.props.children),
          {
            ref: CHILD_REF,
            accessible: true,
            testID: this.props.testID,
          }
        )}
      </View>
    );
  }
});

var PRESS_RECT_OFFSET = {top: 20, left: 20, right: 20, bottom: 30};
var CHILD_REF = keyOf({childRef: null});
var UNDERLAY_REF = keyOf({underlayRef: null});
var INACTIVE_CHILD_PROPS = {
  style: StyleSheet.create({x: {opacity: 1.0}}).x,
};
var INACTIVE_UNDERLAY_PROPS = {
  style: StyleSheet.create({x: {backgroundColor: 'transparent'}}).x,
};

module.exports = TouchableHighlight;
<|endoftext|>


<commit_before>// This file is not used anymore, but I am not deleting it yet due to a block
// of code that has not been ported over into the new format yet.

// var libraryServices = angular.module('libraryServices', ['ngResource']);
//
// /*libraryServices.factory('Book', function($resource){
//     return $resource('/libary/api/book');
// });*/
//
// libraryServices.factory('Book', ['$resource',
//     function ($resource) {
//         'use strict';
//
//         return $resource('/library/api/book', {}, {
//             query: {
//                 method: 'GET',
//                 isArray: true,
//                 headers: {Accept: 'application/json'}
//             },
//             add: {
//                 method: 'POST',
//                 isArray: false,
//                 headers: {Accept: 'application/json'}
//             }
//         });
//     }
// ]);
//
// libraryServices.factory('Book', ['$resource',
//     function ($resource) {
//         'use strict';
//
//         return $resource('/library/api/book/:id', {}, {
//             get: {
//                     method: 'GET',
//                     isArray: false,
//                     headers: {Accept: 'application/json'}
//             },
//             update: {
//                     method: 'PUT',
//                     isArray: false,
//                     headers: {Accept: 'application/json'}
//             },
//             remove: {
//                     method: 'DELETE',
//                     isArray: false,
//                     headers: {Accept: 'application/json'}
//             }
//         });
//     }
// ]);
//
//
// libraryServices.factory('Author', ['$resource',
//     function ($resource) {
//         'use strict';
//
//         return $resource('/library/api/author', {}, {
//             query: {
//                     method: 'GET',
//                     isArray: true,
//                     headers: {Accept: 'application/json'}
//             },
//             add: {
//                     method: 'POST',
//                     isArray: false,
//                     headers: {Accept: 'application/json'}
//             }
//         });
//     }
// ]);
//
//
// /*libraryInterceptor = {
//         responseError : function(rejection) {
//             alert("Unable to complete Request" + rejection.status);
//         }
// }
//
// libraryServices.factory('Book', ['$resource'],
//     function($resource) {
//         return $resource('/library/api/book/list/', {}, {
//             query: {
//                     method: "GET",
//                     isArray: true,
//                     headers: { 'Accept' : 'application/json' },
//                     interceptor: libaryInterceptor
//             },
//             add: {
//                     method: "POST",
//                     url: '/library/api/book/',
//                     isArray: false,
//                     headers: { 'Accept' : 'application/json' },
//                     interceptor: libaryInterceptor
//             },
//             remove: {
//                     method: "DELETE",
//                     url: '/library/api/book/:bookId',
//                     isArray: false,
//                     headers: { 'Accept' : 'application/json' },
//                     interceptor: libaryInterceptor
//             },
//             update: {
//                     method: "PUT",
//                     url: '/library/api/book/:bookId',
//                     isArray: false,
//                     headers: { 'Accept' : 'application/json' },
//                     interceptor: libaryInterceptor
//             }
//         })
//     }) */
<commit_msg>remove legacy file<commit_after><|endoftext|>


<commit_before>var app = app || {};

app.createRecipe = app.createRecipe || {};

(function (app) {
    app.createRecipe.init = function() {
        var dataSource = new kendo.data.DataSource({
           transport: {
                read : function(options) {
                    app.requester.categories().then(function(data) {
                        options.success(data);
                    })
                }
            }
        });

        $('#category-select-dropdown').kendoDropDownList({ dataSource: dataSource });

        var viewModel = kendo.observable({
             allCategories: dataSource,
             name: '',
             description: '',
             imageUrl: '',
             products: '',
             category: 'Main Dish',
             cancel: function() {
                 app.main.navigate('views/all-recipes/all-recipes.html');
             },
             createRecipe: function() {
                 var imageMagic = this.get('imageUrl'); /* magic with file upload or camera */

                 var directions = this.get('directions');
                 if (directions) {
                    directions = this.get('directions').split('.').map(function(dir, index){
                        return {
                            Minutes:5 * (1 + index % 4),
                            StepNumber: index + 1,
                            Text: dir
                        }
                    });
                 }

                 var products = this.get('products');
                 if (products) {
                     products = this.get('products').split(',');
                 }

                 var recipeToCreate = {
                     Name: this.get('name'),
                     Category : this.get('category'),
                     Description: this.get('description'),
                     Image: imageMagic,
                     PreparationSteps : directions,
                     Products : products,
                     PreparationSteps: directions
                 };

                 if (!recipeToCreate.Name || !recipeToCreate.Category ||
                     !recipeToCreate.Description || !recipeToCreate.Image ||
                     !recipeToCreate.PreparationSteps || !recipeToCreate.Products) {
                         app.notifier.warning('Some of the fields are left empty!');
                         return;
                     }

                 app.requester.recipe.create(recipeToCreate).then(function(data) {
                     app.notifier.success('Recipe added');
                     navigator.notification.vibrate(1000);
                     app.main.navigate('views/single-recipe/single-recipe.html?id=' + data);
                 }, app.errorHandler);
             },
             takeImage: function() {
                 var error = function () {
                     navigator.notification.alert("Unfortunately we could not add the image");
                 };

                 var picConfig = {
                     destinationType: Camera.DestinationType.DATA_URL,
                     targetHeight: 400,
                     targetWidth: 400
                 };

                 var picSuccess = function (data) {
                     app.everlive.uploadImage(data, onSuccessUpload, onFailedUpload)
                 };

                 navigator.camera.getPicture(picSuccess, error, picConfig);
             },
             uploadImage: function() {
                 //todo: pass binary data to onSuccessUpload
             }
         });

        function onSuccessUpload(data) {
            app.everlive.getImageData(data.result.Id)
                .then(function (data) {
                    $.ajax({
                       type: "GET",
                       url: app.constants.everlivePictureStorageUri + data.result[0].Uri,
                       contentType: "application/json",
                   }).then(function (picData) {
                       console.log(picData);
                       viewModel.imageUrl = picData.Result.Uri;
                       app.notifier.success('Picture was uploaded successfully!');
                       console.log(viewModel.imageUrl); // TODO
                   })
                }, function () {
                    console.log("Cannot get image data!");
                });
        }

        function onFailedUpload() {
            console.log("Cannot upload an image!");
        }

        kendo.bind($("#create-recipe-view"), viewModel);
    }
}(app));<commit_msg>Changes made by Dzhenko Penev (dzhenko@gmail.com).<commit_after>var app = app || {};

app.createRecipe = app.createRecipe || {};

(function (app) {
    app.createRecipe.init = function() {
        var dataSource = new kendo.data.DataSource({
           transport: {
                read : function(options) {
                    app.requester.categories().then(function(data) {
                        options.success(data);
                    })
                }
            }
        });

        $('#category-select-dropdown').kendoDropDownList({ dataSource: dataSource });

        var viewModel = kendo.observable({
             allCategories: dataSource,
             name: '',
             description: '',
             imageUrl: '',
             products: '',
             category: 'Main Dish',
             cancel: function() {
                 app.main.navigate('views/all-recipes/all-recipes.html');
             },
             createRecipe: function() {
                 var imageMagic = this.get('imageUrl'); /* magic with file upload or camera */

                 var directions = this.get('directions');
                 if (directions) {
                    directions = this.get('directions').split('.').map(function(dir, index){
                        return {
                            Minutes:5 * (1 + index % 4),
                            StepNumber: index + 1,
                            Text: dir
                        }
                    });
                 }

                 var products = this.get('products');
                 if (products) {
                     products = this.get('products').split(',');
                 }

                 var recipeToCreate = {
                     Name: this.get('name'),
                     Category : this.get('category'),
                     Description: this.get('description'),
                     Image: imageMagic,
                     PreparationSteps : directions,
                     Products : products,
                     PreparationSteps: directions
                 };

                 if (!recipeToCreate.Name || !recipeToCreate.Category ||
                     !recipeToCreate.Description || !recipeToCreate.Image ||
                     !recipeToCreate.PreparationSteps || !recipeToCreate.Products) {
                         app.notifier.warning('Some of the fields are left empty!');
                         return;
                     }

                 app.requester.recipe.create(recipeToCreate).then(function(data) {
                     app.notifier.success('Recipe added');
                     navigator.notification.vibrate(1000);
                     app.main.navigate('views/single-recipe/single-recipe.html?id=' + data);
                 }, app.errorHandler);
             },
             takeImage: function() {
                 var error = function () {
                     navigator.notification.alert("Unfortunately we could not add the image");
                 };

                 var picConfig = {
                     destinationType: Camera.DestinationType.DATA_URL,
                     targetHeight: 400,
                     targetWidth: 400
                 };

                 var picSuccess = function (data) {
                     app.everlive.uploadImage(data, onSuccessUpload, onFailedUpload)
                 };

                 navigator.camera.getPicture(picSuccess, error, picConfig);
             },
             uploadImage: function() {
                 var error = function () {
                     navigator.notification.alert("Unfortunately we could not add the image");
                 };

                 var picConfig = {
                     destinationType: Camera.DestinationType.DATA_URL,
                     targetHeight: 400,
                     targetWidth: 400
                 };

                 var picSuccess = function (data) {
                     app.everlive.uploadImage(data, onSuccessUpload, onFailedUpload)
                 };

                 navigator.camera.getPicture(picSuccess, error, picConfig);
             }
         });

        function onSuccessUpload(data) {
            app.everlive.getImageData(data.result.Id)
                .then(function (data) {
                    $.ajax({
                       type: "GET",
                       url: app.constants.everlivePictureStorageUri + data.result[0].Uri,
                       contentType: "application/json",
                   }).then(function (picData) {
                       viewModel.imageUrl = picData.Result.Uri;
                       app.notifier.success('Picture was uploaded successfully!');
                   })
                }, function () {
                    app.notifier.error("Cannot get image data!");
                });
        }

        function onFailedUpload() {
            app.notifier.error("Cannot get image data!");
        }

        kendo.bind($("#create-recipe-view"), viewModel);
    }
}(app));<|endoftext|>


<commit_before>
var should = require('should');
var comparev = require('../index');

describe('should compare() work', function() {
  it('should return 1 when v1 > v2', function() {
    comparev('0.8.10', '0.8.3').should.equal(1);
    comparev('0.9.1', '0.8.30').should.equal(1);
    comparev('0.9.11', '0.8.30').should.equal(1);
    comparev('0.92.11', '0.10.389').should.equal(1);
    comparev('1.7.11', '0.8.30').should.equal(1);
    comparev('1.7.11', '1.3.30').should.equal(1);
    comparev('1.7.11', '1.7.3').should.equal(1);
    comparev('1.217.9', '1.7.3').should.equal(1);
    comparev('0.10.11', '0.9.389').should.equal(1);
    comparev('1.0.0', '0.99.999').should.equal(1);
  });

  it('should return 0 when v1 = v2', function() {
    comparev('0.8.1', '0.8.1').should.equal(0);
  });

  it('should return -1 when v1 < v2', function() {
    comparev('1.7.11', '1.13.30').should.equal(-1);
    comparev('0.8.1', '0.8.3').should.equal(-1);
    comparev('0.9.11', '0.10.3').should.equal(-1);
    comparev('0.92.11', '0.100.3').should.equal(-1);
    comparev('1.7.11', '1.13.30').should.equal(-1);
    comparev('1.7.11', '1.7.39').should.equal(-1);
    comparev('1.7.9', '1.217.3').should.equal(-1);
  });
});<commit_msg>:curly_loop:<commit_after>
var should = require('should');
var comparev = require('../lib/index');

describe('should compare() work', function() {
  it('should return 1 when v1 > v2', function() {
    comparev('0.8.10', '0.8.3').should.equal(1);
    comparev('0.9.1', '0.8.30').should.equal(1);
    comparev('0.9.11', '0.8.30').should.equal(1);
    comparev('0.92.11', '0.10.389').should.equal(1);
    comparev('1.7.11', '0.8.30').should.equal(1);
    comparev('1.7.11', '1.3.30').should.equal(1);
    comparev('1.7.11', '1.7.3').should.equal(1);
    comparev('1.217.9', '1.7.3').should.equal(1);
    comparev('0.10.11', '0.9.389').should.equal(1);
    comparev('1.0.0', '0.99.999').should.equal(1);
  });

  it('should return 0 when v1 = v2', function() {
    comparev('0.8.1', '0.8.1').should.equal(0);
  });

  it('should return -1 when v1 < v2', function() {
    comparev('1.7.11', '1.13.30').should.equal(-1);
    comparev('0.8.1', '0.8.3').should.equal(-1);
    comparev('0.9.11', '0.10.3').should.equal(-1);
    comparev('0.92.11', '0.100.3').should.equal(-1);
    comparev('1.7.11', '1.13.30').should.equal(-1);
    comparev('1.7.11', '1.7.39').should.equal(-1);
    comparev('1.7.9', '1.217.3').should.equal(-1);
  });
});<|endoftext|>


<commit_before>import _ from 'lodash'
import d3 from 'd3'

import formatUtil from 'util/format'

function _sortValue (s, sortCol) {
  // jshint validthis: true
  var options = this._options

  var val = (sortCol === null)
    ? options.seriesName(s)
    : options.value(_.find(options.values(s), d => options.column(d) === sortCol))

  return val
}

var DEFAULTS = {
  cellSize: 16,
  column: _.property('indicator.short_name'),
  sourceColumn: _.property('short_name'),
  fontSize: 12,
  format: formatUtil.general,
  headerText: _.property('short_name'),
  headers: [],
  onClick: null,
  onColumnHeadOver: null,
  onColumnHeadOut: null,
  onMouseMove: null,
  onMouseOut: null,
  onRowClick: null,
  seriesName: _.property('name'),
  sortValue: _sortValue,
  values: _.property('values'),
  value: _.property('value'),
  sortDirection: 1
}

function TableChart () {}

_.extend(TableChart.prototype, {
  defaults: DEFAULTS,
  sortCol: null,

  initialize: function (el, data, options) {
    options = this._options = _.defaults({}, options, DEFAULTS)

    var svg = this._svg = d3.select(el)
        .append('svg')
        .attr('class', 'heatmap sortable')

    var g = svg.append('g').attr('class', 'margin')

    g.append('g').attr('class', 'z axis')
    g.append('g').attr('class', 'y axis')
    g.append('g').attr('class', 'x axis')
    g.append('g').attr('class', 'data')
    g.append('g').attr('class', 'source-footer')
    g.append('g').attr('class', 'legend')

    this.update(data)
  },

  update: function (data, options) {
    options = _.extend(this._options, options)
    var margin = options.margin

    var self = this
    var parentLocationMap = options.parentLocationMap
    var w = 3 * Math.max(options.headers.length * options.cellSize, 0)
    var h = Math.max(options.defaultSortOrder.length * options.cellSize, 0)
    var z = 160 //  extra margin space needed to add the "z" (parent) axis"

    // hacky way to sclae the view box.. this shoudl be done by taking into
    // account the user's screen size... needs some time to get 100% right.. //
    var viewBoxHeightScale = 1 + (options.headers.length - 8) / 10
    if (viewBoxHeightScale < 1) {
      viewBoxHeightScale = 1
    }
    var viewBox = '0 0 ' + (w + margin.left + margin.right) + ' ' + ((h * viewBoxHeightScale) + margin.top + margin.bottom)
    var svg = this._svg
      .attr({
        'viewBox': viewBox,
        'width': (w + margin.left + margin.right),
        'height': (h + margin.top + margin.bottom)
      })
      .datum(data)

    var xScale = d3.scale.ordinal()
        .domain(_.map(options.headers, options.headerText))
        .rangeBands([0, w], 0.1)

    var x = _.flow(options.column, xScale)
    var sourceFlow = _.flow(options.sourceColumn, xScale)

    // if there is a sortCol set, order the data that way. //
    if (this.sortCol) {
      let sortValue = _.partial(options.sortValue.bind(this), _, this.sortCol)
      var domain = _(data).sortBy(sortValue, this).map(options.seriesName).value()
    } else {
      // if not, show default.  This also applies to the third click of a header
      domain = options.defaultSortOrder
      this.sortDirection = 1
    }

    if (this.sortDirection === -1) {
      domain = domain.reverse()
    }

    // For empty data points i need to add the x axis domain items explicitly //
    // otherwise the domain will be less ( and different the ) the yScale //
    // see trello : https://trello.com/c/bCwyqSWs/277-display-bug-when-creating-table-chart //
    if (domain.length < options.defaultSortOrder.length) {
      var diff = options.defaultSortOrder.filter(function (x) {
        return domain.indexOf(x) < 0
      })
      domain = domain.concat(diff)
    }

    var yScale = d3.scale.ordinal()
      .domain(domain)
      .rangeBands([0, h], 0.1)

    var y = _.flow(options.seriesName, yScale)

    var transform = function (d, i) {
      var yVal = y(d) + 10
      return 'translate(' + z + ' , ' + yVal + ')'
    }

    // THIS SETS THE COLOR... MOVE FROM HERE ONCE THE USER CAN SET A PALLETTE
    var targets = _(options.headers)
      .indexBy('id')
      .mapValues(ind => {
        var extents = [ ind.low_bound, ind.high_bound ]
        var names = ['bad', 'ok', 'good']

        if (ind.low_bound > ind.high_bound) {
          names = ['good', 'ok', 'bad']
        }

        return d3.scale.threshold()
          .domain(extents)
          .range(names)
      })
      .value()

    var scale = d3.scale.ordinal()
      .domain(['bad', 'ok', 'good'])
      .range(['#DB5344', '#79909F', '#2FB0D3'])

    var fill = d => scale(_.get(targets, d.indicator.id, _.noop)(d.value))

    svg.select('.margin')
        .attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')')

    var g = svg.select('.data')

    g.on('mouseout', function () { self._onRowOut.apply(self) })

    var row = g.selectAll('.row').data(data)

    row.enter().append('g')
        .attr({
          'class': 'row',
          'transform': transform
        })

    row.exit()
      .transition().duration(300)
      .style('opacity', 0)
      .remove()

    row.on('mouseover', function (d, i) {
      self._onRowOver([d, i])
    })
    .transition()
    .duration(750)
    .attr('transform', transform)

    // Add cells to each row
    var cell = row.selectAll('.cell').data(options.values)

    cell.transition()
      .duration(500)
      .style('fill', fill)
      .attr({
        'height': yScale.rangeBand(),
        'width': xScale.rangeBand(),
        'x': x
      })

    var cg = cell.enter().append('g')

    cg.append('rect')
        .attr({
          'class': 'cell',
          'height': yScale.rangeBand(),
          'x': x,
          'width': xScale.rangeBand()
        })
        .style({
          'opacity': 0,
          'fill': fill
        })
        .transition()
        .duration(500)
        .style('opacity', 1)

    cg.append('text')
          .attr({
            'height': yScale.rangeBand(),
            'x': function (d) { return x(d) + xScale.rangeBand() / 2 },
            'y': options.cellSize / 2,
            'width': xScale.rangeBand(),
            'dominant-baseline': 'central',
            'text-anchor': 'middle',
            'font-weight': 'bold'
          })
          .style({'font-size': options.cellFontSize})
          .text(function (d) { return d.displayValue })
          .transition()
          .duration(500)

    cell.exit()
      .transition()
      .duration(300)
      .style('opacity', 0)
      .remove()

    cell
      .attr('id', d => [d.location.name, d.indicator.short_name].join('-'))
        .style('cursor', _.isFunction(options.onClick) ? 'pointer' : 'initial')
        .on('mousemove', options.onMouseMove)
        .on('mouseout', options.onMouseOut)
        .on('click', options.onClick)

    // Begin X Axis //

    svg.select('.x.axis')
      .transition().duration(500)
      .attr({'transform': 'translate(' + z + ',0)'})
      .call(d3.svg.axis()
        .scale(xScale)
        .orient('top')
        .outerTickSize(0))

    svg.selectAll('.x.axis text')
      .on('click', function (d, i) { self._setSort(d, i) })

    svg.selectAll('.x.axis text').call(this._wrap, xScale.rangeBand())

    // the z axis shows the parent location//
    svg.select('.z.axis')
      .transition().duration(500)
      .attr({'transform': 'translate(0,10)'})
      .call(d3.svg.axis()
        .scale(yScale)
        .tickFormat(function (d) {
          return parentLocationMap[d].parent_location__name
        })
        .orient('left')
        .outerTickSize(0))

    svg.selectAll('.z.axis text')
      .style('font-size', options.fontSize)
      .on('click', function (d, i) {
        options.onRowClick(d, i, this)
      })

    svg.select('.y.axis')
      .transition().duration(500)
      .attr({'transform': 'translate(' + z + ',10)'})
      .call(d3.svg.axis()
        .scale(yScale)
        .orient('left')
        .outerTickSize(0))

    svg.selectAll('.y.axis text')
      .style('font-size', options.fontSize)
      .on('click', function (d, i) {
        options.onRowClick(d, i, this)
      })

    // BEGIN SOURCE FOOTER //

    var singleRowIndicators = options.headers // chartData[0].values
    var sourceFooter = svg.select('.source-footer').attr({'transform': 'translate(0,' + 10 + ')'})
    var sourceCell = sourceFooter.selectAll('.source-cell').data(singleRowIndicators)
    var sourceG = sourceCell.enter().append('g')

    sourceG.append('rect')
        .attr({
          'class': 'cell',
          'height': yScale.rangeBand() * 1.5,
          'transform': 'translate(' + z + ',' + h + ')',
          'x': sourceFlow,
          'width': xScale.rangeBand()
        })
        .style({
          'opacity': 0,
          'fill': '#CCFFFF'
        })
        .transition()
        .duration(500)
      .style('opacity', 1)

    sourceG.append('text')
            .attr({
              'height': yScale.rangeBand(),
              'transform': 'translate(' + z + ',' + h + ')',
              'x': function (d) { return sourceFlow(d) + 45 },
              'y': options.cellSize / 2,
              'width': xScale.rangeBand(),
              'dominant-baseline': 'central',
              'text-anchor': 'middle',
              'font-weight': 'bold'
            })
            .text(function (d) { return d.source_name })
              .transition()
              .duration(500)

    // END SOURCE FOOTER //

    if (options.legend) {
      svg.select('.legend')
        .call(options.legend)
        .attr('transform', function () {
          var bbox = this.getBoundingClientRect()
          var dx = w + margin.right - bbox.width
          var dy = 0

          return 'translate(' + dx + ', ' + dy + ')'
        })
    } else {
      svg.select('.legend').selectAll('*').remove()
    }
  },

  _wrap: function (text, width) {
    text.each(function () {
      var text = d3.select(this)
      var words = text.text().split(/\s+/)
      var line = []
      var lineNumber = 0
      var lineHeight = 1.1 // ems
      var y = text.attr('y') - 10
      var dy = parseFloat(text.attr('dy'))
      var tspan = text.text(null).append('tspan').attr('x', 0).attr('y', y).attr('dy', dy + 'em')
      var i = 0
      for (i = 0; i < words.length; i += 1) {
        var word = words[i]
        line.push(word)
        tspan.text(line.join(' '))
        if (tspan.node().getComputedTextLength() > width) {
          line.pop()
          tspan.text(line.join(' '))
          line = [word]
          tspan = text.append('tspan').attr('x', 0).attr('y', y).attr('dy', ++lineNumber * lineHeight + dy + 'em').text(word)
        }
      }
    })
  },

  _onRowOver: function (d) {
    var seriesName = this._options.seriesName
    this._svg.selectAll('.row')
      .transition().duration(300)
      .style('opacity', function (e) {
        return (seriesName(e) === d[0].name) ? 1 : 0.4
      })
  },

  _onRowOut: function () {
    this._svg.selectAll('.row')
      .transition().duration(300)
      .style('opacity', 1)
  },

  _setSort: function (d) {
    // Fist click, order ascending, Second order descending, third order default
    if (d === this.sortCol && this.sortDirection === -1) {
      this.sortCol = null
    } else if (d === this.sortCol && this.sortDirection === 1) {
      this.sortCol = d
      this.sortDirection = -1
    } else {
      this.sortCol = d
      this.sortDirection = 1
    }

    this.update(this._svg.selectAll('.row').data())
  }
})

export default TableChart
<commit_msg>Update table source box color<commit_after>import _ from 'lodash'
import d3 from 'd3'

import formatUtil from 'util/format'

function _sortValue (s, sortCol) {
  // jshint validthis: true
  var options = this._options

  var val = (sortCol === null)
    ? options.seriesName(s)
    : options.value(_.find(options.values(s), d => options.column(d) === sortCol))

  return val
}

var DEFAULTS = {
  cellSize: 16,
  column: _.property('indicator.short_name'),
  sourceColumn: _.property('short_name'),
  fontSize: 12,
  format: formatUtil.general,
  headerText: _.property('short_name'),
  headers: [],
  onClick: null,
  onColumnHeadOver: null,
  onColumnHeadOut: null,
  onMouseMove: null,
  onMouseOut: null,
  onRowClick: null,
  seriesName: _.property('name'),
  sortValue: _sortValue,
  values: _.property('values'),
  value: _.property('value'),
  sortDirection: 1
}

function TableChart () {}

_.extend(TableChart.prototype, {
  defaults: DEFAULTS,
  sortCol: null,

  initialize: function (el, data, options) {
    options = this._options = _.defaults({}, options, DEFAULTS)

    var svg = this._svg = d3.select(el)
        .append('svg')
        .attr('class', 'heatmap sortable')

    var g = svg.append('g').attr('class', 'margin')

    g.append('g').attr('class', 'z axis')
    g.append('g').attr('class', 'y axis')
    g.append('g').attr('class', 'x axis')
    g.append('g').attr('class', 'data')
    g.append('g').attr('class', 'source-footer')
    g.append('g').attr('class', 'legend')

    this.update(data)
  },

  update: function (data, options) {
    options = _.extend(this._options, options)
    var margin = options.margin

    var self = this
    var parentLocationMap = options.parentLocationMap
    var w = 3 * Math.max(options.headers.length * options.cellSize, 0)
    var h = Math.max(options.defaultSortOrder.length * options.cellSize, 0)
    var z = 160 //  extra margin space needed to add the "z" (parent) axis"

    // hacky way to sclae the view box.. this shoudl be done by taking into
    // account the user's screen size... needs some time to get 100% right.. //
    var viewBoxHeightScale = 1 + (options.headers.length - 8) / 10
    if (viewBoxHeightScale < 1) {
      viewBoxHeightScale = 1
    }
    var viewBox = '0 0 ' + (w + margin.left + margin.right) + ' ' + ((h * viewBoxHeightScale) + margin.top + margin.bottom)
    var svg = this._svg
      .attr({
        'viewBox': viewBox,
        'width': (w + margin.left + margin.right),
        'height': (h + margin.top + margin.bottom)
      })
      .datum(data)

    var xScale = d3.scale.ordinal()
        .domain(_.map(options.headers, options.headerText))
        .rangeBands([0, w], 0.1)

    var x = _.flow(options.column, xScale)
    var sourceFlow = _.flow(options.sourceColumn, xScale)

    // if there is a sortCol set, order the data that way. //
    if (this.sortCol) {
      let sortValue = _.partial(options.sortValue.bind(this), _, this.sortCol)
      var domain = _(data).sortBy(sortValue, this).map(options.seriesName).value()
    } else {
      // if not, show default.  This also applies to the third click of a header
      domain = options.defaultSortOrder
      this.sortDirection = 1
    }

    if (this.sortDirection === -1) {
      domain = domain.reverse()
    }

    // For empty data points i need to add the x axis domain items explicitly //
    // otherwise the domain will be less ( and different the ) the yScale //
    // see trello : https://trello.com/c/bCwyqSWs/277-display-bug-when-creating-table-chart //
    if (domain.length < options.defaultSortOrder.length) {
      var diff = options.defaultSortOrder.filter(function (x) {
        return domain.indexOf(x) < 0
      })
      domain = domain.concat(diff)
    }

    var yScale = d3.scale.ordinal()
      .domain(domain)
      .rangeBands([0, h], 0.1)

    var y = _.flow(options.seriesName, yScale)

    var transform = function (d, i) {
      var yVal = y(d) + 10
      return 'translate(' + z + ' , ' + yVal + ')'
    }

    // THIS SETS THE COLOR... MOVE FROM HERE ONCE THE USER CAN SET A PALLETTE
    var targets = _(options.headers)
      .indexBy('id')
      .mapValues(ind => {
        var extents = [ ind.low_bound, ind.high_bound ]
        var names = ['bad', 'ok', 'good']

        if (ind.low_bound > ind.high_bound) {
          names = ['good', 'ok', 'bad']
        }

        return d3.scale.threshold()
          .domain(extents)
          .range(names)
      })
      .value()

    var scale = d3.scale.ordinal()
      .domain(['bad', 'ok', 'good'])
      .range(['#DB5344', '#79909F', '#2FB0D3'])

    var fill = d => scale(_.get(targets, d.indicator.id, _.noop)(d.value))

    svg.select('.margin')
        .attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')')

    var g = svg.select('.data')

    g.on('mouseout', function () { self._onRowOut.apply(self) })

    var row = g.selectAll('.row').data(data)

    row.enter().append('g')
        .attr({
          'class': 'row',
          'transform': transform
        })

    row.exit()
      .transition().duration(300)
      .style('opacity', 0)
      .remove()

    row.on('mouseover', function (d, i) {
      self._onRowOver([d, i])
    })
    .transition()
    .duration(750)
    .attr('transform', transform)

    // Add cells to each row
    var cell = row.selectAll('.cell').data(options.values)

    cell.transition()
      .duration(500)
      .style('fill', fill)
      .attr({
        'height': yScale.rangeBand(),
        'width': xScale.rangeBand(),
        'x': x
      })

    var cg = cell.enter().append('g')

    cg.append('rect')
        .attr({
          'class': 'cell',
          'height': yScale.rangeBand(),
          'x': x,
          'width': xScale.rangeBand()
        })
        .style({
          'opacity': 0,
          'fill': fill
        })
        .transition()
        .duration(500)
        .style('opacity', 1)

    cg.append('text')
          .attr({
            'height': yScale.rangeBand(),
            'x': function (d) { return x(d) + xScale.rangeBand() / 2 },
            'y': options.cellSize / 2,
            'width': xScale.rangeBand(),
            'dominant-baseline': 'central',
            'text-anchor': 'middle',
            'font-weight': 'bold'
          })
          .style({'font-size': options.cellFontSize})
          .text(function (d) { return d.displayValue })
          .transition()
          .duration(500)

    cell.exit()
      .transition()
      .duration(300)
      .style('opacity', 0)
      .remove()

    cell
      .attr('id', d => [d.location.name, d.indicator.short_name].join('-'))
        .style('cursor', _.isFunction(options.onClick) ? 'pointer' : 'initial')
        .on('mousemove', options.onMouseMove)
        .on('mouseout', options.onMouseOut)
        .on('click', options.onClick)

    // Begin X Axis //

    svg.select('.x.axis')
      .transition().duration(500)
      .attr({'transform': 'translate(' + z + ',0)'})
      .call(d3.svg.axis()
        .scale(xScale)
        .orient('top')
        .outerTickSize(0))

    svg.selectAll('.x.axis text')
      .on('click', function (d, i) { self._setSort(d, i) })

    svg.selectAll('.x.axis text').call(this._wrap, xScale.rangeBand())

    // the z axis shows the parent location//
    svg.select('.z.axis')
      .transition().duration(500)
      .attr({'transform': 'translate(0,10)'})
      .call(d3.svg.axis()
        .scale(yScale)
        .tickFormat(function (d) {
          return parentLocationMap[d].parent_location__name
        })
        .orient('left')
        .outerTickSize(0))

    svg.selectAll('.z.axis text')
      .style('font-size', options.fontSize)
      .on('click', function (d, i) {
        options.onRowClick(d, i, this)
      })

    svg.select('.y.axis')
      .transition().duration(500)
      .attr({'transform': 'translate(' + z + ',10)'})
      .call(d3.svg.axis()
        .scale(yScale)
        .orient('left')
        .outerTickSize(0))

    svg.selectAll('.y.axis text')
      .style('font-size', options.fontSize)
      .on('click', function (d, i) {
        options.onRowClick(d, i, this)
      })

    // BEGIN SOURCE FOOTER //

    var singleRowIndicators = options.headers // chartData[0].values
    var sourceFooter = svg.select('.source-footer').attr({'transform': 'translate(0,' + 10 + ')'})
    var sourceCell = sourceFooter.selectAll('.source-cell').data(singleRowIndicators)
    var sourceG = sourceCell.enter().append('g')

    sourceG.append('rect')
        .attr({
          'class': 'cell',
          'height': yScale.rangeBand() * 1.5,
          'transform': 'translate(' + z + ',' + h + ')',
          'x': sourceFlow,
          'width': xScale.rangeBand()
        })
        .style({
          'opacity': 0,
          'fill': '#F1F1F1'
        })
        .transition()
        .duration(500)
      .style('opacity', 1)

    sourceG.append('text')
            .attr({
              'height': yScale.rangeBand(),
              'transform': 'translate(' + z + ',' + h + ')',
              'x': function (d) { return sourceFlow(d) + 45 },
              'y': options.cellSize / 2,
              'width': xScale.rangeBand(),
              'dominant-baseline': 'central',
              'text-anchor': 'middle',
              'font-weight': 'bold'
            })
            .text(function (d) { return d.source_name })
              .transition()
              .duration(500)

    // END SOURCE FOOTER //

    if (options.legend) {
      svg.select('.legend')
        .call(options.legend)
        .attr('transform', function () {
          var bbox = this.getBoundingClientRect()
          var dx = w + margin.right - bbox.width
          var dy = 0

          return 'translate(' + dx + ', ' + dy + ')'
        })
    } else {
      svg.select('.legend').selectAll('*').remove()
    }
  },

  _wrap: function (text, width) {
    text.each(function () {
      var text = d3.select(this)
      var words = text.text().split(/\s+/)
      var line = []
      var lineNumber = 0
      var lineHeight = 1.1 // ems
      var y = text.attr('y') - 10
      var dy = parseFloat(text.attr('dy'))
      var tspan = text.text(null).append('tspan').attr('x', 0).attr('y', y).attr('dy', dy + 'em')
      var i = 0
      for (i = 0; i < words.length; i += 1) {
        var word = words[i]
        line.push(word)
        tspan.text(line.join(' '))
        if (tspan.node().getComputedTextLength() > width) {
          line.pop()
          tspan.text(line.join(' '))
          line = [word]
          tspan = text.append('tspan').attr('x', 0).attr('y', y).attr('dy', ++lineNumber * lineHeight + dy + 'em').text(word)
        }
      }
    })
  },

  _onRowOver: function (d) {
    var seriesName = this._options.seriesName
    this._svg.selectAll('.row')
      .transition().duration(300)
      .style('opacity', function (e) {
        return (seriesName(e) === d[0].name) ? 1 : 0.4
      })
  },

  _onRowOut: function () {
    this._svg.selectAll('.row')
      .transition().duration(300)
      .style('opacity', 1)
  },

  _setSort: function (d) {
    // Fist click, order ascending, Second order descending, third order default
    if (d === this.sortCol && this.sortDirection === -1) {
      this.sortCol = null
    } else if (d === this.sortCol && this.sortDirection === 1) {
      this.sortCol = d
      this.sortDirection = -1
    } else {
      this.sortCol = d
      this.sortDirection = 1
    }

    this.update(this._svg.selectAll('.row').data())
  }
})

export default TableChart
<|endoftext|>


<commit_before>define('views/listing',
    ['feed_previews', 'format', 'jquery', 'l10n', 'log', 'notification', 'requests', 'settings', 'templates', 'underscore', 'urls', 'utils', 'z'],
    function(feed_previews, format, $, l10n, log, notification, requests, settings, nunjucks, _, urls, utils, z) {
    'use strict';
    var gettext = l10n.gettext;

    z.body.on('click', '.manage-modules-listing .delete', utils._pd(function(e) {
        e.stopPropagation();

        // Delete.
        var $this = $(this);
        var $item = $this.closest('li');

        if (!window.confirm(format.format(gettext('Do you really want to delete {0}?'), $item.data('slug')))) {
            // Confirm.
            return;
        }

        var endpoint = urls.api.url('feed-app', [$item.data('slug')]);
        if ($this.hasClass('collection')) {
            endpoint = urls.api.url('collection', [$item.data('slug')]);
        } else if ($this.hasClass('brand')) {
            endpoint = urls.api.url('feed-brand', [$item.data('slug')]);
        }

        requests.del(endpoint).done(function(data) {
            notification.notification({message: gettext('Successfully deleted')});
        });

        $item.remove();
    }))
    .on('keypress', '.search-elements', _.debounce(function() {
        var $this = $(this);

        if ($this.val().length > 1) {
            requests.get(urls.api.url('feed-element-search', [], {'q': $this.val()})).done(function(data) {
                $('.feed-api-results').hide();
                $('.feed-search-results').html(nunjucks.env.render('search_results.html', {
                    data: data
                })).show();
            });
        } else {
            $('.feed-api-results').show();
            $('.feed-search-results').hide();
        }
    }, 250));

    return function(builder, args) {
        builder.z('title', gettext('Existing Content'));
        builder.z('type', 'manage');
        builder.start('listing/listing.html');

        feed_previews.empty();
    };
});
<commit_msg>bust the cache when deleting stuff too<commit_after>define('views/listing',
    ['cache', 'feed_previews', 'format', 'jquery', 'l10n', 'log', 'notification', 'requests', 'settings', 'templates', 'underscore', 'urls', 'utils', 'z'],
    function(cache, feed_previews, format, $, l10n, log, notification, requests, settings, nunjucks, _, urls, utils, z) {
    'use strict';
    var gettext = l10n.gettext;

    z.body.on('click', '.manage-modules-listing .delete', utils._pd(function(e) {
        e.stopPropagation();

        // Delete.
        var $this = $(this);
        var $item = $this.closest('li');

        if (!window.confirm(format.format(gettext('Do you really want to delete {0}?'), $item.data('slug')))) {
            // Confirm.
            return;
        }

        var endpoint = urls.api.url('feed-app', [$item.data('slug')]);
        if ($this.hasClass('collection')) {
            endpoint = urls.api.url('collection', [$item.data('slug')]);
        } else if ($this.hasClass('brand')) {
            endpoint = urls.api.url('feed-brand', [$item.data('slug')]);
        }

        requests.del(endpoint).done(function(data) {
            notification.notification({message: gettext('Successfully deleted')});
            $item.remove();
            cache.flush();
        }).fail(function() {
            notification.notification({message: gettext('Sorry, we had an error deleting that')});
        });

    }))
    .on('keypress', '.search-elements', _.debounce(function() {
        var $this = $(this);

        if ($this.val().length > 1) {
            requests.get(urls.api.url('feed-element-search', [], {'q': $this.val()})).done(function(data) {
                $('.feed-api-results').hide();
                $('.feed-search-results').html(nunjucks.env.render('search_results.html', {
                    data: data
                })).show();
            });
        } else {
            $('.feed-api-results').show();
            $('.feed-search-results').hide();
        }
    }, 250));

    return function(builder, args) {
        builder.z('title', gettext('Existing Content'));
        builder.z('type', 'manage');
        builder.start('listing/listing.html');

        feed_previews.empty();
    };
});
<|endoftext|>