<?php
/**
* @file classes/controllers/grid/CategoryGridHandler.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 CategoryGridHandler
*
* @ingroup controllers_grid
*
* @brief Class defining basic operations for handling HTML grids with categories.
*/
namespace PKP\controllers\grid;
use APP\template\TemplateManager;
use Illuminate\Support\LazyCollection;
use PKP\core\JSONMessage;
use PKP\core\PKPRequest;
class CategoryGridHandler extends GridHandler
{
/** @var string empty category row locale key */
public $_emptyCategoryRowText = 'grid.noItems';
/** @var array The category grid's data source. */
public $_categoryData;
/** @var ?int The category id that this grid is currently rendering. */
public $_currentCategoryId = null;
/**
* Constructor.
*
* @param null|mixed $dataProvider
*/
public function __construct($dataProvider = null)
{
parent::__construct($dataProvider);
$this->addColumn(new GridColumn(
'indent',
null,
null,
null,
new NullGridCellProvider(),
['indent' => true, 'width' => 2]
));
}
//
// Getters and setters.
//
/**
* Get the empty rows text for a category.
*
* @return string
*/
public function getEmptyCategoryRowText()
{
return $this->_emptyCategoryRowText;
}
/**
* Set the empty rows text for a category.
*
* @param string $translationKey
*/
public function setEmptyCategoryRowText($translationKey)
{
$this->_emptyCategoryRowText = $translationKey;
}
/**
* Get the category id that this grid is currently rendering.
*
* @return int
*/
public function getCurrentCategoryId()
{
return $this->_currentCategoryId;
}
/**
* Override to return the data element sequence value
* inside the passed category, if needed.
*
* @param int $categoryId The data element category id.
* @param mixed $gridDataElement The element to return the
* sequence.
*
* @return int
*/
public function getDataElementInCategorySequence($categoryId, &$gridDataElement)
{
assert(false);
}
/**
* Override to set the data element new sequence inside
* the passed category, if needed.
*
* @param int $categoryId The data element category id.
* @param mixed $gridDataElement The element to set the
* new sequence.
* @param int $newSequence The new sequence value.
*/
public function setDataElementInCategorySequence($categoryId, &$gridDataElement, $newSequence)
{
assert(false);
}
/**
* Override to define whether the data element inside the passed
* category is selected or not.
*
* @param int $categoryId
*/
public function isDataElementInCategorySelected($categoryId, &$gridDataElement)
{
assert(false);
}
/**
* Get the grid category data.
*
* @param PKPRequest $request
* @param mixed $categoryElement The category element.
*
* @return array
*/
public function &getGridCategoryDataElements($request, $categoryElement)
{
$filter = $this->getFilterSelectionData($request);
// Get the category element id.
$categories = $this->getGridDataElements($request);
$categoryElementId = array_search($categoryElement, $categories);
assert($categoryElementId !== false);
// Try to load data if it has not yet been loaded.
if (!is_array($this->_categoryData) || !array_key_exists($categoryElementId, $this->_categoryData)) {
$data = $this->loadCategoryData($request, $categoryElement, $filter);
if (is_null($data)) {
// Initialize data to an empty array.
$data = [];
}
$this->setGridCategoryDataElements($request, $categoryElementId, $data);
}
return $this->_categoryData[$categoryElementId];
}
/**
* Check whether the passed category has grid rows.
*
* @param mixed $categoryElement The category data element
* that will be checked.
* @param PKPRequest $request
*
* @return bool
*/
public function hasGridDataElementsInCategory($categoryElement, $request)
{
$data = & $this->getGridCategoryDataElements($request, $categoryElement);
assert(is_array($data));
return (bool) count($data);
}
/**
* Get the number of elements inside the passed category element.
*
* @param PKPRequest $request
*
* @return int
*/
public function getCategoryItemsCount($categoryElement, $request)
{
$data = $this->getGridCategoryDataElements($request, $categoryElement);
assert(is_array($data));
return count($data);
}
/**
* Set the grid category data.
*
* @param string $categoryElementId The category element id.
* @param mixed $data an array or ItemIterator with category elements data.
*/
public function setGridCategoryDataElements($request, $categoryElementId, $data)
{
// Make sure we have an array to store all categories elements data.
if (!is_array($this->_categoryData)) {
$this->_categoryData = [];
}
// FIXME: We go to arrays for all types of iterators because
// iterators cannot be re-used, see #6498.
if (is_array($data)) {
$this->_categoryData[$categoryElementId] = $data;
} elseif ($data instanceof \PKP\db\DAOResultFactory) {
$this->_categoryData[$categoryElementId] = $data->toAssociativeArray();
} elseif ($data instanceof \PKP\core\ItemIterator) {
$this->_categoryData[$categoryElementId] = $data->toArray();
} elseif ($data instanceof LazyCollection) {
$this->_categoryData[$categoryElementId] = iterator_to_array($data);
} else {
assert(false);
}
}
//
// Public handler methods
//
/**
* Render a category with all the rows inside of it.
*
* @param array $args
* @param PKPRequest $request
*
* @return string the serialized row JSON message or a flag
* that indicates that the row has not been found.
*/
public function fetchCategory($args, $request)
{
// Instantiate the requested row (includes a
// validity check on the row id).
$row = $this->getRequestedCategoryRow($request, $args);
$json = new JSONMessage(true);
if (is_null($row)) {
// Inform the client that the category does no longer exist.
$json->setAdditionalAttributes(['elementNotFound' => (int)$args['rowId']]);
} else {
// Render the requested category
$this->setFirstDataColumn();
$json->setContent($this->_renderCategoryInternally($request, $row));
}
return $json;
}
//
// Extended methods from GridHandler
//
/**
* @copydoc GridHandler::initialize()
*
* @param null|mixed $args
*/
public function initialize($request, $args = null)
{
parent::initialize($request, $args);
if (!is_null($request->getUserVar('rowCategoryId'))) {
$this->_currentCategoryId = (string) $request->getUserVar('rowCategoryId');
}
}
/**
* @see GridHandler::getRequestArgs()
*/
public function getRequestArgs()
{
$args = parent::getRequestArgs();
// If grid is rendering grid rows inside category,
// add current category id value so rows will also know
// their parent category.
if (!is_null($this->_currentCategoryId)) {
if ($this->getCategoryRowIdParameterName()) {
$args[$this->getCategoryRowIdParameterName()] = $this->_currentCategoryId;
}
}
return $args;
}
/**
* @copydoc GridHandler::getJSHandler()
*/
public function getJSHandler()
{
return '$.pkp.controllers.grid.CategoryGridHandler';
}
/**
* @copydoc GridHandler::setUrls()
*/
public function setUrls($request, $extraUrls = [])
{
$router = $request->getRouter();
$extraUrls['fetchCategoryUrl'] = $router->url($request, null, null, 'fetchCategory', null, $this->getRequestArgs());
parent::setUrls($request, $extraUrls);
}
/**
* @copydoc GridHandler::getRowsSequence()
*/
protected function getRowsSequence($request)
{
$categories = $this->getGridDataElements($request);
$categoryElement = $categories[$this->getCurrentCategoryId()];
return array_keys($this->getGridCategoryDataElements($request, $categoryElement));
}
/**
* @see GridHandler::doSpecificFetchGridActions()
*/
protected function doSpecificFetchGridActions($args, $request, $templateMgr)
{
// Render the body elements (category groupings + rows inside a <tbody>)
$gridBodyParts = $this->_renderCategoriesInternally($request);
$templateMgr->assign('gridBodyParts', $gridBodyParts);
}
/**
* @copydoc GridHandler::getRowDataElement()
*/
protected function getRowDataElement($request, &$rowId)
{
$rowData = parent::getRowDataElement($request, $rowId);
$rowCategoryId = $request->getUserVar('rowCategoryId');
if (is_null($rowData) && !is_null($rowCategoryId)) {
// Try to get row data inside category.
$categoryRowData = parent::getRowDataElement($request, $rowCategoryId);
if (!is_null($categoryRowData)) {
$categoryElements = $this->getGridCategoryDataElements($request, $categoryRowData);
assert(is_array($categoryElements));
if (!isset($categoryElements[$rowId])) {
return null;
}
// Let grid (and also rows) knowing the current category id.
// This value will be published by the getRequestArgs method.
$this->_currentCategoryId = $rowCategoryId;
return $categoryElements[$rowId];
}
} else {
return $rowData;
}
}
/**
* @see GridHandler::setFirstDataColumn()
*/
protected function setFirstDataColumn()
{
$columns = & $this->getColumns();
reset($columns);
// Category grids will always have indent column firstly,
// so we need to consider the first column the second one.
$secondColumn = next($columns); /** @var GridColumn $secondColumn */
$secondColumn->addFlag('firstColumn', true);
}
/**
* @see GridHandler::renderRowInternally()
*/
protected function renderRowInternally($request, $row)
{
if ($this->getCategoryRowIdParameterName()) {
$param = $this->getRequestArg($this->getCategoryRowIdParameterName());
$templateMgr = TemplateManager::getManager($request);
$templateMgr->assign('categoryId', $param);
}
return parent::renderRowInternally($request, $row);
}
/**
* 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 GridRow the requested grid row, already
* configured with id and data or null if the row
* could not been found.
*/
protected function getRequestedCategoryRow($request, $args)
{
if (isset($args['rowId'])) {
// A row ID was specified. Fetch it
$elementId = $args['rowId'];
// Retrieve row data for the requested row id
// (we can use the default getRowData element, works for category grids as well).
$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;
}
}
// Instantiate a new row
return $this->_getInitializedCategoryRowInstance($request, $elementId, $dataElement);
}
//
// Protected methods to be overridden/used by subclasses
//
/**
* Get a new instance of a category grid row. May be
* overridden by subclasses if they want to
* provide a custom row definition.
*
* @return CategoryGridRow
*/
protected function getCategoryRowInstance()
{
//provide a sensible default category row definition
return new GridCategoryRow();
}
/**
* Get the category row id parameter name.
*
* @return string
*/
protected function getCategoryRowIdParameterName()
{
// Must be implemented by subclasses.
return null;
}
/**
* Implement this method to load category data into the grid.
*
* @param PKPRequest $request
* @param null|mixed $filter
*
* @return array
*/
protected function loadCategoryData($request, &$categoryDataElement, $filter = null)
{
$gridData = [];
$dataProvider = $this->getDataProvider();
if ($dataProvider instanceof \PKP\controllers\grid\CategoryGridDataProvider) {
// Populate the grid with data from the
// data provider.
$gridData = $dataProvider->loadCategoryData($request, $categoryDataElement, $filter);
}
return $gridData;
}
//
// Private helper methods
//
/**
* Instantiate a new row.
*
* @param PKPRequest $request
* @param string $elementId
*
* @return GridRow
*/
private function _getInitializedCategoryRowInstance($request, $elementId, $element)
{
// Instantiate a new row
$row = $this->getCategoryRowInstance();
$row->setGridId($this->getId());
$row->setId($elementId);
$row->setData($element);
$row->setRequestArgs($this->getRequestArgs());
// Initialize the row before we render it
$row->initialize($request);
$this->callFeaturesHook(
'getInitializedCategoryRowInstance',
['request' => $request,
'grid' => $this,
'categoryId' => $this->_currentCategoryId,
'row' => $row]
);
return $row;
}
/**
* Render all the categories internally
*
* @param PKPRequest $request
*/
private function _renderCategoriesInternally($request)
{
// Iterate through the rows and render them according
// to the row definition.
$renderedCategories = [];
$elements = $this->getGridDataElements($request);
foreach ($elements as $key => $element) {
// Instantiate a new row
$categoryRow = $this->_getInitializedCategoryRowInstance($request, $key, $element);
// Render the row
$renderedCategories[] = $this->_renderCategoryInternally($request, $categoryRow);
}
return $renderedCategories;
}
/**
* Render a category row and its data.
*
* @param PKPRequest $request
* @param GridCategoryRow $categoryRow
*
* @return string HTML for all the rows (including category)
*/
private function _renderCategoryInternally($request, $categoryRow)
{
// Prepare the template to render the category.
$templateMgr = TemplateManager::getManager($request);
$templateMgr->assign('grid', $this);
$columns = $this->getColumns();
$templateMgr->assign('columns', $columns);
$categoryDataElement = $categoryRow->getData();
$rowData = $this->getGridCategoryDataElements($request, $categoryDataElement);
// Render the data rows
$templateMgr->assign('categoryRow', $categoryRow);
// Let grid (and also rows) knowing the current category id.
// This value will be published by the getRequestArgs method.
$this->_currentCategoryId = $categoryRow->getId();
$renderedRows = $this->renderRowsInternally($request, $rowData);
$templateMgr->assign('rows', $renderedRows);
$renderedCategoryRow = $this->renderRowInternally($request, $categoryRow);
// Finished working with this category, erase the current id value.
$this->_currentCategoryId = null;
$templateMgr->assign('renderedCategoryRow', $renderedCategoryRow);
return $templateMgr->fetch('controllers/grid/gridBodyPartWithCategory.tpl');
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\controllers\grid\CategoryGridHandler', '\CategoryGridHandler');
}
|