grav
grav copied to clipboard
Can't use custom date header
Hi!
I found a problem.
If I have date: 09.02.2017
in blog post header and this code:
content:
items: @self.children
order:
by: date
dir: desc
than my sorting works correctly. {{ dump(post.header.date) }}
shows me timestamp date.
But if I have custom date header like releaseDate
and order by this header, then sorting breaks down. For example, 01.09.2012
is older then 09.02.2017
. {{ dump(post.header.releaseDate) }}
shows me string date as is.
Later @flaviocopes told me to use Y.m.d
format (2017.02.09
for example). This temporarily resolved my problem until I want to output the date in the post.
I put {{ dump(page.header.releaseDate|date('d')) }}
line in twig and see an error:
An exception has been thrown during the rendering of a template ("DateTime::__construct():
Failed to parse time string (2017.02.09) at position 5 (0): Double time specification").
How to fix?
p.s. Same problem in the forum: https://discourse.getgrav.org/t/date-format/1555/8
This is actually to be expected. When using a custom field to order by, Grav really has no clue what type of field that is, so it's not smart enough to convert it do a date and compare it that way. One way to fix this would be to store the custom date field as a timestamp.
@rhukster Maybe you can add strtotime
Twig filter into Grav? This will solve problems with converting custom dates to timestamps.
@rhukster
One way to fix this would be to store the custom date field as a timestamp.
It's uncomfortable for reading or changing such date by a human.
I see two ways to solve this problem:
- Add
strtotime
filter as @dimayakovlev mentioned. - Add additional option to specify field data type for Grav.
releaseDate(date): 09.02.2017
, for example. An indication of the data type in parentheses.
You can already convert a date to a time stamp.
https://stackoverflow.com/questions/9242072/how-can-i-convert-a-date-to-a-unix-timestamp-with-twig
@sogl '2017.02.09'|date('U')
- this causes an error, but if you change dots to dashes all works fine. So you can use this code to convert your custom date to timestamp: '2017.02.09'|replace('.', '-')|date('U')
.
@rhukster In latest Grav 1.3.8 I can use this code {{ dump(page.header.myDate|date('d')) }}
successfully without errors.
But I'm still can't order collections by custom date header.
I found a workaround:
- Create a new file in theme:
themename/class/CollectionSorter.php
<?php
namespace Grav\Theme;
use Grav\Common\Grav;
class CollectionSorter
{
protected $grav;
protected $field;
protected $asc;
public function __construct() {
$this->grav = Grav::instance();
}
public function byDate($collection, $field, $asc = true)
{
$this->field = $field;
$this->asc = $asc;
$array = [];
foreach($collection as $p) {
$array[] = $p;
}
usort($array, function($a, $b) {
if (!isset($a->header()->{$this->field}, $b->header()->{$this->field})) {
return 0;
}
$valA = $a->header()->{$this->field};
$valB = $b->header()->{$this->field};
if ($valA == $valB) {
return 0;
}
if ($this->asc) {
return strtotime($valA) - strtotime($valB);
}
return strtotime($valB) - strtotime($valA);
});
return $array;
}
}
- Add this to
themename.php
file:
public function onTwigSiteVariables()
{
require_once __DIR__ . '/class/CollectionSorter.php';
$this->grav['twig']->twig_vars['sorter'] = new CollectionSorter();
}
- Use in twig:
{% set asc_order = sorter.byDate(page.collection, 'custom_date_field') %}
{% set desc_order = sorter.byDate(page.collection, 'custom_date_field', false) %}
But what if I want to handle things like dateRange
, limit
etc... do everything in custom PHP functions?
Devs please @rhukster @w00fz @OleVik @flaviocopes @mahagr
I returned to my project and still can't sort custom header dates with built-in functional 😢
I saw much similar issues: https://github.com/getgrav/grav/issues/1368 https://github.com/getgrav/grav/issues/1640 https://github.com/getgrav/grav/issues/1764 https://github.com/getgrav/grav/issues/1199
For now.
Just tried to use frontmatter and twig SORT_NUMERIC
hack together to show upcoming tours.
I can't set SORT_NUMERIC
straight into the frontmatter because of error:
An exception has been thrown during the rendering of a template
("asort() expects parameter 2 to be long, string given").
in items:
//tour 1
returnDate: '31-08-2018 23:01'
//tour 2
returnDate: '30-07-2018 23:01'
//tour 3
returnDate: '01-08-2018 23:01'
In frontmatter:
nearest:
items:
'@page.children': '/tours'
dateRange:
start: today
field: header.returnDate
in twig:
{% set nearest_collection = page.collection('nearest')
.order('header.returnDate', 'desc', null, 1) %}
{% for tour in nearest_collection %}
{{ dump(tour) }}
{{ dump(tour.header.returnDate) }}
{% endfor %}
Tried to change 1
to SORT_NUMERIC
.
No way:
31-08-2018 23:01
30-07-2018 23:01
01-08-2018 23:01
Also tried to disable this thing:
Maybe any of you @inktrap @erichgoldman @tboulogne @akoebbe @vitopepito
have found a solution?
@Sogl I just tried this out in the devlop
branch and it does work for me. I was using the format of 04/01/2010
(mm/dd/yyyy). Can you see if changing your date format makes a difference? I'm sorry I don't have more time to try different variations. Here's what I have...
Custom item headers (note the years):
#item 1
custom_date: '01/01/2010'
#item 2
custom_date: '02/01/2010'
#item 3
custom_date: '03/01/2019'
#item 4
custom_date: '04/01/2019'
Collection definition (used a blog page for this test):
content:
items: '@self.children'
order:
by: header.custom_date
dir: desc
dateRange:
start: today
field: header.custom_date
pagination: true
url_taxonomy_filters: true
With this set up I only see two items on the page and they are sorted as expected.
@akoebbe Try different dates and months, not only years. Looks like Grav uses string comparsion for custom fields. For example:
#item 1
custom_date: '02/01/2010'
#item 2
custom_date: '01/02/2010'
#item 3
custom_date: '11/10/2019'
#item 4
custom_date: '12/08/2019'
It should be:
'02/01/2010'
'01/02/2010'
'12/08/2019'
'11/10/2019'
Wrong (my situation):
'01/02/2010'
'02/01/2010'
'11/10/2019'
'12/08/2019'
So are you saying you're using dd/mm/yyyy
? Your "Wrong" situation seems like it's sorting mm/dd/yyyy
. Is that correct?
I tried only with dashes and also with dots (see 1st msg here).
I said this for your example (just change days and months differently) because Grav sorts only by days in my situation if header is not date
.
Ok. I see what you're saying. Here is a more clear example...
#item 1
custom_date: '01/01/2010'
#item 2
custom_date: '03/01/2010'
#item 3
custom_date: '02/01/2020'
#item 4
custom_date: '04/01/2019'
is sorted...
'01/01/2010'
'02/01/2020'
'03/01/2010'
'04/01/2019'
So the question is can you use an ISO9601 format instead yyyy-mm-dd hh:mm:ss
since that is string sortable? Granted I get that there might still be a point of debate on casting certain strings as dates.
I also wonder if it would be reasonable to pull a blueprint field definition in to know how to cast the value... OR add a property value_type
to the order
section of content
...
content:
items: '@self.children'
order:
by: header.custom_date
value_type: date
dir: desc
I just did some testing and ISO 9601 formatted dates work with both sorting and range limits using the dates my last example above.
ISO8601 works fine if I set system conf value:
But have 3 problems:
- Need to re-save all pages/items. In Admin panel you can see new format, but in files it still old and compares in old format before saving.
- Not a beautiful date look for site editors.
- It's not known what problems then with such "dates" can be in future.
This is not a solution, but a hack 😄
I'm open to hearing what other devs think about either option I mentioned above for a real solution. Perhaps there are other options?
Date fields on the page frontmatter are strings and as such, they are always ordered as strings, not dates. There is really no way to do anything about it as pages aren't aware of field content type, which makes the proper comparison not possible.
I am personally aware of this issue and we have plans to replace current page logic with something better in upcoming versions (= 2.0).
BTW: be careful when using dashes in the dates: it's in American format: mm/dd/yyyy
. For European dates you need to use dd.mm.yyyy
instead. That said, you're right: neither of those format order properly as strings and you need to use yyyy-mm-dd
instead.
@mahagr could we not tap into the blueprint to inform sorting?
Blueprints are not used really outside of CRUD (admin).
For great ordering you can add option "format" and set like this
header.event.start_datetime: type: datetime label: Start date & time format: 'Y-m-d H:i'
I'm confused as to is there a solution to this problem? I'm in the exact same situation. Building a page for a customer and stuck on this.
As I said, there is a general solution in works and it will be part of Grav 2.0.
In the meantime, the only way to fix this would be to add an attribute to the ordering which tells that the field is a date and should be ordered after converting it to DateTIme
object or by using strtotime()
as it was suggested before.
Thanks. I ended up using Sogl's solutions and his CollectionSorter class...
I modified Sogl's solution a little to be compatible with the https://github.com/getgrav/grav-plugin-pagination Plugin, I know it's a bit dirty recreating the collection, but I did not found any better solution without adding a "setItmes" method to the Collection class:
`<?php namespace Grav\Theme;
use Grav\Common\Grav; use Grav\Common\Page\Collection;
class CollectionSorter { protected $grav; protected $field; protected $asc;
public function __construct() { $this->grav = Grav::instance(); }
public function byDate($collection, $field, $asc = true) { $this->field = $field; $this->asc = $asc;
$array = [];
foreach($collection as $p) {
$array[] = $p;
}
usort($array, function($a, $b) {
if (!isset($a->header()->{$this->field}, $b->header()->{$this->field})) {
return 0;
}
$valA = $a->header()->{$this->field};
$valB = $b->header()->{$this->field};
if ($valA == $valB) {
return 0;
}
if ($this->asc) {
return strtotime($valA) - strtotime($valB);
}
return strtotime($valB) - strtotime($valA);
});
$items = array();
foreach ($array as $delta => $page) {
$items[$page->path()] = array('slug' => $page->slug());
}
return new Collection($items);
} }`
The rest remains as described in https://github.com/getgrav/grav/issues/1641#issuecomment-344122465
In combination with pagination:
In my usecase I do have a custom collection with a seperate limit field, configurable by the user which I pass as second parameter to paginate
{% set collection = sorter.byDate(collection, 'event_date', false) %} {% do paginate(collection, page.header.custom_collection.limit) %}
Out of interest @Hydraner — where would I start in terms of a php fix for this, I wasn't sure where your php code would go? Presumably here? themename/class/CollectionSorter.php