aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--application/front/controllers/TagCloudController.php59
-rw-r--r--doc/md/Translations.md2
-rw-r--r--index.php24
-rw-r--r--tests/front/controller/TagCloudControllerTest.php203
-rw-r--r--tpl/default/changetag.html2
-rw-r--r--tpl/default/tag.list.html4
-rw-r--r--tpl/default/tag.sort.html4
7 files changed, 247 insertions, 51 deletions
diff --git a/application/front/controllers/TagCloudController.php b/application/front/controllers/TagCloudController.php
index 93e3ae27..1ff7c2e6 100644
--- a/application/front/controllers/TagCloudController.php
+++ b/application/front/controllers/TagCloudController.php
@@ -10,12 +10,15 @@ use Slim\Http\Response;
10/** 10/**
11 * Class TagCloud 11 * Class TagCloud
12 * 12 *
13 * Slim controller used to render the tag cloud page. 13 * Slim controller used to render the tag cloud and tag list pages.
14 * 14 *
15 * @package Front\Controller 15 * @package Front\Controller
16 */ 16 */
17class TagCloudController extends ShaarliController 17class TagCloudController extends ShaarliController
18{ 18{
19 protected const TYPE_CLOUD = 'cloud';
20 protected const TYPE_LIST = 'list';
21
19 /** 22 /**
20 * Display the tag cloud through the template engine. 23 * Display the tag cloud through the template engine.
21 * This controller a few filters: 24 * This controller a few filters:
@@ -24,26 +27,53 @@ class TagCloudController extends ShaarliController
24 */ 27 */
25 public function cloud(Request $request, Response $response): Response 28 public function cloud(Request $request, Response $response): Response
26 { 29 {
30 return $this->processRequest(static::TYPE_CLOUD, $request, $response);
31 }
32
33 /**
34 * Display the tag list through the template engine.
35 * This controller a few filters:
36 * - Visibility stored in the session for logged in users
37 * - `searchtags` query parameter: will return tags associated with filter in at least one bookmark
38 * - `sort` query parameters:
39 * + `usage` (default): most used tags first
40 * + `alpha`: alphabetical order
41 */
42 public function list(Request $request, Response $response): Response
43 {
44 return $this->processRequest(static::TYPE_LIST, $request, $response);
45 }
46
47 /**
48 * Process the request for both tag cloud and tag list endpoints.
49 */
50 protected function processRequest(string $type, Request $request, Response $response): Response
51 {
27 if ($this->container->loginManager->isLoggedIn() === true) { 52 if ($this->container->loginManager->isLoggedIn() === true) {
28 $visibility = $this->container->sessionManager->getSessionParameter('visibility'); 53 $visibility = $this->container->sessionManager->getSessionParameter('visibility');
29 } 54 }
30 55
56 $sort = $request->getQueryParam('sort');
31 $searchTags = $request->getQueryParam('searchtags'); 57 $searchTags = $request->getQueryParam('searchtags');
32 $filteringTags = $searchTags !== null ? explode(' ', $searchTags) : []; 58 $filteringTags = $searchTags !== null ? explode(' ', $searchTags) : [];
33 59
34 $tags = $this->container->bookmarkService->bookmarksCountPerTag($filteringTags, $visibility ?? null); 60 $tags = $this->container->bookmarkService->bookmarksCountPerTag($filteringTags, $visibility ?? null);
35 61
36 // TODO: the sorting should be handled by bookmarkService instead of the controller 62 if (static::TYPE_CLOUD === $type || 'alpha' === $sort) {
37 alphabetical_sort($tags, false, true); 63 // TODO: the sorting should be handled by bookmarkService instead of the controller
64 alphabetical_sort($tags, false, true);
65 }
38 66
39 $tagList = $this->formatTagsForCloud($tags); 67 if (static::TYPE_CLOUD === $type) {
68 $tags = $this->formatTagsForCloud($tags);
69 }
40 70
41 $searchTags = implode(' ', escape($filteringTags)); 71 $searchTags = implode(' ', escape($filteringTags));
42 $data = [ 72 $data = [
43 'search_tags' => $searchTags, 73 'search_tags' => $searchTags,
44 'tags' => $tagList, 74 'tags' => $tags,
45 ]; 75 ];
46 $data = $this->executeHooks($data); 76 $data = $this->executeHooks('tag' . $type, $data);
47 foreach ($data as $key => $value) { 77 foreach ($data as $key => $value) {
48 $this->assignView($key, $value); 78 $this->assignView($key, $value);
49 } 79 }
@@ -51,12 +81,19 @@ class TagCloudController extends ShaarliController
51 $searchTags = !empty($searchTags) ? $searchTags .' - ' : ''; 81 $searchTags = !empty($searchTags) ? $searchTags .' - ' : '';
52 $this->assignView( 82 $this->assignView(
53 'pagetitle', 83 'pagetitle',
54 $searchTags . t('Tag cloud') .' - '. $this->container->conf->get('general.title', 'Shaarli') 84 $searchTags . t('Tag '. $type) .' - '. $this->container->conf->get('general.title', 'Shaarli')
55 ); 85 );
56 86
57 return $response->write($this->render('tag.cloud')); 87 return $response->write($this->render('tag.'. $type));
58 } 88 }
59 89
90 /**
91 * Format the tags array for the tag cloud template.
92 *
93 * @param array<string, int> $tags List of tags as key with count as value
94 *
95 * @return mixed[] List of tags as key, with count and expected font size in a subarray
96 */
60 protected function formatTagsForCloud(array $tags): array 97 protected function formatTagsForCloud(array $tags): array
61 { 98 {
62 // We sort tags alphabetically, then choose a font size according to count. 99 // We sort tags alphabetically, then choose a font size according to count.
@@ -81,12 +118,12 @@ class TagCloudController extends ShaarliController
81 /** 118 /**
82 * @param mixed[] $data Template data 119 * @param mixed[] $data Template data
83 * 120 *
84 * @return mixed[] Template data after active plugins render_picwall hook execution. 121 * @return mixed[] Template data after active plugins hook execution.
85 */ 122 */
86 protected function executeHooks(array $data): array 123 protected function executeHooks(string $template, array $data): array
87 { 124 {
88 $this->container->pluginManager->executeHooks( 125 $this->container->pluginManager->executeHooks(
89 'render_tagcloud', 126 'render_'. $template,
90 $data, 127 $data,
91 ['loggedin' => $this->container->loginManager->isLoggedIn()] 128 ['loggedin' => $this->container->loginManager->isLoggedIn()]
92 ); 129 );
diff --git a/doc/md/Translations.md b/doc/md/Translations.md
index b8b7053f..c1a2885d 100644
--- a/doc/md/Translations.md
+++ b/doc/md/Translations.md
@@ -45,7 +45,7 @@ http://<replace_domain>/login
45http://<replace_domain>/picture-wall 45http://<replace_domain>/picture-wall
46http://<replace_domain>/?do=pluginadmin 46http://<replace_domain>/?do=pluginadmin
47http://<replace_domain>/tag-cloud 47http://<replace_domain>/tag-cloud
48http://<replace_domain>/?do=taglist 48http://<replace_domain>/tag-list
49``` 49```
50 50
51#### Improve existing translation 51#### Improve existing translation
diff --git a/index.php b/index.php
index 6ecb9a67..89a1e581 100644
--- a/index.php
+++ b/index.php
@@ -622,28 +622,7 @@ function renderPage($conf, $pluginManager, $bookmarkService, $history, $sessionM
622 622
623 // -------- Tag list 623 // -------- Tag list
624 if ($targetPage == Router::$PAGE_TAGLIST) { 624 if ($targetPage == Router::$PAGE_TAGLIST) {
625 $visibility = ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : ''; 625 header('Location: ./tag-list');
626 $filteringTags = isset($_GET['searchtags']) ? explode(' ', $_GET['searchtags']) : [];
627 $tags = $bookmarkService->bookmarksCountPerTag($filteringTags, $visibility);
628
629 if (! empty($_GET['sort']) && $_GET['sort'] === 'alpha') {
630 alphabetical_sort($tags, false, true);
631 }
632
633 $searchTags = implode(' ', escape($filteringTags));
634 $data = [
635 'search_tags' => $searchTags,
636 'tags' => $tags,
637 ];
638 $pluginManager->executeHooks('render_taglist', $data, ['loggedin' => $loginManager->isLoggedIn()]);
639
640 foreach ($data as $key => $value) {
641 $PAGE->assign($key, $value);
642 }
643
644 $searchTags = ! empty($searchTags) ? $searchTags .' - ' : '';
645 $PAGE->assign('pagetitle', $searchTags . t('Tag list') .' - '. $conf->get('general.title', 'Shaarli'));
646 $PAGE->renderPage('tag.list');
647 exit; 626 exit;
648 } 627 }
649 628
@@ -1870,6 +1849,7 @@ $app->group('', function () {
1870 $this->get('/logout', '\Shaarli\Front\Controller\LogoutController:index')->setName('logout'); 1849 $this->get('/logout', '\Shaarli\Front\Controller\LogoutController:index')->setName('logout');
1871 $this->get('/picture-wall', '\Shaarli\Front\Controller\PictureWallController:index')->setName('picwall'); 1850 $this->get('/picture-wall', '\Shaarli\Front\Controller\PictureWallController:index')->setName('picwall');
1872 $this->get('/tag-cloud', '\Shaarli\Front\Controller\TagCloudController:cloud')->setName('tagcloud'); 1851 $this->get('/tag-cloud', '\Shaarli\Front\Controller\TagCloudController:cloud')->setName('tagcloud');
1852 $this->get('/tag-list', '\Shaarli\Front\Controller\TagCloudController:list')->setName('taglist');
1873 $this->get('/add-tag/{newTag}', '\Shaarli\Front\Controller\TagController:addTag')->setName('add-tag'); 1853 $this->get('/add-tag/{newTag}', '\Shaarli\Front\Controller\TagController:addTag')->setName('add-tag');
1874})->add('\Shaarli\Front\ShaarliMiddleware'); 1854})->add('\Shaarli\Front\ShaarliMiddleware');
1875 1855
diff --git a/tests/front/controller/TagCloudControllerTest.php b/tests/front/controller/TagCloudControllerTest.php
index 352bdee2..719610d7 100644
--- a/tests/front/controller/TagCloudControllerTest.php
+++ b/tests/front/controller/TagCloudControllerTest.php
@@ -30,6 +30,9 @@ class TagCloudControllerTest extends TestCase
30 $this->controller = new TagCloudController($this->container); 30 $this->controller = new TagCloudController($this->container);
31 } 31 }
32 32
33 /**
34 * Tag Cloud - default parameters
35 */
33 public function testValidCloudControllerInvokeDefault(): void 36 public function testValidCloudControllerInvokeDefault(): void
34 { 37 {
35 $this->createValidContainerMockSet(); 38 $this->createValidContainerMockSet();
@@ -42,7 +45,6 @@ class TagCloudControllerTest extends TestCase
42 $expectedOrder = ['abc', 'def', 'ghi']; 45 $expectedOrder = ['abc', 'def', 'ghi'];
43 46
44 $request = $this->createMock(Request::class); 47 $request = $this->createMock(Request::class);
45 $request->expects(static::once())->method('getQueryParam')->with('searchtags')->willReturn(null);
46 $response = new Response(); 48 $response = new Response();
47 49
48 // Save RainTPL assigned variables 50 // Save RainTPL assigned variables
@@ -92,7 +94,7 @@ class TagCloudControllerTest extends TestCase
92 } 94 }
93 95
94 /** 96 /**
95 * Additional parameters: 97 * Tag Cloud - Additional parameters:
96 * - logged in 98 * - logged in
97 * - visibility private 99 * - visibility private
98 * - search tags: `ghi` and `def` (note that filtered tags are not displayed anymore) 100 * - search tags: `ghi` and `def` (note that filtered tags are not displayed anymore)
@@ -101,18 +103,17 @@ class TagCloudControllerTest extends TestCase
101 { 103 {
102 $this->createValidContainerMockSet(); 104 $this->createValidContainerMockSet();
103 105
104 $allTags = [
105 'ghi' => 1,
106 'abc' => 3,
107 'def' => 12,
108 ];
109
110 $request = $this->createMock(Request::class); 106 $request = $this->createMock(Request::class);
111 $request 107 $request
112 ->expects(static::once())
113 ->method('getQueryParam') 108 ->method('getQueryParam')
114 ->with('searchtags') 109 ->with()
115 ->willReturn('ghi def') 110 ->willReturnCallback(function (string $key): ?string {
111 if ('searchtags' === $key) {
112 return 'ghi def';
113 }
114
115 return null;
116 })
116 ; 117 ;
117 $response = new Response(); 118 $response = new Response();
118 119
@@ -162,12 +163,14 @@ class TagCloudControllerTest extends TestCase
162 static::assertLessThan(5, $assignedVariables['tags']['abc']['size']); 163 static::assertLessThan(5, $assignedVariables['tags']['abc']['size']);
163 } 164 }
164 165
166 /**
167 * Tag Cloud - empty
168 */
165 public function testEmptyCloud(): void 169 public function testEmptyCloud(): void
166 { 170 {
167 $this->createValidContainerMockSet(); 171 $this->createValidContainerMockSet();
168 172
169 $request = $this->createMock(Request::class); 173 $request = $this->createMock(Request::class);
170 $request->expects(static::once())->method('getQueryParam')->with('searchtags')->willReturn(null);
171 $response = new Response(); 174 $response = new Response();
172 175
173 // Save RainTPL assigned variables 176 // Save RainTPL assigned variables
@@ -208,6 +211,182 @@ class TagCloudControllerTest extends TestCase
208 static::assertCount(0, $assignedVariables['tags']); 211 static::assertCount(0, $assignedVariables['tags']);
209 } 212 }
210 213
214 /**
215 * Tag List - Default sort is by usage DESC
216 */
217 public function testValidListControllerInvokeDefault(): void
218 {
219 $this->createValidContainerMockSet();
220
221 $allTags = [
222 'def' => 12,
223 'abc' => 3,
224 'ghi' => 1,
225 ];
226
227 $request = $this->createMock(Request::class);
228 $response = new Response();
229
230 // Save RainTPL assigned variables
231 $assignedVariables = [];
232 $this->assignTemplateVars($assignedVariables);
233
234 $this->container->bookmarkService
235 ->expects(static::once())
236 ->method('bookmarksCountPerTag')
237 ->with([], null)
238 ->willReturnCallback(function () use ($allTags): array {
239 return $allTags;
240 })
241 ;
242
243 // Make sure that PluginManager hook is triggered
244 $this->container->pluginManager
245 ->expects(static::at(0))
246 ->method('executeHooks')
247 ->willReturnCallback(function (string $hook, array $data, array $param): array {
248 static::assertSame('render_taglist', $hook);
249 static::assertSame('', $data['search_tags']);
250 static::assertCount(3, $data['tags']);
251
252 static::assertArrayHasKey('loggedin', $param);
253
254 return $data;
255 })
256 ;
257
258 $result = $this->controller->list($request, $response);
259
260 static::assertSame(200, $result->getStatusCode());
261 static::assertSame('tag.list', (string) $result->getBody());
262 static::assertSame('Tag list - Shaarli', $assignedVariables['pagetitle']);
263
264 static::assertSame('', $assignedVariables['search_tags']);
265 static::assertCount(3, $assignedVariables['tags']);
266
267 foreach ($allTags as $tag => $count) {
268 static::assertSame($count, $assignedVariables['tags'][$tag]);
269 }
270 }
271
272 /**
273 * Tag List - Additional parameters:
274 * - logged in
275 * - visibility private
276 * - search tags: `ghi` and `def` (note that filtered tags are not displayed anymore)
277 * - sort alphabetically
278 */
279 public function testValidListControllerInvokeWithParameters(): void
280 {
281 $this->createValidContainerMockSet();
282
283 $request = $this->createMock(Request::class);
284 $request
285 ->method('getQueryParam')
286 ->with()
287 ->willReturnCallback(function (string $key): ?string {
288 if ('searchtags' === $key) {
289 return 'ghi def';
290 } elseif ('sort' === $key) {
291 return 'alpha';
292 }
293
294 return null;
295 })
296 ;
297 $response = new Response();
298
299 // Save RainTPL assigned variables
300 $assignedVariables = [];
301 $this->assignTemplateVars($assignedVariables);
302
303 $this->container->loginManager->method('isLoggedin')->willReturn(true);
304 $this->container->sessionManager->expects(static::once())->method('getSessionParameter')->willReturn('private');
305
306 $this->container->bookmarkService
307 ->expects(static::once())
308 ->method('bookmarksCountPerTag')
309 ->with(['ghi', 'def'], BookmarkFilter::$PRIVATE)
310 ->willReturnCallback(function (): array {
311 return ['abc' => 3];
312 })
313 ;
314
315 // Make sure that PluginManager hook is triggered
316 $this->container->pluginManager
317 ->expects(static::at(0))
318 ->method('executeHooks')
319 ->willReturnCallback(function (string $hook, array $data, array $param): array {
320 static::assertSame('render_taglist', $hook);
321 static::assertSame('ghi def', $data['search_tags']);
322 static::assertCount(1, $data['tags']);
323
324 static::assertArrayHasKey('loggedin', $param);
325
326 return $data;
327 })
328 ;
329
330 $result = $this->controller->list($request, $response);
331
332 static::assertSame(200, $result->getStatusCode());
333 static::assertSame('tag.list', (string) $result->getBody());
334 static::assertSame('ghi def - Tag list - Shaarli', $assignedVariables['pagetitle']);
335
336 static::assertSame('ghi def', $assignedVariables['search_tags']);
337 static::assertCount(1, $assignedVariables['tags']);
338 static::assertSame(3, $assignedVariables['tags']['abc']);
339 }
340
341 /**
342 * Tag List - empty
343 */
344 public function testEmptyList(): void
345 {
346 $this->createValidContainerMockSet();
347
348 $request = $this->createMock(Request::class);
349 $response = new Response();
350
351 // Save RainTPL assigned variables
352 $assignedVariables = [];
353 $this->assignTemplateVars($assignedVariables);
354
355 $this->container->bookmarkService
356 ->expects(static::once())
357 ->method('bookmarksCountPerTag')
358 ->with([], null)
359 ->willReturnCallback(function (array $parameters, ?string $visibility): array {
360 return [];
361 })
362 ;
363
364 // Make sure that PluginManager hook is triggered
365 $this->container->pluginManager
366 ->expects(static::at(0))
367 ->method('executeHooks')
368 ->willReturnCallback(function (string $hook, array $data, array $param): array {
369 static::assertSame('render_taglist', $hook);
370 static::assertSame('', $data['search_tags']);
371 static::assertCount(0, $data['tags']);
372
373 static::assertArrayHasKey('loggedin', $param);
374
375 return $data;
376 })
377 ;
378
379 $result = $this->controller->list($request, $response);
380
381 static::assertSame(200, $result->getStatusCode());
382 static::assertSame('tag.list', (string) $result->getBody());
383 static::assertSame('Tag list - Shaarli', $assignedVariables['pagetitle']);
384
385 static::assertSame('', $assignedVariables['search_tags']);
386 static::assertCount(0, $assignedVariables['tags']);
387 }
388
389
211 protected function createValidContainerMockSet(): void 390 protected function createValidContainerMockSet(): void
212 { 391 {
213 $loginManager = $this->createMock(LoginManager::class); 392 $loginManager = $this->createMock(LoginManager::class);
diff --git a/tpl/default/changetag.html b/tpl/default/changetag.html
index cc74f786..a1a572ca 100644
--- a/tpl/default/changetag.html
+++ b/tpl/default/changetag.html
@@ -32,7 +32,7 @@
32 </div> 32 </div>
33 </form> 33 </form>
34 34
35 <p>{'You can also edit tags in the'|t} <a href="./?do=taglist&sort=usage">{'tag list'|t}</a>.</p> 35 <p>{'You can also edit tags in the'|t} <a href="./tag-list?sort=usage">{'tag list'|t}</a>.</p>
36 </div> 36 </div>
37</div> 37</div>
38{include="page.footer"} 38{include="page.footer"}
diff --git a/tpl/default/tag.list.html b/tpl/default/tag.list.html
index 01b7a642..3e498f89 100644
--- a/tpl/default/tag.list.html
+++ b/tpl/default/tag.list.html
@@ -15,7 +15,7 @@
15 <h2 class="window-title">{'Tag list'|t} - {$countTags} {'tags'|t}</h2> 15 <h2 class="window-title">{'Tag list'|t} - {$countTags} {'tags'|t}</h2>
16 {if="!empty($search_tags)"} 16 {if="!empty($search_tags)"}
17 <p class="center"> 17 <p class="center">
18 <a href="?searchtags={$search_tags|urlencode}" class="pure-button pure-button-shaarli"> 18 <a href="./?searchtags={$search_tags|urlencode}" class="pure-button pure-button-shaarli">
19 {'List all links with those tags'|t} 19 {'List all links with those tags'|t}
20 </a> 20 </a>
21 </p> 21 </p>
@@ -57,7 +57,7 @@
57 {/if} 57 {/if}
58 58
59 <a href="./add-tag/{$key|urlencode}" title="{'Filter by tag'|t}" class="count">{$value}</a> 59 <a href="./add-tag/{$key|urlencode}" title="{'Filter by tag'|t}" class="count">{$value}</a>
60 <a href="?searchtags={$key|urlencode} {$search_tags|urlencode}" class="tag-link">{$key}</a> 60 <a href="./?searchtags={$key|urlencode} {$search_tags|urlencode}" class="tag-link">{$key}</a>
61 61
62 {loop="$value.tag_plugin"} 62 {loop="$value.tag_plugin"}
63 {$value} 63 {$value}
diff --git a/tpl/default/tag.sort.html b/tpl/default/tag.sort.html
index b7aa7d80..f467e34a 100644
--- a/tpl/default/tag.sort.html
+++ b/tpl/default/tag.sort.html
@@ -2,7 +2,7 @@
2 <div class="pure-u-1 pure-alert pure-alert-success tag-sort"> 2 <div class="pure-u-1 pure-alert pure-alert-success tag-sort">
3 {'Sort by:'|t} 3 {'Sort by:'|t}
4 <a href="./tag-cloud">{'Cloud'|t}</a> &middot; 4 <a href="./tag-cloud">{'Cloud'|t}</a> &middot;
5 <a href="./?do=taglist&sort=usage">{'Most used'|t}</a> &middot; 5 <a href="./tag-list?sort=usage">{'Most used'|t}</a> &middot;
6 <a href="./?do=taglist&sort=alpha">{'Alphabetical'|t}</a> 6 <a href="./tag-list?sort=alpha">{'Alphabetical'|t}</a>
7 </div> 7 </div>
8</div> 8</div>