<commit_before>/*

Copyright (c) 2010 Shahbaz Chaudhary

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

*/
//TODO:
//Server should keep track of who is logged on
//Allow server to send messages to individual connections
//Check duplicate senderCompIDs
//Grouped fields
//Drop copy
//expire messages (for resend, send gap fill instead)

//DOING:
//Persistence with node-dirty--how to recover from crash
//while recovering, read in stored messages as 'pos-dup"
//update sequence numbers

var net = require("net");
var events = require("events");
var sys = require("sys");
var logger = require('./lib/logger').createLogger();
var Dirty = require('./lib/dirty').Dirty;
var tags = require('./resources/fixtagnums').keyvals;

//logger.setLevel('debug');

//Message container

//Upon resend request, messages are retreived from this container and replayed.
//{targetCompiD:{incoming:[], outgoing:[]}} //an object containing id of the counter party and their incoming and outgoing messages
var msgContainer = {};

function addInMsg(target, msg){
    var tgt = msgContainer[target];
    if(tgt === undefined){
        msgContainer[target] = {incoming:[msg], outgoing:[]};
    }
    else{
        msgContainer[target].incoming.push(msg);
    }
}

function addOutMsg(target, msg){
    var tgt = msgContainer[target];
    if(tgt === undefined){
        msgContainer[target] = {incoming:[], outgoing:[msg]};
    }
    else{
        msgContainer[target].outgoing.push(msg);
    }
}

function getInMessages(target, beginSeqNo, endSeqNo){
    var tgt = msgContainer[target];
    if(tgt === undefined){
        return [];
    }
    else{
        var msgs = tgt.incoming;
        var msgsarr = [];
        for(var k in msgs){
            var msg = msgs[k];
            var seqNo = parseInt(msg[tags["MsgSeqNum"]],10);
            if(seqNo >= beginSeqNo && seqNo <= endSeqNo){
                msgarr.push(msg);
            }
        }
    }
}

function getOutMessages(target, beginSeqNo, endSeqNo){
    var tgt = msgContainer[target];
    if(tgt === undefined){
        return [];
    }
    else{
        var msgs = tgt.outgoing;
        var msgsarr = [];
        for(var k in msgs){
            var msg = msgs[k];
            var seqNo = parseInt(msg[tags["MsgSeqNum"]],10);
            if(seqNo >= beginSeqNo && seqNo <= endSeqNo){
                msgarr.push(msg);
            }
        }
    }
}
//End Message Container Code

//Utility methods

var tag2txt = function(msg){ return Object.keys(msg).map(function(key){return tags[key]+"="+msg[key];}).join("|");}

logger.format = function(level, timestamp, message) {
  return [timestamp.getUTCFullYear() ,"/", timestamp.getUTCMonth() ,"/", timestamp.getUTCDay() , "-" , timestamp.getUTCHours() , ":" , timestamp.getUTCMinutes() , ":" , timestamp.getUTCSeconds() , "." , timestamp.getUTCMilliseconds() , " [" , level, "] ",  message].join("");
};

function checksum(str){
    var chksm = 0;
    for(var i=0; i<str.length; i++){
        chksm += str.charCodeAt(i);
    }

    chksm = chksm % 256;

    var checksumstr = "";
    if (chksm < 10) {
        checksumstr = "00" + chksm;
    }
    else if (chksm >= 10 && chksm < 100) {
        checksumstr = "0" + chksm;
    }
    else {
        checksumstr = "" + chksm;
    }

    return checksumstr;
}

//static vars
var SOHCHAR = String.fromCharCode(1);
var ENDOFTAG8 = 10;
var STARTOFTAG9VAL = ENDOFTAG8 + 2;
var SIZEOFTAG10 = 8;

function Session(stream, isInitiator,  opt) {
    events.EventEmitter.call(this);
    var self = this;

    stream.setEncoding("ascii");
    stream.setTimeout(1000);

    this.stream = stream;
    var fixVersion = opt.version;
    var headers = opt.headers;
    var trailers = opt.trailers;

    //session vars
    var senderCompID = ""; //senderCompID || "";
    var targetCompID = ""; //targetCompID || "";
    var heartbeatDuration = 0;

    var databuffer = "";
    var charlen = 0;

    var loggedIn = false;
    var incomingSeqNum = 1;
    var outgoingSeqNum = 1;
    var timeOfLastIncoming = 0;
    var timeOfLastOutgoing = 0;
    var resendRequested = false;

    var heartbeatIntervalID ;

    var datastore ;

    this.addListener("end", function () {
        clearInterval(heartbeatIntervalID);
        this.emit("logoff", targetCompID);
    });

    var heartbeatCallback = function () {

        var currentTime = new Date().getTime();

        if (currentTime - timeOfLastOutgoing > heartbeatDuration) {
            writefix({
                "35": "0"
            }); /*write heartbeat*/
        }

        if (currentTime - timeOfLastIncoming > heartbeatDuration * 1.5) {
            writefix({
                "35": "1",
                "112": outgoingSeqNum + ""
            }); /*write testrequest*/
        }

        if (currentTime - timeOfLastIncoming > heartbeatDuration * 3) {
            logger.error("[ERROR] No message received from counterparty and no response to test request.");
            stream.end();
            return;
        }
    };

    //Used for parsing outgoing data -------------------------------
    //this.write = function(msg){ writefix(msg);};
    function writefix(msg) {

        var senderCompIDExtracted = msg[tags["TargetCompID"]];
        var targetCompIDExtracted = msg[tags["SenderCompID"]];

        delete msg[tags["BodyLength"]]; //bodylength
        delete msg[tags["CheckSum"]]; //checksum
        delete msg[tags["SendingTime"]]; //timestamp
        delete msg[tags["BeginString"]]; //fixversion
        delete msg[tags["TargetCompID"]]; //sendercompid
        delete msg[tags["SenderCompID"]]; //targetcompid
        delete msg[tags["MsgSeqNum"]]; //seqnum
        var headermsgarr = [];
        for (var f in Object.keys(headers)) {
            //if (headers.hasOwnProperty(f)) {
                var tag = headers[f];

                if (tag == "8" || tag == "9" || tag == "59" || tag == "52" || tag == "56" || tag == "49" || tag == "34") {
                    continue;
                }

                if (tag.charAt(tag.length - 1) != "?" && msg[tag] === undefined) { //If tag is required, but missing
                    logger.error("[ERROR] tag " + tag + " is required but missing in outgoing message: " + msg);
                    return;
                }

                if (msg[tag] !== undefined) {
                    headermsgarr.push(tag, "=", msg[tag], SOHCHAR);
                    delete msg[tag];
                }
            //}
        }

        var timestamp = new Date();
        headermsgarr.push("52=" , timestamp.getUTCFullYear() , timestamp.getUTCMonth() , timestamp.getUTCDay() , "-" , timestamp.getUTCHours() , ":" , timestamp.getUTCMinutes() , ":" , timestamp.getUTCSeconds() , "." , timestamp.getUTCMilliseconds() , SOHCHAR);
        headermsgarr.push("56=" , (senderCompIDExtracted || senderCompID) , SOHCHAR);
        headermsgarr.push("49=" , (targetCompIDExtracted || targetCompID) , SOHCHAR);
        headermsgarr.push("34=" , (outgoingSeqNum++) , SOHCHAR);

        var trailermsgarr = [];
        for (var f in Object.keys(trailers)) {
            //if (trailers.hasOwnProperty(f)) {
                var tag = trailers[f];

                if (tag == "10") {
                    continue;
                }

                if (tag.charAt(tag.length - 1) != "?" && msg[tag] === undefined) { //If tag is required, but missing
                    logger.error("[ERROR] tag " + tag + " is required but missing in outgoing message: " + msg);
                    return;
                }

                if (msg[tag] !== undefined) {
                    trailermsgarr.push(tag , "=" , msg[tag] , SOHCHAR);
                    delete msg[tag];
                }

            //}
        }

        var bodymsgarr = [];
        for (var tag in msg) {
            if (msg.hasOwnProperty(tag)) {
                bodymsgarr.push( tag , "=" , msg[tag] , SOHCHAR);
            }
        }

        var headermsg = headermsgarr.join("");
        var trailermsg = trailermsgarr.join("");
        var bodymsg = bodymsgarr.join("");

        var outmsgarr = [];
        outmsgarr.push( "8=" , fixVersion , SOHCHAR);
        outmsgarr.push( "9=" , (headermsg.length + bodymsg.length + trailermsg.length) , SOHCHAR);
        outmsgarr.push( headermsg);
        outmsgarr.push( bodymsg);
        outmsgarr.push( trailermsg);

        var outmsg = outmsgarr.join("");

        outmsg += "10=" + checksum(outmsg) + SOHCHAR;

        logger.info("FIX out:" + outmsg);
        timeOfLastOutgoing = new Date().getTime();

        //addOutMsg(targetCompID, outmsg);

        stream.write(outmsg);
    }

    this.write = writefix;

    //Used for parsing incoming data -------------------------------
    function handlefix(data){

        //logger.info("++++++++data received: " + data);

        //Add data to the buffer (to avoid processing fragmented TCP packets)
        databuffer = databuffer + data;
        timeOfLastIncoming = new Date().getTime();

        while (databuffer.length > 0) {
            //logger.info("-------NEW LOOP:" + databuffer.length + ":" + databuffer);

            //====Step 1: Extract complete FIX message====
            //If we don't have enough data to start extracting body length, wait for more data
            if (databuffer.length <= ENDOFTAG8) {
                return;
            }

            var _idxOfEndOfTag9Str = databuffer.substring(ENDOFTAG8).indexOf(SOHCHAR);
            var idxOfEndOfTag9 = parseInt(_idxOfEndOfTag9Str, 10) + ENDOFTAG8;

            if (isNaN(idxOfEndOfTag9)) {
                logger.error("[ERROR] Unable to find the location of the end of tag 9. Message probably misformed: " + databuffer.toString());
                stream.end();
                return;
            }

            //If we don't have enough data to stop extracting body length AND we have received a lot of data
            //then perhaps there is a problem with how the message is formatted and the session should be killed
            if (idxOfEndOfTag9 < 0 && databuffer.length > 100) {
                logger.error("[ERROR] Over 100 character received but body length still not extractable.  Message probably misformed: " + databuffer.toString());
                stream.end();
                return;
            }

            //If we don't have enough data to stop extracting body length, wait for more data
            if (idxOfEndOfTag9 < 0) {
                return;
            }

            var _bodyLengthStr = databuffer.substring(STARTOFTAG9VAL, idxOfEndOfTag9);
            var bodyLength = parseInt(_bodyLengthStr, 10);
            if (isNaN(bodyLength)) {
                logger.error("[ERROR] Unable to parse bodyLength field. Message probably misformed: " + databuffer.toString());
                stream.end();
                return;
            }

            var msgLength = bodyLength + idxOfEndOfTag9 + SIZEOFTAG10;

            //If we don't have enough data for the whole message, wait for more data
            if (databuffer.length < msgLength) {
                return;
            }

            var msg = databuffer.substring(0, msgLength);
            if (msgLength == databuffer.length) {
                databuffer = "";
            }
            else {
                var debugstr = databuffer.substring(msgLength);
                //logger.info("[DEBUG] debugstr:" + debugstr);
                databuffer = debugstr;
            }

            logger.info("FIX in: " + msg);

            //====Step 2: Validate message====
            var calculatedChecksum = checksum(msg.substr(0,msg.length - 7));
            var extractedChecksum = msg.substr(msg.length - 4, 3);

            if (calculatedChecksum !== extractedChecksum) {
                logger.warn("[WARNING] Discarding message because body length or checksum are wrong (expected checksum: "+calculatedChecksum+"): " + msg);
                continue;
            }

            //====Step 3: Convert to map====
            var keyvals = msg.split(SOHCHAR);
            //sys.debug("keyvals:"+keyvals);
            var fix = {};
            for (var kv in Object.keys(keyvals)) {
                //if (keyvals.hasOwnProperty(kv)) {
                    var kvpair = keyvals[kv].split("=");
                    fix[kvpair[0]] = kvpair[1];
                //}
            }

            //====Step 4: Confirm all required fields are available====
            for (var f in Object.keys(headers)) {
                //if (headers.hasOwnProperty(f)) {
                    var tag = headers[f];
                    if (tag.charAt(tag.length - 1) != "?" && fix[tag] === undefined) { //If tag is required, but missing
                        logger.error("[ERROR] tag " + tag + " is required but missing in incoming message: " + msg);
                        if (loggedIn) {
                            writefix({
                                "35": "3",
                                "45": fix[tags["MsgSeqNum"]],
                                "58": "MissingTags"
                            }); /*write session reject*/
                        }
                        else {
                            stream.end();
                            return;
                        }
                    }
                //}
            }

            for (var f in Object.keys(trailers)) {
                //if (trailers.hasOwnProperty(f)) {
                    var tag = trailers[f];
                    if (tag.charAt(tag.length - 1) != "?" && fix[tag] === undefined) { //If tag is required, but missing
                        logger.error("[ERROR] tag " + tag + " is required but missing in incoming message: " + msg);
                        if (loggedIn) {
                            writefix({
                                "35": "3",
                                "45": fix[tags["MsgSeqNum"]],
                                "58": "MissingTags"
                            }); /*write session reject*/
                        }
                        else {
                            stream.end();
                            return;
                        }
                    }
                //}
            }

            //====Step 5: Confirm first message is a logon message and it has a heartbeat
            var msgType = fix[tags["MsgType"]];
            if (!loggedIn && msgType != "A") {
                logger.error("[ERROR] Logon message expected, received message of type " + msgType);
                stream.end();
                return;
            }

            if (msgType == "A" && fix[tags["HeartBtInt"]] === undefined) {
                logger.error("[ERROR] Logon does not have tag 108 (heartbeat) ");
                stream.end();
                return;
            }

            //====Step 6: Confirm incoming sequence number====
            var _seqNum = parseInt(fix[tags["MsgSeqNum"]], 10);
            if(fix[tags["MsgType"]]==="4" /*seq reset*/ && (fix[tags["GapFillFlag"]] === undefined || fix[tags["GapFillFlag"]] === "N")){
                logger.warn("Requence Reset request received: " + msg);
                var resetseqno = parseInt(fix[tags["NewSeqNo"]],10);
                if(resetseqno <= incomingSeqnum){
                    //TODO: Reject, sequence number may only be incremented
                }
                else{
                    incomingSeqNum = resetseqno;
                }
            }
            if (loggedIn && _seqNum == incomingSeqNum) {
                incomingSeqNum++;
                resendRequested = false;
            }
            else if (loggedIn && _seqNum < incomingSeqNum) {
                var posdup = fix[tags["PossDupFlag"]];
                if(posdup !== undefined && posdup === "Y"){
                    logger.warn("This posdup message's seqno has already been processed. Ignoring: "+msg);
                }
                logger.error("[ERROR] Incoming sequence number lower than expected. No way to recover:"+msg);
                stream.end();
                return;
            }
            else if (loggedIn && _seqNum > incomingSeqNum) {
                //Missing messages, write resend request and don't process any more messages
                //until the rewrite request is processed
                //set flag saying "waiting for rewrite"
                if(resendRequested !== true){
                    resendRequested = true;
                    writefix({"35":2, "7":incomingSeqNum, "8":0});
                }
            }

            //====Step 7: Confirm compids and fix version match what was in the logon msg
            var incomingFixVersion = fix[tags["BeginString"]];
            var incomingsenderCompID = fix[tags["TargetCompID"]];
            var incomingTargetCompID = fix[tags["SenderCompID"]];

            if (loggedIn && (fixVersion != incomingFixVersion || senderCompID != incomingsenderCompID || targetCompID != incomingTargetCompID)) {
                logger.warn("[WARNING] Incoming fix version (" + incomingFixVersion + "), sender compid (" + incomingsenderCompID + ") or target compid (" + incomingTargetCompID + ") did not match expected values (" + fixVersion + "," + senderCompID + "," + targetCompID + ")"); /*write session reject*/
            }

            //===Step 8: Record incoming message -- might be needed during resync
            if(loggedIn){
                datastore.add(fix);
                //addInMsg(targetCompID, fix);
            }
            logger.debug("FIXMAP in: " + tag2txt(fix));

            //====Step 9: Messages
            switch (msgType) {
                case "0":
                    //handle heartbeat; break;
                    break;
                case "1":
                    //handle testrequest; break;
                    var testReqID = fix[tags["TestReqID"]];
                    writefix({
                        "35": "0",
                        "112": testReqID
                    }); /*write heartbeat*/
                    break;
                case "2":
                    var beginSeqNo = parseInt(fix[tags["BeginSeqNo"]],10);
                    var endSeqNo = parseInt(fix[tags["EndSeqNo"]],10);
                    outgoingSeqNum = beginSeqNo;
                    var outmsgs = getOutMessages(targetCompID, beginSeqNo, endSeqNo);
                    for(var k in outmsgs){
                        var resendmsg = msgs[k];
                        resendmsg[tags["PossDupFlag"]] = "Y";
                        resendmsg[tags["OrigSendingTime"]] = resendmsg["SendingTime"];
                        writefix(resendmsg);
                    }
                    //handle resendrequest; break;
                    break;
                case "3":
                    //handle sessionreject; break;
                    break;
                case "4":
                    //Gap fill mode
                    if(fix[tags["GapFillFlag"]] === "Y"){
                        var newSeqNo = parseInt(fix[tags["NewSeqNo"]],10);

                        if(newSeqNo <= incomingSeqNo){
                            //TODO: Reject, sequence number may only be incremented
                        }
                        else{
                            incomingSeqNo = newSeqNo;
                        }
                    }
                    break;
                    //Reset mode
                    //Reset mode is handled in step 6, when confirming incoming seqnums
                    //handle seqreset; break;
                case "5":
                    //handle logout; break;
                    writefix({
                        "35": "5"
                    }); /*write a logout ack right back*/
                    break;
                case "A":
                    //handle logon; break;
                    fixVersion = fix[tags["BeginString"]];
                    senderCompID = fix[tags["TargetCompID"]];
                    targetCompID = fix[tags["SenderCompID"]];

                    //create data store
                    datastore = new Dirty(senderCompID + '-' + targetCompID + '-' + fixVersion + '.dat');
                    datastore.add(fix);

                    heartbeatDuration = parseInt(fix[tags["HeartBtInt"]], 10) * 1000;
                    loggedIn = true;
                    heartbeatIntervalID = setInterval(heartbeatCallback, heartbeatDuration);
                    //heartbeatIntervalIDs.push(intervalID);
                    this.emit("logon", targetCompID,stream);
                    logger.info(fix[tags["SenderCompID"]] + " logged on from " + stream.remoteAddress);

                    if(isInitiator === true){
                        writefix({
                            "35": "A",
                            "108": fix[tags["HeartBtInt"]]
                        }); /*write logon ack*/
                    }
                    break;
                default:
            }
            this.emit("data", fix);
        }

    }

    this.handle = handlefix;
    //this.handleTest = function(){ return handlefix;};

    //this.addListener("data", handlefix);

}
sys.inherits(Session, events.EventEmitter);
//Session.prototype.write = function(msg){ this.writefix(msg);};

//---------------------SERVER
function Server() {
    events.EventEmitter.call(this);
    this.clients = {};
    //this.addListener("logon", function(client,stream) { clients[client]=stream; } );

}
sys.inherits(Server, events.EventEmitter);
Server.prototype.listen = function (port) { this.socket.listen(port); };
Server.prototype.write = function(client, msg) { this.clients[client].write(msg); };
Server.prototype.end = function(client){ this.clients[client].end(); };

//---------------------CLIENT
function Client(senderCompID, targetCompID, opt) {
    events.EventEmitter.call(this);

}
sys.inherits(Client, events.EventEmitter);
Client.prototype.end = function () { this.socket.end(); };

//---------------------EXPORTS
//events: connect, end, data, logon
exports.createServer = function (opt, func) {

    var server = new Server();

    server.socket = net.createServer(function (stream) {
        var session = new Session(stream, true, opt);
        func(session);

        stream.addListener("connect", function () {
            session.emit("connect");
        });

        stream.addListener("end", function () {
            stream.end();
            session.emit("end");
        });

        stream.addListener("data", function(data){session.handle(data);});

        session.addListener("logon", function(client,strm){
                server.clients[client]=strm;
                sys.log("------"+ Object.keys(server.clients));
            }
        );
        session.addListener("logoff", function(client){
                delete server.clients[client];
            }
        );

    });

    return server;
};

exports.createConnection = function (senderCompID, targetCompID, heartbeatseconds, opt, port, host) {

    var client = new Client(senderCompID, targetCompID, opt);

    var stream = net.createConnection(port, host);

    var session = new Session(stream, false, opt);

    client.stream = stream;
    stream.addListener("connect", function () {
        session.emit("connect");
        session.write({
            "35": "A",
            "49": senderCompID,
            "56": targetCompID,
            "108": heartbeatseconds,
            "98": 0
        });
    });
    stream.addListener("end", function () {
        session.emit("end");
    });

    stream.addListener("data", function(data){session.handle(data);});

    return session;
};

//Copyright 2010 Shahbaz Chaudhary (shahbazc gmail com)
<commit_msg>deleting fixold.js, no longer needed<commit_after><|endoftext|>


<commit_before>({
    baseUrl: "src",
    out: "bundle.js",
    name: "almond",
    include: "patterns",

    wrap: {
        endFile: "src/wrap-end.js"
    },

    paths: {
        // Externals
        "almond": "bower_components/almond/almond",
        "jquery": "bower_components/jquery/jquery",
        "logging": "bower_components/logging/src/logging",
        "jquery.form": "bower_components/jquery-form/jquery.form",
        "jquery.anythingslider": "bower_components/AnythingSlider/js/jquery.anythingslider",
        "jcrop": "bower_components/jcrop/js/jquery.Jcrop",
        "klass": "bower_components/klass/src/klass",
        "photoswipe": "legacy/photoswipe",
        "parsley": "bower_components/parsleyjs/parsley",
        "parsley.extend": "bower_components/parsleyjs/parsley.extend",
        "patternslib.slides": "bower_components/slides/src/slides",
        "modernizr": "bower_components/modernizr/modernizr",
        "less": "bower_components/less.js/dist/less-1.6.2",
        "prefixfree": "bower_components/prefixfree/prefixfree.min",
        "Markdown.Converter": "legacy/Markdown.Converter",
        "Markdown.Extra": "legacy/Markdown.Extra",
        "Markdown.Sanitizer": "legacy/Markdown.Sanitizer",
        "imagesloaded":  "bower_components/imagesloaded/imagesloaded",
        "eventEmitter/EventEmitter": "bower_components/eventEmitter/EventEmitter",
        "eventie/eventie": "bower_components/eventie/eventie",
        "jquery.chosen": "bower_components/chosen/chosen/chosen.jquery",
        "jquery.fullcalendar": "bower_components/fullcalendar/fullcalendar.min",
        "jquery.fullcalendar.dnd": "bower_components/fullcalendar/lib/jquery-ui.custom.min",
        "jquery.placeholder": "bower_components/jquery-placeholder/jquery.placeholder.min",
        "jquery.textchange": "bower_components/jquery-textchange/jquery.textchange",
        "select2": "bower_components/select2/select2.min",
        "tinymce": "bower_components/jquery.tinymce/jscripts/tiny_mce/jquery.tinymce",
        "spectrum": "bower_components/spectrum/spectrum",

        // Core
        "pat-utils": "core/utils",
        "pat-pluggable": "core/pluggable",
        "pat-compat": "core/compat",
        "pat-jquery-ext": "core/jquery-ext",
        "pat-logger": "core/logger",
        "pat-parser": "core/parser",
        "pat-remove": "core/remove",
        "pat-url": "core/url",
        "pat-store": "core/store",
        "pat-registry": "core/registry",
        "pat-htmlparser": "lib/htmlparser",
        "pat-depends_parse": "lib/depends_parse",
        "pat-dependshandler": "lib/dependshandler",
        "pat-input-change-events": "lib/input-change-events",

        // Patterns
        "patterns": "patterns",
        "pat-ajax": "pat/ajax",
        "pat-autofocus": "pat/autofocus",
        "pat-autoscale": "pat/autoscale",
        "pat-autosubmit": "pat/autosubmit",
        "pat-autosuggest": "pat/autosuggest",
        "pat-breadcrumbs": "pat/breadcrumbs",
        "pat-bumper": "pat/bumper",
        "pat-carousel": "pat/carousel",
        "pat-checkedflag": "pat/checkedflag",
        "pat-checklist": "pat/checklist",
        "pat-chosen": "pat/chosen",
        "pat-collapsible": "pat/collapsible",
        "pat-colour-picket": "pat/colour-picker",
        "pat-depends": "pat/depends",
        "pat-edit-tinymce": "pat/edit-tinymce",
        "pat-equaliser": "pat/equaliser",
        "pat-expandable": "pat/expandable",
        "pat-focus": "pat/focus",
        "pat-formstate": "pat/form-state",
        "pat-forward": "pat/forward",
        "pat-fullcalendar": "pat/fullcalendar",
        "pat-gallery": "pat/gallery",
        "pat-image-crop": "pat/image-crop",
        "pat-inject": "pat/inject",
        "pat-legend": "pat/legend",
        "pat-markdown": "pat/markdown",
        "pat-menu": "pat/menu",
        "pat-modal": "pat/modal",
        "pat-navigation": "pat/navigation",
        "pat-notification": "pat/notification",
        "pat-placeholder": "pat/placeholder",
        "pat-selectbox": "pat/selectbox",
        "pat-skeleton": "pat/skeleton",
        "pat-slides": "pat/slides",
        "pat-slideshow-builder": "pat/slideshow-builder",
        "pat-sortable": "pat/sortable",
        "pat-stacks": "pat/stacks",
        "pat-subform": "pat/subform",
        "pat-switch": "pat/switch",
        "pat-toggle": "pat/toggle",
        "pat-tooltip": "pat/tooltip",
        "pat-validate": "pat/validate",
        "pat-zoom": "pat/zoom",

        // Calendar pattern
        "moment": "bower_components/moment/moment",
        "moment-timezone": "bower_components/moment-timezone/moment-timezone",
        "pat-calendar": "pat/calendar/calendar",
        "pat-calendar-dnd": "pat/calendar/dnd",
        "pat-calendar-moment-timezone-data": "pat/calendar/moment-timezone-data"
    },

    shim: {
        "jquery": {
            exports: "jQuery"
        },
        "jquery.fullcalendar.dnd": {
            depends: "jQuery",
        },
        "tinymce": {
            depends: "jquery"
        },
        "photoswipe": {
            depends: "klass",
        }
    },

    optimize: "none"
})
<commit_msg>fixed build.js<commit_after>({
    baseUrl: "src",
    out: "bundle.js",
    name: "almond",
    include: "patterns",

    wrap: {
        endFile: "src/wrap-end.js"
    },

    paths: {
        // Externals
        "almond": "bower_components/almond/almond",
        "jquery": "bower_components/jquery/jquery",
        "logging": "bower_components/logging/src/logging",
        "jquery.form": "bower_components/jquery-form/jquery.form",
        "jquery.anythingslider": "bower_components/AnythingSlider/js/jquery.anythingslider",
        "jcrop": "bower_components/jcrop/js/jquery.Jcrop",
        "klass": "bower_components/klass/src/klass",
        "photoswipe": "legacy/photoswipe",
        "parsley": "bower_components/parsleyjs/parsley",
        "parsley.extend": "bower_components/parsleyjs/parsley.extend",
        "patternslib.slides": "bower_components/slides/src/slides",
        "modernizr": "bower_components/modernizr/modernizr",
        "less": "bower_components/less.js/dist/less-1.6.2",
        "prefixfree": "bower_components/prefixfree/prefixfree.min",
        "Markdown.Converter": "legacy/Markdown.Converter",
        "Markdown.Extra": "legacy/Markdown.Extra",
        "Markdown.Sanitizer": "legacy/Markdown.Sanitizer",
        "imagesloaded":  "bower_components/imagesloaded/imagesloaded",
        "eventEmitter/EventEmitter": "bower_components/eventEmitter/EventEmitter",
        "eventie/eventie": "bower_components/eventie/eventie",
        "jquery.chosen": "bower_components/chosen/chosen/chosen.jquery",
        "jquery.fullcalendar": "bower_components/fullcalendar/fullcalendar.min",
        "jquery.fullcalendar.dnd": "bower_components/fullcalendar/lib/jquery-ui.custom.min",
        "jquery.placeholder": "bower_components/jquery-placeholder/jquery.placeholder.min",
        "jquery.textchange": "bower_components/jquery-textchange/jquery.textchange",
        "select2": "bower_components/select2/select2.min",
        "tinymce": "bower_components/jquery.tinymce/jscripts/tiny_mce/jquery.tinymce",
        "spectrum": "bower_components/spectrum/spectrum",
        "masonry": "bower_components/masonry/masonry",
        "outlayer": "bower_components/outlayer",
        "doc-ready": "bower_components/doc-ready",
        "get-size": "bower_components/get-size",
        "get-style-property": "bower_components/get-style-property",
        "matches-selector": "bower_components/matches-selector",

        // Core
        "pat-utils": "core/utils",
        "pat-pluggable": "core/pluggable",
        "pat-compat": "core/compat",
        "pat-jquery-ext": "core/jquery-ext",
        "pat-logger": "core/logger",
        "pat-parser": "core/parser",
        "pat-remove": "core/remove",
        "pat-url": "core/url",
        "pat-store": "core/store",
        "pat-registry": "core/registry",
        "pat-htmlparser": "lib/htmlparser",
        "pat-depends_parse": "lib/depends_parse",
        "pat-dependshandler": "lib/dependshandler",
        "pat-input-change-events": "lib/input-change-events",

        // Patterns
        "patterns": "patterns",
        "pat-ajax": "pat/ajax",
        "pat-autofocus": "pat/autofocus",
        "pat-autoscale": "pat/autoscale",
        "pat-autosubmit": "pat/autosubmit",
        "pat-autosuggest": "pat/autosuggest",
        "pat-breadcrumbs": "pat/breadcrumbs",
        "pat-bumper": "pat/bumper",
        "pat-carousel": "pat/carousel",
        "pat-checkedflag": "pat/checkedflag",
        "pat-checklist": "pat/checklist",
        "pat-chosen": "pat/chosen",
        "pat-collapsible": "pat/collapsible",
        "pat-colour-picket": "pat/colour-picker",
        "pat-depends": "pat/depends",
        "pat-edit-tinymce": "pat/edit-tinymce",
        "pat-equaliser": "pat/equaliser",
        "pat-expandable": "pat/expandable",
        "pat-focus": "pat/focus",
        "pat-formstate": "pat/form-state",
        "pat-forward": "pat/forward",
        "pat-fullcalendar": "pat/fullcalendar",
        "pat-gallery": "pat/gallery",
        "pat-image-crop": "pat/image-crop",
        "pat-inject": "pat/inject",
        "pat-legend": "pat/legend",
        "pat-markdown": "pat/markdown",
        "pat-masonry": "pat/masonry",
        "pat-menu": "pat/menu",
        "pat-modal": "pat/modal",
        "pat-navigation": "pat/navigation",
        "pat-notification": "pat/notification",
        "pat-placeholder": "pat/placeholder",
        "pat-selectbox": "pat/selectbox",
        "pat-skeleton": "pat/skeleton",
        "pat-slides": "pat/slides",
        "pat-slideshow-builder": "pat/slideshow-builder",
        "pat-sortable": "pat/sortable",
        "pat-stacks": "pat/stacks",
        "pat-subform": "pat/subform",
        "pat-switch": "pat/switch",
        "pat-toggle": "pat/toggle",
        "pat-tooltip": "pat/tooltip",
        "pat-validate": "pat/validate",
        "pat-zoom": "pat/zoom",

        // Calendar pattern
        "moment": "bower_components/moment/moment",
        "moment-timezone": "bower_components/moment-timezone/moment-timezone",
        "pat-calendar": "pat/calendar/calendar",
        "pat-calendar-dnd": "pat/calendar/dnd",
        "pat-calendar-moment-timezone-data": "pat/calendar/moment-timezone-data"
    },

    shim: {
        "jquery": {
            exports: "jQuery"
        },
        "jquery.fullcalendar.dnd": {
            depends: "jQuery",
        },
        "tinymce": {
            depends: "jquery"
        },
        "photoswipe": {
            depends: "klass",
        }
    },

    optimize: "none"
})
<|endoftext|>


<commit_before>/**
 * @module mapping
 */

var Store = require('./store'),
    SiestaModel = require('./modelInstance'),
    log = require('./operation/log'),
    Operation = require('./operation/operation').Operation,
    InternalSiestaError = require('./error').InternalSiestaError,
    Query = require('./query'),
    cache = require('./cache'),
    util = require('./util'),
    _ = util._,
    async = util.async,
    ChangeType = require('./changes').ChangeType;

var Logger = log.loggerWithName('MappingOperation');

function SiestaError(opts) {
    this.opts = opts;
}
SiestaError.prototype.toString = function () {
    return JSON.stringify(this.opts, null, 4);
};

/**
 * Defines an encapsulated mapping operation where opts takes a mappin
 * @param {Object} opts
 */
function MappingOperation(opts) {
    Operation.call(this);

    this._opts = opts;

    util.extendFromOpts(this, opts, {
        model: null,
        data: null,
        objects: [],
        disableNotifications: false
    });

    //
    ///**
    // * @name mapping
    // * @type {Model}
    // */
    //defineSubProperty.call(this, 'model', this._opts);
    //
    ///**
    // * @name data
    // * @type {Array}
    // */
    //defineSubProperty.call(this, 'data', this._opts);
    //
    ///**
    // * @name objects
    // * @type {Array}
    // */
    //defineSubProperty.call(this, 'objects', this._opts);
    //
    ///**
    // * @name disableNotifications
    // * @type {bool}
    // */
    //defineSubProperty.call(this, 'disableNotifications', this._opts);

    //if (!this.objects) this.objects = [];

    /**
     * Array of errors where indexes map onto same index as the datum that caused an error.
     * @type {Array}
     */
    this.errors = [];

    this.name = 'Model Operation';
    this.work = _.bind(this._start, this);
    this.subOps = {};
}

MappingOperation.prototype = Object.create(Operation.prototype);

_.extend(MappingOperation.prototype, {
    mapAttributes: function () {
        for (var i = 0; i < this.data.length; i++) {
            var datum = this.data[i];
            var object = this.objects[i];
            // No point mapping object onto itself. This happens if a ModelInstance is passed as a relationship.
            if (datum != object) {
                if (object) { // If object is falsy, then there was an error looking up that object/creating it.
                    var fields = this.model._attributeNames;
                    _.each(fields, function (f) {
                        if (datum[f] !== undefined) { // null is fine
                            // If notifications are disabled we update __values object directly. This avoids triggering
                            // notifications which are built into the set function of the property.
                            if (this.disableNotifications) {
                                object.__values[f] = datum[f];
                            }
                            else {
                                object[f] = datum[f];
                            }
                        }
                    }.bind(this));
                    // PouchDB revision (if using storage module).
                    // TODO: Can this be pulled out of core?
                    if (datum._rev) object._rev = datum._rev;
                }
            }
        }
    },
    _map: function () {
        var self = this;
        var err;
        this.mapAttributes();
        var relationshipFields = _.keys(self.subOps);
        _.each(relationshipFields, function (f) {
            var op = self.subOps[f].op;
            var indexes = self.subOps[f].indexes;
            var relatedData = self.getRelatedData(f).relatedData;
            var unflattenedObjects = util.unflattenArray(op.objects, relatedData);
            for (var i = 0; i < unflattenedObjects.length; i++) {
                var idx = indexes[i];
                // Errors are plucked from the suboperations.
                var error = self.errors[idx];
                err = error ? error[f] : null;
                if (!err) {
                    var related = unflattenedObjects[i]; // Can be array or scalar.
                    var object = self.objects[idx];
                    if (object) {
                        err = object.__proxies[f].set(related, {disableNotifications: self.disableNotifications});
                        if (err) {
                            if (!self.errors[idx]) self.errors[idx] = {};
                            self.errors[idx][f] = err;
                        }
                    }
                }
            }
        });
    },
    /**
     * For indices where no object is present, perform lookups, creating a new object if necessary.
     * @private
     */
    _lookup: function (callback) {
        var deferred = window.q ? window.q.defer() : null;
        callback = util.cb(callback, deferred);
        var self = this;
        var remoteLookups = [];
        var localLookups = [];
        for (var i = 0; i < this.data.length; i++) {
            if (!this.objects[i]) {
                var lookup;
                var datum = this.data[i];
                var isScalar = typeof datum == 'string' || typeof datum == 'number' || datum instanceof String;
                if (datum) {
                    if (isScalar) {
                        lookup = {
                            index: i,
                            datum: {}
                        };
                        lookup.datum[self.model.id] = datum;
                        remoteLookups.push(lookup);
                    } else if (datum instanceof SiestaModel) { // We won't need to perform any mapping.
                        this.objects[i] = datum;
                    } else if (datum._id) {
                        localLookups.push({
                            index: i,
                            datum: datum
                        });
                    } else if (datum[self.model.id]) {
                        remoteLookups.push({
                            index: i,
                            datum: datum
                        });
                    } else {
                        // Create a new object if and only if the data has any fields that will actually
                        var datumFields = Object.keys(datum);
                        var objectFields = _.reduce(Object.keys(self.model.relationships).concat(self.model._attributeNames), function (m, x) {
                            m[x] = {};
                            return m;
                        }, {});
                        var shouldCreateNewObject = false;
                        for (var j = 0; j < datumFields.length; j++) {
                            if (objectFields[datumFields[j]]) {
                                shouldCreateNewObject = true;
                                break;
                            }
                        }
                        if (shouldCreateNewObject) {
                            this.objects[i] = self.model._new();
                        }
                    }
                } else {
                    this.objects[i] = null;
                }
            }
        }
        util.async.parallel([
                function (callback) {
                    var localIdentifiers = _.pluck(_.pluck(localLookups, 'datum'), '_id');
                    if (localIdentifiers.length) {
                        Store.getMultipleLocal(localIdentifiers, function (err, objects) {
                            if (!err) {
                                for (var i = 0; i < localIdentifiers.length; i++) {
                                    var obj = objects[i];
                                    var _id = localIdentifiers[i];
                                    var lookup = localLookups[i];
                                    if (!obj) {
                                        // If there are multiple mapping operations going on, there may be
                                        obj = cache.get({_id: _id});
                                        if (!obj)
                                            obj = self.model._new({_id: _id}, !self.disableNotifications);
                                        self.objects[lookup.index] = obj;
                                    } else {
                                        self.objects[lookup.index] = obj;
                                    }
                                }
                            }
                            callback(err);
                        });
                    } else {
                        callback();
                    }
                },
                function (callback) {
                    var remoteIdentifiers = _.pluck(_.pluck(remoteLookups, 'datum'), self.model.id);
                    if (remoteIdentifiers.length) {
                        if (Logger.trace.isEnabled)
                            Logger.trace('Looking up remoteIdentifiers: ' + util.prettyPrint(remoteIdentifiers));
                        Store.getMultipleRemote(remoteIdentifiers, self.model, function (err, objects) {
                            if (!err) {
                                if (Logger.trace.isEnabled) {
                                    var results = {};
                                    for (i = 0; i < objects.length; i++) {
                                        results[remoteIdentifiers[i]] = objects[i] ? objects[i]._id : null;
                                    }
                                    Logger.trace('Results for remoteIdentifiers: ' + util.prettyPrint(results));
                                }
                                for (i = 0; i < objects.length; i++) {
                                    var obj = objects[i];
                                    var lookup = remoteLookups[i];
                                    if (obj) {
                                        self.objects[lookup.index] = obj;
                                    } else {
                                        var data = {};
                                        var remoteId = remoteIdentifiers[i];
                                        data[self.model.id] = remoteId;
                                        var cacheQuery = {
                                            model: self.model
                                        };
                                        cacheQuery[self.model.id] = remoteId;
                                        var cached = cache.get(cacheQuery);
                                        if (cached) {
                                            self.objects[lookup.index] = cached;
                                        } else {
                                            self.objects[lookup.index] = self.model._new();
                                            // It's important that we map the remote identifier here to ensure that it ends
                                            // up in the cache.
                                            self.objects[lookup.index][self.model.id] = remoteId;
                                        }
                                    }
                                }
                            }
                            callback(err);
                        });
                    } else {
                        callback();
                    }
                }
            ],
            callback);
        return deferred ? deferred.promise : null;
    },
    _lookupSingleton: function (callback) {
        var deferred = window.q ? window.q.defer() : null;
        callback = util.cb(callback, deferred);
        var self = this;
        this.model.get(function (err, singleton) {
            // Pick a random _id from the array of data being mapped onto the singleton object. Note that they should
            // always be the same. This is just a precaution.
            var _ids = _.pluck(self.data, '_id'),
                _id;
            for (i = 0; i < _ids.length; i++) {
                if (_ids[i]) {
                    _id = {_id: _ids[i]};
                    break;
                }
            }
            if (!singleton) singleton = self.model._new(_id);
            if (!err) {
                for (var i = 0; i < self.data.length; i++) {
                    self.objects[i] = singleton;
                }
            }
            callback(err);
        });
        return deferred ? deferred.promise : null;
    },
    _start: function (done) {
        if (this.data.length) {
            var self = this;
            var tasks = [];
            var lookupFunc = this.model.singleton ? this._lookupSingleton : this._lookup;
            tasks.push(_.bind(lookupFunc, this));
            tasks.push(_.bind(this._executeSubOperations, this));
            util.async.parallel(tasks, function () {
                self._map();
                done(self.errors.length ? self.errors : null, self.objects);
            });
        } else {
            done(null, []);
        }
    },
    getRelatedData: function (name) {
        var indexes = [];
        var relatedData = [];
        for (var i = 0; i < this.data.length; i++) {
            var datum = this.data[i];
            if (datum) {
                if (datum[name]) {
                    indexes.push(i);
                    relatedData.push(datum[name]);
                }
            }
        }
        return {
            indexes: indexes,
            relatedData: relatedData
        };
    },
    _constructSubOperations: function () {
        var subOps = this.subOps;
        var relationships = this.model.relationships;
        for (var name in relationships) {
            if (relationships.hasOwnProperty(name)) {
                var relationship = relationships[name];
                var reverseModel = relationship.forwardName == name ? relationship.reverseModel : relationship.forwardModel;
                var __ret = this.getRelatedData(name);
                var indexes = __ret.indexes;
                var relatedData = __ret.relatedData;
                if (relatedData.length) {
                    var flatRelatedData = util.flattenArray(relatedData);
                    var op = new MappingOperation({
                        model: reverseModel,
                        data: flatRelatedData,
                        disableNotifications: this.disableNotifications
                    });
                    op.__relationshipName = name;
                    subOps[name] = {
                        op: op,
                        indexes: indexes
                    };
                }
            }
        }
        if (Logger.trace) {
            Logger.trace('Constructed subops for relationships', Object.keys(subOps));
        }
    },
    gatherErrorsFromSubOperations: function () {
        var self = this;
        var relationshipNames = _.keys(this.subOps);
        _.each(relationshipNames, function (name) {
            var op = self.subOps[name].op;
            var indexes = self.subOps[name].indexes;
            var errors = op.errors;
            if (errors.length) {
                var relatedData = self.getRelatedData(name).relatedData;
                var unflattenedErrors = util.unflattenArray(errors, relatedData);
                for (var i = 0; i < unflattenedErrors.length; i++) {
                    var idx = indexes[i];
                    var err = unflattenedErrors[i];
                    var isError = err;
                    if (util.isArray(err)) isError = _.reduce(err, function (memo, x) {
                        return memo || x
                    }, false);
                    if (isError) {
                        if (!self.errors[idx]) self.errors[idx] = {};
                        self.errors[idx][name] = err;
                    }
                }
            }
        });
    },
    _executeSubOperations: function (callback) {
        var self = this;
        this._constructSubOperations();
        var relationshipNames = _.keys(this.subOps);
        if (relationshipNames.length) {
            var tasks = _.map(relationshipNames, function (k) {
                var op = self.subOps[k].op, task;
                task = function (done) {
                    op.onCompletion(function () {
                        task.errors = op.errors;
                        done();
                    });
                    op.start();
                };
                self.subOps[k].task = task;
                return task
            });
            async.parallel(tasks, function () {
                self.gatherErrorsFromSubOperations(relationshipNames);
                callback();
            });
        } else {
            callback();
        }
    }
});

module.exports = MappingOperation;

<commit_msg>gather errors from tasks<commit_after>/**
 * @module mapping
 */

var Store = require('./store'),
    SiestaModel = require('./modelInstance'),
    log = require('./operation/log'),
    Operation = require('./operation/operation').Operation,
    InternalSiestaError = require('./error').InternalSiestaError,
    Query = require('./query'),
    cache = require('./cache'),
    util = require('./util'),
    _ = util._,
    async = util.async,
    ChangeType = require('./changes').ChangeType;

var Logger = log.loggerWithName('MappingOperation');

function SiestaError(opts) {
    this.opts = opts;
}
SiestaError.prototype.toString = function () {
    return JSON.stringify(this.opts, null, 4);
};

/**
 * Defines an encapsulated mapping operation where opts takes a mappin
 * @param {Object} opts
 */
function MappingOperation(opts) {
    Operation.call(this);

    this._opts = opts;

    util.extendFromOpts(this, opts, {
        model: null,
        data: null,
        objects: [],
        disableNotifications: false
    });

    //
    ///**
    // * @name mapping
    // * @type {Model}
    // */
    //defineSubProperty.call(this, 'model', this._opts);
    //
    ///**
    // * @name data
    // * @type {Array}
    // */
    //defineSubProperty.call(this, 'data', this._opts);
    //
    ///**
    // * @name objects
    // * @type {Array}
    // */
    //defineSubProperty.call(this, 'objects', this._opts);
    //
    ///**
    // * @name disableNotifications
    // * @type {bool}
    // */
    //defineSubProperty.call(this, 'disableNotifications', this._opts);

    //if (!this.objects) this.objects = [];

    /**
     * Array of errors where indexes map onto same index as the datum that caused an error.
     * @type {Array}
     */
    this.errors = [];

    this.name = 'Model Operation';
    this.work = _.bind(this._start, this);
    this.subOps = {};
}

MappingOperation.prototype = Object.create(Operation.prototype);

_.extend(MappingOperation.prototype, {
    mapAttributes: function () {
        for (var i = 0; i < this.data.length; i++) {
            var datum = this.data[i];
            var object = this.objects[i];
            // No point mapping object onto itself. This happens if a ModelInstance is passed as a relationship.
            if (datum != object) {
                if (object) { // If object is falsy, then there was an error looking up that object/creating it.
                    var fields = this.model._attributeNames;
                    _.each(fields, function (f) {
                        if (datum[f] !== undefined) { // null is fine
                            // If notifications are disabled we update __values object directly. This avoids triggering
                            // notifications which are built into the set function of the property.
                            if (this.disableNotifications) {
                                object.__values[f] = datum[f];
                            }
                            else {
                                object[f] = datum[f];
                            }
                        }
                    }.bind(this));
                    // PouchDB revision (if using storage module).
                    // TODO: Can this be pulled out of core?
                    if (datum._rev) object._rev = datum._rev;
                }
            }
        }
    },
    _map: function () {
        var self = this;
        var err;
        this.mapAttributes();
        var relationshipFields = _.keys(self.subOps);
        _.each(relationshipFields, function (f) {
            var op = self.subOps[f].op;
            var indexes = self.subOps[f].indexes;
            var relatedData = self.getRelatedData(f).relatedData;
            var unflattenedObjects = util.unflattenArray(op.objects, relatedData);
            for (var i = 0; i < unflattenedObjects.length; i++) {
                var idx = indexes[i];
                // Errors are plucked from the suboperations.
                var error = self.errors[idx];
                err = error ? error[f] : null;
                if (!err) {
                    var related = unflattenedObjects[i]; // Can be array or scalar.
                    var object = self.objects[idx];
                    if (object) {
                        err = object.__proxies[f].set(related, {disableNotifications: self.disableNotifications});
                        if (err) {
                            if (!self.errors[idx]) self.errors[idx] = {};
                            self.errors[idx][f] = err;
                        }
                    }
                }
            }
        });
    },
    /**
     * For indices where no object is present, perform lookups, creating a new object if necessary.
     * @private
     */
    _lookup: function (callback) {
        var deferred = window.q ? window.q.defer() : null;
        callback = util.cb(callback, deferred);
        var self = this;
        var remoteLookups = [];
        var localLookups = [];
        for (var i = 0; i < this.data.length; i++) {
            if (!this.objects[i]) {
                var lookup;
                var datum = this.data[i];
                var isScalar = typeof datum == 'string' || typeof datum == 'number' || datum instanceof String;
                if (datum) {
                    if (isScalar) {
                        lookup = {
                            index: i,
                            datum: {}
                        };
                        lookup.datum[self.model.id] = datum;
                        remoteLookups.push(lookup);
                    } else if (datum instanceof SiestaModel) { // We won't need to perform any mapping.
                        this.objects[i] = datum;
                    } else if (datum._id) {
                        localLookups.push({
                            index: i,
                            datum: datum
                        });
                    } else if (datum[self.model.id]) {
                        remoteLookups.push({
                            index: i,
                            datum: datum
                        });
                    } else {
                        // Create a new object if and only if the data has any fields that will actually
                        var datumFields = Object.keys(datum);
                        var objectFields = _.reduce(Object.keys(self.model.relationships).concat(self.model._attributeNames), function (m, x) {
                            m[x] = {};
                            return m;
                        }, {});
                        var shouldCreateNewObject = false;
                        for (var j = 0; j < datumFields.length; j++) {
                            if (objectFields[datumFields[j]]) {
                                shouldCreateNewObject = true;
                                break;
                            }
                        }
                        if (shouldCreateNewObject) {
                            this.objects[i] = self.model._new();
                        }
                    }
                } else {
                    this.objects[i] = null;
                }
            }
        }
        util.async.parallel([
                function (callback) {
                    var localIdentifiers = _.pluck(_.pluck(localLookups, 'datum'), '_id');
                    if (localIdentifiers.length) {
                        Store.getMultipleLocal(localIdentifiers, function (err, objects) {
                            if (!err) {
                                for (var i = 0; i < localIdentifiers.length; i++) {
                                    var obj = objects[i];
                                    var _id = localIdentifiers[i];
                                    var lookup = localLookups[i];
                                    if (!obj) {
                                        // If there are multiple mapping operations going on, there may be
                                        obj = cache.get({_id: _id});
                                        if (!obj)
                                            obj = self.model._new({_id: _id}, !self.disableNotifications);
                                        self.objects[lookup.index] = obj;
                                    } else {
                                        self.objects[lookup.index] = obj;
                                    }
                                }
                            }
                            callback(err);
                        });
                    } else {
                        callback();
                    }
                },
                function (callback) {
                    var remoteIdentifiers = _.pluck(_.pluck(remoteLookups, 'datum'), self.model.id);
                    if (remoteIdentifiers.length) {
                        if (Logger.trace.isEnabled)
                            Logger.trace('Looking up remoteIdentifiers: ' + util.prettyPrint(remoteIdentifiers));
                        Store.getMultipleRemote(remoteIdentifiers, self.model, function (err, objects) {
                            if (!err) {
                                if (Logger.trace.isEnabled) {
                                    var results = {};
                                    for (i = 0; i < objects.length; i++) {
                                        results[remoteIdentifiers[i]] = objects[i] ? objects[i]._id : null;
                                    }
                                    Logger.trace('Results for remoteIdentifiers: ' + util.prettyPrint(results));
                                }
                                for (i = 0; i < objects.length; i++) {
                                    var obj = objects[i];
                                    var lookup = remoteLookups[i];
                                    if (obj) {
                                        self.objects[lookup.index] = obj;
                                    } else {
                                        var data = {};
                                        var remoteId = remoteIdentifiers[i];
                                        data[self.model.id] = remoteId;
                                        var cacheQuery = {
                                            model: self.model
                                        };
                                        cacheQuery[self.model.id] = remoteId;
                                        var cached = cache.get(cacheQuery);
                                        if (cached) {
                                            self.objects[lookup.index] = cached;
                                        } else {
                                            self.objects[lookup.index] = self.model._new();
                                            // It's important that we map the remote identifier here to ensure that it ends
                                            // up in the cache.
                                            self.objects[lookup.index][self.model.id] = remoteId;
                                        }
                                    }
                                }
                            }
                            callback(err);
                        });
                    } else {
                        callback();
                    }
                }
            ],
            callback);
        return deferred ? deferred.promise : null;
    },
    _lookupSingleton: function (callback) {
        var deferred = window.q ? window.q.defer() : null;
        callback = util.cb(callback, deferred);
        var self = this;
        this.model.get(function (err, singleton) {
            // Pick a random _id from the array of data being mapped onto the singleton object. Note that they should
            // always be the same. This is just a precaution.
            var _ids = _.pluck(self.data, '_id'),
                _id;
            for (i = 0; i < _ids.length; i++) {
                if (_ids[i]) {
                    _id = {_id: _ids[i]};
                    break;
                }
            }
            if (!singleton) singleton = self.model._new(_id);
            if (!err) {
                for (var i = 0; i < self.data.length; i++) {
                    self.objects[i] = singleton;
                }
            }
            callback(err);
        });
        return deferred ? deferred.promise : null;
    },
    _start: function (done) {
        if (this.data.length) {
            var self = this;
            var tasks = [];
            var lookupFunc = this.model.singleton ? this._lookupSingleton : this._lookup;
            tasks.push(_.bind(lookupFunc, this));
            tasks.push(_.bind(this._executeSubOperations, this));
            util.async.parallel(tasks, function () {
                self._map();
                done(self.errors.length ? self.errors : null, self.objects);
            });
        } else {
            done(null, []);
        }
    },
    getRelatedData: function (name) {
        var indexes = [];
        var relatedData = [];
        for (var i = 0; i < this.data.length; i++) {
            var datum = this.data[i];
            if (datum) {
                if (datum[name]) {
                    indexes.push(i);
                    relatedData.push(datum[name]);
                }
            }
        }
        return {
            indexes: indexes,
            relatedData: relatedData
        };
    },
    _constructSubOperations: function () {
        var subOps = this.subOps;
        var relationships = this.model.relationships;
        for (var name in relationships) {
            if (relationships.hasOwnProperty(name)) {
                var relationship = relationships[name];
                var reverseModel = relationship.forwardName == name ? relationship.reverseModel : relationship.forwardModel;
                var __ret = this.getRelatedData(name);
                var indexes = __ret.indexes;
                var relatedData = __ret.relatedData;
                if (relatedData.length) {
                    var flatRelatedData = util.flattenArray(relatedData);
                    var op = new MappingOperation({
                        model: reverseModel,
                        data: flatRelatedData,
                        disableNotifications: this.disableNotifications
                    });
                    op.__relationshipName = name;
                    subOps[name] = {
                        op: op,
                        indexes: indexes
                    };
                }
            }
        }
        if (Logger.trace) {
            Logger.trace('Constructed subops for relationships', Object.keys(subOps));
        }
    },
    gatherErrorsFromTasks: function () {
        var self = this;
        var relationshipNames = _.keys(this.subOps);
        _.each(relationshipNames, function (name) {
            var task = self.subOps[name].task;
            var indexes = self.subOps[name].indexes;
            var errors = task.errors;
            if (errors.length) {
                var relatedData = self.getRelatedData(name).relatedData;
                var unflattenedErrors = util.unflattenArray(errors, relatedData);
                for (var i = 0; i < unflattenedErrors.length; i++) {
                    var idx = indexes[i];
                    var err = unflattenedErrors[i];
                    var isError = err;
                    if (util.isArray(err)) isError = _.reduce(err, function (memo, x) {
                        return memo || x
                    }, false);
                    if (isError) {
                        if (!self.errors[idx]) self.errors[idx] = {};
                        self.errors[idx][name] = err;
                    }
                }
            }
        });
    },
    _executeSubOperations: function (callback) {
        var self = this;
        this._constructSubOperations();
        var relationshipNames = _.keys(this.subOps);
        if (relationshipNames.length) {
            var tasks = _.map(relationshipNames, function (k) {
                var op = self.subOps[k].op, task;
                task = function (done) {
                    op.onCompletion(function () {
                        task.errors = op.errors;
                        done();
                    });
                    op.start();
                };
                self.subOps[k].task = task;
                return task
            });
            async.parallel(tasks, function () {
                self.gatherErrorsFromTasks(relationshipNames);
                callback();
            });
        } else {
            callback();
        }
    }
});

module.exports = MappingOperation;

<|endoftext|>


<commit_before>#!/usr/bin/env node
/**
 * Build script for F2
 *
 * This script requires that the following node packages be installed:
 *   - markitdown (npm install -g markitdown)
 *   - optimist (npm install optimist)
 *   - uglify-js (npm install uglify-js)
 *   - wrench-js (npm install wrench)
 *   - yuidocjs (npm install yuidocjs)
 *     also requires pandoc: http://johnmacfarlane.net/pandoc/installing.html
 */
var exec = require('child_process').exec;
var fs = require('fs');
var jsp = require('uglify-js').parser;
var pro = require('uglify-js').uglify;
var wrench = require('wrench');
var Y = require('yuidocjs');
var optimist = require('optimist');
var argv = optimist
	.usage('Build script for F2\nUsage: $0 [options]')
	.boolean('a').alias('a', 'all').describe('a', 'Build all')
	.boolean('d').alias('d', 'docs').describe('d', 'Build the docs')
	.boolean('l').alias('l', 'less').describe('l', 'Compile LESS')
	.boolean('g').alias('g', 'gh-pages').describe('g', 'Copy docs to gh-pages folder. Must have the gh-pages branch cloned to ../gh-pages')
	.boolean('h').alias('h', 'help').describe('h', 'Display this help information')
	.boolean('j').alias('j', 'js-sdk').describe('j', 'Build just the JS SDK')
	.boolean('y').alias('y', 'yuidoc').describe('y', 'Build the YUIDoc for the SDK')
	.argv;

// constants
// only the files that represent f2
var CORE_FILES = [
	'sdk/src/preamble.js',
	'sdk/src/classes.js',
	'sdk/src/constants.js',
	'sdk/src/container.js',
	'sdk/src/rpc.js',
	'sdk/src/ui.js',
	'sdk/src/ui-dialogs.js'
];
var ENCODING = 'utf-8';
var EOL = '\n';
// files to be packaged
var PACKAGE_FILES = [
	'sdk/src/third-party/json3.js',
	'sdk/src/third-party/eventemitter2.js',
	'sdk/src/third-party/easyXDM/easyXDM.min.js',
	'sdk/f2.no-third-party.js' // this file is created by the build process
];
// a list of options that maps an argument to a function. the options need to be
// in order of dependency, in case the user specifies -a
var OPTIONS = [
	{ arg: 'j', f: js },
	{ arg: 'd', f: docs },
	{ arg: 'l', f: less },
	{ arg: 'y', f: yuidoc },
	{ arg: 'g', f: ghp }
];

// build all if no args are passed
argv.a = argv.a || process.argv.length == 2;
// process -l if -d is passed
argv.l = argv.l || argv.d;

// if help option was passed, only display help
if (argv.h) {
	help();
// else process all options
} else {
	processOptionQueue();
}

/**
 * Process the list of options that were passed. We have to use a queue because
 * of sync issues when running external processes.
 * @method processOptionQueue
 */
function processOptionQueue() {
	var option = OPTIONS.shift();

	if (!option) { return; }

	if (argv[option.arg] || argv.a) {
		option.f();
	} else {
		processOptionQueue();
	}
}

/**
 * Build the documentation for GitHub Pages. The documentation generation is
 * asynchronous, so if anything needs to execute after the generation is
 * complete, those functions can be passed as parameters.
 * @method docs
 */
function docs() {
	console.log('Generating Docs...');
	var callbacks = arguments;

	exec(
		'markitdown ./ --output-path ../ --header ./template/header.html --footer ./template/footer.html --head ./template/style.html --title "F2 Documentation"',
		{ cwd:'./docs/src' },
		function(error, stdout, stderr) {
			if (error) {
				console.log(stderr);
			} else {
				console.log('COMPLETE');
				processOptionQueue();
			}
		}
	);
};

/**
 * Compile LESS into f2.css
 * @method less
 */
function less(){
	console.log('Compiling LESS...');
	exec(
		'lessc ./template/less/bootstrap.less > ../css/F2.css', //--compress
		{ cwd: './docs/src' },
		function(error, stdout, stderr){
			if (error){
				console.log(stderr);
			} else {
				console.log("COMPLETE");
				processOptionQueue();
			}
		}
	);
}

/**
 * Copies all documentation to the gh-pages folder
 * @method ghp
 */
function ghp() {
	console.log('Copying documentation to gh-pages...');
	// temporary - do not overwrite the gh-pages index.html until launch
	fs.renameSync('./docs/index.html', './docs/index-temp.html');
	wrench.copyDirSyncRecursive('./docs', '../gh-pages', { preserve:true });
	// temporary - put index.html back to normal
	fs.renameSync('./docs/index-temp.html', './docs/index.html');
	wrench.copyDirSyncRecursive('./sdk/docs', '../gh-pages/sdk');
	console.log('COMPLETE');

	processOptionQueue();
};

/**
 * Display the help information
 * @method help
 */
function help() {
	optimist.wrap(80).showHelp();
};

/**
 * Build the debug, minified, and no-third-party sdk files
 * @method js
 */
function js() {
	console.log('Building f2.no-third-party.js...');
	var contents = CORE_FILES.map(function(f) {
		return fs.readFileSync(f, ENCODING);
	});
	fs.writeFileSync('./sdk/f2.no-third-party.js', contents.join(EOL), ENCODING);
	console.log('COMPLETE');

	console.log('Building Debug Package...');
	var contents = PACKAGE_FILES.map(function(f) {
		return fs.readFileSync(f, ENCODING);
	});
	fs.writeFileSync('./sdk/f2.debug.js', contents.join(EOL), ENCODING);
	console.log('COMPLETE');

	console.log('Building Minified Package...');
	var contents = PACKAGE_FILES.map(function(f) {

		var code = fs.readFileSync(f, ENCODING);
		var comments = [];
		var token = '"F2: preserved commment block"';

		// borrowed from enter-js
		code = code.replace(/\/\*![\s\S]*?\*\//g, function(comment) {
			comments.push(comment)
			return ';' + token + ';';
		});

		var ast = jsp.parse(code); // parse code and get the initial AST
		ast = pro.ast_mangle(ast); // get a new AST with mangled names
		ast = pro.ast_squeeze(ast); // get an AST with compression optimizations
		code = pro.gen_code(ast); // compressed code here

		code = code.replace(RegExp(token, 'g'), function() {
			return EOL + comments.shift() + EOL;
		});

		return code;
	});
	fs.writeFileSync('./sdk/f2.min.js', contents.join(EOL), ENCODING);
	console.log('COMPLETE');

	processOptionQueue();
};

/**
 * Build the YUIDoc for the sdk
 * @method yuidoc
 */
function yuidoc() {
	var callbacks = arguments;
	var docOptions = {
		quiet:true,
		norecurse:true,
		paths:['./sdk/src'],
		outdir:'./sdk/docs',
		themedir:'./sdk/doc-theme'
	};

	console.log('Generating YUIDoc...');
	var json = (new Y.YUIDoc(docOptions)).run();
	docOptions = Y.Project.mix(json, docOptions);
	(new Y.DocBuilder(docOptions, json)).compile(function() {
		console.log('COMPLETE');
		processOptionQueue();
	});
};<commit_msg>updated build to reflect new file name<commit_after>#!/usr/bin/env node
/**
 * Build script for F2
 *
 * This script requires that the following node packages be installed:
 *   - markitdown (npm install -g markitdown)
 *   - optimist (npm install optimist)
 *   - uglify-js (npm install uglify-js)
 *   - wrench-js (npm install wrench)
 *   - yuidocjs (npm install yuidocjs)
 *     also requires pandoc: http://johnmacfarlane.net/pandoc/installing.html
 */
var exec = require('child_process').exec;
var fs = require('fs');
var jsp = require('uglify-js').parser;
var pro = require('uglify-js').uglify;
var wrench = require('wrench');
var Y = require('yuidocjs');
var optimist = require('optimist');
var argv = optimist
	.usage('Build script for F2\nUsage: $0 [options]')
	.boolean('a').alias('a', 'all').describe('a', 'Build all')
	.boolean('d').alias('d', 'docs').describe('d', 'Build the docs')
	.boolean('l').alias('l', 'less').describe('l', 'Compile LESS')
	.boolean('g').alias('g', 'gh-pages').describe('g', 'Copy docs to gh-pages folder. Must have the gh-pages branch cloned to ../gh-pages')
	.boolean('h').alias('h', 'help').describe('h', 'Display this help information')
	.boolean('j').alias('j', 'js-sdk').describe('j', 'Build just the JS SDK')
	.boolean('y').alias('y', 'yuidoc').describe('y', 'Build the YUIDoc for the SDK')
	.argv;

// constants
// only the files that represent f2
var CORE_FILES = [
	'sdk/src/preamble.js',
	'sdk/src/classes.js',
	'sdk/src/constants.js',
	'sdk/src/container.js',
	'sdk/src/rpc.js',
	'sdk/src/ui.js',
	'sdk/src/ui-modals.js'
];
var ENCODING = 'utf-8';
var EOL = '\n';
// files to be packaged
var PACKAGE_FILES = [
	'sdk/src/third-party/json3.js',
	'sdk/src/third-party/eventemitter2.js',
	'sdk/src/third-party/easyXDM/easyXDM.min.js',
	'sdk/f2.no-third-party.js' // this file is created by the build process
];
// a list of options that maps an argument to a function. the options need to be
// in order of dependency, in case the user specifies -a
var OPTIONS = [
	{ arg: 'j', f: js },
	{ arg: 'd', f: docs },
	{ arg: 'l', f: less },
	{ arg: 'y', f: yuidoc },
	{ arg: 'g', f: ghp }
];

// build all if no args are passed
argv.a = argv.a || process.argv.length == 2;
// process -l if -d is passed
argv.l = argv.l || argv.d;

// if help option was passed, only display help
if (argv.h) {
	help();
// else process all options
} else {
	processOptionQueue();
}

/**
 * Process the list of options that were passed. We have to use a queue because
 * of sync issues when running external processes.
 * @method processOptionQueue
 */
function processOptionQueue() {
	var option = OPTIONS.shift();

	if (!option) { return; }

	if (argv[option.arg] || argv.a) {
		option.f();
	} else {
		processOptionQueue();
	}
}

/**
 * Build the documentation for GitHub Pages. The documentation generation is
 * asynchronous, so if anything needs to execute after the generation is
 * complete, those functions can be passed as parameters.
 * @method docs
 */
function docs() {
	console.log('Generating Docs...');
	var callbacks = arguments;

	exec(
		'markitdown ./ --output-path ../ --header ./template/header.html --footer ./template/footer.html --head ./template/style.html --title "F2 Documentation"',
		{ cwd:'./docs/src' },
		function(error, stdout, stderr) {
			if (error) {
				console.log(stderr);
			} else {
				console.log('COMPLETE');
				processOptionQueue();
			}
		}
	);
};

/**
 * Compile LESS into f2.css
 * @method less
 */
function less(){
	console.log('Compiling LESS...');
	exec(
		'lessc ./template/less/bootstrap.less > ../css/F2.css', //--compress
		{ cwd: './docs/src' },
		function(error, stdout, stderr){
			if (error){
				console.log(stderr);
			} else {
				console.log("COMPLETE");
				processOptionQueue();
			}
		}
	);
}

/**
 * Copies all documentation to the gh-pages folder
 * @method ghp
 */
function ghp() {
	console.log('Copying documentation to gh-pages...');
	// temporary - do not overwrite the gh-pages index.html until launch
	fs.renameSync('./docs/index.html', './docs/index-temp.html');
	wrench.copyDirSyncRecursive('./docs', '../gh-pages', { preserve:true });
	// temporary - put index.html back to normal
	fs.renameSync('./docs/index-temp.html', './docs/index.html');
	wrench.copyDirSyncRecursive('./sdk/docs', '../gh-pages/sdk');
	console.log('COMPLETE');

	processOptionQueue();
};

/**
 * Display the help information
 * @method help
 */
function help() {
	optimist.wrap(80).showHelp();
};

/**
 * Build the debug, minified, and no-third-party sdk files
 * @method js
 */
function js() {
	console.log('Building f2.no-third-party.js...');
	var contents = CORE_FILES.map(function(f) {
		return fs.readFileSync(f, ENCODING);
	});
	fs.writeFileSync('./sdk/f2.no-third-party.js', contents.join(EOL), ENCODING);
	console.log('COMPLETE');

	console.log('Building Debug Package...');
	var contents = PACKAGE_FILES.map(function(f) {
		return fs.readFileSync(f, ENCODING);
	});
	fs.writeFileSync('./sdk/f2.debug.js', contents.join(EOL), ENCODING);
	console.log('COMPLETE');

	console.log('Building Minified Package...');
	var contents = PACKAGE_FILES.map(function(f) {

		var code = fs.readFileSync(f, ENCODING);
		var comments = [];
		var token = '"F2: preserved commment block"';

		// borrowed from enter-js
		code = code.replace(/\/\*![\s\S]*?\*\//g, function(comment) {
			comments.push(comment)
			return ';' + token + ';';
		});

		var ast = jsp.parse(code); // parse code and get the initial AST
		ast = pro.ast_mangle(ast); // get a new AST with mangled names
		ast = pro.ast_squeeze(ast); // get an AST with compression optimizations
		code = pro.gen_code(ast); // compressed code here

		code = code.replace(RegExp(token, 'g'), function() {
			return EOL + comments.shift() + EOL;
		});

		return code;
	});
	fs.writeFileSync('./sdk/f2.min.js', contents.join(EOL), ENCODING);
	console.log('COMPLETE');

	processOptionQueue();
};

/**
 * Build the YUIDoc for the sdk
 * @method yuidoc
 */
function yuidoc() {
	var callbacks = arguments;
	var docOptions = {
		quiet:true,
		norecurse:true,
		paths:['./sdk/src'],
		outdir:'./sdk/docs',
		themedir:'./sdk/doc-theme'
	};

	console.log('Generating YUIDoc...');
	var json = (new Y.YUIDoc(docOptions)).run();
	docOptions = Y.Project.mix(json, docOptions);
	(new Y.DocBuilder(docOptions, json)).compile(function() {
		console.log('COMPLETE');
		processOptionQueue();
	});
};<|endoftext|>


<commit_before>import { shortenString } from '../../../utils/string'
import base from './base'
import Image from '@twreporter/react-article-components/lib/components/img-with-placeholder'
import mq from '../../../utils/media-query'
import React from 'react'
import styled from 'styled-components'

const ImageBorder = styled.div`
  ${mq.tabletAndAbove`
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    border: 0 solid #ffffff;
    transition: border .1s ease;
  `}
`

const ItemsContainer = styled(base.ItemsContainer)`
  ${mq.tabletAndAbove`
    padding: 60px 0 50px 0;
    display: flex;
    margin: 0 auto;
    background-color: inherit;
    width: 90%;
    flex-wrap: wrap;
    justify-content: center;
    align-items: flex-start;
  `}
`

const ItemLink = styled(base.ItemLink)`
  ${mq.tabletAndAbove`
    flex: 0 0 300px;
    width: 300px;
    margin: 0 10px 50px 10px;
    box-shadow: none;
    transition: top .1s ease, box-shadow .2s ease, transform .1s ease;
    &::before {
      transition: border-width .1s ease-out;
      content: "";
      width: 0;
      height: 0;
      top: -2px;
      left: -2px;
      position: absolute;
      z-index: 5;
      border-style: solid;
      border-width: 25px 25px 0 0;
      border-color: #fff transparent transparent;
    }
    :hover {
      &::before {
        border-width: 0;
      }
      transform: translateY(-5px);
      box-shadow: 0 5px 15px 0 rgba(0,0,0,.25);
      ${base.ItemMeta} {
        transform: translateY(0);
      }
      ${ImageBorder} {
        border: 5px solid #ffffff;
      }
      ${base.ItemImageSizing}, ${base.ItemMeta} {
        box-shadow: none;
      }
    }
  `}
`

const ItemImageSizing = styled(base.ItemImageSizing)`
  ${mq.tabletAndAbove`
    width: 100%;
    height: 200px;
    position: relative;
    box-shadow: 0 2px 5px 0 rgba(0,0,0,.1);
    transition: box-shadow .1s ease, transform .2s ease;
  `}
`

const ItemMeta = styled(base.ItemMeta)`
  ${mq.tabletAndAbove`
    width: 100%;
    min-height: 222px;
    position: relative;
    transform: translateY(5px);
    padding: 17px 12px 2.5em 20px;
    box-shadow: 0 2px 5px 0 rgba(0,0,0,.1);
    transition: box-shadow .1s ease, transform .2s ease;
  `}
`

const Date = styled(base.ItemDate)`
  position: absolute;
  right: 15px;
  bottom: 18px;
`

const textLimit = {
  desc: 59
}

class Item extends base.Item {
  render() {
    const {
      linkTo,
      linkTarget,
      image,
      title,
      description,
      publishedDate
    } = this.props
    return (
      <ItemLink
        to={linkTo}
        target={linkTarget}
      >
        <ItemImageSizing>
          <Image
            alt={title}
            defaultImage={image}
            imageSet={[ image ]}
            objectFit="cover"
          />
          <ImageBorder />
        </ItemImageSizing>
        <ItemMeta>
          <base.ItemTitle>{title}</base.ItemTitle>
          <base.ItemDescription>{shortenString(description, textLimit.desc)}</base.ItemDescription>
          <Date>{publishedDate}</Date>
        </ItemMeta>
      </ItemLink>
    )
  }
}

export default {
  ItemsContainer,
  Item
}
<commit_msg>Update topic card heights<commit_after>import { shortenString } from '../../../utils/string'
import base from './base'
import Image from '@twreporter/react-article-components/lib/components/img-with-placeholder'
import mq from '../../../utils/media-query'
import React from 'react'
import styled from 'styled-components'

const ImageBorder = styled.div`
  ${mq.tabletAndAbove`
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    border: 0 solid #ffffff;
    transition: border .1s ease;
  `}
`

const ItemsContainer = styled(base.ItemsContainer)`
  ${mq.tabletAndAbove`
    padding: 60px 0 50px 0;
    display: flex;
    margin: 0 auto;
    background-color: inherit;
    width: 90%;
    flex-wrap: wrap;
    justify-content: center;
    align-items: stretch;
  `}
`

const ItemLink = styled(base.ItemLink)`
  ${mq.tabletAndAbove`
    flex: 0 0 300px;
    width: 300px;
    margin: 0 10px 50px 10px;
    box-shadow: none;
    transition: top .1s ease, box-shadow .2s ease, transform .1s ease;
    display: flex;
    flex-direction: column;
    &::before {
      transition: border-width .1s ease-out;
      content: "";
      width: 0;
      height: 0;
      top: -2px;
      left: -2px;
      position: absolute;
      z-index: 5;
      border-style: solid;
      border-width: 25px 25px 0 0;
      border-color: #fff transparent transparent;
    }
    :hover {
      &::before {
        border-width: 0;
      }
      transform: translateY(-5px);
      box-shadow: 0 5px 15px 0 rgba(0,0,0,.25);
      ${base.ItemMeta} {
        transform: translateY(0);
      }
      ${ImageBorder} {
        border: 5px solid #ffffff;
      }
      ${base.ItemImageSizing}, ${base.ItemMeta} {
        box-shadow: none;
      }
    }
  `}
`

const ItemImageSizing = styled(base.ItemImageSizing)`
  ${mq.tabletAndAbove`
    width: 100%;
    height: 200px;
    flex: 0 0 200px;
    position: relative;
    box-shadow: 0 2px 5px 0 rgba(0,0,0,.1);
    transition: box-shadow .1s ease, transform .2s ease;
  `}
`

const ItemMeta = styled(base.ItemMeta)`
  ${mq.tabletAndAbove`
    width: 100%;
    height: 222px;
    flex: 1 0 222px;
    position: relative;
    transform: translateY(5px);
    padding: 17px 12px 2.5em 20px;
    box-shadow: 0 2px 5px 0 rgba(0,0,0,.1);
    transition: box-shadow .1s ease, transform .2s ease;
  `}
`

const Date = styled(base.ItemDate)`
  position: absolute;
  right: 15px;
  bottom: 18px;
`

const textLimit = {
  desc: 59
}

class Item extends base.Item {
  render() {
    const {
      linkTo,
      linkTarget,
      image,
      title,
      description,
      publishedDate
    } = this.props
    return (
      <ItemLink
        to={linkTo}
        target={linkTarget}
      >
        <ItemImageSizing>
          <Image
            alt={title}
            defaultImage={image}
            imageSet={[ image ]}
            objectFit="cover"
          />
          <ImageBorder />
        </ItemImageSizing>
        <ItemMeta>
          <base.ItemTitle>{title}</base.ItemTitle>
          <base.ItemDescription>{shortenString(description, textLimit.desc)}</base.ItemDescription>
          <Date>{publishedDate}</Date>
        </ItemMeta>
      </ItemLink>
    )
  }
}

export default {
  ItemsContainer,
  Item
}
<|endoftext|>


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

var program = require('commander'),
    crypto = require('crypto'),
    fs = require('fs'),
    git = require('git-rev'),
    path = require('path'),
    rimraf = require('rimraf'),
    spawn = require('child_process').spawn;

function list(val) {
      return val.split(',');
}

program
    .option('-n, --no-minify', 'Do no minify package')
    .option('--no-symlink', 'Do not symlink new bundle')
    .option('-t, --tag [version]', 'Build specific version')
    .option('-m, --modules [module1,module2,...]',
            'Include only these modules in bundle', list)
    .parse(process.argv);

var build = function(tag, cleanup) {
    var filename;
    if (program.modules) {
        // User wants a custom bundle
        var patterns = program.modules.sort().join('&'),
            shasum = crypto.createHash('sha1');
        shasum.update(tag + '?' + patterns);

        var hex = shasum.digest('hex').substr(0,8);
        filename = 'patterns-' + tag+ '-' + hex + (program.minify?'.min':'') + '.js';
    } else {
        filename = 'patterns-' + tag + (program.minify?'.min':'') + '.js';
    }

    var modules = program.modules || ['main'];
    modules = modules.sort();

    // Generate args that are passed to jam
    var fullname = path.join(__dirname, 'bundles', filename);

    // The CLI interface is:
    // jam compile -i pat1 [-i pat2 [-i pat3]] -o outfile
    var args = [
            'build',
            '--almond',
            '--verbose',
            '-i', modules.join(','),
            '-o', fullname
            ];

    if (!program.minify)
        args.push('--no-minify');

    var p = spawn(path.join(__dirname, 'node_modules/.bin/bungle'), args);
    p.stdout.on('data', function(data){
        console.log(data.toString());
    });
    p.stderr.on('data', function(data){
        console.log('stderr ' + data);
    });
    p.on('exit', function(code) {
        if (code === 0) {
            var header = "/**\n" +
                " * Patterns bundle " + tag + (hex?"-"+hex:"") + "\n" +
                " *\n" +
                " * See http://patternslib.com/ for more information.\n" +
                " *\n";
            if (modules.indexOf('main') === -1) {
                header += " * Included patterns:\n * - ";
                header += program.modules.sort().join("\n * - ") + "\n";
            }
            header += " */";

            var js = fs.readFileSync(fullname);

            var init = '';
            if (modules.indexOf('main') === -1) {
                var deps = modules
                    .map(function(e){ return '"'+e+'"';  })
                    .join(', ');

                init = "require(['registry', " + deps +
                    "], function(r){r.init();});";
            }

            var stream = fs.createWriteStream(fullname);
            stream.write(header + js + init);
            stream.end();
            if (program.symlink) {
                var link = path.join(__dirname, 'bundles',
                    'patterns' + (program.minify?'.min':'') + '.js');
                if (fs.existsSync(link)) {
                    fs.unlinkSync(link);
                }
                fs.symlinkSync(filename, link);
            }
            console.log('Great success!');
            return cleanup();
        } else {
            console.log('Build failed ...');
            return cleanup();
        }
    });
    return;
};

var checkout = function(tmpdir, tag) {
    var p = spawn('git', ['checkout', program.tag]);

    p.stdout.on('data', function(data){
        console.log('stdout ' + data);
    });
    p.stderr.on('data', function(data){
        console.log('stderr ' + data);
    });

    p.on('exit', function(code, status){
        if (code === 0) {
            build(tag, function(){
                rimraf.sync(tmpdir);
                process.exit(0);
            });
        } else {
            console.log('could not checkout tag ' + tag);
            process.exit(1);
        }
    });
};

var clone = function(tmpdir, tag) {
    var p = spawn('git', ['clone', '-s', '.', tmpdir]);

    p.stdout.on('data', function(data){
        console.log('stdout ' + data);
    });
    p.stderr.on('data', function(data){
        console.log('stderr ' + data);
    });

    p.on('exit', function(code, status){
        if (code === 0) {
            try {
                process.chdir(tmpdir);
            } catch(err) {
                console.log('chdir: ' + err);
                process.exit(1);
            }
            checkout(tmpdir, tag);
        } else {
            console.log('could not clone');
            process.exit(1);
        }
    });

};

if (program.tag) {
    var randChar = function() {
        var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz';
        return chars[Math.floor(Math.random() * chars.length)];
    };
    var tmpdir;
    do {
        tmpdir = '/tmp/patterns-' + Array(8).join('.').split('').map(randChar).join('');
    } while(fs.existsSync(tmpdir));
    clone(tmpdir, program.tag);
} else {
    git.tag(function (tag) {
        build(tag, function(){});
    });
}

<commit_msg>more linebreaks, beautify and fix #258<commit_after>#!/usr/bin/env node

var program = require('commander'),
    crypto = require('crypto'),
    fs = require('fs'),
    git = require('git-rev'),
    path = require('path'),
    rimraf = require('rimraf'),
    spawn = require('child_process').spawn;

function list(val) {
      return val.split(',');
}

program
    .option('-n, --no-minify', 'Do no minify package')
    .option('--no-symlink', 'Do not symlink new bundle')
    .option('-t, --tag [version]', 'Build specific version')
    .option('-m, --modules [module1,module2,...]',
            'Include only these modules in bundle', list)
    .parse(process.argv);

var build = function(tag, cleanup) {
    var filename;
    if (program.modules) {
        // User wants a custom bundle
        var patterns = program.modules.sort().join('&'),
            shasum = crypto.createHash('sha1');
        shasum.update(tag + '?' + patterns);

        var hex = shasum.digest('hex').substr(0,8);
        filename = 'patterns-' + tag+ '-' + hex + (program.minify?'.min':'') + '.js';
    } else {
        filename = 'patterns-' + tag + (program.minify?'.min':'') + '.js';
    }

    var modules = program.modules || ['main'];
    modules = modules.sort();

    // Generate args that are passed to jam
    var fullname = path.join(__dirname, 'bundles', filename);

    // The CLI interface is:
    // jam compile -i pat1 [-i pat2 [-i pat3]] -o outfile
    var args = [
            'build',
            '--almond',
            '--verbose',
            '-i', modules.join(','),
            '-o', fullname
            ];

    if (!program.minify)
        args.push('--no-minify');

    var p = spawn(path.join(__dirname, 'node_modules/.bin/bungle'), args);
    p.stdout.on('data', function(data){
        console.log(data.toString());
    });
    p.stderr.on('data', function(data){
        console.log('stderr ' + data);
    });
    p.on('exit', function(code) {
        if (code === 0) {
            var header = "/**\n" +
                " * Patterns bundle " + tag + (hex?"-"+hex:"") + "\n" +
                " *\n" +
                " * See http://patternslib.com/ for more information.\n" +
                " *\n";
            if (modules.indexOf('main') === -1) {
                header += " * Included patterns:\n * - ";
                header += program.modules.sort().join("\n * - ") + "\n";
            }
            header += " */\n\n";

            var js = fs.readFileSync(fullname);

            var init = '';
            if (modules.indexOf('main') === -1) {
                var deps = modules
                    .map(function(e){ return '"'+e+'"';  })
                    .join(', ');

                init = "require(['registry', " + deps +
                    "], function(r){r.init();});";
            }

            var stream = fs.createWriteStream(fullname);
            stream.write(header + js + init);
            stream.end();
            if (program.symlink) {
                var link = path.join(__dirname, 'bundles',
                    'patterns' + (program.minify?'.min':'') + '.js');
                if (fs.existsSync(link)) {
                    fs.unlinkSync(link);
                }
                fs.symlinkSync(filename, link);
            }
            console.log('Great success!');
            return cleanup();
        } else {
            console.log('Build failed ...');
            return cleanup();
        }
    });
    return;
};

var checkout = function(tmpdir, tag) {
    var p = spawn('git', ['checkout', program.tag]);

    p.stdout.on('data', function(data){
        console.log('stdout ' + data);
    });
    p.stderr.on('data', function(data){
        console.log('stderr ' + data);
    });

    p.on('exit', function(code, status){
        if (code === 0) {
            build(tag, function(){
                rimraf.sync(tmpdir);
                process.exit(0);
            });
        } else {
            console.log('could not checkout tag ' + tag);
            process.exit(1);
        }
    });
};

var clone = function(tmpdir, tag) {
    var p = spawn('git', ['clone', '-s', '.', tmpdir]);

    p.stdout.on('data', function(data){
        console.log('stdout ' + data);
    });
    p.stderr.on('data', function(data){
        console.log('stderr ' + data);
    });

    p.on('exit', function(code, status){
        if (code === 0) {
            try {
                process.chdir(tmpdir);
            } catch(err) {
                console.log('chdir: ' + err);
                process.exit(1);
            }
            checkout(tmpdir, tag);
        } else {
            console.log('could not clone');
            process.exit(1);
        }
    });

};

if (program.tag) {
    var randChar = function() {
        var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz';
        return chars[Math.floor(Math.random() * chars.length)];
    };
    var tmpdir;
    do {
        tmpdir = '/tmp/patterns-' + Array(8).join('.').split('').map(randChar).join('');
    } while(fs.existsSync(tmpdir));
    clone(tmpdir, program.tag);
} else {
    git.tag(function (tag) {
        build(tag, function(){});
    });
}

<|endoftext|>