diff --git a/CRM/Utils/Array.php b/CRM/Utils/Array.php index c6e8cfb93a86ff2c9ef9d9ace3ba3378f1007b47..575c0e66b880594f254cbab8145633d0c5ebe319 100644 --- a/CRM/Utils/Array.php +++ b/CRM/Utils/Array.php @@ -635,5 +635,48 @@ class CRM_Utils_Array { } return $default; } + + /** + * Generate the Cartesian product of zero or more vectors + * + * @param array $dimensions list of dimensions to multiply; each key is a dimension name; each value is a vector + * @param array $template a base set of values included in every output + * @return array each item is a distinct combination of values from $dimensions + * + * For example, the product of + * { + * fg => {red, blue}, + * bg => {white, black} + * } + * would be + * { + * {fg => red, bg => white}, + * {fg => red, bg => black}, + * {fg => blue, bg => white}, + * {fg => blue, bg => black} + * } + */ + static function product($dimensions, $template = array()) { + if (empty($dimensions)) { + return array($template); + } + + foreach ($dimensions as $key => $value) { + $firstKey = $key; + $firstValues = $value; + break; + } + unset($dimensions[$key]); + + $results = array(); + foreach ($firstValues as $firstValue) { + foreach (self::product($dimensions, $template) as $result) { + $result[$firstKey] = $firstValue; + $results[] = $result; + } + } + + return $results; + } } diff --git a/js/crm.backbone.js b/js/crm.backbone.js index 87c1c5cbc3f0aa0df8e8ce680c570aa72e0522ea..4913abfbe9d114a4f328e01796ad8f36895ff50d 100644 --- a/js/crm.backbone.js +++ b/js/crm.backbone.js @@ -32,14 +32,14 @@ }; switch (method) { case 'read': - CRM.api(model.crmEntityName, 'get', model.toCrmCriteria(), apiOptions); + CRM.api(model.crmEntityName, model.toCrmAction('get'), model.toCrmCriteria(), apiOptions); break; // replace all entities matching "x.crmCriteria" with new entities in "x.models" case 'crm-replace': var params = this.toCrmCriteria(); params.version = 3; params.values = this.toJSON(); - CRM.api(model.crmEntityName, 'replace', params, apiOptions); + CRM.api(model.crmEntityName, model.toCrmAction('replace'), params, apiOptions); break; default: apiOptions.error({is_error: 1, error_message: "CRM.Backbone.sync(" + method + ") not implemented for collections"}); @@ -74,9 +74,9 @@ params.options || (params.options = {}); params.options.reload = 1; if (!model._isDuplicate) { - CRM.api(model.crmEntityName, 'create', params, apiOptions); + CRM.api(model.crmEntityName, model.toCrmAction('create'), params, apiOptions); } else { - CRM.api(model.crmEntityName, 'duplicate', params, apiOptions); + CRM.api(model.crmEntityName, model.toCrmAction('duplicate'), params, apiOptions); } break; case 'read': @@ -87,7 +87,7 @@ apiOptions.error({is_error: 1, error_message: 'Missing ID for ' + model.crmEntityName}); return; } - CRM.api(model.crmEntityName, apiAction, params, apiOptions); + CRM.api(model.crmEntityName, model.toCrmAction(apiAction), params, apiOptions); break; default: apiOptions.error({is_error: 1, error_message: "CRM.Backbone.sync(" + method + ") not implemented for models"}); @@ -116,6 +116,10 @@ // Defaults - if specified in ModelClass, preserve _.defaults(ModelClass.prototype, { crmEntityName: crmEntityName, + crmActions: {}, // map: string backboneActionName => string serverSideActionName + toCrmAction: function(action) { + return this.crmActions[action] ? this.crmActions[action] : action; + }, toCrmCriteria: function() { return (this.get('id')) ? {id: this.get('id')} : {}; }, @@ -308,6 +312,10 @@ // Defaults - if specified in CollectionClass, preserve _.defaults(CollectionClass.prototype, { crmEntityName: CollectionClass.prototype.model.prototype.crmEntityName, + crmActions: {}, // map: string backboneActionName => string serverSideActionName + toCrmAction: function(action) { + return this.crmActions[action] ? this.crmActions[action] : action; + }, toCrmCriteria: function() { return (this.crmCriteria) ? _.extend({}, this.crmCriteria) : {}; }, @@ -365,6 +373,9 @@ } else if (options.crmCriteria) { this.crmCriteria = options.crmCriteria; } + if (options.crmActions) { + this.crmActions = _.extend(this.crmActions, options.crmActions); + } if (origInit) { return origInit.apply(this, arguments); } diff --git a/tests/phpunit/CRM/Utils/ArrayTest.php b/tests/phpunit/CRM/Utils/ArrayTest.php index 8b2509f2fa6d4cc4e2b79df74c70e49f1a722d38..2d31965abc658b9f0374969b2294ae4972d33f6a 100644 --- a/tests/phpunit/CRM/Utils/ArrayTest.php +++ b/tests/phpunit/CRM/Utils/ArrayTest.php @@ -33,7 +33,7 @@ class CRM_Utils_ArrayTest extends CiviUnitTestCase { $inputs[] = array( 'lang' => 'en', 'msgid' => 'greeting', - 'familiar' => false, + 'familiar' => FALSE, 'value' => 'Hello' ); $inputs[] = array( @@ -54,7 +54,7 @@ class CRM_Utils_ArrayTest extends CiviUnitTestCase { $inputs[] = array( 'lang' => 'en', 'msgid' => 'greeting', - 'familiar' => true, + 'familiar' => TRUE, 'value' => 'Hey' ); @@ -82,4 +82,41 @@ class CRM_Utils_ArrayTest extends CiviUnitTestCase { $this->assertEquals($expected, CRM_Utils_Array::collect('catWord', $arr)); } + function testProduct0() { + $actual = CRM_Utils_Array::product( + array(), + array('base data' => 1) + ); + $this->assertEquals(array( + array('base data' => 1), + ), $actual); + } + + function testProduct1() { + $actual = CRM_Utils_Array::product( + array('dim1' => array('a', 'b')), + array('base data' => 1) + ); + $this->assertEquals(array( + array('base data' => 1, 'dim1' => 'a'), + array('base data' => 1, 'dim1' => 'b'), + ), $actual); + } + + function testProduct3() { + $actual = CRM_Utils_Array::product( + array('dim1' => array('a', 'b'), 'dim2' => array('alpha', 'beta'), 'dim3' => array('one', 'two')), + array('base data' => 1) + ); + $this->assertEquals(array( + array('base data' => 1, 'dim1' => 'a', 'dim2' => 'alpha', 'dim3' => 'one'), + array('base data' => 1, 'dim1' => 'a', 'dim2' => 'alpha', 'dim3' => 'two'), + array('base data' => 1, 'dim1' => 'a', 'dim2' => 'beta', 'dim3' => 'one'), + array('base data' => 1, 'dim1' => 'a', 'dim2' => 'beta', 'dim3' => 'two'), + array('base data' => 1, 'dim1' => 'b', 'dim2' => 'alpha', 'dim3' => 'one'), + array('base data' => 1, 'dim1' => 'b', 'dim2' => 'alpha', 'dim3' => 'two'), + array('base data' => 1, 'dim1' => 'b', 'dim2' => 'beta', 'dim3' => 'one'), + array('base data' => 1, 'dim1' => 'b', 'dim2' => 'beta', 'dim3' => 'two'), + ), $actual); + } }