<commit_before>import moment from 'moment';
import {Resource} from 'ember-could-get-used-to-this';
import {TrackedArray} from 'tracked-built-ins';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency-decorators';
import {tracked} from '@glimmer/tracking';

export default class MembersEventsFetcher extends Resource {
    @service ajax;
    @service ghostPaths;

    @tracked data = new TrackedArray([]);
    @tracked isLoading = false;
    @tracked isError = false;
    @tracked errorMessage = null;
    @tracked hasReachedEnd = false;

    cursor = null;

    get value() {
        return {
            isLoading: this.isLoading,
            isError: this.isError,
            errorMessage: this.errorMessage,
            data: this.data,
            loadNextPage: this.loadNextPage,
            hasReachedEnd: this.hasReachedEnd
        };
    }

    async setup() {
        this.cursor = moment.utc().format('YYYY-MM-DD HH:mm:ss');
        let filter = `created_at:<='${this.cursor}'`;

        if (this.args.named.filter) {
            filter += `+${this.args.named.filter}`;
        }

        return this.loadEventsTask.perform({filter});
    }

    @action
    loadNextPage() {
        // NOTE: assumes data is always ordered by created_at desc
        const lastEvent = this.data[this.data.length - 1];

        if (!lastEvent?.data?.created_at) {
            this.hasReachedEnd = true;
            return;
        }

        const cursor = moment.utc(lastEvent.data.created_at).format('YYYY-MM-DD HH:mm:ss');

        if (cursor === this.cursor) {
            this.hasReachedEnd = true;
            return;
        }

        this.cursor = cursor;
        let filter = `created_at:<='${this.cursor}'`;

        if (this.args.named.filter) {
            filter += `+${this.args.named.filter}`;
        }

        this.loadEventsTask.perform({filter});
    }

    @task
    *loadEventsTask(queryParams) {
        try {
            this.isLoading = true;

            const url = this.ghostPaths.url.api('members/events');
            const data = Object.assign({}, queryParams, {limit: this.args.named.pageSize});
            const {events} = yield this.ajax.request(url, {data});

            if (events.length < data.limit) {
                this.hasReachedEnd = true;
            }

            this.data.push(...events);
        } catch (e) {
            this.isError = true;

            const errorMessage = e.payload?.errors?.[0]?.message;
            if (errorMessage) {
                this.errorMessage = errorMessage;
            }

            // TODO: log to Sentry
            console.error(e); // eslint-disable-line
        } finally {
            this.isLoading = false;
        }
    }
}
<commit_msg>Switched from `<=` to `<` in members event pagination<commit_after>import moment from 'moment';
import {Resource} from 'ember-could-get-used-to-this';
import {TrackedArray} from 'tracked-built-ins';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency-decorators';
import {tracked} from '@glimmer/tracking';

export default class MembersEventsFetcher extends Resource {
    @service ajax;
    @service ghostPaths;

    @tracked data = new TrackedArray([]);
    @tracked isLoading = false;
    @tracked isError = false;
    @tracked errorMessage = null;
    @tracked hasReachedEnd = false;

    cursor = null;

    get value() {
        return {
            isLoading: this.isLoading,
            isError: this.isError,
            errorMessage: this.errorMessage,
            data: this.data,
            loadNextPage: this.loadNextPage,
            hasReachedEnd: this.hasReachedEnd
        };
    }

    async setup() {
        this.cursor = moment.utc().format('YYYY-MM-DD HH:mm:ss');
        let filter = `created_at:<'${this.cursor}'`;

        if (this.args.named.filter) {
            filter += `+${this.args.named.filter}`;
        }

        return this.loadEventsTask.perform({filter});
    }

    @action
    loadNextPage() {
        // NOTE: assumes data is always ordered by created_at desc
        const lastEvent = this.data[this.data.length - 1];

        if (!lastEvent?.data?.created_at) {
            this.hasReachedEnd = true;
            return;
        }

        const cursor = moment.utc(lastEvent.data.created_at).format('YYYY-MM-DD HH:mm:ss');

        if (cursor === this.cursor) {
            this.hasReachedEnd = true;
            return;
        }

        this.cursor = cursor;
        let filter = `created_at:<'${this.cursor}'`;

        if (this.args.named.filter) {
            filter += `+${this.args.named.filter}`;
        }

        this.loadEventsTask.perform({filter});
    }

    @task
    *loadEventsTask(queryParams) {
        try {
            this.isLoading = true;

            const url = this.ghostPaths.url.api('members/events');
            const data = Object.assign({}, queryParams, {limit: this.args.named.pageSize});
            const {events} = yield this.ajax.request(url, {data});

            if (events.length < data.limit) {
                this.hasReachedEnd = true;
            }

            this.data.push(...events);
        } catch (e) {
            this.isError = true;

            const errorMessage = e.payload?.errors?.[0]?.message;
            if (errorMessage) {
                this.errorMessage = errorMessage;
            }

            // TODO: log to Sentry
            console.error(e); // eslint-disable-line
        } finally {
            this.isLoading = false;
        }
    }
}
<|endoftext|>


<commit_before>module.exports = {
    "format": {
        "decimal": ",",
        "thousands": ".",
        "grouping": [3],
        "currency": ["€", ""],
        "dateTime": "%A, %e de %B de %Y. %X",
        "date": "%d/%m/%Y",
        "time": "%H:%M:%S",
        "periods": ["AM", "PM"],
        "days": ["Domingo", "Segunda", "Terça", "Quarta", "Quinta", "Sexta", "Sábado"],
        "shortDays": ["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"],
        "months": ["Janeiro", "Fevereiro", "Março", "Abril", "Maio", "Junho", "Julho", "Agosto", "Setembro", "Outubro", "Novembro", "Dezembro"],
        "shortMonths": ["Jan", "Fev", "Mar", "Abr", "Mai", "Jun", "Jul", "Ago", "Set", "Out", "Nov", "Dez"]
    },
    "dev": {
        "accepted": "{0} não é um valor válido para {1}, por favor escolha uma das seguintes opções: {2}.",
        "deprecated": "o método {0} foi removido, por favor atualize o seu código para usar {1}.",
        "noChange": "{0} não foi atualizado porque não houve modificações.",
        "noContainer": "Não foi possível encontrar um elemento na página correspondente a {0}.",
        "of": "de",
        "oldStyle": "as propriedades de {0} já foram incluídas em .{1}().",
        "sameEdge": "bordas não podem vincular a si mesmos. removendo automaticamente borda de auto-referência {0}.",
        "set": "{0} foi definido.",
        "setLong": "{0} foi alterado para {1}.",
        "setContainer": "por favor indique um elemento div através do método .container()"
    },
    "error": {
        "accepted": "{0} não é uma {1} válida para a visualização {2}, por favor escolha uma das seguintes: {3}.",
        "connections": "não existem ligações disponíveis para {0}.",
        "data": "não existem dados disponíveis",
        "dataYear": "não existem dados disponíveis para {0}.",
        "lib": "a visualização {0} necessita que a biblioteca {1} seja carregada.",
        "libs": "a visualização {0} necessita que as seguintes bibliotecas sejam carregadas: {1}.",
        "method": "A visualização {0} exige a definição do método {1}.",
        "methods": "A visualização {0} exige a definição dos seguintes métodos {1}."
    },
    "lowercase": [
        "um",
        "uma",
        "e",
        "como",
        "em",
        "no",
        "na",
        "mas",
        "por",
        "para",
        "pelo",
        "pela",
        "de",
        "do",
        "da",
        "se",
        "perto",
        "nem",
        "ou",
        "que",
        "o",
        "a",
        "com",
        "v"
    ],
    "method": {
        "active": "segmentos activos",
        "color": "cor",
        "depth": "profundidade",
        "dev": "verboso",
        "focus": "foco",
        "icon": "ícone",
        "id": "identificador",
        "height": "altura",
        "labels": "etiquetas",
        "legend": "legenda",
        "margin": "margem",
        "messages": "estado",
        "order": "ordenar",
        "search": "pesquisar",
        "shape": "forma",
        "size": "tamanho",
        "style": "estilo",
        "temp": "segmentos temporários",
        "text": "texto",
        "time": "tempo",
        "timeline": "linha temporal",
        "total": "segmentos no total",
        "type": "tipo",
        "width": "largura",
        "x": "eixo x",
        "y": "eixo y",
        "zoom": "zoom",
        "mode": "#ERROR!",
        "mute": "ocultar",
        "solo": "isolar"
    },
    "time": [
        "data",
        "dia",
        "mês",
        "hora",
        "ano"
    ],
    "visualization": {
        "bubbles": "Bolhas",
        "chart": "Diagrama",
        "geo_map": "Mapa",
        "line": "Gráfico de Linha",
        "network": "Grafo",
        "rings": "Anéis",
        "scatter": "Gráfico de Dispersão",
        "stacked": "Gráfico de Área",
        "tree_map": "Tree Map",
        "bar": "Gráfico de Barras",
        "box": "Diagrama de Caixa e Bigodes",
        "paths": "Caminhos",
        "pie": "Gráfico de Setores",
        "table": "Tabela"
    },
    "ui": {
        "and": "e",
        "back": "voltar",
        "collapse": "Clique para colapsar",
        "error": "erro",
        "expand": "clique para expandir",
        "loading": "a carregar ...",
        "more": "mais {0}",
        "moreInfo": "Clique para mais informações",
        "noResults": "nenhum resultado para {0}.",
        "primary": "ligações principais",
        "share": "proporção",
        "total": "total",
        "values": "valores",
        "including": "Incluindo",
        "or": "ou"
    },
    "message": {
        "data": "a analisar os dados",
        "draw": "a desenhar a visualização",
        "initializing": "a inicializar {0}",
        "loading": "a carregar os dados",
        "tooltipReset": "a actualizar as caixas de informação",
        "ui": "a actualizar o interface"
    },
    "uppercase": [
        "CEO",
        "CEOs",
        "CFO",
        "CFOs",
        "CNC",
        "COO",
        "COOs",
        "CPU",
        "CPUs",
        "PIB",
        "HVAC",
        "ID",
        "TI",
        "I&D",
        "TV",
        "IU"
    ]
}
<commit_msg>updated portuguese translations<commit_after>module.exports = {
    "format": {
        "decimal": ",",
        "thousands": ".",
        "grouping": [3],
        "currency": ["€", ""],
        "dateTime": "%A, %e de %B de %Y. %X",
        "date": "%d/%m/%Y",
        "time": "%H:%M:%S",
        "periods": ["AM", "PM"],
        "days": ["Domingo", "Segunda", "Terça", "Quarta", "Quinta", "Sexta", "Sábado"],
        "shortDays": ["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"],
        "months": ["Janeiro", "Fevereiro", "Março", "Abril", "Maio", "Junho", "Julho", "Agosto", "Setembro", "Outubro", "Novembro", "Dezembro"],
        "shortMonths": ["Jan", "Fev", "Mar", "Abr", "Mai", "Jun", "Jul", "Ago", "Set", "Out", "Nov", "Dez"]
    },
    "dev": {
        "accepted": "{0} não é um valor válido para {1}, por favor escolha uma das seguintes opções: {2}.",
        "deprecated": "o método {0} foi removido, por favor atualize o seu código para usar {1}.",
        "noChange": "{0} não foi atualizado porque não houve modificações.",
        "noContainer": "Não foi possível encontrar um elemento na página correspondente a {0}.",
        "of": "de",
        "oldStyle": "as propriedades de {0} já foram incluídas em .{1}().",
        "sameEdge": "bordas não podem vincular a si mesmos. removendo automaticamente borda de auto-referência {0}.",
        "set": "{0} foi definido.",
        "setLong": "{0} foi alterado para {1}.",
        "setContainer": "por favor indique um elemento div através do método .container()"
    },
    "error": {
        "accepted": "{0} não é uma {1} válida para a visualização {2}, por favor escolha uma das seguintes: {3}.",
        "connections": "não existem ligações disponíveis para {0}.",
        "data": "não existem dados disponíveis",
        "dataYear": "não existem dados disponíveis para {0}.",
        "lib": "a visualização {0} necessita que a biblioteca {1} seja carregada.",
        "libs": "a visualização {0} necessita que as seguintes bibliotecas sejam carregadas: {1}.",
        "method": "A visualização {0} exige a definição do método {1}.",
        "methods": "A visualização {0} exige a definição dos seguintes métodos {1}."
    },
    "lowercase": [
        "um",
        "uma",
        "e",
        "como",
        "em",
        "no",
        "na",
        "mas",
        "por",
        "para",
        "pelo",
        "pela",
        "de",
        "do",
        "da",
        "se",
        "perto",
        "nem",
        "ou",
        "que",
        "o",
        "a",
        "com",
        "v"
    ],
    "method": {
        "active": "segmentos activos",
        "color": "cor",
        "depth": "profundidade",
        "dev": "verboso",
        "focus": "foco",
        "icon": "ícone",
        "id": "identificador",
        "height": "altura",
        "labels": "etiquetas",
        "legend": "legenda",
        "margin": "margem",
        "messages": "estado",
        "order": "ordenar",
        "search": "pesquisar",
        "shape": "forma",
        "size": "tamanho",
        "style": "estilo",
        "temp": "segmentos temporários",
        "text": "texto",
        "time": "tempo",
        "timeline": "linha temporal",
        "total": "segmentos no total",
        "type": "tipo",
        "width": "largura",
        "x": "eixo dos xx",
        "y": "eixo dos yy",
        "zoom": "zoom",
        "mode": "#ERROR!",
        "mute": "ocultar",
        "solo": "isolar"
    },
    "time": [
        "data",
        "dia",
        "mês",
        "hora",
        "ano"
    ],
    "visualization": {
        "bubbles": "Bolhas",
        "chart": "Diagrama",
        "geo_map": "Mapa",
        "line": "Gráfico de Linha",
        "network": "Grafo",
        "rings": "Anéis",
        "scatter": "Gráfico de Dispersão",
        "stacked": "Gráfico de Área",
        "tree_map": "Tree Map",
        "bar": "Gráfico de Barras",
        "box": "Diagrama de Caixa e Bigodes",
        "paths": "Caminhos",
        "pie": "Gráfico de Setores",
        "table": "Tabela"
    },
    "ui": {
        "and": "e",
        "back": "voltar",
        "collapse": "Clique para colapsar",
        "error": "erro",
        "expand": "clique para expandir",
        "loading": "a carregar ...",
        "more": "mais {0}",
        "moreInfo": "Clique para mais informações",
        "noResults": "nenhum resultado para {0}.",
        "primary": "ligações principais",
        "share": "proporção",
        "total": "total",
        "values": "valores",
        "including": "Incluindo",
        "or": "ou"
    },
    "message": {
        "data": "a analisar os dados",
        "draw": "a desenhar a visualização",
        "initializing": "a inicializar {0}",
        "loading": "a carregar os dados",
        "tooltipReset": "a actualizar as caixas de informação",
        "ui": "a actualizar o interface"
    },
    "uppercase": [
        "CEO",
        "CEOs",
        "CFO",
        "CFOs",
        "CNC",
        "COO",
        "COOs",
        "CPU",
        "CPUs",
        "PIB",
        "HVAC",
        "ID",
        "TI",
        "I&D",
        "TV",
        "IU"
    ]
}
<|endoftext|>


<commit_before>import {
  initWebviewApi as initAppplusWebviewApi
} from 'uni-platforms/app-plus/runtime/web-view'
import {
  initWebviewApi as initH5WebviewApi
} from 'uni-platforms/h5/runtime/web-view'
import {
  initWebviewApi as initAlipayWebviewApi
} from 'uni-platforms/mp-alipay/runtime/web-view'
import {
  initWebviewApi as initBaiduWebviewApi
} from 'uni-platforms/mp-baidu/runtime/web-view'
import {
  initWebviewApi as initQQWebviewApi
} from 'uni-platforms/mp-qq/runtime/web-view'
import {
  initWebviewApi as initToutiaoWebviewApi
} from 'uni-platforms/mp-toutiao/runtime/web-view'
import {
  initWebviewApi as initWeixinWebviewApi
} from 'uni-platforms/mp-weixin/runtime/web-view'

const UniAppJSBridgeReady = function () {
  window.UniAppJSBridge = true
  document.dispatchEvent(new CustomEvent('UniAppJSBridgeReady', {
    bubbles: true,
    cancelable: true
  }))
}

const initWebviewApis = [
  initAppplusWebviewApi,
  initWeixinWebviewApi,
  initQQWebviewApi,
  initAlipayWebviewApi,
  initBaiduWebviewApi,
  initToutiaoWebviewApi,
  initH5WebviewApi
]

let webViewApi
for (let i = 0; i < initWebviewApis.length; i++) {
  webViewApi = initWebviewApis[i](UniAppJSBridgeReady)
  if (webViewApi) {
    break
  }
}

if (!webViewApi) {
  webViewApi = {}
}

const api = typeof uni !== 'undefined' ? uni : {}

if (api.navigateTo) {
  api.miniProgram = webViewApi
} else {
  Object.assign(api, webViewApi, {
    miniProgram: webViewApi
  })
}

export default api
<commit_msg>feat(web-view): add uni.webView #1011<commit_after>import {
  initWebviewApi as initAppplusWebviewApi
} from 'uni-platforms/app-plus/runtime/web-view'
import {
  initWebviewApi as initH5WebviewApi
} from 'uni-platforms/h5/runtime/web-view'
import {
  initWebviewApi as initAlipayWebviewApi
} from 'uni-platforms/mp-alipay/runtime/web-view'
import {
  initWebviewApi as initBaiduWebviewApi
} from 'uni-platforms/mp-baidu/runtime/web-view'
import {
  initWebviewApi as initQQWebviewApi
} from 'uni-platforms/mp-qq/runtime/web-view'
import {
  initWebviewApi as initToutiaoWebviewApi
} from 'uni-platforms/mp-toutiao/runtime/web-view'
import {
  initWebviewApi as initWeixinWebviewApi
} from 'uni-platforms/mp-weixin/runtime/web-view'

const UniAppJSBridgeReady = function () {
  window.UniAppJSBridge = true
  document.dispatchEvent(new CustomEvent('UniAppJSBridgeReady', {
    bubbles: true,
    cancelable: true
  }))
}

const initWebviewApis = [
  initAppplusWebviewApi,
  initWeixinWebviewApi,
  initQQWebviewApi,
  initAlipayWebviewApi,
  initBaiduWebviewApi,
  initToutiaoWebviewApi,
  initH5WebviewApi
]

let webViewApi
for (let i = 0; i < initWebviewApis.length; i++) {
  webViewApi = initWebviewApis[i](UniAppJSBridgeReady)
  if (webViewApi) {
    break
  }
}

if (!webViewApi) {
  webViewApi = {}
}

const api = typeof uni !== 'undefined' ? uni : {}

if (api.navigateTo) {
  api.webView = webViewApi
} else {
  Object.assign(api, webViewApi, {
    webView: webViewApi
  })
}

export default api
<|endoftext|>


<commit_before>function WalletTradeFormController($scope, userService, walletTradeService) {
  // [ Form Validation]

  $scope.showErrors = false;

  // [ Template Initialization ]

  $scope.currencyList = userService.getCurrencies(); // [{symbol: 'BTC', addresses:[], name: 'BTC'}, {symbol: 'MSC', addresses:[], name: 'MSC'}, {symbol: 'TMSC', addresses:[], name: 'TMSC'}]

  $scope.selectedCoin = $scope.currencyList[0];
  $scope.currencyList.forEach(function(e, i) {
    if (e.symbol == "MSC")
      $scope.selectedCoin = e;
  });

  $scope.addressList = userService.getAddressesWithPrivkey($scope.selectedCoin.addresses);
  $scope.selectedAddress = $scope.addressList[0];
  $scope.$watch('selectedCoin', function() {
    $scope.addressList = userService.getAddressesWithPrivkey($scope.selectedCoin.addresses);;
    $scope.selectedAddress = $scope.addressList[0];
    $scope.setBalance();
  });

  $scope.minerFees = formatCurrencyInFundamentalUnit(0.0001, 'wtom');

  // [ Retrieve Balances ]
  $scope.currencyUnit = 'stom'; // satoshi to millibitt
  $scope.amountUnit = 'mtow';
  $scope.balanceData = [0];
  var addrListBal = [];
  $scope.addressList.forEach(function(e, i) {
    var promise = walletTradeService.getAddressData(e);
    promise.then(function(successData) {
      var successData = successData.data;
      addrListBal[i] = {
        address: e,
        balance: successData.balance
      };
      $scope.setBalance();
    }, function(errorData) {
      alert("We have encountered a problem accessing the server ... Please try again in a few minutes");
      //console.log('Error, no balance data found for ' + e + ' setting defaults...');
      var balances = [
        {
          symbol: 'MSC',
          value: '0'
        },
        {
          symbol: 'TMSC',
          value: '0'
        },
        {
          symbol: 'BTC',
          value: '0'
        }];
      addrListBal[i] = {
        address: e,
        balance: balances
      };
    });
  });

  $scope.setBalance = function() {
    var coin = $scope.selectedCoin.symbol;
    var address = $scope.selectedAddress;
    $scope.balanceData = [0,0];
    if (address || coin) {
      for (var i = 0; i < addrListBal.length; i++) {
        if (addrListBal[i].address == address) {
          for (var k = 0; k < addrListBal[i].balance.length; k++) {
            if (addrListBal[i].balance[k].symbol == coin) {
              $scope.balanceData[0] = addrListBal[i].balance[k].value;
            }
            if (addrListBal[i].balance[k].symbol == 'BTC') {
              $scope.balanceData[1] = addrListBal[i].balance[k].value;
            }
          }
        }
      }
    }
  };
};<commit_msg>Made TMSC the default trading currency<commit_after>function WalletTradeFormController($scope, userService, walletTradeService) {
  // [ Form Validation]

  $scope.showErrors = false;

  // [ Template Initialization ]

  $scope.currencyList = userService.getCurrencies(); // [{symbol: 'BTC', addresses:[], name: 'BTC'}, {symbol: 'MSC', addresses:[], name: 'MSC'}, {symbol: 'TMSC', addresses:[], name: 'TMSC'}]

  $scope.selectedCoin = $scope.currencyList[0];
  $scope.currencyList.forEach(function(e, i) {
    if (e.symbol == "TMSC")
      $scope.selectedCoin = e;
  });

  $scope.addressList = userService.getAddressesWithPrivkey($scope.selectedCoin.addresses);
  $scope.selectedAddress = $scope.addressList[0];
  $scope.$watch('selectedCoin', function() {
    $scope.addressList = userService.getAddressesWithPrivkey($scope.selectedCoin.addresses);;
    $scope.selectedAddress = $scope.addressList[0];
    $scope.setBalance();
  });

  $scope.minerFees = formatCurrencyInFundamentalUnit(0.0001, 'wtom');

  // [ Retrieve Balances ]
  $scope.currencyUnit = 'stom'; // satoshi to millibitt
  $scope.amountUnit = 'mtow';
  $scope.balanceData = [0];
  var addrListBal = [];
  $scope.addressList.forEach(function(e, i) {
    var promise = walletTradeService.getAddressData(e);
    promise.then(function(successData) {
      var successData = successData.data;
      addrListBal[i] = {
        address: e,
        balance: successData.balance
      };
      $scope.setBalance();
    }, function(errorData) {
      alert("We have encountered a problem accessing the server ... Please try again in a few minutes");
      //console.log('Error, no balance data found for ' + e + ' setting defaults...');
      var balances = [
        {
          symbol: 'MSC',
          value: '0'
        },
        {
          symbol: 'TMSC',
          value: '0'
        },
        {
          symbol: 'BTC',
          value: '0'
        }];
      addrListBal[i] = {
        address: e,
        balance: balances
      };
    });
  });

  $scope.setBalance = function() {
    var coin = $scope.selectedCoin.symbol;
    var address = $scope.selectedAddress;
    $scope.balanceData = [0,0];
    if (address || coin) {
      for (var i = 0; i < addrListBal.length; i++) {
        if (addrListBal[i].address == address) {
          for (var k = 0; k < addrListBal[i].balance.length; k++) {
            if (addrListBal[i].balance[k].symbol == coin) {
              $scope.balanceData[0] = addrListBal[i].balance[k].value;
            }
            if (addrListBal[i].balance[k].symbol == 'BTC') {
              $scope.balanceData[1] = addrListBal[i].balance[k].value;
            }
          }
        }
      }
    }
  };
};<|endoftext|>


<commit_before>$(function()
  {

    window.Sip = Backbone.Model.extend({

      initialize: function()
        {
          this.loadJobs();
        },

      hasFinished: function()
        {
          return false == this.jobs.some(function(job)
            {
              return -1 < jQuery.inArray(job.get('currentstep'), ['Requires approval', 'Executing command(s)']);
            });
        },

      set: function(attributes, options)
        {
          Backbone.Model.prototype.set.call(this, attributes, options);

          if (undefined !== this.jobs && !this.hasFinished())
          {
            this.view.update();
          }
        },

      loadJobs: function()
        {
          // Nested collection
          this.jobs = new JobCollection(this.get('jobs'));

          var self = this;
          this.jobs.each(function(job)
            {
              job.sip = self;
            });
        }

    });

    window.SipCollection = Backbone.Collection.extend({

      model: Sip,

      url: '/sips/go/',

      initialize: function()
        {

        },

      comparator: function(sip)
        {
          return 0 - sip.get('timestamp');
        }

    });

    window.SipView = Backbone.View.extend({

      className: 'sip',

      template: _.template($('#sip-template').html()),

      events: {
        'click .sip-row > .sip-detail-icon-status > a': 'toggleJobs',
        'click .sip-row > .sip-detail-actions > .btn_show_jobs': 'toggleJobs',
        'click .sip-row > .sip-detail-actions > .btn_delete_sip': 'remove',
      },

      initialize: function()
        {
          _.bindAll(this, 'render', 'update', 'add', 'updateIcon');
          this.model.view = this;
          this.model.bind('change:timestamp', this.update);
        },

      render: function()
        {
          $(this.el).html(this.template(this.model.toJSON()));

          this.$jobContainer = this.$('.sip-detail-job-container');

          return this;
        },

      update: function()
        {
          // Reload nested collection
          this.model.loadJobs(); // .refresh() shouldn't work here

          // Update timestamp
          this.$('.sip-detail-timestamp').html(
            new Date(this.model.get('timestamp') * 1000).getArchivematicaDateTime()
          );

          // Update icon
          this.updateIcon();

          if (this.$jobContainer.is(':visible'))
          {
            this.$jobContainer.empty();

            var self = this;

            this.model.jobs.each(function(job)
              {
                var view = new JobView({model: job});
                self.$jobContainer.append(view.render().el);
              });
          }
        },

      updateIcon: function()
        {
          this.$('.sip-detail-icon-status > a').html(this.model.jobs.getIcon());
        },

      toggleJobs: function(event)
        {
          if (event)
          {
            event.preventDefault();
          }

          if (this.$jobContainer.is(':visible'))
          {
            this.$jobContainer.slideUp('fast');
            $(this.el).removeClass('sip-selected');
          }
          else
          {
            this.$jobContainer.empty();

            var self = this;
            this.model.jobs.each(function(job)
              {
                var view = new JobView({model: job});
                self.$jobContainer.append(view.render().el);
              });

            this.$jobContainer.slideDown('fast');
            $(this.el).addClass('sip-selected');
          }
        },

      remove: function(event)
        {
          event.preventDefault();

          $(this.el).addClass('sip-deleting');

          var self = this;

          $('
' +
              '
Are you sure that you want to permanently delete the selected SIP?
' +
              '
Directory: ' + this.model.get('directory') + '
UUID: ' + this.model.get('uuid') + '
Status: ' + $(this.el).find('.sip-detail-icon-status > a > img').attr('title') + '
' +
            '
').dialog(
            {
              modal: true,
              resizable: false,
              title: false,
              draggable: false,
              title: 'Delete SIP',
              width: 480,
              close: function(event, ui)
                {
                  if (event.which !== undefined)
                  {
                    $(self.el).removeClass('sip-deleting');
                  }
                },
              buttons: [
                  {
                    text: 'Confirm',
                    click: function() {

                      var $dialog = $(this);

                      self.model.destroy({
                        success: function (model, response)
                          {
                            $dialog.dialog('close');

                            setTimeout(function()
                              {
                                $(self.el).hide('blind', function()
                                  {
                                    $(this).remove();
                                  });
                              }, 250);
                          },
                        error: function(model, response)
                          {
                            $dialog.dialog('close');
                            $(self.el).removeClass('sip-deleting');
                          }

                      });
                    }
                  },
                  {
                    text: 'Cancel',
                    click: function() {
                        $(this).dialog('close');
                        $(self.el).removeClass('sip-deleting');
                      }
                  }]
            });
        },

      getIngestStartTime: function()
        {
          // Use "Assign file UUIDs and checksums" micro-service to represent ingest start time
          // TODO: fastest solution would be to use the first microservice of the collection, once is ordered correctly
          var job = this.model.jobs.detect(function(job)
            {
              return job.get('microservice') == 'Assign file UUIDs and checksums';
            });

          // Fallback: use last micro-service timestamp
          if (undefined === job)
          {
            job = this.model.jobs.last();
          }

          return new Date(job.get('timestamp') * 1000).getArchivematicaDateTime();
        }
    });

    window.Job = Backbone.Model.extend({

    });

    window.JobCollection = Backbone.Collection.extend({

      model: Job,

      getIcon: function()
        {
          var path = '';
          var title = '';

          if (undefined !== this.find(function(job)
            {
              return 0 < job.get('status') || 'Requires approval' == job.get('currentstep');
            }))
          {
            path = '/media/images/bell.png';
            title = 'Requires approval';
          }
          else if (undefined !== this.find(function(job)
            {
              return 'Failed' == job.get('currentstep');
            }))
          {
            path = '/media/images/cancel.png';
            title = 'Failed';
          }
          else if (undefined !== this.find(function(job)
            {
              return 'Executing command(s)' == job.get('currentstep');
            }))
          {
            path = '/media/images/icons/arrow_refresh.png';
            title = 'Executing command(s)';
          }
          else if (undefined !== this.find(function(job)
            {
              return 'Rejected' == job.get('currentstep');
            }))
          {
            path = '/media/images/icons/control_stop_blue.png';
            title = 'Rejected';
          }
          else
          {
            path = '/media/images/accept.png';
            title = 'Completed successfully';
          }

          return '';
        }

    });

    window.JobView = Backbone.View.extend({

      className: 'job',

      events: {
        'click .btn_browse_job': 'browseJob',
        'click .btn_approve_job': 'approveJob',
        'click .btn_reject_job': 'rejectJob',
        'click .btn_show_tasks': 'showTasks',
      },

      template: _.template($('#job-template').html()),

      initialize: function()
        {
          _.bindAll(this, 'render', 'approveJob', 'rejectJob');
          this.model.bind('change', this.render);
          this.model.view = this;
        },

      render: function()
        {
          $(this.el).html(this.template(this.model.toJSON()));

          if (-1 < jQuery.inArray(this.model.get('currentstep'), ['Requires approval', 'Failed', 'Rejected']))
          {
            $(this.el).css('background-color', '#f2d8d8');
          }
          else if ('Executing command(s)' == this.model.get('currentstep'))
          {
            $(this.el).css('background-color', '#fedda7');
          }
          else
          {
            $(this.el).css('background-color', '#d8f2dc');
          }

          if (1 == this.model.get('status'))
          {
            this.$('.job-detail-actions')
              .append('Browse')
              .append('Approve')
              .append('Reject')
          }

          this.$('.job-detail-microservice > a').tooltip();

          return this;
        },

      showTasks: function(event)
        {
          event.preventDefault();

          $.ajax({
            context: this,
            type: 'GET',
            dataType: 'html',
            success: function(data)
              {
                $('

')
                  .append('
' + $(data).find('tbody').html() + '
')
                  .dialog({
                    title: this.model.sip.get('directory') + ' &raquo ' + this.model.get('microservice') + ' &raquo Tasks',
                    width: 640,
                    height: 480,
                    modal: true,
                    buttons: [
                      {
                        text: 'Close',
                        click: function() { $(this).dialog('close'); }
                      }]
                  });
              },
            url: '/tasks/' + this.model.get('uuid') + '/'
          });
        },

      browseJob: function(event)
        {
          event.preventDefault();

          alert("browsejob " + this.model.get('uuid'));
        },

      approveJob: function(event)
        {
          event.preventDefault();

          $.ajax({
            context: this,
            data: { uuid: this.model.get('uuid') },
            type: 'POST',
            success: function(data)
              {
                this.model.set({
                  'currentstep': 'Executing command(s)',
                  'status': 0
                });

                this.model.sip.view.updateIcon();
              },
            url: '/mcp/approve-job/'
          });
        },

      rejectJob: function(event)
        {
          event.preventDefault();

          $.ajax({
            context: this,
            data: { uuid: this.model.get('uuid') },
            type: 'POST',
            success: function(data)
              {
                this.model.set({
                  'currentstep': 'Rejected',
                  'status': 0
                });

                this.model.sip.view.updateIcon();
                // this.model.sip.view.toggleJobs();
              },
            url: '/mcp/reject-job/'
          });
        },

    });

    window.StatusView = Backbone.View.extend({

      id: 'status',

      template: _.template($('#status-template').html()),

      initialize: function()
        {
          this.render();
        },

      render: function()
        {
          $(this.el).html(this.template()).hide().appendTo('body');

          return this;
        },

      hide: function()
        {
          var self = this;
          setTimeout(function()
            {
              $(self.el).fadeOut('fast');
            }, 1000);
        },

      text: function(message, error)
        {
          $(this.el).show().find('span').html(message);

          if (true === error)
          {
            $(this.el).addClass('status-error');
          }
          else
          {
            $(this.el).removeClass('status-error');
          }
        }

    });

    window.AppView = Backbone.View.extend({

      el: $('#sip-container'),

      initialize: function()
        {
          _.bindAll(this, 'add', 'remove');
          Sips.bind('add', this.add);
          Sips.bind('remove', this.remove);

          window.statusWidget = new window.StatusView();

          this.poll(true);
        },

      add: function(sip)
        {
          var view = new SipView({model: sip});
          var $new = $(view.render().el).hide();

          // Get the current position in the collection
          var position = Sips.indexOf(sip);

          if (0 == position)
          {
            this.el.children('#sip-body').prepend($new);
          }
          else
          {
            var $target = this.el.find('.sip').eq(position);

            if ($target.length)
            {
              $target.before($new);
            }
            else
            {
              this.el.children('#sip-body').append($new);
            }
          }

          if (!this.firstPoll)
          {
            // Animation
            $new.addClass('sip-new').show('blind', {}, 500, function()
              {
                $(this).removeClass('sip-new', 2000);
              });
          }
          else
          {
            $new.show();
          }
        },

      remove: function(sip)
        {
          $(sip.view.el).hide('blind', function()
            {
              $(this).remove();
            });
        },

      poll: function(start)
        {
          this.firstPoll = undefined !== start;

          $.ajax({
            context: this,
            dataType: 'json',
            type: 'GET',
            url: '/sips/go/',
            beforeSend: function()
              {
                window.statusWidget.text(undefined !== start ? 'Loading...' : 'Refreshing...');
              },
            error: function()
              {
                window.statusWidget.text('Error trying to connect to database. Trying again...', true);
              },
            success: function(response)
              {
                var objects = response.objects;

                for (var i in objects)
                {
                  var sip = objects[i];
                  var item = Sips.find(function(item) { return item.get('uuid') == sip.uuid; });

                  if (undefined === item)
                  {
                    // Add new sips
                    Sips.add(sip);
                  }
                  else
                  {
                    // Update sips
                    item.set(sip);
                  }
                }

                // Delete sips
                if (Sips.length > objects.length)
                {
                  var unusedSips = Sips.reject(function(sip)
                      {
                        return -1 < $.inArray(sip.get('uuid'), _.pluck(objects, 'uuid'));
                      });

                  Sips.remove(unusedSips);
                }

                // MCP status
                if (response.mcp)
                {
                  window.statusWidget.hide();
                }
                else
                {
                  window.statusWidget.text('Error trying to connect to MCP server. Trying again...', true);
                }
              },
            complete: function()
              {
                var self = this;
                setTimeout(function()
                  {
                    self.poll();
                  }, window.pollingInterval ? window.pollingInterval * 1000: 5000);
              }
          });
        },

    });

    $.fn.tooltip = function(options)
      {
        var settings = {
          xOffset: 10,
          yOffset: 20,
          width: 280
        };

        return this.each(function()
          {
            var $this = $(this);
            var $tooltip;

            if (options)
            {
              $.extend(settings, options);
            }

            if (undefined === settings.content)
            {
              settings.content = $this.attr('title');
            }

            $this
              .attr('title', '')
              .mouseover(function(event)
                {
                  $tooltip = $('
' + (undefined !== settings.title ? '
' + settings.title + '
' : '') + '
' + settings.content + '

')
                    .hide()
                    .css({
                      top: (event.pageY - settings.xOffset) + 'px',
                      left: (event.pageX + settings.yOffset) + 'px',
                      width: settings.width + 'px'})
                    .fadeIn()
                    .appendTo('body');
                })
              .mouseout(function(event)
                {
                  $tooltip.remove();
                })
              .mousemove(function(event)
                {
                  $tooltip.css({
                    top: (event.pageY - settings.xOffset) + 'px',
                    left: (event.pageX + settings.yOffset) + 'px'});
                })
              .click(function(event)
                {
                  event.preventDefault();
                });
          });
      };

    Date.prototype.getArchivematicaDateTime = function()
      {
        pad = function (n)
          {
            return n < 10 ? '0' + n : n;
          }

        return this.getUTCFullYear() + '-' + pad(this.getUTCMonth() + 1) + '-' + pad(this.getUTCDate()) + ' ' + pad(this.getUTCHours()) + ':' + pad(this.getUTCMinutes()); // + ':' + pad(this.getUTCSeconds());
      };
  }
);<commit_msg>Keep an eye on user activity to trigger idle event and stop polling after the timeout (= settings.POLLING_INTERVAL * 10) is reached. Fixes issue 402.<commit_after>$(function()
  {

    window.Sip = Backbone.Model.extend({

      initialize: function()
        {
          this.loadJobs();
        },

      hasFinished: function()
        {
          return false == this.jobs.some(function(job)
            {
              return -1 < jQuery.inArray(job.get('currentstep'), ['Requires approval', 'Executing command(s)']);
            });
        },

      set: function(attributes, options)
        {
          Backbone.Model.prototype.set.call(this, attributes, options);

          if (undefined !== this.jobs && !this.hasFinished())
          {
            this.view.update();
          }
        },

      loadJobs: function()
        {
          // Nested collection
          this.jobs = new JobCollection(this.get('jobs'));

          var self = this;
          this.jobs.each(function(job)
            {
              job.sip = self;
            });
        }

    });

    window.SipCollection = Backbone.Collection.extend({

      model: Sip,

      url: '/sips/go/',

      initialize: function()
        {

        },

      comparator: function(sip)
        {
          return 0 - sip.get('timestamp');
        }

    });

    window.SipView = Backbone.View.extend({

      className: 'sip',

      template: _.template($('#sip-template').html()),

      events: {
        'click .sip-row > .sip-detail-icon-status > a': 'toggleJobs',
        'click .sip-row > .sip-detail-actions > .btn_show_jobs': 'toggleJobs',
        'click .sip-row > .sip-detail-actions > .btn_delete_sip': 'remove',
      },

      initialize: function()
        {
          _.bindAll(this, 'render', 'update', 'add', 'updateIcon');
          this.model.view = this;
          this.model.bind('change:timestamp', this.update);
        },

      render: function()
        {
          $(this.el).html(this.template(this.model.toJSON()));

          this.$jobContainer = this.$('.sip-detail-job-container');

          return this;
        },

      update: function()
        {
          // Reload nested collection
          this.model.loadJobs(); // .refresh() shouldn't work here

          // Update timestamp
          this.$('.sip-detail-timestamp').html(
            new Date(this.model.get('timestamp') * 1000).getArchivematicaDateTime()
          );

          // Update icon
          this.updateIcon();

          if (this.$jobContainer.is(':visible'))
          {
            this.$jobContainer.empty();

            var self = this;

            this.model.jobs.each(function(job)
              {
                var view = new JobView({model: job});
                self.$jobContainer.append(view.render().el);
              });
          }
        },

      updateIcon: function()
        {
          this.$('.sip-detail-icon-status > a').html(this.model.jobs.getIcon());
        },

      toggleJobs: function(event)
        {
          if (event)
          {
            event.preventDefault();
          }

          if (this.$jobContainer.is(':visible'))
          {
            this.$jobContainer.slideUp('fast');
            $(this.el).removeClass('sip-selected');
          }
          else
          {
            this.$jobContainer.empty();

            var self = this;
            this.model.jobs.each(function(job)
              {
                var view = new JobView({model: job});
                self.$jobContainer.append(view.render().el);
              });

            this.$jobContainer.slideDown('fast');
            $(this.el).addClass('sip-selected');
          }
        },

      remove: function(event)
        {
          event.preventDefault();

          $(this.el).addClass('sip-deleting');

          var self = this;

          $('
' +
              '
Are you sure that you want to permanently delete the selected SIP?
' +
              '
Directory: ' + this.model.get('directory') + '
UUID: ' + this.model.get('uuid') + '
Status: ' + $(this.el).find('.sip-detail-icon-status > a > img').attr('title') + '
' +
            '
').dialog(
            {
              modal: true,
              resizable: false,
              title: false,
              draggable: false,
              title: 'Delete SIP',
              width: 480,
              close: function(event, ui)
                {
                  if (event.which !== undefined)
                  {
                    $(self.el).removeClass('sip-deleting');
                  }
                },
              buttons: [
                  {
                    text: 'Confirm',
                    click: function() {

                      var $dialog = $(this);

                      self.model.destroy({
                        success: function (model, response)
                          {
                            $dialog.dialog('close');

                            setTimeout(function()
                              {
                                $(self.el).hide('blind', function()
                                  {
                                    $(this).remove();
                                  });
                              }, 250);
                          },
                        error: function(model, response)
                          {
                            $dialog.dialog('close');
                            $(self.el).removeClass('sip-deleting');
                          }

                      });
                    }
                  },
                  {
                    text: 'Cancel',
                    click: function() {
                        $(this).dialog('close');
                        $(self.el).removeClass('sip-deleting');
                      }
                  }]
            });
        },

      getIngestStartTime: function()
        {
          // Use "Assign file UUIDs and checksums" micro-service to represent ingest start time
          // TODO: fastest solution would be to use the first microservice of the collection, once is ordered correctly
          var job = this.model.jobs.detect(function(job)
            {
              return job.get('microservice') == 'Assign file UUIDs and checksums';
            });

          // Fallback: use last micro-service timestamp
          if (undefined === job)
          {
            job = this.model.jobs.last();
          }

          return new Date(job.get('timestamp') * 1000).getArchivematicaDateTime();
        }
    });

    window.Job = Backbone.Model.extend({

    });

    window.JobCollection = Backbone.Collection.extend({

      model: Job,

      getIcon: function()
        {
          var path = '';
          var title = '';

          if (undefined !== this.find(function(job)
            {
              return 0 < job.get('status') || 'Requires approval' == job.get('currentstep');
            }))
          {
            path = '/media/images/bell.png';
            title = 'Requires approval';
          }
          else if (undefined !== this.find(function(job)
            {
              return 'Failed' == job.get('currentstep');
            }))
          {
            path = '/media/images/cancel.png';
            title = 'Failed';
          }
          else if (undefined !== this.find(function(job)
            {
              return 'Executing command(s)' == job.get('currentstep');
            }))
          {
            path = '/media/images/icons/arrow_refresh.png';
            title = 'Executing command(s)';
          }
          else if (undefined !== this.find(function(job)
            {
              return 'Rejected' == job.get('currentstep');
            }))
          {
            path = '/media/images/icons/control_stop_blue.png';
            title = 'Rejected';
          }
          else
          {
            path = '/media/images/accept.png';
            title = 'Completed successfully';
          }

          return '';
        }

    });

    window.JobView = Backbone.View.extend({

      className: 'job',

      events: {
        'click .btn_browse_job': 'browseJob',
        'click .btn_approve_job': 'approveJob',
        'click .btn_reject_job': 'rejectJob',
        'click .btn_show_tasks': 'showTasks',
      },

      template: _.template($('#job-template').html()),

      initialize: function()
        {
          _.bindAll(this, 'render', 'approveJob', 'rejectJob');
          this.model.bind('change', this.render);
          this.model.view = this;
        },

      render: function()
        {
          $(this.el).html(this.template(this.model.toJSON()));

          if (-1 < jQuery.inArray(this.model.get('currentstep'), ['Requires approval', 'Failed', 'Rejected']))
          {
            $(this.el).css('background-color', '#f2d8d8');
          }
          else if ('Executing command(s)' == this.model.get('currentstep'))
          {
            $(this.el).css('background-color', '#fedda7');
          }
          else
          {
            $(this.el).css('background-color', '#d8f2dc');
          }

          if (1 == this.model.get('status'))
          {
            this.$('.job-detail-actions')
              .append('Browse')
              .append('Approve')
              .append('Reject')
          }

          this.$('.job-detail-microservice > a').tooltip();

          return this;
        },

      showTasks: function(event)
        {
          event.preventDefault();

          $.ajax({
            context: this,
            type: 'GET',
            dataType: 'html',
            success: function(data)
              {
                $('

')
                  .append('
' + $(data).find('tbody').html() + '
')
                  .dialog({
                    title: this.model.sip.get('directory') + ' &raquo ' + this.model.get('microservice') + ' &raquo Tasks',
                    width: 640,
                    height: 480,
                    modal: true,
                    buttons: [
                      {
                        text: 'Close',
                        click: function() { $(this).dialog('close'); }
                      }]
                  });
              },
            url: '/tasks/' + this.model.get('uuid') + '/'
          });
        },

      browseJob: function(event)
        {
          event.preventDefault();

          alert("browsejob " + this.model.get('uuid'));
        },

      approveJob: function(event)
        {
          event.preventDefault();

          $.ajax({
            context: this,
            data: { uuid: this.model.get('uuid') },
            type: 'POST',
            success: function(data)
              {
                this.model.set({
                  'currentstep': 'Executing command(s)',
                  'status': 0
                });

                this.model.sip.view.updateIcon();
              },
            url: '/mcp/approve-job/'
          });
        },

      rejectJob: function(event)
        {
          event.preventDefault();

          $.ajax({
            context: this,
            data: { uuid: this.model.get('uuid') },
            type: 'POST',
            success: function(data)
              {
                this.model.set({
                  'currentstep': 'Rejected',
                  'status': 0
                });

                this.model.sip.view.updateIcon();
                // this.model.sip.view.toggleJobs();
              },
            url: '/mcp/reject-job/'
          });
        },

    });

    window.StatusView = Backbone.View.extend({

      id: 'status',

      template: _.template($('#status-template').html()),

      initialize: function()
        {
          this.render();
        },

      render: function()
        {
          $(this.el).html(this.template()).hide().appendTo('body');

          return this;
        },

      hide: function()
        {
          var self = this;
          setTimeout(function()
            {
              $(self.el).fadeOut('fast');
            }, 1000);
        },

      text: function(message, error)
        {
          $(this.el).show().find('span').html(message);

          if (true === error)
          {
            $(this.el).addClass('status-error');
          }
          else
          {
            $(this.el).removeClass('status-error');
          }
        }

    });

    window.AppView = Backbone.View.extend({

      el: $('#sip-container'),

      interval: window.pollingInterval ? window.pollingInterval * 1000: 5000,

      idle: false,

      initialize: function()
        {
          _.bindAll(this, 'add', 'remove');
          Sips.bind('add', this.add);
          Sips.bind('remove', this.remove);

          window.statusWidget = new window.StatusView();

          this.manageIdle();

          this.poll(true);
        },

      manageIdle: function()
        {
          $.idleTimer(this.interval * 10);

          var self = this;
          $(document)
            .bind('idle.idleTimer', function()
              {
                self.idle = true;
              })
            .bind('active.idleTimer', function()
              {
                self.idle = false;
                self.poll();
              });
        },

      add: function(sip)
        {
          var view = new SipView({model: sip});
          var $new = $(view.render().el).hide();

          // Get the current position in the collection
          var position = Sips.indexOf(sip);

          if (0 == position)
          {
            this.el.children('#sip-body').prepend($new);
          }
          else
          {
            var $target = this.el.find('.sip').eq(position);

            if ($target.length)
            {
              $target.before($new);
            }
            else
            {
              this.el.children('#sip-body').append($new);
            }
          }

          if (!this.firstPoll)
          {
            // Animation
            $new.addClass('sip-new').show('blind', {}, 500, function()
              {
                $(this).removeClass('sip-new', 2000);
              });
          }
          else
          {
            $new.show();
          }
        },

      remove: function(sip)
        {
          $(sip.view.el).hide('blind', function()
            {
              $(this).remove();
            });
        },

      poll: function(start)
        {
          this.firstPoll = undefined !== start;

          $.ajax({
            context: this,
            dataType: 'json',
            type: 'GET',
            url: '/sips/go/',
            beforeSend: function()
              {
                window.statusWidget.text(undefined !== start ? 'Loading...' : 'Refreshing...');
              },
            error: function()
              {
                window.statusWidget.text('Error trying to connect to database. Trying again...', true);
              },
            success: function(response)
              {
                var objects = response.objects;

                for (var i in objects)
                {
                  var sip = objects[i];
                  var item = Sips.find(function(item) { return item.get('uuid') == sip.uuid; });

                  if (undefined === item)
                  {
                    // Add new sips
                    Sips.add(sip);
                  }
                  else
                  {
                    // Update sips
                    item.set(sip);
                  }
                }

                // Delete sips
                if (Sips.length > objects.length)
                {
                  var unusedSips = Sips.reject(function(sip)
                      {
                        return -1 < $.inArray(sip.get('uuid'), _.pluck(objects, 'uuid'));
                      });

                  Sips.remove(unusedSips);
                }

                // MCP status
                if (response.mcp)
                {
                  window.statusWidget.hide();
                }
                else
                {
                  window.statusWidget.text('Error trying to connect to MCP server. Trying again...', true);
                }
              },
            complete: function()
              {
                var self = this;

                if (!self.idle)
                {
                  setTimeout(function()
                    {
                      self.poll();
                    }, this.interval);
                }
              }
          });
        },

    });

    $.fn.tooltip = function(options)
      {
        var settings = {
          xOffset: 10,
          yOffset: 20,
          width: 280
        };

        return this.each(function()
          {
            var $this = $(this);
            var $tooltip;

            if (options)
            {
              $.extend(settings, options);
            }

            if (undefined === settings.content)
            {
              settings.content = $this.attr('title');
            }

            $this
              .attr('title', '')
              .mouseover(function(event)
                {
                  $tooltip = $('
' + (undefined !== settings.title ? '
' + settings.title + '
' : '') + '
' + settings.content + '

')
                    .hide()
                    .css({
                      top: (event.pageY - settings.xOffset) + 'px',
                      left: (event.pageX + settings.yOffset) + 'px',
                      width: settings.width + 'px'})
                    .fadeIn()
                    .appendTo('body');
                })
              .mouseout(function(event)
                {
                  $tooltip.remove();
                })
              .mousemove(function(event)
                {
                  $tooltip.css({
                    top: (event.pageY - settings.xOffset) + 'px',
                    left: (event.pageX + settings.yOffset) + 'px'});
                })
              .click(function(event)
                {
                  event.preventDefault();
                });
          });
      };

    Date.prototype.getArchivematicaDateTime = function()
      {
        pad = function (n)
          {
            return n < 10 ? '0' + n : n;
          }

        return this.getUTCFullYear() + '-' + pad(this.getUTCMonth() + 1) + '-' + pad(this.getUTCDate()) + ' ' + pad(this.getUTCHours()) + ':' + pad(this.getUTCMinutes()); // + ':' + pad(this.getUTCSeconds());
      };
  }
);<|endoftext|>


<commit_before>angular.module('casa').factory('LocationsService', [ function() {

  var locationsObj = {};

  locationsObj.savedLocations = [
    {
      name : "CASA",
      lat : 43.7015074,
      lng : 7.0301
    },
    {
      name : "Plateau de Calern",
      lat : 43.7608778,
      lng : 6.90125
    },
	{
      name : "Plateau de Caussols",
      lat : 43.7252074,
      lng : 6.904847
    },
	{
      name : "Plateau de la Malle",
      lat : 43.7017139,
      lng : 6.918923
    },
	{
      name : "Château de Gourdon",
      lat : 43.7201152,
      lng : 6.9784499
    },
	{
      name : "Baume Robert - Le Rouret",
      lat : 43.6785604,
      lng : 7.001038
    },
	{
      name : "La Colle du Maçon - Caussols",
      lat : 43.7174875,
      lng : 6.881176
    },
	{
      name : "Le Col du Clapier",
      lat : 43.7134014,
      lng : 6.89977
    },
	{
      name : "Le Haut Montet",
      lat : 43.7162058,
      lng : 6.930729
    },
    {
      name : "La bergerie du Montet",
      lat : 43.7085029,
      lng : 6.929946
    },
    {
      name : "Château Grimaldi",
	  lat : 43.5807427,
      lng : 7.12822
    },
    {
      name : "Donjon - Le Bar-sur-Loup",
      lat : 43.701352,
      lng : 6.989333
    },
    {
      name : "Le Serre de la Madeleine",
      lat : 43.7601562,
      lng : 7.002371
    },
    {
      name : "Château de Villeneuve et parc",
      lat : 43.6598309,
      lng : 7.1241832
    },
    {
      name : "Tour de la Madone - Villeneuve-loubet",
      lat : 43.656238,
      lng : 7.098812
    },
    {
      name : "Donjon - Saint-Paul de Vence",
      lat : 43.696968,
      lng : 7.122259
    },
    {
      name : "Château de Tourrette-sur-Loup",
      lat : 43.715830,
      lng : 7.059306
    },
    {
      name : "Château de Gourdon",
      lat : 43.7201152,
      lng : 6.9784499
    },
    {
      name : "Château de Bar-sur-Loup",
      lat : 43.6911809,
      lng : 6.9809576
    },
    {
      name : "Château de Châteauneuf",
      lat : 43.674775,
      lng : 6.975999
    },
    {
      name : "Château d'Opio",
      lat : 43.668726,
      lng : 6.981965
    },
    {
      name : "Tour médiévale - Tourrettes-sur-Loup",
      lat : 43.716161,
      lng : 7.059036
    },
    {
      name : "Maison de Lartigue - Opio",
      lat : 43.6682697,
      lng : 6.9822241
    },
    {
      name : "Château - Vallauris",
      lat : 43.57984,
      lng : 7.053051
    },
    {
      name : "Place des Arcades - Valbonne",
      lat : 43.6414339,
      lng : 7.008779
    },
    {
      name : "Porte de Vence et Tour à mâchicoulis",
      lat : 43.697988,
      lng : 7.121944
    },
    {
      name : "Fort Carré - Antibes",
      lat : 43.590305,
      lng : 7.127230
    },
    {
      name : "Le bastion St André et ses remparts - Antibes",
      lat : 43.5772412,
      lng : 7.1260527
    },
    {
      name : "Le Clos Saint Pierre - Le Rouret",
      lat : 43.6744097,
      lng : 7.0062755
    },
    {
      name : "Maison seigneuriale - Courmes",
      lat : 43.742330,
      lng : 7.009302
    },
    {
      name : "Église Saint-Lambert - Caussols",
      lat : 43.742240,
      lng : 6.899948
    },
    {
      name : "Grotte chapelle de Calern - Caussols",
      lat : 43.7516442,
      lng : 6.9029889
    },
    {
      name : "Église Saint Grégoire - Tourrettes-sur-Loup",
      lat : 43.716708,
      lng : 7.059366
    },
    {
      name : "Chapelle Saint Jean - Tourrettes-sur-Loup",
      lat : 43.7175277,
      lng : 7.0565838
    },
    {
      name : "Abbaye du Canadel - La Colle sur Loup",
      lat : 43.685581,
      lng : 7.102900
    },
    {
      name : "Chapelle Saint Roch - La Colle sur Loup",
      lat : 43.685471,
      lng : 7.09152
    },
    {
      name : "La Collégiale : Église de la conversion de Saint Paul - Saint Paul de Vence",
      lat : 43.697273,
      lng : 7.121922
    },
    {
      name : "Chapelle Sainte Claire - Saint Paul de Vence",
      lat : 43.699420,
      lng : 7.121561
    },
    {
      name : "Eglise Saint Vincent - Gourdon",
      lat : 43.719634,
      lng : 6.977940
    },
    {
      name : "Chapelle Sainte-Catherine - Gourdon",
      lat : 43.719712,
      lng : 6.977710
    },
    {
      name : "Chapelle Saint-Pons",
      lat : 43.721076,
      lng : 6.977091
    },
    {
      name : "Chapelle de Pont du Loup",
      lat : 43.720750,
      lng : 6.991213
    },
    {
      name : "Église Saint Jacques le Majeur - Le Bar sur Loup",
      lat : 43.701562,
      lng : 6.989715
    },
    {
      name : "Chapelle des Soeurs Trinitaires - Le Bar sur Loup",
      lat : 43.700297,
      lng : 6.987580
    },
    {
      name : "Église paroissiale Saint Martin - Châteauneuf",
      lat : 43.6746727,
      lng : 6.975554
    },
    {
      name : "Église Saint Trophime - Opio",
      lat : 43.668507,
      lng : 6.982157
    },
    {
      name : "Église Saint-Pons - Le Rouret",
      lat : 43.6745469,
      lng : 7.0067754
    },
    {
      name : "Plateau de Calern",
      lat : 43.7608778,
      lng : 6.90125
    },
    {
      name : "Plateau de Calern",
      lat : 43.7608778,
      lng : 6.90125
    },
    {
      name : "Plateau de Calern",
      lat : 43.7608778,
      lng : 6.90125
    }

  ];

  return locationsObj;

}]);<commit_msg>supression d'un / 2 chateau de gourdon<commit_after>angular.module('casa').factory('LocationsService', [ function() {

  var locationsObj = {};

  locationsObj.savedLocations = [
    {
      name : "CASA",
      lat : 43.7015074,
      lng : 7.0301
    },
    {
      name : "Plateau de Calern",
      lat : 43.7608778,
      lng : 6.90125
    },
	{
      name : "Plateau de Caussols",
      lat : 43.7252074,
      lng : 6.904847
    },
	{
      name : "Plateau de la Malle",
      lat : 43.7017139,
      lng : 6.918923
    },
  {
      name : "Baume Robert - Le Rouret",
      lat : 43.6785604,
      lng : 7.001038
    },
	{
      name : "La Colle du Maçon - Caussols",
      lat : 43.7174875,
      lng : 6.881176
    },
	{
      name : "Le Col du Clapier",
      lat : 43.7134014,
      lng : 6.89977
    },
	{
      name : "Le Haut Montet",
      lat : 43.7162058,
      lng : 6.930729
    },
    {
      name : "La bergerie du Montet",
      lat : 43.7085029,
      lng : 6.929946
    },
    {
      name : "Château Grimaldi",
	  lat : 43.5807427,
      lng : 7.12822
    },
    {
      name : "Donjon - Le Bar-sur-Loup",
      lat : 43.701352,
      lng : 6.989333
    },
    {
      name : "Le Serre de la Madeleine",
      lat : 43.7601562,
      lng : 7.002371
    },
    {
      name : "Château de Villeneuve et parc",
      lat : 43.6598309,
      lng : 7.1241832
    },
    {
      name : "Tour de la Madone - Villeneuve-loubet",
      lat : 43.656238,
      lng : 7.098812
    },
    {
      name : "Donjon - Saint-Paul de Vence",
      lat : 43.696968,
      lng : 7.122259
    },
    {
      name : "Château de Tourrette-sur-Loup",
      lat : 43.715830,
      lng : 7.059306
    },
    {
      name : "Château de Gourdon",
      lat : 43.7201152,
      lng : 6.9784499
    },
    {
      name : "Château de Bar-sur-Loup",
      lat : 43.6911809,
      lng : 6.9809576
    },
    {
      name : "Château de Châteauneuf",
      lat : 43.674775,
      lng : 6.975999
    },
    {
      name : "Château d'Opio",
      lat : 43.668726,
      lng : 6.981965
    },
    {
      name : "Tour médiévale - Tourrettes-sur-Loup",
      lat : 43.716161,
      lng : 7.059036
    },
    {
      name : "Maison de Lartigue - Opio",
      lat : 43.6682697,
      lng : 6.9822241
    },
    {
      name : "Château - Vallauris",
      lat : 43.57984,
      lng : 7.053051
    },
    {
      name : "Place des Arcades - Valbonne",
      lat : 43.6414339,
      lng : 7.008779
    },
    {
      name : "Porte de Vence et Tour à mâchicoulis",
      lat : 43.697988,
      lng : 7.121944
    },
    {
      name : "Fort Carré - Antibes",
      lat : 43.590305,
      lng : 7.127230
    },
    {
      name : "Le bastion St André et ses remparts - Antibes",
      lat : 43.5772412,
      lng : 7.1260527
    },
    {
      name : "Le Clos Saint Pierre - Le Rouret",
      lat : 43.6744097,
      lng : 7.0062755
    },
    {
      name : "Maison seigneuriale - Courmes",
      lat : 43.742330,
      lng : 7.009302
    },
    {
      name : "Église Saint-Lambert - Caussols",
      lat : 43.742240,
      lng : 6.899948
    },
    {
      name : "Grotte chapelle de Calern - Caussols",
      lat : 43.7516442,
      lng : 6.9029889
    },
    {
      name : "Église Saint Grégoire - Tourrettes-sur-Loup",
      lat : 43.716708,
      lng : 7.059366
    },
    {
      name : "Chapelle Saint Jean - Tourrettes-sur-Loup",
      lat : 43.7175277,
      lng : 7.0565838
    },
    {
      name : "Abbaye du Canadel - La Colle sur Loup",
      lat : 43.685581,
      lng : 7.102900
    },
    {
      name : "Chapelle Saint Roch - La Colle sur Loup",
      lat : 43.685471,
      lng : 7.09152
    },
    {
      name : "La Collégiale : Église de la conversion de Saint Paul - Saint Paul de Vence",
      lat : 43.697273,
      lng : 7.121922
    },
    {
      name : "Chapelle Sainte Claire - Saint Paul de Vence",
      lat : 43.699420,
      lng : 7.121561
    },
    {
      name : "Eglise Saint Vincent - Gourdon",
      lat : 43.719634,
      lng : 6.977940
    },
    {
      name : "Chapelle Sainte-Catherine - Gourdon",
      lat : 43.719712,
      lng : 6.977710
    },
    {
      name : "Chapelle Saint-Pons",
      lat : 43.721076,
      lng : 6.977091
    },
    {
      name : "Chapelle de Pont du Loup",
      lat : 43.720750,
      lng : 6.991213
    },
    {
      name : "Église Saint Jacques le Majeur - Le Bar sur Loup",
      lat : 43.701562,
      lng : 6.989715
    },
    {
      name : "Chapelle des Soeurs Trinitaires - Le Bar sur Loup",
      lat : 43.700297,
      lng : 6.987580
    },
    {
      name : "Église paroissiale Saint Martin - Châteauneuf",
      lat : 43.6746727,
      lng : 6.975554
    },
    {
      name : "Église Saint Trophime - Opio",
      lat : 43.668507,
      lng : 6.982157
    },
    {
      name : "Église Saint-Pons - Le Rouret",
      lat : 43.6745469,
      lng : 7.0067754
    },
    {
      name : "Plateau de Calern",
      lat : 43.7608778,
      lng : 6.90125
    },
    {
      name : "Plateau de Calern",
      lat : 43.7608778,
      lng : 6.90125
    },
    {
      name : "Plateau de Calern",
      lat : 43.7608778,
      lng : 6.90125
    }

  ];

  return locationsObj;

}]);<|endoftext|>


<commit_before>/**
 * Copyright 2014 - 2015 Solinea, Inc.
 *
 * Licensed under the Solinea Software License Agreement (goldstone),
 * Version 1.0 (the "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at:
 *
 *     http://www.solinea.com/goldstone/LICENSE.pdf
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/*
View is currently directly implemented as Nova VM Spawns Viz
and extended into Nova CPU/Memory/Disk Resource Charts

standard chart usage. instantiate with:
{
    chartTitle: "title",
    collection: collectionName,
    featureSet: null or might be 'cpu/mem/disk/etc',
    height: 300,
    infoCustom: 'info button text set name',
    el: where to put it,
    width: $(el from above).width(),
    yAxisLabel: 'label name',
}
*/

// view is linked to collection when instantiated in api_perf_report.html

var StackedBarChartView = GoldstoneBaseView.extend({

    defaults: {
        margin: {
            top: 45,
            right: 40,
            bottom: 60,
            left: 70
        }
    },

    processOptions: function() {

        // this will invoke the processOptions method of the parent view,
        // and also add an additional param of featureSet which is used
        // to create a polymorphic interface for a variety of charts
        StackedBarChartView.__super__.processOptions.apply(this, arguments);

        this.defaults.featureSet = this.options.featureSet || null;
    },

    specialInit: function() {
        var ns = this.defaults;

        ns.yAxis = d3.svg.axis()
            .scale(ns.y)
            .orient("left")
            .tickFormat(d3.format("01d"));

        // differentiate color sets for mem and cpu charts
        if (ns.featureSet === 'mem' || ns.featureSet === 'cpu') {
            ns.color = d3.scale.ordinal().range(ns.colorArray.distinct[3]);
        } else {
            // this includes "VM Spawns" and "Disk Resources" chars
            ns.color = d3.scale.ordinal()
                .range(ns.colorArray.distinct[2]);
        }

    },

    dataPrep: function(data) {

        /*
        this is where the fetched JSON payload is transformed into a
        dataset than can be consumed by the D3 charts
        each chart may have its own perculiarities

        IMPORTANT:
        the order of items that are 'push'ed into the
        result array matters. After 'eventTime', the items
        will be stacked on the graph from the bottom of
        the graph upward. Or another way of saying it is
        the first item listed will be first one to be rendered
        from the x-axis of the graph going upward.
        */

        var ns = this.defaults;

        var result = [];

        if (ns.featureSet === 'cpu') {

            // CPU Resources chart data prep
            // {timestamp: [used, phys, virt]}
            _.each(data[0], function(item, i) {
                result.push({
                    "eventTime": "" + i,
                    "Used": item[0],
                    "Physical": Math.max((item[1] - item[0]), 0),
                    "Virtual": Math.max((item[2] - item[1]), 0)
                });
            });

        } else if (ns.featureSet === 'disk') {

            // Disk Resources chart data prep
            // {timestamp: [used, total]}
            _.each(data[0], function(item, i) {
                result.push({
                    "eventTime": "" + i,
                    "Used": item[0],
                    "Total": Math.max((item[1] - item[0]), 0)
                });
            });

        } else if (ns.featureSet === 'mem') {

            // Memory Resources chart data prep
            // {timestamp: [used, phys, virt]}
            _.each(data[0], function(item, i) {
                result.push({
                    "eventTime": "" + i,
                    "Used": item[0],
                    "Physical": Math.max((item[1] - item[0]), 0),
                    "Virtual": Math.max((item[2] - item[1]), 0)
                });
            });

        } else {

            // Spawns Resources chart data prep
            // {timestamp: [success, fail]}
            _.each(data[0], function(item, i) {
                result.push({
                    "eventTime": "" + i,
                    "Success": item[0],
                    "Failure": item[1]
                });
            });

        }

        return result;
    },

    update: function() {

        var ns = this.defaults;
        var self = this;

        var data = this.collection.toJSON();
        data = this.dataPrep(data);

        this.hideSpinner();

        if (this.checkReturnedDataSet(data) === false) {
            return;
        }

        // clear elements from previous render
        $(this.el).find('svg').find('rect').remove();
        $(this.el).find('svg').find('line').remove();
        $(this.el).find('svg').find('.axis').remove();
        $(this.el).find('svg').find('.legend').remove();
        $(this.el + '.d3-tip').detach();

        ns.color.domain(d3.keys(data[0]).filter(function(key) {
            return key !== "eventTime";
        }));

        data.forEach(function(d) {
            var y0 = 0;
            d.resultCategories = ns.color.domain().map(function(name) {
                return {
                    name: name,
                    y0: y0,
                    y1: y0 += +d[name]
                };
            });
            d.total = d.resultCategories[d.resultCategories.length - 1].y1;
        });

        ns.x.domain(d3.extent(data, function(d) {
            return d.eventTime;
        }));

        ns.y.domain([0, d3.max(data, function(d) {
            return d.total;
        })]);

        ns.chart.append("g")
            .attr("class", "x axis")
            .attr("transform", "translate(0," + ns.mh + ")")
            .call(ns.xAxis);

        ns.chart.append("g")
            .attr("class", "y axis")
            .call(ns.yAxis)
            .append("text")
            .attr("transform", "rotate(-90)")
            .attr("y", 6)
            .attr("dy", ".71em")
            .style("text-anchor", "end");

        ns.event = ns.chart.selectAll(".event")
            .data(data)
            .enter()
            .append("g")
            .attr("class", "g")
            .attr("transform", function(d) {
                return "translate(" + ns.x(d.eventTime) + ",0)";
            });

        var tip = d3.tip()
            .attr('class', 'd3-tip')
            .attr('id', this.el.slice(1))
            .html(function(d) {
                return "
" + d.name + "
" + d.y1;
            });

        // Invoke the tip in the context of your visualization

        ns.chart.call(tip);

        // used below to determing whether to render as
        // a "rect" or a "line"
        var showOrHide = {
            "Failure": true,
            "Success": true,
            "Virtual": false,
            "Physical": false,
            "Total": false,
            "Used": true
        };

        ns.event.selectAll("rect")
            .data(function(d) {
                return d.resultCategories;
            })
            .enter().append("rect")
            .attr("width", function(d) {
                var segmentWidth = (ns.mw / data.length);
                // spacing corrected
                // for proportional gaps between rects
                return segmentWidth - segmentWidth * 0.07;
            })
            .attr("y", function(d) {
                return ns.y(d.y1);
            })
            .attr("height", function(d) {
                return ns.y(d.y0) - ns.y(d.y1);
            })
            .attr("rx", 0.8)
            .attr("stroke", function(d) {
                return ns.color(d.name);
            })
            .attr("stroke-opacity", function(d) {
                if (!showOrHide[d.name]) {
                    return 0;
                } else {
                    return 0.9;
                }
            })
            .attr("fill-opacity", function(d) {
                if (!showOrHide[d.name]) {
                    return 0;
                } else {
                    return 0.7;
                }
            })
            .attr("stroke-width", 2)
            .style("fill", function(d) {
                return ns.color(d.name);
            }).on('mouseenter', function(d, i) {
                var targ = d3.select(self.el).select('rect');
                tip.offset([0, 0]).show(d, targ);
            }).on('mouseleave', function() {
                tip.hide();
            });

        ns.event.selectAll("line")
            .data(function(d) {
                return d.resultCategories;
            })
            .enter().append("line")
            .attr("x1", function(d) {
                var segmentWidth = (ns.mw / data.length);
                // makes the line solid
                // don't adjust for very small data sets
                if (data.length <= 3) {
                    return 0;
                } else {
                    return segmentWidth * -0.17;
                }
            })
            .attr("x2", function(d) {
                var segmentWidth = (ns.mw / data.length);
                // makes the line solid
                // don't adjust for very small data sets
                if (data.length <= 3) {
                    return segmentWidth;
                } else {
                    return segmentWidth + segmentWidth * 0.17;
                }
            })
            .attr("y1", function(d) {
                return ns.y(d.y1);
            })
            .attr("y2", function(d) {
                // horizontal line, so y1 === y2
                return ns.y(d.y1);
            })
            .attr("stroke", function(d) {
                // color of line
                return ns.color(d.name);
            })
            .attr("stroke-width", function(d) {
                // hide if data already used for "rect" above
                if (showOrHide[d.name]) {
                    return 0;
                } else {
                    return 2;
                }
            }).attr("stroke-dasharray", function(d) {
                if (d.name === "Physical") {
                    return "5, 3";
                } else {
                    return null;
                }
            });

        var legendSpecs = {
            mem: [
                ['Virtual', 2],
                ['Physical', 1],
                ['Used', 0]
            ],
            cpu: [
                ['Virtual', 2],
                ['Physical', 1],
                ['Used', 0]
            ],
            disk: [
                ['Total', 1],
                ['Used', 0]
            ],
            spawn: [
                ['Fail', 1],
                ['Success', 0]
            ]
        };

        if (ns.featureSet !== null) {
            this.appendLegend(legendSpecs[ns.featureSet]);
        } else {
            this.appendLegend(legendSpecs.spawn);
        }
    },

    appendLegend: function(legendSpecs) {

        // abstracts the appending of chart legends based on the
        // passed in array params [['Title', colorSetIndex],['Title', colorSetIndex'],...]

        var ns = this.defaults;

        _.each(legendSpecs, function(item) {
            ns.chart.append('path')
                .attr('class', 'line')
                .attr('id', item[0])
                .attr('data-legend', item[0])
                .attr('data-legend-color', ns.color.range()[item[1]]);
        });

        var legend = ns.chart.append('g')
            .attr('class', 'legend')
            .attr('transform', 'translate(20,-35)')
            .attr('opacity', 0.7)
            .call(d3.legend);
    },

    template: _.template(
        '

'),

    render: function() {

        new ChartHeaderView({
            el: this.el,
            columns: 12,
            chartTitle: this.defaults.chartTitle,
            infoText: this.defaults.infoCustom
        });

        $(this.el).find('.mainContainer').append(this.template());
        return this;
    }

});
<commit_msg>Updates logic for stackedBarChartView to compute popover values correctly.<commit_after>/**
 * Copyright 2014 - 2015 Solinea, Inc.
 *
 * Licensed under the Solinea Software License Agreement (goldstone),
 * Version 1.0 (the "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at:
 *
 *     http://www.solinea.com/goldstone/LICENSE.pdf
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/*
View is currently directly implemented as Nova VM Spawns Viz
and extended into Nova CPU/Memory/Disk Resource Charts

standard chart usage. instantiate with:
{
    chartTitle: "title",
    collection: collectionName,
    featureSet: null or might be 'cpu/mem/disk/etc',
    height: 300,
    infoCustom: 'info button text set name',
    el: where to put it,
    width: $(el from above).width(),
    yAxisLabel: 'label name',
}
*/

// view is linked to collection when instantiated in api_perf_report.html

var StackedBarChartView = GoldstoneBaseView.extend({

    defaults: {
        margin: {
            top: 45,
            right: 40,
            bottom: 60,
            left: 70
        }
    },

    processOptions: function() {

        // this will invoke the processOptions method of the parent view,
        // and also add an additional param of featureSet which is used
        // to create a polymorphic interface for a variety of charts
        StackedBarChartView.__super__.processOptions.apply(this, arguments);

        this.defaults.featureSet = this.options.featureSet || null;
    },

    specialInit: function() {
        var ns = this.defaults;

        ns.yAxis = d3.svg.axis()
            .scale(ns.y)
            .orient("left")
            .tickFormat(d3.format("01d"));

        // differentiate color sets for mem and cpu charts
        if (ns.featureSet === 'mem' || ns.featureSet === 'cpu') {
            ns.color = d3.scale.ordinal().range(ns.colorArray.distinct[3]);
        } else {
            // this includes "VM Spawns" and "Disk Resources" chars
            ns.color = d3.scale.ordinal()
                .range(ns.colorArray.distinct[2]);
        }

    },

    dataPrep: function(data) {

        /*
        this is where the fetched JSON payload is transformed into a
        dataset than can be consumed by the D3 charts
        each chart may have its own perculiarities

        IMPORTANT:
        the order of items that are 'push'ed into the
        result array matters. After 'eventTime', the items
        will be stacked on the graph from the bottom of
        the graph upward. Or another way of saying it is
        the first item listed will be first one to be rendered
        from the x-axis of the graph going upward.
        */

        var ns = this.defaults;

        var result = [];

        if (ns.featureSet === 'cpu') {

            // CPU Resources chart data prep
            // {timestamp: [used, phys, virt]}
            _.each(data[0], function(item, i) {
                result.push({
                    "eventTime": "" + i,
                    "Used": item[0],
                    "Physical": Math.max((item[1] - item[0]), 0),
                    "Virtual": Math.max((item[2] - item[1]), 0)
                });
            });

        } else if (ns.featureSet === 'disk') {

            // Disk Resources chart data prep
            // {timestamp: [used, total]}
            _.each(data[0], function(item, i) {
                result.push({
                    "eventTime": "" + i,
                    "Used": item[0],
                    "Total": Math.max((item[1] - item[0]), 0)
                });
            });

        } else if (ns.featureSet === 'mem') {

            // Memory Resources chart data prep
            // {timestamp: [used, phys, virt]}
            _.each(data[0], function(item, i) {
                result.push({
                    "eventTime": "" + i,
                    "Used": item[0],
                    "Physical": Math.max((item[1] - item[0]), 0),
                    "Virtual": Math.max((item[2] - item[1]), 0)
                });
            });

        } else {

            // Spawns Resources chart data prep
            // {timestamp: [success, fail]}
            _.each(data[0], function(item, i) {
                result.push({
                    "eventTime": "" + i,
                    "Success": item[0],
                    "Failure": item[1]
                });
            });

        }

        return result;
    },

    computeBarHeightPopover: function(d) {

        // 'Failure/Success' is only used by Spawn viz
        if (d.name === undefined) {
            d.name = 'Missing name param';
        }

        // if no y0 or y1 val, don't try to do math on undefined
        if (d.y0 === undefined || d.y1 === undefined) {
            return "
" + d.name + "
" + "No value reported";
        }

        // don't create a tooltip for zero values
        if (d.y0 === d.y1) {
            return null;
        }

        // otherwise return a string in the format of
        // "
Success
10"

        if (d.name === 'Failure' || d.name === 'Success') {

            // VM Spawn chart should only return the exact
            // value represented by the individual rect
            return "
" + d.name + "
" + (d.y1 - d.y0);
        } else {

            // the other charts should return the value
            // represented by the top edge of the rect
            return "
" + d.name + "
" + d.y1;
        }
    },

    update: function() {

        var ns = this.defaults;
        var self = this;

        var data = this.collection.toJSON();
        data = this.dataPrep(data);

        this.hideSpinner();

        if (this.checkReturnedDataSet(data) === false) {
            return;
        }

        // clear elements from previous render
        $(this.el).find('svg').find('rect').remove();
        $(this.el).find('svg').find('line').remove();
        $(this.el).find('svg').find('.axis').remove();
        $(this.el).find('svg').find('.legend').remove();
        $(this.el + '.d3-tip').detach();

        ns.color.domain(d3.keys(data[0]).filter(function(key) {
            return key !== "eventTime";
        }));

        data.forEach(function(d) {
            var y0 = 0;
            d.resultCategories = ns.color.domain().map(function(name) {
                return {
                    name: name,
                    y0: y0,
                    y1: y0 += +d[name]
                };
            });
            d.total = d.resultCategories[d.resultCategories.length - 1].y1;
        });

        ns.x.domain(d3.extent(data, function(d) {
            return d.eventTime;
        }));

        ns.y.domain([0, d3.max(data, function(d) {
            return d.total;
        })]);

        ns.chart.append("g")
            .attr("class", "x axis")
            .attr("transform", "translate(0," + ns.mh + ")")
            .call(ns.xAxis);

        ns.chart.append("g")
            .attr("class", "y axis")
            .call(ns.yAxis)
            .append("text")
            .attr("transform", "rotate(-90)")
            .attr("y", 6)
            .attr("dy", ".71em")
            .style("text-anchor", "end");

        ns.event = ns.chart.selectAll(".event")
            .data(data)
            .enter()
            .append("g")
            .attr("class", "g")
            .attr("transform", function(d) {
                return "translate(" + ns.x(d.eventTime) + ",0)";
            });

        var tip = d3.tip()
            .attr('class', 'd3-tip')
            .attr('id', this.el.slice(1))
            .html(function(d) {
                return self.computeBarHeightPopover(d);
            });

        // Invoke the tip in the context of your visualization

        ns.chart.call(tip);

        // used below to determing whether to render as
        // a "rect" or a "line"
        var showOrHide = {
            "Failure": true,
            "Success": true,
            "Virtual": false,
            "Physical": false,
            "Total": false,
            "Used": true
        };

        ns.event.selectAll("rect")
            .data(function(d) {
                return d.resultCategories;
            })
            .enter().append("rect")
            .attr("width", function(d) {
                var segmentWidth = (ns.mw / data.length);

                // spacing corrected
                // for proportional gaps between rects
                return segmentWidth - segmentWidth * 0.07;
            })
            .attr("y", function(d) {
                return ns.y(d.y1);
            })
            .attr("height", function(d) {
                return ns.y(d.y0) - ns.y(d.y1);
            })
            .attr("rx", 0.8)
            .attr("stroke", function(d) {
                return ns.color(d.name);
            })
            .attr("stroke-opacity", function(d) {
                if (!showOrHide[d.name]) {
                    return 0;
                } else {
                    return 0.9;
                }
            })
            .attr("fill-opacity", function(d) {
                if (!showOrHide[d.name]) {
                    return 0;
                } else {
                    return 0.7;
                }
            })
            .attr("stroke-width", 2)
            .style("fill", function(d) {
                return ns.color(d.name);
            }).on('mouseenter', function(d, i) {
                var targ = d3.select(self.el).select('rect');
                tip.offset([0, 0]).show(d, targ);
            }).on('mouseleave', function() {
                tip.hide();
            });

        ns.event.selectAll("line")
            .data(function(d) {
                return d.resultCategories;
            })
            .enter().append("line")
            .attr("x1", function(d) {
                var segmentWidth = (ns.mw / data.length);

                // makes the line solid
                // don't adjust for very small data sets
                if (data.length <= 3) {
                    return 0;
                } else {
                    return segmentWidth * -0.17;
                }
            })
            .attr("x2", function(d) {
                var segmentWidth = (ns.mw / data.length);
                // makes the line solid
                // don't adjust for very small data sets
                if (data.length <= 3) {
                    return segmentWidth;
                } else {
                    return segmentWidth + segmentWidth * 0.17;
                }
            })
            .attr("y1", function(d) {
                return ns.y(d.y1);
            })
            .attr("y2", function(d) {
                // horizontal line, so y1 === y2
                return ns.y(d.y1);
            })
            .attr("stroke", function(d) {
                // color of line
                return ns.color(d.name);
            })
            .attr("stroke-width", function(d) {
                // hide if data already used for "rect" above
                if (showOrHide[d.name]) {
                    return 0;
                } else {
                    return 2;
                }
            }).attr("stroke-dasharray", function(d) {
                if (d.name === "Physical") {
                    return "5, 3";
                } else {
                    return null;
                }
            });

        var legendSpecs = {
            mem: [
                ['Virtual', 2],
                ['Physical', 1],
                ['Used', 0]
            ],
            cpu: [
                ['Virtual', 2],
                ['Physical', 1],
                ['Used', 0]
            ],
            disk: [
                ['Total', 1],
                ['Used', 0]
            ],
            spawn: [
                ['Fail', 1],
                ['Success', 0]
            ]
        };

        if (ns.featureSet !== null) {
            this.appendLegend(legendSpecs[ns.featureSet]);
        } else {
            this.appendLegend(legendSpecs.spawn);
        }
    },

    appendLegend: function(legendSpecs) {

        // abstracts the appending of chart legends based on the
        // passed in array params [['Title', colorSetIndex],['Title', colorSetIndex'],...]

        var ns = this.defaults;

        _.each(legendSpecs, function(item) {
            ns.chart.append('path')
                .attr('class', 'line')
                .attr('id', item[0])
                .attr('data-legend', item[0])
                .attr('data-legend-color', ns.color.range()[item[1]]);
        });

        var legend = ns.chart.append('g')
            .attr('class', 'legend')
            .attr('transform', 'translate(20,-35)')
            .attr('opacity', 0.7)
            .call(d3.legend);
    },

    template: _.template(
        '

'),

    render: function() {

        new ChartHeaderView({
            el: this.el,
            columns: 12,
            chartTitle: this.defaults.chartTitle,
            infoText: this.defaults.infoCustom
        });

        $(this.el).find('.mainContainer').append(this.template());
        return this;
    }

});
<|endoftext|>