Process tag list page through Slim controller

This commit is contained in:
ArthurHoaro 2020-05-16 14:56:22 +02:00
parent 3772298ee7
commit 60ae241251
7 changed files with 247 additions and 51 deletions

View file

@ -10,12 +10,15 @@
/** /**
* Class TagCloud * Class TagCloud
* *
* Slim controller used to render the tag cloud page. * Slim controller used to render the tag cloud and tag list pages.
* *
* @package Front\Controller * @package Front\Controller
*/ */
class TagCloudController extends ShaarliController class TagCloudController extends ShaarliController
{ {
protected const TYPE_CLOUD = 'cloud';
protected const TYPE_LIST = 'list';
/** /**
* Display the tag cloud through the template engine. * Display the tag cloud through the template engine.
* This controller a few filters: * This controller a few filters:
@ -23,27 +26,54 @@ class TagCloudController extends ShaarliController
* - `searchtags` query parameter: will return tags associated with filter in at least one bookmark * - `searchtags` query parameter: will return tags associated with filter in at least one bookmark
*/ */
public function cloud(Request $request, Response $response): Response public function cloud(Request $request, Response $response): Response
{
return $this->processRequest(static::TYPE_CLOUD, $request, $response);
}
/**
* Display the tag list through the template engine.
* This controller a few filters:
* - Visibility stored in the session for logged in users
* - `searchtags` query parameter: will return tags associated with filter in at least one bookmark
* - `sort` query parameters:
* + `usage` (default): most used tags first
* + `alpha`: alphabetical order
*/
public function list(Request $request, Response $response): Response
{
return $this->processRequest(static::TYPE_LIST, $request, $response);
}
/**
* Process the request for both tag cloud and tag list endpoints.
*/
protected function processRequest(string $type, Request $request, Response $response): Response
{ {
if ($this->container->loginManager->isLoggedIn() === true) { if ($this->container->loginManager->isLoggedIn() === true) {
$visibility = $this->container->sessionManager->getSessionParameter('visibility'); $visibility = $this->container->sessionManager->getSessionParameter('visibility');
} }
$sort = $request->getQueryParam('sort');
$searchTags = $request->getQueryParam('searchtags'); $searchTags = $request->getQueryParam('searchtags');
$filteringTags = $searchTags !== null ? explode(' ', $searchTags) : []; $filteringTags = $searchTags !== null ? explode(' ', $searchTags) : [];
$tags = $this->container->bookmarkService->bookmarksCountPerTag($filteringTags, $visibility ?? null); $tags = $this->container->bookmarkService->bookmarksCountPerTag($filteringTags, $visibility ?? null);
// TODO: the sorting should be handled by bookmarkService instead of the controller if (static::TYPE_CLOUD === $type || 'alpha' === $sort) {
alphabetical_sort($tags, false, true); // TODO: the sorting should be handled by bookmarkService instead of the controller
alphabetical_sort($tags, false, true);
}
$tagList = $this->formatTagsForCloud($tags); if (static::TYPE_CLOUD === $type) {
$tags = $this->formatTagsForCloud($tags);
}
$searchTags = implode(' ', escape($filteringTags)); $searchTags = implode(' ', escape($filteringTags));
$data = [ $data = [
'search_tags' => $searchTags, 'search_tags' => $searchTags,
'tags' => $tagList, 'tags' => $tags,
]; ];
$data = $this->executeHooks($data); $data = $this->executeHooks('tag' . $type, $data);
foreach ($data as $key => $value) { foreach ($data as $key => $value) {
$this->assignView($key, $value); $this->assignView($key, $value);
} }
@ -51,12 +81,19 @@ public function cloud(Request $request, Response $response): Response
$searchTags = !empty($searchTags) ? $searchTags .' - ' : ''; $searchTags = !empty($searchTags) ? $searchTags .' - ' : '';
$this->assignView( $this->assignView(
'pagetitle', 'pagetitle',
$searchTags . t('Tag cloud') .' - '. $this->container->conf->get('general.title', 'Shaarli') $searchTags . t('Tag '. $type) .' - '. $this->container->conf->get('general.title', 'Shaarli')
); );
return $response->write($this->render('tag.cloud')); return $response->write($this->render('tag.'. $type));
} }
/**
* Format the tags array for the tag cloud template.
*
* @param array<string, int> $tags List of tags as key with count as value
*
* @return mixed[] List of tags as key, with count and expected font size in a subarray
*/
protected function formatTagsForCloud(array $tags): array protected function formatTagsForCloud(array $tags): array
{ {
// We sort tags alphabetically, then choose a font size according to count. // We sort tags alphabetically, then choose a font size according to count.
@ -81,12 +118,12 @@ protected function formatTagsForCloud(array $tags): array
/** /**
* @param mixed[] $data Template data * @param mixed[] $data Template data
* *
* @return mixed[] Template data after active plugins render_picwall hook execution. * @return mixed[] Template data after active plugins hook execution.
*/ */
protected function executeHooks(array $data): array protected function executeHooks(string $template, array $data): array
{ {
$this->container->pluginManager->executeHooks( $this->container->pluginManager->executeHooks(
'render_tagcloud', 'render_'. $template,
$data, $data,
['loggedin' => $this->container->loginManager->isLoggedIn()] ['loggedin' => $this->container->loginManager->isLoggedIn()]
); );

View file

@ -45,7 +45,7 @@ http://<replace_domain>/login
http://<replace_domain>/picture-wall http://<replace_domain>/picture-wall
http://<replace_domain>/?do=pluginadmin http://<replace_domain>/?do=pluginadmin
http://<replace_domain>/tag-cloud http://<replace_domain>/tag-cloud
http://<replace_domain>/?do=taglist http://<replace_domain>/tag-list
``` ```
#### Improve existing translation #### Improve existing translation

View file

@ -622,28 +622,7 @@ function renderPage($conf, $pluginManager, $bookmarkService, $history, $sessionM
// -------- Tag list // -------- Tag list
if ($targetPage == Router::$PAGE_TAGLIST) { if ($targetPage == Router::$PAGE_TAGLIST) {
$visibility = ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : ''; header('Location: ./tag-list');
$filteringTags = isset($_GET['searchtags']) ? explode(' ', $_GET['searchtags']) : [];
$tags = $bookmarkService->bookmarksCountPerTag($filteringTags, $visibility);
if (! empty($_GET['sort']) && $_GET['sort'] === 'alpha') {
alphabetical_sort($tags, false, true);
}
$searchTags = implode(' ', escape($filteringTags));
$data = [
'search_tags' => $searchTags,
'tags' => $tags,
];
$pluginManager->executeHooks('render_taglist', $data, ['loggedin' => $loginManager->isLoggedIn()]);
foreach ($data as $key => $value) {
$PAGE->assign($key, $value);
}
$searchTags = ! empty($searchTags) ? $searchTags .' - ' : '';
$PAGE->assign('pagetitle', $searchTags . t('Tag list') .' - '. $conf->get('general.title', 'Shaarli'));
$PAGE->renderPage('tag.list');
exit; exit;
} }
@ -1870,6 +1849,7 @@ function install($conf, $sessionManager, $loginManager)
$this->get('/logout', '\Shaarli\Front\Controller\LogoutController:index')->setName('logout'); $this->get('/logout', '\Shaarli\Front\Controller\LogoutController:index')->setName('logout');
$this->get('/picture-wall', '\Shaarli\Front\Controller\PictureWallController:index')->setName('picwall'); $this->get('/picture-wall', '\Shaarli\Front\Controller\PictureWallController:index')->setName('picwall');
$this->get('/tag-cloud', '\Shaarli\Front\Controller\TagCloudController:cloud')->setName('tagcloud'); $this->get('/tag-cloud', '\Shaarli\Front\Controller\TagCloudController:cloud')->setName('tagcloud');
$this->get('/tag-list', '\Shaarli\Front\Controller\TagCloudController:list')->setName('taglist');
$this->get('/add-tag/{newTag}', '\Shaarli\Front\Controller\TagController:addTag')->setName('add-tag'); $this->get('/add-tag/{newTag}', '\Shaarli\Front\Controller\TagController:addTag')->setName('add-tag');
})->add('\Shaarli\Front\ShaarliMiddleware'); })->add('\Shaarli\Front\ShaarliMiddleware');

View file

@ -30,6 +30,9 @@ public function setUp(): void
$this->controller = new TagCloudController($this->container); $this->controller = new TagCloudController($this->container);
} }
/**
* Tag Cloud - default parameters
*/
public function testValidCloudControllerInvokeDefault(): void public function testValidCloudControllerInvokeDefault(): void
{ {
$this->createValidContainerMockSet(); $this->createValidContainerMockSet();
@ -42,7 +45,6 @@ public function testValidCloudControllerInvokeDefault(): void
$expectedOrder = ['abc', 'def', 'ghi']; $expectedOrder = ['abc', 'def', 'ghi'];
$request = $this->createMock(Request::class); $request = $this->createMock(Request::class);
$request->expects(static::once())->method('getQueryParam')->with('searchtags')->willReturn(null);
$response = new Response(); $response = new Response();
// Save RainTPL assigned variables // Save RainTPL assigned variables
@ -92,7 +94,7 @@ public function testValidCloudControllerInvokeDefault(): void
} }
/** /**
* Additional parameters: * Tag Cloud - Additional parameters:
* - logged in * - logged in
* - visibility private * - visibility private
* - search tags: `ghi` and `def` (note that filtered tags are not displayed anymore) * - search tags: `ghi` and `def` (note that filtered tags are not displayed anymore)
@ -101,18 +103,17 @@ public function testValidCloudControllerInvokeWithParameters(): void
{ {
$this->createValidContainerMockSet(); $this->createValidContainerMockSet();
$allTags = [
'ghi' => 1,
'abc' => 3,
'def' => 12,
];
$request = $this->createMock(Request::class); $request = $this->createMock(Request::class);
$request $request
->expects(static::once())
->method('getQueryParam') ->method('getQueryParam')
->with('searchtags') ->with()
->willReturn('ghi def') ->willReturnCallback(function (string $key): ?string {
if ('searchtags' === $key) {
return 'ghi def';
}
return null;
})
; ;
$response = new Response(); $response = new Response();
@ -162,12 +163,14 @@ public function testValidCloudControllerInvokeWithParameters(): void
static::assertLessThan(5, $assignedVariables['tags']['abc']['size']); static::assertLessThan(5, $assignedVariables['tags']['abc']['size']);
} }
/**
* Tag Cloud - empty
*/
public function testEmptyCloud(): void public function testEmptyCloud(): void
{ {
$this->createValidContainerMockSet(); $this->createValidContainerMockSet();
$request = $this->createMock(Request::class); $request = $this->createMock(Request::class);
$request->expects(static::once())->method('getQueryParam')->with('searchtags')->willReturn(null);
$response = new Response(); $response = new Response();
// Save RainTPL assigned variables // Save RainTPL assigned variables
@ -208,6 +211,182 @@ public function testEmptyCloud(): void
static::assertCount(0, $assignedVariables['tags']); static::assertCount(0, $assignedVariables['tags']);
} }
/**
* Tag List - Default sort is by usage DESC
*/
public function testValidListControllerInvokeDefault(): void
{
$this->createValidContainerMockSet();
$allTags = [
'def' => 12,
'abc' => 3,
'ghi' => 1,
];
$request = $this->createMock(Request::class);
$response = new Response();
// Save RainTPL assigned variables
$assignedVariables = [];
$this->assignTemplateVars($assignedVariables);
$this->container->bookmarkService
->expects(static::once())
->method('bookmarksCountPerTag')
->with([], null)
->willReturnCallback(function () use ($allTags): array {
return $allTags;
})
;
// Make sure that PluginManager hook is triggered
$this->container->pluginManager
->expects(static::at(0))
->method('executeHooks')
->willReturnCallback(function (string $hook, array $data, array $param): array {
static::assertSame('render_taglist', $hook);
static::assertSame('', $data['search_tags']);
static::assertCount(3, $data['tags']);
static::assertArrayHasKey('loggedin', $param);
return $data;
})
;
$result = $this->controller->list($request, $response);
static::assertSame(200, $result->getStatusCode());
static::assertSame('tag.list', (string) $result->getBody());
static::assertSame('Tag list - Shaarli', $assignedVariables['pagetitle']);
static::assertSame('', $assignedVariables['search_tags']);
static::assertCount(3, $assignedVariables['tags']);
foreach ($allTags as $tag => $count) {
static::assertSame($count, $assignedVariables['tags'][$tag]);
}
}
/**
* Tag List - Additional parameters:
* - logged in
* - visibility private
* - search tags: `ghi` and `def` (note that filtered tags are not displayed anymore)
* - sort alphabetically
*/
public function testValidListControllerInvokeWithParameters(): void
{
$this->createValidContainerMockSet();
$request = $this->createMock(Request::class);
$request
->method('getQueryParam')
->with()
->willReturnCallback(function (string $key): ?string {
if ('searchtags' === $key) {
return 'ghi def';
} elseif ('sort' === $key) {
return 'alpha';
}
return null;
})
;
$response = new Response();
// Save RainTPL assigned variables
$assignedVariables = [];
$this->assignTemplateVars($assignedVariables);
$this->container->loginManager->method('isLoggedin')->willReturn(true);
$this->container->sessionManager->expects(static::once())->method('getSessionParameter')->willReturn('private');
$this->container->bookmarkService
->expects(static::once())
->method('bookmarksCountPerTag')
->with(['ghi', 'def'], BookmarkFilter::$PRIVATE)
->willReturnCallback(function (): array {
return ['abc' => 3];
})
;
// Make sure that PluginManager hook is triggered
$this->container->pluginManager
->expects(static::at(0))
->method('executeHooks')
->willReturnCallback(function (string $hook, array $data, array $param): array {
static::assertSame('render_taglist', $hook);
static::assertSame('ghi def', $data['search_tags']);
static::assertCount(1, $data['tags']);
static::assertArrayHasKey('loggedin', $param);
return $data;
})
;
$result = $this->controller->list($request, $response);
static::assertSame(200, $result->getStatusCode());
static::assertSame('tag.list', (string) $result->getBody());
static::assertSame('ghi def - Tag list - Shaarli', $assignedVariables['pagetitle']);
static::assertSame('ghi def', $assignedVariables['search_tags']);
static::assertCount(1, $assignedVariables['tags']);
static::assertSame(3, $assignedVariables['tags']['abc']);
}
/**
* Tag List - empty
*/
public function testEmptyList(): void
{
$this->createValidContainerMockSet();
$request = $this->createMock(Request::class);
$response = new Response();
// Save RainTPL assigned variables
$assignedVariables = [];
$this->assignTemplateVars($assignedVariables);
$this->container->bookmarkService
->expects(static::once())
->method('bookmarksCountPerTag')
->with([], null)
->willReturnCallback(function (array $parameters, ?string $visibility): array {
return [];
})
;
// Make sure that PluginManager hook is triggered
$this->container->pluginManager
->expects(static::at(0))
->method('executeHooks')
->willReturnCallback(function (string $hook, array $data, array $param): array {
static::assertSame('render_taglist', $hook);
static::assertSame('', $data['search_tags']);
static::assertCount(0, $data['tags']);
static::assertArrayHasKey('loggedin', $param);
return $data;
})
;
$result = $this->controller->list($request, $response);
static::assertSame(200, $result->getStatusCode());
static::assertSame('tag.list', (string) $result->getBody());
static::assertSame('Tag list - Shaarli', $assignedVariables['pagetitle']);
static::assertSame('', $assignedVariables['search_tags']);
static::assertCount(0, $assignedVariables['tags']);
}
protected function createValidContainerMockSet(): void protected function createValidContainerMockSet(): void
{ {
$loginManager = $this->createMock(LoginManager::class); $loginManager = $this->createMock(LoginManager::class);

View file

@ -32,7 +32,7 @@ <h2 class="window-title">{"Manage tags"|t}</h2>
</div> </div>
</form> </form>
<p>{'You can also edit tags in the'|t} <a href="./?do=taglist&sort=usage">{'tag list'|t}</a>.</p> <p>{'You can also edit tags in the'|t} <a href="./tag-list?sort=usage">{'tag list'|t}</a>.</p>
</div> </div>
</div> </div>
{include="page.footer"} {include="page.footer"}

View file

@ -15,7 +15,7 @@
<h2 class="window-title">{'Tag list'|t} - {$countTags} {'tags'|t}</h2> <h2 class="window-title">{'Tag list'|t} - {$countTags} {'tags'|t}</h2>
{if="!empty($search_tags)"} {if="!empty($search_tags)"}
<p class="center"> <p class="center">
<a href="?searchtags={$search_tags|urlencode}" class="pure-button pure-button-shaarli"> <a href="./?searchtags={$search_tags|urlencode}" class="pure-button pure-button-shaarli">
{'List all links with those tags'|t} {'List all links with those tags'|t}
</a> </a>
</p> </p>
@ -57,7 +57,7 @@ <h2 class="window-title">{'Tag list'|t} - {$countTags} {'tags'|t}</h2>
{/if} {/if}
<a href="./add-tag/{$key|urlencode}" title="{'Filter by tag'|t}" class="count">{$value}</a> <a href="./add-tag/{$key|urlencode}" title="{'Filter by tag'|t}" class="count">{$value}</a>
<a href="?searchtags={$key|urlencode} {$search_tags|urlencode}" class="tag-link">{$key}</a> <a href="./?searchtags={$key|urlencode} {$search_tags|urlencode}" class="tag-link">{$key}</a>
{loop="$value.tag_plugin"} {loop="$value.tag_plugin"}
{$value} {$value}

View file

@ -2,7 +2,7 @@
<div class="pure-u-1 pure-alert pure-alert-success tag-sort"> <div class="pure-u-1 pure-alert pure-alert-success tag-sort">
{'Sort by:'|t} {'Sort by:'|t}
<a href="./tag-cloud">{'Cloud'|t}</a> &middot; <a href="./tag-cloud">{'Cloud'|t}</a> &middot;
<a href="./?do=taglist&sort=usage">{'Most used'|t}</a> &middot; <a href="./tag-list?sort=usage">{'Most used'|t}</a> &middot;
<a href="./?do=taglist&sort=alpha">{'Alphabetical'|t}</a> <a href="./tag-list?sort=alpha">{'Alphabetical'|t}</a>
</div> </div>
</div> </div>