Skip to content
Permalink
dead5ff318
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
764 lines (626 sloc) 26.7 KB
<?php
/**
* COmanage Registry Standard 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 registry
* @since COmanage Registry v5.0.0
* @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
*/
declare(strict_types = 1);
namespace App\Controller;
use InvalidArgumentException;
use \Cake\Http\Exception\BadRequestException;
use \App\Lib\Enum\SuspendableStatusEnum;
use \App\Lib\Util\StringUtilities;
class StandardController extends AppController {
// Pagination defaults should be set in each controller
public $pagination = [];
/**
* Handle an add action for a Standard object.
*
* @since COmanage Registry v5.0.0
*/
public function add() {
// $this->name = Models (ie: from ModelsTable)
$modelsName = $this->name;
// $table = the actual table object
$table = $this->$modelsName;
// $tableName = models
$tableName = $table->getTable();
if($this->request->is('post')) {
try {
// Try to save
$obj = $table->newEntity($this->request->getData());
if($table->save($obj)) {
$this->Flash->success(__d('result', 'saved'));
// If this is a Pluggable Model, instantiate the plugin and redirect
// into the Entry Point Model
if(!empty($obj->plugin) && method_exists($this, "instantiatePlugin")) {
// instantiatePlugin() is implemented in StandardPluggableController
return $this->instantiatePlugin($obj);
}
return $this->generateRedirect($obj->id);
}
$errors = $obj->getErrors();
if(!empty($errors)) {
$this->Flash->error(__d('error', 'fields', [ implode(',',
array_map(function($v) use ($errors) {
return __d('error', 'flash', [$v, implode(',', array_values($errors[$v]))]);
},
array_keys($errors))) ]));
} else {
$this->Flash->error(__d('error', 'save', [$modelsName]));
}
}
catch(\Exception $e) {
// This throws \Cake\ORM\Exception\RolledbackTransactionException if
// aborted in afterSave
$this->Flash->error($e->getMessage());
}
// Pass $obj as context so the view can render validation errors
$this->set('vv_obj', $obj);
} else {
// Create an empty entity for FormHelper
$this->set('vv_obj', $table->newEmptyEntity());
}
// PrimaryLinkTrait, via AppController
$this->getPrimaryLink();
// AutoViewVarsTrait, via AppController
$this->populateAutoViewVars();
// Default title is add new object
$this->set('vv_title', __d('operation', 'add.a', __d('controller', $modelsName, [1])));
// Supertitle is normally the display name of the parent object when subnavigation exists.
// Set this here as the fallback default. This value is overriden in MVEAController to hold the
// name of the parent object, not the model name of the current object.
// TODO: set this to a better value for other kinds of child objects (e.g. Group member)
$this->set('vv_supertitle', __d('controller', $modelsName, [1]));
// Let the view render
$this->render('/Standard/add-edit-view');
}
/**
* Standard operations before the view is rendered.
*
* @since COmanage Registry v5.0.0
* @param EventInterface $event BeforeRender event
* @return \Cake\Http\Response HTTP Response
*/
// XXX can we merge calls ot (eg) getPrimaryLink and populateAutoViewVars here?
public function beforeRender(\Cake\Event\EventInterface $event) {
// $this->name = Models (ie: from ModelsTable)
$modelsName = $this->name;
// $table = the actual table object
$table = $this->$modelsName;
// Provide some hints to the views
$this->getFieldTypes();
$this->getRequiredFields();
// Set the display field as a view var to make it available to the views
$this->set('vv_display_field', $table->getDisplayField());
// Populate permissions info, which uses the requested object ID if one
// was provided. As a first approximation, those actions that permit lookup
// primary link are also those that pass an $id that can be used to establish
// permissions, and also Cos (which has no primary link).
$id = null;
$params = $this->request->getParam('pass');
if(!empty($params[0])) {
if((method_exists($table, "allowLookupPrimaryLink")
&& $table->allowLookupPrimaryLink($this->request->getParam('action')))
||
$modelsName == 'Cos') {
$id = (int)$params[0];
}
}
$this->set('vv_permissions', $this->RegistryAuth->calculatePermissionsForView($this->request->getParam('action'), $id));
// The template path may vary if we're in a plugin context
$vv_template_path = ROOT . DS . "templates" . DS . $modelsName;
if(!empty($this->getPlugin())) {
$vv_template_path = $this->getPluginPath($this->getPlugin(), "templates") . DS . $modelsName;
}
$this->set('vv_template_path', $vv_template_path);
return parent::beforeRender($event);
}
/**
* Handle a delete action for a Standard object.
*
* @since COmanage Registry v5.0.0
* @param Integer $id Object ID
*/
public function delete($id) {
// $this->name = Models (ie: from ModelsTable)
$modelsName = $this->name;
// $table = the actual table object
$table = $this->$modelsName;
// Allow a delete via a POST or DELETE
$this->request->allowMethod(['post', 'delete']);
// Make sure the requested object exists
try {
$obj = $table->findById($id)->firstOrFail();
// XXX throw 404 on RESTful not found?
// By default, a delete is a soft delete. The exceptions are when
// deleting a CO (AR-CO-1) or when an expunge flag is passed and
// expunge is enabled within the CO (XXX not yet implemented).
$useHardDelete = ($modelsName == "Cos");
$table->deleteOrFail($obj, ['useHardDelete' => $useHardDelete]);
// Use the display field to generate the flash message
$field = $table->getDisplayField();
if(!empty($obj->$field)) {
$this->Flash->success(__d('result', 'deleted.a', [$obj->$field]));
} else {
$this->Flash->success(__d('result', 'deleted'));
}
// Return to index since there is no delete view
return $this->generateRedirect(null);
}
catch(\Cake\ORM\Exception\PersistenceFailedException $e) {
// deleteOrFail throws Cake\ORM\Exception\PersistenceFailedException
// Application Rules that apply to the entity as a whole (or more than
// one field) can use "id" as their errorField, and we'll catch that here.
$errors = $obj->getErrors();
if(!empty($errors['id'])) {
$this->Flash->error(implode(',', array_values($errors['id'])));
} else {
$this->Flash->error($e->getMessage());
}
}
catch(\Exception $e) {
// findById throws Cake\Datasource\Exception\RecordNotFoundException
$errors = $obj->getErrors();
if(!empty($errors)) {
// Format is [field => [rule => error]]
$errstr = "";
foreach($errors as $f => $r) {
foreach($r as $rule => $err) {
$errstr .= $err . ",";
}
}
$this->Flash->error(rtrim($errstr, ","));
} else {
$this->Flash->error($e->getMessage());
}
}
// The record is still valid, so redirect back to it
return $this->redirect(['action' => 'edit', $id]);
}
/**
* Handle an edit action for a Standard object.
*
* @since COmanage Registry v5.0.0
* @param string $id Object ID
*/
public function edit(string $id) {
// $this->name = Models (ie: from ModelsTable)
$modelsName = $this->name;
// $table = the actual table object
$table = $this->$modelsName;
// $tableName = models
$tableName = $table->getTable();
// We use findById() rather than get() so we can apply subsequent
// query modifications via traits
$query = $table->findById($id);
// QueryModificationTrait
if(method_exists($this->$modelsName, "getEditContains")) {
$query = $query->contain($this->$modelsName->getEditContains());
}
try {
// Pull the current record
$obj = $query->firstOrFail();
if(method_exists($obj, "isReadOnly")) {
// If this is a read only record, redirect to view
if($obj->isReadOnly()) {
$redirect = [
'action' => 'view',
$obj->id
];
return $this->redirect($redirect);
}
}
if($this->request->is(['post', 'put'])) {
// This is an update request
$opts = [];
// AssociationTrait
/*
if(method_exists($table, "getPatchAssociated")) {
$opts['associated'] = $table->getPatchAssociated();
}*/
// $obj will have whatever editContains also pulled, but we don't want
// to save all that stuff by default, so we'll pull a new copy of the
// object without the associated data.
$saveObj = $table->findById($id)->firstOrFail();
// Attempt the update the record
$table->patchEntity($saveObj, $this->request->getData(), $opts);
// This throws \Cake\ORM\Exception\RolledbackTransactionException if aborted
// in afterSave
if($table->save($saveObj)) {
$this->Flash->success(__d('result', 'saved'));
return $this->generateRedirect((int)$id);
}
$errors = $saveObj->getErrors();
if(!empty($errors)) {
$this->Flash->error(__d('error', 'fields', [ implode(',',
array_map(function($v) use ($errors) {
return __d('error', 'flash', [$v, implode(',', array_values($errors[$v]))]);
},
array_keys($errors))) ]));
} else {
$this->Flash->error(__d('error', 'save', [$modelsName]));
}
}
}
catch(\Exception $e) {
// findById throws Cake\Datasource\Exception\RecordNotFoundException
$this->Flash->error($e->getMessage());
// XXX This redirects to an Exception page because $id is not found.
// XXX A 404 with error would be better.
return $this->generateRedirect((int)$id);
}
$this->set('vv_obj', $obj);
// XXX should we also set '$model'? cake seems to autopopulate edit fields just fine without it
// note index() uses $tableName, not 'vv_objs' or event 'vv_table_name'
// PrimaryLinkTrait
$this->getPrimaryLink();
// AutoViewVarsTrait
$this->populateAutoViewVars($obj);
if(method_exists($table, 'generateDisplayField')) {
// We don't use a trait for this since each table will implement different logic
$this->set('vv_title', __d('operation', 'edit.ai', $table->generateDisplayField($obj)));
$this->set('vv_supertitle', $table->generateDisplayField($obj));
// Pass the display field also into subtitle for dealing with External IDs
$this->set('vv_subtitle', $table->generateDisplayField($obj));
} else {
// Default view title is edit object display field
$field = $table->getDisplayField();
if(!empty($obj->$field)) {
$this->set('vv_title', __d('operation', 'edit.ai', $obj->$field));
} else {
$this->set('vv_title', __d('operation', 'edit.ai', __d('controller', $modelsName, [1])));
}
}
// Let the view render
$this->render('/Standard/add-edit-view');
}
/**
* Generate a redirect for a Standard Object operation.
*
* @since COmanage Registry v5.0.0
* @param int $id ID of object to redirect to
* @return \Cake\Http\Response
*/
public function generateRedirect(?int $id) {
$redirect = [];
// By default we return to the index, but we'll also accept "self" or "primaryLink".
$redirectGoal = $this->getRedirectGoal();
if(!$redirectGoal) {
// Our default behavior is index unless we're in a plugin context
if(!empty($this->getPlugin())) {
$redirectGoal = 'pluggableLink';
} else {
$redirectGoal = 'index';
}
}
if($redirectGoal == 'self'
&& $id
&& in_array($this->request->getParam('action'), ['add', 'edit'])) {
// Redirect to the edit view of the record just added
// (if the user has add permission, they probably have edit permission)
$redirect = [
'action' => 'edit',
$id
];
} elseif($redirectGoal == 'pluggableLink' || $redirectGoal == 'primaryLink') {
// pluggableLink and primaryLink do basically the same thing, except that
// pluggableLink moves from a plugin to core so we need to drop the plugin
$link = $this->getPrimaryLink(true);
if(!empty($link->attr) && !empty($link->value)) {
$redirect = [
'controller' => StringUtilities::foreignKeyToClassName($link->attr),
'action' => 'edit',
$link->value
];
if($redirectGoal == 'pluggableLink') {
$redirect['plugin'] = null;
}
}
} else {
// Default is to redirect to the index view
$redirect = ['action' => 'index'];
$link = $this->getPrimaryLink(true);
if(!empty($link->attr) && !empty($link->value)) {
$redirect['?'] = [$link->attr => $link->value];
}
}
return $this->redirect($redirect);
}
/**
* Make a list of fields types suitable for FieldHelper
*
* @since COmanage Registry v5.0.0
*/
protected function getFieldTypes() {
// $this->name = Models (ie: from ModelsTable)
$modelsName = $this->name;
// $table = the actual table object
$table = $this->$modelsName;
$schema = $table->getSchema();
// We don't pass the schema object as is, partly because cake might change it
// and partly to simplify access to the parts the views (FieldHelper, really)
// actually need.
// Note the schema does have field lengths for strings, but typeMap
// doesn't return them and we're not doing anything with them at the moment.
$this->set('vv_field_types', $schema->typeMap());
}
/**
* Build a list of required fields suitable for FieldHelper
*
* @since COmanage Registry v5.0.0
*/
protected function getRequiredFields() {
// $this->name = Models (ie: from ModelsTable)
$modelsName = $this->name;
// $table = the actual table object
$table = $this->$modelsName;
// Build a list of required fields for FieldHelper
$reqFields = [];
$validator = $table->getValidator();
$fields = $validator->getIterator();
foreach($fields as $name => $cfg) {
if(!$validator->isEmptyAllowed($name, ($this->request->getParam('action') == 'add'))) {
$reqFields[] = $name;
}
}
$this->set('vv_required_fields', $reqFields);
}
/**
* Generate an index for a set of Standard Objects.
*
* @since COmanage Registry v5.0.0
*/
public function index() {
// $this->name = Models
$modelsName = $this->name;
// $table = the actual table object
$table = $this->$modelsName;
// $tableName = models
$tableName = $table->getTable();
$query = null;
// PrimaryLinkTrait
$link = $this->getPrimaryLink(true);
// AutoViewVarsTrait
$this->populateAutoViewVars();
if(!empty($link->attr)) {
// If a link attribute is defined but no value is provided, then query
// where the link attribute is NULL
$query = $table->find()->where([$table->getAlias().'.'.$link->attr => $link->value]);
} else {
$query = $table->find();
}
// QueryModificationTrait
if(method_exists($table, "getIndexContains")
&& $table->getIndexContains()) {
$query->contain($table->getIndexContains());
}
// SearchFilterTrait
if(method_exists($table, "getSearchableAttributes")) {
$searchableAttributes = $table->getSearchableAttributes($this->name, $this->viewBuilder()->getVar('vv_tz'));
if(!empty($searchableAttributes)) {
// Here we iterate over the attributes, and we add a new where clause for each one
foreach(array_keys($searchableAttributes) as $attribute) {
if(!empty($this->request->getQuery($attribute))) {
$query = $table->whereFilter($query, $attribute, $this->request->getQuery($attribute));
} elseif (!empty($this->request->getQuery($attribute . "_starts_at"))
|| !empty($this->request->getQuery($attribute . "_ends_at"))) {
$search_date = [];
// We allow empty for dates since we might refer to infinity (from whenever or to always)
$search_date[] = $this->request->getQuery($attribute . "_starts_at") ?? "";
$search_date[] = $this->request->getQuery($attribute . "_ends_at") ?? "";
$query = $table->whereFilter($query, $attribute, $search_date);
}
}
$this->set('vv_searchable_attributes', $searchableAttributes);
}
}
// Filter on requested filter, if requested
// QueryModificationTrait
if(method_exists($table, "getIndexFilter")) {
$filter = $table->getIndexFilter();
if(is_callable($filter)) {
$query->where($filter($this->request));
} else {
$query->where($table->getIndexFilter());
}
}
$resultSet = $this->paginate($query);
$this->set($tableName, $resultSet);
$this->set('vv_permission_set', $this->RegistryAuth->calculatePermissionsForResultSet($resultSet));
// Default index view title is model name
$this->set('vv_title', __d('controller', $modelsName, [99]));
// Let the view render
$this->render('/Standard/index');
}
/**
* Populate any auto view variables, as requested via AutoViewVarsTrait.
*
* @since COmanage Registry v5.0.0
* @param object $obj Current object (eg: from edit), if set
*/
protected function populateAutoViewVars(object $obj=null) {
// $this->name = Models
$modelsName = $this->name;
// $table = the actual table object
$table = $this->$modelsName;
// Populate certain view vars (eg: selects) automatically.
// AutoViewVarsTrait
if(method_exists($table, "getAutoViewVars")
&& $table->getAutoViewVars()) {
foreach($table->getAutoViewVars() as $vvar => $avv) {
switch($avv['type']) {
case 'array':
// Use the provided array of values. By default, we use the values
// for the keys as well, to generate HTML along the lines of
// <option value="Foo">"Foo"</option>
$this->set($vvar, array_combine($avv['array'], $avv['array']));
break;
case 'enum':
// We just want the localized text strings for the defined constants
$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. "type" is a shorthand for "select" for type_id.
case 'type':
// Inject configuration. Since we're only ever looking at the types
// table, inject the current CO along with the requested attribute
$avv['model'] = 'Types';
$avv['where'] = [
'attribute' => $avv['attribute'],
'status' => SuspendableStatusEnum::Active
];
// fall through
case 'auxiliary':
// XXX add list as in match?
case 'select':
// We assume $modelName has a direct relationship to $avv['model']
$avvmodel = $avv['model'];
$this->$avvmodel = $this->fetchTable($avvmodel);
if($avv['type'] == 'auxiliary') {
$query = $this->$avvmodel->find();
} else {
$query = $this->$avvmodel->find('list');
}
if(!empty($avv['find'])) {
if($avv['find'] == 'filterPrimaryLink') {
// We're filtering the requested model, not our current model.
// See if the requested key is available, and if so run the find.
$linkFilter = $table->getPrimaryLink();
if($linkFilter) {
// Try to find the $linkFilter value
$v = null;
// We might have been passed an object with the current value
if($obj && !empty($obj->$linkFilter)) {
$v = $obj->$linkFilter;
} elseif(!empty($this->request->getQuery($linkFilter))) {
$v = $this->request->getQuery($linkFilter);
}
// XXX also need to check getData()?
// XXX shouldn't this use $this->getPrimaryLink() instead? Or maybe move $this->primaryLink
// to PrimaryLinkTrait and call it there?
if($v) {
$avv['where'][$table->getAlias().'.'.$linkFilter] = $v;
//$query = $query->where([$table->getAlias().'.'.$linkFilter => $v]);
}
}
} else {
// Use the specified finder, if configured
$query = $query->find($avv['find']);
}
} else {
// XXX is this the best logic? maybe some relation to filterPrimaryLink?
// By default, filter everything on CO ID
$avv['where']['co_id'] = $this->getCOID();
//$query = $query->where([$table->getAlias().'.co_id' => $this->getCOID()]);
}
if(!empty($avv['where'])) {
// Filter on the specified clause (of the form [column=>value])
$query = $query->where($avv['where']);
}
// Sort the list by display field
if(!empty($avv['model']) && method_exists($this->$avvmodel, "getDisplayField")) {
$query->order([$this->$avvmodel->getDisplayField() => 'ASC']);
} elseif(method_exists($table, "getDisplayField")) {
$query->order([$table->getDisplayField() => 'ASC']);
}
$this->set($vvar, $query->toArray());
break;
case 'parent':
$modelsName = $this->name;
// $table = the actual table object
$table = $this->$modelsName;
// XXX We assume that all models that load the Tree behavior will
// implement a potentialParents method
$this->set($vvar, $table->potentialParents($this->getCOID()));
break;
case 'plugin':
$PluginTable = $this->getTableLocator()->get('Plugins');
$this->set($vvar, $PluginTable->getActivePluginModels($avv['pluginType']));
break;
default:
// XXX I18n? and in match?
throw new \LogicException('Unknonwn Auto View Var Type {0}', [$avv['type']]);
break;
}
}
}
}
/**
* Handle a view action for a Standard object.
*
* @since COmanage Registry v5.0.0
* @param Integer $id Object ID
*/
public function view($id = null) {
// $this->name = Models
$modelsName = $this->name;
// $table = the actual table object
$table = $this->$modelsName;
// $tableName = models
$tableName = $table->getTable();
// We use findById() rather than get() so we can apply subsequent
// query modifications via traits
$query = $table->findById($id);
// QueryModificationTrait
if(method_exists($table, "getViewContains")) {
$query = $query->contain($table->getViewContains());
}
try {
// Pull the current record
$obj = $query->firstOrFail();
}
catch(\Exception $e) {
// findById throws Cake\Datasource\Exception\RecordNotFoundException
$this->Flash->error($e->getMessage());
// XXX This redirects to an Exception page because $id is not found.
// XXX A 404 with error would be better.
return $this->generateRedirect((int)$id);
}
$this->set('vv_obj', $obj);
// PrimaryLinkTrait
$this->getPrimaryLink();
// AutoViewVarsTrait
// We still used this in view() to map select values
$this->populateAutoViewVars($obj);
if(method_exists($table, 'generateDisplayField')) {
// We don't use a trait for this since each table will implement different logic
$this->set('vv_title', __d('operation', 'view.ai', $table->generateDisplayField($obj)));
$this->set('vv_supertitle', $table->generateDisplayField($obj));
// Pass the display field also into subtitle for dealing with External IDs
$this->set('vv_subtitle', $table->generateDisplayField($obj));
} else {
// Default view title is the object display field
$field = $table->getDisplayField();
if(!empty($obj->$field)) {
$this->set('vv_title', __d('operation', 'view.ai', $obj->$field));
} else {
$this->set('vv_title', __d('operation', 'view.ai', __d('controller', $modelsName, [1])));
}
}
// Let the view render
$this->render('/Standard/add-edit-view');
}
}