<commit_before>import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Modal } from 'react-bootstrap';
import values from 'lodash/values';
import filter from 'lodash/filter';
import clone from 'lodash/clone';
import isEmpty from 'lodash/isEmpty';

import Errors from '../Errors.js';
import NestedSearchBar from '../NestedSearchBar';

import { surveillanceSystemsProps }from '../../prop-types/surveillance_system_props';
import { surveillanceProgramsProps } from '../../prop-types/surveillance_program_props';

// This is an abstract class that is never intended to
// be used directly.
export default class ProfileEditor extends Component {
  constructor(props) {
    super(props);
    this.programSearch = this.programSearch.bind(this);
    this.systemSearch  = this.systemSearch.bind(this);
  }

  componentWillReceiveProps(nextProps){
    var surveillanceSystems  =  values(nextProps.surveillanceSystems);
    var surveillancePrograms =  values(nextProps.surveillancePrograms);
    this.setState({surveillanceSystems:  surveillanceSystems,
      surveillancePrograms: surveillancePrograms,
      defaultProgramId: (surveillancePrograms[0] && surveillancePrograms[0].id) || -1,
      defaultSystemId:  (surveillanceSystems[0] && surveillanceSystems[0].id) || -1
    });
  }

  render() {
    return (
      <Modal animation={false} show={this.props.show}  onHide={this.props.closer} aria-label={this.title()}>
        <Modal.Header closeButton>
          <Modal.Title componentClass="h1">{this.title()}</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <form>
            <Errors errors={this.state.errors} />
            {!this.disableUserUpdate() && this.renderUserInfo() }
            {this.extraContent()}

              {this.surveillanceProgramsField()}

              {this.surveillanceSystemsField()}

          </form>
        </Modal.Body>
        <Modal.Footer>
          <button type="button" className="btn btn-default" onClick={this.props.closer}>Close</button>
          {this.actionButton()}
        </Modal.Footer>
      </Modal>
    );
  }

  disableUserUpdate(){
    return this.props.disableUserUpdate == 'true'
  }

  renderUserInfo(){
    return(

      E-mail

        <input autoFocus="autofocus" placeholder="" className="form-control input-lg" type="email"
               value={this.state.email} name="email" id="email" onChange={this.handleChange('email')} />

Please provide your E-mail

      First name
      <input className="form-control input-lg" type="text" name="firstName" id="firstName"
             value={this.state.firstName} onChange={this.handleChange('firstName')}/>

      Last name
      <input className="form-control input-lg" type="text" name="lastName" id="lastName"
             value={this.state.lastName} onChange={this.handleChange('lastName')}/>

);
  }
  programSearch(programSearchTerm){
    var surveillancePrograms = values(this.props.surveillancePrograms);
    if(programSearchTerm && programSearchTerm.length > 1){
      surveillancePrograms = filter(surveillancePrograms, (sp) => sp.name.toLowerCase().includes(programSearchTerm.toLowerCase()) || sp.id === this.state.lastProgramId || sp.id.toString() === this.state.lastProgramId);
    }
    this.setState({surveillancePrograms: surveillancePrograms, defaultProgramId: (surveillancePrograms[0] && surveillancePrograms[0].id) || -1});
  }

  systemSearch(systemSearchTerm){
    var surveillanceSystems = values(this.props.surveillanceSystems);
    if(systemSearchTerm && systemSearchTerm.length > 1){
      surveillanceSystems = filter(surveillanceSystems, (ss) => ss.name.toLowerCase().includes(systemSearchTerm.toLowerCase()) || ss.id === this.state.lastSystemId || ss.id.toString() === this.state.lastSystemId);
    }
    this.setState({surveillanceSystems: surveillanceSystems, defaultSystemId:(surveillanceSystems[0] && surveillanceSystems[0].id) || -1});
  }

  surveillanceProgramsField() {
    if (isEmpty(this.props.surveillancePrograms)) {
      return
No surveillance programs loaded in the database
;
    } else {
      return (

          Surveillance Program
          <NestedSearchBar onSearchTermChange={this.programSearch} modelName="Program" />
          <select size='5' className="form-control" name="lastProgramId" id="lastProgramId" value={this.state.lastProgramId} onChange={this.handleChange('lastProgramId')} >
            {this.state.surveillancePrograms && this.state.surveillancePrograms.map((sp) => {
              return <option key={sp.id} value={sp.id}>{sp.name}</option>;
            })}
          </select>

);
    }
  }

  surveillanceSystemsField() {
    if (isEmpty(this.props.surveillanceSystems)) {
      return
No surveillance systems loaded in the database
;
    } else {
      return (

          Surveillance System
          <NestedSearchBar onSearchTermChange={this.systemSearch} modelName="System" />
          <select size='5' className="form-control" name="lastSystemId" id="lastSystemId" value={this.state.lastSystemId} onChange={this.handleChange('lastSystemId')} >
            {this.state.surveillanceSystems && this.state.surveillanceSystems.map((ss) => {
              return <option key={ss.id} value={ss.id}>{ss.name}</option>;
            })}
          </select>

);
    }
  }

  profileInformation() {
    let profileInformation = clone(this.state);
    delete profileInformation.errors;
    if (profileInformation.lastSystemId === -1) {
      if (profileInformation.defaultSystemId !== -1){
        profileInformation.lastSystemId = profileInformation.defaultSystemId;
      } else {
        delete profileInformation.lastSystemId;
      }
    }
    if (profileInformation.lastProgramId === -1) {
      if (profileInformation.defaultProgramId !== -1){
        profileInformation.lastProgramId = profileInformation.defaultProgramId;
      } else {
        delete profileInformation.lastProgramId;
      }
    }
    return profileInformation;
  }

  handleChange(field) {
    return (event) => {
      let newState = {};
      newState[field] = event.target.value;
      this.setState(newState);
    };
  }
}

ProfileEditor.propTypes = {
  closer: PropTypes.func.isRequired,
  show: PropTypes.bool.isRequired,
  surveillanceSystems: surveillanceSystemsProps,
  surveillancePrograms: surveillanceProgramsProps
};
<commit_msg>linting fixes<commit_after>import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Modal } from 'react-bootstrap';
import values from 'lodash/values';
import filter from 'lodash/filter';
import clone from 'lodash/clone';
import isEmpty from 'lodash/isEmpty';

import Errors from '../Errors.js';
import NestedSearchBar from '../NestedSearchBar';

import { surveillanceSystemsProps }from '../../prop-types/surveillance_system_props';
import { surveillanceProgramsProps } from '../../prop-types/surveillance_program_props';

// This is an abstract class that is never intended to
// be used directly.
export default class ProfileEditor extends Component {
  constructor(props) {
    super(props);
    this.programSearch = this.programSearch.bind(this);
    this.systemSearch  = this.systemSearch.bind(this);
  }

  componentWillReceiveProps(nextProps){
    var surveillanceSystems  =  values(nextProps.surveillanceSystems);
    var surveillancePrograms =  values(nextProps.surveillancePrograms);
    this.setState({surveillanceSystems:  surveillanceSystems,
      surveillancePrograms: surveillancePrograms,
      defaultProgramId: (surveillancePrograms[0] && surveillancePrograms[0].id) || -1,
      defaultSystemId:  (surveillanceSystems[0] && surveillanceSystems[0].id) || -1
    });
  }

  render() {
    return (
      <Modal animation={false} show={this.props.show}  onHide={this.props.closer} aria-label={this.title()}>
        <Modal.Header closeButton>
          <Modal.Title componentClass="h1">{this.title()}</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <form>
            <Errors errors={this.state.errors} />
            {!this.disableUserUpdate() && this.renderUserInfo() }
            {this.extraContent()}

              {this.surveillanceProgramsField()}

              {this.surveillanceSystemsField()}

          </form>
        </Modal.Body>
        <Modal.Footer>
          <button type="button" className="btn btn-default" onClick={this.props.closer}>Close</button>
          {this.actionButton()}
        </Modal.Footer>
      </Modal>
    );
  }

  disableUserUpdate(){
    return this.props.disableUserUpdate == 'true';
  }

  renderUserInfo(){
    return(

      E-mail

        <input autoFocus="autofocus" placeholder="" className="form-control input-lg" type="email"
               value={this.state.email} name="email" id="email" onChange={this.handleChange('email')} />

Please provide your E-mail

      First name
      <input className="form-control input-lg" type="text" name="firstName" id="firstName"
             value={this.state.firstName} onChange={this.handleChange('firstName')}/>

      Last name
      <input className="form-control input-lg" type="text" name="lastName" id="lastName"
             value={this.state.lastName} onChange={this.handleChange('lastName')}/>

);
  }
  programSearch(programSearchTerm){
    var surveillancePrograms = values(this.props.surveillancePrograms);
    if(programSearchTerm && programSearchTerm.length > 1){
      surveillancePrograms = filter(surveillancePrograms, (sp) => sp.name.toLowerCase().includes(programSearchTerm.toLowerCase()) || sp.id === this.state.lastProgramId || sp.id.toString() === this.state.lastProgramId);
    }
    this.setState({surveillancePrograms: surveillancePrograms, defaultProgramId: (surveillancePrograms[0] && surveillancePrograms[0].id) || -1});
  }

  systemSearch(systemSearchTerm){
    var surveillanceSystems = values(this.props.surveillanceSystems);
    if(systemSearchTerm && systemSearchTerm.length > 1){
      surveillanceSystems = filter(surveillanceSystems, (ss) => ss.name.toLowerCase().includes(systemSearchTerm.toLowerCase()) || ss.id === this.state.lastSystemId || ss.id.toString() === this.state.lastSystemId);
    }
    this.setState({surveillanceSystems: surveillanceSystems, defaultSystemId:(surveillanceSystems[0] && surveillanceSystems[0].id) || -1});
  }

  surveillanceProgramsField() {
    if (isEmpty(this.props.surveillancePrograms)) {
      return
No surveillance programs loaded in the database
;
    } else {
      return (

          Surveillance Program
          <NestedSearchBar onSearchTermChange={this.programSearch} modelName="Program" />
          <select size='5' className="form-control" name="lastProgramId" id="lastProgramId" value={this.state.lastProgramId} onChange={this.handleChange('lastProgramId')} >
            {this.state.surveillancePrograms && this.state.surveillancePrograms.map((sp) => {
              return <option key={sp.id} value={sp.id}>{sp.name}</option>;
            })}
          </select>

);
    }
  }

  surveillanceSystemsField() {
    if (isEmpty(this.props.surveillanceSystems)) {
      return
No surveillance systems loaded in the database
;
    } else {
      return (

          Surveillance System
          <NestedSearchBar onSearchTermChange={this.systemSearch} modelName="System" />
          <select size='5' className="form-control" name="lastSystemId" id="lastSystemId" value={this.state.lastSystemId} onChange={this.handleChange('lastSystemId')} >
            {this.state.surveillanceSystems && this.state.surveillanceSystems.map((ss) => {
              return <option key={ss.id} value={ss.id}>{ss.name}</option>;
            })}
          </select>

);
    }
  }

  profileInformation() {
    let profileInformation = clone(this.state);
    delete profileInformation.errors;
    if (profileInformation.lastSystemId === -1) {
      if (profileInformation.defaultSystemId !== -1){
        profileInformation.lastSystemId = profileInformation.defaultSystemId;
      } else {
        delete profileInformation.lastSystemId;
      }
    }
    if (profileInformation.lastProgramId === -1) {
      if (profileInformation.defaultProgramId !== -1){
        profileInformation.lastProgramId = profileInformation.defaultProgramId;
      } else {
        delete profileInformation.lastProgramId;
      }
    }
    return profileInformation;
  }

  handleChange(field) {
    return (event) => {
      let newState = {};
      newState[field] = event.target.value;
      this.setState(newState);
    };
  }
}

ProfileEditor.propTypes = {
  closer: PropTypes.func.isRequired,
  show: PropTypes.bool.isRequired,
  disableUserUpdate:PropTypes.string,
  surveillanceSystems: surveillanceSystemsProps,
  surveillancePrograms: surveillanceProgramsProps
};
<|endoftext|>


<commit_before>/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
/* Teams handlers (invoked by router to render templates)                                         */
/*                                                                                */
/* All functions here either render or redirect, or throw.                                        */
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */

import Team             from '../../models/team.js';
import TeamMember       from '../../models/team-member.js';
import Db               from '../../lib/mysqldb.js';
import validationErrors from '../../lib/validation-errors.js';

class TeamsHandlers {

    /**
     * GET /teams - render list-teams page.
     *
     * Results can be filtered with URL query strings eg /teams?name=alpha.
     */
    static async list(ctx) {
        // build sql query including any query-string filters; eg ?field1=val1&field2=val2 becomes
        // "Where field1 = :field1 And field2 = :field2"
        let sql = 'Select * From Team';
        if (ctx.request.querystring) {
            const filter = Object.keys(ctx.request.query).map(q => `${q} = :${q}`).join(' and ');
            sql += ' Where '+filter;
        }
        sql +=  ' Order By Name';

        try {

            const [ teams ] = await Db.query(sql, ctx.request.query);

            await ctx.render('teams-list', { teams });

        } catch (e) {
            switch (e.code) {
                case 'ER_BAD_FIELD_ERROR': ctx.throw(403, 'Unrecognised Team field'); break;
                default: throw e;
            }
        }
    }

    /**
     * GET /teams/:id - render view-team page.
     */
    static async view(ctx) {
        // team details
        const team = await Team.get(ctx.params.id);
        if (!team) ctx.throw(404, 'Team not found');

        // team members
        const sql = `Select TeamMemberId, MemberId, Firstname, Lastname
                     From Member Inner Join Team Using (MemberId)
                     Where TeamId = :id`;
        const [ members ] = await Db.query(sql, { id: ctx.params.id });

        const context = team;
        context.members = members;
        await ctx.render('teams-view', context);
    }

    /**
     * GET /teams/add - render add-team page
     */
    static async add(ctx) {
        const context = ctx.flash.formdata || {}; // failed validation? fill in previous values
        await ctx.render('teams-add', context);
    }

    /**
     * GET /teams/:id/edit - render edit-team page.
     */
    static async edit(ctx) {
        // team details
        const team = await Team.get(ctx.params.id);
        if (!team) ctx.throw(404, 'Team not found');
        if (ctx.flash.formdata) Object.assign(team, ctx.flash.formdata); // failed validation? fill in previous values

        // team members
        const sqlT = `Select TeamMemberId, MemberId, Firstname, Lastname
                      From TeamMember Inner Join Member Using (MemberId)
                      Where TeamId = :id
                      Order By Firstname, Lastname`;
        const [ teamMembers ] = await Db.query(sqlT, { id: ctx.params.id });
        team.teamMembers = teamMembers;

        // members not in this team (for add picklist)
        let members = team.teamMembers.map(function(m) { return m.MemberId; }); // array of id's
        if (members.length == 0) members = [ 0 ]; // dummy to satisfy sql 'in' syntax
        const sqlM = `Select MemberId, Firstname, Lastname
                      From Member
                      Where MemberId Not In (`+members.join(',')+`)
                      Order By Firstname, Lastname`;
        const [ notTeamMembers ] = await Db.query(sqlM, members);
        team.notTeamMembers = notTeamMembers;

        const context = team;
        await ctx.render('teams-edit', context);
    }

    /**
     * GET /teams/:id/delete - render delete-team page.
     */
    static async delete(ctx) {
        const team = await Team.get(ctx.params.id);
        if (!team) ctx.throw(404, 'Team not found');

        const context = team;
        await ctx.render('teams-delete', context);
    }

    /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
    /* POST processing                                                                            */
    /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */

    /**
     * POST /teams/add - process add-team.
     */
    static async processAdd(ctx) {
        if (ctx.state.user.Role != 'admin') {
            ctx.flash = { _error: 'Team management requires admin privileges' };
            return ctx.response.redirect('/login'+ctx.request.url);
        }

        const body = ctx.request.body;

        try {

            const validation = { // back-end validation matching HTML5 validation
                Name: 'required',
            };

            if (validationErrors(body, validation)) {
                throw new Error(validationErrors(body, validation));
            }

            const id = await Team.insert(body);
            ctx.response.set('X-Insert-Id', id); // for integration tests

            // return to list of members
            ctx.response.redirect('/teams');

        } catch (e) {
            // stay on same page to report error (with current filled fields)
            ctx.flash = { formdata: body, _error: e.message };
            ctx.response.redirect(ctx.request.url);
        }
    }

    /**
     * POST /teams/:id/edit - process edit-team.
     */
    static async processEdit(ctx) {
        if (ctx.state.user.Role != 'admin') {
            ctx.flash = { _error: 'Team management requires admin privileges' };
            return ctx.response.redirect('/login'+ctx.request.url);
        }

        const body = ctx.request.body;

        // update team details
        if ('Name' in body) {
            try {

                const validation = { // back-end validation matching HTML5 validation
                    Name: 'required',
                };

                if (validationErrors(body, validation)) {
                    throw new Error(validationErrors(body, validation));
                }

                await Team.update(ctx.params.id, body);

                // return to list of members
                ctx.response.redirect('/teams');

            } catch (e) {
                // stay on same page to report error (with current filled fields)
                ctx.flash = { formdata: body, _error: e.message };
                ctx.response.redirect(ctx.request.url);
            }
        }

        // add member to team
        if ('add-member' in body) {
            const values = {
                TeamId:   ctx.params.id,
                MemberId: body['add-member'],
                JoinedOn: new Date().toISOString().replace('T', ' ').split('.')[0],
            };

            try {

                const id = await TeamMember.insert(values);
                ctx.response.set('X-Insert-Id', id); // for integration tests

                // stay on same page showing new team member
                ctx.response.redirect(ctx.request.url);

            } catch (e) {
                // stay on same page to report error
                ctx.flash = { formdata: body, _error: e.message };
                ctx.response.redirect(ctx.request.url);
            }
        }

        // remove member from team
        if ('del-member' in body) {
            try {

                await TeamMember.delete(body['del-member']);
                // stay on same page showing new members list
                ctx.response.redirect(ctx.request.url);

            } catch (e) {
                // stay on same page to report error
                ctx.flash = { _error: e.message };
                ctx.response.redirect(ctx.request.url);
            }
        }
    }

    /**
     * POST /teams/:id/delete - process delete-team.
     */
    static async processDelete(ctx) {
        if (ctx.state.user.Role != 'admin') {
            ctx.flash = { _error: 'Team management requires admin privileges' };
            return ctx.response.redirect('/login'+ctx.request.url);
        }

        try {

            await Team.delete(ctx.params.id);

            // return to list of teams
            ctx.response.redirect('/teams');

        } catch (e) {
            // stay on same page to report error
            ctx.flash = { _error: e.message };
            ctx.response.redirect(ctx.request.url);
        }
    }

}

/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */

export default TeamsHandlers;
<commit_msg>Fix query<commit_after>/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
/* Teams handlers (invoked by router to render templates)                                         */
/*                                                                                */
/* All functions here either render or redirect, or throw.                                        */
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */

import Team             from '../../models/team.js';
import TeamMember       from '../../models/team-member.js';
import Db               from '../../lib/mysqldb.js';
import validationErrors from '../../lib/validation-errors.js';

class TeamsHandlers {

    /**
     * GET /teams - render list-teams page.
     *
     * Results can be filtered with URL query strings eg /teams?name=alpha.
     */
    static async list(ctx) {
        // build sql query including any query-string filters; eg ?field1=val1&field2=val2 becomes
        // "Where field1 = :field1 And field2 = :field2"
        let sql = 'Select * From Team';
        if (ctx.request.querystring) {
            const filter = Object.keys(ctx.request.query).map(q => `${q} = :${q}`).join(' and ');
            sql += ' Where '+filter;
        }
        sql +=  ' Order By Name';

        try {

            const [ teams ] = await Db.query(sql, ctx.request.query);

            await ctx.render('teams-list', { teams });

        } catch (e) {
            switch (e.code) {
                case 'ER_BAD_FIELD_ERROR': ctx.throw(403, 'Unrecognised Team field'); break;
                default: throw e;
            }
        }
    }

    /**
     * GET /teams/:id - render view-team page.
     */
    static async view(ctx) {
        // team details
        const team = await Team.get(ctx.params.id);
        if (!team) ctx.throw(404, 'Team not found');

        // team members
        const sql = `Select TeamMemberId, MemberId, Firstname, Lastname
                     From Member Inner Join TeamMember Using (MemberId)
                     Where TeamId = :id`;
        const [ members ] = await Db.query(sql, { id: ctx.params.id });

        const context = team;
        context.members = members;
        await ctx.render('teams-view', context);
    }

    /**
     * GET /teams/add - render add-team page
     */
    static async add(ctx) {
        const context = ctx.flash.formdata || {}; // failed validation? fill in previous values
        await ctx.render('teams-add', context);
    }

    /**
     * GET /teams/:id/edit - render edit-team page.
     */
    static async edit(ctx) {
        // team details
        const team = await Team.get(ctx.params.id);
        if (!team) ctx.throw(404, 'Team not found');
        if (ctx.flash.formdata) Object.assign(team, ctx.flash.formdata); // failed validation? fill in previous values

        // team members
        const sqlT = `Select TeamMemberId, MemberId, Firstname, Lastname
                      From TeamMember Inner Join Member Using (MemberId)
                      Where TeamId = :id
                      Order By Firstname, Lastname`;
        const [ teamMembers ] = await Db.query(sqlT, { id: ctx.params.id });
        team.teamMembers = teamMembers;

        // members not in this team (for add picklist)
        let members = team.teamMembers.map(function(m) { return m.MemberId; }); // array of id's
        if (members.length == 0) members = [ 0 ]; // dummy to satisfy sql 'in' syntax
        const sqlM = `Select MemberId, Firstname, Lastname
                      From Member
                      Where MemberId Not In (`+members.join(',')+`)
                      Order By Firstname, Lastname`;
        const [ notTeamMembers ] = await Db.query(sqlM, members);
        team.notTeamMembers = notTeamMembers;

        const context = team;
        await ctx.render('teams-edit', context);
    }

    /**
     * GET /teams/:id/delete - render delete-team page.
     */
    static async delete(ctx) {
        const team = await Team.get(ctx.params.id);
        if (!team) ctx.throw(404, 'Team not found');

        const context = team;
        await ctx.render('teams-delete', context);
    }

    /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
    /* POST processing                                                                            */
    /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */

    /**
     * POST /teams/add - process add-team.
     */
    static async processAdd(ctx) {
        if (ctx.state.user.Role != 'admin') {
            ctx.flash = { _error: 'Team management requires admin privileges' };
            return ctx.response.redirect('/login'+ctx.request.url);
        }

        const body = ctx.request.body;

        try {

            const validation = { // back-end validation matching HTML5 validation
                Name: 'required',
            };

            if (validationErrors(body, validation)) {
                throw new Error(validationErrors(body, validation));
            }

            const id = await Team.insert(body);
            ctx.response.set('X-Insert-Id', id); // for integration tests

            // return to list of members
            ctx.response.redirect('/teams');

        } catch (e) {
            // stay on same page to report error (with current filled fields)
            ctx.flash = { formdata: body, _error: e.message };
            ctx.response.redirect(ctx.request.url);
        }
    }

    /**
     * POST /teams/:id/edit - process edit-team.
     */
    static async processEdit(ctx) {
        if (ctx.state.user.Role != 'admin') {
            ctx.flash = { _error: 'Team management requires admin privileges' };
            return ctx.response.redirect('/login'+ctx.request.url);
        }

        const body = ctx.request.body;

        // update team details
        if ('Name' in body) {
            try {

                const validation = { // back-end validation matching HTML5 validation
                    Name: 'required',
                };

                if (validationErrors(body, validation)) {
                    throw new Error(validationErrors(body, validation));
                }

                await Team.update(ctx.params.id, body);

                // return to list of members
                ctx.response.redirect('/teams');

            } catch (e) {
                // stay on same page to report error (with current filled fields)
                ctx.flash = { formdata: body, _error: e.message };
                ctx.response.redirect(ctx.request.url);
            }
        }

        // add member to team
        if ('add-member' in body) {
            const values = {
                TeamId:   ctx.params.id,
                MemberId: body['add-member'],
                JoinedOn: new Date().toISOString().replace('T', ' ').split('.')[0],
            };

            try {

                const id = await TeamMember.insert(values);
                ctx.response.set('X-Insert-Id', id); // for integration tests

                // stay on same page showing new team member
                ctx.response.redirect(ctx.request.url);

            } catch (e) {
                // stay on same page to report error
                ctx.flash = { formdata: body, _error: e.message };
                ctx.response.redirect(ctx.request.url);
            }
        }

        // remove member from team
        if ('del-member' in body) {
            try {

                await TeamMember.delete(body['del-member']);
                // stay on same page showing new members list
                ctx.response.redirect(ctx.request.url);

            } catch (e) {
                // stay on same page to report error
                ctx.flash = { _error: e.message };
                ctx.response.redirect(ctx.request.url);
            }
        }
    }

    /**
     * POST /teams/:id/delete - process delete-team.
     */
    static async processDelete(ctx) {
        if (ctx.state.user.Role != 'admin') {
            ctx.flash = { _error: 'Team management requires admin privileges' };
            return ctx.response.redirect('/login'+ctx.request.url);
        }

        try {

            await Team.delete(ctx.params.id);

            // return to list of teams
            ctx.response.redirect('/teams');

        } catch (e) {
            // stay on same page to report error
            ctx.flash = { _error: e.message };
            ctx.response.redirect(ctx.request.url);
        }
    }

}

/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */

export default TeamsHandlers;
<|endoftext|>


<commit_before>/*
 * Copyright (C) 2021 - present Instructure, Inc.
 *
 * This file is part of Canvas.
 *
 * Canvas is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License as published by the Free
 * Software Foundation, version 3 of the License.
 *
 * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Affero General Public License along
 * with this program. If not, see <http://www.gnu.org/licenses/>.
 */

import {Alert} from '../../components/Alert/Alert'
import {AlertManagerContext} from '@canvas/alerts/react/AlertManager'
import {Button} from '@instructure/ui-buttons'
import DateHelper from '../../../../../shared/datetime/dateHelper'
import DirectShareUserModal from '../../../../../shared/direct-sharing/react/components/DirectShareUserModal'
import DirectShareCourseTray from '../../../../../shared/direct-sharing/react/components/DirectShareCourseTray'
import {Discussion} from '../../../graphql/Discussion'
import {DiscussionPostToolbar} from '../../components/DiscussionPostToolbar/DiscussionPostToolbar'
import {DiscussionEdit} from '../../components/DiscussionEdit/DiscussionEdit'
import {Flex} from '@instructure/ui-flex'
import I18n from 'i18n!discussion_posts'
import {PostMessage} from '../../components/PostMessage/PostMessage'
import {PostToolbar} from '../../components/PostToolbar/PostToolbar'
import {
  PUBLISH_DISCUSSION_TOPIC,
  SUBSCRIBE_TO_DISCUSSION_TOPIC,
  DELETE_DISCUSSION_TOPIC
} from '../../../graphql/Mutations'
import React, {useContext, useState} from 'react'
import {useMutation} from 'react-apollo'
import {isGraded, getSpeedGraderUrl, getEditUrl} from '../../utils'
import {View} from '@instructure/ui-view'

export const DiscussionTopicContainer = props => {
  const {setOnFailure, setOnSuccess} = useContext(AlertManagerContext)
  const [sendToOpen, setSendToOpen] = useState(false)
  const [copyToOpen, setCopyToOpen] = useState(false)
  const [expandedReply, setExpandedReply] = useState(false)

  const discussionTopicData = {
    _id: props.discussionTopic._id,
    authorName: props.discussionTopic?.author?.name || '',
    avatarUrl: props.discussionTopic?.author?.avatarUrl || '',
    message: props.discussionTopic?.message || '',
    permissions: props.discussionTopic?.permissions || {},
    postedAt: DateHelper.formatDatetimeForDiscussions(props.discussionTopic?.postedAt),
    published: props.discussionTopic?.published || false,
    subscribed: props.discussionTopic?.subscribed || false,
    title: props.discussionTopic?.title || '',
    unread: props.discussionTopic?.entryCounts?.unreadCount,
    replies: props.discussionTopic?.entryCounts?.repliesCount,
    assignment: props.discussionTopic?.assignment
  }

  // TODO: Change this to the new canGrade permission.
  const canGrade =
    (isGraded(discussionTopicData.assignment) && discussionTopicData?.permissions?.update) || false
  const canDelete = discussionTopicData?.permissions?.delete || false
  const canReadAsAdmin = !!discussionTopicData?.permissions?.readAsAdmin || false
  const canUpdate = discussionTopicData?.permissions?.update || false
  const canUnpublish = props.discussionTopic.canUnpublish || false

  if (isGraded(discussionTopicData.assignment)) {
    discussionTopicData.dueAt = DateHelper.formatDatetimeForDiscussions(
      props.discussionTopic.assignment.dueAt
    )
    discussionTopicData.pointsPossible = props.discussionTopic.assignment.pointsPossible || 0
  }

  const [deleteDiscussionTopic] = useMutation(DELETE_DISCUSSION_TOPIC, {
    onCompleted: () => {
      setOnSuccess(I18n.t('The discussion topic was successfully deleted.'))
      window.location.assign(`/courses/${ENV.course_id}/discussion_topics`)
    },
    onError: () => {
      setOnFailure(I18n.t('There was an unexpected error deleting the discussion topic.'))
    }
  })

  const course = {
    id: ENV?.context_asset_string
      ? ENV.context_asset_string.split('_')[0] === 'course'
        ? ENV.context_asset_string.split('_')[1]
        : null
      : null
  }

  const directShareUserModalProps = {
    open: sendToOpen,
    courseId: course.id,
    contentShare: {content_type: 'discussion_topic', content_id: props.discussionTopic._id},
    onDismiss: () => {
      setSendToOpen(false)
    }
  }

  const directShareCourseTrayProps = {
    open: copyToOpen,
    sourceCourseId: course.id,
    contentSelection: {discussion_topics: [props.discussionTopic._id]},
    onDismiss: () => {
      setCopyToOpen(false)
    }
  }

  const [publishDiscussionTopic] = useMutation(PUBLISH_DISCUSSION_TOPIC, {
    onCompleted: data => {
      if (!data.updateDiscussionTopic.errors) {
        setOnSuccess(
          data.updateDiscussionTopic.discussionTopic.published
            ? I18n.t('You have successfully published the discussion topic.')
            : I18n.t('You have successfully unpublished the discussion topic.')
        )
      } else {
        setOnFailure(I18n.t('There was an unexpected error updating the discussion topic.'))
      }
    },
    onError: () => {
      setOnFailure(I18n.t('There was an unexpected error updating the discussion topic.'))
    }
  })

  const onPublish = () => {
    publishDiscussionTopic({
      variables: {
        discussionTopicId: discussionTopicData._id,
        published: !discussionTopicData.published
      }
    })
  }

  const [subscribeToDiscussionTopic] = useMutation(SUBSCRIBE_TO_DISCUSSION_TOPIC, {
    onCompleted: data => {
      if (!data.subscribeToDiscussionTopic.errors) {
        setOnSuccess(
          data.subscribeToDiscussionTopic.discussionTopic.subscribed
            ? I18n.t('You have successfully subscribed to the discussion topic.')
            : I18n.t('You have successfully unsubscribed from the discussion topic.')
        )
      } else {
        setOnFailure(I18n.t('There was an unexpected error updating the discussion topic.'))
      }
    },
    onError: () => {
      setOnFailure(I18n.t('There was an unexpected error updating the discussion topic.'))
    }
  })

  const onSubscribe = () => {
    subscribeToDiscussionTopic({
      variables: {
        discussionTopicId: discussionTopicData._id,
        subscribed: !discussionTopicData.subscribed
      }
    })
  }

  return (
    <>
      <Flex as="div" direction="column">
        <Flex.Item margin="0 0 large" overflowY="hidden" overflowX="hidden">
          <DiscussionPostToolbar
            selectedView="all"
            sortDirection="asc"
            isCollapsedReplies
            onSearchChange={() => {}}
            onViewFilter={() => {}}
            onSortClick={() => {}}
            onCollapseRepliesToggle={() => {}}
            onTopClick={() => {}}
          />
        </Flex.Item>
        <Flex.Item>
          <View
            as="div"
            borderWidth="small"
            borderRadius="medium"
            borderStyle="solid"
            borderColor="primary"
          >
            {isGraded(discussionTopicData.assignment) && (
              <View as="div" padding="none medium none">
                <Alert
                  contextDisplayText="Section 2"
                  dueAtDisplayText={discussionTopicData.dueAt}
                  pointsPossible={discussionTopicData.pointsPossible}
                />
              </View>
            )}
            <Flex direction="column">
              <Flex.Item>
                <Flex
                  direction="row"
                  justifyItems="space-between"
                  padding="medium small none"
                  alignItems="start"
                >
                  <Flex.Item shouldShrink shouldGrow>
                    <PostMessage
                      authorName={discussionTopicData.authorName}
                      avatarUrl={discussionTopicData.avatarUrl}
                      pillText={I18n.t('Author')}
                      timingDisplay={discussionTopicData.postedAt}
                      message={discussionTopicData.message}
                    >
                      <Button
                        color="primary"
                        onClick={() => {
                          setExpandedReply(true)
                        }}
                        data-testid="discussion-topic-reply"
                      >
                        {I18n.t('Reply')}
                      </Button>
                    </PostMessage>
                  </Flex.Item>
                  <Flex.Item>
                    <PostToolbar
                      onToggleComments={canReadAsAdmin ? () => {} : null}
                      onDelete={
                        canDelete
                          ? () => {
                              if (
                                // eslint-disable-next-line no-alert
                                window.confirm(
                                  I18n.t('Are you sure you want to delete this topic?')
                                )
                              ) {
                                deleteDiscussionTopic({
                                  variables: {
                                    id: discussionTopicData._id
                                  }
                                })
                              }
                            }
                          : null
                      }
                      repliesCount={discussionTopicData.replies}
                      unreadCount={discussionTopicData.unread}
                      onSend={
                        canReadAsAdmin
                          ? () => {
                              setSendToOpen(true)
                            }
                          : null
                      }
                      onCopy={
                        canReadAsAdmin
                          ? () => {
                              setCopyToOpen(true)
                            }
                          : null
                      }
                      onEdit={
                        canReadAsAdmin
                          ? () => {
                              window.location.assign(
                                getEditUrl(ENV.course_id, discussionTopicData._id)
                              )
                            }
                          : null
                      }
                      onTogglePublish={canReadAsAdmin && canUpdate ? onPublish : null}
                      onToggleSubscription={onSubscribe}
                      onOpenSpeedgrader={
                        canGrade
                          ? () => {
                              window.location.assign(
                                getSpeedGraderUrl(ENV.course_id, discussionTopicData.assignment._id)
                              )
                            }
                          : null
                      }
                      isPublished={discussionTopicData.published}
                      canUnpublish={canUnpublish}
                      isSubscribed={discussionTopicData.subscribed}
                      commentsEnabled
                    />
                  </Flex.Item>
                </Flex>
              </Flex.Item>
              <Flex.Item
                shouldShrink
                shouldGrow
                padding={
                  expandedReply ? 'none medium medium xx-large' : 'none medium none xx-large'
                }
                overflowX="hidden"
                overflowY="hidden"
              >
                <DiscussionEdit
                  show={expandedReply}
                  onCancel={() => {
                    setExpandedReply(false)
                  }}
                  onSubmit={() => {}}
                />
              </Flex.Item>
            </Flex>
          </View>
        </Flex.Item>
      </Flex>
      <DirectShareUserModal {...directShareUserModalProps} />
      <DirectShareCourseTray {...directShareCourseTrayProps} />
    </>
  )
}

DiscussionTopicContainer.propTypes = {
  /**
   * Indicates if this Discussion Topic is graded.
   * Providing this property will result in the graded info
   * to be rendered
   */
  discussionTopic: Discussion.shape.isRequired
}

export default DiscussionTopicContainer
<commit_msg>make discussion post toolbar sticky<commit_after>/*
 * Copyright (C) 2021 - present Instructure, Inc.
 *
 * This file is part of Canvas.
 *
 * Canvas is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License as published by the Free
 * Software Foundation, version 3 of the License.
 *
 * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Affero General Public License along
 * with this program. If not, see <http://www.gnu.org/licenses/>.
 */

import {Alert} from '../../components/Alert/Alert'
import {AlertManagerContext} from '@canvas/alerts/react/AlertManager'
import {Button} from '@instructure/ui-buttons'
import DateHelper from '../../../../../shared/datetime/dateHelper'
import DirectShareUserModal from '../../../../../shared/direct-sharing/react/components/DirectShareUserModal'
import DirectShareCourseTray from '../../../../../shared/direct-sharing/react/components/DirectShareCourseTray'
import {Discussion} from '../../../graphql/Discussion'
import {DiscussionPostToolbar} from '../../components/DiscussionPostToolbar/DiscussionPostToolbar'
import {DiscussionEdit} from '../../components/DiscussionEdit/DiscussionEdit'
import {Flex} from '@instructure/ui-flex'
import I18n from 'i18n!discussion_posts'
import {PostMessage} from '../../components/PostMessage/PostMessage'
import {PostToolbar} from '../../components/PostToolbar/PostToolbar'
import {
  PUBLISH_DISCUSSION_TOPIC,
  SUBSCRIBE_TO_DISCUSSION_TOPIC,
  DELETE_DISCUSSION_TOPIC
} from '../../../graphql/Mutations'
import React, {useContext, useState} from 'react'
import {useMutation} from 'react-apollo'
import {isGraded, getSpeedGraderUrl, getEditUrl} from '../../utils'
import {View} from '@instructure/ui-view'

export const DiscussionTopicContainer = props => {
  const {setOnFailure, setOnSuccess} = useContext(AlertManagerContext)
  const [sendToOpen, setSendToOpen] = useState(false)
  const [copyToOpen, setCopyToOpen] = useState(false)
  const [expandedReply, setExpandedReply] = useState(false)

  const discussionTopicData = {
    _id: props.discussionTopic._id,
    authorName: props.discussionTopic?.author?.name || '',
    avatarUrl: props.discussionTopic?.author?.avatarUrl || '',
    message: props.discussionTopic?.message || '',
    permissions: props.discussionTopic?.permissions || {},
    postedAt: DateHelper.formatDatetimeForDiscussions(props.discussionTopic?.postedAt),
    published: props.discussionTopic?.published || false,
    subscribed: props.discussionTopic?.subscribed || false,
    title: props.discussionTopic?.title || '',
    unread: props.discussionTopic?.entryCounts?.unreadCount,
    replies: props.discussionTopic?.entryCounts?.repliesCount,
    assignment: props.discussionTopic?.assignment
  }

  // TODO: Change this to the new canGrade permission.
  const canGrade =
    (isGraded(discussionTopicData.assignment) && discussionTopicData?.permissions?.update) || false
  const canDelete = discussionTopicData?.permissions?.delete || false
  const canReadAsAdmin = !!discussionTopicData?.permissions?.readAsAdmin || false
  const canUpdate = discussionTopicData?.permissions?.update || false
  const canUnpublish = props.discussionTopic.canUnpublish || false

  if (isGraded(discussionTopicData.assignment)) {
    discussionTopicData.dueAt = DateHelper.formatDatetimeForDiscussions(
      props.discussionTopic.assignment.dueAt
    )
    discussionTopicData.pointsPossible = props.discussionTopic.assignment.pointsPossible || 0
  }

  const [deleteDiscussionTopic] = useMutation(DELETE_DISCUSSION_TOPIC, {
    onCompleted: () => {
      setOnSuccess(I18n.t('The discussion topic was successfully deleted.'))
      window.location.assign(`/courses/${ENV.course_id}/discussion_topics`)
    },
    onError: () => {
      setOnFailure(I18n.t('There was an unexpected error deleting the discussion topic.'))
    }
  })

  const course = {
    id: ENV?.context_asset_string
      ? ENV.context_asset_string.split('_')[0] === 'course'
        ? ENV.context_asset_string.split('_')[1]
        : null
      : null
  }

  const directShareUserModalProps = {
    open: sendToOpen,
    courseId: course.id,
    contentShare: {content_type: 'discussion_topic', content_id: props.discussionTopic._id},
    onDismiss: () => {
      setSendToOpen(false)
    }
  }

  const directShareCourseTrayProps = {
    open: copyToOpen,
    sourceCourseId: course.id,
    contentSelection: {discussion_topics: [props.discussionTopic._id]},
    onDismiss: () => {
      setCopyToOpen(false)
    }
  }

  const [publishDiscussionTopic] = useMutation(PUBLISH_DISCUSSION_TOPIC, {
    onCompleted: data => {
      if (!data.updateDiscussionTopic.errors) {
        setOnSuccess(
          data.updateDiscussionTopic.discussionTopic.published
            ? I18n.t('You have successfully published the discussion topic.')
            : I18n.t('You have successfully unpublished the discussion topic.')
        )
      } else {
        setOnFailure(I18n.t('There was an unexpected error updating the discussion topic.'))
      }
    },
    onError: () => {
      setOnFailure(I18n.t('There was an unexpected error updating the discussion topic.'))
    }
  })

  const onPublish = () => {
    publishDiscussionTopic({
      variables: {
        discussionTopicId: discussionTopicData._id,
        published: !discussionTopicData.published
      }
    })
  }

  const [subscribeToDiscussionTopic] = useMutation(SUBSCRIBE_TO_DISCUSSION_TOPIC, {
    onCompleted: data => {
      if (!data.subscribeToDiscussionTopic.errors) {
        setOnSuccess(
          data.subscribeToDiscussionTopic.discussionTopic.subscribed
            ? I18n.t('You have successfully subscribed to the discussion topic.')
            : I18n.t('You have successfully unsubscribed from the discussion topic.')
        )
      } else {
        setOnFailure(I18n.t('There was an unexpected error updating the discussion topic.'))
      }
    },
    onError: () => {
      setOnFailure(I18n.t('There was an unexpected error updating the discussion topic.'))
    }
  })

  const onSubscribe = () => {
    subscribeToDiscussionTopic({
      variables: {
        discussionTopicId: discussionTopicData._id,
        subscribed: !discussionTopicData.subscribed
      }
    })
  }

  return (
    <>
      <div style={{position: 'sticky', top: 0, zIndex: 10, marginTop: '-24px'}}>
        <View as="div" padding="medium 0" background="primary">
          <DiscussionPostToolbar
            selectedView="all"
            sortDirection="asc"
            isCollapsedReplies
            onSearchChange={() => {}}
            onViewFilter={() => {}}
            onSortClick={() => {}}
            onCollapseRepliesToggle={() => {}}
            onTopClick={() => {}}
          />
        </View>
      </div>
      <Flex as="div" direction="column">
        <Flex.Item>
          <View
            as="div"
            borderWidth="small"
            borderRadius="medium"
            borderStyle="solid"
            borderColor="primary"
          >
            {isGraded(discussionTopicData.assignment) && (
              <View as="div" padding="none medium none">
                <Alert
                  contextDisplayText="Section 2"
                  dueAtDisplayText={discussionTopicData.dueAt}
                  pointsPossible={discussionTopicData.pointsPossible}
                />
              </View>
            )}
            <Flex direction="column">
              <Flex.Item>
                <Flex
                  direction="row"
                  justifyItems="space-between"
                  padding="medium small none"
                  alignItems="start"
                >
                  <Flex.Item shouldShrink shouldGrow>
                    <PostMessage
                      authorName={discussionTopicData.authorName}
                      avatarUrl={discussionTopicData.avatarUrl}
                      pillText={I18n.t('Author')}
                      timingDisplay={discussionTopicData.postedAt}
                      message={discussionTopicData.message}
                    >
                      <Button
                        color="primary"
                        onClick={() => {
                          setExpandedReply(true)
                        }}
                        data-testid="discussion-topic-reply"
                      >
                        {I18n.t('Reply')}
                      </Button>
                    </PostMessage>
                  </Flex.Item>
                  <Flex.Item>
                    <PostToolbar
                      onToggleComments={canReadAsAdmin ? () => {} : null}
                      onDelete={
                        canDelete
                          ? () => {
                              if (
                                // eslint-disable-next-line no-alert
                                window.confirm(
                                  I18n.t('Are you sure you want to delete this topic?')
                                )
                              ) {
                                deleteDiscussionTopic({
                                  variables: {
                                    id: discussionTopicData._id
                                  }
                                })
                              }
                            }
                          : null
                      }
                      repliesCount={discussionTopicData.replies}
                      unreadCount={discussionTopicData.unread}
                      onSend={
                        canReadAsAdmin
                          ? () => {
                              setSendToOpen(true)
                            }
                          : null
                      }
                      onCopy={
                        canReadAsAdmin
                          ? () => {
                              setCopyToOpen(true)
                            }
                          : null
                      }
                      onEdit={
                        canReadAsAdmin
                          ? () => {
                              window.location.assign(
                                getEditUrl(ENV.course_id, discussionTopicData._id)
                              )
                            }
                          : null
                      }
                      onTogglePublish={canReadAsAdmin && canUpdate ? onPublish : null}
                      onToggleSubscription={onSubscribe}
                      onOpenSpeedgrader={
                        canGrade
                          ? () => {
                              window.location.assign(
                                getSpeedGraderUrl(ENV.course_id, discussionTopicData.assignment._id)
                              )
                            }
                          : null
                      }
                      isPublished={discussionTopicData.published}
                      canUnpublish={canUnpublish}
                      isSubscribed={discussionTopicData.subscribed}
                      commentsEnabled
                    />
                  </Flex.Item>
                </Flex>
              </Flex.Item>
              <Flex.Item
                shouldShrink
                shouldGrow
                padding={
                  expandedReply ? 'none medium medium xx-large' : 'none medium none xx-large'
                }
                overflowX="hidden"
                overflowY="hidden"
              >
                <DiscussionEdit
                  show={expandedReply}
                  onCancel={() => {
                    setExpandedReply(false)
                  }}
                  onSubmit={() => {}}
                />
              </Flex.Item>
            </Flex>
          </View>
        </Flex.Item>
      </Flex>
      <DirectShareUserModal {...directShareUserModalProps} />
      <DirectShareCourseTray {...directShareCourseTrayProps} />
    </>
  )
}

DiscussionTopicContainer.propTypes = {
  /**
   * Indicates if this Discussion Topic is graded.
   * Providing this property will result in the graded info
   * to be rendered
   */
  discussionTopic: Discussion.shape.isRequired
}

export default DiscussionTopicContainer
<|endoftext|>


<commit_before>import React from 'react';
import SPELLS from 'common/SPELLS';
import SpellLink from 'common/SpellLink';
import SpellIcon from 'common/SpellIcon';
import { formatPercentage, formatThousands } from 'common/format';
import Analyzer from 'Parser/Core/Analyzer';
import Combatants from 'Parser/Core/Modules/Combatants';
import StatisticBox, { STATISTIC_ORDER } from 'Main/StatisticBox';

const debug = false;

class IronSkinBrew extends Analyzer {
  static dependencies = {
    combatants: Combatants,
  }

  lastIronSkinBrewBuffApplied = 0;

  hitsWithIronSkinBrew = 0;
  damageWithIronSkinBrew = 0;
  hitsWithoutIronSkinBrew = 0;
  damageWithoutIronSkinBrew = 0;

  // duration tracking to record clipping of buff
  lastDurationCheck = 0;
  totalDuration = 0;
  durationLost = 0;
  currentDuration = 0;
  durationPerCast = 6000; // base
  durationPerPurify = 0;
  durationCap = -1;

  on_initialized() {
    this.durationPerCast += 500 * this.combatants.selected.traitsBySpellId[SPELLS.POTENT_KICK.id];
    this.durationCap = 3 * this.durationPerCast;
    this.durationPerPurify = 1000 * this.combatants.selected.traitsBySpellId[SPELLS.QUICK_SIP.id];
  }

  on_byPlayer_cast(event) {
    const spellId = event.ability.guid;
    if (SPELLS.IRONSKIN_BREW.id === spellId || SPELLS.PURIFYING_BREW.id === spellId) {
      // determine the current duration on ISB
      if (this.currentDuration > 0) {
        this.currentDuration -= event.timestamp - this.lastDurationCheck;
        this.currentDuration = Math.max(this.currentDuration, 0);
      }
      // add the duration from this buff application (?)
      let addedDuration = 0;
      if (SPELLS.IRONSKIN_BREW.id === spellId) {
        addedDuration = this.durationPerCast;
      } else if (SPELLS.PURIFYING_BREW.id === spellId) {
        addedDuration = this.durationPerPurify;
      }
      this.currentDuration += addedDuration;
      if (this.currentDuration > this.durationCap) {
        this.durationLost += this.currentDuration - this.durationCap;
        this.currentDuration = this.durationCap;
      }
      // add this duration to the total duration
      this.totalDuration += addedDuration;
      this.lastDurationCheck = event.timestamp;
    }
  }

  on_byPlayer_applybuff(event) {
    const spellId = event.ability.guid;
    if (SPELLS.IRONSKIN_BREW_BUFF.id === spellId) {
      this.lastIronSkinBrewBuffApplied = event.timestamp;
    }
  }

  on_byPlayer_removebuff(event) {
    const spellId = event.ability.guid;
    if (SPELLS.IRONSKIN_BREW_BUFF.id === spellId) {
      this.lastIronSkinBrewBuffApplied = 0;
      this.currentDuration = 0;
    }
  }

  on_toPlayer_damage(event) {
    if (event.ability.guid !== SPELLS.STAGGER_TAKEN.id) {
      if (this.lastIronSkinBrewBuffApplied > 0) {
        this.hitsWithIronSkinBrew += 1;
        this.damageWithIronSkinBrew += event.amount + (event.absorbed || 0) + (event.overkill || 0);
      } else {
        this.hitsWithoutIronSkinBrew += 1;
        this.damageWithoutIronSkinBrew += event.amount + (event.absorbed || 0) + (event.overkill || 0);
      }
    }
  }

  on_toPlayer_absorbed(event) {
    if (event.ability.guid === SPELLS.STAGGER.id) {
      if (this.lastIronSkinBrewBuffApplied > 0) {
        this.damageWithIronSkinBrew -= event.amount;
      } else {
        this.damageWithoutIronSkinBrew -= event.amount;
      }
    }
  }

  on_finished() {
    if (debug) {
      console.log(`Hits with IronSkinBrew ${this.hitsWithIronSkinBrew}`);
      console.log(`Damage with IronSkinBrew ${this.damageWithIronSkinBrew}`);
      console.log(`Hits without IronSkinBrew ${this.hitsWithoutIronSkinBrew}`);
      console.log(`Damage without IronSkinBrew ${this.damageWithoutIronSkinBrew}`);
      console.log(`Total damage ${this.damageWithIronSkinBrew + this.damageWithoutIronSkinBrew + this.staggerDot}`);
    }
  }

  suggestions(when) {
    const isbUptimePercentage = this.combatants.selected.getBuffUptime(SPELLS.IRONSKIN_BREW_BUFF.id) / this.owner.fightDuration;

    when(isbUptimePercentage).isLessThan(0.9)
      .addSuggestion((suggest, actual, recommended) => {
        return suggest( Your <SpellLink id={SPELLS.IRONSKIN_BREW.id} /> uptime was {formatPercentage(isbUptimePercentage)}%, unless there are extended periods of downtime it should be near 100%.)
          .icon(SPELLS.IRONSKIN_BREW.icon)
          .actual(`${formatPercentage(actual)}% uptime`)
          .recommended(`${Math.round(formatPercentage(recommended))}% or more is recommended`)
          .regular(recommended - 0.1).major(recommended - 0.2);
      });
  }

  get suggestionThreshold() {
    return {
      actual: this.hitsWithIronSkinBrew / (this.hitsWithIronSkinBrew + this.hitsWithoutIronSkinBrew),
      isLessThan: {
        minor: 0.99,
        average: 0.98,
        major: 0.95,
      },
      style: 'percentage',
    };
  }

  get clipSuggestionThreshold() {
    return {
      actual: this.durationLost / this.totalDuration,
      isGreaterThan: {
        minor: 0.02,
        average: 0.05,
        major: 0.1,
      },
      style: 'percentage',
    };
  }

  statistic() {
    const isbUptime = this.combatants.selected.getBuffUptime(SPELLS.IRONSKIN_BREW_BUFF.id) / this.owner.fightDuration;
    const hitsMitigatedPercent = this.hitsWithIronSkinBrew / (this.hitsWithIronSkinBrew + this.hitsWithoutIronSkinBrew);
    return (
      <StatisticBox
        icon={<SpellIcon id={SPELLS.IRONSKIN_BREW.id} />}
        value={`${formatPercentage(isbUptime)}%`}
        label="Ironskin Brew uptime"
        tooltip={`Ironskin Brew breakdown (these values are direct damage and does not include damage added to stagger):

You were hit ${this.hitsWithIronSkinBrew} times with your Ironskin Brew buff (${formatThousands(this.damageWithIronSkinBrew)} damage).

You were hit ${this.hitsWithoutIronSkinBrew} times without your Ironskin Brew buff (${formatThousands(this.damageWithoutIronSkinBrew)} damage).

            ${formatPercentage(hitsMitigatedPercent)}% of attacks were mitigated with Ironskin Brew.`}
          />
    );
  }
  statisticOrder = STATISTIC_ORDER.OPTIONAL();
}

export default IronSkinBrew;
<commit_msg>Hacked around the WCL bug with 100% ISB uptime<commit_after>import React from 'react';
import SPELLS from 'common/SPELLS';
import SpellLink from 'common/SpellLink';
import SpellIcon from 'common/SpellIcon';
import { formatPercentage, formatThousands } from 'common/format';
import Analyzer from 'Parser/Core/Analyzer';
import Combatants from 'Parser/Core/Modules/Combatants';
import StatisticBox, { STATISTIC_ORDER } from 'Main/StatisticBox';

const debug = false;

class IronSkinBrew extends Analyzer {
  static dependencies = {
    combatants: Combatants,
  }

  lastIronSkinBrewBuffApplied = 0;

  hitsWithIronSkinBrew = 0;
  damageWithIronSkinBrew = 0;
  hitsWithoutIronSkinBrew = 0;
  damageWithoutIronSkinBrew = 0;

  // duration tracking to record clipping of buff
  lastDurationCheck = 0;
  totalDuration = 0;
  durationLost = 0;
  currentDuration = 0;
  durationPerCast = 6000; // base
  durationPerPurify = 0;
  durationCap = -1;

  buffShouldBeApplied = false;
  buffNeverApplied = true;

  on_initialized() {
    this.durationPerCast += 500 * this.combatants.selected.traitsBySpellId[SPELLS.POTENT_KICK.id];
    this.durationCap = 3 * this.durationPerCast;
    this.durationPerPurify = 1000 * this.combatants.selected.traitsBySpellId[SPELLS.QUICK_SIP.id];
  }

  on_byPlayer_cast(event) {
    const spellId = event.ability.guid;
    if (SPELLS.IRONSKIN_BREW.id === spellId || SPELLS.PURIFYING_BREW.id === spellId) {
      // determine the current duration on ISB
      if (this.currentDuration > 0) {
        this.currentDuration -= event.timestamp - this.lastDurationCheck;
        this.currentDuration = Math.max(this.currentDuration, 0);
      }
      // add the duration from this buff application (?)
      let addedDuration = 0;
      if (SPELLS.IRONSKIN_BREW.id === spellId) {
        this.buffShouldBeApplied = true;
        addedDuration = this.durationPerCast;
      } else if (SPELLS.PURIFYING_BREW.id === spellId) {
        addedDuration = this.durationPerPurify;
      }
      this.currentDuration += addedDuration;
      if (this.currentDuration > this.durationCap) {
        this.durationLost += this.currentDuration - this.durationCap;
        this.currentDuration = this.durationCap;
      }
      // add this duration to the total duration
      this.totalDuration += addedDuration;
      this.lastDurationCheck = event.timestamp;
    }
  }

  on_byPlayer_applybuff(event) {
    const spellId = event.ability.guid;
    if (SPELLS.IRONSKIN_BREW_BUFF.id === spellId) {
      this.buffNeverApplied = false;
      this.lastIronSkinBrewBuffApplied = event.timestamp;
    }
  }

  on_byPlayer_refreshbuff(event) {
    const spellId = event.ability.guid;
    if (SPELLS.IRONSKIN_BREW_BUFF.id === spellId) {
      this.buffNeverApplied = false;
    }
  }

  on_byPlayer_removebuff(event) {
    const spellId = event.ability.guid;
    if (SPELLS.IRONSKIN_BREW_BUFF.id === spellId) {
      this.lastIronSkinBrewBuffApplied = 0;
      this.currentDuration = 0;
    }
  }

  on_toPlayer_damage(event) {
    if (event.ability.guid !== SPELLS.STAGGER_TAKEN.id) {
      if (this.lastIronSkinBrewBuffApplied > 0) {
        this.hitsWithIronSkinBrew += 1;
        this.damageWithIronSkinBrew += event.amount + (event.absorbed || 0) + (event.overkill || 0);
      } else {
        this.hitsWithoutIronSkinBrew += 1;
        this.damageWithoutIronSkinBrew += event.amount + (event.absorbed || 0) + (event.overkill || 0);
      }
    }
  }

  on_toPlayer_absorbed(event) {
    if (event.ability.guid === SPELLS.STAGGER.id) {
      if (this.lastIronSkinBrewBuffApplied > 0) {
        this.damageWithIronSkinBrew -= event.amount;
      } else {
        this.damageWithoutIronSkinBrew -= event.amount;
      }
    }
  }

  // returns true if we hit the WCL bug with 100%/0% uptime on ISB
  get wclUptimeBug() {
    return this.buffShouldBeApplied && this.buffNeverApplied;
  }

  on_finished() {
    if (this.wclUptimeBug) {
      // WCL bug: if we start the fight with ISB up and never let it
      // drop ever, it is never shown as applied
      this.hitsWithIronSkinBrew = this.hitsWithoutIronSkinBrew;
      this.hitsWithoutIronSkinBrew = 0;
      this.damageWithIronSkinBrew = this.damageWithoutIronSkinBrew;
      this.damageWithoutIronSkinBrew = 0;
    }
    if (debug) {
      console.log(`Hits with IronSkinBrew ${this.hitsWithIronSkinBrew}`);
      console.log(`Damage with IronSkinBrew ${this.damageWithIronSkinBrew}`);
      console.log(`Hits without IronSkinBrew ${this.hitsWithoutIronSkinBrew}`);
      console.log(`Damage without IronSkinBrew ${this.damageWithoutIronSkinBrew}`);
      console.log(`Total damage ${this.damageWithIronSkinBrew + this.damageWithoutIronSkinBrew + this.staggerDot}`);
    }
  }

  suggestions(when) {
    const isbUptimePercentage = this.combatants.selected.getBuffUptime(SPELLS.IRONSKIN_BREW_BUFF.id) / this.owner.fightDuration;

    when(isbUptimePercentage).isLessThan(0.9)
      .addSuggestion((suggest, actual, recommended) => {
        return suggest( Your <SpellLink id={SPELLS.IRONSKIN_BREW.id} /> uptime was {formatPercentage(isbUptimePercentage)}%, unless there are extended periods of downtime it should be near 100%.)
          .icon(SPELLS.IRONSKIN_BREW.icon)
          .actual(`${formatPercentage(actual)}% uptime`)
          .recommended(`${Math.round(formatPercentage(recommended))}% or more is recommended`)
          .regular(recommended - 0.1).major(recommended - 0.2);
      });
  }

  get suggestionThreshold() {
    return {
      actual: this.hitsWithIronSkinBrew / (this.hitsWithIronSkinBrew + this.hitsWithoutIronSkinBrew),
      isLessThan: {
        minor: 0.99,
        average: 0.98,
        major: 0.95,
      },
      style: 'percentage',
    };
  }

  get clipSuggestionThreshold() {
    return {
      actual: this.durationLost / this.totalDuration,
      isGreaterThan: {
        minor: 0.02,
        average: 0.05,
        major: 0.1,
      },
      style: 'percentage',
    };
  }

  statistic() {
    const isbUptime = this.combatants.selected.getBuffUptime(SPELLS.IRONSKIN_BREW_BUFF.id) / this.owner.fightDuration;
    const hitsMitigatedPercent = this.hitsWithIronSkinBrew / (this.hitsWithIronSkinBrew + this.hitsWithoutIronSkinBrew);
    return (
      <StatisticBox
        icon={<SpellIcon id={SPELLS.IRONSKIN_BREW.id} />}
        value={`${formatPercentage(isbUptime)}%`}
        label="Ironskin Brew uptime"
        tooltip={`Ironskin Brew breakdown (these values are direct damage and does not include damage added to stagger):

You were hit ${this.hitsWithIronSkinBrew} times with your Ironskin Brew buff (${formatThousands(this.damageWithIronSkinBrew)} damage).

You were hit ${this.hitsWithoutIronSkinBrew} times without your Ironskin Brew buff (${formatThousands(this.damageWithoutIronSkinBrew)} damage).

            ${formatPercentage(hitsMitigatedPercent)}% of attacks were mitigated with Ironskin Brew.`}
          />
    );
  }
  statisticOrder = STATISTIC_ORDER.OPTIONAL();
}

export default IronSkinBrew;
<|endoftext|>


<commit_before>var fs = require('fs');

/**
 * Creates a random string
 *
 * @param     {Object}    options
 * @return    {String}
 */
function randomString(options) {
  var set = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz';

  if (options.symbols === true) {
    set += '!@#$%^&*()<>?/[]{},.:;';
  }

  var key = '';
  for (var i = 0; i < options.length; i++) {
    key += set.charAt(Math.floor(Math.random() * set.length));
  }

  return key;
}

/**
 * Generates a random base32 secret key and optionally a qrcode
 *
 * @param     {Object}    options
 * @return    {Object}
 */
function generateKey(options) {
  // Options
  options = _.extend({
    length: 32,
    name: '',
    symbols: false,
    google: false,
    qrCode: false,
    type: 'base32'
  }, options || {});

  // Generate the random string
  var key = randomString(options);

  if (options.type === 'ascii') {
    return key;
  } else if (options.type === 'base32') {
    // Encode the ascii string into base32 and remove any `=` signs which google
    // doesn't like
    key = base32.encode(key).toString().replace(/=/g, '');

    return key;
  } else {
    throw new Error('InvalidKeyType');
  }
}

/**
 * Generates a random key andsaves to disk. Used to store secrets
 *
 * @param     {String}    filename
 */
module.exports = function GenerateSecretKey(pkg, overrides) {
  overrides = overrides || {};

  var options = {
    rc: '.' + pkg.name + 'rc',
    length: 64
  };

  for (var key in overrides) {
    if (overrides.hasOwnProperty(key) && overrides[key]) {
      options[key] = overrides[key];
    }
  }

  var key = generateKey({
    symbols: true,
    type: 'ascii',
    length: options.length
  });

  // Chaining for semantics
  return {
    save: function(callback) {
      callback = callback || function() {}
      try {
        var rc = {};

        // If the file already exists read it so we don't lose anything
        if (fs.existsSync(options.rc)) {
          rc = JSON.parse(fs.readFileSync(options.rc));
        }

        rc.secret = key;

        fs.writeFileSync(options.rc, JSON.stringify(rc, null, 4));

        callback(null, options);
      } catch (e) {
        callback(e);
      }
    }
  };
}
<commit_msg>fix bad reference to lodash<commit_after>var fs = require('fs');

/**
 * Creates a random string
 *
 * @param     {Object}    options
 * @return    {String}
 */
function randomString(options) {
  var set = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz';

  if (options.symbols === true) {
    set += '!@#$%^&*()<>?/[]{},.:;';
  }

  var key = '';
  for (var i = 0; i < options.length; i++) {
    key += set.charAt(Math.floor(Math.random() * set.length));
  }

  return key;
}

/**
 * Generates a random base32 secret key and optionally a qrcode
 *
 * @param     {Object}    options
 * @return    {Object}
 */
function generateKey(options) {
  // Options
  options = Object.assign({
    length: 32,
    name: '',
    symbols: false,
    google: false,
    qrCode: false,
    type: 'base32'
  }, options || {});

  // Generate the random string
  var key = randomString(options);

  if (options.type === 'ascii') {
    return key;
  } else if (options.type === 'base32') {
    // Encode the ascii string into base32 and remove any `=` signs which google
    // doesn't like
    key = base32.encode(key).toString().replace(/=/g, '');

    return key;
  } else {
    throw new Error('InvalidKeyType');
  }
}

/**
 * Generates a random key andsaves to disk. Used to store secrets
 *
 * @param     {String}    filename
 */
module.exports = function GenerateSecretKey(pkg, overrides) {
  overrides = overrides || {};

  var options = {
    rc: '.' + pkg.name + 'rc',
    length: 64
  };

  for (var key in overrides) {
    if (overrides.hasOwnProperty(key) && overrides[key]) {
      options[key] = overrides[key];
    }
  }

  var key = generateKey({
    symbols: true,
    type: 'ascii',
    length: options.length
  });

  // Chaining for semantics
  return {
    save: function(callback) {
      callback = callback || function() {}
      try {
        var rc = {};

        // If the file already exists read it so we don't lose anything
        if (fs.existsSync(options.rc)) {
          rc = JSON.parse(fs.readFileSync(options.rc));
        }

        rc.secret = key;

        fs.writeFileSync(options.rc, JSON.stringify(rc, null, 4));

        callback(null, options);
      } catch (e) {
        callback(e);
      }
    }
  };
}
<|endoftext|>


<commit_before>/*
 * decaffeinate suggestions:
 * DS102: Remove unnecessary code created because of implicit returns
 * DS207: Consider shorter variations of null checks
 * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
 */
//     NoFlo - Flow-Based Programming for JavaScript
//     (c) 2013-2017 Flowhub UG
//     (c) 2011-2012 Henri Bergius, Nemein
//     NoFlo may be freely distributed under the MIT license
const {EventEmitter} = require('events');
const IP = require('./IP');

// ## Internal Sockets
//
// The default communications mechanism between NoFlo processes is
// an _internal socket_, which is responsible for accepting information
// packets sent from processes' outports, and emitting corresponding
// events so that the packets can be caught to the inport of the
// connected process.
class InternalSocket extends EventEmitter {
  regularEmitEvent(event, data) {
    this.emit(event, data);
  }

  debugEmitEvent(event, data) {
    try {
      this.emit(event, data);
    } catch (error) {
      if (error.id && error.metadata && error.error) {
        // Wrapped debuggable error coming from downstream, no need to wrap
        if (this.listeners('error').length === 0) { throw error.error; }
        this.emit('error', error);
        return;
      }

      if (this.listeners('error').length === 0) { throw error; }

      this.emit('error', {
        id: this.to.process.id,
        error,
        metadata: this.metadata
      }
      );
    }

  }

  constructor(metadata) {
    if (metadata == null) { metadata = {}; }
    super();
    this.metadata = metadata;
    this.brackets = [];
    this.connected = false;
    this.dataDelegate = null;
    this.debug = false;
    this.emitEvent = this.regularEmitEvent;
  }

  // ## Socket connections
  //
  // Sockets that are attached to the ports of processes may be
  // either connected or disconnected. The semantical meaning of
  // a connection is that the outport is in the process of sending
  // data. Disconnecting means an end of transmission.
  //
  // This can be used for example to signal the beginning and end
  // of information packets resulting from the reading of a single
  // file or a database query.
  //
  // Example, disconnecting when a file has been completely read:
  //
  //     readBuffer: (fd, position, size, buffer) ->
  //       fs.read fd, buffer, 0, buffer.length, position, (err, bytes, buffer) =>
  //         # Send data. The first send will also connect if not
  //         # already connected.
  //         @outPorts.out.send buffer.slice 0, bytes
  //         position += buffer.length
  //
  //         # Disconnect when the file has been completely read
  //         return @outPorts.out.disconnect() if position >= size
  //
  //         # Otherwise, call same method recursively
  //         @readBuffer fd, position, size, buffer
  connect() {
    if (this.connected) { return; }
    this.connected = true;
    this.emitEvent('connect', null);
  }

  disconnect() {
    if (!this.connected) { return; }
    this.connected = false;
    this.emitEvent('disconnect', null);
  }

  isConnected() { return this.connected; }

  // ## Sending information packets
  //
  // The _send_ method is used by a processe's outport to
  // send information packets. The actual packet contents are
  // not defined by NoFlo, and may be any valid JavaScript data
  // structure.
  //
  // The packet contents however should be such that may be safely
  // serialized or deserialized via JSON. This way the NoFlo networks
  // can be constructed with more flexibility, as file buffers or
  // message queues can be used as additional packet relay mechanisms.
  send(data) {
    if ((data === undefined) && (typeof this.dataDelegate === 'function')) { data = this.dataDelegate(); }
    this.handleSocketEvent('data', data);
  }

  // ## Sending information packets without open bracket
  //
  // As _connect_ event is considered as open bracket, it needs to be followed
  // by a _disconnect_ event or a closing bracket. In the new simplified
  // sending semantics single IP objects can be sent without open/close brackets.
  post(ip, autoDisconnect) {
    if (autoDisconnect == null) { autoDisconnect = true; }
    if ((ip === undefined) && (typeof this.dataDelegate === 'function')) { ip = this.dataDelegate(); }
    // Send legacy connect/disconnect if needed
    if (!this.isConnected() && (this.brackets.length === 0)) {
      (this.connect)();
    }
    this.handleSocketEvent('ip', ip, false);
    if (autoDisconnect && this.isConnected() && (this.brackets.length === 0)) {
      (this.disconnect)();
    }
  }

  // ## Information Packet grouping
  //
  // Processes sending data to sockets may also group the packets
  // when necessary. This allows transmitting tree structures as
  // a stream of packets.
  //
  // For example, an object could be split into multiple packets
  // where each property is identified by a separate grouping:
  //
  //     # Group by object ID
  //     @outPorts.out.beginGroup object.id
  //
  //     for property, value of object
  //       @outPorts.out.beginGroup property
  //       @outPorts.out.send value
  //       @outPorts.out.endGroup()
  //
  //     @outPorts.out.endGroup()
  //
  // This would cause a tree structure to be sent to the receiving
  // process as a stream of packets. So, an article object may be
  // as packets like:
  //
  // * `/<article id>/title/Lorem ipsum`
  // * `/<article id>/author/Henri Bergius`
  //
  // Components are free to ignore groupings, but are recommended
  // to pass received groupings onward if the data structures remain
  // intact through the component's processing.
  beginGroup(group) {
    this.handleSocketEvent('begingroup', group);
  }

  endGroup() {
    this.handleSocketEvent('endgroup');
  }

  // ## Socket data delegation
  //
  // Sockets have the option to receive data from a delegate function
  // should the `send` method receive undefined for `data`.  This
  // helps in the case of defaulting values.
  setDataDelegate(delegate) {
    if (typeof delegate !== 'function') {
      throw Error('A data delegate must be a function.');
    }
    this.dataDelegate = delegate;
  }

  // ## Socket debug mode
  //
  // Sockets can catch exceptions happening in processes when data is
  // sent to them. These errors can then be reported to the network for
  // notification to the developer.
  setDebug(active) {
    this.debug = active;
    this.emitEvent = this.debug ? this.debugEmitEvent : this.regularEmitEvent;
  }

  // ## Socket identifiers
  //
  // Socket identifiers are mainly used for debugging purposes.
  // Typical identifiers look like _ReadFile:OUT -> Display:IN_,
  // but for sockets sending initial information packets to
  // components may also loom like _DATA -> ReadFile:SOURCE_.
  getId() {
    const fromStr = from => `${from.process.id}() ${from.port.toUpperCase()}`;
    const toStr = to => `${to.port.toUpperCase()} ${to.process.id}()`;

    if (!this.from && !this.to) { return "UNDEFINED"; }
    if (this.from && !this.to) { return `${fromStr(this.from)} -> ANON`; }
    if (!this.from) { return `DATA -> ${toStr(this.to)}`; }
    return `${fromStr(this.from)} -> ${toStr(this.to)}`;
  }

  legacyToIp(event, payload) {
    // No need to wrap modern IP Objects
    if (IP.isIP(payload)) { return payload; }

    // Wrap legacy events into appropriate IP objects
    switch (event) {
      case 'begingroup':
        return new IP('openBracket', payload);
      case 'endgroup':
        return new IP('closeBracket');
      case 'data':
        return new IP('data', payload);
      default:
        return null;
    }
  }

  ipToLegacy(ip) {
    let legacy;
    switch (ip.type) {
      case 'openBracket':
        return legacy = {
          event: 'begingroup',
          payload: ip.data
        };
      case 'data':
        return legacy = {
          event: 'data',
          payload: ip.data
        };
      case 'closeBracket':
        return legacy = {
          event: 'endgroup',
          payload: ip.data
        };
    }
  }

  handleSocketEvent(event, payload, autoConnect) {
    if (autoConnect == null) { autoConnect = true; }
    const isIP = (event === 'ip') && IP.isIP(payload);
    const ip = isIP ? payload : this.legacyToIp(event, payload);
    if (!ip) { return; }

    if (!this.isConnected() && autoConnect && (this.brackets.length === 0)) {
      // Connect before sending
      this.connect();
    }

    if (event === 'begingroup') {
      this.brackets.push(payload);
    }
    if (isIP && (ip.type === 'openBracket')) {
      this.brackets.push(ip.data);
    }

    if (event === 'endgroup') {
      // Prevent closing already closed groups
      if (this.brackets.length === 0) { return; }
      // Add group name to bracket
      ip.data = this.brackets.pop();
      payload = ip.data;
    }
    if (isIP && (payload.type === 'closeBracket')) {
      // Prevent closing already closed brackets
      if (this.brackets.length === 0) { return; }
      this.brackets.pop();
    }

    // Emit the IP Object
    this.emitEvent('ip', ip);

    // Emit the legacy event
    if (!ip || !ip.type) { return; }

    if (isIP) {
      const legacy = this.ipToLegacy(ip);
      ({
        event
      } = legacy);
      ({
        payload
      } = legacy);
    }

    if (event === 'connect') { this.connected = true; }
    if (event === 'disconnect') { this.connected = false; }
    this.emitEvent(event, payload);
  }
}

exports.InternalSocket = InternalSocket;

exports.createSocket = () => new InternalSocket;
<commit_msg>decaffeinate: Run post-processing cleanups on InternalSocket.coffee<commit_after>/* eslint-disable
    class-methods-use-this,
    consistent-return,
    default-case,
    no-param-reassign,
    no-return-assign,
    no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
 * decaffeinate suggestions:
 * DS102: Remove unnecessary code created because of implicit returns
 * DS207: Consider shorter variations of null checks
 * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
 */
//     NoFlo - Flow-Based Programming for JavaScript
//     (c) 2013-2017 Flowhub UG
//     (c) 2011-2012 Henri Bergius, Nemein
//     NoFlo may be freely distributed under the MIT license
const { EventEmitter } = require('events');
const IP = require('./IP');

// ## Internal Sockets
//
// The default communications mechanism between NoFlo processes is
// an _internal socket_, which is responsible for accepting information
// packets sent from processes' outports, and emitting corresponding
// events so that the packets can be caught to the inport of the
// connected process.
class InternalSocket extends EventEmitter {
  regularEmitEvent(event, data) {
    this.emit(event, data);
  }

  debugEmitEvent(event, data) {
    try {
      this.emit(event, data);
    } catch (error) {
      if (error.id && error.metadata && error.error) {
        // Wrapped debuggable error coming from downstream, no need to wrap
        if (this.listeners('error').length === 0) { throw error.error; }
        this.emit('error', error);
        return;
      }

      if (this.listeners('error').length === 0) { throw error; }

      this.emit('error', {
        id: this.to.process.id,
        error,
        metadata: this.metadata,
      });
    }
  }

  constructor(metadata) {
    if (metadata == null) { metadata = {}; }
    super();
    this.metadata = metadata;
    this.brackets = [];
    this.connected = false;
    this.dataDelegate = null;
    this.debug = false;
    this.emitEvent = this.regularEmitEvent;
  }

  // ## Socket connections
  //
  // Sockets that are attached to the ports of processes may be
  // either connected or disconnected. The semantical meaning of
  // a connection is that the outport is in the process of sending
  // data. Disconnecting means an end of transmission.
  //
  // This can be used for example to signal the beginning and end
  // of information packets resulting from the reading of a single
  // file or a database query.
  //
  // Example, disconnecting when a file has been completely read:
  //
  //     readBuffer: (fd, position, size, buffer) ->
  //       fs.read fd, buffer, 0, buffer.length, position, (err, bytes, buffer) =>
  //         # Send data. The first send will also connect if not
  //         # already connected.
  //         @outPorts.out.send buffer.slice 0, bytes
  //         position += buffer.length
  //
  //         # Disconnect when the file has been completely read
  //         return @outPorts.out.disconnect() if position >= size
  //
  //         # Otherwise, call same method recursively
  //         @readBuffer fd, position, size, buffer
  connect() {
    if (this.connected) { return; }
    this.connected = true;
    this.emitEvent('connect', null);
  }

  disconnect() {
    if (!this.connected) { return; }
    this.connected = false;
    this.emitEvent('disconnect', null);
  }

  isConnected() { return this.connected; }

  // ## Sending information packets
  //
  // The _send_ method is used by a processe's outport to
  // send information packets. The actual packet contents are
  // not defined by NoFlo, and may be any valid JavaScript data
  // structure.
  //
  // The packet contents however should be such that may be safely
  // serialized or deserialized via JSON. This way the NoFlo networks
  // can be constructed with more flexibility, as file buffers or
  // message queues can be used as additional packet relay mechanisms.
  send(data) {
    if ((data === undefined) && (typeof this.dataDelegate === 'function')) { data = this.dataDelegate(); }
    this.handleSocketEvent('data', data);
  }

  // ## Sending information packets without open bracket
  //
  // As _connect_ event is considered as open bracket, it needs to be followed
  // by a _disconnect_ event or a closing bracket. In the new simplified
  // sending semantics single IP objects can be sent without open/close brackets.
  post(ip, autoDisconnect) {
    if (autoDisconnect == null) { autoDisconnect = true; }
    if ((ip === undefined) && (typeof this.dataDelegate === 'function')) { ip = this.dataDelegate(); }
    // Send legacy connect/disconnect if needed
    if (!this.isConnected() && (this.brackets.length === 0)) {
      (this.connect)();
    }
    this.handleSocketEvent('ip', ip, false);
    if (autoDisconnect && this.isConnected() && (this.brackets.length === 0)) {
      (this.disconnect)();
    }
  }

  // ## Information Packet grouping
  //
  // Processes sending data to sockets may also group the packets
  // when necessary. This allows transmitting tree structures as
  // a stream of packets.
  //
  // For example, an object could be split into multiple packets
  // where each property is identified by a separate grouping:
  //
  //     # Group by object ID
  //     @outPorts.out.beginGroup object.id
  //
  //     for property, value of object
  //       @outPorts.out.beginGroup property
  //       @outPorts.out.send value
  //       @outPorts.out.endGroup()
  //
  //     @outPorts.out.endGroup()
  //
  // This would cause a tree structure to be sent to the receiving
  // process as a stream of packets. So, an article object may be
  // as packets like:
  //
  // * `/<article id>/title/Lorem ipsum`
  // * `/<article id>/author/Henri Bergius`
  //
  // Components are free to ignore groupings, but are recommended
  // to pass received groupings onward if the data structures remain
  // intact through the component's processing.
  beginGroup(group) {
    this.handleSocketEvent('begingroup', group);
  }

  endGroup() {
    this.handleSocketEvent('endgroup');
  }

  // ## Socket data delegation
  //
  // Sockets have the option to receive data from a delegate function
  // should the `send` method receive undefined for `data`.  This
  // helps in the case of defaulting values.
  setDataDelegate(delegate) {
    if (typeof delegate !== 'function') {
      throw Error('A data delegate must be a function.');
    }
    this.dataDelegate = delegate;
  }

  // ## Socket debug mode
  //
  // Sockets can catch exceptions happening in processes when data is
  // sent to them. These errors can then be reported to the network for
  // notification to the developer.
  setDebug(active) {
    this.debug = active;
    this.emitEvent = this.debug ? this.debugEmitEvent : this.regularEmitEvent;
  }

  // ## Socket identifiers
  //
  // Socket identifiers are mainly used for debugging purposes.
  // Typical identifiers look like _ReadFile:OUT -> Display:IN_,
  // but for sockets sending initial information packets to
  // components may also loom like _DATA -> ReadFile:SOURCE_.
  getId() {
    const fromStr = (from) => `${from.process.id}() ${from.port.toUpperCase()}`;
    const toStr = (to) => `${to.port.toUpperCase()} ${to.process.id}()`;

    if (!this.from && !this.to) { return 'UNDEFINED'; }
    if (this.from && !this.to) { return `${fromStr(this.from)} -> ANON`; }
    if (!this.from) { return `DATA -> ${toStr(this.to)}`; }
    return `${fromStr(this.from)} -> ${toStr(this.to)}`;
  }

  legacyToIp(event, payload) {
    // No need to wrap modern IP Objects
    if (IP.isIP(payload)) { return payload; }

    // Wrap legacy events into appropriate IP objects
    switch (event) {
      case 'begingroup':
        return new IP('openBracket', payload);
      case 'endgroup':
        return new IP('closeBracket');
      case 'data':
        return new IP('data', payload);
      default:
        return null;
    }
  }

  ipToLegacy(ip) {
    let legacy;
    switch (ip.type) {
      case 'openBracket':
        return legacy = {
          event: 'begingroup',
          payload: ip.data,
        };
      case 'data':
        return legacy = {
          event: 'data',
          payload: ip.data,
        };
      case 'closeBracket':
        return legacy = {
          event: 'endgroup',
          payload: ip.data,
        };
    }
  }

  handleSocketEvent(event, payload, autoConnect) {
    if (autoConnect == null) { autoConnect = true; }
    const isIP = (event === 'ip') && IP.isIP(payload);
    const ip = isIP ? payload : this.legacyToIp(event, payload);
    if (!ip) { return; }

    if (!this.isConnected() && autoConnect && (this.brackets.length === 0)) {
      // Connect before sending
      this.connect();
    }

    if (event === 'begingroup') {
      this.brackets.push(payload);
    }
    if (isIP && (ip.type === 'openBracket')) {
      this.brackets.push(ip.data);
    }

    if (event === 'endgroup') {
      // Prevent closing already closed groups
      if (this.brackets.length === 0) { return; }
      // Add group name to bracket
      ip.data = this.brackets.pop();
      payload = ip.data;
    }
    if (isIP && (payload.type === 'closeBracket')) {
      // Prevent closing already closed brackets
      if (this.brackets.length === 0) { return; }
      this.brackets.pop();
    }

    // Emit the IP Object
    this.emitEvent('ip', ip);

    // Emit the legacy event
    if (!ip || !ip.type) { return; }

    if (isIP) {
      const legacy = this.ipToLegacy(ip);
      ({
        event,
      } = legacy);
      ({
        payload,
      } = legacy);
    }

    if (event === 'connect') { this.connected = true; }
    if (event === 'disconnect') { this.connected = false; }
    this.emitEvent(event, payload);
  }
}

exports.InternalSocket = InternalSocket;

exports.createSocket = () => new InternalSocket();
<|endoftext|>


<commit_before>var striptags = require('striptags'),
  dom = require('../services/dom'),
  getInput = require('../services/get-input');

/**
 * toggle classes on elements
 * @param {boolean} isTooLong
 * @param {Element} span
 * @param {Element} input
 */
function toggleClass(isTooLong, span, input) {
  if (isTooLong) {
    span.classList.add('too-long');
    input.classList.add('input-too-long');
  } else {
    span.classList.remove('too-long');
    input.classList.remove('input-too-long');
  }
}

/**
 * set styles depending on the remaining length
 * @param {number} remaining
 * @param {Element} span of the soft-maxlength
 * @param {Element} input
 * @returns {string} to display in the span
 */
function setStyles(remaining, span, input) {
  if (remaining > 0) {
    toggleClass(false, span, input);
    return 'Remaining: ' + remaining;
  } else if (remaining === 0) {
    toggleClass(false, span, input);
    return 'At the character limit';
  } else if (remaining === -1) {
    toggleClass(true, span, input);
    return -remaining + ' character over the limit';
  } else {
    toggleClass(true, span, input);
    return -remaining + ' characters over the limit';
  }
}

/**
 * Remove tags and white spaces.
 * @param {string} value
 * @returns {string}
 */
function cleanValue(value) {
  var clean = striptags(value);

  clean = clean.replace(/(\u00a0| | )/ig, ' '); // remove
  return clean;
}

/**
 * Add soft max length: appends el and adds formatter.
 * @param {{name: string, el: Element, bindings: {}, formatters: {}}} result
 * @param {{value: string}} args
 * @returns {{}}
 */
module.exports = function (result, args) {
  var el = result.el,
    bindings = result.bindings,
    name = result.name,
    tpl = `
      { ${name}.max | charsRemaining ${name}.data.value }
    `,
    span = dom.create(tpl);

  bindings.max = args.value;
  result.formatters.charsRemaining = function (max, value) {
    var length = cleanValue(value).length,
      remaining = max - length,
      input = getInput(el); // needs to happen after wysiwyg is instantiated

    return setStyles(remaining, span, input);
  };

  el.appendChild(span);
  return result;
};
<commit_msg>updated soft-maxlength<commit_after>var striptags = require('striptags'),
  dom = require('../services/dom'),
  getInput = require('../services/get-input');

/**
 * toggle classes on elements
 * @param {boolean} isTooLong
 * @param {Element} span
 * @param {Element} input
 */
function toggleClass(isTooLong, span, input) {
  if (isTooLong) {
    span.classList.add('too-long');
    input.classList.add('input-too-long');
  } else {
    span.classList.remove('too-long');
    input.classList.remove('input-too-long');
  }
}

/**
 * set text in a span
 * @param {string} text
 * @param {Element} span
 */
function setText(text, span) {
  span.innerHTML = text;
}

/**
 * set styles depending on the remaining length
 * @param {number} remaining
 * @param {Element} el
 */
function setStyles(remaining, el) {
  var input = getInput(el.parentNode),
    span = el;

  if (remaining > 0) {
    toggleClass(false, span, input);
    setText('Remaining: ' + remaining, span);
  } else if (remaining === 0) {
    toggleClass(false, span, input);
    setText('At the character limit', span);
  } else if (remaining === -1) {
    toggleClass(true, span, input);
    setText(-remaining + ' character over the limit', span);
  } else {
    toggleClass(true, span, input);
    setText(-remaining + ' characters over the limit', span);
  }
}

/**
 * Remove tags and white spaces.
 * @param {string} value
 * @returns {string}
 */
function cleanValue(value) {
  var clean = striptags(value);

  clean = clean.replace(/(\u00a0| | )/ig, ' '); // remove
  return clean;
}

/**
 * Add soft max length: appends el and adds formatter.
 * @param {{name: string, el: Element, bindings: {}, formatters: {}}} result
 * @param {{value: string}} args
 * @returns {{}}
 */
module.exports = function (result, args) {
  var el = result.el,
    bindings = result.bindings,
    name = result.name,
    tpl = `

    `,
    span = dom.create(tpl);

  bindings.max = args.value;

  result.binders.remaining = function (el, value) {
    var length = cleanValue(value).length,
      remaining = bindings.max - length;

    setStyles(remaining, el);
  };

  el.appendChild(span);
  return result;
};
<|endoftext|>