diff --git a/app/src/Controller/AppController.php b/app/src/Controller/AppController.php index afe6c2366..ce508fd5f 100644 --- a/app/src/Controller/AppController.php +++ b/app/src/Controller/AppController.php @@ -151,8 +151,8 @@ public function beforeRender(\Cake\Event\Event $event) { */ protected function getPrimaryLink(bool $lookup=false) { - // Did we already figure this out? - if($this->cur_pl) { + // Did we already figure this out? (But only if $lookup) + if($lookup && isset($this->cur_pl['linkvalue'])) { return $this->cur_pl; } @@ -171,28 +171,50 @@ protected function getPrimaryLink(bool $lookup=false) { if($lookup) { // Try to find a value - if($this->request->getQuery($this->cur_pl['linkattr'])) { - $this->cur_pl['linkvalue'] = $this->request->getQuery($this->cur_pl['linkattr']); - } elseif($this->request->getData($this->cur_pl['linkattr'])) { - $this->cur_pl['linkvalue'] = $this->request->getData($this->cur_pl['linkattr']); - } elseif($this->request->getData($modelName . "." . $this->cur_pl['linkattr'])) { - $this->cur_pl['linkvalue'] = $this->request->getData($modelName . "." . $this->cur_pl['linkattr']); - } else { - // Try to map via the requested ID, if known - $param = (int)$this->request->getParam('pass.0'); - - if(!empty($param)) { - $this->cur_pl['linkvalue'] = $this->$modelsName->calculateMatchgridId($param); + + 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) + if($this->$modelsName->allowUnkeyedPrimaryLink($this->request->getParam('action')) + && $this->request->getQuery($this->cur_pl['linkattr'])) { + $this->cur_pl['linkvalue'] = $this->request->getQuery($this->cur_pl['linkattr']); + } 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)) { + $this->cur_pl['linkvalue'] = $this->$modelsName->calculatePrimaryLinkId($param); + } } - - if(!$this->cur_pl['linkvalue'] && !$this->$modelsName->allowEmptyPrimaryLink()) { - throw new \RuntimeException(__('match.er.primary_link', [ $this->cur_pl['linkattr'] ])); + } elseif($this->request->is('post') || $this->request->is('put')) { + // Look in the data for the primary link ID + if(!empty($this->request->getData($this->cur_pl['linkattr']))) { + $this->cur_pl['linkvalue'] = $this->request->getData($this->cur_pl['linkattr']); + } elseif(!empty($this->request->getData($modelName . "." . $this->cur_pl['linkattr']))) { + $this->cur_pl['linkvalue'] = $this->request->getData($modelName . "." . $this->cur_pl['linkattr']); + } 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)) { + $this->cur_pl['linkvalue'] = $this->$modelsName->calculatePrimaryLinkId($param); + } } } + + if(empty($this->cur_pl['linkvalue']) && !$this->$modelsName->allowEmptyPrimaryLink()) { + throw new \RuntimeException(__('match.er.primary_link', [ $this->cur_pl['linkattr'] ])); + } } if(!empty($this->cur_pl['linkvalue'])) { - $this->set('vv_primary_link_id', $this->cur_pl['linkvalue']); + // Look up the link value to find the related entity + + $linkModelName = $this->$modelsName->getPrimaryLinkTableName(); + $linkModel = TableRegistry::get($linkModelName); + + $this->set('vv_primary_link_model', $linkModelName); + $this->set('vv_primary_link_obj', $linkModel->findById($this->cur_pl['linkvalue'])->firstOrFail()); } } @@ -213,6 +235,8 @@ protected function setMatchgrid() { // $this->name = Models $modelsName = $this->name; + // $modelName = Model + $modelName = \Cake\Utility\Inflector::singularize($this->name); if(!method_exists($this->$modelsName, "requiresMatchgrid") || !$this->$modelsName->requiresMatchgrid()) { @@ -220,8 +244,8 @@ protected function setMatchgrid() { return; } - // Not all models have matchgrid as their primary link. This will trigger - // setting of the viewVar for breadcrumbs and anything else. + // Not all models have matchgrid as their primary link. This will also + // trigger setting of the viewVar for breadcrumbs and anything else. // PrimaryLinkTrait $link = $this->getPrimaryLink(true); @@ -229,42 +253,53 @@ protected function setMatchgrid() { $mgid = null; if($this->request->is('get')) { - // If this action allows unkeyed, asserted matchgrid IDs, check the query string - // (eg: 'add' or 'index' allow matchgrid_id to be passed in) - if($this->$modelsName->allowUnkeyedMatchgrid($this->request->getParam('action'))) { - $mgid = $this->request->getQuery('matchgrid_id'); - } - } - - if($this->request->is('post')) { - // Accept the matchgrid ID from the posted data - $mgid = $this->request->getData('matchgrid_id'); - } - - if(!$mgid) { - // Try to map the requested object ID - $param = (int)$this->request->getParam('pass.0'); - - if(!empty($param)) { - try { - $mgid = $this->$modelsName->calculateMatchgridId($param); + // 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; + } } - catch(Exception $e) { - // Ignore errors and keep trying + } 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) { - // Use the primary link (if found) to calculate the matchgrid ID - - if(!empty($link['linkvalue'])) { - $m = \Cake\Utility\Inflector::pluralize( - \Cake\Utility\Inflector::camelize( - substr($link['linkattr'], 0, strlen($link['linkattr'])-3))); + } elseif($this->request->is('post') || $this->request->is('put')) { + 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); + } + } + } else { + // We already have the primary link object in a viewvar - $linkModel = TableRegistry::get($m); - $mgid = $linkModel->calculateMatchgridId((int)$link['linkvalue']); + $plObj = $this->viewVars['vv_primary_link_obj']; + + if(!empty($plObj->matchgrid_id)) { + $mgid = $plObj->matchgrid_id; + } } } diff --git a/app/src/Lib/Traits/MatchgridLinkTrait.php b/app/src/Lib/Traits/MatchgridLinkTrait.php index 8fafd9bfd..fe6f61231 100644 --- a/app/src/Lib/Traits/MatchgridLinkTrait.php +++ b/app/src/Lib/Traits/MatchgridLinkTrait.php @@ -29,6 +29,8 @@ namespace App\Lib\Traits; +use Cake\ORM\TableRegistry; + trait MatchgridLinkTrait { // Does the associated model require a matchgrid ID? private $requiresMatchgrid = false; @@ -36,9 +38,6 @@ trait MatchgridLinkTrait { // If we normally require a matchgrid, can we proceed without one? private $allowEmptyMatchgrid = false; - // Actions that can have an unkeyed (ie: self asserted) matchgrid ID - private $unkeyedActions = ['add', 'index']; - /** * If the associated controller normally requires a Matchgrid ID, whether the * Matchgrid ID can be empty. @@ -50,18 +49,6 @@ trait MatchgridLinkTrait { public function allowEmptyMatchgrid() { return $this->allowEmptyMatchgrid; } - - /** - * Check to see whether the specified action is allowed to assert a matchgrid ID - * directly (ie: not via lookup of an associated record). - * - * @param string $action Action - * @return boolean true if permitted, false otherwise - */ - - public function allowUnkeyedMatchgrid(string $action) { - return in_array($action, $this->unkeyedActions, true); - } /** * Calculate the Matchgrid ID associated with the requested object ID. @@ -73,11 +60,36 @@ public function allowUnkeyedMatchgrid(string $action) { */ public function calculateMatchgridId(int $id) { + // First see if the object has a direct link to Matchgrid. + // Even if it doesn't, we'll still need the object's primary link. // For now we assume we have a direct foreign key to Matchgrids. $obj = $this->findById($id)->firstOrFail(); - return $obj->matchgrid_id; + if(!empty($obj->matchgrid_id)) { + return $obj->matchgrid_id; + } + + // This model probably has a primary key that isn't matchgrid. + + if(method_exists($this, "getPrimaryLink")) { + $linkAttr = $this->getPrimaryLink(); + + if(!empty($obj->$linkAttr)) { + // Look up $linkAttr to get its Matchgrid. We currently assume only + // one level of nesting. + + $linkTable = TableRegistry::get($this->getPrimaryLinkTableName()); + + return $linkTable->calculateMatchgridId($obj->$linkAttr); + } + } + + if($this->requiresMatchgrid && !$this->allowEmptyMatchgrid) { + throw new \LogicException("MatchgridLinkTrait::calculateMatchgridId()"); + } + + return null; } /** diff --git a/app/src/Lib/Traits/PrimaryLinkTrait.php b/app/src/Lib/Traits/PrimaryLinkTrait.php index f1e74a662..ba5afb7e5 100644 --- a/app/src/Lib/Traits/PrimaryLinkTrait.php +++ b/app/src/Lib/Traits/PrimaryLinkTrait.php @@ -33,9 +33,18 @@ trait PrimaryLinkTrait { // Primary Link field (eg: model:matchgrid_id) private $primaryLink = null; + // Primary Link table name (eg: Matchgrids) + private $primaryLinkTable = null; + // Allow empty primary link? private $allowEmpty = false; + // Actions that can have an unkeyed (ie: self asserted) primary link ID + private $unkeyedActions = ['add', 'index']; + + // Actions where the primary link can be obtained by looking up the record ID + private $lookupActions = ['delete', 'edit', 'view']; + /** * Whether the primary link is permitted to be empty. * @@ -47,6 +56,32 @@ public function allowEmptyPrimaryLink() { return $this->allowEmpty; } + /** + * Check to see whether the specified action allows a record ID to be passed + * in the URL, which can be used to lookup a primary link. + * + * @since COmanage Match v1.0.0 + * @param string $action Action + * @return boolean true if permitted, false otherwise + */ + + public function allowLookupPrimaryLink(string $action) { + return in_array($action, $this->lookupActions, true); + } + + /** + * Check to see whether the specified action is allowed to assert a primary link ID + * directly (ie: not via lookup of an associated record). + * + * @since COmanage Match v1.0.0 + * @param string $action Action + * @return boolean true if permitted, false otherwise + */ + + public function allowUnkeyedPrimaryLink(string $action) { + return in_array($action, $this->unkeyedActions, true); + } + /** * Calculate the Primary Link ID associated with the requested object ID. * @@ -58,9 +93,8 @@ public function allowEmptyPrimaryLink() { public function calculatePrimaryLinkId(int $id) { $obj = $this->findById($id)->firstOrFail(); - //select([$this->primaryLink]); - return $obj->${$this->primaryLink}; + return $obj->{$this->primaryLink}; } /** @@ -87,6 +121,17 @@ public function getPrimaryLink() { return $this->primaryLink; } + /** + * Obtain the primary link's table name. + * + * @since COmanage Match v1.0.0 + * @return string Primary link table name + */ + + public function getPrimaryLinkTableName() { + return $this->primaryLinkTable; + } + /** * Set whether the primary link is permitted to be empty. * @@ -98,6 +143,17 @@ public function setAllowEmptyPrimaryLink(bool $allowEmpty) { $this->allowEmpty = $allowEmpty; } + /** + * Set whether the primary link can be resolved via the object ID in the URL. + * + * @since COmanage Match v1.0.0 + * @param boolean $allowEmpty true if the primary link can be resolved via the URL ID + */ + + public function setAllowLookupPrimaryLink(array $actions) { + $this->lookupActions = array_merge($this->lookupActions, $actions); + } + /** * Set the primary link attribute. * @@ -107,5 +163,10 @@ public function setAllowEmptyPrimaryLink(bool $allowEmpty) { public function setPrimaryLink($field) { $this->primaryLink = $field; + + // Calculate the table name for future reference + if(preg_match('/^(.*?)_id$/', $field, $f)) { + $this->primaryLinkTable = \Cake\Utility\Inflector::camelize(\Cake\Utility\Inflector::pluralize($f[1])); + } } } diff --git a/app/src/Model/Table/MatchgridsTable.php b/app/src/Model/Table/MatchgridsTable.php index 647850f8c..d9b996f37 100644 --- a/app/src/Model/Table/MatchgridsTable.php +++ b/app/src/Model/Table/MatchgridsTable.php @@ -41,6 +41,7 @@ class MatchgridsTable extends Table { use \App\Lib\Traits\AutoViewVarsTrait; use \App\Lib\Traits\MatchgridLinkTrait; + use \App\Lib\Traits\PrimaryLinkTrait; /** * Perform Cake Model initialization. @@ -90,6 +91,8 @@ public function initialize(array $config) { 'class' => 'StatusEnum' ] ]); + + $this->setAllowLookupPrimaryLink(['manage', 'pending', 'reconcile']); } /** diff --git a/app/src/Template/Element/breadcrumbs.ctp b/app/src/Template/Element/breadcrumbs.ctp index 63ee620a7..0f57a5a1c 100644 --- a/app/src/Template/Element/breadcrumbs.ctp +++ b/app/src/Template/Element/breadcrumbs.ctp @@ -58,6 +58,31 @@ if($this->request->getRequestTarget(false) != '/') { ); } + if(!empty($vv_primary_link_obj) + && !empty($vv_primary_link_model) + && $vv_primary_link_model != 'Matchgrids') { + // If the primary link is not matchgrid, render a link to it (and the parent index). + // We'll need to calculate the controller name. + + if(!empty($vv_cur_mg->id)) { + // We currently assume the parent of a primary link is matchgrid, which + // might or might not always be true in the future. + $this->Breadcrumbs->add( + __('match.ct.'.\Cake\Utility\Inflector::underscore($vv_primary_link_model), [99]), + ['controller' => \Cake\Utility\Inflector::dasherize($vv_primary_link_model), + 'action' => 'index', + 'matchgrid_id' => $vv_cur_mg->id] + ); + } + + $this->Breadcrumbs->add( + $vv_primary_link_obj->name, + ['controller' => \Cake\Utility\Inflector::dasherize($vv_primary_link_model), + 'action' => 'edit', + $vv_primary_link_obj->id] + ); + } + if($action != 'index' && ($modelsName != 'Matchgrids' && $action != 'pending')) { // Default parent is index, to which we might need to append the Primary Link ID @@ -67,8 +92,8 @@ if($this->request->getRequestTarget(false) != '/') { 'action' => 'index' ]; - if(!empty($vv_primary_link) && !empty($vv_primary_link_id)) { - $target[$vv_primary_link] = $vv_primary_link_id; + if(!empty($vv_primary_link) && !empty($vv_primary_link_obj->id)) { + $target[$vv_primary_link] = $vv_primary_link_obj->id; } $this->Breadcrumbs->add(