<commit_before>var CardNames = require('./names.js');
var InputCard = require('../card/card-input.js');
var TitledCard = require('../card/card-titled.js');
var TextareaCard = require('../card/card-textarea.js');
var ConditionalInputCard = require('../card/conditional-input-card.js');
var Snackbar = require('../views/snackbar/snackbar.js');
var HeaderService = require('../services/header-service.js');
var SheetsService = require('../services/sheets-service.js');
var EmailService = require('../services/email-service.js');

var CardsConfig = {};

CardsConfig.maxResults = 5;

/**
 * Initializes a Card repository.
 *
 * @return {Object<string, Card>} The repository used for storing Cards. These may not be in the program flow.
 */
CardsConfig.buildCardRepo = function(contentArea) {

  var repo = {};
  var hService = new HeaderService();
  var sService = new SheetsService();
  var eService = new EmailService();

  repo[CardNames.title] = new InputCard(contentArea, {
    title: 'Title',
    help: 'What should this merge be called? This title will help you differentiate this merge from others.',
    label: 'Title...'
  });

  repo[CardNames.sheet] = new InputCard(contentArea, {
    title: 'Tab',
    help: 'Which tab are we sending from? This tab must contain all the information you may want to send in an email.',
    label: 'Tab...'
  });
  sService.get().then(
    function(result) {
      repo[CardNames.sheet].setAutocomplete({
        results: result,
        maxResults: CardsConfig.maxResults,
        triggerOnFocus: true
      });
    },
    function(err) {
      console.error(err);
    }
  ).done();

  repo[CardNames.to] = new InputCard(contentArea, {
    title: 'To',
    help: 'Who are you sending to? This is the column filled with the email addresses of the recipients.',
    label: 'To...',
    error: {
      hint: 'Must be a single header surrounded by whack whacks',
      pattern: '<<[^<>]*>>'
    }
  });

  repo[CardNames.row] = new InputCard(contentArea, {
    title: 'Header Row',
    help: 'Which row contains your header titles?',
    label: 'Header row...',
    error: {
      hint: 'Must be a number greater than 0',
      pattern: '[1-9][0-9]*'
    }
  });
  repo[CardNames.row].setValue(1); // Default row 1.
  repo[CardNames.row].attachEvent('card.hide', function(event, card) {
    // Set the header row
    var row = card.getValue();
    var sheet = repo[CardNames.sheet].getValue();

    hService.get(sheet, row).then(
      function(values) {
        repo[CardNames.to].setAutocomplete({
          results: values,
          trigger: '<<',
          prepend: '<<',
          append: '>>',
          maxResults: CardsConfig.maxResults,
          triggerOnFocus: true
        });

        repo[CardNames.subject].setAutocomplete({
          results: values,
          trigger: '<<',
          prepend: '<<',
          append: '>>',
          maxResults: CardsConfig.maxResults
        });

        repo[CardNames.body].setAutocomplete({
          results: values,
          trigger: '<<',
          prepend: '<<',
          append: '>>',
          maxResults: CardsConfig.maxResults
        });

        repo[CardNames.conditional].setAutocomplete({
          results: values,
          trigger: '<<',
          prepend: '<<',
          append: '>>',
          maxResults: CardsConfig.maxResults,
          triggerOnFocus: true
        });
      },
      function(err) {
        console.error(err);
      }
    ).done();

  });

  repo[CardNames.subject] = new InputCard(contentArea, {
    title: 'Subject',
    help: 'What\'s your subject? Recipients will see this as the subject line of the email. ' +
      'Type << to see a list of column names. ' +
      'These whack whacks will be swapped out with the associated values in the Sheet.',
    label: 'Subject...'
  });

  repo[CardNames.body] = new TextareaCard(contentArea, {
    title: 'Body',
    help: 'What\'s in the body? Recipients will see this as the body of the email. ' +
      'Type << to see a list of column names. These whack whacks ' +
      'will be swapped out with the associated values in the Sheet.',
    label: 'Body...'
  });

  repo[CardNames.sendNow] = new TitledCard(contentArea, {
    title: 'Save',
    paragraphs: [
      'This will save the merge. It won\'t send any emails yet.',
      'If you would like to send yourself a test email, select the option from the lower right.'
    ]
  });
  repo[CardNames.sendNow].addOption('Send test email', function() {
    var sheet = repo[CardNames.sheet].getValue();
    var row = repo[CardNames.row].getValue();
    var subject = repo[CardNames.subject].getValue();
    var body = repo[CardNames.body].getValue();

    eService.sendTest(sheet, row, subject, body).then(
      function() {},
      function(err) {
        console.error(err);
      }
    ).done();

    Snackbar.show('Sending test email...');
  });

  repo[CardNames.conditional] = new ConditionalInputCard(contentArea, {
    title: 'Conditional send (optional)',
    help: 'This column is used to determine when to send an email. If a given row reads TRUE, ' +
      'Mailman will send an email. Any other value and Mailman won\'t send. This can be useful for scheduling your ' +
      'merges or ensuring you don\'t accidentally email someone twice.',
    label: 'Conditional',
    error: {
      hint: 'Must be a single header surrounded by whack whacks',
      pattern: '<<[^<>]*>>'
    },
    enabled: true,
    checkboxText: 'Filter this merge?'
  });

  return repo;
};

/** */
module.exports = CardsConfig;
<commit_msg>Added some text encouraging users to type whack whacks.<commit_after>var CardNames = require('./names.js');
var InputCard = require('../card/card-input.js');
var TitledCard = require('../card/card-titled.js');
var TextareaCard = require('../card/card-textarea.js');
var ConditionalInputCard = require('../card/conditional-input-card.js');
var Snackbar = require('../views/snackbar/snackbar.js');
var HeaderService = require('../services/header-service.js');
var SheetsService = require('../services/sheets-service.js');
var EmailService = require('../services/email-service.js');

var CardsConfig = {};

CardsConfig.maxResults = 5;

/**
 * Initializes a Card repository.
 *
 * @return {Object<string, Card>} The repository used for storing Cards. These may not be in the program flow.
 */
CardsConfig.buildCardRepo = function(contentArea) {

  var repo = {};
  var hService = new HeaderService();
  var sService = new SheetsService();
  var eService = new EmailService();

  repo[CardNames.title] = new InputCard(contentArea, {
    title: 'Title',
    help: 'What should this merge be called? This title will help you differentiate this merge from others.',
    label: 'Title...'
  });

  repo[CardNames.sheet] = new InputCard(contentArea, {
    title: 'Tab',
    help: 'Which tab are we sending from? This tab must contain all the information you may want to send in an email.',
    label: 'Tab...'
  });
  sService.get().then(
    function(result) {
      repo[CardNames.sheet].setAutocomplete({
        results: result,
        maxResults: CardsConfig.maxResults,
        triggerOnFocus: true
      });
    },
    function(err) {
      console.error(err);
    }
  ).done();

  repo[CardNames.to] = new InputCard(contentArea, {
    title: 'To',
    help: 'Who are you sending to? This is the column filled with the email addresses of the recipients.',
    label: 'To...',
    error: {
      hint: 'Must be a single header surrounded by whack whacks',
      pattern: '<<[^<>]*>>'
    }
  });

  repo[CardNames.row] = new InputCard(contentArea, {
    title: 'Header Row',
    help: 'Which row contains your header titles?',
    label: 'Header row...',
    error: {
      hint: 'Must be a number greater than 0',
      pattern: '[1-9][0-9]*'
    }
  });
  repo[CardNames.row].setValue(1); // Default row 1.
  repo[CardNames.row].attachEvent('card.hide', function(event, card) {
    // Set the header row
    var row = card.getValue();
    var sheet = repo[CardNames.sheet].getValue();

    hService.get(sheet, row).then(
      function(values) {
        repo[CardNames.to].setAutocomplete({
          results: values,
          trigger: '<<',
          prepend: '<<',
          append: '>>',
          maxResults: CardsConfig.maxResults,
          triggerOnFocus: true
        });

        repo[CardNames.subject].setAutocomplete({
          results: values,
          trigger: '<<',
          prepend: '<<',
          append: '>>',
          maxResults: CardsConfig.maxResults
        });

        repo[CardNames.body].setAutocomplete({
          results: values,
          trigger: '<<',
          prepend: '<<',
          append: '>>',
          maxResults: CardsConfig.maxResults
        });

        repo[CardNames.conditional].setAutocomplete({
          results: values,
          trigger: '<<',
          prepend: '<<',
          append: '>>',
          maxResults: CardsConfig.maxResults,
          triggerOnFocus: true
        });
      },
      function(err) {
        console.error(err);
      }
    ).done();

  });

  repo[CardNames.subject] = new InputCard(contentArea, {
    title: 'Subject',
    paragraphs: [
      'Tip: try typing <<'
    ],
    help: 'What\'s your subject? Recipients will see this as the subject line of the email. ' +
      'Type << to see a list of column names. ' +
      'These whack whacks will be swapped out with the associated values in the Sheet.',
    label: 'Subject...'
  });

  repo[CardNames.body] = new TextareaCard(contentArea, {
    title: 'Body',
    paragraphs: [
      'Tip: try typing <<'
    ],
    help: 'What\'s in the body? Recipients will see this as the body of the email. ' +
      'Type << to see a list of column names. These whack whacks ' +
      'will be swapped out with the associated values in the Sheet.',
    label: 'Body...'
  });

  repo[CardNames.sendNow] = new TitledCard(contentArea, {
    title: 'Save',
    paragraphs: [
      'This will save the merge. It won\'t send any emails yet.',
      'If you would like to send yourself a test email, select the option from the lower right.'
    ]
  });
  repo[CardNames.sendNow].addOption('Send test email', function() {
    var sheet = repo[CardNames.sheet].getValue();
    var row = repo[CardNames.row].getValue();
    var subject = repo[CardNames.subject].getValue();
    var body = repo[CardNames.body].getValue();

    eService.sendTest(sheet, row, subject, body).then(
      function() {},
      function(err) {
        console.error(err);
      }
    ).done();

    Snackbar.show('Sending test email...');
  });

  repo[CardNames.conditional] = new ConditionalInputCard(contentArea, {
    title: 'Conditional send (optional)',
    help: 'This column is used to determine when to send an email. If a given row reads TRUE, ' +
      'Mailman will send an email. Any other value and Mailman won\'t send. This can be useful for scheduling your ' +
      'merges or ensuring you don\'t accidentally email someone twice.',
    label: 'Conditional',
    error: {
      hint: 'Must be a single header surrounded by whack whacks',
      pattern: '<<[^<>]*>>'
    },
    enabled: true,
    checkboxText: 'Filter this merge?'
  });

  return repo;
};

/** */
module.exports = CardsConfig;
<|endoftext|>


<commit_before>#!/usr/bin/env node

const fs = require('fs');
const request = require('request-promise-native');
const yargs = require('yargs');
const {createBrowser} = require('./lib/browser');
const {hyphenize} = require('./lib/util');
const argv = getArgv();
const browser = createBrowser();
const startUrl = 'https://fairelections.ocf.dc.gov/public/FinancialReport';
const outputDir = __dirname + '/fair-elections';

main()
    .then(() => console.warn('Finished'))
    .catch(function (err) {
        console.trace(err);
        process.exit(1);
    });

async function main() {
    console.warn(`Getting ${startUrl}`);
    browser.silent = true;
    await browser.visit(startUrl);
    await browser.select('#pageSizeOptions', '100');
    if (argv.year) {
        await browser.select('#reportYear', argv.year.toString());
    }
    if (argv.report) {
        await browser.select('#reportId', argv.report);
    }
    if (argv.committee) {
        await browser.fill('input[placeholder="Search by Committee Name"]', argv.committee);
    }
    const rows = await browser.querySelectorAll('table > tbody > tr');
    for (const row of rows) {
        const cells = Array.from(row.querySelectorAll('td'));
        const link = cells[0].querySelector('a');
        const [committeeName, reportName, /* filingYear */, submittedDate] = cells.map(n => n.textContent);
        console.warn(`Getting ${submittedDate} ${reportName} for ${committeeName}`);
        const pdfFile =
            outputDir + '/' +
            hyphenize(
                committeeName.toLowerCase() + ' ' +
                reportName.replace(' Report', '') + ' ' +
                submittedDate.replace(/^(\d\d)\/(\d\d)\/(\d{4})$/, '$3$1$2')
            ) +
            '.pdf';
        if (!argv.refresh) {
            try {
                fs.accessSync(pdfFile);
                console.warn('File exists');
                continue;
            }
            catch (err) {
                // File doesn't exist, so go on
            }
        }
        await browser.click(link);
        const resource = browser.resources[browser.resources.length - 1];
        // A zombie bug causes the PDF content to be corrupted by conversion to a UTF-8 string,
        // so we have to make the request again with the request module
        const pdfUrl = resource.response.url;
        const jar = request.jar();
        for (const cookie of browser.cookies) {
            jar.setCookie(request.cookie(cookie.toString()), pdfUrl);
        }
        const pdfContent = await request({url: pdfUrl, gzip: true, encoding: null, jar});
        fs.writeFileSync(pdfFile, pdfContent);
        console.warn(`Wrote ${pdfContent.length} bytes to ${pdfFile}`);
    }
}

function getArgv() {
    return yargs
        .options({
            year: {
                type: 'number',
                describe: 'filing year',
            },
            report: {
                type: 'string',
                describe: 'report name',
            },
            committee: {
                type: 'string',
                describe: 'committee to download (search pattern)',
            },
            refresh: {
                type: 'boolean',
                describe: 'download file even if it already exists',
            },
        })
        .strict(true)
        .check(function (argv) {
            if (argv.report && !argv.year) {
                throw new Error('For report you must also specify year');
            }
            return true;
        })
        .argv;
}
<commit_msg>modify download-fair-elections.js to handle multiple pages of results<commit_after>#!/usr/bin/env node

const fs = require('fs');
const request = require('request-promise-native');
const yargs = require('yargs');
const {createBrowser} = require('./lib/browser');
const {hyphenize} = require('./lib/util');
const argv = getArgv();
const browser = createBrowser();
const startUrl = 'https://fairelections.ocf.dc.gov/public/FinancialReport';
const outputDir = __dirname + '/fair-elections';

main()
    .then(() => console.warn('Finished'))
    .catch(function (err) {
        console.trace(err);
        process.exit(1);
    });

async function main() {
    console.warn(`Getting ${startUrl}`);
    browser.silent = true;
    await browser.visit(startUrl);
    await browser.select('#pageSizeOptions', '100');
    if (argv.year) {
        await browser.select('#reportYear', argv.year.toString());
    }
    if (argv.report) {
        await browser.select('#reportId', argv.report);
    }
    if (argv.committee) {
        await browser.fill('input[placeholder="Search by Committee Name"]', argv.committee);
    }
    let page = 1;
    doTable: while (true) {
        const rows = await browser.querySelectorAll('table > tbody > tr');
        for (const row of rows) {
            const cells = Array.from(row.querySelectorAll('td'));
            const link = cells[0].querySelector('a');
            const [committeeName, reportName, /* filingYear */, submittedDate] = cells.map(n => n.textContent);
            console.warn(`Getting ${submittedDate} ${reportName} for ${committeeName}`);
            const pdfFile =
                outputDir + '/' +
                hyphenize(
                    committeeName.toLowerCase() + ' ' +
                    reportName.replace(' Report', '') + ' ' +
                    submittedDate.replace(/^(\d\d)\/(\d\d)\/(\d{4})$/, '$3$1$2')
                ) +
                '.pdf';
            if (!argv.refresh) {
                try {
                    fs.accessSync(pdfFile);
                    console.warn('File exists');
                    continue;
                }
                catch (err) {
                    // File doesn't exist, so go on
                }
            }
            await browser.click(link);
            const resource = browser.resources[browser.resources.length - 1];
            // A zombie bug causes the PDF content to be corrupted by conversion to a UTF-8 string,
            // so we have to make the request again with the request module
            const pdfUrl = resource.response.url;
            const jar = request.jar();
            for (const cookie of browser.cookies) {
                jar.setCookie(request.cookie(cookie.toString()), pdfUrl);
            }
            const pdfContent = await request({url: pdfUrl, gzip: true, encoding: null, jar});
            fs.writeFileSync(pdfFile, pdfContent);
            console.warn(`Wrote ${pdfContent.length} bytes to ${pdfFile}`);
        }
        page++;
        const buttons = await browser.querySelectorAll('ul.pagination > li.page-item > button');
        for (const button of buttons) {
            if (+button.textContent === page) {
                console.warn(`Clicking ${page}`);
                await browser.click(button);
                continue doTable;
            }
        }
        console.warn('Last page');
        break;
    }
}

function getArgv() {
    return yargs
        .options({
            year: {
                type: 'number',
                describe: 'filing year',
            },
            report: {
                type: 'string',
                describe: 'report name',
            },
            committee: {
                type: 'string',
                describe: 'committee to download (search pattern)',
            },
            refresh: {
                type: 'boolean',
                describe: 'download file even if it already exists',
            },
        })
        .strict(true)
        .check(function (argv) {
            if (argv.report && !argv.year) {
                throw new Error('For report you must also specify year');
            }
            return true;
        })
        .argv;
}
<|endoftext|>


<commit_before>//----------------------------//

function Utils(){};

//----------------------------//

/*
 * zeroPad(5, 2) 	--> "05"
   zeroPad(1234, 2) --> "1234"
 */
Utils.prototype.zeroPad = function(num, places)
{
   var zero = places - num.toString().length + 1;
   return Array(+(zero > 0 && zero)).join("0") + num;
}

/*
 * now as YYYY-MM-DD
 */
Utils.prototype.dateTime = function()
{
   var now = new Date();
   return now.getFullYear() + "-"
   + this.zeroPad(now.getMonth()+1, 2) + "-"
   + this.zeroPad(now.getDate(), 2);
}

//----------------------------------------------------------------------------------------//

Utils.prototype.alert = function (area, type, title, message) {
   $("#" + area).append($("

"+title+"
 " + message + "
"));
   //$(".alert-message").delay(2000).fadeOut("slow", function () { $(this).remove(); });
}

//----------------------------------------------------------------------------------------//

/*
 * helpers for html encoding and decoding
 */
Utils.prototype.htmlEncode = function (value){
   return $('
').text(value).html();
}

Utils.prototype.htmlDecode = function(value){
   return $('
').html(value).text();
}

//----------------------------------------------------------------------------------------//

/*
 */
Utils.prototype.replaceAll = function(chain, value, replacement)
{
   return chain.replace(new RegExp(value, 'g'), replacement);
}

//----------------------------------------------------------------------------------------//

Utils.prototype.rgbToHex = function (r, g, b) {
   if (r > 255 || g > 255 || b > 255)
      throw "Invalid color component";
   return ((r << 16) | (g << 8) | b).toString(16);
}

//----------------------------------------------------------------------------------------//

/***
 * bytes = 36550
 * return 36.55 KB
 */
Utils.prototype.formatFileSize = function (bytes)
{
   if (typeof bytes !== 'number') {
      return '';
   }
   if (bytes >= 1000000000) {
      return (bytes / 1000000000).toFixed(2) + ' GB';
   }
   if (bytes >= 1000000) {
      return (bytes / 1000000).toFixed(2) + ' MB';
   }
   return (bytes / 1000).toFixed(2) + ' KB';
}
//----------------------------------------------------------------------------------------//

/***
 * timestamp = 1355342389711
 * return 12/12/2012
 *
 * timestamp = undefined => use today.
 *
 * @Improve #MAP-12
 */
Utils.prototype.formatDate = function(timestamp)
{
   var now = timestamp == undefined ? new Date() : new Date(timestamp);
   var day = this.zeroPad(now.getDate(), 2);
   var month = this.zeroPad(now.getMonth() + 1, 2); //Months are zero based
   var year = now.getFullYear();

   return day + "/" + month + "/" + year;
}

//----------------------------------------------------------------------------------------//

//return 1->i
Utils.prototype.random1 = function(i){
   return Math.floor(Math.random()*i) + 1;
}

//return 0->i
Utils.prototype.random0 = function(i){
   return Math.floor(Math.random()*(i+1));
}

//----------------------------------------------------------------------------------------//

Utils.prototype.generateGuid = function()
{
   var result, i, j;
   result = '';
   for(j=0; j<32; j++) {
      if( j == 8 || j == 12|| j == 16|| j == 20)
         result = result + '_';
      i = this.random0(15).toString(16).toUpperCase();
      result = result + i;
   }
   return result;
}

//----------------------------------------------------------------------------------------//

Utils.prototype.generateUID = function()
{
   var timestamp = new Date().getTime().toString(16);
   var random    = (Math.random() * Math.pow(2, 32)).toString(16);

   return timestamp + random;
}

//----------------------------------------------------------------------------------------//

Utils.prototype.popup = function(url, title, width, height)
{
   var left = (screen.width/2)-(width/2);
   var top = (screen.height/2)-(height/2);
   return window.open(url, title, 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, width='+width+', height='+height+', top='+top+', left='+left);
}

//----------------------------------------------------------------------------------------//

/**
 * custom mustache evaluation : )
 * data is used for the functions-in-customMustache parameters
 *
 * http://map.x-ray.fr/wiki/display/IDEES/Custom+Mustaches
 */
Utils.prototype.toHtml = function(template)
{
   while(template.indexOf("{") != -1)
   {
      var customMustache = template.substring(template.indexOf("{"), template.indexOf("}")+1);

      var html = eval(customMustache);
      template = template.replace(customMustache, html);
   }

   return template;
}

//----------------------------------------------------------------------------------------//

Utils.prototype.isObject = function(stuff)
{
   return Object.prototype.toString.call( stuff ) === '[object Object]' ;
}

/**
 * Ember : edition + binding of objects contained in an array : thanks to ObjectProxy
 */
Utils.prototype.editObjectInArray = function(object, property, value)
{
   var proxy = Ember.ObjectProxy.create({
      content: object
   });

   proxy.set(property, value);
}

//----------------------------------------------------------------------------------------//

Utils.prototype.styleThumbURL = function(styleUID, size)
{
   return this.thumbURL(styleUID, "style", size)
}

Utils.prototype.colorbarThumbURL = function(colorbarUID)
{
   return this.thumbURL(colorbarUID, "colorbar")
}

//----------------------------------------------------------------------------------------//

Utils.prototype.thumbURL = function(uid, type, size)
{
   if(uid == undefined || uid == null)
      return "";

   if(size == undefined || size == null)
      size = "";
   else
      size = "_"+size;

   var end = uid.substring(uid.length-4);
   var folders = end.split("");

   var url = "http://static.maperial.com/thumbs/" + type;
   folders.forEach(function(folder) {
      url += "/" + folder;
   });

   return url + "/" + uid + size + ".png";
}

//----------------------------------------------------------------------------------------//

Utils.prototype.getSourceThumb = function(layer) {

   switch(layer.source.type){
      case Source.MaperialOSM:
         return " src=\""+this.styleThumbURL(layer.params.styles[layer.params.selectedStyle], "l")+"\"";

      case Source.Vector:
      case Source.Images:
      case Source.WMS:
         return " src=\"http://static.maperial.localhost/images/icons/layer."+layer.source.params.src+".png\"";

      case Source.Raster:
         return " src=\"http://static.maperial.localhost/images/icons/layer.raster.png\""; // TODO : thumb du raster
   }

   switch(layer.type){
      case LayersManager.SRTM:
         return " src=\"http://static.maperial.localhost/images/icons/layer.srtm.png\"";

      case LayersManager.Shade:
      default:
         return " src=\"http://static.maperial.localhost/images/icons/layer.shade.png\"";
   }
}

//----------------------------------------------------------------------------------------//

//ui-slider-handle ui-state-default ui-corner-all
Utils.prototype.buildSliderStyle = function (id){

   $("#" + id + " a").css({color:"#000"});
   $("#" + id + " a").css({textDecoration:"none"});
   $("#" + id + " a").css({textAlign:"center"});
   $("#" + id + " a").css({width:"20px"});
   $("#" + id + " a").css({height:"20px"});
   $("#" + id + " a").css({borderTopLeftRadius:"30px"});
   $("#" + id + " a").css({borderTopRightRadius:"30px"});
   $("#" + id + " a").css({borderBottomLeftRadius:"30px"});
   $("#" + id + " a").css({borderBottomRightRadius:"30px"});
   $("#" + id + " a").css({outline:"none"});
   $("#" + id + " a").css({cursor:"pointer"});
   $("#" + id + " a").css({cursor:"hand"});

}

//----------------------------------------------------------------------------------------//

Utils.prototype.apply = function (toObject, methodName){
   return (function(param1, param2, param3, param4, param5, param6){toObject[methodName](param1, param2, param3, param4, param5, param6)});
}

Utils.prototype.getPoint = function (event) {
   return {
       x : event.clientX - $(event.target).offset().left,
       y : event.clientY - $(event.target).offset().top
   };
}

Utils.prototype.randomRotate = function (element) {

   var rotation = this.random0(15) - 8
   if(Math.abs(rotation) < 2)
      this.randomRotate(element)
   else{
      $("#"+element).css("-webkit-transform", "rotate("+rotation+"deg)")
      $("#"+element).css("-moz-transform", "rotate("+rotation+"deg)")
   }

}

//----------------------------------------------------------------------------------------//

Utils.prototype.prepareOptions = function (options, mainParam) {

   if(options === undefined){
      return null
   }

   else if(typeof options == "string"){
      var value = options
      var newOptions = {}
      newOptions[mainParam] = value
      return newOptions
   }

   else if(options[mainParam] === undefined){
      console.log("Could not find " + mainParam + ". Check your options.")
      return null
   }

   else
      return options
}

//----------------------------------------------------------------------------------------//

Utils.prototype.cloneJsonObject = function (jsonObject) {
   return $.parseJSON(JSON.stringify(jsonObject));
}

Utils.prototype.odump = function(o){
   console.log(this.cloneJsonObject(o));
}

//------------------------------------------------------------------//

module.exports = new Utils();<commit_msg>cleanup utils<commit_after>//----------------------------//

function Utils(){};

//----------------------------//

/*
 * zeroPad(5, 2) 	--> "05"
   zeroPad(1234, 2) --> "1234"
 */
Utils.prototype.zeroPad = function(num, places)
{
   var zero = places - num.toString().length + 1;
   return Array(+(zero > 0 && zero)).join("0") + num;
}

/*
 * now as YYYY-MM-DD
 */
Utils.prototype.dateTime = function()
{
   var now = new Date();
   return now.getFullYear() + "-"
   + this.zeroPad(now.getMonth()+1, 2) + "-"
   + this.zeroPad(now.getDate(), 2);
}

//----------------------------------------------------------------------------------------//

Utils.prototype.alert = function (area, type, title, message) {
   $("#" + area).append($("

"+title+"
 " + message + "
"));
   //$(".alert-message").delay(2000).fadeOut("slow", function () { $(this).remove(); });
}

//----------------------------------------------------------------------------------------//

/*
 * helpers for html encoding and decoding
 */
Utils.prototype.htmlEncode = function (value){
   return $('
').text(value).html();
}

Utils.prototype.htmlDecode = function(value){
   return $('
').html(value).text();
}

//----------------------------------------------------------------------------------------//

/*
 */
Utils.prototype.replaceAll = function(chain, value, replacement)
{
   return chain.replace(new RegExp(value, 'g'), replacement);
}

//----------------------------------------------------------------------------------------//

Utils.prototype.rgbToHex = function (r, g, b) {
   if (r > 255 || g > 255 || b > 255)
      throw "Invalid color component";
   return ((r << 16) | (g << 8) | b).toString(16);
}

//----------------------------------------------------------------------------------------//

/***
 * bytes = 36550
 * return 36.55 KB
 */
Utils.prototype.formatFileSize = function (bytes)
{
   if (typeof bytes !== 'number') {
      return '';
   }
   if (bytes >= 1000000000) {
      return (bytes / 1000000000).toFixed(2) + ' GB';
   }
   if (bytes >= 1000000) {
      return (bytes / 1000000).toFixed(2) + ' MB';
   }
   return (bytes / 1000).toFixed(2) + ' KB';
}
//----------------------------------------------------------------------------------------//

/***
 * timestamp = 1355342389711
 * return 12/12/2012
 *
 * timestamp = undefined => use today.
 *
 * @Improve #MAP-12
 */
Utils.prototype.formatDate = function(timestamp)
{
   var now = timestamp == undefined ? new Date() : new Date(timestamp);
   var day = this.zeroPad(now.getDate(), 2);
   var month = this.zeroPad(now.getMonth() + 1, 2); //Months are zero based
   var year = now.getFullYear();

   return day + "/" + month + "/" + year;
}

//----------------------------------------------------------------------------------------//

//return 1->i
Utils.prototype.random1 = function(i){
   return Math.floor(Math.random()*i) + 1;
}

//return 0->i
Utils.prototype.random0 = function(i){
   return Math.floor(Math.random()*(i+1));
}

//----------------------------------------------------------------------------------------//

Utils.prototype.generateUID = function()
{
   var timestamp = new Date().getTime().toString(16);
   var random    = (Math.random() * Math.pow(2, 32)).toString(16);

   return timestamp + random;
}

//----------------------------------------------------------------------------------------//

Utils.prototype.popup = function(url, title, width, height)
{
   var left = (screen.width/2)-(width/2);
   var top = (screen.height/2)-(height/2);
   return window.open(url, title, 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, width='+width+', height='+height+', top='+top+', left='+left);
}

//----------------------------------------------------------------------------------------//

Utils.prototype.isObject = function(stuff)
{
   return Object.prototype.toString.call( stuff ) === '[object Object]' ;
}

//----------------------------------------------------------------------------------------//

Utils.prototype.styleThumbURL = function(styleUID, size)
{
   return this.thumbURL(styleUID, "style", size)
}

Utils.prototype.colorbarThumbURL = function(colorbarUID)
{
   return this.thumbURL(colorbarUID, "colorbar")
}

//----------------------------------------------------------------------------------------//

Utils.prototype.thumbURL = function(uid, type, size)
{
   if(uid == undefined || uid == null)
      return "";

   if(size == undefined || size == null)
      size = "";
   else
      size = "_"+size;

   var end = uid.substring(uid.length-4);
   var folders = end.split("");

   var url = "http://static.maperial.com/thumbs/" + type;
   folders.forEach(function(folder) {
      url += "/" + folder;
   });

   return url + "/" + uid + size + ".png";
}

//----------------------------------------------------------------------------------------//

Utils.prototype.getSourceThumb = function(layer) {

   switch(layer.source.type){
      case Source.MaperialOSM:
         return " src=\""+this.styleThumbURL(layer.params.styles[layer.params.selectedStyle], "l")+"\"";

      case Source.Vector:
      case Source.Images:
      case Source.WMS:
         return " src=\"http://static.maperial.localhost/images/icons/layer."+layer.source.params.src+".png\"";

      case Source.Raster:
         return " src=\"http://static.maperial.localhost/images/icons/layer.raster.png\""; // TODO : thumb du raster
   }

   switch(layer.type){
      case LayersManager.SRTM:
         return " src=\"http://static.maperial.localhost/images/icons/layer.srtm.png\"";

      case LayersManager.Shade:
      default:
         return " src=\"http://static.maperial.localhost/images/icons/layer.shade.png\"";
   }
}

//----------------------------------------------------------------------------------------//

//ui-slider-handle ui-state-default ui-corner-all
Utils.prototype.buildSliderStyle = function (id){

   $("#" + id + " a").css({color:"#000"});
   $("#" + id + " a").css({textDecoration:"none"});
   $("#" + id + " a").css({textAlign:"center"});
   $("#" + id + " a").css({width:"20px"});
   $("#" + id + " a").css({height:"20px"});
   $("#" + id + " a").css({borderTopLeftRadius:"30px"});
   $("#" + id + " a").css({borderTopRightRadius:"30px"});
   $("#" + id + " a").css({borderBottomLeftRadius:"30px"});
   $("#" + id + " a").css({borderBottomRightRadius:"30px"});
   $("#" + id + " a").css({outline:"none"});
   $("#" + id + " a").css({cursor:"pointer"});
   $("#" + id + " a").css({cursor:"hand"});

}

//----------------------------------------------------------------------------------------//

Utils.prototype.getPoint = function (event) {
   return {
       x : event.clientX - $(event.target).offset().left,
       y : event.clientY - $(event.target).offset().top
   };
}

Utils.prototype.randomRotate = function (element) {

   var rotation = this.random0(15) - 8
   if(Math.abs(rotation) < 2)
      this.randomRotate(element)
   else{
      $("#"+element).css("-webkit-transform", "rotate("+rotation+"deg)")
      $("#"+element).css("-moz-transform", "rotate("+rotation+"deg)")
   }

}

//----------------------------------------------------------------------------------------//

Utils.prototype.prepareOptions = function (options, mainParam) {

   if(options === undefined){
      return null
   }

   else if(typeof options == "string"){
      var value = options
      var newOptions = {}
      newOptions[mainParam] = value
      return newOptions
   }

   else if(options[mainParam] === undefined){
      console.log("Could not find " + mainParam + ". Check your options.")
      return null
   }

   else
      return options
}

//----------------------------------------------------------------------------------------//

Utils.prototype.cloneJsonObject = function (jsonObject) {
   return $.parseJSON(JSON.stringify(jsonObject));
}

Utils.prototype.odump = function(o){
   console.log(this.cloneJsonObject(o));
}

//------------------------------------------------------------------//

module.exports = new Utils();<|endoftext|>


<commit_before>window.onload = () => {
    history.replaceState("", document.title, window.location.pathname);
}

function showSearchResults() {
    $.ajax({
        method: "GET",
        url: "/search",
        cache: false
    })
    .done((results) => {
        history.pushState("", document.title, window.location + "?page=1")
        // $('#search-results').html(results)
        $(results).appendTo("#search-results")
    })
}

function inWork() {
    $(event.target).parent().hide()
    let title = $(event.target).parent().find('a')
    console.log(title.attr('href'))
    console.log(title.text())
    let obj = {
        title: title.text(),
        url: title.attr('href')
    }
    $.ajax({
        method: "GET",
        data: { data: JSON.stringify(obj) },
        url: "/inwork",
        cache: false
    })
    .done(result => {
        let dest = $('#issue-in-work').find('ul')
        $(result).appendTo(dest)
        console.log('Issue with href=' + obj.url + ' was added to inwork list successfully')
    })
}

function delIssue() {
    let row = $(event.target).parent().parent()
    row.hide();
    let href = row.find('a').attr('href')
    console.log(href)
    $.ajax({
        method: "GET",
        data: {href: href},
        url: "/del",
        cache: false
    })
    .done((msg) => {
        console.log(msg)
    })
}

function prIssue() {
    let row = $(event.target).parent().parent()
    row.hide();
    let title = row.find('a')
    let issue = {
        url: title.attr('href'),
        title: title.text()
    }
    $.ajax({
        method: "GET",
        data: { issue: JSON.stringify(issue) },
        url: "/pr",
        cache: false
    })
    .done(result => {
        let dest = $('#pr-issue').find('ul')
        $(result).appendTo(dest)
        console.log('Issue with href=' + issue.url + ' was pr-ed list successfully')
    })
}
<commit_msg>Iterate 'page' param when 'Show More' clicked<commit_after>window.onload = () => {
    history.replaceState("", document.title, window.location.pathname);
}

function showSearchResults() {
    $.ajax({
        method: "GET",
        url: "/search",
        cache: false
    })
    .done((results) => {
        history.pushState("", document.title, window.location + "?page=1")
        // $('#search-results').html(results)
        $(results).appendTo("#search-results")
    })
}

function showMore() {
    let pageNumber = window.location.search.substr(1).split("=")[1]
    pageNumber++
    history.pushState("", document.title, window.location.pathname + "?page=" + pageNumber.toString())
}

function inWork() {
    $(event.target).parent().hide()
    let title = $(event.target).parent().find('a')
    console.log(title.attr('href'))
    console.log(title.text())
    let obj = {
        title: title.text(),
        url: title.attr('href')
    }
    $.ajax({
        method: "GET",
        data: { data: JSON.stringify(obj) },
        url: "/inwork",
        cache: false
    })
    .done(result => {
        let dest = $('#issue-in-work').find('ul')
        $(result).appendTo(dest)
        console.log('Issue with href=' + obj.url + ' was added to inwork list successfully')
    })
}

function delIssue() {
    let row = $(event.target).parent().parent()
    row.hide();
    let href = row.find('a').attr('href')
    console.log(href)
    $.ajax({
        method: "GET",
        data: {href: href},
        url: "/del",
        cache: false
    })
    .done((msg) => {
        console.log(msg)
    })
}

function prIssue() {
    let row = $(event.target).parent().parent()
    row.hide();
    let title = row.find('a')
    let issue = {
        url: title.attr('href'),
        title: title.text()
    }
    $.ajax({
        method: "GET",
        data: { issue: JSON.stringify(issue) },
        url: "/pr",
        cache: false
    })
    .done(result => {
        let dest = $('#pr-issue').find('ul')
        $(result).appendTo(dest)
        console.log('Issue with href=' + issue.url + ' was pr-ed list successfully')
    })
}
<|endoftext|>


<commit_before>// `use strict` is still necessary here since this file is not treated as a module.
'use strict'; // eslint-disable-line strict, lines-around-directive

const BACKEND = process.env.BACKEND || 'https://treeherder.mozilla.org';

module.exports = {
  options: {
    source: 'ui/',
    mains: {
      index: {
        entry: 'job-view/index.jsx',
        favicon: 'ui/img/tree_open.png',
        title: 'Treeherder',
      },
      logviewer: {
        entry: 'logviewer/index.jsx',
        favicon: 'ui/img/logviewerIcon.png',
        title: 'Treeherder Logviewer',
      },
      userguide: {
        entry: 'userguide/index.jsx',
        favicon: 'ui/img/tree_open.png',
        title: 'Treeherder User Guide',
      },
      login: {
        entry: 'login-callback/index.jsx',
        title: 'Treeherder Login',
      },
      testview: {
        entry: 'test-view/index.jsx',
        title: 'Treeherder Test View',
      },
      perf: {
        entry: 'entry-perf.js',
        template: 'ui/perf.html',
      },
      'intermittent-failures': {
        entry: 'intermittent-failures/index.jsx',
        title: 'Intermittent Failures View',
      },
    },
    tests: 'tests/ui/',
  },
  use: [
    process.env.NODE_ENV === 'development' && ['@neutrinojs/eslint', {
      eslint: {
        // Treat ESLint errors as warnings so they don't block the webpack build.
        // Remove if/when changed in @neutrinojs/eslint.
        emitWarning: true,
        // We manage our lint config in .eslintrc.js instead of here.
        useEslintrc: true,
      },
    }],
    ['@neutrinojs/react', {
      devServer: {
        historyApiFallback: false,
        open: !process.env.MOZ_HEADLESS,
        // Remove when enabled by default (https://github.com/neutrinojs/neutrino/issues/1131).
        overlay: true,
        proxy: {
          // Proxy any paths not recognised by webpack to the specified backend.
          '*': {
            changeOrigin: true,
            headers: {
              // Prevent Django CSRF errors, whilst still making it clear
              // that the requests were from local development.
              referer: `${BACKEND}/webpack-dev-server`,
            },
            target: BACKEND,
            onProxyRes: (proxyRes) => {
              // Strip the cookie `secure` attribute, otherwise production's cookies
              // will be rejected by the browser when using non-HTTPS localhost:
              // https://github.com/nodejitsu/node-http-proxy/pull/1166
              const removeSecure = str => str.replace(/; secure/i, '');
              const cookieHeader = proxyRes.headers['set-cookie'];
              if (cookieHeader) {
                proxyRes.headers['set-cookie'] = Array.isArray(cookieHeader)
                  ? cookieHeader.map(removeSecure)
                  : removeSecure(cookieHeader);
              }
            },
          },
        },
        // Inside Vagrant filesystem watching has to be performed using polling mode,
        // since inotify doesn't work with Virtualbox shared folders.
        watchOptions: process.env.USE_WATCH_POLLING && {
          // Poll only once a second and ignore the node_modules folder to keep CPU usage down.
          poll: 1000,
          ignored: /node_modules/,
        },
      },
      devtool: {
        // Enable source maps for `yarn build` too (but not on CI, since it doubles build times).
        production: process.env.CI ? false : 'source-map',
      },
      html: {
        // Disable the default viewport meta tag, since Treeherder doesn't work well at
        // small viewport sizes, so shouldn't use `width=device-width` (see bug 1505417).
        meta: false,
      },
      style: {
        // Disable Neutrino's CSS modules support, since we don't use it.
        modules: false,
      },
      targets: {
        browsers: [
          'last 1 Chrome versions',
          'last 1 Edge versions',
          'last 1 Firefox versions',
          'last 1 Safari versions',
        ],
      },
    }],
    ['@neutrinojs/copy', {
      patterns: [
        'ui/contribute.json',
        'ui/revision.txt',
        'ui/robots.txt',
      ],
    }],
    (neutrino) => {
      neutrino.config
        .plugin('provide')
        .use(require.resolve('webpack/lib/ProvidePlugin'), [{
          // Required since AngularJS and jquery.flot don't import jQuery themselves.
          jQuery: 'jquery',
          'window.jQuery': 'jquery',
        }]);

      if (process.env.NODE_ENV === 'production') {
        // Fail the build if these file size thresholds (in bytes) are exceeded,
        // to help prevent unknowingly regressing the bundle size (bug 1384255).
        neutrino.config.performance
          .hints('error')
          .maxAssetSize(1.20 * 1024 * 1024)
          .maxEntrypointSize(1.64 * 1024 * 1024);
      }
    },
  ],
};
<commit_msg>Bump asset threshold to 1.3MiB<commit_after>// `use strict` is still necessary here since this file is not treated as a module.
'use strict'; // eslint-disable-line strict, lines-around-directive

const BACKEND = process.env.BACKEND || 'https://treeherder.mozilla.org';

module.exports = {
  options: {
    source: 'ui/',
    mains: {
      index: {
        entry: 'job-view/index.jsx',
        favicon: 'ui/img/tree_open.png',
        title: 'Treeherder',
      },
      logviewer: {
        entry: 'logviewer/index.jsx',
        favicon: 'ui/img/logviewerIcon.png',
        title: 'Treeherder Logviewer',
      },
      userguide: {
        entry: 'userguide/index.jsx',
        favicon: 'ui/img/tree_open.png',
        title: 'Treeherder User Guide',
      },
      login: {
        entry: 'login-callback/index.jsx',
        title: 'Treeherder Login',
      },
      testview: {
        entry: 'test-view/index.jsx',
        title: 'Treeherder Test View',
      },
      perf: {
        entry: 'entry-perf.js',
        template: 'ui/perf.html',
      },
      'intermittent-failures': {
        entry: 'intermittent-failures/index.jsx',
        title: 'Intermittent Failures View',
      },
    },
    tests: 'tests/ui/',
  },
  use: [
    process.env.NODE_ENV === 'development' && ['@neutrinojs/eslint', {
      eslint: {
        // Treat ESLint errors as warnings so they don't block the webpack build.
        // Remove if/when changed in @neutrinojs/eslint.
        emitWarning: true,
        // We manage our lint config in .eslintrc.js instead of here.
        useEslintrc: true,
      },
    }],
    ['@neutrinojs/react', {
      devServer: {
        historyApiFallback: false,
        open: !process.env.MOZ_HEADLESS,
        // Remove when enabled by default (https://github.com/neutrinojs/neutrino/issues/1131).
        overlay: true,
        proxy: {
          // Proxy any paths not recognised by webpack to the specified backend.
          '*': {
            changeOrigin: true,
            headers: {
              // Prevent Django CSRF errors, whilst still making it clear
              // that the requests were from local development.
              referer: `${BACKEND}/webpack-dev-server`,
            },
            target: BACKEND,
            onProxyRes: (proxyRes) => {
              // Strip the cookie `secure` attribute, otherwise production's cookies
              // will be rejected by the browser when using non-HTTPS localhost:
              // https://github.com/nodejitsu/node-http-proxy/pull/1166
              const removeSecure = str => str.replace(/; secure/i, '');
              const cookieHeader = proxyRes.headers['set-cookie'];
              if (cookieHeader) {
                proxyRes.headers['set-cookie'] = Array.isArray(cookieHeader)
                  ? cookieHeader.map(removeSecure)
                  : removeSecure(cookieHeader);
              }
            },
          },
        },
        // Inside Vagrant filesystem watching has to be performed using polling mode,
        // since inotify doesn't work with Virtualbox shared folders.
        watchOptions: process.env.USE_WATCH_POLLING && {
          // Poll only once a second and ignore the node_modules folder to keep CPU usage down.
          poll: 1000,
          ignored: /node_modules/,
        },
      },
      devtool: {
        // Enable source maps for `yarn build` too (but not on CI, since it doubles build times).
        production: process.env.CI ? false : 'source-map',
      },
      html: {
        // Disable the default viewport meta tag, since Treeherder doesn't work well at
        // small viewport sizes, so shouldn't use `width=device-width` (see bug 1505417).
        meta: false,
      },
      style: {
        // Disable Neutrino's CSS modules support, since we don't use it.
        modules: false,
      },
      targets: {
        browsers: [
          'last 1 Chrome versions',
          'last 1 Edge versions',
          'last 1 Firefox versions',
          'last 1 Safari versions',
        ],
      },
    }],
    ['@neutrinojs/copy', {
      patterns: [
        'ui/contribute.json',
        'ui/revision.txt',
        'ui/robots.txt',
      ],
    }],
    (neutrino) => {
      neutrino.config
        .plugin('provide')
        .use(require.resolve('webpack/lib/ProvidePlugin'), [{
          // Required since AngularJS and jquery.flot don't import jQuery themselves.
          jQuery: 'jquery',
          'window.jQuery': 'jquery',
        }]);

      if (process.env.NODE_ENV === 'production') {
        // Fail the build if these file size thresholds (in bytes) are exceeded,
        // to help prevent unknowingly regressing the bundle size (bug 1384255).
        neutrino.config.performance
          .hints('error')
          .maxAssetSize(1.30 * 1024 * 1024)
          .maxEntrypointSize(1.64 * 1024 * 1024);
      }
    },
  ],
};
<|endoftext|>


<commit_before>define(['jquery', 'backbone', 'underscore', 'favourites/collections/Favourites', 'hbs!favourites/templates/favourites'],
    function($, Backbone, _, Favourites, favouritesTemplate) {
        var FavouritesView = Backbone.View.extend({
            initialize: function() {
                this.collection.on("remove add", this.render, this);
                this.collection.on("change", this.render, this);
            },
            render: function() {
                Backbone.trigger('domchange:title', "Favourites");
                this.$el.html(favouritesTemplate({favourites: this.collection.toJSON()}));
            }
        });
        return FavouritesView;
    }
);
<commit_msg>FavouritesView -> LayoutManager and set EditMode on the fav button<commit_after>define(['jquery', 'backbone', 'underscore', 'favourites/collections/Favourites', 'hbs!favourites/templates/favourites'],
    function($, Backbone, _, Favourites, favouritesTemplate) {
        var FavouritesView = Backbone.View.extend({
            initialize: function(options) {
                options = options || {};
                this.button = options.button;
                this.collection.on("remove add", this.render, this);
                this.collection.on("change", this.render, this);
            },
            manage: true,
            template: favouritesTemplate,
            serialize: function() {
                return {favourites: this.collection.toJSON()};
            },
            afterRender: function() {
                Backbone.trigger('domchange:title', "Favourites");
                this.button.editMode(_.bind(this.editCallback, this), _.bind(this.saveCallback, this));
            },
            editCallback: function() {
                console.log("Edit callback");
            },
            saveCallback: function() {
                console.log("Save callback");
            }
        });
        return FavouritesView;
    }
);
<|endoftext|>


<commit_before>odoo.define("web.ModelFieldSelector", function (require) {
"use strict";

var core = require("web.core");
var Model = require("web.DataModel");
var Widget = require("web.Widget");

/// The ModelFieldSelector widget can be used to select a particular field chain from a given model.
var ModelFieldSelector = Widget.extend({
    template: "FieldSelector",
    events: {
        // Handle popover opening and closing
        "focusin": function () {
            clearTimeout(this._hidePopoverTimeout);
            this.showPopover();
        },
        "focusout": function () {
            this._hidePopoverTimeout = _.defer(this.hidePopover.bind(this));
        },
        "click .o_field_selector_close": "hidePopover",

        // Handle popover field navigation
        "click .o_field_selector_prev_page": "goToPrevPage",
        "click .o_field_selector_next_page": function (e) {
            e.stopPropagation();
            this.goToNextPage(this._getLastPageField($(e.currentTarget).data("name")));
        },
        "click li.o_field_selector_select_button": function (e) {
            this.selectField(this._getLastPageField($(e.currentTarget).data("name")));
        },

        // Handle a direct change in the debug input
        "change input": function() {
            this.setChain(this.$input.val());
            this.validate(true);
            this._prefill().then(this.displayPage.bind(this, ""));
            this.trigger_up("field_chain_changed", {chain: this.chain});
        },

        // Handle keyboard and mouse navigation to build the field chain
        "mouseover li.o_field_selector_item": function (e) {
            this.$("li.o_field_selector_item").removeClass("active");
            $(e.currentTarget).addClass("active");
        },
        "keydown": function (e) {
            if (!this.$popover.is(":visible")) return;
            var inputHasFocus = this.$input.is(":focus");

            switch (e.which) {
                case $.ui.keyCode.UP:
                case $.ui.keyCode.DOWN:
                    e.preventDefault();
                    var $active = this.$("li.o_field_selector_item.active");
                    var $to = $active[e.which === $.ui.keyCode.DOWN ? "next" : "prev"](".o_field_selector_item");
                    if ($to.length) {
                        $active.removeClass("active");
                        $to.addClass("active");
                        this.$popover.focus();

                        var $page = $to.closest(".o_field_selector_page");
                        var full_height = $page.height();
                        var el_position = $to.position().top;
                        var el_height = $to.outerHeight();
                        var current_scroll = $page.scrollTop();
                        if (el_position < 0) {
                            $page.scrollTop(current_scroll - el_height);
                        } else if (full_height < el_position + el_height) {
                            $page.scrollTop(current_scroll + el_height);
                        }
                    }
                    break;
                case $.ui.keyCode.RIGHT:
                    if (inputHasFocus) break;
                    e.preventDefault();
                    var name = this.$("li.o_field_selector_item.active").data("name");
                    if (name) {
                        var field = this._getLastPageField(name);
                        if (field.relation) {
                            this.goToNextPage(field);
                        }
                    }
                    break;
                case $.ui.keyCode.LEFT:
                    if (inputHasFocus) break;
                    e.preventDefault();
                    this.goToPrevPage();
                    break;
                case $.ui.keyCode.ESCAPE:
                    e.stopPropagation();
                    this.hidePopover();
                    break;
                case $.ui.keyCode.ENTER:
                    if (inputHasFocus) break;
                    e.preventDefault();
                    this.selectField(this._getLastPageField(this.$("li.o_field_selector_item.active").data("name")));
                    break;
            }
        },
    },
    /// The ModelFieldSelector requires a model and a initial field chain to work with.
    /// @param model - a string with the model name (e.g. "res.partner")
    /// @param chain - a string with the initial field chain (e.g. "company_id.name")
    /// @param options - an object with several options:
    ///                     - filters: an object which contains a series of filters to use
    ///                     - debugMode: a boolean which is true if the widget is in debug mode
    init: function (parent, model, chain, options) {
        this._super.apply(this, arguments);

        this.model = model;
        this.chain = chain;
        this.options = _.extend({
            filters: {},
            debugMode: false,
        }, options || {});

        this.pages = [];
        this.selectedField = false;
        this.isSelected = true;
        this.dirty = false;

        this.debug = this.options.debugMode;
    },
    willStart: function () {
        return $.when(
            this._super.apply(this, arguments),
            this._prefill()
        );
    },
    start: function () {
        this.$input = this.$("input");
        this.$popover = this.$(".o_field_selector_popover");
        this.displayPage();

        return this._super.apply(this, arguments);
    },
    /// The setChain method saves a new field chain string and displays it in the DOM input element.
    /// @param chain - the new field chain string
    setChain: function (chain) {
        this.chain = chain;
        this.$input.val(this.chain);
    },
    /// The addChainNode method adds a field name to the current field chain.
    /// @param fieldName - the new field name to add at the end of the current field chain
    addChainNode: function (fieldName) {
        this.dirty = true;
        if (this.isSelected) {
            this.removeChainNode();
            this.isSelected = false;
        }
        if (!this.valid) {
            this.setChain("");
            this.validate(true);
        }
        this.setChain((this.chain ? (this.chain + ".") : "") + fieldName);
    },
    /// The removeChainNode method removes the last field name at the end of the current field chain.
    removeChainNode: function () {
        this.dirty = true;
        this.setChain(this.chain.substring(0, this.chain.lastIndexOf(".")));
    },
    /// The validate method toggles the valid status of the widget and display the error message if it
    /// is not valid.
    /// @param valid - a boolean which is true if the widget is valid
    validate: function (valid) {
        this.$(".o_field_selector_warning").toggleClass("hidden", valid);
        this.valid = valid;
    },
    /// The showPopover method shows the popover to select the field chain. It prepares the popover pages
    /// before actually showing it. (if already open, does nothing)
    showPopover: function () {
        if (this._isOpen) return;
        this._isOpen = true;
        this._prefill().then((function () {
            this.displayPage();
            this.$popover.removeClass("hidden");
        }).bind(this));
    },
    /// The hidePopover method closes the popover and mark the field as selected. If the field chain changed,
    /// it notifies its parents. (if not open, does nothing)
    hidePopover: function () {
        if (!this._isOpen) return;
        this._isOpen = false;
        this.$popover.addClass("hidden");
        this.isSelected = true;
        if (this.dirty) {
            this.trigger_up("field_chain_changed", {chain: this.chain});
            this.dirty = false;
        }
    },
    /// The private _prefill method prepares the popover by filling its pages according to the current field chain.
    /// @return a deferred which is resolved once the last page is shown
    _prefill: function () {
        this.pages = [];
        return this._pushPageData(this.model).then((function() {
            return (this.chain ? processChain.call(this, this.chain.split(".").reverse()) : $.when());
        }).bind(this));

        function processChain(chain) {
            var field = this._getLastPageField(chain.pop());
            if (field && field.relation && chain.length > 0) { // Fetch next chain node if any and possible
                return this._pushPageData(field.relation).then(processChain.bind(this, chain));
            } else if (field && chain.length === 0) { // Last node fetched, save it
                this.selectedField = field;
                this.validate(true);
            } else { // Wrong node chain
                this.validate(false);
            }
            return $.when();
        }
    },
    /// The private _pushPageData method gets the field of a particular model and adds them for the new
    /// last popover page.
    /// @param model - the model name whose fields have to be fetched
    /// @return a deferred which is resolved once the fields have been added
    _pushPageData: function (model) {
        return fieldsCache.getFields(model, this.options.filters).then((function (fields) {
            this.pages.push(fields);
        }).bind(this));
    },
    /// The displayPage method shows the last page content of the popover. It also adapts the title according
    /// to the previous page.
    /// @param animation - an optional animation class to add to the page
    displayPage: function (animation) {
        this.$(".o_field_selector_prev_page").toggleClass("hidden", this.pages.length === 1);

        var page = _.last(this.pages);
        var title = "";
        if (this.pages.length > 1) {
            var chainParts = this.chain.split(".");
            var prevField = _.findWhere(this.pages[this.pages.length - 2], {
                name: this.isSelected ? chainParts[chainParts.length - 2] : _.last(chainParts),
            });
            if (prevField) title = prevField.string;
        }
        this.$(".o_field_selector_popover_header .o_field_selector_title").text(title);
        this.$(".o_field_selector_page").replaceWith(core.qweb.render("FieldSelector.page", {lines: page, animation: animation, debug: this.debug}));
    },
    /// The goToPrevPage method removes the last page, adapts the field chain and displays the new last page.
    goToPrevPage: function () {
        if (this.pages.length <= 1) return;
        this.pages.pop();
        this.removeChainNode();
        this.displayPage("o_animate_slide_left");
    },
    /// The goToNextPage method adds a new page to the popover following the given field relation and adapts
    /// the chain node according to this given field.
    /// @param field - the field to add to the chain node
    goToNextPage: function (field) {
        this.addChainNode(field.name);
        this.selectedField = field;
        this._pushPageData(field.relation).then(this.displayPage.bind(this, "o_animate_slide_right"));
    },
    /// The selectField method selects the given field and adapts the chain node according to it. It also closes
    /// the popover and thus notifies the parents about the change.
    /// @param field - the field to select
    selectField: function (field) {
        this.addChainNode(field.name);
        this.selectedField = field;
        this.hidePopover();
    },
    /// The private _getLastPageField search a field in the last page by its name.
    /// @return the field data (an object) found in the last popover page thanks to its name
    _getLastPageField: function (name) {
        return _.findWhere(_.last(this.pages), {
            name: name,
        });
    },
});

/// Field Selector Cache
///
/// * Stores fields per model used in field selector
/// * Apply filters on the fly
var fieldsCache = {
    cache: {},
    cacheDefs: {},
    getFields: function (model, filters) {
        return (this.cacheDefs[model] ? this.cacheDefs[model] : this.updateCache(model)).then((function () {
            return this.filter(model, filters);
        }).bind(this));
    },
    updateCache: function (model) {
        this.cacheDefs[model] = new Model(model).call("fields_get", [
            false,
            ["store", "searchable", "type", "string", "relation", "selection", "related"],
        ]).then((function (fields) {
            var field_data = [];
            _.each(_.sortBy(_.keys(fields), function (f) { return fields[f].string; }), function (key) {
                fields[key]["name"] = key;
                field_data.push(fields[key]);
            });
            this.cache[model] = field_data;
        }).bind(this));
        return this.cacheDefs[model];
    },
    filter: function (model, filters) {
        filters = _.defaults(filters || {}, {searchable: true});
        return _.filter(this.cache[model], function (f) {
            return !filters.searchable || f.searchable;
        });
    },
};

return ModelFieldSelector;
});
<commit_msg>[FIX] web: change selected field value on chain shortening<commit_after>odoo.define("web.ModelFieldSelector", function (require) {
"use strict";

var core = require("web.core");
var Model = require("web.DataModel");
var Widget = require("web.Widget");

/// The ModelFieldSelector widget can be used to select a particular field chain from a given model.
var ModelFieldSelector = Widget.extend({
    template: "FieldSelector",
    events: {
        // Handle popover opening and closing
        "focusin": function () {
            clearTimeout(this._hidePopoverTimeout);
            this.showPopover();
        },
        "focusout": function () {
            this._hidePopoverTimeout = _.defer(this.hidePopover.bind(this));
        },
        "click .o_field_selector_close": "hidePopover",

        // Handle popover field navigation
        "click .o_field_selector_prev_page": "goToPrevPage",
        "click .o_field_selector_next_page": function (e) {
            e.stopPropagation();
            this.goToNextPage(this._getLastPageField($(e.currentTarget).data("name")));
        },
        "click li.o_field_selector_select_button": function (e) {
            this.selectField(this._getLastPageField($(e.currentTarget).data("name")));
        },

        // Handle a direct change in the debug input
        "change input": function() {
            this.setChain(this.$input.val());
            this.validate(true);
            this._prefill().then(this.displayPage.bind(this, ""));
            this.trigger_up("field_chain_changed", {chain: this.chain});
        },

        // Handle keyboard and mouse navigation to build the field chain
        "mouseover li.o_field_selector_item": function (e) {
            this.$("li.o_field_selector_item").removeClass("active");
            $(e.currentTarget).addClass("active");
        },
        "keydown": function (e) {
            if (!this.$popover.is(":visible")) return;
            var inputHasFocus = this.$input.is(":focus");

            switch (e.which) {
                case $.ui.keyCode.UP:
                case $.ui.keyCode.DOWN:
                    e.preventDefault();
                    var $active = this.$("li.o_field_selector_item.active");
                    var $to = $active[e.which === $.ui.keyCode.DOWN ? "next" : "prev"](".o_field_selector_item");
                    if ($to.length) {
                        $active.removeClass("active");
                        $to.addClass("active");
                        this.$popover.focus();

                        var $page = $to.closest(".o_field_selector_page");
                        var full_height = $page.height();
                        var el_position = $to.position().top;
                        var el_height = $to.outerHeight();
                        var current_scroll = $page.scrollTop();
                        if (el_position < 0) {
                            $page.scrollTop(current_scroll - el_height);
                        } else if (full_height < el_position + el_height) {
                            $page.scrollTop(current_scroll + el_height);
                        }
                    }
                    break;
                case $.ui.keyCode.RIGHT:
                    if (inputHasFocus) break;
                    e.preventDefault();
                    var name = this.$("li.o_field_selector_item.active").data("name");
                    if (name) {
                        var field = this._getLastPageField(name);
                        if (field.relation) {
                            this.goToNextPage(field);
                        }
                    }
                    break;
                case $.ui.keyCode.LEFT:
                    if (inputHasFocus) break;
                    e.preventDefault();
                    this.goToPrevPage();
                    break;
                case $.ui.keyCode.ESCAPE:
                    e.stopPropagation();
                    this.hidePopover();
                    break;
                case $.ui.keyCode.ENTER:
                    if (inputHasFocus) break;
                    e.preventDefault();
                    this.selectField(this._getLastPageField(this.$("li.o_field_selector_item.active").data("name")));
                    break;
            }
        },
    },
    /// The ModelFieldSelector requires a model and a initial field chain to work with.
    /// @param model - a string with the model name (e.g. "res.partner")
    /// @param chain - a string with the initial field chain (e.g. "company_id.name")
    /// @param options - an object with several options:
    ///                     - filters: an object which contains a series of filters to use
    ///                     - debugMode: a boolean which is true if the widget is in debug mode
    init: function (parent, model, chain, options) {
        this._super.apply(this, arguments);

        this.model = model;
        this.chain = chain;
        this.options = _.extend({
            filters: {},
            debugMode: false,
        }, options || {});

        this.pages = [];
        this.selectedField = false;
        this.isSelected = true;
        this.dirty = false;

        this.debug = this.options.debugMode;
    },
    willStart: function () {
        return $.when(
            this._super.apply(this, arguments),
            this._prefill()
        );
    },
    start: function () {
        this.$input = this.$("input");
        this.$popover = this.$(".o_field_selector_popover");
        this.displayPage();

        return this._super.apply(this, arguments);
    },
    /// The setChain method saves a new field chain string and displays it in the DOM input element.
    /// @param chain - the new field chain string
    setChain: function (chain) {
        this.chain = chain;
        this.$input.val(this.chain);
    },
    /// The addChainNode method adds a field name to the current field chain.
    /// @param fieldName - the new field name to add at the end of the current field chain
    addChainNode: function (fieldName) {
        this.dirty = true;
        if (this.isSelected) {
            this.removeChainNode();
            this.isSelected = false;
        }
        if (!this.valid) {
            this.setChain("");
            this.validate(true);
        }
        this.setChain((this.chain ? (this.chain + ".") : "") + fieldName);
    },
    /// The removeChainNode method removes the last field name at the end of the current field chain.
    removeChainNode: function () {
        this.dirty = true;
        this.setChain(this.chain.substring(0, this.chain.lastIndexOf(".")));
    },
    /// The validate method toggles the valid status of the widget and display the error message if it
    /// is not valid.
    /// @param valid - a boolean which is true if the widget is valid
    validate: function (valid) {
        this.$(".o_field_selector_warning").toggleClass("hidden", valid);
        this.valid = valid;
    },
    /// The showPopover method shows the popover to select the field chain. It prepares the popover pages
    /// before actually showing it. (if already open, does nothing)
    showPopover: function () {
        if (this._isOpen) return;
        this._isOpen = true;
        this._prefill().then((function () {
            this.displayPage();
            this.$popover.removeClass("hidden");
        }).bind(this));
    },
    /// The hidePopover method closes the popover and mark the field as selected. If the field chain changed,
    /// it notifies its parents. (if not open, does nothing)
    hidePopover: function () {
        if (!this._isOpen) return;
        this._isOpen = false;
        this.$popover.addClass("hidden");
        this.isSelected = true;
        if (this.dirty) {
            this.trigger_up("field_chain_changed", {chain: this.chain});
            this.dirty = false;
        }
    },
    /// The private _prefill method prepares the popover by filling its pages according to the current field chain.
    /// @return a deferred which is resolved once the last page is shown
    _prefill: function () {
        this.pages = [];
        return this._pushPageData(this.model).then((function() {
            return (this.chain ? processChain.call(this, this.chain.split(".").reverse()) : $.when());
        }).bind(this));

        function processChain(chain) {
            var field = this._getLastPageField(chain.pop());
            if (field && field.relation && chain.length > 0) { // Fetch next chain node if any and possible
                return this._pushPageData(field.relation).then(processChain.bind(this, chain));
            } else if (field && chain.length === 0) { // Last node fetched, save it
                this.selectedField = field;
                this.validate(true);
            } else { // Wrong node chain
                this.validate(false);
            }
            return $.when();
        }
    },
    /// The private _pushPageData method gets the field of a particular model and adds them for the new
    /// last popover page.
    /// @param model - the model name whose fields have to be fetched
    /// @return a deferred which is resolved once the fields have been added
    _pushPageData: function (model) {
        return fieldsCache.getFields(model, this.options.filters).then((function (fields) {
            this.pages.push(fields);
        }).bind(this));
    },
    /// The displayPage method shows the last page content of the popover. It also adapts the title according
    /// to the previous page.
    /// @param animation - an optional animation class to add to the page
    displayPage: function (animation) {
        this.$(".o_field_selector_prev_page").toggleClass("hidden", this.pages.length === 1);

        var page = _.last(this.pages);
        var title = "";
        if (this.pages.length > 1) {
            var chainParts = this.chain.split(".");
            var prevField = _.findWhere(this.pages[this.pages.length - 2], {
                name: this.isSelected ? chainParts[chainParts.length - 2] : _.last(chainParts),
            });
            if (prevField) title = prevField.string;
        }
        this.$(".o_field_selector_popover_header .o_field_selector_title").text(title);
        this.$(".o_field_selector_page").replaceWith(core.qweb.render("FieldSelector.page", {lines: page, animation: animation, debug: this.debug}));
    },
    /// The goToPrevPage method removes the last page, adapts the field chain and displays the new last page.
    goToPrevPage: function () {
        if (this.pages.length <= 1) return;
        this.pages.pop();
        this.removeChainNode();
        this.selectedField = this._getLastPageField(_.last(this.chain.split(".")));
        this.displayPage("o_animate_slide_left");
    },
    /// The goToNextPage method adds a new page to the popover following the given field relation and adapts
    /// the chain node according to this given field.
    /// @param field - the field to add to the chain node
    goToNextPage: function (field) {
        this.addChainNode(field.name);
        this.selectedField = field;
        this._pushPageData(field.relation).then(this.displayPage.bind(this, "o_animate_slide_right"));
    },
    /// The selectField method selects the given field and adapts the chain node according to it. It also closes
    /// the popover and thus notifies the parents about the change.
    /// @param field - the field to select
    selectField: function (field) {
        this.addChainNode(field.name);
        this.selectedField = field;
        this.hidePopover();
    },
    /// The private _getLastPageField search a field in the last page by its name.
    /// @return the field data (an object) found in the last popover page thanks to its name
    _getLastPageField: function (name) {
        return _.findWhere(_.last(this.pages), {
            name: name,
        });
    },
});

/// Field Selector Cache
///
/// * Stores fields per model used in field selector
/// * Apply filters on the fly
var fieldsCache = {
    cache: {},
    cacheDefs: {},
    getFields: function (model, filters) {
        return (this.cacheDefs[model] ? this.cacheDefs[model] : this.updateCache(model)).then((function () {
            return this.filter(model, filters);
        }).bind(this));
    },
    updateCache: function (model) {
        this.cacheDefs[model] = new Model(model).call("fields_get", [
            false,
            ["store", "searchable", "type", "string", "relation", "selection", "related"],
        ]).then((function (fields) {
            var field_data = [];
            _.each(_.sortBy(_.keys(fields), function (f) { return fields[f].string; }), function (key) {
                fields[key]["name"] = key;
                field_data.push(fields[key]);
            });
            this.cache[model] = field_data;
        }).bind(this));
        return this.cacheDefs[model];
    },
    filter: function (model, filters) {
        filters = _.defaults(filters || {}, {searchable: true});
        return _.filter(this.cache[model], function (f) {
            return !filters.searchable || f.searchable;
        });
    },
};

return ModelFieldSelector;
});
<|endoftext|>


<commit_before>
(function($) {

    window.console = (window.console || { log: function() {}, error: function() {}});

    var app = window.pig = {
        cookieName: "piggy_token",

        $messageNode: null,

        init: function(page, options) {
            options = (options || {});

            console.log("Starting piggy client on '" + page + "' page", options);

            app.$messageNode = $("aside.messages");

            if (app[page] && app[page].init && app[page].init.apply) {
                app[page].init.apply(app[page], [options]);
            }

        },

        main: {
            init: function() {
                $("form[action=\\/transaction\\/add]").on("submit", this.handleAddTransaction);
            },

            handleAddTransaction: function(e) {
                var form = $(e.currentTarget),
                    data = {};

                e.preventDefault();

                app.alerts.clearAll();

                form.serializeArray().forEach(function(item) {
                    data[item.name] = item.value;
                });

                if (!Number(data.amount)) {
                    app.alerts.error("Please enter an amount (greater than zero)");
                    form.find(".ui-submit").removeClass("ui-btn-active");
                    return false;
                }
                data.amount = Number(data.amount);

                // if an expense, flip the amount to negative
                if (form.find("[name=amount]").hasClass("expense") && data.amount > 0) {
                    data.amount *= -1;
                }

                app.trans.add(data, {
                    success: function(trans) {
                        console.log(trans);
                        app.alerts.success("Transaction added successfully!");
                        form.find("[name=description]").val("");
                        form.find("[name=amount]").val("").focus();
                        form.find(".ui-submit").removeClass("ui-btn-active");
                    },
                    error: function(err) {
                        app.alerts.error(err);
                        form.find(".ui-submit").removeClass("ui-btn-active");
                    }
                });

                return false;
            }

        },

        login: {
            $account: null,
            $lock: null,

            init: function() {
                $("form[action=\\/account\\/login]").on("submit", function(e) {
                    console.error("Login form submission (should not occur)");
                    e.preventDefault();
                    return false;
                });

                app.login.$account = $("[name=account]");
                app.login.$lock = $(".pattern-lock");

                if (localStorage.account && !app.login.$account.val().length) {
                    app.login.$account.val(localStorage.account).blur();
                }

                app.login.$lock.patternInput({
                    onFinish: this.handleLoginPattern
                });
            },

            handleLoginPattern: function(pattern) {
                app.alerts.clearAll();

                app.login.$account.attr("disabled", "disabled");
                app.login.$lock.hide();

                app.alerts.info("Working...");

                $.ajax({
                    url: "/account/login",
                    type: "post",
                    data: {
                        owner: app.login.$account.val(),
                        pattern: pattern.join("")
                    },
                    dataType: "json",
                    success: app.ajaxSuccess(app.login.success, app.login.error),
                    error: app.ajaxError(app.login.error)
                });
            },

            success: function() {
                localStorage.account = app.login.$account.val();
                window.location.replace($("[name=location]").val());
            },

            error: function(err) {
                app.alerts.clearAll();
                app.alerts.error(err);
                app.login.$lock.show();
                app.login.$account.attr("disabled", "");
            }
        },

        trans: {
            add: function(data, cb) {
                var cat,
                    valid = true;

                console.log("adding transaction with", data);

                cb = (cb || {});

                if (!data.amount || !Number(data.amount)) {
                    app.alerts.error("Please enter a number for the amount");
                    valid = false;
                }
                if (!data.date || !Date(data.date)) {
                    app.alerts.error("Please enter a valid date");
                    valid = false;
                }
                cat = Number(data.category);
                if (!cat && cat !== 0) {
                    app.alerts.error("Please select a valid category");
                    valid = false;
                }

                if (!valid) { return; }

                $.ajax({
                    url: "/transaction/add",
                    type: "post",
                    data: data,
                    dataType: "json",
                    success: app.ajaxSuccess(cb.success, cb.error),
                    error: app.ajaxError(cb.error)
                });
            }
        },

        ajaxSuccess: function(success, error) {
            success = (success || function() {});
            error = (error || function() {});
            return function(data) {
                data = (data || {});
                if (data.error) {
                    (app.ajaxError(error))({
                        responseText: JSON.stringify(data),
                        status: data.status
                    });
                } else {
                    success(data);
                }
            };
        },

        ajaxError: function(handler) {
            handler = (handler || function() {});
            return function(xhr) {
                var errJSON;
                try {
                    errJSON = JSON.parse(xhr.responseText);
                } catch(err) {
                    errJSON = {
                        error: xhr.responseText,
                        status: xhr.status
                    };
                }

                if (errJSON.status === 404) {
                    console.error("Unable to find endpoint (404)", errJSON.error);
                    handler("Sorry, but I couldn't complete that request");
                } else if (errJSON.status && errJSON.status < 500) {
                    handler(errJSON.error);
                } else {
                    console.error("Server error: ", errJSON.error);
                    handler("Sorry, but I couldn't complete that request");
                }
            };
        },

        alerts: {
            error: function(msg) { this.add("error", msg); },
            warn: function(msg) { this.add("warn", msg); },
            info: function(msg) { this.add("info", msg); },
            success: function(msg) { this.add("success", msg); },

            add: function(cls, msg) {
                app.$messageNode.append("
" + msg + "
");
            },

            clearAll: function() {
                app.$messageNode.get(0).innerHTML = "";
            }
        }

    };

})(window.jQuery);<commit_msg>Fixed login redirect<commit_after>
(function($) {

    window.console = (window.console || { log: function() {}, error: function() {}});

    var app = window.pig = {
        cookieName: "piggy_token",

        $messageNode: null,

        init: function(page, options) {
            options = (options || {});

            console.log("Starting piggy client on '" + page + "' page", options);

            app.$messageNode = $("aside.messages");

            if (app[page] && app[page].init && app[page].init.apply) {
                app[page].init.apply(app[page], [options]);
            }

        },

        main: {
            init: function() {
                $("form[action=\\/transaction\\/add]").on("submit", this.handleAddTransaction);
            },

            handleAddTransaction: function(e) {
                var form = $(e.currentTarget),
                    data = {};

                e.preventDefault();

                app.alerts.clearAll();

                form.serializeArray().forEach(function(item) {
                    data[item.name] = item.value;
                });

                if (!Number(data.amount)) {
                    app.alerts.error("Please enter an amount (greater than zero)");
                    form.find(".ui-submit").removeClass("ui-btn-active");
                    return false;
                }
                data.amount = Number(data.amount);

                // if an expense, flip the amount to negative
                if (form.find("[name=amount]").hasClass("expense") && data.amount > 0) {
                    data.amount *= -1;
                }

                app.trans.add(data, {
                    success: function(trans) {
                        console.log(trans);
                        app.alerts.success("Transaction added successfully!");
                        form.find("[name=description]").val("");
                        form.find("[name=amount]").val("").focus();
                        form.find(".ui-submit").removeClass("ui-btn-active");
                    },
                    error: function(err) {
                        app.alerts.error(err);
                        form.find(".ui-submit").removeClass("ui-btn-active");
                    }
                });

                return false;
            }

        },

        login: {
            $account: null,
            $lock: null,

            init: function() {
                var loc;

                $("form[action=\\/account\\/login]").on("submit", function(e) {
                    console.error("Login form submission (should not occur)");
                    e.preventDefault();
                    return false;
                });

                app.login.$account = $("[name=account]");
                app.login.$lock = $(".pattern-lock");

                if (window.location.hash) {
                    loc = $("[name=location]");
                    loc.val(loc.val() + window.location.hash);
                }

                if (localStorage.account && !app.login.$account.val().length) {
                    app.login.$account.val(localStorage.account).blur();
                }

                app.login.$lock.patternInput({
                    onFinish: this.handleLoginPattern
                });
            },

            handleLoginPattern: function(pattern) {
                app.alerts.clearAll();

                app.login.$account.attr("disabled", "disabled");
                app.login.$lock.hide();

                app.alerts.info("Working...");

                $.ajax({
                    url: "/account/login",
                    type: "post",
                    data: {
                        owner: app.login.$account.val(),
                        pattern: pattern.join("")
                    },
                    dataType: "json",
                    success: app.ajaxSuccess(app.login.success, app.login.error),
                    error: app.ajaxError(app.login.error)
                });
            },

            success: function() {
                localStorage.account = app.login.$account.val();
                window.location.replace($("[name=location]").val());
            },

            error: function(err) {
                app.alerts.clearAll();
                app.alerts.error(err);
                app.login.$lock.show();
                app.login.$account.attr("disabled", "");
            }
        },

        trans: {
            add: function(data, cb) {
                var cat,
                    valid = true;

                console.log("adding transaction with", data);

                cb = (cb || {});

                if (!data.amount || !Number(data.amount)) {
                    app.alerts.error("Please enter a number for the amount");
                    valid = false;
                }
                if (!data.date || !Date(data.date)) {
                    app.alerts.error("Please enter a valid date");
                    valid = false;
                }
                cat = Number(data.category);
                if (!cat && cat !== 0) {
                    app.alerts.error("Please select a valid category");
                    valid = false;
                }

                if (!valid) { return; }

                $.ajax({
                    url: "/transaction/add",
                    type: "post",
                    data: data,
                    dataType: "json",
                    success: app.ajaxSuccess(cb.success, cb.error),
                    error: app.ajaxError(cb.error)
                });
            }
        },

        ajaxSuccess: function(success, error) {
            success = (success || function() {});
            error = (error || function() {});
            return function(data) {
                data = (data || {});
                if (data.error) {
                    (app.ajaxError(error))({
                        responseText: JSON.stringify(data),
                        status: data.status
                    });
                } else {
                    success(data);
                }
            };
        },

        ajaxError: function(handler) {
            handler = (handler || function() {});
            return function(xhr) {
                var errJSON;
                try {
                    errJSON = JSON.parse(xhr.responseText);
                } catch(err) {
                    errJSON = {
                        error: xhr.responseText,
                        status: xhr.status
                    };
                }

                if (errJSON.status === 404) {
                    console.error("Unable to find endpoint (404)", errJSON.error);
                    handler("Sorry, but I couldn't complete that request");
                } else if (errJSON.status && errJSON.status < 500) {
                    handler(errJSON.error);
                } else {
                    console.error("Server error: ", errJSON.error);
                    handler("Sorry, but I couldn't complete that request");
                }
            };
        },

        alerts: {
            error: function(msg) { this.add("error", msg); },
            warn: function(msg) { this.add("warn", msg); },
            info: function(msg) { this.add("info", msg); },
            success: function(msg) { this.add("success", msg); },

            add: function(cls, msg) {
                app.$messageNode.append("
" + msg + "
");
            },

            clearAll: function() {
                app.$messageNode.get(0).innerHTML = "";
            }
        }

    };

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