<commit_before>var there_is_an_issue = false;
var what_are_the_issue = "";

$(".dispenser").each(function(){
  var filling = parseInt($(this).children('.filling_dispenser').text());
  var battery = parseInt($(this).children('.battery_dispenser').text());
  if(filling < 20 || battery < 20){
    $(this).css('background-color','orange');
    if (filling < 5) {
      what_are_the_issue += "Dispenser "+ $(this).children('.id_dispenser').text() + " is nearly empty \n";
      $(this).css('background-color','red');
      there_is_an_issue = true;

    } else if (battery < 2) {
      what_are_the_issue += "Dispenser "+ $(this).children('.id_dispenser').text() + " battery is nearly empty\n";
      $(this).css('background-color','red');
      there_is_an_issue = true;
    }
  }
});

if (there_is_an_issue){
  window.alert(what_are_the_issue);
}

var connection = new WebSocket('ws://192.168.50.1:9876');
$("button").click(function() {
  connection.onopen = function () {
    var data_send = "ask_dispenser" + $(this).children(".id_dispenser").text();
    connection.send(data_send); // Send the message 'Ping' to the server
    console.log(data_send);

    var io = require('socket.io').listen(80);
    io.sockets.on('connection', function (socket) {
      socket.on('join', function (data) {
        socket.join(data.email); // We are using room of socket io
      });
    });
  };
});
<commit_msg><commit_after>var there_is_an_issue = false;
var what_are_the_issue = "";

$(".dispenser").each(parse_button($(this)));

if (there_is_an_issue){
  window.alert(what_are_the_issue);
}

var connection = new WebSocket('ws://192.168.50.1:9876');
$(".dispenser").click(function() {
  console.log("bouton");
  connection.onopen = function () {
    var data_send = new Object();
    data_send.data.type = "ask_dispenser";
    data_send.data.id = "" + $(this).children(".id_dispenser").text();
    var string = JSON.stringify(data_send);
    connection.send(string); // Send the message 'Ping' to the server
    console.log(string);
  };
});

connection.onmessage(function(event) {
  var id_receive = "";
  var filling_receive = "";
  var battery_receive = "";
  var last_wash_date = "";
  var dirty_dispenser = "";
  var sleep_time = "";
  var wake_up_time = "";
  var cycle_time = "";
  var msg = JSON.parse(event.data);
  switch (msg.type) {
    case "detail_answer":
    id_receive += msg.id;
    filling_receive += msg.filling;
    battery_receive += msg.battery;
    last_wash_date += msg.date;
    setDetailWindow(id_receive, filling_receive, battery_receive, last_wash_date);
    break;
    case "update":
    id_receive += msg.id;
    filling_receive += msg.filling;
    battery_receive += msg.battery;
    setUpdateButton(id_receive, filling_receive, battery_receive);
    break;
    case "dirty_alert":
    for (var count = 0 ; count < msg.dirty_dispenser.length ; count ++){
      window.alert(msg.dirty_dispenser[count]);
    }
    break;
    case "network_answer":
    sleep_time += msg.sleep_time;
    wake_up_time += msg.wake_up_time;
    cycle_time += msg.cycle_time;
    setNetworkWindow(sleep_time, wake_up_time, cycle_time);
    break;
    default:
    window.alert("c'est la merde, on recoit des types chelous");
  }
});

function setDetailWindow(id, filling, battery, date){
  window.alert("Detail Window not in beta version");
}

function setNetworkWindow(sleep, wake_up, cycle){
  window.alert("Network Window not in beta version");
}

function setUpdateButton(id_updated, new_filling, new_battery){
  var id_button = "#button_" + id_updated;
  $(id_button).children(".filling_dispenser").text(new_filling);
  $(id_button).children(".battery_dispenser").text(new_battery);
  parse_button($(id_button));
}

function parse_button(current_button){
  var filling = parseInt(current_button.children('.filling_dispenser').text());
  var battery = parseInt(current_button.children('.battery_dispenser').text());
  if(filling < 20 || battery < 20){
    current_button.css('background-color','orange');
    if (filling < 5) {
      what_are_the_issue += "Dispenser "+ current_button.children('.id_dispenser').text() + " is nearly empty \n";
      current_button.css('background-color','red');
      there_is_an_issue = true;

    } else if (battery < 2) {
      what_are_the_issue += "Dispenser "+current_button.children('.id_dispenser').text() + " battery is nearly empty\n";
      current_button.css('background-color','red');
      there_is_an_issue = true;
    }
  }
}
<|endoftext|>


<commit_before>/**
 * Info for a specific pet or
 * a long sentance over a few
 * lines
 *
 *
 * GET: /v1/pets/{petId}
 *
 * path:
 *   petId {array} The id of the pet to retrieve.
 *
 */
exports.handler = function showPetById(req, res, next) {
  console.log(req.params.petId[0], req.params.petId)
  res.send('showPetById')
  next()
}
<commit_msg>removing log<commit_after>/**
 * Info for a specific pet or
 * a long sentance over a few
 * lines
 *
 *
 * GET: /v1/pets/{petId}
 *
 * path:
 *   petId {array} The id of the pet to retrieve.
 *
 */
exports.handler = function showPetById(req, res, next) {
  res.send('showPetById')
  next()
}
<|endoftext|>


<commit_before>var uuid = require('node-uuid'),
	url = require('url'),
	util = require('util'),
	amqp = require('amqp'),
	redis = require('redis'),
	assert = require('assert'),
	events = require('events');

var createMessage = require('./protocol').createMessage;

function Configuration(options) {
	var self = this;

	for (var o in options) {
		if (options.hasOwnProperty(o)) {
			self[o.replace(/^CELERY_/, '')] = options[o];
		}
	}

	self.BROKER_URL = self.BROKER_URL || 'amqp://';
	self.DEFAULT_QUEUE = self.DEFAULT_QUEUE || 'celery';
	self.RESULT_EXCHANGE = self.RESULT_EXCHANGE || 'celeryresults';
	self.TASK_RESULT_EXPIRES = self.TASK_RESULT_EXPIRES || 86400000; // 1 day

	if (self.RESULT_BACKEND && self.RESULT_BACKEND.toLowerCase() === 'amqp') {
		self.backend_type = 'amqp';
	} else if (self.RESULT_BACKEND && url.parse(self.RESULT_BACKEND)
		.protocol === 'redis:') {
		self.backend_type = 'redis';
	}
}

function Client(conf) {
	var self = this;

	self.conf = new Configuration(conf);
	self.ready = false;
	self.broker_connected = false;
	self.backend_connected = false;

	self.broker = amqp.createConnection({
		url: self.conf.BROKER_URL,
	});

	if (self.conf.backend_type === 'amqp') {
		self.backend = self.broker;
		self.backend_connected = true;
	} else if (self.conf.backend_type === 'redis') {
		var purl = url.parse(self.conf.RESULT_BACKEND);
		self.backend = redis.createClient(purl.port, purl.hostname);

		var on_ready = function() {
			self.backend_connected = true;
			if (self.broker_connected) {
				self.ready = true;
				self.emit('connect');
			}
		};

		self.backend.on('connect', function() {
			if (purl.auth) {
				self.backend.auth(purl.auth.split(':')[1], on_ready);
			} else {
				on_ready();
			}
		});

		self.backend.on('error', function(err) {
			self.emit('error', err);
		});
	}

	self.broker.on('ready', function() {
		self.broker_connected = true;
		if (self.backend_connected) {
			self.ready = true;
			self.emit('connect');
		}
	});

	self.broker.on('error', function(err) {
		self.emit('error', err);
	});

	self.broker.on('end', function() {
		self.emit('end');
	});
}

util.inherits(Client, events.EventEmitter);

Client.prototype.createTask = function(name, options) {
	return new Task(this, name, options);
};

Client.prototype.end = function() {
	this.broker.end();
	if (this.broker !== this.backend) {
		this.backend.end();
	}
};

Client.prototype.call = function(name, args, kwargs, options, callback) {
    var task = this.createTask(name),
        result = task.call(args, kwargs, options);

    if (callback) {
        result.on('ready', callback);
    }
    return result;
};

function Task(client, name, options) {
	var self = this;

	self.client = client;
	self.name = name;
	self.options = options || {};

	self.publish = function(args, kwargs, options, callback) {
		var id = uuid.v4();
		self.client.broker.publish(
		options.queue || self.client.conf.DEFAULT_QUEUE,
		createMessage(self.name, args, kwargs, options, id), {
			'contentType': 'application/json'
		},
		callback);
		return new Result(id, self.client);
	};
}

Task.prototype.call = function(args, kwargs, options, callback) {
	var self = this;

	args = args || [];
	kwargs = kwargs || {};
    options = options || {};

    assert(self.client.ready);
	return self.publish(args, kwargs, options, callback);
};

function Result(taskid, client) {
	var self = this;

	events.EventEmitter.call(self);
	self.taskid = taskid;
	self.client = client;
	self.result = null;

	if (self.client.conf.backend_type === 'amqp') {
		self.client.backend.queue(
		self.taskid.replace(/-/g, ''), {
			"arguments": {
				'x-expires': self.client.conf.TASK_RESULT_EXPIRES
			}
		},

		function(q) {
			q.bind(self.client.conf.RESULT_EXCHANGE, '#');
			q.subscribe(function(message) {
				self.result = message;
				//q.unbind('#');
				self.emit('ready', message);
			});
		});
	}
}

util.inherits(Result, events.EventEmitter);

Result.prototype.get = function(callback) {
	var self = this;
	if (callback && self.result == null) {
		self.client.backend.get('celery-task-meta-' + self.taskid, function(err, reply) {
			self.result = JSON.parse(reply);
			callback(self.result);
		});
	} else {
		return self.result;
	}
};

exports.createClient = function(config) {
	return new Client(config);
};
<commit_msg>Result emits events by task status<commit_after>var uuid = require('node-uuid'),
	url = require('url'),
	util = require('util'),
	amqp = require('amqp'),
	redis = require('redis'),
	assert = require('assert'),
	events = require('events');

var createMessage = require('./protocol').createMessage;

function Configuration(options) {
	var self = this;

	for (var o in options) {
		if (options.hasOwnProperty(o)) {
			self[o.replace(/^CELERY_/, '')] = options[o];
		}
	}

	self.BROKER_URL = self.BROKER_URL || 'amqp://';
	self.DEFAULT_QUEUE = self.DEFAULT_QUEUE || 'celery';
	self.RESULT_EXCHANGE = self.RESULT_EXCHANGE || 'celeryresults';
	self.TASK_RESULT_EXPIRES = self.TASK_RESULT_EXPIRES || 86400000; // 1 day

	if (self.RESULT_BACKEND && self.RESULT_BACKEND.toLowerCase() === 'amqp') {
		self.backend_type = 'amqp';
	} else if (self.RESULT_BACKEND && url.parse(self.RESULT_BACKEND)
		.protocol === 'redis:') {
		self.backend_type = 'redis';
	}
}

function Client(conf) {
	var self = this;

	self.conf = new Configuration(conf);
	self.ready = false;
	self.broker_connected = false;
	self.backend_connected = false;

	self.broker = amqp.createConnection({
		url: self.conf.BROKER_URL,
	});

	if (self.conf.backend_type === 'amqp') {
		self.backend = self.broker;
		self.backend_connected = true;
	} else if (self.conf.backend_type === 'redis') {
		var purl = url.parse(self.conf.RESULT_BACKEND);
		self.backend = redis.createClient(purl.port, purl.hostname);

		var on_ready = function() {
			self.backend_connected = true;
			if (self.broker_connected) {
				self.ready = true;
				self.emit('connect');
			}
		};

		self.backend.on('connect', function() {
			if (purl.auth) {
				self.backend.auth(purl.auth.split(':')[1], on_ready);
			} else {
				on_ready();
			}
		});

		self.backend.on('error', function(err) {
			self.emit('error', err);
		});
	}

	self.broker.on('ready', function() {
		self.broker_connected = true;
		if (self.backend_connected) {
			self.ready = true;
			self.emit('connect');
		}
	});

	self.broker.on('error', function(err) {
		self.emit('error', err);
	});

	self.broker.on('end', function() {
		self.emit('end');
	});
}

util.inherits(Client, events.EventEmitter);

Client.prototype.createTask = function(name, options) {
	return new Task(this, name, options);
};

Client.prototype.end = function() {
	this.broker.end();
	if (this.broker !== this.backend) {
		this.backend.end();
	}
};

Client.prototype.call = function(name, args, kwargs, options, callback) {
    var task = this.createTask(name),
        result = task.call(args, kwargs, options);

    if (callback) {
        result.on('ready', callback);
    }
    return result;
};

function Task(client, name, options) {
	var self = this;

	self.client = client;
	self.name = name;
	self.options = options || {};

	self.publish = function(args, kwargs, options, callback) {
		var id = uuid.v4();
		self.client.broker.publish(
		options.queue || self.client.conf.DEFAULT_QUEUE,
		createMessage(self.name, args, kwargs, options, id), {
			'contentType': 'application/json'
		},
		callback);
		return new Result(id, self.client);
	};
}

Task.prototype.call = function(args, kwargs, options, callback) {
	var self = this;

	args = args || [];
	kwargs = kwargs || {};
    options = options || {};

    assert(self.client.ready);
	return self.publish(args, kwargs, options, callback);
};

function Result(taskid, client) {
	var self = this;

	events.EventEmitter.call(self);
	self.taskid = taskid;
	self.client = client;
	self.result = null;

	if (self.client.conf.backend_type === 'amqp') {
		self.client.backend.queue(
		self.taskid.replace(/-/g, ''), {
			"arguments": {
				'x-expires': self.client.conf.TASK_RESULT_EXPIRES
			}
		},

		function(q) {
			q.bind(self.client.conf.RESULT_EXCHANGE, '#');
			q.subscribe(function(message) {
				self.result = message;
				//q.unbind('#');
				self.emit('ready', message);
                self.emit(message.status.toLowerCase(), message);
			});
		});
	}
}

util.inherits(Result, events.EventEmitter);

Result.prototype.get = function(callback) {
	var self = this;
	if (callback && self.result == null) {
		self.client.backend.get('celery-task-meta-' + self.taskid, function(err, reply) {
			self.result = JSON.parse(reply);
			callback(self.result);
		});
	} else {
		return self.result;
	}
};

exports.createClient = function(config) {
	return new Client(config);
};
<|endoftext|>


<commit_before>/* Copyright 2013-2018 Reahl Software Services (Pty) Ltd. All rights reserved. */
/*
    This file is part of Reahl.

    Reahl is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as
    published by the Free Software Foundation; version 3 of the License.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

(function($) {
"use strict";

function getTraditionallyNamedFragment() {
    return mapToTraditionalNames($.deparam.fragment(window.location.hash));
}

function mapToTraditionalNames(untraditionallyNamedArguments) {
    var traditionallyNamedArguments = {};
    for (var cleanName in untraditionallyNamedArguments) {
        var value = untraditionallyNamedArguments[cleanName];
        var traditionalName;
        if (Array.isArray(value)) {
            traditionalName = cleanName+'[]';
        } else {
            traditionalName = cleanName;
        }
        traditionallyNamedArguments[traditionalName] = value;
    }
    return traditionallyNamedArguments;
}

function HashArgument(name, defaultValue) {
    this.name = name;
    this.value = defaultValue;
    this.changed = false;
    this.changeValue = function(value) {
        if (!_.isEqual(this.value, value)) {
            this.changed = true;
            this.value = value;
        }
    }

    this.getIsList = function() {
        return this.name.match('\\[\\]$');
    }

    this.getIsEmptyList = function() {
        return this.getIsList() && (_.isEqual(this.value, []));
    }

    this.getEmptyListSentinelName = function() {
        return this.name+'-';
    }

    this.getDefaultedSentinelName = function() {
        return this.name+'_';
    }

    this.updateFromHashObject = function(hashObject) {
        this.changed = false;
        var currentValue = hashObject[this.name];
        if (currentValue) {
            this.changeValue(currentValue);
        } else if (this.getIsList()) {
            if (hashObject[this.getEmptyListSentinelName()] != undefined) {
                this.changeValue([]);
            }
        }
    }

    this.updateHashObject = function(hashObject) {
        delete hashObject[this.name];
        delete hashObject[this.getEmptyListSentinelName()];
        delete hashObject[this.getDefaultedSentinelName()];

        var nameInHash;
        var valueInHash;
        if (this.getIsEmptyList()) {
            nameInHash = this.getEmptyListSentinelName();
            valueInHash = '';
        } else {
            nameInHash = this.name;
            valueInHash = this.value;
        }

        hashObject[nameInHash] = valueInHash;
    }

    this.clearFromHashObject = function(hashObject) {
        delete hashObject[this.name];
        delete hashObject[this.getEmptyListSentinelName()];
        hashObject[this.getDefaultedSentinelName()] = '';
    }

}

$.widget('reahl.hashchange', {
    options: {
            url: '',
            cache: true,
            errorMessage: 'Ajax error',
            timeoutMessage: 'Ajax timeout',
            params: []
    },

    _create: function() {
        this.arguments = [];
        this.registered_callbacks = {};

        var _this = this;

        for (name in _this.options.params) {
            _this.arguments.push(new HashArgument(name, _this.options.params[name]))
        }

        var namespaced_submit = 'submit.'+this.element.attr('id');
        this.element.parents('form').off(namespaced_submit).on(namespaced_submit, function(e) {
            var form = $(this);
            _this.updateFormActionWithCurrentQueryString(form);
        });

        var namespaced_hashchange = 'hashchange.'+this.element.attr('id');
        $(window).off(namespaced_hashchange).on(namespaced_hashchange, function(e) {
            var currentFragment = getTraditionallyNamedFragment();
            var changedArguments = _this.calculateChangedArguments(currentFragment);
            if (_this.hasChanged(changedArguments)) {
                _this.triggerChange(currentFragment, changedArguments);
            };
            return true;
        });
        $(window).trigger( 'hashchange' );
    },
    getArguments: function() {
        return this.arguments;
    },
    clearArgumentsFromHash: function(hashValues) {
        var hashArguments = this.getArguments();
        for (var i=0; i<hashArguments.length; i++) {
            var argument = hashArguments[i];
            argument.clearFromHashObject(hashValues);
        }
    },
    addCallback: function(name, callback) {
        this.registered_callbacks[name] = callback;
    },
    popCallback: function() {
        var currentFragment = getTraditionallyNamedFragment();
        var name = currentFragment['__and_then__'];
        if (name !== undefined) {
            delete currentFragment['__and_then__'];
        }
        window.location.hash = $.param(currentFragment, true);

        var callback = this.registered_callbacks[name];
        if (callback !== undefined) {
            delete this.registered_callbacks[name];
            return callback;
        } else {
            return function(){}
        }
    },
    updateFormActionWithCurrentQueryString(form) {
        var fragmentInput = $('input[form="'+ form.attr('id')+'"][name="reahl-fragment"]');
        var completeFragment;
        var partialFragment;
        if (fragmentInput.length == 0) {
            fragmentInput = $('<input name="reahl-fragment" form="' + form.attr('id') + '" type="hidden">');
            fragmentInput.appendTo(form);
            partialFragment = getTraditionallyNamedFragment();
        } else {
            var handledFragmentPart = $.deparam.querystring(fragmentInput.val());
            partialFragment = $.extend(true, getTraditionallyNamedFragment(), handledFragmentPart);
        }

        completeFragment = this.addArgumentsToHash(partialFragment, this.getArguments());
        fragmentInput.val($.param(completeFragment, true));
    },
    addArgumentsToHash(currentHashValues, hashArguments) {
        var values = $.extend(true, {}, currentHashValues);
        for (var i=0; i<hashArguments.length; i++) {
            var argument = hashArguments[i];
            argument.updateHashObject(values);
        }
        return values;
    },
    calculateQueryStringValues(currentHashValues, hashArguments) {
        var values = this.addArgumentsToHash(currentHashValues, hashArguments);
        var relevantFormInputs = this.getFormInputsAsArguments().filter(function(i, v){ return ! (v.getDefaultedSentinelName() in values) });
        values = this.addArgumentsToHash(values, relevantFormInputs);
        var urlQueryString = $.deparam.querystring(this.options.url);
        for (var i in urlQueryString) {
            if (i in values){
                delete values[i];
            }
        }
        return values;
    },
    getFormInputsAsArguments: function(){
        return $('.reahl-primitiveinput').map(function(i, v) {
            var primitiveInput = $(v).data('reahlPrimitiveinput');
            return new HashArgument(primitiveInput.getName(), primitiveInput.getCurrentInputValue());
        });
    },

    triggerChange: function(currentHashValues, newArguments) {
        var _this = this;

        var after_handler = _this.popCallback();
        var data = _this.calculateQueryStringValues(currentHashValues, newArguments);
        delete data['__and_then__'];

        _this.element.block({overlayCSS: {backgroundColor: '#fff', opacity: 0.3}, message: '', fadeIn: 0, fadeout: 0});
        $.ajax({url:     _this.options.url,
                cache:   _this.options.cache,
                data:    data,
                success: function(data){
                    _this.element.html(data);
                    _this.arguments = newArguments;
                },
                complete: function(data){
                    _this.element.unblock();
                    after_handler();
                },
                traditional: true
        });
    },
    hasChanged: function(newArguments){
        var _this = this;
        var changed = false;
        for (var i=0; i<newArguments.length; i++) {
            var argument = newArguments[i];
            if (argument.changed) {
                changed = true;
            };
        }
        return changed;
    },
    calculateChangedArguments: function(currentHashValues) {
        var _this = this;
        var hashArguments = $.extend(true, [], _this.getArguments());
        for (var i=0; i<hashArguments.length; i++) {
            var argument = hashArguments[i];
            argument.updateFromHashObject(currentHashValues);
        };
        return hashArguments;
    },
});

$.extend($.reahl.hashchange, {
    version: '1.8'
});

$.widget('reahl.changenotifier', {
    options: {
        name: undefined,
        widget_id: undefined,
        argument: undefined
    },
    _create: function() {
            var o = this.options;
            var element = this.element;
            var _this = this;

            if (!o.name) { throw new Error("No name given in options. This is a required option.")}

            $(element).attr('data-target-widget', o.widget_id);
            $(element).on( 'change', function(e) {
                if (_this.getIsSelfValid()) {
                    _this.updateCurrentValue(e.target);
                }
                if (_this.getIsValid()) {
                    var currentFragment = getTraditionallyNamedFragment();
                    _this.blockAllSiblingInputs(currentFragment);
                    _this.removeNestedWidgetArguments(currentFragment);
                    _this.updateHashWithAllSiblingValues(currentFragment);
                    this.focus();
                    _this.unblockWidget();
                } else { _this.blockWidget(); }
                return true;
            });
    },
    updateCurrentValue: function(currentInput) {
        var _this = this;
        var primitiveInput = $(currentInput).data('reahlPrimitiveinput');
        var currentInputValue = primitiveInput.getCurrentInputValue();
        if (_this.options.argument == undefined) {
            _this.options.argument = new HashArgument(this.options.name, currentInputValue);
        }
        var argument = _this.options.argument;
        argument.changeValue(currentInputValue);
    },
    blockAllSiblingInputs: function(currentFragment) {
        var _this = this;
        var uniqueId = new Date().getTime();
        uniqueId += (parseInt(Math.random() * 100)).toString();
        uniqueId = 'cb-' + uniqueId;

        var form = this.getForm();
        form.block({overlayCSS: {backgroundColor: '#fff', opacity: 0.3}, message: '', fadeIn: 0, fadeout: 0});
        this.addAfterHandler(uniqueId, function() { form.unblock(); } );
        currentFragment['__and_then__'] = uniqueId;
    },
    removeNestedWidgetArguments: function(currentFragment) {
        var o = this.options;
        $('#'+this.options.widget_id).find('*').filter(function(){ return $(this).data('reahlHashchange')}).each(function(i){
            $(this).data('reahlHashchange').clearArgumentsFromHash(currentFragment);
        });
    },
    updateHashWithAllSiblingValues: function(currentFragment) {
        this.getSiblingChangeNotifiers().each(function() {
            this.addCurrentInputValueTo(currentFragment);

        });
        var newHash = $.param(currentFragment, true);
        window.location.hash = newHash;
    },
    addCurrentInputValueTo: function(fragment) {
        var argument = this.options.argument;
        if (argument !== undefined) {
            argument.updateHashObject(fragment);
        }
    },
    getInputs: function() {
        if (this.element[0].form !== undefined) {
            return this.element;
        } else {
            return this.element.find('input');
        }
    },
    getForm: function() {
        return $(this.getInputs()[0].form);
    },
    getIsSelfValid: function() {
        var validator = this.getForm().data('validator');
        return validator.element(this.getInputs());
    },
    addAfterHandler: function(name, handler) {
        $('#'+this.options.widget_id).data('reahlHashchange').addCallback(name, handler);
    },
    getSiblings: function() {
        return $('[data-target-widget="' + this.options.widget_id + '"]');
    },
    getSiblingChangeNotifiers: function() {
        return this.getSiblings().map(function() {
            return $(this).data('reahlChangenotifier')
        });
    },
    getIsSiblingArgumentsValid: function() {
        var _this = this;
        var valid = true;
        this.getSiblingChangeNotifiers().each(function() {
            var sibling = this;
            if (sibling !== _this) {
                valid = valid && sibling.getIsSelfValid();
            }
        });
        return valid;
    },
    getIsValid: function() {
        return this.getIsSelfValid() && this.getIsSiblingArgumentsValid();
    },
    blockWidget: function() {
        $('#'+this.options.widget_id).block({overlayCSS: {backgroundColor: '#fff', opacity: 0.3}, message: '', fadeIn: 0, fadeout: 0});
    },
    unblockWidget: function() {
        $('#'+this.options.widget_id).unblock();
    }
});

$.extend($.reahl.changenotifier, {
version: '1.0'
});

})(jQuery);

<commit_msg>clean up the urls of hashchangehandlers if they get genereated with defaultedSentinels on them<commit_after>/* Copyright 2013-2018 Reahl Software Services (Pty) Ltd. All rights reserved. */
/*
    This file is part of Reahl.

    Reahl is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as
    published by the Free Software Foundation; version 3 of the License.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

(function($) {
"use strict";

function getTraditionallyNamedFragment() {
    return mapToTraditionalNames($.deparam.fragment(window.location.hash));
}

function mapToTraditionalNames(untraditionallyNamedArguments) {
    var traditionallyNamedArguments = {};
    for (var cleanName in untraditionallyNamedArguments) {
        var value = untraditionallyNamedArguments[cleanName];
        var traditionalName;
        if (Array.isArray(value)) {
            traditionalName = cleanName+'[]';
        } else {
            traditionalName = cleanName;
        }
        traditionallyNamedArguments[traditionalName] = value;
    }
    return traditionallyNamedArguments;
}

function HashArgument(name, defaultValue) {
    this.name = name;
    this.value = defaultValue;
    this.changed = false;
    this.changeValue = function(value) {
        if (!_.isEqual(this.value, value)) {
            this.changed = true;
            this.value = value;
        }
    }

    this.getIsList = function() {
        return this.name.match('\\[\\]$');
    }

    this.getIsEmptyList = function() {
        return this.getIsList() && (_.isEqual(this.value, []));
    }

    this.getEmptyListSentinelName = function() {
        return this.name+'-';
    }

    this.getDefaultedSentinelName = function() {
        return this.name+'_';
    }

    this.updateFromHashObject = function(hashObject) {
        this.changed = false;
        var currentValue = hashObject[this.name];
        if (currentValue) {
            this.changeValue(currentValue);
        } else if (this.getIsList()) {
            if (hashObject[this.getEmptyListSentinelName()] != undefined) {
                this.changeValue([]);
            }
        }
    }

    this.updateHashObject = function(hashObject) {
        delete hashObject[this.name];
        delete hashObject[this.getEmptyListSentinelName()];
        delete hashObject[this.getDefaultedSentinelName()];

        var nameInHash;
        var valueInHash;
        if (this.getIsEmptyList()) {
            nameInHash = this.getEmptyListSentinelName();
            valueInHash = '';
        } else {
            nameInHash = this.name;
            valueInHash = this.value;
        }

        hashObject[nameInHash] = valueInHash;
    }

    this.clearFromHashObject = function(hashObject) {
        delete hashObject[this.name];
        delete hashObject[this.getEmptyListSentinelName()];
        hashObject[this.getDefaultedSentinelName()] = '';
    }
}

$.widget('reahl.hashchange', {
    options: {
            url: '',
            cache: true,
            errorMessage: 'Ajax error',
            timeoutMessage: 'Ajax timeout',
            params: []
    },

    _create: function() {
        this.arguments = [];
        this.registered_callbacks = {};

        var _this = this;

        for (name in _this.options.params) {
            _this.arguments.push(new HashArgument(name, _this.options.params[name]))
        }

        var namespaced_submit = 'submit.'+this.element.attr('id');
        this.element.parents('form').off(namespaced_submit).on(namespaced_submit, function(e) {
            var form = $(this);
            _this.updateFormActionWithCurrentQueryString(form);
        });

        var namespaced_hashchange = 'hashchange.'+this.element.attr('id');
        $(window).off(namespaced_hashchange).on(namespaced_hashchange, function(e) {
            var currentFragment = getTraditionallyNamedFragment();
            var changedArguments = _this.calculateChangedArguments(currentFragment);
            if (_this.hasChanged(changedArguments)) {
                _this.triggerChange(currentFragment, changedArguments);
            };
            return true;
        });
        $(window).trigger( 'hashchange' );
    },
    getArguments: function() {
        return this.arguments;
    },
    clearArgumentsFromHash: function(hashValues) {
        var hashArguments = this.getArguments();
        for (var i=0; i<hashArguments.length; i++) {
            var argument = hashArguments[i];
            argument.clearFromHashObject(hashValues);
        }
    },
    addCallback: function(name, callback) {
        this.registered_callbacks[name] = callback;
    },
    popCallback: function() {
        var currentFragment = getTraditionallyNamedFragment();
        var name = currentFragment['__and_then__'];
        if (name !== undefined) {
            delete currentFragment['__and_then__'];
        }
        window.location.hash = $.param(currentFragment, true);

        var callback = this.registered_callbacks[name];
        if (callback !== undefined) {
            delete this.registered_callbacks[name];
            return callback;
        } else {
            return function(){}
        }
    },
    updateFormActionWithCurrentQueryString(form) {
        var fragmentInput = $('input[form="'+ form.attr('id')+'"][name="reahl-fragment"]');
        var completeFragment;
        var partialFragment;
        if (fragmentInput.length == 0) {
            fragmentInput = $('<input name="reahl-fragment" form="' + form.attr('id') + '" type="hidden">');
            fragmentInput.appendTo(form);
            partialFragment = getTraditionallyNamedFragment();
        } else {
            var handledFragmentPart = $.deparam.querystring(fragmentInput.val());
            partialFragment = $.extend(true, getTraditionallyNamedFragment(), handledFragmentPart);
        }

        completeFragment = this.addArgumentsToHash(partialFragment, this.getArguments());
        fragmentInput.val($.param(completeFragment, true));
    },
    addArgumentsToHash(currentHashValues, hashArguments) {
        var values = $.extend(true, {}, currentHashValues);
        for (var i=0; i<hashArguments.length; i++) {
            var argument = hashArguments[i];
            argument.updateHashObject(values);
        }
        return values;
    },
    calculateQueryStringValues(currentHashValues, hashArguments) {
        var values = this.addArgumentsToHash(currentHashValues, hashArguments);
        var relevantFormInputs = this.getFormInputsAsArguments().filter(function(i, v){ return ! (v.getDefaultedSentinelName() in values) });
        values = this.addArgumentsToHash(values, relevantFormInputs);
        var urlQueryString = $.deparam.querystring(this.options.url);
        for (var i in urlQueryString) {
            if (i in values){
                delete values[i];
            }
        };

        return values;
    },
    getFormInputsAsArguments: function(){
        return $('.reahl-primitiveinput').map(function(i, v) {
            var primitiveInput = $(v).data('reahlPrimitiveinput');
            return new HashArgument(primitiveInput.getName(), primitiveInput.getCurrentInputValue());
        });
    },
    getUrlWithDefaultedSentinelsCleared: function(){
        var queryStringIndex = this.options.url.indexOf('?');
        var urlQueryString = {};
        var url = this.options.url;
        if (queryStringIndex >= 0) {
            url = this.options.url.substr(0, queryStringIndex)+'?';
            urlQueryString = $.deparam.querystring(this.options.url);
            for (var i in urlQueryString) {
                if (i.endsWith('_')){
                    delete urlQueryString[i];
                }
            };
        }
        return url+$.param(urlQueryString, true);
    },
    triggerChange: function(currentHashValues, newArguments) {
        var _this = this;

        var after_handler = _this.popCallback();
        var data = _this.calculateQueryStringValues(currentHashValues, newArguments);
        delete data['__and_then__'];

        _this.element.block({overlayCSS: {backgroundColor: '#fff', opacity: 0.3}, message: '', fadeIn: 0, fadeout: 0});
        $.ajax({url:     _this.getUrlWithDefaultedSentinelsCleared(),
                cache:   _this.options.cache,
                data:    data,
                success: function(data){
                    _this.element.html(data);
                    _this.arguments = newArguments;
                },
                complete: function(data){
                    _this.element.unblock();
                    after_handler();
                },
                traditional: true
        });
    },
    hasChanged: function(newArguments){
        var _this = this;
        var changed = false;
        for (var i=0; i<newArguments.length; i++) {
            var argument = newArguments[i];
            if (argument.changed) {
                changed = true;
            };
        }
        return changed;
    },
    calculateChangedArguments: function(currentHashValues) {
        var _this = this;
        var hashArguments = $.extend(true, [], _this.getArguments());
        for (var i=0; i<hashArguments.length; i++) {
            var argument = hashArguments[i];
            argument.updateFromHashObject(currentHashValues);
        };
        return hashArguments;
    },
});

$.extend($.reahl.hashchange, {
    version: '1.8'
});

$.widget('reahl.changenotifier', {
    options: {
        name: undefined,
        widget_id: undefined,
        argument: undefined
    },
    _create: function() {
            var o = this.options;
            var element = this.element;
            var _this = this;

            if (!o.name) { throw new Error("No name given in options. This is a required option.")}

            $(element).attr('data-target-widget', o.widget_id);
            $(element).on( 'change', function(e) {
                if (_this.getIsSelfValid()) {
                    _this.updateCurrentValue(e.target);
                }
                if (_this.getIsValid()) {
                    var currentFragment = getTraditionallyNamedFragment();
                    _this.blockAllSiblingInputs(currentFragment);
                    _this.removeNestedWidgetArguments(currentFragment);
                    _this.updateHashWithAllSiblingValues(currentFragment);
                    this.focus();
                    _this.unblockWidget();
                } else { _this.blockWidget(); }
                return true;
            });
    },
    updateCurrentValue: function(currentInput) {
        var _this = this;
        var primitiveInput = $(currentInput).data('reahlPrimitiveinput');
        var currentInputValue = primitiveInput.getCurrentInputValue();
        if (_this.options.argument == undefined) {
            _this.options.argument = new HashArgument(this.options.name, currentInputValue);
        }
        var argument = _this.options.argument;
        argument.changeValue(currentInputValue);
    },
    blockAllSiblingInputs: function(currentFragment) {
        var _this = this;
        var uniqueId = new Date().getTime();
        uniqueId += (parseInt(Math.random() * 100)).toString();
        uniqueId = 'cb-' + uniqueId;

        var form = this.getForm();
        form.block({overlayCSS: {backgroundColor: '#fff', opacity: 0.3}, message: '', fadeIn: 0, fadeout: 0});
        this.addAfterHandler(uniqueId, function() { form.unblock(); } );
        currentFragment['__and_then__'] = uniqueId;
    },
    removeNestedWidgetArguments: function(currentFragment) {
        var o = this.options;
        $('#'+this.options.widget_id).find('*').filter(function(){ return $(this).data('reahlHashchange')}).each(function(i){
            $(this).data('reahlHashchange').clearArgumentsFromHash(currentFragment);
        });
    },
    updateHashWithAllSiblingValues: function(currentFragment) {
        this.getSiblingChangeNotifiers().each(function() {
            this.addCurrentInputValueTo(currentFragment);

        });
        var newHash = $.param(currentFragment, true);
        window.location.hash = newHash;
    },
    addCurrentInputValueTo: function(fragment) {
        var argument = this.options.argument;
        if (argument !== undefined) {
            argument.updateHashObject(fragment);
        }
    },
    getInputs: function() {
        if (this.element[0].form !== undefined) {
            return this.element;
        } else {
            return this.element.find('input');
        }
    },
    getForm: function() {
        return $(this.getInputs()[0].form);
    },
    getIsSelfValid: function() {
        var validator = this.getForm().data('validator');
        return validator.element(this.getInputs());
    },
    addAfterHandler: function(name, handler) {
        $('#'+this.options.widget_id).data('reahlHashchange').addCallback(name, handler);
    },
    getSiblings: function() {
        return $('[data-target-widget="' + this.options.widget_id + '"]');
    },
    getSiblingChangeNotifiers: function() {
        return this.getSiblings().map(function() {
            return $(this).data('reahlChangenotifier')
        });
    },
    getIsSiblingArgumentsValid: function() {
        var _this = this;
        var valid = true;
        this.getSiblingChangeNotifiers().each(function() {
            var sibling = this;
            if (sibling !== _this) {
                valid = valid && sibling.getIsSelfValid();
            }
        });
        return valid;
    },
    getIsValid: function() {
        return this.getIsSelfValid() && this.getIsSiblingArgumentsValid();
    },
    blockWidget: function() {
        $('#'+this.options.widget_id).block({overlayCSS: {backgroundColor: '#fff', opacity: 0.3}, message: '', fadeIn: 0, fadeout: 0});
    },
    unblockWidget: function() {
        $('#'+this.options.widget_id).unblock();
    }
});

$.extend($.reahl.changenotifier, {
version: '1.0'
});

})(jQuery);

<|endoftext|>


<commit_before>define(["dojo/_base/Deferred", "dojo/topic", "dojo/request/xhr",
	"dojo/promise/all", "dojo/store/Memory", "dojo/store/Observable", "dojo/when"
], function(Deferred, Topic, xhr,
			All, MemoryStore, Observable, when){

	var self = this;
	var TIME_OUT = 5000; // in ms

	// state model of filters applied to jobs
	self.filters = {
		app: 'all',
		status: null
	};

	// state of status (used to detect changes)
	var StatusSummary = {init: null};

	var _DataStore = new MemoryStore({
		idProperty: "id",
		data: []
	})

	// kick off the polling
	setTimeout(PollJobs, 1000);

	function PollJobs(){
		// leaving this here since instantiation order is unpredictable
		if(!(window.App && window.App.api && window.App.api.service)){
			setTimeout(PollJobs, 1000);
			return;
		}

		// check for status change.  if change, update jobs list
		var prom = getStatus();
		prom.then(function(statusChange){
			if(statusChange) updateJobsList();

			setTimeout(PollJobs, TIME_OUT);
		})
	}

	/**
	 * listen for job filtering to store filter state locally
	 */
	Topic.subscribe("/JobFilter", function(filter){
		Object.assign(self.filters, filter)
	});

	/**
	 * updates the job list (see JobsGrid.js)
	 */
	function updateJobsList(){
		Topic.publish("/Jobs", {status: 'loading'});

		var prom = window.App.api.service("AppService.enumerate_tasks", [0, 20000]);
		return Deferred.when(prom, function(res){
			var jobs = res[0];
			_DataStore.setData(jobs)

			if(self.filters.app || self.filters.status){
				Topic.publish("/Jobs", {status: 'filtered', jobs: _DataStore.data});
				Topic.publish("/JobFilter", self.filters);
				return;
			}

			Topic.publish("/Jobs", {status: 'updated', jobs: _DataStore.data});
		});
	}

	/**
	 * sets status locally, publishes status for jobs ticker, and returns True if any changes
	 */
	function getStatus(){
		var prom = window.App.api.service("AppService.query_task_summary", []);
		return prom.then(function(res){
			var status = res[0];

			var queued = (status.queued || 0) + (status.pending || 0) + (status.init || 0);
			var	inProgress = status['in-progress'] || 0;
			var	completed = status.completed || 0;
			var	failed = status.failed || 0;

			// check for any changes in status
			var change = false;
			if(queued !== StatusSummary.queued ||
			   inProgress !== StatusSummary.inProgress ||
			   completed !== StatusSummary.completed ||
			   failed !== StatusSummary.failed){
				change = true;
			}

			StatusSummary = {
				'queued': queued,
				'inProgress': inProgress,
				'completed': completed,
				'failed': failed
			}

			// publish job status for jobs ticker
			Topic.publish("/JobStatus", StatusSummary);

			return change; // bool
		})
	}

	return {
		queryTaskDetail: function(id, stdout, stderr){
			return Deferred.when(window.App.api.service("AppService.query_task_details", [id]), function(detail){
				detail = detail[0];
				var defs = [];
				if(detail.stderr_url && stderr){
					defs.push(Deferred.when(xhr.get(detail.stderr_url, {
						headers: {
							"Authorization": "Oauth " + window.App.authorizationToken,
							"X-Requested-With": false
						}
					}), function(txt){
						detail.stderr = txt;

					}));
				}
				if(detail.stdout_url && stdout){
					defs.push(Deferred.when(xhr.get(detail.stdout_url, {
						headers: {
							"Authorization": "Oauth " + window.App.authorizationToken,
							"X-Requested-With": false
						}
					}), function(txt){
						detail.stdout = txt;

					}));
				}
				if(defs.length < 1){
					return detail;
				}else{
					return Deferred.when(All(defs), function(){
						return detail;
					});
				}
			});
		},

		getStore: function(){
			return _DataStore;
		}
	}
});
<commit_msg>ensure jobs list update is finished before resuming polling<commit_after>define(["dojo/_base/Deferred", "dojo/topic", "dojo/request/xhr",
	"dojo/promise/all", "dojo/store/Memory", "dojo/store/Observable", "dojo/when"
], function(Deferred, Topic, xhr,
			All, MemoryStore, Observable, when){

	var self = this;
	var TIME_OUT = 5000; // in ms

	// state model of filters applied to jobs
	self.filters = {
		app: 'all',
		status: null
	};

	// state of status (used to detect changes)
	var StatusSummary = {init: null};

	var _DataStore = new MemoryStore({
		idProperty: "id",
		data: []
	})

	// kick off the polling
	setTimeout(PollJobs, 1000);

	function PollJobs(){
		// leaving this here since instantiation order is unpredictable
		if(!(window.App && window.App.api && window.App.api.service)){
			setTimeout(PollJobs, 1000);
			return;
		}

		// check for status change.  if change, update jobs list
		var prom = getStatus();
		prom.then(function(statusChange){
			if(statusChange){
				updateJobsList().then(function(){
					setTimeout(PollJobs, TIME_OUT);
				})
				return;
			}

			setTimeout(PollJobs, TIME_OUT);
		})
	}

	/**
	 * listen for job filtering to store filter state locally
	 */
	Topic.subscribe("/JobFilter", function(filter){
		Object.assign(self.filters, filter)
	});

	/**
	 * updates the job list (see JobsGrid.js)
	 */
	function updateJobsList(){
		Topic.publish("/Jobs", {status: 'loading'});

		var prom = window.App.api.service("AppService.enumerate_tasks", [0, 20000]);
		return prom.then(function(res){
			var jobs = res[0];
			_DataStore.setData(jobs)

			if(self.filters.app || self.filters.status){
				Topic.publish("/Jobs", {status: 'filtered', jobs: _DataStore.data});
				Topic.publish("/JobFilter", self.filters);
				return;
			}

			Topic.publish("/Jobs", {status: 'updated', jobs: _DataStore.data});
		});
	}

	/**
	 * sets status locally, publishes status for jobs ticker, and returns True if any changes
	 */
	function getStatus(){
		var prom = window.App.api.service("AppService.query_task_summary", []);
		return prom.then(function(res){
			var status = res[0];

			var queued = (status.queued || 0) + (status.pending || 0) + (status.init || 0);
			var	inProgress = status['in-progress'] || 0;
			var	completed = status.completed || 0;
			var	failed = status.failed || 0;

			// check for any changes in status
			var change = false;
			if(queued !== StatusSummary.queued ||
			   inProgress !== StatusSummary.inProgress ||
			   completed !== StatusSummary.completed ||
			   failed !== StatusSummary.failed){
				change = true;
			}

			StatusSummary = {
				'queued': queued,
				'inProgress': inProgress,
				'completed': completed,
				'failed': failed
			}

			// publish job status for jobs ticker
			Topic.publish("/JobStatus", StatusSummary);

			return change; // bool
		})
	}

	return {
		queryTaskDetail: function(id, stdout, stderr){
			return Deferred.when(window.App.api.service("AppService.query_task_details", [id]), function(detail){
				detail = detail[0];
				var defs = [];
				if(detail.stderr_url && stderr){
					defs.push(Deferred.when(xhr.get(detail.stderr_url, {
						headers: {
							"Authorization": "Oauth " + window.App.authorizationToken,
							"X-Requested-With": false
						}
					}), function(txt){
						detail.stderr = txt;

					}));
				}
				if(detail.stdout_url && stdout){
					defs.push(Deferred.when(xhr.get(detail.stdout_url, {
						headers: {
							"Authorization": "Oauth " + window.App.authorizationToken,
							"X-Requested-With": false
						}
					}), function(txt){
						detail.stdout = txt;

					}));
				}
				if(defs.length < 1){
					return detail;
				}else{
					return Deferred.when(All(defs), function(){
						return detail;
					});
				}
			});
		},

		getStore: function(){
			return _DataStore;
		}
	}
});
<|endoftext|>


<commit_before>(function(jQuery) {

  var $ = jQuery;

  // jquery selectors which are used in the tour definition
  var SELECTOR = {
	home_button: 'a:contains("Context Viewer")',
  };

  var EXAMPLE_URL = "/lis_context_viewer/index.html#/search/lis/phavu.Phvul.002G085200?numNeighbors=10&numMatchedFamilies=4&numNonFamily=5&algorithm=repeat&match=10&mismatch=-1&gap=-1&score=30&threshold=25&track_regexp=&order=distance&sources=lis";

  var tour = new Tour({
    name: 'genome-context-viewer',
    debug: true,
    orphan: true,

    steps: [
/*
        {
          title: "Genome Context Viewer Tour: Welcome to LIS!",
          content: 'Let\'s go the legumeinfo.org homepage, where the Genome Context Viewer Tour will begin.  Use the Next button or → (right arrow key) to advance the tour. Use the Prev button or ← (left arrow key) to step back.',
           element: "#site-name",
           placement: "bottom",
           reflex: "true",
        },
        {
          title: "Genome Context Viewer Tour: Getting started",
          path: "/home",
          content: 'The Genome Context Viewer can be linked to directly using gene names, and is linked to from many places on the site where genes are present. For this tour, we\'ll go directly via the front page.',
          element: SELECTOR.home_button,
          placement: "bottom",
          reflex: "true",
          onNext: function() {
            $(SELECTOR.home_button)[0].click();
          }
        },
*/
        {
          title: "Genome Context Viewer Tour: Landing Page",
	  // Dont use complex path properties, because if the URL
	  // path/hash/query string etc.  is changed by angular then
	  // bootstrap-tour will go into a redirect loop trying to
	  // load the stated path. This onShow() approach can be used
	  // instead.

          //path: "/lis_context_viewer/#/instructions",
          //path: "/lis_context_viewer/index.html#/search/lis/phavu.Phvul.002G085200?numNeighbors=10&numMatchedFamilies=4&numNonFamily=5&algorithm=repeat&match=10&mismatch=-1&gap=-1&score=30&threshold=25&track_regexp=&order=distance&sources=lis",
	  onShow: function(tour) {
	    // if the URL is not matching lis_context_viewer
	    if(document.location.href.indexOf('lis_context_viewer/') === -1) {
	      console.log('redirecting to example context viewer: '+ EXAMPLE_URL);
	      document.location.href = EXAMPLE_URL;
	      return (new jQuery.Deferred()).promise();
	    }
	  },
          content: "The Genome Context Viewer allows you to view regions of genomes considered primarily with respect to the ordering and orientation of their annotated gene content, as well as to search for segments similar in gene content and organization. This allows a quick but powerful view into microsyntenic relationships among difference chromosomal segments.",
          element: 'h1:contains("Welcome")',
        },
        {
          title: "Genome Context Viewer Tour: Examples",
          content: "We've provided some examples to get you started. For this tour, we'll focus on the first example.",
          element: 'h2:contains("Examples") + li',
          placement: "right",
        },
        {
          title: "Gene Families: Legend",
          content: "Each gene in a Context View is colored by the gene family it belongs to as indicated in the legend (genes belonging to families with no other representatives in a view are left uncolored, while genes not belonging to families are uncolored and have dotted outlines). In the case where a single gene is used to invoke the viewer, that gene's context track will be used as a query to find other tracks annotated with similar gene family content in the database. If a set of genes from a gene family was used to request a view, these genes will be centered in each context but no alignment of the tracks will be performed.",
          element: "legend",
          placement: "left"
         },
        {
          title: "Gene Families",
          content: "Since genes in a family tend to have relatively similar sequences, we can use them to predict the functions of newly identified genes based on their relations to other known genes, especially in cases where the genes are found in similar syntenic contexts. Gene family colors in the legend will display all representatives of the family in the current view when moused-over, or when clicked will list those genes with the option to view them in the context of the family's phylogenetic tree. ",
          element: ".legend",
          placement: "left"
           },
        {
          title: "Context Viewer",
          content: "The context viewer displays corresponding regions around a selected gene or set of genes in a subtree. It makes it easy to find functional gene groups as well as help make hypotheses about their evolutionary histories. Gene glyphs have mouse over and click interactivity. If a glyph is moused over its name and genomic position are shown. If a gene is clicked a widget will appear with a variety of links related to the gene, such as a link to the source of the gene annotation. The thicker the connecting line between the genes, the longer the intergenic distance on the chromosome; intergenic distances and gene identities for tracks can also be displayed by mousing over the track labels to the left of the tracks.",
          element: ".rail",
          placement: "bottom",
         },
        {
          title: "Dot Plots",
                content: "Dot plots are useful in identifying interspersed, lost, or gained repeat motifs.",
          element: ".axis_right",
          placement: "top",
          multipage: true,
              onNext: function() {
        $('.axis_right text').d3Click();
         $(document).ready(waitForplot);
            function waitForplot() {
        if (! $('#plot').is(':visible')) {
        setTimeout(waitForplot, 900);
        }
        else { return hopscotch.startTour(tour, 6)}
                    }}
             },
        {
         title: "The Plot Thickens",
              content: "The main diagonal shows the sequence's alignment with itself, while patterns off the main diagonal indicate structural similarities among different parts of the sequences. Lines off the main diagonal imply different regions with a high similarity.",
         element: "local-plot",
         placement: "left",
         onNext: function() {
             $('#gloplot').trigger("click");
                 $('.axis_right text').d3Click();
             $(document).ready(waitForglobal);
            function waitForglobal() {
        if (! $('#global-plot').is(':visible')) {
        setTimeout(waitForglobal, 900);
                    }
        else { return hopscotch.startTour(tour, 7)}
                    }}
        },
        {
         title: "Global Plots",
         content: "Just like the local plot but instead of focusing only on the matched syntenic segment, the global plot displays all instances of genes from the families of the query track in across the chromosome from which the matched syntenic segment was taken. This gives a better sense for the frequency with which members of these families occur outside the matched context and can reveal wider syntenic properties.",
              element: "#global-plot",
         placement: "left"
        },
        {
             title: "Parameters",
              content: "These parameters allow you to fine-tune the candidate tracks retrieved and the alignments produced from them.",
         element: "top-nav",
         placement: "bottom",
         arrowOffset: "100",
         onNext: function() {
        $('#parambtn').trigger('click');
        $(document).ready(waitForslider);
        function waitForslider()   {

        if (! $('#left-slider').is(':visible')) {
        setTimeout(waitForslider, 1000);
        }
        else {
        return hopscotch.startTour(tour, 9);
            }}
                        },
         multipage: true
        },
        {
         title: "Neighbors",
         content: "The 'Neighbor' value controls the number of genes that surround the central gene. By default, the regions extend out by 8 genes upstream and down from the selected genes. Increasing this value will give you longer contexts with more sensitivity for finding distant matches, but will increase retrieval times and may make the display harder to interpret.",
            element: "neighborpane", //Should point to the field input, but depends on the size/shape of the window.
         placement: "right",
        },
        {
         title: "Scroll Control",
         content: "The scroll input is used to scroll in either direction on the query track's chromosome. In other words, given a scroll distance and direction from the current focus gene, a new query is made with the track built around the new focus found with these parameters. The allowed scroll values are constrained so that the new focus gene after scrolling is present in the context before scrolling.",
         element: "form-wrapper",
         placement: "right"
            },
        {
         title: "Algorithms",
              content: "Synteny between tracks is determined via a modified Smith-Waterman or Repeat alignment algorithm. For Smith-Waterman, the orientation (forward/reverse) with the higher score is displayed. For the Repeat algorithm, all alignments are kept and displayed as related tracks. This has the advantage of nicely capturing inversions.",
         element: "algpar",
         placement: "right",
        },
        {
         title: "Alerts",
             content: "A context search is performed by querying a database for tracks with similar gene family content to a query track. The result tracks found are then aligned to the query track using alignment based on common family memberships. The search view displays the query track and the alignments that meet the specified alignment criteria. If the number of tracks returned exceeds the number aligned, you may consider altering the Alignment Parameters to see if you are missing out on some more complex syntenic relationships.",
         element: "alerts",
          placement: "bottom",
         arrowOffset: "center"
        },
        {
         title: "Want to know more?",
         content: "This\
         documentation link will take you back to the LIS homepage.",
         element: "helpbtn",
         placement: "bottom"
         }
    ],
  });

  lisTours.register(tour);

}(window.__jquery));

<commit_msg>still WIP, but mostly working<commit_after>(function(jQuery) {

  var $ = jQuery;

  // jquery selectors which are used in the tour definition
  var SELECTOR = {
    home_button: 'a:contains("Context Viewer")',
  };

  var EXAMPLE_URL = "/lis_context_viewer/index.html#/search/lis/phavu.Phvul.002G085200?numNeighbors=10&numMatchedFamilies=4&numNonFamily=5&algorithm=repeat&match=10&mismatch=-1&gap=-1&score=30&threshold=25&track_regexp=&order=distance&sources=lis";
  var LANDING_URL = "/lis_context_viewer/#/instructions";

  var tour = new Tour({

    name: 'genome-context-viewer',
    debug: true,
    orphan: true,

    steps: [
        {
          title: "Genome Context Viewer Tour: Welcome to LIS!",
          content: 'Let\'s go the legumeinfo.org homepage, where the Genome Context Viewer Tour will begin.  Use the Next button or → (right arrow key) to advance the tour. Use the Prev button or ← (left arrow key) to step back.',
          element: "#site-name",
          placement: "bottom",
          reflex: "true",
        },
        {
          title: "Genome Context Viewer Tour: Getting started",
          path: "/home",
          content: 'The Genome Context Viewer can be linked to directly using gene names, and is linked to from many places on the site where genes are present. For this tour, we\'ll go directly via the front page.',
          element: SELECTOR.home_button,
          placement: "bottom",
          reflex: "true",
          onNext: function() {
            $(SELECTOR.home_button)[0].click();
          }
        },
        {
          title: "Genome Context Viewer Tour: Landing Page",
      // Dont use complex path properties, because if the URL
      // path/hash/query string etc.  is changed by angular then
      // bootstrap-tour will go into a redirect loop trying to
      // load the stated path. This onShow() approach can be used
      // instead.

          //path: "/lis_context_viewer/#/instructions",
          //path: "/lis_context_viewer/index.html#/search/lis/phavu.Phvul.002G085200?numNeighbors=10&numMatchedFamilies=4&numNonFamily=5&algorithm=repeat&match=10&mismatch=-1&gap=-1&score=30&threshold=25&track_regexp=&order=distance&sources=lis",
      onShow: function(tour) {
        // if the URL is not matching lis_context_viewer
        if(document.location.href.indexOf('lis_context_viewer/') === -1) {
          console.log('redirecting to example context viewer: '+ LANDING_URL);
          document.location.href = LANDING_URL;
          return (new jQuery.Deferred()).promise();
        }
      },
          content: "The Genome Context Viewer allows you to view regions of genomes considered primarily with respect to the ordering and orientation of their annotated gene content, as well as to search for segments similar in gene content and organization. This allows a quick but powerful view into microsyntenic relationships among difference chromosomal segments.",
          element: 'h1:contains("Welcome")',
        },
        {
          title: "Genome Context Viewer Tour: Examples",
          content: "We've provided some examples to get you started. For this tour, we'll focus on the first example.",
          element: 'h2:contains("Examples") + li',
          placement: "right",
        },
        {
          title: "Gene Families: Legend",
          content: "Each gene in a Context View is colored by the gene family it belongs to as indicated in the legend (genes belonging to families with no other representatives in a view are left uncolored, while genes not belonging to families are uncolored and have dotted outlines). In the case where a single gene is used to invoke the viewer, that gene's context track will be used as a query to find other tracks annotated with similar gene family content in the database. If a set of genes from a gene family was used to request a view, these genes will be centered in each context but no alignment of the tracks will be performed.",
          element: "#legend",
          placement: "left",
      onShow: function(tour) {
        // if the URL is not matching lis_context_viewer
        if (document.location.href.indexOf(EXAMPLE_URL) === -1) {
          console.log('redirecting to example context viewer: '+ EXAMPLE_URL);
          document.location.href = EXAMPLE_URL;
          return (new jQuery.Deferred()).promise();
        }
      }
         },
        {
          title: "Gene Families",
          content: "Since genes in a family tend to have relatively similar sequences, we can use them to predict the functions of newly identified genes based on their relations to other known genes, especially in cases where the genes are found in similar syntenic contexts. Gene family colors in the legend will display all representatives of the family in the current view when moused-over, or when clicked will list those genes with the option to view them in the context of the family's phylogenetic tree. ",
          element: "#legend-content",
          placement: "left"
           },
        {
          title: "Context Viewer",
          content: "The context viewer displays corresponding regions around a selected gene or set of genes in a subtree. It makes it easy to find functional gene groups as well as help make hypotheses about their evolutionary histories. Gene glyphs have mouse over and click interactivity. If a glyph is moused over its name and genomic position are shown. If a gene is clicked a widget will appear with a variety of links related to the gene, such as a link to the source of the gene annotation. The thicker the connecting line between the genes, the longer the intergenic distance on the chromosome; intergenic distances and gene identities for tracks can also be displayed by mousing over the track labels to the left of the tracks.",
          element: ".rail:last",
          placement: "bottom",
         },
        {
          title: "Dot Plots",
                content: "Dot plots are useful in identifying interspersed, lost, or gained repeat motifs.",
          element: ".axis_right",
          placement: "top",
          multipage: true,
          onNext: function() {
              $.fn.d3Click = function () {
                this.each(function (i, e) {
                  var evt = new MouseEvent('click');
                  e.dispatchEvent(evt);
                });
              };
              $('.axis_right text:first').d3Click();
              var promise = lisTours.waitForContent(
                tour,
                function() {
                  return $("#plot")[0];
                });
              return promise;
           }
        },
        {
         title: "The Plot Thickens",
         content: "The main diagonal shows the sequence's alignment with itself, while patterns off the main diagonal indicate structural similarities among different parts of the sequences. Lines off the main diagonal imply different regions with a high similarity.",
         element: "local-plot",
         placement: "left",
         onNext: function() {
             $('#gloplot')[0].click();
              $.fn.d3Click = function () {
                this.each(function (i, e) {
                  var evt = new MouseEvent('click');
                  e.dispatchEvent(evt);
                });
              };
             $('.axis_right text:first').d3Click();

              var promise = lisTours.waitForContent(
                tour,
                function() {
                  return $("#global-plot")[0];
                });
              return promise;
           }
        },
        {
         title: "Global Plots",
         content: "Just like the local plot but instead of focusing only on the matched syntenic segment, the global plot displays all instances of genes from the families of the query track in across the chromosome from which the matched syntenic segment was taken. This gives a better sense for the frequency with which members of these families occur outside the matched context and can reveal wider syntenic properties.",
         element: "#global-plot",
         placement: "left"
        },
        {
         title: "Parameters",
         content: "These parameters allow you to fine-tune the candidate tracks retrieved and the alignments produced from them.",
         element: "#parambtn",
         placement: "bottom",
         onNext: function() {
            $('#parambtn')[0].click();
            if (! $('#left-slider').is(':visible')) {
            }
         }
        },
        {
         title: "Neighbors",
         content: "The 'Neighbor' value controls the number of genes that surround the central gene. By default, the regions extend out by 8 genes upstream and down from the selected genes. Increasing this value will give you longer contexts with more sensitivity for finding distant matches, but will increase retrieval times and may make the display harder to interpret.",
         element: "#neighborpane", //Should point to the field input, but depends on the size/shape of the window.
         placement: "right",
        },
        {
         title: "Scroll Control",
         content: "The scroll input is used to scroll in either direction on the query track's chromosome. In other words, given a scroll distance and direction from the current focus gene, a new query is made with the track built around the new focus found with these parameters. The allowed scroll values are constrained so that the new focus gene after scrolling is present in the context before scrolling.",
         element: "#form-wrapper",
         placement: "right"
            },
        {
         title: "Algorithms",
         content: "Synteny between tracks is determined via a modified Smith-Waterman or Repeat alignment algorithm. For Smith-Waterman, the orientation (forward/reverse) with the higher score is displayed. For the Repeat algorithm, all alignments are kept and displayed as related tracks. This has the advantage of nicely capturing inversions.",
         element: "#algpar",
         placement: "right",
        },
        {
         title: "Alerts",
         content: "A context search is performed by querying a database for tracks with similar gene family content to a query track. The result tracks found are then aligned to the query track using alignment based on common family memberships. The search view displays the query track and the alignments that meet the specified alignment criteria. If the number of tracks returned exceeds the number aligned, you may consider altering the Alignment Parameters to see if you are missing out on some more complex syntenic relationships.",
         element: "#alerts",
         placement: "bottom",
         arrowOffset: "center"
        },
        {
         title: "Want to know more?",
         content: "This\
         documentation link will take you back to the LIS homepage.",
         element: "#helpbtn",
         placement: "bottom"
         }
    ],
  });

  lisTours.register(tour);

}(window.__jquery));

<|endoftext|>


<commit_before>import {
  merge,
  length,
  find,
  propEq,
  without,
  contains,
  forEach,
  pluck,
  reject
} from 'ramda'
import Commands from './commands'
import validate from './validation'
import { observable, computed, asFlat } from 'mobx'
import { repair } from './repairSerialization'
import WebSocket from 'ws'

const DEFAULTS = {
  port: 9090, // the port to live (required)
  onCommand: command => null, // handles inbound commands
  onStart: () => null, // handles inbound commands
  onStop: () => null, // handles inbound commands
  onConnect: connection => null, // notify connections
  onDisconnect: connection => null // notify disconnections
}

class Server {
  // the configuration options
  @observable options = merge({}, DEFAULTS)
  started = false
  messageId = 0
  subscriptions = []
  partialConnections = []
  wss

  /**
   * Holds the commands the client has sent.
   */
  commands = new Commands();

  /**
   * Holds the currently connected clients.
   */
  @observable connections = asFlat([]);

  /**
   * How many people are connected?
   */
  @computed get connectionCount () {
    return length(this.connections)
  }

  constructor (createTransport) {
    this.send = this.send.bind(this)
  }

  findConnectionById = id => find(propEq('id', id), this.connections);
  findPartialConnectionById = id =>
    find(propEq('id', id), this.partialConnections);

  /**
   * Set the configuration options.
   */
  configure (options = {}) {
    // options get merged & validated before getting set
    const newOptions = merge(this.options, options)
    validate(newOptions)
    this.options = newOptions
    return this
  }

  /**
   * Starts the server
   */
  start () {
    this.started = true
    const { port, onStart } = this.options
    const { onCommand, onConnect, onDisconnect } = this.options

    // start listening
    this.wss = new WebSocket.Server({ port })

    // register events
    this.wss.on('connection', socket => {
      // a wild client appears
      const partialConnection = {
        id: socket.id,
        address: 'dunno', // socket.request.connection.remoteAddress,
        socket
      }

      // tuck them away in a "almost connected status"
      this.partialConnections.push(partialConnection)

      // trigger onConnect
      onConnect(partialConnection)

      // when this client disconnects
      socket.on('disconnect', () => {
        onDisconnect(socket.id)
        // remove them from the list partial list
        this.partialConnections = reject(
          propEq('id', socket.id),
          this.partialConnections
        )

        // remove them from the main connections list
        const severingConnection = find(
          propEq('id', socket.id),
          this.connections
        )
        if (severingConnection) {
          this.connections.remove(severingConnection)
          onDisconnect && onDisconnect(severingConnection)
        }
      })

      // when we receive a command from the client
      socket.on('message', incoming => {
        const message = JSON.parse(incoming)
        const { type, important, payload } = message
        this.messageId++
        const date = new Date()
        const fullCommand = {
          type,
          important,
          payload,
          messageId: this.messageId,
          date
        }

        repair(payload)
        // for client intros
        if (type === 'client.intro') {
          // find them in the partial connection list
          const partialConnection = find(
            propEq('id', socket.id),
            this.partialConnections
          )

          // add their address in
          fullCommand.payload.address = partialConnection.address

          // remove them from the partial connections list
          this.partialConnections = reject(
            propEq('id', socket.id),
            this.partialConnections
          )

          // bestow the payload onto the connection
          const connection = merge(payload, {
            id: socket.id,
            address: partialConnection.address
          })

          // then trigger the connection
          this.connections.push(connection)
        }

        // refresh subscriptions
        if (type === 'state.values.change') {
          this.subscriptions = pluck('path', payload.changes || [])
        }

        // assign a name to the backups since the client doesn't pass one.  without it, we have to
        // call extendObservable instead of a standard assignment, which is very confusing.
        if (type === 'state.backup.response') {
          fullCommand.payload.name = null
        }

        // clear
        if (type === 'clear') {
          this.commands.all.clear()
        } else {
          this.commands.addCommand(fullCommand)
          onCommand(fullCommand)
        }
      })

      // resend the subscriptions to the client upon connecting
      this.stateValuesSendSubscriptions()
    })

    // trigger the start message
    onStart && onStart()

    return this
  }

  /**
   * Stops the server
   */
  stop () {
    const { onStop } = this.options
    this.started = false
    forEach(
      s => s && s.connected && s.disconnect(),
      pluck('socket', this.connections)
    )
    this.wss.close()

    // trigger the stop message
    onStop && onStop()

    return this
  }

  /**
   * Sends a command to the client
   */
  send (type, payload) {
    this.wss.clients.forEach(client => {
      if (client.readyState === WebSocket.OPEN) {
        client.send(JSON.stringify({ type, payload }))
      }
    })
  }

  /**
   * Sends a request to the client for state values.
   */
  stateValuesRequest (path) {
    this.send('state.values.request', { path })
  }

  /**
   * Sends a request to the client for keys for a state object.
   */
  stateKeysRequest (path) {
    this.send('state.keys.request', { path })
  }

  /**
   * Dispatches an action through to the state.
   */
  stateActionDispatch (action) {
    this.send('state.action.dispatch', { action })
  }

  /**
   * Sends a list of subscribed paths to the client for state subscription.
   */
  stateValuesSendSubscriptions () {
    this.send('state.values.subscribe', { paths: this.subscriptions })
  }

  /**
   * Subscribe to a path in the client's state.
   */
  stateValuesSubscribe (path) {
    // prevent duplicates
    if (contains(path, this.subscriptions)) return
    // subscribe
    this.subscriptions.push(path)
    this.stateValuesSendSubscriptions()
  }

  /**
   * Unsubscribe from this path.
   */
  stateValuesUnsubscribe (path) {
    // if it doesn't exist, jet
    if (!contains(path, this.subscriptions)) return
    this.subscriptions = without([path], this.subscriptions)
    this.stateValuesSendSubscriptions()
  }

  /**
   * Clears the subscriptions.
   */
  stateValuesClearSubscriptions () {
    this.subscriptions = []
    this.stateValuesSendSubscriptions()
  }

  /**
   * Asks the client for a copy of the current state.
   */
  stateBackupRequest () {
    this.send('state.backup.request', {})
  }

  /**
   * Asks the client to substitute this new state.  Good luck!  Hope it is compatible!
   */
  stateRestoreRequest (state) {
    this.send('state.restore.request', { state })
  }

  /**
   * Sends a request for the client to open the file in editor.
   */
  openInEditor (details) {
    const { file, lineNumber } = details
    this.send('editor.open', { file, lineNumber })
  }
}

export default Server

// convenience factory function
export const createServer = options => {
  const server = new Server()
  server.configure(options)
  return server
}
<commit_msg>Cleanup the entire message! (#462)<commit_after>import {
  merge,
  length,
  find,
  propEq,
  without,
  contains,
  forEach,
  pluck,
  reject
} from 'ramda'
import Commands from './commands'
import validate from './validation'
import { observable, computed, asFlat } from 'mobx'
import { repair } from './repairSerialization'
import WebSocket from 'ws'

const DEFAULTS = {
  port: 9090, // the port to live (required)
  onCommand: command => null, // handles inbound commands
  onStart: () => null, // handles inbound commands
  onStop: () => null, // handles inbound commands
  onConnect: connection => null, // notify connections
  onDisconnect: connection => null // notify disconnections
}

class Server {
  // the configuration options
  @observable options = merge({}, DEFAULTS)
  started = false
  messageId = 0
  subscriptions = []
  partialConnections = []
  wss

  /**
   * Holds the commands the client has sent.
   */
  commands = new Commands();

  /**
   * Holds the currently connected clients.
   */
  @observable connections = asFlat([]);

  /**
   * How many people are connected?
   */
  @computed get connectionCount () {
    return length(this.connections)
  }

  constructor (createTransport) {
    this.send = this.send.bind(this)
  }

  findConnectionById = id => find(propEq('id', id), this.connections);
  findPartialConnectionById = id =>
    find(propEq('id', id), this.partialConnections);

  /**
   * Set the configuration options.
   */
  configure (options = {}) {
    // options get merged & validated before getting set
    const newOptions = merge(this.options, options)
    validate(newOptions)
    this.options = newOptions
    return this
  }

  /**
   * Starts the server
   */
  start () {
    this.started = true
    const { port, onStart } = this.options
    const { onCommand, onConnect, onDisconnect } = this.options

    // start listening
    this.wss = new WebSocket.Server({ port })

    // register events
    this.wss.on('connection', socket => {
      // a wild client appears
      const partialConnection = {
        id: socket.id,
        address: 'dunno', // socket.request.connection.remoteAddress,
        socket
      }

      // tuck them away in a "almost connected status"
      this.partialConnections.push(partialConnection)

      // trigger onConnect
      onConnect(partialConnection)

      // when this client disconnects
      socket.on('disconnect', () => {
        onDisconnect(socket.id)
        // remove them from the list partial list
        this.partialConnections = reject(
          propEq('id', socket.id),
          this.partialConnections
        )

        // remove them from the main connections list
        const severingConnection = find(
          propEq('id', socket.id),
          this.connections
        )
        if (severingConnection) {
          this.connections.remove(severingConnection)
          onDisconnect && onDisconnect(severingConnection)
        }
      })

      // when we receive a command from the client
      socket.on('message', incoming => {
        const message = JSON.parse(incoming)
        repair(message)
        const { type, important, payload } = message
        this.messageId++
        const date = new Date()
        const fullCommand = {
          type,
          important,
          payload,
          messageId: this.messageId,
          date
        }

        // for client intros
        if (type === 'client.intro') {
          // find them in the partial connection list
          const partialConnection = find(
            propEq('id', socket.id),
            this.partialConnections
          )

          // add their address in
          fullCommand.payload.address = partialConnection.address

          // remove them from the partial connections list
          this.partialConnections = reject(
            propEq('id', socket.id),
            this.partialConnections
          )

          // bestow the payload onto the connection
          const connection = merge(payload, {
            id: socket.id,
            address: partialConnection.address
          })

          // then trigger the connection
          this.connections.push(connection)
        }

        // refresh subscriptions
        if (type === 'state.values.change') {
          this.subscriptions = pluck('path', payload.changes || [])
        }

        // assign a name to the backups since the client doesn't pass one.  without it, we have to
        // call extendObservable instead of a standard assignment, which is very confusing.
        if (type === 'state.backup.response') {
          fullCommand.payload.name = null
        }

        // clear
        if (type === 'clear') {
          this.commands.all.clear()
        } else {
          this.commands.addCommand(fullCommand)
          onCommand(fullCommand)
        }
      })

      // resend the subscriptions to the client upon connecting
      this.stateValuesSendSubscriptions()
    })

    // trigger the start message
    onStart && onStart()

    return this
  }

  /**
   * Stops the server
   */
  stop () {
    const { onStop } = this.options
    this.started = false
    forEach(
      s => s && s.connected && s.disconnect(),
      pluck('socket', this.connections)
    )
    this.wss.close()

    // trigger the stop message
    onStop && onStop()

    return this
  }

  /**
   * Sends a command to the client
   */
  send (type, payload) {
    this.wss.clients.forEach(client => {
      if (client.readyState === WebSocket.OPEN) {
        client.send(JSON.stringify({ type, payload }))
      }
    })
  }

  /**
   * Sends a request to the client for state values.
   */
  stateValuesRequest (path) {
    this.send('state.values.request', { path })
  }

  /**
   * Sends a request to the client for keys for a state object.
   */
  stateKeysRequest (path) {
    this.send('state.keys.request', { path })
  }

  /**
   * Dispatches an action through to the state.
   */
  stateActionDispatch (action) {
    this.send('state.action.dispatch', { action })
  }

  /**
   * Sends a list of subscribed paths to the client for state subscription.
   */
  stateValuesSendSubscriptions () {
    this.send('state.values.subscribe', { paths: this.subscriptions })
  }

  /**
   * Subscribe to a path in the client's state.
   */
  stateValuesSubscribe (path) {
    // prevent duplicates
    if (contains(path, this.subscriptions)) return
    // subscribe
    this.subscriptions.push(path)
    this.stateValuesSendSubscriptions()
  }

  /**
   * Unsubscribe from this path.
   */
  stateValuesUnsubscribe (path) {
    // if it doesn't exist, jet
    if (!contains(path, this.subscriptions)) return
    this.subscriptions = without([path], this.subscriptions)
    this.stateValuesSendSubscriptions()
  }

  /**
   * Clears the subscriptions.
   */
  stateValuesClearSubscriptions () {
    this.subscriptions = []
    this.stateValuesSendSubscriptions()
  }

  /**
   * Asks the client for a copy of the current state.
   */
  stateBackupRequest () {
    this.send('state.backup.request', {})
  }

  /**
   * Asks the client to substitute this new state.  Good luck!  Hope it is compatible!
   */
  stateRestoreRequest (state) {
    this.send('state.restore.request', { state })
  }

  /**
   * Sends a request for the client to open the file in editor.
   */
  openInEditor (details) {
    const { file, lineNumber } = details
    this.send('editor.open', { file, lineNumber })
  }
}

export default Server

// convenience factory function
export const createServer = options => {
  const server = new Server()
  server.configure(options)
  return server
}
<|endoftext|>


<commit_before>/* vim: ts=4:sw=4:expandtab
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
(function () {
    'use strict';
    window.Whisper = window.Whisper || {};

    Whisper.NewGroupUpdateView = Whisper.View.extend({
        tagName:   "div",
        className: "new-group-update-form",
        template: $('#new-group-update-form').html(),
        initialize: function(options) {
            this.render();
            this.avatarInput = new Whisper.FileInputView({
                el: this.$('.group-avatar'),
                window: options.window
            });

            this.recipients_view = new Whisper.RecipientsInputView({
                placeholder: "Add member"
            });
            this.listenTo(this.recipients_view.typeahead, 'sync', function() {
                this.model.contactCollection.models.forEach(function(model) {
                    if (this.recipients_view.typeahead.get(model)) {
                        this.recipients_view.typeahead.remove(model);
                    }
                }.bind(this));
            });
            this.$('.scrollable').append(this.recipients_view.el);

            this.$('.avatar').addClass('default');

            this.member_list_view = new Whisper.ConversationListView({
                collection: this.model.contactCollection,
                className: 'members'
            });
            this.member_list_view.render();
            this.$('.scrollable').append(this.member_list_view.el);
        },
        events: {
            'click .back': 'goBack',
            'click .send': 'send',
            'keyup input.search': 'toggleResults'
        },
        toggleResults: function() {
            if (this.recipients_view.$input.val().length >= 2) {
                this.$('.results').show();
            } else {
                this.$('.results').hide();
            }
        },
        goBack: function() {
            this.trigger('back');
        },
        render_attributes: function() {
            return {
                name: this.model.getTitle(),
                avatar: {url: this.model.getAvatar()}
            };
        },
        send: function() {
            return this.avatarInput.getThumbnail().then(function(avatarFile) {
                this.model.set({
                    name: this.$('.name').val(),
                    members: _.union(this.model.get('members'), this.recipients_view.recipients.pluck('id'))
                });
                if (avatarFile) {
                    this.model.set({avatar: avatarFile});
                    this.model.trigger('change:avatar');
                }
                this.model.save();
                textsecure.messaging.updateGroup(
                    this.model.id,
                    this.model.get('name'),
                    this.model.get('avatar'),
                    this.model.get('members')
                );
                this.goBack();
            }.bind(this));
        }
    });
})();
<commit_msg>Save outgoing group updates<commit_after>/* vim: ts=4:sw=4:expandtab
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
(function () {
    'use strict';
    window.Whisper = window.Whisper || {};

    Whisper.NewGroupUpdateView = Whisper.View.extend({
        tagName:   "div",
        className: "new-group-update-form",
        template: $('#new-group-update-form').html(),
        initialize: function(options) {
            this.render();
            this.avatarInput = new Whisper.FileInputView({
                el: this.$('.group-avatar'),
                window: options.window
            });

            this.recipients_view = new Whisper.RecipientsInputView({
                placeholder: "Add member"
            });
            this.listenTo(this.recipients_view.typeahead, 'sync', function() {
                this.model.contactCollection.models.forEach(function(model) {
                    if (this.recipients_view.typeahead.get(model)) {
                        this.recipients_view.typeahead.remove(model);
                    }
                }.bind(this));
            });
            this.$('.scrollable').append(this.recipients_view.el);

            this.$('.avatar').addClass('default');

            this.member_list_view = new Whisper.ConversationListView({
                collection: this.model.contactCollection,
                className: 'members'
            });
            this.member_list_view.render();
            this.$('.scrollable').append(this.member_list_view.el);
        },
        events: {
            'click .back': 'goBack',
            'click .send': 'send',
            'keyup input.search': 'toggleResults'
        },
        toggleResults: function() {
            if (this.recipients_view.$input.val().length >= 2) {
                this.$('.results').show();
            } else {
                this.$('.results').hide();
            }
        },
        goBack: function() {
            this.trigger('back');
        },
        render_attributes: function() {
            return {
                name: this.model.getTitle(),
                avatar: {url: this.model.getAvatar()}
            };
        },
        send: function() {
            return this.avatarInput.getThumbnail().then(function(avatarFile) {
                var attrs = {
                    name: this.$('.name').val(),
                    members: _.union(this.model.get('members'), this.recipients_view.recipients.pluck('id'))
                };
                if (avatarFile) {
                    attrs.avatar = avatarFile;
                }
                this.model.set(attrs);
                var group_update = this.model.changed;
                this.model.save();

                if (group_update.avatar) {
                    this.model.trigger('change:avatar');
                }

                var now = Date.now();
                var message = this.model.messageCollection.add({
                    conversationId : this.model.id,
                    type           : 'outgoing',
                    sent_at        : now,
                    received_at    : now,
                    group_update   : group_update
                });
                message.save();
                textsecure.messaging.updateGroup(
                    this.model.id,
                    this.model.get('name'),
                    this.model.get('avatar'),
                    this.model.get('members')
                ).catch(function(errors) {
                    message.save({errors: errors.map(function(e){return e.error;})});
                }).then(function() {
                    message.save({sent: true});
                });

                this.goBack();
            }.bind(this));
        }
    });
})();
<|endoftext|>


<commit_before>import express from 'express';
import UserController from '../controllers/UserController';

const userRouter = express.Router();
userRouter.post('/api/user/signup', UserController.createUser());
userRouter.post('/api/user/signin', [
  UserController.validateRequest(),
  UserController.authenticateUser()
]);
userRouter.use('/api', [
  UserController.getClientAuthToken(),
  UserController.authorizeUser()
]);
userRouter.get('/api/user/:id', UserController.getUser());
userRouter.put('/api/user', UserController.updateUser());
userRouter.delete('/api/user', UserController.deleteUser());
userRouter.get('/api/user/groups', UserController.getUserGroups());

export default userRouter;
<commit_msg>[fix]: Change route param from id to username<commit_after>import express from 'express';
import UserController from '../controllers/UserController';

const userRouter = express.Router();
userRouter.post('/api/user/signup', UserController.createUser());
userRouter.post('/api/user/signin', [
  UserController.validateRequest(),
  UserController.authenticateUser()
]);
userRouter.use('/api', [
  UserController.getClientAuthToken(),
  UserController.authorizeUser()
]);
userRouter.get('/api/user/:username', UserController.getUser());
userRouter.put('/api/user', UserController.updateUser());
userRouter.delete('/api/user', UserController.deleteUser());
userRouter.get('/api/user/groups', UserController.getUserGroups());

export default userRouter;
<|endoftext|>