<commit_before>import React, { Component } from 'react';
import {Modal, OverlayTrigger, Button, FormControl, FormGroup, ControlLabel, Checkbox, HelpBlock } from 'react-bootstrap';
import update from 'immutability-helper';
import axios from 'axios';

import DropdownFilter from '../DropdownFilter/DropdownFilter.js';
import DropdownConverter from '../../DropdownConverter.js';

require('./VolunteerAddModal.css')

export default class VolunteerAddModal extends Component{
    state = {
        email: '',
        department: 0,
        role: 4,
        production: false,
        errorTexts: [],
        isButtonEnabled: true
    }

    getInputChangeHandler = (field) => {
        return (event) => this.handleChange(field,event);
    }

    handleChange = (field, event) => {
        console.log(`VolunteerAddModal.handleInputChange. field ${field}`);
        let converter = new DropdownConverter();
        let val = event.target.value;

        this.state.errorTexts = []
        this.setState( (state) => update(state, {$merge:{[field]: converter.convertFromDisplay(val)}} ));
    }

    handleSubmit = () => {
        console.log(`VolunteerAddModal.handleSubmit: state:${this.state}`);

        let emails = this.splitEmailString(this.state.email);
        if (emails.length == 0) {
            this.state.errorTexts = ['Please enter an email address']
            this.setState(this.state)
            return;
        }

        var invalidEmails = []
        emails.forEach((email) => {
            if (!this.validateEmail(email)) {
                invalidEmails.push(email)
            }
        })

        if (invalidEmails.length > 0) {
            this.state.errorTexts = invalidEmails.map((email) => {return `Error: ${email} - Invalid email`})
            this.setState(this.state)
            return;
        }

        // add volunteers
        let department = this.state.department
        let role = this.state.role
        let production = this.state.production

        this.state.errorTexts = ['Adding... ']
        this.state.isButtonEnabled = false
        this.setState(this.state)

        axios.post(`/api/v1/departments/${department}/volunteers`,
            {
                role: role,
                is_production: production,
                emails: emails
            })
            .then(response => {
                this.state.isButtonEnabled = true
                this.props.onSuccess()
                this.setState(this.state)
                this.handleServerResponse(response)
            })
            .catch(error => {
                this.state.errorTexts = ['Server Error']
                this.state.isButtonEnabled = true
                this.setState(this.state)
            });
        // let emails = this.splitEmailString(this.state.email);
        // console.log(`VolunteerAddModal.handleSubmit: role:${this.state.role}, department:${this.state.department}, emails:${emails}`);
        // this.props.onSubmit(this.state.department,this.state.role,this.state.production,emails);
        // this.handleClose();
    }

    handleServerResponse = (response) => {
        console.log(response)
        var errors = []
        var successCounter = 0

        response.data.forEach(({email, status}) => {
            if (status != 'OK') {
                errors.push(`Error: ${email} - ${status}`)
            } else {
                successCounter += 1
            }
        })

        var text = []
        if (successCounter > 1) {
            text.push(`${successCounter} volunteers were added successfully`)
        } else if (successCounter == 1) {
            text.push(`1 volunteer was added successfully`)
        }
        text = text.concat(errors)

        this.state.errorTexts = text
        this.setState(this.state)
    }

    handleClose = () => {
        // Close modal
        this.props.onHide();
    }

    validateEmail = (email) => {
        var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
        return re.test(email);
    }

    splitEmailString = (emailStr) => {
        console.log(`emails string:${emailStr}`)
        var emailArr = emailStr.split(/\r|\n/).map((email) => {return email.trim()})
        emailArr = emailArr.filter((email) => { return email != '' });

        return emailArr
        // let filteredArr = emailArr.filter(this.validateEmail);
        // console.log((emailArr.length - filteredArr.length) + ' emails were incorrect');
        // return filteredArr;
    }

    testLastEmail = (email) => {
        let emailArr = email.split(',');
        return this.validateEmail(emailArr[emailArr.length - 2]);
    }

    getRolesOptions = () => {
        if (this.props.roles == undefined || this.props.roles.length == 0) {
            return <option value="" key="">Loading..</option>
        }

        return this.props.roles.map(({name, id}) =>
            <option value={id} key={name}>{name}</option>
        )
    }

    getDepartmentOptions = () => {
        if (this.props.departments == undefined || this.props.departments.length == 0) {
            return <option value="" key="">Loading..</option>
        }

        return this.props.departments.map(({name, id}) =>
            <option value={id} key={name}>{name}</option>
        )
    }

    getErrorText = () => (

{this.state.errorTexts.map((text, index) => <li key={`li-${index}`}>{text}
)}

    )

    render(){
        let converter = new DropdownConverter();
        return (
            <Modal show={this.props.show} onHide={this.handleClose}>
                <Modal.Header closeButton>
                    <Modal.Title>Add new volunteer</Modal.Title>
                </Modal.Header>
                <Modal.Body>

                    <FormGroup controlId="Department">
                        <ControlLabel>Department</ControlLabel>
                        <FormControl componentClass="select" onChange={this.getInputChangeHandler('department')}
                            value={this.state.department}
                            className="form-control" >
                                {this.getDepartmentOptions()}
                        </FormControl>
                    </FormGroup>

                    <FormGroup controlId="Role">
                        <ControlLabel>Role</ControlLabel>
                        <FormControl componentClass="select" onChange ={this.getInputChangeHandler('role')}
                            value={this.state.role}
                            className="form-control" >
                                {this.getRolesOptions()}
                        </FormControl>
                    </FormGroup>

                    <FormGroup controlId="Production">
                        <ControlLabel>Production</ControlLabel>
                        <FormControl componentClass="select" onChange ={this.getInputChangeHandler('production')}
                            value={converter.convertToDisplay(this.state.production)}
                            className="form-control" >
                                {
                                    ['Yes', 'No'].map(
                                    (option)=> <option value={option} key={option}>{option}</option>
                                    )
                                }
                        </FormControl>
                    </FormGroup>

                    <FormGroup controlId="email">
                        <ControlLabel>User Email</ControlLabel>
                        <FormControl componentClass="textarea" onChange={this.getInputChangeHandler('email')}
                            value={this.state.email}
                            className="form-control" placeholder="Please enter all volunteer emails - one in each line"></FormControl>
                    </FormGroup>
                    {this.getErrorText()}
                </Modal.Body>
            <Modal.Footer>
                <Button onClick={this.handleClose}>Close</Button>
                <Button bsStyle="primary" onClick={this.handleSubmit} disabled={!this.state.isButtonEnabled}>Add Volunteer</Button>
            </Modal.Footer>
            </Modal>
        )
    }
}<commit_msg>add volunteer - department id default value<commit_after>import React, { Component } from 'react';
import {Modal, OverlayTrigger, Button, FormControl, FormGroup, ControlLabel, Checkbox, HelpBlock } from 'react-bootstrap';
import update from 'immutability-helper';
import axios from 'axios';

import DropdownFilter from '../DropdownFilter/DropdownFilter.js';
import DropdownConverter from '../../DropdownConverter.js';

require('./VolunteerAddModal.css')

export default class VolunteerAddModal extends Component{
    state = {
        email: '',
        department: -1,
        role: 4,
        production: false,
        errorTexts: [],
        isButtonEnabled: true
    }

    componentWillReceiveProps() {
        if (this.props.departments.length > 0 && this.state.department == -1) {
            this.setState((state)=>update(state, {department:{$set:this.props.departments[0].id}}))
        }
    }

    getInputChangeHandler = (field) => {
        return (event) => this.handleChange(field,event);
    }

    handleChange = (field, event) => {
        console.log(`VolunteerAddModal.handleInputChange. field ${field}`);
        let converter = new DropdownConverter();
        let val = event.target.value;

        this.state.errorTexts = []
        this.setState( (state) => update(state, {$merge:{[field]: converter.convertFromDisplay(val)}} ));
    }

    handleSubmit = () => {
        console.log(`VolunteerAddModal.handleSubmit: state:${this.state}`);

        let emails = this.splitEmailString(this.state.email);
        if (emails.length == 0) {
            this.state.errorTexts = ['Please enter an email address']
            this.setState(this.state)
            return;
        }

        var invalidEmails = []
        emails.forEach((email) => {
            if (!this.validateEmail(email)) {
                invalidEmails.push(email)
            }
        })

        if (invalidEmails.length > 0) {
            this.state.errorTexts = invalidEmails.map((email) => {return `Error: ${email} - Invalid email`})
            this.setState(this.state)
            return;
        }

        // add volunteers
        let department = this.state.department
        let role = this.state.role
        let production = this.state.production

        this.state.errorTexts = ['Adding... ']
        this.state.isButtonEnabled = false
        this.setState(this.state)

        axios.post(`/api/v1/departments/${department}/volunteers`,
            {
                role: role,
                is_production: production,
                emails: emails
            })
            .then(response => {
                this.state.isButtonEnabled = true
                this.props.onSuccess()
                this.setState(this.state)
                this.handleServerResponse(response)
            })
            .catch(error => {
                this.state.errorTexts = ['Server Error']
                this.state.isButtonEnabled = true
                this.setState(this.state)
            });
        // let emails = this.splitEmailString(this.state.email);
        // console.log(`VolunteerAddModal.handleSubmit: role:${this.state.role}, department:${this.state.department}, emails:${emails}`);
        // this.props.onSubmit(this.state.department,this.state.role,this.state.production,emails);
        // this.handleClose();
    }

    handleServerResponse = (response) => {
        console.log(response)
        var errors = []
        var successCounter = 0

        response.data.forEach(({email, status}) => {
            if (status != 'OK') {
                errors.push(`Error: ${email} - ${status}`)
            } else {
                successCounter += 1
            }
        })

        var text = []
        if (successCounter > 1) {
            text.push(`${successCounter} volunteers were added successfully`)
        } else if (successCounter == 1) {
            text.push(`1 volunteer was added successfully`)
        }
        text = text.concat(errors)

        this.state.errorTexts = text
        this.setState(this.state)
    }

    handleClose = () => {
        // Close modal
        this.props.onHide();
    }

    validateEmail = (email) => {
        var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
        return re.test(email);
    }

    splitEmailString = (emailStr) => {
        console.log(`emails string:${emailStr}`)
        var emailArr = emailStr.split(/\r|\n/).map((email) => {return email.trim()})
        emailArr = emailArr.filter((email) => { return email != '' });

        return emailArr
        // let filteredArr = emailArr.filter(this.validateEmail);
        // console.log((emailArr.length - filteredArr.length) + ' emails were incorrect');
        // return filteredArr;
    }

    testLastEmail = (email) => {
        let emailArr = email.split(',');
        return this.validateEmail(emailArr[emailArr.length - 2]);
    }

    getRolesOptions = () => {
        if (this.props.roles == undefined || this.props.roles.length == 0) {
            return <option value="" key="">Loading..</option>
        }

        return this.props.roles.map(({name, id}) =>
            <option value={id} key={name}>{name}</option>
        )
    }

    getDepartmentOptions = () => {
        if (this.props.departments == undefined || this.props.departments.length == 0) {
            return <option value="" key="">Loading..</option>
        }

        return this.props.departments.map(({name, id}) =>
            <option value={id} key={name}>{name}</option>
        )
    }

    getErrorText = () => (

{this.state.errorTexts.map((text, index) => <li key={`li-${index}`}>{text}
)}

    )

    render(){
        let converter = new DropdownConverter();

        return (
            <Modal show={this.props.show} onHide={this.handleClose}>
                <Modal.Header closeButton>
                    <Modal.Title>Add new volunteer</Modal.Title>
                </Modal.Header>
                <Modal.Body>

                    <FormGroup controlId="Department">
                        <ControlLabel>Department</ControlLabel>
                        <FormControl componentClass="select" onChange={this.getInputChangeHandler('department')}
                            value={this.state.department}
                            className="form-control" >
                                {this.getDepartmentOptions()}
                        </FormControl>
                    </FormGroup>

                    <FormGroup controlId="Role">
                        <ControlLabel>Role</ControlLabel>
                        <FormControl componentClass="select" onChange ={this.getInputChangeHandler('role')}
                            value={this.state.role}
                            className="form-control" >
                                {this.getRolesOptions()}
                        </FormControl>
                    </FormGroup>

                    <FormGroup controlId="Production">
                        <ControlLabel>Production</ControlLabel>
                        <FormControl componentClass="select" onChange ={this.getInputChangeHandler('production')}
                            value={converter.convertToDisplay(this.state.production)}
                            className="form-control" >
                                {
                                    ['Yes', 'No'].map(
                                    (option)=> <option value={option} key={option}>{option}</option>
                                    )
                                }
                        </FormControl>
                    </FormGroup>

                    <FormGroup controlId="email">
                        <ControlLabel>User Email</ControlLabel>
                        <FormControl componentClass="textarea" onChange={this.getInputChangeHandler('email')}
                            value={this.state.email}
                            className="form-control" placeholder="Please enter all volunteer emails - one in each line"></FormControl>
                    </FormGroup>
                    {this.getErrorText()}
                </Modal.Body>
            <Modal.Footer>
                <Button onClick={this.handleClose}>Close</Button>
                <Button bsStyle="primary" onClick={this.handleSubmit} disabled={!this.state.isButtonEnabled}>Add Volunteer</Button>
            </Modal.Footer>
            </Modal>
        )
    }
}<|endoftext|>


<commit_before>var sys        = require('sys')
var exec       = require('child_process').exec;
var canRun = true;
var counter = 1;

function onJavaReturn(error, stdout, stderr) {

	// console.log("./Data/ImageQuery/query"+counter+".jpg");
	// console.log(stdout);
	var result = JSON.parse(stdout);
	var hit = 0;
	for(var j = 0; j < result.length; j++){
		var imageName = result[j];
		var name = imageName.split(".")[0];
		if (name > 20*(counter-1) && name <= counter*20){
			hit++;
		}
	}
	var P = hit*1.0/result.length;
	var R = hit*1.0/20;
	console.log(counter+"\t"+hit+"\t"+R.toFixed(2)+"\t"+P.toFixed(2)+"\t"+((2*P*R)/(P+R)).toFixed(2));
	// console.log("query\t"+counter);
	// console.log("Hits: \t"+ hit);
	// console.log("Recall: \t"+ R);
	// console.log("Precision: \t"+P);
	// console.log("F1: \t"+(2*P*R)/(P+R));

	counter++;
	if(counter <= 20){
		exec("java -jar -Xmx4g ImageQuery.jar ./Data/ImageQuery/query"+counter+".jpg 20", onJavaReturn);
	}
}

	console.log("Query\tHits\tRecall\tPrecision\tFi")
		exec("java -jar -Xmx4g ImageQuery.jar ./Data/ImageQuery/query"+counter+".jpg 20", onJavaReturn);
<commit_msg>updated SampleQuery.js to accept args<commit_after>var sys        = require('sys')
var exec       = require('child_process').exec;
var canRun = true;
var counter = 1;

function onJavaReturn(error, stdout, stderr) {
  console.log(stderr);
  var result = JSON.parse(stdout);
  var hit = 0;
  for(var j = 0; j < result.length; j++){
    var imageName = result[j];
    var name = imageName.split(".")[0];
    name = parseInt(name, 10);
    if (name > 20*(counter-1) && name <= counter*20){
      hit++;
    }
  }
  var P = hit*1.0/result.length;
  var R = hit*1.0/20;
  var F1 = (2*P*R)/(P+R);
  console.log(counter+"\t"+hit+"\t"+R.toFixed(3)+"\t"+P.toFixed(3)+"\t\t"+F1.toFixed(3));

  counter ++;
  if (counter <= 20) {
    exec("java -Xmx4g -cp " + jarFile + ":lib/gson-2.2.4.jar cs3246/a2/web/WebServiceHandler " + imagePath + "query" + counter + ".jpg " + numResult, {cwd: cwd}, onJavaReturn);
  }
}

console.log("Running SampleQuery.js \n");
// print process.argv
process.argv.forEach(function(val, index, array) {
   console.log(index + ': ' + val);
});

var cwd = process.argv[2];
var jarFile = process.argv[3];
var imagePath = process.argv[4];
var numResult = process.argv[5];

console.log("Query\tHits\tRecall\tPrecision\tFi");
exec("java -Xmx4g -cp " + jarFile + ":lib/gson-2.2.4.jar cs3246/a2/web/WebServiceHandler " + imagePath + "query" + counter + ".jpg " + numResult, {cwd: cwd}, onJavaReturn);
<|endoftext|>


<commit_before>/* eslint-env jest */

import React from 'react';
import { mount } from 'enzyme';

import TabPanel from './TabPanel';

describe('Tab panel component', () => {
  const instanceToRender = (
    <TabPanel>
      <TabPanel.Tab name="test1">

Test Content 1

      </TabPanel.Tab>
      <TabPanel.Tab name="test2">

Test Content 2

      </TabPanel.Tab>
      <TabPanel.TabList name="testList">

Test Content 3

Test Content 4

      </TabPanel.TabList>
    </TabPanel>
  );

  const complexInstanceToRender = (
    <TabPanel>
      <TabPanel.TabList name="testList">

Test Content 3

Test Content 4

      </TabPanel.TabList>
      <TabPanel.Tab name="test1">

Test Content 1

      </TabPanel.Tab>
      <TabPanel.Tab name="test2">

Test Content 2

      </TabPanel.Tab>
    </TabPanel>
  );

  it('Will select first child on load', () => {
    const tabPanelInstance = mount(instanceToRender);
    expect(tabPanelInstance.state('value')).toBe('test1');
  });

  it('Will select first list item when rendering', () => {
    const tabPanelInstance = mount(complexInstanceToRender);
    expect(tabPanelInstance.state('value')).toBe('test3');
  });

  it('Properly selects a list', () => {
    const tabPanelInstance = mount(instanceToRender);
    tabPanelInstance.setState({ value: 'test3' });
    expect(tabPanelInstance.find({ name: 'testList' }).props().selected).toBe(
      true
    );
  });

  it('Properly selects a tab', () => {
    const tabPanelInstance = mount(instanceToRender);
    tabPanelInstance.setState({ value: 'test2' });
    expect(tabPanelInstance.find({ name: 'test2' }).props().selected).toBe(
      true
    );
  });

  it('Clicks on tab select them', () => {
    const tabPanelInstance = mount(instanceToRender);
    const tab = tabPanelInstance.find({ name: 'test2' });
    const tabButton = tab.find('button');
    tabButton.simulate('click');
    expect(tabPanelInstance.state('value')).toBe('test2');
  });
});
<commit_msg>Fix the model in the spec for tabpanels<commit_after>/* eslint-env jest */

import React from 'react';
import { mount } from 'enzyme';

import TabPanel from './TabPanel';

describe('Tab panel component', () => {
  const instanceToRender = (
    <TabPanel optionKeyFunc={x => x.replace(/\s/g, '')}>
      <TabPanel.Tab name="test1">

Test Content 1

      </TabPanel.Tab>
      <TabPanel.Tab name="test2">

Test Content 2

      </TabPanel.Tab>
      <TabPanel.TabList name="testList">

Test Content 3

Test Content 4

      </TabPanel.TabList>
    </TabPanel>
  );

  const complexInstanceToRender = (
    <TabPanel optionKeyFunc={x => x.replace(/\s/g, '')}>
      <TabPanel.TabList name="testList">

Test Content 3

Test Content 4

      </TabPanel.TabList>
      <TabPanel.Tab name="test1">

Test Content 1

      </TabPanel.Tab>
      <TabPanel.Tab name="test2">

Test Content 2

      </TabPanel.Tab>
    </TabPanel>
  );

  it('Will select first child on load', () => {
    const tabPanelInstance = mount(instanceToRender);
    expect(tabPanelInstance.state('value')).toBe('test1');
  });

  it('Will select first list item when rendering', () => {
    const tabPanelInstance = mount(complexInstanceToRender);
    expect(tabPanelInstance.state('value')).toBe('test3');
  });

  it('Properly selects a list', () => {
    const tabPanelInstance = mount(instanceToRender);
    tabPanelInstance.setState({ value: 'test3' });
    expect(tabPanelInstance.find({ name: 'testList' }).props().selected).toBe(
      true
    );
  });

  it('Properly selects a tab', () => {
    const tabPanelInstance = mount(instanceToRender);
    tabPanelInstance.setState({ value: 'test2' });
    expect(tabPanelInstance.find({ name: 'test2' }).props().selected).toBe(
      true
    );
  });

  it('Clicks on tab select them', () => {
    const tabPanelInstance = mount(instanceToRender);
    const tab = tabPanelInstance.find({ name: 'test2' });
    const tabButton = tab.find('button');
    tabButton.simulate('click');
    expect(tabPanelInstance.state('value')).toBe('test2');
  });
});
<|endoftext|>


<commit_before>/*global PushNotification:false */
'use strict';

var WEEK_IN_MILLIS = 7 * 24 * 60 * 60 * 1000;
angular.module('coreGamesIonicUi.services').factory('jtbPushNotifications',
    ['$http', '$rootScope', '$window', 'jtbLocalStorage', 'jtbPlayerService',
        function ($http, $rootScope, $window, jtbLocalStorage, jtbPlayerService) {

            var deviceToken;

            function registerToken(token) {
                console.log('register token called with ' + JSON.stringify(token));
                if (angular.isDefined(token) && token != null && token.length > 0 && token !== 'OK') {
                    console.log('Checking..');
                    var keyBase = 'pushNotification-' + jtbPlayerService.currentPlayer().md5;
                    var keyToken = keyBase + '-token';
                    var keyRegistered = keyBase + '-last';
                    var lastToken = jtbLocalStorage.get(keyToken, '');
                    var lastReg = new Date(parseInt(jtbLocalStorage.get(keyRegistered, '0')));
                    console.log('Last reg ' + lastReg.toLocaleString());
                    var lastRegDiff = lastReg - new Date();
                    if (lastRegDiff > WEEK_IN_MILLIS || token !== lastToken) {
                        console.log('Registering');
                        $http.put('/api/notifications/register/' + token).success(function () {
                            jtbLocalStorage.set(keyToken, token);
                            jtbLocalStorage.set(keyRegistered, new Date().getTime());
                            deviceToken = token;
                            console.log('registered device');
                        }).error(function (error) {
                            console.error('failed to register device ' + JSON.stringify(error));
                            //  TODO
                        });
                    } else {
                        console.log('Recently registered.');
                    }
                } else {
                    console.log('Not registering');
                }
            }

            function handleNotification(notification) {
                console.log(JSON.stringify(notification));
            }

            function handleError(error) {
                console.log('error in push ' + JSON.stringify(error));
            }

            function handleRegistration(data) {
                console.log('registration event ' + JSON.stringify(data));
                registerToken(data.registrationId);
            }

            var pushNotification, pushInstance;
            try {
                if ($window.location.href.indexOf('file:') === 0 && angular.isDefined($window.PushNotification)) {
                    pushNotification = PushNotification;
                }
            } catch (ex) {
                pushNotification = undefined;
            }

            $rootScope.$on('playerLoaded', function () {
                $http.get('/api/notifications/senderID').success(function (id) {
                    if (angular.isDefined(pushNotification) && id !== 'NOTSET') {
                        var config;
                        //  TODO
                        config = {
                            android: {
                                senderId: id,
                                sound: false,
                                vibrate: true
                            },
                            ios: {
                                senderId: id,
                                alert: true,
                                badge: true,
                                sound: false,
                                clearBadge: true
                            },
                            windows: {

                            }
                        };
                        pushInstance = pushNotification.init(config);
                        pushInstance.on('registration', handleRegistration);
                        pushInstance.on('notification', handleNotification);
                        pushInstance.on('error', handleError);
                    } else {
                        console.log('No pushNotification defined');
                    }
                }).error(function (error) {
                    console.log('Not able to get senderID ' + JSON.stringify(error));
                    //  TODO
                });
            });

            return {
                register: function () {
                }
            };
        }
    ]
);
<commit_msg>senderId -> senderID<commit_after>/*global PushNotification:false */
'use strict';

var WEEK_IN_MILLIS = 7 * 24 * 60 * 60 * 1000;
angular.module('coreGamesIonicUi.services').factory('jtbPushNotifications',
    ['$http', '$rootScope', '$window', 'jtbLocalStorage', 'jtbPlayerService',
        function ($http, $rootScope, $window, jtbLocalStorage, jtbPlayerService) {

            var deviceToken;

            function registerToken(token) {
                console.log('register token called with ' + JSON.stringify(token));
                if (angular.isDefined(token) && token != null && token.length > 0 && token !== 'OK') {
                    console.log('Checking..');
                    var keyBase = 'pushNotification-' + jtbPlayerService.currentPlayer().md5;
                    var keyToken = keyBase + '-token';
                    var keyRegistered = keyBase + '-last';
                    var lastToken = jtbLocalStorage.get(keyToken, '');
                    var lastReg = new Date(parseInt(jtbLocalStorage.get(keyRegistered, '0')));
                    console.log('Last reg ' + lastReg.toLocaleString());
                    var lastRegDiff = lastReg - new Date();
                    if (lastRegDiff > WEEK_IN_MILLIS || token !== lastToken) {
                        console.log('Registering');
                        $http.put('/api/notifications/register/' + token).success(function () {
                            jtbLocalStorage.set(keyToken, token);
                            jtbLocalStorage.set(keyRegistered, new Date().getTime());
                            deviceToken = token;
                            console.log('registered device');
                        }).error(function (error) {
                            console.error('failed to register device ' + JSON.stringify(error));
                            //  TODO
                        });
                    } else {
                        console.log('Recently registered.');
                    }
                } else {
                    console.log('Not registering');
                }
            }

            function handleNotification(notification) {
                console.log(JSON.stringify(notification));
            }

            function handleError(error) {
                console.log('error in push ' + JSON.stringify(error));
            }

            function handleRegistration(data) {
                console.log('registration event ' + JSON.stringify(data));
                registerToken(data.registrationId);
            }

            var pushNotification, pushInstance;
            try {
                if ($window.location.href.indexOf('file:') === 0 && angular.isDefined($window.PushNotification)) {
                    pushNotification = PushNotification;
                }
            } catch (ex) {
                pushNotification = undefined;
            }

            $rootScope.$on('playerLoaded', function () {
                $http.get('/api/notifications/senderID').success(function (id) {
                    if (angular.isDefined(pushNotification) && id !== 'NOTSET') {
                        var config;
                        //  TODO
                        config = {
                            android: {
                                senderID: id,
                                sound: false,
                                vibrate: true
                            },
                            ios: {
                                senderID: id,
                                alert: true,
                                badge: true,
                                sound: false,
                                clearBadge: true
                            },
                            windows: {

                            }
                        };
                        pushInstance = pushNotification.init(config);
                        pushInstance.on('registration', handleRegistration);
                        pushInstance.on('notification', handleNotification);
                        pushInstance.on('error', handleError);
                    } else {
                        console.log('No pushNotification defined');
                    }
                }).error(function (error) {
                    console.log('Not able to get senderID ' + JSON.stringify(error));
                    //  TODO
                });
            });

            return {
                register: function () {
                }
            };
        }
    ]
);
<|endoftext|>


<commit_before>import React from 'react';

import TickIcon from 'Icons/Tick';
import CrossIcon from 'Icons/Cross';
import ChevronIcon from 'Icons/Chevron';
import InformationIcon from 'Icons/Information';

import Analyzer from 'Parser/Core/Analyzer';
import Expandable from 'Main/Expandable';
import SpellLink from 'common/SpellLink';
import { formatNumber, formatPercentage, formatThousands } from 'common/format';
import Wrapper from 'common/Wrapper';

export function performanceForThresholds(thresholds) {
  if (thresholds.isGreaterThan) {
    if (typeof thresholds.isGreaterThan === 'object') {
      return performanceForGreaterThanThresholds(thresholds.actual, thresholds.isGreaterThan);
    } else {
      return thresholds.isGreaterThan / thresholds.actual;
    }
  } else if (thresholds.isLessThan) {
    if (typeof thresholds.isLessThan === 'object') {
      return performanceForLessThanThresholds(thresholds.actual, thresholds.isLessThan);
    } else {
      return thresholds.actual / thresholds.isLessThan;
    }
  } else if (thresholds.is !== undefined) {
    return thresholds.actual !== thresholds.is ? 1 : 0;
  } else {
    throw new Error('Failed to recognize threshold type');
  }
}
/**
 *   0 - 33% major This is different from the *minor* threshold which is at 100% instead of 66%. The reason for this is that the minor threshold being at 75% and then 75%-100% being minor - max is that this would suggest going for max is best while this is not always the case. Something like Crusader Strike (with the Crusader's Might talent) has a recommended cast efficiency of 35% *because* you should only cast it enough to benefit you, more than that would be good but not 100% cast efficiency as then you're losing healing.
 * 33% - 66% average
 * 66% - 99% minor
 * 100% good (no issue)
 * @param actual
 * @param minor
 * @param average
 * @param major
 * @returns {number}
 */
export function performanceForLessThanThresholds(actual, { minor, average, major } ) {
  if (actual >= minor) {
    // no issue
    return 1;
  }
  if (actual >= average) {
    // minor issue (between average and minor issue)
    return 0.666 + 0.333 * ((actual - average) / (minor - average));
  }
  if (actual >= major) {
    // average issue (between major and average issue)
    return 0.333 + 0.333 * ((actual - major) / (average - major));
  }
  // major issue
  return 0.333 * actual / major;
}
export function performanceForGreaterThanThresholds(actual, { minor, average, major } ) {
  if (actual <= minor) {
    // no issue
    return 1;
  }
  if (actual < average) {
    // minor issue (between average and minor issue)
    return 0.666 + 0.333 * ((actual - minor) / (average - minor));
  }
  if (actual < major) {
    // average issue (between major and average issue)
    return 0.333 + 0.333 * ((actual - average) / (major - average));
  }
  // major issue
  return 0.333 * major / actual;
}
function colorForPerformance(performance) {
  let color = null;
  if (performance >= 1) {
    color = 'Hunter-bg';
  } else if (performance > 0.666) {
    color = 'Mage-bg';
  } else if (performance > 0.333) {
    color = 'Druid-bg';
  } else if (performance <= 0.333) {
    color = 'DeathKnight-bg';
  }
  return color;
}
function formatThresholdsActual(thresholds) {
  switch (thresholds.style) {
    case 'percentage':
      return `${formatPercentage(thresholds.actual)}%`;
    case 'number':
      return `${formatNumber(thresholds.actual)}`;
    case 'thousands':
      return `${formatThousands(thresholds.actual)}`;
    case 'boolean':
      return thresholds.actual ? 'Yes' : 'No';
    default:
      throw new Error(`Unknown style: ${thresholds.style}`);
  }
}
function calculateMedian(values) {
  const arr = [...values];
  arr.sort((a, b) => a - b);

  const half = Math.floor(arr.length / 2);

  if (arr.length % 2) {
    return arr[half];
  } else {
    return (arr[half - 1] + arr[half]) / 2.0;
  }
}

export class Rule {
  name = null;
  requirements = null;
  when = null;
  constructor(options) {
    Object.keys(options).forEach(key => {
      this[key] = options[key];
    });
  }
}

export class Requirement {
  name = null;
  check = null;
  when = null;
  constructor(options) {
    Object.keys(options).forEach(key => {
      this[key] = options[key];
    });
  }
}

export class GenericCastEfficiencyRequirement extends Requirement {
  constructor({ spell, ...others }) {
    super({
      name: <SpellLink id={spell.id} icon />,
      check: function () {
        const { efficiency, recommendedEfficiency: minor, averageIssueEfficiency: average, majorIssueEfficiency: major } = this.castEfficiency.getCastEfficiencyForSpellId(spell.id);
        return {
          actual: efficiency,
          isLessThan: {
            minor,
            average,
            major,
          },
          style: 'percentage',
        };
      },
      ...others,
    });
  }
}

class Checklist extends Analyzer {
  rules = [];

  constructor(...args) {
    super(...args);
    this.whenFilter = this.whenFilter.bind(this);
    this.renderRule = this.renderRule.bind(this);
  }

  whenFilter(rule) {
    if (rule.when === null) {
      // Rule isn't conditional
      return true;
    }
    if (typeof rule.when === 'function') {
      return rule.when.call(this);
    }

    return !!rule.when;
  }
  renderRule(rule) {
    const requirements = rule.requirements.call(this)
      .filter(this.whenFilter)
      .map(requirement => {
        const thresholds = requirement.check.call(this);
        const performance = performanceForThresholds(thresholds);
        return {
          requirement,
          thresholds,
          performance,
        };
      });

    const requirementPerformances = requirements.map(requirement => requirement.performance);
    // const lowest = requirementPerformances.reduce((lowest, performance) => Math.min(lowest, performance), Infinity);
    // const total = requirementPerformances.reduce((total, performance) => total + performance, 0);
    // const average = total / requirementPerformances.length;
    // Lowest would be too punishing for small mistakes, if you want to have a single value tank the rule consider making it its own rule.
    // Average would mark things as OK when one thing was OK and 3 things were "average", I think this is wrong and it should mark the rule as average. Median achieves this.
    const median = calculateMedian(requirementPerformances);

    const rulePerformance = median;

    return (
      <Expandable
        key={rule.name}
        header={(

{/* this div ensures vertical alignment */}
                {rulePerformance > 0.666 ? <TickIcon style={{ color: 'green' }} /> : <CrossIcon style={{ color: 'red' }} />}

              {rule.name}

                <div
                  className={`performance-bar small ${colorForPerformance(rulePerformance)}`}
                  style={{ width: `${rulePerformance * 100}%`, transition: 'background-color 800ms' }}
                />

{/* this div ensures vertical alignment */}
                <ChevronIcon />

        )}
      >
        {rule.description && (

{/* this div ensures vertical alignment */}
                    <InformationIcon />

                  {rule.description}

        )}

          {requirements.map(({ requirement, thresholds, performance }) => (

                  {requirement.name}

                  {thresholds.prefix} {formatThresholdsActual(thresholds)} {thresholds.suffix}

                    <div
                      className={`performance-bar small ${colorForPerformance(performance)}`}
                      style={{ width: `${performance * 100}%`, transition: 'background-color 800ms' }}
                    />

          ))}

      </Expandable>
    );
  }

  render({ footer }) {
    return (
      <Wrapper>
        {this.rules.length === 0 && (

            Sorry, the checklist for this spec has not been implemented yet.

        )}
        {this.rules
          .filter(this.whenFilter)
          .map(this.renderRule)}
        {footer && (

            {footer}

        )}
      </Wrapper>
    );
  }
}

export default Checklist;
<commit_msg>Fix duplicate keys warning (due to name sometimes being a React node)<commit_after>import React from 'react';

import TickIcon from 'Icons/Tick';
import CrossIcon from 'Icons/Cross';
import ChevronIcon from 'Icons/Chevron';
import InformationIcon from 'Icons/Information';

import Analyzer from 'Parser/Core/Analyzer';
import Expandable from 'Main/Expandable';
import SpellLink from 'common/SpellLink';
import { formatNumber, formatPercentage, formatThousands } from 'common/format';
import Wrapper from 'common/Wrapper';

export function performanceForThresholds(thresholds) {
  if (thresholds.isGreaterThan) {
    if (typeof thresholds.isGreaterThan === 'object') {
      return performanceForGreaterThanThresholds(thresholds.actual, thresholds.isGreaterThan);
    } else {
      return thresholds.isGreaterThan / thresholds.actual;
    }
  } else if (thresholds.isLessThan) {
    if (typeof thresholds.isLessThan === 'object') {
      return performanceForLessThanThresholds(thresholds.actual, thresholds.isLessThan);
    } else {
      return thresholds.actual / thresholds.isLessThan;
    }
  } else if (thresholds.is !== undefined) {
    return thresholds.actual !== thresholds.is ? 1 : 0;
  } else {
    throw new Error('Failed to recognize threshold type');
  }
}
/**
 *   0 - 33% major This is different from the *minor* threshold which is at 100% instead of 66%. The reason for this is that the minor threshold being at 75% and then 75%-100% being minor - max is that this would suggest going for max is best while this is not always the case. Something like Crusader Strike (with the Crusader's Might talent) has a recommended cast efficiency of 35% *because* you should only cast it enough to benefit you, more than that would be good but not 100% cast efficiency as then you're losing healing.
 * 33% - 66% average
 * 66% - 99% minor
 * 100% good (no issue)
 * @param actual
 * @param minor
 * @param average
 * @param major
 * @returns {number}
 */
export function performanceForLessThanThresholds(actual, { minor, average, major } ) {
  if (actual >= minor) {
    // no issue
    return 1;
  }
  if (actual >= average) {
    // minor issue (between average and minor issue)
    return 0.666 + 0.333 * ((actual - average) / (minor - average));
  }
  if (actual >= major) {
    // average issue (between major and average issue)
    return 0.333 + 0.333 * ((actual - major) / (average - major));
  }
  // major issue
  return 0.333 * actual / major;
}
export function performanceForGreaterThanThresholds(actual, { minor, average, major } ) {
  if (actual <= minor) {
    // no issue
    return 1;
  }
  if (actual < average) {
    // minor issue (between average and minor issue)
    return 0.666 + 0.333 * ((actual - minor) / (average - minor));
  }
  if (actual < major) {
    // average issue (between major and average issue)
    return 0.333 + 0.333 * ((actual - average) / (major - average));
  }
  // major issue
  return 0.333 * major / actual;
}
function colorForPerformance(performance) {
  let color = null;
  if (performance >= 1) {
    color = 'Hunter-bg';
  } else if (performance > 0.666) {
    color = 'Mage-bg';
  } else if (performance > 0.333) {
    color = 'Druid-bg';
  } else if (performance <= 0.333) {
    color = 'DeathKnight-bg';
  }
  return color;
}
function formatThresholdsActual(thresholds) {
  switch (thresholds.style) {
    case 'percentage':
      return `${formatPercentage(thresholds.actual)}%`;
    case 'number':
      return `${formatNumber(thresholds.actual)}`;
    case 'thousands':
      return `${formatThousands(thresholds.actual)}`;
    case 'boolean':
      return thresholds.actual ? 'Yes' : 'No';
    default:
      throw new Error(`Unknown style: ${thresholds.style}`);
  }
}
function calculateMedian(values) {
  const arr = [...values];
  arr.sort((a, b) => a - b);

  const half = Math.floor(arr.length / 2);

  if (arr.length % 2) {
    return arr[half];
  } else {
    return (arr[half - 1] + arr[half]) / 2.0;
  }
}

export class Rule {
  name = null;
  requirements = null;
  when = null;
  constructor(options) {
    Object.keys(options).forEach(key => {
      this[key] = options[key];
    });
  }
}

export class Requirement {
  name = null;
  check = null;
  when = null;
  constructor(options) {
    Object.keys(options).forEach(key => {
      this[key] = options[key];
    });
  }
}

export class GenericCastEfficiencyRequirement extends Requirement {
  constructor({ spell, ...others }) {
    super({
      name: <SpellLink id={spell.id} icon />,
      check: function () {
        const { efficiency, recommendedEfficiency: minor, averageIssueEfficiency: average, majorIssueEfficiency: major } = this.castEfficiency.getCastEfficiencyForSpellId(spell.id);
        return {
          actual: efficiency,
          isLessThan: {
            minor,
            average,
            major,
          },
          style: 'percentage',
        };
      },
      ...others,
    });
  }
}

class Checklist extends Analyzer {
  rules = [];

  constructor(...args) {
    super(...args);
    this.whenFilter = this.whenFilter.bind(this);
    this.renderRule = this.renderRule.bind(this);
  }

  whenFilter(rule) {
    if (rule.when === null) {
      // Rule isn't conditional
      return true;
    }
    if (typeof rule.when === 'function') {
      return rule.when.call(this);
    }

    return !!rule.when;
  }
  renderRule(rule, index) {
    const requirements = rule.requirements.call(this)
      .filter(this.whenFilter)
      .map(requirement => {
        const thresholds = requirement.check.call(this);
        const performance = performanceForThresholds(thresholds);
        return {
          requirement,
          thresholds,
          performance,
        };
      });

    const requirementPerformances = requirements.map(requirement => requirement.performance);
    // const lowest = requirementPerformances.reduce((lowest, performance) => Math.min(lowest, performance), Infinity);
    // const total = requirementPerformances.reduce((total, performance) => total + performance, 0);
    // const average = total / requirementPerformances.length;
    // Lowest would be too punishing for small mistakes, if you want to have a single value tank the rule consider making it its own rule.
    // Average would mark things as OK when one thing was OK and 3 things were "average", I think this is wrong and it should mark the rule as average. Median achieves this.
    const median = calculateMedian(requirementPerformances);

    const rulePerformance = median;

    return (
      <Expandable
        key={index}
        header={(

{/* this div ensures vertical alignment */}
                {rulePerformance > 0.666 ? <TickIcon style={{ color: 'green' }} /> : <CrossIcon style={{ color: 'red' }} />}

              {rule.name}

                <div
                  className={`performance-bar small ${colorForPerformance(rulePerformance)}`}
                  style={{ width: `${rulePerformance * 100}%`, transition: 'background-color 800ms' }}
                />

{/* this div ensures vertical alignment */}
                <ChevronIcon />

        )}
      >
        {rule.description && (

{/* this div ensures vertical alignment */}
                    <InformationIcon />

                  {rule.description}

        )}

          {requirements.map(({ requirement, thresholds, performance }) => (

                  {requirement.name}

                  {thresholds.prefix} {formatThresholdsActual(thresholds)} {thresholds.suffix}

                    <div
                      className={`performance-bar small ${colorForPerformance(performance)}`}
                      style={{ width: `${performance * 100}%`, transition: 'background-color 800ms' }}
                    />

          ))}

      </Expandable>
    );
  }

  render({ footer }) {
    return (
      <Wrapper>
        {this.rules.length === 0 && (

            Sorry, the checklist for this spec has not been implemented yet.

        )}
        {this.rules
          .filter(this.whenFilter)
          .map(this.renderRule)}
        {footer && (

            {footer}

        )}
      </Wrapper>
    );
  }
}

export default Checklist;
<|endoftext|>


<commit_before>// WebTorrent Desktop server API
// - Tell auto-updaters the latest version
// - Log crash reports
// - Log telemetry
module.exports = { serve }

const bodyParser = require('body-parser')
const cp = require('child_process')
const express = require('express')
const fs = require('fs')
const mkdirp = require('mkdirp')
const multer = require('multer')
const path = require('path')
const semver = require('semver')
const url = require('url')

const auth = require('./auth')
const config = require('../config')
const secret = require('../secret')

var DESKTOP_VERSION = config.desktopVersion
var RELEASES_URL = 'https://github.com/feross/webtorrent-desktop/releases/download'

var TELEMETRY_PATH = path.join(config.logPath, 'telemetry')
var CRASH_REPORTS_PATH = path.join(config.logPath, 'crash-reports')

var SUMMARIZE_PATH = path.join(__dirname, '..', 'bin', 'summarize-telemetry.js')

// Attempt to create the needed log folders
try { mkdirp.sync(TELEMETRY_PATH) } catch (err) {}
try { mkdirp.sync(CRASH_REPORTS_PATH) } catch (err) {}

/**
 * Summarize the telemetry into summary.json, every once in a while. Takes
 * a bit of time to crunch, so we do it in the background. The dashboard uses
 * the data in summary.json.
 *
 * This is a poor-man's cron-job :) It's nice because it keeps everything in
 * this repo, though.
 */
summarizeTelemetry()
setInterval(summarizeTelemetry, 24 * 3600 * 1000)

function serve (app) {
  serveTelemetryAPI(app)
  serveCrashReportsAPI(app)
  serveAnnouncementsAPI(app)
  serveUpdateAPI(app)
}

// Log telemetry JSON summaries to a file, one per line
function serveTelemetryAPI (app) {
  app.post('/desktop/telemetry', bodyParser.json(), function (req, res) {
    var summary = req.body
    summary.ip = req.ip
    var summaryJSON = JSON.stringify(summary)

    var today = new Date().toISOString().substring(0, 10) // YYYY-MM-DD
    var telemetryPath = path.join(TELEMETRY_PATH, today + '.log')

    fs.appendFile(telemetryPath, summaryJSON + '\n', function (err) {
      if (err) {
        console.error('Error saving telemetry: ' + err.message)
        res.status(500)
      }
      res.end()
    })
  })

  app.use('/desktop/telemetry/', [
    auth(secret.credentials),
    serveTelemetryDashboard,
    express.static(TELEMETRY_PATH)
  ])
}

// Summarize telemetry information: active users, monthly growth, most common errors, etc
function serveTelemetryDashboard (req, res, next) {
  if (req.url !== '/') return next()
  var orig = req.originalUrl // eg '/desktop/telemetry'
  if (!orig.endsWith('/')) return res.redirect(orig + '/')

  // Once we get here, we're serving this exact path: /desktop/telemetry/
  fs.readdir(TELEMETRY_PATH, function (err, files) {
    if (err) return res.status(500).send(err.message)

    var filesByMonth = []
    files.forEach(function (file, i) {
      if (i === 0 || file.substring(0, 7) !== files[i - 1].substring(0, 7)) {
        filesByMonth.push([])
      }
      filesByMonth[filesByMonth.length - 1].push(file)
    })

    var summaryPath = path.join(TELEMETRY_PATH, 'summary.json')
    fs.readFile(summaryPath, 'utf8', function (err, summaryJSON) {
      var summary = err
        ? { telemetry: [] }
        : JSON.parse(summaryJSON)
      if (summary.telemetry && summary.telemetry.length > 0) {
        var today = summary.telemetry[summary.telemetry.length - 1]
        var tMinus7 = summary.telemetry[summary.telemetry.length - 8]
        var percentWeeklyGrowth =
          (100 * today.actives.day7 / tMinus7.actives.day7 - 100).toFixed(1)
      }
      res.render('telemetry-dashboard',
        {filesByMonth, summary, percentWeeklyGrowth})
    })
  })
}

// Save electron process crash reports (from Crashpad), each in its own file
function serveCrashReportsAPI (app) {
  var upload = multer({ dest: CRASH_REPORTS_PATH }).single('upload_file_minidump')

  app.post('/desktop/crash-report', upload, function (req, res) {
    req.body.filename = req.file.filename
    var crashLog = JSON.stringify(req.body, undefined, 2)

    fs.writeFile(req.file.path + '.json', crashLog, function (err) {
      if (err) {
        console.error('Error saving crash report: ' + err.message)
        res.status(500)
      }
      res.end()
    })
  })
}

// This lets us send a message to all WebTorrent Desktop users
function serveAnnouncementsAPI (app) {
  app.get('/desktop/announcement', function (req, res) {
    res.status(204).end()
  })
}

// Tell the auto updaters when new version is available
function serveUpdateAPI (app) {
  // Deprecated: WebTorrent Desktop v0.0.0 - 0.2.0 use this update URL
  app.get('/app/update/?*', function (req, res) {
    res.redirect(301, req.url.replace('/app/', '/desktop/'))
  })

  // WebTorrent Desktop Mac auto-update endpoint
  app.get('/desktop/update', function (req, res) {
    var version = req.query.version
    logUpdateCheck({
      date: (new Date()).toString(),
      platform: req.query.platform,
      version: version,
      ip: req.ip
    })
    if (!semver.valid(version) || semver.lt(version, DESKTOP_VERSION)) {
      // Update is required. Send update JSON.
      // Response format docs: https://github.com/Squirrel/Squirrel.Mac#update-json-format
      res.status(200).send({
        name: 'WebTorrent v' + DESKTOP_VERSION,
        url: `${RELEASES_URL}/v${DESKTOP_VERSION}/WebTorrent-v${DESKTOP_VERSION}-darwin.zip`,
        version: DESKTOP_VERSION
      })
    } else {
      // No update required. User is on latest app version.
      res.status(204).end()
    }
  })

  // WebTorrent Desktop Windows auto-update endpoint
  app.get('/desktop/update/*', function (req, res) {
    var pathname = url.parse(req.url).pathname
    var file = pathname.replace(/^\/desktop\/update\//i, '')
    var fileVersion
    if (file === 'RELEASES') {
      fileVersion = DESKTOP_VERSION
      logUpdateCheck({
        date: (new Date()).toString(),
        platform: req.query.platform,
        version: req.query.version,
        ip: req.ip
      })
    } else {
      var match = /-(\d+\.\d+\.\d+)-/.exec(file)
      fileVersion = match && match[1]
    }
    if (!fileVersion) {
      return res.status(404).end()
    }
    var redirectURL = `${RELEASES_URL}/v${fileVersion}/${file}`
    res.redirect(302, redirectURL)
  })
}

function logUpdateCheck (log) {
  console.log('UPDATE CHECK: ' + JSON.stringify(log))
}

function summarizeTelemetry () {
  var child = cp.spawn(SUMMARIZE_PATH, [])
  child.on('close', function (code) {
    console.log(`Summarize telemetry script exited with code: ${code}`)
  })
}
<commit_msg>Telemetry: summarize every 3h<commit_after>// WebTorrent Desktop server API
// - Tell auto-updaters the latest version
// - Log crash reports
// - Log telemetry
module.exports = { serve }

const bodyParser = require('body-parser')
const cp = require('child_process')
const express = require('express')
const fs = require('fs')
const mkdirp = require('mkdirp')
const multer = require('multer')
const path = require('path')
const semver = require('semver')
const url = require('url')

const auth = require('./auth')
const config = require('../config')
const secret = require('../secret')

var DESKTOP_VERSION = config.desktopVersion
var RELEASES_URL = 'https://github.com/feross/webtorrent-desktop/releases/download'

var TELEMETRY_PATH = path.join(config.logPath, 'telemetry')
var CRASH_REPORTS_PATH = path.join(config.logPath, 'crash-reports')

var SUMMARIZE_PATH = path.join(__dirname, '..', 'bin', 'summarize-telemetry.js')

// Attempt to create the needed log folders
try { mkdirp.sync(TELEMETRY_PATH) } catch (err) {}
try { mkdirp.sync(CRASH_REPORTS_PATH) } catch (err) {}

/**
 * Summarize the telemetry into summary.json, every three hours. Takes
 * a bit of time to crunch, so we do it in the background. The dashboard uses
 * the data in summary.json.
 *
 * This is a poor-man's cron-job. Not running in development by default to
 * avoid hitting Github's API too often.
 */
if (process.env.NODE_ENV === 'production') {
  summarizeTelemetry()
  setInterval(summarizeTelemetry, 3 * 3600 * 1000)
}

function serve (app) {
  serveTelemetryAPI(app)
  serveCrashReportsAPI(app)
  serveAnnouncementsAPI(app)
  serveUpdateAPI(app)
}

// Log telemetry JSON summaries to a file, one per line
function serveTelemetryAPI (app) {
  app.post('/desktop/telemetry', bodyParser.json(), function (req, res) {
    var summary = req.body
    summary.ip = req.ip
    var summaryJSON = JSON.stringify(summary)

    var today = new Date().toISOString().substring(0, 10) // YYYY-MM-DD
    var telemetryPath = path.join(TELEMETRY_PATH, today + '.log')

    fs.appendFile(telemetryPath, summaryJSON + '\n', function (err) {
      if (err) {
        console.error('Error saving telemetry: ' + err.message)
        res.status(500)
      }
      res.end()
    })
  })

  app.use('/desktop/telemetry/', [
    auth(secret.credentials),
    serveTelemetryDashboard,
    express.static(TELEMETRY_PATH)
  ])
}

// Summarize telemetry information: active users, monthly growth, most common errors, etc
function serveTelemetryDashboard (req, res, next) {
  if (req.url !== '/') return next()
  var orig = req.originalUrl // eg '/desktop/telemetry'
  if (!orig.endsWith('/')) return res.redirect(orig + '/')

  // Once we get here, we're serving this exact path: /desktop/telemetry/
  fs.readdir(TELEMETRY_PATH, function (err, files) {
    if (err) return res.status(500).send(err.message)

    var filesByMonth = []
    files.forEach(function (file, i) {
      if (i === 0 || file.substring(0, 7) !== files[i - 1].substring(0, 7)) {
        filesByMonth.push([])
      }
      filesByMonth[filesByMonth.length - 1].push(file)
    })

    var summaryPath = path.join(TELEMETRY_PATH, 'summary.json')
    fs.readFile(summaryPath, 'utf8', function (err, summaryJSON) {
      var summary = err
        ? { telemetry: [] }
        : JSON.parse(summaryJSON)
      if (summary.telemetry && summary.telemetry.length > 0) {
        var today = summary.telemetry[summary.telemetry.length - 1]
        var tMinus7 = summary.telemetry[summary.telemetry.length - 8]
        var percentWeeklyGrowth =
          (100 * today.actives.day7 / tMinus7.actives.day7 - 100).toFixed(1)
      }
      res.render('telemetry-dashboard',
        {filesByMonth, summary, percentWeeklyGrowth})
    })
  })
}

// Save electron process crash reports (from Crashpad), each in its own file
function serveCrashReportsAPI (app) {
  var upload = multer({ dest: CRASH_REPORTS_PATH }).single('upload_file_minidump')

  app.post('/desktop/crash-report', upload, function (req, res) {
    req.body.filename = req.file.filename
    var crashLog = JSON.stringify(req.body, undefined, 2)

    fs.writeFile(req.file.path + '.json', crashLog, function (err) {
      if (err) {
        console.error('Error saving crash report: ' + err.message)
        res.status(500)
      }
      res.end()
    })
  })
}

// This lets us send a message to all WebTorrent Desktop users
function serveAnnouncementsAPI (app) {
  app.get('/desktop/announcement', function (req, res) {
    res.status(204).end()
  })
}

// Tell the auto updaters when new version is available
function serveUpdateAPI (app) {
  // Deprecated: WebTorrent Desktop v0.0.0 - 0.2.0 use this update URL
  app.get('/app/update/?*', function (req, res) {
    res.redirect(301, req.url.replace('/app/', '/desktop/'))
  })

  // WebTorrent Desktop Mac auto-update endpoint
  app.get('/desktop/update', function (req, res) {
    var version = req.query.version
    logUpdateCheck({
      date: (new Date()).toString(),
      platform: req.query.platform,
      version: version,
      ip: req.ip
    })
    if (!semver.valid(version) || semver.lt(version, DESKTOP_VERSION)) {
      // Update is required. Send update JSON.
      // Response format docs: https://github.com/Squirrel/Squirrel.Mac#update-json-format
      res.status(200).send({
        name: 'WebTorrent v' + DESKTOP_VERSION,
        url: `${RELEASES_URL}/v${DESKTOP_VERSION}/WebTorrent-v${DESKTOP_VERSION}-darwin.zip`,
        version: DESKTOP_VERSION
      })
    } else {
      // No update required. User is on latest app version.
      res.status(204).end()
    }
  })

  // WebTorrent Desktop Windows auto-update endpoint
  app.get('/desktop/update/*', function (req, res) {
    var pathname = url.parse(req.url).pathname
    var file = pathname.replace(/^\/desktop\/update\//i, '')
    var fileVersion
    if (file === 'RELEASES') {
      fileVersion = DESKTOP_VERSION
      logUpdateCheck({
        date: (new Date()).toString(),
        platform: req.query.platform,
        version: req.query.version,
        ip: req.ip
      })
    } else {
      var match = /-(\d+\.\d+\.\d+)-/.exec(file)
      fileVersion = match && match[1]
    }
    if (!fileVersion) {
      return res.status(404).end()
    }
    var redirectURL = `${RELEASES_URL}/v${fileVersion}/${file}`
    res.redirect(302, redirectURL)
  })
}

function logUpdateCheck (log) {
  console.log('UPDATE CHECK: ' + JSON.stringify(log))
}

function summarizeTelemetry () {
  var child = cp.spawn(SUMMARIZE_PATH, [])
  child.on('close', function (code) {
    console.log(`Summarize telemetry script exited with code: ${code}`)
  })
}
<|endoftext|>


<commit_before>// Copyright (c) IPython Development Team.
// Distributed under the terms of the Modified BSD License.

define([
    'jquery',
    'base/js/utils',
    'codemirror/lib/codemirror',
    'codemirror/mode/meta',
    'codemirror/addon/search/search'
    ],
function($,
    utils,
    CodeMirror
) {
    var Editor = function(selector, options) {
        this.selector = selector;
        this.contents = options.contents;
        this.events = options.events;
        this.base_url = options.base_url;
        this.file_path = options.file_path;

        this.codemirror = CodeMirror($(this.selector)[0]);

        this.save_enabled = false;
    };

    Editor.prototype.load = function() {
        var that = this;
        var cm = this.codemirror;
        this.contents.get(this.file_path, {type: 'file', format: 'text'})
            .then(function(model) {
                cm.setValue(model.content);

                // Find and load the highlighting mode
                var modeinfo = CodeMirror.findModeByMIME(model.mimetype);
                if (modeinfo) {
                    utils.requireCodeMirrorMode(modeinfo.mode, function() {
                        cm.setOption('mode', modeinfo.mode);
                    });
                }
                that.save_enabled = true;
            },
            function(error) {
                cm.setValue("Error! " + error.message +
                                "\nSaving disabled.");
                that.save_enabled = false;
            }
        );
        cm.setOption("extraKeys", {
            "Ctrl-S": $.proxy(this.save, this),
        });
    };

    Editor.prototype.save = function() {
        if (!this.save_enabled) {
            console.log("Not saving, save disabled");
            return;
        }
        var model = {
            path: this.file_path,
            type: 'file',
            format: 'text',
            content: this.codemirror.getValue(),
        };
        var that = this;
        this.contents.save(this.file_path, model).then(function() {
            that.events.trigger("save_succeeded.TextEditor");
        });
    };

    return {Editor: Editor};
});
<commit_msg>Better way of saving through CodeMirror<commit_after>// Copyright (c) IPython Development Team.
// Distributed under the terms of the Modified BSD License.

define([
    'jquery',
    'base/js/utils',
    'codemirror/lib/codemirror',
    'codemirror/mode/meta',
    'codemirror/addon/search/search'
    ],
function($,
    utils,
    CodeMirror
) {
    var Editor = function(selector, options) {
        this.selector = selector;
        this.contents = options.contents;
        this.events = options.events;
        this.base_url = options.base_url;
        this.file_path = options.file_path;

        this.codemirror = CodeMirror($(this.selector)[0]);

        // It appears we have to set commands on the CodeMirror class, not the
        // instance. I'd like to be wrong, but since there should only be one CM
        // instance on the page, this is good enough for now.
        CodeMirror.commands.save = $.proxy(this.save, this);

        this.save_enabled = false;
    };

    Editor.prototype.load = function() {
        var that = this;
        var cm = this.codemirror;
        this.contents.get(this.file_path, {type: 'file', format: 'text'})
            .then(function(model) {
                cm.setValue(model.content);

                // Find and load the highlighting mode
                var modeinfo = CodeMirror.findModeByMIME(model.mimetype);
                if (modeinfo) {
                    utils.requireCodeMirrorMode(modeinfo.mode, function() {
                        cm.setOption('mode', modeinfo.mode);
                    });
                }
                that.save_enabled = true;
            },
            function(error) {
                cm.setValue("Error! " + error.message +
                                "\nSaving disabled.");
                that.save_enabled = false;
            }
        );
    };

    Editor.prototype.save = function() {
        if (!this.save_enabled) {
            console.log("Not saving, save disabled");
            return;
        }
        var model = {
            path: this.file_path,
            type: 'file',
            format: 'text',
            content: this.codemirror.getValue(),
        };
        var that = this;
        this.contents.save(this.file_path, model).then(function() {
            that.events.trigger("save_succeeded.TextEditor");
        });
    };

    return {Editor: Editor};
});
<|endoftext|>


<commit_before>var colors      = require("colors");
var STATE       = require("./models/GameState");
var lobbyManager = require("./interface/lobbyManager");
var apeManager = require("./game/apeManager");
var gameState = require("./gameState");
var ROLE        = require("./models/ROLE");

//Used to update the game/lobby on intervals
var updateInterval;

//Link to the current io instance
var io;

var linkIO = function(ioInstance){
    io = ioInstance;
}
exports.linkIO = linkIO;

var startGame = function() {
    var theApe = parseInt(Math.random()*gameState.players.length);
    for(var i=0;i<gameState.players.length;i++){
        if(i === theApe){
            gameState.players[i].setApe();
            gameState.players[i].socket.emit("player_role",ROLE.APE);

            //Listen for input
            apeManager.setupEvents(gameState.players[i]);

            //Wait for the ape to give us level info
            console.log("Game is ready, waiting for Ape.".cyan + " (" + gameState.players[i].name + ")");
            gameState.players[i].socket.on("level_start",startLevel);
        } else {
            gameState.players[i].setJailer();
            gameState.players[i].socket.emit("player_role",ROLE.JAILER);
        }
    }
};
exports.startGame = startGame;

var startLevel = function(levelData){
    if(gameState.state !== STATE.GAME){
        //First time
        gameState.state = STATE.GAME;
        clearInterval(updateInterval);
        updateInterval = setInterval(updateGame,10);

        gameState.state = 1;

        //Let everyone know that the lobby is now in game.
        io.emit("lobby_state",gameState.getLobbyState());
    }
    console.log("Starting level ".cyan + levelData.levelNumber + "!".cyan);

    //Update the traps
    gameState.traps = levelData.traps;

    //Let everyone know the level started
    io.emit("level_start", levelData.levelNumber);
}
exports.startLevel = startLevel;

var endGame = function() {
    clearInterval(updateInterval);
    io.emit("game_end",true);
    for(var i=0;i<gameState.players.length;i++){
        gameState.players[i].role = ROLE.NONE;
        gameState.players[i].gameState = {};
    }

    if(gameState.state === STATE.GAME){
        //Clean up!
        gameState.state = STATE.GAME_OVER;
        setTimeout(function(){
            gameState.state = STATE.LOBBY;
            cleanPlayerData();
            console.log("Game -> Lobby".cyan);
        },1);//Change later
        console.log("Game ended. ".cyan);
    }
};
exports.endGame = endGame;

var cleanPlayerData = function(){
    for(var i=0;i<gameState.players.length;i++){
        gameState.players[i].clean();
    }
}

var updateGame = function() {
    console.log(".".red);
    //Update only jailers
    for(var i=0;i<gameState.players.length;i++){
        if(gameState.players[i].role === ROLE.JAILER){
            gameState.players[i].socket.emit("game_state",gameState.getInGameState());
        }
    }
};
exports.updateGame = updateGame;<commit_msg>Trying to fix server side lag<commit_after>var colors      = require("colors");
var STATE       = require("./models/GameState");
var lobbyManager = require("./interface/lobbyManager");
var apeManager = require("./game/apeManager");
var gameState = require("./gameState");
var ROLE        = require("./models/ROLE");

//Used to update the game/lobby on intervals
var updateInterval;

//Link to the current io instance
var io;

var linkIO = function(ioInstance){
    io = ioInstance;
}
exports.linkIO = linkIO;

var startGame = function() {
    var theApe = parseInt(Math.random()*gameState.players.length);
    for(var i=0;i<gameState.players.length;i++){
        if(i === theApe){
            gameState.players[i].setApe();
            gameState.players[i].socket.emit("player_role",ROLE.APE);

            //Listen for input
            apeManager.setupEvents(gameState.players[i]);

            //Wait for the ape to give us level info
            console.log("Game is ready, waiting for Ape.".cyan + " (" + gameState.players[i].name + ")");
            gameState.players[i].socket.on("level_start",startLevel);
        } else {
            gameState.players[i].setJailer();
            gameState.players[i].socket.emit("player_role",ROLE.JAILER);
        }
    }
};
exports.startGame = startGame;

var startLevel = function(levelData){
    if(gameState.state !== STATE.GAME){
        //First time
        gameState.state = STATE.GAME;
        clearInterval(updateInterval);
        updateInterval = setInterval(updateGame,1);

        gameState.state = 1;

        //Let everyone know that the lobby is now in game.
        io.emit("lobby_state",gameState.getLobbyState());
    }
    console.log("Starting level ".cyan + levelData.levelNumber + "!".cyan);

    //Update the traps
    gameState.traps = levelData.traps;

    //Let everyone know the level started
    io.emit("level_start", levelData.levelNumber);
}
exports.startLevel = startLevel;

var endGame = function() {
    clearInterval(updateInterval);
    io.emit("game_end",true);
    for(var i=0;i<gameState.players.length;i++){
        gameState.players[i].role = ROLE.NONE;
        gameState.players[i].gameState = {};
    }

    if(gameState.state === STATE.GAME){
        //Clean up!
        gameState.state = STATE.GAME_OVER;
        setTimeout(function(){
            gameState.state = STATE.LOBBY;
            cleanPlayerData();
            console.log("Game -> Lobby".cyan);
        },1);//Change later
        console.log("Game ended. ".cyan);
    }
};
exports.endGame = endGame;

var cleanPlayerData = function(){
    for(var i=0;i<gameState.players.length;i++){
        gameState.players[i].clean();
    }
}

var updateGame = function() {
    console.log(".".red);
    //Update only jailers
    for(var i=0;i<gameState.players.length;i++){
        if(gameState.players[i].role === ROLE.JAILER){
            gameState.players[i].socket.emit("game_state",gameState.getInGameState());
        }
    }
};
exports.updateGame = updateGame;<|endoftext|>


<commit_before>"use strict";

var global = this;

// for crypto.js
function BenchmarkSuite() {
}
global.BenchmarkSuite = BenchmarkSuite

var I = undefined;
global.url = undefined;
global.benchmarkSuite = undefined;
global.testSuite = undefined;
var base = this.parent !== undefined ? this.parent : {
  postMessage: function (message) {
    global.postMessage(message);
  }
};

var loadScripts = function (src, callback) {
  if (src === "") {
    setTimeout(callback, 0);
  } else {
    if (global.importScripts !== undefined) {
      global.importScripts(src);
      setTimeout(callback, 0);
    } else {
      var script = document.createElement("script");
      script.src = src;
      script.onload = callback;
      script.onreadystatechange = function() { // IE
        if (script.readyState === "complete" || script.readyState === "loaded") {
          script.onload = undefined;
          callback();
        }
      };
      document.documentElement.appendChild(script);
    }
  }
};

var transform = function (f) {
  return eval("(" + f.toString().replace(/I\.([a-zA-Z]+)\(([^,\)]+)(?:,([^,\)]+))?(?:,([^,\)]+))?\)/g, function (p, o, a, b, c) {
    if (I[o] == undefined || I[o] === "") {
      return p;
    }
    return I[o].replace(/([a-zA-Z]+)/g, function (t) {
      return t === "a" ? a.trim() : (t === "b" ? b.trim() : (t === "c" ? c.trim() : t));
    });
  }) + ")");
};

var invervalId = setInterval(function () {
  console.log("!");
}, 10000);

self.onmessage = function (event) {
  if (event.data === "start:benchmarks" || event.data === "start:tests") {
    var type = event.data;
    var src = decodeURIComponent(/src\=([^&]*)/.exec(location.search)[1]);
    loadScripts("benchmark.js", function () {
      Benchmark.options.minTime = 1 / 128;
      Benchmark.options.maxTime = 1 / 4;
      Benchmark.options.minSamples = 7;

      benchmarkSuite = new Benchmark.Suite();
      benchmarkSuite.on("cycle", function (event) {
        base.postMessage(JSON.stringify({
          message: event.target.toString(),
          url: url,
          name: event.target.name,
          result: (1 / event.target.times.period)
        }), "*");
      });
      var complete = false;
      var finish = function () {
        if (!complete) {
          complete = true;
          base.postMessage(JSON.stringify({
            message: "",
            url: url,
            name: "complete",
            result: 0
          }), "*");
          clearInterval(invervalId);
        }
      };
      benchmarkSuite.on("error", function (event) {
        finish();
      });
      benchmarkSuite.on("complete", function (event) {
        finish();
      });
      testSuite = {
        callbacks: [],
        add: function (title, callback) {
          this.callbacks.push({
            title: title,
            callback: callback
          });
        },
        run: function (options) {
          for (var i = 0; i < this.callbacks.length; i += 1) {
            var test = this.callbacks[i];
            var data = "";
            try {
              test.callback(I);
            } catch (e) {
              data = e.toString();
              if (e.message === "-") {
                data = "N/A";
              }
            }
            base.postMessage(JSON.stringify({
              message: test.title,
              url: url,
              name: "test",
              result: data
            }), "*");
          }
        }
      };
      loadScripts("libs.js", function () {
        var special = {
          "data:text/plain,number": "",
          "data:text/plain,bigint": "",
          "data:text/plain,wrapped_number": "./libs/SmallInteger.js",
          "data:text/plain,wrapped_number2": "./libs/SmallIntegerWithChecks.js"
        };
        loadScripts(src in special ? special[src] : src, function () {
          var lib = undefined;
          for (var i = 0; i < libs.length; i += 1) {
            if (libs[i].src === src) {
              lib = libs[i];
            }
          }
          I = lib;
          url = lib.url;//!
          loadScripts("tests.js", function () {
            if (I.setup != undefined) {
              I.setup();
            }
            var f = transform(wrapper.toString());
            f();
            setTimeout(function () {
              if (type === "start:tests") {
                if (src.indexOf("data:") !== 0) {
                  testSuite.run();
                }
                finish();
              }
              if (type === "start:benchmarks") {
                benchmarkSuite.run({
                  async: true
                });
              }
            }, 64);
          });
        });
      });
    });
  }
};
<commit_msg>Update iframe.js<commit_after>"use strict";

var global = this;

// for crypto.js
function BenchmarkSuite() {
}
global.BenchmarkSuite = BenchmarkSuite

var I = undefined;
global.url = undefined;
global.benchmarkSuite = undefined;
global.testSuite = undefined;
var base = this.parent !== undefined ? this.parent : {
  postMessage: function (message) {
    global.postMessage(message);
  }
};

var loadScripts = function (src, callback) {
  if (src === "") {
    setTimeout(callback, 0);
  } else {
    if (global.importScripts !== undefined) {
      global.importScripts(src);
      setTimeout(callback, 0);
    } else {
      var script = document.createElement("script");
      script.src = src;
      script.onload = callback;
      script.onreadystatechange = function() { // IE
        if (script.readyState === "complete" || script.readyState === "loaded") {
          script.onload = undefined;
          callback();
        }
      };
      document.documentElement.appendChild(script);
    }
  }
};

var transform = function (f) {
  return eval("(" + f.toString().replace(/I\.([a-zA-Z]+)\(([^,\)]+)(?:,([^,\)]+))?(?:,([^,\)]+))?\)/g, function (p, o, a, b, c) {
    if (I[o] == undefined || I[o] === "") {
      return p;
    }
    return I[o].replace(/([a-zA-Z]+)/g, function (t) {
      return t === "a" ? a.trim() : (t === "b" ? b.trim() : (t === "c" ? c.trim() : t));
    });
  }) + ")");
};

var invervalId = setInterval(function () {
  console.log("!");
}, 10000);

self.onmessage = function (event) {
  if (event.data === "start:benchmarks" || event.data === "start:tests") {
    var type = event.data;
    var src = decodeURIComponent(/src\=([^&]*)/.exec(location.search)[1]);
    loadScripts("benchmark.js", function () {
      Benchmark.options.minTime = 1 / 128;
      Benchmark.options.maxTime = 1 / 4;
      Benchmark.options.minSamples = 7;

      benchmarkSuite = new Benchmark.Suite();
      benchmarkSuite.on("cycle", function (event) {
        base.postMessage(JSON.stringify({
          message: event.target.toString(),
          url: url,
          name: event.target.name,
          result: (1 / event.target.times.period)
        }), "*");
      });
      var complete = false;
      var finish = function () {
        if (!complete) {
          complete = true;
          base.postMessage(JSON.stringify({
            message: "",
            url: url,
            name: "complete",
            result: 0
          }), "*");
          clearInterval(invervalId);
        }
      };
      benchmarkSuite.on("error", function (event) {
        finish();
      });
      benchmarkSuite.on("complete", function (event) {
        finish();
      });
      testSuite = {
        callbacks: [],
        add: function (title, callback) {
          this.callbacks.push({
            title: title,
            callback: callback
          });
        },
        run: function (options) {
          for (var i = 0; i < this.callbacks.length; i += 1) {
            var test = this.callbacks[i];
            var data = "";
            try {
              test.callback(I);
            } catch (e) {
              data = e.toString();
              if (e.message === "-") {
                data = "N/A";
              }
            }
            base.postMessage(JSON.stringify({
              message: test.title,
              url: url,
              name: "test",
              result: data
            }), "*");
          }
        }
      };
      loadScripts("libs.js", function () {
        var special = {
          "data:text/plain,number": "",
          "data:text/plain,bigint": "",
          "data:text/plain,wrapped_number": "./libs/SmallInteger.js",
          "data:text/plain,wrapped_number2": "./libs/SmallIntegerWithChecks.js"
        };
        var notTestable = {
          "data:text/plain,number": "",
          "data:text/plain,wrapped_number": "./libs/SmallInteger.js",
          "data:text/plain,wrapped_number2": "./libs/SmallIntegerWithChecks.js"
        };
        loadScripts(src in special ? special[src] : src, function () {
          var lib = undefined;
          for (var i = 0; i < libs.length; i += 1) {
            if (libs[i].src === src) {
              lib = libs[i];
            }
          }
          I = lib;
          url = lib.url;//!
          loadScripts("tests.js", function () {
            if (I.setup != undefined) {
              I.setup();
            }
            var f = transform(wrapper.toString());
            f();
            setTimeout(function () {
              if (type === "start:tests") {
                if (!(src in notTestable)) {
                  testSuite.run();
                }
                finish();
              }
              if (type === "start:benchmarks") {
                benchmarkSuite.run({
                  async: true
                });
              }
            }, 64);
          });
        });
      });
    });
  }
};
<|endoftext|>


<commit_before>import Promise from 'bluebird';
import _ from 'lodash';
import debugLib from 'debug';
import slugify from 'limax';
import { Op } from 'sequelize';

import CustomDataTypes from './DataTypes';
import { maxInteger } from '../constants/math';
import { capitalize, pluralize, days, formatCurrency } from '../lib/utils';

const debug = debugLib('tier');

export default function(Sequelize, DataTypes) {
  const { models } = Sequelize;

  const Tier = Sequelize.define(
    'Tier',
    {
      id: {
        type: DataTypes.INTEGER,
        primaryKey: true,
        autoIncrement: true,
      },

      CollectiveId: {
        type: DataTypes.INTEGER,
        references: {
          model: 'Collectives',
          key: 'id',
        },
        onDelete: 'SET NULL',
        onUpdate: 'CASCADE',
      },

      // human readable way to uniquely access a tier for a given collective or collective/event combo
      slug: {
        type: DataTypes.STRING,
        set(slug) {
          if (slug && slug.toLowerCase) {
            this.setDataValue('slug', slugify(slug));
          }
        },
      },

      name: {
        type: DataTypes.STRING,
        allowNull: false,
        set(name) {
          if (!this.getDataValue('slug')) {
            this.slug = name;
          }
          this.setDataValue('name', name);
        },
      },

      type: {
        type: DataTypes.STRING, // TIER, TICKET, DONATION, SERVICE, PRODUCT, MEMBERSHIP
        defaultValue: 'TIER',
      },

      description: DataTypes.STRING,

      longDescription: {
        type: DataTypes.TEXT,
        validate: {
          // Max length for arround 1_000_000 characters ~4MB of text
          len: [0, 1000000],
        },
      },

      button: DataTypes.STRING,

      amount: {
        type: DataTypes.INTEGER, // In cents
        validate: {
          min: 0,
        },
      },

      presets: {
        type: DataTypes.ARRAY(DataTypes.INTEGER),
      },

      minimumAmount: {
        type: DataTypes.INTEGER,
        validate: {
          min: 0,
        },
      },

      currency: CustomDataTypes(DataTypes).currency,

      interval: {
        type: DataTypes.STRING(8),
        validate: {
          isIn: {
            args: [['month', 'year']],
            msg: 'Must be month or year',
          },
        },
      },

      // Max quantity of tickets to sell (0 for unlimited)
      maxQuantity: {
        type: DataTypes.INTEGER,
        validate: {
          min: 0,
        },
      },

      // Max quantity of tickets per user (0 for unlimited)
      maxQuantityPerUser: {
        type: DataTypes.INTEGER,
        validate: {
          min: 0,
        },
      },

      // Goal to reach
      goal: {
        type: DataTypes.INTEGER,
        validate: {
          min: 0,
        },
      },

      password: {
        type: DataTypes.STRING,
      },

      startsAt: {
        type: DataTypes.DATE,
        defaultValue: Sequelize.NOW,
      },

      endsAt: {
        type: DataTypes.DATE,
        defaultValue: Sequelize.NOW,
      },

      createdAt: {
        type: DataTypes.DATE,
        defaultValue: Sequelize.NOW,
      },

      updatedAt: {
        type: DataTypes.DATE,
        defaultValue: Sequelize.NOW,
      },

      deletedAt: {
        type: DataTypes.DATE,
      },
    },
    {
      paranoid: true,

      getterMethods: {
        info() {
          return {
            id: this.id,
            name: this.name,
            description: this.description,
            amount: this.amount,
            currency: this.currency,
            maxQuantity: this.maxQuantity,
            startsAt: this.startsAt,
            endsAt: this.endsAt,
            createdAt: this.createdAt,
            updatedAt: this.updatedAt,
          };
        },

        minimal() {
          return {
            id: this.id,
            type: this.type,
            name: this.name,
          };
        },

        title() {
          return capitalize(pluralize(this.name));
        },

        amountStr() {
          let str = `${formatCurrency(this.minimumAmount || 0, this.currency)}+`;
          if (this.interval) {
            str += ` per ${this.interval}`;
          }
          return str;
        },
      },
    },
  );

  /**
   * Instance Methods
   */

  /**
   * Check if a backer is active
   * True if there is an entry in the Members table for this Backer/Collective/Tier couple created before `until`
   * If this tier has an interval, returns true if the membership started within the month/year
   * or if the last transaction happened wihtin the month/year
   */
  Tier.prototype.isBackerActive = function(backerCollective, until = new Date()) {
    return models.Member.findOne({
      where: {
        CollectiveId: this.CollectiveId,
        MemberCollectiveId: backerCollective.id,
        TierId: this.id,
        createdAt: { [Op.lte]: until },
      },
    }).then(membership => {
      if (!membership) return false;
      if (!this.interval) return true;
      if (this.interval === 'month' && days(membership.createdAt, until) <= 31) return true;
      if (this.interval === 'year' && days(membership.createdAt, until) <= 365) return true;
      return models.Order.findOne({
        where: {
          CollectiveId: this.CollectiveId,
          FromCollectiveId: backerCollective.id,
          TierId: this.id,
        },
      }).then(order => {
        if (!order) return false;
        return models.Transaction.findOne({
          where: { OrderId: order.id, CollectiveId: this.CollectiveId },
          order: [['createdAt', 'DESC']],
        }).then(transaction => {
          if (!transaction) {
            debug('No transaction found for order', order.dataValues);
            return false;
          }
          if (this.interval === 'month' && days(transaction.createdAt, until) <= 31) return true;
          if (this.interval === 'year' && days(transaction.createdAt, until) <= 365) return true;
          return false;
        });
      });
    });
  };

  // TODO: Check for maxQuantityPerUser
  Tier.prototype.availableQuantity = function() {
    return models.Order.sum('quantity', {
      where: {
        TierId: this.id,
        processedAt: { [Op.ne]: null },
      },
    }).then(usedQuantity => {
      debug('availableQuantity', 'usedQuantity:', usedQuantity, 'maxQuantity', this.maxQuantity);
      if (this.maxQuantity && usedQuantity) {
        return this.maxQuantity - usedQuantity;
      } else if (this.maxQuantity) {
        return this.maxQuantity;
      } else {
        return maxInteger; // GraphQL doesn't like infinity
      }
    });
  };

  Tier.prototype.checkAvailableQuantity = function(quantityNeeded = 1) {
    return this.availableQuantity().then(available => available - quantityNeeded >= 0);
  };

  /**
   * Class Methods
   */
  Tier.createMany = (tiers, defaultValues = {}) => {
    return Promise.map(tiers, t => Tier.create(_.defaults({}, t, defaultValues)), { concurrency: 1 });
  };

  /**
   * Append tier to each backer in an array of backers
   */
  Tier.appendTier = (collective, backerCollectives) => {
    const backerCollectivesIds = backerCollectives.map(b => b.id);
    debug('appendTier', collective.name, 'backers: ', backerCollectives.length);
    return models.Member.findAll({
      where: {
        MemberCollectiveId: { [Op.in]: backerCollectivesIds },
        CollectiveId: collective.id,
      },
      include: [{ model: models.Tier }],
    }).then(memberships => {
      const membershipsForBackerCollective = {};
      memberships.map(m => {
        membershipsForBackerCollective[m.MemberCollectiveId] = m.Tier;
      });
      return backerCollectives.map(backerCollective => {
        backerCollective.tier = membershipsForBackerCollective[backerCollective.id];
        debug('appendTier for', backerCollective.name, ':', backerCollective.tier && backerCollective.tier.slug);
        return backerCollective;
      });
    });
  };

  return Tier;
}
<commit_msg>chore(Tier): Add strip_tags to longDescription<commit_after>import Promise from 'bluebird';
import _ from 'lodash';
import debugLib from 'debug';
import slugify from 'limax';
import { Op } from 'sequelize';

import CustomDataTypes from './DataTypes';
import { maxInteger } from '../constants/math';
import { capitalize, pluralize, days, formatCurrency, strip_tags } from '../lib/utils';

const debug = debugLib('tier');

export default function(Sequelize, DataTypes) {
  const { models } = Sequelize;

  const Tier = Sequelize.define(
    'Tier',
    {
      id: {
        type: DataTypes.INTEGER,
        primaryKey: true,
        autoIncrement: true,
      },

      CollectiveId: {
        type: DataTypes.INTEGER,
        references: {
          model: 'Collectives',
          key: 'id',
        },
        onDelete: 'SET NULL',
        onUpdate: 'CASCADE',
      },

      // human readable way to uniquely access a tier for a given collective or collective/event combo
      slug: {
        type: DataTypes.STRING,
        set(slug) {
          if (slug && slug.toLowerCase) {
            this.setDataValue('slug', slugify(slug));
          }
        },
      },

      name: {
        type: DataTypes.STRING,
        allowNull: false,
        set(name) {
          if (!this.getDataValue('slug')) {
            this.slug = name;
          }
          this.setDataValue('name', name);
        },
      },

      type: {
        type: DataTypes.STRING, // TIER, TICKET, DONATION, SERVICE, PRODUCT, MEMBERSHIP
        defaultValue: 'TIER',
      },

      description: DataTypes.STRING,

      longDescription: {
        type: DataTypes.TEXT,
        validate: {
          // Max length for arround 1_000_000 characters ~4MB of text
          len: [0, 1000000],
        },
        set(content) {
          if (!content) {
            this.setDataValue('longDescription', null);
          } else {
            this.setDataValue('longDescription', strip_tags(content));
          }
        },
      },

      button: DataTypes.STRING,

      amount: {
        type: DataTypes.INTEGER, // In cents
        validate: {
          min: 0,
        },
      },

      presets: {
        type: DataTypes.ARRAY(DataTypes.INTEGER),
      },

      minimumAmount: {
        type: DataTypes.INTEGER,
        validate: {
          min: 0,
        },
      },

      currency: CustomDataTypes(DataTypes).currency,

      interval: {
        type: DataTypes.STRING(8),
        validate: {
          isIn: {
            args: [['month', 'year']],
            msg: 'Must be month or year',
          },
        },
      },

      // Max quantity of tickets to sell (0 for unlimited)
      maxQuantity: {
        type: DataTypes.INTEGER,
        validate: {
          min: 0,
        },
      },

      // Max quantity of tickets per user (0 for unlimited)
      maxQuantityPerUser: {
        type: DataTypes.INTEGER,
        validate: {
          min: 0,
        },
      },

      // Goal to reach
      goal: {
        type: DataTypes.INTEGER,
        validate: {
          min: 0,
        },
      },

      password: {
        type: DataTypes.STRING,
      },

      startsAt: {
        type: DataTypes.DATE,
        defaultValue: Sequelize.NOW,
      },

      endsAt: {
        type: DataTypes.DATE,
        defaultValue: Sequelize.NOW,
      },

      createdAt: {
        type: DataTypes.DATE,
        defaultValue: Sequelize.NOW,
      },

      updatedAt: {
        type: DataTypes.DATE,
        defaultValue: Sequelize.NOW,
      },

      deletedAt: {
        type: DataTypes.DATE,
      },
    },
    {
      paranoid: true,

      getterMethods: {
        info() {
          return {
            id: this.id,
            name: this.name,
            description: this.description,
            amount: this.amount,
            currency: this.currency,
            maxQuantity: this.maxQuantity,
            startsAt: this.startsAt,
            endsAt: this.endsAt,
            createdAt: this.createdAt,
            updatedAt: this.updatedAt,
          };
        },

        minimal() {
          return {
            id: this.id,
            type: this.type,
            name: this.name,
          };
        },

        title() {
          return capitalize(pluralize(this.name));
        },

        amountStr() {
          let str = `${formatCurrency(this.minimumAmount || 0, this.currency)}+`;
          if (this.interval) {
            str += ` per ${this.interval}`;
          }
          return str;
        },
      },
    },
  );

  /**
   * Instance Methods
   */

  /**
   * Check if a backer is active
   * True if there is an entry in the Members table for this Backer/Collective/Tier couple created before `until`
   * If this tier has an interval, returns true if the membership started within the month/year
   * or if the last transaction happened wihtin the month/year
   */
  Tier.prototype.isBackerActive = function(backerCollective, until = new Date()) {
    return models.Member.findOne({
      where: {
        CollectiveId: this.CollectiveId,
        MemberCollectiveId: backerCollective.id,
        TierId: this.id,
        createdAt: { [Op.lte]: until },
      },
    }).then(membership => {
      if (!membership) return false;
      if (!this.interval) return true;
      if (this.interval === 'month' && days(membership.createdAt, until) <= 31) return true;
      if (this.interval === 'year' && days(membership.createdAt, until) <= 365) return true;
      return models.Order.findOne({
        where: {
          CollectiveId: this.CollectiveId,
          FromCollectiveId: backerCollective.id,
          TierId: this.id,
        },
      }).then(order => {
        if (!order) return false;
        return models.Transaction.findOne({
          where: { OrderId: order.id, CollectiveId: this.CollectiveId },
          order: [['createdAt', 'DESC']],
        }).then(transaction => {
          if (!transaction) {
            debug('No transaction found for order', order.dataValues);
            return false;
          }
          if (this.interval === 'month' && days(transaction.createdAt, until) <= 31) return true;
          if (this.interval === 'year' && days(transaction.createdAt, until) <= 365) return true;
          return false;
        });
      });
    });
  };

  // TODO: Check for maxQuantityPerUser
  Tier.prototype.availableQuantity = function() {
    return models.Order.sum('quantity', {
      where: {
        TierId: this.id,
        processedAt: { [Op.ne]: null },
      },
    }).then(usedQuantity => {
      debug('availableQuantity', 'usedQuantity:', usedQuantity, 'maxQuantity', this.maxQuantity);
      if (this.maxQuantity && usedQuantity) {
        return this.maxQuantity - usedQuantity;
      } else if (this.maxQuantity) {
        return this.maxQuantity;
      } else {
        return maxInteger; // GraphQL doesn't like infinity
      }
    });
  };

  Tier.prototype.checkAvailableQuantity = function(quantityNeeded = 1) {
    return this.availableQuantity().then(available => available - quantityNeeded >= 0);
  };

  /**
   * Class Methods
   */
  Tier.createMany = (tiers, defaultValues = {}) => {
    return Promise.map(tiers, t => Tier.create(_.defaults({}, t, defaultValues)), { concurrency: 1 });
  };

  /**
   * Append tier to each backer in an array of backers
   */
  Tier.appendTier = (collective, backerCollectives) => {
    const backerCollectivesIds = backerCollectives.map(b => b.id);
    debug('appendTier', collective.name, 'backers: ', backerCollectives.length);
    return models.Member.findAll({
      where: {
        MemberCollectiveId: { [Op.in]: backerCollectivesIds },
        CollectiveId: collective.id,
      },
      include: [{ model: models.Tier }],
    }).then(memberships => {
      const membershipsForBackerCollective = {};
      memberships.map(m => {
        membershipsForBackerCollective[m.MemberCollectiveId] = m.Tier;
      });
      return backerCollectives.map(backerCollective => {
        backerCollective.tier = membershipsForBackerCollective[backerCollective.id];
        debug('appendTier for', backerCollective.name, ':', backerCollective.tier && backerCollective.tier.slug);
        return backerCollective;
      });
    });
  };

  return Tier;
}
<|endoftext|>