Search code examples
apostrophe-cms

How to override method "self.list" from apostrophe-pieces properly


I'm trying to automatically filter a piece by a field that simulate the locale where you can find it. For doing this I've overriden the self.list method of my piece, and it works but I don't really know if I've done it in the best possible way.

I know that I can manage the different locales of my project with apostrophe-workflow and that also I can add a filter to my piece and filter with that, but neither of those alternatives solves my problem, because I want that automatically when the pieces modal list them, it only show the pieces that have a concret value in the field I'm talking about.

I show you the field declaration code.

{
  name: 'locale',
  label: 'Locale',
  type: 'string',
  contextual: true
}   

Also I show the override of the self.list method.

self.list = function(req, options, callback) {
        var cursor;
        var filters = options.filters || options;
        if (options.chooser) {
          cursor = self.find(req);
        } else {
          cursor = self.findForEditing(req);
        }
        if (options.format === 'allIds') {
          cursor.projection({ _id: 1 });
        } else {
          cursor.perPage(self.options.perPage || 10);
        }
        cursor.queryToFilters(filters);

        var results = {};

        return async.series({

        toCount: function(callback) {
            if (options.format === 'allIds') {
              return callback(null);
            }
            return cursor
            .toCount(function(err, count) {
                if (err) {
                  return callback(err);
                }
                results.total = count;
                results.totalPages = cursor.get('totalPages');
                return callback(null);
            });
        },

        populateFilters: function(callback) {
            if (options.format === 'allIds') {
              return callback(null);
            }

            // Populate manage view filters by the same technique used
            // for the `piecesFilters` option of `apostrophe-pieces-pages`

            var allowedFilters = options.chooser ? _.filter(self.filters, function(item) {
              return item.allowedInChooser !== false;
            }) : self.filters;
            results.filters = {
              options: [],
              q: filters.search,
              choices: {}
            };

            return async.eachSeries(allowedFilters, 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 _cursor = cursor.clone();
              // The default might not be good for our purposes. Set it to
              // `null`, which appropriate filters, like `trash`, understand to mean
              // "I am interested in things that are ignored by default and also live things"
              _cursor[filter.name](null);
              return _cursor.toChoices(filter.name, { legacyFilterChoices: true }, function(err, choices) {
                  if (err) {
                    return callback(err);
                  }
                  // Array of all filter objects allowed in this context:
                  //
                  // results.filters.options = [ { name: 'published', choices: [ ... usual ... ], def: ... } ]
                  //
                  // Single object with a property containing the PRESENT value of EACH filter:
                  //
                  // results.filters.choices = {
                  //   published: true
                  // }
                  var _filter = _.clone(filter);
                  results.filters.options.push(_.assign(_filter, { choices: choices }));
                  // These are the "choices you have made," not the "choices you can make."
                  results.filters.choices[_filter.name] = filters[_filter.name];
                  return callback(null);
              });
            }, callback);

        },

        toArray: function(callback) {
            return cursor
            .toArray(function(err, pieces) {
                if (err) {
                  return callback(err);
                }
                if (options.format === 'allIds') {
                  results.ids = _.pluck(pieces, '_id');
                  return callback(null);
                }
                results.skip = cursor.get('skip');
                results.limit = cursor.get('limit');
                results.page = cursor.get('page');
                for( var i = 0; i < pieces.length; i++){
                  if (typeof pieces[i].locale != 'undefined') {
                    if(pieces[i].locale != req.locale) {
                      pieces.splice(i, 1);
                      i--;
                    }
                    else {
                      if(!self.apos.permissions.can(req, 'admin')) {
                        if(pieces[i].userId != req.user._id) {
                          pieces.splice(i, 1);
                          i--;
                        }
                      }
                    }
                  }
                }
                results.pieces = pieces;
                return callback(null);
            });
        }

        }, function(err) {
        if (err) {
            return callback(err);
        }
        // Helps the frontend display the active sort and filter states
        results.cursor = cursor;
        return callback(null, results);
        });
      };

As you can see, I have only overriden the toArray function. I have two principal doubts with this.

  1. It works with a little amount of pieces, but I don't know if it's going to work properly with big amount of them.

  2. Is there any way to override only the toArray function without having to override all the self.list method?

Thanks in advance!


Solution

  • First, I should note that apostrophe-workflow does do exactly this — it will only fetch docs with the appropriate workflowLocale. So I am not sure you are turning down the use of that module for the right reasons. I would recommend that you give it a serious test drive first and make sure you understand what it already provides.

    Moving on to the nuts and bolts of your question though, overriding the entire list method is definitely the least forwards-compatible way to do it and does not address what users might see on the front end.

    Also, filtering after the toArray call like this will break pagination. The frontend will ask for page 1, and a page's worth will be queried, and only half of them turn out to be for the locale, so you get half a page.

    What you might do instead is borrow from apostrophe-workflow, which says this (simplified for your use case):

    // in lib/modules/YOUR-PIECES-MODULE/lib/cursor.js
    module.exports = {
      construct: function(self, options) {
        self.addFilter('workflowLocale', {
          def: true,
          finalize: function() {
            var setting = self.get('workflowLocale');
            if (setting === null) {
              return;
            }
            if (setting === true) {
              setting = self.get('req').locale;
            }
            self.and({ locale: setting });
          }
        }
      }
    });
    

    This code adds a new cursor filter method to the cursors for your piece type, such that it will automatically filter them by req.locale by default.

    Again though, this is part of what workflow does for you, so consider that too. Maybe you want the localization but don't want a commit process for new changes at all; that could be a good reason to do it this way.