Skip to content

Commit 76709b7

Browse files
Amaia Anabitartesarjona
Amaia Anabitarte
authored andcommitted
MDL-83873 core_calendar: New human date format
1 parent 7a318d5 commit 76709b7

File tree

6 files changed

+477
-0
lines changed

6 files changed

+477
-0
lines changed

calendar/classes/output/humandate.php

+170
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
<?php
2+
// This file is part of Moodle - http://moodle.org/
3+
//
4+
// Moodle is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// Moodle is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU General Public License
15+
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16+
17+
namespace core_calendar\output;
18+
19+
use core\output\pix_icon;
20+
use core\output\templatable;
21+
use core\output\renderable;
22+
use core\output\renderer_base;
23+
use core\url;
24+
25+
/**
26+
* Class humandate.
27+
*
28+
* This class is used to render a timestamp as a human readable date.
29+
* The main difference between userdate and this class is that this class
30+
* will render the date as "Today", "Yesterday", "Tomorrow" if the date is
31+
* close to the current date. Also, it will add alert styling if the date
32+
* is near.
33+
*
34+
* @package core_calendar
35+
* @copyright 2024 Ferran Recio <ferran@moodle.com>
36+
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
37+
*/
38+
class humandate implements renderable, templatable {
39+
/** @var int $now the current timestamp. */
40+
protected int $now;
41+
42+
/**
43+
* Class constructor.
44+
*
45+
* @param int $timestamp The timestamp.
46+
* @param int|null $near The number of seconds that indicates a nearby date. Default to DAYSECS, use null for no indication.
47+
* @param bool $timeonly Wether the date should be shown completely or time only.
48+
* @param url|null $link URL to link the date to.
49+
* @param string|null $langtimeformat Lang date and time format to use to format the date.
50+
* @param bool $userelatives Whether to use human common words or not.
51+
*/
52+
public function __construct(
53+
/** @var int $timestamp the timestamp. */
54+
protected int $timestamp,
55+
/** @var int|null $near the number of seconds within which a date is considered near. 1 day by default. */
56+
protected int|null $near = DAYSECS,
57+
/** @var bool $timeonly whether we should show time only or date and time. */
58+
protected bool $timeonly = false,
59+
/** @var url|null $link Link for the date. */
60+
protected url|null $link = null,
61+
/** @var string|null $langtimeformat an optional date format to apply. */
62+
protected string|null $langtimeformat = null,
63+
/** @var bool $userelatives whether to use human relative terminology. */
64+
protected bool $userelatives = true,
65+
) {
66+
$this->now = time();
67+
}
68+
69+
#[\Override]
70+
public function export_for_template(renderer_base $output): array {
71+
$userdate = userdate($this->timestamp, get_string('strftimedayshort'));
72+
$due = $this->timestamp - $this->now;
73+
$relative = null;
74+
if ($this->userelatives) {
75+
$relative = $this->format_relative_date();
76+
}
77+
78+
if ($this->timeonly) {
79+
$date = null;
80+
} else {
81+
$date = $relative ?? $userdate;
82+
}
83+
$data = [
84+
'timestamp' => $this->timestamp,
85+
'userdate' => $userdate,
86+
'date' => $date,
87+
'time' => $this->format_time(),
88+
'ispast' => $this->timestamp < $this->now,
89+
'needtitle' => ($relative !== null || $this->timeonly),
90+
'link' => $this->link ? $this->link->out(false) : '',
91+
];
92+
if (($this->near !== null) && ($due < $this->near && $due > 0)) {
93+
$icon = new pix_icon(
94+
pix: 'i/warning',
95+
alt: get_string('warning'),
96+
component: 'moodle',
97+
attributes: ['class' => 'me-0 pb-1']
98+
);
99+
$data['isnear'] = true;
100+
$data['nearicon'] = $icon->export_for_template($output);
101+
}
102+
return $data;
103+
}
104+
105+
/**
106+
* Formats the timestamp as a relative date string (e.g., "Today", "Yesterday", "Tomorrow").
107+
*
108+
* This method compares the given timestamp with the current date and returns a formatted
109+
* string representing the relative date. If the timestamp corresponds to today, yesterday,
110+
* or tomorrow, it returns the appropriate string. Otherwise, it returns null.
111+
*
112+
* @return string|null
113+
*/
114+
private function format_relative_date(): string|null {
115+
$this->now = time();
116+
117+
if (date('Y-m-d', $this->timestamp) == date('Y-m-d', $this->now)) {
118+
$format = get_string('strftimerelativetoday', 'langconfig');
119+
} else if (date('Y-m-d', $this->timestamp) == date('Y-m-d', strtotime('yesterday', $this->now))) {
120+
$format = get_string('strftimerelativeyesterday', 'langconfig');
121+
} else if (date('Y-m-d', $this->timestamp) == date('Y-m-d', strtotime('tomorrow', $this->now))) {
122+
$format = get_string('strftimerelativetomorrow', 'langconfig');
123+
} else {
124+
return null;
125+
}
126+
127+
$calendartype = \core_calendar\type_factory::get_calendar_instance();
128+
return $calendartype->timestamp_to_date_string(
129+
time: $this->timestamp,
130+
format: $format,
131+
timezone: 99,
132+
fixday: true,
133+
fixhour: true,
134+
);
135+
}
136+
137+
/**
138+
* Formats the timestamp as a human readable time.
139+
*
140+
* This method compares the given timestamp with the current date and returns a formatted
141+
* string representing the time.
142+
*
143+
* @return string
144+
*/
145+
private function format_time(): string {
146+
147+
$timeformat = get_user_preferences('calendar_timeformat');
148+
if (empty($timeformat)) {
149+
$timeformat = get_config(null, 'calendar_site_timeformat');
150+
}
151+
152+
// Allow language customization of selected time format.
153+
if ($timeformat === CALENDAR_TF_12) {
154+
$timeformat = get_string('strftimetime12', 'langconfig');
155+
} else if ($timeformat === CALENDAR_TF_24) {
156+
$timeformat = get_string('strftimetime24', 'langconfig');
157+
}
158+
159+
if ($timeformat) {
160+
return userdate($this->timestamp, $timeformat);
161+
}
162+
163+
// Let's use default format.
164+
if ($this->langtimeformat === null) {
165+
$langtimeformat = get_string('strftimetime');
166+
}
167+
168+
return userdate($this->timestamp, $langtimeformat);
169+
}
170+
}
+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<?php
2+
// This file is part of Moodle - http://moodle.org/
3+
//
4+
// Moodle is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// Moodle is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU General Public License
15+
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16+
17+
namespace core_calendar\output;
18+
19+
use core\output\templatable;
20+
use core\output\renderable;
21+
use core\output\renderer_base;
22+
use core\url;
23+
24+
/**
25+
* Class humantimeperiod.
26+
*
27+
* This class is used to render a time period as a human readable date.
28+
* The main difference between userdate and this class is that this class
29+
* will render the date as "Today", "Yesterday", "Tomorrow" if the date is
30+
* close to the current date. Also, it will add styling if the date
31+
* is near.
32+
*
33+
* @package core_calendar
34+
* @copyright 2025 Amaia Anabitarte <amaia@moodle.com>
35+
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
36+
*/
37+
class humantimeperiod implements renderable, templatable {
38+
39+
/**
40+
* Class constructor.
41+
*
42+
* @param int $starttimestamp The starting timestamp.
43+
* @param int $endtimestamp The ending timestamp.
44+
* @param int|null $near The number of seconds that indicates a nearby date. Default to DAYSECS, use null for no indication.
45+
* @param url|null $link URL to link the date to.
46+
* @param string|null $langtimeformat Lang date and time format to use to format the date.
47+
* @param bool $userelatives Whether to use human common words or not.
48+
*/
49+
public function __construct(
50+
/** @var int $starttimestamp the starting timestamp. */
51+
protected int $starttimestamp,
52+
/** @var int $endtimestamp the ending timestamp. */
53+
protected int $endtimestamp,
54+
/** @var int|null $near the number of seconds within which a date is considered near. 1 day by default. */
55+
protected int|null $near = DAYSECS,
56+
/** @var url|null $link Link for the dates. */
57+
protected url|null $link = null,
58+
/** @var string|null $langtimeformat an optional date format to apply. */
59+
protected string|null $langtimeformat = null,
60+
/** @var bool $userelatives whether to use human relative terminology. */
61+
protected bool $userelatives = true,
62+
) {
63+
}
64+
65+
#[\Override]
66+
public function export_for_template(renderer_base $output): array {
67+
$period = $this->format_period();
68+
return [
69+
'startdate' => $period['startdate']->export_for_template($output),
70+
'enddate' => $period['enddate'] ? $period['enddate']->export_for_template($output) : null,
71+
];
72+
}
73+
74+
/**
75+
* Format a time periods based on 2 dates.
76+
*
77+
* @return array An array of one or two humandate elements.
78+
*/
79+
private function format_period(): array {
80+
81+
$linkstart = null;
82+
$linkend = null;
83+
if ($this->link) {
84+
$linkstart = new url($this->link, ['view' => 'day', 'time' => $this->starttimestamp]);
85+
$linkend = new url($this->link, ['view' => 'day', 'time' => $this->endtimestamp]);
86+
}
87+
88+
$startdate = new humandate(
89+
timestamp: $this->starttimestamp,
90+
near: $this->near,
91+
link: $linkstart,
92+
langtimeformat: $this->langtimeformat,
93+
userelatives: $this->userelatives,
94+
);
95+
96+
if ($this->endtimestamp == null || $this->starttimestamp == $this->endtimestamp) {
97+
return [
98+
'startdate' => $startdate,
99+
'enddate' => null,
100+
];
101+
}
102+
103+
// Get the midnight of the day the event will start.
104+
$usermidnightstart = usergetmidnight($this->starttimestamp);
105+
// Get the midnight of the day the event will end.
106+
$usermidnightend = usergetmidnight($this->endtimestamp);
107+
// Check if we will still be on the same day.
108+
$issameday = ($usermidnightstart == $usermidnightend);
109+
110+
$enddate = new humandate(
111+
timestamp: $this->endtimestamp,
112+
near: $this->near,
113+
timeonly: $issameday,
114+
link: $linkend,
115+
langtimeformat: $this->langtimeformat,
116+
userelatives: $this->userelatives,
117+
);
118+
119+
return [
120+
'startdate' => $startdate,
121+
'enddate' => $enddate,
122+
];
123+
}
124+
}

calendar/templates/humandate.mustache

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{{!
2+
This file is part of Moodle - http://moodle.org/
3+
4+
Moodle is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation, either version 3 of the License, or
7+
(at your option) any later version.
8+
9+
Moodle is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU General Public License for more details.
13+
14+
You should have received a copy of the GNU General Public License
15+
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16+
}}
17+
{{!
18+
@template core_calendar/humandate
19+
20+
Print a human readable date format.
21+
22+
Example context (json):
23+
{
24+
"userdate": "2021-09-01",
25+
"date": "Tomorrow, 23 January",
26+
"time": "12:00 AM",
27+
"timestamp": "1630512000",
28+
"ispast": false,
29+
"isnear": true,
30+
"nearicon": {
31+
"extraclasses": "me-0 pb-1",
32+
"attributes": [
33+
{"name": "src", "value": "../../../pix/i/warning.svg"},
34+
{"name": "alt", "value": "Warning"}
35+
]
36+
},
37+
"needtitle": true,
38+
"link": "https://example.com/calendar/view.php"
39+
}
40+
}}
41+
{{#link}}
42+
<a href="{{link}}" title="{{userdate}}">
43+
{{/link}}
44+
<span
45+
class="date {{#ispast}}dimmed_text{{/ispast}} {{#isnear}}text-danger{{/isnear}}"
46+
data-timestamp="{{timestamp}}"
47+
{{#needtitle}} title="{{userdate}}" {{/needtitle}}
48+
>
49+
{{#nearicon}}
50+
{{>core/pix_icon}}
51+
{{/nearicon}}
52+
{{#date}}{{date}}{{#time}}, {{/time}}{{/date}}{{time}}
53+
</span>
54+
{{#link}}
55+
</a>
56+
{{/link}}

0 commit comments

Comments
 (0)