diff --git a/app/src/Controller/AppController.php b/app/src/Controller/AppController.php index 39f112ccb..c741a08a1 100644 --- a/app/src/Controller/AppController.php +++ b/app/src/Controller/AppController.php @@ -252,44 +252,19 @@ protected function setMatchgrid() { // Try to find the requested matchgrid $mgid = null; - if($this->request->is('get')) { - // If this action allows unkeyed, asserted primary link IDs, check the query - // string (eg: 'add' or 'index' allow matchgrid_id to be passed in), and - // possibly dereference it if the primary key is not matchgrid_id. - if($this->$modelsName->allowUnkeyedPrimaryLink($this->request->getParam('action'))) { - if($link['linkattr'] == 'matchgrid_id') { - // Simply accept the passed matchgrid ID - $mgid = $this->request->getQuery('matchgrid_id'); - } else { - // We already have the primary link object in a viewvar - - $plObj = $this->viewVars['vv_primary_link_obj']; - - if(!empty($plObj->matchgrid_id)) { - $mgid = $plObj->matchgrid_id; - } - } - } elseif($this->$modelsName->allowLookupPrimaryLink($this->request->getParam('action'))) { - // Try to map the requested object ID - $param = (int)$this->request->getParam('pass.0'); - - if(!empty($param)) { - $mgid = $this->$modelsName->calculateMatchgridId($param); - } - } - } elseif($this->request->is('post') || $this->request->is('put')) { + // If this action allows unkeyed, asserted primary link IDs, check the query + // string (eg: 'add' or 'index' allow matchgrid_id to be passed in), and + // possibly dereference it if the primary key is not matchgrid_id. + if($this->$modelsName->allowUnkeyedPrimaryLink($this->request->getParam('action'))) { if($link['linkattr'] == 'matchgrid_id') { // Simply accept the passed matchgrid ID - if(!empty($this->request->getData('matchgrid_id'))) { - $mgid = $this->request->getData('matchgrid_id'); - } elseif(!empty($this->request->getData($modelName . ".matchgrid_id"))) { - $mgid = $this->request->getData($modelName . ".matchgrid_id"); - } elseif($this->$modelsName->allowLookupPrimaryLink($this->request->getParam('action'))) { - // Try to map the requested object ID (this is probably a delete, so no attribute in post body) - $param = (int)$this->request->getParam('pass.0'); - - if(!empty($param)) { - $mgid = $this->$modelsName->calculateMatchgridId($param); + if($this->request->is('get')) { + $mgid = $this->request->getQuery('matchgrid_id'); + } elseif($this->request->is('post') || $this->request->is('put')) { + if(!empty($this->request->getData('matchgrid_id'))) { + $mgid = $this->request->getData('matchgrid_id'); + } elseif(!empty($this->request->getData($modelName . ".matchgrid_id"))) { + $mgid = $this->request->getData($modelName . ".matchgrid_id"); } } } else { @@ -301,6 +276,13 @@ protected function setMatchgrid() { $mgid = $plObj->matchgrid_id; } } + } elseif($this->$modelsName->allowLookupPrimaryLink($this->request->getParam('action'))) { + // Try to map the requested object ID + $param = (int)$this->request->getParam('pass.0'); + + if(!empty($param)) { + $mgid = $this->$modelsName->calculateMatchgridId($param); + } } if(!$mgid && !$this->$modelsName->allowEmptyMatchgrid()) { diff --git a/app/src/Controller/MatchgridRecordsController.php b/app/src/Controller/MatchgridRecordsController.php new file mode 100644 index 000000000..103f1e260 --- /dev/null +++ b/app/src/Controller/MatchgridRecordsController.php @@ -0,0 +1,415 @@ + [ + 'sor' => 'asc', + 'sorid' => 'asc', + ] + ]; + + /** + * Initialization callback. + * + * @since COmanage Match v1.0.0 + */ + + public function initialize() { + parent::initialize(); + + // In order to configure MatchgridRecords with the correct table name, we + // need to know the requested Matchgrid ID. However, AppController doesn't + // set that until beforeFilter(), at which point it is too late to configure + // the table. We also can't manually call setMatchgrid() (or really anything) + // since that will trigger the instantiation of the model. + + $Matchgrids = TableRegistry::get('Matchgrids'); + $obj = null; + + // Note we allow matchgrid_id to be asserted by *all* actions, since the + // record ID is specific to the matchgrid. + $obj = $Matchgrids->findById($this->request->getQuery('matchgrid_id'))->firstOrFail(); + + if(!$obj) { + throw new \RuntimeException(__('match.er.mgid')); + } + + // Set the table name for MatchgridRecordsTable + TableRegistry::getTableLocator()->setConfig('MatchgridRecords', ['table' => $obj->prefixed_table_name]); + } + + /** + * Handle an add action for a Matchgrid Record object. + * + * @since COmanage Match v1.0.0 + */ + + public function add() { + if($this->request->is('post')) { + // We don't want StandardController behavior here... + + try { + // Pull out the core attributes + $reqData = $this->request->getData(); + + $sor = $reqData['sor']; + $sorid = $reqData['sorid']; + $referenceid = $reqData['referenceid']; + + if(empty($sor) || empty($sorid)) { + throw new \InvalidArgumentException(__('match.er.records.sorid')); + } + + unset($reqData['sor']); + unset($reqData['sorid']); + unset($reqData['referenceid']); + unset($reqData['matchgrid_id']); + + $MatchService = new \App\Lib\Match\MatchService(); + + $MatchService->connect(); + $MatchService->setConfig($this->cur_mg->id); + + // Before we perform the request, see if we already have an entry for this SORID + $requestId = $MatchService->getRequestIdForSorId($sor, $sorid); + + if($requestId) { + throw new \InvalidArgumentException(__('match.er.records.exists', [$requestId])); + } + + // Instantiate the AttributeManager + $AttributeManager = new \App\Lib\Match\AttributeManager(); + + $AttributeManager->parseFromArray($reqData); + + // performMatch will issue a redirect on success + $this->performMatch($MatchService, $sor, $sorid, $AttributeManager); + } + catch(\Exception $e) { + $this->Flash->error($e->getMessage()); + + // We could coerce validation errors into a newEntity, but we probably don't have them + // $this->set('vv_obj', $this->MatchgridRecords->newEntity($this->request->getData())); + // Create an empty entity for FormHelper + $this->set('vv_obj', $this->MatchgridRecords->newEntity()); + + // We can't call parent::add, since it will try to reprocess the save, so we have + // to manually make some calls. + // PrimaryLinkTrait + $this->getPrimaryLink(); + + // AutoViewVarsTrait + $this->populateAutoViewVars(); + + // Default title is add new object + $this->set('vv_title', __('match.op.add.a', __('match.ct.MatchgridRecords', [1]))); + + // Let the view render + $this->render('/Standard/add-edit-view'); + } + } else { + parent::add(); + } + } + + /** + * Callback run prior to the view rendering. + * + * @since COmanage Match v1.0.0 + * @param Event $event Cake Event + */ + + public function beforeRender(\Cake\Event\Event $event) { + parent::beforeRender($event); + + // Pull the Matchgrid configuration in order to pass the attribute configuration. + // We can almost, but not quite, use autoViewVars for this, since we need the + // attribute grouping to create the full api name. + $Matchgrids = TableRegistry::get('Matchgrids'); + + try { + $mg = $Matchgrids->getMatchgridConfig($this->cur_mg->id); + + $this->set('attributes', Hash::sort($mg->attributes, '{n}.name', 'asc')); + } + catch(RecordNotFoundException $e) { + $this->Flash->error(__("match.er.notfound", [__("match.ct.matchgrids", [1]), $this->cur_mg->id])); + } + } + + /** + * Handle an add action for a Matchgrid Record object. + * + * @since COmanage Match v1.0.0 + * @param Integer $id Object ID + */ + + public function edit($id) { + if($this->request->is(['post', 'put'])) { + // We don't want StandardController behavior here... + // There is quite a bit of overlap with add(), we could probably merge + // them together with a bit of refactoring. + + try { + // Pull out the core attributes from the request + $reqData = $this->request->getData(); + + // Pull the current record + $query = $this->MatchgridRecords->findById($id); + $obj = $query->firstOrFail(); + + $sor = $reqData['sor']; + $sorid = $reqData['sorid']; + + if(empty($sor) || empty($sorid)) { + throw new \InvalidArgumentException(__('match.er.records.sorid')); + } + + unset($reqData['sor']); + unset($reqData['sorid']); + unset($reqData['matchgrid_id']); + + $MatchService = new MatchService(); + + $MatchService->connect(); + $MatchService->setConfig($this->cur_mg->id); + + // Unlike add (which verifies no entries for sor+sorid), we don't sanity + // check on edit. Basically, we assume the Matchgrid Admin knows what they're + // doing, and if they have a use case for changing the SOR of a matchgrid + // entry, then let them do it. + + // Instantiate the AttributeManager + $AttributeManager = new AttributeManager(); + + $AttributeManager->parseFromArray($reqData); + + // We behave differently depending on Reference ID state + // Note $reqData is an array while $obj is an object + if(!empty($reqData['referenceid']) && !empty($obj->referenceid)) { + if($reqData['referenceid'] == $obj->referenceid) { + // Update Match Attributes request + + Log::write('debug', $sor . "/". $sorid . " Updating existing SOR attributes for Row ID " . $id); + + $MatchService->updateSorAttributes((int)$id, $AttributeManager); + } else { + // Join/Split, possibly with updated attributes + + if(strtolower($reqData['referenceid']) != "new") { + // The requested Reference ID must be an already existing ID or the + // word "new" (to assign a new one). We do not allow manually + // assigning Reference IDs, as it may interfere with the Reference ID + // assignment algorithm (eg: cause a sequence to be out of sync). + + $results = $MatchService->getRequestsForReferenceId($reqData['referenceid']); + + if($results->count() == 0) { + throw new \InvalidArgumentException(__('match.er.val.refid')); + } + } + + Log::write('debug', $sor . "/". $sorid . " Updating Reference ID and attributes for Row ID " . $id); + + $MatchService->attachReferenceId($sor, $sorid, $AttributeManager, $reqData['referenceId']); + } + } elseif(!empty($reqData['referenceid']) && empty($obj->referenceid)) { + // Forced Reconciliation Request + + if(strtolower($reqData['referenceid']) != "new") { + // Same requirements as Join/Split, above + + $results = $MatchService->getRequestsForReferenceId($reqData['referenceid']); + + if($results->count() == 0) { + throw new \InvalidArgumentException(__('match.er.val.refid')); + } + } + + Log::write('debug', $sor . "/". $sorid . " Processing forced reconciliation request for Reference ID " . $reqData['referenceid']); + + $MatchService->attachReferenceId($sor, $sorid, $AttributeManager, $reqData['referenceid']); + } elseif(empty($reqData['referenceid']) && !empty($obj->referenceid)) { + // By removing an existing Reference ID, the admin is effectively asking + // to re-reconcile the row + + Log::write('debug', $sor . "/". $sorid . " Removing Reference ID and re-executing match rules for Row ID " . $id); + + // We have to clear out the existing reference ID, or we will simply + // rematch to the same record + $MatchService->removeReferenceId($sor, $sorid); + + $this->performMatch($MatchService, $sor, $sorid, $AttributeManager); + } elseif(empty($reqData['referenceid']) && empty($obj->referenceid)) { + // Treat like add(), but possibly updating an existing Match request + + Log::write('debug', $sor . "/". $sorid . " Re-executing match rules for Row ID " . $id); + + $this->performMatch($MatchService, $sor, $sorid, $AttributeManager); + } + + $this->Flash->success(__('match.rs.saved')); + + // Force a page reload to refresh the object + // (Redirect would normally be issued by performMatch) + return $this->redirect([ + 'action' => 'edit', + $id, + 'matchgrid_id' => $this->cur_mg->id + ]); + } + catch(\Exception $e) { + $this->Flash->error($e->getMessage()); + + // We could coerce validation errors into a newEntity, but we probably don't have them + // $this->set('vv_obj', $this->MatchgridRecords->newEntity($this->request->getData())); + } + + $this->set('vv_obj', $obj); + + // We can't call parent::edit, since it will try to reprocess the save, so we have + // to manually make some calls. Similar code in add(), above. + // PrimaryLinkTrait + $this->getPrimaryLink(); + + // AutoViewVarsTrait + $this->populateAutoViewVars(); + + $field = $this->MatchgridRecords->getDisplayField(); + + if(!empty($obj->$field)) { + $this->set('vv_title', __('match.op.edit.a', $obj->$field)); + } else { + $this->set('vv_title', __('match.op.edit.a', __('match.ct.MatchgridRecords', [1]))); + } + + // Let the view render + $this->render('/Standard/add-edit-view'); + } else { + parent::edit($id); + } + } + + /** + * 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) { + $mgid = isset($this->cur_mg->id) ? $this->cur_mg->id : null; + + $platformAdmin = $this->Authorization->isPlatformAdmin($user['username']); + + $mgAdmin = $this->Authorization->isMatchAdmin($user['username'], $mgid); + + $p = [ + 'add' => $platformAdmin || $mgAdmin, + 'delete' => $platformAdmin || $mgAdmin, + 'edit' => $platformAdmin || $mgAdmin, // Should reconciliation managers be allowed to add/edit/delete a record? + 'index' => $platformAdmin || $mgAdmin, + 'view' => $platformAdmin || $mgAdmin // Also reconciliation manager || support + ]; + + $this->set('vv_permissions', $p); + return $p[$this->request->getParam('action')]; + } + + /** + * Perform a Match operation based on the provided attributes. + * + * @since COmanage Match v1.0.0 + * @param MatchService $MatchService MatchService object + * @param string $sor SOR label + * @param string $sorid SOR ID + * @param AttributeManager $AttributeManager AttributeManager object + * @return Cake Redirect + */ + + protected function performMatch(MatchService $MatchService, string $sor, string $sorid, AttributeManager $AttributeManager) { + $results = $MatchService->searchReferenceId($sor, $sorid, $AttributeManager); + + // Did any rules run successfully? If not (eg: no attributes provided in the + // request, no rules defined) then throw an error. + if(empty($results->getSuccessfulRules())) { + throw new \RuntimeException(__('match.er.rules.unsuccessful')); + } + + if($results->count() == 0) { + // No match + $refId = $MatchService->assignReferenceId($sor, $sorid, $AttributeManager); + + $this->Flash->success(__('match.rs.refid.assigned', [$refId])); + } elseif($results->getConfidenceMode() == ConfidenceModeEnum::Canonical) { + // Exact match + $refIds = $results->getReferenceIds(); + + if(!empty($refIds[0])) { + $MatchService->attachReferenceId($sor, $sorid, $AttributeManager, (string)$refIds[0]); + + $this->Flash->sucess(__('match.rs.refid.matched', [(string)$refIds[0]])); + } + } else { + // Fuzzy match, we insert the record but do NOT send notification + $matchRequest = $MatchService->insertPending($sor, $sorid, $AttributeManager); + + $this->Flash->information(__('match.rs.refid.pending', [$matchRequest])); + + // Redirect to the reconcilation page + return $this->redirect([ + 'controller' => 'matchgrids', + 'action' => 'reconcile', + $this->cur_mg->id, + 'rowid' => $matchRequest + ]); + } + + $id = $MatchService->getRequestIdForSorId($sor, $sorid); + + return $this->redirect([ + 'action' => 'edit', + $id, + 'matchgrid_id' => $this->cur_mg->id + ]); + } +} \ No newline at end of file diff --git a/app/src/Controller/MatchgridSettingsController.php b/app/src/Controller/MatchgridSettingsController.php index 8ddaee495..e8a9cc0da 100644 --- a/app/src/Controller/MatchgridSettingsController.php +++ b/app/src/Controller/MatchgridSettingsController.php @@ -48,7 +48,7 @@ public function beforeRender(\Cake\Event\Event $event) { // Override page title if($this->request->getParam('action') == 'edit') { - $this->set('vv_title', __("match.op.edit.a", [__("match.ct.matchgrid_settings", 1)])); + $this->set('vv_title', __("match.op.edit.a", [__("match.ct.MatchgridSettings", 1)])); } } diff --git a/app/src/Controller/MatchgridsController.php b/app/src/Controller/MatchgridsController.php index cd4a6a09c..cf173f614 100644 --- a/app/src/Controller/MatchgridsController.php +++ b/app/src/Controller/MatchgridsController.php @@ -97,6 +97,7 @@ public function isAuthorized(Array $user) { 'add' => $platformAdmin, 'build' => $platformAdmin || $mgAdmin, 'delete' => $platformAdmin, + 'display' => $platformAdmin || $mgAdmin, 'edit' => $platformAdmin, 'index' => $platformAdmin, 'manage' => $platformAdmin || $mgAdmin, diff --git a/app/src/Controller/StandardController.php b/app/src/Controller/StandardController.php index 979f32c20..31a0d29dd 100644 --- a/app/src/Controller/StandardController.php +++ b/app/src/Controller/StandardController.php @@ -41,8 +41,6 @@ class StandardController extends AppController { public function add() { // $this->name = Models (ie: from ModelsTable) $modelsName = $this->name; - // $tableName = models - $tableName = $this->$modelsName->getTable(); if($this->request->is('post')) { // Try to save @@ -73,7 +71,7 @@ public function add() { $this->populateAutoViewVars(); // Default title is add new object - $this->set('vv_title', __('match.op.add.a', __('match.ct.'.$tableName, [1]))); + $this->set('vv_title', __('match.op.add.a', __('match.ct.'.$modelsName, [1]))); // Let the view render $this->render('/Standard/add-edit-view'); @@ -190,7 +188,7 @@ public function edit($id) { if(!empty($obj->$field)) { $this->set('vv_title', __('match.op.edit.a', $obj->$field)); } else { - $this->set('vv_title', __('match.op.edit.a', __('match.ct.'.$tableName, [1]))); + $this->set('vv_title', __('match.op.edit.a', __('match.ct.'.$modelsName, [1]))); } // Let the view render @@ -236,18 +234,22 @@ public function index() { // AutoViewVarsTrait $this->populateAutoViewVars(); - if(!empty($link['linkattr'])) { + // Filter on link attribute, except for MatchgridRecords where the link value + // (matchgrid_id) is implied by the table itself + 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'])]); } else { $query = $this->$modelsName->find(); } - + $this->set($tableName, $this->Paginator->paginate($query, $this->paginate)); + $this->set('vv_tablename', $tableName); + $this->set('vv_modelname', $modelsName); // Default index view title is model name - $this->set('vv_title', __('match.ct.'.$tableName, [99])); + $this->set('vv_title', __('match.ct.'.$modelsName, [99])); // Let the view render $this->render('/Standard/index'); @@ -276,10 +278,11 @@ protected function populateAutoViewVars(object $obj=null) { $class = '\\App\\Lib\\Enum\\'.$avv['class']; $this->set($vvar, $class::getLocalizedConsts()); break; - // "auxiliary" and "select" do basically the same thing, but the former - // returns the full object and the latter just returns a hash suitable - // for a select + // "auxiliary", "list, and "select" do basically the same thing, but + // "auxiliary" returns the full object, while the other two return a + // hash suitable for a select. "list" allows customized fields. case 'auxiliary': + case 'list': case 'select': // We assume $modelName has a direct relationship to $avv['model'] $avvmodel = $avv['model']; @@ -288,7 +291,13 @@ protected function populateAutoViewVars(object $obj=null) { if($avv['type'] == 'auxiliary') { $query = $this->$avvmodel->find(); } else { - $query = $this->$avvmodel->find('list'); + $fields = []; + + if($avv['type'] == 'list') { + $fields = $avv['fields']; + } + + $query = $this->$avvmodel->find('list', $fields); } if(!empty($avv['find'])) { @@ -320,10 +329,15 @@ protected function populateAutoViewVars(object $obj=null) { } } + // CO-1681 -- port to PE? + if(!empty($avv['order'])) { + $query = $query->order($avv['order']); + } + $this->set($vvar, $query->toArray()); break; default: - throw new \LogicException('Unknonwn Auto View Var Type {0}', [$avv['type']]); + throw new \LogicException('Unknown Auto View Var Type {0}', [$avv['type']]); break; } } diff --git a/app/src/Controller/TierApiController.php b/app/src/Controller/TierApiController.php index ea14ea466..04129afd8 100644 --- a/app/src/Controller/TierApiController.php +++ b/app/src/Controller/TierApiController.php @@ -237,6 +237,47 @@ protected function doMatchRequest(bool $searchOnly=false) { // Forced Reconciliation request. Skip the search and jump to the insert. // (attachReferenceId will insert or update as appropriate.) + if($requestedReferenceId != "new") { + // Check that the requested SOR/SORID doesn't already have a Reference ID. + // If it does, a Matchgrid administrator should make whatever changes are + // requested, not the SOR over the API. + + if($currentReferenceId) { + Log::write('debug', $sor . "/". $sorid . " Rejecting forced reconciliation request for Reference ID " . $requestedReferenceId . " (already reconciled)"); + throw new \InvalidArgumentException(__('match.er.reconcile.done.api', [$curid])); + } + + // Next check that the requested Reference ID is a valid candidate + // (basically by re-performing the search). + + $results = $MatchService->searchReferenceId($sor, $sorid, $AttributeManager); + + // Did any rules run successfully? If not (eg: no attributes provided in the + // request, no rules defined) then throw an error. + if(empty($results->getSuccessfulRules())) { + throw new \RuntimeException(__('match.er.rules.unsuccessful')); + } + + if($results->count() == 0) { + // No match + Log::write('debug', $sor . "/". $sorid . " Rejecting forced reconciliation request for Reference ID " . $requestedReferenceId . " (no matches)"); + throw new \InvalidArgumentException(__('match.er.reconcile.invalid.api')); + } else { + $refIds = $results->getReferenceIds(); + + if(!in_array($requestedReferenceId, $refIds)) { + // Requested Reference ID is not in the candidate pool + Log::write('debug', $sor . "/". $sorid . " Rejecting forced reconciliation request for Reference ID " . $requestedReferenceId . " (invalid candidate)"); + throw new \InvalidArgumentException(__('match.er.reconcile.invalid.api')); + } + } + + // Note that we don't further check that Reference ID matches an + // existing record (as we do via the UI) because we've effectively + // already just performed that check (a candidate Reference ID must + // by definition already be in use). + } + Log::write('debug', $sor . "/". $sorid . " Processing forced reconciliation request for Reference ID " . $requestedReferenceId); $referenceId = $MatchService->attachReferenceId($sor, $sorid, $AttributeManager, $requestedReferenceId); diff --git a/app/src/Lib/Match/MatchService.php b/app/src/Lib/Match/MatchService.php index 9a0e61f86..74f90202e 100644 --- a/app/src/Lib/Match/MatchService.php +++ b/app/src/Lib/Match/MatchService.php @@ -454,6 +454,32 @@ public function remove(string $sor, string $sorid) { return !empty($ret); } + /** + * Remove the Reference ID associated with an SOR record. + * + * @since COmanage Match v1.0.0 + * @param string $sor SOR Label + * @param string $sorid SOR Record Identifier + * @return boolean True if the Reference ID was removed + * @throws RuntimeException + */ + + public function removeReferenceId(string $sor, string $sorid) { + $sql = "UPDATE " . $this->mgTable . " + SET referenceid=null, + resolution_time=null + WHERE sor=? + AND sorid=?"; + + $stmt = $this->dbc->Prepare($sql); + + if(!$this->dbc->Execute($stmt, [$sor, $sorid])) { + throw new \RuntimeException($this->dbc->errorMsg()); + } + + return true; + } + /** * Perform a search of the matchgrid according to the current configuration. * @@ -682,7 +708,7 @@ public function setConfig(int $matchgridId) { ]]) ->firstOrFail(); - $this->mgTable = "mg_" . $this->mgConfig->table_name; + $this->mgTable = $this->mgConfig->prefixed_table_name; } /** diff --git a/app/src/Lib/Match/MatchgridBuilder.php b/app/src/Lib/Match/MatchgridBuilder.php index 1ed60367d..4427ed239 100644 --- a/app/src/Lib/Match/MatchgridBuilder.php +++ b/app/src/Lib/Match/MatchgridBuilder.php @@ -81,7 +81,7 @@ protected function configToSchema($dbc, \Cake\Datasource\EntityInterface $Matchg $schema = new Schema(); // Create the table - $table = $schema->createTable("mg_" . $Matchgrid->table_name); + $table = $schema->createTable($Matchgrid->prefixed_table_name); // For type definitions see https://www.doctrine-project.org/api/dbal/2.9/Doctrine/DBAL/Types/Type.html diff --git a/app/src/Locale/en_US/default.po b/app/src/Locale/en_US/default.po index 9dcfeb63e..034457bd9 100644 --- a/app/src/Locale/en_US/default.po +++ b/app/src/Locale/en_US/default.po @@ -99,34 +99,37 @@ msgid "match.cmd.se.salt" msgstr "- Generating salt file" ### Controllers (Models) -msgid "match.ct.api_users" +msgid "match.ct.ApiUsers" msgstr "{0,plural,=1{API User} other{API Users}}" -msgid "match.ct.attribute_groups" +msgid "match.ct.AttributeGroups" msgstr "{0,plural,=1{Attribute Group} other{Attribute Groups}}" -msgid "match.ct.attribute_mappings" +msgid "match.ct.AttributeMappings" msgstr "{0,plural,=1{Attribute Mapping} other{Attribute Mappings}}" -msgid "match.ct.attribute_maps" +msgid "match.ct.AttributeMaps" msgstr "{0,plural,=1{Attribute Map} other{Attribute Maps}}" -msgid "match.ct.attributes" +msgid "match.ct.Attributes" msgstr "{0,plural,=1{Attribute} other{Attributes}}" -msgid "match.ct.matchgrid_settings" +msgid "match.ct.MatchgridSettings" msgstr "{0,plural,=1{Matchgrid Settings} other{Matchgrid Settings}}" -msgid "match.ct.matchgrids" +msgid "match.ct.Matchgrids" msgstr "{0,plural,=1{Matchgrid} other{Matchgrids}}" -msgid "match.ct.permissions" +msgid "match.ct.MatchgridRecords" +msgstr "{0,plural,=1{Matchgrid Record} other{Matchgrid Records}}" + +msgid "match.ct.Permissions" msgstr "{0,plural,=1{Permission} other{Permissions}}" -msgid "match.ct.systems_of_record" +msgid "match.ct.SystemsOfRecord" msgstr "{0,plural,=1{System of Record} other{Systems of Record}}" -msgid "match.ct.rules" +msgid "match.ct.Rules" msgstr "{0,plural,=1{Rule} other{Rules}}" ### Enumerations @@ -248,9 +251,21 @@ msgstr "Error obtaining pending requests: {0}" msgid "match.er.reconcile.done" msgstr "Request ID {0} already resolved" +msgid "match.er.reconcile.done.api" +msgstr "Request ID {0} already has a Reference ID assigned, contact an administrator for assistance" + +msgid "match.er.reconcile.invalid.api" +msgstr "The requested Reference ID is not a valid candidate, resubmit the request or contact an administrator for assistance" + msgid "match.er.reconcile.notfound" msgstr "Request ID {0} not found" +msgid "match.er.records.exists" +msgstr "Match Request {0} already exists for this System of Record ID" + +msgid "match.er.records.sorid" +msgstr "A System of Record ID is required" + msgid "match.er.rules.unsuccessful" msgstr "No rules successfully completed" @@ -278,6 +293,9 @@ msgstr "Provided value exceeds maximum length of {0}" msgid "match.er.val.range" msgstr "Value must be between {0} and {1}" +msgid "match.er.val.refid" +msgstr "Requested Reference ID must already be in use, or be the keyword 'new'" + ### Fields ### Keys of the form match.fd.MyModels.field_name[.desc] will apply only to MyModels.field_name ### Keys of the form match.fd.field_name[.desc] will apply if no model specific key is found @@ -305,6 +323,9 @@ msgstr "Value used as a matching string to query key" msgid "match.fd.Attributes.name.desc" msgstr "Value must be a valid SQL identifier, as it will be used to construct the matchgrid column name" +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.case_sensitive" msgstr "Case Sensitive" @@ -350,8 +371,8 @@ msgstr "Permission" msgid "match.fd.query" msgstr "Query" -msgid "match.fd.referenceids" -msgstr "Reference IDs" +msgid "match.fd.referenceid" +msgstr "Reference ID" msgid "match.fd.referenceid_method" msgstr "Reference ID Assignment Method" @@ -395,6 +416,9 @@ msgstr "Search Substring For" msgid "match.fd.search_types" msgstr "Search Types" +msgid "match.fd.select" +msgstr "(Please select a value)" + msgid "match.fd.sor" msgstr "System of Record" @@ -452,6 +476,9 @@ msgstr "Delete" msgid "match.op.delete.confirm" msgstr "Are you sure you wish to delete this record ({0})?" +msgid "match.op.display" +msgstr "Display" + msgid "match.op.edit" msgstr "Edit" @@ -534,5 +561,11 @@ msgstr "{0,plural,=1{# Pending Match} other{# Pending Matches}}" msgid "match.rs.refid.assigned" msgstr "Assigned Reference ID {0}" +msgid "match.rs.refid.matched" +msgstr "Matched to existing Reference ID {0}" + +msgid "match.rs.refid.pending" +msgstr "Request could not be canonically resolved, pending record {0} created" + msgid "match.rs.saved" msgstr "Saved" diff --git a/app/src/Model/Entity/Matchgrid.php b/app/src/Model/Entity/Matchgrid.php index c61d9f543..abd19922c 100644 --- a/app/src/Model/Entity/Matchgrid.php +++ b/app/src/Model/Entity/Matchgrid.php @@ -37,4 +37,15 @@ class Matchgrid extends Entity { 'id' => false, 'slug' => false, ]; + + /** + * Determine the prefixed name of the matchgrid table (ie: mg_foo). + * + * @since COmanage Match v1.0.0 + * @return string Prefixed table name + */ + + protected function _getPrefixedTableName() { + return "mg_" . $this->table_name; + } } \ No newline at end of file diff --git a/app/src/Model/Entity/MatchgridRecord.php b/app/src/Model/Entity/MatchgridRecord.php new file mode 100644 index 000000000..7bb7b0ba4 --- /dev/null +++ b/app/src/Model/Entity/MatchgridRecord.php @@ -0,0 +1,40 @@ + true, + 'id' => false, + 'slug' => false, + ]; +} \ No newline at end of file diff --git a/app/src/Model/Table/MatchgridRecordsTable.php b/app/src/Model/Table/MatchgridRecordsTable.php new file mode 100644 index 000000000..b3a020284 --- /dev/null +++ b/app/src/Model/Table/MatchgridRecordsTable.php @@ -0,0 +1,76 @@ +setPrimaryLink('matchgrid_id'); + $this->setRequiresMatchgrid(true); + // We allow unkeyed primary link here because a record ID is not by itself + // sufficient to identify a matchgrid, since each matchgrid gets its own + // physical table. + $this->setAllowUnkeyedPrimaryLink(['edit']); + + $this->setAutoViewVars([ + 'sor' => [ + 'type' => 'list', + 'model' => 'SystemsOfRecord', + 'find' => 'filterPrimaryLink', + // Because matchgrids use the actual SOR label and not systems_of_record_id + // we need a custom select field + 'fields' => ['keyField' => 'label', 'valueField' => 'label'] + ] + ]); + } + + /** + * Set validation rules. + * + * @param Validator $validator Validator + * @return $validator Validator + * + + public function validationDefault(Validator $validator) { + // We don't do validation here since MatchService should handle that + }*/ +} \ No newline at end of file diff --git a/app/src/Template/Element/breadcrumbs.ctp b/app/src/Template/Element/breadcrumbs.ctp index 75c1a26b9..cb196db31 100644 --- a/app/src/Template/Element/breadcrumbs.ctp +++ b/app/src/Template/Element/breadcrumbs.ctp @@ -25,14 +25,14 @@ * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) */ +use \Cake\Utility\Inflector; + if($this->request->getRequestTarget(false) != '/') { // Don't bother rendering breadcrumbs if we're already at the top page $action = $this->template; // $this->name = Models $modelsName = $this->name; - // $tablename = models - $tableName = \Cake\Utility\Inflector::tableize($this->name); $this->Breadcrumbs->setTemplates([ 'wrapper' => '{{content}}', @@ -88,7 +88,7 @@ if($this->request->getRequestTarget(false) != '/') { // Default parent is index, to which we might need to append the Primary Link ID $target = [ - 'controller' => $tableName, + 'controller' => Inflector::dasherize($modelsName), 'action' => 'index' ]; @@ -97,11 +97,11 @@ if($this->request->getRequestTarget(false) != '/') { } $this->Breadcrumbs->add( - __('match.ct.'.$tableName, [99]), + __('match.ct.'.$modelsName, [99]), $target ); } - + if(!empty($vv_title)) { $this->Breadcrumbs->add( $vv_title diff --git a/app/src/Template/Element/menuMain.ctp b/app/src/Template/Element/menuMain.ctp index 5cefc0753..bbe015e73 100644 --- a/app/src/Template/Element/menuMain.ctp +++ b/app/src/Template/Element/menuMain.ctp @@ -48,7 +48,7 @@ print '
| = __($product.'.fd.action'); ?> | |
|---|---|
| @@ -183,7 +185,7 @@ function _column_key($modelsName, $c, $tz=null) { // AutoViewVar $foos is set, and if so render the lookup value instead $f = null; if(preg_match('/^(.*?)_id$/', $col, $f)) { - $avv = \Cake\Utility\Inflector::variable(\Cake\Utility\Inflector::pluralize($f[1])); + $avv = Inflector::variable(Inflector::pluralize($f[1])); if(!empty(${$avv}[$entity->$col])) { // We found the viewvar (eg: $foos), and it has a corresponding value @@ -199,7 +201,7 @@ function _column_key($modelsName, $c, $tz=null) { } break; case 'link': - print $this->Html->link($entity->$col, ['action' => $primaryAction, $entity->id]); + print $this->Html->link($entity->$col, array_merge(['action' => $primaryAction], $linkArgs)); break; case 'echo': default: @@ -215,7 +217,7 @@ function _column_key($modelsName, $c, $tz=null) { if($vv_permissions['edit']) { print $this->Html->link( __($product.'.op.edit'), - ['action' => 'edit', $entity->id], + array_merge(['action' => 'edit'], $linkArgs), ['class' => 'editbutton'] ); } @@ -225,7 +227,7 @@ function _column_key($modelsName, $c, $tz=null) { // probably because this is using Form helper, but we're outside of a form? print $this->Form->postLink( __($product.'.op.delete'), - ['action' => 'delete', $entity->id], + array_merge(['action' => 'delete'], $linkArgs), // XXX should be configurable which field we put in, maybe displayField? ['confirm' => __($product.'.op.delete.confirm', [$entity->id]), 'class' => 'deletebutton'] @@ -248,7 +250,7 @@ function _column_key($modelsName, $c, $tz=null) { print $this->Form->postLink( __($product.'.op.' . $a['action']), - ['action' => $a['action'], $entity->id], + array_merge(['action' => $a['action']], $linkArgs), // XXX should be configurable which field we put in, maybe displayField? ['confirm' => __($confirmKey, [$entity->id]), 'class' => $a['class']] @@ -265,7 +267,7 @@ function _column_key($modelsName, $c, $tz=null) { } else { print $this->Html->link( __($product.'.op.' . $a['action']), - ['action' => $a['action'], $entity->id], + array_merge(['action' => $a['action']], $linkArgs), ['class' => $a['class']] ); } @@ -276,7 +278,7 @@ function _column_key($modelsName, $c, $tz=null) { ?> |