<commit_before>import _ from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import {
  FormattedMessage,
  injectIntl,
  intlShape,
} from 'react-intl';

import { makeQueryFunction, SearchAndSort } from '@folio/stripes/smart-components';
import { AppIcon } from '@folio/stripes/core';

import uuid from 'uuid';
import ViewUser from './ViewUser';
import UserForm from './UserForm';
import { toUserAddresses } from './converters/address';
import { getFullName } from './util';
import packageInfo from '../package';
import { HasCommand } from './components/Commander';

const INITIAL_RESULT_COUNT = 30;
const RESULT_COUNT_INCREMENT = 30;

const filterConfig = [
  {
    label: <FormattedMessage id="ui-users.status" />,
    name: 'active',
    cql: 'active',
    values: [
      { name: 'Include inactive users', cql: 'false' },
      { name: 'Show active users', cql: 'true', hidden: true },
    ],
  },
  {
    label: <FormattedMessage id="ui-users.information.patronGroup" />,
    name: 'pg',
    cql: 'patronGroup',
    values: [], // will be filled in by componentDidUpdate
  },
];

class Users extends React.Component {
  static manifest = Object.freeze({
    initializedFilterConfig: { initialValue: false },
    query: { initialValue: { sort: 'name' } },
    resultCount: { initialValue: INITIAL_RESULT_COUNT },
    records: {
      type: 'okapi',
      records: 'users',
      recordsRequired: '%{resultCount}',
      perRequest: 30,
      path: 'users',
      GET: {
        params: {
          query: makeQueryFunction(
            'cql.allRecords=1',
            '(username="%{query.query}*" or personal.firstName="%{query.query}*" or personal.lastName="%{query.query}*" or personal.email="%{query.query}*" or barcode="%{query.query}*" or id="%{query.query}*" or externalSystemId="%{query.query}*")',
            {
              // the keys in this object must match those passed to
              // SearchAndSort's columnMapping prop
              'active': 'active',
              'name': 'personal.lastName personal.firstName',
              'patronGroup': 'patronGroup.group',
              'username': 'username',
              'barcode': 'barcode',
              'email': 'personal.email',
            },
            filterConfig,
            2,
          ),
        },
        staticFallback: { params: {} },
      },
    },
    creds: {
      type: 'okapi',
      path: 'authn/credentials',
      fetch: false,
    },
    perms: {
      type: 'okapi',
      path: 'perms/users',
      fetch: false,
    },
    patronGroups: {
      type: 'okapi',
      path: 'groups',
      params: {
        query: 'cql.allRecords=1 sortby group',
        limit: '40',
      },
      records: 'usergroups',
    },
    addressTypes: {
      type: 'okapi',
      path: 'addresstypes?query=cql.allRecords=1 sortby desc',
      records: 'addressTypes',
    },
    uniquenessValidator: {
      type: 'okapi',
      records: 'users',
      accumulate: 'true',
      path: 'users',
      fetch: false,
    },
  });

  static propTypes = {
    resources: PropTypes.shape({
      patronGroups: PropTypes.shape({
        records: PropTypes.arrayOf(PropTypes.object),
      }),
      addressTypes: PropTypes.shape({
        records: PropTypes.arrayOf(PropTypes.object),
      }),
    }).isRequired,
    mutator: PropTypes.shape({
      creds: PropTypes.shape({
        POST: PropTypes.func.isRequired,
      }),
      initializedFilterConfig: PropTypes.shape({
        replace: PropTypes.func.isRequired,
      }),
      perms: PropTypes.shape({
        POST: PropTypes.func.isRequired,
      }),
      records: PropTypes.shape({
        POST: PropTypes.func.isRequired,
      }),
    }).isRequired,
    onSelectRow: PropTypes.func,
    onComponentWillUnmount: PropTypes.func,
    visibleColumns: PropTypes.arrayOf(PropTypes.string),
    disableRecordCreation: PropTypes.bool,
    showSingleResult: PropTypes.bool, // eslint-disable-line react/no-unused-prop-types
    browseOnly: PropTypes.bool,
    intl: intlShape.isRequired,
    packageInfo: PropTypes.object,
  };

  static defaultProps = {
    showSingleResult: true,
    browseOnly: false,
  }

  constructor(props) {
    super(props);
    this.keyboardCommands = [
      {
        name: 'new',
        handler: this.goToNew
      },
      {
        name: 'search',
        handler: this.goToSearch
      }
    ];
  }

  componentDidUpdate() {
    const pg = (this.props.resources.patronGroups || {}).records || [];
    if (pg.length) {
      const pgFilterConfig = filterConfig.find(group => group.name === 'pg');
      const oldValuesLength = pgFilterConfig.values.length;
      pgFilterConfig.values = pg.map(rec => ({ name: rec.group, cql: rec.id }));
      if (oldValuesLength === 0) {
        this.props.mutator.initializedFilterConfig.replace(true); // triggers refresh of users
      }
    }
  }

  // XXX something prevents exceptions in this function from being received: see STRIPES-483
  create = (userdata) => {
    const { mutator } = this.props;
    if (userdata.username) {
      const creds = Object.assign({}, userdata.creds, { username: userdata.username }, userdata.creds.password ? {} : { password: '' });
      const user = Object.assign({}, userdata, { id: uuid() });
      if (user.creds) delete user.creds;

      mutator.records.POST(user)
        .then(newUser => mutator.creds.POST(Object.assign(creds, { userId: newUser.id })))
        .then(newCreds => mutator.perms.POST({ userId: newCreds.userId, permissions: [] }))
        .then((perms) => {
          mutator.query.update({ _path: `/users/view/${perms.userId}`, layer: null });
        });
    } else {
      const user = Object.assign({}, userdata, { id: uuid() });
      if (user.creds) delete user.creds;

      mutator.records.POST(user)
        .then((newUser) => mutator.perms.POST({ userId: newUser.id, permissions: [] }))
        .then((perms) => {
          mutator.query.update({ _path: `/users/view/${perms.userId}`, layer: null });
        });
    }
  }

  massageNewRecord = (userdata) => {
    if (userdata.personal.addresses) {
      const addressTypes = (this.props.resources.addressTypes || {}).records || [];
      // eslint-disable-next-line no-param-reassign
      userdata.personal.addresses = toUserAddresses(userdata.personal.addresses, addressTypes);
    }
  }

  goToNew = () => {
    const { mutator } = this.props;
    mutator.query.update({ layer: 'create' });
  }

  goToSearch = () => {
    const { mutator } = this.props;
    mutator.query.update({ layer: null });
    const searchField = document.getElementById('input-user-search');
    if (searchField) {
      searchField.focus();
    }
  }

  render() {
    const {
      onSelectRow,
      disableRecordCreation,
      onComponentWillUnmount,
      showSingleResult,
      browseOnly,
      intl,
    } = this.props;

    const patronGroups = (this.props.resources.patronGroups || {}).records || [];

    const resultsFormatter = {
      status: user => (
        <AppIcon app="users" size="small">
          {user.active ? <FormattedMessage id="ui-users.active" /> : <FormattedMessage id="ui-users.inactive" />}
        </AppIcon>
      ),
      name: user => getFullName(user),
      barcode: user => user.barcode,
      patronGroup: (user) => {
        const pg = patronGroups.filter(g => g.id === user.patronGroup)[0];
        return pg ? pg.group : '?';
      },
      username: user => user.username,
      email: user => _.get(user, ['personal', 'email']),
    };

    return (
      <div data-test-user-instances>
        <HasCommand commands={this.keyboardCommands}>
          <SearchAndSort
            packageInfo={this.props.packageInfo || packageInfo}
            objectName="user"
            filterConfig={filterConfig}
            initialResultCount={INITIAL_RESULT_COUNT}
            resultCountIncrement={RESULT_COUNT_INCREMENT}
            viewRecordComponent={ViewUser}
            editRecordComponent={UserForm}
            newRecordInitialValues={{ active: true, personal: { preferredContactTypeId: '002' } }}
            visibleColumns={this.props.visibleColumns ? this.props.visibleColumns : ['status', 'name', 'barcode', 'patronGroup', 'username', 'email']}
            resultsFormatter={resultsFormatter}
            onSelectRow={onSelectRow}
            onCreate={this.create}
            onComponentWillUnmount={onComponentWillUnmount}
            massageNewRecord={this.massageNewRecord}
            finishedResourceName="perms"
            viewRecordPerms="users.item.get"
            newRecordPerms="users.item.post,login.item.post,perms.users.item.post"
            disableRecordCreation={disableRecordCreation}
            parentResources={this.props.resources}
            parentMutator={this.props.mutator}
            showSingleResult={showSingleResult}
            columnMapping={{
              status: intl.formatMessage({ id: 'ui-users.active' }),
              name: intl.formatMessage({ id: 'ui-users.information.name' }),
              barcode: intl.formatMessage({ id: 'ui-users.information.barcode' }),
              patronGroup: intl.formatMessage({ id: 'ui-users.information.patronGroup' }),
              username: intl.formatMessage({ id: 'ui-users.information.username' }),
              email: intl.formatMessage({ id: 'ui-users.contact.email' }),
            }}
            browseOnly={browseOnly}
          />
        </HasCommand>
      </div>);
  }
}

export default injectIntl(Users);
<commit_msg>Match visible column name to the corresponding property on the backend, refs UIU-962 (#788)<commit_after>import _ from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import {
  FormattedMessage,
  injectIntl,
  intlShape,
} from 'react-intl';

import { makeQueryFunction, SearchAndSort } from '@folio/stripes/smart-components';
import { AppIcon } from '@folio/stripes/core';

import uuid from 'uuid';
import ViewUser from './ViewUser';
import UserForm from './UserForm';
import { toUserAddresses } from './converters/address';
import { getFullName } from './util';
import packageInfo from '../package';
import { HasCommand } from './components/Commander';

const INITIAL_RESULT_COUNT = 30;
const RESULT_COUNT_INCREMENT = 30;

const filterConfig = [
  {
    label: <FormattedMessage id="ui-users.status" />,
    name: 'active',
    cql: 'active',
    values: [
      { name: 'Include inactive users', cql: 'false' },
      { name: 'Show active users', cql: 'true', hidden: true },
    ],
  },
  {
    label: <FormattedMessage id="ui-users.information.patronGroup" />,
    name: 'pg',
    cql: 'patronGroup',
    values: [], // will be filled in by componentDidUpdate
  },
];

class Users extends React.Component {
  static manifest = Object.freeze({
    initializedFilterConfig: { initialValue: false },
    query: { initialValue: { sort: 'name' } },
    resultCount: { initialValue: INITIAL_RESULT_COUNT },
    records: {
      type: 'okapi',
      records: 'users',
      recordsRequired: '%{resultCount}',
      perRequest: 30,
      path: 'users',
      GET: {
        params: {
          query: makeQueryFunction(
            'cql.allRecords=1',
            '(username="%{query.query}*" or personal.firstName="%{query.query}*" or personal.lastName="%{query.query}*" or personal.email="%{query.query}*" or barcode="%{query.query}*" or id="%{query.query}*" or externalSystemId="%{query.query}*")',
            {
              // the keys in this object must match those passed to
              // SearchAndSort's columnMapping prop
              'active': 'active',
              'name': 'personal.lastName personal.firstName',
              'patronGroup': 'patronGroup.group',
              'username': 'username',
              'barcode': 'barcode',
              'email': 'personal.email',
            },
            filterConfig,
            2,
          ),
        },
        staticFallback: { params: {} },
      },
    },
    creds: {
      type: 'okapi',
      path: 'authn/credentials',
      fetch: false,
    },
    perms: {
      type: 'okapi',
      path: 'perms/users',
      fetch: false,
    },
    patronGroups: {
      type: 'okapi',
      path: 'groups',
      params: {
        query: 'cql.allRecords=1 sortby group',
        limit: '40',
      },
      records: 'usergroups',
    },
    addressTypes: {
      type: 'okapi',
      path: 'addresstypes?query=cql.allRecords=1 sortby desc',
      records: 'addressTypes',
    },
    uniquenessValidator: {
      type: 'okapi',
      records: 'users',
      accumulate: 'true',
      path: 'users',
      fetch: false,
    },
  });

  static propTypes = {
    resources: PropTypes.shape({
      patronGroups: PropTypes.shape({
        records: PropTypes.arrayOf(PropTypes.object),
      }),
      addressTypes: PropTypes.shape({
        records: PropTypes.arrayOf(PropTypes.object),
      }),
    }).isRequired,
    mutator: PropTypes.shape({
      creds: PropTypes.shape({
        POST: PropTypes.func.isRequired,
      }),
      initializedFilterConfig: PropTypes.shape({
        replace: PropTypes.func.isRequired,
      }),
      perms: PropTypes.shape({
        POST: PropTypes.func.isRequired,
      }),
      records: PropTypes.shape({
        POST: PropTypes.func.isRequired,
      }),
    }).isRequired,
    onSelectRow: PropTypes.func,
    onComponentWillUnmount: PropTypes.func,
    visibleColumns: PropTypes.arrayOf(PropTypes.string),
    disableRecordCreation: PropTypes.bool,
    showSingleResult: PropTypes.bool, // eslint-disable-line react/no-unused-prop-types
    browseOnly: PropTypes.bool,
    intl: intlShape.isRequired,
    packageInfo: PropTypes.object,
  };

  static defaultProps = {
    showSingleResult: true,
    browseOnly: false,
  }

  constructor(props) {
    super(props);
    this.keyboardCommands = [
      {
        name: 'new',
        handler: this.goToNew
      },
      {
        name: 'search',
        handler: this.goToSearch
      }
    ];
  }

  componentDidUpdate() {
    const pg = (this.props.resources.patronGroups || {}).records || [];
    if (pg.length) {
      const pgFilterConfig = filterConfig.find(group => group.name === 'pg');
      const oldValuesLength = pgFilterConfig.values.length;
      pgFilterConfig.values = pg.map(rec => ({ name: rec.group, cql: rec.id }));
      if (oldValuesLength === 0) {
        this.props.mutator.initializedFilterConfig.replace(true); // triggers refresh of users
      }
    }
  }

  // XXX something prevents exceptions in this function from being received: see STRIPES-483
  create = (userdata) => {
    const { mutator } = this.props;
    if (userdata.username) {
      const creds = Object.assign({}, userdata.creds, { username: userdata.username }, userdata.creds.password ? {} : { password: '' });
      const user = Object.assign({}, userdata, { id: uuid() });
      if (user.creds) delete user.creds;

      mutator.records.POST(user)
        .then(newUser => mutator.creds.POST(Object.assign(creds, { userId: newUser.id })))
        .then(newCreds => mutator.perms.POST({ userId: newCreds.userId, permissions: [] }))
        .then((perms) => {
          mutator.query.update({ _path: `/users/view/${perms.userId}`, layer: null });
        });
    } else {
      const user = Object.assign({}, userdata, { id: uuid() });
      if (user.creds) delete user.creds;

      mutator.records.POST(user)
        .then((newUser) => mutator.perms.POST({ userId: newUser.id, permissions: [] }))
        .then((perms) => {
          mutator.query.update({ _path: `/users/view/${perms.userId}`, layer: null });
        });
    }
  }

  massageNewRecord = (userdata) => {
    if (userdata.personal.addresses) {
      const addressTypes = (this.props.resources.addressTypes || {}).records || [];
      // eslint-disable-next-line no-param-reassign
      userdata.personal.addresses = toUserAddresses(userdata.personal.addresses, addressTypes);
    }
  }

  goToNew = () => {
    const { mutator } = this.props;
    mutator.query.update({ layer: 'create' });
  }

  goToSearch = () => {
    const { mutator } = this.props;
    mutator.query.update({ layer: null });
    const searchField = document.getElementById('input-user-search');
    if (searchField) {
      searchField.focus();
    }
  }

  render() {
    const {
      onSelectRow,
      disableRecordCreation,
      onComponentWillUnmount,
      showSingleResult,
      browseOnly,
      intl,
    } = this.props;

    const patronGroups = (this.props.resources.patronGroups || {}).records || [];

    const resultsFormatter = {
      active: user => (
        <AppIcon app="users" size="small">
          {user.active ? <FormattedMessage id="ui-users.active" /> : <FormattedMessage id="ui-users.inactive" />}
        </AppIcon>
      ),
      name: user => getFullName(user),
      barcode: user => user.barcode,
      patronGroup: (user) => {
        const pg = patronGroups.filter(g => g.id === user.patronGroup)[0];
        return pg ? pg.group : '?';
      },
      username: user => user.username,
      email: user => _.get(user, ['personal', 'email']),
    };

    return (
      <div data-test-user-instances>
        <HasCommand commands={this.keyboardCommands}>
          <SearchAndSort
            packageInfo={this.props.packageInfo || packageInfo}
            objectName="user"
            filterConfig={filterConfig}
            initialResultCount={INITIAL_RESULT_COUNT}
            resultCountIncrement={RESULT_COUNT_INCREMENT}
            viewRecordComponent={ViewUser}
            editRecordComponent={UserForm}
            newRecordInitialValues={{ active: true, personal: { preferredContactTypeId: '002' } }}
            visibleColumns={this.props.visibleColumns ? this.props.visibleColumns : ['active', 'name', 'barcode', 'patronGroup', 'username', 'email']}
            resultsFormatter={resultsFormatter}
            onSelectRow={onSelectRow}
            onCreate={this.create}
            onComponentWillUnmount={onComponentWillUnmount}
            massageNewRecord={this.massageNewRecord}
            finishedResourceName="perms"
            viewRecordPerms="users.item.get"
            newRecordPerms="users.item.post,login.item.post,perms.users.item.post"
            disableRecordCreation={disableRecordCreation}
            parentResources={this.props.resources}
            parentMutator={this.props.mutator}
            showSingleResult={showSingleResult}
            columnMapping={{
              active: intl.formatMessage({ id: 'ui-users.active' }),
              name: intl.formatMessage({ id: 'ui-users.information.name' }),
              barcode: intl.formatMessage({ id: 'ui-users.information.barcode' }),
              patronGroup: intl.formatMessage({ id: 'ui-users.information.patronGroup' }),
              username: intl.formatMessage({ id: 'ui-users.information.username' }),
              email: intl.formatMessage({ id: 'ui-users.contact.email' }),
            }}
            browseOnly={browseOnly}
          />
        </HasCommand>
      </div>);
  }
}

export default injectIntl(Users);
<|endoftext|>


<commit_before>/*
* Poll chat plugin
* By bumbadadabum and Zarel.
*/

'use strict';

const permission = 'announce';

let Poll = (function () {
	function Poll(room, question, options) {
		if (room.pollNumber) {
			room.pollNumber++;
		} else {
			room.pollNumber = 1;
		}
		this.room = room;
		this.question = question;
		this.voters = {};
		this.voterIps = {};
		this.totalVotes = 0;
		this.timeout = null;
		this.timeoutMins = 0;

		this.options = new Map();
		for (let i = 0; i < options.length; i++) {
			this.options.set(i + 1, {name: options[i], votes: 0});
		}
	}

	Poll.prototype.vote = function (user, option) {
		let ip = user.latestIp;
		let userid = user.userid;

		if (userid in this.voters || ip in this.voterIps) {
			return user.sendTo(this.room, "You have already voted for this poll.");
		}

		this.voters[userid] = option;
		this.voterIps[ip] = option;
		this.options.get(option).votes++;
		this.totalVotes++;

		this.update();
	};

	Poll.prototype.blankvote = function (user, option) {
		let ip = user.latestIp;
		let userid = user.userid;

		if (userid in this.voters || ip in this.voterIps) {
			return user.sendTo(this.room, "You're already looking at the results.");
		} else {
			this.voters[userid] = 0;
			this.voterIps[ip] = 0;
		}

		this.update();
	};

	Poll.prototype.generateVotes = function () {
		let output = '

 Poll ' + Tools.escapeHTML(this.question) + '
';
		this.options.forEach(function (option, number) {
			output += '
<button value="/poll vote ' + number + '" name="send" title="Vote for ' + number + '. ' + Tools.escapeHTML(option.name) + '">' + number + '. ' + Tools.escapeHTML(option.name) + '</button>
';
		});
		output += '
';

		return output;
	};

	Poll.prototype.generateResults = function (ended, option) {
		let icon = ' ' + (ended ? "Poll ended" : "Poll") + '';
		let output = '

' + icon + ' ' + Tools.escapeHTML(this.question) + '
';
		let iter = this.options.entries();

		let i = iter.next();
		let c = 0;
		let colors = ['#79A', '#8A8', '#88B'];
		while (!i.done) {
			let percentage = Math.round((i.value[1].votes * 100) / (this.totalVotes || 1));
			output += '
' + i.value[0] + '. ' + (i.value[0] === option ? '' : '') + Tools.escapeHTML(i.value[1].name) + (i.value[0] === option ? '' : '') + ' (' + i.value[1].votes + ' vote' + (i.value[1].votes === 1 ? '' : 's') + ')
 ' + percentage + '%
';
			i = iter.next();
			c++;
		}
		output += '
';

		return output;
	};

	Poll.prototype.update = function () {
		let results = [];

		for (let i = 0; i <= this.options.size; i++) {
			results.push(this.generateResults(false, i));
		}

		// Update the poll results for everyone that has voted
		for (let i in this.room.users) {
			let user = this.room.users[i];
			if (user.userid in this.voters) {
				user.sendTo(this.room, '|uhtmlchange|poll' + this.room.pollNumber + '|' + results[this.voters[user.userid]]);
			} else if (user.latestIp in this.voterIps) {
				user.sendTo(this.room, '|uhtmlchange|poll' + this.room.pollNumber + '|' + results[this.voterIps[user.latestIp]]);
			}
		}
	};

	Poll.prototype.display = function (user, broadcast) {
		let votes = this.generateVotes();

		let results = [];

		for (let i = 0; i <= this.options.size; i++) {
			results.push(this.generateResults(false, i));
		}

		let target = {};

		if (broadcast) {
			target = this.room.users;
		} else {
			target[0] = user;
		}

		for (let i in target) {
			let thisUser = target[i];
			if (thisUser.userid in this.voters) {
				thisUser.sendTo(this.room, '|uhtml|poll' + this.room.pollNumber + '|' + results[this.voters[thisUser.userid]]);
			} else if (thisUser.latestIp in this.voterIps) {
				thisUser.sendTo(this.room, '|uhtml|poll' + this.room.pollNumber + '|' + results[this.voterIps[thisUser.latestIp]]);
			} else {
				thisUser.sendTo(this.room, '|uhtml|poll' + this.room.pollNumber + '|' + votes);
			}
		}
	};

	Poll.prototype.end = function () {
		let results = this.generateResults(true);

		this.room.send('|uhtmlchange|poll' + this.room.pollNumber + '|
(The poll has ended – scroll down to see the results)
');
		this.room.send('|html|' + results);
	};

	return Poll;
})();

exports.commands = {
	poll: {
		create: 'new',
		new: function (target, room, user, connection, cmd, message) {
			if (!target) return this.parse('/help poll new');
			if (target.length > 1024) return this.errorReply("Poll too long.");
			let params = target.split(target.includes('|') ? '|' : ',').map(function (param) { return param.trim(); });

			if (!this.can(permission, null, room)) return false;
			if (!this.canTalk()) return this.errorReply("You cannot do this while unable to talk.");
			if (room.poll) return this.errorReply("There is already a poll in progress in this room.");

			if (params.length < 3) return this.errorReply("Not enough arguments for /poll new.");
			let options = [];

			for (let i = 1; i < params.length; i++) {
				options.push(params[i]);
			}

			if (options.length > 8) {
				return this.errorReply("Too many options for poll (maximum is 8).");
			}

			room.poll = new Poll(room, params[0], options);
			room.poll.display(user, true);

			this.logEntry("" + user.name + " used " + message);
			return this.privateModCommand("(A poll was started by " + user.name + ".)");
		},
		newhelp: ["/poll create [question], [option1], [option2], [...] - Creates a poll. Requires: % @ # & ~"],

		vote: function (target, room, user) {
			if (!room.poll) return this.errorReply("There is no poll running in this room.");
			if (!target) return this.parse('/help poll vote');

			if (target === 'blank') {
				room.poll.blankvote(user);
				return;
			}

			let parsed = parseInt(target, 10);
			if (isNaN(parsed)) return this.errorReply("To vote, specify the number of the option.");

			if (!room.poll.options.has(parsed)) return this.sendReply("Option not in poll.");

			room.poll.vote(user, parsed);
		},
		votehelp: ["/poll vote [number] - Votes for option [number]."],

		timer: function (target, room, user) {
			if (!room.poll) return this.errorReply("There is no poll running in this room.");

			if (target) {
				if (!this.can(permission, null, room)) return false;
				if (target === 'clear') {
					if (!room.poll.timeout) return this.errorReply("There is no timer to clear.");
					clearTimeout(room.poll.timeout);
					room.poll.timeout = null;
					room.poll.timeoutMins = 0;
					return this.add("The timeout for the poll was cleared.");
				}
				let timeout = parseFloat(target);
				if (isNaN(timeout) || timeout <= 0 || timeout > 0x7FFFFFFF) return this.errorReply("Invalid time given.");
				if (room.poll.timeout) clearTimeout(room.poll.timeout);
				room.poll.timeoutMins = timeout;
				room.poll.timeout = setTimeout((function () {
					room.poll.end();
					delete room.poll;
				}), (timeout * 60000));
				room.add("The timeout for the poll was set to " + timeout + " minutes.");
				return this.privateModCommand("(The poll timeout was set to " + timeout + " minutes by " + user.name + ".)");
			} else {
				if (!this.canBroadcast()) return;
				if (room.poll.timeout) {
					return this.sendReply("The timeout for the poll is " + room.poll.timeoutMins + " minutes.");
				} else {
					return this.sendReply("There's no timer for this poll.");
				}
			}
		},
		timerhelp: ["/poll timer [minutes] - Sets the poll to automatically end after [minutes] minutes. Requires: % @ # & ~", "/poll timer clear - Clears the poll's timer. Requires: % @ # & ~"],

		results: function (target, room, user) {
			if (!room.poll) return this.errorReply("There is no poll running in this room.");

			return room.poll.blankvote(user);
		},
		resultshelp: ["/poll results - Shows the results of the poll without voting. NOTE: you can't go back and vote after using this."],

		close: 'end',
		stop: 'end',
		end: function (target, room, user) {
			if (!this.can(permission, null, room)) return false;
			if (!this.canTalk()) return this.errorReply("You cannot do this while unable to talk.");
			if (!room.poll) return this.errorReply("There is no poll running in this room.");
			if (room.poll.timeout) clearTimeout(room.poll.timeout);

			room.poll.end();
			delete room.poll;
			return this.privateModCommand("(The poll was ended by " + user.name + ".)");
		},
		endhelp: ["/poll end - Ends a poll and displays the results. Requires: % @ # & ~"],

		show: 'display',
		display: function (target, room, user) {
			if (!room.poll) return this.errorReply("There is no poll running in this room.");
			if (!this.canBroadcast()) return;
			room.update();

			room.poll.display(user, this.broadcasting);
		},
		displayhelp: ["/poll display - Displays the poll"],

		'': function (target, room, user) {
			this.parse('/help poll');
		}
	},
	pollhelp: ["/poll allows rooms to run their own polls. These polls are limited to one poll at a time per room.",
				"Accepts the following commands:",
				"/poll create [question], [option1], [option2], [...] - Creates a poll. Requires: % @ # & ~",
				"/poll vote [number] - Votes for option [number].",
				"/poll timer [minutes] - Sets the poll to automatically end after [minutes]. Requires: % @ # & ~",
				"/poll results - Shows the results of the poll without voting. NOTE: you can't go back and vote after using this.",
				"/poll display - Displays the poll",
				"/poll end - Ends a poll and displays the results. Requires: % @ # & ~"]
};
<commit_msg>Poll: convert poll to the new class syntax<commit_after>/*
* Poll chat plugin
* By bumbadadabum and Zarel.
*/

'use strict';

const permission = 'announce';

class Poll {
	constructor(room, question, options) {
		if (room.pollNumber) {
			room.pollNumber++;
		} else {
			room.pollNumber = 1;
		}
		this.room = room;
		this.question = question;
		this.voters = {};
		this.voterIps = {};
		this.totalVotes = 0;
		this.timeout = null;
		this.timeoutMins = 0;

		this.options = new Map();
		for (let i = 0; i < options.length; i++) {
			this.options.set(i + 1, {name: options[i], votes: 0});
		}
	}

	vote(user, option) {
		let ip = user.latestIp;
		let userid = user.userid;

		if (userid in this.voters || ip in this.voterIps) {
			return user.sendTo(this.room, "You have already voted for this poll.");
		}

		this.voters[userid] = option;
		this.voterIps[ip] = option;
		this.options.get(option).votes++;
		this.totalVotes++;

		this.update();
	}

	blankvote(user, option) {
		let ip = user.latestIp;
		let userid = user.userid;

		if (userid in this.voters || ip in this.voterIps) {
			return user.sendTo(this.room, "You're already looking at the results.");
		} else {
			this.voters[userid] = 0;
			this.voterIps[ip] = 0;
		}

		this.update();
	}

	generateVotes() {
		let output = '

 Poll ' + Tools.escapeHTML(this.question) + '
';
		this.options.forEach(function (option, number) {
			output += '
<button value="/poll vote ' + number + '" name="send" title="Vote for ' + number + '. ' + Tools.escapeHTML(option.name) + '">' + number + '. ' + Tools.escapeHTML(option.name) + '</button>
';
		});
		output += '
';

		return output;
	}

	generateResults(ended, option) {
		let icon = ' ' + (ended ? "Poll ended" : "Poll") + '';
		let output = '

' + icon + ' ' + Tools.escapeHTML(this.question) + '
';
		let iter = this.options.entries();

		let i = iter.next();
		let c = 0;
		let colors = ['#79A', '#8A8', '#88B'];
		while (!i.done) {
			let percentage = Math.round((i.value[1].votes * 100) / (this.totalVotes || 1));
			output += '
' + i.value[0] + '. ' + (i.value[0] === option ? '' : '') + Tools.escapeHTML(i.value[1].name) + (i.value[0] === option ? '' : '') + ' (' + i.value[1].votes + ' vote' + (i.value[1].votes === 1 ? '' : 's') + ')
 ' + percentage + '%
';
			i = iter.next();
			c++;
		}
		output += '
';

		return output;
	}

	update() {
		let results = [];

		for (let i = 0; i <= this.options.size; i++) {
			results.push(this.generateResults(false, i));
		}

		// Update the poll results for everyone that has voted
		for (let i in this.room.users) {
			let user = this.room.users[i];
			if (user.userid in this.voters) {
				user.sendTo(this.room, '|uhtmlchange|poll' + this.room.pollNumber + '|' + results[this.voters[user.userid]]);
			} else if (user.latestIp in this.voterIps) {
				user.sendTo(this.room, '|uhtmlchange|poll' + this.room.pollNumber + '|' + results[this.voterIps[user.latestIp]]);
			}
		}
	}

	display(user, broadcast) {
		let votes = this.generateVotes();

		let results = [];

		for (let i = 0; i <= this.options.size; i++) {
			results.push(this.generateResults(false, i));
		}

		let target = {};

		if (broadcast) {
			target = this.room.users;
		} else {
			target[0] = user;
		}

		for (let i in target) {
			let thisUser = target[i];
			if (thisUser.userid in this.voters) {
				thisUser.sendTo(this.room, '|uhtml|poll' + this.room.pollNumber + '|' + results[this.voters[thisUser.userid]]);
			} else if (thisUser.latestIp in this.voterIps) {
				thisUser.sendTo(this.room, '|uhtml|poll' + this.room.pollNumber + '|' + results[this.voterIps[thisUser.latestIp]]);
			} else {
				thisUser.sendTo(this.room, '|uhtml|poll' + this.room.pollNumber + '|' + votes);
			}
		}
	}

	end() {
		let results = this.generateResults(true);

		this.room.send('|uhtmlchange|poll' + this.room.pollNumber + '|
(The poll has ended – scroll down to see the results)
');
		this.room.send('|html|' + results);
	}
}

exports.commands = {
	poll: {
		create: 'new',
		new: function (target, room, user, connection, cmd, message) {
			if (!target) return this.parse('/help poll new');
			if (target.length > 1024) return this.errorReply("Poll too long.");
			let params = target.split(target.includes('|') ? '|' : ',').map(function (param) { return param.trim(); });

			if (!this.can(permission, null, room)) return false;
			if (!this.canTalk()) return this.errorReply("You cannot do this while unable to talk.");
			if (room.poll) return this.errorReply("There is already a poll in progress in this room.");

			if (params.length < 3) return this.errorReply("Not enough arguments for /poll new.");
			let options = [];

			for (let i = 1; i < params.length; i++) {
				options.push(params[i]);
			}

			if (options.length > 8) {
				return this.errorReply("Too many options for poll (maximum is 8).");
			}

			room.poll = new Poll(room, params[0], options);
			room.poll.display(user, true);

			this.logEntry("" + user.name + " used " + message);
			return this.privateModCommand("(A poll was started by " + user.name + ".)");
		},
		newhelp: ["/poll create [question], [option1], [option2], [...] - Creates a poll. Requires: % @ # & ~"],

		vote: function (target, room, user) {
			if (!room.poll) return this.errorReply("There is no poll running in this room.");
			if (!target) return this.parse('/help poll vote');

			if (target === 'blank') {
				room.poll.blankvote(user);
				return;
			}

			let parsed = parseInt(target, 10);
			if (isNaN(parsed)) return this.errorReply("To vote, specify the number of the option.");

			if (!room.poll.options.has(parsed)) return this.sendReply("Option not in poll.");

			room.poll.vote(user, parsed);
		},
		votehelp: ["/poll vote [number] - Votes for option [number]."],

		timer: function (target, room, user) {
			if (!room.poll) return this.errorReply("There is no poll running in this room.");

			if (target) {
				if (!this.can(permission, null, room)) return false;
				if (target === 'clear') {
					if (!room.poll.timeout) return this.errorReply("There is no timer to clear.");
					clearTimeout(room.poll.timeout);
					room.poll.timeout = null;
					room.poll.timeoutMins = 0;
					return this.add("The timeout for the poll was cleared.");
				}
				let timeout = parseFloat(target);
				if (isNaN(timeout) || timeout <= 0 || timeout > 0x7FFFFFFF) return this.errorReply("Invalid time given.");
				if (room.poll.timeout) clearTimeout(room.poll.timeout);
				room.poll.timeoutMins = timeout;
				room.poll.timeout = setTimeout((function () {
					room.poll.end();
					delete room.poll;
				}), (timeout * 60000));
				room.add("The timeout for the poll was set to " + timeout + " minutes.");
				return this.privateModCommand("(The poll timeout was set to " + timeout + " minutes by " + user.name + ".)");
			} else {
				if (!this.canBroadcast()) return;
				if (room.poll.timeout) {
					return this.sendReply("The timeout for the poll is " + room.poll.timeoutMins + " minutes.");
				} else {
					return this.sendReply("There's no timer for this poll.");
				}
			}
		},
		timerhelp: ["/poll timer [minutes] - Sets the poll to automatically end after [minutes] minutes. Requires: % @ # & ~", "/poll timer clear - Clears the poll's timer. Requires: % @ # & ~"],

		results: function (target, room, user) {
			if (!room.poll) return this.errorReply("There is no poll running in this room.");

			return room.poll.blankvote(user);
		},
		resultshelp: ["/poll results - Shows the results of the poll without voting. NOTE: you can't go back and vote after using this."],

		close: 'end',
		stop: 'end',
		end: function (target, room, user) {
			if (!this.can(permission, null, room)) return false;
			if (!this.canTalk()) return this.errorReply("You cannot do this while unable to talk.");
			if (!room.poll) return this.errorReply("There is no poll running in this room.");
			if (room.poll.timeout) clearTimeout(room.poll.timeout);

			room.poll.end();
			delete room.poll;
			return this.privateModCommand("(The poll was ended by " + user.name + ".)");
		},
		endhelp: ["/poll end - Ends a poll and displays the results. Requires: % @ # & ~"],

		show: 'display',
		display: function (target, room, user) {
			if (!room.poll) return this.errorReply("There is no poll running in this room.");
			if (!this.canBroadcast()) return;
			room.update();

			room.poll.display(user, this.broadcasting);
		},
		displayhelp: ["/poll display - Displays the poll"],

		'': function (target, room, user) {
			this.parse('/help poll');
		}
	},
	pollhelp: ["/poll allows rooms to run their own polls. These polls are limited to one poll at a time per room.",
				"Accepts the following commands:",
				"/poll create [question], [option1], [option2], [...] - Creates a poll. Requires: % @ # & ~",
				"/poll vote [number] - Votes for option [number].",
				"/poll timer [minutes] - Sets the poll to automatically end after [minutes]. Requires: % @ # & ~",
				"/poll results - Shows the results of the poll without voting. NOTE: you can't go back and vote after using this.",
				"/poll display - Displays the poll",
				"/poll end - Ends a poll and displays the results. Requires: % @ # & ~"]
};
<|endoftext|>


<commit_before>import moment from 'moment'
import 'moment-duration-format'

export function BigNumWeiToDisplayString(bignum, unit='ether') {
        return window.web3.fromWei(bignum, unit).toFixed()
    }

export function secondsToDisplayString(seconds) {
        if (seconds < 60 * 60 * 24) // less than one day?
            return moment.duration(seconds, "seconds").format("hh:mm.ss", {trim: false})
        return moment.duration(seconds, "seconds").format("d [days]")
    }<commit_msg>Implement workaround for faulty fromWei() in production build.<commit_after>import moment from 'moment'
import 'moment-duration-format'

export function BigNumWeiToDisplayString(bignum, unit='ether') {
        // Following code does not work in webpack production build:
        //
        //  return window.web3.fromWei(bignum, unit).toFixed()
        //
        // Contrary to documentation, "fromWei" always returns a string in production build, so
        // calling <string>.toFixed does not work. In dev build it returns the expected "bignum" instance!

        let x = window.web3.fromWei(bignum, unit)
        if (typeof(x) === 'string') {
            // console.log("Expected bigNumber, got String instead: " + x)
            // console.log("Input was: " + typeof (bignum))
            return x;
        } else {
            // console.log("Got expected bigNumber, returning .toFixed.")
            return x.toFixed()
        }
    }

export function secondsToDisplayString(seconds) {
        if (seconds < 60 * 60 * 24) // less than one day?
            return moment.duration(seconds, "seconds").format("hh:mm.ss", {trim: false})
        return moment.duration(seconds, "seconds").format("d [days]")
    }<|endoftext|>


<commit_before>/*
* Poll chat plugin
* By bumbadadabum and Zarel.
*/

var permission = 'broadcast';
var bluebutton = 'padding:2px; background-color:#000; border:1px solid #666; color:white';

var Poll = (function () {
	function Poll(room, question, options) {
		if (room.pollNumber) {
			room.pollNumber++;
		} else {
			room.pollNumber = 1;
		}
		this.room = room;
		this.question = question;
		this.voters = new Set();
		this.totalVotes = 0;
		this.timeout = null;
		this.timeoutMins = 0;

		this.options = new Map();
		for (var i = 0; i < options.length; i++) {
			this.options.set(i + 1, {name: options[i], votes: 0});
		}
	}

	Poll.prototype.vote = function (user, option) {
		if (this.voters.has(user.latestIp)) {
			return user.sendTo(this.room, "You have already voted for this poll.");
		} else {
			this.voters.add(user.latestIp);
			if (this.room.id === 'lobby' && user.named) {
				economy.writeMoney('money', user.userid, +1);
				user.sendTo(this.room, "Thank you for voting for this Lobby poll.  You have received 1 buck as a result!");
			}
		}

		this.options.get(option).votes++;
		this.totalVotes++;

		this.update();
	};

	Poll.prototype.blankvote = function (user, option) {
		if (this.voters.has(user.latestIp)) {
			return user.sendTo(this.room, "You're already looking at the results.");
		} else {
			this.voters.add(user.latestIp);
		}

		this.update();
	};

	Poll.prototype.generateVotes = function () {
		var output = '

 Poll ' + Tools.escapeHTML(this.question) + '
';
		this.options.forEach(function (option, number) {
			output += '
<button style="' + blackbutton + '" value="/poll vote ' + number + '" name="send" title="Vote for ' + number + '. ' + Tools.escapeHTML(option.name) + '">' + number + '. ' + Tools.escapeHTML(option.name) + '</button>
';
		});
		output += '
';

		return output;
	};

	Poll.prototype.generateResults = function (ended) {
		var icon = ' ' + (ended ? "Poll ended" : "Poll") + '';
		var totalVotes = '
[Total Votes: ' + this.totalVotes + ']
';
		var output = '

' + icon + ' ' + Tools.escapeHTML(this.question) + '
' + totalVotes;
		var iter = this.options.entries();

		var i = iter.next();
		var c = 0;
		var colors = ['#79A', '#8A8', '#88B'];
		while (!i.done) {
			var percentage = Math.round((i.value[1].votes * 100) / (this.totalVotes || 1));
			output += '
' + i.value[0] + '. ' + Tools.escapeHTML(i.value[1].name) + '  | ' + percentage + '% (' + i.value[1].votes + ' vote' + (i.value[1].votes === 1 ? '' : 's') + ')

';
			i = iter.next();
			c++;
		}
		output += '
';

		return output;
	};

	Poll.prototype.update = function () {
		var results = this.generateResults();

		// Update the poll results for everyone that has voted
		for (var i in this.room.users) {
			var user = this.room.users[i];
			if (this.voters.has(user.latestIp)) {
				user.sendTo(this.room, '|uhtmlchange|poll' + this.room.pollNumber +  '|' + results);
			}
		}
	};

	Poll.prototype.display = function (user, broadcast) {
		var votes = this.generateVotes();
		var results = this.generateResults();

		var target = {};

		if (broadcast) {
			target = this.room.users;
		} else {
			target[0] = user;
		}

		for (var i in target) {
			var thisUser = target[i];
			if (this.voters.has(thisUser.latestIp)) {
				thisUser.sendTo(this.room, '|uhtml|poll' + this.room.pollNumber +  '|' + results);
			} else {
				thisUser.sendTo(this.room, '|uhtml|poll' + this.room.pollNumber +  '|' + votes);
			}
		}
	};

	Poll.prototype.end = function () {
		var results = this.generateResults(true);

		this.room.send('|uhtmlchange|poll' + this.room.pollNumber +  '|
(The poll has ended – scroll down to see the results)
');
		this.room.send('|html|' + results);
	};

	return Poll;
})();

exports.commands = {
	poll: {
		create: 'new',
		new: function (target, room, user) {
			if (room.type === 'battle') return false;
			if (target.length > 1024) return this.errorReply("Poll too long.");
			var params = target.split(target.includes('|') ? '|' : ',').map(function (param) { return param.trim(); });

			if (!this.can(permission, null, room)) return false;
			if (room.poll) return this.errorReply("There is already a poll in progress in this room.");

			if (params.length < 3) return this.errorReply("Not enough arguments for /poll new.");
			var options = [];

			for (var i = 1; i < params.length; i++) {
				options.push(params[i]);
			}

			if (options.length > 12) {
				return this.errorReply("Too many options for poll (maximum is 12).");
			}

			room.poll = new Poll(room, params[0], options);
			room.poll.display(user, true);
			return this.privateModCommand("(A poll was started by " + user.name + ".)");
		},
		newhelp: ["/poll create [question], [option1], [option2], [...] - Creates a poll. Requires: % @ # & ~"],

		vote: function (target, room, user) {
			if (!room.poll) return this.errorReply("There is no poll running in this room.");
			if (!target) return this.errorReply("Please specify an option.");

			if (target === 'blank') {
				room.poll.blankvote(user);
				return;
			}

			var parsed = parseInt(target);
			if (isNaN(parsed)) return this.errorReply("To vote, specify the number of the option.");

			if (!room.poll.options.has(parsed)) return this.sendReply("Option not in poll.");

			room.poll.vote(user, parsed);
		},
		votehelp: ["/poll vote [number] - Votes for option [number]."],

		timer: function (target, room, user) {
			if (!room.poll) return this.errorReply("There is no poll running in this room.");

			if (target) {
				if (!this.can(permission, null, room)) return false;
				if (target === 'clear') {
					if (room.poll.timeout) {
						clearTimeout(room.poll.timeout);
						room.poll.timeout = null;
						room.poll.timeoutMins = 0;
						return room.add("The timeout for the poll was cleared.");
					} else {
						return this.errorReply("No timer to clear.");
					}
				}
				var timeout = parseFloat(target);
				if (isNaN(timeout) || timeout <= 0) return this.errorReply("Invalid time given.");
				if (room.poll.timeout) clearTimeout(room.poll.timeout);
				room.poll.timeoutMins = timeout;
				room.poll.timeout = setTimeout((function () {
					room.poll.end();
					delete room.poll;
				}), (timeout * 60000));
				room.add("The timeout for the poll was set to " + timeout + " minutes.");
				return this.privateModCommand("(The poll timeout was set to " + timeout + " minutes by " + user.name + ".)");
			} else {
				if (!this.canBroadcast()) return;
				if (room.poll.timeout) {
					return this.sendReply("The timeout for the poll is " + room.poll.timeoutMins + " minutes.");
				} else {
					return this.sendReply("There's no timer for this poll.");
				}
			}
		},
		timerhelp: ["/poll timer [minutes] - Sets the poll to automatically end after [minutes] minutes. Requires: % @ # & ~", "/poll timer clear - Clears the poll's timer. Requires: % @ # & ~"],

		results: function (target, room, user) {
			if (!room.poll) return this.errorReply("There is no poll running in this room.");

			return room.poll.blankvote(user);
		},
		resultshelp: ["/poll results - Shows the results of the poll without voting. NOTE: you can't go back and vote after using this."],

		close: 'end',
		stop: 'end',
		end: function (target, room, user) {
			if (!this.can(permission, null, room)) return false;
			if (!room.poll) return this.errorReply("There is no poll running in this room.");
			if (room.poll.timeout) clearTimeout(room.poll.timeout);

			room.poll.end();
			delete room.poll;
			return this.privateModCommand("(The poll was ended by " + user.name + ".)");
		},
		endhelp: ["/poll end - Ends a poll and displays the results. Requires: % @ # & ~"],

		timer: function (target, room, user) {
			if (!this.can(permission, null, room)) return false;
			if (!room.poll) return this.errorReply("There is no poll running in this room.");

			var timeout = parseFloat(target);
			if (isNaN(timeout)) return this.errorReply("No time given.");
			if (room.poll.timeout) clearTimeout(room.poll.timeout);
			room.poll.timeout = setTimeout((function () {
				room.poll.end();
				delete room.poll;
			}), (timeout * 60000));
			return this.privateModCommand("(The poll timeout was set to " + timeout + " minutes by " + user.name + ".)");
		},
		timerhelp: ["/poll timer [minutes] - Sets the poll to automatically end after [minutes] minutes. Requires: % @ # & ~"],

		pr: 'display',
		pollremind: 'display',
		display: function (target, room, user) {
			if (!room.poll) return this.errorReply("There is no poll running in this room.");
			if (!this.canBroadcast()) return;
			room.update();

			room.poll.display(user, this.broadcasting);
		},
		displayhelp: ["/poll display - Displays the poll"],

		show: 'display',
		display: function (target, room, user) {
			if (!room.poll) return this.errorReply("There is no poll running in this room.");
			if (!this.canBroadcast()) return;
			room.update();

			room.poll.display(user, this.broadcasting);
		},
		displayhelp: ["/poll display - Displays the poll"],

		'': function (target, room, user) {
			this.parse('/pollhelp');
		},
	},
	pollhelp: function(target, room, user) {
		if (!this.canBroadcast()) return;
		return this.sendReplyBox(
			"/poll allows rooms to run their own polls. These polls are limited to one poll at a time per room.
" +
			"The poll status is displayed to the users and updated in real time.
" +
			"Accepts the following commands:
" +
			"/poll create [question], [option1], [option2], [...] - Creates a poll. Requires: + % @ # & ~
" +
			"/poll vote [number] - Votes for option [number] (or click the button).
" +
			"/poll timer [minutes] - Sets the poll to automatically end after [minutes]. Requires: + % @ # & ~
" +
			"/poll display - Displays the poll
" +
			"/poll end - Ends a poll and displays the results. Requires: + % @ # & ~"
		);
	},
	votes: function(target, room, user) {
		if (!room.poll) return this.errorReply("There is no poll running in this room.");
		if (!this.canBroadcast()) return;
		var votes = room.poll.totalVotes;
		var lbl = (votes > 1 ? ' VOTES' : ' VOTE');
		return this.sendReplyBox("TOTAL VOTES: " + votes + lbl);
	},
	ep: 'endpoll',
	endpoll: function(target, room, user) {
		this.parse('/poll end');
	},
	pr: 'pollremind',
	pollremind: function(target, room, user) {
		if (!room.poll) return this.errorReply("There is no poll running in this room.");
		if (!this.canBroadcast()) return;
		room.update();

		room.poll.display(user, this.broadcasting);
	},
	oraspoll: function(target, room, user) {
		var tiers = ['Random Battle', 'OU', 'Ubers', 'UU', 'RU', 'NU', 'LC', 'Anything Goes', 'Battle Spot Singles'];
		this.parse('/poll new ORAS Single tier?, ' + tiers);

	},
	randpoll: function(target, room, user) {
		var tiers = ['Random Doubles Battle', 'Random Triples Battle', 'CC1v1', 'Random Monotype', 'Random Sky Battle', 'Random Ubers', 'Random LC', 'Random CAP', 'Random Monogen', 'CC2v2', 'Challenge Cup Metronome', 'Random Haxmons'];
		this.parse('/poll new Random Tier?, ' + tiers);
        },
        ompoll: function(target, room, user) {
		var tiers = ['CAP', 'Balanced Hackmons', '1v1', 'Monotype', 'Tier Shift', 'PU', 'Inverse Battle', 'Almost Any Ability', 'STABmons', 'LC UU', 'Snowy OU'];
		this.parse('/poll new Other Metas Tournament?, ' + tiers);
	},

	easytour: 'etour',
	elimtour: 'etour',
	etour: function (target, room, user) {
		if (!this.can('broadcast', null, room)) return;
		this.parse('/tour new ' + target + ', elimination');
	},

	roundrobintour: 'rtour',
	cancertour: 'rtour',
	rtour: function (target, room, user) {
		if (!this.can('broadcast', null, room)) return;
		this.parse('/tour new ' + target + ', roundrobin');
	},
	vote: function(target, room, user) {
		if (!target) return this.errorReply("Usage: /vote [poll option number] - votes for the [option] in the current poll.");
		this.parse('/poll vote ' + target);
	}
};
<commit_msg>Reverting to the new poll.<commit_after>/*
* Poll chat plugin
* This plugin allows roomauth (default: driver and above) to run a poll in a room.
* Every room can have one poll, and every user can vote. The results are displayed for
* users that have voted, and are updated in real time. The poll can be closed with /endpoll.
* By bumbadadabum with (a lot of) help from Zarel.
*/

var permission = 'broadcast';
var bluebutton = 'padding:2px; background-color:#0099FF; border:1px solid #0099FF; color:white';

var Poll = (function () {
	function Poll(room, question, options) {
		if (room.pollNumber) {
			room.pollNumber++;
		} else {
			room.pollNumber = 1;
		}
		this.room = room;
		this.question = question;
		this.voters = new Set();
		this.totalVotes = 0;
		this.timeout = null;

		this.options = new Map();
		for (var i = 0; i < options.length; i++) {
			this.options.set(i + 1, {name: options[i], votes: 0});
		}
	}

	Poll.prototype.vote = function (user, option) {
		if (this.voters.has(user.latestIp)) {
			return user.sendTo(this.room, "You have already voted for this poll.");
		} else {
			this.voters.add(user.latestIp);
		}

		this.options.get(option).votes++;
		this.totalVotes++;

		this.update();
	};

	Poll.prototype.generateVotes = function () {
		var output = '

 Poll ' + Tools.escapeHTML(this.question) + '
';
		this.options.forEach(function (option, number) {
			output += '
<button style="' + bluebutton + '" value="/poll vote ' + number + '" name="send" title="Vote for ' + number + '. ' + Tools.escapeHTML(option.name) + '">' + number + '. ' + Tools.escapeHTML(option.name) + '</button>
';
		});
		output += '
';

		return output;
	};

	Poll.prototype.generateResults = function (ended) {
		var icon = ' ' + (ended ? "Poll ended" : "Poll") + '';
		var output = '

' + icon + ' ' + Tools.escapeHTML(this.question) + '
';
		var iter = this.options.entries();

		var i = iter.next();
		var c = 0;
		var colors = ['#79A', '#8A8', '#88B'];
		while (!i.done) {
			var percentage = Math.round((i.value[1].votes * 100) / (this.totalVotes || 1));
			output += '
' + i.value[0] + '. ' + Tools.escapeHTML(i.value[1].name) + '  | ' + percentage + '% (' + i.value[1].votes + ' votes)

';
			i = iter.next();
			c++;
		}
		output += '
';

		return output;
	};

	Poll.prototype.update = function () {
		var results = this.generateResults();

		// Update the poll results for everyone that has voted
		for (var i in this.room.users) {
			var user = this.room.users[i];
			if (this.voters.has(user.latestIp)) {
				user.sendTo(this.room, '|uhtmlchange|poll' + this.room.pollNumber +  '|' + results);
			}
		}
	};

	Poll.prototype.display = function (user, broadcast) {
		var votes = this.generateVotes();
		var results = this.generateResults();

		var target = {};

		if (broadcast) {
			target = this.room.users;
		} else {
			target[0] = user;
		}

		for (var i in target) {
			var thisUser = target[i];
			if (this.voters.has(thisUser.latestIp)) {
				thisUser.sendTo(this.room, '|uhtml|poll' + this.room.pollNumber +  '|' + results);
			} else {
				thisUser.sendTo(this.room, '|uhtml|poll' + this.room.pollNumber +  '|' + votes);
			}
		}
	};

	Poll.prototype.end = function () {
		var results = this.generateResults(true);

		this.room.send('|uhtmlchange|poll' + this.room.pollNumber +  '|
(The poll has ended – scroll down to see the results)
');
		this.room.send('|html|' + results);
	};

	return Poll;
})();

exports.commands = {
	poll: {
		create: 'new',
		new: function (target, room, user) {
			if (target.length > 1024) return this.errorReply("Poll too long.");
			var params = target.split(target.includes('|') ? '|' : ',').map(function (param) { return param.trim(); });

			if (!this.can(permission, null, room)) return false;
			if (room.poll) return this.errorReply("There is already a poll in progress in this room.");

			if (params.length < 3) {
				return this.errorReply("Not enough arguments for /poll new.");
			}

			var options = [];

			for (var i = 1; i < params.length; i++) {
				options.push(params[i]);
			}

			if (options.length > 12) {
				return this.errorReply("Too many options for poll (maximum is 12).");
			}

			room.poll = new Poll(room, params[0], options);
			room.poll.display(user, true);
			return this.privateModCommand("(A poll was started by " + user.name + ".)");
		},
		newhelp: ["/poll create [question], [option1], [option2], [...] - Creates a poll. Requires: % @ # & ~"],

		vote: function (target, room, user) {
			if (!room.poll) return this.errorReply("There is no poll running in this room.");
			if (!target) return this.errorReply("Please specify an option.");

			var parsed = parseInt(target);
			if (isNaN(parsed)) return this.errorReply("To vote, specify the number of the option.");

			if (!room.poll.options.has(parsed)) return this.sendReply("Option not in poll.");

			room.poll.vote(user, parsed);
		},
		votehelp: ["/poll vote [number] - Votes for option [number] in the poll. This can also be done by clicking the option in the poll itself."],

		close: 'end',
		stop: 'end',
		end: function (target, room, user) {
			if (!this.can(permission, null, room)) return false;
			if (!room.poll) return this.errorReply("There is no poll running in this room.");

			room.poll.end();
			delete room.poll;
			return this.privateModCommand("(The poll was ended by " + user.name + ".)");
		},
		endhelp: ["/poll end - Ends a poll and displays the results. Requires: % @ # & ~"],

		timer: function (target, room, user) {
			if (!this.can(permission, null, room)) return false;
			if (!room.poll) return this.errorReply("There is no poll running in this room.");

			var timeout = parseFloat(target);
			if (isNaN(timeout)) return this.errorReply("No time given.");
			if (room.poll.timeout) clearTimeout(room.poll.timeout);
			room.poll.timeout = setTimeout((function () {
				room.poll.end();
				delete room.poll;
			}), (timeout * 60000));
			return this.privateModCommand("(The timeout was set to " + timeout + " minutes by " + user.name + ".)");
		},
		timerhelp: ["/poll timer [minutes] - Sets the poll to automatically end after [minutes] minutes. Requires: % @ # & ~"],

		pr: 'display',
		pollremind: 'display',
		display: function (target, room, user) {
			if (!room.poll) return this.errorReply("There is no poll running in this room.");
			if (!this.canBroadcast()) return;
			room.update();

			room.poll.display(user, this.broadcasting);
		},
		displayhelp: ["/poll display - Displays the poll"],

		'': function (target, room, user) {
			this.parse('/help poll new');
			this.parse('/help poll end');
		},
	},
	pollhelp: function(target, room, user) {
		if (!this.canBroadcast()) return;
		return this.sendReplyBox(
			"/poll allows rooms to run their own polls. These polls are limited to one poll at a time per room.
" +
			"The poll status is displayed to the users and updated in real time.
" +
			"Accepts the following commands:
" +
			"/poll create [question], [option1], [option2], [...] - Creates a poll. Requires: + % @ # & ~
" +
			"/poll vote [number] - Votes for option [number].
" +
			"/poll timer [minutes] - Sets the poll to automatically end after [minutes]. Requires: + % @ # & ~
" +
			"/poll display - Displays the poll
" +
			"/poll end - Ends a poll and displays the results. Requires: + % @ # & ~"
		)
	},
	votes: function(target, room, user) {
		if (!room.poll) return this.errorReply("There is no poll running in this room.");
		if (!this.canBroadcast()) return;
		var votes = room.poll.totalVotes;
		var lbl = (votes > 1 ? ' VOTES' : ' VOTE');
		return this.sendReplyBox("TOTAL VOTES: " + votes + lbl);
	},
	ep: 'endpoll',
	endpoll: function(target, room, user) {
		this.parse('/poll end');
	},
	oraspoll: function(target, room, user) {
		var tiers = ['Random Battle', 'OU', 'Ubers', 'UU', 'RU', 'NU', 'LC', 'Anything Goes', 'Battle Spot Singles'];
		this.parse('/poll new ORAS Single tier?, ' + tiers);

	},
	randpoll: function(target, room, user) {
		var tiers = ['Random Doubles Battle', 'Random Triples Battle', 'CC1v1', 'Random Monotype', 'Random Sky Battle', 'Random Ubers', 'Random LC', 'Random CAP', 'Random Monogen', 'CC2v2', 'Challenge Cup Metronome', 'Random Haxmons'];
		this.parse('/poll new Random Tier?, ' + tiers);
       },
       ompoll: function(target, room, user) {
		var tiers = ['CAP', 'Balanced Hackmons', '1v1', 'Monotype', 'Tier Shift', 'PU', 'Inverse Battle', 'Almost Any Ability', 'STABmons', 'LC UU', 'Snowy OU'];
		this.parse('/poll new Other Metas Tournament?, ' + tiers);
	},

	easytour: 'etour',
	elimtour: 'etour',
	etour: function (target, room, user) {
		if (!this.can('broadcast', null, room)) return;
		this.parse('/tour new ' + target + ', elimination');
	},

	roundrobintour: 'rtour',
	cancertour: 'rtour',
	rtour: function (target, room, user) {
		if (!this.can('broadcast', null, room)) return;
		this.parse('/tour new ' + target + ', roundrobin');
}
};
<|endoftext|>


<commit_before>/* jshint
 funcscope: true,
 newcap: true,
 nonew: true,
 shadow: false,
 unused: true,

 maxlen: 90,
 maxparams: 3,
 maxstatements: 200
 */
/* global $ */
'use strict';

var _ = require('../utils').getLodash();
var i18n = require('../../locale/current/netsim');
var netsimConstants = require('./netsimConstants');

var EncodingType = netsimConstants.EncodingType;

/**
 * Make a new SVG element, appropriately namespaced, wrapped in a jQuery
 * object for (semi-)easy manipulation.
 * @param {string} type - the tagname for the svg element.
 * @returns {jQuery}
 */
exports.jQuerySvgElement = function (type) {
  var newElement = $(document.createElementNS('http://www.w3.org/2000/svg', type));

  /**
   * Override addClass since jQuery addClass doesn't work on svg.
   * @param {string} className
   */
  newElement.addClass = function (className) {
    var oldClasses = newElement.attr('class');
    if (!oldClasses) {
      newElement.attr('class', className);
    } else if (!oldClasses.split(/\s+/g).some(function (existingClass) {
          return existingClass === className;
        })) {
      newElement.attr('class', oldClasses + ' ' + className);
    }
    // Return element for chaining
    return newElement;
  };

  return newElement;
};

/**
 * Checks configuration against tab type to decide whether tab
 * of type should be shown.
 * @param {netsimLevelConfiguration} levelConfig
 * @param {NetSimTabType} tabType
 */
exports.shouldShowTab = function (levelConfig, tabType) {
  return levelConfig.showTabs.indexOf(tabType) > -1;
};

/**
 * Get the localized string for the given encoding type.
 * @param {EncodingType} encodingType
 * @returns {string} localized encoding name
 */
exports.getEncodingLabel = function (encodingType) {
  if (encodingType === EncodingType.ASCII) {
    return i18n.ascii();
  } else if (encodingType === EncodingType.DECIMAL) {
    return i18n.decimal();
  } else if (encodingType === EncodingType.HEXADECIMAL) {
    return i18n.hex();
  } else if (encodingType === EncodingType.BINARY) {
    return i18n.binary();
  } else if (encodingType === EncodingType.A_AND_B) {
    return i18n.a_and_b();
  }
  return '';
};

/**
 * @param {Object} enumObj - Technically any object, but should be used with
 *        an enum like those found in netsimConstants
 * @param {function} func - A function to call for each value in the enum,
 *        which gets passed the enum value.
 */
exports.forEachEnumValue = function (enumObj, func) {
  for (var enumKey in enumObj) {
    if (enumObj.hasOwnProperty(enumKey)) {
      func(enumObj[enumKey]);
    }
  }
};

/**
 * Rules used by serializeNumber and deserializeNumber to map unsupported
 * JavaScript values into JSON and back.
 * @type {{jsVal: number, jsonVal: string}[]}
 * @readonly
 */
var NUMBER_SERIALIZATION_RULES = [
  { jsVal: Infinity, jsonVal: 'Infinity' },
  { jsVal: -Infinity, jsonVal: '-Infinity' },
  { jsVal: NaN, jsonVal: 'NaN' },
  { jsVal: undefined, jsonVal: 'undefined' }
];

/**
 * Checks that the provided value is actually the special value NaN, unlike
 * standard isNaN which returns true for anything that's not a number.
 * @param {*} val - any value
 * @returns {boolean}
 */
var isExactlyNaN = function (val) {
  // NaN is the only value in JavaScript that is not exactly equal to itself.
  // Therefore, if val !== val, then val must be NaN.
  return val !== val;
};

/**
 * Because JSON doesn't support the values Infinity, NaN, or undefined, you can
 * use this method to store those values in JSON as strings.
 * @param {number|NaN} num
 * @returns {number|string}
 */
exports.serializeNumber = function (num) {
  var applicableRule = _.find(NUMBER_SERIALIZATION_RULES, function (rule) {
    return rule.jsVal === num || (isExactlyNaN(rule.jsVal) && isExactlyNaN(num));
  });
  return applicableRule ? applicableRule.jsonVal : num;
};

/**
 * Because JSON doesn't support the values Infinity, NaN, or undefined, you can
 * use this method to retrieve a value from JSON that is either a number or one
 * of those values.
 * @param {number|string} storedNum
 * @returns {number|NaN}
 */
exports.deserializeNumber = function (storedNum) {
  var applicableRule = _.find(NUMBER_SERIALIZATION_RULES, function (rule) {
    return rule.jsonVal === storedNum;
  });
  return applicableRule ? applicableRule.jsVal : storedNum;
};

/**
 * @param {netsimLevelConfiguration} levelConfig
 * @returns {netsimLevelConfiguration} same thing, but with certain values
 *          converted or cleaned.
 * @private
 */
exports.scrubLevelConfiguration_ = function (levelConfig) {
  var scrubbedLevel = _.clone(levelConfig, true);

  // Read string "Infinity" as Infinity
  scrubbedLevel.defaultPacketSizeLimit = exports.deserializeNumber(
      scrubbedLevel.defaultPacketSizeLimit);
  scrubbedLevel.defaultRouterBandwidth = exports.deserializeNumber(
      scrubbedLevel.defaultRouterBandwidth);
  scrubbedLevel.defaultRouterMemory = exports.deserializeNumber(
      scrubbedLevel.defaultRouterMemory);

  return scrubbedLevel;
};

/**
 * Converts a number of bits into a localized representation of that data
 * size in bytes, kilobytes, megabytes, gigabytes.
 * @param {number} bits
 * @returns {string} - localized string representation of size in bytes
 */
exports.bitsToLocalizedRoundedBytesize = function (bits) {
  if (bits === Infinity) {
    return i18n.unlimited();
  }

  var gbytes = Math.floor(bits / netsimConstants.BITS_PER_GIGABYTE);
  if (gbytes > 0) {
    return i18n.x_GBytes({ x: gbytes });
  }

  var mbytes = Math.floor(bits / netsimConstants.BITS_PER_MEGABYTE);
  if (mbytes > 0) {
    return i18n.x_MBytes({ x: mbytes });
  }

  var kbytes = Math.floor(bits / netsimConstants.BITS_PER_KILOBYTE);
  if (kbytes > 0) {
    return i18n.x_KBytes({ x: kbytes });
  }

  var bytes = Math.floor(bits / netsimConstants.BITS_PER_BYTE);
  if (bytes > 0) {
    return i18n.x_Bytes({ x: bytes });
  }

  return i18n.x_bits({ x: bits });
};

/**
 * Converts a bitrate into a localized representation of that data
 * size in bits/sec, kilobits, megabits, gigabits.
 * @param {number} bitsPerSecond
 * @returns {string} - localized string representation of speed in bits
 */
exports.bitrateToLocalizedRoundedBitrate = function (bitsPerSecond) {
  if (bitsPerSecond === Infinity) {
    return i18n.unlimited();
  }

  var gbps = Math.floor(bitsPerSecond / netsimConstants.BITS_PER_GIGABIT);
  if (gbps > 0) {
    return i18n.x_Gbps({ x: gbps });
  }

  var mbps = Math.floor(bitsPerSecond / netsimConstants.BITS_PER_MEGABIT);
  if (mbps > 0) {
    return i18n.x_Mbps({ x: mbps });
  }

  var kbps = Math.floor(bitsPerSecond / netsimConstants.BITS_PER_KILOBIT);
  if (kbps > 0) {
    return i18n.x_Kbps({ x: kbps });
  }

  var bps = Math.floor(bitsPerSecond * 100) / 100;
  return i18n.x_bps({ x: bps });
};

exports.zeroPadLeft = function (string, desiredWidth) {
  var padding = '0'.repeat(desiredWidth);
  return (padding + string).slice(-desiredWidth);
};

exports.zeroPadRight = function (string, desiredWidth) {
  var padding = '0'.repeat(desiredWidth);
  return (string + padding).substr(0, desiredWidth);
};

<commit_msg>Make sure to convert BitRate and ChunkSize options from levelbuilder to support Infinity values.<commit_after>/* jshint
 funcscope: true,
 newcap: true,
 nonew: true,
 shadow: false,
 unused: true,

 maxlen: 90,
 maxparams: 3,
 maxstatements: 200
 */
/* global $ */
'use strict';

var _ = require('../utils').getLodash();
var i18n = require('../../locale/current/netsim');
var netsimConstants = require('./netsimConstants');

var logger = require('./NetSimLogger').getSingleton();

var EncodingType = netsimConstants.EncodingType;

/**
 * Make a new SVG element, appropriately namespaced, wrapped in a jQuery
 * object for (semi-)easy manipulation.
 * @param {string} type - the tagname for the svg element.
 * @returns {jQuery}
 */
exports.jQuerySvgElement = function (type) {
  var newElement = $(document.createElementNS('http://www.w3.org/2000/svg', type));

  /**
   * Override addClass since jQuery addClass doesn't work on svg.
   * @param {string} className
   */
  newElement.addClass = function (className) {
    var oldClasses = newElement.attr('class');
    if (!oldClasses) {
      newElement.attr('class', className);
    } else if (!oldClasses.split(/\s+/g).some(function (existingClass) {
          return existingClass === className;
        })) {
      newElement.attr('class', oldClasses + ' ' + className);
    }
    // Return element for chaining
    return newElement;
  };

  return newElement;
};

/**
 * Checks configuration against tab type to decide whether tab
 * of type should be shown.
 * @param {netsimLevelConfiguration} levelConfig
 * @param {NetSimTabType} tabType
 */
exports.shouldShowTab = function (levelConfig, tabType) {
  return levelConfig.showTabs.indexOf(tabType) > -1;
};

/**
 * Get the localized string for the given encoding type.
 * @param {EncodingType} encodingType
 * @returns {string} localized encoding name
 */
exports.getEncodingLabel = function (encodingType) {
  if (encodingType === EncodingType.ASCII) {
    return i18n.ascii();
  } else if (encodingType === EncodingType.DECIMAL) {
    return i18n.decimal();
  } else if (encodingType === EncodingType.HEXADECIMAL) {
    return i18n.hex();
  } else if (encodingType === EncodingType.BINARY) {
    return i18n.binary();
  } else if (encodingType === EncodingType.A_AND_B) {
    return i18n.a_and_b();
  }
  return '';
};

/**
 * @param {Object} enumObj - Technically any object, but should be used with
 *        an enum like those found in netsimConstants
 * @param {function} func - A function to call for each value in the enum,
 *        which gets passed the enum value.
 */
exports.forEachEnumValue = function (enumObj, func) {
  for (var enumKey in enumObj) {
    if (enumObj.hasOwnProperty(enumKey)) {
      func(enumObj[enumKey]);
    }
  }
};

/**
 * Rules used by serializeNumber and deserializeNumber to map unsupported
 * JavaScript values into JSON and back.
 * @type {{jsVal: number, jsonVal: string}[]}
 * @readonly
 */
var NUMBER_SERIALIZATION_RULES = [
  { jsVal: Infinity, jsonVal: 'Infinity' },
  { jsVal: -Infinity, jsonVal: '-Infinity' },
  { jsVal: NaN, jsonVal: 'NaN' },
  { jsVal: undefined, jsonVal: 'undefined' }
];

/**
 * Checks that the provided value is actually the special value NaN, unlike
 * standard isNaN which returns true for anything that's not a number.
 * @param {*} val - any value
 * @returns {boolean}
 */
var isExactlyNaN = function (val) {
  // NaN is the only value in JavaScript that is not exactly equal to itself.
  // Therefore, if val !== val, then val must be NaN.
  return val !== val;
};

/**
 * Because JSON doesn't support the values Infinity, NaN, or undefined, you can
 * use this method to store those values in JSON as strings.
 * @param {number|NaN} num
 * @returns {number|string}
 */
exports.serializeNumber = function (num) {
  var applicableRule = _.find(NUMBER_SERIALIZATION_RULES, function (rule) {
    return rule.jsVal === num || (isExactlyNaN(rule.jsVal) && isExactlyNaN(num));
  });
  return applicableRule ? applicableRule.jsonVal : num;
};

/**
 * Because JSON doesn't support the values Infinity, NaN, or undefined, you can
 * use this method to retrieve a value from JSON that is either a number or one
 * of those values.
 * @param {number|string} storedNum
 * @returns {number|NaN}
 */
exports.deserializeNumber = function (storedNum) {
  var applicableRule = _.find(NUMBER_SERIALIZATION_RULES, function (rule) {
    return rule.jsonVal === storedNum;
  });
  return applicableRule ? applicableRule.jsVal : storedNum;
};

/**
 * @param {netsimLevelConfiguration} levelConfig
 * @returns {netsimLevelConfiguration} same thing, but with certain values
 *          converted or cleaned.
 * @private
 */
exports.scrubLevelConfiguration_ = function (levelConfig) {
  var scrubbedLevel = _.clone(levelConfig, true);

  // Explicitly list fields that we suspect may have a string value that
  // needs to be converted to a number, like "Infinity"
  scrubbedLevel.defaultPacketSizeLimit = exports.deserializeNumber(
      scrubbedLevel.defaultPacketSizeLimit);
  scrubbedLevel.defaultBitRateBitsPerSecond = exports.deserializeNumber(
      scrubbedLevel.defaultBitRateBitsPerSecond);
  scrubbedLevel.defaultChunkSizeBits = exports.deserializeNumber(
      scrubbedLevel.defaultChunkSizeBits);
  scrubbedLevel.defaultRouterBandwidth = exports.deserializeNumber(
      scrubbedLevel.defaultRouterBandwidth);
  scrubbedLevel.defaultRouterMemory = exports.deserializeNumber(
      scrubbedLevel.defaultRouterMemory);

  // Generate a warning if we see a possible missed conversion (development aid)
  Object.keys(scrubbedLevel).filter(function (key) {
    // Ignore level params with underscores, they are the dashboard versions
    // of the camelCase parameters that the app actually uses.
    return !/_/.test(key);
  }).forEach(function (key) {
    var unconvertedValue = NUMBER_SERIALIZATION_RULES.some(function (rule) {
      return scrubbedLevel[key] === rule.jsonVal;
    });
    if (unconvertedValue) {
      logger.warn("Level option '" + key +
      "' has unconverted string value '" + scrubbedLevel[key] + "'");
    }
  });

  return scrubbedLevel;
};

/**
 * Converts a number of bits into a localized representation of that data
 * size in bytes, kilobytes, megabytes, gigabytes.
 * @param {number} bits
 * @returns {string} - localized string representation of size in bytes
 */
exports.bitsToLocalizedRoundedBytesize = function (bits) {
  if (bits === Infinity) {
    return i18n.unlimited();
  }

  var gbytes = Math.floor(bits / netsimConstants.BITS_PER_GIGABYTE);
  if (gbytes > 0) {
    return i18n.x_GBytes({ x: gbytes });
  }

  var mbytes = Math.floor(bits / netsimConstants.BITS_PER_MEGABYTE);
  if (mbytes > 0) {
    return i18n.x_MBytes({ x: mbytes });
  }

  var kbytes = Math.floor(bits / netsimConstants.BITS_PER_KILOBYTE);
  if (kbytes > 0) {
    return i18n.x_KBytes({ x: kbytes });
  }

  var bytes = Math.floor(bits / netsimConstants.BITS_PER_BYTE);
  if (bytes > 0) {
    return i18n.x_Bytes({ x: bytes });
  }

  return i18n.x_bits({ x: bits });
};

/**
 * Converts a bitrate into a localized representation of that data
 * size in bits/sec, kilobits, megabits, gigabits.
 * @param {number} bitsPerSecond
 * @returns {string} - localized string representation of speed in bits
 */
exports.bitrateToLocalizedRoundedBitrate = function (bitsPerSecond) {
  if (bitsPerSecond === Infinity) {
    return i18n.unlimited();
  }

  var gbps = Math.floor(bitsPerSecond / netsimConstants.BITS_PER_GIGABIT);
  if (gbps > 0) {
    return i18n.x_Gbps({ x: gbps });
  }

  var mbps = Math.floor(bitsPerSecond / netsimConstants.BITS_PER_MEGABIT);
  if (mbps > 0) {
    return i18n.x_Mbps({ x: mbps });
  }

  var kbps = Math.floor(bitsPerSecond / netsimConstants.BITS_PER_KILOBIT);
  if (kbps > 0) {
    return i18n.x_Kbps({ x: kbps });
  }

  var bps = Math.floor(bitsPerSecond * 100) / 100;
  return i18n.x_bps({ x: bps });
};

exports.zeroPadLeft = function (string, desiredWidth) {
  var padding = '0'.repeat(desiredWidth);
  return (padding + string).slice(-desiredWidth);
};

exports.zeroPadRight = function (string, desiredWidth) {
  var padding = '0'.repeat(desiredWidth);
  return (string + padding).substr(0, desiredWidth);
};

<|endoftext|>


<commit_before>var Settings = {
    URL_PREFIX: "/",
    URL_DATA_PREFIX: "/api/"
};

var StringUtil = {
    sanitizeUrl: function(url){
        url = url.replace(/([^:])[\/]+/g, '$1/');
        return url;
    },
    humanizeName: function(name){
        name = name.replace(/_/g, " ");
        name = name.replace(/\.jpe?g/i, "");
        return name;
    }
};

var Fullscreen = {
    open: function(element){
        if (element.requestFullscreen) {
            element.requestFullscreen();
        } else if (element.webkitRequestFullscreen) {
            element.webkitRequestFullscreen();
        } else if (element.mozRequestFullScreen) {
            element.mozRequestFullScreen();
        } else if (element.msRequestFullscreen) {
            element.msRequestFullscreen();
        }
    },

    close: function close() {
        if (document.exitFullscreen) {
            document.exitFullscreen();
        } else if (document.webkitExitFullscreen) {
            document.webkitExitFullscreen();
        } else if (document.mozCancelFullScreen) {
            document.mozCancelFullScreen();
        } else if (document.msExitFullscreen) {
            document.msExitFullscreen();
        }
    },

    onchange: function(handler){
        document.addEventListener("fullscreenchange", handler);
        document.addEventListener("webkitfullscreenchange", handler);
        document.addEventListener("mozfullscreenchange", handler);
        document.addEventListener("MSFullscreenChange", handler);
    },

    isActive: function(){
        return document.fullscreenElement ||
        document.mozFullScreenElement ||
        document.webkitFullscreenElement ||
        document.msFullscreenElement;
    }
};

function AlbumMenu(model, conf){

    var self = this;

    var $view = null;
    var $detailsButton = null;
    var $fullscreenButton = null;

    function init(){
        $view = conf.view;
        $detailsButton = conf.detailsButton;
        $fullscreenButton = conf.fullscreenButton;

        watch(model, "selectedPictureIndex", function(){
            pinMenu();
            showHideDetailsButton();
            showHideFullscreenButton();
        });

        $detailsButton.click(function(event){
            event.preventDefault();
            showHideDetails();
        });

        $fullscreenButton.click(function(event){
            event.preventDefault();
            openCloseFullscreen();
        });

        Fullscreen.onchange(function(event){
            $fullscreenButton.toggleClass("selected", Fullscreen.isActive());
        });

        showHideDetailsButton();
        showHideFullscreenButton();
    }

    function pinMenu(){
        if (model.selectedPictureIndex !== null){
            $view.addClass("headroom--pinned").addClass("headroom--top");
            $view.removeClass("headroom--not-top").removeClass("headroom--unpinned");
        }
    }

    function showHideDetailsButton(){
        if (model.selectedPictureIndex === null){
            model.detailsOn = false;
            $detailsButton.hide();
        } else {
            $detailsButton.show();
        }
    }

    function showHideDetails(){
        model.detailsOn = !model.detailsOn;
        $detailsButton.toggleClass("selected", model.detailsOn);
    }

    function showHideFullscreenButton(){
        if (model.selectedPictureIndex === null){
            model.detailsOn = false;
            $fullscreenButton.hide();
        } else {
            $fullscreenButton.show();
        }
    }

    function openCloseFullscreen(){
        if (Fullscreen.isActive()){
            Fullscreen.close();
        } else {
            Fullscreen.open(document.getElementById("content"));
        }
    }

    init();
}

function AlbumPhotos(model, conf){

    var self = this;
    var $view = null;
    var $viewList = null;
    var template = null;
    var currentWidth = 0;
    var heightProportion = null;
    var lazyLoad = false;

    var margin = 0;

    function init(){
        setConfiguration();

        watch(model, "pictures", function(prop, action, newvalue, oldvalue){
            var picturesChanged = Array.isArray(newvalue);
            self.displayPictures(picturesChanged);
        });

        $(window).resize(function(){
            self.resizePictures();
        });
    }

    function setConfiguration(){
        // Required
        $view = conf.view;
        template = conf.template;

        // Optional
        $viewList = (conf.listClass)? $view.find("."+conf.listClass) : $view;
        heightProportion = (conf.heightProportion)? conf.heightProportion : 0.45;
        lazyLoad = (conf.lazyLoad)? conf.lazyLoad : false;
        margin = (conf.margin)?  conf.margin : 0;
    }

    this.displayPictures = function(picturesChanged){
        if (picturesChanged===false){
            return;
        }

        $viewList.empty();
        if(!model.pictures || model.pictures.length === 0){
            $view.hide();
            return;
        }
        $view.show();

        var resize = new Resize(model.pictures, heightProportion);
        currentWidth = $view.width();
        var newPictures = resize.doResize(currentWidth, $(window).height());

        var content = "";
        for (var i=0; i<newPictures.length; i++){
            var p = newPictures[i];
            var params = {
                    width: p.newWidth-margin,
                    height: p.newHeight-margin
            };
            if (!lazyLoad){
                params.src = model.pictures[i].thumb;
            }
            content += Mustache.render(template, params);
        }
        $viewList.html(content);
        $viewList.find("img")
            .each(function(i, el){
                $(el).data("index", i);
            })
            .click(function(){
                model.selectedPictureIndex = $(this).data("index");
            });
        if (lazyLoad){
            startLazyLoading();
        }
    };

    this.resizePictures = function(){
        var newWidth = $view.width();
        if (newWidth == currentWidth) return;
        currentWidth = $view.width();
        var resize = new Resize(model.pictures, heightProportion);
        var newPictures = resize.doResize(currentWidth, $(window).height());
        $viewList.children().each(function(index, item){
            var p = newPictures[index];
            var width = (p.newWidth-margin);
            var height = (p.newHeight-margin);
            $(this).css("width", width).css("height", height);
            $(this).find("img").attr("width", width).attr("height", height);
        });
    };

    function startLazyLoading(){

        function loadNextPicture(){
            if (index >= model.pictures.length){
                return;
            }
            if (image.src == model.pictures[index].thumb){
                index++;
                loadNextPicture();
            } else {
                image.src = model.pictures[index].thumb;
            }
        }

        var index = 0;
        var image = new Image();
        image.onload = function(){
            $viewList.find("img:eq("+index+")")
                .attr("src", this.src)
                .show();
            index++;
            loadNextPicture();
        };

        loadNextPicture();
    }

    init();
}<commit_msg>Removing unused menu controls<commit_after>var Settings = {
    URL_PREFIX: "/",
    URL_DATA_PREFIX: "/api/"
};

var StringUtil = {
    sanitizeUrl: function(url){
        url = url.replace(/([^:])[\/]+/g, '$1/');
        return url;
    },
    humanizeName: function(name){
        name = name.replace(/_/g, " ");
        name = name.replace(/\.jpe?g/i, "");
        return name;
    }
};

var Fullscreen = {
    open: function(element){
        if (element.requestFullscreen) {
            element.requestFullscreen();
        } else if (element.webkitRequestFullscreen) {
            element.webkitRequestFullscreen();
        } else if (element.mozRequestFullScreen) {
            element.mozRequestFullScreen();
        } else if (element.msRequestFullscreen) {
            element.msRequestFullscreen();
        }
    },

    close: function close() {
        if (document.exitFullscreen) {
            document.exitFullscreen();
        } else if (document.webkitExitFullscreen) {
            document.webkitExitFullscreen();
        } else if (document.mozCancelFullScreen) {
            document.mozCancelFullScreen();
        } else if (document.msExitFullscreen) {
            document.msExitFullscreen();
        }
    },

    onchange: function(handler){
        document.addEventListener("fullscreenchange", handler);
        document.addEventListener("webkitfullscreenchange", handler);
        document.addEventListener("mozfullscreenchange", handler);
        document.addEventListener("MSFullscreenChange", handler);
    },

    isActive: function(){
        return document.fullscreenElement ||
        document.mozFullScreenElement ||
        document.webkitFullscreenElement ||
        document.msFullscreenElement;
    }
};

function AlbumPhotos(model, conf){

    var self = this;
    var $view = null;
    var $viewList = null;
    var template = null;
    var currentWidth = 0;
    var heightProportion = null;
    var lazyLoad = false;

    var margin = 0;

    function init(){
        setConfiguration();

        watch(model, "pictures", function(prop, action, newvalue, oldvalue){
            var picturesChanged = Array.isArray(newvalue);
            self.displayPictures(picturesChanged);
        });

        $(window).resize(function(){
            self.resizePictures();
        });
    }

    function setConfiguration(){
        // Required
        $view = conf.view;
        template = conf.template;

        // Optional
        $viewList = (conf.listClass)? $view.find("."+conf.listClass) : $view;
        heightProportion = (conf.heightProportion)? conf.heightProportion : 0.45;
        lazyLoad = (conf.lazyLoad)? conf.lazyLoad : false;
        margin = (conf.margin)?  conf.margin : 0;
    }

    this.displayPictures = function(picturesChanged){
        if (picturesChanged===false){
            return;
        }

        $viewList.empty();
        if(!model.pictures || model.pictures.length === 0){
            $view.hide();
            return;
        }
        $view.show();

        var resize = new Resize(model.pictures, heightProportion);
        currentWidth = $view.width();
        var newPictures = resize.doResize(currentWidth, $(window).height());

        var content = "";
        for (var i=0; i<newPictures.length; i++){
            var p = newPictures[i];
            var params = {
                    width: p.newWidth-margin,
                    height: p.newHeight-margin
            };
            if (!lazyLoad){
                params.src = model.pictures[i].thumb;
            }
            content += Mustache.render(template, params);
        }
        $viewList.html(content);
        $viewList.find("img")
            .each(function(i, el){
                $(el).data("index", i);
            })
            .click(function(){
                model.selectedPictureIndex = $(this).data("index");
            });
        if (lazyLoad){
            startLazyLoading();
        }
    };

    this.resizePictures = function(){
        var newWidth = $view.width();
        if (newWidth == currentWidth) return;
        currentWidth = $view.width();
        var resize = new Resize(model.pictures, heightProportion);
        var newPictures = resize.doResize(currentWidth, $(window).height());
        $viewList.children().each(function(index, item){
            var p = newPictures[index];
            var width = (p.newWidth-margin);
            var height = (p.newHeight-margin);
            $(this).css("width", width).css("height", height);
            $(this).find("img").attr("width", width).attr("height", height);
        });
    };

    function startLazyLoading(){

        function loadNextPicture(){
            if (index >= model.pictures.length){
                return;
            }
            if (image.src == model.pictures[index].thumb){
                index++;
                loadNextPicture();
            } else {
                image.src = model.pictures[index].thumb;
            }
        }

        var index = 0;
        var image = new Image();
        image.onload = function(){
            $viewList.find("img:eq("+index+")")
                .attr("src", this.src)
                .show();
            index++;
            loadNextPicture();
        };

        loadNextPicture();
    }

    init();
}<|endoftext|>


<commit_before>/******************************************************************************
 *                              ~b.lack.array~                                *
 *                            ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾                              *
 * Provides missing Array functionality.                                      *
 *                                                                            *
 *     ________________________________________________________________       *
 *       Copyright (c) 2011 Quildreen Motta // Licenced under MIT/X11         *
 ******************************************************************************/

(function (root) {

	// Get the fallback function
	if (typeof exports != "undefined")  fallback = require('./core').fallback
	else                                fallback = b.lack.fallback

	// Alias, error messages and stuff
	var classOf = Object.prototype.toString
	  , abs     = Math.abs
	  , max     = Math.max
	  , min     = Math.min
	  , fallback

	///// Function `isArray` /////////////////////////////////////////////////
	//
	//     isArray(Obj:obj) → Bool
	//
	// Returns `true` if a variable is an array, `false` if it is not.
	//
	function arrayp(obj) {
		return classOf.call(obj) == '[object Array]'
	}

	///// Function `indexOf` /////////////////////////////////////////////////
	//
	//     #indexOf(value[, Num:start]) → Num
	//
	// Compares `value` to the elements of the array, in ascending order, using
	// the Strict Equality Comparison algorithm. If a match is found, that
	// match is returned, otherwise, -1 is returned.
	//
	// The optional `start` argument defines the array index at which the
	// search should start. If it's not present, or 0, the whole array will be
    // searched, otherwise, only the part starting at the given index will be
	// searched.
	//
	// Follows the algorithm described in ES-262 15.4.4.14
	//
	function search(value, start) {
		var obj = Object(this)
		  , len = obj.length >> 0
		  , key

		start = +start || 0
		if (!len || start >= len)  return -1
		if (start < 0)             start = max(0, len - abs(start))

		for (key = start; key < len; ++key)
			if (key in obj && obj[key] === value) return key

		return -1
	}

	///// Function `lastIndexOf` /////////////////////////////////////////////
	//
	//     #lastIndexOf(value[, Num:start]) → Num
	//
	// Compares `value` to the elements of the array, in descending
	// order, using the Strict Equality Comparison algorithm. If a match
	// is found, that match is returned, otherwise, -1 is returned.
	//
	// The optional `start` argument defines the array index at which
	// the search should start. If it's not present, or if it's great or
	// equal to the length of the array, the whole array will be
	// searched, otherwise, only the part starting at the given index
	// will be searched.
	//
	// Follows the algorithm described in ES-262 15.4.4.15
	//
	function search_right(value, start) {
		function get_start() { return clamp((!isNaN(+start)) ? +start : len)  }
		function clamp(n)    { return (n >= 0) ? min(n, len-1) : len - abs(n) }

		var obj = Object(this)
		  , len = obj.length >> 0
		  , key

		if (!len) return -1

		for (key = get_start(); key < len; --key)
			if (key in obj && obj[key] === value) return key

		return -1
	}

	///// Provides the fallbacks /////////////////////////////////////////////
	fallback(Array, { isArray: arrayp })

	fallback(Array.prototype, { indexOf:     search
	                          , lastIndexOf: search_right })

})(this);<commit_msg>Minor comment editing to limit each line to 72 characters.<commit_after>/******************************************************************************
 *                              ~b.lack.array~                                *
 *                            ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾                              *
 * Provides missing Array functionality.                                      *
 *                                                                            *
 *     ________________________________________________________________       *
 *       Copyright (c) 2011 Quildreen Motta // Licenced under MIT/X11         *
 ******************************************************************************/

(function (root) {

	// Get the fallback function
	if (typeof exports != "undefined")  fallback = require('./core').fallback
	else                                fallback = b.lack.fallback

	// Alias, error messages and stuff
	var classOf = Object.prototype.toString
	  , abs     = Math.abs
	  , max     = Math.max
	  , min     = Math.min
	  , fallback

	///// Function `isArray` /////////////////////////////////////////////////
	//
	//     isArray(Obj:obj) → Bool
	//
	// Returns `true` if a variable is an array, `false` if it is not.
	//
	function arrayp(obj) {
		return classOf.call(obj) == '[object Array]'
	}

	///// Function `indexOf` /////////////////////////////////////////////////
	//
	//     #indexOf(value[, Num:start]) → Num
	//
	// Compares `value` to the elements of the array, in ascending
	// order, using the Strict Equality Comparison algorithm. If a match
	// is found, that match is returned, otherwise, -1 is returned.
	//
	// The optional `start` argument defines the array index at which
	// the search should start. If it's not present, or 0, the whole
	// array will be searched, otherwise, only the part starting at the
	// given index will be searched.
	//
	// Follows the algorithm described in ES-262 15.4.4.14
	//
	function search(value, start) {
		var obj = Object(this)
		  , len = obj.length >> 0
		  , key

		start = +start || 0
		if (!len || start >= len)  return -1
		if (start < 0)             start = max(0, len - abs(start))

		for (key = start; key < len; ++key)
			if (key in obj && obj[key] === value) return key

		return -1
	}

	///// Function `lastIndexOf` /////////////////////////////////////////////
	//
	//     #lastIndexOf(value[, Num:start]) → Num
	//
	// Compares `value` to the elements of the array, in descending
	// order, using the Strict Equality Comparison algorithm. If a match
	// is found, that match is returned, otherwise, -1 is returned.
	//
	// The optional `start` argument defines the array index at which
	// the search should start. If it's not present, or if it's great or
	// equal to the length of the array, the whole array will be
	// searched, otherwise, only the part starting at the given index
	// will be searched.
	//
	// Follows the algorithm described in ES-262 15.4.4.15
	//
	function search_right(value, start) {
		function get_start() { return clamp((!isNaN(+start)) ? +start : len)  }
		function clamp(n)    { return (n >= 0) ? min(n, len-1) : len - abs(n) }

		var obj = Object(this)
		  , len = obj.length >> 0
		  , key

		if (!len) return -1

		for (key = get_start(); key < len; --key)
			if (key in obj && obj[key] === value) return key

		return -1
	}

	///// Provides the fallbacks /////////////////////////////////////////////
	fallback(Array, { isArray: arrayp })

	fallback(Array.prototype, { indexOf:     search
	                          , lastIndexOf: search_right })

})(this);<|endoftext|>