Process picwall rendering through Slim controller + UT

This commit is contained in:
ArthurHoaro 2020-01-26 11:15:15 +01:00
parent bee33239ed
commit 485b168a96
15 changed files with 346 additions and 88 deletions

View file

@ -14,7 +14,7 @@ indent_size = 4
indent_size = 2 indent_size = 2
[*.php] [*.php]
max_line_length = 100 max_line_length = 120
[Dockerfile] [Dockerfile]
max_line_length = 80 max_line_length = 80

View file

@ -346,7 +346,7 @@ public function setTags($tags)
/** /**
* Get the Thumbnail. * Get the Thumbnail.
* *
* @return string|bool * @return string|bool|null
*/ */
public function getThumbnail() public function getThumbnail()
{ {

View file

@ -7,6 +7,7 @@
use Shaarli\Bookmark\BookmarkFileService; use Shaarli\Bookmark\BookmarkFileService;
use Shaarli\Bookmark\BookmarkServiceInterface; use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\Formatter\FormatterFactory;
use Shaarli\History; use Shaarli\History;
use Shaarli\Plugin\PluginManager; use Shaarli\Plugin\PluginManager;
use Shaarli\Render\PageBuilder; use Shaarli\Render\PageBuilder;
@ -76,6 +77,10 @@ public function build(): ShaarliContainer
return new PluginManager($container->conf); return new PluginManager($container->conf);
}; };
$container['formatterFactory'] = function (ShaarliContainer $container): FormatterFactory {
return new FormatterFactory($container->conf, $container->loginManager->isLoggedIn());
};
return $container; return $container;
} }
} }

View file

@ -6,6 +6,7 @@
use Shaarli\Bookmark\BookmarkServiceInterface; use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\Formatter\FormatterFactory;
use Shaarli\History; use Shaarli\History;
use Shaarli\Plugin\PluginManager; use Shaarli\Plugin\PluginManager;
use Shaarli\Render\PageBuilder; use Shaarli\Render\PageBuilder;
@ -23,6 +24,7 @@
* @property BookmarkServiceInterface $bookmarkService * @property BookmarkServiceInterface $bookmarkService
* @property PageBuilder $pageBuilder * @property PageBuilder $pageBuilder
* @property PluginManager $pluginManager * @property PluginManager $pluginManager
* @property FormatterFactory $formatterFactory
*/ */
class ShaarliContainer extends Container class ShaarliContainer extends Container
{ {

View file

@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace Shaarli\Front\Controller;
use Shaarli\Front\Exception\ThumbnailsDisabledException;
use Shaarli\Thumbnailer;
use Slim\Http\Request;
use Slim\Http\Response;
/**
* Class PicturesWallController
*
* Slim controller used to render the pictures wall page.
* If thumbnails mode is set to NONE, we just render the template without any image.
*
* @package Front\Controller
*/
class PictureWallController extends ShaarliController
{
public function index(Request $request, Response $response): Response
{
if ($this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) === Thumbnailer::MODE_NONE) {
throw new ThumbnailsDisabledException();
}
$this->assignView(
'pagetitle',
t('Picture wall') .' - '. $this->container->conf->get('general.title', 'Shaarli')
);
// Optionally filter the results:
$links = $this->container->bookmarkService->search($request->getQueryParams());
$linksToDisplay = [];
// Get only bookmarks which have a thumbnail.
// Note: we do not retrieve thumbnails here, the request is too heavy.
$formatter = $this->container->formatterFactory->getFormatter('raw');
foreach ($links as $key => $link) {
if (!empty($link->getThumbnail())) {
$linksToDisplay[] = $formatter->format($link);
}
}
$data = $this->executeHooks($linksToDisplay);
foreach ($data as $key => $value) {
$this->assignView($key, $value);
}
return $response->write($this->render('picwall'));
}
/**
* @param mixed[] $linksToDisplay List of formatted bookmarks
*
* @return mixed[] Template data after active plugins render_picwall hook execution.
*/
protected function executeHooks(array $linksToDisplay): array
{
$data = [
'linksToDisplay' => $linksToDisplay,
];
$this->container->pluginManager->executeHooks(
'render_picwall',
$data,
['loggedin' => $this->container->loginManager->isLoggedIn()]
);
return $data;
}
}

View file

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Shaarli\Front\Exception;
class ThumbnailsDisabledException extends ShaarliException
{
public function __construct()
{
$message = t('Picture wall unavailable (thumbnails are disabled).');
parent::__construct($message, 400);
}
}

View file

@ -2,8 +2,8 @@
namespace Shaarli\Updater; namespace Shaarli\Updater;
use Shaarli\Config\ConfigManager;
use Shaarli\Bookmark\BookmarkServiceInterface; use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\Config\ConfigManager;
use Shaarli\Updater\Exception\UpdaterException; use Shaarli\Updater\Exception\UpdaterException;
/** /**
@ -111,4 +111,20 @@ public function getDoneUpdates()
{ {
return $this->doneUpdates; return $this->doneUpdates;
} }
/**
* With the Slim routing system, default header link should be `./` instead of `?`.
* Otherwise you can not go back to the home page. Example: `/picture-wall` -> `/picture-wall?` instead of `/`.
*/
public function updateMethodRelativeHomeLink(): bool
{
$link = trim($this->conf->get('general.header_link'));
if ($link[0] === '?') {
$link = './'. ltrim($link, '?');
$this->conf->set('general.header_link', $link, true, true);
}
return true;
}
} }

View file

@ -23,6 +23,6 @@ For example, if you want to subscribe only to links tagged `photography`:
- The same method **also works for a full-text search** (_Search_ box) **and for the Picture Wall** (want to only see pictures about `nature`?) - The same method **also works for a full-text search** (_Search_ box) **and for the Picture Wall** (want to only see pictures about `nature`?)
- You can also build the URLs manually: - You can also build the URLs manually:
- `https://my.shaarli.domain/?do=rss&searchtags=nature` - `https://my.shaarli.domain/?do=rss&searchtags=nature`
- `https://my.shaarli.domain/links/?do=picwall&searchterm=poney` - `https://my.shaarli.domain/links/picture-wall?searchterm=poney`
![](images/rss-filter-1.png) ![](images/rss-filter-2.png) ![](images/rss-filter-1.png) ![](images/rss-filter-2.png)

View file

@ -42,7 +42,7 @@ http://<replace_domain>/?post
http://<replace_domain>/?do=export http://<replace_domain>/?do=export
http://<replace_domain>/?do=import http://<replace_domain>/?do=import
http://<replace_domain>/login http://<replace_domain>/login
http://<replace_domain>/?do=picwall http://<replace_domain>/picture-wall
http://<replace_domain>/?do=pluginadmin http://<replace_domain>/?do=pluginadmin
http://<replace_domain>/?do=tagcloud http://<replace_domain>/?do=tagcloud
http://<replace_domain>/?do=taglist http://<replace_domain>/?do=taglist

View file

@ -610,37 +610,7 @@ function renderPage($conf, $pluginManager, $bookmarkService, $history, $sessionM
// -------- Picture wall // -------- Picture wall
if ($targetPage == Router::$PAGE_PICWALL) { if ($targetPage == Router::$PAGE_PICWALL) {
$PAGE->assign('pagetitle', t('Picture wall') .' - '. $conf->get('general.title', 'Shaarli')); header('Location: ./picture-wall');
if (! $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) === Thumbnailer::MODE_NONE) {
$PAGE->assign('linksToDisplay', []);
$PAGE->renderPage('picwall');
exit;
}
// Optionally filter the results:
$links = $bookmarkService->search($_GET);
$linksToDisplay = [];
// Get only bookmarks which have a thumbnail.
// Note: we do not retrieve thumbnails here, the request is too heavy.
$factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
$formatter = $factory->getFormatter();
foreach ($links as $key => $link) {
if ($link->getThumbnail() !== false) {
$linksToDisplay[] = $formatter->format($link);
}
}
$data = [
'linksToDisplay' => $linksToDisplay,
];
$pluginManager->executeHooks('render_picwall', $data, ['loggedin' => $loginManager->isLoggedIn()]);
foreach ($data as $key => $value) {
$PAGE->assign($key, $value);
}
$PAGE->renderPage('picwall');
exit; exit;
} }
@ -1944,6 +1914,7 @@ function install($conf, $sessionManager, $loginManager)
$app->group('', function () { $app->group('', function () {
$this->get('/login', '\Shaarli\Front\Controller\LoginController:index')->setName('login'); $this->get('/login', '\Shaarli\Front\Controller\LoginController:index')->setName('login');
$this->get('/picture-wall', '\Shaarli\Front\Controller\PictureWallController:index')->setName('picwall');
})->add('\Shaarli\Front\ShaarliMiddleware'); })->add('\Shaarli\Front\ShaarliMiddleware');
$response = $app->run(true); $response = $app->run(true);

View file

@ -7,6 +7,7 @@
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Shaarli\Bookmark\BookmarkServiceInterface; use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\Formatter\FormatterFactory;
use Shaarli\History; use Shaarli\History;
use Shaarli\Render\PageBuilder; use Shaarli\Render\PageBuilder;
use Shaarli\Security\LoginManager; use Shaarli\Security\LoginManager;
@ -30,7 +31,9 @@ public function setUp(): void
{ {
$this->conf = new ConfigManager('tests/utils/config/configJson'); $this->conf = new ConfigManager('tests/utils/config/configJson');
$this->sessionManager = $this->createMock(SessionManager::class); $this->sessionManager = $this->createMock(SessionManager::class);
$this->loginManager = $this->createMock(LoginManager::class); $this->loginManager = $this->createMock(LoginManager::class);
$this->loginManager->method('isLoggedIn')->willReturn(true);
$this->containerBuilder = new ContainerBuilder($this->conf, $this->sessionManager, $this->loginManager); $this->containerBuilder = new ContainerBuilder($this->conf, $this->sessionManager, $this->loginManager);
} }
@ -45,5 +48,6 @@ public function testBuildContainer(): void
static::assertInstanceOf(History::class, $container->history); static::assertInstanceOf(History::class, $container->history);
static::assertInstanceOf(BookmarkServiceInterface::class, $container->bookmarkService); static::assertInstanceOf(BookmarkServiceInterface::class, $container->bookmarkService);
static::assertInstanceOf(PageBuilder::class, $container->pageBuilder); static::assertInstanceOf(PageBuilder::class, $container->pageBuilder);
static::assertInstanceOf(FormatterFactory::class, $container->formatterFactory);
} }
} }

View file

@ -0,0 +1,180 @@
<?php
declare(strict_types=1);
namespace Shaarli\Front\Controller;
use PHPUnit\Framework\TestCase;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\Config\ConfigManager;
use Shaarli\Container\ShaarliContainer;
use Shaarli\Formatter\BookmarkFormatter;
use Shaarli\Formatter\BookmarkRawFormatter;
use Shaarli\Formatter\FormatterFactory;
use Shaarli\Front\Exception\ThumbnailsDisabledException;
use Shaarli\Plugin\PluginManager;
use Shaarli\Render\PageBuilder;
use Shaarli\Security\LoginManager;
use Shaarli\Thumbnailer;
use Slim\Http\Request;
use Slim\Http\Response;
class PictureWallControllerTest extends TestCase
{
/** @var ShaarliContainer */
protected $container;
/** @var PictureWallController */
protected $controller;
public function setUp(): void
{
$this->container = $this->createMock(ShaarliContainer::class);
$this->controller = new PictureWallController($this->container);
}
public function testValidControllerInvokeDefault(): void
{
$this->createValidContainerMockSet();
$request = $this->createMock(Request::class);
$request->expects(static::once())->method('getQueryParams')->willReturn([]);
$response = new Response();
// ConfigManager: thumbnails are enabled
$this->container->conf->method('get')->willReturnCallback(function (string $parameter, $default) {
if ($parameter === 'thumbnails.mode') {
return Thumbnailer::MODE_COMMON;
}
return $default;
});
// Save RainTPL assigned variables
$assignedVariables = [];
$this->container->pageBuilder
->expects(static::atLeastOnce())
->method('assign')
->willReturnCallback(function ($key, $value) use (&$assignedVariables) {
$assignedVariables[$key] = $value;
return $this;
})
;
// Links dataset: 2 links with thumbnails
$this->container->bookmarkService
->expects(static::once())
->method('search')
->willReturnCallback(function (array $parameters, ?string $visibility): array {
// Visibility is set through the container, not the call
static::assertNull($visibility);
// No query parameters
if (count($parameters) === 0) {
return [
(new Bookmark())->setId(1)->setUrl('http://url.tld')->setThumbnail('thumb1'),
(new Bookmark())->setId(2)->setUrl('http://url2.tld'),
(new Bookmark())->setId(3)->setUrl('http://url3.tld')->setThumbnail('thumb2'),
];
}
})
;
// 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_picwall', $hook);
static::assertArrayHasKey('linksToDisplay', $data);
static::assertCount(2, $data['linksToDisplay']);
static::assertSame(1, $data['linksToDisplay'][0]['id']);
static::assertSame(3, $data['linksToDisplay'][1]['id']);
static::assertArrayHasKey('loggedin', $param);
return $data;
});
$result = $this->controller->index($request, $response);
static::assertSame(200, $result->getStatusCode());
static::assertSame('picwall', (string) $result->getBody());
static::assertSame('Picture wall - Shaarli', $assignedVariables['pagetitle']);
static::assertCount(2, $assignedVariables['linksToDisplay']);
$link = $assignedVariables['linksToDisplay'][0];
static::assertSame(1, $link['id']);
static::assertSame('http://url.tld', $link['url']);
static::assertSame('thumb1', $link['thumbnail']);
$link = $assignedVariables['linksToDisplay'][1];
static::assertSame(3, $link['id']);
static::assertSame('http://url3.tld', $link['url']);
static::assertSame('thumb2', $link['thumbnail']);
}
public function testControllerWithThumbnailsDisabled(): void
{
$this->expectException(ThumbnailsDisabledException::class);
$this->createValidContainerMockSet();
$request = $this->createMock(Request::class);
$response = new Response();
// ConfigManager: thumbnails are disabled
$this->container->conf->method('get')->willReturnCallback(function (string $parameter, $default) {
if ($parameter === 'thumbnails.mode') {
return Thumbnailer::MODE_NONE;
}
return $default;
});
$this->controller->index($request, $response);
}
protected function createValidContainerMockSet(): void
{
$loginManager = $this->createMock(LoginManager::class);
$this->container->loginManager = $loginManager;
// Config
$conf = $this->createMock(ConfigManager::class);
$this->container->conf = $conf;
// PageBuilder
$pageBuilder = $this->createMock(PageBuilder::class);
$pageBuilder
->method('render')
->willReturnCallback(function (string $template): string {
return $template;
})
;
$this->container->pageBuilder = $pageBuilder;
// Plugin Manager
$pluginManager = $this->createMock(PluginManager::class);
$this->container->pluginManager = $pluginManager;
// BookmarkService
$bookmarkService = $this->createMock(BookmarkServiceInterface::class);
$this->container->bookmarkService = $bookmarkService;
// Formatter
$formatterFactory = $this->createMock(FormatterFactory::class);
$formatterFactory
->method('getFormatter')
->willReturnCallback(function (string $type): BookmarkFormatter {
if ($type === 'raw') {
return new BookmarkRawFormatter($this->container->conf, true);
}
})
;
$this->container->formatterFactory = $formatterFactory;
}
}

View file

@ -34,7 +34,7 @@
</li> </li>
{if="$thumbnails_enabled"} {if="$thumbnails_enabled"}
<li class="pure-menu-item" id="shaarli-menu-picwall"> <li class="pure-menu-item" id="shaarli-menu-picwall">
<a href="./?do=picwall{$searchcrits}" class="pure-menu-link">{'Picture wall'|t}</a> <a href="./picture-wall?{function="ltrim($searchcrits, '&')"}" class="pure-menu-link">{'Picture wall'|t}</a>
</li> </li>
{/if} {/if}
<li class="pure-menu-item" id="shaarli-menu-daily"> <li class="pure-menu-item" id="shaarli-menu-daily">

View file

@ -5,59 +5,52 @@
</head> </head>
<body> <body>
{include="page.header"} {include="page.header"}
{if="!$thumbnails_enabled"}
<div class="pure-g pure-alert pure-alert-warning page-single-alert"> {if="count($linksToDisplay)===0 && $is_logged_in"}
<div class="pure-u-1 center"> <div class="pure-g pure-alert pure-alert-warning page-single-alert">
{'Picture wall unavailable (thumbnails are disabled).'|t} <div class="pure-u-1 center">
</div> {'There is no cached thumbnail. Try to <a href="./?do=thumbs_update">synchronize them</a>.'|t}
</div>
{else}
{if="count($linksToDisplay)===0 && $is_logged_in"}
<div class="pure-g pure-alert pure-alert-warning page-single-alert">
<div class="pure-u-1 center">
{'There is no cached thumbnail. Try to <a href="./?do=thumbs_update">synchronize them</a>.'|t}
</div>
</div> </div>
{/if}
<div class="pure-g">
<div class="pure-u-lg-1-6 pure-u-1-24"></div>
<div class="pure-u-lg-2-3 pure-u-22-24 page-form page-visitor">
{$countPics=count($linksToDisplay)}
<h2 class="window-title">{'Picture Wall'|t} - {$countPics} {'pics'|t}</h2>
<div id="plugin_zone_start_picwall" class="plugin_zone">
{loop="$plugin_start_zone"}
{$value}
{/loop}
</div>
<div id="picwall-container" class="picwall-container" role="list">
{loop="$linksToDisplay"}
<div class="picwall-pictureframe" role="listitem">
{ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore}
<img data-src="{$value.thumbnail}#" class="b-lazy"
src=""
alt="" width="{$thumbnails_width}" height="{$thumbnails_height}" />
<a href="{$value.real_url}"><span class="info">{$value.title}</span></a>
{loop="$value.picwall_plugin"}
{$value}
{/loop}
</div>
{/loop}
<div class="clear"></div>
</div>
<div id="plugin_zone_end_picwall" class="plugin_zone">
{loop="$plugin_end_zone"}
{$value}
{/loop}
</div>
</div>
<div class="pure-u-lg-1-6 pure-u-1-24"></div>
</div> </div>
{/if} {/if}
<div class="pure-g">
<div class="pure-u-lg-1-6 pure-u-1-24"></div>
<div class="pure-u-lg-2-3 pure-u-22-24 page-form page-visitor">
{$countPics=count($linksToDisplay)}
<h2 class="window-title">{'Picture Wall'|t} - {$countPics} {'pics'|t}</h2>
<div id="plugin_zone_start_picwall" class="plugin_zone">
{loop="$plugin_start_zone"}
{$value}
{/loop}
</div>
<div id="picwall-container" class="picwall-container" role="list">
{loop="$linksToDisplay"}
<div class="picwall-pictureframe" role="listitem">
{ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore}
<img data-src="{$value.thumbnail}#" class="b-lazy"
src=""
alt="" width="{$thumbnails_width}" height="{$thumbnails_height}" />
<a href="{$value.real_url}"><span class="info">{$value.title}</span></a>
{loop="$value.picwall_plugin"}
{$value}
{/loop}
</div>
{/loop}
<div class="clear"></div>
</div>
<div id="plugin_zone_end_picwall" class="plugin_zone">
{loop="$plugin_end_zone"}
{$value}
{/loop}
</div>
</div>
<div class="pure-u-lg-1-6 pure-u-1-24"></div>
</div>
{include="page.footer"} {include="page.footer"}
<script src="js/thumbnails.min.js?v={$version_hash}"></script> <script src="js/thumbnails.min.js?v={$version_hash}"></script>
</body> </body>

View file

@ -32,7 +32,7 @@
<li><a href="{$feedurl}?do=atom{$searchcrits}" class="nomobile">ATOM Feed</a></li> <li><a href="{$feedurl}?do=atom{$searchcrits}" class="nomobile">ATOM Feed</a></li>
{/if} {/if}
<li><a href="./?do=tagcloud">Tag cloud</a></li> <li><a href="./?do=tagcloud">Tag cloud</a></li>
<li><a href="./?do=picwall{$searchcrits}">Picture wall</a></li> <li><a href="./picture-wall{function="ltrim($searchcrits, '&')"}">Picture wall</a></li>
<li><a href="./?do=daily">Daily</a></li> <li><a href="./?do=daily">Daily</a></li>
{loop="$plugins_header.buttons_toolbar"} {loop="$plugins_header.buttons_toolbar"}
<li><a <li><a