<commit_before>/**
 * Views for applicaiton.  A single View to handle most of
 * the applicaiton logic as the model side is light.
 */
(function($, window, undefined) {

// Check our namespace
window.MinnPost = window.MinnPost || {};

var BikeApplication = window.MinnPost.BikeApplication = Backbone.View.extend({

  // Various options, easy to set and change.
  options: {
    'mapOptions': {
      'minZoom': 8,
      'maxZoom': 15
    },
    'mapID': 'map',
    'mapDefaultCenter': new L.LatLng(44.96552667056723, -93.25298309326172),
    'mapDefaultZoom': 12,
    'dataDir': 'data',
    'dataDirRemote': 'https://s3.amazonaws.com/data.minnpost/projects/minnpost-nice-ride/data',
    'loadingSelector': '.loading',
    'loadingMessageSelector': '.loading .loading-message',
    'controlsContainerSelector': '.leaflet-top.leaflet-left',
    'flotOptions': {
      xaxis: { mode: 'time', timeformat: '%h:%M %p' },
      yaxis: { ticks: 2 },
      crosshair: { mode: 'x', lineWidth: 3, color: '#1B8ADA' },
      legend: { backgroundColor: 'inherit', border: null },
      grid: { borderWidth: 1, borderColor: '#BBBBBB' }
    },
    'flotAverageData': {
      label: 'Avg bike density',
      data: [],
      color: '#7F3E10',
      lines: { show: true, fill: true }
    },
    'flotSelector': '#density-average-graph',
    'flotContainerSelector': '.leaflet-bottom.leaflet-left',
    'timeContainerSelector': '.leaflet-bottom.leaflet-right',
    'timeAnimDaySecs': 24 * 60 * 60,
    'timeAnimLengthSecs': 5 * 60,
    'timeAnimInterval': 100,
    'timeAnimDate': {
      'y': 2011,
      'm': 4, // Month is zero based
      'd': 18
    },
    'timeAnimTimeOffsetSecs': ((4 * 60) + 30) * 60 // 4:30
  },

  // Default objects for holding data.
  bikeAnimations: {},
  rentals: {},
  routes: {},
  densityAverage: {},
  timelineFlot: {},
  bikeAnimations: new MinnPost.BikeAnimations(),

  // Events
  events: {
    'click .control-play': 'play',
    'click .control-pause': 'pause'
  },

  // Initialize function when View is first initialized.
  initialize: function() {
    // Mark a loading; create map; load data;
    this.isLoading()
      .checkHost()
      .createMap()
      .loadDataRentals()
      .loadDataRoutes()
      .loadDataDensityAverage();
  },

  // Render function.
  render: function() {

  },

  // Called when loading all data.  We are not actually sure
  // what is done yet, so check and then fire if all is loaded.
  dataLoaded: function() {
    if (!_.isEmpty(this.rentals) && !_.isEmpty(this.routes) && !_.isEmpty(this.densityAverage)) {
      this.drawGraph()
        .makeBikeAnimations()
        .makeAnimator()
        .addControls()
        .doneLoading();
    }

    return this;
  },

  // Play main animation
  play: function(e) {
    e.preventDefault();

    // Change button to pause
    $('.control-play-pause').addClass('control-pause').removeClass('control-play').attr('title', 'Pause');

    // Ensure that the animation is loaded
    if (this.animation !== undefined && !_.isEmpty(this.animation)) {
      this.animation.seekTo(1);
      // Start all already started animations
      this.bikeAnimations.each(function(anim) {
        if (anim.animation.state > 0 && anim.animation.state < 1) {
          anim.animation.seekTo(1);
        }
      });
    }

    return this;
  },

  // Pause animation
  pause: function(e) {
    e.preventDefault();

    // Change button to play
    $('.control-play-pause').removeClass('control-pause').addClass('control-play').attr('title', 'Play');

    // Ensure that the animation is loaded
    if (this.animation !== undefined && !_.isEmpty(this.animation)) {
      this.animation.stop();
      // Pause all animations.  Do all as some are missed in the mixed
      this.bikeAnimations.each(function(anim) {
        anim.animation.stop();
      });
    }

    return this;
  },

  // Pause animation
  resetAnimations: function() {
    // Change button to play
    $('.control-play-pause').removeClass('control-pause').addClass('control-play').attr('title', 'Play');

    // Stop and change all states to 0.
    if (this.animation !== undefined && !_.isEmpty(this.animation)) {
      this.animation.stop();
      this.animation.state = 0;
      this.bikeAnimations.each(function(anim) {
        anim.resetAnimation();
      });
    }

    return this;
  },

  // Controls
  addControls: function() {
    // Play pause buttons
    var template = _.template($("#template-controls").html(), { });
    $(this.options.controlsContainerSelector).append(template);

    // Time
    template = _.template($("#template-time").html(), { });
    $(this.options.timeContainerSelector).append(template);

    return this;
  },

  // Mark as loading
  isLoading: function() {
    $(this.options.loadingSelector).addClass('is-loading');
    $(this.options.loadingMessageSelector).html('Loading data and animations...');

    return this;
  },

  // Done loading
  doneLoading: function() {
    $(this.options.loadingMessageSelector).html('Loaded.');
    $(this.options.loadingSelector).fadeOut('slow').removeClass('is-loading');

    return this;
  },

  // Create map.
  createMap: function() {
    this.map = new L.Map(this.options.mapID, this.options.mapOptions);
    this.addMapLayers();
    this.map.setView(this.options.mapDefaultCenter, this.options.mapDefaultZoom);
    this.map.addControl(new L.Control.Fullscreen());

    // Move attributioin to footnotes
    this.map.attributionControl.setPrefix('');
    $('.footnote').html($('.footnote').html() + ' ' + this.map.attributionControl._container.innerHTML);
    this.map.removeControl(this.map.attributionControl);

    return this;
  },

  // Add layers to map.
  addMapLayers: function() {
    var thisView = this;

    // Minnnpost base map
    var minnpost = new L.TileLayer('http://{s}.tiles.minnpost.com/minnpost-basemaps/minnpost-minnesota-greyscale-no-labels/{z}/{x}/{y}.png',
     { attribution: 'Map imagery from MinnPost; Map data from OpenStreetMap.',
      scheme: 'tms' });
    this.map.addLayer(minnpost);

    // Labels
    var minnpostLabels = new L.TileLayer('http://{s}.tiles.minnpost.com/minnpost-basemaps/minnpost-minnesota-greyscale-labels/{z}/{x}/{y}.png',
     { scheme: 'tms' });

    // Add route layer with TileJSON, then add labels on top
    wax.tilejson('http://a.tiles.minnpost.com/minnpost-nice-ride/minnpost-nice-ride-routes/tilejson.jsonp', function(tilejson) {
      // Hackaround
      tilejson.tiles = [
        'http://a.tiles.minnpost.com/minnpost-nice-ride/minnpost-nice-ride-routes/{z}/{x}/{y}.png',
        'http://b.tiles.minnpost.com/minnpost-nice-ride/minnpost-nice-ride-routes/{z}/{x}/{y}.png',
        'http://c.tiles.minnpost.com/minnpost-nice-ride/minnpost-nice-ride-routes/{z}/{x}/{y}.png',
        'http://d.tiles.minnpost.com/minnpost-nice-ride/minnpost-nice-ride-routes/{z}/{x}/{y}.png',
      ];
      tilejson.grids = [
        'http://a.tiles.minnpost.com/minnpost-nice-ride/minnpost-nice-ride-routes/{z}/{x}/{y}.grid.json',
        'http://b.tiles.minnpost.com/minnpost-nice-ride/minnpost-nice-ride-routes/{z}/{x}/{y}.grid.json',
        'http://c.tiles.minnpost.com/minnpost-nice-ride/minnpost-nice-ride-routes/{z}/{x}/{y}.grid.json',
        'http://d.tiles.minnpost.com/minnpost-nice-ride/minnpost-nice-ride-routes/{z}/{x}/{y}.grid.json',
      ];
      tilejson.scheme = 'tms';
      tilejson.attribution = 'Data provided by Nice Ride MN; ';

      thisView.map.addLayer(new wax.leaf.connector(tilejson));
      thisView.map.addLayer(minnpostLabels);
      wax.leaf.interaction()
        .map(thisView.map)
        .tilejson(tilejson)
        .on(wax.tooltip().parent(thisView.map._container).events());
    });

    return this;
  },

  // Create flot graph
  drawGraph: function() {
    // We want to insert this in the leaflet map container
    var template = _.template($("#template-flot-timeline").html(), { });
    $(this.options.flotContainerSelector).append(template);

    this.options.flotAverageData.data = this.densityAverage;
    this.timelineFlot = $.plot($(this.options.flotSelector), [this.options.flotAverageData], this.options.flotOptions);
    this.timelineFlot.setCrosshair();
    this.timelineFlot.lockCrosshair();

    return this;
  },

  // Make bike animations
  makeBikeAnimations: function() {
    var i;
    var ba;
    var route;
    var ratio = (this.options.timeAnimLengthSecs / this.options.timeAnimDaySecs);

    // Go through rentals, match route, make a bike animation then add to collection
    for (i in this.rentals) {
      // Check for the route
      if (this.routes[this.rentals[i].st + '-' + this.rentals[i].et] !== undefined) {
        route = this.routes[this.rentals[i].st + '-' + this.rentals[i].et];

        // Add some data
        this.rentals[i].timeRatio = ratio;
        this.rentals[i].animInterval = this.options.timeAnimInterval;
        this.rentals[i].map = this.map;

        // Create model and add to collection.  Model initializes stuff.
        var ba = new MinnPost.BikeAnimation(this.rentals[i], route);
        this.bikeAnimations.add(ba, { silent: true });
      }
    }

    return this;
  },

  // Makes fulls animator
  makeAnimator: function() {
    var thisView = this;

    this.animation = new Animator({
      duration: this.options.timeAnimLengthSecs * 1000,
      interval: this.options.timeAnimInterval,
      transition: Animator.tx.linear,
      onComplete: function() {
        // As stop will call this as well, check for state
        if (this.state == 1) {
          thisView.resetAnimations();
        }
      }
    });

    this.animation.addSubject(function(interval) {
      // Determine the time of day it is in the animation
      var totalSecElapsed = interval * thisView.options.timeAnimDaySecs + thisView.options.timeAnimTimeOffsetSecs;
      var aD = thisView.options.timeAnimDate;
      var timeObj = timeExtender.secondsToTime(totalSecElapsed);
      var currentTime = new XDate(aD.y, aD.m, aD.d, timeObj.h, timeObj.m, timeObj.s, 0, true);

      // Show time
      thisView.showTime(currentTime);

      // Find any animations that have not been played yet
      // that start before now and end after now.  Animation
      // state is between 0 and 1.
      thisView.bikeAnimations.each(function(anim) {
        if (anim.get('s') <= currentTime && anim.get('e') > currentTime && anim.animation.state === 0) {
          anim.animation.play();
        }
      });

      // Update graph.  TODO: Pull in bike density dat and overlay it
      // as it goes.
      //var graph_data = da_flot.getData();
      //graph_data[1].data.push([])
      thisView.timelineFlot.setCrosshair({ x: currentTime.getTime() });
    });

    return this;
  },

  showTime: function(date) {
    $('.time-day').html(date.toUTCString('h:ss tt'));

    return this;
  },

  // Check the host
  checkHost: function() {
    if (location.host.indexOf('localhost') == -1) {
      this.options.dataDir = this.options.dataDirRemote;
    }

    return this;
  },

  // Load rental data.
  loadDataRentals: function() {
    var thisView = this;
    window.callback_rentals = function(rentals) {
      thisView.rentals = rentals;
      thisView.dataLoaded();
    }
    $.ajax({
      type: 'GET',
      dataType: 'jsonp',
      url: thisView.options.dataDir + '/rentals.jsonp?callback=callback_rentals',
      jsonp: 'callback'
    });

    return this;
  },

  // Load routes data.
  loadDataRoutes: function() {
    var thisView = this;
    window.callback_routes = function(routes) {
      thisView.routes = routes;
      thisView.dataLoaded();
    }
    $.ajax({
      type: 'GET',
      dataType: 'jsonp',
      url: thisView.options.dataDir + '/routes.jsonp?callback=callback_routes',
      jsonp: 'callback'
    });

    return this;
  },

  // Load density average data.
  loadDataDensityAverage: function() {
    var thisView = this;
    window.callback_density_average = function(avgs) {
      thisView.densityAverage = avgs;
      thisView.dataLoaded();
    }
    $.ajax({
      type: 'GET',
      dataType: 'jsonp',
      url: thisView.options.dataDir + '/density_average.jsonp?callback=callback_density_average',
      jsonp: 'callback'
    });

    return this;
  }

});

})(jQuery, window);<commit_msg>Fixed second minute bug thing.<commit_after>/**
 * Views for applicaiton.  A single View to handle most of
 * the applicaiton logic as the model side is light.
 */
(function($, window, undefined) {

// Check our namespace
window.MinnPost = window.MinnPost || {};

var BikeApplication = window.MinnPost.BikeApplication = Backbone.View.extend({

  // Various options, easy to set and change.
  options: {
    'mapOptions': {
      'minZoom': 8,
      'maxZoom': 15
    },
    'mapID': 'map',
    'mapDefaultCenter': new L.LatLng(44.96552667056723, -93.25298309326172),
    'mapDefaultZoom': 12,
    'dataDir': 'data',
    'dataDirRemote': 'https://s3.amazonaws.com/data.minnpost/projects/minnpost-nice-ride/data',
    'loadingSelector': '.loading',
    'loadingMessageSelector': '.loading .loading-message',
    'controlsContainerSelector': '.leaflet-top.leaflet-left',
    'flotOptions': {
      xaxis: { mode: 'time', timeformat: '%h:%M %p' },
      yaxis: { ticks: 2 },
      crosshair: { mode: 'x', lineWidth: 3, color: '#1B8ADA' },
      legend: { backgroundColor: 'inherit', border: null },
      grid: { borderWidth: 1, borderColor: '#BBBBBB' }
    },
    'flotAverageData': {
      label: 'Avg bike density',
      data: [],
      color: '#7F3E10',
      lines: { show: true, fill: true }
    },
    'flotSelector': '#density-average-graph',
    'flotContainerSelector': '.leaflet-bottom.leaflet-left',
    'timeContainerSelector': '.leaflet-bottom.leaflet-right',
    'timeAnimDaySecs': 24 * 60 * 60,
    'timeAnimLengthSecs': 5 * 60,
    'timeAnimInterval': 100,
    'timeAnimDate': {
      'y': 2011,
      'm': 4, // Month is zero based
      'd': 18
    },
    'timeAnimTimeOffsetSecs': ((4 * 60) + 30) * 60 // 4:30
  },

  // Default objects for holding data.
  bikeAnimations: {},
  rentals: {},
  routes: {},
  densityAverage: {},
  timelineFlot: {},
  bikeAnimations: new MinnPost.BikeAnimations(),

  // Events
  events: {
    'click .control-play': 'play',
    'click .control-pause': 'pause'
  },

  // Initialize function when View is first initialized.
  initialize: function() {
    // Mark a loading; create map; load data;
    this.isLoading()
      .checkHost()
      .createMap()
      .loadDataRentals()
      .loadDataRoutes()
      .loadDataDensityAverage();
  },

  // Render function.
  render: function() {

  },

  // Called when loading all data.  We are not actually sure
  // what is done yet, so check and then fire if all is loaded.
  dataLoaded: function() {
    if (!_.isEmpty(this.rentals) && !_.isEmpty(this.routes) && !_.isEmpty(this.densityAverage)) {
      this.drawGraph()
        .makeBikeAnimations()
        .makeAnimator()
        .addControls()
        .doneLoading();
    }

    return this;
  },

  // Play main animation
  play: function(e) {
    e.preventDefault();

    // Change button to pause
    $('.control-play-pause').addClass('control-pause').removeClass('control-play').attr('title', 'Pause');

    // Ensure that the animation is loaded
    if (this.animation !== undefined && !_.isEmpty(this.animation)) {
      this.animation.seekTo(1);
      // Start all already started animations
      this.bikeAnimations.each(function(anim) {
        if (anim.animation.state > 0 && anim.animation.state < 1) {
          anim.animation.seekTo(1);
        }
      });
    }

    return this;
  },

  // Pause animation
  pause: function(e) {
    e.preventDefault();

    // Change button to play
    $('.control-play-pause').removeClass('control-pause').addClass('control-play').attr('title', 'Play');

    // Ensure that the animation is loaded
    if (this.animation !== undefined && !_.isEmpty(this.animation)) {
      this.animation.stop();
      // Pause all animations.  Do all as some are missed in the mixed
      this.bikeAnimations.each(function(anim) {
        anim.animation.stop();
      });
    }

    return this;
  },

  // Pause animation
  resetAnimations: function() {
    // Change button to play
    $('.control-play-pause').removeClass('control-pause').addClass('control-play').attr('title', 'Play');

    // Stop and change all states to 0.
    if (this.animation !== undefined && !_.isEmpty(this.animation)) {
      this.animation.stop();
      this.animation.state = 0;
      this.bikeAnimations.each(function(anim) {
        anim.resetAnimation();
      });
    }

    return this;
  },

  // Controls
  addControls: function() {
    // Play pause buttons
    var template = _.template($("#template-controls").html(), { });
    $(this.options.controlsContainerSelector).append(template);

    // Time
    template = _.template($("#template-time").html(), { });
    $(this.options.timeContainerSelector).append(template);

    return this;
  },

  // Mark as loading
  isLoading: function() {
    $(this.options.loadingSelector).addClass('is-loading');
    $(this.options.loadingMessageSelector).html('Loading data and animations...');

    return this;
  },

  // Done loading
  doneLoading: function() {
    $(this.options.loadingMessageSelector).html('Loaded.');
    $(this.options.loadingSelector).fadeOut('slow').removeClass('is-loading');

    return this;
  },

  // Create map.
  createMap: function() {
    this.map = new L.Map(this.options.mapID, this.options.mapOptions);
    this.addMapLayers();
    this.map.setView(this.options.mapDefaultCenter, this.options.mapDefaultZoom);
    this.map.addControl(new L.Control.Fullscreen());

    // Move attributioin to footnotes
    this.map.attributionControl.setPrefix('');
    $('.footnote').html($('.footnote').html() + ' ' + this.map.attributionControl._container.innerHTML);
    this.map.removeControl(this.map.attributionControl);

    return this;
  },

  // Add layers to map.
  addMapLayers: function() {
    var thisView = this;

    // Minnnpost base map
    var minnpost = new L.TileLayer('http://{s}.tiles.minnpost.com/minnpost-basemaps/minnpost-minnesota-greyscale-no-labels/{z}/{x}/{y}.png',
     { attribution: 'Map imagery from MinnPost; Map data from OpenStreetMap.',
      scheme: 'tms' });
    this.map.addLayer(minnpost);

    // Labels
    var minnpostLabels = new L.TileLayer('http://{s}.tiles.minnpost.com/minnpost-basemaps/minnpost-minnesota-greyscale-labels/{z}/{x}/{y}.png',
     { scheme: 'tms' });

    // Add route layer with TileJSON, then add labels on top
    wax.tilejson('http://a.tiles.minnpost.com/minnpost-nice-ride/minnpost-nice-ride-routes/tilejson.jsonp', function(tilejson) {
      // Hackaround
      tilejson.tiles = [
        'http://a.tiles.minnpost.com/minnpost-nice-ride/minnpost-nice-ride-routes/{z}/{x}/{y}.png',
        'http://b.tiles.minnpost.com/minnpost-nice-ride/minnpost-nice-ride-routes/{z}/{x}/{y}.png',
        'http://c.tiles.minnpost.com/minnpost-nice-ride/minnpost-nice-ride-routes/{z}/{x}/{y}.png',
        'http://d.tiles.minnpost.com/minnpost-nice-ride/minnpost-nice-ride-routes/{z}/{x}/{y}.png',
      ];
      tilejson.grids = [
        'http://a.tiles.minnpost.com/minnpost-nice-ride/minnpost-nice-ride-routes/{z}/{x}/{y}.grid.json',
        'http://b.tiles.minnpost.com/minnpost-nice-ride/minnpost-nice-ride-routes/{z}/{x}/{y}.grid.json',
        'http://c.tiles.minnpost.com/minnpost-nice-ride/minnpost-nice-ride-routes/{z}/{x}/{y}.grid.json',
        'http://d.tiles.minnpost.com/minnpost-nice-ride/minnpost-nice-ride-routes/{z}/{x}/{y}.grid.json',
      ];
      tilejson.scheme = 'tms';
      tilejson.attribution = 'Data provided by Nice Ride MN; ';

      thisView.map.addLayer(new wax.leaf.connector(tilejson));
      thisView.map.addLayer(minnpostLabels);
      wax.leaf.interaction()
        .map(thisView.map)
        .tilejson(tilejson)
        .on(wax.tooltip().parent(thisView.map._container).events());
    });

    return this;
  },

  // Create flot graph
  drawGraph: function() {
    // We want to insert this in the leaflet map container
    var template = _.template($("#template-flot-timeline").html(), { });
    $(this.options.flotContainerSelector).append(template);

    this.options.flotAverageData.data = this.densityAverage;
    this.timelineFlot = $.plot($(this.options.flotSelector), [this.options.flotAverageData], this.options.flotOptions);
    this.timelineFlot.setCrosshair();
    this.timelineFlot.lockCrosshair();

    return this;
  },

  // Make bike animations
  makeBikeAnimations: function() {
    var i;
    var ba;
    var route;
    var ratio = (this.options.timeAnimLengthSecs / this.options.timeAnimDaySecs);

    // Go through rentals, match route, make a bike animation then add to collection
    for (i in this.rentals) {
      // Check for the route
      if (this.routes[this.rentals[i].st + '-' + this.rentals[i].et] !== undefined) {
        route = this.routes[this.rentals[i].st + '-' + this.rentals[i].et];

        // Add some data
        this.rentals[i].timeRatio = ratio;
        this.rentals[i].animInterval = this.options.timeAnimInterval;
        this.rentals[i].map = this.map;

        // Create model and add to collection.  Model initializes stuff.
        var ba = new MinnPost.BikeAnimation(this.rentals[i], route);
        this.bikeAnimations.add(ba, { silent: true });
      }
    }

    return this;
  },

  // Makes fulls animator
  makeAnimator: function() {
    var thisView = this;

    this.animation = new Animator({
      duration: this.options.timeAnimLengthSecs * 1000,
      interval: this.options.timeAnimInterval,
      transition: Animator.tx.linear,
      onComplete: function() {
        // As stop will call this as well, check for state
        if (this.state == 1) {
          thisView.resetAnimations();
        }
      }
    });

    this.animation.addSubject(function(interval) {
      // Determine the time of day it is in the animation
      var totalSecElapsed = interval * thisView.options.timeAnimDaySecs + thisView.options.timeAnimTimeOffsetSecs;
      var aD = thisView.options.timeAnimDate;
      var timeObj = timeExtender.secondsToTime(totalSecElapsed);
      var currentTime = new XDate(aD.y, aD.m, aD.d, timeObj.h, timeObj.m, timeObj.s, 0, true);

      // Show time
      thisView.showTime(currentTime);

      // Find any animations that have not been played yet
      // that start before now and end after now.  Animation
      // state is between 0 and 1.
      thisView.bikeAnimations.each(function(anim) {
        if (anim.get('s') <= currentTime && anim.get('e') > currentTime && anim.animation.state === 0) {
          anim.animation.play();
        }
      });

      // Update graph.  TODO: Pull in bike density dat and overlay it
      // as it goes.
      //var graph_data = da_flot.getData();
      //graph_data[1].data.push([])
      thisView.timelineFlot.setCrosshair({ x: currentTime.getTime() });
    });

    return this;
  },

  showTime: function(date) {
    $('.time-day').html(date.toUTCString('h:mm tt'));

    return this;
  },

  // Check the host
  checkHost: function() {
    if (location.host.indexOf('localhost') == -1) {
      this.options.dataDir = this.options.dataDirRemote;
    }

    return this;
  },

  // Load rental data.
  loadDataRentals: function() {
    var thisView = this;
    window.callback_rentals = function(rentals) {
      thisView.rentals = rentals;
      thisView.dataLoaded();
    }
    $.ajax({
      type: 'GET',
      dataType: 'jsonp',
      url: thisView.options.dataDir + '/rentals.jsonp?callback=callback_rentals',
      jsonp: 'callback'
    });

    return this;
  },

  // Load routes data.
  loadDataRoutes: function() {
    var thisView = this;
    window.callback_routes = function(routes) {
      thisView.routes = routes;
      thisView.dataLoaded();
    }
    $.ajax({
      type: 'GET',
      dataType: 'jsonp',
      url: thisView.options.dataDir + '/routes.jsonp?callback=callback_routes',
      jsonp: 'callback'
    });

    return this;
  },

  // Load density average data.
  loadDataDensityAverage: function() {
    var thisView = this;
    window.callback_density_average = function(avgs) {
      thisView.densityAverage = avgs;
      thisView.dataLoaded();
    }
    $.ajax({
      type: 'GET',
      dataType: 'jsonp',
      url: thisView.options.dataDir + '/density_average.jsonp?callback=callback_density_average',
      jsonp: 'callback'
    });

    return this;
  }

});

})(jQuery, window);<|endoftext|>


<commit_before>import Ember from 'ember';
import Configuration from '../configuration';
import TokenAuthenticator from './token';

/**
  Extended Token Authenticator that includes refresh by default.

  _The factory for this authenticator is registered as
  `'simple-auth-authenticator:jwt'` in Ember's container._

  @class Jwt
  @namespace SimpleAuth.Authenticators
  @module simple-auth-token/authenticators/jwt
  @extends TokenAuthenticator
*/
export default TokenAuthenticator.extend({
  /**
    The endpoint on the server for refreshing a token.
    @property serverTokenRefreshEndpoint
    @type String
    @default '/api-token-refresh/'
  */
  serverTokenRefreshEndpoint: '/api-token-refresh/',

  /**
    Sets whether the authenticator automatically refreshes access tokens.
    @property refreshAccessTokens
    @type Boolean
    @default true
  */
  refreshAccessTokens: true,

  /**
    The amount of time to wait before refreshing the token - set automatically.
    @property refreshTokenTimeout
    @private
  */
  refreshTokenTimeout: null,

  /**
    The name for which decoded token field represents the token expire time.
    @property tokenExpireName
    @type String
    @default 'exp'
  */
  tokenExpireName: 'exp',

  /**
    Default time unit.
    @property timeFactor
    @type Integer
    @default 1 (seconds)
  */
  timeFactor: 1,

  /**
    @method init
    @private
  */
  init: function() {
    this.serverTokenEndpoint = Configuration.serverTokenEndpoint;
    this.serverTokenRefreshEndpoint = Configuration.serverTokenRefreshEndpoint;
    this.identificationField = Configuration.identificationField;
    this.tokenPropertyName = Configuration.tokenPropertyName;
    this.refreshAccessTokens = Configuration.refreshAccessTokens;
    this.tokenExpireName = Configuration.tokenExpireName;
    this.timeFactor = Configuration.timeFactor;
  },

  /**
    Restores the session from a set of session properties; __will return a
    resolving promise when there's a non-empty `token` in the `data`__
    and a rejecting promise otherwise.
    This method also schedules automatic token refreshing when there are values
    for `token` and `expiresAt` in the `data` and automatic token
    refreshing is not disabled.
    @method restore
    @param {Object} data The data to restore the session from
    @return {Ember.RSVP.Promise} A promise that when it resolves results in the session being authenticated
  */
  restore: function(data){
    var _this = this;
    return new Ember.RSVP.Promise(function(resolve, reject){
      var now = (new Date()).getTime();
      if(!Ember.isEmpty(data.expiresAt) && data.expiresAt < now){
        if(_this.refreshAccessTokens){
          _this.refreshAccessToken(data.token).then(function(data){
            resolve(data);
          }, reject);
        }else{
          reject();
        }
      }else{
        if(Ember.isEmpty(data.token)){
          reject();
        }else{
          _this.scheduleAccessTokenRefresh(data.expiresAt, data.token);
          resolve(data);
        }
      }
    });
  },

  /**
    Authenticates the session with the specified `credentials`; the credentials
    are `POST`ed to the
    [`Authenticators.Token#serverTokenEndpoint`](#SimpleAuth-Authenticators-Token-serverTokenEndpoint)
    and if they are valid the server returns an auth token in
    response. __If the credentials are valid and authentication succeeds, a
    promise that resolves with the server's response is returned__, otherwise a
    promise that rejects with the server error is returned.

    @method authenticate
    @param {Object} options The credentials to authenticate the session with
    @return {Ember.RSVP.Promise} A promise that resolves when an auth token is successfully acquired from the server and rejects otherwise
  */
  authenticate: function(credentials) {
    var _this = this;
    return new Ember.RSVP.Promise(function(resolve, reject) {
      var data = _this.getAuthenticateData(credentials);
      _this.makeRequest(_this.serverTokenEndpoint, data).then(function(response) {
        Ember.run(function() {
          var tokenData = _this.getTokenData(response),
              expiresAt = tokenData[_this.tokenExpireName];
          _this.scheduleAccessTokenRefresh(expiresAt, response.token);
          response = Ember.merge(response, { expiresAt: expiresAt });
          resolve(_this.getResponseData(response));
        });
      }, function(xhr) {
        Ember.run(function() {
          reject(xhr.responseJSON || xhr.responseText);
        });
      });
    });
  },

  /**
    Cancels any outstanding automatic token refreshes and returns a resolving
    promise.
    @method invalidate
    @param {Object} data The data of the session to be invalidated
    @return {Ember.RSVP.Promise} A resolving promise
  */
  invalidate: function(data) {
    Ember.run.cancel(this._refreshTokenTimeout);
    delete this._refreshTokenTimeout;
    return new Ember.RSVP.resolve();
  },

  /**
    @method scheduleAccessTokenRefresh
    @private
  */
  scheduleAccessTokenRefresh: function(expiresAt, token) {
    if(this.refreshAccessTokens){
      expiresAt = this.resolveTime(expiresAt);
      var now = (new Date()).getTime(),
        wait = expiresAt - now;
      if(!Ember.isEmpty(token) && !Ember.isEmpty(expiresAt) && expiresAt > now){
        Ember.run.cancel(this._refreshTokenTimeout);
        delete this._refreshTokenTimeout;
        if(!Ember.testing){
          this._refreshTokenTimeout = Ember.run.later(this, this.refreshAccessToken, token, wait);
        }
      }
    }
  },

  /**
    Handles converting between time units for data between different systems.
    Default: seconds(1)
    @method resolveTime
    @private
  */
  resolveTime: function(time){
    return new Date(time * this.timeFactor).getTime();
  },

  /**
    @method refreshAccessToken
    @private
  */
  refreshAccessToken: function(token) {
    var _this = this;
    var data  = {token: token};
    return new Ember.RSVP.Promise(function(resolve, reject) {
      _this.makeRequest(_this.serverTokenRefreshEndpoint, data).then(function(response) {
        Ember.run(function() {
          var tokenData = _this.getTokenData(response),
            expiresAt = tokenData[_this.tokenExpireName],
            data = Ember.merge(response, {expiresAt: expiresAt, token: response.token});
          _this.scheduleAccessTokenRefresh(expiresAt, response.token);
          _this.trigger('sessionDataUpdated', data);
          resolve(response);
        });
      }, function(xhr, status, error) {
        Ember.Logger.warn('Access token could not be refreshed - server responded with ' + error + '.');
        reject();
      });
    });
  },

  /**
    Returns the decoded token with accessible returned values.

    @method getTokenData
    @return {object} An object with properties for the session.
  */
  getTokenData: function(response) {
    var token = response.token.split('.')[1];
    return JSON.parse(atob(token));
  },

  /**
    @method makeRequest
    @private
  */
  makeRequest: function(url, data) {
    return Ember.$.ajax({
      url: url,
      type: 'POST',
      data: JSON.stringify(data),
      dataType: 'json',
      contentType: 'application/json',
      beforeSend: function(xhr, settings) {
        xhr.setRequestHeader('Accept', settings.accepts.json);
      }
    });
  },
});
<commit_msg>Removed unused data arg<commit_after>import Ember from 'ember';
import Configuration from '../configuration';
import TokenAuthenticator from './token';

/**
  Extended Token Authenticator that includes refresh by default.

  _The factory for this authenticator is registered as
  `'simple-auth-authenticator:jwt'` in Ember's container._

  @class Jwt
  @namespace SimpleAuth.Authenticators
  @module simple-auth-token/authenticators/jwt
  @extends TokenAuthenticator
*/
export default TokenAuthenticator.extend({
  /**
    The endpoint on the server for refreshing a token.
    @property serverTokenRefreshEndpoint
    @type String
    @default '/api-token-refresh/'
  */
  serverTokenRefreshEndpoint: '/api-token-refresh/',

  /**
    Sets whether the authenticator automatically refreshes access tokens.
    @property refreshAccessTokens
    @type Boolean
    @default true
  */
  refreshAccessTokens: true,

  /**
    The amount of time to wait before refreshing the token - set automatically.
    @property refreshTokenTimeout
    @private
  */
  refreshTokenTimeout: null,

  /**
    The name for which decoded token field represents the token expire time.
    @property tokenExpireName
    @type String
    @default 'exp'
  */
  tokenExpireName: 'exp',

  /**
    Default time unit.
    @property timeFactor
    @type Integer
    @default 1 (seconds)
  */
  timeFactor: 1,

  /**
    @method init
    @private
  */
  init: function() {
    this.serverTokenEndpoint = Configuration.serverTokenEndpoint;
    this.serverTokenRefreshEndpoint = Configuration.serverTokenRefreshEndpoint;
    this.identificationField = Configuration.identificationField;
    this.tokenPropertyName = Configuration.tokenPropertyName;
    this.refreshAccessTokens = Configuration.refreshAccessTokens;
    this.tokenExpireName = Configuration.tokenExpireName;
    this.timeFactor = Configuration.timeFactor;
  },

  /**
    Restores the session from a set of session properties; __will return a
    resolving promise when there's a non-empty `token` in the `data`__
    and a rejecting promise otherwise.
    This method also schedules automatic token refreshing when there are values
    for `token` and `expiresAt` in the `data` and automatic token
    refreshing is not disabled.
    @method restore
    @param {Object} data The data to restore the session from
    @return {Ember.RSVP.Promise} A promise that when it resolves results in the session being authenticated
  */
  restore: function(data){
    var _this = this;
    return new Ember.RSVP.Promise(function(resolve, reject){
      var now = (new Date()).getTime();
      if(!Ember.isEmpty(data.expiresAt) && data.expiresAt < now){
        if(_this.refreshAccessTokens){
          _this.refreshAccessToken(data.token).then(function(data){
            resolve(data);
          }, reject);
        }else{
          reject();
        }
      }else{
        if(Ember.isEmpty(data.token)){
          reject();
        }else{
          _this.scheduleAccessTokenRefresh(data.expiresAt, data.token);
          resolve(data);
        }
      }
    });
  },

  /**
    Authenticates the session with the specified `credentials`; the credentials
    are `POST`ed to the
    [`Authenticators.Token#serverTokenEndpoint`](#SimpleAuth-Authenticators-Token-serverTokenEndpoint)
    and if they are valid the server returns an auth token in
    response. __If the credentials are valid and authentication succeeds, a
    promise that resolves with the server's response is returned__, otherwise a
    promise that rejects with the server error is returned.

    @method authenticate
    @param {Object} options The credentials to authenticate the session with
    @return {Ember.RSVP.Promise} A promise that resolves when an auth token is successfully acquired from the server and rejects otherwise
  */
  authenticate: function(credentials) {
    var _this = this;
    return new Ember.RSVP.Promise(function(resolve, reject) {
      var data = _this.getAuthenticateData(credentials);
      _this.makeRequest(_this.serverTokenEndpoint, data).then(function(response) {
        Ember.run(function() {
          var tokenData = _this.getTokenData(response),
              expiresAt = tokenData[_this.tokenExpireName];
          _this.scheduleAccessTokenRefresh(expiresAt, response.token);
          response = Ember.merge(response, { expiresAt: expiresAt });
          resolve(_this.getResponseData(response));
        });
      }, function(xhr) {
        Ember.run(function() {
          reject(xhr.responseJSON || xhr.responseText);
        });
      });
    });
  },

  /**
    Cancels any outstanding automatic token refreshes and returns a resolving
    promise.
    @method invalidate
    @param {Object} data The data of the session to be invalidated
    @return {Ember.RSVP.Promise} A resolving promise
  */
  invalidate: function() {
    Ember.run.cancel(this._refreshTokenTimeout);
    delete this._refreshTokenTimeout;
    return new Ember.RSVP.resolve();
  },

  /**
    @method scheduleAccessTokenRefresh
    @private
  */
  scheduleAccessTokenRefresh: function(expiresAt, token) {
    if(this.refreshAccessTokens){
      expiresAt = this.resolveTime(expiresAt);
      var now = (new Date()).getTime(),
        wait = expiresAt - now;
      if(!Ember.isEmpty(token) && !Ember.isEmpty(expiresAt) && expiresAt > now){
        Ember.run.cancel(this._refreshTokenTimeout);
        delete this._refreshTokenTimeout;
        if(!Ember.testing){
          this._refreshTokenTimeout = Ember.run.later(this, this.refreshAccessToken, token, wait);
        }
      }
    }
  },

  /**
    Handles converting between time units for data between different systems.
    Default: seconds(1)
    @method resolveTime
    @private
  */
  resolveTime: function(time){
    return new Date(time * this.timeFactor).getTime();
  },

  /**
    @method refreshAccessToken
    @private
  */
  refreshAccessToken: function(token) {
    var _this = this;
    var data  = {token: token};
    return new Ember.RSVP.Promise(function(resolve, reject) {
      _this.makeRequest(_this.serverTokenRefreshEndpoint, data).then(function(response) {
        Ember.run(function() {
          var tokenData = _this.getTokenData(response),
            expiresAt = tokenData[_this.tokenExpireName],
            data = Ember.merge(response, {expiresAt: expiresAt, token: response.token});
          _this.scheduleAccessTokenRefresh(expiresAt, response.token);
          _this.trigger('sessionDataUpdated', data);
          resolve(response);
        });
      }, function(xhr, status, error) {
        Ember.Logger.warn('Access token could not be refreshed - server responded with ' + error + '.');
        reject();
      });
    });
  },

  /**
    Returns the decoded token with accessible returned values.

    @method getTokenData
    @return {object} An object with properties for the session.
  */
  getTokenData: function(response) {
    var token = response.token.split('.')[1];
    return JSON.parse(atob(token));
  },

  /**
    @method makeRequest
    @private
  */
  makeRequest: function(url, data) {
    return Ember.$.ajax({
      url: url,
      type: 'POST',
      data: JSON.stringify(data),
      dataType: 'json',
      contentType: 'application/json',
      beforeSend: function(xhr, settings) {
        xhr.setRequestHeader('Accept', settings.accepts.json);
      }
    });
  },
});
<|endoftext|>


<commit_before>import Ember from 'ember';
import layout from '../templates/components/ui-icon';
import SharedStylist from 'ember-cli-stylist/mixins/shared-stylist';
const { keys, create } = Object; // jshint ignore:line
const {computed, observer, $, A, run, on, typeOf, debug, defineProperty, get, set, inject, isEmpty} = Ember;  // jshint ignore:line

const icon = Ember.Component.extend(SharedStylist, {
  layout: layout,
  tagName: '',

  styleBindings: ['fontSize', 'color', 'width', 'height', 'padding','borderRadius', 'background', 'fontWeight', 'border', 'opacity', 'cursor'],
  fontFamily: 'fa',
  role: undefined,
  _role: computed('role', function() {
    const {role, pointer, cursor} = this.getProperties('role', 'pointer', 'cursor');

    if (role) {
      return role;
    } else {
      return pointer || cursor === 'pointer' ? 'link' : 'img';
    }
  }),
  labeledBy: undefined,
  _labeledBy: computed('labeledBy', function() {
    const {labeledBy, icon} = this.getProperties('labeledBy', 'icon');

    return labeledBy ? labeledBy : (icon || String()).split('-')[0];
  }),
  fw: true,
  _fw: computed('fw', function() {
    let fw = this.get('fw');
    let fontFamily = this.get('fontFamily');
    return fw ? ` ${fontFamily}-fw` : null;
  }),
  _icon: computed('icon', function() {
    let family = this.get('fontFamily');
    let icon = this.get('icon');
    return icon ? ` ${family}-${icon}` : null;
  }),
  spin: null,
  _spin: computed('spin', function() {
    return this.get('spin') ? ` ${this.get('fontFamily')}-spin` : '';
  }),
  pulse: null,
  _pulse: computed('pulse', function() {
    return this.get('pulse') ? ` ${this.get('fontFamily')}-pulse` : '';
  }),
  size: computed.alias('fontSize'),
  color: null,
  border: false,
  circular: false,
  padded: false,
  value: null, // values are passed by an action
  _value: computed('value', 'elementId', function() {
    let { value, elementId } = this.getProperties('value','elementId');
    return value !== null ? value : elementId;
  }),

  mood: null,
  _mood: computed('mood', function() {
    const {mood} = this.getProperties('mood');
    return mood ? ` text-${mood}` : '';
  }),

  opacity: computed('muted', {
    set(_, value) {
      return value;
    },
    get() {
      return this.get('muted') ? 0.5 : 1;
    }
  }),
  _pointer: computed('pointer', function() {
    const pointer = this.get('pointer');
    if(typeOf(pointer) === 'boolean' && pointer) {
      return ' pointer';
    } else {
      return '';
    }
  }),

  actions: {
    onClick: function(evt) {
      if(typeOf(this.attrs.onClick) === 'function') {
        this.attrs.onClick({
          event: evt,
          object: this,
          value: get(this, 'value'),
          style: 'closure'
        });
      } else {
        this.sendAction('onClick', {
          event: evt,
          object: this,
          value: get(this, 'value'),
          style: 'classic'
        });
      }
    },
  }

});

icon.reopenClass({
  positionalParams: ['icon']
});
icon[Ember.NAME_KEY] = 'ui-icon';
export default icon;
<commit_msg>ensure DOM ID is set<commit_after>import Ember from 'ember';
import layout from '../templates/components/ui-icon';
import SharedStylist from 'ember-cli-stylist/mixins/shared-stylist';
const { keys, create } = Object; // jshint ignore:line
const {computed, observer, $, A, run, on, typeOf, debug, defineProperty, get, set, inject, isEmpty} = Ember;  // jshint ignore:line

const icon = Ember.Component.extend(SharedStylist, {
  layout: layout,
  tagName: '',
  init() {
    this._super(...arguents);
    if(!this.get('elementId')) {
      this.set('elementId', 'ember-' + Math.random().toString(36).substr(2, 9));
    }
  },

  styleBindings: ['fontSize', 'color', 'width', 'height', 'padding','borderRadius', 'background', 'fontWeight', 'border', 'opacity', 'cursor'],
  fontFamily: 'fa',
  role: undefined,
  _role: computed('role', function() {
    const {role, pointer, cursor} = this.getProperties('role', 'pointer', 'cursor');

    if (role) {
      return role;
    } else {
      return pointer || cursor === 'pointer' ? 'link' : 'img';
    }
  }),
  labeledBy: undefined,
  _labeledBy: computed('labeledBy', function() {
    const {labeledBy, icon} = this.getProperties('labeledBy', 'icon');

    return labeledBy ? labeledBy : (icon || String()).split('-')[0];
  }),
  fw: true,
  _fw: computed('fw', function() {
    let fw = this.get('fw');
    let fontFamily = this.get('fontFamily');
    return fw ? ` ${fontFamily}-fw` : null;
  }),
  _icon: computed('icon', function() {
    let family = this.get('fontFamily');
    let icon = this.get('icon');
    return icon ? ` ${family}-${icon}` : null;
  }),
  spin: null,
  _spin: computed('spin', function() {
    return this.get('spin') ? ` ${this.get('fontFamily')}-spin` : '';
  }),
  pulse: null,
  _pulse: computed('pulse', function() {
    return this.get('pulse') ? ` ${this.get('fontFamily')}-pulse` : '';
  }),
  size: computed.alias('fontSize'),
  color: null,
  border: false,
  circular: false,
  padded: false,
  value: null, // values are passed by an action
  _value: computed('value', 'elementId', function() {
    let { value, elementId } = this.getProperties('value','elementId');
    return value !== null ? value : elementId;
  }),

  mood: null,
  _mood: computed('mood', function() {
    const {mood} = this.getProperties('mood');
    return mood ? ` text-${mood}` : '';
  }),

  opacity: computed('muted', {
    set(_, value) {
      return value;
    },
    get() {
      return this.get('muted') ? 0.5 : 1;
    }
  }),
  _pointer: computed('pointer', function() {
    const pointer = this.get('pointer');
    if(typeOf(pointer) === 'boolean' && pointer) {
      return ' pointer';
    } else {
      return '';
    }
  }),

  actions: {
    onClick: function(evt) {
      if(typeOf(this.attrs.onClick) === 'function') {
        this.attrs.onClick({
          event: evt,
          object: this,
          value: get(this, 'value'),
          style: 'closure'
        });
      } else {
        this.sendAction('onClick', {
          event: evt,
          object: this,
          value: get(this, 'value'),
          style: 'classic'
        });
      }
    },
  }

});

icon.reopenClass({
  positionalParams: ['icon']
});
icon[Ember.NAME_KEY] = 'ui-icon';
export default icon;
<|endoftext|>


<commit_before>// Inspired by Lauren Tan
// https://medium.com/delightful-ui-for-ember-apps/ember-js-detecting-if-a-dom-element-is-in-the-viewport-eafcc77a6f86

import Ember from 'ember';

var on           = Ember.on;
var get          = Ember.get;
var Mixin        = Ember.Mixin;
var debounce     = Ember.run.debounce;
var scheduleOnce = Ember.run.scheduleOnce;

export default Mixin.create({
  scrollTimeout:   100,
  enteredViewport: null,
  threshold:       0,

  _setViewport: function() {
    var rect      = this.$() ? this.$()[0].getBoundingClientRect() : null;
    var threshold = get(this, 'threshold');

    if (!rect) {
      return;
    }

    this.set('enteredViewport',
      rect.top >= 0 &&
      rect.left >= 0 &&
      (rect.bottom - threshold) <= (window.innerHeight || document.documentElement.clientHeight) &&
      rect.right <= (window.innerWidth || document.documentElement.clientWidth)
    );
  },

  _setInitialViewport: on('didInsertElement', function() {
    scheduleOnce('afterRender', this, function() {
      this._setViewport();
    });
  }),

  _scrollHandler: function() {
    debounce(this, function() {
      this._setViewport();
    }, get(this, 'scrollTimeout'));
  },

  _bindScroll: on('didInsertElement', function() {
    var component = this;

    Ember.$(document).on('touchmove.scrollable', function() {
      component._scrollHandler();
    });

    Ember.$(window).on('scroll.scrollable', function() {
      component._scrollHandler();
    });

    Ember.$(window).on('resize.resizable', function() {
      component._scrollHandler();
    });
  }),

  _unbindScroll: on('willDestroyElement', function() {
    Ember.$(window).off('.scrollable');
    Ember.$(window).off('.resizable');
    Ember.$(document).off('.scrollable');
  }),
});
<commit_msg>respect threshold on horizontal scrolling<commit_after>// Inspired by Lauren Tan
// https://medium.com/delightful-ui-for-ember-apps/ember-js-detecting-if-a-dom-element-is-in-the-viewport-eafcc77a6f86

import Ember from 'ember';

var on           = Ember.on;
var get          = Ember.get;
var Mixin        = Ember.Mixin;
var debounce     = Ember.run.debounce;
var scheduleOnce = Ember.run.scheduleOnce;

export default Mixin.create({
  scrollTimeout:   100,
  enteredViewport: null,
  threshold:       0,

  _setViewport: function() {
    var rect      = this.$() ? this.$()[0].getBoundingClientRect() : null;
    var threshold = get(this, 'threshold');

    if (!rect) {
      return;
    }

    this.set('enteredViewport',
      rect.top >= 0 &&
      rect.left >= 0 &&
      (rect.bottom - threshold) <= (window.innerHeight || document.documentElement.clientHeight) &&
      (rect.right - threshold) <= (window.innerWidth || document.documentElement.clientWidth)
    );
  },

  _setInitialViewport: on('didInsertElement', function() {
    scheduleOnce('afterRender', this, function() {
      this._setViewport();
    });
  }),

  _scrollHandler: function() {
    debounce(this, function() {
      this._setViewport();
    }, get(this, 'scrollTimeout'));
  },

  _bindScroll: on('didInsertElement', function() {
    var component = this;

    Ember.$(document).on('touchmove.scrollable', function() {
      component._scrollHandler();
    });

    Ember.$(window).on('scroll.scrollable', function() {
      component._scrollHandler();
    });

    Ember.$(window).on('resize.resizable', function() {
      component._scrollHandler();
    });
  }),

  _unbindScroll: on('willDestroyElement', function() {
    Ember.$(window).off('.scrollable');
    Ember.$(window).off('.resizable');
    Ember.$(document).off('.scrollable');
  }),
});
<|endoftext|>


<commit_before>import Ember from 'ember';
import MagicCrud from './magic-crud';

const {
  getProperties
} = Ember;

let capitalize = function(string){
  return string.charAt(0).toUpperCase() + string.slice(1);
}

export default Ember.Mixin.create(MagicCrud, {

  // This is for the objects not to change and mess up the observers
  editDone: false,

  // Validation Object name
  validationObject: 'validations',

  // Definitions Object name
  definitionObject: 'formDefinitionsMC',

  // Magic Crud Options object name
  magicCrudObject: 'magicCrud',

  // Route that corresponds to editing
  editRoute: 'edit',

  // Route that corresponds to adding
  addRoute: 'add',

  // Template for the form
  formTemplate: 'magic-crud/form',

  // Template for the table
  tableTemplate: 'magic-crud/table',

  // Save success message
  saveMessage: 'Record saved successfully',

  // Delete success message
  deleteMessageSuccess: 'Record deleted',

  // Delete fail message
  deleteMessageFailed: 'The record couldn\'t be deleted',

  // Split route name
  routeSplit: Ember.computed('routeName', function(){
    return this.get('routeName').split('.');
  }),

  // Route base name
  routeBase: Ember.computed('routeSplit', function(){
    return this.get('routeSplit')[0];
  }),

  // Route method name
  routeMethod: Ember.computed('routeSplit', function(){
    return this.get('routeSplit')[1];
  }),

  // Route's model
  model: function(param){
    const{
      addRoute,
      editRoute,
      routeBase,
      routeMethod
    } = getProperties(this, 'addRoute', 'editRoute', 'routeBase', 'routeMethod');

    switch (routeMethod) {
      case addRoute:
        return this.store.createRecord(routeBase);
      case editRoute:
        return this.store.findRecord(routeBase, param.id);
      default:
        return this.store.findAll(routeBase);
    }
  },

  // Sets controller's route name related properties
  setControllerRouteNameMethod(){
    const{
      controller,
      routeBase,
      routeMethod,
    } = getProperties(this, 'controller', 'routeBase', 'routeMethod');

    controller.setProperties({
      capitalModelName: (routeBase) ? capitalize(routeBase) : null,
      capitalMethodName: (routeMethod) ? capitalize(routeMethod) : null
    });
  },

  // Mixin and set controller definitions
  mixinAndSetControllerDefinitionObjects(){
    const{
      controller,
      routeBase,
      validationObject,
      definitionObject,
      magicCrudObject,
    } = getProperties(this, 'controller', 'routeBase', 'validationObject', 'definitionObject', 'magicCrudObject');

    controller.reopen(MagicCrud);

    if(!controller.get(validationObject)){
      controller.set(validationObject, this.controllerFor(routeBase).get(validationObject));
    }

    if(!controller.get(definitionObject)){
      controller.set(definitionObject, this.controllerFor(routeBase).get(definitionObject));
    }

    if(!controller.get(magicCrudObject)){
      controller.set(magicCrudObject, this.controllerFor(routeBase).get(magicCrudObject));
    }
  },

  // Set templates for controller rendering
  setTemplatesToRender(){
    const{
      controller,
      routeMethod,
      addRoute,
      editRoute,
      formTemplate,
      tableTemplate
    } = getProperties(this, 'controller', 'routeMethod', 'addRoute', 'editRoute', 'formTemplate', 'tableTemplate');

    let formTemplateRenderer = function(){
      this.render(formTemplate, {
        outlet: 'magic-form',
        controller: controller
      });
    }

    let tableTemplateRenderer = function(){
      this.render(tableTemplate, {
        controller: controller
      });
    }

    if(this.isAnActionRoute()){
      this.set('renderTemplate', formTemplateRenderer);
    }
    else{
      this.set('renderTemplate', tableTemplateRenderer);
    }
  },

  // True if route is edit or add
  isAnActionRoute(){
    const{
      routeMethod,
      addRoute,
      editRoute,
    } = getProperties(this, 'routeMethod', 'addRoute', 'editRoute');

    return routeMethod == addRoute || routeMethod == editRoute;
  },

  // Sets up the Route's Controller
  setupController(controller, model) {
    const{
      addRoute,
      editRoute,
      routeBase,
      routeMethod,
      editDone
    } = getProperties(this, 'addRoute', 'editRoute', 'routeBase', 'routeMethod', 'editDone');

    this._super(controller, model);
    this.setControllerRouteNameMethod();
    this.setTemplatesToRender();

    if(this.isAnActionRoute()){
      if(editDone && routeMethod == editRoute){
        return;
      }
      this.mixinAndSetControllerDefinitionObjects();
      this.set('editDone', true);
    }
    this.controller.init();
  },

  // Success saving record promisse callback
  saveRecordSuccess(){
    let controller = this.get('controller');
    let routeBase = this.get('routeBase');
    let saveMessage = this.get('saveMessage');
    let flashMessages = Ember.get(this, 'flashMessages');

    controller.get('model').save().then(() => {
      let routeAfter;
      if(controller.get('magicCrud') && (routeAfter = controller.get('magicCrud.routeAfter'))){
        controller.transitionTo(routeAfter);
      }
      else{
        controller.transitionTo(routeBase);
      }
      flashMessages.success(saveMessage);
    });
  },

  // Fail saving record promisse callback
  saveRecordFail(){
    let controller = this.get('controller');
    let flashMessages = Ember.get(this, 'flashMessages');
    let definitionObject = this.get('definitionObject');

    let errors = controller.get('errors.model');
    for(let item in errors){
      if(errors.hasOwnProperty(item)){
        let definitions = controller.get(definitionObject);
        for(let def in definitions){
          if(definitions[def] && 'model.'+item === definitions[def].attribute && errors.get(item).length){
            flashMessages.danger(definitions[def].label + ' ' + errors.get(item));
          }
        }
      }
    }
  },

  // Route Actions
  actions: {
    //Delete record from store
    deleteRecord(item){
      const{
        routeBase,
        deleteMessageSuccess,
        deleteMessageFailed
      } = getProperties(this, 'routeBase', 'deleteMessageSuccess', 'deleteMessageFailed');

      this.transitionTo(routeBase);
      let flashMessages = Ember.get(this, 'flashMessages');

      item.deleteRecord();
      item.save().then(() => {
        flashMessages.success(deleteMessageSuccess);
      },() => {
        flashMessages.danger(deleteMessageFailed);
      });
    },

    // Transition to edit route
    editRecord(item){
      const{
        routeBase,
        editRoute
      } = getProperties(this, 'routeBase', 'editRoute');

      this.transitionTo(routeBase + '.' + editRoute, item);
    },

    // Transition to add route
    addRecord(){
      const{
        routeBase,
        addRoute
      } = getProperties(this, 'routeBase', 'addRoute');

      this.transitionTo(routeBase + '.' + addRoute);
    },

    // Save record
    saveRecord(){
      const{
        saveMessage,
        controller
      } = getProperties(this, 'saveMessage', 'controller');

      controller.set('submitted', true);
      controller.validate().then(() => {
        this.saveRecordSuccess();
      }, () => {
        this.saveRecordFail();
      });
    },

    // Rollback model
    willTransition(transition) {
      const{
        definitionObject,
        routeBase,
        routeMethod,
        controller
      } = getProperties(this, 'definitionObject', 'routeBase', 'routeMethod', 'controller');

      if(this.isAnActionRoute()){
        controller.get('model').rollback();
      }
    }
  }
});
<commit_msg>Add model rollback on relationship changes<commit_after>import Ember from 'ember';
import MagicCrud from './magic-crud';

const {
  getProperties
} = Ember;

let capitalize = function(string){
  return string.charAt(0).toUpperCase() + string.slice(1);
}

export default Ember.Mixin.create(MagicCrud, {

  // This is for the objects not to change and mess up the observers
  editDone: false,

  // Validation Object name
  validationObject: 'validations',

  // Definitions Object name
  definitionObject: 'formDefinitionsMC',

  // Magic Crud Options object name
  magicCrudObject: 'magicCrud',

  // Route that corresponds to editing
  editRoute: 'edit',

  // Route that corresponds to adding
  addRoute: 'add',

  // Template for the form
  formTemplate: 'magic-crud/form',

  // Template for the table
  tableTemplate: 'magic-crud/table',

  // Save success message
  saveMessage: 'Record saved successfully',

  // Delete success message
  deleteMessageSuccess: 'Record deleted',

  // Delete fail message
  deleteMessageFailed: 'The record couldn\'t be deleted',

  canRollbackModel: true,

  // Split route name
  routeSplit: Ember.computed('routeName', function(){
    return this.get('routeName').split('.');
  }),

  // Route base name
  routeBase: Ember.computed('routeSplit', function(){
    return this.get('routeSplit')[0];
  }),

  // Route method name
  routeMethod: Ember.computed('routeSplit', function(){
    return this.get('routeSplit')[1];
  }),

  // Route's model
  model: function(param){
    const{
      addRoute,
      editRoute,
      routeBase,
      routeMethod
    } = getProperties(this, 'addRoute', 'editRoute', 'routeBase', 'routeMethod');

    switch (routeMethod) {
      case addRoute:
        return this.store.createRecord(routeBase);
      case editRoute:
        return this.store.findRecord(routeBase, param.id);
      default:
        return this.store.findAll(routeBase);
    }
  },

  // Sets controller's route name related properties
  setControllerRouteNameMethod(){
    const{
      controller,
      routeBase,
      routeMethod,
    } = getProperties(this, 'controller', 'routeBase', 'routeMethod');

    controller.setProperties({
      capitalModelName: (routeBase) ? capitalize(routeBase) : null,
      capitalMethodName: (routeMethod) ? capitalize(routeMethod) : null
    });
  },

  // Mixin and set controller definitions
  mixinAndSetControllerDefinitionObjects(){
    const{
      controller,
      routeBase,
      validationObject,
      definitionObject,
      magicCrudObject,
    } = getProperties(this, 'controller', 'routeBase', 'validationObject', 'definitionObject', 'magicCrudObject');

    controller.reopen(MagicCrud);

    if(!controller.get(validationObject)){
      controller.set(validationObject, this.controllerFor(routeBase).get(validationObject));
    }

    if(!controller.get(definitionObject)){
      controller.set(definitionObject, this.controllerFor(routeBase).get(definitionObject));
    }

    if(!controller.get(magicCrudObject)){
      controller.set(magicCrudObject, this.controllerFor(routeBase).get(magicCrudObject));
    }
  },

  // Set templates for controller rendering
  setTemplatesToRender(){
    const{
      controller,
      routeMethod,
      addRoute,
      editRoute,
      formTemplate,
      tableTemplate
    } = getProperties(this, 'controller', 'routeMethod', 'addRoute', 'editRoute', 'formTemplate', 'tableTemplate');

    let formTemplateRenderer = function(){
      this.render(formTemplate, {
        outlet: 'magic-form',
        controller: controller
      });
    }

    let tableTemplateRenderer = function(){
      this.render(tableTemplate, {
        controller: controller
      });
    }

    if(this.isAnActionRoute()){
      this.set('renderTemplate', formTemplateRenderer);
    }
    else{
      this.set('renderTemplate', tableTemplateRenderer);
    }
  },

  // True if route is edit or add
  isAnActionRoute(){
    const{
      routeMethod,
      addRoute,
      editRoute,
    } = getProperties(this, 'routeMethod', 'addRoute', 'editRoute');

    return routeMethod == addRoute || routeMethod == editRoute;
  },

  // Sets up the Route's Controller
  setupController(controller, model) {
    const{
      addRoute,
      editRoute,
      routeBase,
      routeMethod,
      editDone
    } = getProperties(this, 'addRoute', 'editRoute', 'routeBase', 'routeMethod', 'editDone');

    this._super(controller, model);
    this.setControllerRouteNameMethod();
    this.setTemplatesToRender();

    if(this.isAnActionRoute()){
      if(editDone && routeMethod == editRoute){
        return;
      }
      this.mixinAndSetControllerDefinitionObjects();
      this.set('editDone', true);
    }
    this.controller.init();
  },

  // Success saving record promisse callback
  saveRecordSuccess(){
    let controller = this.get('controller');
    let routeBase = this.get('routeBase');
    let saveMessage = this.get('saveMessage');
    let flashMessages = Ember.get(this, 'flashMessages');

    controller.get('model').save().then(() => {
      let routeAfter;
      if(controller.get('magicCrud') && (routeAfter = controller.get('magicCrud.routeAfter'))){
        controller.transitionTo(routeAfter);
      }
      else{
        controller.transitionTo(routeBase);
      }
      flashMessages.success(saveMessage);
    });
  },

  // Fail saving record promisse callback
  saveRecordFail(){
    let controller = this.get('controller');
    let flashMessages = Ember.get(this, 'flashMessages');
    let definitionObject = this.get('definitionObject');

    let errors = controller.get('errors.model');
    for(let item in errors){
      if(errors.hasOwnProperty(item)){
        let definitions = controller.get(definitionObject);
        for(let def in definitions){
          if(definitions[def] && 'model.'+item === definitions[def].attribute && errors.get(item).length){
            flashMessages.danger(definitions[def].label + ' ' + errors.get(item));
          }
        }
      }
    }
  },

  // Route Actions
  actions: {
    //Delete record from store
    deleteRecord(item){
      const{
        routeBase,
        deleteMessageSuccess,
        deleteMessageFailed
      } = getProperties(this, 'routeBase', 'deleteMessageSuccess', 'deleteMessageFailed');

      this.transitionTo(routeBase);
      let flashMessages = Ember.get(this, 'flashMessages');

      item.deleteRecord();
      item.save().then(() => {
        flashMessages.success(deleteMessageSuccess);
      },() => {
        flashMessages.danger(deleteMessageFailed);
      });
    },

    // Transition to edit route
    editRecord(item){
      const{
        routeBase,
        editRoute
      } = getProperties(this, 'routeBase', 'editRoute');

      this.transitionTo(routeBase + '.' + editRoute, item);
    },

    // Transition to add route
    addRecord(){
      const{
        routeBase,
        addRoute
      } = getProperties(this, 'routeBase', 'addRoute');

      this.transitionTo(routeBase + '.' + addRoute);
    },

    // Save record
    saveRecord(){
      const{
        saveMessage,
        controller
      } = getProperties(this, 'saveMessage', 'controller');

      this.set('canRollbackModel', false);

      controller.set('submitted', true);
      controller.validate().then(() => {
        this.saveRecordSuccess();
      }, () => {
        this.saveRecordFail();
      });
    },

    // Rollback model
    willTransition(transition) {
      const{
        definitionObject,
        routeBase,
        routeMethod,
        controller
      } = getProperties(this, 'definitionObject', 'routeBase', 'routeMethod', 'controller');

      if(this.isAnActionRoute() && this.get('canRollbackModel')){
        controller.get('model').rollback();
        if(controller.get('model').rollbackAttributes){
          controller.get('model').rollbackAttributes();
        }
      }

      this.set('canRollbackModel', true);
    }
  }
});
<|endoftext|>


<commit_before>import Ember from 'ember';
import DS from 'ember-data';
import LFAdapter from 'ember-localforage-adapter/adapters/localforage';
import LFSerializer from 'ember-localforage-adapter/serializers/localforage';
import generateUniqueId from '../utils/generate-unique-id';

/**
 * @module ember-flexberry-offline
 */

/**
 * Store that used in offline mode.
 *
 *
 * @class LocalStore
 * @extends DS.Store
 */
export default DS.Store.extend({
  init: function() {
    let owner = Ember.getOwner(this);
    let localStore = this;
    var serializer = LFSerializer.extend(DS.EmbeddedRecordsMixin, {
      /**
       `serializeBelongsTo` can be used to customize how `DS.belongsTo` properties are serialized.
       If there is set option `odata-id` at serializer and `DS.belongsTo` property is not null,
       then property will be serialized like `ids`.

       @method serializeBelongsTo
       @param {DS.Snapshot} snapshot
       @param {Object} json
       @param {Object} relationship
      */
      serializeBelongsTo(snapshot, json, relationship) {
        var option = this.attrsOption(relationship.key);
        if (!option || option.serialize !== 'odata-id') {
          this._super(snapshot, json, relationship);
          return;
        }

        var key = relationship.key;
        var belongsToId = snapshot.belongsTo(key, { id: true });
        var payloadKey = this.keyForRelationship(key, relationship.kind, 'serialize');
        if (Ember.isNone(belongsToId)) {
          json[payloadKey] = null;
        } else {
          json[payloadKey] = belongsToId;
        }
      }
    }).create(owner.ownerInjection(), {
      store: localStore
    });

    var adapter = LFAdapter
      .extend({
        init() {
          this._super(...arguments);
          window.localforage.setDriver(window.localforage.INDEXEDDB);
        },
        generateIdForRecord: generateUniqueId,
        getPaginationQuery: function(page, perPage) {
          let query = {};
          return query;
        },

        getSortingQuery: function(sortingInfo, serializer) {
          let query = {};

          return query;
        },

        getLimitFunctionQuery: function(limitFunction, projectionName) {
          let query = {};

          if (projectionName && typeof (projectionName) === 'string' && projectionName.length > 0) {
            Ember.merge(query, { projection: projectionName });
          }

          return query;
        },
      })
      .create(owner.ownerInjection(), {
        caching: 'none',
        namespace: 'ember-flexberry-offline:store',
        serializer: serializer,
        clear: clearLFAdapter
      });

    this.set('adapter', adapter);

    this._super(...arguments);
  },

  /**
   * Serializer is fetched via this method or adapter.serializer
   *
   * @method serializerFor
   * @public
   */
  serializerFor: function() {
    return this.get('adapter.serializer');
  },

  /**
   * Adapter is fetched via this method or adapter property
   *
   * @method adapterFor
   * @public
   */
  adapterFor: function() {
    return this.get('adapter');
  },

  /**
   * Finds the records for the given model type.
   *
   * See {{#crossLink "DS.Store/findAll:method"}}{{/crossLink}} for details.
   *
   * @method findAll
   * @public
   *
   * @param {String} modelName The name of the model type.
   * @param {Object} [options] Options.
   * @param {String} options.projection Projection name.
   * @return {DS.AdapterPopulatedRecordArray} Records promise.
   */
  findAll: function(modelName, options) {
    if (options && options.projection) {
      return this.query(modelName, {
        projection: options.projection
      });
    }

    return this._super(...arguments);
  },

  /**
   * Returns a record for a given type and id combination.
   *
   * See {{#crossLink "DS.Store/findRecord:method"}}{{/crossLink}} for details.
   *
   * @method findRecord
   * @public
   *
   * @param {String} modelName The name of the model type.
   * @param {String|Integer} id Record ID.
   * @param {Object} [options] Options.
   * @param {String} options.projection Projection name.
   * @return {Promise} Record promise.
   */
  findRecord: function(modelName, id, options) {
    if (options && options.projection) {
      // TODO: case of options.reload === false.
      return this.queryRecord(modelName, {
        id: id,
        projection: options.projection
      });
    }

    return this._super(...arguments);
  },

  /**
   * This method delegates a query to the adapter.
   *
   * See {{#crossLink "DS.Store/query:method"}}{{/crossLink}} for details.
   *
   * @method query
   * @public
   *
   * @param {String} modelName The name of the model type.
   * @param {Object} query An opaque query to be used by the adapter.
   * @param {String} [query.projection] Projection name.
   * @return {Promise} A promise, which is resolved with a
   *                   {{#crossLink "DS.RecordArray"}}RecordArray{{/crossLink}}
   *                   once the server returns.
   */
  query: function(modelName, query) {
    var proj = this._extractProjectionFromQuery(modelName, query);
    var _this = this;
    return this._super(modelName, query).then(function(recordArray) {
      let promises = Ember.A();
      for (let i = 0; i < recordArray.get('length'); i++) {
        let record = recordArray.objectAt(i);
        promises.pushObject(_this._completeLoadRecord(record, proj));
      }

      return Ember.RSVP.all(promises).then(() => {
        return recordArray;
      });
    }).then(function(completeRecordArray) {
      return completeRecordArray;
    });
  },

  /**
   * This method delegates a query to the adapter.
   *
   * See {{#crossLink "DS.Store/queryRecord:method"}}{{/crossLink}} for details.
   *
   * @method queryRecord
   * @public
   *
   * @param {String} modelName The name of the model type.
   * @param {Object} query An opaque query to be used by the adapter.
   * @param {String} [query.projection] Projection name.
   * @return {Promise} A promise, which is resolved with a
   *                   {{#crossLink "DS.RecordObject"}}RecordObject{{/crossLink}}
   *                   once the server returns.
   */
  queryRecord: function(modelName, query) {
    var proj = this._extractProjectionFromQuery(modelName, query);
    var _this = this;
    return this._super(modelName, query).then(function(record) {
      return _this._completeLoadRecord(record, proj);
    }).then(function(completeRecord) {
      return completeRecord;
    });
  },

  /**
   * Retrieves projection from query and returns it.
   * Retrieved projection removes from the query.
   *
   * @method _extractProjectionFromQuery
   * @private
   *
   * @param {String} modelName The name of the model type.
   * @param {Object} [query] Query parameters.
   * @param {String} query.projection Projection name.
   * @return {Object} Extracted projection from query or null
   *                  if projection is not set in query.
   */
  _extractProjectionFromQuery: function(modelName, query) {
    if (query && query.projection) {
      let proj = query.projection;
      if (typeof query.projection === 'string') {
        let projName = query.projection;
        let typeClass = this.modelFor(modelName);
        proj = typeClass.projections.get(projName);
      }

      delete query.projection;
      return proj;
    }

    return null;
  },

  /**
   * Completes loading record for given projection.
   *
   * @method _completeLoadingRecord
   * @private
   *
   * @param {Object} record Main record loaded from local store.
   * @param {Object} projection Projection for complete loading of record.
   * @return {Object} Completely loaded record with all properties
   *                  include relationships corresponds to given projection
   */
  _completeLoadRecord: function(record, projection) {
    function loadRelatedRecord(mainRecord, id, proj) {
      let relatedRecord = this.peekRecord(proj.modelName, id);
      if (Ember.isNone(relatedRecord)) {
        let options = { projection: proj };
        return this.findRecord(proj.modelName, id, options);
      }
      else {
        return this._completeLoadRecord(relatedRecord, proj);
      }
    }

    let promises = Ember.A();
    let attributes = projection.attributes;
    let snapshot = record._createSnapshot();
    let localStore = this;
    for (var attrName in attributes) {
      if (attributes.hasOwnProperty(attrName)) {
        let attr = attributes[attrName];
        switch (attr.kind) {
          case 'attr':
            break;
          case 'belongsTo':
            let id = snapshot.belongsTo(attrName, { id: true });
            if (!Ember.isNone(id)) {
              promises.pushObject(loadRelatedRecord.call(this, record, id, attr).then((relatedRecord) => {
                delete record[attrName];
                record[attrName] = relatedRecord;
              }));
            }

            break;
          case 'hasMany':
            let ids = Ember.copy(snapshot.hasMany(attrName, { ids: true }));
            let initizlizedRelationships = record._internalModel._relationships.initializedRelationships;
            let manyRelationship = initizlizedRelationships ? initizlizedRelationships[attrName] : null;
            let relationshipMeta = Ember.get(record.constructor, 'relationshipsByName').get(attrName);
            let hasManyArray = DS.ManyArray.create({
              canonicalState: [],
              store: localStore,
              relationship: manyRelationship,
              type: relationshipMeta.type,
              record: record._internalModel,
			});
            hasManyArray.isPolymorphic = relationshipMeta.options.polymorphic;
            delete record[attrName];
            record[attrName] = hasManyArray;
            for (let i = 0; i < ids.length; i++) {
              let id = ids[i];
              promises.pushObject(loadRelatedRecord.call(this, record, id, attr).then((relatedRecord) => {
                hasManyArray.pushObject(relatedRecord);
              }));
            }

            break;
          default:
            throw new Error(`Unknown kind of projection attribute: ${attr.kind}`);
        }
      }
    }

    return Ember.RSVP.all(promises).then(() => {
      return record;
    });
  }
});

function clearLFAdapter() {
  // clear cache
  var cache = this.get('cache');
  if(cache) {
    cache.clear();
  }

  // clear data in localforage
  return window.localforage.setItem(this._adapterNamespace(), []);
}
<commit_msg>Fix pushing related records for hasMany relationship<commit_after>import Ember from 'ember';
import DS from 'ember-data';
import LFAdapter from 'ember-localforage-adapter/adapters/localforage';
import LFSerializer from 'ember-localforage-adapter/serializers/localforage';
import generateUniqueId from '../utils/generate-unique-id';

/**
 * @module ember-flexberry-offline
 */

/**
 * Store that used in offline mode.
 *
 *
 * @class LocalStore
 * @extends DS.Store
 */
export default DS.Store.extend({
  init: function() {
    let owner = Ember.getOwner(this);
    let localStore = this;
    var serializer = LFSerializer.extend(DS.EmbeddedRecordsMixin, {
      /**
       `serializeBelongsTo` can be used to customize how `DS.belongsTo` properties are serialized.
       If there is set option `odata-id` at serializer and `DS.belongsTo` property is not null,
       then property will be serialized like `ids`.

       @method serializeBelongsTo
       @param {DS.Snapshot} snapshot
       @param {Object} json
       @param {Object} relationship
      */
      serializeBelongsTo(snapshot, json, relationship) {
        var option = this.attrsOption(relationship.key);
        if (!option || option.serialize !== 'odata-id') {
          this._super(snapshot, json, relationship);
          return;
        }

        var key = relationship.key;
        var belongsToId = snapshot.belongsTo(key, { id: true });
        var payloadKey = this.keyForRelationship(key, relationship.kind, 'serialize');
        if (Ember.isNone(belongsToId)) {
          json[payloadKey] = null;
        } else {
          json[payloadKey] = belongsToId;
        }
      }
    }).create(owner.ownerInjection(), {
      store: localStore
    });

    var adapter = LFAdapter
      .extend({
        init() {
          this._super(...arguments);
          window.localforage.setDriver(window.localforage.INDEXEDDB);
        },
        generateIdForRecord: generateUniqueId,
        getPaginationQuery: function(page, perPage) {
          let query = {};
          return query;
        },

        getSortingQuery: function(sortingInfo, serializer) {
          let query = {};

          return query;
        },

        getLimitFunctionQuery: function(limitFunction, projectionName) {
          let query = {};

          if (projectionName && typeof (projectionName) === 'string' && projectionName.length > 0) {
            Ember.merge(query, { projection: projectionName });
          }

          return query;
        },
      })
      .create(owner.ownerInjection(), {
        caching: 'none',
        namespace: 'ember-flexberry-offline:store',
        serializer: serializer,
        clear: clearLFAdapter
      });

    this.set('adapter', adapter);

    this._super(...arguments);
  },

  /**
   * Serializer is fetched via this method or adapter.serializer
   *
   * @method serializerFor
   * @public
   */
  serializerFor: function() {
    return this.get('adapter.serializer');
  },

  /**
   * Adapter is fetched via this method or adapter property
   *
   * @method adapterFor
   * @public
   */
  adapterFor: function() {
    return this.get('adapter');
  },

  /**
   * Finds the records for the given model type.
   *
   * See {{#crossLink "DS.Store/findAll:method"}}{{/crossLink}} for details.
   *
   * @method findAll
   * @public
   *
   * @param {String} modelName The name of the model type.
   * @param {Object} [options] Options.
   * @param {String} options.projection Projection name.
   * @return {DS.AdapterPopulatedRecordArray} Records promise.
   */
  findAll: function(modelName, options) {
    if (options && options.projection) {
      return this.query(modelName, {
        projection: options.projection
      });
    }

    return this._super(...arguments);
  },

  /**
   * Returns a record for a given type and id combination.
   *
   * See {{#crossLink "DS.Store/findRecord:method"}}{{/crossLink}} for details.
   *
   * @method findRecord
   * @public
   *
   * @param {String} modelName The name of the model type.
   * @param {String|Integer} id Record ID.
   * @param {Object} [options] Options.
   * @param {String} options.projection Projection name.
   * @return {Promise} Record promise.
   */
  findRecord: function(modelName, id, options) {
    if (options && options.projection) {
      // TODO: case of options.reload === false.
      return this.queryRecord(modelName, {
        id: id,
        projection: options.projection
      });
    }

    return this._super(...arguments);
  },

  /**
   * This method delegates a query to the adapter.
   *
   * See {{#crossLink "DS.Store/query:method"}}{{/crossLink}} for details.
   *
   * @method query
   * @public
   *
   * @param {String} modelName The name of the model type.
   * @param {Object} query An opaque query to be used by the adapter.
   * @param {String} [query.projection] Projection name.
   * @return {Promise} A promise, which is resolved with a
   *                   {{#crossLink "DS.RecordArray"}}RecordArray{{/crossLink}}
   *                   once the server returns.
   */
  query: function(modelName, query) {
    var proj = this._extractProjectionFromQuery(modelName, query);
    var _this = this;
    return this._super(modelName, query).then(function(recordArray) {
      let promises = Ember.A();
      for (let i = 0; i < recordArray.get('length'); i++) {
        let record = recordArray.objectAt(i);
        promises.pushObject(_this._completeLoadRecord(record, proj));
      }

      return Ember.RSVP.all(promises).then(() => {
        return recordArray;
      });
    }).then(function(completeRecordArray) {
      return completeRecordArray;
    });
  },

  /**
   * This method delegates a query to the adapter.
   *
   * See {{#crossLink "DS.Store/queryRecord:method"}}{{/crossLink}} for details.
   *
   * @method queryRecord
   * @public
   *
   * @param {String} modelName The name of the model type.
   * @param {Object} query An opaque query to be used by the adapter.
   * @param {String} [query.projection] Projection name.
   * @return {Promise} A promise, which is resolved with a
   *                   {{#crossLink "DS.RecordObject"}}RecordObject{{/crossLink}}
   *                   once the server returns.
   */
  queryRecord: function(modelName, query) {
    var proj = this._extractProjectionFromQuery(modelName, query);
    var _this = this;
    return this._super(modelName, query).then(function(record) {
      return _this._completeLoadRecord(record, proj);
    }).then(function(completeRecord) {
      return completeRecord;
    });
  },

  /**
   * Retrieves projection from query and returns it.
   * Retrieved projection removes from the query.
   *
   * @method _extractProjectionFromQuery
   * @private
   *
   * @param {String} modelName The name of the model type.
   * @param {Object} [query] Query parameters.
   * @param {String} query.projection Projection name.
   * @return {Object} Extracted projection from query or null
   *                  if projection is not set in query.
   */
  _extractProjectionFromQuery: function(modelName, query) {
    if (query && query.projection) {
      let proj = query.projection;
      if (typeof query.projection === 'string') {
        let projName = query.projection;
        let typeClass = this.modelFor(modelName);
        proj = typeClass.projections.get(projName);
      }

      delete query.projection;
      return proj;
    }

    return null;
  },

  /**
   * Completes loading record for given projection.
   *
   * @method _completeLoadingRecord
   * @private
   *
   * @param {Object} record Main record loaded from local store.
   * @param {Object} projection Projection for complete loading of record.
   * @return {Object} Completely loaded record with all properties
   *                  include relationships corresponds to given projection
   */
  _completeLoadRecord: function(record, projection) {
    function loadRelatedRecord(mainRecord, id, proj) {
      let relatedRecord = this.peekRecord(proj.modelName, id);
      if (Ember.isNone(relatedRecord)) {
        let options = { projection: proj };
        return this.findRecord(proj.modelName, id, options);
      }
      else {
        return this._completeLoadRecord(relatedRecord, proj);
      }
    }

    let promises = Ember.A();
    let attributes = projection.attributes;
    let snapshot = record._createSnapshot();
    let localStore = this;
    for (var attrName in attributes) {
      if (attributes.hasOwnProperty(attrName)) {
        let attr = attributes[attrName];
        switch (attr.kind) {
          case 'attr':
            break;
          case 'belongsTo':
            let id = snapshot.belongsTo(attrName, { id: true });
            if (!Ember.isNone(id)) {
              promises.pushObject(loadRelatedRecord.call(this, record, id, attr).then((relatedRecord) => {
                delete record[attrName];
                record[attrName] = relatedRecord;
              }));
            }

            break;
          case 'hasMany':
            let ids = Ember.copy(snapshot.hasMany(attrName, { ids: true }));
            let initizlizedRelationships = record._internalModel._relationships.initializedRelationships;
            let manyRelationship = initizlizedRelationships ? initizlizedRelationships[attrName] : null;
            let relationshipMeta = Ember.get(record.constructor, 'relationshipsByName').get(attrName);
            let hasManyArray = DS.ManyArray.create({
              canonicalState: [],
              store: localStore,
              relationship: manyRelationship,
              type: relationshipMeta.type,
              record: record._internalModel,
			});
            hasManyArray.isPolymorphic = relationshipMeta.options.polymorphic;
            delete record[attrName];
            record[attrName] = hasManyArray;
            for (var i = 0; i < ids.length; i++) {
              let id = ids[i];
              promises.pushObject(loadRelatedRecord.call(this, record, id, attr).then((relatedRecord) => {
                record[attrName].pushObject(relatedRecord);
              }));
            }

            break;
          default:
            throw new Error(`Unknown kind of projection attribute: ${attr.kind}`);
        }
      }
    }

    return Ember.RSVP.all(promises).then(() => {
      return record;
    });
  }
});

function clearLFAdapter() {
  // clear cache
  var cache = this.get('cache');
  if(cache) {
    cache.clear();
  }

  // clear data in localforage
  return window.localforage.setItem(this._adapterNamespace(), []);
}
<|endoftext|>


<commit_before>var Tabula;
window.Tabula = Tabula || {};

Tabula.UI_VERSION = "0.9.9-2015-03-28" // when we make releases, we should remember to up this.
// I've decided to decouple the UI version from the "API" version in preparation for actually
// turning htem into different projects.

var TabulaRouter = Backbone.Router.extend({
  routes: {
    "":                            "upload",
    "/":                           "upload",
    "pdf/:file_id":                "view",
    "pdf/:file_id/extract":        "view", // you have to make selections first, so going directly to /extract doesn't work.
    "queue/:file_id":              'status',
    "error":                       'uploadError',
    "help":                        'help',
    "about":                       'about'
  },

  help: function(){
    document.title="Help | Tabula";
    $('nav li a').removeClass('active'); $('nav #help-nav').addClass('active');
    $('#tabula-app').html( _.template( $('#help-template').html().replace(/nestedscript/g, 'script') )({
    }) );
  },

  about: function(){
    document.title="About | Tabula";
    $('nav li a').removeClass('active'); $('nav #about-nav').addClass('active');
    $('#tabula-app').html( _.template( $('#about-template').html().replace(/nestedscript/g, 'script') )({
    }) );
  },

  upload: function() {
    document.title="Import | Tabula";
    $('nav li a').removeClass('active'); $('nav #upload-nav').addClass('active');
    $.ajax({
      url: "/js/library.js",
      dataType: "script",
      async: true,
      success: function(data, status, jqxhr){
        Tabula.library = new Tabula.Library({el: $('#tabula-app')[0]}).render();
      },
      error: function(a,b,c){
        console.log(a,b,c);
      }
    });
  },

  view: function(file_id) {
    $('nav li a').removeClass('active');
    // $('body').prepend( _.template( $('#navbar-template').html().replace(/nestedscript/g, 'script') )({}) ); // navbar.
    $('body').addClass('page-selections')
    $('#tabula-app').html( _.template( $('#pdf-view-template').html().replace(/nestedscript/g, 'script') )({}) );

    $.ajax({
      url: "/js/pdf_view.js",
      dataType: "script",
      async: true,
      success: function(data, status, jqxhr){
        Tabula.pdf_view = new Tabula.PDFView({pdf_id: file_id});
        Tabula.pdf_view.getData();
      },
      error: function(a,b,c){
        console.log(a,b,c);
      }
    });
  },
});

Tabula.getVersion = function(){
  Tabula.notification = new Backbone.Model({});
  Tabula.new_version = new Backbone.Model({});
  $.getJSON("/version", function(data){
    Tabula.api_version = data["api"];
    Tabula.getNotifications();

    // if(Tabula.api_version.slice(0,3) == "rev"){
    //   $('#dev-mode-ribbon').show();
    // }

  })
}
Tabula.getNotifications = function(){
  if(localStorage.getItem("tabula-notifications") === false) return;
  $.get('https://api.github.com/repos/tabulapdf/tabula/releases',
       function(data) {
         if (data.length < 1) return;
         if (Tabula.api_version.indexOf('rev') == 0) return;
         // check if new version
         var i = data.map(function(d) { return d.name; }).indexOf(Tabula.api_version);
         // if index >= 1, current release is not the newest
         if (i == 0) return;
         var new_release = data[0];
         if(new_release){
            Tabula.new_version.set(new_release);
         }
     });
  $.ajax({
    url: 'http://tabula.jeremybmerrill.com/tabula/notifications.jsonp',
    dataType: "jsonp",
    jsonpCallback: 'notifications',
    success: function(data){
      if(data.length < 1) return;

      // find the first listed notification where today is between its `live_date` and `expires_date`
      // and within the `versions` list.
      // we might use this for, say, notifying users if a version urgently needs an update or something
      //
      var notifications = $.grep(data, function(d){
        var today = new Date();
        if ( (d.expires_date && (new Date(d.expires_date) < today)) || (d.live_date && (new Date(d.live_date) > today)) ){
          return false;
        }
        if( d.versions && d.versions.length > 0){
          return (d.versions.indexOf(Tabula.api_version) > -1);
        }else{
          return true;
        }
      });

      if(notifications.length >= 1){
        console.log(notifications.length + " matching notifications:", notifications);
        Tabula.notification.set(notifications[0]);
      }else{
        console.log("no notifications")
      }
    }});
}

$(function(){
  Tabula.getVersion();
  window.tabula_router = new TabulaRouter();
  Backbone.history.start({pushState: true});
});<commit_msg>version bump to 1.0.0 in UI; more thorough versioning code, since we'll be using github prereleases a bit<commit_after>var Tabula;
window.Tabula = Tabula || {};

Tabula.UI_VERSION = "1.0.0-2015-08-05" // when we make releases, we should remember to up this.
// Add '-pre' to the end of this for a prerelease version; this will let
// our "new version" check give you that channel.

// Note that this is separate from the "API version" (internal app version)
// which is what we check against GitHub. In the future, this will allow us
// to modularize the UI from the backend some more.

var TabulaRouter = Backbone.Router.extend({
  routes: {
    "":                            "upload",
    "/":                           "upload",
    "pdf/:file_id":                "view",
    "pdf/:file_id/extract":        "view", // you have to make selections first, so going directly to /extract doesn't work.
    "queue/:file_id":              'status',
    "error":                       'uploadError',
    "help":                        'help',
    "about":                       'about'
  },

  help: function(){
    document.title="Help | Tabula";
    $('nav li a').removeClass('active'); $('nav #help-nav').addClass('active');
    $('#tabula-app').html( _.template( $('#help-template').html().replace(/nestedscript/g, 'script') )({
    }) );
  },

  about: function(){
    document.title="About | Tabula";
    $('nav li a').removeClass('active'); $('nav #about-nav').addClass('active');
    $('#tabula-app').html( _.template( $('#about-template').html().replace(/nestedscript/g, 'script') )({
    }) );
  },

  upload: function() {
    document.title="Import | Tabula";
    $('nav li a').removeClass('active'); $('nav #upload-nav').addClass('active');
    $.ajax({
      url: "/js/library.js",
      dataType: "script",
      async: true,
      success: function(data, status, jqxhr){
        Tabula.library = new Tabula.Library({el: $('#tabula-app')[0]}).render();
      },
      error: function(a,b,c){
        console.log(a,b,c);
      }
    });
  },

  view: function(file_id) {
    $('nav li a').removeClass('active');
    // $('body').prepend( _.template( $('#navbar-template').html().replace(/nestedscript/g, 'script') )({}) ); // navbar.
    $('body').addClass('page-selections')
    $('#tabula-app').html( _.template( $('#pdf-view-template').html().replace(/nestedscript/g, 'script') )({}) );

    $.ajax({
      url: "/js/pdf_view.js",
      dataType: "script",
      async: true,
      success: function(data, status, jqxhr){
        Tabula.pdf_view = new Tabula.PDFView({pdf_id: file_id});
        Tabula.pdf_view.getData();
      },
      error: function(a,b,c){
        console.log(a,b,c);
      }
    });
  },
});

Tabula.getVersion = function(){
  Tabula.notification = new Backbone.Model({});
  Tabula.new_version = new Backbone.Model({});
  $.getJSON("/version", function(data){
    Tabula.api_version = data["api"];
    Tabula.getNotifications();

    // if(Tabula.api_version.slice(0,3) == "rev"){
    //   $('#dev-mode-ribbon').show();
    // }

  })
}
Tabula.getNotifications = function(){
  if(localStorage.getItem("tabula-notifications") === false) return;
  $.get('https://api.github.com/repos/tabulapdf/tabula/releases',
      function(data) {
        if (data.length < 1) return;
        if (Tabula.api_version.indexOf('rev') == 0) return;

        var prerelease = (Tabula.UI_VERSION.indexOf("-pre") !== -1);
        if (prerelease) {console.log("Is prerelease");}

        // check if new version
        var non_prerelease_i = 0;
        for (var i=0; i<data.length; i++) {
          var d = data[i];
          if (!!d.draft) { continue; } // ignore drafts
          if (!prerelease && !!d.prerelease) { continue; } // ignore prereleases unless we're on a prerelease
          console.log("checking " + d.name + " vs " + Tabula.api_version);
          if ((non_prerelease_i === 0) && (d.name == Tabula.api_version)){
            // if index == 0, current release is the newest, so break out of this fn
            console.log(" -> IS LATEST");
            return;
          } else {
            // keep iterating, maybe we'll find this version later in list
            non_prerelease_i += 1;
          }
        }

        // We're not the latest release, grab data from GitHub & tell user
        var new_release = data[0];
        if(new_release){
          Tabula.new_version.set(new_release);
        }
      }
  );
  $.ajax({
    url: 'http://tabula.jeremybmerrill.com/tabula/notifications.jsonp',
    dataType: "jsonp",
    jsonpCallback: 'notifications',
    success: function(data){
      if(data.length < 1) return;

      // find the first listed notification where today is between its `live_date` and `expires_date`
      // and within the `versions` list.
      // we might use this for, say, notifying users if a version urgently needs an update or something
      //
      var notifications = $.grep(data, function(d){
        var today = new Date();
        if ( (d.expires_date && (new Date(d.expires_date) < today)) || (d.live_date && (new Date(d.live_date) > today)) ){
          return false;
        }
        if( d.versions && d.versions.length > 0){
          return (d.versions.indexOf(Tabula.api_version) > -1);
        }else{
          return true;
        }
      });

      if(notifications.length >= 1){
        console.log(notifications.length + " matching notifications:", notifications);
        Tabula.notification.set(notifications[0]);
      }else{
        console.log("no notifications")
      }
    }});
}

$(function(){
  Tabula.getVersion();
  window.tabula_router = new TabulaRouter();
  Backbone.history.start({pushState: true});
});
<|endoftext|>