aboutsummaryrefslogtreecommitdiffhomepage
path: root/application
diff options
context:
space:
mode:
authorArthurHoaro <arthur@hoa.ro>2020-10-16 11:50:53 +0200
committerArthurHoaro <arthur@hoa.ro>2020-10-27 19:45:02 +0100
commit36e6d88dbfd753665224664d5214f39ccfbbf6a5 (patch)
tree7a4a8a7ec50d571d0de554cd80bb74750ae24569 /application
parentc2cd15dac2bfaebe6d32f7649fbdedc07400fa08 (diff)
downloadShaarli-36e6d88dbfd753665224664d5214f39ccfbbf6a5.tar.gz
Shaarli-36e6d88dbfd753665224664d5214f39ccfbbf6a5.tar.zst
Shaarli-36e6d88dbfd753665224664d5214f39ccfbbf6a5.zip
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
Diffstat (limited to 'application')
-rw-r--r--application/Utils.php33
-rw-r--r--application/bookmark/BookmarkFileService.php38
-rw-r--r--application/bookmark/BookmarkServiceInterface.php27
-rw-r--r--application/front/controller/visitor/DailyController.php105
-rw-r--r--application/helper/DailyPageHelper.php208
5 files changed, 338 insertions, 73 deletions
diff --git a/application/Utils.php b/application/Utils.php
index bc1c9f5d..db046893 100644
--- a/application/Utils.php
+++ b/application/Utils.php
@@ -327,6 +327,23 @@ function format_date($date, $time = true, $intl = true)
327} 327}
328 328
329/** 329/**
330 * Format the date month according to the locale.
331 *
332 * @param DateTimeInterface $date to format.
333 *
334 * @return bool|string Formatted date, or false if the input is invalid.
335 */
336function format_month(DateTimeInterface $date)
337{
338 if (! $date instanceof DateTimeInterface) {
339 return false;
340 }
341
342 return strftime('%B', $date->getTimestamp());
343}
344
345
346/**
330 * Check if the input is an integer, no matter its real type. 347 * Check if the input is an integer, no matter its real type.
331 * 348 *
332 * PHP is a bit messy regarding this: 349 * PHP is a bit messy regarding this:
@@ -454,16 +471,20 @@ function alphabetical_sort(&$data, $reverse = false, $byKeys = false)
454 * Wrapper function for translation which match the API 471 * Wrapper function for translation which match the API
455 * of gettext()/_() and ngettext(). 472 * of gettext()/_() and ngettext().
456 * 473 *
457 * @param string $text Text to translate. 474 * @param string $text Text to translate.
458 * @param string $nText The plural message ID. 475 * @param string $nText The plural message ID.
459 * @param int $nb The number of items for plural forms. 476 * @param int $nb The number of items for plural forms.
460 * @param string $domain The domain where the translation is stored (default: shaarli). 477 * @param string $domain The domain where the translation is stored (default: shaarli).
478 * @param array $variables Associative array of variables to replace in translated text.
479 * @param bool $fixCase Apply `ucfirst` on the translated string, might be useful for strings with variables.
461 * 480 *
462 * @return string Text translated. 481 * @return string Text translated.
463 */ 482 */
464function t($text, $nText = '', $nb = 1, $domain = 'shaarli') 483function t($text, $nText = '', $nb = 1, $domain = 'shaarli', $variables = [], $fixCase = false)
465{ 484{
466 return dn__($domain, $text, $nText, $nb); 485 $postFunction = $fixCase ? 'ucfirst' : function ($input) { return $input; };
486
487 return $postFunction(dn__($domain, $text, $nText, $nb, $variables));
467} 488}
468 489
469/** 490/**
diff --git a/application/bookmark/BookmarkFileService.php b/application/bookmark/BookmarkFileService.php
index 14b3d620..0df2f47f 100644
--- a/application/bookmark/BookmarkFileService.php
+++ b/application/bookmark/BookmarkFileService.php
@@ -343,26 +343,42 @@ class BookmarkFileService implements BookmarkServiceInterface
343 /** 343 /**
344 * @inheritDoc 344 * @inheritDoc
345 */ 345 */
346 public function days(): array 346 public function findByDate(
347 { 347 \DateTimeInterface $from,
348 $bookmarkDays = []; 348 \DateTimeInterface $to,
349 foreach ($this->search() as $bookmark) { 349 ?\DateTimeInterface &$previous,
350 $bookmarkDays[$bookmark->getCreated()->format('Ymd')] = 0; 350 ?\DateTimeInterface &$next
351 ): array {
352 $out = [];
353 $previous = null;
354 $next = null;
355
356 foreach ($this->search([], null, false, false, true) as $bookmark) {
357 if ($to < $bookmark->getCreated()) {
358 $next = $bookmark->getCreated();
359 } else if ($from < $bookmark->getCreated() && $to > $bookmark->getCreated()) {
360 $out[] = $bookmark;
361 } else {
362 if ($previous !== null) {
363 break;
364 }
365 $previous = $bookmark->getCreated();
366 }
351 } 367 }
352 $bookmarkDays = array_keys($bookmarkDays);
353 sort($bookmarkDays);
354 368
355 return array_map('strval', $bookmarkDays); 369 return $out;
356 } 370 }
357 371
358 /** 372 /**
359 * @inheritDoc 373 * @inheritDoc
360 */ 374 */
361 public function filterDay(string $request) 375 public function getLatest(): ?Bookmark
362 { 376 {
363 $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC; 377 foreach ($this->search([], null, false, false, true) as $bookmark) {
378 return $bookmark;
379 }
364 380
365 return $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_DAY, $request, false, $visibility); 381 return null;
366 } 382 }
367 383
368 /** 384 /**
diff --git a/application/bookmark/BookmarkServiceInterface.php b/application/bookmark/BookmarkServiceInterface.php
index 9fa61533..08cdbb4e 100644
--- a/application/bookmark/BookmarkServiceInterface.php
+++ b/application/bookmark/BookmarkServiceInterface.php
@@ -156,22 +156,29 @@ interface BookmarkServiceInterface
156 public function bookmarksCountPerTag(array $filteringTags = [], ?string $visibility = null): array; 156 public function bookmarksCountPerTag(array $filteringTags = [], ?string $visibility = null): array;
157 157
158 /** 158 /**
159 * Returns the list of days containing articles (oldest first) 159 * Return a list of bookmark matching provided period of time.
160 * It also update directly previous and next date outside of given period found in the datastore.
160 * 161 *
161 * @return array containing days (in format YYYYMMDD). 162 * @param \DateTimeInterface $from Starting date.
163 * @param \DateTimeInterface $to Ending date.
164 * @param \DateTimeInterface|null $previous (by reference) updated with first created date found before $from.
165 * @param \DateTimeInterface|null $next (by reference) updated with first created date found after $to.
166 *
167 * @return array List of bookmarks matching provided period of time.
162 */ 168 */
163 public function days(): array; 169 public function findByDate(
170 \DateTimeInterface $from,
171 \DateTimeInterface $to,
172 ?\DateTimeInterface &$previous,
173 ?\DateTimeInterface &$next
174 ): array;
164 175
165 /** 176 /**
166 * Returns the list of articles for a given day. 177 * Returns the latest bookmark by creation date.
167 *
168 * @param string $request day to filter. Format: YYYYMMDD.
169 * 178 *
170 * @return Bookmark[] list of shaare found. 179 * @return Bookmark|null Found Bookmark or null if the datastore is empty.
171 *
172 * @throws BookmarkNotFoundException
173 */ 180 */
174 public function filterDay(string $request); 181 public function getLatest(): ?Bookmark;
175 182
176 /** 183 /**
177 * Creates the default database after a fresh install. 184 * Creates the default database after a fresh install.
diff --git a/application/front/controller/visitor/DailyController.php b/application/front/controller/visitor/DailyController.php
index 07617cf1..728bc2d8 100644
--- a/application/front/controller/visitor/DailyController.php
+++ b/application/front/controller/visitor/DailyController.php
@@ -5,8 +5,8 @@ declare(strict_types=1);
5namespace Shaarli\Front\Controller\Visitor; 5namespace Shaarli\Front\Controller\Visitor;
6 6
7use DateTime; 7use DateTime;
8use DateTimeImmutable;
9use Shaarli\Bookmark\Bookmark; 8use Shaarli\Bookmark\Bookmark;
9use Shaarli\Helper\DailyPageHelper;
10use Shaarli\Render\TemplatePage; 10use Shaarli\Render\TemplatePage;
11use Slim\Http\Request; 11use Slim\Http\Request;
12use Slim\Http\Response; 12use Slim\Http\Response;
@@ -26,32 +26,20 @@ class DailyController extends ShaarliVisitorController
26 */ 26 */
27 public function index(Request $request, Response $response): Response 27 public function index(Request $request, Response $response): Response
28 { 28 {
29 $day = $request->getQueryParam('day') ?? date('Ymd'); 29 $type = DailyPageHelper::extractRequestedType($request);
30 30 $format = DailyPageHelper::getFormatByType($type);
31 $availableDates = $this->container->bookmarkService->days(); 31 $latestBookmark = $this->container->bookmarkService->getLatest();
32 $nbAvailableDates = count($availableDates); 32 $dateTime = DailyPageHelper::extractRequestedDateTime($type, $request->getQueryParam($type), $latestBookmark);
33 $index = array_search($day, $availableDates); 33 $start = DailyPageHelper::getStartDateTimeByType($type, $dateTime);
34 34 $end = DailyPageHelper::getEndDateTimeByType($type, $dateTime);
35 if ($index === false) { 35 $dailyDesc = DailyPageHelper::getDescriptionByType($type, $dateTime);
36 // no bookmarks for day, but at least one day with bookmarks 36
37 $day = $availableDates[$nbAvailableDates - 1] ?? $day; 37 $linksToDisplay = $this->container->bookmarkService->findByDate(
38 $previousDay = $availableDates[$nbAvailableDates - 2] ?? ''; 38 $start,
39 } else { 39 $end,
40 $previousDay = $availableDates[$index - 1] ?? ''; 40 $previousDay,
41 $nextDay = $availableDates[$index + 1] ?? ''; 41 $nextDay
42 } 42 );
43
44 if ($day === date('Ymd')) {
45 $this->assignView('dayDesc', t('Today'));
46 } elseif ($day === date('Ymd', strtotime('-1 days'))) {
47 $this->assignView('dayDesc', t('Yesterday'));
48 }
49
50 try {
51 $linksToDisplay = $this->container->bookmarkService->filterDay($day);
52 } catch (\Exception $exc) {
53 $linksToDisplay = [];
54 }
55 43
56 $formatter = $this->container->formatterFactory->getFormatter(); 44 $formatter = $this->container->formatterFactory->getFormatter();
57 $formatter->addContextData('base_path', $this->container->basePath); 45 $formatter->addContextData('base_path', $this->container->basePath);
@@ -63,13 +51,15 @@ class DailyController extends ShaarliVisitorController
63 $linksToDisplay[$key]['description'] = $bookmark->getDescription(); 51 $linksToDisplay[$key]['description'] = $bookmark->getDescription();
64 } 52 }
65 53
66 $dayDate = DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000');
67 $data = [ 54 $data = [
68 'linksToDisplay' => $linksToDisplay, 55 'linksToDisplay' => $linksToDisplay,
69 'day' => $dayDate->getTimestamp(), 56 'dayDate' => $start,
70 'dayDate' => $dayDate, 57 'day' => $start->getTimestamp(),
71 'previousday' => $previousDay ?? '', 58 'previousday' => $previousDay ? $previousDay->format($format) : '',
72 'nextday' => $nextDay ?? '', 59 'nextday' => $nextDay ? $nextDay->format($format) : '',
60 'dayDesc' => $dailyDesc,
61 'type' => $type,
62 'localizedType' => $this->translateType($type),
73 ]; 63 ];
74 64
75 // Hooks are called before column construction so that plugins don't have to deal with columns. 65 // Hooks are called before column construction so that plugins don't have to deal with columns.
@@ -82,7 +72,7 @@ class DailyController extends ShaarliVisitorController
82 $mainTitle = $this->container->conf->get('general.title', 'Shaarli'); 72 $mainTitle = $this->container->conf->get('general.title', 'Shaarli');
83 $this->assignView( 73 $this->assignView(
84 'pagetitle', 74 'pagetitle',
85 t('Daily') .' - '. format_date($dayDate, false) . ' - ' . $mainTitle 75 $data['localizedType'] . ' - ' . $data['dayDesc'] . ' - ' . $mainTitle
86 ); 76 );
87 77
88 return $response->write($this->render(TemplatePage::DAILY)); 78 return $response->write($this->render(TemplatePage::DAILY));
@@ -106,11 +96,14 @@ class DailyController extends ShaarliVisitorController
106 } 96 }
107 97
108 $days = []; 98 $days = [];
99 $type = DailyPageHelper::extractRequestedType($request);
100 $format = DailyPageHelper::getFormatByType($type);
101 $length = DailyPageHelper::getRssLengthByType($type);
109 foreach ($this->container->bookmarkService->search() as $bookmark) { 102 foreach ($this->container->bookmarkService->search() as $bookmark) {
110 $day = $bookmark->getCreated()->format('Ymd'); 103 $day = $bookmark->getCreated()->format($format);
111 104
112 // Stop iterating after DAILY_RSS_NB_DAYS entries 105 // Stop iterating after DAILY_RSS_NB_DAYS entries
113 if (count($days) === static::$DAILY_RSS_NB_DAYS && !isset($days[$day])) { 106 if (count($days) === $length && !isset($days[$day])) {
114 break; 107 break;
115 } 108 }
116 109
@@ -127,12 +120,19 @@ class DailyController extends ShaarliVisitorController
127 120
128 /** @var Bookmark[] $bookmarks */ 121 /** @var Bookmark[] $bookmarks */
129 foreach ($days as $day => $bookmarks) { 122 foreach ($days as $day => $bookmarks) {
130 $dayDatetime = DateTimeImmutable::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000'); 123 $dayDateTime = DailyPageHelper::extractRequestedDateTime($type, (string) $day);
124 $endDateTime = DailyPageHelper::getEndDateTimeByType($type, $dayDateTime);
125
126 // We only want the RSS entry to be published when the period is over.
127 if (new DateTime() < $endDateTime) {
128 continue;
129 }
130
131 $dataPerDay[$day] = [ 131 $dataPerDay[$day] = [
132 'date' => $dayDatetime, 132 'date' => $endDateTime,
133 'date_rss' => $dayDatetime->format(DateTime::RSS), 133 'date_rss' => $endDateTime->format(DateTime::RSS),
134 'date_human' => format_date($dayDatetime, false, true), 134 'date_human' => DailyPageHelper::getDescriptionByType($type, $dayDateTime),
135 'absolute_url' => $indexUrl . 'daily?day=' . $day, 135 'absolute_url' => $indexUrl . 'daily?'. $type .'=' . $day,
136 'links' => [], 136 'links' => [],
137 ]; 137 ];
138 138
@@ -141,16 +141,20 @@ class DailyController extends ShaarliVisitorController
141 141
142 // Make permalink URL absolute 142 // Make permalink URL absolute
143 if ($bookmark->isNote()) { 143 if ($bookmark->isNote()) {
144 $dataPerDay[$day]['links'][$key]['url'] = $indexUrl . $bookmark->getUrl(); 144 $dataPerDay[$day]['links'][$key]['url'] = rtrim($indexUrl, '/') . $bookmark->getUrl();
145 } 145 }
146 } 146 }
147 } 147 }
148 148
149 $this->assignView('title', $this->container->conf->get('general.title', 'Shaarli')); 149 $this->assignAllView([
150 $this->assignView('index_url', $indexUrl); 150 'title' => $this->container->conf->get('general.title', 'Shaarli'),
151 $this->assignView('page_url', $pageUrl); 151 'index_url' => $indexUrl,
152 $this->assignView('hide_timestamps', $this->container->conf->get('privacy.hide_timestamps', false)); 152 'page_url' => $pageUrl,
153 $this->assignView('days', $dataPerDay); 153 'hide_timestamps' => $this->container->conf->get('privacy.hide_timestamps', false),
154 'days' => $dataPerDay,
155 'type' => $type,
156 'localizedType' => $this->translateType($type),
157 ]);
154 158
155 $rssContent = $this->render(TemplatePage::DAILY_RSS); 159 $rssContent = $this->render(TemplatePage::DAILY_RSS);
156 160
@@ -189,4 +193,13 @@ class DailyController extends ShaarliVisitorController
189 193
190 return $columns; 194 return $columns;
191 } 195 }
196
197 protected function translateType($type): string
198 {
199 return [
200 t('day') => t('Daily'),
201 t('week') => t('Weekly'),
202 t('month') => t('Monthly'),
203 ][t($type)] ?? t('Daily');
204 }
192} 205}
diff --git a/application/helper/DailyPageHelper.php b/application/helper/DailyPageHelper.php
new file mode 100644
index 00000000..5fabc907
--- /dev/null
+++ b/application/helper/DailyPageHelper.php
@@ -0,0 +1,208 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Helper;
6
7use Shaarli\Bookmark\Bookmark;
8use Slim\Http\Request;
9
10class DailyPageHelper
11{
12 public const MONTH = 'month';
13 public const WEEK = 'week';
14 public const DAY = 'day';
15
16 /**
17 * Extracts the type of the daily to display from the HTTP request parameters
18 *
19 * @param Request $request HTTP request
20 *
21 * @return string month/week/day
22 */
23 public static function extractRequestedType(Request $request): string
24 {
25 if ($request->getQueryParam(static::MONTH) !== null) {
26 return static::MONTH;
27 } elseif ($request->getQueryParam(static::WEEK) !== null) {
28 return static::WEEK;
29 }
30
31 return static::DAY;
32 }
33
34 /**
35 * Extracts a DateTimeImmutable from provided HTTP request.
36 * If no parameter is provided, we rely on the creation date of the latest provided created bookmark.
37 * If the datastore is empty or no bookmark is provided, we use the current date.
38 *
39 * @param string $type month/week/day
40 * @param string|null $requestedDate Input string extracted from the request
41 * @param Bookmark|null $latestBookmark Latest bookmark found in the datastore (by date)
42 *
43 * @return \DateTimeImmutable from input or latest bookmark.
44 *
45 * @throws \Exception Type not supported.
46 */
47 public static function extractRequestedDateTime(
48 string $type,
49 ?string $requestedDate,
50 Bookmark $latestBookmark = null
51 ): \DateTimeImmutable {
52 $format = static::getFormatByType($type);
53 if (empty($requestedDate)) {
54 return $latestBookmark instanceof Bookmark
55 ? new \DateTimeImmutable($latestBookmark->getCreated()->format(\DateTime::ATOM))
56 : new \DateTimeImmutable()
57 ;
58 }
59
60 // W is not supported by createFromFormat...
61 if ($type === static::WEEK) {
62 return (new \DateTimeImmutable())
63 ->setISODate((int) substr($requestedDate, 0, 4), (int) substr($requestedDate, 4, 2))
64 ;
65 }
66
67 return \DateTimeImmutable::createFromFormat($format, $requestedDate);
68 }
69
70 /**
71 * Get the DateTime format used by provided type
72 * Examples:
73 * - day: 20201016 (<year><month><day>)
74 * - week: 202041 (<year><week number>)
75 * - month: 202010 (<year><month>)
76 *
77 * @param string $type month/week/day
78 *
79 * @return string DateTime compatible format
80 *
81 * @see https://www.php.net/manual/en/datetime.format.php
82 *
83 * @throws \Exception Type not supported.
84 */
85 public static function getFormatByType(string $type): string
86 {
87 switch ($type) {
88 case static::MONTH:
89 return 'Ym';
90 case static::WEEK:
91 return 'YW';
92 case static::DAY:
93 return 'Ymd';
94 default:
95 throw new \Exception('Unsupported daily format type');
96 }
97 }
98
99 /**
100 * Get the first DateTime of the time period depending on given datetime and type.
101 * Note: DateTimeImmutable is required because we rely heavily on DateTime->modify() syntax
102 * and we don't want to alter original datetime.
103 *
104 * @param string $type month/week/day
105 * @param \DateTimeImmutable $requested DateTime extracted from request input
106 * (should come from extractRequestedDateTime)
107 *
108 * @return \DateTimeInterface First DateTime of the time period
109 *
110 * @throws \Exception Type not supported.
111 */
112 public static function getStartDateTimeByType(string $type, \DateTimeImmutable $requested): \DateTimeInterface
113 {
114 switch ($type) {
115 case static::MONTH:
116 return $requested->modify('first day of this month midnight');
117 case static::WEEK:
118 return $requested->modify('Monday this week midnight');
119 case static::DAY:
120 return $requested->modify('Today midnight');
121 default:
122 throw new \Exception('Unsupported daily format type');
123 }
124 }
125
126 /**
127 * Get the last DateTime of the time period depending on given datetime and type.
128 * Note: DateTimeImmutable is required because we rely heavily on DateTime->modify() syntax
129 * and we don't want to alter original datetime.
130 *
131 * @param string $type month/week/day
132 * @param \DateTimeImmutable $requested DateTime extracted from request input
133 * (should come from extractRequestedDateTime)
134 *
135 * @return \DateTimeInterface Last DateTime of the time period
136 *
137 * @throws \Exception Type not supported.
138 */
139 public static function getEndDateTimeByType(string $type, \DateTimeImmutable $requested): \DateTimeInterface
140 {
141 switch ($type) {
142 case static::MONTH:
143 return $requested->modify('last day of this month 23:59:59');
144 case static::WEEK:
145 return $requested->modify('Sunday this week 23:59:59');
146 case static::DAY:
147 return $requested->modify('Today 23:59:59');
148 default:
149 throw new \Exception('Unsupported daily format type');
150 }
151 }
152
153 /**
154 * Get localized description of the time period depending on given datetime and type.
155 * Example: for a month period, it returns `October, 2020`.
156 *
157 * @param string $type month/week/day
158 * @param \DateTimeImmutable $requested DateTime extracted from request input
159 * (should come from extractRequestedDateTime)
160 *
161 * @return string Localized time period description
162 *
163 * @throws \Exception Type not supported.
164 */
165 public static function getDescriptionByType(string $type, \DateTimeImmutable $requested): string
166 {
167 switch ($type) {
168 case static::MONTH:
169 return $requested->format('F') . ', ' . $requested->format('Y');
170 case static::WEEK:
171 $requested = $requested->modify('Monday this week');
172 return t('Week') . ' ' . $requested->format('W') . ' (' . format_date($requested, false) . ')';
173 case static::DAY:
174 $out = '';
175 if ($requested->format('Ymd') === date('Ymd')) {
176 $out = t('Today') . ' - ';
177 } elseif ($requested->format('Ymd') === date('Ymd', strtotime('-1 days'))) {
178 $out = t('Yesterday') . ' - ';
179 }
180 return $out . format_date($requested, false);
181 default:
182 throw new \Exception('Unsupported daily format type');
183 }
184 }
185
186 /**
187 * Get the number of items to display in the RSS feed depending on the given type.
188 *
189 * @param string $type month/week/day
190 *
191 * @return int number of elements
192 *
193 * @throws \Exception Type not supported.
194 */
195 public static function getRssLengthByType(string $type): int
196 {
197 switch ($type) {
198 case static::MONTH:
199 return 12; // 1 year
200 case static::WEEK:
201 return 26; // ~6 months
202 case static::DAY:
203 return 30; // ~1 month
204 default:
205 throw new \Exception('Unsupported daily format type');
206 }
207 }
208}