<commit_before>/* global rome */
(function() {
  'use strict';

  var mod = angular.module('cla.directives');

  mod.directive('callbackModal', ['AppSettings', 'moment', 'postal', '$timeout', 'ClaPostalService', 'hotkeys', 'flash', 'form_utils', '$state', function (AppSettings, moment, postal, $timeout, ClaPostalService, hotkeys, flash, form_utils, $state) {
    var timeRounding = 30 * 60 * 1000; // to nearest 30 mins
    var startBuffer = 30; // in mins

    return {
      restrict: 'E',
      replace: true,
      templateUrl: 'directives/callbackModal.html',
      scope: {},
      controller: function ($scope, $element) {
        if (!AppSettings.callMeBackEnabled) {
          $element.remove();
        }

        var minStart = moment().add(startBuffer, 'minutes');
        var start = moment(Math.ceil((+minStart) / timeRounding) * timeRounding);
        $scope.setCurrentDateTime = function(dt) {
          var mdt = moment(dt);
          $scope.currentDateTime = mdt;
          $scope.endTimeValue = mdt.add(30, 'minutes').format('HH:mm');
        };

        $scope.setCurrentDateTime(start);

        // datepicker conf has to be set before template and other directive loads
        $scope.datePickerConf = {
          timeValidator: function (d) {
            var m = moment(d);
            var day = m.clone().day();
            var start = m.clone().hour(8).minute(59).second(59);
            var end = m.clone().hour(19).minute(30).second(1);
            var satEnd = m.clone().hour(12).minute(0).second(1);

            if (day === 0) { // sunday
              return false;
            }
            if (day === 6) { // saturday
              return m.isAfter(start) && m.isBefore(satEnd);
            }
            return m.isAfter(start) && m.isBefore(end);
          },
          dateValidator: function (d) {
            var m = moment(d);
            var today = moment();
            var day = m.clone().day();
            var start = m.clone().hour(8).minute(59).second(59);
            var end = day === 6 ? m.clone().hour(12).minute(0).second(1) : m.clone().hour(19).minute(30).second(1);

            if (day === 0) { // sunday
              return false;
            }
            if (m.isSame(today, 'day')) { // if today
              return m.isAfter(start) && m.isBefore(end);
            }
            return true;
          },
          initialValue: start,
          min: minStart
        };
      },
      link: function (scope, elm) {
        var calendar = rome.find(angular.element('[datetime-picker]', elm)[0]);
        var target;
        scope.isVisible = false;

        var bookSub = postal.subscribe({
          channel: 'CallBack',
          topic: 'toggle',
          callback: function (data) {
            if (data._case) {
              var show = true;

              setCase(data._case);

              if (scope.isVisible) {
                show = false;
              }

              // if a targer has been passed, store it
              if (data.target) {
                target = data.target;
              }

              toggleModal(show);
            } else {
              console.warn('You must pass a case object to a callback modal');
            }
          }
        });

        scope.$on('$destroy', function handleDestroyEvent () {
          bookSub.unsubscribe();
        });

        calendar.on('data', function (value) {
          var m = moment(formatUkDateTime(value));
          var firstSlot = m.hour(9).minutes(0);
          var today = moment();

          if (!m.isSame(scope.currentDateTime, 'day')) {
            if (m.isSame(today, 'day')) {
              scope.setToday();
            } else {
              calendar.setValue(firstSlot);
            }
          }

          scope.setCurrentDateTime(calendar.getDate());

          postal.publish({ channel: 'CallBack', topic: 'set.date' });
        });

        var formatUkDateTime = function(str) {
          var date = str.split(' ')[0];
          var time = str.split(' ')[1];
          var datePieces = date.split('/');
          var timePieces = time.split(':');
          return new Date(datePieces[2], datePieces[1] - 1, datePieces[0], timePieces[0], timePieces[1]);
        };

        var toggleModal = function (toggle) {
          scope.isVisible = toggle;

          // bind/unbind click listener to show/hide results
          if (toggle) {
            angular.element('body').on('click.callbackDelegate', function (e) {
              var $target = angular.element(e.target);

              if (
                !$target.hasClass('CallbackModal') &&
                !$target.hasClass('rd-day-prev-month') &&
                !$target.hasClass('rd-day-next-month') &&
                angular.element(e.target).parents('.CallbackModal').length < 1
              ) {
                toggleModal(false);
              }
            });

            // keyboard shortcut to close modal
            hotkeys
              .bindTo(scope)
              .add({
                combo: 'esc',
                description: 'Close callback overlay',
                callback: function(e, hotkey) {
                  ClaPostalService.publishHotKey(hotkey);
                  toggleModal(false);
                }
              });

            $timeout(function() {
              elm.find('button').first().focus();
            });

            postal.publish({ channel: 'CallBack', topic: 'open' });
          } else {
            angular.element('body').off('click.callbackDelegate');
            hotkeys.del('esc');

            if (target) {
              $timeout(function() {
                target.focus();
              });
            }

            postal.publish({ channel: 'CallBack', topic: 'close' });
          }
        };

        var setCase = function (_case) {
          scope.case = _case;
          scope.canBeCalledBack = _case.canBeCalledBack();
          scope.createdByWeb = _case.createdByWeb();
          scope.isFinalCallBack = _case.isFinalCallback();

          if (scope.canBeCalledBack) {
            if (_case.isFinalCallback()) {
              scope.callbackTitle = 'Schedule final callback';
            } else if (_case.getCallbackDatetime()) {
              scope.callbackTitle = 'Schedule a new callback';
            } else {
              scope.callbackTitle = 'Schedule a callback';
            }
          } else {
            scope.callbackTitle = 'Cancel callback';
          }
        };

        scope.setToday = function () {
          var minTime = moment().add(startBuffer, 'minutes');
          var today = moment(Math.ceil((+minTime) / timeRounding) * timeRounding);
          calendar.setValue(today);
          postal.publish({ channel: 'CallBack', topic: 'set.today' });
        };

        scope.setTomorrow = function () {
          var m = moment().add(1, 'days');
          var firstSlotTomorrow;

          if (m.day() === 0) { // sunday
            m.add(1, 'days');
          }

          firstSlotTomorrow = m.hour(9).minute(0);
          calendar.setValue(firstSlotTomorrow);
          postal.publish({ channel: 'CallBack', topic: 'set.tomorrow' });
        };

        scope.bookCallback = function (form) {
          scope.case.$call_me_back({
            'datetime': (function () {
              var local = moment.tz(calendar.getDate(), 'Europe/London');
              return local.tz('UTC').format('DD/MM/YYYY HH:mm');
            })(),
            'notes': scope.callbackNotes || '',
            'priority_callback': !!scope.priorityCallback
          }).then(function() {
            flash('success', 'Callback scheduled successfully.');
            $state.go('case_list');
          }, function(data) {
            form_utils.ctrlFormErrorCallback(scope, data, form);
          });
        };

        scope.cancelCallback = function() {
          scope.case.$cancel_call_me_back().then(function() {
            flash('success', 'Callback cancelled successfully.');
            $state.go('case_list');
          });
        };

        scope.close = function() {
          toggleModal(false);
        };
      }
    };
  }]);
})();
<commit_msg>Set min and initial times separately<commit_after>/* global rome */
(function() {
  'use strict';

  var mod = angular.module('cla.directives');

  mod.directive('callbackModal', ['AppSettings', 'moment', 'postal', '$timeout', 'ClaPostalService', 'hotkeys', 'flash', 'form_utils', '$state', function (AppSettings, moment, postal, $timeout, ClaPostalService, hotkeys, flash, form_utils, $state) {
    var timeRounding = 30 * 60 * 1000; // to nearest 30 mins
    var earliestStartBuffer = -30; // in mins
    var initialStartBuffer = 0; // in mins

    return {
      restrict: 'E',
      replace: true,
      templateUrl: 'directives/callbackModal.html',
      scope: {},
      controller: function ($scope, $element) {
        if (!AppSettings.callMeBackEnabled) {
          $element.remove();
        }

        var preliminaryMinStart = moment().add(earliestStartBuffer, 'minutes');
        var minStart = moment(Math.ceil((+preliminaryMinStart) / timeRounding) * timeRounding);
        var preliminaryStart = moment().add(initialStartBuffer, 'minutes');
        var start = moment(Math.ceil((+preliminaryStart) / timeRounding) * timeRounding);
        $scope.setCurrentDateTime = function(dt) {
          var mdt = moment(dt);
          $scope.currentDateTime = mdt;
          $scope.endTimeValue = mdt.add(30, 'minutes').format('HH:mm');
        };

        $scope.setCurrentDateTime(start);

        // datepicker conf has to be set before template and other directive loads
        $scope.datePickerConf = {
          timeValidator: function (d) {
            var m = moment(d);
            var day = m.clone().day();
            var start = m.clone().hour(8).minute(59).second(59);
            var end = m.clone().hour(19).minute(30).second(1);
            var satEnd = m.clone().hour(12).minute(0).second(1);

            if (day === 0) { // sunday
              return false;
            }
            if (day === 6) { // saturday
              return m.isAfter(start) && m.isBefore(satEnd);
            }
            return m.isAfter(start) && m.isBefore(end);
          },
          dateValidator: function (d) {
            var m = moment(d);
            var today = moment();
            var day = m.clone().day();
            var start = m.clone().hour(8).minute(59).second(59);
            var end = day === 6 ? m.clone().hour(12).minute(0).second(1) : m.clone().hour(19).minute(30).second(1);

            if (day === 0) { // sunday
              return false;
            }
            if (m.isSame(today, 'day')) { // if today
              return m.isAfter(start) && m.isBefore(end);
            }
            return true;
          },
          initialValue: start,
          min: minStart
        };
      },
      link: function (scope, elm) {
        var calendar = rome.find(angular.element('[datetime-picker]', elm)[0]);
        var target;
        scope.isVisible = false;

        var bookSub = postal.subscribe({
          channel: 'CallBack',
          topic: 'toggle',
          callback: function (data) {
            if (data._case) {
              var show = true;

              setCase(data._case);

              if (scope.isVisible) {
                show = false;
              }

              // if a targer has been passed, store it
              if (data.target) {
                target = data.target;
              }

              toggleModal(show);
            } else {
              console.warn('You must pass a case object to a callback modal');
            }
          }
        });

        scope.$on('$destroy', function handleDestroyEvent () {
          bookSub.unsubscribe();
        });

        calendar.on('data', function (value) {
          var m = moment(formatUkDateTime(value));
          var firstSlot = m.hour(9).minutes(0);
          var today = moment();

          if (!m.isSame(scope.currentDateTime, 'day')) {
            if (m.isSame(today, 'day')) {
              scope.setToday();
            } else {
              calendar.setValue(firstSlot);
            }
          }

          scope.setCurrentDateTime(calendar.getDate());

          postal.publish({ channel: 'CallBack', topic: 'set.date' });
        });

        var formatUkDateTime = function(str) {
          var date = str.split(' ')[0];
          var time = str.split(' ')[1];
          var datePieces = date.split('/');
          var timePieces = time.split(':');
          return new Date(datePieces[2], datePieces[1] - 1, datePieces[0], timePieces[0], timePieces[1]);
        };

        var toggleModal = function (toggle) {
          scope.isVisible = toggle;

          // bind/unbind click listener to show/hide results
          if (toggle) {
            angular.element('body').on('click.callbackDelegate', function (e) {
              var $target = angular.element(e.target);

              if (
                !$target.hasClass('CallbackModal') &&
                !$target.hasClass('rd-day-prev-month') &&
                !$target.hasClass('rd-day-next-month') &&
                angular.element(e.target).parents('.CallbackModal').length < 1
              ) {
                toggleModal(false);
              }
            });

            // keyboard shortcut to close modal
            hotkeys
              .bindTo(scope)
              .add({
                combo: 'esc',
                description: 'Close callback overlay',
                callback: function(e, hotkey) {
                  ClaPostalService.publishHotKey(hotkey);
                  toggleModal(false);
                }
              });

            $timeout(function() {
              elm.find('button').first().focus();
            });

            postal.publish({ channel: 'CallBack', topic: 'open' });
          } else {
            angular.element('body').off('click.callbackDelegate');
            hotkeys.del('esc');

            if (target) {
              $timeout(function() {
                target.focus();
              });
            }

            postal.publish({ channel: 'CallBack', topic: 'close' });
          }
        };

        var setCase = function (_case) {
          scope.case = _case;
          scope.canBeCalledBack = _case.canBeCalledBack();
          scope.createdByWeb = _case.createdByWeb();
          scope.isFinalCallBack = _case.isFinalCallback();

          if (scope.canBeCalledBack) {
            if (_case.isFinalCallback()) {
              scope.callbackTitle = 'Schedule final callback';
            } else if (_case.getCallbackDatetime()) {
              scope.callbackTitle = 'Schedule a new callback';
            } else {
              scope.callbackTitle = 'Schedule a callback';
            }
          } else {
            scope.callbackTitle = 'Cancel callback';
          }
        };

        scope.setToday = function () {
          var minTime = moment().add(initialStartBuffer, 'minutes');
          var today = moment(Math.ceil((+minTime) / timeRounding) * timeRounding);
          calendar.setValue(today);
          postal.publish({ channel: 'CallBack', topic: 'set.today' });
        };

        scope.setTomorrow = function () {
          var m = moment().add(1, 'days');
          var firstSlotTomorrow;

          if (m.day() === 0) { // sunday
            m.add(1, 'days');
          }

          firstSlotTomorrow = m.hour(9).minute(0);
          calendar.setValue(firstSlotTomorrow);
          postal.publish({ channel: 'CallBack', topic: 'set.tomorrow' });
        };

        scope.bookCallback = function (form) {
          scope.case.$call_me_back({
            'datetime': (function () {
              var local = moment.tz(calendar.getDate(), 'Europe/London');
              return local.tz('UTC').format('DD/MM/YYYY HH:mm');
            })(),
            'notes': scope.callbackNotes || '',
            'priority_callback': !!scope.priorityCallback
          }).then(function() {
            flash('success', 'Callback scheduled successfully.');
            $state.go('case_list');
          }, function(data) {
            form_utils.ctrlFormErrorCallback(scope, data, form);
          });
        };

        scope.cancelCallback = function() {
          scope.case.$cancel_call_me_back().then(function() {
            flash('success', 'Callback cancelled successfully.');
            $state.go('case_list');
          });
        };

        scope.close = function() {
          toggleModal(false);
        };
      }
    };
  }]);
})();
<|endoftext|>


<commit_before>ROOT = process.cwd()
HELPERS = require( ROOT + '/helpers/general.js' );
log = HELPERS.log
var config = require( ROOT + '/config.json' );

var fs = require( 'fs' );
var os = require("os");

var DeviceHandler = require( ROOT + '/Models/devicesHandler' );
var DEVICE_HANDLER = new DeviceHandler();

var ACTION_INSTANCE_DATA_HANDLER = new( require( ROOT + '/Models/actionInstanceData' ) );

this.showIndexView = function( req, res ) {

    log('HOST: ' + os.hostname());

	res.render( 'index', {
		'locals': {
			'title': config.app_name,
			'marketingMode': config.web_console.marketing_mode,
			'hostName': req.headers.host, //config.server.host,
			'pubsubPort': config.services.ojsConsole.port,
		}
	} );

};

this.getActionMetadata = function( req, res ) {
	var actionName = req.params.actionName;
	log( 'metadata for: ' + actionName );
	ACTION_INSTANCE_DATA_HANDLER.findactionInstanceData( actionName, function( err, metadata ) {

		if ( err ) {
			res.writeHead( 500, {
				'Content-Type': 'text/plain'
			} );
			res.write( err + '\n' );
			res.end();
		} else if ( metadata == null || metadata == undefined || metadata.args == null || metadata.args == undefined ) {
			res.writeHead( 404, {
				"Content-Type": "application/json"
			} );
			res.write( 'No metadata found for ' + actionName );
			res.end();

		} else {

			var arguments = JSON.stringify( metadata.args );
			arguments = JSON.parse( arguments );

			res.writeHead( 200, {
				"Content-Type": "application/json"
			} );
			res.write(
				JSON.stringify( {
					'args': metadata.args
				} )
			);
			res.end();
		}
	} );
};

this.getActions = function( req, res ) {
	var actionsPath = ROOT + '/resources/actions/';
	fs.readdir( actionsPath, function( err, files ) {
		if ( err ) {
			log( err );
			res.writeHead( 500, {
				'Content-Type': 'text/plain'
			} );
			res.write( err + '\n' );
			res.end();
		} else {

			var fileNames = [];
			for ( var i = 0; i < files.length; i++ ) {
				var file = files[ i ];
				if ( file[ 0 ] == '.' || file.slice( -3 ) != '.js' || file == 'misc.js' ) {
					continue;
				}
				fileNames.push( file.replace( '.js', '' ) );
			}
			res.writeHead( 200, {
				"Content-Type": "application/json"
			} );
			res.write(
				JSON.stringify( {
					//'actions': fileNames.sort()
					'files': fileNames.sort()
				} )
			);
			res.end();
		}
	} );
};

this.getAction = function( req, res ) {
	var actionName = req.params.actionName;
	fs.readFile( ROOT + '/resources/actions/' + actionName + '.js', 'binary', function( error, file ) {

		if ( error ) {
			res.writeHead( 500, {
				'Content-Type': 'text/plain'
			} );
			res.write( error + '\n' );
			res.end();
		} else {
			res.writeHead( 200, {
				'Content-Type': 'text/plain'
			} );
			res.write( file, 'binary' );
			res.end();
		}
	} );

};

this.getCapabilities = function( req, res ) {
	fs.readdir( ROOT + config.resources.capabilities, function( err, files ) {
		if ( err ) {
			log( err );
			res.writeHead( 500, {
				'Content-Type': 'text/plain'
			} );
			res.write( err + '\n' );
			res.end();
		} else {

			var fileNames = [];
			for ( var i = 0; i < files.length; i++ ) {
				var file = files[ i ];
				if ( file[ 0 ] == '.' || file.slice( -3 ) != '.js' || file == 'misc.js' ) {
					continue;
				}
				fileNames.push( file.replace( '.js', '' ) );
			}
			res.writeHead( 200, {
				"Content-Type": "application/json"
			} );
			res.write(
				JSON.stringify( {
					//'capabilities': fileNames.sort()
					files: fileNames.sort(),
					capabilities: fileNames.sort(),
					deviceTypes: config.device_types
				} )
			);
			res.end();
		}
	} );
};

this.getCapability = function( req, res ) {
	var capabilityName = req.params.capabilityName;
	log( capabilityName );

	fs.readFile( ROOT + config.resources.capabilities + capabilityName + '.js', 'binary', function( error, file ) {
		if ( error ) {
			res.writeHead( 500, {
				'Content-Type': 'text/plain'
			} );
			res.write( error + '\n' );
			res.end();
		} else {
			res.writeHead( 200, {
				'Content-Type': 'text/plain'
			} );
			res.write( file, 'binary' );
			res.end();
		}
	} );

};

this.getDownloads = function( req, res ) {
	fs.readdir( ROOT + config.clients_path, function( err, files ) {
		if ( err ) {
			log( err );
			res.writeHead( 500, {
				'Content-Type': 'text/plain'
			} );
			res.write( err + '\n' );
			res.end();
		} else {

			var fileNames = [];
			for ( var i = 0; i < files.length; i++ ) {
				var file = files[ i ];
				if ( file[ 0 ] == '.' || file.slice( -4 ) != '.apk' ) {
					continue;
				}
				fileNames.push( file );
			}
			res.writeHead( 200, {
				"Content-Type": "application/json"
			} );
			res.write(
				JSON.stringify( {
					'client_app_names': fileNames.sort()
				} )
			);
			res.end();
		}
	} );
};

this.getDownload = function( req, res ) {
	var clientName = req.params.clientName;
	fs.readFile( ROOT + config.clients_path + clientName, 'binary', function( error, file ) {
		if ( error ) {
			res.writeHead( 500, {
				'Content-Type': 'text/plain'
			} );
			res.write( error + '\n' );
			res.end();
		} else {
			res.writeHead( 200, {
				'Content-Type': 'text/plain'
			} );
			res.write( file, 'binary' );
			res.end();
		}
	} );
};<commit_msg>hostname issue in OJS console<commit_after>ROOT = process.cwd()
HELPERS = require( ROOT + '/helpers/general.js' );
log = HELPERS.log
var config = require( ROOT + '/config.json' );

var fs = require( 'fs' );
var os = require("os");

var DeviceHandler = require( ROOT + '/Models/devicesHandler' );
var DEVICE_HANDLER = new DeviceHandler();

var ACTION_INSTANCE_DATA_HANDLER = new( require( ROOT + '/Models/actionInstanceData' ) );

this.showIndexView = function( req, res ) {

    log('HOST: ' + req.headers.host.replace(':'+config.server.port) );

	res.render( 'index', {
		'locals': {
			'title': config.app_name,
			'marketingMode': config.web_console.marketing_mode,
			'hostName': req.headers.host.replace(':'+config.server.port), //config.server.host,
			'pubsubPort': config.services.ojsConsole.port,
		}
	} );

};

this.getActionMetadata = function( req, res ) {
	var actionName = req.params.actionName;
	log( 'metadata for: ' + actionName );
	ACTION_INSTANCE_DATA_HANDLER.findactionInstanceData( actionName, function( err, metadata ) {

		if ( err ) {
			res.writeHead( 500, {
				'Content-Type': 'text/plain'
			} );
			res.write( err + '\n' );
			res.end();
		} else if ( metadata == null || metadata == undefined || metadata.args == null || metadata.args == undefined ) {
			res.writeHead( 404, {
				"Content-Type": "application/json"
			} );
			res.write( 'No metadata found for ' + actionName );
			res.end();

		} else {

			var arguments = JSON.stringify( metadata.args );
			arguments = JSON.parse( arguments );

			res.writeHead( 200, {
				"Content-Type": "application/json"
			} );
			res.write(
				JSON.stringify( {
					'args': metadata.args
				} )
			);
			res.end();
		}
	} );
};

this.getActions = function( req, res ) {
	var actionsPath = ROOT + '/resources/actions/';
	fs.readdir( actionsPath, function( err, files ) {
		if ( err ) {
			log( err );
			res.writeHead( 500, {
				'Content-Type': 'text/plain'
			} );
			res.write( err + '\n' );
			res.end();
		} else {

			var fileNames = [];
			for ( var i = 0; i < files.length; i++ ) {
				var file = files[ i ];
				if ( file[ 0 ] == '.' || file.slice( -3 ) != '.js' || file == 'misc.js' ) {
					continue;
				}
				fileNames.push( file.replace( '.js', '' ) );
			}
			res.writeHead( 200, {
				"Content-Type": "application/json"
			} );
			res.write(
				JSON.stringify( {
					//'actions': fileNames.sort()
					'files': fileNames.sort()
				} )
			);
			res.end();
		}
	} );
};

this.getAction = function( req, res ) {
	var actionName = req.params.actionName;
	fs.readFile( ROOT + '/resources/actions/' + actionName + '.js', 'binary', function( error, file ) {

		if ( error ) {
			res.writeHead( 500, {
				'Content-Type': 'text/plain'
			} );
			res.write( error + '\n' );
			res.end();
		} else {
			res.writeHead( 200, {
				'Content-Type': 'text/plain'
			} );
			res.write( file, 'binary' );
			res.end();
		}
	} );

};

this.getCapabilities = function( req, res ) {
	fs.readdir( ROOT + config.resources.capabilities, function( err, files ) {
		if ( err ) {
			log( err );
			res.writeHead( 500, {
				'Content-Type': 'text/plain'
			} );
			res.write( err + '\n' );
			res.end();
		} else {

			var fileNames = [];
			for ( var i = 0; i < files.length; i++ ) {
				var file = files[ i ];
				if ( file[ 0 ] == '.' || file.slice( -3 ) != '.js' || file == 'misc.js' ) {
					continue;
				}
				fileNames.push( file.replace( '.js', '' ) );
			}
			res.writeHead( 200, {
				"Content-Type": "application/json"
			} );
			res.write(
				JSON.stringify( {
					//'capabilities': fileNames.sort()
					files: fileNames.sort(),
					capabilities: fileNames.sort(),
					deviceTypes: config.device_types
				} )
			);
			res.end();
		}
	} );
};

this.getCapability = function( req, res ) {
	var capabilityName = req.params.capabilityName;
	log( capabilityName );

	fs.readFile( ROOT + config.resources.capabilities + capabilityName + '.js', 'binary', function( error, file ) {
		if ( error ) {
			res.writeHead( 500, {
				'Content-Type': 'text/plain'
			} );
			res.write( error + '\n' );
			res.end();
		} else {
			res.writeHead( 200, {
				'Content-Type': 'text/plain'
			} );
			res.write( file, 'binary' );
			res.end();
		}
	} );

};

this.getDownloads = function( req, res ) {
	fs.readdir( ROOT + config.clients_path, function( err, files ) {
		if ( err ) {
			log( err );
			res.writeHead( 500, {
				'Content-Type': 'text/plain'
			} );
			res.write( err + '\n' );
			res.end();
		} else {

			var fileNames = [];
			for ( var i = 0; i < files.length; i++ ) {
				var file = files[ i ];
				if ( file[ 0 ] == '.' || file.slice( -4 ) != '.apk' ) {
					continue;
				}
				fileNames.push( file );
			}
			res.writeHead( 200, {
				"Content-Type": "application/json"
			} );
			res.write(
				JSON.stringify( {
					'client_app_names': fileNames.sort()
				} )
			);
			res.end();
		}
	} );
};

this.getDownload = function( req, res ) {
	var clientName = req.params.clientName;
	fs.readFile( ROOT + config.clients_path + clientName, 'binary', function( error, file ) {
		if ( error ) {
			res.writeHead( 500, {
				'Content-Type': 'text/plain'
			} );
			res.write( error + '\n' );
			res.end();
		} else {
			res.writeHead( 200, {
				'Content-Type': 'text/plain'
			} );
			res.write( file, 'binary' );
			res.end();
		}
	} );
};<|endoftext|>


<commit_before>/**
 * Copyright (c) 2016, salesforce.com, inc.
 * All rights reserved.
 * Licensed under the BSD 3-Clause license.
 * For full license text, see LICENSE.txt file in the repo root or
 * https://opensource.org/licenses/BSD-3-Clause
 */

/**
 * db/model/aspect.js
 */

const common = require('../helpers/common');
const u = require('../helpers/aspectUtils');
const constants = require('../constants');
const dbErrors = require('../dbErrors');
const assoc = {};
const timeoutLength = 10;
const timeoutRegex = /^[0-9]{1,9}[SMHDsmhd]$/;
const valueLabelLength = 10;
const redisOps = require('../../cache/redisOps');
const aspectType = redisOps.aspectType;
const sampleType = redisOps.sampleType;
const Promise = require('bluebird');
const Op = require('sequelize').Op;
const publishSample = require('../../realtime/redisPublisher').publishSample;
const sampleEventNames = {
  add: 'refocus.internal.realtime.sample.add',
  upd: 'refocus.internal.realtime.sample.update',
  del: 'refocus.internal.realtime.sample.remove',
};

module.exports = function aspect(seq, dataTypes) {
  const Aspect = seq.define('Aspect', {
    description: {
      type: dataTypes.STRING(constants.fieldlen.longish),
    },
    helpEmail: {
      type: dataTypes.STRING(constants.fieldlen.email),
      validate: { isEmail: true },
    },
    helpUrl: {
      type: dataTypes.STRING(constants.fieldlen.url),
      validate: { isUrl: true },
    },
    id: {
      type: dataTypes.UUID,
      primaryKey: true,
      defaultValue: dataTypes.UUIDV4,
    },
    isDeleted: {
      type: dataTypes.BIGINT,
      defaultValue: 0,
      allowNull: false,
    },
    imageUrl: {
      type: dataTypes.STRING(constants.fieldlen.url),
      validate: { isUrl: true },
    },
    isPublished: {
      type: dataTypes.BOOLEAN,
      allowNull: false,
      defaultValue: false,
    },
    name: {
      type: dataTypes.STRING(constants.fieldlen.normalName),
      allowNull: false,
      validate: {
        is: constants.nameRegex,
      },
    },
    criticalRange: {
      type: dataTypes.ARRAY(dataTypes.FLOAT),
      allowNull: true,
      validate: {
        rangeOk: u.validateStatusRange,
      },
    },
    warningRange: {
      type: dataTypes.ARRAY(dataTypes.FLOAT),
      allowNull: true,
      validate: {
        rangeOk: u.validateStatusRange,
      },
    },
    infoRange: {
      type: dataTypes.ARRAY(dataTypes.FLOAT),
      allowNull: true,
      validate: {
        rangeOk: u.validateStatusRange,
      },
    },
    okRange: {
      type: dataTypes.ARRAY(dataTypes.FLOAT),
      allowNull: true,
      validate: {
        rangeOk: u.validateStatusRange,
      },
    },
    rank: {
      type: dataTypes.INTEGER,
      allowNull: true,
    },
    timeout: {
      type: dataTypes.STRING(timeoutLength),
      allowNull: false,
      validate: {
        is: timeoutRegex,
      },
    },
    valueLabel: {
      type: dataTypes.STRING(valueLabelLength),
    },
    valueType: {
      type: dataTypes.ENUM('BOOLEAN', 'NUMERIC', 'PERCENT'),
      defaultValue: 'BOOLEAN',
    },
    relatedLinks: {
      type: dataTypes.ARRAY(dataTypes.JSON),
      allowNull: true,
      defaultValue: constants.defaultJsonArrayValue,
      validate: {
        validateJsonSchema(value) {
          common.validateJsonSchema(value);
        },
      },
    },
    tags: {
      type: dataTypes.ARRAY(dataTypes.STRING(constants.fieldlen.normalName)),
      allowNull: true,
      defaultValue: constants.defaultArrayValue,
    },
  }, {
    hooks: {

      /**
       * TODO:
       * 1. Have a look at the sampleStore logic and confirm that it is
       * doing what it is supposed to do.
       *
       */

      /**
       * When an aspect is created. Add its entry in the aspectStore
       * and the sampleStore if any.
       *
       * @param {Aspect} inst - The deleted instance
       * @returns {Promise}
       */
      afterCreate(inst /* , opts */) {
        // Prevent any changes to original inst dataValues object
        const instDataObj = JSON.parse(JSON.stringify(inst.get()));
        return Promise.join(
          redisOps.addKey(aspectType, inst.getDataValue('name')),
          redisOps.hmSet(aspectType, inst.name, instDataObj)
        );
      }, // hooks.afterCreate

      /**
       * Check Generator references and set the isDeleted timestamp.
       *
       * @param {Aspect} inst - The instance being destroyed
       * @returns {Promise}
       */
      beforeDestroy(inst /* , opts */) {
        return inst.checkGeneratorReferences('delete')
        .then(() => common.setIsDeleted(seq.Promise, inst));
      }, // hooks.beforeDestroy

      /**
       * Check Generator references before unpublishing or renaming
       * @param {Aspect} inst - The instance being updated
       * @returns {Promise}
       */
      beforeUpdate(inst /* , opts */) {
        const unpublished = inst.previous('isPublished') && !inst.isPublished;
        const renamed = inst.previous('name') !== inst.name;
        if (unpublished || renamed) {
          const action = unpublished ? 'unpublish' : 'rename';
          return inst.checkGeneratorReferences(action);
        }

        /*
         * If aspect is published and tags change, send a "del" realtime
         * event for all the samples for this aspect. (The afterUpdate hook
         * will send an "add" event.) This way, perspectives
         * which filter by aspect tags will get the right samples.
         */
        if (inst.isPublished && inst.changed('tags')) {
          return new seq.Promise((resolve, reject) => {
            redisOps.getSamplesFromAspectName(inst.name)
            .each((samp) =>
              publishSample(
                samp, seq.models.Subject, sampleEventNames.del,
                seq.models.Aspect
              ))
            .then(() => resolve(inst))
            .catch(reject);
          });
        } // tags changed

        return seq.Promise.resolve(true);
      }, // hooks.beforeUpdate

      /**
       * If isPublished is being updated from true to false or the name of the
       * aspect is changed, delete any samples associated with the aspect.
       *
       * @param {Aspect} inst - The updated instance
       * @returns {Promise}
       */
      afterUpdate(inst /* , opts */) {
        const promiseArr = [];
        const nameChanged = inst.previous('name') !== inst.getDataValue('name');
        const isPublishedChanged =
          inst.previous('isPublished') !== inst.getDataValue('isPublished');

        /*
         * When the sample store feature is enabled do the following
         * 1. if aspect name is changed and it is published, rename the entry
         * on aspectStore and the aspect hash.
         * 2. if the aspect is updated to published, add an entry to the
         * aspectStore and create the aspect hash
         * 3. if the aspect is updated to unpublished, delete the entry in the
         * aspectStore, delete the aspect hash and delete the related samples
         * 4. if the aspect that is updated is already published, update the
         * the aspect with the new values.
        */
        if (nameChanged && inst.isPublished) {
          const newAspName = inst.name;
          const oldAspectName = inst._previousDataValues.name;

          // rename entry in aspectStore
          promiseArr.push(redisOps.renameKey(aspectType,
            oldAspectName, newAspName));

          /*
           * delete multiple possible sample entries in the sample master
           * list of index and the related sample hashes
           */
          promiseArr.push(redisOps.deleteKeys(sampleType, aspectType,
            oldAspectName));
        } else if (isPublishedChanged) {
          // Prevent any changes to original inst dataValues object
          const instDataObj = JSON.parse(JSON.stringify(inst.get()));
          promiseArr.push(redisOps.hmSet(aspectType, inst.name, instDataObj));

          // add the aspect to the aspect master list regardless of isPublished
          promiseArr.push(redisOps.addKey(aspectType, inst.name));
          if (!inst.isPublished) {
            /*
             * Delete multiple possible entries in the sample master list of
             * index
             */
            promiseArr.push(redisOps.deleteKeys(sampleType,
              aspectType, inst.name));
          }
        } else if (inst.isPublished) {
          const instChanged = {};
          Object.keys(inst._changed)
          .filter((key) => inst._changed[key])
          .forEach((key) => {
            instChanged[key] = inst[key];
          });
          promiseArr.push(redisOps.hmSet(aspectType, inst.name, instChanged));
        }

        /*
         * If aspect is published and tags change, send an "add" realtime
         * event for all the samples for this aspect. (The beforeUpdate hook
         * will already have sent a "delete" event.) This way, perspectives
         * which filter by aspect tags will get the right samples.
         */
        if (inst.isPublished && inst.changed('tags')) {
          return new seq.Promise((resolve, reject) => {
            redisOps.getSamplesFromAspectName(inst.name)
            .each((samp) =>
              publishSample(
                samp, seq.models.Subject, sampleEventNames.add,
                seq.models.Aspect
              ))
            .then(() => resolve(inst))
            .catch(reject);
          });
        } // tags changed

        return seq.Promise.all(promiseArr);
      }, // hooks.afterUpdate

      /**
       * When a publihsed aspect is deleted. Delete its entry in the aspectStore
       * and the sampleStore if any.
       *
       * @param {Aspect} inst - The deleted instance
       * @returns {Promise}
       */
      afterDelete(inst /* , opts */) {
        return Promise.join(
          redisOps.deleteKey(aspectType, inst.name),
          redisOps.deleteKeys(sampleType, aspectType, inst.name)
        );
      }, // hooks.afterDelete

      /**
       * Makes sure isUrl/isEmail validations will handle empty strings
       * appropriately.
       *
       * @param {Aspect} inst - The instance being validated
       * @returns {undefined} - OK
       */
      beforeValidate(inst /* , opts */) {
        if (inst.changed('helpUrl') &&
          inst.helpUrl !== null &&
          inst.helpUrl.length === 0) {
          inst.helpUrl = null;
        }

        if (inst.changed('imageUrl') &&
          inst.imageUrl !== null &&
          inst.imageUrl.length === 0) {
          inst.imageUrl = null;
        }

        if (inst.changed('helpEmail') &&
          inst.helpEmail !== null &&
          inst.helpEmail.length === 0) {
          inst.helpEmail = null;
        }
      },

    }, // hooks
    indexes: [
      {
        name: 'AspectUniqueLowercaseNameIsDeleted',
        unique: true,
        fields: [
          seq.fn('lower', seq.col('name')),
          'isDeleted',
        ],
      },
    ],
    paranoid: true,
  });

  /**
   * Class Methods:
   */

  Aspect.getAspectAssociations = function () {
    return assoc;
  };

  Aspect.getProfileAccessField = function () {
    return 'aspectAccess';
  };

  Aspect.postImport = function (models) {
    assoc.user = Aspect.belongsTo(models.User, {
      foreignKey: 'createdBy',
      as: 'user',
    });
    assoc.samples = Aspect.hasMany(models.Sample, {
      as: 'samples',
      foreignKey: 'aspectId',
      hooks: true,
    });
    assoc.writers = Aspect.belongsToMany(models.User, {
      as: 'writers',
      through: 'AspectWriters',
      foreignKey: 'aspectId',
    });

    Aspect.addScope('baseScope', {
      order: ['name'],
    });

    Aspect.addScope('defaultScope', {
      include: [
        {
          association: assoc.user,
          attributes: ['name', 'email', 'fullName'],
        },
      ],
      order: ['name'],
    }, {
      override: true,
    });

    Aspect.addScope('user', {
      include: [
        {
          association: assoc.user,
          attributes: ['name', 'email', 'fullName'],
        },
      ],
    });

    Aspect.addScope('forRealTime', (value) => ({
      where: {
        name: { [Op.iLike]: value },
      },
      include: [
        {
          association: assoc.user,
          attributes: ['name', 'email', 'fullName'],
        },
      ],
    }));
  };

  /**
   * Instance Methods:
   */

  Aspect.prototype.isWritableBy = function (who) {
    return new seq.Promise((resolve /* , reject */) =>
      this.getWriters()
      .then((writers) => {
        if (!writers.length) {
          resolve(true);
        }

        const found = writers.filter((w) =>
          w.name === who || w.id === who);
        resolve(found.length === 1);
      }));
  }; // isWritableBy

  Aspect.prototype.checkGeneratorReferences = function (action) {
    const aspectName = this.previous('name');

    // the aspect names are stored in lowercase on the generator
    const where = { aspects: { [Op.contains]: [aspectName.toLowerCase()] } };
    return seq.models.Generator.findAll({ where })
    .then((gens) => {
      if (gens.length) {
        const genNames = gens.map(g => g.name);
        let usedBy;
        if (gens.length === 1) {
          usedBy = 'a Sample Generator';
        } else {
          usedBy = `${gens.length} Sample Generators`;
        }

        throw new dbErrors.ReferencedByGenerator({
          message: `Cannot ${action} Aspect ${aspectName}. It is ` +
          `currently in use by ${usedBy}: ${genNames}`,
        });
      }
    });
  };

  return Aspect;
};
<commit_msg>send sample del events when aspect is unpublished or deleted (#891)<commit_after>/**
 * Copyright (c) 2016, salesforce.com, inc.
 * All rights reserved.
 * Licensed under the BSD 3-Clause license.
 * For full license text, see LICENSE.txt file in the repo root or
 * https://opensource.org/licenses/BSD-3-Clause
 */

/**
 * db/model/aspect.js
 */

const common = require('../helpers/common');
const u = require('../helpers/aspectUtils');
const constants = require('../constants');
const dbErrors = require('../dbErrors');
const assoc = {};
const timeoutLength = 10;
const timeoutRegex = /^[0-9]{1,9}[SMHDsmhd]$/;
const valueLabelLength = 10;
const redisOps = require('../../cache/redisOps');
const aspectType = redisOps.aspectType;
const sampleType = redisOps.sampleType;
const Promise = require('bluebird');
const Op = require('sequelize').Op;
const publishSample = require('../../realtime/redisPublisher').publishSample;
const sampleEventNames = {
  add: 'refocus.internal.realtime.sample.add',
  upd: 'refocus.internal.realtime.sample.update',
  del: 'refocus.internal.realtime.sample.remove',
};

module.exports = function aspect(seq, dataTypes) {
  const Aspect = seq.define('Aspect', {
    description: {
      type: dataTypes.STRING(constants.fieldlen.longish),
    },
    helpEmail: {
      type: dataTypes.STRING(constants.fieldlen.email),
      validate: { isEmail: true },
    },
    helpUrl: {
      type: dataTypes.STRING(constants.fieldlen.url),
      validate: { isUrl: true },
    },
    id: {
      type: dataTypes.UUID,
      primaryKey: true,
      defaultValue: dataTypes.UUIDV4,
    },
    isDeleted: {
      type: dataTypes.BIGINT,
      defaultValue: 0,
      allowNull: false,
    },
    imageUrl: {
      type: dataTypes.STRING(constants.fieldlen.url),
      validate: { isUrl: true },
    },
    isPublished: {
      type: dataTypes.BOOLEAN,
      allowNull: false,
      defaultValue: false,
    },
    name: {
      type: dataTypes.STRING(constants.fieldlen.normalName),
      allowNull: false,
      validate: {
        is: constants.nameRegex,
      },
    },
    criticalRange: {
      type: dataTypes.ARRAY(dataTypes.FLOAT),
      allowNull: true,
      validate: {
        rangeOk: u.validateStatusRange,
      },
    },
    warningRange: {
      type: dataTypes.ARRAY(dataTypes.FLOAT),
      allowNull: true,
      validate: {
        rangeOk: u.validateStatusRange,
      },
    },
    infoRange: {
      type: dataTypes.ARRAY(dataTypes.FLOAT),
      allowNull: true,
      validate: {
        rangeOk: u.validateStatusRange,
      },
    },
    okRange: {
      type: dataTypes.ARRAY(dataTypes.FLOAT),
      allowNull: true,
      validate: {
        rangeOk: u.validateStatusRange,
      },
    },
    rank: {
      type: dataTypes.INTEGER,
      allowNull: true,
    },
    timeout: {
      type: dataTypes.STRING(timeoutLength),
      allowNull: false,
      validate: {
        is: timeoutRegex,
      },
    },
    valueLabel: {
      type: dataTypes.STRING(valueLabelLength),
    },
    valueType: {
      type: dataTypes.ENUM('BOOLEAN', 'NUMERIC', 'PERCENT'),
      defaultValue: 'BOOLEAN',
    },
    relatedLinks: {
      type: dataTypes.ARRAY(dataTypes.JSON),
      allowNull: true,
      defaultValue: constants.defaultJsonArrayValue,
      validate: {
        validateJsonSchema(value) {
          common.validateJsonSchema(value);
        },
      },
    },
    tags: {
      type: dataTypes.ARRAY(dataTypes.STRING(constants.fieldlen.normalName)),
      allowNull: true,
      defaultValue: constants.defaultArrayValue,
    },
  }, {
    hooks: {

      /**
       * TODO:
       * 1. Have a look at the sampleStore logic and confirm that it is
       * doing what it is supposed to do.
       *
       */

      /**
       * When an aspect is created. Add its entry in the aspectStore
       * and the sampleStore if any.
       *
       * @param {Aspect} inst - The deleted instance
       * @returns {Promise}
       */
      afterCreate(inst /* , opts */) {
        // Prevent any changes to original inst dataValues object
        const instDataObj = JSON.parse(JSON.stringify(inst.get()));
        return Promise.join(
          redisOps.addKey(aspectType, inst.getDataValue('name')),
          redisOps.hmSet(aspectType, inst.name, instDataObj)
        );
      }, // hooks.afterCreate

      /**
       * Check Generator references and set the isDeleted timestamp.
       * If aspect is deleted then send realtime "del" event to delete all
       * samples associated with that aspect for perspectives.
       *
       * @param {Aspect} inst - The instance being destroyed
       * @returns {Promise}
       */
      beforeDestroy(inst /* , opts */) {
        const promiseArr = [];

        promiseArr.push(redisOps.getSamplesFromAspectName(inst.name)
          .each((samp) =>
            publishSample(
              samp, seq.models.Subject, sampleEventNames.del,
              seq.models.Aspect
            )
          )
        );

        promiseArr.push(inst.checkGeneratorReferences('delete')
        .then(() => common.setIsDeleted(seq.Promise, inst)));

        return seq.Promise.all(promiseArr);
      }, // hooks.beforeDestroy

      /**
       * Check Generator references before unpublishing or renaming
       * @param {Aspect} inst - The instance being updated
       * @returns {Promise}
       */
      beforeUpdate(inst /* , opts */) {
        const promiseArr = [];
        const unpublished = inst.previous('isPublished') && !inst.isPublished;
        const renamed = inst.previous('name') !== inst.name;
        if (unpublished || renamed) {
          const action = unpublished ? 'unpublish' : 'rename';
          promiseArr.push(inst.checkGeneratorReferences(action));
        }

        /*
         * If aspect is published and tags change or unpublished,
         * send a "del" realtime event for all the
         * samples for this aspect. (The afterUpdate hook
         * will send an "add" event.) This way, perspectives
         * which filter by aspect tags will get the right samples.
         */
        if (inst.isPublished && inst.changed('tags') ||
          (inst.changed('isPublished') && inst.previous('isPublished'))) {
          promiseArr.push(redisOps.getSamplesFromAspectName(inst.name)
            .each((samp) =>
              publishSample(
                samp, seq.models.Subject, sampleEventNames.del,
                seq.models.Aspect
              )
            )
          );
        } // tags changed

        return seq.Promise.all(promiseArr);
      }, // hooks.beforeUpdate

      /**
       * If isPublished is being updated from true to false or the name of the
       * aspect is changed, delete any samples associated with the aspect.
       *
       * @param {Aspect} inst - The updated instance
       * @returns {Promise}
       */
      afterUpdate(inst /* , opts */) {
        const promiseArr = [];
        const nameChanged = inst.previous('name') !== inst.getDataValue('name');
        const isPublishedChanged =
          inst.previous('isPublished') !== inst.getDataValue('isPublished');

        /*
         * When the sample store feature is enabled do the following
         * 1. if aspect name is changed and it is published, rename the entry
         * on aspectStore and the aspect hash.
         * 2. if the aspect is updated to published, add an entry to the
         * aspectStore and create the aspect hash
         * 3. if the aspect is updated to unpublished, delete the entry in the
         * aspectStore, delete the aspect hash and delete the related samples
         * 4. if the aspect that is updated is already published, update the
         * the aspect with the new values.
        */
        if (nameChanged && inst.isPublished) {
          const newAspName = inst.name;
          const oldAspectName = inst._previousDataValues.name;

          // rename entry in aspectStore
          promiseArr.push(redisOps.renameKey(aspectType,
            oldAspectName, newAspName));

          /*
           * delete multiple possible sample entries in the sample master
           * list of index and the related sample hashes
           */
          promiseArr.push(redisOps.deleteKeys(sampleType, aspectType,
            oldAspectName));
        } else if (isPublishedChanged) {
          // Prevent any changes to original inst dataValues object
          const instDataObj = JSON.parse(JSON.stringify(inst.get()));
          promiseArr.push(redisOps.hmSet(aspectType, inst.name, instDataObj));

          // add the aspect to the aspect master list regardless of isPublished
          promiseArr.push(redisOps.addKey(aspectType, inst.name));
          if (!inst.isPublished) {
            /*
             * Delete multiple possible entries in the sample master list of
             * index
             */
            promiseArr.push(redisOps.deleteKeys(sampleType,
              aspectType, inst.name));
          }
        } else if (inst.isPublished) {
          const instChanged = {};
          Object.keys(inst._changed)
          .filter((key) => inst._changed[key])
          .forEach((key) => {
            instChanged[key] = inst[key];
          });
          promiseArr.push(redisOps.hmSet(aspectType, inst.name, instChanged));
        }

        /*
         * If aspect is published and tags change, send an "add" realtime
         * event for all the samples for this aspect. (The beforeUpdate hook
         * will already have sent a "delete" event.) This way, perspectives
         * which filter by aspect tags will get the right samples.
         */
        if (inst.isPublished && inst.changed('tags')) {
          return new seq.Promise((resolve, reject) => {
            redisOps.getSamplesFromAspectName(inst.name)
            .each((samp) =>
              publishSample(
                samp, seq.models.Subject, sampleEventNames.add,
                seq.models.Aspect
              ))
            .then(() => resolve(inst))
            .catch(reject);
          });
        } // tags changed

        return seq.Promise.all(promiseArr);
      }, // hooks.afterUpdate

      /**
       * When a publihsed aspect is deleted. Delete its entry in the aspectStore
       * and the sampleStore if any.
       *
       * @param {Aspect} inst - The deleted instance
       * @returns {Promise}
       */
      afterDelete(inst /* , opts */) {
        return Promise.join(
          redisOps.deleteKey(aspectType, inst.name),
          redisOps.deleteKeys(sampleType, aspectType, inst.name)
        );
      }, // hooks.afterDelete

      /**
       * Makes sure isUrl/isEmail validations will handle empty strings
       * appropriately.
       *
       * @param {Aspect} inst - The instance being validated
       * @returns {undefined} - OK
       */
      beforeValidate(inst /* , opts */) {
        if (inst.changed('helpUrl') &&
          inst.helpUrl !== null &&
          inst.helpUrl.length === 0) {
          inst.helpUrl = null;
        }

        if (inst.changed('imageUrl') &&
          inst.imageUrl !== null &&
          inst.imageUrl.length === 0) {
          inst.imageUrl = null;
        }

        if (inst.changed('helpEmail') &&
          inst.helpEmail !== null &&
          inst.helpEmail.length === 0) {
          inst.helpEmail = null;
        }
      },

    }, // hooks
    indexes: [
      {
        name: 'AspectUniqueLowercaseNameIsDeleted',
        unique: true,
        fields: [
          seq.fn('lower', seq.col('name')),
          'isDeleted',
        ],
      },
    ],
    paranoid: true,
  });

  /**
   * Class Methods:
   */

  Aspect.getAspectAssociations = function () {
    return assoc;
  };

  Aspect.getProfileAccessField = function () {
    return 'aspectAccess';
  };

  Aspect.postImport = function (models) {
    assoc.user = Aspect.belongsTo(models.User, {
      foreignKey: 'createdBy',
      as: 'user',
    });
    assoc.samples = Aspect.hasMany(models.Sample, {
      as: 'samples',
      foreignKey: 'aspectId',
      hooks: true,
    });
    assoc.writers = Aspect.belongsToMany(models.User, {
      as: 'writers',
      through: 'AspectWriters',
      foreignKey: 'aspectId',
    });

    Aspect.addScope('baseScope', {
      order: ['name'],
    });

    Aspect.addScope('defaultScope', {
      include: [
        {
          association: assoc.user,
          attributes: ['name', 'email', 'fullName'],
        },
      ],
      order: ['name'],
    }, {
      override: true,
    });

    Aspect.addScope('user', {
      include: [
        {
          association: assoc.user,
          attributes: ['name', 'email', 'fullName'],
        },
      ],
    });

    Aspect.addScope('forRealTime', (value) => ({
      where: {
        name: { [Op.iLike]: value },
      },
      include: [
        {
          association: assoc.user,
          attributes: ['name', 'email', 'fullName'],
        },
      ],
    }));
  };

  /**
   * Instance Methods:
   */

  Aspect.prototype.isWritableBy = function (who) {
    return new seq.Promise((resolve /* , reject */) =>
      this.getWriters()
      .then((writers) => {
        if (!writers.length) {
          resolve(true);
        }

        const found = writers.filter((w) =>
          w.name === who || w.id === who);
        resolve(found.length === 1);
      }));
  }; // isWritableBy

  Aspect.prototype.checkGeneratorReferences = function (action) {
    const aspectName = this.previous('name');

    // the aspect names are stored in lowercase on the generator
    const where = { aspects: { [Op.contains]: [aspectName.toLowerCase()] } };
    return seq.models.Generator.findAll({ where })
    .then((gens) => {
      if (gens.length) {
        const genNames = gens.map(g => g.name);
        let usedBy;
        if (gens.length === 1) {
          usedBy = 'a Sample Generator';
        } else {
          usedBy = `${gens.length} Sample Generators`;
        }

        throw new dbErrors.ReferencedByGenerator({
          message: `Cannot ${action} Aspect ${aspectName}. It is ` +
          `currently in use by ${usedBy}: ${genNames}`,
        });
      }
    });
  };

  return Aspect;
};
<|endoftext|>


<commit_before>import React from 'react';
import { Surface, Legend } from 'recharts';

const data = [
  { value: 'Apple', color: '#ff7300' },
  { value: 'Samsung', color: '#bb7300' },
  { value: 'Huawei', color: '#887300' },
  { value: 'Sony', color: '#667300' },
];

const data2 = [
  { value: 'Apple', type: 'circle', color: '#ff7300' },
  { value: 'Samsung', type: 'circle', color: '#bb7300' },
  { value: 'Huawei', type: 'circle', color: '#bb0067' },
  { value: 'Sony', type: 'circle', color: '#167300' },
];

const data3 = [
  { value: 'Apple', type: 'line', color: '#ff7300' },
  { value: 'Samsung', type: 'line', color: '#bb7300' },
  { value: 'Huawei', type: 'line', color: '#bb7300' },
  { value: 'Sony', type: 'line', color: '#ff7812' },
];

export default React.createClass({
  render () {
    return (

          <Legend width={500} height={30} payload={data} />

          <Legend layout='vertical' width={200} height={100} payload={data2} />

          <Legend width={200} height={30} payload={data3} />

    );
  }
});

<commit_msg>Make vertical legend visible in demo<commit_after>import React from 'react';
import { Surface, Legend } from 'recharts';

const data = [
  { value: 'Apple', color: '#ff7300' },
  { value: 'Samsung', color: '#bb7300' },
  { value: 'Huawei', color: '#887300' },
  { value: 'Sony', color: '#667300' },
];

const data2 = [
  { value: 'Apple', type: 'circle', color: '#ff7300' },
  { value: 'Samsung', type: 'circle', color: '#bb7300' },
  { value: 'Huawei', type: 'circle', color: '#bb0067' },
  { value: 'Sony', type: 'circle', color: '#167300' },
];

const data3 = [
  { value: 'Apple', type: 'line', color: '#ff7300' },
  { value: 'Samsung', type: 'line', color: '#bb7300' },
  { value: 'Huawei', type: 'line', color: '#bb7300' },
  { value: 'Sony', type: 'line', color: '#ff7812' },
];

export default React.createClass({
  render() {
    return (

          <Legend width={500} height={30} payload={data} />

          <Legend layout="vertical" width={200} height={100} payload={data2} />

          <Legend width={200} height={30} payload={data3} />

    );
  },
});
<|endoftext|>


<commit_before>var N3 = require('n3');
var media = require('./media.js');				// media types
var ldp = require('./vocab/ldp.js');			// LDP vocabulary
var rdf = require('./vocab/rdf.js');			// RDF vocabulary

exports.serialize = function(triples, callback) {
	var writer = N3.Writer();
	writer.addTriples(triples);
	writer.end(function(err, content) {
		callback(err, media.turtle, content);
	});
};

exports.parse = function(req, resourceURI, callback) {
	var parser = N3.Parser({ documentURI: resourceURI }), triples = [];
	parser.parse(req.rawBody, function(err, triple) {
		if (err) {
			callback(err);
		} else if (triple) {
			triples.push(triple);
		} else {
			// when last triple is null, we're done parsing
			callback(null, triples);
		}
	});
}
<commit_msg>normalize URIs<commit_after>var N3 = require('n3');
var url = require('url');
var path = require('path');
var media = require('./media.js');				// media types
var ldp = require('./vocab/ldp.js');			// LDP vocabulary
var rdf = require('./vocab/rdf.js');			// RDF vocabulary

// normalize paths (remove '.' and '..')
function normalize(urlStr) {
	var urlObj = url.parse(urlStr);
	urlObj.pathname = path.normalize(urlObj.pathname);
	return urlObj.format();
}

exports.serialize = function(triples, callback) {
	var writer = N3.Writer();
	writer.addTriples(triples);
	writer.end(function(err, content) {
		callback(err, media.turtle, content);
	});
};

exports.parse = function(req, resourceURI, callback) {
	var parser = N3.Parser({ documentURI: resourceURI }), triples = [];
	parser.parse(req.rawBody, function(err, triple) {
		if (err) {
			callback(err);
		} else if (triple) {
			triple.subject = normalize(triple.subject);
			if (N3.Util.isUri(triple.object)) {
				triple.object = normalize(triple.object);
			}
			triples.push(triple);
		} else {
			// when last triple is null, we're done parsing
			callback(null, triples);
		}
	});
}
<|endoftext|>


<commit_before>/*jshint undef: true, unused: true, browser: true, curly: true*/
/*global Ext, Prism*/
Ext.define('Site.page.Docs', {
    singleton: true,
    requires: [
        'Ext.util.Collection',
        'Ext.String',
        'Ext.XTemplate',
        'Ext.Ajax'
    ],

    config: {
        currentTocItem: null
    },

    requestTpl: [
        '
',
            'Request:',
            ' {method:uppercase}',
            ' {[encodeURI(values.url)]}',
            '<tpl if="Ext.Object.getSize(params.query)">',
                '?{[Ext.Object.toQueryString(values.params.query)]}',
            '</tpl>',
        '
',

        '
',
        '<tpl foreach="headers">',
            '
',
                '
{$}
',
                '
{.}
',
            '
',
        '</tpl>',
        '
'
    ],

    responseTpl: [
        '
Response: {status_code} {status_reason}
',

        '
',
        '<tpl foreach="headers">',
            '
',
                '
{$}
',
                '
{.}
',
            '
',
        '</tpl>',
        '
',

        '
Response Body
',
        '
{body}
'
    ],

    pathParamRe: /\{(\w+)\}/,
    numberTypeRe: /number|integer/,

    httpErrorReasons: {
        400: 'Bad Request',
        401: 'Unauthorized',
        402: 'Payment Required',
        403: 'Forbidden',
        404: 'Not Found',
        405: 'Method Not Allowed',
        406: 'Not Acceptable',
        407: 'Proxy Authentication Required',
        408: 'Request Timeout',
        409: 'Conflict',
        410: 'Gone',
        411: 'Length Required',
        412: 'Precondition Failed',
        413: 'Payload Too Large',
        414: 'URI Too Long',
        415: 'Unsupported Media Type',
        416: 'Range Not Satisfiable',
        417: 'Expectation Failed',
        422: 'Unprocessable Entity',
        423: 'Locked',
        424: 'Failed Dependency',
        425: 'Unassigned',
        426: 'Upgrade Required',
        427: 'Unassigned',
        428: 'Precondition Required',
        429: 'Too Many Requests',
        430: 'Unassigned',
        431: 'Request Header Fields Too Large',
        500: 'Internal Server Error',
        501: 'Not Implemented',
        502: 'Bad Gateway',
        503: 'Service Unavailable',
        504: 'Gateway Timeout',
        505: 'HTTP Version Not Supported',
        506: 'Variant Also Negotiates',
        507: 'Insufficient Storage',
        508: 'Loop Detected',
        509: 'Unassigned',
        510: 'Not Extended',
        511: 'Network Authentication Required'
    },

    // template methods
    constructor: function() {
        Ext.onReady(this.onDocReady, this);
    },

    // config handlers
    updateCurrentTocItem: function(currentToc, oldCurrentToc) {
        if (oldCurrentToc) {
            oldCurrentToc.linkEl.removeCls('current');
        }

        if (currentToc) {
            currentToc.linkEl.addCls('current');
        }
    },

    // event handlers
    onDocReady: function() {
        var me = this,
            docsCt;

        // grab references to key elements
        me.docsCt = docsCt = Ext.getBody().down('.endpoint-docs');

        // grab key settings from DOM
        me.apiSchemes = docsCt.getAttribute('data-schemes').split(',');
        me.apiHost = docsCt.getAttribute('data-host');
        me.apiBasePath = docsCt.getAttribute('data-basepath');
        me.apiHandle = docsCt.getAttribute('data-handle');
        me.apiKeyRequired = docsCt.dom.getAttribute('data-key-required') !== null;

        // initialize  features
        me.initializeToc();
        me.initializeTryItOut();
        me.initializeSubscribe();

        Ext.get(document).on('scroll', 'onDocumentScroll', me);
    },

    onDocumentScroll: function(ev, t) {
        this.syncTocFromScroll();
    },

    // public methods
    initializeToc: function() {
        var me = this,
            toc = me.toc = new Ext.util.Collection(),
            tocTops = me.tocTops = [];

        Ext.getBody().down('.docs-toc').select('a[href^="#"]').each(function(linkEl) {
            var targetId = linkEl.dom.hash.substr(1),
                targetEl = Ext.get(targetId),
                targetTop = targetEl && targetEl.getTop();

            if (!targetEl) {
                return;
            }

            toc.add({
                id: targetId,
                linkEl: Ext.get(linkEl),
                targetEl: targetEl,
                targetTop: targetTop
            });

            tocTops.push(targetTop);
        });

        me.syncTocFromScroll();
    },

    initializeTryItOut: function() {
        var me = this,
            requestTpl = Ext.XTemplate.getTpl(me, 'requestTpl'),
            responseTpl = Ext.XTemplate.getTpl(me, 'responseTpl'),
            keysCt = Ext.get('keys'),
            paramCollectionSeperators = {
                csv: ',',
                ssv: ' ',
                tsv: '\t',
                pipes: '|'
            },
            paramInputTypes = {
                number: 'number',
                integer: 'number',
                string: 'text',
                file: 'file',
                'boolean': 'checkbox'
            };

        // wire test consoles
        Ext.select('.endpoint-path-method', true).each(function(pathMethodEl) {
            var btn = pathMethodEl.appendChild({
                    tag: 'button',
                    html: 'Try it out!'
                }),
                method = pathMethodEl.getAttribute('data-method'),
                path = pathMethodEl.up('.endpoint-path').getAttribute('data-path'),
                requestCt = pathMethodEl.appendChild({
                    cls: 'request-container'
                }),
                responseCt = pathMethodEl.appendChild({
                    cls: 'response-container'
                }),
                tableEl = pathMethodEl.down('.parameters-table');

            tableEl.down('thead th').insertSibling({tag: 'th', html: 'Value'}, 'after');

            tableEl.select('tbody > tr').each(function(rowEl) {
                var type = rowEl.getAttribute('data-type'),
                    inputConfig = {
                        tag: 'input',
                        cls: 'field-control',
                        type: paramInputTypes[type]
                    },
                    collectionFormat;

                if (type === 'array') {
                    // how will we differentiate between types of the items, they take the same arguments as the parameter type. it may make more sense to allow you to [+] and [-] objects
                    // we could run the same validation routines possibly.
                    inputConfig.placeholder = ['item1', 'item2', '...'].join((paramCollectionSeperators[rowEl.getAttribute('data-collectionformat') || 'csv']));
                }

                rowEl.down('td').insertSibling({ tag: 'td', cn: [inputConfig] }, 'after');
            });

            btn.on('click', function (ev, t) {
                var checkedKeyEl = keysCt.down('input[name=primary-key]:checked'),
                    parameters = {
                        path: {},
                        query: {}
                    },
                    url = me.apiSchemes[0] + '://' + me.apiHost + me.apiBasePath,
                    headers = {},
                    language = '',
                    firstErrorEl;

                // check API key and set header
                if (me.apiKeyRequired && !checkedKeyEl) {
                    window.alert('An API key is required for making calls to this API. Please request one in the API Keys section above');
                    window.location = '#keys';
                    return;
                } else if (checkedKeyEl) {
                    headers.Authorization = 'Gatekeeper-Key ' + checkedKeyEl.getValue();
                }

                // validate fields
                pathMethodEl.select('.parameters-table tbody > tr').each(function(rowEl) {
                    var inputEl = rowEl.down('input'),
                        isEmpty = (inputEl.dom.value === ''),
                        paramIn = rowEl.getAttribute('data-in'),
                        val;

                    if (isEmpty && (rowEl.getAttribute('data-required') || paramIn === 'path')) {
                        inputEl.addCls('invalid');

                        if (!firstErrorEl) {
                            firstErrorEl = Ext.get(inputEl);
                        }
                    } else if (!isEmpty) {
                        val = inputEl.dom.value;

                        if (rowEl.getAttribute('data-type') === 'boolean') {
                            val = inputEl.dom.checked ? 'true': 'false';
                        }

                        parameters[paramIn][rowEl.getAttribute('data-name')] = val;
                    }

                    // FIXME: move this somewhere or add single: true so that listeners aren't duplicated each time btn is clicked
                    inputEl.on('input', function(ev, t){
                        Ext.fly(t).removeCls('invalid');
                    });
                });

                if (firstErrorEl) {
                    firstErrorEl.dom.focus();

                    // FIXME: move this somewhere so it doesn't get added multiple times
                    pathMethodEl.on('blur', function(ev, t) {
                        Ext.fly(t).toggleCls('invalid', t.value === '');
                    }, null, { delegate: 'input.invalid' });

                    return;
                }

                url += me.populatePlaceholders(path, parameters.path);

                // render request info
                requestTpl.overwrite(requestCt, {
                    method: method,
                    url: url,
                    params: parameters,
                    headers: headers
                });

                // make request
                Ext.Ajax.request({
                    method: method,
                    url: url,
                    params: parameters.query,
                    disableCaching: false,
                    headers: headers,
                    callback: function(options, success, response) {
                        var headers = response.getAllResponseHeaders(),
                            body = response.responseText;

                        if (/(x|ht)ml/.test(headers['content-type'])) {
                            language = 'markup';
                            body = me.prettyPrintXML(body);
                        } else if (headers['content-type'].indexOf('json') !== -1) {
                            try {
                                body = JSON.stringify(JSON.parse(body), null, '   ');
                                language = 'javascript';
                            } catch(e) {
                                body = response.responseText;
                            }
                        }

                        if (language !== '') {
                            body = Prism.highlight(body, Prism.languages[language]);
                        } else {
                            body = Ext.util.Format.htmlEncode(body);
                        }

                        responseTpl.overwrite(responseCt, {
                            body: body,
                            headers: headers,
                            language: language,
                            status_code: response.status,
                            status_reason: success ? (response.statusText || 'OK') : (response.statusText || me.httpErrorReasons[response.status] || 'Error')
                        });
                    }
                });
            });
        });
    },

    initializeSubscribe: function() {
        var me = this;

        Ext.select('.subscribe.toggle', true).each(function(toggleEl) {
            var inputEl = toggleEl.down('input');

            inputEl.on('change', function(ev, inputDom) {
                var subscribe = inputDom.checked;

                toggleEl[subscribe ? 'addCls' : 'removeCls']('off-to-on');
                toggleEl[subscribe ? 'removeCls' : 'addCls']('on-to-off');

                Ext.Ajax.request({
                    method: subscribe ? 'POST' : 'DELETE',
                    url: '/subscriptions/' + me.apiHandle,
                    headers: {
                        Accept: 'application/json'
                    },
                    success: function(response) {
                        var r = Ext.decode(response.responseText);

                        inputDom.checked = !!r.data;
                        toggleEl.removeCls(['off-to-on', 'on-to-off']);
                    },
                    failure: function(response) {
                        if (response.status == 401) {
                            location.href = '/login?return=' + encodeURIComponent(location.pathname + location.search);
                        } else {
                            window.alert('Failed to subscribe, please try again later');
                        }
                    }
                });
            });
        });
    },

    getTocFromY: function(y) {
        var me = this,
            tocTops = me.tocTops,
            i = 1, tocLen = tocTops.length, tocTop;

        for (; i < tocLen; i++) {
            tocTop = tocTops[i];
            if (y < tocTop - 25) {
                return me.toc.getAt(i - 1);
            }
        }

        return null;
    },

    syncTocFromScroll: function() {
        var me = this,
            toc = me.getTocFromY(Ext.getBody().getScroll().top);

        me.setCurrentTocItem(toc);
    },

    populatePlaceholders: function (template, values) {
        var pathParamRe = this.pathParamRe,
            match;

        while ((match = pathParamRe.exec(template)) !== null) {
            template = template.replace(match[0], values[match[1]]);
        }

        return template;
    },

    prettyPrintXML: function formatXml(xml) {
        var formatted = '',
            reg = /(>)(<)(\/*)/g,
            pad = 0,
            nodes;

        xml = xml.toString().replace(reg, '$1\r\n$2$3');
        nodes = xml.split('\r\n');

        nodes.forEach(function(node) {
            var indent = 0,
                pad;

            if (node.match(/.+<\/\w[^>]*>$/)) {
                indent = 0;
            } else if (node.match(/^<\/\w/)) {
                if (pad !== 0) {
                    pad -= 1;
                }
            } else if (node.match(/^<\w[^>]*[^\/]>.*$/)) {
                indent = 1;
            } else {
                indent = 0;
            }

            formatted += Ext.String.repeat('   ', pad) + node + '\r\n';
            pad += indent;
        });

        return formatted;
    }
});<commit_msg>wrapped console element in container and added loading class<commit_after>/*jshint undef: true, unused: true, browser: true, curly: true*/
/*global Ext, Prism*/
Ext.define('Site.page.Docs', {
    singleton: true,
    requires: [
        'Ext.util.Collection',
        'Ext.String',
        'Ext.XTemplate',
        'Ext.Ajax'
    ],

    config: {
        currentTocItem: null
    },

    requestTpl: [
        '
',
            'Request:',
            ' {method:uppercase}',
            ' {[encodeURI(values.url)]}',
            '<tpl if="Ext.Object.getSize(params.query)">',
                '?{[Ext.Object.toQueryString(values.params.query)]}',
            '</tpl>',
        '
',

        '
',
        '<tpl foreach="headers">',
            '
',
                '
{$}
',
                '
{.}
',
            '
',
        '</tpl>',
        '
'
    ],

    responseTpl: [
        '
Response: {status_code} {status_reason}
',

        '
',
        '<tpl foreach="headers">',
            '
',
                '
{$}
',
                '
{.}
',
            '
',
        '</tpl>',
        '
',

        '
Response Body
',
        '
{body}
'
    ],

    pathParamRe: /\{(\w+)\}/,
    numberTypeRe: /number|integer/,

    httpErrorReasons: {
        400: 'Bad Request',
        401: 'Unauthorized',
        402: 'Payment Required',
        403: 'Forbidden',
        404: 'Not Found',
        405: 'Method Not Allowed',
        406: 'Not Acceptable',
        407: 'Proxy Authentication Required',
        408: 'Request Timeout',
        409: 'Conflict',
        410: 'Gone',
        411: 'Length Required',
        412: 'Precondition Failed',
        413: 'Payload Too Large',
        414: 'URI Too Long',
        415: 'Unsupported Media Type',
        416: 'Range Not Satisfiable',
        417: 'Expectation Failed',
        422: 'Unprocessable Entity',
        423: 'Locked',
        424: 'Failed Dependency',
        425: 'Unassigned',
        426: 'Upgrade Required',
        427: 'Unassigned',
        428: 'Precondition Required',
        429: 'Too Many Requests',
        430: 'Unassigned',
        431: 'Request Header Fields Too Large',
        500: 'Internal Server Error',
        501: 'Not Implemented',
        502: 'Bad Gateway',
        503: 'Service Unavailable',
        504: 'Gateway Timeout',
        505: 'HTTP Version Not Supported',
        506: 'Variant Also Negotiates',
        507: 'Insufficient Storage',
        508: 'Loop Detected',
        509: 'Unassigned',
        510: 'Not Extended',
        511: 'Network Authentication Required'
    },

    // template methods
    constructor: function() {
        Ext.onReady(this.onDocReady, this);
    },

    // config handlers
    updateCurrentTocItem: function(currentToc, oldCurrentToc) {
        if (oldCurrentToc) {
            oldCurrentToc.linkEl.removeCls('current');
        }

        if (currentToc) {
            currentToc.linkEl.addCls('current');
        }
    },

    // event handlers
    onDocReady: function() {
        var me = this,
            docsCt;

        // grab references to key elements
        me.docsCt = docsCt = Ext.getBody().down('.endpoint-docs');

        // grab key settings from DOM
        me.apiSchemes = docsCt.getAttribute('data-schemes').split(',');
        me.apiHost = docsCt.getAttribute('data-host');
        me.apiBasePath = docsCt.getAttribute('data-basepath');
        me.apiHandle = docsCt.getAttribute('data-handle');
        me.apiKeyRequired = docsCt.dom.getAttribute('data-key-required') !== null;

        // initialize  features
        me.initializeToc();
        me.initializeTryItOut();
        me.initializeSubscribe();

        Ext.get(document).on('scroll', 'onDocumentScroll', me);
    },

    onDocumentScroll: function(ev, t) {
        this.syncTocFromScroll();
    },

    // public methods
    initializeToc: function() {
        var me = this,
            toc = me.toc = new Ext.util.Collection(),
            tocTops = me.tocTops = [];

        Ext.getBody().down('.docs-toc').select('a[href^="#"]').each(function(linkEl) {
            var targetId = linkEl.dom.hash.substr(1),
                targetEl = Ext.get(targetId),
                targetTop = targetEl && targetEl.getTop();

            if (!targetEl) {
                return;
            }

            toc.add({
                id: targetId,
                linkEl: Ext.get(linkEl),
                targetEl: targetEl,
                targetTop: targetTop
            });

            tocTops.push(targetTop);
        });

        me.syncTocFromScroll();
    },

    initializeTryItOut: function() {
        var me = this,
            requestTpl = Ext.XTemplate.getTpl(me, 'requestTpl'),
            responseTpl = Ext.XTemplate.getTpl(me, 'responseTpl'),
            keysCt = Ext.get('keys'),
            paramCollectionSeperators = {
                csv: ',',
                ssv: ' ',
                tsv: '\t',
                pipes: '|'
            },
            paramInputTypes = {
                number: 'number',
                integer: 'number',
                string: 'text',
                file: 'file',
                'boolean': 'checkbox'
            };

        // wire test consoles
        Ext.select('.endpoint-path-method', true).each(function(pathMethodEl) {
            var consoleCt = pathMethodEl.appendChild({
                    cls: 'console-ct'
                }),

                btn = consoleCt.appendChild({
                    tag: 'button',
                    html: 'Try it out!'
                }),

                loader = consoleCt.appendChild({
                    tag: 'span',
                    cls: 'loading-indicator',
                    html: 'Loading…'
                }),

                method = pathMethodEl.getAttribute('data-method'),
                path = pathMethodEl.up('.endpoint-path').getAttribute('data-path'),
                requestCt = consoleCt.appendChild({
                    cls: 'request-ct'
                }),
                responseCt = consoleCt.appendChild({
                    cls: 'response-ct'
                }),
                tableEl = pathMethodEl.down('.parameters-table');

            tableEl.down('thead th').insertSibling({tag: 'th', html: 'Value'}, 'after');

            tableEl.select('tbody > tr').each(function(rowEl) {
                var type = rowEl.getAttribute('data-type'),
                    inputConfig = {
                        tag: 'input',
                        cls: 'field-control',
                        type: paramInputTypes[type]
                    },
                    collectionFormat;

                if (type === 'array') {
                    // how will we differentiate between types of the items, they take the same arguments as the parameter type. it may make more sense to allow you to [+] and [-] objects
                    // we could run the same validation routines possibly.
                    inputConfig.placeholder = ['item1', 'item2', '...'].join((paramCollectionSeperators[rowEl.getAttribute('data-collectionformat') || 'csv']));
                }

                rowEl.down('td').insertSibling({ tag: 'td', cn: [inputConfig] }, 'after');
            });

            btn.on('click', function (ev, t) {
                var checkedKeyEl = keysCt.down('input[name=primary-key]:checked'),
                    parameters = {
                        path: {},
                        query: {}
                    },
                    url = me.apiSchemes[0] + '://' + me.apiHost + me.apiBasePath,
                    headers = {},
                    language = '',
                    firstErrorEl;

                // check API key and set header
                if (me.apiKeyRequired && !checkedKeyEl) {
                    window.alert('An API key is required for making calls to this API. Please request one in the API Keys section above');
                    window.location = '#keys';
                    return;
                } else if (checkedKeyEl) {
                    headers.Authorization = 'Gatekeeper-Key ' + checkedKeyEl.getValue();
                }

                // validate fields
                pathMethodEl.select('.parameters-table tbody > tr').each(function(rowEl) {
                    var inputEl = rowEl.down('input'),
                        isEmpty = (inputEl.dom.value === ''),
                        paramIn = rowEl.getAttribute('data-in'),
                        val;

                    if (isEmpty && (rowEl.getAttribute('data-required') || paramIn === 'path')) {
                        inputEl.addCls('invalid');

                        if (!firstErrorEl) {
                            firstErrorEl = Ext.get(inputEl);
                        }
                    } else if (!isEmpty) {
                        val = inputEl.dom.value;

                        if (rowEl.getAttribute('data-type') === 'boolean') {
                            val = inputEl.dom.checked ? 'true': 'false';
                        }

                        parameters[paramIn][rowEl.getAttribute('data-name')] = val;
                    }

                    // FIXME: move this somewhere or add single: true so that listeners aren't duplicated each time btn is clicked
                    inputEl.on('input', function(ev, t){
                        Ext.fly(t).removeCls('invalid');
                    });
                });

                if (firstErrorEl) {
                    firstErrorEl.dom.focus();

                    // FIXME: move this somewhere so it doesn't get added multiple times
                    pathMethodEl.on('blur', function(ev, t) {
                        Ext.fly(t).toggleCls('invalid', t.value === '');
                    }, null, { delegate: 'input.invalid' });

                    return;
                }

                consoleCt.addCls('is-loading');

                url += me.populatePlaceholders(path, parameters.path);

                // render request info
                requestTpl.overwrite(requestCt, {
                    method: method,
                    url: url,
                    params: parameters,
                    headers: headers
                });

                // make request
                Ext.Ajax.request({
                    method: method,
                    url: url,
                    params: parameters.query,
                    disableCaching: false,
                    headers: headers,
                    callback: function(options, success, response) {
                        var headers = response.getAllResponseHeaders(),
                            body = response.responseText;

                        if (/(x|ht)ml/.test(headers['content-type'])) {
                            language = 'markup';
                            body = me.prettyPrintXML(body);
                        } else if (headers['content-type'].indexOf('json') !== -1) {
                            try {
                                body = JSON.stringify(JSON.parse(body), null, '   ');
                                language = 'javascript';
                            } catch(e) {
                                body = response.responseText;
                            }
                        }

                        if (language !== '') {
                            body = Prism.highlight(body, Prism.languages[language]);
                        } else {
                            body = Ext.util.Format.htmlEncode(body);
                        }

                        responseTpl.overwrite(responseCt, {
                            body: body,
                            headers: headers,
                            language: language,
                            status_code: response.status,
                            status_reason: success ? (response.statusText || 'OK') : (response.statusText || me.httpErrorReasons[response.status] || 'Error')
                        });

                        consoleCt.removeCls('is-loading');
                    }
                });
            });
        });
    },

    initializeSubscribe: function() {
        var me = this;

        Ext.select('.subscribe.toggle', true).each(function(toggleEl) {
            var inputEl = toggleEl.down('input');

            inputEl.on('change', function(ev, inputDom) {
                var subscribe = inputDom.checked;

                toggleEl[subscribe ? 'addCls' : 'removeCls']('off-to-on');
                toggleEl[subscribe ? 'removeCls' : 'addCls']('on-to-off');

                Ext.Ajax.request({
                    method: subscribe ? 'POST' : 'DELETE',
                    url: '/subscriptions/' + me.apiHandle,
                    headers: {
                        Accept: 'application/json'
                    },
                    success: function(response) {
                        var r = Ext.decode(response.responseText);

                        inputDom.checked = !!r.data;
                        toggleEl.removeCls(['off-to-on', 'on-to-off']);
                    },
                    failure: function(response) {
                        if (response.status == 401) {
                            location.href = '/login?return=' + encodeURIComponent(location.pathname + location.search);
                        } else {
                            window.alert('Failed to subscribe, please try again later');
                        }
                    }
                });
            });
        });
    },

    getTocFromY: function(y) {
        var me = this,
            tocTops = me.tocTops,
            i = 1, tocLen = tocTops.length, tocTop;

        for (; i < tocLen; i++) {
            tocTop = tocTops[i];
            if (y < tocTop - 25) {
                return me.toc.getAt(i - 1);
            }
        }

        return null;
    },

    syncTocFromScroll: function() {
        var me = this,
            toc = me.getTocFromY(Ext.getBody().getScroll().top);

        me.setCurrentTocItem(toc);
    },

    populatePlaceholders: function (template, values) {
        var pathParamRe = this.pathParamRe,
            match;

        while ((match = pathParamRe.exec(template)) !== null) {
            template = template.replace(match[0], values[match[1]]);
        }

        return template;
    },

    prettyPrintXML: function formatXml(xml) {
        var formatted = '',
            reg = /(>)(<)(\/*)/g,
            pad = 0,
            nodes;

        xml = xml.toString().replace(reg, '$1\r\n$2$3');
        nodes = xml.split('\r\n');

        nodes.forEach(function(node) {
            var indent = 0,
                pad;

            if (node.match(/.+<\/\w[^>]*>$/)) {
                indent = 0;
            } else if (node.match(/^<\/\w/)) {
                if (pad !== 0) {
                    pad -= 1;
                }
            } else if (node.match(/^<\w[^>]*[^\/]>.*$/)) {
                indent = 1;
            } else {
                indent = 0;
            }

            formatted += Ext.String.repeat('   ', pad) + node + '\r\n';
            pad += indent;
        });

        return formatted;
    }
});<|endoftext|>


<commit_before>/**
 * Copyright (c) 2017-present, Wonday (@wonday.org)
 * All rights reserved.
 *
 * This source code is licensed under the MIT-style license found in the
 * LICENSE file in the root directory of this source tree.
 */

'use strict';
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {
    View,
    StyleSheet,
    PanResponder,
    ViewPropTypes,
} from 'react-native';

export default class PinchZoomView extends Component {

    static propTypes = {
        ...ViewPropTypes,
        scalable: PropTypes.bool,
        onScaleChanged: PropTypes.func,
    };

    static defaultProps = {
        scalable: true,
        onScaleChanged: (scale) => {
        },
    };

    constructor(props) {

        super(props);
        this.state = {};
        this.distant = 0;

    }

    componentDidMount() {

        this.gestureHandlers = PanResponder.create({
            onStartShouldSetPanResponder: this._handleStartShouldSetPanResponder,
            onMoveShouldSetResponderCapture: (evt, gestureState) => (true),
            onMoveShouldSetPanResponder: this._handleMoveShouldSetPanResponder,
            onPanResponderGrant: this._handlePanResponderGrant,
            onPanResponderMove: this._handlePanResponderMove,
            onPanResponderRelease: this._handlePanResponderEnd,
            onPanResponderTerminationRequest: evt => false,
            onPanResponderTerminate: this._handlePanResponderTerminate,
            onShouldBlockNativeResponder: evt => true
        });

    }

    _handleStartShouldSetPanResponder = (e, gestureState) => {

        // don't respond to single touch to avoid shielding click on child components
        return false;

    };

    _handleMoveShouldSetPanResponder = (e, gestureState) => {

        return this.props.scalable && (e.nativeEvent.changedTouches.length >= 2 || gestureState.numberActiveTouches >= 2);

    };

    _handlePanResponderGrant = (e, gestureState) => {

        if (e.nativeEvent.changedTouches.length >= 2 || gestureState.numberActiveTouches >= 2) {
            let dx = Math.abs(e.nativeEvent.touches[0].pageX - e.nativeEvent.touches[1].pageX);
            let dy = Math.abs(e.nativeEvent.touches[0].pageY - e.nativeEvent.touches[1].pageY);
            this.distant = Math.sqrt(dx * dx + dy * dy);
        }

    };

    _handlePanResponderEnd = (e, gestureState) => {

        this.distant = 0;

    };

    _handlePanResponderTerminate = (e, gestureState) => {

        this.distant = 0;

    };

    _handlePanResponderMove = (e, gestureState) => {

        if ((e.nativeEvent.changedTouches.length >= 2 || gestureState.numberActiveTouches >= 2) && this.distant > 100) {

            let dx = Math.abs(e.nativeEvent.touches[0].pageX - e.nativeEvent.touches[1].pageX);
            let dy = Math.abs(e.nativeEvent.touches[0].pageY - e.nativeEvent.touches[1].pageY);
            let distant = Math.sqrt(dx * dx + dy * dy);
            let scale = (distant / this.distant);
            let pageX = (e.nativeEvent.touches[0].pageX + e.nativeEvent.touches[1].pageX) / 2;
            let pageY = (e.nativeEvent.touches[0].pageY + e.nativeEvent.touches[1].pageY) / 2;
            let pinchInfo = {scale: scale, pageX: pageX, pageY: pageY};

            this.props.onScaleChanged(pinchInfo);
            this.distant = distant;

        }

    };

    render() {

        return (
            <View
                {...this.props}
                {...this.gestureHandlers.panHandlers}
                style={[styles.container, this.props.style]}>
                {this.props.children}
            </View>
        );

    }
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center'
    }
});
<commit_msg>Fix #437 TypeError: undefined is not an object, crashing on ios<commit_after>/**
 * Copyright (c) 2017-present, Wonday (@wonday.org)
 * All rights reserved.
 *
 * This source code is licensed under the MIT-style license found in the
 * LICENSE file in the root directory of this source tree.
 */

'use strict';
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {
    View,
    StyleSheet,
    PanResponder,
    ViewPropTypes,
} from 'react-native';

export default class PinchZoomView extends Component {

    static propTypes = {
        ...ViewPropTypes,
        scalable: PropTypes.bool,
        onScaleChanged: PropTypes.func,
    };

    static defaultProps = {
        scalable: true,
        onScaleChanged: (scale) => {
        },
    };

    constructor(props) {

        super(props);
        this.state = {};
        this.distant = 0;
        this.gestureHandlers = PanResponder.create({
            onStartShouldSetPanResponder: this._handleStartShouldSetPanResponder,
            onMoveShouldSetResponderCapture: (evt, gestureState) => (true),
            onMoveShouldSetPanResponder: this._handleMoveShouldSetPanResponder,
            onPanResponderGrant: this._handlePanResponderGrant,
            onPanResponderMove: this._handlePanResponderMove,
            onPanResponderRelease: this._handlePanResponderEnd,
            onPanResponderTerminationRequest: evt => false,
            onPanResponderTerminate: this._handlePanResponderTerminate,
            onShouldBlockNativeResponder: evt => true
        });

    }

    _handleStartShouldSetPanResponder = (e, gestureState) => {

        // don't respond to single touch to avoid shielding click on child components
        return false;

    };

    _handleMoveShouldSetPanResponder = (e, gestureState) => {

        return this.props.scalable && (e.nativeEvent.changedTouches.length >= 2 || gestureState.numberActiveTouches >= 2);

    };

    _handlePanResponderGrant = (e, gestureState) => {

        if (e.nativeEvent.changedTouches.length >= 2 || gestureState.numberActiveTouches >= 2) {
            let dx = Math.abs(e.nativeEvent.touches[0].pageX - e.nativeEvent.touches[1].pageX);
            let dy = Math.abs(e.nativeEvent.touches[0].pageY - e.nativeEvent.touches[1].pageY);
            this.distant = Math.sqrt(dx * dx + dy * dy);
        }

    };

    _handlePanResponderEnd = (e, gestureState) => {

        this.distant = 0;

    };

    _handlePanResponderTerminate = (e, gestureState) => {

        this.distant = 0;

    };

    _handlePanResponderMove = (e, gestureState) => {

        if ((e.nativeEvent.changedTouches.length >= 2 || gestureState.numberActiveTouches >= 2) && this.distant > 100) {

            let dx = Math.abs(e.nativeEvent.touches[0].pageX - e.nativeEvent.touches[1].pageX);
            let dy = Math.abs(e.nativeEvent.touches[0].pageY - e.nativeEvent.touches[1].pageY);
            let distant = Math.sqrt(dx * dx + dy * dy);
            let scale = (distant / this.distant);
            let pageX = (e.nativeEvent.touches[0].pageX + e.nativeEvent.touches[1].pageX) / 2;
            let pageY = (e.nativeEvent.touches[0].pageY + e.nativeEvent.touches[1].pageY) / 2;
            let pinchInfo = {scale: scale, pageX: pageX, pageY: pageY};

            this.props.onScaleChanged(pinchInfo);
            this.distant = distant;

        }

    };

    render() {

        return (
            <View
                {...this.props}
                {...this.gestureHandlers.panHandlers}
                style={[styles.container, this.props.style]}>
                {this.props.children}
            </View>
        );

    }
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center'
    }
});
<|endoftext|>