Feature: add weekly and monthly view/RSS feed for daily page

- Heavy refactoring of DailyController
  - Add a banner like in tag cloud to display monthly and weekly links
  - Translations: t() now supports variables with optional first letter
uppercase

Fixes #160
This commit is contained in:
ArthurHoaro 2020-10-16 11:50:53 +02:00
parent c2cd15dac2
commit 36e6d88dbf
11 changed files with 1186 additions and 314 deletions

View file

@ -326,6 +326,23 @@ function format_date($date, $time = true, $intl = true)
return $formatter->format($date); return $formatter->format($date);
} }
/**
* Format the date month according to the locale.
*
* @param DateTimeInterface $date to format.
*
* @return bool|string Formatted date, or false if the input is invalid.
*/
function format_month(DateTimeInterface $date)
{
if (! $date instanceof DateTimeInterface) {
return false;
}
return strftime('%B', $date->getTimestamp());
}
/** /**
* Check if the input is an integer, no matter its real type. * Check if the input is an integer, no matter its real type.
* *
@ -454,16 +471,20 @@ function alphabetical_sort(&$data, $reverse = false, $byKeys = false)
* Wrapper function for translation which match the API * Wrapper function for translation which match the API
* of gettext()/_() and ngettext(). * of gettext()/_() and ngettext().
* *
* @param string $text Text to translate. * @param string $text Text to translate.
* @param string $nText The plural message ID. * @param string $nText The plural message ID.
* @param int $nb The number of items for plural forms. * @param int $nb The number of items for plural forms.
* @param string $domain The domain where the translation is stored (default: shaarli). * @param string $domain The domain where the translation is stored (default: shaarli).
* @param array $variables Associative array of variables to replace in translated text.
* @param bool $fixCase Apply `ucfirst` on the translated string, might be useful for strings with variables.
* *
* @return string Text translated. * @return string Text translated.
*/ */
function t($text, $nText = '', $nb = 1, $domain = 'shaarli') function t($text, $nText = '', $nb = 1, $domain = 'shaarli', $variables = [], $fixCase = false)
{ {
return dn__($domain, $text, $nText, $nb); $postFunction = $fixCase ? 'ucfirst' : function ($input) { return $input; };
return $postFunction(dn__($domain, $text, $nText, $nb, $variables));
} }
/** /**

View file

@ -343,26 +343,42 @@ class BookmarkFileService implements BookmarkServiceInterface
/** /**
* @inheritDoc * @inheritDoc
*/ */
public function days(): array public function findByDate(
{ \DateTimeInterface $from,
$bookmarkDays = []; \DateTimeInterface $to,
foreach ($this->search() as $bookmark) { ?\DateTimeInterface &$previous,
$bookmarkDays[$bookmark->getCreated()->format('Ymd')] = 0; ?\DateTimeInterface &$next
} ): array {
$bookmarkDays = array_keys($bookmarkDays); $out = [];
sort($bookmarkDays); $previous = null;
$next = null;
return array_map('strval', $bookmarkDays); foreach ($this->search([], null, false, false, true) as $bookmark) {
if ($to < $bookmark->getCreated()) {
$next = $bookmark->getCreated();
} else if ($from < $bookmark->getCreated() && $to > $bookmark->getCreated()) {
$out[] = $bookmark;
} else {
if ($previous !== null) {
break;
}
$previous = $bookmark->getCreated();
}
}
return $out;
} }
/** /**
* @inheritDoc * @inheritDoc
*/ */
public function filterDay(string $request) public function getLatest(): ?Bookmark
{ {
$visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC; foreach ($this->search([], null, false, false, true) as $bookmark) {
return $bookmark;
}
return $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_DAY, $request, false, $visibility); return null;
} }
/** /**

View file

@ -156,22 +156,29 @@ interface BookmarkServiceInterface
public function bookmarksCountPerTag(array $filteringTags = [], ?string $visibility = null): array; public function bookmarksCountPerTag(array $filteringTags = [], ?string $visibility = null): array;
/** /**
* Returns the list of days containing articles (oldest first) * Return a list of bookmark matching provided period of time.
* It also update directly previous and next date outside of given period found in the datastore.
* *
* @return array containing days (in format YYYYMMDD). * @param \DateTimeInterface $from Starting date.
* @param \DateTimeInterface $to Ending date.
* @param \DateTimeInterface|null $previous (by reference) updated with first created date found before $from.
* @param \DateTimeInterface|null $next (by reference) updated with first created date found after $to.
*
* @return array List of bookmarks matching provided period of time.
*/ */
public function days(): array; public function findByDate(
\DateTimeInterface $from,
\DateTimeInterface $to,
?\DateTimeInterface &$previous,
?\DateTimeInterface &$next
): array;
/** /**
* Returns the list of articles for a given day. * Returns the latest bookmark by creation date.
* *
* @param string $request day to filter. Format: YYYYMMDD. * @return Bookmark|null Found Bookmark or null if the datastore is empty.
*
* @return Bookmark[] list of shaare found.
*
* @throws BookmarkNotFoundException
*/ */
public function filterDay(string $request); public function getLatest(): ?Bookmark;
/** /**
* Creates the default database after a fresh install. * Creates the default database after a fresh install.

View file

@ -5,8 +5,8 @@ declare(strict_types=1);
namespace Shaarli\Front\Controller\Visitor; namespace Shaarli\Front\Controller\Visitor;
use DateTime; use DateTime;
use DateTimeImmutable;
use Shaarli\Bookmark\Bookmark; use Shaarli\Bookmark\Bookmark;
use Shaarli\Helper\DailyPageHelper;
use Shaarli\Render\TemplatePage; use Shaarli\Render\TemplatePage;
use Slim\Http\Request; use Slim\Http\Request;
use Slim\Http\Response; use Slim\Http\Response;
@ -26,32 +26,20 @@ class DailyController extends ShaarliVisitorController
*/ */
public function index(Request $request, Response $response): Response public function index(Request $request, Response $response): Response
{ {
$day = $request->getQueryParam('day') ?? date('Ymd'); $type = DailyPageHelper::extractRequestedType($request);
$format = DailyPageHelper::getFormatByType($type);
$latestBookmark = $this->container->bookmarkService->getLatest();
$dateTime = DailyPageHelper::extractRequestedDateTime($type, $request->getQueryParam($type), $latestBookmark);
$start = DailyPageHelper::getStartDateTimeByType($type, $dateTime);
$end = DailyPageHelper::getEndDateTimeByType($type, $dateTime);
$dailyDesc = DailyPageHelper::getDescriptionByType($type, $dateTime);
$availableDates = $this->container->bookmarkService->days(); $linksToDisplay = $this->container->bookmarkService->findByDate(
$nbAvailableDates = count($availableDates); $start,
$index = array_search($day, $availableDates); $end,
$previousDay,
if ($index === false) { $nextDay
// no bookmarks for day, but at least one day with bookmarks );
$day = $availableDates[$nbAvailableDates - 1] ?? $day;
$previousDay = $availableDates[$nbAvailableDates - 2] ?? '';
} else {
$previousDay = $availableDates[$index - 1] ?? '';
$nextDay = $availableDates[$index + 1] ?? '';
}
if ($day === date('Ymd')) {
$this->assignView('dayDesc', t('Today'));
} elseif ($day === date('Ymd', strtotime('-1 days'))) {
$this->assignView('dayDesc', t('Yesterday'));
}
try {
$linksToDisplay = $this->container->bookmarkService->filterDay($day);
} catch (\Exception $exc) {
$linksToDisplay = [];
}
$formatter = $this->container->formatterFactory->getFormatter(); $formatter = $this->container->formatterFactory->getFormatter();
$formatter->addContextData('base_path', $this->container->basePath); $formatter->addContextData('base_path', $this->container->basePath);
@ -63,13 +51,15 @@ class DailyController extends ShaarliVisitorController
$linksToDisplay[$key]['description'] = $bookmark->getDescription(); $linksToDisplay[$key]['description'] = $bookmark->getDescription();
} }
$dayDate = DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000');
$data = [ $data = [
'linksToDisplay' => $linksToDisplay, 'linksToDisplay' => $linksToDisplay,
'day' => $dayDate->getTimestamp(), 'dayDate' => $start,
'dayDate' => $dayDate, 'day' => $start->getTimestamp(),
'previousday' => $previousDay ?? '', 'previousday' => $previousDay ? $previousDay->format($format) : '',
'nextday' => $nextDay ?? '', 'nextday' => $nextDay ? $nextDay->format($format) : '',
'dayDesc' => $dailyDesc,
'type' => $type,
'localizedType' => $this->translateType($type),
]; ];
// Hooks are called before column construction so that plugins don't have to deal with columns. // Hooks are called before column construction so that plugins don't have to deal with columns.
@ -82,7 +72,7 @@ class DailyController extends ShaarliVisitorController
$mainTitle = $this->container->conf->get('general.title', 'Shaarli'); $mainTitle = $this->container->conf->get('general.title', 'Shaarli');
$this->assignView( $this->assignView(
'pagetitle', 'pagetitle',
t('Daily') .' - '. format_date($dayDate, false) . ' - ' . $mainTitle $data['localizedType'] . ' - ' . $data['dayDesc'] . ' - ' . $mainTitle
); );
return $response->write($this->render(TemplatePage::DAILY)); return $response->write($this->render(TemplatePage::DAILY));
@ -106,11 +96,14 @@ class DailyController extends ShaarliVisitorController
} }
$days = []; $days = [];
$type = DailyPageHelper::extractRequestedType($request);
$format = DailyPageHelper::getFormatByType($type);
$length = DailyPageHelper::getRssLengthByType($type);
foreach ($this->container->bookmarkService->search() as $bookmark) { foreach ($this->container->bookmarkService->search() as $bookmark) {
$day = $bookmark->getCreated()->format('Ymd'); $day = $bookmark->getCreated()->format($format);
// Stop iterating after DAILY_RSS_NB_DAYS entries // Stop iterating after DAILY_RSS_NB_DAYS entries
if (count($days) === static::$DAILY_RSS_NB_DAYS && !isset($days[$day])) { if (count($days) === $length && !isset($days[$day])) {
break; break;
} }
@ -127,12 +120,19 @@ class DailyController extends ShaarliVisitorController
/** @var Bookmark[] $bookmarks */ /** @var Bookmark[] $bookmarks */
foreach ($days as $day => $bookmarks) { foreach ($days as $day => $bookmarks) {
$dayDatetime = DateTimeImmutable::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000'); $dayDateTime = DailyPageHelper::extractRequestedDateTime($type, (string) $day);
$endDateTime = DailyPageHelper::getEndDateTimeByType($type, $dayDateTime);
// We only want the RSS entry to be published when the period is over.
if (new DateTime() < $endDateTime) {
continue;
}
$dataPerDay[$day] = [ $dataPerDay[$day] = [
'date' => $dayDatetime, 'date' => $endDateTime,
'date_rss' => $dayDatetime->format(DateTime::RSS), 'date_rss' => $endDateTime->format(DateTime::RSS),
'date_human' => format_date($dayDatetime, false, true), 'date_human' => DailyPageHelper::getDescriptionByType($type, $dayDateTime),
'absolute_url' => $indexUrl . 'daily?day=' . $day, 'absolute_url' => $indexUrl . 'daily?'. $type .'=' . $day,
'links' => [], 'links' => [],
]; ];
@ -141,16 +141,20 @@ class DailyController extends ShaarliVisitorController
// Make permalink URL absolute // Make permalink URL absolute
if ($bookmark->isNote()) { if ($bookmark->isNote()) {
$dataPerDay[$day]['links'][$key]['url'] = $indexUrl . $bookmark->getUrl(); $dataPerDay[$day]['links'][$key]['url'] = rtrim($indexUrl, '/') . $bookmark->getUrl();
} }
} }
} }
$this->assignView('title', $this->container->conf->get('general.title', 'Shaarli')); $this->assignAllView([
$this->assignView('index_url', $indexUrl); 'title' => $this->container->conf->get('general.title', 'Shaarli'),
$this->assignView('page_url', $pageUrl); 'index_url' => $indexUrl,
$this->assignView('hide_timestamps', $this->container->conf->get('privacy.hide_timestamps', false)); 'page_url' => $pageUrl,
$this->assignView('days', $dataPerDay); 'hide_timestamps' => $this->container->conf->get('privacy.hide_timestamps', false),
'days' => $dataPerDay,
'type' => $type,
'localizedType' => $this->translateType($type),
]);
$rssContent = $this->render(TemplatePage::DAILY_RSS); $rssContent = $this->render(TemplatePage::DAILY_RSS);
@ -189,4 +193,13 @@ class DailyController extends ShaarliVisitorController
return $columns; return $columns;
} }
protected function translateType($type): string
{
return [
t('day') => t('Daily'),
t('week') => t('Weekly'),
t('month') => t('Monthly'),
][t($type)] ?? t('Daily');
}
} }

View file

@ -0,0 +1,208 @@
<?php
declare(strict_types=1);
namespace Shaarli\Helper;
use Shaarli\Bookmark\Bookmark;
use Slim\Http\Request;
class DailyPageHelper
{
public const MONTH = 'month';
public const WEEK = 'week';
public const DAY = 'day';
/**
* Extracts the type of the daily to display from the HTTP request parameters
*
* @param Request $request HTTP request
*
* @return string month/week/day
*/
public static function extractRequestedType(Request $request): string
{
if ($request->getQueryParam(static::MONTH) !== null) {
return static::MONTH;
} elseif ($request->getQueryParam(static::WEEK) !== null) {
return static::WEEK;
}
return static::DAY;
}
/**
* Extracts a DateTimeImmutable from provided HTTP request.
* If no parameter is provided, we rely on the creation date of the latest provided created bookmark.
* If the datastore is empty or no bookmark is provided, we use the current date.
*
* @param string $type month/week/day
* @param string|null $requestedDate Input string extracted from the request
* @param Bookmark|null $latestBookmark Latest bookmark found in the datastore (by date)
*
* @return \DateTimeImmutable from input or latest bookmark.
*
* @throws \Exception Type not supported.
*/
public static function extractRequestedDateTime(
string $type,
?string $requestedDate,
Bookmark $latestBookmark = null
): \DateTimeImmutable {
$format = static::getFormatByType($type);
if (empty($requestedDate)) {
return $latestBookmark instanceof Bookmark
? new \DateTimeImmutable($latestBookmark->getCreated()->format(\DateTime::ATOM))
: new \DateTimeImmutable()
;
}
// W is not supported by createFromFormat...
if ($type === static::WEEK) {
return (new \DateTimeImmutable())
->setISODate((int) substr($requestedDate, 0, 4), (int) substr($requestedDate, 4, 2))
;
}
return \DateTimeImmutable::createFromFormat($format, $requestedDate);
}
/**
* Get the DateTime format used by provided type
* Examples:
* - day: 20201016 (<year><month><day>)
* - week: 202041 (<year><week number>)
* - month: 202010 (<year><month>)
*
* @param string $type month/week/day
*
* @return string DateTime compatible format
*
* @see https://www.php.net/manual/en/datetime.format.php
*
* @throws \Exception Type not supported.
*/
public static function getFormatByType(string $type): string
{
switch ($type) {
case static::MONTH:
return 'Ym';
case static::WEEK:
return 'YW';
case static::DAY:
return 'Ymd';
default:
throw new \Exception('Unsupported daily format type');
}
}
/**
* Get the first DateTime of the time period depending on given datetime and type.
* Note: DateTimeImmutable is required because we rely heavily on DateTime->modify() syntax
* and we don't want to alter original datetime.
*
* @param string $type month/week/day
* @param \DateTimeImmutable $requested DateTime extracted from request input
* (should come from extractRequestedDateTime)
*
* @return \DateTimeInterface First DateTime of the time period
*
* @throws \Exception Type not supported.
*/
public static function getStartDateTimeByType(string $type, \DateTimeImmutable $requested): \DateTimeInterface
{
switch ($type) {
case static::MONTH:
return $requested->modify('first day of this month midnight');
case static::WEEK:
return $requested->modify('Monday this week midnight');
case static::DAY:
return $requested->modify('Today midnight');
default:
throw new \Exception('Unsupported daily format type');
}
}
/**
* Get the last DateTime of the time period depending on given datetime and type.
* Note: DateTimeImmutable is required because we rely heavily on DateTime->modify() syntax
* and we don't want to alter original datetime.
*
* @param string $type month/week/day
* @param \DateTimeImmutable $requested DateTime extracted from request input
* (should come from extractRequestedDateTime)
*
* @return \DateTimeInterface Last DateTime of the time period
*
* @throws \Exception Type not supported.
*/
public static function getEndDateTimeByType(string $type, \DateTimeImmutable $requested): \DateTimeInterface
{
switch ($type) {
case static::MONTH:
return $requested->modify('last day of this month 23:59:59');
case static::WEEK:
return $requested->modify('Sunday this week 23:59:59');
case static::DAY:
return $requested->modify('Today 23:59:59');
default:
throw new \Exception('Unsupported daily format type');
}
}
/**
* Get localized description of the time period depending on given datetime and type.
* Example: for a month period, it returns `October, 2020`.
*
* @param string $type month/week/day
* @param \DateTimeImmutable $requested DateTime extracted from request input
* (should come from extractRequestedDateTime)
*
* @return string Localized time period description
*
* @throws \Exception Type not supported.
*/
public static function getDescriptionByType(string $type, \DateTimeImmutable $requested): string
{
switch ($type) {
case static::MONTH:
return $requested->format('F') . ', ' . $requested->format('Y');
case static::WEEK:
$requested = $requested->modify('Monday this week');
return t('Week') . ' ' . $requested->format('W') . ' (' . format_date($requested, false) . ')';
case static::DAY:
$out = '';
if ($requested->format('Ymd') === date('Ymd')) {
$out = t('Today') . ' - ';
} elseif ($requested->format('Ymd') === date('Ymd', strtotime('-1 days'))) {
$out = t('Yesterday') . ' - ';
}
return $out . format_date($requested, false);
default:
throw new \Exception('Unsupported daily format type');
}
}
/**
* Get the number of items to display in the RSS feed depending on the given type.
*
* @param string $type month/week/day
*
* @return int number of elements
*
* @throws \Exception Type not supported.
*/
public static function getRssLengthByType(string $type): int
{
switch ($type) {
case static::MONTH:
return 12; // 1 year
case static::WEEK:
return 26; // ~6 months
case static::DAY:
return 30; // ~1 month
default:
throw new \Exception('Unsupported daily format type');
}
}
}

View file

@ -1,8 +1,8 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Shaarli\n" "Project-Id-Version: Shaarli\n"
"POT-Creation-Date: 2020-10-27 19:32+0100\n" "POT-Creation-Date: 2020-10-27 19:44+0100\n"
"PO-Revision-Date: 2020-10-27 19:32+0100\n" "PO-Revision-Date: 2020-10-27 19:44+0100\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Shaarli\n" "Language-Team: Shaarli\n"
"Language: fr_FR\n" "Language: fr_FR\n"
@ -20,78 +20,11 @@ msgstr ""
"X-Poedit-SearchPath-3: init.php\n" "X-Poedit-SearchPath-3: init.php\n"
"X-Poedit-SearchPath-4: plugins\n" "X-Poedit-SearchPath-4: plugins\n"
#: application/ApplicationUtils.php:162 #: application/History.php:180
#, php-format
msgid ""
"Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus "
"cannot run. Your PHP version has known security vulnerabilities and should "
"be updated as soon as possible."
msgstr ""
"Votre version de PHP est obsolète ! Shaarli nécessite au moins PHP %s, et ne "
"peut donc pas fonctionner. Votre version de PHP a des failles de sécurités "
"connues et devrait être mise à jour au plus tôt."
#: application/ApplicationUtils.php:195 application/ApplicationUtils.php:215
msgid "directory is not readable"
msgstr "le répertoire n'est pas accessible en lecture"
#: application/ApplicationUtils.php:218
msgid "directory is not writable"
msgstr "le répertoire n'est pas accessible en écriture"
#: application/ApplicationUtils.php:240
msgid "file is not readable"
msgstr "le fichier n'est pas accessible en lecture"
#: application/ApplicationUtils.php:243
msgid "file is not writable"
msgstr "le fichier n'est pas accessible en écriture"
#: application/ApplicationUtils.php:277
msgid "Configuration parsing"
msgstr "Chargement de la configuration"
#: application/ApplicationUtils.php:278
msgid "Slim Framework (routing, etc.)"
msgstr "Slim Framwork (routage, etc.)"
#: application/ApplicationUtils.php:279
msgid "Multibyte (Unicode) string support"
msgstr "Support des chaînes de caractère multibytes (Unicode)"
#: application/ApplicationUtils.php:280
msgid "Required to use thumbnails"
msgstr "Obligatoire pour utiliser les miniatures"
#: application/ApplicationUtils.php:281
msgid "Localized text sorting (e.g. e->è->f)"
msgstr "Tri des textes traduits (ex : e->è->f)"
#: application/ApplicationUtils.php:282
msgid "Better retrieval of bookmark metadata and thumbnail"
msgstr "Meilleure récupération des meta-données des marque-pages et minatures"
#: application/ApplicationUtils.php:283
msgid "Use the translation system in gettext mode"
msgstr "Utiliser le système de traduction en mode gettext"
#: application/ApplicationUtils.php:284
msgid "Login using LDAP server"
msgstr "Authentification via un serveur LDAP"
#: application/FileUtils.php:100
msgid "Provided path is not a directory."
msgstr "Le chemin fourni n'est pas un dossier."
#: application/FileUtils.php:104
msgid "Trying to delete a folder outside of Shaarli path."
msgstr "Tentative de supprimer un dossier en dehors du chemin de Shaarli."
#: application/History.php:179
msgid "History file isn't readable or writable" msgid "History file isn't readable or writable"
msgstr "Le fichier d'historique n'est pas accessible en lecture ou en écriture" msgstr "Le fichier d'historique n'est pas accessible en lecture ou en écriture"
#: application/History.php:190 #: application/History.php:191
msgid "Could not parse history file" msgid "Could not parse history file"
msgstr "Format incorrect pour le fichier d'historique" msgstr "Format incorrect pour le fichier d'historique"
@ -123,27 +56,27 @@ msgstr ""
"l'extension php-gd doit être chargée pour utiliser les miniatures. Les " "l'extension php-gd doit être chargée pour utiliser les miniatures. Les "
"miniatures sont désormais désactivées. Rechargez la page." "miniatures sont désormais désactivées. Rechargez la page."
#: application/Utils.php:385 #: application/Utils.php:402
msgid "Setting not set" msgid "Setting not set"
msgstr "Paramètre non défini" msgstr "Paramètre non défini"
#: application/Utils.php:392 #: application/Utils.php:409
msgid "Unlimited" msgid "Unlimited"
msgstr "Illimité" msgstr "Illimité"
#: application/Utils.php:395 #: application/Utils.php:412
msgid "B" msgid "B"
msgstr "o" msgstr "o"
#: application/Utils.php:395 #: application/Utils.php:412
msgid "kiB" msgid "kiB"
msgstr "ko" msgstr "ko"
#: application/Utils.php:395 #: application/Utils.php:412
msgid "MiB" msgid "MiB"
msgstr "Mo" msgstr "Mo"
#: application/Utils.php:395 #: application/Utils.php:412
msgid "GiB" msgid "GiB"
msgstr "Go" msgstr "Go"
@ -156,7 +89,7 @@ msgstr "Vous n'êtes pas autorisé à modifier les données"
#: application/bookmark/BookmarkFileService.php:208 #: application/bookmark/BookmarkFileService.php:208
msgid "This bookmarks already exists" msgid "This bookmarks already exists"
msgstr "Ce marque-page existe déjà." msgstr "Ce marque-page existe déjà"
#: application/bookmark/BookmarkInitializer.php:39 #: application/bookmark/BookmarkInitializer.php:39
msgid "(private bookmark with thumbnail demo)" msgid "(private bookmark with thumbnail demo)"
@ -354,7 +287,8 @@ msgid "Direct link"
msgstr "Liens directs" msgstr "Liens directs"
#: application/feed/FeedBuilder.php:181 #: application/feed/FeedBuilder.php:181
#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96 #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103
#: tmp/dailyrss.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:179 #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:179
msgid "Permalink" msgid "Permalink"
msgstr "Permalien" msgstr "Permalien"
@ -537,20 +471,36 @@ msgstr "Outils"
msgid "Search: " msgid "Search: "
msgstr "Recherche : " msgstr "Recherche : "
#: application/front/controller/visitor/DailyController.php:45 #: application/front/controller/visitor/DailyController.php:200
msgid "Today" msgid "day"
msgstr "Aujourd'hui" msgstr "jour"
#: application/front/controller/visitor/DailyController.php:47 #: application/front/controller/visitor/DailyController.php:200
msgid "Yesterday" #: application/front/controller/visitor/DailyController.php:203
msgstr "Hier" #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
#: application/front/controller/visitor/DailyController.php:85
#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:48 #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:48
msgid "Daily" msgid "Daily"
msgstr "Quotidien" msgstr "Quotidien"
#: application/front/controller/visitor/DailyController.php:201
msgid "week"
msgstr "semaine"
#: application/front/controller/visitor/DailyController.php:201
#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
msgid "Weekly"
msgstr "Hebdomadaire"
#: application/front/controller/visitor/DailyController.php:202
msgid "month"
msgstr "mois"
#: application/front/controller/visitor/DailyController.php:202
#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
msgid "Monthly"
msgstr "Mensuel"
#: application/front/controller/visitor/ErrorController.php:33 #: application/front/controller/visitor/ErrorController.php:33
msgid "An unexpected error occurred." msgid "An unexpected error occurred."
msgstr "Une erreur inattendue s'est produite." msgstr "Une erreur inattendue s'est produite."
@ -616,7 +566,7 @@ msgstr "Mur d'images"
#: application/front/controller/visitor/TagCloudController.php:88 #: application/front/controller/visitor/TagCloudController.php:88
msgid "Tag " msgid "Tag "
msgstr "Tag" msgstr "Tag "
#: application/front/exceptions/AlreadyInstalledException.php:11 #: application/front/exceptions/AlreadyInstalledException.php:11
msgid "Shaarli has already been installed. Login to edit the configuration." msgid "Shaarli has already been installed. Login to edit the configuration."
@ -644,6 +594,86 @@ msgstr ""
msgid "Wrong token." msgid "Wrong token."
msgstr "Jeton invalide." msgstr "Jeton invalide."
#: application/helper/ApplicationUtils.php:162
#, php-format
msgid ""
"Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus "
"cannot run. Your PHP version has known security vulnerabilities and should "
"be updated as soon as possible."
msgstr ""
"Votre version de PHP est obsolète ! Shaarli nécessite au moins PHP %s, et ne "
"peut donc pas fonctionner. Votre version de PHP a des failles de sécurités "
"connues et devrait être mise à jour au plus tôt."
#: application/helper/ApplicationUtils.php:195
#: application/helper/ApplicationUtils.php:215
msgid "directory is not readable"
msgstr "le répertoire n'est pas accessible en lecture"
#: application/helper/ApplicationUtils.php:218
msgid "directory is not writable"
msgstr "le répertoire n'est pas accessible en écriture"
#: application/helper/ApplicationUtils.php:240
msgid "file is not readable"
msgstr "le fichier n'est pas accessible en lecture"
#: application/helper/ApplicationUtils.php:243
msgid "file is not writable"
msgstr "le fichier n'est pas accessible en écriture"
#: application/helper/ApplicationUtils.php:277
msgid "Configuration parsing"
msgstr "Chargement de la configuration"
#: application/helper/ApplicationUtils.php:278
msgid "Slim Framework (routing, etc.)"
msgstr "Slim Framwork (routage, etc.)"
#: application/helper/ApplicationUtils.php:279
msgid "Multibyte (Unicode) string support"
msgstr "Support des chaînes de caractère multibytes (Unicode)"
#: application/helper/ApplicationUtils.php:280
msgid "Required to use thumbnails"
msgstr "Obligatoire pour utiliser les miniatures"
#: application/helper/ApplicationUtils.php:281
msgid "Localized text sorting (e.g. e->è->f)"
msgstr "Tri des textes traduits (ex : e->è->f)"
#: application/helper/ApplicationUtils.php:282
msgid "Better retrieval of bookmark metadata and thumbnail"
msgstr "Meilleure récupération des meta-données des marque-pages et minatures"
#: application/helper/ApplicationUtils.php:283
msgid "Use the translation system in gettext mode"
msgstr "Utiliser le système de traduction en mode gettext"
#: application/helper/ApplicationUtils.php:284
msgid "Login using LDAP server"
msgstr "Authentification via un serveur LDAP"
#: application/helper/DailyPageHelper.php:172
msgid "Week"
msgstr "Semaine"
#: application/helper/DailyPageHelper.php:176
msgid "Today"
msgstr "Aujourd'hui"
#: application/helper/DailyPageHelper.php:178
msgid "Yesterday"
msgstr "Hier"
#: application/helper/FileUtils.php:100
msgid "Provided path is not a directory."
msgstr "Le chemin fourni n'est pas un dossier."
#: application/helper/FileUtils.php:104
msgid "Trying to delete a folder outside of Shaarli path."
msgstr "Tentative de supprimer un dossier en dehors du chemin de Shaarli."
#: application/legacy/LegacyLinkDB.php:131 #: application/legacy/LegacyLinkDB.php:131
msgid "You are not authorized to add a link." msgid "You are not authorized to add a link."
msgstr "Vous n'êtes pas autorisé à ajouter un lien." msgstr "Vous n'êtes pas autorisé à ajouter un lien."
@ -1103,25 +1133,30 @@ msgstr "Aucune"
msgid "Save" msgid "Save"
msgstr "Enregistrer" msgstr "Enregistrer"
#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
msgid "The Daily Shaarli" msgid "1 RSS entry per :type"
msgstr "Le Quotidien Shaarli" msgid_plural ""
msgstr[0] "1 entrée RSS par :type"
msgstr[1] ""
#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17 #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49
msgid "1 RSS entry per day" msgid "Previous :type"
msgstr "1 entrée RSS par jour" msgid_plural ""
msgstr[0] ":type précédent"
msgstr[1] "Jour précédent"
#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:37 #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56
msgid "Previous day" #: tmp/dailyrss.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7
msgstr "Jour précédent" msgid "All links of one :type in a single page."
msgid_plural ""
msgstr[0] "Tous les liens d'un :type sur une page."
msgstr[1] "Tous les liens d'un jour sur une page."
#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63
msgid "All links of one day in a single page." msgid "Next :type"
msgstr "Tous les liens d'un jour sur une page." msgid_plural ""
msgstr[0] ":type suivant"
#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51 msgstr[1] ""
msgid "Next day"
msgstr "Jour suivant"
#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21 #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21
msgid "Edit Shaare" msgid "Edit Shaare"
@ -1821,8 +1856,11 @@ msgstr ""
"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « " "Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « "
"Ajouter aux favoris »" "Ajouter aux favoris »"
#~ msgid "Rename" #~ msgid "Display:"
#~ msgstr "Renommer" #~ msgstr "Afficher :"
#~ msgid "The Daily Shaarli"
#~ msgstr "Le Quotidien Shaarli"
#, fuzzy #, fuzzy
#~| msgid "Selection" #~| msgid "Selection"

View file

@ -685,22 +685,6 @@ class BookmarkFileServiceTest extends TestCase
$this->assertEquals(0, $linkDB->count()); $this->assertEquals(0, $linkDB->count());
} }
/**
* List the days for which bookmarks have been posted
*/
public function testDays()
{
$this->assertSame(
['20100309', '20100310', '20121206', '20121207', '20130614', '20150310'],
$this->publicLinkDB->days()
);
$this->assertSame(
['20100309', '20100310', '20121206', '20121207', '20130614', '20141125', '20150310'],
$this->privateLinkDB->days()
);
}
/** /**
* The URL corresponds to an existing entry in the DB * The URL corresponds to an existing entry in the DB
*/ */
@ -1074,33 +1058,105 @@ class BookmarkFileServiceTest extends TestCase
} }
/** /**
* Test filterDay while logged in * Test find by dates in the middle of the datastore (sorted by dates) with a single bookmark as a result.
*/ */
public function testFilterDayLoggedIn(): void public function testFilterByDateMidTimePeriodSingleBookmark(): void
{ {
$bookmarks = $this->privateLinkDB->filterDay('20121206'); $bookmarks = $this->privateLinkDB->findByDate(
$expectedIds = [4, 9, 1, 0]; DateTime::createFromFormat('Ymd_His', '20121206_150000'),
DateTime::createFromFormat('Ymd_His', '20121206_160000'),
$before,
$after
);
static::assertCount(4, $bookmarks); static::assertCount(1, $bookmarks);
foreach ($bookmarks as $bookmark) {
$i = ($i ?? -1) + 1; static::assertSame(9, $bookmarks[0]->getId());
static::assertSame($expectedIds[$i], $bookmark->getId()); static::assertEquals(DateTime::createFromFormat('Ymd_His', '20121206_142300'), $before);
} static::assertEquals(DateTime::createFromFormat('Ymd_His', '20121206_172539'), $after);
} }
/** /**
* Test filterDay while logged out * Test find by dates in the middle of the datastore (sorted by dates) with a multiple bookmarks as a result.
*/ */
public function testFilterDayLoggedOut(): void public function testFilterByDateMidTimePeriodMultipleBookmarks(): void
{ {
$bookmarks = $this->publicLinkDB->filterDay('20121206'); $bookmarks = $this->privateLinkDB->findByDate(
$expectedIds = [4, 9, 1]; DateTime::createFromFormat('Ymd_His', '20121206_150000'),
DateTime::createFromFormat('Ymd_His', '20121206_180000'),
$before,
$after
);
static::assertCount(3, $bookmarks); static::assertCount(2, $bookmarks);
foreach ($bookmarks as $bookmark) {
$i = ($i ?? -1) + 1; static::assertSame(1, $bookmarks[0]->getId());
static::assertSame($expectedIds[$i], $bookmark->getId()); static::assertSame(9, $bookmarks[1]->getId());
} static::assertEquals(DateTime::createFromFormat('Ymd_His', '20121206_142300'), $before);
static::assertEquals(DateTime::createFromFormat('Ymd_His', '20121206_182539'), $after);
}
/**
* Test find by dates at the end of the datastore (sorted by dates).
*/
public function testFilterByDateLastTimePeriod(): void
{
$after = new DateTime();
$bookmarks = $this->privateLinkDB->findByDate(
DateTime::createFromFormat('Ymd_His', '20150310_114640'),
DateTime::createFromFormat('Ymd_His', '20450101_010101'),
$before,
$after
);
static::assertCount(1, $bookmarks);
static::assertSame(41, $bookmarks[0]->getId());
static::assertEquals(DateTime::createFromFormat('Ymd_His', '20150310_114633'), $before);
static::assertNull($after);
}
/**
* Test find by dates at the beginning of the datastore (sorted by dates).
*/
public function testFilterByDateFirstTimePeriod(): void
{
$before = new DateTime();
$bookmarks = $this->privateLinkDB->findByDate(
DateTime::createFromFormat('Ymd_His', '20000101_101010'),
DateTime::createFromFormat('Ymd_His', '20100309_110000'),
$before,
$after
);
static::assertCount(1, $bookmarks);
static::assertSame(11, $bookmarks[0]->getId());
static::assertNull($before);
static::assertEquals(DateTime::createFromFormat('Ymd_His', '20100310_101010'), $after);
}
/**
* Test getLatest with a sticky bookmark: it should be ignored and return the latest by creation date instead.
*/
public function testGetLatestWithSticky(): void
{
$bookmark = $this->publicLinkDB->getLatest();
static::assertSame(41, $bookmark->getId());
}
/**
* Test getLatest with a sticky bookmark: it should be ignored and return the latest by creation date instead.
*/
public function testGetLatestEmptyDatastore(): void
{
unlink($this->conf->get('resource.datastore'));
$this->publicLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, false);
$bookmark = $this->publicLinkDB->getLatest();
static::assertNull($bookmark);
} }
/** /**

View file

@ -28,52 +28,49 @@ class DailyControllerTest extends TestCase
public function testValidIndexControllerInvokeDefault(): void public function testValidIndexControllerInvokeDefault(): void
{ {
$currentDay = new \DateTimeImmutable('2020-05-13'); $currentDay = new \DateTimeImmutable('2020-05-13');
$previousDate = new \DateTime('2 days ago 00:00:00');
$nextDate = new \DateTime('today 00:00:00');
$request = $this->createMock(Request::class); $request = $this->createMock(Request::class);
$request->method('getQueryParam')->willReturn($currentDay->format('Ymd')); $request->method('getQueryParam')->willReturnCallback(function (string $key) use ($currentDay): ?string {
return $key === 'day' ? $currentDay->format('Ymd') : null;
});
$response = new Response(); $response = new Response();
// Save RainTPL assigned variables // Save RainTPL assigned variables
$assignedVariables = []; $assignedVariables = [];
$this->assignTemplateVars($assignedVariables); $this->assignTemplateVars($assignedVariables);
// Links dataset: 2 links with thumbnails
$this->container->bookmarkService $this->container->bookmarkService
->expects(static::once()) ->expects(static::once())
->method('days') ->method('findByDate')
->willReturnCallback(function () use ($currentDay): array { ->willReturnCallback(
return [ function ($from, $to, &$previous, &$next) use ($currentDay, $previousDate, $nextDate): array {
'20200510', $previous = $previousDate;
$currentDay->format('Ymd'), $next = $nextDate;
'20200516',
]; return [
}) (new Bookmark())
; ->setId(1)
$this->container->bookmarkService ->setUrl('http://url.tld')
->expects(static::once()) ->setTitle(static::generateString(50))
->method('filterDay') ->setDescription(static::generateString(500))
->willReturnCallback(function (): array { ,
return [ (new Bookmark())
(new Bookmark()) ->setId(2)
->setId(1) ->setUrl('http://url2.tld')
->setUrl('http://url.tld') ->setTitle(static::generateString(50))
->setTitle(static::generateString(50)) ->setDescription(static::generateString(500))
->setDescription(static::generateString(500)) ,
, (new Bookmark())
(new Bookmark()) ->setId(3)
->setId(2) ->setUrl('http://url3.tld')
->setUrl('http://url2.tld') ->setTitle(static::generateString(50))
->setTitle(static::generateString(50)) ->setDescription(static::generateString(500))
->setDescription(static::generateString(500)) ,
, ];
(new Bookmark()) }
->setId(3) )
->setUrl('http://url3.tld')
->setTitle(static::generateString(50))
->setDescription(static::generateString(500))
,
];
})
; ;
// Make sure that PluginManager hook is triggered // Make sure that PluginManager hook is triggered
@ -81,20 +78,22 @@ class DailyControllerTest extends TestCase
->expects(static::atLeastOnce()) ->expects(static::atLeastOnce())
->method('executeHooks') ->method('executeHooks')
->withConsecutive(['render_daily']) ->withConsecutive(['render_daily'])
->willReturnCallback(function (string $hook, array $data, array $param) use ($currentDay): array { ->willReturnCallback(
if ('render_daily' === $hook) { function (string $hook, array $data, array $param) use ($currentDay, $previousDate, $nextDate): array {
static::assertArrayHasKey('linksToDisplay', $data); if ('render_daily' === $hook) {
static::assertCount(3, $data['linksToDisplay']); static::assertArrayHasKey('linksToDisplay', $data);
static::assertSame(1, $data['linksToDisplay'][0]['id']); static::assertCount(3, $data['linksToDisplay']);
static::assertSame($currentDay->getTimestamp(), $data['day']); static::assertSame(1, $data['linksToDisplay'][0]['id']);
static::assertSame('20200510', $data['previousday']); static::assertSame($currentDay->getTimestamp(), $data['day']);
static::assertSame('20200516', $data['nextday']); static::assertSame($previousDate->format('Ymd'), $data['previousday']);
static::assertSame($nextDate->format('Ymd'), $data['nextday']);
static::assertArrayHasKey('loggedin', $param); static::assertArrayHasKey('loggedin', $param);
}
return $data;
} }
)
return $data;
})
; ;
$result = $this->controller->index($request, $response); $result = $this->controller->index($request, $response);
@ -107,6 +106,11 @@ class DailyControllerTest extends TestCase
); );
static::assertEquals($currentDay, $assignedVariables['dayDate']); static::assertEquals($currentDay, $assignedVariables['dayDate']);
static::assertEquals($currentDay->getTimestamp(), $assignedVariables['day']); static::assertEquals($currentDay->getTimestamp(), $assignedVariables['day']);
static::assertSame($previousDate->format('Ymd'), $assignedVariables['previousday']);
static::assertSame($nextDate->format('Ymd'), $assignedVariables['nextday']);
static::assertSame('day', $assignedVariables['type']);
static::assertSame('May 13, 2020', $assignedVariables['dayDesc']);
static::assertSame('Daily', $assignedVariables['localizedType']);
static::assertCount(3, $assignedVariables['linksToDisplay']); static::assertCount(3, $assignedVariables['linksToDisplay']);
$link = $assignedVariables['linksToDisplay'][0]; $link = $assignedVariables['linksToDisplay'][0];
@ -171,26 +175,19 @@ class DailyControllerTest extends TestCase
$currentDay = new \DateTimeImmutable('2020-05-13'); $currentDay = new \DateTimeImmutable('2020-05-13');
$request = $this->createMock(Request::class); $request = $this->createMock(Request::class);
$request->method('getQueryParam')->willReturnCallback(function (string $key) use ($currentDay): ?string {
return $key === 'day' ? $currentDay->format('Ymd') : null;
});
$response = new Response(); $response = new Response();
// Save RainTPL assigned variables // Save RainTPL assigned variables
$assignedVariables = []; $assignedVariables = [];
$this->assignTemplateVars($assignedVariables); $this->assignTemplateVars($assignedVariables);
// Links dataset: 2 links with thumbnails
$this->container->bookmarkService $this->container->bookmarkService
->expects(static::once()) ->expects(static::once())
->method('days') ->method('findByDate')
->willReturnCallback(function () use ($currentDay): array { ->willReturnCallback(function () use ($currentDay): array {
return [
$currentDay->format($currentDay->format('Ymd')),
];
})
;
$this->container->bookmarkService
->expects(static::once())
->method('filterDay')
->willReturnCallback(function (): array {
return [ return [
(new Bookmark()) (new Bookmark())
->setId(1) ->setId(1)
@ -250,20 +247,10 @@ class DailyControllerTest extends TestCase
$assignedVariables = []; $assignedVariables = [];
$this->assignTemplateVars($assignedVariables); $this->assignTemplateVars($assignedVariables);
// Links dataset: 2 links with thumbnails
$this->container->bookmarkService $this->container->bookmarkService
->expects(static::once()) ->expects(static::once())
->method('days') ->method('findByDate')
->willReturnCallback(function () use ($currentDay): array { ->willReturnCallback(function () use ($currentDay): array {
return [
$currentDay->format($currentDay->format('Ymd')),
];
})
;
$this->container->bookmarkService
->expects(static::once())
->method('filterDay')
->willReturnCallback(function (): array {
return [ return [
(new Bookmark())->setId(1)->setUrl('http://url.tld')->setTitle('title'), (new Bookmark())->setId(1)->setUrl('http://url.tld')->setTitle('title'),
(new Bookmark()) (new Bookmark())
@ -320,14 +307,7 @@ class DailyControllerTest extends TestCase
// Links dataset: 2 links with thumbnails // Links dataset: 2 links with thumbnails
$this->container->bookmarkService $this->container->bookmarkService
->expects(static::once()) ->expects(static::once())
->method('days') ->method('findByDate')
->willReturnCallback(function (): array {
return [];
})
;
$this->container->bookmarkService
->expects(static::once())
->method('filterDay')
->willReturnCallback(function (): array { ->willReturnCallback(function (): array {
return []; return [];
}) })
@ -347,7 +327,7 @@ class DailyControllerTest extends TestCase
static::assertSame(200, $result->getStatusCode()); static::assertSame(200, $result->getStatusCode());
static::assertSame('daily', (string) $result->getBody()); static::assertSame('daily', (string) $result->getBody());
static::assertCount(0, $assignedVariables['linksToDisplay']); static::assertCount(0, $assignedVariables['linksToDisplay']);
static::assertSame('Today', $assignedVariables['dayDesc']); static::assertSame('Today - ' . (new \DateTime())->format('F d, Y'), $assignedVariables['dayDesc']);
static::assertEquals((new \DateTime())->setTime(0, 0)->getTimestamp(), $assignedVariables['day']); static::assertEquals((new \DateTime())->setTime(0, 0)->getTimestamp(), $assignedVariables['day']);
static::assertEquals((new \DateTime())->setTime(0, 0), $assignedVariables['dayDate']); static::assertEquals((new \DateTime())->setTime(0, 0), $assignedVariables['dayDate']);
} }
@ -361,6 +341,7 @@ class DailyControllerTest extends TestCase
new \DateTimeImmutable('2020-05-17'), new \DateTimeImmutable('2020-05-17'),
new \DateTimeImmutable('2020-05-15'), new \DateTimeImmutable('2020-05-15'),
new \DateTimeImmutable('2020-05-13'), new \DateTimeImmutable('2020-05-13'),
new \DateTimeImmutable('+1 month'),
]; ];
$request = $this->createMock(Request::class); $request = $this->createMock(Request::class);
@ -371,6 +352,7 @@ class DailyControllerTest extends TestCase
(new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'), (new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'),
(new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'), (new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'),
(new Bookmark())->setId(4)->setCreated($dates[2])->setUrl('http://domain.tld/4'), (new Bookmark())->setId(4)->setCreated($dates[2])->setUrl('http://domain.tld/4'),
(new Bookmark())->setId(5)->setCreated($dates[3])->setUrl('http://domain.tld/5'),
]); ]);
$this->container->pageCacheManager $this->container->pageCacheManager
@ -397,13 +379,14 @@ class DailyControllerTest extends TestCase
static::assertSame('http://shaarli/subfolder/', $assignedVariables['index_url']); static::assertSame('http://shaarli/subfolder/', $assignedVariables['index_url']);
static::assertSame('http://shaarli/subfolder/daily-rss', $assignedVariables['page_url']); static::assertSame('http://shaarli/subfolder/daily-rss', $assignedVariables['page_url']);
static::assertFalse($assignedVariables['hide_timestamps']); static::assertFalse($assignedVariables['hide_timestamps']);
static::assertCount(2, $assignedVariables['days']); static::assertCount(3, $assignedVariables['days']);
$day = $assignedVariables['days'][$dates[0]->format('Ymd')]; $day = $assignedVariables['days'][$dates[0]->format('Ymd')];
$date = $dates[0]->setTime(23, 59, 59);
static::assertEquals($dates[0], $day['date']); static::assertEquals($date, $day['date']);
static::assertSame($dates[0]->format(\DateTime::RSS), $day['date_rss']); static::assertSame($date->format(\DateTime::RSS), $day['date_rss']);
static::assertSame(format_date($dates[0], false), $day['date_human']); static::assertSame(format_date($date, false), $day['date_human']);
static::assertSame('http://shaarli/subfolder/daily?day='. $dates[0]->format('Ymd'), $day['absolute_url']); static::assertSame('http://shaarli/subfolder/daily?day='. $dates[0]->format('Ymd'), $day['absolute_url']);
static::assertCount(1, $day['links']); static::assertCount(1, $day['links']);
static::assertSame(1, $day['links'][0]['id']); static::assertSame(1, $day['links'][0]['id']);
@ -411,10 +394,11 @@ class DailyControllerTest extends TestCase
static::assertEquals($dates[0], $day['links'][0]['created']); static::assertEquals($dates[0], $day['links'][0]['created']);
$day = $assignedVariables['days'][$dates[1]->format('Ymd')]; $day = $assignedVariables['days'][$dates[1]->format('Ymd')];
$date = $dates[1]->setTime(23, 59, 59);
static::assertEquals($dates[1], $day['date']); static::assertEquals($date, $day['date']);
static::assertSame($dates[1]->format(\DateTime::RSS), $day['date_rss']); static::assertSame($date->format(\DateTime::RSS), $day['date_rss']);
static::assertSame(format_date($dates[1], false), $day['date_human']); static::assertSame(format_date($date, false), $day['date_human']);
static::assertSame('http://shaarli/subfolder/daily?day='. $dates[1]->format('Ymd'), $day['absolute_url']); static::assertSame('http://shaarli/subfolder/daily?day='. $dates[1]->format('Ymd'), $day['absolute_url']);
static::assertCount(2, $day['links']); static::assertCount(2, $day['links']);
@ -424,6 +408,18 @@ class DailyControllerTest extends TestCase
static::assertSame(3, $day['links'][1]['id']); static::assertSame(3, $day['links'][1]['id']);
static::assertSame('http://domain.tld/3', $day['links'][1]['url']); static::assertSame('http://domain.tld/3', $day['links'][1]['url']);
static::assertEquals($dates[1], $day['links'][1]['created']); static::assertEquals($dates[1], $day['links'][1]['created']);
$day = $assignedVariables['days'][$dates[2]->format('Ymd')];
$date = $dates[2]->setTime(23, 59, 59);
static::assertEquals($date, $day['date']);
static::assertSame($date->format(\DateTime::RSS), $day['date_rss']);
static::assertSame(format_date($date, false), $day['date_human']);
static::assertSame('http://shaarli/subfolder/daily?day='. $dates[2]->format('Ymd'), $day['absolute_url']);
static::assertCount(1, $day['links']);
static::assertSame(4, $day['links'][0]['id']);
static::assertSame('http://domain.tld/4', $day['links'][0]['url']);
static::assertEquals($dates[2], $day['links'][0]['created']);
} }
/** /**
@ -475,4 +471,246 @@ class DailyControllerTest extends TestCase
static::assertFalse($assignedVariables['hide_timestamps']); static::assertFalse($assignedVariables['hide_timestamps']);
static::assertCount(0, $assignedVariables['days']); static::assertCount(0, $assignedVariables['days']);
} }
/**
* Test simple display index with week parameter
*/
public function testSimpleIndexWeekly(): void
{
$currentDay = new \DateTimeImmutable('2020-05-13');
$expectedDay = new \DateTimeImmutable('2020-05-11');
$request = $this->createMock(Request::class);
$request->method('getQueryParam')->willReturnCallback(function (string $key) use ($currentDay): ?string {
return $key === 'week' ? $currentDay->format('YW') : null;
});
$response = new Response();
// Save RainTPL assigned variables
$assignedVariables = [];
$this->assignTemplateVars($assignedVariables);
$this->container->bookmarkService
->expects(static::once())
->method('findByDate')
->willReturnCallback(
function (): array {
return [
(new Bookmark())
->setId(1)
->setUrl('http://url.tld')
->setTitle(static::generateString(50))
->setDescription(static::generateString(500))
,
(new Bookmark())
->setId(2)
->setUrl('http://url2.tld')
->setTitle(static::generateString(50))
->setDescription(static::generateString(500))
,
];
}
)
;
$result = $this->controller->index($request, $response);
static::assertSame(200, $result->getStatusCode());
static::assertSame('daily', (string) $result->getBody());
static::assertSame(
'Weekly - Week 20 (May 11, 2020) - Shaarli',
$assignedVariables['pagetitle']
);
static::assertCount(2, $assignedVariables['linksToDisplay']);
static::assertEquals($expectedDay->setTime(0, 0), $assignedVariables['dayDate']);
static::assertSame($expectedDay->setTime(0, 0)->getTimestamp(), $assignedVariables['day']);
static::assertSame('', $assignedVariables['previousday']);
static::assertSame('', $assignedVariables['nextday']);
static::assertSame('Week 20 (May 11, 2020)', $assignedVariables['dayDesc']);
static::assertSame('week', $assignedVariables['type']);
static::assertSame('Weekly', $assignedVariables['localizedType']);
}
/**
* Test simple display index with month parameter
*/
public function testSimpleIndexMonthly(): void
{
$currentDay = new \DateTimeImmutable('2020-05-13');
$expectedDay = new \DateTimeImmutable('2020-05-01');
$request = $this->createMock(Request::class);
$request->method('getQueryParam')->willReturnCallback(function (string $key) use ($currentDay): ?string {
return $key === 'month' ? $currentDay->format('Ym') : null;
});
$response = new Response();
// Save RainTPL assigned variables
$assignedVariables = [];
$this->assignTemplateVars($assignedVariables);
$this->container->bookmarkService
->expects(static::once())
->method('findByDate')
->willReturnCallback(
function (): array {
return [
(new Bookmark())
->setId(1)
->setUrl('http://url.tld')
->setTitle(static::generateString(50))
->setDescription(static::generateString(500))
,
(new Bookmark())
->setId(2)
->setUrl('http://url2.tld')
->setTitle(static::generateString(50))
->setDescription(static::generateString(500))
,
];
}
)
;
$result = $this->controller->index($request, $response);
static::assertSame(200, $result->getStatusCode());
static::assertSame('daily', (string) $result->getBody());
static::assertSame(
'Monthly - May, 2020 - Shaarli',
$assignedVariables['pagetitle']
);
static::assertCount(2, $assignedVariables['linksToDisplay']);
static::assertEquals($expectedDay->setTime(0, 0), $assignedVariables['dayDate']);
static::assertSame($expectedDay->setTime(0, 0)->getTimestamp(), $assignedVariables['day']);
static::assertSame('', $assignedVariables['previousday']);
static::assertSame('', $assignedVariables['nextday']);
static::assertSame('May, 2020', $assignedVariables['dayDesc']);
static::assertSame('month', $assignedVariables['type']);
static::assertSame('Monthly', $assignedVariables['localizedType']);
}
/**
* Test simple display RSS with week parameter
*/
public function testSimpleRssWeekly(): void
{
$dates = [
new \DateTimeImmutable('2020-05-19'),
new \DateTimeImmutable('2020-05-13'),
];
$expectedDates = [
new \DateTimeImmutable('2020-05-24 23:59:59'),
new \DateTimeImmutable('2020-05-17 23:59:59'),
];
$this->container->environment['QUERY_STRING'] = 'week';
$request = $this->createMock(Request::class);
$request->method('getQueryParam')->willReturnCallback(function (string $key): ?string {
return $key === 'week' ? '' : null;
});
$response = new Response();
$this->container->bookmarkService->expects(static::once())->method('search')->willReturn([
(new Bookmark())->setId(1)->setCreated($dates[0])->setUrl('http://domain.tld/1'),
(new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'),
(new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'),
]);
// Save RainTPL assigned variables
$assignedVariables = [];
$this->assignTemplateVars($assignedVariables);
$result = $this->controller->rss($request, $response);
static::assertSame(200, $result->getStatusCode());
static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]);
static::assertSame('dailyrss', (string) $result->getBody());
static::assertSame('Shaarli', $assignedVariables['title']);
static::assertSame('http://shaarli/subfolder/', $assignedVariables['index_url']);
static::assertSame('http://shaarli/subfolder/daily-rss?week', $assignedVariables['page_url']);
static::assertFalse($assignedVariables['hide_timestamps']);
static::assertCount(2, $assignedVariables['days']);
$day = $assignedVariables['days'][$dates[0]->format('YW')];
$date = $expectedDates[0];
static::assertEquals($date, $day['date']);
static::assertSame($date->format(\DateTime::RSS), $day['date_rss']);
static::assertSame('Week 21 (May 18, 2020)', $day['date_human']);
static::assertSame('http://shaarli/subfolder/daily?week='. $dates[0]->format('YW'), $day['absolute_url']);
static::assertCount(1, $day['links']);
$day = $assignedVariables['days'][$dates[1]->format('YW')];
$date = $expectedDates[1];
static::assertEquals($date, $day['date']);
static::assertSame($date->format(\DateTime::RSS), $day['date_rss']);
static::assertSame('Week 20 (May 11, 2020)', $day['date_human']);
static::assertSame('http://shaarli/subfolder/daily?week='. $dates[1]->format('YW'), $day['absolute_url']);
static::assertCount(2, $day['links']);
}
/**
* Test simple display RSS with month parameter
*/
public function testSimpleRssMonthly(): void
{
$dates = [
new \DateTimeImmutable('2020-05-19'),
new \DateTimeImmutable('2020-04-13'),
];
$expectedDates = [
new \DateTimeImmutable('2020-05-31 23:59:59'),
new \DateTimeImmutable('2020-04-30 23:59:59'),
];
$this->container->environment['QUERY_STRING'] = 'month';
$request = $this->createMock(Request::class);
$request->method('getQueryParam')->willReturnCallback(function (string $key): ?string {
return $key === 'month' ? '' : null;
});
$response = new Response();
$this->container->bookmarkService->expects(static::once())->method('search')->willReturn([
(new Bookmark())->setId(1)->setCreated($dates[0])->setUrl('http://domain.tld/1'),
(new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'),
(new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'),
]);
// Save RainTPL assigned variables
$assignedVariables = [];
$this->assignTemplateVars($assignedVariables);
$result = $this->controller->rss($request, $response);
static::assertSame(200, $result->getStatusCode());
static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]);
static::assertSame('dailyrss', (string) $result->getBody());
static::assertSame('Shaarli', $assignedVariables['title']);
static::assertSame('http://shaarli/subfolder/', $assignedVariables['index_url']);
static::assertSame('http://shaarli/subfolder/daily-rss?month', $assignedVariables['page_url']);
static::assertFalse($assignedVariables['hide_timestamps']);
static::assertCount(2, $assignedVariables['days']);
$day = $assignedVariables['days'][$dates[0]->format('Ym')];
$date = $expectedDates[0];
static::assertEquals($date, $day['date']);
static::assertSame($date->format(\DateTime::RSS), $day['date_rss']);
static::assertSame('May, 2020', $day['date_human']);
static::assertSame('http://shaarli/subfolder/daily?month='. $dates[0]->format('Ym'), $day['absolute_url']);
static::assertCount(1, $day['links']);
$day = $assignedVariables['days'][$dates[1]->format('Ym')];
$date = $expectedDates[1];
static::assertEquals($date, $day['date']);
static::assertSame($date->format(\DateTime::RSS), $day['date_rss']);
static::assertSame('April, 2020', $day['date_human']);
static::assertSame('http://shaarli/subfolder/daily?month='. $dates[1]->format('Ym'), $day['absolute_url']);
static::assertCount(2, $day['links']);
}
} }

View file

@ -0,0 +1,262 @@
<?php
declare(strict_types=1);
namespace Shaarli\Helper;
use Shaarli\Bookmark\Bookmark;
use Shaarli\TestCase;
use Slim\Http\Request;
class DailyPageHelperTest extends TestCase
{
/**
* @dataProvider getRequestedTypes
*/
public function testExtractRequestedType(array $queryParams, string $expectedType): void
{
$request = $this->createMock(Request::class);
$request->method('getQueryParam')->willReturnCallback(function ($key) use ($queryParams): ?string {
return $queryParams[$key] ?? null;
});
$type = DailyPageHelper::extractRequestedType($request);
static::assertSame($type, $expectedType);
}
/**
* @dataProvider getRequestedDateTimes
*/
public function testExtractRequestedDateTime(
string $type,
string $input,
?Bookmark $bookmark,
\DateTimeInterface $expectedDateTime,
string $compareFormat = 'Ymd'
): void {
$dateTime = DailyPageHelper::extractRequestedDateTime($type, $input, $bookmark);
static::assertSame($dateTime->format($compareFormat), $expectedDateTime->format($compareFormat));
}
public function testExtractRequestedDateTimeExceptionUnknownType(): void
{
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Unsupported daily format type');
DailyPageHelper::extractRequestedDateTime('nope', null, null);
}
/**
* @dataProvider getFormatsByType
*/
public function testGetFormatByType(string $type, string $expectedFormat): void
{
$format = DailyPageHelper::getFormatByType($type);
static::assertSame($expectedFormat, $format);
}
public function testGetFormatByTypeExceptionUnknownType(): void
{
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Unsupported daily format type');
DailyPageHelper::getFormatByType('nope');
}
/**
* @dataProvider getStartDatesByType
*/
public function testGetStartDatesByType(
string $type,
\DateTimeImmutable $dateTime,
\DateTimeInterface $expectedDateTime
): void {
$startDateTime = DailyPageHelper::getStartDateTimeByType($type, $dateTime);
static::assertEquals($expectedDateTime, $startDateTime);
}
public function testGetStartDatesByTypeExceptionUnknownType(): void
{
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Unsupported daily format type');
DailyPageHelper::getStartDateTimeByType('nope', new \DateTimeImmutable());
}
/**
* @dataProvider getEndDatesByType
*/
public function testGetEndDatesByType(
string $type,
\DateTimeImmutable $dateTime,
\DateTimeInterface $expectedDateTime
): void {
$endDateTime = DailyPageHelper::getEndDateTimeByType($type, $dateTime);
static::assertEquals($expectedDateTime, $endDateTime);
}
public function testGetEndDatesByTypeExceptionUnknownType(): void
{
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Unsupported daily format type');
DailyPageHelper::getEndDateTimeByType('nope', new \DateTimeImmutable());
}
/**
* @dataProvider getDescriptionsByType
*/
public function testGeDescriptionsByType(
string $type,
\DateTimeImmutable $dateTime,
string $expectedDescription
): void {
$description = DailyPageHelper::getDescriptionByType($type, $dateTime);
static::assertEquals($expectedDescription, $description);
}
public function getDescriptionByTypeExceptionUnknownType(): void
{
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Unsupported daily format type');
DailyPageHelper::getDescriptionByType('nope', new \DateTimeImmutable());
}
/**
* @dataProvider getRssLengthsByType
*/
public function testGeRssLengthsByType(string $type): void {
$length = DailyPageHelper::getRssLengthByType($type);
static::assertIsInt($length);
}
public function testGeRssLengthsByTypeExceptionUnknownType(): void
{
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Unsupported daily format type');
DailyPageHelper::getRssLengthByType('nope');
}
/**
* Data provider for testExtractRequestedType() test method.
*/
public function getRequestedTypes(): array
{
return [
[['month' => null], DailyPageHelper::DAY],
[['month' => ''], DailyPageHelper::MONTH],
[['month' => 'content'], DailyPageHelper::MONTH],
[['week' => null], DailyPageHelper::DAY],
[['week' => ''], DailyPageHelper::WEEK],
[['week' => 'content'], DailyPageHelper::WEEK],
[['day' => null], DailyPageHelper::DAY],
[['day' => ''], DailyPageHelper::DAY],
[['day' => 'content'], DailyPageHelper::DAY],
];
}
/**
* Data provider for testExtractRequestedDateTime() test method.
*/
public function getRequestedDateTimes(): array
{
return [
[DailyPageHelper::DAY, '20201013', null, new \DateTime('2020-10-13')],
[
DailyPageHelper::DAY,
'',
(new Bookmark())->setCreated($date = new \DateTime('2020-10-13 12:05:31')),
$date,
],
[DailyPageHelper::DAY, '', null, new \DateTime()],
[DailyPageHelper::WEEK, '202030', null, new \DateTime('2020-07-20')],
[
DailyPageHelper::WEEK,
'',
(new Bookmark())->setCreated($date = new \DateTime('2020-10-13 12:05:31')),
new \DateTime('2020-10-13'),
],
[DailyPageHelper::WEEK, '', null, new \DateTime(), 'Ym'],
[DailyPageHelper::MONTH, '202008', null, new \DateTime('2020-08-01'), 'Ym'],
[
DailyPageHelper::MONTH,
'',
(new Bookmark())->setCreated($date = new \DateTime('2020-10-13 12:05:31')),
new \DateTime('2020-10-13'),
'Ym'
],
[DailyPageHelper::MONTH, '', null, new \DateTime(), 'Ym'],
];
}
/**
* Data provider for testGetFormatByType() test method.
*/
public function getFormatsByType(): array
{
return [
[DailyPageHelper::DAY, 'Ymd'],
[DailyPageHelper::WEEK, 'YW'],
[DailyPageHelper::MONTH, 'Ym'],
];
}
/**
* Data provider for testGetStartDatesByType() test method.
*/
public function getStartDatesByType(): array
{
return [
[DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 00:00:00')],
[DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-05 00:00:00')],
[DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-01 00:00:00')],
];
}
/**
* Data provider for testGetEndDatesByType() test method.
*/
public function getEndDatesByType(): array
{
return [
[DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 23:59:59')],
[DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-11 23:59:59')],
[DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-31 23:59:59')],
];
}
/**
* Data provider for testGetDescriptionsByType() test method.
*/
public function getDescriptionsByType(): array
{
return [
[DailyPageHelper::DAY, $date = new \DateTimeImmutable(), 'Today - ' . $date->format('F d, Y')],
[DailyPageHelper::DAY, $date = new \DateTimeImmutable('-1 day'), 'Yesterday - ' . $date->format('F d, Y')],
[DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), 'October 9, 2020'],
[DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), 'Week 41 (October 5, 2020)'],
[DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), 'October, 2020'],
];
}
/**
* Data provider for testGetDescriptionsByType() test method.
*/
public function getRssLengthsByType(): array
{
return [
[DailyPageHelper::DAY],
[DailyPageHelper::WEEK],
[DailyPageHelper::MONTH],
];
}
}

View file

@ -6,12 +6,25 @@
<body> <body>
{include="page.header"} {include="page.header"}
<div class="pure-g">
<div class="pure-u-1 pure-alert pure-alert-success tag-sort">
<a href="{$base_path}/daily?day">{'Daily'|t}</a>
<a href="{$base_path}/daily?week">{'Weekly'|t}</a>
<a href="{$base_path}/daily?month">{'Monthly'|t}</a>
</div>
</div>
<div class="pure-g"> <div class="pure-g">
<div class="pure-u-lg-1-6 pure-u-1-24"></div> <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" id="daily"> <div class="pure-u-lg-2-3 pure-u-22-24 page-form page-visitor" id="daily">
<h2 class="window-title"> <h2 class="window-title">
{'The Daily Shaarli'|t} {$localizedType} Shaarli
<a href="{$base_path}/daily-rss" title="{'1 RSS entry per day'|t}"><i class="fa fa-rss"></i></a> <a href="{$base_path}/daily-rss?{$type}"
title="{function="t('1 RSS entry per :type', '', 1, 'shaarli', [':type' => t($type)])"}"
>
<i class="fa fa-rss"></i>
</a>
</h2> </h2>
<div id="plugin_zone_start_daily" class="plugin_zone"> <div id="plugin_zone_start_daily" class="plugin_zone">
@ -25,19 +38,19 @@
<div class="pure-g"> <div class="pure-g">
<div class="pure-u-lg-1-3 pure-u-1 center"> <div class="pure-u-lg-1-3 pure-u-1 center">
{if="$previousday"} {if="$previousday"}
<a href="{$base_path}/daily?day={$previousday}"> <a href="{$base_path}/daily?{$type}={$previousday}">
<i class="fa fa-arrow-left"></i> <i class="fa fa-arrow-left"></i>
{'Previous day'|t} {function="t('Previous :type', '', 1, 'shaarli', [':type' => t($type)], true)"}
</a> </a>
{/if} {/if}
</div> </div>
<div class="daily-desc pure-u-lg-1-3 pure-u-1 center"> <div class="daily-desc pure-u-lg-1-3 pure-u-1 center">
{'All links of one day in a single page.'|t} {function="t('All links of one :type in a single page.', '', 1, 'shaarli', [':type' => t($type)])"}
</div> </div>
<div class="pure-u-lg-1-3 pure-u-1 center"> <div class="pure-u-lg-1-3 pure-u-1 center">
{if="$nextday"} {if="$nextday"}
<a href="{$base_path}/daily?day={$nextday}"> <a href="{$base_path}/daily?{$type}={$nextday}">
{'Next day'|t} {function="t('Next :type', '', 1, 'shaarli', [':type' => t($type)], true)"}
<i class="fa fa-arrow-right"></i> <i class="fa fa-arrow-right"></i>
</a> </a>
{/if} {/if}
@ -45,10 +58,7 @@
</div> </div>
<div> <div>
<h3 class="window-subtitle"> <h3 class="window-subtitle">
{if="!empty($dayDesc)"} {$dayDesc}
{$dayDesc} -
{/if}
{function="format_date($dayDate, false)"}
</h3> </h3>
<div id="plugin_zone_about_daily" class="plugin_zone"> <div id="plugin_zone_about_daily" class="plugin_zone">

View file

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"> <rss version="2.0">
<channel> <channel>
<title>Daily - {$title}</title> <title>{$localizedType} - {$title}</title>
<link>{$index_url}</link> <link>{$index_url}</link>
<description>Daily shaared bookmarks</description> <description>{function="t('All links of one :type in a single page.', '', 1, 'shaarli', [':type' => t($type)])"}</description>
<language>{$language}</language> <language>{$language}</language>
<copyright>{$index_url}</copyright> <copyright>{$index_url}</copyright>
<generator>Shaarli</generator> <generator>Shaarli</generator>
@ -18,12 +18,15 @@
{loop="$value.links"} {loop="$value.links"}
<h3><a href="{$value.url}">{$value.title}</a></h3> <h3><a href="{$value.url}">{$value.title}</a></h3>
<small> <small>
{if="!$hide_timestamps"}{$value.created|format_date} - {/if}{if="$value.tags"}{$value.tags}{/if}<br> {if="!$hide_timestamps"}{$value.created|format_date} &#8212; {/if}
<a href="{$index_url}shaare/{$value.shorturl}">{'Permalink'|t}</a>
{if="$value.tags"} &#8212; {$value.tags}{/if}
<br>
{$value.url} {$value.url}
</small><br> </small><br>
{if="$value.thumbnail"}<img src="{$index_url}{$value.thumbnail}#" alt="thumbnail" />{/if}<br> {if="$value.thumbnail"}<img src="{$index_url}{$value.thumbnail}#" alt="thumbnail" />{/if}<br>
{if="$value.description"}{$value.description}{/if} {if="$value.description"}{$value.description}{/if}
<br><br><hr> <br><hr>
{/loop} {/loop}
]]></description> ]]></description>
</item> </item>