Skip to content

Commit

Permalink
[FEATURE] Add content changes widget
Browse files Browse the repository at this point in the history
Adds a new widget which shows:

* Pending publishing state change, if any. Lists records
  which have starttime / endtime in the future.
* Changed records along with name of user who made
  the change.

Attempts to select a total of $limit (default 5) records
while distributing the number of records equally between
the "pending" and "changed" sets, giving preference to
the "pending" set:

* If limit is 5 and 3 "pending" were selected, only 2 "changed"
  will be selected.
* If limit is 5 and no "pending" were selected, all 5 are
  selected from the "changed" set.

If there are no records in a group the table containing
the record data is not shown. if there are no results in
either of the two groups, a "nothing to show" message
is shown instead.
  • Loading branch information
NamelessCoder committed Oct 18, 2019
1 parent 5eecce3 commit a3c2bba
Show file tree
Hide file tree
Showing 4 changed files with 250 additions and 0 deletions.
145 changes: 145 additions & 0 deletions Classes/Widgets/ContentChangesWidget.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<?php
declare(strict_types=1);

namespace FriendsOfTYPO3\Dashboard\Widgets;

use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;

class ContentChangesWidget extends AbstractListWidget
{
/**
* @var string
*/
protected $templateName = 'contentChanges';

public function __construct()
{
AbstractListWidget::__construct();
$this->width = 4;
$this->height = 4;
$this->title = 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.contentchanges.title';
$this->description = 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.contentchanges.description';
$this->iconIdentifier = 'dashboard-typo3';
}

public function prepareData(): void
{
$this->items['pendingRecords'] = $this->getPublishStateChangePendingRecords();
$this->items['changedRecords'] = $this->getMostRecentChangedRecords(count($this->items['pendingRecords']));
}

protected function getPublishStateChangePendingRecords(): iterable
{
$limit = ceil($this->limit / 2);
$pendingRecords = $this->getPublishStateChangePendingRecordsFromTable('pages') + $this->getPublishStateChangePendingRecordsFromTable('tt_content');
usort($pendingRecords, function(array $a, array $b) {
return min($b['starttime'], $b['endtime']) <=> min($a['starttime'], $a['endtime']);
});
return count($pendingRecords) > $limit ? array_slice($pendingRecords, 0, $limit) : $pendingRecords;
}

protected function getMostRecentChangedRecords(int $alreadyCollectedItemCount): iterable
{
$limit = $this->limit - $alreadyCollectedItemCount;
$changedRecords = $this->getMostRecentChangedRecordsFromTable('pages') + $this->getMostRecentChangedRecordsFromTable('tt_content');
usort($changedRecords, function(array $a, array $b) {
return $b['tstamp'] <=> $a['tstamp'];
});
return count($changedRecords) > $limit ? array_slice($changedRecords, 0, $limit) : $changedRecords;
}

protected function getPublishStateChangePendingRecordsFromTable(string $table): array
{
$labelFieldsForQuery = $this->resolveLabelFieldsForTable($table);
$now = time();

$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
$queryBuilder->getRestrictions()->removeByType(StartTimeRestriction::class)->removeByType(EndTimeRestriction::class);
$query = $queryBuilder->select(...$labelFieldsForQuery)
->from($table, 't')
->orWhere(
$queryBuilder->expr()->gte('t.starttime', $queryBuilder->createNamedParameter($now, \PDO::PARAM_INT)),
$queryBuilder->expr()->gte('t.endtime', $queryBuilder->createNamedParameter($now, \PDO::PARAM_INT))
)->setMaxResults($this->limit);

return $this->processItemsQuery($table, $query);
}

protected function getMostRecentChangedRecordsFromTable(string $table): array
{
$labelFieldsForQuery = $this->resolveLabelFieldsForTable($table);
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
$query = $queryBuilder->select(
'u.username',
...$labelFieldsForQuery
)
->from($table, 't')
->rightJoin(
't',
'sys_log',
'l',
(string) $queryBuilder->expr()->andX(
$queryBuilder->expr()->eq('l.recuid', 't.uid'),
$queryBuilder->expr()->eq('l.tablename', $queryBuilder->createNamedParameter($table, \PDO::PARAM_STR))
)
)
->leftJoin(
'l',
'be_users',
'u',
'u.uid = l.userid'
)
->orderBy('t.tstamp', 'DESC')
->groupBy('t.uid', 't.pid', 't.sys_language_uid', 'u.username', ...$labelFieldsForQuery)
->setMaxResults($this->limit);

return $this->processItemsQuery($table, $query);
}

protected function resolveLabelFieldsForTable(string $table): array
{
$labelFields = [$GLOBALS['TCA'][$table]['ctrl']['label']] + explode(',', $GLOBALS['TCA'][$table]['ctrl']['label_alt'] ?? '');
$labelFields = array_filter($labelFields);
$labelFieldsForQuery = [];
foreach ($labelFields as $labelField) {
$labelFieldsForQuery[] = 't.' . $labelField;
}
return $labelFieldsForQuery;
}

protected function processItemsQuery(string $table, QueryBuilder $query): array
{
$iconFactory = $this->getIconFactory();

$query->addSelect('t.uid', 't.pid', 't.tstamp', 't.sys_language_uid', 't.starttime', 't.endtime');
if ($table === 'pages') {
$query->addSelect('t.doktype', 't.is_siteroot');
} elseif ($table === 'tt_content') {
$query->addSelect('t.CType');
}

$results = $query->execute()->fetchAll();
if (!$results) {
return [];
}
foreach ($results as &$result) {
$result['type'] = $table;
$result['label'] = BackendUtility::getRecordTitle($table, $result);
$result['icon'] = $iconFactory->getIconForRecord($table, $result, Icon::SIZE_SMALL);
}

return $results;
}

protected function getIconFactory(): IconFactory
{
return GeneralUtility::makeInstance(IconFactory::class);
}
}
34 changes: 34 additions & 0 deletions Resources/Private/Language/locallang.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,40 @@
<source>This overview is showing all (%s) pages without description</source>
</trans-unit>

<trans-unit id="widgets.contentchanges.title" xml:space="preserve">
<source>Content Changes</source>
</trans-unit>
<trans-unit id="widgets.contentchanges.description" xml:space="preserve">
<source>Shows most recently edited pages/content and up-coming publishing and un-publishing based on start/end time</source>
</trans-unit>
<trans-unit id="widgets.contentchanges.label.changed" xml:space="preserve">
<source>Changed item</source>
</trans-unit>
<trans-unit id="widgets.contentchanges.label.pending" xml:space="preserve">
<source>Pending publish/unpublish</source>
</trans-unit>
<trans-unit id="widgets.contentchanges.type" xml:space="preserve">
<source>Type</source>
</trans-unit>
<trans-unit id="widgets.contentchanges.date" xml:space="preserve">
<source>Date</source>
</trans-unit>
<trans-unit id="widgets.contentchanges.user" xml:space="preserve">
<source>Edit by</source>
</trans-unit>
<trans-unit id="widgets.contentchanges.starts" xml:space="preserve">
<source>Publishes on</source>
</trans-unit>
<trans-unit id="widgets.contentchanges.ends" xml:space="preserve">
<source>Unpublishes on</source>
</trans-unit>
<trans-unit id="widgets.contentchanges.id" xml:space="preserve">
<source>ID</source>
</trans-unit>
<trans-unit id="widgets.contentchanges.nothing_to_show" xml:space="preserve">
<source>Nothing to show</source>
</trans-unit>

<trans-unit id="widgets.lastLogins.title" xml:space="preserve">
<source>Last 5 logins</source>
</trans-unit>
Expand Down
70 changes: 70 additions & 0 deletions Resources/Private/Templates/Widgets/ContentChanges.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" xmlns:backend="http://typo3.org/ns/TYPO3/CMS/Backend/ViewHelpers" xmlns:core="http://typo3.org/ns/TYPO3/CMS/Core/ViewHelpers" data-namespace-typo3-fluid="true">
<f:layout name="WidgetWithTitle" />

<f:section name="main">
<f:if condition="!{items.changedRecords} && !{items.pendingRecords}">
<h4><f:translate key="widgets.contentchanges.nothing_to_show" extensionName="dashboard" /></h4>
</f:if>
<f:if condition="{items.pendingRecords}">
<table class="widget-table">
<thead>
<tr>
<th><f:translate key="widgets.contentchanges.label.pending" extensionName="dashboard" /></th>
<th><f:translate key="widgets.contentchanges.type" extensionName="dashboard" /></th>
<th><f:translate key="widgets.contentchanges.id" extensionName="dashboard" /></th>
<th><f:translate key="widgets.contentchanges.starts" extensionName="dashboard" /></th>
<th><f:translate key="widgets.contentchanges.ends" extensionName="dashboard" /></th>
<th></th>
</tr>
</thead>
<tbody>
<f:for each="{items.pendingRecords}" as="item">
<tr>
<td>{item.icon | f:format.raw()} {item.label}</td>
<td>{item.type}</td>
<td>{item.uid}</td>
<td><f:if condition="{item.starttime}"><f:format.date format="%d-%m-%Y %H:%M">{item.starttime}</f:format.date></f:if></td>
<td><f:if condition="{item.endtime}"><f:format.date format="%d-%m-%Y %H:%M">{item.endtime}</f:format.date></f:if></td>
<td class="widget-edit">
<backend:link.editRecord uid="{item.uid}" table="{item.type}" returnUrl="{f:be.uri(route: 'dashboard')}">
<core:icon identifier="actions-open" alternativeMarkupIdentifier="inline" />
</backend:link.editRecord>
</td>
</tr>
</f:for>
</tbody>
</table>
</f:if>
<f:if condition="{items.changedRecords}">
<table class="widget-table">
<thead>
<tr>
<th><f:translate key="widgets.contentchanges.label.changed" extensionName="dashboard" /></th>
<th><f:translate key="widgets.contentchanges.type" extensionName="dashboard" /></th>
<th><f:translate key="widgets.contentchanges.id" extensionName="dashboard" /></th>
<th><f:translate key="widgets.contentchanges.date" extensionName="dashboard" /></th>
<th><f:translate key="widgets.contentchanges.user" extensionName="dashboard" /></th>
<th></th>
</tr>
</thead>
<tbody>
<f:for each="{items.changedRecords}" as="item">
<tr>
<td>{item.icon | f:format.raw()} {item.label}</td>
<td>{item.type}</td>
<td>{item.uid}</td>
<td><f:format.date format="%d-%m-%Y %H:%M">{item.tstamp}</f:format.date></td>
<td>{item.username}</td>
<td class="widget-edit">
<backend:link.editRecord uid="{item.uid}" table="{item.type}" returnUrl="{f:be.uri(route: 'dashboard')}">
<core:icon identifier="actions-open" alternativeMarkupIdentifier="inline" />
</backend:link.editRecord>
</td>
</tr>
</f:for>
</tbody>
</table>
</f:if>
</f:section>

</html>
1 change: 1 addition & 0 deletions ext_localconf.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
$widgetRegistry->registerWidget('sysLogErrors', \FriendsOfTYPO3\Dashboard\Widgets\SysLogErrorsWidget::class);
$widgetRegistry->registerWidget('t3News', \FriendsOfTYPO3\Dashboard\Widgets\T3NewsWidget::class);
$widgetRegistry->registerWidget('documentation', \FriendsOfTYPO3\Dashboard\Widgets\DocumentationWidget::class);
$widgetRegistry->registerWidget('contentChanges', \FriendsOfTYPO3\Dashboard\Widgets\ContentChangesWidget::class);

$dashboardRegistry = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\FriendsOfTYPO3\Dashboard\Registry\DashboardRegistry::class);
$dashboardRegistry->registerDashboard('default', 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:dashboard.default');
Expand Down

0 comments on commit a3c2bba

Please sign in to comment.