MDL-83873 core_calendar: New human date format
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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}} »
|
||||
{{> core_calendar/humandate }}
|
||||
{{/enddate}}
|
||||
@@ -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"
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user