<commit_before>const express = require('express');
const sendErrorResponse = require('./utils').sendErrorResponse;
const replaceId = require('./utils').replaceId;
const ObjectID = require('mongodb').ObjectID;
const indicative = require('indicative');
const util = require('util');

const router = express.Router();

// Posts API Feature
router.get('/', (req, res) => {
    req.app.locals.db.collection('posts').find().toArray().then(posts => {
        res.json(posts.map(p => replaceId(p)));
    });
});
router.get('/:id', (req, res) => {
    const post = req.app.locals.db.collection('posts').findOne(new ObjectID(req.params.id)).then(post => {
        if (!post) {
            sendErrorResponse(req, res, 404, `Post with ID=${+req.params.id} does not exist`);
        }
        res.json(post);
    });
});

router.post('/', function (req, res) {
    const post = req.body;
    indicative.validator.validate(post, {
        // id: 'required|regex:^[0-9a-f]{24}',
        title: 'required|string|min:2|max:60',
        subtitle: 'string|max:255',
        description: 'string',
    }).then(() => {
        req.app.locals.db.collection('posts').insertOne(post).then(r => {
            if (r.result.ok && r.insertedCount === 1) {
                delete  post._id;
                post.id = r.insertedId;
                console.log(`Created post: ${post}`);
                res.status(201).location(`/posts/${post.id}`).json(post);
            } else {
                sendErrorResponse(req, res, 500, `Server error: ${err.message}`, err);
            }
        }).catch(err => {
            console.log("Error: Update unsuccessfull.");
            sendErrorResponse(req, res, 500, `Server error: ${err.message}`, err);
        });
    }).catch(errors => {
        sendErrorResponse(req, res, 400, `Invalid user data: ${util.inspect(errors)}`);
    });
});

router.put('/:id', (req, res) => {
    const old = posts.get(+req.params.id);
    if (!old) {
        sendErrorResponse(req, res, 404, `Post with ID=${+req.params.id} does not exist`);
    }
    const post = req.body;
    if (old.id !== post.id) {
        sendErrorResponse(req, res, 400, `Post ID=${post.id} does not match URL ID=${+req.params.id}`);
    }

    posts.set(post.id, post);
    console.log(`Updated post: ${post}`);
    res.json(post);
});

router.delete('/:id', (req, res) => {
    const old = posts.get(+req.params.id);
    if (!old) {
        sendErrorResponse(req, res, 404, `Post with ID=${+req.params.id} does not exist`);
    }

    posts.delete(+req.params.id);
    console.log(`Deleted post: ${old}`);
    res.json(old);
});

module.exports = router;<commit_msg>CRUD for posts completed<commit_after>const express = require('express');
const sendErrorResponse = require('./utils').sendErrorResponse;
const replaceId = require('./utils').replaceId;
const ObjectID = require('mongodb').ObjectID;
const indicative = require('indicative');
const util = require('util');

const router = express.Router();

// Posts API Feature
router.get('/', async(req, res) => {
    try{
        const posts = await req.app.locals.db.collection('posts').find().toArray();
        res.json(posts.map(p => replaceId(p)));
    } catch(err) {
        sendErrorResponse(req, res, 500, `Server error: ${err.message}`, err);
    }
});
router.get('/:id', async (req, res) => {
    const params = req.params;
    try {
        await indicative.validator.validate(params, { id: 'required|regex:^[0-9a-f]{24}$' });
        const post = await req.app.locals.db.collection('posts').findOne({ _id: new ObjectID(req.params.id) });
        if (!post) {
            sendErrorResponse(req, res, 404, `Post with ID=${req.params.id} does not exist`);
            return;
        }
        res.json(post);
    } catch (errors) {
        sendErrorResponse(req, res, 400, `Invalid user data: ${util.inspect(errors)}`);
    }
});

router.post('/', function(req, res) {
    const post = req.body;
    indicative.validator.validate(post, {
        // id: 'required|regex:^[0-9a-f]{24}',
        title: 'required|string|min:2|max:60',
        subtitle: 'string|max:255',
        content: 'string',
    }).then(() => {
        req.app.locals.db.collection('posts').insertOne(post).then(r => {
            if (r.result.ok && r.insertedCount === 1) {
                delete post._id;
                post.id = r.insertedId;
                console.log(`Created post: ${post}`);
                res.status(201).location(`/posts/${post.id}`).json(post);
            } else {
                sendErrorResponse(req, res, 500, `Server error: ${err.message}`, err);
            }
        }).catch(err => {
            console.log("Error: Update unsuccessfull.");
            sendErrorResponse(req, res, 500, `Server error: ${err.message}`, err);
        });
    }).catch(errors => {
        sendErrorResponse(req, res, 400, `Invalid user data: ${util.inspect(errors)}`);
    });
});

router.put('/:id', async (req, res) => {
    const old = await req.app.locals.db.collection('posts').findOne({ _id: new ObjectID(req.params.id) });
    if (!old) {
        sendErrorResponse(req, res, 404, `Post with ID=${req.params.id} does not exist`);
        return;
    }
    const post = req.body;
    if (old._id.toString() !== post.id) {
        sendErrorResponse(req, res, 400, `Post ID=${post.id} does not match URL ID=${req.params.id}`);
        return;
    }
    try {
        await indicative.validator.validate(post, {
            id: 'required|regex:^[0-9a-f]{24}',
            title: 'required|string|min:2|max:60',
            subtitle: 'string|max:255',
            content: 'string',
        });
        try {
            r = await req.app.locals.db.collection('posts').updateOne({ _id: new ObjectID(req.params.id) }, { $set: post });
            if (r.result.ok) {
                delete post._id;
                console.log(`Updated post: ${JSON.stringify(post)}`);
                if (r.modifiedCount === 0) {
                    console.log(`The old and the new posts are the same.`);
                }
                res.json(post);
            } else {
                sendErrorResponse(req, res, 500, `Unable to update post: ${post.title}`);
            }
        } catch (err) {
            console.log("Error: Update unsuccessfull.");
            sendErrorResponse(req, res, 500, `Server error: ${err.message}`, err);
        }
    } catch (errors) {
        sendErrorResponse(req, res, 400, `Invalid user data: ${util.inspect(errors)}`);
    }
});

router.delete('/:id', async (req, res) => {
    const params = req.params;
    try {
        await indicative.validator.validate(params, { id: 'required|regex:^[0-9a-f]{24}$' });
        const old = await req.app.locals.db.collection('posts').findOne({ _id: new ObjectID(req.params.id) });
        if (!old) {
            sendErrorResponse(req, res, 404, `Post with ID=${req.params.id} does not exist`);
            return;
        }
        replaceId(old);
        const r = await req.app.locals.db.collection('posts').deleteOne({ _id: new ObjectID(req.params.id) });
        if(r.result.ok && r.deletedCount === 1) {
            res.json(old);
        } else {
            sendErrorResponse(req, res, 500, `Unable to delete post: ${old.id}: ${old.title}`);
        }
    } catch (errors) {
        sendErrorResponse(req, res, 400, `Invalid user data: ${util.inspect(errors)}`);
    }
});

module.exports = router;<|endoftext|>


<commit_before>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const { registerSuite } = intern.getInterface('object');
const assert = intern.getPlugin('chai').assert;
const cp = require('child_process');
var suite = {
  tests: {}
};

// This test cannot be run remotely like the other tests in tests/server. So,
// if production, just skip these tests (register a suite with no tests).
if (intern._config.fxaProduction) {
  registerSuite(suite);
  return;
}

function spawnServer (cb) {

  var proc = cp.spawn('./scripts/run_locally.js', [], {
    env: {
      HTTP_PORT: '3090',
      I18N_SUPPORTED_LANGUAGES: 'en,blah',
      PATH: process.env.PATH,
      PORT: '3040'
    }
  });

  var errData = '';

  proc.stderr.setEncoding('utf8');
  proc.stderr.on('data', function (data) {
    errData += data;
  });

  proc.on('error', function (err) {
    cb(err);
  });

  proc.on('exit', function (/*code*/) {
    cb(null, errData);
  });

}

suite.tests['#test incompatible locale lists 2'] = function () {
  var dfd = this.async(10000);

  spawnServer(dfd.callback(function (err, data) {
    assert.isNull(err);
    assert.ok(data.indexOf('Configuration error: (blah) is missing from the default list of supportedLanguages') >= 0, data);

    dfd.resolve();
  }, dfd.reject.bind(dfd)));

  return dfd;
};

registerSuite('configuration', suite);
<commit_msg>fix(tests): fix up server test register function<commit_after>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const { registerSuite } = intern.getInterface('object');
const assert = intern.getPlugin('chai').assert;
const cp = require('child_process');
var suite = {
  tests: {}
};

// This test cannot be run remotely like the other tests in tests/server. So,
// if production, just skip these tests (register a suite with no tests).
if (intern._config.fxaProduction) {
  registerSuite('configuration', suite);
  return;
}

function spawnServer (cb) {

  var proc = cp.spawn('./scripts/run_locally.js', [], {
    env: {
      HTTP_PORT: '3090',
      I18N_SUPPORTED_LANGUAGES: 'en,blah',
      PATH: process.env.PATH,
      PORT: '3040'
    }
  });

  var errData = '';

  proc.stderr.setEncoding('utf8');
  proc.stderr.on('data', function (data) {
    errData += data;
  });

  proc.on('error', function (err) {
    cb(err);
  });

  proc.on('exit', function (/*code*/) {
    cb(null, errData);
  });

}

suite.tests['#test incompatible locale lists 2'] = function () {
  var dfd = this.async(10000);

  spawnServer(dfd.callback(function (err, data) {
    assert.isNull(err);
    assert.ok(data.indexOf('Configuration error: (blah) is missing from the default list of supportedLanguages') >= 0, data);

    dfd.resolve();
  }, dfd.reject.bind(dfd)));

  return dfd;
};

registerSuite('configuration', suite);
<|endoftext|>


<commit_before>function loadJSON(filename, callback) {
    // https://codepen.io/KryptoniteDove/post/load-json-file-locally-using-pure-javascript

    var xobj = new XMLHttpRequest();
    xobj.overrideMimeType("application/json");
    xobj.onreadystatechange = function() {
        if(4 == xobj.readyState && 200 == xobj.status) {
            callback(JSON.parse(xobj.response));
        }
    }
    xobj.open("GET", filename, true);
    xobj.send();
}

const CurrentGameDataVersion = 1;

var MessageType = Object.freeze({
    None: 0,
    StartGame: 1,
    ConfirmStaging: 2,
    SetTeam: 3,
    SetSlot: 4,
    ClearSlot: 5
});

var templateScript;

var airconsole;
var viewManager;

var isMasterPlayer = false;
var isConfirmed = false;
var playerName;
var playerTeamName;

function App() {

    app = this;

    // compile content template and set it with the initial values
    // so that the airconsole scripts don't choke on missing ids
    var template = $("#content-template").html();
    app.templateScript = Handlebars.compile(template);

    // init AirConsole
    app.airconsole = new AirConsole({"orientation": "landscape"});

    app.airconsole.onReady = function(code) {

        app.debugLog("onReady", code);

        app.playerName = app.airconsole.getNickname();
        app.updateContent();

        app.viewManager = new AirConsoleViewManager(app.airconsole);

        // load game data
        app.debugLog("Requesting game data...");
        var gameData = {};
        loadJSON("/data/GameData.json", function(response) {
            if(!response.version || CurrentGameDataVersion != response.version) {
                app.debugLog("Invalid game data version. Got " + response.version + ", expected " + CurrentGameDataVersion);
                return;
            }

            gameData = response;
            app.debugLog("Received game data version " + gameData.version);
        });
    }

    app.airconsole.onMessage = function(from, data) {

        app.debugLog("onMessage", from, data);

        var messageType = data.type;
        switch(messageType) {
            case MessageType.SetTeam:
                app.playerTeamName = data.teamName;
                app.updateContent();
                app.airconsole.setCustomDeviceStateProperty("teamData", data);
                break;
            case MessageType.SetSlot:
app.debugLog("TODO: handle set slot message");
                break;
            case MessageType.ClearSlot:
app.debugLog("TODO: handle set slot message");
                break;
            default:
                alert("Invalid message type: " + messageType);
                break;
        }
    };

    app.airconsole.onCustomDeviceStateChange = function(deviceId, data) {

        app.debugLog("onCustomDeviceStateChange", deviceId, data);
        app.checkForMasterPlayer(data);

        app.viewManager.onViewChange(data, function(viewId) {
            app.debugLog("onViewChange", viewId);
        });
    };
}

App.prototype.debugLog = function() {

    var args = Array.from(arguments);
    if(app.airconsole) {
        args.unshift(app.airconsole.getDeviceId());
    }
    return window.console && console.log && Function.apply.call(console.log, console, args);
}

App.prototype.updateContent = function() {

    app.debugLog("Updating content...");
    var compiledHtml = app.templateScript({
        "playerName": app.playerName,
        "playerTeamName": app.playerTeamName,
        "isMasterPlayer": app.isMasterPlayer
    });
    $("#content-placeholder").html(compiledHtml);

    // control hookups
    if(app.isMasterPlayer) {
        var buttonStart = new Button("button-start", {
            "down": function() {
                app.startGame();
            }
        });
    }

    var buttonConfirm = new Button("button-confirm", {
        "down": function() {
            app.confirmStaging();
        }
    });

    // view manager must reset views
    if(app.viewManager) {
        app.viewManager.setupViews();
    }
}

App.prototype.checkForMasterPlayer = function(data) {

    if(!data.masterPlayer) {
        return;
    }

    var wasMasterPlayer = app.isMasterPlayer;
    app.isMasterPlayer = data.masterPlayer === app.airconsole.getDeviceId();
    if(wasMasterPlayer != isMasterPlayer) {
        app.updateContent();
    }
}

App.prototype.startGame = function(msg) {

    app.debugLog("Starting game...");
    app.sendMessageToScreen({
        "type": MessageType.StartGame
    });
}

App.prototype.confirmStaging = function(msg) {

    isConfirmed = !isConfirmed;

    app.debugLog("Confirming staging: ", isConfirmed);
    app.sendMessageToScreen({
        "type": MessageType.ConfirmStaging,
        "isConfirmed": isConfirmed
    });
    $("#button-confirm-text").html(isConfirmed ? "Unconfirm" : "Confirm");
}

App.prototype.sendMessageToScreen = function(msg) {

    app.airconsole.message(AirConsole.SCREEN, msg);
}

App.prototype.broadcastMessage = function(msg) {

    app.airconsole.broadcast(msg);
}
<commit_msg>Small bugfix<commit_after>function loadJSON(filename, callback) {
    // https://codepen.io/KryptoniteDove/post/load-json-file-locally-using-pure-javascript

    var xobj = new XMLHttpRequest();
    xobj.overrideMimeType("application/json");
    xobj.onreadystatechange = function() {
        if(4 == xobj.readyState && 200 == xobj.status) {
            callback(JSON.parse(xobj.response));
        }
    }
    xobj.open("GET", filename, true);
    xobj.send();
}

const CurrentGameDataVersion = 1;

var MessageType = Object.freeze({
    None: 0,
    StartGame: 1,
    ConfirmStaging: 2,
    SetTeam: 3,
    SetSlot: 4,
    ClearSlot: 5
});

var templateScript;

var airconsole;
var viewManager;

var isMasterPlayer = false;
var isConfirmed = false;
var playerName;
var playerTeamName;

function App() {

    app = this;

    // compile content template and set it with the initial values
    // so that the airconsole scripts don't choke on missing ids
    var template = $("#content-template").html();
    app.templateScript = Handlebars.compile(template);

    // init AirConsole
    app.airconsole = new AirConsole({"orientation": "landscape"});

    app.airconsole.onReady = function(code) {

        app.debugLog("onReady", code);

        app.playerName = app.airconsole.getNickname();
        app.updateContent();

        app.viewManager = new AirConsoleViewManager(app.airconsole);

        // load game data
        app.debugLog("Requesting game data...");
        var gameData = {};
        loadJSON("/data/GameData.json", function(response) {
            if(!response.version || CurrentGameDataVersion != response.version) {
                app.debugLog("Invalid game data version. Got " + response.version + ", expected " + CurrentGameDataVersion);
                return;
            }

            gameData = response;
            app.debugLog("Received game data version " + gameData.version);
        });
    }

    app.airconsole.onMessage = function(from, data) {

        app.debugLog("onMessage", from, data);

        var messageType = data.type;
        switch(messageType) {
            case MessageType.SetTeam:
                app.playerTeamName = data.teamName;
                app.updateContent();
                app.airconsole.setCustomDeviceStateProperty("teamData", data);
                break;
            case MessageType.SetSlot:
app.debugLog("TODO: handle set slot message");
                break;
            case MessageType.ClearSlot:
app.debugLog("TODO: handle set slot message");
                break;
            default:
                alert("Invalid message type: " + messageType);
                break;
        }
    };

    app.airconsole.onCustomDeviceStateChange = function(deviceId, data) {

        app.debugLog("onCustomDeviceStateChange", deviceId, data);
        app.checkForMasterPlayer(data);

        app.viewManager.onViewChange(data, function(viewId) {
            app.debugLog("onViewChange", viewId);
        });
    };
}

App.prototype.debugLog = function() {

    var args = Array.from(arguments);
    if(app.airconsole) {
        args.unshift(app.airconsole.getDeviceId());
    }
    return window.console && console.log && Function.apply.call(console.log, console, args);
}

App.prototype.updateContent = function() {

    app.debugLog("Updating content...");
    var compiledHtml = app.templateScript({
        "playerName": app.playerName,
        "playerTeamName": app.playerTeamName,
        "isMasterPlayer": app.isMasterPlayer
    });
    $("#content-placeholder").html(compiledHtml);

    // control hookups
    if(app.isMasterPlayer) {
        var buttonStart = new Button("button-start", {
            "down": function() {
                app.startGame();
            }
        });
    }

    var buttonConfirm = new Button("button-confirm", {
        "down": function() {
            app.confirmStaging();
        }
    });

    // view manager must reset views
    if(app.viewManager) {
        app.viewManager.setupViews();
    }
}

App.prototype.checkForMasterPlayer = function(data) {

    if(!data.masterPlayer) {
        return;
    }

    var wasMasterPlayer = app.isMasterPlayer;
    app.isMasterPlayer = data.masterPlayer === app.airconsole.getDeviceId();
    if(wasMasterPlayer != app.isMasterPlayer) {
        app.updateContent();
    }
}

App.prototype.startGame = function(msg) {

    app.debugLog("Starting game...");
    app.sendMessageToScreen({
        "type": MessageType.StartGame
    });
}

App.prototype.confirmStaging = function(msg) {

    isConfirmed = !isConfirmed;

    app.debugLog("Confirming staging: ", isConfirmed);
    app.sendMessageToScreen({
        "type": MessageType.ConfirmStaging,
        "isConfirmed": isConfirmed
    });
    $("#button-confirm-text").html(isConfirmed ? "Unconfirm" : "Confirm");
}

App.prototype.sendMessageToScreen = function(msg) {

    app.airconsole.message(AirConsole.SCREEN, msg);
}

App.prototype.broadcastMessage = function(msg) {

    app.airconsole.broadcast(msg);
}
<|endoftext|>


<commit_before>test('watcher/Watcher', [

  '../../lib/watcher/Watcher',
  '../../lib/core/Model',
  '../../lib/core/Collection'

], function (test) {

  var instance;

  test.beforeEach(function () {

    var templates = new test.deps.Collection({
      'name': 'simple',
      'path': 'simple'
    });
    var pages = new test.deps.Collection();

    instance = new test.deps.Watcher(null, {
      'paths': {
        'styles': ['tests/mocks/styles']
      },
      'templates': templates,
      'pages': pages,
      'muteLog': true
    });
  });

  test.afterEach(function () {
    instance.destroy();
  });

  test.spec('startWatching (string path)', function () {

    test.when('path is NOT a string', function () {

      test.it('should throw an error', function () {

        expect(function () {

          instance.startWatching(null);
        }).to.throw(Error);
      });
    });

    test.when('path is a string', function () {

      test.when('a file in the path is added', function () {

        test.it('should call self.handleChange', function (done) {

          this.timeout(5000);

          var filename = test.getTempFilename();
          var content = '7s89d7a9sd7';

          instance.handleChange = function (path, oldStat, newStat, type) {
            if (path.indexOf(filename) >= 0) {
              expect(type).to.equal('created');
              done();
            }
          };

          instance.startWatching(test.options.tempPath);

          setTimeout(function () {
            test.writeTempFile(filename, content);
          }, 10);
        });
      });

      test.when('a file in the path is removed', function () {

        test.it('should call self.handleChange', function (done) {

          var filename = test.getTempFilename();
          var content = 'foo';

          instance.handleChange = function (path, oldStat, newStat, type) {
            if (path.indexOf(filename) >= 0) {
              expect(type).to.equal('deleted');
              done();
            }
          };

          test.writeTempFile(filename, content);
          instance.startWatching(test.options.tempPath);
          test.removeTempFile(filename);
        });
      });

      test.when('a file in the path is changed', function () {

        test.it('should call self.handleChange', function () {

          var filename = test.getTempFilename();
          var content1 = 'foo';
          var content2 = 'barbaz';

          instance.handleChange = function (path, oldStat, newStat, type) {
            if (path.indexOf(filename) >= 0) {
              expect(type).to.equal('removed');
              done();
            }
          };

          test.writeTempFile(filename, content1);
          instance.startWatching(test.options.tempPath);
          test.writeTempFile(filename, content2);
        });
      });
    });
  });

  test.spec('watch (object model)', function () {

    test.when('model.attributes.type is not defined', function () {

      test.it('should throw an error', function () {

        var model = new test.deps.Model({
          'path': 'foo',
          'raw': 'bar'
        });

        expect(function () {
          instance.watch(model);
        }).to.throw(Error);
      });
    });

    test.when('model.attributes.path is not defined', function () {

      test.it('should throw an error', function () {

        var model = new test.deps.Model({
          'type': 'foo',
          'raw': 'bar'
        });

        expect(function () {
          instance.watch(model);
        }).to.throw(Error);
      });
    });

    test.when('model.attributes.raw is not defined', function () {

      test.it('should throw an error', function () {

        var model = new test.deps.Model({
          'path': 'foo',
          'type': 'bar'
        });

        expect(function () {
          instance.watch(model);
        }).to.throw(Error);
      });
    });
  });

  test.spec('handleChange (string path, object oldStat, object newStat, string type)', function () {

    var extensions = {
      'page': 'txt',
      'template': 'tmpl'
    };

    function testHandleChange (type) {

      var actions = ['created', 'modified', 'deleted'];

      for (var i = 0, max = actions.length; i < max; i++) {
        testHandleChangeForAction(type, actions[i]);
      }
    }

    function testHandleChangeForAction (type, action) {

      test.when('a ' + type + ' file is ' + action, function () {
        test.it('should call ' + type + '.' + action + ' handler', function () {

          var spy = sinon.spy(instance._handlers[type], action);
          var path = 'a9ua09dua90sud09sad.' + extensions[type];
          instance.handleChange(path, {}, {}, action);

          expect(spy).to.have.been.calledOnce;
          // expect(spy).to.have.been.calledWith(model, null, instance);

          instance._handlers[type][action].restore();
        });
      });
    }

    testHandleChange('page');
    testHandleChange('template');
    testHandleChange('styleSheet');
  });

  test.spec('parseDependencies (object model)', function () {

    test.when('there is no matching parser for model.attributes.type', function () {

      test.it('should throw an error', function () {

        var model = new test.deps.Model({
          'path': 'foo',
          'type': 'bar',
          'raw': 'baz'
        });

        expect(function () {
          instance.parseDependencies(model);
        }).to.throw(Error);
      });
    });

    test.when('there is a matching parser for model.attributes.type', function () {

      test.it('should pass model as first argument', function () {

        var model = new test.deps.Model({
          'path': 'foo',
          'type': 'styleSheet',
          'raw': 'baz'
        });

        var spy = sinon.spy(instance._parsers.styleSheet, 'parse');

        instance.parseDependencies(model);
        expect(spy).to.have.been.calledWith(model);
        instance._parsers.styleSheet.parse.restore();
      });

      test.it('should pass itself as third argument', function () {

        var model = new test.deps.Model({
          'path': 'foo',
          'type': 'styleSheet',
          'raw': 'baz'
        });

        var spy = sinon.spy(instance._parsers.styleSheet, 'parse');

        instance.parseDependencies(model);
        expect(spy).to.have.been.calledWith(model, null, instance);
        instance._parsers.styleSheet.parse.restore();
      });
    });
  });
});<commit_msg>fix tests<commit_after>test('watcher/Watcher', [

  '../../lib/watcher/Watcher',
  '../../lib/core/Model',
  '../../lib/core/Collection'

], function (test) {

  var instance;

  test.beforeEach(function () {

    var templates = new test.deps.Collection({
      'name': 'simple',
      'path': 'simple'
    });
    var pages = new test.deps.Collection();

    instance = new test.deps.Watcher(null, {
      'paths': {
        'styles': ['tests/mocks/styles']
      },
      'templates': templates,
      'pages': pages,
      'muteLog': true
    });
  });

  test.afterEach(function () {
    instance.destroy();
  });

  test.spec('startWatching (string path)', function () {

    test.when('path is NOT a string', function () {

      test.it('should throw an error', function () {

        expect(function () {

          instance.startWatching(null);
        }).to.throw(Error);
      });
    });

    test.when('path is a string', function () {

      test.when('a file in the path is added', function () {

        test.it('should call self.handleChange', function (done) {

          this.timeout(5000);

          var filename = test.getTempFilename();
          var content = '7s89d7a9sd7';

          instance.handleChange = function (path, oldStat, newStat, type) {
            if (path.indexOf(filename) >= 0) {
              expect(type).to.equal('created');
              done();
            }
          };

          instance.startWatching(test.options.tempPath);

          setTimeout(function () {
            test.writeTempFile(filename, content);
          }, 10);
        });
      });

      test.when('a file in the path is removed', function () {

        test.it('should call self.handleChange', function (done) {

          var filename = test.getTempFilename();
          var content = 'foo';

          instance.handleChange = function (path, oldStat, newStat, type) {
            if (path.indexOf(filename) >= 0) {
              expect(type).to.equal('deleted');
              done();
            }
          };

          test.writeTempFile(filename, content);
          instance.startWatching(test.options.tempPath);
          test.removeTempFile(filename);
        });
      });

      test.when('a file in the path is changed', function () {

        test.it('should call self.handleChange', function () {

          var filename = test.getTempFilename();
          var content1 = 'foo';
          var content2 = 'barbaz';

          instance.handleChange = function (path, oldStat, newStat, type) {
            if (path.indexOf(filename) >= 0) {
              expect(type).to.equal('removed');
              done();
            }
          };

          test.writeTempFile(filename, content1);
          instance.startWatching(test.options.tempPath);
          test.writeTempFile(filename, content2);
        });
      });
    });
  });

  test.spec('watch (object model)', function () {

    test.when('model.attributes.type is not defined', function () {

      test.it('should throw an error', function () {

        var model = new test.deps.Model({
          'path': 'foo',
          'raw': 'bar'
        });

        expect(function () {
          instance.watch(model);
        }).to.throw(Error);
      });
    });

    test.when('model.attributes.path is not defined', function () {

      test.it('should throw an error', function () {

        var model = new test.deps.Model({
          'type': 'foo',
          'raw': 'bar'
        });

        expect(function () {
          instance.watch(model);
        }).to.throw(Error);
      });
    });

    test.when('model.attributes.raw is not defined', function () {

      test.it('should throw an error', function () {

        var model = new test.deps.Model({
          'path': 'foo',
          'type': 'bar'
        });

        expect(function () {
          instance.watch(model);
        }).to.throw(Error);
      });
    });
  });

  test.spec('handleChange (string path, object oldStat, object newStat, string type)', function () {

    var extensions = {
      'page': 'txt',
      'template': 'tmpl',
      'styleSheet': 'styl'
    };

    function testHandleChange (type) {

      var actions = ['created', 'modified', 'deleted'];

      for (var i = 0, max = actions.length; i < max; i++) {
        testHandleChangeForAction(type, actions[i]);
      }
    }

    function testHandleChangeForAction (type, action) {

      test.when('a ' + type + ' file is ' + action, function () {
        test.it('should call ' + type + '.' + action + ' handler', function () {

          var spy = sinon.spy(instance._handlers[type], action);
          var path = 'a9ua09dua90sud09sad.' + extensions[type];
          instance.handleChange(path, {}, {}, action);

          expect(spy).to.have.been.calledOnce;
          // expect(spy).to.have.been.calledWith(model, null, instance);

          instance._handlers[type][action].restore();
        });
      });
    }

    testHandleChange('page');
    testHandleChange('template');
    testHandleChange('styleSheet');
  });

  test.spec('parseDependencies (object model)', function () {

    test.when('there is no matching parser for model.attributes.type', function () {

      test.it('should throw an error', function () {

        var model = new test.deps.Model({
          'path': 'foo',
          'type': 'bar',
          'raw': 'baz'
        });

        expect(function () {
          instance.parseDependencies(model);
        }).to.throw(Error);
      });
    });

    test.when('there is a matching parser for model.attributes.type', function () {

      test.it('should pass model as first argument', function () {

        var model = new test.deps.Model({
          'path': 'foo',
          'type': 'styleSheet',
          'raw': 'baz'
        });

        var spy = sinon.spy(instance._parsers.styleSheet, 'parse');

        instance.parseDependencies(model);
        expect(spy).to.have.been.calledWith(model);
        instance._parsers.styleSheet.parse.restore();
      });

      test.it('should pass itself as third argument', function () {

        var model = new test.deps.Model({
          'path': 'foo',
          'type': 'styleSheet',
          'raw': 'baz'
        });

        var spy = sinon.spy(instance._parsers.styleSheet, 'parse');

        instance.parseDependencies(model);
        expect(spy).to.have.been.calledWith(model, null, instance);
        instance._parsers.styleSheet.parse.restore();
      });
    });
  });
});<|endoftext|>


<commit_before>/*global SelectBox, addEvent, gettext, interpolate, quickElement, SelectFilter*/
/*
SelectFilter2 - Turns a multiple-select box into a filter interface.

Requires core.js, SelectBox.js and addevent.js.
*/
(function($) {
    'use strict';
    function findForm(node) {
        // returns the node of the form containing the given node
        if (node.tagName.toLowerCase() !== 'form') {
            return findForm(node.parentNode);
        }
        return node;
    }

    window.SelectFilter = {
        init: function(field_id, field_name, is_stacked) {
            if (field_id.match(/__prefix__/)) {
                // Don't initialize on empty forms.
                return;
            }
            var from_box = document.getElementById(field_id);
            from_box.id += '_from'; // change its ID
            from_box.className = 'filtered';

            var ps = from_box.parentNode.getElementsByTagName('p');
            for (var i = 0; i < ps.length; i++) {
                if (ps[i].className.indexOf("info") !== -1) {
                    // Remove
, because it just gets in the way.
                    from_box.parentNode.removeChild(ps[i]);
                } else if (ps[i].className.indexOf("help") !== -1) {
                    // Move help text up to the top so it isn't below the select
                    // boxes or wrapped off on the side to the right of the add
                    // button:
                    from_box.parentNode.insertBefore(ps[i], from_box.parentNode.firstChild);
                }
            }

            //
 or

            var selector_div = quickElement('div', from_box.parentNode);
            selector_div.className = is_stacked ? 'selector stacked' : 'selector';

            //

            var selector_available = quickElement('div', selector_div);
            selector_available.className = 'selector-available';
            var title_available = quickElement('h2', selector_available, interpolate(gettext('Available %s') + ' ', [field_name]));
            quickElement(
                'span', title_available, '',
                'class', 'help help-tooltip help-icon',
                'title', interpolate(
                    gettext(
                        'This is the list of available %s. You may choose some by ' +
                        'selecting them in the box below and then clicking the ' +
                        '"Choose" arrow between the two boxes.'
                    ),
                    [field_name]
                )
            );

            var filter_p = quickElement('p', selector_available, '', 'id', field_id + '_filter');
            filter_p.className = 'selector-filter';

            var search_filter_label = quickElement('label', filter_p, '', 'for', field_id + '_input');

            quickElement(
                'span', search_filter_label, '',
                'class', 'help-tooltip search-label-icon',
                'title', interpolate(gettext("Type into this box to filter down the list of available %s."), [field_name])
            );

            filter_p.appendChild(document.createTextNode(' '));

            var filter_input = quickElement('input', filter_p, '', 'type', 'text', 'placeholder', gettext("Filter"));
            filter_input.id = field_id + '_input';

            selector_available.appendChild(from_box);
            var choose_all = quickElement('a', selector_available, gettext('Choose all'), 'title', interpolate(gettext('Click to choose all %s at once.'), [field_name]), 'href', 'javascript:void(0);', 'id', field_id + '_add_all_link');
            choose_all.className = 'selector-chooseall';

            //

            var selector_chooser = quickElement('ul', selector_div);
            selector_chooser.className = 'selector-chooser';
            var add_link = quickElement('a', quickElement('li', selector_chooser), gettext('Choose'), 'title', gettext('Choose'), 'href', 'javascript:void(0);', 'id', field_id + '_add_link');
            add_link.className = 'selector-add';
            var remove_link = quickElement('a', quickElement('li', selector_chooser), gettext('Remove'), 'title', gettext('Remove'), 'href', 'javascript:void(0);', 'id', field_id + '_remove_link');
            remove_link.className = 'selector-remove';

            //

            var selector_chosen = quickElement('div', selector_div);
            selector_chosen.className = 'selector-chosen';
            var title_chosen = quickElement('h2', selector_chosen, interpolate(gettext('Chosen %s') + ' ', [field_name]));
            quickElement(
                'span', title_chosen, '',
                'class', 'help help-tooltip help-icon',
                'title', interpolate(
                    gettext(
                        'This is the list of chosen %s. You may remove some by ' +
                        'selecting them in the box below and then clicking the ' +
                        '"Remove" arrow between the two boxes.'
                    ),
                    [field_name]
                )
            );

            var to_box = quickElement('select', selector_chosen, '', 'id', field_id + '_to', 'multiple', 'multiple', 'size', from_box.size, 'name', from_box.getAttribute('name'));
            to_box.className = 'filtered';
            var clear_all = quickElement('a', selector_chosen, gettext('Remove all'), 'title', interpolate(gettext('Click to remove all chosen %s at once.'), [field_name]), 'href', 'javascript:void(0);', 'id', field_id + '_remove_all_link');
            clear_all.className = 'selector-clearall';

            from_box.setAttribute('name', from_box.getAttribute('name') + '_old');

            // Set up the JavaScript event handlers for the select box filter interface
            addEvent(choose_all, 'click', function() { SelectBox.move_all(field_id + '_from', field_id + '_to'); SelectFilter.refresh_icons(field_id); });
            addEvent(add_link, 'click', function() { SelectBox.move(field_id + '_from', field_id + '_to'); SelectFilter.refresh_icons(field_id); });
            addEvent(remove_link, 'click', function() { SelectBox.move(field_id + '_to', field_id + '_from'); SelectFilter.refresh_icons(field_id); });
            addEvent(clear_all, 'click', function() { SelectBox.move_all(field_id + '_to', field_id + '_from'); SelectFilter.refresh_icons(field_id); });
            addEvent(filter_input, 'keypress', function(e) { SelectFilter.filter_key_press(e, field_id); });
            addEvent(filter_input, 'keyup', function(e) { SelectFilter.filter_key_up(e, field_id); });
            addEvent(filter_input, 'keydown', function(e) { SelectFilter.filter_key_down(e, field_id); });
            addEvent(from_box, 'change', function(e) { SelectFilter.refresh_icons(field_id); });
            addEvent(to_box, 'change', function(e) { SelectFilter.refresh_icons(field_id); });
            addEvent(from_box, 'dblclick', function() { SelectBox.move(field_id + '_from', field_id + '_to'); SelectFilter.refresh_icons(field_id); });
            addEvent(to_box, 'dblclick', function() { SelectBox.move(field_id + '_to', field_id + '_from'); SelectFilter.refresh_icons(field_id); });
            addEvent(findForm(from_box), 'submit', function() { SelectBox.select_all(field_id + '_to'); });
            SelectBox.init(field_id + '_from');
            SelectBox.init(field_id + '_to');
            // Move selected from_box options to to_box
            SelectBox.move(field_id + '_from', field_id + '_to');

            if (!is_stacked) {
                // In horizontal mode, give the same height to the two boxes.
                var j_from_box = $(from_box);
                var j_to_box = $(to_box);
                var resize_filters = function() { j_to_box.height($(filter_p).outerHeight() + j_from_box.outerHeight()); };
                if (j_from_box.outerHeight() > 0) {
                    resize_filters(); // This fieldset is already open. Resize now.
                } else {
                    // This fieldset is probably collapsed. Wait for its 'show' event.
                    j_to_box.closest('fieldset').one('show.fieldset', resize_filters);
                }
            }

            // Initial icon refresh
            SelectFilter.refresh_icons(field_id);
        },
        refresh_icons: function(field_id) {
            var from = $('#' + field_id + '_from');
            var to = $('#' + field_id + '_to');
            var is_from_selected = from.find('option:selected').length > 0;
            var is_to_selected = to.find('option:selected').length > 0;
            // Active if at least one item is selected
            $('#' + field_id + '_add_link').toggleClass('active', is_from_selected);
            $('#' + field_id + '_remove_link').toggleClass('active', is_to_selected);
            // Active if the corresponding box isn't empty
            $('#' + field_id + '_add_all_link').toggleClass('active', from.find('option').length > 0);
            $('#' + field_id + '_remove_all_link').toggleClass('active', to.find('option').length > 0);
        },
        filter_key_press: function(event, field_id) {
            var from = document.getElementById(field_id + '_from');
            // don't submit form if user pressed Enter
            if ((event.which && event.which === 13) || (event.keyCode && event.keyCode === 13)) {
                from.selectedIndex = 0;
                SelectBox.move(field_id + '_from', field_id + '_to');
                from.selectedIndex = 0;
                event.preventDefault();
                return false;
            }
        },
        filter_key_up: function(event, field_id) {
            var from = document.getElementById(field_id + '_from');
            var temp = from.selectedIndex;
            SelectBox.filter(field_id + '_from', document.getElementById(field_id + '_input').value);
            from.selectedIndex = temp;
            return true;
        },
        filter_key_down: function(event, field_id) {
            var from = document.getElementById(field_id + '_from');
            // right arrow -- move across
            if ((event.which && event.which === 39) || (event.keyCode && event.keyCode === 39)) {
                var old_index = from.selectedIndex;
                SelectBox.move(field_id + '_from', field_id + '_to');
                from.selectedIndex = (old_index === from.length) ? from.length - 1 : old_index;
                return false;
            }
            // down arrow -- wrap around
            if ((event.which && event.which === 40) || (event.keyCode && event.keyCode === 40)) {
                from.selectedIndex = (from.length === from.selectedIndex + 1) ? 0 : from.selectedIndex + 1;
            }
            // up arrow -- wrap around
            if ((event.which && event.which === 38) || (event.keyCode && event.keyCode === 38)) {
                from.selectedIndex = (from.selectedIndex === 0) ? from.length - 1 : from.selectedIndex - 1;
            }
            return true;
        }
    };

})(django.jQuery);
<commit_msg>Update tmp/admin/js/SelectFilter2.js<commit_after><|endoftext|>


<commit_before>// Copyright 2015-present 650 Industries. All rights reserved.

'use strict';

import 'instapromise';

import crayon from '@ccheever/crayon';
import fs from 'fs';
import shell from 'shelljs';
import path from 'path';
import { getManifestAsync, saveUrlToPathAsync } from './shell-app-utils';
import {
  modifyIOSPropertyListAsync,
  cleanIOSPropertyListBackupAsync,
} from './ios-utils';

function validateConfigArguments(manifest, cmdArgs, configFilePath) {
  if (!configFilePath) {
    throw new Error('No path to config files provided');
  }
  if ((!manifest.ios && !cmdArgs.bundleIdentifier) ||
      (!manifest.ios.bundleIdentifier && !cmdArgs.bundleIdentifier)) {
    throw new Error('No bundle identifier found in either the manifest or argv');
  }
  if (!manifest.name) {
    throw new Error('Manifest does not have a name');
  }

  if (!cmdArgs.privateConfigFile) {
    crayon.yellow.warn('Warning: No config file specified.');
  }
  return true;
}

/**
 * Writes Fabric config to private-shell-app-config.json if necessary. Used by
 * generate-dynamic-macros when building.
 */
async function configureShellAppSecretsAsync(args, iosDir) {
  if (!args.privateConfigFile) {
    return;
  }

  shell.exec(`cp ${args.privateConfigFile} ${path.join(iosDir, 'private-shell-app-config.json')}`);
}

async function configurePropertyListsAsync(manifest, args, configFilePath) {
  // make sure we have all the required info
  validateConfigArguments(manifest, args, configFilePath);
  console.log(`Modifying config files under ${configFilePath}...`);

  let {
    url,
    bundleIdentifier,
    privateConfigFile,
  } = args;

  let privateConfig;
  if (privateConfigFile) {
    let privateConfigContents = await fs.promise.readFile(privateConfigFile, 'utf8');
    privateConfig = JSON.parse(privateConfigContents);
  }

  // generate new shell config
  await modifyIOSPropertyListAsync(configFilePath, 'EXShell', (shellConfig) => {
    shellConfig.isShell = true;
    shellConfig.manifestUrl = shellConfig.manifestUrl || url;
    if (manifest.ios && manifest.ios.permissions) {
      shellConfig.permissions = manifest.ios.permissions;
    }

    console.log('Using shell config:', shellConfig);
    return shellConfig;
  });

  // modify Info.plist based on manifest
  await modifyIOSPropertyListAsync(configFilePath, 'Info', (config) => {
    // bundle id
    config.CFBundleIdentifier = manifest.ios.bundleIdentifier || bundleIdentifier;

    // app name
    config.CFBundleName = manifest.name;

    // determine app linking schemes
    let linkingSchemes = (manifest.scheme) ? [manifest.scheme] : [];
    if (manifest.facebookScheme && manifest.facebookScheme.startsWith('fb')) {
      linkingSchemes.push(manifest.facebookScheme);
    }

    // remove exp scheme, add app scheme(s)
    config.CFBundleURLTypes = [{
      CFBundleURLSchemes: linkingSchemes,
    }];

    // use shell-specific launch screen
    config.UILaunchStoryboardName = 'LaunchScreenShell';

    // permanently save the exponent client version at time of configuration
    config.EXClientVersion = config.CFBundleVersion;

    // use version from manifest
    let version = (manifest.version) ? manifest.version : '0.0.0';
    let buildNumber = (manifest.ios && manifest.ios.buildNumber) ?
      manifest.ios.buildNumber :
      '1';
    config.CFBundleShortVersionString = version;
    config.CFBundleVersion = buildNumber;

    let internalKeys;
    try {
      internalKeys = require('../__internal__/keys.json');
    } catch (e) {
      internalKeys = require('../template-files/keys.json');
    }
    const defaultFabricKey = internalKeys.FABRIC_API_KEY;

    config.Fabric = {
      APIKey: (privateConfig && privateConfig.fabric && privateConfig.fabric.apiKey) || defaultFabricKey,
      Kits: [{
        KitInfo: {},
        KitName: 'Crashlytics',
      }],
    };

    return config;
  });
}

/**
 * Ensure that the proper icon images exist -- Info.plist already points at them
 * under CFBundleIcons.CFBundlePrimaryIcon.CFBundleIconFiles
 */
async function configureIconsAsync(manifest, args, configFilePath) {
  let defaultIconFilename;
  if (manifest.iconUrl) {
     defaultIconFilename = 'exp-icon.png';
     await saveUrlToPathAsync(manifest.iconUrl, `${configFilePath}/${defaultIconFilename}`);
   }

  let iconSizes = [29, 40, 60];
  let iconResolutions = [2, 3];
  iconSizes.forEach(iconSize => {
    iconResolutions.forEach(async (iconResolution) => {
      let iconQualifier = `${iconSize}x${iconSize}@${iconResolution}x`;
      let iconKey = `iconUrl${iconQualifier}`;
      let rawIconFilename;
      let usesDefault = false;
      if (manifest.ios && manifest.ios.hasOwnProperty(iconKey)) {
        // manifest specifies an image just for this size/resolution, use that
        rawIconFilename = `exp-icon${iconQualifier}.png`;
        await saveUrlToPathAsync(manifest.ios[iconKey], `${configFilePath}/${rawIconFilename}`);
      } else {
        // use default manifest.iconUrl
        usesDefault = true;
        if (defaultIconFilename) {
          rawIconFilename = defaultIconFilename;
        } else {
          console.warn(`Manifest does not specify ios.${iconKey} nor a default iconUrl. Bundle will use the Exponent logo.`);
          return;
        }
      }
      let iconFilename = `AppIcon${iconQualifier}.png`;
      let iconSizePx = iconSize * iconResolution;
      let iconCommands = [
        `pushd ${configFilePath}`,
        `cp ${rawIconFilename} ${iconFilename}`,
        `sips -Z ${iconSizePx} ${iconFilename}`,
      ];
      if (!usesDefault) {
        // non-default icon used, clean up the downloaded version
        iconCommands = iconCommands.concat([`rm ${rawIconFilename}`]);
      }
      shell.exec(iconCommands.join(' && '));
    });
  });

  // clean up default icon
  if (defaultIconFilename) {
    shell.exec(`rm ${configFilePath}/${defaultIconFilename}`);
  }
  return;
}

/**
 * Write the manifest and JS bundle to the iOS NSBundle.
 */
async function preloadManifestAndBundleAsync(manifest, args, configFilePath) {
  let bundleUrl = manifest.bundleUrl;
  await fs.promise.writeFile(`${configFilePath}/shell-app-manifest.json`, JSON.stringify(manifest));
  await saveUrlToPathAsync(bundleUrl, `${configFilePath}/shell-app.bundle`);
  return;
}

async function cleanPropertyListBackupsAsync(configFilePath, restoreOriginals) {
  console.log('Cleaning up...');
  await cleanIOSPropertyListBackupAsync(configFilePath, 'EXShell', restoreOriginals);
  await cleanIOSPropertyListBackupAsync(configFilePath, 'Info', restoreOriginals);
}

/**
 *  Build the iOS binary from source.
 *  @return the path to the resulting .app
 */
async function buildAsync(args, iOSRootPath, relativeBuildDestination) {
  let { action, configuration, verbose, type } = args;

  let buildCmd, buildDest, pathToApp;
  if (type === 'simulator') {
    buildDest = `${iOSRootPath}/${relativeBuildDestination}-simulator`;
    buildCmd = `xcodebuild -workspace Exponent.xcworkspace -scheme Exponent -sdk iphonesimulator -configuration ${configuration} -arch i386 -derivedDataPath ${buildDest} CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO SKIP_INSTALL=NO | xcpretty`;
    pathToApp = `${buildDest}/Build/Products/${configuration}-iphonesimulator/Exponent.app`;
  } else if (type === 'archive') {
    buildDest = `${iOSRootPath}/${relativeBuildDestination}-archive`;
    buildCmd = `xcodebuild -workspace Exponent.xcworkspace -scheme Exponent archive -configuration ${configuration} -derivedDataPath ${buildDest} -archivePath ${buildDest}/Exponent.xcarchive CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO SKIP_INSTALL=NO | xcpretty`;
    pathToApp = `${buildDest}/Exponent.xcarchive/Products/Applications/Exponent.app`;
  }

  if (buildCmd) {
    if (!verbose) {
      buildCmd = `${buildCmd} > /dev/null`;
    }

    console.log(`Building shell app under ${iOSRootPath}/${relativeBuildDestination}`);
    console.log(`  (action: ${action}, configuration: ${configuration})...`);
    console.log(buildCmd);
    shell.exec(`pushd ${iOSRootPath} && ${buildCmd}`);

    let artifactLocation = `${iOSRootPath}/../shellAppBase-builds/${type}/${configuration}/`;
    shell.rm('-rf', artifactLocation);
    shell.mkdir('-p', artifactLocation);

    if (type === 'archive') {
      shell.cp('-R', `${buildDest}/Exponent.xcarchive`, artifactLocation);
    } else if (type === 'simulator') {
      shell.cp('-R', pathToApp, artifactLocation);
    }

  }
  return pathToApp;
}

function validateArgs(args) {
  args.type = args.type || 'archive';
  args.configuration = args.configuration || 'Release';
  args.verbose = args.verbose || false;

  switch (args.type) {
    case 'simulator': {
      if (args.configuration !== 'Debug' && args.configuration !== 'Release') {
        throw new Error(`Unsupported build configuration ${args.configuration}`);
      }
      break;
    }
    case 'archive': {
      if (args.configuration !== 'Release') {
        throw new Error('Release is the only supported configuration when archiving');
      }
      break;
    }
    default: {
      throw new Error(`Unsupported build type ${args.type}`);
    }
  }

  switch (args.action) {
    case 'configure': {
      if (!args.url) {
        throw new Error('Must run with `--url MANIFEST_URL`');
      }
      if (!args.sdkVersion) {
        throw new Error('Must run with `--sdkVersion SDK_VERSION`');
      }
      if (!args.archivePath) {
        throw new Error('Need to provide --archivePath <path to existing archive for configuration>');
      }
      break;
    }
    case 'build': {
      break;
    }
    default: {
      throw new Error(`Unsupported build action ${args.action}`);
    }
  }

  return args;
}

/**
*  @param url manifest url for shell experience
*  @param sdkVersion sdk to use when requesting the manifest
*  @param action
*    build - build a binary
*    configure - don't build anything, just configure the files in an existing .app bundle
*  @param type simulator or archive, for action == build
*  @param configuration Debug or Release, for type == simulator (default Release)
*  @param archivePath path to existing bundle, for action == configure
*  @param privateConfigFile path to a private config file containing, e.g., private api keys
*  @param bundleIdentifier iOS CFBundleIdentifier to use in the bundle config
*  @param verbose show all xcodebuild output (default false)
*/
export async function createIOSShellAppAsync(args) {
  let configFilePath;
  args = validateArgs(args);

  if (args.action !== 'configure') {
    // build the app before configuring
    await configureShellAppSecretsAsync(args, '../ios');
    configFilePath = await buildAsync(args, '../ios', '../shellAppBase');
  } else {
    let {
      url,
      sdkVersion,
      output,
      type,
    } = args;

    // fetch manifest
    let manifest = await getManifestAsync(url, {
      'Exponent-SDK-Version': sdkVersion,
      'Exponent-Platform': 'ios',
    });

    // action === 'configure'
    configFilePath = args.archivePath;
    // just configure, don't build anything
    await configurePropertyListsAsync(manifest, args, configFilePath);
    await configureIconsAsync(manifest, args, configFilePath);
    await preloadManifestAndBundleAsync(manifest, args, configFilePath);
    await cleanPropertyListBackupsAsync(configFilePath, false);

    let archiveName = manifest.name.replace(/\s+/g, '');
    if (type === 'simulator') {
      shell.exec(`cd ${configFilePath}/.. && mv Exponent.app ${archiveName}.app && tar cvf ${output} ${archiveName}.app`);
    } else if (type === 'archive') {
      shell.exec(`cd ${configFilePath}/../../../.. && mv Exponent.xcarchive ${output}`);
    }
  }

  return;
}
<commit_msg>Support iPad icon sizes<commit_after>// Copyright 2015-present 650 Industries. All rights reserved.

'use strict';

import 'instapromise';

import crayon from '@ccheever/crayon';
import fs from 'fs';
import shell from 'shelljs';
import path from 'path';
import { getManifestAsync, saveUrlToPathAsync } from './shell-app-utils';
import {
  modifyIOSPropertyListAsync,
  cleanIOSPropertyListBackupAsync,
} from './ios-utils';

function validateConfigArguments(manifest, cmdArgs, configFilePath) {
  if (!configFilePath) {
    throw new Error('No path to config files provided');
  }
  if ((!manifest.ios && !cmdArgs.bundleIdentifier) ||
      (!manifest.ios.bundleIdentifier && !cmdArgs.bundleIdentifier)) {
    throw new Error('No bundle identifier found in either the manifest or argv');
  }
  if (!manifest.name) {
    throw new Error('Manifest does not have a name');
  }

  if (!cmdArgs.privateConfigFile) {
    crayon.yellow.warn('Warning: No config file specified.');
  }
  return true;
}

/**
 * Writes Fabric config to private-shell-app-config.json if necessary. Used by
 * generate-dynamic-macros when building.
 */
async function configureShellAppSecretsAsync(args, iosDir) {
  if (!args.privateConfigFile) {
    return;
  }

  shell.exec(`cp ${args.privateConfigFile} ${path.join(iosDir, 'private-shell-app-config.json')}`);
}

async function configurePropertyListsAsync(manifest, args, configFilePath) {
  // make sure we have all the required info
  validateConfigArguments(manifest, args, configFilePath);
  console.log(`Modifying config files under ${configFilePath}...`);

  let {
    url,
    bundleIdentifier,
    privateConfigFile,
  } = args;

  let privateConfig;
  if (privateConfigFile) {
    let privateConfigContents = await fs.promise.readFile(privateConfigFile, 'utf8');
    privateConfig = JSON.parse(privateConfigContents);
  }

  // generate new shell config
  await modifyIOSPropertyListAsync(configFilePath, 'EXShell', (shellConfig) => {
    shellConfig.isShell = true;
    shellConfig.manifestUrl = shellConfig.manifestUrl || url;
    if (manifest.ios && manifest.ios.permissions) {
      shellConfig.permissions = manifest.ios.permissions;
    }

    console.log('Using shell config:', shellConfig);
    return shellConfig;
  });

  // modify Info.plist based on manifest
  await modifyIOSPropertyListAsync(configFilePath, 'Info', (config) => {
    // bundle id
    config.CFBundleIdentifier = manifest.ios.bundleIdentifier || bundleIdentifier;

    // app name
    config.CFBundleName = manifest.name;

    // determine app linking schemes
    let linkingSchemes = (manifest.scheme) ? [manifest.scheme] : [];
    if (manifest.facebookScheme && manifest.facebookScheme.startsWith('fb')) {
      linkingSchemes.push(manifest.facebookScheme);
    }

    // remove exp scheme, add app scheme(s)
    config.CFBundleURLTypes = [{
      CFBundleURLSchemes: linkingSchemes,
    }];

    // use shell-specific launch screen
    config.UILaunchStoryboardName = 'LaunchScreenShell';

    // permanently save the exponent client version at time of configuration
    config.EXClientVersion = config.CFBundleVersion;

    // use version from manifest
    let version = (manifest.version) ? manifest.version : '0.0.0';
    let buildNumber = (manifest.ios && manifest.ios.buildNumber) ?
      manifest.ios.buildNumber :
      '1';
    config.CFBundleShortVersionString = version;
    config.CFBundleVersion = buildNumber;

    let internalKeys;
    try {
      internalKeys = require('../__internal__/keys.json');
    } catch (e) {
      internalKeys = require('../template-files/keys.json');
    }
    const defaultFabricKey = internalKeys.FABRIC_API_KEY;

    config.Fabric = {
      APIKey: (privateConfig && privateConfig.fabric && privateConfig.fabric.apiKey) || defaultFabricKey,
      Kits: [{
        KitInfo: {},
        KitName: 'Crashlytics',
      }],
    };

    return config;
  });
}

/**
 * Ensure that the proper icon images exist -- Info.plist already points at them
 * under CFBundleIcons.CFBundlePrimaryIcon.CFBundleIconFiles
 */
async function configureIconsAsync(manifest, args, configFilePath) {
  let defaultIconFilename;
  if (manifest.iconUrl) {
     defaultIconFilename = 'exp-icon.png';
     await saveUrlToPathAsync(manifest.iconUrl, `${configFilePath}/${defaultIconFilename}`);
   }

  let iconSizes = [29, 40, 60, 76, 83.5];
  iconSizes.forEach(iconSize => {
    let iconResolutions;
    if (iconSize === 76) {
      // iPad has 1x and 2x icons for this size only
      iconResolutions = [1, 2];
    } else {
      iconResolutions = [2, 3];
    }
    iconResolutions.forEach(async (iconResolution) => {
      let iconQualifier = `${iconSize}x${iconSize}@${iconResolution}x`;
      let iconKey = `iconUrl${iconQualifier}`;
      let rawIconFilename;
      let usesDefault = false;
      if (manifest.ios && manifest.ios.hasOwnProperty(iconKey)) {
        // manifest specifies an image just for this size/resolution, use that
        rawIconFilename = `exp-icon${iconQualifier}.png`;
        await saveUrlToPathAsync(manifest.ios[iconKey], `${configFilePath}/${rawIconFilename}`);
      } else {
        // use default manifest.iconUrl
        usesDefault = true;
        if (defaultIconFilename) {
          rawIconFilename = defaultIconFilename;
        } else {
          console.warn(`Manifest does not specify ios.${iconKey} nor a default iconUrl. Bundle will use the Exponent logo.`);
          return;
        }
      }
      let iconFilename = `AppIcon${iconQualifier}.png`;
      let iconSizePx = iconSize * iconResolution;
      let iconCommands = [
        `pushd ${configFilePath}`,
        `cp ${rawIconFilename} ${iconFilename}`,
        `sips -Z ${iconSizePx} ${iconFilename}`,
      ];
      if (!usesDefault) {
        // non-default icon used, clean up the downloaded version
        iconCommands = iconCommands.concat([`rm ${rawIconFilename}`]);
      }
      shell.exec(iconCommands.join(' && '));
    });
  });

  // clean up default icon
  if (defaultIconFilename) {
    shell.exec(`rm ${configFilePath}/${defaultIconFilename}`);
  }
  return;
}

/**
 * Write the manifest and JS bundle to the iOS NSBundle.
 */
async function preloadManifestAndBundleAsync(manifest, args, configFilePath) {
  let bundleUrl = manifest.bundleUrl;
  await fs.promise.writeFile(`${configFilePath}/shell-app-manifest.json`, JSON.stringify(manifest));
  await saveUrlToPathAsync(bundleUrl, `${configFilePath}/shell-app.bundle`);
  return;
}

async function cleanPropertyListBackupsAsync(configFilePath, restoreOriginals) {
  console.log('Cleaning up...');
  await cleanIOSPropertyListBackupAsync(configFilePath, 'EXShell', restoreOriginals);
  await cleanIOSPropertyListBackupAsync(configFilePath, 'Info', restoreOriginals);
}

/**
 *  Build the iOS binary from source.
 *  @return the path to the resulting .app
 */
async function buildAsync(args, iOSRootPath, relativeBuildDestination) {
  let { action, configuration, verbose, type } = args;

  let buildCmd, buildDest, pathToApp;
  if (type === 'simulator') {
    buildDest = `${iOSRootPath}/${relativeBuildDestination}-simulator`;
    buildCmd = `xcodebuild -workspace Exponent.xcworkspace -scheme Exponent -sdk iphonesimulator -configuration ${configuration} -arch i386 -derivedDataPath ${buildDest} CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO SKIP_INSTALL=NO | xcpretty`;
    pathToApp = `${buildDest}/Build/Products/${configuration}-iphonesimulator/Exponent.app`;
  } else if (type === 'archive') {
    buildDest = `${iOSRootPath}/${relativeBuildDestination}-archive`;
    buildCmd = `xcodebuild -workspace Exponent.xcworkspace -scheme Exponent archive -configuration ${configuration} -derivedDataPath ${buildDest} -archivePath ${buildDest}/Exponent.xcarchive CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO SKIP_INSTALL=NO | xcpretty`;
    pathToApp = `${buildDest}/Exponent.xcarchive/Products/Applications/Exponent.app`;
  }

  if (buildCmd) {
    if (!verbose) {
      buildCmd = `${buildCmd} > /dev/null`;
    }

    console.log(`Building shell app under ${iOSRootPath}/${relativeBuildDestination}`);
    console.log(`  (action: ${action}, configuration: ${configuration})...`);
    console.log(buildCmd);
    shell.exec(`pushd ${iOSRootPath} && ${buildCmd}`);

    let artifactLocation = `${iOSRootPath}/../shellAppBase-builds/${type}/${configuration}/`;
    shell.rm('-rf', artifactLocation);
    shell.mkdir('-p', artifactLocation);

    if (type === 'archive') {
      shell.cp('-R', `${buildDest}/Exponent.xcarchive`, artifactLocation);
    } else if (type === 'simulator') {
      shell.cp('-R', pathToApp, artifactLocation);
    }

  }
  return pathToApp;
}

function validateArgs(args) {
  args.type = args.type || 'archive';
  args.configuration = args.configuration || 'Release';
  args.verbose = args.verbose || false;

  switch (args.type) {
    case 'simulator': {
      if (args.configuration !== 'Debug' && args.configuration !== 'Release') {
        throw new Error(`Unsupported build configuration ${args.configuration}`);
      }
      break;
    }
    case 'archive': {
      if (args.configuration !== 'Release') {
        throw new Error('Release is the only supported configuration when archiving');
      }
      break;
    }
    default: {
      throw new Error(`Unsupported build type ${args.type}`);
    }
  }

  switch (args.action) {
    case 'configure': {
      if (!args.url) {
        throw new Error('Must run with `--url MANIFEST_URL`');
      }
      if (!args.sdkVersion) {
        throw new Error('Must run with `--sdkVersion SDK_VERSION`');
      }
      if (!args.archivePath) {
        throw new Error('Need to provide --archivePath <path to existing archive for configuration>');
      }
      break;
    }
    case 'build': {
      break;
    }
    default: {
      throw new Error(`Unsupported build action ${args.action}`);
    }
  }

  return args;
}

/**
*  @param url manifest url for shell experience
*  @param sdkVersion sdk to use when requesting the manifest
*  @param action
*    build - build a binary
*    configure - don't build anything, just configure the files in an existing .app bundle
*  @param type simulator or archive, for action == build
*  @param configuration Debug or Release, for type == simulator (default Release)
*  @param archivePath path to existing bundle, for action == configure
*  @param privateConfigFile path to a private config file containing, e.g., private api keys
*  @param bundleIdentifier iOS CFBundleIdentifier to use in the bundle config
*  @param verbose show all xcodebuild output (default false)
*/
export async function createIOSShellAppAsync(args) {
  let configFilePath;
  args = validateArgs(args);

  if (args.action !== 'configure') {
    // build the app before configuring
    await configureShellAppSecretsAsync(args, '../ios');
    configFilePath = await buildAsync(args, '../ios', '../shellAppBase');
  } else {
    let {
      url,
      sdkVersion,
      output,
      type,
    } = args;

    // fetch manifest
    let manifest = await getManifestAsync(url, {
      'Exponent-SDK-Version': sdkVersion,
      'Exponent-Platform': 'ios',
    });

    // action === 'configure'
    configFilePath = args.archivePath;
    // just configure, don't build anything
    await configurePropertyListsAsync(manifest, args, configFilePath);
    await configureIconsAsync(manifest, args, configFilePath);
    await preloadManifestAndBundleAsync(manifest, args, configFilePath);
    await cleanPropertyListBackupsAsync(configFilePath, false);

    let archiveName = manifest.name.replace(/\s+/g, '');
    if (type === 'simulator') {
      shell.exec(`cd ${configFilePath}/.. && mv Exponent.app ${archiveName}.app && tar cvf ${output} ${archiveName}.app`);
    } else if (type === 'archive') {
      shell.exec(`cd ${configFilePath}/../../../.. && mv Exponent.xcarchive ${output}`);
    }
  }

  return;
}
<|endoftext|>


<commit_before>//----------------------------------------------------------------------------
//  Copyright (C) 2013 The IPython Development Team
//
//  Distributed under the terms of the BSD License.  The full license is in
//  the file COPYING, distributed as part of this software.
//----------------------------------------------------------------------------

//============================================================================
// Base Widget Model and View classes
//============================================================================

/**
 * @module IPython
 * @namespace IPython
 **/

define(["notebook/js/widgetmanager",
        "underscore",
        "backbone"],
function(WidgetManager, Underscore, Backbone){

    var WidgetModel = Backbone.Model.extend({
        constructor: function (widget_manager, model_id, comm) {
            // Construcctor
            //
            // Creates a WidgetModel instance.
            //
            // Parameters
            // ----------
            // widget_manager : WidgetManager instance
            // model_id : string
            //      An ID unique to this model.
            // comm : Comm instance (optional)
            this.widget_manager = widget_manager;
            this.pending_msgs = 0;
            this.msg_throttle = 2;
            this.msg_buffer = null;
            this.key_value_lock = null;
            this.id = model_id;
            this.views = [];

            if (comm !== undefined) {
                // Remember comm associated with the model.
                this.comm = comm;
                comm.model = this;

                // Hook comm messages up to model.
                comm.on_close($.proxy(this._handle_comm_closed, this));
                comm.on_msg($.proxy(this._handle_comm_msg, this));
            }
            return Backbone.Model.apply(this);
        },

        send: function (content, callbacks) {
            // Send a custom msg over the comm.
            if (this.comm !== undefined) {
                var data = {method: 'custom', content: content};
                this.comm.send(data, callbacks);
            }
        },

        _handle_comm_closed: function (msg) {
            // Handle when a widget is closed.
            this.trigger('comm:close');
            delete this.comm.model; // Delete ref so GC will collect widget model.
            delete this.comm;
            delete this.model_id; // Delete id from model so widget manager cleans up.
            // TODO: Handle deletion, like this.destroy(), and delete views, etc.
        },

        _handle_comm_msg: function (msg) {
            // Handle incoming comm msg.
            var method = msg.content.data.method;
            switch (method) {
                case 'update':
                    this.apply_update(msg.content.data.state);
                    break;
                case 'custom':
                    this.trigger('msg:custom', msg.content.data.content);
                    break;
                case 'display':
                    this.widget_manager.display_view(msg, this);
                    break;
            }
        },

        apply_update: function (state) {
            // Handle when a widget is updated via the python side.
            for (var key in state) {
                if (state.hasOwnProperty(key)) {
                    var value = state[key];
                    this.key_value_lock = [key, value];
                    try {
                        this.set(key, this._unpack_models(value));
                    } finally {
                        this.key_value_lock = null;
                    }
                }
            }
        },

        _handle_status: function (msg, callbacks) {
            // Handle status msgs.

            // execution_state : ('busy', 'idle', 'starting')
            if (this.comm !== undefined) {
                if (msg.content.execution_state ==='idle') {
                    // Send buffer if this message caused another message to be
                    // throttled.
                    if (this.msg_buffer !== null &&
                        this.msg_throttle === this.pending_msgs) {
                        var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
                        this.comm.send(data, callbacks);
                        this.msg_buffer = null;
                    } else {
                        --this.pending_msgs;
                    }
                }
            }
        },

        callbacks: function(view) {
            // Create msg callbacks for a comm msg.
            var callbacks = this.widget_manager.callbacks(view);

            if (callbacks.iopub === undefined) {
                callbacks.iopub = {};
            }

            var that = this;
            callbacks.iopub.status = function (msg) {
                that._handle_status(msg, callbacks);
            }
            return callbacks;
        },

        sync: function (method, model, options) {

            // Make sure a comm exists.
            var error = options.error || function() {
                console.error('Backbone sync error:', arguments);
            }
            if (this.comm === undefined) {
                error();
                return false;
            }

            // Delete any key value pairs that the back-end already knows about.
            var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
            if (this.key_value_lock !== null) {
                var key = this.key_value_lock[0];
                var value = this.key_value_lock[1];
                if (attrs[key] === value) {
                    delete attrs[key];
                }
            }

            // Only sync if there are attributes to send to the back-end.
            if (_.size(attrs) !== 0) {
                var callbacks = options.callbacks || {};
                if (this.pending_msgs >= this.msg_throttle) {
                    // The throttle has been exceeded, buffer the current msg so
                    // it can be sent once the kernel has finished processing
                    // some of the existing messages.

                    // Combine updates if it is a 'patch' sync, otherwise replace updates
                    switch (method) {
                        case 'patch':
                            this.msg_buffer = _.extend(this.msg_buffer || {}, attrs);
                            break;
                        case 'update':
                        case 'create':
                            this.msg_buffer = attrs;
                            break;
                        default:
                            error();
                            return false;
                    }
                    this.msg_buffer_callbacks = callbacks;

                } else {
                    // We haven't exceeded the throttle, send the message like
                    // normal.  If this is a patch operation, just send the
                    // changes.
                    var data = {method: 'backbone', sync_data: attrs};
                    this.comm.send(data, callbacks);
                    this.pending_msgs++;
                }
            }
            // Since the comm is a one-way communication, assume the message
            // arrived.  Don't call success since we don't have a model back from the server
            // this means we miss out on the 'sync' event.
        },

        save_changes: function(callbacks) {
            // Push this model's state to the back-end
            //
            // This invokes a Backbone.Sync.
            this.save(this.changedAttributes(), {patch: true, callbacks: callbacks});
        },

        _pack_models: function(value) {
            // Replace models with model ids recursively.
            if (value instanceof Backbone.Model) {
                return value.id;
            } else if (value instanceof Object) {
                var packed = {};
                for (var key in value) {
                    packed[key] = this._pack_models(value[key]);
                }
                return packed;
            } else {
                return value;
            }
        },

        _unpack_models: function(value) {
            // Replace model ids with models recursively.
            if (value instanceof Object) {
                var unpacked = {};
                for (var key in value) {
                    unpacked[key] = this._unpack_models(value[key]);
                }
                return unpacked;
            } else {
                var model = this.widget_manager.get_model(value);
                if (model !== null) {
                    return model;
                } else {
                    return value;
                }
            }
        },

    });
    WidgetManager.register_widget_model('WidgetModel', WidgetModel);

    var WidgetView = Backbone.View.extend({
        initialize: function(parameters) {
            // Public constructor.
            this.model.on('change',this.update,this);
            this.options = parameters.options;
            this.child_views = [];
            this.model.views.push(this);
        },

        update: function(){
            // Triggered on model change.
            //
            // Update view to be consistent with this.model
        },

        create_child_view: function(child_model, options) {
            // Create and return a child view.
            //
            // -given a model and (optionally) a view name if the view name is
            // not given, it defaults to the model's default view attribute.

            // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
            // it would be great to have the widget manager add the cell metadata
            // to the subview without having to add it here.
            options = options || {};
            options.cell = this.options.cell;
            var child_view = this.model.widget_manager.create_view(child_model, options);
            this.child_views[child_model.id] = child_view;
            return child_view;
        },

        delete_child_view: function(child_model, options) {
            // Delete a child view that was previously created using create_child_view.
            var view = this.child_views[child_model.id];
            delete this.child_views[child_model.id];
            view.remove();
        },

        do_diff: function(old_list, new_list, removed_callback, added_callback) {
            // Difference a changed list and call remove and add callbacks for
            // each removed and added item in the new list.
            //
            // Parameters
            // ----------
            // old_list : array
            // new_list : array
            // removed_callback : Callback(item)
            //      Callback that is called for each item removed.
            // added_callback : Callback(item)
            //      Callback that is called for each item added.

            // removed items
            _.each(_.difference(old_list, new_list), function(item, index, list) {
                removed_callback(item);
            }, this);

            // added items
            _.each(_.difference(new_list, old_list), function(item, index, list) {
                added_callback(item);
            }, this);
        },

        callbacks: function(){
            // Create msg callbacks for a comm msg.
            return this.model.callbacks(this);
        },

        render: function(){
            // Render the view.
            //
            // By default, this is only called the first time the view is created
        },

        send: function (content) {
            // Send a custom msg associated with this view.
            this.model.send(content, this.callbacks());
        },

        touch: function () {
            this.model.save_changes(this.callbacks());
        },

    });

    var DOMWidgetView = WidgetView.extend({
        initialize: function (options) {
            // Public constructor

            // In the future we may want to make changes more granular
            // (e.g., trigger on visible:change).
            this.model.on('change', this.update, this);
            this.model.on('msg:custom', this.on_msg, this);
            DOMWidgetView.__super__.initialize.apply(this, arguments);
        },

        on_msg: function(msg) {
            // Handle DOM specific msgs.
            switch(msg.msg_type) {
                case 'add_class':
                    this.add_class(msg.selector, msg.class_list);
                    break;
                case 'remove_class':
                    this.remove_class(msg.selector, msg.class_list);
                    break;
            }
        },

        add_class: function (selector, class_list) {
            // Add a DOM class to an element.
            this._get_selector_element(selector).addClass(class_list);
        },

        remove_class: function (selector, class_list) {
            // Remove a DOM class from an element.
            this._get_selector_element(selector).removeClass(class_list);
        },

        update: function () {
            // Update the contents of this view
            //
            // Called when the model is changed.  The model may have been
            // changed by another view or by a state update from the back-end.
            //      The very first update seems to happen before the element is
            // finished rendering so we use setTimeout to give the element time
            // to render
            var e = this.$el;
            var visible = this.model.get('visible');
            setTimeout(function() {e.toggle(visible)},0);

            var css = this.model.get('_css');
            if (css === undefined) {return;}
            for (var selector in css) {
                if (css.hasOwnProperty(selector)) {
                    // Apply the css traits to all elements that match the selector.
                    var elements = this._get_selector_element(selector);
                    if (elements.length > 0) {
                        var css_traits = css[selector];
                        for (var css_key in css_traits) {
                            if (css_traits.hasOwnProperty(css_key)) {
                                elements.css(css_key, css_traits[css_key]);
                            }
                        }
                    }
                }
            }
        },

        _get_selector_element: function (selector) {
            // Get the elements via the css selector.

            // If the selector is blank, apply the style to the $el_to_style
            // element.  If the $el_to_style element is not defined, use apply
            // the style to the view's element.
            var elements;
            if (selector === undefined || selector === null || selector === '') {
                if (this.$el_to_style === undefined) {
                    elements = this.$el;
                } else {
                    elements = this.$el_to_style;
                }
            } else {
                elements = this.$el.find(selector);
            }
            return elements;
        },
    });

    IPython.WidgetModel = WidgetModel;
    IPython.WidgetView = WidgetView;
    IPython.DOMWidgetView = DOMWidgetView;

    // Pass through WidgetManager namespace.
    return WidgetManager;
});
<commit_msg>Prefer JQuery to Underscore<commit_after>//----------------------------------------------------------------------------
//  Copyright (C) 2013 The IPython Development Team
//
//  Distributed under the terms of the BSD License.  The full license is in
//  the file COPYING, distributed as part of this software.
//----------------------------------------------------------------------------

//============================================================================
// Base Widget Model and View classes
//============================================================================

/**
 * @module IPython
 * @namespace IPython
 **/

define(["notebook/js/widgetmanager",
        "underscore",
        "backbone"],
function(WidgetManager, Underscore, Backbone){

    var WidgetModel = Backbone.Model.extend({
        constructor: function (widget_manager, model_id, comm) {
            // Construcctor
            //
            // Creates a WidgetModel instance.
            //
            // Parameters
            // ----------
            // widget_manager : WidgetManager instance
            // model_id : string
            //      An ID unique to this model.
            // comm : Comm instance (optional)
            this.widget_manager = widget_manager;
            this.pending_msgs = 0;
            this.msg_throttle = 2;
            this.msg_buffer = null;
            this.key_value_lock = null;
            this.id = model_id;
            this.views = [];

            if (comm !== undefined) {
                // Remember comm associated with the model.
                this.comm = comm;
                comm.model = this;

                // Hook comm messages up to model.
                comm.on_close($.proxy(this._handle_comm_closed, this));
                comm.on_msg($.proxy(this._handle_comm_msg, this));
            }
            return Backbone.Model.apply(this);
        },

        send: function (content, callbacks) {
            // Send a custom msg over the comm.
            if (this.comm !== undefined) {
                var data = {method: 'custom', content: content};
                this.comm.send(data, callbacks);
            }
        },

        _handle_comm_closed: function (msg) {
            // Handle when a widget is closed.
            this.trigger('comm:close');
            delete this.comm.model; // Delete ref so GC will collect widget model.
            delete this.comm;
            delete this.model_id; // Delete id from model so widget manager cleans up.
            // TODO: Handle deletion, like this.destroy(), and delete views, etc.
        },

        _handle_comm_msg: function (msg) {
            // Handle incoming comm msg.
            var method = msg.content.data.method;
            switch (method) {
                case 'update':
                    this.apply_update(msg.content.data.state);
                    break;
                case 'custom':
                    this.trigger('msg:custom', msg.content.data.content);
                    break;
                case 'display':
                    this.widget_manager.display_view(msg, this);
                    break;
            }
        },

        apply_update: function (state) {
            // Handle when a widget is updated via the python side.
            for (var key in state) {
                if (state.hasOwnProperty(key)) {
                    var value = state[key];
                    this.key_value_lock = [key, value];
                    try {
                        this.set(key, this._unpack_models(value));
                    } finally {
                        this.key_value_lock = null;
                    }
                }
            }
        },

        _handle_status: function (msg, callbacks) {
            // Handle status msgs.

            // execution_state : ('busy', 'idle', 'starting')
            if (this.comm !== undefined) {
                if (msg.content.execution_state ==='idle') {
                    // Send buffer if this message caused another message to be
                    // throttled.
                    if (this.msg_buffer !== null &&
                        this.msg_throttle === this.pending_msgs) {
                        var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
                        this.comm.send(data, callbacks);
                        this.msg_buffer = null;
                    } else {
                        --this.pending_msgs;
                    }
                }
            }
        },

        callbacks: function(view) {
            // Create msg callbacks for a comm msg.
            var callbacks = this.widget_manager.callbacks(view);

            if (callbacks.iopub === undefined) {
                callbacks.iopub = {};
            }

            var that = this;
            callbacks.iopub.status = function (msg) {
                that._handle_status(msg, callbacks);
            }
            return callbacks;
        },

        sync: function (method, model, options) {

            // Make sure a comm exists.
            var error = options.error || function() {
                console.error('Backbone sync error:', arguments);
            }
            if (this.comm === undefined) {
                error();
                return false;
            }

            // Delete any key value pairs that the back-end already knows about.
            var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
            if (this.key_value_lock !== null) {
                var key = this.key_value_lock[0];
                var value = this.key_value_lock[1];
                if (attrs[key] === value) {
                    delete attrs[key];
                }
            }

            // Only sync if there are attributes to send to the back-end.
            if (attr.length > 0) {
                var callbacks = options.callbacks || {};
                if (this.pending_msgs >= this.msg_throttle) {
                    // The throttle has been exceeded, buffer the current msg so
                    // it can be sent once the kernel has finished processing
                    // some of the existing messages.

                    // Combine updates if it is a 'patch' sync, otherwise replace updates
                    switch (method) {
                        case 'patch':
                            this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
                            break;
                        case 'update':
                        case 'create':
                            this.msg_buffer = attrs;
                            break;
                        default:
                            error();
                            return false;
                    }
                    this.msg_buffer_callbacks = callbacks;

                } else {
                    // We haven't exceeded the throttle, send the message like
                    // normal.  If this is a patch operation, just send the
                    // changes.
                    var data = {method: 'backbone', sync_data: attrs};
                    this.comm.send(data, callbacks);
                    this.pending_msgs++;
                }
            }
            // Since the comm is a one-way communication, assume the message
            // arrived.  Don't call success since we don't have a model back from the server
            // this means we miss out on the 'sync' event.
        },

        save_changes: function(callbacks) {
            // Push this model's state to the back-end
            //
            // This invokes a Backbone.Sync.
            this.save(this.changedAttributes(), {patch: true, callbacks: callbacks});
        },

        _pack_models: function(value) {
            // Replace models with model ids recursively.
            if (value instanceof Backbone.Model) {
                return value.id;
            } else if (value instanceof Object) {
                var packed = {};
                for (var key in value) {
                    packed[key] = this._pack_models(value[key]);
                }
                return packed;
            } else {
                return value;
            }
        },

        _unpack_models: function(value) {
            // Replace model ids with models recursively.
            if (value instanceof Object) {
                var unpacked = {};
                for (var key in value) {
                    unpacked[key] = this._unpack_models(value[key]);
                }
                return unpacked;
            } else {
                var model = this.widget_manager.get_model(value);
                if (model !== null) {
                    return model;
                } else {
                    return value;
                }
            }
        },

    });
    WidgetManager.register_widget_model('WidgetModel', WidgetModel);

    var WidgetView = Backbone.View.extend({
        initialize: function(parameters) {
            // Public constructor.
            this.model.on('change',this.update,this);
            this.options = parameters.options;
            this.child_views = [];
            this.model.views.push(this);
        },

        update: function(){
            // Triggered on model change.
            //
            // Update view to be consistent with this.model
        },

        create_child_view: function(child_model, options) {
            // Create and return a child view.
            //
            // -given a model and (optionally) a view name if the view name is
            // not given, it defaults to the model's default view attribute.

            // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
            // it would be great to have the widget manager add the cell metadata
            // to the subview without having to add it here.
            options = options || {};
            options.cell = this.options.cell;
            var child_view = this.model.widget_manager.create_view(child_model, options);
            this.child_views[child_model.id] = child_view;
            return child_view;
        },

        delete_child_view: function(child_model, options) {
            // Delete a child view that was previously created using create_child_view.
            var view = this.child_views[child_model.id];
            delete this.child_views[child_model.id];
            view.remove();
        },

        do_diff: function(old_list, new_list, removed_callback, added_callback) {
            // Difference a changed list and call remove and add callbacks for
            // each removed and added item in the new list.
            //
            // Parameters
            // ----------
            // old_list : array
            // new_list : array
            // removed_callback : Callback(item)
            //      Callback that is called for each item removed.
            // added_callback : Callback(item)
            //      Callback that is called for each item added.

            // removed items
            _.each(_.difference(old_list, new_list), function(item, index, list) {
                removed_callback(item);
            }, this);

            // added items
            _.each(_.difference(new_list, old_list), function(item, index, list) {
                added_callback(item);
            }, this);
        },

        callbacks: function(){
            // Create msg callbacks for a comm msg.
            return this.model.callbacks(this);
        },

        render: function(){
            // Render the view.
            //
            // By default, this is only called the first time the view is created
        },

        send: function (content) {
            // Send a custom msg associated with this view.
            this.model.send(content, this.callbacks());
        },

        touch: function () {
            this.model.save_changes(this.callbacks());
        },

    });

    var DOMWidgetView = WidgetView.extend({
        initialize: function (options) {
            // Public constructor

            // In the future we may want to make changes more granular
            // (e.g., trigger on visible:change).
            this.model.on('change', this.update, this);
            this.model.on('msg:custom', this.on_msg, this);
            DOMWidgetView.__super__.initialize.apply(this, arguments);
        },

        on_msg: function(msg) {
            // Handle DOM specific msgs.
            switch(msg.msg_type) {
                case 'add_class':
                    this.add_class(msg.selector, msg.class_list);
                    break;
                case 'remove_class':
                    this.remove_class(msg.selector, msg.class_list);
                    break;
            }
        },

        add_class: function (selector, class_list) {
            // Add a DOM class to an element.
            this._get_selector_element(selector).addClass(class_list);
        },

        remove_class: function (selector, class_list) {
            // Remove a DOM class from an element.
            this._get_selector_element(selector).removeClass(class_list);
        },

        update: function () {
            // Update the contents of this view
            //
            // Called when the model is changed.  The model may have been
            // changed by another view or by a state update from the back-end.
            //      The very first update seems to happen before the element is
            // finished rendering so we use setTimeout to give the element time
            // to render
            var e = this.$el;
            var visible = this.model.get('visible');
            setTimeout(function() {e.toggle(visible)},0);

            var css = this.model.get('_css');
            if (css === undefined) {return;}
            for (var selector in css) {
                if (css.hasOwnProperty(selector)) {
                    // Apply the css traits to all elements that match the selector.
                    var elements = this._get_selector_element(selector);
                    if (elements.length > 0) {
                        var css_traits = css[selector];
                        for (var css_key in css_traits) {
                            if (css_traits.hasOwnProperty(css_key)) {
                                elements.css(css_key, css_traits[css_key]);
                            }
                        }
                    }
                }
            }
        },

        _get_selector_element: function (selector) {
            // Get the elements via the css selector.

            // If the selector is blank, apply the style to the $el_to_style
            // element.  If the $el_to_style element is not defined, use apply
            // the style to the view's element.
            var elements;
            if (selector === undefined || selector === null || selector === '') {
                if (this.$el_to_style === undefined) {
                    elements = this.$el;
                } else {
                    elements = this.$el_to_style;
                }
            } else {
                elements = this.$el.find(selector);
            }
            return elements;
        },
    });

    IPython.WidgetModel = WidgetModel;
    IPython.WidgetView = WidgetView;
    IPython.DOMWidgetView = DOMWidgetView;

    // Pass through WidgetManager namespace.
    return WidgetManager;
});
<|endoftext|>