<commit_before>// `apostrophe-pieces-pages` implements "index pages" that display pieces of a
// particular type in a paginated, filterable way. It's great for implementing
// blogs, event listings, project listings, staff directories... almost any
// content type.
//
// You will `extend` this module in new modules corresponding to your modules
// that extend `apostrophe-pieces`.
//
// To learn more and see complete examples, see:
//
// [Reusable content with pieces](../../tutorials/getting-started/reusable-content-with-pieces.html)
//
// ## Options
//
// ### `piecesFilters`
//
// If present, this is an array of objects with `name` properties. The named cursor filters are
// marked as `safeFor: "public"` if they exist, and an array of choices for each is populated
// in `req.data.piecesFilters.tags` (if the field in question is `tags`), etc. The choices in the
// array are objects with `label` and `value` properties.

var _ = require('lodash');
var async = require('async');

module.exports = {
  extend: 'apostrophe-custom-pages',

  afterConstruct: function(self) {
    self.dispatchAll();
    self.enableAddUrlsToPieces();
  },

  construct: function(self, options) {
    self.options.addFields = [
      {
        type: 'tags',
        name: 'withTags',
        label: 'Show Content With These Tags'
      }
    ].concat(self.options.addFields || []);
    self.label = self.options.label;
    self.perPage = options.perPage || 10;

    self.piecesModuleName = self.options.piecesModuleName || self.__meta.name.replace(/\-pages$/, '');
    self.pieces = self.apos.modules[self.piecesModuleName];
    self.piecesCssName = self.apos.utils.cssName(self.pieces.name);

    self.piecesFilters = self.options.piecesFilters || [];

    self.contextMenu = options.contextMenu || [
      {
        action: 'create-' + self.piecesCssName,
        label: 'Create ' + self.pieces.label
      },
      {
        action: 'edit-' + self.piecesCssName,
        label: 'Update ' + self.pieces.label,
        value: true
      },
      {
        action: 'manage-' + self.piecesCssName,
        label: 'Manage ' + self.pieces.pluralLabel
      },
      {
        action: 'versions-' + self.piecesCssName,
        label: self.pieces.label + ' Versions',
        value: true
      },
      {
        action: 'trash-' + self.piecesCssName,
        label: 'Trash ' + self.pieces.label,
        value: true
      }
    ];

    self.publishMenu = options.publishMenu || [
      {
        action: 'publish-' + self.piecesCssName,
        label: 'Publish ' + self.pieces.label,
        value: true
      },
    ];

    // Extend this method for your piece type to call additional
    // chainable filters by default. You should not add entirely new
    // filters here. For that, define the appropriate subclass of
    // `apostrophe-pieces-cursor` in your subclass of
    // `apostrophe-pieces`.

    self.indexCursor = function(req) {
      var cursor = self.pieces.find(req, {})
        .safeFilters(_.pluck(self.piecesFilters, 'name'))
        .queryToFilters(req.query, 'public')
        .perPage(self.perPage);
      self.filterByIndexPageTags(cursor, req.data.page);
      return cursor;
    };

    // At the URL of the page, display an index view (list view) of
    // the pieces, with support for pagination.

    self.indexPage = function(req, callback) {

      if (self.pieces.contextual) {
        req.contextMenu = _.filter(self.contextMenu, function(item) {
          // Items specific to a particular piece don't make sense
          // on the index page
          return !item.value;
          // Add the standard items, they have to be available sometime
          // when working with an index page
        }).concat(self.apos.pages.options.contextMenu);
        req.publishMenu = _.filter(self.publishMenu, function(item) {
          // Items specific to a particular piece don't make sense
          // on the index page
          return !item.value;
        });
      }

      var cursor = self.indexCursor(req);

      function getFilters(callback) {
        return self.populatePiecesFilters(cursor, callback);
      }

      function totalPieces(callback) {
        cursor.toCount(function(err, count) {
          if (err) {
            return callback(err);
          }
          if (cursor.get('page') > cursor.get('totalPages')) {
            req.notFound = true;
            return callback(null);
          }
          req.data.totalPieces = count;
          req.data.totalPages = cursor.get('totalPages');
          return callback();
        });
      }

      function findPieces(callback) {
        cursor.toArray(function(err, docs) {
          if (err) {
            return callback(err);
          }
          if (self.apos.utils.isAjaxRequest(req)) {
            req.template = self.renderer('indexAjax');
          } else {
            req.template = self.renderer('index');
          }
          req.data.currentPage = cursor.get('page');
          req.data.pieces = docs;
          return callback();
        });
      }

      return async.series([getFilters, totalPieces, findPieces], function(err) {
        return self.beforeIndex(req, callback);
      });
    };

    // Called before `indexPage`. By default, does nothing.
    // A convenient way to extend the functionality.

    self.beforeIndex = function(req, callback) {
      return setImmediate(callback);
    };

    // Invokes filters on the given cursor to ensure it only fetches
    // results with the tags this page has been locked down to via
    // page settings. If it has not been locked down, no filtering occurs.
    // Override to change the behavior.

    self.filterByIndexPageTags = function(cursor, page) {
      if (page.withTags && page.withTags.length) {
        cursor.and({ tags: { $in: page.withTags } });
      }
    };

    // Invoked to display a piece by itself, a "show page." Renders
    // the `show.html` template after setting `data.piece`.
    //
    // If the pieces module is set `contextual: true`, the context menu
    // (the gear at lower left) is updated appropriately if the user has
    // editing permissions.

    self.showPage = function(req, callback) {

      // We'll try to find the piece as an ordinary reader and, if the piece type
      // is contextual, we'll also try it as an editor if needed

      var doc;
      var previous;
      var next;

      return async.series([
        findAsReader,
        findAsEditor,
        findPrevious,
        findNext
      ], function(err) {
        if (err) {
          return callback(err);
        }
        if (!doc) {
          req.notFound = true;
          return callback(null);
        }
        if (self.pieces.contextual) {
          req.contextMenu = _.map(self.contextMenu, function(item) {
            if (item.value) {
              // Don't modify a shared item, race conditions
              // could give us the wrong ids
              item = _.clone(item);
              item.value = doc._id;
            }
            return item;
          });
          req.publishMenu = _.map(self.publishMenu, function(item) {
            if (item.value) {
              // Don't modify a shared item, race conditions
              // could give us the wrong ids
              item = _.clone(item);
              item.value = doc._id;
            }
            return item;
          });
        }
        req.template = self.renderer('show');
        req.data.piece = doc;
        req.data.previous = previous;
        req.data.next = next;
        return self.beforeShow(req, callback);
      });

      function findAsReader(callback) {
        var cursor = self.pieces.find(req, { slug: req.params.slug });
        return cursor.toObject(function(err, _doc) {
          if (err) {
            return callback(err);
          }
          doc = _doc;
          return callback(null);
        });
      }

      function findAsEditor(callback) {
        if (doc || (!req.user) || (!self.pieces.contextual)) {
          return callback(null);
        }
        // Use findForEditing to allow subclasses to extend the set of filters that
        // don't apply by default in an editing context. -Tom
        var cursor = self.pieces.findForEditing(req, { slug: req.params.slug });
        return cursor.toObject(function(err, _doc) {
          if (err) {
            return callback(err);
          }
          doc = _doc;
          return callback(null);
        });
      }

      function findPrevious(callback) {
        if (!self.options.previous) {
          return callback(null);
        }
        if (!doc) {
          return callback(null);
        }
        var cursor = self.indexCursor(req);
        return cursor.previous(doc).toObject(function(err, _previous) {
          if (err) {
            return callback(err);
          }
          previous = _previous;
          return callback(null);
        });
      }

      function findNext(callback) {
        if (!self.options.next) {
          return callback(null);
        }
        if (!doc) {
          return callback(null);
        }
        var cursor = self.indexCursor(req);
        return cursor.next(doc).toObject(function(err, _next) {
          if (err) {
            return callback(err);
          }
          next = _next;
          return callback(null);
        });
      }

    };

    // Invoked just before the piece is displayed on its "show page." By default,
    // does nothing. A useful override point.

    self.beforeShow = function(req, callback) {
      return setImmediate(callback);
    };

    // Set the dispatch routes. By default, the bare URL of the page displays
    // the index view via `indexPage`; if the URL has an additional component,
    // e.g. `/blog/good-article`, it is assumed to be the slug of the
    // article and `showPage` is invoked. You can override this method,
    // for instance to also accept `/:year/:month/:day/:slug` as a way of
    // invoking `self.showPage`. See [apostrophe-custom-pages](../apostrophe-custom-pages/index.html)
    // for more about what you can do with dispatch routes.

    self.dispatchAll = function() {
      self.dispatch('/', self.indexPage);
      self.dispatch('/:slug', self.showPage);
    };

    // Given an array containing all of the index pages of this type that
    // exist on the site and an individual piece, return the
    // index page that is the best fit for use in the URL of
    // the piece. The algorithm is based on shared tags, with
    // an emphasis on matching tags, and also favors an index
    // page with no preferred tags over bad matches. Override to
    // replace the algorithm.
    //
    // This method is called for you. In the presence of index pages, the
    // cursors for the corresponding pieces are automatically enhanced to
    // invoke it when building URLs.

    self.chooseParentPage = function(pages, piece) {
      var property = self.options.chooseParentPageBy || 'tags';
      // The "tags" property was moved to "withTags" to distinguish
      // what a page *shows* from what a page *is*
      var pageProperty = (property === 'tags') ? 'withTags' : property;
      var tags = piece[property] || [];
      var bestScore;
      var best = null;
      _.each(pages, function(page) {
        var score = 0;
        var pageTags = page[pageProperty] ? page[pageProperty] : [];
        if (!pageTags.length) {
          score = 1;
        }
        var intersect = _.intersection(tags, pageTags);
        var diff = _.difference(tags, pageTags);
        score += intersect.length * 2 - diff.length;
        if ((!best) || (score > bestScore)) {
          bestScore = score;
          best = page;
        }
      });
      return best;
    };

    // Given an index page and a piece, build a complete URL to
    // the piece. If you override `dispatch` to change how
    // "show page" URLs are matched, you will also want to override
    // this method to build them differently.

    self.buildUrl = function(page, piece) {
      if (!page) {
        return false;
      }
      return page._url + '/' + piece.slug;
    };

    // Make the browser-side `apos` object aware of the current
    // in-context piece, as `apos.contextPiece`. Just enough to
    // help the contextual editing tools in various modules

    self.pushContextPiece = function(req) {
      if (self.pieces.options.contextual && req.data.piece) {
        req.browserCall('apos.contextPiece = ?', _.pick(req.data.piece, '_id', 'title', 'slug', 'type'));
      }
    };

    var superPageBeforeSend = self.pageBeforeSend;

    // Calls `pushContextPiece` to make `apos.contextPiece` available
    // in the browser

    self.pageBeforeSend = function(req) {
      self.pushContextPiece(req);
      return superPageBeforeSend(req);
    };

    // Adds the `._url` property to all of the provided pieces,
    // which are assumed to be of the appropriate type for this module.
    // Aliased as the `addUrls` method of [apostrophe-pieces](../apostrophe-pieces/index.html), which
    // is invoked by the `addUrls` filter of [apostrophe-cursor](../apostrophe-docs/server-apostrophe-cursor.html).

    self.addUrlsToPieces = function(req, results, callback) {
      var pieceName = self.pieces.name;
      return async.series({
        getIndexPages: function(callback) {
          if (req.aposParentPageCache && req.aposParentPageCache[pieceName]) {
            return setImmediate(callback);
          }
          return self.findForAddUrlsToPieces(req)
            .toArray(function(err, pages) {
              if (err) {
                return callback(err);
              }
              if (!req.aposParentPageCache) {
                req.aposParentPageCache = {};
              }
              req.aposParentPageCache[pieceName] = pages;
              return callback(null);
            }
          );
        }
      }, function(err) {
        _.each(results, function(piece) {
          var parentPage = self.chooseParentPage(req.aposParentPageCache[pieceName], piece);
          if (parentPage) {
            piece._url = self.buildUrl(parentPage, piece);
            piece._parentUrl = parentPage._url;
          }
        });
        return callback(null);
      });
    };

    // Returns a cursor suitable for finding pieces-pages for the
    // purposes of assigning URLs to pieces based on the best match.
    //
    // Should be as fast as possible while still returning enough
    // information to do that. For instance, tags are essential for the standard
    // `chooseParentPage` algorithm, but joins and areas are not.
    //
    // The default implementation returns a cursor with areas
    // and joins shut off.

    self.findForAddUrlsToPieces = function(req) {
      return self.find(req)
        .areas(false)
        .joins(false);
    };

    // Configure our `addUrlsToPieces` method as the `addUrls` method
    // of the related pieces module.

    self.enableAddUrlsToPieces = function() {
      self.pieces.setAddUrls(self.addUrlsToPieces);
    };

    // Populate `req.data.piecesFilters` with arrays of choice objects,
    // with label and value properties, for each filter configured in the
    // `piecesFilters` array option. Each filter in that array must have a
    // `name` property. Distinct values are fetched for the corresponding
    // cursor filter (note that most schema fields automatically get a
    // corresponding cursor filter method). Each filter's choices are
    // reduced by the other filters; for instance, "tags" might only reveal
    // choices not ruled out by the current "topic" filter setting.

    self.populatePiecesFilters = function(cursor, callback) {
      var req = cursor.get('req');
      req.data.piecesFilters = req.data.piecesFilters || {};
      return async.eachSeries(self.piecesFilters, function(filter, callback) {
        // The choices for each filter should reflect the effect of all filters
        // except this one (filtering by topic pares down the list of categories and
        // vice versa)
        var value = cursor.get(filter);
        var _cursor = cursor.clone();
        _cursor[filter.name](undefined);
        return _cursor.toChoices(filter.name, function(err, choices) {
          if (err) {
            return callback(err);
          }
          req.data.piecesFilters[filter.name] = choices;
          return callback(null);
        });
      }, callback);
    };
  }
};
<commit_msg>Allow projections for next and previous<commit_after>// `apostrophe-pieces-pages` implements "index pages" that display pieces of a
// particular type in a paginated, filterable way. It's great for implementing
// blogs, event listings, project listings, staff directories... almost any
// content type.
//
// You will `extend` this module in new modules corresponding to your modules
// that extend `apostrophe-pieces`.
//
// To learn more and see complete examples, see:
//
// [Reusable content with pieces](../../tutorials/getting-started/reusable-content-with-pieces.html)
//
// ## Options
//
// ### `piecesFilters`
//
// If present, this is an array of objects with `name` properties. The named cursor filters are
// marked as `safeFor: "public"` if they exist, and an array of choices for each is populated
// in `req.data.piecesFilters.tags` (if the field in question is `tags`), etc. The choices in the
// array are objects with `label` and `value` properties.

var _ = require('lodash');
var async = require('async');

module.exports = {
  extend: 'apostrophe-custom-pages',

  afterConstruct: function(self) {
    self.dispatchAll();
    self.enableAddUrlsToPieces();
  },

  construct: function(self, options) {
    self.options.addFields = [
      {
        type: 'tags',
        name: 'withTags',
        label: 'Show Content With These Tags'
      }
    ].concat(self.options.addFields || []);
    self.label = self.options.label;
    self.perPage = options.perPage || 10;

    self.piecesModuleName = self.options.piecesModuleName || self.__meta.name.replace(/\-pages$/, '');
    self.pieces = self.apos.modules[self.piecesModuleName];
    self.piecesCssName = self.apos.utils.cssName(self.pieces.name);

    self.piecesFilters = self.options.piecesFilters || [];

    self.contextMenu = options.contextMenu || [
      {
        action: 'create-' + self.piecesCssName,
        label: 'Create ' + self.pieces.label
      },
      {
        action: 'edit-' + self.piecesCssName,
        label: 'Update ' + self.pieces.label,
        value: true
      },
      {
        action: 'manage-' + self.piecesCssName,
        label: 'Manage ' + self.pieces.pluralLabel
      },
      {
        action: 'versions-' + self.piecesCssName,
        label: self.pieces.label + ' Versions',
        value: true
      },
      {
        action: 'trash-' + self.piecesCssName,
        label: 'Trash ' + self.pieces.label,
        value: true
      }
    ];

    self.publishMenu = options.publishMenu || [
      {
        action: 'publish-' + self.piecesCssName,
        label: 'Publish ' + self.pieces.label,
        value: true
      },
    ];

    // Extend this method for your piece type to call additional
    // chainable filters by default. You should not add entirely new
    // filters here. For that, define the appropriate subclass of
    // `apostrophe-pieces-cursor` in your subclass of
    // `apostrophe-pieces`.

    self.indexCursor = function(req) {
      var cursor = self.pieces.find(req, {})
        .safeFilters(_.pluck(self.piecesFilters, 'name'))
        .queryToFilters(req.query, 'public')
        .perPage(self.perPage);
      self.filterByIndexPageTags(cursor, req.data.page);
      return cursor;
    };

    // At the URL of the page, display an index view (list view) of
    // the pieces, with support for pagination.

    self.indexPage = function(req, callback) {

      if (self.pieces.contextual) {
        req.contextMenu = _.filter(self.contextMenu, function(item) {
          // Items specific to a particular piece don't make sense
          // on the index page
          return !item.value;
          // Add the standard items, they have to be available sometime
          // when working with an index page
        }).concat(self.apos.pages.options.contextMenu);
        req.publishMenu = _.filter(self.publishMenu, function(item) {
          // Items specific to a particular piece don't make sense
          // on the index page
          return !item.value;
        });
      }

      var cursor = self.indexCursor(req);

      function getFilters(callback) {
        return self.populatePiecesFilters(cursor, callback);
      }

      function totalPieces(callback) {
        cursor.toCount(function(err, count) {
          if (err) {
            return callback(err);
          }
          if (cursor.get('page') > cursor.get('totalPages')) {
            req.notFound = true;
            return callback(null);
          }
          req.data.totalPieces = count;
          req.data.totalPages = cursor.get('totalPages');
          return callback();
        });
      }

      function findPieces(callback) {
        cursor.toArray(function(err, docs) {
          if (err) {
            return callback(err);
          }
          if (self.apos.utils.isAjaxRequest(req)) {
            req.template = self.renderer('indexAjax');
          } else {
            req.template = self.renderer('index');
          }
          req.data.currentPage = cursor.get('page');
          req.data.pieces = docs;
          return callback();
        });
      }

      return async.series([getFilters, totalPieces, findPieces], function(err) {
        return self.beforeIndex(req, callback);
      });
    };

    // Called before `indexPage`. By default, does nothing.
    // A convenient way to extend the functionality.

    self.beforeIndex = function(req, callback) {
      return setImmediate(callback);
    };

    // Invokes filters on the given cursor to ensure it only fetches
    // results with the tags this page has been locked down to via
    // page settings. If it has not been locked down, no filtering occurs.
    // Override to change the behavior.

    self.filterByIndexPageTags = function(cursor, page) {
      if (page.withTags && page.withTags.length) {
        cursor.and({ tags: { $in: page.withTags } });
      }
    };

    // Invoked to display a piece by itself, a "show page." Renders
    // the `show.html` template after setting `data.piece`.
    //
    // If the pieces module is set `contextual: true`, the context menu
    // (the gear at lower left) is updated appropriately if the user has
    // editing permissions.

    self.showPage = function(req, callback) {

      // We'll try to find the piece as an ordinary reader and, if the piece type
      // is contextual, we'll also try it as an editor if needed

      var doc;
      var previous;
      var next;

      return async.series([
        findAsReader,
        findAsEditor,
        findPrevious,
        findNext
      ], function(err) {
        if (err) {
          return callback(err);
        }
        if (!doc) {
          req.notFound = true;
          return callback(null);
        }
        if (self.pieces.contextual) {
          req.contextMenu = _.map(self.contextMenu, function(item) {
            if (item.value) {
              // Don't modify a shared item, race conditions
              // could give us the wrong ids
              item = _.clone(item);
              item.value = doc._id;
            }
            return item;
          });
          req.publishMenu = _.map(self.publishMenu, function(item) {
            if (item.value) {
              // Don't modify a shared item, race conditions
              // could give us the wrong ids
              item = _.clone(item);
              item.value = doc._id;
            }
            return item;
          });
        }
        req.template = self.renderer('show');
        req.data.piece = doc;
        req.data.previous = previous;
        req.data.next = next;
        return self.beforeShow(req, callback);
      });

      function findAsReader(callback) {
        var cursor = self.pieces.find(req, { slug: req.params.slug });
        return cursor.toObject(function(err, _doc) {
          if (err) {
            return callback(err);
          }
          doc = _doc;
          return callback(null);
        });
      }

      function findAsEditor(callback) {
        if (doc || (!req.user) || (!self.pieces.contextual)) {
          return callback(null);
        }
        // Use findForEditing to allow subclasses to extend the set of filters that
        // don't apply by default in an editing context. -Tom
        var cursor = self.pieces.findForEditing(req, { slug: req.params.slug });
        return cursor.toObject(function(err, _doc) {
          if (err) {
            return callback(err);
          }
          doc = _doc;
          return callback(null);
        });
      }

      function findPrevious(callback) {
        if (!self.options.previous) {
          return callback(null);
        }
        if (!doc) {
          return callback(null);
        }
        var cursor = self.indexCursor(req);
        return cursor.previous(doc)
          .applyFilters(
            typeof(self.options.previous) === 'object' ?
              self.options.previous : {}
          )
          .toObject(function(err, _previous) {
          if (err) {
            return callback(err);
          }
          previous = _previous;
          return callback(null);
        });
      }

      function findNext(callback) {
        if (!self.options.next) {
          return callback(null);
        }
        if (!doc) {
          return callback(null);
        }
        var cursor = self.indexCursor(req);
        return cursor.next(doc)
        .applyFilters(
          typeof(self.options.next) === 'object' ?
            self.options.next : {}
        )
        .toObject(function(err, _next) {
          if (err) {
            return callback(err);
          }
          next = _next;
          console.log(next);
          return callback(null);
        });
      }

    };

    // Invoked just before the piece is displayed on its "show page." By default,
    // does nothing. A useful override point.

    self.beforeShow = function(req, callback) {
      return setImmediate(callback);
    };

    // Set the dispatch routes. By default, the bare URL of the page displays
    // the index view via `indexPage`; if the URL has an additional component,
    // e.g. `/blog/good-article`, it is assumed to be the slug of the
    // article and `showPage` is invoked. You can override this method,
    // for instance to also accept `/:year/:month/:day/:slug` as a way of
    // invoking `self.showPage`. See [apostrophe-custom-pages](../apostrophe-custom-pages/index.html)
    // for more about what you can do with dispatch routes.

    self.dispatchAll = function() {
      self.dispatch('/', self.indexPage);
      self.dispatch('/:slug', self.showPage);
    };

    // Given an array containing all of the index pages of this type that
    // exist on the site and an individual piece, return the
    // index page that is the best fit for use in the URL of
    // the piece. The algorithm is based on shared tags, with
    // an emphasis on matching tags, and also favors an index
    // page with no preferred tags over bad matches. Override to
    // replace the algorithm.
    //
    // This method is called for you. In the presence of index pages, the
    // cursors for the corresponding pieces are automatically enhanced to
    // invoke it when building URLs.

    self.chooseParentPage = function(pages, piece) {
      var property = self.options.chooseParentPageBy || 'tags';
      // The "tags" property was moved to "withTags" to distinguish
      // what a page *shows* from what a page *is*
      var pageProperty = (property === 'tags') ? 'withTags' : property;
      var tags = piece[property] || [];
      var bestScore;
      var best = null;
      _.each(pages, function(page) {
        var score = 0;
        var pageTags = page[pageProperty] ? page[pageProperty] : [];
        if (!pageTags.length) {
          score = 1;
        }
        var intersect = _.intersection(tags, pageTags);
        var diff = _.difference(tags, pageTags);
        score += intersect.length * 2 - diff.length;
        if ((!best) || (score > bestScore)) {
          bestScore = score;
          best = page;
        }
      });
      return best;
    };

    // Given an index page and a piece, build a complete URL to
    // the piece. If you override `dispatch` to change how
    // "show page" URLs are matched, you will also want to override
    // this method to build them differently.

    self.buildUrl = function(page, piece) {
      if (!page) {
        return false;
      }
      return page._url + '/' + piece.slug;
    };

    // Make the browser-side `apos` object aware of the current
    // in-context piece, as `apos.contextPiece`. Just enough to
    // help the contextual editing tools in various modules

    self.pushContextPiece = function(req) {
      if (self.pieces.options.contextual && req.data.piece) {
        req.browserCall('apos.contextPiece = ?', _.pick(req.data.piece, '_id', 'title', 'slug', 'type'));
      }
    };

    var superPageBeforeSend = self.pageBeforeSend;

    // Calls `pushContextPiece` to make `apos.contextPiece` available
    // in the browser

    self.pageBeforeSend = function(req) {
      self.pushContextPiece(req);
      return superPageBeforeSend(req);
    };

    // Adds the `._url` property to all of the provided pieces,
    // which are assumed to be of the appropriate type for this module.
    // Aliased as the `addUrls` method of [apostrophe-pieces](../apostrophe-pieces/index.html), which
    // is invoked by the `addUrls` filter of [apostrophe-cursor](../apostrophe-docs/server-apostrophe-cursor.html).

    self.addUrlsToPieces = function(req, results, callback) {
      var pieceName = self.pieces.name;
      return async.series({
        getIndexPages: function(callback) {
          if (req.aposParentPageCache && req.aposParentPageCache[pieceName]) {
            return setImmediate(callback);
          }
          return self.findForAddUrlsToPieces(req)
            .toArray(function(err, pages) {
              if (err) {
                return callback(err);
              }
              if (!req.aposParentPageCache) {
                req.aposParentPageCache = {};
              }
              req.aposParentPageCache[pieceName] = pages;
              return callback(null);
            }
          );
        }
      }, function(err) {
        _.each(results, function(piece) {
          var parentPage = self.chooseParentPage(req.aposParentPageCache[pieceName], piece);
          if (parentPage) {
            piece._url = self.buildUrl(parentPage, piece);
            piece._parentUrl = parentPage._url;
          }
        });
        return callback(null);
      });
    };

    // Returns a cursor suitable for finding pieces-pages for the
    // purposes of assigning URLs to pieces based on the best match.
    //
    // Should be as fast as possible while still returning enough
    // information to do that. For instance, tags are essential for the standard
    // `chooseParentPage` algorithm, but joins and areas are not.
    //
    // The default implementation returns a cursor with areas
    // and joins shut off.

    self.findForAddUrlsToPieces = function(req) {
      return self.find(req)
        .areas(false)
        .joins(false);
    };

    // Configure our `addUrlsToPieces` method as the `addUrls` method
    // of the related pieces module.

    self.enableAddUrlsToPieces = function() {
      self.pieces.setAddUrls(self.addUrlsToPieces);
    };

    // Populate `req.data.piecesFilters` with arrays of choice objects,
    // with label and value properties, for each filter configured in the
    // `piecesFilters` array option. Each filter in that array must have a
    // `name` property. Distinct values are fetched for the corresponding
    // cursor filter (note that most schema fields automatically get a
    // corresponding cursor filter method). Each filter's choices are
    // reduced by the other filters; for instance, "tags" might only reveal
    // choices not ruled out by the current "topic" filter setting.

    self.populatePiecesFilters = function(cursor, callback) {
      var req = cursor.get('req');
      req.data.piecesFilters = req.data.piecesFilters || {};
      return async.eachSeries(self.piecesFilters, function(filter, callback) {
        // The choices for each filter should reflect the effect of all filters
        // except this one (filtering by topic pares down the list of categories and
        // vice versa)
        var value = cursor.get(filter);
        var _cursor = cursor.clone();
        _cursor[filter.name](undefined);
        return _cursor.toChoices(filter.name, function(err, choices) {
          if (err) {
            return callback(err);
          }
          req.data.piecesFilters[filter.name] = choices;
          return callback(null);
        });
      }, callback);
    };
  }
};
<|endoftext|>


<commit_before>/*
Copyright (c) 2012, Sid Nallu, Chris Umbel

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

/*
 * contribution by sidred123
 */

/*
 * Compute the Levenshtein distance between two strings.
 * Algorithm based from Speech and Language Processing - Daniel Jurafsky and James H. Martin.
 */

var _ = require('underscore');

// Walk the path back from the matchEnd to the beginning of the match.
// Do this by traversing the distanceMatrix as you would a linked list,
// following going from cell child to parent until reach row 0.
function _getMatchStart(distanceMatrix, matchEnd, sourceLength) {
  var row = sourceLength;
  var column = matchEnd;
  var tmpRow;
  var tmpColumn;

  // match will be empty string
  if (matchEnd === 0) { return 0; }

  while(row > 1 && column > 1) {
   tmpRow = row;
   tmpColumn = column;
   row = distanceMatrix[tmpRow][tmpColumn].parentCell.row;
   column = distanceMatrix[tmpRow][tmpColumn].parentCell.column;
  }

  return column-1;
}

function getMinCostSubstring(distanceMatrix, source, target) {
  var sourceLength = source.length;
  var targetLength = target.length;
  var minDistance = sourceLength + targetLength;
  var matchEnd = targetLength;

  // Find minimum value in last row of the cost matrix. This cell marks the
  // end of the match string.
  for (var column = 0; column <= targetLength; column++) {
    if (minDistance > distanceMatrix[sourceLength][column].cost) {
      minDistance = distanceMatrix[sourceLength][column].cost;
      matchEnd = column;
    }
  }

  matchStart = _getMatchStart(distanceMatrix, matchEnd, sourceLength);
  return {substring: target.slice(matchStart, matchEnd), distance: minDistance};
}

/*
* Returns the Damerau-Levenshtein distance between strings. Counts the distance
* between two strings by returning the number of edit operations required to
* convert `source` into `target`.
*
* Valid edit operations are:
*  - transposition, insertion, deletion, and substitution
*
* Options:
*  insertion_cost: (default: 1)
*  deletion_cost: number (default: 1)
*  substitution_cost: number (default: 1)
*  transposition_cost: number (default: 1)
*  search: boolean (default: false)
*  restricted: boolean (default: false)
*/
function DamerauLevenshteinDistance(source, target, options) {
    var damLevOptions = _.assign(
        { transposition_cost: 1, restricted: false },
        options || {},
        { damerau: true },
    );
    return levenshteinDistance(source, target, damLevOptions);
}

function LevenshteinDistance(source, target, options) {
    var levOptions = _.assign({}, options || {}, { damerau: false });
    return levenshteinDistance(source, target, levOptions);
}

function levenshteinDistance (source, target, options) {
    if(isNaN(options.insertion_cost)) options.insertion_cost = 1;
    if(isNaN(options.deletion_cost)) options.deletion_cost = 1;
    if(isNaN(options.substitution_cost)) options.substitution_cost = 1;

    if(typeof options.search !== 'boolean') options.search = false;

    var isUnrestrictedDamerau = options.damerau && !options.restricted;
    var isRestrictedDamerau = options.damerau && options.restricted;

    if (isUnrestrictedDamerau) {
        var lastRowMap = {};
    }

    var sourceLength = source.length;
    var targetLength = target.length;
    var distanceMatrix = [[{cost: 0}]]; //the root, has no parent cell

    for (var row =  1; row <= sourceLength; row++) {
        distanceMatrix[row] = [];
        distanceMatrix[row][0] = {cost: distanceMatrix[row-1][0].cost + options.deletion_cost, parentCell: {row: row-1, column: 0}};
    }

    for (var column = 1; column <= targetLength; column++) {
        if (options.search) {
          distanceMatrix[0][column] = {cost: 0};
        } else {
          distanceMatrix[0][column] = {cost: distanceMatrix[0][column-1].cost + options.insertion_cost, parentCell: {row: 0, column: column-1}};
        }
    }

    for (var row = 1; row <= sourceLength; row++) {
        if (isUnrestrictedDamerau) {
            var lastColMatch = null;
        }
        for (var column = 1; column <= targetLength; column++) {
            var costToInsert = distanceMatrix[row][column-1].cost + options.insertion_cost;
            var costToDelete = distanceMatrix[row-1][column].cost + options.deletion_cost;

            var sourceElement = source[row-1];
            var targetElement = target[column-1];
            var costToSubstitute = distanceMatrix[row-1][column-1].cost;
            if (sourceElement !== targetElement) {
                costToSubstitute = costToSubstitute + options.substitution_cost;
            }

            var possibleParents = [
              {cost: costToInsert, coordinates: {row: row, column: column-1}},
              {cost: costToDelete, coordinates: {row: row-1, column: column}},
              {cost: costToSubstitute, coordinates: {row: row-1, column: column-1}}
            ];

            // We can add damerau to the possibleParents if the current
            // target-letter has been encountered in our lastRowMap,
            // and if there exists a previous column in this row where the
            // row & column letters matched
            var canDamerau = isUnrestrictedDamerau
                && row > 1 && column > 1
                && lastColMatch
                && targetElement in lastRowMap;

            if (canDamerau) {
                var lastRowMatch = lastRowMap[targetElement];
                var costBeforeTransposition =
                    distanceMatrix[lastRowMatch - 1][lastColMatch - 1].cost;
                var costToTranspose = costBeforeTransposition
                    + ((row - lastRowMatch - 1) * options.deletion_cost)
                    + ((column - lastColMatch - 1) * options.insertion_cost)
                    + options.transposition_cost;
                possibleParents.push({
                    cost: costToTranspose,
                    coordinates: {
                        row: lastRowMatch - 1,
                        column: lastColMatch - 1,
                    },
                });
            }
            // Source and target chars are 1-indexed in the distanceMatrix so previous
            // source/target element is (col/row - 2)
            var canDoRestrictedDamerau = isRestrictedDamerau
                && row > 1 && column > 1
                && sourceElement === target[column - 2]
                && source[row - 2] === targetElement;

            if (canDoRestrictedDamerau) {
                var costBeforeTransposition = distanceMatrix[row - 2][column - 2].cost;
                possibleParents.push({
                    cost: costBeforeTransposition + options.transposition_cost,
                    coordinates: { row: row - 2, column: column - 2 },
                });
            }

            var minCostParent = _.min(possibleParents, function(p) { return p.cost; });

            distanceMatrix[row][column] = {cost: minCostParent.cost, parentCell: minCostParent.coordinates};

            if (isUnrestrictedDamerau) {
                lastRowMap[sourceElement] = row;
                if (sourceElement === targetElement) {
                    lastColMatch = column;
                }
            }
        }
    }

    if (!options.search) {
        return distanceMatrix[sourceLength][targetLength].cost;
    }

    return getMinCostSubstring(distanceMatrix, source, target);
}

module.exports = {
    LevenshteinDistance: LevenshteinDistance,
    DamerauLevenshteinDistance: DamerauLevenshteinDistance,
};
<commit_msg>Remove extra comma in levenstein distance.js<commit_after>/*
Copyright (c) 2012, Sid Nallu, Chris Umbel

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

/*
 * contribution by sidred123
 */

/*
 * Compute the Levenshtein distance between two strings.
 * Algorithm based from Speech and Language Processing - Daniel Jurafsky and James H. Martin.
 */

var _ = require('underscore');

// Walk the path back from the matchEnd to the beginning of the match.
// Do this by traversing the distanceMatrix as you would a linked list,
// following going from cell child to parent until reach row 0.
function _getMatchStart(distanceMatrix, matchEnd, sourceLength) {
  var row = sourceLength;
  var column = matchEnd;
  var tmpRow;
  var tmpColumn;

  // match will be empty string
  if (matchEnd === 0) { return 0; }

  while(row > 1 && column > 1) {
   tmpRow = row;
   tmpColumn = column;
   row = distanceMatrix[tmpRow][tmpColumn].parentCell.row;
   column = distanceMatrix[tmpRow][tmpColumn].parentCell.column;
  }

  return column-1;
}

function getMinCostSubstring(distanceMatrix, source, target) {
  var sourceLength = source.length;
  var targetLength = target.length;
  var minDistance = sourceLength + targetLength;
  var matchEnd = targetLength;

  // Find minimum value in last row of the cost matrix. This cell marks the
  // end of the match string.
  for (var column = 0; column <= targetLength; column++) {
    if (minDistance > distanceMatrix[sourceLength][column].cost) {
      minDistance = distanceMatrix[sourceLength][column].cost;
      matchEnd = column;
    }
  }

  matchStart = _getMatchStart(distanceMatrix, matchEnd, sourceLength);
  return {substring: target.slice(matchStart, matchEnd), distance: minDistance};
}

/*
* Returns the Damerau-Levenshtein distance between strings. Counts the distance
* between two strings by returning the number of edit operations required to
* convert `source` into `target`.
*
* Valid edit operations are:
*  - transposition, insertion, deletion, and substitution
*
* Options:
*  insertion_cost: (default: 1)
*  deletion_cost: number (default: 1)
*  substitution_cost: number (default: 1)
*  transposition_cost: number (default: 1)
*  search: boolean (default: false)
*  restricted: boolean (default: false)
*/
function DamerauLevenshteinDistance(source, target, options) {
    var damLevOptions = _.assign(
        { transposition_cost: 1, restricted: false },
        options || {},
        { damerau: true }
    );
    return levenshteinDistance(source, target, damLevOptions);
}

function LevenshteinDistance(source, target, options) {
    var levOptions = _.assign({}, options || {}, { damerau: false });
    return levenshteinDistance(source, target, levOptions);
}

function levenshteinDistance (source, target, options) {
    if(isNaN(options.insertion_cost)) options.insertion_cost = 1;
    if(isNaN(options.deletion_cost)) options.deletion_cost = 1;
    if(isNaN(options.substitution_cost)) options.substitution_cost = 1;

    if(typeof options.search !== 'boolean') options.search = false;

    var isUnrestrictedDamerau = options.damerau && !options.restricted;
    var isRestrictedDamerau = options.damerau && options.restricted;

    if (isUnrestrictedDamerau) {
        var lastRowMap = {};
    }

    var sourceLength = source.length;
    var targetLength = target.length;
    var distanceMatrix = [[{cost: 0}]]; //the root, has no parent cell

    for (var row =  1; row <= sourceLength; row++) {
        distanceMatrix[row] = [];
        distanceMatrix[row][0] = {cost: distanceMatrix[row-1][0].cost + options.deletion_cost, parentCell: {row: row-1, column: 0}};
    }

    for (var column = 1; column <= targetLength; column++) {
        if (options.search) {
          distanceMatrix[0][column] = {cost: 0};
        } else {
          distanceMatrix[0][column] = {cost: distanceMatrix[0][column-1].cost + options.insertion_cost, parentCell: {row: 0, column: column-1}};
        }
    }

    for (var row = 1; row <= sourceLength; row++) {
        if (isUnrestrictedDamerau) {
            var lastColMatch = null;
        }
        for (var column = 1; column <= targetLength; column++) {
            var costToInsert = distanceMatrix[row][column-1].cost + options.insertion_cost;
            var costToDelete = distanceMatrix[row-1][column].cost + options.deletion_cost;

            var sourceElement = source[row-1];
            var targetElement = target[column-1];
            var costToSubstitute = distanceMatrix[row-1][column-1].cost;
            if (sourceElement !== targetElement) {
                costToSubstitute = costToSubstitute + options.substitution_cost;
            }

            var possibleParents = [
              {cost: costToInsert, coordinates: {row: row, column: column-1}},
              {cost: costToDelete, coordinates: {row: row-1, column: column}},
              {cost: costToSubstitute, coordinates: {row: row-1, column: column-1}}
            ];

            // We can add damerau to the possibleParents if the current
            // target-letter has been encountered in our lastRowMap,
            // and if there exists a previous column in this row where the
            // row & column letters matched
            var canDamerau = isUnrestrictedDamerau
                && row > 1 && column > 1
                && lastColMatch
                && targetElement in lastRowMap;

            if (canDamerau) {
                var lastRowMatch = lastRowMap[targetElement];
                var costBeforeTransposition =
                    distanceMatrix[lastRowMatch - 1][lastColMatch - 1].cost;
                var costToTranspose = costBeforeTransposition
                    + ((row - lastRowMatch - 1) * options.deletion_cost)
                    + ((column - lastColMatch - 1) * options.insertion_cost)
                    + options.transposition_cost;
                possibleParents.push({
                    cost: costToTranspose,
                    coordinates: {
                        row: lastRowMatch - 1,
                        column: lastColMatch - 1,
                    },
                });
            }
            // Source and target chars are 1-indexed in the distanceMatrix so previous
            // source/target element is (col/row - 2)
            var canDoRestrictedDamerau = isRestrictedDamerau
                && row > 1 && column > 1
                && sourceElement === target[column - 2]
                && source[row - 2] === targetElement;

            if (canDoRestrictedDamerau) {
                var costBeforeTransposition = distanceMatrix[row - 2][column - 2].cost;
                possibleParents.push({
                    cost: costBeforeTransposition + options.transposition_cost,
                    coordinates: { row: row - 2, column: column - 2 },
                });
            }

            var minCostParent = _.min(possibleParents, function(p) { return p.cost; });

            distanceMatrix[row][column] = {cost: minCostParent.cost, parentCell: minCostParent.coordinates};

            if (isUnrestrictedDamerau) {
                lastRowMap[sourceElement] = row;
                if (sourceElement === targetElement) {
                    lastColMatch = column;
                }
            }
        }
    }

    if (!options.search) {
        return distanceMatrix[sourceLength][targetLength].cost;
    }

    return getMinCostSubstring(distanceMatrix, source, target);
}

module.exports = {
    LevenshteinDistance: LevenshteinDistance,
    DamerauLevenshteinDistance: DamerauLevenshteinDistance,
};
<|endoftext|>


<commit_before>{
  "id":26,
  "name":"Digital Delivery Platform",
  "description":"",
  "theme": "Universal Credit",
  "location": "Leeds",
  "phase":"backlog",
  "sro":"TBC",
  "service_man":"TBC",
  "facing":"internal",
  "phase-history":{
    "discovery": [
      {"label":"Predicted Start Date", "date": "TBC"}
    ]
  },
  "priority":"Top"
}
<commit_msg>Update uc-digital-delivery-platform.js<commit_after>{
  "id":26,
  "name":"Digital Delivery Platform",
  "description":"Discovery will look at how to support the digital implementation and delivery of the full Universal Credit service into Jobcentres and Service Centres.",
  "theme": "Universal Credit",
  "location": "Leeds",
  "phase":"backlog",
  "sro":"Erica Thompson",
  "service_man":"Neil Couling",
  "facing":"internal",
  "phase-history":{
    "discovery": [
      {"label":"Predicted Start Date", "date": "February 2016"}
    ]
  },
  "priority":"Top"
}
<|endoftext|>


<commit_before>(function(root) {

  'use strict';

  var availity = root.availity;

  availity.demo.controller('ValidationController', function($scope) {

    $scope.demo = {
      firstName: "robert",
      lastName: null,
      ssn: null,
      date: null,
      rule: 'defaultRules',
      states: [
        { id: 'AL', name: 'Alabama' },
        { id: 'CA', name: 'California' },
        { id: 'NM', name: 'New Mexico' },
        { id: 'TX', name: 'Texas' },
        { id: 'WY', name: 'Wyoming' }
      ],
      selectedSate: null,
      onChangeRules: function() {
        $scope.demo.rule = $scope.demo.rule === 'stateRules' ? 'defaultRules' : 'stateRules';
      }
    };

  });

  availity.demo.config(function(avValProvider) {

    var defaultRules = {
      'ssn': {
        'required': {
          'message': 'SSN is required.'
        },
        'pattern': {
          'value': /\d{3}-?\d{2}-?\d{4}/,
          'message': 'SSN must be between in a format like ***-**-****.'
        }
      },
      'lastName': {
         'required': {
          'message': 'Last name is required.'
        },
        'size': {
          'min': 2,
          'max': 10,
          'message': 'Last name must be between 2 and 10 characters.'
        }
      },
      'firstName': {
        'required': {
          'message': 'First name is required.'
        },
        'size': {
          'min': 2,
          'max': 20,
          'message': 'First name must be between 2 and 20 characters.'
        }
      },
      'date': {

        'dateFormat': {
          'format': 'MM/DD/YYYY',
          'message': 'Format needs to be MM/DD/YYYY'
        },
        'dateRange':{
          'start': {
            'value': '-10',
            'units': 'days'
          },
          'end': {
            'value': '10',
            'units': 'days'
          },
          'format': 'MMDDYYYY',
          'message': 'Date Range is not correct.'
        }
      },
      'icd9': {
        'pattern': {
          'value': /(V\d{2}(\.\d{1,2})?|\d{3}(\.\d{1,2})?|E\d{3}(\.\d)?)/,
          'message': 'ICD-9 code is invalid'
        }
      },
      'taxId': {
        'pattern': {
          'value': /^\d{3}-?\d{2}-?\d{4}|[0-9]{2}-[0-9]{7}|9\d{2}-[7,8]\d-\d{4}|p?-?\d{8}$/,
          'message': 'Tax ID needs to be a valid SSN, EIN, ITIN, ATIN, PTIN'
        }
      }
    };

    var stateRules = _.extend({}, defaultRules, {
      'lastName': null,
      'firstName': {
        'required': null,
        'size': {
          'min': 2,
          'max': 20,
          'message': 'First name must be between 2 and 20 characters.'
        }
      },
      'states': {
        'required': {
          'message': 'State selection is required.'
        }
      },
      'date': {
        'required': {
          'message': 'Date is required.'
        },
        'dateFormat': {
          'format': 'MM/DD/YYYY',
          'message': 'Format needs to be MM/DD/YYYY'
        },
        'dateRange':{
          'start': {
           'value': '05022015'
          },
          'end': {
            'value': '05152015'
          },
          'format': 'MMDDYYYY',
          'message': 'Date Range is not correct.'
        }
      }
    });

    avValProvider.addRules({
      'defaultRules': defaultRules,
      'stateRules': stateRules
    });

  });

})(window);
<commit_msg>update date range example<commit_after>(function(root) {

  'use strict';

  var availity = root.availity;

  availity.demo.controller('ValidationController', function($scope) {

    $scope.demo = {
      firstName: "robert",
      lastName: null,
      ssn: null,
      date: null,
      rule: 'defaultRules',
      states: [
        { id: 'AL', name: 'Alabama' },
        { id: 'CA', name: 'California' },
        { id: 'NM', name: 'New Mexico' },
        { id: 'TX', name: 'Texas' },
        { id: 'WY', name: 'Wyoming' }
      ],
      selectedSate: null,
      onChangeRules: function() {
        $scope.demo.rule = $scope.demo.rule === 'stateRules' ? 'defaultRules' : 'stateRules';
      }
    };

  });

  availity.demo.config(function(avValProvider) {

    var defaultRules = {
      'ssn': {
        'required': {
          'message': 'SSN is required.'
        },
        'pattern': {
          'value': /\d{3}-?\d{2}-?\d{4}/,
          'message': 'SSN must be between in a format like ***-**-****.'
        }
      },
      'lastName': {
         'required': {
          'message': 'Last name is required.'
        },
        'size': {
          'min': 2,
          'max': 10,
          'message': 'Last name must be between 2 and 10 characters.'
        }
      },
      'firstName': {
        'required': {
          'message': 'First name is required.'
        },
        'size': {
          'min': 2,
          'max': 20,
          'message': 'First name must be between 2 and 20 characters.'
        }
      },
      'date': {
        'dateFormat': {
          'format': 'MM/DD/YYYY',
          'message': 'Format needs to be MM/DD/YYYY'
        },
        'dateRange':{
          'start': {
            'value': '-10',
            'units': 'days'
          },
          'end': {
            'value': '10',
            'units': 'days'
          },
          'format': 'MM/DD/YYYY',
          'message': 'Date is not within 10 days from today'
        }
      },
      'icd9': {
        'pattern': {
          'value': /(V\d{2}(\.\d{1,2})?|\d{3}(\.\d{1,2})?|E\d{3}(\.\d)?)/,
          'message': 'ICD-9 code is invalid'
        }
      },
      'taxId': {
        'pattern': {
          'value': /^\d{3}-?\d{2}-?\d{4}|[0-9]{2}-[0-9]{7}|9\d{2}-[7,8]\d-\d{4}|p?-?\d{8}$/,
          'message': 'Tax ID needs to be a valid SSN, EIN, ITIN, ATIN, PTIN'
        }
      }
    };

    var stateRules = _.extend({}, defaultRules, {
      'lastName': null,
      'firstName': {
        'required': null,
        'size': {
          'min': 2,
          'max': 20,
          'message': 'First name must be between 2 and 20 characters.'
        }
      },
      'states': {
        'required': {
          'message': 'State selection is required.'
        }
      },
      'date': {
        'required': {
          'message': 'Date is required.'
        },
        'dateFormat': {
          'format': 'MM/DD/YYYY',
          'message': 'Format needs to be MM/DD/YYYY'
        },
        'dateRange':{
          'start': {
           'value': '05022015'
          },
          'end': {
            'value': '05152015'
          },
          'format': 'MMDDYYYY',
          'message': 'Date Range is not correct.'
        }
      }
    });

    avValProvider.addRules({
      'defaultRules': defaultRules,
      'stateRules': stateRules
    });

  });

})(window);
<|endoftext|>


<commit_before>/*
	ColorBox v1.05 - a full featured, light-weight, customizable lightbox based on jQuery 1.3
	(c) 2009 Jack Moore - www.colorpowered.com - jack@colorpowered.com
	Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
*/

(function($){
var interfaceHeight, interfaceWidth, loadedHeight, loadedWidth, index, related, closeModal, loadingElement, modal, modalOverlay, modalLoadingOverlay, modalContent, modalLoadedContent, modalClose, borderTopLeft, borderTopCenter, borderTopRight, borderMiddleLeft, borderMiddleRight, borderBottomLeft, borderBottomCenter, borderBottomRight;
function setModalOverlay(){
	$([modalOverlay]).css({"position":"absolute", width:$(window).width(), height:$(window).height(), top:$(window).scrollTop(), left:$(window).scrollLeft()});
}
function keypressEvents(e){
	if(e.keyCode == 37){
		$("a#contentPrevious").click();
		$(document).unbind('keydown', keypressEvents);
	}
	else if(e.keyCode == 39){
		$("a#contentNext").click();
		$(document).unbind('keydown', keypressEvents);
	}
}
closeModal = function(){
	$(modal).removeData("open");
	$([modalOverlay, modal]).fadeOut("fast", function(){
		$(modalLoadedContent).empty();
		$([modalOverlay, modal]).hide();//Seems unnecessary, but sometimes IE6 does not hide the modal.
	});
	if(loadingElement){$(loadingElement).remove();}
	$(document).unbind('keydown', keypressEvents);
	$(window).unbind('resize scroll', setModalOverlay);
};
// Convert % values to pixels
function setSize(size, dimension){
	return (typeof size == 'string') ? (size.match(/%/) ? (dimension/100)*parseInt(size) : parseInt(size)) : size;
}
function windowHeight(){return typeof window.innerHeight == 'number' ? window.innerHeight : document.documentElement.clientHeight;}
function windowWidth(){return typeof window.innerWidth == 'number' ? window.innerWidth : document.documentElement.clientWidth;}

//Initialize the modal: store common calculations, preload the interface graphics, append the html.
$(function(){
	$("body").append(
		$([
			modalOverlay = $('
')[0],
			modal = $('
')[0]
		]).hide()
	);
	$(modal).append(
		$([
			borderTopLeft = $('
')[0],
			borderTopCenter = $('
')[0],
			borderTopRight = $('
')[0],
			borderMiddleLeft = $('
')[0],
			borderMiddleRight = $('
')[0],
			borderBottomLeft = $('
')[0],
			borderBottomCenter = $('
')[0],
			borderBottomRight = $('
')[0],
			modalContent = $('
')[0]
		])
	);
	$(modalContent).append(
		$([
			modalLoadedContent = $('

')[0],
			modalLoadingOverlay = $('
')[0],
			modalClose = $('')[0]
		])
	);
	$(modalClose).click(function(){
		closeModal();
		return false;
	});
	$(document).bind('keydown', function(e){if(e.keyCode == 27){closeModal();}});

	$(modal).css("opacity", 0).show();
	loadedHeight = $(modalLoadedContent).outerHeight(true);
	loadedWidth = $(modalLoadedContent).outerWidth(true);
	interfaceHeight = $(borderTopCenter).height()+$(borderBottomCenter).height() + loadedHeight;
	interfaceWidth = $(borderMiddleLeft).width()+$(borderMiddleRight).width() + loadedWidth;
	$(borderMiddleLeft).css({top:$(borderTopLeft).height()});
	$(borderMiddleRight).css({top:$(borderTopRight).height()});
	$(borderTopCenter).css({left:$(borderTopLeft).width()});
	$(borderBottomCenter).css({left:$(borderBottomLeft).width()});
	$(modalContent).css({top:$(borderTopLeft).height(), left:$(borderTopLeft).width()});
	$(modal).hide().css("opacity", 1);
});

$.fn.colorbox = function(settings) {

	settings = $.extend({}, $.fn.colorbox.settings, settings);

	//sets the position of the modal on screen.  A transition speed of 0 will result in no animation.
	function modalPosition(modalWidth, modalHeight, transitionSpeed, callback){
		var winHeight = windowHeight();
		var colorboxHeight = modalHeight + interfaceHeight;
		var colorboxWidth = modalWidth + interfaceWidth;

		var posTop = winHeight/2 - colorboxHeight/2 + $(window).scrollTop();
		var posLeft = $(window).width()/2 - colorboxWidth/2 + $(window).scrollLeft();
		if(colorboxHeight > winHeight){
			posTop -=(colorboxHeight - winHeight);
		}
		if(posTop < 0){posTop = 0;} //keeps the box from expanding to an inaccessible area offscreen.
		if(posLeft < 0){posLeft = 0;}
		//each part is animated seperately to keep them from disappearing during the animation process, which is what would happen if they were positioned relative to a single element being animated.
		$(modal).animate({top:posTop, left:posLeft, height:modalHeight + interfaceHeight, width:modalWidth + interfaceWidth}, transitionSpeed);
		$([borderMiddleLeft, borderMiddleRight]).animate({height:modalHeight+loadedHeight}, transitionSpeed);
		$([borderTopCenter, borderBottomCenter]).animate({width:modalWidth+loadedWidth}, transitionSpeed);
		$(modalContent).animate({height:modalHeight+loadedHeight, width:modalWidth+loadedWidth}, transitionSpeed, function(){
			if(callback){callback();}
			$(document).bind('keydown', keypressEvents);
			if($.browser.msie && $.browser.version < 7){setModalOverlay();}
		});
	}

	var preloads = [];

	function preload(){
		if(settings.preloading !== false && related.length>1){
			var previous, next;
			previous = index > 0 ? related[index-1].href : related[related.length-1].href;
			next = index < related.length-1 ? related[index+1].href : related[0].href;
			return [$("").attr("src", next), $("").attr("src", previous)];
		}
	}

	function centerModal(contentHtml, contentInfo){
		$(modalLoadedContent).hide().html(contentHtml).append(contentInfo);

		if(settings.fixedWidth){$(modalLoadedContent).css({"width":settings.fixedWidth - interfaceWidth}); }
		if(settings.fixedHeight){$(modalLoadedContent).css({"height":settings.fixedHeight - interfaceHeight});}

		if (settings.transition == "elastic") {
			modalPosition($(modalLoadedContent).width(), $(modalLoadedContent).height(), settings.transitionSpeed, function(){
				$(modalLoadedContent).show();
				$(modalLoadingOverlay).hide();
			});
		}
		else {
			$(modal).animate({"opacity":0}, settings.transitionSpeed, function(){
				modalPosition($(modalLoadedContent).outerWidth(true), $(modalLoadedContent).outerHeight(true), 0, function(){
					$(modalLoadedContent).show();
					$(modalLoadingOverlay).hide();
					$(modal).animate({"opacity":1}, settings.transitionSpeed);
				});
			});
		}
		var preloads = preload();
	}

	function buildGallery(that){

		var contentInfo = "
"+that.title+"";

		if(related.length>1){
			contentInfo += " " + settings.contentCurrent + "";
			contentInfo = contentInfo.replace(/\{current\}/, index+1).replace(/\{total\}/, related.length);
			contentInfo += ""+settings.contentPrevious+" ";
			contentInfo += ""+settings.contentNext+" ";
		}

		if (settings.contentInline) {
			centerModal($(settings.contentInline).html(), contentInfo);
		} else if (settings.contentIframe) {
			centerModal("", contentInfo);
		} else if (that.href.match(/.(gif|png|jpg|jpeg|bmp|tif)$/i) && !settings.contentAjax){
			loadingElement = $("").load(function(){
				centerModal("", contentInfo);
			}).attr("src",that.href);
		}else {
			loadingElement = $('

').load(((settings.contentAjax) ? settings.contentAjax : that.href), function(data, textStatus){
				if(textStatus == "success"){
					centerModal($(this).html(), contentInfo);
				} else {
					centerModal("
Ajax request unsuccessful
");
				}
			});
		}
	}

	function contentNav(){
		$(modalLoadingOverlay).show();
		if($(this).attr("id") == "contentPrevious"){
			index = index > 0 ? index-1 : related.length-1;
		} else {
			index = index < related.length-1 ? index+1 : 0;
		}
		buildGallery(related[index]);
		return false;
	}

	$(this).bind("click.colorbox", function () {
		if ($(modal).data("open") !== true) {
			$(modal).data("open", true);
			$(modalLoadedContent).empty().css({
				"height": "auto",
				"width": "auto"
			});
			$(modalClose).html(settings.modalClose);
			$(modalOverlay).css({"opacity": settings.bgOpacity});

			$([modal, modalLoadingOverlay, modalOverlay]).show();

			if(settings.fixedWidth){ settings.fixedWidth = setSize(settings.fixedWidth, windowWidth());}
			if(settings.fixedHeight){ settings.fixedHeight = setSize(settings.fixedHeight, windowHeight());}

			modalPosition(setSize(settings.initialWidth, windowWidth()), setSize(settings.initialHeight, windowHeight()), 0);

			if (this.rel) {
				related = $("a[rel='" + this.rel + "']");
				index = $(related).index(this);
			}
			else {
				related = $(this);
				index = 0;
			}
			buildGallery(related[index]);
			$("a#contentPrevious, a#contentNext").die().live("click", contentNav);
			$(document).bind('keydown', keypressEvents);
			if ($.browser.msie && $.browser.version < 7) {
				$(window).bind("resize scroll", setModalOverlay);
			}
		}
		return false;
	});

	if(settings.open!==false && $(modal).data("open")!==true){
		$(this).triggerHandler('click.colorbox');
	}

	return this.each(function() {
	});
};

/*
	ColorBox Default Settings.

	The colorbox() function takes one argument, an object of key/value pairs, that are used to initialize the modal.

	Please do not change these settings here, instead overwrite these settings when attaching the colorbox() event to your anchors.
	Example (Global)	: $.fn.colorbox.settings.transition = "fade"; //changes the transition to fade for all colorBox() events proceeding it's declaration.
	Example (Specific)	: $("a[href='http://www.google.com']").colorbox({fixedWidth:"700px", fixedHeight:"450px", contentIframe:true});
*/
$.fn.colorbox.settings = {
	transition : "elastic", // "elastic" or "fade". Set transitionSpeed to 0 for no transition.
	transitionSpeed : 250, // Sets the speed of the fade and elastic transition, in milliseconds. Set to 0 for no transition.
	initialWidth : "50%", // Set the initial width of the modal, prior to any content being loaded.
	initialHeight : "50%", // Set the initial height of the modal, prior to any content being loaded.
	fixedWidth : false, // Set a fixed width for div#modalLoadedContent.  Example: "500px"
	fixedHeight : false, // Set a fixed height for div#modalLoadedContent.  Example: "500px"
	contentAjax : false, // Set this to the file, or file+selector of content that will be loaded through an external file.  Example "include.html" or "company.inc.php div#ceo_bio"
	contentInline : false, // Set this to the selector, in jQuery selector format, of inline content to be displayed.  Example "#myHiddenDiv".
	contentIframe : false, // If 'true' specifies that content should be displayed in an iFrame.
	bgOpacity : 0.85, // The modalBackgroundOverlay opacity level. Range: 0 to 1.
	preloading : true, // Allows for preloading of 'Next' and 'Previous' content in a shared relation group (same values for the 'rel' attribute), after the current content has finished loading.  Set to 'false' to disable.
	contentCurrent : "{current} of {total}", // the format of the contentCurrent information
	contentPrevious : "previous", // the anchor text for the previous link in a shared relation group (same values for 'rel').
	contentNext : "next", // the anchor text for the next link in a shared relation group (same 'rel' attribute').
	modalClose : "close", // the anchor text for the close link.  Esc will also close the modal.
	open : false //Automatically opens ColorBox. (fires the click.colorbox event without waiting for user input).
};

})(jQuery);

<commit_msg>Signed-off-by: Jack Moore <jack@colorpowered.com><commit_after>/*
	ColorBox v1.05 - a full featured, light-weight, customizable lightbox based on jQuery 1.3
	(c) 2009 Jack Moore - www.colorpowered.com - jack@colorpowered.com
	Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
*/

(function($){
var interfaceHeight, interfaceWidth, index, related, closeModal, loadingElement, modal, modalOverlay, modalLoadingOverlay, modalContent, modalLoadedContent, modalClose, borderTopLeft, borderTopCenter, borderTopRight, borderMiddleLeft, borderMiddleRight, borderBottomLeft, borderBottomCenter, borderBottomRight;
function setModalOverlay(){
	$([modalOverlay]).css({"position":"absolute", width:$(window).width(), height:$(window).height(), top:$(window).scrollTop(), left:$(window).scrollLeft()});
}
function keypressEvents(e){
	if(e.keyCode == 37){
		$("a#contentPrevious").click();
		$(document).unbind('keydown', keypressEvents);
	}
	else if(e.keyCode == 39){
		$("a#contentNext").click();
		$(document).unbind('keydown', keypressEvents);
	}
}
closeModal = function(){
	$(modal).removeData("open");
	$([modalOverlay, modal]).fadeOut("fast", function(){
		$(modalLoadedContent).empty();
		$([modalOverlay, modal]).hide();//Seems unnecessary, but sometimes IE6 does not hide the modal.
	});
	if(loadingElement){$(loadingElement).remove();}
	$(document).unbind('keydown', keypressEvents);
	$(window).unbind('resize scroll', setModalOverlay);
};
// Convert % values to pixels
function setSize(size, dimension){
	return (typeof size == 'string') ? (size.match(/%/) ? (dimension/100)*parseInt(size) : parseInt(size)) : size;
}
function windowHeight(){return typeof window.innerHeight == 'number' ? window.innerHeight : document.documentElement.clientHeight;}
function windowWidth(){return typeof window.innerWidth == 'number' ? window.innerWidth : document.documentElement.clientWidth;}

//Initialize the modal: store common calculations, preload the interface graphics, append the html.
$(function(){
	$("body").append(
		$([
			modalOverlay = $('
')[0],
			modal = $('
')[0]
		]).hide()
	);
	$(modal).append(
		$([
			borderTopLeft = $('
')[0],
			borderTopCenter = $('
')[0],
			borderTopRight = $('
')[0],
			borderMiddleLeft = $('
')[0],
			borderMiddleRight = $('
')[0],
			borderBottomLeft = $('
')[0],
			borderBottomCenter = $('
')[0],
			borderBottomRight = $('
')[0],
			modalContent = $('
')[0]
		])
	);
	$(modalContent).append(
		$([
			modalLoadedContent = $('

')[0],
			modalLoadingOverlay = $('
')[0],
			modalClose = $('')[0]
		])
	);
	$(modalClose).click(function(){
		closeModal();
		return false;
	});
	$(document).bind('keydown', function(e){if(e.keyCode == 27){closeModal();}});

	$(modal).css("opacity", 0).show();
	interfaceHeight = $(borderTopCenter).height()+$(borderBottomCenter).height();
	interfaceWidth = $(borderMiddleLeft).width()+$(borderMiddleRight).width();
	$(borderMiddleLeft).css({top:$(borderTopLeft).height()});
	$(borderMiddleRight).css({top:$(borderTopRight).height()});
	$(borderTopCenter).css({left:$(borderTopLeft).width()});
	$(borderBottomCenter).css({left:$(borderBottomLeft).width()});
	$(modalContent).css({top:$(borderTopLeft).height(), left:$(borderTopLeft).width()});
	$(modal).hide().css("opacity", 1);
});

$.fn.colorbox = function(settings) {

	settings = $.extend({}, $.fn.colorbox.settings, settings);

	//sets the position of the modal on screen.  A transition speed of 0 will result in no animation.
	function modalPosition(modalWidth, modalHeight, transitionSpeed, callback){
		var winHeight = windowHeight();
		var posTop = winHeight/2 - modalHeight/2 + $(window).scrollTop();
		var posLeft = $(window).width()/2 - modalWidth/2 + $(window).scrollLeft();

		if(modalHeight > winHeight){posTop -=(modalHeight - winHeight);}
		if(posTop < 0){posTop = 0;} //keeps the box from expanding to an inaccessible area offscreen.
		if(posLeft < 0){posLeft = 0;}
		posTop = 0;
		posLeft = 0;
		//each part is animated seperately to keep them from disappearing during the animation process, which is what would happen if they were positioned relative to a single element being animated.
		$(modal).animate({top:posTop, left:posLeft, height:modalHeight, width:modalWidth}, transitionSpeed);
		$([borderMiddleLeft, borderMiddleRight]).animate({height:modalHeight-interfaceHeight}, transitionSpeed);
		$([borderTopCenter, borderBottomCenter]).animate({width:modalWidth-interfaceWidth}, transitionSpeed);
		$(modalContent).animate({height:modalHeight-interfaceHeight, width:modalWidth-interfaceWidth}, transitionSpeed, function(){
			if(callback){callback();}
			$(document).bind('keydown', keypressEvents);
			if($.browser.msie && $.browser.version < 7){setModalOverlay();}
		});
	}

	var preloads = [];

	function preload(){
		if(settings.preloading !== false && related.length>1){
			var previous, next;
			previous = index > 0 ? related[index-1].href : related[related.length-1].href;
			next = index < related.length-1 ? related[index+1].href : related[0].href;
			return [$("").attr("src", next), $("").attr("src", previous)];
		}
	}

	function centerModal(contentHtml, contentInfo){
		$(modalLoadedContent).hide().html(contentHtml).append(contentInfo);

		if(settings.fixedWidth){$(modalLoadedContent).css({"width":settings.fixedWidth - interfaceWidth}); }
		if(settings.fixedHeight){$(modalLoadedContent).css({"height":settings.fixedHeight - interfaceHeight});}

		if (settings.transition == "elastic") {
			modalPosition($(modalLoadedContent).outerWidth(true)+interfaceWidth, $(modalLoadedContent).outerHeight(true)+interfaceHeight, settings.transitionSpeed, function(){
				$(modalLoadedContent).show();
				$(modalLoadingOverlay).hide();
			});
		}
		else {
			$(modal).animate({"opacity":0}, settings.transitionSpeed, function(){
				modalPosition($(modalLoadedContent).outerWidth(true)+interfaceWidth, $(modalLoadedContent).outerHeight(true)+interfaceHeight, 0, function(){
					$(modalLoadedContent).show();
					$(modalLoadingOverlay).hide();
					$(modal).animate({"opacity":1}, settings.transitionSpeed);
				});
			});
		}
		var preloads = preload();
	}

	function buildGallery(that){

		var contentInfo = ""+that.title+"";

		if(related.length>1){
			contentInfo += " " + settings.contentCurrent + "";
			contentInfo = contentInfo.replace(/\{current\}/, index+1).replace(/\{total\}/, related.length);
			contentInfo += ""+settings.contentPrevious+" ";
			contentInfo += ""+settings.contentNext+" ";
		}

		if (settings.contentInline) {
			centerModal($(settings.contentInline).html(), contentInfo);
		} else if (settings.contentIframe) {
			centerModal("", contentInfo);
		} else if (that.href.match(/.(gif|png|jpg|jpeg|bmp|tif)$/i) && !settings.contentAjax){
			loadingElement = $("").load(function(){
				centerModal("", contentInfo);
			}).attr("src",that.href);
		}else {
			loadingElement = $('

').load(((settings.contentAjax) ? settings.contentAjax : that.href), function(data, textStatus){
				if(textStatus == "success"){
					centerModal($(this).html(), contentInfo);
				} else {
					centerModal("
Ajax request unsuccessful
");
				}
			});
		}
	}

	function contentNav(){
		$(modalLoadingOverlay).show();
		if($(this).attr("id") == "contentPrevious"){
			index = index > 0 ? index-1 : related.length-1;
		} else {
			index = index < related.length-1 ? index+1 : 0;
		}
		buildGallery(related[index]);
		return false;
	}

	$(this).bind("click.colorbox", function () {
		if ($(modal).data("open") !== true) {
			$(modal).data("open", true);
			$(modalLoadedContent).empty().css({
				"height": "auto",
				"width": "auto"
			});
			$(modalClose).html(settings.modalClose);
			$(modalOverlay).css({"opacity": settings.bgOpacity});

			$([modal, modalLoadingOverlay, modalOverlay]).show();

			if(settings.fixedWidth){ settings.fixedWidth = setSize(settings.fixedWidth, windowWidth());}
			if(settings.fixedHeight){ settings.fixedHeight = setSize(settings.fixedHeight, windowHeight());}

			//modalPosition(setSize(settings.initialWidth, windowWidth()), setSize(settings.initialHeight, windowHeight()), 0);
			modalPosition(200, 200, 0);

			if (this.rel) {
				related = $("a[rel='" + this.rel + "']");
				index = $(related).index(this);
			}
			else {
				related = $(this);
				index = 0;
			}
			buildGallery(related[index]);
			$("a#contentPrevious, a#contentNext").die().live("click", contentNav);
			$(document).bind('keydown', keypressEvents);
			if ($.browser.msie && $.browser.version < 7) {
				$(window).bind("resize scroll", setModalOverlay);
			}
		}
		return false;
	});

	if(settings.open!==false && $(modal).data("open")!==true){
		$(this).triggerHandler('click.colorbox');
	}

	return this.each(function() {
	});
};

/*
	ColorBox Default Settings.

	The colorbox() function takes one argument, an object of key/value pairs, that are used to initialize the modal.

	Please do not change these settings here, instead overwrite these settings when attaching the colorbox() event to your anchors.
	Example (Global)	: $.fn.colorbox.settings.transition = "fade"; //changes the transition to fade for all colorBox() events proceeding it's declaration.
	Example (Specific)	: $("a[href='http://www.google.com']").colorbox({fixedWidth:"700px", fixedHeight:"450px", contentIframe:true});
*/
$.fn.colorbox.settings = {
	transition : "elastic", // "elastic" or "fade". Set transitionSpeed to 0 for no transition.
	transitionSpeed : 250, // Sets the speed of the fade and elastic transition, in milliseconds. Set to 0 for no transition.
	initialWidth : "500", // Set the initial width of the modal, prior to any content being loaded.
	initialHeight : "500", // Set the initial height of the modal, prior to any content being loaded.
	fixedWidth : false, // Set a fixed width for div#modalLoadedContent.  Example: "500px"
	fixedHeight : false, // Set a fixed height for div#modalLoadedContent.  Example: "500px"
	contentAjax : false, // Set this to the file, or file+selector of content that will be loaded through an external file.  Example "include.html" or "company.inc.php div#ceo_bio"
	contentInline : false, // Set this to the selector, in jQuery selector format, of inline content to be displayed.  Example "#myHiddenDiv".
	contentIframe : false, // If 'true' specifies that content should be displayed in an iFrame.
	bgOpacity : 0.85, // The modalBackgroundOverlay opacity level. Range: 0 to 1.
	preloading : true, // Allows for preloading of 'Next' and 'Previous' content in a shared relation group (same values for the 'rel' attribute), after the current content has finished loading.  Set to 'false' to disable.
	contentCurrent : "{current} of {total}", // the format of the contentCurrent information
	contentPrevious : "previous", // the anchor text for the previous link in a shared relation group (same values for 'rel').
	contentNext : "next", // the anchor text for the next link in a shared relation group (same 'rel' attribute').
	modalClose : "close", // the anchor text for the close link.  Esc will also close the modal.
	open : false //Automatically opens ColorBox. (fires the click.colorbox event without waiting for user input).
};

})(jQuery);

<|endoftext|>


<commit_before>var onVideoFail = function(e) {
  if(e == 'NO_DEVICES_FOUND') {
      $('#no-webcam').show();
  }
  else {
    console.log('Failed to get camera access!', e);
  }
};

// Returns true if we are capable of video capture (regardless of whether the
// user has given permission).
function initVideoCapture() {
  window.URL = window.URL || window.webkitURL;
  navigator.getUserMedia  = navigator.getUserMedia || navigator.webkitGetUserMedia ||
                            navigator.mozGetUserMedia || navigator.msGetUserMedia;
  return !(navigator.getUserMedia == undefined);
}

var submitToPaymentProcessing = function() {
  var contribution_input = $("input[name='contribution']:checked")
  var contribution = 0;
  if(contribution_input.attr('id') == 'contribution-other')
  {
      contribution = $("input[name='contribution-other-amt']").val();
  }
  else
  {
      contribution = contribution_input.val();
  }
  var course_id = $("input[name='course_id']").val();
  var xhr = $.post(
    "/verify_student/create_order",
    {
      "course_id" : course_id,
      "contribution": contribution,
      "face_image" : $("#face_image")[0].src,
      "photo_id_image" : $("#photo_id_image")[0].src
    },
    function(data) {
      for (prop in data) {
        $('<input>').attr({
            type: 'hidden',
            name: prop,
            value: data[prop]
        }).appendTo('#pay_form');
      }
    }
  )
  .done(function(data) {
    $("#pay_form").submit();
  })
  .fail(function(jqXhr,text_status, error_thrown) {
    alert(jqXhr.responseText);
  });
}

function doResetButton(resetButton, captureButton, approveButton, nextButtonNav, nextLink) {
  approveButton.removeClass('approved');
  nextButtonNav.addClass('is-not-ready');
  nextLink.attr('href', "#");

  captureButton.show();
  resetButton.hide();
  approveButton.hide();
}

function doApproveButton(approveButton, nextButtonNav, nextLink) {
  nextButtonNav.removeClass('is-not-ready');
  approveButton.addClass('approved');
  nextLink.attr('href', "#next");
}

function doSnapshotButton(captureButton, resetButton, approveButton) {
  captureButton.hide();
  resetButton.show();
  approveButton.show();
}

function submitNameChange(event) {
  event.preventDefault();
  $("#lean_overlay").fadeOut(200);
  $("#edit-name").css({ 'display' : 'none' });
  var full_name = $('input[name="name"]').val();
  var xhr = $.post(
    "/change_name",
    {
      "new_name" : full_name,
      "rationale": "Want to match ID for ID Verified Certificates."
    },
    function(data) {
        $('#full-name').html(full_name);
    }
  )
  .fail(function(jqXhr,text_status, error_thrown) {
    $('.message-copy').html(jqXhr.responseText);
  });

}

function initSnapshotHandler(names, hasHtml5CameraSupport) {
  var name = names.pop();
  if (name == undefined) {
    return;
  }

  var video = $('#' + name + '_video');
  var canvas = $('#' + name + '_canvas');
  var image = $('#' + name + "_image");
  var captureButton = $("#" + name + "_capture_button");
  var resetButton = $("#" + name + "_reset_button");
  var approveButton = $("#" + name + "_approve_button");
  var nextButtonNav = $("#" + name + "_next_button_nav");
  var nextLink = $("#" + name + "_next_link");
  var flashCapture = $("#" + name + "_flash");

  var ctx = null;
  if (hasHtml5CameraSupport) {
    ctx = canvas[0].getContext('2d');
  }

  var localMediaStream = null;

  function snapshot(event) {
    if (hasHtml5CameraSupport) {
      if (localMediaStream) {
        ctx.drawImage(video[0], 0, 0);
        image[0].src = canvas[0].toDataURL('image/png');
      }
      else {
        return false;
      }
      video[0].pause();
    }
    else {
      if (flashCapture[0].cameraAuthorized()) {
        image[0].src = flashCapture[0].snap();
      }
      else {
        return false;
      }
    }

    doSnapshotButton(captureButton, resetButton, approveButton);
    return false;
  }

  function reset() {
    image[0].src = "";

    if (hasHtml5CameraSupport) {
      video[0].play();
    }
    else {
      flashCapture[0].reset();
    }

    doResetButton(resetButton, captureButton, approveButton, nextButtonNav, nextLink);
    return false;
  }

  function approve() {
    doApproveButton(approveButton, nextButtonNav, nextLink)
    return false;
  }

  // Initialize state for this picture taker
  captureButton.show();
  resetButton.hide();
  approveButton.hide();
  nextButtonNav.addClass('is-not-ready');
  nextLink.attr('href', "#");

  // Connect event handlers...
  video.click(snapshot);
  captureButton.click(snapshot);
  resetButton.click(reset);
  approveButton.click(approve);

  // If it's flash-based, we can just immediate initialize the next one.
  // If it's HTML5 based, we have to do it in the callback from getUserMedia
  // so that Firefox doesn't eat the second request.
  if (hasHtml5CameraSupport) {
    navigator.getUserMedia({video: true}, function(stream) {
      video[0].src = window.URL.createObjectURL(stream);
      localMediaStream = stream;

      // We do this in a recursive call on success because Firefox seems to
      // simply eat the request if you stack up two on top of each other before
      // the user has a chance to approve the first one.
      initSnapshotHandler(names, hasHtml5CameraSupport);
    }, onVideoFail);
  }
  else {
    initSnapshotHandler(names, hasHtml5CameraSupport);
  }

}

function browserHasFlash() {
  var hasFlash = false;
  try {
      var fo = new ActiveXObject('ShockwaveFlash.ShockwaveFlash');
      if(fo) hasFlash = true;
  } catch(e) {
      if(navigator.mimeTypes["application/x-shockwave-flash"] != undefined) hasFlash = true;
  }
  return hasFlash;
}

function objectTagForFlashCamera(name) {
  // detect whether or not flash is available
  if(browserHasFlash()) {
      // I manually update this to have ?v={2,3,4, etc} to avoid caching of flash
      // objects on local dev.
      return '<object type="application/x-shockwave-flash" id="' +
             name + '" name="' + name + '" data=' +
             '"/static/js/verify_student/CameraCapture.swf?v=3"' +
              'width="500" height="375"><param name="quality" ' +
              'value="high"><param name="allowscriptaccess" ' +
              'value="sameDomain"></object>';
  }
  else {
      // display a message informing the user to install flash
      $('#no-flash').show();
  }
}

function linkNewWindow(e) {
        window.open($(e.target).attr('href'));
            e.preventDefault();
}

$(document).ready(function() {
  $(".carousel-nav").addClass('sr');
  $("#pay_button").click(function(){
      analytics.pageview("Payment Form");
      submitToPaymentProcessing();
  });

  // prevent browsers from keeping this button checked
  $("#confirm_pics_good").prop("checked", false)
  $("#confirm_pics_good").change(function() {
      $("#pay_button").toggleClass('disabled');
  });

  // add in handlers to add/remove the correct classes to the body
  // when moving between steps
  $('#face_next_link').click(function(){
      analytics.pageview("Capture ID Photo");
      $('body').addClass('step-photos-id').removeClass('step-photos-cam')
  })

  $('#photo_id_next_link').click(function(){
      analytics.pageview("Review Photos");
      $('body').addClass('step-review').removeClass('step-photos-id')
  })

  // set up edit information dialog
  $('#edit-name div[role="alert"]').hide();
  $('#edit-name .action-save').click(submitNameChange);

  var hasHtml5CameraSupport = initVideoCapture();

  // If HTML5 WebRTC capture is not supported, we initialize jpegcam
  if (!hasHtml5CameraSupport) {
    $("#face_capture_div").html(objectTagForFlashCamera("face_flash"));
    $("#photo_id_capture_div").html(objectTagForFlashCamera("photo_id_flash"));
    // wait for the flash object to be loaded
    // TODO: we need a better solution for this
    setTimeout(function() {
        if(browserHasFlash() && !$('#face_flash')[0].hasOwnProperty('hasCamera')) {
            onVideoFail('NO_DEVICES_FOUND');
        }
    }, 1000);
  }

  analytics.pageview("Capture Face Photo");
  initSnapshotHandler(["photo_id", "face"], hasHtml5CameraSupport);

  $('a[rel="external"]').attr('title', gettext('This link will open in a new browser window/tab')).bind('click', linkNewWindow);

});
<commit_msg>Wait until the flash object is loaded before looking for a camera.<commit_after>var onVideoFail = function(e) {
  if(e == 'NO_DEVICES_FOUND') {
      $('#no-webcam').show();
  }
  else {
    console.log('Failed to get camera access!', e);
  }
};

// Returns true if we are capable of video capture (regardless of whether the
// user has given permission).
function initVideoCapture() {
  window.URL = window.URL || window.webkitURL;
  navigator.getUserMedia  = navigator.getUserMedia || navigator.webkitGetUserMedia ||
                            navigator.mozGetUserMedia || navigator.msGetUserMedia;
  return !(navigator.getUserMedia == undefined);
}

var submitToPaymentProcessing = function() {
  var contribution_input = $("input[name='contribution']:checked")
  var contribution = 0;
  if(contribution_input.attr('id') == 'contribution-other')
  {
      contribution = $("input[name='contribution-other-amt']").val();
  }
  else
  {
      contribution = contribution_input.val();
  }
  var course_id = $("input[name='course_id']").val();
  var xhr = $.post(
    "/verify_student/create_order",
    {
      "course_id" : course_id,
      "contribution": contribution,
      "face_image" : $("#face_image")[0].src,
      "photo_id_image" : $("#photo_id_image")[0].src
    },
    function(data) {
      for (prop in data) {
        $('<input>').attr({
            type: 'hidden',
            name: prop,
            value: data[prop]
        }).appendTo('#pay_form');
      }
    }
  )
  .done(function(data) {
    $("#pay_form").submit();
  })
  .fail(function(jqXhr,text_status, error_thrown) {
    alert(jqXhr.responseText);
  });
}

function doResetButton(resetButton, captureButton, approveButton, nextButtonNav, nextLink) {
  approveButton.removeClass('approved');
  nextButtonNav.addClass('is-not-ready');
  nextLink.attr('href', "#");

  captureButton.show();
  resetButton.hide();
  approveButton.hide();
}

function doApproveButton(approveButton, nextButtonNav, nextLink) {
  nextButtonNav.removeClass('is-not-ready');
  approveButton.addClass('approved');
  nextLink.attr('href', "#next");
}

function doSnapshotButton(captureButton, resetButton, approveButton) {
  captureButton.hide();
  resetButton.show();
  approveButton.show();
}

function submitNameChange(event) {
  event.preventDefault();
  $("#lean_overlay").fadeOut(200);
  $("#edit-name").css({ 'display' : 'none' });
  var full_name = $('input[name="name"]').val();
  var xhr = $.post(
    "/change_name",
    {
      "new_name" : full_name,
      "rationale": "Want to match ID for ID Verified Certificates."
    },
    function(data) {
        $('#full-name').html(full_name);
    }
  )
  .fail(function(jqXhr,text_status, error_thrown) {
    $('.message-copy').html(jqXhr.responseText);
  });

}

function initSnapshotHandler(names, hasHtml5CameraSupport) {
  var name = names.pop();
  if (name == undefined) {
    return;
  }

  var video = $('#' + name + '_video');
  var canvas = $('#' + name + '_canvas');
  var image = $('#' + name + "_image");
  var captureButton = $("#" + name + "_capture_button");
  var resetButton = $("#" + name + "_reset_button");
  var approveButton = $("#" + name + "_approve_button");
  var nextButtonNav = $("#" + name + "_next_button_nav");
  var nextLink = $("#" + name + "_next_link");
  var flashCapture = $("#" + name + "_flash");

  var ctx = null;
  if (hasHtml5CameraSupport) {
    ctx = canvas[0].getContext('2d');
  }

  var localMediaStream = null;

  function snapshot(event) {
    if (hasHtml5CameraSupport) {
      if (localMediaStream) {
        ctx.drawImage(video[0], 0, 0);
        image[0].src = canvas[0].toDataURL('image/png');
      }
      else {
        return false;
      }
      video[0].pause();
    }
    else {
      if (flashCapture[0].cameraAuthorized()) {
        image[0].src = flashCapture[0].snap();
      }
      else {
        return false;
      }
    }

    doSnapshotButton(captureButton, resetButton, approveButton);
    return false;
  }

  function reset() {
    image[0].src = "";

    if (hasHtml5CameraSupport) {
      video[0].play();
    }
    else {
      flashCapture[0].reset();
    }

    doResetButton(resetButton, captureButton, approveButton, nextButtonNav, nextLink);
    return false;
  }

  function approve() {
    doApproveButton(approveButton, nextButtonNav, nextLink)
    return false;
  }

  // Initialize state for this picture taker
  captureButton.show();
  resetButton.hide();
  approveButton.hide();
  nextButtonNav.addClass('is-not-ready');
  nextLink.attr('href', "#");

  // Connect event handlers...
  video.click(snapshot);
  captureButton.click(snapshot);
  resetButton.click(reset);
  approveButton.click(approve);

  // If it's flash-based, we can just immediate initialize the next one.
  // If it's HTML5 based, we have to do it in the callback from getUserMedia
  // so that Firefox doesn't eat the second request.
  if (hasHtml5CameraSupport) {
    navigator.getUserMedia({video: true}, function(stream) {
      video[0].src = window.URL.createObjectURL(stream);
      localMediaStream = stream;

      // We do this in a recursive call on success because Firefox seems to
      // simply eat the request if you stack up two on top of each other before
      // the user has a chance to approve the first one.
      initSnapshotHandler(names, hasHtml5CameraSupport);
    }, onVideoFail);
  }
  else {
    initSnapshotHandler(names, hasHtml5CameraSupport);
  }

}

function browserHasFlash() {
  var hasFlash = false;
  try {
      var fo = new ActiveXObject('ShockwaveFlash.ShockwaveFlash');
      if(fo) hasFlash = true;
  } catch(e) {
      if(navigator.mimeTypes["application/x-shockwave-flash"] != undefined) hasFlash = true;
  }
  return hasFlash;
}

function objectTagForFlashCamera(name) {
  // detect whether or not flash is available
  if(browserHasFlash()) {
      // I manually update this to have ?v={2,3,4, etc} to avoid caching of flash
      // objects on local dev.
      return '<object type="application/x-shockwave-flash" id="' +
             name + '" name="' + name + '" data=' +
             '"/static/js/verify_student/CameraCapture.swf?v=3"' +
              'width="500" height="375"><param name="quality" ' +
              'value="high"><param name="allowscriptaccess" ' +
              'value="sameDomain"></object>';
  }
  else {
      // display a message informing the user to install flash
      $('#no-flash').show();
  }
}

function linkNewWindow(e) {
        window.open($(e.target).attr('href'));
            e.preventDefault();
}

function waitForFlashLoad(func, flash_object) {
    if(!flash_object.hasOwnProperty('percentLoaded') || flash_object.percentLoaded() < 100){
        setTimeout(function() {
            waitForFlashLoad(func, flash_object);
        },
        50);
    }
    else {
        func(flash_object);
    }
}

$(document).ready(function() {
  $(".carousel-nav").addClass('sr');
  $("#pay_button").click(function(){
      analytics.pageview("Payment Form");
      submitToPaymentProcessing();
  });

  // prevent browsers from keeping this button checked
  $("#confirm_pics_good").prop("checked", false)
  $("#confirm_pics_good").change(function() {
      $("#pay_button").toggleClass('disabled');
  });

  // add in handlers to add/remove the correct classes to the body
  // when moving between steps
  $('#face_next_link').click(function(){
      analytics.pageview("Capture ID Photo");
      $('body').addClass('step-photos-id').removeClass('step-photos-cam')
  })

  $('#photo_id_next_link').click(function(){
      analytics.pageview("Review Photos");
      $('body').addClass('step-review').removeClass('step-photos-id')
  })

  // set up edit information dialog
  $('#edit-name div[role="alert"]').hide();
  $('#edit-name .action-save').click(submitNameChange);

  var hasHtml5CameraSupport = initVideoCapture();

  // If HTML5 WebRTC capture is not supported, we initialize jpegcam
  if (!hasHtml5CameraSupport) {
    $("#face_capture_div").html(objectTagForFlashCamera("face_flash"));
    $("#photo_id_capture_div").html(objectTagForFlashCamera("photo_id_flash"));
    // wait for the flash object to be loaded and then check for a camera
    if(browserHasFlash()) {
        waitForFlashLoad(function(flash_object) {
            if(!flash_object.hasOwnProperty('hasCamera')){
                onVideoFail('NO_DEVICES_FOUND');
            }
        }, $('#face_flash')[0]);
    }
  }

  analytics.pageview("Capture Face Photo");
  initSnapshotHandler(["photo_id", "face"], hasHtml5CameraSupport);

  $('a[rel="external"]').attr('title', gettext('This link will open in a new browser window/tab')).bind('click', linkNewWindow);

});
<|endoftext|>


<commit_before>import { connect } from 'react-redux'
import { startDrag } from '../actions'

const mapDispatchToProps = (dispatch) => {
  return {
    handleMouseDown: (elementId, mouseEvent, elementPosition) => {
      dispatch(
        startDrag(
          elementId,
          { x: mouseEvent.clientX, y: mouseEvent.clientY },
          elementPosition
        )
      )
    }
  }
}

export default connect(null, mapDispatchToProps)
<commit_msg>Only start drag on left click<commit_after>import { connect } from 'react-redux'
import { startDrag } from '../actions'

const LEFT = 0

const mapDispatchToProps = (dispatch) => {
  return {
    handleMouseDown: (elementId, mouseEvent, elementPosition) => {
      if(mouseEvent.button != LEFT)
        return

      dispatch(
        startDrag(
          elementId,
          { x: mouseEvent.clientX, y: mouseEvent.clientY },
          elementPosition
        )
      )
    }
  }
}

export default connect(null, mapDispatchToProps)
<|endoftext|>