<commit_before>/**
 * Application logic for Geierlein XUL-based interface.
 *
 * @author Stefan Siegl
 *
 * Copyright (c) 2012, 2013 Stefan Siegl <stesie@brokenpipe.de>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/*global
  document, window, alert,
  Components, FileUtils, NetUtil
 */

var xulapp = (function() {
    "use strict";

    Components.utils.import("resource://gre/modules/NetUtil.jsm");
    Components.utils.import("resource://gre/modules/FileUtils.jsm");
    var C = Components.classes;
    var I = Components.interfaces;

    var doc = document.getElementById('doc');
    var cW = null;
    var filePath;
    var fileChanged = false;

    function storeStringToFile(data, filePath, cb) {
        var converter = C["@mozilla.org/intl/scriptableunicodeconverter"]
            .createInstance(I.nsIScriptableUnicodeConverter);
        converter.charset = "UTF-8";

        var ostream = FileUtils.openSafeFileOutputStream(filePath);
        var istream = converter.convertToInputStream(data);

        NetUtil.asyncCopy(istream, ostream, function(status) {
            if(!Components.isSuccessCode(status)) {
                alert('Beim Schreiben der Datei ist ein Fehler aufgetreten!');
                return;
            }

            if(typeof cb === 'function') {
                cb();
            }
        });
    }

    function modalFileSaveAsDialog() {
        var nsIFilePicker = I.nsIFilePicker;
        var fp = C["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);

        fp.init(window, 'Datei speichern', nsIFilePicker.modeSave);
        fp.appendFilters(nsIFilePicker.filterText | nsIFilePicker.filterAll);

        return new Promise(function(resolve, reject) {
            var res = fp.open(function(res) {
                if (res === nsIFilePicker.returnOK || res === nsIFilePicker.returnReplace) {
                    resolve(fp.file);
                } else {
                    reject();
                }
            });
        });
    }

    function modalAskSaveChanges() {
        if(!fileChanged) {
            return false;
        }

        var ps = C["@mozilla.org/embedcomp/prompt-service;1"].getService(I.nsIPromptService);
        var rv = ps.confirmEx(window, 'Geierlein',
            'Am Formular wurden Änderungen vorgenommen, die noch nicht ' +
            'gespeichert wurden.  Sollen diese gespeichert werden?',
            ps.BUTTON_TITLE_SAVE * ps.BUTTON_POS_0 +
            ps.BUTTON_TITLE_DONT_SAVE * ps.BUTTON_POS_1 +
            ps.BUTTON_TITLE_CANCEL * ps.BUTTON_POS_2,
            null, null, null, null, {});

        if(rv === 0) {  /* save */
            xulapp.saveFile();
        } else if(rv === 2) {   /* cancel */
            return true;
        }

        return false;   /* proceed to new/open/quit. */
    }

    window.addEventListener("load", function() {
        cW = doc.contentWindow;
        cW.$('body')
            .removeClass('native')
            .addClass('xulapp')
            .on('reset-form', xulapp.autofillTimeRange)
            .on('send-taxcase', function(ev, asTestcase) {
                if(cW.geierlein.prefstore.getBoolPref('autosave.geierfile')) {
                    var fp = xulapp.getAutosaveFilepath();
                    var data = cW.geierlein.serialize();
                    storeStringToFile(data, fp);
                }

                if(!asTestcase) {
                    cW.geierlein.prefstore.setIntPref('autofill.time.lastyear', cW.$('#jahr').val());
                    cW.geierlein.prefstore.setIntPref('autofill.time.lastmonth', cW.$('#zeitraum').val());
                }
            })
            .on('show-protocol', function(ev, res) {
                if(cW.geierlein.prefstore.getBoolPref('autosave.protocol')) {
                    var fp = xulapp.getAutosaveFilepath('.proto.xml');
                    storeStringToFile(res, fp);
                }
            });

        /* Get nsICommandLine instance. */
        var cmdLine = window.arguments[0];
        cmdLine = cmdLine.QueryInterface(I.nsICommandLine);

        /* Check command line arguments for -load flag. */
        var loadFlag = cmdLine.handleFlagWithParam('load', false);
        if(loadFlag) {
            var fp;

            if(loadFlag.substr(0, 1) === '/') {
                /* Absolute path name */
                fp = C["@mozilla.org/file/local;1"].createInstance(I.nsILocalFile);
                fp.initWithPath(loadFlag);
            } else {
                /* Relative path name */
                var dirService = C["@mozilla.org/file/directory_service;1"].getService(I.nsIProperties);
                var curProcDir = dirService.get("CurWorkD", I.nsIFile);

                fp = curProcDir.clone().QueryInterface(I.nsILocalFile);
                fp.appendRelativePath(loadFlag);
            }

            if(!fp.exists()) {
                alert('Die angegebene Datei existiert nicht.');
            } else if(!fp.isReadable()) {
                alert('Die angegebene Datei kann nicht gelesen werden.');
            } else {
                xulapp.openFile(fp);
            }
        } else {
            /* Load default address data from preferences system. */
            cW.geierlein.loadDefaultAddressData();
            xulapp.autofillTimeRange();
        }

        if(cW.geierlein.isDatenlieferantValid()) {
            cW.$('#accordion-unternehmer').collapse();
        }

        /* Bind change-handler on form to notice changes. */
        cW.$('.ustva, .datenlieferant').on('change keyup', function() {
            fileChanged = true;
        });

        /* Bind save-button in protocol popup. */
        cW.$('#protocol-save').click(function() {
            modalFileSaveAsDialog().then(function(file) {
                var src = cW.$('#protocol-frame')[0].src;
                src = decodeURIComponent(src.substr(src.indexOf(',') + 1));
                storeStringToFile(src, file);
            });
        });

        /* Bind external links to open in default browser. */
        cW.$('a[href^="http"]').click(function(ev) {
            var ios = C['@mozilla.org/network/io-service;1']
                .getService(I.nsIIOService);
            var uri = ios.newURI(ev.target.href, null, null);
            var extps = C['@mozilla.org/uriloader/external-protocol-service;1']
                .getService(I.nsIExternalProtocolService);
            extps.loadURI(uri, null);
            return false;
        });

        /* We're running in chrome context, no need for reverse proxying. */
        cW.geierlein.transfer = cW.geierlein.transferDirect;

        /* Show developer menu if allowed by pref. */
        if(cW.geierlein.prefstore.getBoolPref('debug.showDevelMenu')) {
            var elements = document.getElementsByClassName('hideDevel');

            while(elements.length) {
                elements[0].className = '';
            }
        }
    }, false);

    window.addEventListener("close", function(event) {
        if(!xulapp.shutdownQuery()) {
            event.preventDefault();
        }
    }, false);

    return {
        autofillTimeRange: function() {
            switch(cW.geierlein.prefstore.getIntPref('autofill.time.mode')) {
                case 1: /* last month */
                    /* do nothing, it's Geierlein's default to preselect
                       the last month. */
                    break;
                case 2: /* last quarter */
                    var d = new Date();
                    d.setMonth(d.getMonth() - (d.getMonth() % 3));
                    d.setMonth(d.getMonth() - 3);
                    cW.$('#jahr').val(d.getFullYear()).change();
                    cW.$('#zeitraum').val((d.getMonth() / 3) + 41).change();
                    break;
                case 3: /* last transmission date */
                    try {
                        var year = cW.geierlein.prefstore.getIntPref('autofill.time.lastyear');
                        var month = cW.geierlein.prefstore.getIntPref('autofill.time.lastmonth');
                        if(month === 12) {
                            /* Select first month of next year */
                            month = 1;
                            year ++;
                        } else if(month === 44) {
                            /* Select first quarter of next year */
                            month = 41;
                            year ++;
                        } else {
                            month ++;
                        }
                        cW.$('#jahr').val(year).change();
                        cW.$('#zeitraum').val(month).change();
                    } catch(e) {}
                    break;
            }
        },

        /* Get autosave-dir as nsILocalFile instance.
         *
         * If the geierlein.autosave.dir preference is set, the path specified
         * by the preference is returned.  If it doesn't exist, a folder named
         * "protos" is created within the profile directory and returned.
         */
        getAutosaveDir: function() {
            var fp;
            try {
                fp = cW.geierlein.prefstore.prefs.getComplexValue("autosave.dir", I.nsILocalFile);
            } catch(e) {
                var dirService = C["@mozilla.org/file/directory_service;1"].getService(I.nsIProperties);
                var curProcDir = dirService.get("PrefD", I.nsIFile);

                fp = curProcDir.clone().QueryInterface(I.nsILocalFile);
                fp.append('protos');
                if(!fp.exists()) {
                    fp.create(I.nsIFile.DIRECTORY_TYPE, 448 /* octal 0700 */);
                }
            }
            return fp;
        },

        getAutosaveFilepath: function(suffix) {
            var fp = xulapp.getAutosaveDir();
            var fname = cW.geierlein.getTaxcaseIdentifier();

            function pad(n) {
                return ("0" + n).substr(-2);
            }

            var d = new Date();
            fname += "_" + d.getFullYear() + pad(d.getMonth() + 1) + pad(d.getDate());
            fname += "_" + pad(d.getHours()) + pad(d.getMinutes()) + pad(d.getSeconds());

            if(suffix !== undefined) {
                fname += suffix;
            }

            fp.append(fname);
            return fp;
        },

        resetForm: function() {
            if(modalAskSaveChanges()) {
                return; /* User asked to cancel. */
            }

            cW.geierlein.resetForm();
            filePath = undefined;
            fileChanged = false;
        },

        openPrefWindow: function() {
            var features = "chrome,titlebar,toolbar,centerscreen,dialog=yes";
            window.openDialog('chrome://geierlein/content/pref/pref.xul', "Einstellungen", features, xulapp);
        },

        openFile: function(loadFilePath) {
            if(modalAskSaveChanges()) {
                return; /* User asked to cancel. */
            }

            if(loadFilePath !== undefined) {
                loadFilePath = Promise.resolve(loadFilePath);
            } else {
                var nsIFilePicker = I.nsIFilePicker;
                var fp = C["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);

                fp.init(window, 'Datei öffnen', nsIFilePicker.modeOpen);
                fp.appendFilters(nsIFilePicker.filterText | nsIFilePicker.filterAll);

                loadFilePath = new Promise(function(resolve, reject) {
                    fp.open(function(res) {
                        if (res !== nsIFilePicker.returnOK) {
                            reject();
                        } else {
                            resolve(fp.file);
                        }
                    });
                });
            }

            loadFilePath.then(function(file) {
                var channel = NetUtil.newChannel(file);
                channel.contentType = 'text/xml';
                channel.contentCharset = 'UTF-8';

                NetUtil.asyncFetch(channel, function(inStream, status) {
                    if(!Components.isSuccessCode(status)) {
                        alert('Beim Lesen der Datei ist ein Fehler aufgetreten!');
                        return;
                    }

                    var data = NetUtil.readInputStreamToString(inStream,
                        inStream.available(), { charset: 'UTF-8' });

                    if(cW.geierlein.unserialize(data)) {
                        filePath = file;
                        fileChanged = false;
                    } else {
                        alert('Das Format der Datei ist fehlerhaft.  ' +
                            'Die Datei kann nicht geöffnet werden');
                    }
                });
            });
        },

        saveFile: function() {
            if(filePath === undefined) {
                return xulapp.saveFileAs();
            }

            var data = cW.geierlein.serialize();
            storeStringToFile(data, filePath, function() {
                fileChanged = false;
            });
        },

        saveFileAs: function() {
            modalFileSaveAsDialog().then(function(file) {
                /* Store chosen filepath and call the saveFile function (again). */
                filePath = file;
                xulapp.saveFile();
            });
        },

        send: function(asTestcase) {
            return cW.geierlein.startSendData(asTestcase);
        },

        showInfo: function() {
            cW.$('#about').modal();
        },

        showUStSvzA: function() {
            cW.geierlein.showUStSvzA();
        },

        reprintProto: function() {
            var nsIFilePicker = I.nsIFilePicker;
            var fp = C["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);

            fp.init(window, 'Protokoll nachdrucken', nsIFilePicker.modeOpen);
            fp.appendFilters(nsIFilePicker.filterText | nsIFilePicker.filterAll);

            fp.open(function(res) {
                if (res !== nsIFilePicker.returnOK) {
                    return;
                }

                var channel = NetUtil.newChannel(fp.file);
                channel.contentType = 'text/xml';
                channel.contentCharset = 'UTF-8';

                NetUtil.asyncFetch(channel, function(inStream, status) {
                    if(!Components.isSuccessCode(status)) {
                        alert('Beim Lesen der Datei ist ein Fehler aufgetreten!');
                        return;
                    }

                    var data = NetUtil.readInputStreamToString(inStream,
                        inStream.available(), { charset: 'UTF-8' });

                    cW.geierlein.showProtocol(data);
                });
            });
        },

        shutdownQuery: function() {
            return !modalAskSaveChanges();
        },

        shutdown: function() {
            if(!xulapp.shutdownQuery()) {
                return;
            }

            var appStartup = C['@mozilla.org/toolkit/app-startup;1'].getService(I.nsIAppStartup);
            appStartup.quit(I.nsIAppStartup.eAttemptQuit);
        }
    };
}());
<commit_msg>nsILocalFile -> nsIFile<commit_after>/**
 * Application logic for Geierlein XUL-based interface.
 *
 * @author Stefan Siegl
 *
 * Copyright (c) 2012, 2013 Stefan Siegl <stesie@brokenpipe.de>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/*global
  document, window, alert,
  Components, FileUtils, NetUtil
 */

var xulapp = (function() {
    "use strict";

    Components.utils.import("resource://gre/modules/NetUtil.jsm");
    Components.utils.import("resource://gre/modules/FileUtils.jsm");
    var C = Components.classes;
    var I = Components.interfaces;

    var doc = document.getElementById('doc');
    var cW = null;
    var filePath;
    var fileChanged = false;

    function storeStringToFile(data, filePath, cb) {
        var converter = C["@mozilla.org/intl/scriptableunicodeconverter"]
            .createInstance(I.nsIScriptableUnicodeConverter);
        converter.charset = "UTF-8";

        var ostream = FileUtils.openSafeFileOutputStream(filePath);
        var istream = converter.convertToInputStream(data);

        NetUtil.asyncCopy(istream, ostream, function(status) {
            if(!Components.isSuccessCode(status)) {
                alert('Beim Schreiben der Datei ist ein Fehler aufgetreten!');
                return;
            }

            if(typeof cb === 'function') {
                cb();
            }
        });
    }

    function modalFileSaveAsDialog() {
        var nsIFilePicker = I.nsIFilePicker;
        var fp = C["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);

        fp.init(window, 'Datei speichern', nsIFilePicker.modeSave);
        fp.appendFilters(nsIFilePicker.filterText | nsIFilePicker.filterAll);

        return new Promise(function(resolve, reject) {
            var res = fp.open(function(res) {
                if (res === nsIFilePicker.returnOK || res === nsIFilePicker.returnReplace) {
                    resolve(fp.file);
                } else {
                    reject();
                }
            });
        });
    }

    function modalAskSaveChanges() {
        if(!fileChanged) {
            return false;
        }

        var ps = C["@mozilla.org/embedcomp/prompt-service;1"].getService(I.nsIPromptService);
        var rv = ps.confirmEx(window, 'Geierlein',
            'Am Formular wurden Änderungen vorgenommen, die noch nicht ' +
            'gespeichert wurden.  Sollen diese gespeichert werden?',
            ps.BUTTON_TITLE_SAVE * ps.BUTTON_POS_0 +
            ps.BUTTON_TITLE_DONT_SAVE * ps.BUTTON_POS_1 +
            ps.BUTTON_TITLE_CANCEL * ps.BUTTON_POS_2,
            null, null, null, null, {});

        if(rv === 0) {  /* save */
            xulapp.saveFile();
        } else if(rv === 2) {   /* cancel */
            return true;
        }

        return false;   /* proceed to new/open/quit. */
    }

    window.addEventListener("load", function() {
        cW = doc.contentWindow;
        cW.$('body')
            .removeClass('native')
            .addClass('xulapp')
            .on('reset-form', xulapp.autofillTimeRange)
            .on('send-taxcase', function(ev, asTestcase) {
                if(cW.geierlein.prefstore.getBoolPref('autosave.geierfile')) {
                    var fp = xulapp.getAutosaveFilepath();
                    var data = cW.geierlein.serialize();
                    storeStringToFile(data, fp);
                }

                if(!asTestcase) {
                    cW.geierlein.prefstore.setIntPref('autofill.time.lastyear', cW.$('#jahr').val());
                    cW.geierlein.prefstore.setIntPref('autofill.time.lastmonth', cW.$('#zeitraum').val());
                }
            })
            .on('show-protocol', function(ev, res) {
                if(cW.geierlein.prefstore.getBoolPref('autosave.protocol')) {
                    var fp = xulapp.getAutosaveFilepath('.proto.xml');
                    storeStringToFile(res, fp);
                }
            });

        /* Get nsICommandLine instance. */
        var cmdLine = window.arguments[0];
        cmdLine = cmdLine.QueryInterface(I.nsICommandLine);

        /* Check command line arguments for -load flag. */
        var loadFlag = cmdLine.handleFlagWithParam('load', false);
        if(loadFlag) {
            var fp;

            if(loadFlag.substr(0, 1) === '/') {
                /* Absolute path name */
                fp = C["@mozilla.org/file/local;1"].createInstance(I.nsIFile);
                fp.initWithPath(loadFlag);
            } else {
                /* Relative path name */
                var dirService = C["@mozilla.org/file/directory_service;1"].getService(I.nsIProperties);
                var curProcDir = dirService.get("CurWorkD", I.nsIFile);

                fp = curProcDir.clone().QueryInterface(I.nsIFile);
                fp.appendRelativePath(loadFlag);
            }

            if(!fp.exists()) {
                alert('Die angegebene Datei existiert nicht.');
            } else if(!fp.isReadable()) {
                alert('Die angegebene Datei kann nicht gelesen werden.');
            } else {
                xulapp.openFile(fp);
            }
        } else {
            /* Load default address data from preferences system. */
            cW.geierlein.loadDefaultAddressData();
            xulapp.autofillTimeRange();
        }

        if(cW.geierlein.isDatenlieferantValid()) {
            cW.$('#accordion-unternehmer').collapse();
        }

        /* Bind change-handler on form to notice changes. */
        cW.$('.ustva, .datenlieferant').on('change keyup', function() {
            fileChanged = true;
        });

        /* Bind save-button in protocol popup. */
        cW.$('#protocol-save').click(function() {
            modalFileSaveAsDialog().then(function(file) {
                var src = cW.$('#protocol-frame')[0].src;
                src = decodeURIComponent(src.substr(src.indexOf(',') + 1));
                storeStringToFile(src, file);
            });
        });

        /* Bind external links to open in default browser. */
        cW.$('a[href^="http"]').click(function(ev) {
            var ios = C['@mozilla.org/network/io-service;1']
                .getService(I.nsIIOService);
            var uri = ios.newURI(ev.target.href, null, null);
            var extps = C['@mozilla.org/uriloader/external-protocol-service;1']
                .getService(I.nsIExternalProtocolService);
            extps.loadURI(uri, null);
            return false;
        });

        /* We're running in chrome context, no need for reverse proxying. */
        cW.geierlein.transfer = cW.geierlein.transferDirect;

        /* Show developer menu if allowed by pref. */
        if(cW.geierlein.prefstore.getBoolPref('debug.showDevelMenu')) {
            var elements = document.getElementsByClassName('hideDevel');

            while(elements.length) {
                elements[0].className = '';
            }
        }
    }, false);

    window.addEventListener("close", function(event) {
        if(!xulapp.shutdownQuery()) {
            event.preventDefault();
        }
    }, false);

    return {
        autofillTimeRange: function() {
            switch(cW.geierlein.prefstore.getIntPref('autofill.time.mode')) {
                case 1: /* last month */
                    /* do nothing, it's Geierlein's default to preselect
                       the last month. */
                    break;
                case 2: /* last quarter */
                    var d = new Date();
                    d.setMonth(d.getMonth() - (d.getMonth() % 3));
                    d.setMonth(d.getMonth() - 3);
                    cW.$('#jahr').val(d.getFullYear()).change();
                    cW.$('#zeitraum').val((d.getMonth() / 3) + 41).change();
                    break;
                case 3: /* last transmission date */
                    try {
                        var year = cW.geierlein.prefstore.getIntPref('autofill.time.lastyear');
                        var month = cW.geierlein.prefstore.getIntPref('autofill.time.lastmonth');
                        if(month === 12) {
                            /* Select first month of next year */
                            month = 1;
                            year ++;
                        } else if(month === 44) {
                            /* Select first quarter of next year */
                            month = 41;
                            year ++;
                        } else {
                            month ++;
                        }
                        cW.$('#jahr').val(year).change();
                        cW.$('#zeitraum').val(month).change();
                    } catch(e) {}
                    break;
            }
        },

        /* Get autosave-dir as nsIFile instance.
         *
         * If the geierlein.autosave.dir preference is set, the path specified
         * by the preference is returned.  If it doesn't exist, a folder named
         * "protos" is created within the profile directory and returned.
         */
        getAutosaveDir: function() {
            var fp;
            try {
                fp = cW.geierlein.prefstore.prefs.getComplexValue("autosave.dir", I.nsIFile);
            } catch(e) {
                var dirService = C["@mozilla.org/file/directory_service;1"].getService(I.nsIProperties);
                var curProcDir = dirService.get("PrefD", I.nsIFile);

                fp = curProcDir.clone().QueryInterface(I.nsIFile);
                fp.append('protos');
                if(!fp.exists()) {
                    fp.create(I.nsIFile.DIRECTORY_TYPE, 448 /* octal 0700 */);
                }
            }
            return fp;
        },

        getAutosaveFilepath: function(suffix) {
            var fp = xulapp.getAutosaveDir();
            var fname = cW.geierlein.getTaxcaseIdentifier();

            function pad(n) {
                return ("0" + n).substr(-2);
            }

            var d = new Date();
            fname += "_" + d.getFullYear() + pad(d.getMonth() + 1) + pad(d.getDate());
            fname += "_" + pad(d.getHours()) + pad(d.getMinutes()) + pad(d.getSeconds());

            if(suffix !== undefined) {
                fname += suffix;
            }

            fp.append(fname);
            return fp;
        },

        resetForm: function() {
            if(modalAskSaveChanges()) {
                return; /* User asked to cancel. */
            }

            cW.geierlein.resetForm();
            filePath = undefined;
            fileChanged = false;
        },

        openPrefWindow: function() {
            var features = "chrome,titlebar,toolbar,centerscreen,dialog=yes";
            window.openDialog('chrome://geierlein/content/pref/pref.xul', "Einstellungen", features, xulapp);
        },

        openFile: function(loadFilePath) {
            if(modalAskSaveChanges()) {
                return; /* User asked to cancel. */
            }

            if(loadFilePath !== undefined) {
                loadFilePath = Promise.resolve(loadFilePath);
            } else {
                var nsIFilePicker = I.nsIFilePicker;
                var fp = C["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);

                fp.init(window, 'Datei öffnen', nsIFilePicker.modeOpen);
                fp.appendFilters(nsIFilePicker.filterText | nsIFilePicker.filterAll);

                loadFilePath = new Promise(function(resolve, reject) {
                    fp.open(function(res) {
                        if (res !== nsIFilePicker.returnOK) {
                            reject();
                        } else {
                            resolve(fp.file);
                        }
                    });
                });
            }

            loadFilePath.then(function(file) {
                var channel = NetUtil.newChannel(file);
                channel.contentType = 'text/xml';
                channel.contentCharset = 'UTF-8';

                NetUtil.asyncFetch(channel, function(inStream, status) {
                    if(!Components.isSuccessCode(status)) {
                        alert('Beim Lesen der Datei ist ein Fehler aufgetreten!');
                        return;
                    }

                    var data = NetUtil.readInputStreamToString(inStream,
                        inStream.available(), { charset: 'UTF-8' });

                    if(cW.geierlein.unserialize(data)) {
                        filePath = file;
                        fileChanged = false;
                    } else {
                        alert('Das Format der Datei ist fehlerhaft.  ' +
                            'Die Datei kann nicht geöffnet werden');
                    }
                });
            });
        },

        saveFile: function() {
            if(filePath === undefined) {
                return xulapp.saveFileAs();
            }

            var data = cW.geierlein.serialize();
            storeStringToFile(data, filePath, function() {
                fileChanged = false;
            });
        },

        saveFileAs: function() {
            modalFileSaveAsDialog().then(function(file) {
                /* Store chosen filepath and call the saveFile function (again). */
                filePath = file;
                xulapp.saveFile();
            });
        },

        send: function(asTestcase) {
            return cW.geierlein.startSendData(asTestcase);
        },

        showInfo: function() {
            cW.$('#about').modal();
        },

        showUStSvzA: function() {
            cW.geierlein.showUStSvzA();
        },

        reprintProto: function() {
            var nsIFilePicker = I.nsIFilePicker;
            var fp = C["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);

            fp.init(window, 'Protokoll nachdrucken', nsIFilePicker.modeOpen);
            fp.appendFilters(nsIFilePicker.filterText | nsIFilePicker.filterAll);

            fp.open(function(res) {
                if (res !== nsIFilePicker.returnOK) {
                    return;
                }

                var channel = NetUtil.newChannel(fp.file);
                channel.contentType = 'text/xml';
                channel.contentCharset = 'UTF-8';

                NetUtil.asyncFetch(channel, function(inStream, status) {
                    if(!Components.isSuccessCode(status)) {
                        alert('Beim Lesen der Datei ist ein Fehler aufgetreten!');
                        return;
                    }

                    var data = NetUtil.readInputStreamToString(inStream,
                        inStream.available(), { charset: 'UTF-8' });

                    cW.geierlein.showProtocol(data);
                });
            });
        },

        shutdownQuery: function() {
            return !modalAskSaveChanges();
        },

        shutdown: function() {
            if(!xulapp.shutdownQuery()) {
                return;
            }

            var appStartup = C['@mozilla.org/toolkit/app-startup;1'].getService(I.nsIAppStartup);
            appStartup.quit(I.nsIAppStartup.eAttemptQuit);
        }
    };
}());
<|endoftext|>


<commit_before>define(['marionette', 'tpl!templates/stats/breakdown.html',
    'utils',
    'jquery',
    'jquery.flot',
    'jquery.flot.time',
    'jquery.flot.selection',
    'jquery.flot.tooltip',
    ], function(Marionette, template, utils, $) {

    return Marionette.ItemView.extend({
        template: template,

        modelEvents: {
            'sync': 'onDomRefresh'
        },

        events: {
            'plotselected #dc_hist': 'zoomTime',
            'plotselected #avg_time': 'zoomTime',
            'plotclick #avg_time': 'showDC',
            'plotselected #overview': 'zoomOverview',
            'click a[name=reset]': 'resetPlots',
        },

        onRender: function() {
            if (this.getOption('large')) this.$el.find('#avg_time').addClass('large')
        },

        resetPlots: function(e) {
            e.preventDefault()
            this.main.setSelection({ xaxis: { from: this.model.get('info').start, to: this.model.get('info').end } })
            this.overview.clearSelection();
        },

        zoomTime: function(e, ranges) {
            _.each([this.main, this.extra], function(p) {
                var opts = p.getOptions()
                opts.xaxes[0].min = ranges.xaxis.from
                opts.xaxes[0].max = ranges.xaxis.to

                p.setupGrid()
                p.draw()
                p.clearSelection()
            })

            this.overview.setSelection(ranges, true);
        },

        zoomOverview: function(e, ranges) {
            this.main.setSelection(ranges);
        },

        showDC:  function(e, pos, item) {
            if (!item) return

            var types = {
                dc: 'dc',
                ed: 'edge',
                mca: 'mca',
            }
            if (item.series.id) {
                app.trigger('dc:show', types[item.series.type], item.series.id, item.series.visit)
            }
        },

        onDomRefresh: function() {
            if (this.model.get('data')) {
                this.options = {
                  grid: {
                      borderWidth: 0,
                  },

                  selection: { mode: 'x' },

                  bars: {
                    horizontal: true,
                    show: true,
                    lineWidth: 0.5,
                    barWidth: 0.8,
                    stack: true,
                  },

                  xaxis: {
                        mode: 'time',
                        timezone: 'Europe/London',
                        min: this.model.get('info').start,
                        max: this.model.get('info').end,
                  },

                  yaxis: {
                    show: false,
                  },
                }

                this.options2 = _.extend({}, this.options);
                this.options2.tooltip = true;
                this.options2.grid.hoverable = true
                this.options2.grid.clickable = true
                this.options2.tooltipOpts = { content: this.getToolTip.bind(this) }

                var dc = _.where(this.model.get('data'), { type: 'dc' })
                var pids = _.unique(_.pluck(this.model.get('data'), 'pid'))
                var cols = utils.getColors(pids.length)

                _.each(dc, function(d) {
                    d.color = cols[pids.indexOf(d.pid)]
                })

                // var markings = []
                // var vis = _.where(this.model.get('data'), { type: 'visit' })
                // _.each(vis, function(v) {
                //     var vdc = _.where(dc, { visit: v['visit'] })

                //     if (!vdc.length) return

                //     var f = _.first(vdc)
                //     var l = _.last(vdc)

                //     markings.push({ color: '#000', lineWidth: 1, yaxis: { from: 0, to: v['data'][0][1] }, xaxis: { from: f['data'][0][0], to: f['data'][0][0] } })
                //     markings.push({ color: '#000', lineWidth: 1, yaxis: { from: 0, to: v['data'][1][1] }, xaxis: { from: l['data'][1][2], to: l['data'][1][2] } })
                // }, this)

                // this.options2.grid.markings = markings

                this.main = $.plot(this.$el.find('#avg_time'), this.model.get('data'), this.options2);
                this.overview = $.plot(this.$el.find('#overview'), this.model.get('data'), this.options);

                this.options3 = {
                    xaxis: {
                        mode: 'time',
                        timezone: 'Europe/London',
                        min: this.model.get('info').start,
                        max: this.model.get('info').end,
                    },

                    tooltip: true,
                    grid: {
                        hoverable: true,
                        borderWidth: 0,
                    },

                    yaxes: [{ position: 'right' }, { position: 'right' }, { position: 'right' }],
                }
                this.extra = $.plot(this.$el.find('#dc_hist'), this.model.get('lines'), this.options3);

            }

        },

        getToolTip: function(lab, x, y, item) {
            var titles = {
                ai: 'Auto Indexing',
                dc: 'Data Collection',
                mca: 'MCA Spectrum',
                ed: 'Edge Scan',
                robot: 'Robot Action',
                fault: 'Fault',
                nobeam: 'Beam Dump',
                cent: 'Centring',
                visit: 'Visit',
            }

            var len = (item.datapoint[0] - item.datapoint[2]) / 1000
            if (len > 3600) {
                len = (len/3600).toFixed(1) + 'hrs'
            } else if (len > 60) {
                len = (len/60).toFixed(1) + 'mins'
            } else {
                len += 's'
            }
            return titles[item.series.type] + ': ' + len + ' ' + (item.series.status ? item.series.status : '')
        },
    })

})<commit_msg>colour queued visits<commit_after>define(['marionette', 'tpl!templates/stats/breakdown.html',
    'utils',
    'jquery',
    'jquery.flot',
    'jquery.flot.time',
    'jquery.flot.selection',
    'jquery.flot.tooltip',
    ], function(Marionette, template, utils, $) {

    return Marionette.ItemView.extend({
        template: template,

        modelEvents: {
            'sync': 'onDomRefresh'
        },

        events: {
            'plotselected #dc_hist': 'zoomTime',
            'plotselected #avg_time': 'zoomTime',
            'plotclick #avg_time': 'showDC',
            'plotselected #overview': 'zoomOverview',
            'click a[name=reset]': 'resetPlots',
        },

        onRender: function() {
            if (this.getOption('large')) this.$el.find('#avg_time').addClass('large')
        },

        resetPlots: function(e) {
            e.preventDefault()
            this.main.setSelection({ xaxis: { from: this.model.get('info').start, to: this.model.get('info').end } })
            this.overview.clearSelection();
        },

        zoomTime: function(e, ranges) {
            _.each([this.main, this.extra], function(p) {
                var opts = p.getOptions()
                opts.xaxes[0].min = ranges.xaxis.from
                opts.xaxes[0].max = ranges.xaxis.to

                p.setupGrid()
                p.draw()
                p.clearSelection()
            })

            this.overview.setSelection(ranges, true);
        },

        zoomOverview: function(e, ranges) {
            this.main.setSelection(ranges);
        },

        showDC:  function(e, pos, item) {
            if (!item) return

            var types = {
                dc: 'dc',
                ed: 'edge',
                mca: 'mca',
            }
            if (item.series.id) {
                app.trigger('dc:show', types[item.series.type], item.series.id, item.series.visit)
            }
        },

        onDomRefresh: function() {
            if (this.model.get('data')) {
                this.options = {
                  grid: {
                      borderWidth: 0,
                  },

                  selection: { mode: 'x' },

                  bars: {
                    horizontal: true,
                    show: true,
                    lineWidth: 0.5,
                    barWidth: 0.8,
                    stack: true,
                  },

                  xaxis: {
                        mode: 'time',
                        timezone: 'Europe/London',
                        min: this.model.get('info').start,
                        max: this.model.get('info').end,
                  },

                  yaxis: {
                    show: false,
                  },
                }

                this.options2 = _.extend({}, this.options);
                this.options2.tooltip = true;
                this.options2.grid.hoverable = true
                this.options2.grid.clickable = true
                this.options2.tooltipOpts = { content: this.getToolTip.bind(this) }

                var dc = _.where(this.model.get('data'), { type: 'dc' })
                var pids = _.unique(_.pluck(dc, 'pid'))
                var cols = utils.shuffle(utils.getColors(pids.length))
                _.each(dc, function(d) {
                    if (d.pid) d.color = cols[pids.indexOf(d.pid)]
                })

                var vis = _.where(this.model.get('data'), { type: 'visit_ns' })
                var vids = _.unique(_.pluck(vis, 'visit'))
                var cols = utils.shuffle(utils.getColors(vids.length))
                _.each(vis, function(v) {
                    v.color = cols[vids.indexOf(v.visit)]
                })

                // var markings = []
                // var vis = _.where(this.model.get('data'), { type: 'visit' })
                // _.each(vis, function(v) {
                //     var vdc = _.where(dc, { visit: v['visit'] })

                //     if (!vdc.length) return

                //     var f = _.first(vdc)
                //     var l = _.last(vdc)

                //     markings.push({ color: '#000', lineWidth: 1, yaxis: { from: 0, to: v['data'][0][1] }, xaxis: { from: f['data'][0][0], to: f['data'][0][0] } })
                //     markings.push({ color: '#000', lineWidth: 1, yaxis: { from: 0, to: v['data'][1][1] }, xaxis: { from: l['data'][1][2], to: l['data'][1][2] } })
                // }, this)

                // this.options2.grid.markings = markings

                this.main = $.plot(this.$el.find('#avg_time'), this.model.get('data'), this.options2);
                this.overview = $.plot(this.$el.find('#overview'), this.model.get('data'), this.options);

                this.options3 = {
                    xaxis: {
                        mode: 'time',
                        timezone: 'Europe/London',
                        min: this.model.get('info').start,
                        max: this.model.get('info').end,
                    },

                    tooltip: true,
                    grid: {
                        hoverable: true,
                        borderWidth: 0,
                    },

                    yaxes: [{ position: 'right' }, { position: 'right' }, { position: 'right' }],
                }
                this.extra = $.plot(this.$el.find('#dc_hist'), this.model.get('lines'), this.options3);

            }

        },

        getToolTip: function(lab, x, y, item) {
            var titles = {
                ai: 'Auto Indexing',
                dc: 'Data Collection',
                mca: 'MCA Spectrum',
                ed: 'Edge Scan',
                robot: 'Robot Action',
                fault: 'Fault',
                nobeam: 'Beam Dump',
                cent: 'Centring',
                visit: 'Visit (Scheduled)',
                visit_ns: 'Visit (Queued)',
            }

            var len = (item.datapoint[0] - item.datapoint[2]) / 1000
            if (len > 3600) {
                len = (len/3600).toFixed(1) + 'hrs'
            } else if (len > 60) {
                len = (len/60).toFixed(1) + 'mins'
            } else {
                len += 's'
            }
            return titles[item.series.type] + ': ' + len + ' ' + (item.series.status ? item.series.status : '')
        },
    })

})<|endoftext|>


<commit_before>'use strict'
const path = require('path')

module.exports = {
<<<<<<< HEAD
    port: 3000,
    title: 'React-Semantic.UI-Starter',
    publicPath: '/',
    srcPath: path.join(__dirname, './../common'),
    // add these dependencies to a standalone vendor bundle
    vendor: [
        'react',
        'react-dom',
        'react-router',
        'redux',
        'react-router-redux',
        'redux-thunk',
        'semantic-ui-react',
        'whatwg-fetch',
        'semantic-ui-css/semantic.css',
        'offline-plugin/runtime',
        'prop-types'
    ],
    // enable babelrc
    babel: {
        babelrc: true
    },
    // your web app's manifest.json
    manifest: {
        name: 'React-Semantic.UI-Starter',
        short_name: 'RSUIS',
        description: 'https://github.com/Metnew/react-semantic.ui-starter',
        icons: [
            {
                src: 'images/touch/icon-128x128.png',
                sizes: '128x128',
                type: 'image/png'
            }, {
                src: 'images/touch/apple-touch-icon.png',
                sizes: '152x152',
                type: 'image/png'
            }, {
                src: 'images/touch/ms-touch-icon-144x144-precomposed.png',
                sizes: '144x144',
                type: 'image/png'
            }, {
                src: 'images/touch/chrome-touch-icon-192x192.png',
                sizes: '192x192',
                type: 'image/png'
            }
        ],
        start_url: '.',
        display: 'standalone',
        background_color: '#f7f7f7',
        theme_color: '#1b1c1d'
    },
    cssModules: true
=======
	port: process.env.PORT || 3000,
	title: 'React-Semantic.UI-Starter',
	publicPath: '/' ,
	srcPath: path.join(__dirname, '../src'),
	srcCommonPath: path.join(__dirname, '../src/common'),
	distPath: path.join(__dirname, '../dist'),
	// your app's manifest.json
	manifest: {
		name: 'React-Semantic.UI-Starter',
		short_name: 'RSUIS',
		description: 'https://github.com/Metnew/react-semantic.ui-starter',
		icons: [
	    {
	      "src": "icons/android-chrome-36x36.png",
	      "sizes": "36x36",
	      "type": "image/png"
	    },
	    {
	      "src": "icons/android-chrome-48x48.png",
	      "sizes": "48x48",
	      "type": "image/png"
	    },
	    {
	      "src": "icons/android-chrome-72x72.png",
	      "sizes": "72x72",
	      "type": "image/png"
	    },
	    {
	      "src": "icons/android-chrome-96x96.png",
	      "sizes": "96x96",
	      "type": "image/png"
	    },
	    {
	      "src": "icons/android-chrome-144x144.png",
	      "sizes": "144x144",
	      "type": "image/png"
	    },
	    {
	      "src": "icons/android-chrome-192x192.png",
	      "sizes": "192x192",
	      "type": "image/png"
	    },
	    {
	      "src": "icons/android-chrome-256x256.png",
	      "sizes": "256x256",
	      "type": "image/png"
	    },
	    {
	      "src": "icons/android-chrome-384x384.png",
	      "sizes": "384x384",
	      "type": "image/png"
	    },
	    {
	      "src": "icons/android-chrome-512x512.png",
	      "sizes": "512x512",
	      "type": "image/png"
	    }
	  ],
		start_url: '.',
		display: 'standalone',
		background_color: '#f7f7f7',
		theme_color: '#009688'
	}
>>>>>>> master
}
<commit_msg>fix(webpack_config/config): fix merge<commit_after>'use strict'
const path = require('path')

module.exports = {
	port: process.env.PORT || 3000,
	title: 'React-Semantic.UI-Starter',
	publicPath: '/' ,
	srcPath: path.join(__dirname, '../src'),
	srcCommonPath: path.join(__dirname, '../src/common'),
	distPath: path.join(__dirname, '../dist'),
	// your app's manifest.json
	manifest: {
		name: 'React-Semantic.UI-Starter',
		short_name: 'RSUIS',
		description: 'https://github.com/Metnew/react-semantic.ui-starter',
		icons: [
	    {
	      "src": "icons/android-chrome-36x36.png",
	      "sizes": "36x36",
	      "type": "image/png"
	    },
	    {
	      "src": "icons/android-chrome-48x48.png",
	      "sizes": "48x48",
	      "type": "image/png"
	    },
	    {
	      "src": "icons/android-chrome-72x72.png",
	      "sizes": "72x72",
	      "type": "image/png"
	    },
	    {
	      "src": "icons/android-chrome-96x96.png",
	      "sizes": "96x96",
	      "type": "image/png"
	    },
	    {
	      "src": "icons/android-chrome-144x144.png",
	      "sizes": "144x144",
	      "type": "image/png"
	    },
	    {
	      "src": "icons/android-chrome-192x192.png",
	      "sizes": "192x192",
	      "type": "image/png"
	    },
	    {
	      "src": "icons/android-chrome-256x256.png",
	      "sizes": "256x256",
	      "type": "image/png"
	    },
	    {
	      "src": "icons/android-chrome-384x384.png",
	      "sizes": "384x384",
	      "type": "image/png"
	    },
	    {
	      "src": "icons/android-chrome-512x512.png",
	      "sizes": "512x512",
	      "type": "image/png"
	    }
	  ],
		start_url: '.',
		display: 'standalone',
		background_color: '#f7f7f7',
		theme_color: '#009688'
	}
}
<|endoftext|>


<commit_before>/*
Add this file to $(jupyter --data-dir)/nbextensions/gist.js
And load it with:

require(["nbextensions/gist"], function (gist_extension) {
    console.log('gist extension loaded');
    gist_extension.load_ipython_extension();
});

or add the following to your jupyter_notebook_config.py to
load for every notebook

from notebook.services.config import ConfigManager
cm = ConfigManager()
cm.update('notebook', {"load_extensions": {"gist": True}})
*/

function get_base_path() {
    var loc = window.location;
    var proto = loc.protocol;
    var host = loc.hostname;
    var port = loc.port;

    var base = proto + "//" + host;
    if (parseInt(port) != 80) {
        base += ":" + port;
    }
    return base;
}

function url_path_split(path) {
    var idx = path.lastIndexOf('/');
    if (idx === -1) {
        return ['', path];
    } else {
        return [ path.slice(0, idx), path.slice(idx + 1) ];
    }
}

function is_url_valid(url) {
    return /^(https?|s?ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test(url);
}

define(function () {
    var github_redirect_uri = get_base_path() + "/create_gist";
    var gist_notebook = function () {
        // Save the notebook and create a checkpoint to ensure that we create
        // the gist using the most up-to-date content
        Jupyter.notebook.save_checkpoint();

        var github_client_id = Jupyter.notebook.config.data.oauth_client_id;
        // Get notebook path and encode it in base64
        // Characters like # get decoded by the github API and will mess up
        // getting the file path on the server if we use URI percent encoding,
        // so we use base64 instead
        var nb_path = window.btoa(Jupyter.notebook.base_url + Jupyter.notebook.notebook_path);

        // Start OAuth dialog
        window.open("https://github.com/login/oauth/authorize?client_id=" + github_client_id +
          "&scope=gist&redirect_uri=" + github_redirect_uri + "?nb_path=" + nb_path);
    };

    var load_from_url = function() {
        var url = prompt("Enter the URL to a Gist or a .ipynb.");
        if (url == "" || url == null) {
            // prompt() returns "" if empty value, or null
            // if user clicked cancel, want to abort in either case
            return;
        }
        if (!is_url_valid(url)) {
            alert('Invalid URL.');
            return;
        }
        var parser = document.createElement('a');
        parser.href = url;
        if (parser.hostname.indexOf('gist.github.com') > -1) {
            // this is a gist URL, extract the raw_url for the .ipynb file
            load_from_gist_url(url);
        } else if (url.indexOf('.ipynb', url.length - '.ipynb'.length) !== -1) {
            // URL is a raw .ipynb file
            var nb_pathname_parts = parser.pathname.split('/');
            var filename = decodeURIComponent(nb_pathname_parts[nb_pathname_parts.length - 1]);
            download_nb_on_server(url, filename, false);
        }
    }

    var load_from_gist_url = function(url) {
        // this is a gist URL, extract the raw_url for the .ipynb file
        var gist_url_parts = url.split('/');
        var gist_id = gist_url_parts[gist_url_parts.length-1];

        var gist_api_url = "https://api.github.com/gists/" + gist_id;

        var xhr = new XMLHttpRequest();
        xhr.onreadystatechange = function() {
            if (xhr.readyState == XMLHttpRequest.DONE) {
                if (xhr.status == 200) {
                    var res = JSON.parse(xhr.responseText);
                    for (var filename in res.files) {
                        if (!res.files.hasOwnProperty(filename)) continue;
                        if (filename.endsWith('.ipynb')) {
                            download_nb_on_server(res.files[filename].raw_url, filename, false);
                        }
                    }
                    console.log("no ipynb files found");
                    console.log(res);
                } else if (xhr.status == 404) {
                    alert("Gist not found")
                } else {
                    alert("Couldn't load Gist.")
                }
            }
        }
        xhr.open("GET", gist_api_url, true);
        xhr.send(null);
    }

    var download_nb_on_server = function(url, name, force_download) {
        var xhr = new XMLHttpRequest();
        var nb_info = {
            nb_url: url,
            nb_name: window.btoa(name),
            force_download: force_download
        }
        xhr.open("POST",  "/download_notebook", true);
        xhr.setRequestHeader('Content-Type', 'application/json');
        xhr.onload = function () {
            if (xhr.status == 409) {
                // 409 Conflict
                // used if file already exists
                var newname = prompt("File already exists. Please enter a new name.\nNote: This may overwrite existing files.",
                                     name);
                if (newname == "" || newname == null) {
                    // prompt() returns "" if empty value, or null
                    // if user clicked cancel, want to abort in either case
                    return;
                }
                download_nb_on_server(url, newname, true);
            } else if (xhr.status == 200) {
                window.open(url_path_split(Jupyter.notebook.notebook_path)[0] + encodeURIComponent(this.responseText));
            } else if (xhr.status == 400) {
                alert("File did not download");
            }
        };
        xhr.send(JSON.stringify(nb_info));
    }

    var load_user_gists = function() {
        // TODO: Figure out how to deal with page redirect when obtaining GitHub access code
        // For now, prompt user for their GitHub username to load their public gists
        var github_username = prompt("Please enter your GitHub username in order to retrieve your public gists.");
        if (github_username == "" || github_username == null) {
            // Do not send a request if user did not input anything
            return;
        };

        var gist_api_url = "https://api.github.com/users/"+github_username+"/gists";

        if (!is_url_valid(gist_api_url)) {
            alert('GitHub username is invalid.');
        };

        var xhr = new XMLHttpRequest();
        xhr.open("GET", gist_api_url, true);
        xhr.setRequestHeader("Content-Type", "application/json");
        xhr.onload = function(){
            Jupyter.dialog.modal({
                title: "Gists",
                body: format_user_gists(xhr.responseText),
                buttons: {
                    "OK": {}
                }
            });
        };
        xhr.send(null);
    };

    var format_user_gists = function(responseText) {
        var body = $('
').addClass("table");
        var header = $('
').addClass("row list_header");
        header.append(""+"Filename"+"	");
        header.append(""+"Description"+"	");
        header.append(""+"Last Updated" + "	");
        header.append(""+"Gist URL"+"	");
        body.append(header);
        var row, button, files;
        var json_response = JSON.parse(responseText);
        for (var i=0; i<json_response.length; i++) {
            files = json_response[i].files;
            // Only load notebook gists
            if (!files[Object.keys(files)[0]].filename.endsWith('.ipynb')) continue;
            // Create row containing gist information
            row = $('
').addClass("list_item row");
            row.append("" + files[Object.keys(files)[0]].filename + "	");
            row.append("" + json_response[i].description + "	");
            row.append("" + json_response[i].updated_at + "	");
            // Create button to load notebook
            button = $('<button>Load Gist</button>').addClass("btn btn-default btn-sm");
            button.click({url: json_response[i].html_url}, load_gist_from_click);
            button.appendTo(row);
            body.append(row);
        };

        return body;
    };

    var load_gist_from_click = function(event) {
        var url = event.data.url;
        load_from_gist_url(url);
    };

    var gist_button = function () {
        if (!Jupyter.toolbar) {
            $([Jupyter.events]).on("app_initialized.NotebookApp", gist_button);
            return;
        }
        if ($("#gist_notebook").length === 0) {
            Jupyter.toolbar.add_buttons_group([
                {
                    'label'   : 'save notebook as gist',
                    'icon'    : 'fa-github',
                    'callback': gist_notebook,
                    'id'      : 'gist_notebook'
                }, {
                    'label'   : 'load notebook from URL',
                    'icon'    : 'fa-link',
                    'callback': load_from_url,
                    'id'      : 'load_notebook_from_url'
                }, {
                    'label'   : 'load user gists',
                    'icon'    : 'fa-list-alt',
                    'callback': load_user_gists,
                    'id'      : 'load_user_gists',
                }
            ]);
        }
    };

    var load_ipython_extension = function () {
        gist_button();
    };

    return {
        load_ipython_extension: load_ipython_extension
    };
});
<commit_msg>rendering of 'last updated' date<commit_after>/*
Add this file to $(jupyter --data-dir)/nbextensions/gist.js
And load it with:

require(["nbextensions/gist"], function (gist_extension) {
    console.log('gist extension loaded');
    gist_extension.load_ipython_extension();
});

or add the following to your jupyter_notebook_config.py to
load for every notebook

from notebook.services.config import ConfigManager
cm = ConfigManager()
cm.update('notebook', {"load_extensions": {"gist": True}})
*/

function get_base_path() {
    var loc = window.location;
    var proto = loc.protocol;
    var host = loc.hostname;
    var port = loc.port;

    var base = proto + "//" + host;
    if (parseInt(port) != 80) {
        base += ":" + port;
    }
    return base;
}

function url_path_split(path) {
    var idx = path.lastIndexOf('/');
    if (idx === -1) {
        return ['', path];
    } else {
        return [ path.slice(0, idx), path.slice(idx + 1) ];
    }
}

function is_url_valid(url) {
    return /^(https?|s?ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test(url);
}

define([
    'base/js/utils',
    'moment',
    ], function (utils, moment) {
    var github_redirect_uri = get_base_path() + "/create_gist";
    var gist_notebook = function () {
        // Save the notebook and create a checkpoint to ensure that we create
        // the gist using the most up-to-date content
        Jupyter.notebook.save_checkpoint();

        var github_client_id = Jupyter.notebook.config.data.oauth_client_id;
        // Get notebook path and encode it in base64
        // Characters like # get decoded by the github API and will mess up
        // getting the file path on the server if we use URI percent encoding,
        // so we use base64 instead
        var nb_path = window.btoa(Jupyter.notebook.base_url + Jupyter.notebook.notebook_path);

        // Start OAuth dialog
        window.open("https://github.com/login/oauth/authorize?client_id=" + github_client_id +
          "&scope=gist&redirect_uri=" + github_redirect_uri + "?nb_path=" + nb_path);
    };

    var load_from_url = function() {
        var url = prompt("Enter the URL to a Gist or a .ipynb.");
        if (url == "" || url == null) {
            // prompt() returns "" if empty value, or null
            // if user clicked cancel, want to abort in either case
            return;
        }
        if (!is_url_valid(url)) {
            alert('Invalid URL.');
            return;
        }
        var parser = document.createElement('a');
        parser.href = url;
        if (parser.hostname.indexOf('gist.github.com') > -1) {
            // this is a gist URL, extract the raw_url for the .ipynb file
            load_from_gist_url(url);
        } else if (url.indexOf('.ipynb', url.length - '.ipynb'.length) !== -1) {
            // URL is a raw .ipynb file
            var nb_pathname_parts = parser.pathname.split('/');
            var filename = decodeURIComponent(nb_pathname_parts[nb_pathname_parts.length - 1]);
            download_nb_on_server(url, filename, false);
        }
    }

    var load_from_gist_url = function(url) {
        // this is a gist URL, extract the raw_url for the .ipynb file
        var gist_url_parts = url.split('/');
        var gist_id = gist_url_parts[gist_url_parts.length-1];

        var gist_api_url = "https://api.github.com/gists/" + gist_id;

        var xhr = new XMLHttpRequest();
        xhr.onreadystatechange = function() {
            if (xhr.readyState == XMLHttpRequest.DONE) {
                if (xhr.status == 200) {
                    var res = JSON.parse(xhr.responseText);
                    for (var filename in res.files) {
                        if (!res.files.hasOwnProperty(filename)) continue;
                        if (filename.endsWith('.ipynb')) {
                            download_nb_on_server(res.files[filename].raw_url, filename, false);
                        }
                    }
                    console.log("no ipynb files found");
                    console.log(res);
                } else if (xhr.status == 404) {
                    alert("Gist not found")
                } else {
                    alert("Couldn't load Gist.")
                }
            }
        }
        xhr.open("GET", gist_api_url, true);
        xhr.send(null);
    }

    var download_nb_on_server = function(url, name, force_download) {
        var xhr = new XMLHttpRequest();
        var nb_info = {
            nb_url: url,
            nb_name: window.btoa(name),
            force_download: force_download
        }
        xhr.open("POST",  "/download_notebook", true);
        xhr.setRequestHeader('Content-Type', 'application/json');
        xhr.onload = function () {
            if (xhr.status == 409) {
                // 409 Conflict
                // used if file already exists
                var newname = prompt("File already exists. Please enter a new name.\nNote: This may overwrite existing files.",
                                     name);
                if (newname == "" || newname == null) {
                    // prompt() returns "" if empty value, or null
                    // if user clicked cancel, want to abort in either case
                    return;
                }
                download_nb_on_server(url, newname, true);
            } else if (xhr.status == 200) {
                window.open(url_path_split(Jupyter.notebook.notebook_path)[0] + encodeURIComponent(this.responseText));
            } else if (xhr.status == 400) {
                alert("File did not download");
            }
        };
        xhr.send(JSON.stringify(nb_info));
    }

    var load_user_gists = function() {
        // TODO: Figure out how to deal with page redirect when obtaining GitHub access code
        // For now, prompt user for their GitHub username to load their public gists
        var github_username = prompt("Please enter your GitHub username in order to retrieve your public gists.");
        if (github_username == "" || github_username == null) {
            // Do not send a request if user did not input anything
            return;
        };

        var gist_api_url = "https://api.github.com/users/"+github_username+"/gists";

        if (!is_url_valid(gist_api_url)) {
            alert('GitHub username is invalid.');
        };

        var xhr = new XMLHttpRequest();
        xhr.open("GET", gist_api_url, true);
        xhr.setRequestHeader("Content-Type", "application/json");
        xhr.onload = function(){
            Jupyter.dialog.modal({
                title: "Gists",
                body: format_user_gists(xhr.responseText),
                buttons: {
                    "OK": {}
                }
            });
        };
        xhr.send(null);
    };

    var format_user_gists = function(responseText) {
        var body = $('
').addClass("table");
        var header = $('
').addClass("row list_header");
        header.append(""+"Filename"+"	");
        header.append(""+"Description"+"	");
        header.append(""+"Last Updated" + "	");
        header.append(""+"Gist URL"+"	");
        body.append(header);
        var row, button, files, last_updated, pretty_date;
        var json_response = JSON.parse(responseText);
        for (var i=0; i<json_response.length; i++) {
            files = json_response[i].files;

            // Use same formatting for date as SaveWidget's _render_chickpoint
            last_updated = moment(json_response[i].updated_at);
            var tdelta = Math.ceil(new Date() - last_updated);
            if (tdelta < utils.time.milliseconds.d){
                // less than 24 hours old, use relative date
                pretty_date = last_updated.fromNow();
            } else {
                // otherwise show calendar
                // <Today | yesterday|...> at hh,mm,ss
                pretty_date = last_updated.calendar();
            }

            // Only load notebook gists
            if (!files[Object.keys(files)[0]].filename.endsWith('.ipynb')) continue;
            // Create row containing gist information
            row = $('
').addClass("list_item row");
            row.append("" + files[Object.keys(files)[0]].filename + "	");
            row.append("" + json_response[i].description + "	");
            row.append("" + pretty_date + "	");
            // Create button to load notebook
            button = $('<button>Load Gist</button>').addClass("btn btn-default btn-sm");
            button.click({url: json_response[i].html_url}, load_gist_from_click);
            button.appendTo(row);
            body.append(row);
        };

        return body;
    };

    var load_gist_from_click = function(event) {
        var url = event.data.url;
        load_from_gist_url(url);
    };

    var gist_button = function () {
        if (!Jupyter.toolbar) {
            $([Jupyter.events]).on("app_initialized.NotebookApp", gist_button);
            return;
        }
        if ($("#gist_notebook").length === 0) {
            Jupyter.toolbar.add_buttons_group([
                {
                    'label'   : 'save notebook as gist',
                    'icon'    : 'fa-github',
                    'callback': gist_notebook,
                    'id'      : 'gist_notebook'
                }, {
                    'label'   : 'load notebook from URL',
                    'icon'    : 'fa-link',
                    'callback': load_from_url,
                    'id'      : 'load_notebook_from_url'
                }, {
                    'label'   : 'load user gists',
                    'icon'    : 'fa-list-alt',
                    'callback': load_user_gists,
                    'id'      : 'load_user_gists',
                }
            ]);
        }
    };

    var load_ipython_extension = function () {
        gist_button();
    };

    return {
        load_ipython_extension: load_ipython_extension
    };
});
<|endoftext|>


<commit_before>import forUsers from './Search/forUsers'
import forPosts from './Search/forPosts'
import { countTotal } from '../../lib/util/knex'
import addTermToQueryBuilder from './Search/addTermToQueryBuilder'
import { transform } from 'lodash'
import { flatten, flow, uniq, get } from 'lodash/fp'

module.exports = {
  forPosts,
  forUsers,

  forCommunities: function (opts) {
    return Community.query(qb => {
      if (opts.communities) {
        qb.whereIn('communities.id', opts.communities)
      }

      if (opts.autocomplete) {
        qb.whereRaw('communities.name ilike ?', opts.autocomplete + '%')
      }

      if (opts.term) {
        addTermToQueryBuilder(opts.term, qb, {
          columns: ['communities.name']
        })
      }

      // this counts total rows matching the criteria, disregarding limit,
      // which is useful for pagination
      countTotal(qb, 'communities', opts.totalColumnName)

      qb.limit(opts.limit)
      qb.offset(opts.offset)
      qb.groupBy('communities.id')
      qb.orderBy('communities.name', 'asc')
    })
  },

  forTags: function (opts) {
    return Tag.query(q => {
      if (opts.communities) {
        q.join('communities_tags', 'communities_tags.tag_id', '=', 'tags.id')
        q.whereIn('communities_tags.community_id', opts.communities)
      }
      if (opts.name) {
        q.where('tags.name', opts.name)
      }
      if (opts.autocomplete) {
        q.whereRaw('tags.name ilike ?', opts.autocomplete + '%')
      }

      countTotal(q, 'tags', opts.totalColumnName)

      q.groupBy('tags.id')
      q.limit(opts.limit)
    })
  },

  fullTextSearch: function (userId, args) {
    var items, total
    args.limit = args.first
    return fetchAllCommunityIds(userId)
    .then(communityIds =>
      FullTextSearch.searchInCommunities(communityIds, args))
      .then(items_ => {
        items = items_
        total = get('0.total', items)

        var ids = transform(items, (ids, item) => {
          var type = item.post_id ? 'posts'
            : item.comment_id ? 'comments' : 'people'

          if (!ids[type]) ids[type] = []
          var id = item.post_id || item.comment_id || item.user_id
          ids[type].push(id)
        }, {})

        return Promise.join(
          ids.posts && Post.where('id', 'in', ids.posts).fetchAll({
            withRelated: PostPresenter.relations(userId)
          }),

          ids.comments && Comment.where('id', 'in', ids.comments).fetchAll({
            withRelated: [
              {'user': userColumns},
              {'post': q => q.column('id', 'type', 'name', 'user_id')},
              {'post.user': userColumns},
              {'post.relatedUsers': userColumns},
              {'thanks.thankedBy': userColumns}
            ]
          }),

          ids.people && User.where('id', 'in', ids.people).fetchAll({
            withRelated: 'tags'
          }),

          (posts, comments, people) =>
            items.map(presentResult(posts, comments, people))
        )
      })
      .then(models => ({models, total}))
  }
}

const fetchAllCommunityIds = userId => Promise.resolve([29])
  // Promise.join(
  //   Network.activeCommunityIds(userId),
  //   Membership.activeCommunityIds(userId)
  // ).then(flow(flatten, uniq))

const presentResult = (posts, comments, people) => item => {
  if (item.user_id) {
    return {
      id: `Person-${item.user_id}`,
      content: people.find(p => p.id === item.user_id)
    }
  } else if (item.post_id) {
    return {
      id: `Post-${item.post_id}`,
      content: posts.find(p => p.id === item.post_id)
    }
  } else if (item.comment_id) {
    return {
      id: `Comment-${item.comment_id}`,
      content: comments.find(c => c.id === item.comment_id)
    }
  }
  return null
}

const userColumns = q => q.column('id', 'name', 'avatar_url')
<commit_msg>uncomment fetchAllCommunityIds<commit_after>import forUsers from './Search/forUsers'
import forPosts from './Search/forPosts'
import { countTotal } from '../../lib/util/knex'
import addTermToQueryBuilder from './Search/addTermToQueryBuilder'
import { transform } from 'lodash'
import { flatten, flow, uniq, get } from 'lodash/fp'

module.exports = {
  forPosts,
  forUsers,

  forCommunities: function (opts) {
    return Community.query(qb => {
      if (opts.communities) {
        qb.whereIn('communities.id', opts.communities)
      }

      if (opts.autocomplete) {
        qb.whereRaw('communities.name ilike ?', opts.autocomplete + '%')
      }

      if (opts.term) {
        addTermToQueryBuilder(opts.term, qb, {
          columns: ['communities.name']
        })
      }

      // this counts total rows matching the criteria, disregarding limit,
      // which is useful for pagination
      countTotal(qb, 'communities', opts.totalColumnName)

      qb.limit(opts.limit)
      qb.offset(opts.offset)
      qb.groupBy('communities.id')
      qb.orderBy('communities.name', 'asc')
    })
  },

  forTags: function (opts) {
    return Tag.query(q => {
      if (opts.communities) {
        q.join('communities_tags', 'communities_tags.tag_id', '=', 'tags.id')
        q.whereIn('communities_tags.community_id', opts.communities)
      }
      if (opts.name) {
        q.where('tags.name', opts.name)
      }
      if (opts.autocomplete) {
        q.whereRaw('tags.name ilike ?', opts.autocomplete + '%')
      }

      countTotal(q, 'tags', opts.totalColumnName)

      q.groupBy('tags.id')
      q.limit(opts.limit)
    })
  },

  fullTextSearch: function (userId, args) {
    var items, total
    args.limit = args.first
    return fetchAllCommunityIds(userId)
    .then(communityIds =>
      FullTextSearch.searchInCommunities(communityIds, args))
      .then(items_ => {
        items = items_
        total = get('0.total', items)

        var ids = transform(items, (ids, item) => {
          var type = item.post_id ? 'posts'
            : item.comment_id ? 'comments' : 'people'

          if (!ids[type]) ids[type] = []
          var id = item.post_id || item.comment_id || item.user_id
          ids[type].push(id)
        }, {})

        return Promise.join(
          ids.posts && Post.where('id', 'in', ids.posts).fetchAll({
            withRelated: PostPresenter.relations(userId)
          }),

          ids.comments && Comment.where('id', 'in', ids.comments).fetchAll({
            withRelated: [
              {'user': userColumns},
              {'post': q => q.column('id', 'type', 'name', 'user_id')},
              {'post.user': userColumns},
              {'post.relatedUsers': userColumns},
              {'thanks.thankedBy': userColumns}
            ]
          }),

          ids.people && User.where('id', 'in', ids.people).fetchAll({
            withRelated: 'tags'
          }),

          (posts, comments, people) =>
            items.map(presentResult(posts, comments, people))
        )
      })
      .then(models => ({models, total}))
  }
}

const fetchAllCommunityIds = userId =>
  Promise.join(
    Network.activeCommunityIds(userId),
    Membership.activeCommunityIds(userId)
  ).then(flow(flatten, uniq))

const presentResult = (posts, comments, people) => item => {
  if (item.user_id) {
    return {
      id: `Person-${item.user_id}`,
      content: people.find(p => p.id === item.user_id)
    }
  } else if (item.post_id) {
    return {
      id: `Post-${item.post_id}`,
      content: posts.find(p => p.id === item.post_id)
    }
  } else if (item.comment_id) {
    return {
      id: `Comment-${item.comment_id}`,
      content: comments.find(c => c.id === item.comment_id)
    }
  }
  return null
}

const userColumns = q => q.column('id', 'name', 'avatar_url')
<|endoftext|>


<commit_before>import assert from 'assert';
import makeDebug from 'debug';
import errors from 'feathers-errors';
import { plural } from 'pluralize';
import fp from 'ramda';
import validator from 'validator';
import util from 'util';
import { getField, setField, setFieldByKey } from '../helpers';

const debug = makeDebug('mostly:feathers-mongoose:hooks:populate');

const defaultOptions = {
  idField: 'id',
  retained: true // retain existing field when populate as path
};

function isPopulated(obj) {
  const isPlain = (val) => val && !(fp.is(String, val) || validator.isMongoId(val.toString()));
  return fp.reduce((result, val) => {
    return result && isPlain(val)
  }, true, [].concat(obj));
}

function populateField(hook, item, target, options) {
  let field = options.field || target;

  // Find by the field value by default or a custom query
  let entry = null;
  if (Array.isArray(item)) {
    entry = fp.compose(
      fp.reject(fp.isNil),
      fp.flatten,
      fp.map(it => getField(it, field))
    )(item);
  } else {
    entry = getField(item, field);
  }

  // invalid entry id
  if (!entry || entry.length === 0) {
    return Promise.resolve(item);
  }

  // id with service `document:1`, convert as options.path
  if (!options.path && !options.service) {
    function getPath(value) {
      if (value.indexOf(':') > 0) {
        let [path, id] = value.split(':');
        return { _id: value, _type: path, [options.idField]: id };
      }
      return value;
    }

    options.path = '_type';
    // options.retained = false;
    if (Array.isArray(entry)) {
      entry = fp.map((it) => getPath(it), entry);
      item.forEach((it) => setField(it, field, entry, field, { idField: '_id' }));
    } else {
      entry = getPath(entry);
      setField(item, field, entry, field, { idField: '_id' });
    }
  }
  //debug('==> %s populate %s/%s, \n\tid: %j', options.service, target, field, entry);
  //debug(' \n\twith: %j', item);

  if (!options.path && isPopulated(entry)) {
    debug('==> %s already populate %s/%s, \n\tid: %j', options, target, field, entry);
    return Promise.resolve(item);
  }

  // If it's a mongoose model then
  if (typeof item.toObject === 'function') {
    item = item.toObject(options);
  }
  // If it's a Sequelize model
  else if (typeof item.toJSON === 'function') {
    item = item.toJSON(options);
  }
  // Remove any query from params as it's not related
  let params = {}; // Object.assign({}, hook.params, { query: undefined });
  //console.log('populate:', field, entry, params);

  params.populate = options.recursive; // recursive populate
  params.softDelete = options.softDelete || false; // enforce destroyedAt

  // If the relationship is an array of ids, fetch and resolve an object for each,
  // otherwise just fetch the object.
  let promise = null;

  if (Array.isArray(entry)) {
    let services = [];
    if (options.path) {
      let entries = fp.groupBy(fp.prop(options.path), entry);
      services = fp.map((entry) => {
        return fp.map(fp.prop(options.idField), entry);
      }, entries);
      debug('populate services', services);
    } else {
      services = {
        [options.service]: entry
      };
    }
    params.paginate = false; // disable paginate
    promise = Promise.all(fp.map((service) => {
      let groupParams = Object.assign({}, params);
      groupParams.query = { _id: { $in: services[service] } };
      return hook.app.service(plural(service)).find(groupParams);
    }, Object.keys(services)));
  } else {
    let service = options.service;
    let id = entry;
    if (options.path) {
      if (entry[options.path]) {
        service = plural(entry[options.path]);
      } else {
        service = options.path;
      }
      id = entry[options.idField];
      debug('populate service', service, id);
    }
    promise = hook.app.service(service).get(id, params);
  }
  return promise.then((results) => {
    debug('services found', results);
    let data = results.data || results;
    if (Array.isArray(results)) {
      data = fp.flatten(fp.map(result => result.data || result, results));
    }
    // debug('setField %j \n ==> %s \n ==> %j', entry, field, data);
    if (Array.isArray(item)) {
      item.forEach(it => setField(it, target, data, field, options));
    } else {
      setField(item, target, data, field, options);
    }

    // try nested populate(s)
    if (options.populate) {
      let pPopulates = Array.isArray(options.populate)? options.populate : [options.populate];
      let pPromises = fp.reduce((promises, pOptions) => {
        let pItem = fp.flatten(getField(item, field));
        let pTarget = pOptions.target || pOptions.field;
        //debug('>>> nested populate %s/%s(options=%s) with %s',
        //  pTarget, pOptions.field, util.inspect(pOptions), util.inspect(pItem));
        promises.push(populateField(pItem, pTarget, pOptions));
        return promises;
      }, [], pPopulates);
      return Promise.all(pPromises).then(() => item);
    } else {
      return item;
    }
  }).catch(function(err) {
    console.error(" ERROR: populate %s error %s", options.service, util.inspect(err));
    setField(item, target, {}, field, options);
    return item;
  });
}

/**
 * The populate hook uses a property from the result (or every item if it is a list)
 * to retrieve a single related object from a service and add it to the original object.
 * It is meant to be used as an after hook on any service method.
 *
 * @param {string} target - The prop name to contain the populated item or array of populated items.
 *    This is also the default for options.field if that is not specified.
 * @param {Object} options - options
 *    For a mongoose model, these are the options for item.toObject().
 *    For a Sequelize model, these are the options for item.toJSON().
 * @param {string} options.service - The service for the related object, e.g. '/messages'.
 * @param {string|Array.<string>} options.field - The field containing the key(s)
 *    for the item(s) in options.service.
 * @returns {Function} hook function(hook):Promise resolving to the hook.
 *
 * 'options.field' is the foreign key for one related item in options.service,
 *    i.e. item[options.field] === foreignItem[idField].
 * 'target' is set to this related item once it is read successfully.
 *
 * If 'options.field' is not present in the hook result item, the hook is ignored.
 *
 * So if the hook result has the message item
 *    { _id: '1...1', senderId: 'a...a', text: 'Jane, are you there?' }
 * and the /users service has the item
 *    { _id: 'a...a', name: 'John Doe'}
 * and then the hook is run
 *    hooks.populate('sender', { field: 'userId', service: '/users' })
 * the hook result will contain
 *    { _id: '1...1', senderId : 'a...a', text: 'Jane, are you there?',
 *      sender: { _id: 'a...a', name: 'John Doe'} }
 *
 * If 'senderId' is an array of keys, then 'sender' will be an array of populated items.
 */
export function populate(target, opts) {
  opts = Object.assign({}, defaultOptions, opts);

  return function(hook) {
    let options = Object.assign({}, opts);  // clone for change

    // If it was an internal call then do not recursive populate
    options.recursive = !!hook.params.provider;

    if (hook.type !== 'after') {
      throw new errors.GeneralError('Can not populate on before hook. (populate)');
    }

    if (hook.params.populate === false) return hook;

    let isPaginated = hook.method === 'find' && hook.result.data;
    let data = isPaginated ? hook.result.data : hook.result;

    if (Array.isArray(data) && data.length === 0) return hook;

    return populateField(hook, data, target, options).then(result => {
      //debug('> populate result', util.inspect(result));
      if (isPaginated) {
        hook.result.data = result;
      } else {
        hook.result = result;
      }
      return hook;
    });
  };
}

export function depopulate(target, opts = { idField: 'id' }) {

  return function(hook) {
    let options = Object.assign({}, opts);

    function getDepopulated(item, target) {
      let field = fp.prop(target, item);
      if (field === undefined) return undefined;
      if (Array.isArray(field)) {
        field = fp.map((it) => it[options.idField] || it, field);
      } else if (field) {
        field = field[options.idField] || field;
      }
      return field? field : null;
    }

    function setTarget(data, target, value) {
      if (value !== undefined) {
        return fp.assocPath(target.split('.'), value, data);
      }
    }

    if (hook.type === 'before') {
      hook.data = setTarget(hook.data, target, getDepopulated(hook.data, target));
    } else {
      if (hook.result) {
        if (hook.result.data) {
          hook.result.data = setTarget(hook.result.data, target, getDepopulated(hook.result.data, target));
        } else {
          hook.result = setTarget(hook.result, target, getDepopulated(hook.result, target));
        }
      }
    }
    if (target === 'parent') {
      debug('depopulate parent', hook.data.parent, typeof hook.data.parent, hook.data);
    }
    return hook;
  };
}<commit_msg>Assert populate options<commit_after>import assert from 'assert';
import makeDebug from 'debug';
import errors from 'feathers-errors';
import { plural } from 'pluralize';
import fp from 'ramda';
import validator from 'validator';
import util from 'util';
import { getField, setField, setFieldByKey } from '../helpers';

const debug = makeDebug('mostly:feathers-mongoose:hooks:populate');

const defaultOptions = {
  idField: 'id',
  retained: true // retain existing field when populate as path
};

function isPopulated(obj) {
  const isPlain = (val) => val && !(fp.is(String, val) || validator.isMongoId(val.toString()));
  return fp.reduce((result, val) => {
    return result && isPlain(val)
  }, true, [].concat(obj));
}

function populateField(hook, item, target, options) {
  let field = options.field || target;

  // Find by the field value by default or a custom query
  let entry = null;
  if (Array.isArray(item)) {
    entry = fp.compose(
      fp.reject(fp.isNil),
      fp.flatten,
      fp.map(it => getField(it, field))
    )(item);
  } else {
    entry = getField(item, field);
  }

  // invalid entry id
  if (!entry || entry.length === 0) {
    return Promise.resolve(item);
  }

  // id with service `document:1`, convert as options.path
  if (!options.path && !options.service) {
    function getPath(value) {
      if (value.indexOf(':') > 0) {
        let [path, id] = value.split(':');
        return { _id: value, _type: path, [options.idField]: id };
      }
      return value;
    }

    options.path = '_type';
    if (Array.isArray(entry)) {
      entry = fp.map((it) => getPath(it), entry);
      item.forEach((it) => setField(it, field, entry, field, { idField: '_id' }));
    } else {
      entry = getPath(entry);
      setField(item, field, entry, field, { idField: '_id' });
    }
  }
  //debug('==> %s populate %s/%s, \n\tid: %j', options.service, target, field, entry);
  //debug(' \n\twith: %j', item);

  if (!options.path && isPopulated(entry)) {
    debug('==> %s already populate %s/%s, \n\tid: %j', options, target, field, entry);
    return Promise.resolve(item);
  }

  assert(options.service || options.path, 'You need to provide a service or path');

  // If it's a mongoose model then
  if (typeof item.toObject === 'function') {
    item = item.toObject(options);
  }
  // If it's a Sequelize model
  else if (typeof item.toJSON === 'function') {
    item = item.toJSON(options);
  }
  // Remove any query from params as it's not related
  let params = {}; // Object.assign({}, hook.params, { query: undefined });
  //console.log('populate:', field, entry, params);

  params.populate = options.recursive; // recursive populate
  params.softDelete = options.softDelete || false; // enforce destroyedAt

  // If the relationship is an array of ids, fetch and resolve an object for each,
  // otherwise just fetch the object.
  let promise = null;

  if (Array.isArray(entry)) {
    let services = [];
    if (options.path) {
      let entries = fp.groupBy(fp.prop(options.path), entry);
      services = fp.map((entry) => {
        return fp.map(fp.prop(options.idField), entry);
      }, entries);
      debug('populate services', services);
    } else {
      services = {
        [options.service]: entry
      };
    }
    params.paginate = false; // disable paginate
    promise = Promise.all(fp.map((service) => {
      let groupParams = Object.assign({}, params);
      groupParams.query = { _id: { $in: services[service] } };
      return hook.app.service(plural(service)).find(groupParams);
    }, Object.keys(services)));
  } else {
    let service = options.service;
    let id = entry;
    if (options.path) {
      if (entry[options.path]) {
        service = plural(entry[options.path]);
      } else {
        service = options.path;
      }
      id = entry[options.idField];
      debug('populate service', service, id);
    }
    promise = hook.app.service(service).get(id, params);
  }
  return promise.then((results) => {
    debug('services found', results);
    let data = results.data || results;
    if (Array.isArray(results)) {
      data = fp.flatten(fp.map(result => result.data || result, results));
    }
    // debug('setField %j \n ==> %s \n ==> %j', entry, field, data);
    if (Array.isArray(item)) {
      item.forEach(it => setField(it, target, data, field, options));
    } else {
      setField(item, target, data, field, options);
    }

    // try nested populate(s)
    if (options.populate) {
      let pPopulates = Array.isArray(options.populate)? options.populate : [options.populate];
      let pPromises = fp.reduce((promises, pOptions) => {
        let pItem = fp.flatten(getField(item, field));
        let pTarget = pOptions.target || pOptions.field;
        //debug('>>> nested populate %s/%s(options=%s) with %s',
        //  pTarget, pOptions.field, util.inspect(pOptions), util.inspect(pItem));
        promises.push(populateField(pItem, pTarget, pOptions));
        return promises;
      }, [], pPopulates);
      return Promise.all(pPromises).then(() => item);
    } else {
      return item;
    }
  }).catch(function(err) {
    console.error(" ERROR: populate %s error %s", options.service, util.inspect(err));
    setField(item, target, {}, field, options);
    return item;
  });
}

/**
 * The populate hook uses a property from the result (or every item if it is a list)
 * to retrieve a single related object from a service and add it to the original object.
 * It is meant to be used as an after hook on any service method.
 *
 * @param {string} target - The prop name to contain the populated item or array of populated items.
 *    This is also the default for options.field if that is not specified.
 * @param {Object} options - options
 *    For a mongoose model, these are the options for item.toObject().
 *    For a Sequelize model, these are the options for item.toJSON().
 * @param {string} options.service - The service for the related object, e.g. '/messages'.
 * @param {string|Array.<string>} options.field - The field containing the key(s)
 *    for the item(s) in options.service.
 * @returns {Function} hook function(hook):Promise resolving to the hook.
 *
 * 'options.field' is the foreign key for one related item in options.service,
 *    i.e. item[options.field] === foreignItem[idField].
 * 'target' is set to this related item once it is read successfully.
 *
 * If 'options.field' is not present in the hook result item, the hook is ignored.
 *
 * So if the hook result has the message item
 *    { _id: '1...1', senderId: 'a...a', text: 'Jane, are you there?' }
 * and the /users service has the item
 *    { _id: 'a...a', name: 'John Doe'}
 * and then the hook is run
 *    hooks.populate('sender', { field: 'userId', service: '/users' })
 * the hook result will contain
 *    { _id: '1...1', senderId : 'a...a', text: 'Jane, are you there?',
 *      sender: { _id: 'a...a', name: 'John Doe'} }
 *
 * If 'senderId' is an array of keys, then 'sender' will be an array of populated items.
 */
export function populate(target, opts) {
  opts = Object.assign({}, defaultOptions, opts);

  return function(hook) {
    let options = Object.assign({}, opts);  // clone for change

    // If it was an internal call then do not recursive populate
    options.recursive = !!hook.params.provider;

    if (hook.type !== 'after') {
      throw new errors.GeneralError('Can not populate on before hook. (populate)');
    }

    if (hook.params.populate === false) return hook;

    let isPaginated = hook.method === 'find' && hook.result.data;
    let data = isPaginated ? hook.result.data : hook.result;

    if (Array.isArray(data) && data.length === 0) return hook;

    return populateField(hook, data, target, options).then(result => {
      //debug('> populate result', util.inspect(result));
      if (isPaginated) {
        hook.result.data = result;
      } else {
        hook.result = result;
      }
      return hook;
    });
  };
}

export function depopulate(target, opts = { idField: 'id' }) {

  return function(hook) {
    let options = Object.assign({}, opts);

    function getDepopulated(item, target) {
      let field = fp.prop(target, item);
      if (field === undefined) return undefined;
      if (Array.isArray(field)) {
        field = fp.map((it) => it[options.idField] || it, field);
      } else if (field) {
        field = field[options.idField] || field;
      }
      return field? field : null;
    }

    function setTarget(data, target, value) {
      if (value !== undefined) {
        return fp.assocPath(target.split('.'), value, data);
      }
    }

    if (hook.type === 'before') {
      hook.data = setTarget(hook.data, target, getDepopulated(hook.data, target));
    } else {
      if (hook.result) {
        if (hook.result.data) {
          hook.result.data = setTarget(hook.result.data, target, getDepopulated(hook.result.data, target));
        } else {
          hook.result = setTarget(hook.result, target, getDepopulated(hook.result, target));
        }
      }
    }
    if (target === 'parent') {
      debug('depopulate parent', hook.data.parent, typeof hook.data.parent, hook.data);
    }
    return hook;
  };
}<|endoftext|>