grav icon indicating copy to clipboard operation
grav copied to clipboard

Can't use custom date header

Open Sogl opened this issue 7 years ago • 25 comments

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

Sogl avatar Sep 05 '17 23:09 Sogl

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 avatar Sep 06 '17 03:09 rhukster

@rhukster Maybe you can add strtotime Twig filter into Grav? This will solve problems with converting custom dates to timestamps.

dimayakovlev avatar Sep 06 '17 04:09 dimayakovlev

@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:

  1. Add strtotime filter as @dimayakovlev mentioned.
  2. 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.

Sogl avatar Sep 07 '17 03:09 Sogl

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

rhukster avatar Sep 07 '17 03:09 rhukster

@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').

dimayakovlev avatar Sep 08 '17 15:09 dimayakovlev

@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:

  1. 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;
    }
}
  1. Add this to themename.php file:
public function onTwigSiteVariables()
{
    require_once __DIR__ . '/class/CollectionSorter.php';
    $this->grav['twig']->twig_vars['sorter'] = new CollectionSorter();
}
  1. 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?

Sogl avatar Nov 14 '17 02:11 Sogl

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: image

Maybe any of you @inktrap @erichgoldman @tboulogne @akoebbe @vitopepito
have found a solution?

Sogl avatar Jul 26 '18 01:07 Sogl

@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 avatar Jul 26 '18 02:07 akoebbe

@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'

Sogl avatar Jul 26 '18 03:07 Sogl

So are you saying you're using dd/mm/yyyy? Your "Wrong" situation seems like it's sorting mm/dd/yyyy. Is that correct?

akoebbe avatar Jul 26 '18 03:07 akoebbe

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.

Sogl avatar Jul 26 '18 03:07 Sogl

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.

akoebbe avatar Jul 26 '18 03:07 akoebbe

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

akoebbe avatar Jul 26 '18 03:07 akoebbe

I just did some testing and ISO 9601 formatted dates work with both sorting and range limits using the dates my last example above.

akoebbe avatar Jul 26 '18 03:07 akoebbe

ISO8601 works fine if I set system conf value: image

But have 3 problems:

  1. 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.
  2. Not a beautiful date look for site editors.
  3. It's not known what problems then with such "dates" can be in future.

This is not a solution, but a hack 😄

Sogl avatar Jul 26 '18 03:07 Sogl

I'm open to hearing what other devs think about either option I mentioned above for a real solution. Perhaps there are other options?

akoebbe avatar Jul 26 '18 04:07 akoebbe

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 avatar Jul 31 '18 13:07 mahagr

@mahagr could we not tap into the blueprint to inform sorting?

akoebbe avatar Jul 31 '18 18:07 akoebbe

Blueprints are not used really outside of CRUD (admin).

mahagr avatar Jul 31 '18 20:07 mahagr

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'

evpanov avatar Oct 17 '18 15:10 evpanov

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.

drnasin avatar Nov 22 '18 18:11 drnasin

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.

mahagr avatar Nov 28 '18 06:11 mahagr

Thanks. I ended up using Sogl's solutions and his CollectionSorter class...

drnasin avatar Nov 28 '18 13:11 drnasin

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) %}

Hydraner avatar Jul 12 '19 07:07 Hydraner

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

MattAppleton avatar Nov 02 '21 18:11 MattAppleton