Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Can't use custom date header #1641

Open
Sogl opened this issue Sep 5, 2017 · 25 comments
Open

Can't use custom date header #1641

Sogl opened this issue Sep 5, 2017 · 25 comments
Labels

Comments

@Sogl
Copy link
Contributor

Sogl commented Sep 5, 2017

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

@rhukster
Copy link
Member

rhukster commented Sep 6, 2017

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.

@dimayakovlev
Copy link
Contributor

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

@Sogl
Copy link
Contributor Author

Sogl commented Sep 7, 2017

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

@rhukster
Copy link
Member

rhukster commented Sep 7, 2017

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

@dimayakovlev
Copy link
Contributor

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

@Sogl
Copy link
Contributor Author

Sogl commented Nov 14, 2017

@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
Copy link
Contributor Author

Sogl commented Jul 26, 2018

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:
#1368
#1640
#1764
#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?

@akoebbe
Copy link
Contributor

akoebbe commented Jul 26, 2018

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

@Sogl
Copy link
Contributor Author

Sogl commented Jul 26, 2018

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

@akoebbe
Copy link
Contributor

akoebbe commented Jul 26, 2018

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

@Sogl
Copy link
Contributor Author

Sogl commented Jul 26, 2018

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.

@akoebbe
Copy link
Contributor

akoebbe commented Jul 26, 2018

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
Copy link
Contributor

akoebbe commented Jul 26, 2018

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
Copy link
Contributor

akoebbe commented Jul 26, 2018

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

@Sogl
Copy link
Contributor Author

Sogl commented Jul 26, 2018

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 😄

@akoebbe
Copy link
Contributor

akoebbe commented Jul 26, 2018

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

@mahagr
Copy link
Member

mahagr commented Jul 31, 2018

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.

@akoebbe
Copy link
Contributor

akoebbe commented Jul 31, 2018

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

@mahagr
Copy link
Member

mahagr commented Jul 31, 2018

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

@evpanov
Copy link

evpanov commented Oct 17, 2018

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'

@drnasin
Copy link
Contributor

drnasin commented Nov 22, 2018

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.

@mahagr
Copy link
Member

mahagr commented Nov 28, 2018

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.

@drnasin
Copy link
Contributor

drnasin commented Nov 28, 2018

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

@Hydraner
Copy link
Contributor

Hydraner commented Jul 12, 2019

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 #1641 (comment)

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

@MattAppleton
Copy link
Contributor

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

9 participants