<?php
/**
* @file classes/controllers/grid/GridHandler.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class GridHandler
*
* @ingroup classes_controllers_grid
*
* @brief This class defines basic operations for handling HTML grids. Grids
* are used to implement a standardized listing of elements, as would commonly
* be laid out in an HTML table, permitting rows, columns, row actions (such
* as "delete" and "edit" actions, which operate on a single row), and grid
* actions (such as "new element", which operates on the grid as a whole), and
* other functionality to be implemented consistently.
*
* An implemented grid consists of several classes, with a subclass of
* GridHandler as the centerpiece. Each row is described by an instance of a
* GridRow, which is generally extended for the row in question; each column
* is described by an instance of GridColumn (for which several generic columns
* are implemented). Often grids will make use of a specific subclass of
* DataProvider in order to prepare data for display in the grid.
*
* Actions (be they row or grid actions) are implemented by LinkAction
* instances.
*
* There are several subclasses of GridHandler that provide generalized grids
* of particular forms, such as CategoryGridHandler and ListbuilderHandler.
*
* The JavaScript front-end is described at <https://pkp.sfu.ca/wiki/index.php?title=JavaScript_widget_controllers#Grids>.
*
* For a concrete example of a grid handler (and related classes), see
* AnnouncementTypeGridHandler.
*/
namespace PKP\controllers\grid;
use APP\template\TemplateManager;
use Illuminate\Support\Enumerable;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use PKP\controllers\grid\files\FilesGridDataProvider;
use PKP\core\ItemIterator;
use PKP\core\JSONMessage;
use PKP\core\PKPRequest;
use PKP\db\DBResultRange;
use PKP\form\Form;
use PKP\handler\PKPHandler;
use PKP\linkAction\LinkAction;
use PKP\linkAction\request\NullAction;
use PKP\plugins\Hook;
use PKP\template\PKPTemplateManager;
class GridHandler extends PKPHandler
{
public const GRID_ACTION_POSITION_DEFAULT = 'default';
public const GRID_ACTION_POSITION_ABOVE = 'above';
public const GRID_ACTION_POSITION_LASTCOL = 'lastcol';
public const GRID_ACTION_POSITION_BELOW = 'below';
/** @var string grid title locale key */
public $_title = '';
/** @var string empty row locale key */
public $_emptyRowText = 'grid.noItems';
/** @var string Grid foot note locale key */
public $_footNote = '';
/** @var GridDataProvider */
public $_dataProvider;
/**
* @var array Grid actions. The first key represents
* the position of the action in the grid, the second key
* represents the action id.
*/
public $_actions = [self::GRID_ACTION_POSITION_DEFAULT => []];
/** @var array The GridColumns of this grid. */
public $_columns = [];
/** @var array The grid's data source. */
public $_data;
/** @var ItemIterator The item iterator to be used for paging. */
public $_itemIterator;
/** @var string The grid template. */
public $_template;
/** @var array The urls that will be used in JS handler. */
public $_urls;
/** @var array The grid features. */
public $_features;
/** @var array Constants that should be passed to the template */
public $_constants = [];
/**
* Constructor.
*
* @param GridDataProvider $dataProvider An optional data provider
* for the grid. If no data provider is given then the grid
* assumes that child classes will override default method
* implementations.
*/
public function __construct($dataProvider = null)
{
$this->_dataProvider = $dataProvider;
parent::__construct();
}
//
// Getters and Setters
//
/**
* Get the data provider.
*
* @return FilesGridDataProvider
*/
public function getDataProvider()
{
return $this->_dataProvider;
}
/**
* Get the grid request parameters. These
* are the parameters that uniquely identify the
* data within a grid.
*
* NB: You should make sure to authorize and/or
* validate parameters before you publish them
* through this interface. Callers will assume that
* data accessed through this method will not have
* to be sanitized.
*
* The default implementation tries to retrieve
* request parameters from a data provider if there
* is one.
*
* @return array
*/
public function getRequestArgs()
{
$dataProvider = $this->getDataProvider();
$requestArgs = [];
if ($dataProvider instanceof GridDataProvider) {
$requestArgs = $dataProvider->getRequestArgs();
}
$this->callFeaturesHook('getRequestArgs', ['grid' => &$this, 'requestArgs' => &$requestArgs]);
return $requestArgs;
}
/**
* Get a single grid request parameter.
*
* @see getRequestArgs()
*
* @param string $key The name of the parameter to retrieve.
*/
public function getRequestArg($key)
{
$requestArgs = $this->getRequestArgs();
assert(isset($requestArgs[$key]));
return $requestArgs[$key];
}
/**
* Get the grid title.
*
* @return string locale key
*/
public function getTitle()
{
return $this->_title;
}
/**
* Set the grid title.
*
* @param string $title locale key
*/
public function setTitle($title)
{
$this->_title = $title;
}
/**
* Get the no items locale key
*
* @return string locale key
*/
public function getEmptyRowText()
{
return $this->_emptyRowText;
}
/**
* Set the no items locale key
*
* @param string $emptyRowText locale key
*/
public function setEmptyRowText($emptyRowText)
{
$this->_emptyRowText = $emptyRowText;
}
/**
* Get the grid foot note.
*
* @return string locale key
*/
public function getFootNote()
{
return $this->_footNote;
}
/**
* Set the grid foot note.
*
* @param string $footNote locale key
*/
public function setFootNote($footNote)
{
$this->_footNote = $footNote;
}
/**
* Get all actions for a given position within the grid.
*
* @param string $position The position of the actions.
*
* @return array The LinkActions for the given position.
*/
public function getActions($position = self::GRID_ACTION_POSITION_ABOVE)
{
if (!isset($this->_actions[$position])) {
return [];
}
return $this->_actions[$position];
}
/**
* Add an action.
*
* @param mixed $action a single action.
* @param string $position The position of the action.
*/
public function addAction($action, $position = self::GRID_ACTION_POSITION_ABOVE)
{
if (!isset($this->_actions[$position])) {
$this->_actions[$position] = [];
}
$this->_actions[$position][$action->getId()] = $action;
}
/**
* Get all columns.
*
* @return array An array of GridColumn instances.
*/
public function &getColumns()
{
return $this->_columns;
}
/**
* Retrieve a single column by id.
*
* @param int $columnId
*
* @return GridColumn
*/
public function getColumn($columnId)
{
assert(isset($this->_columns[$columnId]));
return $this->_columns[$columnId];
}
/**
* Get columns by flag.
*
* @param string $flag
*
* @return array
*/
public function &getColumnsByFlag($flag)
{
$columns = [];
foreach ($this->getColumns() as $column) {
if ($column->hasFlag($flag)) {
$columns[$column->getId()] = $column;
}
}
return $columns;
}
/**
* Get columns number. If a flag is passed, the columns
* using it will not be counted.
*
* @param string $flag optional
*
* @return int
*/
public function getColumnsCount($flag = null)
{
$count = 0;
foreach ($this->getColumns() as $column) {
if (!$column->hasFlag($flag)) {
$count++;
}
}
return $count;
}
/**
* Checks whether a column exists.
*
* @param int $columnId
*
* @return bool
*/
public function hasColumn($columnId)
{
return isset($this->_columns[$columnId]);
}
/**
* Add a column.
*
* @param mixed $column A single GridColumn instance.
*/
public function addColumn($column)
{
assert($column instanceof \PKP\controllers\grid\GridColumn);
$this->_columns[$column->getId()] = $column;
}
/**
* Get the grid data.
*
* @param PKPRequest $request
*
* @return array
*/
public function &getGridDataElements($request)
{
$filter = $this->getFilterSelectionData($request);
// Try to load data if it has not yet been loaded.
if (is_null($this->_data)) {
$data = $this->loadData($request, $filter);
if (is_null($data)) {
// Initialize data to an empty array.
$data = [];
}
$this->setGridDataElements($data);
}
$this->callFeaturesHook('getGridDataElements', ['request' => &$request, 'grid' => &$this, 'gridData' => &$this->_data, 'filter' => &$filter]);
return $this->_data;
}
/**
* Check whether the grid has rows.
*
* @return bool
*/
public function hasGridDataElements($request)
{
$data = & $this->getGridDataElements($request);
assert(is_array($data));
return (bool) count($data);
}
/**
* Set the grid data.
*
* @param mixed $data an array or ItemIterator with element data
*/
public function setGridDataElements($data)
{
$this->callFeaturesHook('setGridDataElements', ['grid' => &$this, 'data' => &$data]);
if ($data instanceof Enumerable) {
$this->_data = $this->toAssociativeArray($data);
} elseif (is_iterable($data)) {
$this->_data = $data;
} elseif ($data instanceof \PKP\db\DAOResultFactory) {
$this->_data = $data->toAssociativeArray();
} elseif ($data instanceof ItemIterator) {
$this->_data = $data->toArray();
} else {
assert(false);
}
}
/**
* Get the grid template.
*
* @return string
*/
public function getTemplate()
{
if (is_null($this->_template)) {
$this->setTemplate('controllers/grid/grid.tpl');
}
return $this->_template;
}
/**
* Set the grid template.
*
* @param string $template
*/
public function setTemplate($template)
{
$this->_template = $template;
}
/**
* Return all grid urls that will be used
* in JS handler.
*
* @return array
*/
public function getUrls()
{
return $this->_urls;
}
/**
* Define the urls that will be used
* in JS handler.
*
* @param PKPRequest $request
* @param array $extraUrls Optional extra urls.
*/
public function setUrls($request, $extraUrls = [])
{
$router = $request->getRouter();
$urls = [
'fetchGridUrl' => $router->url($request, null, null, 'fetchGrid', null, $this->getRequestArgs()),
'fetchRowsUrl' => $router->url($request, null, null, 'fetchRows', null, $this->getRequestArgs()),
'fetchRowUrl' => $router->url($request, null, null, 'fetchRow', null, $this->getRequestArgs())
];
$this->_urls = array_merge($urls, $extraUrls);
}
/**
* Override this method to return true if you want
* to use the grid within another component (e.g. to
* remove the title or change the layout accordingly).
*
* @return bool
*/
public function getIsSubcomponent()
{
return false;
}
/**
* Get all grid attached features.
*
* @return array
*/
public function getFeatures()
{
return $this->_features;
}
/**
* Get the item iterator that represents this grid data.
* Should only be used for retrieving paging data.
* See #6498.
*
* @return ItemIterator
*/
public function getItemIterator()
{
return $this->_itemIterator;
}
/**
* Get "publish data changed" event list.
*
* @return array
*/
public function getPublishChangeEvents()
{
return [];
}
// FIXME: Since we've moved to PHP5, maybe those methods
// should be moved into interfaces like OrderableItems
// and SelectableItems. Then each grid can implement
// them in a clear way. It will also simplify this base
// class hiding optional interfaces.
//
// Orderable items.
//
/**
* Override to return the data element sequence value.
*
*
* @return int
*/
public function getDataElementSequence($gridDataElement)
{
return 0; // Ordering is ambiguous or irrelevant.
}
/**
* Override to set the data element new sequence.
*
* @param PKPRequest $request
* @param int $rowId
* @param int $newSequence
*/
public function setDataElementSequence($request, $rowId, $gridDataElement, $newSequence)
{
assert(false);
}
//
// Selectable items.
//
/**
* Returns the current selection state
* of the grid data element.
*
*
* @return bool
*/
public function isDataElementSelected($gridDataElement)
{
assert(false);
}
/**
* Get the select parameter name to store
* the selected files.
*
* @return string
*/
public function getSelectName()
{
assert(false);
}
/**
* Tries to identify the data element in the grids
* data source that corresponds to the requested row id.
* Raises a fatal error if such an element cannot be
* found.
*
* @param PKPRequest $request
* @param array $args
*
* @return ?\PKP\controllers\grid\GridRow the requested grid row, already
* configured with id and data or null if the row
* could not been found.
*/
public function getRequestedRow($request, $args)
{
$isModified = isset($args['modify']);
if (isset($args['rowId']) && !$isModified) {
// A row ID was specified. Fetch it
$elementId = $args['rowId'];
// Retrieve row data for the requested row id
$dataElement = $this->getRowDataElement($request, $elementId);
if (is_null($dataElement)) {
// If the row doesn't exist then
// return null. It may be that the
// row has been deleted in the meantime
// and the client does not yet know about this.
$nullVar = null;
return $nullVar;
}
} elseif ($isModified) {
$elementId = null;
// The row is modified. The client may be asking
// for a formatted new entry, to be saved later, or
// for a representation of a modified row.
$dataElement = $this->getRowDataElement($request, $elementId);
if (isset($args['rowId'])) {
// the rowId holds the elementId being modified
$elementId = $args['rowId'];
}
}
// Instantiate a new row
return $this->_getInitializedRowInstance($request, $elementId, $dataElement, $isModified);
}
/**
* Render the passed row and return its markup.
*
* @param PKPRequest $request
* @param \PKP\controllers\grid\GridRow $row
*
* @return string
*/
public function renderRow($request, $row)
{
$this->setFirstDataColumn();
return $this->renderRowInternally($request, $row);
}
/**
* Get grid range info.
*
* @param PKPRequest $request
* @param string $rangeName The grid id.
* @param null|mixed $contextData
*
* @return DBResultRange
*/
public function getGridRangeInfo($request, $rangeName, $contextData = null)
{
$rangeInfo = parent::getRangeInfo($request, $rangeName, $contextData);
$this->callFeaturesHook('getGridRangeInfo', ['request' => &$request, 'grid' => &$this, 'rangeInfo' => $rangeInfo]);
return $rangeInfo;
}
//
// Overridden methods from PKPHandler
//
/**
* @copydoc PKPHandler::authorize()
*/
public function authorize($request, &$args, $roleAssignments)
{
$dataProvider = $this->getDataProvider();
$hasDataProvider = $dataProvider instanceof \PKP\controllers\grid\GridDataProvider;
if ($hasDataProvider) {
$this->addPolicy($dataProvider->getAuthorizationPolicy($request, $args, $roleAssignments));
}
$success = parent::authorize($request, $args, $roleAssignments);
if ($hasDataProvider && $success === true) {
$dataProvider->setAuthorizedContext($this->getAuthorizedContext());
}
return $success;
}
/**
* @see PKPHandler::initialize()
*
* @param PKPRequest $request
* @param array $args optional
*/
public function initialize($request, $args = null)
{
parent::initialize($request);
if ($this->getFilterForm() && $this->isFilterFormCollapsible()) {
$this->addAction(
new LinkAction(
'search',
new NullAction(),
__('common.search'),
'search_extras_expand'
)
);
}
// Give a chance to grid add features before calling hooks.
// Because we must control when features are added to a grid,
// this is the only place that should use the _addFeature() method.
$this->_addFeatures($this->initFeatures($request, $args));
$this->callFeaturesHook('gridInitialize', ['grid' => &$this]);
}
//
// Public handler methods
//
/**
* Render the entire grid controller and send
* it to the client.
*
* @param array $args
* @param PKPRequest $request
*
* @return JSONMessage JSON object
*/
public function fetchGrid($args, $request)
{
$this->checkIfResetActionsNeeded($request);
$this->setUrls($request);
// Prepare the template to render the grid.
$templateMgr = TemplateManager::getManager($request);
$templateMgr->assign('grid', $this);
$templateMgr->assign('request', $request);
// Add rendered filter
$renderedFilter = $this->renderFilter($request);
$templateMgr->assign('gridFilterForm', $renderedFilter);
// Add columns.
$this->setFirstDataColumn();
$columns = $this->getColumns();
$templateMgr->assign('columns', $columns);
$this->_fixColumnWidths();
// Do specific actions to fetch this grid.
$this->doSpecificFetchGridActions($args, $request, $templateMgr);
// Assign additional params for the fetchRow and fetchGrid URLs to use.
$templateMgr->assign('gridRequestArgs', $this->getRequestArgs());
$this->callFeaturesHook('fetchGrid', ['grid' => &$this, 'request' => &$request]);
// Assign features.
$templateMgr->assign('features', $this->getFeatures());
// Assign constants.
$templateMgr->assign('gridConstants', $this->_constants);
// Let the view render the grid.
return new JSONMessage(true, $templateMgr->fetch($this->getTemplate()));
}
/**
* Fetch all grid rows from loaded data.
*
* @param array $args
* @param PKPRequest $request
*
* @return JSONMessage JSON object.
*/
public function fetchRows($args, $request)
{
// Render the rows.
$this->setFirstDataColumn();
$elements = $this->getGridDataElements($request);
$renderedRows = $this->renderRowsInternally($request, $elements);
$json = new JSONMessage();
$json->setStatus(false);
if ($renderedRows) {
$renderedRowsString = null;
foreach ($renderedRows as $rowString) {
$renderedRowsString .= $rowString;
}
$json->setStatus(true);
$json->setContent($renderedRowsString);
}
$this->callFeaturesHook('fetchRows', ['request' => &$request, 'grid' => &$this, 'jsonMessage' => &$json]);
return $json;
}
/**
* Render a row and send it to the client. If the row no
* longer exists then inform the client.
*
* @param array $args
* @param PKPRequest $request
*
* @return JSONMessage JSON object.
*/
public function fetchRow($args, $request)
{
// Instantiate the requested row (includes a
// validity check on the row id).
$row = $this->getRequestedRow($request, $args);
$json = new JSONMessage(true);
if (is_null($row)) {
// Inform the client that the row does no longer exist.
$json->setAdditionalAttributes(['elementNotFound' => $args['rowId']]);
} else {
// Render the requested row
$renderedRow = $this->renderRow($request, $row);
$json->setContent($renderedRow);
// Add the sequence map so grid can place the row at the correct position.
$sequenceMap = $this->getRowsSequence($request);
$json->setAdditionalAttributes(['sequenceMap' => $sequenceMap]);
}
$this->callFeaturesHook('fetchRow', ['request' => &$request, 'grid' => &$this, 'row' => &$row, 'jsonMessage' => &$json]);
// Render and return the JSON message.
return $json;
}
/**
* Render a cell and send it to the client
*
* @param array $args
* @param PKPRequest $request
*
* @return JSONMessage JSON object
*/
public function fetchCell(&$args, $request)
{
// Check the requested column
if (!isset($args['columnId'])) {
fatalError('Missing column id!');
}
if (!$this->hasColumn($args['columnId'])) {
fatalError('Invalid column id!');
}
$this->setFirstDataColumn();
$column = $this->getColumn($args['columnId']);
// Instantiate the requested row
$row = $this->getRequestedRow($request, $args);
if (is_null($row)) {
fatalError('Row not found!');
}
// Render the cell
return new JSONMessage(true, $this->_renderCellInternally($request, $row, $column));
}
/**
* Hook opportunity for grid features to request a save items sequence
* operation. If no grid feature that implements the saveSequence
* hook is attached to this grid, this operation will only return
* the data changed event json message.
*
* @param array $args
* @param PKPRequest $request
*
* @return JSONMessage JSON object
*/
public function saveSequence($args, $request)
{
if (!$request->checkCSRF()) {
throw new \Exception('CSRF mismatch!');
}
$this->callFeaturesHook('saveSequence', ['request' => &$request, 'grid' => &$this]);
return \PKP\db\DAO::getDataChangedEvent();
}
/**
* Get the js handler for this component.
*
* @return string
*/
public function getJSHandler()
{
return '$.pkp.controllers.grid.GridHandler';
}
//
// Protected methods to be overridden/used by subclasses
//
/**
* Return the sequence map of the current loaded grid items.
* This is not the sequence value of the data represented by the
* row, it's just the mapping of the rows sequence, in the order
* that they are loaded. To handle grid items ordering, see
* OrderItemsFeature class.
*
* @param PKPRequest $request
*
* @return array
*/
protected function getRowsSequence($request)
{
return array_keys($this->getGridDataElements($request));
}
/**
* Get a new instance of a grid row. May be
* overridden by subclasses if they want to
* provide a custom row definition.
*
* @return \PKP\controllers\grid\GridRow
*/
protected function getRowInstance()
{
//provide a sensible default row definition
return new GridRow();
}
/**
* Create a data element from a request. This is used to format
* new rows prior to their insertion or existing rows that have
* been edited but not saved.
*
* @param PKPRequest $request
* @param int $elementId Reference to be filled with element
* ID (if one is to be used)
*
* @return object
*/
protected function &getDataElementFromRequest($request, &$elementId)
{
fatalError('Grid does not support data element creation!');
}
/**
* Retrieve a single data element from the grid's data
* source corresponding to the given row id. If none is
* found then return null.
*
* @param PKPRequest $request
* @param string $rowId The row ID; reference permits modification.
*/
protected function getRowDataElement($request, &$rowId)
{
$elements = & $this->getGridDataElements($request);
assert(is_array($elements));
if (!isset($elements[$rowId])) {
return null;
}
return $elements[$rowId];
}
/**
* Implement this method to load data into the grid.
*
* @param PKPRequest $request
* @param ?array $filter An associative array with filter data as returned by
* getFilterSelectionData(). If no filter has been selected by the user
* then the array will be empty.
*
* @return array grid data
*/
protected function loadData($request, $filter)
{
$gridData = null;
$dataProvider = $this->getDataProvider();
if ($dataProvider instanceof \PKP\controllers\grid\GridDataProvider) {
// Populate the grid with data from the
// data provider.
$gridData = $dataProvider->loadData($filter);
}
$this->callFeaturesHook('loadData', ['request' => &$request, 'grid' => &$this, 'gridData' => &$gridData]);
return $gridData;
}
/**
* Returns a Form object or the path name of a filter template.
*
* @return Form|string|null
*/
protected function getFilterForm()
{
return null;
}
/**
* Determine whether a filter form should be collapsible.
*
* @return bool
*/
protected function isFilterFormCollapsible()
{
return true;
}
/**
* Method that extracts the user's filter selection from the request either
* by instantiating the filter's Form object or by reading the request directly
* (if using a simple filter template only).
*
* @param PKPRequest $request
*
* @return ?array
*/
protected function getFilterSelectionData($request)
{
return null;
}
/**
* Render the filter (a template).
*
* @param PKPRequest $request
* @param array $filterData Data to be used by the filter template.
*
* @return string
*/
protected function renderFilter($request, $filterData = [])
{
$form = $this->getFilterForm();
switch (true) {
case $form === null: // No filter form.
return '';
case is_string($form): // HTML mark-up
$templateMgr = TemplateManager::getManager($request);
// Assign data to the filter.
$templateMgr->assign('filterData', $filterData);
// Assign current selected filter data.
$filterSelectionData = $this->getFilterSelectionData($request);
$templateMgr->assign('filterSelectionData', $filterSelectionData);
return $templateMgr->fetch($form);
}
assert(false);
}
/**
* Returns a common 'no matches' result when subclasses find no results for
* AJAX autocomplete requests.
*
* @return JSONMessage JSON object
*/
protected function noAutocompleteResults()
{
$returner = [];
$returner[] = ['label' => __('common.noMatches'), 'value' => ''];
return new JSONMessage(true, $returner);
}
/**
* Override this method if your subclass needs to perform
* different actions than the ones implemented here.
* This method is called by GridHandler::fetchGrid()
*
* @param array $args
* @param PKPRequest $request
* @param PKPTemplateManager $templateMgr
*/
protected function doSpecificFetchGridActions($args, $request, $templateMgr)
{
// Render the body elements.
$gridBodyParts = $this->renderGridBodyPartsInternally($request);
$templateMgr->assign('gridBodyParts', $gridBodyParts);
}
/**
* Define the first column that will contain
* grid data.
*
* Override this method to define a different column
* than the first one.
*/
protected function setFirstDataColumn()
{
$columns = & $this->getColumns();
$firstColumn = reset($columns);
$firstColumn->addFlag('firstColumn', true);
}
/**
* Override to init grid features.
* This method is called by GridHandler::initialize()
* method that use the returned array with the initialized
* features to add them to grid.
*
* @param PKPRequest $request
* @param array $args
*
* @return array Array with initialized grid features objects.
*/
protected function initFeatures($request, $args)
{
$returner = [];
$classNameParts = explode('\\', get_class($this)); // Separate namespace info from class name
Hook::call(strtolower_codesafe(end($classNameParts) . '::initFeatures'), [$this, $request, $args, &$returner]);
return $returner;
}
/**
* Call the passed hook in all attached features.
*
* @param string $hookName
* @param array $args Arguments provided by this handler.
*/
protected function callFeaturesHook($hookName, $args)
{
$features = $this->getFeatures();
if (is_array($features)) {
foreach ($features as &$feature) {
if (is_callable([$feature, $hookName])) {
$feature->$hookName($args);
} else {
assert(false);
}
}
}
}
/**
* Cycle through the data and get generate the row HTML.
*
* @param PKPRequest $request
* @param array $elements The grid data elements to be rendered.
*
* @return array of HTML Strings for Grid Rows.
*/
protected function renderRowsInternally($request, $elements)
{
// Iterate through the rows and render them according
// to the row definition.
$renderedRows = [];
foreach ($elements as $elementId => $element) {
// Instantiate a new row.
$row = $this->_getInitializedRowInstance($request, $elementId, $element);
// Render the row
$renderedRows[] = $this->renderRowInternally($request, $row);
}
return $renderedRows;
}
/**
* Method that renders a single row.
*
* NB: You must have initialized the row
* before you call this method.
*
* @param PKPRequest $request
* @param \PKP\controllers\grid\GridRow $row
*
* @return string the row HTML
*/
protected function renderRowInternally($request, $row)
{
// Iterate through the columns and render the
// cells for the given row.
$renderedCells = [];
$columns = $this->getColumns();
foreach ($columns as $column) {
assert($column instanceof \PKP\controllers\grid\GridColumn);
$renderedCells[] = $this->_renderCellInternally($request, $row, $column);
}
// Pass control to the view to render the row
$templateMgr = TemplateManager::getManager($request);
$templateMgr->assign([
'grid' => $this,
'columns' => $columns,
'cells' => $renderedCells,
'row' => $row,
]);
return $templateMgr->fetch($row->getTemplate());
}
/**
* Method that renders tbodys to go in the grid main body.
*
* @param PKPRequest $request
*
* @return array
*/
protected function renderGridBodyPartsInternally($request)
{
// Render the rows.
$elements = $this->getGridDataElements($request);
$renderedRows = $this->renderRowsInternally($request, $elements);
// Render the body part.
$templateMgr = TemplateManager::getManager($request);
$gridBodyParts = [];
if (count($renderedRows) > 0) {
$templateMgr->assign('grid', $this);
$templateMgr->assign('rows', $renderedRows);
$gridBodyParts[] = $templateMgr->fetch('controllers/grid/gridBodyPart.tpl');
}
return $gridBodyParts;
}
//
// Private helper methods
//
/**
* Instantiate a new row.
*
* @param PKPRequest $request
* @param string $elementId
* @param bool $isModified optional
*
* @return \PKP\controllers\grid\GridRow
*/
private function _getInitializedRowInstance($request, $elementId, &$element, $isModified = false)
{
// Instantiate a new row
$row = $this->getRowInstance();
$row->setGridId($this->getId());
$row->setId($elementId);
$row->setData($element);
$row->setRequestArgs($this->getRequestArgs());
$row->setIsModified($isModified);
// Initialize the row before we render it
$row->initialize($request);
$this->callFeaturesHook('getInitializedRowInstance', ['grid' => &$this, 'row' => &$row]);
return $row;
}
/**
* Method that renders a cell.
*
* NB: You must have initialized the row
* before you call this method.
*
* @param PKPRequest $request
* @param \PKP\controllers\grid\GridRow $row
* @param GridColumn $column
*
* @return string the cell HTML
*/
private function _renderCellInternally($request, $row, $column)
{
// If there is no object, then we want to return an empty row.
// override the assigned GridCellProvider and provide the default.
$element = & $row->getData();
if (is_null($element) && $row->getIsModified()) {
$cellProvider = new GridCellProvider();
return $cellProvider->render($request, $row, $column);
}
// Otherwise, get the cell content.
// If row defines a cell provider, use it.
$cellProvider = $row->getCellProvider();
if (!$cellProvider instanceof \PKP\controllers\grid\GridCellProvider) {
// Remove reference to the row variable.
unset($cellProvider);
// Get cell provider from column.
$cellProvider = $column->getCellProvider();
}
return $cellProvider->render($request, $row, $column);
}
/**
* Method that grabs all the existing columns and makes sure the column widths add to exactly 100
* N.B. We do some extra column fetching because PHP makes copies of arrays with foreach.
*/
private function _fixColumnWidths()
{
$columns = & $this->getColumns();
$width = 0;
$noSpecifiedWidthCount = 0;
// Find the total width and how many columns do not specify their width.
foreach ($columns as $column) {
if ($column->hasFlag('width')) {
$width += $column->getFlag('width');
} else {
$noSpecifiedWidthCount++;
}
}
// Four cases: we have to add or remove some width, and either we have wiggle room or not.
// First case, width less than 100 and some unspecified columns to add it to.
if ($width < 100) {
if ($noSpecifiedWidthCount > 0) {
// We need to add width to columns that did not specify it.
foreach ($columns as $column) {
if (!$column->hasFlag('width')) {
$modifyColumn = $this->getColumn($column->getId());
$modifyColumn->addFlag('width', round((100 - $width) / $noSpecifiedWidthCount));
unset($modifyColumn);
}
}
}
}
// Second case, width higher than 100 and all columns width specified.
if ($width > 100) {
if ($noSpecifiedWidthCount == 0) {
// We need to remove width from all columns equally.
$columnsToModify = $columns;
foreach ($columns as $key => $column) {
// We don't want to change the indent column widht, so avoid it.
if ($column->getId() == 'indent') {
unset($columnsToModify[$key]);
}
}
// Calculate the value to remove from all columns.
$difference = $width - 100;
$columnsCount = count($columnsToModify);
$removeValue = round($difference / $columnsCount);
foreach ($columnsToModify as $column) {
$modifyColumn = $this->getColumn($column->getId());
if (end($columnsToModify) === $column) {
// Handle rounding problems.
$totalWidth = $width - ($removeValue * $columnsCount);
if ($totalWidth < 100) {
$removeValue -= 100 - $totalWidth;
}
}
$modifyColumn->addFlag('width', $modifyColumn->getFlag('width') - $removeValue);
}
}
}
}
/**
* Add grid features.
*
* @param array $features
*/
private function _addFeatures($features)
{
assert(is_array($features));
foreach ($features as &$feature) {
assert($feature instanceof \PKP\controllers\grid\feature\GridFeature);
$this->_features[$feature->getId()] = $feature;
}
}
private function checkIfResetActionsNeeded($request)
{
// #8696: This is added in order to reset the page of a grid to 1 if the "search" button is clicked, effectively executing a
// new search.
// Check if the grid has any PagingFeature features
if ($this->getFeatures() != null) {
$pagingFeatureArray = array_filter($this->getFeatures(), function ($value) {
return $value instanceof \PKP\controllers\grid\feature\PagingFeature;
});
if (!empty($pagingFeatureArray)) {
if (array_key_exists('search', $request->getUserVars()) && array_key_exists('submitFormButton', $request->getUserVars())) {
$filteredKeys = array_filter(array_keys($request->getUserVars()), function ($key) {
return Str::endsWith($key, 'gridPage');
});
if (!empty($filteredKeys)) {
$request->_requestVars[$filteredKeys[0]] = 1;
}
}
}
}
}
/**
* Legacy function for grid handlers.
*Prepares LazyCollections in associative array GridHandler expects, e.g. [item_id => item]
*
* @see PKP\db\DAOResultFactory::toAssociativeArray()
*
*/
public static function toAssociativeArray(LazyCollection $lazyCollection, string $idField = 'id'): array
{
$returner = [];
foreach ($lazyCollection as $item) {
$returner[$item->getData($idField)] = $item;
}
return $returner;
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\controllers\grid\GridHandler', '\GridHandler');
foreach ([
'GRID_ACTION_POSITION_DEFAULT',
'GRID_ACTION_POSITION_ABOVE',
'GRID_ACTION_POSITION_LASTCOL',
'GRID_ACTION_POSITION_BELOW',
] as $constantName) {
define($constantName, constant('\GridHandler::' . $constantName));
}
}
|