<commit_before>L.MapListSync = L.Class.extend({
    includes: L.Mixin.Events,
    options: {
        filter: null,
        /* { form: $('#mainfilter'),
             submitbutton: $('#filter'),
             resetbutton: $('#reset'),
             bboxfield: $('#id_bbox'),
           }
        */
    },

    initialize: function (datatables, map, objectsLayer, options) {
        this.dt = datatables;
        this.map = map;
        this.layer = objectsLayer;
        L.Util.setOptions(this, options);

        this.selectorOnce = this.__initSelectorOnce(); // TODO: rename this and refactor
        this._dtcontainer = this.dt.fnSettings().nTableWrapper;

        this.dt.fnSettings().fnCreatedRow = this._onRowCreated.bind(this);
        this.layer.on('click', this._onObjectClick.bind(this));

        this._ignoreMap = false;
        this._loading = false;
        this._initialBounds = null;
        if (this.map._loaded) {
            this._initialBounds = this.map.getBounds();
        }
        else {
            this.map.on('load', function () {
                this._initialBounds = this.map.getBounds();
                this._onMapViewChanged();
            }, this);
        }
        this.map.on('moveend', this._onMapViewChanged, this);

        if (this.options.filter) {
            this.options.filter.submitbutton.click(this._onFormSubmit.bind(this));
            this.options.filter.resetbutton.click(this._onFormReset.bind(this));
        }
    },

    _onMapViewChanged: function (e) {
        if (this._ignoreMap || !this.map._loaded)
            return;
        this._formSetBounds();
        this._reloadList();
    },

    _onFormSubmit: function (e) {
        this._formSetBounds();
        this._reloadList(true);
    },

    _onFormReset: function (e) {
        this._ignoreMap = true;
        if (this._initialBounds) this.map.fitBounds(this._initialBounds);
        this._ignoreMap = false;

        this._formClear($(this.options.filter.form)); // clear all fields
        this._formSetBounds(); // re-fill current bbox
        this._reloadList();
        this.layer.updateFromPks(Object.keys(this.layer._objects));
    },

    _onObjectClick: function (e) {
        var self = this;
        var search_pk = e.layer.properties.pk;
        JQDataTable.goToPage(this.dt,
            function pk_equals(row) {
                return row[0] === search_pk;
            }, function($row) {
                self.selectorOnce.select(search_pk, $row);
            }
        );
    },

    _onRowCreated: function(nRow, aData, iDataIndex ) {
        var pk = aData[0];
        $(nRow).hover(
            function(){
                this.layer.highlight(pk);
            },
            function(){
                this.layer.highlight(pk, false);
            }
        );

        // select from row
        var self = this;
        $(nRow).click(function() {
            self.selectorOnce.select(pk, $(nRow));
        });
        $(nRow).dblclick(function() {
            self.layer.jumpTo(pk);
        });
    },

    _reloadList: function (refreshLayer) {
        if (this._loading)
            return;
        this._loading = true;
        var spinner = new Spinner().spin(this._dtcontainer);

        // on JSON load, return the json used by dataTable
        // Update also the map given the layer's pk
        var self = this;
        var extract_data_and_pks = function(data, type, callback_args) {
            callback_args.map_obj_pk = data.map_obj_pk;
            return data.aaData;
        };
        var on_data_loaded = function (oSettings, callback_args) {
            self._loading = false;
            self.fire('reloaded', {
                nbrecords: self.dt.fnSettings().fnRecordsTotal(),
            });
            if(refreshLayer) self.layer.updateFromPks(callback_args.map_obj_pk);
            spinner.stop();
        };

        var url = this.options.url;
        if (this.options.filter) {
            url = this.options.filter.form.attr("action") + '?' + this.options.filter.form.serialize();
        }
        this.dt.fnReloadAjax(url, extract_data_and_pks, on_data_loaded);
        return false;
    },

    _formSetBounds: function () {
        if (!this.options.filter)
            return;

        if (!this.map._loaded) {
            console.warn("Map view not set, cannot get bounds.");
            return;
        }
        var bounds = this.map.getBounds(),
            rect = new L.Rectangle([bounds._northEast, bounds._southWest]);
        this.options.filter.bboxfield.val(L.Util.getWKT(rect));
    },

    _formClear: function ($form) {
        $form.find('input:text, input:password, input:file, select, textarea').val('');
        $form.find('input:radio, input:checkbox')
             .removeAttr('checked').removeAttr('selected');
    },

    __initSelectorOnce: function () {
        /**
         * This code was moved from entity list main page. A massive simplification
         * is required.
         */
        var self = this;
        var selectorOnce = (function() {
                var current = { 'pk': null, 'row': null };

                function toggleSelectRow($prevRow, $nextRow) {
                    function nextRowAnim() {
                        if ($nextRow) {
                            $nextRow.hide('fast')
                                    .show('fast', function() { $nextRow.addClass('selected-row'); });
                        }
                    }

                    if ($prevRow) {
                        $prevRow.hide('fast', function() { $prevRow.removeClass('selected-row'); })
                                .show('fast', nextRowAnim);
                    } else {
                        nextRowAnim();
                    }
                }

                function toggleSelectObject(pk, on) {
                    on = on === undefined ? true : on;
                    self.layer.select(pk, on);
                };

                return {
                    'select': function(pk, row) {
                        // Click on already selected => unselect
                        if (pk == current.pk) {
                            pk = null, row = null;
                        }

                        var prev = current;
                        current = {'pk': pk, 'row': row}

                        toggleSelectRow(prev.row, row);

                        if (prev.pk && prev.row) {
                            toggleSelectObject(prev.pk, false);
                        }
                        if (row && pk) {
                            toggleSelectObject(pk, true);
                        }
                    }
                };
            })();
        return selectorOnce;
    }
});
<commit_msg>Simplify behaviour, do not reset view on form reset (ref #334)<commit_after>L.MapListSync = L.Class.extend({
    includes: L.Mixin.Events,
    options: {
        filter: null,
        /* { form: $('#mainfilter'),
             submitbutton: $('#filter'),
             resetbutton: $('#reset'),
             bboxfield: $('#id_bbox'),
           }
        */
    },

    initialize: function (datatables, map, objectsLayer, options) {
        this.dt = datatables;
        this.map = map;
        this.layer = objectsLayer;
        L.Util.setOptions(this, options);

        this.selectorOnce = this.__initSelectorOnce(); // TODO: rename this and refactor
        this._dtcontainer = this.dt.fnSettings().nTableWrapper;

        this.dt.fnSettings().fnCreatedRow = this._onRowCreated.bind(this);
        this.layer.on('click', this._onObjectClick.bind(this));

        this._loading = false;
        this.map.on('moveend', this._onMapViewChanged, this);

        if (this.options.filter) {
            this.options.filter.submitbutton.click(this._onFormSubmit.bind(this));
            this.options.filter.resetbutton.click(this._onFormReset.bind(this));
        }
    },

    _onMapViewChanged: function (e) {
        if (!this.map._loaded) {
            // leaflet bug, fire again !
            // fixed in unstable version : https://github.com/CloudMade/Leaflet/commit/fbf91fef546125bd4950937fa04ad1bf0f5dc955
            setTimeout(L.Util.bind(function() { this.map.fire('moveend'); }, this), 20);
            return;
        }
        this._formSetBounds();
        this._reloadList();
    },

    _onFormSubmit: function (e) {
        this._formSetBounds();
        this._reloadList(true);
    },

    _onFormReset: function (e) {
        this._formClear($(this.options.filter.form)); // clear all fields
        this._formSetBounds(); // re-fill current bbox
        this._reloadList();
        this.layer.updateFromPks(Object.keys(this.layer._objects));
    },

    _onObjectClick: function (e) {
        var self = this;
        var search_pk = e.layer.properties.pk;
        JQDataTable.goToPage(this.dt,
            function pk_equals(row) {
                return row[0] === search_pk;
            }, function($row) {
                self.selectorOnce.select(search_pk, $row);
            }
        );
    },

    _onRowCreated: function(nRow, aData, iDataIndex ) {
        var pk = aData[0];
        $(nRow).hover(
            function(){
                this.layer.highlight(pk);
            },
            function(){
                this.layer.highlight(pk, false);
            }
        );

        // select from row
        var self = this;
        $(nRow).click(function() {
            self.selectorOnce.select(pk, $(nRow));
        });
        $(nRow).dblclick(function() {
            self.layer.jumpTo(pk);
        });
    },

    _reloadList: function (refreshLayer) {
        if (this._loading)
            return;
        this._loading = true;
        var spinner = new Spinner().spin(this._dtcontainer);

        // on JSON load, return the json used by dataTable
        // Update also the map given the layer's pk
        var self = this;
        var extract_data_and_pks = function(data, type, callback_args) {
            callback_args.map_obj_pk = data.map_obj_pk;
            return data.aaData;
        };
        var on_data_loaded = function (oSettings, callback_args) {
            self._loading = false;
            self.fire('reloaded', {
                nbrecords: self.dt.fnSettings().fnRecordsTotal(),
            });
            if(refreshLayer) self.layer.updateFromPks(callback_args.map_obj_pk);
            spinner.stop();
        };

        var url = this.options.url;
        if (this.options.filter) {
            url = this.options.filter.form.attr("action") + '?' + this.options.filter.form.serialize();
        }
        this.dt.fnReloadAjax(url, extract_data_and_pks, on_data_loaded);
        return false;
    },

    _formSetBounds: function () {
        if (!this.options.filter)
            return;

        if (!this.map._loaded) {
            console.warn("Map view not set, cannot get bounds.");
            return;
        }
        var bounds = this.map.getBounds(),
            rect = new L.Rectangle([bounds._northEast, bounds._southWest]);
        this.options.filter.bboxfield.val(L.Util.getWKT(rect));
    },

    _formClear: function ($form) {
        $form.find('input:text, input:password, input:file, select, textarea').val('');
        $form.find('input:radio, input:checkbox')
             .removeAttr('checked').removeAttr('selected');
    },

    __initSelectorOnce: function () {
        /**
         * This code was moved from entity list main page. A massive simplification
         * is required.
         */
        var self = this;
        var selectorOnce = (function() {
                var current = { 'pk': null, 'row': null };

                function toggleSelectRow($prevRow, $nextRow) {
                    function nextRowAnim() {
                        if ($nextRow) {
                            $nextRow.hide('fast')
                                    .show('fast', function() { $nextRow.addClass('selected-row'); });
                        }
                    }

                    if ($prevRow) {
                        $prevRow.hide('fast', function() { $prevRow.removeClass('selected-row'); })
                                .show('fast', nextRowAnim);
                    } else {
                        nextRowAnim();
                    }
                }

                function toggleSelectObject(pk, on) {
                    on = on === undefined ? true : on;
                    self.layer.select(pk, on);
                };

                return {
                    'select': function(pk, row) {
                        // Click on already selected => unselect
                        if (pk == current.pk) {
                            pk = null, row = null;
                        }

                        var prev = current;
                        current = {'pk': pk, 'row': row}

                        toggleSelectRow(prev.row, row);

                        if (prev.pk && prev.row) {
                            toggleSelectObject(prev.pk, false);
                        }
                        if (row && pk) {
                            toggleSelectObject(pk, true);
                        }
                    }
                };
            })();
        return selectorOnce;
    }
});
<|endoftext|>


<commit_before>(function(context){

  var config = {
    startTime: "20:00",
    endTime: "12:00"
  }

  function getNext(now, time) {
    var timeArr = time.split(":");
    goalHours = parseInt(timeArr[0], 10);
    goalMinutes = parseInt(timeArr[1], 10);
    nowHour = now.hours();
    nowMinues = now.minutes();

    if (
      nowHour > goalHours ||
      (nowHour === goalHours && nowMinues >= goalMinutes)
    ){
      // passed, set to tomorrow goal time
      return now.add(1, 'days').hours(goalHours).minutes(goalMinutes);
    }
    return now.hours(goalHours).minutes(goalMinutes);
  }

  function initTimers(now) {
    var $currentTimeExact = context.document.getElementById('currentTimeExact');
    $currentTimeExact.innerHTML = now.format('LLL');

    var $startTime = context.document.getElementById('startTime');
    var $startTimeExact = context.document.getElementById('startTimeExact');
    var startTime = getNext(now.clone(), config.startTime);
    $startTime.innerHTML = now.to(startTime);
    $startTimeExact.innerHTML = startTime.calendar();

    var $endTime = context.document.getElementById('endTime');
    var $endTimeExact = context.document.getElementById('endTimeExact');
    var endTime = getNext(now.clone(), config.endTime);
    $endTime.innerHTML = now.to(endTime);
    $endTimeExact.innerHTML = endTime.calendar();
  }

  function setupMoment() {
    moment.updateLocale('en', {
      longDateFormat : {
          LT: "HH:mm",
          LTS: "HH:mm:ss",
          L: "MM/DD/YYYY",
          l: "M/D/YYYY",
          LL: "MMMM Do YYYY",
          ll: "MMM D YYYY",
          LLL: "MMMM Do YYYY LT",
          lll: "MMM D YYYY LT",
          LLLL: "dddd, MMMM Do YYYY LT",
          llll: "ddd, MMM D YYYY LT"
      }
  });
  }

  function init(context) {
    setupMoment();
    initTimers(moment());
  }

  context.document.addEventListener('DOMContentLoaded', function() {
    init(context);
  }, false);

})(window);
<commit_msg>utoupdate<commit_after>(function(context){

  var config = {
    startTime: "20:00",
    endTime: "12:00"
  }

  var $currentTimeExact;
  var $startTime;
  var $startTimeExact;
  var $endTime;
  var $endTimeExact;

  function getNext(now, time) {
    var timeArr = time.split(":");
    goalHours = parseInt(timeArr[0], 10);
    goalMinutes = parseInt(timeArr[1], 10);
    nowHour = now.hours();
    nowMinues = now.minutes();

    if (nowHour > goalHours || (nowHour === goalHours && nowMinues >= goalMinutes)) {
      return now.add(1, 'days').hours(goalHours).minutes(goalMinutes);
    }
    return now.hours(goalHours).minutes(goalMinutes);
  }

  function initTimers(now) {
    $currentTimeExact.innerHTML = now.format('LLL');

    var startTime = getNext(now.clone(), config.startTime);
    $startTime.innerHTML = now.to(startTime);
    $startTimeExact.innerHTML = startTime.calendar();

    var endTime = getNext(now.clone(), config.endTime);
    $endTime.innerHTML = now.to(endTime);
    $endTimeExact.innerHTML = endTime.calendar();
  }

  function setupMoment() {
    moment.updateLocale('en', {
      longDateFormat : {
          LT: "HH:mm",
          LTS: "HH:mm:ss",
          L: "MM/DD/YYYY",
          l: "M/D/YYYY",
          LL: "MMMM Do YYYY",
          ll: "MMM D YYYY",
          LLL: "MMMM Do YYYY LT",
          lll: "MMM D YYYY LT",
          LLLL: "dddd, MMMM Do YYYY LT",
          llll: "ddd, MMM D YYYY LT"
      }
  });
  }

  function init(context) {
    $currentTimeExact = context.document.getElementById('currentTimeExact');
    $startTime = context.document.getElementById('startTime');
    $startTimeExact = context.document.getElementById('startTimeExact');
    $endTime = context.document.getElementById('endTime');
    $endTimeExact = context.document.getElementById('endTimeExact');

    setupMoment();
    initTimers(moment());
    setInterval(function(){
      initTimers(moment());
    }, 10000)
  }

  context.document.addEventListener('DOMContentLoaded', function() {
    init(context);
  }, false);

})(window);
<|endoftext|>


<commit_before>/**
 * Created by wes.risenmay on 7/21/14.
 */

var test = {
    "NR_043598.1":6.09097567733726
    ,"NG_033251.1":2.1765233598378306,
    "NC_013761.1":4.3301469436537685,
    "NG_008148.1":2.1104698740077588,
    "NW_006918865.1":5.787050782311936,"NR_114413.1":6.087545230232171,"NC_014271.1":5.68757048054799,"NR_026117.1":7.2280106976842475,"NR_044562.1":7.566447881409501,
    "NW_006918297.1":3.3938032656816413,"NW_006917867.1":6.851664503814195,"NC_015808.1":5.933917434610158,"NR_029360.1":6.082971362261912,"NR_028988.1":9.571779302457843,
    "NR_112344.1":9.015887784279997,"NC_003477.1":5.703578348895861,"NW_006917797.1":7.650812390058489,"NC_004071.1":6.219567514161194,"NR_104950.1":7.731490383259447,
    "NW_006918917.1":4.360513900237879,"NC_020498.1":6.5191297739030745,"NW_006917729.1":6.534396017918776,"NR_119821.1":6.986398501883604,"NR_111799.1":8.645642615615008,
    "NW_006918330.1":4.791066743577456,"NC_001956.1":5.506982491816964,"NR_120167.1":9.614608932343835,"NC_010348.1":6.487877345511952,"NR_027209.1":8.073786151369598,
    "NR_119639.1":6.35345955384954,"NC_014714.1":10.455954066295535,"NC_008141.1":9.702240902852198,"NG_032011.2":1.132350731987886,"NC_020745.1":4.19148544452781,
    "NR_120102.1":8.644762935479683,"NC_003612.1":14.530319989450595,"NR_040902.1":7.11846810498366,"NW_006920364.1":6.095097286704065,"NC_002762.1":4.665542717252189,
    "NR_041092.1":8.55543699158854,"NG_023325.1":1.9322282816847371,"NW_006919303.1":9.839180040326317, "target": "NC_005816.1"};

$(document).ready(function() {
    URI = window.location.href.split("/");
    $.ajax({
        url: 'http://capstonebb.cs.pdx.edu:8080/codonpdx/results/' + URI[5],
        type: 'GET',
        success: function(response) {
            var json = jQuery.parseJSON(response)

            if(json == null) {
                json = response
            }

            if(response.Error) {
                alert(response.Error)
            }
            else if(json != null && json["target"]) {
                var data = json;
                var UUID = URI[5];

                $("#organism").append(data["target"])

                for (var k in data){
                    url = window.location.href + "/" + k
                    newRow = '
' + k + '	' + '<a href="' + url +'"' + data[k][1] + '	' + data[k][2] + '	' + data[k][0] + '
'
                    if(k != "target") {
                        $("#resultsTable").find("tbody").append(newRow)
                    }
                }
                $("#missingData").hide()
            }
            else {
                alert("Something went wrong")
            }
        },
        error: function() {
            alert("Something went wrong")
        }
    })

});

<commit_msg>Got rid of junk in js file<commit_after>/**
 * Created by wes.risenmay on 7/21/14.
 */
$(document).ready(function() {
    URI = window.location.href.split("/");
    $.ajax({
        url: 'http://capstonebb.cs.pdx.edu:8080/codonpdx/results/' + URI[5],
        type: 'GET',
        success: function(response) {
            var json = jQuery.parseJSON(response)

            if(json == null) {
                json = response
            }

            if(response.Error) {
                alert(response.Error)
            }
            else if(json != null && json["target"]) {
                var data = json;
                var UUID = URI[5];

                $("#organism").append(data["target"])

                for (var k in data){
                    url = window.location.href + "/" + k
                    newRow = '
' + k + '	' + '<a href="' + url +'"' + data[k][1] + '	' + data[k][2] + '	' + data[k][0] + '
'
                    if(k != "target") {
                        $("#resultsTable").find("tbody").append(newRow)
                    }
                }
                $("#missingData").hide()
            }
            else {
                alert("Something went wrong")
            }
        },
        error: function() {
            alert("Something went wrong")
        }
    })

});

<|endoftext|>


<commit_before>var FileCtrl = function ($scope, $location, $timeout) {
    Scope = $scope;
    Scope.uploadPackageFile = function () {
        document.forms["upload-file-form"].submit();
        /*$timeout(function () {
            $location.path('/app');
        }, 1000);*/

    };
    Scope.uploadPackageUrl = function () {
        document.forms["upload-url-form"].submit();
        /*$timeout(function () {
            $location.path('/app');
        }, 1000);*/
    }
    $("#root-file-manager").css('height', $(window).height()).css('width', '100%').show();
    $("#root-file-manager iframe").css('height', $(window).height()).css('width', '100%').attr("src", CurrentServer + '/public/admin/filemanager/?path=/&allowroot=true').show();
    $(window).resize(function () {
        $('#root-file-manager').css('height', $(window).height()).css('width', '100%').css('width', '100%');
        $("#root-file-manager iframe").css('height', $(window).height()).css('width', '100%');
    });
};

function checkResults(iframe) {

    var str = $(iframe).contents().text();
    console.log("checkResults=" + str);
    if(str && str.length > 0) {
        if (isErrorString(str)) {
            var response = {};
            response.responseText = str;
            alertErr(response);
        } else {
            alert("The app was imported successfully!");
        }
    }
}<commit_msg>Remove console.log.<commit_after>var FileCtrl = function ($scope, $location, $timeout) {
    Scope = $scope;
    Scope.uploadPackageFile = function () {
        document.forms["upload-file-form"].submit();
        /*$timeout(function () {
            $location.path('/app');
        }, 1000);*/

    };
    Scope.uploadPackageUrl = function () {
        document.forms["upload-url-form"].submit();
        /*$timeout(function () {
            $location.path('/app');
        }, 1000);*/
    }
    $("#root-file-manager").css('height', $(window).height()).css('width', '100%').show();
    $("#root-file-manager iframe").css('height', $(window).height()).css('width', '100%').attr("src", CurrentServer + '/public/admin/filemanager/?path=/&allowroot=true').show();
    $(window).resize(function () {
        $('#root-file-manager').css('height', $(window).height()).css('width', '100%').css('width', '100%');
        $("#root-file-manager iframe").css('height', $(window).height()).css('width', '100%');
    });
};

function checkResults(iframe) {

    var str = $(iframe).contents().text();
    if(str && str.length > 0) {
        if (isErrorString(str)) {
            var response = {};
            response.responseText = str;
            alertErr(response);
        } else {
            alert("The app was imported successfully!");
        }
    }
}<|endoftext|>


<commit_before>import constants, { errorCodes } from '../../constants';
import safeBrowser from './browser';
import withUtils from './utils';

const defaultOptions = {
    forceDeactivate: true,
    useHash: false,
    hashPrefix: '',
    base: false
};

const source = 'popstate';

function browserPluginFactory(opts = {}, browser = safeBrowser) {
    const options = { ...defaultOptions, ...opts };
    const transitionOptions = { forceDeactivate: options.forceDeactivate, source };

    function browserPlugin(router) {
        const routerOptions = router.getOptions();
        const routerStart = router.start;

        withUtils(router, options);

        router.start = function(...args) {
            if (args.length === 0 || typeof args[0] === 'function') {
                routerStart(browser.getLocation(options), ...args);
            } else {
                routerStart(...args);
            }

            return router;
        };

        router.replaceHistoryState = function(name, params = {}) {
            const state = router.buildState(name, params);
            const url = router.buildUrl(name, params);
            router.lastKnownState = state;
            browser.replaceState(state, '', url);
        };

        function updateBrowserState(state, url, replace) {
            if (replace) browser.replaceState(state, '', url);
            else browser.pushState(state, '', url);
        }

        function onPopState(evt) {
            const routerState = router.getState();
            // Do nothing if no state or if last know state is poped state (it should never happen)
            const newState = !evt.state || !evt.state.name;
            const state = newState ? router.matchPath(browser.getLocation(options), source) : evt.state;
            const { defaultRoute, defaultParams } = routerOptions;

            if (!state) {
                // If current state is already the default route, we will have a double entry
                // Navigating back and forth will emit SAME_STATES error
                defaultRoute && router.navigateToDefault({ ...transitionOptions, reload: true, replace: true });
                return;
            }
            if (routerState && router.areStatesEqual(state, routerState, false)) {
                return;
            }

            router.transitionToState(state, routerState, transitionOptions, (err, toState) => {
                if (err) {
                    if (err.redirect) {
                        const { name, params } = err.redirect;

                        router.navigate(name, params, { ...transitionOptions, replace: true });
                    } else if (err === errorCodes.CANNOT_DEACTIVATE) {
                        const url = router.buildUrl(routerState.name, routerState.params);
                        if (!newState) {
                            // Keep history state unchanged but use current URL
                            updateBrowserState(state, url, true);
                        }
                        // else do nothing or history will be messed up
                        // TODO: history.back()?
                    } else {
                        // Force navigation to default state
                        defaultRoute && router.navigate(defaultRoute, defaultParams, { ...transitionOptions, reload: true, replace: true});
                    }
                } else {
                    router.invokeEventListeners(constants.TRANSITION_SUCCESS, toState, routerState, {replace: true});
                }
            });
        }

        function onStart() {
            if (options.useHash && !options.base) {
                // Guess base
                options.base = browser.getBase();
            }

            browser.addPopstateListener(onPopState);
        }

        function onStop() {
            browser.removePopstateListener(onPopState);
        }

        function onTransitionSuccess(toState, fromState, opts) {
            const historyState = browser.getState();
            const replace = opts.replace || fromState && router.areStatesEqual(toState, fromState, false) ||
                opts.reload && historyState && router.areStatesEqual(toState, historyState, false);
            updateBrowserState(toState, router.buildUrl(toState.name, toState.params), replace);
        }

        return { onStart, onStop, onTransitionSuccess, onPopState };
    };

    browserPlugin.pluginName = 'BROWSER_PLUGIN';

    return browserPlugin;
}

export default browserPluginFactory;
<commit_msg>feat: add mergeState option in browser plugin to merge router state into current history state @iotch<commit_after>import constants, { errorCodes } from '../../constants';
import safeBrowser from './browser';
import withUtils from './utils';

const defaultOptions = {
    forceDeactivate: true,
    useHash: false,
    hashPrefix: '',
    base: false,
    mergeState: false
};

const source = 'popstate';

function browserPluginFactory(opts = {}, browser = safeBrowser) {
    const options = { ...defaultOptions, ...opts };
    const transitionOptions = { forceDeactivate: options.forceDeactivate, source };

    function browserPlugin(router) {
        const routerOptions = router.getOptions();
        const routerStart = router.start;

        withUtils(router, options);

        router.start = function(...args) {
            if (args.length === 0 || typeof args[0] === 'function') {
                routerStart(browser.getLocation(options), ...args);
            } else {
                routerStart(...args);
            }

            return router;
        };

        router.replaceHistoryState = function(name, params = {}) {
            const state = router.buildState(name, params);
            const url = router.buildUrl(name, params);
            router.lastKnownState = state;
            browser.replaceState(state, '', url);
        };

        function updateBrowserState(state, url, replace) {
            const finalState = options.mergeState === true ? { ...browser.getState(), ...state } : state;
            if (replace) browser.replaceState(finalState, '', url);
            else browser.pushState(finalState, '', url);
        }

        function onPopState(evt) {
            const routerState = router.getState();
            // Do nothing if no state or if last know state is poped state (it should never happen)
            const newState = !evt.state || !evt.state.name;
            const state = newState ? router.matchPath(browser.getLocation(options), source) : evt.state;
            const { defaultRoute, defaultParams } = routerOptions;

            if (!state) {
                // If current state is already the default route, we will have a double entry
                // Navigating back and forth will emit SAME_STATES error
                defaultRoute && router.navigateToDefault({ ...transitionOptions, reload: true, replace: true });
                return;
            }
            if (routerState && router.areStatesEqual(state, routerState, false)) {
                return;
            }

            router.transitionToState(state, routerState, transitionOptions, (err, toState) => {
                if (err) {
                    if (err.redirect) {
                        const { name, params } = err.redirect;

                        router.navigate(name, params, { ...transitionOptions, replace: true });
                    } else if (err === errorCodes.CANNOT_DEACTIVATE) {
                        const url = router.buildUrl(routerState.name, routerState.params);
                        if (!newState) {
                            // Keep history state unchanged but use current URL
                            updateBrowserState(state, url, true);
                        }
                        // else do nothing or history will be messed up
                        // TODO: history.back()?
                    } else {
                        // Force navigation to default state
                        defaultRoute && router.navigate(defaultRoute, defaultParams, { ...transitionOptions, reload: true, replace: true});
                    }
                } else {
                    router.invokeEventListeners(constants.TRANSITION_SUCCESS, toState, routerState, {replace: true});
                }
            });
        }

        function onStart() {
            if (options.useHash && !options.base) {
                // Guess base
                options.base = browser.getBase();
            }

            browser.addPopstateListener(onPopState);
        }

        function onStop() {
            browser.removePopstateListener(onPopState);
        }

        function onTransitionSuccess(toState, fromState, opts) {
            const historyState = browser.getState();
            const replace = opts.replace || fromState && router.areStatesEqual(toState, fromState, false) ||
                opts.reload && historyState && router.areStatesEqual(toState, historyState, false);
            updateBrowserState(toState, router.buildUrl(toState.name, toState.params), replace);
        }

        return { onStart, onStop, onTransitionSuccess, onPopState };
    };

    browserPlugin.pluginName = 'BROWSER_PLUGIN';

    return browserPlugin;
}

export default browserPluginFactory;
<|endoftext|>


<commit_before>/**
 * Embedded iframe with the same styles as its parent.
 *
 * @class
 * @extends OO.ui.Element
 * @mixins OO.EventEmitter
 *
 * @constructor
 * @param {Object} [config] Configuration options
 */
OO.ui.Frame = function OoUiFrame( config ) {
	// Parent constructor
	OO.ui.Frame.super.call( this, config );

	// Mixin constructors
	OO.EventEmitter.call( this );

	// Properties
	this.loading = null;
	this.config = config;

	// Initialize
	this.$element
		.addClass( 'oo-ui-frame' )
		.attr( { frameborder: 0, scrolling: 'no' } );

};

/* Setup */

OO.inheritClass( OO.ui.Frame, OO.ui.Element );
OO.mixinClass( OO.ui.Frame, OO.EventEmitter );

/* Static Properties */

/**
 * @static
 * @inheritdoc
 */
OO.ui.Frame.static.tagName = 'iframe';

/* Events */

/**
 * @event load
 */

/* Static Methods */

/**
 * Transplant the CSS styles from as parent document to a frame's document.
 *
 * This loops over the style sheets in the parent document, and copies their nodes to the
 * frame's document. It then polls the document to see when all styles have loaded, and once they
 * have, resolves the promise.
 *
 * If the styles still haven't loaded after a long time (5 seconds by default), we give up waiting
 * and resolve the promise anyway. This protects against cases like a display: none; iframe in
 * Firefox, where the styles won't load until the iframe becomes visible.
 *
 * For details of how we arrived at the strategy used in this function, see #load.
 *
 * @static
 * @inheritable
 * @param {HTMLDocument} parentDoc Document to transplant styles from
 * @param {HTMLDocument} frameDoc Document to transplant styles to
 * @param {number} [timeout=5000] How long to wait before giving up (in ms). If 0, never give up.
 * @return {jQuery.Promise} Promise resolved when styles have loaded
 */
OO.ui.Frame.static.transplantStyles = function ( parentDoc, frameDoc, timeout ) {
	var i, numSheets, styleNode, newNode, timeoutID, pollNodeId, $pendingPollNodes,
		$pollNodes = $( [] ),
		// Fake font-family value
		fontFamily = 'oo-ui-frame-transplantStyles-loaded',
		deferred = $.Deferred();

	for ( i = 0, numSheets = parentDoc.styleSheets.length; i < numSheets; i++ ) {
		styleNode = parentDoc.styleSheets[i].ownerNode;
		if ( styleNode.disabled ) {
			continue;
		}
		if ( styleNode.nodeName.toLowerCase() === 'link' ) {
			// External stylesheet
			// Create a node with a unique ID that we're going to monitor to see when the CSS
			// has loaded
			pollNodeId = 'oo-ui-frame-transplantStyles-loaded-' + i;
			$pollNodes = $pollNodes.add( $( '
', frameDoc )
				.attr( 'id', pollNodeId )
				.appendTo( frameDoc.body )
			);

			// Add
			// The font-family rule will only take effect once the @import finishes
			newNode = frameDoc.createElement( 'style' );
			newNode.textContent = '@import url(' + styleNode.href + ');\n' +
				'#' + pollNodeId + ' { font-family: ' + fontFamily + '; }';
		} else {
			// Not an external stylesheet, or no polling required; just copy the node over
			newNode = frameDoc.importNode( styleNode, true );
		}
		frameDoc.head.appendChild( newNode );
	}

	// Poll every 100ms until all external stylesheets have loaded
	$pendingPollNodes = $pollNodes;
	timeoutID = setTimeout( function pollExternalStylesheets() {
		while (
			$pendingPollNodes.length > 0 &&
			$pendingPollNodes.eq( 0 ).css( 'font-family' ) === fontFamily
		) {
			$pendingPollNodes = $pendingPollNodes.slice( 1 );
		}

		if ( $pendingPollNodes.length === 0 ) {
			// We're done!
			if ( timeoutID !== null ) {
				timeoutID = null;
				$pollNodes.remove();
				deferred.resolve();
			}
		} else {
			timeoutID = setTimeout( pollExternalStylesheets, 100 );
		}
	}, 100 );
	// ...but give up after a while
	if ( timeout !== 0 ) {
		setTimeout( function () {
			if ( timeoutID ) {
				clearTimeout( timeoutID );
				timeoutID = null;
				$pollNodes.remove();
				deferred.reject();
			}
		}, timeout || 5000 );
	}

	return deferred.promise();
};

/* Methods */

/**
 * Load the frame contents.
 *
 * Once the iframe's stylesheets are loaded, the `load` event will be emitted and the returned
 * promise will be resolved. Calling while loading will return a promise but not trigger a new
 * loading cycle. Calling after loading is complete will return a promise that's already been
 * resolved.
 *
 * Sounds simple right? Read on...
 *
 * When you create a dynamic iframe using open/write/close, the window.load event for the
 * iframe is triggered when you call close, and there's no further load event to indicate that
 * everything is actually loaded.
 *
 * In Chrome, stylesheets don't show up in document.styleSheets until they have loaded, so we could
 * just poll that array and wait for it to have the right length. However, in Firefox, stylesheets
 * are added to document.styleSheets immediately, and the only way you can determine whether they've
 * loaded is to attempt to access .cssRules and wait for that to stop throwing an exception. But
 * cross-domain stylesheets never allow .cssRules to be accessed even after they have loaded.
 *
 * The workaround is to change all `<link href="...">` tags to `<style>@import url(...)</style>` tags.
 * Because `@import` is blocking, Chrome won't add the stylesheet to document.styleSheets until
 * the `@import` has finished, and Firefox won't allow .cssRules to be accessed until the `@import`
 * has finished. And because the contents of the `<style>` tag are from the same origin, accessing
 * .cssRules is allowed.
 *
 * However, now that we control the styles we're injecting, we might as well do away with
 * browser-specific polling hacks like document.styleSheets and .cssRules, and instead inject
 * `<style>@import url(...); #foo { font-family: someValue; }</style>`, then create `<div id="foo">`
 * and wait for its font-family to change to someValue. Because `@import` is blocking, the font-family
 * rule is not applied until after the `@import` finishes.
 *
 * All this stylesheet injection and polling magic is in #transplantStyles.
 *
 * @return {jQuery.Promise} Promise resolved when loading is complete
 * @fires load
 */
OO.ui.Frame.prototype.load = function () {
	var win, doc,
		frame = this;

	// Return existing promise if already loading or loaded
	if ( this.loading ) {
		return this.loading.promise();
	}

	// Load the frame
	this.loading = $.Deferred();

	win = this.$element.prop( 'contentWindow' );
	doc = win.document;

	// Figure out directionality:
	this.dir = OO.ui.Element.getDir( this.$element ) || 'ltr';

	// Initialize contents
	doc.open();
	doc.write(
		'<!doctype html>' +
		'' +
			'' +
			'' +
		''
	);
	doc.close();

	// Properties
	this.$ = OO.ui.Element.getJQuery( doc, this );
	this.$content = this.$( '.oo-ui-frame-content' ).attr( 'tabIndex', 0 );
	this.$document = this.$( doc );

	// Initialization
	this.constructor.static.transplantStyles( this.getElementDocument(), this.$document[0] )
		.always( function () {
			frame.emit( 'load' );
			frame.loading.resolve();
		} );

	return this.loading.promise();
};

/**
 * Set the size of the frame.
 *
 * @param {number} width Frame width in pixels
 * @param {number} height Frame height in pixels
 * @chainable
 */
OO.ui.Frame.prototype.setSize = function ( width, height ) {
	this.$element.css( { width: width, height: height } );
	return this;
};
<commit_msg>Remove direction style attribute<commit_after>/**
 * Embedded iframe with the same styles as its parent.
 *
 * @class
 * @extends OO.ui.Element
 * @mixins OO.EventEmitter
 *
 * @constructor
 * @param {Object} [config] Configuration options
 */
OO.ui.Frame = function OoUiFrame( config ) {
	// Parent constructor
	OO.ui.Frame.super.call( this, config );

	// Mixin constructors
	OO.EventEmitter.call( this );

	// Properties
	this.loading = null;
	this.config = config;

	// Initialize
	this.$element
		.addClass( 'oo-ui-frame' )
		.attr( { frameborder: 0, scrolling: 'no' } );

};

/* Setup */

OO.inheritClass( OO.ui.Frame, OO.ui.Element );
OO.mixinClass( OO.ui.Frame, OO.EventEmitter );

/* Static Properties */

/**
 * @static
 * @inheritdoc
 */
OO.ui.Frame.static.tagName = 'iframe';

/* Events */

/**
 * @event load
 */

/* Static Methods */

/**
 * Transplant the CSS styles from as parent document to a frame's document.
 *
 * This loops over the style sheets in the parent document, and copies their nodes to the
 * frame's document. It then polls the document to see when all styles have loaded, and once they
 * have, resolves the promise.
 *
 * If the styles still haven't loaded after a long time (5 seconds by default), we give up waiting
 * and resolve the promise anyway. This protects against cases like a display: none; iframe in
 * Firefox, where the styles won't load until the iframe becomes visible.
 *
 * For details of how we arrived at the strategy used in this function, see #load.
 *
 * @static
 * @inheritable
 * @param {HTMLDocument} parentDoc Document to transplant styles from
 * @param {HTMLDocument} frameDoc Document to transplant styles to
 * @param {number} [timeout=5000] How long to wait before giving up (in ms). If 0, never give up.
 * @return {jQuery.Promise} Promise resolved when styles have loaded
 */
OO.ui.Frame.static.transplantStyles = function ( parentDoc, frameDoc, timeout ) {
	var i, numSheets, styleNode, newNode, timeoutID, pollNodeId, $pendingPollNodes,
		$pollNodes = $( [] ),
		// Fake font-family value
		fontFamily = 'oo-ui-frame-transplantStyles-loaded',
		deferred = $.Deferred();

	for ( i = 0, numSheets = parentDoc.styleSheets.length; i < numSheets; i++ ) {
		styleNode = parentDoc.styleSheets[i].ownerNode;
		if ( styleNode.disabled ) {
			continue;
		}
		if ( styleNode.nodeName.toLowerCase() === 'link' ) {
			// External stylesheet
			// Create a node with a unique ID that we're going to monitor to see when the CSS
			// has loaded
			pollNodeId = 'oo-ui-frame-transplantStyles-loaded-' + i;
			$pollNodes = $pollNodes.add( $( '
', frameDoc )
				.attr( 'id', pollNodeId )
				.appendTo( frameDoc.body )
			);

			// Add
			// The font-family rule will only take effect once the @import finishes
			newNode = frameDoc.createElement( 'style' );
			newNode.textContent = '@import url(' + styleNode.href + ');\n' +
				'#' + pollNodeId + ' { font-family: ' + fontFamily + '; }';
		} else {
			// Not an external stylesheet, or no polling required; just copy the node over
			newNode = frameDoc.importNode( styleNode, true );
		}
		frameDoc.head.appendChild( newNode );
	}

	// Poll every 100ms until all external stylesheets have loaded
	$pendingPollNodes = $pollNodes;
	timeoutID = setTimeout( function pollExternalStylesheets() {
		while (
			$pendingPollNodes.length > 0 &&
			$pendingPollNodes.eq( 0 ).css( 'font-family' ) === fontFamily
		) {
			$pendingPollNodes = $pendingPollNodes.slice( 1 );
		}

		if ( $pendingPollNodes.length === 0 ) {
			// We're done!
			if ( timeoutID !== null ) {
				timeoutID = null;
				$pollNodes.remove();
				deferred.resolve();
			}
		} else {
			timeoutID = setTimeout( pollExternalStylesheets, 100 );
		}
	}, 100 );
	// ...but give up after a while
	if ( timeout !== 0 ) {
		setTimeout( function () {
			if ( timeoutID ) {
				clearTimeout( timeoutID );
				timeoutID = null;
				$pollNodes.remove();
				deferred.reject();
			}
		}, timeout || 5000 );
	}

	return deferred.promise();
};

/* Methods */

/**
 * Load the frame contents.
 *
 * Once the iframe's stylesheets are loaded, the `load` event will be emitted and the returned
 * promise will be resolved. Calling while loading will return a promise but not trigger a new
 * loading cycle. Calling after loading is complete will return a promise that's already been
 * resolved.
 *
 * Sounds simple right? Read on...
 *
 * When you create a dynamic iframe using open/write/close, the window.load event for the
 * iframe is triggered when you call close, and there's no further load event to indicate that
 * everything is actually loaded.
 *
 * In Chrome, stylesheets don't show up in document.styleSheets until they have loaded, so we could
 * just poll that array and wait for it to have the right length. However, in Firefox, stylesheets
 * are added to document.styleSheets immediately, and the only way you can determine whether they've
 * loaded is to attempt to access .cssRules and wait for that to stop throwing an exception. But
 * cross-domain stylesheets never allow .cssRules to be accessed even after they have loaded.
 *
 * The workaround is to change all `<link href="...">` tags to `<style>@import url(...)</style>` tags.
 * Because `@import` is blocking, Chrome won't add the stylesheet to document.styleSheets until
 * the `@import` has finished, and Firefox won't allow .cssRules to be accessed until the `@import`
 * has finished. And because the contents of the `<style>` tag are from the same origin, accessing
 * .cssRules is allowed.
 *
 * However, now that we control the styles we're injecting, we might as well do away with
 * browser-specific polling hacks like document.styleSheets and .cssRules, and instead inject
 * `<style>@import url(...); #foo { font-family: someValue; }</style>`, then create `<div id="foo">`
 * and wait for its font-family to change to someValue. Because `@import` is blocking, the font-family
 * rule is not applied until after the `@import` finishes.
 *
 * All this stylesheet injection and polling magic is in #transplantStyles.
 *
 * @return {jQuery.Promise} Promise resolved when loading is complete
 * @fires load
 */
OO.ui.Frame.prototype.load = function () {
	var win, doc,
		frame = this;

	// Return existing promise if already loading or loaded
	if ( this.loading ) {
		return this.loading.promise();
	}

	// Load the frame
	this.loading = $.Deferred();

	win = this.$element.prop( 'contentWindow' );
	doc = win.document;

	// Figure out directionality:
	this.dir = OO.ui.Element.getDir( this.$element ) || 'ltr';

	// Initialize contents
	doc.open();
	doc.write(
		'<!doctype html>' +
		'' +
			'' +
			'' +
		''
	);
	doc.close();

	// Properties
	this.$ = OO.ui.Element.getJQuery( doc, this );
	this.$content = this.$( '.oo-ui-frame-content' ).attr( 'tabIndex', 0 );
	this.$document = this.$( doc );

	// Initialization
	this.constructor.static.transplantStyles( this.getElementDocument(), this.$document[0] )
		.always( function () {
			frame.emit( 'load' );
			frame.loading.resolve();
		} );

	return this.loading.promise();
};

/**
 * Set the size of the frame.
 *
 * @param {number} width Frame width in pixels
 * @param {number} height Frame height in pixels
 * @chainable
 */
OO.ui.Frame.prototype.setSize = function ( width, height ) {
	this.$element.css( { width: width, height: height } );
	return this;
};
<|endoftext|>


<commit_before>"use strict";
// 3rd party
var Router = require('koa-router');
var _ = require('lodash');
var debug = require('debug')('app:routes:convos');
var bouncer = require('koa-bouncer');
// 1st party
var db = require('../db');
var belt = require('../belt');
var pre = require('../presenters');
var config = require('../config');
var welcomePm = require('../welcome_pm');
var cancan = require('../cancan');
var avatar = require('../avatar');
var bbcode = require('../bbcode');
var paginate = require('../paginate');

var router = new Router();

////////////////////////////////////////////////////////////

//
// Create convo
// Params:
// - 'to': Comma-delimited string of unames user wants to send to
// - 'title'
// - 'markup'
//
router.post('/convos', async (ctx) => {
  if (!config.IS_PM_SYSTEM_ONLINE) {
    ctx.body = 'PM system currently disabled';
    return;
  }

  ctx.assertAuthorized(ctx.currUser, 'CREATE_CONVO');

  // Light input validation
  ctx.validateBody('title')
     .isLength(1, config.MAX_TOPIC_TITLE_LENGTH,
               `Title must be 1-${config.MAX_TOPIC_TITLE_LENGTH} chars`)
  ctx.validateBody('markup')
    .isLength(config.MIN_POST_LENGTH, config.MAX_POST_LENGTH,
              'Post text must be ' + config.MIN_POST_LENGTH +
              '-' + config.MAX_POST_LENGTH + ' chars long');
  // Array of lowercase uname strings
  // Remove empty (Note: unames contains lowercase unames)
  ctx.validateBody('to')
    .tap((v) => {
      return v.split(',').map((uname) => {
        return uname.trim().toLowerCase()
      }).filter(Boolean)
    })
    // Ensure user didn't specify themself
    .tap((unames) => {
      return unames.filter((uname) => {
        return uname !== ctx.currUser.uname.toLowerCase();
      })
    })
    // Remove duplicates
    .uniq()
    .isLength(0, 5, 'You cannot send a PM to more than 5 people at once');

  // TODO: Validation, Error msgs, preserve params

  var unames = ctx.vals.to;
  var title = ctx.vals.title;
  var markup = ctx.vals.markup;

  debug('==============');
  debug(ctx.vals);

  // Ensure they are all real users
  var users = await db.findUsersByUnames(unames);

  // If not all unames resolved into users, then we return user to form
  // to fix it.
  if (users.length !== unames.length) {
    var rejectedUnames = _.difference(unames, users.map(function(user) {
      return user.uname.toLowerCase();
    }));
    ctx.flash = {
      message: [
        'danger',
        'No users were found with these names: ' + rejectedUnames.join(', ')
      ]
    };
    ctx.response.redirect('/convos/new?to=' + unames.join(','));
    return;
  }

  // Render bbcode
  var html = bbcode(markup);

  // If all unames are valid, then we can create a convo
  const toUserIds = users.map((x) => x.id)
  const convo = await db.createConvo({
    userId: ctx.currUser.id,
    toUserIds,
    title,
    markup,
    html,
    ipAddress: ctx.request.ip
  }).then(pre.presentConvo);

  // Create CONVO notification for each recipient
  toUserIds.map(function (toUserId) {
    return db.createConvoNotification({
      from_user_id: ctx.currUser.id,
      to_user_id: toUserId,
      convo_id: convo.id
    });
  });

  ctx.response.redirect(convo.url);
});

////////////////////////////////////////////////////////////

//
// New Convo
//
// TODO: Implement typeahead
router.get('/convos/new', async (ctx) => {
  if (!config.IS_PM_SYSTEM_ONLINE) {
    ctx.body = 'PM system currently disabled';
    return;
  }

  ctx.assertAuthorized(ctx.currUser, 'CREATE_CONVO');
  // TODO: Validation, Error msgs, preserve params
  await ctx.render('new_convo', {
    ctx,
    to: ctx.request.query.to,
    title: 'New Conversation'
  });
});

////////////////////////////////////////////////////////////

//
// Create PM
// Body params
// - markup
//
router.post('/convos/:convoId/pms', async (ctx) => {
  if (!config.IS_PM_SYSTEM_ONLINE) {
    ctx.body = 'PM system currently disabled';
    return;
  }

  ctx.assert(ctx.currUser, 403);

  try {
    ctx.validateBody('markup')
      .isLength(config.MIN_POST_LENGTH, config.MAX_POST_LENGTH);
  } catch(ex) {
    if (ex instanceof bouncer.ValidationError) {
      ctx.flash = {
        message: ['danger', ex.message],
        params: ctx.request.body
      };
      ctx.redirect('/convos/' + ctx.params.convoId);
    }
    throw ex;
  }

  const convo = await db.convos.getConvo(ctx.params.convoId);
  ctx.assert(convo, 404);
  ctx.assertAuthorized(ctx.currUser, 'CREATE_PM', convo);

  // Render bbcode
  var html = bbcode(ctx.vals.markup);

  var pm = await db.createPm({
    userId: ctx.currUser.id,
    ipAddress: ctx.request.ip,
    convoId: ctx.params.convoId,
    markup: ctx.vals.markup,
    html: html
  });
  pre.presentPm(pm);

  // Get only userIds of the *other* participants
  // Don't want to create notification for ourself
  var toUserIds = (await db.convos.findParticipantIds(ctx.params.convoId))
    .filter((userId) => userId !== ctx.currUser.id)

  // Upsert notifications table
  // TODO: config.MAX_CONVO_PARTICIPANTS instead of hard-coded 5
  toUserIds.map(function (toUserId) {
    return db.createPmNotification({
      from_user_id: ctx.currUser.id,
      to_user_id: toUserId,
      convo_id: ctx.params.convoId
    });
  });

  ctx.redirect(pm.url);
});

////////////////////////////////////////////////////////////

// Empty trash folder
router.delete('/me/convos/trash', async (ctx) => {
  ctx.assert(ctx.currUser, 404)

  await db.convos.deleteTrash(ctx.currUser.id)

  ctx.flash = { message: ['success', 'Trash deleted'] }
  ctx.redirect('/me/convos')
})

//
// Delete convo
//
// Body: { ids: [Int] }
router.delete('/me/convos', async (ctx) => {
  const ids = ctx.validateBody('ids')
    .toArray()
    .toInts()
    .val()

  const convos = await db.convos.getConvos(ids)
    .then((xs) => xs.map(pre.presentConvo))

  ctx.assert(
    convos.every((convo) => cancan.can(ctx.currUser, 'DELETE_CONVO', convo)),
    401,
    'You do not have access to all of the selected convos'
  )

  await db.convos.deleteConvos(ctx.currUser.id, convos.map((c) => c.id))

  ctx.flash = { message: ['success', 'Convos deleted'] }
  ctx.redirect('/me/convos')
})

//
// Delete convo
//
router.delete('/convos/:convoId', async (ctx) => {
  const {convoId} = ctx.params
  const convo = await db.convos.getConvo(convoId)
    .then(pre.presentConvo)
  ctx.assert(convo, 404)
  ctx.assertAuthorized(ctx.currUser, 'DELETE_CONVO', convo)

  await db.convos.deleteConvos(ctx.currUser.id, [convo.id])

  ctx.flash = { message: ['success', 'Convo deleted'] }
  ctx.redirect('/me/convos')
})

////////////////////////////////////////////////////////////

//
// Show convo
//
router.get('/convos/:convoId', async (ctx) => {
  var convoId = ctx.params.convoId;

  if (!config.IS_PM_SYSTEM_ONLINE) {
    ctx.body = 'PM system currently disabled';
    return;
  }

  ctx.assert(ctx.currUser, 404);
  var convo = await db.convos.getConvo(convoId)
  ctx.assert(convo, 404);
  ctx.assertAuthorized(ctx.currUser, 'READ_CONVO', convo);

  const folder = (() => {
    return convo.cp.filter(cp => cp.user_id === ctx.currUser.id)[0].folder;
  })();

  ctx.validateQuery('page')
    .defaultTo(1)
    .toInt()
    // Clamp it to minimum of 1
    .tap((n) => Math.max(1, n))

  // If ?page=1 was given, then redirect without param
  // since page 1 is already the canonical destination of a convo url
  if (ctx.query.page && ctx.vals.page === 1) {
    ctx.status = 301;
    return ctx.redirect(ctx.path);
  }

  var page = ctx.vals.page;
  var totalItems = convo.pms_count;
  var totalPages = belt.calcTotalPostPages(totalItems);

  // Redirect to the highest page if page parameter exceeded it
  if (page > totalPages) {
    var redirectUrl = page === 1 ? ctx.path : ctx.path + '?page=' + totalPages;
    return ctx.redirect(redirectUrl);
  }

  // 0 or 1
  var count = await db.deleteConvoNotification(ctx.currUser.id, convoId);

  // Update the stale user's counts so that the notification count is reduced
  // appropriately when the page loads. Otherwise, the counts won't be updated
  // til next request.
  ctx.currUser.notifications_count -= count;
  ctx.currUser.convo_notifications_count -= count;

  var pms = await db.findPmsByConvoId(convoId, page);
  convo.pms = pms;
  convo = pre.presentConvo(convo);
  await ctx.render('show_convo', {
    ctx,
    convo: convo,
    title: convo.title,
    // Pagination
    currPage: page,
    totalPages: totalPages,
    folder
  });
});

////////////////////////////////////////////////////////////

function showConvosHandler (folder) {
  return async function _showConvosHandler (ctx) {
    if (!config.IS_PM_SYSTEM_ONLINE) {
      ctx.body = 'PM system currently disabled'
      return
    }

    ctx.validateQuery('page')
      .defaultTo(1)
      .toInt()
      .tap(n => Math.max(1, n))

    ctx.assert(ctx.currUser, 404)

    const [convos, counts] = await Promise.all([
      db.convos.findConvosInvolvingUserId(ctx.currUser.id, folder, ctx.vals.page)
        .then((xs) => xs.map(pre.presentConvo)),
      db.convos.getConvoFolderCounts(ctx.currUser.id)
    ])

    const itemsInFolder = counts[`${folder.toLowerCase()}_count`]
    const fullPaginator = paginate.makeFullPaginator(ctx.vals.page, itemsInFolder)

    var nextBeforeId = convos.length > 0 ? _.last(convos).latest_pm_id : null
    await ctx.render('me_convos', {
      ctx,
      title: 'My Private Conversations',
      counts,
      folderEmpty: itemsInFolder === 0,
      convos,
      folder,
      // FullPagination
      fullPaginator,
      // Pagination
      beforeId: ctx.vals['before-id'],
      nextBeforeId,
      perPage: config.CONVOS_PER_PAGE
    })
  }
}

router.get('/me/convos', showConvosHandler('INBOX'));
router.get('/me/convos/star', showConvosHandler('STAR'));
router.get('/me/convos/archive', showConvosHandler('ARCHIVE'));
router.get('/me/convos/trash', showConvosHandler('TRASH'));

router.put('/convos/:convoId/folder', async (ctx) => {
  var folder = ctx.request.body.folder;
  ctx.assert(['INBOX', 'STAR', 'ARCHIVE', 'TRASH'].includes(folder), 400)

  let convo = await db.convos.getConvo(ctx.params.convoId);
  ctx.assert(convo, 404);
  ctx.assertAuthorized(ctx.currUser, 'READ_CONVO', convo);

  await db.updateConvoFolder(ctx.currUser.id, convo.id, folder);

  ctx.flash = { message: ['success', 'Convo updated'] };
  ctx.redirect(`/convos/${convo.id}`);
});

////////////////////////////////////////////////////////////

// body: { ids: [Int], folder: String }
router.post('/me/convos/move', async (ctx) => {
  const {folder} = ctx.request.body
  ctx.assert(['INBOX', 'STAR', 'ARCHIVE', 'TRASH'].includes(folder), 400)

  const ids = ctx.validateBody('ids')
    .toArray()
    .toInts()
    .val()

  const convos = await db.convos.getConvos(ids)
    .then((xs) => xs.map(pre.presentConvo))

  ctx.assert(
    convos.every((convo) => cancan.can(ctx.currUser, 'READ_CONVO', convo)),
    401,
    'You do not have access to all of the selected convos'
  )

  await db.convos.moveConvos(ctx.currUser.id, ids, folder)

  ctx.flash = { message: ['success', `Convos moved to ${folder}`] }
  ctx.redirect(`/me/convos/${folder.toLowerCase()}`)
})

////////////////////////////////////////////////////////////

module.exports = router;
<commit_msg>Fix move-convo redirect<commit_after>"use strict";
// 3rd party
var Router = require('koa-router');
var _ = require('lodash');
var debug = require('debug')('app:routes:convos');
var bouncer = require('koa-bouncer');
// 1st party
var db = require('../db');
var belt = require('../belt');
var pre = require('../presenters');
var config = require('../config');
var welcomePm = require('../welcome_pm');
var cancan = require('../cancan');
var avatar = require('../avatar');
var bbcode = require('../bbcode');
var paginate = require('../paginate');

var router = new Router();

////////////////////////////////////////////////////////////

//
// Create convo
// Params:
// - 'to': Comma-delimited string of unames user wants to send to
// - 'title'
// - 'markup'
//
router.post('/convos', async (ctx) => {
  if (!config.IS_PM_SYSTEM_ONLINE) {
    ctx.body = 'PM system currently disabled';
    return;
  }

  ctx.assertAuthorized(ctx.currUser, 'CREATE_CONVO');

  // Light input validation
  ctx.validateBody('title')
     .isLength(1, config.MAX_TOPIC_TITLE_LENGTH,
               `Title must be 1-${config.MAX_TOPIC_TITLE_LENGTH} chars`)
  ctx.validateBody('markup')
    .isLength(config.MIN_POST_LENGTH, config.MAX_POST_LENGTH,
              'Post text must be ' + config.MIN_POST_LENGTH +
              '-' + config.MAX_POST_LENGTH + ' chars long');
  // Array of lowercase uname strings
  // Remove empty (Note: unames contains lowercase unames)
  ctx.validateBody('to')
    .tap((v) => {
      return v.split(',').map((uname) => {
        return uname.trim().toLowerCase()
      }).filter(Boolean)
    })
    // Ensure user didn't specify themself
    .tap((unames) => {
      return unames.filter((uname) => {
        return uname !== ctx.currUser.uname.toLowerCase();
      })
    })
    // Remove duplicates
    .uniq()
    .isLength(0, 5, 'You cannot send a PM to more than 5 people at once');

  // TODO: Validation, Error msgs, preserve params

  var unames = ctx.vals.to;
  var title = ctx.vals.title;
  var markup = ctx.vals.markup;

  debug('==============');
  debug(ctx.vals);

  // Ensure they are all real users
  var users = await db.findUsersByUnames(unames);

  // If not all unames resolved into users, then we return user to form
  // to fix it.
  if (users.length !== unames.length) {
    var rejectedUnames = _.difference(unames, users.map(function(user) {
      return user.uname.toLowerCase();
    }));
    ctx.flash = {
      message: [
        'danger',
        'No users were found with these names: ' + rejectedUnames.join(', ')
      ]
    };
    ctx.response.redirect('/convos/new?to=' + unames.join(','));
    return;
  }

  // Render bbcode
  var html = bbcode(markup);

  // If all unames are valid, then we can create a convo
  const toUserIds = users.map((x) => x.id)
  const convo = await db.createConvo({
    userId: ctx.currUser.id,
    toUserIds,
    title,
    markup,
    html,
    ipAddress: ctx.request.ip
  }).then(pre.presentConvo);

  // Create CONVO notification for each recipient
  toUserIds.map(function (toUserId) {
    return db.createConvoNotification({
      from_user_id: ctx.currUser.id,
      to_user_id: toUserId,
      convo_id: convo.id
    });
  });

  ctx.response.redirect(convo.url);
});

////////////////////////////////////////////////////////////

//
// New Convo
//
// TODO: Implement typeahead
router.get('/convos/new', async (ctx) => {
  if (!config.IS_PM_SYSTEM_ONLINE) {
    ctx.body = 'PM system currently disabled';
    return;
  }

  ctx.assertAuthorized(ctx.currUser, 'CREATE_CONVO');
  // TODO: Validation, Error msgs, preserve params
  await ctx.render('new_convo', {
    ctx,
    to: ctx.request.query.to,
    title: 'New Conversation'
  });
});

////////////////////////////////////////////////////////////

//
// Create PM
// Body params
// - markup
//
router.post('/convos/:convoId/pms', async (ctx) => {
  if (!config.IS_PM_SYSTEM_ONLINE) {
    ctx.body = 'PM system currently disabled';
    return;
  }

  ctx.assert(ctx.currUser, 403);

  try {
    ctx.validateBody('markup')
      .isLength(config.MIN_POST_LENGTH, config.MAX_POST_LENGTH);
  } catch(ex) {
    if (ex instanceof bouncer.ValidationError) {
      ctx.flash = {
        message: ['danger', ex.message],
        params: ctx.request.body
      };
      ctx.redirect('/convos/' + ctx.params.convoId);
    }
    throw ex;
  }

  const convo = await db.convos.getConvo(ctx.params.convoId);
  ctx.assert(convo, 404);
  ctx.assertAuthorized(ctx.currUser, 'CREATE_PM', convo);

  // Render bbcode
  var html = bbcode(ctx.vals.markup);

  var pm = await db.createPm({
    userId: ctx.currUser.id,
    ipAddress: ctx.request.ip,
    convoId: ctx.params.convoId,
    markup: ctx.vals.markup,
    html: html
  });
  pre.presentPm(pm);

  // Get only userIds of the *other* participants
  // Don't want to create notification for ourself
  var toUserIds = (await db.convos.findParticipantIds(ctx.params.convoId))
    .filter((userId) => userId !== ctx.currUser.id)

  // Upsert notifications table
  // TODO: config.MAX_CONVO_PARTICIPANTS instead of hard-coded 5
  toUserIds.map(function (toUserId) {
    return db.createPmNotification({
      from_user_id: ctx.currUser.id,
      to_user_id: toUserId,
      convo_id: ctx.params.convoId
    });
  });

  ctx.redirect(pm.url);
});

////////////////////////////////////////////////////////////

// Empty trash folder
router.delete('/me/convos/trash', async (ctx) => {
  ctx.assert(ctx.currUser, 404)

  await db.convos.deleteTrash(ctx.currUser.id)

  ctx.flash = { message: ['success', 'Trash deleted'] }
  ctx.redirect('/me/convos')
})

//
// Delete convo
//
// Body: { ids: [Int] }
router.delete('/me/convos', async (ctx) => {
  const ids = ctx.validateBody('ids')
    .toArray()
    .toInts()
    .val()

  const convos = await db.convos.getConvos(ids)
    .then((xs) => xs.map(pre.presentConvo))

  ctx.assert(
    convos.every((convo) => cancan.can(ctx.currUser, 'DELETE_CONVO', convo)),
    401,
    'You do not have access to all of the selected convos'
  )

  await db.convos.deleteConvos(ctx.currUser.id, convos.map((c) => c.id))

  ctx.flash = { message: ['success', 'Convos deleted'] }
  ctx.redirect('/me/convos')
})

//
// Delete convo
//
router.delete('/convos/:convoId', async (ctx) => {
  const {convoId} = ctx.params
  const convo = await db.convos.getConvo(convoId)
    .then(pre.presentConvo)
  ctx.assert(convo, 404)
  ctx.assertAuthorized(ctx.currUser, 'DELETE_CONVO', convo)

  await db.convos.deleteConvos(ctx.currUser.id, [convo.id])

  ctx.flash = { message: ['success', 'Convo deleted'] }
  ctx.redirect('/me/convos')
})

////////////////////////////////////////////////////////////

//
// Show convo
//
router.get('/convos/:convoId', async (ctx) => {
  var convoId = ctx.params.convoId;

  if (!config.IS_PM_SYSTEM_ONLINE) {
    ctx.body = 'PM system currently disabled';
    return;
  }

  ctx.assert(ctx.currUser, 404);
  var convo = await db.convos.getConvo(convoId)
  ctx.assert(convo, 404);
  ctx.assertAuthorized(ctx.currUser, 'READ_CONVO', convo);

  const folder = (() => {
    return convo.cp.filter(cp => cp.user_id === ctx.currUser.id)[0].folder;
  })();

  ctx.validateQuery('page')
    .defaultTo(1)
    .toInt()
    // Clamp it to minimum of 1
    .tap((n) => Math.max(1, n))

  // If ?page=1 was given, then redirect without param
  // since page 1 is already the canonical destination of a convo url
  if (ctx.query.page && ctx.vals.page === 1) {
    ctx.status = 301;
    return ctx.redirect(ctx.path);
  }

  var page = ctx.vals.page;
  var totalItems = convo.pms_count;
  var totalPages = belt.calcTotalPostPages(totalItems);

  // Redirect to the highest page if page parameter exceeded it
  if (page > totalPages) {
    var redirectUrl = page === 1 ? ctx.path : ctx.path + '?page=' + totalPages;
    return ctx.redirect(redirectUrl);
  }

  // 0 or 1
  var count = await db.deleteConvoNotification(ctx.currUser.id, convoId);

  // Update the stale user's counts so that the notification count is reduced
  // appropriately when the page loads. Otherwise, the counts won't be updated
  // til next request.
  ctx.currUser.notifications_count -= count;
  ctx.currUser.convo_notifications_count -= count;

  var pms = await db.findPmsByConvoId(convoId, page);
  convo.pms = pms;
  convo = pre.presentConvo(convo);
  await ctx.render('show_convo', {
    ctx,
    convo: convo,
    title: convo.title,
    // Pagination
    currPage: page,
    totalPages: totalPages,
    folder
  });
});

////////////////////////////////////////////////////////////

function showConvosHandler (folder) {
  return async function _showConvosHandler (ctx) {
    if (!config.IS_PM_SYSTEM_ONLINE) {
      ctx.body = 'PM system currently disabled'
      return
    }

    ctx.validateQuery('page')
      .defaultTo(1)
      .toInt()
      .tap(n => Math.max(1, n))

    ctx.assert(ctx.currUser, 404)

    const [convos, counts] = await Promise.all([
      db.convos.findConvosInvolvingUserId(ctx.currUser.id, folder, ctx.vals.page)
        .then((xs) => xs.map(pre.presentConvo)),
      db.convos.getConvoFolderCounts(ctx.currUser.id)
    ])

    const itemsInFolder = counts[`${folder.toLowerCase()}_count`]
    const fullPaginator = paginate.makeFullPaginator(ctx.vals.page, itemsInFolder)

    var nextBeforeId = convos.length > 0 ? _.last(convos).latest_pm_id : null
    await ctx.render('me_convos', {
      ctx,
      title: 'My Private Conversations',
      counts,
      folderEmpty: itemsInFolder === 0,
      convos,
      folder,
      // FullPagination
      fullPaginator,
      // Pagination
      beforeId: ctx.vals['before-id'],
      nextBeforeId,
      perPage: config.CONVOS_PER_PAGE
    })
  }
}

router.get('/me/convos', showConvosHandler('INBOX'));
router.get('/me/convos/star', showConvosHandler('STAR'));
router.get('/me/convos/archive', showConvosHandler('ARCHIVE'));
router.get('/me/convos/trash', showConvosHandler('TRASH'));

router.put('/convos/:convoId/folder', async (ctx) => {
  var folder = ctx.request.body.folder;
  ctx.assert(['INBOX', 'STAR', 'ARCHIVE', 'TRASH'].includes(folder), 400)

  let convo = await db.convos.getConvo(ctx.params.convoId);
  ctx.assert(convo, 404);
  ctx.assertAuthorized(ctx.currUser, 'READ_CONVO', convo);

  await db.updateConvoFolder(ctx.currUser.id, convo.id, folder);

  ctx.flash = { message: ['success', 'Convo updated'] };
  ctx.redirect(`/convos/${convo.id}`);
});

////////////////////////////////////////////////////////////

// body: { ids: [Int], folder: String }
router.post('/me/convos/move', async (ctx) => {
  const {folder} = ctx.request.body
  ctx.assert(['INBOX', 'STAR', 'ARCHIVE', 'TRASH'].includes(folder), 400)

  const ids = ctx.validateBody('ids')
    .toArray()
    .toInts()
    .val()

  const convos = await db.convos.getConvos(ids)
    .then((xs) => xs.map(pre.presentConvo))

  ctx.assert(
    convos.every((convo) => cancan.can(ctx.currUser, 'READ_CONVO', convo)),
    401,
    'You do not have access to all of the selected convos'
  )

  await db.convos.moveConvos(ctx.currUser.id, ids, folder)

  ctx.flash = { message: ['success', `Convos moved to ${folder}`] }
  if (folder === 'INBOX') {
    ctx.redirect(`/me/convos`)
  } else {
    ctx.redirect(`/me/convos/${folder.toLowerCase()}`)
  }
})

////////////////////////////////////////////////////////////

module.exports = router;
<|endoftext|>


<commit_before>(function () {
    'use strict';

    var crypto = require('crypto');
    var bodyParser = require('body-parser');
    var r = require('rethinkdb');
    var config = require('../config');
    var router = require('express').Router();

    var lastUpdate = 0;

    router.get('/photo', function (req, res) {
        if (req.query['hub.verify_token'] == config.instagram.verify) {
            res.send(req.query['hub.challenge']);
        } else {
            config.logger.error('token verification incorrect');
            res.status(500).json({err: 'Verify token incorrect'});
        }
    });

    router.use(bodyParser.json({
        verify: function (req, res, buf) {
            var hmac = crypto.createHmac('sha1', config.instagram.secret);
            var hash = hmac.update(buf).digest('hex');

            if (req.header('X-Hub-Signature') == hash) {
                req.validOrigin = true;
            }
        }
    }));

    router.post('/photo', function (req, res) {
        if (!req.validOrigin) {
            config.logger.error('Invalid signature in POST /api/instagram/photo');
            return res.status(500).json({err: 'Invalid signature'});
        }

        var update = req.body[0];
        res.json({success: true, kind: update.object});

        if (update.time - lastUpdate < 1) return;
        lastUpdate = update.time;

        var path = 'https://api.instagram.com/v1/tags/' + update.object_id + '/media/recent?client_id=' + config.instagram.client;

        var conn;
        r.connect(config.database)
            .then(function (c) {
                conn = c;
                return r.table('pictures')
                    .insert(r.http(path)('data').merge(function (item) {
                        return {
                            created_time: r.epochTime(item('created_time').coerceTo('number')),
                            time: r.now(),
                            place: r.point(
                                item('location')('longitude'),
                                item('location')('latitude')).default(null)
                        }
                    })).run(conn)
            })
            .error(function (err) {
                config.logger.error(err);
            })
            .finally(function () {
                if (conn)
                    conn.close();
            });
    });

    module.exports = router;
})();
<commit_msg>adding logging statements.<commit_after>(function () {
    'use strict';

    var crypto = require('crypto');
    var bodyParser = require('body-parser');
    var r = require('rethinkdb');
    var config = require('../config');
    var router = require('express').Router();

    var lastUpdate = 0;

    router.get('/photo', function (req, res) {
        config.logger.info('GET request for /photo. query["hub.challenge"] = ' + req.query['hub.challenge']);

        if (req.query['hub.verify_token'] == config.instagram.verify) {
            res.send(req.query['hub.challenge']);
        } else {
            config.logger.error('token verification incorrect');
            res.status(500).json({err: 'Verify token incorrect'});
        }
    });

    router.use(bodyParser.json({
        verify: function (req, res, buf) {
            var hmac = crypto.createHmac('sha1', config.instagram.secret);
            var hash = hmac.update(buf).digest('hex');

            if (req.header('X-Hub-Signature') == hash) {
                req.validOrigin = true;
            }
        }
    }));

    router.post('/photo', function (req, res) {
        if (!req.validOrigin) {
            config.logger.error('Invalid signature in POST /api/instagram/photo');
            return res.status(500).json({err: 'Invalid signature'});
        }

        var update = req.body[0];
        res.json({success: true, kind: update.object});

        if (update.time - lastUpdate < 1) return;
        lastUpdate = update.time;

        config.logger.info('POST request for /photo. object updated: ' + update.object_id);

        var path = 'https://api.instagram.com/v1/tags/' + update.object_id + '/media/recent?client_id=' + config.instagram.client;

        var conn;
        r.connect(config.database)
            .then(function (c) {
                conn = c;
                return r.table('pictures')
                    .insert(r.http(path)('data').merge(function (item) {
                        return {
                            created_time: r.epochTime(item('created_time').coerceTo('number')),
                            time: r.now(),
                            place: r.point(
                                item('location')('longitude'),
                                item('location')('latitude')).default(null)
                        }
                    })).run(conn)
            })
            .error(function (err) {
                config.logger.error(err);
            })
            .finally(function () {
                if (conn)
                    conn.close();
            });
    });

    module.exports = router;
})();
<|endoftext|>