Skip to content
Permalink
d1cf34dbef
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
 
 
Cannot retrieve contributors at this time
479 lines (378 sloc) 18.7 KB
<?php
/**
* COmanage Matchgrid Records Controller
*
* Portions licensed to the University Corporation for Advanced Internet
* Development, Inc. ("UCAID") under one or more contributor license agreements.
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* UCAID licenses this file to you under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @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)
*/
declare(strict_types = 1);
namespace App\Controller;
use Cake\Log\Log;
use Cake\Utility\Hash;
use Cake\Event\EventInterface;
use \App\Lib\Enum\ConfidenceModeEnum;
use \App\Lib\Enum\MatchgridActionEnum;
use \App\Lib\Match\AttributeManager;
use \App\Lib\Match\MatchService;
class MatchgridRecordsController extends StandardController {
public $pagination = [
'order' => [
'sor' => 'asc',
'sorid' => 'asc',
]
];
/**
* Initialization callback.
*
* @since COmanage Match v1.0.0
*/
public function initialize(): void {
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 = $this->getTableLocator()->get('Matchgrids');
// 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 and Matchgrid ID for MatchgridRecordsTable
$this->getTableLocator()->setConfig('MatchgridRecords', [
'table' => $obj->prefixed_table_name,
'matchgrid_id' => $this->request->getQuery('matchgrid_id')
]);
}
/**
* 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);
$MatchgridHistory = $this->getTableLocator()->get('MatchgridHistoryRecords');
$MatchgridHistory->record($this->cur_mg->id,
$sor,
$sorid,
MatchgridActionEnum::NewMatchRequestUI,
__('match.en.MatchgridActionEnum.NEWU'),
$this->request->getEnv('REMOTE_ADDR'),
$this->request->getSession()->read('Auth.User.username'));
// 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->newEmptyEntity());
// 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 \Cake\Event\EventInterface $event Cake Event
*/
public function beforeRender(EventInterface $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 = $this->getTableLocator()->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'));
}
if($sorid != $obj->sorid) {
// As a sanity check, we don't permit the SORID to be changed, since
// MatchService does not behave consistently at the moment. (Upsert
// can't tell the row changed since we don't pass it the row ID, so it
// may create a duplicate row.) This should ultimately get fixed as
// part of CO-2138.
throw new \InvalidArgumentException(__('match.er.records.sorid.diff'));
}
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.
// Instantiate the AttributeManager
$AttributeManager = new AttributeManager();
$AttributeManager->parseFromArray($reqData);
$MatchgridHistory = $this->getTableLocator()->get('MatchgridHistoryRecords');
// 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);
$MatchgridHistory->record($this->cur_mg->id,
$sor,
$sorid,
MatchgridActionEnum::UpdateMatchRequestAPI,
__('match.en.MatchgridActionEnum.UPDU'),
$this->request->getEnv('REMOTE_ADDR'),
$this->request->getSession()->read('Auth.User.username'));
$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);
$MatchgridHistory->record($this->cur_mg->id,
$sor,
$sorid,
MatchgridActionEnum::ReferenceIDReassignedUI,
__('match.en.MatchgridActionEnum.RIRU', $obj->referenceid),
$this->request->getEnv('REMOTE_ADDR'),
$this->request->getSession()->read('Auth.User.username'));
$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']);
$MatchgridHistory->record($this->cur_mg->id,
$sor,
$sorid,
MatchgridActionEnum::ForcedReconciliationRequestUI,
__('match.en.MatchgridActionEnum.FRRU'),
$this->request->getEnv('REMOTE_ADDR'),
$this->request->getSession()->read('Auth.User.username'));
$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);
$MatchgridHistory->record($this->cur_mg->id,
$sor,
$sorid,
MatchgridActionEnum::RereconcileRequestUI,
__('match.en.MatchgridActionEnum.REDU', $obj->referenceid),
$this->request->getEnv('REMOTE_ADDR'),
$this->request->getSession()->read('Auth.User.username'));
$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);
$MatchgridHistory->record($this->cur_mg->id,
$sor,
$sorid,
MatchgridActionEnum::RereconcileRequestUI,
__('match.en.MatchgridActionEnum.REDU', "none"),
$this->request->getEnv('REMOTE_ADDR'),
$this->request->getSession()->read('Auth.User.username'));
$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->success(__('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]
]);
}
}