<commit_before>import Controller, { inject as controller } from '@ember/controller';
import { action, computed } from '@ember/object';
import { alias } from '@ember/object/computed';
import { inject as service } from '@ember/service';
import { isPresent } from '@ember/utils';
import { tracked } from '@glimmer/tracking';
import Message from 'hyperchannel/models/message';

export default class BaseChannelController extends Controller {

  @tracked newMessage = null;
  @controller application;
  @service coms;
  @service router;
  @service('remotestorage') storage;

  @alias('application.showChannelMenu') showChannelMenu;

  createMessage (message, type) {
    return new Message({
      type: type,
      date: new Date(),
      // TODO  nickname per channel
      nickname: this.model.account.nickname,
      content: message
    });
  }

  @computed('router.currentRouteName')
  get sidebarClass () {
    const route = this.router.currentRouteName;
    const wideBars = ['shares', 'settings'].map(r => `channel.${r}`);
    return wideBars.includes(route) ? 'sidebar-wide' : 'sidebar-normal';
  }

  @action
  menu (which, what) {
    // Do not toggle sidebav on desktop
    if (which.match(/(global|channel)/) && window.innerWidth > 900) return;

    let menuProp = `show${which.capitalize()}Menu`;

    switch(what) {
      case 'show':
        this.application.set(menuProp, true);
        break;
      case 'hide':
        this.application.set(menuProp, false);
        break;
      case 'toggle':
        this.application.toggleProperty(menuProp);
        break;
    }
  }

  @action
  sendMessage (newMessage) {
    const message = this.createMessage(newMessage, 'message-chat');

    this.coms.transferMessage(
      this.model,
      message.content
    );

    this.model.addMessage(message);
    this.newMessage = null;
  }

  @action
  executeCommand (newMessage) {
    const availableCommands = [
      "help",
      "join",
      "leave",
      "me",
      "msg",
      "part",
      "topic"
    ];
    let commandText = newMessage.substr(1);
    let commandSplitted = commandText.split(" ");
    let command = commandSplitted[0];

    if (availableCommands.includes(command.toLowerCase())) {
      this.send(command + 'Command', commandSplitted.slice(1));
    } else {
      console.warn('[channel]', 'Unknown command', commandText);
    }

    this.newMessage = null;
  }

  @action
  joinCommand (args) {
    const channel = this.coms.createChannel(this.model.account, args[0]);
    // TODO this.storage.saveChannel(channel);
    this.transitionToRoute('channel', channel);
  }

  @action
  partCommand () {
    this.coms.removeChannel(this.model);
    const lastChannel = this.coms.channels.lastObject;
    if (isPresent(lastChannel)) {
      this.transitionToRoute('channel', lastChannel);
    } else {
      // TODO handle zero channels left
      console.warn('No channels left to transition to');
      this.transitionToRoute('index');
    }
  }

  @action
  leaveCommand () {
    this.send('partCommand');
  }

  @action
  helpCommand () {
  }

  @action
  meCommand (args) {
    let newMessage = args.join(' ');

    let message = this.createMessage(newMessage, 'message-chat-me');

    this.coms.transferMeMessage(
      this.model.account,
      this.model.sockethubChannelId,
      message.content
    );

    this.model.addMessage(message);
  }

  @action
  msgCommand (args) {
    let username = args.shift();
    this.coms.createUserChannel(this.model.account, username);
    // TODO fix this, sockethub sends a failure event with error
    // "TypeError: Cannot read property 'indexOf' of undefined"
    // this.coms.transferMessage(username, args.join(' '));
  }

  @action
  topicCommand (args) {
    let channel = this.model;
    let topic = args.join(' ');

    this.coms.changeTopic(channel, topic);
  }

  @action
  addUsernameMentionToMessage (username) {
    const msg = this.newMessage || '';
    if (!msg.match(new RegExp(`^${username}`))) {
      this.newMessage = `${username}: ${msg}`;
    }
  }

}
<commit_msg>Mark outgoing XMPP messages as pending<commit_after>import Controller, { inject as controller } from '@ember/controller';
import { action, computed } from '@ember/object';
import { alias } from '@ember/object/computed';
import { inject as service } from '@ember/service';
import { isPresent } from '@ember/utils';
import { tracked } from '@glimmer/tracking';
import Message from 'hyperchannel/models/message';

export default class BaseChannelController extends Controller {

  @tracked newMessage = null;
  @controller application;
  @service coms;
  @service router;
  @service('remotestorage') storage;

  @alias('application.showChannelMenu') showChannelMenu;

  createMessage (content, type) {
    const message = new Message({
      type: type,
      date: new Date(),
      // TODO  nickname per channel
      nickname: this.model.account.nickname,
      content: content
    });

    if (this.model.protocol === 'XMPP') {
      message.pending = true;
    }

    return message;
  }

  @computed('router.currentRouteName')
  get sidebarClass () {
    const route = this.router.currentRouteName;
    const wideBars = ['shares', 'settings'].map(r => `channel.${r}`);
    return wideBars.includes(route) ? 'sidebar-wide' : 'sidebar-normal';
  }

  @action
  menu (which, what) {
    // Do not toggle sidebav on desktop
    if (which.match(/(global|channel)/) && window.innerWidth > 900) return;

    let menuProp = `show${which.capitalize()}Menu`;

    switch(what) {
      case 'show':
        this.application.set(menuProp, true);
        break;
      case 'hide':
        this.application.set(menuProp, false);
        break;
      case 'toggle':
        this.application.toggleProperty(menuProp);
        break;
    }
  }

  @action
  sendMessage (newMessage) {
    const message = this.createMessage(newMessage, 'message-chat');

    this.coms.transferMessage(
      this.model,
      message.content
    );

    this.model.addMessage(message);
    this.newMessage = null;
  }

  @action
  executeCommand (newMessage) {
    const availableCommands = [
      "help",
      "join",
      "leave",
      "me",
      "msg",
      "part",
      "topic"
    ];
    let commandText = newMessage.substr(1);
    let commandSplitted = commandText.split(" ");
    let command = commandSplitted[0];

    if (availableCommands.includes(command.toLowerCase())) {
      this.send(command + 'Command', commandSplitted.slice(1));
    } else {
      console.warn('[channel]', 'Unknown command', commandText);
    }

    this.newMessage = null;
  }

  @action
  joinCommand (args) {
    const channel = this.coms.createChannel(this.model.account, args[0]);
    // TODO this.storage.saveChannel(channel);
    this.transitionToRoute('channel', channel);
  }

  @action
  partCommand () {
    this.coms.removeChannel(this.model);
    const lastChannel = this.coms.channels.lastObject;
    if (isPresent(lastChannel)) {
      this.transitionToRoute('channel', lastChannel);
    } else {
      // TODO handle zero channels left
      console.warn('No channels left to transition to');
      this.transitionToRoute('index');
    }
  }

  @action
  leaveCommand () {
    this.send('partCommand');
  }

  @action
  helpCommand () {
  }

  @action
  meCommand (args) {
    let newMessage = args.join(' ');

    let message = this.createMessage(newMessage, 'message-chat-me');

    this.coms.transferMeMessage(
      this.model.account,
      this.model.sockethubChannelId,
      message.content
    );

    this.model.addMessage(message);
  }

  @action
  msgCommand (args) {
    let username = args.shift();
    this.coms.createUserChannel(this.model.account, username);
    // TODO fix this, sockethub sends a failure event with error
    // "TypeError: Cannot read property 'indexOf' of undefined"
    // this.coms.transferMessage(username, args.join(' '));
  }

  @action
  topicCommand (args) {
    let channel = this.model;
    let topic = args.join(' ');

    this.coms.changeTopic(channel, topic);
  }

  @action
  addUsernameMentionToMessage (username) {
    const msg = this.newMessage || '';
    if (!msg.match(new RegExp(`^${username}`))) {
      this.newMessage = `${username}: ${msg}`;
    }
  }

}
<|endoftext|>


<commit_before>var boom = require('boom')
var braveHapi = require('../brave-hapi')
var bson = require('bson')
var helper = require('./helper')
var Joi = require('joi')
var underscore = require('underscore')

var v1 = {}

/*
   GET /v1/users/{userId}/sessions[? [since={milliseconds since epoch}&] [limit={positive integer}] ]
       defaults to since=0 limit=100

 */

v1.getSessions =
{ handler: function (runtime) {
  return async function (request, reply) {
    var entries, modifiers, query, result
    var userId = request.params.userId
    var limit = parseInt(request.query.limit, 10)
    var timestamp = request.query.timestamp
    var sessionState = runtime.db.get('session_state')

    try { timestamp = (timestamp || 0) ? bson.Timestamp.fromString(timestamp) : bson.Timestamp.ZERO } catch (ex) {
      return reply(boom.badRequest('invalid value for the timestamp parameter: ' + timestamp))
    }

    if (isNaN(limit) || (limit > 100)) limit = 100
    query = { userId: userId, timestamp: { $gte: timestamp } }
    modifiers = { sort: { timestamp: 1 } }

    entries = await sessionState.find(query, underscore.extend({ limit: limit }, modifiers))
    result = []
    entries.forEach(entry => {
      result.push(underscore.extend(underscore.omit(entry, '_id', 'timestamp'),
                                    { timestamp: entry.timestamp.toString() }))
    })

    reply(result)
  }
},

  description: 'Incrementally returns session information for a user',
  notes: 'The "timestamp" parameter corresponding to an opaque value, defaulting to "0". The "limit" parameter defaults to "100". The result is a JSON array containing zero or more entries.',
  tags: ['api'],

  validate:
    { params: { userId: Joi.string().guid().required().description('the identity of the user entry') },
      query:
      { timestamp: Joi.string().regex(/^[0-9]+$/).optional().description('an opaque, monotonically-increasing value'),
        limit: Joi.number().positive().optional().description('the maximum number of entries to return')
      }
    },

  response: {
    schema: Joi.array().items(Joi.object().keys({
      sessionId: Joi.string().guid().required().description('the identity of the session'),
      type: Joi.string().min(1).required().description('the name of the type'),
      userId: Joi.string().guid().required().description('the identity of the user entry'),
      envelope: Joi.any().required(),
      payload: Joi.object().required().description('an opaque JSON object'),
      timestamp: Joi.string().required().description('an opaque, monotonically-increasing value')
    }))
/*
    status: {
      400: Joi.object({
        boomlet: Joi.string().required().description('invalid value for the timestamp parameter')
      })
    }
 */
  }
}

/*
   GET /v1/users/{userId}/sessions/{sessionId}/{types}[? [since={milliseconds since epoch}&] [limit={positive integer}] ]
       defaults to since=0 limit=100

 */

v1.getTypes =
{ handler: function (runtime) {
  return async function (request, reply) {
    var entries, modifiers, query, result
    var userId = request.params.userId
    var sessionId = request.params.sessionId
    var limit = parseInt(request.query.limit, 10)
    var timestamp = request.query.timestamp
    var sessionState = runtime.db.get('session_state')

    try { timestamp = (timestamp || 0) ? bson.Timestamp.fromString(timestamp) : bson.Timestamp.ZERO } catch (ex) {
      return reply(boom.badRequest('invalid value for the timestamp parameter: ' + timestamp))
    }

    if (isNaN(limit) || (limit > 100)) limit = 100
    query = { userId: userId, sessionId: sessionId, timestamp: { $gte: timestamp } }
    modifiers = { sort: { timestamp: 1 } }

    entries = await sessionState.find(query, underscore.extend({ limit: limit }, modifiers))
    result = []
    entries.forEach(entry => {
      result.push(underscore.extend(underscore.omit(entry, '_id', 'timestamp'),
                                    { timestamp: entry.timestamp.toString() }))
    })

    reply(result)
  }
},

  description: 'Incrementally returns state information for a session',
  notes: 'The "timestamp" parameter corresponding to an opaque value, defaulting to "0". The "limit" parameter defaults to "100". The result is a JSON array containing zero or more entries.',
  tags: ['api'],

  validate:
    { params:
      { userId: Joi.string().guid().required().description('the identity of the user entry'),
        sessionId: Joi.string().guid().required().description('the identity of the session')
      },
      query:
      { timestamp: Joi.string().regex(/^[0-9]+$/).optional().description('an opaque, monotonically-increasing value'),
        limit: Joi.number().positive().optional().description('the maximum number of entries to return')
      }
    },

  response: {
    schema: Joi.array().items(Joi.object().keys({
      sessionId: Joi.string().guid().required().description('the identity of the session'),
      type: Joi.string().min(1).required().description('the name of the type'),
      userId: Joi.string().guid().required().description('the identity of the user entry'),
      envelope: Joi.any().required(),
      payload: Joi.object().required().description('an opaque JSON object'),
      timestamp: Joi.string().required().description('an opaque, monotonically-increasing value')
    }))
/*
    status: {
      400: Joi.object({
        boomlet: Joi.string().required().description('invalid value for the timestamp parameter')
      })
    }
 */
  }
}

/*
   GET /v1/users/{userId}/sessions/{sessionId}/types/{type}
 */

v1.getSessionType =
{ handler: function (runtime) {
  return async function (request, reply) {
    var result
    var userId = request.params.userId
    var sessionId = request.params.sessionId
    var type = request.params.type
    var sessionState = runtime.db.get('session_state')

    result = await sessionState.findOne({ userId: userId, sessionId: sessionId, type: type })
    if (!result) { return reply(boom.notFound('session/type entry does not exist: ' + sessionId + '/' + type)) }
    result = underscore.extend(underscore.omit(result, '_id', 'timestamp'), { timestamp: result.timestamp.toString() })

    reply(result)
  }
},

  description: 'Returns session information for a particular user',
  tags: ['api'],

  validate:
    { params:
      { userId: Joi.string().guid().required().description('the identity of the user entry'),
        sessionId: Joi.string().guid().required().description('the identity of the session'),
        type: Joi.string().min(1).required().description('the name of the type')
      }
    },

  response: {
    schema: Joi.object({
      sessionId: Joi.string().guid().required().description('the identity of the session'),
      type: Joi.string().min(1).required().description('the name of the type'),
      userId: Joi.string().guid().required().description('the identity of the user entry'),
      envelope: Joi.any().required(),
      payload: Joi.object().required().description('an opaque JSON object'),
      timestamp: Joi.string().required().description('an opaque, monotonically-increasing value')
    })
/*
    status: {
      404: Joi.object({
        boomlet: Joi.string().required().description('ad-manifest entry does not exist')
      })
    }
 */
  }
}

/*
   PUT /v1/users/{userId}/sessions/{sessionId}/types/{type}
        create/update (entry MAY already exist)
 */

v1.putSessionType =
{ handler: function (runtime) {
  return async function (request, reply) {
    var result, state, user
    var debug = braveHapi.debug(module, request)
    var userId = request.params.userId
    var sessionId = request.params.sessionId
    var type = request.params.type
    var users = runtime.db.get('users')
    var sessionState = runtime.db.get('session_state')

    user = await users.findOne({ userId: userId })
    if (!user) { return reply(boom.notFound('user entry does not exist: ' + userId)) }

    result = await helper.verify(debug, user, request.payload)
    if (result) return reply(result)

    state = { $currentDate: { timestamp: { $type: 'timestamp' } },
              $set: request.payload
            }
    await sessionState.update({ userId: userId, sessionId: sessionId, type }, state, { upsert: true })

    reply().code(204)
  }
},

  auth:
    { strategy: 'session',
      scope: [ 'devops' ],
      mode: 'required'
    },

  description: 'Sets session information for a particular user',
  tags: ['api'],

  validate:
    { params:
      { userId: Joi.string().guid().required().description('the identity of the user entry'),
        sessionId: Joi.string().guid().required().description('the identity of the session'),
        type: Joi.string().min(1).required().description('the name of the type')
      },
      payload: Joi.object().keys({
        envelope: Joi.any().required(),
        payload: Joi.object().required().description('an opaque JSON object')
      })
    },

  response: {
    schema: Joi.any()
/*
    status: {
      500: Joi.object({
        boomlet: Joi.string().required().description('database update failed')
      })
    }
 */
  }
}

module.exports.routes =
[ braveHapi.routes.async().path('/v1/users/{userId}/sessions').config(v1.getSessions),
  braveHapi.routes.async().path('/v1/users/{userId}/sessions/{sessionId}/types').config(v1.getTypes),
  braveHapi.routes.async().path('/v1/users/{userId}/sessions/{sessionId}/types/{type}').config(v1.getSessionType),
  braveHapi.routes.async().put().path('/v1/users/{userId}/sessions/{sessionId}/types/{type}').config(v1.putSessionType)
]

module.exports.initialize = async function (debug, runtime) {
  runtime.db.checkIndices(debug,
  [ { category: runtime.db.get('session_state'),
      name: 'session_state',
      property: 'sessionId',
      empty: { userId: '', sessionId: '', type: '', timestamp: bson.Timestamp.ZERO },
      others: [ { userId: 0 }, { sessionId: 0 }, { type: 1 }, { timestamp: 1 } ]
    }
  ])
}
<commit_msg>[docs] small edits<commit_after>var boom = require('boom')
var braveHapi = require('../brave-hapi')
var bson = require('bson')
var helper = require('./helper')
var Joi = require('joi')
var underscore = require('underscore')

var v1 = {}

/*
   GET /v1/users/{userId}/sessions[? [since={milliseconds since epoch}&] [limit={positive integer}] ]
       defaults to since=0 limit=100

 */

v1.getSessions =
{ handler: function (runtime) {
  return async function (request, reply) {
    var entries, modifiers, query, result
    var userId = request.params.userId
    var limit = parseInt(request.query.limit, 10)
    var timestamp = request.query.timestamp
    var sessionState = runtime.db.get('session_state')

    try { timestamp = (timestamp || 0) ? bson.Timestamp.fromString(timestamp) : bson.Timestamp.ZERO } catch (ex) {
      return reply(boom.badRequest('invalid value for the timestamp parameter: ' + timestamp))
    }

    if (isNaN(limit) || (limit > 100)) limit = 100
    query = { userId: userId, timestamp: { $gte: timestamp } }
    modifiers = { sort: { timestamp: 1 } }

    entries = await sessionState.find(query, underscore.extend({ limit: limit }, modifiers))
    result = []
    entries.forEach(entry => {
      result.push(underscore.extend(underscore.omit(entry, '_id', 'timestamp'),
                                    { timestamp: entry.timestamp.toString() }))
    })

    reply(result)
  }
},

  description: 'Incrementally returns session information for a user entry',
  notes: 'The "timestamp" parameter corresponding to an opaque value, defaulting to "0". The "limit" parameter defaults to "100". The result is a JSON array containing zero or more entries.',
  tags: ['api'],

  validate:
    { params: { userId: Joi.string().guid().required().description('the identity of the user entry') },
      query:
      { timestamp: Joi.string().regex(/^[0-9]+$/).optional().description('an opaque, monotonically-increasing value'),
        limit: Joi.number().positive().optional().description('the maximum number of entries to return')
      }
    },

  response: {
    schema: Joi.array().items(Joi.object().keys({
      sessionId: Joi.string().guid().required().description('the identity of the session'),
      type: Joi.string().min(1).required().description('the name of the type'),
      userId: Joi.string().guid().required().description('the identity of the user entry'),
      envelope: Joi.any().required(),
      payload: Joi.object().required().description('an opaque JSON object'),
      timestamp: Joi.string().required().description('an opaque, monotonically-increasing value')
    }))
/*
    status: {
      400: Joi.object({
        boomlet: Joi.string().required().description('invalid value for the timestamp parameter')
      })
    }
 */
  }
}

/*
   GET /v1/users/{userId}/sessions/{sessionId}/{types}[? [since={milliseconds since epoch}&] [limit={positive integer}] ]
       defaults to since=0 limit=100

 */

v1.getTypes =
{ handler: function (runtime) {
  return async function (request, reply) {
    var entries, modifiers, query, result
    var userId = request.params.userId
    var sessionId = request.params.sessionId
    var limit = parseInt(request.query.limit, 10)
    var timestamp = request.query.timestamp
    var sessionState = runtime.db.get('session_state')

    try { timestamp = (timestamp || 0) ? bson.Timestamp.fromString(timestamp) : bson.Timestamp.ZERO } catch (ex) {
      return reply(boom.badRequest('invalid value for the timestamp parameter: ' + timestamp))
    }

    if (isNaN(limit) || (limit > 100)) limit = 100
    query = { userId: userId, sessionId: sessionId, timestamp: { $gte: timestamp } }
    modifiers = { sort: { timestamp: 1 } }

    entries = await sessionState.find(query, underscore.extend({ limit: limit }, modifiers))
    result = []
    entries.forEach(entry => {
      result.push(underscore.extend(underscore.omit(entry, '_id', 'timestamp'),
                                    { timestamp: entry.timestamp.toString() }))
    })

    reply(result)
  }
},

  description: 'Incrementally returns state information for a session',
  notes: 'The "timestamp" parameter corresponding to an opaque value, defaulting to "0". The "limit" parameter defaults to "100". The result is a JSON array containing zero or more entries.',
  tags: ['api'],

  validate:
    { params:
      { userId: Joi.string().guid().required().description('the identity of the user entry'),
        sessionId: Joi.string().guid().required().description('the identity of the session')
      },
      query:
      { timestamp: Joi.string().regex(/^[0-9]+$/).optional().description('an opaque, monotonically-increasing value'),
        limit: Joi.number().positive().optional().description('the maximum number of entries to return')
      }
    },

  response: {
    schema: Joi.array().items(Joi.object().keys({
      sessionId: Joi.string().guid().required().description('the identity of the session'),
      type: Joi.string().min(1).required().description('the name of the type'),
      userId: Joi.string().guid().required().description('the identity of the user entry'),
      envelope: Joi.any().required(),
      payload: Joi.object().required().description('an opaque JSON object'),
      timestamp: Joi.string().required().description('an opaque, monotonically-increasing value')
    }))
/*
    status: {
      400: Joi.object({
        boomlet: Joi.string().required().description('invalid value for the timestamp parameter')
      })
    }
 */
  }
}

/*
   GET /v1/users/{userId}/sessions/{sessionId}/types/{type}
 */

v1.getSessionType =
{ handler: function (runtime) {
  return async function (request, reply) {
    var result
    var userId = request.params.userId
    var sessionId = request.params.sessionId
    var type = request.params.type
    var sessionState = runtime.db.get('session_state')

    result = await sessionState.findOne({ userId: userId, sessionId: sessionId, type: type })
    if (!result) { return reply(boom.notFound('session/type entry does not exist: ' + sessionId + '/' + type)) }
    result = underscore.extend(underscore.omit(result, '_id', 'timestamp'), { timestamp: result.timestamp.toString() })

    reply(result)
  }
},

  description: 'Returns session information for a particular user entry',
  tags: ['api'],

  validate:
    { params:
      { userId: Joi.string().guid().required().description('the identity of the user entry'),
        sessionId: Joi.string().guid().required().description('the identity of the session'),
        type: Joi.string().min(1).required().description('the name of the type')
      }
    },

  response: {
    schema: Joi.object({
      sessionId: Joi.string().guid().required().description('the identity of the session'),
      type: Joi.string().min(1).required().description('the name of the type'),
      userId: Joi.string().guid().required().description('the identity of the user entry'),
      envelope: Joi.any().required(),
      payload: Joi.object().required().description('an opaque JSON object'),
      timestamp: Joi.string().required().description('an opaque, monotonically-increasing value')
    })
/*
    status: {
      404: Joi.object({
        boomlet: Joi.string().required().description('ad-manifest entry does not exist')
      })
    }
 */
  }
}

/*
   PUT /v1/users/{userId}/sessions/{sessionId}/types/{type}
        create/update (entry MAY already exist)
 */

v1.putSessionType =
{ handler: function (runtime) {
  return async function (request, reply) {
    var result, state, user
    var debug = braveHapi.debug(module, request)
    var userId = request.params.userId
    var sessionId = request.params.sessionId
    var type = request.params.type
    var users = runtime.db.get('users')
    var sessionState = runtime.db.get('session_state')

    user = await users.findOne({ userId: userId })
    if (!user) { return reply(boom.notFound('user entry does not exist: ' + userId)) }

    result = await helper.verify(debug, user, request.payload)
    if (result) return reply(result)

    state = { $currentDate: { timestamp: { $type: 'timestamp' } },
              $set: request.payload
            }
    await sessionState.update({ userId: userId, sessionId: sessionId, type }, state, { upsert: true })

    reply().code(204)
  }
},

  auth:
    { strategy: 'session',
      scope: [ 'devops' ],
      mode: 'required'
    },

  description: 'Records session information for a particular user entry',
  tags: ['api'],

  validate:
    { params:
      { userId: Joi.string().guid().required().description('the identity of the user entry'),
        sessionId: Joi.string().guid().required().description('the identity of the session'),
        type: Joi.string().min(1).required().description('the name of the type')
      },
      payload: Joi.object().keys({
        envelope: Joi.any().required(),
        payload: Joi.object().required().description('an opaque JSON object')
      })
    },

  response: {
    schema: Joi.any()
/*
    status: {
      500: Joi.object({
        boomlet: Joi.string().required().description('database update failed')
      })
    }
 */
  }
}

module.exports.routes =
[ braveHapi.routes.async().path('/v1/users/{userId}/sessions').config(v1.getSessions),
  braveHapi.routes.async().path('/v1/users/{userId}/sessions/{sessionId}/types').config(v1.getTypes),
  braveHapi.routes.async().path('/v1/users/{userId}/sessions/{sessionId}/types/{type}').config(v1.getSessionType),
  braveHapi.routes.async().put().path('/v1/users/{userId}/sessions/{sessionId}/types/{type}').config(v1.putSessionType)
]

module.exports.initialize = async function (debug, runtime) {
  runtime.db.checkIndices(debug,
  [ { category: runtime.db.get('session_state'),
      name: 'session_state',
      property: 'sessionId',
      empty: { userId: '', sessionId: '', type: '', timestamp: bson.Timestamp.ZERO },
      others: [ { userId: 0 }, { sessionId: 0 }, { type: 1 }, { timestamp: 1 } ]
    }
  ])
}
<|endoftext|>


<commit_before>/*global define, console*/

/*
 | Copyright 2015 ESRI (UK) Limited
 |
 | Licensed under the Apache License, Version 2.0 (the "License");
 | you may not use this file except in compliance with the License.
 | You may obtain a copy of the License at
 |
 |    http://www.apache.org/licenses/LICENSE-2.0
 |
 | Unless required by applicable law or agreed to in writing, software
 | distributed under the License is distributed on an "AS IS" BASIS,
 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | See the License for the specific language governing permissions and
 | limitations under the License.
 */

define([
    'dojo',
    'dojo/text!./templates/NearestItem.html',
    'dojo/_base/declare',
    "dojo/_base/lang",
    'dijit/_Widget',
    'dijit/_TemplatedMixin',
    'dijit/_WidgetsInTemplateMixin',
    'dojo/dom-construct',
    'dojo/topic',
    'dojo/on',
    './_NearestBase',
    'dojo/i18n!./nls/Nearest'
],
function (
    dojo, template, declare, lang,
    _Widget, _TemplatedMixin, _WidgetsInTemplateMixin, domConstruct, topic, on, _NearestBase, i18n) {

    return declare([_Widget, _NearestBase, _TemplatedMixin, _WidgetsInTemplateMixin], {
        // description:
        //    Find the nearest features around a point

        templateString: template,
        baseClass: 'nearestItem',
        widgetsInTemplate: true,

        // Properties to be sent into constructor

        constructor: function (options, srcRefNode) {
            this.options = {
                feature: null,
                layerItemId: "",
                distance: 999, // The distance the feature is from the location.
                featureNumber: 0, // The index or number this feature is in the list
                description: "",
                titleText: "",
                titleField: "",
                renderer: null,
                layerOptions: null,
                opacity: 1
            };

            // mix in settings and defaults
            var defaults = lang.mixin({}, this.options, options);

            // Set properties
            this.set("feature", defaults.feature);
            this.set("layerItemId", defaults.layerItemId);
            this.set("units", i18n.distanceUnits);
            this.set("distance", defaults.distance);
            this.set("featureNumber", defaults.featureNumber);
            this.set("showOnMapLinktext", i18n.showOnMapLinktext);
            this.set("description", defaults.description);
            this.set("fieldValues", defaults.fieldValues);
            this.set("titleText", defaults.titleText);
            this.set("titleField", defaults.titleField);
            this.set("renderer", defaults.renderer);
            this.set("layerOptions", defaults.layerOptions);
            this.set("summaryDetailsTitle", i18n.summaryDetailsTitle);
            this.set("showOnMapTitleText", i18n.showOnMapTitleText);
            this.set("opacity", defaults.opacity);

            // widget node
            this.domNode = srcRefNode;
        },

        postMixInProperties: function () {
            var featureNameEle, attributes = this.feature.feature.attributes;

            if (!this._isNullOrEmpty(attributes[this.titleField[0]])) {
                featureNameEle = attributes[this.titleField[0]];
                featureNameEle = featureNameEle.toString().replace(/ /g, '-');
            }
            else {
                // Crap data, null value so lets just make one up.
                featureNameEle = this.layerItemId + "-" + this.featureNumber + "title";
            }
            // Remove any special characters that may cause element name errors
            featureNameEle = featureNameEle.replace(/[^\w\s-]/gi, '');
            featureNameEle = featureNameEle + "-" + this.featureNumber + "-" + this.layerItemId;

            this.set("featureId", featureNameEle);
            this.set("featureTitle", this._fieldReplace(this.titleText, attributes));

            // Show/Hide map link
            if (this.layerOptions.showOnMap) {
                this.set("showOnMapVisible", "inline-block");
            }
            else {
                this.set("showOnMapVisible", "none");
            }

            if (this.layerOptions.showCounters) {
                this.set("showCountersVisible", "block");
            }
            else {
                this.set("showCountersVisible", "none");
            }

            if (this.layerOptions.showDistance) {
                this.set("showDistanceVisible", "inline-block");
            }
            else {
                this.set("showDistanceVisible", "none");
            }

            this.inherited(arguments);
        },

        postCreate: function () {
            // summary:
            //    Overrides method of same name in dijit._Widget.
            // tags:
            //    private

            if (!this._isNullOrEmpty(this.description)) {
                domConstruct.place(this.description, this.featureDetails, "last");

                // Remove the header as we added that manually
                dojo.query(".header", this.featureDetails).forEach(dojo.empty);
            }

            this.setupConnections();
            this.inherited(arguments);
        },

        // start widget. called by user
        startup: function () {

        },

        // connections/subscriptions will be cleaned up during the destroy() lifecycle phase
        destroy: function () {
            // call the superclass method of the same name.
            this.inherited(arguments);
        },

        setupConnections: function () {
            // summary:
            //    wire events, and such
            //
            var _this = this, item;

            // Fire show on map click event
            on(this.mapButton, "click", function (evt) {
                topic.publish("Nearest::show-feature", _this.feature, _this.renderer, _this.opacity);
            });

            // Fire show item details click event
            item = dojo.byId(this.featureId + "-feature-name");

            if(item) {
                on(item, "click", function (evt) {
                    dojo.stopEvent(evt);
                    topic.publish("Nearest::show-feature-detail", _this.feature, _this.featureId + "-field-values");
                });
            }
        }

        /* ---------------- */
        /* Public Functions */
        /* ---------------- */

        /* ---------------- */
        /* Private Functions */
        /* ---------------- */

    });
});<commit_msg>Fix for distane units<commit_after>/*global define, console*/

/*
 | Copyright 2015 ESRI (UK) Limited
 |
 | Licensed under the Apache License, Version 2.0 (the "License");
 | you may not use this file except in compliance with the License.
 | You may obtain a copy of the License at
 |
 |    http://www.apache.org/licenses/LICENSE-2.0
 |
 | Unless required by applicable law or agreed to in writing, software
 | distributed under the License is distributed on an "AS IS" BASIS,
 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | See the License for the specific language governing permissions and
 | limitations under the License.
 */

define([
    'dojo',
    'dojo/text!./templates/NearestItem.html',
    'dojo/_base/declare',
    "dojo/_base/lang",
    'dijit/_Widget',
    'dijit/_TemplatedMixin',
    'dijit/_WidgetsInTemplateMixin',
    'dojo/dom-construct',
    'dojo/topic',
    'dojo/on',
    './_NearestBase',
    'dojo/i18n!./nls/Nearest'
],
function (
    dojo, template, declare, lang,
    _Widget, _TemplatedMixin, _WidgetsInTemplateMixin, domConstruct, topic, on, _NearestBase, i18n) {

    return declare([_Widget, _NearestBase, _TemplatedMixin, _WidgetsInTemplateMixin], {
        // description:
        //    Find the nearest features around a point

        templateString: template,
        baseClass: 'nearestItem',
        widgetsInTemplate: true,

        // Properties to be sent into constructor

        constructor: function (options, srcRefNode) {
            this.options = {
                feature: null,
                layerItemId: "",
                distance: 999, // The distance the feature is from the location.
                featureNumber: 0, // The index or number this feature is in the list
                description: "",
                titleText: "",
                titleField: "",
                renderer: null,
                layerOptions: null,
                opacity: 1
            };

            // mix in settings and defaults
            var defaults = lang.mixin({}, this.options, options);

            // Set properties
            this.set("feature", defaults.feature);
            this.set("layerItemId", defaults.layerItemId);
            this.set("units", this._getDistanceUnits());
            this.set("distance", defaults.distance);
            this.set("featureNumber", defaults.featureNumber);
            this.set("showOnMapLinktext", i18n.showOnMapLinktext);
            this.set("description", defaults.description);
            this.set("fieldValues", defaults.fieldValues);
            this.set("titleText", defaults.titleText);
            this.set("titleField", defaults.titleField);
            this.set("renderer", defaults.renderer);
            this.set("layerOptions", defaults.layerOptions);
            this.set("summaryDetailsTitle", i18n.summaryDetailsTitle);
            this.set("showOnMapTitleText", i18n.showOnMapTitleText);
            this.set("opacity", defaults.opacity);

            // widget node
            this.domNode = srcRefNode;
        },

        postMixInProperties: function () {
            var featureNameEle, attributes = this.feature.feature.attributes;

            if (!this._isNullOrEmpty(attributes[this.titleField[0]])) {
                featureNameEle = attributes[this.titleField[0]];
                featureNameEle = featureNameEle.toString().replace(/ /g, '-');
            }
            else {
                // Crap data, null value so lets just make one up.
                featureNameEle = this.layerItemId + "-" + this.featureNumber + "title";
            }
            // Remove any special characters that may cause element name errors
            featureNameEle = featureNameEle.replace(/[^\w\s-]/gi, '');
            featureNameEle = featureNameEle + "-" + this.featureNumber + "-" + this.layerItemId;

            this.set("featureId", featureNameEle);
            this.set("featureTitle", this._fieldReplace(this.titleText, attributes));

            // Show/Hide map link
            if (this.layerOptions.showOnMap) {
                this.set("showOnMapVisible", "inline-block");
            }
            else {
                this.set("showOnMapVisible", "none");
            }

            if (this.layerOptions.showCounters) {
                this.set("showCountersVisible", "block");
            }
            else {
                this.set("showCountersVisible", "none");
            }

            if (this.layerOptions.showDistance) {
                this.set("showDistanceVisible", "inline-block");
            }
            else {
                this.set("showDistanceVisible", "none");
            }

            this.inherited(arguments);
        },

        postCreate: function () {
            // summary:
            //    Overrides method of same name in dijit._Widget.
            // tags:
            //    private

            if (!this._isNullOrEmpty(this.description)) {
                domConstruct.place(this.description, this.featureDetails, "last");

                // Remove the header as we added that manually
                dojo.query(".header", this.featureDetails).forEach(dojo.empty);
            }

            this.setupConnections();
            this.inherited(arguments);
        },

        // start widget. called by user
        startup: function () {

        },

        // connections/subscriptions will be cleaned up during the destroy() lifecycle phase
        destroy: function () {
            // call the superclass method of the same name.
            this.inherited(arguments);
        },

        setupConnections: function () {
            // summary:
            //    wire events, and such
            //
            var _this = this, item;

            // Fire show on map click event
            on(this.mapButton, "click", function (evt) {
                topic.publish("Nearest::show-feature", _this.feature, _this.renderer, _this.opacity);
            });

            // Fire show item details click event
            item = dojo.byId(this.featureId + "-feature-name");

            if(item) {
                on(item, "click", function (evt) {
                    dojo.stopEvent(evt);
                    topic.publish("Nearest::show-feature-detail", _this.feature, _this.featureId + "-field-values");
                });
            }
        },

        /* ---------------- */
        /* Public Functions */
        /* ---------------- */

        /* ---------------- */
        /* Private Functions */
        /* ---------------- */
        _getDistanceUnits: function() {
            switch (this.distanceUnits) {
                case "m":
                    return i18n.distanceUnitsMiles;

                case "km":
                    return i18n.distanceUnitsKm;

                default:
                    return i18n.distanceUnitsMiles;
            }
        }

    });
});<|endoftext|>


<commit_before>import { createSelector } from 'reselect'
import _ from 'lodash'
import distance from '@turf/distance'
import { bindActionCreators } from 'redux'
import { point } from '@turf/helpers'

import { csrftoken } from '../Django'
import Queue from 'promise-queue'

export const REQUEST_MODELS = 'REQUEST_MODELS'
export const RECEIVE_MODELS = 'RECEIVE_MODELS'
export const UPDATE_MODEL = 'UPDATE_MODEL'
export const SAVING_MODEL = 'SAVING_MODEL'
export const SAVED_MODEL = 'SAVED_MODEL'

const fetchQueue = new Queue(2)

function queuedFetch() {
    const args = arguments
    return fetchQueue.add(() => {
        return fetch.apply(null, args)
            .then(response => {
                if (!response.ok) {
                    return Promise.reject(response)
                } else {
                    return response
                }
            })
    })
}

const getAllModels = state => state.getIn(['model', 'models']).toJS()

const modelGetter = _.memoize((name) => {
    return createSelector(
        [getAllModels],
        models => {
            return _.get(models, name, [])
        }
    )
})

export class ModelSelector {

    constructor(slice) {
        this.slice = slice
        this[Symbol.iterator] = function* () {
            yield* this.slice
        }
    }

    all() {
        return this.filter(_.constant(true))
    }

    filter(filter = _.constant(true)) {
        return new ModelSelector(_.filter(this.slice, filter))
    }

    filterBy(key, value) {
        return this.filter(_.matchesProperty(key, value))
    }

    first() {
        return _.head(this.slice)
    }

    exists(id) {
        return this.get(id) != undefined
    }

    get(id) {
        return _.find(this.slice, {id: id})
    }

    sortBy(key) {
        if (key.startsWith('-')) {
            return new ModelSelector(_.reverse(_.sortBy(this.slice, _.property(key.substr(1)))))
        } else {
            return new ModelSelector(_.sortBy(this.slice, _.property(key)))
        }
    }

    map(f) {
        return new ModelSelector(_.map(this.slice, f))
    }

    hasGeo() {
        return this.withGeo().filter(m => m.geo)
    }

    withGeo() {
        return this.map(m => (
            {...m, geo: (m.geo && m.geo.lat && m.geo.lng) ? point([m.geo.lat, m.geo.lng]) : undefined}
        ))
    }

    nearby(currentLocation, radius = 0) {
        const modelsWithDistance = _.map(this.hasGeo().slice, m => ({
            ...m,
            distance: distance(m.geo, currentLocation ? currentLocation : m.geo)
        }))
        const sorted = _.sortBy(modelsWithDistance, [m => m.distance])
        if (radius > 0) {
            return new ModelSelector(_.filter(sorted, m => m.distance <= radius))
        } else {
            return new ModelSelector(sorted)
        }
    }
}

export default class Model {
    constructor(name, options = {}) {
        this.name = name
        this.options = options
    }

    select(state) {
        return new ModelSelector(modelGetter(this.name)(state))
    }

    bindActionCreators(dispatch) {
        const funcNames = ['saving', 'saved', 'save', 'fetchOne', 'fetchIfNeeded', 'refresh', 'fetchAll', 'update', 'create', 'updateAndSave', 'request', 'receive']
        const funcPairs = _.map(funcNames, name => [name, _.bind(_.get(this, name), this)])
        const bindable = _.fromPairs(funcPairs)
        return bindActionCreators(bindable, dispatch)
    }

    saving(id) {
        return {
            type: SAVING_MODEL,
            id: Number(id)
        }
    }

    saved(id) {
        return {
            type: SAVED_MODEL,
            id: Number(id)
        }
    }

    save(id) {
        return (dispatch, getState) => {
            const selector = new Model(this.name).select(getState())
            const model = selector.filterBy('id', id).first()
            dispatch(this.saving(id))
            const data = {
                method: 'PUT',
                credentials: 'include',
                headers: {
                    'X-CSRFToken': csrftoken,
                    'Content-Type': 'application/json; charset=utf-8'
                },
                body: JSON.stringify(model)
            }

            console.groupCollapsed('PUT %s %s', this.name, id)
            console.log(data)
            console.groupEnd()
            return queuedFetch('/api/'+this.name+'/'+id+'/', data).then(() => {
                dispatch(this.saved(id))
                return Promise.resolve()
            })
        }
    }

    fetchIfNeeded(id) {
        return (dispatch, getState) => {
            if (!this.select(getState()).exists(id)) {
                return dispatch(this.fetchOne(id))
            }
        }
    }

    fetchOne(id) {
        return dispatch => {
            dispatch(this.request())
            console.log('GET %s %s', this.name, id)
            return queuedFetch('/api/'+this.name+'/'+id+'/', {credentials: 'include'})
                .then(response => response.json())
                .then(json => {
                    if (!_.isEmpty(json))
                        return dispatch(this.receive([json]))
                })
        }
    }

    refresh() {
        return (dispatch, getState) => {
            if (this.select(getState()).all().slice.length == 0) {
                return dispatch(this.fetchAll())
            }
        }
    }

    fetchAll(params = {}) {
        return dispatch => {
            dispatch(this.request())
            const url = _.get(this.options, 'url', '/api/'+this.name+'/')
            const urlParams = new URLSearchParams(Object.entries(_.pickBy(params, _.negate(_.isUndefined))))
            console.groupCollapsed('GET %s page=%s', this.name, _.get(params, 'page', 1))
            console.log(params)
            console.groupEnd()
            return queuedFetch(url+'?'+urlParams, {credentials: 'include'})
                .then(response => response.json())
                .then(json => {
                    if (json.next) {
                        const nextPage = (params.page || 1) + 1
                        const ret = dispatch(this.fetchAll({...params, page: nextPage}, this.options))
                        dispatch(this.receive(json.results))
                        return ret
                    } else {
                        return dispatch(this.receive(json.results))
                    }
                })
        }
    }

    update(id, dataOrCallback) {
        return (dispatch, getState) => {
            if (typeof dataOrCallback == 'function') {
                const foundItem = this.select(getState()).filterBy('id', id).first()
                return dispatch(this.update(id, dataOrCallback(foundItem)))
            } else {
                console.groupCollapsed('update %s %s', this.name, id)
                console.log(dataOrCallback)
                console.groupEnd()
                dispatch({
                    type: UPDATE_MODEL,
                    id: id,
                    data: dataOrCallback,
                    name: this.name
                })
                return Promise.resolve()
            }
        }
    }

    create(modelData) {
        return dispatch => {
            const data = {
                method: 'POST',
                credentials: 'include',
                headers: {
                    'X-CSRFToken': csrftoken,
                    'Content-Type': 'application/json; charset=utf-8'
                },
                body: JSON.stringify(modelData)
            }
            console.groupCollapsed('POST %s', this.name)
            console.log(data)
            console.groupEnd()
            return queuedFetch('/api/'+this.name+'/', data)
                .then(response => response.json())
                .then(json => {
                    if (!_.isEmpty(json)) {
                        dispatch(this.update(json.id, json))
                        return Promise.resolve(json.id)
                    }
                })
        }
    }

    updateAndSave(id, dataOrCallback) {
        return dispatch => {
            return dispatch(this.update(id, dataOrCallback))
                .then(() => dispatch(this.save(id)))
        }
    }

    request() {
        return {
            type: REQUEST_MODELS,
            name: this.name
        }
    }

    receive(results) {
        return {
            type: RECEIVE_MODELS,
            name: this.name,
            models: results || []
        }
    }
}
<commit_msg>store: model: push immutablejs up one layer<commit_after>import { createSelector } from 'reselect'
import _ from 'lodash'
import distance from '@turf/distance'
import { bindActionCreators } from 'redux'
import { point } from '@turf/helpers'

import { csrftoken } from '../Django'
import Queue from 'promise-queue'

export const REQUEST_MODELS = 'REQUEST_MODELS'
export const RECEIVE_MODELS = 'RECEIVE_MODELS'
export const UPDATE_MODEL = 'UPDATE_MODEL'
export const SAVING_MODEL = 'SAVING_MODEL'
export const SAVED_MODEL = 'SAVED_MODEL'

const fetchQueue = new Queue(2)

function queuedFetch() {
    const args = arguments
    return fetchQueue.add(() => {
        return fetch.apply(null, args)
            .then(response => {
                if (!response.ok) {
                    return Promise.reject(response)
                } else {
                    return response
                }
            })
    })
}

const getAllModels = state => state.getIn(['model', 'models'])

const modelGetter = _.memoize((name) => {
    return createSelector(
        [getAllModels],
        models => {
            return models.get(name, {})
        }
    )
})

export class ModelSelector {

    constructor(slice) {
        this.slice = slice.toJS()
        this[Symbol.iterator] = function* () {
            yield* this.slice
        }
    }

    all() {
        return this.filter(_.constant(true))
    }

    filter(filter = _.constant(true)) {
        return new ModelSelector(_.filter(this.slice, filter))
    }

    filterBy(key, value) {
        return this.filter(_.matchesProperty(key, value))
    }

    first() {
        return _.head(this.slice)
    }

    exists(id) {
        return this.get(id) != undefined
    }

    get(id) {
        return _.find(this.slice, {id: id})
    }

    sortBy(key) {
        if (key.startsWith('-')) {
            return new ModelSelector(_.reverse(_.sortBy(this.slice, _.property(key.substr(1)))))
        } else {
            return new ModelSelector(_.sortBy(this.slice, _.property(key)))
        }
    }

    map(f) {
        return new ModelSelector(_.map(this.slice, f))
    }

    hasGeo() {
        return this.withGeo().filter(m => m.geo)
    }

    withGeo() {
        return this.map(m => (
            {...m, geo: (m.geo && m.geo.lat && m.geo.lng) ? point([m.geo.lat, m.geo.lng]) : undefined}
        ))
    }

    nearby(currentLocation, radius = 0) {
        const modelsWithDistance = _.map(this.hasGeo().slice, m => ({
            ...m,
            distance: distance(m.geo, currentLocation ? currentLocation : m.geo)
        }))
        const sorted = _.sortBy(modelsWithDistance, [m => m.distance])
        if (radius > 0) {
            return new ModelSelector(_.filter(sorted, m => m.distance <= radius))
        } else {
            return new ModelSelector(sorted)
        }
    }
}

export default class Model {
    constructor(name, options = {}) {
        this.name = name
        this.options = options
    }

    select(state) {
        return new ModelSelector(modelGetter(this.name)(state))
    }

    bindActionCreators(dispatch) {
        const funcNames = ['saving', 'saved', 'save', 'fetchOne', 'fetchIfNeeded', 'refresh', 'fetchAll', 'update', 'create', 'updateAndSave', 'request', 'receive']
        const funcPairs = _.map(funcNames, name => [name, _.bind(_.get(this, name), this)])
        const bindable = _.fromPairs(funcPairs)
        return bindActionCreators(bindable, dispatch)
    }

    saving(id) {
        return {
            type: SAVING_MODEL,
            id: Number(id)
        }
    }

    saved(id) {
        return {
            type: SAVED_MODEL,
            id: Number(id)
        }
    }

    save(id) {
        return (dispatch, getState) => {
            const selector = new Model(this.name).select(getState())
            const model = selector.filterBy('id', id).first()
            dispatch(this.saving(id))
            const data = {
                method: 'PUT',
                credentials: 'include',
                headers: {
                    'X-CSRFToken': csrftoken,
                    'Content-Type': 'application/json; charset=utf-8'
                },
                body: JSON.stringify(model)
            }

            console.groupCollapsed('PUT %s %s', this.name, id)
            console.log(data)
            console.groupEnd()
            return queuedFetch('/api/'+this.name+'/'+id+'/', data).then(() => {
                dispatch(this.saved(id))
                return Promise.resolve()
            })
        }
    }

    fetchIfNeeded(id) {
        return (dispatch, getState) => {
            if (!this.select(getState()).exists(id)) {
                return dispatch(this.fetchOne(id))
            }
        }
    }

    fetchOne(id) {
        return dispatch => {
            dispatch(this.request())
            console.log('GET %s %s', this.name, id)
            return queuedFetch('/api/'+this.name+'/'+id+'/', {credentials: 'include'})
                .then(response => response.json())
                .then(json => {
                    if (!_.isEmpty(json))
                        return dispatch(this.receive([json]))
                })
        }
    }

    refresh() {
        return (dispatch, getState) => {
            if (this.select(getState()).all().slice.length == 0) {
                return dispatch(this.fetchAll())
            }
        }
    }

    fetchAll(params = {}) {
        return dispatch => {
            dispatch(this.request())
            const url = _.get(this.options, 'url', '/api/'+this.name+'/')
            const urlParams = new URLSearchParams(Object.entries(_.pickBy(params, _.negate(_.isUndefined))))
            console.groupCollapsed('GET %s page=%s', this.name, _.get(params, 'page', 1))
            console.log(params)
            console.groupEnd()
            return queuedFetch(url+'?'+urlParams, {credentials: 'include'})
                .then(response => response.json())
                .then(json => {
                    if (json.next) {
                        const nextPage = (params.page || 1) + 1
                        const ret = dispatch(this.fetchAll({...params, page: nextPage}, this.options))
                        dispatch(this.receive(json.results))
                        return ret
                    } else {
                        return dispatch(this.receive(json.results))
                    }
                })
        }
    }

    update(id, dataOrCallback) {
        return (dispatch, getState) => {
            if (typeof dataOrCallback == 'function') {
                const foundItem = this.select(getState()).filterBy('id', id).first()
                return dispatch(this.update(id, dataOrCallback(foundItem)))
            } else {
                console.groupCollapsed('update %s %s', this.name, id)
                console.log(dataOrCallback)
                console.groupEnd()
                dispatch({
                    type: UPDATE_MODEL,
                    id: id,
                    data: dataOrCallback,
                    name: this.name
                })
                return Promise.resolve()
            }
        }
    }

    create(modelData) {
        return dispatch => {
            const data = {
                method: 'POST',
                credentials: 'include',
                headers: {
                    'X-CSRFToken': csrftoken,
                    'Content-Type': 'application/json; charset=utf-8'
                },
                body: JSON.stringify(modelData)
            }
            console.groupCollapsed('POST %s', this.name)
            console.log(data)
            console.groupEnd()
            return queuedFetch('/api/'+this.name+'/', data)
                .then(response => response.json())
                .then(json => {
                    if (!_.isEmpty(json)) {
                        dispatch(this.update(json.id, json))
                        return Promise.resolve(json.id)
                    }
                })
        }
    }

    updateAndSave(id, dataOrCallback) {
        return dispatch => {
            return dispatch(this.update(id, dataOrCallback))
                .then(() => dispatch(this.save(id)))
        }
    }

    request() {
        return {
            type: REQUEST_MODELS,
            name: this.name
        }
    }

    receive(results) {
        return {
            type: RECEIVE_MODELS,
            name: this.name,
            models: results || []
        }
    }
}
<|endoftext|>


<commit_before>var randomChooser;
if(!randomChooser) {
  randomChooser = {};
}
randomChooser.createModel = function() {'use strict';
  var lists, selectedListName, db = randomChooser.localStorage, checkListIsSelected;
  checkListIsSelected = function() {
    var currentlySelectedList = lists[selectedListName];
    if(currentlySelectedList === undefined) {
      throw 'Invalid list is selected: \'' + selectedListName + '\'';
    }
    return currentlySelectedList;
  };
  lists = db.getItem('random-chooser-lists');
  if(lists === null || lists === undefined) {
    lists = {};
    db.setItem('random-chooser-lists', JSON.stringify(lists));
  } else {
    lists = JSON.parse(lists);
  }
  return {
    addList : function(listName) {
      lists[listName] = [];
      db.setItem('random-chooser-lists', JSON.stringify(lists));
    },
    deleteList : function(listName) {
      delete lists[listName];
      db.setItem('random-chooser-lists', JSON.stringify(lists));
    },
    setSelectedListName : function(listName) {
      selectedListName = listName;
    },
    getSelectedListName : function() {
      return selectedListName;
    },
    isItemNameUniqueInSelectedList : function(itemName) {
      var i, currentlySelectedList = checkListIsSelected();
      for( i = 0; i < currentlySelectedList.length; i += 1 ) {
        if(currentlySelectedList[i] === itemName) {
          return false;
        }
      }
      return true;
    },
    getListNames : function() {
      var listNames = [], i;
      for(i in lists) {
        if(lists.hasOwnProperty(i)) {
          listNames.push(i);
        }
      }
      return listNames;
    },
    addItem : function(itemName) {
      var currentlySelectedList = checkListIsSelected();
      currentlySelectedList[currentlySelectedList.length] = itemName;
      db.setItem('random-chooser-lists', JSON.stringify(lists));
    },
    deleteItem : function(itemName) {
      var i, list = checkListIsSelected(), item;
      for( i = 0; i < list.length; i += 1) {
        item = list[i];
        if(item === itemName) {
          list.splice(i, 1);
        }
      }
      db.setItem('random-chooser-lists', JSON.stringify(lists));
    },
    getSelectedList : function() {
      return randomChooser.model.getList(selectedListName);
    },
    getList : function(listName) {
      return lists[listName];
    }
  };
};
randomChooser.model = randomChooser.createModel();
randomChooser.view = {
  redrawLists : function(listNames) {
    var i = 0, lists = $('#lists');
    lists.empty();
    lists.listview('refresh');
    listNames.sort();
    for(; i < listNames.length; i++) {
      randomChooser.view.addList(listNames[i]);
    }
    lists.listview('refresh');
  },
  addList : function(listName) {
    var listViewAnchor, deleteListAnchor;
    listViewAnchor = $('<a/>', {
      'href' : '#viewListPage',
      'data-transition' : 'slide',
      'text' : listName
    });
    deleteListAnchor = $('<a/>', {
      'href' : '#deleteListPage',
      'data-transition' : 'slide',
      'data-role' : 'button',
      'data-rel' : 'dialog',
      'data-transition' : 'pop',
      'text' : 'Delete List'
    });
    deleteListAnchor[0].onclick = function() {
      randomChooser.view.askToDeleteList(listName);
    };
    listViewAnchor[0].onclick = function() {
      randomChooser.controller.setSelectedList(listName);
    };
    $('#lists').append($('<li/>', {}).append(listViewAnchor).append(deleteListAnchor));
  },
  addItem : function(itemName) {
    var itemViewAnchor, deleteItemAnchor;
    itemViewAnchor = $('<a/>', {
      'text' : itemName
    });
    deleteItemAnchor = $('<a/>', {
      'href' : '#deleteItemPage',
      'data-transition' : 'slide',
      'data-role' : 'button',
      'data-rel' : 'dialog',
      'data-transition' : 'pop',
      'text' : 'Delete Item'
    });
    deleteItemAnchor[0].onclick = function() {
      randomChooser.view.askToDeleteItem(itemName);
    };
    $('#listItems').append($('<li/>', {}).append(itemViewAnchor).append(deleteItemAnchor));
  },
  drawList : function(listName, list) {
    $('#listNameLabel').text("list: " + listName);
    var i = 0;
    $('#listItems').empty();
    $('#listItems').listview('refresh');
    list.sort();
    for(; i < list.length; i++) {
      randomChooser.view.addItem(list[i]);
    }
    $('#listItems').listview('refresh');
  },
  setRandomDisabled : function(disabled) {
    if(disabled) {
      $('#random').addClass('ui-disabled');
    } else {
      $('#random').removeClass('ui-disabled');
    }
  },
  displayItem : function(itemName) {
    $('#itemNameLabel').text(itemName);
  },
  askToDeleteItem : function(name) {
    var deleteItem = $('#deleteItem');
    $('#deleteItemLabel').text(name);
    deleteItem.click(function () {
      randomChooser.controller.deleteItem(name);
      $('.ui-dialog').dialog ('close');
    });
    $('#deleteItemPage').bind('keyup', function(event) {
      if(event.keyCode === 13) {
        deleteItem.click();
      }
      return false;
    });
  },
  askToDeleteList : function(name) {
    var deleteList = $('#deleteList');
    $('#deleteListLabel').text(name);
    deleteList.click(function () {
      randomChooser.controller.deleteList(name);
      $('.ui-dialog').dialog ('close');
    });
    $('#deleteListPage').bind('keyup', function(event) {
      if(event.keyCode === 13) {
        deleteList.click();
      }
      return false;
    });
  }
};
randomChooser.controller = {
  addList : function(listName) {
    if(randomChooser.model.getList(listName) === undefined) {
      randomChooser.model.addList(listName);
      randomChooser.controller.redrawFirstPage();
      return true;
    }
    return false;
  },
  redrawFirstPage : function() {
    randomChooser.view.redrawLists(randomChooser.model.getListNames());
  },
  setSelectedList : function(selectedListName) {
    randomChooser.model.setSelectedListName(selectedListName);
  },
  drawList : function(listName) {
    randomChooser.view.drawList(listName, randomChooser.model.getList(listName));
  },
  drawSelectedList : function() {
    randomChooser.controller.drawList(randomChooser.model.getSelectedListName());
  },
  deleteList : function(listName) {
    randomChooser.model.deleteList(listName);
    randomChooser.view.redrawLists(randomChooser.model.getListNames());
  },
  deleteItem : function(itemName) {
    randomChooser.model.deleteItem(itemName);
    randomChooser.controller.enableDisableRandom();
    randomChooser.controller.drawSelectedList();
  },
  addItemToSelectedList : function(itemName) {
    randomChooser.model.addItem(itemName);
    randomChooser.controller.drawSelectedList();
  },
  enableDisableRandom : function() {
    randomChooser.view.setRandomDisabled(randomChooser.model.getSelectedListName() === undefined || randomChooser.model.getSelectedList().length <= 0);
  },
  selectRandomItem : function() {
    var randomlySelectedItem = randomChooser.model.getSelectedList()[Math.floor(Math.random() * randomChooser.model.getSelectedList().length)];
    randomChooser.view.displayItem(randomlySelectedItem);
  }
};
$('#firstPage').live('pageinit', function(event) {
  randomChooser.controller.redrawFirstPage();
});
$('#addListPage').live('pageinit', function(event) {
  var addListOk = $('#addListOk');
  addListOk.addClass('ui-disabled');
  $('#listName').keyup(function(event) {
    var listName = event.currentTarget.value.trim();
    if(listName.length === 0 || randomChooser.model.getList(listName) !== undefined) {
      addListOk.addClass('ui-disabled');
    } else {
      addListOk.removeClass('ui-disabled');
    }
  });
  addListOk.click(function() {
    randomChooser.controller.addList($('#listName').val().trim());
  });
  $("#addListPage").bind('keyup', function(event) {
    if(event.keyCode === 13 && addListOk[0].className.indexOf('ui-disabled') === -1) {
      addListOk.click();
    }
    return false;
  });
});
$('#addItemPage').live('pageinit', function(event) {
  var addItemOk = $('#addItemOk');
  addItemOk.addClass('ui-disabled');
  $('#itemName').keyup(function(event) {
    var itemName = event.currentTarget.value.trim();
    if(itemName.length === 0 || !randomChooser.model.isItemNameUniqueInSelectedList(itemName)) {
      addItemOk.addClass('ui-disabled');
    } else {
      addItemOk.removeClass('ui-disabled');
    }
  });
  addItemOk.click(function() {
    randomChooser.controller.addItemToSelectedList($('#itemName').val().trim());
  });
  $("#addItemPage").bind('keyup', function(event) {
    if(event.keyCode === 13 && addItemOk[0].className.indexOf('ui-disabled') === -1) {
      addItemOk.click();
    }
    return false;
  });
});
$('#viewListPage').live('pageinit', function(event) {
  $('#random').click(function() {
    randomChooser.controller.selectRandomItem();
  });
});
$('#viewItemPage').live('pageinit', function(event) {
  var selectAnother = $('#selectAnother');
  selectAnother.click(function() {
    randomChooser.controller.selectRandomItem();
  })
});
$('#deletePage').live('pageinit', function (event) {

});
$("#viewItemPage").bind('keyup', function(event) {
  if(event.keyCode === 13 && selectAnother[0].className.indexOf('ui-disabled') === -1) {
    selectAnother.click();
  }
  return false;
});
$('#viewListPage').live('pagebeforeshow', function(event) {
  randomChooser.controller.enableDisableRandom();
  randomChooser.controller.drawSelectedList();
});
$('#addListPage').live('pagehide', function(event, ui) {
  $('#listName').val('');
  $('#addListOk').addClass('ui-disabled');
});
$('#addListPage').live('pageshow', function(event, ui) {
  $('#listName').focus();
});
$('#addItemPage').live('pagehide', function(event, ui) {
  $('#itemName').val('');
  $('#addItemOk').addClass('ui-disabled');
});
$('#addItemPage').live('pageshow', function(event, ui) {
  $('#itemName').focus();
});
<commit_msg>bug with multiple click handlers<commit_after>var randomChooser;
if(!randomChooser) {
  randomChooser = {};
}
randomChooser.createModel = function() {'use strict';
  var lists, selectedListName, db = randomChooser.localStorage, checkListIsSelected;
  checkListIsSelected = function() {
    var currentlySelectedList = lists[selectedListName];
    if(currentlySelectedList === undefined) {
      throw 'Invalid list is selected: \'' + selectedListName + '\'';
    }
    return currentlySelectedList;
  };
  lists = db.getItem('random-chooser-lists');
  if(lists === null || lists === undefined) {
    lists = {};
    db.setItem('random-chooser-lists', JSON.stringify(lists));
  } else {
    lists = JSON.parse(lists);
  }
  return {
    addList : function(listName) {
      lists[listName] = [];
      db.setItem('random-chooser-lists', JSON.stringify(lists));
    },
    deleteList : function(listName) {
      delete lists[listName];
      db.setItem('random-chooser-lists', JSON.stringify(lists));
    },
    setSelectedListName : function(listName) {
      selectedListName = listName;
    },
    getSelectedListName : function() {
      return selectedListName;
    },
    isItemNameUniqueInSelectedList : function(itemName) {
      var i, currentlySelectedList = checkListIsSelected();
      for( i = 0; i < currentlySelectedList.length; i += 1 ) {
        if(currentlySelectedList[i] === itemName) {
          return false;
        }
      }
      return true;
    },
    getListNames : function() {
      var listNames = [], i;
      for(i in lists) {
        if(lists.hasOwnProperty(i)) {
          listNames.push(i);
        }
      }
      return listNames;
    },
    addItem : function(itemName) {
      var currentlySelectedList = checkListIsSelected();
      currentlySelectedList[currentlySelectedList.length] = itemName;
      db.setItem('random-chooser-lists', JSON.stringify(lists));
    },
    deleteItem : function(itemName) {
      var i, list = checkListIsSelected(), item;
      for( i = 0; i < list.length; i += 1) {
        item = list[i];
        if(item === itemName) {
          list.splice(i, 1);
        }
      }
      db.setItem('random-chooser-lists', JSON.stringify(lists));
    },
    getSelectedList : function() {
      return randomChooser.model.getList(selectedListName);
    },
    getList : function(listName) {
      return lists[listName];
    }
  };
};
randomChooser.model = randomChooser.createModel();
randomChooser.view = {
  redrawLists : function(listNames) {
    var i = 0, lists = $('#lists');
    lists.empty();
    lists.listview('refresh');
    listNames.sort();
    for(; i < listNames.length; i++) {
      randomChooser.view.addList(listNames[i]);
    }
    lists.listview('refresh');
  },
  addList : function(listName) {
    var listViewAnchor, deleteListAnchor;
    listViewAnchor = $('<a/>', {
      'href' : '#viewListPage',
      'data-transition' : 'slide',
      'text' : listName
    });
    deleteListAnchor = $('<a/>', {
      'href' : '#deleteListPage',
      'data-transition' : 'slide',
      'data-role' : 'button',
      'data-rel' : 'dialog',
      'data-transition' : 'pop',
      'text' : 'Delete List'
    });
    deleteListAnchor[0].onclick = function() {
      randomChooser.view.askToDeleteList(listName);
    };
    listViewAnchor[0].onclick = function() {
      randomChooser.controller.setSelectedList(listName);
    };
    $('#lists').append($('<li/>', {}).append(listViewAnchor).append(deleteListAnchor));
  },
  addItem : function(itemName) {
    var itemViewAnchor, deleteItemAnchor;
    itemViewAnchor = $('<a/>', {
      'text' : itemName
    });
    deleteItemAnchor = $('<a/>', {
      'href' : '#deleteItemPage',
      'data-transition' : 'slide',
      'data-role' : 'button',
      'data-rel' : 'dialog',
      'data-transition' : 'pop',
      'text' : 'Delete Item'
    });
    deleteItemAnchor[0].onclick = function() {
      randomChooser.view.askToDeleteItem(itemName);
    };
    $('#listItems').append($('<li/>', {}).append(itemViewAnchor).append(deleteItemAnchor));
  },
  drawList : function(listName, list) {
    $('#listNameLabel').text("list: " + listName);
    var i = 0;
    $('#listItems').empty();
    $('#listItems').listview('refresh');
    list.sort();
    for(; i < list.length; i++) {
      randomChooser.view.addItem(list[i]);
    }
    $('#listItems').listview('refresh');
  },
  setRandomDisabled : function(disabled) {
    if(disabled) {
      $('#random').addClass('ui-disabled');
    } else {
      $('#random').removeClass('ui-disabled');
    }
  },
  displayItem : function(itemName) {
    $('#itemNameLabel').text(itemName);
  },
  askToDeleteItem : function(name) {
    var deleteItem = $('#deleteItem');
    $('#deleteItemLabel').text(name);
    deleteItem.unbind('click');
    deleteItem.click(function () {
      randomChooser.controller.deleteItem(name);
      $('.ui-dialog').dialog ('close');
    });
    $('#deleteItemPage').bind('keyup', function(event) {
      if(event.keyCode === 13) {
        deleteItem.click();
      }
      return false;
    });
  },
  askToDeleteList : function(name) {
    var deleteList = $('#deleteList');
    $('#deleteListLabel').text(name);
    deleteList.unbind('click');
    deleteList.click(function () {
      randomChooser.controller.deleteList(name);
      $('.ui-dialog').dialog ('close');
    });
    $('#deleteListPage').bind('keyup', function(event) {
      if(event.keyCode === 13) {
        deleteList.click();
      }
      return false;
    });
  }
};
randomChooser.controller = {
  addList : function(listName) {
    if(randomChooser.model.getList(listName) === undefined) {
      randomChooser.model.addList(listName);
      randomChooser.controller.redrawFirstPage();
      return true;
    }
    return false;
  },
  redrawFirstPage : function() {
    randomChooser.view.redrawLists(randomChooser.model.getListNames());
  },
  setSelectedList : function(selectedListName) {
    randomChooser.model.setSelectedListName(selectedListName);
  },
  drawList : function(listName) {
    randomChooser.view.drawList(listName, randomChooser.model.getList(listName));
  },
  drawSelectedList : function() {
    randomChooser.controller.drawList(randomChooser.model.getSelectedListName());
  },
  deleteList : function(listName) {
    randomChooser.model.deleteList(listName);
    randomChooser.view.redrawLists(randomChooser.model.getListNames());
  },
  deleteItem : function(itemName) {
    randomChooser.model.deleteItem(itemName);
    randomChooser.controller.enableDisableRandom();
    randomChooser.controller.drawSelectedList();
  },
  addItemToSelectedList : function(itemName) {
    randomChooser.model.addItem(itemName);
    randomChooser.controller.drawSelectedList();
  },
  enableDisableRandom : function() {
    randomChooser.view.setRandomDisabled(randomChooser.model.getSelectedListName() === undefined || randomChooser.model.getSelectedList().length <= 0);
  },
  selectRandomItem : function() {
    var randomlySelectedItem = randomChooser.model.getSelectedList()[Math.floor(Math.random() * randomChooser.model.getSelectedList().length)];
    randomChooser.view.displayItem(randomlySelectedItem);
  }
};
$('#firstPage').live('pageinit', function(event) {
  randomChooser.controller.redrawFirstPage();
});
$('#addListPage').live('pageinit', function(event) {
  var addListOk = $('#addListOk');
  addListOk.addClass('ui-disabled');
  $('#listName').keyup(function(event) {
    var listName = event.currentTarget.value.trim();
    if(listName.length === 0 || randomChooser.model.getList(listName) !== undefined) {
      addListOk.addClass('ui-disabled');
    } else {
      addListOk.removeClass('ui-disabled');
    }
  });
  addListOk.click(function() {
    randomChooser.controller.addList($('#listName').val().trim());
  });
  $("#addListPage").bind('keyup', function(event) {
    if(event.keyCode === 13 && addListOk[0].className.indexOf('ui-disabled') === -1) {
      addListOk.click();
    }
    return false;
  });
});
$('#addItemPage').live('pageinit', function(event) {
  var addItemOk = $('#addItemOk');
  addItemOk.addClass('ui-disabled');
  $('#itemName').keyup(function(event) {
    var itemName = event.currentTarget.value.trim();
    if(itemName.length === 0 || !randomChooser.model.isItemNameUniqueInSelectedList(itemName)) {
      addItemOk.addClass('ui-disabled');
    } else {
      addItemOk.removeClass('ui-disabled');
    }
  });
  addItemOk.click(function() {
    randomChooser.controller.addItemToSelectedList($('#itemName').val().trim());
  });
  $("#addItemPage").bind('keyup', function(event) {
    if(event.keyCode === 13 && addItemOk[0].className.indexOf('ui-disabled') === -1) {
      addItemOk.click();
    }
    return false;
  });
});
$('#viewListPage').live('pageinit', function(event) {
  $('#random').click(function() {
    randomChooser.controller.selectRandomItem();
  });
});
$('#viewItemPage').live('pageinit', function(event) {
  var selectAnother = $('#selectAnother');
  selectAnother.click(function() {
    randomChooser.controller.selectRandomItem();
  })
});
$('#deletePage').live('pageinit', function (event) {

});
$("#viewItemPage").bind('keyup', function(event) {
  if(event.keyCode === 13 && selectAnother[0].className.indexOf('ui-disabled') === -1) {
    selectAnother.click();
  }
  return false;
});
$('#viewListPage').live('pagebeforeshow', function(event) {
  randomChooser.controller.enableDisableRandom();
  randomChooser.controller.drawSelectedList();
});
$('#addListPage').live('pagehide', function(event, ui) {
  $('#listName').val('');
  $('#addListOk').addClass('ui-disabled');
});
$('#addListPage').live('pageshow', function(event, ui) {
  $('#listName').focus();
});
$('#addItemPage').live('pagehide', function(event, ui) {
  $('#itemName').val('');
  $('#addItemOk').addClass('ui-disabled');
});
$('#addItemPage').live('pageshow', function(event, ui) {
  $('#itemName').focus();
});
<|endoftext|>


<commit_before>'use strict';

var ensureOpts = require('util/ensure-opts');

var spy = require('test/helper/util/spy');

var select = require('test/helper/vdom').select,
    render = require('test/helper/vdom').render,
    simulateEvent = require('test/helper/vdom').simulateEvent;

function describeEditor(name, options) {

  ensureOpts([
    'createEditor',
    'initialXML',
    'otherXML',
    'isDiagramEditor'
  ], options);

  var createEditor = options.createEditor;

  var initialXML = options.initialXML;

  var otherXML = options.otherXML;

  var describeFn = options.only ? describe.only : describe;

  var hasGlobalUndo = !!options.globalUndo;

  var isDiagramEditor = options.isDiagramEditor;

  /**
   * A common describe for all mountable editors
   */
  describeFn(name + ' - base', function() {

    var editor;

    beforeEach(function() {
      editor = createEditor();
    });

    it('should mount editor', function(done) {

      // given
      var $el = document.createElement('div');

      editor.once('mounted', function() {

        // then
        // editor got mounted
        expect($el.childNodes).to.have.length(1);

        done();
      });

      // when
      editor.mountEditor($el);
    });

    it('should unmount editor', function(done) {

      // given
      var $el = document.createElement('div');

      editor.mountEditor($el);

      editor.once('unmounted', function() {

        // then
        // editor got unmounted
        expect($el.childNodes).to.have.length(0);

        done();
      });

      // when
      editor.unmountEditor($el);
    });

    describe('events', function() {

      it('should emit <shown> without XML', function(done) {

        // given
        var $el = document.createElement('div');

        // wait for diagram shown / imported
        editor.on('shown', function(context) {
          done();
        });

        // when
        editor.mountEditor($el);
      });

      it('should emit <shown> with XML', function(done) {

        // given
        var $el = document.createElement('div');

        // wait for diagram shown / imported
        editor.on('shown', function(context) {

          expect(context).to.exist;
          done();
        });

        // when
        editor.setXML(initialXML);

        editor.mountEditor($el);
      });

      it('should emit <state-updated> before <shown>', function(done) {

        // given
        var $el = document.createElement('div');

        var eventSequence = [];

        editor.on('state-updated', function() {
          eventSequence.push('state-updated');
        });

        // wait for diagram shown / imported
        editor.on('shown', function() {
          eventSequence.push('shown');

          expect(eventSequence).to.eql([ 'state-updated', 'shown' ]);

          done();
        });

        // when
        editor.mountEditor($el);
      });

    });

    describe('importing', function() {

      it('should not import new XML if unmounted');

      it('should import new XML on mount', function(done) {

        // given
        var $el = document.createElement('div');

        editor.once('imported', function(context) {
          // then
          expect(context).to.exist;

          done();
        });

        // when
        editor.setXML(initialXML);

        editor.mountEditor($el);
      });

      it('should import new XML if already mounted', function(done) {

        // given
        var $el = document.createElement('div');

        editor.once('shown', function(context) {

          editor.once('imported', function(context) {

            // then
            expect(context).to.exist;

            done();
          });

          // when
          editor.setXML(otherXML);
        });

        editor.setXML(initialXML);
        editor.mountEditor($el);
      });

      it('should not reimport same XML', function(done) {

        // given
        var $el = document.createElement('div');

        var reimportSpy = spy(function() { });

        editor.once('shown', function(context) {

          editor.once('imported', reimportSpy);

          editor.once('shown', function(context) {

            // then
            expect(reimportSpy).not.to.have.been.called;

            done();
          });

          // when
          editor.setXML(initialXML);
        });

        editor.setXML(initialXML);
        editor.mountEditor($el);
      });

      it('should not import without XML');

    });

    describe('editor state', function() {

      // todo: remove skip once dmn is able to correctly manage it's state
      it.skip('should update on import', function(done) {
        // given
        var $el = document.createElement('div');

        // when
        editor.mountEditor($el);

        // wait for diagram shown / imported
        editor.once('state-updated', function(context) {

          // then
          expect(context).to.have.property('undo', false);
          expect(context).to.have.property('redo', false);
          expect(context).to.have.property('dirty', false);

          done();
        });

        editor.setXML(initialXML, {});
      });

      it('should update on re-mount', function(done) {

        // given
        var $el = document.createElement('div');

        // when
        // mount -> unmount -> remount

        // (1.1) mounted
        editor.once('shown', function() {

          // (2) unmount
          editor.unmountEditor($el);

          editor.once('state-updated', function(context) {

            // then
            expect(context).to.have.property('undo', false);
            expect(context).to.have.property('redo', false);
            expect(context).to.have.property('dirty', false);

            done();
          });

          // (3) remount
          editor.mountEditor($el);
        });

        // (1) mount
        editor.setXML(initialXML, {});

        editor.mountEditor($el);
      });

      // todo: remove skip once dmn is able to correctly manage it's state
      it.skip('should update on new XML', function(done) {

        // given
        var newXML = otherXML;

        var $el = document.createElement('div');

        // when
        editor.once('shown', function() {

          editor.once('state-updated', function(context) {

            // then
            expect(context).to.have.property('undo', hasGlobalUndo);
            expect(context).to.have.property('redo', false);
            expect(context).to.have.property('dirty', true);

            done();
          });

          // when
          // updating to new file
          editor.setXML(newXML);

        }, 300);

        editor.setXML(initialXML, {});
        editor.mountEditor($el);
      });

      it('should reflect initial dirty state', function(done) {

        // wait for diagram shown / imported
        editor.once('state-updated', function(context) {

          // then
          expect(context).to.have.property('undo', false);
          expect(context).to.have.property('redo', false);
          expect(context).to.have.property('dirty', true);

          done();
        });

        // when
        editor.setXML(initialXML, { dirty: true });

        editor.mountEditor(document.createElement('div'));
      });

    });

    // ** From here downwards, only test editors that inherit from DiagramEditor **
    if (!isDiagramEditor) {
      return;
    }

    describe('import warnings overlay', function() {

      it('should hide without import information', function() {

        // given
        editor.lastImport = null;

        // when
        var tree = render(editor);

        // then
        expect(select('[ref=warnings-overlay]', tree)).not.to.exist;
      });

      it('should hide if no warnings', function() {

        // given
        editor.lastImport = {
          warnings: []
        };

        // when
        var tree = render(editor);

        // then
        expect(select('[ref=warnings-overlay]', tree)).not.to.exist;
      });

      it('should show if warnings', function() {

        // given
        editor.lastImport = {
          warnings: [
            new Error('foo bar')
          ]
        };

        // when
        var tree = render(editor);

        // then
        expect(select('[ref=warnings-overlay]', tree)).to.exist;
      });

      it('should hide', function() {

        // given
        editor.lastImport = {
          warnings: [
            new Error('foo bar')
          ]
        };

        // when
        var tree = render(editor);

        var hideWarningsElement = select('[ref=warnings-hide-link]', tree);

        simulateEvent(hideWarningsElement, 'click');

        // then
        // we simply discard the last import information
        expect(editor.lastImport).not.to.exist;
      });

      it('should open log', function(done) {

        // given
        editor.lastImport = {
          warnings: [
            new Error('foo bar'),
            new Error('foo BABA')
          ]
        };

        editor.once('log:toggle', function(state) {

          // then
          expect(state.open).to.be.true;

          tree = render(editor);

          // then expect warnings overlay to not be there
          expect(select('[ref=warnings-details]', tree)).to.not.exist;

          done();
        });

        // when
        var tree = render(editor);

        var openLogElement = select('[ref=warnings-details-link]', tree);

        simulateEvent(openLogElement, 'click');
      });

      it('should close warnings overlay when log is cleared', function(done) {

        // given
        var hideWarningsSpy = spy(editor, 'hideWarnings');

        editor.once('layout:update', function(newLayout) {

          // then
          expect(hideWarningsSpy).to.have.been.called;

          done();
        });

        // when
        editor.emit('layout:update', {
          log: {
            open: false,
            cleared: true
          }
        });
      });

    });
  });

}

module.exports.describeEditor = describeEditor;

module.exports.describeEditor.only = function(name, options) {
  options.only = true;

  return describeEditor(name, options);
};
<commit_msg>test(client): only .skip DMN editor tests<commit_after>'use strict';

var ensureOpts = require('util/ensure-opts');

var spy = require('test/helper/util/spy');

var select = require('test/helper/vdom').select,
    render = require('test/helper/vdom').render,
    simulateEvent = require('test/helper/vdom').simulateEvent;

function describeEditor(name, options) {

  ensureOpts([
    'createEditor',
    'initialXML',
    'otherXML',
    'isDiagramEditor'
  ], options);

  var createEditor = options.createEditor;

  var initialXML = options.initialXML;

  var otherXML = options.otherXML;

  var describeFn = options.only ? describe.only : describe;

  var hasGlobalUndo = !!options.globalUndo;

  var isDiagramEditor = options.isDiagramEditor;

  /**
   * A common describe for all mountable editors
   */
  describeFn(name + ' - base', function() {

    var editor;

    beforeEach(function() {
      editor = createEditor();
    });

    it('should mount editor', function(done) {

      // given
      var $el = document.createElement('div');

      editor.once('mounted', function() {

        // then
        // editor got mounted
        expect($el.childNodes).to.have.length(1);

        done();
      });

      // when
      editor.mountEditor($el);
    });

    it('should unmount editor', function(done) {

      // given
      var $el = document.createElement('div');

      editor.mountEditor($el);

      editor.once('unmounted', function() {

        // then
        // editor got unmounted
        expect($el.childNodes).to.have.length(0);

        done();
      });

      // when
      editor.unmountEditor($el);
    });

    describe('events', function() {

      it('should emit <shown> without XML', function(done) {

        // given
        var $el = document.createElement('div');

        // wait for diagram shown / imported
        editor.on('shown', function(context) {
          done();
        });

        // when
        editor.mountEditor($el);
      });

      it('should emit <shown> with XML', function(done) {

        // given
        var $el = document.createElement('div');

        // wait for diagram shown / imported
        editor.on('shown', function(context) {

          expect(context).to.exist;
          done();
        });

        // when
        editor.setXML(initialXML);

        editor.mountEditor($el);
      });

      it('should emit <state-updated> before <shown>', function(done) {

        // given
        var $el = document.createElement('div');

        var eventSequence = [];

        editor.on('state-updated', function() {
          eventSequence.push('state-updated');
        });

        // wait for diagram shown / imported
        editor.on('shown', function() {
          eventSequence.push('shown');

          expect(eventSequence).to.eql([ 'state-updated', 'shown' ]);

          done();
        });

        // when
        editor.mountEditor($el);
      });

    });

    describe('importing', function() {

      it('should not import new XML if unmounted');

      it('should import new XML on mount', function(done) {

        // given
        var $el = document.createElement('div');

        editor.once('imported', function(context) {
          // then
          expect(context).to.exist;

          done();
        });

        // when
        editor.setXML(initialXML);

        editor.mountEditor($el);
      });

      it('should import new XML if already mounted', function(done) {

        // given
        var $el = document.createElement('div');

        editor.once('shown', function(context) {

          editor.once('imported', function(context) {

            // then
            expect(context).to.exist;

            done();
          });

          // when
          editor.setXML(otherXML);
        });

        editor.setXML(initialXML);
        editor.mountEditor($el);
      });

      it('should not reimport same XML', function(done) {

        // given
        var $el = document.createElement('div');

        var reimportSpy = spy(function() { });

        editor.once('shown', function(context) {

          editor.once('imported', reimportSpy);

          editor.once('shown', function(context) {

            // then
            expect(reimportSpy).not.to.have.been.called;

            done();
          });

          // when
          editor.setXML(initialXML);
        });

        editor.setXML(initialXML);
        editor.mountEditor($el);
      });

      it('should not import without XML');

    });

    describe('editor state', function() {

      // TODO(ricardomatias): remove skip once dmn is able
      // to correctly manage it's state
      if (name !== 'DmnEditor') {

        it('should update on import', function(done) {
          // given
          var $el = document.createElement('div');

          // when
          editor.mountEditor($el);

          // wait for diagram shown / imported
          editor.once('state-updated', function(context) {

            // then
            expect(context).to.have.property('undo', false);
            expect(context).to.have.property('redo', false);
            expect(context).to.have.property('dirty', false);

            done();
          });

          editor.setXML(initialXML, {});
        });
      }

      it('should update on re-mount', function(done) {

        // given
        var $el = document.createElement('div');

        // when
        // mount -> unmount -> remount

        // (1.1) mounted
        editor.once('shown', function() {

          // (2) unmount
          editor.unmountEditor($el);

          editor.once('state-updated', function(context) {

            // then
            expect(context).to.have.property('undo', false);
            expect(context).to.have.property('redo', false);
            expect(context).to.have.property('dirty', false);

            done();
          });

          // (3) remount
          editor.mountEditor($el);
        });

        // (1) mount
        editor.setXML(initialXML, {});

        editor.mountEditor($el);
      });

      // TODO(ricardomatias): remove skip once dmn is able
      // to correctly manage it's state
      if (name !== 'DmnEditor') {

        it('should update on new XML', function(done) {

          // given
          var newXML = otherXML;

          var $el = document.createElement('div');

          // when
          editor.once('shown', function() {

            editor.once('state-updated', function(context) {

              // then
              expect(context).to.have.property('undo', hasGlobalUndo);
              expect(context).to.have.property('redo', false);
              expect(context).to.have.property('dirty', true);

              done();
            });

            // when
            // updating to new file
            editor.setXML(newXML);

          }, 300);

          editor.setXML(initialXML, {});
          editor.mountEditor($el);
        });
      }

      it('should reflect initial dirty state', function(done) {

        // wait for diagram shown / imported
        editor.once('state-updated', function(context) {

          // then
          expect(context).to.have.property('undo', false);
          expect(context).to.have.property('redo', false);
          expect(context).to.have.property('dirty', true);

          done();
        });

        // when
        editor.setXML(initialXML, { dirty: true });

        editor.mountEditor(document.createElement('div'));
      });

    });

    // ** From here downwards, only test editors that inherit from DiagramEditor **
    if (!isDiagramEditor) {
      return;
    }

    describe('import warnings overlay', function() {

      it('should hide without import information', function() {

        // given
        editor.lastImport = null;

        // when
        var tree = render(editor);

        // then
        expect(select('[ref=warnings-overlay]', tree)).not.to.exist;
      });

      it('should hide if no warnings', function() {

        // given
        editor.lastImport = {
          warnings: []
        };

        // when
        var tree = render(editor);

        // then
        expect(select('[ref=warnings-overlay]', tree)).not.to.exist;
      });

      it('should show if warnings', function() {

        // given
        editor.lastImport = {
          warnings: [
            new Error('foo bar')
          ]
        };

        // when
        var tree = render(editor);

        // then
        expect(select('[ref=warnings-overlay]', tree)).to.exist;
      });

      it('should hide', function() {

        // given
        editor.lastImport = {
          warnings: [
            new Error('foo bar')
          ]
        };

        // when
        var tree = render(editor);

        var hideWarningsElement = select('[ref=warnings-hide-link]', tree);

        simulateEvent(hideWarningsElement, 'click');

        // then
        // we simply discard the last import information
        expect(editor.lastImport).not.to.exist;
      });

      it('should open log', function(done) {

        // given
        editor.lastImport = {
          warnings: [
            new Error('foo bar'),
            new Error('foo BABA')
          ]
        };

        editor.once('log:toggle', function(state) {

          // then
          expect(state.open).to.be.true;

          tree = render(editor);

          // then expect warnings overlay to not be there
          expect(select('[ref=warnings-details]', tree)).to.not.exist;

          done();
        });

        // when
        var tree = render(editor);

        var openLogElement = select('[ref=warnings-details-link]', tree);

        simulateEvent(openLogElement, 'click');
      });

      it('should close warnings overlay when log is cleared', function(done) {

        // given
        var hideWarningsSpy = spy(editor, 'hideWarnings');

        editor.once('layout:update', function(newLayout) {

          // then
          expect(hideWarningsSpy).to.have.been.called;

          done();
        });

        // when
        editor.emit('layout:update', {
          log: {
            open: false,
            cleared: true
          }
        });
      });

    });
  });

}

module.exports.describeEditor = describeEditor;

module.exports.describeEditor.only = function(name, options) {
  options.only = true;

  return describeEditor(name, options);
};
<|endoftext|>