<commit_before>import React, { Component, PropTypes } from 'react'; // eslint-disable-line
import Match from 'react-router/Match'; // eslint-disable-line
import {Row, Col} from 'react-bootstrap'; // eslint-disable-line

/* shared stripes components */
import Pane from '@folio/stripes-components/lib/Pane'; // eslint-disable-line
import Paneset from '@folio/stripes-components/lib/Paneset'; // eslint-disable-line
import PaneMenu from '@folio/stripes-components/lib/PaneMenu'; // eslint-disable-line
import Button from '@folio/stripes-components/lib/Button'; // eslint-disable-line
import Icon from '@folio/stripes-components/lib/Icon'; // eslint-disable-line
import MultiColumnListUsers from './lib/MultiColumnList'; // eslint-disable-line
import KeyValue from '@folio/stripes-components/lib/KeyValue'; // eslint-disable-line
import TextField from '@folio/stripes-components/lib/TextField'; // eslint-disable-line
import Checkbox from '@folio/stripes-components/lib/Checkbox'; // eslint-disable-line
import FilterPaneSearch from '@folio/stripes-components/lib/FilterPaneSearch'; // eslint-disable-line
import FilterControlGroup from '@folio/stripes-components/lib/FilterControlGroup'; // eslint-disable-line
import Select from '@folio/stripes-components/lib/Select'; // eslint-disable-line
import Layer from '@folio/stripes-components/lib/Layer'; // eslint-disable-line

import UserForm from './UserForm';
import ViewUser from './ViewUser';

class Users extends Component {
  static contextTypes = {
    router: PropTypes.object.isRequired,
  }

  static propTypes = {
    data: PropTypes.object.isRequired,
    params: PropTypes.object,
    pathname: PropTypes.string.isRequired,
    location: PropTypes.object.isRequired,
    mutator: PropTypes.object,
  };

  static manifest = {
    searchResults: [],
    /* detail: {
      fineHistory:[]
    } */
    user: {
      type: 'okapi',
      path: 'users',
      fetch: false,
    },
    users: {
      type: 'okapi',
      records: 'users',
      path: 'users?query=[{"field":"' + "'" + 'username' + "'" + '","value":"?{query}","op":"="}]',
      staticFallback: { path: 'users' },
    },
  };

  constructor(props) {
    super(props);
    this.state = {
      // Search/Filter state
      patronFilter: true,
      employeeFilter: false,
      searchTerm: '',
      addUserMode: false,
    };

    this.onClickAddNewUser = this.onClickAddNewUser.bind(this);
    this.onClickCloseNewUser = this.onClickCloseNewUser.bind(this);
    this.onChangeFilter = this.onChangeFilter.bind(this);
    this.onChangeSearch = this.onChangeSearch.bind(this);
    this.onClearSearch = this.onClearSearch.bind(this);
    this.onClickItemHandler = this.onClickItemHandler.bind(this);
  }

  componentWillMount() {
    const resultData = [{ Name: 'Pete Sherman', Address: '391 W. Richardson St. Duarte, CA 91010', Fines: '$34.23' }];
    // const fineHistory = [{"Due Date": "11/12/2014", "Amount":"34.23", "Status":"Unpaid"}];
    this.props.mutator.searchResults.replace(resultData);
    // this.props.mutator.detail.replace({fineHistory: fineHistory});
  }

  // search Handlers...
  onChangeFilter(e) {
    const stateObj = {};
    stateObj[e.target.id] = !this.state[e.target.id];
    this.setState(stateObj);
  }

  onChangeSearch(e) {
    const term = e.target.value;
    console.log('User searched:', term, 'at', this.props.location.pathname);
    this.setState({ searchTerm: term });
    this.context.router.transitionTo(`${this.props.location.pathname}?query=${term}`);
  }

  onClearSearch() {
    console.log('User cleared search');
    this.setState({ searchTerm: '' });
    this.context.router.transitionTo(this.props.location.pathname);
  }

  onClickItemHandler(userId) {
    console.log('User clicked', userId, 'location = ', this.props.location);
    this.context.router.transitionTo(`/users/view/${userId}${this.props.location.search}`);
  }

  // end search Handlers

  // AddUser Handlers
  onClickAddNewUser() {
    console.log('add Clicked');
    this.setState({
      addUserMode: true,
    });
  }

  onClickCloseNewUser() {
    this.setState({
      addUserMode: false,
    });
  }
  // end AddUser Handlers
  create(data) {
    this.props.mutator.user.POST(data);
  }

  render() {
    const { data, params, pathname } = this.props;
    if (!data.users) return <div />;
    const resultMenu = <PaneMenu><button><Icon icon="bookmark" /></button></PaneMenu>;
    const fineHistory = [{ 'Due Date': '11/12/2014', 'Amount': '34.23', 'Status': 'Unpaid' }]; // eslint-disable-line
    const displayUsers = data.users.reduce((results, user) => {
      results.push({
        id: user.id,
        Name: user.personal.full_name,
        Username: user.username,
        Email: user.personal.email_primary,
      });
      return results;
    }, []);

    /* searchHeader is a 'custom pane header'*/
    const searchHeader = <FilterPaneSearch id="SearchField" onChange={this.onChangeSearch} onClear={this.onClearSearch} value={this.state.searchTerm} />;
    console.log(params);

    return (
      <Paneset>
        {/* Filter Pane */}
        <Pane defaultWidth="16%" header={searchHeader}>
          <FilterControlGroup label="Filters">
            <Checkbox
              id="patronFilter"
              label="Patrons"
              checked={this.state.patronFilter}
              onChange={this.onChangeFilter}
              marginBottom0
              hover
              fullWidth
            />
            <Checkbox
              id="employeeFilter"
              label="Employees"
              checked={this.state.employeeFilter}
              onChange={this.onChangeFilter}
              marginBottom0 hover fullWidth
            />
          </FilterControlGroup>
          <FilterControlGroup label="Actions">
            <Button fullWidth onClick={this.onClickAddNewUser}>Add User</Button>
          </FilterControlGroup>
        </Pane>

        {/* Results Pane */}
        <Pane defaultWidth="fit-content" paneTitle="Results" lastMenu={resultMenu}>
          <MultiColumnListUsers
            contentData={displayUsers}
            onClickItemHandler={this.onClickItemHandler}
          />
        </Pane>

        {/* Details Pane */}
        <Match pattern={`${pathname}/view/:userid`} component={ViewUser} />
        <Layer isOpen={this.state.addUserMode} label="Add New User Dialog">
          <UserForm
            onSubmit={(record) => { this.create(record); }}
            onCancel={this.onClickCloseNewUser}
          />
        </Layer>
      </Paneset>
    );
  }
}

export default Users;
<commit_msg>Modify quoting of quotes in manifest path: removes four no-useless-concat ESLint errors (STRIPES-100).<commit_after>import React, { Component, PropTypes } from 'react'; // eslint-disable-line
import Match from 'react-router/Match'; // eslint-disable-line
import {Row, Col} from 'react-bootstrap'; // eslint-disable-line

/* shared stripes components */
import Pane from '@folio/stripes-components/lib/Pane'; // eslint-disable-line
import Paneset from '@folio/stripes-components/lib/Paneset'; // eslint-disable-line
import PaneMenu from '@folio/stripes-components/lib/PaneMenu'; // eslint-disable-line
import Button from '@folio/stripes-components/lib/Button'; // eslint-disable-line
import Icon from '@folio/stripes-components/lib/Icon'; // eslint-disable-line
import MultiColumnListUsers from './lib/MultiColumnList'; // eslint-disable-line
import KeyValue from '@folio/stripes-components/lib/KeyValue'; // eslint-disable-line
import TextField from '@folio/stripes-components/lib/TextField'; // eslint-disable-line
import Checkbox from '@folio/stripes-components/lib/Checkbox'; // eslint-disable-line
import FilterPaneSearch from '@folio/stripes-components/lib/FilterPaneSearch'; // eslint-disable-line
import FilterControlGroup from '@folio/stripes-components/lib/FilterControlGroup'; // eslint-disable-line
import Select from '@folio/stripes-components/lib/Select'; // eslint-disable-line
import Layer from '@folio/stripes-components/lib/Layer'; // eslint-disable-line

import UserForm from './UserForm';
import ViewUser from './ViewUser';

class Users extends Component {
  static contextTypes = {
    router: PropTypes.object.isRequired,
  }

  static propTypes = {
    data: PropTypes.object.isRequired,
    params: PropTypes.object,
    pathname: PropTypes.string.isRequired,
    location: PropTypes.object.isRequired,
    mutator: PropTypes.object,
  };

  static manifest = {
    searchResults: [],
    /* detail: {
      fineHistory:[]
    } */
    user: {
      type: 'okapi',
      path: 'users',
      fetch: false,
    },
    users: {
      type: 'okapi',
      records: 'users',
      path: 'users?query=[{"field":"\'username\'","value":"?{query}","op":"="}]',
      staticFallback: { path: 'users' },
    },
  };

  constructor(props) {
    super(props);
    this.state = {
      // Search/Filter state
      patronFilter: true,
      employeeFilter: false,
      searchTerm: '',
      addUserMode: false,
    };

    this.onClickAddNewUser = this.onClickAddNewUser.bind(this);
    this.onClickCloseNewUser = this.onClickCloseNewUser.bind(this);
    this.onChangeFilter = this.onChangeFilter.bind(this);
    this.onChangeSearch = this.onChangeSearch.bind(this);
    this.onClearSearch = this.onClearSearch.bind(this);
    this.onClickItemHandler = this.onClickItemHandler.bind(this);
  }

  componentWillMount() {
    const resultData = [{ Name: 'Pete Sherman', Address: '391 W. Richardson St. Duarte, CA 91010', Fines: '$34.23' }];
    // const fineHistory = [{"Due Date": "11/12/2014", "Amount":"34.23", "Status":"Unpaid"}];
    this.props.mutator.searchResults.replace(resultData);
    // this.props.mutator.detail.replace({fineHistory: fineHistory});
  }

  // search Handlers...
  onChangeFilter(e) {
    const stateObj = {};
    stateObj[e.target.id] = !this.state[e.target.id];
    this.setState(stateObj);
  }

  onChangeSearch(e) {
    const term = e.target.value;
    console.log('User searched:', term, 'at', this.props.location.pathname);
    this.setState({ searchTerm: term });
    this.context.router.transitionTo(`${this.props.location.pathname}?query=${term}`);
  }

  onClearSearch() {
    console.log('User cleared search');
    this.setState({ searchTerm: '' });
    this.context.router.transitionTo(this.props.location.pathname);
  }

  onClickItemHandler(userId) {
    console.log('User clicked', userId, 'location = ', this.props.location);
    this.context.router.transitionTo(`/users/view/${userId}${this.props.location.search}`);
  }

  // end search Handlers

  // AddUser Handlers
  onClickAddNewUser() {
    console.log('add Clicked');
    this.setState({
      addUserMode: true,
    });
  }

  onClickCloseNewUser() {
    this.setState({
      addUserMode: false,
    });
  }
  // end AddUser Handlers
  create(data) {
    this.props.mutator.user.POST(data);
  }

  render() {
    const { data, params, pathname } = this.props;
    if (!data.users) return <div />;
    const resultMenu = <PaneMenu><button><Icon icon="bookmark" /></button></PaneMenu>;
    const fineHistory = [{ 'Due Date': '11/12/2014', 'Amount': '34.23', 'Status': 'Unpaid' }]; // eslint-disable-line
    const displayUsers = data.users.reduce((results, user) => {
      results.push({
        id: user.id,
        Name: user.personal.full_name,
        Username: user.username,
        Email: user.personal.email_primary,
      });
      return results;
    }, []);

    /* searchHeader is a 'custom pane header'*/
    const searchHeader = <FilterPaneSearch id="SearchField" onChange={this.onChangeSearch} onClear={this.onClearSearch} value={this.state.searchTerm} />;
    console.log(params);

    return (
      <Paneset>
        {/* Filter Pane */}
        <Pane defaultWidth="16%" header={searchHeader}>
          <FilterControlGroup label="Filters">
            <Checkbox
              id="patronFilter"
              label="Patrons"
              checked={this.state.patronFilter}
              onChange={this.onChangeFilter}
              marginBottom0
              hover
              fullWidth
            />
            <Checkbox
              id="employeeFilter"
              label="Employees"
              checked={this.state.employeeFilter}
              onChange={this.onChangeFilter}
              marginBottom0 hover fullWidth
            />
          </FilterControlGroup>
          <FilterControlGroup label="Actions">
            <Button fullWidth onClick={this.onClickAddNewUser}>Add User</Button>
          </FilterControlGroup>
        </Pane>

        {/* Results Pane */}
        <Pane defaultWidth="fit-content" paneTitle="Results" lastMenu={resultMenu}>
          <MultiColumnListUsers
            contentData={displayUsers}
            onClickItemHandler={this.onClickItemHandler}
          />
        </Pane>

        {/* Details Pane */}
        <Match pattern={`${pathname}/view/:userid`} component={ViewUser} />
        <Layer isOpen={this.state.addUserMode} label="Add New User Dialog">
          <UserForm
            onSubmit={(record) => { this.create(record); }}
            onCancel={this.onClickCloseNewUser}
          />
        </Layer>
      </Paneset>
    );
  }
}

export default Users;
<|endoftext|>


<commit_before>/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow strict-local
 * @format
 */

'use strict';

const React = require('React');

const ReactCurrentOwner =
  // $FlowFixMe
  React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner;

function readContext<T>(Context: React.Context<T>): T {
  const dispatcher = ReactCurrentOwner.currentDispatcher;
  return dispatcher.readContext(Context);
}

module.exports = readContext;
<commit_msg>Update react-relay to account for React-internals change<commit_after>/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow strict-local
 * @format
 */

'use strict';

const React = require('React');

const {ReactCurrentDispatcher, ReactCurrentOwner} =
  /* $FlowFixMe Flow doesn't know about React's internals for good reason,
              but for now, Relay needs the dispatcher to read context. */
  React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;

function readContext<T>(Context: React.Context<T>): T {
  const dispatcher =
    ReactCurrentDispatcher != null
      ? ReactCurrentDispatcher.current
      : ReactCurrentOwner.currentDispatcher;
  return dispatcher.readContext(Context);
}

module.exports = readContext;
<|endoftext|>


<commit_before>var _ = require('underscore');

_.extend(GLOBAL, {

});

GLOBAL.MYSQL_CONFIG = {
  user : 'root',
  host : 'localhost',
  port : 3306,
  password : '',
  database : 'git_scan_db',
  debug : false
}

//get utils
var TorFactory = require('./utils/Tor.js');
var Data = require('./utils/Data.js');
var Q = require('q');
var Test = require('./test/test.js');

//get models
var Scraper = require("./Scraper");
var Users = require('./models/Repositories.js');
var strftime = require('strftime');

// Scraper.start();

GLOBAL.TorFactory = TorFactory.makeCircuits(5, 9500, 15000);
<commit_msg>added configs again for now<commit_after>var _ = require('underscore');

_.extend(GLOBAL, {

});

GLOBAL.MYSQL_CONFIG = {
  user : 'root',
  host : 'localhost',
  port : 3306,
  password : '',
  database : 'git_scan_db',
  debug : false
}

GLOBAL.GITHUB_API_HTTPS = {
  protocol: 'https:',
  hostname: 'api.github.com',
  socksPort: TOR_SOCKS_PORT,
  port: 443,
  path: '/',
  headers: {'user-agent': 'node.js'}
}

GLOBAL.GITHUB_API_HTTP = {
  protocol: 'http',
  hostname: 'api.github.com',
  socksPort: TOR_SOCKS_PORT,
  port: 80,
  path: '/'
}

//get utils
var TorFactory = require('./utils/Tor.js');
var Data = require('./utils/Data.js');
var Q = require('q');
var Test = require('./test/test.js');

//get models
var Scraper = require("./Scraper");
var Users = require('./models/Repositories.js');
var strftime = require('strftime');

// Scraper.start();

GLOBAL.TorFactory = TorFactory.makeCircuits(5, 9500, 15000);
<|endoftext|>


<commit_before>import _ from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import Route from 'react-router-dom/Route';
import queryString from 'query-string';
import fetch from 'isomorphic-fetch';

import Pane from '@folio/stripes-components/lib/Pane';
import Paneset from '@folio/stripes-components/lib/Paneset';
import PaneMenu from '@folio/stripes-components/lib/PaneMenu';
import Button from '@folio/stripes-components/lib/Button';
import MultiColumnList from '@folio/stripes-components/lib/MultiColumnList';
import FilterPaneSearch from '@folio/stripes-components/lib/FilterPaneSearch';
import Layer from '@folio/stripes-components/lib/Layer';
import FilterGroups, { initialFilterState, onChangeFilter as commonChangeFilter } from '@folio/stripes-components/lib/FilterGroups';
import SRStatus from '@folio/stripes-components/lib/SRStatus';

import transitionToParams from '@folio/stripes-components/util/transitionToParams';
import makeQueryFunction from '@folio/stripes-components/util/makeQueryFunction';
import IfPermission from '@folio/stripes-components/lib/IfPermission';

import UserForm from './UserForm';
import ViewUser from './ViewUser';

import contactTypes from './data/contactTypes';
import { toUserAddresses } from './converters/address';

const INITIAL_RESULT_COUNT = 30;
const RESULT_COUNT_INCREMENT = 30;

const filterConfig = [
  {
    label: 'User status',
    name: 'active',
    cql: 'active',
    values: [
      { name: 'Active', cql: 'true' },
      { name: 'Inactive', cql: 'false' },
    ],
  },
  {
    label: 'Patron group',
    name: 'pg',
    cql: 'patronGroup',
    values: [], // will be filled in by componentWillUpdate
  },
];

class Users extends React.Component {
  static propTypes = {
    stripes: PropTypes.shape({
      logger: PropTypes.shape({
        log: PropTypes.func.isRequired,
      }).isRequired,
      connect: PropTypes.func.isRequired,
      hasPerm: PropTypes.func.isRequired,
    }).isRequired,
    data: PropTypes.object.isRequired,
    resources: PropTypes.shape({
      users: PropTypes.shape({
        hasLoaded: PropTypes.bool.isRequired,
        other: PropTypes.shape({
          totalRecords: PropTypes.number.isRequired,
        }),
        isPending: PropTypes.bool.isPending,
        successfulMutations: PropTypes.arrayOf(
          PropTypes.shape({
            record: PropTypes.shape({
              id: PropTypes.string.isRequired,
              username: PropTypes.string.isRequired,
            }).isRequired,
          }),
        ),
      }),
    }).isRequired,
    history: PropTypes.shape({
      push: PropTypes.func.isRequired,
    }).isRequired,
    location: PropTypes.shape({
      pathname: PropTypes.string.isRequired,
      search: PropTypes.string,
    }).isRequired,
    match: PropTypes.shape({
      path: PropTypes.string.isRequired,
    }).isRequired,
    mutator: PropTypes.shape({
      addUserMode: PropTypes.shape({
        replace: PropTypes.func,
      }),
      userCount: PropTypes.shape({
        replace: PropTypes.func,
      }),
      users: PropTypes.shape({
        POST: PropTypes.func,
      }),
    }).isRequired,
    okapi: PropTypes.shape({
      url: PropTypes.string.isRequired,
      tenant: PropTypes.string.isRequired,
      token: PropTypes.string.isRequired,
    }).isRequired,
    onSelectRow: PropTypes.func,
  };

  static manifest = Object.freeze({
    addUserMode: { initialValue: { mode: false } },
    userCount: { initialValue: INITIAL_RESULT_COUNT },
    users: {
      type: 'okapi',
      records: 'users',
      recordsRequired: '%{userCount}',
      perRequest: RESULT_COUNT_INCREMENT,
      path: 'users',
      GET: {
        params: {
          query: makeQueryFunction(
            'username=*',
            'username="$QUERY*" or personal.firstName="$QUERY*" or personal.lastName="$QUERY*"',
            {
              Active: 'active',
              Name: 'personal.lastName personal.firstName',
              'Patron Group': 'patronGroup',
              'User ID': 'username',
              Email: 'personal.email',
            },
            filterConfig,
          ),
        },
        staticFallback: { params: {} },
      },
    },
    patronGroups: {
      type: 'okapi',
      path: 'groups',
      records: 'usergroups',
    },
    addressTypes: {
      type: 'okapi',
      path: 'addresstypes',
      records: 'addressTypes',
    },
  });

  constructor(props) {
    super(props);

    const query = props.location.search ? queryString.parse(props.location.search) : {};

    let initiallySelected = {};
    if (/users\/view/.test(this.props.location.pathname)) {
      const id = /view\/(.*)\//.exec(this.props.location.pathname)[1];
      initiallySelected = { id };
    }

    this.state = {
      filters: initialFilterState(filterConfig, query.filters),
      selectedItem: initiallySelected,
      searchTerm: query.query || '',
      sortOrder: query.sort || '',
    };

    this.okapi = props.okapi;

    this.commonChangeFilter = commonChangeFilter.bind(this);
    this.transitionToParams = transitionToParams.bind(this);
    this.connectedViewUser = props.stripes.connect(ViewUser);
    const logger = props.stripes.logger;
    this.log = logger.log.bind(logger);

    this.anchoredRowFormatter = this.anchoredRowFormatter.bind(this);

    this.resultsList = null;
    this.SRStatus = null;
  }

  componentWillReceiveProps(nextProps) {
    const resource = this.props.resources.users;
    if (resource) {
      const sm = nextProps.resources.users.successfulMutations;
      if (sm.length > resource.successfulMutations.length)
        this.onSelectRow(undefined, { id: sm[0].record.id, username: sm[0].record.username });
    }

    if (resource && resource.isPending && !nextProps.resources.users.isPending) {
      this.log('event', 'new search-result');
      const resultAmount = nextProps.resources.users.other.totalRecords;
      this.SRStatus.sendMessage(`Search returned ${resultAmount} result${resultAmount !== 1 ? 's' : ''}`);
    }
  }

  componentWillUpdate() {
    const pg = this.props.data.patronGroups;
    if (pg && pg.length) {
      filterConfig[1].values = pg.map(rec => ({ name: rec.group, cql: rec.id }));
    }
  }

  onClearSearch = () => {
    this.log('action', 'cleared search');
    this.setState({ searchTerm: '' });
    this.props.history.push(this.props.location.pathname);
  }

  onSort = (e, meta) => {
    const newOrder = meta.alias;
    const oldOrder = this.state.sortOrder;

    const orders = oldOrder ? oldOrder.split(',') : [];
    if (newOrder === orders[0].replace(/^-/, '')) {
      orders[0] = `-${orders[0]}`.replace(/^--/, '');
    } else {
      orders.unshift(newOrder);
    }

    const sortOrder = orders.slice(0, 2).join(',');
    this.log('action', `sorted by ${sortOrder}`);
    this.setState({ sortOrder });
    this.transitionToParams({ sort: sortOrder });
  }

  onSelectRow = this.props.onSelectRow ? this.props.onSelectRow : (e, meta) => {
    const userId = meta.id;
    const username = meta.username;
    this.log('action', `clicked ${userId}, selected user =`, meta);
    this.setState({ selectedItem: meta });
    this.props.history.push(`/users/view/${userId}/${username}${this.props.location.search}`);
  }

  onClickAddNewUser = (e) => {
    if (e) e.preventDefault();
    this.log('action', 'clicked "add new user"');
    this.props.mutator.addUserMode.replace({ mode: true });
  }

  onClickCloseNewUser = (e) => {
    if (e) e.preventDefault();
    this.log('action', 'clicked "close new user"');
    this.props.mutator.addUserMode.replace({ mode: false });
  }

  onChangeFilter = (e) => {
    this.props.mutator.userCount.replace(INITIAL_RESULT_COUNT);
    this.commonChangeFilter(e);
  }

  onChangeSearch = (e) => {
    const query = e.target.value;
    this.props.mutator.userCount.replace(INITIAL_RESULT_COUNT);
    this.setState({ searchTerm: query });
    this.performSearch(query);
  }

  onNeedMore = () => {
    this.props.mutator.userCount.replace(this.props.data.userCount + RESULT_COUNT_INCREMENT);
  }

  getRowURL(rowData) {
    return `/users/view/${rowData.id}/${rowData.username}${this.props.location.search}`;
  }

  performSearch = _.debounce((query) => {
    this.log('action', `searched for '${query}'`);
    this.transitionToParams({ query });
  }, 350);

  updateFilters = (filters) => { // provided for onChangeFilter
    this.transitionToParams({ filters: Object.keys(filters).filter(key => filters[key]).join(',') });
  }

  create = (data) => {
    if (data.personal.addresses) {
      data.personal.addresses = toUserAddresses(data.personal.addresses, this.props.data.addressTypes); // eslint-disable-line no-param-reassign
    }

    // extract creds object from user object
    const creds = Object.assign({}, data.creds, { username: data.username });
    if (data.creds) delete data.creds; // eslint-disable-line no-param-reassign
    // POST user record
    this.props.mutator.users.POST(data);
    // POST credentials, permission-user, permissions;
    this.postCreds(data.username, creds);
    this.onClickCloseNewUser();
  }

  postCreds = (username, creds) => {
    this.log('xhr', `POST credentials for new user '${username}':`, creds);
    const localCreds = Object.assign({}, creds, creds.password ? {} : { password: '' });
    fetch(`${this.okapi.url}/authn/credentials`, {
      method: 'POST',
      headers: Object.assign({}, { 'X-Okapi-Tenant': this.okapi.tenant, 'X-Okapi-Token': this.okapi.token, 'Content-Type': 'application/json' }),
      body: JSON.stringify(localCreds),
    }).then((response) => {
      if (response.status >= 400) {
        this.log('xhr', 'Users. POST of creds failed.');
      } else {
        this.postPerms(username, []); // create empty permissions user
      }
    });
  }

  postPerms = (username, perms) => {
    this.log('xhr', `POST permissions for new user '${username}':`, perms);
    fetch(`${this.okapi.url}/perms/users`, {
      method: 'POST',
      headers: Object.assign({}, { 'X-Okapi-Tenant': this.okapi.tenant, 'X-Okapi-Token': this.okapi.token, 'Content-Type': 'application/json' }),
      body: JSON.stringify({ username, permissions: perms }),
    }).then((response) => {
      if (response.status >= 400) {
        this.log('xhr', 'Users. POST of users permissions failed.');
      } else {
        // nothing to do
      }
    });
  }

  collapseDetails = () => {
    this.setState({
      selectedItem: {},
    });
    this.props.history.push(`${this.props.match.path}${this.props.location.search}`);
  }

  // custom row formatter to wrap rows in anchor tags.
  anchoredRowFormatter(
    { rowIndex,
      rowClass,
      rowData,
      cells,
      rowProps,
      labelStrings,
    },
  ) {
    return (
      <a
        href={this.getRowURL(rowData)} key={`row-${rowIndex}`}
        aria-label={labelStrings && labelStrings.join('...')}
        role="listitem"
        className={rowClass}
        {...rowProps}
      >
        {cells}

    );
  }

  render() {
    const { data, stripes } = this.props;
    const users = data.users || [];

    /* searchHeader is a 'custom pane header'*/
    const searchHeader = <FilterPaneSearch id="SearchField" onChange={this.onChangeSearch} onClear={this.onClearSearch} resultsList={this.resultsList} value={this.state.searchTerm} />;

    const newUserButton = (
      <IfPermission perm="users.item.post">
        <IfPermission perm="login.item.post">
          <IfPermission perm="perms.users.item.post">
            <PaneMenu>
              <Button id="button-newuser" title="Add New User" onClick={this.onClickAddNewUser}>+ New</Button>
            </PaneMenu>
          </IfPermission>
        </IfPermission>
      </IfPermission>
    );

    const resultsFormatter = {
      Active: user => user.active,
      Name: user => `${_.get(user, ['personal', 'lastName'], '')}, ${_.get(user, ['personal', 'firstName'], '')}`,
      'Patron Group': (user) => {
        const pg = this.props.data.patronGroups.filter(g => g.id === user.patronGroup)[0];
        return pg ? pg.group : '?';
      },
      'User ID': user => user.username,
      Email: user => _.get(user, ['personal', 'email']),
    };

    const detailsPane = (
      this.props.stripes.hasPerm('users.item.get') ?
        (<Route
          path={`${this.props.match.path}/view/:userid/:username`}
          render={props => <this.connectedViewUser stripes={stripes} okapi={this.okapi} paneWidth="44%" onClose={this.collapseDetails} addressTypes={data.addressTypes} {...props} />}
        />) :
        (

Permission Error

Sorry - your user permissions do not allow access to this page.

));

    const resource = this.props.resources.users;
    return (
      <Paneset>
        <SRStatus ref={(ref) => { this.SRStatus = ref; }} />
        {/* Filter Pane */}
        <Pane id="pane-filter" defaultWidth="16%" header={searchHeader}>
          <FilterGroups config={filterConfig} filters={this.state.filters} onChangeFilter={this.onChangeFilter} />
        </Pane>
        {/* Results Pane */}
        <Pane
          id="pane-results"
          defaultWidth="fill"
          paneTitle={

              Results

                {resource && resource.hasLoaded ? resource.other.totalRecords : ''} Result{users.length === 1 ? '' : 's'} Found

          }
          lastMenu={newUserButton}
        >
          <MultiColumnList
            id="list-users"
            contentData={users}
            selectedRow={this.state.selectedItem}
            rowMetadata={['id', 'username']}
            formatter={resultsFormatter}
            onRowClick={this.onSelectRow}
            onHeaderClick={this.onSort}
            onNeedMoreData={this.onNeedMore}
            visibleColumns={['Active', 'Name', 'Patron Group', 'User ID', 'Email']}
            sortOrder={this.state.sortOrder.replace(/^-/, '').replace(/,.*/, '')}
            sortDirection={this.state.sortOrder.startsWith('-') ? 'descending' : 'ascending'}
            isEmptyMessage={`No results found for "${this.state.searchTerm}". Please check your spelling and filters.`}
            columnMapping={{ 'User ID': 'username' }}
            loading={resource ? resource.isPending : false}
            autosize
            virtualize
            ariaLabel={'User search results'}
            rowFormatter={this.anchoredRowFormatter}
            containerRef={(ref) => { this.resultsList = ref; }}
          />
        </Pane>

        {detailsPane}
        <Layer isOpen={data.addUserMode ? data.addUserMode.mode : false} label="Add New User Dialog">
          <UserForm
            id="userform-adduser"
            initialValues={{ active: true, personal: { preferredContactTypeId: '002' } }}
            addressTypes={data.addressTypes}
            onSubmit={(record) => { this.create(record); }}
            onCancel={this.onClickCloseNewUser}
            okapi={this.okapi}
            optionLists={{ patronGroups: this.props.data.patronGroups, contactTypes }}
          />
        </Layer>
      </Paneset>
    );
  }
}

export default Users;
<commit_msg>Add barcode column to Users list. Fixes UIU-123.<commit_after>import _ from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import Route from 'react-router-dom/Route';
import queryString from 'query-string';
import fetch from 'isomorphic-fetch';

import Pane from '@folio/stripes-components/lib/Pane';
import Paneset from '@folio/stripes-components/lib/Paneset';
import PaneMenu from '@folio/stripes-components/lib/PaneMenu';
import Button from '@folio/stripes-components/lib/Button';
import MultiColumnList from '@folio/stripes-components/lib/MultiColumnList';
import FilterPaneSearch from '@folio/stripes-components/lib/FilterPaneSearch';
import Layer from '@folio/stripes-components/lib/Layer';
import FilterGroups, { initialFilterState, onChangeFilter as commonChangeFilter } from '@folio/stripes-components/lib/FilterGroups';
import SRStatus from '@folio/stripes-components/lib/SRStatus';

import transitionToParams from '@folio/stripes-components/util/transitionToParams';
import makeQueryFunction from '@folio/stripes-components/util/makeQueryFunction';
import IfPermission from '@folio/stripes-components/lib/IfPermission';

import UserForm from './UserForm';
import ViewUser from './ViewUser';

import contactTypes from './data/contactTypes';
import { toUserAddresses } from './converters/address';

const INITIAL_RESULT_COUNT = 30;
const RESULT_COUNT_INCREMENT = 30;

const filterConfig = [
  {
    label: 'User status',
    name: 'active',
    cql: 'active',
    values: [
      { name: 'Active', cql: 'true' },
      { name: 'Inactive', cql: 'false' },
    ],
  },
  {
    label: 'Patron group',
    name: 'pg',
    cql: 'patronGroup',
    values: [], // will be filled in by componentWillUpdate
  },
];

class Users extends React.Component {
  static propTypes = {
    stripes: PropTypes.shape({
      logger: PropTypes.shape({
        log: PropTypes.func.isRequired,
      }).isRequired,
      connect: PropTypes.func.isRequired,
      hasPerm: PropTypes.func.isRequired,
    }).isRequired,
    data: PropTypes.object.isRequired,
    resources: PropTypes.shape({
      users: PropTypes.shape({
        hasLoaded: PropTypes.bool.isRequired,
        other: PropTypes.shape({
          totalRecords: PropTypes.number.isRequired,
        }),
        isPending: PropTypes.bool.isPending,
        successfulMutations: PropTypes.arrayOf(
          PropTypes.shape({
            record: PropTypes.shape({
              id: PropTypes.string.isRequired,
              username: PropTypes.string.isRequired,
            }).isRequired,
          }),
        ),
      }),
    }).isRequired,
    history: PropTypes.shape({
      push: PropTypes.func.isRequired,
    }).isRequired,
    location: PropTypes.shape({
      pathname: PropTypes.string.isRequired,
      search: PropTypes.string,
    }).isRequired,
    match: PropTypes.shape({
      path: PropTypes.string.isRequired,
    }).isRequired,
    mutator: PropTypes.shape({
      addUserMode: PropTypes.shape({
        replace: PropTypes.func,
      }),
      userCount: PropTypes.shape({
        replace: PropTypes.func,
      }),
      users: PropTypes.shape({
        POST: PropTypes.func,
      }),
    }).isRequired,
    okapi: PropTypes.shape({
      url: PropTypes.string.isRequired,
      tenant: PropTypes.string.isRequired,
      token: PropTypes.string.isRequired,
    }).isRequired,
    onSelectRow: PropTypes.func,
  };

  static manifest = Object.freeze({
    addUserMode: { initialValue: { mode: false } },
    userCount: { initialValue: INITIAL_RESULT_COUNT },
    users: {
      type: 'okapi',
      records: 'users',
      recordsRequired: '%{userCount}',
      perRequest: RESULT_COUNT_INCREMENT,
      path: 'users',
      GET: {
        params: {
          query: makeQueryFunction(
            'username=*',
            'username="$QUERY*" or personal.firstName="$QUERY*" or personal.lastName="$QUERY*"',
            {
              Active: 'active',
              Name: 'personal.lastName personal.firstName',
              'Patron Group': 'patronGroup',
              'User ID': 'username',
              Email: 'personal.email',
            },
            filterConfig,
          ),
        },
        staticFallback: { params: {} },
      },
    },
    patronGroups: {
      type: 'okapi',
      path: 'groups',
      records: 'usergroups',
    },
    addressTypes: {
      type: 'okapi',
      path: 'addresstypes',
      records: 'addressTypes',
    },
  });

  constructor(props) {
    super(props);

    const query = props.location.search ? queryString.parse(props.location.search) : {};

    let initiallySelected = {};
    if (/users\/view/.test(this.props.location.pathname)) {
      const id = /view\/(.*)\//.exec(this.props.location.pathname)[1];
      initiallySelected = { id };
    }

    this.state = {
      filters: initialFilterState(filterConfig, query.filters),
      selectedItem: initiallySelected,
      searchTerm: query.query || '',
      sortOrder: query.sort || '',
    };

    this.okapi = props.okapi;

    this.commonChangeFilter = commonChangeFilter.bind(this);
    this.transitionToParams = transitionToParams.bind(this);
    this.connectedViewUser = props.stripes.connect(ViewUser);
    const logger = props.stripes.logger;
    this.log = logger.log.bind(logger);

    this.anchoredRowFormatter = this.anchoredRowFormatter.bind(this);

    this.resultsList = null;
    this.SRStatus = null;
  }

  componentWillReceiveProps(nextProps) {
    const resource = this.props.resources.users;
    if (resource) {
      const sm = nextProps.resources.users.successfulMutations;
      if (sm.length > resource.successfulMutations.length)
        this.onSelectRow(undefined, { id: sm[0].record.id, username: sm[0].record.username });
    }

    if (resource && resource.isPending && !nextProps.resources.users.isPending) {
      this.log('event', 'new search-result');
      const resultAmount = nextProps.resources.users.other.totalRecords;
      this.SRStatus.sendMessage(`Search returned ${resultAmount} result${resultAmount !== 1 ? 's' : ''}`);
    }
  }

  componentWillUpdate() {
    const pg = this.props.data.patronGroups;
    if (pg && pg.length) {
      filterConfig[1].values = pg.map(rec => ({ name: rec.group, cql: rec.id }));
    }
  }

  onClearSearch = () => {
    this.log('action', 'cleared search');
    this.setState({ searchTerm: '' });
    this.props.history.push(this.props.location.pathname);
  }

  onSort = (e, meta) => {
    const newOrder = meta.alias;
    const oldOrder = this.state.sortOrder;

    const orders = oldOrder ? oldOrder.split(',') : [];
    if (newOrder === orders[0].replace(/^-/, '')) {
      orders[0] = `-${orders[0]}`.replace(/^--/, '');
    } else {
      orders.unshift(newOrder);
    }

    const sortOrder = orders.slice(0, 2).join(',');
    this.log('action', `sorted by ${sortOrder}`);
    this.setState({ sortOrder });
    this.transitionToParams({ sort: sortOrder });
  }

  onSelectRow = this.props.onSelectRow ? this.props.onSelectRow : (e, meta) => {
    const userId = meta.id;
    const username = meta.username;
    this.log('action', `clicked ${userId}, selected user =`, meta);
    this.setState({ selectedItem: meta });
    this.props.history.push(`/users/view/${userId}/${username}${this.props.location.search}`);
  }

  onClickAddNewUser = (e) => {
    if (e) e.preventDefault();
    this.log('action', 'clicked "add new user"');
    this.props.mutator.addUserMode.replace({ mode: true });
  }

  onClickCloseNewUser = (e) => {
    if (e) e.preventDefault();
    this.log('action', 'clicked "close new user"');
    this.props.mutator.addUserMode.replace({ mode: false });
  }

  onChangeFilter = (e) => {
    this.props.mutator.userCount.replace(INITIAL_RESULT_COUNT);
    this.commonChangeFilter(e);
  }

  onChangeSearch = (e) => {
    const query = e.target.value;
    this.props.mutator.userCount.replace(INITIAL_RESULT_COUNT);
    this.setState({ searchTerm: query });
    this.performSearch(query);
  }

  onNeedMore = () => {
    this.props.mutator.userCount.replace(this.props.data.userCount + RESULT_COUNT_INCREMENT);
  }

  getRowURL(rowData) {
    return `/users/view/${rowData.id}/${rowData.username}${this.props.location.search}`;
  }

  performSearch = _.debounce((query) => {
    this.log('action', `searched for '${query}'`);
    this.transitionToParams({ query });
  }, 350);

  updateFilters = (filters) => { // provided for onChangeFilter
    this.transitionToParams({ filters: Object.keys(filters).filter(key => filters[key]).join(',') });
  }

  create = (data) => {
    if (data.personal.addresses) {
      data.personal.addresses = toUserAddresses(data.personal.addresses, this.props.data.addressTypes); // eslint-disable-line no-param-reassign
    }

    // extract creds object from user object
    const creds = Object.assign({}, data.creds, { username: data.username });
    if (data.creds) delete data.creds; // eslint-disable-line no-param-reassign
    // POST user record
    this.props.mutator.users.POST(data);
    // POST credentials, permission-user, permissions;
    this.postCreds(data.username, creds);
    this.onClickCloseNewUser();
  }

  postCreds = (username, creds) => {
    this.log('xhr', `POST credentials for new user '${username}':`, creds);
    const localCreds = Object.assign({}, creds, creds.password ? {} : { password: '' });
    fetch(`${this.okapi.url}/authn/credentials`, {
      method: 'POST',
      headers: Object.assign({}, { 'X-Okapi-Tenant': this.okapi.tenant, 'X-Okapi-Token': this.okapi.token, 'Content-Type': 'application/json' }),
      body: JSON.stringify(localCreds),
    }).then((response) => {
      if (response.status >= 400) {
        this.log('xhr', 'Users. POST of creds failed.');
      } else {
        this.postPerms(username, []); // create empty permissions user
      }
    });
  }

  postPerms = (username, perms) => {
    this.log('xhr', `POST permissions for new user '${username}':`, perms);
    fetch(`${this.okapi.url}/perms/users`, {
      method: 'POST',
      headers: Object.assign({}, { 'X-Okapi-Tenant': this.okapi.tenant, 'X-Okapi-Token': this.okapi.token, 'Content-Type': 'application/json' }),
      body: JSON.stringify({ username, permissions: perms }),
    }).then((response) => {
      if (response.status >= 400) {
        this.log('xhr', 'Users. POST of users permissions failed.');
      } else {
        // nothing to do
      }
    });
  }

  collapseDetails = () => {
    this.setState({
      selectedItem: {},
    });
    this.props.history.push(`${this.props.match.path}${this.props.location.search}`);
  }

  // custom row formatter to wrap rows in anchor tags.
  anchoredRowFormatter(
    { rowIndex,
      rowClass,
      rowData,
      cells,
      rowProps,
      labelStrings,
    },
  ) {
    return (
      <a
        href={this.getRowURL(rowData)} key={`row-${rowIndex}`}
        aria-label={labelStrings && labelStrings.join('...')}
        role="listitem"
        className={rowClass}
        {...rowProps}
      >
        {cells}

    );
  }

  render() {
    const { data, stripes } = this.props;
    const users = data.users || [];

    /* searchHeader is a 'custom pane header'*/
    const searchHeader = <FilterPaneSearch id="SearchField" onChange={this.onChangeSearch} onClear={this.onClearSearch} resultsList={this.resultsList} value={this.state.searchTerm} />;

    const newUserButton = (
      <IfPermission perm="users.item.post">
        <IfPermission perm="login.item.post">
          <IfPermission perm="perms.users.item.post">
            <PaneMenu>
              <Button id="button-newuser" title="Add New User" onClick={this.onClickAddNewUser}>+ New</Button>
            </PaneMenu>
          </IfPermission>
        </IfPermission>
      </IfPermission>
    );

    const resultsFormatter = {
      Active: user => user.active,
      Name: user => `${_.get(user, ['personal', 'lastName'], '')}, ${_.get(user, ['personal', 'firstName'], '')}`,
      Barcode: user => user.barcode,
      'Patron Group': (user) => {
        const pg = this.props.data.patronGroups.filter(g => g.id === user.patronGroup)[0];
        return pg ? pg.group : '?';
      },
      'User ID': user => user.username,
      Email: user => _.get(user, ['personal', 'email']),
    };

    const detailsPane = (
      this.props.stripes.hasPerm('users.item.get') ?
        (<Route
          path={`${this.props.match.path}/view/:userid/:username`}
          render={props => <this.connectedViewUser stripes={stripes} okapi={this.okapi} paneWidth="44%" onClose={this.collapseDetails} addressTypes={data.addressTypes} {...props} />}
        />) :
        (

Permission Error

Sorry - your user permissions do not allow access to this page.

));

    const resource = this.props.resources.users;
    return (
      <Paneset>
        <SRStatus ref={(ref) => { this.SRStatus = ref; }} />
        {/* Filter Pane */}
        <Pane id="pane-filter" defaultWidth="16%" header={searchHeader}>
          <FilterGroups config={filterConfig} filters={this.state.filters} onChangeFilter={this.onChangeFilter} />
        </Pane>
        {/* Results Pane */}
        <Pane
          id="pane-results"
          defaultWidth="fill"
          paneTitle={

              Results

                {resource && resource.hasLoaded ? resource.other.totalRecords : ''} Result{users.length === 1 ? '' : 's'} Found

          }
          lastMenu={newUserButton}
        >
          <MultiColumnList
            id="list-users"
            contentData={users}
            selectedRow={this.state.selectedItem}
            rowMetadata={['id', 'username']}
            formatter={resultsFormatter}
            onRowClick={this.onSelectRow}
            onHeaderClick={this.onSort}
            onNeedMoreData={this.onNeedMore}
            visibleColumns={['Active', 'Name', 'Barcode', 'Patron Group', 'User ID', 'Email']}
            sortOrder={this.state.sortOrder.replace(/^-/, '').replace(/,.*/, '')}
            sortDirection={this.state.sortOrder.startsWith('-') ? 'descending' : 'ascending'}
            isEmptyMessage={`No results found for "${this.state.searchTerm}". Please check your spelling and filters.`}
            columnMapping={{ 'User ID': 'username' }}
            loading={resource ? resource.isPending : false}
            autosize
            virtualize
            ariaLabel={'User search results'}
            rowFormatter={this.anchoredRowFormatter}
            containerRef={(ref) => { this.resultsList = ref; }}
          />
        </Pane>

        {detailsPane}
        <Layer isOpen={data.addUserMode ? data.addUserMode.mode : false} label="Add New User Dialog">
          <UserForm
            id="userform-adduser"
            initialValues={{ active: true, personal: { preferredContactTypeId: '002' } }}
            addressTypes={data.addressTypes}
            onSubmit={(record) => { this.create(record); }}
            onCancel={this.onClickCloseNewUser}
            okapi={this.okapi}
            optionLists={{ patronGroups: this.props.data.patronGroups, contactTypes }}
          />
        </Layer>
      </Paneset>
    );
  }
}

export default Users;
<|endoftext|>


<commit_before>require('dotenv').config({ silent: true });
var debug = require('debug')('video');
var mongoose = require('mongoose');
const path = require('path');
var Schema = mongoose.Schema;
const glob = require('glob');
const async = require('async');
const fs = require('fs');
const exec = require("child_process").exec;
var random = require('mongoose-simple-random');
var which = require('which');

var ffprobe = "ffprobe";
which('ffprobe', function (err, resolvedPath) {
	if(err) throw err;
	ffprobe = resolvedPath;
});

var VideoSchema = Schema({
	name: 			{ type: String, required: true },
	file_present:   { type: Boolean, default: false },
	duration: 		{ type: Number, default: null },
	score: 			{ type: Number, default: 0 },
	blacklisted: 	{ type: Boolean, default: false },
	places: 		[ { type: String } ],
	items: 			[ { type: String } ],
	themes: 		[ { type: String } ]
});

VideoSchema.plugin(random);

var lean = function(doc, ret, options) {
	ret.id = ret._id;
	delete ret._id;
	delete ret.__v;
	return ret;
}

if (!VideoSchema.options.toObject) VideoSchema.options.toObject = {};
VideoSchema.options.toObject.transform = lean;

if (!VideoSchema.options.toJSON) VideoSchema.options.toJSON = {};
VideoSchema.options.toJSON.transform = lean;

// --------------------------------------------------------------------------------------
VideoSchema.virtual('full_path').get(function(){
	return path.join(process.env.VIDEO_ROOT, this.name);
});

// --------------------------------------------------------------------------------------
VideoSchema.virtual('osc_address').get(function(){
	return '/'+path.basename(this.name, path.extname(this.name));
});

// --------------------------------------------------------------------------------------
VideoSchema.pre('save', function(next) {
	if(this.duration) next(null);
	else this.set_duration(next);
});

// --------------------------------------------------------------------------------------
VideoSchema.methods.as_playlist = function() {
	return {
		id: this._id,
		path: this.full_path,
		score: this.score
	};
}

// --------------------------------------------------------------------------------------
VideoSchema.methods.set_duration = function(callback) {
	var cmd = `${ffprobe} -i "${this.full_path}"`;
	debug(cmd);
	exec(cmd, (err, stdout, stderr) => {
		var matches = stderr.match(/Duration:\s{1}(\d+?):(\d+?):(\d+\.\d+?),/);
		if(err) {
			callback(err);
		} else if(!matches || matches.length < 4) {
			callback("Couldn't determine video duration");
		} else {
			var hours = parseInt(matches[1], 10);
			var minutes = parseInt(matches[2], 10);
			var seconds = parseFloat(matches[3], 10);
			var duration = (hours*3600) + (minutes*60) + seconds;

			this.duration = duration;
			callback();
		}
	});
}

// --------------------------------------------------------------------------------------
VideoSchema.statics.list = function(callback) {
	return this.find().exec( function( err, videos ) {
		if( err ) return callback( err );
		callback(null, videos);
	});
};

// --------------------------------------------------------------------------------------
VideoSchema.statics.scanFiles = function(done) {

	debug("scan_files", process.env.VIDEO_ROOT)
	glob("*.mp4", {cwd: process.env.VIDEO_ROOT}, (err, files) => {
		if( err ) return done( err );

		//debug("looping through mp4s", files)
		async.eachSeries(files, (item, callback) => {
			var _root = path.basename(item);
			//debug("searching for", _root, "in database");

			this.find({name: _root}).exec((err, docs) => {
				if(err) return callback(err);

				var video = null;
				if(docs.length==0) {
					video = new this({name: _root});
				} else {
					video = docs[0]
				}
				video.file_present = true;
				video.save(callback);
			});
		}, done);
	})
}

// --------------------------------------------------------------------------------------
VideoSchema.statics.scanDatabase = function(done) {

	debug("scan_db")

	this.find().exec( ( err, docs ) => {
		if( err ) return done( err );
		//debug("looping through db documents")

		async.eachSeries(docs, (item, callback) => {
			var _path = path.join(process.env.VIDEO_ROOT, item.name)

			//debug("looking for", _path, "in filesystem");
			fs.stat(_path, (err, stat) => {
				if(err == null) {
					item.file_present = true;
				} else if(err.code == 'ENOENT') {
					item.file_present = false;
				} else {
					console.log('Some other error: ', err.code);
				}
				item.save(callback);
			});
		}, done);
	});
}

// --------------------------------------------------------------------------------------
VideoSchema.statics.minCount = function(done) {
	this.find().exec( ( err, docs ) => {
		if(err) return done(err);
		if(docs.length < 100) return done("not enough videos in database");
		done(null);
	});
}

// --------------------------------------------------------------------------------------
VideoSchema.statics.scan = function(callback) {
	var tasks = [
		this.scanFiles.bind(this),
		this.scanDatabase.bind(this),
		this.minCount.bind(this)
	];
	return async.series(tasks, callback);
}

// --------------------------------------------------------------------------------------
var get_score = function(terms, tags) {
	var results = [];

	terms.forEach((t) => {
		if(t.text) {
			var i = tags.indexOf( t.text.toLowerCase() );
			if(i > -1) results.push(t);
		}
	});

	var total = results.reduce((total, term) => {
		return total + term.relevance;
	}, 0);

	return total;
}

// --------------------------------------------------------------------------------------
VideoSchema.statics.setScores = function(terms, callback) {

	// this.update({}, {'$set': {'score' : 0}}, {multi: true}, function(err, properties){
	// 	if(err) return callback(err);

		this.find().exec( function( err, docs ) {
			if( err ) return callback( err );

			async.each(docs, (doc, done) => {

				var places = get_score(terms, doc.places);
				var items = get_score(terms, doc.items);
				var themes = get_score(terms, doc.themes);

				doc.score = (items*3) + (places*2) + (themes);

				doc.save(done);

			}, callback);
		});
	//});
}

// --------------------------------------------------------------------------------------
VideoSchema.statics.getPlaylist = function(terms, length, callback) {
	this.setScores(terms, (err) => {
		if(err) return callback(err);

		var query = {
			blacklisted: false,
			file_present: true,
			score: { "$gt": 0 }
		};
		this.find(query).sort({'score': -1}).exec((err, scored) => {
			if(err) return callback(err);

			if(scored.length >= length) {
				return callback(null, scored);
			} else {
				var limit = length - scored.length;
				var query = {
					file_present: true,
					_id: { "$nin": blacklist },
					_id: { "$nin": scored.map( doc => { return doc._id }) }
				};
				this.findRandom(query, {}, {"limit": limit}, (err, random) => {
					if (err) return callback(err);

					callback(null, scored.concat(random));
				});
			} // end else
		});
	});
}

module.exports = mongoose.model( 'Video', VideoSchema, 'videos' );

<commit_msg>whoops<commit_after>require('dotenv').config({ silent: true });
var debug = require('debug')('video');
var mongoose = require('mongoose');
const path = require('path');
var Schema = mongoose.Schema;
const glob = require('glob');
const async = require('async');
const fs = require('fs');
const exec = require("child_process").exec;
var random = require('mongoose-simple-random');
var which = require('which');

var ffprobe = "ffprobe";
which('ffprobe', function (err, resolvedPath) {
	if(err) throw err;
	ffprobe = resolvedPath;
});

var VideoSchema = Schema({
	name: 			{ type: String, required: true },
	file_present:   { type: Boolean, default: false },
	duration: 		{ type: Number, default: null },
	score: 			{ type: Number, default: 0 },
	blacklisted: 	{ type: Boolean, default: false },
	places: 		[ { type: String } ],
	items: 			[ { type: String } ],
	themes: 		[ { type: String } ]
});

VideoSchema.plugin(random);

var lean = function(doc, ret, options) {
	ret.id = ret._id;
	delete ret._id;
	delete ret.__v;
	return ret;
}

if (!VideoSchema.options.toObject) VideoSchema.options.toObject = {};
VideoSchema.options.toObject.transform = lean;

if (!VideoSchema.options.toJSON) VideoSchema.options.toJSON = {};
VideoSchema.options.toJSON.transform = lean;

// --------------------------------------------------------------------------------------
VideoSchema.virtual('full_path').get(function(){
	return path.join(process.env.VIDEO_ROOT, this.name);
});

// --------------------------------------------------------------------------------------
VideoSchema.virtual('osc_address').get(function(){
	return '/'+path.basename(this.name, path.extname(this.name));
});

// --------------------------------------------------------------------------------------
VideoSchema.pre('save', function(next) {
	if(this.duration) next(null);
	else this.set_duration(next);
});

// --------------------------------------------------------------------------------------
VideoSchema.methods.as_playlist = function() {
	return {
		id: this._id,
		path: this.full_path,
		score: this.score
	};
}

// --------------------------------------------------------------------------------------
VideoSchema.methods.set_duration = function(callback) {
	var cmd = `${ffprobe} -i "${this.full_path}"`;
	debug(cmd);
	exec(cmd, (err, stdout, stderr) => {
		var matches = stderr.match(/Duration:\s{1}(\d+?):(\d+?):(\d+\.\d+?),/);
		if(err) {
			callback(err);
		} else if(!matches || matches.length < 4) {
			callback("Couldn't determine video duration");
		} else {
			var hours = parseInt(matches[1], 10);
			var minutes = parseInt(matches[2], 10);
			var seconds = parseFloat(matches[3], 10);
			var duration = (hours*3600) + (minutes*60) + seconds;

			this.duration = duration;
			callback();
		}
	});
}

// --------------------------------------------------------------------------------------
VideoSchema.statics.list = function(callback) {
	return this.find().exec( function( err, videos ) {
		if( err ) return callback( err );
		callback(null, videos);
	});
};

// --------------------------------------------------------------------------------------
VideoSchema.statics.scanFiles = function(done) {

	debug("scan_files", process.env.VIDEO_ROOT)
	glob("*.mp4", {cwd: process.env.VIDEO_ROOT}, (err, files) => {
		if( err ) return done( err );

		//debug("looping through mp4s", files)
		async.eachSeries(files, (item, callback) => {
			var _root = path.basename(item);
			//debug("searching for", _root, "in database");

			this.find({name: _root}).exec((err, docs) => {
				if(err) return callback(err);

				var video = null;
				if(docs.length==0) {
					video = new this({name: _root});
				} else {
					video = docs[0]
				}
				video.file_present = true;
				video.save(callback);
			});
		}, done);
	})
}

// --------------------------------------------------------------------------------------
VideoSchema.statics.scanDatabase = function(done) {

	debug("scan_db")

	this.find().exec( ( err, docs ) => {
		if( err ) return done( err );
		//debug("looping through db documents")

		async.eachSeries(docs, (item, callback) => {
			var _path = path.join(process.env.VIDEO_ROOT, item.name)

			//debug("looking for", _path, "in filesystem");
			fs.stat(_path, (err, stat) => {
				if(err == null) {
					item.file_present = true;
				} else if(err.code == 'ENOENT') {
					item.file_present = false;
				} else {
					console.log('Some other error: ', err.code);
				}
				item.save(callback);
			});
		}, done);
	});
}

// --------------------------------------------------------------------------------------
VideoSchema.statics.minCount = function(done) {
	this.find().exec( ( err, docs ) => {
		if(err) return done(err);
		if(docs.length < 100) return done("not enough videos in database");
		done(null);
	});
}

// --------------------------------------------------------------------------------------
VideoSchema.statics.scan = function(callback) {
	var tasks = [
		this.scanFiles.bind(this),
		this.scanDatabase.bind(this),
		this.minCount.bind(this)
	];
	return async.series(tasks, callback);
}

// --------------------------------------------------------------------------------------
var get_score = function(terms, tags) {
	var results = [];

	terms.forEach((t) => {
		if(t.text) {
			var i = tags.indexOf( t.text.toLowerCase() );
			if(i > -1) results.push(t);
		}
	});

	var total = results.reduce((total, term) => {
		return total + term.relevance;
	}, 0);

	return total;
}

// --------------------------------------------------------------------------------------
VideoSchema.statics.setScores = function(terms, callback) {

	// this.update({}, {'$set': {'score' : 0}}, {multi: true}, function(err, properties){
	// 	if(err) return callback(err);

		this.find().exec( function( err, docs ) {
			if( err ) return callback( err );

			async.each(docs, (doc, done) => {

				var places = get_score(terms, doc.places);
				var items = get_score(terms, doc.items);
				var themes = get_score(terms, doc.themes);

				doc.score = (items*3) + (places*2) + (themes);

				doc.save(done);

			}, callback);
		});
	//});
}

// --------------------------------------------------------------------------------------
VideoSchema.statics.getPlaylist = function(terms, length, callback) {
	this.setScores(terms, (err) => {
		if(err) return callback(err);

		var query = {
			blacklisted: false,
			file_present: true,
			score: { "$gt": 0 }
		};
		this.find(query).sort({'score': -1}).exec((err, scored) => {
			if(err) return callback(err);

			if(scored.length >= length) {
				return callback(null, scored);
			} else {
				var limit = length - scored.length;
				var query = {
					file_present: true,
					blacklisted: false,
					_id: { "$nin": scored.map( doc => { return doc._id }) }
				};
				this.findRandom(query, {}, {"limit": limit}, (err, random) => {
					if (err) return callback(err);

					callback(null, scored.concat(random));
				});
			} // end else
		});
	});
}

module.exports = mongoose.model( 'Video', VideoSchema, 'videos' );

<|endoftext|>


<commit_before>"use strict"

class Magma // technically a magma has exactly one operation, i have an arbitrary number. oh, well..
{
	constructor (lut = [], ops = [])
	{ this.lut = lut; this.ops = ops }

	opFromString (symbol, opString, separators)
	{
		function splitRecursive (splitString, depth)
		{
			var content = []

			if (depth < separators.length)
				for (var substring of splitString.split (separators [depth]))
					content.push (splitRecursive (substring, depth + 1))
			else content = splitString

			return content
		}

		this.lut[symbol] = splitRecursive (opString, 0)

		this.ops.push (symbol)
	}

	static allBinaryOps (limit = 3)
	{
		var result = []

		const total = Math.pow(limit, Math.pow(limit,2))

		var magmaCount = 0

		while (magmaCount < total)
		{
			result.push (Magma.binaryOpFromNumber (magmaCount, limit))
			magmaCount++
		}

		return result
	}

	static filteredBinaryOps (propertyList = [], limit = 3)
	{
		var result = []

		const total = Math.pow(limit, Math.pow(limit,2))

		var magmaCount = 0

		while (magmaCount < total)
		{
			var magma = Magma.binaryOpFromNumber (magmaCount, limit)

			var keep = true; var filterCount = 0
			while (keep && filterCount < propertyList.length)
			{
				keep = Query.fromPropertyString
				(propertyList[filterCount], magma).evaluate ()
				filterCount++
			}

			if (keep) result.push (magma)

			magmaCount++
		}

		return result
	}

	static binaryOpFromNumber (magmaNumber, limit = 3, symbol = "!")
	{
		var op = []

		var cellCount = 0

		const cellNum = Math.pow(limit,2)

		while (cellCount < cellNum)
		{
			if (! op [Math.floor (cellCount / limit)]) op [Math.floor(cellCount / limit)] = []

			op [Math.floor (cellCount / limit)] [cellCount % limit] =
			Math.floor ((magmaNumber / Math.pow (limit, cellCount)) % limit)
			cellCount++
		}

		return new Magma ({"!":op}, ["!"])
	}

	static filter (inputMagmas = [], propertyList = [])
	{
		if (propertyList.length == 0) return inputMagmas

		var result = []

		for (var magma of inputMagmas)
		{
			var query = Query.fromPropertyString(propertyList[0], magma)

			//query.magma = magma

			if (query.evaluate ()) result.push (magma)
		}

		return Magma.filter (result, propertyList.splice (1))

	}

	getDepth (symbol)
	{
		depth = function (current)
		{
			if (Array.isArray(current))
				return depth (current [0]) + 1
			else
				return 0
		}
		return depth (this.lut[symbol])
	}

	getLimit ()
	{ return this.lut[this.ops[0]].length }

	opToTable (op = this.ops[0])
	{
		var array = []
		for (var entry of this.lut[op])
			array.push (entry.join(""))
		return array.join("\n")
	}

	opToHTMLTable (op = this.ops[0])
	{
		var table = document.createElement("table")
		for (var row of this.lut[op])
		{
			var tr = document.createElement ("tr")
			for (var cell of row)
			{
				var td = document.createElement ("td")
				td.innerHTML = cell
				tr.appendChild(td)
			}
			table.appendChild(tr)
		}
		table.className = "operator"
		return table
	}
}

class Term
{
	constructor (input = [], magma = new Magma ())
	{
		this.input = input; this.lut = magma.lut; this.magma = magma
		this.progress = 0; this.stack = []
	}

	static fromString (termString = "", magma = new Magma (), separator = "")
	{ return new Term (termString.split(separator), magma) }

	lookup (currentItem = this.input [this.progress], currentLut = this.lut)
	{
		if (Array.isArray (currentLut [currentItem]))
			return this.lookup (this.stack.pop (), currentLut [currentItem])
		else if (currentItem in currentLut)
			return (currentLut [currentItem])
		else
			return (currentItem)
	}

	evaluate ()
	{
		while (this.progress < this.input.length)
		{
			this.stack.push (this.lookup())
			this.progress ++
		}
		var stack = this.stack
		this.stack = []
		return stack
	}

	isEqual ()
	{
		var stack = this.evaluate()
		var result = true
		for (var element of stack)
			result = result && stack[0] == element
		return result
	}

}

class Relation // todo: equations probably won't cut it forever
{}

class Query // checks if properties apply
{
	constructor (quantifiers = [], limit = 0, term = new Term ())
	{
		this.quantifiers = quantifiers; this.limit = limit
		this.term = term
	}

	static fromString (quantifiersString = "", limit = 0, term = new Term ())
	{ return new Query (quantifiersString.match (/.{2}/g), limit, term) }

	static fromPropertyString (propString, magma)
	{
		const lower = "abcdefghijklmnopqrstuvwxyz"
		const upper = lower.toUpperCase()
		const opSymbols = "*+/-&|%$"

		var opCount = 0
		while (propString.includes (opSymbols [opCount]))
		{
			while (propString.includes (opSymbols [opCount]))
				propString = propString.replace
				(opSymbols [opCount], magma.ops[opCount])
			opCount++
		}

		var quanifierCount = 0; var quantifiers = []

		while (quanifierCount < lower.length)
		{
			if (propString.includes (lower[quanifierCount]))
				quantifiers.push (lower[quanifierCount] + "E")
			else if (propString.includes (upper[quanifierCount]))
				quantifiers.push (upper[quanifierCount] + "A")
			else break
			quanifierCount++
		}

		return new Query
		(
			quantifiers,
			magma.getLimit (),
			Term.fromString(propString, magma, "")
		)

	}

	evaluate ()
	{

		if (this.quantifiers.length == 0)
			return this.term.isEqual ()
		else
		{
			const all_not_ex = this.quantifiers[0][1] == "A"
			const replaceThis = this.quantifiers [0][0]
			var count = 0; var result = all_not_ex

			while (count < this.limit && result == all_not_ex)
			{
				var newInput = this.term.input.map (function (variable)
				{ return variable == replaceThis ? count : variable; })
				var subQuery = new Query
				(
					this.quantifiers.slice (1),
					this.limit,
					new Term (newInput, this.term.magma)
				)
				result = subQuery.evaluate ()
				count ++
			}
			return result
		}
	}
}

class MagmaGenerator //todo: generate magmas with given properties
{}

class QueryGenerator //todo: generate properties
{}<commit_msg>using "| 0" instead of Math.floor()<commit_after>"use strict"

class Magma // technically a magma has exactly one operation, i have an arbitrary number. oh, well..
{
	constructor (lut = [], ops = [])
	{ this.lut = lut; this.ops = ops }

	opFromString (symbol, opString, separators)
	{
		function splitRecursive (splitString, depth)
		{
			var content = []

			if (depth < separators.length)
				for (var substring of splitString.split (separators [depth]))
					content.push (splitRecursive (substring, depth + 1))
			else content = splitString

			return content
		}

		this.lut[symbol] = splitRecursive (opString, 0)

		this.ops.push (symbol)
	}

	static allBinaryOps (limit = 3)
	{
		var result = []

		const total = Math.pow(limit, Math.pow(limit,2))

		var magmaCount = 0

		while (magmaCount < total)
		{
			result.push (Magma.binaryOpFromNumber (magmaCount, limit))
			magmaCount++
		}

		return result
	}

	static filteredBinaryOps (propertyList = [], limit = 3)
	{
		var result = []

		const total = Math.pow(limit, Math.pow(limit,2))

		var magmaCount = 0

		while (magmaCount < total)
		{
			var magma = Magma.binaryOpFromNumber (magmaCount, limit)

			var keep = true; var filterCount = 0
			while (keep && filterCount < propertyList.length)
			{
				keep = Query.fromPropertyString
				(propertyList[filterCount], magma).evaluate ()
				filterCount++
			}

			if (keep) result.push (magma)

			magmaCount++
		}

		return result
	}

	static binaryOpFromNumber (magmaNumber, limit = 3, symbol = "!")
	{
		var op = []

		var cellCount = 0

		const cellNum = Math.pow(limit,2)

		while (cellCount < cellNum)
		{
			// num | 0 should be much faster than Math.floor()
			if (! op [(cellCount / limit) | 0]) op [(cellCount / limit) | 0] = []

			op [(cellCount / limit) | 0] [cellCount % limit] =
			((magmaNumber / Math.pow (limit, cellCount)) % limit) | 0
			cellCount ++
		}

		return new Magma ({"!":op}, ["!"])
	}

	static filter (inputMagmas = [], propertyList = [])
	{
		if (propertyList.length == 0) return inputMagmas

		var result = []

		for (var magma of inputMagmas)
		{
			var query = Query.fromPropertyString(propertyList[0], magma)

			//query.magma = magma

			if (query.evaluate ()) result.push (magma)
		}

		return Magma.filter (result, propertyList.splice (1))

	}

	getDepth (symbol)
	{
		depth = function (current)
		{
			if (Array.isArray(current))
				return depth (current [0]) + 1
			else
				return 0
		}
		return depth (this.lut[symbol])
	}

	getLimit ()
	{ return this.lut[this.ops[0]].length }

	opToTable (op = this.ops[0])
	{
		var array = []
		for (var entry of this.lut[op])
			array.push (entry.join(""))
		return array.join("\n")
	}

	opToHTMLTable (op = this.ops[0])
	{
		var table = document.createElement("table")
		for (var row of this.lut[op])
		{
			var tr = document.createElement ("tr")
			for (var cell of row)
			{
				var td = document.createElement ("td")
				td.innerHTML = cell
				tr.appendChild(td)
			}
			table.appendChild(tr)
		}
		table.className = "operator"
		return table
	}
}

class Term
{
	constructor (input = [], magma = new Magma ())
	{
		this.input = input; this.lut = magma.lut; this.magma = magma
		this.progress = 0; this.stack = []
	}

	static fromString (termString = "", magma = new Magma (), separator = "")
	{ return new Term (termString.split(separator), magma) }

	lookup (currentItem = this.input [this.progress], currentLut = this.lut)
	{
		if (Array.isArray (currentLut [currentItem]))
			return this.lookup (this.stack.pop (), currentLut [currentItem])
		else if (currentItem in currentLut)
			return (currentLut [currentItem])
		else
			return (currentItem)
	}

	evaluate ()
	{
		while (this.progress < this.input.length)
		{
			this.stack.push (this.lookup())
			this.progress ++
		}
		var stack = this.stack
		this.stack = []
		return stack
	}

	isEqual ()
	{
		var stack = this.evaluate()
		var result = true
		for (var element of stack)
			result = result && stack[0] == element
		return result
	}

}

class Relation // todo: equations probably won't cut it forever
{}

class Query // checks if properties apply
{
	constructor (quantifiers = [], limit = 0, term = new Term ())
	{
		this.quantifiers = quantifiers; this.limit = limit
		this.term = term
	}

	static fromString (quantifiersString = "", limit = 0, term = new Term ())
	{ return new Query (quantifiersString.match (/.{2}/g), limit, term) }

	static fromPropertyString (propString, magma)
	{
		const lower = "abcdefghijklmnopqrstuvwxyz"
		const upper = lower.toUpperCase()
		const opSymbols = "*+/-&|%$"

		var opCount = 0
		while (propString.includes (opSymbols [opCount]))
		{
			while (propString.includes (opSymbols [opCount]))
				propString = propString.replace
				(opSymbols [opCount], magma.ops[opCount])
			opCount++
		}

		var quanifierCount = 0; var quantifiers = []

		while (quanifierCount < lower.length)
		{
			if (propString.includes (lower[quanifierCount]))
				quantifiers.push (lower[quanifierCount] + "E")
			else if (propString.includes (upper[quanifierCount]))
				quantifiers.push (upper[quanifierCount] + "A")
			else break
			quanifierCount++
		}

		return new Query
		(
			quantifiers,
			magma.getLimit (),
			Term.fromString(propString, magma, "")
		)

	}

	evaluate ()
	{

		if (this.quantifiers.length == 0)
			return this.term.isEqual ()
		else
		{
			const all_not_ex = this.quantifiers[0][1] == "A"
			const replaceThis = this.quantifiers [0][0]
			var count = 0; var result = all_not_ex

			while (count < this.limit && result == all_not_ex)
			{
				var newInput = this.term.input.map (function (variable)
				{ return variable == replaceThis ? count : variable; })
				var subQuery = new Query
				(
					this.quantifiers.slice (1),
					this.limit,
					new Term (newInput, this.term.magma)
				)
				result = subQuery.evaluate ()
				count ++
			}
			return result
		}
	}
}

class MagmaGenerator //todo: generate magmas with given properties
{}

class QueryGenerator //todo: generate properties
{}<|endoftext|>