From 1ec6b4d65f3c7dbfa99f63c0cd992c7f8227c38f Mon Sep 17 00:00:00 2001 From: Benn Oshrin Date: Mon, 2 Aug 2021 13:50:02 -0400 Subject: [PATCH] Implement Required (CO-2134) and Crosscheck (CO-1763) on RuleAttribute --- app/config/schema/schema.json | 9 +- app/src/Controller/ApiUsersController.php | 4 +- .../Controller/AttributeGroupsController.php | 4 +- .../AttributeMappingsController.php | 4 +- .../Controller/AttributeMapsController.php | 4 +- app/src/Controller/AttributesController.php | 4 +- .../Controller/MatchgridRecordsController.php | 4 +- .../MatchgridSettingsController.php | 4 +- app/src/Controller/MatchgridsController.php | 4 +- app/src/Controller/PermissionsController.php | 9 +- .../Controller/RuleAttributesController.php | 69 +++++++++++ app/src/Controller/RulesController.php | 4 +- app/src/Controller/StandardController.php | 32 ++++- .../Controller/SystemsOfRecordController.php | 4 +- app/src/Lib/Match/MatchService.php | 115 ++++++++++++------ app/src/Lib/Traits/QueryModificationTrait.php | 57 +++++++++ app/src/Locale/en_US/default.po | 10 +- app/src/Model/Table/AttributesTable.php | 11 +- app/src/Model/Table/RuleAttributesTable.php | 48 ++++++++ app/src/Model/Table/RulesTable.php | 4 - app/src/Template/Attributes/fields.inc | 1 - app/src/Template/RuleAttributes/columns.inc | 44 +++++++ app/src/Template/RuleAttributes/fields.inc | 34 ++++++ app/src/Template/Rules/columns.inc | 10 +- app/src/Template/Rules/fields.inc | 63 +--------- app/src/Template/Standard/add-edit-view.ctp | 2 +- app/src/Template/Standard/index.ctp | 44 +++++-- app/src/View/Helper/FieldHelper.php | 47 +++++-- 28 files changed, 484 insertions(+), 165 deletions(-) create mode 100644 app/src/Controller/RuleAttributesController.php create mode 100644 app/src/Lib/Traits/QueryModificationTrait.php create mode 100644 app/src/Template/RuleAttributes/columns.inc create mode 100644 app/src/Template/RuleAttributes/fields.inc diff --git a/app/config/schema/schema.json b/app/config/schema/schema.json index e05ca815b..1eccbb93a 100644 --- a/app/config/schema/schema.json +++ b/app/config/schema/schema.json @@ -142,7 +142,6 @@ "case_sensitive": { "type": "boolean" }, "invalidates": { "type": "boolean" }, "null_equivalents": { "type": "boolean" }, - "required": { "type": "boolean" }, "search_distance": { "type": "integer" }, "search_exact": { "type": "boolean" }, "search_substr_from": { "type": "integer" }, @@ -183,7 +182,9 @@ "id": {}, "rule_id": { "type": "integer", "foreignkey": { "table": "rules", "column": "id" } }, "attribute_id": { "type": "integer", "foreignkey": { "table": "attributes", "column": "id" } }, - "search_type": { "type": "string", "size": 2 } + "crosscheck_attribute_id": { "type": "integer", "foreignkey": { "table": "attributes", "column": "id" } }, + "search_type": { "type": "string", "size": 2 }, + "required": { "type": "boolean" } }, "indexes": { "rule_attributes_i1": { @@ -192,6 +193,10 @@ "rule_attributes_i2": { "comment": "We don't really need this index but DBAL will create it anyway, with a random name", "columns": [ "attribute_id" ] + }, + "rule_attributes_i3": { + "comment": "We don't really need this index but DBAL will create it anyway, with a random name", + "columns": [ "crosscheck_attribute_id" ] } }, "changelog": false diff --git a/app/src/Controller/ApiUsersController.php b/app/src/Controller/ApiUsersController.php index 0554915f5..906565d81 100644 --- a/app/src/Controller/ApiUsersController.php +++ b/app/src/Controller/ApiUsersController.php @@ -19,7 +19,7 @@ * See the License for the specific language governing permissions and * limitations under the License. * - * @link http://www.internet2.edu/comanage COmanage Project + * @link https://www.internet2.edu/comanage COmanage Project * @package match * @since COmanage Match v1.0.0 * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) @@ -32,7 +32,7 @@ use Cake\Routing\Router; class ApiUsersController extends StandardController { - public $paginate = [ + public $pagination = [ 'order' => [ 'ApiUsers.username' => 'asc' ] diff --git a/app/src/Controller/AttributeGroupsController.php b/app/src/Controller/AttributeGroupsController.php index ebc3ea5ab..c0ec1f0d8 100644 --- a/app/src/Controller/AttributeGroupsController.php +++ b/app/src/Controller/AttributeGroupsController.php @@ -19,7 +19,7 @@ * See the License for the specific language governing permissions and * limitations under the License. * - * @link http://www.internet2.edu/comanage COmanage Project + * @link https://www.internet2.edu/comanage COmanage Project * @package match * @since COmanage Match v1.0.0 * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) @@ -30,7 +30,7 @@ namespace App\Controller; class AttributeGroupsController extends StandardController { - public $paginate = [ + public $pagination = [ 'order' => [ 'AttributeGroups.name' => 'asc' ] diff --git a/app/src/Controller/AttributeMappingsController.php b/app/src/Controller/AttributeMappingsController.php index 392f4ab14..808d7d968 100644 --- a/app/src/Controller/AttributeMappingsController.php +++ b/app/src/Controller/AttributeMappingsController.php @@ -19,7 +19,7 @@ * See the License for the specific language governing permissions and * limitations under the License. * - * @link http://www.internet2.edu/comanage COmanage Project + * @link https://www.internet2.edu/comanage COmanage Project * @package match * @since COmanage Match v1.0.0 * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) @@ -30,7 +30,7 @@ namespace App\Controller; class AttributeMappingsController extends StandardController { - public $paginate = [ + public $pagination = [ 'order' => [ 'AttributeMappings.query' => 'asc', 'AttributeMappings.value' => 'asc' diff --git a/app/src/Controller/AttributeMapsController.php b/app/src/Controller/AttributeMapsController.php index b13bd9ddf..f8d6709cc 100644 --- a/app/src/Controller/AttributeMapsController.php +++ b/app/src/Controller/AttributeMapsController.php @@ -19,7 +19,7 @@ * See the License for the specific language governing permissions and * limitations under the License. * - * @link http://www.internet2.edu/comanage COmanage Project + * @link https://www.internet2.edu/comanage COmanage Project * @package match * @since COmanage Match v1.0.0 * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) @@ -30,7 +30,7 @@ namespace App\Controller; class AttributeMapsController extends StandardController { - public $paginate = [ + public $pagination = [ 'order' => [ 'AttributeMaps.name' => 'asc' ] diff --git a/app/src/Controller/AttributesController.php b/app/src/Controller/AttributesController.php index 8352592a0..fda0d1cf6 100644 --- a/app/src/Controller/AttributesController.php +++ b/app/src/Controller/AttributesController.php @@ -19,7 +19,7 @@ * See the License for the specific language governing permissions and * limitations under the License. * - * @link http://www.internet2.edu/comanage COmanage Project + * @link https://www.internet2.edu/comanage COmanage Project * @package match * @since COmanage Match v1.0.0 * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) @@ -30,7 +30,7 @@ namespace App\Controller; class AttributesController extends StandardController { - public $paginate = [ + public $pagination = [ 'order' => [ 'Attributes.name' => 'asc' ] diff --git a/app/src/Controller/MatchgridRecordsController.php b/app/src/Controller/MatchgridRecordsController.php index 7a5734cfe..023c2d9f1 100644 --- a/app/src/Controller/MatchgridRecordsController.php +++ b/app/src/Controller/MatchgridRecordsController.php @@ -19,7 +19,7 @@ * See the License for the specific language governing permissions and * limitations under the License. * - * @link http://www.internet2.edu/comanage COmanage Project + * @link https://www.internet2.edu/comanage COmanage Project * @package match * @since COmanage Match v1.0.0 * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) @@ -38,7 +38,7 @@ use \App\Lib\Match\MatchService; class MatchgridRecordsController extends StandardController { - public $paginate = [ + public $pagination = [ 'order' => [ 'sor' => 'asc', 'sorid' => 'asc', diff --git a/app/src/Controller/MatchgridSettingsController.php b/app/src/Controller/MatchgridSettingsController.php index e8a9cc0da..c1fadc118 100644 --- a/app/src/Controller/MatchgridSettingsController.php +++ b/app/src/Controller/MatchgridSettingsController.php @@ -19,7 +19,7 @@ * See the License for the specific language governing permissions and * limitations under the License. * - * @link http://www.internet2.edu/comanage COmanage Project + * @link https://www.internet2.edu/comanage COmanage Project * @package match * @since COmanage Match v1.0.0 * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) @@ -30,7 +30,7 @@ namespace App\Controller; class MatchgridSettingsController extends StandardController { - public $paginate = [ + public $pagination = [ 'order' => [ 'MatchgridSetting.matchgrid_id' => 'asc' ] diff --git a/app/src/Controller/MatchgridsController.php b/app/src/Controller/MatchgridsController.php index cf173f614..be9faadab 100644 --- a/app/src/Controller/MatchgridsController.php +++ b/app/src/Controller/MatchgridsController.php @@ -19,7 +19,7 @@ * See the License for the specific language governing permissions and * limitations under the License. * - * @link http://www.internet2.edu/comanage COmanage Project + * @link https://www.internet2.edu/comanage COmanage Project * @package match * @since COmanage Match v1.0.0 * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) @@ -33,7 +33,7 @@ use \App\Lib\Enum\PermissionEnum; class MatchgridsController extends StandardController { - public $paginate = [ + public $pagination = [ 'order' => [ 'Matchgrids.table_name' => 'asc' ] diff --git a/app/src/Controller/PermissionsController.php b/app/src/Controller/PermissionsController.php index fe7dc21e1..1b0fc8938 100644 --- a/app/src/Controller/PermissionsController.php +++ b/app/src/Controller/PermissionsController.php @@ -19,7 +19,7 @@ * See the License for the specific language governing permissions and * limitations under the License. * - * @link http://www.internet2.edu/comanage COmanage Project + * @link https://www.internet2.edu/comanage COmanage Project * @package match * @since COmanage Match v1.0.0 * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) @@ -30,10 +30,15 @@ namespace App\Controller; class PermissionsController extends StandardController { - public $paginate = [ + public $pagination = [ 'order' => [ 'Matchgrids.table_name' => 'asc', 'Permissions.name' => 'asc' + ], +// 'sortWhitelist' is renamed 'sortableFields' in Cake 4.1 +// 'sortableFields' => [ + 'sortWhitelist' => [ + 'Matchgrids.table_name' ] ]; diff --git a/app/src/Controller/RuleAttributesController.php b/app/src/Controller/RuleAttributesController.php new file mode 100644 index 000000000..cbe510e9f --- /dev/null +++ b/app/src/Controller/RuleAttributesController.php @@ -0,0 +1,69 @@ + [ + 'Attributes.name' => 'asc' + ], +// 'sortWhitelist' is renamed 'sortableFields' in Cake 4.1 +// 'sortableFields' => [ + 'sortWhitelist' => [ + 'Attributes.name' + ] + ]; + + /** + * Authorization for this Controller, called by Auth component + * - postcondition: $vv_permissions set with calculated permissions for this Controller + * + * @since COmanage Match v1.0.0 + * @param Array $user Array of user data + * @return Boolean True if authorized for the current action, false otherwise + */ + + public function isAuthorized(Array $user) { + $platformAdmin = $this->Authorization->isPlatformAdmin($user['username']); + + $mgAdmin = $this->Authorization->isMatchAdmin($user['username'], $this->cur_mg->id); + + $p = [ + 'add' => $platformAdmin || $mgAdmin, + 'delete' => $platformAdmin || $mgAdmin, + 'edit' => $platformAdmin || $mgAdmin, + 'index' => $platformAdmin || $mgAdmin, + 'view' => false + ]; + + $this->set('vv_permissions', $p); + return $p[$this->request->getParam('action')]; + } +} \ No newline at end of file diff --git a/app/src/Controller/RulesController.php b/app/src/Controller/RulesController.php index cbbaa172b..992be6f86 100644 --- a/app/src/Controller/RulesController.php +++ b/app/src/Controller/RulesController.php @@ -19,7 +19,7 @@ * See the License for the specific language governing permissions and * limitations under the License. * - * @link http://www.internet2.edu/comanage COmanage Project + * @link https://www.internet2.edu/comanage COmanage Project * @package match * @since COmanage Match v1.0.0 * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) @@ -30,7 +30,7 @@ namespace App\Controller; class RulesController extends StandardController { - public $paginate = [ + public $pagination = [ 'order' => [ 'Rules.confidence_mode' => 'asc', 'Rules.ordr' => 'asc' diff --git a/app/src/Controller/StandardController.php b/app/src/Controller/StandardController.php index 364da8f6d..7448031dd 100644 --- a/app/src/Controller/StandardController.php +++ b/app/src/Controller/StandardController.php @@ -32,6 +32,9 @@ use InvalidArgumentException; class StandardController extends AppController { + // Pagination defaults should be set in each controller + public $pagination = []; + /** * Handle an add action for a Standard object. * @@ -223,8 +226,10 @@ public function generateRedirect() { public function index() { // $this->name = Models $modelsName = $this->name; + // $table = the actual table object + $table = $this->$modelsName; // $tableName = models - $tableName = $this->$modelsName->getTable(); + $tableName = $table->getTable(); $query = null; @@ -239,10 +244,10 @@ public function index() { if(!empty($link['linkattr']) && $modelsName != 'MatchgridRecords') { // If a link attribute is defined but no value is provided, then query // where the link attribute is NULL - $query = $this->$modelsName->find()->where([$link['linkattr'].' IS' => $this->request->getQuery($link['linkattr'])]); + $query = $table->find()->where([$link['linkattr'].' IS' => $this->request->getQuery($link['linkattr'])]); } else { try { - $query = $this->$modelsName->find(); + $query = $table->find(); } catch(\Cake\Database\Exception $e) { if($modelsName == 'MatchgridRecords' && $e->getCode() == 500) { @@ -257,7 +262,17 @@ public function index() { } } - $this->set($tableName, $this->Paginator->paginate($query, $this->paginate)); + // QueryModificationTrait + if(method_exists($table, "getIndexContains") + && $table->getIndexContains()) { + $query->contain($table->getIndexContains()); + } + + // The Cake documents describe $this->paginate (which worked in Cake 2), + // but it doesn't seem to work in Cake 3/4. So we just use $this->pagination + // ourselves here. + + $this->set($tableName, $this->Paginator->paginate($query, $this->pagination)); $this->set('vv_tablename', $tableName); $this->set('vv_modelname', $modelsName); @@ -333,9 +348,16 @@ protected function populateAutoViewVars(object $obj=null) { // XXX also need to check getData()? if($v) { - $query = $query->find($avv['find'], [$linkFilter => $v]); + $query = $query->where([$linkFilter => $v]); } } + } elseif($avv['find'] == 'filterMatchgrid') { + // In many/most cases, filterPrimaryLink is also filterMatchgrid, + // but for indirect models this will force the filter to be on + // the matchgrid instead of the primary link + + // For now we only support direct relations to matchgrid + $query->where(['matchgrid_id' => $this->cur_mg->id]); } else { // Use the specified finder, if configured $query = $query->find($avv['find']); diff --git a/app/src/Controller/SystemsOfRecordController.php b/app/src/Controller/SystemsOfRecordController.php index 9ccd0fc4a..c1023ab5a 100644 --- a/app/src/Controller/SystemsOfRecordController.php +++ b/app/src/Controller/SystemsOfRecordController.php @@ -19,7 +19,7 @@ * See the License for the specific language governing permissions and * limitations under the License. * - * @link http://www.internet2.edu/comanage COmanage Project + * @link https://www.internet2.edu/comanage COmanage Project * @package match * @since COmanage Match v1.0.0 * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) @@ -30,7 +30,7 @@ namespace App\Controller; class SystemsOfRecordController extends StandardController { - public $paginate = [ + public $pagination = [ 'order' => [ 'SystemsOfRecord.label' => 'asc' ] diff --git a/app/src/Lib/Match/MatchService.php b/app/src/Lib/Match/MatchService.php index e79af3661..d94ddc671 100644 --- a/app/src/Lib/Match/MatchService.php +++ b/app/src/Lib/Match/MatchService.php @@ -517,11 +517,17 @@ protected function search(string $mode, Log::write('debug', $sor . "/" . $sorid . " Searching with confidence mode " . $mode); foreach($this->mgConfig->$ruleObjs as $rule) { - $sql = "SELECT * - FROM " . $this->mgTable . " - WHERE referenceid IS NOT NULL"; // Don't match pending requests + // We generate SQL fragments for each rule attribute, then we OR together + // any SQL fragments for the same attribute (eg: if Crosscheck is in use, + // we want any one of them to succeed) and then AND together the SQL + // fragment across attributes (any attributes specified must match). - $vals = []; + // Each of 'sql' and 'vals' will hold an array of arrays, keyed on the + // attribute id + $attrSql = [ + 'sql' => [], + 'vals' => [] + ]; foreach($rule->rule_attributes as $ruleattr) { if($ruleattr->search_type == SearchTypeEnum::Skip) { @@ -532,29 +538,35 @@ protected function search(string $mode, $val = $attributes->getValueByAttribute($ruleattr->attribute); if(!$val) { - if($ruleattr->attribute->required) { - Log::write('debug', $sor . "/" . $sorid . " No value found for required attribute " . $ruleattr->attribute->name . " skipping rule " . $rule->name); + if($ruleattr->required) { + Log::write('debug', $sor . "/" . $sorid . " No value found for required attribute " . $ruleattr->attribute->name . ", skipping rule " . $rule->name); continue 2; } - Log::write('debug', $sor . "/" . $sorid . " No value found for attribute " . $ruleattr->attribute->name . " skipping"); + Log::write('debug', $sor . "/" . $sorid . " No value found for optional attribute " . $ruleattr->attribute->name . ", ignoring it"); continue; } - $andclause = ""; + // From here, we use the Crosscheck Attribute if it is specified instead + // of the original Attribute. In other words, Crosscheck allows a single + // attribute from the API message to map to multiple database columns. + + $attribute = $ruleattr->crosscheck_attribute ?: $ruleattr->attribute; + + $attrclause = ""; $colclause = ""; // The column name - $colclause = $ruleattr->attribute->name; + $colclause = $attribute->name; // If the attribute is case insensitive, we insert LOWER clauses - if(!$ruleattr->attribute->case_sensitive) { + if(!$attribute->case_sensitive) { $colclause = "LOWER(" . $colclause . ")"; $val = strtolower($val); } // If the attribute is alphanumeric only, we strip out non-alphanumeric characters - if($ruleattr->attribute->alphanumeric) { + if($attribute->alphanumeric) { $colclause = "REGEXP_REPLACE(" . $colclause . ", '[^A-Za-z0-9]', '', 'g')"; $val = preg_replace('/[^A-Za-z0-9]/', '', $val); } @@ -565,51 +577,53 @@ protected function search(string $mode, // how we handle all this switch($ruleattr->search_type) { case SearchTypeEnum::Distance: - $maxdistance = (int)($ruleattr->attribute->search_distance)+1; - $andclause = "LEVENSHTEIN_LESS_EQUAL(" - . $colclause - . ",?," - . $ruleattr->attribute->search_distance - . ") < " - . $maxdistance; + $maxdistance = (int)($attribute->search_distance)+1; + $attrclause = "LEVENSHTEIN_LESS_EQUAL(" + . $colclause + . ",?," + . $attribute->search_distance + . ") < " + . $maxdistance; break; case SearchTypeEnum::Exact: - $andclause = $colclause . "=?"; + $attrclause = $colclause . "=?"; break; case SearchTypeEnum::Mapping: - $qclause = (!$ruleattr->attribute->case_sensitive ? "LOWER(query)" : "query"); - $andclause = "(" . $colclause . " + $qclause = (!$attribute->case_sensitive ? "LOWER(query)" : "query"); + $attrclause = "(" . $colclause . " IN (SELECT value FROM attribute_mappings - WHERE attribute_map_id=" . $ruleattr->attribute->attribute_map_id ." + WHERE attribute_map_id=" . $attribute->attribute_map_id ." AND " . $qclause . "=?) OR " . $colclause . "=?)"; // We need two copies of $val in the param list - $vals[] = (!$ruleattr->attribute->case_sensitive ? strtolower($val) : $val); + $attrSql['vals'][$ruleattr->attribute->id][] = (!$attribute->case_sensitive ? strtolower($val) : $val); break; case SearchTypeEnum::Substring: - $andclause = "SUBSTRING(" - . $colclause - . " FROM " - . $ruleattr->attribute->search_substr_from - . " FOR " - . $ruleattr->attribute->search_substr_for - . ") = SUBSTRING(? FROM " - . $ruleattr->attribute->search_substr_from - . " FOR " - . $ruleattr->attribute->search_substr_for - . ")"; + $attrclause = "SUBSTRING(" + . $colclause + . " FROM " + . $attribute->search_substr_from + . " FOR " + . $attribute->search_substr_for + . ") = SUBSTRING(? FROM " + . $attribute->search_substr_from + . " FOR " + . $attribute->search_substr_for + . ")"; break; default: throw new LogicException(__('match.er.search_type', [$ruleattr->search_type])); break; } - $sql .= " AND " . $andclause; - $vals[] = $val; + // Note here we revert to using the original Attribute ID, since if + // multiple configurations are specified for it we want to OR them together + $attrSql['sql'][$ruleattr->attribute->id][] = $attrclause; + $attrSql['vals'][$ruleattr->attribute->id][] = $val; } - if(count($vals) == 0) { + if(empty($attrSql['vals'])) { // We need at least one attribute to search on. If we didn't process // any in the request, complain. @@ -617,6 +631,28 @@ protected function search(string $mode, continue; } + // Start building the actual SQL + + $sql = "SELECT * + FROM " . $this->mgTable . " + WHERE referenceid IS NOT NULL"; // Don't match pending requests + + $vals = []; + + foreach(array_keys($attrSql['sql']) as $attrId) { + if(count($attrSql['sql'][$attrId]) > 1) { + // We OR together all of the clauses + + $sql .= " AND (" . implode(" OR ", $attrSql['sql'][$attrId]) . ")"; + } else { + // We simply append the clause + + $sql .= " AND " . $attrSql['sql'][$attrId][0]; + } + + $vals = array_merge($vals, $attrSql['vals'][$attrId]); + } + LOG::write('debug', $sor . "/" . $sorid . " SQL: " . $sql); $stmt = $this->dbc->Prepare($sql); @@ -720,7 +756,10 @@ public function setConfig(int $matchgridId) { 'CanonicalRules' => [ // We already pull attributes above, but this makes it // easier to access them in search() - 'RuleAttributes' => ['Attributes' => 'AttributeGroups'], + 'RuleAttributes' => [ + 'Attributes' => 'AttributeGroups', + 'CrosscheckAttributes' => 'AttributeGroups' + ], 'sort' => ['CanonicalRules.ordr' => 'ASC'] ], 'PotentialRules' => [ diff --git a/app/src/Lib/Traits/QueryModificationTrait.php b/app/src/Lib/Traits/QueryModificationTrait.php new file mode 100644 index 000000000..4f1bc7017 --- /dev/null +++ b/app/src/Lib/Traits/QueryModificationTrait.php @@ -0,0 +1,57 @@ +indexContains; + } + + /** + * Set containable models for index actions. + * + * @since COmanage Match v1.0.0 + * @param array $contains Containable models + */ + + public function setIndexContains(array $contains) { + $this->indexContains = $contains; + } +} diff --git a/app/src/Locale/en_US/default.po b/app/src/Locale/en_US/default.po index c6efc1771..c928a6072 100644 --- a/app/src/Locale/en_US/default.po +++ b/app/src/Locale/en_US/default.po @@ -135,6 +135,9 @@ msgstr "{0,plural,=1{System of Record} other{Systems of Record}}" msgid "match.ct.Rules" msgstr "{0,plural,=1{Rule} other{Rules}}" +msgid "match.ct.RuleAttributes" +msgstr "{0,plural,=1{Rule Attribute} other{Rule Attributes}}" + ### Actions msgid "match.ac.PendingRequests" msgstr "{0,plural,=1{Pending Request} other{Pending Requests}}" @@ -339,6 +342,9 @@ msgstr "Value must be a valid SQL identifier, as it will be used to construct th msgid "match.fd.MatchgridRecords.referenceid.desc" msgstr "Manually assigning a Reference Identifier is not recommended, except when forcing a match to an existing Matchgrid entry. For more information, see the documentation." +msgid "match.fd.RuleAttributes.crosscheck_attribute_id" +msgstr "Crosscheck Attribute" + msgid "match.fd.case_sensitive" msgstr "Case Sensitive" @@ -429,8 +435,8 @@ msgstr "Search Substring From" msgid "match.fd.search_substr_for" msgstr "Search Substring For" -msgid "match.fd.search_types" -msgstr "Search Types" +msgid "match.fd.search_type" +msgstr "Search Type" msgid "match.fd.select" msgstr "(Please select a value)" diff --git a/app/src/Model/Table/AttributesTable.php b/app/src/Model/Table/AttributesTable.php index ad9035024..534962bd0 100644 --- a/app/src/Model/Table/AttributesTable.php +++ b/app/src/Model/Table/AttributesTable.php @@ -52,9 +52,11 @@ public function initialize(array $config) { $this->belongsTo('AttributeGroups'); $this->belongsTo('AttributeMaps'); $this->belongsTo('Matchgrids'); - //$this->belongsToMany('Rules', ['through' => 'RuleAttributes']); $this->hasMany('RuleAttributes') ->setDependent(true); + $this->hasMany('CrosscheckAttributes', ['className' => 'RuleAttributes']) + ->setForeignKey('crosscheck_attribute_id') + ->setProperty('crosscheck_attribute'); $this->setDisplayField('name'); @@ -156,13 +158,6 @@ public function validationDefault(Validator $validator) { [ 'rule' => [ 'boolean' ] ] ); $validator->notEmpty('null_equivalents'); - - $validator->add( - 'required', - 'toggle', - [ 'rule' => [ 'boolean' ] ] - ); - $validator->notEmpty('required'); $validator->add( 'search_distance', diff --git a/app/src/Model/Table/RuleAttributesTable.php b/app/src/Model/Table/RuleAttributesTable.php index 68919eacb..26e9c6c60 100644 --- a/app/src/Model/Table/RuleAttributesTable.php +++ b/app/src/Model/Table/RuleAttributesTable.php @@ -35,6 +35,11 @@ use \App\Lib\Enum\SearchTypeEnum; class RuleAttributesTable extends Table { + use \App\Lib\Traits\AutoViewVarsTrait; + use \App\Lib\Traits\MatchgridLinkTrait; + use \App\Lib\Traits\PrimaryLinkTrait; + use \App\Lib\Traits\QueryModificationTrait; + /** * Perform Cake Model initialization. * @@ -47,7 +52,36 @@ public function initialize(array $config) { // Define associations $this->belongsTo('Attributes'); + + $this->belongsTo('CrosscheckAttributes', ['className' => 'Attributes']) + ->setForeignKey('crosscheck_attribute_id') + ->setProperty('crosscheck_attribute'); + $this->belongsTo('Rules'); + + $this->setDisplayField('id'); + + $this->setPrimaryLink('rule_id'); + $this->setRequiresMatchgrid(true); + + $this->setIndexContains(['Attributes']); + + $this->setAutoViewVars([ + 'attributes' => [ + 'type' => 'select', + 'model' => 'Attributes', + 'find' => 'filterMatchgrid' + ], + 'crosscheckAttributes' => [ + 'type' => 'select', + 'model' => 'Attributes', + 'find' => 'filterMatchgrid' + ], + 'searchTypes' => [ + 'type' => 'enum', + 'class' => 'SearchTypeEnum' + ] + ]); } /** @@ -73,6 +107,13 @@ public function validationDefault(Validator $validator) { ); $validator->notEmpty('attribute_id'); + $validator->add( + 'crosscheck_attribute_id', + 'content', + [ 'rule' => 'isInteger' ] + ); + $validator->allowEmpty('crosscheck_attribute_id'); + $validator->add( 'search_type', 'content', @@ -86,6 +127,13 @@ public function validationDefault(Validator $validator) { ); $validator->notEmpty('search_type'); + $validator->add( + 'required', + 'toggle', + [ 'rule' => [ 'boolean' ] ] + ); + $validator->notEmpty('required'); + return $validator; } } \ No newline at end of file diff --git a/app/src/Model/Table/RulesTable.php b/app/src/Model/Table/RulesTable.php index af212a246..e2f37a75c 100644 --- a/app/src/Model/Table/RulesTable.php +++ b/app/src/Model/Table/RulesTable.php @@ -74,10 +74,6 @@ public function initialize(array $config) { 'confidenceModes' => [ 'type' => 'enum', 'class' => 'ConfidenceModeEnum' - ], - 'searchTypes' => [ - 'type' => 'enum', - 'class' => 'SearchTypeEnum' ] ]); } diff --git a/app/src/Template/Attributes/fields.inc b/app/src/Template/Attributes/fields.inc index 69485c9e3..f10424672 100644 --- a/app/src/Template/Attributes/fields.inc +++ b/app/src/Template/Attributes/fields.inc @@ -38,7 +38,6 @@ if($action == 'add' || $action == 'edit') { // CO-1762 // print $this->Field->control('invalidates', [], false); print $this->Field->control('null_equivalents', [], false); - print $this->Field->control('required', [], false); print $this->Field->control('search_distance', [], false); print $this->Field->control('search_exact', [], false); diff --git a/app/src/Template/RuleAttributes/columns.inc b/app/src/Template/RuleAttributes/columns.inc new file mode 100644 index 000000000..e5f0de1f5 --- /dev/null +++ b/app/src/Template/RuleAttributes/columns.inc @@ -0,0 +1,44 @@ + [ + 'type' => 'link', + 'model' => 'attribute', + 'field' => 'name', + 'sortable' => 'Attributes.name' + ], + 'crosscheck_attribute_id' => [ + 'type' => 'fk', + 'label' => __('match.fd.RuleAttributes.crosscheck_attribute_id') + ], + 'search_type' => [ + 'type' => 'enum', + 'class' => 'SearchTypeEnum', + 'sortable' => true + ] +]; \ No newline at end of file diff --git a/app/src/Template/RuleAttributes/fields.inc b/app/src/Template/RuleAttributes/fields.inc new file mode 100644 index 000000000..923238725 --- /dev/null +++ b/app/src/Template/RuleAttributes/fields.inc @@ -0,0 +1,34 @@ +Field->control('attribute_id', ['empty' => true]); + print $this->Field->control('crosscheck_attribute_id', ['empty' => true], false, __('match.fd.RuleAttributes.crosscheck_attribute_id')); + print $this->Field->control('search_type', ['empty' => true]); + print $this->Field->control('required'); +} \ No newline at end of file diff --git a/app/src/Template/Rules/columns.inc b/app/src/Template/Rules/columns.inc index 617a13cd7..f530d3d0c 100644 --- a/app/src/Template/Rules/columns.inc +++ b/app/src/Template/Rules/columns.inc @@ -19,7 +19,7 @@ * See the License for the specific language governing permissions and * limitations under the License. * - * @link http://www.internet2.edu/comanage COmanage Project + * @link https://www.internet2.edu/comanage COmanage Project * @package match * @since COmanage Match v1.0.0 * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) @@ -36,4 +36,12 @@ $indexColumns = [ 'ordr' => [ 'type' => 'echo' ], +]; + +$indexActions = [ + [ + 'controller' => 'rule_attributes', + 'action' => 'index', + 'class' => 'linkbutton' + ] ]; \ No newline at end of file diff --git a/app/src/Template/Rules/fields.inc b/app/src/Template/Rules/fields.inc index 545c84227..a21030a87 100644 --- a/app/src/Template/Rules/fields.inc +++ b/app/src/Template/Rules/fields.inc @@ -19,77 +19,16 @@ * See the License for the specific language governing permissions and * limitations under the License. * - * @link http://www.internet2.edu/comanage COmanage Project + * @link https://www.internet2.edu/comanage COmanage Project * @package match * @since COmanage Match v1.0.0 * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) */ -use \App\Lib\Enum\ConfidenceModeEnum; -use \App\Lib\Enum\SearchTypeEnum; -?> - -Field->control('name'); print $this->Field->control('description', [], false); print $this->Field->control('confidence_mode', ['empty' => true]); print $this->Field->control('ordr'); - -// XXX only list attribute that make sense for canonical vs potential -// eg if "Search Exact" is not ticked (and/or maybe subtring), attribute should not be available for -// canonical rules -// XXX we need $attributes passed directly from the controller (where matchgrid_id=cur_id) - - print "
  • " . __('match.fd.search_types') . "
  • \n"; - - $i = 0; - - foreach($attributes as $a) { - // Calculate the current value, since cake automagic doesn't seem to get it - $id = null; - $val = SearchTypeEnum::Skip; - - if(!empty($vv_obj->rule_attributes)) { - // This will return an array even though we expect one value - $curvals = \Cake\Utility\Hash::extract($vv_obj->rule_attributes, '{n}[attribute_id='.$a->id.']'); - - if(!empty($curvals[0])) { - $id = $curvals[0]->id; - $val = $curvals[0]->search_type; - } - } - - if($id) { - print $this->Form->hidden('rule_attributes.'.$i.'.id', ['value' => $id]); - } - print $this->Form->hidden('rule_attributes.'.$i.'.attribute_id', ['value' => $a->id]); - // XXX don't allow Distance or Substring if Canonical (though substring could be ok for canonical?) - print $this->Field->control('rule_attributes.'.$i.'.search_type', ['value' => $val], true, $a->name); - $i++; - } } \ No newline at end of file diff --git a/app/src/Template/Standard/add-edit-view.ctp b/app/src/Template/Standard/add-edit-view.ctp index 0e200899b..cafaeb7ee 100644 --- a/app/src/Template/Standard/add-edit-view.ctp +++ b/app/src/Template/Standard/add-edit-view.ctp @@ -72,7 +72,7 @@ if(!empty($vv_primary_link)) { } } -print $this->Field->startControlSet($this->name, $action, ($action == 'add' || $action == 'edit')); +print $this->Field->startControlSet($vv_obj, $this->name, $action, ($action == 'add' || $action == 'edit')); include(APP . "Template/" . $modelsName . "/fields.inc"); diff --git a/app/src/Template/Standard/index.ctp b/app/src/Template/Standard/index.ctp index 34315995e..3d896640d 100644 --- a/app/src/Template/Standard/index.ctp +++ b/app/src/Template/Standard/index.ctp @@ -42,8 +42,8 @@ $modelsName = $this->name; // $tablefk = model_id $tableFK = Inflector::singularize($vv_tablename) . "_id"; -// Our default link action is edit, unless the model config overrides it -$primaryAction = 'edit'; +// Our default link actions, in order of preference, unless the column config overrides it +$linkActions = ['edit', 'view']; // Read the index configuration ($indexColumns) for this model include(APP . "Template/" . $modelsName . "/columns.inc"); @@ -141,7 +141,11 @@ function _column_key($modelsName, $c, $tz=null) { $label = !empty($cfg['label']) ? $cfg['label'] : _column_key($modelsName, $col, $vv_tz); if(isset($cfg['sortable']) && $cfg['sortable']) { - print $this->Paginator->sort($col, $label); + if(is_string($cfg['sortable'])) { + print $this->Paginator->sort($cfg['sortable'], $label); + } else { + print $this->Paginator->sort($col, $label); + } } else { print $label; } @@ -201,13 +205,39 @@ function _column_key($modelsName, $c, $tz=null) { } break; case 'link': - print $this->Html->link($entity->$col, array_merge(['action' => $primaryAction], $linkArgs)); - break; case 'echo': default: - // Just echo the value - print $entity->$col; + // By default our label is the column value, but it might be overridden + $label = $entity->$col; + + if(!empty($cfg['model']) && !empty($cfg['field'])) { + $m = $cfg['model']; + $f = $cfg['field']; + + if(!empty($entity->$m->$f)) { + $label = $entity->$m->$f; + } + } + + $linked = false; + + if($cfg['type'] == 'link') { + foreach($linkActions as $a) { + // Does this user have permission for this action? + if($vv_permissions[$a]) { + print $this->Html->link($label, ['action' => $a, $entity->id]); + $linked = true; + break 2; + } + } + } + + if(!$linked) { + // Just echo the value + print $label; + } break; + // XXX dates can be rendered as eg $entity->created->format(DATE_RFC850); } ?> diff --git a/app/src/View/Helper/FieldHelper.php b/app/src/View/Helper/FieldHelper.php index 96719b6a2..792c997e0 100644 --- a/app/src/View/Helper/FieldHelper.php +++ b/app/src/View/Helper/FieldHelper.php @@ -19,7 +19,7 @@ * See the License for the specific language governing permissions and * limitations under the License. * - * @link http://www.internet2.edu/comanage COmanage Project + * @link https://www.internet2.edu/comanage COmanage Project * @package match * @since COmanage Match v1.0.0 * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) @@ -29,6 +29,7 @@ namespace App\View\Helper; +use \Cake\Utility\Hash; use \Cake\Utility\Inflector; use \Cake\View\Helper; @@ -41,22 +42,27 @@ class FieldHelper extends Helper { // Our current modelname protected $modelName = null; + // The model object, as provided by StandardController + protected $viewObj = null; + /** * Emit a form control. * * @since COmanage Match v1.0.0 - * @param string $fieldName Form field - * @param array $options FormHelper control options - * @param boolean $required True if this attribute is required - * @param string $labelText Label text (fieldName language key used by default) - * @param string $default Default value for field + * @param string $fieldName Form field + * @param array $options FormHelper control options + * @param boolean $required True if this attribute is required + * @param string $labelText Label text (fieldName language key used by default) + * @param string $default Default value for field + * @param array $childControls HTML for child controls to render within this control * @return string HTML for control */ public function control(string $fieldName, array $options=[], bool $required=true, - string $labelText=null) { + string $labelText=null, + array $childControls=[]) { $coptions = $options; $coptions['label'] = false; @@ -100,6 +106,20 @@ public function control(string $fieldName, } } + $children = ''; + + if(!empty($childControls)) { + // Embed the child control HTML into the appropriate div + + $children = '"; + } + return '
  • @@ -112,6 +132,7 @@ public function control(string $fieldName,
    ' . $this->Form->control($fieldName, $coptions) . ' + ' . $children . '
  • '; @@ -153,13 +174,15 @@ public function endControlSet() { * Start a set of form controls. * * @since COmanage Match v1.0.0 - * @param String $modelName Model name for form - * @param String $action Current action - * @param String $editable True if controls are read/write, false for read only - * @return String + * @param object $viewObj View object as provided by StandardController + * @param string $modelName Model name for form + * @param string $action Current action + * @param string $editable True if controls are read/write, false for read only + * @return string */ - public function startControlSet($modelName, $action, $editable=true) { + public function startControlSet($viewObj, $modelName, $action, $editable=true) { + $this->viewObj = $viewObj; $this->editable = $editable; $this->modelName = $modelName;