MDL-83873 core_calendar: New human date format

This commit is contained in:
Amaia Anabitarte
2025-02-26 12:31:42 +01:00
committed by Sara Arjona
parent 7a318d5c85
commit e6813f315e
8 changed files with 1129 additions and 0 deletions
+335
View File
@@ -0,0 +1,335 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_calendar\output;
use DateInterval;
use DateTimeInterface;
use DateTimeImmutable;
use core\output\pix_icon;
use core\output\templatable;
use core\output\renderable;
use core\output\renderer_base;
use core\clock;
use core\url;
/**
* Class humandate.
*
* This class is used to render a timestamp as a human readable date.
* The main difference between userdate and this class is that this class
* will render the date as "Today", "Yesterday", "Tomorrow" if the date is
* close to the current date. Also, it will add alert styling if the date
* is near.
*
* @package core_calendar
* @copyright 2024 Ferran Recio <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class humandate implements renderable, templatable {
/** @var int|null The number of seconds within which a date is considered near. 1 day by default. */
protected ?int $near = DAYSECS;
/** @var bool Whether we should show time only or date and time. */
protected bool $timeonly = false;
/** @var url|null Link for the date. */
protected ?url $link = null;
/** @var string|null An optional date format to apply. */
protected ?string $langtimeformat = null;
/** @var bool Whether to use human relative terminology. */
protected bool $userelatives = true;
/** @var clock The clock interface to handle time. */
protected clock $clock;
/**
* Class constructor.
*
* Use the factory methods, such as create_from_timestamp or create_from_datetime, instead.
*
* @param DateTimeImmutable $datetime The datetime.
*/
protected function __construct(
/** @var DateTimeImmutable $datetime The datetime. **/
protected DateTimeImmutable $datetime,
) {
$this->clock = \core\di::get(clock::class);
}
/**
* Creates a new humandate instance from a timestamp.
*
* @param int $timestamp The timestamp.
* @param int|null $near The number of seconds within which a date is considered near. 1 day by default.
* @param bool $timeonly Whether we should show time only or date and time.
* @param url|null $link Link for the date.
* @param string|null $langtimeformat An optional date format to apply.
* @param bool $userelatives Whether to use human relative terminology.
* @return humandate The new instance.
*/
public static function create_from_timestamp(
int $timestamp,
?int $near = DAYSECS,
bool $timeonly = false,
?url $link = null,
?string $langtimeformat = null,
bool $userelatives = true,
): self {
return self::create_from_datetime(
(new DateTimeImmutable("@{$timestamp}")),
$near,
$timeonly,
$link,
$langtimeformat,
$userelatives
);
}
/**
* Creates a new humandate instance from a datetime.
*
* @param DateTimeInterface $datetime The datetime.
* @param int|null $near The number of seconds within which a date is considered near. 1 day by default.
* @param bool $timeonly Whether we should show time only or date and time.
* @param url|null $link Link for the date.
* @param string|null $langtimeformat An optional date format to apply.
* @param bool $userelatives Whether to use human relative terminology.
* @return humandate The new instance.
*/
public static function create_from_datetime(
DateTimeInterface $datetime,
?int $near = DAYSECS,
bool $timeonly = false,
?url $link = null,
?string $langtimeformat = null,
bool $userelatives = true,
): self {
if (!($datetime instanceof DateTimeImmutable)) {
// Always use an Immutable object to ensure that the value does not change externally before it is rendered.
$datetime = DateTimeImmutable::createFromInterface($datetime);
}
return (new self($datetime))
->set_near_limit($near)
->set_display_time_only($timeonly)
->set_link($link)
->set_lang_time_format($langtimeformat)
->set_use_relatives($userelatives);
}
/**
* Sets the number of seconds within which a date is considered near.
*
* @param int|null $near The number of seconds within which a date is considered near.
* @return humandate The instance.
*/
public function set_near_limit(?int $near): self {
$this->near = $near;
return $this;
}
/**
* Sets whether we should show time only or date and time.
*
* @param bool $timeonly Whether we should show time only or date and time.
* @return humandate The instance.
*/
public function set_display_time_only(bool $timeonly): self {
$this->timeonly = $timeonly;
return $this;
}
/**
* Sets the link for the date. If null, no link will be added.
*
* @param url|null $link The link for the date.
* @return humandate The instance.
*/
public function set_link(?url $link): self {
$this->link = $link;
return $this;
}
/**
* Sets an optional date format to apply.
*
* @param string|null $langtimeformat Lang date and time format to use to format the date.
* @return humandate The instance.
*/
public function set_lang_time_format(?string $langtimeformat): self {
$this->langtimeformat = $langtimeformat;
return $this;
}
/**
* Sets whether to use human relative terminology.
*
* @param bool $userelatives Whether to use human relative terminology.
* @return humandate The instance.
*/
public function set_use_relatives(bool $userelatives): self {
$this->userelatives = $userelatives;
return $this;
}
#[\Override]
public function export_for_template(renderer_base $output): array {
$timestamp = $this->datetime->getTimestamp();
$userdate = userdate($timestamp, get_string('strftimedayshort'));
$relative = null;
if ($this->userelatives) {
$relative = $this->format_relative_date();
}
if ($this->timeonly) {
$date = null;
} else {
$date = $relative ?? $userdate;
}
$data = [
'timestamp' => $timestamp,
'userdate' => $userdate,
'date' => $date,
'time' => $this->format_time(),
'ispast' => $this->datetime < $this->clock->now(),
'needtitle' => ($relative !== null || $this->timeonly),
'link' => $this->link ? $this->link->out(false) : '',
];
if ($this->is_near()) {
$icon = new pix_icon(
pix: 'i/warning',
alt: get_string('warning'),
component: 'moodle',
attributes: ['class' => 'me-0 pb-1']
);
$data['isnear'] = true;
$data['nearicon'] = $icon->export_for_template($output);
}
return $data;
}
/**
* Checks if the date is near.
*
* @return bool Whether the date is near.
*/
private function is_near(): bool {
if ($this->near === null) {
return false;
}
$due = $this->datetime->diff($this->clock->now());
$intervalseconds = $this->interval_to_seconds($due);
return $intervalseconds < $this->near && $intervalseconds > 0;
}
/**
* Converts a DateInterval object to total seconds.
*
* @param \DateInterval $interval The interval to convert.
* @return int The total number of seconds.
*/
private function interval_to_seconds(DateInterval $interval): int {
$reference = new DateTimeImmutable();
$entime = $reference->add($interval);
return $reference->getTimestamp() - $entime->getTimestamp();
}
/**
* Formats the timestamp as a relative date string (e.g., "Today", "Yesterday", "Tomorrow").
*
* This method compares the given timestamp with the current date and returns a formatted
* string representing the relative date. If the timestamp corresponds to today, yesterday,
* or tomorrow, it returns the appropriate string. Otherwise, it returns null.
*
* @return string|null
*/
private function format_relative_date(): ?string {
$usertimestamp = $this->get_user_date($this->datetime->getTimestamp());
if ($usertimestamp == $this->get_user_date($this->clock->now()->getTimestamp())) {
$format = get_string('strftimerelativetoday', 'langconfig');
} else if ($usertimestamp == $this->get_user_date(strtotime('yesterday', $this->clock->now()->getTimestamp()))) {
$format = get_string('strftimerelativeyesterday', 'langconfig');
} else if ($usertimestamp == $this->get_user_date(strtotime('tomorrow', $this->clock->now()->getTimestamp()))) {
$format = get_string('strftimerelativetomorrow', 'langconfig');
} else {
return null;
}
return userdate($this->datetime->getTimestamp(), $format);
}
/**
* Formats the timestamp as a human readable time.
*
* @param int $timestamp The timestamp to format.
* @param string $format The format to use.
* @return string The formatted date.
*/
private function get_user_date(int $timestamp, string $format = '%Y-%m-%d'): string {
$calendartype = \core_calendar\type_factory::get_calendar_instance();
$timezone = \core_date::get_user_timezone_object();
return $calendartype->timestamp_to_date_string(
time: $timestamp,
format: $format,
timezone: $timezone->getName(),
fixday: true,
fixhour: true,
);
}
/**
* Formats the timestamp as a human readable time.
*
* This method compares the given timestamp with the current date and returns a formatted
* string representing the time.
*
* @return string
*/
private function format_time(): string {
global $CFG;
// Ensure calendar constants are loaded.
require_once($CFG->dirroot . '/calendar/lib.php');
$timeformat = get_user_preferences('calendar_timeformat');
if (empty($timeformat)) {
$timeformat = get_config(null, 'calendar_site_timeformat');
}
// Allow language customization of selected time format.
if ($timeformat === CALENDAR_TF_12) {
$timeformat = get_string('strftimetime12', 'langconfig');
} else if ($timeformat === CALENDAR_TF_24) {
$timeformat = get_string('strftimetime24', 'langconfig');
}
if ($timeformat) {
return userdate($this->datetime->getTimestamp(), $timeformat);
}
// Let's use default format.
if ($this->langtimeformat === null) {
$langtimeformat = get_string('strftimetime');
}
return userdate($this->datetime->getTimestamp(), $langtimeformat);
}
}
+235
View File
@@ -0,0 +1,235 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_calendar\output;
Use DateTimeInterface;
use DateTimeImmutable;
use core\output\templatable;
use core\output\renderable;
use core\output\renderer_base;
use core\url;
/**
* Class humantimeperiod.
*
* This class is used to render a time period as a human readable date.
* The main difference between userdate and this class is that this class
* will render the date as "Today", "Yesterday", "Tomorrow" if the date is
* close to the current date. Also, it will add styling if the date
* is near.
*
* @package core_calendar
* @copyright 2025 Amaia Anabitarte <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class humantimeperiod implements renderable, templatable {
/** @var int|null Number of seconds that indicates a nearby date. Default to DAYSECS. Use null for no indication. */
protected $near = DAYSECS;
/** @var url|null URL to link the date to. */
protected ?url $link = null;
/** @var string|null An optional date format to apply. */
protected ?string $langtimeformat = null;
/** @var bool Whether to use human common words or not. */
protected bool $userelatives = true;
/**
* Class constructor.
*
* @param DateTimeImmutable $startdatetime The starting timestamp.
* @param DateTimeImmutable|null $enddatetime The ending timestamp.
*/
protected function __construct(
/** @var DateTimeImmutable $startdatetime The starting date time. */
protected DateTimeImmutable $startdatetime,
/** @var DateTimeImmutable|null $enddatetime The ending date time. */
protected ?DateTimeImmutable $enddatetime,
) {
}
/**
* Creates a new humantimeperiod instance from a timestamp.
*
* @param int $starttimestamp The starting timestamp.
* @param int|null $endtimestamp The ending timestamp.
* @param int|null $near The number of seconds that indicates a nearby date. Default to DAYSECS, use null for no indication.
* @param url|null $link URL to link the date to.
* @param string|null $langtimeformat Lang date and time format to use to format the date.
* @param bool $userelatives Whether to use human common words or not.
* @return humantimeperiod The new instance.
*/
public static function create_from_timestamp(
int $starttimestamp,
?int $endtimestamp,
?int $near = DAYSECS,
?url $link = null,
?string $langtimeformat = null,
bool $userelatives = true,
): self {
return self::create_from_datetime(
(new DateTimeImmutable("@{$starttimestamp}")),
$endtimestamp ? (new DateTimeImmutable("@{$endtimestamp}")) : null,
$near,
$link,
$langtimeformat,
$userelatives
);
}
/**
* Creates a new humantimeperiod instance from a datetime.
*
* @param DateTimeInterface $startdatetime The starting datetime.
* @param DateTimeInterface|null $enddatetime The ending datetime.
* @param int|null $near The number of seconds that indicates a nearby date. Default to DAYSECS, use null for no indication.
* @param url|null $link URL to link the date to.
* @param string|null $langtimeformat Lang date and time format to use to format the date.
* @param bool $userelatives Whether to use human common words or not.
* @return humantimeperiod The new instance.
*/
public static function create_from_datetime(
DateTimeInterface $startdatetime,
?DateTimeInterface $enddatetime,
?int $near = DAYSECS,
?url $link = null,
?string $langtimeformat = null,
bool $userelatives = true,
): self {
// Always use an Immutable object to ensure that the value does not change externally before it is rendered.
if (!($startdatetime instanceof DateTimeImmutable)) {
$startdatetime = DateTimeImmutable::createFromInterface($startdatetime);
}
if ($enddatetime != null && !($enddatetime instanceof DateTimeImmutable)) {
$enddatetime = DateTimeImmutable::createFromInterface($enddatetime);
}
return (new self($startdatetime, $enddatetime))
->set_near_limit($near)
->set_link($link)
->set_lang_time_format($langtimeformat)
->set_use_relatives($userelatives);
}
/**
* Sets the number of seconds within which a date is considered near.
*
* @param int|null $near The number of seconds within which a date is considered near.
* @return humantimeperiod The instance.
*/
public function set_near_limit(?int $near): self {
$this->near = $near;
return $this;
}
/**
* Sets the link for the date. If null, no link will be added.
*
* @param url|null $link The link for the date.
* @return humantimeperiod The instance.
*/
public function set_link(?url $link): self {
$this->link = $link;
return $this;
}
/**
* Sets an optional date format to apply.
*
* @param string|null $langtimeformat Lang date and time format to use to format the date.
* @return humantimeperiod The instance.
*/
public function set_lang_time_format(?string $langtimeformat): self {
$this->langtimeformat = $langtimeformat;
return $this;
}
/**
* Sets whether to use human relative terminology.
*
* @param bool $userelatives Whether to use human relative terminology.
* @return humantimeperiod The instance.
*/
public function set_use_relatives(bool $userelatives): self {
$this->userelatives = $userelatives;
return $this;
}
#[\Override]
public function export_for_template(renderer_base $output): array {
$period = $this->format_period();
return [
'startdate' => $period['startdate']->export_for_template($output),
'enddate' => $period['enddate'] ? $period['enddate']->export_for_template($output) : null,
];
}
/**
* Format a time periods based on 2 dates.
*
* @return array An array of one or two humandate elements.
*/
private function format_period(): array {
$linkstart = null;
$linkend = null;
if ($this->link) {
$linkstart = new url($this->link, ['view' => 'day', 'time' => $this->startdatetime->getTimestamp()]);
$linkend = new url($this->link, ['view' => 'day', 'time' => $this->enddatetime->getTimestamp()]);
}
$startdate = humandate::create_from_datetime(
datetime: $this->startdatetime,
near: $this->near,
link: $linkstart,
langtimeformat: $this->langtimeformat,
userelatives: $this->userelatives
);
if ($this->enddatetime == null || $this->startdatetime == $this->enddatetime) {
return [
'startdate' => $startdate,
'enddate' => null,
];
}
// Get the midnight of the day the event will start.
$usermidnightstart = usergetmidnight($this->startdatetime->getTimestamp());
// Get the midnight of the day the event will end.
$usermidnightend = usergetmidnight($this->enddatetime->getTimestamp());
// Check if we will still be on the same day.
$issameday = ($usermidnightstart == $usermidnightend);
$enddate = humandate::create_from_datetime(
datetime: $this->enddatetime,
near: $this->near,
timeonly: $issameday,
link: $linkend,
langtimeformat: $this->langtimeformat,
userelatives: $this->userelatives
);
return [
'startdate' => $startdate,
'enddate' => $enddate,
];
}
}
+56
View File
@@ -0,0 +1,56 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core_calendar/humandate
Print a human readable date format.
Example context (json):
{
"userdate": "2021-09-01",
"date": "Tomorrow, 23 January",
"time": "12:00 AM",
"timestamp": "1630512000",
"ispast": false,
"isnear": true,
"nearicon": {
"extraclasses": "me-0 pb-1",
"attributes": [
{"name": "src", "value": "../../../pix/i/warning.svg"},
{"name": "alt", "value": "Warning"}
]
},
"needtitle": true,
"link": "https://example.com/calendar/view.php"
}
}}
{{#link}}
<a href="{{link}}" title="{{userdate}}">
{{/link}}
<span
class="date {{#ispast}}dimmed_text{{/ispast}} {{#isnear}}text-danger{{/isnear}}"
data-timestamp="{{timestamp}}"
{{#needtitle}} title="{{userdate}}" {{/needtitle}}
>
{{#nearicon}}
{{>core/pix_icon}}
{{/nearicon}}
{{#date}}{{date}}{{#time}}, {{/time}}{{/date}}{{time}}
</span>
{{#link}}
</a>
{{/link}}
@@ -0,0 +1,62 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core_calendar/humantimeperiod
Print a human readable time period.
Example context (json):
{
"startdate": {
"userdate": "2021-09-01",
"date": "Tomorrow 12:00 AM",
"timestamp": "1630512000",
"ispast": false,
"isnear": true,
"nearicon": {
"extraclasses": "me-0 pb-1",
"attributes": [
{"name": "src", "value": "../../../pix/i/warning.svg"},
{"name": "alt", "value": "Warning"}
]
},
"needtitle": true
},
"enddate": {
"userdate": "2021-09-01",
"date": "Tomorrow 12:00 AM",
"timestamp": "1630512000",
"ispast": false,
"isnear": true,
"nearicon": {
"extraclasses": "me-0 pb-1",
"attributes": [
{"name": "src", "value": "../../../pix/i/warning.svg"},
{"name": "alt", "value": "Warning"}
]
},
"needtitle": true
}
}
}}
{{#startdate}}
{{> core_calendar/humandate }}
{{/startdate}}
{{#enddate}} &raquo;
{{> core_calendar/humandate }}
{{/enddate}}
+62
View File
@@ -0,0 +1,62 @@
@core @core_calendar
Feature: Confirm dates are human readable
In order to ensure the calendar dates are human readable
As an admin
I need to create calendar events in near days
Background:
And the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | topics |
And the following "blocks" exist:
| blockname | contextlevel | reference | pagetypepattern | defaultregion |
| calendar_upcoming | System | 1 | my-index | content |
@javascript
Scenario: Create human readable events
Given I log in as "admin"
And the following "events" exist:
| name | eventtype | timestart | timeduration |
# Starts today and ends today in the future
| This day one hour event | user | ##today midnight +1 seconds## | 86398 |
# Starts today, ends tomorrow
| This day and 1 day event | user | ##today midnight +1 seconds## | 87000 |
# Starts yesterday, ends today in the future
| Last day one day event | user | ##today noon -1 days## | 129598 |
# Starts yesterday, ends in the past
| Last day less than one day event | user | ##today noon -1 days## | 86400 |
# Starts tomorrow
| Next day event | user | ##today midnight +1 days## | 86400 |
# Far in the future
| Future event | user | ##today noon +2 days## | 86400 |
When I am on homepage
And I click on "This day one hour event" "link"
Then I should see "Today" in the "This day one hour event" "dialogue"
And I should not see "Yesterday" in the "This day one hour event" "dialogue"
And I should not see "Tomorrow" in the "This day one hour event" "dialogue"
And "Warning" "icon" should exist in the "This day one hour event" "dialogue"
And I click on "Close" "button" in the "This day one hour event" "dialogue"
And I click on "This day and 1 day event" "link"
And I should see "Today" in the "This day and 1 day event" "dialogue"
And I should not see "Yesterday" in the "This day and 1 day event" "dialogue"
And I should see "Tomorrow" in the "This day and 1 day event" "dialogue"
And "Warning" "icon" should exist in the "This day and 1 day event" "dialogue"
And I click on "Close" "button" in the "This day and 1 day event" "dialogue"
And I click on "Last day one day event" "link"
And I should see "Today" in the "Last day one day event" "dialogue"
And I should see "Yesterday" in the "Last day one day event" "dialogue"
And I should not see "Tomorrow" in the "Last day one day event" "dialogue"
And "Warning" "icon" should exist in the "Last day one day event" "dialogue"
And I click on "Close" "button" in the "Last day one day event" "dialogue"
And I click on "Next day event" "link"
And I should not see "Today" in the "Next day event" "dialogue"
And I should not see "Yesterday" in the "Next day event" "dialogue"
And I should see "Tomorrow" in the "Next day event" "dialogue"
And "Warning" "icon" should exist in the "Next day event" "dialogue"
And I click on "Close" "button" in the "Next day event" "dialogue"
And I click on "Future event" "link"
And I should not see "Today" in the "Future event" "dialogue"
And I should not see "Yesterday" in the "Future event" "dialogue"
And I should not see "Tomorrow" in the "Future event" "dialogue"
And "Warning" "icon" should not exist in the "Future event" "dialogue"
And I click on "Close" "button" in the "Future event" "dialogue"
+210
View File
@@ -0,0 +1,210 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_calendar\output;
use DateTime;
/**
* Tests for humandate class.
*
* @covers \core_calendar\output\humandate
* @package core_calendar
* @category test
* @copyright 2025 Sara Arjona <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
final class humandate_test extends \advanced_testcase {
/**
* Test export_for_template() method.
*
* @dataProvider provider_export_for_template
* @param int $addseconds The number of seconds to add to the current time.
* @param bool $userelatives Whether to use relative dates.
* @param string|null $date For relative dates, the expected string (Tomorrow, Today, Yesterday).
* @param bool $ispast Whether the date is in the past.
* @param bool $needtitle Whether the date needs a title.
* @param bool $isnear Whether the date is near.
*/
public function test_export_for_template(
int $addseconds,
bool $userelatives,
?string $date,
bool $ispast,
bool $needtitle,
bool $isnear,
): void {
global $PAGE;
$this->resetAfterTest();
$clock = $this->mock_clock_with_frozen();
$renderer = $PAGE->get_renderer('core');
$timestamp = $clock->time() + $addseconds;
$expected = [
'timestamp' => $timestamp,
'date' => $date,
'userdate' => userdate($timestamp, get_string('strftimedayshort')),
'ispast' => $ispast,
'needtitle' => $needtitle,
'isnear' => $isnear,
];
$humandate = humandate::create_from_timestamp($timestamp);
$humandate->set_use_relatives($userelatives);
$result = $humandate->export_for_template($renderer);
$this->compare_output($expected, $result, $userelatives);
}
/**
* Data provider.
*
* @return array
*/
public static function provider_export_for_template(): array {
return [
'Now with relatives' => [
'addseconds' => 0,
'userelatives' => true,
'date' => 'Today',
'ispast' => true,
'needtitle' => true,
'isnear' => false,
],
'Tomorrow with relatives' => [
'addseconds' => 87000,
'userelatives' => true,
'date' => 'Tomorrow',
'ispast' => false,
'needtitle' => true,
'isnear' => false,
],
'Yesterday with relatives' => [
'addseconds' => -87000,
'userelatives' => true,
'date' => 'Yesterday',
'ispast' => true,
'needtitle' => true,
'isnear' => false,
],
'One hour future with relatives' => [
'addseconds' => 3600,
'userelatives' => true,
'date' => null,
'ispast' => false,
'needtitle' => true,
'isnear' => true,
],
'One hour past with relatives' => [
'addseconds' => -3600,
'userelatives' => true,
'date' => null,
'ispast' => true,
'needtitle' => true,
'isnear' => false,
],
'Now without relatives' => [
'addseconds' => 0,
'userelatives' => false,
'date' => 'Today',
'ispast' => true,
'needtitle' => false,
'isnear' => false,
],
'Tomorrow without relatives' => [
'addseconds' => 87000,
'userelatives' => false,
'date' => 'Tomorrow',
'ispast' => false,
'needtitle' => false,
'isnear' => false,
],
'Yesterday without relatives' => [
'addseconds' => -87000,
'userelatives' => false,
'date' => 'Yesterday',
'ispast' => true,
'needtitle' => false,
'isnear' => false,
],
'One hour future without relatives' => [
'addseconds' => 3600,
'userelatives' => false,
'date' => null,
'ispast' => false,
'needtitle' => false,
'isnear' => true,
],
'One hour past without relatives' => [
'addseconds' => -3600,
'userelatives' => false,
'date' => null,
'ispast' => true,
'needtitle' => false,
'isnear' => false,
],
];
}
public function test_create_from_timestamp(): void {
$this->resetAfterTest();
$clock = $this->mock_clock_with_frozen();
$timestamp = $clock->time();
$humandate = humandate::create_from_timestamp($timestamp);
$this->assertInstanceOf(humandate::class, $humandate);
}
public function test_create_from_datetime(): void {
$this->resetAfterTest();
$humandate = humandate::create_from_datetime(new DateTime());
$this->assertInstanceOf(humandate::class, $humandate);
}
/**
* Compare humandate output.
*
* @param array $expected The expected output.
* @param array $actual The actual output.
* @param bool $userelatives Whether to use relative dates.
*/
protected function compare_output(
array $expected,
array $actual,
bool $userelatives,
): void {
$fields = ['timestamp', 'userdate', 'ispast', 'needtitle'];
foreach ($fields as $field) {
$this->assertEquals($expected[$field], $actual[$field], "Field $field does not match");
}
if ($expected['isnear']) {
$this->assertEquals($expected[$field], $actual[$field], "Field isnear does not match");
} else {
$this->assertArrayNotHasKey('isnear', $actual);
}
if (!is_null($expected['date'])) {
if ($userelatives) {
$this->assertStringContainsString($expected['date'], $actual['date']);
} else {
$this->assertStringNotContainsString($expected['date'], $actual['date']);
}
}
}
}
@@ -0,0 +1,166 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_calendar\output;
use DateTime;
/**
* Tests for humantimeperiod_test class.
*
* @covers \core_calendar\output\humantimeperiod
* @package core_calendar
* @category test
* @copyright 2025 Sara Arjona <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
final class humantimeperiod_test extends \advanced_testcase {
/**
* Test format_period() method.
*
* @dataProvider provider_format_period
* @param int|null $addsecondsend The number of seconds to add to the current time for the end date.
* @param bool $expectedendnull Whether the end date is null.
*/
public function test_format_period(
?int $addsecondsend,
bool $expectedendnull,
): void {
$this->resetAfterTest();
// 26 February 2025 15:30:00 (GMT).
$clock = $this->mock_clock_with_frozen(1740583800);
$timestampstart = $clock->time();
$timestampend = is_null($addsecondsend) ? null : $clock->time() + $addsecondsend;
$humandateend = null;
if (!$expectedendnull) {
$humandateend = humandate::create_from_timestamp(
timestamp: $timestampend,
timeonly: (abs($addsecondsend) < 1800),
);
}
$expected = [
'startdate' => humandate::create_from_timestamp($timestampstart),
'enddate' => $humandateend,
];
$humantimeperiod = humantimeperiod::create_from_timestamp($timestampstart, $timestampend);
$reflection = new \ReflectionClass($humantimeperiod);
$method = $reflection->getMethod('format_period');
$method->setAccessible(true);
$result = $method->invoke($humantimeperiod);
$this->compare_output($expected, $result);
}
/**
* Data provider.
*
* @return array
*/
public static function provider_format_period(): array {
return [
'Same start and end' => [
'addsecondsend' => 0,
'expectedendnull' => true,
],
'Null end date' => [
'addsecondsend' => null,
'expectedendnull' => true,
],
'1 day ahead' => [
'addsecondsend' => 87000,
'expectedendnull' => false,
],
'1 day behind' => [
'addsecondsend' => -87000,
'expectedendnull' => false,
],
'29 minutes ahead' => [
'addsecondsend' => 1799,
'expectedendnull' => false,
],
'30 minutes ahead (Midnight)' => [
'addsecondsend' => 1800,
'expectedendnull' => false,
],
'31 minutes ahead (Midnight)' => [
'addsecondsend' => 1801,
'expectedendnull' => false,
],
];
}
/**
* Test create_from_timestamp.
*/
public function test_create_from_timestamp(): void {
$this->resetAfterTest();
$clock = \core\di::get(\core\clock::class);
$timestamp = $clock->time();
$humantimeperiod = humantimeperiod::create_from_timestamp($timestamp, $timestamp + 3600);
$this->assertInstanceOf(humantimeperiod::class, $humantimeperiod);
}
/**
* Test create_from_timestamp with enddatetime null.
*/
public function test_create_from_timestamp_endnull(): void {
$this->resetAfterTest();
$clock = $this->mock_clock_with_frozen();
$timestamp = $clock->time();
$humantimeperiod = humantimeperiod::create_from_timestamp($timestamp, null);
$this->assertInstanceOf(humantimeperiod::class, $humantimeperiod);
}
/**
* Test create_from_datetime.
*/
public function test_create_from_datetime(): void {
$this->resetAfterTest();
$humantimeperiod = humantimeperiod::create_from_datetime(new DateTime(), new DateTime('+1 hour'));
$this->assertInstanceOf(humantimeperiod::class, $humantimeperiod);
}
/**
* Test create_from_datetime with enddatetime null.
*/
public function test_create_from_datetime_endnull(): void {
$this->resetAfterTest();
$humantimeperiod = humantimeperiod::create_from_datetime(new DateTime(), null);
$this->assertInstanceOf(humantimeperiod::class, $humantimeperiod);
}
/**
* Compare humantimeperiod output.
*
* @param array $expected The expected output.
* @param array $actual The actual output.
*/
protected function compare_output(
array $expected,
array $actual,
): void {
$fields = ['startdate', 'enddate'];
foreach ($fields as $field) {
$this->assertEquals($expected[$field], $actual[$field], "Field $field does not match");
}
}
}
+3
View File
@@ -189,6 +189,9 @@ $string['strftimemonth'] = '%B';
$string['strftimemonthyear'] = '%B %Y';
$string['strftimerecent'] = '%d %b, %H:%M';
$string['strftimerecentfull'] = '%a, %d %b %Y, %I:%M %p';
$string['strftimerelativetoday'] = 'Today, %d %B';
$string['strftimerelativetomorrow'] = 'Tomorrow, %d %B';
$string['strftimerelativeyesterday'] = 'Yesterday, %d %B';
$string['strftimetime'] = '%I:%M %p';
$string['strftimetime12'] = '%I:%M %p';
$string['strftimetime24'] = '%H:%M';