<commit_before>'use strict';

import React, {Component} from 'react';
import {
  View,
  Text,
  ActivityIndicator,
  ListView,
  StyleSheet
} from 'react-native';

// Import 3rd Party Node Modules
  import digestCall from 'digest-auth-request-rn';
  import DropdownAlert from 'react-native-dropdownalert';

// Import Scenes & Components
  import ArticlesListItem from '../components/ArticlesListItem';

// Import Globals, Configs & Helpers
  import appConfig from '../config/settings';
  import globalStyles, {width, height} from '../config/globalStyles';

const url = appConfig.apiCredentials_test.url;
const {apiUser} = appConfig.apiCredentials_test;
const {apiKey} = appConfig.apiCredentials_test;

export default class DepositArticlesApiCalls extends Component {

  constructor(props) {
    super(props);
    this.ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
    this.customerReturnedArticles = [];
    this.state = {
      currentlyLoading: true,
      dataSource: this.ds.cloneWithRows([]),
      dataSourceReduced: this.ds.cloneWithRows([]),
    };
    this.getArticleList = this.getArticleList.bind(this);
    this.collectReturnedItems = this.collectReturnedItems.bind(this);
    this.mergeReturnedArticlesToObj = this.mergeReturnedArticlesToObj.bind(this);
    this.createSubmitObject = this.createSubmitObject.bind(this);
    this.submitArticlesRequest = this.submitArticlesRequest.bind(this);
  }

  render() {
    const spinner = (
      this.state.currentlyLoading && <ActivityIndicator
        animating={this.state.currentlyLoading}
        size="large"
        style={{
          justifyContent: 'center',
          marginTop: 50
        }}
      />
    ) || <View/>;

    const dropdownAlertSuccess = (
      <DropdownAlert
        ref={(ref) => this.dropdownSuccess = ref}
        containerStyle={[styles.dropdownAlert, {
          backgroundColor: 'mediumseagreen',
        }]}
        titleStyle={styles.dropdownAlertTitle}
        closeInterval={1500} />
    );

    const dropdownAlertError = (
      <DropdownAlert
        ref={(ref) => this.dropdownError = ref}
        containerStyle={[styles.dropdownAlert, {
          backgroundColor: 'crimson',
        }]}
        titleStyle={styles.dropdownAlertTitle}
        closeInterval={10000} />
    );

    if (this.props.submitArticles) { this.mergeReturnedArticlesToObj() }

    return (
      <View style={globalStyles.container}>
        {spinner}

        <ListView
          dataSource={this.state.dataSource}
          renderRow={(rowData) =>
            <ArticlesListItem title={rowData.name} img={rowData.image} id={rowData.id}
                              returnCount={this.props.articlesReturned}
                              filterOn={this.props.filterListOnButtonConfirm}
                              collectItems={this.collectReturnedItems}/>
          }
          automaticallyAdjustContentInsets={false}
          enableEmptySections={true}
          initialListSize={20} // render all Rows at once (20 = more than total)
          style={this.props.filterListOnButtonConfirm ?
            {height: height - 183} :
            {height: height - 132}}
        />

        {dropdownAlertSuccess}
        {dropdownAlertError}
      </View>
        // height-132px = screen height - Status-/Nav-/Subtitle-Bars - Button
        //(otherwise hidden since 'automaticallyAdj...' above)
        // even 51px less for Confirm Screen (w/ second button)
    );
  }

  componentDidMount() {
    this.getArticleList();
  }

  getArticleList() {
    const req = new digestCall(
      'GET',
      url + '/depositArticles',
      apiUser,
      apiKey
    );

    req.request((result) => {
      // changed to ES6-Arrow-Functions (for preserving 'this' -> no binding necessary)
      this.setState({
        currentlyLoading: false,
        dataSource: this.ds.cloneWithRows(result.data),
      });

      this.props.hideConfirmButtonWhileLoading();

    }, (error) => {
      console.error(error);
    });
  }

  collectReturnedItems(articleId, articleName, add, remove) {
    this.customerReturnedArticles.push({
      id: articleId,
      name: articleName,
      addAmount: add,
      removeAmount: remove
    });
  }

  mergeReturnedArticlesToObj() {
    let merged = {};

    this.customerReturnedArticles.forEach((arrObj) => {
      if (merged.hasOwnProperty(arrObj.id)) {
        merged[arrObj.id] += arrObj.addAmount;
        merged[arrObj.id] -= arrObj.removeAmount;
      } else {
        merged[arrObj.id] = arrObj.addAmount;
      }
    });

    this.createSubmitObject(merged)
  }

  createSubmitObject(obj) {
    let submitObject = {
      returnedArticles: {
        customerId: this.props.customerId,
        articles: []
      }
    };

    for (let prop in obj) {
      if (obj[prop] > 0 && obj.hasOwnProperty(prop)) { // ignore amount = 0 and check if hasOwnProp
        submitObject.returnedArticles.articles.push({
          articleId: prop,
          returnAmount: obj[prop]
        });
      }
    }

    this.submitArticlesRequest(submitObject);
  }

  submitArticlesRequest(submitObj) {
          const postData = submitObj;

          console.log('postData: ', postData);

          const postReq = new digestCall(
            'POST',
            url + '/deposit',
            apiUser,
            apiKey
          );

          postReq.request((data) => {
            console.log('success alert: ', data);
            this.dropdownSuccess.alertWithType('custom', 'Pfand erfolgreich zurückgebucht');
            setTimeout(this.props.goBackToHome, 1000);

          }, (error) => {
            console.error(error);
            this.dropdownError.alertWithType('custom', 'Fehler. Bitte noch mal versuchen.');

          }, postData);

  }
}

const styles = StyleSheet.create({
  dropdownAlert: {
    paddingTop: 0,
    paddingBottom: 20,
    paddingLeft:  16
  },
  dropdownAlertTitle: {
    textAlign: 'center',
    fontSize: 16,
    fontWeight: 'bold',
    color: '#fff'
  }
});
<commit_msg>Remove whitespaces<commit_after>'use strict';

import React, {Component} from 'react';
import {
  View,
  Text,
  ActivityIndicator,
  ListView,
  StyleSheet
} from 'react-native';

// Import 3rd Party Node Modules
  import digestCall from 'digest-auth-request-rn';
  import DropdownAlert from 'react-native-dropdownalert';

// Import Scenes & Components
  import ArticlesListItem from '../components/ArticlesListItem';

// Import Globals, Configs & Helpers
  import appConfig from '../config/settings';
  import globalStyles, {width, height} from '../config/globalStyles';

const url = appConfig.apiCredentials_test.url;
const {apiUser} = appConfig.apiCredentials_test;
const {apiKey} = appConfig.apiCredentials_test;

export default class DepositArticlesApiCalls extends Component {

  constructor(props) {
    super(props);
    this.ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
    this.customerReturnedArticles = [];
    this.state = {
      currentlyLoading: true,
      dataSource: this.ds.cloneWithRows([]),
      dataSourceReduced: this.ds.cloneWithRows([]),
    };
    this.getArticleList = this.getArticleList.bind(this);
    this.collectReturnedItems = this.collectReturnedItems.bind(this);
    this.mergeReturnedArticlesToObj = this.mergeReturnedArticlesToObj.bind(this);
    this.createSubmitObject = this.createSubmitObject.bind(this);
    this.submitArticlesRequest = this.submitArticlesRequest.bind(this);
  }

  render() {
    const spinner = (
      this.state.currentlyLoading && <ActivityIndicator
        animating={this.state.currentlyLoading}
        size="large"
        style={{
          justifyContent: 'center',
          marginTop: 50
        }}
      />
    ) || <View/>;

    const dropdownAlertSuccess = (
      <DropdownAlert
        ref={(ref) => this.dropdownSuccess = ref}
        containerStyle={[styles.dropdownAlert, {
          backgroundColor: 'mediumseagreen',
        }]}
        titleStyle={styles.dropdownAlertTitle}
        closeInterval={1500} />
    );

    const dropdownAlertError = (
      <DropdownAlert
        ref={(ref) => this.dropdownError = ref}
        containerStyle={[styles.dropdownAlert, {
          backgroundColor: 'crimson',
        }]}
        titleStyle={styles.dropdownAlertTitle}
        closeInterval={10000} />
    );

    if (this.props.submitArticles) { this.mergeReturnedArticlesToObj() }

    return (
      <View style={globalStyles.container}>
        {spinner}

        <ListView
          dataSource={this.state.dataSource}
          renderRow={(rowData) =>
            <ArticlesListItem title={rowData.name} img={rowData.image} id={rowData.id}
                              returnCount={this.props.articlesReturned}
                              filterOn={this.props.filterListOnButtonConfirm}
                              collectItems={this.collectReturnedItems}/>
          }
          automaticallyAdjustContentInsets={false}
          enableEmptySections={true}
          initialListSize={20} // render all Rows at once (20 = more than total)
          style={this.props.filterListOnButtonConfirm ?
            {height: height - 183} :
            {height: height - 132}}
        />

        {dropdownAlertSuccess}
        {dropdownAlertError}
      </View>
        // height-132px = screen height - Status-/Nav-/Subtitle-Bars - Button
        //(otherwise hidden since 'automaticallyAdj...' above)
        // even 51px less for Confirm Screen (w/ second button)
    );
  }

  componentDidMount() {
    this.getArticleList();
  }

  getArticleList() {
    const req = new digestCall(
      'GET',
      url + '/depositArticles',
      apiUser,
      apiKey
    );

    req.request((result) => {
      // changed to ES6-Arrow-Functions (for preserving 'this' -> no binding necessary)
      this.setState({
        currentlyLoading: false,
        dataSource: this.ds.cloneWithRows(result.data),
      });

      this.props.hideConfirmButtonWhileLoading();

    }, (error) => {
      console.error(error);
    });
  }

  collectReturnedItems(articleId, articleName, add, remove) {
    this.customerReturnedArticles.push({
      id: articleId,
      name: articleName,
      addAmount: add,
      removeAmount: remove
    });
  }

  mergeReturnedArticlesToObj() {
    let merged = {};

    this.customerReturnedArticles.forEach((arrObj) => {
      if (merged.hasOwnProperty(arrObj.id)) {
        merged[arrObj.id] += arrObj.addAmount;
        merged[arrObj.id] -= arrObj.removeAmount;
      } else {
        merged[arrObj.id] = arrObj.addAmount;
      }
    });

    this.createSubmitObject(merged)
  }

  createSubmitObject(obj) {
    let submitObject = {
      returnedArticles: {
        customerId: this.props.customerId,
        articles: []
      }
    };

    for (let prop in obj) {
      if (obj[prop] > 0 && obj.hasOwnProperty(prop)) { // ignore amount = 0 and check if hasOwnProp
        submitObject.returnedArticles.articles.push({
          articleId: prop,
          returnAmount: obj[prop]
        });
      }
    }

    this.submitArticlesRequest(submitObject);
  }

  submitArticlesRequest(submitObj) {
    const postData = submitObj;

    console.log('postData: ', postData);

    const postReq = new digestCall(
      'POST',
      url + '/deposit',
      apiUser,
      apiKey
    );

    postReq.request((data) => {
      console.log('success alert: ', data);
      this.dropdownSuccess.alertWithType('custom', 'Pfand erfolgreich zurückgebucht');
      setTimeout(this.props.goBackToHome, 1000);

    }, (error) => {
      console.error(error);
      this.dropdownError.alertWithType('custom', 'Fehler. Bitte noch mal versuchen.');

    }, postData);
  }
}

const styles = StyleSheet.create({
  dropdownAlert: {
    paddingTop: 0,
    paddingBottom: 20,
    paddingLeft:  16
  },
  dropdownAlertTitle: {
    textAlign: 'center',
    fontSize: 16,
    fontWeight: 'bold',
    color: '#fff'
  }
});
<|endoftext|>


<commit_before>/*
 * Racquetball
 * (C) 2014, Bang Bang Attack Studios
 * http://www.bangbangattackstudios.com
*/
var Racquetball = (function() {
    var ns = {};

    Crafty.init(320, 480, document.getElementById('game'));
    Crafty.canvas.init();
    Crafty.background('#303030');

    Crafty.scene('Loading', function() {
        // load resources
        var resources = [
            'img/ball.png',
            'img/racquet.png',
            'img/play.png',
            'img/pause.png',
            'img/splash-bg.png',
            'img/splash-logo.png',
            'img/speaker-on2.png',
            'img/speaker-off2.png',

            'sfx/bounce.ogg','sfx/bounce.mp3','sfx/bounce.wav',
            'sfx/bounce2.ogg','sfx/bounce2.mp3','sfx/bounce2.wav',
            'sfx/lose.ogg','sfx/lose.mp3','sfx/lose.wav',
            'sfx/win.ogg','sfx/win.mp3','sfx/win.wav',

            'bgm/track1.mp3','bgm/track1.ogg','bgm/track1.wav'
        ];

        function onLoad() {

            Crafty.audio.add({
                bgm: ['bgm/track1.mp3','bgm/track1.ogg','bgm/track1.wav'],
                bounce: ['sfx/bounce.ogg','sfx/bounce.mp3','sfx/bounce.wav'],
                bounce2: ['sfx/bounce2.ogg','sfx/bounce2.mp3','sfx/bounce2.wav'],
                win: ['sfx/win.ogg','sfx/win.mp3','sfx/win.wav'],
                lose: ['sfx/lose.ogg','sfx/lose.mp3','sfx/lose.wav']
            });

            // to splash scene
            Crafty.scene('Splash');

        }

        function onProgress(p) {
            // p is { loaded: j, total: total, percent: (j / total * 100) ,src:src})
            console.log('loading ' + p.src + ' [' + p.percent + '%]');
        }

        function onError(e) {
            console.log('failed to load asset ' + e);
        }

        Crafty.load(resources, onLoad, onProgress, onError);

    });

    Crafty.scene('Splash', function() {

        var skipped = false;

        Crafty.e("2D, DOM, Image").image("img/splash-bg.png");

        Crafty.e("2D, DOM, Image, Tween").image("img/splash-logo.png")
        .attr({alpha: 0.0, x: 10, y: -129 })
        .tween({alpha: 1.0, y: 240-67 }, 1500);

        Crafty.e('Delay').delay(function() {
            if (!skipped) {
                Crafty.scene('Title');

            }
        }, 3000, 0);

        Crafty.e('2D, DOM, Color, Mouse')
        .color('black')
        .attr({x: 0, y: 0, w: 320, h: 480, alpha: 0.0 })
        .bind('Click', function() {
            skipped = true;
            Crafty.scene('Title');
        });
    });

    Crafty.scene('Title', function() {

        Crafty.e('2D, Canvas, Text')
        .attr({x: 10, y: 24})
        .textColor('#ffffff')
        .textFont({size: '56px', family: 'Helvetica' })
        .text('Racquetball');

        Crafty.e('2D, Canvas, Text, Mouse')
        .attr({x: 104, y: 224})
        .textColor('#ffffff')
        .textFont({size: '34px', family: 'Helvetica' })
        .text('START')
        .bind('Click', function() {
            Crafty.scene('Court');
            Crafty.audio.play('bgm', -1);
        });

        Crafty.e('2D, Canvas, Text, Mouse')
        .attr({x: 84, y: 424})
        .textColor('#ffffff')
        .textFont({size: '34px', family: 'Helvetica' })
        .text('CREDITS')
        .bind('Click', function() {
            Crafty.scene('Credits');
        });

    });

    Crafty.scene('Credits', function() {
        Crafty.e('2D, Canvas, Text')
        .attr({x: 10, y: 224})
        .textColor('#ffffff')
        .textFont({size: '24px', family: 'Helvetica' })
        .text('Developed by Richard Marks');

        Crafty.e('2D, DOM, Color, Mouse')
        .color('black')
        .attr({x: 0, y: 0, w: 320, h: 480, alpha: 0.0 })
        .bind('Click', function() {
            Crafty.scene('Title');
        });

    });

    Crafty.c('Edges', {
        init: function() {
            var t = this;
            Object.defineProperties(t, {
                left:    { get: function() { return t.x; } },
                right:   { get: function() { return t.x + t.w; } },
                top:     { get: function() { return t.y; } },
                bottom:  { get: function() { return t.y + t.h; } },
                centerX: { get: function() { return t.x + t.w / 2; } },
                centerY: { get: function() { return t.y + t.h / 2; } },
                center:  { get: function() { return { x: t.centerX, y: t.centerY }; } }
            });
        }
    });

    Crafty.c('Racquet', {
        _held: false,
        _hitbox: { x: 160 - 48, y: 480 - (48 + 52), w: 96, h: 32 },
        _bounds: { x: { l: 0, h: 320 }, y: { l: 0, h: 480 } },
        init: function() {
            this.requires('2D, Canvas, Image, Mouse, Edges');
            this.image('img/racquet.png')
            .bind('MouseDown', function() { this._held = true; })
            .bind('MouseUp', function() { this._held = false; })
            .bind('MouseMove', function(e) {
                var t = this;
                if (!t._held) {
                    return;
                }

                t._hitbox.x = e.realX - (t._hitbox.w / 2);

                if (t._hitbox.x < t._bounds.x.l) {
                    t._hitbox.x = t._bounds.x.l;
                } else if (t._hitbox.x > t._bounds.x.h - t._hitbox.w) {
                    t._hitbox.x = t._bounds.x.h - t._hitbox.w;
                }

                t.attr({ x: t._hitbox.x }).trigger('Moved', { x: this._x, y: this._y });

            })
            .attr({ x: this._hitbox.x, y: this._hitbox.y, width: this._hitbox.w, height: this._hitbox.h });
        }
    });

    Crafty.c('Ball', {
        _locked: false,
        _hitbox: { x: 160 - 48, y: 480 - (48 + 52), w: 96, h: 32 },
        _bounds: { x: { l: 0, h: 320 }, y: { l: 0, h: 480 } },
        _vel: { x: 1.0, y: -1.0 },
        _speed: 14.0,
        _aorb: true,
        _prevPos: { x: 0, y: 0 },
        init: function() {
            this.requires('2D, Canvas, Image, Collision, Edges');
            this.image('img/ball.png')
            .attr({ x: 160, y: 120, width: 16, height: 16 })
            .bind('EnterFrame', this._onEnterFrame);

            this._hitbox.x.l += 8;
            this._hitbox.x.h -= 8;
            this._hitbox.y.l += 8;
            this._hitbox.y.h -= 8;
        },

        reset: function() {
            var t = this;
            t.attr({ x: 160, y: 120 });
            t._vel.x = 1.0;
            t._vel.y = -1.0;
            t._speed = 14.0;
            t._locked = false;
        },

        _onEnterFrame: function() {
            var t = this, dt = 0.33;
            if (t._locked) {
                return;
            }

            t._aorb = !t._aorb;

            t._prevPos.x = t.x;
            t._prevPos.y = t.y;

            t.x += t._vel.x * t._speed * dt;
            t.y += t._vel.y * t._speed * dt;

            t._checkLostBall();
            t._checkWallCollisions();
            t._checkPaddleCollision();
        },

        _playBounceSfx: function() {
            Crafty.audio.play(this._aorb ? 'bounce' : 'bounce2');
        },

        _playScoreSfx: function() {
            Crafty.audio.play('win');
        },

        _playLostSfx: function() {
            Crafty.audio.play('lose');
        },

        _speedUp: function() {
            var t = this;
            t._speed += 0.8;
            if (t._speed > 60) {
                t._speed = 60;
            }
        },

        _checkLostBall: function() {
            var t = this;
            if (t.y < t._bounds.y.h) {
                return;
            }

            t._playLostSfx();
            t.destroy();
            t._locked = true;

            setTimeout(function() {
                Crafty.trigger('BallLost');
            }, 1000);
        },

        _checkWallCollisions: function() {
            var t = this;

            if (t.x < t._bounds.x.l || t.x > t._bounds.x.h) {
                t.x = t._prevPos.x;
                t._vel.x *= -1;
                t._playBounceSfx();
            }

            if (t.y < t._bounds.y.l) {
                t._playScoreSfx();
                t._speedUp();
                t.y = t._prevPos.y;
                t._vel.y *= -1;
            }
        },

        _checkPaddleCollision: function() {
            var t = this;
            if (t._vel.y < 0) {
                return;
            }

            var hit = t.hit('Racquet')[0];

            if (hit) {
                t._playBounceSfx();
                t._speedUp();
                t._vel.y *= -1;
                t._vel.x = t._calculateBounceVelocity(hit.obj);
            }
        },

        _calculateBounceVelocity: function(subject) {
            var t = this,
                dist = Crafty.math.distance(subject.centerX, subject.centerY, t.centerX, t.centerY);

            var magnitude = dist - ((t.h - subject.h) * 0.5);
            var ratio = magnitude / (subject.w / 2) * 2.5;
            if (t.centerX < subject.centerX) {
                ratio = -ratio;
            }
            return ratio;
        }
    });

    Crafty.scene('Court', function() {

        var fieldBounds = { x: { l: 0, h: 320 }, y: { l: 0, h: 432 } },
            score = 0,
            racquet = null,
            ball = null,
            scoreDisplay = null,
            muteButton = null,
            pauseButton = null;

        scoreDisplay = Crafty.e('ScoreDisplay');
        ball = Crafty.e('Ball');
        racquet = Crafty.e('Racquet');
        muteButton = Crafty.e('MuteButton');
        pauseButton = Crafty.e('PauseButton');

        Crafty.bind('BallLost', function() {
            Crafty.audio.stop();
            Crafty.trigger('MessageEvent', 'You Lose!');
            Crafty.scene('Message');
        });

    });

    Crafty.scene('Message', function() {
        Crafty.bind('MessageEvent', function(msg) {
            Crafty.e('2D, Canvas, Text')
            .attr({x: 10, y: 24})
            .textColor('#ffffff')
            .textFont({size: '56px', family: 'Helvetica' })
            .text(msg);
        });

        Crafty.e('2D, DOM, Color, Mouse')
        .color('black')
        .attr({x: 0, y: 0, w: 320, h: 480, alpha: 0.0 })
        .bind('Click', function() {
            Crafty.scene('Title');
        });
    });

    Crafty.scene('Loading');

    return {
        VERSION: '1.0',
        COPYRIGHT: '(C) 2014, Bang Bang Attack Studios',
        WEBSITE: 'http://www.bangbangattackstudios.com',
        DEVELOPER: 'Richard Marks of Bang Bang Attack Studios'
    };
})();<commit_msg>making progress I think<commit_after>/*
 * Racquetball
 * (C) 2014, Bang Bang Attack Studios
 * http://www.bangbangattackstudios.com
*/
var Racquetball = (function() {
    var ns = {};

    Crafty.init(320, 480, document.getElementById('game'));
    Crafty.canvas.init();
    Crafty.background('#303030');

    Crafty.scene('Loading', function() {
        // load resources
        var resources = [
            'img/ball.png',
            'img/racquet.png',
            'img/play.png',
            'img/pause.png',
            'img/splash-bg.png',
            'img/splash-logo.png',
            'img/speaker-on2.png',
            'img/speaker-off2.png',

            'sfx/bounce.ogg','sfx/bounce.mp3','sfx/bounce.wav',
            'sfx/bounce2.ogg','sfx/bounce2.mp3','sfx/bounce2.wav',
            'sfx/lose.ogg','sfx/lose.mp3','sfx/lose.wav',
            'sfx/win.ogg','sfx/win.mp3','sfx/win.wav',

            'bgm/track1.mp3','bgm/track1.ogg','bgm/track1.wav'
        ];

        function onLoad() {

            Crafty.audio.add({
                bgm: ['bgm/track1.mp3','bgm/track1.ogg','bgm/track1.wav'],
                bounce: ['sfx/bounce.ogg','sfx/bounce.mp3','sfx/bounce.wav'],
                bounce2: ['sfx/bounce2.ogg','sfx/bounce2.mp3','sfx/bounce2.wav'],
                win: ['sfx/win.ogg','sfx/win.mp3','sfx/win.wav'],
                lose: ['sfx/lose.ogg','sfx/lose.mp3','sfx/lose.wav']
            });

            // to splash scene
            Crafty.scene('Splash');

        }

        function onProgress(p) {
            // p is { loaded: j, total: total, percent: (j / total * 100) ,src:src})
            console.log('loading ' + p.src + ' [' + p.percent + '%]');
        }

        function onError(e) {
            console.log('failed to load asset ' + e);
        }

        Crafty.load(resources, onLoad, onProgress, onError);

    });

    Crafty.scene('Splash', function() {

        var skipped = false;

        Crafty.e("2D, DOM, Image").image("img/splash-bg.png");

        Crafty.e("2D, DOM, Image, Tween").image("img/splash-logo.png")
        .attr({alpha: 0.0, x: 10, y: -129 })
        .tween({alpha: 1.0, y: 240-67 }, 1500);

        Crafty.e('Delay').delay(function() {
            if (!skipped) {
                Crafty.scene('Title');

            }
        }, 3000, 0);

        Crafty.e('2D, DOM, Color, Mouse')
        .color('black')
        .attr({x: 0, y: 0, w: 320, h: 480, alpha: 0.0 })
        .bind('Click', function() {
            skipped = true;
            Crafty.scene('Title');
        });
    });

    Crafty.scene('Title', function() {

        Crafty.e('2D, Canvas, Text')
        .attr({x: 10, y: 24})
        .textColor('#ffffff')
        .textFont({size: '56px', family: 'Helvetica' })
        .text('Racquetball');

        Crafty.e('2D, Canvas, Text, Mouse')
        .attr({x: 104, y: 224})
        .textColor('#ffffff')
        .textFont({size: '34px', family: 'Helvetica' })
        .text('START')
        .bind('Click', function() {
            Crafty.scene('Court');
            Crafty.audio.play('bgm', -1);
        });

        Crafty.e('2D, Canvas, Text, Mouse')
        .attr({x: 84, y: 424})
        .textColor('#ffffff')
        .textFont({size: '34px', family: 'Helvetica' })
        .text('CREDITS')
        .bind('Click', function() {
            Crafty.scene('Credits');
        });

    });

    Crafty.scene('Credits', function() {
        Crafty.e('2D, Canvas, Text')
        .attr({x: 10, y: 224})
        .textColor('#ffffff')
        .textFont({size: '24px', family: 'Helvetica' })
        .text('Developed by Richard Marks');

        Crafty.e('2D, DOM, Color, Mouse')
        .color('black')
        .attr({x: 0, y: 0, w: 320, h: 480, alpha: 0.0 })
        .bind('Click', function() {
            Crafty.scene('Title');
        });

    });

    Crafty.c('Edges', {
        init: function() {
            var t = this;
            Object.defineProperties(t, {
                left:    { get: function() { return t.x; } },
                right:   { get: function() { return t.x + t.w; } },
                top:     { get: function() { return t.y; } },
                bottom:  { get: function() { return t.y + t.h; } },
                centerX: { get: function() { return t.x + t.w / 2; } },
                centerY: { get: function() { return t.y + t.h / 2; } },
                center:  { get: function() { return { x: t.centerX, y: t.centerY }; } }
            });
        }
    });

    Crafty.c('Racquet', {
        _held: false,
        _hitbox: { x: 160 - 48, y: 480 - (48 + 52), w: 96, h: 32 },
        _bounds: { x: { l: 0, h: 320 }, y: { l: 0, h: 480 } },
        init: function() {
            this.requires('2D, Canvas, Image, Mouse, Edges');
            this.image('img/racquet.png')
            .bind('MouseDown', function() { this._held = true; })
            .bind('MouseUp', function() { this._held = false; })
            .bind('MouseMove', function(e) {
                var t = this;
                if (!t._held) {
                    return;
                }

                t._hitbox.x = e.realX - (t._hitbox.w / 2);

                if (t._hitbox.x < t._bounds.x.l) {
                    t._hitbox.x = t._bounds.x.l;
                } else if (t._hitbox.x > t._bounds.x.h - t._hitbox.w) {
                    t._hitbox.x = t._bounds.x.h - t._hitbox.w;
                }

                t.attr({ x: t._hitbox.x }).trigger('Moved', { x: this._x, y: this._y });

            })
            .attr({ x: this._hitbox.x, y: this._hitbox.y, width: this._hitbox.w, height: this._hitbox.h });
        }
    });

    Crafty.c('Ball', {
        _locked: false,
        _hitbox: { x: 160 - 48, y: 480 - (48 + 52), w: 96, h: 32 },
        _bounds: { x: { l: 0, h: 320 }, y: { l: 0, h: 480 } },
        _vel: { x: 1.0, y: -1.0 },
        _speed: 14.0,
        _aorb: true,
        _prevPos: { x: 0, y: 0 },
        init: function() {
            this.requires('2D, Canvas, Image, Collision, Edges');
            this.image('img/ball.png')
            .attr({ x: 160, y: 120, width: 16, height: 16 })
            .bind('EnterFrame', this._onEnterFrame);

            this._hitbox.x.l += 8;
            this._hitbox.x.h -= 8;
            this._hitbox.y.l += 8;
            this._hitbox.y.h -= 8;
        },

        reset: function() {
            var t = this;
            t.attr({ x: 160, y: 120 });
            t._vel.x = 1.0;
            t._vel.y = -1.0;
            t._speed = 14.0;
            t._locked = false;
        },

        _onEnterFrame: function() {
            var t = this, dt = 0.33;
            if (t._locked) {
                return;
            }

            t._aorb = !t._aorb;

            t._prevPos.x = t.x;
            t._prevPos.y = t.y;

            t.x += t._vel.x * t._speed * dt;
            t.y += t._vel.y * t._speed * dt;

            t._checkLostBall();
            t._checkWallCollisions();
            t._checkPaddleCollision();
        },

        _playBounceSfx: function() {
            Crafty.audio.play(this._aorb ? 'bounce' : 'bounce2');
        },

        _playScoreSfx: function() {
            Crafty.audio.play('win');
        },

        _playLostSfx: function() {
            Crafty.audio.play('lose');
        },

        _speedUp: function() {
            var t = this;
            t._speed += 0.8;
            if (t._speed > 60) {
                t._speed = 60;
            }
        },

        _checkLostBall: function() {
            var t = this;
            if (t.y < t._bounds.y.h) {
                return;
            }

            t._playLostSfx();
            t.destroy();
            t._locked = true;

            setTimeout(function() {
                Crafty.trigger('BallLost');
            }, 1000);
        },

        _checkWallCollisions: function() {
            var t = this;

            if (t.x < t._bounds.x.l || t.x > t._bounds.x.h) {
                t.x = t._prevPos.x;
                t._vel.x *= -1;
                t._playBounceSfx();
            }

            if (t.y < t._bounds.y.l) {
                t._playScoreSfx();
                t._speedUp();
                t.y = t._prevPos.y;
                t._vel.y *= -1;
            }
        },

        _checkPaddleCollision: function() {
            var t = this;
            if (t._vel.y < 0) {
                return;
            }

            var hit = t.hit('Racquet')[0];

            if (hit) {
                t._playBounceSfx();
                t._speedUp();
                t._vel.y *= -1;
                t._vel.x = t._calculateBounceVelocity(hit.obj);
            }
        },

        _calculateBounceVelocity: function(subject) {
            var t = this,
                dist = Crafty.math.distance(subject.centerX, subject.centerY, t.centerX, t.centerY);

            var magnitude = dist - ((t.h - subject.h) * 0.5);
            var ratio = magnitude / (subject.w / 2) * 2.5;
            if (t.centerX < subject.centerX) {
                ratio = -ratio;
            }
            return ratio;
        }
    });

    Crafty.scene('Court', function() {
        /*
        var fieldBounds = { x: { l: 0, h: 320 }, y: { l: 0, h: 432 } },
            score = 0,
            racquet = null,
            ball = null,
            scoreDisplay = null,
            muteButton = null,
            pauseButton = null;

        scoreDisplay = Crafty.e('ScoreDisplay');
        ball = Crafty.e('Ball');
        racquet = Crafty.e('Racquet');
        muteButton = Crafty.e('MuteButton');
        pauseButton = Crafty.e('PauseButton');

        Crafty.bind('BallLost', function() {
            Crafty.audio.stop();
            Crafty.trigger('MessageEvent', 'You Lose!');
            Crafty.scene('Message');
        });
        */

        // REFACTORING

        // entities
        // -ball
        // -racquet
        // -score display
        // -mute button
        // -pause button
        // -message display
        // -blackbox

        // ball bounces off racquet and left, top, and right walls
        // ball is destroyed when it passes racquet, and score when hitting top wall
        // racquet moves left and right
        // mute button toggles audio playback
        // pause button halts ball and racquet and displays  'paused' in the message display
        // blackbox is used to fade in and fade out the court scene

        // events
        // -court_loading - triggered immediately when the Court scene is started
        // -court_ready - triggered after court_loading fade completes
        // -court_closing - triggered after message is closed
        // -ball_scored - triggered when ball hits top wall
        // -ball_lost - triggered when ball leaves bottom edge of screen

        // components
        // -actor
        //  provides place(), reset(), show(), activate() functions
        // -blackbox
        //  provides cover(), uncover(), fadeOut(), fadeIn() functions
        // -display
        //  provides display(), modal() functions

        Crafty.c('blackbox', {
            init: function() {
                this.requires('2D, DOM, Color, Tween')
                .attr({ x: 0, y: 0, w: 320, h: 480, alpha: 1.0 })
                .color('#000000');
            },
            cover: function() {
                this.attr({ alpha: 1.0 });
                return this;
            },
            uncover: function() {
                this.attr({ alpha: 0.0 });
                return this;
            },
            fadeOut: function(duration) {
                this.tween({alpha: 0.0 }, duration);
                return this;
            },
            fadeIn: function(duration) {
                this.tween({alpha: 1.0 }, duration);
                return this;
            }
        });

        Crafty.c('actor', {
            _startX: 0,
            _startY: 0,
            _activated: false,
            init: function() {
                this.requires('2D, Canvas');
            },
            place: function(x, y) {
                this._startX = x;
                this._startY = y;
                this.x = x;
                this.y = y;
                return this;
            },
            reset: function() {
                this.x = this._startX;
                this.y = this._startY;
                return this;
            },
            show: function() {
                this.attr({ alpha: 1.0 });
                return this;
            },
            activate: function() {
                this._activated = true;
                return this;
            }
        });

        Crafty.c('display', {
            init: function() {
                this.requires('2D, DOM, Text');
            },
            display: function(what) {
                this.textColor('#ffffff')
                .textFont({size: '36px', family: 'Helvetica' })
		.text(what)
		.css('text-align', 'center');
                return this;
            },
            modal: function() {
                var t = this;
                Crafty.e('2D, DOM, Mouse, modal')
                .attr({
                    x: 0,
                    y: 0,
                    w: Crafty.stage.elem.clientWidth,
                    h: Crafty.stage.elem.clientHeight
                })
                .bind('Click', function() {
                    t.trigger('dismiss');
                    this.destroy();
                });
                return this;
            }
        });

        Crafty.c('racquet', {
            init: function() {
                this.requires('Image');
            },
        });

        var blackbox = Crafty.e('blackbox').uncover();

        var racquet = Crafty.e('actor, racquet').place();
        var ball = Crafty.e('actor, ball').place();

        var scoreDisplay = Crafty.e('display');

        Crafty.bind('court_loading', function() {
            blackbox.cover().fadeOut(1500).bind('TweenEnd', function() {
                racquet.reset().show();
                ball.reset().show();
                score = 0;
                scoreDisplay.display(score);
                Crafty.trigger('court_ready');
            });
        });

        Crafty.bind('court_ready', function() {
            setTimeout(function() {
                racquet.activate();
                ball.activate();
            }, 1000);
        });

        Crafty.bind('court_closing', function() {
            blackbox.uncover().fadeIn(1500).bind('TweenEnd', function() {
                Crafty.scene('Title');
            });
        });

        Crafty.bind('ball_scored', function() {
            score++;
            scoreDisplay.display(score);
            if (score >= 15) {
                message.display('You Win!').modal().bind('dismiss', function() {
                    Crafty.trigger('court_closing');
                });
            }
        });

        Crafty.bind('ball_lost', function() {
            message.display('You Lose!').modal().bind('dismiss', function() {
                Crafty.trigger('court_closing');
            });
        });

        // the court scene actually starts here
        Crafty.trigger('court_loading');

    }, function() {
        // remove global event bindings when the court scene exits
        Crafty.unbind('court_loading');
        Crafty.unbind('court_ready');
        Crafty.unbind('court_closing');
        Crafty.unbind('ball_scored');
        Crafty.unbind('ball_lost');

        // stop audio playback
        Crafty.audio.stop();
    });

    Crafty.scene('Message', function() {
        Crafty.bind('MessageEvent', function(msg) {
            Crafty.e('2D, Canvas, Text')
            .attr({x: 10, y: 24})
            .textColor('#ffffff')
            .textFont({size: '56px', family: 'Helvetica' })
            .text(msg);
        });

        Crafty.e('2D, DOM, Color, Mouse')
        .color('black')
        .attr({x: 0, y: 0, w: 320, h: 480, alpha: 0.0 })
        .bind('Click', function() {
            Crafty.scene('Title');
        });
    });

    Crafty.scene('Loading');

    return {
        VERSION: '1.0',
        COPYRIGHT: '(C) 2014, Bang Bang Attack Studios',
        WEBSITE: 'http://www.bangbangattackstudios.com',
        DEVELOPER: 'Richard Marks of Bang Bang Attack Studios'
    };
})();<|endoftext|>


<commit_before>import _ from 'lodash';
import config from 'config';
import fetch from 'isomorphic-fetch';
import sgDomain from './domain';
import { objects as sgObjects } from './object';
import { objectCalls as sgObjectCalls } from './object-call';
import { names as sgObjectNames } from './object';
import sgUtil from './util';
import URI from 'urijs';

// https://apihelp.surveygizmo.com/help/article/link/versions-methods
const restApiVersion = 'v4';

const prototype = {
  config: Object.assign({}, config.get('restApi')),
  getRestApiUri(path) {
    return new URI(
      {
        protocol: 'https',
        hostname: sgDomain(this.config.regionName).hostname,
        path: `/${restApiVersion}/${path}`,
      }
    )
    .query({
      api_token: this.config.auth.key,
      api_token_secret: this.config.auth.secretKey,
    })
    ;
  },

  wrapRestApiFetch(method, path, options = {}, callback = undefined) {
    const restApiUri = this.getRestApiUri(path);
    return fetch(
      restApiUri.toString(), {
        method,
      }
    )
    .then(response => {
      if (response.status >= 400) {
        throw new Error(
          `REST API responded to ${method} ${restApiUri.path()} with status ${response.status}`
        );
      }
      return response.json();
    })
    .then(result => {
      if (_.isUndefined(result.result_ok)) {
        throw new Error(
          `REST API responded to ${method} ${restApiUri.path()} with undefined result`
        );
      }

      if (!result.result_ok) {
        throw new Error(
          `REST API responded to ${method} ${restApiUri.path()} with error result: `
          + `${result.message} (code ${result.code})`
        );
      }

      return result.data;
    })
    .then(data => {
      if (_.isUndefined(callback)) {
        return data;
      }
      return callback(null, data);
    })
    .catch(reason => {
      if (_.isUndefined(callback)) {
        throw new Error(reason);
      }
      return callback(reason);
    })
    ;
  },
};

const assignOptionsAndCallbackFromArgs = (args) => {
  let options = {};
  let callback;

  if (args.length !== 0) {
    if (args.length === 1) {
      if (!_.isFunction(args[0]) && _.isObject(args[0])) {
        options = args[0];
      } else if (_.isFunction(args[0])) {
        callback = args[0];
      } else {
        throw new Error(
          'Invalid argument'
        );
      }
    } else if (args.length === 2) {
      if ((!_.isFunction(args[0]) && _.isObject(args[0])) && _.isFunction(args[1])) {
        [options, callback] = args;
      } else {
        throw new Error(
          'Invalid arguments'
        );
      }
    } else {
      throw new Error(
        'Invalid number of arguments'
      );
    }
  }

  return [options, callback];
};

const objectCallerFactories = {};

objectCallerFactories[sgObjectCalls.LIST.name] = object => ({
  name: `get${_.upperFirst(object.pluralName)}`,
  func(...args) {
    const [options, callback] = assignOptionsAndCallbackFromArgs(args);
    return this.wrapRestApiFetch(
      'GET', `${object.getRoute(options)}`, options, callback
    );
  },
});

objectCallerFactories[sgObjectCalls.GET.name] = object => ({
  name: `get${_.upperFirst(object.name)}`,
  func(objectId, ...args) {
    const [options, callback] = assignOptionsAndCallbackFromArgs(args);
    options[`${object.name}Id`] = objectId;
    return this.wrapRestApiFetch(
      'GET', `${object.getRoute(options)}`, options, callback
    );
  },
});

objectCallerFactories[sgObjectCalls.CREATE.name] = object => ({
  name: `create${_.upperFirst(object.name)}`,
  func(objectId, ...args) {
    const [options, callback] = assignOptionsAndCallbackFromArgs(args);
    options[`${object.name}Id`] = objectId;
    console.log('Stub CREATE ${object.name}');
  },
});

objectCallerFactories[sgObjectCalls.UPDATE.name] = object => ({
  name: `update${_.upperFirst(object.name)}`,
  func(objectId, ...args) {
    const [options, callback] = assignOptionsAndCallbackFromArgs(args);
    options[`${object.name}Id`] = objectId;
    console.log('Stub UPDATE ${object.name} ${objectId}');
  },
});

objectCallerFactories[sgObjectCalls.DELETE.name] = object => ({
  name: `delete${_.upperFirst(object.name)}`,
  func(objectId, ...args) {
    const [options, callback] = assignOptionsAndCallbackFromArgs(args);
    options[`${object.name}Id`] = objectId;
    console.log('Stub DELETE ${object.name} ${objectId}');
  },
});

objectCallerFactories[sgObjectCalls.COPY.name] = object => ({
  name: `copy${_.upperFirst(object.name)}`,
  func(objectId, ...args) {
    const [options, callback] = assignOptionsAndCallbackFromArgs(args);
    options[`${object.name}Id`] = objectId;
    console.log('Stub COPY ${object.name} ${objectId}');
  },
});

sgObjectNames.forEach(objectName => {
  const object = sgObjects[objectName];
  if (!_.isUndefined(object.calls)) {
    Object.keys(object.calls).forEach(objectCallName => {
      if (!sgUtil.isValidObjectCallName(objectCallName)) {
        throw new Error(`SurveyGizmo Object ${objectName} defines invalid call ${objectCallName}`);
      }
      const objectCaller = objectCallerFactories[objectCallName](object);
      prototype[objectCaller.name] = objectCaller.func;
    });
  }
});

const restApi = (configOverrides = {}) => {
  const instance = Object.create(prototype);
  instance.config = Object.assign({}, instance.config, configOverrides);

  return instance;
};

export default restApi;
<commit_msg>feat(rest-api): introduce DELETE method wrapper<commit_after>import _ from 'lodash';
import config from 'config';
import fetch from 'isomorphic-fetch';
import sgDomain from './domain';
import { objects as sgObjects } from './object';
import { objectCalls as sgObjectCalls } from './object-call';
import { names as sgObjectNames } from './object';
import sgUtil from './util';
import URI from 'urijs';

// https://apihelp.surveygizmo.com/help/article/link/versions-methods
const restApiVersion = 'v4';

const prototype = {
  config: Object.assign({}, config.get('restApi')),
  getRestApiUri(path) {
    return new URI(
      {
        protocol: 'https',
        hostname: sgDomain(this.config.regionName).hostname,
        path: `/${restApiVersion}/${path}`,
      }
    )
    .query({
      api_token: this.config.auth.key,
      api_token_secret: this.config.auth.secretKey,
    })
    ;
  },

  wrapRestApiFetch(method, path, options = {}, callback = undefined) {
    const restApiUri = this.getRestApiUri(path);
    return fetch(
      restApiUri.toString(), {
        method,
      }
    )
    .then(response => {
      if (response.status >= 400) {
        throw new Error(
          `REST API responded to ${method} ${restApiUri.path()} with status ${response.status}`
        );
      }
      return response.json();
    })
    .then(result => {
      if (_.isUndefined(result.result_ok)) {
        throw new Error(
          `REST API responded to ${method} ${restApiUri.path()} with undefined result`
        );
      }

      if (!result.result_ok) {
        throw new Error(
          `REST API responded to ${method} ${restApiUri.path()} with error result: `
          + `${result.message} (code ${result.code})`
        );
      }

      return result.data;
    })
    .then(data => {
      if (_.isUndefined(callback)) {
        return data;
      }
      return callback(null, data);
    })
    .catch(reason => {
      if (_.isUndefined(callback)) {
        throw new Error(reason);
      }
      return callback(reason);
    })
    ;
  },
};

const assignOptionsAndCallbackFromArgs = (args) => {
  let options = {};
  let callback;

  if (args.length !== 0) {
    if (args.length === 1) {
      if (!_.isFunction(args[0]) && _.isObject(args[0])) {
        options = args[0];
      } else if (_.isFunction(args[0])) {
        callback = args[0];
      } else {
        throw new Error(
          'Invalid argument'
        );
      }
    } else if (args.length === 2) {
      if ((!_.isFunction(args[0]) && _.isObject(args[0])) && _.isFunction(args[1])) {
        [options, callback] = args;
      } else {
        throw new Error(
          'Invalid arguments'
        );
      }
    } else {
      throw new Error(
        'Invalid number of arguments'
      );
    }
  }

  return [options, callback];
};

const objectCallerFactories = {};

objectCallerFactories[sgObjectCalls.LIST.name] = object => ({
  name: `get${_.upperFirst(object.pluralName)}`,
  func(...args) {
    const [options, callback] = assignOptionsAndCallbackFromArgs(args);
    return this.wrapRestApiFetch(
      'GET', `${object.getRoute(options)}`, options, callback
    );
  },
});

objectCallerFactories[sgObjectCalls.GET.name] = object => ({
  name: `get${_.upperFirst(object.name)}`,
  func(objectId, ...args) {
    const [options, callback] = assignOptionsAndCallbackFromArgs(args);
    options[`${object.name}Id`] = objectId;
    return this.wrapRestApiFetch(
      'GET', `${object.getRoute(options)}`, options, callback
    );
  },
});

objectCallerFactories[sgObjectCalls.CREATE.name] = object => ({
  name: `create${_.upperFirst(object.name)}`,
  func(objectId, ...args) {
    const [options, callback] = assignOptionsAndCallbackFromArgs(args);
    options[`${object.name}Id`] = objectId;
    console.log('Stub CREATE ${object.name}');
  },
});

objectCallerFactories[sgObjectCalls.UPDATE.name] = object => ({
  name: `update${_.upperFirst(object.name)}`,
  func(objectId, ...args) {
    const [options, callback] = assignOptionsAndCallbackFromArgs(args);
    options[`${object.name}Id`] = objectId;
    console.log('Stub UPDATE ${object.name} ${objectId}');
  },
});

objectCallerFactories[sgObjectCalls.DELETE.name] = object => ({
  name: `delete${_.upperFirst(object.name)}`,
  func(objectId, ...args) {
    const [options, callback] = assignOptionsAndCallbackFromArgs(args);
    options[`${object.name}Id`] = objectId;
    return this.wrapRestApiFetch(
      'DELETE', `${object.getRoute(options)}`, options, callback
    );
  },
});

objectCallerFactories[sgObjectCalls.COPY.name] = object => ({
  name: `copy${_.upperFirst(object.name)}`,
  func(objectId, ...args) {
    const [options, callback] = assignOptionsAndCallbackFromArgs(args);
    options[`${object.name}Id`] = objectId;
    console.log('Stub COPY ${object.name} ${objectId}');
  },
});

sgObjectNames.forEach(objectName => {
  const object = sgObjects[objectName];
  if (!_.isUndefined(object.calls)) {
    Object.keys(object.calls).forEach(objectCallName => {
      if (!sgUtil.isValidObjectCallName(objectCallName)) {
        throw new Error(`SurveyGizmo Object ${objectName} defines invalid call ${objectCallName}`);
      }
      const objectCaller = objectCallerFactories[objectCallName](object);
      prototype[objectCaller.name] = objectCaller.func;
    });
  }
});

const restApi = (configOverrides = {}) => {
  const instance = Object.create(prototype);
  instance.config = Object.assign({}, instance.config, configOverrides);

  return instance;
};

export default restApi;
<|endoftext|>


<commit_before>
import Vue from 'vue'
import VueI18n from 'vue-i18n'

const DEFAULT_LOCALE = 'en-US'

Vue.use(VueI18n)

const i18n = new VueI18n({
    locale: navigator.language, // set locale
    fallbackLocale: 'en-US',
    messages: {
      'en-US': require('./locales/en-US.json'),
      'sk': require('./locales/sk.json')
    }
})

export default i18n
<commit_msg>workaround for invalid i18n locale fallback<commit_after>
import Vue from 'vue'
import VueI18n from 'vue-i18n'

const DEFAULT_LOCALE = 'en-US'

Vue.use(VueI18n)

const i18n = new VueI18n({
    locale: navigator.language, // set locale
    fallbackLocale: 'en-US',
    messages: {
      'en-US': require('./locales/en-US.json'),
      'sk-SK': require('./locales/sk.json'),
      'sk': require('./locales/sk.json')
    }
})

export default i18n
<|endoftext|>


<commit_before>/**
 * This class creates an interface for validating, storing, and retrieving xAPI statements
 *
 * For more information, see:
 * https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Data.md
 */
import filter from 'lodash/filter';
import find from 'lodash/find';
import get from 'lodash/get';
import set from 'lodash/set';
import unset from 'lodash/unset';
import isMatch from 'lodash/isMatch';
import isNumber from 'lodash/isNumber';
import overSome from 'lodash/overSome';
import BaseShim from '../baseShim';
import { XAPIVerbMap } from './xAPIVocabulary';
import { OBJECT_TYPES } from './xAPIConstants';

function actorsEqual(actor1, actor2) {
  if (actor1.mbox && actor2.mbox) {
    return actor1.mbox === actor2.mbox;
  }
  if (actor1.mbox_sha1sum && actor2.mbox_sha1sum) {
    return actor1.mbox_sha1sum === actor2.mbox_sha1sum;
  }
  if (actor1.openid && actor2.openid) {
    return actor2.openid === actor2.openid;
  }
  if (actor1.account && actor2.account) {
    return actor1.homePage === actor2.homePage && actor1.name && actor2.name;
  }
  return false;
}

const STATE = 'state';

const ACTIVITY_PROFILE = 'activityProfile';

const AGENT_PROFILE = 'agentProfile';

const STATEMENT = 'statement';
export default class xAPI extends BaseShim {
  constructor(mediator) {
    super(mediator);
    this.data = {};
    this.userData = {};
    this.nameSpace = 'xAPI';
    this.__setData = this.__setData.bind(this);
    this.__setUserData = this.__setUserData.bind(this);
    this.on(this.events.STATEUPDATE, this.__setData);
    this.on(this.events.USERDATAUPDATE, this.__setUserData);
  }

  __setData(data = {}) {
    this.data = data;
  }

  __setUserData(userData = {}) {
    this.userData = userData;
  }

  /*
   * Applies heuristics based on the CMI5 vocabulary:
   * https://github.com/AICC/CMI-5_Spec_Current/blob/quartz/cmi5_spec.md#xapi_data_model
   * and the verbs that are defined for use in H5P, in order to calculate progress
   * @return {Number|null} returns a value between 0 or 1 or null if no progress could
   * be calculated.
   */
  __calculateProgress() {
    if (
      find(
        this.data[STATEMENT],
        s =>
          s.verb.id === XAPIVerbMap.mastered ||
          s.verb.id === XAPIVerbMap.passed ||
          s.verb.id === XAPIVerbMap.completed ||
          (s.result && s.result.success)
      )
    ) {
      return 1;
    }
    const scoreStatement = find(
      this.data[STATEMENT],
      s =>
        s.result &&
        s.result.score &&
        (s.result.score.scaled || (s.result.score.min && s.result.score.max && s.result.score.raw))
    );
    if (scoreStatement) {
      if (scoreStatement.result.score.scaled) {
        return scoreStatement.result.score.scaled;
      }
      return (
        (scoreStatement.result.score.raw - scoreStatement.result.score.min) /
        (scoreStatement.result.score.max - scoreStatement.result.score.min)
      );
    }
    return null;
  }

  createAgent() {
    return {
      mbox: `mailto:${this.userData.userId}@kolibri.to`,
    };
  }

  iframeInitialize(contentWindow) {
    this.__setXAPIInterface();
    Object.defineProperty(contentWindow, this.nameSpace, {
      value: this.xAPIInterface,
      configurable: true,
    });
  }

  getStatement(statementId) {
    return (this.data[STATEMENT] || []).find(s => s.id === statementId);
  }

  getStatements({
    statementId,
    voidedStatementId,
    agent,
    verb,
    activity,
    registration,
    related_activities,
    related_agents,
    since,
    until,
    limit,
    ascending,
  }) {
    const orFns = [];
    let statements = this.data[STATEMENT] || [];
    if (agent) {
      if (!related_agents) {
        if (!actorsEqual(agent, this.createAgent())) {
          // Searching by an agent that is not our agent, so just return nothing
          statements = [];
        }
      } else {
        orFns.push(
          s => actorsEqual(s.object, agent),
          s =>
            s.object.objectType === OBJECT_TYPES.SUBSTATEMENT && actorsEqual(s.object.actor, agent),
          s => s.context && s.context.instructor && actorsEqual(s.context.instructor, agent),
          s => s.context && s.context.team && actorsEqual(s.context.team, agent),
          s => s.authority && s.authority && actorsEqual(s.authority, agent)
        );
      }
    }
    if (statementId && voidedStatementId) {
      return null;
    } else if (statementId) {
      return statements.find(s => s.id === statementId);
    } else if (voidedStatementId) {
      return statements.find(s => s.id === voidedStatementId);
    }
    const match = {};
    if (verb) {
      match.verb = { id: verb };
    }
    if (activity) {
      const activityMatch = {
        object: { id: activity, objectType: OBJECT_TYPES.ACTIVITY },
      };
      if (!related_activities) {
        Object.assign(match, activityMatch);
      } else {
        orFns.push(
          s => isMatch(s, activityMatch),
          s => s.object.objectType === OBJECT_TYPES.SUBSTATEMENT && isMatch(activityMatch),
          s =>
            s.context &&
            Object.values(s.context.contextActivities || {}).some(a =>
              a.some(act => act.id === activity)
            )
        );
      }
    }
    if (registration) {
      match.context = { registration: registration };
    }
    statements = filter(statements, match);
    const orFilter = overSome(orFns);
    statements = statements.filter(statements, orFilter);
    if (since) {
      statements = statements.filter(s => s.stored > since);
    }
    if (until) {
      statements = statements.filter(s => s.stored <= until);
    }
    if (ascending) {
      // Because we used filter above, this should be a new array
      // not directly from the data 'statements' property, so reversing in place is safe
      statements.reverse();
    }
    if (limit && isNumber(limit)) {
      statements = statements.slice(0, limit);
    }
    return statements;
  }

  storeStatements(...statements) {
    // Note that this stores statements with the most recent first
    // to allow for the default get behaviour.
    for (let i = 0; i < statements.length; i++) {
      statements[i].stored = this.__now().toISOString();
      if (!statements[i].timestamp) {
        statements[i].timestamp = this.__now().toISOString();
      }
      // We do not currently support attachments, so remove them before
      // saving.
      delete statements[i].attachments;
    }
    this.data[STATEMENT] = this.data[STATEMENT] || [];
    this.data[STATEMENT].unshift(...statements.reverse());
    this.stateUpdated();
    return statements.reverse();
  }

  getValue(nameSpace, id) {
    if (id) {
      return get(this.data, [nameSpace, id, 'value']);
    }
    return get(this.data, nameSpace, {});
  }

  getIds(nameSpace, since = '') {
    return Object.keys(this.data[nameSpace] || {}).filter(
      id => this.data[nameSpace][id].stored > since
    );
  }

  setValue(nameSpace, id, stateObject) {
    set(this.data, [nameSpace, id], {
      stored: this.__now().toISOString(),
      value: stateObject,
    });
    this.stateUpdated();
  }

  sendValue(nameSpace, id, value) {
    return new Promise((resolve, reject) => {
      try {
        JSON.stringify(value);
      } catch (e) {
        reject('Can only store JSON serializable values');
      }
      if (!id) {
        reject('Must specify an id');
      }
      this.setValue(nameSpace, id, value);
      resolve();
    });
  }

  deleteValue(nameSpace, id) {
    if (id) {
      unset(this.data, [nameSpace, id]);
    } else {
      unset(this.data, nameSpace);
    }
    this.stateUpdated();
  }

  /*
   * A simple interface for interacting with xAPI
   * Can be used by Kolibri aware HTML5 apps to
   * directly manage xAPI data.
   */
  __setXAPIInterface() {
    const self = this;

    class Shim {
      /*
       * Modify an xAPI statement in place to add non-learning activity specific properties.
       * @param {Object} statement - an object representing an xAPI statement's JSON
       */
      prepareStatement(statement) {
        if (!statement.actor) {
          statement.actor = self.createAgent();
        }
        if (!statement.timestamp) {
          statement.timestamp = self.__now().toISOString();
        }
      }
      /*
       * Store a statement.
       * @param {Object} statement - an object representing an xAPI statement's JSON
       * @param {Boolean} [compress=false] - whether we should discard some data before storage
       * @return {Promise} a Promise that resolves when the statement has been successfully stored
       */
      sendStatement(statement, compress = false) {
        return new Promise((resolve, reject) => {
          return import(
            /* webpackChunkName: "xAPISchema", webpackPrefetch: true */ './xAPISchema'
          ).then(({ Statement }) => {
            this.prepareStatement(statement);
            try {
              statement = Statement.clean(statement);
            } catch (e) {
              reject(e);
              return;
            }
            if (compress) {
              // If we are compressing, then remove things that we
              // can probably reconstruct
              delete statement.actor;
              delete statement.authority;
              if (
                !statement.object.objectType ||
                statement.object.objectType === OBJECT_TYPES.ACTIVITY
              ) {
                delete statement.object;
              }
            }
            self.storeStatements(statement);
            self.stateUpdated();
            resolve();
          });
        });
      }
      /*
       * Get statements that have previously been recorded
       * Based on LRS behaviour defined here:
       * https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Communication.md#213-get-statements
       * @param {Object} params - an object with search params for statements
       * @param {string} [params.statementId] - Id of Statement
       * @param {string} [params.voidedStatementId] - Id of voided Statement
       * @param {Object} [params.agent] - Agent object that is the agent of the statement
       * @param {string} [params.verb] - Verb IRI
       * @param {string} [params.activity] - Activity IRI that matches Object of statement
       * @param {string} [params.registration] - Registration UUID
       * @param {Boolean} [params.related_activities=false] - Check activity IRI against any
       * activity in the statement
       * @param {Boolean} [params.related_agents=false] - Check agent object against any
       * agent in the statement
       * @param {string} [params.since] - Time to check statements since
       * @param {string} [params.until] - Time to check statements until
       * @param {Number} [params.limit] - Maximum number of statements to return
       * @param {Boolean} [params.ascending=false] - Whether to return statements in ascending order
       * @return {Promise} Promise that will resolve with the statements
       */
      getStatements(params) {
        return Promise.resolve(self.getStatements(params));
      }
      /*
       * Gets an Activity object if stored.
       * @param {string} id   the id of the Activity to get
       * @return {Promise} Promise that will resolve to the activity
       */
      getActivities(id) {
        return Promise.resolve({ id });
      }
      /*
       * Store state
       * @param {string} stateId - key for the state to store
       * @param {string} stateValue - the value for the state to store
       * @return {Promise} Promise that will resolve when the state is stored
       */
      sendState(stateId, stateValue) {
        return self.sendValue(STATE, stateId, stateValue);
      }
      /*
       * Retrieve state
       * @param {string} stateId - key for the state
       * @return {Promise} Promise that will resolve with the value or null
       */
      getState(stateId) {
        return Promise.resolve(self.getValue(STATE, stateId) || null);
      }
      /*
       * Retrieve multiple pieces of state, stored since a certain time
       * https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Communication.md#multiple-document-get
       * @param {string} since - timestamp to retrieve state stored since
       * @return {Promise} Promise that will resolve with the matching state ids
       */
      getStateIds(since) {
        return Promise.resolve(self.getIds(STATE, since));
      }
      /*
       * Delete state
       * @param {string} stateId - key for the state
       * @return {Promise} Promise that will resolve when deleted
       */
      deleteState(stateId) {
        self.deleteValue(STATE, stateId);
        return Promise.resolve();
      }
      /*
       * Store activity profile id and value
       * https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Communication.md#single-document-put--post--get--delete-1
       * @param {string} id - key for the activity profile to store
       * @param {string} value - the value for the activity profile to store
       * @return {Promise} Promise that will resolve when the activity profile is stored
       */
      sendActivityProfile(id, value) {
        return self.sendValue(ACTIVITY_PROFILE, id, value);
      }
      /*
       * Get activity profile by id
       * https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Communication.md#single-document-put--post--get--delete-1
       * @param {string} id - key for the activity profile to store
       * @return {Promise} Promise that will resolve with the activity profile or null
       */
      getActivityProfile(id) {
        return Promise.resolve(self.getValue(ACTIVITY_PROFILE, id) || null);
      }
      /*
       * Retrieve multiple activity profile ids, stored since a certain time
       * https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Communication.md#multiple-document-get-2
       * @param {string} since - timestamp to retrieve activity profiles stored since
       * @return {Promise} Promise that will resolve with the matching activity profile ids
       */
      getActivityProfileIds(since) {
        return Promise.resolve(self.getIds(ACTIVITY_PROFILE, since));
      }
      /*
       * Delete activity profile
       * @param {string} id - key for the activity profile
       * @return {Promise} Promise that will resolve when deleted
       */
      deleteActivityProfile(id) {
        self.deleteValue(ACTIVITY_PROFILE, id);
        return Promise.resolve();
      }
      /*
       * Return the agent to use in statements
       * https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Communication.md#combined-information-get
       * return {Promise} Promise that resolves to the agent
       */
      getAgent() {
        return Promise.resolve(self.createAgent());
      }
      /*
       * Store agent profile id and value
       * https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Communication.md#single-agent-or-profile-document-put--post--get--delete
       * @param {string} id - key for the agent profile to store
       * @param {string} value - the value for the agent profile to store
       * @return {Promise} Promise that will resolve when the agent profile is stored
       */
      sendAgentProfile(id, value) {
        return self.sendValue(AGENT_PROFILE, id, value);
      }
      /*
       * Get agent profile by id
       * https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Communication.md#single-agent-or-profile-document-put--post--get--delete
       * @param {string} id - key for the agent profile to store
       * @return {Promise} Promise that will resolve with the agent profile or null
       */
      getAgentProfile(id) {
        return Promise.resolve(self.getValue(AGENT_PROFILE, id) || null);
      }
      /*
       * Retrieve multiple agent profile ids, stored since a certain time
       * https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Communication.md#multiple-document-get-1
       * @param {string} since - timestamp to retrieve agent profiles stored since
       * @return {Promise} Promise that will resolve with the matching agent profile ids
       */
      getAgentProfileIds(since) {
        return Promise.resolve(self.getIds(AGENT_PROFILE, since));
      }
      /*
       * Delete agent profile
       * @param {string} id - key for the agent profile
       * @return {Promise} Promise that will resolve when deleted
       */
      deleteAgentProfile(id) {
        self.deleteValue(AGENT_PROFILE, id);
        return Promise.resolve();
      }
    }
    this.xAPIInterface = new Shim();

    return this.xAPIInterface;
  }
}
<commit_msg>Simplify xAPI progress calculation code to prevent premature completion.<commit_after>/**
 * This class creates an interface for validating, storing, and retrieving xAPI statements
 *
 * For more information, see:
 * https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Data.md
 */
import filter from 'lodash/filter';
import find from 'lodash/find';
import get from 'lodash/get';
import set from 'lodash/set';
import unset from 'lodash/unset';
import isMatch from 'lodash/isMatch';
import isNumber from 'lodash/isNumber';
import overSome from 'lodash/overSome';
import BaseShim from '../baseShim';
import { XAPIVerbMap } from './xAPIVocabulary';
import { OBJECT_TYPES } from './xAPIConstants';

function actorsEqual(actor1, actor2) {
  if (actor1.mbox && actor2.mbox) {
    return actor1.mbox === actor2.mbox;
  }
  if (actor1.mbox_sha1sum && actor2.mbox_sha1sum) {
    return actor1.mbox_sha1sum === actor2.mbox_sha1sum;
  }
  if (actor1.openid && actor2.openid) {
    return actor2.openid === actor2.openid;
  }
  if (actor1.account && actor2.account) {
    return actor1.homePage === actor2.homePage && actor1.name && actor2.name;
  }
  return false;
}

const STATE = 'state';

const ACTIVITY_PROFILE = 'activityProfile';

const AGENT_PROFILE = 'agentProfile';

const STATEMENT = 'statement';
export default class xAPI extends BaseShim {
  constructor(mediator) {
    super(mediator);
    this.data = {};
    this.userData = {};
    this.nameSpace = 'xAPI';
    this.__setData = this.__setData.bind(this);
    this.__setUserData = this.__setUserData.bind(this);
    this.on(this.events.STATEUPDATE, this.__setData);
    this.on(this.events.USERDATAUPDATE, this.__setUserData);
  }

  __setData(data = {}) {
    this.data = data;
  }

  __setUserData(userData = {}) {
    this.userData = userData;
  }

  /*
   * Applies heuristics based on the CMI5 vocabulary:
   * https://github.com/AICC/CMI-5_Spec_Current/blob/quartz/cmi5_spec.md#xapi_data_model
   * and the verbs that are defined for use in H5P, in order to calculate progress
   * @return {Number|null} returns a value between 0 or 1 or null if no progress could
   * be calculated.
   */
  __calculateProgress() {
    const successStatement = find(
      this.data[STATEMENT],
      s =>
        s.verb.id === XAPIVerbMap.mastered ||
        s.verb.id === XAPIVerbMap.passed ||
        s.verb.id === XAPIVerbMap.completed
    );
    if (successStatement) {
      return 1;
    }
    // If there has been any interaction return some progress, otherwise null.
    return Object.keys(this.data[STATEMENT]).length ? 0.01 : null;
  }

  createAgent() {
    return {
      mbox: `mailto:${this.userData.userId}@kolibri.to`,
    };
  }

  iframeInitialize(contentWindow) {
    this.__setXAPIInterface();
    Object.defineProperty(contentWindow, this.nameSpace, {
      value: this.xAPIInterface,
      configurable: true,
    });
  }

  getStatement(statementId) {
    return (this.data[STATEMENT] || []).find(s => s.id === statementId);
  }

  getStatements({
    statementId,
    voidedStatementId,
    agent,
    verb,
    activity,
    registration,
    related_activities,
    related_agents,
    since,
    until,
    limit,
    ascending,
  }) {
    const orFns = [];
    let statements = this.data[STATEMENT] || [];
    if (agent) {
      if (!related_agents) {
        if (!actorsEqual(agent, this.createAgent())) {
          // Searching by an agent that is not our agent, so just return nothing
          statements = [];
        }
      } else {
        orFns.push(
          s => actorsEqual(s.object, agent),
          s =>
            s.object.objectType === OBJECT_TYPES.SUBSTATEMENT && actorsEqual(s.object.actor, agent),
          s => s.context && s.context.instructor && actorsEqual(s.context.instructor, agent),
          s => s.context && s.context.team && actorsEqual(s.context.team, agent),
          s => s.authority && s.authority && actorsEqual(s.authority, agent)
        );
      }
    }
    if (statementId && voidedStatementId) {
      return null;
    } else if (statementId) {
      return statements.find(s => s.id === statementId);
    } else if (voidedStatementId) {
      return statements.find(s => s.id === voidedStatementId);
    }
    const match = {};
    if (verb) {
      match.verb = { id: verb };
    }
    if (activity) {
      const activityMatch = {
        object: { id: activity, objectType: OBJECT_TYPES.ACTIVITY },
      };
      if (!related_activities) {
        Object.assign(match, activityMatch);
      } else {
        orFns.push(
          s => isMatch(s, activityMatch),
          s => s.object.objectType === OBJECT_TYPES.SUBSTATEMENT && isMatch(activityMatch),
          s =>
            s.context &&
            Object.values(s.context.contextActivities || {}).some(a =>
              a.some(act => act.id === activity)
            )
        );
      }
    }
    if (registration) {
      match.context = { registration: registration };
    }
    statements = filter(statements, match);
    const orFilter = overSome(orFns);
    statements = statements.filter(statements, orFilter);
    if (since) {
      statements = statements.filter(s => s.stored > since);
    }
    if (until) {
      statements = statements.filter(s => s.stored <= until);
    }
    if (ascending) {
      // Because we used filter above, this should be a new array
      // not directly from the data 'statements' property, so reversing in place is safe
      statements.reverse();
    }
    if (limit && isNumber(limit)) {
      statements = statements.slice(0, limit);
    }
    return statements;
  }

  storeStatements(...statements) {
    // Note that this stores statements with the most recent first
    // to allow for the default get behaviour.
    for (let i = 0; i < statements.length; i++) {
      statements[i].stored = this.__now().toISOString();
      if (!statements[i].timestamp) {
        statements[i].timestamp = this.__now().toISOString();
      }
      // We do not currently support attachments, so remove them before
      // saving.
      delete statements[i].attachments;
    }
    this.data[STATEMENT] = this.data[STATEMENT] || [];
    this.data[STATEMENT].unshift(...statements.reverse());
    this.stateUpdated();
    return statements.reverse();
  }

  getValue(nameSpace, id) {
    if (id) {
      return get(this.data, [nameSpace, id, 'value']);
    }
    return get(this.data, nameSpace, {});
  }

  getIds(nameSpace, since = '') {
    return Object.keys(this.data[nameSpace] || {}).filter(
      id => this.data[nameSpace][id].stored > since
    );
  }

  setValue(nameSpace, id, stateObject) {
    set(this.data, [nameSpace, id], {
      stored: this.__now().toISOString(),
      value: stateObject,
    });
    this.stateUpdated();
  }

  sendValue(nameSpace, id, value) {
    return new Promise((resolve, reject) => {
      try {
        JSON.stringify(value);
      } catch (e) {
        reject('Can only store JSON serializable values');
      }
      if (!id) {
        reject('Must specify an id');
      }
      this.setValue(nameSpace, id, value);
      resolve();
    });
  }

  deleteValue(nameSpace, id) {
    if (id) {
      unset(this.data, [nameSpace, id]);
    } else {
      unset(this.data, nameSpace);
    }
    this.stateUpdated();
  }

  /*
   * A simple interface for interacting with xAPI
   * Can be used by Kolibri aware HTML5 apps to
   * directly manage xAPI data.
   */
  __setXAPIInterface() {
    const self = this;

    class Shim {
      /*
       * Modify an xAPI statement in place to add non-learning activity specific properties.
       * @param {Object} statement - an object representing an xAPI statement's JSON
       */
      prepareStatement(statement) {
        if (!statement.actor) {
          statement.actor = self.createAgent();
        }
        if (!statement.timestamp) {
          statement.timestamp = self.__now().toISOString();
        }
      }
      /*
       * Store a statement.
       * @param {Object} statement - an object representing an xAPI statement's JSON
       * @param {Boolean} [compress=false] - whether we should discard some data before storage
       * @return {Promise} a Promise that resolves when the statement has been successfully stored
       */
      sendStatement(statement, compress = false) {
        return new Promise((resolve, reject) => {
          return import(
            /* webpackChunkName: "xAPISchema", webpackPrefetch: true */ './xAPISchema'
          ).then(({ Statement }) => {
            this.prepareStatement(statement);
            try {
              statement = Statement.clean(statement);
            } catch (e) {
              reject(e);
              return;
            }
            if (compress) {
              // If we are compressing, then remove things that we
              // can probably reconstruct
              delete statement.actor;
              delete statement.authority;
              if (
                !statement.object.objectType ||
                statement.object.objectType === OBJECT_TYPES.ACTIVITY
              ) {
                delete statement.object;
              }
            }
            self.storeStatements(statement);
            self.stateUpdated();
            resolve();
          });
        });
      }
      /*
       * Get statements that have previously been recorded
       * Based on LRS behaviour defined here:
       * https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Communication.md#213-get-statements
       * @param {Object} params - an object with search params for statements
       * @param {string} [params.statementId] - Id of Statement
       * @param {string} [params.voidedStatementId] - Id of voided Statement
       * @param {Object} [params.agent] - Agent object that is the agent of the statement
       * @param {string} [params.verb] - Verb IRI
       * @param {string} [params.activity] - Activity IRI that matches Object of statement
       * @param {string} [params.registration] - Registration UUID
       * @param {Boolean} [params.related_activities=false] - Check activity IRI against any
       * activity in the statement
       * @param {Boolean} [params.related_agents=false] - Check agent object against any
       * agent in the statement
       * @param {string} [params.since] - Time to check statements since
       * @param {string} [params.until] - Time to check statements until
       * @param {Number} [params.limit] - Maximum number of statements to return
       * @param {Boolean} [params.ascending=false] - Whether to return statements in ascending order
       * @return {Promise} Promise that will resolve with the statements
       */
      getStatements(params) {
        return Promise.resolve(self.getStatements(params));
      }
      /*
       * Gets an Activity object if stored.
       * @param {string} id   the id of the Activity to get
       * @return {Promise} Promise that will resolve to the activity
       */
      getActivities(id) {
        return Promise.resolve({ id });
      }
      /*
       * Store state
       * @param {string} stateId - key for the state to store
       * @param {string} stateValue - the value for the state to store
       * @return {Promise} Promise that will resolve when the state is stored
       */
      sendState(stateId, stateValue) {
        return self.sendValue(STATE, stateId, stateValue);
      }
      /*
       * Retrieve state
       * @param {string} stateId - key for the state
       * @return {Promise} Promise that will resolve with the value or null
       */
      getState(stateId) {
        return Promise.resolve(self.getValue(STATE, stateId) || null);
      }
      /*
       * Retrieve multiple pieces of state, stored since a certain time
       * https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Communication.md#multiple-document-get
       * @param {string} since - timestamp to retrieve state stored since
       * @return {Promise} Promise that will resolve with the matching state ids
       */
      getStateIds(since) {
        return Promise.resolve(self.getIds(STATE, since));
      }
      /*
       * Delete state
       * @param {string} stateId - key for the state
       * @return {Promise} Promise that will resolve when deleted
       */
      deleteState(stateId) {
        self.deleteValue(STATE, stateId);
        return Promise.resolve();
      }
      /*
       * Store activity profile id and value
       * https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Communication.md#single-document-put--post--get--delete-1
       * @param {string} id - key for the activity profile to store
       * @param {string} value - the value for the activity profile to store
       * @return {Promise} Promise that will resolve when the activity profile is stored
       */
      sendActivityProfile(id, value) {
        return self.sendValue(ACTIVITY_PROFILE, id, value);
      }
      /*
       * Get activity profile by id
       * https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Communication.md#single-document-put--post--get--delete-1
       * @param {string} id - key for the activity profile to store
       * @return {Promise} Promise that will resolve with the activity profile or null
       */
      getActivityProfile(id) {
        return Promise.resolve(self.getValue(ACTIVITY_PROFILE, id) || null);
      }
      /*
       * Retrieve multiple activity profile ids, stored since a certain time
       * https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Communication.md#multiple-document-get-2
       * @param {string} since - timestamp to retrieve activity profiles stored since
       * @return {Promise} Promise that will resolve with the matching activity profile ids
       */
      getActivityProfileIds(since) {
        return Promise.resolve(self.getIds(ACTIVITY_PROFILE, since));
      }
      /*
       * Delete activity profile
       * @param {string} id - key for the activity profile
       * @return {Promise} Promise that will resolve when deleted
       */
      deleteActivityProfile(id) {
        self.deleteValue(ACTIVITY_PROFILE, id);
        return Promise.resolve();
      }
      /*
       * Return the agent to use in statements
       * https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Communication.md#combined-information-get
       * return {Promise} Promise that resolves to the agent
       */
      getAgent() {
        return Promise.resolve(self.createAgent());
      }
      /*
       * Store agent profile id and value
       * https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Communication.md#single-agent-or-profile-document-put--post--get--delete
       * @param {string} id - key for the agent profile to store
       * @param {string} value - the value for the agent profile to store
       * @return {Promise} Promise that will resolve when the agent profile is stored
       */
      sendAgentProfile(id, value) {
        return self.sendValue(AGENT_PROFILE, id, value);
      }
      /*
       * Get agent profile by id
       * https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Communication.md#single-agent-or-profile-document-put--post--get--delete
       * @param {string} id - key for the agent profile to store
       * @return {Promise} Promise that will resolve with the agent profile or null
       */
      getAgentProfile(id) {
        return Promise.resolve(self.getValue(AGENT_PROFILE, id) || null);
      }
      /*
       * Retrieve multiple agent profile ids, stored since a certain time
       * https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Communication.md#multiple-document-get-1
       * @param {string} since - timestamp to retrieve agent profiles stored since
       * @return {Promise} Promise that will resolve with the matching agent profile ids
       */
      getAgentProfileIds(since) {
        return Promise.resolve(self.getIds(AGENT_PROFILE, since));
      }
      /*
       * Delete agent profile
       * @param {string} id - key for the agent profile
       * @return {Promise} Promise that will resolve when deleted
       */
      deleteAgentProfile(id) {
        self.deleteValue(AGENT_PROFILE, id);
        return Promise.resolve();
      }
    }
    this.xAPIInterface = new Shim();

    return this.xAPIInterface;
  }
}
<|endoftext|>


<commit_before>;(function($){
    $.convert_format = function(format){
        var fields = {
            d: 'dd',
            H: 'hh',
            I: "HH",
            m: 'mm',
            M: 'MM',
            p: 'PM/AM',
            S: 'ss',
            w: 'w',
            y: 'yy',
            Y: 'yyyy',
            '%' : '%'
        };
        var result = '', i = 0;
        while (i < format.length) {
            if (format.charAt(i) === '%') {
                if(f = fields[format.charAt(i + 1)]){
                  result = result + f;
                }
                ++i;
            } else {
                result = result + format.charAt(i);
            }
            ++i;
        }
        return result;
    }

    $.date_local = {
      days: gettext("Sunday Monday Tuesday Wednesday Thursday Friday Saturday Sunday").split(' '),
      daysShort: gettext("Sun Mon Tue Wed Thu Fri Sat Sun").split(' '),
      daysMin: gettext("Su Mo Tu We Th Fr Sa Su").split(' '),
      months: gettext('January February March April May June July August September October November December').split(' '),
      monthsShort: gettext("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec").split(' '),
      today: gettext("Today"),
      date_string: gettext('%a %d %b %Y %T %Z'),
      ampm: gettext("AM PM").split(' '),
      ampmLower: gettext("am pm").split(' '),
      dateFormat: get_format('DATE_INPUT_FORMATS')[0],
      dateJSFormat: $.convert_format(get_format('DATE_INPUT_FORMATS')[0]),
      timeRepr: gettext('%T')
    }

    $.fn.datepicker.dates['exadmin'] = $.date_local;

    $.fn.exform.renders.push(function(f){
      f.find('.input-append.date').datepicker({format: $.date_local.dateJSFormat, language: 'exadmin', todayBtn: true});
      f.find('.input-append.time input').timepicker({showMeridian: false, showSeconds: true});
    });

})(jQuery)<commit_msg>fix bug. when there isn't time field, no need to initial time picker widget.<commit_after>;(function($){
    $.convert_format = function(format){
        var fields = {
            d: 'dd',
            H: 'hh',
            I: "HH",
            m: 'mm',
            M: 'MM',
            p: 'PM/AM',
            S: 'ss',
            w: 'w',
            y: 'yy',
            Y: 'yyyy',
            '%' : '%'
        };
        var result = '', i = 0;
        while (i < format.length) {
            if (format.charAt(i) === '%') {
                if(f = fields[format.charAt(i + 1)]){
                  result = result + f;
                }
                ++i;
            } else {
                result = result + format.charAt(i);
            }
            ++i;
        }
        return result;
    }

    $.date_local = {
      days: gettext("Sunday Monday Tuesday Wednesday Thursday Friday Saturday Sunday").split(' '),
      daysShort: gettext("Sun Mon Tue Wed Thu Fri Sat Sun").split(' '),
      daysMin: gettext("Su Mo Tu We Th Fr Sa Su").split(' '),
      months: gettext('January February March April May June July August September October November December').split(' '),
      monthsShort: gettext("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec").split(' '),
      today: gettext("Today"),
      date_string: gettext('%a %d %b %Y %T %Z'),
      ampm: gettext("AM PM").split(' '),
      ampmLower: gettext("am pm").split(' '),
      dateFormat: get_format('DATE_INPUT_FORMATS')[0],
      dateJSFormat: $.convert_format(get_format('DATE_INPUT_FORMATS')[0]),
      timeRepr: gettext('%T')
    }

    $.fn.datepicker.dates['exadmin'] = $.date_local;

    $.fn.exform.renders.push(function(f){
      f.find('.input-append.date').datepicker({format: $.date_local.dateJSFormat, language: 'exadmin', todayBtn: true});
      if($.fn.timepicker){
        f.find('.input-append.time input').timepicker({showMeridian: false, showSeconds: true});
      }
    });

})(jQuery)<|endoftext|>