<commit_before>import Controller from '@ember/controller';
import config from 'ghost-admin/config/environment';
import fetch from 'fetch';
import ghostPaths from 'ghost-admin/utils/ghost-paths';
import moment from 'moment';
import {A} from '@ember/array';
import {action} from '@ember/object';
import {capitalize} from '@ember/string';
import {ghPluralize} from 'ghost-admin/helpers/gh-pluralize';
import {resetQueryParams} from 'ghost-admin/helpers/reset-query-params';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency-decorators';
import {timeout} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';

const PAID_PARAMS = [{
    name: 'All members',
    value: null
}, {
    name: 'Free members',
    value: 'false'
}, {
    name: 'Paid members',
    value: 'true'
}];

export default class MembersController extends Controller {
    @service ajax;
    @service config;
    @service ellaSparse;
    @service feature;
    @service ghostPaths;
    @service membersStats;
    @service router;
    @service store;

    queryParams = [
        'label',
        {paidParam: 'paid'},
        {searchParam: 'search'},
        {orderParam: 'order'},
        {filterParam: 'filter'}
    ];

    @tracked members = A([]);
    @tracked searchText = '';
    @tracked searchParam = '';
    @tracked filterParam = null;
    @tracked paidParam = null;
    @tracked label = null;
    @tracked orderParam = null;
    @tracked modalLabel = null;
    @tracked showLabelModal = false;
    @tracked showDeleteMembersModal = false;
    @tracked showUnsubscribeMembersModal = false;
    @tracked showAddMembersLabelModal = false;
    @tracked showRemoveMembersLabelModal = false;
    @tracked filters = A([]);

    @tracked _availableLabels = A([]);

    paidParams = PAID_PARAMS;

    constructor() {
        super(...arguments);
        this._availableLabels = this.store.peekAll('label');

        if (this.isTesting === undefined) {
            this.isTesting = config.environment === 'test';
        }
    }

    // Computed properties -----------------------------------------------------

    get listHeader() {
        let {searchText, selectedLabel, members} = this;

        if (members.loading) {
            return 'Loading...';
        }

        if (searchText) {
            return 'Search result';
        }

        let count = ghPluralize(members.length, 'member');

        if (selectedLabel && selectedLabel.slug) {
            if (members.length > 1) {
                return `${count} match current filter`;
            } else {
                return `${count} matches current filter`;
            }
        }

        return count;
    }

    get showingAll() {
        return !this.searchParam && !this.paidParam && !this.label && !this.filterParam;
    }

    get availableOrders() {
        // don't return anything if email analytics is disabled because
        // we don't want to show an order dropdown with only a single option

        if (this.feature.get('emailAnalytics')) {
            return [{
                name: 'Newest',
                value: null
            }, {
                name: 'Open rate',
                value: 'email_open_rate'
            }];
        }

        return [];
    }

    get selectedOrder() {
        return this.availableOrders.find(order => order.value === this.orderParam);
    }

    get availableLabels() {
        let labels = this._availableLabels
            .filter(label => !label.isNew)
            .filter(label => label.id !== null)
            .sort((labelA, labelB) => labelA.name.localeCompare(labelB.name, undefined, {ignorePunctuation: true}));
        let options = labels.toArray();

        options.unshiftObject({name: 'All labels', slug: null});

        return options;
    }

    get selectedLabel() {
        let {label, availableLabels} = this;
        return availableLabels.findBy('slug', label);
    }

    get labelModalData() {
        let label = this.modalLabel;
        let labels = this.availableLabels;

        return {
            label,
            labels
        };
    }

    get selectedPaidParam() {
        return this.paidParams.findBy('value', this.paidParam) || {value: '!unknown'};
    }

    get isFiltered() {
        return !!(this.label || this.paidParam || this.searchParam || this.filterParam);
    }

    get filterColumns() {
        const defaultColumns = ['name', 'email'];
        return this.filters.map((filter) => {
            return filter.type;
        }).filter((f, idx, arr) => {
            return arr.indexOf(f) === idx;
        }).filter(d => !defaultColumns.includes(d));
    }

    get filterColumnLabels() {
        return this.filterColumns.map((d) => {
            return capitalize(d.replace(/_/g, ' '));
        });
    }

    getApiQueryObject({params, extraFilters = []} = {}) {
        let {label, paidParam, searchParam, filterParam} = params ? params : this;

        let filters = [];

        filters = filters.concat(extraFilters);

        if (label) {
            filters.push(`label:'${label}'`);
        }

        if (paidParam !== null) {
            if (paidParam === 'true') {
                filters.push('status:-free');
            } else {
                filters.push('status:free');
            }
        }

        if (filterParam && this.feature.get('membersFiltering')) {
            filters.push(filterParam);
        }

        let searchQuery = searchParam ? {search: searchParam} : {};

        return Object.assign({}, {filter: filters.join('+')}, searchQuery);
    }

    // Actions -----------------------------------------------------------------

    @action
    refreshData() {
        this.fetchMembersTask.perform();
        this.fetchLabelsTask.perform();
        this.membersStats.invalidate();
        this.membersStats.fetchCounts();
    }

    @action
    changeOrder(order) {
        this.orderParam = order.value;
    }

    @action
    applyFilter(filterStr, filters) {
        this.filters = filters;
        this.filterParam = filterStr || null;
    }

    @action
    resetFilter() {
        this.filters = A([]);
        this.filterParam = null;
    }

    @action
    search(e) {
        this.searchTask.perform(e.target.value);
    }

    @action
    exportData() {
        let exportUrl = ghostPaths().url.api('members/upload');
        let downloadParams = new URLSearchParams(this.getApiQueryObject());
        downloadParams.set('limit', 'all');

        let iframe = document.getElementById('iframeDownload');

        if (!iframe) {
            iframe = document.createElement('iframe');
            iframe.id = 'iframeDownload';
            iframe.style.display = 'none';
            document.body.append(iframe);
        }
        iframe.setAttribute('src', `${exportUrl}?${downloadParams.toString()}`);
    }

    @action
    changeLabel(label, e) {
        if (e) {
            e.preventDefault();
            e.stopPropagation();
        }
        this.label = label.slug;
    }

    @action
    addLabel(e) {
        if (e) {
            e.preventDefault();
            e.stopPropagation();
        }
        const newLabel = this.store.createRecord('label');
        this.modalLabel = newLabel;
        this.showLabelModal = !this.showLabelModal;
    }

    @action
    editLabel(label, e) {
        if (e) {
            e.preventDefault();
            e.stopPropagation();
        }
        let modalLabel = this.availableLabels.findBy('slug', label);
        this.modalLabel = modalLabel;
        this.showLabelModal = !this.showLabelModal;
    }

    @action
    toggleLabelModal() {
        this.showLabelModal = !this.showLabelModal;
    }

    @action
    changePaidParam(paid) {
        this.paidParam = paid.value;
    }

    @action
    toggleDeleteMembersModal() {
        this.showDeleteMembersModal = !this.showDeleteMembersModal;
    }

    @action
    toggleUnsubscribeMembersModal() {
        this.showUnsubscribeMembersModal = !this.showUnsubscribeMembersModal;
    }

    @action
    toggleAddMembersLabelModal() {
        this.showAddMembersLabelModal = !this.showAddMembersLabelModal;
    }

    @action
    toggleRemoveMembersLabelModal() {
        this.showRemoveMembersLabelModal = !this.showRemoveMembersLabelModal;
    }

    @action
    deleteMembers() {
        return this.deleteMembersTask.perform();
    }

    @action
    unsubscribeMembers() {
        return this.unsubscribeMembersTask.perform();
    }

    @action
    addLabelsToMembers() {
        return this.addLabelToMembersTask.perform();
    }

    @action
    removeLabelsFromMembers() {
        return this.removeLabelFromMembersTask.perform();
    }

    // Tasks -------------------------------------------------------------------

    @task({restartable: true})
    *searchTask(query) {
        yield timeout(250); // debounce
        this.searchParam = query;
    }

    @task
    *fetchLabelsTask() {
        yield this.store.query('label', {limit: 'all'});
    }

    @task({restartable: true})
    *fetchMembersTask(params) {
        // params is undefined when called as a "refresh" of the model
        let {label, paidParam, searchParam, orderParam, filterParam} = typeof params === 'undefined' ? this : params;

        if (!searchParam) {
            this.resetSearch();
        }

        // use a fixed created_at date so that subsequent pages have a consistent index
        let startDate = new Date();

        // bypass the stale data shortcut if params change
        let forceReload = !params
            || label !== this._lastLabel
            || paidParam !== this._lastPaidParam
            || searchParam !== this._lastSearchParam
            || orderParam !== this._lastOrderParam
            || filterParam !== this._lastFilterParam;
        this._lastLabel = label;
        this._lastPaidParam = paidParam;
        this._lastSearchParam = searchParam;
        this._lastOrderParam = orderParam;
        this._lastFilterParam = filterParam;

        // unless we have a forced reload, do not re-fetch the members list unless it's more than a minute old
        // keeps navigation between list->details->list snappy
        if (!forceReload && this._startDate && !(this._startDate - startDate > 1 * 60 * 1000)) {
            return this.members;
        }

        this._startDate = startDate;

        this.members = yield this.ellaSparse.array((range = {}, query = {}) => {
            const searchQuery = this.getApiQueryObject({
                params,
                extraFilters: [`created_at:<='${moment.utc(this._startDate).format('YYYY-MM-DD HH:mm:ss')}'`]
            });
            const order = orderParam ? `${orderParam} desc` : `created_at desc`;

            query = Object.assign({
                order,
                limit: range.length,
                page: range.page
            }, searchQuery, query);

            return this.store.query('member', query).then((result) => {
                return {
                    data: result,
                    total: result.meta.pagination.total
                };
            });
        }, {
            limit: 50
        });
    }

    @task({drop: true})
    *deleteMembersTask() {
        const query = new URLSearchParams(this.getApiQueryObject());

        // Trigger download before deleting. Uses the CSV export endpoint but
        // needs to fetch the file and trigger a download directly rather than
        // via an iframe. The iframe approach can't tell us when a download has
        // started/finished meaning we could end up deleting the data before exporting it
        const exportUrl = ghostPaths().url.api('members/upload');
        const exportParams = new URLSearchParams(this.getApiQueryObject());
        exportParams.set('limit', 'all');

        yield fetch(exportUrl, {method: 'GET'})
            .then(res => res.blob())
            .then((blob) => {
                const blobUrl = window.URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = blobUrl;
                a.download = `members.${moment().format('YYYY-MM-DD')}.csv`;
                document.body.appendChild(a);
                if (!this.isTesting) {
                    a.click();
                }
                a.remove();
                URL.revokeObjectURL(blobUrl);
            });

        // backup downloaded, continue with deletion

        const deleteUrl = `${this.ghostPaths.url.api('members')}?${query}`;

        // response contains details of which members failed to be deleted
        const response = yield this.ajax.del(deleteUrl);

        // reset and reload
        this.store.unloadAll('member');
        this.router.transitionTo('members.index', {queryParams: Object.assign(resetQueryParams('members.index'))});
        this.membersStats.invalidate();
        this.membersStats.fetchCounts();

        return response.meta;
    }

    @task({drop: true})
    *unsubscribeMembersTask() {
        yield Promise.resolve();

        // reset and reload
        this.store.unloadAll('member');
        this.router.transitionTo('members.index', {queryParams: Object.assign(resetQueryParams('members.index'))});
        this.membersStats.invalidate();
        this.membersStats.fetchCounts();

        return {};
    }

    @task({drop: true})
    *addLabelToMembersTask() {
        yield Promise.resolve();

        // reset and reload
        this.store.unloadAll('member');
        this.router.transitionTo('members.index', {queryParams: Object.assign(resetQueryParams('members.index'))});
        this.membersStats.invalidate();
        this.membersStats.fetchCounts();

        return {};
    }

    @task({drop: true})
    *removeLabelFromMembersTask() {
        yield Promise.resolve();

        // reset and reload
        this.store.unloadAll('member');
        this.router.transitionTo('members.index', {queryParams: Object.assign(resetQueryParams('members.index'))});
        this.membersStats.invalidate();
        this.membersStats.fetchCounts();

        return {};
    }
    // Internal ----------------------------------------------------------------

    resetSearch() {
        this.searchText = '';
    }

    reset() {
        this.filterParam = null;
    }

    reload(params) {
        this.membersStats.invalidate();
        this.membersStats.fetchCounts();
        this.fetchMembersTask.perform(params);
    }
}
<commit_msg>Added custom column labels for filter columns<commit_after>import Controller from '@ember/controller';
import config from 'ghost-admin/config/environment';
import fetch from 'fetch';
import ghostPaths from 'ghost-admin/utils/ghost-paths';
import moment from 'moment';
import {A} from '@ember/array';
import {action} from '@ember/object';
import {capitalize} from '@ember/string';
import {ghPluralize} from 'ghost-admin/helpers/gh-pluralize';
import {resetQueryParams} from 'ghost-admin/helpers/reset-query-params';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency-decorators';
import {timeout} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';

const PAID_PARAMS = [{
    name: 'All members',
    value: null
}, {
    name: 'Free members',
    value: 'false'
}, {
    name: 'Paid members',
    value: 'true'
}];

export default class MembersController extends Controller {
    @service ajax;
    @service config;
    @service ellaSparse;
    @service feature;
    @service ghostPaths;
    @service membersStats;
    @service router;
    @service store;

    queryParams = [
        'label',
        {paidParam: 'paid'},
        {searchParam: 'search'},
        {orderParam: 'order'},
        {filterParam: 'filter'}
    ];

    @tracked members = A([]);
    @tracked searchText = '';
    @tracked searchParam = '';
    @tracked filterParam = null;
    @tracked paidParam = null;
    @tracked label = null;
    @tracked orderParam = null;
    @tracked modalLabel = null;
    @tracked showLabelModal = false;
    @tracked showDeleteMembersModal = false;
    @tracked showUnsubscribeMembersModal = false;
    @tracked showAddMembersLabelModal = false;
    @tracked showRemoveMembersLabelModal = false;
    @tracked filters = A([]);

    @tracked _availableLabels = A([]);

    paidParams = PAID_PARAMS;

    constructor() {
        super(...arguments);
        this._availableLabels = this.store.peekAll('label');

        if (this.isTesting === undefined) {
            this.isTesting = config.environment === 'test';
        }
    }

    // Computed properties -----------------------------------------------------

    get listHeader() {
        let {searchText, selectedLabel, members} = this;

        if (members.loading) {
            return 'Loading...';
        }

        if (searchText) {
            return 'Search result';
        }

        let count = ghPluralize(members.length, 'member');

        if (selectedLabel && selectedLabel.slug) {
            if (members.length > 1) {
                return `${count} match current filter`;
            } else {
                return `${count} matches current filter`;
            }
        }

        return count;
    }

    get showingAll() {
        return !this.searchParam && !this.paidParam && !this.label && !this.filterParam;
    }

    get availableOrders() {
        // don't return anything if email analytics is disabled because
        // we don't want to show an order dropdown with only a single option

        if (this.feature.get('emailAnalytics')) {
            return [{
                name: 'Newest',
                value: null
            }, {
                name: 'Open rate',
                value: 'email_open_rate'
            }];
        }

        return [];
    }

    get selectedOrder() {
        return this.availableOrders.find(order => order.value === this.orderParam);
    }

    get availableLabels() {
        let labels = this._availableLabels
            .filter(label => !label.isNew)
            .filter(label => label.id !== null)
            .sort((labelA, labelB) => labelA.name.localeCompare(labelB.name, undefined, {ignorePunctuation: true}));
        let options = labels.toArray();

        options.unshiftObject({name: 'All labels', slug: null});

        return options;
    }

    get selectedLabel() {
        let {label, availableLabels} = this;
        return availableLabels.findBy('slug', label);
    }

    get labelModalData() {
        let label = this.modalLabel;
        let labels = this.availableLabels;

        return {
            label,
            labels
        };
    }

    get selectedPaidParam() {
        return this.paidParams.findBy('value', this.paidParam) || {value: '!unknown'};
    }

    get isFiltered() {
        return !!(this.label || this.paidParam || this.searchParam || this.filterParam);
    }

    get filterColumns() {
        const defaultColumns = ['name', 'email'];
        return this.filters.map((filter) => {
            return filter.type;
        }).filter((f, idx, arr) => {
            return arr.indexOf(f) === idx;
        }).filter(d => !defaultColumns.includes(d));
    }

    get filterColumnLabels() {
        const filterColumnLabelMap = {
            'subscriptions.plan_interval': 'Plan Interval',
            'subscriptions.status': 'Subscription Status'
        };
        return this.filterColumns.map((d) => {
            return filterColumnLabelMap[d] ? filterColumnLabelMap[d] : capitalize(d.replace(/_/g, ' '));
        });
    }

    getApiQueryObject({params, extraFilters = []} = {}) {
        let {label, paidParam, searchParam, filterParam} = params ? params : this;

        let filters = [];

        filters = filters.concat(extraFilters);

        if (label) {
            filters.push(`label:'${label}'`);
        }

        if (paidParam !== null) {
            if (paidParam === 'true') {
                filters.push('status:-free');
            } else {
                filters.push('status:free');
            }
        }

        if (filterParam && this.feature.get('membersFiltering')) {
            filters.push(filterParam);
        }

        let searchQuery = searchParam ? {search: searchParam} : {};

        return Object.assign({}, {filter: filters.join('+')}, searchQuery);
    }

    // Actions -----------------------------------------------------------------

    @action
    refreshData() {
        this.fetchMembersTask.perform();
        this.fetchLabelsTask.perform();
        this.membersStats.invalidate();
        this.membersStats.fetchCounts();
    }

    @action
    changeOrder(order) {
        this.orderParam = order.value;
    }

    @action
    applyFilter(filterStr, filters) {
        this.filters = filters;
        this.filterParam = filterStr || null;
    }

    @action
    resetFilter() {
        this.filters = A([]);
        this.filterParam = null;
    }

    @action
    search(e) {
        this.searchTask.perform(e.target.value);
    }

    @action
    exportData() {
        let exportUrl = ghostPaths().url.api('members/upload');
        let downloadParams = new URLSearchParams(this.getApiQueryObject());
        downloadParams.set('limit', 'all');

        let iframe = document.getElementById('iframeDownload');

        if (!iframe) {
            iframe = document.createElement('iframe');
            iframe.id = 'iframeDownload';
            iframe.style.display = 'none';
            document.body.append(iframe);
        }
        iframe.setAttribute('src', `${exportUrl}?${downloadParams.toString()}`);
    }

    @action
    changeLabel(label, e) {
        if (e) {
            e.preventDefault();
            e.stopPropagation();
        }
        this.label = label.slug;
    }

    @action
    addLabel(e) {
        if (e) {
            e.preventDefault();
            e.stopPropagation();
        }
        const newLabel = this.store.createRecord('label');
        this.modalLabel = newLabel;
        this.showLabelModal = !this.showLabelModal;
    }

    @action
    editLabel(label, e) {
        if (e) {
            e.preventDefault();
            e.stopPropagation();
        }
        let modalLabel = this.availableLabels.findBy('slug', label);
        this.modalLabel = modalLabel;
        this.showLabelModal = !this.showLabelModal;
    }

    @action
    toggleLabelModal() {
        this.showLabelModal = !this.showLabelModal;
    }

    @action
    changePaidParam(paid) {
        this.paidParam = paid.value;
    }

    @action
    toggleDeleteMembersModal() {
        this.showDeleteMembersModal = !this.showDeleteMembersModal;
    }

    @action
    toggleUnsubscribeMembersModal() {
        this.showUnsubscribeMembersModal = !this.showUnsubscribeMembersModal;
    }

    @action
    toggleAddMembersLabelModal() {
        this.showAddMembersLabelModal = !this.showAddMembersLabelModal;
    }

    @action
    toggleRemoveMembersLabelModal() {
        this.showRemoveMembersLabelModal = !this.showRemoveMembersLabelModal;
    }

    @action
    deleteMembers() {
        return this.deleteMembersTask.perform();
    }

    @action
    unsubscribeMembers() {
        return this.unsubscribeMembersTask.perform();
    }

    @action
    addLabelsToMembers() {
        return this.addLabelToMembersTask.perform();
    }

    @action
    removeLabelsFromMembers() {
        return this.removeLabelFromMembersTask.perform();
    }

    // Tasks -------------------------------------------------------------------

    @task({restartable: true})
    *searchTask(query) {
        yield timeout(250); // debounce
        this.searchParam = query;
    }

    @task
    *fetchLabelsTask() {
        yield this.store.query('label', {limit: 'all'});
    }

    @task({restartable: true})
    *fetchMembersTask(params) {
        // params is undefined when called as a "refresh" of the model
        let {label, paidParam, searchParam, orderParam, filterParam} = typeof params === 'undefined' ? this : params;

        if (!searchParam) {
            this.resetSearch();
        }

        // use a fixed created_at date so that subsequent pages have a consistent index
        let startDate = new Date();

        // bypass the stale data shortcut if params change
        let forceReload = !params
            || label !== this._lastLabel
            || paidParam !== this._lastPaidParam
            || searchParam !== this._lastSearchParam
            || orderParam !== this._lastOrderParam
            || filterParam !== this._lastFilterParam;
        this._lastLabel = label;
        this._lastPaidParam = paidParam;
        this._lastSearchParam = searchParam;
        this._lastOrderParam = orderParam;
        this._lastFilterParam = filterParam;

        // unless we have a forced reload, do not re-fetch the members list unless it's more than a minute old
        // keeps navigation between list->details->list snappy
        if (!forceReload && this._startDate && !(this._startDate - startDate > 1 * 60 * 1000)) {
            return this.members;
        }

        this._startDate = startDate;

        this.members = yield this.ellaSparse.array((range = {}, query = {}) => {
            const searchQuery = this.getApiQueryObject({
                params,
                extraFilters: [`created_at:<='${moment.utc(this._startDate).format('YYYY-MM-DD HH:mm:ss')}'`]
            });
            const order = orderParam ? `${orderParam} desc` : `created_at desc`;

            query = Object.assign({
                order,
                limit: range.length,
                page: range.page
            }, searchQuery, query);

            return this.store.query('member', query).then((result) => {
                return {
                    data: result,
                    total: result.meta.pagination.total
                };
            });
        }, {
            limit: 50
        });
    }

    @task({drop: true})
    *deleteMembersTask() {
        const query = new URLSearchParams(this.getApiQueryObject());

        // Trigger download before deleting. Uses the CSV export endpoint but
        // needs to fetch the file and trigger a download directly rather than
        // via an iframe. The iframe approach can't tell us when a download has
        // started/finished meaning we could end up deleting the data before exporting it
        const exportUrl = ghostPaths().url.api('members/upload');
        const exportParams = new URLSearchParams(this.getApiQueryObject());
        exportParams.set('limit', 'all');

        yield fetch(exportUrl, {method: 'GET'})
            .then(res => res.blob())
            .then((blob) => {
                const blobUrl = window.URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = blobUrl;
                a.download = `members.${moment().format('YYYY-MM-DD')}.csv`;
                document.body.appendChild(a);
                if (!this.isTesting) {
                    a.click();
                }
                a.remove();
                URL.revokeObjectURL(blobUrl);
            });

        // backup downloaded, continue with deletion

        const deleteUrl = `${this.ghostPaths.url.api('members')}?${query}`;

        // response contains details of which members failed to be deleted
        const response = yield this.ajax.del(deleteUrl);

        // reset and reload
        this.store.unloadAll('member');
        this.router.transitionTo('members.index', {queryParams: Object.assign(resetQueryParams('members.index'))});
        this.membersStats.invalidate();
        this.membersStats.fetchCounts();

        return response.meta;
    }

    @task({drop: true})
    *unsubscribeMembersTask() {
        yield Promise.resolve();

        // reset and reload
        this.store.unloadAll('member');
        this.router.transitionTo('members.index', {queryParams: Object.assign(resetQueryParams('members.index'))});
        this.membersStats.invalidate();
        this.membersStats.fetchCounts();

        return {};
    }

    @task({drop: true})
    *addLabelToMembersTask() {
        yield Promise.resolve();

        // reset and reload
        this.store.unloadAll('member');
        this.router.transitionTo('members.index', {queryParams: Object.assign(resetQueryParams('members.index'))});
        this.membersStats.invalidate();
        this.membersStats.fetchCounts();

        return {};
    }

    @task({drop: true})
    *removeLabelFromMembersTask() {
        yield Promise.resolve();

        // reset and reload
        this.store.unloadAll('member');
        this.router.transitionTo('members.index', {queryParams: Object.assign(resetQueryParams('members.index'))});
        this.membersStats.invalidate();
        this.membersStats.fetchCounts();

        return {};
    }
    // Internal ----------------------------------------------------------------

    resetSearch() {
        this.searchText = '';
    }

    reset() {
        this.filterParam = null;
    }

    reload(params) {
        this.membersStats.invalidate();
        this.membersStats.fetchCounts();
        this.fetchMembersTask.perform(params);
    }
}
<|endoftext|>


<commit_before>import "./";

orb.vector.rotationMatrix = function(α, e) {

  α = α % ( 2*Math.PI );

  var cosα, sinα;

  if ( α === 0 ) {
    cosα = 1;
    sinα = 0;
  } else if ( α === Math.PI/2 || α === -3/2*Math.PI ) {
    cosα = 0;
    sinα = 1;
  } else if ( α === Math.PI || α === -Math.PI ) {
    cosα = -1;
    sinα = 0;
  } else if ( α === 3/2*Math.PI || α === -Math.PI/2 ) {
    cosα = 0;
    sinα = -1;
  } else {
    cosα = Math.cos(α);
    sinα = Math.sin(α);
  }

  switch(e) {
    case 1:
      return [
        1,     0,    0,
        0,  cosα, sinα,
        0, -sinα, cosα
      ];

    case 2:
      return [
        cosα, 0, -sinα,
           0, 1,     0,
        sinα, 0,  cosα
      ];

    case 3:
      return [
         cosα, sinα, 0,
        -sinα, cosα, 0,
            0,    0, 1
      ];

    default:
      throw new Error('rotation axis has to be 1, 2 or 3');

  }
};

orb.vector.r = orb.vector.rotationMatrix;
<commit_msg>[ADD] Documentation for rotationMatrix()<commit_after>import "./";

/**
 * rotationMatrix() returns a matrix for a coordinate system rotation
 * of α radians around axis e relative to the origin.
 * These rotation matrices are for CRS transformations not geometric
 * point transformations!
 *
 * @param <Number> α [radians]
          <Integer> e {1,2,3}
 * @return <[Number]> // 3x3 matrix represented as Array with 9 elements
 */
orb.vector.rotationMatrix = function(α, e) {

  α = α % ( 2*Math.PI );

  var cosα, sinα;

  if ( α === 0 ) {
    cosα = 1;
    sinα = 0;
  } else if ( α === Math.PI/2 || α === -3/2*Math.PI ) {
    cosα = 0;
    sinα = 1;
  } else if ( α === Math.PI || α === -Math.PI ) {
    cosα = -1;
    sinα = 0;
  } else if ( α === 3/2*Math.PI || α === -Math.PI/2 ) {
    cosα = 0;
    sinα = -1;
  } else {
    cosα = Math.cos(α);
    sinα = Math.sin(α);
  }

  switch(e) {
    case 1:
      return [
        1,     0,    0,
        0,  cosα, sinα,
        0, -sinα, cosα
      ];

    case 2:
      return [
        cosα, 0, -sinα,
           0, 1,     0,
        sinα, 0,  cosα
      ];

    case 3:
      return [
         cosα, sinα, 0,
        -sinα, cosα, 0,
            0,    0, 1
      ];

    default:
      throw new Error('rotation axis has to be 1, 2 or 3');

  }
};

orb.vector.r = orb.vector.rotationMatrix;
<|endoftext|>


<commit_before>
var Template = require('../models/templateModel');
var email = require('./email');
var moment = require('moment');
var mongoose = require('mongoose');
var ObjectID = require('mongodb').ObjectID;
var Entities = require('html-entities').Html5Entities;
var entities = new Entities();

// Get options from config file
var config = require('../config');

// Return the current template
exports.getTemplate = function (req, res, next) {
  Template.findOne({}, null, {sort: {version: -1}}, function (err0, results0) {
    if (err0) {return console.log(err0);}
    if (!results0) {return console.log("No templates were found. Please create a template first");}
    Template.find({version: results0.version}, null, {sort: {index: 1}}, function (err1, results1) {
      res.json(results1);
    });
  });
};

// Set a new template
exports.newTemplate = function (req, res, next) {
  Template.findOne({}, null, {sort: {version: -1}}, function (err0, results0) {
    var i, index,utime, nSpots, nExec, newUsers, version, eachTemplate;
    if (typeof req.body.d === 'undefined' || req.body.d.length === 0) {return console.log("Template was undefined or had zero length. req.body: ", req.body);}
    for (i = 0; i < req.body.d.length; i++) {
      index = i;
      if (!results0) {version = 0;} else {version = results0.version + 1;}
      if (typeof req.body.d[i].time === 'string') {time = entities.encode(req.body.d[i].time.trim());} else {console.log("time was not a string");continue;}
      if (!isNaN(parseInt(req.body.d[i].nSpots, 10))) {nSpots = parseInt(req.body.d[i].nSpots, 10)} else {console.log("nSpots was not a number");continue;}
      if (!isNaN(parseInt(req.body.d[i].nExec, 10))) {nExec= parseInt(req.body.d[i].nExec, 10)} else {console.log("nExec was not a number");continue;}
      if (req.body.d[i].newUsers === 'true') {newUsers = true} else if (req.body.d[i].newUsers === 'false') {newUsers = false}  else {console.log("newUsers was not a boolean");continue;}

      eachTemplate = new Template({index: index, time: time, nSpots: nSpots, nExec: nExec, newUsers: newUsers, version: version, author: req.user._id});
      eachTemplate.save(function (err1, results1) {
        if (err1) {return console.log(err1);}
        if (( results1.index + 1 ) === req.body.d.length) {email.newTemplate(config.opt.email);}
      });
    }
    res.json(req.body);
  });
};
<commit_msg>Permit line breaks in template times<commit_after>
var Template = require('../models/templateModel');
var email = require('./email');
var moment = require('moment');
var mongoose = require('mongoose');
var ObjectID = require('mongodb').ObjectID;
var Entities = require('html-entities').Html5Entities;
var entities = new Entities();

// Get options from config file
var config = require('../config');

// Return the current template
exports.getTemplate = function (req, res, next) {
  Template.findOne({}, null, {sort: {version: -1}}, function (err0, results0) {
    if (err0) {return console.log(err0);}
    if (!results0) {return console.log("No templates were found. Please create a template first");}
    Template.find({version: results0.version}, null, {sort: {index: 1}}, function (err1, results1) {
      res.json(results1);
    });
  });
};

// Set a new template
exports.newTemplate = function (req, res, next) {
  Template.findOne({}, null, {sort: {version: -1}}, function (err0, results0) {
    var i, index,utime, nSpots, nExec, newUsers, version, eachTemplate;
    if (typeof req.body.d === 'undefined' || req.body.d.length === 0) {return console.log("Template was undefined or had zero length. req.body: ", req.body);}
    for (i = 0; i < req.body.d.length; i++) {
      index = i;
      if (!results0) {version = 0;} else {version = results0.version + 1;}
      if (typeof req.body.d[i].time === 'string') {time = entities.encode(req.body.d[i].time.trim()).replace("<br>", "
").replace("<br/>", "
").replace("<br />", "
");} else {console.log("time was not a string");continue;}
      if (!isNaN(parseInt(req.body.d[i].nSpots, 10))) {nSpots = parseInt(req.body.d[i].nSpots, 10)} else {console.log("nSpots was not a number");continue;}
      if (!isNaN(parseInt(req.body.d[i].nExec, 10))) {nExec= parseInt(req.body.d[i].nExec, 10)} else {console.log("nExec was not a number");continue;}
      if (req.body.d[i].newUsers === 'true') {newUsers = true} else if (req.body.d[i].newUsers === 'false') {newUsers = false}  else {console.log("newUsers was not a boolean");continue;}

      eachTemplate = new Template({index: index, time: time, nSpots: nSpots, nExec: nExec, newUsers: newUsers, version: version, author: req.user._id});
      eachTemplate.save(function (err1, results1) {
        if (err1) {return console.log(err1);}
        if (( results1.index + 1 ) === req.body.d.length) {email.newTemplate(config.opt.email);}
      });
    }
    res.json(req.body);
  });
};
<|endoftext|>


<commit_before>define( [
            'dojo/_base/declare',
            'dojo/_base/array',
            'JBrowse/Util',
            'JBrowse/View/Track/CanvasFeatures',
            'JBrowse/View/Track/Alignments/MismatchesMixin'
        ],
        function( declare, array, Util, CanvasFeatureTrack, MismatchesMixin ) {

return declare( [ CanvasFeatureTrack, MismatchesMixin ], {
    constructor: function() {

        // if showMismatches is false, stub out this object's
        // _drawMismatches to be a no-op
        if( ! this.config.style.showMismatches )
            this._drawMismatches = function() {};
    },

    _defaultConfig: function() {
        return Util.deepUpdate(
            dojo.clone( this.inherited(arguments) ),
            {
                //maxFeatureScreenDensity: 400
                layoutPitchY: 3,
                style: {
                    bgcolor: function( feature ) {
                        var missing_mate = feature.get('multi_segment_template') && !feature.get('multi_segment_all_aligned');
                        var strand = feature.get('strand');
                        return                  missing_mate ? this.getStyle( feature, 'bgcolor_missing_mate' ) :
                               strand == 1  || strand == '+' ? this.getStyle( feature, 'bgcolor_fwd_strand' )   :
                               strand == -1 || strand == '-' ? this.getStyle( feature, 'bgcolor_rev_strand' )   :
                                                               this.colorForBase('reference');
                    },
                    bgcolor_fwd_strand: '#EC8B8B',
                    bgcolor_rev_strand: '#898FD8',
                    bgcolor_missing_mate: '#D11919',
                    height: 3,
                    marginBottom: 0,
                    showMismatches: true,
                    font: 'bold 10px Courier New,monospace'
                }
            }
        );
    },

    // draw each feature
    renderFeature: function( context, viewArgs, fRect ) {
        // background
        var bgcolor = this.getStyle( fRect.f, 'bgcolor' );
        if( bgcolor ) {
            context.fillStyle = bgcolor;
            context.fillRect( fRect.l, fRect.t, fRect.w, fRect.h );
        }

        // draw mismatches if zoomed in far enough
        if( viewArgs.scale > 0.3 )
            this._drawMismatches( context, viewArgs, fRect );

        // foreground border
        var fgcolor = this.getStyle( fRect.f, 'fgcolor' );
        if( fgcolor ) {
            if( fRect.h > 3 ) {
                context.lineWidth = 1;
                context.strokeStyle = fgcolor;

                // need to stroke a smaller rectangle to remain within
                // the bounds of the feature's overall height and
                // width, because of the way stroking is done in
                // canvas.  thus the +0.5 and -1 business.
                context.strokeRect( fRect.l+0.5, fRect.t+0.5, fRect.w-1, fRect.h-1 );
            } else {
                context.fillStyle = fgcolor;
                context.fillRect( fRect.l, fRect.t+fRect.h-1, fRect.w, 1 );
            }
        }
    },

    _drawMismatches: function( context, viewArgs, fRect ) {
        var feature = fRect.f;
        var scale = viewArgs.scale;
        // recall: scale is pixels/basepair

        if ( fRect.w > 1 ) {
            var mismatches = this._getMismatches( feature );
            var charSize = this.getCharacterMeasurements( context );
            var drawChars = scale >= charSize.w && fRect.h >= charSize.h;
            array.forEach( mismatches, function( mismatch ) {
                var start = feature.get('start') + mismatch.start;
                var end = start + mismatch.length;

                var mRect = {
                    h: fRect.h,
                    l: fRect.toX( start ),
                    t: fRect.t
                };
                mRect.w = fRect.toX( end ) - mRect.l;

                if( mismatch.type == 'mismatch' || mismatch.type == 'deletion' ) {
                    context.fillStyle = this.colorForBase( mismatch.base );
                    context.fillRect( mRect.l, mRect.t, mRect.w, mRect.h );

                    if( mRect.w >= charSize.w && mRect.h >= charSize.h ) {
                        context.font = this.config.style.font;
                        context.fillStyle = mismatch.type == 'deletion' ? 'white' : 'black';
                        context.fillText( mismatch.base, mRect.l+(mRect.w-charSize.w)/2+1, mRect.t+mRect.h-(mRect.h-charSize.h)/2-1 );
                    }
                }
                else if( mismatch.type == 'insertion' ) {
                    context.fillStyle = 'black';
                    context.fillRect( mRect.l-1, mRect.t, 2, mRect.h );
                    if( mRect.w >= charSize.w && mRect.h >= charSize.h ) {
                        context.font = this.config.style.font;
                        context.fillStyle = 'black';
                        context.fillText( '('+mismatch.base+')', mRect.l+2, mRect.t+mRect.h-(mRect.h-charSize.h)/2-2 );
                    }
                }
            },this);
        }
    },

    getCharacterMeasurements: function( context ) {
        return this.charSize = this.charSize || function() {
            var fpx = (this.config.style.font.match(/(\d+)px/i)||[])[1] || Infinity;
            return { w: fpx, h: fpx };
        }.call(this);
    }
});
});<commit_msg>add support for skips in Alignments/PileUp<commit_after>define( [
            'dojo/_base/declare',
            'dojo/_base/array',
            'JBrowse/Util',
            'JBrowse/View/Track/CanvasFeatures',
            'JBrowse/View/Track/Alignments/MismatchesMixin'
        ],
        function( declare, array, Util, CanvasFeatureTrack, MismatchesMixin ) {

return declare( [ CanvasFeatureTrack, MismatchesMixin ], {
    constructor: function() {

        // if showMismatches is false, stub out this object's
        // _drawMismatches to be a no-op
        if( ! this.config.style.showMismatches )
            this._drawMismatches = function() {};
    },

    _defaultConfig: function() {
        return Util.deepUpdate(
            dojo.clone( this.inherited(arguments) ),
            {
                //maxFeatureScreenDensity: 400
                layoutPitchY: 3,
                style: {
                    bgcolor: function( feature ) {
                        var missing_mate = feature.get('multi_segment_template') && !feature.get('multi_segment_all_aligned');
                        var strand = feature.get('strand');
                        return                  missing_mate ? this.getStyle( feature, 'bgcolor_missing_mate' ) :
                               strand == 1  || strand == '+' ? this.getStyle( feature, 'bgcolor_fwd_strand' )   :
                               strand == -1 || strand == '-' ? this.getStyle( feature, 'bgcolor_rev_strand' )   :
                                                               this.colorForBase('reference');
                    },
                    bgcolor_fwd_strand: '#EC8B8B',
                    bgcolor_rev_strand: '#898FD8',
                    bgcolor_missing_mate: '#D11919',
                    height: 3,
                    marginBottom: 0,
                    showMismatches: true,
                    font: 'bold 10px Courier New,monospace'
                }
            }
        );
    },

    // draw each feature
    renderFeature: function( context, viewArgs, fRect ) {
        // background
        var bgcolor = this.getStyle( fRect.f, 'bgcolor' );
        if( bgcolor ) {
            context.fillStyle = bgcolor;
            context.fillRect( fRect.l, fRect.t, fRect.w, fRect.h );
        }

        // foreground border
        var fgcolor = this.getStyle( fRect.f, 'fgcolor' );
        if( fgcolor ) {
            if( fRect.h > 3 ) {
                context.lineWidth = 1;
                context.strokeStyle = fgcolor;

                // need to stroke a smaller rectangle to remain within
                // the bounds of the feature's overall height and
                // width, because of the way stroking is done in
                // canvas.  thus the +0.5 and -1 business.
                context.strokeRect( fRect.l+0.5, fRect.t+0.5, fRect.w-1, fRect.h-1 );
            } else {
                context.fillStyle = fgcolor;
                context.fillRect( fRect.l, fRect.t+fRect.h-1, fRect.w, 1 );
            }
        }

        // draw mismatches if zoomed in far enough
        if( viewArgs.scale > 0.3 )
            this._drawMismatches( context, viewArgs, fRect );
    },

    _drawMismatches: function( context, viewArgs, fRect ) {
        var feature = fRect.f;
        var scale = viewArgs.scale;
        // recall: scale is pixels/basepair

        if ( fRect.w > 1 ) {
            var mismatches = this._getMismatches( feature );
            var charSize = this.getCharacterMeasurements( context );
            var drawChars = scale >= charSize.w && fRect.h >= charSize.h;
            array.forEach( mismatches, function( mismatch ) {
                var start = feature.get('start') + mismatch.start;
                var end = start + mismatch.length;

                var mRect = {
                    h: fRect.h,
                    l: fRect.toX( start ),
                    t: fRect.t
                };
                mRect.w = fRect.toX( end ) - mRect.l;

                if( mismatch.type == 'mismatch' || mismatch.type == 'deletion' ) {
                    context.fillStyle = this.colorForBase( mismatch.base );
                    context.fillRect( mRect.l, mRect.t, mRect.w, mRect.h );

                    if( mRect.w >= charSize.w && mRect.h >= charSize.h ) {
                        context.font = this.config.style.font;
                        context.fillStyle = mismatch.type == 'deletion' ? 'white' : 'black';
                        context.fillText( mismatch.base, mRect.l+(mRect.w-charSize.w)/2+1, mRect.t+mRect.h-(mRect.h-charSize.h)/2-1 );
                    }
                }
                else if( mismatch.type == 'insertion' ) {
                    context.fillStyle = 'black';
                    context.fillRect( mRect.l-1, mRect.t, 2, mRect.h );
                    if( mRect.w >= charSize.w && mRect.h >= charSize.h ) {
                        context.font = this.config.style.font;
                        context.fillStyle = 'black';
                        context.fillText( '('+mismatch.base+')', mRect.l+2, mRect.t+mRect.h-(mRect.h-charSize.h)/2-2 );
                    }
                }
                else if( mismatch.type == 'skip' ) {
                    context.clearRect( mRect.l, mRect.t, mRect.w, mRect.h );
                    context.fillStyle = '#333';
                    context.fillRect( mRect.l, mRect.t+(mRect.h-2)/2, mRect.w, 2 );
                }
            },this);
        }
    },

    getCharacterMeasurements: function( context ) {
        return this.charSize = this.charSize || function() {
            var fpx = (this.config.style.font.match(/(\d+)px/i)||[])[1] || Infinity;
            return { w: fpx, h: fpx };
        }.call(this);
    }
});
});<|endoftext|>


<commit_before>'use strict';

console.log(''); // new line
var vm = require('vm');
var fs = require('fs');
var zlib = require('zlib');
var http = require('http');
var WebSocketServer = require("ws").Server

/**
 * Declares the gameEngine variable for a later use.
 * This variable will be set to an instance of murmures.GameEngine after all classes are loaded.
 * @instance
 * @type {murmures.GameEngine}
 */
var gameEngine = {};

/**
 * The main namespace. All classes should be prefixed with it.
 * @namespace
 */
var murmures = {
    /**
     * Startup time.
     * @private
     */
    startTime: process.hrtime(),

    /**
     * Writes timestamped log to the console.
     * @public
     */
    serverLog: function (txt) {
        let diff = process.hrtime(this.startTime);
        let fdiff = diff[0] + diff[1] / 1e9;
        if (typeof txt !== 'undefined') {
            console.log(fdiff.toFixed(6) + ' - ' + txt);
        }
        return fdiff;
    },

    restartGame: function (targetLevel) {
        gameEngine.levels = [];
        gameEngine.activeLevel = 0;
        gameEngine.levelIds = fs.readdirSync('./data/staticlevels/', 'utf8');
        let loopCounter = 0;
        gameEngine.levelIds.forEach(function (levelName) {
            let level1Txt = fs.readFileSync('./data/staticlevels/' + levelName, 'utf8').toString().replace(/^\uFEFF/, '');
            let level1 = new murmures.Level();
            level1.build(JSON.parse(level1Txt));
            level1.id = levelName.replace('.json', '');
            gameEngine.levels.push(level1);
            if (typeof targetLevel !== 'undefined' && targetLevel && targetLevel === levelName) {
                gameEngine.activeLevel = loopCounter;
            }
            loopCounter++;
        }, this);
        gameEngine.level = gameEngine.levels[gameEngine.activeLevel];

        gameEngine.heros = [];
        let allHeroesKeys = [];
        for (let assetId in gameEngine.bodies) {
            let ref = gameEngine.bodies[assetId];
            if (murmures.C.LAYERS[ref.layerId][0] === 'Hero') allHeroesKeys.push(assetId);
        }

        let chosenHeroesKeys = [];
        let chosenHero;
        for (loopCounter = 0; loopCounter < 3; loopCounter++) {
            do {
                let rand = Math.floor(Math.random() * allHeroesKeys.length);
                chosenHero = allHeroesKeys[rand];
            } while (chosenHeroesKeys.indexOf(chosenHero) >= 0); // This loop prevents duplicate heroes
            chosenHeroesKeys.push(chosenHero);
            let hero1 = new murmures.Character();
            hero1.build(gameEngine.level.getStartingPoint(), chosenHero);
            hero1.typeCharacter = murmures.C.TYPE_CHARACTER_HERO;
            if (loopCounter === 0) {
                hero1.setVision();
                hero1.stateOrder = murmures.C.STATE_HERO_ORDER_INPROGRESS;
            }

            let chosenSkill= [];
            for(let loopSkill =0;loopSkill < 2;loopSkill ++){
              let rd=1;
              let nbSkill = 0;
              for(let itSkill in gameEngine.skills) nbSkill ++;
              do{
                rd = (Math.floor(Math.random() *nbSkill) + 1)
              }while (chosenSkill.indexOf(rd)>=0);
              chosenSkill.push(rd);
              if(loopSkill ==0){
                hero1.activeSkill = rd;
              }
              hero1.skills[rd] = gameEngine.skills[rd];
            }

            gameEngine.heros.push(hero1);
        }

        gameEngine.reportQueue = [];
        gameEngine.state = murmures.C.STATE_ENGINE_INIT;
    }
};

murmures.serverLog('Loading classes');

/**
 * Loads classes into the murmures namespace
 */
(function () {
    const ctx = {
        murmures: murmures,
        gameEngine: {}
    };
    vm.createContext(ctx);

    let constantsjs = fs.readFileSync('./src/js/core/constants.js', 'utf8').toString().replace(/^\uFEFF/, '');
    vm.runInContext(constantsjs, ctx, { filename: 'constants.js' });
    let gameEnginejs = fs.readFileSync('./src/js/core/gameengine.js', 'utf8').toString().replace(/^\uFEFF/, '');
    vm.runInContext(gameEnginejs, ctx, { filename: 'gameengine.js' });

    ctx.gameEngine = new murmures.GameEngine();

    let playerjs = fs.readFileSync('./src/js/core/player.js', 'utf8').toString().replace(/^\uFEFF/, '');
    vm.runInContext(playerjs, ctx, { filename: 'player.js' });
    let tilejs = fs.readFileSync('./src/js/core/tile.js', 'utf8').toString().replace(/^\uFEFF/, '');
    vm.runInContext(tilejs, ctx, { filename: 'tile.js' });
    let leveljs = fs.readFileSync('./src/js/core/level.js', 'utf8').toString().replace(/^\uFEFF/, '');
    vm.runInContext(leveljs, ctx, { filename: 'level.js' });
    let characterjs = fs.readFileSync('./src/js/core/character.js', 'utf8').toString().replace(/^\uFEFF/, '');
    vm.runInContext(characterjs, ctx, { filename: 'character.js' });
    let orderjs = fs.readFileSync('./src/js/core/order.js', 'utf8').toString().replace(/^\uFEFF/, '');
    vm.runInContext(orderjs, ctx, { filename: 'order.js' });
    let turnReportjs = fs.readFileSync('./src/js/core/turnreport.js', 'utf8').toString().replace(/^\uFEFF/, '');
    vm.runInContext(turnReportjs, ctx, { filename: 'turnreport.js' });
    let physicalBodyjs = fs.readFileSync('./src/js/core/physicalbody.js', 'utf8').toString().replace(/^\uFEFF/, '');
    vm.runInContext(physicalBodyjs, ctx, { filename: 'physicalbody.js' });
    let behaviorjs = fs.readFileSync('./src/js/core/behavior.js', 'utf8').toString().replace(/^\uFEFF/, '');
    vm.runInContext(behaviorjs, ctx, { filename: 'behavior.js' });
    let skillbehaviorjs = fs.readFileSync('./src/js/core/skillbehavior.js', 'utf8').toString().replace(/^\uFEFF/, '');
    vm.runInContext(skillbehaviorjs, ctx, { filename: 'skillbehavior.js' });
    let skilljs = fs.readFileSync('./src/js/core/skill.js', 'utf8').toString().replace(/^\uFEFF/, '');
    vm.runInContext(skilljs, ctx, { filename: 'skill.js' });
    let vmperfjs = fs.readFileSync('./src/js/test/vmperf.js', 'utf8').toString().replace(/^\uFEFF/, '');
    vm.runInContext(vmperfjs, ctx, { filename: 'vmperf.js' });
    let servertestjs = fs.readFileSync('./src/js/test/servertest.js', 'utf8').toString().replace(/^\uFEFF/, '');
    vm.runInContext(servertestjs, ctx, { filename: 'servertest.js' });

    gameEngine = ctx.gameEngine;
})();

murmures.serverLog('Initializing game');

/**
 * Initializes game
 */
(function () {
    let assetsJson = fs.readFileSync('./data/reference/assets.json', 'utf8').toString().replace(/^\uFEFF/, '');
    gameEngine.bodies = JSON.parse(assetsJson);

    let localefrJson = fs.readFileSync('./data/locale/fr.json', 'utf8').toString().replace(/^\uFEFF/, '');
    let localeenJson = fs.readFileSync('./data/locale/en.json', 'utf8').toString().replace(/^\uFEFF/, '');
    gameEngine.locale = {};
    gameEngine.locale.fr = JSON.parse(localefrJson);
    gameEngine.locale.en = JSON.parse(localeenJson);

    let localSkills = JSON.parse(fs.readFileSync('./data/reference/skill.json', 'utf8').toString().replace(/^\uFEFF/, ''));
    for (let skillName in localSkills){
      let tempSkill = new murmures.Skill();
      tempSkill.build(localSkills[skillName],skillName);
      gameEngine.skills[tempSkill.id] = tempSkill;
    }
    murmures.restartGame();
})();

murmures.serverLog('Starting HTTP server');

// Tries to compress (gzip) the response, if the client browser allows it
function compressAndSend(request, response, contType, txt, callback) {
    let acceptEncoding = request.headers['accept-encoding'];
    if (!acceptEncoding) {
        acceptEncoding = '';
    }
    if (acceptEncoding.match(/\bgzip\b/) && contType !== 'image/png') {
        zlib.gzip(txt, function (err, zipped) {
            if (err) throw err;
            murmures.serverLog('Response compressed');
            response.writeHead(200, { 'Content-Type': contType, 'Content-Encoding': 'gzip' });
            response.end(zipped, 'utf8', callback);
        });
    } else if (contType === 'image/png') {
        let stats = fs.statSync('./src/img/murmures.png');
        response.writeHead(200, { 'Content-Type': contType, 'Content-Length': stats.size });
        response.end(txt);
    } else {
        response.writeHead(200, { 'Content-Type': contType });
        response.end(txt);
    }
}

var server = http.createServer(function (request, response) {
    if (request.url === '/favicon.ico') {
        response.writeHead(204); // No content
        response.end();
    }
    else if (request.url === '/') {
        response.writeHead(301, { 'Location': '/src/pages/client.html' });
        response.end();
    }
    else if (request.url === '/allClient.js') {
        fs.readFile('./src/js/client/client.js', function (clientErr, clientFile) {
            if (clientErr) throw clientErr;
            fs.readFile('./src/js/client/event.js', function (eventErr, eventFile) {
                if (eventErr) throw eventErr;
                fs.readFile('./src/js/client/renderer.js', function (rendererErr, rendererFile) {
                    if (rendererErr) throw rendererErr;
                    fs.readFile('./src/js/client/base.js', function (baseErr, baseFile) {
                        if (baseErr) throw baseErr;
                        compressAndSend(request, response, 'application/javascript', ''
                        + baseFile.toString() + '\n\n'
                        + rendererFile.toString().replace(/^\uFEFF/, '') + '\n\n' // strip BOM
                        + eventFile.toString().replace(/^\uFEFF/, '') + '\n\n' // strip BOM
                        + clientFile.toString().replace(/^\uFEFF/, '') + '\n\n' // strip BOM
                        );
                    });
                });
            });
        });
    }
    else if (request.url.startsWith('/src/')) {
        // #region Static Pages
        try {
            let fileName = request.url;
            fs.readFile('.' + fileName, function (err, fileContent) {
                if (err) throw err;
                if (fileName.endsWith('.js')) {
                    compressAndSend(request, response, 'application/javascript', fileContent.toString());
                }
                else if (fileName.endsWith('.css')) {
                    compressAndSend(request, response, 'text/css', fileContent.toString());
                }
                else if (fileName.endsWith('.png')) {
                    compressAndSend(request, response, 'image/png', fileContent);
                }
                else if (fileName.endsWith('.html')) {
                    compressAndSend(request, response, 'text/html', fileContent.toString());
                }
                else {
                    response.writeHead(400); // Bad Request
                    response.end();
                }
            });
        } catch (e) {
            response.writeHead(404);
            response.end();
        }
        // #endregion
    }
    else {
        response.writeHead(404);
        response.end();
    }
}).listen(process.env.PORT || 15881);

var wss = new WebSocketServer({ server: server })
wss.on('connection', function (ws) {
    ws.on('message', function (messageTxt) {
        let message = JSON.parse(messageTxt);
        if (message.service === 'getLevel') {
            ws.send(JSON.stringify({ fn: 'init', payload: gameEngine }));
        }
        else if (message.service === 'order') {
            murmures.serverLog('Request received');
            let clientOrder = new murmures.Order();
            let parsing = clientOrder.build(message.payload);
            if (parsing.valid) {
                let check = gameEngine.checkOrder(clientOrder);
                if (check.valid) {
                    //gameEngine.gameTurn++;
                    murmures.serverLog('Order checked');
                    let beforeState = gameEngine.clone(); // TODO : clone AFTER the turn.
                    murmures.serverLog('State saved');
                    gameEngine.saveOrder(clientOrder);
                    let ge = gameEngine.compare(beforeState);
                    let res = JSON.stringify({ fn: 'o', payload: ge });
                    murmures.serverLog('Response stringified');
                    // broadcast to all clients
                    wss.clients.forEach(function each(client) {
                        client.send(res);
                    });
                }
                else {
                    ws.send(JSON.stringify({ fn: 'o', payload: { error: check.reason } }));
                }
            }
            else {
                ws.send(JSON.stringify({ fn: 'o', payload: { error: parsing.reason } }));
            }
            murmures.serverLog('Response sent');
        }
        else if (message.service === 'restart') {
            murmures.restartGame(message.payload);
            wss.clients.forEach(function each(client) {
                client.send(JSON.stringify({ fn: 'init', payload: gameEngine }));
            });
        }
        else if (message.service === 'test') {
            let test = new murmures.ServerTest();
            test.run(require);
        }
        else {
            murmures.serverLog('Received an incorrect request:' + messageTxt.toString());
        }
    });
});

murmures.serverLog('Listening on http://127.0.0.1:' + (process.env.PORT || 15881).toString() + '/');
<commit_msg>fixing unwanted rollback<commit_after>'use strict';

console.log(''); // new line
var vm = require('vm');
var fs = require('fs');
var zlib = require('zlib');
var http = require('http');
var WebSocketServer = require("ws").Server

/**
 * Declares the gameEngine variable for a later use.
 * This variable will be set to an instance of murmures.GameEngine after all classes are loaded.
 * @instance
 * @type {murmures.GameEngine}
 */
var gameEngine = {};

/**
 * The main namespace. All classes should be prefixed with it.
 * @namespace
 */
var murmures = {
    /**
     * Startup time.
     * @private
     */
    startTime: process.hrtime(),

    /**
     * Writes timestamped log to the console.
     * @public
     */
    serverLog: function (txt) {
        let diff = process.hrtime(this.startTime);
        let fdiff = diff[0] + diff[1] / 1e9;
        if (typeof txt !== 'undefined') {
            console.log(fdiff.toFixed(6) + ' - ' + txt);
        }
        return fdiff;
    },

    restartGame: function (targetLevel) {
        gameEngine.levels = [];
        gameEngine.activeLevel = 0;
        gameEngine.levelIds = fs.readdirSync('./data/staticlevels/', 'utf8');
        let loopCounter = 0;
        gameEngine.levelIds.forEach(function (levelName) {
            let level1Txt = fs.readFileSync('./data/staticlevels/' + levelName, 'utf8').toString().replace(/^\uFEFF/, '');
            let level1 = new murmures.Level();
            level1.build(JSON.parse(level1Txt));
            level1.id = levelName.replace('.json', '');
            gameEngine.levels.push(level1);
            if (typeof targetLevel !== 'undefined' && targetLevel && targetLevel === levelName) {
                gameEngine.activeLevel = loopCounter;
            }
            loopCounter++;
        }, this);
        gameEngine.level = gameEngine.levels[gameEngine.activeLevel];

        gameEngine.heros = [];
        let allHeroesKeys = [];
        for (let assetId in gameEngine.bodies) {
            let ref = gameEngine.bodies[assetId];
            if (murmures.C.LAYERS[ref.layerId][0] === 'Hero') allHeroesKeys.push(assetId);
        }

        let chosenHeroesKeys = [];
        let chosenHero;
        for (loopCounter = 0; loopCounter < 3; loopCounter++) {
            do {
                let rand = Math.floor(Math.random() * allHeroesKeys.length);
                chosenHero = allHeroesKeys[rand];
            } while (chosenHeroesKeys.indexOf(chosenHero) >= 0); // This loop prevents duplicate heroes
            chosenHeroesKeys.push(chosenHero);
            let hero1 = new murmures.Character();
            hero1.build(gameEngine.level.getStartingPoint(), chosenHero);
            hero1.typeCharacter = murmures.C.TYPE_CHARACTER_HERO;
            if (loopCounter === 0) {
                hero1.setVision();
                hero1.stateOrder = murmures.C.STATE_HERO_ORDER_INPROGRESS;
            }

            let chosenSkill= [];
            let nbSkill = Object.keys(gameEngine.skills).length;
            //mainSkill is used to retrieve the min id of the skills of the hero in the aim to active the first skil on browser
            //The skills are shown on browser in Id Order
            let mainSkill = -1;
            for(let loopSkill =0;loopSkill < 2;loopSkill ++){
              let rd=1;
              do{
                rd = (Math.floor(Math.random() *nbSkill) + 1)
              }while (chosenSkill.indexOf(rd)>=0);
              chosenSkill.push(rd);
              if (mainSkill == -1 || mainSkill > rd) mainSkill = rd;
              hero1.skills[rd] = gameEngine.skills[rd];
            }
            if (mainSkill != -1) hero1.activeSkill = mainSkill;

            gameEngine.heros.push(hero1);
        }

        gameEngine.reportQueue = [];
        gameEngine.state = murmures.C.STATE_ENGINE_INIT;
    }
};

murmures.serverLog('Loading classes');

/**
 * Loads classes into the murmures namespace
 */
(function () {
    const ctx = {
        murmures: murmures,
        gameEngine: {}
    };
    vm.createContext(ctx);

    let constantsjs = fs.readFileSync('./src/js/core/constants.js', 'utf8').toString().replace(/^\uFEFF/, '');
    vm.runInContext(constantsjs, ctx, { filename: 'constants.js' });
    let gameEnginejs = fs.readFileSync('./src/js/core/gameengine.js', 'utf8').toString().replace(/^\uFEFF/, '');
    vm.runInContext(gameEnginejs, ctx, { filename: 'gameengine.js' });

    ctx.gameEngine = new murmures.GameEngine();

    let playerjs = fs.readFileSync('./src/js/core/player.js', 'utf8').toString().replace(/^\uFEFF/, '');
    vm.runInContext(playerjs, ctx, { filename: 'player.js' });
    let tilejs = fs.readFileSync('./src/js/core/tile.js', 'utf8').toString().replace(/^\uFEFF/, '');
    vm.runInContext(tilejs, ctx, { filename: 'tile.js' });
    let leveljs = fs.readFileSync('./src/js/core/level.js', 'utf8').toString().replace(/^\uFEFF/, '');
    vm.runInContext(leveljs, ctx, { filename: 'level.js' });
    let characterjs = fs.readFileSync('./src/js/core/character.js', 'utf8').toString().replace(/^\uFEFF/, '');
    vm.runInContext(characterjs, ctx, { filename: 'character.js' });
    let orderjs = fs.readFileSync('./src/js/core/order.js', 'utf8').toString().replace(/^\uFEFF/, '');
    vm.runInContext(orderjs, ctx, { filename: 'order.js' });
    let turnReportjs = fs.readFileSync('./src/js/core/turnreport.js', 'utf8').toString().replace(/^\uFEFF/, '');
    vm.runInContext(turnReportjs, ctx, { filename: 'turnreport.js' });
    let physicalBodyjs = fs.readFileSync('./src/js/core/physicalbody.js', 'utf8').toString().replace(/^\uFEFF/, '');
    vm.runInContext(physicalBodyjs, ctx, { filename: 'physicalbody.js' });
    let behaviorjs = fs.readFileSync('./src/js/core/behavior.js', 'utf8').toString().replace(/^\uFEFF/, '');
    vm.runInContext(behaviorjs, ctx, { filename: 'behavior.js' });
    let skillbehaviorjs = fs.readFileSync('./src/js/core/skillbehavior.js', 'utf8').toString().replace(/^\uFEFF/, '');
    vm.runInContext(skillbehaviorjs, ctx, { filename: 'skillbehavior.js' });
    let skilljs = fs.readFileSync('./src/js/core/skill.js', 'utf8').toString().replace(/^\uFEFF/, '');
    vm.runInContext(skilljs, ctx, { filename: 'skill.js' });
    let vmperfjs = fs.readFileSync('./src/js/test/vmperf.js', 'utf8').toString().replace(/^\uFEFF/, '');
    vm.runInContext(vmperfjs, ctx, { filename: 'vmperf.js' });
    let servertestjs = fs.readFileSync('./src/js/test/servertest.js', 'utf8').toString().replace(/^\uFEFF/, '');
    vm.runInContext(servertestjs, ctx, { filename: 'servertest.js' });

    gameEngine = ctx.gameEngine;
})();

murmures.serverLog('Initializing game');

/**
 * Initializes game
 */
(function () {
    let assetsJson = fs.readFileSync('./data/reference/assets.json', 'utf8').toString().replace(/^\uFEFF/, '');
    gameEngine.bodies = JSON.parse(assetsJson);

    let localefrJson = fs.readFileSync('./data/locale/fr.json', 'utf8').toString().replace(/^\uFEFF/, '');
    let localeenJson = fs.readFileSync('./data/locale/en.json', 'utf8').toString().replace(/^\uFEFF/, '');
    gameEngine.locale = {};
    gameEngine.locale.fr = JSON.parse(localefrJson);
    gameEngine.locale.en = JSON.parse(localeenJson);

    let localSkills = JSON.parse(fs.readFileSync('./data/reference/skill.json', 'utf8').toString().replace(/^\uFEFF/, ''));
    for (let skillName in localSkills){
      let tempSkill = new murmures.Skill();
      tempSkill.build(localSkills[skillName],skillName);
      gameEngine.skills[tempSkill.id] = tempSkill;
    }
    murmures.restartGame();
})();

murmures.serverLog('Starting HTTP server');

// Tries to compress (gzip) the response, if the client browser allows it
function compressAndSend(request, response, contType, txt, callback) {
    let acceptEncoding = request.headers['accept-encoding'];
    if (!acceptEncoding) {
        acceptEncoding = '';
    }
    if (acceptEncoding.match(/\bgzip\b/) && contType !== 'image/png') {
        zlib.gzip(txt, function (err, zipped) {
            if (err) throw err;
            murmures.serverLog('Response compressed');
            response.writeHead(200, { 'Content-Type': contType, 'Content-Encoding': 'gzip' });
            response.end(zipped, 'utf8', callback);
        });
    } else if (contType === 'image/png') {
        let stats = fs.statSync('./src/img/murmures.png');
        response.writeHead(200, { 'Content-Type': contType, 'Content-Length': stats.size });
        response.end(txt);
    } else {
        response.writeHead(200, { 'Content-Type': contType });
        response.end(txt);
    }
}

var server = http.createServer(function (request, response) {
    if (request.url === '/favicon.ico') {
        response.writeHead(204); // No content
        response.end();
    }
    else if (request.url === '/') {
        response.writeHead(301, { 'Location': '/src/pages/client.html' });
        response.end();
    }
    else if (request.url === '/allClient.js') {
        fs.readFile('./src/js/client/client.js', function (clientErr, clientFile) {
            if (clientErr) throw clientErr;
            fs.readFile('./src/js/client/event.js', function (eventErr, eventFile) {
                if (eventErr) throw eventErr;
                fs.readFile('./src/js/client/renderer.js', function (rendererErr, rendererFile) {
                    if (rendererErr) throw rendererErr;
                    fs.readFile('./src/js/client/base.js', function (baseErr, baseFile) {
                        if (baseErr) throw baseErr;
                        compressAndSend(request, response, 'application/javascript', ''
                        + baseFile.toString() + '\n\n'
                        + rendererFile.toString().replace(/^\uFEFF/, '') + '\n\n' // strip BOM
                        + eventFile.toString().replace(/^\uFEFF/, '') + '\n\n' // strip BOM
                        + clientFile.toString().replace(/^\uFEFF/, '') + '\n\n' // strip BOM
                        );
                    });
                });
            });
        });
    }
    else if (request.url.startsWith('/src/')) {
        // #region Static Pages
        try {
            let fileName = request.url;
            fs.readFile('.' + fileName, function (err, fileContent) {
                if (err) throw err;
                if (fileName.endsWith('.js')) {
                    compressAndSend(request, response, 'application/javascript', fileContent.toString());
                }
                else if (fileName.endsWith('.css')) {
                    compressAndSend(request, response, 'text/css', fileContent.toString());
                }
                else if (fileName.endsWith('.png')) {
                    compressAndSend(request, response, 'image/png', fileContent);
                }
                else if (fileName.endsWith('.html')) {
                    compressAndSend(request, response, 'text/html', fileContent.toString());
                }
                else {
                    response.writeHead(400); // Bad Request
                    response.end();
                }
            });
        } catch (e) {
            response.writeHead(404);
            response.end();
        }
        // #endregion
    }
    else {
        response.writeHead(404);
        response.end();
    }
}).listen(process.env.PORT || 15881);

var wss = new WebSocketServer({ server: server })
wss.on('connection', function (ws) {
    ws.on('message', function (messageTxt) {
        let message = JSON.parse(messageTxt);
        if (message.service === 'getLevel') {
            ws.send(JSON.stringify({ fn: 'init', payload: gameEngine }));
        }
        else if (message.service === 'order') {
            murmures.serverLog('Request received');
            let clientOrder = new murmures.Order();
            let parsing = clientOrder.build(message.payload);
            if (parsing.valid) {
                let check = gameEngine.checkOrder(clientOrder);
                if (check.valid) {
                    //gameEngine.gameTurn++;
                    murmures.serverLog('Order checked');
                    let beforeState = gameEngine.clone(); // TODO : clone AFTER the turn.
                    murmures.serverLog('State saved');
                    gameEngine.saveOrder(clientOrder);
                    let ge = gameEngine.compare(beforeState);
                    let res = JSON.stringify({ fn: 'o', payload: ge });
                    murmures.serverLog('Response stringified');
                    // broadcast to all clients
                    wss.clients.forEach(function each(client) {
                        client.send(res);
                    });
                }
                else {
                    ws.send(JSON.stringify({ fn: 'o', payload: { error: check.reason } }));
                }
            }
            else {
                ws.send(JSON.stringify({ fn: 'o', payload: { error: parsing.reason } }));
            }
            murmures.serverLog('Response sent');
        }
        else if (message.service === 'restart') {
            murmures.restartGame(message.payload);
            wss.clients.forEach(function each(client) {
                client.send(JSON.stringify({ fn: 'init', payload: gameEngine }));
            });
        }
        else if (message.service === 'test') {
            let test = new murmures.ServerTest();
            test.run(require);
        }
        else {
            murmures.serverLog('Received an incorrect request:' + messageTxt.toString());
        }
    });
});

murmures.serverLog('Listening on http://127.0.0.1:' + (process.env.PORT || 15881).toString() + '/');
<|endoftext|>


<commit_before>/*
* simplestore
* Copyright 2015 Chris Diana
* https://github.com/cdmedia/simplestore
* Free to use under the MIT license.
* http://www.opensource.org/licenses/mit-license.php
*/

var simpleStore = {

    products: [],
    plugins: {},

    // Default settings
    settings: {
        numColumns: 3,
        brand: "SimpleStore",
        mode: "JSON",
        JSONFile: "products.json",
        fadeSpeed: 200,
        buttonColor: null,
        backgroundColor: null,
        textColor: null,
        container: $('.simpleStore_container'),
        cartContainer: $('.simpleStore_cart_container'),
        rowClass: 'simpleStore_row_',
        columnWidthClasses: {
            1: "",
            2: "one-half",
            3: "one-third"
        }
    },

    productPageOptions: [
        'OneOfAKind'
    ],

    extend: function (target, opts, callback) {
        var next;
        if (typeof opts === "undefined") {
            opts = target;
            target = simpleStore;
        }
        for (next in opts) {
            if (Object.prototype.hasOwnProperty.call(opts, next)) {
                target[next] = opts[next];
            }
        }
        callback(); // check user config options
        return target;
    },

    render: function (url, s) {
        var type = url.split('/')[0];

        var map = {
            // Main view
            '': function () {
                simpleStore.renderProducts(simpleStore.products, s);
            },
            // Detail view
            '#product': function () {
                var id = url.split('#product/')[1].trim();
                simpleStore.renderSingleProduct(id, s);
            },
            // Cart view
            '#cart': function () {
                simpleStore.renderCart(s);
            }
        };

        if (map[type]) {
            map[type]();
        } else {
            simpleStore.renderError(s);
        }
    },

    insertData: function (tmpl, product) {
        tmpl.find('.item_thumb').attr("src", product.image);
        tmpl.find('.item_name').text(product.name);
        tmpl.find('.item_price').text(product.price);
        tmpl.find('.item_description').text(product.description);
    },

    renderProducts: function (products, s) {

        var rowCount = 1,
            numProducts = products.length,
            numRows = Math.ceil(products.length / s.numColumns),
            itemWidth;

        s.cartContainer.hide();
        s.container.fadeOut(s.fadeSpeed, function () {

            // Empty out main container on load
            s.container.html('').fadeIn(s.fadeSpeed);

            // Build rows based on number of products
            for (var r = 0; r < numRows; r++) {
                s.container.append('

');
            }

            // Get item column width
            var widthClasses = s.columnWidthClasses;
            for (var k in widthClasses) {
                if (k == s.numColumns) {
                    itemWidth = widthClasses[k];
                }
            }

            // List layout
            products.forEach(function (product, i) {
                var tmpl = $('#products-template').html(),
                    $tmpl = $(tmpl);

                // Set item width
                $tmpl.first().addClass(itemWidth);

                // Insert data into template
                simpleStore.insertData($tmpl, product);

                // Render detail view on hash change
                var getDetail = $tmpl.find('.simpleStore_getDetail');
                getDetail.on('click', function (e) {
                    e.preventDefault();
                    window.location.hash = 'product/' + product.id;
                });

                // Check where to add new item based on row
                if (i === 0) {
                    i = 1;
                }
                if (i % (s.numColumns) === 0) {
                    rowCount++;
                }

                // Append to appropriate container
                $('.' + s.rowClass + rowCount).append($tmpl);
            });
        });
    },

    renderProductOptions: function (options, s) {

        var optionsLayout = '';

        options.forEach(function (option) {
            if (!(simpleStore.productPageOptions in option)) {
                var selectItems = '';
                var attributeLabel = Object.keys(option)[0].trim();
                var attributeValues = option[attributeLabel].trim().split(",");

                // Set attribute values
                $(attributeValues).each(function (attribute, attributeValue) {
                    selectItems += '<option value="' + attributeValue.replace(/ /g, "_").toLowerCase() + '"> ' + attributeValue + ' </option>';
                });

                // Build options layout
                if (options.length) {
                    optionsLayout += '' + attributeLabel + '<select class="item_' + attributeLabel.replace(/ /g, "_").toLowerCase() + '">' + selectItems + '</select>';
                }
            } else {
                simpleStore.renderProductPageOptions(option);
            }
        });

        return optionsLayout;
    },

    renderProductPageOptions: function (option) {
        if (option.OneOfAKind) {
            $('.item_qty').hide();
        }
    },

    renderSingleProduct: function (id, s) {

        s.container.fadeOut(s.fadeSpeed, function () {

            var tmpl = $('#product-detail-template').html(),
                $tmpl = $(tmpl);

            simpleStore.products.forEach(function (product) {
                if (product.id == id) {

                    // Insert data into template
                    simpleStore.insertData($tmpl, product);

                    // Load detail view into main container
                    s.container.html($tmpl);

                    // Render product options
                    if (product.options.length) {
                        var options = simpleStore.renderProductOptions(product.options, s);
                        $('.simpleStore_options').append(options);
                    }
                    s.container.fadeIn(s.fadeSpeed);
                }
            });
        });
    },

    renderCart: function (s) {
        s.container.fadeOut(s.fadeSpeed, function () {
            s.cartContainer.fadeIn(s.fadeSpeed);
        });
    },

    renderError: function (s, msg) {
        var tmpl = $('#error-template').html(),
            $tmpl = $(tmpl);

		// Empty out main container on load
		s.container.html('').fadeIn(s.fadeSpeed);

		if (msg.length) {
			$tmpl.find('.error_text').text(msg);
		}
		s.container.append($tmpl);
		s.container.fadeIn(s.fadeSpeed);

		$tmpl.find('.alert_close').on('click', function (e) {
			e.preventDefault();
			$tmpl.fadeOut(s.fadeSpeed, function() {
				$tmpl.remove();
			});
		});
    },

	handleFailure:  function(s, errorMsg) {
		setTimeout(function () {
			simpleStore.renderError(s, errorMsg);
		}, 1000);
	},

    initJSON: function (s) {
        var errorMsg = 'There was an error loading the JSON file.' +
            ' Please make sure you have "' + s.JSONFile + '" file in' +
            ' your main directory.';

        // Checks to make sure file exists
        $.get(s.JSONFile)
            .success(function () {
                // Get product data from JSON file
                $.getJSON(s.JSONFile, function (data) {
                    simpleStore.setProducts(data.products);
                })
                .fail(function () { simpleStore.handleFailure(s, errorMsg); });
            })
            .fail(function () { simpleStore.handleFailure(s, errorMsg); });
    },

    checkMode : function (s) {
        if (s.hasOwnProperty("spreadsheetID") || s.hasOwnProperty("spreadsheetId")) {
            s.mode = "Google";
        }
    },

	checkout : function (s, checkoutData) {
		if (!$.isEmptyObject(checkoutData)) {
        	simpleCart.checkout();
			s.cartContainer.fadeOut(s.fadeSpeed, function () {
				s.container.html('');
				s.container.fadeIn(s.fadeSpeed);
			});
		}
	},

	verifyCheckoutData : function (cdata, adata, v) {
		for (var d in cdata) {
			if (cdata.hasOwnProperty(d)) {
				var cp = cdata[d], cn = cp.name, cpp = cp.price;
				for (var i = 0; i < adata.length; i++) {
					var ap = adata[i], an = ap.name, app = ap.price;
					if (cn === an) {if (cpp != app) { v = false; }}
				}
			}
		}
		return v;
	},

    validatePrices : function (s) {
        var checkoutData = JSON.parse(localStorage.simpleCart_items),
			errorMsg = 'There was an error validating your cart.';

		if (s.mode === "JSON") {
			 $.get(s.JSONFile)
				.success(function () {
					$.getJSON(s.JSONFile, function (data) {
						var JSONData = data.products;
						if (simpleStore.verifyCheckoutData(checkoutData, JSONData, true)) {
        					simpleStore.checkout(s, checkoutData);
						} else {
							simpleStore.renderError(s, errorMsg);
						}
					})
                	.fail(function () { simpleStore.handleFailure(s, errorMsg); });
				})
                .fail(function () { simpleStore.handleFailure(s, errorMsg); });
		} else {
			var plugin = s.mode.toLowerCase();
			if(simpleStore.plugins[plugin]) {
				simpleStore.plugins[plugin].validate(checkoutData);
			}
		}
    },

    setProducts: function (products, s) {
        if (products.length > 0) {
            products.forEach(function (product, index) {
                product.id = index + 1;
                simpleStore.products.push(product);
            });
        }

        // Manually trigger on initial load
        $(window).trigger('hashchange');
    },

	setLayout: function (s) {
		// Set brand
        if (s.brand.match('^http://') || s.brand.match('^https://') || s.brand.match('^www.')) {
            $('.brand').html('');
        } else {
            $('.brand').html('
' + s.brand + '
');
        }

		// Set title
		$('title').html(s.brand);
	},

    generateCart: function (s) {
        var tmpl = $('#cart-template').html(),
            $tmpl = $(tmpl);
        s.cartContainer.html($tmpl);
    },

    generateStore: function () {

        var s = this.settings;

        // Set mode
        this.checkMode(s);

        // Check for hash changes
        $(window).on('hashchange', function () {
            simpleStore.render(window.location.hash, s);
        });

        // Set products based on mode
        switch (s.mode) {
            case 'JSON':
                this.initJSON(s);
                break;
            case 'Google':
				if(simpleStore.plugins.google) {
					simpleStore.plugins.google.init(function (products) {
						simpleStore.setProducts(products, s);
					});
				} else {
					var errorMsg = 'There was an error loading the Google plugin. Make sure it is installed properly.';
					simpleStore.renderError(s, errorMsg);
				}
                break;
            default:
                this.initJSON(s);
        }

        // Because simpleCart items appends to cart, set up only once
        this.generateCart(s);

        // Setup layout
        this.setLayout(s);

		// Handle Checkout
        $('.simpleStore_checkout').on('click', function (e) {
            e.preventDefault();
            simpleStore.validatePrices(s);
        });

        // View Cart
        $('.simpleStore_viewCart').on('click', function (e) {
            e.preventDefault();
            window.location = '#cart';
        });

        // Go to home on close
        $('.view_close').on('click', function (e) {
            e.preventDefault();
            window.location.hash = '';
        });
    },

    init: function (options) {
        if ($.isPlainObject(options)) {
            return this.extend(this.settings, options, function () {
                simpleStore.generateStore();
            });
        }
    }
};
<commit_msg>Fix quantity selector for one-of-a-kind items<commit_after>/*
* simplestore
* Copyright 2015 Chris Diana
* https://github.com/cdmedia/simplestore
* Free to use under the MIT license.
* http://www.opensource.org/licenses/mit-license.php
*/

var simpleStore = {

    products: [],
    plugins: {},

    // Default settings
    settings: {
        numColumns: 3,
        brand: "SimpleStore",
        mode: "JSON",
        JSONFile: "products.json",
        fadeSpeed: 200,
        buttonColor: null,
        backgroundColor: null,
        textColor: null,
        container: $('.simpleStore_container'),
        cartContainer: $('.simpleStore_cart_container'),
        rowClass: 'simpleStore_row_',
        columnWidthClasses: {
            1: "",
            2: "one-half",
            3: "one-third"
        }
    },

    productPageOptions: [
        'OneOfAKind'
    ],

    extend: function (target, opts, callback) {
        var next;
        if (typeof opts === "undefined") {
            opts = target;
            target = simpleStore;
        }
        for (next in opts) {
            if (Object.prototype.hasOwnProperty.call(opts, next)) {
                target[next] = opts[next];
            }
        }
        callback(); // check user config options
        return target;
    },

    render: function (url, s) {
        var type = url.split('/')[0];

        var map = {
            // Main view
            '': function () {
                simpleStore.renderProducts(simpleStore.products, s);
            },
            // Detail view
            '#product': function () {
                var id = url.split('#product/')[1].trim();
                simpleStore.renderSingleProduct(id, s);
            },
            // Cart view
            '#cart': function () {
                simpleStore.renderCart(s);
            }
        };

        if (map[type]) {
            map[type]();
        } else {
            simpleStore.renderError(s);
        }
    },

    insertData: function (tmpl, product) {
        tmpl.find('.item_thumb').attr("src", product.image);
        tmpl.find('.item_name').text(product.name);
        tmpl.find('.item_price').text(product.price);
        tmpl.find('.item_description').text(product.description);
    },

    renderProducts: function (products, s) {

        var rowCount = 1,
            numProducts = products.length,
            numRows = Math.ceil(products.length / s.numColumns),
            itemWidth;

        s.cartContainer.hide();
        s.container.fadeOut(s.fadeSpeed, function () {

            // Empty out main container on load
            s.container.html('').fadeIn(s.fadeSpeed);

            // Build rows based on number of products
            for (var r = 0; r < numRows; r++) {
                s.container.append('

');
            }

            // Get item column width
            var widthClasses = s.columnWidthClasses;
            for (var k in widthClasses) {
                if (k == s.numColumns) {
                    itemWidth = widthClasses[k];
                }
            }

            // List layout
            products.forEach(function (product, i) {
                var tmpl = $('#products-template').html(),
                    $tmpl = $(tmpl);

                // Set item width
                $tmpl.first().addClass(itemWidth);

                // Insert data into template
                simpleStore.insertData($tmpl, product);

                // Render detail view on hash change
                var getDetail = $tmpl.find('.simpleStore_getDetail');
                getDetail.on('click', function (e) {
                    e.preventDefault();
                    window.location.hash = 'product/' + product.id;
                });

                // Check where to add new item based on row
                if (i === 0) {
                    i = 1;
                }
                if (i % (s.numColumns) === 0) {
                    rowCount++;
                }

                // Append to appropriate container
                $('.' + s.rowClass + rowCount).append($tmpl);
            });
        });
    },

    renderProductOptions: function (options, s) {

        var optionsLayout = '';

        options.forEach(function (option) {
            if (!(simpleStore.productPageOptions in option)) {
                var selectItems = '';
                var attributeLabel = Object.keys(option)[0].trim();
                var attributeValues = option[attributeLabel].trim().split(",");

                // Set attribute values
                $(attributeValues).each(function (attribute, attributeValue) {
                    selectItems += '<option value="' + attributeValue.replace(/ /g, "_").toLowerCase() + '"> ' + attributeValue + ' </option>';
                });

                // Build options layout
                if (options.length) {
                    optionsLayout += '' + attributeLabel + '<select class="item_' + attributeLabel.replace(/ /g, "_").toLowerCase() + '">' + selectItems + '</select>';
                }
            } else {
                simpleStore.renderProductPageOptions(option);
            }
        });

        return optionsLayout;
    },

    renderProductPageOptions: function (option) {
        if (option.OneOfAKind) {
            $('.qty').hide();
        }
    },

    renderSingleProduct: function (id, s) {

        s.container.fadeOut(s.fadeSpeed, function () {

            var tmpl = $('#product-detail-template').html(),
                $tmpl = $(tmpl);

            simpleStore.products.forEach(function (product) {
                if (product.id == id) {

                    // Insert data into template
                    simpleStore.insertData($tmpl, product);

                    // Load detail view into main container
                    s.container.html($tmpl);

                    // Render product options
                    if (product.options.length) {
                        var options = simpleStore.renderProductOptions(product.options, s);
                        $('.simpleStore_options').append(options);
                    }
                    s.container.fadeIn(s.fadeSpeed);
                }
            });
        });
    },

    renderCart: function (s) {
        s.container.fadeOut(s.fadeSpeed, function () {
            s.cartContainer.fadeIn(s.fadeSpeed);
        });
    },

    renderError: function (s, msg) {
        var tmpl = $('#error-template').html(),
            $tmpl = $(tmpl);

		// Empty out main container on load
		s.container.html('').fadeIn(s.fadeSpeed);

		if (msg.length) {
			$tmpl.find('.error_text').text(msg);
		}
		s.container.append($tmpl);
		s.container.fadeIn(s.fadeSpeed);

		$tmpl.find('.alert_close').on('click', function (e) {
			e.preventDefault();
			$tmpl.fadeOut(s.fadeSpeed, function() {
				$tmpl.remove();
			});
		});
    },

	handleFailure:  function(s, errorMsg) {
		setTimeout(function () {
			simpleStore.renderError(s, errorMsg);
		}, 1000);
	},

    initJSON: function (s) {
        var errorMsg = 'There was an error loading the JSON file.' +
            ' Please make sure you have "' + s.JSONFile + '" file in' +
            ' your main directory.';

        // Checks to make sure file exists
        $.get(s.JSONFile)
            .success(function () {
                // Get product data from JSON file
                $.getJSON(s.JSONFile, function (data) {
                    simpleStore.setProducts(data.products);
                })
                .fail(function () { simpleStore.handleFailure(s, errorMsg); });
            })
            .fail(function () { simpleStore.handleFailure(s, errorMsg); });
    },

    checkMode : function (s) {
        if (s.hasOwnProperty("spreadsheetID") || s.hasOwnProperty("spreadsheetId")) {
            s.mode = "Google";
        }
    },

	checkout : function (s, checkoutData) {
		if (!$.isEmptyObject(checkoutData)) {
        	simpleCart.checkout();
			s.cartContainer.fadeOut(s.fadeSpeed, function () {
				s.container.html('');
				s.container.fadeIn(s.fadeSpeed);
			});
		}
	},

	verifyCheckoutData : function (cdata, adata, v) {
		for (var d in cdata) {
			if (cdata.hasOwnProperty(d)) {
				var cp = cdata[d], cn = cp.name, cpp = cp.price;
				for (var i = 0; i < adata.length; i++) {
					var ap = adata[i], an = ap.name, app = ap.price;
					if (cn === an) {if (cpp != app) { v = false; }}
				}
			}
		}
		return v;
	},

    validatePrices : function (s) {
        var checkoutData = JSON.parse(localStorage.simpleCart_items),
			errorMsg = 'There was an error validating your cart.';

		if (s.mode === "JSON") {
			 $.get(s.JSONFile)
				.success(function () {
					$.getJSON(s.JSONFile, function (data) {
						var JSONData = data.products;
						if (simpleStore.verifyCheckoutData(checkoutData, JSONData, true)) {
        					simpleStore.checkout(s, checkoutData);
						} else {
							simpleStore.renderError(s, errorMsg);
						}
					})
                	.fail(function () { simpleStore.handleFailure(s, errorMsg); });
				})
                .fail(function () { simpleStore.handleFailure(s, errorMsg); });
		} else {
			var plugin = s.mode.toLowerCase();
			if(simpleStore.plugins[plugin]) {
				simpleStore.plugins[plugin].validate(checkoutData);
			}
		}
    },

    setProducts: function (products, s) {
        if (products.length > 0) {
            products.forEach(function (product, index) {
                product.id = index + 1;
                simpleStore.products.push(product);
            });
        }

        // Manually trigger on initial load
        $(window).trigger('hashchange');
    },

	setLayout: function (s) {
		// Set brand
        if (s.brand.match('^http://') || s.brand.match('^https://') || s.brand.match('^www.')) {
            $('.brand').html('');
        } else {
            $('.brand').html('
' + s.brand + '
');
        }

		// Set title
		$('title').html(s.brand);
	},

    generateCart: function (s) {
        var tmpl = $('#cart-template').html(),
            $tmpl = $(tmpl);
        s.cartContainer.html($tmpl);
    },

    generateStore: function () {

        var s = this.settings;

        // Set mode
        this.checkMode(s);

        // Check for hash changes
        $(window).on('hashchange', function () {
            simpleStore.render(window.location.hash, s);
        });

        // Set products based on mode
        switch (s.mode) {
            case 'JSON':
                this.initJSON(s);
                break;
            case 'Google':
				if(simpleStore.plugins.google) {
					simpleStore.plugins.google.init(function (products) {
						simpleStore.setProducts(products, s);
					});
				} else {
					var errorMsg = 'There was an error loading the Google plugin. Make sure it is installed properly.';
					simpleStore.renderError(s, errorMsg);
				}
                break;
            default:
                this.initJSON(s);
        }

        // Because simpleCart items appends to cart, set up only once
        this.generateCart(s);

        // Setup layout
        this.setLayout(s);

		// Handle Checkout
        $('.simpleStore_checkout').on('click', function (e) {
            e.preventDefault();
            simpleStore.validatePrices(s);
        });

        // View Cart
        $('.simpleStore_viewCart').on('click', function (e) {
            e.preventDefault();
            window.location = '#cart';
        });

        // Go to home on close
        $('.view_close').on('click', function (e) {
            e.preventDefault();
            window.location.hash = '';
        });
    },

    init: function (options) {
        if ($.isPlainObject(options)) {
            return this.extend(this.settings, options, function () {
                simpleStore.generateStore();
            });
        }
    }
};
<|endoftext|>