<commit_before>/*********
 *  Panel use to draw line and dotCharts
 *     LScatter is for graphs with a linear base-axis
 *
 *  The original ScatterChartPanel was difficult to generalize as
 *  many (scattered) changes were needed in the long create function.
 *     OScatter could be develofor graphs with a ordinal base-axis
 *
 *  Later we might consider to merge LScatter and OScatter again, and
 *  refactor the general stuff to an abstract base class.
 *********/

/*
 * Scatter chart panel. Base class for generating the other xy charts. Specific options are:
 * orientation - horizontal or vertical. Default: vertical
 * showDots - Show or hide dots. Default: true
 * showValues - Show or hide line value. Default: false
 * stacked -  Stacked? Default: false
 * panelSizeRatio - Ratio of the band occupied by the pane;. Default: 0.5 (50%)
 * lineSizeRatio - In multiple series, percentage of inner
 * band occupied by lines. Default: 0.5 (50%)
 * maxLineSize - Maximum size of a line in pixels. Default: 2000
 *
 * Has the following protovis extension points:
 *
 * chart_ - for the main chart Panel
 * line_ - for the actual line
 * linePanel_ - for the panel where the lines sit
 * lineDot_ - the dots on the line
 * lineLabel_ - for the main line label
 */

pvc.MetricScatterChartPanel = pvc.BasePanel.extend({

  _parent: null,
  pvLine: null,
  pvArea: null,
  pvDot: null,
  pvLabel: null,
  pvCategoryPanel: null,
  data: null,

  timeSeries: false,
  timeSeriesFormat: "%Y-%m-%d",

  stacked: false,
  showAreas: false,
  showLines: true,
  showDots: true,
  showValues: true,
  showTooltips: true,
  valuesAnchor: "right",
  orientation: "vertical",

  constructor: function(chart, options){

    this.base(chart,options);

  },

  prepareDataFunctions:  function() {
    /*
        This function implements a number of helper functions via
        closures. The helper functions are all stored in this.DF
        Overriding this function allows you to implement
        a different ScatterScart.
     */
    var myself = this;

    var baseScale = this.chart.getLinearBaseScale(true);
    var orthoScale = this.chart.getLinearScale(true);

    if(this.timeSeries){
      tScale = this.chart.getTimeseriesScale(true);
    }

    // create empty container for the functions and data
    myself.DF = {}

    myself.DF.baseValues = this.chart.dataEngine.getVisibleCategories();
    myself.DF.visibleSerieIds = this.chart.dataEngine.getVisibleSeriesIndexes()
//    myself.DF.data = this.chart.dataEngine.getVisibleTransposedValues();

    // calculate a position along the base-axis
    myself.DF.baseCalculation = (myself.timeSeries) ?
      function(d){ return tScale(parser.parse(d.category)); }   :
      function(d) { return baseScale(d.category);  };

    // calculate a position along the orthogonal axis
    myself.DF.orthoCalculation = function(d){
      return myself.chart.animate(0, orthoScale(d.value));
    };

    // get a data-series for the ID
    var pFunc = null;
    if (this.timeSeries) {
      var parser = pv.Format.date(this.timeSeriesFormat);
      pFunc = function(a,b){
        return parser.parse(a.category)-parser.parse(b.category);};
    }
    myself.DF.getDataForSerieId =
      function(d){ var res = myself.chart.dataEngine
            .getObjectsForSeriesIndex(d, pFunc);
            res.sort(function(a, b) {return a.category - b.category; })
            return res;
          };

    var colors = this.chart.colors(
         pv.range(this.chart.dataEngine.getSeriesSize()));
    myself.DF.colorFunc = function(d){
      // return colors(d.serieIndex)
      return colors(myself.chart.dataEngine.getVisibleSeriesIndexes()
              [this.parent.index])
    };

  },

  create: function(){

    var myself = this;
    this.width = this._parent.width;
    this.height = this._parent.height;

    this.pvPanel = this._parent.getPvPanel().add(this.type)
    .width(this.width)
    .height(this.height);

    // add clipping for bounds
    if  (   (myself.chart.options.orthoFixedMin != null)
         || (myself.chart.options.orthoFixedMax != null) )
      this.pvPanel["overflow"]("hidden");

    if(this.showTooltips || this.chart.options.clickable ){
      this.pvPanel
      .events("all")
      .event("mousemove", pv.Behavior.point(Infinity));
    }

    var anchor = this.orientation == "vertical"?"bottom":"left";

    // prepare data and functions when creating (rendering) the chart.
    this.prepareDataFunctions();

    var maxLineSize;

    // Stacked?
    if (this.stacked){

      pvc.log("WARNING: the stacked option of metric charts still needs to be implemented.");

/*    CvK:  have to rewrite this code
      this.pvScatterPanel = this.pvPanel.add(pv.Layout.Stack)
      .layers(pvc.padMatrixWithZeros(this.chart.dataEngine.getVisibleTransposedValues()))
      [this.orientation == "vertical"?"x":"y"](function(){
        if(myself.timeSeries){
          return tScale(parser.parse(myself.chart.dataEngine.getCategoryByIndex(this.index)));
        }
        else{
          return oScale(myself.chart.dataEngine.getCategoryByIndex(this.index)) + oScale.range().band/2;
        }
      })
      [anchor](lScale(0))
      [this.orientation == "vertical"?"y":"x"](function(d){
        return myself.chart.animate(0,lScale(d)-lScale(0));
      })

      this.pvArea = this.pvScatterPanel.layer.add(pv.Area)
      .fillStyle(this.showAreas?colorFunc:null);

      this.pvLine = this.pvArea.anchor(pvc.BasePanel.oppositeAnchor[anchor]).add(pv.Line)
      .lineWidth(this.showLines?1.5:0.001);
    //[pvc.BasePanel.parallelLength[anchor]](maxLineSize)
    */
    }
    else {

      // Add the serie identifiers to the scatterPanel
      // CvK: Why do we need a new pvPanel and can't we use existing pvPanel?
      this.pvScatterPanel = this.pvPanel.add(pv.Panel)
           .data(myself.DF.visibleSerieIds);

      // add the area's
      // CvK: why adding area's if showArea
      this.pvArea = this.pvScatterPanel.add(pv.Area)
        .fillStyle(this.showAreas?myself.DF.colorFunc:null);

      var lineWidth = this.showLines ? 1.5 : 0.001;
      // add line and make lines invisible if not needed.
      this.pvLine = this.pvArea.add(pv.Line)
      .data(myself.DF.getDataForSerieId)
      .lineWidth(lineWidth)
      [pvc.BasePanel.relativeAnchor[anchor]](myself.DF.baseCalculation)
      [anchor](myself.DF.orthoCalculation)
    }

    this.pvLine
      .strokeStyle(myself.DF.colorFunc)
      .text(function(d){
        var v, c;
        var s = myself.chart.dataEngine.getVisibleSeries()[this.parent.index]
        if( typeof d == "object"){
          v = d.value;
          c = d.category
        }
        else{
          v = d
          c = myself.chart.dataEngine.getVisibleCategories()[this.index]
        };
        return myself.chart.options.tooltipFormat.call(myself,s,c,v);
      })

    if(this.showTooltips){
      this.extend(this.chart.tipsySettings,"tooltip_");
      this.pvLine
        .event("point", pv.Behavior.tipsy(this.chart.tipsySettings));
    }

    this.pvDot = this.pvLine.add(pv.Dot)
    .shapeSize(12)
    .lineWidth(1.5)
    .strokeStyle(this.showDots?myself.DF.colorFunc:null)
    .fillStyle(this.showDots?myself.DF.colorFunc:null)

    if (this.chart.options.clickable){
      this.pvDot
      .cursor("pointer")
      .event("click",function(d){
        var v, c, e;
        var s = myself.chart.dataEngine.getSeries()[this.parent.index]
        if( typeof d == "object"){
          v = d.value;
          c = d.category
        }
        else{
          v = d
          c = myself.chart.dataEngine.getCategories()[this.index]
        }
        e = arguments[arguments.length-1];
        return myself.chart.options.clickAction(s, c, v, e);
      });
    }

    if(this.showValues){
      this.pvLabel = this.pvDot
      .anchor(this.valuesAnchor)
      .add(pv.Label)
      .bottom(0)
      .text(function(d){
        return myself.chart.options.valueFormat(typeof d == "object"?d.value:d)
      })

      // Extend lineLabel
      this.extend(this.pvLabel,"lineLabel_");
    }

    // Extend line and linePanel
    this.extend(this.pvScatterPanel,"scatterPanel_");
    this.extend(this.pvArea,"area_");
    this.extend(this.pvLine,"line_");
    this.extend(this.pvDot,"dot_");
    this.extend(this.pvLabel,"label_");

    // Extend body
    this.extend(this.pvPanel,"chart_");

  }

});

<commit_msg>solved dangling variable reference<commit_after>/*********
 *  Panel use to draw line and dotCharts
 *     LScatter is for graphs with a linear base-axis
 *
 *  The original ScatterChartPanel was difficult to generalize as
 *  many (scattered) changes were needed in the long create function.
 *     OScatter could be develofor graphs with a ordinal base-axis
 *
 *  Later we might consider to merge LScatter and OScatter again, and
 *  refactor the general stuff to an abstract base class.
 *********/

/*
 * Scatter chart panel. Base class for generating the other xy charts. Specific options are:
 * orientation - horizontal or vertical. Default: vertical
 * showDots - Show or hide dots. Default: true
 * showValues - Show or hide line value. Default: false
 * stacked -  Stacked? Default: false
 * panelSizeRatio - Ratio of the band occupied by the pane;. Default: 0.5 (50%)
 * lineSizeRatio - In multiple series, percentage of inner
 * band occupied by lines. Default: 0.5 (50%)
 * maxLineSize - Maximum size of a line in pixels. Default: 2000
 *
 * Has the following protovis extension points:
 *
 * chart_ - for the main chart Panel
 * line_ - for the actual line
 * linePanel_ - for the panel where the lines sit
 * lineDot_ - the dots on the line
 * lineLabel_ - for the main line label
 */

pvc.MetricScatterChartPanel = pvc.BasePanel.extend({

  _parent: null,
  pvLine: null,
  pvArea: null,
  pvDot: null,
  pvLabel: null,
  pvCategoryPanel: null,
  data: null,

  timeSeries: false,
  timeSeriesFormat: "%Y-%m-%d",

  stacked: false,
  showAreas: false,
  showLines: true,
  showDots: true,
  showValues: true,
  showTooltips: true,
  valuesAnchor: "right",
  orientation: "vertical",

  constructor: function(chart, options){

    this.base(chart,options);

  },

  prepareDataFunctions:  function() {
    /*
        This function implements a number of helper functions via
        closures. The helper functions are all stored in this.DF
        Overriding this function allows you to implement
        a different ScatterScart.
     */
    var myself = this;

    var baseScale = this.chart.getLinearBaseScale(true);
    var orthoScale = this.chart.getLinearScale(true);
	var tScale;
    if(this.timeSeries){
      tScale = this.chart.getTimeseriesScale(true);
    }

    // create empty container for the functions and data
    myself.DF = {}

    myself.DF.baseValues = this.chart.dataEngine.getVisibleCategories();
    myself.DF.visibleSerieIds = this.chart.dataEngine.getVisibleSeriesIndexes()
//    myself.DF.data = this.chart.dataEngine.getVisibleTransposedValues();

    // calculate a position along the base-axis
    myself.DF.baseCalculation = (myself.timeSeries) ?
      function(d){ return tScale(parser.parse(d.category)); }   :
      function(d) { return baseScale(d.category);  };

    // calculate a position along the orthogonal axis
    myself.DF.orthoCalculation = function(d){
      return myself.chart.animate(0, orthoScale(d.value));
    };

    // get a data-series for the ID
    var pFunc = null;
    if (this.timeSeries) {
      var parser = pv.Format.date(this.timeSeriesFormat);
      pFunc = function(a,b){
        return parser.parse(a.category)-parser.parse(b.category);};
    }
    myself.DF.getDataForSerieId =
      function(d){ var res = myself.chart.dataEngine
            .getObjectsForSeriesIndex(d, pFunc);
            res.sort(function(a, b) {return a.category - b.category; })
            return res;
          };

    var colors = this.chart.colors(
         pv.range(this.chart.dataEngine.getSeriesSize()));
    myself.DF.colorFunc = function(d){
      // return colors(d.serieIndex)
      return colors(myself.chart.dataEngine.getVisibleSeriesIndexes()
              [this.parent.index])
    };

  },

  create: function(){

    var myself = this;
    this.width = this._parent.width;
    this.height = this._parent.height;

    this.pvPanel = this._parent.getPvPanel().add(this.type)
    .width(this.width)
    .height(this.height);

    // add clipping for bounds
    if  (   (myself.chart.options.orthoFixedMin != null)
         || (myself.chart.options.orthoFixedMax != null) )
      this.pvPanel["overflow"]("hidden");

    if(this.showTooltips || this.chart.options.clickable ){
      this.pvPanel
      .events("all")
      .event("mousemove", pv.Behavior.point(Infinity));
    }

    var anchor = this.orientation == "vertical"?"bottom":"left";

    // prepare data and functions when creating (rendering) the chart.
    this.prepareDataFunctions();

    var maxLineSize;

    // Stacked?
    if (this.stacked){

      pvc.log("WARNING: the stacked option of metric charts still needs to be implemented.");

/*    CvK:  have to rewrite this code
      this.pvScatterPanel = this.pvPanel.add(pv.Layout.Stack)
      .layers(pvc.padMatrixWithZeros(this.chart.dataEngine.getVisibleTransposedValues()))
      [this.orientation == "vertical"?"x":"y"](function(){
        if(myself.timeSeries){
          return tScale(parser.parse(myself.chart.dataEngine.getCategoryByIndex(this.index)));
        }
        else{
          return oScale(myself.chart.dataEngine.getCategoryByIndex(this.index)) + oScale.range().band/2;
        }
      })
      [anchor](lScale(0))
      [this.orientation == "vertical"?"y":"x"](function(d){
        return myself.chart.animate(0,lScale(d)-lScale(0));
      })

      this.pvArea = this.pvScatterPanel.layer.add(pv.Area)
      .fillStyle(this.showAreas?colorFunc:null);

      this.pvLine = this.pvArea.anchor(pvc.BasePanel.oppositeAnchor[anchor]).add(pv.Line)
      .lineWidth(this.showLines?1.5:0.001);
    //[pvc.BasePanel.parallelLength[anchor]](maxLineSize)
    */
    }
    else {

      // Add the serie identifiers to the scatterPanel
      // CvK: Why do we need a new pvPanel and can't we use existing pvPanel?
      this.pvScatterPanel = this.pvPanel.add(pv.Panel)
           .data(myself.DF.visibleSerieIds);

      // add the area's
      // CvK: why adding area's if showArea
      this.pvArea = this.pvScatterPanel.add(pv.Area)
        .fillStyle(this.showAreas?myself.DF.colorFunc:null);

      var lineWidth = this.showLines ? 1.5 : 0.001;
      // add line and make lines invisible if not needed.
      this.pvLine = this.pvArea.add(pv.Line)
      .data(myself.DF.getDataForSerieId)
      .lineWidth(lineWidth)
      [pvc.BasePanel.relativeAnchor[anchor]](myself.DF.baseCalculation)
      [anchor](myself.DF.orthoCalculation)
    }

    this.pvLine
      .strokeStyle(myself.DF.colorFunc)
      .text(function(d){
        var v, c;
        var s = myself.chart.dataEngine.getVisibleSeries()[this.parent.index]
        if( typeof d == "object"){
          v = d.value;
          c = d.category
        }
        else{
          v = d
          c = myself.chart.dataEngine.getVisibleCategories()[this.index]
        };
        return myself.chart.options.tooltipFormat.call(myself,s,c,v);
      })

    if(this.showTooltips){
      this.extend(this.chart.tipsySettings,"tooltip_");
      this.pvLine
        .event("point", pv.Behavior.tipsy(this.chart.tipsySettings));
    }

    this.pvDot = this.pvLine.add(pv.Dot)
    .shapeSize(12)
    .lineWidth(1.5)
    .strokeStyle(this.showDots?myself.DF.colorFunc:null)
    .fillStyle(this.showDots?myself.DF.colorFunc:null)

    if (this.chart.options.clickable){
      this.pvDot
      .cursor("pointer")
      .event("click",function(d){
        var v, c, e;
        var s = myself.chart.dataEngine.getSeries()[this.parent.index]
        if( typeof d == "object"){
          v = d.value;
          c = d.category
        }
        else{
          v = d
          c = myself.chart.dataEngine.getCategories()[this.index]
        }
        e = arguments[arguments.length-1];
        return myself.chart.options.clickAction(s, c, v, e);
      });
    }

    if(this.showValues){
      this.pvLabel = this.pvDot
      .anchor(this.valuesAnchor)
      .add(pv.Label)
      .bottom(0)
      .text(function(d){
        return myself.chart.options.valueFormat(typeof d == "object"?d.value:d)
      })

      // Extend lineLabel
      this.extend(this.pvLabel,"lineLabel_");
    }

    // Extend line and linePanel
    this.extend(this.pvScatterPanel,"scatterPanel_");
    this.extend(this.pvArea,"area_");
    this.extend(this.pvLine,"line_");
    this.extend(this.pvDot,"dot_");
    this.extend(this.pvLabel,"label_");

    // Extend body
    this.extend(this.pvPanel,"chart_");

  }

});

<|endoftext|>


<commit_before>import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import wait from 'ember-test-helpers/wait';
import windowProxy from 'ghost-admin/utils/window-proxy';
import {Response} from 'ember-cli-mirage';
import {afterEach, beforeEach, describe, it} from 'mocha';
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
import {blur, click, currentRouteName, currentURL, fillIn, find, findAll} from '@ember/test-helpers';
import {errorOverride, errorReset} from 'ghost-admin/tests/helpers/adapter-error';
import {expect} from 'chai';
import {run} from '@ember/runloop';
import {setupApplicationTest} from 'ember-mocha';
import {timeout} from 'ember-concurrency';
import {visit} from '../../helpers/visit';

// Grabbed from keymaster's testing code because Ember's `keyEvent` helper
// is for some reason not triggering the events in a way that keymaster detects:
// https://github.com/madrobby/keymaster/blob/master/test/keymaster.html#L31
const modifierMap = {
    16: 'shiftKey',
    18: 'altKey',
    17: 'ctrlKey',
    91: 'metaKey'
};
let keydown = function (code, modifiers, el) {
    let event = document.createEvent('Event');
    event.initEvent('keydown', true, true);
    event.keyCode = code;
    if (modifiers && modifiers.length > 0) {
        for (let i in modifiers) {
            event[modifierMap[modifiers[i]]] = true;
        }
    }
    (el || document).dispatchEvent(event);
};
let keyup = function (code, el) {
    let event = document.createEvent('Event');
    event.initEvent('keyup', true, true);
    event.keyCode = code;
    (el || document).dispatchEvent(event);
};

describe('Acceptance: Settings - Tags', function () {
    let hooks = setupApplicationTest();
    setupMirage(hooks);

    it('redirects to signin when not authenticated', async function () {
        await invalidateSession();
        await visit('/settings/tags');

        expect(currentURL()).to.equal('/signin');
    });

    it('redirects to staff page when authenticated as contributor', async function () {
        let role = this.server.create('role', {name: 'Contributor'});
        this.server.create('user', {roles: [role], slug: 'test-user'});

        await authenticateSession();
        await visit('/settings/design');

        expect(currentURL(), 'currentURL').to.equal('/staff/test-user');
    });

    it('redirects to staff page when authenticated as author', async function () {
        let role = this.server.create('role', {name: 'Author'});
        this.server.create('user', {roles: [role], slug: 'test-user'});

        await authenticateSession();
        await visit('/settings/design');

        expect(currentURL(), 'currentURL').to.equal('/staff/test-user');
    });

    describe('when logged in', function () {
        let newLocation, originalReplaceState;

        beforeEach(async function () {
            let role = this.server.create('role', {name: 'Administrator'});
            this.server.create('user', {roles: [role]});

            originalReplaceState = windowProxy.replaceState;
            windowProxy.replaceState = function (params, title, url) {
                newLocation = url;
            };
            newLocation = undefined;

            return await authenticateSession();
        });

        afterEach(function () {
            windowProxy.replaceState = originalReplaceState;
        });

        it('it renders, can be navigated, can edit, create & delete tags', async function () {
            let tag1 = this.server.create('tag');
            let tag2 = this.server.create('tag');

            await visit('/settings/tags');

            // second wait is needed for the vertical-collection to settle
            await wait();

            // it redirects to first tag
            expect(currentURL(), 'currentURL').to.equal(`/settings/tags/${tag1.slug}`);

            // it has correct page title
            expect(document.title, 'page title').to.equal('Settings - Tags - Test Blog');

            // it highlights nav menu
            expect(find('[data-test-nav="tags"]'), 'highlights nav menu item')
                .to.have.class('active');

            // it lists all tags
            expect(findAll('.settings-tags .settings-tag').length, 'tag list count')
                .to.equal(2);
            let tag = find('.settings-tags .settings-tag');
            expect(tag.querySelector('.tag-title').textContent, 'tag list item title')
                .to.equal(tag1.name);

            // it highlights selected tag
            expect(find(`a[href="/ghost/settings/tags/${tag1.slug}"]`), 'highlights selected tag')
                .to.have.class('active');

            // it shows selected tag form
            expect(find('.tag-settings-pane h4').textContent, 'settings pane title')
                .to.equal('Tag settings');
            expect(find('.tag-settings-pane input[name="name"]').value, 'loads correct tag into form')
                .to.equal(tag1.name);

            // click the second tag in the list
            let tagEditButtons = findAll('.tag-edit-button');
            await click(tagEditButtons[tagEditButtons.length - 1]);

            // it navigates to selected tag
            expect(currentURL(), 'url after clicking tag').to.equal(`/settings/tags/${tag2.slug}`);

            // it highlights selected tag
            expect(find(`a[href="/ghost/settings/tags/${tag2.slug}"]`), 'highlights selected tag')
                .to.have.class('active');

            // it shows selected tag form
            expect(find('.tag-settings-pane input[name="name"]').value, 'loads correct tag into form')
                .to.equal(tag2.name);

            // simulate up arrow press
            run(() => {
                keydown(38);
                keyup(38);
            });

            await wait();

            // it navigates to previous tag
            expect(currentURL(), 'url after keyboard up arrow').to.equal(`/settings/tags/${tag1.slug}`);

            // it highlights selected tag
            expect(find(`a[href="/ghost/settings/tags/${tag1.slug}"]`), 'selects previous tag')
                .to.have.class('active');

            // simulate down arrow press
            run(() => {
                keydown(40);
                keyup(40);
            });

            await wait();

            // it navigates to previous tag
            expect(currentURL(), 'url after keyboard down arrow').to.equal(`/settings/tags/${tag2.slug}`);

            // it highlights selected tag
            expect(find(`a[href="/ghost/settings/tags/${tag2.slug}"]`), 'selects next tag')
                .to.have.class('active');

            // trigger save
            await fillIn('.tag-settings-pane input[name="name"]', 'New Name');
            await blur('.tag-settings-pane input[name="name"]');

            // extra timeout needed for Travis - sometimes it doesn't update
            // quick enough and an extra wait() call doesn't help
            await timeout(100);

            // check we update with the data returned from the server
            let tags = findAll('.settings-tags .settings-tag');
            tag = tags[0];
            expect(tag.querySelector('.tag-title').textContent, 'tag list updates on save')
                .to.equal('New Name');
            expect(find('.tag-settings-pane input[name="name"]').value, 'settings form updates on save')
                .to.equal('New Name');

            // start new tag
            await click('.view-actions .gh-btn-green');

            // it navigates to the new tag route
            expect(currentURL(), 'new tag URL').to.equal('/settings/tags/new');

            // it displays the new tag form
            expect(find('.tag-settings-pane h4').textContent, 'settings pane title')
                .to.equal('New Tag');

            // all fields start blank
            findAll('.tag-settings-pane input, .tag-settings-pane textarea').forEach(function (elem) {
                expect(elem.value, `input field for ${elem.getAttribute('name')}`)
                    .to.be.empty;
            });

            // save new tag
            await fillIn('.tag-settings-pane input[name="name"]', 'New Tag');
            await blur('.tag-settings-pane input[name="name"]');

            // extra timeout needed for FF on Linux - sometimes it doesn't update
            // quick enough, especially on Travis, and an extra wait() call
            // doesn't help
            await timeout(100);

            // it redirects to the new tag's URL
            expect(currentURL(), 'URL after tag creation').to.equal('/settings/tags/new-tag');

            // it adds the tag to the list and selects
            tags = findAll('.settings-tags .settings-tag');
            tag = tags[1]; // second tag in list due to alphabetical ordering
            expect(tags.length, 'tag list count after creation')
                .to.equal(3);

            // new tag will be second in the list due to alphabetical sorting
            expect(findAll('.settings-tags .settings-tag')[1].querySelector('.tag-title').textContent.trim(), 'new tag list item title');
            expect(tag.querySelector('.tag-title').textContent, 'new tag list item title')
                .to.equal('New Tag');
            expect(find('a[href="/ghost/settings/tags/new-tag"]'), 'highlights new tag')
                .to.have.class('active');

            // delete tag
            await click('.settings-menu-delete-button');
            await click('.fullscreen-modal .gh-btn-red');

            // it redirects to the first tag
            expect(currentURL(), 'URL after tag deletion').to.equal(`/settings/tags/${tag1.slug}`);

            // it removes the tag from the list
            expect(findAll('.settings-tags .settings-tag').length, 'tag list count after deletion')
                .to.equal(2);
        });

        // TODO: Unskip and fix
        // skipped because it was failing most of the time on Travis
        // see https://github.com/TryGhost/Ghost/issues/8805
        it.skip('loads tag via slug when accessed directly', async function () {
            this.server.createList('tag', 2);

            await visit('/settings/tags/tag-1');

            // second wait is needed for the vertical-collection to settle
            await wait();

            expect(currentURL(), 'URL after direct load').to.equal('/settings/tags/tag-1');

            // it loads all other tags
            expect(findAll('.settings-tags .settings-tag').length, 'tag list count after direct load')
                .to.equal(2);

            // selects tag in list
            expect(find('a[href="/ghost/settings/tags/tag-1"]').classList.contains('active'), 'highlights requested tag')
                .to.be.true;

            // shows requested tag in settings pane
            expect(find('.tag-settings-pane input[name="name"]').value, 'loads correct tag into form')
                .to.equal('Tag 1');
        });

        it('shows the internal tag label', async function () {
            this.server.create('tag', {name: '#internal-tag', slug: 'hash-internal-tag', visibility: 'internal'});

            await visit('settings/tags/');

            // second wait is needed for the vertical-collection to settle
            await wait();

            expect(currentURL()).to.equal('/settings/tags/hash-internal-tag');

            expect(findAll('.settings-tags .settings-tag').length, 'tag list count')
                .to.equal(1);

            let tag = find('.settings-tags .settings-tag');

            expect(tag.querySelectorAll('.label.label-blue').length, 'internal tag label')
                .to.equal(1);

            expect(tag.querySelector('.label.label-blue').textContent.trim(), 'internal tag label text')
                .to.equal('internal');
        });

        it('updates the URL when slug changes', async function () {
            this.server.createList('tag', 2);

            await visit('/settings/tags/tag-1');

            // second wait is needed for the vertical-collection to settle
            await wait();

            expect(currentURL(), 'URL after direct load').to.equal('/settings/tags/tag-1');

            // update the slug
            await fillIn('.tag-settings-pane input[name="slug"]', 'test');
            await blur('.tag-settings-pane input[name="slug"]');

            // tests don't have a location.hash so we can only check that the
            // slug portion is updated correctly
            expect(newLocation, 'URL after slug change').to.equal('test');
        });

        it('redirects to 404 when tag does not exist', async function () {
            this.server.get('/tags/slug/unknown/', function () {
                return new Response(404, {'Content-Type': 'application/json'}, {errors: [{message: 'Tag not found.', errorType: 'NotFoundError'}]});
            });

            errorOverride();

            await visit('settings/tags/unknown');

            errorReset();
            expect(currentRouteName()).to.equal('error404');
            expect(currentURL()).to.equal('/settings/tags/unknown');
        });

        it('sorts tags correctly', async function () {
            this.server.create('tag', {name: 'B - Third', slug: 'third'});
            this.server.create('tag', {name: 'Z - Last', slug: 'last'});
            this.server.create('tag', {name: '#A - Second', slug: 'second'});
            this.server.create('tag', {name: 'A - First', slug: 'first'});

            await visit('settings/tags');

            // second wait is needed for the vertical-collection to settle
            await wait();

            let tags = findAll('[data-test-tag]');

            expect(tags[0].querySelector('[data-test-name]').textContent.trim()).to.equal('A - First');
            expect(tags[1].querySelector('[data-test-name]').textContent.trim()).to.equal('#A - Second');
            expect(tags[2].querySelector('[data-test-name]').textContent.trim()).to.equal('B - Third');
            expect(tags[3].querySelector('[data-test-name]').textContent.trim()).to.equal('Z - Last');
        });
    });
});
<commit_msg>Fixed tests for tags case changes<commit_after>import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import wait from 'ember-test-helpers/wait';
import windowProxy from 'ghost-admin/utils/window-proxy';
import {Response} from 'ember-cli-mirage';
import {afterEach, beforeEach, describe, it} from 'mocha';
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
import {blur, click, currentRouteName, currentURL, fillIn, find, findAll} from '@ember/test-helpers';
import {errorOverride, errorReset} from 'ghost-admin/tests/helpers/adapter-error';
import {expect} from 'chai';
import {run} from '@ember/runloop';
import {setupApplicationTest} from 'ember-mocha';
import {timeout} from 'ember-concurrency';
import {visit} from '../../helpers/visit';

// Grabbed from keymaster's testing code because Ember's `keyEvent` helper
// is for some reason not triggering the events in a way that keymaster detects:
// https://github.com/madrobby/keymaster/blob/master/test/keymaster.html#L31
const modifierMap = {
    16: 'shiftKey',
    18: 'altKey',
    17: 'ctrlKey',
    91: 'metaKey'
};
let keydown = function (code, modifiers, el) {
    let event = document.createEvent('Event');
    event.initEvent('keydown', true, true);
    event.keyCode = code;
    if (modifiers && modifiers.length > 0) {
        for (let i in modifiers) {
            event[modifierMap[modifiers[i]]] = true;
        }
    }
    (el || document).dispatchEvent(event);
};
let keyup = function (code, el) {
    let event = document.createEvent('Event');
    event.initEvent('keyup', true, true);
    event.keyCode = code;
    (el || document).dispatchEvent(event);
};

describe('Acceptance: Settings - Tags', function () {
    let hooks = setupApplicationTest();
    setupMirage(hooks);

    it('redirects to signin when not authenticated', async function () {
        await invalidateSession();
        await visit('/settings/tags');

        expect(currentURL()).to.equal('/signin');
    });

    it('redirects to staff page when authenticated as contributor', async function () {
        let role = this.server.create('role', {name: 'Contributor'});
        this.server.create('user', {roles: [role], slug: 'test-user'});

        await authenticateSession();
        await visit('/settings/design');

        expect(currentURL(), 'currentURL').to.equal('/staff/test-user');
    });

    it('redirects to staff page when authenticated as author', async function () {
        let role = this.server.create('role', {name: 'Author'});
        this.server.create('user', {roles: [role], slug: 'test-user'});

        await authenticateSession();
        await visit('/settings/design');

        expect(currentURL(), 'currentURL').to.equal('/staff/test-user');
    });

    describe('when logged in', function () {
        let newLocation, originalReplaceState;

        beforeEach(async function () {
            let role = this.server.create('role', {name: 'Administrator'});
            this.server.create('user', {roles: [role]});

            originalReplaceState = windowProxy.replaceState;
            windowProxy.replaceState = function (params, title, url) {
                newLocation = url;
            };
            newLocation = undefined;

            return await authenticateSession();
        });

        afterEach(function () {
            windowProxy.replaceState = originalReplaceState;
        });

        it('it renders, can be navigated, can edit, create & delete tags', async function () {
            let tag1 = this.server.create('tag');
            let tag2 = this.server.create('tag');

            await visit('/settings/tags');

            // second wait is needed for the vertical-collection to settle
            await wait();

            // it redirects to first tag
            expect(currentURL(), 'currentURL').to.equal(`/settings/tags/${tag1.slug}`);

            // it has correct page title
            expect(document.title, 'page title').to.equal('Settings - Tags - Test Blog');

            // it highlights nav menu
            expect(find('[data-test-nav="tags"]'), 'highlights nav menu item')
                .to.have.class('active');

            // it lists all tags
            expect(findAll('.settings-tags .settings-tag').length, 'tag list count')
                .to.equal(2);
            let tag = find('.settings-tags .settings-tag');
            expect(tag.querySelector('.tag-title').textContent, 'tag list item title')
                .to.equal(tag1.name);

            // it highlights selected tag
            expect(find(`a[href="/ghost/settings/tags/${tag1.slug}"]`), 'highlights selected tag')
                .to.have.class('active');

            // it shows selected tag form
            expect(find('.tag-settings-pane h4').textContent, 'settings pane title')
                .to.equal('Tag settings');
            expect(find('.tag-settings-pane input[name="name"]').value, 'loads correct tag into form')
                .to.equal(tag1.name);

            // click the second tag in the list
            let tagEditButtons = findAll('.tag-edit-button');
            await click(tagEditButtons[tagEditButtons.length - 1]);

            // it navigates to selected tag
            expect(currentURL(), 'url after clicking tag').to.equal(`/settings/tags/${tag2.slug}`);

            // it highlights selected tag
            expect(find(`a[href="/ghost/settings/tags/${tag2.slug}"]`), 'highlights selected tag')
                .to.have.class('active');

            // it shows selected tag form
            expect(find('.tag-settings-pane input[name="name"]').value, 'loads correct tag into form')
                .to.equal(tag2.name);

            // simulate up arrow press
            run(() => {
                keydown(38);
                keyup(38);
            });

            await wait();

            // it navigates to previous tag
            expect(currentURL(), 'url after keyboard up arrow').to.equal(`/settings/tags/${tag1.slug}`);

            // it highlights selected tag
            expect(find(`a[href="/ghost/settings/tags/${tag1.slug}"]`), 'selects previous tag')
                .to.have.class('active');

            // simulate down arrow press
            run(() => {
                keydown(40);
                keyup(40);
            });

            await wait();

            // it navigates to previous tag
            expect(currentURL(), 'url after keyboard down arrow').to.equal(`/settings/tags/${tag2.slug}`);

            // it highlights selected tag
            expect(find(`a[href="/ghost/settings/tags/${tag2.slug}"]`), 'selects next tag')
                .to.have.class('active');

            // trigger save
            await fillIn('.tag-settings-pane input[name="name"]', 'New Name');
            await blur('.tag-settings-pane input[name="name"]');

            // extra timeout needed for Travis - sometimes it doesn't update
            // quick enough and an extra wait() call doesn't help
            await timeout(100);

            // check we update with the data returned from the server
            let tags = findAll('.settings-tags .settings-tag');
            tag = tags[0];
            expect(tag.querySelector('.tag-title').textContent, 'tag list updates on save')
                .to.equal('New Name');
            expect(find('.tag-settings-pane input[name="name"]').value, 'settings form updates on save')
                .to.equal('New Name');

            // start new tag
            await click('.view-actions .gh-btn-green');

            // it navigates to the new tag route
            expect(currentURL(), 'new tag URL').to.equal('/settings/tags/new');

            // it displays the new tag form
            expect(find('.tag-settings-pane h4').textContent, 'settings pane title')
                .to.equal('New tag');

            // all fields start blank
            findAll('.tag-settings-pane input, .tag-settings-pane textarea').forEach(function (elem) {
                expect(elem.value, `input field for ${elem.getAttribute('name')}`)
                    .to.be.empty;
            });

            // save new tag
            await fillIn('.tag-settings-pane input[name="name"]', 'New tag');
            await blur('.tag-settings-pane input[name="name"]');

            // extra timeout needed for FF on Linux - sometimes it doesn't update
            // quick enough, especially on Travis, and an extra wait() call
            // doesn't help
            await timeout(100);

            // it redirects to the new tag's URL
            expect(currentURL(), 'URL after tag creation').to.equal('/settings/tags/new-tag');

            // it adds the tag to the list and selects
            tags = findAll('.settings-tags .settings-tag');
            tag = tags[1]; // second tag in list due to alphabetical ordering
            expect(tags.length, 'tag list count after creation')
                .to.equal(3);

            // new tag will be second in the list due to alphabetical sorting
            expect(findAll('.settings-tags .settings-tag')[1].querySelector('.tag-title').textContent.trim(), 'new tag list item title');
            expect(tag.querySelector('.tag-title').textContent, 'new tag list item title')
                .to.equal('New tag');
            expect(find('a[href="/ghost/settings/tags/new-tag"]'), 'highlights new tag')
                .to.have.class('active');

            // delete tag
            await click('.settings-menu-delete-button');
            await click('.fullscreen-modal .gh-btn-red');

            // it redirects to the first tag
            expect(currentURL(), 'URL after tag deletion').to.equal(`/settings/tags/${tag1.slug}`);

            // it removes the tag from the list
            expect(findAll('.settings-tags .settings-tag').length, 'tag list count after deletion')
                .to.equal(2);
        });

        // TODO: Unskip and fix
        // skipped because it was failing most of the time on Travis
        // see https://github.com/TryGhost/Ghost/issues/8805
        it.skip('loads tag via slug when accessed directly', async function () {
            this.server.createList('tag', 2);

            await visit('/settings/tags/tag-1');

            // second wait is needed for the vertical-collection to settle
            await wait();

            expect(currentURL(), 'URL after direct load').to.equal('/settings/tags/tag-1');

            // it loads all other tags
            expect(findAll('.settings-tags .settings-tag').length, 'tag list count after direct load')
                .to.equal(2);

            // selects tag in list
            expect(find('a[href="/ghost/settings/tags/tag-1"]').classList.contains('active'), 'highlights requested tag')
                .to.be.true;

            // shows requested tag in settings pane
            expect(find('.tag-settings-pane input[name="name"]').value, 'loads correct tag into form')
                .to.equal('Tag 1');
        });

        it('shows the internal tag label', async function () {
            this.server.create('tag', {name: '#internal-tag', slug: 'hash-internal-tag', visibility: 'internal'});

            await visit('settings/tags/');

            // second wait is needed for the vertical-collection to settle
            await wait();

            expect(currentURL()).to.equal('/settings/tags/hash-internal-tag');

            expect(findAll('.settings-tags .settings-tag').length, 'tag list count')
                .to.equal(1);

            let tag = find('.settings-tags .settings-tag');

            expect(tag.querySelectorAll('.label.label-blue').length, 'internal tag label')
                .to.equal(1);

            expect(tag.querySelector('.label.label-blue').textContent.trim(), 'internal tag label text')
                .to.equal('internal');
        });

        it('updates the URL when slug changes', async function () {
            this.server.createList('tag', 2);

            await visit('/settings/tags/tag-1');

            // second wait is needed for the vertical-collection to settle
            await wait();

            expect(currentURL(), 'URL after direct load').to.equal('/settings/tags/tag-1');

            // update the slug
            await fillIn('.tag-settings-pane input[name="slug"]', 'test');
            await blur('.tag-settings-pane input[name="slug"]');

            // tests don't have a location.hash so we can only check that the
            // slug portion is updated correctly
            expect(newLocation, 'URL after slug change').to.equal('test');
        });

        it('redirects to 404 when tag does not exist', async function () {
            this.server.get('/tags/slug/unknown/', function () {
                return new Response(404, {'Content-Type': 'application/json'}, {errors: [{message: 'Tag not found.', errorType: 'NotFoundError'}]});
            });

            errorOverride();

            await visit('settings/tags/unknown');

            errorReset();
            expect(currentRouteName()).to.equal('error404');
            expect(currentURL()).to.equal('/settings/tags/unknown');
        });

        it('sorts tags correctly', async function () {
            this.server.create('tag', {name: 'B - Third', slug: 'third'});
            this.server.create('tag', {name: 'Z - Last', slug: 'last'});
            this.server.create('tag', {name: '#A - Second', slug: 'second'});
            this.server.create('tag', {name: 'A - First', slug: 'first'});

            await visit('settings/tags');

            // second wait is needed for the vertical-collection to settle
            await wait();

            let tags = findAll('[data-test-tag]');

            expect(tags[0].querySelector('[data-test-name]').textContent.trim()).to.equal('A - First');
            expect(tags[1].querySelector('[data-test-name]').textContent.trim()).to.equal('#A - Second');
            expect(tags[2].querySelector('[data-test-name]').textContent.trim()).to.equal('B - Third');
            expect(tags[3].querySelector('[data-test-name]').textContent.trim()).to.equal('Z - Last');
        });
    });
});
<|endoftext|>


<commit_before>'use strict';

const Arangojs = require('arangojs');
const util = require('util');
const slug = require('slug');
const _ = require('lodash');
const qb = require('aqb');

const DB_SYSTEM = '_system';

function* query(db, collection, q, bind) {
  try {
    return yield db.query(q, bind);
  } catch (err) {
    if (err.message && err.message.startsWith('collection not found')) {
      const c = db.collection(collection);
      yield c.create();
    } else {
      throw err;
    }
  }
  return yield db.query(q, bind);
}

function idToKey(id) {
  return slug(id, {
    replacement: '_'
  });
}

function ensureKey(document) {
  const doc = _.clone(document);
  if (_.has(doc, '_key')) {
    return doc;
  }
  const id = doc.id;
  if (id) {
    _.set(doc, '_key', idToKey(id));
  }
  return doc;
}

function sanitizeFields(document) {
  const doc = _.clone(document);
  _.unset(doc, '_id');
  _.unset(doc, '_key');
  _.unset(doc, '_rev');
  return doc;
}

/**
 * Auto-casting reference value by using native function of arangoDB
 *
 * @param {string} key
 * @param {object} value - raw value
 * @returns {object} interpreted value
 */
function autoCastKey(key, value) {
  if (util.isDate(value)) { // Date
    return qb.fn('DATE_TIMESTAMP')(key);
  }
  return key;
}

/**
 * Auto-casting raw data
 *
 * @param {object} value - raw value
 * @returns {object} interpreted value
 */
function autoCastValue(value) {
  if (util.isArray(value)) {
    return value.map(qb.str);
  }
  if (util.isString(value)) { // String
    return qb.str(value);
  }
  if (util.isBoolean(value)) { // Boolean
    return qb.bool(value);
  }
  if (util.isNumber(value)) {
    const i = parseInt(value, 10);
    if (i.toFixed(0) === value) { // Integer
      return qb.int(value);
    }
    return qb.num(value);
  }
  if (util.isDate(value)) { // Date
    return qb.fn('DATE_TIMESTAMP')(qb.int(value));
  }
  return value;
}

function buildFilter(filter, conjunction, path) {
  const p = path || 'node';
  const items = [];
  _.forEach(filter, (value, key) => {
    if (_.isArray(filter)) {
      items.push(buildFilter(value, conjunction, p));
      return;
    }
    const kp = p + '.' + key;
    switch (key) {
      case '$or':
        items.push(buildFilter(value, 'or', p));
        break;
      case '$and':
        items.push(buildFilter(value, 'and', p));
        break;
      default:
        if (_.isEmpty(value)) {
          return;
        }
        items.push(qb.eq(autoCastKey(kp, value), autoCastValue(value)));
    }
  });
  if (items.length === 0) {
    return qb;
  }
  let q = items[0];
  for (let i = 1; i < items.length; i++) {
    q = q[conjunction](items[i]);
  }
  return q;
}

function buildLimiter(q, options) {
  // LIMIT count
  // LIMIT offset, count
  if (options.limit) {
    if (options.offset) {
      return q.limit(options.offset, options.limit);
    }
    return q.limit(options.limit);
  }
  return q;
}

class Arango {
  /**
   * ArangoDB provider
   *
   * @param {Object} conn Arangojs database connection.
   */
  constructor(conn) {
    this.$db = conn;
  }

  /**
   * Inserts documents into database.
   *
   * @param  {String} collection Collection name
   * @param  {Object|array.Object} documents  A single or multiple documents.
   */
  * insert(collection, documents) {
    if (_.isNil(collection) || !_.isString(collection) || _.isEmpty(collection)) {
      throw new Error('invalid or missing collection argument');
    }
    if (_.isNil(documents)) {
      throw new Error('invalid or missing documents argument');
    }
    let docs = documents;
    if (!_.isArray(documents)) {
      docs = [documents];
    }
    _.forEach(docs, (document, i) => {
      docs[i] = ensureKey(_.clone(document));
    });
    let q = qb.for('document').in(qb(docs));
    q = q.insert('document').in('@@collection');
    const bindVars = {
      '@collection': collection,
    };
    yield query(this.$db, collection, q, bindVars);
  }

  /**
   * Find documents based on filter.
   *
   * @param  {String} collection Collection name
   * @param  {Object} filter     Key, value Object
   * @param  {Object} options     options.limit, options.offset
   * @return {array.Object}            A list of found documents.
   */
  *find(collection, filter, options) {
    if (_.isNil(collection) || !_.isString(collection) || _.isEmpty(collection)) {
      throw new Error('invalid or missing collection argument');
    }
    const fil = filter || {};
    const opts = options || {};
    let q = qb.for('node').in('@@collection');
    if (_.size(fil) > 0) {
      const f = buildFilter(fil, 'and');
      q = q.filter(f);
    }
    q = buildLimiter(q, opts);
    q = q.return('node');
    const bindVars = {
      '@collection': collection,
    };
    const res = yield query(this.$db, collection, q, bindVars);
    const docs = yield res.all();
    _.forEach(docs, (doc, i) => {
      docs[i] = sanitizeFields(doc);
    });
    return docs;
  }

  /**
   * Find documents by id (_key).
   *
   * @param  {String} collection Collection name
   * @param  {String|array.String} identifications        A single ID or multiple IDs.
   * @return {array.Object}            A list of found documents.
   */
  *findByID(collection, identifications) {
    if (_.isNil(collection) || !_.isString(collection) || _.isEmpty(collection)) {
      throw new Error('invalid or missing collection argument');
    }
    if (_.isNil(identifications)) {
      throw new Error('invalid or missing ids argument');
    }
    let ids = identifications;
    if (!_.isArray(identifications)) {
      ids = [identifications];
    }
    const keys = new Array(ids.length);
    _.forEach(ids, (id, i) => {
      keys[i] = idToKey(id);
    });

    let q = qb.for('key').in(qb(keys));
    q = q.for('node').in('@@collection');
    q = q.filter(qb.eq('node._key', 'key'));
    q = q.return('node');
    const bindVars = {
      '@collection': collection,
    };
    const res = yield query(this.$db, collection, q, bindVars);
    const docs = yield res.all();
    _.forEach(docs, (doc, i) => {
      docs[i] = sanitizeFields(doc);
    });
    return docs;
  }

  /**
   * Finds documents by id (_key) and updates them.
   *
   * @param  {String} collection Collection name
   * @param  {Object} filter     Key, value Object
   * @param  {Object} document  A document patch.
   */
  *update(collection, filter, document) {
    if (_.isNil(collection) ||
      !_.isString(collection) || _.isEmpty(collection)) {
      throw new Error('invalid or missing collection argument');
    }
    if (_.isNil(document)) {
      throw new Error('invalid or missing document argument');
    }
    const doc = ensureKey(_.clone(document));
    const fil = filter || {};
    let q = qb.for('node').in('@@collection');
    if (_.size(fil) > 0) {
      const f = buildFilter(fil, 'and');
      q = q.filter(f);
    }
    q = q.update('node').with(qb(doc)).in('@@collection');
    const bindVars = {
      '@collection': collection,
    };
    yield query(this.$db, collection, q, bindVars);
  }

  /**
   * Deletes all documents selected by filter.
   *
   * @param  {String} collection Collection name
   * @param  {Object} filter
   */
  *delete(collection, filter) {
    if (_.isNil(collection) ||
      !_.isString(collection) || _.isEmpty(collection)) {
      throw new Error('invalid or missing collection argument');
    }
    const fil = filter || {};
    const b = buildFilter(fil, 'and');
    let q = qb.for('node').in('@@collection');
    q = q.filter(b).remove('node').in('@@collection');
    const bindVars = {
      '@collection': collection,
    };
    yield query(this.$db, collection, q, bindVars);
  }
}

function* connect(conf, logger) {
  const dbHost = conf.host || '127.0.0.1';
  const dbPort = conf.port || 8529;
  const dbName = conf.database || 'arango';
  const autoCreate = conf.autoCreate || false;
  const attempts = conf.retries || 3;

  let mainError;
  for (let currentAttempt = 1; currentAttempt <= attempts; currentAttempt++) {
    try {
      logger.info(
        'Attempt to connect database', dbHost, dbPort, dbName, {
          attempt: currentAttempt
        });
      const db = new Arangojs('http://' + dbHost + ':' + dbPort);
      try {
        db.useDatabase(dbName);
        yield db.get();
      } catch (err) {
        if (err.name === 'ArangoError' && err.errorNum === 1228) {
          if (autoCreate) {
            logger.verbose(
              util.format('auto creating arango database %s', dbName));
            // Database does not exist, create a new one
            db.useDatabase(DB_SYSTEM);
            yield db.createDatabase(dbName);
            db.useDatabase(dbName);
            return db;
          }
        }
        throw err;
      }
      return db;
    } catch (err) {
      logger.error(
        'Database connection error',
        err, dbHost, dbPort, dbName, {
          attempt: currentAttempt
        });
      mainError = err;
    }
  }
  throw mainError;
}

/**
 * Creates a new connected ArangoDB provider.
 *
 * @param  {Object} conf   ArangoDB configuration
 * @param  {Object} logger Logger
 * @return {Arango}        ArangoDB provider
 */
module.exports.create = function* create(conf, logger) {
  const conn = yield connect(conf, logger);
  return new Arango(conn);
};
<commit_msg>Clone documents before modifying<commit_after>'use strict';

const Arangojs = require('arangojs');
const util = require('util');
const slug = require('slug');
const _ = require('lodash');
const qb = require('aqb');

const DB_SYSTEM = '_system';

function* query(db, collection, q, bind) {
  try {
    return yield db.query(q, bind);
  } catch (err) {
    if (err.message && err.message.startsWith('collection not found')) {
      const c = db.collection(collection);
      yield c.create();
    } else {
      throw err;
    }
  }
  return yield db.query(q, bind);
}

function idToKey(id) {
  return slug(id, {
    replacement: '_'
  });
}

function ensureKey(document) {
  const doc = _.clone(document);
  if (_.has(doc, '_key')) {
    return doc;
  }
  const id = doc.id;
  if (id) {
    _.set(doc, '_key', idToKey(id));
  }
  return doc;
}

function sanitizeFields(document) {
  const doc = _.clone(document);
  _.unset(doc, '_id');
  _.unset(doc, '_key');
  _.unset(doc, '_rev');
  return doc;
}

/**
 * Auto-casting reference value by using native function of arangoDB
 *
 * @param {string} key
 * @param {object} value - raw value
 * @returns {object} interpreted value
 */
function autoCastKey(key, value) {
  if (util.isDate(value)) { // Date
    return qb.fn('DATE_TIMESTAMP')(key);
  }
  return key;
}

/**
 * Auto-casting raw data
 *
 * @param {object} value - raw value
 * @returns {object} interpreted value
 */
function autoCastValue(value) {
  if (util.isArray(value)) {
    return value.map(qb.str);
  }
  if (util.isString(value)) { // String
    return qb.str(value);
  }
  if (util.isBoolean(value)) { // Boolean
    return qb.bool(value);
  }
  if (util.isNumber(value)) {
    const i = parseInt(value, 10);
    if (i.toFixed(0) === value) { // Integer
      return qb.int(value);
    }
    return qb.num(value);
  }
  if (util.isDate(value)) { // Date
    return qb.fn('DATE_TIMESTAMP')(qb.int(value));
  }
  return value;
}

function buildFilter(filter, conjunction, path) {
  const p = path || 'node';
  const items = [];
  _.forEach(filter, (value, key) => {
    if (_.isArray(filter)) {
      items.push(buildFilter(value, conjunction, p));
      return;
    }
    const kp = p + '.' + key;
    switch (key) {
      case '$or':
        items.push(buildFilter(value, 'or', p));
        break;
      case '$and':
        items.push(buildFilter(value, 'and', p));
        break;
      default:
        if (_.isEmpty(value)) {
          return;
        }
        items.push(qb.eq(autoCastKey(kp, value), autoCastValue(value)));
    }
  });
  if (items.length === 0) {
    return qb;
  }
  let q = items[0];
  for (let i = 1; i < items.length; i++) {
    q = q[conjunction](items[i]);
  }
  return q;
}

function buildLimiter(q, options) {
  // LIMIT count
  // LIMIT offset, count
  if (options.limit) {
    if (options.offset) {
      return q.limit(options.offset, options.limit);
    }
    return q.limit(options.limit);
  }
  return q;
}

class Arango {
  /**
   * ArangoDB provider
   *
   * @param {Object} conn Arangojs database connection.
   */
  constructor(conn) {
    this.$db = conn;
  }

  /**
   * Inserts documents into database.
   *
   * @param  {String} collection Collection name
   * @param  {Object|array.Object} documents  A single or multiple documents.
   */
  * insert(collection, documents) {
    if (_.isNil(collection) || !_.isString(collection) || _.isEmpty(collection)) {
      throw new Error('invalid or missing collection argument');
    }
    if (_.isNil(documents)) {
      throw new Error('invalid or missing documents argument');
    }
    let docs = _.cloneDeep(documents);
    if (!_.isArray(documents)) {
      docs = [documents];
    }
    _.forEach(docs, (document, i) => {
      docs[i] = ensureKey(document);
    });
    let q = qb.for('document').in(qb(docs));
    q = q.insert('document').in('@@collection');
    const bindVars = {
      '@collection': collection,
    };
    yield query(this.$db, collection, q, bindVars);
  }

  /**
   * Find documents based on filter.
   *
   * @param  {String} collection Collection name
   * @param  {Object} filter     Key, value Object
   * @param  {Object} options     options.limit, options.offset
   * @return {array.Object}            A list of found documents.
   */
  *find(collection, filter, options) {
    if (_.isNil(collection) || !_.isString(collection) || _.isEmpty(collection)) {
      throw new Error('invalid or missing collection argument');
    }
    const fil = filter || {};
    const opts = options || {};
    let q = qb.for('node').in('@@collection');
    if (_.size(fil) > 0) {
      const f = buildFilter(fil, 'and');
      q = q.filter(f);
    }
    q = buildLimiter(q, opts);
    q = q.return('node');
    const bindVars = {
      '@collection': collection,
    };
    const res = yield query(this.$db, collection, q, bindVars);
    const docs = yield res.all();
    _.forEach(docs, (doc, i) => {
      docs[i] = sanitizeFields(doc);
    });
    return docs;
  }

  /**
   * Find documents by id (_key).
   *
   * @param  {String} collection Collection name
   * @param  {String|array.String} identifications        A single ID or multiple IDs.
   * @return {array.Object}            A list of found documents.
   */
  *findByID(collection, identifications) {
    if (_.isNil(collection) || !_.isString(collection) || _.isEmpty(collection)) {
      throw new Error('invalid or missing collection argument');
    }
    if (_.isNil(identifications)) {
      throw new Error('invalid or missing ids argument');
    }
    let ids = identifications;
    if (!_.isArray(identifications)) {
      ids = [identifications];
    }
    const keys = new Array(ids.length);
    _.forEach(ids, (id, i) => {
      keys[i] = idToKey(id);
    });

    let q = qb.for('key').in(qb(keys));
    q = q.for('node').in('@@collection');
    q = q.filter(qb.eq('node._key', 'key'));
    q = q.return('node');
    const bindVars = {
      '@collection': collection,
    };
    const res = yield query(this.$db, collection, q, bindVars);
    const docs = yield res.all();
    _.forEach(docs, (doc, i) => {
      docs[i] = sanitizeFields(doc);
    });
    return docs;
  }

  /**
   * Finds documents by id (_key) and updates them.
   *
   * @param  {String} collection Collection name
   * @param  {Object} filter     Key, value Object
   * @param  {Object} document  A document patch.
   */
  *update(collection, filter, document) {
    if (_.isNil(collection) ||
      !_.isString(collection) || _.isEmpty(collection)) {
      throw new Error('invalid or missing collection argument');
    }
    if (_.isNil(document)) {
      throw new Error('invalid or missing document argument');
    }
    const doc = ensureKey(_.clone(document));
    const fil = filter || {};
    let q = qb.for('node').in('@@collection');
    if (_.size(fil) > 0) {
      const f = buildFilter(fil, 'and');
      q = q.filter(f);
    }
    q = q.update('node').with(qb(doc)).in('@@collection');
    const bindVars = {
      '@collection': collection,
    };
    yield query(this.$db, collection, q, bindVars);
  }

  *upsert(collection, filter, document) {
    if (_.isNil(collection) ||
      !_.isString(collection) || _.isEmpty(collection)) {
      throw new Error('invalid or missing collection argument');
    }
    if (_.isNil(document)) {
      throw new Error('invalid or missing document argument');
    }
    const doc = ensureKey(_.clone(document));
    const fil = filter || {};
    let q = qb.for('node').in('@@collection');
    if (_.size(fil) > 0) {
      const f = buildFilter(fil, 'and');
      q = q.filter(f);
    }
    q = q.upsert('node')
      .insert(qb(doc))
      .replace(qb(doc))
      .in('@@collection');
    console.log(q.toAQL());
    const bindVars = {
      '@collection': collection,
    };
    yield query(this.$db, collection, q, bindVars);
  }

  /**
   * Deletes all documents selected by filter.
   *
   * @param  {String} collection Collection name
   * @param  {Object} filter
   */
  *delete(collection, filter) {
    if (_.isNil(collection) ||
      !_.isString(collection) || _.isEmpty(collection)) {
      throw new Error('invalid or missing collection argument');
    }
    const fil = filter || {};
    const b = buildFilter(fil, 'and');
    let q = qb.for('node').in('@@collection');
    q = q.filter(b).remove('node').in('@@collection');
    const bindVars = {
      '@collection': collection,
    };
    yield query(this.$db, collection, q, bindVars);
  }
}

function* connect(conf, logger) {
  const dbHost = conf.host || '127.0.0.1';
  const dbPort = conf.port || 8529;
  const dbName = conf.database || 'arango';
  const autoCreate = conf.autoCreate || false;
  const attempts = conf.retries || 3;

  let mainError;
  for (let currentAttempt = 1; currentAttempt <= attempts; currentAttempt++) {
    try {
      logger.info(
        'Attempt to connect database', dbHost, dbPort, dbName, {
          attempt: currentAttempt
        });
      const db = new Arangojs('http://' + dbHost + ':' + dbPort);
      try {
        db.useDatabase(dbName);
        yield db.get();
      } catch (err) {
        if (err.name === 'ArangoError' && err.errorNum === 1228) {
          if (autoCreate) {
            logger.verbose(
              util.format('auto creating arango database %s', dbName));
            // Database does not exist, create a new one
            db.useDatabase(DB_SYSTEM);
            yield db.createDatabase(dbName);
            db.useDatabase(dbName);
            return db;
          }
        }
        throw err;
      }
      return db;
    } catch (err) {
      logger.error(
        'Database connection error',
        err, dbHost, dbPort, dbName, {
          attempt: currentAttempt
        });
      mainError = err;
    }
  }
  throw mainError;
}

/**
 * Creates a new connected ArangoDB provider.
 *
 * @param  {Object} conf   ArangoDB configuration
 * @param  {Object} logger Logger
 * @return {Arango}        ArangoDB provider
 */
module.exports.create = function* create(conf, logger) {
  const conn = yield connect(conf, logger);
  return new Arango(conn);
};
<|endoftext|>


<commit_before>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

let NS_XUL = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';

function install(data, reason) {}
function uninstall(data, reason) {}

// Add the menu item to the Web Developer menu entry
function startup(data, reason) {

    Components.utils.import('chrome://websocket-disabler/content/menu.jsm');
    Components.utils.import('chrome://websocket-disabler/content/watchwindows.jsm');

    // Use a boolean to keep track of whether WebSockets are enabled or not and
    // declare a simple function to generate the menu label based on that
    var enabled = false;
    function menuItemTitle() {
        return (enabled ? "Disable" : "Enable") + " WebSockets";
    }

    // Run a callback each time a new window is opened and for all windows that
    // exist when the add-on first starts
    watchWindows(function(window) {

        // Create the menu item, which will be added to the bottom of the
        // developer menu in each window
        var item = addItemToDesktopMenu(
            window,
            'menuWebDeveloperPopup',
            'menu_websocket_disabler',
            menuItemTitle(),
            function() {

                // Toggle the enabled state of WebSockets
                enabled = !enabled;

                // Update the menu item's label
                item.setAttribute('label', menuItemTitle());
            }
        );

        // This is a nasty hack but it works - every time the location
        // of a browser object changes, remove the WebSocket property
        // from the browser's window object
        window.gBrowser.addTabsProgressListener({
            onLocationChange: function(browser) {
                if(!enabled) {
                    browser.contentWindow.wrappedJSObject.WebSocket = undefined;
                }
            }
        });
    });
}

// Remove the entry from the Web Developer menu
function shutdown(data, reason) {

    Components.utils.import('chrome://websocket-disabler/content/unload.jsm');

    // Unless the application is shutting down, remove the items
    if(reason != APP_SHUTDOWN) {
        unload();
    }
}
<commit_msg>Preserve setting across sessions (fixes #1).<commit_after>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

let NS_XUL = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
let PREF_ENABLED = 'extensions.websocket-disabler.enabled';

function install(data, reason) {}
function uninstall(data, reason) {}

// Add the menu item to the Web Developer menu entry
function startup(data, reason) {

    Components.utils.import('chrome://websocket-disabler/content/menu.jsm');
    Components.utils.import('chrome://websocket-disabler/content/watchwindows.jsm');

    // Set the default for the add-on
    var prefs = Components.classes['@mozilla.org/preferences-service;1']
            .getService(Components.interfaces.nsIPrefService);
    prefs.getDefaultBranch('').setBoolPref(PREF_ENABLED, false);

    // Use a boolean to keep track of whether WebSockets are enabled or not and
    // declare a simple function to generate the menu label based on that
    var enabled = prefs.getBranch('').getBoolPref(PREF_ENABLED);
    function menuItemTitle() {
        return (enabled ? "Enable" : "Disable") + " WebSockets";
    }

    // Run a callback each time a new window is opened and for all windows that
    // exist when the add-on first starts
    watchWindows(function(window) {

        // Create the menu item, which will be added to the bottom of the
        // developer menu in each window
        var item = addItemToDesktopMenu(
            window,
            'menuWebDeveloperPopup',
            'menu_websocket_disabler',
            menuItemTitle(),
            function() {

                // Toggle the enabled state of WebSockets
                enabled = !enabled;

                // Update the menu item's label
                item.setAttribute('label', menuItemTitle());

                // Store the pref
                prefs.getBranch('').setBoolPref(PREF_ENABLED, enabled);
            }
        );

        // This is a nasty hack but it works - every time the location
        // of a browser object changes, remove the WebSocket property
        // from the browser's window object
        window.gBrowser.addTabsProgressListener({
            onLocationChange: function(browser) {
                if(enabled) {
                    browser.contentWindow.wrappedJSObject.WebSocket = undefined;
                }
            }
        });
    });
}

// Remove the entry from the Web Developer menu
function shutdown(data, reason) {

    Components.utils.import('chrome://websocket-disabler/content/unload.jsm');

    // Unless the application is shutting down, remove the items
    if(reason != APP_SHUTDOWN) {
        unload();
    }
}
<|endoftext|>


<commit_before>/**
 * File: CMSMain.EditForm.js
 */
(function($) {
	$.entwine('ss', function($){
		/**
		 * Class: .cms-edit-form :input[name=ClassName]
		 * Alert the user on change of page-type. This might have implications
		 * on the available form fields etc.
		 */
		$('.cms-edit-form :input[name=ClassName]').entwine({
			// Function: onchange
			onchange: function() {
				alert(ss.i18n._t('CMSMAIN.ALERTCLASSNAME'));
			}
		});

		/**
		* Class: .cms-edit-form input[name=URLSegment]
		*
		 * Input validation on the URLSegment field
		 */
		$('.cms-edit-form input[name=URLSegment]').entwine({
			/**
			 * Property: FilterRegex
			 * Regex
			 */
			FilterRegex: /[^A-Za-z0-9-]+/,

			/**
			 * Property: ValidationMessage
			 * String
			 */
			ValidationMessage: ss.i18n._t('CMSMAIN.URLSEGMENTVALIDATION'),

			/**
			 * Property: MaxLength
			 * Int
			 */
			MaxLength: 50,

			/**
			 * Constructor: onmatch
			 */
			onmatch : function() {
				var self = this;

				// intercept change event, do our own writing
				this.bind('change', function(e) {
					if(!self.validate()) {
						jQuery.noticeAdd(self.getValidationMessage());
					}
					self.val(self.suggestValue(e.target.value));
					return false;
				});

				this._super();
			},

			/**
			 * Function: suggestValue
			 *
			 * Return a value matching the criteria.
			 *
			 * Parameters:
			 *  (String) val
			 *
			 * Returns:
			 *  String
			 */
			suggestValue: function(val) {
				// TODO Do we want to enforce lowercasing in URLs?
				return val.substr(0, this.getMaxLength()).replace(this.getFilterRegex(), '').toLowerCase();
			},

			/**
			 * Function: validate
			 *
			 * Returns:
			 *  Boolean
			 */
			validate: function() {
				return (
					this.val().length > this.getMaxLength()
					|| this.val().match(this.getFilterRegex())
				);
			}
		});

		/**
		 * Class: .cms-edit-form input[name=Title]
		 *
		 * Input validation on the Title field
		 */
		$('.cms-edit-form input[name=Title]').entwine({
			// Constructor: onmatch
			onmatch : function() {
				var self = this;

				this.bind('change', function(e) {
					self.updateURLSegment(jQuery('.cms-edit-form input[name=URLSegment]'));
					// TODO We should really user-confirm these changes
					self.parents('form').find('input[name=MetaTitle], input[name=MenuTitle]').val(self.val());
				});

				this._super();
			},

			/**
			 * Function: updateURLSegment
			 *
			 * Parameters:
			 *  (DOMElement) field
			 */
			updateURLSegment: function(field) {
				if(!field || !field.length) return;

				// TODO language/logic coupling
				var isNew = this.val().indexOf("new") == 0;
				var suggestion = field.entwine('ss').suggestValue(this.val());
				var confirmMessage = ss.i18n.sprintf(
					ss.i18n._t(
						'UPDATEURL.CONFIRM',
						'Would you like me to change the URL to:\n\n'
						+ '%s/\n\nClick Ok to change the URL, '
						+ 'click Cancel to leave it as:\n\n%s'
					),
					suggestion,
					field.val()
				);

				// don't ask for replacement if record is considered 'new' as defined by its title
				if(isNew || (suggestion != field.val() && confirm(confirmMessage))) {
					field.val(suggestion);
				}
			}
		});

		/**
		 * Class: .cms-edit-form .parentTypeSelector
		 *
		 * ParentID field combination - mostly toggling between
		 * the two radiobuttons and setting the hidden "ParentID" field
		 */
		$('.cms-edit-form .parentTypeSelector').entwine({
			// Constructor: onmatch
			onmatch : function() {
				var self = this;
				this.find(':input[name=ParentType]').bind('click', function(e) {self._toggleSelection(e);});
				this._toggleSelection();

				this._super();
			},

			/**
			 * Function: _toggleSelection
			 *
			 * Parameters:
			 *  (Event) e
			 */
			_toggleSelection: function(e) {
				var selected = this.find(':input[name=ParentType]:checked').val();
				// reset parent id if 'root' radiobutton is selected
				if(selected == 'root') this.find(':input[name=ParentID]').val(0);
				// toggle tree dropdown based on selection
				this.find('#ParentID').toggle(selected != 'root');
			}
		});

		/**
		 * Class: .cms-edit-form #CanViewType, .cms-edit-form #CanEditType
		 *
		 * Toggle display of group dropdown in "access" tab,
		 * based on selection of radiobuttons.
		 */
		$('.cms-edit-form #CanViewType, .cms-edit-form #CanEditType').entwine({
			// Constructor: onmatch
			onmatch: function() {
				// TODO Decouple
				var dropdown;
				if(this.attr('id') == 'CanViewType') dropdown = $('#ViewerGroups');
				else if(this.attr('id') == 'CanEditType') dropdown = $('#EditorGroups');

				this.find('.optionset :input').bind('change', function(e) {
					dropdown.toggle(e.target.value == 'OnlyTheseUsers');
				});

				// initial state
				var currentVal = this.find('input[name=' + this.attr('id') + ']:checked').val();
				dropdown.toggle(currentVal == 'OnlyTheseUsers');

				this._super();
			}
		});

		/**
		 * Class: .cms-edit-form .Actions #Form_EditForm_action_email
		 *
		 * Email containing the link to the archived version of the page.
		 * Visible on readonly older versions of a specific page at the moment.
		 */
		$('.cms-edit-form .Actions #Form_EditForm_action_email').entwine({
			/**
			 * Function: onclick
			 *
			 * Parameters:
			 *  (Event) e
			 */
			onclick: function(e) {
				window.open(
					'mailto:?subject='
						+ $('input[name=ArchiveEmailSubject]', this[0].form).val()
						+ '&body='
						+ $(':input[name=ArchiveEmailMessage]', this[0].form).val(),
					'archiveemail'
				);

				return false;
			}
		});

		/**
		 * Class: .cms-edit-form .Actions #Form_EditForm_action_print
		 *
		 * Open a printable representation of the form in a new window.
		 * Used for readonly older versions of a specific page.
		 */
		$('.cms-edit-form .Actions #Form_EditForm_action_print').entwine({
			/**
			 * Function: onclick
			 *
			 * Parameters:
			 *  (Event) e
			 */
			onclick: function(e) {
				var printURL = $(this[0].form).attr('action').replace(/\?.*$/,'')
					+ '/printable/'
					+ $(':input[name=ID]',this[0].form).val();
				if(printURL.substr(0,7) != 'http://') printURL = $('base').attr('href') + printURL;

				window.open(printURL, 'printable');

				return false;
			}
		});

		/**
		 * Class: .cms-edit-form .Actions #Form_EditForm_action_rollback
		 *
		 * A "rollback" to a specific version needs user confirmation.
		 */
		$('.cms-edit-form .Actions #Form_EditForm_action_rollback').entwine({

			/**
			 * Function: onclick
			 *
			 * Parameters:
			 *  (Event) e
			 */
			onclick: function(e) {
				// @todo i18n
				var form = this.parents('form:first'), version = form.find(':input[name=Version]').val(), message = '';
				if(version) {
					message = "Do you really want to roll back to version #" + version + " of this page?";
				} else {
					message = "Do you really want to copy the published content to the stage site?";
				}
				return confirm(message);
			}
		});
	});
}(jQuery));<commit_msg>MINOR Fixed CreateToplevelGroups toggling in CMSMain.EditForm.js<commit_after>/**
 * File: CMSMain.EditForm.js
 */
(function($) {
	$.entwine('ss', function($){
		/**
		 * Class: .cms-edit-form :input[name=ClassName]
		 * Alert the user on change of page-type. This might have implications
		 * on the available form fields etc.
		 */
		$('.cms-edit-form :input[name=ClassName]').entwine({
			// Function: onchange
			onchange: function() {
				alert(ss.i18n._t('CMSMAIN.ALERTCLASSNAME'));
			}
		});

		/**
		* Class: .cms-edit-form input[name=URLSegment]
		*
		 * Input validation on the URLSegment field
		 */
		$('.cms-edit-form input[name=URLSegment]').entwine({
			/**
			 * Property: FilterRegex
			 * Regex
			 */
			FilterRegex: /[^A-Za-z0-9-]+/,

			/**
			 * Property: ValidationMessage
			 * String
			 */
			ValidationMessage: ss.i18n._t('CMSMAIN.URLSEGMENTVALIDATION'),

			/**
			 * Property: MaxLength
			 * Int
			 */
			MaxLength: 50,

			/**
			 * Constructor: onmatch
			 */
			onmatch : function() {
				var self = this;

				// intercept change event, do our own writing
				this.bind('change', function(e) {
					if(!self.validate()) {
						jQuery.noticeAdd(self.getValidationMessage());
					}
					self.val(self.suggestValue(e.target.value));
					return false;
				});

				this._super();
			},

			/**
			 * Function: suggestValue
			 *
			 * Return a value matching the criteria.
			 *
			 * Parameters:
			 *  (String) val
			 *
			 * Returns:
			 *  String
			 */
			suggestValue: function(val) {
				// TODO Do we want to enforce lowercasing in URLs?
				return val.substr(0, this.getMaxLength()).replace(this.getFilterRegex(), '').toLowerCase();
			},

			/**
			 * Function: validate
			 *
			 * Returns:
			 *  Boolean
			 */
			validate: function() {
				return (
					this.val().length > this.getMaxLength()
					|| this.val().match(this.getFilterRegex())
				);
			}
		});

		/**
		 * Class: .cms-edit-form input[name=Title]
		 *
		 * Input validation on the Title field
		 */
		$('.cms-edit-form input[name=Title]').entwine({
			// Constructor: onmatch
			onmatch : function() {
				var self = this;

				this.bind('change', function(e) {
					self.updateURLSegment(jQuery('.cms-edit-form input[name=URLSegment]'));
					// TODO We should really user-confirm these changes
					self.parents('form').find('input[name=MetaTitle], input[name=MenuTitle]').val(self.val());
				});

				this._super();
			},

			/**
			 * Function: updateURLSegment
			 *
			 * Parameters:
			 *  (DOMElement) field
			 */
			updateURLSegment: function(field) {
				if(!field || !field.length) return;

				// TODO language/logic coupling
				var isNew = this.val().indexOf("new") == 0;
				var suggestion = field.entwine('ss').suggestValue(this.val());
				var confirmMessage = ss.i18n.sprintf(
					ss.i18n._t(
						'UPDATEURL.CONFIRM',
						'Would you like me to change the URL to:\n\n'
						+ '%s/\n\nClick Ok to change the URL, '
						+ 'click Cancel to leave it as:\n\n%s'
					),
					suggestion,
					field.val()
				);

				// don't ask for replacement if record is considered 'new' as defined by its title
				if(isNew || (suggestion != field.val() && confirm(confirmMessage))) {
					field.val(suggestion);
				}
			}
		});

		/**
		 * Class: .cms-edit-form .parentTypeSelector
		 *
		 * ParentID field combination - mostly toggling between
		 * the two radiobuttons and setting the hidden "ParentID" field
		 */
		$('.cms-edit-form .parentTypeSelector').entwine({
			// Constructor: onmatch
			onmatch : function() {
				var self = this;
				this.find(':input[name=ParentType]').bind('click', function(e) {self._toggleSelection(e);});
				this._toggleSelection();

				this._super();
			},

			/**
			 * Function: _toggleSelection
			 *
			 * Parameters:
			 *  (Event) e
			 */
			_toggleSelection: function(e) {
				var selected = this.find(':input[name=ParentType]:checked').val();
				// reset parent id if 'root' radiobutton is selected
				if(selected == 'root') this.find(':input[name=ParentID]').val(0);
				// toggle tree dropdown based on selection
				this.find('#ParentID').toggle(selected != 'root');
			}
		});

		/**
		 * Class: .cms-edit-form #CanViewType, .cms-edit-form #CanEditType
		 *
		 * Toggle display of group dropdown in "access" tab,
		 * based on selection of radiobuttons.
		 */
		$('.cms-edit-form #CanViewType, .cms-edit-form #CanEditType, .cms-edit-form #CanCreateTopLevelType').entwine({
			// Constructor: onmatch
			onmatch: function() {
				// TODO Decouple
				var dropdown;
				if(this.attr('id') == 'CanViewType') dropdown = $('#ViewerGroups');
				else if(this.attr('id') == 'CanEditType') dropdown = $('#EditorGroups');
				else if(this.attr('id') == 'CanCreateTopLevelType') dropdown = $('#CreateTopLevelGroups');

				this.find('.optionset :input').bind('change', function(e) {
					dropdown[e.target.value == 'OnlyTheseUsers' ? 'show' : 'hide']();
				});

				// initial state
				var currentVal = this.find('input[name=' + this.attr('id') + ']:checked').val();
				dropdown[currentVal == 'OnlyTheseUsers' ? 'show' : 'hide']();

				this._super();
			}
		});

		/**
		 * Class: .cms-edit-form .Actions #Form_EditForm_action_email
		 *
		 * Email containing the link to the archived version of the page.
		 * Visible on readonly older versions of a specific page at the moment.
		 */
		$('.cms-edit-form .Actions #Form_EditForm_action_email').entwine({
			/**
			 * Function: onclick
			 *
			 * Parameters:
			 *  (Event) e
			 */
			onclick: function(e) {
				window.open(
					'mailto:?subject='
						+ $('input[name=ArchiveEmailSubject]', this[0].form).val()
						+ '&body='
						+ $(':input[name=ArchiveEmailMessage]', this[0].form).val(),
					'archiveemail'
				);

				return false;
			}
		});

		/**
		 * Class: .cms-edit-form .Actions #Form_EditForm_action_print
		 *
		 * Open a printable representation of the form in a new window.
		 * Used for readonly older versions of a specific page.
		 */
		$('.cms-edit-form .Actions #Form_EditForm_action_print').entwine({
			/**
			 * Function: onclick
			 *
			 * Parameters:
			 *  (Event) e
			 */
			onclick: function(e) {
				var printURL = $(this[0].form).attr('action').replace(/\?.*$/,'')
					+ '/printable/'
					+ $(':input[name=ID]',this[0].form).val();
				if(printURL.substr(0,7) != 'http://') printURL = $('base').attr('href') + printURL;

				window.open(printURL, 'printable');

				return false;
			}
		});

		/**
		 * Class: .cms-edit-form .Actions #Form_EditForm_action_rollback
		 *
		 * A "rollback" to a specific version needs user confirmation.
		 */
		$('.cms-edit-form .Actions #Form_EditForm_action_rollback').entwine({

			/**
			 * Function: onclick
			 *
			 * Parameters:
			 *  (Event) e
			 */
			onclick: function(e) {
				// @todo i18n
				var form = this.parents('form:first'), version = form.find(':input[name=Version]').val(), message = '';
				if(version) {
					message = "Do you really want to roll back to version #" + version + " of this page?";
				} else {
					message = "Do you really want to copy the published content to the stage site?";
				}
				return confirm(message);
			}
		});
	});
}(jQuery));<|endoftext|>


<commit_before>/*===========================
Device/OS Detection
===========================*/
Framework7.prototype.device = (function () {
    var device = {};
    var ua = navigator.userAgent;
    var $ = Dom7;

    var android = ua.match(/(Android);?[\s\/]+([\d.]+)?/);
    var ipad = ua.match(/(iPad).*OS\s([\d_]+)/);
    var ipod = ua.match(/(iPod)(.*OS\s([\d_]+))?/);
    var iphone = !ipad && ua.match(/(iPhone\sOS|iOS)\s([\d_]+)/);

    device.ios = device.android = device.iphone = device.ipod = device.ipad = device.androidChrome = false;

    // Android
    if (android) {
        device.os = 'android';
        device.osVersion = android[2];
        device.android = true;
        device.androidChrome = ua.toLowerCase().indexOf('chrome') >= 0;
    }
    if (ipad || iphone || ipod) {
        device.os = 'ios';
        device.ios = true;
    }
    // iOS
    if (iphone && !ipod) {
        device.osVersion = iphone[2].replace(/_/g, '.');
        device.iphone = true;
    }
    if (ipad) {
        device.osVersion = ipad[2].replace(/_/g, '.');
        device.ipad = true;
    }
    if (ipod) {
        device.osVersion = ipod[3] ? ipod[3].replace(/_/g, '.') : null;
        device.iphone = true;
    }
    // iOS 8+ changed UA
    if (device.ios && device.osVersion && ua.indexOf('Version/') >= 0) {
        if (device.osVersion.split('.')[0] === '10') {
            device.osVersion = ua.toLowerCase().split('version/')[1].split(' ')[0];
        }
    }

    // Webview
    device.webView = (iphone || ipad || ipod) && ua.match(/.*AppleWebKit(?!.*Safari)/i);

    // Minimal UI
    if (device.os && device.os === 'ios') {
        var osVersionArr = device.osVersion.split('.');
        device.minimalUi = !device.webView &&
                            (ipod || iphone) &&
                            (osVersionArr[0] * 1 === 7 ? osVersionArr[1] * 1 >= 1 : osVersionArr[0] * 1 > 7) &&
                            $('meta[name="viewport"]').length > 0 && $('meta[name="viewport"]').attr('content').indexOf('minimal-ui') >= 0;
    }

    // Check for status bar and fullscreen app mode
    var windowWidth = $(window).width();
    var windowHeight = $(window).height();

    device.needsStatusBar = function () {
        if (device.webView && (windowWidth * windowHeight === screen.width * screen.height)) {
            return true;
        }
        return false;
    };
    device.statusBar = device.needsStatusBar();

    // Classes
    var classNames = [];

    // Pixel Ratio
    device.pixelRatio = window.devicePixelRatio || 1;
    classNames.push('pixel-ratio-' + Math.floor(device.pixelRatio));
    if (device.pixelRatio >= 2) {
        classNames.push('retina');
    }

    // OS classes
    if (device.os) {
        classNames.push(device.os, device.os + '-' + device.osVersion.split('.')[0], device.os + '-' + device.osVersion.replace(/\./g, '-'));
        if (device.os === 'ios') {
            var major = parseInt(device.osVersion.split('.')[0], 10);
            for (var i = major - 1; i >= 6; i--) {
                classNames.push('ios-gt-' + i);
            }
        }

    }
    // Status bar classes
    if (device.statusBar) {
        classNames.push('with-statusbar-overlay');
    }
    else {
        $('html').removeClass('with-statusbar-overlay');
    }

    // Add html classes
    if (classNames.length > 0) $('html').addClass(classNames.join(' '));

    // Export object
    return device;
})();
<commit_msg>Fix windows detection<commit_after>/*===========================
Device/OS Detection
===========================*/
Framework7.prototype.device = (function () {
    var device = {};
    var ua = navigator.userAgent;
    var $ = Dom7;

    var windows = /windows phone/i.test(ua);
    var android = ua.match(/(Android);?[\s\/]+([\d.]+)?/);
    var ipad = ua.match(/(iPad).*OS\s([\d_]+)/);
    var ipod = ua.match(/(iPod)(.*OS\s([\d_]+))?/);
    var iphone = !ipad && ua.match(/(iPhone\sOS|iOS)\s([\d_]+)/);

    device.ios = device.android = device.windows = device.iphone = device.ipod = device.ipad = device.androidChrome = false;

    // Windows
    if (windows) {
        device.os = 'windows';
        device.windows = true;
    }
    // Android
    if (android && !windows) {
        device.os = 'android';
        device.osVersion = android[2];
        device.android = true;
        device.androidChrome = ua.toLowerCase().indexOf('chrome') >= 0;
    }
    if (ipad || iphone || ipod) {
        device.os = 'ios';
        device.ios = true;
    }
    // iOS
    if (iphone && !ipod) {
        device.osVersion = iphone[2].replace(/_/g, '.');
        device.iphone = true;
    }
    if (ipad) {
        device.osVersion = ipad[2].replace(/_/g, '.');
        device.ipad = true;
    }
    if (ipod) {
        device.osVersion = ipod[3] ? ipod[3].replace(/_/g, '.') : null;
        device.iphone = true;
    }
    // iOS 8+ changed UA
    if (device.ios && device.osVersion && ua.indexOf('Version/') >= 0) {
        if (device.osVersion.split('.')[0] === '10') {
            device.osVersion = ua.toLowerCase().split('version/')[1].split(' ')[0];
        }
    }

    // Webview
    device.webView = (iphone || ipad || ipod) && ua.match(/.*AppleWebKit(?!.*Safari)/i);

    // Minimal UI
    if (device.os && device.os === 'ios') {
        var osVersionArr = device.osVersion.split('.');
        device.minimalUi = !device.webView &&
                            (ipod || iphone) &&
                            (osVersionArr[0] * 1 === 7 ? osVersionArr[1] * 1 >= 1 : osVersionArr[0] * 1 > 7) &&
                            $('meta[name="viewport"]').length > 0 && $('meta[name="viewport"]').attr('content').indexOf('minimal-ui') >= 0;
    }

    // Check for status bar and fullscreen app mode
    var windowWidth = $(window).width();
    var windowHeight = $(window).height();

    device.needsStatusBar = function () {
        if (device.webView && (windowWidth * windowHeight === screen.width * screen.height)) {
            return true;
        }
        return false;
    };
    device.statusBar = device.needsStatusBar();

    // Classes
    var classNames = [];

    // Pixel Ratio
    device.pixelRatio = window.devicePixelRatio || 1;
    classNames.push('pixel-ratio-' + Math.floor(device.pixelRatio));
    if (device.pixelRatio >= 2) {
        classNames.push('retina');
    }

    // OS classes
    if (device.os) {
        classNames.push(device.os, device.os + '-' + device.osVersion.split('.')[0], device.os + '-' + device.osVersion.replace(/\./g, '-'));
        if (device.os === 'ios') {
            var major = parseInt(device.osVersion.split('.')[0], 10);
            for (var i = major - 1; i >= 6; i--) {
                classNames.push('ios-gt-' + i);
            }
        }

    }
    // Status bar classes
    if (device.statusBar) {
        classNames.push('with-statusbar-overlay');
    }
    else {
        $('html').removeClass('with-statusbar-overlay');
    }

    // Add html classes
    if (classNames.length > 0) $('html').addClass(classNames.join(' '));

    // Export object
    return device;
})();
<|endoftext|>


<commit_before>const _ = require('lodash');

module.exports = {
  find: async function (params = {}, populate) {
    const records = await this.query(function(qb) {
      _.forEach(params.where, (where, key) => {
        if (_.isArray(where.value)) {
          for (const value in where.value) {
            qb[value ? 'where' : 'orWhere'](key, where.symbol, where.value[value]);
          }
        } else {
          qb.where(key, where.symbol, where.value);
        }
      });

      if (params.start) {
        qb.offset(params.start);
      }

      if (params.limit) {
        qb.limit(params.limit);
      }
    })
      .orderBy(params.sort)
      .fetchAll({
        withRelated: populate || _.keys(_.groupBy(_.reject(this.associations, { autoPopulate: false }), 'alias'))
      });

    return records ? records.toJSON() : records;
  },

  count: async function (params = {}) {
    return await this
      .where(params)
      .count();
  },

  findOne: async function (params, populate) {
    const primaryKey = params[this.primaryKey] || params._id;

    if (primaryKey) {
      params = {
        [this.primaryKey]: primaryKey
      };
    }

    const record = await this
      .forge(params)
      .fetch({
        withRelated: populate || this.associations.map(x => x.alias)
      });

    return record ? record.toJSON() : record;
  },

  create: async function (params) {
    return this
      .forge()
      .save(Object.keys(params).reduce((acc, current) => {
        if (_.get(this._attributes, [current, 'type']) || _.get(this._attributes, [current, 'model'])) {
          acc[current] = params[current];
        }

        return acc;
      }, {}))
      .catch((err) => {
        if (err.detail) {
          const field = _.last(_.words(err.detail.split('=')[0]));
          err = { message: `This ${field} is already taken`, field };
        }

        throw err;
      });
  },

  update: async function (search, params = {}) {
    if (_.isEmpty(params)) {
      params = search;
    }

    const primaryKey = search[this.primaryKey] || search.id;

    if (primaryKey) {
      search = {
        [this.primaryKey]: primaryKey
      };
    } else {
      const entry = await module.exports.findOne.call(this, search);

      search = {
        [this.primaryKey]: entry[this.primaryKey] || entry.id
      };
    }

    return this.forge(search)
      .save(params, {
        patch: true
      })
      .catch((err) => {
        const field = _.last(_.words(err.detail.split('=')[0]));
        const error = { message: `This ${field} is already taken`, field };

        throw error;
      });
  },

  delete: async function (params) {
    return await this
      .forge({
        [this.primaryKey]: params[this.primaryKey] || params.id
      })
      .destroy();
  },

  deleteMany: async function (params) {
    return await this
      .query(qb => {
        qb.whereIn(this.primaryKey, params[this.primaryKey] || params.id);
      })
      .destroy();
  },

  search: async function (params) {
    return this
      .query(function(qb) {
        qb
          .where('username', 'LIKE', `%${params.id}%`)
          .orWhere('email', 'LIKE', `%${params.id}%`);
      })
      .fetchAll();
  },

  addPermission: async function (params) {
    return this
      .forge(params)
      .save();
  },

  removePermission: async function (params) {
    const value = params[this.primaryKey] ? {
      [this.primaryKey]: params[this.primaryKey] || params.id
    } : params;

    return this
      .forge()
      .where(value)
      .destroy();
  }
};
<commit_msg>Fix sort query bookshelf users-permissions<commit_after>const _ = require('lodash');

module.exports = {
  find: async function (params = {}, populate) {
    const records = await this.query(function(qb) {
      _.forEach(params.where, (where, key) => {
        if (_.isArray(where.value)) {
          for (const value in where.value) {
            qb[value ? 'where' : 'orWhere'](key, where.symbol, where.value[value]);
          }
        } else {
          qb.where(key, where.symbol, where.value);
        }
      });

      if (params.start) {
        qb.offset(params.start);
      }

      if (params.limit) {
        qb.limit(params.limit);
      }
    })
      .orderBy(params.sort.key, params.sort.order)
      .fetchAll({
        withRelated: populate || _.keys(_.groupBy(_.reject(this.associations, { autoPopulate: false }), 'alias'))
      });

    return records ? records.toJSON() : records;
  },

  count: async function (params = {}) {
    return await this
      .where(params)
      .count();
  },

  findOne: async function (params, populate) {
    const primaryKey = params[this.primaryKey] || params._id;

    if (primaryKey) {
      params = {
        [this.primaryKey]: primaryKey
      };
    }

    const record = await this
      .forge(params)
      .fetch({
        withRelated: populate || this.associations.map(x => x.alias)
      });

    return record ? record.toJSON() : record;
  },

  create: async function (params) {
    return this
      .forge()
      .save(Object.keys(params).reduce((acc, current) => {
        if (_.get(this._attributes, [current, 'type']) || _.get(this._attributes, [current, 'model'])) {
          acc[current] = params[current];
        }

        return acc;
      }, {}))
      .catch((err) => {
        if (err.detail) {
          const field = _.last(_.words(err.detail.split('=')[0]));
          err = { message: `This ${field} is already taken`, field };
        }

        throw err;
      });
  },

  update: async function (search, params = {}) {
    if (_.isEmpty(params)) {
      params = search;
    }

    const primaryKey = search[this.primaryKey] || search.id;

    if (primaryKey) {
      search = {
        [this.primaryKey]: primaryKey
      };
    } else {
      const entry = await module.exports.findOne.call(this, search);

      search = {
        [this.primaryKey]: entry[this.primaryKey] || entry.id
      };
    }

    return this.forge(search)
      .save(params, {
        patch: true
      })
      .catch((err) => {
        const field = _.last(_.words(err.detail.split('=')[0]));
        const error = { message: `This ${field} is already taken`, field };

        throw error;
      });
  },

  delete: async function (params) {
    return await this
      .forge({
        [this.primaryKey]: params[this.primaryKey] || params.id
      })
      .destroy();
  },

  deleteMany: async function (params) {
    return await this
      .query(qb => {
        qb.whereIn(this.primaryKey, params[this.primaryKey] || params.id);
      })
      .destroy();
  },

  search: async function (params) {
    return this
      .query(function(qb) {
        qb
          .where('username', 'LIKE', `%${params.id}%`)
          .orWhere('email', 'LIKE', `%${params.id}%`);
      })
      .fetchAll();
  },

  addPermission: async function (params) {
    return this
      .forge(params)
      .save();
  },

  removePermission: async function (params) {
    const value = params[this.primaryKey] ? {
      [this.primaryKey]: params[this.primaryKey] || params.id
    } : params;

    return this
      .forge()
      .where(value)
      .destroy();
  }
};
<|endoftext|>