<commit_before>var RingBuffer = require('./ring-buffer.js');
var SonicCoder = require('./sonic-coder.js');

var audioContext = window.audioContext || new webkitAudioContext();
/**
 * Extracts meaning from audio streams.
 *
 * (assumes audioContext is a WebAudioContext global variable.)
 *
 * 1. Listen to the microphone.
 * 2. Do an FFT on the input.
 * 3. Extract frequency peaks in the ultrasonic range.
 * 4. Keep track of frequency peak history in a ring buffer.
 * 5. Call back when a peak comes up often enough.
 */
function SonicServer(params) {
  params = params || {};
  this.peakThreshold = params.peakThreshold || -65;
  this.minRunLength = params.minRunLength || 2;
  this.coder = params.coder || new SonicCoder(params);
  // How long (in ms) to wait for the next character.
  this.timeout = params.timeout || 300;
  this.debug = !!params.debug;

  this.peakHistory = new RingBuffer(16);
  this.peakTimes = new RingBuffer(16);

  this.callbacks = {};

  this.buffer = '';
  this.state = State.IDLE;
  this.isRunning = false;
  this.iteration = 0;
}

var State = {
  IDLE: 1,
  RECV: 2
};

/**
 * Start processing the audio stream.
 */
SonicServer.prototype.start = function() {
  // Start listening for microphone. Continue init in onStream.
  var constraints = {
    audio: { optional: [{ echoCancellation: false }] }
  };
  navigator.webkitGetUserMedia(constraints,
      this.onStream_.bind(this), this.onStreamError_.bind(this));
};

/**
 * Stop processing the audio stream.
 */
SonicServer.prototype.stop = function() {
  this.isRunning = false;
  this.stream.stop();
};

SonicServer.prototype.on = function(event, callback) {
  if (event == 'message') {
    this.callbacks.message = callback;
  }
};

SonicServer.prototype.setDebug = function(value) {
  this.debug = value;

  var canvas = document.querySelector('canvas');
  if (canvas) {
    // Remove it.
    canvas.parentElement.removeChild(canvas);
  }
};

SonicServer.prototype.fire_ = function(callback, arg) {
  callback(arg);
};

SonicServer.prototype.onStream_ = function(stream) {
  this.stream = stream;
  // Setup audio graph.
  var input = audioContext.createMediaStreamSource(stream);
  var analyser = audioContext.createAnalyser();
  input.connect(analyser);
  // Create the frequency array.
  this.freqs = new Float32Array(analyser.frequencyBinCount);
  // Save the analyser for later.
  this.analyser = analyser;
  this.isRunning = true;
  // Do an FFT and check for inaudible peaks.
  this.raf_(this.loop.bind(this));
};

SonicServer.prototype.onStreamError_ = function(e) {
  console.error('Audio input error:', e);
};

/**
 * Given an FFT frequency analysis, return the peak frequency in a frequency
 * range.
 */
SonicServer.prototype.getPeakFrequency = function() {
  // Find where to start.
  var start = this.freqToIndex(this.coder.freqMin);
  // TODO: use first derivative to find the peaks, and then find the largest peak.
  // Just do a max over the set.
  var max = -Infinity;
  var index = -1;
  for (var i = start; i < this.freqs.length; i++) {
    if (this.freqs[i] > max) {
      max = this.freqs[i];
      index = i;
    }
  }
  // Only care about sufficiently tall peaks.
  if (max > this.peakThreshold) {
    return this.indexToFreq(index);
  }
  return null;
};

SonicServer.prototype.loop = function() {
  this.analyser.getFloatFrequencyData(this.freqs);
  // Sanity check the peaks every 5 seconds.
  if ((this.iteration + 1) % (60 * 5) == 0) {
    this.restartServerIfSanityCheckFails();
  }
  // Calculate peaks, and add them to history.
  var freq = this.getPeakFrequency();
  if (freq) {
    var char = this.coder.freqToChar(freq);
    // DEBUG ONLY: Output the transcribed char.
    if (this.debug) {
      console.log('Transcribed char: ' + char);
    }
    this.peakHistory.add(char);
    this.peakTimes.add(new Date());
  } else {
    // If no character was detected, see if we've timed out.
    var lastPeakTime = this.peakTimes.last();
    if (lastPeakTime && new Date() - lastPeakTime > this.timeout) {
      // Last detection was over 300ms ago.
      this.state = State.IDLE;
      if (this.debug) {
        console.log('Token', this.buffer, 'timed out');
      }
      this.peakTimes.clear();
    }
  }
  // Analyse the peak history.
  this.analysePeaks();
  // DEBUG ONLY: Draw the frequency response graph.
  if (this.debug) {
    this.debugDraw_();
  }
  if (this.isRunning) {
    this.raf_(this.loop.bind(this));
  }
  this.iteration += 1;
};

SonicServer.prototype.indexToFreq = function(index) {
  var nyquist = audioContext.sampleRate/2;
  return nyquist/this.freqs.length * index;
};

SonicServer.prototype.freqToIndex = function(frequency) {
  var nyquist = audioContext.sampleRate/2;
  return Math.round(frequency/nyquist * this.freqs.length);
};

/**
 * Analyses the peak history to find true peaks (repeated over several frames).
 */
SonicServer.prototype.analysePeaks = function() {
  // Look for runs of repeated characters.
  var char = this.getLastRun();
  if (!char) {
    return;
  }
  if (this.state == State.IDLE) {
    // If idle, look for start character to go into recv mode.
    if (char == this.coder.startChar) {
      this.buffer = '';
      this.state = State.RECV;
    }
  } else if (this.state == State.RECV) {
    // If receiving, look for character changes.
    if (char != this.lastChar &&
        char != this.coder.startChar && char != this.coder.endChar) {
      this.buffer += char;
      this.lastChar = char;
    }
    // Also look for the end character to go into idle mode.
    if (char == this.coder.endChar) {
      this.state = State.IDLE;
      this.fire_(this.callbacks.message, this.buffer);
      this.buffer = '';
    }
  }
};

SonicServer.prototype.getLastRun = function() {
  var lastChar = this.peakHistory.last();
  var runLength = 0;
  // Look at the peakHistory array for patterns like ajdlfhlkjxxxxxx$.
  for (var i = this.peakHistory.length() - 2; i >= 0; i--) {
    var char = this.peakHistory.get(i);
    if (char == lastChar) {
      runLength += 1;
    } else {
      break;
    }
  }
  if (runLength > this.minRunLength) {
    // Remove it from the buffer.
    this.peakHistory.remove(i + 1, runLength + 1);
    return lastChar;
  }
  return null;
};

/**
 * DEBUG ONLY.
 */
SonicServer.prototype.debugDraw_ = function() {
  var canvas = document.querySelector('canvas');
  if (!canvas) {
    canvas = document.createElement('canvas');
    document.body.appendChild(canvas);
  }
  canvas.width = document.body.offsetWidth;
  canvas.height = 480;
  drawContext = canvas.getContext('2d');
  // Plot the frequency data.
  for (var i = 0; i < this.freqs.length; i++) {
    var value = this.freqs[i];
    // Transform this value (in db?) into something that can be plotted.
    var height = value + 400;
    var offset = canvas.height - height - 1;
    var barWidth = canvas.width/this.freqs.length;
    drawContext.fillStyle = 'black';
    drawContext.fillRect(i * barWidth, offset, 1, 1);
  }
};

/**
 * A request animation frame shortcut. This one is intended to work even in
 * background pages of an extension.
 */
SonicServer.prototype.raf_ = function(callback) {
  var isCrx = !!(window.chrome && chrome.extension);
  if (isCrx) {
    setTimeout(callback, 1000/60);
  } else {
    requestAnimationFrame(callback);
  }
};

SonicServer.prototype.restartServerIfSanityCheckFails = function() {
  // Strange state 1: peaks gradually get quieter and quieter until they
  // stabilize around -800.
  if (this.freqs[0] < -300) {
    console.error('freqs[0] < -300. Restarting.');
    this.restart();
    return;
  }
  // Strange state 2: all of the peaks are -100. Check just the first few.
  var isValid = true;
  for (var i = 0; i < 10; i++) {
    if (this.freqs[i] == -100) {
      isValid = false;
    }
  }
  if (!isValid) {
    console.error('freqs[0:10] == -100. Restarting.');
    this.restart();
  }
}

SonicServer.prototype.restart = function() {
  //this.stop();
  //this.start();
  window.location.reload();
};

module.exports = SonicServer;
<commit_msg>add callback for individual character decoding<commit_after>var RingBuffer = require('./ring-buffer.js');
var SonicCoder = require('./sonic-coder.js');

var audioContext = window.audioContext || new webkitAudioContext();
/**
 * Extracts meaning from audio streams.
 *
 * (assumes audioContext is a WebAudioContext global variable.)
 *
 * 1. Listen to the microphone.
 * 2. Do an FFT on the input.
 * 3. Extract frequency peaks in the ultrasonic range.
 * 4. Keep track of frequency peak history in a ring buffer.
 * 5. Call back when a peak comes up often enough.
 */
function SonicServer(params) {
  params = params || {};
  this.peakThreshold = params.peakThreshold || -65;
  this.minRunLength = params.minRunLength || 2;
  this.coder = params.coder || new SonicCoder(params);
  // How long (in ms) to wait for the next character.
  this.timeout = params.timeout || 300;
  this.debug = !!params.debug;

  this.peakHistory = new RingBuffer(16);
  this.peakTimes = new RingBuffer(16);

  this.callbacks = {};

  this.buffer = '';
  this.state = State.IDLE;
  this.isRunning = false;
  this.iteration = 0;
}

var State = {
  IDLE: 1,
  RECV: 2
};

/**
 * Start processing the audio stream.
 */
SonicServer.prototype.start = function() {
  // Start listening for microphone. Continue init in onStream.
  var constraints = {
    audio: { optional: [{ echoCancellation: false }] }
  };
  navigator.webkitGetUserMedia(constraints,
      this.onStream_.bind(this), this.onStreamError_.bind(this));
};

/**
 * Stop processing the audio stream.
 */
SonicServer.prototype.stop = function() {
  this.isRunning = false;
  this.stream.stop();
};

SonicServer.prototype.on = function(event, callback) {
  if (event == 'message') {
    this.callbacks.message = callback;
  }
  if (event == 'character') {
    this.callbacks.character = callback;
  }
};

SonicServer.prototype.setDebug = function(value) {
  this.debug = value;

  var canvas = document.querySelector('canvas');
  if (canvas) {
    // Remove it.
    canvas.parentElement.removeChild(canvas);
  }
};

SonicServer.prototype.fire_ = function(callback, arg) {
  callback(arg);
};

SonicServer.prototype.onStream_ = function(stream) {
  this.stream = stream;
  // Setup audio graph.
  var input = audioContext.createMediaStreamSource(stream);
  var analyser = audioContext.createAnalyser();
  input.connect(analyser);
  // Create the frequency array.
  this.freqs = new Float32Array(analyser.frequencyBinCount);
  // Save the analyser for later.
  this.analyser = analyser;
  this.isRunning = true;
  // Do an FFT and check for inaudible peaks.
  this.raf_(this.loop.bind(this));
};

SonicServer.prototype.onStreamError_ = function(e) {
  console.error('Audio input error:', e);
};

/**
 * Given an FFT frequency analysis, return the peak frequency in a frequency
 * range.
 */
SonicServer.prototype.getPeakFrequency = function() {
  // Find where to start.
  var start = this.freqToIndex(this.coder.freqMin);
  // TODO: use first derivative to find the peaks, and then find the largest peak.
  // Just do a max over the set.
  var max = -Infinity;
  var index = -1;
  for (var i = start; i < this.freqs.length; i++) {
    if (this.freqs[i] > max) {
      max = this.freqs[i];
      index = i;
    }
  }
  // Only care about sufficiently tall peaks.
  if (max > this.peakThreshold) {
    return this.indexToFreq(index);
  }
  return null;
};

SonicServer.prototype.loop = function() {
  this.analyser.getFloatFrequencyData(this.freqs);
  // Sanity check the peaks every 5 seconds.
  if ((this.iteration + 1) % (60 * 5) == 0) {
    this.restartServerIfSanityCheckFails();
  }
  // Calculate peaks, and add them to history.
  var freq = this.getPeakFrequency();
  if (freq) {
    var char = this.coder.freqToChar(freq);
    // DEBUG ONLY: Output the transcribed char.
    if (this.debug) {
      console.log('Transcribed char: ' + char);
    }
    this.peakHistory.add(char);
    this.peakTimes.add(new Date());
  } else {
    // If no character was detected, see if we've timed out.
    var lastPeakTime = this.peakTimes.last();
    if (lastPeakTime && new Date() - lastPeakTime > this.timeout) {
      // Last detection was over 300ms ago.
      this.state = State.IDLE;
      if (this.debug) {
        console.log('Token', this.buffer, 'timed out');
      }
      this.peakTimes.clear();
    }
  }
  // Analyse the peak history.
  this.analysePeaks();
  // DEBUG ONLY: Draw the frequency response graph.
  if (this.debug) {
    this.debugDraw_();
  }
  if (this.isRunning) {
    this.raf_(this.loop.bind(this));
  }
  this.iteration += 1;
};

SonicServer.prototype.indexToFreq = function(index) {
  var nyquist = audioContext.sampleRate/2;
  return nyquist/this.freqs.length * index;
};

SonicServer.prototype.freqToIndex = function(frequency) {
  var nyquist = audioContext.sampleRate/2;
  return Math.round(frequency/nyquist * this.freqs.length);
};

/**
 * Analyses the peak history to find true peaks (repeated over several frames).
 */
SonicServer.prototype.analysePeaks = function() {
  // Look for runs of repeated characters.
  var char = this.getLastRun();
  if (!char) {
    return;
  }
  if (this.state == State.IDLE) {
    // If idle, look for start character to go into recv mode.
    if (char == this.coder.startChar) {
      this.buffer = '';
      this.state = State.RECV;
    }
  } else if (this.state == State.RECV) {
    // If receiving, look for character changes.
    if (char != this.lastChar &&
        char != this.coder.startChar && char != this.coder.endChar) {
      this.buffer += char;
      this.lastChar = char;
      this.fire_(this.callbacks.character, char);
    }
    // Also look for the end character to go into idle mode.
    if (char == this.coder.endChar) {
      this.state = State.IDLE;
      this.fire_(this.callbacks.message, this.buffer);
      this.buffer = '';
    }
  }
};

SonicServer.prototype.getLastRun = function() {
  var lastChar = this.peakHistory.last();
  var runLength = 0;
  // Look at the peakHistory array for patterns like ajdlfhlkjxxxxxx$.
  for (var i = this.peakHistory.length() - 2; i >= 0; i--) {
    var char = this.peakHistory.get(i);
    if (char == lastChar) {
      runLength += 1;
    } else {
      break;
    }
  }
  if (runLength > this.minRunLength) {
    // Remove it from the buffer.
    this.peakHistory.remove(i + 1, runLength + 1);
    return lastChar;
  }
  return null;
};

/**
 * DEBUG ONLY.
 */
SonicServer.prototype.debugDraw_ = function() {
  var canvas = document.querySelector('canvas');
  if (!canvas) {
    canvas = document.createElement('canvas');
    document.body.appendChild(canvas);
  }
  canvas.width = document.body.offsetWidth;
  canvas.height = 480;
  drawContext = canvas.getContext('2d');
  // Plot the frequency data.
  for (var i = 0; i < this.freqs.length; i++) {
    var value = this.freqs[i];
    // Transform this value (in db?) into something that can be plotted.
    var height = value + 400;
    var offset = canvas.height - height - 1;
    var barWidth = canvas.width/this.freqs.length;
    drawContext.fillStyle = 'black';
    drawContext.fillRect(i * barWidth, offset, 1, 1);
  }
};

/**
 * A request animation frame shortcut. This one is intended to work even in
 * background pages of an extension.
 */
SonicServer.prototype.raf_ = function(callback) {
  var isCrx = !!(window.chrome && chrome.extension);
  if (isCrx) {
    setTimeout(callback, 1000/60);
  } else {
    requestAnimationFrame(callback);
  }
};

SonicServer.prototype.restartServerIfSanityCheckFails = function() {
  // Strange state 1: peaks gradually get quieter and quieter until they
  // stabilize around -800.
  if (this.freqs[0] < -300) {
    console.error('freqs[0] < -300. Restarting.');
    this.restart();
    return;
  }
  // Strange state 2: all of the peaks are -100. Check just the first few.
  var isValid = true;
  for (var i = 0; i < 10; i++) {
    if (this.freqs[i] == -100) {
      isValid = false;
    }
  }
  if (!isValid) {
    console.error('freqs[0:10] == -100. Restarting.');
    this.restart();
  }
}

SonicServer.prototype.restart = function() {
  //this.stop();
  //this.start();
  window.location.reload();
};

module.exports = SonicServer;
<|endoftext|>


<commit_before>/**
 *
 * @properties={typeid:24,uuid:"FFE14AC8-BECE-4D27-9AC1-0EE22A0032FF"}
 */
function BLOCK_choose()
{
	application.showFormInDialog(forms.WEB_0F__image__P_choose,-1,-1,-1,-1,"Image", false, false, "chooseImage")

	//update display
	if (forms.WEB_0F_page.TRIGGER_mode_set() == "DESIGN") {
		forms.WEB_0F_page__design__content_1L_block.ACTION_set_simple_display()
	}
	else {
		forms.WEB_0F_page__browser__editor.FORM_on_show()
	}
}

/**
 *
 * @properties={typeid:24,uuid:"43C94817-C701-46EF-809A-33BE2CFC738C"}
 */
function BLOCK_scale()
{
	application.showFormInDialog(forms.WEB_0F__image__P_scale,-1,-1,-1,-1,"Image", false, false, "imageScale")

	//update display
	if (forms.WEB_0F_page.TRIGGER_mode_set() == "DESIGN") {
		forms.WEB_0F_page__design__content_1L_block.ACTION_set_simple_display()
	}
	else {
		forms.WEB_0F_page__browser__editor.FORM_on_show()
	}
}

/**
 * Perform the element default action.
 *
 * @param {JSEvent} event the event that triggered the action
 *
 * @properties={typeid:24,uuid:"A1AAE46C-FB9C-4B17-9F80-3684F7D8F81B"}
 */
function ACTION_scale(event) {
	forms.WEB_0B_image.ACTION_image_scale()
}

/**
 *
 * @properties={typeid:24,uuid:"D5507344-C123-4997-A29D-32181865B93F"}
 */
function BLOCK_import()
{
	// TODO: handle reading text files

	// input file
	var file = plugins.file.showFileOpenDialog()
	if ( !file ) {
		return "Selection cancelled"
	}

	var imageTemp =  plugins.images.getImage(file)

	var ext = file.getName().split('.')
	//file has an extension
	if (ext && ext.length > 1) {
		var fileExt = ext[ext.length-1].toLowerCase()
	}

	// set image details object
	var fileOBJ = {}
	fileOBJ.image_name	= file.getName().replace(/ /g, "_")
	fileOBJ.image_type	= file.getContentType()
	fileOBJ.image_extension	= fileExt
	fileOBJ.width		= imageTemp.getWidth()
	fileOBJ.height		= imageTemp.getHeight()
	fileOBJ.width_original		= imageTemp.getWidth()
	fileOBJ.height_original		= imageTemp.getHeight()
	fileOBJ.directory	= "sites/" + forms.WEB_0F_site.directory + "/images/"
	fileOBJ.rec_created = new Date()

	/* copy image details to block data points
	var data = forms.WEB_0F_page__design.web_page_to_block_data_by_area_by_block
	for (var i = 0; i < data.getSize(); i++) {
		var record = data.getRecord(i + 1)
		record.data_value = fileOBJ[record.data_key]
	}
	*/

	// create image record
	var record = forms.WEB_0F_image.foundset.getRecord(forms.WEB_0F_image.foundset.newRecord(true,true))
	record.image_title		= fileOBJ.image_name
	record.image_type		= fileOBJ.image_type
	record.image_size		= plugins.file.getFileSize(file)
	record.directory		= fileOBJ.directory
	record.image_extension	= fileExt
	record.width			= fileOBJ.width
	record.height			= fileOBJ.height
	record.thumbnail		= imageTemp.resize((40*fileOBJ.width)/fileOBJ.height, 40)  // scale to 40px height
	record.id_site			= forms.WEB_0F_site.id_site

	// save file
	// TODO: stream image upload to server from client

	var outputImage			= forms.WEB_0F_install.ACTION_get_install() +
							'/application_server/server/webapps/ROOT/sutraCMS/sites/' +
							forms.WEB_0F_site.directory + '/images/' + fileOBJ.image_name

	// application_server/server/webapps/ROOT/sutraCMS/sites/4community

	if (forms.WEB_0F_install.ACTION_get_server() == "Windows"){
		outputImage		= outputImage.replace(/\//g, "\\")
	}
	var success = plugins.file.copyFile(file, outputImage)
	if ( !success ) {
		return "File save error"
	}
	plugins.dialogs.showInfoDialog("Image",  "Image uploaded")
}

/**
 * param {} obj Data object passed to all markup methods
 * @properties={typeid:24,uuid:"5AABEBFD-5C92-42EA-9C3D-B0135AA33FC8"}
 */
function VIEW_default(obj)
{

	// template
	var markup = 	'<p id="data-<<id_block>>"><img width="<<width>>" height="<<height>>" border="0"' +
					'src="<<directory>><<image_name>>"' +
					'alt="" />
'

	// replace
	markup = markup.replace(/<<id_block>>/ig, obj.block.id)
	markup = markup.replace(/<<width>>/ig, obj.data.width)
	markup = markup.replace(/<<height>>/ig, obj.data.height)
	markup = markup.replace(/<<image_name>>/ig, obj.data.image_name)
	markup = markup.replace(/<<directory>>/ig, obj.data.directory)

	// return
	return markup
}

/**
 * @properties={typeid:24,uuid:"D177F9D8-57D4-4E9C-BC4B-D87BF04B1B00"}
 */
function VIEW_download()
{
}

/**
 * @properties={typeid:24,uuid:"FFB42F41-3AB3-4F26-A006-9FDF03BE1CF7"}
 */
function VIEW_lightbox()
{
}

/**
 *
 * @properties={typeid:24,uuid:"C3D50AA7-5A76-484F-BF22-E86C40042A94"}
 */
function FORM_on_load() {

	/* moved
		var url = "http://localhost:8080" + directory + image_name
		elements.bn_browser.navigateTo(url)
	*/

}

/**
 *
 * @properties={typeid:24,uuid:"581D1472-7339-4669-A110-353A1904B241"}
 */
function TOGGLE_buttons(editStatus) {
	elements.btn_choose.enabled = editStatus
	elements.btn_import.enabled = editStatus
	elements.btn_scale.enabled = editStatus
	elements.lbl_choose.enabled = editStatus
	elements.lbl_import.enabled = editStatus
	elements.lbl_scale.enabled = editStatus
}

/**
 * @properties={typeid:24,uuid:"E9062B39-C69D-4841-A367-94BDC60849FF"}
 */
function LOADER_init(recBlock,flagEdit) {
	//foundset with image datapoints
	var fsBlockData = recBlock.web_block_to_block_data

	//create object with all properties
	var objImage = new Object()
	for (var i = 1; i <= fsBlockData.getSize(); i++) {
		var record = fsBlockData.getRecord(i)
		objImage[record.data_key] = record.data_value
	}

	//no image set yet
	if (!objImage.image_name){
		var html = 	'' +
					'No image chosen yet' +
					''
	}
	// image is set
	else {
		var siteURL = recBlock.web_block_to_area.web_area_to_version.web_version_to_page.web_page_to_site.url

		if (siteURL) {
			siteURL = 'http://' + siteURL

			var port = application.getServerURL()
			port = port.split(':')
			if (port.length > 2) {
				siteURL += ':' + port[2]
			}
		}
		else {
			var siteDirectory = recBlock.web_block_to_area.web_area_to_version.web_version_to_page.web_page_to_site.directory

			siteURL = application.getServerURL() + '/' + siteDirectory
		}

		var html = 	'' +
					'' +
					''
	}

	forms.WEB_0F__image.elements.bn_browser.html = html
	forms.WEB_0F_page__design__content_1F_block_data.elements.tab_detail.removeTabAt(2)
	forms.WEB_0F_page__design__content_1F_block_data.elements.tab_detail.addTab(forms.WEB_0F__image)
	forms.WEB_0F_page__design__content_1F_block_data.elements.tab_detail.tabIndex = 2

	forms.WEB_0F__image.TOGGLE_buttons(flagEdit)
}

/**
 * @properties={typeid:24,uuid:"5896D844-D8AA-4C31-84B2-D77EAE012F1D"}
 */
function INIT_block() {

	// main data object to build
	var block = {}

	// function to read methods on this form. naming conventions supply method categories and names
	function getMethods(type) {
		var methods = allmethods
		var clientActionsBlock = {}
		var clientActionsPage = {}
		var webActions = {}
		var views = {}
		for (var i in methods) {
			if ( methods[i].substr(0,5) == "BLOCK" ) {
				clientActionsBlock[methods[i].substr(6,100)] = methods[i]
			}
			if ( methods[i].substr(0,4) == "PAGE" ) {
				clientActionsPage[methods[i].substr(5,100)] = methods[i]
			}
			else if ( methods[i].substr(0,3) == "WEB" ) {
				webActions[methods[i].substr(4,100)] = methods[i]
			}
			else if ( methods[i].substr(0,4) == "VIEW" ) {
				views[methods[i].substr(5,100)] = methods[i]
			}
		}
		switch (type) {
			case "BLOCK":
				return clientActionsBlock
				break
			case "PAGE":
				return clientActionsPage
			case "WEB":
				return webActions
				break
			case "VIEW":
				return views
				break
			default:
				return null
				break
		}
	}

	// block record data
	block.record = {
			block_name			: 'Image',
			block_description	: 'Images resource library',
			form_name			: 'WEB_0F__image'
		}

	// block views
	block.views = getMethods("VIEW")

	// block data points
	block.data = {
		image_name : 'TEXT',
		image_type : 'TEXT',
		image_extension : 'TEXT',
		height : 'INTEGER',
		width : 'INTEGER',
		directory : 'TEXT',
		height_original : 'INTEGER',
		width_original : 'INTEGER'
	}

	// block client actions - Block
	block.clientActionsBlock = getMethods("BLOCK")

	// block client actions - Page
	block.clientActionsPage = getMethods("PAGE")

	// block web actions
	block.webActions = getMethods("WEB")

	// block configure data points
	block.blockConfigure = {
		Test : 'TEXT'
	}

	return block

}
<commit_msg>shortened image path<commit_after>/**
 *
 * @properties={typeid:24,uuid:"FFE14AC8-BECE-4D27-9AC1-0EE22A0032FF"}
 */
function BLOCK_choose()
{
	application.showFormInDialog(forms.WEB_0F__image__P_choose,-1,-1,-1,-1,"Image", false, false, "chooseImage")

	//update display
	if (forms.WEB_0F_page.TRIGGER_mode_set() == "DESIGN") {
		forms.WEB_0F_page__design__content_1L_block.ACTION_set_simple_display()
	}
	else {
		forms.WEB_0F_page__browser__editor.FORM_on_show()
	}
}

/**
 *
 * @properties={typeid:24,uuid:"43C94817-C701-46EF-809A-33BE2CFC738C"}
 */
function BLOCK_scale()
{
	application.showFormInDialog(forms.WEB_0F__image__P_scale,-1,-1,-1,-1,"Image", false, false, "imageScale")

	//update display
	if (forms.WEB_0F_page.TRIGGER_mode_set() == "DESIGN") {
		forms.WEB_0F_page__design__content_1L_block.ACTION_set_simple_display()
	}
	else {
		forms.WEB_0F_page__browser__editor.FORM_on_show()
	}
}

/**
 * Perform the element default action.
 *
 * @param {JSEvent} event the event that triggered the action
 *
 * @properties={typeid:24,uuid:"A1AAE46C-FB9C-4B17-9F80-3684F7D8F81B"}
 */
function ACTION_scale(event) {
	forms.WEB_0B_image.ACTION_image_scale()
}

/**
 *
 * @properties={typeid:24,uuid:"D5507344-C123-4997-A29D-32181865B93F"}
 */
function BLOCK_import()
{
	// TODO: handle reading text files

	// input file
	var file = plugins.file.showFileOpenDialog()
	if ( !file ) {
		return "Selection cancelled"
	}

	var imageTemp =  plugins.images.getImage(file)

	var ext = file.getName().split('.')
	//file has an extension
	if (ext && ext.length > 1) {
		var fileExt = ext[ext.length-1].toLowerCase()
	}

	// set image details object
	var fileOBJ = {}
	fileOBJ.image_name	= file.getName().replace(/ /g, "_")
	fileOBJ.image_type	= file.getContentType()
	fileOBJ.image_extension	= fileExt
	fileOBJ.width		= imageTemp.getWidth()
	fileOBJ.height		= imageTemp.getHeight()
	fileOBJ.width_original		= imageTemp.getWidth()
	fileOBJ.height_original		= imageTemp.getHeight()
	fileOBJ.directory	= "images/"
	fileOBJ.rec_created = new Date()

	/* copy image details to block data points
	var data = forms.WEB_0F_page__design.web_page_to_block_data_by_area_by_block
	for (var i = 0; i < data.getSize(); i++) {
		var record = data.getRecord(i + 1)
		record.data_value = fileOBJ[record.data_key]
	}
	*/

	// create image record
	var record = forms.WEB_0F_image.foundset.getRecord(forms.WEB_0F_image.foundset.newRecord(true,true))
	record.image_title		= fileOBJ.image_name
	record.image_type		= fileOBJ.image_type
	record.image_size		= plugins.file.getFileSize(file)
	record.directory		= fileOBJ.directory
	record.image_extension	= fileExt
	record.width			= fileOBJ.width
	record.height			= fileOBJ.height
	record.thumbnail		= imageTemp.resize((40*fileOBJ.width)/fileOBJ.height, 40)  // scale to 40px height
	record.id_site			= forms.WEB_0F_site.id_site

	// save file
	// TODO: stream image upload to server from client

	var outputImage			= forms.WEB_0F_install.ACTION_get_install() +
							'/application_server/server/webapps/ROOT/sutraCMS/sites/' +
							forms.WEB_0F_site.directory + '/images/' + fileOBJ.image_name

	// application_server/server/webapps/ROOT/sutraCMS/sites/4community

	if (forms.WEB_0F_install.ACTION_get_server() == "Windows"){
		outputImage		= outputImage.replace(/\//g, "\\")
	}
	var success = plugins.file.copyFile(file, outputImage)
	if ( !success ) {
		return "File save error"
	}
	plugins.dialogs.showInfoDialog("Image",  "Image uploaded")
}

/**
 * param {} obj Data object passed to all markup methods
 * @properties={typeid:24,uuid:"5AABEBFD-5C92-42EA-9C3D-B0135AA33FC8"}
 */
function VIEW_default(obj)
{

	// template
	var markup = 	'<p id="data-<<id_block>>"><img width="<<width>>" height="<<height>>" border="0"' +
					'src="<<directory>><<image_name>>"' +
					'alt="" />
'

	// replace
	markup = markup.replace(/<<id_block>>/ig, obj.block.id)
	markup = markup.replace(/<<width>>/ig, obj.data.width)
	markup = markup.replace(/<<height>>/ig, obj.data.height)
	markup = markup.replace(/<<image_name>>/ig, obj.data.image_name)
	markup = markup.replace(/<<directory>>/ig, obj.data.directory)

	// return
	return markup
}

/**
 * @properties={typeid:24,uuid:"D177F9D8-57D4-4E9C-BC4B-D87BF04B1B00"}
 */
function VIEW_download()
{
}

/**
 * @properties={typeid:24,uuid:"FFB42F41-3AB3-4F26-A006-9FDF03BE1CF7"}
 */
function VIEW_lightbox()
{
}

/**
 *
 * @properties={typeid:24,uuid:"C3D50AA7-5A76-484F-BF22-E86C40042A94"}
 */
function FORM_on_load() {

	/* moved
		var url = "http://localhost:8080" + directory + image_name
		elements.bn_browser.navigateTo(url)
	*/

}

/**
 *
 * @properties={typeid:24,uuid:"581D1472-7339-4669-A110-353A1904B241"}
 */
function TOGGLE_buttons(editStatus) {
	elements.btn_choose.enabled = editStatus
	elements.btn_import.enabled = editStatus
	elements.btn_scale.enabled = editStatus
	elements.lbl_choose.enabled = editStatus
	elements.lbl_import.enabled = editStatus
	elements.lbl_scale.enabled = editStatus
}

/**
 * @properties={typeid:24,uuid:"E9062B39-C69D-4841-A367-94BDC60849FF"}
 */
function LOADER_init(recBlock,flagEdit) {
	//foundset with image datapoints
	var fsBlockData = recBlock.web_block_to_block_data

	//create object with all properties
	var objImage = new Object()
	for (var i = 1; i <= fsBlockData.getSize(); i++) {
		var record = fsBlockData.getRecord(i)
		objImage[record.data_key] = record.data_value
	}

	//no image set yet
	if (!objImage.image_name){
		var html = 	'' +
					'No image chosen yet' +
					''
	}
	// image is set
	else {
		var siteURL = recBlock.web_block_to_area.web_area_to_version.web_version_to_page.web_page_to_site.url

		if (siteURL) {
			siteURL = 'http://' + siteURL

			var port = application.getServerURL()
			port = port.split(':')
			if (port.length > 2) {
				siteURL += ':' + port[2]
			}
		}
		else {
			var siteDirectory = recBlock.web_block_to_area.web_area_to_version.web_version_to_page.web_page_to_site.directory

			siteURL = application.getServerURL() + '/' + siteDirectory
		}

		var html = 	'' +
					'' +
					''
	}

	forms.WEB_0F__image.elements.bn_browser.html = html
	forms.WEB_0F_page__design__content_1F_block_data.elements.tab_detail.removeTabAt(2)
	forms.WEB_0F_page__design__content_1F_block_data.elements.tab_detail.addTab(forms.WEB_0F__image)
	forms.WEB_0F_page__design__content_1F_block_data.elements.tab_detail.tabIndex = 2

	forms.WEB_0F__image.TOGGLE_buttons(flagEdit)
}

/**
 * @properties={typeid:24,uuid:"5896D844-D8AA-4C31-84B2-D77EAE012F1D"}
 */
function INIT_block() {

	// main data object to build
	var block = {}

	// function to read methods on this form. naming conventions supply method categories and names
	function getMethods(type) {
		var methods = allmethods
		var clientActionsBlock = {}
		var clientActionsPage = {}
		var webActions = {}
		var views = {}
		for (var i in methods) {
			if ( methods[i].substr(0,5) == "BLOCK" ) {
				clientActionsBlock[methods[i].substr(6,100)] = methods[i]
			}
			if ( methods[i].substr(0,4) == "PAGE" ) {
				clientActionsPage[methods[i].substr(5,100)] = methods[i]
			}
			else if ( methods[i].substr(0,3) == "WEB" ) {
				webActions[methods[i].substr(4,100)] = methods[i]
			}
			else if ( methods[i].substr(0,4) == "VIEW" ) {
				views[methods[i].substr(5,100)] = methods[i]
			}
		}
		switch (type) {
			case "BLOCK":
				return clientActionsBlock
				break
			case "PAGE":
				return clientActionsPage
			case "WEB":
				return webActions
				break
			case "VIEW":
				return views
				break
			default:
				return null
				break
		}
	}

	// block record data
	block.record = {
			block_name			: 'Image',
			block_description	: 'Images resource library',
			form_name			: 'WEB_0F__image'
		}

	// block views
	block.views = getMethods("VIEW")

	// block data points
	block.data = {
		image_name : 'TEXT',
		image_type : 'TEXT',
		image_extension : 'TEXT',
		height : 'INTEGER',
		width : 'INTEGER',
		directory : 'TEXT',
		height_original : 'INTEGER',
		width_original : 'INTEGER'
	}

	// block client actions - Block
	block.clientActionsBlock = getMethods("BLOCK")

	// block client actions - Page
	block.clientActionsPage = getMethods("PAGE")

	// block web actions
	block.webActions = getMethods("WEB")

	// block configure data points
	block.blockConfigure = {
		Test : 'TEXT'
	}

	return block

}
<|endoftext|>


<commit_before>(function($) {

  GOD.Ajax.options = GOD.Ajax.options || {};

  GOD.Ajax.options.bannerList = {
    root: 'xxx',
    path: 'lists',
    key: 'default'
  };

  GOD.Ajax.getBannerListUrl = function(options) {
    return [
      options && options.root || this.options.bannerList.root,
      options && options.path || this.options.bannerList.path,
      options && options.url || this.options.bannerList.url
    ].join('/');
  };

  GOD.Ajax.getBannerList = function(key, root) {
    return $.ajax({
      type: 'GET',
      url: GOD.Ajax.getBannerListUrl({
        root: root,
        url: key
      }),
      dataType: 'json',
      cache: false,
      error: function(xhr, ajaxOptions, thrownError) {
        GOD.Ajax.checkError(xhr, ajaxOptions, thrownError);
      }
    });
  };

  $.fn.bannerList = (function() {
    var id_ = 0;

    return function(options) {
      var $list;
      var self = this;

      options = $.extend({}, $.fn.bannerList.defaults, options);

      id_ += 1;

      $list = $('
', {
        id: options.id + id_
      });

      GOD.Ajax
        .getBannerList(options.listKey, options.serviceRoot)
        .done(function(data) {
          var index = 0;

          if (data.ListItems) {

            $.each(data.ListItems, function(key, listItem) {

              // use source from properties or generate with index
              var imageSrc = listItem.Properties ? listItem.Properties[0].Value : options.img.src(index + 1);

              // generate image-link or use name text as content
              var content = listItem.Link ? options.getImageLink(listItem.Link, imageSrc) : listItem.Name;

              $list.before(
                $('
', {
                    id: [id_, options.itemId, index].join(''),
                    'class': options.itemClassName
                  })
                  .html(content)

                  /* show first item only */
                  .toggle(index === 0)
              );

              index += 1;
            });
          }

          options.onSuccess($list);
          self.prepend($list);

        });

      return this;
    };
  })();

  $.fn.bannerList.defaults = {
    id: 'loadingDiv_',
    itemId: 'banner',
    itemClassName: 'banner',
    onSuccess: function() {},
    getImageLink: function(url, src) {
      return [' '].join('"');
    },
    img: {
      src: function(id) {
        return ['Customers/HiddenName/Content/images/panel/panel', id || 1, '-sml.jpg'].join('');
      }
    }
  };

})(jQuery);
<commit_msg>travis fix<commit_after>(function($) {

  GOD.Ajax.options = GOD.Ajax.options || {};

  GOD.Ajax.options.bannerList = {
    root: 'xxx',
    path: 'lists',
    key: 'default'
  };

  GOD.Ajax.getBannerListUrl = function(options) {
    return [
      options && options.root || this.options.bannerList.root,
      options && options.path || this.options.bannerList.path,
      options && options.url || this.options.bannerList.url
    ].join('/');
  };

  GOD.Ajax.getBannerList = function(key, root) {
    return $.ajax({
      type: 'GET',
      url: GOD.Ajax.getBannerListUrl({
        root: root,
        url: key
      }),
      dataType: 'json',
      cache: false,
      error: function(xhr, ajaxOptions, thrownError) {
        GOD.Ajax.checkError(xhr, ajaxOptions, thrownError);
      }
    });
  };

  $.fn.bannerList = (function() {
    var id_ = 0;

    return function(options) {
      var $list;
      var self = this;

      options = $.extend({}, $.fn.bannerList.defaults, options);

      id_ += 1;

      $list = $('
', {
        id: options.id + id_
      });

      GOD.Ajax
        .getBannerList(options.listKey, options.serviceRoot)
        .done(function(data) {
          var index = 0;

          if (data.ListItems) {

            $.each(data.ListItems, function(key, listItem) {

              // use source from properties or generate with index
              var imageSrc = listItem.Properties ? listItem.Properties[0].Value : options.img.src(index + 1);

              // generate image-link or use name text as content
              var content = listItem.Link ? options.getImageLink(listItem.Link, imageSrc) : listItem.Name;

              $list.before(
                $('
', {
                    id: [id_, options.itemId, index].join(''),
                    'class': options.itemClassName
                  })
                  .html(content)

                  /* show first item only */
                  .toggle(index === 0)
              );

              index += 1;
            });
          }

          options.onSuccess($list);
          self.prepend($list);

        });

      return this;
    };
  })();

  $.fn.bannerList.defaults = {
    id: 'loadingDiv_',
    itemId: 'banner',
    itemClassName: 'banner',
    onSuccess: function() {},
    getImageLink: function(url, src) {
      return [' '].join('"');
    },
    img: {
      src: function(id) {
        return ['Customers/HiddenName/Content/images/panel/panel', id || 1, '-sml.jpg'].join('');
      }
    }
  };

})(this.jQuery || {});
<|endoftext|>


<commit_before>// /**
//  * Explore-ES6 (http://exploringjs.com/es6/ch_destructuring.html)
//  */
//
//
// /**
//  * Object Destructuring
//  */
//
// // 1 Destructuring objects
// const obj = {
//   first: 'peng',
//   last:  'Lyu',
// };
// const {first: f, last: l} = obj
// //f = 'peng' l = 'Lyu'
//
// // {prop} is short {prop: prop}
// const {first, last} = obj
// //first = 'peng' last = 'Lyu'
//
// // 2 processing return values
// const obj = {foo : 123};
// const {writable, configurable} = Object.getOwnPropertyDescriptor(obj, 'foo')

// http://zhouweicsu.github.io/javascript/2015/09/01/es6-destructuring-in-depth
/**
 * 解构在很多情况下都能派上用场。下面我例举一些最常用的场景。
 首先，我使用最多的一个场景是，在许多模块顶部的import声明中，解构可以从模块的公共API中
 只加载你需要的模块。举个栗子：
 */

/**
 * DEMO1 只加载需要的方法
 */

import {square, diag} from './math.js'  // import * as math from './math.js' 加载全部方法

console.log(square(4))
console.log(diag(3,4))

//第二个场景，当你的方法返回一个对象时，解构也让赋值变得非常简洁。
function getCoords (){
  return {x : 1, y: 2};
}
const {x, y} = getCoords();

//第三个使用场景， 函数定义参数 & 以及使用默认值
function random({ min = 1, max = 100}) {
  return Math.floor(Math.random() * (max - min)) + min
}

random({}) // {}不可以省略 , 否则报错
random({max = 200})

function random2({min = 1, max = 100} = {}){
  return Math.floor(Math.random() * (max - min)) + min
}

random2() // {} 可以省略

//另外一个使用场景 :使用解构 接收正则表达式返回值
var url = 'https://developer.mozilla.org/en-US/Web/JavaScript'
var parsedURL = /^(\w+)\:\/\/([^\/]+)\/(.*)$/.exec(url);

var [ ,protocol, fullhost, fullpath] = parsedURL;

//另外一个使用场景 : 交换两个变量

function removeBreakPoint({url, line, column}){
  // ...
}

//我们通过一个简单的可变参数函数containsAll给大家演示不定参数的用法。
//可以检查一个字符串中是否包含若干个子串，例如：containsAll("javascript", "java", "script")返回true。

//不定参数
function containsAll(word){
  for(let argument of arguments) {
    if(argument !== word) {
      return false
    }
  }
  return true
}
<commit_msg>Deault + Spread<commit_after>// /**
//  * Explore-ES6 (http://exploringjs.com/es6/ch_destructuring.html)
//  */
//
//
// /**
//  * Object Destructuring
//  */
//
// // 1 Destructuring objects
// const obj = {
//   first: 'peng',
//   last:  'Lyu',
// };
// const {first: f, last: l} = obj
// //f = 'peng' l = 'Lyu'
//
// // {prop} is short {prop: prop}
// const {first, last} = obj
// //first = 'peng' last = 'Lyu'
//
// // 2 processing return values
// const obj = {foo : 123};
// const {writable, configurable} = Object.getOwnPropertyDescriptor(obj, 'foo')

// http://zhouweicsu.github.io/javascript/2015/09/01/es6-destructuring-in-depth
/**
 * 解构在很多情况下都能派上用场。下面我例举一些最常用的场景。
 首先，我使用最多的一个场景是，在许多模块顶部的import声明中，解构可以从模块的公共API中
 只加载你需要的模块。举个栗子：
 */

/**
 * DEMO1 只加载需要的方法
 */

import {square, diag} from './math.js'  // import * as math from './math.js' 加载全部方法

console.log(square(4))
console.log(diag(3,4))

//第二个场景，当你的方法返回一个对象时，解构也让赋值变得非常简洁。
function getCoords (){
  return {x : 1, y: 2};
}
const {x, y} = getCoords();

//第三个使用场景， 函数定义参数 & 以及使用默认值
function random({ min = 1, max = 100}) {
  return Math.floor(Math.random() * (max - min)) + min
}

random({}) // {}不可以省略 , 否则报错
random({max = 200})

function random2({min = 1, max = 100} = {}){
  return Math.floor(Math.random() * (max - min)) + min
}

random2() // {} 可以省略

//另外一个使用场景 :使用解构 接收正则表达式返回值
var url = 'https://developer.mozilla.org/en-US/Web/JavaScript'
var parsedURL = /^(\w+)\:\/\/([^\/]+)\/(.*)$/.exec(url)

var [ ,protocol, fullhost, fullpath] = parsedURL;

//另外一个使用场景 : 交换两个变量

function removeBreakPoint({url, line, column}){
  // ...
}

//我们通过一个简单的可变参数函数containsAll给大家演示不定参数的用法。
//可以检查一个字符串中是否包含若干个子串，例如：containsAll("javascript", "java", "script")返回true。

//不定参数
function containsAll(word){
  for(let argument of arguments) {
    if(argument !== word) {
      return false
    }
  }
  return true
}
<|endoftext|>


<commit_before>import { mount } from '@vue/test-utils';
import navComponents from 'kolibri.utils.navComponents';
import { UserKinds, NavComponentSections } from 'kolibri.coreVue.vuex.constants';
import SideNav from '../../src/views/SideNav';
import logoutSideNavEntry from '../../src/views/LogoutSideNavEntry';
import { coreStoreFactory as makeStore } from '../../src/state/store';

jest.mock('kolibri.urls');

function createWrapper({ navShown = true, headerHeight = 20, width = 100 } = {}) {
  return mount(SideNav, {
    propsData: {
      navShown,
      headerHeight,
      width,
    },
    store: makeStore(),
  });
}

function setUserKind(store, userKind) {
  let canManageContent = false;
  if (userKind == UserKinds.CAN_MANAGE_CONTENT) {
    userKind = UserKinds.LEARNER;
    canManageContent = true;
  }
  store.commit('CORE_SET_SESSION', {
    id: 'test',
    username: 'test',
    full_name: 'testing test',
    user_id: 'test_id',
    facility_id: 'a real school',
    kind: [userKind],
    can_manage_content: canManageContent,
  });
}

const filterableUserKinds = [
  UserKinds.ANONYMOUS,
  UserKinds.LEARNER,
  UserKinds.COACH,
  UserKinds.ADMIN,
  UserKinds.CAN_MANAGE_CONTENT,
  UserKinds.SUPERUSER,
];

//
describe('side nav component', () => {
  it('should be hidden if navShown is false', () => {
    const wrapper = createWrapper({ navShown: false });
    expect(wrapper.find('.side-nav').isVisible()).toBe(false);
  });
  it('should show nothing if no components are added and user is not logged in', () => {
    expect(navComponents).toHaveLength(0);
    const wrapper = createWrapper();
    expect(wrapper.contains('a.ui-menu-option:not(.is-divider)')).toBe(false);
  });
  it('should show logout if no components are added and user is logged in', async () => {
    expect(navComponents).toHaveLength(0);
    const wrapper = createWrapper();
    setUserKind(wrapper.vm.$store, UserKinds.LEARNER);
      await wrapper.vm.$nextTick();
    expect(wrapper.contains(logoutSideNavEntry)).toBe(true);
  });
  filterableUserKinds.forEach(kind => {
    afterEach(() => {
      // Clean up the registered component
      navComponents.pop();
    });
    it(`should show ${kind} component if added and user is ${kind}`, async () => {
      const component = {
        name: `${kind}SideNavEntry`,
        render() {
          return '';
        },
        role: kind,
      };
      navComponents.register(component);
      expect(navComponents).toHaveLength(1);
      const wrapper = createWrapper();
      setUserKind(wrapper.vm.$store, kind);
      await wrapper.vm.$nextTick();
      expect(wrapper.contains(component)).toBe(true);
    });
  });
  // These UserKinds have monotonically escalating privileges.
  const escalatingPrivileges = [
    UserKinds.LEARNER,
    UserKinds.COACH,
    UserKinds.ADMIN,
    UserKinds.SUPERUSER,
  ];
  escalatingPrivileges.forEach(kind => {
    // This is slightly duplicative of the tests above, but not harmful.
    escalatingPrivileges.slice(0, escalatingPrivileges.indexOf(kind)).forEach(otherKind => {
      afterEach(() => {
        // Clean up the registered component
        navComponents.pop();
      });
      it(`should show ${otherKind} component if added and user is ${kind}`, async () => {
        const component = {
          name: `${otherKind}SideNavEntry`,
          render() {
            return '';
          },
          role: otherKind,
        };
        navComponents.register(component);
        expect(navComponents).toHaveLength(1);
        const wrapper = createWrapper();
        setUserKind(wrapper.vm.$store, kind);
        await wrapper.vm.$nextTick();
        expect(wrapper.contains(component)).toBe(true);
      });
    });
  });
  filterableUserKinds.forEach(kind => {
    filterableUserKinds.slice(filterableUserKinds.indexOf(kind) + 1, -1).forEach(otherKind => {
      afterEach(() => {
        // Clean up the registered component
        navComponents.pop();
      });
      it(`should not show ${otherKind} component if added and user is ${kind}`, async () => {
        const component = {
          name: `${otherKind}SideNavEntry`,
          render() {
            return '';
          },
          role: otherKind,
        };
        navComponents.register(component);
        expect(navComponents).toHaveLength(1);
        const wrapper = createWrapper();
        setUserKind(wrapper.vm.$store, kind);
        await wrapper.vm.$nextTick();
        expect(wrapper.contains(component)).toBe(false);
      });
    });
  });
  describe('with multiple components', () => {
    // All user kinds that can be copresented in the side nav.
    const userKinds = [
      UserKinds.LEARNER,
      UserKinds.COACH,
      UserKinds.ADMIN,
      UserKinds.CAN_MANAGE_CONTENT,
      UserKinds.SUPERUSER,
    ];
    userKinds.forEach(kind => {
      userKinds.slice(userKinds.indexOf(kind) + 1, -1).forEach(otherKind => {
        afterEach(() => {
          // Clean up the registered components
          navComponents.pop();
          navComponents.pop();
        });
        it(`should show ${otherKind} component below ${kind} component`, () => {
          const component1 = {
            name: `${kind}SideNavEntry`,
            render() {
              return '';
            },
            role: kind,
          };
          const component2 = {
            name: `${otherKind}SideNavEntry`,
            render() {
              return '';
            },
            role: otherKind,
          };
          navComponents.register(component2);
          navComponents.register(component1);
          expect(navComponents).toHaveLength(2);
          const wrapper = createWrapper();
          setUserKind(wrapper.vm.$store, UserKinds.SUPERUSER);
          expect(wrapper.vm.menuOptions[0]).toBe(component1);
          expect(wrapper.vm.menuOptions[1]).toBe(component2);
        });
      });
    });
  });
  describe('and the priority flag', () => {
    afterEach(() => {
      // Clean up the registered components
      navComponents.pop();
      navComponents.pop();
    });
    it('should show higher priority component above lower priority component', () => {
      const component1 = {
        name: '1SideNavEntry',
        render() {
          return '';
        },
        priority: 1,
      };
      const component2 = {
        name: '2SideNavEntry',
        render() {
          return '';
        },
        priority: 10,
      };
      navComponents.register(component2);
      navComponents.register(component1);
      expect(navComponents).toHaveLength(2);
      const wrapper = createWrapper();
      expect(wrapper.vm.menuOptions[0]).toBe(component1);
      expect(wrapper.vm.menuOptions[1]).toBe(component2);
    });
    it('should show account section component below lower priority component', () => {
      const component1 = {
        name: '1SideNavEntry',
        render() {
          return '';
        },
        priority: 1,
        section: NavComponentSections.ACCOUNT,
      };
      const component2 = {
        name: '2SideNavEntry',
        render() {
          return '';
        },
        priority: 10,
      };
      navComponents.register(component2);
      navComponents.register(component1);
      expect(navComponents).toHaveLength(2);
      const wrapper = createWrapper();
      expect(wrapper.vm.menuOptions[2]).toBe(component1);
      expect(wrapper.vm.menuOptions[0]).toBe(component2);
    });
    it('should show component with priority above undefined priority component', () => {
      const component1 = {
        name: '1SideNavEntry',
        render() {
          return '';
        },
      };
      const component2 = {
        name: '2SideNavEntry',
        render() {
          return '';
        },
        priority: 10,
      };
      navComponents.register(component2);
      navComponents.register(component1);
      expect(navComponents).toHaveLength(2);
      const wrapper = createWrapper();
      expect(wrapper.vm.menuOptions[1]).toBe(component1);
      expect(wrapper.vm.menuOptions[0]).toBe(component2);
    });
  });
});
<commit_msg>Extract shared test code<commit_after>import { mount } from '@vue/test-utils';
import navComponents from 'kolibri.utils.navComponents';
import { UserKinds, NavComponentSections } from 'kolibri.coreVue.vuex.constants';
import SideNav from '../../src/views/SideNav';
import logoutSideNavEntry from '../../src/views/LogoutSideNavEntry';
import { coreStoreFactory as makeStore } from '../../src/state/store';

jest.mock('kolibri.urls');

function createWrapper({ navShown = true, headerHeight = 20, width = 100 } = {}) {
  return mount(SideNav, {
    propsData: {
      navShown,
      headerHeight,
      width,
    },
    store: makeStore(),
  });
}

function setUserKind(store, userKind) {
  let canManageContent = false;
  if (userKind == UserKinds.CAN_MANAGE_CONTENT) {
    userKind = UserKinds.LEARNER;
    canManageContent = true;
  }
  store.commit('CORE_SET_SESSION', {
    id: 'test',
    username: 'test',
    full_name: 'testing test',
    user_id: 'test_id',
    facility_id: 'a real school',
    kind: [userKind],
    can_manage_content: canManageContent,
  });
}

const filterableUserKinds = [
  UserKinds.ANONYMOUS,
  UserKinds.LEARNER,
  UserKinds.COACH,
  UserKinds.ADMIN,
  UserKinds.CAN_MANAGE_CONTENT,
  UserKinds.SUPERUSER,
];

//
describe('side nav component', () => {
  it('should be hidden if navShown is false', () => {
    const wrapper = createWrapper({ navShown: false });
    expect(wrapper.find('.side-nav').isVisible()).toBe(false);
  });
  it('should show nothing if no components are added and user is not logged in', () => {
    expect(navComponents).toHaveLength(0);
    const wrapper = createWrapper();
    expect(wrapper.contains('a.ui-menu-option:not(.is-divider)')).toBe(false);
  });
  it('should show logout if no components are added and user is logged in', async () => {
    expect(navComponents).toHaveLength(0);
    const wrapper = createWrapper();
    setUserKind(wrapper.vm.$store, UserKinds.LEARNER);
    await wrapper.vm.$nextTick();
    expect(wrapper.contains(logoutSideNavEntry)).toBe(true);
  });
  describe('SideNav components are shown/hidden depending on role', () => {
    afterEach(() => {
      // Clean up the registered component
      navComponents.pop();
    });
    async function testSideNavVisibility(kind, otherKind, shouldShow) {
      const component = {
        name: `${otherKind}SideNavEntry`,
        render() {
          return '';
        },
        role: otherKind,
      };
      navComponents.register(component);
      expect(navComponents).toHaveLength(1);
      const wrapper = createWrapper();
      setUserKind(wrapper.vm.$store, kind);
      await wrapper.vm.$nextTick();
      expect(wrapper.contains(component)).toBe(shouldShow);
    }
    filterableUserKinds.forEach(kind => {
      it(`should show ${kind} component if added and user is ${kind}`, async () => {
        await testSideNavVisibility(kind, kind, true);
      });
    });
    // These UserKinds have monotonically escalating privileges.
    const escalatingPrivileges = [
      UserKinds.LEARNER,
      UserKinds.COACH,
      UserKinds.ADMIN,
      UserKinds.SUPERUSER,
    ];
    escalatingPrivileges.forEach(kind => {
      // This is slightly duplicative of the tests above, but not harmful.
      escalatingPrivileges.slice(0, escalatingPrivileges.indexOf(kind)).forEach(otherKind => {
        it(`should show ${otherKind} component if added and user is ${kind}`, async () => {
          await testSideNavVisibility(kind, otherKind, true);
        });
      });
    });
    filterableUserKinds.forEach(kind => {
      filterableUserKinds.slice(filterableUserKinds.indexOf(kind) + 1, -1).forEach(otherKind => {
        it(`should not show ${otherKind} component if added and user is ${kind}`, async () => {
          await testSideNavVisibility(kind, otherKind, false);
        });
      });
    });
  });

  describe('with multiple components', () => {
    // All user kinds that can be copresented in the side nav.
    const userKinds = [
      UserKinds.LEARNER,
      UserKinds.COACH,
      UserKinds.ADMIN,
      UserKinds.CAN_MANAGE_CONTENT,
      UserKinds.SUPERUSER,
    ];
    userKinds.forEach(kind => {
      userKinds.slice(userKinds.indexOf(kind) + 1, -1).forEach(otherKind => {
        afterEach(() => {
          // Clean up the registered components
          navComponents.pop();
          navComponents.pop();
        });
        it(`should show ${otherKind} component below ${kind} component`, () => {
          const component1 = {
            name: `${kind}SideNavEntry`,
            render() {
              return '';
            },
            role: kind,
          };
          const component2 = {
            name: `${otherKind}SideNavEntry`,
            render() {
              return '';
            },
            role: otherKind,
          };
          navComponents.register(component2);
          navComponents.register(component1);
          expect(navComponents).toHaveLength(2);
          const wrapper = createWrapper();
          setUserKind(wrapper.vm.$store, UserKinds.SUPERUSER);
          expect(wrapper.vm.menuOptions[0]).toBe(component1);
          expect(wrapper.vm.menuOptions[1]).toBe(component2);
        });
      });
    });
  });
  describe('and the priority flag', () => {
    afterEach(() => {
      // Clean up the registered components
      navComponents.pop();
      navComponents.pop();
    });
    it('should show higher priority component above lower priority component', () => {
      const component1 = {
        name: '1SideNavEntry',
        render() {
          return '';
        },
        priority: 1,
      };
      const component2 = {
        name: '2SideNavEntry',
        render() {
          return '';
        },
        priority: 10,
      };
      navComponents.register(component2);
      navComponents.register(component1);
      expect(navComponents).toHaveLength(2);
      const wrapper = createWrapper();
      expect(wrapper.vm.menuOptions[0]).toBe(component1);
      expect(wrapper.vm.menuOptions[1]).toBe(component2);
    });
    it('should show account section component below lower priority component', () => {
      const component1 = {
        name: '1SideNavEntry',
        render() {
          return '';
        },
        priority: 1,
        section: NavComponentSections.ACCOUNT,
      };
      const component2 = {
        name: '2SideNavEntry',
        render() {
          return '';
        },
        priority: 10,
      };
      navComponents.register(component2);
      navComponents.register(component1);
      expect(navComponents).toHaveLength(2);
      const wrapper = createWrapper();
      expect(wrapper.vm.menuOptions[2]).toBe(component1);
      expect(wrapper.vm.menuOptions[0]).toBe(component2);
    });
    it('should show component with priority above undefined priority component', () => {
      const component1 = {
        name: '1SideNavEntry',
        render() {
          return '';
        },
      };
      const component2 = {
        name: '2SideNavEntry',
        render() {
          return '';
        },
        priority: 10,
      };
      navComponents.register(component2);
      navComponents.register(component1);
      expect(navComponents).toHaveLength(2);
      const wrapper = createWrapper();
      expect(wrapper.vm.menuOptions[1]).toBe(component1);
      expect(wrapper.vm.menuOptions[0]).toBe(component2);
    });
  });
});
<|endoftext|>


<commit_before>this.streaming = {};

Backbone.sync = function (method, model, options) {
    var message = {
        method: method,
        url: model.url(),
        cid: model.cid,
        data: options.arguments || model.toJSON() || {},
        emit: options.emit
    };

    streaming.send("sync", message, function (err, response) {
        if (err) {
            options.error(err);
        } else {
            options.success(response);
        }
    });
};

_.extend(streaming, {
    subscriptions: {},
    callbacks: {},

    setUser: function (user_id) {
        this.user_id = user_id;
    },

    setup: function setup(socket) {
        streaming.socket = socket;

        this.user_id = 0;
        this.db = new PouchDB('streaming.io');
        this.db.allDocs({include_docs: true}, _.bind(function (err, response) {
            var deletes = [];

            if (!err && response) {
                var now = Date.now();
                _.each(response.rows, function (row) {
                    if (row.doc.expire && row.doc.expire > now) {
                        return;
                    }
                    row.doc._deleted = true;
                    deletes.push(row.doc);
                });
            }

            this.db.bulkDocs({
                docs: deletes
            }, _.bind(function () {
                this.db.compact();
            }, this));
        }, this));

        socket.on('connect', function() {
            streaming.connect(socket);
        });

        socket.on('disconnect', function () {
            streaming.disconnect();
        });

        socket.on('stream', _.bind(streaming.sync, streaming));
    },

    sync: function sync(message) {
        var item = streaming.subscriptions[message.url];
        var data = streaming.uncompress(message.data);
        item.cache = data;
        streaming.apply(data, item.clients);

        this.db.put({
            _id: message.url,
            _rev: item.revision,
            hash: message.hash,
            data: message.data,
            user_id: this.user_id,
            expire: new Date(new Date().getTime() + 2 * 60 * 60 * 1000)
        });
    },

    apply: function apply(data, items) {
        _.each(items, function (model) {
            if (!model.streaming) {
                return;
            }

            var initial = model.streaming.options.initial;
            model.streaming.options.initial = false;

            if (model.models.length > 0) {
                var idAttribute = model.models[0].idAttribute;
                var equals = false;
                do {
                    if (initial === true) {
                        break;
                    }

                    if (model.models.length != data.length) {
                        break;
                    }

                    var local = _.pluck(model.models, "id");
                    var remote = _.pluck(data, idAttribute)

                    var i = 0, n = model.models.length;
                    for (;i !=n; ++i) {
                        if (local[i] != remote[i]) {
                            break;
                        }
                    }
                    if (i != n) {
                        break;
                    }
                    equals = true;
                } while (0);
                if (equals) {
                    model.forEach(function (current, index) {
                        current.set(current.parse(data[index]));
                    });
                    model.trigger('sync', model, data, {});
                } else {
                    model.reset(model.parse(data));
                }

            } else {
                model.reset(model.parse(data));
            }
        });
    },

    emit: function emit(url, item) {
        item.updating = true;

        var message = {
            url: url
        };

        this.db.get(url, _.bind(function (err, doc) {
            if (!err && doc) {
                message.hash = doc.hash;
                item.revision = doc._rev;

                if ((doc.user_id == this.user_id) && item.options.precache) {
                    item.cache = streaming.uncompress(doc.data);
                    streaming.apply(item.cache, item.clients);
                }
            }

            streaming.send("stream", message, _.bind(function (err, response) {
                delete item.updating;

                if (err) {
                    return;
                }

                var data;
                if (response.hash == message.hash) {
                    var limit = new Date().getTime() + 1 * 60 * 60 * 1000;
                    if (doc.expire < limit || (doc.user_id != this.user_id)) {
                        this.db.put({
                            _id: url,
                            _rev: item.revision,
                            hash: message.hash,
                            data: doc.data,
                            user_id: this.user_id,
                            expire: new Date(new Date().getTime() + 2 * 60 * 60 * 1000)
                        });
                    }

                    if (!item.cache) {
                        item.cache = data = streaming.uncompress(doc.data);
                    }
                } else {
                    item.cache = data = streaming.uncompress(response.data);
                    this.db.put({
                        _id: url,
                        _rev: item.revision,
                        hash: response.hash,
                        data: response.data,
                        user_id: this.user_id,
                        expire: new Date(new Date().getTime() + 2 * 60 * 60 * 1000)
                    });
                }

                if (data) {
                    streaming.apply(data, item.clients);
                }
            }, this));

        }, this));
    },

    connect: function (socket) {
        streaming.socket = socket;

        _.each(streaming.subscriptions, function (item, url) {
            streaming.emit(url, item);
        });
    },

    disconnect: function () {
        _.each(streaming.subscriptions, function (item) {
            delete item.cache;
            if (item.timeout) {
                clearTimeout(item.timeout);
                delete item.timeout;
            }
        });

        _.each(streaming.callbacks, function (callback, id) {
            callback("Disconnected");
        });
        streaming.callbacks = {};

        delete streaming.socket;
    },

    add: function (target, options) {
        var url = target.url();
        if (target.streaming) {
            return;
        }

        options = options ? _.clone(options) : {};
        _.defaults(options, {
            "precache": target.hasOwnProperty("precache") && !!target.precache,
            "initial": true
        });

        var item = streaming.subscriptions[url];
        if (!_.isUndefined(item)) {
            if (item.timeout) {
                clearTimeout(item.timeout);
                delete item.timeout;
            }

            target.streaming = { url: url, options: options };
            item.clients[target.cid] = target;

            if (item.cache) {
                streaming.apply(item.cache, [target]);
            }
            return;
        } else {
            item = { clients: {}, options: options };
            item.clients[target.cid] = target;

            target.streaming = { url: url, options: options };
            streaming.subscriptions[url] = item;
        }

        if (_.isUndefined(streaming.socket)) {
            return;
        }

        streaming.emit(url, item);
    },

    remove: function (target) {
        if (target.streaming) {
            var url = target.streaming.url;
            var item = streaming.subscriptions[url];

            delete item.clients[target.cid];
            if (_.keys(item.clients).length == 0) {
                item.timeout = setTimeout(function () {
                    streaming.socket.emit("unstream", { url: url }, function (err) {});
                    delete streaming.subscriptions[url];
                }, 5000);
            }

            delete target.streaming;
        }
    },

    send: function (type, message, callback) {
        if (!streaming.socket) {
            _.defer(function () {
                callback("No connection");
            });
            return;
        }

        var id = _.uniqueId('cb');
        streaming.callbacks[id] = callback;

        streaming.socket.emit(type, message, function (err, response) {
            delete streaming.callbacks[id];
            callback(err, streaming.uncompress(response));
        });
    },

    uncompress: function (data) {
        if (!_.isObject(data)) {
            return data;
        }

        var objectKeys = _.keys(data);

        if ((objectKeys.length != 2) || (_.without(objectKeys, 'keys', 'rows').length != 0)) {
            return data;
        }

        var keys = data.keys;
        var uncompressed = _.map(data.rows, function (row) {
            return _.object(keys, row);
        });

        return uncompressed;
    }
});

_.extend(Backbone.Model.prototype, {
    stream: function () {
        alert("Streaming models is not currently supported");
    },

    unstream: function () {},

    incrementVersion: function () {
        var versionName = this.versionAttribute;
        if (!_.isUndefined(versionName)) {
            this.set(versionName, this.parseVersionNumber(this.get(versionName)) + 1, {silent: true});
        }
    },

    parseVersionNumber: function (versionNumber) {
        return _.isFinite(versionNumber) ? versionNumber : 0;
    },

    parse: function (resp) {
        var versionName = this.versionAttribute;
        if (_.isObject(resp) && !_.isUndefined(versionName)) {
            var localVersion = this.parseVersionNumber(this.get(versionName));
            var remoteVersion = this.parseVersionNumber(resp[versionName]);

            if (localVersion > remoteVersion) {
                return {};
            }
        }
        return resp;
    },

    emit: function(method, args, options) {
        if (_.isUndefined(options)) {
            options = args ? _.clone(args) : {};
            args = undefined;
        } else {
            options = options ? _.clone(options) : {};
        }

        var model = this;

        var triggerEmit = _.bind(function() {
            model.trigger('emit emit:' + method, model, options);
        }, this);

        options.emit = method;
        options.arguments = args;

        var success = options.success;
        options.success = function (resp) {
            if (success) success(model, resp, options);
            if (options.wait) triggerEmit();
        };
        var error = options.error;
        options.error = function (resp) {
            if (error) error(model, resp, options);
            model.trigger('error', model, resp, options);
        }

        var xhr = this.sync('emit', this, options);
        if (!options.wait) triggerEmit();
        return xhr;
    }
});

_.extend(Backbone.Collection.prototype, {
    stream: function (options) {
        options = options || {};
        if (_.isUndefined(this.cid)) {
            this.cid = _.uniqueId('coll');
        }

        _.defer(_.bind(function () {
            streaming.add(this, options);
        }, this));
    },

    unstream: function () {
        streaming.remove(this);
    },

    emit: function (method, args, options) {
        if (_.isUndefined(options)) {
            options = args ? _.clone(args) : {};
            args = undefined;
        } else {
            options = options ? _.clone(options) : {};
        }

        var model = this;

        var triggerEmit = _.bind(function() {
            model.trigger('emit emit:' + method, model, options);
        }, this);

        options.emit = method;
        options.arguments = args;

        var success = options.success;
        options.success = function (resp) {
            if (success) success(model, resp, options);
            if (options.wait) triggerEmit();
        };
        var error = options.error;
        options.error = function (resp) {
            if (error) error(model, resp, options);
            model.trigger('error', model, resp, options);
        }

        var xhr = this.sync('emit', this, options);
        if (!options.wait) triggerEmit();
        return xhr;
    }
});
<commit_msg>Moved all declarations inside anonymous function Global namespace is now called Streaming instead of streaming No longer globally replacing Backbone.sync, instead introducing Streaming.Model and Streaming.Collection<commit_after>(function () {
    var streaming = {};

    _.extend(streaming, {
        subscriptions: {},
        callbacks: {},

        setUser: function (user_id) {
            this.user_id = user_id;
        },

        setup: function setup(socket) {
            streaming.socket = socket;

            this.user_id = 0;
            this.db = new PouchDB('streaming.io');
            this.db.allDocs({include_docs: true}, _.bind(function (err, response) {
                var deletes = [];

                if (!err && response) {
                    var now = Date.now();
                    _.each(response.rows, function (row) {
                        if (row.doc.expire && row.doc.expire > now) {
                            return;
                        }
                        row.doc._deleted = true;
                        deletes.push(row.doc);
                    });
                }

                this.db.bulkDocs({
                    docs: deletes
                }, _.bind(function () {
                    this.db.compact();
                }, this));
            }, this));

            socket.on('connect', function() {
                streaming.connect(socket);
            });

            socket.on('disconnect', function () {
                streaming.disconnect();
            });

            socket.on('stream', _.bind(streaming.sync, streaming));
        },

        sync: function sync(message) {
            var item = streaming.subscriptions[message.url];
            var data = streaming.uncompress(message.data);
            item.cache = data;
            streaming.apply(data, item.clients);

            this.db.put({
                _id: message.url,
                _rev: item.revision,
                hash: message.hash,
                data: message.data,
                user_id: this.user_id,
                expire: new Date(new Date().getTime() + 2 * 60 * 60 * 1000)
            });
        },

        apply: function apply(data, items) {
            _.each(items, function (model) {
                if (!model.streaming) {
                    return;
                }

                var initial = model.streaming.options.initial;
                model.streaming.options.initial = false;

                if (model.models.length > 0) {
                    var idAttribute = model.models[0].idAttribute;
                    var equals = false;
                    do {
                        if (initial === true) {
                            break;
                        }

                        if (model.models.length != data.length) {
                            break;
                        }

                        var local = _.pluck(model.models, "id");
                        var remote = _.pluck(data, idAttribute)

                        var i = 0, n = model.models.length;
                        for (;i !=n; ++i) {
                            if (local[i] != remote[i]) {
                                break;
                            }
                        }
                        if (i != n) {
                            break;
                        }
                        equals = true;
                    } while (0);
                    if (equals) {
                        model.forEach(function (current, index) {
                            current.set(current.parse(data[index]));
                        });
                        model.trigger('sync', model, data, {});
                    } else {
                        model.reset(model.parse(data));
                    }

                } else {
                    model.reset(model.parse(data));
                }
            });
        },

        emit: function emit(url, item) {
            item.updating = true;

            var message = {
                url: url
            };

            this.db.get(url, _.bind(function (err, doc) {
                if (!err && doc) {
                    message.hash = doc.hash;
                    item.revision = doc._rev;

                    if ((doc.user_id == this.user_id) && item.options.precache) {
                        item.cache = streaming.uncompress(doc.data);
                        streaming.apply(item.cache, item.clients);
                    }
                }

                streaming.send("stream", message, _.bind(function (err, response) {
                    delete item.updating;

                    if (err) {
                        return;
                    }

                    var data;
                    if (response.hash == message.hash) {
                        var limit = new Date().getTime() + 1 * 60 * 60 * 1000;
                        if (doc.expire < limit || (doc.user_id != this.user_id)) {
                            this.db.put({
                                _id: url,
                                _rev: item.revision,
                                hash: message.hash,
                                data: doc.data,
                                user_id: this.user_id,
                                expire: new Date(new Date().getTime() + 2 * 60 * 60 * 1000)
                            });
                        }

                        if (!item.cache) {
                            item.cache = data = streaming.uncompress(doc.data);
                        }
                    } else {
                        item.cache = data = streaming.uncompress(response.data);
                        this.db.put({
                            _id: url,
                            _rev: item.revision,
                            hash: response.hash,
                            data: response.data,
                            user_id: this.user_id,
                            expire: new Date(new Date().getTime() + 2 * 60 * 60 * 1000)
                        });
                    }

                    if (data) {
                        streaming.apply(data, item.clients);
                    }
                }, this));

            }, this));
        },

        connect: function (socket) {
            streaming.socket = socket;

            _.each(streaming.subscriptions, function (item, url) {
                streaming.emit(url, item);
            });
        },

        disconnect: function () {
            _.each(streaming.subscriptions, function (item) {
                delete item.cache;
                if (item.timeout) {
                    clearTimeout(item.timeout);
                    delete item.timeout;
                }
            });

            _.each(streaming.callbacks, function (callback, id) {
                callback("Disconnected");
            });
            streaming.callbacks = {};

            delete streaming.socket;
        },

        add: function (target, options) {
            var url = target.url();
            if (target.streaming) {
                return;
            }

            options = options ? _.clone(options) : {};
            _.defaults(options, {
                "precache": target.hasOwnProperty("precache") && !!target.precache,
                "initial": true
            });

            var item = streaming.subscriptions[url];
            if (!_.isUndefined(item)) {
                if (item.timeout) {
                    clearTimeout(item.timeout);
                    delete item.timeout;
                }

                target.streaming = { url: url, options: options };
                item.clients[target.cid] = target;

                if (item.cache) {
                    streaming.apply(item.cache, [target]);
                }
                return;
            } else {
                item = { clients: {}, options: options };
                item.clients[target.cid] = target;

                target.streaming = { url: url, options: options };
                streaming.subscriptions[url] = item;
            }

            if (_.isUndefined(streaming.socket)) {
                return;
            }

            streaming.emit(url, item);
        },

        remove: function (target) {
            if (target.streaming) {
                var url = target.streaming.url;
                var item = streaming.subscriptions[url];

                delete item.clients[target.cid];
                if (_.keys(item.clients).length == 0) {
                    item.timeout = setTimeout(function () {
                        streaming.socket.emit("unstream", { url: url }, function (err) {});
                        delete streaming.subscriptions[url];
                    }, 5000);
                }

                delete target.streaming;
            }
        },

        send: function (type, message, callback) {
            if (!streaming.socket) {
                _.defer(function () {
                    callback("No connection");
                });
                return;
            }

            var id = _.uniqueId('cb');
            streaming.callbacks[id] = callback;

            streaming.socket.emit(type, message, function (err, response) {
                delete streaming.callbacks[id];
                callback(err, streaming.uncompress(response));
            });
        },

        uncompress: function (data) {
            if (!_.isObject(data)) {
                return data;
            }

            var objectKeys = _.keys(data);

            if ((objectKeys.length != 2) || (_.without(objectKeys, 'keys', 'rows').length != 0)) {
                return data;
            }

            var keys = data.keys;
            var uncompressed = _.map(data.rows, function (row) {
                return _.object(keys, row);
            });

            return uncompressed;
        }
    });

    sync = function (method, model, options) {
        var message = {
            method: method,
            url: model.url(),
            cid: model.cid,
            data: options.arguments || model.toJSON() || {},
            emit: options.emit
        };

        streaming.send("sync", message, function (err, response) {
            if (err) {
                options.error(err);
            } else {
                options.success(response);
            }
        });
    };

    streaming.Model = Backbone.Model.extend({
        stream: function () {
            console.trace();
            alert("Streaming models is not currently supported");
        },

        unstream: function () {},

        incrementVersion: function () {
            var versionName = this.versionAttribute;
            if (!_.isUndefined(versionName)) {
                this.set(versionName, this.parseVersionNumber(this.get(versionName)) + 1, {silent: true});
            }
        },

        parseVersionNumber: function (versionNumber) {
            return _.isFinite(versionNumber) ? versionNumber : 0;
        },

        parse: function (resp) {
            var versionName = this.versionAttribute;
            if (_.isObject(resp) && !_.isUndefined(versionName)) {
                var localVersion = this.parseVersionNumber(this.get(versionName));
                var remoteVersion = this.parseVersionNumber(resp[versionName]);

                if (localVersion > remoteVersion) {
                    return {};
                }
            }
            return resp;
        },

        emit: function(method, args, options) {
            if (_.isUndefined(options)) {
                options = args ? _.clone(args) : {};
                args = undefined;
            } else {
                options = options ? _.clone(options) : {};
            }

            var model = this;

            var triggerEmit = _.bind(function() {
                model.trigger('emit emit:' + method, model, options);
            }, this);

            options.emit = method;
            options.arguments = args;

            var success = options.success;
            options.success = function (resp) {
                if (success) success(model, resp, options);
                if (options.wait) triggerEmit();
            };
            var error = options.error;
            options.error = function (resp) {
                if (error) error(model, resp, options);
                model.trigger('error', model, resp, options);
            }

            var xhr = this.sync('emit', this, options);
            if (!options.wait) triggerEmit();
            return xhr;
        },

        sync: sync
    });

    streaming.Collection = Backbone.Collection.extend({
        stream: function (options) {
            options = options || {};
            if (_.isUndefined(this.cid)) {
                this.cid = _.uniqueId('coll');
            }

            _.defer(_.bind(function () {
                streaming.add(this, options);
            }, this));
        },

        unstream: function () {
            streaming.remove(this);
        },

        emit: function (method, args, options) {
            if (_.isUndefined(options)) {
                options = args ? _.clone(args) : {};
                args = undefined;
            } else {
                options = options ? _.clone(options) : {};
            }

            var model = this;

            var triggerEmit = _.bind(function() {
                model.trigger('emit emit:' + method, model, options);
            }, this);

            options.emit = method;
            options.arguments = args;

            var success = options.success;
            options.success = function (resp) {
                if (success) success(model, resp, options);
                if (options.wait) triggerEmit();
            };
            var error = options.error;
            options.error = function (resp) {
                if (error) error(model, resp, options);
                model.trigger('error', model, resp, options);
            }

            var xhr = this.sync('emit', this, options);
            if (!options.wait) triggerEmit();
            return xhr;
        },

        sync: sync
    });

    window.Streaming = streaming;
})();

<|endoftext|>