<commit_before>/* global $ */

import { b64_sha1, Strophe } from 'strophe.js'; // eslint-disable-line camelcase

import XMPPEvents from '../../service/xmpp/XMPPEvents';
import Listenable from '../util/Listenable';

const logger = require('jitsi-meet-logger').getLogger(__filename);

/**
 * The property
 */
const IDENTITY_PROPERTIES = [ 'category', 'type', 'lang', 'name' ];
const IDENTITY_PROPERTIES_FOR_COMPARE = [ 'category', 'type', 'lang' ];
const HASH = 'sha-1';

export const ERROR_FEATURE_VERSION_MISMATCH = 'Feature version mismatch';

/**
 *
 * @param a
 * @param b
 */
function compareIdentities(a, b) {
    let res = 0;

    IDENTITY_PROPERTIES_FOR_COMPARE.some(key =>
        (res = ((a[key] > b[key]) && 1) || ((a[key] < b[key]) && -1)) !== 0
    );

    return res;
}

/**
 * Produces a sha-1 from provided identity and features values.
 *
 * @param {Array<Object>} identities - The identity objects.
 * @param {Array<string>} features - The features.
 * @returns {string}
 */
function generateSha(identities, features) {
    const sortedIdentities = identities.sort(compareIdentities).reduce(
        (accumulatedValue, identity) => `${
            IDENTITY_PROPERTIES.reduce(
                (tmp, key, idx) =>
                    tmp
                        + (idx === 0 ? '' : '/')
                        + (identity[key] ? identity[key] : ''),
                '')
        }<`, '');
    const sortedFeatures = features.sort().reduce(
        (tmp, feature) => `${tmp + feature}<`, '');

    return b64_sha1(sortedIdentities + sortedFeatures);
}

/**
 * Implements xep-0115 ( http://xmpp.org/extensions/xep-0115.html )
 */
export default class Caps extends Listenable {
    /**
     * Constructs new Caps instance.
     * @param {Strophe.Connection} connection the strophe connection object
     * @param {String} node the value of the node attribute of the "c" xml node
     * that will be sent to the other participants
     */
    constructor(connection = {}, node = 'http://jitsi.org/jitsimeet') {
        super();
        this.node = node;
        this.disco = connection.disco;
        if (!this.disco) {
            throw new Error(
                'Missing strophe-plugins '
                + '(disco plugin is required)!');
        }

        this.versionToCapabilities = Object.create(null);
        this.jidToVersion = Object.create(null);
        this.version = '';
        this.rooms = new Set();

        const emuc = connection.emuc;

        emuc.addListener(XMPPEvents.EMUC_ROOM_ADDED,
            room => this._addChatRoom(room));
        emuc.addListener(XMPPEvents.EMUC_ROOM_REMOVED,
            room => this._removeChatRoom(room));
        Object.keys(emuc.rooms).forEach(jid => {
            this._addChatRoom(emuc.rooms[jid]);
        });

        Strophe.addNamespace('CAPS', 'http://jabber.org/protocol/caps');
        this.disco.addFeature(Strophe.NS.CAPS);
        connection.addHandler(this._handleCaps.bind(this), Strophe.NS.CAPS);

        this._onMucMemberLeft = this._removeJidToVersionEntry.bind(this);
    }

    /**
     * Adds new feature to the list of supported features for the local
     * participant
     * @param {String} feature the name of the feature.
     * @param {boolean} submit if true - new presence with updated "c" node
     * will be sent.
     */
    addFeature(feature, submit = false) {
        this.disco.addFeature(feature);
        this._generateVersion();
        if (submit) {
            this.submit();
        }
    }

    /**
     * Removes a feature from the list of supported features for the local
     * participant
     * @param {String} feature the name of the feature.
     * @param {boolean} submit if true - new presence with updated "c" node
     * will be sent.
     */
    removeFeature(feature, submit = false) {
        this.disco.removeFeature(feature);
        this._generateVersion();
        if (submit) {
            this.submit();
        }
    }

    /**
     * Sends new presence stanza for every room from the list of rooms.
     */
    submit() {
        this.rooms.forEach(room => room.sendPresence());
    }

    /**
     * Returns a set with the features for a participant.
     * @param {String} jid the jid of the participant
     * @param {int} timeout the timeout in ms for reply from the participant.
     * @returns {Promise<Set<String>, Error>}
     */
    getFeatures(jid, timeout = 5000) {
        const user
            = jid in this.jidToVersion ? this.jidToVersion[jid] : null;

        if (!user || !(user.version in this.versionToCapabilities)) {
            const node = user ? `${user.node}#${user.version}` : null;

            return this._getDiscoInfo(jid, node, timeout)
                .then(({ features, identities }) => {
                    if (user) {
                        const sha = generateSha(
                            Array.from(identities),
                            Array.from(features)
                        );
                        const receivedNode = `${user.node}#${sha}`;

                        if (receivedNode === node) {
                            this.versionToCapabilities[receivedNode] = features;

                            return features;
                        }

                        // Check once if it has been cached asynchronously.
                        if (this.versionToCapabilities[receivedNode]) {
                            return this.versionToCapabilities[receivedNode];
                        }

                        logger.error(`Expected node ${node} but received ${
                            receivedNode}`);

                        return Promise.reject(ERROR_FEATURE_VERSION_MISMATCH);
                    }
                });
        }

        return Promise.resolve(this.versionToCapabilities[user.version]);
    }

    /**
     * Returns a set with the features for a host.
     * @param {String} jid the jid of the host
     * @param {int} timeout the timeout in ms for reply from the host.
     * @returns {Promise<Set<String>, Error>}
     */
    getFeaturesAndIdentities(jid, node, timeout = 5000) {
        return this._getDiscoInfo(jid, node, timeout);
    }

    /**
     * Returns a set with the features and identities for a host.
     * @param {String} jid the jid of the host
     * @param {String|null} node the node to query
     * @param {int} timeout the timeout in ms for reply from the host.
     * @returns {Promise<Object>}
     * @private
     */
    _getDiscoInfo(jid, node, timeout) {
        return new Promise((resolve, reject) =>
            this.disco.info(jid, node, response => {
                const features = new Set();
                const identities = new Set();

                $(response)
                    .find('>query>feature')
                    .each(
                        (_, el) => features.add(el.getAttribute('var')));
                $(response)
                    .find('>query>identity')
                    .each(
                        (_, el) => identities.add({
                            type: el.getAttribute('type'),
                            name: el.getAttribute('name'),
                            category: el.getAttribute('category')
                        }));
                resolve({
                    features,
                    identities });
            }, reject, timeout)
        );
    }

    /**
     * Adds ChatRoom instance to the list of rooms. Adds listeners to the room
     * and adds "c" element to the presences of the room.
     * @param {ChatRoom} room the room.
     */
    _addChatRoom(room) {
        this.rooms.add(room);
        room.addListener(XMPPEvents.MUC_MEMBER_LEFT, this._onMucMemberLeft);
        this._fixChatRoomPresenceMap(room);
    }

    /**
     * Removes ChatRoom instance from the list of rooms. Removes listeners
     * added from the Caps class.
     * @param {ChatRoom} room the room.
     */
    _removeChatRoom(room) {
        this.rooms.delete(room);
        room.removeListener(XMPPEvents.MUC_MEMBER_LEFT, this._onMucMemberLeft);
    }

    /**
     * Creates/updates the "c" xml node into the presence of the passed room.
     * @param {ChatRoom} room the room.
     */
    _fixChatRoomPresenceMap(room) {
        room.addToPresence('c', {
            attributes: {
                xmlns: Strophe.NS.CAPS,
                hash: HASH,
                node: this.node,
                ver: this.version
            }
        });
    }

    /**
     * Handles this.version changes.
     */
    _notifyVersionChanged() {
        // update the version for all rooms
        this.rooms.forEach(room => this._fixChatRoomPresenceMap(room));
    }

    /**
     * Generates the value for the "ver" attribute.
     */
    _generateVersion() {
        this.version
            = generateSha(this.disco._identities, this.disco._features);

        this._notifyVersionChanged();
    }

    /**
     * Parses the "c" xml node from presence.
     * @param {DOMElement} stanza the presence packet
     */
    _handleCaps(stanza) {
        const from = stanza.getAttribute('from');
        const caps = stanza.querySelector('c');
        const version = caps.getAttribute('ver');
        const node = caps.getAttribute('node');
        const oldVersion = this.jidToVersion[from];

        this.jidToVersion[from] = { version,
            node };
        if (oldVersion && oldVersion.version !== version) {
            this.eventEmitter.emit(XMPPEvents.PARTCIPANT_FEATURES_CHANGED,
                from);
        }

        // return true to not remove the handler from Strophe
        return true;
    }

    /**
     * Removes entry from this.jidToVersion map.
     * @param {String} jid the jid to be removed.
     */
    _removeJidToVersionEntry(jid) {
        if (jid in this.jidToVersion) {
            delete this.jidToVersion[jid];
        }
    }
}
<commit_msg>fix(xmpp/Caps): features for a user without caps support<commit_after>/* global $ */

import { b64_sha1, Strophe } from 'strophe.js'; // eslint-disable-line camelcase

import XMPPEvents from '../../service/xmpp/XMPPEvents';
import Listenable from '../util/Listenable';

const logger = require('jitsi-meet-logger').getLogger(__filename);

/**
 * The property
 */
const IDENTITY_PROPERTIES = [ 'category', 'type', 'lang', 'name' ];
const IDENTITY_PROPERTIES_FOR_COMPARE = [ 'category', 'type', 'lang' ];
const HASH = 'sha-1';

export const ERROR_FEATURE_VERSION_MISMATCH = 'Feature version mismatch';

/**
 *
 * @param a
 * @param b
 */
function compareIdentities(a, b) {
    let res = 0;

    IDENTITY_PROPERTIES_FOR_COMPARE.some(key =>
        (res = ((a[key] > b[key]) && 1) || ((a[key] < b[key]) && -1)) !== 0
    );

    return res;
}

/**
 * Produces a sha-1 from provided identity and features values.
 *
 * @param {Array<Object>} identities - The identity objects.
 * @param {Array<string>} features - The features.
 * @returns {string}
 */
function generateSha(identities, features) {
    const sortedIdentities = identities.sort(compareIdentities).reduce(
        (accumulatedValue, identity) => `${
            IDENTITY_PROPERTIES.reduce(
                (tmp, key, idx) =>
                    tmp
                        + (idx === 0 ? '' : '/')
                        + (identity[key] ? identity[key] : ''),
                '')
        }<`, '');
    const sortedFeatures = features.sort().reduce(
        (tmp, feature) => `${tmp + feature}<`, '');

    return b64_sha1(sortedIdentities + sortedFeatures);
}

/**
 * Implements xep-0115 ( http://xmpp.org/extensions/xep-0115.html )
 */
export default class Caps extends Listenable {
    /**
     * Constructs new Caps instance.
     * @param {Strophe.Connection} connection the strophe connection object
     * @param {String} node the value of the node attribute of the "c" xml node
     * that will be sent to the other participants
     */
    constructor(connection = {}, node = 'http://jitsi.org/jitsimeet') {
        super();
        this.node = node;
        this.disco = connection.disco;
        if (!this.disco) {
            throw new Error(
                'Missing strophe-plugins '
                + '(disco plugin is required)!');
        }

        this.versionToCapabilities = Object.create(null);
        this.jidToVersion = Object.create(null);
        this.version = '';
        this.rooms = new Set();

        const emuc = connection.emuc;

        emuc.addListener(XMPPEvents.EMUC_ROOM_ADDED,
            room => this._addChatRoom(room));
        emuc.addListener(XMPPEvents.EMUC_ROOM_REMOVED,
            room => this._removeChatRoom(room));
        Object.keys(emuc.rooms).forEach(jid => {
            this._addChatRoom(emuc.rooms[jid]);
        });

        Strophe.addNamespace('CAPS', 'http://jabber.org/protocol/caps');
        this.disco.addFeature(Strophe.NS.CAPS);
        connection.addHandler(this._handleCaps.bind(this), Strophe.NS.CAPS);

        this._onMucMemberLeft = this._removeJidToVersionEntry.bind(this);
    }

    /**
     * Adds new feature to the list of supported features for the local
     * participant
     * @param {String} feature the name of the feature.
     * @param {boolean} submit if true - new presence with updated "c" node
     * will be sent.
     */
    addFeature(feature, submit = false) {
        this.disco.addFeature(feature);
        this._generateVersion();
        if (submit) {
            this.submit();
        }
    }

    /**
     * Removes a feature from the list of supported features for the local
     * participant
     * @param {String} feature the name of the feature.
     * @param {boolean} submit if true - new presence with updated "c" node
     * will be sent.
     */
    removeFeature(feature, submit = false) {
        this.disco.removeFeature(feature);
        this._generateVersion();
        if (submit) {
            this.submit();
        }
    }

    /**
     * Sends new presence stanza for every room from the list of rooms.
     */
    submit() {
        this.rooms.forEach(room => room.sendPresence());
    }

    /**
     * Returns a set with the features for a participant.
     * @param {String} jid the jid of the participant
     * @param {int} timeout the timeout in ms for reply from the participant.
     * @returns {Promise<Set<String>, Error>}
     */
    getFeatures(jid, timeout = 5000) {
        const user
            = jid in this.jidToVersion ? this.jidToVersion[jid] : null;

        if (!user || !(user.version in this.versionToCapabilities)) {
            const node = user ? `${user.node}#${user.version}` : null;

            return this._getDiscoInfo(jid, node, timeout)
                .then(({ features, identities }) => {
                    if (user) {
                        const sha = generateSha(
                            Array.from(identities),
                            Array.from(features)
                        );
                        const receivedNode = `${user.node}#${sha}`;

                        if (receivedNode === node) {
                            this.versionToCapabilities[receivedNode] = features;

                            return features;
                        }

                        // Check once if it has been cached asynchronously.
                        if (this.versionToCapabilities[receivedNode]) {
                            return this.versionToCapabilities[receivedNode];
                        }

                        logger.error(`Expected node ${node} but received ${
                            receivedNode}`);

                        return Promise.reject(ERROR_FEATURE_VERSION_MISMATCH);
                    }

                    return features;
                });
        }

        return Promise.resolve(this.versionToCapabilities[user.version]);
    }

    /**
     * Returns a set with the features for a host.
     * @param {String} jid the jid of the host
     * @param {int} timeout the timeout in ms for reply from the host.
     * @returns {Promise<Set<String>, Error>}
     */
    getFeaturesAndIdentities(jid, node, timeout = 5000) {
        return this._getDiscoInfo(jid, node, timeout);
    }

    /**
     * Returns a set with the features and identities for a host.
     * @param {String} jid the jid of the host
     * @param {String|null} node the node to query
     * @param {int} timeout the timeout in ms for reply from the host.
     * @returns {Promise<Object>}
     * @private
     */
    _getDiscoInfo(jid, node, timeout) {
        return new Promise((resolve, reject) =>
            this.disco.info(jid, node, response => {
                const features = new Set();
                const identities = new Set();

                $(response)
                    .find('>query>feature')
                    .each(
                        (_, el) => features.add(el.getAttribute('var')));
                $(response)
                    .find('>query>identity')
                    .each(
                        (_, el) => identities.add({
                            type: el.getAttribute('type'),
                            name: el.getAttribute('name'),
                            category: el.getAttribute('category')
                        }));
                resolve({
                    features,
                    identities });
            }, reject, timeout)
        );
    }

    /**
     * Adds ChatRoom instance to the list of rooms. Adds listeners to the room
     * and adds "c" element to the presences of the room.
     * @param {ChatRoom} room the room.
     */
    _addChatRoom(room) {
        this.rooms.add(room);
        room.addListener(XMPPEvents.MUC_MEMBER_LEFT, this._onMucMemberLeft);
        this._fixChatRoomPresenceMap(room);
    }

    /**
     * Removes ChatRoom instance from the list of rooms. Removes listeners
     * added from the Caps class.
     * @param {ChatRoom} room the room.
     */
    _removeChatRoom(room) {
        this.rooms.delete(room);
        room.removeListener(XMPPEvents.MUC_MEMBER_LEFT, this._onMucMemberLeft);
    }

    /**
     * Creates/updates the "c" xml node into the presence of the passed room.
     * @param {ChatRoom} room the room.
     */
    _fixChatRoomPresenceMap(room) {
        room.addToPresence('c', {
            attributes: {
                xmlns: Strophe.NS.CAPS,
                hash: HASH,
                node: this.node,
                ver: this.version
            }
        });
    }

    /**
     * Handles this.version changes.
     */
    _notifyVersionChanged() {
        // update the version for all rooms
        this.rooms.forEach(room => this._fixChatRoomPresenceMap(room));
    }

    /**
     * Generates the value for the "ver" attribute.
     */
    _generateVersion() {
        this.version
            = generateSha(this.disco._identities, this.disco._features);

        this._notifyVersionChanged();
    }

    /**
     * Parses the "c" xml node from presence.
     * @param {DOMElement} stanza the presence packet
     */
    _handleCaps(stanza) {
        const from = stanza.getAttribute('from');
        const caps = stanza.querySelector('c');
        const version = caps.getAttribute('ver');
        const node = caps.getAttribute('node');
        const oldVersion = this.jidToVersion[from];

        this.jidToVersion[from] = { version,
            node };
        if (oldVersion && oldVersion.version !== version) {
            this.eventEmitter.emit(XMPPEvents.PARTCIPANT_FEATURES_CHANGED,
                from);
        }

        // return true to not remove the handler from Strophe
        return true;
    }

    /**
     * Removes entry from this.jidToVersion map.
     * @param {String} jid the jid to be removed.
     */
    _removeJidToVersionEntry(jid) {
        if (jid in this.jidToVersion) {
            delete this.jidToVersion[jid];
        }
    }
}
<|endoftext|>


<commit_before>import Ember from 'ember';
import { moduleForComponent, test } from 'ember-qunit';

const testOptions = {
    chartOptions: {
        chart: {
            type: 'bar'
        },

        xAxis: {
            categories: [ 'Apples', 'Bananas', 'Oranges' ]
        },

        yAxis: {
            title: {
                text: 'Fruit Eaten'
            }
        }
    }
};

const testSeries = [{
    name: 'Alice',
    data: [ 1, 0, 4 ]
}];

moduleForComponent( 'sl-chart', 'Unit | Component | sl chart', {
    unit: true
});

test( 'Default classNames are present', function( assert ) {
    this.subject({
        options: testOptions,
        series: testSeries
    });

    assert.ok(
        this.$().hasClass( 'panel' ),
        'Default rendered component has class "panel"'
    );

    assert.ok(
        this.$().hasClass( 'panel-default' ),
        'Default rendered component has class "panel-default"'
    );

    assert.ok(
        this.$().hasClass( 'sl-chart' ),
        'Default rendered component has class "sl-chart"'
    );

    assert.ok(
        this.$().hasClass( 'sl-panel' ),
        'Default rendered component has class "sl-panel"'
    );
});

test( 'Loading state adds loading class', function( assert ) {
    const component = this.subject({
        options: testOptions,
        series: testSeries
    });

    assert.strictEqual(
        this.$().hasClass( 'sl-loading' ),
        false,
        'Default rendered component does not have "sl-loading" class'
    );

    Ember.run( () => {
        component.set( 'isLoading', true );
    });

    assert.ok(
        this.$().hasClass( 'sl-loading' ),
        'Rendered component gains "sl-loading" class'
    );
});

test( 'updateData() is called after series property is modified', function( assert ) {
    const component = this.subject({
        options: testOptions,
        series: testSeries
    });

    this.render();

    const spy = sinon.spy( component, 'updateData' );
    const changedTestSeries = [];

    component.set( 'series' , changedTestSeries );

    assert.ok(
        spy.calledOnce,
        'updateData() is called once after series modified'
    );
});

test( '"Options" property needs to be an object', function( assert ) {

    // Null
    let assertionThrown = false;

    try {
        this.subject({
            options: null,
            series: testSeries
        });
    } catch( error ) {
        assertionThrown = true;
    }

    assert.ok(
        assertionThrown,
        'property was null'
    );

    // Array
    assertionThrown = false;

    try {
        this.subject({
            options: [],
            series: testSeries
        });
    } catch( error ) {
        assertionThrown = true;
    }

    assert.ok(
        assertionThrown,
        'property was an array'
    );

    // String
    assertionThrown = false;

    try {
        this.subject({
            options: "string",
            series: testSeries
        });
    } catch( error ) {
        assertionThrown = true;
    }

    assert.ok(
        assertionThrown,
        'property was string'
    );

    // Undefined
    assertionThrown = false;

    try {
        this.subject({
            options: undefined,
            series: testSeries
        });
    } catch( error ) {
        assertionThrown = true;
    }

    assert.ok(
        assertionThrown,
        'property was undefined'
    );

    // Boolean
    assertionThrown = false;

    try {
        this.subject({
            options: true,
            series: testSeries
        });
    } catch( error ) {
        assertionThrown = true;
    }

    assert.ok(
        assertionThrown,
        'property was a boolean'
    );

    // Number
    assertionThrown = false;

    try {
        this.subject({
            options: 123,
            series: testSeries
        });
    } catch( error ) {
        assertionThrown = true;
    }

    assert.ok(
        assertionThrown,
        'property was number'
    );

    // Function
    assertionThrown = false;

    try {
        this.subject({
            options: function(){},
            series: testSeries
        });
    } catch( error ) {
        assertionThrown = true;
    }

    assert.ok(
        assertionThrown,
        'property was a function'
    );

    // Object
    assertionThrown = false;

    try {
        this.subject({
            options: testOptions,
            series: testSeries
        });
    } catch( error ) {
        assertionThrown = true;
    }

    assert.ok(
        !assertionThrown,
        'property was an Object'
    );

    // Symbol
    assertionThrown = false;

    try {
        const sym = Symbol( 'foo' );
        this.subject({
            options: sym,
            series: testSeries
        });
    } catch( error ) {
        assertionThrown = true;
    }

    assert.ok(
        assertionThrown,
        'property was a symbol'
    );
});

test( '"Series" property needs to be an array', function( assert ) {

    // Null
    let assertionThrown = false;

    try {
        this.subject({
            options: testOptions,
            series: null
        });
    } catch( error ) {
        assertionThrown = true;
    }

    assert.ok(
        assertionThrown,
        'property was null'
    );

    // Object
    assertionThrown = false;

    try {
        this.subject({
            options: testOptions,
            series: {}
        });
    } catch( error ) {
        assertionThrown = true;
    }

    assert.ok(
        assertionThrown,
        'property was an object'
    );

    // Symbol
    assertionThrown = false;

    try {
        const sym = Symbol( 'foo' );
        this.subject({
            options: testOptions,
            series: sym
        });
    } catch( error ) {
        assertionThrown = true;
    }

    assert.ok(
        assertionThrown,
        'property was a symbol'
    );

    // String
    assertionThrown = false;

    try {
        this.subject({
            options: testOptions,
            series: "string"
        });
    } catch( error ) {
        assertionThrown = true;
    }

    assert.ok(
        assertionThrown,
        'property was string'
    );

    // Undefined
    assertionThrown = false;

    try {
        this.subject({
            options: testOptions,
            series: undefined
        });
    } catch( error ) {
        assertionThrown = true;
    }

    assert.ok(
        assertionThrown,
        'property was undefined'
    );

    // Boolean
    assertionThrown = false;

    try {
        this.subject({
            options: testOptions,
            series: true
        });
    } catch( error ) {
        assertionThrown = true;
    }

    assert.ok(
        assertionThrown,
        'property was a boolean'
    );

    // Number
    assertionThrown = false;

    try {
        this.subject({
            options: testOptions,
            series: 123
        });
    } catch( error ) {
        assertionThrown = true;
    }

    assert.ok(
        assertionThrown,
        'property was number'
    );

    // Function
    assertionThrown = false;

    try {
        this.subject({
            options: testOptions,
            series: function(){}
        });
    } catch( error ) {
        assertionThrown = true;
    }

    assert.ok(
        assertionThrown,
        'property was a function'
    );

    // Array
    assertionThrown = false;

    try {
        this.subject({
            options: testOptions,
            series: testSeries
        });
    } catch( error ) {
        assertionThrown = true;
    }

    assert.ok(
        !assertionThrown,
        'property was an Array'
    );
});

test( "Chart div uses the correct style", function( assert ) {
    const component = this.subject({
        options: testOptions,
        series: testSeries
    });

    assert.equal(
        component.get( 'style' ),
        'height: auto; width: auto;'
    );

    assert.equal(
        this.$( 'div.chart' ).attr( 'style' ),
        component.get( 'style' ),
        'Chart div has automatic height and width'
    );

    Ember.run( () => {
        component.set( 'height', 10 );
        component.set( 'width', 20 );
    });

    assert.equal(
        component.get( 'style' ),
        'height: 10; width: 20;'
    );

    assert.equal(
        this.$( 'div.chart' ).attr( 'style' ),
        component.get( 'style' ),
        'Chart div has height 10 and width 20'
    );
});

test( "Title property is set", function( assert ) {
    const component = this.subject({
        options: testOptions,
        series: testSeries
    });

    assert.strictEqual(
        this.$( '.panel-heading' )[0],
        undefined,
        'No chart title is rendered when title is not set'
    );

    const testTitle = "Peak server hours";

    Ember.run( () => {
        component.set( 'title', testTitle );
    });

    assert.equal(
        this.$( '.panel-heading' ).html(),
        testTitle,
        'Chart title is created with title value'
    );
});

test( "setupChart initializes chart and updates data upon render", function( assert ) {
    const chartTest = "a test chart";
    const chartDivMock = {
        highcharts( options ) {
            if ( Ember.typeOf( options ) === "undefined" ) {
                return chartTest;
            } else {
                return null;
            }
        }
    };

    const component = this.subject({
        options: testOptions,
        series: testSeries,
        $: function() {
            return chartDivMock;
        },
        updateData: function() {
            return;
        }
    });

    const setupSpy = sinon.spy( component, 'setupChart' );
    const updateSpy = sinon.spy( component, 'updateData' );
    const highchartsSpy = sinon.spy( chartDivMock, 'highcharts' );

    assert.equal(
        component.get( 'chart' ),
        null,
        'Chart is null upon initilization'
    );

    this.render();

    assert.ok(
        setupSpy.calledOnce,
        'setupChart was called once upon render'
    );

    assert.ok(
        updateSpy.calledOnce,
        'updateData was called once upon render'
    );

    assert.ok(
        highchartsSpy.calledTwice,
        'highcharts was called twice upon render'
    );

    assert.ok(
        highchartsSpy.calledWithExactly( component.get( 'highchartsOptions' ) ),
        'highcharts was called once with options'
    );

    assert.ok(
        highchartsSpy.calledWithExactly(),
        'highcharts was called once with no parameters'
    );

    assert.equal(
        component.get( 'chart' ),
        chartTest,
        'chart is initialized'
    );
});

test( 'highchartsOptions returns expected options', function( assert ) {
    const component = this.subject({
        options: testOptions,
        series: testSeries
    });
    const chartStyle = {
        fontFamily: '"Benton Sans", "Helvetica Neue", Helvetica, Arial, sans-serif',
        fontSize: '13px'
    };
    const options = Ember.$.extend( true, {
        chart: {
            animation: false,
            backgroundColor: 'rgba(255, 255, 255, 0)',
            style: chartStyle
        },
        title: null,
        colors: [
            '#298fce',
            '#94302e',
            '#00a14b',
            '#f29c1e',
            '#fadb00',
            '#34495d'
        ],
        credits: {
            enabled: false
        },
        legend: {
            itemStyle: chartStyle
        },
        plotOptions: {
            bar: {
                borderColor: 'transparent'
            },
            series: {
                animation: false
            }
        },
        tooltip: {
            animation: false,
            backgroundColor: 'rgba(0, 0, 0, 0.8)',
            borderWidth: 0,
            shadow: false,
            style: {
                color: '#fff'
            }
        },
        xAxis: {
            labels: {
                style: chartStyle
            }
        },
        yAxis: {
            labels: {
                style: chartStyle
            }
        }
    }, component.get( 'options' ) || {} );

    assert.deepEqual(
        options,
        component.get( 'highchartsOptions' ),
        'highchartsOptions returns expected options'
    );
});

test('title property is not missing in highchartsOptions and set to null', function( assert ) {
    const component = this.subject({
        options: testOptions,
        series: testSeries
    });

    assert.strictEqual(
        null,
        component.get( 'highchartsOptions' ).title,
        'title property in highchartsOptions is set to null in order to supress default behavior for our usage'
    );

});<commit_msg>deleted extra line in last test function in sl-chart-test.js<commit_after>import Ember from 'ember';
import { moduleForComponent, test } from 'ember-qunit';

const testOptions = {
    chartOptions: {
        chart: {
            type: 'bar'
        },

        xAxis: {
            categories: [ 'Apples', 'Bananas', 'Oranges' ]
        },

        yAxis: {
            title: {
                text: 'Fruit Eaten'
            }
        }
    }
};

const testSeries = [{
    name: 'Alice',
    data: [ 1, 0, 4 ]
}];

moduleForComponent( 'sl-chart', 'Unit | Component | sl chart', {
    unit: true
});

test( 'Default classNames are present', function( assert ) {
    this.subject({
        options: testOptions,
        series: testSeries
    });

    assert.ok(
        this.$().hasClass( 'panel' ),
        'Default rendered component has class "panel"'
    );

    assert.ok(
        this.$().hasClass( 'panel-default' ),
        'Default rendered component has class "panel-default"'
    );

    assert.ok(
        this.$().hasClass( 'sl-chart' ),
        'Default rendered component has class "sl-chart"'
    );

    assert.ok(
        this.$().hasClass( 'sl-panel' ),
        'Default rendered component has class "sl-panel"'
    );
});

test( 'Loading state adds loading class', function( assert ) {
    const component = this.subject({
        options: testOptions,
        series: testSeries
    });

    assert.strictEqual(
        this.$().hasClass( 'sl-loading' ),
        false,
        'Default rendered component does not have "sl-loading" class'
    );

    Ember.run( () => {
        component.set( 'isLoading', true );
    });

    assert.ok(
        this.$().hasClass( 'sl-loading' ),
        'Rendered component gains "sl-loading" class'
    );
});

test( 'updateData() is called after series property is modified', function( assert ) {
    const component = this.subject({
        options: testOptions,
        series: testSeries
    });

    this.render();

    const spy = sinon.spy( component, 'updateData' );
    const changedTestSeries = [];

    component.set( 'series' , changedTestSeries );

    assert.ok(
        spy.calledOnce,
        'updateData() is called once after series modified'
    );
});

test( '"Options" property needs to be an object', function( assert ) {

    // Null
    let assertionThrown = false;

    try {
        this.subject({
            options: null,
            series: testSeries
        });
    } catch( error ) {
        assertionThrown = true;
    }

    assert.ok(
        assertionThrown,
        'property was null'
    );

    // Array
    assertionThrown = false;

    try {
        this.subject({
            options: [],
            series: testSeries
        });
    } catch( error ) {
        assertionThrown = true;
    }

    assert.ok(
        assertionThrown,
        'property was an array'
    );

    // String
    assertionThrown = false;

    try {
        this.subject({
            options: "string",
            series: testSeries
        });
    } catch( error ) {
        assertionThrown = true;
    }

    assert.ok(
        assertionThrown,
        'property was string'
    );

    // Undefined
    assertionThrown = false;

    try {
        this.subject({
            options: undefined,
            series: testSeries
        });
    } catch( error ) {
        assertionThrown = true;
    }

    assert.ok(
        assertionThrown,
        'property was undefined'
    );

    // Boolean
    assertionThrown = false;

    try {
        this.subject({
            options: true,
            series: testSeries
        });
    } catch( error ) {
        assertionThrown = true;
    }

    assert.ok(
        assertionThrown,
        'property was a boolean'
    );

    // Number
    assertionThrown = false;

    try {
        this.subject({
            options: 123,
            series: testSeries
        });
    } catch( error ) {
        assertionThrown = true;
    }

    assert.ok(
        assertionThrown,
        'property was number'
    );

    // Function
    assertionThrown = false;

    try {
        this.subject({
            options: function(){},
            series: testSeries
        });
    } catch( error ) {
        assertionThrown = true;
    }

    assert.ok(
        assertionThrown,
        'property was a function'
    );

    // Object
    assertionThrown = false;

    try {
        this.subject({
            options: testOptions,
            series: testSeries
        });
    } catch( error ) {
        assertionThrown = true;
    }

    assert.ok(
        !assertionThrown,
        'property was an Object'
    );

    // Symbol
    assertionThrown = false;

    try {
        const sym = Symbol( 'foo' );
        this.subject({
            options: sym,
            series: testSeries
        });
    } catch( error ) {
        assertionThrown = true;
    }

    assert.ok(
        assertionThrown,
        'property was a symbol'
    );
});

test( '"Series" property needs to be an array', function( assert ) {

    // Null
    let assertionThrown = false;

    try {
        this.subject({
            options: testOptions,
            series: null
        });
    } catch( error ) {
        assertionThrown = true;
    }

    assert.ok(
        assertionThrown,
        'property was null'
    );

    // Object
    assertionThrown = false;

    try {
        this.subject({
            options: testOptions,
            series: {}
        });
    } catch( error ) {
        assertionThrown = true;
    }

    assert.ok(
        assertionThrown,
        'property was an object'
    );

    // Symbol
    assertionThrown = false;

    try {
        const sym = Symbol( 'foo' );
        this.subject({
            options: testOptions,
            series: sym
        });
    } catch( error ) {
        assertionThrown = true;
    }

    assert.ok(
        assertionThrown,
        'property was a symbol'
    );

    // String
    assertionThrown = false;

    try {
        this.subject({
            options: testOptions,
            series: "string"
        });
    } catch( error ) {
        assertionThrown = true;
    }

    assert.ok(
        assertionThrown,
        'property was string'
    );

    // Undefined
    assertionThrown = false;

    try {
        this.subject({
            options: testOptions,
            series: undefined
        });
    } catch( error ) {
        assertionThrown = true;
    }

    assert.ok(
        assertionThrown,
        'property was undefined'
    );

    // Boolean
    assertionThrown = false;

    try {
        this.subject({
            options: testOptions,
            series: true
        });
    } catch( error ) {
        assertionThrown = true;
    }

    assert.ok(
        assertionThrown,
        'property was a boolean'
    );

    // Number
    assertionThrown = false;

    try {
        this.subject({
            options: testOptions,
            series: 123
        });
    } catch( error ) {
        assertionThrown = true;
    }

    assert.ok(
        assertionThrown,
        'property was number'
    );

    // Function
    assertionThrown = false;

    try {
        this.subject({
            options: testOptions,
            series: function(){}
        });
    } catch( error ) {
        assertionThrown = true;
    }

    assert.ok(
        assertionThrown,
        'property was a function'
    );

    // Array
    assertionThrown = false;

    try {
        this.subject({
            options: testOptions,
            series: testSeries
        });
    } catch( error ) {
        assertionThrown = true;
    }

    assert.ok(
        !assertionThrown,
        'property was an Array'
    );
});

test( "Chart div uses the correct style", function( assert ) {
    const component = this.subject({
        options: testOptions,
        series: testSeries
    });

    assert.equal(
        component.get( 'style' ),
        'height: auto; width: auto;'
    );

    assert.equal(
        this.$( 'div.chart' ).attr( 'style' ),
        component.get( 'style' ),
        'Chart div has automatic height and width'
    );

    Ember.run( () => {
        component.set( 'height', 10 );
        component.set( 'width', 20 );
    });

    assert.equal(
        component.get( 'style' ),
        'height: 10; width: 20;'
    );

    assert.equal(
        this.$( 'div.chart' ).attr( 'style' ),
        component.get( 'style' ),
        'Chart div has height 10 and width 20'
    );
});

test( "Title property is set", function( assert ) {
    const component = this.subject({
        options: testOptions,
        series: testSeries
    });

    assert.strictEqual(
        this.$( '.panel-heading' )[0],
        undefined,
        'No chart title is rendered when title is not set'
    );

    const testTitle = "Peak server hours";

    Ember.run( () => {
        component.set( 'title', testTitle );
    });

    assert.equal(
        this.$( '.panel-heading' ).html(),
        testTitle,
        'Chart title is created with title value'
    );
});

test( "setupChart initializes chart and updates data upon render", function( assert ) {
    const chartTest = "a test chart";
    const chartDivMock = {
        highcharts( options ) {
            if ( Ember.typeOf( options ) === "undefined" ) {
                return chartTest;
            } else {
                return null;
            }
        }
    };

    const component = this.subject({
        options: testOptions,
        series: testSeries,
        $: function() {
            return chartDivMock;
        },
        updateData: function() {
            return;
        }
    });

    const setupSpy = sinon.spy( component, 'setupChart' );
    const updateSpy = sinon.spy( component, 'updateData' );
    const highchartsSpy = sinon.spy( chartDivMock, 'highcharts' );

    assert.equal(
        component.get( 'chart' ),
        null,
        'Chart is null upon initilization'
    );

    this.render();

    assert.ok(
        setupSpy.calledOnce,
        'setupChart was called once upon render'
    );

    assert.ok(
        updateSpy.calledOnce,
        'updateData was called once upon render'
    );

    assert.ok(
        highchartsSpy.calledTwice,
        'highcharts was called twice upon render'
    );

    assert.ok(
        highchartsSpy.calledWithExactly( component.get( 'highchartsOptions' ) ),
        'highcharts was called once with options'
    );

    assert.ok(
        highchartsSpy.calledWithExactly(),
        'highcharts was called once with no parameters'
    );

    assert.equal(
        component.get( 'chart' ),
        chartTest,
        'chart is initialized'
    );
});

test( 'highchartsOptions returns expected options', function( assert ) {
    const component = this.subject({
        options: testOptions,
        series: testSeries
    });
    const chartStyle = {
        fontFamily: '"Benton Sans", "Helvetica Neue", Helvetica, Arial, sans-serif',
        fontSize: '13px'
    };
    const options = Ember.$.extend( true, {
        chart: {
            animation: false,
            backgroundColor: 'rgba(255, 255, 255, 0)',
            style: chartStyle
        },
        title: null,
        colors: [
            '#298fce',
            '#94302e',
            '#00a14b',
            '#f29c1e',
            '#fadb00',
            '#34495d'
        ],
        credits: {
            enabled: false
        },
        legend: {
            itemStyle: chartStyle
        },
        plotOptions: {
            bar: {
                borderColor: 'transparent'
            },
            series: {
                animation: false
            }
        },
        tooltip: {
            animation: false,
            backgroundColor: 'rgba(0, 0, 0, 0.8)',
            borderWidth: 0,
            shadow: false,
            style: {
                color: '#fff'
            }
        },
        xAxis: {
            labels: {
                style: chartStyle
            }
        },
        yAxis: {
            labels: {
                style: chartStyle
            }
        }
    }, component.get( 'options' ) || {} );

    assert.deepEqual(
        options,
        component.get( 'highchartsOptions' ),
        'highchartsOptions returns expected options'
    );
});

test('title property is not missing in highchartsOptions and set to null', function( assert ) {
    const component = this.subject({
        options: testOptions,
        series: testSeries
    });

    assert.strictEqual(
        component.get( 'highchartsOptions' ).title,
        null,
        'title property in highchartsOptions is set to null in order to supress default behavior for our usage'
    );
});<|endoftext|>


<commit_before>require("./test_helper.js");
require("../../lib/handler_stack.js");

context("handlerStack",
  setup(() => {
    stub(global, "DomUtils", {});
    stub(DomUtils, "consumeKeyup", () => {});
    stub(DomUtils, "suppressEvent", () => {});
    stub(DomUtils, "suppressPropagation", () => {});
    this.handlerStack = new HandlerStack;
    this.handler1Called = false;
    this.handler2Called = false;
  }),

  should("bubble events", () => {
    this.handlerStack.push({ keydown: () => { return this.handler1Called = true; } });
    this.handlerStack.push({ keydown: () => { return this.handler2Called = true; } });
    this.handlerStack.bubbleEvent('keydown', {});
    assert.isTrue(this.handler2Called);
    assert.isTrue(this.handler1Called);
  }),

  should("terminate bubbling on falsy return value", () => {
    this.handlerStack.push({ keydown: () => { return this.handler1Called = true; } });
    this.handlerStack.push({
      keydown: () => {
      this.handler2Called = true;
      return false;
      }
    });
    this.handlerStack.bubbleEvent('keydown', {});
    assert.isTrue(this.handler2Called);
    assert.isFalse(this.handler1Called);
  }),

  should("terminate bubbling on passEventToPage, and be true", () => {
    this.handlerStack.push({ keydown: () => { return this.handler1Called = true; } });
    this.handlerStack.push({
      keydown: () => {
        this.handler2Called = true;
        return this.handlerStack.passEventToPage;
      }
    });
    assert.isTrue(this.handlerStack.bubbleEvent('keydown', {}));
    assert.isTrue(this.handler2Called);
    assert.isFalse(this.handler1Called);
  }),

  should("terminate bubbling on passEventToPage, and be false", () => {
    this.handlerStack.push({ keydown: () => { return this.handler1Called = true; } });
    this.handlerStack.push({
      keydown: () => {
        this.handler2Called = true;
        return this.handlerStack.suppressPropagation;
      }
    });
    assert.isFalse(this.handlerStack.bubbleEvent('keydown', {}));
    assert.isTrue(this.handler2Called);
    assert.isFalse(this.handler1Called);
  }),

  should("restart bubbling on restartBubbling", () => {
    this.handler1Called = 0;
    this.handler2Called = 0;
    var id = this.handlerStack.push({
      keydown: () => {
        this.handler1Called++;
        this.handlerStack.remove(id);
        return this.handlerStack.restartBubbling;
      }
    });
    this.handlerStack.push({
      keydown: () => {
        this.handler2Called++;
        return true;
      }
    });
    assert.isTrue(this.handlerStack.bubbleEvent('keydown', {}));
    assert.isTrue(this.handler1Called === 1);
    assert.isTrue(this.handler2Called === 2);
  }),

  should("remove handlers correctly", () => {
    this.handlerStack.push({ keydown: () => { this.handler1Called = true; } });
    const handlerId = this.handlerStack.push({ keydown: () => { this.handler2Called = true; } });
    this.handlerStack.remove(handlerId);
    this.handlerStack.bubbleEvent('keydown', {});
    assert.isFalse(this.handler2Called);
    assert.isTrue(this.handler1Called);
  }),

  should("remove handlers correctly", () => {
    const handlerId = this.handlerStack.push({ keydown: () => { this.handler1Called = true; } });
    this.handlerStack.push({ keydown: () => { this.handler2Called = true; } });
    this.handlerStack.remove(handlerId);
    this.handlerStack.bubbleEvent('keydown', {});
    assert.isTrue(this.handler2Called);
    assert.isFalse(this.handler1Called);
  }),

  should("handle self-removing handlers correctly", () => {
    const ctx = this;
    this.handlerStack.push({ keydown: () => { this.handler1Called = true; } });
    this.handlerStack.push({ keydown() {
      ctx.handler2Called = true;
      this.remove();
      return true;
    }
    });
    this.handlerStack.bubbleEvent('keydown', {});
    assert.isTrue(this.handler2Called);
    assert.isTrue(this.handler1Called);
    assert.equal(this.handlerStack.stack.length, 1);
  })
);
<commit_msg>Port handler_stacker_test to new shoulda.js syntax<commit_after>require("./test_helper.js");
require("../../lib/handler_stack.js");

context("handlerStack", () => {
  let handlerStack, handler1Called, handler2Called;

  setup(() => {
    stub(global, "DomUtils", {});
    stub(DomUtils, "consumeKeyup", () => {});
    stub(DomUtils, "suppressEvent", () => {});
    stub(DomUtils, "suppressPropagation", () => {});
    handlerStack = new HandlerStack;
    handler1Called = false;
    handler2Called = false;
  });

  should("bubble events", () => {
    handlerStack.push({ keydown: () => { return handler1Called = true; } });
    handlerStack.push({ keydown: () => { return handler2Called = true; } });
    handlerStack.bubbleEvent('keydown', {});
    assert.isTrue(handler2Called);
    assert.isTrue(handler1Called);
  });

  should("terminate bubbling on falsy return value", () => {
    handlerStack.push({ keydown: () => { return handler1Called = true; } });
    handlerStack.push({
      keydown: () => {
      handler2Called = true;
      return false;
      }
    });
    handlerStack.bubbleEvent('keydown', {});
    assert.isTrue(handler2Called);
    assert.isFalse(handler1Called);
  });

  should("terminate bubbling on passEventToPage, and be true", () => {
    handlerStack.push({ keydown: () => { return handler1Called = true; } });
    handlerStack.push({
      keydown: () => {
        handler2Called = true;
        return handlerStack.passEventToPage;
      }
    });
    assert.isTrue(handlerStack.bubbleEvent('keydown', {}));
    assert.isTrue(handler2Called);
    assert.isFalse(handler1Called);
  });

  should("terminate bubbling on passEventToPage, and be false", () => {
    handlerStack.push({ keydown: () => { return handler1Called = true; } });
    handlerStack.push({
      keydown: () => {
        handler2Called = true;
        return handlerStack.suppressPropagation;
      }
    });
    assert.isFalse(handlerStack.bubbleEvent('keydown', {}));
    assert.isTrue(handler2Called);
    assert.isFalse(handler1Called);
  });

  should("restart bubbling on restartBubbling", () => {
    handler1Called = 0;
    handler2Called = 0;
    var id = handlerStack.push({
      keydown: () => {
        handler1Called++;
        handlerStack.remove(id);
        return handlerStack.restartBubbling;
      }
    });
    handlerStack.push({
      keydown: () => {
        handler2Called++;
        return true;
      }
    });
    assert.isTrue(handlerStack.bubbleEvent('keydown', {}));
    assert.isTrue(handler1Called === 1);
    assert.isTrue(handler2Called === 2);
  });

  should("remove handlers correctly", () => {
    handlerStack.push({ keydown: () => { handler1Called = true; } });
    const handlerId = handlerStack.push({ keydown: () => { handler2Called = true; } });
    handlerStack.remove(handlerId);
    handlerStack.bubbleEvent('keydown', {});
    assert.isFalse(handler2Called);
    assert.isTrue(handler1Called);
  });

  should("remove handlers correctly", () => {
    const handlerId = handlerStack.push({ keydown: () => { handler1Called = true; } });
    handlerStack.push({ keydown: () => { handler2Called = true; } });
    handlerStack.remove(handlerId);
    handlerStack.bubbleEvent('keydown', {});
    assert.isTrue(handler2Called);
    assert.isFalse(handler1Called);
  });

  should("handle self-removing handlers correctly", () => {
    handlerStack.push({ keydown: () => { handler1Called = true; } });
    handlerStack.push({
      keydown() {
        handler2Called = true;
        this.remove();
        return true;
      }
    });
    handlerStack.bubbleEvent('keydown', {});
    assert.isTrue(handler2Called);
    assert.isTrue(handler1Called);
    assert.equal(handlerStack.stack.length, 1);
  });
});
<|endoftext|>


<commit_before>/**
 * Copyright 2013-2015, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @providesModule React
 */

/* globals __REACT_DEVTOOLS_GLOBAL_HOOK__*/

'use strict';

var EventPluginUtils = require('EventPluginUtils');
var ReactChildren = require('ReactChildren');
var ReactComponent = require('ReactComponent');
var ReactClass = require('ReactClass');
var ReactContext = require('ReactContext');
var ReactCurrentOwner = require('ReactCurrentOwner');
var ReactElement = require('ReactElement');
var ReactElementValidator = require('ReactElementValidator');
var ReactDOM = require('ReactDOM');
var ReactDOMTextComponent = require('ReactDOMTextComponent');
var ReactDefaultInjection = require('ReactDefaultInjection');
var ReactInstanceHandles = require('ReactInstanceHandles');
var ReactMount = require('ReactMount');
var ReactPerf = require('ReactPerf');
var ReactPropTypes = require('ReactPropTypes');
var ReactReconciler = require('ReactReconciler');
var ReactServerRendering = require('ReactServerRendering');

var assign = require('Object.assign');
var findDOMNode = require('findDOMNode');
var onlyChild = require('onlyChild');

ReactDefaultInjection.inject();

var createElement = ReactElement.createElement;
var createFactory = ReactElement.createFactory;
var cloneElement = ReactElement.cloneElement;

if (__DEV__) {
  createElement = ReactElementValidator.createElement;
  createFactory = ReactElementValidator.createFactory;
  cloneElement = ReactElementValidator.cloneElement;
}

var render = ReactPerf.measure('React', 'render', ReactMount.render);

var React = {
  Children: {
    map: ReactChildren.map,
    forEach: ReactChildren.forEach,
    count: ReactChildren.count,
    only: onlyChild
  },
  Component: ReactComponent,
  DOM: ReactDOM,
  PropTypes: ReactPropTypes,
  initializeTouchEvents: function(shouldUseTouch) {
    EventPluginUtils.useTouchEvents = shouldUseTouch;
  },
  createClass: ReactClass.createClass,
  createElement: createElement,
  cloneElement: cloneElement,
  createFactory: createFactory,
  createMixin: function(mixin) {
    // Currently a noop. Will be used to validate and trace mixins.
    return mixin;
  },
  constructAndRenderComponent: ReactMount.constructAndRenderComponent,
  constructAndRenderComponentByID: ReactMount.constructAndRenderComponentByID,
  findDOMNode: findDOMNode,
  render: render,
  renderToString: ReactServerRendering.renderToString,
  renderToStaticMarkup: ReactServerRendering.renderToStaticMarkup,
  unmountComponentAtNode: ReactMount.unmountComponentAtNode,
  isValidElement: ReactElement.isValidElement,
  withContext: ReactContext.withContext,

  // Hook for JSX spread, don't use this for anything else.
  __spread: assign
};

// Inject the runtime into a devtools global hook regardless of browser.
// Allows for debugging when the hook is injected on the page.
if (
  typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined' &&
  typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.inject === 'function') {
  __REACT_DEVTOOLS_GLOBAL_HOOK__.inject({
    CurrentOwner: ReactCurrentOwner,
    InstanceHandles: ReactInstanceHandles,
    Mount: ReactMount,
    Reconciler: ReactReconciler,
    TextComponent: ReactDOMTextComponent
  });
}

if (__DEV__) {
  var ExecutionEnvironment = require('ExecutionEnvironment');
  if (ExecutionEnvironment.canUseDOM && window.top === window.self) {

    // If we're in Chrome, look for the devtools marker and provide a download
    // link if not installed.
    if (navigator.userAgent.indexOf('Chrome') > -1) {
      if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ === 'undefined') {
        console.debug(
          'Download the React DevTools for a better development experience: ' +
          'http://fb.me/react-devtools'
        );
      }
    }

    // If we're in IE8, check to see if we are in combatibility mode and provide
    // information on preventing compatibility mode
    if (document.documentMode && document.documentMode < 8) {
      console.debug(
        'Internet Explorer is running in compatibility mode, please add the following ' +
        'tag to your HTML to prevent this from happening: ' +
        '<meta http-equiv="X-UA-Compatible" content="IE=edge" />'
      );
    }

    var expectedFeatures = [
      // shims
      Array.isArray,
      Array.prototype.every,
      Array.prototype.forEach,
      Array.prototype.indexOf,
      Array.prototype.map,
      Date.now,
      Function.prototype.bind,
      Object.keys,
      String.prototype.split,
      String.prototype.trim,

      // shams
      Object.create,
      Object.freeze
    ];

    for (var i = 0; i < expectedFeatures.length; i++) {
      if (!expectedFeatures[i]) {
        console.error(
          'One or more ES5 shim/shams expected by React are not available: ' +
          'http://fb.me/react-warning-polyfills'
        );
        break;
      }
    }
  }
}

React.version = '0.13.0-rc2';

module.exports = React;
<commit_msg>use warning module instead of console.debug<commit_after>/**
 * Copyright 2013-2015, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @providesModule React
 */

/* globals __REACT_DEVTOOLS_GLOBAL_HOOK__*/

'use strict';

var EventPluginUtils = require('EventPluginUtils');
var ReactChildren = require('ReactChildren');
var ReactComponent = require('ReactComponent');
var ReactClass = require('ReactClass');
var ReactContext = require('ReactContext');
var ReactCurrentOwner = require('ReactCurrentOwner');
var ReactElement = require('ReactElement');
var ReactElementValidator = require('ReactElementValidator');
var ReactDOM = require('ReactDOM');
var ReactDOMTextComponent = require('ReactDOMTextComponent');
var ReactDefaultInjection = require('ReactDefaultInjection');
var ReactInstanceHandles = require('ReactInstanceHandles');
var ReactMount = require('ReactMount');
var ReactPerf = require('ReactPerf');
var ReactPropTypes = require('ReactPropTypes');
var ReactReconciler = require('ReactReconciler');
var ReactServerRendering = require('ReactServerRendering');

var assign = require('Object.assign');
var findDOMNode = require('findDOMNode');
var onlyChild = require('onlyChild');
var warning = require('warning');

ReactDefaultInjection.inject();

var createElement = ReactElement.createElement;
var createFactory = ReactElement.createFactory;
var cloneElement = ReactElement.cloneElement;

if (__DEV__) {
  createElement = ReactElementValidator.createElement;
  createFactory = ReactElementValidator.createFactory;
  cloneElement = ReactElementValidator.cloneElement;
}

var render = ReactPerf.measure('React', 'render', ReactMount.render);

var React = {
  Children: {
    map: ReactChildren.map,
    forEach: ReactChildren.forEach,
    count: ReactChildren.count,
    only: onlyChild
  },
  Component: ReactComponent,
  DOM: ReactDOM,
  PropTypes: ReactPropTypes,
  initializeTouchEvents: function(shouldUseTouch) {
    EventPluginUtils.useTouchEvents = shouldUseTouch;
  },
  createClass: ReactClass.createClass,
  createElement: createElement,
  cloneElement: cloneElement,
  createFactory: createFactory,
  createMixin: function(mixin) {
    // Currently a noop. Will be used to validate and trace mixins.
    return mixin;
  },
  constructAndRenderComponent: ReactMount.constructAndRenderComponent,
  constructAndRenderComponentByID: ReactMount.constructAndRenderComponentByID,
  findDOMNode: findDOMNode,
  render: render,
  renderToString: ReactServerRendering.renderToString,
  renderToStaticMarkup: ReactServerRendering.renderToStaticMarkup,
  unmountComponentAtNode: ReactMount.unmountComponentAtNode,
  isValidElement: ReactElement.isValidElement,
  withContext: ReactContext.withContext,

  // Hook for JSX spread, don't use this for anything else.
  __spread: assign
};

// Inject the runtime into a devtools global hook regardless of browser.
// Allows for debugging when the hook is injected on the page.
if (
  typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined' &&
  typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.inject === 'function') {
  __REACT_DEVTOOLS_GLOBAL_HOOK__.inject({
    CurrentOwner: ReactCurrentOwner,
    InstanceHandles: ReactInstanceHandles,
    Mount: ReactMount,
    Reconciler: ReactReconciler,
    TextComponent: ReactDOMTextComponent
  });
}

if (__DEV__) {
  var ExecutionEnvironment = require('ExecutionEnvironment');
  if (ExecutionEnvironment.canUseDOM && window.top === window.self) {

    // If we're in Chrome, look for the devtools marker and provide a download
    // link if not installed.
    if (navigator.userAgent.indexOf('Chrome') > -1) {
      if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ === 'undefined') {
        console.debug(
          'Download the React DevTools for a better development experience: ' +
          'http://fb.me/react-devtools'
        );
      }
    }

    // If we're in IE8, check to see if we are in combatibility mode and provide
    // information on preventing compatibility mode
    var ieCompatibilityMode = document.documentMode && document.documentMode < 8;

    warning(
      !ieCompatibilityMode,
      'Internet Explorer is running in compatibility mode, please add the following ' +
      'tag to your HTML to prevent this from happening: ' +
      '<meta http-equiv="X-UA-Compatible" content="IE=edge" />'
    );

    var expectedFeatures = [
      // shims
      Array.isArray,
      Array.prototype.every,
      Array.prototype.forEach,
      Array.prototype.indexOf,
      Array.prototype.map,
      Date.now,
      Function.prototype.bind,
      Object.keys,
      String.prototype.split,
      String.prototype.trim,

      // shams
      Object.create,
      Object.freeze
    ];

    for (var i = 0; i < expectedFeatures.length; i++) {
      if (!expectedFeatures[i]) {
        console.error(
          'One or more ES5 shim/shams expected by React are not available: ' +
          'http://fb.me/react-warning-polyfills'
        );
        break;
      }
    }
  }
}

React.version = '0.13.0-rc2';

module.exports = React;
<|endoftext|>


<commit_before>"use strict";

const $ = require("jquery");

const unsee = require("./unsee");

var selectors = {
    button: "button.silence-delete"
};

function unsilenceButtonByID(alertmanagerURI, silenceID) {
    var amSelector = "[data-alertmanager-uri='" + alertmanagerURI + "']";
    var silenceSelector = "[data-silence-id='" + silenceID + "']";
    return $(selectors.button + amSelector + silenceSelector);
}

function markInProgress(alertmanagerURI, silenceID) {
    var elem = unsilenceButtonByID(alertmanagerURI, silenceID);
    elem.attr("title", "Silence is being deleted from Alertmanager");
    elem.tooltip("fixTitle");
    elem.find(".fa").removeClass("fa-times").addClass("fa-refresh fa-spin");
}

function markFailed(alertmanagerURI, silenceID, xhr) {
    var err = unsee.parseAJAXError(xhr, "Failed to delete this silence from Alertmanager");
    var elem = unsilenceButtonByID(alertmanagerURI, silenceID);
    elem.attr("title", err);
    elem.tooltip("fixTitle");
    elem.find(".fa").removeClass("fa-times fa-refresh fa-spin").addClass("fa-exclamation-circle text-danger");

    // Disable button, wait 5s and reset button to the original state
    elem.data("disabled", "true");
    setTimeout(function() {
        elem.find(".fa").removeClass("fa-exclamation-circle text-danger").addClass("fa-times");
        elem.removeData("disabled");
        elem.attr("title", "Delete this silence");
        elem.tooltip("fixTitle");
    }, 5000);
}

function markSuccess(alertmanagerURI, silenceID) {
    var elem = unsilenceButtonByID(alertmanagerURI, silenceID);
    elem.attr("title", "Silence deleted from Alertmanager");
    elem.tooltip("fixTitle");
    elem.find(".fa").removeClass("fa-times fa-refresh fa-spin").addClass("fa-check-circle text-success");
    // disable button so it's no longer clickable
    elem.data("disabled", "true");
}

function deleteSilence(alertmanagerURI, silenceID) {
    $.ajax({
        type: "DELETE",
        url: alertmanagerURI + "/api/v1/silence/" + silenceID,
        error: function(xhr) {
            markFailed(alertmanagerURI, silenceID, xhr);
        },
        success: function() {
            markSuccess(alertmanagerURI, silenceID);
        },
        dataType: "json"
    });
}

function init() {
    $("body").on("click", selectors.button, function(event) {
        var elem = $(event.currentTarget);

        if (elem.data("disabled")) {
            // if we marked button as disabled then skip all actions
            // we use data attr to keep tooplips working on disabled buttons
            // setting attr(disabled) via jquery disables bootstrap tooltips
            return false;
        }

        // hide tooltip for button that triggers this action
        elem.tooltip("hide");

        var amURI = elem.data("alertmanager-uri");
        var silenceID = elem.data("silence-id");

        // change icon to indicate progress
        markInProgress(amURI, silenceID);

        // send DELETE request to Alertmanager
        deleteSilence(amURI, silenceID);
    });
}

exports.init = init;
<commit_msg>Use correct icon<commit_after>"use strict";

const $ = require("jquery");

const unsee = require("./unsee");

var selectors = {
    button: "button.silence-delete"
};

function unsilenceButtonByID(alertmanagerURI, silenceID) {
    var amSelector = "[data-alertmanager-uri='" + alertmanagerURI + "']";
    var silenceSelector = "[data-silence-id='" + silenceID + "']";
    return $(selectors.button + amSelector + silenceSelector);
}

function markInProgress(alertmanagerURI, silenceID) {
    var elem = unsilenceButtonByID(alertmanagerURI, silenceID);
    elem.attr("title", "Silence is being deleted from Alertmanager");
    elem.tooltip("fixTitle");
    elem.find(".fa").removeClass("fa-trash-o").addClass("fa-refresh fa-spin");
}

function markFailed(alertmanagerURI, silenceID, xhr) {
    var err = unsee.parseAJAXError(xhr, "Failed to delete this silence from Alertmanager");
    var elem = unsilenceButtonByID(alertmanagerURI, silenceID);
    elem.attr("title", err);
    elem.tooltip("fixTitle");
    elem.find(".fa").removeClass("fa-trash-o fa-refresh fa-spin").addClass("fa-exclamation-circle text-danger");

    // Disable button, wait 5s and reset button to the original state
    elem.data("disabled", "true");
    setTimeout(function() {
        elem.find(".fa").removeClass("fa-exclamation-circle text-danger").addClass("fa-trash-o");
        elem.removeData("disabled");
        elem.attr("title", "Delete this silence");
        elem.tooltip("fixTitle");
    }, 5000);
}

function markSuccess(alertmanagerURI, silenceID) {
    var elem = unsilenceButtonByID(alertmanagerURI, silenceID);
    elem.attr("title", "Silence deleted from Alertmanager");
    elem.tooltip("fixTitle");
    elem.find(".fa").removeClass("fa-trash-o fa-refresh fa-spin").addClass("fa-check-circle text-success");
    // disable button so it's no longer clickable
    elem.data("disabled", "true");
}

function deleteSilence(alertmanagerURI, silenceID) {
    $.ajax({
        type: "DELETE",
        url: alertmanagerURI + "/api/v1/silence/" + silenceID,
        error: function(xhr) {
            markFailed(alertmanagerURI, silenceID, xhr);
        },
        success: function() {
            markSuccess(alertmanagerURI, silenceID);
        },
        dataType: "json"
    });
}

function init() {
    $("body").on("click", selectors.button, function(event) {
        var elem = $(event.currentTarget);

        if (elem.data("disabled")) {
            // if we marked button as disabled then skip all actions
            // we use data attr to keep tooplips working on disabled buttons
            // setting attr(disabled) via jquery disables bootstrap tooltips
            return false;
        }

        // hide tooltip for button that triggers this action
        elem.tooltip("hide");

        var amURI = elem.data("alertmanager-uri");
        var silenceID = elem.data("silence-id");

        // change icon to indicate progress
        markInProgress(amURI, silenceID);

        // send DELETE request to Alertmanager
        deleteSilence(amURI, silenceID);
    });
}

exports.init = init;
<|endoftext|>


<commit_before>var express = require('express');
var request = require('request');
var cheerio = require('cheerio');
var router = express.Router();
var Bookmark = require('../models/bookmark');
var HttpStatus = require('http-status-codes');
var MyError = require('../models/error');
const escapeStringRegexp = require('escape-string-regexp');

const MAX_NUMBER_RETURNED_RESULTS = 100;

/**
 *  Returns the public bookmarks
 */
router.get('/', async (req, res) => {
  try {
    if (req.query.term) {
      var regExpTerm = new RegExp(req.query.term, 'i');
      var regExpSearch = [{name: {$regex: regExpTerm}}, {description: {$regex: regExpTerm}}, {category: {$regex: regExpTerm}}, {tags: {$regex: regExpTerm}}];
      const bookmarks = await Bookmark.find({'$or': regExpSearch})
      res.send(bookmarks);
    } else if (req.query.query) {
      //split in text and tags

      const limit = parseInt(req.query.limit);
      const searchedTermsAndTags = splitSearchQuery(req.query.query);
      const searchedTerms = searchedTermsAndTags[0];
      const searchedTags = searchedTermsAndTags[1];
      let bookmarks = [];
      if (searchedTerms.length > 0 && searchedTags.length > 0) {
        bookmarks = await Bookmark.find({
          $and: [
            {
              tags:
                {
                  $all: searchedTags
                }
            },
            {
              $text:
                {
                  $search: searchedTerms.join(' ')
                }
            }
          ]
        }
          )
          .sort({createdAt: -1})
          .lean()
          .exec();
        if (searchedTerms.length > 1) {
          searchedTerms.forEach(term => {
            bookmarks = bookmarks.filter(x => bookmarkContainsSearchedTerm(x, term.trim()));
          });
        }
        bookmarks = bookmarks.slice(0, limit);
      } else if (searchedTerms.length > 0) {
        const termsJoined = searchedTerms.join(' ');
        const termsQuery = escapeStringRegexp(termsJoined);
        bookmarks = await Bookmark.find(
          {
            $text: {$search: termsQuery},
          },
          {
            score: {$meta: "textScore"}
          }
        )
        //.sort({createdAt: -1}) let's give it a try with text score
          .sort({score: {$meta: "textScore"}})
          .lean()
          .exec();
        //double check if multiple words it must contain both terms (capability not supported by mongo text index, it finds both)
        if (searchedTerms.length > 1) {
          searchedTerms.forEach(term => {
            bookmarks = bookmarks.filter(x => bookmarkContainsSearchedTerm(x, term.trim()));
          });
        }
        bookmarks = bookmarks.slice(0, limit);
      } else {
        bookmarks = await Bookmark.find({tags: {$all: searchedTags}}).sort({createdAt: -1}).limit(limit).lean().exec();
      }

      res.send(bookmarks);
    } else if (req.query.location) {
      const bookmark = await Bookmark.findOne({'shared': true, location: req.query.location}).lean().exec();
      if (!bookmark) {
        return res.status(HttpStatus.NOT_FOUND).send("Bookmark not found");
      }
      res.send(bookmark);
    } else if (req.query.tag) {//get all bookmarks tagged with "tag"
      const bookmarks = await Bookmark.find({tags: req.query.tag}).sort({createdAt: -1}).limit(MAX_NUMBER_RETURNED_RESULTS).lean().exec();
      res.send(bookmarks);
    } else {//no filter - all bookmarks ordered by creation date descending
      const bookmarks = await Bookmark.find({'shared': true})
        .sort({createdAt: -1})
        .limit(MAX_NUMBER_RETURNED_RESULTS)
        .lean().exec();
      res.send(bookmarks);
    }
  } catch (err) {
    return res.status(HttpStatus.INTERNAL_SERVER_ERROR).send(err);
  }

});

function bookmarkContainsSearchedTerm(bookmark, searchedTerm) {
  let result = false;
  // const escapedSearchPattern = '\\b' + this.escapeRegExp(searchedTerm.toLowerCase()) + '\\b'; word boundary was not enough, especially for special characters which can happen in coding
  // https://stackoverflow.com/questions/23458872/javascript-regex-word-boundary-b-issue
  const separatingChars = '\\s\\.,;#\\-\\/_\\[\\]\\(\\)\\*\\+';
  const escapedSearchPattern = `(^|[${separatingChars}])(${escapeRegExp(searchedTerm.toLowerCase())})(?=$|[${separatingChars}])`;
  const pattern = new RegExp(escapedSearchPattern);
  if ((bookmark.name && pattern.test(bookmark.name.toLowerCase()))
    || (bookmark.location && pattern.test(bookmark.location.toLowerCase()))
    || (bookmark.description && pattern.test(bookmark.description.toLowerCase()))
    || (bookmark.githubURL && pattern.test(bookmark.githubURL.toLowerCase()))
  ) {
    result = true;
  }

  if (result) {
    return true;
  } else {
    // if not found already look through the tags also
    bookmark.tags.forEach(tag => {
      if (pattern.test(tag.toLowerCase())) {
        result = true;
      }
    });
  }

  return result;
}

function escapeRegExp(str) {
  const specials = [
      // order matters for these
      '-'
      , '['
      , ']'
      // order doesn't matter for any of these
      , '/'
      , '{'
      , '}'
      , '('
      , ')'
      , '*'
      , '+'
      , '?'
      , '.'
      , '\\'
      , '^'
      , '$'
      , '|'
    ],
    regex = RegExp('[' + specials.join('\\') + ']', 'g');
  return str.replace(regex, '\\$&'); // $& means the whole matched string
}

/* GET title of bookmark given its url */
router.get('/scrape', function (req, res, next) {
  if (req.query.url) {
    request(req.query.url, function (error, response, body) {
      if (!error && response.statusCode == 200) {
        const $ = cheerio.load(body);
        const webpageTitle = $("title").text();
        const metaDescription = $('meta[name=description]').attr("content");
        const webpage = {
          title: webpageTitle,
          metaDescription: metaDescription
        }
        res.send(webpage);
      }
    });
  }
});

/**
 * Returns the bookmarks added recently.
 *
 * The since query parameter is a timestamp which specifies the date since we want to look forward to present time.
 * If this parameter is present it has priority. If it is not present, we might specify the number of days to look back via
 * the query parameter numberOfDays. If not present it defaults to 7 days, last week.
 *
 */
router.get('/latest-entries', async (req, res) => {
  try {
    if (req.query.since) {
      const fromDate = new Date(parseFloat(req.query.since, 0));
      const toDate = req.query.to ? new Date(parseFloat(req.query.to, 0)) : new Date();
      if (fromDate > toDate) {
        return res
          .status(HttpStatus.BAD_REQUEST)
          .send(new MyError('timing query parameters values', ['<Since> param value must be before <to> parameter value']));
      }
      const bookmarks = await Bookmark.find(
        {
          'shared': true,
          createdAt: {
            $gte: fromDate,
            $lte: toDate
          }

        }).sort({createdAt: 'desc'}).lean().exec();

      res.send(bookmarks);
    } else {
      const numberOfDaysToLookBack = req.query.days ? req.query.days : 7;

      const bookmarks = await Bookmark.find(
        {
          'shared': true,
          createdAt: {$gte: new Date((new Date().getTime() - (numberOfDaysToLookBack * 24 * 60 * 60 * 1000)))}
        }).sort({createdAt: 'desc'}).lean().exec();

      res.send(bookmarks);
    }

  } catch (err) {
    return res.status(HttpStatus.INTERNAL_SERVER_ERROR).send(err);
  }
});

/* GET bookmark by id. */
router.get('/:id', function (req, res, next) {
  Bookmark.findById(req.params.id, function (err, bookmark) {
    if (err) {
      return res.status(HttpStatus.INTERNAL_SERVER_ERROR).send(err);
    }
    if (!bookmark) {
      return res.status(HttpStatus.NOT_FOUND).send("Bookmark not found");
    }
    res.send(bookmark);
  });

});

/* TODO - maybe implement later advancedSearch */
router.get('/advanced-search', function (req, res, next) {
  var regexSearch = [];
  if (req.query.name) {
    var regExpName = new RegExp(req.query.category, 'i');
    regexSearch.push({'name': {$regex: regExpName}});
    regexSearch.push({'description': {$regex: regExpName}});
  }
  if (req.query.category) {
    var regExpCategory = new RegExp(req.query.category, 'i');
    regexSearch.push({'category': {$regex: regExpCategory}});
  }
  if (req.query.tag) {
    var regExpTag = new RegExp(req.query.tag, 'i');
    regexSearch.push({'tags': {$regex: regExpTag}});
  }
  if (regexSearch.length > 0) {
    Bookmark.find().or(regexSearch, function (err, bookmarks) {
      if (err) {
        return res.status(HttpStatus.INTERNAL_SERVER_ERROR).send(err);
      }
      res.send(bookmarks);
    });
  } else {//no filter - all bookmarks
    Bookmark.find({}, function (err, bookmarks) {
      if (err) {
        return res.status(HttpStatus.INTERNAL_SERVER_ERROR).send(err);
      }
      res.send(bookmarks);
    });
  }

});

function splitSearchQuery(query) {

  const result = [[], []];

  const terms = [];
  let term = '';
  const tags = [];
  let tag = '';

  let isInsideTerm = false;
  let isInsideTag = false;

  for (let i = 0; i < query.length; i++) {
    const currentCharacter = query[i];
    if (currentCharacter === ' ') {
      if (!isInsideTag) {
        if (!isInsideTerm) {
          continue;
        } else {
          terms.push(term);
          isInsideTerm = false;
          term = '';
        }
      } else {
        tag += ' ';
      }
    } else if (currentCharacter === '[') {
      if (isInsideTag) {
        tags.push(tag.trim());
        tag = '';
      } else {
        isInsideTag = true;
      }
    } else if (currentCharacter === ']') {
      if (isInsideTag) {
        isInsideTag = false;
        tags.push(tag.trim());
        tag = '';
      }
    } else {
      if (isInsideTag) {
        tag += currentCharacter;
      } else {
        isInsideTerm = true;
        term += currentCharacter;
      }
    }
  }

  if (tag.length > 0) {
    tags.push(tag.trim());
  }

  if (term.length > 0) {
    terms.push(term);
  }

  result[0] = terms;
  result[1] = tags;

  return result;
}

module.exports = router;
<commit_msg>refactor to make the main get method more readable<commit_after>var express = require('express');
var request = require('request');
var cheerio = require('cheerio');
var router = express.Router();
var Bookmark = require('../models/bookmark');
var HttpStatus = require('http-status-codes');
var MyError = require('../models/error');
const escapeStringRegexp = require('escape-string-regexp');

const MAX_NUMBER_RETURNED_RESULTS = 100;

/**
 *  Returns the public bookmarks
 */
router.get('/', async (req, res) => {
  try {
    if (req.query.query) {
      //split in text and tags
      const limit = parseInt(req.query.limit);
      const searchedTermsAndTags = splitSearchQuery(req.query.query);
      const searchedTerms = searchedTermsAndTags[0];
      const searchedTags = searchedTermsAndTags[1];
      let bookmarks = [];
      const lang = req.query.lang;
      if (searchedTerms.length > 0 && searchedTags.length > 0) {
        bookmarks = await getBookmarksForTagsAndTerms(bookmarks, searchedTags, searchedTerms);
      } else if (searchedTerms.length > 0) {
        bookmarks = await getBookmarksForSearchedTerms(searchedTerms, bookmarks, limit);
      } else {
        bookmarks = await getBookmarksforSearchedTags(bookmarks, searchedTags, limit);
      }
      if (lang && lang !== 'all') {
        bookmarks = bookmarks.filter(x => x.language === lang);
      }

      res.send(bookmarks);
    } else if (req.query.location) {
      const bookmark = await Bookmark.findOne({
        'shared': true,
        location: req.query.location
      }).lean().exec();
      if (!bookmark) {
        return res.status(HttpStatus.NOT_FOUND).send("Bookmark not found");
      }
      res.send(bookmark);
    } else {//no filter - all bookmarks ordered by creation date descending
      const bookmarks = await Bookmark.find({'shared': true})
        .sort({createdAt: -1})
        .limit(MAX_NUMBER_RETURNED_RESULTS)
        .lean().exec();
      res.send(bookmarks);
    }
  } catch (err) {
    return res.status(HttpStatus.INTERNAL_SERVER_ERROR).send(err);
  }
});

let getBookmarksForTagsAndTerms = async function (bookmarks, searchedTags, searchedTerms) {
  bookmarks = await Bookmark.find({
      $and: [
        {
          tags:
            {
              $all: searchedTags
            }
        },
        {
          $text:
            {
              $search: searchedTerms.join(' ')
            }
        }
      ]
    }
  )
    .sort({createdAt: -1})
    .lean()
    .exec();
  if (searchedTerms.length > 1) {
    searchedTerms.forEach(term => {
      bookmarks = bookmarks.filter(x => bookmarkContainsSearchedTerm(x, term.trim()));
    });
  }

  bookmarks = bookmarks.slice(0, limit);
  return bookmarks;
};

let getBookmarksForSearchedTerms = async function (searchedTerms, bookmarks, limit) {
  const termsJoined = searchedTerms.join(' ');
  const termsQuery = escapeStringRegexp(termsJoined);
  bookmarks = await Bookmark.find(
    {
      shared: true,
      $text: {$search: termsQuery},
    },
    {
      score: {$meta: "textScore"}
    }
  )
    .sort({createdAt: -1}) //let's give it a try with text score
    //.sort({score: {$meta: "textScore"}})
    .lean()
    .exec();

  searchedTerms.forEach(term => {
    bookmarks = bookmarks.filter(x => bookmarkContainsSearchedTerm(x, term.trim()));
  });

  bookmarks = bookmarks.slice(0, limit);

  return bookmarks;
};

let getBookmarksforSearchedTags = async function (bookmarks, searchedTags, limit) {
  console.log('searchedTags', searchedTags);
  bookmarks = await Bookmark.find(
    {
      shared: true,
      tags:
        {
          $all: searchedTags
        },
    }
  )
    .sort({createdAt: -1})
    .limit(limit)
    .lean()
    .exec();
  return bookmarks;
};

function bookmarkContainsSearchedTerm(bookmark, searchedTerm) {
  let result = false;
  // const escapedSearchPattern = '\\b' + this.escapeRegExp(searchedTerm.toLowerCase()) + '\\b'; word boundary was not enough, especially for special characters which can happen in coding
  // https://stackoverflow.com/questions/23458872/javascript-regex-word-boundary-b-issue
  const separatingChars = '\\s\\.,;#\\-\\/_\\[\\]\\(\\)\\*\\+';
  const escapedSearchPattern = `(^|[${separatingChars}])(${escapeRegExp(searchedTerm.toLowerCase())})(?=$|[${separatingChars}])`;
  const pattern = new RegExp(escapedSearchPattern);
  if ((bookmark.name && pattern.test(bookmark.name.toLowerCase()))
    || (bookmark.location && pattern.test(bookmark.location.toLowerCase()))
    || (bookmark.description && pattern.test(bookmark.description.toLowerCase()))
    || (bookmark.githubURL && pattern.test(bookmark.githubURL.toLowerCase()))
  ) {
    result = true;
  }

  if (result) {
    return true;
  } else {
    // if not found already look through the tags also
    bookmark.tags.forEach(tag => {
      if (pattern.test(tag.toLowerCase())) {
        result = true;
      }
    });
  }

  return result;
}

function escapeRegExp(str) {
  const specials = [
      // order matters for these
      '-'
      , '['
      , ']'
      // order doesn't matter for any of these
      , '/'
      , '{'
      , '}'
      , '('
      , ')'
      , '*'
      , '+'
      , '?'
      , '.'
      , '\\'
      , '^'
      , '$'
      , '|'
    ],
    regex = RegExp('[' + specials.join('\\') + ']', 'g');
  return str.replace(regex, '\\$&'); // $& means the whole matched string
}

router.get('/tagged/:tag', async (req, res) => {
  try {
    const bookmarks = await Bookmark.find({
      shared: true,
      tags: req.params.tag
    })
      .sort({createdAt: -1})
      .limit(MAX_NUMBER_RETURNED_RESULTS)
      .lean()
      .exec();

    res.send(bookmarks);

  } catch (err) {
    return res.status(HttpStatus.INTERNAL_SERVER_ERROR).send(err);
  }
});

/* GET title of bookmark given its url */
router.get('/scrape', function (req, res, next) {
  if (req.query.url) {
    request(req.query.url, function (error, response, body) {
      if (!error && response.statusCode == 200) {
        const $ = cheerio.load(body);
        const webpageTitle = $("title").text();
        const metaDescription = $('meta[name=description]').attr("content");
        const webpage = {
          title: webpageTitle,
          metaDescription: metaDescription
        }
        res.send(webpage);
      }
    });
  }
});

/**
 * Returns the bookmarks added recently.
 *
 * The since query parameter is a timestamp which specifies the date since we want to look forward to present time.
 * If this parameter is present it has priority. If it is not present, we might specify the number of days to look back via
 * the query parameter numberOfDays. If not present it defaults to 7 days, last week.
 *
 */
router.get('/latest-entries', async (req, res) => {
  try {
    if (req.query.since) {
      const fromDate = new Date(parseFloat(req.query.since, 0));
      const toDate = req.query.to ? new Date(parseFloat(req.query.to, 0)) : new Date();
      if (fromDate > toDate) {
        return res
          .status(HttpStatus.BAD_REQUEST)
          .send(new MyError('timing query parameters values', ['<Since> param value must be before <to> parameter value']));
      }
      const bookmarks = await Bookmark.find(
        {
          'shared': true,
          createdAt: {
            $gte: fromDate,
            $lte: toDate
          }

        }).sort({createdAt: 'desc'}).lean().exec();

      res.send(bookmarks);
    } else {
      const numberOfDaysToLookBack = req.query.days ? req.query.days : 7;

      const bookmarks = await Bookmark.find(
        {
          'shared': true,
          createdAt: {$gte: new Date((new Date().getTime() - (numberOfDaysToLookBack * 24 * 60 * 60 * 1000)))}
        }).sort({createdAt: 'desc'}).lean().exec();

      res.send(bookmarks);
    }

  } catch (err) {
    return res.status(HttpStatus.INTERNAL_SERVER_ERROR).send(err);
  }
});

/* GET bookmark by id. */
router.get('/:id', function (req, res, next) {
  Bookmark.findById(req.params.id, function (err, bookmark) {
    if (err) {
      return res.status(HttpStatus.INTERNAL_SERVER_ERROR).send(err);
    }
    if (!bookmark) {
      return res.status(HttpStatus.NOT_FOUND).send("Bookmark not found");
    }
    res.send(bookmark);
  });

});

/* TODO - maybe implement later advancedSearch */
router.get('/advanced-search', function (req, res, next) {
  var regexSearch = [];
  if (req.query.name) {
    var regExpName = new RegExp(req.query.category, 'i');
    regexSearch.push({'name': {$regex: regExpName}});
    regexSearch.push({'description': {$regex: regExpName}});
  }
  if (req.query.category) {
    var regExpCategory = new RegExp(req.query.category, 'i');
    regexSearch.push({'category': {$regex: regExpCategory}});
  }
  if (req.query.tag) {
    var regExpTag = new RegExp(req.query.tag, 'i');
    regexSearch.push({'tags': {$regex: regExpTag}});
  }
  if (regexSearch.length > 0) {
    Bookmark.find().or(regexSearch, function (err, bookmarks) {
      if (err) {
        return res.status(HttpStatus.INTERNAL_SERVER_ERROR).send(err);
      }
      res.send(bookmarks);
    });
  } else {//no filter - all bookmarks
    Bookmark.find({}, function (err, bookmarks) {
      if (err) {
        return res.status(HttpStatus.INTERNAL_SERVER_ERROR).send(err);
      }
      res.send(bookmarks);
    });
  }

});

function splitSearchQuery(query) {

  const result = [[], []];

  const terms = [];
  let term = '';
  const tags = [];
  let tag = '';

  let isInsideTerm = false;
  let isInsideTag = false;

  for (let i = 0; i < query.length; i++) {
    const currentCharacter = query[i];
    if (currentCharacter === ' ') {
      if (!isInsideTag) {
        if (!isInsideTerm) {
          continue;
        } else {
          terms.push(term);
          isInsideTerm = false;
          term = '';
        }
      } else {
        tag += ' ';
      }
    } else if (currentCharacter === '[') {
      if (isInsideTag) {
        tags.push(tag.trim());
        tag = '';
      } else {
        isInsideTag = true;
      }
    } else if (currentCharacter === ']') {
      if (isInsideTag) {
        isInsideTag = false;
        tags.push(tag.trim());
        tag = '';
      }
    } else {
      if (isInsideTag) {
        tag += currentCharacter;
      } else {
        isInsideTerm = true;
        term += currentCharacter;
      }
    }
  }

  if (tag.length > 0) {
    tags.push(tag.trim());
  }

  if (term.length > 0) {
    terms.push(term);
  }

  result[0] = terms;
  result[1] = tags;

  return result;
}

module.exports = router;
<|endoftext|>


<commit_before><commit_msg>Listing 3.7 - logging multiple players<commit_after><|endoftext|>


<commit_before>var app = angular.module('app', ['ngMessages'])

app.config(function($interpolateProvider) {
    // change up angular's templating syntax to avoid
    // collisions with Jinja2
    $interpolateProvider.startSymbol('{a');
    $interpolateProvider.endSymbol('a}');
});

// tweet_url, page_url and title_url are defined in the base template
// by Flask's url_for

function TweetService($http) {
    var TweetService = {};

    function tweets(term) {
        return $http.get(tweet_url + term);
    };

    TweetService.getTweets = tweets;
    return TweetService;

};

function WikiPageService($http) {
    var WikiPageService = {};

    function getTitles(term) {
        return $http.get(title_url + term);
    };

    function getPage(title) {
        return $http.get(page_url + title)
    }

    WikiPageService.getTitles = getTitles;
    WikiPageService.getPage = getPage
    return WikiPageService
}

function TermService($location) {
    var TermService = {};

    TermService.term = $location.hash().split('+').join(' ');
    return TermService;
}

function MainController(TweetService, WikiPageService) {
    var vm = this;

    vm.activated = false;

    function init() {
        vm.twitter = {
            loaded: false,
            error: false,
            tweets: []
        };

        vm.wiki = {
            loaded: false,
            error: false,
            pages: []
        };

    };

    this.submit = function(term) {
        init();
        vm.activated = true;
        getTweets(term);
        getPages(term);
    };

    function getTweets(term) {
        return TweetService.getTweets(term)
            .success(function(data, status, headers, config) {
                vm.twitter.loaded = true;
                vm.twitter.tweets = data.tweets
            })
            .error(function(data, status, headers, config) {
                vm.twitter.error = true;
                vm.twitter.msg = data.msg;
            });
    };

    function getPages(term) {
        return WikiPageService.getTitles(term)
            .success(function(data, status, headers, config) {
                vm.wiki.pages = data.titles;
                vm.wiki.loaded = true;

                for (i=0; i < vm.wiki.pages.length; ++i) {
                    getPage(vm.wiki.pages[i]);
                };
            })
            .error(function(data, status, headers, config) {
                vm.wiki.error = true;
                vm.wiki.msg = data.msg;
            });
    };

    function getPage(page) {
        return WikiPageService.getPage(page.title)
            .success(function(data, status, headers, config) {
                page.summary = data.page.summary
            })
            .error(function(data, status, headers, config) {
                page.summary = data.msg
            });
    }
};

app.factory('TweetService', TweetService);
app.factory('WikiPageService', WikiPageService);
app.factory('TermService', TermService);
app.controller('MainController', MainController);
<commit_msg>Remove extraneous service<commit_after>var app = angular.module('app', ['ngMessages'])

app.config(function($interpolateProvider) {
    // change up angular's templating syntax to avoid
    // collisions with Jinja2
    $interpolateProvider.startSymbol('{a');
    $interpolateProvider.endSymbol('a}');
});

// tweet_url, page_url and title_url are defined in the base template
// by Flask's url_for

function TweetService($http) {
    var TweetService = {};

    function tweets(term) {
        return $http.get(tweet_url + term);
    };

    TweetService.getTweets = tweets;
    return TweetService;

};

function WikiPageService($http) {
    var WikiPageService = {};

    function getTitles(term) {
        return $http.get(title_url + term);
    };

    function getPage(title) {
        return $http.get(page_url + title)
    }

    WikiPageService.getTitles = getTitles;
    WikiPageService.getPage = getPage
    return WikiPageService
}

function MainController(TweetService, WikiPageService) {
    var vm = this;

    vm.activated = false;

    function init() {
        vm.twitter = {
            loaded: false,
            error: false,
            tweets: []
        };

        vm.wiki = {
            loaded: false,
            error: false,
            pages: []
        };

    };

    this.submit = function(term) {
        init();
        vm.activated = true;
        getTweets(term);
        getPages(term);
    };

    function getTweets(term) {
        return TweetService.getTweets(term)
            .success(function(data, status, headers, config) {
                vm.twitter.loaded = true;
                vm.twitter.tweets = data.tweets
            })
            .error(function(data, status, headers, config) {
                vm.twitter.error = true;
                vm.twitter.msg = data.msg;
            });
    };

    function getPages(term) {
        return WikiPageService.getTitles(term)
            .success(function(data, status, headers, config) {
                vm.wiki.pages = data.titles;
                vm.wiki.loaded = true;

                for (i=0; i < vm.wiki.pages.length; ++i) {
                    getPage(vm.wiki.pages[i]);
                };
            })
            .error(function(data, status, headers, config) {
                vm.wiki.error = true;
                vm.wiki.msg = data.msg;
            });
    };

    function getPage(page) {
        return WikiPageService.getPage(page.title)
            .success(function(data, status, headers, config) {
                page.summary = data.page.summary
            })
            .error(function(data, status, headers, config) {
                page.summary = data.msg
            });
    }
};

app.factory('TweetService', TweetService);
app.factory('WikiPageService', WikiPageService);
app.factory('TermService', TermService);
app.controller('MainController', MainController);
<|endoftext|>